feat(career): CV Builder UI — 3-tab builder, live preview, variants, public CV
Frontend for Phase 4. /career/builder lists CV variants; the editor has the
three spec tabs (Content / Customize / AI Tools, plus History) beside an
always-on live preview that re-renders through the server theme engine on a
debounce. Content: reorder/hide/rename sections, headline override, custom
sections. Customize: 8-theme picker, accent colour, fonts, density, page size,
photo/icons/page-number toggles. AI Tools: suggestion-only assistance (never
auto-applied). Autosave with version history + restore, public on/off with a
copyable /cv/{slug} link, PDF export. Public read-only page at /cv/:slug.
- cvBuilder.ts (types + API), CvBuilderPage, CvBuilderEditor, PublicCvPage
- routes + nav wired in App.tsx; "Open CV Builder" entry on Career Workspace
- 2 component tests; tsc + production build clean
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,121 @@
|
||||
import { api } from "./api";
|
||||
|
||||
// Mirrors the backend CvVariantSettings (the lens over the master career profile).
|
||||
export type CvSectionSetting = { key: string; hidden?: boolean; title?: string };
|
||||
export type CvItemOverride = { hidden?: boolean; title?: string; subtitle?: string; bullets?: string[] };
|
||||
export type CvCustomSectionSetting = { key: string; title?: string; items: string[]; hidden?: boolean };
|
||||
|
||||
export type CvVariantSettings = {
|
||||
themeId: string;
|
||||
accentColor?: string | null;
|
||||
headingFont?: string | null;
|
||||
bodyFont?: string | null;
|
||||
density?: string | null;
|
||||
pageSize?: string | null;
|
||||
dateFormat?: string | null;
|
||||
language?: string | null;
|
||||
headline?: string | null;
|
||||
showPhoto: boolean;
|
||||
showPageNumbers: boolean;
|
||||
showIcons: boolean;
|
||||
sections: CvSectionSetting[];
|
||||
overrides: Record<string, CvItemOverride>;
|
||||
customSections: CvCustomSectionSetting[];
|
||||
};
|
||||
|
||||
export type CvTheme = {
|
||||
id: string;
|
||||
name: string;
|
||||
category: string;
|
||||
description: string;
|
||||
layout: string;
|
||||
accent: string;
|
||||
photoShape: string;
|
||||
supportsIcons: boolean;
|
||||
swatches: string[];
|
||||
};
|
||||
|
||||
export type CvVariantSummary = {
|
||||
id: number;
|
||||
name: string;
|
||||
themeId: string;
|
||||
publicSlug: string;
|
||||
isPublic: boolean;
|
||||
version: number;
|
||||
jobApplicationId: number | null;
|
||||
updatedAtUtc: string;
|
||||
};
|
||||
|
||||
export type CvVariant = {
|
||||
id: number;
|
||||
name: string;
|
||||
settings: CvVariantSettings;
|
||||
isPublic: boolean;
|
||||
publicSlug: string;
|
||||
version: number;
|
||||
jobApplicationId: number | null;
|
||||
updatedAtUtc: string;
|
||||
};
|
||||
|
||||
export type CvVariantVersionInfo = { version: number; source: string; createdAtUtc: string; isCurrent: boolean };
|
||||
export type CvRender = { themeId: string; html: string; suggestedFileName: string };
|
||||
|
||||
export const AI_ACTIONS: { key: string; label: string }[] = [
|
||||
{ key: "improve", label: "Improve writing" },
|
||||
{ key: "professional", label: "Professional tone" },
|
||||
{ key: "shorten", label: "Shorten" },
|
||||
{ key: "expand", label: "Expand" },
|
||||
{ key: "grammar", label: "Fix grammar" },
|
||||
{ key: "ats", label: "ATS optimise" },
|
||||
{ key: "bullets", label: "Generate bullets" },
|
||||
{ key: "summary", label: "Generate summary" },
|
||||
{ key: "rewrite", label: "Rewrite" },
|
||||
{ key: "tailor", label: "Tailor to role" },
|
||||
];
|
||||
|
||||
export const DEFAULT_SECTION_ORDER = [
|
||||
"summary", "experience", "education", "projects", "skills", "certifications", "languages", "interests",
|
||||
];
|
||||
|
||||
export const SECTION_LABELS: Record<string, string> = {
|
||||
summary: "Professional Summary",
|
||||
experience: "Experience",
|
||||
education: "Education",
|
||||
projects: "Projects",
|
||||
skills: "Skills",
|
||||
certifications: "Certifications",
|
||||
languages: "Languages",
|
||||
interests: "Interests",
|
||||
};
|
||||
|
||||
export function emptyCvVariantSettings(themeId = "modern"): CvVariantSettings {
|
||||
return {
|
||||
themeId,
|
||||
showPhoto: false,
|
||||
showPageNumbers: false,
|
||||
showIcons: true,
|
||||
sections: [],
|
||||
overrides: {},
|
||||
customSections: [],
|
||||
};
|
||||
}
|
||||
|
||||
// --- API ---
|
||||
export const cvBuilderApi = {
|
||||
themes: () => api.get<CvTheme[]>("/cv/themes").then((r) => r.data),
|
||||
list: () => api.get<CvVariantSummary[]>("/cv/variants").then((r) => r.data),
|
||||
create: (body: { name?: string; jobApplicationId?: number | null; settings?: CvVariantSettings }) =>
|
||||
api.post<CvVariant>("/cv/variants", body).then((r) => r.data),
|
||||
get: (id: number) => api.get<CvVariant>(`/cv/variants/${id}`).then((r) => r.data),
|
||||
save: (id: number, body: { name?: string; settings: CvVariantSettings; source?: string }) =>
|
||||
api.put<CvVariant>(`/cv/variants/${id}`, body).then((r) => r.data),
|
||||
remove: (id: number) => api.delete(`/cv/variants/${id}`),
|
||||
duplicate: (id: number, name?: string) => api.post<CvVariant>(`/cv/variants/${id}/duplicate`, { name }).then((r) => r.data),
|
||||
setPublic: (id: number, isPublic: boolean) => api.put<CvVariant>(`/cv/variants/${id}/public`, { isPublic }).then((r) => r.data),
|
||||
versions: (id: number) => api.get<CvVariantVersionInfo[]>(`/cv/variants/${id}/versions`).then((r) => r.data),
|
||||
restore: (id: number, version: number) => api.post<CvVariant>(`/cv/variants/${id}/versions/${version}/restore`).then((r) => r.data),
|
||||
previewSettings: (settings: CvVariantSettings) => api.post<CvRender>("/cv/preview", { settings }).then((r) => r.data),
|
||||
aiAssist: (body: { action: string; text: string; role?: string; language?: string; context?: string }) =>
|
||||
api.post<{ original: string; result: string }>("/cv/ai/assist", body).then((r) => r.data),
|
||||
exportPdfUrl: (id: number) => `/cv/variants/${id}/export-pdf`,
|
||||
};
|
||||
Reference in New Issue
Block a user