fix: preserve blank CVs and reliable editor undo
This commit is contained in:
@@ -2,7 +2,7 @@ import React, { useEffect, useRef, useState } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
|
||||
import {
|
||||
Alert, Box, Button, Chip, Dialog, DialogActions, DialogContent, DialogTitle, IconButton, Menu, MenuItem, Paper, Stack, TextField, Typography,
|
||||
Alert, Box, Button, Checkbox, Chip, Dialog, DialogActions, DialogContent, DialogTitle, FormControlLabel, IconButton, Menu, MenuItem, Paper, Radio, RadioGroup, Stack, TextField, Typography,
|
||||
} from "@mui/material";
|
||||
import AddIcon from "@mui/icons-material/Add";
|
||||
import MoreVertIcon from "@mui/icons-material/MoreVert";
|
||||
@@ -10,7 +10,7 @@ import PublicIcon from "@mui/icons-material/Public";
|
||||
|
||||
import { api, getApiErrorMessage } from "../api";
|
||||
import { useToast } from "../toast";
|
||||
import { CvTheme, CvVariantSummary, cvBuilderApi, emptyCvVariantSettings } from "../cvBuilder";
|
||||
import { CvOutline, CvTheme, CvVariantSummary, cvBuilderApi, emptyCvVariantSettings, getCvExportFailureKey } from "../cvBuilder";
|
||||
import { useDialogActions } from "../dialogs";
|
||||
import CvTemplateGallery from "../components/CvTemplateGallery";
|
||||
import CvTemplateThumbnail from "../components/CvTemplateThumbnail";
|
||||
@@ -45,6 +45,20 @@ export default function CvBuilderPage() {
|
||||
const [newName, setNewName] = useState(t("cvDashboardUntitled"));
|
||||
const [newTheme, setNewTheme] = useState("modern");
|
||||
const [creating, setCreating] = useState(false);
|
||||
const [contentSource, setContentSource] = useState("blank");
|
||||
const [profileOutline, setProfileOutline] = useState<CvOutline | null>(null);
|
||||
const [profileLoading, setProfileLoading] = useState(false);
|
||||
const [profileFailed, setProfileFailed] = useState(false);
|
||||
const [selectedSections, setSelectedSections] = useState<string[]>([]);
|
||||
const [includeContact, setIncludeContact] = useState(false);
|
||||
|
||||
const loadProfile = async () => {
|
||||
setProfileLoading(true);
|
||||
setProfileFailed(false);
|
||||
try { setProfileOutline(await cvBuilderApi.outline()); }
|
||||
catch { setProfileFailed(true); }
|
||||
finally { setProfileLoading(false); }
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (pending) contentRef.current?.focus();
|
||||
@@ -77,7 +91,12 @@ export default function CvBuilderPage() {
|
||||
setCreateError(null);
|
||||
setCreating(true);
|
||||
try {
|
||||
const variant = await cvBuilderApi.create({ name: newName.trim(), settings: emptyCvVariantSettings(newTheme) });
|
||||
const variant = await cvBuilderApi.create({ name: newName.trim(), settings: {
|
||||
...emptyCvVariantSettings(newTheme),
|
||||
sectionsExplicit: true,
|
||||
includeProfileContact: contentSource === "profile" && includeContact,
|
||||
sections: contentSource === "profile" ? selectedSections.map((key) => ({ key })) : [],
|
||||
} });
|
||||
setCreateOpen(false);
|
||||
navigate(`/career/builder/${variant.id}`);
|
||||
} catch (err) {
|
||||
@@ -144,8 +163,8 @@ export default function CvBuilderPage() {
|
||||
link.click();
|
||||
URL.revokeObjectURL(url);
|
||||
} catch (err) {
|
||||
const status = (err as { response?: { status?: number } })?.response?.status;
|
||||
setActionError({ error: status === 503 || status === 504 ? undefined : err, key: status === 503 ? "cvPdfBusy" : status === 504 ? "cvPdfTimedOut" : "cvDashboardDownloadFailed" });
|
||||
const key = await getCvExportFailureKey(err);
|
||||
setActionError({ error: key ? undefined : err, key: key ?? "cvDashboardDownloadFailed" });
|
||||
} finally { finishAction(); }
|
||||
};
|
||||
|
||||
@@ -216,10 +235,27 @@ export default function CvBuilderPage() {
|
||||
<Typography id="create-cv-help" color="text.secondary" sx={{ mb: 2 }}>{t("cvDashboardCreateHelp")}</Typography>
|
||||
{createError && <Alert id="create-cv-error" severity="error" sx={{ mb: 2 }}>{getApiErrorMessage(createError.error, t("cvDashboardCreateFailed"))}</Alert>}
|
||||
<TextField autoFocus fullWidth disabled={creating} required label={t("cvEditorName")} value={newName} onChange={(event) => setNewName(event.target.value)} error={!newName.trim()} helperText={!newName.trim() ? t("cvDashboardEnterName") : t("cvDashboardNameExample")} sx={{ mb: 2, "& .MuiFormHelperText-root.Mui-disabled": { color: "text.secondary" } }} />
|
||||
<RadioGroup aria-label={t("cvDashboardContentSource")} value={contentSource} onChange={(event) => {
|
||||
setContentSource(event.target.value);
|
||||
if (event.target.value === "profile" && !profileOutline && !profileLoading) void loadProfile();
|
||||
}}>
|
||||
<FormControlLabel value="blank" control={<Radio disabled={creating} />} label={t("cvDashboardStartBlank")} />
|
||||
<FormControlLabel value="profile" control={<Radio disabled={creating} />} label={t("cvDashboardFromProfile")} />
|
||||
</RadioGroup>
|
||||
<Typography variant="body2" color="text.secondary" sx={{ mb: 2 }}>{t(contentSource === "profile" ? "cvDashboardProfileSelectionHelp" : "cvDashboardBlankHelp")}</Typography>
|
||||
{contentSource === "profile" && <Stack sx={{ mb: 2 }}>
|
||||
{profileLoading && <Typography role="status">{t("loading")}</Typography>}
|
||||
{profileFailed && <Alert severity="error" action={<Button onClick={() => void loadProfile()}>{t("retry")}</Button>}>{t("cvDashboardProfileLoadFailed")}</Alert>}
|
||||
{profileOutline && <>
|
||||
<FormControlLabel control={<Checkbox disabled={creating} checked={includeContact} onChange={(event) => setIncludeContact(event.target.checked)} />} label={t("cvEditorIncludeProfileContact")} />
|
||||
{profileOutline.sections.length === 0 && <Typography>{t("cvDashboardProfileEmpty")}</Typography>}
|
||||
{profileOutline.sections.map((section) => <FormControlLabel key={section.key} label={section.title} control={<Checkbox disabled={creating} checked={selectedSections.includes(section.key)} onChange={(event) => setSelectedSections((previous) => event.target.checked ? [...previous, section.key] : previous.filter((key) => key !== section.key))} />} />)}
|
||||
</>}
|
||||
</Stack>}
|
||||
<Typography component="h3" variant="sectionTitle" sx={{ mb: 1 }}>{t("cvDashboardChooseTemplate")}</Typography>
|
||||
<CvTemplateGallery disabled={creating} themes={themes.length ? themes : [{ id: "modern", name: t("cvDashboardDefaultTemplate"), category: t("cvEditorTemplate"), description: "", layout: "header-band", accent: "#3157d5", photoShape: "none", supportsIcons: false, atsFriendly: true, requiresPro: false, available: true, swatches: ["#3157d5", "#eef1f4", "#fff"] }]} selectedId={newTheme} onSelect={(theme) => { if (!actionBusy.current) setNewTheme(theme.id); }} previewHeight={125} showDescriptions={false} />
|
||||
</DialogContent>
|
||||
<DialogActions><Button onClick={() => setCreateOpen(false)} disabled={creating}>{t("cancel")}</Button><Button variant="contained" disabled={creating || !newName.trim()} onClick={() => void confirmCreate()}>{creating ? t("cvDashboardCreating") : t("cvDashboardCreateCv")}</Button></DialogActions>
|
||||
<DialogActions><Button onClick={() => setCreateOpen(false)} disabled={creating}>{t("cancel")}</Button><Button variant="contained" disabled={creating || !newName.trim() || (contentSource === "profile" && (!profileOutline || profileLoading))} onClick={() => void confirmCreate()}>{creating ? t("cvDashboardCreating") : t("cvDashboardCreateCv")}</Button></DialogActions>
|
||||
</Dialog>
|
||||
</Box>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user