@xiwebdesign/xicms-core 7.1.0 → 7.3.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/XiCMSProvider.d.ts +2 -8
- package/dist/XiCMSProvider.js +1 -8
- package/dist/XiCMSProvider.js.map +1 -1
- package/dist/admin/AdminApp.js +7 -12
- package/dist/admin/AdminApp.js.map +1 -1
- package/dist/admin/NewsEditor.d.ts +8 -1
- package/dist/admin/NewsEditor.js +19 -18
- package/dist/admin/NewsEditor.js.map +1 -1
- package/dist/admin/SuperadminPanel.js +6 -7
- package/dist/admin/SuperadminPanel.js.map +1 -1
- package/dist/admin/customBlocks/CustomBlocksPanel.js +3 -4
- package/dist/admin/customBlocks/CustomBlocksPanel.js.map +1 -1
- package/dist/admin/lib/pagesClient.js +3 -4
- package/dist/admin/lib/pagesClient.js.map +1 -1
- package/dist/admin/lib/symbolsClient.js +3 -4
- package/dist/admin/lib/symbolsClient.js.map +1 -1
- package/dist/admin/pages/PagesPanel.js +5 -2
- package/dist/admin/pages/PagesPanel.js.map +1 -1
- package/dist/admin/settings/MailSettings.js +2 -3
- package/dist/admin/settings/MailSettings.js.map +1 -1
- package/dist/admin/settings/MailTemplates.js +2 -3
- package/dist/admin/settings/MailTemplates.js.map +1 -1
- package/dist/admin/settings/SettingsPanel.js +250 -225
- package/dist/admin/settings/SettingsPanel.js.map +1 -1
- package/dist/admin/settings/SnapshotsCard.js +3 -4
- package/dist/admin/settings/SnapshotsCard.js.map +1 -1
- package/dist/admin/shop/lib.js +2 -3
- package/dist/admin/shop/lib.js.map +1 -1
- package/dist/admin/visual/VisualEditor.js +239 -103
- package/dist/admin/visual/VisualEditor.js.map +1 -1
- package/dist/admin.css +1 -1
- package/dist/blocks/builtins/Agenda.js +6 -6
- package/dist/blocks/builtins/Agenda.js.map +1 -1
- package/dist/blocks/builtins/Banner.js +1 -1
- package/dist/blocks/builtins/Banner.js.map +1 -1
- package/dist/blocks/builtins/Breadcrumbs.js +5 -2
- package/dist/blocks/builtins/Breadcrumbs.js.map +1 -1
- package/dist/blocks/builtins/Cards.js +2 -2
- package/dist/blocks/builtins/Cards.js.map +1 -1
- package/dist/blocks/builtins/Contact.js +3 -3
- package/dist/blocks/builtins/Contact.js.map +1 -1
- package/dist/blocks/builtins/Cta.js +3 -3
- package/dist/blocks/builtins/Cta.js.map +1 -1
- package/dist/blocks/builtins/Downloads.js +5 -5
- package/dist/blocks/builtins/Downloads.js.map +1 -1
- package/dist/blocks/builtins/Elements.js +8 -5
- package/dist/blocks/builtins/Elements.js.map +1 -1
- package/dist/blocks/builtins/Hero.js +1 -1
- package/dist/blocks/builtins/Hero.js.map +1 -1
- package/dist/blocks/builtins/Jobs.js +4 -1
- package/dist/blocks/builtins/Jobs.js.map +1 -1
- package/dist/blocks/builtins/MapBlock.js +11 -11
- package/dist/blocks/builtins/MapBlock.js.map +1 -1
- package/dist/blocks/builtins/Newsletter.js +3 -2
- package/dist/blocks/builtins/Newsletter.js.map +1 -1
- package/dist/blocks/builtins/OpeningHours.js +2 -2
- package/dist/blocks/builtins/OpeningHours.js.map +1 -1
- package/dist/blocks/builtins/PdfBlock.js +2 -2
- package/dist/blocks/builtins/PdfBlock.js.map +1 -1
- package/dist/blocks/builtins/Portfolio.js +4 -4
- package/dist/blocks/builtins/Portfolio.js.map +1 -1
- package/dist/blocks/builtins/Pricing.js +3 -3
- package/dist/blocks/builtins/Pricing.js.map +1 -1
- package/dist/blocks/builtins/Sections.js +7 -7
- package/dist/blocks/builtins/Sections.js.map +1 -1
- package/dist/blocks/builtins/Team.js +3 -3
- package/dist/blocks/builtins/Team.js.map +1 -1
- package/dist/blocks/builtins/Widgets.js +28 -28
- package/dist/blocks/builtins/Widgets.js.map +1 -1
- package/dist/blocks/builtins/lib/submitForm.js +3 -4
- package/dist/blocks/builtins/lib/submitForm.js.map +1 -1
- package/dist/blocks/ui/index.d.ts +8 -1
- package/dist/blocks/ui/index.js +3 -2
- package/dist/blocks/ui/index.js.map +1 -1
- package/dist/components/EditBar.d.ts +9 -1
- package/dist/components/EditBar.js +6 -5
- package/dist/components/EditBar.js.map +1 -1
- package/dist/components/PageView.js +3 -5
- package/dist/components/PageView.js.map +1 -1
- package/dist/firebase/admin.js +4 -0
- package/dist/firebase/admin.js.map +1 -1
- package/dist/firebase/client.js +23 -5
- package/dist/firebase/client.js.map +1 -1
- package/dist/firebase/emulators.d.ts +9 -0
- package/dist/firebase/emulators.js +24 -0
- package/dist/firebase/emulators.js.map +1 -0
- package/dist/firebase/functionsClient.d.ts +18 -0
- package/dist/firebase/functionsClient.js +17 -0
- package/dist/firebase/functionsClient.js.map +1 -0
- package/dist/lock/DemoLock.js +19 -2
- package/dist/lock/DemoLock.js.map +1 -1
- package/dist/modules/shop/cartStore.js +2 -1
- package/dist/modules/shop/cartStore.js.map +1 -1
- package/dist/modules/shop/lib.js +9 -10
- package/dist/modules/shop/lib.js.map +1 -1
- package/dist/server/index.d.ts +1 -1
- package/dist/server/index.js +2 -0
- package/dist/server/index.js.map +1 -1
- package/dist/server/pages.d.ts +11 -0
- package/dist/server/pages.js +47 -0
- package/dist/server/pages.js.map +1 -1
- package/dist/server/version.d.ts +7 -0
- package/dist/server/version.js +2 -1
- package/dist/server/version.js.map +1 -1
- package/dist/styles.css +130 -29
- package/dist/theme/color.d.ts +17 -0
- package/dist/theme/color.js +12 -0
- package/dist/theme/color.js.map +1 -0
- package/dist/version.js +1 -1
- package/package.json +1 -1
package/dist/XiCMSProvider.d.ts
CHANGED
|
@@ -3,6 +3,8 @@ import { type BlockDefinition } from "./blocks/registry.js";
|
|
|
3
3
|
import { type CustomBlock as CustomBlockData } from "./schema/customBlocks.js";
|
|
4
4
|
import { type BlockOverrides } from "./modules/overrides.js";
|
|
5
5
|
import type { ModuleDefinition } from "./modules/types.js";
|
|
6
|
+
import { isDarkColor } from "./theme/color.js";
|
|
7
|
+
export { isDarkColor };
|
|
6
8
|
import { type PublicSiteSettings } from "./schema/settings.js";
|
|
7
9
|
import { type DefinedTheme } from "@astryxdesign/core/theme";
|
|
8
10
|
import { type LinkComponentType } from "@astryxdesign/core/Link";
|
|
@@ -72,11 +74,3 @@ export declare function useThemeVars(): {
|
|
|
72
74
|
vars: Record<string, string>;
|
|
73
75
|
darkAuto: boolean;
|
|
74
76
|
};
|
|
75
|
-
/**
|
|
76
|
-
* Is deze (hex-)kleur donker? Bepaalt de Astryx-thememodus voor sites met een vaste donkere
|
|
77
|
-
* achtergrond (bv. presets donker-modern/luxe-donker, `dark.auto: false`): zonder deze afleiding
|
|
78
|
-
* bleef Astryx op "light" staan en tekenden zijn componenten lichte kaarten op een donkere pagina.
|
|
79
|
-
* Puur en exporteerbaar voor tests; niet-hex waarden gelden als licht (veilige default).
|
|
80
|
-
*/
|
|
81
|
-
export declare function isDarkColor(color?: string): boolean;
|
|
82
|
-
export {};
|
package/dist/XiCMSProvider.js
CHANGED
|
@@ -13,6 +13,7 @@ import { ModulesProvider } from "./modules/context.js";
|
|
|
13
13
|
import { applyBlockOverrides } from "./modules/overrides.js";
|
|
14
14
|
import { builtinModules } from "./modules/builtins.js";
|
|
15
15
|
import { EditorProvider } from "./editor/EditorProvider.js";
|
|
16
|
+
import { isDarkColor } from "./theme/color.js";
|
|
16
17
|
import { SiteSettingsProvider, useSiteSettings, useSiteSettingsLiveSync } from "./settings/useSiteSettings.js";
|
|
17
18
|
import { mergeThemeTokens } from "./schema/settings.js";
|
|
18
19
|
import { initAppCheck } from "./firebase/appcheck.js";
|
|
@@ -128,14 +129,6 @@ function themeCssVars(theme) {
|
|
|
128
129
|
}
|
|
129
130
|
return cssVars;
|
|
130
131
|
}
|
|
131
|
-
function isDarkColor(color) {
|
|
132
|
-
const m = /^#([0-9a-f]{3}|[0-9a-f]{6})$/i.exec(color?.trim() ?? "");
|
|
133
|
-
if (!m) return false;
|
|
134
|
-
let hex = m[1];
|
|
135
|
-
if (hex.length === 3) hex = hex.replace(/./g, (c) => c + c);
|
|
136
|
-
const [r, g, b] = [0, 2, 4].map((i) => parseInt(hex.slice(i, i + 2), 16) / 255);
|
|
137
|
-
return 0.2126 * r + 0.7152 * g + 0.0722 * b < 0.45;
|
|
138
|
-
}
|
|
139
132
|
function ModuleLayer({
|
|
140
133
|
config,
|
|
141
134
|
blocks = NO_BLOCKS,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/XiCMSProvider.tsx"],"sourcesContent":["\"use client\";\n\nimport { createContext, useContext, useEffect, useMemo, useState, type ReactNode } from \"react\";\nimport { ConfigProvider } from \"./config\";\nimport { AuthProvider, useAuth } from \"./auth/AuthProvider\";\nimport { canEdit } from \"./auth/roles\";\nimport { RegistryProvider } from \"./blocks/context\";\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 { 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 { 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).\n *\n * De ZICHTBARE site-chrome (bewerkbalk, cookiebanner, demo) staat in `<SiteChrome />` en hoort in\n * de layout van je publieke routes — niet hier, anders verschijnt hij ook over /beheer.\n */\nexport function XiCMSProvider({ config, blocks = NO_BLOCKS, modules = NO_MODULES, overrides, astryxTheme, linkComponent, imageComponent, customBlocksData, settingsData, children }: ProviderProps) {\n useEffect(() => {\n // App Check hoort bij het praten mét Firebase; een bezoeker doet dat niet. Alleen aanroepen\n // als er een sleutel is, en de module zelf laadt zijn SDK dynamisch.\n if (!process.env.NEXT_PUBLIC_FIREBASE_APPCHECK_KEY) return;\n void initAppCheck();\n }, []);\n\n // Dev-vangnet: ontbrekende SSR-seeds. Zonder `settingsData` haalt élke bezoeker de publieke\n // settings zelf uit Firestore — één read per bezoeker, én het downloaden van de Firebase-SDK\n // (~680 KB) die anders nooit nodig is. Dat faalt volkomen stil: de site ziet er goed uit.\n // De referentiesite groenhof liep hier maandenlang op, dus: melden zodra het misgaat.\n useEffect(() => {\n if (process.env.NODE_ENV === \"production\") return;\n if (!process.env.NEXT_PUBLIC_FIREBASE_PROJECT_ID) return; // geen Firebase → geen client-read\n if (settingsData === undefined) {\n warnOnce(\n \"[xicms] `settingsData` ontbreekt op XiCMSProvider. Elke bezoeker doet daardoor zélf een \" +\n \"Firestore-read voor de site-instellingen en downloadt de Firebase-SDK. Lees ze server-side \" +\n \"in je layout met `getPublicSiteSettings(siteId)` en geef ze door — zie template/app/layout.tsx.\",\n );\n }\n if (customBlocksData === undefined) {\n warnOnce(\n \"[xicms] `customBlocksData` ontbreekt op XiCMSProvider. Custom blocks worden dan pas na \" +\n \"hydratie zichtbaar (SEO-gat + flits). Lees ze server-side met `getSiteCustomBlocks(siteId)`.\",\n );\n }\n }, [settingsData, customBlocksData]);\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 * De berekende theme-tokens, beschikbaar BUITEN `.xicms-root`.\n *\n * De visuele editor rendert als overlay naast die div (EditorProvider staat erboven in de boom),\n * dus zijn canvas erfde de merk-tokens niet: een groene primaire kleur werd daar de globale\n * indigo-fallback, en knoptekst kon zelfs samenvallen met de knopachtergrond. Door dezelfde\n * `cssVars` via context te delen toont het canvas exact wat de bezoeker straks ziet — zonder de\n * mapping te dupliceren of de overlay-structuur te verbouwen.\n */\nconst ThemeVarsCtx = createContext<{ vars: Record<string, string>; darkAuto: boolean }>({ vars: {}, darkAuto: false });\n\n/** Theme-tokens van de live site — voor UI die buiten `.xicms-root` rendert (de editor). */\nexport function useThemeVars() {\n return useContext(ThemeVarsCtx);\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 */\n/**\n * De donkere modus in één plek bepaald, zodat de tokenwaarden en de `data-dark`/`color-scheme` op\n * `.xicms-root` nooit uit elkaar kunnen lopen. `mode` wint; zonder `mode` valt hij terug op de\n * oude boolean, zodat bestaande site-configs (auto: true/false) ongewijzigd blijven werken.\n */\nfunction themeDarkMode(theme: ThemeTokens): \"light\" | \"dark\" | \"auto\" {\n return theme.dark?.mode ?? (theme.dark?.auto ? \"auto\" : \"light\");\n}\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 // Driestand. `mode` wint; zonder `mode` valt hij terug op de oude boolean, zodat bestaande\n // site-configs (auto: true/false) ongewijzigd blijven werken.\n const donkerModus = themeDarkMode(theme);\n const auto = donkerModus === \"auto\";\n // Bij \"auto\" schakelt de tokenwaarde zélf mee via light-dark(); bij een vaste stand kiezen we hier\n // al. Dat moet op tokenniveau en niet via een @media-blok, want deze variabelen staan INLINE op\n // `.xicms-root` en inline wint altijd van een media-query in styles.css.\n const ld = (light: string, dark: string) =>\n donkerModus === \"auto\" ? `light-dark(${light}, ${dark})` : donkerModus === \"dark\" ? 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 <PreviewDraftSync />\n <CustomBlocksSync siteId={config.siteId} onData={setCustomBlocks} />\n <SiteSettingsSync siteId={config.siteId} />\n <ThemeVarsCtx.Provider value={{ vars: cssVars, darkAuto: themeDarkMode(theme) === \"auto\" }}>\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={themeDarkMode(theme) === \"auto\" ? \"system\" : themeDarkMode(theme) === \"dark\" || 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 // Stuurt `color-scheme` (styles.css): \"auto\" = light dark, \"dark\" = dark.\n data-dark={themeDarkMode(theme) === \"light\" ? undefined : themeDarkMode(theme)}\n style={cssVars as React.CSSProperties}\n >\n {/* De zichtbare site-chrome (demo-balk, bewerkbalk, feedback, cookiebanner) is\n BEWUST geen onderdeel meer van de provider: die hoort bij het site-oppervlak\n en werd zo ook over /beheer heen gerenderd — een cookiebanner en een\n \"Zet live\"-balk bovenop het CMS. Sites renderen nu `<SiteChrome />` in hun\n eigen (site)-layout; zie ./SiteChrome.tsx. */}\n {children}\n </div>\n </ImageProvider>\n </LinkProvider>\n </Theme>\n </EditorProvider>\n </ThemeVarsCtx.Provider>\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 */\n/**\n * Zet Next's Draft Mode aan zodra een editor is ingelogd, zodat de pagina-route de gecommitte\n * content-bundel + de ISR-cache overslaat (server/pages.ts, `opts.skipBundle`) en deze browser\n * altijd de nieuwste GEPUBLICEERDE stand ziet — een publicatie in /beheer komt anders pas via het\n * ISR-vangnet mee.\n *\n * WAAROM EEN FETCH EN GEEN `document.cookie` ZOALS HIERVOOR: de vorige opzet zette zelf een\n * `xicms-preview`-cookie, die een proxy vertaalde naar `?preview=1`. Dat werkte niet: een route\n * die `searchParams` in zijn signatuur heeft merkt Next áltijd als dynamisch — ongeacht of de\n * proxy ooit herschrijft. Gemeten op groenhof: `ƒ (Dynamic)` met\n * `Cache-Control: private, no-cache, no-store` voor élke bezoeker. De cache was dus voor iedereen\n * stuk om een knop te bedienen die alleen de beheerder gebruikt.\n *\n * Draft Mode lost precies dát op: het zet de ondertekende `__prerender_bypass`-cookie die Next\n * VÓÓR de cache controleert, buiten de route om. Gemeten met dezelfde opzet: publiek houdt\n * `● (SSG)` met `s-maxage=300`, terwijl het request mét cookie `private, no-cache, no-store`\n * krijgt en vers rendert.\n *\n * Draagt geen rechten — net als de oude cookie. Er wordt nooit meer getoond dan de al-publieke,\n * gepubliceerde content, dus zelf `/api/draft` aanroepen levert hoogstens een verse blik op iets\n * dat toch al openbaar is. Wat het wél kost is een ongecachte render per request; wil je dat\n * dichttimmeren, laat de route dan een Firebase-ID-token verifiëren voor hij `enable()` doet.\n *\n * Faalt stil en onschadelijk als de site `/api/draft` niet heeft (zie template/): dan blijft de\n * beheerder gewoon op de gecachte stand, precies zoals een site zonder preview.\n */\nfunction PreviewDraftSync() {\n const { role } = useAuth();\n useEffect(() => {\n const gewenst = canEdit(role);\n // Alleen aanroepen als de stand écht moet veranderen. Zonder deze vergelijking zou élke\n // publieke bezoeker bij élke paginaweergave een POST doen om preview \"uit\" te zetten die al\n // uit stond — een serverless-aanroep per bezoeker, precies de kosten die deze hele omzetting\n // moet vermijden.\n //\n // De marker is een SESSIE-cookie, net als Next's `__prerender_bypass`: die laatste is httpOnly\n // en dus onleesbaar voor deze code, dus we houden er zelf een spiegel van bij. Omdat beide\n // met het browservenster verdwijnen lopen ze niet uit de pas — een marker die de cookie\n // overleeft zou preview stilletjes uitgeschakeld laten.\n const gemarkeerd = document.cookie.includes(\"xicms-draft=1\");\n if (gewenst === gemarkeerd) return;\n void fetch(`/api/draft?aan=${gewenst ? \"1\" : \"0\"}`, { method: \"POST\" })\n .then((r) => {\n if (!r.ok) return; // site zonder /api/draft: laat de marker met rust, dan blijft dit stil.\n document.cookie = gewenst ? \"xicms-draft=1; path=/; samesite=lax\" : \"xicms-draft=; path=/; max-age=0\";\n })\n .catch(() => {});\n }, [role]);\n return null;\n}\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 // Alleen editors luisteren mee (kosten + verbinding), en de SDK laadt pas dán.\n if (!editing) return;\n let unsub: (() => void) | undefined;\n let afgebroken = false;\n void (async () => {\n const [{ getDb }, { collection, onSnapshot }] = await Promise.all([\n import(\"./firebase/client\"),\n import(\"firebase/firestore\"),\n ]);\n const db = getDb();\n if (!db || afgebroken) return;\n unsub = 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 })();\n return () => {\n afgebroken = true;\n unsub?.();\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 { createContext, useContext, useEffect, useMemo, useState, type ReactNode } from \"react\";\nimport { ConfigProvider } from \"./config\";\nimport { AuthProvider, useAuth } from \"./auth/AuthProvider\";\nimport { canEdit } from \"./auth/roles\";\nimport { RegistryProvider } from \"./blocks/context\";\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 { EditorProvider } from \"./editor/EditorProvider\";\n// Puur en dependency-vrij; hier alleen nog geherexporteerd zodat de publieke API gelijk blijft.\nimport { isDarkColor } from \"./theme/color\";\nexport { isDarkColor };\nimport { SiteSettingsProvider, useSiteSettings, useSiteSettingsLiveSync } from \"./settings/useSiteSettings\";\nimport { mergeThemeTokens, type ThemeTokens, type PublicSiteSettings } from \"./schema/settings\";\nimport { initAppCheck } from \"./firebase/appcheck\";\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).\n *\n * De ZICHTBARE site-chrome (bewerkbalk, cookiebanner, demo) staat in `<SiteChrome />` en hoort in\n * de layout van je publieke routes — niet hier, anders verschijnt hij ook over /beheer.\n */\nexport function XiCMSProvider({ config, blocks = NO_BLOCKS, modules = NO_MODULES, overrides, astryxTheme, linkComponent, imageComponent, customBlocksData, settingsData, children }: ProviderProps) {\n useEffect(() => {\n // App Check hoort bij het praten mét Firebase; een bezoeker doet dat niet. Alleen aanroepen\n // als er een sleutel is, en de module zelf laadt zijn SDK dynamisch.\n if (!process.env.NEXT_PUBLIC_FIREBASE_APPCHECK_KEY) return;\n void initAppCheck();\n }, []);\n\n // Dev-vangnet: ontbrekende SSR-seeds. Zonder `settingsData` haalt élke bezoeker de publieke\n // settings zelf uit Firestore — één read per bezoeker, én het downloaden van de Firebase-SDK\n // (~680 KB) die anders nooit nodig is. Dat faalt volkomen stil: de site ziet er goed uit.\n // De referentiesite groenhof liep hier maandenlang op, dus: melden zodra het misgaat.\n useEffect(() => {\n if (process.env.NODE_ENV === \"production\") return;\n if (!process.env.NEXT_PUBLIC_FIREBASE_PROJECT_ID) return; // geen Firebase → geen client-read\n if (settingsData === undefined) {\n warnOnce(\n \"[xicms] `settingsData` ontbreekt op XiCMSProvider. Elke bezoeker doet daardoor zélf een \" +\n \"Firestore-read voor de site-instellingen en downloadt de Firebase-SDK. Lees ze server-side \" +\n \"in je layout met `getPublicSiteSettings(siteId)` en geef ze door — zie template/app/layout.tsx.\",\n );\n }\n if (customBlocksData === undefined) {\n warnOnce(\n \"[xicms] `customBlocksData` ontbreekt op XiCMSProvider. Custom blocks worden dan pas na \" +\n \"hydratie zichtbaar (SEO-gat + flits). Lees ze server-side met `getSiteCustomBlocks(siteId)`.\",\n );\n }\n }, [settingsData, customBlocksData]);\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 * De berekende theme-tokens, beschikbaar BUITEN `.xicms-root`.\n *\n * De visuele editor rendert als overlay naast die div (EditorProvider staat erboven in de boom),\n * dus zijn canvas erfde de merk-tokens niet: een groene primaire kleur werd daar de globale\n * indigo-fallback, en knoptekst kon zelfs samenvallen met de knopachtergrond. Door dezelfde\n * `cssVars` via context te delen toont het canvas exact wat de bezoeker straks ziet — zonder de\n * mapping te dupliceren of de overlay-structuur te verbouwen.\n */\nconst ThemeVarsCtx = createContext<{ vars: Record<string, string>; darkAuto: boolean }>({ vars: {}, darkAuto: false });\n\n/** Theme-tokens van de live site — voor UI die buiten `.xicms-root` rendert (de editor). */\nexport function useThemeVars() {\n return useContext(ThemeVarsCtx);\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 */\n/**\n * De donkere modus in één plek bepaald, zodat de tokenwaarden en de `data-dark`/`color-scheme` op\n * `.xicms-root` nooit uit elkaar kunnen lopen. `mode` wint; zonder `mode` valt hij terug op de\n * oude boolean, zodat bestaande site-configs (auto: true/false) ongewijzigd blijven werken.\n */\nfunction themeDarkMode(theme: ThemeTokens): \"light\" | \"dark\" | \"auto\" {\n return theme.dark?.mode ?? (theme.dark?.auto ? \"auto\" : \"light\");\n}\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 // Driestand. `mode` wint; zonder `mode` valt hij terug op de oude boolean, zodat bestaande\n // site-configs (auto: true/false) ongewijzigd blijven werken.\n const donkerModus = themeDarkMode(theme);\n const auto = donkerModus === \"auto\";\n // Bij \"auto\" schakelt de tokenwaarde zélf mee via light-dark(); bij een vaste stand kiezen we hier\n // al. Dat moet op tokenniveau en niet via een @media-blok, want deze variabelen staan INLINE op\n // `.xicms-root` en inline wint altijd van een media-query in styles.css.\n const ld = (light: string, dark: string) =>\n donkerModus === \"auto\" ? `light-dark(${light}, ${dark})` : donkerModus === \"dark\" ? 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 * 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 <PreviewDraftSync />\n <CustomBlocksSync siteId={config.siteId} onData={setCustomBlocks} />\n <SiteSettingsSync siteId={config.siteId} />\n <ThemeVarsCtx.Provider value={{ vars: cssVars, darkAuto: themeDarkMode(theme) === \"auto\" }}>\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={themeDarkMode(theme) === \"auto\" ? \"system\" : themeDarkMode(theme) === \"dark\" || 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 // Stuurt `color-scheme` (styles.css): \"auto\" = light dark, \"dark\" = dark.\n data-dark={themeDarkMode(theme) === \"light\" ? undefined : themeDarkMode(theme)}\n style={cssVars as React.CSSProperties}\n >\n {/* De zichtbare site-chrome (demo-balk, bewerkbalk, feedback, cookiebanner) is\n BEWUST geen onderdeel meer van de provider: die hoort bij het site-oppervlak\n en werd zo ook over /beheer heen gerenderd — een cookiebanner en een\n \"Zet live\"-balk bovenop het CMS. Sites renderen nu `<SiteChrome />` in hun\n eigen (site)-layout; zie ./SiteChrome.tsx. */}\n {children}\n </div>\n </ImageProvider>\n </LinkProvider>\n </Theme>\n </EditorProvider>\n </ThemeVarsCtx.Provider>\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 */\n/**\n * Zet Next's Draft Mode aan zodra een editor is ingelogd, zodat de pagina-route de gecommitte\n * content-bundel + de ISR-cache overslaat (server/pages.ts, `opts.skipBundle`) en deze browser\n * altijd de nieuwste GEPUBLICEERDE stand ziet — een publicatie in /beheer komt anders pas via het\n * ISR-vangnet mee.\n *\n * WAAROM EEN FETCH EN GEEN `document.cookie` ZOALS HIERVOOR: de vorige opzet zette zelf een\n * `xicms-preview`-cookie, die een proxy vertaalde naar `?preview=1`. Dat werkte niet: een route\n * die `searchParams` in zijn signatuur heeft merkt Next áltijd als dynamisch — ongeacht of de\n * proxy ooit herschrijft. Gemeten op groenhof: `ƒ (Dynamic)` met\n * `Cache-Control: private, no-cache, no-store` voor élke bezoeker. De cache was dus voor iedereen\n * stuk om een knop te bedienen die alleen de beheerder gebruikt.\n *\n * Draft Mode lost precies dát op: het zet de ondertekende `__prerender_bypass`-cookie die Next\n * VÓÓR de cache controleert, buiten de route om. Gemeten met dezelfde opzet: publiek houdt\n * `● (SSG)` met `s-maxage=300`, terwijl het request mét cookie `private, no-cache, no-store`\n * krijgt en vers rendert.\n *\n * Draagt geen rechten — net als de oude cookie. Er wordt nooit meer getoond dan de al-publieke,\n * gepubliceerde content, dus zelf `/api/draft` aanroepen levert hoogstens een verse blik op iets\n * dat toch al openbaar is. Wat het wél kost is een ongecachte render per request; wil je dat\n * dichttimmeren, laat de route dan een Firebase-ID-token verifiëren voor hij `enable()` doet.\n *\n * Faalt stil en onschadelijk als de site `/api/draft` niet heeft (zie template/): dan blijft de\n * beheerder gewoon op de gecachte stand, precies zoals een site zonder preview.\n */\nfunction PreviewDraftSync() {\n const { role } = useAuth();\n useEffect(() => {\n const gewenst = canEdit(role);\n // Alleen aanroepen als de stand écht moet veranderen. Zonder deze vergelijking zou élke\n // publieke bezoeker bij élke paginaweergave een POST doen om preview \"uit\" te zetten die al\n // uit stond — een serverless-aanroep per bezoeker, precies de kosten die deze hele omzetting\n // moet vermijden.\n //\n // De marker is een SESSIE-cookie, net als Next's `__prerender_bypass`: die laatste is httpOnly\n // en dus onleesbaar voor deze code, dus we houden er zelf een spiegel van bij. Omdat beide\n // met het browservenster verdwijnen lopen ze niet uit de pas — een marker die de cookie\n // overleeft zou preview stilletjes uitgeschakeld laten.\n const gemarkeerd = document.cookie.includes(\"xicms-draft=1\");\n if (gewenst === gemarkeerd) return;\n void fetch(`/api/draft?aan=${gewenst ? \"1\" : \"0\"}`, { method: \"POST\" })\n .then((r) => {\n if (!r.ok) return; // site zonder /api/draft: laat de marker met rust, dan blijft dit stil.\n document.cookie = gewenst ? \"xicms-draft=1; path=/; samesite=lax\" : \"xicms-draft=; path=/; max-age=0\";\n })\n .catch(() => {});\n }, [role]);\n return null;\n}\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 // Alleen editors luisteren mee (kosten + verbinding), en de SDK laadt pas dán.\n if (!editing) return;\n let unsub: (() => void) | undefined;\n let afgebroken = false;\n void (async () => {\n const [{ getDb }, { collection, onSnapshot }] = await Promise.all([\n import(\"./firebase/client\"),\n import(\"firebase/firestore\"),\n ]);\n const db = getDb();\n if (!db || afgebroken) return;\n unsub = 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 })();\n return () => {\n afgebroken = true;\n unsub?.();\n };\n }, [editing, siteId, onData]);\n return null;\n}\n"],"mappings":"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","names":[]}
|
package/dist/admin/AdminApp.js
CHANGED
|
@@ -67,18 +67,13 @@ function AdminApp({ extraPanels = [] }) {
|
|
|
67
67
|
},
|
|
68
68
|
p.moduleId
|
|
69
69
|
)) }),
|
|
70
|
-
/* @__PURE__ */ jsxs("div", { className: "mt-auto hidden min-w-0 border-t border-slate-200 pt-3 md:
|
|
71
|
-
/* @__PURE__ */
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
className: "mt-2 rounded text-xs text-slate-500 underline hover:text-slate-900 focus-visible:ring-2 focus-visible:ring-brand-500 focus-visible:outline-none",
|
|
78
|
-
onClick: () => void signOut(),
|
|
79
|
-
children: "Uitloggen"
|
|
80
|
-
}
|
|
81
|
-
)
|
|
70
|
+
/* @__PURE__ */ jsxs("div", { className: "mt-auto hidden min-w-0 flex-col gap-3 border-t border-slate-200 pt-3 md:flex", children: [
|
|
71
|
+
/* @__PURE__ */ jsxs("div", { className: "min-w-0", children: [
|
|
72
|
+
/* @__PURE__ */ jsx("p", { className: "truncate text-xs font-medium text-slate-600", title: user.email ?? void 0, children: user.email }),
|
|
73
|
+
/* @__PURE__ */ jsx("p", { className: "text-xs text-slate-400", children: role })
|
|
74
|
+
] }),
|
|
75
|
+
/* @__PURE__ */ jsx(CoreVersion, {}),
|
|
76
|
+
/* @__PURE__ */ jsx(Button, { variant: "secondary", size: "sm", onClick: () => void signOut(), children: "Uitloggen" })
|
|
82
77
|
] })
|
|
83
78
|
]
|
|
84
79
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/admin/AdminApp.tsx"],"sourcesContent":["\"use client\";\n\nimport { useState } from \"react\";\nimport { useAuth } from \"../auth/AuthProvider\";\nimport { LoginForm } from \"../auth/LoginForm\";\nimport { useSiteConfig } from \"../config\";\nimport { useModules } from \"../modules/context\";\nimport { canEdit, canManageSite, type Role } from \"../auth/roles\";\nimport { Tabs, TabsContent } from \"./ui/tabs\";\nimport * as TabsPrimitive from \"@radix-ui/react-tabs\";\nimport { ToastProvider } from \"./ui/toast\";\nimport { ConfirmHost } from \"./ui/ConfirmDialog\";\nimport { Button } from \"./ui/primitives\";\nimport { PanelIcon } from \"./ui/PanelIcon\";\nimport { builtinPanels, type PanelDefinition } from \"./panelRegistry\";\nimport { AdminHelp } from \"./AdminHelp\";\nimport { CoreVersion } from \"./CoreVersion\";\n\n/**\n * Rol-bewaakt beheer (XiCMS 3.0). De secties komen uit de paneel-registry, gefilterd op de actieve\n * modules (`SiteConfig.modules`/`features`) en de rol van de gebruiker. Een module met een eigen\n * paneel (ook klant-modules via `extraPanels`) verschijnt automatisch.\n *\n * VORM: een schermvullend werkblad met een vaste zijbalk, geen kaart in de pagina. Het beheer viel\n * hiervoor binnen de site-chrome — nav erboven, footer eronder, cookiebanner eroverheen — en las\n * daardoor als een blok ergens op de website in plaats van als gereedschap. De site-chrome leeft nu\n * in `app/(site)/layout.tsx`; /beheer valt daarbuiten.\n *\n * De zijbalk verving de horizontale tabstrip omdat die bij zeven of meer panelen al omsloeg naar een\n * tweede regel. Verticaal is er bovendien ruimte voor een icoon naast het woord, en `orientation`\n * houdt de pijltjestoetsen correct (Radix).\n *\n * Wikkel in `.xicms-admin` zodat de Tailwind-stijlen uit `@xiwebdesign/xicms-core/admin.css` scoped\n * blijven en niet naar de site lekken. Importeer die CSS alleen op de /beheer-route.\n */\nfunction roleAllows(role: Role | null, min: PanelDefinition[\"minRole\"]): boolean {\n return min === \"siteadmin\" ? canManageSite(role) : canEdit(role);\n}\n\n/** Gecentreerde boodschap voor de toestanden waarin er (nog) geen beheer te tonen is. */\nfunction Melding({ children }: { children: React.ReactNode }) {\n return (\n <div className=\"grid min-h-screen place-items-center p-6\">\n <div className=\"w-full max-w-md rounded-xl border border-slate-200 bg-white p-6 text-sm shadow-sm\">{children}</div>\n </div>\n );\n}\n\nexport function AdminApp({ extraPanels = [] }: { extraPanels?: PanelDefinition[] }) {\n const { user, role, loading, configured, signOut } = useAuth();\n const { name, siteId } = useSiteConfig();\n const { activeIds } = useModules();\n\n // Panelen van actieve modules waar de rol toegang toe heeft, gesorteerd op volgorde.\n const panels = [...builtinPanels, ...extraPanels]\n .filter((p) => activeIds.has(p.moduleId) && roleAllows(role, p.minRole))\n .sort((a, b) => a.order - b.order);\n const firstTab = panels[0]?.moduleId;\n // Gecontroleerd i.p.v. `defaultValue`: het werkblad toont de naam van de sectie waar je in zit.\n // Zonder die kop lees je alleen aan de zijbalk-markering af waar je bent, en dat is te weinig\n // houvast op een breed scherm waar de zijbalk ver van je blik af staat.\n const [actief, setActief] = useState<string | undefined>(firstTab);\n const actievePanel = panels.find((p) => p.moduleId === (actief ?? firstTab));\n\n return (\n <div className=\"xicms-admin\">\n <ToastProvider>\n <ConfirmHost>\n {!configured ? (\n <Melding>\n Firebase is nog niet geconfigureerd. Vul de <code>NEXT_PUBLIC_FIREBASE_*</code>-variabelen in\n om het beheer aan te zetten.\n </Melding>\n ) : loading ? (\n <Melding>\n <span className=\"text-slate-500\">Bezig met laden…</span>\n </Melding>\n ) : !user ? (\n <div className=\"grid min-h-screen place-items-center p-6\">\n <div className=\"w-full max-w-sm\">\n <p className=\"mb-4 text-center text-sm text-slate-500\">Beheer · {name}</p>\n <LoginForm />\n </div>\n </div>\n ) : !canEdit(role) ? (\n <Melding>\n Je bent ingelogd als <strong>{user.email}</strong>, maar hebt geen beheerrechten voor deze site.\n <div className=\"mt-3\">\n <Button variant=\"secondary\" size=\"sm\" onClick={() => void signOut()}>Uitloggen</Button>\n </div>\n </Melding>\n ) : panels.length === 0 ? (\n <Melding>Geen actieve beheer-modules voor deze site.</Melding>\n ) : (\n <Tabs value={actief ?? firstTab} onValueChange={setActief} orientation=\"vertical\" className=\"min-h-screen md:grid md:grid-cols-[248px_1fr]\">\n {/* ── Zijbalk ─────────────────────────────────────────────────────────── */}\n <div\n className=\"flex flex-col gap-4 border-b border-slate-200 p-4 md:sticky md:top-0 md:h-screen md:border-r md:border-b-0\"\n style={{ background: \"var(--admin-sidebar)\" }}\n >\n <div className=\"min-w-0 px-2 pt-1\">\n <p className=\"truncate text-sm font-semibold\">{name}</p>\n <p className=\"text-xs text-slate-500\">Beheer</p>\n </div>\n\n <TabsPrimitive.List aria-label=\"Beheer-secties\" className=\"-mx-1 flex gap-1 overflow-x-auto md:flex-col md:overflow-visible\">\n {panels.map((p) => (\n <TabsPrimitive.Trigger\n key={p.moduleId}\n value={p.moduleId}\n className=\"flex shrink-0 items-center gap-2.5 rounded-lg px-3 py-2 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 data-[state=active]:bg-slate-100 data-[state=active]:text-slate-900 data-[state=active]:shadow-[inset_2px_0_0_0_var(--color-brand-500)]\"\n >\n <PanelIcon id={p.moduleId} />\n {p.label}\n </TabsPrimitive.Trigger>\n ))}\n </TabsPrimitive.List>\n\n <div className=\"mt-auto hidden min-w-0 border-t border-slate-200 pt-3 md:block\">\n <p className=\"truncate text-xs text-slate-500\">{user.email}</p>\n <p className=\"text-xs text-slate-400\">{role}</p>\n <div className=\"mt-2\"><CoreVersion /></div>\n <button\n className=\"mt-2 rounded text-xs text-slate-500 underline hover:text-slate-900 focus-visible:ring-2 focus-visible:ring-brand-500 focus-visible:outline-none\"\n onClick={() => void signOut()}\n >\n Uitloggen\n </button>\n </div>\n </div>\n\n {/* ── Werkblad ────────────────────────────────────────────────────────── */}\n <div className=\"min-w-0\">\n <header className=\"sticky top-0 z-10 flex items-center justify-between gap-4 border-b border-slate-200 px-4 py-3 md:px-8\" style={{ background: \"var(--admin-canvas)\" }}>\n <h1 className=\"truncate text-base font-semibold\">{actievePanel?.label ?? \"Beheer\"}</h1>\n <a\n className=\"inline-flex h-8 shrink-0 items-center rounded-lg border border-slate-200 px-3 text-sm hover:bg-slate-100 focus-visible:ring-2 focus-visible:ring-brand-500 focus-visible:outline-none\"\n href=\"/\"\n >\n Bekijk site ↗\n </a>\n </header>\n <div className=\"grid gap-6 p-4 md:p-8\">\n {panels.map((p) => (\n <TabsContent key={p.moduleId} value={p.moduleId} className=\"pt-0\">\n <p.Panel siteId={siteId} />\n </TabsContent>\n ))}\n <AdminHelp />\n </div>\n </div>\n </Tabs>\n )}\n </ConfirmHost>\n </ToastProvider>\n </div>\n );\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../src/admin/AdminApp.tsx"],"sourcesContent":["\"use client\";\n\nimport { useState } from \"react\";\nimport { useAuth } from \"../auth/AuthProvider\";\nimport { LoginForm } from \"../auth/LoginForm\";\nimport { useSiteConfig } from \"../config\";\nimport { useModules } from \"../modules/context\";\nimport { canEdit, canManageSite, type Role } from \"../auth/roles\";\nimport { Tabs, TabsContent } from \"./ui/tabs\";\nimport * as TabsPrimitive from \"@radix-ui/react-tabs\";\nimport { ToastProvider } from \"./ui/toast\";\nimport { ConfirmHost } from \"./ui/ConfirmDialog\";\nimport { Button } from \"./ui/primitives\";\nimport { PanelIcon } from \"./ui/PanelIcon\";\nimport { builtinPanels, type PanelDefinition } from \"./panelRegistry\";\nimport { AdminHelp } from \"./AdminHelp\";\nimport { CoreVersion } from \"./CoreVersion\";\n\n/**\n * Rol-bewaakt beheer (XiCMS 3.0). De secties komen uit de paneel-registry, gefilterd op de actieve\n * modules (`SiteConfig.modules`/`features`) en de rol van de gebruiker. Een module met een eigen\n * paneel (ook klant-modules via `extraPanels`) verschijnt automatisch.\n *\n * VORM: een schermvullend werkblad met een vaste zijbalk, geen kaart in de pagina. Het beheer viel\n * hiervoor binnen de site-chrome — nav erboven, footer eronder, cookiebanner eroverheen — en las\n * daardoor als een blok ergens op de website in plaats van als gereedschap. De site-chrome leeft nu\n * in `app/(site)/layout.tsx`; /beheer valt daarbuiten.\n *\n * De zijbalk verving de horizontale tabstrip omdat die bij zeven of meer panelen al omsloeg naar een\n * tweede regel. Verticaal is er bovendien ruimte voor een icoon naast het woord, en `orientation`\n * houdt de pijltjestoetsen correct (Radix).\n *\n * Wikkel in `.xicms-admin` zodat de Tailwind-stijlen uit `@xiwebdesign/xicms-core/admin.css` scoped\n * blijven en niet naar de site lekken. Importeer die CSS alleen op de /beheer-route.\n */\nfunction roleAllows(role: Role | null, min: PanelDefinition[\"minRole\"]): boolean {\n return min === \"siteadmin\" ? canManageSite(role) : canEdit(role);\n}\n\n/** Gecentreerde boodschap voor de toestanden waarin er (nog) geen beheer te tonen is. */\nfunction Melding({ children }: { children: React.ReactNode }) {\n return (\n <div className=\"grid min-h-screen place-items-center p-6\">\n <div className=\"w-full max-w-md rounded-xl border border-slate-200 bg-white p-6 text-sm shadow-sm\">{children}</div>\n </div>\n );\n}\n\nexport function AdminApp({ extraPanels = [] }: { extraPanels?: PanelDefinition[] }) {\n const { user, role, loading, configured, signOut } = useAuth();\n const { name, siteId } = useSiteConfig();\n const { activeIds } = useModules();\n\n // Panelen van actieve modules waar de rol toegang toe heeft, gesorteerd op volgorde.\n const panels = [...builtinPanels, ...extraPanels]\n .filter((p) => activeIds.has(p.moduleId) && roleAllows(role, p.minRole))\n .sort((a, b) => a.order - b.order);\n const firstTab = panels[0]?.moduleId;\n // Gecontroleerd i.p.v. `defaultValue`: het werkblad toont de naam van de sectie waar je in zit.\n // Zonder die kop lees je alleen aan de zijbalk-markering af waar je bent, en dat is te weinig\n // houvast op een breed scherm waar de zijbalk ver van je blik af staat.\n const [actief, setActief] = useState<string | undefined>(firstTab);\n const actievePanel = panels.find((p) => p.moduleId === (actief ?? firstTab));\n\n return (\n <div className=\"xicms-admin\">\n <ToastProvider>\n <ConfirmHost>\n {!configured ? (\n <Melding>\n Firebase is nog niet geconfigureerd. Vul de <code>NEXT_PUBLIC_FIREBASE_*</code>-variabelen in\n om het beheer aan te zetten.\n </Melding>\n ) : loading ? (\n <Melding>\n <span className=\"text-slate-500\">Bezig met laden…</span>\n </Melding>\n ) : !user ? (\n <div className=\"grid min-h-screen place-items-center p-6\">\n <div className=\"w-full max-w-sm\">\n <p className=\"mb-4 text-center text-sm text-slate-500\">Beheer · {name}</p>\n <LoginForm />\n </div>\n </div>\n ) : !canEdit(role) ? (\n <Melding>\n Je bent ingelogd als <strong>{user.email}</strong>, maar hebt geen beheerrechten voor deze site.\n <div className=\"mt-3\">\n <Button variant=\"secondary\" size=\"sm\" onClick={() => void signOut()}>Uitloggen</Button>\n </div>\n </Melding>\n ) : panels.length === 0 ? (\n <Melding>Geen actieve beheer-modules voor deze site.</Melding>\n ) : (\n <Tabs value={actief ?? firstTab} onValueChange={setActief} orientation=\"vertical\" className=\"min-h-screen md:grid md:grid-cols-[248px_1fr]\">\n {/* ── Zijbalk ─────────────────────────────────────────────────────────── */}\n <div\n className=\"flex flex-col gap-4 border-b border-slate-200 p-4 md:sticky md:top-0 md:h-screen md:border-r md:border-b-0\"\n style={{ background: \"var(--admin-sidebar)\" }}\n >\n <div className=\"min-w-0 px-2 pt-1\">\n <p className=\"truncate text-sm font-semibold\">{name}</p>\n <p className=\"text-xs text-slate-500\">Beheer</p>\n </div>\n\n <TabsPrimitive.List aria-label=\"Beheer-secties\" className=\"-mx-1 flex gap-1 overflow-x-auto md:flex-col md:overflow-visible\">\n {panels.map((p) => (\n <TabsPrimitive.Trigger\n key={p.moduleId}\n value={p.moduleId}\n className=\"flex shrink-0 items-center gap-2.5 rounded-lg px-3 py-2 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 data-[state=active]:bg-slate-100 data-[state=active]:text-slate-900 data-[state=active]:shadow-[inset_2px_0_0_0_var(--color-brand-500)]\"\n >\n <PanelIcon id={p.moduleId} />\n {p.label}\n </TabsPrimitive.Trigger>\n ))}\n </TabsPrimitive.List>\n\n {/* Voet van de zijbalk: wie ben ik, welke engine draait er, en eruit. Stond eerder als\n vier losse regels op elkaar gepakt — e-mail, rol, versie en een onderstreepte link\n liepen visueel in elkaar over. Nu drie duidelijke lagen met lucht ertussen, en\n Uitloggen als een echte knop in plaats van een tekstlink. */}\n <div className=\"mt-auto hidden min-w-0 flex-col gap-3 border-t border-slate-200 pt-3 md:flex\">\n <div className=\"min-w-0\">\n <p className=\"truncate text-xs font-medium text-slate-600\" title={user.email ?? undefined}>{user.email}</p>\n <p className=\"text-xs text-slate-400\">{role}</p>\n </div>\n <CoreVersion />\n <Button variant=\"secondary\" size=\"sm\" onClick={() => void signOut()}>\n Uitloggen\n </Button>\n </div>\n </div>\n\n {/* ── Werkblad ────────────────────────────────────────────────────────── */}\n <div className=\"min-w-0\">\n <header className=\"sticky top-0 z-10 flex items-center justify-between gap-4 border-b border-slate-200 px-4 py-3 md:px-8\" style={{ background: \"var(--admin-canvas)\" }}>\n <h1 className=\"truncate text-base font-semibold\">{actievePanel?.label ?? \"Beheer\"}</h1>\n <a\n className=\"inline-flex h-8 shrink-0 items-center rounded-lg border border-slate-200 px-3 text-sm hover:bg-slate-100 focus-visible:ring-2 focus-visible:ring-brand-500 focus-visible:outline-none\"\n href=\"/\"\n >\n Bekijk site ↗\n </a>\n </header>\n <div className=\"grid gap-6 p-4 md:p-8\">\n {panels.map((p) => (\n <TabsContent key={p.moduleId} value={p.moduleId} className=\"pt-0\">\n <p.Panel siteId={siteId} />\n </TabsContent>\n ))}\n <AdminHelp />\n </div>\n </div>\n </Tabs>\n )}\n </ConfirmHost>\n </ToastProvider>\n </div>\n );\n}\n"],"mappings":"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","names":[]}
|
|
@@ -1,2 +1,9 @@
|
|
|
1
|
-
/**
|
|
1
|
+
/**
|
|
2
|
+
* Beheer van nieuwsartikelen — zelfde look & feel als de rest van /beheer.
|
|
3
|
+
*
|
|
4
|
+
* Schrijft via callables, niet rechtstreeks naar Firestore. Nieuws was het laatste publieke
|
|
5
|
+
* contenttype dat de client zelf schreef; daardoor was er geen plek waar de cache-invalidatie kon
|
|
6
|
+
* plaatsvinden en verscheen een gepubliceerd artikel niet op een live-reads-site (SITE-AUDIT §11).
|
|
7
|
+
* De callable saneert, auditeert en verklaart de nieuws-tag ongeldig — net als bij symbolen.
|
|
8
|
+
*/
|
|
2
9
|
export declare function NewsEditor(): import("react").JSX.Element;
|
package/dist/admin/NewsEditor.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
3
3
|
import { useRef, useState } from "react";
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
4
|
+
import { getFirebaseApp } from "../firebase/client.js";
|
|
5
|
+
import { callable } from "../firebase/functionsClient.js";
|
|
6
6
|
import { useSiteConfig } from "../config.js";
|
|
7
7
|
import { useNews } from "../components/News.js";
|
|
8
8
|
import { uploadImage } from "./lib/upload.js";
|
|
@@ -39,32 +39,28 @@ function NewsEditor() {
|
|
|
39
39
|
});
|
|
40
40
|
}
|
|
41
41
|
async function save() {
|
|
42
|
-
const db = getDb();
|
|
43
|
-
if (!db) return;
|
|
44
42
|
if (!form.title.trim()) {
|
|
45
43
|
toast({ title: "Titel ontbreekt", description: "Geef het artikel een titel.", tone: "error" });
|
|
46
44
|
return;
|
|
47
45
|
}
|
|
48
46
|
setBusy(true);
|
|
49
47
|
try {
|
|
50
|
-
const
|
|
48
|
+
const app = getFirebaseApp();
|
|
49
|
+
if (!app) throw new Error("Firebase is niet geconfigureerd.");
|
|
50
|
+
await callable(app, "saveNewsArticle")({
|
|
51
|
+
siteId,
|
|
52
|
+
...editingId ? { articleId: editingId } : {},
|
|
51
53
|
title: form.title.trim(),
|
|
52
54
|
slug: slugify(form.title),
|
|
53
55
|
excerpt: form.excerpt.trim(),
|
|
54
56
|
body: form.body,
|
|
55
|
-
// Rich text
|
|
56
|
-
//
|
|
57
|
+
// Rich text: de editor haalt het al door DOMPurify, de callable saneert server-side, en de
|
|
58
|
+
// weergave sanitized nogmaals bij het renderen — drie lagen, elk met een eigen reden.
|
|
57
59
|
bodyHtml: form.bodyHtml,
|
|
58
60
|
coverUrl: form.coverUrl.trim(),
|
|
59
61
|
date: form.date || (/* @__PURE__ */ new Date()).toISOString().slice(0, 10),
|
|
60
|
-
published: form.published
|
|
61
|
-
|
|
62
|
-
};
|
|
63
|
-
if (editingId) {
|
|
64
|
-
await updateDoc(doc(db, "sites", siteId, "news", editingId), payload);
|
|
65
|
-
} else {
|
|
66
|
-
await addDoc(collection(db, "sites", siteId, "news"), { ...payload, createdAt: serverTimestamp() });
|
|
67
|
-
}
|
|
62
|
+
published: form.published
|
|
63
|
+
});
|
|
68
64
|
toast({ title: editingId ? "Artikel bijgewerkt" : "Artikel opgeslagen", tone: "success" });
|
|
69
65
|
reset();
|
|
70
66
|
} catch (e) {
|
|
@@ -74,8 +70,8 @@ function NewsEditor() {
|
|
|
74
70
|
}
|
|
75
71
|
}
|
|
76
72
|
async function remove(id, title) {
|
|
77
|
-
const
|
|
78
|
-
if (!
|
|
73
|
+
const app = getFirebaseApp();
|
|
74
|
+
if (!app) return;
|
|
79
75
|
const ok = await confirm({
|
|
80
76
|
title: "Artikel verwijderen?",
|
|
81
77
|
description: `\u201C${title}\u201D wordt definitief verwijderd.`,
|
|
@@ -83,7 +79,12 @@ function NewsEditor() {
|
|
|
83
79
|
tone: "danger"
|
|
84
80
|
});
|
|
85
81
|
if (!ok) return;
|
|
86
|
-
|
|
82
|
+
try {
|
|
83
|
+
await callable(app, "deleteNewsArticle")({ siteId, articleId: id });
|
|
84
|
+
} catch (e) {
|
|
85
|
+
toast({ title: "Verwijderen mislukt", description: e.message, tone: "error" });
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
87
88
|
if (editingId === id) reset();
|
|
88
89
|
toast({ title: "Artikel verwijderd", tone: "success" });
|
|
89
90
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/admin/NewsEditor.tsx"],"sourcesContent":["\"use client\";\n\nimport { useRef, useState } from \"react\";\nimport { addDoc, collection, deleteDoc, doc, serverTimestamp, updateDoc } from \"firebase/firestore\";\nimport { getDb } from \"../firebase/client\";\nimport { useSiteConfig } from \"../config\";\nimport { useNews } from \"../components/News\";\nimport { uploadImage } from \"./lib/upload\";\nimport { slugify } from \"./lib/slugify\";\nimport { useConfirm } from \"./ui/ConfirmDialog\";\nimport { useToast } from \"./ui/toast\";\nimport { Button, Card, Field, Input, Textarea, Badge } from \"./ui/primitives\";\nimport { RichTextEditor } from \"./editors/RichTextEditor\";\nimport type { NewsArticle } from \"../content/types\";\n\nconst empty = { title: \"\", excerpt: \"\", body: \"\", bodyHtml: \"\", coverUrl: \"\", date: \"\", published: false };\n\n/** Beheer van nieuwsartikelen — zelfde look & feel als de rest van /beheer. */\nexport function NewsEditor() {\n const { siteId } = useSiteConfig();\n const { articles } = useNews({ onlyPublished: false });\n const confirm = useConfirm();\n const { toast } = useToast();\n const [form, setForm] = useState<typeof empty>(empty);\n const [editingId, setEditingId] = useState<string | null>(null);\n const [busy, setBusy] = useState(false);\n const [coverBusy, setCoverBusy] = useState(false);\n const coverInputRef = useRef<HTMLInputElement>(null);\n\n function reset() {\n setForm(empty);\n setEditingId(null);\n }\n\n function loadIntoForm(a: NewsArticle) {\n setEditingId(a.id);\n setForm({\n title: a.title,\n excerpt: a.excerpt ?? \"\",\n body: a.body,\n bodyHtml: a.bodyHtml ?? \"\",\n coverUrl: a.coverUrl ?? \"\",\n date: a.date,\n published: a.published,\n });\n }\n\n async function save() {\n const db = getDb();\n if (!db) return;\n if (!form.title.trim()) {\n toast({ title: \"Titel ontbreekt\", description: \"Geef het artikel een titel.\", tone: \"error\" });\n return;\n }\n setBusy(true);\n try {\n const payload = {\n title: form.title.trim(),\n slug: slugify(form.title),\n excerpt: form.excerpt.trim(),\n body: form.body,\n // Rich text (client-side al door DOMPurify gehaald in de editor; de weergave sanitized\n // bovendien opnieuw bij het renderen — dubbele verdediging).\n bodyHtml: form.bodyHtml,\n coverUrl: form.coverUrl.trim(),\n date: form.date || new Date().toISOString().slice(0, 10),\n published: form.published,\n updatedAt: serverTimestamp(),\n };\n if (editingId) {\n await updateDoc(doc(db, \"sites\", siteId, \"news\", editingId), payload);\n } else {\n await addDoc(collection(db, \"sites\", siteId, \"news\"), { ...payload, createdAt: serverTimestamp() });\n }\n toast({ title: editingId ? \"Artikel bijgewerkt\" : \"Artikel opgeslagen\", tone: \"success\" });\n reset();\n } catch (e) {\n toast({ title: \"Opslaan mislukt\", description: (e as Error).message, tone: \"error\" });\n } finally {\n setBusy(false);\n }\n }\n\n async function remove(id: string, title: string) {\n const db = getDb();\n if (!db) return;\n const ok = await confirm({\n title: \"Artikel verwijderen?\",\n description: `“${title}” wordt definitief verwijderd.`,\n confirmLabel: \"Verwijderen\",\n tone: \"danger\",\n });\n if (!ok) return;\n await deleteDoc(doc(db, \"sites\", siteId, \"news\", id));\n if (editingId === id) reset();\n toast({ title: \"Artikel verwijderd\", tone: \"success\" });\n }\n\n async function onCover(file: File) {\n setCoverBusy(true);\n try {\n const { url } = await uploadImage(siteId, file);\n setForm((prev) => ({ ...prev, coverUrl: url }));\n toast({ title: \"Foto toegevoegd\", description: \"Automatisch verkleind voor het web.\", tone: \"success\" });\n } catch (e) {\n toast({ title: \"Upload mislukt\", description: (e as Error).message, tone: \"error\" });\n } finally {\n setCoverBusy(false);\n }\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\">{editingId ? \"Artikel bewerken\" : \"Nieuw artikel\"}</h3>\n {editingId && (\n <Button variant=\"secondary\" size=\"sm\" onClick={reset}>+ Nieuw artikel</Button>\n )}\n </div>\n <div className=\"grid gap-3\">\n <Field label=\"Titel\">\n <Input value={form.title} onChange={(e) => setForm({ ...form, title: e.target.value })} placeholder=\"Kop van het artikel\" />\n </Field>\n <Field label=\"Korte samenvatting\" hint=\"Verschijnt in de nieuwslijst.\">\n <Input value={form.excerpt} onChange={(e) => setForm({ ...form, excerpt: e.target.value })} />\n </Field>\n <Field label=\"Coverfoto\">\n <div className=\"flex items-center gap-2\">\n <Input value={form.coverUrl} onChange={(e) => setForm({ ...form, coverUrl: e.target.value })} placeholder=\"URL of upload →\" />\n <Button type=\"button\" variant=\"secondary\" size=\"sm\" disabled={coverBusy} onClick={() => coverInputRef.current?.click()}>\n {coverBusy ? \"Uploaden…\" : \"Upload\"}\n </Button>\n <input\n ref={coverInputRef}\n type=\"file\"\n accept=\"image/*\"\n hidden\n onChange={(e) => {\n const f = e.target.files?.[0];\n if (f) void onCover(f);\n e.target.value = \"\";\n }}\n />\n </div>\n </Field>\n {form.coverUrl && (\n // eslint-disable-next-line @next/next/no-img-element\n <img src={form.coverUrl} alt=\"\" className=\"h-32 w-full rounded-lg object-cover\" />\n )}\n <div className=\"grid gap-3 md:grid-cols-2\">\n <Field label=\"Datum\">\n <Input type=\"date\" value={form.date} onChange={(e) => setForm({ ...form, date: e.target.value })} />\n </Field>\n <Field label=\"Zichtbaarheid\" hint=\"Aangevinkt = direct zichtbaar op de site; uit = concept.\">\n <label className=\"flex h-9 items-center gap-2 text-sm\">\n <input type=\"checkbox\" checked={form.published} onChange={(e) => setForm({ ...form, published: e.target.checked })} />\n Gepubliceerd\n </label>\n </Field>\n </div>\n <Field label=\"Tekst\" hint=\"Met opmaak (koppen, lijsten, links, afbeeldingen). Bestaande artikelen zonder opmaak blijven gewoon werken.\">\n <RichTextEditor\n siteId={siteId}\n value={form.bodyHtml || (form.body ? `<p>${form.body.split(/\\n{2,}/).join(\"</p><p>\")}</p>` : \"\")}\n onChange={(html) => setForm((f) => ({ ...f, bodyHtml: html }))}\n />\n </Field>\n <div>\n <Button disabled={busy} onClick={() => void save()}>\n {busy ? \"Opslaan…\" : editingId ? \"Wijzigingen opslaan\" : \"Artikel opslaan\"}\n </Button>\n </div>\n </div>\n </Card>\n\n <Card>\n <h3 className=\"mb-3 text-base font-semibold\">Artikelen</h3>\n {articles.length === 0 ? (\n <p className=\"text-sm text-slate-500\">Nog geen artikelen. Maak hierboven je eerste nieuwsartikel.</p>\n ) : (\n <div className=\"grid gap-2\">\n {articles.map((a) => (\n <div key={a.id} className=\"flex items-center justify-between gap-3 rounded-lg border border-slate-200 p-3\">\n <span className=\"flex items-center gap-2 text-sm\">\n <span className=\"font-medium\">{a.title}</span>\n <span className=\"text-slate-400\">· {a.date}</span>\n {a.published ? <Badge tone=\"green\">zichtbaar</Badge> : <Badge tone=\"amber\">concept</Badge>}\n </span>\n <span className=\"flex gap-2\">\n <Button variant=\"ghost\" size=\"sm\" onClick={() => loadIntoForm(a)}>Bewerk</Button>\n <Button variant=\"ghost\" size=\"sm\" onClick={() => void remove(a.id, a.title)}>Verwijder</Button>\n </span>\n </div>\n ))}\n </div>\n )}\n </Card>\n </div>\n );\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../src/admin/NewsEditor.tsx"],"sourcesContent":["\"use client\";\n\nimport { useRef, useState } from \"react\";\nimport { getFirebaseApp } from \"../firebase/client\";\nimport { callable } from \"../firebase/functionsClient\";\nimport { useSiteConfig } from \"../config\";\nimport { useNews } from \"../components/News\";\nimport { uploadImage } from \"./lib/upload\";\nimport { slugify } from \"./lib/slugify\";\nimport { useConfirm } from \"./ui/ConfirmDialog\";\nimport { useToast } from \"./ui/toast\";\nimport { Button, Card, Field, Input, Textarea, Badge } from \"./ui/primitives\";\nimport { RichTextEditor } from \"./editors/RichTextEditor\";\nimport type { NewsArticle } from \"../content/types\";\n\nconst empty = { title: \"\", excerpt: \"\", body: \"\", bodyHtml: \"\", coverUrl: \"\", date: \"\", published: false };\n\n/**\n * Beheer van nieuwsartikelen — zelfde look & feel als de rest van /beheer.\n *\n * Schrijft via callables, niet rechtstreeks naar Firestore. Nieuws was het laatste publieke\n * contenttype dat de client zelf schreef; daardoor was er geen plek waar de cache-invalidatie kon\n * plaatsvinden en verscheen een gepubliceerd artikel niet op een live-reads-site (SITE-AUDIT §11).\n * De callable saneert, auditeert en verklaart de nieuws-tag ongeldig — net als bij symbolen.\n */\nexport function NewsEditor() {\n const { siteId } = useSiteConfig();\n const { articles } = useNews({ onlyPublished: false });\n const confirm = useConfirm();\n const { toast } = useToast();\n const [form, setForm] = useState<typeof empty>(empty);\n const [editingId, setEditingId] = useState<string | null>(null);\n const [busy, setBusy] = useState(false);\n const [coverBusy, setCoverBusy] = useState(false);\n const coverInputRef = useRef<HTMLInputElement>(null);\n\n function reset() {\n setForm(empty);\n setEditingId(null);\n }\n\n function loadIntoForm(a: NewsArticle) {\n setEditingId(a.id);\n setForm({\n title: a.title,\n excerpt: a.excerpt ?? \"\",\n body: a.body,\n bodyHtml: a.bodyHtml ?? \"\",\n coverUrl: a.coverUrl ?? \"\",\n date: a.date,\n published: a.published,\n });\n }\n\n async function save() {\n if (!form.title.trim()) {\n toast({ title: \"Titel ontbreekt\", description: \"Geef het artikel een titel.\", tone: \"error\" });\n return;\n }\n setBusy(true);\n try {\n const app = getFirebaseApp();\n if (!app) throw new Error(\"Firebase is niet geconfigureerd.\");\n await callable(app, \"saveNewsArticle\")({\n siteId,\n ...(editingId ? { articleId: editingId } : {}),\n title: form.title.trim(),\n slug: slugify(form.title),\n excerpt: form.excerpt.trim(),\n body: form.body,\n // Rich text: de editor haalt het al door DOMPurify, de callable saneert server-side, en de\n // weergave sanitized nogmaals bij het renderen — drie lagen, elk met een eigen reden.\n bodyHtml: form.bodyHtml,\n coverUrl: form.coverUrl.trim(),\n date: form.date || new Date().toISOString().slice(0, 10),\n published: form.published,\n });\n toast({ title: editingId ? \"Artikel bijgewerkt\" : \"Artikel opgeslagen\", tone: \"success\" });\n reset();\n } catch (e) {\n toast({ title: \"Opslaan mislukt\", description: (e as Error).message, tone: \"error\" });\n } finally {\n setBusy(false);\n }\n }\n\n async function remove(id: string, title: string) {\n const app = getFirebaseApp();\n if (!app) return;\n const ok = await confirm({\n title: \"Artikel verwijderen?\",\n description: `“${title}” wordt definitief verwijderd.`,\n confirmLabel: \"Verwijderen\",\n tone: \"danger\",\n });\n if (!ok) return;\n try {\n await callable(app, \"deleteNewsArticle\")({ siteId, articleId: id });\n } catch (e) {\n toast({ title: \"Verwijderen mislukt\", description: (e as Error).message, tone: \"error\" });\n return;\n }\n if (editingId === id) reset();\n toast({ title: \"Artikel verwijderd\", tone: \"success\" });\n }\n\n async function onCover(file: File) {\n setCoverBusy(true);\n try {\n const { url } = await uploadImage(siteId, file);\n setForm((prev) => ({ ...prev, coverUrl: url }));\n toast({ title: \"Foto toegevoegd\", description: \"Automatisch verkleind voor het web.\", tone: \"success\" });\n } catch (e) {\n toast({ title: \"Upload mislukt\", description: (e as Error).message, tone: \"error\" });\n } finally {\n setCoverBusy(false);\n }\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\">{editingId ? \"Artikel bewerken\" : \"Nieuw artikel\"}</h3>\n {editingId && (\n <Button variant=\"secondary\" size=\"sm\" onClick={reset}>+ Nieuw artikel</Button>\n )}\n </div>\n <div className=\"grid gap-3\">\n <Field label=\"Titel\">\n <Input value={form.title} onChange={(e) => setForm({ ...form, title: e.target.value })} placeholder=\"Kop van het artikel\" />\n </Field>\n <Field label=\"Korte samenvatting\" hint=\"Verschijnt in de nieuwslijst.\">\n <Input value={form.excerpt} onChange={(e) => setForm({ ...form, excerpt: e.target.value })} />\n </Field>\n <Field label=\"Coverfoto\">\n <div className=\"flex items-center gap-2\">\n <Input value={form.coverUrl} onChange={(e) => setForm({ ...form, coverUrl: e.target.value })} placeholder=\"URL of upload →\" />\n <Button type=\"button\" variant=\"secondary\" size=\"sm\" disabled={coverBusy} onClick={() => coverInputRef.current?.click()}>\n {coverBusy ? \"Uploaden…\" : \"Upload\"}\n </Button>\n <input\n ref={coverInputRef}\n type=\"file\"\n accept=\"image/*\"\n hidden\n onChange={(e) => {\n const f = e.target.files?.[0];\n if (f) void onCover(f);\n e.target.value = \"\";\n }}\n />\n </div>\n </Field>\n {form.coverUrl && (\n // eslint-disable-next-line @next/next/no-img-element\n <img src={form.coverUrl} alt=\"\" className=\"h-32 w-full rounded-lg object-cover\" />\n )}\n <div className=\"grid gap-3 md:grid-cols-2\">\n <Field label=\"Datum\">\n <Input type=\"date\" value={form.date} onChange={(e) => setForm({ ...form, date: e.target.value })} />\n </Field>\n <Field label=\"Zichtbaarheid\" hint=\"Aangevinkt = direct zichtbaar op de site; uit = concept.\">\n <label className=\"flex h-9 items-center gap-2 text-sm\">\n <input type=\"checkbox\" checked={form.published} onChange={(e) => setForm({ ...form, published: e.target.checked })} />\n Gepubliceerd\n </label>\n </Field>\n </div>\n <Field label=\"Tekst\" hint=\"Met opmaak (koppen, lijsten, links, afbeeldingen). Bestaande artikelen zonder opmaak blijven gewoon werken.\">\n <RichTextEditor\n siteId={siteId}\n value={form.bodyHtml || (form.body ? `<p>${form.body.split(/\\n{2,}/).join(\"</p><p>\")}</p>` : \"\")}\n onChange={(html) => setForm((f) => ({ ...f, bodyHtml: html }))}\n />\n </Field>\n <div>\n <Button disabled={busy} onClick={() => void save()}>\n {busy ? \"Opslaan…\" : editingId ? \"Wijzigingen opslaan\" : \"Artikel opslaan\"}\n </Button>\n </div>\n </div>\n </Card>\n\n <Card>\n <h3 className=\"mb-3 text-base font-semibold\">Artikelen</h3>\n {articles.length === 0 ? (\n <p className=\"text-sm text-slate-500\">Nog geen artikelen. Maak hierboven je eerste nieuwsartikel.</p>\n ) : (\n <div className=\"grid gap-2\">\n {articles.map((a) => (\n <div key={a.id} className=\"flex items-center justify-between gap-3 rounded-lg border border-slate-200 p-3\">\n <span className=\"flex items-center gap-2 text-sm\">\n <span className=\"font-medium\">{a.title}</span>\n <span className=\"text-slate-400\">· {a.date}</span>\n {a.published ? <Badge tone=\"green\">zichtbaar</Badge> : <Badge tone=\"amber\">concept</Badge>}\n </span>\n <span className=\"flex gap-2\">\n <Button variant=\"ghost\" size=\"sm\" onClick={() => loadIntoForm(a)}>Bewerk</Button>\n <Button variant=\"ghost\" size=\"sm\" onClick={() => void remove(a.id, a.title)}>Verwijder</Button>\n </span>\n </div>\n ))}\n </div>\n )}\n </Card>\n </div>\n );\n}\n"],"mappings":"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","names":[]}
|
|
@@ -2,16 +2,15 @@
|
|
|
2
2
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
3
3
|
import { useEffect, useState } from "react";
|
|
4
4
|
import { collection, getDocs, limit, orderBy, query, where } from "firebase/firestore";
|
|
5
|
-
import {
|
|
5
|
+
import { callable } from "../firebase/functionsClient.js";
|
|
6
6
|
import { getDb, getFirebaseApp } from "../firebase/client.js";
|
|
7
|
-
import { functionsRegion } from "../firebase/region.js";
|
|
8
7
|
import { Button, Card, Field, Input } from "./ui/primitives.js";
|
|
9
8
|
import { Select } from "./ui/select.js";
|
|
10
9
|
import { useToast } from "./ui/toast.js";
|
|
11
|
-
function
|
|
10
|
+
function superadminCall(name) {
|
|
12
11
|
const app = getFirebaseApp();
|
|
13
12
|
if (!app) throw new Error("Firebase niet geconfigureerd.");
|
|
14
|
-
return
|
|
13
|
+
return callable(app, name);
|
|
15
14
|
}
|
|
16
15
|
function SuperadminPanel({ siteId }) {
|
|
17
16
|
const { toast } = useToast();
|
|
@@ -49,7 +48,7 @@ function SuperadminPanel({ siteId }) {
|
|
|
49
48
|
/* @__PURE__ */ jsx(Field, { label: "siteId", children: /* @__PURE__ */ jsx(Input, { value: site.siteId, onChange: (e) => setSite({ ...site, siteId: e.target.value }), placeholder: "bv. bakkerij-jansen" }) }),
|
|
50
49
|
/* @__PURE__ */ jsx(Field, { label: "Naam", children: /* @__PURE__ */ jsx(Input, { value: site.name, onChange: (e) => setSite({ ...site, name: e.target.value }) }) }),
|
|
51
50
|
/* @__PURE__ */ jsx(Field, { label: "Domein", children: /* @__PURE__ */ jsx(Input, { value: site.domain, onChange: (e) => setSite({ ...site, domain: e.target.value }) }) }),
|
|
52
|
-
/* @__PURE__ */ jsx(Button, { disabled: busy || !site.siteId, onClick: () => run(() =>
|
|
51
|
+
/* @__PURE__ */ jsx(Button, { disabled: busy || !site.siteId, onClick: () => run(() => superadminCall("createSite")(site), "Site aangemaakt"), children: "Site aanmaken" })
|
|
53
52
|
] })
|
|
54
53
|
] }),
|
|
55
54
|
/* @__PURE__ */ jsxs(Card, { children: [
|
|
@@ -69,14 +68,14 @@ function SuperadminPanel({ siteId }) {
|
|
|
69
68
|
]
|
|
70
69
|
}
|
|
71
70
|
) }),
|
|
72
|
-
/* @__PURE__ */ jsx(Button, { disabled: busy || !account.siteId || !account.email, onClick: () => run(() =>
|
|
71
|
+
/* @__PURE__ */ jsx(Button, { disabled: busy || !account.siteId || !account.email, onClick: () => run(() => superadminCall("createSiteAccount")(account), "Account aangemaakt"), children: "Account aanmaken" })
|
|
73
72
|
] })
|
|
74
73
|
] }),
|
|
75
74
|
/* @__PURE__ */ jsxs(Card, { children: [
|
|
76
75
|
/* @__PURE__ */ jsx("h3", { className: "mb-3 text-base font-semibold", children: "Wachtwoord-reset versturen" }),
|
|
77
76
|
/* @__PURE__ */ jsxs("div", { className: "grid gap-3", children: [
|
|
78
77
|
/* @__PURE__ */ jsx(Field, { label: "E-mail", children: /* @__PURE__ */ jsx(Input, { type: "email", value: resetEmail, onChange: (e) => setResetEmail(e.target.value) }) }),
|
|
79
|
-
/* @__PURE__ */ jsx(Button, { variant: "secondary", disabled: busy || !resetEmail, onClick: () => run(() =>
|
|
78
|
+
/* @__PURE__ */ jsx(Button, { variant: "secondary", disabled: busy || !resetEmail, onClick: () => run(() => superadminCall("requestPasswordReset")({ email: resetEmail }), "Reset-link verstuurd"), children: "Reset-link versturen" })
|
|
80
79
|
] })
|
|
81
80
|
] }),
|
|
82
81
|
/* @__PURE__ */ jsxs(Card, { children: [
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/admin/SuperadminPanel.tsx"],"sourcesContent":["\"use client\";\n\nimport { useEffect, useState } from \"react\";\nimport { collection, getDocs, limit, orderBy, query, where } from \"firebase/firestore\";\nimport {
|
|
1
|
+
{"version":3,"sources":["../../src/admin/SuperadminPanel.tsx"],"sourcesContent":["\"use client\";\n\nimport { useEffect, useState } from \"react\";\nimport { collection, getDocs, limit, orderBy, query, where } from \"firebase/firestore\";\nimport { callable } from \"../firebase/functionsClient\";\nimport { getDb, getFirebaseApp } from \"../firebase/client\";\nimport { Button, Card, Field, Input } from \"./ui/primitives\";\nimport { Select } from \"./ui/select\";\nimport { useToast } from \"./ui/toast\";\n\n/** Callable van dit paneel: zoekt de app op en geeft de standaard `{ message }`-respons terug. */\nfunction superadminCall<T>(name: string) {\n const app = getFirebaseApp();\n if (!app) throw new Error(\"Firebase niet geconfigureerd.\");\n return callable<T, { message?: string }>(app, name);\n}\n\n/** Superadmin: sites/accounts aanmaken, wachtwoord-reset versturen en audit-log inzien. */\nexport function SuperadminPanel({ siteId }: { siteId: string }) {\n const { toast } = useToast();\n const [site, setSite] = useState({ siteId: \"\", name: \"\", domain: \"\" });\n const [account, setAccount] = useState({ siteId: \"\", email: \"\", password: \"\", role: \"siteadmin\" });\n const [resetEmail, setResetEmail] = useState(\"\");\n const [busy, setBusy] = useState(false);\n const [audit, setAudit] = useState<{ id: string; action?: string; target?: string; actorUid?: string }[]>([]);\n\n useEffect(() => {\n const db = getDb();\n if (!db) return;\n const q = query(\n collection(db, \"audit_log\"),\n where(\"siteId\", \"in\", [siteId, \"*\"]),\n orderBy(\"at\", \"desc\"),\n limit(20),\n );\n getDocs(q)\n .then((snap) => setAudit(snap.docs.map((d) => ({ id: d.id, ...(d.data() as Record<string, string>) }))))\n .catch(() => setAudit([]));\n }, [siteId]);\n\n async function run(fn: () => Promise<{ data: { message?: string } }>, ok: string) {\n setBusy(true);\n try {\n const res = await fn();\n toast({ title: res.data.message ?? ok, tone: \"success\" });\n } catch (e) {\n toast({ title: \"Mislukt\", description: (e as Error).message, tone: \"error\" });\n } finally {\n setBusy(false);\n }\n }\n\n return (\n <div className=\"grid gap-4 lg:grid-cols-2\">\n <Card>\n <h3 className=\"mb-3 text-base font-semibold\">Nieuwe site</h3>\n <div className=\"grid gap-3\">\n <Field label=\"siteId\"><Input value={site.siteId} onChange={(e) => setSite({ ...site, siteId: e.target.value })} placeholder=\"bv. bakkerij-jansen\" /></Field>\n <Field label=\"Naam\"><Input value={site.name} onChange={(e) => setSite({ ...site, name: e.target.value })} /></Field>\n <Field label=\"Domein\"><Input value={site.domain} onChange={(e) => setSite({ ...site, domain: e.target.value })} /></Field>\n <Button disabled={busy || !site.siteId} onClick={() => run(() => superadminCall<typeof site>(\"createSite\")(site), \"Site aangemaakt\")}>\n Site aanmaken\n </Button>\n </div>\n </Card>\n\n <Card>\n <h3 className=\"mb-3 text-base font-semibold\">Beheeraccount</h3>\n <div className=\"grid gap-3\">\n <Field label=\"siteId\"><Input value={account.siteId} onChange={(e) => setAccount({ ...account, siteId: e.target.value })} /></Field>\n <Field label=\"E-mail\"><Input type=\"email\" value={account.email} onChange={(e) => setAccount({ ...account, email: e.target.value })} /></Field>\n <Field label=\"Tijdelijk wachtwoord\"><Input value={account.password} onChange={(e) => setAccount({ ...account, password: e.target.value })} /></Field>\n <Field label=\"Rol\">\n <Select\n value={account.role}\n onValueChange={(v) => setAccount({ ...account, role: v })}\n options={[\n { value: \"siteadmin\", label: \"siteadmin (volledig)\" },\n { value: \"editor\", label: \"editor (alleen content)\" },\n ]}\n />\n </Field>\n <Button disabled={busy || !account.siteId || !account.email} onClick={() => run(() => superadminCall<typeof account>(\"createSiteAccount\")(account), \"Account aangemaakt\")}>\n Account aanmaken\n </Button>\n </div>\n </Card>\n\n <Card>\n <h3 className=\"mb-3 text-base font-semibold\">Wachtwoord-reset versturen</h3>\n <div className=\"grid gap-3\">\n <Field label=\"E-mail\"><Input type=\"email\" value={resetEmail} onChange={(e) => setResetEmail(e.target.value)} /></Field>\n <Button variant=\"secondary\" disabled={busy || !resetEmail} onClick={() => run(() => superadminCall<{ email: string }>(\"requestPasswordReset\")({ email: resetEmail }), \"Reset-link verstuurd\")}>\n Reset-link versturen\n </Button>\n </div>\n </Card>\n\n <Card>\n <h3 className=\"mb-3 text-base font-semibold\">Audit-log (laatste 20)</h3>\n {audit.length === 0 ? (\n <p className=\"text-sm text-slate-500\">Geen recente acties (of geen rechten/index).</p>\n ) : (\n <ul className=\"text-sm text-slate-600\">\n {audit.map((a) => (\n <li key={a.id} className=\"border-b border-slate-100 py-1 last:border-0\">\n <span className=\"font-medium\">{a.action}</span> · {a.target} <span className=\"text-slate-400\">({a.actorUid?.slice(0, 6)})</span>\n </li>\n ))}\n </ul>\n )}\n </Card>\n </div>\n );\n}\n"],"mappings":"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","names":[]}
|
|
@@ -2,9 +2,8 @@
|
|
|
2
2
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
3
3
|
import { useEffect, useState } from "react";
|
|
4
4
|
import { collection, onSnapshot } from "firebase/firestore";
|
|
5
|
-
import {
|
|
5
|
+
import { callable } from "../../firebase/functionsClient.js";
|
|
6
6
|
import { getDb, getFirebaseApp } from "../../firebase/client.js";
|
|
7
|
-
import { functionsRegion } from "../../firebase/region.js";
|
|
8
7
|
import { CustomBlockSchema } from "../../schema/customBlocks.js";
|
|
9
8
|
import { FieldsSchema } from "../../schema/fields.js";
|
|
10
9
|
import { Button, Field, Input, Textarea } from "../ui/primitives.js";
|
|
@@ -82,7 +81,7 @@ function CustomBlocksPanel({ siteId }) {
|
|
|
82
81
|
if (!app) return;
|
|
83
82
|
setBusy(true);
|
|
84
83
|
try {
|
|
85
|
-
const fn =
|
|
84
|
+
const fn = callable(app, "saveCustomBlock");
|
|
86
85
|
const res = await fn({ siteId, ...candidate.data });
|
|
87
86
|
toast({ title: "Opgeslagen", description: res.data.message, tone: "success" });
|
|
88
87
|
setEditing(null);
|
|
@@ -98,7 +97,7 @@ function CustomBlocksPanel({ siteId }) {
|
|
|
98
97
|
const app = getFirebaseApp();
|
|
99
98
|
if (!app) return;
|
|
100
99
|
try {
|
|
101
|
-
await
|
|
100
|
+
await callable(app, "deleteCustomBlock")({ siteId, type });
|
|
102
101
|
toast({ title: "Verwijderd", tone: "success" });
|
|
103
102
|
} catch (e) {
|
|
104
103
|
toast({ title: "Verwijderen mislukt", description: e.message, tone: "error" });
|