@xiwebdesign/xicms-core 7.2.0 → 7.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (123) hide show
  1. package/dist/XiCMSProvider.d.ts +2 -8
  2. package/dist/XiCMSProvider.js +1 -8
  3. package/dist/XiCMSProvider.js.map +1 -1
  4. package/dist/admin/AdminApp.js +7 -12
  5. package/dist/admin/AdminApp.js.map +1 -1
  6. package/dist/admin/NewsEditor.d.ts +8 -1
  7. package/dist/admin/NewsEditor.js +19 -18
  8. package/dist/admin/NewsEditor.js.map +1 -1
  9. package/dist/admin/SuperadminPanel.js +6 -7
  10. package/dist/admin/SuperadminPanel.js.map +1 -1
  11. package/dist/admin/customBlocks/CustomBlocksPanel.js +3 -4
  12. package/dist/admin/customBlocks/CustomBlocksPanel.js.map +1 -1
  13. package/dist/admin/lib/pagesClient.js +3 -4
  14. package/dist/admin/lib/pagesClient.js.map +1 -1
  15. package/dist/admin/lib/symbolsClient.js +3 -4
  16. package/dist/admin/lib/symbolsClient.js.map +1 -1
  17. package/dist/admin/pages/PagesPanel.js +5 -2
  18. package/dist/admin/pages/PagesPanel.js.map +1 -1
  19. package/dist/admin/settings/MailSettings.js +2 -3
  20. package/dist/admin/settings/MailSettings.js.map +1 -1
  21. package/dist/admin/settings/MailTemplates.js +2 -3
  22. package/dist/admin/settings/MailTemplates.js.map +1 -1
  23. package/dist/admin/settings/SettingsPanel.js +250 -225
  24. package/dist/admin/settings/SettingsPanel.js.map +1 -1
  25. package/dist/admin/settings/SnapshotsCard.js +3 -4
  26. package/dist/admin/settings/SnapshotsCard.js.map +1 -1
  27. package/dist/admin/shop/lib.js +2 -3
  28. package/dist/admin/shop/lib.js.map +1 -1
  29. package/dist/admin/visual/VisualEditor.js +239 -103
  30. package/dist/admin/visual/VisualEditor.js.map +1 -1
  31. package/dist/admin.css +1 -1
  32. package/dist/auth/AuthProvider.d.ts +16 -0
  33. package/dist/auth/AuthProvider.js +58 -2
  34. package/dist/auth/AuthProvider.js.map +1 -1
  35. package/dist/auth/LoginForm.d.ts +13 -1
  36. package/dist/auth/LoginForm.js +153 -29
  37. package/dist/auth/LoginForm.js.map +1 -1
  38. package/dist/blocks/builtins/Agenda.js +6 -6
  39. package/dist/blocks/builtins/Agenda.js.map +1 -1
  40. package/dist/blocks/builtins/Banner.js +1 -1
  41. package/dist/blocks/builtins/Banner.js.map +1 -1
  42. package/dist/blocks/builtins/Breadcrumbs.js +5 -2
  43. package/dist/blocks/builtins/Breadcrumbs.js.map +1 -1
  44. package/dist/blocks/builtins/Cards.js +2 -2
  45. package/dist/blocks/builtins/Cards.js.map +1 -1
  46. package/dist/blocks/builtins/Contact.js +3 -3
  47. package/dist/blocks/builtins/Contact.js.map +1 -1
  48. package/dist/blocks/builtins/Cta.js +3 -3
  49. package/dist/blocks/builtins/Cta.js.map +1 -1
  50. package/dist/blocks/builtins/Downloads.js +5 -5
  51. package/dist/blocks/builtins/Downloads.js.map +1 -1
  52. package/dist/blocks/builtins/Elements.js +8 -5
  53. package/dist/blocks/builtins/Elements.js.map +1 -1
  54. package/dist/blocks/builtins/Hero.js +1 -1
  55. package/dist/blocks/builtins/Hero.js.map +1 -1
  56. package/dist/blocks/builtins/Jobs.js +4 -1
  57. package/dist/blocks/builtins/Jobs.js.map +1 -1
  58. package/dist/blocks/builtins/MapBlock.js +11 -11
  59. package/dist/blocks/builtins/MapBlock.js.map +1 -1
  60. package/dist/blocks/builtins/Newsletter.js +3 -2
  61. package/dist/blocks/builtins/Newsletter.js.map +1 -1
  62. package/dist/blocks/builtins/OpeningHours.js +2 -2
  63. package/dist/blocks/builtins/OpeningHours.js.map +1 -1
  64. package/dist/blocks/builtins/PdfBlock.js +2 -2
  65. package/dist/blocks/builtins/PdfBlock.js.map +1 -1
  66. package/dist/blocks/builtins/Portfolio.js +4 -4
  67. package/dist/blocks/builtins/Portfolio.js.map +1 -1
  68. package/dist/blocks/builtins/Pricing.js +3 -3
  69. package/dist/blocks/builtins/Pricing.js.map +1 -1
  70. package/dist/blocks/builtins/Sections.js +7 -7
  71. package/dist/blocks/builtins/Sections.js.map +1 -1
  72. package/dist/blocks/builtins/Team.js +3 -3
  73. package/dist/blocks/builtins/Team.js.map +1 -1
  74. package/dist/blocks/builtins/Widgets.js +28 -28
  75. package/dist/blocks/builtins/Widgets.js.map +1 -1
  76. package/dist/blocks/builtins/lib/submitForm.js +3 -4
  77. package/dist/blocks/builtins/lib/submitForm.js.map +1 -1
  78. package/dist/blocks/ui/index.d.ts +8 -1
  79. package/dist/blocks/ui/index.js +3 -2
  80. package/dist/blocks/ui/index.js.map +1 -1
  81. package/dist/components/EditBar.d.ts +9 -1
  82. package/dist/components/EditBar.js +6 -5
  83. package/dist/components/EditBar.js.map +1 -1
  84. package/dist/components/PageView.js +3 -5
  85. package/dist/components/PageView.js.map +1 -1
  86. package/dist/firebase/admin.js +4 -0
  87. package/dist/firebase/admin.js.map +1 -1
  88. package/dist/firebase/client.js +23 -5
  89. package/dist/firebase/client.js.map +1 -1
  90. package/dist/firebase/config.d.ts +0 -10
  91. package/dist/firebase/config.js +7 -1
  92. package/dist/firebase/config.js.map +1 -1
  93. package/dist/firebase/emulators.d.ts +9 -0
  94. package/dist/firebase/emulators.js +24 -0
  95. package/dist/firebase/emulators.js.map +1 -0
  96. package/dist/firebase/functionsClient.d.ts +18 -0
  97. package/dist/firebase/functionsClient.js +17 -0
  98. package/dist/firebase/functionsClient.js.map +1 -0
  99. package/dist/lock/DemoLock.js +19 -2
  100. package/dist/lock/DemoLock.js.map +1 -1
  101. package/dist/modules/shop/cartStore.js +2 -1
  102. package/dist/modules/shop/cartStore.js.map +1 -1
  103. package/dist/modules/shop/lib.js +9 -10
  104. package/dist/modules/shop/lib.js.map +1 -1
  105. package/dist/next/authRewrites.d.ts +40 -0
  106. package/dist/next/authRewrites.js +20 -0
  107. package/dist/next/authRewrites.js.map +1 -0
  108. package/dist/next/index.cjs +47 -0
  109. package/dist/next/index.d.ts +5 -0
  110. package/dist/next/index.js +5 -0
  111. package/dist/next/index.js.map +1 -0
  112. package/dist/server/index.d.ts +1 -1
  113. package/dist/server/index.js +2 -0
  114. package/dist/server/index.js.map +1 -1
  115. package/dist/server/pages.d.ts +11 -0
  116. package/dist/server/pages.js +47 -0
  117. package/dist/server/pages.js.map +1 -1
  118. package/dist/styles.css +130 -29
  119. package/dist/theme/color.d.ts +17 -0
  120. package/dist/theme/color.js +12 -0
  121. package/dist/theme/color.js.map +1 -0
  122. package/dist/version.js +1 -1
  123. package/package.json +7 -2
@@ -11,6 +11,12 @@ import { hasAuthHint, isFirebaseConfigured, setAuthHint } from "../firebase/conf
11
11
  import { roleForSite } from "./roles.js";
12
12
  import { DEMO_EVENT, isDemoLoggedIn, setDemoLoggedIn } from "../demo/demoStore.js";
13
13
  const AuthContext = createContext(null);
