@xiwebdesign/xicms-core 7.15.0 → 7.16.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.
@@ -15,6 +15,7 @@ import { ConfirmHost } from "./ui/ConfirmDialog.js";
15
15
  import { Button } from "./ui/primitives.js";
16
16
  import { PanelIcon } from "./ui/PanelIcon.js";
17
17
  import { builtinPanels } from "./panelRegistry.js";
18
+ import { AdminNavContext } from "./navContext.js";
18
19
  import { AdminHelp } from "./AdminHelp.js";
19
20
  import { CoreVersion } from "./CoreVersion.js";
20
21
  function roleAllows(role, min) {
@@ -52,7 +53,7 @@ function AdminApp({
52
53
  /* @__PURE__ */ jsx("strong", { children: user.email }),
53
54
  ", maar hebt geen beheerrechten voor deze site.",
54
55
  /* @__PURE__ */ jsx("div", { className: "mt-3", children: /* @__PURE__ */ jsx(Button, { variant: "secondary", size: "sm", onClick: () => void signOut(), children: "Uitloggen" }) })
55
- ] }) : 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: [
56
+ ] }) : panels.length === 0 ? /* @__PURE__ */ jsx(Melding, { children: "Geen actieve beheer-modules voor deze site." }) : /* @__PURE__ */ jsx(AdminNavContext.Provider, { value: setActief, children: /* @__PURE__ */ jsxs(Tabs, { value: actief ?? firstTab, onValueChange: setActief, orientation: "vertical", className: "xicms-admin__raster md:grid md:grid-cols-[248px_1fr]", children: [
56
57
  /* @__PURE__ */ jsxs(
57
58
  "div",
58
59
  {
@@ -69,7 +70,7 @@ function AdminApp({
69
70
  value: sleutel(p),
70
71
  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)]",
71
72
  children: [
72
- /* @__PURE__ */ jsx(PanelIcon, { id: p.moduleId }),
73
+ /* @__PURE__ */ jsx(PanelIcon, { id: sleutel(p) }),
73
74
  p.label
74
75
  ]
75
76
  },
@@ -103,7 +104,7 @@ function AdminApp({
103
104
  /* @__PURE__ */ jsx(AdminHelp, {})
104
105
  ] })
105
106
  ] })
106
- ] }) }) }) });
107
+ ] }) }) }) }) });
107
108
  }
