import React, { useEffect, useId, useState } from "react"; import CheckIcon from "@mui/icons-material/Check"; import ExpandMoreIcon from "@mui/icons-material/ExpandMore"; import { Accordion, AccordionDetails, AccordionSummary, Box, Button, FormControl, FormControlLabel, IconButton, InputAdornment, InputLabel, MenuItem, Select, Stack, Switch, TextField, Typography, } from "@mui/material"; import { applyCvTheme, CvTheme, CvVariantSettings, normalizeCvLanguage, SECTION_LABELS } from "../../cvBuilder"; import { useConfirm } from "../../confirm"; import { useI18n } from "../../i18n/I18nProvider"; import CvTemplateGallery from "../CvTemplateGallery"; const FONTS = [ "'Segoe UI', Roboto, Arial, sans-serif", "Arial, Helvetica, sans-serif", "Georgia, 'Times New Roman', serif", "'Helvetica Neue', Arial, sans-serif", "'Roboto', Arial, sans-serif", "'Poppins', 'Segoe UI', Arial, sans-serif", ]; const FONT_LABELS = ["Segoe UI", "Arial", "Georgia (serif)", "Helvetica Neue", "Roboto", "Poppins"]; const CONTROL_GRID = { display: "grid", gridTemplateColumns: { xs: "minmax(0, 1fr)", sm: "repeat(2, minmax(0, 1fr))" }, gap: 1.5 }; type Props = { mode: "template" | "customize"; settings: CvVariantSettings; update: (patch: Partial) => void; themes: CvTheme[]; sectionLabel: (key: string, fallback: string) => string; }; export default function CvCustomizeTab({ mode, settings, update, themes, sectionLabel }: Props) { const { t } = useI18n(); const { confirm } = useConfirm(); const id = useId(); const activeTheme = themes.find(theme => theme.id === settings.themeId); const supports = (setting: string) => !activeTheme?.supportedSettings?.length || activeTheme.supportedSettings.includes(setting); const sidebarSections = settings.sidebarSections ?? ["contact", "skills", "languages"]; const layout = settings.layout ?? activeTheme?.layout; const selectTemplate = async (theme: CvTheme) => { const next = applyCvTheme(settings, theme.id); const resetsOverrides = Object.entries(next).some(([key, value]) => value === null && settings[key as keyof CvVariantSettings] != null); if (resetsOverrides && !await confirm({ title: t("cvEditorChangeTemplateTitle"), message: t("cvEditorChangeTemplateMessage"), confirmLabel: t("cvEditorApplyTemplate") })) return; update(next); }; const toggleSidebarSection = (key: string) => update({ sidebarSections: sidebarSections.includes(key) ? sidebarSections.filter(item => item !== key) : [...sidebarSections, key] }); const defaultOption: [string, string] = ["", t("cvEditorTemplateDefault")]; const fonts: [string, string][] = [defaultOption, ...FONTS.map((font, index): [string, string] => [font, FONT_LABELS[index]])]; if (mode === "template") return {t("cvEditorTemplate")} {t("cvEditorThemeHelp")} ; return {t("cvEditorAppearanceHelp")} {t("cvEditorStyle")} {supports("accent") && {t("cvEditorColour")} {["#3157d5", "#0f766e", "#9f1239", "#7c3aed", "#b45309", "#334155"].map(color => update({ accentColor: color })} sx={{ width: 40, height: 40, border: "1px solid", borderColor: settings.accentColor === color ? "text.primary" : "divider" }}> {settings.accentColor === color && } )} update({ accentColor: event.target.value })} slotProps={{ inputLabel: { shrink: true }, htmlInput: { "aria-label": t("cvEditorCustomAccent"), style: { height: 28, cursor: "pointer" } } }} /> {t("cvEditorAccentHelp")} {settings.accentColor && } } {supports("typography") && <> update({ headingFont: value || null })} /> update({ bodyFont: value || null })} /> update({ baseFontSizePt: value })} /> update({ headingSizePt: value })} /> {t("cvEditorFontHelp")} } {t("cvEditorLayout")} {supports("page") && update({ pageSize })} />} {supports("spacing") && <> {t("cvEditorMarginHelp")} update({ pageMarginMm: value })} /> update({ sectionGapMm: value })} /> update({ entryGapMm: value })} /> } }> {t("cvEditorAdvancedSettings")} {t("cvEditorAdvancedHelp")} {activeTheme?.supportedSettings?.length ? {t("cvEditorTemplateCompatibility")} : null} {supports("typography") && update({ lineHeight: value })} />} {supports("heading") && update({ headingStyle: (value || null) as CvVariantSettings["headingStyle"] })} />} {supports("header") && update({ headerStyle: (value || null) as CvVariantSettings["headerStyle"] })} />} {supports("skills") && update({ skillsStyle: (value || null) as CvVariantSettings["skillsStyle"] })} />} {supports("page") && update({ density })} />} update({ language })} /> update({ dateFormat: dateFormat || null })} /> {supports("layout") && update({ layout: (value || null) as CvVariantSettings["layout"] })} />} {supports("skills") && (settings.skillsStyle === "grouped" || !!settings.skillGroups?.length) && group.name + ": " + group.items.join(", ")).join("\n")} onChange={event => update({ skillGroups: event.target.value.split("\n").filter(Boolean).map(line => { const [name, ...items] = line.split(":"); return { name: name.trim(), items: items.join(":").split(",").map(item => item.trim()).filter(Boolean) }; }) })} />} {supports("sidebar") && (layout === "sidebar-left" || layout === "sidebar-right") && {t("cvEditorSidebarContent")} update({ sidebarWidthMm: value })} /> {settings.sidebarSections == null ? <> {t("cvEditorDefaultValue")}. {t("cvEditorSidebarDefaultHelp")} : <> {["contact", "skills", "languages", "certifications", "projects", "interests"].map(key => toggleSidebarSection(key)} />} label={key === "contact" ? t("cvEditorContactDetails") : sectionLabel(key, SECTION_LABELS[key] ?? key)} />)} } } {supports("photo") && update({ showPhoto: event.target.checked })} />} label={t("cvEditorShowPhoto")} />} {supports("icons") && update({ showIcons: event.target.checked })} />} label={t("cvEditorContactIcons")} />} ; } function ControlSelect({ label, value, options, onChange }: { label: string; value: string; options: [string, string][]; onChange: (value: string) => void }) { const id = useId(); const { t } = useI18n(); return {label} ; } function ControlNumber({ label, value, min, max, step, unit = "", onChange }: { label: string; value?: number | null; min: number; max: number; step: number; unit?: string; onChange: (value: number | null) => void }) { const { t } = useI18n(); const [draft, setDraft] = useState(value?.toString() ?? ""); const [invalid, setInvalid] = useState(false); useEffect(() => { setDraft(value?.toString() ?? ""); setInvalid(false); }, [value]); return {unit} : undefined } }} onChange={event => { const input = event.target as HTMLInputElement; setDraft(input.value); const number = Number(input.value); const steps = (number - min) / step; const valid = !input.validity.badInput && (input.value === "" || (Number.isFinite(number) && number >= min && number <= max && Math.abs(steps - Math.round(steps)) < 1e-8)); setInvalid(!valid); if (valid) onChange(input.value === "" ? null : number); }} /> {value != null && } ; }