feat(career): builder UX overhaul — DnD, rich text, per-item editing, richer preview
Phase 4.5 (priorities 1, 2, 5, 6). - Content tab now reads GET /api/cv/outline: each entry-based section expands to its entries with per-item hide, title/subtitle override, and rich-text bullet editing (RichTextField markdown toolbar). - Native HTML5 drag-and-drop reorder for sections AND entries (useDragReorder, zero deps), with drop-line + dim animations; arrow buttons remain for keyboard. - Preview: zoom presets (+/- , slider, Fit), measured page count with page navigation and dashed page-break indicators, an "updating…" chip, 300ms debounce. - Save indicator now distinguishes Unsaved / Saving / Saved / failed (aria-live). - Loading skeletons, better empty states, ATS-friendly theme badge, relative times in History; a11y: ARIA labels, keyboard-selectable theme cards, focus rings. - Pure helpers moveItem/wrapSelection extracted + unit-tested; tsc + build clean. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
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 CvSectionSetting = { key: string; hidden?: boolean; title?: string; itemOrder?: string[] };
|
||||
export type CvItemOverride = { hidden?: boolean; title?: string; subtitle?: string; bullets?: string[] };
|
||||
export type CvCustomSectionSetting = { key: string; title?: string; items: string[]; hidden?: boolean };
|
||||
|
||||
@@ -32,9 +32,15 @@ export type CvTheme = {
|
||||
accent: string;
|
||||
photoShape: string;
|
||||
supportsIcons: boolean;
|
||||
atsFriendly: boolean;
|
||||
swatches: string[];
|
||||
};
|
||||
|
||||
// Master profile resolved to sections+entries (GET /api/cv/outline) — what the Content tab edits.
|
||||
export type CvOutlineEntry = { key?: string; title?: string; subtitle?: string; meta?: string; bullets: string[]; tags: string[] };
|
||||
export type CvOutlineSection = { key: string; title: string; kind: "bullets" | "tags" | "entries"; bullets: string[]; tags: string[]; entries: CvOutlineEntry[] };
|
||||
export type CvOutline = { fullName: string; headline?: string; sections: CvOutlineSection[] };
|
||||
|
||||
export type CvVariantSummary = {
|
||||
id: number;
|
||||
name: string;
|
||||
@@ -101,8 +107,35 @@ export function emptyCvVariantSettings(themeId = "modern"): CvVariantSettings {
|
||||
}
|
||||
|
||||
// --- API ---
|
||||
// --- Pure helpers (unit-tested) ---
|
||||
|
||||
// Move an array item from one index to another, returning a new array.
|
||||
export function moveItem<T>(arr: T[], from: number, to: number): T[] {
|
||||
if (from === to || from < 0 || to < 0 || from >= arr.length || to >= arr.length) return arr;
|
||||
const next = [...arr];
|
||||
const [item] = next.splice(from, 1);
|
||||
next.splice(to, 0, item);
|
||||
return next;
|
||||
}
|
||||
|
||||
// Wrap the [start,end) selection of `text` with markdown markers, for the rich-text toolbar.
|
||||
// Returns the new text and the selection to restore. If nothing is selected, inserts a placeholder.
|
||||
export function wrapSelection(
|
||||
text: string,
|
||||
start: number,
|
||||
end: number,
|
||||
before: string,
|
||||
after: string,
|
||||
placeholder = "text",
|
||||
): { text: string; selStart: number; selEnd: number } {
|
||||
const selected = text.slice(start, end) || placeholder;
|
||||
const next = text.slice(0, start) + before + selected + after + text.slice(end);
|
||||
return { text: next, selStart: start + before.length, selEnd: start + before.length + selected.length };
|
||||
}
|
||||
|
||||
export const cvBuilderApi = {
|
||||
themes: () => api.get<CvTheme[]>("/cv/themes").then((r) => r.data),
|
||||
outline: () => api.get<CvOutline>("/cv/outline").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),
|
||||
|
||||
Reference in New Issue
Block a user