108
109
  export {
109
110
  AdminApp
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/admin/AdminApp.tsx"],"sourcesContent":["\"use client\";\n\nimport { registerBuiltinSchemas } from \"../blocks/builtinSchemas\";\n// Schema's zijn lazy (7.13): dit is een editor-chunk, dus hier horen ze thuis.\nregisterBuiltinSchemas();\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 {/*\n Na inloggen naar de HOMEPAGINA, niet naar dit scherm.\n\n Dat is een sturende keuze, geen detail. Wie op het beheer uitkomt gaat daar\n rondkijken en bewerkt zijn teksten in formulieren. Wie op zijn eigen site uitkomt\n ziet de beheerbalk over de pagina heen en past de tekst aan op de plek waar hij\n staat — en dat is waar XiCMS voor gemaakt is. Het beheer blijft één klik weg, via\n \"Beheren\" in die balk, voor het werk dat er wél thuishoort.\n\n Ook hier en niet alleen op /login: je komt op /beheer terecht via een bladwijzer of\n een oude link, en dan gold die keuze ineens niet.\n */}\n <LoginForm onSuccess={() => { window.location.replace(\"/\"); }} />\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":";AA8CM,cA4CI,YA5CJ;AA5CN,SAAS,8BAA8B;AAEvC,uBAAuB;AACvB,SAAS,gBAAgB;AACzB,SAAS,eAAe;AACxB,SAAS,iBAAiB;AAC1B,SAAS,qBAAqB;AAC9B,SAAS,kBAAkB;AAC3B,SAAS,SAAS,qBAAgC;AAClD,SAAS,MAAM,mBAAmB;AAClC,YAAY,mBAAmB;AAC/B,SAAS,qBAAqB;AAC9B,SAAS,mBAAmB;AAC5B,SAAS,cAAc;AACvB,SAAS,iBAAiB;AAC1B,SAAS,qBAA2C;AACpD,SAAS,iBAAiB;AAC1B,SAAS,mBAAmB;AAmB5B,SAAS,WAAW,MAAmB,KAA0C;AAC/E,SAAO,QAAQ,cAAc,cAAc,IAAI,IAAI,QAAQ,IAAI;AACjE;AAGA,SAAS,QAAQ,EAAE,SAAS,GAAkC;AAC5D,SACE,oBAAC,SAAI,WAAU,mDACb,8BAAC,SAAI,WAAU,qFAAqF,UAAS,GAC/G;AAEJ;AAEO,SAAS,SAAS;AAAA,EACvB,cAAc,CAAC;AAAA,EACf,WAAW;AACb,GAaG;AACD,QAAM,EAAE,MAAM,MAAM,SAAS,YAAY,QAAQ,IAAI,QAAQ;AAC7D,QAAM,EAAE,MAAM,OAAO,IAAI,cAAc;AACvC,QAAM,EAAE,UAAU,IAAI,WAAW;AAGjC,QAAM,SAAS,CAAC,GAAG,eAAe,GAAG,WAAW,EAC7C,OAAO,CAAC,MAAM,UAAU,IAAI,EAAE,QAAQ,KAAK,WAAW,MAAM,EAAE,OAAO,CAAC,EACtE,KAAK,CAAC,GAAG,MAAM,EAAE,QAAQ,EAAE,KAAK;AAEnC,QAAM,UAAU,CAAC,MAAuB,EAAE,MAAM,EAAE;AAClD,QAAM,WAAW,OAAO,CAAC,KAAK,QAAQ,OAAO,CAAC,CAAC;AAI/C,QAAM,CAAC,QAAQ,SAAS,IAAI,SAA6B,QAAQ;AACjE,QAAM,eAAe,OAAO,KAAK,CAAC,MAAM,QAAQ,CAAC,OAAO,UAAU,SAAS;AAE3E,SACE,oBAAC,SAAI,WAAW,WAAW,qCAAqC,eAC9D,8BAAC,iBACA,8BAAC,eACC,WAAC,aACA,qBAAC,WAAQ;AAAA;AAAA,IACqC,oBAAC,UAAK,oCAAsB;AAAA,IAAO;AAAA,KAEjF,IACE,UACF,oBAAC,WACC,8BAAC,UAAK,WAAU,kBAAiB,mCAAgB,GACnD,IACE,CAAC,OACH,oBAAC,SAAI,WAAU,mDACb,+BAAC,SAAI,WAAU,mBACb;AAAA,yBAAC,OAAE,WAAU,2CAA0C;AAAA;AAAA,MAAU;AAAA,OAAK;AAAA,IAatE,oBAAC,aAAU,WAAW,MAAM;AAAE,aAAO,SAAS,QAAQ,GAAG;AAAA,IAAG,GAAG;AAAA,KACjE,GACF,IACE,CAAC,QAAQ,IAAI,IACf,qBAAC,WAAQ;AAAA;AAAA,IACc,oBAAC,YAAQ,eAAK,OAAM;AAAA,IAAS;AAAA,IAClD,oBAAC,SAAI,WAAU,QACb,8BAAC,UAAO,SAAQ,aAAY,MAAK,MAAK,SAAS,MAAM,KAAK,QAAQ,GAAG,uBAAS,GAChF;AAAA,KACF,IACE,OAAO,WAAW,IACpB,oBAAC,WAAQ,yDAA2C,IAEpD,qBAAC,QAAK,OAAO,UAAU,UAAU,eAAe,WAAW,aAAY,YAAW,WAAU,wDAE1F;AAAA;AAAA,MAAC;AAAA;AAAA,QACC,WAAU;AAAA,QACV,OAAO,EAAE,YAAY,uBAAuB;AAAA,QAE5C;AAAA,+BAAC,SAAI,WAAU,qBACb;AAAA,gCAAC,OAAE,WAAU,kCAAkC,gBAAK;AAAA,YACpD,oBAAC,OAAE,WAAU,0BAAyB,oBAAM;AAAA,aAC9C;AAAA,UAEA,oBAAC,cAAc,MAAd,EAAmB,cAAW,kBAAiB,WAAU,uFACvD,iBAAO,IAAI,CAAC,MACX;AAAA,YAAC,cAAc;AAAA,YAAd;AAAA,cAEC,OAAO,QAAQ,CAAC;AAAA,cAChB,WAAU;AAAA,cAEV;AAAA,oCAAC,aAAU,IAAI,EAAE,UAAU;AAAA,gBAC1B,EAAE;AAAA;AAAA;AAAA,YALE,QAAQ,CAAC;AAAA,UAMhB,CACD,GACH;AAAA,UAMA,qBAAC,SAAI,WAAU,kGACb;AAAA,iCAAC,SAAI,WAAU,WACb;AAAA,kCAAC,OAAE,WAAU,+CAA8C,OAAO,KAAK,SAAS,QAAY,eAAK,OAAM;AAAA,cACvG,oBAAC,OAAE,WAAU,0BAA0B,gBAAK;AAAA,eAC9C;AAAA,YACA,oBAAC,eAAY;AAAA,YACb,oBAAC,UAAO,SAAQ,aAAY,MAAK,MAAK,SAAS,MAAM,KAAK,QAAQ,GAAG,uBAErE;AAAA,aACF;AAAA;AAAA;AAAA,IACF;AAAA,IAGA,qBAAC,SAAI,WAAU,WACb;AAAA,2BAAC,YAAO,WAAU,0HAAyH,OAAO,EAAE,YAAY,sBAAsB,GACpL;AAAA,4BAAC,QAAG,WAAU,oCAAoC,wBAAc,SAAS,UAAS;AAAA,QAClF;AAAA,UAAC;AAAA;AAAA,YACC,WAAU;AAAA,YACV,MAAK;AAAA,YACN;AAAA;AAAA,QAED;AAAA,SACF;AAAA,MACA,qBAAC,SAAI,WAAU,yBACZ;AAAA,eAAO,IAAI,CAAC,MACX,oBAAC,eAA6B,OAAO,QAAQ,CAAC,GAAG,WAAU,QACzD,8BAAC,EAAE,OAAF,EAAQ,QAAgB,KADT,QAAQ,CAAC,CAE3B,CACD;AAAA,QACD,oBAAC,aAAU;AAAA,SACb;AAAA,OACF;AAAA,KACF,GAEH,GACD,GACF;AAEJ;","names":[]}
1
+ {"version":3,"sources":["../../src/admin/AdminApp.tsx"],"sourcesContent":["\"use client\";\n\nimport { registerBuiltinSchemas } from \"../blocks/builtinSchemas\";\n// Schema's zijn lazy (7.13): dit is een editor-chunk, dus hier horen ze thuis.\nregisterBuiltinSchemas();\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 { AdminNavContext } from \"./navContext\";\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 {/*\n Na inloggen naar de HOMEPAGINA, niet naar dit scherm.\n\n Dat is een sturende keuze, geen detail. Wie op het beheer uitkomt gaat daar\n rondkijken en bewerkt zijn teksten in formulieren. Wie op zijn eigen site uitkomt\n ziet de beheerbalk over de pagina heen en past de tekst aan op de plek waar hij\n staat — en dat is waar XiCMS voor gemaakt is. Het beheer blijft één klik weg, via\n \"Beheren\" in die balk, voor het werk dat er wél thuishoort.\n\n Ook hier en niet alleen op /login: je komt op /beheer terecht via een bladwijzer of\n een oude link, en dan gold die keuze ineens niet.\n */}\n <LoginForm onSuccess={() => { window.location.replace(\"/\"); }} />\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 <AdminNavContext.Provider value={setActief}>\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 {/* Op paneel-sleutel, niet module-id: Menu, Bouwstenen en Start delen de\n content-module maar zijn drie verschillende plekken met drie vormen. */}\n <PanelIcon id={sleutel(p)} />\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 </AdminNavContext.Provider>\n )}\n </ConfirmHost>\n </ToastProvider>\n </div>\n );\n}\n"],"mappings":"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","names":[]}
@@ -0,0 +1,3 @@
1
+ export declare function DashboardPanel({ siteId }: {
2
+ siteId: string;
3
+ }): import("react").JSX.Element;
@@ -0,0 +1,329 @@
1
+ "use client";
2
+ import { jsx, jsxs } from "react/jsx-runtime";
3
+ import { useEffect, useMemo, useState } from "react";
4
+ import {
5
+ collection,
6
+ doc,
7
+ getCountFromServer,
8
+ getDoc,
9
+ getDocs,
10
+ limit,
11
+ orderBy,
12
+ query
13
+ } from "firebase/firestore";
14
+ import { getDb, getFirebaseApp } from "../../firebase/client.js";
15
+ import { callable } from "../../firebase/functionsClient.js";
16
+ import { useAuth } from "../../auth/AuthProvider.js";
17
+ import { canManageSite } from "../../auth/roles.js";
18
+ import { useModules } from "../../modules/context.js";
19
+ import { formatCents } from "../../schema/shop.js";
20
+ import { Badge, Button, Card, Input } from "../ui/primitives.js";
21
+ import { useToast } from "../ui/toast.js";
22
+ import { useAdminNav } from "../navContext.js";
23
+ function updatedMs(v) {
24
+ if (v && typeof v.toDate === "function") return v.toDate().getTime();
25
+ if (typeof v === "string") {
26
+ const t = Date.parse(v);
27
+ return Number.isNaN(t) ? 0 : t;
28
+ }
29
+ return 0;
30
+ }
31
+ function fmtDatum(v) {
32
+ const ms = updatedMs(v);
33
+ if (!ms) return "\u2014";
34
+ return new Date(ms).toLocaleString("nl-NL", { dateStyle: "short", timeStyle: "short" });
35
+ }
36
+ function WidgetKop({ titel, tab, tabLabel }) {
37
+ const gaNaar = useAdminNav();
38
+ return /* @__PURE__ */ jsxs("div", { className: "mb-3 flex items-center justify-between gap-2", children: [
39
+ /* @__PURE__ */ jsx("h2", { className: "text-sm font-semibold text-slate-800", children: titel }),
40
+ tab && /* @__PURE__ */ jsxs(
41
+ "button",
42
+ {
43
+ type: "button",
44
+ onClick: () => gaNaar(tab),
45
+ className: "shrink-0 text-xs font-medium text-brand-700 hover:underline focus-visible:ring-2 focus-visible:ring-brand-500 focus-visible:outline-none",
46
+ children: [
47
+ tabLabel ?? "Bekijk alles",
48
+ " \u2192"
49
+ ]
50
+ }
51
+ )
52
+ ] });
53
+ }
54
+ function DashboardPanel({ siteId }) {
55
+ const { role } = useAuth();
56
+ const { activeIds } = useModules();
57
+ const { toast } = useToast();
58
+ const gaNaar = useAdminNav();
59
+ const heeftShop = activeIds.has("shop");
60
+ const magInstellen = canManageSite(role);
61
+ const [paginas, setPaginas] = useState(null);
62
+ const [maand, setMaand] = useState(void 0);
63
+ const [orders, setOrders] = useState(null);
64
+ const [formulieren, setFormulieren] = useState(null);
65
+ const [links, setLinks] = useState([]);
66
+ const [linksBewerken, setLinksBewerken] = useState(null);
67
+ const [linksOpslaan, setLinksOpslaan] = useState(false);
68
+ useEffect(() => {
69
+ const db = getDb();
70
+ if (!db) {
71
+ setPaginas([]);
72
+ setMaand(null);
73
+ setOrders([]);
74
+ setFormulieren([]);
75
+ return;
76
+ }
77
+ getDocs(collection(db, "sites", siteId, "drafts")).then(
78
+ (snap) => setPaginas(
79
+ snap.docs.map((d) => ({ id: d.id, ...d.data() })).sort((a, b) => updatedMs(b.updatedAt) - updatedMs(a.updatedAt)).slice(0, 5)
80
+ )
81
+ ).catch(() => setPaginas([]));
82
+ const nu = /* @__PURE__ */ new Date();
83
+ const maandId = `${nu.getUTCFullYear()}-${String(nu.getUTCMonth() + 1).padStart(2, "0")}`;
84
+ getDoc(doc(db, "sites", siteId, "stats", maandId)).then((snap) => setMaand(snap.exists() ? snap.data() : null)).catch(() => setMaand(null));
85
+ if (heeftShop) {
86
+ getDocs(query(collection(db, "sites", siteId, "orders"), orderBy("createdAt", "desc"), limit(5))).then((snap) => setOrders(snap.docs.map((d) => ({ id: d.id, ...d.data() })))).catch(() => setOrders([]));
87
+ } else {
88
+ setOrders([]);
89
+ }
90
+ getDocs(collection(db, "sites", siteId, "forms")).then(async (snap) => {
91
+ const forms = snap.docs.slice(0, 4);
92
+ const uit = await Promise.all(
93
+ forms.map(async (f) => {
94
+ const subs = collection(db, "sites", siteId, "forms", f.id, "submissions");
95
+ const [count, laatste] = await Promise.all([
96
+ getCountFromServer(subs).then((c) => c.data().count).catch(() => 0),
97
+ getDocs(query(subs, orderBy("createdAt", "desc"), limit(1))).then((s) => s.docs[0]?.data()?.createdAt).catch(() => void 0)
98
+ ]);
99
+ return { id: f.id, name: f.data().name ?? f.id, aantal: count, laatste };
100
+ })
101
+ );
102
+ setFormulieren(uit);
103
+ }).catch(() => setFormulieren([]));
104
+ getDoc(doc(db, "sites", siteId, "settings", "site")).then((snap) => {
105
+ const d = snap.exists() ? snap.data() : {};
106
+ setLinks(Array.isArray(d.dashboardLinks) ? d.dashboardLinks : []);
107
+ }).catch(() => setLinks([]));
108
+ }, [siteId, heeftShop]);
109
+ const vandaag = useMemo(() => {
110
+ const dag = String((/* @__PURE__ */ new Date()).getUTCDate()).padStart(2, "0");
111
+ return maand?.days?.[dag] ?? 0;
112
+ }, [maand]);
113
+ const topPaden = useMemo(
114
+ () => Object.entries(maand?.paths ?? {}).sort((a, b) => b[1] - a[1]).slice(0, 3),
115
+ [maand]
116
+ );
117
+ async function slaLinksOp() {
118
+ if (!linksBewerken) return;
119
+ const schoon = linksBewerken.filter((l) => l.label.trim() && l.href.trim());
120
+ const app = getFirebaseApp();
121
+ if (!app) return;
122
+ setLinksOpslaan(true);
123
+ try {
124
+ const fn = callable(app, "saveSiteSettings");
125
+ await fn({ siteId, settings: { dashboardLinks: schoon } });
126
+ setLinks(schoon);
127
+ setLinksBewerken(null);
128
+ toast({ title: "Snelkoppelingen opgeslagen", tone: "success" });
129
+ } catch (e) {
130
+ toast({ title: "Opslaan mislukt", description: e.message, tone: "error" });
131
+ } finally {
132
+ setLinksOpslaan(false);
133
+ }
134
+ }
135
+ return /* @__PURE__ */ jsxs("div", { className: "grid gap-4", children: [
136
+ /* @__PURE__ */ jsxs(Card, { children: [
137
+ /* @__PURE__ */ jsx("h2", { className: "text-sm font-semibold text-slate-800", children: "Zo werkt je site" }),
138
+ /* @__PURE__ */ jsxs("div", { className: "mt-2 grid gap-2 text-sm text-slate-600 md:grid-cols-3", children: [
139
+ /* @__PURE__ */ jsxs("p", { children: [
140
+ /* @__PURE__ */ jsx("strong", { className: "text-slate-800", children: "1 \xB7 Bewerk op de site zelf." }),
141
+ " Klik rechtsboven op",
142
+ " ",
143
+ /* @__PURE__ */ jsx("em", { children: "Bekijk site" }),
144
+ " en kies ",
145
+ /* @__PURE__ */ jsx("em", { children: "Bewerken" }),
146
+ " in de zwarte balk. Elk blok krijgt een",
147
+ " ",
148
+ "potlood (tekst) en een tandwiel (instellingen) \u2014 je typt gewoon in de pagina."
149
+ ] }),
150
+ /* @__PURE__ */ jsxs("p", { children: [
151
+ /* @__PURE__ */ jsx("strong", { className: "text-slate-800", children: "2 \xB7 Publiceer als het goed is." }),
152
+ " Alles wat je wijzigt is eerst een ",
153
+ /* @__PURE__ */ jsx("em", { children: "werkversie" }),
154
+ ": bezoekers zien er niets van. Pas de knop",
155
+ " ",
156
+ /* @__PURE__ */ jsx("em", { children: "Publiceren" }),
157
+ " (rechtsonder) zet het live \u2014 en ",
158
+ /* @__PURE__ */ jsx("em", { children: "Verwerpen" }),
159
+ " gooit het weg."
160
+ ] }),
161
+ /* @__PURE__ */ jsxs("p", { children: [
162
+ /* @__PURE__ */ jsx("strong", { className: "text-slate-800", children: "3 \xB7 De rest staat hier." }),
163
+ " Nieuwe pagina's, het menu, foto's, formulieren",
164
+ heeftShop ? ", de webwinkel" : "",
165
+ " en statistieken vind je in de secties hiernaast. Onderaan de zijbalk staat ook een korte helptekst."
166
+ ] })
167
+ ] })
168
+ ] }),
169
+ /* @__PURE__ */ jsxs("div", { className: "grid gap-4 md:grid-cols-2", children: [
170
+ /* @__PURE__ */ jsxs(Card, { children: [
171
+ /* @__PURE__ */ jsx(WidgetKop, { titel: "Bezoek deze maand", tab: "stats", tabLabel: "Statistieken" }),
172
+ maand === void 0 ? /* @__PURE__ */ jsx("p", { className: "text-sm text-slate-500", children: "Laden\u2026" }) : maand === null ? /* @__PURE__ */ jsx("p", { className: "text-sm text-slate-500", children: "Nog geen bezoekersdata deze maand." }) : /* @__PURE__ */ jsxs("div", { className: "grid gap-3", children: [
173
+ /* @__PURE__ */ jsxs("div", { className: "flex items-baseline gap-4", children: [
174
+ /* @__PURE__ */ jsxs("p", { children: [
175
+ /* @__PURE__ */ jsx("span", { className: "text-2xl font-semibold text-slate-900", children: maand.total ?? 0 }),
176
+ " ",
177
+ /* @__PURE__ */ jsx("span", { className: "text-xs text-slate-500", children: "weergaven deze maand" })
178
+ ] }),
179
+ /* @__PURE__ */ jsxs("p", { children: [
180
+ /* @__PURE__ */ jsx("span", { className: "text-lg font-semibold text-slate-700", children: vandaag }),
181
+ " ",
182
+ /* @__PURE__ */ jsx("span", { className: "text-xs text-slate-500", children: "vandaag" })
183
+ ] })
184
+ ] }),
185
+ topPaden.length > 0 && /* @__PURE__ */ jsx("ul", { className: "grid gap-1 text-sm text-slate-600", children: topPaden.map(([pad, n]) => /* @__PURE__ */ jsxs("li", { className: "flex justify-between gap-2", children: [
186
+ /* @__PURE__ */ jsx("span", { className: "truncate", title: pad, children: pad }),
187
+ /* @__PURE__ */ jsx("span", { className: "shrink-0 text-slate-400", children: n })
188
+ ] }, pad)) })
189
+ ] })
190
+ ] }),
191
+ /* @__PURE__ */ jsxs(Card, { children: [
192
+ /* @__PURE__ */ jsx(WidgetKop, { titel: "Laatst bewerkt", tab: "pages", tabLabel: "Pagina's" }),
193
+ paginas === null ? /* @__PURE__ */ jsx("p", { className: "text-sm text-slate-500", children: "Laden\u2026" }) : paginas.length === 0 ? /* @__PURE__ */ jsx("p", { className: "text-sm text-slate-500", children: "Nog geen bewerkte pagina's." }) : /* @__PURE__ */ jsx("ul", { className: "grid gap-1", children: paginas.map((p) => /* @__PURE__ */ jsx("li", { children: /* @__PURE__ */ jsxs(
194
+ "button",
195
+ {
196
+ type: "button",
197
+ onClick: () => gaNaar("pages"),
198
+ className: "flex w-full items-center justify-between gap-2 rounded-lg px-2 py-1.5 text-left text-sm hover:bg-slate-100",
199
+ title: `${p.title ?? p.id} \u2014 ${p.path ?? ""}`,
200
+ children: [
201
+ /* @__PURE__ */ jsxs("span", { className: "min-w-0", children: [
202
+ /* @__PURE__ */ jsx("span", { className: "block truncate text-slate-800", children: p.title ?? p.id }),
203
+ /* @__PURE__ */ jsx("span", { className: "block truncate text-xs text-slate-400", children: p.path })
204
+ ] }),
205
+ /* @__PURE__ */ jsx("span", { className: "shrink-0 text-xs text-slate-400", children: fmtDatum(p.updatedAt) })
206
+ ]
207
+ }
208
+ ) }, p.id)) })
209
+ ] }),
210
+ heeftShop && /* @__PURE__ */ jsxs(Card, { children: [
211
+ /* @__PURE__ */ jsx(WidgetKop, { titel: "Laatste bestellingen", tab: "shop", tabLabel: "Webwinkel" }),
212
+ orders === null ? /* @__PURE__ */ jsx("p", { className: "text-sm text-slate-500", children: "Laden\u2026" }) : orders.length === 0 ? /* @__PURE__ */ jsx("p", { className: "text-sm text-slate-500", children: "Nog geen bestellingen." }) : /* @__PURE__ */ jsx("ul", { className: "grid gap-1", children: orders.map((o) => /* @__PURE__ */ jsx("li", { children: /* @__PURE__ */ jsxs(
213
+ "button",
214
+ {
215
+ type: "button",
216
+ onClick: () => gaNaar("shop"),
217
+ className: "flex w-full items-center justify-between gap-2 rounded-lg px-2 py-1.5 text-left text-sm hover:bg-slate-100",
218
+ children: [
219
+ /* @__PURE__ */ jsxs("span", { className: "min-w-0", children: [
220
+ /* @__PURE__ */ jsx("span", { className: "block truncate text-slate-800", children: o.customer?.name ?? "Onbekend" }),
221
+ /* @__PURE__ */ jsx("span", { className: "block text-xs text-slate-400", children: fmtDatum(o.createdAt) })
222
+ ] }),
223
+ /* @__PURE__ */ jsxs("span", { className: "flex shrink-0 items-center gap-2", children: [
224
+ typeof o.amount?.totalCents === "number" && /* @__PURE__ */ jsx("span", { className: "text-sm font-medium text-slate-700", children: formatCents(o.amount.totalCents) }),
225
+ /* @__PURE__ */ jsx(Badge, { tone: o.status === "paid" || o.status === "shipped" ? "green" : "amber", children: o.status ?? "?" })
226
+ ] })
227
+ ]
228
+ }
229
+ ) }, o.id)) })
230
+ ] }),
231
+ /* @__PURE__ */ jsxs(Card, { children: [
232
+ /* @__PURE__ */ jsx(WidgetKop, { titel: "Formulieren", tab: "forms", tabLabel: "Formulieren" }),
233
+ formulieren === null ? /* @__PURE__ */ jsx("p", { className: "text-sm text-slate-500", children: "Laden\u2026" }) : formulieren.length === 0 ? /* @__PURE__ */ jsx("p", { className: "text-sm text-slate-500", children: "Nog geen formulieren." }) : /* @__PURE__ */ jsx("ul", { className: "grid gap-1", children: formulieren.map((f) => /* @__PURE__ */ jsx("li", { children: /* @__PURE__ */ jsxs(
234
+ "button",
235
+ {
236
+ type: "button",
237
+ onClick: () => gaNaar("forms"),
238
+ className: "flex w-full items-center justify-between gap-2 rounded-lg px-2 py-1.5 text-left text-sm hover:bg-slate-100",
239
+ children: [
240
+ /* @__PURE__ */ jsx("span", { className: "truncate text-slate-800", title: f.name, children: f.name }),
241
+ /* @__PURE__ */ jsxs("span", { className: "shrink-0 text-xs text-slate-500", children: [
242
+ f.aantal,
243
+ " ",
244
+ f.aantal === 1 ? "inzending" : "inzendingen",
245
+ f.laatste ? ` \xB7 laatste ${fmtDatum(f.laatste)}` : ""
246
+ ] })
247
+ ]
248
+ }
249
+ ) }, f.id)) })
250
+ ] }),
251
+ /* @__PURE__ */ jsxs(Card, { children: [
252
+ /* @__PURE__ */ jsxs("div", { className: "mb-3 flex items-center justify-between gap-2", children: [
253
+ /* @__PURE__ */ jsx("h2", { className: "text-sm font-semibold text-slate-800", children: "Snelkoppelingen" }),
254
+ magInstellen && linksBewerken === null && /* @__PURE__ */ jsx(Button, { variant: "secondary", size: "sm", onClick: () => setLinksBewerken(links.length ? [...links] : [{ label: "", href: "" }]), children: "Instellen" })
255
+ ] }),
256
+ linksBewerken ? /* @__PURE__ */ jsxs("div", { className: "grid gap-2", children: [
257
+ /* @__PURE__ */ jsxs("div", { className: "grid grid-cols-[1fr_1.4fr_auto] gap-2 text-xs font-medium text-slate-500", children: [
258
+ /* @__PURE__ */ jsx("span", { children: "Naam" }),
259
+ /* @__PURE__ */ jsx("span", { children: "Adres" }),
260
+ /* @__PURE__ */ jsx("span", { "aria-hidden": "true" })
261
+ ] }),
262
+ linksBewerken.map((l, i) => /* @__PURE__ */ jsxs("div", { className: "grid grid-cols-[1fr_1.4fr_auto] gap-2", children: [
263
+ /* @__PURE__ */ jsx(
264
+ Input,
265
+ {
266
+ value: l.label,
267
+ placeholder: "bv. Webwinkel",
268
+ "aria-label": "Naam van de snelkoppeling",
269
+ onChange: (e) => setLinksBewerken(linksBewerken.map((x, j) => j === i ? { ...x, label: e.target.value } : x))
270
+ }
271
+ ),
272
+ /* @__PURE__ */ jsx(
273
+ Input,
274
+ {
275
+ value: l.href,
276
+ placeholder: "/webwinkel of https://\u2026",
277
+ "aria-label": "Adres van de snelkoppeling",
278
+ onChange: (e) => setLinksBewerken(linksBewerken.map((x, j) => j === i ? { ...x, href: e.target.value } : x))
279
+ }
280
+ ),
281
+ /* @__PURE__ */ jsx(
282
+ Button,
283
+ {
284
+ variant: "ghost",
285
+ size: "sm",
286
+ "aria-label": "Verwijderen",
287
+ onClick: () => setLinksBewerken(linksBewerken.filter((_, j) => j !== i)),
288
+ children: "\u2715"
289
+ }
290
+ )
291
+ ] }, i)),
292
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between gap-2", children: [
293
+ /* @__PURE__ */ jsx(
294
+ Button,
295
+ {
296
+ variant: "secondary",
297
+ size: "sm",
298
+ disabled: linksBewerken.length >= 12,
299
+ onClick: () => setLinksBewerken([...linksBewerken, { label: "", href: "" }]),
300
+ children: "+ Snelkoppeling"
301
+ }
302
+ ),
303
+ /* @__PURE__ */ jsxs("div", { className: "flex gap-2", children: [
304
+ /* @__PURE__ */ jsx(Button, { variant: "secondary", size: "sm", onClick: () => setLinksBewerken(null), children: "Annuleren" }),
305
+ /* @__PURE__ */ jsx(Button, { size: "sm", disabled: linksOpslaan, onClick: () => void slaLinksOp(), children: linksOpslaan ? "Bezig\u2026" : "Opslaan" })
306
+ ] })
307
+ ] })
308
+ ] }) : links.length === 0 ? /* @__PURE__ */ jsx("p", { className: "text-sm text-slate-500", children: magInstellen ? "Nog geen snelkoppelingen. Zet hier de plekken waar je vaak moet zijn \u2014 je webwinkel, een pagina, een extern systeem." : "Nog geen snelkoppelingen ingesteld." }) : /* @__PURE__ */ jsx("ul", { className: "flex flex-wrap gap-2", children: links.map((l, i) => /* @__PURE__ */ jsx("li", { children: /* @__PURE__ */ jsxs(
309
+ "a",
310
+ {
311
+ href: l.href,
312
+ target: /^https?:\/\//i.test(l.href) ? "_blank" : void 0,
313
+ rel: "noreferrer",
314
+ className: "inline-flex items-center gap-1.5 rounded-lg border border-slate-200 px-3 py-1.5 text-sm text-slate-700 hover:bg-slate-100",
315
+ children: [
316
+ l.label,
317
+ " ",
318
+ /* @__PURE__ */ jsx("span", { "aria-hidden": "true", className: "text-slate-400", children: "\u2197" })
319
+ ]
320
+ }
321
+ ) }, i)) })
322
+ ] })
323
+ ] })
324
+ ] });
325
+ }
326
+ export {
327
+ DashboardPanel
328
+ };
329
+ //# sourceMappingURL=DashboardPanel.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../src/admin/dashboard/DashboardPanel.tsx"],"sourcesContent":["\"use client\";\n\n/**\n * Startpagina van /beheer (7.16).\n *\n * /beheer opende recht in het pagina-overzicht: een lijst technische titels als eerste indruk, en\n * nergens een plek die uitlegt hoe het systeem in elkaar zit. Deze startpagina doet drie dingen:\n * uitleggen hoe je de site bewerkt (op de site zelf, niet hier), in één oogopslag tonen wat er\n * gebeurt (bezoek, bestellingen, inzendingen, laatst bewerkt), en snelkoppelingen bieden die de\n * siteadmin zelf kan instellen.\n *\n * Bewust eenmalige reads (geen live listeners): dit is een overzicht bij binnenkomst, geen\n * monitor. Elke widget vangt zijn eigen fouten — een dichtgezette collectie (rules, module uit)\n * mag nooit de hele startpagina blokkeren.\n */\nimport { useEffect, useMemo, useState } from \"react\";\nimport {\n collection,\n doc,\n getCountFromServer,\n getDoc,\n getDocs,\n limit,\n orderBy,\n query,\n} from \"firebase/firestore\";\nimport type { Timestamp } from \"firebase/firestore\";\nimport { getDb, getFirebaseApp } from \"../../firebase/client\";\nimport { callable } from \"../../firebase/functionsClient\";\nimport { useAuth } from \"../../auth/AuthProvider\";\nimport { canManageSite } from \"../../auth/roles\";\nimport { useModules } from \"../../modules/context\";\nimport { formatCents } from \"../../schema/shop\";\nimport { Badge, Button, Card, Input } from \"../ui/primitives\";\nimport { useToast } from \"../ui/toast\";\nimport { useAdminNav } from \"../navContext\";\n\ninterface RecentePagina {\n id: string;\n title?: string;\n path?: string;\n /** Timestamp uit de editor, ISO-string uit import-scripts — zie updatedMs(). */\n updatedAt?: unknown;\n}\ninterface MaandStat {\n total?: number;\n days?: Record<string, number>;\n paths?: Record<string, number>;\n}\ninterface RecenteOrder {\n id: string;\n status?: string;\n createdAt?: Timestamp;\n customer?: { name?: string };\n amount?: { totalCents?: number };\n}\ninterface FormOverzicht {\n id: string;\n name: string;\n aantal: number;\n laatste?: Timestamp;\n}\ninterface Snelkoppeling {\n label: string;\n href: string;\n}\n\n/**\n * `updatedAt` is in de praktijk een Firestore-Timestamp (editor-saves) ÓF een ISO-string\n * (import-/migratiescripts schrijven via REST) — live aangetroffen op christelijkkraamcadeau,\n * dóór elkaar in dezelfde collectie. Firestore sorteert die in aparte typegroepen, dus een\n * server-side orderBy geeft dan een fout beeld; we normaliseren en sorteren daarom client-side.\n */\nfunction updatedMs(v: unknown): number {\n if (v && typeof (v as Timestamp).toDate === \"function\") return (v as Timestamp).toDate().getTime();\n if (typeof v === \"string\") {\n const t = Date.parse(v);\n return Number.isNaN(t) ? 0 : t;\n }\n return 0;\n}\n\nfunction fmtDatum(v: unknown): string {\n const ms = updatedMs(v);\n if (!ms) return \"—\";\n return new Date(ms).toLocaleString(\"nl-NL\", { dateStyle: \"short\", timeStyle: \"short\" });\n}\n\n/** Kop van een widget met een \"ga ernaartoe\"-pijl die naar het bijbehorende paneel springt. */\nfunction WidgetKop({ titel, tab, tabLabel }: { titel: string; tab?: string; tabLabel?: string }) {\n const gaNaar = useAdminNav();\n return (\n <div className=\"mb-3 flex items-center justify-between gap-2\">\n <h2 className=\"text-sm font-semibold text-slate-800\">{titel}</h2>\n {tab && (\n <button\n type=\"button\"\n onClick={() => gaNaar(tab)}\n className=\"shrink-0 text-xs font-medium text-brand-700 hover:underline focus-visible:ring-2 focus-visible:ring-brand-500 focus-visible:outline-none\"\n >\n {tabLabel ?? \"Bekijk alles\"} →\n </button>\n )}\n </div>\n );\n}\n\nexport function DashboardPanel({ siteId }: { siteId: string }) {\n const { role } = useAuth();\n const { activeIds } = useModules();\n const { toast } = useToast();\n const gaNaar = useAdminNav();\n const heeftShop = activeIds.has(\"shop\");\n const magInstellen = canManageSite(role);\n\n const [paginas, setPaginas] = useState<RecentePagina[] | null>(null);\n const [maand, setMaand] = useState<MaandStat | null | undefined>(undefined);\n const [orders, setOrders] = useState<RecenteOrder[] | null>(null);\n const [formulieren, setFormulieren] = useState<FormOverzicht[] | null>(null);\n const [links, setLinks] = useState<Snelkoppeling[]>([]);\n const [linksBewerken, setLinksBewerken] = useState<Snelkoppeling[] | null>(null);\n const [linksOpslaan, setLinksOpslaan] = useState(false);\n\n useEffect(() => {\n const db = getDb();\n if (!db) {\n setPaginas([]);\n setMaand(null);\n setOrders([]);\n setFormulieren([]);\n return;\n }\n\n // Laatst bewerkte pagina's. Bewust GEEN server-side orderBy op updatedAt: dat veld is een\n // mengsel van Timestamps en ISO-strings (zie updatedMs) en Firestore sorteert per typegroep.\n // Drafts-collecties zijn klein; alles ophalen en zelf sorteren is hier de betrouwbare weg.\n getDocs(collection(db, \"sites\", siteId, \"drafts\"))\n .then((snap) =>\n setPaginas(\n snap.docs\n .map((d) => ({ id: d.id, ...(d.data() as Omit<RecentePagina, \"id\">) }))\n .sort((a, b) => updatedMs(b.updatedAt) - updatedMs(a.updatedAt))\n .slice(0, 5),\n ),\n )\n .catch(() => setPaginas([]));\n\n // Bezoek deze maand (zelfde bron als het Statistieken-paneel).\n const nu = new Date();\n const maandId = `${nu.getUTCFullYear()}-${String(nu.getUTCMonth() + 1).padStart(2, \"0\")}`;\n getDoc(doc(db, \"sites\", siteId, \"stats\", maandId))\n .then((snap) => setMaand(snap.exists() ? (snap.data() as MaandStat) : null))\n .catch(() => setMaand(null));\n\n // Laatste bestellingen — alleen als de shop-module actief is.\n if (heeftShop) {\n getDocs(query(collection(db, \"sites\", siteId, \"orders\"), orderBy(\"createdAt\", \"desc\"), limit(5)))\n .then((snap) => setOrders(snap.docs.map((d) => ({ id: d.id, ...(d.data() as Omit<RecenteOrder, \"id\">) }))))\n .catch(() => setOrders([]));\n } else {\n setOrders([]);\n }\n\n // Formulieren: naam + aantal inzendingen + laatste inzending (aggregatie-count, geen\n // volledige submissions-download). Max vier formulieren — de widget is een signaal, geen lijst.\n getDocs(collection(db, \"sites\", siteId, \"forms\"))\n .then(async (snap) => {\n const forms = snap.docs.slice(0, 4);\n const uit = await Promise.all(\n forms.map(async (f) => {\n const subs = collection(db, \"sites\", siteId, \"forms\", f.id, \"submissions\");\n const [count, laatste] = await Promise.all([\n getCountFromServer(subs).then((c) => c.data().count).catch(() => 0),\n getDocs(query(subs, orderBy(\"createdAt\", \"desc\"), limit(1)))\n .then((s) => (s.docs[0]?.data() as { createdAt?: Timestamp } | undefined)?.createdAt)\n .catch(() => undefined),\n ]);\n return { id: f.id, name: (f.data() as { name?: string }).name ?? f.id, aantal: count, laatste };\n }),\n );\n setFormulieren(uit);\n })\n .catch(() => setFormulieren([]));\n\n // Snelkoppelingen uit settings/site (editor mag lezen; instellen is siteadmin-werk).\n getDoc(doc(db, \"sites\", siteId, \"settings\", \"site\"))\n .then((snap) => {\n const d = snap.exists() ? (snap.data() as { dashboardLinks?: Snelkoppeling[] }) : {};\n setLinks(Array.isArray(d.dashboardLinks) ? d.dashboardLinks : []);\n })\n .catch(() => setLinks([]));\n }, [siteId, heeftShop]);\n\n const vandaag = useMemo(() => {\n const dag = String(new Date().getUTCDate()).padStart(2, \"0\");\n return maand?.days?.[dag] ?? 0;\n }, [maand]);\n const topPaden = useMemo(\n () => Object.entries(maand?.paths ?? {}).sort((a, b) => b[1] - a[1]).slice(0, 3),\n [maand],\n );\n\n async function slaLinksOp() {\n if (!linksBewerken) return;\n const schoon = linksBewerken.filter((l) => l.label.trim() && l.href.trim());\n const app = getFirebaseApp();\n if (!app) return;\n setLinksOpslaan(true);\n try {\n // Zelfde route als de rest van de instellingen: de saveSiteSettings-callable (merge).\n const fn = callable(app, \"saveSiteSettings\");\n await fn({ siteId, settings: { dashboardLinks: schoon } });\n setLinks(schoon);\n setLinksBewerken(null);\n toast({ title: \"Snelkoppelingen opgeslagen\", tone: \"success\" });\n } catch (e) {\n toast({ title: \"Opslaan mislukt\", description: (e as Error).message, tone: \"error\" });\n } finally {\n setLinksOpslaan(false);\n }\n }\n\n return (\n <div className=\"grid gap-4\">\n {/* ── Zo werkt het ── */}\n <Card>\n <h2 className=\"text-sm font-semibold text-slate-800\">Zo werkt je site</h2>\n <div className=\"mt-2 grid gap-2 text-sm text-slate-600 md:grid-cols-3\">\n <p>\n <strong className=\"text-slate-800\">1 · Bewerk op de site zelf.</strong> Klik rechtsboven op{\" \"}\n <em>Bekijk site</em> en kies <em>Bewerken</em> in de zwarte balk. Elk blok krijgt een{\" \"}\n potlood (tekst) en een tandwiel (instellingen) — je typt gewoon in de pagina.\n </p>\n <p>\n <strong className=\"text-slate-800\">2 · Publiceer als het goed is.</strong> Alles wat je\n wijzigt is eerst een <em>werkversie</em>: bezoekers zien er niets van. Pas de knop{\" \"}\n <em>Publiceren</em> (rechtsonder) zet het live — en <em>Verwerpen</em> gooit het weg.\n </p>\n <p>\n <strong className=\"text-slate-800\">3 · De rest staat hier.</strong> Nieuwe pagina&apos;s, het\n menu, foto&apos;s, formulieren{heeftShop ? \", de webwinkel\" : \"\"} en statistieken vind je in\n de secties hiernaast. Onderaan de zijbalk staat ook een korte helptekst.\n </p>\n </div>\n </Card>\n\n <div className=\"grid gap-4 md:grid-cols-2\">\n {/* ── Statistieken ── */}\n <Card>\n <WidgetKop titel=\"Bezoek deze maand\" tab=\"stats\" tabLabel=\"Statistieken\" />\n {maand === undefined ? (\n <p className=\"text-sm text-slate-500\">Laden…</p>\n ) : maand === null ? (\n <p className=\"text-sm text-slate-500\">Nog geen bezoekersdata deze maand.</p>\n ) : (\n <div className=\"grid gap-3\">\n <div className=\"flex items-baseline gap-4\">\n <p><span className=\"text-2xl font-semibold text-slate-900\">{maand.total ?? 0}</span>{\" \"}\n <span className=\"text-xs text-slate-500\">weergaven deze maand</span></p>\n <p><span className=\"text-lg font-semibold text-slate-700\">{vandaag}</span>{\" \"}\n <span className=\"text-xs text-slate-500\">vandaag</span></p>\n </div>\n {topPaden.length > 0 && (\n <ul className=\"grid gap-1 text-sm text-slate-600\">\n {topPaden.map(([pad, n]) => (\n <li key={pad} className=\"flex justify-between gap-2\">\n <span className=\"truncate\" title={pad}>{pad}</span>\n <span className=\"shrink-0 text-slate-400\">{n}</span>\n </li>\n ))}\n </ul>\n )}\n </div>\n )}\n </Card>\n\n {/* ── Laatst bewerkte pagina's ── */}\n <Card>\n <WidgetKop titel=\"Laatst bewerkt\" tab=\"pages\" tabLabel=\"Pagina's\" />\n {paginas === null ? (\n <p className=\"text-sm text-slate-500\">Laden…</p>\n ) : paginas.length === 0 ? (\n <p className=\"text-sm text-slate-500\">Nog geen bewerkte pagina&apos;s.</p>\n ) : (\n <ul className=\"grid gap-1\">\n {paginas.map((p) => (\n <li key={p.id}>\n <button\n type=\"button\"\n onClick={() => gaNaar(\"pages\")}\n className=\"flex w-full items-center justify-between gap-2 rounded-lg px-2 py-1.5 text-left text-sm hover:bg-slate-100\"\n title={`${p.title ?? p.id} — ${p.path ?? \"\"}`}\n >\n <span className=\"min-w-0\">\n <span className=\"block truncate text-slate-800\">{p.title ?? p.id}</span>\n <span className=\"block truncate text-xs text-slate-400\">{p.path}</span>\n </span>\n <span className=\"shrink-0 text-xs text-slate-400\">{fmtDatum(p.updatedAt)}</span>\n </button>\n </li>\n ))}\n </ul>\n )}\n </Card>\n\n {/* ── Bestellingen (alleen met shop) ── */}\n {heeftShop && (\n <Card>\n <WidgetKop titel=\"Laatste bestellingen\" tab=\"shop\" tabLabel=\"Webwinkel\" />\n {orders === null ? (\n <p className=\"text-sm text-slate-500\">Laden…</p>\n ) : orders.length === 0 ? (\n <p className=\"text-sm text-slate-500\">Nog geen bestellingen.</p>\n ) : (\n <ul className=\"grid gap-1\">\n {orders.map((o) => (\n <li key={o.id}>\n <button\n type=\"button\"\n onClick={() => gaNaar(\"shop\")}\n className=\"flex w-full items-center justify-between gap-2 rounded-lg px-2 py-1.5 text-left text-sm hover:bg-slate-100\"\n >\n <span className=\"min-w-0\">\n <span className=\"block truncate text-slate-800\">{o.customer?.name ?? \"Onbekend\"}</span>\n <span className=\"block text-xs text-slate-400\">{fmtDatum(o.createdAt)}</span>\n </span>\n <span className=\"flex shrink-0 items-center gap-2\">\n {typeof o.amount?.totalCents === \"number\" && (\n <span className=\"text-sm font-medium text-slate-700\">{formatCents(o.amount.totalCents)}</span>\n )}\n <Badge tone={o.status === \"paid\" || o.status === \"shipped\" ? \"green\" : \"amber\"}>\n {o.status ?? \"?\"}\n </Badge>\n </span>\n </button>\n </li>\n ))}\n </ul>\n )}\n </Card>\n )}\n\n {/* ── Formulieren ── */}\n <Card>\n <WidgetKop titel=\"Formulieren\" tab=\"forms\" tabLabel=\"Formulieren\" />\n {formulieren === null ? (\n <p className=\"text-sm text-slate-500\">Laden…</p>\n ) : formulieren.length === 0 ? (\n <p className=\"text-sm text-slate-500\">Nog geen formulieren.</p>\n ) : (\n <ul className=\"grid gap-1\">\n {formulieren.map((f) => (\n <li key={f.id}>\n <button\n type=\"button\"\n onClick={() => gaNaar(\"forms\")}\n className=\"flex w-full items-center justify-between gap-2 rounded-lg px-2 py-1.5 text-left text-sm hover:bg-slate-100\"\n >\n <span className=\"truncate text-slate-800\" title={f.name}>{f.name}</span>\n <span className=\"shrink-0 text-xs text-slate-500\">\n {f.aantal} {f.aantal === 1 ? \"inzending\" : \"inzendingen\"}\n {f.laatste ? ` · laatste ${fmtDatum(f.laatste)}` : \"\"}\n </span>\n </button>\n </li>\n ))}\n </ul>\n )}\n </Card>\n\n {/* ── Snelkoppelingen ── */}\n <Card>\n <div className=\"mb-3 flex items-center justify-between gap-2\">\n <h2 className=\"text-sm font-semibold text-slate-800\">Snelkoppelingen</h2>\n {magInstellen && linksBewerken === null && (\n <Button variant=\"secondary\" size=\"sm\" onClick={() => setLinksBewerken(links.length ? [...links] : [{ label: \"\", href: \"\" }])}>\n Instellen\n </Button>\n )}\n </div>\n {linksBewerken ? (\n <div className=\"grid gap-2\">\n <div className=\"grid grid-cols-[1fr_1.4fr_auto] gap-2 text-xs font-medium text-slate-500\">\n <span>Naam</span>\n <span>Adres</span>\n <span aria-hidden=\"true\" />\n </div>\n {linksBewerken.map((l, i) => (\n <div key={i} className=\"grid grid-cols-[1fr_1.4fr_auto] gap-2\">\n <Input value={l.label} placeholder=\"bv. Webwinkel\" aria-label=\"Naam van de snelkoppeling\"\n onChange={(e) => setLinksBewerken(linksBewerken.map((x, j) => (j === i ? { ...x, label: e.target.value } : x)))} />\n <Input value={l.href} placeholder=\"/webwinkel of https://…\" aria-label=\"Adres van de snelkoppeling\"\n onChange={(e) => setLinksBewerken(linksBewerken.map((x, j) => (j === i ? { ...x, href: e.target.value } : x)))} />\n <Button variant=\"ghost\" size=\"sm\" aria-label=\"Verwijderen\"\n onClick={() => setLinksBewerken(linksBewerken.filter((_, j) => j !== i))}>✕</Button>\n </div>\n ))}\n <div className=\"flex items-center justify-between gap-2\">\n <Button variant=\"secondary\" size=\"sm\" disabled={linksBewerken.length >= 12}\n onClick={() => setLinksBewerken([...linksBewerken, { label: \"\", href: \"\" }])}>\n + Snelkoppeling\n </Button>\n <div className=\"flex gap-2\">\n <Button variant=\"secondary\" size=\"sm\" onClick={() => setLinksBewerken(null)}>Annuleren</Button>\n <Button size=\"sm\" disabled={linksOpslaan} onClick={() => void slaLinksOp()}>\n {linksOpslaan ? \"Bezig…\" : \"Opslaan\"}\n </Button>\n </div>\n </div>\n </div>\n ) : links.length === 0 ? (\n <p className=\"text-sm text-slate-500\">\n {magInstellen\n ? \"Nog geen snelkoppelingen. Zet hier de plekken waar je vaak moet zijn — je webwinkel, een pagina, een extern systeem.\"\n : \"Nog geen snelkoppelingen ingesteld.\"}\n </p>\n ) : (\n <ul className=\"flex flex-wrap gap-2\">\n {links.map((l, i) => (\n <li key={i}>\n <a\n href={l.href}\n target={/^https?:\\/\\//i.test(l.href) ? \"_blank\" : undefined}\n rel=\"noreferrer\"\n className=\"inline-flex items-center gap-1.5 rounded-lg border border-slate-200 px-3 py-1.5 text-sm text-slate-700 hover:bg-slate-100\"\n >\n {l.label} <span aria-hidden=\"true\" className=\"text-slate-400\">↗</span>\n </a>\n </li>\n ))}\n </ul>\n )}\n </Card>\n </div>\n </div>\n );\n}\n"],"mappings":"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","names":[]}
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Navigatie binnen /beheer: van paneel wisselen zonder dat een paneel `AdminApp` hoeft te kennen.
3
+ *
4
+ * De tabs zijn gecontroleerde state in `AdminApp` (`setActief`). De startpagina (7.16) wil
5
+ * daarheen kunnen springen ("Bekijk je statistieken →"), en een prop door de generieke
6
+ * `PanelDefinition`-signatuur heen breken zou élk paneel raken. Een context is hier het juiste
7
+ * gereedschap: wie hem nodig heeft pakt hem, de rest merkt niets.
8
+ *
9
+ * Buiten een provider is het bewust een no-op: een paneel dat los gerenderd wordt (test, storybook,
10
+ * embedded gebruik) mag nooit crashen op een ontbrekende navigatie.
11
+ */
12
+ export declare const AdminNavContext: import("react").Context<(tab: string) => void>;
13
+ /** Spring naar een ander beheer-paneel (sleutel = `id ?? moduleId` uit de paneel-registry). */
14
+ export declare function useAdminNav(): (tab: string) => void;
@@ -0,0 +1,12 @@
1
+ "use client";
2
+ import { createContext, useContext } from "react";
3
+ const AdminNavContext = createContext(() => {
4
+ });
5
+ function useAdminNav() {
6
+ return useContext(AdminNavContext);
7
+ }
8
+ export {
9
+ AdminNavContext,
10
+ useAdminNav
11
+ };
12
+ //# sourceMappingURL=navContext.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/admin/navContext.ts"],"sourcesContent":["\"use client\";\n\nimport { createContext, useContext } from \"react\";\n\n/**\n * Navigatie binnen /beheer: van paneel wisselen zonder dat een paneel `AdminApp` hoeft te kennen.\n *\n * De tabs zijn gecontroleerde state in `AdminApp` (`setActief`). De startpagina (7.16) wil\n * daarheen kunnen springen (\"Bekijk je statistieken →\"), en een prop door de generieke\n * `PanelDefinition`-signatuur heen breken zou élk paneel raken. Een context is hier het juiste\n * gereedschap: wie hem nodig heeft pakt hem, de rest merkt niets.\n *\n * Buiten een provider is het bewust een no-op: een paneel dat los gerenderd wordt (test, storybook,\n * embedded gebruik) mag nooit crashen op een ontbrekende navigatie.\n */\nexport const AdminNavContext = createContext<(tab: string) => void>(() => {});\n\n/** Spring naar een ander beheer-paneel (sleutel = `id ?? moduleId` uit de paneel-registry). */\nexport function useAdminNav(): (tab: string) => void {\n return useContext(AdminNavContext);\n}\n"],"mappings":";AAEA,SAAS,eAAe,kBAAkB;AAanC,MAAM,kBAAkB,cAAqC,MAAM;AAAC,CAAC;AAGrE,SAAS,cAAqC;AACnD,SAAO,WAAW,eAAe;AACnC;","names":[]}
@@ -10,6 +10,36 @@ import { PageEditor } from "./PageEditor.js";
10
10
  function slugify(s) {
11
11
  return s.toLowerCase().normalize("NFD").replace(/[̀-ͯ]/g, "").replace(/[^a-z0-9]+/g, "-").replace(/(^-|-$)/g, "");
12
12
  }
