187 lines
11 KiB
TypeScript
187 lines
11 KiB
TypeScript
"use client";
|
|
import { useState } from "react";
|
|
import Link from "next/link";
|
|
import { marked } from "marked";
|
|
|
|
export default function ResumeEditor({ resume }: { resume: { id: string; name: string; markdown: string } }) {
|
|
const [md, setMd] = useState(resume.markdown);
|
|
const [saved, setSaved] = useState(true);
|
|
const [saving, setSaving] = useState(false);
|
|
const [preview, setPreview] = useState(true);
|
|
|
|
async function save() {
|
|
setSaving(true);
|
|
try {
|
|
await fetch(`/api/resumes/${resume.id}`, {
|
|
method: "PATCH",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({ markdown: md }),
|
|
});
|
|
setSaved(true);
|
|
} finally {
|
|
setSaving(false);
|
|
}
|
|
}
|
|
|
|
function printResume() {
|
|
const w = window.open("", "_blank");
|
|
if (!w) return;
|
|
w.document.write(`<!DOCTYPE html><html><head><title>${resume.name}</title><style>
|
|
@page { margin: 1.5cm; }
|
|
body {
|
|
font-family: 'Times New Roman', Times, Georgia, serif;
|
|
max-width: 800px;
|
|
margin: 2rem auto;
|
|
padding: 0 1.5rem;
|
|
line-height: 1.5;
|
|
color: #111827;
|
|
}
|
|
h1 { font-size: 1.8rem; margin-bottom: 0.2rem; font-weight: bold; border-bottom: 1.5px solid #374151; padding-bottom: 4px; }
|
|
h2 { font-size: 1.15rem; border-bottom: 1px solid #9ca3af; padding-bottom: 2px; margin-top: 1.2rem; margin-bottom: 0.4rem; text-transform: uppercase; letter-spacing: 0.05em; font-family: Arial, sans-serif; }
|
|
h3 { font-size: 1rem; margin-bottom: 0.1rem; margin-top: 0.6rem; }
|
|
p { margin-top: 0.2rem; margin-bottom: 0.4rem; font-size: 0.95rem; }
|
|
ul { margin-top: 0.2rem; padding-left: 1.2rem; margin-bottom: 0.6rem; }
|
|
li { margin: 0.15rem 0; font-size: 0.95rem; }
|
|
a { color: #111827; text-decoration: none; }
|
|
@media print { body { margin: 0; padding: 0; } }
|
|
</style></head><body>${marked.parse(md)}</body></html>`);
|
|
w.document.close();
|
|
setTimeout(() => w.print(), 300);
|
|
}
|
|
|
|
return (
|
|
<div className="space-y-6">
|
|
{/* Top Navigation & Toolbar */}
|
|
<div className="rounded-2xl border border-zinc-200/80 dark:border-zinc-800/80 bg-white dark:bg-zinc-900 p-4 shadow-2xs">
|
|
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
|
|
<div>
|
|
<Link
|
|
href="/resumes"
|
|
className="inline-flex items-center gap-1.5 text-xs font-semibold text-zinc-500 hover:text-zinc-900 dark:hover:text-zinc-100 transition-colors mb-1 group"
|
|
>
|
|
<svg className="w-3.5 h-3.5 group-hover:-translate-x-0.5 transition-transform" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
|
<path strokeLinecap="round" strokeLinejoin="round" d="M10.5 19.5L3 12m0 0l7.5-7.5M3 12h18" />
|
|
</svg>
|
|
<span>Back to Resumes</span>
|
|
</Link>
|
|
<div className="flex items-center gap-3">
|
|
<h1 className="text-xl font-extrabold tracking-tight text-zinc-900 dark:text-white">
|
|
{resume.name}
|
|
</h1>
|
|
{saved ? (
|
|
<span className="inline-flex items-center gap-1.5 px-2.5 py-0.5 rounded-full text-xs font-medium bg-emerald-50 text-emerald-700 border border-emerald-200 dark:bg-emerald-950/50 dark:text-emerald-300 dark:border-emerald-800/60">
|
|
<span className="w-1.5 h-1.5 rounded-full bg-emerald-500" />
|
|
Saved
|
|
</span>
|
|
) : (
|
|
<span className="inline-flex items-center gap-1.5 px-2.5 py-0.5 rounded-full text-xs font-medium bg-amber-50 text-amber-700 border border-amber-200 dark:bg-amber-950/50 dark:text-amber-300 dark:border-amber-800/60 animate-pulse">
|
|
<span className="w-1.5 h-1.5 rounded-full bg-amber-500" />
|
|
Unsaved changes
|
|
</span>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Action Toolbar Buttons */}
|
|
<div className="flex items-center gap-2 shrink-0">
|
|
<button
|
|
onClick={() => setPreview(!preview)}
|
|
className="inline-flex items-center gap-1.5 px-3.5 py-2 rounded-xl text-xs font-semibold text-zinc-700 dark:text-zinc-300 bg-zinc-50 dark:bg-zinc-800 border border-zinc-200 dark:border-zinc-700 hover:bg-zinc-100 dark:hover:bg-zinc-700 transition-colors"
|
|
>
|
|
<svg className="w-4 h-4 text-zinc-500" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
|
<path strokeLinecap="round" strokeLinejoin="round" d="M2.036 12.322a1.012 1.012 0 010-.639C3.423 7.51 7.36 4.5 12 4.5c4.638 0 8.573 3.007 9.963 7.178.07.207.07.431 0 .639C20.577 16.49 16.64 19.5 12 19.5c-4.638 0-8.573-3.007-9.963-7.178z" />
|
|
<path strokeLinecap="round" strokeLinejoin="round" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
|
|
</svg>
|
|
<span>{preview ? "Hide Preview" : "Show Preview"}</span>
|
|
</button>
|
|
|
|
<button
|
|
onClick={printResume}
|
|
className="inline-flex items-center gap-1.5 px-3.5 py-2 rounded-xl text-xs font-semibold text-zinc-700 dark:text-zinc-300 bg-zinc-50 dark:bg-zinc-800 border border-zinc-200 dark:border-zinc-700 hover:bg-zinc-100 dark:hover:bg-zinc-700 transition-colors"
|
|
>
|
|
<svg className="w-4 h-4 text-zinc-500" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
|
<path strokeLinecap="round" strokeLinejoin="round" d="M6.72 13.829c-.24.03-.48.062-.72.096m.72-.096a42.415 42.415 0 0110.56 0m-10.56 0L6.34 18m10.94-4.171c.24.03.48.062.72.096m-.72-.096L17.66 18m0 0l.229 2.523a1.125 1.125 0 01-1.12 1.227H7.231c-.662 0-1.18-.568-1.12-1.227L6.34 18m11.318 0h1.091A2.25 2.25 0 0021 15.75V9.456c0-1.081-.768-2.015-1.837-2.175a48.055 48.055 0 00-1.913-.247M6.34 18H5.25A2.25 2.25 0 013 15.75V9.456c0-1.081.768-2.015 1.837-2.175a48.041 48.041 0 011.913-.247m10.5 0a48.536 48.536 0 00-10.5 0m10.5 0V3.375c0-.621-.504-1.125-1.125-1.125h-8.25c-.621 0-1.125.504-1.125 1.125v3.659M18 10.5h.008v.008H18V10.5zm-3 0h.008v.008H15V10.5z" />
|
|
</svg>
|
|
<span>Print / PDF</span>
|
|
</button>
|
|
|
|
<button
|
|
onClick={save}
|
|
disabled={saved || saving}
|
|
className={`inline-flex items-center gap-1.5 px-4 py-2 rounded-xl text-xs font-semibold text-white transition-all shadow-sm ${
|
|
saved
|
|
? "bg-zinc-400 dark:bg-zinc-700 opacity-60 cursor-default"
|
|
: "bg-gradient-to-r from-indigo-600 via-indigo-600 to-violet-600 hover:from-indigo-500 hover:via-indigo-500 hover:to-violet-500 shadow-indigo-500/20 active:scale-95 cursor-pointer"
|
|
}`}
|
|
>
|
|
{saving ? (
|
|
<>
|
|
<svg className="animate-spin w-3.5 h-3.5 text-white" fill="none" viewBox="0 0 24 24">
|
|
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
|
|
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z" />
|
|
</svg>
|
|
<span>Saving…</span>
|
|
</>
|
|
) : (
|
|
<>
|
|
<svg className="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
|
<path strokeLinecap="round" strokeLinejoin="round" d="M4.5 12.75l6 6 9-13.5" />
|
|
</svg>
|
|
<span>{saved ? "Saved" : "Save Changes"}</span>
|
|
</>
|
|
)}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Editor & Preview Split Grid */}
|
|
<div className={`grid gap-6 ${preview ? "lg:grid-cols-2" : "grid-cols-1"}`}>
|
|
{/* Monospace Markdown Editor */}
|
|
<div className="rounded-2xl border border-zinc-200/80 dark:border-zinc-800/80 bg-white dark:bg-zinc-900 shadow-2xs overflow-hidden flex flex-col">
|
|
<div className="px-4 py-2.5 bg-zinc-50 dark:bg-zinc-900/60 border-b border-zinc-200 dark:border-zinc-800 flex items-center justify-between text-xs text-zinc-500 dark:text-zinc-400 font-semibold">
|
|
<span className="flex items-center gap-1.5">
|
|
<svg className="w-4 h-4 text-indigo-500" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
|
<path strokeLinecap="round" strokeLinejoin="round" d="M17.25 6.75L22.5 12l-5.25 5.25m-10.5 0L1.5 12l5.25-5.25m7.5-3l-4.5 16.5" />
|
|
</svg>
|
|
Markdown Source
|
|
</span>
|
|
<span>{md.length} characters</span>
|
|
</div>
|
|
<textarea
|
|
value={md}
|
|
onChange={(e) => {
|
|
setMd(e.target.value);
|
|
setSaved(false);
|
|
}}
|
|
rows={30}
|
|
className="w-full flex-1 p-4 font-mono text-xs sm:text-sm leading-relaxed bg-white dark:bg-zinc-950/60 text-zinc-900 dark:text-zinc-100 focus:outline-hidden resize-y min-h-[500px]"
|
|
placeholder="# Your Name Email | Phone | Location ## Summary ..."
|
|
/>
|
|
</div>
|
|
|
|
{/* Live Resume Document Preview */}
|
|
{preview && (
|
|
<div className="rounded-2xl border border-zinc-200/80 dark:border-zinc-800/80 bg-white dark:bg-zinc-900 shadow-2xs overflow-hidden flex flex-col">
|
|
<div className="px-4 py-2.5 bg-zinc-50 dark:bg-zinc-900/60 border-b border-zinc-200 dark:border-zinc-800 flex items-center justify-between text-xs text-zinc-500 dark:text-zinc-400 font-semibold">
|
|
<span className="flex items-center gap-1.5">
|
|
<svg className="w-4 h-4 text-purple-500" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
|
<path strokeLinecap="round" strokeLinejoin="round" d="M19.5 14.25v-2.625a3.375 3.375 0 00-3.375-3.375h-1.5A1.125 1.125 0 0113.5 7.125v-1.5a3.375 3.375 0 00-3.375-3.375H8.25m0 12.75h7.5m-7.5 3H12M10.5 2.25H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 00-9-9z" />
|
|
</svg>
|
|
Document Live Preview
|
|
</span>
|
|
<span>Formatted Output</span>
|
|
</div>
|
|
<div className="p-6 md:p-8 bg-zinc-50/40 dark:bg-zinc-950/40 flex-1 overflow-auto max-h-[850px]">
|
|
<div
|
|
className="resume-preview max-w-2xl mx-auto bg-white dark:bg-zinc-900 p-8 rounded-xl shadow-2xs border border-zinc-200/60 dark:border-zinc-800/80"
|
|
dangerouslySetInnerHTML={{ __html: marked.parse(md) as string }}
|
|
/>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|