14
+ const LINK_EMAIL_KEY = "xicms.loginlink.email";
15
+ function lijktOpInloglink() {
16
+ if (typeof window === "undefined") return false;
17
+ const q = window.location.search;
18
+ return q.includes("mode=signIn") && q.includes("oobCode=");
19
+ }
14
20
  function AuthProvider({
15
21
  siteId,
16
22
  demo = false,
@@ -21,6 +27,20 @@ function AuthProvider({
21
27
  const [user, setUser] = useState(null);
22
28
  const [claims, setClaims] = useState(null);
23
29
  const [demoLoggedIn, setDemoLoggedInState] = useState(false);
30
+ const [linkVraagtEmail, setLinkVraagtEmail] = useState(false);
31
+ const [linkFout, setLinkFout] = useState(null);
32
+ function opruimenNaLinkLogin() {
33
+ try {
34
+ window.localStorage.removeItem(LINK_EMAIL_KEY);
35
+ } catch {
36
+ }
37
+ setLinkVraagtEmail(false);
38
+ setLinkFout(null);
39
+ try {
40
+ window.history.replaceState({}, "", window.location.pathname + window.location.hash);
41
+ } catch {
42
+ }
43
+ }
24
44
  useEffect(() => {
25
45
  if (!demo) return;
26
46
  const sync = () => setDemoLoggedInState(isDemoLoggedIn());
@@ -46,7 +66,7 @@ function AuthProvider({
46
66
  });
47
67
  }
48
68
  useEffect(() => {
49
- if (!configured || !hasAuthHint()) {
69
+ if (!configured || !hasAuthHint() && !lijktOpInloglink()) {
50
70
  setLoading(false);
51
71
  return;
52
72
  }
@@ -61,6 +81,24 @@ function AuthProvider({
61
81
  void sdk.getRedirectResult(auth).catch((e) => {
62
82
  console.error("[xicms] inloggen via redirect mislukt:", e);
63
83
  });
84
+ if (sdk.isSignInWithEmailLink(auth, window.location.href)) {
85
+ let bewaard = null;
86
+ try {
87
+ bewaard = window.localStorage.getItem(LINK_EMAIL_KEY);
88
+ } catch {
89
+ }
90
+ if (bewaard) {
91
+ try {
92
+ await sdk.signInWithEmailLink(auth, bewaard, window.location.href);
93
+ opruimenNaLinkLogin();
94
+ } catch (e) {
95
+ console.error("[xicms] inloggen via link mislukt:", e);
96
+ setLinkFout(e);
97
+ }
98
+ } else if (!afgebroken) {
99
+ setLinkVraagtEmail(true);
100
+ }
101
+ }
64
102
  unsub = sdk.onAuthStateChanged(auth, async (u) => {
65
103
  setUser(u);
66
104
  setAuthHint(Boolean(u));
@@ -100,6 +138,24 @@ function AuthProvider({
100
138
  await sdk.signInWithRedirect(auth, provider);
101
139
  }
102
140
  },
141
+ async sendLoginLink(email) {
142
+ const { auth, sdk } = await authSdk();
143
+ const adres = email.trim();
144
+ const url = `${window.location.origin}${window.location.pathname}`;
145
+ await sdk.sendSignInLinkToEmail(auth, adres, { url, handleCodeInApp: true });
146
+ try {
147
+ window.localStorage.setItem(LINK_EMAIL_KEY, adres);
148
+ } catch {
149
+ }
150
+ },
151
+ loginLinkNeedsEmail: linkVraagtEmail,
152
+ loginLinkError: linkFout,
153
+ async completeLoginLink(email) {
154
+ const { auth, sdk } = await authSdk();
155
+ await sdk.signInWithEmailLink(auth, email.trim(), window.location.href);
156
+ opruimenNaLinkLogin();
157
+ setAuthHint(true);
158
+ },
103
159
  async signOut() {
104
160
  if (demo) {
105
161
  setDemoLoggedIn(false);
@@ -118,7 +174,7 @@ function AuthProvider({
118
174
  setDemoLoggedIn(true);
119
175
  }
120
176
  };
121
- }, [configured, loading, user, claims, siteId, demo, demoLoggedIn]);
177
+ }, [configured, loading, user, claims, siteId, demo, demoLoggedIn, linkVraagtEmail, linkFout]);
122
178
  return /* @__PURE__ */ jsx(AuthContext.Provider, { value, children });
123
179
  }
124
180
  function useAuth() {
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/auth/AuthProvider.tsx"],"sourcesContent":["\"use client\";\n\nimport {\n createContext,\n useContext,\n useEffect,\n useMemo,\n useState,\n type ReactNode,\n} from \"react\";\nimport type { User } from \"firebase/auth\";\nimport { hasAuthHint, isFirebaseConfigured, setAuthHint } from \"../firebase/config\";\nimport { roleForSite, type Role, type XiClaims } from \"./roles\";\nimport { DEMO_EVENT, isDemoLoggedIn, setDemoLoggedIn } from \"../demo/demoStore\";\n\ninterface AuthState {\n /** Of Firebase überhaupt geconfigureerd is. */\n configured: boolean;\n /** Bezig met laden van de auth-status. */\n loading: boolean;\n user: User | null;\n claims: XiClaims | null;\n /** Effectieve rol op de huidige site (uit `siteId`). */\n role: Role | null;\n signIn: (email: string, password: string) => Promise<void>;\n /** Inloggen met Google (popup). */\n signInWithGoogle: () => Promise<void>;\n signOut: () => Promise<void>;\n /** Forceer het opnieuw inlezen van claims (na een rolwijziging). */\n refresh: () => Promise<void>;\n /** Demo-bewerkmodus actief (config.demoEditing) — pseudo-login, niets wordt opgeslagen. */\n demo: boolean;\n /** Pseudo-login voor de demo: geeft lokaal een siteadmin-rol zonder Firebase. */\n demoSignIn: () => void;\n}\n\nconst AuthContext = createContext<AuthState | null>(null);\n\nexport function AuthProvider({\n siteId,\n demo = false,\n children,\n}: {\n siteId: string;\n demo?: boolean;\n children: ReactNode;\n}) {\n const configured = isFirebaseConfigured();\n const [loading, setLoading] = useState(configured);\n const [user, setUser] = useState<User | null>(null);\n const [claims, setClaims] = useState<XiClaims | null>(null);\n const [demoLoggedIn, setDemoLoggedInState] = useState(false);\n\n // Demo-login uit de (sessie)opslag inlezen + meeluisteren met wijzigingen (login/logout/reset).\n useEffect(() => {\n if (!demo) return;\n const sync = () => setDemoLoggedInState(isDemoLoggedIn());\n sync();\n window.addEventListener(DEMO_EVENT, sync);\n return () => window.removeEventListener(DEMO_EVENT, sync);\n }, [demo]);\n\n /** Laadt de auth-SDK pas wanneer er iets mee moet gebeuren (inloggen, uitloggen, verversen). */\n async function authSdk() {\n const [client, sdk] = await Promise.all([import(\"../firebase/client\"), import(\"firebase/auth\")]);\n const auth = client.getFirebaseAuth();\n if (!auth) throw new Error(\"Firebase is niet geconfigureerd.\");\n return { auth, sdk };\n }\n\n async function loadClaims(u: User | null, forceRefresh = false) {\n if (!u) {\n setClaims(null);\n return;\n }\n // Standaard GEEN geforceerde token-refresh: dat is een netwerk-roundtrip op élke\n // auth-state-change (incl. eerste load van elke ingelogde editor). Alleen refresh() —\n // na een rolwijziging — forceert, want dan is het gecachte token per definitie verouderd.\n const token = await u.getIdTokenResult(forceRefresh);\n setClaims({\n superadmin: token.claims.superadmin === true,\n roles: (token.claims.roles as XiClaims[\"roles\"]) ?? {},\n });\n }\n\n useEffect(() => {\n // Zonder sessie-hint laden we de auth-SDK NIET: een anonieme bezoeker hoeft ~1 MB niet te\n // downloaden om te ontdekken dat hij niet is ingelogd. Zodra hij inlogt of het beheer opent\n // komt de SDK alsnog, en vindt onAuthStateChanged een bestaande sessie meteen terug.\n if (!configured || !hasAuthHint()) {\n setLoading(false);\n return;\n }\n let unsub: (() => void) | undefined;\n let afgebroken = false;\n void (async () => {\n const { auth, sdk } = await authSdk().catch(() => ({ auth: null, sdk: null }) as never);\n if (!auth || afgebroken) {\n setLoading(false);\n return;\n }\n // Terugkeer van een redirect-login afronden (zie signInWithGoogle). `onAuthStateChanged`\n // pikt de sessie zelf al op; deze aanroep is er om een FOUT bij terugkomst niet stil te\n // laten verdwijnen — anders sta je weer op het inlogscherm zonder te weten waarom.\n void sdk.getRedirectResult(auth).catch((e: unknown) => {\n console.error(\"[xicms] inloggen via redirect mislukt:\", e);\n });\n\n unsub = sdk.onAuthStateChanged(auth, async (u: User | null) => {\n setUser(u);\n setAuthHint(Boolean(u));\n await loadClaims(u);\n setLoading(false);\n });\n })();\n return () => {\n afgebroken = true;\n unsub?.();\n };\n }, [configured]);\n\n const value = useMemo<AuthState>(() => {\n // In de demo met actieve pseudo-login: doe alsof een siteadmin is ingelogd (lokaal, geen Firebase).\n const demoActiveLogin = demo && demoLoggedIn;\n const effectiveUser = demoActiveLogin\n ? ({ uid: \"demo\", email: \"demo@voorbeeld.nl\", displayName: \"Demo-beheerder\" } as unknown as User)\n : user;\n const effectiveClaims: XiClaims | null = demoActiveLogin ? { roles: { [siteId]: \"siteadmin\" } } : claims;\n\n return {\n configured,\n loading,\n user: effectiveUser,\n claims: effectiveClaims,\n role: roleForSite(effectiveClaims, siteId),\n async signIn(email, password) {\n const { auth, sdk } = await authSdk();\n await sdk.signInWithEmailAndPassword(auth, email, password);\n setAuthHint(true);\n },\n async signInWithGoogle() {\n const { auth, sdk } = await authSdk();\n const provider = new sdk.GoogleAuthProvider();\n try {\n await sdk.signInWithPopup(auth, provider);\n setAuthHint(true);\n } catch (e) {\n // TERUGVAL OP REDIRECT. `signInWithPopup` faalt in de praktijk vaker dan je denkt: een\n // popup-blokkade, een browser die de popup als tab opent (dan raakt Firebase het\n // venster kwijt), of strengere cookie-instellingen. Gemeten op localhost: de popup ging\n // dicht, de gebruiker bleef uitgelogd, en er kwam GEEN foutcode — waardoor het\n // inlogscherm alleen \"mislukt of geannuleerd\" kon melden. Doodlopend voor de klant.\n //\n // De redirect-variant heeft geen venster nodig en werkt daarom overal; het resultaat\n // wordt bij terugkomst opgepikt (zie de getRedirectResult-effect hieronder). Alleen bij\n // een échte annulering door de gebruiker gooien we door — dan opnieuw beginnen is\n // precies niet wat hij wil.\n const code = (e as { code?: string })?.code ?? \"\";\n if (code === \"auth/cancelled-popup-request\" || code === \"auth/popup-closed-by-user\") throw e;\n await sdk.signInWithRedirect(auth, provider);\n }\n },\n async signOut() {\n if (demo) {\n setDemoLoggedIn(false);\n return;\n }\n const { auth, sdk } = await authSdk();\n await sdk.signOut(auth);\n setAuthHint(false);\n },\n async refresh() {\n // Altijd de actuele Firebase-user (niet de gememoïseerde) — na een snelle\n // uitlog/inlog-wissel zou de closure anders claims van de vorige user verversen.\n // Force-refresh: na een rolwijziging is het gecachte token verouderd.\n const { auth } = await authSdk();\n await loadClaims(auth.currentUser ?? user, true);\n },\n demo,\n demoSignIn() {\n setDemoLoggedIn(true);\n },\n };\n }, [configured, loading, user, claims, siteId, demo, demoLoggedIn]);\n\n return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;\n}\n\nexport function useAuth(): AuthState {\n const ctx = useContext(AuthContext);\n if (!ctx) throw new Error(\"useAuth moet binnen <XiCMSProvider> worden gebruikt.\");\n return ctx;\n}\n"],"mappings":";AAyLS;AAvLT;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OAEK;AAEP,SAAS,aAAa,sBAAsB,mBAAmB;AAC/D,SAAS,mBAA6C;AACtD,SAAS,YAAY,gBAAgB,uBAAuB;AAuB5D,MAAM,cAAc,cAAgC,IAAI;AAEjD,SAAS,aAAa;AAAA,EAC3B;AAAA,EACA,OAAO;AAAA,EACP;AACF,GAIG;AACD,QAAM,aAAa,qBAAqB;AACxC,QAAM,CAAC,SAAS,UAAU,IAAI,SAAS,UAAU;AACjD,QAAM,CAAC,MAAM,OAAO,IAAI,SAAsB,IAAI;AAClD,QAAM,CAAC,QAAQ,SAAS,IAAI,SAA0B,IAAI;AAC1D,QAAM,CAAC,cAAc,oBAAoB,IAAI,SAAS,KAAK;AAG3D,YAAU,MAAM;AACd,QAAI,CAAC,KAAM;AACX,UAAM,OAAO,MAAM,qBAAqB,eAAe,CAAC;AACxD,SAAK;AACL,WAAO,iBAAiB,YAAY,IAAI;AACxC,WAAO,MAAM,OAAO,oBAAoB,YAAY,IAAI;AAAA,EAC1D,GAAG,CAAC,IAAI,CAAC;AAGT,iBAAe,UAAU;AACvB,UAAM,CAAC,QAAQ,GAAG,IAAI,MAAM,QAAQ,IAAI,CAAC,OAAO,oBAAoB,GAAG,OAAO,eAAe,CAAC,CAAC;AAC/F,UAAM,OAAO,OAAO,gBAAgB;AACpC,QAAI,CAAC,KAAM,OAAM,IAAI,MAAM,kCAAkC;AAC7D,WAAO,EAAE,MAAM,IAAI;AAAA,EACrB;AAEA,iBAAe,WAAW,GAAgB,eAAe,OAAO;AAC9D,QAAI,CAAC,GAAG;AACN,gBAAU,IAAI;AACd;AAAA,IACF;AAIA,UAAM,QAAQ,MAAM,EAAE,iBAAiB,YAAY;AACnD,cAAU;AAAA,MACR,YAAY,MAAM,OAAO,eAAe;AAAA,MACxC,OAAQ,MAAM,OAAO,SAA+B,CAAC;AAAA,IACvD,CAAC;AAAA,EACH;AAEA,YAAU,MAAM;AAId,QAAI,CAAC,cAAc,CAAC,YAAY,GAAG;AACjC,iBAAW,KAAK;AAChB;AAAA,IACF;AACA,QAAI;AACJ,QAAI,aAAa;AACjB,UAAM,YAAY;AAChB,YAAM,EAAE,MAAM,IAAI,IAAI,MAAM,QAAQ,EAAE,MAAM,OAAO,EAAE,MAAM,MAAM,KAAK,KAAK,EAAW;AACtF,UAAI,CAAC,QAAQ,YAAY;AACvB,mBAAW,KAAK;AAChB;AAAA,MACF;AAIA,WAAK,IAAI,kBAAkB,IAAI,EAAE,MAAM,CAAC,MAAe;AACrD,gBAAQ,MAAM,0CAA0C,CAAC;AAAA,MAC3D,CAAC;AAED,cAAQ,IAAI,mBAAmB,MAAM,OAAO,MAAmB;AAC7D,gBAAQ,CAAC;AACT,oBAAY,QAAQ,CAAC,CAAC;AACtB,cAAM,WAAW,CAAC;AAClB,mBAAW,KAAK;AAAA,MAClB,CAAC;AAAA,IACH,GAAG;AACH,WAAO,MAAM;AACX,mBAAa;AACb,cAAQ;AAAA,IACV;AAAA,EACF,GAAG,CAAC,UAAU,CAAC;AAEf,QAAM,QAAQ,QAAmB,MAAM;AAErC,UAAM,kBAAkB,QAAQ;AAChC,UAAM,gBAAgB,kBACjB,EAAE,KAAK,QAAQ,OAAO,qBAAqB,aAAa,iBAAiB,IAC1E;AACJ,UAAM,kBAAmC,kBAAkB,EAAE,OAAO,EAAE,CAAC,MAAM,GAAG,YAAY,EAAE,IAAI;AAElG,WAAO;AAAA,MACL;AAAA,MACA;AAAA,MACA,MAAM;AAAA,MACN,QAAQ;AAAA,MACR,MAAM,YAAY,iBAAiB,MAAM;AAAA,MACzC,MAAM,OAAO,OAAO,UAAU;AAC5B,cAAM,EAAE,MAAM,IAAI,IAAI,MAAM,QAAQ;AACpC,cAAM,IAAI,2BAA2B,MAAM,OAAO,QAAQ;AAC1D,oBAAY,IAAI;AAAA,MAClB;AAAA,MACA,MAAM,mBAAmB;AACvB,cAAM,EAAE,MAAM,IAAI,IAAI,MAAM,QAAQ;AACpC,cAAM,WAAW,IAAI,IAAI,mBAAmB;AAC5C,YAAI;AACF,gBAAM,IAAI,gBAAgB,MAAM,QAAQ;AACxC,sBAAY,IAAI;AAAA,QAClB,SAAS,GAAG;AAWV,gBAAM,OAAQ,GAAyB,QAAQ;AAC/C,cAAI,SAAS,kCAAkC,SAAS,4BAA6B,OAAM;AAC3F,gBAAM,IAAI,mBAAmB,MAAM,QAAQ;AAAA,QAC7C;AAAA,MACF;AAAA,MACA,MAAM,UAAU;AACd,YAAI,MAAM;AACR,0BAAgB,KAAK;AACrB;AAAA,QACF;AACA,cAAM,EAAE,MAAM,IAAI,IAAI,MAAM,QAAQ;AACpC,cAAM,IAAI,QAAQ,IAAI;AACtB,oBAAY,KAAK;AAAA,MACnB;AAAA,MACA,MAAM,UAAU;AAId,cAAM,EAAE,KAAK,IAAI,MAAM,QAAQ;AAC/B,cAAM,WAAW,KAAK,eAAe,MAAM,IAAI;AAAA,MACjD;AAAA,MACA;AAAA,MACA,aAAa;AACX,wBAAgB,IAAI;AAAA,MACtB;AAAA,IACF;AAAA,EACF,GAAG,CAAC,YAAY,SAAS,MAAM,QAAQ,QAAQ,MAAM,YAAY,CAAC;AAElE,SAAO,oBAAC,YAAY,UAAZ,EAAqB,OAAe,UAAS;AACvD;AAEO,SAAS,UAAqB;AACnC,QAAM,MAAM,WAAW,WAAW;AAClC,MAAI,CAAC,IAAK,OAAM,IAAI,MAAM,sDAAsD;AAChF,SAAO;AACT;","names":[]}
1
+ {"version":3,"sources":["../../src/auth/AuthProvider.tsx"],"sourcesContent":["\"use client\";\n\nimport {\n createContext,\n useContext,\n useEffect,\n useMemo,\n useState,\n type ReactNode,\n} from \"react\";\nimport type { User } from \"firebase/auth\";\nimport { hasAuthHint, isFirebaseConfigured, setAuthHint } from \"../firebase/config\";\nimport { roleForSite, type Role, type XiClaims } from \"./roles\";\nimport { DEMO_EVENT, isDemoLoggedIn, setDemoLoggedIn } from \"../demo/demoStore\";\n\ninterface AuthState {\n /** Of Firebase überhaupt geconfigureerd is. */\n configured: boolean;\n /** Bezig met laden van de auth-status. */\n loading: boolean;\n user: User | null;\n claims: XiClaims | null;\n /** Effectieve rol op de huidige site (uit `siteId`). */\n role: Role | null;\n signIn: (email: string, password: string) => Promise<void>;\n /** Inloggen met Google (popup). */\n signInWithGoogle: () => Promise<void>;\n /**\n * Stuur een inloglink naar dit adres (magic link). Geen wachtwoord, dus ook niets te vergeten —\n * en anders dan de Google-flow raakt dit geen enkel domein van een derde partij, waardoor het\n * werkt in browsers die opslag van derden blokkeren.\n */\n sendLoginLink: (email: string) => Promise<void>;\n /**\n * Er staat een geldige inloglink in de adresbalk, maar het bijbehorende adres is hier niet\n * bekend — de link is op een ander apparaat geopend dan waar hij is aangevraagd. Het\n * inlogscherm vraagt er dan om; zonder die vraag zou een geldige link doodlopen.\n */\n loginLinkNeedsEmail: boolean;\n /** De link inwisselen met een handmatig opgegeven adres (zie `loginLinkNeedsEmail`). */\n completeLoginLink: (email: string) => Promise<void>;\n /** Ging het inwisselen van de link mis (verlopen, al gebruikt, ander adres)? */\n loginLinkError: unknown;\n signOut: () => Promise<void>;\n /** Forceer het opnieuw inlezen van claims (na een rolwijziging). */\n refresh: () => Promise<void>;\n /** Demo-bewerkmodus actief (config.demoEditing) — pseudo-login, niets wordt opgeslagen. */\n demo: boolean;\n /** Pseudo-login voor de demo: geeft lokaal een siteadmin-rol zonder Firebase. */\n demoSignIn: () => void;\n}\n\nconst AuthContext = createContext<AuthState | null>(null);\n\n/**\n * Het adres waar de inloglink heen ging, bewaard tot de klik terugkomt.\n *\n * Firebase eist bij het afronden hetzelfde adres als waar de link naartoe is gestuurd — dat is de\n * hele beveiliging van deze methode: wie de link onderschept maar het adres niet weet, komt er\n * niet in. Staat het er niet (link geopend op een ander apparaat, of opslag geweigerd), dan vraagt\n * het inlogscherm er alsnog om in plaats van te falen.\n */\nconst LINK_EMAIL_KEY = \"xicms.loginlink.email\";\n\n/**\n * Is deze URL een inloglink? Bewust ZONDER de SDK: deze vraag bepaalt óf we de auth-SDK van ~1 MB\n * gaan laden, dus hem beantwoorden met de SDK zou de besparing meteen opeten. Firebase hangt de\n * link altijd op aan `mode=signIn` met een `oobCode`; dat is genoeg voor de voorselectie, waarna\n * `isSignInWithEmailLink` het echte oordeel velt.\n */\nfunction lijktOpInloglink(): boolean {\n if (typeof window === \"undefined\") return false;\n const q = window.location.search;\n return q.includes(\"mode=signIn\") && q.includes(\"oobCode=\");\n}\n\nexport function AuthProvider({\n siteId,\n demo = false,\n children,\n}: {\n siteId: string;\n demo?: boolean;\n children: ReactNode;\n}) {\n const configured = isFirebaseConfigured();\n const [loading, setLoading] = useState(configured);\n const [user, setUser] = useState<User | null>(null);\n const [claims, setClaims] = useState<XiClaims | null>(null);\n const [demoLoggedIn, setDemoLoggedInState] = useState(false);\n // Er staat een geldige inloglink in de adresbalk, maar we weten niet bij welk adres hij hoort.\n const [linkVraagtEmail, setLinkVraagtEmail] = useState(false);\n const [linkFout, setLinkFout] = useState<unknown>(null);\n\n /**\n * Na een geslaagde link-login: het bewaarde adres weg, en de `oobCode` uit de adresbalk.\n *\n * Die code is eenmalig, maar hij blijft wél in de geschiedenis en in gedeelde/gekopieerde URL's\n * staan. Hem laten staan levert bovendien een verwarrende herhaling op: één keer verversen en de\n * SDK probeert een verbruikte code opnieuw in te wisselen, met een foutmelding terwijl je gewoon\n * ingelogd bent. `replaceState` haalt hem weg zonder de pagina opnieuw te laden.\n */\n function opruimenNaLinkLogin(): void {\n try {\n window.localStorage.removeItem(LINK_EMAIL_KEY);\n } catch {\n /* niets bewaard, niets op te ruimen */\n }\n setLinkVraagtEmail(false);\n setLinkFout(null);\n try {\n window.history.replaceState({}, \"\", window.location.pathname + window.location.hash);\n } catch {\n /* geschiedenis niet bewerkbaar: onschuldig, de login is al gelukt */\n }\n }\n\n // Demo-login uit de (sessie)opslag inlezen + meeluisteren met wijzigingen (login/logout/reset).\n useEffect(() => {\n if (!demo) return;\n const sync = () => setDemoLoggedInState(isDemoLoggedIn());\n sync();\n window.addEventListener(DEMO_EVENT, sync);\n return () => window.removeEventListener(DEMO_EVENT, sync);\n }, [demo]);\n\n /** Laadt de auth-SDK pas wanneer er iets mee moet gebeuren (inloggen, uitloggen, verversen). */\n async function authSdk() {\n const [client, sdk] = await Promise.all([import(\"../firebase/client\"), import(\"firebase/auth\")]);\n const auth = client.getFirebaseAuth();\n if (!auth) throw new Error(\"Firebase is niet geconfigureerd.\");\n return { auth, sdk };\n }\n\n async function loadClaims(u: User | null, forceRefresh = false) {\n if (!u) {\n setClaims(null);\n return;\n }\n // Standaard GEEN geforceerde token-refresh: dat is een netwerk-roundtrip op élke\n // auth-state-change (incl. eerste load van elke ingelogde editor). Alleen refresh() —\n // na een rolwijziging — forceert, want dan is het gecachte token per definitie verouderd.\n const token = await u.getIdTokenResult(forceRefresh);\n setClaims({\n superadmin: token.claims.superadmin === true,\n roles: (token.claims.roles as XiClaims[\"roles\"]) ?? {},\n });\n }\n\n useEffect(() => {\n // Zonder sessie-hint laden we de auth-SDK NIET: een anonieme bezoeker hoeft ~1 MB niet te\n // downloaden om te ontdekken dat hij niet is ingelogd. Zodra hij inlogt of het beheer opent\n // komt de SDK alsnog, en vindt onAuthStateChanged een bestaande sessie meteen terug.\n // …behalve als dit een inloglink IS. Wie op de link in zijn mail klikt komt per definitie\n // zonder sessie-hint binnen — vaak zelfs in een andere browser dan waar hij hem aanvroeg.\n // Zonder deze uitzondering laadt de SDK niet, wordt de link nooit ingewisseld en belandt hij\n // op het inlogscherm met de geldige link ongebruikt in de adresbalk.\n if (!configured || (!hasAuthHint() && !lijktOpInloglink())) {\n setLoading(false);\n return;\n }\n let unsub: (() => void) | undefined;\n let afgebroken = false;\n void (async () => {\n const { auth, sdk } = await authSdk().catch(() => ({ auth: null, sdk: null }) as never);\n if (!auth || afgebroken) {\n setLoading(false);\n return;\n }\n // Terugkeer van een redirect-login afronden (zie signInWithGoogle). `onAuthStateChanged`\n // pikt de sessie zelf al op; deze aanroep is er om een FOUT bij terugkomst niet stil te\n // laten verdwijnen — anders sta je weer op het inlogscherm zonder te weten waarom.\n void sdk.getRedirectResult(auth).catch((e: unknown) => {\n console.error(\"[xicms] inloggen via redirect mislukt:\", e);\n });\n\n // Een aangeklikte inloglink meteen inwisselen. Het adres staat lokaal opgeslagen sinds het\n // versturen; ontbreekt het — link geopend op een ander apparaat, of opslag geweigerd — dan\n // laten we de link stáán en vraagt het inlogscherm erom. Weggooien zou de gebruiker terug\n // naar zijn mailbox sturen voor een link die hij al heeft.\n if (sdk.isSignInWithEmailLink(auth, window.location.href)) {\n let bewaard: string | null = null;\n try {\n bewaard = window.localStorage.getItem(LINK_EMAIL_KEY);\n } catch {\n /* opslag geweigerd → hieronder vragen we het adres gewoon */\n }\n if (bewaard) {\n try {\n await sdk.signInWithEmailLink(auth, bewaard, window.location.href);\n opruimenNaLinkLogin();\n } catch (e) {\n console.error(\"[xicms] inloggen via link mislukt:\", e);\n setLinkFout(e);\n }\n } else if (!afgebroken) {\n setLinkVraagtEmail(true);\n }\n }\n\n unsub = sdk.onAuthStateChanged(auth, async (u: User | null) => {\n setUser(u);\n setAuthHint(Boolean(u));\n await loadClaims(u);\n setLoading(false);\n });\n })();\n return () => {\n afgebroken = true;\n unsub?.();\n };\n }, [configured]);\n\n const value = useMemo<AuthState>(() => {\n // In de demo met actieve pseudo-login: doe alsof een siteadmin is ingelogd (lokaal, geen Firebase).\n const demoActiveLogin = demo && demoLoggedIn;\n const effectiveUser = demoActiveLogin\n ? ({ uid: \"demo\", email: \"demo@voorbeeld.nl\", displayName: \"Demo-beheerder\" } as unknown as User)\n : user;\n const effectiveClaims: XiClaims | null = demoActiveLogin ? { roles: { [siteId]: \"siteadmin\" } } : claims;\n\n return {\n configured,\n loading,\n user: effectiveUser,\n claims: effectiveClaims,\n role: roleForSite(effectiveClaims, siteId),\n async signIn(email, password) {\n const { auth, sdk } = await authSdk();\n await sdk.signInWithEmailAndPassword(auth, email, password);\n setAuthHint(true);\n },\n async signInWithGoogle() {\n const { auth, sdk } = await authSdk();\n const provider = new sdk.GoogleAuthProvider();\n try {\n await sdk.signInWithPopup(auth, provider);\n setAuthHint(true);\n } catch (e) {\n // TERUGVAL OP REDIRECT. `signInWithPopup` faalt in de praktijk vaker dan je denkt: een\n // popup-blokkade, een browser die de popup als tab opent (dan raakt Firebase het\n // venster kwijt), of strengere cookie-instellingen. Gemeten op localhost: de popup ging\n // dicht, de gebruiker bleef uitgelogd, en er kwam GEEN foutcode — waardoor het\n // inlogscherm alleen \"mislukt of geannuleerd\" kon melden. Doodlopend voor de klant.\n //\n // De redirect-variant heeft geen venster nodig en werkt daarom overal; het resultaat\n // wordt bij terugkomst opgepikt (zie de getRedirectResult-effect hieronder). Alleen bij\n // een échte annulering door de gebruiker gooien we door — dan opnieuw beginnen is\n // precies niet wat hij wil.\n const code = (e as { code?: string })?.code ?? \"\";\n if (code === \"auth/cancelled-popup-request\" || code === \"auth/popup-closed-by-user\") throw e;\n await sdk.signInWithRedirect(auth, provider);\n }\n },\n async sendLoginLink(email: string) {\n const { auth, sdk } = await authSdk();\n const adres = email.trim();\n // De link komt terug op DEZE pagina, zodat de gebruiker na het inloggen staat waar hij\n // begon (meestal /login of /beheer) in plaats van op de voorpagina.\n const url = `${window.location.origin}${window.location.pathname}`;\n await sdk.sendSignInLinkToEmail(auth, adres, { url, handleCodeInApp: true });\n try {\n window.localStorage.setItem(LINK_EMAIL_KEY, adres);\n } catch {\n // Opslag geweigerd (private mode): niet fataal. Bij terugkomst vraagt het inlogscherm\n // het adres alsnog — dezelfde route als bij een link die op een ander apparaat opengaat.\n }\n },\n loginLinkNeedsEmail: linkVraagtEmail,\n loginLinkError: linkFout,\n async completeLoginLink(email: string) {\n const { auth, sdk } = await authSdk();\n await sdk.signInWithEmailLink(auth, email.trim(), window.location.href);\n opruimenNaLinkLogin();\n setAuthHint(true);\n },\n async signOut() {\n if (demo) {\n setDemoLoggedIn(false);\n return;\n }\n const { auth, sdk } = await authSdk();\n await sdk.signOut(auth);\n setAuthHint(false);\n },\n async refresh() {\n // Altijd de actuele Firebase-user (niet de gememoïseerde) — na een snelle\n // uitlog/inlog-wissel zou de closure anders claims van de vorige user verversen.\n // Force-refresh: na een rolwijziging is het gecachte token verouderd.\n const { auth } = await authSdk();\n await loadClaims(auth.currentUser ?? user, true);\n },\n demo,\n demoSignIn() {\n setDemoLoggedIn(true);\n },\n };\n }, [configured, loading, user, claims, siteId, demo, demoLoggedIn, linkVraagtEmail, linkFout]);\n\n return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;\n}\n\nexport function useAuth(): AuthState {\n const ctx = useContext(AuthContext);\n if (!ctx) throw new Error(\"useAuth moet binnen <XiCMSProvider> worden gebruikt.\");\n return ctx;\n}\n"],"mappings":"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","names":[]}
@@ -1,4 +1,16 @@
1
- /** Eenvoudig inlogformulier voor beheerders/editors (e-mail/wachtwoord of Google). */
1
+ /**
2
+ * Inlogscherm voor beheerders/editors — wachtwoordloos van opzet.
3
+ *
4
+ * Twee wegen naar binnen: Google, of een inloglink per mail. Geen van beide kent een wachtwoord,
5
+ * en dus ook geen vergeten-wachtwoord-mailtjes. De inloglink is bovendien de betrouwbaarste van de
6
+ * twee: hij raakt geen enkel domein van een derde partij en werkt daarom ook in browsers die
7
+ * opslag van derden blokkeren — precies waar de Google-redirect op stukliep.
8
+ *
9
+ * Het wachtwoordformulier is niet weggegooid maar weggevouwen. Als beide wachtwoordloze wegen over
10
+ * e-mail lopen, is e-mail een enkelvoudig storingspunt: hapert de mailbezorging, dan komt niemand
11
+ * er meer in — ook de beheerder niet. Deze terugval blijft daarom bestaan voor wie hem nodig heeft,
12
+ * zonder hem aan iedereen op te dringen.
13
+ */
2
14
  export declare function LoginForm({ onSuccess }: {
3
15
  onSuccess?: () => void;
4
16
  }): import("react").JSX.Element;
@@ -1,6 +1,6 @@
1
1
  "use client";
2
2
  import { jsx, jsxs } from "react/jsx-runtime";
3
- import { useState } from "react";
3
+ import { useEffect, useState } from "react";
4
4
  import { useAuth } from "./AuthProvider.js";
5
5
  function authErrorMessage(e, fallback) {
6
6
  const code = e?.code ?? "";
@@ -14,7 +14,7 @@ function authErrorMessage(e, fallback) {
14
14
  case "auth/configuration-not-found":
15
15
  return "Firebase Authentication is niet (juist) ingericht voor dit project. Controleer de NEXT_PUBLIC_FIREBASE_*-env en of Authentication aan staat.";
16
16
  case "auth/operation-not-allowed":
17
- return "Deze inlogmethode staat uit. Zet 'm aan in Firebase \u2192 Authentication \u2192 Sign-in method (E-mail/wachtwoord of Google).";
17
+ return "Deze inlogmethode staat uit. Zet 'm aan in Firebase \u2192 Authentication \u2192 Sign-in method (Google of E-mailkoppeling).";
18
18
  case "auth/network-request-failed":
19
19
  return "Geen verbinding met Firebase. Controleer je netwerk en de NEXT_PUBLIC_FIREBASE_*-config.";
20
20
  case "auth/invalid-credential":
@@ -26,16 +26,35 @@ function authErrorMessage(e, fallback) {
26
26
  case "auth/popup-closed-by-user":
27
27
  case "auth/cancelled-popup-request":
28
28
  return "De Google-login-popup werd geblokkeerd of gesloten. Sta popups toe en probeer opnieuw.";
29
+ // ── Inloglink ──────────────────────────────────────────────────────────────────────────
30
+ case "auth/expired-action-code":
31
+ return "Deze inloglink is verlopen. Vraag hieronder een nieuwe aan.";
32
+ case "auth/invalid-action-code":
33
+ return "Deze inloglink is al gebruikt of niet meer geldig. Vraag hieronder een nieuwe aan.";
29
34
  default:
30
35
  return code ? `${fallback} (${code})` : fallback;
31
36
  }
32
37
  }
33
38
  function LoginForm({ onSuccess }) {
34
- const { signIn, signInWithGoogle, configured, user } = useAuth();
39
+ const {
40
+ signIn,
41
+ signInWithGoogle,
42
+ sendLoginLink,
43
+ loginLinkNeedsEmail,
44
+ loginLinkError,
45
+ completeLoginLink,
46
+ configured,
47
+ user
48
+ } = useAuth();
35
49
  const [email, setEmail] = useState("");
36
50
  const [password, setPassword] = useState("");
37
51
  const [error, setError] = useState(null);
52
+ const [linkVerstuurd, setLinkVerstuurd] = useState(false);
53
+ const [wachtwoordModus, setWachtwoordModus] = useState(false);
38
54
  const [busy, setBusy] = useState(false);
55
+ useEffect(() => {
56
+ if (loginLinkError) setError(authErrorMessage(loginLinkError, "Inloggen via de link mislukt."));
57
+ }, [loginLinkError]);
39
58
  async function handleGoogle() {
40
59
  setError(null);
41
60
  setBusy(true);
@@ -62,21 +81,125 @@ function LoginForm({ onSuccess }) {
62
81
  "."
63
82
  ] }) });
64
83
  }
65
- async function handleSubmit(e) {
84
+ async function handleEmail(e) {
85
+ e.preventDefault();
86
+ setError(null);
87
+ setBusy(true);
88
+ try {
89
+ if (loginLinkNeedsEmail) {
90
+ await completeLoginLink(email);
91
+ onSuccess?.();
92
+ } else {
93
+ await sendLoginLink(email);
94
+ setLinkVerstuurd(true);
95
+ }
96
+ } catch (err) {
97
+ setError(authErrorMessage(err, "Versturen van de inloglink mislukt."));
98
+ } finally {
99
+ setBusy(false);
100
+ }
101
+ }
102
+ async function handleWachtwoord(e) {
66
103
  e.preventDefault();
67
104
  setError(null);
68
105
  setBusy(true);
69
106
  try {
70
107
  await signIn(email, password);
71
108
  onSuccess?.();
72
- } catch (e2) {
73
- setError(authErrorMessage(e2, "Inloggen mislukt. Controleer je e-mailadres en wachtwoord."));
109
+ } catch (err) {
110
+ setError(authErrorMessage(err, "Inloggen mislukt. Controleer je e-mailadres en wachtwoord."));
74
111
  } finally {
75
112
  setBusy(false);
76
113
  }
77
114
  }
78
- return /* @__PURE__ */ jsxs("form", { onSubmit: handleSubmit, className: "xicms-card", style: { padding: "1.5rem", display: "grid", gap: "0.75rem", maxWidth: 360 }, children: [
115
+ const kaart = {
116
+ padding: "1.5rem",
117
+ display: "grid",
118
+ gap: "0.75rem",
119
+ maxWidth: 360
120
+ };
121
+ if (linkVerstuurd) {
122
+ return /* @__PURE__ */ jsxs("div", { className: "xicms-card", style: kaart, children: [
123
+ /* @__PURE__ */ jsx("h2", { style: { margin: 0 }, children: "Kijk in je mail" }),
124
+ /* @__PURE__ */ jsxs("p", { style: { margin: 0 }, children: [
125
+ "We stuurden een inloglink naar ",
126
+ /* @__PURE__ */ jsx("strong", { children: email }),
127
+ ". Klik erop en je bent binnen \u2014 geen wachtwoord nodig."
128
+ ] }),
129
+ /* @__PURE__ */ jsxs("p", { style: { margin: 0, fontSize: 13, opacity: 0.7 }, children: [
130
+ "Niets ontvangen? Kijk in je spam, of",
131
+ " ",
132
+ /* @__PURE__ */ jsx(
133
+ "button",
134
+ {
135
+ type: "button",
136
+ onClick: () => setLinkVerstuurd(false),
137
+ className: "xicms-btn xicms-btn--ghost",
138
+ style: { padding: 0, border: 0, background: "none", textDecoration: "underline", cursor: "pointer" },
139
+ children: "probeer een ander adres"
140
+ }
141
+ ),
142
+ "."
143
+ ] })
144
+ ] });
145
+ }
146
+ if (wachtwoordModus) {
147
+ return /* @__PURE__ */ jsxs("form", { onSubmit: handleWachtwoord, className: "xicms-card", style: kaart, children: [
148
+ /* @__PURE__ */ jsx("h2", { style: { margin: 0 }, children: "Inloggen met wachtwoord" }),
149
+ /* @__PURE__ */ jsxs("label", { style: { display: "grid", gap: "0.25rem" }, children: [
150
+ /* @__PURE__ */ jsx("span", { children: "E-mailadres" }),
151
+ /* @__PURE__ */ jsx(
152
+ "input",
153
+ {
154
+ type: "email",
155
+ required: true,
156
+ value: email,
157
+ onChange: (e) => setEmail(e.target.value),
158
+ autoComplete: "email",
159
+ className: "xicms-input"
160
+ }
161
+ )
162
+ ] }),
163
+ /* @__PURE__ */ jsxs("label", { style: { display: "grid", gap: "0.25rem" }, children: [
164
+ /* @__PURE__ */ jsx("span", { children: "Wachtwoord" }),
165
+ /* @__PURE__ */ jsx(
166
+ "input",
167
+ {
168
+ type: "password",
169
+ required: true,
170
+ value: password,
171
+ onChange: (e) => setPassword(e.target.value),
172
+ autoComplete: "current-password",
173
+ className: "xicms-input"
174
+ }
175
+ )
176
+ ] }),
177
+ error && /* @__PURE__ */ jsx("p", { style: { color: "#dc2626", margin: 0 }, children: error }),
178
+ /* @__PURE__ */ jsx("button", { type: "submit", disabled: busy, className: "xicms-btn", children: busy ? "Bezig\u2026" : "Inloggen" }),
179
+ /* @__PURE__ */ jsx(
180
+ "button",
181
+ {
182
+ type: "button",
183
+ onClick: () => {
184
+ setWachtwoordModus(false);
185
+ setError(null);
186
+ },
187
+ className: "xicms-btn xicms-btn--ghost",
188
+ children: "Liever zonder wachtwoord"
189
+ }
190
+ )
191
+ ] });
192
+ }
193
+ return /* @__PURE__ */ jsxs("form", { onSubmit: handleEmail, className: "xicms-card", style: kaart, children: [
79
194
  /* @__PURE__ */ jsx("h2", { style: { margin: 0 }, children: "Inloggen" }),
195
+ loginLinkNeedsEmail && /* @__PURE__ */ jsx("p", { style: { margin: 0, fontSize: 14 }, children: "Bevestig even bij welk e-mailadres deze inloglink hoort. Dat is wat de link beveiligt: wie hem onderschept maar het adres niet weet, komt er niet in." }),
196
+ /* @__PURE__ */ jsx("button", { type: "button", onClick: handleGoogle, disabled: busy, className: "xicms-btn", children: "Inloggen met Google" }),
197
+ /* @__PURE__ */ jsxs("div", { style: { display: "flex", alignItems: "center", gap: "0.5rem", opacity: 0.6, fontSize: 13 }, children: [
198
+ /* @__PURE__ */ jsx("span", { style: { flex: 1, height: 1, background: "currentColor" } }),
199
+ " of",
200
+ " ",
201
+ /* @__PURE__ */ jsx("span", { style: { flex: 1, height: 1, background: "currentColor" } })
202
+ ] }),
80
203
  /* @__PURE__ */ jsxs("label", { style: { display: "grid", gap: "0.25rem" }, children: [
81
204
  /* @__PURE__ */ jsx("span", { children: "E-mailadres" }),
82
205
  /* @__PURE__ */ jsx(
@@ -91,29 +214,30 @@ function LoginForm({ onSuccess }) {
91
214
  }
92
215
  )
93
216
  ] }),
94
- /* @__PURE__ */ jsxs("label", { style: { display: "grid", gap: "0.25rem" }, children: [
95
- /* @__PURE__ */ jsx("span", { children: "Wachtwoord" }),
96
- /* @__PURE__ */ jsx(
97
- "input",
98
- {
99
- type: "password",
100
- required: true,
101
- value: password,
102
- onChange: (e) => setPassword(e.target.value),
103
- autoComplete: "current-password",
104
- className: "xicms-input"
105
- }
106
- )
107
- ] }),
108
217
  error && /* @__PURE__ */ jsx("p", { style: { color: "#dc2626", margin: 0 }, children: error }),
109
- /* @__PURE__ */ jsx("button", { type: "submit", disabled: busy, className: "xicms-btn", children: busy ? "Bezig\u2026" : "Inloggen" }),
110
- /* @__PURE__ */ jsxs("div", { style: { display: "flex", alignItems: "center", gap: "0.5rem", opacity: 0.6, fontSize: 13 }, children: [
111
- /* @__PURE__ */ jsx("span", { style: { flex: 1, height: 1, background: "currentColor" } }),
112
- " of",
113
- " ",
114
- /* @__PURE__ */ jsx("span", { style: { flex: 1, height: 1, background: "currentColor" } })
115
- ] }),
116
- /* @__PURE__ */ jsx("button", { type: "button", onClick: handleGoogle, disabled: busy, className: "xicms-btn xicms-btn--ghost", children: "Inloggen met Google" })
218
+ /* @__PURE__ */ jsx("button", { type: "submit", disabled: busy, className: "xicms-btn xicms-btn--ghost", children: busy ? "Bezig\u2026" : loginLinkNeedsEmail ? "Inloggen afronden" : "Stuur mij een inloglink" }),
219
+ !loginLinkNeedsEmail && /* @__PURE__ */ jsx(
220
+ "button",
221
+ {
222
+ type: "button",
223
+ onClick: () => {
224
+ setWachtwoordModus(true);
225
+ setError(null);
226
+ },
227
+ style: {
228
+ justifySelf: "start",
229
+ padding: 0,
230
+ border: 0,
231
+ background: "none",
232
+ font: "inherit",
233
+ fontSize: 12,
234
+ opacity: 0.55,
235
+ textDecoration: "underline",
236
+ cursor: "pointer"
237
+ },
238
+ children: "Inloggen met wachtwoord"
239
+ }
240
+ )
117
241
  ] });
118
242
  }
119
243
  export {
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/auth/LoginForm.tsx"],"sourcesContent":["\"use client\";\n\nimport { useState, type FormEvent } from \"react\";\nimport { useAuth } from \"./AuthProvider\";\n\n/**\n * Vertaalt een Firebase-auth-fout naar een bruikbare melding. Maakt de meest voorkomende\n * deploy-/configuratieproblemen zelf-diagnosticerend i.p.v. een generieke \"inloggen mislukt\".\n */\nfunction authErrorMessage(e: unknown, fallback: string): string {\n const code = (e as { code?: string })?.code ?? \"\";\n switch (code) {\n case \"auth/unauthorized-domain\":\n return \"Dit domein mag nog niet inloggen. Voeg het toe in Firebase → Authentication → Settings → Authorized domains (bv. je Vercel-domein).\";\n case \"auth/invalid-api-key\":\n case \"auth/api-key-not-valid\":\n case \"auth/api-key-not-valid.-please-pass-a-valid-api-key.\":\n return \"De Firebase API-sleutel klopt niet. Controleer NEXT_PUBLIC_FIREBASE_API_KEY in de (Vercel-)omgevingsvariabelen.\";\n case \"auth/configuration-not-found\":\n return \"Firebase Authentication is niet (juist) ingericht voor dit project. Controleer de NEXT_PUBLIC_FIREBASE_*-env en of Authentication aan staat.\";\n case \"auth/operation-not-allowed\":\n return \"Deze inlogmethode staat uit. Zet 'm aan in Firebase → Authentication → Sign-in method (E-mail/wachtwoord of Google).\";\n case \"auth/network-request-failed\":\n return \"Geen verbinding met Firebase. Controleer je netwerk en de NEXT_PUBLIC_FIREBASE_*-config.\";\n case \"auth/invalid-credential\":\n case \"auth/wrong-password\":\n case \"auth/user-not-found\":\n case \"auth/invalid-email\":\n return \"Inloggen mislukt. Controleer je e-mailadres en wachtwoord.\";\n case \"auth/popup-blocked\":\n case \"auth/popup-closed-by-user\":\n case \"auth/cancelled-popup-request\":\n return \"De Google-login-popup werd geblokkeerd of gesloten. Sta popups toe en probeer opnieuw.\";\n default:\n return code ? `${fallback} (${code})` : fallback;\n }\n}\n\n/** Eenvoudig inlogformulier voor beheerders/editors (e-mail/wachtwoord of Google). */\nexport function LoginForm({ onSuccess }: { onSuccess?: () => void }) {\n const { signIn, signInWithGoogle, configured, user } = useAuth();\n const [email, setEmail] = useState(\"\");\n const [password, setPassword] = useState(\"\");\n const [error, setError] = useState<string | null>(null);\n const [busy, setBusy] = useState(false);\n\n async function handleGoogle() {\n setError(null);\n setBusy(true);\n try {\n await signInWithGoogle();\n onSuccess?.();\n } catch (e) {\n setError(authErrorMessage(e, \"Inloggen met Google mislukt of geannuleerd.\"));\n } finally {\n setBusy(false);\n }\n }\n\n if (!configured) {\n return (\n <div className=\"xicms-card\" style={{ padding: \"1.5rem\" }}>\n <p>\n Firebase is nog niet geconfigureerd. Vul de <code>NEXT_PUBLIC_FIREBASE_*</code>\n omgevingsvariabelen in om inloggen aan te zetten.\n </p>\n </div>\n );\n }\n\n if (user) {\n return (\n <div className=\"xicms-card\" style={{ padding: \"1.5rem\" }}>\n <p>Je bent ingelogd als <strong>{user.email}</strong>.</p>\n </div>\n );\n }\n\n async function handleSubmit(e: FormEvent) {\n e.preventDefault();\n setError(null);\n setBusy(true);\n try {\n await signIn(email, password);\n onSuccess?.();\n } catch (e) {\n setError(authErrorMessage(e, \"Inloggen mislukt. Controleer je e-mailadres en wachtwoord.\"));\n } finally {\n setBusy(false);\n }\n }\n\n return (\n <form onSubmit={handleSubmit} className=\"xicms-card\" style={{ padding: \"1.5rem\", display: \"grid\", gap: \"0.75rem\", maxWidth: 360 }}>\n <h2 style={{ margin: 0 }}>Inloggen</h2>\n <label style={{ display: \"grid\", gap: \"0.25rem\" }}>\n <span>E-mailadres</span>\n <input\n type=\"email\"\n required\n value={email}\n onChange={(e) => setEmail(e.target.value)}\n autoComplete=\"email\"\n className=\"xicms-input\"\n />\n </label>\n <label style={{ display: \"grid\", gap: \"0.25rem\" }}>\n <span>Wachtwoord</span>\n <input\n type=\"password\"\n required\n value={password}\n onChange={(e) => setPassword(e.target.value)}\n autoComplete=\"current-password\"\n className=\"xicms-input\"\n />\n </label>\n {error && <p style={{ color: \"#dc2626\", margin: 0 }}>{error}</p>}\n <button type=\"submit\" disabled={busy} className=\"xicms-btn\">\n {busy ? \"Bezig…\" : \"Inloggen\"}\n </button>\n <div style={{ display: \"flex\", alignItems: \"center\", gap: \"0.5rem\", opacity: 0.6, fontSize: 13 }}>\n <span style={{ flex: 1, height: 1, background: \"currentColor\" }} /> of{\" \"}\n <span style={{ flex: 1, height: 1, background: \"currentColor\" }} />\n </div>\n <button type=\"button\" onClick={handleGoogle} disabled={busy} className=\"xicms-btn xicms-btn--ghost\">\n Inloggen met Google\n </button>\n </form>\n );\n}\n"],"mappings":"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","names":["e"]}
1
+ {"version":3,"sources":["../../src/auth/LoginForm.tsx"],"sourcesContent":["\"use client\";\n\nimport { useEffect, useState, type CSSProperties, type FormEvent } from \"react\";\nimport { useAuth } from \"./AuthProvider\";\n\n/**\n * Vertaalt een Firebase-auth-fout naar een bruikbare melding. Maakt de meest voorkomende\n * deploy-/configuratieproblemen zelf-diagnosticerend i.p.v. een generieke \"inloggen mislukt\".\n */\nfunction authErrorMessage(e: unknown, fallback: string): string {\n const code = (e as { code?: string })?.code ?? \"\";\n switch (code) {\n case \"auth/unauthorized-domain\":\n return \"Dit domein mag nog niet inloggen. Voeg het toe in Firebase → Authentication → Settings → Authorized domains (bv. je Vercel-domein).\";\n case \"auth/invalid-api-key\":\n case \"auth/api-key-not-valid\":\n case \"auth/api-key-not-valid.-please-pass-a-valid-api-key.\":\n return \"De Firebase API-sleutel klopt niet. Controleer NEXT_PUBLIC_FIREBASE_API_KEY in de (Vercel-)omgevingsvariabelen.\";\n case \"auth/configuration-not-found\":\n return \"Firebase Authentication is niet (juist) ingericht voor dit project. Controleer de NEXT_PUBLIC_FIREBASE_*-env en of Authentication aan staat.\";\n case \"auth/operation-not-allowed\":\n return \"Deze inlogmethode staat uit. Zet 'm aan in Firebase → Authentication → Sign-in method (Google of E-mailkoppeling).\";\n case \"auth/network-request-failed\":\n return \"Geen verbinding met Firebase. Controleer je netwerk en de NEXT_PUBLIC_FIREBASE_*-config.\";\n case \"auth/invalid-credential\":\n case \"auth/wrong-password\":\n case \"auth/user-not-found\":\n case \"auth/invalid-email\":\n return \"Inloggen mislukt. Controleer je e-mailadres en wachtwoord.\";\n case \"auth/popup-blocked\":\n case \"auth/popup-closed-by-user\":\n case \"auth/cancelled-popup-request\":\n return \"De Google-login-popup werd geblokkeerd of gesloten. Sta popups toe en probeer opnieuw.\";\n // ── Inloglink ──────────────────────────────────────────────────────────────────────────\n case \"auth/expired-action-code\":\n return \"Deze inloglink is verlopen. Vraag hieronder een nieuwe aan.\";\n case \"auth/invalid-action-code\":\n return \"Deze inloglink is al gebruikt of niet meer geldig. Vraag hieronder een nieuwe aan.\";\n default:\n return code ? `${fallback} (${code})` : fallback;\n }\n}\n\n/**\n * Inlogscherm voor beheerders/editors — wachtwoordloos van opzet.\n *\n * Twee wegen naar binnen: Google, of een inloglink per mail. Geen van beide kent een wachtwoord,\n * en dus ook geen vergeten-wachtwoord-mailtjes. De inloglink is bovendien de betrouwbaarste van de\n * twee: hij raakt geen enkel domein van een derde partij en werkt daarom ook in browsers die\n * opslag van derden blokkeren — precies waar de Google-redirect op stukliep.\n *\n * Het wachtwoordformulier is niet weggegooid maar weggevouwen. Als beide wachtwoordloze wegen over\n * e-mail lopen, is e-mail een enkelvoudig storingspunt: hapert de mailbezorging, dan komt niemand\n * er meer in — ook de beheerder niet. Deze terugval blijft daarom bestaan voor wie hem nodig heeft,\n * zonder hem aan iedereen op te dringen.\n */\nexport function LoginForm({ onSuccess }: { onSuccess?: () => void }) {\n const {\n signIn,\n signInWithGoogle,\n sendLoginLink,\n loginLinkNeedsEmail,\n loginLinkError,\n completeLoginLink,\n configured,\n user,\n } = useAuth();\n const [email, setEmail] = useState(\"\");\n const [password, setPassword] = useState(\"\");\n const [error, setError] = useState<string | null>(null);\n const [linkVerstuurd, setLinkVerstuurd] = useState(false);\n const [wachtwoordModus, setWachtwoordModus] = useState(false);\n const [busy, setBusy] = useState(false);\n\n // Een mislukte link-inwisseling gebeurt in de provider (bij het laden), niet hier. Zonder deze\n // doorgifte zou de gebruiker een leeg inlogscherm zien terwijl er wél iets mis was.\n useEffect(() => {\n if (loginLinkError) setError(authErrorMessage(loginLinkError, \"Inloggen via de link mislukt.\"));\n }, [loginLinkError]);\n\n async function handleGoogle() {\n setError(null);\n setBusy(true);\n try {\n await signInWithGoogle();\n onSuccess?.();\n } catch (e) {\n setError(authErrorMessage(e, \"Inloggen met Google mislukt of geannuleerd.\"));\n } finally {\n setBusy(false);\n }\n }\n\n if (!configured) {\n return (\n <div className=\"xicms-card\" style={{ padding: \"1.5rem\" }}>\n <p>\n Firebase is nog niet geconfigureerd. Vul de <code>NEXT_PUBLIC_FIREBASE_*</code>\n omgevingsvariabelen in om inloggen aan te zetten.\n </p>\n </div>\n );\n }\n\n if (user) {\n return (\n <div className=\"xicms-card\" style={{ padding: \"1.5rem\" }}>\n <p>Je bent ingelogd als <strong>{user.email}</strong>.</p>\n </div>\n );\n }\n\n /** Verstuur de inloglink, of — bij terugkomst zonder bekend adres — wissel de link juist in. */\n async function handleEmail(e: FormEvent) {\n e.preventDefault();\n setError(null);\n setBusy(true);\n try {\n if (loginLinkNeedsEmail) {\n await completeLoginLink(email);\n onSuccess?.();\n } else {\n await sendLoginLink(email);\n setLinkVerstuurd(true);\n }\n } catch (err) {\n setError(authErrorMessage(err, \"Versturen van de inloglink mislukt.\"));\n } finally {\n setBusy(false);\n }\n }\n\n async function handleWachtwoord(e: FormEvent) {\n e.preventDefault();\n setError(null);\n setBusy(true);\n try {\n await signIn(email, password);\n onSuccess?.();\n } catch (err) {\n setError(authErrorMessage(err, \"Inloggen mislukt. Controleer je e-mailadres en wachtwoord.\"));\n } finally {\n setBusy(false);\n }\n }\n\n const kaart: CSSProperties = {\n padding: \"1.5rem\",\n display: \"grid\",\n gap: \"0.75rem\",\n maxWidth: 360,\n };\n\n // Bevestiging ná het versturen. Bewust geen formulier meer: de volgende stap staat in de mailbox,\n // en een nog zichtbaar veld nodigt uit tot nog een keer klikken (waarmee de eerste link vervalt).\n if (linkVerstuurd) {\n return (\n <div className=\"xicms-card\" style={kaart}>\n <h2 style={{ margin: 0 }}>Kijk in je mail</h2>\n <p style={{ margin: 0 }}>\n We stuurden een inloglink naar <strong>{email}</strong>. Klik erop en je bent binnen — geen\n wachtwoord nodig.\n </p>\n <p style={{ margin: 0, fontSize: 13, opacity: 0.7 }}>\n Niets ontvangen? Kijk in je spam, of{\" \"}\n <button\n type=\"button\"\n onClick={() => setLinkVerstuurd(false)}\n className=\"xicms-btn xicms-btn--ghost\"\n style={{ padding: 0, border: 0, background: \"none\", textDecoration: \"underline\", cursor: \"pointer\" }}\n >\n probeer een ander adres\n </button>\n .\n </p>\n </div>\n );\n }\n\n if (wachtwoordModus) {\n return (\n <form onSubmit={handleWachtwoord} className=\"xicms-card\" style={kaart}>\n <h2 style={{ margin: 0 }}>Inloggen met wachtwoord</h2>\n <label style={{ display: \"grid\", gap: \"0.25rem\" }}>\n <span>E-mailadres</span>\n <input\n type=\"email\"\n required\n value={email}\n onChange={(e) => setEmail(e.target.value)}\n autoComplete=\"email\"\n className=\"xicms-input\"\n />\n </label>\n <label style={{ display: \"grid\", gap: \"0.25rem\" }}>\n <span>Wachtwoord</span>\n <input\n type=\"password\"\n required\n value={password}\n onChange={(e) => setPassword(e.target.value)}\n autoComplete=\"current-password\"\n className=\"xicms-input\"\n />\n </label>\n {error && <p style={{ color: \"#dc2626\", margin: 0 }}>{error}</p>}\n <button type=\"submit\" disabled={busy} className=\"xicms-btn\">\n {busy ? \"Bezig…\" : \"Inloggen\"}\n </button>\n <button\n type=\"button\"\n onClick={() => {\n setWachtwoordModus(false);\n setError(null);\n }}\n className=\"xicms-btn xicms-btn--ghost\"\n >\n Liever zonder wachtwoord\n </button>\n </form>\n );\n }\n\n return (\n <form onSubmit={handleEmail} className=\"xicms-card\" style={kaart}>\n <h2 style={{ margin: 0 }}>Inloggen</h2>\n\n {loginLinkNeedsEmail && (\n <p style={{ margin: 0, fontSize: 14 }}>\n Bevestig even bij welk e-mailadres deze inloglink hoort. Dat is wat de link beveiligt: wie\n hem onderschept maar het adres niet weet, komt er niet in.\n </p>\n )}\n\n <button type=\"button\" onClick={handleGoogle} disabled={busy} className=\"xicms-btn\">\n Inloggen met Google\n </button>\n\n <div style={{ display: \"flex\", alignItems: \"center\", gap: \"0.5rem\", opacity: 0.6, fontSize: 13 }}>\n <span style={{ flex: 1, height: 1, background: \"currentColor\" }} /> of{\" \"}\n <span style={{ flex: 1, height: 1, background: \"currentColor\" }} />\n </div>\n\n <label style={{ display: \"grid\", gap: \"0.25rem\" }}>\n <span>E-mailadres</span>\n <input\n type=\"email\"\n required\n value={email}\n onChange={(e) => setEmail(e.target.value)}\n autoComplete=\"email\"\n className=\"xicms-input\"\n />\n </label>\n {error && <p style={{ color: \"#dc2626\", margin: 0 }}>{error}</p>}\n <button type=\"submit\" disabled={busy} className=\"xicms-btn xicms-btn--ghost\">\n {busy ? \"Bezig…\" : loginLinkNeedsEmail ? \"Inloggen afronden\" : \"Stuur mij een inloglink\"}\n </button>\n\n {/* De terugval: aanwezig voor wie hem nodig heeft, stil voor wie dat niet heeft. */}\n {!loginLinkNeedsEmail && (\n <button\n type=\"button\"\n onClick={() => {\n setWachtwoordModus(true);\n setError(null);\n }}\n style={{\n justifySelf: \"start\",\n padding: 0,\n border: 0,\n background: \"none\",\n font: \"inherit\",\n fontSize: 12,\n opacity: 0.55,\n textDecoration: \"underline\",\n cursor: \"pointer\",\n }}\n >\n Inloggen met wachtwoord\n </button>\n )}\n </form>\n );\n}\n"],"mappings":"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","names":[]}
@@ -64,22 +64,22 @@ function eventJsonLd(events) {
64
64
  };
65
65
  }
66
66
  function AgendaRender({ block }) {
67
- const events = selectAgendaEvents(block.events, { hidePast: block.hidePast });
67
+ const events = selectAgendaEvents((block.events ?? []).map((e, i) => ({ ...e, _i: i })), { hidePast: block.hidePast });
68
68
  if (events.length === 0) return null;
69
69
  return /* @__PURE__ */ jsxs("section", { className: "xicms-agenda", children: [
70
70
  block.heading && /* @__PURE__ */ jsx(Heading, { level: 2, "data-xicms-edit": "heading", children: block.heading }),
71
- /* @__PURE__ */ jsx("ul", { className: "xicms-agenda__list", children: events.map((e, i) => /* @__PURE__ */ jsxs("li", { className: "xicms-agenda__item", children: [
71
+ /* @__PURE__ */ jsx("ul", { className: "xicms-agenda__list", children: events.map((e) => /* @__PURE__ */ jsxs("li", { className: "xicms-agenda__item", children: [
72
72
  /* @__PURE__ */ jsx("time", { className: "xicms-agenda__date", dateTime: e.start, children: formatEventDate(e.start) }),
73
73
  /* @__PURE__ */ jsxs("div", { className: "xicms-agenda__body", children: [
74
- /* @__PURE__ */ jsx("strong", { className: "xicms-agenda__title", children: e.url ? /* @__PURE__ */ jsx("a", { href: e.url, children: e.title }) : e.title }),
75
- e.location && /* @__PURE__ */ jsx("span", { className: "xicms-agenda__location", children: e.location }),
76
- e.description && /* @__PURE__ */ jsx("p", { className: "xicms-agenda__desc", children: e.description }),
74
+ /* @__PURE__ */ jsx("strong", { className: "xicms-agenda__title", children: e.url ? /* @__PURE__ */ jsx("a", { href: e.url, "data-xicms-edit": `events.${e._i}.title`, children: e.title }) : /* @__PURE__ */ jsx("span", { "data-xicms-edit": `events.${e._i}.title`, children: e.title }) }),
75
+ e.location && /* @__PURE__ */ jsx("span", { className: "xicms-agenda__location", "data-xicms-edit": `events.${e._i}.location`, children: e.location }),
76
+ e.description && /* @__PURE__ */ jsx("p", { className: "xicms-agenda__desc", "data-xicms-edit": `events.${e._i}.description`, children: e.description }),
77
77
  (() => {
78
78
  const ics = icsDataUrl(e);
79
79
  return ics ? /* @__PURE__ */ jsx("a", { className: "xicms-agenda__ics", href: ics, download: `${(e.title || "evenement").toLowerCase().replace(/\s+/g, "-").slice(0, 40)}.ics`, children: "\u{1F4C5} Zet in agenda" }) : null;
80
80
  })()
81
81
  ] })
82
- ] }, i)) }),
82
+ ] }, e._i)) }),
83
83
  block.jsonLd && /* @__PURE__ */ jsx(
84
84
  "script",
85
85
  {
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/blocks/builtins/Agenda.tsx"],"sourcesContent":["import type { z } from \"zod\";\nimport { jsonLdHtml } from \"./jsonld\";\nimport { AgendaBlockSchema } from \"../../schema/blocks\";\nimport { defineBlock, type BlockRenderProps } from \"../registry\";\nimport { Heading } from \"../ui\";\nimport { icsDataUrl } from \"./lib/ics\";\n\ntype Agenda = z.infer<typeof AgendaBlockSchema>;\n\n/** Eén event zoals zowel het Agenda-blok als de Agenda-widget het kennen. */\nexport interface AgendaEvent {\n title: string;\n start: string;\n end?: string;\n location?: string;\n description?: string;\n url?: string;\n}\n\n/**\n * Parseert een event-datum als LOKALE tijd. Cruciaal voor datum-zonder-tijd: `new Date(\"2026-08-01\")`\n * is per spec UTC-middernacht, waardoor west-van-UTC-bezoekers \"31 juli\" te zien kregen (en de\n * server/client bij hydration konden verschillen). \"2026-08-01\" is hier gewoon 1 augustus, overal.\n */\nexport function parseEventDate(value: string): Date {\n const m = /^(\\d{4})-(\\d{2})-(\\d{2})$/.exec(value.trim());\n if (m) return new Date(Number(m[1]), Number(m[2]) - 1, Number(m[3]));\n return new Date(value);\n}\n\n/** Toont een datum leesbaar (nl-NL). Valt terug op de ruwe string bij ongeldige invoer. */\nexport function formatEventDate(value: string): string {\n if (!value) return \"\";\n const d = parseEventDate(value);\n if (Number.isNaN(d.getTime())) return value;\n const hasTime = value.includes(\"T\");\n return d.toLocaleDateString(\"nl-NL\", {\n weekday: \"short\",\n day: \"numeric\",\n month: \"long\",\n year: \"numeric\",\n ...(hasTime ? { hour: \"2-digit\", minute: \"2-digit\" } : {}),\n });\n}\n\n/**\n * Selecteert de te tonen events: filtert lege titels, optioneel verlopen events, sorteert\n * chronologisch (events zonder geldige datum achteraan, in invoervolgorde) en limiteert. Gedeeld\n * door het volwaardige Agenda-blok en de compacte Agenda-widget — zo \"haalt\" de widget alles\n * uit dezelfde logica als de volledige versie.\n */\nexport function selectAgendaEvents<T extends AgendaEvent>(\n events: T[] | undefined,\n opts: { hidePast?: boolean; limit?: number } = {},\n): T[] {\n let list = (events ?? []).filter((e) => e.title);\n if (opts.hidePast) {\n const today = new Date();\n today.setHours(0, 0, 0, 0);\n list = list.filter((e) => {\n const d = parseEventDate(e.end || e.start);\n return Number.isNaN(d.getTime()) || d >= today;\n });\n }\n list = [...list].sort((a, b) => {\n const ta = parseEventDate(a.start).getTime();\n const tb = parseEventDate(b.start).getTime();\n if (Number.isNaN(ta) && Number.isNaN(tb)) return 0;\n if (Number.isNaN(ta)) return 1;\n if (Number.isNaN(tb)) return -1;\n return ta - tb;\n });\n return typeof opts.limit === \"number\" ? list.slice(0, opts.limit) : list;\n}\n\nfunction eventJsonLd(events: Agenda[\"events\"]): Record<string, unknown> {\n return {\n \"@context\": \"https://schema.org\",\n \"@type\": \"ItemList\",\n // Google vereist `location` voor Event-rich-results: events zonder locatie renderen wel op\n // de pagina, maar blijven uit de JSON-LD (voorkomt gegarandeerde Search-Console-errors).\n itemListElement: events\n .filter((e) => e.title && e.start && e.location)\n .map((e, i) => ({\n \"@type\": \"ListItem\",\n position: i + 1,\n item: {\n \"@type\": \"Event\",\n name: e.title,\n startDate: e.start,\n ...(e.end ? { endDate: e.end } : {}),\n location: { \"@type\": \"Place\", name: e.location },\n ...(e.description ? { description: e.description } : {}),\n ...(e.url ? { url: e.url } : {}),\n },\n })),\n };\n}\n\n/** Agenda/evenementen. Server-safe (geen client-state); `hidePast` filtert verlopen events. */\nfunction AgendaRender({ block }: BlockRenderProps<Agenda>) {\n const events = selectAgendaEvents(block.events, { hidePast: block.hidePast });\n if (events.length === 0) return null;\n\n return (\n <section className=\"xicms-agenda\">\n {/* Alleen de kop is inline bewerkbaar: de items worden gefilterd/gesorteerd, dus hun\n render-index wijst niet betrouwbaar naar block.events[i]. */}\n {block.heading && <Heading level={2} data-xicms-edit=\"heading\">{block.heading}</Heading>}\n <ul className=\"xicms-agenda__list\">\n {events.map((e, i) => (\n <li key={i} className=\"xicms-agenda__item\">\n <time className=\"xicms-agenda__date\" dateTime={e.start}>\n {formatEventDate(e.start)}\n </time>\n <div className=\"xicms-agenda__body\">\n <strong className=\"xicms-agenda__title\">\n {e.url ? <a href={e.url}>{e.title}</a> : e.title}\n </strong>\n {e.location && <span className=\"xicms-agenda__location\">{e.location}</span>}\n {e.description && <p className=\"xicms-agenda__desc\">{e.description}</p>}\n {(() => {\n const ics = icsDataUrl(e);\n return ics ? (\n <a className=\"xicms-agenda__ics\" href={ics} download={`${(e.title || \"evenement\").toLowerCase().replace(/\\s+/g, \"-\").slice(0, 40)}.ics`}>\n 📅 Zet in agenda\n </a>\n ) : null;\n })()}\n </div>\n </li>\n ))}\n </ul>\n {block.jsonLd && (\n <script\n type=\"application/ld+json\"\n // eslint-disable-next-line react/no-danger\n dangerouslySetInnerHTML={{ __html: jsonLdHtml(eventJsonLd(events)) }}\n />\n )}\n </section>\n );\n}\n\nexport const agendaBlock = defineBlock({\n type: \"agenda\",\n schema: AgendaBlockSchema,\n Render: AgendaRender,\n label: \"Agenda\",\n icon: \"📅\",\n defaults: () => ({\n heading: \"Agenda\",\n events: [{ title: \"Evenement\", start: \"\" }],\n hidePast: true,\n jsonLd: true,\n }),\n});\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../../src/blocks/builtins/Agenda.tsx"],"sourcesContent":["import type { z } from \"zod\";\nimport { jsonLdHtml } from \"./jsonld\";\nimport { AgendaBlockSchema } from \"../../schema/blocks\";\nimport { defineBlock, type BlockRenderProps } from \"../registry\";\nimport { Heading } from \"../ui\";\nimport { icsDataUrl } from \"./lib/ics\";\n\ntype Agenda = z.infer<typeof AgendaBlockSchema>;\n\n/** Eén event zoals zowel het Agenda-blok als de Agenda-widget het kennen. */\nexport interface AgendaEvent {\n title: string;\n start: string;\n end?: string;\n location?: string;\n description?: string;\n url?: string;\n}\n\n/**\n * Parseert een event-datum als LOKALE tijd. Cruciaal voor datum-zonder-tijd: `new Date(\"2026-08-01\")`\n * is per spec UTC-middernacht, waardoor west-van-UTC-bezoekers \"31 juli\" te zien kregen (en de\n * server/client bij hydration konden verschillen). \"2026-08-01\" is hier gewoon 1 augustus, overal.\n */\nexport function parseEventDate(value: string): Date {\n const m = /^(\\d{4})-(\\d{2})-(\\d{2})$/.exec(value.trim());\n if (m) return new Date(Number(m[1]), Number(m[2]) - 1, Number(m[3]));\n return new Date(value);\n}\n\n/** Toont een datum leesbaar (nl-NL). Valt terug op de ruwe string bij ongeldige invoer. */\nexport function formatEventDate(value: string): string {\n if (!value) return \"\";\n const d = parseEventDate(value);\n if (Number.isNaN(d.getTime())) return value;\n const hasTime = value.includes(\"T\");\n return d.toLocaleDateString(\"nl-NL\", {\n weekday: \"short\",\n day: \"numeric\",\n month: \"long\",\n year: \"numeric\",\n ...(hasTime ? { hour: \"2-digit\", minute: \"2-digit\" } : {}),\n });\n}\n\n/**\n * Selecteert de te tonen events: filtert lege titels, optioneel verlopen events, sorteert\n * chronologisch (events zonder geldige datum achteraan, in invoervolgorde) en limiteert. Gedeeld\n * door het volwaardige Agenda-blok en de compacte Agenda-widget — zo \"haalt\" de widget alles\n * uit dezelfde logica als de volledige versie.\n */\nexport function selectAgendaEvents<T extends AgendaEvent>(\n events: T[] | undefined,\n opts: { hidePast?: boolean; limit?: number } = {},\n): T[] {\n let list = (events ?? []).filter((e) => e.title);\n if (opts.hidePast) {\n const today = new Date();\n today.setHours(0, 0, 0, 0);\n list = list.filter((e) => {\n const d = parseEventDate(e.end || e.start);\n return Number.isNaN(d.getTime()) || d >= today;\n });\n }\n list = [...list].sort((a, b) => {\n const ta = parseEventDate(a.start).getTime();\n const tb = parseEventDate(b.start).getTime();\n if (Number.isNaN(ta) && Number.isNaN(tb)) return 0;\n if (Number.isNaN(ta)) return 1;\n if (Number.isNaN(tb)) return -1;\n return ta - tb;\n });\n return typeof opts.limit === \"number\" ? list.slice(0, opts.limit) : list;\n}\n\nfunction eventJsonLd(events: Agenda[\"events\"]): Record<string, unknown> {\n return {\n \"@context\": \"https://schema.org\",\n \"@type\": \"ItemList\",\n // Google vereist `location` voor Event-rich-results: events zonder locatie renderen wel op\n // de pagina, maar blijven uit de JSON-LD (voorkomt gegarandeerde Search-Console-errors).\n itemListElement: events\n .filter((e) => e.title && e.start && e.location)\n .map((e, i) => ({\n \"@type\": \"ListItem\",\n position: i + 1,\n item: {\n \"@type\": \"Event\",\n name: e.title,\n startDate: e.start,\n ...(e.end ? { endDate: e.end } : {}),\n location: { \"@type\": \"Place\", name: e.location },\n ...(e.description ? { description: e.description } : {}),\n ...(e.url ? { url: e.url } : {}),\n },\n })),\n };\n}\n\n/** Agenda/evenementen. Server-safe (geen client-state); `hidePast` filtert verlopen events. */\nfunction AgendaRender({ block }: BlockRenderProps<Agenda>) {\n const events = selectAgendaEvents((block.events ?? []).map((e, i) => ({ ...e, _i: i })), { hidePast: block.hidePast });\n if (events.length === 0) return null;\n\n return (\n <section className=\"xicms-agenda\">\n {block.heading && <Heading level={2} data-xicms-edit=\"heading\">{block.heading}</Heading>}\n <ul className=\"xicms-agenda__list\">\n {events.map((e) => (\n <li key={e._i} className=\"xicms-agenda__item\">\n <time className=\"xicms-agenda__date\" dateTime={e.start}>\n {formatEventDate(e.start)}\n </time>\n <div className=\"xicms-agenda__body\">\n <strong className=\"xicms-agenda__title\">\n {e.url\n ? <a href={e.url} data-xicms-edit={`events.${e._i}.title`}>{e.title}</a>\n : <span data-xicms-edit={`events.${e._i}.title`}>{e.title}</span>}\n </strong>\n {e.location && <span className=\"xicms-agenda__location\" data-xicms-edit={`events.${e._i}.location`}>{e.location}</span>}\n {e.description && <p className=\"xicms-agenda__desc\" data-xicms-edit={`events.${e._i}.description`}>{e.description}</p>}\n {(() => {\n const ics = icsDataUrl(e);\n return ics ? (\n <a className=\"xicms-agenda__ics\" href={ics} download={`${(e.title || \"evenement\").toLowerCase().replace(/\\s+/g, \"-\").slice(0, 40)}.ics`}>\n 📅 Zet in agenda\n </a>\n ) : null;\n })()}\n </div>\n </li>\n ))}\n </ul>\n {block.jsonLd && (\n <script\n type=\"application/ld+json\"\n // eslint-disable-next-line react/no-danger\n dangerouslySetInnerHTML={{ __html: jsonLdHtml(eventJsonLd(events)) }}\n />\n )}\n </section>\n );\n}\n\nexport const agendaBlock = defineBlock({\n type: \"agenda\",\n schema: AgendaBlockSchema,\n Render: AgendaRender,\n label: \"Agenda\",\n icon: \"📅\",\n defaults: () => ({\n heading: \"Agenda\",\n events: [{ title: \"Evenement\", start: \"\" }],\n hidePast: true,\n jsonLd: true,\n }),\n});\n"],"mappings":"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","names":[]}
@@ -26,7 +26,7 @@ function BannerRender({ block, mode }) {
26
26
  return /* @__PURE__ */ jsxs("div", { className: "xicms-banner", "data-tone": block.tone, role: "region", "aria-label": "Aankondiging", children: [
27
27
  /* @__PURE__ */ jsxs("span", { children: [
28
28
  /* @__PURE__ */ jsx("span", { "data-xicms-edit": "text", children: block.text }),
29
- block.href && /* @__PURE__ */ jsx("a", { className: "xicms-banner__link", href: block.href, children: block.linkLabel ?? "Meer" })
29
+ block.href && /* @__PURE__ */ jsx("a", { className: "xicms-banner__link", href: block.href, "data-xicms-edit": "linkLabel", children: block.linkLabel ?? "Meer" })
30
30
  ] }),
31
31
  block.dismissible && /* @__PURE__ */ jsx("button", { type: "button", className: "xicms-banner__close", "aria-label": "Sluiten", onClick: dismiss, children: "\u2715" })
32
32
  ] });
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/blocks/builtins/Banner.tsx"],"sourcesContent":["\"use client\";\n\nimport { useEffect, useState } from \"react\";\nimport type { z } from \"zod\";\nimport { BannerBlockSchema } from \"../../schema/blocks\";\nimport { defineBlock, type BlockRenderProps } from \"../registry\";\n\ntype Banner = z.infer<typeof BannerBlockSchema>;\n\n/** Sleutel per blok (+ optionele versie): wegklikken onthouden we per banner-inhoud. */\nfunction storageKey(block: Banner): string {\n return `xicms-banner:${block._key}${block.dismissVersion ? `:${block.dismissVersion}` : \"\"}`;\n}\n\n/**\n * Aankondigingsbalk, optioneel sluitbaar. Wegklikken wordt in localStorage onthouden (per blok +\n * `dismissVersion`), zodat de balk niet bij elke paginaweergave terugkomt. In de editor-preview\n * tonen we hem altijd (anders kan een editor zijn eigen weggeklikte banner niet meer bewerken).\n */\nfunction BannerRender({ block, mode }: BlockRenderProps<Banner>) {\n const [closed, setClosed] = useState(false);\n\n // localStorage pas na mount lezen (SSR-veilig; voorkomt hydration-mismatch).\n useEffect(() => {\n if (mode === \"preview\" || !block.dismissible) return;\n try {\n if (window.localStorage.getItem(storageKey(block)) === \"1\") setClosed(true);\n } catch {\n /* opslag onbeschikbaar: banner gewoon tonen */\n }\n }, [block, mode]);\n\n const dismiss = () => {\n setClosed(true);\n try {\n window.localStorage.setItem(storageKey(block), \"1\");\n } catch {\n /* opslag geweigerd: sluiten geldt dan alleen voor deze weergave */\n }\n };\n\n if (!block.text || (closed && mode !== \"preview\")) return null;\n return (\n <div className=\"xicms-banner\" data-tone={block.tone} role=\"region\" aria-label=\"Aankondiging\">\n <span>\n <span data-xicms-edit=\"text\">{block.text}</span>\n {block.href && (\n <a className=\"xicms-banner__link\" href={block.href}>\n {block.linkLabel ?? \"Meer\"}\n </a>\n )}\n </span>\n {block.dismissible && (\n <button type=\"button\" className=\"xicms-banner__close\" aria-label=\"Sluiten\" onClick={dismiss}>\n ✕\n </button>\n )}\n </div>\n );\n}\n\nexport const bannerBlock = defineBlock({\n type: \"banner\",\n schema: BannerBlockSchema,\n Render: BannerRender,\n label: \"Aankondiging\",\n icon: \"📢\",\n defaults: () => ({ text: \"Nieuw: …\", tone: \"info\", dismissible: true }),\n});\n"],"mappings":";AA4CM,SACE,KADF;AA1CN,SAAS,WAAW,gBAAgB;AAEpC,SAAS,yBAAyB;AAClC,SAAS,mBAA0C;AAKnD,SAAS,WAAW,OAAuB;AACzC,SAAO,gBAAgB,MAAM,IAAI,GAAG,MAAM,iBAAiB,IAAI,MAAM,cAAc,KAAK,EAAE;AAC5F;AAOA,SAAS,aAAa,EAAE,OAAO,KAAK,GAA6B;AAC/D,QAAM,CAAC,QAAQ,SAAS,IAAI,SAAS,KAAK;AAG1C,YAAU,MAAM;AACd,QAAI,SAAS,aAAa,CAAC,MAAM,YAAa;AAC9C,QAAI;AACF,UAAI,OAAO,aAAa,QAAQ,WAAW,KAAK,CAAC,MAAM,IAAK,WAAU,IAAI;AAAA,IAC5E,QAAQ;AAAA,IAER;AAAA,EACF,GAAG,CAAC,OAAO,IAAI,CAAC;AAEhB,QAAM,UAAU,MAAM;AACpB,cAAU,IAAI;AACd,QAAI;AACF,aAAO,aAAa,QAAQ,WAAW,KAAK,GAAG,GAAG;AAAA,IACpD,QAAQ;AAAA,IAER;AAAA,EACF;AAEA,MAAI,CAAC,MAAM,QAAS,UAAU,SAAS,UAAY,QAAO;AAC1D,SACE,qBAAC,SAAI,WAAU,gBAAe,aAAW,MAAM,MAAM,MAAK,UAAS,cAAW,gBAC5E;AAAA,yBAAC,UACC;AAAA,0BAAC,UAAK,mBAAgB,QAAQ,gBAAM,MAAK;AAAA,MACxC,MAAM,QACL,oBAAC,OAAE,WAAU,sBAAqB,MAAM,MAAM,MAC3C,gBAAM,aAAa,QACtB;AAAA,OAEJ;AAAA,IACC,MAAM,eACL,oBAAC,YAAO,MAAK,UAAS,WAAU,uBAAsB,cAAW,WAAU,SAAS,SAAS,oBAE7F;AAAA,KAEJ;AAEJ;AAEO,MAAM,cAAc,YAAY;AAAA,EACrC,MAAM;AAAA,EACN,QAAQ;AAAA,EACR,QAAQ;AAAA,EACR,OAAO;AAAA,EACP,MAAM;AAAA,EACN,UAAU,OAAO,EAAE,MAAM,iBAAY,MAAM,QAAQ,aAAa,KAAK;AACvE,CAAC;","names":[]}
1
+ {"version":3,"sources":["../../../src/blocks/builtins/Banner.tsx"],"sourcesContent":["\"use client\";\n\nimport { useEffect, useState } from \"react\";\nimport type { z } from \"zod\";\nimport { BannerBlockSchema } from \"../../schema/blocks\";\nimport { defineBlock, type BlockRenderProps } from \"../registry\";\n\ntype Banner = z.infer<typeof BannerBlockSchema>;\n\n/** Sleutel per blok (+ optionele versie): wegklikken onthouden we per banner-inhoud. */\nfunction storageKey(block: Banner): string {\n return `xicms-banner:${block._key}${block.dismissVersion ? `:${block.dismissVersion}` : \"\"}`;\n}\n\n/**\n * Aankondigingsbalk, optioneel sluitbaar. Wegklikken wordt in localStorage onthouden (per blok +\n * `dismissVersion`), zodat de balk niet bij elke paginaweergave terugkomt. In de editor-preview\n * tonen we hem altijd (anders kan een editor zijn eigen weggeklikte banner niet meer bewerken).\n */\nfunction BannerRender({ block, mode }: BlockRenderProps<Banner>) {\n const [closed, setClosed] = useState(false);\n\n // localStorage pas na mount lezen (SSR-veilig; voorkomt hydration-mismatch).\n useEffect(() => {\n if (mode === \"preview\" || !block.dismissible) return;\n try {\n if (window.localStorage.getItem(storageKey(block)) === \"1\") setClosed(true);\n } catch {\n /* opslag onbeschikbaar: banner gewoon tonen */\n }\n }, [block, mode]);\n\n const dismiss = () => {\n setClosed(true);\n try {\n window.localStorage.setItem(storageKey(block), \"1\");\n } catch {\n /* opslag geweigerd: sluiten geldt dan alleen voor deze weergave */\n }\n };\n\n if (!block.text || (closed && mode !== \"preview\")) return null;\n return (\n <div className=\"xicms-banner\" data-tone={block.tone} role=\"region\" aria-label=\"Aankondiging\">\n <span>\n <span data-xicms-edit=\"text\">{block.text}</span>\n {block.href && (\n <a className=\"xicms-banner__link\" href={block.href} data-xicms-edit=\"linkLabel\">\n {block.linkLabel ?? \"Meer\"}\n </a>\n )}\n </span>\n {block.dismissible && (\n <button type=\"button\" className=\"xicms-banner__close\" aria-label=\"Sluiten\" onClick={dismiss}>\n ✕\n </button>\n )}\n </div>\n );\n}\n\nexport const bannerBlock = defineBlock({\n type: \"banner\",\n schema: BannerBlockSchema,\n Render: BannerRender,\n label: \"Aankondiging\",\n icon: \"📢\",\n defaults: () => ({ text: \"Nieuw: …\", tone: \"info\", dismissible: true }),\n});\n"],"mappings":";AA4CM,SACE,KADF;AA1CN,SAAS,WAAW,gBAAgB;AAEpC,SAAS,yBAAyB;AAClC,SAAS,mBAA0C;AAKnD,SAAS,WAAW,OAAuB;AACzC,SAAO,gBAAgB,MAAM,IAAI,GAAG,MAAM,iBAAiB,IAAI,MAAM,cAAc,KAAK,EAAE;AAC5F;AAOA,SAAS,aAAa,EAAE,OAAO,KAAK,GAA6B;AAC/D,QAAM,CAAC,QAAQ,SAAS,IAAI,SAAS,KAAK;AAG1C,YAAU,MAAM;AACd,QAAI,SAAS,aAAa,CAAC,MAAM,YAAa;AAC9C,QAAI;AACF,UAAI,OAAO,aAAa,QAAQ,WAAW,KAAK,CAAC,MAAM,IAAK,WAAU,IAAI;AAAA,IAC5E,QAAQ;AAAA,IAER;AAAA,EACF,GAAG,CAAC,OAAO,IAAI,CAAC;AAEhB,QAAM,UAAU,MAAM;AACpB,cAAU,IAAI;AACd,QAAI;AACF,aAAO,aAAa,QAAQ,WAAW,KAAK,GAAG,GAAG;AAAA,IACpD,QAAQ;AAAA,IAER;AAAA,EACF;AAEA,MAAI,CAAC,MAAM,QAAS,UAAU,SAAS,UAAY,QAAO;AAC1D,SACE,qBAAC,SAAI,WAAU,gBAAe,aAAW,MAAM,MAAM,MAAK,UAAS,cAAW,gBAC5E;AAAA,yBAAC,UACC;AAAA,0BAAC,UAAK,mBAAgB,QAAQ,gBAAM,MAAK;AAAA,MACxC,MAAM,QACL,oBAAC,OAAE,WAAU,sBAAqB,MAAM,MAAM,MAAM,mBAAgB,aACjE,gBAAM,aAAa,QACtB;AAAA,OAEJ;AAAA,IACC,MAAM,eACL,oBAAC,YAAO,MAAK,UAAS,WAAU,uBAAsB,cAAW,WAAU,SAAS,SAAS,oBAE7F;AAAA,KAEJ;AAEJ;AAEO,MAAM,cAAc,YAAY;AAAA,EACrC,MAAM;AAAA,EACN,QAAQ;AAAA,EACR,QAAQ;AAAA,EACR,OAAO;AAAA,EACP,MAAM;AAAA,EACN,UAAU,OAAO,EAAE,MAAM,iBAAY,MAAM,QAAQ,aAAa,KAAK;AACvE,CAAC;","names":[]}
@@ -41,14 +41,17 @@ function BreadcrumbsRender({ block, mode }) {
41
41
  }, []);
42
42
  if (mode === "preview" && (path === null || path === "/")) {
43
43
  return /* @__PURE__ */ jsxs("nav", { className: "xicms-breadcrumbs", "aria-label": "Kruimelpad", children: [
44
- block.homeLabel,
44
+ /* @__PURE__ */ jsx("span", { "data-xicms-edit": "homeLabel", children: block.homeLabel }),
45
45
  " / Voorbeeldpagina"
46
46
  ] });
47
47
  }
48
48
  if (path === null || path === "/") return null;
49
49
  const crumbs = crumbsFromPath(path, block.homeLabel);
50
50
  return /* @__PURE__ */ jsxs("nav", { className: "xicms-breadcrumbs", "aria-label": "Kruimelpad", children: [
51
- /* @__PURE__ */ jsx("ol", { className: "xicms-breadcrumbs__list", children: crumbs.map((c, i) => /* @__PURE__ */ jsx("li", { className: "xicms-breadcrumbs__item", children: i < crumbs.length - 1 ? /* @__PURE__ */ jsx(LinkC, { href: c.href, children: c.label }) : /* @__PURE__ */ jsx("span", { "aria-current": "page", children: c.label }) }, c.href)) }),
51
+ /* @__PURE__ */ jsx("ol", { className: "xicms-breadcrumbs__list", children: crumbs.map((c, i) => /* @__PURE__ */ jsx("li", { className: "xicms-breadcrumbs__item", children: i < crumbs.length - 1 ? (
52
+ // De eerste kruimel ís `homeLabel`; de rest komt uit het pad en is dus geen blokveld.
53
+ /* @__PURE__ */ jsx(LinkC, { href: c.href, children: i === 0 ? /* @__PURE__ */ jsx("span", { "data-xicms-edit": "homeLabel", children: c.label }) : c.label })
54
+ ) : /* @__PURE__ */ jsx("span", { "aria-current": "page", children: c.label }) }, c.href)) }),
52
55
  block.jsonLd && /* @__PURE__ */ jsx(
53
56
  "script",
54
57
  {