63 lines
3.6 KiB
TypeScript
63 lines
3.6 KiB
TypeScript
import React, { useEffect, useId, useRef, useState } from "react";
|
|
import { Alert, Box, Button, Dialog, DialogActions, DialogContent, DialogTitle, Skeleton, Stack, Typography } from "@mui/material";
|
|
import { cvBuilderApi, CvRender, CvTheme, getCvPageMetrics, getCvPreviewFitZoom } from "../../cvBuilder";
|
|
import { useI18n } from "../../i18n/I18nProvider";
|
|
import { getCvTemplateLabels } from "../CvTemplateGallery";
|
|
|
|
type Props = { theme: CvTheme; onClose: () => void; onApply: (theme: CvTheme) => void };
|
|
|
|
export default function CvTemplatePreviewDialog({ theme, onClose, onApply }: Props) {
|
|
const { t, language } = useI18n();
|
|
const labels = getCvTemplateLabels(theme, language, t);
|
|
const id = useId();
|
|
const host = useRef<HTMLDivElement>(null);
|
|
const [sample, setSample] = useState<CvRender | null>(null);
|
|
const [failed, setFailed] = useState(false);
|
|
const [attempt, setAttempt] = useState(0);
|
|
const [zoom, setZoom] = useState(0.5);
|
|
const metrics = getCvPageMetrics("a4");
|
|
const locked = theme.available === false;
|
|
const currentSample = sample?.themeId === theme.id ? sample : null;
|
|
|
|
useEffect(() => {
|
|
let alive = true;
|
|
setSample(null);
|
|
setFailed(false);
|
|
if (!locked) cvBuilderApi.themePreview(theme.id).then(render => {
|
|
if (alive) setSample(render);
|
|
}).catch(() => { if (alive) setFailed(true); });
|
|
return () => { alive = false; };
|
|
}, [theme.id, locked, attempt]);
|
|
|
|
useEffect(() => {
|
|
const element = host.current;
|
|
if (!element) return;
|
|
const fit = () => setZoom(getCvPreviewFitZoom(element.clientWidth, metrics.widthPx, 16, 0.2));
|
|
fit();
|
|
const observer = new ResizeObserver(fit);
|
|
observer.observe(element);
|
|
return () => observer.disconnect();
|
|
}, [metrics.widthPx, currentSample]);
|
|
|
|
return <Dialog open onClose={onClose} fullWidth maxWidth="md" aria-labelledby={`${id}-title`} aria-describedby={`${id}-help`}>
|
|
<DialogTitle id={`${id}-title`}>{t("cvEditorPreviewDesign")} · {labels.name}</DialogTitle>
|
|
<DialogContent dividers role="region" tabIndex={0} aria-label={t("cvEditorTemplateSampleTitle")}>
|
|
<Stack spacing={1.5}>
|
|
<Typography id={`${id}-help`} variant="body2" color="text.secondary">{t("cvEditorTemplateSampleHelp")}</Typography>
|
|
<Typography variant="body2">{labels.description}</Typography>
|
|
<Typography variant="body2">{t("cvEditorTemplateApplyHelp")}</Typography>
|
|
{locked ? <Alert severity="warning">{t("cvEditorPro")}</Alert> : failed ? <Alert severity="error" action={<Button onClick={() => setAttempt(value => value + 1)}>{t("cvEditorRetryPreview")}</Button>}>{t("cvEditorTemplatePreviewFailed")}</Alert> : null}
|
|
<Box ref={host} aria-busy={!currentSample && !failed && !locked} sx={{ minWidth: 0, bgcolor: "background.default", p: 1, overflow: "auto" }}>
|
|
{currentSample ? <Box sx={{ width: metrics.widthPx * zoom, height: metrics.heightPx * zoom, overflow: "hidden", mx: "auto", bgcolor: "common.white", boxShadow: 1 }}>
|
|
<iframe title={t("cvEditorTemplateSampleTitle")} srcDoc={currentSample.html} sandbox="" style={{ display: "block", border: 0, width: metrics.widthPx, height: metrics.heightPx, transform: `scale(${zoom})`, transformOrigin: "top left", background: "white" }} />
|
|
</Box> : !failed && !locked ? <Skeleton variant="rectangular" height={400} /> : null}
|
|
</Box>
|
|
</Stack>
|
|
</DialogContent>
|
|
<DialogActions sx={{ flexWrap: "wrap", px: 3, py: 2 }}>
|
|
<Button autoFocus onClick={onClose}>{t("cancel")}</Button>
|
|
<Button variant="contained" disabled={locked || !currentSample || failed} onClick={() => onApply(theme)}>{t("cvEditorApplyTemplate")}</Button>
|
|
</DialogActions>
|
|
</Dialog>;
|
|
}
|