@xiwebdesign/xicms-core 7.4.0 → 7.6.1

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.
Files changed (52) hide show
  1. package/dist/admin/AdminApp.d.ts +12 -1
  2. package/dist/admin/AdminApp.js +18 -14
  3. package/dist/admin/AdminApp.js.map +1 -1
  4. package/dist/admin/NewsEditor.js +15 -4
  5. package/dist/admin/NewsEditor.js.map +1 -1
  6. package/dist/admin/menu/MenuPanel.d.ts +20 -0
  7. package/dist/admin/menu/MenuPanel.js +144 -0
  8. package/dist/admin/menu/MenuPanel.js.map +1 -0
  9. package/dist/admin/panelRegistry.d.ts +10 -0
  10. package/dist/admin/panelRegistry.js +5 -1
  11. package/dist/admin/panelRegistry.js.map +1 -1
  12. package/dist/admin/settings/SettingsPanel.js +10 -2
  13. package/dist/admin/settings/SettingsPanel.js.map +1 -1
  14. package/dist/admin/ui/dialog.js +6 -3
  15. package/dist/admin/ui/dialog.js.map +1 -1
  16. package/dist/admin/ui/toast.d.ts +16 -0
  17. package/dist/admin/ui/toast.js +8 -4
  18. package/dist/admin/ui/toast.js.map +1 -1
  19. package/dist/admin/visual/VisualEditor.js +89 -123
  20. package/dist/admin/visual/VisualEditor.js.map +1 -1
  21. package/dist/admin.css +1 -1
  22. package/dist/auth/AuthProvider.js +11 -5
  23. package/dist/auth/AuthProvider.js.map +1 -1
  24. package/dist/auth/LoginForm.js +2 -1
  25. package/dist/auth/LoginForm.js.map +1 -1
  26. package/dist/components/ExternEmbed.d.ts +23 -0
  27. package/dist/components/ExternEmbed.js +52 -0
  28. package/dist/components/ExternEmbed.js.map +1 -0
  29. package/dist/editor/EditorProvider.d.ts +8 -0
  30. package/dist/editor/EditorProvider.js +30 -6
  31. package/dist/editor/EditorProvider.js.map +1 -1
  32. package/dist/index.d.ts +1 -0
  33. package/dist/index.js +2 -0
  34. package/dist/index.js.map +1 -1
  35. package/dist/next/authRewrites.d.ts +29 -0
  36. package/dist/next/authRewrites.js +11 -0
  37. package/dist/next/authRewrites.js.map +1 -1
  38. package/dist/next/index.cjs +12 -0
  39. package/dist/next/index.d.ts +1 -1
  40. package/dist/next/index.js +2 -1
  41. package/dist/next/index.js.map +1 -1
  42. package/dist/render/PublishedPage.js +9 -1
  43. package/dist/render/PublishedPage.js.map +1 -1
  44. package/dist/schema/settings.d.ts +62 -1
  45. package/dist/schema/settings.js +23 -1
  46. package/dist/schema/settings.js.map +1 -1
  47. package/dist/schema/siteConfig.d.ts +5 -0
  48. package/dist/settings/useSiteSettings.js +1 -1
  49. package/dist/settings/useSiteSettings.js.map +1 -1
  50. package/dist/styles.css +67 -12
  51. package/dist/version.js +1 -1
  52. package/package.json +1 -1
@@ -1,4 +1,15 @@
1
1
  import { type PanelDefinition } from "./panelRegistry.js";
