@xiwebdesign/xicms-core 7.6.1 → 7.8.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 (36) hide show
  1. package/dist/admin/AdminApp.js +3 -1
  2. package/dist/admin/AdminApp.js.map +1 -1
  3. package/dist/admin/lib/pagesClient.d.ts +13 -0
  4. package/dist/admin/lib/pagesClient.js +13 -0
  5. package/dist/admin/lib/pagesClient.js.map +1 -1
  6. package/dist/admin/shop/ProductPaneel.d.ts +15 -0
  7. package/dist/admin/shop/ProductPaneel.js +55 -0
  8. package/dist/admin/shop/ProductPaneel.js.map +1 -0
  9. package/dist/admin/shop/ProductsTab.d.ts +27 -0
  10. package/dist/admin/shop/ProductsTab.js +3 -2
  11. package/dist/admin/shop/ProductsTab.js.map +1 -1
  12. package/dist/admin/shop/productClient.d.ts +32 -0
  13. package/dist/admin/shop/productClient.js +31 -0
  14. package/dist/admin/shop/productClient.js.map +1 -0
  15. package/dist/admin/visual/VisualEditor.js +296 -43
  16. package/dist/admin/visual/VisualEditor.js.map +1 -1
  17. package/dist/admin/visual/wijzigingen.d.ts +46 -0
  18. package/dist/admin/visual/wijzigingen.js +78 -0
  19. package/dist/admin/visual/wijzigingen.js.map +1 -0
  20. package/dist/auth/AuthProvider.js +22 -3
  21. package/dist/auth/AuthProvider.js.map +1 -1
  22. package/dist/auth/LoginForm.js +7 -2
  23. package/dist/auth/LoginForm.js.map +1 -1
  24. package/dist/components/EditBar.d.ts +30 -0
  25. package/dist/components/EditBar.js +32 -6
  26. package/dist/components/EditBar.js.map +1 -1
  27. package/dist/editor/EditorProvider.d.ts +5 -0
  28. package/dist/editor/EditorProvider.js +28 -11
  29. package/dist/editor/EditorProvider.js.map +1 -1
  30. package/dist/modules/shop/blocks/ProductDetail.js +3 -3
  31. package/dist/modules/shop/blocks/ProductDetail.js.map +1 -1
  32. package/dist/modules/shop/blocks/ProductGrid.js +5 -2
  33. package/dist/modules/shop/blocks/ProductGrid.js.map +1 -1
  34. package/dist/styles.css +163 -46
  35. package/dist/version.js +1 -1
  36. package/package.json +1 -1