13
+ const FUNCTIONELE_TYPES = /* @__PURE__ */ new Set([
14
+ "shopCategories",
15
+ "productGrid",
16
+ "productDetail",
17
+ "cart",
18
+ "checkout",
19
+ "donation",
20
+ "orderStatus"
21
+ ]);
22
+ function isFunctioneel(p) {
23
+ if (p.path === "/") return false;
24
+ return (p.blocks ?? []).some((b) => FUNCTIONELE_TYPES.has(String(b._type)));
25
+ }
26
+ function PaginaKnop({ p, selected, onClick }) {
27
+ return /* @__PURE__ */ jsxs(
28
+ "button",
29
+ {
30
+ onClick,
31
+ title: `${p.title} \u2014 ${p.path}`,
32
+ className: `flex w-full items-center justify-between gap-2 rounded-lg px-3 py-2 text-left text-sm ${selected ? "bg-brand-50 text-brand-700" : "hover:bg-slate-100"}`,
33
+ children: [
34
+ /* @__PURE__ */ jsxs("span", { className: "min-w-0", children: [
35
+ /* @__PURE__ */ jsx("span", { className: "block truncate", children: p.title }),
36
+ /* @__PURE__ */ jsx("span", { className: `block truncate text-xs ${selected ? "text-brand-700/70" : "text-slate-400"}`, children: p.path })
37
+ ] }),
38
+ p.published ? /* @__PURE__ */ jsx(Badge, { tone: "green", children: "live" }) : /* @__PURE__ */ jsx(Badge, { tone: "amber", children: "concept" })
39
+ ]
40
+ }
41
+ );
42
+ }
13
43
  function PagesPanel({ siteId }) {
14
44
  const { toast } = useToast();
15
45
  const confirm = useConfirm();
@@ -17,8 +47,10 @@ function PagesPanel({ siteId }) {
17
47
  const [selected, setSelected] = useState(null);
18
48
  const [newTitle, setNewTitle] = useState("");
19
49
  useEffect(() => subscribePages(siteId, setPages), [siteId]);
50
+ const gewoon = pages.filter((p) => !isFunctioneel(p));
51
+ const functioneel = pages.filter(isFunctioneel);
20
52
  useEffect(() => {
21
- if (!selected && pages.length) setSelected(pages[0].id);
53
+ if (!selected && pages.length) setSelected((gewoon[0] ?? pages[0]).id);
22
54
  }, [pages, selected]);
23
55
  const current = pages.find((p) => p.id === selected) ?? null;
24
56
  async function create() {
@@ -50,17 +82,12 @@ function PagesPanel({ siteId }) {
50
82
  ] }) })
