feat(career): CV Builder UI — 3-tab builder, live preview, variants, public CV
CI and Deploy / test (push) Failing after 1m57s
CI and Deploy / deploy (push) Has been skipped

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:
cesnimda
2026-07-18 10:05:21 +02:00
parent a3e18e4b44
commit 158dd02b00
7 changed files with 883 additions and 6 deletions
+121
View File
@@ -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`,
};