@@ -0,0 +1,78 @@
1
+ const NEGEER = /* @__PURE__ */ new Set(["_key", "_type"]);
2
+ const GEEN_WIJZIGINGEN = {
3
+ blokken: /* @__PURE__ */ new Set(),
4
+ velden: /* @__PURE__ */ new Map(),
5
+ verwijderd: 0,
6
+ iets: false
7
+ };
8
+ function loop(nieuw, oud, pad, uit) {
9
+ if (Array.isArray(nieuw) || Array.isArray(oud)) {
10
+ const a = Array.isArray(nieuw) ? nieuw : [];
11
+ const b = Array.isArray(oud) ? oud : [];
12
+ for (let i = 0; i < Math.max(a.length, b.length); i++) {
13
+ loop(a[i], b[i], pad ? `${pad}.${i}` : String(i), uit);
14
+ }
15
+ return;
16
+ }
17
+ const nObj = nieuw && typeof nieuw === "object";
18
+ const oObj = oud && typeof oud === "object";
19
+ if (nObj || oObj) {
20
+ const sleutels = /* @__PURE__ */ new Set([
21
+ ...Object.keys(nObj ? nieuw : {}),
22
+ ...Object.keys(oObj ? oud : {})
23
+ ]);
24
+ for (const k of sleutels) {
25
+ if (!pad && NEGEER.has(k)) continue;
26
+ const n = nObj ? nieuw[k] : void 0;
27
+ const o = oObj ? oud[k] : void 0;
28
+ loop(n, o, pad ? `${pad}.${k}` : k, uit);
29
+ }
30
+ return;
31
+ }
32
+ const links = nieuw === void 0 || nieuw === null ? "" : String(nieuw);
33
+ const rechts = oud === void 0 || oud === null ? "" : String(oud);
34
+ if (links !== rechts && pad) uit.add(pad);
35
+ }
36
+ function bepaalWijzigingen(werk, live) {
37
+ const w = werk ?? [];
38
+ if (!live) {
39
+ const blokken2 = new Set(w.map((b) => String(b._key ?? "")).filter(Boolean));
40
+ return { blokken: blokken2, velden: /* @__PURE__ */ new Map(), verwijderd: 0, iets: blokken2.size > 0 };
41
+ }
42
+ const opKey = new Map(live.map((b, i) => [String(b._key ?? ""), { blok: b, index: i }]));
43
+ const blokken = /* @__PURE__ */ new Set();
44
+ const velden = /* @__PURE__ */ new Map();
45
+ w.forEach((blok, index) => {
46
+ const key = String(blok._key ?? "");
47
+ if (!key) return;
48
+ const eerder = opKey.get(key);
49
+ if (!eerder) {
50
+ blokken.add(key);
51
+ return;
52
+ }
53
+ if (eerder.index !== index) blokken.add(key);
54
+ const verschillen = /* @__PURE__ */ new Set();
55
+ loop(blok, eerder.blok, "", verschillen);
56
+ if (verschillen.size > 0) velden.set(key, verschillen);
57
+ });
58
+ const inWerk = new Set(w.map((b) => String(b._key ?? "")));
59
+ const verwijderd = live.filter((b) => !inWerk.has(String(b._key ?? ""))).length;
60
+ return {
61
+ blokken,
62
+ velden,
63
+ verwijderd,
64
+ iets: blokken.size > 0 || velden.size > 0 || verwijderd > 0
65
+ };
66
+ }
67
+ function aantalWijzigingen(w) {
68
+ let n = w.blokken.size + w.verwijderd;
69
+ for (const velden of w.velden.values()) n += velden.size;
70
+ if (n === 0) return "niets gewijzigd";
71
+ return n === 1 ? "1 wijziging" : `${n} wijzigingen`;
72
+ }
73
+ export {
74
+ GEEN_WIJZIGINGEN,
75
+ aantalWijzigingen,
76
+ bepaalWijzigingen
77
+ };
78
+ //# sourceMappingURL=wijzigingen.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../src/admin/visual/wijzigingen.ts"],"sourcesContent":["/**\n * Wat is er veranderd sinds de laatste publicatie?\n *\n * De editor wist tot 7.8 alleen of de WERKVERSIE nog opgeslagen moest worden. Dat is een andere\n * vraag dan wat een bezoeker ziet: je kunt iets wijzigen, het wordt netjes automatisch opgeslagen,\n * en er staat nog steeds niets live. \"Opgeslagen\" en \"gepubliceerd\" zijn twee toestanden en die\n * werden op één hoop gegooid — vandaar een publiceerknop die altijd even hard riep en een\n * waarschuwing die afging op het verkeerde moment.\n *\n * Dit bestand vergelijkt de werkversie met de gepubliceerde kopie en levert twee dingen:\n * welke BLOKKEN nieuw of verplaatst zijn, en welke VELDEN een andere waarde hebben. Daarmee kan\n * de pagina laten zien wat er nog niet live staat (rood/onderstreept) en kan de knop rechtsonder\n * eerlijk zeggen of er iets te publiceren valt.\n *\n * De veldpaden zijn dezelfde puntnotatie die de blokken in `data-xicms-edit` zetten\n * (`title`, `items.2.aHtml`), zodat de markering rechtstreeks op de DOM-haken past en er geen\n * tweede, uit de pas lopende vertaalslag nodig is.\n */\n\ntype Blok = Record<string, unknown>;\n\n/** Sleutels die over de administratie gaan, niet over de inhoud — die maken een pagina niet \"gewijzigd\". */\nconst NEGEER = new Set([\"_key\", \"_type\"]);\n\nexport interface Wijzigingen {\n /** Blok-keys die nieuw zijn of van plek zijn veranderd. */\n blokken: Set<string>;\n /** Blok-key → veldpaden met een andere waarde dan wat er live staat. */\n velden: Map<string, Set<string>>;\n /** Blok-keys die in de werkversie zijn VERWIJDERD (alleen voor de telling). */\n verwijderd: number;\n /** Is er hoe dan ook iets te publiceren? */\n iets: boolean;\n}\n\nexport const GEEN_WIJZIGINGEN: Wijzigingen = {\n blokken: new Set(),\n velden: new Map(),\n verwijderd: 0,\n iets: false,\n};\n\n/**\n * Loop een waarde af en noteer elk pad waarvan de inhoud verschilt.\n *\n * Arrays worden op index vergeleken, want dat is precies hoe de blokken hun paden opbouwen\n * (`items.2.q`). Een ingevoegd item in het midden markeert daardoor alles erna als gewijzigd —\n * technisch waar (die posities tonen nu andere tekst) en in de praktijk het eerlijkste signaal:\n * er is aan die lijst gesleuteld en dat staat nog niet live.\n */\nfunction loop(nieuw: unknown, oud: unknown, pad: string, uit: Set<string>): void {\n if (Array.isArray(nieuw) || Array.isArray(oud)) {\n const a = Array.isArray(nieuw) ? nieuw : [];\n const b = Array.isArray(oud) ? oud : [];\n for (let i = 0; i < Math.max(a.length, b.length); i++) {\n loop(a[i], b[i], pad ? `${pad}.${i}` : String(i), uit);\n }\n return;\n }\n const nObj = nieuw && typeof nieuw === \"object\";\n const oObj = oud && typeof oud === \"object\";\n if (nObj || oObj) {\n const sleutels = new Set([\n ...Object.keys(nObj ? (nieuw as object) : {}),\n ...Object.keys(oObj ? (oud as object) : {}),\n ]);\n for (const k of sleutels) {\n if (!pad && NEGEER.has(k)) continue;\n const n = nObj ? (nieuw as Record<string, unknown>)[k] : undefined;\n const o = oObj ? (oud as Record<string, unknown>)[k] : undefined;\n loop(n, o, pad ? `${pad}.${k}` : k, uit);\n }\n return;\n }\n // Bladwaarde. `undefined` en een lege string zijn hier hetzelfde: een veld dat nooit is ingevuld\n // en een veld dat je leegmaakte en weer invulde mogen niet als wijziging blijven hangen.\n const links = nieuw === undefined || nieuw === null ? \"\" : String(nieuw);\n const rechts = oud === undefined || oud === null ? \"\" : String(oud);\n if (links !== rechts && pad) uit.add(pad);\n}\n\n/**\n * Vergelijk de blokken van de werkversie met die van de gepubliceerde pagina.\n *\n * Is er nog géén gepubliceerde versie (`live === null`), dan is de hele pagina nieuw. Dat markeren\n * we bewust NIET veld voor veld: een compleet rode pagina is geen informatie, alleen lawaai. De\n * blokken krijgen de markering, de knop rechtsonder wordt blauw, en dat is de boodschap.\n */\nexport function bepaalWijzigingen(werk: Blok[] | null | undefined, live: Blok[] | null | undefined): Wijzigingen {\n const w = werk ?? [];\n if (!live) {\n const blokken = new Set(w.map((b) => String(b._key ?? \"\")).filter(Boolean));\n return { blokken, velden: new Map(), verwijderd: 0, iets: blokken.size > 0 };\n }\n const opKey = new Map(live.map((b, i) => [String(b._key ?? \"\"), { blok: b, index: i }]));\n const blokken = new Set<string>();\n const velden = new Map<string, Set<string>>();\n\n w.forEach((blok, index) => {\n const key = String(blok._key ?? \"\");\n if (!key) return;\n const eerder = opKey.get(key);\n if (!eerder) {\n // Nieuw blok: markeer het blok, niet elk veld apart (zie hierboven).\n blokken.add(key);\n return;\n }\n if (eerder.index !== index) blokken.add(key);\n const verschillen = new Set<string>();\n loop(blok, eerder.blok, \"\", verschillen);\n if (verschillen.size > 0) velden.set(key, verschillen);\n });\n\n const inWerk = new Set(w.map((b) => String(b._key ?? \"\")));\n const verwijderd = live.filter((b) => !inWerk.has(String(b._key ?? \"\"))).length;\n\n return {\n blokken,\n velden,\n verwijderd,\n iets: blokken.size > 0 || velden.size > 0 || verwijderd > 0,\n };\n}\n\n/**\n * Hoeveel staat er klaar — in gewone woorden, want een getal zonder eenheid zegt niets.\n *\n * Bewust grof: \"3 wijzigingen\" is genoeg om te weten dát er iets ligt. Wie precies wil weten wát,\n * ziet dat op de pagina zelf staan (rood/onderstreept); daar hoort deze knop niet mee te wedijveren.\n */\nexport function aantalWijzigingen(w: Wijzigingen): string {\n let n = w.blokken.size + w.verwijderd;\n for (const velden of w.velden.values()) n += velden.size;\n if (n === 0) return \"niets gewijzigd\";\n return n === 1 ? \"1 wijziging\" : `${n} wijzigingen`;\n}\n"],"mappings":"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","names":["blokken"]}
@@ -53,6 +53,18 @@ function AuthProvider({
53
53
  window.addEventListener(DEMO_EVENT, sync);
54
54
  return () => window.removeEventListener(DEMO_EVENT, sync);
55
55
  }, [demo]);
