Single-container app with Postgres catalog, Typesense search, Authelia Remote-User admin, portfolio views, resume/PDF, media uploads, and ArtStation import tooling.
150 lines
4.4 KiB
TypeScript
150 lines
4.4 KiB
TypeScript
import { useEffect, useState } from "react";
|
|
import { api, type Resume } from "../../lib/api";
|
|
|
|
export function AdminResume() {
|
|
const [form, setForm] = useState({
|
|
fullName: "",
|
|
title: "",
|
|
email: "",
|
|
phone: "",
|
|
location: "",
|
|
website: "",
|
|
summary: "",
|
|
sectionsJson: "[]",
|
|
htmlContent: "",
|
|
});
|
|
const [msg, setMsg] = useState<string | null>(null);
|
|
const [err, setErr] = useState<string | null>(null);
|
|
|
|
useEffect(() => {
|
|
api.adminResume().then((r) => {
|
|
if (!r) return;
|
|
setForm({
|
|
fullName: r.fullName,
|
|
title: r.title || "",
|
|
email: r.email || "",
|
|
phone: r.phone || "",
|
|
location: r.location || "",
|
|
website: r.website || "",
|
|
summary: r.summary || "",
|
|
sectionsJson: JSON.stringify(r.sections ?? [], null, 2),
|
|
htmlContent: r.htmlContent || "",
|
|
});
|
|
});
|
|
}, []);
|
|
|
|
async function save(e: React.FormEvent) {
|
|
e.preventDefault();
|
|
setErr(null);
|
|
setMsg(null);
|
|
let sections: unknown = [];
|
|
try {
|
|
sections = JSON.parse(form.sectionsJson || "[]");
|
|
} catch {
|
|
setErr("Sections JSON is invalid");
|
|
return;
|
|
}
|
|
try {
|
|
await api.saveResume({
|
|
fullName: form.fullName,
|
|
title: form.title,
|
|
email: form.email || null,
|
|
phone: form.phone || null,
|
|
location: form.location || null,
|
|
website: form.website || null,
|
|
summary: form.summary,
|
|
sections,
|
|
htmlContent: form.htmlContent || null,
|
|
});
|
|
setMsg("Resume saved");
|
|
} catch (e) {
|
|
setErr((e as Error).message);
|
|
}
|
|
}
|
|
|
|
return (
|
|
<div className="admin-page">
|
|
<h1>Resume</h1>
|
|
<p className="admin-page__sub">
|
|
Public at <a href="/resume">/resume</a> · PDF at{" "}
|
|
<a href="/api/v1/resume/pdf">/api/v1/resume/pdf</a>
|
|
</p>
|
|
{msg && <div className="admin-msg admin-msg--ok">{msg}</div>}
|
|
{err && <div className="admin-msg admin-msg--err">{err}</div>}
|
|
|
|
<form className="admin-form admin-form--wide" onSubmit={save}>
|
|
<div className="field">
|
|
<label>Full name</label>
|
|
<input
|
|
required
|
|
value={form.fullName}
|
|
onChange={(e) => setForm({ ...form, fullName: e.target.value })}
|
|
/>
|
|
</div>
|
|
<div className="field">
|
|
<label>Title</label>
|
|
<input value={form.title} onChange={(e) => setForm({ ...form, title: e.target.value })} />
|
|
</div>
|
|
<div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: "1rem" }}>
|
|
<div className="field">
|
|
<label>Email</label>
|
|
<input
|
|
value={form.email}
|
|
onChange={(e) => setForm({ ...form, email: e.target.value })}
|
|
/>
|
|
</div>
|
|
<div className="field">
|
|
<label>Phone</label>
|
|
<input
|
|
value={form.phone}
|
|
onChange={(e) => setForm({ ...form, phone: e.target.value })}
|
|
/>
|
|
</div>
|
|
<div className="field">
|
|
<label>Location</label>
|
|
<input
|
|
value={form.location}
|
|
onChange={(e) => setForm({ ...form, location: e.target.value })}
|
|
/>
|
|
</div>
|
|
<div className="field">
|
|
<label>Website</label>
|
|
<input
|
|
value={form.website}
|
|
onChange={(e) => setForm({ ...form, website: e.target.value })}
|
|
/>
|
|
</div>
|
|
</div>
|
|
<div className="field">
|
|
<label>Summary</label>
|
|
<textarea
|
|
rows={4}
|
|
value={form.summary}
|
|
onChange={(e) => setForm({ ...form, summary: e.target.value })}
|
|
/>
|
|
</div>
|
|
<div className="field">
|
|
<label>Sections (JSON)</label>
|
|
<textarea
|
|
rows={16}
|
|
style={{ fontFamily: "ui-monospace, monospace", fontSize: 13 }}
|
|
value={form.sectionsJson}
|
|
onChange={(e) => setForm({ ...form, sectionsJson: e.target.value })}
|
|
/>
|
|
</div>
|
|
<div className="field">
|
|
<label>Optional HTML override (replaces structured layout when set)</label>
|
|
<textarea
|
|
rows={6}
|
|
value={form.htmlContent}
|
|
onChange={(e) => setForm({ ...form, htmlContent: e.target.value })}
|
|
/>
|
|
</div>
|
|
<button type="submit" className="btn btn--primary">
|
|
Save resume
|
|
</button>
|
|
</form>
|
|
</div>
|
|
);
|
|
}
|