feat(career): builder UX overhaul — DnD, rich text, per-item editing, richer preview
CI and Deploy / test (push) Failing after 1m56s
CI and Deploy / deploy (push) Has been skipped

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:
cesnimda
2026-07-18 14:46:37 +02:00
parent e3b255f226
commit 582c4e07e9
5 changed files with 498 additions and 99 deletions
+34 -1
View File
@@ -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),