56
+ async function weigerZonderToegang(auth, sdk) {
57
+ const u = auth.currentUser;
58
+ if (!u) return;
59
+ const token = await u.getIdTokenResult(true);
60
+ const c = token.claims;
61
+ const rol = c.roles?.[siteId];
62
+ if (c.superadmin === true || rol === "siteadmin" || rol === "editor") return;
63
+ await sdk.signOut(auth);
64
+ const fout = new Error("geen-toegang");
65
+ fout.code = "xicms/geen-toegang";
66
+ throw fout;
67
+ }
56
68
  async function authSdk() {
57
69
  const [client, sdk] = await Promise.all([import("../firebase/client.js"), import("firebase/auth")]);
58
70
  const auth = client.getFirebaseAuth();
@@ -129,6 +141,7 @@ function AuthProvider({
129
141
  async signIn(email, password) {
130
142
  const { auth, sdk } = await authSdk();
131
143
  await sdk.signInWithEmailAndPassword(auth, email, password);
144
+ await weigerZonderToegang(auth, sdk);
132
145
  wekOp();
133
146
  },
134
147
  async signInWithGoogle() {
@@ -137,6 +150,7 @@ function AuthProvider({
137
150
  wekOp();
138
151
  try {
139
152
  await sdk.signInWithPopup(auth, provider);
153
+ await weigerZonderToegang(auth, sdk);
140
154
  } catch (e) {
141
155
  const code = e?.code ?? "";
142
156
  if (code !== "auth/popup-blocked") throw e;
@@ -145,10 +159,14 @@ function AuthProvider({
145
159
  }
146
160
  },
147
161
  async sendLoginLink(email) {
148
- const { auth, sdk } = await authSdk();
149
162
  const adres = email.trim();
150
- const url = `${window.location.origin}${window.location.pathname}`;
151
- await sdk.sendSignInLinkToEmail(auth, adres, { url, handleCodeInApp: true });
163
+ const [client, fns] = await Promise.all([
164
+ import("../firebase/client.js"),
165
+ import("../firebase/functionsClient.js")
166
+ ]);
167
+ const app = client.getFirebaseApp();
168
+ if (!app) throw new Error("Firebase is niet geconfigureerd.");
169
+ await fns.callable(app, "requestLoginLink")({ siteId, email: adres });
152
170
  try {
153
171
  window.localStorage.setItem(LINK_EMAIL_KEY, adres);
154
172
  } catch {
@@ -159,6 +177,7 @@ function AuthProvider({
159
177
  async completeLoginLink(email) {
160
178
  const { auth, sdk } = await authSdk();
161
179
  await sdk.signInWithEmailLink(auth, email.trim(), window.location.href);
180
+ await weigerZonderToegang(auth, sdk);
162
181
  opruimenNaLinkLogin();
163
182
  wekOp();
164
183
  },
@@ -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 /**\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 // Alleen om het abonnement-effect opnieuw te laten draaien na een login zonder paginawissel.\n const [wakker, setWakker] = useState(0);\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 /**\n * Zet de sessie-hint ÉN maak het effect hieronder wakker.\n *\n * Dat tweede is de kern van een bug die zich als \"ik kom er niet in\" voordeed. Het effect dat\n * `onAuthStateChanged` abonneert draait alleen als er bij het LADEN van de pagina al een hint is,\n * en hangt af van `[configured]` — een hint die later wordt gezet laat het dus niet opnieuw\n * draaien. Gevolg: log je in zónder paginawissel (popup of wachtwoord), dan maakt Firebase keurig\n * een sessie, maar luistert er niemand. React hoort het nooit en het inlogscherm blijft staan.\n *\n * Dat het toch vaak leek te werken kwam door de redirect-terugval: die verlaat de pagina en komt\n * terug op een verse load, waar de hint er wél staat. Precies daarom viel dit pas op ná het\n * repareren van de popup — toen verdween de herlaad die het gebrek maskeerde.\n *\n * De teller hoeft nergens te worden uitgelezen; hij bestaat alleen om de afhankelijkheidslijst\n * te laten wijzigen.\n */\n function wekOp(): void {\n setAuthHint(true);\n setWakker((n) => n + 1);\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, wakker]);\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 wekOp();\n },\n async signInWithGoogle() {\n const { auth, sdk } = await authSdk();\n const provider = new sdk.GoogleAuthProvider();\n /**\n * Abonneren VÓÓR de popup opengaat, niet erna.\n *\n * `signInWithPopup` lost pas op als Firebase de hele tokenuitwisseling heeft afgerond —\n * `signInWithIdp`, dan `lookup` — en pas dán sluit de handler zijn eigen venster. Wachtte de\n * UI op die belofte, dan bleef het inlogscherm al die tijd staan terwijl de sessie er vaak\n * al lang was. Voor de gebruiker las dat als \"er gebeurt niets\".\n *\n * Met de luisteraar al actief reageert het scherm op het moment dat de sessie ontstaat, niet\n * op het moment dat Firebase klaar is met opruimen. Het venster sluit daarna alsnog wat later\n * — dat is Firebase' eigen tijd en die willen we niet inkorten, want dan sluiten we het\n * midden in de tokenuitwisseling.\n */\n wekOp();\n try {\n await sdk.signInWithPopup(auth, provider);\n } catch (e) {\n // TERUGVAL OP REDIRECT — maar ALLEEN als de popup niet eens open kón.\n //\n // Dit stond hier breed: elke fout behalve annuleren stuurde de hele pagina naar Google.\n // Dat is destructief zodra de popup het wél doet, en precies dat gebeurde. Waargenomen op\n // christelijkkraamcadeau: je koos je account in de popup, en tegelijk opende de\n // ONDERLIGGENDE beheerpagina een tweede Google-scherm. In de console was ondertussen te\n // zien dat `accounts:signInWithIdp` en `accounts:lookup` gewoon slaagden — Firebase had je\n // dus allang ingelogd. Twee flows die door elkaar heen liepen, met \"Inloggen met Google\n // mislukt of geannuleerd\" als eindresultaat terwijl er niets mislukt was.\n //\n // Een redirect gooit de hele pagina weg. Dat is een zwaar middel en hoort alleen bij de\n // ene fout waarbij er echt geen andere weg is: de browser weigerde het venster te openen.\n // Alle andere fouten — netwerk, configuratie, een afgebroken relay — horen zichtbaar te\n // falen, niet stilzwijgend een tweede inlogpoging te starten.\n //\n // De aanleiding voor de brede terugval was een popup die dichtging zonder foutcode. Die\n // kwam door de COOP-blokkade (zie next/authRewrites.ts), en die is nu bij de bron\n // opgelost. Daarmee vervalt de reden om hier ruim te zijn.\n // Merk op dat hier GEEN `wekOp()` meer staat: de luisteraar is al vóór de popup\n // geabonneerd. Dat dekt meteen het geval dat de belofte afwijst terwijl er tóch een sessie\n // is — in de gemeten gevallen slaagden `signInWithIdp` en `lookup` gewoon terwijl deze\n // catch liep. `onAuthStateChanged` ziet die sessie dan alsnog en het inlogscherm verdwijnt.\n const code = (e as { code?: string })?.code ?? \"\";\n if (code !== \"auth/popup-blocked\") throw e;\n // ZET DE HINT VÓÓR HET VERTREK. Zonder deze regel heeft de redirect-terugval nog nooit\n // gewerkt voor een eerste login, en wel zo stil dat het op de popup werd geschoven. De\n // keten: je gaat naar Google, komt terug op een verse pagina, en daar besluit het effect\n // hierboven de auth-SDK NIET te laden — want er is geen sessie-hint (je was immers nog\n // nooit ingelogd) en het is geen inloglink. Laadt de SDK niet, dan draait\n // `getRedirectResult` niet, en wordt het antwoord van Google nooit opgehaald.\n setAuthHint(true);\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 wekOp();\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
+ {"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 // Alleen om het abonnement-effect opnieuw te laten draaien na een login zonder paginawissel.\n const [wakker, setWakker] = useState(0);\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 /**\n * Zet de sessie-hint ÉN maak het effect hieronder wakker.\n *\n * Dat tweede is de kern van een bug die zich als \"ik kom er niet in\" voordeed. Het effect dat\n * `onAuthStateChanged` abonneert draait alleen als er bij het LADEN van de pagina al een hint is,\n * en hangt af van `[configured]` — een hint die later wordt gezet laat het dus niet opnieuw\n * draaien. Gevolg: log je in zónder paginawissel (popup of wachtwoord), dan maakt Firebase keurig\n * een sessie, maar luistert er niemand. React hoort het nooit en het inlogscherm blijft staan.\n *\n * Dat het toch vaak leek te werken kwam door de redirect-terugval: die verlaat de pagina en komt\n * terug op een verse load, waar de hint er wél staat. Precies daarom viel dit pas op ná het\n * repareren van de popup — toen verdween de herlaad die het gebrek maskeerde.\n *\n * De teller hoeft nergens te worden uitgelezen; hij bestaat alleen om de afhankelijkheidslijst\n * te laten wijzigen.\n */\n function wekOp(): void {\n setAuthHint(true);\n setWakker((n) => n + 1);\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 /**\n * Meteen weer uitloggen als dit account geen toegang heeft tot DEZE site.\n *\n * Bij Google kun je niet vooraf weten wie er inlogt — je krijgt het adres pas ná de\n * accountkeuze. Zonder deze controle komt een willekeurige Google-gebruiker binnen met een\n * geldige sessie, ziet hij \"je hebt geen beheerrechten\", en blijft hij ingelogd op een site\n * waar hij niets te zoeken heeft. Dat is niet gevaarlijk (de rules kijken naar claims), maar\n * het is wel een half-open deur en het leest als een fout in plaats van als een grens.\n *\n * `forceRefresh` is nodig: het token dat je meteen na het inloggen krijgt kan nog van vóór een\n * recente rolwijziging zijn, en dan zou een net toegevoegde beheerder onterecht worden\n * geweigerd.\n */\n async function weigerZonderToegang(\n auth: { currentUser: User | null; },\n sdk: { signOut: (a: never) => Promise<void> },\n ): Promise<void> {\n const u = auth.currentUser;\n if (!u) return;\n const token = await u.getIdTokenResult(true);\n const c = token.claims as { superadmin?: boolean; roles?: Record<string, string> };\n const rol = c.roles?.[siteId];\n if (c.superadmin === true || rol === \"siteadmin\" || rol === \"editor\") return;\n\n await sdk.signOut(auth as never);\n const fout = new Error(\"geen-toegang\") as Error & { code?: string };\n fout.code = \"xicms/geen-toegang\";\n throw fout;\n }\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, wakker]);\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 // Zelfde grens als bij Google: een geldig wachtwoord is niet hetzelfde als toegang tot\n // DEZE site. Zonder deze regel zou de terugval een ruimere deur zijn dan de hoofdingang.\n await weigerZonderToegang(auth, sdk);\n wekOp();\n },\n async signInWithGoogle() {\n const { auth, sdk } = await authSdk();\n const provider = new sdk.GoogleAuthProvider();\n /**\n * Abonneren VÓÓR de popup opengaat, niet erna.\n *\n * `signInWithPopup` lost pas op als Firebase de hele tokenuitwisseling heeft afgerond —\n * `signInWithIdp`, dan `lookup` — en pas dán sluit de handler zijn eigen venster. Wachtte de\n * UI op die belofte, dan bleef het inlogscherm al die tijd staan terwijl de sessie er vaak\n * al lang was. Voor de gebruiker las dat als \"er gebeurt niets\".\n *\n * Met de luisteraar al actief reageert het scherm op het moment dat de sessie ontstaat, niet\n * op het moment dat Firebase klaar is met opruimen. Het venster sluit daarna alsnog wat later\n * — dat is Firebase' eigen tijd en die willen we niet inkorten, want dan sluiten we het\n * midden in de tokenuitwisseling.\n */\n wekOp();\n try {\n await sdk.signInWithPopup(auth, provider);\n await weigerZonderToegang(auth, sdk);\n } catch (e) {\n // TERUGVAL OP REDIRECT — maar ALLEEN als de popup niet eens open kón.\n //\n // Dit stond hier breed: elke fout behalve annuleren stuurde de hele pagina naar Google.\n // Dat is destructief zodra de popup het wél doet, en precies dat gebeurde. Waargenomen op\n // christelijkkraamcadeau: je koos je account in de popup, en tegelijk opende de\n // ONDERLIGGENDE beheerpagina een tweede Google-scherm. In de console was ondertussen te\n // zien dat `accounts:signInWithIdp` en `accounts:lookup` gewoon slaagden — Firebase had je\n // dus allang ingelogd. Twee flows die door elkaar heen liepen, met \"Inloggen met Google\n // mislukt of geannuleerd\" als eindresultaat terwijl er niets mislukt was.\n //\n // Een redirect gooit de hele pagina weg. Dat is een zwaar middel en hoort alleen bij de\n // ene fout waarbij er echt geen andere weg is: de browser weigerde het venster te openen.\n // Alle andere fouten — netwerk, configuratie, een afgebroken relay — horen zichtbaar te\n // falen, niet stilzwijgend een tweede inlogpoging te starten.\n //\n // De aanleiding voor de brede terugval was een popup die dichtging zonder foutcode. Die\n // kwam door de COOP-blokkade (zie next/authRewrites.ts), en die is nu bij de bron\n // opgelost. Daarmee vervalt de reden om hier ruim te zijn.\n // Merk op dat hier GEEN `wekOp()` meer staat: de luisteraar is al vóór de popup\n // geabonneerd. Dat dekt meteen het geval dat de belofte afwijst terwijl er tóch een sessie\n // is — in de gemeten gevallen slaagden `signInWithIdp` en `lookup` gewoon terwijl deze\n // catch liep. `onAuthStateChanged` ziet die sessie dan alsnog en het inlogscherm verdwijnt.\n const code = (e as { code?: string })?.code ?? \"\";\n if (code !== \"auth/popup-blocked\") throw e;\n // ZET DE HINT VÓÓR HET VERTREK. Zonder deze regel heeft de redirect-terugval nog nooit\n // gewerkt voor een eerste login, en wel zo stil dat het op de popup werd geschoven. De\n // keten: je gaat naar Google, komt terug op een verse pagina, en daar besluit het effect\n // hierboven de auth-SDK NIET te laden — want er is geen sessie-hint (je was immers nog\n // nooit ingelogd) en het is geen inloglink. Laadt de SDK niet, dan draait\n // `getRedirectResult` niet, en wordt het antwoord van Google nooit opgehaald.\n setAuthHint(true);\n await sdk.signInWithRedirect(auth, provider);\n }\n },\n async sendLoginLink(email: string) {\n const adres = email.trim();\n /**\n * Via de SERVER, niet via de client-SDK.\n *\n * `sendSignInLinkToEmail` stuurt een link naar élk adres dat je intikt. Firebase maakt bij\n * het inwisselen een account aan, dat vervolgens nergens een rol heeft en dus niets kan —\n * functioneel veilig, want de rules kijken naar claims en niet naar \"is ingelogd\". Maar het\n * betekent wel dat iedereen op het internet een account kan aanmaken op dit project, en dat\n * een vreemde een echte, werkende inloglink in zijn mailbox krijgt.\n *\n * De `requestLoginLink`-callable draait de volgorde om: eerst kijken of het adres een rol\n * heeft op DEZE site, en pas dan een link maken en versturen. Hij antwoordt altijd\n * hetzelfde, ook als er niets is verstuurd — anders zou het scherm verklappen wie beheerder\n * is. Daarom mag de bevestiging hierna ook niet beweren dát er een mail onderweg is.\n */\n // DYNAMISCH importeren, net als `authSdk()`. Zowel `firebase/client` als `functionsClient`\n // trekken de Firebase-SDK statisch mee; een gewone import bovenaan dit bestand zou die ~1 MB\n // aan élke anonieme bezoeker uitleveren — precies wat de sessie-hint hierboven voorkomt.\n const [client, fns] = await Promise.all([\n import(\"../firebase/client\"),\n import(\"../firebase/functionsClient\"),\n ]);\n const app = client.getFirebaseApp();\n if (!app) throw new Error(\"Firebase is niet geconfigureerd.\");\n await fns.callable(app, \"requestLoginLink\")({ siteId, email: adres });\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 // Ook hier, ondanks dat de server alleen naar beheerders stuurt: een mail kan worden\n // doorgestuurd, en dan zou de link in andere handen alsnog een sessie opleveren.\n await weigerZonderToegang(auth, sdk);\n opruimenNaLinkLogin();\n wekOp();\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":";AA0ZS;AAxZT;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;AAuC5D,MAAM,cAAc,cAAgC,IAAI;AAUxD,MAAM,iBAAiB;AAQvB,SAAS,mBAA4B;AACnC,MAAI,OAAO,WAAW,YAAa,QAAO;AAC1C,QAAM,IAAI,OAAO,SAAS;AAC1B,SAAO,EAAE,SAAS,aAAa,KAAK,EAAE,SAAS,UAAU;AAC3D;AAEO,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;AAE3D,QAAM,CAAC,iBAAiB,kBAAkB,IAAI,SAAS,KAAK;AAC5D,QAAM,CAAC,UAAU,WAAW,IAAI,SAAkB,IAAI;AAEtD,QAAM,CAAC,QAAQ,SAAS,IAAI,SAAS,CAAC;AAUtC,WAAS,sBAA4B;AACnC,QAAI;AACF,aAAO,aAAa,WAAW,cAAc;AAAA,IAC/C,QAAQ;AAAA,IAER;AACA,uBAAmB,KAAK;AACxB,gBAAY,IAAI;AAChB,QAAI;AACF,aAAO,QAAQ,aAAa,CAAC,GAAG,IAAI,OAAO,SAAS,WAAW,OAAO,SAAS,IAAI;AAAA,IACrF,QAAQ;AAAA,IAER;AAAA,EACF;AAkBA,WAAS,QAAc;AACrB,gBAAY,IAAI;AAChB,cAAU,CAAC,MAAM,IAAI,CAAC;AAAA,EACxB;AAGA,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;AAeT,iBAAe,oBACb,MACA,KACe;AACf,UAAM,IAAI,KAAK;AACf,QAAI,CAAC,EAAG;AACR,UAAM,QAAQ,MAAM,EAAE,iBAAiB,IAAI;AAC3C,UAAM,IAAI,MAAM;AAChB,UAAM,MAAM,EAAE,QAAQ,MAAM;AAC5B,QAAI,EAAE,eAAe,QAAQ,QAAQ,eAAe,QAAQ,SAAU;AAEtE,UAAM,IAAI,QAAQ,IAAa;AAC/B,UAAM,OAAO,IAAI,MAAM,cAAc;AACrC,SAAK,OAAO;AACZ,UAAM;AAAA,EACR;AAGA,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;AAQd,QAAI,CAAC,cAAe,CAAC,YAAY,KAAK,CAAC,iBAAiB,GAAI;AAC1D,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;AAMD,UAAI,IAAI,sBAAsB,MAAM,OAAO,SAAS,IAAI,GAAG;AACzD,YAAI,UAAyB;AAC7B,YAAI;AACF,oBAAU,OAAO,aAAa,QAAQ,cAAc;AAAA,QACtD,QAAQ;AAAA,QAER;AACA,YAAI,SAAS;AACX,cAAI;AACF,kBAAM,IAAI,oBAAoB,MAAM,SAAS,OAAO,SAAS,IAAI;AACjE,gCAAoB;AAAA,UACtB,SAAS,GAAG;AACV,oBAAQ,MAAM,sCAAsC,CAAC;AACrD,wBAAY,CAAC;AAAA,UACf;AAAA,QACF,WAAW,CAAC,YAAY;AACtB,6BAAmB,IAAI;AAAA,QACzB;AAAA,MACF;AAEA,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,YAAY,MAAM,CAAC;AAEvB,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;AAG1D,cAAM,oBAAoB,MAAM,GAAG;AACnC,cAAM;AAAA,MACR;AAAA,MACA,MAAM,mBAAmB;AACvB,cAAM,EAAE,MAAM,IAAI,IAAI,MAAM,QAAQ;AACpC,cAAM,WAAW,IAAI,IAAI,mBAAmB;AAc5C,cAAM;AACN,YAAI;AACF,gBAAM,IAAI,gBAAgB,MAAM,QAAQ;AACxC,gBAAM,oBAAoB,MAAM,GAAG;AAAA,QACrC,SAAS,GAAG;AAuBV,gBAAM,OAAQ,GAAyB,QAAQ;AAC/C,cAAI,SAAS,qBAAsB,OAAM;AAOzC,sBAAY,IAAI;AAChB,gBAAM,IAAI,mBAAmB,MAAM,QAAQ;AAAA,QAC7C;AAAA,MACF;AAAA,MACA,MAAM,cAAc,OAAe;AACjC,cAAM,QAAQ,MAAM,KAAK;AAkBzB,cAAM,CAAC,QAAQ,GAAG,IAAI,MAAM,QAAQ,IAAI;AAAA,UACtC,OAAO,oBAAoB;AAAA,UAC3B,OAAO,6BAA6B;AAAA,QACtC,CAAC;AACD,cAAM,MAAM,OAAO,eAAe;AAClC,YAAI,CAAC,IAAK,OAAM,IAAI,MAAM,kCAAkC;AAC5D,cAAM,IAAI,SAAS,KAAK,kBAAkB,EAAE,EAAE,QAAQ,OAAO,MAAM,CAAC;AACpE,YAAI;AACF,iBAAO,aAAa,QAAQ,gBAAgB,KAAK;AAAA,QACnD,QAAQ;AAAA,QAGR;AAAA,MACF;AAAA,MACA,qBAAqB;AAAA,MACrB,gBAAgB;AAAA,MAChB,MAAM,kBAAkB,OAAe;AACrC,cAAM,EAAE,MAAM,IAAI,IAAI,MAAM,QAAQ;AACpC,cAAM,IAAI,oBAAoB,MAAM,MAAM,KAAK,GAAG,OAAO,SAAS,IAAI;AAGtE,cAAM,oBAAoB,MAAM,GAAG;AACnC,4BAAoB;AACpB,cAAM;AAAA,MACR;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,cAAc,iBAAiB,QAAQ,CAAC;AAE7F,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":[]}
@@ -28,6 +28,11 @@ function authErrorMessage(e, fallback) {
28
28
  case "auth/cancelled-popup-request":
29
29
  return "Het Google-venster werd gesloten voordat het inloggen klaar was. Probeer het opnieuw, of gebruik de inloglink hieronder.";
30
30
  // ── Inloglink ──────────────────────────────────────────────────────────────────────────
31
+ // Geen Firebase-code maar de onze: het account bestaat en de login slaagde, maar het hoort
32
+ // niet bij deze site. Bewust concreet — "inloggen mislukt" zou de gebruiker laten zoeken naar
33
+ // een fout die er niet is.
34
+ case "xicms/geen-toegang":
35
+ return "Dit account heeft geen toegang tot deze site. Vraag de beheerder om je toe te voegen, of log in met een ander account.";
31
36
  case "auth/expired-action-code":
32
37
  return "Deze inloglink is verlopen. Vraag hieronder een nieuwe aan.";
33
38
  case "auth/invalid-action-code":
@@ -123,9 +128,9 @@ function LoginForm({ onSuccess }) {
123
128
  return /* @__PURE__ */ jsxs("div", { className: "xicms-card", style: kaart, children: [
124
129
  /* @__PURE__ */ jsx("h2", { style: { margin: 0 }, children: "Kijk in je mail" }),
125
130
  /* @__PURE__ */ jsxs("p", { style: { margin: 0 }, children: [
126
- "We stuurden een inloglink naar ",
131
+ "Heeft ",
127
132
  /* @__PURE__ */ jsx("strong", { children: email }),
128
- ". Klik erop en je bent binnen \u2014 geen wachtwoord nodig."
133
+ " toegang tot deze site, dan staat er nu een inloglink in die mailbox. Klik erop en je bent binnen \u2014 geen wachtwoord nodig."
129
134
  ] }),
130
135
  /* @__PURE__ */ jsxs("p", { style: { margin: 0, fontSize: 13, opacity: 0.7 }, children: [
131
136
  "Niets ontvangen? Kijk in je spam, of",
@@ -1 +1 @@
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 return \"De browser blokkeerde het Google-venster. Sta popups toe voor deze site, of gebruik de inloglink hieronder.\";\n case \"auth/popup-closed-by-user\":\n case \"auth/cancelled-popup-request\":\n return \"Het Google-venster werd gesloten voordat het inloggen klaar was. Probeer het opnieuw, of gebruik de inloglink hieronder.\";\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":[]}
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 return \"De browser blokkeerde het Google-venster. Sta popups toe voor deze site, of gebruik de inloglink hieronder.\";\n case \"auth/popup-closed-by-user\":\n case \"auth/cancelled-popup-request\":\n return \"Het Google-venster werd gesloten voordat het inloggen klaar was. Probeer het opnieuw, of gebruik de inloglink hieronder.\";\n // ── Inloglink ──────────────────────────────────────────────────────────────────────────\n // Geen Firebase-code maar de onze: het account bestaat en de login slaagde, maar het hoort\n // niet bij deze site. Bewust concreet — \"inloggen mislukt\" zou de gebruiker laten zoeken naar\n // een fout die er niet is.\n case \"xicms/geen-toegang\":\n return \"Dit account heeft geen toegang tot deze site. Vraag de beheerder om je toe te voegen, of log in met een ander account.\";\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 {/* Bewust voorwaardelijk geformuleerd. De server stuurt alleen een link naar een adres dat\n ook echt beheerder is van deze site, en antwoordt altijd hetzelfde — anders zou dit\n scherm verklappen wie dat zijn. Zou hier \"we stuurden een link naar X\" staan, dan is dat\n een bewering die we niet kunnen waarmaken én een oracle. */}\n <p style={{ margin: 0 }}>\n Heeft <strong>{email}</strong> toegang tot deze site, dan staat er nu een inloglink in die\n mailbox. Klik erop en je bent binnen — geen 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":[]}
@@ -1,3 +1,4 @@
1
+ import { type Bewerkmodus } from "../editor/EditorProvider.js";
1
2
  /**
2
3
  * Zwevende werkbalk voor ingelogde beheerders/editors.
3
4
  *
@@ -6,5 +7,34 @@
6
7
  * half de huisstijl over (en op plekken waar de tokens niet in scope zijn, viel het terug op een
7
8
  * indigo die bij niets hoorde). Gereedschap hoort er als gereedschap uit te zien — hetzelfde op
8
9
  * elke site, herkenbaar los van de pagina eronder.
10
+ *
11
+ * ── VIER OPTIES, ALTIJD DEZELFDE (7.8) ────────────────────────────────────────────────────
12
+ *
13
+ * De balk had één hoofdactie ("Pagina bewerken") en verdween zodra je die indrukte: de editor
14
+ * bracht zijn eigen balk mee, met andere knoppen op andere plekken. Je verloor dus je houvast op
15
+ * het moment dat je het het hardst nodig had, en er was geen weg terug naar de live site zonder
16
+ * de editor te sluiten.
17
+ *
18
+ * Nu staan er vier vaste opties — Live site · Bewerken · Beheren · Uitloggen — en die staan er in
19
+ * béide toestanden, op dezelfde plek. `AdminModes` hieronder is letterlijk hetzelfde component in
20
+ * de balk van de editor, zodat het overschakelen geen wisseling van balk is maar alleen een andere
21
+ * knop die oplicht.
9
22
  */
10
23
  export declare function EditBar(): import("react").JSX.Element | null;
24
+ /**
25
+ * De vier opties zelf, los zodat de editor ze kan hergebruiken.
26
+ *
27
+ * `actief` zegt welke toestand je nú bekijkt, niet wat de knop doet: "Live site" is ingedrukt
28
+ * zolang je de pagina ziet zoals een bezoeker hem ziet. Zonder die markering is er geen verschil
29
+ * te zien tussen "ik kijk naar de live site" en "ik bewerk maar heb nog niets veranderd", en dat
30
+ * verschil is precies waar deze balk over gaat.
31
+ */
32
+ export declare function AdminModes({ actief, voorSwitch, }: {
33
+ actief: Bewerkmodus;
34
+ /**
35
+ * Kans voor de editor om iets te zeggen vóór je weggaat — bijvoorbeeld dat er nog wijzigingen
36
+ * klaarstaan die niet live staan. Bewust niet blokkerend: sluiten bewaart je werk, dus een
37
+ * bevestigingsvraag zou alleen maar in de weg zitten. Melden is genoeg, tegenhouden is te veel.
38
+ */
39
+ voorSwitch?: (modus: Bewerkmodus) => void;
40
+ }): import("react").JSX.Element;
@@ -1,21 +1,47 @@
1
1
  "use client";
2
- import { jsx, jsxs } from "react/jsx-runtime";
2
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
3
3
  import { useAuth } from "../auth/AuthProvider.js";
4
4
  import { canEdit } from "../auth/roles.js";
5
5
  import { useEditor } from "../editor/EditorProvider.js";
6
6
  function EditBar() {
7
- const { user, role, signOut, demo } = useAuth();
8
- const { open, openEditor } = useEditor();
7
+ const { user, role } = useAuth();
8
+ const { open } = useEditor();
9
9
  if (!canEdit(role) || !user || open) return null;
10
10
  return /* @__PURE__ */ jsxs("div", { className: "xicms-editbar", role: "toolbar", "aria-label": "Beheer", children: [
11
- /* @__PURE__ */ jsx("span", { className: "xicms-editbar__brand", children: demo ? "XiCMS \xB7 demo" : "XiCMS" }),
12
- /* @__PURE__ */ jsx("button", { type: "button", className: "xicms-editbar__btn xicms-editbar__btn--primary", onClick: openEditor, children: "Pagina bewerken" }),
11
+ /* @__PURE__ */ jsx("span", { className: "xicms-editbar__brand", children: "XiCMS" }),
12
+ /* @__PURE__ */ jsx(AdminModes, { actief: "live" })
13
+ ] });
14
+ }
15
+ function AdminModes({
16
+ actief,
17
+ voorSwitch
18
+ }) {
19
+ const { demo, signOut } = useAuth();
20
+ const { zetModus } = useEditor();
21
+ const knop = (modus, label, titel) => /* @__PURE__ */ jsx(
22
+ "button",
23
+ {
24
+ type: "button",
25
+ className: `xicms-editbar__btn${actief === modus ? " is-actief" : ""}`,
26
+ "aria-pressed": actief === modus,
27
+ title: titel,
28
+ onClick: () => {
29
+ if (modus !== actief) voorSwitch?.(modus);
30
+ zetModus(modus);
31
+ },
32
+ children: label
33
+ }
34
+ );
35
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
36
+ knop("live", "Live site", "De pagina precies zoals een bezoeker hem ziet"),
37
+ knop("bewerken", "Bewerken", "Dezelfde pagina, met de werkversie en potlood/tandwiel per blok"),
13
38
  /* @__PURE__ */ jsx("span", { className: "xicms-editbar__sep", "aria-hidden": "true" }),
14
- !demo && /* @__PURE__ */ jsx("a", { href: "/beheer", className: "xicms-editbar__btn", children: "Beheer" }),
39
+ !demo && /* @__PURE__ */ jsx("a", { href: "/beheer", className: "xicms-editbar__btn", title: "Pagina's, media, instellingen, webwinkel", children: "Beheren" }),
15
40
  /* @__PURE__ */ jsx("button", { type: "button", className: "xicms-editbar__btn", onClick: () => void signOut(), children: "Uitloggen" })
16
41
  ] });
17
42
  }
18
43
  export {
44
+ AdminModes,
19
45
  EditBar
20
46
  };
21
47
  //# sourceMappingURL=EditBar.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/components/EditBar.tsx"],"sourcesContent":["\"use client\";\n\nimport { useAuth } from \"../auth/AuthProvider\";\nimport { canEdit } from \"../auth/roles\";\nimport { useEditor } from \"../editor/EditorProvider\";\n\n/**\n * Zwevende werkbalk voor ingelogde beheerders/editors.\n *\n * Bewust met EIGEN klassen en eigen kleuren, niet met `.xicms-btn`. Die knop leest de\n * `--xicms-*`-tokens van de klantsite, en dit balkje zweeft over die site heen: het nam daardoor\n * half de huisstijl over (en op plekken waar de tokens niet in scope zijn, viel het terug op een\n * indigo die bij niets hoorde). Gereedschap hoort er als gereedschap uit te zien — hetzelfde op\n * elke site, herkenbaar los van de pagina eronder.\n */\nexport function EditBar() {\n // Bewust via useAuth (niet via het deprecated ContentProvider-pad): de EditBar hoort bij het\n // block-model en mag niet aan het legacy inline-systeem hangen.\n const { user, role, signOut, demo } = useAuth();\n const { open, openEditor } = useEditor();\n\n if (!canEdit(role) || !user || open) return null;\n\n return (\n <div className=\"xicms-editbar\" role=\"toolbar\" aria-label=\"Beheer\">\n <span className=\"xicms-editbar__brand\">{demo ? \"XiCMS · demo\" : \"XiCMS\"}</span>\n {/* Eén heldere hoofdactie: opent de visuele editor waarin je tekst ter plekke (inline)\n bewerkt én blokken aanpast. Woordelijk hetzelfde als waar de demo-balk naar verwijst. */}\n <button type=\"button\" className=\"xicms-editbar__btn xicms-editbar__btn--primary\" onClick={openEditor}>\n Pagina bewerken\n </button>\n <span className=\"xicms-editbar__sep\" aria-hidden=\"true\" />\n {/* In de demo bestaat er geen /beheer-route; daar bewerk je alleen op de pagina. */}\n {!demo && <a href=\"/beheer\" className=\"xicms-editbar__btn\">Beheer</a>}\n <button type=\"button\" className=\"xicms-editbar__btn\" onClick={() => void signOut()}>\n Uitloggen\n </button>\n </div>\n );\n}\n"],"mappings":";AAwBI,SACE,KADF;AAtBJ,SAAS,eAAe;AACxB,SAAS,eAAe;AACxB,SAAS,iBAAiB;AAWnB,SAAS,UAAU;AAGxB,QAAM,EAAE,MAAM,MAAM,SAAS,KAAK,IAAI,QAAQ;AAC9C,QAAM,EAAE,MAAM,WAAW,IAAI,UAAU;AAEvC,MAAI,CAAC,QAAQ,IAAI,KAAK,CAAC,QAAQ,KAAM,QAAO;AAE5C,SACE,qBAAC,SAAI,WAAU,iBAAgB,MAAK,WAAU,cAAW,UACvD;AAAA,wBAAC,UAAK,WAAU,wBAAwB,iBAAO,oBAAiB,SAAQ;AAAA,IAGxE,oBAAC,YAAO,MAAK,UAAS,WAAU,kDAAiD,SAAS,YAAY,6BAEtG;AAAA,IACA,oBAAC,UAAK,WAAU,sBAAqB,eAAY,QAAO;AAAA,IAEvD,CAAC,QAAQ,oBAAC,OAAE,MAAK,WAAU,WAAU,sBAAqB,oBAAM;AAAA,IACjE,oBAAC,YAAO,MAAK,UAAS,WAAU,sBAAqB,SAAS,MAAM,KAAK,QAAQ,GAAG,uBAEpF;AAAA,KACF;AAEJ;","names":[]}
1
+ {"version":3,"sources":["../../src/components/EditBar.tsx"],"sourcesContent":["\"use client\";\n\nimport { useAuth } from \"../auth/AuthProvider\";\nimport { canEdit } from \"../auth/roles\";\nimport { useEditor, type Bewerkmodus } from \"../editor/EditorProvider\";\n\n/**\n * Zwevende werkbalk voor ingelogde beheerders/editors.\n *\n * Bewust met EIGEN klassen en eigen kleuren, niet met `.xicms-btn`. Die knop leest de\n * `--xicms-*`-tokens van de klantsite, en dit balkje zweeft over die site heen: het nam daardoor\n * half de huisstijl over (en op plekken waar de tokens niet in scope zijn, viel het terug op een\n * indigo die bij niets hoorde). Gereedschap hoort er als gereedschap uit te zien — hetzelfde op\n * elke site, herkenbaar los van de pagina eronder.\n *\n * ── VIER OPTIES, ALTIJD DEZELFDE (7.8) ────────────────────────────────────────────────────\n *\n * De balk had één hoofdactie (\"Pagina bewerken\") en verdween zodra je die indrukte: de editor\n * bracht zijn eigen balk mee, met andere knoppen op andere plekken. Je verloor dus je houvast op\n * het moment dat je het het hardst nodig had, en er was geen weg terug naar de live site zonder\n * de editor te sluiten.\n *\n * Nu staan er vier vaste opties — Live site · Bewerken · Beheren · Uitloggen — en die staan er in\n * béide toestanden, op dezelfde plek. `AdminModes` hieronder is letterlijk hetzelfde component in\n * de balk van de editor, zodat het overschakelen geen wisseling van balk is maar alleen een andere\n * knop die oplicht.\n */\nexport function EditBar() {\n // Bewust via useAuth (niet via het deprecated ContentProvider-pad): de EditBar hoort bij het\n // block-model en mag niet aan het legacy inline-systeem hangen.\n const { user, role } = useAuth();\n const { open } = useEditor();\n\n // In de bewerkmodus tekent de editor de balk (met dezelfde vier opties plus zijn gereedschap);\n // twee balken onder elkaar zou de site twee keer zo ver omlaag duwen.\n if (!canEdit(role) || !user || open) return null;\n\n return (\n <div className=\"xicms-editbar\" role=\"toolbar\" aria-label=\"Beheer\">\n <span className=\"xicms-editbar__brand\">XiCMS</span>\n <AdminModes actief=\"live\" />\n </div>\n );\n}\n\n/**\n * De vier opties zelf, los zodat de editor ze kan hergebruiken.\n *\n * `actief` zegt welke toestand je nú bekijkt, niet wat de knop doet: \"Live site\" is ingedrukt\n * zolang je de pagina ziet zoals een bezoeker hem ziet. Zonder die markering is er geen verschil\n * te zien tussen \"ik kijk naar de live site\" en \"ik bewerk maar heb nog niets veranderd\", en dat\n * verschil is precies waar deze balk over gaat.\n */\nexport function AdminModes({\n actief,\n voorSwitch,\n}: {\n actief: Bewerkmodus;\n /**\n * Kans voor de editor om iets te zeggen vóór je weggaat — bijvoorbeeld dat er nog wijzigingen\n * klaarstaan die niet live staan. Bewust niet blokkerend: sluiten bewaart je werk, dus een\n * bevestigingsvraag zou alleen maar in de weg zitten. Melden is genoeg, tegenhouden is te veel.\n */\n voorSwitch?: (modus: Bewerkmodus) => void;\n}) {\n const { demo, signOut } = useAuth();\n const { zetModus } = useEditor();\n\n const knop = (modus: Bewerkmodus, label: string, titel: string) => (\n <button\n type=\"button\"\n className={`xicms-editbar__btn${actief === modus ? \" is-actief\" : \"\"}`}\n aria-pressed={actief === modus}\n title={titel}\n onClick={() => {\n if (modus !== actief) voorSwitch?.(modus);\n zetModus(modus);\n }}\n >\n {label}\n </button>\n );\n\n return (\n <>\n {knop(\"live\", \"Live site\", \"De pagina precies zoals een bezoeker hem ziet\")}\n {knop(\"bewerken\", \"Bewerken\", \"Dezelfde pagina, met de werkversie en potlood/tandwiel per blok\")}\n <span className=\"xicms-editbar__sep\" aria-hidden=\"true\" />\n {/* In de demo bestaat er geen /beheer-route; daar bewerk je alleen op de pagina. */}\n {!demo && (\n <a href=\"/beheer\" className=\"xicms-editbar__btn\" title=\"Pagina's, media, instellingen, webwinkel\">\n Beheren\n </a>\n )}\n <button type=\"button\" className=\"xicms-editbar__btn\" onClick={() => void signOut()}>\n Uitloggen\n </button>\n </>\n );\n}\n"],"mappings":";AAsCI,SA8CA,UA7CE,KADF;AApCJ,SAAS,eAAe;AACxB,SAAS,eAAe;AACxB,SAAS,iBAAmC;AAuBrC,SAAS,UAAU;AAGxB,QAAM,EAAE,MAAM,KAAK,IAAI,QAAQ;AAC/B,QAAM,EAAE,KAAK,IAAI,UAAU;AAI3B,MAAI,CAAC,QAAQ,IAAI,KAAK,CAAC,QAAQ,KAAM,QAAO;AAE5C,SACE,qBAAC,SAAI,WAAU,iBAAgB,MAAK,WAAU,cAAW,UACvD;AAAA,wBAAC,UAAK,WAAU,wBAAuB,mBAAK;AAAA,IAC5C,oBAAC,cAAW,QAAO,QAAO;AAAA,KAC5B;AAEJ;AAUO,SAAS,WAAW;AAAA,EACzB;AAAA,EACA;AACF,GAQG;AACD,QAAM,EAAE,MAAM,QAAQ,IAAI,QAAQ;AAClC,QAAM,EAAE,SAAS,IAAI,UAAU;AAE/B,QAAM,OAAO,CAAC,OAAoB,OAAe,UAC/C;AAAA,IAAC;AAAA;AAAA,MACC,MAAK;AAAA,MACL,WAAW,qBAAqB,WAAW,QAAQ,eAAe,EAAE;AAAA,MACpE,gBAAc,WAAW;AAAA,MACzB,OAAO;AAAA,MACP,SAAS,MAAM;AACb,YAAI,UAAU,OAAQ,cAAa,KAAK;AACxC,iBAAS,KAAK;AAAA,MAChB;AAAA,MAEC;AAAA;AAAA,EACH;AAGF,SACE,iCACG;AAAA,SAAK,QAAQ,aAAa,+CAA+C;AAAA,IACzE,KAAK,YAAY,YAAY,iEAAiE;AAAA,IAC/F,oBAAC,UAAK,WAAU,sBAAqB,eAAY,QAAO;AAAA,IAEvD,CAAC,QACA,oBAAC,OAAE,MAAK,WAAU,WAAU,sBAAqB,OAAM,4CAA2C,qBAElG;AAAA,IAEF,oBAAC,YAAO,MAAK,UAAS,WAAU,sBAAqB,SAAS,MAAM,KAAK,QAAQ,GAAG,uBAEpF;AAAA,KACF;AAEJ;","names":[]}
@@ -1,8 +1,13 @@
1
1
  import { type ReactNode } from "react";
2
2
  import type { Page } from "../schema/pages.js";
3
3
  type PaginaBlokken = Page["blocks"];
4
+ /** "live" = precies wat een bezoeker ziet. "bewerken" = dezelfde pagina, met werkversie en gereedschap. */
5
+ export type Bewerkmodus = "live" | "bewerken";
4
6
  interface EditorCtx {
7
+ modus: Bewerkmodus;
8
+ /** Blijft bestaan naast `modus` omdat het door de hele codebase heen gelezen wordt. */
5
9
  open: boolean;
10
+ zetModus: (modus: Bewerkmodus) => void;
6
11
  openEditor: () => void;
7
12
  closeEditor: () => void;
8
13
  /** De blokken van de werkversie die de pagina moet tonen; `null` = toon gewoon het gepubliceerde. */
@@ -1,9 +1,13 @@
1
1
  "use client";
2
2
  import { jsx, jsxs } from "react/jsx-runtime";
3
- import { createContext, useCallback, useContext, useMemo, useState, lazy, Suspense } from "react";
3
+ import { createContext, useCallback, useContext, useEffect, useMemo, useState, lazy, Suspense } from "react";
4
4
  const VisualEditor = lazy(() => import("../admin/visual/VisualEditor.js"));
5
+ const OPSLAGSLEUTEL = "xicms-bewerkmodus";
5
6
  const Ctx = createContext({
7
+ modus: "live",
6
8
  open: false,
9
+ zetModus: () => {
10
+ },
7
11
  openEditor: () => {
8
12
  },
9
13
  closeEditor: () => {
@@ -14,26 +18,39 @@ const Ctx = createContext({
14
18
  }
15
19
  });
16
20
  function EditorProvider({ children }) {
17
- const [open, setOpen] = useState(false);
21
+ const [modus, setModusState] = useState("live");
18
22
  const [draft, setDraft] = useState(null);
23
+ useEffect(() => {
24
+ try {
25
+ if (sessionStorage.getItem(OPSLAGSLEUTEL) === "bewerken") setModusState("bewerken");
26
+ } catch {
27
+ }
28
+ }, []);
29
+ const zetModus = useCallback((next) => {
30
+ setModusState(next);
31
+ try {
32
+ if (next === "bewerken") sessionStorage.setItem(OPSLAGSLEUTEL, "bewerken");
33
+ else sessionStorage.removeItem(OPSLAGSLEUTEL);
34
+ } catch {
35
+ }
36
+ if (next === "live") setDraft(null);
37
+ }, []);
19
38
  const publishDraft = useCallback((path, blocks) => {
20
39
  setDraft(blocks ? { path, blocks } : null);
21
40
  }, []);
22
- const closeEditor = useCallback(() => {
23
- setOpen(false);
24
- setDraft(null);
25
- }, []);
26
41
  const value = useMemo(() => ({
27
- open,
28
- openEditor: () => setOpen(true),
29
- closeEditor,
42
+ modus,
43
+ open: modus === "bewerken",
44
+ zetModus,
45
+ openEditor: () => zetModus("bewerken"),
46
+ closeEditor: () => zetModus("live"),
30
47
  draftBlocks: draft?.blocks ?? null,
31
48
  editPath: draft?.path ?? null,
32
49
  publishDraft
33
- }), [open, draft, closeEditor, publishDraft]);
50
+ }), [modus, draft, zetModus, publishDraft]);
34
51
  return /* @__PURE__ */ jsxs(Ctx.Provider, { value, children: [
35
52
  children,
36
- open && /* @__PURE__ */ jsx(Suspense, { fallback: null, children: /* @__PURE__ */ jsx(VisualEditor, { onClose: closeEditor }) })
53
+ modus === "bewerken" && /* @__PURE__ */ jsx(Suspense, { fallback: null, children: /* @__PURE__ */ jsx(VisualEditor, { onClose: () => zetModus("live") }) })
37
54
  ] });
38
55
  }
39
56
  function useEditor() {