51
83
  ] })
52
84
  ] }),
53
- /* @__PURE__ */ jsx("ul", { className: "grid gap-1", children: pages.map((p) => /* @__PURE__ */ jsx("li", { children: /* @__PURE__ */ jsxs(
54
- "button",
55
- {
56
- onClick: () => setSelected(p.id),
57
- className: `flex w-full items-center justify-between rounded-lg px-3 py-2 text-left text-sm ${p.id === selected ? "bg-brand-50 text-brand-700" : "hover:bg-slate-100"}`,
58
- children: [
59
- /* @__PURE__ */ jsx("span", { className: "truncate", children: p.title }),
60
- p.published ? /* @__PURE__ */ jsx(Badge, { tone: "green", children: "live" }) : /* @__PURE__ */ jsx(Badge, { tone: "amber", children: "concept" })
61
- ]
62
- }
63
- ) }, p.id)) })
85
+ /* @__PURE__ */ jsx("ul", { className: "grid gap-1", children: gewoon.map((p) => /* @__PURE__ */ jsx("li", { className: "min-w-0", children: /* @__PURE__ */ jsx(PaginaKnop, { p, selected: p.id === selected, onClick: () => setSelected(p.id) }) }, p.id)) }),
86
+ functioneel.length > 0 && /* @__PURE__ */ jsxs("div", { className: "mt-4 border-t border-slate-200 pt-3", children: [
87
+ /* @__PURE__ */ jsx("p", { className: "mb-1 px-3 text-xs font-medium tracking-wide text-slate-500 uppercase", children: "Winkel & functioneel" }),
88
+ /* @__PURE__ */ jsx("p", { className: "mb-2 px-3 text-xs text-slate-400", children: "Deze pagina's zijn het omhulsel van een functie. Producten en bestellingen beheer je onder Webwinkel; hier pas je alleen de tekst eromheen aan." }),
89
+ /* @__PURE__ */ jsx("ul", { className: "grid gap-1", children: functioneel.map((p) => /* @__PURE__ */ jsx("li", { className: "min-w-0", children: /* @__PURE__ */ jsx(PaginaKnop, { p, selected: p.id === selected, onClick: () => setSelected(p.id) }) }, p.id)) })
90
+ ] })
64
91
  ] }),
65
92
  /* @__PURE__ */ jsx("div", { children: current ? /* @__PURE__ */ jsxs(Fragment, { children: [
66
93
  /* @__PURE__ */ jsx(PageEditor, { siteId, page: current, allPages: pages }),