@xiwebdesign/xicms-core 7.19.1 → 7.22.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/admin/settings/SettingsPanel.js +8 -1
- package/dist/admin/settings/SettingsPanel.js.map +1 -1
- package/dist/admin/shop/OrdersTab.js +21 -6
- package/dist/admin/shop/OrdersTab.js.map +1 -1
- package/dist/admin/shop/ProductsTab.d.ts +0 -6
- package/dist/admin/shop/ProductsTab.js +36 -1
- package/dist/admin/shop/ProductsTab.js.map +1 -1
- package/dist/admin/shop/ShopSettingsTab.js +57 -2
- package/dist/admin/shop/ShopSettingsTab.js.map +1 -1
- package/dist/admin.css +1 -1
- package/dist/modules/shop/blocks/Cart.js +8 -2
- package/dist/modules/shop/blocks/Cart.js.map +1 -1
- package/dist/modules/shop/blocks/Checkout.js +32 -8
- package/dist/modules/shop/blocks/Checkout.js.map +1 -1
- package/dist/modules/shop/blocks/ProductGrid.js +5 -1
- package/dist/modules/shop/blocks/ProductGrid.js.map +1 -1
- package/dist/modules/shop/settings.d.ts +8 -0
- package/dist/modules/shop/settings.js +21 -1
- package/dist/modules/shop/settings.js.map +1 -1
- package/dist/modules/shop/strings.d.ts +2 -0
- package/dist/modules/shop/strings.js +4 -0
- package/dist/modules/shop/strings.js.map +1 -1
- package/dist/schema/shop.d.ts +7 -0
- package/dist/schema/shop.js +17 -1
- package/dist/schema/shop.js.map +1 -1
- package/dist/schema/shopGeld.d.ts +30 -3
- package/dist/schema/shopGeld.js +19 -4
- package/dist/schema/shopGeld.js.map +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
|
@@ -190,7 +190,14 @@ function SettingsPanel({ siteId }) {
|
|
|
190
190
|
}
|
|
191
191
|
) }),
|
|
192
192
|
/* @__PURE__ */ jsxs("div", { className: "grid gap-3 md:grid-cols-2", children: [
|
|
193
|
-
/* @__PURE__ */ jsx(
|
|
193
|
+
/* @__PURE__ */ jsx(
|
|
194
|
+
Field,
|
|
195
|
+
{
|
|
196
|
+
label: "Primaire kleur",
|
|
197
|
+
hint: "Wordt ook gebruikt in je e-mails en op de factuur-PDF",
|
|
198
|
+
children: /* @__PURE__ */ jsx(Input, { value: theme.primary, onChange: (e) => setS({ ...s, theme: { ...theme, primary: e.target.value } }) })
|
|
199
|
+
}
|
|
200
|
+
),
|
|
194
201
|
/* @__PURE__ */ jsx(Field, { label: "Accentkleur", children: /* @__PURE__ */ jsx(Input, { value: theme.accent ?? "", onChange: (e) => setS({ ...s, theme: { ...theme, accent: e.target.value } }) }) }),
|
|
195
202
|
/* @__PURE__ */ jsx(Field, { label: "Achtergrondkleur", children: /* @__PURE__ */ jsx(Input, { value: theme.background ?? "", onChange: (e) => setS({ ...s, theme: { ...theme, background: e.target.value } }), placeholder: "#ffffff" }) }),
|
|
196
203
|
/* @__PURE__ */ jsx(Field, { label: "Tekstkleur", children: /* @__PURE__ */ jsx(Input, { value: theme.foreground ?? "", onChange: (e) => setS({ ...s, theme: { ...theme, foreground: e.target.value } }), placeholder: "#0f172a" }) })
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/admin/settings/SettingsPanel.tsx"],"sourcesContent":["\"use client\";\n\nimport { useEffect, useRef, useState } from \"react\";\nimport { doc, onSnapshot } from \"firebase/firestore\";\nimport { callable } from \"../../firebase/functionsClient\";\nimport { getDb, getFirebaseApp } from \"../../firebase/client\";\nimport { SiteSettingsSchema, type SiteSettings } from \"../../schema/settings\";\nimport { useModules } from \"../../modules/context\";\nimport { useSiteConfig } from \"../../config\";\nimport type { ModulesSiteConfig } from \"../../modules/types\";\nimport { builtinThemePresets } from \"../../theme/presets\";\nimport { Button, Card, Field, Input } from \"../ui/primitives\";\nimport { Select } from \"../ui/select\";\nimport { Switch } from \"../ui/switch\";\nimport { ArrayEditor } from \"../ui/ArrayEditor\";\nimport { useToast } from \"../ui/toast\";\nimport { useConfirm } from \"../ui/ConfirmDialog\";\nimport { ModuleSettingsForm } from \"./moduleSettingsForm\";\nimport { SnapshotsCard } from \"./SnapshotsCard\";\nimport { MailSettings } from \"./MailSettings\";\n\nexport function SettingsPanel({ siteId }: { siteId: string }) {\n const { toast } = useToast();\n const confirm = useConfirm();\n const { defs } = useModules();\n const cfg = useSiteConfig();\n const [s, setS] = useState<SiteSettings>({});\n const [loaded, setLoaded] = useState(false);\n const [exporting, setExporting] = useState(false);\n const [restoring, setRestoring] = useState(false);\n const restoreInput = useRef<HTMLInputElement>(null);\n\n async function downloadBackup(silent = false): Promise<boolean> {\n const app = getFirebaseApp();\n if (!app) return false;\n setExporting(true);\n try {\n const fn = callable<{ siteId: string }, Record<string, unknown>>(app, \"exportSite\");\n const res = await fn({ siteId });\n const blob = new Blob([JSON.stringify(res.data, null, 2)], { type: \"application/json\" });\n const url = URL.createObjectURL(blob);\n const a = document.createElement(\"a\");\n a.href = url;\n a.download = `xicms-backup-${siteId}-${new Date().toISOString().slice(0, 10)}.json`;\n a.click();\n URL.revokeObjectURL(url);\n if (!silent) toast({ title: \"Back-up gedownload\", tone: \"success\" });\n return true;\n } catch (e) {\n toast({ title: \"Back-up mislukt\", description: (e as Error).message, tone: \"error\" });\n return false;\n } finally {\n setExporting(false);\n }\n }\n\n async function restoreFromFile(file: File) {\n let backup: Record<string, unknown>;\n try {\n backup = JSON.parse(await file.text()) as Record<string, unknown>;\n } catch {\n toast({ title: \"Ongeldig bestand\", description: \"Dit is geen geldig back-upbestand.\", tone: \"error\" });\n return;\n }\n if (backup._format !== \"xicms-export\") {\n toast({ title: \"Ongeldig bestand\", description: \"Dit lijkt geen XiCMS-back-up.\", tone: \"error\" });\n return;\n }\n if (backup.siteId && backup.siteId !== siteId) {\n const ok = await confirm({\n title: \"Back-up van een andere site\",\n description: `Deze back-up is van “${String(backup.siteId)}”, niet van deze site (${siteId}). Toch terugzetten?`,\n confirmLabel: \"Toch terugzetten\",\n tone: \"danger\",\n });\n if (!ok) return;\n }\n const ok = await confirm({\n title: \"Back-up terugzetten?\",\n description: `Dit VERVANGT alle huidige pagina's, nieuws, media en instellingen door de inhoud van de back-up${backup.exportedAt ? ` van ${new Date(String(backup.exportedAt)).toLocaleString(\"nl-NL\")}` : \"\"}. We maken eerst automatisch een back-up van de huidige stand.`,\n confirmLabel: \"Terugzetten\",\n tone: \"danger\",\n });\n if (!ok) return;\n setRestoring(true);\n try {\n // Veiligheid: eerst de huidige stand wegschrijven (als download) vóór we overschrijven.\n // Lukt die back-up niet, dan NIET doorgaan met de destructieve restore (anders ben je de\n // huidige stand kwijt zonder vangnet). De gebruiker kan het daarna opnieuw proberen.\n if (!(await downloadBackup(true))) {\n toast({ title: \"Terugzetten afgebroken\", description: \"De veiligheidsback-up van de huidige stand mislukte; er is niets overschreven.\", tone: \"error\" });\n return;\n }\n const app = getFirebaseApp();\n if (!app) throw new Error(\"Firebase niet geconfigureerd.\");\n const fn = callable(app, \"restoreSite\");\n await fn({ siteId, backup });\n toast({ title: \"Back-up teruggezet\", description: \"Herlaad de pagina om alles bij te werken.\", tone: \"success\" });\n } catch (e) {\n toast({ title: \"Terugzetten mislukt\", description: (e as Error).message, tone: \"error\" });\n } finally {\n setRestoring(false);\n }\n }\n\n /**\n * Instellingen was één scroll van negen kaarten — van sitenaam tot back-up, alles onder elkaar.\n * Dat leest niet als \"waar stel ik mijn huisstijl in?\" maar als \"zoek het maar\". De kaarten zijn\n * nu per onderwerp gegroepeerd; je ziet er één tegelijk. De staat zit in `s` hierboven, dus\n * wisselen van sectie raakt niets kwijt en één \"Opslaan\" bewaart nog steeds het geheel.\n */\n const [sectie, setSectie] = useState<string>(\"algemeen\");\n\n useEffect(() => {\n const db = getDb();\n if (!db) {\n setLoaded(true);\n return;\n }\n return onSnapshot(doc(db, \"sites\", siteId, \"settings\", \"site\"), (snap) => {\n const uitDb = snap.exists() ? (snap.data() as SiteSettings) : {};\n // Sitenaam, domein en contact-e-mail stonden hier leeg, en dat las als \"niet ingevuld\".\n // Ze wáren ingevuld, alleen ergens anders: in `content/site.config.ts`, waar ze bij het\n // bouwen van de site zijn gezet. Het instellingen-document kent ze pas zodra iemand hier\n // op Opslaan drukt, en dat had nog nooit iemand gedaan.\n //\n // Terugvallen op de site-config maakt het eerlijk: je ziet de waarde die de site\n // daadwerkelijk gebruikt, en kunt hem van daaruit aanpassen. Wat in het document staat wint\n // altijd — anders zou een bewust leeggemaakt veld telkens terugkomen.\n setS({\n ...uitDb,\n name: uitDb.name ?? cfg.name,\n domain: uitDb.domain ?? cfg.domain,\n contactEmail: uitDb.contactEmail ?? cfg.contactEmail,\n });\n setLoaded(true);\n });\n }, [siteId, cfg.name, cfg.domain, cfg.contactEmail]);\n\n async function save() {\n const db = getDb();\n if (!db) return;\n const parsed = SiteSettingsSchema.safeParse(s);\n if (!parsed.success) {\n toast({ title: \"Ongeldige instellingen\", description: parsed.error.issues[0]?.message, tone: \"error\" });\n return;\n }\n // Via de callable i.p.v. rechtstreeks: die valideert server-side, leidt de publieke subset\n // zelf af (zodat `site` en `public` niet uit elkaar kunnen lopen) én duwt de cache-tag door,\n // waardoor de wijziging binnen seconden live staat i.p.v. na het ISR-vangnet.\n try {\n const app = getFirebaseApp();\n if (!app) throw new Error(\"Firebase is niet geconfigureerd.\");\n const fn = callable(app, \"saveSiteSettings\");\n const res = await fn({ siteId, settings: parsed.data });\n toast({ title: \"Opgeslagen\", description: (res.data as { message?: string })?.message ?? \"Wijzigingen zijn live.\", tone: \"success\" });\n } catch (e) {\n toast({ title: \"Opslaan mislukt\", description: (e as Error).message, tone: \"error\" });\n }\n }\n\n if (!loaded) return <p className=\"text-sm text-slate-500\">Laden…</p>;\n\n const forms = s.forms ?? { mode: \"xicms\" as const };\n const theme = s.theme ?? { primary: \"#4f46e5\" };\n // Zelfde terugval als XiCMSProvider: `mode` wint, anders de oude boolean.\n const donkerModus: \"light\" | \"dark\" | \"auto\" = theme.dark?.mode ?? (theme.dark?.auto ? \"auto\" : \"light\");\n const footer = s.footer ?? {};\n\n // Hulpfuncties voor de module-instellingen.\n const moduleState = (id: string, defaultEnabled: boolean) => {\n const raw = (s.modules as ModulesSiteConfig | undefined)?.[id];\n if (typeof raw === \"boolean\") return { enabled: raw, config: {} as Record<string, unknown> };\n return { enabled: raw?.enabled ?? defaultEnabled, config: raw?.config ?? {} };\n };\n const setModule = (id: string, next: { enabled: boolean; config: Record<string, unknown> }) =>\n setS({ ...s, modules: { ...(s.modules as ModulesSiteConfig | undefined), [id]: next } });\n\n const SECTIES: { sleutel: string; label: string }[] = [\n { sleutel: \"algemeen\", label: \"Algemeen\" },\n { sleutel: \"huisstijl\", label: \"Huisstijl\" },\n { sleutel: \"seo\", label: \"SEO\" },\n { sleutel: \"footer\", label: \"Footer\" },\n { sleutel: \"modules\", label: \"Modules\" },\n { sleutel: \"mail\", label: \"E-mail\" },\n { sleutel: \"backup\", label: \"Back-up\" },\n ];\n\n return (\n /* Een formulier leest niet op 1300px breed; een invoerveld hoort niet breder te zijn dan de\n tekst die erin komt. Vandaar een leesbreedte in plaats van het hele werkblad vullen. */\n <div className=\"grid max-w-4xl gap-4\">\n <nav aria-label=\"Onderdelen van de instellingen\" className=\"flex flex-wrap gap-1\">\n {SECTIES.map((x) => (\n <button\n key={x.sleutel}\n type=\"button\"\n aria-current={sectie === x.sleutel ? \"page\" : undefined}\n onClick={() => setSectie(x.sleutel)}\n className=\"rounded-lg px-3 py-1.5 text-sm font-medium text-slate-600 hover:bg-slate-200 hover:text-slate-900 focus-visible:ring-2 focus-visible:ring-brand-500 focus-visible:outline-none aria-[current=page]:bg-slate-200 aria-[current=page]:text-slate-900\"\n >\n {x.label}\n </button>\n ))}\n </nav>\n\n {sectie === \"algemeen\" && (\n <>\n <Card>\n <h3 className=\"mb-3 text-base font-semibold\">Algemeen</h3>\n <div className=\"grid gap-3 md:grid-cols-2\">\n <Field label=\"Sitenaam\"><Input value={s.name ?? \"\"} onChange={(e) => setS({ ...s, name: e.target.value })} /></Field>\n <Field label=\"Domein\"><Input value={s.domain ?? \"\"} onChange={(e) => setS({ ...s, domain: e.target.value })} /></Field>\n <Field label=\"Contact-e-mail\"><Input value={s.contactEmail ?? \"\"} onChange={(e) => setS({ ...s, contactEmail: e.target.value })} /></Field>\n </div>\n </Card>\n </>\n )}\n\n {sectie === \"huisstijl\" && (\n <>\n <Card>\n <h3 className=\"mb-3 text-base font-semibold\">Huisstijl</h3>\n <div className=\"grid gap-3\">\n <Field label=\"Thema-preset\" hint=\"Vult kleuren, lettertype en afronding in één keer in.\">\n <Select\n value=\"\"\n placeholder=\"Kies een preset…\"\n onValueChange={(id) => {\n const preset = builtinThemePresets.find((p) => p.id === id);\n if (preset) setS({ ...s, theme: { ...theme, ...preset.theme } });\n }}\n options={builtinThemePresets.map((p) => ({ value: p.id, label: p.label }))}\n />\n </Field>\n <div className=\"grid gap-3 md:grid-cols-2\">\n <Field label=\"Primaire kleur\"><Input value={theme.primary} onChange={(e) => setS({ ...s, theme: { ...theme, primary: e.target.value } })} /></Field>\n <Field label=\"Accentkleur\"><Input value={theme.accent ?? \"\"} onChange={(e) => setS({ ...s, theme: { ...theme, accent: e.target.value } })} /></Field>\n <Field label=\"Achtergrondkleur\"><Input value={theme.background ?? \"\"} onChange={(e) => setS({ ...s, theme: { ...theme, background: e.target.value } })} placeholder=\"#ffffff\" /></Field>\n <Field label=\"Tekstkleur\"><Input value={theme.foreground ?? \"\"} onChange={(e) => setS({ ...s, theme: { ...theme, foreground: e.target.value } })} placeholder=\"#0f172a\" /></Field>\n </div>\n {/* Donkere modus. Het schema en de rendering ondersteunden dit al, maar er was geen knop:\n donker aanzetten kon alleen door site.config.ts te bewerken — dus niet door de klant\n zelf. De kleuren verschijnen pas als ze ergens toe doen; bij \"altijd licht\" zou een\n donkere achtergrondkleur alleen verwarren. */}\n <Field\n label=\"Donkere modus\"\n hint={\n donkerModus === \"auto\"\n ? \"Volgt de voorkeur van de bezoeker: wie zijn telefoon of computer op donker heeft staan, ziet de site donker.\"\n : donkerModus === \"dark\"\n ? \"De site is voor iedereen donker, ongeacht de voorkeur van de bezoeker.\"\n : \"De site is voor iedereen licht.\"\n }\n >\n <Select\n value={donkerModus}\n onValueChange={(v) =>\n setS({ ...s, theme: { ...theme, dark: { ...(theme.dark ?? {}), mode: v as \"light\" | \"dark\" | \"auto\" } } })\n }\n options={[\n { value: \"light\", label: \"Altijd licht\" },\n { value: \"dark\", label: \"Altijd donker\" },\n { value: \"auto\", label: \"Automatisch — volg de bezoeker\" },\n ]}\n />\n </Field>\n {donkerModus !== \"light\" && (\n <div className=\"grid gap-3 sm:grid-cols-3\">\n <Field label=\"Donkere achtergrond\">\n <Input\n value={theme.dark?.background ?? \"\"}\n onChange={(e) => setS({ ...s, theme: { ...theme, dark: { ...(theme.dark ?? {}), background: e.target.value } } })}\n placeholder=\"#0f172a\"\n />\n </Field>\n <Field label=\"Donkere tekstkleur\">\n <Input\n value={theme.dark?.foreground ?? \"\"}\n onChange={(e) => setS({ ...s, theme: { ...theme, dark: { ...(theme.dark ?? {}), foreground: e.target.value } } })}\n placeholder=\"#e2e8f0\"\n />\n </Field>\n {/* Een merkkleur die op wit werkt is op zwart vaak te donker; daarom een eigen waarde. */}\n <Field label=\"Donkere merkkleur\" hint=\"Laat leeg om dezelfde te gebruiken.\">\n <Input\n value={theme.dark?.primary ?? \"\"}\n onChange={(e) => setS({ ...s, theme: { ...theme, dark: { ...(theme.dark ?? {}), primary: e.target.value } } })}\n placeholder={theme.primary}\n />\n </Field>\n </div>\n )}\n <Field label=\"Logo-URL (e-mail & factuur)\" hint=\"Absolute https-URL naar een PNG/JPG. Verschijnt in de mail-header en op de factuur-PDF.\">\n <Input value={theme.logo ?? \"\"} onChange={(e) => setS({ ...s, theme: { ...theme, logo: e.target.value } })} placeholder=\"https://…/logo.png\" />\n </Field>\n </div>\n </Card>\n </>\n )}\n\n {sectie === \"seo\" && (\n <>\n <Card>\n <h3 className=\"mb-3 text-base font-semibold\">SEO (standaard)</h3>\n <div className=\"grid gap-3\">\n <Field label=\"Standaard-omschrijving\">\n <Input value={s.seoDefaults?.description ?? \"\"} onChange={(e) => setS({ ...s, seoDefaults: { ...s.seoDefaults, description: e.target.value } })} />\n </Field>\n <Field label=\"OG-afbeelding (URL)\">\n <Input value={s.seoDefaults?.ogImage ?? \"\"} onChange={(e) => setS({ ...s, seoDefaults: { ...s.seoDefaults, ogImage: e.target.value } })} />\n </Field>\n </div>\n </Card>\n </>\n )}\n\n {sectie === \"footer\" && (\n <>\n <Card>\n <h3 className=\"mb-1 text-base font-semibold\">Footer</h3>\n <p className=\"mb-3 text-xs text-slate-500\">Kolommen met links, social-links en copyright. Wijzigingen zijn direct live.</p>\n <div className=\"grid gap-4\">\n <div>\n <span className=\"text-sm font-medium\">Kolommen</span>\n <ArrayEditor\n items={footer.columns ?? []}\n onChange={(columns) => setS({ ...s, footer: { ...footer, columns } })}\n newItem={() => ({ heading: \"\", links: [] })}\n addLabel=\"+ Kolom\"\n emptyLabel=\"Nog geen kolommen.\"\n renderRow={(col, update) => (\n <div className=\"grid gap-2\">\n <Field label=\"Kop\"><Input value={col.heading ?? \"\"} onChange={(e) => update({ heading: e.target.value })} /></Field>\n <ArrayEditor\n items={col.links ?? []}\n onChange={(links) => update({ links })}\n newItem={() => ({ label: \"\", href: \"\" })}\n addLabel=\"+ Link\"\n renderRow={(link, updL) => (\n <div className=\"grid grid-cols-2 gap-2\">\n <Input value={link.label} placeholder=\"Label\" onChange={(e) => updL({ label: e.target.value })} />\n <Input value={link.href} placeholder=\"/pad of https://…\" onChange={(e) => updL({ href: e.target.value })} />\n </div>\n )}\n />\n </div>\n )}\n />\n </div>\n <div>\n <span className=\"text-sm font-medium\">Social-links</span>\n <ArrayEditor\n items={footer.social ?? []}\n onChange={(social) => setS({ ...s, footer: { ...footer, social } })}\n newItem={() => ({ platform: \"\", href: \"\" })}\n addLabel=\"+ Social-link\"\n renderRow={(link, update) => (\n <div className=\"grid grid-cols-2 gap-2\">\n <Input value={link.platform} placeholder=\"bv. Instagram\" onChange={(e) => update({ platform: e.target.value })} />\n <Input value={link.href} placeholder=\"https://…\" onChange={(e) => update({ href: e.target.value })} />\n </div>\n )}\n />\n </div>\n <div className=\"grid gap-3 md:grid-cols-2\">\n <Field label=\"Copyright (leeg = automatisch)\">\n <Input value={footer.copyright ?? \"\"} onChange={(e) => setS({ ...s, footer: { ...footer, copyright: e.target.value } })} />\n </Field>\n <label className=\"flex items-center gap-2 self-end text-sm\">\n <Switch checked={footer.showAdminLink !== false} onCheckedChange={(v) => setS({ ...s, footer: { ...footer, showAdminLink: v } })} />\n “Beheer”-link tonen\n </label>\n </div>\n </div>\n </Card>\n </>\n )}\n\n {sectie === \"modules\" && (\n <>\n <Card>\n <h3 className=\"mb-1 text-base font-semibold\">Modules</h3>\n <p className=\"mb-3 text-xs text-slate-500\">Zet functies aan/uit en stel ze in. Uitgezette modules verbergen hun blokken en beheer-paneel.</p>\n <div className=\"grid gap-3\">\n {defs\n .filter((d) => !d.core)\n .map((d) => {\n const st = moduleState(d.id, d.defaultEnabled ?? true);\n return (\n <div key={d.id} className=\"rounded-lg border border-slate-200 p-3\">\n <label className=\"flex items-center justify-between gap-2\">\n <span>\n <strong className=\"text-sm\">{d.label}</strong>\n {d.description && <span className=\"block text-xs text-slate-500\">{d.description}</span>}\n </span>\n <Switch checked={st.enabled} onCheckedChange={(v) => setModule(d.id, { enabled: v, config: st.config })} />\n </label>\n {st.enabled && d.settingsSchema && (\n <div className=\"mt-3\">\n <ModuleSettingsForm\n schema={d.settingsSchema}\n value={st.config}\n onChange={(config) => setModule(d.id, { enabled: st.enabled, config })}\n />\n </div>\n )}\n </div>\n );\n })}\n </div>\n </Card>\n </>\n )}\n\n {sectie === \"mail\" && (\n <>\n {/* Site-brede mailprovider + opmaak (formulieren én shop). Zie docs/SHARED-SETTINGS.md. */}\n <MailSettings siteId={siteId} />\n\n <Card>\n <h3 className=\"mb-1 text-base font-semibold\">E-mailteksten</h3>\n <p className=\"text-sm text-slate-600\">\n De teksten van automatische e-mails bewerk je bij het onderdeel zelf: formulier-mails onder{\" \"}\n <strong>Formulieren</strong>, webwinkel-mails onder <strong>Webwinkel → E-mails</strong>. Provider en\n opmaak (logo, huisstijlkleur) gelden site-breed en stel je hier in.\n </p>\n </Card>\n\n <Card>\n <h3 className=\"mb-1 text-base font-semibold\">Formulier-afzender</h3>\n <p className=\"mb-3 text-xs text-slate-500\">\n De afzender-identiteit die formulier-notificaties gebruiken. Kies de XiCMS-afzender\n (werkt direct) of het eigen domein van de klant (vereist domeinverificatie). De provider\n waarmee daadwerkelijk verstuurd wordt, stel je hierboven bij <strong>E-mail</strong> in.\n </p>\n <div className=\"grid gap-3 md:grid-cols-2\">\n <Field label=\"Afzender-modus\">\n <Select\n value={forms.mode}\n onValueChange={(v) => setS({ ...s, forms: { ...forms, mode: v as \"xicms\" | \"client\" } })}\n options={[\n { value: \"xicms\", label: \"XiCMS-domein (aanbevolen)\" },\n { value: \"client\", label: \"Eigen domein van de klant\" },\n ]}\n />\n </Field>\n <Field label=\"Van (from) e-mail\" hint=\"Moet een geverifieerd domein zijn.\">\n <Input value={forms.fromEmail ?? \"\"} onChange={(e) => setS({ ...s, forms: { ...forms, fromEmail: e.target.value } })} />\n </Field>\n <Field label=\"Van (naam)\"><Input value={forms.fromName ?? \"\"} onChange={(e) => setS({ ...s, forms: { ...forms, fromName: e.target.value } })} /></Field>\n <Field label=\"Notificaties naar\" hint=\"Meestal het mailadres van de klant.\">\n <Input value={forms.notifyTo ?? \"\"} onChange={(e) => setS({ ...s, forms: { ...forms, notifyTo: e.target.value } })} />\n </Field>\n </div>\n </Card>\n </>\n )}\n\n {sectie === \"backup\" && (\n <>\n <Card>\n <h3 className=\"mb-1 text-base font-semibold\">Back-up & herstel</h3>\n <p className=\"mb-3 text-xs text-slate-500\">\n Sla alle content van deze site (pagina's, nieuws, formulieren, instellingen en je foto-galerij)\n op in één bestand. Bewaar dat veilig; je kunt het later weer terugzetten. De foto-bestanden\n zelf blijven in de cloud staan — de back-up bevat de teksten, instellingen en verwijzingen.\n </p>\n <div className=\"flex flex-wrap gap-2\">\n <Button variant=\"secondary\" onClick={() => void downloadBackup()} disabled={exporting || restoring}>\n {exporting ? \"Bezig…\" : \"Back-up downloaden\"}\n </Button>\n <Button variant=\"secondary\" onClick={() => restoreInput.current?.click()} disabled={restoring || exporting}>\n {restoring ? \"Terugzetten…\" : \"Back-up terugzetten…\"}\n </Button>\n <input\n ref={restoreInput}\n type=\"file\"\n accept=\"application/json,.json\"\n hidden\n onChange={(e) => {\n const f = e.target.files?.[0];\n if (f) void restoreFromFile(f);\n e.target.value = \"\";\n }}\n />\n </div>\n <p className=\"mt-2 text-xs text-slate-400\">\n Terugzetten vervangt de huidige content door de back-up. We maken automatisch eerst een\n back-up van de huidige stand, zodat je nooit iets kwijtraakt.\n </p>\n </Card>\n\n <SnapshotsCard siteId={siteId} />\n </>\n )}\n\n {/* Sticky: één formulier, één opslaan-knop — die moet bereikbaar blijven vanuit elke sectie,\n ook onderaan een lange (Huisstijl, Footer). */}\n <div className=\"sticky bottom-0 -mx-4 flex justify-end border-t px-4 py-3\" style={{ borderColor: \"var(--color-border)\", background: \"var(--admin-canvas)\" }}>\n <Button onClick={save}>Instellingen opslaan</Button>\n </div>\n </div>\n );\n}\n"],"mappings":"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","names":["ok"]}
|
|
1
|
+
{"version":3,"sources":["../../../src/admin/settings/SettingsPanel.tsx"],"sourcesContent":["\"use client\";\n\nimport { useEffect, useRef, useState } from \"react\";\nimport { doc, onSnapshot } from \"firebase/firestore\";\nimport { callable } from \"../../firebase/functionsClient\";\nimport { getDb, getFirebaseApp } from \"../../firebase/client\";\nimport { SiteSettingsSchema, type SiteSettings } from \"../../schema/settings\";\nimport { useModules } from \"../../modules/context\";\nimport { useSiteConfig } from \"../../config\";\nimport type { ModulesSiteConfig } from \"../../modules/types\";\nimport { builtinThemePresets } from \"../../theme/presets\";\nimport { Button, Card, Field, Input } from \"../ui/primitives\";\nimport { Select } from \"../ui/select\";\nimport { Switch } from \"../ui/switch\";\nimport { ArrayEditor } from \"../ui/ArrayEditor\";\nimport { useToast } from \"../ui/toast\";\nimport { useConfirm } from \"../ui/ConfirmDialog\";\nimport { ModuleSettingsForm } from \"./moduleSettingsForm\";\nimport { SnapshotsCard } from \"./SnapshotsCard\";\nimport { MailSettings } from \"./MailSettings\";\n\nexport function SettingsPanel({ siteId }: { siteId: string }) {\n const { toast } = useToast();\n const confirm = useConfirm();\n const { defs } = useModules();\n const cfg = useSiteConfig();\n const [s, setS] = useState<SiteSettings>({});\n const [loaded, setLoaded] = useState(false);\n const [exporting, setExporting] = useState(false);\n const [restoring, setRestoring] = useState(false);\n const restoreInput = useRef<HTMLInputElement>(null);\n\n async function downloadBackup(silent = false): Promise<boolean> {\n const app = getFirebaseApp();\n if (!app) return false;\n setExporting(true);\n try {\n const fn = callable<{ siteId: string }, Record<string, unknown>>(app, \"exportSite\");\n const res = await fn({ siteId });\n const blob = new Blob([JSON.stringify(res.data, null, 2)], { type: \"application/json\" });\n const url = URL.createObjectURL(blob);\n const a = document.createElement(\"a\");\n a.href = url;\n a.download = `xicms-backup-${siteId}-${new Date().toISOString().slice(0, 10)}.json`;\n a.click();\n URL.revokeObjectURL(url);\n if (!silent) toast({ title: \"Back-up gedownload\", tone: \"success\" });\n return true;\n } catch (e) {\n toast({ title: \"Back-up mislukt\", description: (e as Error).message, tone: \"error\" });\n return false;\n } finally {\n setExporting(false);\n }\n }\n\n async function restoreFromFile(file: File) {\n let backup: Record<string, unknown>;\n try {\n backup = JSON.parse(await file.text()) as Record<string, unknown>;\n } catch {\n toast({ title: \"Ongeldig bestand\", description: \"Dit is geen geldig back-upbestand.\", tone: \"error\" });\n return;\n }\n if (backup._format !== \"xicms-export\") {\n toast({ title: \"Ongeldig bestand\", description: \"Dit lijkt geen XiCMS-back-up.\", tone: \"error\" });\n return;\n }\n if (backup.siteId && backup.siteId !== siteId) {\n const ok = await confirm({\n title: \"Back-up van een andere site\",\n description: `Deze back-up is van “${String(backup.siteId)}”, niet van deze site (${siteId}). Toch terugzetten?`,\n confirmLabel: \"Toch terugzetten\",\n tone: \"danger\",\n });\n if (!ok) return;\n }\n const ok = await confirm({\n title: \"Back-up terugzetten?\",\n description: `Dit VERVANGT alle huidige pagina's, nieuws, media en instellingen door de inhoud van de back-up${backup.exportedAt ? ` van ${new Date(String(backup.exportedAt)).toLocaleString(\"nl-NL\")}` : \"\"}. We maken eerst automatisch een back-up van de huidige stand.`,\n confirmLabel: \"Terugzetten\",\n tone: \"danger\",\n });\n if (!ok) return;\n setRestoring(true);\n try {\n // Veiligheid: eerst de huidige stand wegschrijven (als download) vóór we overschrijven.\n // Lukt die back-up niet, dan NIET doorgaan met de destructieve restore (anders ben je de\n // huidige stand kwijt zonder vangnet). De gebruiker kan het daarna opnieuw proberen.\n if (!(await downloadBackup(true))) {\n toast({ title: \"Terugzetten afgebroken\", description: \"De veiligheidsback-up van de huidige stand mislukte; er is niets overschreven.\", tone: \"error\" });\n return;\n }\n const app = getFirebaseApp();\n if (!app) throw new Error(\"Firebase niet geconfigureerd.\");\n const fn = callable(app, \"restoreSite\");\n await fn({ siteId, backup });\n toast({ title: \"Back-up teruggezet\", description: \"Herlaad de pagina om alles bij te werken.\", tone: \"success\" });\n } catch (e) {\n toast({ title: \"Terugzetten mislukt\", description: (e as Error).message, tone: \"error\" });\n } finally {\n setRestoring(false);\n }\n }\n\n /**\n * Instellingen was één scroll van negen kaarten — van sitenaam tot back-up, alles onder elkaar.\n * Dat leest niet als \"waar stel ik mijn huisstijl in?\" maar als \"zoek het maar\". De kaarten zijn\n * nu per onderwerp gegroepeerd; je ziet er één tegelijk. De staat zit in `s` hierboven, dus\n * wisselen van sectie raakt niets kwijt en één \"Opslaan\" bewaart nog steeds het geheel.\n */\n const [sectie, setSectie] = useState<string>(\"algemeen\");\n\n useEffect(() => {\n const db = getDb();\n if (!db) {\n setLoaded(true);\n return;\n }\n return onSnapshot(doc(db, \"sites\", siteId, \"settings\", \"site\"), (snap) => {\n const uitDb = snap.exists() ? (snap.data() as SiteSettings) : {};\n // Sitenaam, domein en contact-e-mail stonden hier leeg, en dat las als \"niet ingevuld\".\n // Ze wáren ingevuld, alleen ergens anders: in `content/site.config.ts`, waar ze bij het\n // bouwen van de site zijn gezet. Het instellingen-document kent ze pas zodra iemand hier\n // op Opslaan drukt, en dat had nog nooit iemand gedaan.\n //\n // Terugvallen op de site-config maakt het eerlijk: je ziet de waarde die de site\n // daadwerkelijk gebruikt, en kunt hem van daaruit aanpassen. Wat in het document staat wint\n // altijd — anders zou een bewust leeggemaakt veld telkens terugkomen.\n setS({\n ...uitDb,\n name: uitDb.name ?? cfg.name,\n domain: uitDb.domain ?? cfg.domain,\n contactEmail: uitDb.contactEmail ?? cfg.contactEmail,\n });\n setLoaded(true);\n });\n }, [siteId, cfg.name, cfg.domain, cfg.contactEmail]);\n\n async function save() {\n const db = getDb();\n if (!db) return;\n const parsed = SiteSettingsSchema.safeParse(s);\n if (!parsed.success) {\n toast({ title: \"Ongeldige instellingen\", description: parsed.error.issues[0]?.message, tone: \"error\" });\n return;\n }\n // Via de callable i.p.v. rechtstreeks: die valideert server-side, leidt de publieke subset\n // zelf af (zodat `site` en `public` niet uit elkaar kunnen lopen) én duwt de cache-tag door,\n // waardoor de wijziging binnen seconden live staat i.p.v. na het ISR-vangnet.\n try {\n const app = getFirebaseApp();\n if (!app) throw new Error(\"Firebase is niet geconfigureerd.\");\n const fn = callable(app, \"saveSiteSettings\");\n const res = await fn({ siteId, settings: parsed.data });\n toast({ title: \"Opgeslagen\", description: (res.data as { message?: string })?.message ?? \"Wijzigingen zijn live.\", tone: \"success\" });\n } catch (e) {\n toast({ title: \"Opslaan mislukt\", description: (e as Error).message, tone: \"error\" });\n }\n }\n\n if (!loaded) return <p className=\"text-sm text-slate-500\">Laden…</p>;\n\n const forms = s.forms ?? { mode: \"xicms\" as const };\n const theme = s.theme ?? { primary: \"#4f46e5\" };\n // Zelfde terugval als XiCMSProvider: `mode` wint, anders de oude boolean.\n const donkerModus: \"light\" | \"dark\" | \"auto\" = theme.dark?.mode ?? (theme.dark?.auto ? \"auto\" : \"light\");\n const footer = s.footer ?? {};\n\n // Hulpfuncties voor de module-instellingen.\n const moduleState = (id: string, defaultEnabled: boolean) => {\n const raw = (s.modules as ModulesSiteConfig | undefined)?.[id];\n if (typeof raw === \"boolean\") return { enabled: raw, config: {} as Record<string, unknown> };\n return { enabled: raw?.enabled ?? defaultEnabled, config: raw?.config ?? {} };\n };\n const setModule = (id: string, next: { enabled: boolean; config: Record<string, unknown> }) =>\n setS({ ...s, modules: { ...(s.modules as ModulesSiteConfig | undefined), [id]: next } });\n\n const SECTIES: { sleutel: string; label: string }[] = [\n { sleutel: \"algemeen\", label: \"Algemeen\" },\n { sleutel: \"huisstijl\", label: \"Huisstijl\" },\n { sleutel: \"seo\", label: \"SEO\" },\n { sleutel: \"footer\", label: \"Footer\" },\n { sleutel: \"modules\", label: \"Modules\" },\n { sleutel: \"mail\", label: \"E-mail\" },\n { sleutel: \"backup\", label: \"Back-up\" },\n ];\n\n return (\n /* Een formulier leest niet op 1300px breed; een invoerveld hoort niet breder te zijn dan de\n tekst die erin komt. Vandaar een leesbreedte in plaats van het hele werkblad vullen. */\n <div className=\"grid max-w-4xl gap-4\">\n <nav aria-label=\"Onderdelen van de instellingen\" className=\"flex flex-wrap gap-1\">\n {SECTIES.map((x) => (\n <button\n key={x.sleutel}\n type=\"button\"\n aria-current={sectie === x.sleutel ? \"page\" : undefined}\n onClick={() => setSectie(x.sleutel)}\n className=\"rounded-lg px-3 py-1.5 text-sm font-medium text-slate-600 hover:bg-slate-200 hover:text-slate-900 focus-visible:ring-2 focus-visible:ring-brand-500 focus-visible:outline-none aria-[current=page]:bg-slate-200 aria-[current=page]:text-slate-900\"\n >\n {x.label}\n </button>\n ))}\n </nav>\n\n {sectie === \"algemeen\" && (\n <>\n <Card>\n <h3 className=\"mb-3 text-base font-semibold\">Algemeen</h3>\n <div className=\"grid gap-3 md:grid-cols-2\">\n <Field label=\"Sitenaam\"><Input value={s.name ?? \"\"} onChange={(e) => setS({ ...s, name: e.target.value })} /></Field>\n <Field label=\"Domein\"><Input value={s.domain ?? \"\"} onChange={(e) => setS({ ...s, domain: e.target.value })} /></Field>\n <Field label=\"Contact-e-mail\"><Input value={s.contactEmail ?? \"\"} onChange={(e) => setS({ ...s, contactEmail: e.target.value })} /></Field>\n </div>\n </Card>\n </>\n )}\n\n {sectie === \"huisstijl\" && (\n <>\n <Card>\n <h3 className=\"mb-3 text-base font-semibold\">Huisstijl</h3>\n <div className=\"grid gap-3\">\n <Field label=\"Thema-preset\" hint=\"Vult kleuren, lettertype en afronding in één keer in.\">\n <Select\n value=\"\"\n placeholder=\"Kies een preset…\"\n onValueChange={(id) => {\n const preset = builtinThemePresets.find((p) => p.id === id);\n if (preset) setS({ ...s, theme: { ...theme, ...preset.theme } });\n }}\n options={builtinThemePresets.map((p) => ({ value: p.id, label: p.label }))}\n />\n </Field>\n <div className=\"grid gap-3 md:grid-cols-2\">\n <Field\n label=\"Primaire kleur\"\n hint=\"Wordt ook gebruikt in je e-mails en op de factuur-PDF\"\n ><Input value={theme.primary} onChange={(e) => setS({ ...s, theme: { ...theme, primary: e.target.value } })} /></Field>\n <Field label=\"Accentkleur\"><Input value={theme.accent ?? \"\"} onChange={(e) => setS({ ...s, theme: { ...theme, accent: e.target.value } })} /></Field>\n <Field label=\"Achtergrondkleur\"><Input value={theme.background ?? \"\"} onChange={(e) => setS({ ...s, theme: { ...theme, background: e.target.value } })} placeholder=\"#ffffff\" /></Field>\n <Field label=\"Tekstkleur\"><Input value={theme.foreground ?? \"\"} onChange={(e) => setS({ ...s, theme: { ...theme, foreground: e.target.value } })} placeholder=\"#0f172a\" /></Field>\n </div>\n {/* Donkere modus. Het schema en de rendering ondersteunden dit al, maar er was geen knop:\n donker aanzetten kon alleen door site.config.ts te bewerken — dus niet door de klant\n zelf. De kleuren verschijnen pas als ze ergens toe doen; bij \"altijd licht\" zou een\n donkere achtergrondkleur alleen verwarren. */}\n <Field\n label=\"Donkere modus\"\n hint={\n donkerModus === \"auto\"\n ? \"Volgt de voorkeur van de bezoeker: wie zijn telefoon of computer op donker heeft staan, ziet de site donker.\"\n : donkerModus === \"dark\"\n ? \"De site is voor iedereen donker, ongeacht de voorkeur van de bezoeker.\"\n : \"De site is voor iedereen licht.\"\n }\n >\n <Select\n value={donkerModus}\n onValueChange={(v) =>\n setS({ ...s, theme: { ...theme, dark: { ...(theme.dark ?? {}), mode: v as \"light\" | \"dark\" | \"auto\" } } })\n }\n options={[\n { value: \"light\", label: \"Altijd licht\" },\n { value: \"dark\", label: \"Altijd donker\" },\n { value: \"auto\", label: \"Automatisch — volg de bezoeker\" },\n ]}\n />\n </Field>\n {donkerModus !== \"light\" && (\n <div className=\"grid gap-3 sm:grid-cols-3\">\n <Field label=\"Donkere achtergrond\">\n <Input\n value={theme.dark?.background ?? \"\"}\n onChange={(e) => setS({ ...s, theme: { ...theme, dark: { ...(theme.dark ?? {}), background: e.target.value } } })}\n placeholder=\"#0f172a\"\n />\n </Field>\n <Field label=\"Donkere tekstkleur\">\n <Input\n value={theme.dark?.foreground ?? \"\"}\n onChange={(e) => setS({ ...s, theme: { ...theme, dark: { ...(theme.dark ?? {}), foreground: e.target.value } } })}\n placeholder=\"#e2e8f0\"\n />\n </Field>\n {/* Een merkkleur die op wit werkt is op zwart vaak te donker; daarom een eigen waarde. */}\n <Field label=\"Donkere merkkleur\" hint=\"Laat leeg om dezelfde te gebruiken.\">\n <Input\n value={theme.dark?.primary ?? \"\"}\n onChange={(e) => setS({ ...s, theme: { ...theme, dark: { ...(theme.dark ?? {}), primary: e.target.value } } })}\n placeholder={theme.primary}\n />\n </Field>\n </div>\n )}\n <Field label=\"Logo-URL (e-mail & factuur)\" hint=\"Absolute https-URL naar een PNG/JPG. Verschijnt in de mail-header en op de factuur-PDF.\">\n <Input value={theme.logo ?? \"\"} onChange={(e) => setS({ ...s, theme: { ...theme, logo: e.target.value } })} placeholder=\"https://…/logo.png\" />\n </Field>\n </div>\n </Card>\n </>\n )}\n\n {sectie === \"seo\" && (\n <>\n <Card>\n <h3 className=\"mb-3 text-base font-semibold\">SEO (standaard)</h3>\n <div className=\"grid gap-3\">\n <Field label=\"Standaard-omschrijving\">\n <Input value={s.seoDefaults?.description ?? \"\"} onChange={(e) => setS({ ...s, seoDefaults: { ...s.seoDefaults, description: e.target.value } })} />\n </Field>\n <Field label=\"OG-afbeelding (URL)\">\n <Input value={s.seoDefaults?.ogImage ?? \"\"} onChange={(e) => setS({ ...s, seoDefaults: { ...s.seoDefaults, ogImage: e.target.value } })} />\n </Field>\n </div>\n </Card>\n </>\n )}\n\n {sectie === \"footer\" && (\n <>\n <Card>\n <h3 className=\"mb-1 text-base font-semibold\">Footer</h3>\n <p className=\"mb-3 text-xs text-slate-500\">Kolommen met links, social-links en copyright. Wijzigingen zijn direct live.</p>\n <div className=\"grid gap-4\">\n <div>\n <span className=\"text-sm font-medium\">Kolommen</span>\n <ArrayEditor\n items={footer.columns ?? []}\n onChange={(columns) => setS({ ...s, footer: { ...footer, columns } })}\n newItem={() => ({ heading: \"\", links: [] })}\n addLabel=\"+ Kolom\"\n emptyLabel=\"Nog geen kolommen.\"\n renderRow={(col, update) => (\n <div className=\"grid gap-2\">\n <Field label=\"Kop\"><Input value={col.heading ?? \"\"} onChange={(e) => update({ heading: e.target.value })} /></Field>\n <ArrayEditor\n items={col.links ?? []}\n onChange={(links) => update({ links })}\n newItem={() => ({ label: \"\", href: \"\" })}\n addLabel=\"+ Link\"\n renderRow={(link, updL) => (\n <div className=\"grid grid-cols-2 gap-2\">\n <Input value={link.label} placeholder=\"Label\" onChange={(e) => updL({ label: e.target.value })} />\n <Input value={link.href} placeholder=\"/pad of https://…\" onChange={(e) => updL({ href: e.target.value })} />\n </div>\n )}\n />\n </div>\n )}\n />\n </div>\n <div>\n <span className=\"text-sm font-medium\">Social-links</span>\n <ArrayEditor\n items={footer.social ?? []}\n onChange={(social) => setS({ ...s, footer: { ...footer, social } })}\n newItem={() => ({ platform: \"\", href: \"\" })}\n addLabel=\"+ Social-link\"\n renderRow={(link, update) => (\n <div className=\"grid grid-cols-2 gap-2\">\n <Input value={link.platform} placeholder=\"bv. Instagram\" onChange={(e) => update({ platform: e.target.value })} />\n <Input value={link.href} placeholder=\"https://…\" onChange={(e) => update({ href: e.target.value })} />\n </div>\n )}\n />\n </div>\n <div className=\"grid gap-3 md:grid-cols-2\">\n <Field label=\"Copyright (leeg = automatisch)\">\n <Input value={footer.copyright ?? \"\"} onChange={(e) => setS({ ...s, footer: { ...footer, copyright: e.target.value } })} />\n </Field>\n <label className=\"flex items-center gap-2 self-end text-sm\">\n <Switch checked={footer.showAdminLink !== false} onCheckedChange={(v) => setS({ ...s, footer: { ...footer, showAdminLink: v } })} />\n “Beheer”-link tonen\n </label>\n </div>\n </div>\n </Card>\n </>\n )}\n\n {sectie === \"modules\" && (\n <>\n <Card>\n <h3 className=\"mb-1 text-base font-semibold\">Modules</h3>\n <p className=\"mb-3 text-xs text-slate-500\">Zet functies aan/uit en stel ze in. Uitgezette modules verbergen hun blokken en beheer-paneel.</p>\n <div className=\"grid gap-3\">\n {defs\n .filter((d) => !d.core)\n .map((d) => {\n const st = moduleState(d.id, d.defaultEnabled ?? true);\n return (\n <div key={d.id} className=\"rounded-lg border border-slate-200 p-3\">\n <label className=\"flex items-center justify-between gap-2\">\n <span>\n <strong className=\"text-sm\">{d.label}</strong>\n {d.description && <span className=\"block text-xs text-slate-500\">{d.description}</span>}\n </span>\n <Switch checked={st.enabled} onCheckedChange={(v) => setModule(d.id, { enabled: v, config: st.config })} />\n </label>\n {st.enabled && d.settingsSchema && (\n <div className=\"mt-3\">\n <ModuleSettingsForm\n schema={d.settingsSchema}\n value={st.config}\n onChange={(config) => setModule(d.id, { enabled: st.enabled, config })}\n />\n </div>\n )}\n </div>\n );\n })}\n </div>\n </Card>\n </>\n )}\n\n {sectie === \"mail\" && (\n <>\n {/* Site-brede mailprovider + opmaak (formulieren én shop). Zie docs/SHARED-SETTINGS.md. */}\n <MailSettings siteId={siteId} />\n\n <Card>\n <h3 className=\"mb-1 text-base font-semibold\">E-mailteksten</h3>\n <p className=\"text-sm text-slate-600\">\n De teksten van automatische e-mails bewerk je bij het onderdeel zelf: formulier-mails onder{\" \"}\n <strong>Formulieren</strong>, webwinkel-mails onder <strong>Webwinkel → E-mails</strong>. Provider en\n opmaak (logo, huisstijlkleur) gelden site-breed en stel je hier in.\n </p>\n </Card>\n\n <Card>\n <h3 className=\"mb-1 text-base font-semibold\">Formulier-afzender</h3>\n <p className=\"mb-3 text-xs text-slate-500\">\n De afzender-identiteit die formulier-notificaties gebruiken. Kies de XiCMS-afzender\n (werkt direct) of het eigen domein van de klant (vereist domeinverificatie). De provider\n waarmee daadwerkelijk verstuurd wordt, stel je hierboven bij <strong>E-mail</strong> in.\n </p>\n <div className=\"grid gap-3 md:grid-cols-2\">\n <Field label=\"Afzender-modus\">\n <Select\n value={forms.mode}\n onValueChange={(v) => setS({ ...s, forms: { ...forms, mode: v as \"xicms\" | \"client\" } })}\n options={[\n { value: \"xicms\", label: \"XiCMS-domein (aanbevolen)\" },\n { value: \"client\", label: \"Eigen domein van de klant\" },\n ]}\n />\n </Field>\n <Field label=\"Van (from) e-mail\" hint=\"Moet een geverifieerd domein zijn.\">\n <Input value={forms.fromEmail ?? \"\"} onChange={(e) => setS({ ...s, forms: { ...forms, fromEmail: e.target.value } })} />\n </Field>\n <Field label=\"Van (naam)\"><Input value={forms.fromName ?? \"\"} onChange={(e) => setS({ ...s, forms: { ...forms, fromName: e.target.value } })} /></Field>\n <Field label=\"Notificaties naar\" hint=\"Meestal het mailadres van de klant.\">\n <Input value={forms.notifyTo ?? \"\"} onChange={(e) => setS({ ...s, forms: { ...forms, notifyTo: e.target.value } })} />\n </Field>\n </div>\n </Card>\n </>\n )}\n\n {sectie === \"backup\" && (\n <>\n <Card>\n <h3 className=\"mb-1 text-base font-semibold\">Back-up & herstel</h3>\n <p className=\"mb-3 text-xs text-slate-500\">\n Sla alle content van deze site (pagina's, nieuws, formulieren, instellingen en je foto-galerij)\n op in één bestand. Bewaar dat veilig; je kunt het later weer terugzetten. De foto-bestanden\n zelf blijven in de cloud staan — de back-up bevat de teksten, instellingen en verwijzingen.\n </p>\n <div className=\"flex flex-wrap gap-2\">\n <Button variant=\"secondary\" onClick={() => void downloadBackup()} disabled={exporting || restoring}>\n {exporting ? \"Bezig…\" : \"Back-up downloaden\"}\n </Button>\n <Button variant=\"secondary\" onClick={() => restoreInput.current?.click()} disabled={restoring || exporting}>\n {restoring ? \"Terugzetten…\" : \"Back-up terugzetten…\"}\n </Button>\n <input\n ref={restoreInput}\n type=\"file\"\n accept=\"application/json,.json\"\n hidden\n onChange={(e) => {\n const f = e.target.files?.[0];\n if (f) void restoreFromFile(f);\n e.target.value = \"\";\n }}\n />\n </div>\n <p className=\"mt-2 text-xs text-slate-400\">\n Terugzetten vervangt de huidige content door de back-up. We maken automatisch eerst een\n back-up van de huidige stand, zodat je nooit iets kwijtraakt.\n </p>\n </Card>\n\n <SnapshotsCard siteId={siteId} />\n </>\n )}\n\n {/* Sticky: één formulier, één opslaan-knop — die moet bereikbaar blijven vanuit elke sectie,\n ook onderaan een lange (Huisstijl, Footer). */}\n <div className=\"sticky bottom-0 -mx-4 flex justify-end border-t px-4 py-3\" style={{ borderColor: \"var(--color-border)\", background: \"var(--admin-canvas)\" }}>\n <Button onClick={save}>Instellingen opslaan</Button>\n </div>\n </div>\n );\n}\n"],"mappings":"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","names":["ok"]}
|
|
@@ -3,7 +3,7 @@ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
|
3
3
|
import { useEffect, useMemo, useState } from "react";
|
|
4
4
|
import { collection, getDocs, limit, onSnapshot, orderBy, query, where } from "firebase/firestore";
|
|
5
5
|
import { getDb } from "../../firebase/client.js";
|
|
6
|
-
import { formatCents } from "../../schema/shop.js";
|
|
6
|
+
import { formatCents, orderRegelNaam } from "../../schema/shop.js";
|
|
7
7
|
import { Badge, Button, Card, Field, Input, Textarea } from "../ui/primitives.js";
|
|
8
8
|
import { Select } from "../ui/select.js";
|
|
9
9
|
import { Switch } from "../ui/switch.js";
|
|
@@ -48,6 +48,7 @@ function OrdersTab({ siteId }) {
|
|
|
48
48
|
const [manual, setManual] = useState({ name: "", email: "", phone: "", address: "", zip: "", city: "", shipping: "", note: "", paid: true, notify: false });
|
|
49
49
|
const [manualLines, setManualLines] = useState([]);
|
|
50
50
|
const [producten, setProducten] = useState([]);
|
|
51
|
+
const [catNamen, setCatNamen] = useState({});
|
|
51
52
|
useEffect(() => {
|
|
52
53
|
if (!manualOpen || producten.length > 0) return;
|
|
53
54
|
const db = getDb();
|
|
@@ -57,7 +58,14 @@ function OrdersTab({ siteId }) {
|
|
|
57
58
|
snap.docs.map((d) => ({ id: d.id, ...d.data() })).sort((a, b) => a.name.localeCompare(b.name, "nl"))
|
|
58
59
|
)
|
|
59
60
|
);
|
|
61
|
+
void getDocs(collection(db, "sites", siteId, "shopCategories")).then(
|
|
62
|
+
(snap) => setCatNamen(Object.fromEntries(snap.docs.map((d) => [d.id, String(d.data().name ?? d.id)])))
|
|
63
|
+
);
|
|
60
64
|
}, [manualOpen, producten.length, siteId]);
|
|
65
|
+
const productLabel = (p) => {
|
|
66
|
+
const cat = catNamen[p.categoryIds?.[0] ?? ""];
|
|
67
|
+
return cat ? `${p.name} \u2014 ${cat}` : p.name;
|
|
68
|
+
};
|
|
61
69
|
const [trackingUrl, setTrackingUrl] = useState("");
|
|
62
70
|
const [notify, setNotify] = useState(true);
|
|
63
71
|
useEffect(() => {
|
|
@@ -284,7 +292,7 @@ function OrdersTab({ siteId }) {
|
|
|
284
292
|
o.status,
|
|
285
293
|
o.customer.name,
|
|
286
294
|
o.customer.email,
|
|
287
|
-
o.items.map((i) => `${i.qty}x ${i
|
|
295
|
+
o.items.map((i) => `${i.qty}x ${orderRegelNaam(i)}${i.variantKey ? ` (${i.variantKey})` : ""}`).join(" | "),
|
|
288
296
|
(o.amount.itemsCents / 100).toFixed(2),
|
|
289
297
|
((o.amount.discountCents ?? 0) / 100).toFixed(2),
|
|
290
298
|
(o.amount.shippingCents / 100).toFixed(2),
|
|
@@ -434,7 +442,7 @@ function OrdersTab({ siteId }) {
|
|
|
434
442
|
/* @__PURE__ */ jsxs("td", { className: "py-1 pr-2", children: [
|
|
435
443
|
i.qty,
|
|
436
444
|
"\xD7 ",
|
|
437
|
-
i
|
|
445
|
+
orderRegelNaam(i),
|
|
438
446
|
i.variantKey && /* @__PURE__ */ jsxs("span", { className: "text-slate-500", children: [
|
|
439
447
|
" (",
|
|
440
448
|
i.variantKey.split("|").join(" \xB7 "),
|
|
@@ -478,10 +486,17 @@ function OrdersTab({ siteId }) {
|
|
|
478
486
|
] })
|
|
479
487
|
] }) })
|
|
480
488
|
] }),
|
|
481
|
-
current.mail?.error
|
|
489
|
+
current.mail?.error ? /* @__PURE__ */ jsxs("p", { className: "rounded-lg bg-amber-50 px-3 py-2 text-sm text-amber-700", children: [
|
|
482
490
|
"Bevestigingsmail mislukt: ",
|
|
483
491
|
current.mail.error
|
|
484
|
-
] })
|
|
492
|
+
] }) : current.mail?.confirmationSentAt ? (
|
|
493
|
+
/* Geslaagde verzending werd wél vastgelegd maar nergens getoond: je zag alleen
|
|
494
|
+
iets bij een FOUT, en moest dus maar aannemen dat de mail was aangekomen. */
|
|
495
|
+
/* @__PURE__ */ jsxs("p", { className: "text-xs text-slate-400", children: [
|
|
496
|
+
"Bevestigingsmail verstuurd op ",
|
|
497
|
+
fmtDate(current.mail.confirmationSentAt)
|
|
498
|
+
] })
|
|
499
|
+
) : null,
|
|
485
500
|
shipOpen && current.status === "paid" && /* @__PURE__ */ jsxs("div", { className: "grid gap-3 rounded-lg border border-slate-200 p-3", children: [
|
|
486
501
|
/* @__PURE__ */ jsx(
|
|
487
502
|
Input,
|
|
@@ -560,7 +575,7 @@ function OrdersTab({ siteId }) {
|
|
|
560
575
|
value: l.productId,
|
|
561
576
|
onValueChange: (v) => setManualLines((p) => p.map((x, j) => j === i ? { ...x, productId: v, variantKey: "" } : x)),
|
|
562
577
|
placeholder: "Kies product\u2026",
|
|
563
|
-
options: producten.map((p) => ({ value: p.id, label: p
|
|
578
|
+
options: producten.map((p) => ({ value: p.id, label: productLabel(p) }))
|
|
564
579
|
}
|
|
565
580
|
),
|
|
566
581
|
varianten.length > 0 ? /* @__PURE__ */ jsx(
|