@xiwebdesign/xicms-core 6.2.0 → 6.4.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.
@@ -5,6 +5,7 @@ import { ConfigProvider } from "./config";
5
5
  import { AuthProvider, useAuth } from "./auth/AuthProvider";
6
6
  import { canEdit } from "./auth/roles";
7
7
  import { EditBar } from "./components/EditBar";
8
+ import { FeedbackWidget } from "./components/FeedbackWidget";
8
9
  import { DemoLock } from "./lock/DemoLock";
9
10
  import { RegistryProvider } from "./blocks/context";
10
11
  import { getDb } from "./firebase/client";
@@ -164,6 +165,7 @@ function ModuleLayer({
164
165
  /* @__PURE__ */ jsx(DemoStudio, {}),
165
166
  children,
166
167
  /* @__PURE__ */ jsx(EditBar, {}),
168
+ /* @__PURE__ */ jsx(FeedbackWidget, { siteId: config.siteId }),
167
169
  /* @__PURE__ */ jsx(DemoLock, {}),
168
170
  resolved.activeIds.has("consent") && /* @__PURE__ */ jsx(ConsentBanner, { settings: resolved.settings.consent })
169
171
  ]
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/XiCMSProvider.tsx"],"sourcesContent":["\"use client\";\n\nimport { useEffect, useMemo, useState, type ReactNode } from \"react\";\nimport { ConfigProvider } from \"./config\";\nimport { AuthProvider, useAuth } from \"./auth/AuthProvider\";\nimport { canEdit } from \"./auth/roles\";\nimport { EditBar } from \"./components/EditBar\";\nimport { DemoLock } from \"./lock/DemoLock\";\nimport { RegistryProvider } from \"./blocks/context\";\nimport { getDb } from \"./firebase/client\";\nimport { collection, onSnapshot } from \"firebase/firestore\";\nimport { createRegistry, mergeRegistry, type BlockDefinition } from \"./blocks/registry\";\nimport { CustomBlockSchema, type CustomBlock as CustomBlockData } from \"./schema/customBlocks\";\nimport { customBlockToDefinition } from \"./blocks/customTemplate/definition\";\nimport { resolveModules } from \"./modules/registry\";\nimport { ModulesProvider } from \"./modules/context\";\nimport { applyBlockOverrides, type BlockOverrides } from \"./modules/overrides\";\nimport { builtinModules } from \"./modules/builtins\";\nimport type { ModuleDefinition, ModulesSiteConfig } from \"./modules/types\";\nimport { ConsentBanner, type ConsentSettings } from \"./modules/consent/ConsentBanner\";\nimport { EditorProvider } from \"./editor/EditorProvider\";\nimport { SiteSettingsProvider, useSiteSettings, useSiteSettingsLiveSync } from \"./settings/useSiteSettings\";\nimport { mergeThemeTokens, type ThemeTokens, type PublicSiteSettings } from \"./schema/settings\";\nimport { initAppCheck } from \"./firebase/appcheck\";\nimport { DemoStudio } from \"./demo/DemoStudio\";\nimport { setDemoActive } from \"./demo/demoStore\";\nimport { Theme, type DefinedTheme } from \"@astryxdesign/core/theme\";\nimport { neutralTheme } from \"@astryxdesign/theme-neutral/built\";\nimport { LinkProvider, type LinkComponentType } from \"@astryxdesign/core/Link\";\nimport { ImageProvider, type ImageComponentType } from \"./media/ImageContext\";\nimport { validateSiteConfig, type SiteConfig } from \"./schema/siteConfig\";\nimport { validateModuleContract } from \"./modules/contract\";\n\ninterface ProviderProps {\n config: SiteConfig;\n /** Losse klant-blokken (via `defineBlock`) buiten een module om. Altijd actief. */\n blocks?: BlockDefinition[];\n /**\n * Klant-modules (via `defineModule`) bovenop de ingebouwde. Worden gemerged op `id` (klant wint)\n * en respecteren `config.modules` voor activatie.\n */\n modules?: ModuleDefinition[];\n /**\n * Per-site presentatie-overrides per block-type (`{ gallery: { Render: MijnGalerij } }`). Vervangt\n * alleen het uiterlijk; schema/editor/logica blijven van core. De krachtigste maatwerk-naad.\n */\n overrides?: BlockOverrides;\n /**\n * SSR-startset van custom blocks (5.0): geef het resultaat van `getSiteCustomBlocks(siteId)`\n * (server) door zodat runtime-gedefinieerde blokken al in de server-HTML zitten. Zonder deze\n * prop laadt de provider ze alsnog client-side (werkend, maar pas na hydratie zichtbaar).\n */\n customBlocksData?: CustomBlockData[];\n /**\n * SSR-startset van de publieke site-instellingen: geef het resultaat van\n * `getPublicSiteSettings(siteId)` (server) door zodat klant-thema/nav/footer al in de\n * server-HTML zitten (geen theme-flits bij eerste paint) én publiek geen client-read meer\n * doet — de ISR-gecachte layout ververst de settings per revalidate. Zonder deze prop laadt\n * de provider ze alsnog client-side (werkend, maar met mogelijke flits + 1 read per bezoeker).\n */\n settingsData?: PublicSiteSettings;\n /**\n * Astryx-basistheme voor de hele site (radius-/spacing-/neutralen-karakter). Standaard `neutral`.\n * Een site kan een ander pakket kiezen (`@astryxdesign/theme-butter`, `-stone`, …) of een eigen\n * `defineTheme(...)` doorgeven voor component-overrides — zónder core te forken.\n */\n astryxTheme?: DefinedTheme;\n /**\n * Framework-link-component (bv. Next `Link`) voor soepele client-navigatie + prefetch. Astryx-\n * componenten met een `href` (knoppen/links/tabs) gebruiken 'm dan i.p.v. een rauwe `<a>` (= volledige\n * page-reload). Core importeert `next/link` niet zelf (optionele peer) — geef 'm hier door vanuit de site.\n */\n linkComponent?: LinkComponentType;\n /**\n * Framework-beeldcomponent (bv. Next `Image`) voor automatische beeldoptimalisatie (responsive\n * srcset, AVIF/WebP, geen layout-shift). Core importeert `next/image` niet zelf — geef 'm hier door.\n */\n imageComponent?: ImageComponentType;\n children: ReactNode;\n}\n\n// Stabiele lege defaults: een `= []`-defaultparameter maakt bij ELKE render een nieuwe\n// array-referentie en laat memo's/effects met `modules`/`blocks` in de deps onnodig hervuren.\nconst NO_BLOCKS: BlockDefinition[] = [];\nconst NO_MODULES: ModuleDefinition[] = [];\n\n// Dev-waarschuwingen één keer per boodschap (React strict-mode dubbelt effects; zonder dedupe\n// verschijnt dezelfde config-waarschuwing telkens opnieuw in de console).\nconst warned = new Set<string>();\nfunction warnOnce(message: string) {\n if (warned.has(message)) return;\n warned.add(message);\n console.warn(message);\n}\n\n/**\n * Wikkel de hele site in deze provider (in `app/layout.tsx`). Hij zet:\n * - de SiteConfig-context (siteId, theme, mode) + live publieke settings (footer/nav/modules);\n * - auth + content (Firebase, env-gedreven; werkt ook zonder Firebase);\n * - de block-registry (ingebouwde + klant-blokken) voor de page-renderer;\n * - de themavariabelen als CSS custom properties;\n * - App Check (alleen als geconfigureerd) en de EditBar/DemoLock.\n */\nexport function XiCMSProvider({ config, blocks = NO_BLOCKS, modules = NO_MODULES, overrides, astryxTheme, linkComponent, imageComponent, customBlocksData, settingsData, children }: ProviderProps) {\n useEffect(() => {\n initAppCheck();\n }, []);\n\n // Dev-vangnet voor (agent-)geschreven configs: meld config- en module-contractfouten meteen in\n // de console i.p.v. een subtiel kapotte site. In productie geen werk — en er wordt nooit gegooid.\n useEffect(() => {\n if (process.env.NODE_ENV === \"production\") return;\n const configIssues = validateSiteConfig(config);\n if (configIssues.length > 0) {\n warnOnce(`[xicms] site.config.ts bevat problemen:\\n- ${configIssues.join(\"\\n- \")}`);\n }\n if (modules.length > 0) {\n const contractIssues = validateModuleContract(modules);\n if (contractIssues.length > 0) {\n warnOnce(`[xicms] klant-modules voldoen niet aan het module-contract:\\n- ${contractIssues.join(\"\\n- \")}`);\n }\n }\n }, [config, modules]);\n\n return (\n <ConfigProvider config={config}>\n <SiteSettingsProvider siteId={config.siteId} initial={settingsData}>\n <ModuleLayer config={config} blocks={blocks} modules={modules} overrides={overrides} astryxTheme={astryxTheme} linkComponent={linkComponent} imageComponent={imageComponent} customBlocksData={customBlocksData}>\n {children}\n </ModuleLayer>\n </SiteSettingsProvider>\n </ConfigProvider>\n );\n}\n\n/**\n * Theme-tokens → CSS custom properties. Puur; wordt in `ModuleLayer` op het LIVE-gemergde thema\n * toegepast (SiteConfig + `settings/public.theme`), zodat huisstijl-wijzigingen uit /beheer direct\n * op de site verschijnen — net als module-instellingen.\n */\nfunction themeCssVars(theme: ThemeTokens): Record<string, string> {\n // Dark-auto via light-dark(): deze variabelen staan INLINE op `.xicms-root`, en inline wint\n // altijd van het `@media (prefers-color-scheme: dark)`-blok in styles.css. Alleen dat blok kan\n // een inline lichte waarde dus nooit overrulen — de tokenwaarde zelf moet meeschakelen.\n // (styles.css zet `color-scheme: light dark` op `[data-dark=\"auto\"]`, vereist voor light-dark().)\n const auto = Boolean(theme.dark?.auto);\n const ld = (light: string, dark: string) => (auto ? `light-dark(${light}, ${dark})` : light);\n const cssVars: Record<string, string> = {\n \"--xicms-primary\": theme.dark?.primary ? ld(theme.primary, theme.dark.primary) : theme.primary,\n \"--xicms-accent\": theme.accent ?? theme.primary,\n \"--xicms-bg\": ld(theme.background ?? \"#ffffff\", theme.dark?.background ?? \"#0f172a\"),\n \"--xicms-fg\": ld(theme.foreground ?? \"#0f172a\", theme.dark?.foreground ?? \"#e2e8f0\"),\n };\n if (theme.fontFamily) cssVars[\"--xicms-font\"] = theme.fontFamily;\n // Optionele schalen → CSS-variabelen (alleen gezet als opgegeven; styles.css heeft defaults).\n for (const [k, v] of Object.entries(theme.spacing ?? {})) if (v) cssVars[`--xicms-spacing-${k}`] = v;\n if (theme.radius?.sm) cssVars[\"--xicms-radius-sm\"] = theme.radius.sm;\n if (theme.radius?.md) cssVars[\"--xicms-radius\"] = theme.radius.md;\n if (theme.radius?.lg) cssVars[\"--xicms-radius-lg\"] = theme.radius.lg;\n if (theme.container?.maxWidth) cssVars[\"--xicms-container-max\"] = theme.container.maxWidth;\n if (theme.container?.padding) cssVars[\"--xicms-container-pad\"] = theme.container.padding;\n if (theme.dark?.background) cssVars[\"--xicms-bg-dark\"] = theme.dark.background;\n if (theme.dark?.foreground) cssVars[\"--xicms-fg-dark\"] = theme.dark.foreground;\n if (theme.dark?.primary) cssVars[\"--xicms-primary-dark\"] = theme.dark.primary;\n\n // Astryx-tokens op XiCMS-waarden mappen, zodat de Astryx-componenten elke site z'n eigen merk\n // (kleur/typografie/radius) volgen. Inline op `.xicms-root` → wint over de theme.css-defaults.\n cssVars[\"--color-accent\"] = \"var(--xicms-primary)\";\n // Tekst óp de accentkleur: wit op een donkere merkkleur, donker op een lichte (geel/pastel) —\n // hardcoded wit maakte knop- en bannertekst onleesbaar op lichte primaire kleuren.\n const onAccent = isDarkColor(theme.primary) ? \"#ffffff\" : \"#0f172a\";\n cssVars[\"--color-on-accent\"] = onAccent;\n cssVars[\"--xicms-on-primary\"] = onAccent;\n cssVars[\"--color-text-accent\"] = \"var(--xicms-primary)\";\n cssVars[\"--radius-element\"] = \"var(--xicms-radius-sm, 8px)\";\n cssVars[\"--radius-container\"] = \"var(--xicms-radius, 14px)\";\n cssVars[\"--radius-page\"] = \"var(--xicms-radius-lg, 24px)\";\n // Volledige paletmapping — ALLEEN als de site een eigen achtergrond/tekst kiest. Zo verschilt niet\n // enkel het accent maar de hele sfeer (kaarten, borders, secundaire tekst) per site; een minimale\n // config blijft op de gepolijste neutrale basis (incl. automatische dark-mode via light-dark()).\n if (theme.background) {\n cssVars[\"--color-background-body\"] = \"var(--xicms-bg)\";\n cssVars[\"--color-background-surface\"] = \"color-mix(in srgb, var(--xicms-fg) 4%, var(--xicms-bg))\";\n cssVars[\"--color-background-card\"] = \"color-mix(in srgb, var(--xicms-fg) 3%, var(--xicms-bg))\";\n cssVars[\"--color-background-popover\"] = \"var(--xicms-bg)\";\n cssVars[\"--color-background-muted\"] = \"color-mix(in srgb, var(--xicms-fg) 6%, var(--xicms-bg))\";\n // Ook de XiCMS-oppervlaktetokens meeschakelen: hun defaults in styles.css zijn licht (#fff/zwart-\n // rgba) en het dark-blok vuurt alleen bij `data-dark=\"auto\"`. Zonder deze mapping tonen presets\n // met een vaste donkere achtergrond (donker-modern, luxe-donker) witte kaarten en zwarte borders.\n cssVars[\"--xicms-surface\"] = \"color-mix(in srgb, var(--xicms-fg) 3%, var(--xicms-bg))\";\n cssVars[\"--xicms-muted\"] = \"color-mix(in srgb, var(--xicms-fg) 5%, var(--xicms-bg))\";\n }\n if (theme.foreground) {\n cssVars[\"--color-text-primary\"] = \"var(--xicms-fg)\";\n cssVars[\"--color-text-secondary\"] = \"color-mix(in srgb, var(--xicms-fg) 65%, transparent)\";\n cssVars[\"--color-neutral\"] = \"var(--xicms-fg)\";\n cssVars[\"--color-border\"] = \"color-mix(in srgb, var(--xicms-fg) 14%, transparent)\";\n cssVars[\"--xicms-border\"] = \"color-mix(in srgb, var(--xicms-fg) 12%, transparent)\";\n cssVars[\"--xicms-border-strong\"] = \"color-mix(in srgb, var(--xicms-fg) 22%, transparent)\";\n }\n if (theme.headingFont) cssVars[\"--xicms-font-heading\"] = theme.headingFont;\n if (theme.fontFamily) {\n cssVars[\"--font-family-body\"] = \"var(--xicms-font)\";\n // Koppen volgen een apart display/serif-font als dat is opgegeven, anders het body-font.\n cssVars[\"--font-family-heading\"] = theme.headingFont ? \"var(--xicms-font-heading)\" : \"var(--xicms-font)\";\n } else if (theme.headingFont) {\n cssVars[\"--font-family-heading\"] = \"var(--xicms-font-heading)\";\n }\n\n return cssVars;\n}\n\n/**\n * Is deze (hex-)kleur donker? Bepaalt de Astryx-thememodus voor sites met een vaste donkere\n * achtergrond (bv. presets donker-modern/luxe-donker, `dark.auto: false`): zonder deze afleiding\n * bleef Astryx op \"light\" staan en tekenden zijn componenten lichte kaarten op een donkere pagina.\n * Puur en exporteerbaar voor tests; niet-hex waarden gelden als licht (veilige default).\n */\nexport function isDarkColor(color?: string): boolean {\n const m = /^#([0-9a-f]{3}|[0-9a-f]{6})$/i.exec(color?.trim() ?? \"\");\n if (!m) return false;\n let hex = m[1];\n if (hex.length === 3) hex = hex.replace(/./g, (c) => c + c);\n const [r, g, b] = [0, 2, 4].map((i) => parseInt(hex.slice(i, i + 2), 16) / 255);\n return 0.2126 * r + 0.7152 * g + 0.0722 * b < 0.45;\n}\n\n/**\n * Binnenlaag: leest de live publieke settings en resolvet daarmee de modules (settings/public.modules\n * legt zich over SiteConfig.modules) én het thema (settings/public.theme over SiteConfig.theme).\n * Zo werken module-aan/uit, -instellingen én huisstijl die de klant in /beheer zet direct door.\n */\nfunction ModuleLayer({\n config,\n blocks = NO_BLOCKS,\n modules = NO_MODULES,\n overrides,\n customBlocksData,\n astryxTheme,\n linkComponent,\n imageComponent,\n children,\n}: ProviderProps) {\n const { settings } = useSiteSettings();\n\n // LIVE thema: wijzigingen uit /beheer (settings/public.theme) winnen per token van SiteConfig.\n const theme = useMemo(() => mergeThemeTokens(config.theme, settings.theme), [config.theme, settings.theme]);\n const cssVars = useMemo(() => themeCssVars(theme), [theme]);\n\n // Demo-bewerkmodus globaal markeren zodat de (Firebase-)dataclients lokaal-only werken.\n // In een effect (niet tijdens render): render-pure + veilig onder concurrent rendering. De\n // demoStore-default is `false`, dus vóór het effect gedraagt alles zich als niet-demo.\n useEffect(() => {\n setDemoActive(Boolean(config.demoEditing));\n }, [config.demoEditing]);\n\n const resolved = useMemo(\n () =>\n resolveModules([...builtinModules, ...modules], {\n modules: config.modules,\n firestoreModules: settings.modules as ModulesSiteConfig | undefined,\n }),\n [config.modules, modules, settings.modules],\n );\n\n // Custom blocks (5.0): runtime-gedefinieerde bloktypes uit sites/{siteId}/customBlocks.\n // Publieke bezoekers krijgen UITSLUITEND de SSR-startset (customBlocksData, server-side gelezen) —\n // géén Firestore-listener per bezoeker (kosten/verbinding). Alleen ingelogde editors krijgen een\n // live sync (CustomBlocksSync, binnen AuthProvider) zodat een nieuw blok uit /beheer of een\n // agent-push zonder redeploy in het toevoeg-menu en op de pagina verschijnt.\n const [customBlocks, setCustomBlocks] = useState<CustomBlockData[]>(customBlocksData ?? []);\n const customDefs = useMemo(() => customBlocks.map(customBlockToDefinition), [customBlocks]);\n\n const registry = useMemo(\n () => applyBlockOverrides(mergeRegistry(mergeRegistry(createRegistry(resolved.blocks), customDefs), blocks), overrides),\n [resolved.blocks, customDefs, blocks, overrides],\n );\n\n return (\n <ModulesProvider value={resolved}>\n <RegistryProvider registry={registry}>\n <AuthProvider siteId={config.siteId} demo={Boolean(config.demoEditing)}>\n <CustomBlocksSync siteId={config.siteId} onData={setCustomBlocks} />\n <SiteSettingsSync siteId={config.siteId} />\n <EditorProvider>\n {/* React 19 hijst <link rel=\"stylesheet\"> automatisch naar <head> → lettertype laadt. */}\n {theme.fontUrl && <link rel=\"stylesheet\" href={theme.fontUrl} />}\n {/* Astryx-thema (Neutral basis) — de XiCMS-token-mapping op `.xicms-root` legt elke\n site z'n eigen merk eroverheen. */}\n <Theme theme={astryxTheme ?? neutralTheme} mode={theme.dark?.auto ? \"system\" : isDarkColor(theme.background) ? \"dark\" : \"light\"}>\n {/* Framework-router voor Astryx-links/knoppen (soepele client-nav + prefetch);\n zonder `linkComponent` valt het terug op een gewone `<a>` (huidig gedrag). */}\n <LinkProvider component={linkComponent ?? \"a\"}>\n <ImageProvider component={imageComponent}>\n <div\n className=\"xicms-root\"\n data-dark={theme.dark?.auto ? \"auto\" : undefined}\n style={cssVars as React.CSSProperties}\n >\n <DemoStudio />\n {children}\n <EditBar />\n <DemoLock />\n {resolved.activeIds.has(\"consent\") && (\n <ConsentBanner settings={resolved.settings.consent as ConsentSettings} />\n )}\n </div>\n </ImageProvider>\n </LinkProvider>\n </Theme>\n </EditorProvider>\n </AuthProvider>\n </RegistryProvider>\n </ModulesProvider>\n );\n}\n\n/**\n * Live sync van custom blocks — ALLEEN voor ingelogde editors. Publieke bezoekers krijgen de\n * SSR-startset (customBlocksData) en géén Firestore-listener: dat scheelt een collection-read +\n * open verbinding per bezoeker, conform het getDoc-voor-publiek-patroon van useSiteSettings/News.\n */\n/**\n * Live sync van de publieke site-instellingen — ALLEEN voor ingelogde editors (zelfde patroon als\n * CustomBlocksSync). Zo ziet een editor een /beheer-wijziging (footer/nav/thema) meteen op de site,\n * terwijl publiek op de SSR-seed (settingsData) of de eenmalige getDoc blijft draaien.\n */\nfunction SiteSettingsSync({ siteId }: { siteId: string }) {\n const { role } = useAuth();\n useSiteSettingsLiveSync(siteId, canEdit(role));\n return null;\n}\n\nfunction CustomBlocksSync({ siteId, onData }: { siteId: string; onData: (defs: CustomBlockData[]) => void }) {\n const { role } = useAuth();\n const editing = canEdit(role);\n useEffect(() => {\n if (!editing) return;\n const db = getDb();\n if (!db) return;\n return onSnapshot(\n collection(db, \"sites\", siteId, \"customBlocks\"),\n (snap) => {\n const defs: CustomBlockData[] = [];\n snap.forEach((d) => {\n const res = CustomBlockSchema.safeParse({ ...(d.data() as Record<string, unknown>), type: (d.data() as { type?: string }).type ?? d.id });\n if (res.success) defs.push(res.data);\n });\n onData(defs);\n },\n () => undefined, // leesfout → SSR-set blijft staan\n );\n }, [editing, siteId, onData]);\n return null;\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/XiCMSProvider.tsx"],"sourcesContent":["\"use client\";\n\nimport { useEffect, useMemo, useState, type ReactNode } from \"react\";\nimport { ConfigProvider } from \"./config\";\nimport { AuthProvider, useAuth } from \"./auth/AuthProvider\";\nimport { canEdit } from \"./auth/roles\";\nimport { EditBar } from \"./components/EditBar\";\nimport { FeedbackWidget } from \"./components/FeedbackWidget\";\nimport { DemoLock } from \"./lock/DemoLock\";\nimport { RegistryProvider } from \"./blocks/context\";\nimport { getDb } from \"./firebase/client\";\nimport { collection, onSnapshot } from \"firebase/firestore\";\nimport { createRegistry, mergeRegistry, type BlockDefinition } from \"./blocks/registry\";\nimport { CustomBlockSchema, type CustomBlock as CustomBlockData } from \"./schema/customBlocks\";\nimport { customBlockToDefinition } from \"./blocks/customTemplate/definition\";\nimport { resolveModules } from \"./modules/registry\";\nimport { ModulesProvider } from \"./modules/context\";\nimport { applyBlockOverrides, type BlockOverrides } from \"./modules/overrides\";\nimport { builtinModules } from \"./modules/builtins\";\nimport type { ModuleDefinition, ModulesSiteConfig } from \"./modules/types\";\nimport { ConsentBanner, type ConsentSettings } from \"./modules/consent/ConsentBanner\";\nimport { EditorProvider } from \"./editor/EditorProvider\";\nimport { SiteSettingsProvider, useSiteSettings, useSiteSettingsLiveSync } from \"./settings/useSiteSettings\";\nimport { mergeThemeTokens, type ThemeTokens, type PublicSiteSettings } from \"./schema/settings\";\nimport { initAppCheck } from \"./firebase/appcheck\";\nimport { DemoStudio } from \"./demo/DemoStudio\";\nimport { setDemoActive } from \"./demo/demoStore\";\nimport { Theme, type DefinedTheme } from \"@astryxdesign/core/theme\";\nimport { neutralTheme } from \"@astryxdesign/theme-neutral/built\";\nimport { LinkProvider, type LinkComponentType } from \"@astryxdesign/core/Link\";\nimport { ImageProvider, type ImageComponentType } from \"./media/ImageContext\";\nimport { validateSiteConfig, type SiteConfig } from \"./schema/siteConfig\";\nimport { validateModuleContract } from \"./modules/contract\";\n\ninterface ProviderProps {\n config: SiteConfig;\n /** Losse klant-blokken (via `defineBlock`) buiten een module om. Altijd actief. */\n blocks?: BlockDefinition[];\n /**\n * Klant-modules (via `defineModule`) bovenop de ingebouwde. Worden gemerged op `id` (klant wint)\n * en respecteren `config.modules` voor activatie.\n */\n modules?: ModuleDefinition[];\n /**\n * Per-site presentatie-overrides per block-type (`{ gallery: { Render: MijnGalerij } }`). Vervangt\n * alleen het uiterlijk; schema/editor/logica blijven van core. De krachtigste maatwerk-naad.\n */\n overrides?: BlockOverrides;\n /**\n * SSR-startset van custom blocks (5.0): geef het resultaat van `getSiteCustomBlocks(siteId)`\n * (server) door zodat runtime-gedefinieerde blokken al in de server-HTML zitten. Zonder deze\n * prop laadt de provider ze alsnog client-side (werkend, maar pas na hydratie zichtbaar).\n */\n customBlocksData?: CustomBlockData[];\n /**\n * SSR-startset van de publieke site-instellingen: geef het resultaat van\n * `getPublicSiteSettings(siteId)` (server) door zodat klant-thema/nav/footer al in de\n * server-HTML zitten (geen theme-flits bij eerste paint) én publiek geen client-read meer\n * doet — de ISR-gecachte layout ververst de settings per revalidate. Zonder deze prop laadt\n * de provider ze alsnog client-side (werkend, maar met mogelijke flits + 1 read per bezoeker).\n */\n settingsData?: PublicSiteSettings;\n /**\n * Astryx-basistheme voor de hele site (radius-/spacing-/neutralen-karakter). Standaard `neutral`.\n * Een site kan een ander pakket kiezen (`@astryxdesign/theme-butter`, `-stone`, …) of een eigen\n * `defineTheme(...)` doorgeven voor component-overrides — zónder core te forken.\n */\n astryxTheme?: DefinedTheme;\n /**\n * Framework-link-component (bv. Next `Link`) voor soepele client-navigatie + prefetch. Astryx-\n * componenten met een `href` (knoppen/links/tabs) gebruiken 'm dan i.p.v. een rauwe `<a>` (= volledige\n * page-reload). Core importeert `next/link` niet zelf (optionele peer) — geef 'm hier door vanuit de site.\n */\n linkComponent?: LinkComponentType;\n /**\n * Framework-beeldcomponent (bv. Next `Image`) voor automatische beeldoptimalisatie (responsive\n * srcset, AVIF/WebP, geen layout-shift). Core importeert `next/image` niet zelf — geef 'm hier door.\n */\n imageComponent?: ImageComponentType;\n children: ReactNode;\n}\n\n// Stabiele lege defaults: een `= []`-defaultparameter maakt bij ELKE render een nieuwe\n// array-referentie en laat memo's/effects met `modules`/`blocks` in de deps onnodig hervuren.\nconst NO_BLOCKS: BlockDefinition[] = [];\nconst NO_MODULES: ModuleDefinition[] = [];\n\n// Dev-waarschuwingen één keer per boodschap (React strict-mode dubbelt effects; zonder dedupe\n// verschijnt dezelfde config-waarschuwing telkens opnieuw in de console).\nconst warned = new Set<string>();\nfunction warnOnce(message: string) {\n if (warned.has(message)) return;\n warned.add(message);\n console.warn(message);\n}\n\n/**\n * Wikkel de hele site in deze provider (in `app/layout.tsx`). Hij zet:\n * - de SiteConfig-context (siteId, theme, mode) + live publieke settings (footer/nav/modules);\n * - auth + content (Firebase, env-gedreven; werkt ook zonder Firebase);\n * - de block-registry (ingebouwde + klant-blokken) voor de page-renderer;\n * - de themavariabelen als CSS custom properties;\n * - App Check (alleen als geconfigureerd) en de EditBar/DemoLock.\n */\nexport function XiCMSProvider({ config, blocks = NO_BLOCKS, modules = NO_MODULES, overrides, astryxTheme, linkComponent, imageComponent, customBlocksData, settingsData, children }: ProviderProps) {\n useEffect(() => {\n initAppCheck();\n }, []);\n\n // Dev-vangnet voor (agent-)geschreven configs: meld config- en module-contractfouten meteen in\n // de console i.p.v. een subtiel kapotte site. In productie geen werk — en er wordt nooit gegooid.\n useEffect(() => {\n if (process.env.NODE_ENV === \"production\") return;\n const configIssues = validateSiteConfig(config);\n if (configIssues.length > 0) {\n warnOnce(`[xicms] site.config.ts bevat problemen:\\n- ${configIssues.join(\"\\n- \")}`);\n }\n if (modules.length > 0) {\n const contractIssues = validateModuleContract(modules);\n if (contractIssues.length > 0) {\n warnOnce(`[xicms] klant-modules voldoen niet aan het module-contract:\\n- ${contractIssues.join(\"\\n- \")}`);\n }\n }\n }, [config, modules]);\n\n return (\n <ConfigProvider config={config}>\n <SiteSettingsProvider siteId={config.siteId} initial={settingsData}>\n <ModuleLayer config={config} blocks={blocks} modules={modules} overrides={overrides} astryxTheme={astryxTheme} linkComponent={linkComponent} imageComponent={imageComponent} customBlocksData={customBlocksData}>\n {children}\n </ModuleLayer>\n </SiteSettingsProvider>\n </ConfigProvider>\n );\n}\n\n/**\n * Theme-tokens → CSS custom properties. Puur; wordt in `ModuleLayer` op het LIVE-gemergde thema\n * toegepast (SiteConfig + `settings/public.theme`), zodat huisstijl-wijzigingen uit /beheer direct\n * op de site verschijnen — net als module-instellingen.\n */\nfunction themeCssVars(theme: ThemeTokens): Record<string, string> {\n // Dark-auto via light-dark(): deze variabelen staan INLINE op `.xicms-root`, en inline wint\n // altijd van het `@media (prefers-color-scheme: dark)`-blok in styles.css. Alleen dat blok kan\n // een inline lichte waarde dus nooit overrulen — de tokenwaarde zelf moet meeschakelen.\n // (styles.css zet `color-scheme: light dark` op `[data-dark=\"auto\"]`, vereist voor light-dark().)\n const auto = Boolean(theme.dark?.auto);\n const ld = (light: string, dark: string) => (auto ? `light-dark(${light}, ${dark})` : light);\n const cssVars: Record<string, string> = {\n \"--xicms-primary\": theme.dark?.primary ? ld(theme.primary, theme.dark.primary) : theme.primary,\n \"--xicms-accent\": theme.accent ?? theme.primary,\n \"--xicms-bg\": ld(theme.background ?? \"#ffffff\", theme.dark?.background ?? \"#0f172a\"),\n \"--xicms-fg\": ld(theme.foreground ?? \"#0f172a\", theme.dark?.foreground ?? \"#e2e8f0\"),\n };\n if (theme.fontFamily) cssVars[\"--xicms-font\"] = theme.fontFamily;\n // Optionele schalen → CSS-variabelen (alleen gezet als opgegeven; styles.css heeft defaults).\n for (const [k, v] of Object.entries(theme.spacing ?? {})) if (v) cssVars[`--xicms-spacing-${k}`] = v;\n if (theme.radius?.sm) cssVars[\"--xicms-radius-sm\"] = theme.radius.sm;\n if (theme.radius?.md) cssVars[\"--xicms-radius\"] = theme.radius.md;\n if (theme.radius?.lg) cssVars[\"--xicms-radius-lg\"] = theme.radius.lg;\n if (theme.container?.maxWidth) cssVars[\"--xicms-container-max\"] = theme.container.maxWidth;\n if (theme.container?.padding) cssVars[\"--xicms-container-pad\"] = theme.container.padding;\n if (theme.dark?.background) cssVars[\"--xicms-bg-dark\"] = theme.dark.background;\n if (theme.dark?.foreground) cssVars[\"--xicms-fg-dark\"] = theme.dark.foreground;\n if (theme.dark?.primary) cssVars[\"--xicms-primary-dark\"] = theme.dark.primary;\n\n // Astryx-tokens op XiCMS-waarden mappen, zodat de Astryx-componenten elke site z'n eigen merk\n // (kleur/typografie/radius) volgen. Inline op `.xicms-root` → wint over de theme.css-defaults.\n cssVars[\"--color-accent\"] = \"var(--xicms-primary)\";\n // Tekst óp de accentkleur: wit op een donkere merkkleur, donker op een lichte (geel/pastel) —\n // hardcoded wit maakte knop- en bannertekst onleesbaar op lichte primaire kleuren.\n const onAccent = isDarkColor(theme.primary) ? \"#ffffff\" : \"#0f172a\";\n cssVars[\"--color-on-accent\"] = onAccent;\n cssVars[\"--xicms-on-primary\"] = onAccent;\n cssVars[\"--color-text-accent\"] = \"var(--xicms-primary)\";\n cssVars[\"--radius-element\"] = \"var(--xicms-radius-sm, 8px)\";\n cssVars[\"--radius-container\"] = \"var(--xicms-radius, 14px)\";\n cssVars[\"--radius-page\"] = \"var(--xicms-radius-lg, 24px)\";\n // Volledige paletmapping — ALLEEN als de site een eigen achtergrond/tekst kiest. Zo verschilt niet\n // enkel het accent maar de hele sfeer (kaarten, borders, secundaire tekst) per site; een minimale\n // config blijft op de gepolijste neutrale basis (incl. automatische dark-mode via light-dark()).\n if (theme.background) {\n cssVars[\"--color-background-body\"] = \"var(--xicms-bg)\";\n cssVars[\"--color-background-surface\"] = \"color-mix(in srgb, var(--xicms-fg) 4%, var(--xicms-bg))\";\n cssVars[\"--color-background-card\"] = \"color-mix(in srgb, var(--xicms-fg) 3%, var(--xicms-bg))\";\n cssVars[\"--color-background-popover\"] = \"var(--xicms-bg)\";\n cssVars[\"--color-background-muted\"] = \"color-mix(in srgb, var(--xicms-fg) 6%, var(--xicms-bg))\";\n // Ook de XiCMS-oppervlaktetokens meeschakelen: hun defaults in styles.css zijn licht (#fff/zwart-\n // rgba) en het dark-blok vuurt alleen bij `data-dark=\"auto\"`. Zonder deze mapping tonen presets\n // met een vaste donkere achtergrond (donker-modern, luxe-donker) witte kaarten en zwarte borders.\n cssVars[\"--xicms-surface\"] = \"color-mix(in srgb, var(--xicms-fg) 3%, var(--xicms-bg))\";\n cssVars[\"--xicms-muted\"] = \"color-mix(in srgb, var(--xicms-fg) 5%, var(--xicms-bg))\";\n }\n if (theme.foreground) {\n cssVars[\"--color-text-primary\"] = \"var(--xicms-fg)\";\n cssVars[\"--color-text-secondary\"] = \"color-mix(in srgb, var(--xicms-fg) 65%, transparent)\";\n cssVars[\"--color-neutral\"] = \"var(--xicms-fg)\";\n cssVars[\"--color-border\"] = \"color-mix(in srgb, var(--xicms-fg) 14%, transparent)\";\n cssVars[\"--xicms-border\"] = \"color-mix(in srgb, var(--xicms-fg) 12%, transparent)\";\n cssVars[\"--xicms-border-strong\"] = \"color-mix(in srgb, var(--xicms-fg) 22%, transparent)\";\n }\n if (theme.headingFont) cssVars[\"--xicms-font-heading\"] = theme.headingFont;\n if (theme.fontFamily) {\n cssVars[\"--font-family-body\"] = \"var(--xicms-font)\";\n // Koppen volgen een apart display/serif-font als dat is opgegeven, anders het body-font.\n cssVars[\"--font-family-heading\"] = theme.headingFont ? \"var(--xicms-font-heading)\" : \"var(--xicms-font)\";\n } else if (theme.headingFont) {\n cssVars[\"--font-family-heading\"] = \"var(--xicms-font-heading)\";\n }\n\n return cssVars;\n}\n\n/**\n * Is deze (hex-)kleur donker? Bepaalt de Astryx-thememodus voor sites met een vaste donkere\n * achtergrond (bv. presets donker-modern/luxe-donker, `dark.auto: false`): zonder deze afleiding\n * bleef Astryx op \"light\" staan en tekenden zijn componenten lichte kaarten op een donkere pagina.\n * Puur en exporteerbaar voor tests; niet-hex waarden gelden als licht (veilige default).\n */\nexport function isDarkColor(color?: string): boolean {\n const m = /^#([0-9a-f]{3}|[0-9a-f]{6})$/i.exec(color?.trim() ?? \"\");\n if (!m) return false;\n let hex = m[1];\n if (hex.length === 3) hex = hex.replace(/./g, (c) => c + c);\n const [r, g, b] = [0, 2, 4].map((i) => parseInt(hex.slice(i, i + 2), 16) / 255);\n return 0.2126 * r + 0.7152 * g + 0.0722 * b < 0.45;\n}\n\n/**\n * Binnenlaag: leest de live publieke settings en resolvet daarmee de modules (settings/public.modules\n * legt zich over SiteConfig.modules) én het thema (settings/public.theme over SiteConfig.theme).\n * Zo werken module-aan/uit, -instellingen én huisstijl die de klant in /beheer zet direct door.\n */\nfunction ModuleLayer({\n config,\n blocks = NO_BLOCKS,\n modules = NO_MODULES,\n overrides,\n customBlocksData,\n astryxTheme,\n linkComponent,\n imageComponent,\n children,\n}: ProviderProps) {\n const { settings } = useSiteSettings();\n\n // LIVE thema: wijzigingen uit /beheer (settings/public.theme) winnen per token van SiteConfig.\n const theme = useMemo(() => mergeThemeTokens(config.theme, settings.theme), [config.theme, settings.theme]);\n const cssVars = useMemo(() => themeCssVars(theme), [theme]);\n\n // Demo-bewerkmodus globaal markeren zodat de (Firebase-)dataclients lokaal-only werken.\n // In een effect (niet tijdens render): render-pure + veilig onder concurrent rendering. De\n // demoStore-default is `false`, dus vóór het effect gedraagt alles zich als niet-demo.\n useEffect(() => {\n setDemoActive(Boolean(config.demoEditing));\n }, [config.demoEditing]);\n\n const resolved = useMemo(\n () =>\n resolveModules([...builtinModules, ...modules], {\n modules: config.modules,\n firestoreModules: settings.modules as ModulesSiteConfig | undefined,\n }),\n [config.modules, modules, settings.modules],\n );\n\n // Custom blocks (5.0): runtime-gedefinieerde bloktypes uit sites/{siteId}/customBlocks.\n // Publieke bezoekers krijgen UITSLUITEND de SSR-startset (customBlocksData, server-side gelezen) —\n // géén Firestore-listener per bezoeker (kosten/verbinding). Alleen ingelogde editors krijgen een\n // live sync (CustomBlocksSync, binnen AuthProvider) zodat een nieuw blok uit /beheer of een\n // agent-push zonder redeploy in het toevoeg-menu en op de pagina verschijnt.\n const [customBlocks, setCustomBlocks] = useState<CustomBlockData[]>(customBlocksData ?? []);\n const customDefs = useMemo(() => customBlocks.map(customBlockToDefinition), [customBlocks]);\n\n const registry = useMemo(\n () => applyBlockOverrides(mergeRegistry(mergeRegistry(createRegistry(resolved.blocks), customDefs), blocks), overrides),\n [resolved.blocks, customDefs, blocks, overrides],\n );\n\n return (\n <ModulesProvider value={resolved}>\n <RegistryProvider registry={registry}>\n <AuthProvider siteId={config.siteId} demo={Boolean(config.demoEditing)}>\n <CustomBlocksSync siteId={config.siteId} onData={setCustomBlocks} />\n <SiteSettingsSync siteId={config.siteId} />\n <EditorProvider>\n {/* React 19 hijst <link rel=\"stylesheet\"> automatisch naar <head> → lettertype laadt. */}\n {theme.fontUrl && <link rel=\"stylesheet\" href={theme.fontUrl} />}\n {/* Astryx-thema (Neutral basis) — de XiCMS-token-mapping op `.xicms-root` legt elke\n site z'n eigen merk eroverheen. */}\n <Theme theme={astryxTheme ?? neutralTheme} mode={theme.dark?.auto ? \"system\" : isDarkColor(theme.background) ? \"dark\" : \"light\"}>\n {/* Framework-router voor Astryx-links/knoppen (soepele client-nav + prefetch);\n zonder `linkComponent` valt het terug op een gewone `<a>` (huidig gedrag). */}\n <LinkProvider component={linkComponent ?? \"a\"}>\n <ImageProvider component={imageComponent}>\n <div\n className=\"xicms-root\"\n data-dark={theme.dark?.auto ? \"auto\" : undefined}\n style={cssVars as React.CSSProperties}\n >\n <DemoStudio />\n {children}\n <EditBar />\n <FeedbackWidget siteId={config.siteId} />\n <DemoLock />\n {resolved.activeIds.has(\"consent\") && (\n <ConsentBanner settings={resolved.settings.consent as ConsentSettings} />\n )}\n </div>\n </ImageProvider>\n </LinkProvider>\n </Theme>\n </EditorProvider>\n </AuthProvider>\n </RegistryProvider>\n </ModulesProvider>\n );\n}\n\n/**\n * Live sync van custom blocks — ALLEEN voor ingelogde editors. Publieke bezoekers krijgen de\n * SSR-startset (customBlocksData) en géén Firestore-listener: dat scheelt een collection-read +\n * open verbinding per bezoeker, conform het getDoc-voor-publiek-patroon van useSiteSettings/News.\n */\n/**\n * Live sync van de publieke site-instellingen — ALLEEN voor ingelogde editors (zelfde patroon als\n * CustomBlocksSync). Zo ziet een editor een /beheer-wijziging (footer/nav/thema) meteen op de site,\n * terwijl publiek op de SSR-seed (settingsData) of de eenmalige getDoc blijft draaien.\n */\nfunction SiteSettingsSync({ siteId }: { siteId: string }) {\n const { role } = useAuth();\n useSiteSettingsLiveSync(siteId, canEdit(role));\n return null;\n}\n\nfunction CustomBlocksSync({ siteId, onData }: { siteId: string; onData: (defs: CustomBlockData[]) => void }) {\n const { role } = useAuth();\n const editing = canEdit(role);\n useEffect(() => {\n if (!editing) return;\n const db = getDb();\n if (!db) return;\n return onSnapshot(\n collection(db, \"sites\", siteId, \"customBlocks\"),\n (snap) => {\n const defs: CustomBlockData[] = [];\n snap.forEach((d) => {\n const res = CustomBlockSchema.safeParse({ ...(d.data() as Record<string, unknown>), type: (d.data() as { type?: string }).type ?? d.id });\n if (res.success) defs.push(res.data);\n });\n onData(defs);\n },\n () => undefined, // leesfout → SSR-set blijft staan\n );\n }, [editing, siteId, onData]);\n return null;\n}\n"],"mappings":"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","names":[]}
@@ -18,6 +18,8 @@ import { Select } from "../ui/select";
18
18
  import { Switch } from "../ui/switch";