2
- export declare function AdminApp({ extraPanels }: {
2
+ export declare function AdminApp({ extraPanels, embedded, }: {
3
3
  extraPanels?: PanelDefinition[];
4
+ /**
5
+ * Draait het beheer BINNEN de site-schil (header, logo, menu, footer eromheen) in plaats van als
6
+ * schermvullend werkblad op een kale pagina?
7
+ *
8
+ * Het verschil is niet cosmetisch. Los rekent het werkblad met het VENSTER: `min-h-screen`, en een
9
+ * zijbalk die met `sticky top-0` aan de bovenkant van het scherm plakt. Zet je daar een
10
+ * site-header boven, dan loopt de pagina 100vh + de hoogte van die header, en plakt de zijbalk
11
+ * onder de header vast in plaats van erlangs. Ingebed laat de zijbalk daarom los van het venster
12
+ * en wordt hij een balk over de volle breedte — dat vecht niet met de navigatie erboven.
13
+ */
14
+ embedded?: boolean;
4
15
  }): import("react").JSX.Element;
@@ -19,21 +19,25 @@ function roleAllows(role, min) {
19
19
  return min === "siteadmin" ? canManageSite(role) : canEdit(role);
20
20
  }
21
21
  function Melding({ children }) {
22
- return /* @__PURE__ */ jsx("div", { className: "grid min-h-screen place-items-center p-6", children: /* @__PURE__ */ jsx("div", { className: "w-full max-w-md rounded-xl border border-slate-200 bg-white p-6 text-sm shadow-sm", children }) });
22
+ return /* @__PURE__ */ jsx("div", { className: "xicms-admin__midden grid place-items-center p-6", children: /* @__PURE__ */ jsx("div", { className: "w-full max-w-md rounded-xl border border-slate-200 bg-white p-6 text-sm shadow-sm", children }) });
23
23
  }
24
- function AdminApp({ extraPanels = [] }) {
24
+ function AdminApp({
25
+ extraPanels = [],
26
+ embedded = false
27
+ }) {
25
28
  const { user, role, loading, configured, signOut } = useAuth();
26
29
  const { name, siteId } = useSiteConfig();
27
30
  const { activeIds } = useModules();
28
31
  const panels = [...builtinPanels, ...extraPanels].filter((p) => activeIds.has(p.moduleId) && roleAllows(role, p.minRole)).sort((a, b) => a.order - b.order);
29
- const firstTab = panels[0]?.moduleId;
32
+ const sleutel = (p) => p.id ?? p.moduleId;
33
+ const firstTab = panels[0] && sleutel(panels[0]);
30
34
  const [actief, setActief] = useState(firstTab);
31
- const actievePanel = panels.find((p) => p.moduleId === (actief ?? firstTab));
32
- return /* @__PURE__ */ jsx("div", { className: "xicms-admin", children: /* @__PURE__ */ jsx(ToastProvider, { children: /* @__PURE__ */ jsx(ConfirmHost, { children: !configured ? /* @__PURE__ */ jsxs(Melding, { children: [
35
+ const actievePanel = panels.find((p) => sleutel(p) === (actief ?? firstTab));
36
+ return /* @__PURE__ */ jsx("div", { className: embedded ? "xicms-admin xicms-admin--ingebed" : "xicms-admin", children: /* @__PURE__ */ jsx(ToastProvider, { children: /* @__PURE__ */ jsx(ConfirmHost, { children: !configured ? /* @__PURE__ */ jsxs(Melding, { children: [
33
37
  "Firebase is nog niet geconfigureerd. Vul de ",
34
38
  /* @__PURE__ */ jsx("code", { children: "NEXT_PUBLIC_FIREBASE_*" }),
35
39
  "-variabelen in om het beheer aan te zetten."
36
- ] }) : loading ? /* @__PURE__ */ jsx(Melding, { children: /* @__PURE__ */ jsx("span", { className: "text-slate-500", children: "Bezig met laden\u2026" }) }) : !user ? /* @__PURE__ */ jsx("div", { className: "grid min-h-screen place-items-center p-6", children: /* @__PURE__ */ jsxs("div", { className: "w-full max-w-sm", children: [
40
+ ] }) : loading ? /* @__PURE__ */ jsx(Melding, { children: /* @__PURE__ */ jsx("span", { className: "text-slate-500", children: "Bezig met laden\u2026" }) }) : !user ? /* @__PURE__ */ jsx("div", { className: "xicms-admin__midden grid place-items-center p-6", children: /* @__PURE__ */ jsxs("div", { className: "w-full max-w-sm", children: [
37
41
  /* @__PURE__ */ jsxs("p", { className: "mb-4 text-center text-sm text-slate-500", children: [
38
42
  "Beheer \xB7 ",
39
43
  name
@@ -44,30 +48,30 @@ function AdminApp({ extraPanels = [] }) {
44
48
  /* @__PURE__ */ jsx("strong", { children: user.email }),
45
49
  ", maar hebt geen beheerrechten voor deze site.",
46
50
  /* @__PURE__ */ jsx("div", { className: "mt-3", children: /* @__PURE__ */ jsx(Button, { variant: "secondary", size: "sm", onClick: () => void signOut(), children: "Uitloggen" }) })
47
- ] }) : panels.length === 0 ? /* @__PURE__ */ jsx(Melding, { children: "Geen actieve beheer-modules voor deze site." }) : /* @__PURE__ */ jsxs(Tabs, { value: actief ?? firstTab, onValueChange: setActief, orientation: "vertical", className: "min-h-screen md:grid md:grid-cols-[248px_1fr]", children: [
51
+ ] }) : panels.length === 0 ? /* @__PURE__ */ jsx(Melding, { children: "Geen actieve beheer-modules voor deze site." }) : /* @__PURE__ */ jsxs(Tabs, { value: actief ?? firstTab, onValueChange: setActief, orientation: "vertical", className: "xicms-admin__raster md:grid md:grid-cols-[248px_1fr]", children: [
48
52
  /* @__PURE__ */ jsxs(
49
53
  "div",
50
54
  {
51
- 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",
55
+ className: "xicms-admin__rail 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",
52
56
  style: { background: "var(--admin-sidebar)" },
53
57
  children: [
54
58
  /* @__PURE__ */ jsxs("div", { className: "min-w-0 px-2 pt-1", children: [
55
59
  /* @__PURE__ */ jsx("p", { className: "truncate text-sm font-semibold", children: name }),
56
60
  /* @__PURE__ */ jsx("p", { className: "text-xs text-slate-500", children: "Beheer" })
57
61
  ] }),
58
- /* @__PURE__ */ jsx(TabsPrimitive.List, { "aria-label": "Beheer-secties", className: "-mx-1 flex gap-1 overflow-x-auto md:flex-col md:overflow-visible", children: panels.map((p) => /* @__PURE__ */ jsxs(
62
+ /* @__PURE__ */ jsx(TabsPrimitive.List, { "aria-label": "Beheer-secties", className: "xicms-admin__lijst -mx-1 flex gap-1 overflow-x-auto md:flex-col md:overflow-visible", children: panels.map((p) => /* @__PURE__ */ jsxs(
59
63
  TabsPrimitive.Trigger,
60
64
  {
61
- value: p.moduleId,
65
+ value: sleutel(p),
62
66
  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)]",
63
67
  children: [
64
68
  /* @__PURE__ */ jsx(PanelIcon, { id: p.moduleId }),
65
69
  p.label
66
70
  ]
67
71
  },
68
- p.moduleId
72
+ sleutel(p)
69
73
  )) }),
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: [
74
+ /* @__PURE__ */ jsxs("div", { className: "xicms-admin__voet mt-auto hidden min-w-0 flex-col gap-3 border-t border-slate-200 pt-3 md:flex", children: [
71
75
  /* @__PURE__ */ jsxs("div", { className: "min-w-0", children: [
72
76
  /* @__PURE__ */ jsx("p", { className: "truncate text-xs font-medium text-slate-600", title: user.email ?? void 0, children: user.email }),
73
77
  /* @__PURE__ */ jsx("p", { className: "text-xs text-slate-400", children: role })
@@ -79,7 +83,7 @@ function AdminApp({ extraPanels = [] }) {
79
83
  }
80
84
  ),
81
85
  /* @__PURE__ */ jsxs("div", { className: "min-w-0", children: [
82
- /* @__PURE__ */ jsxs("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)" }, children: [
86
+ /* @__PURE__ */ jsxs("header", { className: "xicms-admin__kop 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)" }, children: [
83
87
  /* @__PURE__ */ jsx("h1", { className: "truncate text-base font-semibold", children: actievePanel?.label ?? "Beheer" }),
84
88
  /* @__PURE__ */ jsx(
85
89
  "a",
@@ -91,7 +95,7 @@ function AdminApp({ extraPanels = [] }) {
91
95
  )
92
96
  ] }),
93
97
  /* @__PURE__ */ jsxs("div", { className: "grid gap-6 p-4 md:p-8", children: [
94
- panels.map((p) => /* @__PURE__ */ jsx(TabsContent, { value: p.moduleId, className: "pt-0", children: /* @__PURE__ */ jsx(p.Panel, { siteId }) }, p.moduleId)),
98
+ panels.map((p) => /* @__PURE__ */ jsx(TabsContent, { value: sleutel(p), className: "pt-0", children: /* @__PURE__ */ jsx(p.Panel, { siteId }) }, sleutel(p))),
95
99
  /* @__PURE__ */ jsx(AdminHelp, {})
96
100
  ] })
97
101
  ] })
@@ -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 {/* 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
+ {"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=\"xicms-admin__midden grid 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({\n extraPanels = [],\n embedded = false,\n}: {\n extraPanels?: PanelDefinition[];\n /**\n * Draait het beheer BINNEN de site-schil (header, logo, menu, footer eromheen) in plaats van als\n * schermvullend werkblad op een kale pagina?\n *\n * Het verschil is niet cosmetisch. Los rekent het werkblad met het VENSTER: `min-h-screen`, en een\n * zijbalk die met `sticky top-0` aan de bovenkant van het scherm plakt. Zet je daar een\n * site-header boven, dan loopt de pagina 100vh + de hoogte van die header, en plakt de zijbalk\n * onder de header vast in plaats van erlangs. Ingebed laat de zijbalk daarom los van het venster\n * en wordt hij een balk over de volle breedte — dat vecht niet met de navigatie erboven.\n */\n embedded?: boolean;\n}) {\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 /** Sleutel van een paneel in de tabbladen: eigen `id` als die er is, anders de module-id. */\n const sleutel = (p: PanelDefinition) => p.id ?? p.moduleId;\n const firstTab = panels[0] && sleutel(panels[0]);\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) => sleutel(p) === (actief ?? firstTab));\n\n return (\n <div className={embedded ? \"xicms-admin xicms-admin--ingebed\" : \"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=\"xicms-admin__midden grid 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=\"xicms-admin__raster md:grid md:grid-cols-[248px_1fr]\">\n {/* ── Zijbalk ─────────────────────────────────────────────────────────── */}\n <div\n className=\"xicms-admin__rail 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=\"xicms-admin__lijst -mx-1 flex gap-1 overflow-x-auto md:flex-col md:overflow-visible\">\n {panels.map((p) => (\n <TabsPrimitive.Trigger\n key={sleutel(p)}\n value={sleutel(p)}\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=\"xicms-admin__voet 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=\"xicms-admin__kop 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={sleutel(p)} value={sleutel(p)} 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":[]}
@@ -20,14 +20,22 @@ function NewsEditor() {
20
20
  const [form, setForm] = useState(empty);
21
21
  const [editingId, setEditingId] = useState(null);
22
22
  const [busy, setBusy] = useState(false);
23
+ const [formulierOpen, setFormulierOpen] = useState(false);
23
24
  const [coverBusy, setCoverBusy] = useState(false);
24
25
  const coverInputRef = useRef(null);
25
26
  function reset() {
26
27
  setForm(empty);
27
28
  setEditingId(null);
29
+ setFormulierOpen(false);
30
+ }
31
+ function nieuwArtikel() {
32
+ setForm(empty);
33
+ setEditingId(null);
34
+ setFormulierOpen(true);
28
35
  }
29
36
  function loadIntoForm(a) {
30
37
  setEditingId(a.id);
38
+ setFormulierOpen(true);
31
39
  setForm({
32
40
  title: a.title,
33
41
  excerpt: a.excerpt ?? "",
@@ -101,10 +109,10 @@ function NewsEditor() {
101
109
  }
102
110
  }
103
111
  return /* @__PURE__ */ jsxs("div", { className: "grid gap-4", children: [
104
- /* @__PURE__ */ jsxs(Card, { children: [
112
+ formulierOpen && /* @__PURE__ */ jsxs(Card, { children: [
105
113
  /* @__PURE__ */ jsxs("div", { className: "mb-3 flex items-center justify-between", children: [
106
114
  /* @__PURE__ */ jsx("h3", { className: "text-base font-semibold", children: editingId ? "Artikel bewerken" : "Nieuw artikel" }),
107
- editingId && /* @__PURE__ */ jsx(Button, { variant: "secondary", size: "sm", onClick: reset, children: "+ Nieuw artikel" })
115
+ /* @__PURE__ */ jsx(Button, { variant: "secondary", size: "sm", onClick: reset, children: "Sluiten" })
108
116
  ] }),
109
117
  /* @__PURE__ */ jsxs("div", { className: "grid gap-3", children: [
110
118
  /* @__PURE__ */ jsx(Field, { label: "Titel", children: /* @__PURE__ */ jsx(Input, { value: form.title, onChange: (e) => setForm({ ...form, title: e.target.value }), placeholder: "Kop van het artikel" }) }),
@@ -148,8 +156,11 @@ function NewsEditor() {
148
156
  ] })
149
157
  ] }),
150
158
  /* @__PURE__ */ jsxs(Card, { children: [
151
- /* @__PURE__ */ jsx("h3", { className: "mb-3 text-base font-semibold", children: "Artikelen" }),
152
- articles.length === 0 ? /* @__PURE__ */ jsx("p", { className: "text-sm text-slate-500", children: "Nog geen artikelen. Maak hierboven je eerste nieuwsartikel." }) : /* @__PURE__ */ jsx("div", { className: "grid gap-2", children: articles.map((a) => /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between gap-3 rounded-lg border border-slate-200 p-3", children: [
159
+ /* @__PURE__ */ jsxs("div", { className: "mb-3 flex items-center justify-between", children: [
160
+ /* @__PURE__ */ jsx("h3", { className: "text-base font-semibold", children: "Artikelen" }),
161
+ /* @__PURE__ */ jsx(Button, { size: "sm", onClick: nieuwArtikel, children: "+ Nieuw artikel" })
162
+ ] }),
163
+ articles.length === 0 ? /* @__PURE__ */ jsx("p", { className: "text-sm text-slate-500", children: "Nog geen artikelen. Klik op \u201C+ Nieuw artikel\u201D om te beginnen." }) : /* @__PURE__ */ jsx("div", { className: "grid gap-2", children: articles.map((a) => /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between gap-3 rounded-lg border border-slate-200 p-3", children: [
153
164
  /* @__PURE__ */ jsxs("span", { className: "flex items-center gap-2 text-sm", children: [
154
165
  /* @__PURE__ */ jsx("span", { className: "font-medium", children: a.title }),
155
166
  /* @__PURE__ */ jsxs("span", { className: "text-slate-400", children: [
@@ -1 +1 @@
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":[]}
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 /**\n * Staat het formulier open?\n *\n * Stond altijd open, en dat zette de lijst met bestaande artikelen onder een schermvullend\n * \"Nieuw artikel\"-blok. Wie alleen een typefout wilde herstellen moest eerst langs een leeg\n * formulier scrollen dat hij niet had gevraagd. Nu is de LIJST het uitgangspunt — dat is wat je\n * meestal komt doen — en opent het formulier pas als je er zelf om vraagt: via \"Nieuw artikel\",\n * of door op Bewerk te klikken bij een bestaand artikel.\n */\n const [formulierOpen, setFormulierOpen] = 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 setFormulierOpen(false);\n }\n\n /** Leeg formulier openen voor een nieuw artikel. */\n function nieuwArtikel() {\n setForm(empty);\n setEditingId(null);\n setFormulierOpen(true);\n }\n\n function loadIntoForm(a: NewsArticle) {\n setEditingId(a.id);\n setFormulierOpen(true);\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 {formulierOpen && (\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 <Button variant=\"secondary\" size=\"sm\" onClick={reset}>Sluiten</Button>\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\n <Card>\n <div className=\"mb-3 flex items-center justify-between\">\n <h3 className=\"text-base font-semibold\">Artikelen</h3>\n {/* De enige plek waar een leeg formulier vandaan komt. Dat is het verschil met eerder:\n je krijgt het als je erom vraagt, niet als openingsbeeld. */}\n <Button size=\"sm\" onClick={nieuwArtikel}>+ Nieuw artikel</Button>\n </div>\n {articles.length === 0 ? (\n <p className=\"text-sm text-slate-500\">Nog geen artikelen. Klik op “+ Nieuw artikel” om te beginnen.</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":[]}
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Beheer van de hoofdnavigatie.
3
+ *
4
+ * WAAR HET MENU WOONT. Tot nu toe stond de navigatie in `content/site.config.ts` — in Git, dus
5
+ * alleen te wijzigen door wie de repo kan deployen. Dat is precies het soort werk dat een klant
6
+ * zelf zou moeten kunnen doen: een pagina bijzetten, een volgorde omgooien.
7
+ *
8
+ * Het menu verhuist daarom naar Firestore, met de config als TERUGVAL. Staat er niets in de
9
+ * instellingen, dan blijft de nav uit de site-config werken. Dat is geen halfslachtigheid maar de
10
+ * enige veilige omzetting: elke bestaande site houdt zijn menu, en je migreert per site op het
11
+ * moment dat iemand hier voor het eerst opslaat.
12
+ *
13
+ * PAGINA OF EXTERN. In de opslag staat één ondubbelzinnig adres — geen apart `type`-veld dat uit
14
+ * de pas kan lopen met de href. Of iets extern is, lees je aan de href af. Dit scherm biedt de
15
+ * keuze wél aan, want "kies een pagina" is een andere handeling dan "plak een adres", en een
16
+ * keuzelijst met bestaande pagina's voorkomt typefouten in interne links.
17
+ */
18
+ export declare function MenuPanel({ siteId }: {
19
+ siteId: string;
20
+ }): import("react").JSX.Element;
@@ -0,0 +1,144 @@
1
+ "use client";
2
+ import { jsx, jsxs } from "react/jsx-runtime";
3
+ import { useEffect, useState } from "react";
4
+ import { doc, onSnapshot } from "firebase/firestore";
5
+ import { getDb, getFirebaseApp } from "../../firebase/client.js";
6
+ import { callable } from "../../firebase/functionsClient.js";
7
+ import { useSiteConfig } from "../../config.js";
8
+ import { SiteSettingsSchema, isExterneLink } from "../../schema/settings.js";
9
+ import { subscribePages } from "../lib/pagesClient.js";
10
+ import { Button, Card, Field, Input } from "../ui/primitives.js";
11
+ import { Select } from "../ui/select.js";
12
+ import { ArrayEditor } from "../ui/ArrayEditor.js";
13
+ import { useToast } from "../ui/toast.js";
14
+ function MenuPanel({ siteId }) {
15
+ const cfg = useSiteConfig();
16
+ const { toast } = useToast();
17
+ const [items, setItems] = useState(null);
18
+ const [overigeInstellingen, setOverigeInstellingen] = useState({});
19
+ const [paden, setPaden] = useState([]);
20
+ const [busy, setBusy] = useState(false);
21
+ useEffect(() => subscribePages(siteId, (p) => {
22
+ setPaden(p.map((x) => ({ path: x.path, title: x.title || x.path })).sort((a, b) => a.path.localeCompare(b.path)));
23
+ }), [siteId]);
24
+ useEffect(() => {
25
+ const db = getDb();
26
+ if (!db) return;
27
+ return onSnapshot(doc(db, "sites", siteId, "settings", "site"), (snap) => {
28
+ const s = snap.exists() ? snap.data() : {};
29
+ setOverigeInstellingen(s);
30
+ setItems(s.nav ?? cfg.nav ?? []);
31
+ });
32
+ }, [siteId, cfg.nav]);
33
+ async function opslaan() {
34
+ if (!items) return;
35
+ const leeg = items.findIndex((i) => !i.label.trim() || !i.href.trim());
36
+ if (leeg >= 0) {
37
+ toast({ title: "Onvolledig menu-item", description: `Item ${leeg + 1} mist een naam of een link.`, tone: "error" });
38
+ return;
39
+ }
40
+ const kandidaat = { ...overigeInstellingen, nav: items };
41
+ const parsed = SiteSettingsSchema.safeParse(kandidaat);
42
+ if (!parsed.success) {
43
+ toast({ title: "Ongeldig menu", description: parsed.error.issues[0]?.message, tone: "error" });
44
+ return;
45
+ }
46
+ setBusy(true);
47
+ try {
48
+ const app = getFirebaseApp();
49
+ if (!app) throw new Error("Firebase is niet geconfigureerd.");
50
+ await callable(app, "saveSiteSettings")({ siteId, settings: parsed.data });
51
+ toast({ title: "Menu opgeslagen", description: "De navigatie is bijgewerkt.", tone: "success" });
52
+ } catch (e) {
53
+ toast({ title: "Opslaan mislukt", description: e.message, tone: "error" });
54
+ } finally {
55
+ setBusy(false);
56
+ }
57
+ }
58
+ if (!items) return /* @__PURE__ */ jsx(Card, { children: /* @__PURE__ */ jsx("p", { className: "text-sm text-slate-500", children: "Menu laden\u2026" }) });
59
+ const uitConfig = !overigeInstellingen.nav && (cfg.nav?.length ?? 0) > 0;
60
+ return /* @__PURE__ */ jsx("div", { className: "grid gap-4", children: /* @__PURE__ */ jsxs(Card, { children: [
61
+ /* @__PURE__ */ jsxs("div", { className: "mb-3 flex items-center justify-between gap-4", children: [
62
+ /* @__PURE__ */ jsxs("div", { className: "min-w-0", children: [
63
+ /* @__PURE__ */ jsx("h3", { className: "text-base font-semibold", children: "Hoofdmenu" }),
64
+ /* @__PURE__ */ jsx("p", { className: "text-xs text-slate-500", children: "De volgorde hier is de volgorde in de menubalk." })
65
+ ] }),
66
+ /* @__PURE__ */ jsx(Button, { disabled: busy, onClick: () => void opslaan(), children: busy ? "Opslaan\u2026" : "Menu opslaan" })
67
+ ] }),
68
+ uitConfig && // Eerlijk zijn over wat je bewerkt. Zonder dit lijkt het menu al beheerd te worden vanuit
69
+ // dit scherm, terwijl het nog uit Git komt en een deploy het zou overschrijven.
70
+ /* @__PURE__ */ jsxs("p", { className: "mb-3 rounded-lg bg-amber-50 p-3 text-xs text-amber-900", children: [
71
+ "Dit menu komt nu nog uit ",
72
+ /* @__PURE__ */ jsx("code", { children: "content/site.config.ts" }),
73
+ ". Zodra je hier opslaat, neemt deze versie het over en beheer je het menu vanuit het beheer."
74
+ ] }),
75
+ /* @__PURE__ */ jsx(
76
+ ArrayEditor,
77
+ {
78
+ items,
79
+ onChange: setItems,
80
+ newItem: () => ({ label: "", href: "" }),
81
+ addLabel: "+ Menu-item",
82
+ emptyLabel: "Nog geen menu-items.",
83
+ renderRow: (item, update) => {
84
+ const extern = isExterneLink(item.href);
85
+ return /* @__PURE__ */ jsxs("div", { className: "grid gap-2 md:grid-cols-[1fr_140px_1fr_150px]", children: [
86
+ /* @__PURE__ */ jsx(Field, { label: "Naam", children: /* @__PURE__ */ jsx(Input, { value: item.label, onChange: (e) => update({ label: e.target.value }), placeholder: "Over ons" }) }),
87
+ /* @__PURE__ */ jsx(Field, { label: "Verwijst naar", children: /* @__PURE__ */ jsx(
88
+ Select,
89
+ {
90
+ value: extern ? "extern" : "pagina",
91
+ onValueChange: (v) => (
92
+ // Bij het wisselen de href LEEGMAKEN. Een achtergebleven "/contact" onder de
93
+ // keuze "externe link" is een val: het scherm zegt dan iets anders dan er
94
+ // staat, en de gebruiker ziet het verschil niet.
95
+ update({ href: "", target: v === "extern" ? item.target : item.target === "_embed" ? void 0 : item.target })
96
+ ),
97
+ options: [
98
+ { value: "pagina", label: "Pagina" },
99
+ { value: "extern", label: "Externe link" }
100
+ ]
101
+ }
102
+ ) }),
103
+ /* @__PURE__ */ jsx(Field, { label: extern ? "Adres" : "Pagina", children: extern ? /* @__PURE__ */ jsx(
104
+ Input,
105
+ {
106
+ value: item.href,
107
+ onChange: (e) => update({ href: e.target.value }),
108
+ placeholder: "https://\u2026"
109
+ }
110
+ ) : /* @__PURE__ */ jsx(
111
+ Select,
112
+ {
113
+ value: item.href,
114
+ onValueChange: (v) => update({ href: v }),
115
+ options: [
116
+ { value: "", label: "\u2014 kies een pagina \u2014" },
117
+ ...paden.map((p) => ({ value: p.path, label: `${p.title} (${p.path})` }))
118
+ ]
119
+ }
120
+ ) }),
121
+ /* @__PURE__ */ jsx(Field, { label: "Openen", children: /* @__PURE__ */ jsx(
122
+ Select,
123
+ {
124
+ value: item.target ?? "_self",
125
+ onValueChange: (v) => update({ target: v === "_self" ? void 0 : v }),
126
+ options: [
127
+ { value: "_self", label: "In dit venster" },
128
+ { value: "_blank", label: "In een nieuw venster" },
129
+ // Inkaderen kan alleen bij een extern adres — een eigen pagina in een kader
130
+ // binnen diezelfde site is een lus, geen functie.
131
+ ...extern ? [{ value: "_embed", label: "In een kader in de site" }] : []
132
+ ]
133
+ }
134
+ ) })
135
+ ] });
136
+ }
137
+ }
138
+ )
139
+ ] }) });
140
+ }
141
+ export {
142
+ MenuPanel
143
+ };
144
+ //# sourceMappingURL=MenuPanel.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../src/admin/menu/MenuPanel.tsx"],"sourcesContent":["\"use client\";\n\nimport { useEffect, useState } from \"react\";\nimport { doc, onSnapshot } from \"firebase/firestore\";\nimport { getDb, getFirebaseApp } from \"../../firebase/client\";\nimport { callable } from \"../../firebase/functionsClient\";\nimport { useSiteConfig } from \"../../config\";\nimport { SiteSettingsSchema, isExterneLink, type NavItem, type SiteSettings } from \"../../schema/settings\";\nimport { subscribePages } from \"../lib/pagesClient\";\nimport { Button, Card, Field, Input } from \"../ui/primitives\";\nimport { Select } from \"../ui/select\";\nimport { ArrayEditor } from \"../ui/ArrayEditor\";\nimport { useToast } from \"../ui/toast\";\n\n/**\n * Beheer van de hoofdnavigatie.\n *\n * WAAR HET MENU WOONT. Tot nu toe stond de navigatie in `content/site.config.ts` — in Git, dus\n * alleen te wijzigen door wie de repo kan deployen. Dat is precies het soort werk dat een klant\n * zelf zou moeten kunnen doen: een pagina bijzetten, een volgorde omgooien.\n *\n * Het menu verhuist daarom naar Firestore, met de config als TERUGVAL. Staat er niets in de\n * instellingen, dan blijft de nav uit de site-config werken. Dat is geen halfslachtigheid maar de\n * enige veilige omzetting: elke bestaande site houdt zijn menu, en je migreert per site op het\n * moment dat iemand hier voor het eerst opslaat.\n *\n * PAGINA OF EXTERN. In de opslag staat één ondubbelzinnig adres — geen apart `type`-veld dat uit\n * de pas kan lopen met de href. Of iets extern is, lees je aan de href af. Dit scherm biedt de\n * keuze wél aan, want \"kies een pagina\" is een andere handeling dan \"plak een adres\", en een\n * keuzelijst met bestaande pagina's voorkomt typefouten in interne links.\n */\nexport function MenuPanel({ siteId }: { siteId: string }) {\n const cfg = useSiteConfig();\n const { toast } = useToast();\n const [items, setItems] = useState<NavItem[] | null>(null);\n const [overigeInstellingen, setOverigeInstellingen] = useState<SiteSettings>({});\n const [paden, setPaden] = useState<{ path: string; title: string }[]>([]);\n const [busy, setBusy] = useState(false);\n\n // Bestaande pagina's voor de keuzelijst. Zonder deze lijst zou een interne link handwerk zijn,\n // en een typefout in een pad levert een menu-item op dat naar een 404 wijst.\n useEffect(() => subscribePages(siteId, (p) => {\n setPaden(p.map((x) => ({ path: x.path, title: x.title || x.path })).sort((a, b) => a.path.localeCompare(b.path)));\n }), [siteId]);\n\n useEffect(() => {\n const db = getDb();\n if (!db) return;\n return onSnapshot(doc(db, \"sites\", siteId, \"settings\", \"site\"), (snap) => {\n const s = (snap.exists() ? snap.data() : {}) as SiteSettings;\n setOverigeInstellingen(s);\n // De terugval. `?? cfg.nav` en niet `|| cfg.nav`: een bewust LEEGGEMAAKT menu is een geldige\n // keuze en moet leeg blijven, in plaats van telkens de config terug te toveren.\n setItems(s.nav ?? cfg.nav ?? []);\n });\n }, [siteId, cfg.nav]);\n\n async function opslaan() {\n if (!items) return;\n const leeg = items.findIndex((i) => !i.label.trim() || !i.href.trim());\n if (leeg >= 0) {\n toast({ title: \"Onvolledig menu-item\", description: `Item ${leeg + 1} mist een naam of een link.`, tone: \"error\" });\n return;\n }\n // Het hele instellingen-document terugschrijven, niet alleen `nav`: de callable verwacht de\n // volledige set en leidt daar de publieke subset uit af.\n const kandidaat: SiteSettings = { ...overigeInstellingen, nav: items };\n const parsed = SiteSettingsSchema.safeParse(kandidaat);\n if (!parsed.success) {\n toast({ title: \"Ongeldig menu\", description: parsed.error.issues[0]?.message, 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, \"saveSiteSettings\")({ siteId, settings: parsed.data });\n toast({ title: \"Menu opgeslagen\", description: \"De navigatie is bijgewerkt.\", tone: \"success\" });\n } catch (e) {\n toast({ title: \"Opslaan mislukt\", description: (e as Error).message, tone: \"error\" });\n } finally {\n setBusy(false);\n }\n }\n\n if (!items) return <Card><p className=\"text-sm text-slate-500\">Menu laden…</p></Card>;\n\n const uitConfig = !overigeInstellingen.nav && (cfg.nav?.length ?? 0) > 0;\n\n return (\n <div className=\"grid gap-4\">\n <Card>\n <div className=\"mb-3 flex items-center justify-between gap-4\">\n <div className=\"min-w-0\">\n <h3 className=\"text-base font-semibold\">Hoofdmenu</h3>\n <p className=\"text-xs text-slate-500\">\n De volgorde hier is de volgorde in de menubalk.\n </p>\n </div>\n <Button disabled={busy} onClick={() => void opslaan()}>{busy ? \"Opslaan…\" : \"Menu opslaan\"}</Button>\n </div>\n\n {uitConfig && (\n // Eerlijk zijn over wat je bewerkt. Zonder dit lijkt het menu al beheerd te worden vanuit\n // dit scherm, terwijl het nog uit Git komt en een deploy het zou overschrijven.\n <p className=\"mb-3 rounded-lg bg-amber-50 p-3 text-xs text-amber-900\">\n Dit menu komt nu nog uit <code>content/site.config.ts</code>. Zodra je hier opslaat,\n neemt deze versie het over en beheer je het menu vanuit het beheer.\n </p>\n )}\n\n <ArrayEditor<NavItem>\n items={items}\n onChange={setItems}\n newItem={() => ({ label: \"\", href: \"\" })}\n addLabel=\"+ Menu-item\"\n emptyLabel=\"Nog geen menu-items.\"\n renderRow={(item, update) => {\n const extern = isExterneLink(item.href);\n return (\n <div className=\"grid gap-2 md:grid-cols-[1fr_140px_1fr_150px]\">\n <Field label=\"Naam\">\n <Input value={item.label} onChange={(e) => update({ label: e.target.value })} placeholder=\"Over ons\" />\n </Field>\n\n <Field label=\"Verwijst naar\">\n <Select\n value={extern ? \"extern\" : \"pagina\"}\n onValueChange={(v) =>\n // Bij het wisselen de href LEEGMAKEN. Een achtergebleven \"/contact\" onder de\n // keuze \"externe link\" is een val: het scherm zegt dan iets anders dan er\n // staat, en de gebruiker ziet het verschil niet.\n update({ href: \"\", target: v === \"extern\" ? item.target : (item.target === \"_embed\" ? undefined : item.target) })\n }\n options={[\n { value: \"pagina\", label: \"Pagina\" },\n { value: \"extern\", label: \"Externe link\" },\n ]}\n />\n </Field>\n\n <Field label={extern ? \"Adres\" : \"Pagina\"}>\n {extern ? (\n <Input\n value={item.href}\n onChange={(e) => update({ href: e.target.value })}\n placeholder=\"https://…\"\n />\n ) : (\n <Select\n value={item.href}\n onValueChange={(v) => update({ href: v })}\n options={[\n { value: \"\", label: \"— kies een pagina —\" },\n ...paden.map((p) => ({ value: p.path, label: `${p.title} (${p.path})` })),\n ]}\n />\n )}\n </Field>\n\n <Field label=\"Openen\">\n <Select\n value={item.target ?? \"_self\"}\n // `_self` slaan we op als `undefined`: dat is het standaardgedrag van een gewone\n // link, en een veld dat niets toevoegt hoort niet in de opslag.\n onValueChange={(v) => update({ target: v === \"_self\" ? undefined : (v as NavItem[\"target\"]) })}\n options={[\n { value: \"_self\", label: \"In dit venster\" },\n { value: \"_blank\", label: \"In een nieuw venster\" },\n // Inkaderen kan alleen bij een extern adres — een eigen pagina in een kader\n // binnen diezelfde site is een lus, geen functie.\n ...(extern ? [{ value: \"_embed\", label: \"In een kader in de site\" }] : []),\n ]}\n />\n </Field>\n </div>\n );\n }}\n />\n </Card>\n </div>\n );\n}\n"],"mappings":"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","names":[]}
@@ -8,6 +8,16 @@ export type PanelMinRole = "editor" | "siteadmin";
8
8
  export interface PanelDefinition {
9
9
  /** Moet overeenkomen met de module-`id`; bepaalt of het paneel actief is. */
10
10
  moduleId: string;
11
+ /**
12
+ * Unieke sleutel van dit paneel in de tabbladen. Standaard `moduleId`.
13
+ *
14
+ * Die twee liepen door elkaar: `moduleId` bepaalde zowel of een paneel actief is als welk
15
+ * tabblad het is. Zolang elke module één paneel had viel dat niet op, maar het betekende dat
16
+ * twee panelen van dezelfde module elkaars tabblad overschreven — Radix koppelt op de waarde, en
17
+ * React op de key. Menu en Bouwstenen hangen allebei aan de altijd-actieve content-module, en
18
+ * dat maakte het zichtbaar.
19
+ */
20
+ id?: string;
11
21
  label: string;
12
22
  /** Sorteervolgorde (laag = links). Instellingen staat vast achteraan (100). */
13
23
  order: number;
@@ -1,6 +1,7 @@
1
1
  "use client";
2
2
  import { jsx } from "react/jsx-runtime";
3
3
  import { PagesPanel } from "./pages/PagesPanel.js";
4
+ import { MenuPanel } from "./menu/MenuPanel.js";
4
5
  import { MediaManager } from "./media/MediaManager.js";
5
6
  import { FormsPanel } from "./forms/FormsPanel.js";
6
7
  import { StatsPanel } from "./stats/StatsPanel.js";
@@ -12,13 +13,16 @@ function NewsPanel(_) {
12
13
  }
13
14
  const builtinPanels = [
14
15
  { moduleId: "pages", label: "Pagina's", order: 10, minRole: "editor", Panel: PagesPanel },
16
+ // Menu hoort bij de content-module (altijd actief) en staat vlak na Pagina's: een menu-item
17
+ // wijst naar een pagina, dus die volgorde leest zoals je werkt.
18
+ { moduleId: "content", id: "menu", label: "Menu", order: 15, minRole: "siteadmin", Panel: MenuPanel },
15
19
  { moduleId: "news", label: "Nieuws", order: 20, minRole: "editor", Panel: NewsPanel },
16
20
  { moduleId: "media", label: "Media", order: 30, minRole: "editor", Panel: MediaManager },
17
21
  { moduleId: "forms", label: "Formulieren", order: 40, minRole: "editor", Panel: FormsPanel },
18
22
  { moduleId: "stats", label: "Statistieken", order: 50, minRole: "editor", Panel: StatsPanel },
19
23
  // Webwinkel (6.0): opt-in via `@xiwebdesign/xicms-core/admin/shop` → `shopPanel` in AdminApp.extraPanels.
20
24
  // Bouwstenen (5.0): custom blocks zonder deploy — hangt aan de altijd-actieve content-module.
21
- { moduleId: "content", label: "Bouwstenen", order: 60, minRole: "siteadmin", Panel: CustomBlocksPanel },
25
+ { moduleId: "content", id: "bouwstenen", label: "Bouwstenen", order: 60, minRole: "siteadmin", Panel: CustomBlocksPanel },
22
26
  { moduleId: "settings", label: "Instellingen", order: 100, minRole: "siteadmin", Panel: SettingsPanel }
23
27
  ];
24
28
  export {
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/admin/panelRegistry.tsx"],"sourcesContent":["\"use client\";\n\n/**\n * Paneel-registry — koppelt een beheer-paneel aan een module-id. `AdminApp` rendert de tabs\n * hieruit (gesorteerd op `order`, gated op rol én actieve-module-set) i.p.v. hardgecodeerd. Een\n * nieuwe module met een eigen paneel hoeft `AdminApp` dus niet meer aan te raken.\n */\nimport type { ComponentType } from \"react\";\nimport { PagesPanel } from \"./pages/PagesPanel\";\nimport { MediaManager } from \"./media/MediaManager\";\nimport { FormsPanel } from \"./forms/FormsPanel\";\nimport { StatsPanel } from \"./stats/StatsPanel\";\nimport { SettingsPanel } from \"./settings/SettingsPanel\";\nimport { NewsEditor } from \"./NewsEditor\";\nimport { CustomBlocksPanel } from \"./customBlocks/CustomBlocksPanel\";\n\nexport type PanelMinRole = \"editor\" | \"siteadmin\";\n\nexport interface PanelDefinition {\n /** Moet overeenkomen met de module-`id`; bepaalt of het paneel actief is. */\n moduleId: string;\n label: string;\n /** Sorteervolgorde (laag = links). Instellingen staat vast achteraan (100). */\n order: number;\n minRole: PanelMinRole;\n Panel: ComponentType<{ siteId: string }>;\n}\n\n/** Adapter: de NewsEditor leest zelf siteId via context; wrap voor een uniforme prop-signatuur. */\nfunction NewsPanel(_: { siteId: string }) {\n return <NewsEditor />;\n}\n\n/** De panelen van de ingebouwde modules. */\nexport const builtinPanels: PanelDefinition[] = [\n { moduleId: \"pages\", label: \"Pagina's\", order: 10, minRole: \"editor\", Panel: PagesPanel },\n { moduleId: \"news\", label: \"Nieuws\", order: 20, minRole: \"editor\", Panel: NewsPanel },\n { moduleId: \"media\", label: \"Media\", order: 30, minRole: \"editor\", Panel: MediaManager },\n { moduleId: \"forms\", label: \"Formulieren\", order: 40, minRole: \"editor\", Panel: FormsPanel },\n { moduleId: \"stats\", label: \"Statistieken\", order: 50, minRole: \"editor\", Panel: StatsPanel },\n // Webwinkel (6.0): opt-in via `@xiwebdesign/xicms-core/admin/shop` → `shopPanel` in AdminApp.extraPanels.\n // Bouwstenen (5.0): custom blocks zonder deploy — hangt aan de altijd-actieve content-module.\n { moduleId: \"content\", label: \"Bouwstenen\", order: 60, minRole: \"siteadmin\", Panel: CustomBlocksPanel },\n { moduleId: \"settings\", label: \"Instellingen\", order: 100, minRole: \"siteadmin\", Panel: SettingsPanel },\n];\n"],"mappings":";AA8BS;AAtBT,SAAS,kBAAkB;AAC3B,SAAS,oBAAoB;AAC7B,SAAS,kBAAkB;AAC3B,SAAS,kBAAkB;AAC3B,SAAS,qBAAqB;AAC9B,SAAS,kBAAkB;AAC3B,SAAS,yBAAyB;AAelC,SAAS,UAAU,GAAuB;AACxC,SAAO,oBAAC,cAAW;AACrB;AAGO,MAAM,gBAAmC;AAAA,EAC9C,EAAE,UAAU,SAAS,OAAO,YAAY,OAAO,IAAI,SAAS,UAAU,OAAO,WAAW;AAAA,EACxF,EAAE,UAAU,QAAQ,OAAO,UAAU,OAAO,IAAI,SAAS,UAAU,OAAO,UAAU;AAAA,EACpF,EAAE,UAAU,SAAS,OAAO,SAAS,OAAO,IAAI,SAAS,UAAU,OAAO,aAAa;AAAA,EACvF,EAAE,UAAU,SAAS,OAAO,eAAe,OAAO,IAAI,SAAS,UAAU,OAAO,WAAW;AAAA,EAC3F,EAAE,UAAU,SAAS,OAAO,gBAAgB,OAAO,IAAI,SAAS,UAAU,OAAO,WAAW;AAAA;AAAA;AAAA,EAG5F,EAAE,UAAU,WAAW,OAAO,cAAc,OAAO,IAAI,SAAS,aAAa,OAAO,kBAAkB;AAAA,EACtG,EAAE,UAAU,YAAY,OAAO,gBAAgB,OAAO,KAAK,SAAS,aAAa,OAAO,cAAc;AACxG;","names":[]}
1
+ {"version":3,"sources":["../../src/admin/panelRegistry.tsx"],"sourcesContent":["\"use client\";\n\n/**\n * Paneel-registry — koppelt een beheer-paneel aan een module-id. `AdminApp` rendert de tabs\n * hieruit (gesorteerd op `order`, gated op rol én actieve-module-set) i.p.v. hardgecodeerd. Een\n * nieuwe module met een eigen paneel hoeft `AdminApp` dus niet meer aan te raken.\n */\nimport type { ComponentType } from \"react\";\nimport { PagesPanel } from \"./pages/PagesPanel\";\nimport { MenuPanel } from \"./menu/MenuPanel\";\nimport { MediaManager } from \"./media/MediaManager\";\nimport { FormsPanel } from \"./forms/FormsPanel\";\nimport { StatsPanel } from \"./stats/StatsPanel\";\nimport { SettingsPanel } from \"./settings/SettingsPanel\";\nimport { NewsEditor } from \"./NewsEditor\";\nimport { CustomBlocksPanel } from \"./customBlocks/CustomBlocksPanel\";\n\nexport type PanelMinRole = \"editor\" | \"siteadmin\";\n\nexport interface PanelDefinition {\n /** Moet overeenkomen met de module-`id`; bepaalt of het paneel actief is. */\n moduleId: string;\n /**\n * Unieke sleutel van dit paneel in de tabbladen. Standaard `moduleId`.\n *\n * Die twee liepen door elkaar: `moduleId` bepaalde zowel of een paneel actief is als welk\n * tabblad het is. Zolang elke module één paneel had viel dat niet op, maar het betekende dat\n * twee panelen van dezelfde module elkaars tabblad overschreven — Radix koppelt op de waarde, en\n * React op de key. Menu en Bouwstenen hangen allebei aan de altijd-actieve content-module, en\n * dat maakte het zichtbaar.\n */\n id?: string;\n label: string;\n /** Sorteervolgorde (laag = links). Instellingen staat vast achteraan (100). */\n order: number;\n minRole: PanelMinRole;\n Panel: ComponentType<{ siteId: string }>;\n}\n\n/** Adapter: de NewsEditor leest zelf siteId via context; wrap voor een uniforme prop-signatuur. */\nfunction NewsPanel(_: { siteId: string }) {\n return <NewsEditor />;\n}\n\n/** De panelen van de ingebouwde modules. */\nexport const builtinPanels: PanelDefinition[] = [\n { moduleId: \"pages\", label: \"Pagina's\", order: 10, minRole: \"editor\", Panel: PagesPanel },\n // Menu hoort bij de content-module (altijd actief) en staat vlak na Pagina's: een menu-item\n // wijst naar een pagina, dus die volgorde leest zoals je werkt.\n { moduleId: \"content\", id: \"menu\", label: \"Menu\", order: 15, minRole: \"siteadmin\", Panel: MenuPanel },\n { moduleId: \"news\", label: \"Nieuws\", order: 20, minRole: \"editor\", Panel: NewsPanel },\n { moduleId: \"media\", label: \"Media\", order: 30, minRole: \"editor\", Panel: MediaManager },\n { moduleId: \"forms\", label: \"Formulieren\", order: 40, minRole: \"editor\", Panel: FormsPanel },\n { moduleId: \"stats\", label: \"Statistieken\", order: 50, minRole: \"editor\", Panel: StatsPanel },\n // Webwinkel (6.0): opt-in via `@xiwebdesign/xicms-core/admin/shop` → `shopPanel` in AdminApp.extraPanels.\n // Bouwstenen (5.0): custom blocks zonder deploy — hangt aan de altijd-actieve content-module.\n { moduleId: \"content\", id: \"bouwstenen\", label: \"Bouwstenen\", order: 60, minRole: \"siteadmin\", Panel: CustomBlocksPanel },\n { moduleId: \"settings\", label: \"Instellingen\", order: 100, minRole: \"siteadmin\", Panel: SettingsPanel },\n];\n"],"mappings":";AAyCS;AAjCT,SAAS,kBAAkB;AAC3B,SAAS,iBAAiB;AAC1B,SAAS,oBAAoB;AAC7B,SAAS,kBAAkB;AAC3B,SAAS,kBAAkB;AAC3B,SAAS,qBAAqB;AAC9B,SAAS,kBAAkB;AAC3B,SAAS,yBAAyB;AAyBlC,SAAS,UAAU,GAAuB;AACxC,SAAO,oBAAC,cAAW;AACrB;AAGO,MAAM,gBAAmC;AAAA,EAC9C,EAAE,UAAU,SAAS,OAAO,YAAY,OAAO,IAAI,SAAS,UAAU,OAAO,WAAW;AAAA;AAAA;AAAA,EAGxF,EAAE,UAAU,WAAW,IAAI,QAAQ,OAAO,QAAQ,OAAO,IAAI,SAAS,aAAa,OAAO,UAAU;AAAA,EACpG,EAAE,UAAU,QAAQ,OAAO,UAAU,OAAO,IAAI,SAAS,UAAU,OAAO,UAAU;AAAA,EACpF,EAAE,UAAU,SAAS,OAAO,SAAS,OAAO,IAAI,SAAS,UAAU,OAAO,aAAa;AAAA,EACvF,EAAE,UAAU,SAAS,OAAO,eAAe,OAAO,IAAI,SAAS,UAAU,OAAO,WAAW;AAAA,EAC3F,EAAE,UAAU,SAAS,OAAO,gBAAgB,OAAO,IAAI,SAAS,UAAU,OAAO,WAAW;AAAA;AAAA;AAAA,EAG5F,EAAE,UAAU,WAAW,IAAI,cAAc,OAAO,cAAc,OAAO,IAAI,SAAS,aAAa,OAAO,kBAAkB;AAAA,EACxH,EAAE,UAAU,YAAY,OAAO,gBAAgB,OAAO,KAAK,SAAS,aAAa,OAAO,cAAc;AACxG;","names":[]}
@@ -6,6 +6,7 @@ import { callable } from "../../firebase/functionsClient.js";
6
6
  import { getDb, getFirebaseApp } from "../../firebase/client.js";
7
7
  import { SiteSettingsSchema } from "../../schema/settings.js";
8
8
  import { useModules } from "../../modules/context.js";
9
+ import { useSiteConfig } from "../../config.js";
9
10
  import { builtinThemePresets } from "../../theme/presets.js";
10
11
  import { Button, Card, Field, Input } from "../ui/primitives.js";
11
12
  import { Select } from "../ui/select.js";
@@ -20,6 +21,7 @@ function SettingsPanel({ siteId }) {
20
21
  const { toast } = useToast();
21
22
  const confirm = useConfirm();
22
23
  const { defs } = useModules();
24
+ const cfg = useSiteConfig();
23
25
  const [s, setS] = useState({});
24
26
  const [loaded, setLoaded] = useState(false);
25
27
  const [exporting, setExporting] = useState(false);
@@ -101,10 +103,16 @@ function SettingsPanel({ siteId }) {
101
103
  return;
102
104
  }
103
105
  return onSnapshot(doc(db, "sites", siteId, "settings", "site"), (snap) => {
104
- if (snap.exists()) setS(snap.data());
106
+ const uitDb = snap.exists() ? snap.data() : {};
107
+ setS({
108
+ ...uitDb,
109
+ name: uitDb.name ?? cfg.name,
110
+ domain: uitDb.domain ?? cfg.domain,
111
+ contactEmail: uitDb.contactEmail ?? cfg.contactEmail
112
+ });
105
113
  setLoaded(true);
106
114
  });
107
- }, [siteId]);
115
+ }, [siteId, cfg.name, cfg.domain, cfg.contactEmail]);
108
116
  async function save() {
109
117
  const db = getDb();
110
118
  if (!db) return;