feat(cv): qualify templates and reliable local import processing
This commit is contained in:
@@ -19,11 +19,13 @@ type Props = { settings: CoverLetterDocumentSettings; disabled: boolean; onChang
|
||||
export function CoverLetterDocumentFields({ settings, disabled, onChange }: Props) {
|
||||
const { t } = useI18n();
|
||||
const [copy, setCopy] = useState<"contact" | "design" | null>(null);
|
||||
const [choosingTemplate, setChoosingTemplate] = useState(false);
|
||||
return <Stack spacing={1.5} sx={{ mb: 2 }}>
|
||||
<Typography component="h4" variant="subtitle2">{t("letterDetails")}</Typography>
|
||||
<Stack direction="row" gap={1} flexWrap="wrap">
|
||||
<Button disabled={disabled} onClick={() => setCopy("contact")}>{t("letterCopyContact")}</Button>
|
||||
<Button disabled={disabled} onClick={() => setCopy("design")}>{t("letterMatchDesign")}</Button>
|
||||
<Button disabled={disabled} onClick={() => setChoosingTemplate(true)}>{t("letterChooseTemplate")}</Button>
|
||||
</Stack>
|
||||
<TextField select size="small" label={t("letterDocumentLanguage")} value={settings.language ?? "en"} disabled={disabled}
|
||||
onChange={event => onChange({ ...settings, language: event.target.value as "en" | "nb-NO" })}>
|
||||
@@ -36,10 +38,52 @@ export function CoverLetterDocumentFields({ settings, disabled, onChange }: Prop
|
||||
onChange={event => onChange({ ...settings, [key]: event.target.value })} />)}
|
||||
</Box>
|
||||
{copy && <CopyLetterSnapshot kind={copy} settings={settings} onClose={() => setCopy(null)} onApply={next => { if (!disabled) onChange(next); setCopy(null); }} />}
|
||||
{choosingTemplate && <LetterTemplatePicker settings={settings} disabled={disabled} onClose={() => setChoosingTemplate(false)} onApply={next => { if (!disabled) onChange(next); setChoosingTemplate(false); }} />}
|
||||
</Stack>;
|
||||
}
|
||||
|
||||
type ThemeDefaults = CvTheme & { bodyFont: string; baseFontSizePt: number; lineHeight: number };
|
||||
|
||||
function LetterTemplatePicker({ settings, disabled, onClose, onApply }: Omit<Props, "onChange"> & { onClose: () => void; onApply: (settings: CoverLetterDocumentSettings) => void }) {
|
||||
const { t } = useI18n();
|
||||
const id = useId();
|
||||
const [themes, setThemes] = useState<CvTheme[]>([]);
|
||||
const [selected, setSelected] = useState(settings.themeId);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [failed, setFailed] = useState(false);
|
||||
const [attempt, setAttempt] = useState(0);
|
||||
useEffect(() => {
|
||||
let current = true;
|
||||
setLoading(true); setFailed(false);
|
||||
cvBuilderApi.themes().then(rows => {
|
||||
if (current) setThemes(rows);
|
||||
}).catch(() => { if (current) setFailed(true); }).finally(() => { if (current) setLoading(false); });
|
||||
return () => { current = false; };
|
||||
}, [attempt]);
|
||||
const theme = themes.find(row => row.id === selected);
|
||||
const ready = theme?.available && !!theme.bodyFont && !!theme.baseFontSizePt && !!theme.lineHeight && !!theme.pageMarginMm;
|
||||
const apply = () => {
|
||||
if (!ready || !theme || disabled) return;
|
||||
onApply({ ...settings, designVersion: 2, themeId: theme.id, accentColor: theme.accent,
|
||||
bodyFont: theme.bodyFont!, headingFont: theme.headingFont ?? theme.bodyFont!, baseFontSizePt: theme.baseFontSizePt!,
|
||||
lineHeight: theme.lineHeight!, pageMarginMm: theme.pageMarginMm! });
|
||||
};
|
||||
return <Dialog open onClose={onClose} fullWidth maxWidth="sm" aria-labelledby={`${id}-title`} aria-describedby={`${id}-help`}>
|
||||
<DialogTitle id={`${id}-title`}>{t("letterChooseTemplate")}</DialogTitle>
|
||||
<DialogContent dividers><Stack spacing={2}>
|
||||
<Typography id={`${id}-help`} variant="body2">{t("letterTemplateHelp")}</Typography>
|
||||
{loading && <Typography role="status">{t("loading")}</Typography>}
|
||||
{failed && <Alert severity="error" action={<Button disabled={disabled} onClick={() => setAttempt(value => value + 1)}>{t("retry")}</Button>}>{t("letterTemplateFailed")}</Alert>}
|
||||
<TextField select fullWidth size="small" label={t("letterTemplate")} disabled={loading || failed || disabled}
|
||||
value={themes.some(row => row.id === selected) ? selected : ""} onChange={event => setSelected(event.target.value)}>
|
||||
{themes.map(row => <MenuItem key={row.id} value={row.id} disabled={!row.available}>{row.name ?? row.id}{row.available ? "" : ` — ${t("cvEditorPro")}`}</MenuItem>)}
|
||||
</TextField>
|
||||
{!loading && !failed && !ready && <Typography variant="body2">{t("letterTemplateUnavailable")}</Typography>}
|
||||
</Stack></DialogContent>
|
||||
<DialogActions><Button autoFocus onClick={onClose}>{t("cancel")}</Button><Button variant="contained" disabled={disabled || loading || failed || !ready} onClick={apply}>{t("letterTemplateApply")}</Button></DialogActions>
|
||||
</Dialog>;
|
||||
}
|
||||
|
||||
function CopyLetterSnapshot({ kind, settings, onClose, onApply }: { kind: "contact" | "design"; settings: CoverLetterDocumentSettings; onClose: () => void; onApply: (settings: CoverLetterDocumentSettings) => void }) {
|
||||
const { t } = useI18n();
|
||||
const id = useId();
|
||||
@@ -75,7 +119,8 @@ function CopyLetterSnapshot({ kind, settings, onClose, onApply }: { kind: "conta
|
||||
const source = variant.settings;
|
||||
const density = source.density === "compact" ? 0.82 : source.density === "roomy" ? 1.18 : 1;
|
||||
const margin = Math.min(28, Math.max(8, Math.round((source.pageMarginMm ?? theme.pageMarginMm) * density * 100) / 100));
|
||||
return { ...settings, themeId: theme.id, accentColor: source.accentColor ?? theme.accent, bodyFont: source.bodyFont ?? theme.bodyFont,
|
||||
return { ...settings, designVersion: 2, themeId: theme.id, accentColor: source.accentColor ?? theme.accent, bodyFont: source.bodyFont ?? theme.bodyFont,
|
||||
headingFont: source.headingFont ?? theme.headingFont ?? theme.bodyFont,
|
||||
baseFontSizePt: source.baseFontSizePt ?? theme.baseFontSizePt, lineHeight: source.lineHeight ?? theme.lineHeight,
|
||||
pageMarginMm: margin, pageSize: source.pageSize ?? "a4", sourceCvId: selected };
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user