Redesign UI: modern SaaS dashboard styling with indigo/violet accents
This commit is contained in:
@@ -6,52 +6,179 @@ 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() {
|
||||
await fetch(`/api/resumes/${resume.id}`, {
|
||||
method: "PATCH",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ markdown: md }),
|
||||
});
|
||||
setSaved(true);
|
||||
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>
|
||||
body{font-family:Georgia,serif;max-width:800px;margin:2rem auto;padding:0 1rem;line-height:1.5;color:#222}
|
||||
h1{font-size:1.6rem;margin-bottom:0} h2{font-size:1.1rem;border-bottom:1px solid #ccc;padding-bottom:2px;margin-top:1.2rem}
|
||||
h3{font-size:1rem;margin-bottom:0} ul{margin-top:.3rem} li{margin:.15rem 0} @media print{body{margin:0}}
|
||||
@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-4">
|
||||
<div className="flex items-center justify-between flex-wrap gap-2">
|
||||
<div>
|
||||
<Link href="/resumes" className="text-sm text-zinc-500 hover:text-blue-600">← Resumes</Link>
|
||||
<h1 className="text-xl font-bold">{resume.name}</h1>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<button onClick={() => setPreview(!preview)} className="rounded border border-zinc-300 dark:border-zinc-700 px-3 py-1.5 text-sm">
|
||||
{preview ? "Hide" : "Show"} preview
|
||||
</button>
|
||||
<button onClick={printResume} className="rounded border border-zinc-300 dark:border-zinc-700 px-3 py-1.5 text-sm">🖨 Print / PDF</button>
|
||||
<button onClick={save} disabled={saved} className="rounded bg-blue-600 text-white px-4 py-1.5 text-sm disabled:opacity-50 hover:bg-blue-700">
|
||||
{saved ? "Saved" : "Save"}
|
||||
</button>
|
||||
<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-xs">
|
||||
<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 text-xs font-medium text-zinc-500 hover:text-zinc-900 dark:hover:text-zinc-100 transition-colors mb-1"
|
||||
>
|
||||
<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="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-bold 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">
|
||||
<button
|
||||
onClick={() => setPreview(!preview)}
|
||||
className="inline-flex items-center gap-1.5 px-3.5 py-2 rounded-xl text-xs font-medium 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-medium 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 to-violet-600 hover:from-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>
|
||||
<div className={`grid gap-4 ${preview ? "md:grid-cols-2" : ""}`}>
|
||||
<textarea value={md} onChange={(e) => { setMd(e.target.value); setSaved(false); }} rows={30}
|
||||
className="w-full rounded border border-zinc-300 dark:border-zinc-700 bg-white dark:bg-zinc-900 p-3 font-mono text-sm" />
|
||||
|
||||
{/* 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-xs 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-medium">
|
||||
<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-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 border border-zinc-200 dark:border-zinc-800 bg-white dark:bg-zinc-900 p-4 prose prose-sm dark:prose-invert max-w-none overflow-auto"
|
||||
dangerouslySetInnerHTML={{ __html: marked.parse(md) as string }} />
|
||||
<div className="rounded-2xl border border-zinc-200/80 dark:border-zinc-800/80 bg-white dark:bg-zinc-900 shadow-xs 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-medium">
|
||||
<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 View</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-xs border border-zinc-200/60 dark:border-zinc-800/80"
|
||||
dangerouslySetInnerHTML={{ __html: marked.parse(md) as string }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user