19
19
  import { useToast } from "../ui/toast";
20
20
  import { useConfirm } from "../ui/ConfirmDialog";
21
+ import { MailTemplates } from "../settings/MailTemplates";
22
+ import { FORM_TEMPLATE_KEYS } from "../../schema/mail";
21
23
  const FIELD_TYPES = [
22
24
  { value: "text", label: "Tekst" },
23
25
  { value: "email", label: "E-mail" },
@@ -53,31 +55,42 @@ function FormsPanel({ siteId }) {
53
55
  });
54
56
  setSelected(ref.id);
55
57
  }
56
- return /* @__PURE__ */ jsxs("div", { className: "grid gap-4 md:grid-cols-[260px_1fr]", children: [
57
- /* @__PURE__ */ jsxs(Card, { className: "h-fit", children: [
58
- /* @__PURE__ */ jsxs("div", { className: "mb-3 flex items-center justify-between", children: [
59
- /* @__PURE__ */ jsx("h3", { className: "text-sm font-semibold", children: "Formulieren" }),
60
- /* @__PURE__ */ jsx(Button, { size: "sm", onClick: createForm, children: "+ Nieuw" })
58
+ return /* @__PURE__ */ jsxs("div", { className: "grid gap-6", children: [
59
+ /* @__PURE__ */ jsxs("div", { className: "grid gap-4 md:grid-cols-[260px_1fr]", children: [
60
+ /* @__PURE__ */ jsxs(Card, { className: "h-fit", children: [
61
+ /* @__PURE__ */ jsxs("div", { className: "mb-3 flex items-center justify-between", children: [
62
+ /* @__PURE__ */ jsx("h3", { className: "text-sm font-semibold", children: "Formulieren" }),
63
+ /* @__PURE__ */ jsx(Button, { size: "sm", onClick: createForm, children: "+ Nieuw" })
64
+ ] }),
65
+ /* @__PURE__ */ jsxs("ul", { className: "grid gap-1", children: [
66
+ forms.map((f) => /* @__PURE__ */ jsx("li", { children: /* @__PURE__ */ jsxs(
67
+ "button",
68
+ {
69
+ onClick: () => setSelected(f.id),
70
+ className: `w-full truncate rounded-lg px-3 py-2 text-left text-sm ${f.id === selected ? "bg-brand-50 text-brand-700" : "hover:bg-slate-100"}`,
71
+ children: [
72
+ f.name,
73
+ /* @__PURE__ */ jsxs("span", { className: "ml-1 text-xs text-slate-400", children: [
74
+ "#",
75
+ f.id.slice(0, 5)
76
+ ] })
77
+ ]
78
+ }
79
+ ) }, f.id)),
80
+ forms.length === 0 && /* @__PURE__ */ jsx("li", { className: "px-3 py-2 text-sm text-slate-500", children: "Nog geen formulieren." })
81
+ ] })
61
82
  ] }),
62
- /* @__PURE__ */ jsxs("ul", { className: "grid gap-1", children: [
63
- forms.map((f) => /* @__PURE__ */ jsx("li", { children: /* @__PURE__ */ jsxs(
64
- "button",
65
- {
66
- onClick: () => setSelected(f.id),
67
- className: `w-full truncate rounded-lg px-3 py-2 text-left text-sm ${f.id === selected ? "bg-brand-50 text-brand-700" : "hover:bg-slate-100"}`,
68
- children: [
69
- f.name,
70
- /* @__PURE__ */ jsxs("span", { className: "ml-1 text-xs text-slate-400", children: [
71
- "#",
72
- f.id.slice(0, 5)
73
- ] })
74
- ]
75
- }
76
- ) }, f.id)),
77
- forms.length === 0 && /* @__PURE__ */ jsx("li", { className: "px-3 py-2 text-sm text-slate-500", children: "Nog geen formulieren." })
78
- ] })
83
+ current ? /* @__PURE__ */ jsx(FormDetail, { siteId, form: current, onDeleted: () => setSelected(null), onToast: toast }, current.id) : /* @__PURE__ */ jsx(Card, { children: /* @__PURE__ */ jsx("p", { className: "text-sm text-slate-500", children: "Selecteer of maak een formulier." }) })
79
84
  ] }),
80
- current ? /* @__PURE__ */ jsx(FormDetail, { siteId, form: current, onDeleted: () => setSelected(null), onToast: toast }, current.id) : /* @__PURE__ */ jsx(Card, { children: /* @__PURE__ */ jsx("p", { className: "text-sm text-slate-500", children: "Selecteer of maak een formulier." }) })
85
+ /* @__PURE__ */ jsxs("div", { children: [
86
+ /* @__PURE__ */ jsx("h3", { className: "mb-1 text-base font-semibold", children: "E-mailteksten voor formulieren" }),
87
+ /* @__PURE__ */ jsxs("p", { className: "mb-3 text-sm text-slate-600", children: [
88
+ "Gelden voor \xE1lle formulieren: de notificatie naar jou en de bevestiging naar de invuller. De opmaak (huisstijl) en verzendprovider stel je site-breed in bij ",
89
+ /* @__PURE__ */ jsx("strong", { children: "Instellingen \u2192 E-mail" }),
90
+ "."
91
+ ] }),
92
+ /* @__PURE__ */ jsx(MailTemplates, { siteId, keys: FORM_TEMPLATE_KEYS })
93
+ ] })
81
94
  ] });
82
95
  }
83
96
  function FormDetail({
@@ -125,9 +138,28 @@ function FormDetail({
125
138
  /* @__PURE__ */ jsx(Field, { label: "Naam", children: /* @__PURE__ */ jsx(Input, { value: def.name, onChange: (e) => setDef({ ...def, name: e.target.value }) }) }),
126
139
  /* @__PURE__ */ jsx(Field, { label: "Verstuurknop", children: /* @__PURE__ */ jsx(Input, { value: def.submitLabel, onChange: (e) => setDef({ ...def, submitLabel: e.target.value }) }) }),
127
140
  /* @__PURE__ */ jsx(Field, { label: "Bedankbericht", children: /* @__PURE__ */ jsx(Input, { value: def.successMessage, onChange: (e) => setDef({ ...def, successMessage: e.target.value }) }) }),
128
- /* @__PURE__ */ jsxs("div", { className: "grid grid-cols-2 items-end gap-2", children: [
129
- /* @__PURE__ */ jsx(Field, { label: "Bedankt-pagina (optioneel)", hint: "Na verzenden doorsturen naar dit pad, bv. /bedankt", children: /* @__PURE__ */ jsx(Input, { value: def.successRedirect ?? "", onChange: (e) => setDef({ ...def, successRedirect: e.target.value || void 0 }) }) }),
130
- /* @__PURE__ */ jsxs("label", { className: "flex h-9 items-center gap-2 text-sm", children: [
141
+ /* @__PURE__ */ jsx(Field, { label: "Bedankt-pagina (optioneel)", hint: "Na verzenden doorsturen naar dit pad, bv. /bedankt", children: /* @__PURE__ */ jsx(Input, { value: def.successRedirect ?? "", onChange: (e) => setDef({ ...def, successRedirect: e.target.value || void 0 }) }) })
142
+ ] }),
143
+ /* @__PURE__ */ jsxs("div", { className: "mt-4 rounded-lg border border-slate-200 p-4", children: [
144
+ /* @__PURE__ */ jsx("h4", { className: "text-sm font-semibold", children: "Wat gebeurt er bij een inzending?" }),
145
+ /* @__PURE__ */ jsxs("p", { className: "mb-3 mt-0.5 text-xs text-slate-500", children: [
146
+ "Elke inzending wordt altijd bewaard onder ",
147
+ /* @__PURE__ */ jsx("strong", { children: "Inzendingen" }),
148
+ ". Daarnaast kun je een kopie mailen naar jezelf en/of een bevestiging naar de invuller. De opmaak en verzendprovider stel je site-breed in bij ",
149
+ /* @__PURE__ */ jsx("strong", { children: "Instellingen \u2192 E-mail" }),
150
+ "."
151
+ ] }),
152
+ /* @__PURE__ */ jsxs("div", { className: "grid gap-3 md:grid-cols-2", children: [
153
+ /* @__PURE__ */ jsx(Field, { label: "Kopie van inzending naar", hint: "Leeg = het notificatie-adres uit Instellingen \u2192 E-mail.", children: /* @__PURE__ */ jsx(
154
+ Input,
155
+ {
156
+ type: "email",
157
+ value: def.notifyTo ?? "",
158
+ onChange: (e) => setDef({ ...def, notifyTo: e.target.value || void 0 }),
159
+ placeholder: "jij@voorbeeld.nl"
160
+ }
161
+ ) }),
162
+ /* @__PURE__ */ jsxs("label", { className: "mt-6 flex h-9 items-center gap-2 text-sm", children: [
131
163
  /* @__PURE__ */ jsx(
132
164
  "input",
133
165
  {
@@ -136,7 +168,7 @@ function FormDetail({
136
168
  onChange: (e) => setDef({ ...def, confirmSubmitter: e.target.checked })
137
169
  }
138
170
  ),
139
- "Bevestigingsmail naar inzender (eerste e-mailveld)"
171
+ "Stuur de invuller ook een bevestiging (naar het eerste e-mailveld)"
140
172
  ] })
141
173
  ] })
142
174
  ] }),
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/admin/forms/FormsPanel.tsx"],"sourcesContent":["\"use client\";\n\nimport { useEffect, useState } from \"react\";\nimport {\n addDoc,\n collection,\n deleteDoc,\n doc,\n onSnapshot,\n orderBy,\n query,\n serverTimestamp,\n setDoc,\n} from \"firebase/firestore\";\nimport { getDb } from \"../../firebase/client\";\nimport { FormSchema, type FormDefinition, type FormField, type FieldType } from \"../../schema/forms\";\nimport { Button, Card, Field, Input, Badge } from \"../ui/primitives\";\nimport { Select } from \"../ui/select\";\nimport { Switch } from \"../ui/switch\";\nimport { useToast } from \"../ui/toast\";\nimport { useConfirm } from \"../ui/ConfirmDialog\";\n\ntype StoredForm = FormDefinition & { id: string };\n\nconst FIELD_TYPES: { value: FieldType; label: string }[] = [\n { value: \"text\", label: \"Tekst\" },\n { value: \"email\", label: \"E-mail\" },\n { value: \"tel\", label: \"Telefoon\" },\n { value: \"textarea\", label: \"Tekstvak\" },\n { value: \"select\", label: \"Keuzelijst\" },\n { value: \"radio\", label: \"Keuzerondjes\" },\n { value: \"checkbox\", label: \"Vinkje\" },\n { value: \"date\", label: \"Datum\" },\n { value: \"number\", label: \"Getal\" },\n];\n\nexport function FormsPanel({ siteId }: { siteId: string }) {\n const { toast } = useToast();\n const [forms, setForms] = useState<StoredForm[]>([]);\n const [selected, setSelected] = useState<string | null>(null);\n\n useEffect(() => {\n const db = getDb();\n if (!db) return;\n return onSnapshot(collection(db, \"sites\", siteId, \"forms\"), (snap) =>\n setForms(snap.docs.map((d) => ({ id: d.id, ...(d.data() as Omit<StoredForm, \"id\">) }))),\n );\n }, [siteId]);\n\n const current = forms.find((f) => f.id === selected) ?? null;\n\n async function createForm() {\n const db = getDb();\n if (!db) return;\n const ref = await addDoc(collection(db, \"sites\", siteId, \"forms\"), {\n name: \"Nieuw formulier\",\n fields: [{ name: \"naam\", label: \"Naam\", type: \"text\", required: true }],\n submitLabel: \"Versturen\",\n successMessage: \"Bedankt! We nemen snel contact op.\",\n } satisfies FormDefinition);\n setSelected(ref.id);\n }\n\n return (\n <div className=\"grid gap-4 md:grid-cols-[260px_1fr]\">\n <Card className=\"h-fit\">\n <div className=\"mb-3 flex items-center justify-between\">\n <h3 className=\"text-sm font-semibold\">Formulieren</h3>\n <Button size=\"sm\" onClick={createForm}>+ Nieuw</Button>\n </div>\n <ul className=\"grid gap-1\">\n {forms.map((f) => (\n <li key={f.id}>\n <button\n onClick={() => setSelected(f.id)}\n className={`w-full truncate rounded-lg px-3 py-2 text-left text-sm ${\n f.id === selected ? \"bg-brand-50 text-brand-700\" : \"hover:bg-slate-100\"\n }`}\n >\n {f.name}\n <span className=\"ml-1 text-xs text-slate-400\">#{f.id.slice(0, 5)}</span>\n </button>\n </li>\n ))}\n {forms.length === 0 && <li className=\"px-3 py-2 text-sm text-slate-500\">Nog geen formulieren.</li>}\n </ul>\n </Card>\n {current ? (\n <FormDetail key={current.id} siteId={siteId} form={current} onDeleted={() => setSelected(null)} onToast={toast} />\n ) : (\n <Card>\n <p className=\"text-sm text-slate-500\">Selecteer of maak een formulier.</p>\n </Card>\n )}\n </div>\n );\n}\n\nfunction FormDetail({\n siteId,\n form,\n onDeleted,\n onToast,\n}: {\n siteId: string;\n form: StoredForm;\n onDeleted: () => void;\n onToast: (t: { title: string; description?: string; tone?: \"default\" | \"success\" | \"error\" }) => void;\n}) {\n const [def, setDef] = useState<FormDefinition>(form);\n const confirm = useConfirm();\n\n async function save() {\n const db = getDb();\n if (!db) return;\n const parsed = FormSchema.safeParse(def);\n if (!parsed.success) {\n onToast({ title: \"Ongeldig formulier\", description: parsed.error.issues[0]?.message, tone: \"error\" });\n return;\n }\n await setDoc(doc(db, \"sites\", siteId, \"forms\", form.id), parsed.data, { merge: true });\n onToast({ title: \"Opgeslagen\", tone: \"success\" });\n }\n\n async function remove() {\n const db = getDb();\n if (!db) return;\n const ok = await confirm({ title: \"Formulier verwijderen?\", description: \"Het formulier wordt verwijderd. Inzendingen blijven bewaard.\", confirmLabel: \"Verwijderen\", tone: \"danger\" });\n if (!ok) return;\n await deleteDoc(doc(db, \"sites\", siteId, \"forms\", form.id));\n onDeleted();\n }\n\n function setField(i: number, part: Partial<FormField>) {\n const fields = [...def.fields];\n fields[i] = { ...fields[i], ...part };\n setDef({ ...def, fields });\n }\n\n return (\n <div className=\"grid gap-4\">\n <Card>\n <div className=\"mb-3 flex items-center justify-between\">\n <h3 className=\"text-base font-semibold\">Formulier bewerken</h3>\n <div className=\"flex gap-2\">\n <Button variant=\"danger\" size=\"sm\" onClick={remove}>Verwijderen</Button>\n <Button size=\"sm\" onClick={save}>Opslaan</Button>\n </div>\n </div>\n <div className=\"grid gap-3 md:grid-cols-2\">\n <Field label=\"Naam\"><Input value={def.name} onChange={(e) => setDef({ ...def, name: e.target.value })} /></Field>\n <Field label=\"Verstuurknop\"><Input value={def.submitLabel} onChange={(e) => setDef({ ...def, submitLabel: e.target.value })} /></Field>\n <Field label=\"Bedankbericht\"><Input value={def.successMessage} onChange={(e) => setDef({ ...def, successMessage: e.target.value })} /></Field>\n <div className=\"grid grid-cols-2 items-end gap-2\">\n <Field label=\"Bedankt-pagina (optioneel)\" hint=\"Na verzenden doorsturen naar dit pad, bv. /bedankt\">\n <Input value={def.successRedirect ?? \"\"} onChange={(e) => setDef({ ...def, successRedirect: e.target.value || undefined })} />\n </Field>\n <label className=\"flex h-9 items-center gap-2 text-sm\">\n <input\n type=\"checkbox\"\n checked={Boolean(def.confirmSubmitter)}\n onChange={(e) => setDef({ ...def, confirmSubmitter: e.target.checked })}\n />\n Bevestigingsmail naar inzender (eerste e-mailveld)\n </label>\n </div>\n </div>\n <p className=\"mt-3 text-xs text-slate-500\">Formulier-id (voor het Formulier-blok): <code>{form.id}</code></p>\n </Card>\n\n <Card>\n <h4 className=\"mb-3 text-sm font-semibold\">Velden</h4>\n <div className=\"grid gap-3\">\n {def.fields.map((f, i) => (\n <div key={i} className=\"grid items-end gap-2 rounded-lg border border-slate-200 p-3 md:grid-cols-[1fr_1fr_140px_auto_auto]\">\n <Field label=\"Naam (sleutel)\"><Input value={f.name} onChange={(e) => setField(i, { name: e.target.value })} /></Field>\n <Field label=\"Label\"><Input value={f.label} onChange={(e) => setField(i, { label: e.target.value })} /></Field>\n <Field label=\"Type\">\n <Select value={f.type} onValueChange={(v) => setField(i, { type: v as FieldType })} options={FIELD_TYPES} />\n </Field>\n <Switch checked={f.required} onCheckedChange={(v) => setField(i, { required: v })} label=\"Verplicht\" />\n <Button variant=\"ghost\" size=\"sm\" onClick={() => setDef({ ...def, fields: def.fields.filter((_, idx) => idx !== i) })}>🗑</Button>\n {(f.type === \"select\" || f.type === \"radio\") && (\n <div className=\"md:col-span-5\">\n <Field label=\"Opties (komma-gescheiden)\">\n <Input\n value={(f.options ?? []).join(\", \")}\n onChange={(e) => setField(i, { options: e.target.value.split(\",\").map((s) => s.trim()).filter(Boolean) })}\n />\n </Field>\n </div>\n )}\n </div>\n ))}\n </div>\n <div className=\"mt-3\">\n <Button\n variant=\"secondary\"\n size=\"sm\"\n onClick={() => setDef({ ...def, fields: [...def.fields, { name: `veld${def.fields.length + 1}`, label: \"Nieuw veld\", type: \"text\", required: false }] })}\n >\n + Veld toevoegen\n </Button>\n </div>\n </Card>\n\n <Submissions siteId={siteId} formId={form.id} />\n </div>\n );\n}\n\nfunction Submissions({ siteId, formId }: { siteId: string; formId: string }) {\n const [subs, setSubs] = useState<{ id: string; values: Record<string, unknown>; createdAt?: unknown }[]>([]);\n useEffect(() => {\n const db = getDb();\n if (!db) return;\n const q = query(collection(db, \"sites\", siteId, \"forms\", formId, \"submissions\"), orderBy(\"createdAt\", \"desc\"));\n return onSnapshot(q, (snap) => setSubs(snap.docs.map((d) => ({ id: d.id, ...(d.data() as { values: Record<string, unknown> }) }))));\n }, [siteId, formId]);\n\n return (\n <Card>\n <h4 className=\"mb-3 text-sm font-semibold\">Inzendingen <Badge>{subs.length}</Badge></h4>\n {subs.length === 0 ? (\n <p className=\"text-sm text-slate-500\">Nog geen inzendingen.</p>\n ) : (\n <div className=\"grid gap-2\">\n {subs.map((s) => (\n <div key={s.id} className=\"rounded-lg border border-slate-200 p-3 text-sm\">\n {Object.entries(s.values ?? {}).map(([k, v]) => (\n <div key={k}>\n <span className=\"font-medium text-slate-600\">{k}:</span> {String(v)}\n </div>\n ))}\n </div>\n ))}\n </div>\n )}\n </Card>\n );\n}\n"],"mappings":";AAkEQ,SACE,KADF;AAhER,SAAS,WAAW,gBAAgB;AACpC;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EAEA;AAAA,OACK;AACP,SAAS,aAAa;AACtB,SAAS,kBAAuE;AAChF,SAAS,QAAQ,MAAM,OAAO,OAAO,aAAa;AAClD,SAAS,cAAc;AACvB,SAAS,cAAc;AACvB,SAAS,gBAAgB;AACzB,SAAS,kBAAkB;AAI3B,MAAM,cAAqD;AAAA,EACzD,EAAE,OAAO,QAAQ,OAAO,QAAQ;AAAA,EAChC,EAAE,OAAO,SAAS,OAAO,SAAS;AAAA,EAClC,EAAE,OAAO,OAAO,OAAO,WAAW;AAAA,EAClC,EAAE,OAAO,YAAY,OAAO,WAAW;AAAA,EACvC,EAAE,OAAO,UAAU,OAAO,aAAa;AAAA,EACvC,EAAE,OAAO,SAAS,OAAO,eAAe;AAAA,EACxC,EAAE,OAAO,YAAY,OAAO,SAAS;AAAA,EACrC,EAAE,OAAO,QAAQ,OAAO,QAAQ;AAAA,EAChC,EAAE,OAAO,UAAU,OAAO,QAAQ;AACpC;AAEO,SAAS,WAAW,EAAE,OAAO,GAAuB;AACzD,QAAM,EAAE,MAAM,IAAI,SAAS;AAC3B,QAAM,CAAC,OAAO,QAAQ,IAAI,SAAuB,CAAC,CAAC;AACnD,QAAM,CAAC,UAAU,WAAW,IAAI,SAAwB,IAAI;AAE5D,YAAU,MAAM;AACd,UAAM,KAAK,MAAM;AACjB,QAAI,CAAC,GAAI;AACT,WAAO;AAAA,MAAW,WAAW,IAAI,SAAS,QAAQ,OAAO;AAAA,MAAG,CAAC,SAC3D,SAAS,KAAK,KAAK,IAAI,CAAC,OAAO,EAAE,IAAI,EAAE,IAAI,GAAI,EAAE,KAAK,EAA6B,EAAE,CAAC;AAAA,IACxF;AAAA,EACF,GAAG,CAAC,MAAM,CAAC;AAEX,QAAM,UAAU,MAAM,KAAK,CAAC,MAAM,EAAE,OAAO,QAAQ,KAAK;AAExD,iBAAe,aAAa;AAC1B,UAAM,KAAK,MAAM;AACjB,QAAI,CAAC,GAAI;AACT,UAAM,MAAM,MAAM,OAAO,WAAW,IAAI,SAAS,QAAQ,OAAO,GAAG;AAAA,MACjE,MAAM;AAAA,MACN,QAAQ,CAAC,EAAE,MAAM,QAAQ,OAAO,QAAQ,MAAM,QAAQ,UAAU,KAAK,CAAC;AAAA,MACtE,aAAa;AAAA,MACb,gBAAgB;AAAA,IAClB,CAA0B;AAC1B,gBAAY,IAAI,EAAE;AAAA,EACpB;AAEA,SACE,qBAAC,SAAI,WAAU,uCACb;AAAA,yBAAC,QAAK,WAAU,SACd;AAAA,2BAAC,SAAI,WAAU,0CACb;AAAA,4BAAC,QAAG,WAAU,yBAAwB,yBAAW;AAAA,QACjD,oBAAC,UAAO,MAAK,MAAK,SAAS,YAAY,qBAAO;AAAA,SAChD;AAAA,MACA,qBAAC,QAAG,WAAU,cACX;AAAA,cAAM,IAAI,CAAC,MACV,oBAAC,QACC;AAAA,UAAC;AAAA;AAAA,YACC,SAAS,MAAM,YAAY,EAAE,EAAE;AAAA,YAC/B,WAAW,0DACT,EAAE,OAAO,WAAW,+BAA+B,oBACrD;AAAA,YAEC;AAAA,gBAAE;AAAA,cACH,qBAAC,UAAK,WAAU,+BAA8B;AAAA;AAAA,gBAAE,EAAE,GAAG,MAAM,GAAG,CAAC;AAAA,iBAAE;AAAA;AAAA;AAAA,QACnE,KATO,EAAE,EAUX,CACD;AAAA,QACA,MAAM,WAAW,KAAK,oBAAC,QAAG,WAAU,oCAAmC,mCAAqB;AAAA,SAC/F;AAAA,OACF;AAAA,IACC,UACC,oBAAC,cAA4B,QAAgB,MAAM,SAAS,WAAW,MAAM,YAAY,IAAI,GAAG,SAAS,SAAxF,QAAQ,EAAuF,IAEhH,oBAAC,QACC,8BAAC,OAAE,WAAU,0BAAyB,8CAAgC,GACxE;AAAA,KAEJ;AAEJ;AAEA,SAAS,WAAW;AAAA,EAClB;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,GAKG;AACD,QAAM,CAAC,KAAK,MAAM,IAAI,SAAyB,IAAI;AACnD,QAAM,UAAU,WAAW;AAE3B,iBAAe,OAAO;AACpB,UAAM,KAAK,MAAM;AACjB,QAAI,CAAC,GAAI;AACT,UAAM,SAAS,WAAW,UAAU,GAAG;AACvC,QAAI,CAAC,OAAO,SAAS;AACnB,cAAQ,EAAE,OAAO,sBAAsB,aAAa,OAAO,MAAM,OAAO,CAAC,GAAG,SAAS,MAAM,QAAQ,CAAC;AACpG;AAAA,IACF;AACA,UAAM,OAAO,IAAI,IAAI,SAAS,QAAQ,SAAS,KAAK,EAAE,GAAG,OAAO,MAAM,EAAE,OAAO,KAAK,CAAC;AACrF,YAAQ,EAAE,OAAO,cAAc,MAAM,UAAU,CAAC;AAAA,EAClD;AAEA,iBAAe,SAAS;AACtB,UAAM,KAAK,MAAM;AACjB,QAAI,CAAC,GAAI;AACT,UAAM,KAAK,MAAM,QAAQ,EAAE,OAAO,0BAA0B,aAAa,gEAAgE,cAAc,eAAe,MAAM,SAAS,CAAC;AACtL,QAAI,CAAC,GAAI;AACT,UAAM,UAAU,IAAI,IAAI,SAAS,QAAQ,SAAS,KAAK,EAAE,CAAC;AAC1D,cAAU;AAAA,EACZ;AAEA,WAAS,SAAS,GAAW,MAA0B;AACrD,UAAM,SAAS,CAAC,GAAG,IAAI,MAAM;AAC7B,WAAO,CAAC,IAAI,EAAE,GAAG,OAAO,CAAC,GAAG,GAAG,KAAK;AACpC,WAAO,EAAE,GAAG,KAAK,OAAO,CAAC;AAAA,EAC3B;AAEA,SACE,qBAAC,SAAI,WAAU,cACb;AAAA,yBAAC,QACC;AAAA,2BAAC,SAAI,WAAU,0CACb;AAAA,4BAAC,QAAG,WAAU,2BAA0B,gCAAkB;AAAA,QAC1D,qBAAC,SAAI,WAAU,cACb;AAAA,8BAAC,UAAO,SAAQ,UAAS,MAAK,MAAK,SAAS,QAAQ,yBAAW;AAAA,UAC/D,oBAAC,UAAO,MAAK,MAAK,SAAS,MAAM,qBAAO;AAAA,WAC1C;AAAA,SACF;AAAA,MACA,qBAAC,SAAI,WAAU,6BACb;AAAA,4BAAC,SAAM,OAAM,QAAO,8BAAC,SAAM,OAAO,IAAI,MAAM,UAAU,CAAC,MAAM,OAAO,EAAE,GAAG,KAAK,MAAM,EAAE,OAAO,MAAM,CAAC,GAAG,GAAE;AAAA,QACzG,oBAAC,SAAM,OAAM,gBAAe,8BAAC,SAAM,OAAO,IAAI,aAAa,UAAU,CAAC,MAAM,OAAO,EAAE,GAAG,KAAK,aAAa,EAAE,OAAO,MAAM,CAAC,GAAG,GAAE;AAAA,QAC/H,oBAAC,SAAM,OAAM,iBAAgB,8BAAC,SAAM,OAAO,IAAI,gBAAgB,UAAU,CAAC,MAAM,OAAO,EAAE,GAAG,KAAK,gBAAgB,EAAE,OAAO,MAAM,CAAC,GAAG,GAAE;AAAA,QACtI,qBAAC,SAAI,WAAU,oCACb;AAAA,8BAAC,SAAM,OAAM,8BAA6B,MAAK,sDAC7C,8BAAC,SAAM,OAAO,IAAI,mBAAmB,IAAI,UAAU,CAAC,MAAM,OAAO,EAAE,GAAG,KAAK,iBAAiB,EAAE,OAAO,SAAS,OAAU,CAAC,GAAG,GAC9H;AAAA,UACA,qBAAC,WAAM,WAAU,uCACf;AAAA;AAAA,cAAC;AAAA;AAAA,gBACC,MAAK;AAAA,gBACL,SAAS,QAAQ,IAAI,gBAAgB;AAAA,gBACrC,UAAU,CAAC,MAAM,OAAO,EAAE,GAAG,KAAK,kBAAkB,EAAE,OAAO,QAAQ,CAAC;AAAA;AAAA,YACxE;AAAA,YAAE;AAAA,aAEJ;AAAA,WACF;AAAA,SACF;AAAA,MACA,qBAAC,OAAE,WAAU,+BAA8B;AAAA;AAAA,QAAwC,oBAAC,UAAM,eAAK,IAAG;AAAA,SAAO;AAAA,OAC3G;AAAA,IAEA,qBAAC,QACC;AAAA,0BAAC,QAAG,WAAU,8BAA6B,oBAAM;AAAA,MACjD,oBAAC,SAAI,WAAU,cACZ,cAAI,OAAO,IAAI,CAAC,GAAG,MAClB,qBAAC,SAAY,WAAU,sGACrB;AAAA,4BAAC,SAAM,OAAM,kBAAiB,8BAAC,SAAM,OAAO,EAAE,MAAM,UAAU,CAAC,MAAM,SAAS,GAAG,EAAE,MAAM,EAAE,OAAO,MAAM,CAAC,GAAG,GAAE;AAAA,QAC9G,oBAAC,SAAM,OAAM,SAAQ,8BAAC,SAAM,OAAO,EAAE,OAAO,UAAU,CAAC,MAAM,SAAS,GAAG,EAAE,OAAO,EAAE,OAAO,MAAM,CAAC,GAAG,GAAE;AAAA,QACvG,oBAAC,SAAM,OAAM,QACX,8BAAC,UAAO,OAAO,EAAE,MAAM,eAAe,CAAC,MAAM,SAAS,GAAG,EAAE,MAAM,EAAe,CAAC,GAAG,SAAS,aAAa,GAC5G;AAAA,QACA,oBAAC,UAAO,SAAS,EAAE,UAAU,iBAAiB,CAAC,MAAM,SAAS,GAAG,EAAE,UAAU,EAAE,CAAC,GAAG,OAAM,aAAY;AAAA,QACrG,oBAAC,UAAO,SAAQ,SAAQ,MAAK,MAAK,SAAS,MAAM,OAAO,EAAE,GAAG,KAAK,QAAQ,IAAI,OAAO,OAAO,CAAC,GAAG,QAAQ,QAAQ,CAAC,EAAE,CAAC,GAAG,uBAAE;AAAA,SACvH,EAAE,SAAS,YAAY,EAAE,SAAS,YAClC,oBAAC,SAAI,WAAU,iBACb,8BAAC,SAAM,OAAM,6BACX;AAAA,UAAC;AAAA;AAAA,YACC,QAAQ,EAAE,WAAW,CAAC,GAAG,KAAK,IAAI;AAAA,YAClC,UAAU,CAAC,MAAM,SAAS,GAAG,EAAE,SAAS,EAAE,OAAO,MAAM,MAAM,GAAG,EAAE,IAAI,CAAC,MAAM,EAAE,KAAK,CAAC,EAAE,OAAO,OAAO,EAAE,CAAC;AAAA;AAAA,QAC1G,GACF,GACF;AAAA,WAhBM,CAkBV,CACD,GACH;AAAA,MACA,oBAAC,SAAI,WAAU,QACb;AAAA,QAAC;AAAA;AAAA,UACC,SAAQ;AAAA,UACR,MAAK;AAAA,UACL,SAAS,MAAM,OAAO,EAAE,GAAG,KAAK,QAAQ,CAAC,GAAG,IAAI,QAAQ,EAAE,MAAM,OAAO,IAAI,OAAO,SAAS,CAAC,IAAI,OAAO,cAAc,MAAM,QAAQ,UAAU,MAAM,CAAC,EAAE,CAAC;AAAA,UACxJ;AAAA;AAAA,MAED,GACF;AAAA,OACF;AAAA,IAEA,oBAAC,eAAY,QAAgB,QAAQ,KAAK,IAAI;AAAA,KAChD;AAEJ;AAEA,SAAS,YAAY,EAAE,QAAQ,OAAO,GAAuC;AAC3E,QAAM,CAAC,MAAM,OAAO,IAAI,SAAiF,CAAC,CAAC;AAC3G,YAAU,MAAM;AACd,UAAM,KAAK,MAAM;AACjB,QAAI,CAAC,GAAI;AACT,UAAM,IAAI,MAAM,WAAW,IAAI,SAAS,QAAQ,SAAS,QAAQ,aAAa,GAAG,QAAQ,aAAa,MAAM,CAAC;AAC7G,WAAO,WAAW,GAAG,CAAC,SAAS,QAAQ,KAAK,KAAK,IAAI,CAAC,OAAO,EAAE,IAAI,EAAE,IAAI,GAAI,EAAE,KAAK,EAA0C,EAAE,CAAC,CAAC;AAAA,EACpI,GAAG,CAAC,QAAQ,MAAM,CAAC;AAEnB,SACE,qBAAC,QACC;AAAA,yBAAC,QAAG,WAAU,8BAA6B;AAAA;AAAA,MAAY,oBAAC,SAAO,eAAK,QAAO;AAAA,OAAQ;AAAA,IAClF,KAAK,WAAW,IACf,oBAAC,OAAE,WAAU,0BAAyB,mCAAqB,IAE3D,oBAAC,SAAI,WAAU,cACZ,eAAK,IAAI,CAAC,MACT,oBAAC,SAAe,WAAU,kDACvB,iBAAO,QAAQ,EAAE,UAAU,CAAC,CAAC,EAAE,IAAI,CAAC,CAAC,GAAG,CAAC,MACxC,qBAAC,SACC;AAAA,2BAAC,UAAK,WAAU,8BAA8B;AAAA;AAAA,QAAE;AAAA,SAAC;AAAA,MAAO;AAAA,MAAE,OAAO,CAAC;AAAA,SAD1D,CAEV,CACD,KALO,EAAE,EAMZ,CACD,GACH;AAAA,KAEJ;AAEJ;","names":[]}
1
+ {"version":3,"sources":["../../../src/admin/forms/FormsPanel.tsx"],"sourcesContent":["\"use client\";\n\nimport { useEffect, useState } from \"react\";\nimport {\n addDoc,\n collection,\n deleteDoc,\n doc,\n onSnapshot,\n orderBy,\n query,\n serverTimestamp,\n setDoc,\n} from \"firebase/firestore\";\nimport { getDb } from \"../../firebase/client\";\nimport { FormSchema, type FormDefinition, type FormField, type FieldType } from \"../../schema/forms\";\nimport { Button, Card, Field, Input, Badge } from \"../ui/primitives\";\nimport { Select } from \"../ui/select\";\nimport { Switch } from \"../ui/switch\";\nimport { useToast } from \"../ui/toast\";\nimport { useConfirm } from \"../ui/ConfirmDialog\";\nimport { MailTemplates } from \"../settings/MailTemplates\";\nimport { FORM_TEMPLATE_KEYS } from \"../../schema/mail\";\n\ntype StoredForm = FormDefinition & { id: string };\n\nconst FIELD_TYPES: { value: FieldType; label: string }[] = [\n { value: \"text\", label: \"Tekst\" },\n { value: \"email\", label: \"E-mail\" },\n { value: \"tel\", label: \"Telefoon\" },\n { value: \"textarea\", label: \"Tekstvak\" },\n { value: \"select\", label: \"Keuzelijst\" },\n { value: \"radio\", label: \"Keuzerondjes\" },\n { value: \"checkbox\", label: \"Vinkje\" },\n { value: \"date\", label: \"Datum\" },\n { value: \"number\", label: \"Getal\" },\n];\n\nexport function FormsPanel({ siteId }: { siteId: string }) {\n const { toast } = useToast();\n const [forms, setForms] = useState<StoredForm[]>([]);\n const [selected, setSelected] = useState<string | null>(null);\n\n useEffect(() => {\n const db = getDb();\n if (!db) return;\n return onSnapshot(collection(db, \"sites\", siteId, \"forms\"), (snap) =>\n setForms(snap.docs.map((d) => ({ id: d.id, ...(d.data() as Omit<StoredForm, \"id\">) }))),\n );\n }, [siteId]);\n\n const current = forms.find((f) => f.id === selected) ?? null;\n\n async function createForm() {\n const db = getDb();\n if (!db) return;\n const ref = await addDoc(collection(db, \"sites\", siteId, \"forms\"), {\n name: \"Nieuw formulier\",\n fields: [{ name: \"naam\", label: \"Naam\", type: \"text\", required: true }],\n submitLabel: \"Versturen\",\n successMessage: \"Bedankt! We nemen snel contact op.\",\n } satisfies FormDefinition);\n setSelected(ref.id);\n }\n\n return (\n <div className=\"grid gap-6\">\n <div className=\"grid gap-4 md:grid-cols-[260px_1fr]\">\n <Card className=\"h-fit\">\n <div className=\"mb-3 flex items-center justify-between\">\n <h3 className=\"text-sm font-semibold\">Formulieren</h3>\n <Button size=\"sm\" onClick={createForm}>+ Nieuw</Button>\n </div>\n <ul className=\"grid gap-1\">\n {forms.map((f) => (\n <li key={f.id}>\n <button\n onClick={() => setSelected(f.id)}\n className={`w-full truncate rounded-lg px-3 py-2 text-left text-sm ${\n f.id === selected ? \"bg-brand-50 text-brand-700\" : \"hover:bg-slate-100\"\n }`}\n >\n {f.name}\n <span className=\"ml-1 text-xs text-slate-400\">#{f.id.slice(0, 5)}</span>\n </button>\n </li>\n ))}\n {forms.length === 0 && <li className=\"px-3 py-2 text-sm text-slate-500\">Nog geen formulieren.</li>}\n </ul>\n </Card>\n {current ? (\n <FormDetail key={current.id} siteId={siteId} form={current} onDeleted={() => setSelected(null)} onToast={toast} />\n ) : (\n <Card>\n <p className=\"text-sm text-slate-500\">Selecteer of maak een formulier.</p>\n </Card>\n )}\n </div>\n\n <div>\n <h3 className=\"mb-1 text-base font-semibold\">E-mailteksten voor formulieren</h3>\n <p className=\"mb-3 text-sm text-slate-600\">\n Gelden voor álle formulieren: de notificatie naar jou en de bevestiging naar de invuller. De opmaak\n (huisstijl) en verzendprovider stel je site-breed in bij <strong>Instellingen → E-mail</strong>.\n </p>\n <MailTemplates siteId={siteId} keys={FORM_TEMPLATE_KEYS} />\n </div>\n </div>\n );\n}\n\nfunction FormDetail({\n siteId,\n form,\n onDeleted,\n onToast,\n}: {\n siteId: string;\n form: StoredForm;\n onDeleted: () => void;\n onToast: (t: { title: string; description?: string; tone?: \"default\" | \"success\" | \"error\" }) => void;\n}) {\n const [def, setDef] = useState<FormDefinition>(form);\n const confirm = useConfirm();\n\n async function save() {\n const db = getDb();\n if (!db) return;\n const parsed = FormSchema.safeParse(def);\n if (!parsed.success) {\n onToast({ title: \"Ongeldig formulier\", description: parsed.error.issues[0]?.message, tone: \"error\" });\n return;\n }\n await setDoc(doc(db, \"sites\", siteId, \"forms\", form.id), parsed.data, { merge: true });\n onToast({ title: \"Opgeslagen\", tone: \"success\" });\n }\n\n async function remove() {\n const db = getDb();\n if (!db) return;\n const ok = await confirm({ title: \"Formulier verwijderen?\", description: \"Het formulier wordt verwijderd. Inzendingen blijven bewaard.\", confirmLabel: \"Verwijderen\", tone: \"danger\" });\n if (!ok) return;\n await deleteDoc(doc(db, \"sites\", siteId, \"forms\", form.id));\n onDeleted();\n }\n\n function setField(i: number, part: Partial<FormField>) {\n const fields = [...def.fields];\n fields[i] = { ...fields[i], ...part };\n setDef({ ...def, fields });\n }\n\n return (\n <div className=\"grid gap-4\">\n <Card>\n <div className=\"mb-3 flex items-center justify-between\">\n <h3 className=\"text-base font-semibold\">Formulier bewerken</h3>\n <div className=\"flex gap-2\">\n <Button variant=\"danger\" size=\"sm\" onClick={remove}>Verwijderen</Button>\n <Button size=\"sm\" onClick={save}>Opslaan</Button>\n </div>\n </div>\n <div className=\"grid gap-3 md:grid-cols-2\">\n <Field label=\"Naam\"><Input value={def.name} onChange={(e) => setDef({ ...def, name: e.target.value })} /></Field>\n <Field label=\"Verstuurknop\"><Input value={def.submitLabel} onChange={(e) => setDef({ ...def, submitLabel: e.target.value })} /></Field>\n <Field label=\"Bedankbericht\"><Input value={def.successMessage} onChange={(e) => setDef({ ...def, successMessage: e.target.value })} /></Field>\n <Field label=\"Bedankt-pagina (optioneel)\" hint=\"Na verzenden doorsturen naar dit pad, bv. /bedankt\">\n <Input value={def.successRedirect ?? \"\"} onChange={(e) => setDef({ ...def, successRedirect: e.target.value || undefined })} />\n </Field>\n </div>\n\n <div className=\"mt-4 rounded-lg border border-slate-200 p-4\">\n <h4 className=\"text-sm font-semibold\">Wat gebeurt er bij een inzending?</h4>\n <p className=\"mb-3 mt-0.5 text-xs text-slate-500\">\n Elke inzending wordt altijd bewaard onder <strong>Inzendingen</strong>. Daarnaast kun je een kopie\n mailen naar jezelf en/of een bevestiging naar de invuller. De opmaak en verzendprovider stel je\n site-breed in bij <strong>Instellingen → E-mail</strong>.\n </p>\n <div className=\"grid gap-3 md:grid-cols-2\">\n <Field label=\"Kopie van inzending naar\" hint=\"Leeg = het notificatie-adres uit Instellingen → E-mail.\">\n <Input\n type=\"email\"\n value={def.notifyTo ?? \"\"}\n onChange={(e) => setDef({ ...def, notifyTo: e.target.value || undefined })}\n placeholder=\"jij@voorbeeld.nl\"\n />\n </Field>\n <label className=\"mt-6 flex h-9 items-center gap-2 text-sm\">\n <input\n type=\"checkbox\"\n checked={Boolean(def.confirmSubmitter)}\n onChange={(e) => setDef({ ...def, confirmSubmitter: e.target.checked })}\n />\n Stuur de invuller ook een bevestiging (naar het eerste e-mailveld)\n </label>\n </div>\n </div>\n <p className=\"mt-3 text-xs text-slate-500\">Formulier-id (voor het Formulier-blok): <code>{form.id}</code></p>\n </Card>\n\n <Card>\n <h4 className=\"mb-3 text-sm font-semibold\">Velden</h4>\n <div className=\"grid gap-3\">\n {def.fields.map((f, i) => (\n <div key={i} className=\"grid items-end gap-2 rounded-lg border border-slate-200 p-3 md:grid-cols-[1fr_1fr_140px_auto_auto]\">\n <Field label=\"Naam (sleutel)\"><Input value={f.name} onChange={(e) => setField(i, { name: e.target.value })} /></Field>\n <Field label=\"Label\"><Input value={f.label} onChange={(e) => setField(i, { label: e.target.value })} /></Field>\n <Field label=\"Type\">\n <Select value={f.type} onValueChange={(v) => setField(i, { type: v as FieldType })} options={FIELD_TYPES} />\n </Field>\n <Switch checked={f.required} onCheckedChange={(v) => setField(i, { required: v })} label=\"Verplicht\" />\n <Button variant=\"ghost\" size=\"sm\" onClick={() => setDef({ ...def, fields: def.fields.filter((_, idx) => idx !== i) })}>🗑</Button>\n {(f.type === \"select\" || f.type === \"radio\") && (\n <div className=\"md:col-span-5\">\n <Field label=\"Opties (komma-gescheiden)\">\n <Input\n value={(f.options ?? []).join(\", \")}\n onChange={(e) => setField(i, { options: e.target.value.split(\",\").map((s) => s.trim()).filter(Boolean) })}\n />\n </Field>\n </div>\n )}\n </div>\n ))}\n </div>\n <div className=\"mt-3\">\n <Button\n variant=\"secondary\"\n size=\"sm\"\n onClick={() => setDef({ ...def, fields: [...def.fields, { name: `veld${def.fields.length + 1}`, label: \"Nieuw veld\", type: \"text\", required: false }] })}\n >\n + Veld toevoegen\n </Button>\n </div>\n </Card>\n\n <Submissions siteId={siteId} formId={form.id} />\n </div>\n );\n}\n\nfunction Submissions({ siteId, formId }: { siteId: string; formId: string }) {\n const [subs, setSubs] = useState<{ id: string; values: Record<string, unknown>; createdAt?: unknown }[]>([]);\n useEffect(() => {\n const db = getDb();\n if (!db) return;\n const q = query(collection(db, \"sites\", siteId, \"forms\", formId, \"submissions\"), orderBy(\"createdAt\", \"desc\"));\n return onSnapshot(q, (snap) => setSubs(snap.docs.map((d) => ({ id: d.id, ...(d.data() as { values: Record<string, unknown> }) }))));\n }, [siteId, formId]);\n\n return (\n <Card>\n <h4 className=\"mb-3 text-sm font-semibold\">Inzendingen <Badge>{subs.length}</Badge></h4>\n {subs.length === 0 ? (\n <p className=\"text-sm text-slate-500\">Nog geen inzendingen.</p>\n ) : (\n <div className=\"grid gap-2\">\n {subs.map((s) => (\n <div key={s.id} className=\"rounded-lg border border-slate-200 p-3 text-sm\">\n {Object.entries(s.values ?? {}).map(([k, v]) => (\n <div key={k}>\n <span className=\"font-medium text-slate-600\">{k}:</span> {String(v)}\n </div>\n ))}\n </div>\n ))}\n </div>\n )}\n </Card>\n );\n}\n"],"mappings":"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","names":[]}
@@ -1,9 +1,14 @@
1
+ import { type MailTemplateKey } from "../../schema/mail";
2
+ type MailTemplatesProps = {
3
+ siteId: string;
4
+ /** Welke templates tonen — bv. FORM_TEMPLATE_KEYS in Formulieren, SHOP_TEMPLATE_KEYS in de shop. */
5
+ keys?: readonly MailTemplateKey[];
6
+ };
1
7
  /**
2
8
  * E-mailteksten — bewerkbare templates in `settings/mailTemplates` (site-breed, siteadmin). Een
3
9
  * leeg veld gebruikt de ingebouwde standaardtekst, dus er gaat altijd een correcte mail uit. Je
4
10
  * bewerkt platte tekst met {{invulvelden}}; de engine verpakt die in je huisstijl (kleur, logo,
5
11
  * footer) en stuurt een nette HTML-mail. Shop-templates verschijnen alleen op sites met webwinkel.
6
12
  */
7
- export declare function MailTemplates({ siteId }: {
8
- siteId: string;
9
- }): import("react").JSX.Element;
13
+ export declare function MailTemplates({ siteId, keys }: MailTemplatesProps): import("react").JSX.Element;
14
+ export {};
@@ -17,10 +17,9 @@ function callFunction(name) {
17
17
  return res.data;
18
18
  };
19
19
  }
20
- function MailTemplates({ siteId }) {
20
+ function MailTemplates({ siteId, keys }) {
21
21
  const { toast } = useToast();
22
22
  const [templates, setTemplates] = useState(null);
23
- const [shopActive, setShopActive] = useState(false);
24
23
  const [busy, setBusy] = useState(null);
25
24
  useEffect(() => {
26
25
  const db = getDb();
@@ -28,10 +27,6 @@ function MailTemplates({ siteId }) {
28
27
  void getDoc(doc(db, "sites", siteId, "settings", "mailTemplates")).then(
29
28
  (snap) => setTemplates(snap.data() ?? {})
30
29
  );
31
- void getDoc(doc(db, "sites", siteId, "settings", "public")).then((snap) => {
32
- const entry = (snap.data()?.modules ?? {}).shop;
33
- setShopActive(entry === true || typeof entry === "object" && entry !== null);
34
- });
35
30
  }, [siteId]);
36
31
  async function save() {
37
32
  const db = getDb();
@@ -61,7 +56,7 @@ function MailTemplates({ siteId }) {
61
56
  }
62
57
  }
63
58
  if (!templates) return /* @__PURE__ */ jsx("p", { className: "text-sm text-slate-500", children: "E-mailteksten laden\u2026" });
64
- const defs = MAIL_TEMPLATE_DEFS.filter((d) => d.module !== "shop" || shopActive);
59
+ const defs = keys ? MAIL_TEMPLATE_DEFS.filter((d) => keys.includes(d.key)) : MAIL_TEMPLATE_DEFS;
65
60
  return /* @__PURE__ */ jsxs("div", { className: "grid gap-4", children: [
66
61
  /* @__PURE__ */ jsxs("p", { className: "text-sm text-slate-600", children: [
67
62
  "Laat een veld leeg voor de standaardtekst. De opmaak (huisstijlkleur, logo, footer) zet de engine er automatisch omheen \u2014 je bewerkt alleen de tekst. Gebruik de ",
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/admin/settings/MailTemplates.tsx"],"sourcesContent":["\"use client\";\n\nimport { useEffect, useState } from \"react\";\nimport { doc, getDoc, setDoc } from \"firebase/firestore\";\nimport { getFunctions, httpsCallable } from \"firebase/functions\";\nimport { getDb, getFirebaseApp } from \"../../firebase/client\";\nimport { functionsRegion } from \"../../firebase/region\";\nimport { MAIL_TEMPLATE_DEFS, type MailTemplateKey, type MailTemplates as MailTemplatesData } from \"../../schema/mail\";\nimport { Button, Card, Field, Input, Textarea } from \"../ui/primitives\";\nimport { useToast } from \"../ui/toast\";\n\n/** Generieke callable-helper (niet shop-gebonden) — mail is een site-brede instelling. */\nfunction callFunction<TIn, TOut>(name: string) {\n return async (data: TIn): Promise<TOut> => {\n const app = getFirebaseApp();\n if (!app) throw new Error(\"Firebase niet beschikbaar.\");\n const fn = httpsCallable(getFunctions(app, functionsRegion()), name);\n const res = await fn(data as Record<string, unknown>);\n return res.data as TOut;\n };\n}\n\n/**\n * E-mailteksten — bewerkbare templates in `settings/mailTemplates` (site-breed, siteadmin). Een\n * leeg veld gebruikt de ingebouwde standaardtekst, dus er gaat altijd een correcte mail uit. Je\n * bewerkt platte tekst met {{invulvelden}}; de engine verpakt die in je huisstijl (kleur, logo,\n * footer) en stuurt een nette HTML-mail. Shop-templates verschijnen alleen op sites met webwinkel.\n */\nexport function MailTemplates({ siteId }: { siteId: string }) {\n const { toast } = useToast();\n const [templates, setTemplates] = useState<MailTemplatesData | null>(null);\n const [shopActive, setShopActive] = useState(false);\n const [busy, setBusy] = useState<string | null>(null);\n\n useEffect(() => {\n const db = getDb();\n if (!db) return;\n void getDoc(doc(db, \"sites\", siteId, \"settings\", \"mailTemplates\")).then((snap) =>\n setTemplates((snap.data() as MailTemplatesData | undefined) ?? {}),\n );\n void getDoc(doc(db, \"sites\", siteId, \"settings\", \"public\")).then((snap) => {\n const entry = ((snap.data()?.modules ?? {}) as Record<string, unknown>).shop;\n setShopActive(entry === true || (typeof entry === \"object\" && entry !== null));\n });\n }, [siteId]);\n\n async function save() {\n const db = getDb();\n if (!db || !templates) return;\n setBusy(\"save\");\n try {\n await setDoc(doc(db, \"sites\", siteId, \"settings\", \"mailTemplates\"), templates);\n toast({ title: \"E-mailteksten opgeslagen\", tone: \"success\" });\n } catch (e) {\n toast({ title: \"Opslaan mislukt\", description: e instanceof Error ? e.message : undefined, tone: \"error\" });\n } finally {\n setBusy(null);\n }\n }\n\n async function preview(key: MailTemplateKey) {\n setBusy(key);\n try {\n const res = await callFunction<{ siteId: string; template: MailTemplateKey }, { to: string }>(\"sendTemplatePreview\")({\n siteId,\n template: key,\n });\n toast({ title: \"Voorbeeld verstuurd\", description: `Naar ${res.to} (met voorbeeldgegevens en je huisstijl).`, tone: \"success\" });\n } catch (e) {\n toast({ title: \"Voorbeeld mislukt\", description: e instanceof Error ? e.message : undefined, tone: \"error\" });\n } finally {\n setBusy(null);\n }\n }\n\n if (!templates) return <p className=\"text-sm text-slate-500\">E-mailteksten laden…</p>;\n\n const defs = MAIL_TEMPLATE_DEFS.filter((d) => d.module !== \"shop\" || shopActive);\n\n return (\n <div className=\"grid gap-4\">\n <p className=\"text-sm text-slate-600\">\n Laat een veld leeg voor de standaardtekst. De opmaak (huisstijlkleur, logo, footer) zet de engine er\n automatisch omheen — je bewerkt alleen de tekst. Gebruik de {\"{{invulvelden}}\"} die per e-mail staan vermeld.\n </p>\n\n {defs.map((def) => {\n const key = def.key as MailTemplateKey;\n const tpl = templates[key] ?? {};\n const set = (patch: { subject?: string; bodyText?: string }) =>\n setTemplates((p) => ({ ...p, [key]: { ...p?.[key], ...patch } }));\n return (\n <Card key={key}>\n <div className=\"mb-2 flex items-start justify-between gap-3\">\n <div>\n <h4 className=\"text-sm font-semibold\">{def.label}</h4>\n <p className=\"text-xs text-slate-500\">{def.hint}</p>\n </div>\n <Button size=\"sm\" variant=\"secondary\" disabled={busy !== null} onClick={() => preview(key)}>\n {busy === key ? \"Versturen…\" : \"Stuur voorbeeld naar mij\"}\n </Button>\n </div>\n <div className=\"grid gap-3\">\n <Field label=\"Onderwerp\" hint=\"Leeg = standaard\">\n <Input value={tpl.subject ?? \"\"} onChange={(e) => set({ subject: e.target.value })} />\n </Field>\n <Field label=\"Tekst\" hint=\"Leeg = standaard\">\n <Textarea rows={6} value={tpl.bodyText ?? \"\"} onChange={(e) => set({ bodyText: e.target.value })} />\n </Field>\n <p className=\"text-xs text-slate-500\">\n Invulvelden:{\" \"}\n <code className=\"rounded bg-slate-100 px-1 py-0.5 text-[11px]\">{def.placeholders}</code>\n </p>\n </div>\n </Card>\n );\n })}\n\n <div>\n <Button onClick={save} disabled={busy !== null}>\n {busy === \"save\" ? \"Opslaan…\" : \"E-mailteksten opslaan\"}\n </Button>\n </div>\n </div>\n );\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../../src/admin/settings/MailTemplates.tsx"],"sourcesContent":["\"use client\";\n\nimport { useEffect, useState } from \"react\";\nimport { doc, getDoc, setDoc } from \"firebase/firestore\";\nimport { getFunctions, httpsCallable } from \"firebase/functions\";\nimport { getDb, getFirebaseApp } from \"../../firebase/client\";\nimport { functionsRegion } from \"../../firebase/region\";\nimport { MAIL_TEMPLATE_DEFS, type MailTemplateKey, type MailTemplates as MailTemplatesData } from \"../../schema/mail\";\nimport { Button, Card, Field, Input, Textarea } from \"../ui/primitives\";\nimport { useToast } from \"../ui/toast\";\n\ntype MailTemplatesProps = {\n siteId: string;\n /** Welke templates tonen — bv. FORM_TEMPLATE_KEYS in Formulieren, SHOP_TEMPLATE_KEYS in de shop. */\n keys?: readonly MailTemplateKey[];\n};\n\n/** Generieke callable-helper (niet shop-gebonden) — mail is een site-brede instelling. */\nfunction callFunction<TIn, TOut>(name: string) {\n return async (data: TIn): Promise<TOut> => {\n const app = getFirebaseApp();\n if (!app) throw new Error(\"Firebase niet beschikbaar.\");\n const fn = httpsCallable(getFunctions(app, functionsRegion()), name);\n const res = await fn(data as Record<string, unknown>);\n return res.data as TOut;\n };\n}\n\n/**\n * E-mailteksten — bewerkbare templates in `settings/mailTemplates` (site-breed, siteadmin). Een\n * leeg veld gebruikt de ingebouwde standaardtekst, dus er gaat altijd een correcte mail uit. Je\n * bewerkt platte tekst met {{invulvelden}}; de engine verpakt die in je huisstijl (kleur, logo,\n * footer) en stuurt een nette HTML-mail. Shop-templates verschijnen alleen op sites met webwinkel.\n */\nexport function MailTemplates({ siteId, keys }: MailTemplatesProps) {\n const { toast } = useToast();\n const [templates, setTemplates] = useState<MailTemplatesData | null>(null);\n const [busy, setBusy] = useState<string | null>(null);\n\n useEffect(() => {\n const db = getDb();\n if (!db) return;\n void getDoc(doc(db, \"sites\", siteId, \"settings\", \"mailTemplates\")).then((snap) =>\n setTemplates((snap.data() as MailTemplatesData | undefined) ?? {}),\n );\n }, [siteId]);\n\n async function save() {\n const db = getDb();\n if (!db || !templates) return;\n setBusy(\"save\");\n try {\n await setDoc(doc(db, \"sites\", siteId, \"settings\", \"mailTemplates\"), templates);\n toast({ title: \"E-mailteksten opgeslagen\", tone: \"success\" });\n } catch (e) {\n toast({ title: \"Opslaan mislukt\", description: e instanceof Error ? e.message : undefined, tone: \"error\" });\n } finally {\n setBusy(null);\n }\n }\n\n async function preview(key: MailTemplateKey) {\n setBusy(key);\n try {\n const res = await callFunction<{ siteId: string; template: MailTemplateKey }, { to: string }>(\"sendTemplatePreview\")({\n siteId,\n template: key,\n });\n toast({ title: \"Voorbeeld verstuurd\", description: `Naar ${res.to} (met voorbeeldgegevens en je huisstijl).`, tone: \"success\" });\n } catch (e) {\n toast({ title: \"Voorbeeld mislukt\", description: e instanceof Error ? e.message : undefined, tone: \"error\" });\n } finally {\n setBusy(null);\n }\n }\n\n if (!templates) return <p className=\"text-sm text-slate-500\">E-mailteksten laden…</p>;\n\n const defs = keys ? MAIL_TEMPLATE_DEFS.filter((d) => keys.includes(d.key as MailTemplateKey)) : MAIL_TEMPLATE_DEFS;\n\n return (\n <div className=\"grid gap-4\">\n <p className=\"text-sm text-slate-600\">\n Laat een veld leeg voor de standaardtekst. De opmaak (huisstijlkleur, logo, footer) zet de engine er\n automatisch omheen — je bewerkt alleen de tekst. Gebruik de {\"{{invulvelden}}\"} die per e-mail staan vermeld.\n </p>\n\n {defs.map((def) => {\n const key = def.key as MailTemplateKey;\n const tpl = templates[key] ?? {};\n const set = (patch: { subject?: string; bodyText?: string }) =>\n setTemplates((p) => ({ ...p, [key]: { ...p?.[key], ...patch } }));\n return (\n <Card key={key}>\n <div className=\"mb-2 flex items-start justify-between gap-3\">\n <div>\n <h4 className=\"text-sm font-semibold\">{def.label}</h4>\n <p className=\"text-xs text-slate-500\">{def.hint}</p>\n </div>\n <Button size=\"sm\" variant=\"secondary\" disabled={busy !== null} onClick={() => preview(key)}>\n {busy === key ? \"Versturen…\" : \"Stuur voorbeeld naar mij\"}\n </Button>\n </div>\n <div className=\"grid gap-3\">\n <Field label=\"Onderwerp\" hint=\"Leeg = standaard\">\n <Input value={tpl.subject ?? \"\"} onChange={(e) => set({ subject: e.target.value })} />\n </Field>\n <Field label=\"Tekst\" hint=\"Leeg = standaard\">\n <Textarea rows={6} value={tpl.bodyText ?? \"\"} onChange={(e) => set({ bodyText: e.target.value })} />\n </Field>\n <p className=\"text-xs text-slate-500\">\n Invulvelden:{\" \"}\n <code className=\"rounded bg-slate-100 px-1 py-0.5 text-[11px]\">{def.placeholders}</code>\n </p>\n </div>\n </Card>\n );\n })}\n\n <div>\n <Button onClick={save} disabled={busy !== null}>\n {busy === \"save\" ? \"Opslaan…\" : \"E-mailteksten opslaan\"}\n </Button>\n </div>\n </div>\n );\n}\n"],"mappings":";AA4EyB,cAMnB,YANmB;AA1EzB,SAAS,WAAW,gBAAgB;AACpC,SAAS,KAAK,QAAQ,cAAc;AACpC,SAAS,cAAc,qBAAqB;AAC5C,SAAS,OAAO,sBAAsB;AACtC,SAAS,uBAAuB;AAChC,SAAS,0BAAyF;AAClG,SAAS,QAAQ,MAAM,OAAO,OAAO,gBAAgB;AACrD,SAAS,gBAAgB;AASzB,SAAS,aAAwB,MAAc;AAC7C,SAAO,OAAO,SAA6B;AACzC,UAAM,MAAM,eAAe;AAC3B,QAAI,CAAC,IAAK,OAAM,IAAI,MAAM,4BAA4B;AACtD,UAAM,KAAK,cAAc,aAAa,KAAK,gBAAgB,CAAC,GAAG,IAAI;AACnE,UAAM,MAAM,MAAM,GAAG,IAA+B;AACpD,WAAO,IAAI;AAAA,EACb;AACF;AAQO,SAAS,cAAc,EAAE,QAAQ,KAAK,GAAuB;AAClE,QAAM,EAAE,MAAM,IAAI,SAAS;AAC3B,QAAM,CAAC,WAAW,YAAY,IAAI,SAAmC,IAAI;AACzE,QAAM,CAAC,MAAM,OAAO,IAAI,SAAwB,IAAI;AAEpD,YAAU,MAAM;AACd,UAAM,KAAK,MAAM;AACjB,QAAI,CAAC,GAAI;AACT,SAAK,OAAO,IAAI,IAAI,SAAS,QAAQ,YAAY,eAAe,CAAC,EAAE;AAAA,MAAK,CAAC,SACvE,aAAc,KAAK,KAAK,KAAuC,CAAC,CAAC;AAAA,IACnE;AAAA,EACF,GAAG,CAAC,MAAM,CAAC;AAEX,iBAAe,OAAO;AACpB,UAAM,KAAK,MAAM;AACjB,QAAI,CAAC,MAAM,CAAC,UAAW;AACvB,YAAQ,MAAM;AACd,QAAI;AACF,YAAM,OAAO,IAAI,IAAI,SAAS,QAAQ,YAAY,eAAe,GAAG,SAAS;AAC7E,YAAM,EAAE,OAAO,4BAA4B,MAAM,UAAU,CAAC;AAAA,IAC9D,SAAS,GAAG;AACV,YAAM,EAAE,OAAO,mBAAmB,aAAa,aAAa,QAAQ,EAAE,UAAU,QAAW,MAAM,QAAQ,CAAC;AAAA,IAC5G,UAAE;AACA,cAAQ,IAAI;AAAA,IACd;AAAA,EACF;AAEA,iBAAe,QAAQ,KAAsB;AAC3C,YAAQ,GAAG;AACX,QAAI;AACF,YAAM,MAAM,MAAM,aAA4E,qBAAqB,EAAE;AAAA,QACnH;AAAA,QACA,UAAU;AAAA,MACZ,CAAC;AACD,YAAM,EAAE,OAAO,uBAAuB,aAAa,QAAQ,IAAI,EAAE,6CAA6C,MAAM,UAAU,CAAC;AAAA,IACjI,SAAS,GAAG;AACV,YAAM,EAAE,OAAO,qBAAqB,aAAa,aAAa,QAAQ,EAAE,UAAU,QAAW,MAAM,QAAQ,CAAC;AAAA,IAC9G,UAAE;AACA,cAAQ,IAAI;AAAA,IACd;AAAA,EACF;AAEA,MAAI,CAAC,UAAW,QAAO,oBAAC,OAAE,WAAU,0BAAyB,uCAAoB;AAEjF,QAAM,OAAO,OAAO,mBAAmB,OAAO,CAAC,MAAM,KAAK,SAAS,EAAE,GAAsB,CAAC,IAAI;AAEhG,SACE,qBAAC,SAAI,WAAU,cACb;AAAA,yBAAC,OAAE,WAAU,0BAAyB;AAAA;AAAA,MAEyB;AAAA,MAAkB;AAAA,OACjF;AAAA,IAEC,KAAK,IAAI,CAAC,QAAQ;AACjB,YAAM,MAAM,IAAI;AAChB,YAAM,MAAM,UAAU,GAAG,KAAK,CAAC;AAC/B,YAAM,MAAM,CAAC,UACX,aAAa,CAAC,OAAO,EAAE,GAAG,GAAG,CAAC,GAAG,GAAG,EAAE,GAAG,IAAI,GAAG,GAAG,GAAG,MAAM,EAAE,EAAE;AAClE,aACE,qBAAC,QACC;AAAA,6BAAC,SAAI,WAAU,+CACb;AAAA,+BAAC,SACC;AAAA,gCAAC,QAAG,WAAU,yBAAyB,cAAI,OAAM;AAAA,YACjD,oBAAC,OAAE,WAAU,0BAA0B,cAAI,MAAK;AAAA,aAClD;AAAA,UACA,oBAAC,UAAO,MAAK,MAAK,SAAQ,aAAY,UAAU,SAAS,MAAM,SAAS,MAAM,QAAQ,GAAG,GACtF,mBAAS,MAAM,oBAAe,4BACjC;AAAA,WACF;AAAA,QACA,qBAAC,SAAI,WAAU,cACb;AAAA,8BAAC,SAAM,OAAM,aAAY,MAAK,oBAC5B,8BAAC,SAAM,OAAO,IAAI,WAAW,IAAI,UAAU,CAAC,MAAM,IAAI,EAAE,SAAS,EAAE,OAAO,MAAM,CAAC,GAAG,GACtF;AAAA,UACA,oBAAC,SAAM,OAAM,SAAQ,MAAK,oBACxB,8BAAC,YAAS,MAAM,GAAG,OAAO,IAAI,YAAY,IAAI,UAAU,CAAC,MAAM,IAAI,EAAE,UAAU,EAAE,OAAO,MAAM,CAAC,GAAG,GACpG;AAAA,UACA,qBAAC,OAAE,WAAU,0BAAyB;AAAA;AAAA,YACvB;AAAA,YACb,oBAAC,UAAK,WAAU,gDAAgD,cAAI,cAAa;AAAA,aACnF;AAAA,WACF;AAAA,WArBS,GAsBX;AAAA,IAEJ,CAAC;AAAA,IAED,oBAAC,SACC,8BAAC,UAAO,SAAS,MAAM,UAAU,SAAS,MACvC,mBAAS,SAAS,kBAAa,yBAClC,GACF;AAAA,KACF;AAEJ;","names":[]}
@@ -17,7 +17,6 @@ import { useConfirm } from "../ui/ConfirmDialog";
17
17
  import { ModuleSettingsForm } from "./moduleSettingsForm";
18
18
  import { SnapshotsCard } from "./SnapshotsCard";
19
19
  import { MailSettings } from "./MailSettings";
20
- import { MailTemplates } from "./MailTemplates";
21
20
  function SettingsPanel({ siteId }) {
22
21
  const { toast } = useToast();
23
22
  const confirm = useConfirm();
@@ -260,9 +259,16 @@ function SettingsPanel({ siteId }) {
260
259
  }) })
261
260
  ] }),
262
261
  /* @__PURE__ */ jsx(MailSettings, { siteId }),
263
- /* @__PURE__ */ jsxs("div", { children: [
264
- /* @__PURE__ */ jsx("h3", { className: "mb-2 text-base font-semibold", children: "E-mailteksten" }),
265
- /* @__PURE__ */ jsx(MailTemplates, { siteId })
262
+ /* @__PURE__ */ jsxs(Card, { children: [
263
+ /* @__PURE__ */ jsx("h3", { className: "mb-1 text-base font-semibold", children: "E-mailteksten" }),
264
+ /* @__PURE__ */ jsxs("p", { className: "text-sm text-slate-600", children: [
265
+ "De teksten van automatische e-mails bewerk je bij het onderdeel zelf: formulier-mails onder",
266
+ " ",
267
+ /* @__PURE__ */ jsx("strong", { children: "Formulieren" }),
268
+ ", webwinkel-mails onder ",
269
+ /* @__PURE__ */ jsx("strong", { children: "Webwinkel \u2192 E-mails" }),
270
+ ". Provider en opmaak (logo, huisstijlkleur) gelden site-breed en stel je hier in."
271
+ ] })
266
272
  ] }),
267
273
  /* @__PURE__ */ jsxs(Card, { children: [
268
274
  /* @__PURE__ */ jsx("h3", { className: "mb-1 text-base font-semibold", children: "Formulier-afzender" }),