@xiwebdesign/xicms-core 7.22.2 → 7.23.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.
@@ -7,6 +7,7 @@ import { getDb } from "../../firebase/client.js";
7
7
  import {
8
8
  ProductSchema,
9
9
  formatCents,
10
+ optieHeeftBeeldPerWaarde,
10
11
  saneerProductDoc
11
12
  } from "../../schema/shop.js";
12
13
  import { Badge, Button, Card, Field, Input, Textarea } from "../ui/primitives.js";
@@ -164,6 +165,12 @@ function ProductForm({
164
165
  return { ...p, options };
165
166
  });
166
167
  }
168
+ function zetBeeldAs(index, aan) {
169
+ setDef((p) => ({
170
+ ...p,
171
+ options: (p.options ?? []).map((o, j) => ({ ...o, imagePerValue: j === index ? aan : false }))
172
+ }));
173
+ }
167
174
  function variantFor(key) {
168
175
  return (def.variants ?? []).find((v) => v.key === key);
169
176
  }
@@ -200,8 +207,14 @@ function ProductForm({
200
207
  (v) => validKeys.has(v.key) && (v.priceCents !== void 0 || v.stock !== void 0 || v.sku !== void 0 || v.active === false)
201
208
  );
202
209
  const cleanPersonalization = (def.personalization ?? []).filter((f) => f.label.trim().length > 0);
210
+ const cleanOptions = (def.options ?? []).map((o) => {
211
+ if (!o.images || !optieHeeftBeeldPerWaarde(o)) return o;
212
+ const images = Object.fromEntries(Object.entries(o.images).filter(([waarde]) => o.values.includes(waarde)));
213
+ return { ...o, images: Object.keys(images).length > 0 ? images : void 0 };
214
+ });
203
215
  const parsed = ProductSchema.safeParse({
204
216
  ...def,
217
+ options: cleanOptions,
205
218
  variants: cleanVariants,
206
219
  personalization: cleanPersonalization.length > 0 ? cleanPersonalization : void 0
207
220
  });
@@ -366,7 +379,19 @@ function ProductForm({
366
379
  }
367
380
  )
368
381
  ] }),
369
- /^kleur|^colou?r/i.test(opt.name) && opt.values.length > 0 && /* @__PURE__ */ jsx("div", { className: "flex flex-wrap items-center gap-2", children: opt.values.map((value) => {
382
+ /* @__PURE__ */ jsxs("label", { className: "flex w-fit items-center gap-2 text-xs text-slate-600", children: [
383
+ /* @__PURE__ */ jsx(
384
+ "input",
385
+ {
386
+ type: "checkbox",
387
+ checked: optieHeeftBeeldPerWaarde(opt),
388
+ onChange: (e) => zetBeeldAs(i, e.target.checked)
389
+ }
390
+ ),
391
+ "Elke waarde een eigen foto",
392
+ /* @__PURE__ */ jsx("span", { className: "text-slate-400", children: "\u2014 het hoofdbeeld wisselt mee met deze keuze" })
393
+ ] }),
394
+ optieHeeftBeeldPerWaarde(opt) && opt.values.length > 0 && /* @__PURE__ */ jsx("div", { className: "flex flex-wrap items-center gap-2", children: opt.values.map((value) => {
370
395
  const url = opt.images?.[value];
371
396
  return /* @__PURE__ */ jsxs("span", { className: "flex items-center gap-1 rounded-full border border-slate-200 px-2 py-1 text-xs", children: [
372
397
  url && /* @__PURE__ */ jsx("img", { src: url, alt: "", className: "h-6 w-6 rounded object-cover" }),
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/admin/shop/ProductsTab.tsx"],"sourcesContent":["\"use client\";\n\nimport { useEffect, useMemo, useState } from \"react\";\nimport { collection, doc, onSnapshot } from \"firebase/firestore\";\nimport { removeProduct, saveProductDoc } from \"./productClient\";\nimport { getDb } from \"../../firebase/client\";\nimport {\n ProductSchema,\n formatCents,\n saneerProductDoc,\n type PersonalizationField,\n type Product,\n type ProductOption,\n type ProductVariant,\n type ShopCategory,\n} from \"../../schema/shop\";\nimport { Badge, Button, Card, Field, Input, Textarea } from \"../ui/primitives\";\nimport { Select } from \"../ui/select\";\nimport { Switch } from \"../ui/switch\";\nimport { useToast } from \"../ui/toast\";\nimport { useConfirm, type ConfirmOptions } from \"../ui/ConfirmDialog\";\nimport { MediaPickerButton } from \"../media/MediaPicker\";\nimport { slugify } from \"../lib/slugify\";\nimport { allVariantKeys, centsToEuroInput, eurosToCents } from \"./lib\";\n\ntype StoredProduct = Product & { id: string };\ntype StoredCategory = ShopCategory & { id: string };\n\nconst STATUS_OPTIONS = [\n { value: \"draft\", label: \"Concept\" },\n { value: \"active\", label: \"Actief\" },\n { value: \"archived\", label: \"Gearchiveerd\" },\n];\n\nexport function ProductsTab({ siteId }: { siteId: string }) {\n const { toast } = useToast();\n const confirm = useConfirm();\n const [products, setProducts] = useState<StoredProduct[]>([]);\n const [cats, setCats] = useState<StoredCategory[]>([]);\n const [selected, setSelected] = useState<string | null>(null);\n const [q, setQ] = useState(\"\");\n\n useEffect(() => {\n const db = getDb();\n if (!db) return;\n const un1 = onSnapshot(collection(db, \"sites\", siteId, \"products\"), (snap) =>\n setProducts(\n snap.docs\n // saneerProductDoc: historische docs dragen null-velden (zie schema/shop.ts) — schoon\n // ze bij het lezen, zodat de form-state ze niet via {...def} opnieuw naar de server lekt.\n .map((d) => ({ id: d.id, ...saneerProductDoc(d.data() as Product) }))\n .sort((a, b) => (a.order ?? 0) - (b.order ?? 0) || a.name.localeCompare(b.name, \"nl\")),\n ),\n );\n const un2 = onSnapshot(collection(db, \"sites\", siteId, \"shopCategories\"), (snap) =>\n setCats(snap.docs.map((d) => ({ id: d.id, ...(d.data() as ShopCategory) }))),\n );\n return () => {\n un1();\n un2();\n };\n }, [siteId]);\n\n async function createProduct() {\n // Ook aanmaken loopt via de callable: een vers product is een draft (onzichtbaar voor de\n // cache), maar zo staat élke productmutatie in de audit-log en is er één pad i.p.v. twee.\n const nieuw = {\n name: \"Nieuw product\",\n slug: `product-${products.length + 1}`,\n description: \"\",\n categoryIds: [],\n images: [],\n priceCents: 0,\n vatRate: 21,\n options: [],\n variants: [],\n trackStock: false,\n status: \"draft\",\n order: products.length,\n featured: false,\n personalizationEnabled: true,\n } satisfies Product;\n try {\n const id = await saveProductDoc(siteId, undefined, nieuw);\n setSelected(id);\n } catch (e) {\n // Zichtbaar falen: dit gooide een tijdlang stil (unhandled) terwijl de knop gewoon niets\n // leek te doen — precies de melding \"bij aanmaken product krijg ik iets met permissions\".\n toast({ title: \"Product aanmaken mislukt\", description: e instanceof Error ? e.message : undefined, tone: \"error\" });\n }\n }\n\n const filtered = useMemo(() => {\n const needle = q.trim().toLowerCase();\n if (!needle) return products;\n return products.filter((p) => p.name.toLowerCase().includes(needle) || p.slug.includes(needle));\n }, [products, q]);\n\n const current = products.find((p) => p.id === selected) ?? null;\n\n return (\n <div className=\"grid gap-4 md:grid-cols-[300px_1fr]\">\n <Card className=\"h-fit\">\n <div className=\"mb-3 flex items-center justify-between\">\n <h3 className=\"text-sm font-semibold\">Producten</h3>\n <Button size=\"sm\" onClick={createProduct}>+ Nieuw</Button>\n </div>\n <Input placeholder=\"Zoeken…\" value={q} onChange={(e) => setQ(e.target.value)} className=\"mb-2\" />\n <ul className=\"grid max-h-[60vh] gap-1 overflow-y-auto\">\n {filtered.map((p) => (\n <li key={p.id}>\n <button\n onClick={() => setSelected(p.id)}\n className={`flex w-full items-center gap-2 rounded-lg px-3 py-2 text-left text-sm ${\n p.id === selected ? \"bg-brand-50 text-brand-700\" : \"hover:bg-slate-100\"\n }`}\n >\n {p.images?.[0] && <img src={p.images[0]} alt=\"\" className=\"h-8 w-8 rounded object-cover\" />}\n {/* Categorie erbij: hetzelfde ontwerp bestaat vaak als rompertje, slabbetje én\n t-shirt, en dan staat dezelfde naam vijf keer onder elkaar in deze lijst.\n Op een eigen regel eronder, want achter de naam is de kolom te smal — dan werd\n juist het onderscheidende deel afgekapt (\"Geschapen! — Korte mouw t-s…\"). */}\n <span className=\"min-w-0 flex-1\">\n <span className=\"block truncate\">{p.name}</span>\n {(() => {\n const cat = cats.find((c) => c.id === (p.categoryIds ?? [])[0]);\n return cat ? <span className=\"block truncate text-xs text-slate-400\">{cat.name}</span> : null;\n })()}\n </span>\n {/* Voorraadsignaal: uitverkocht of bijna op moet je zien zónder elk product te\n openen — anders merk je pas bij een bestelling dat er niets meer is. */}\n {(() => {\n const n = voorraadStand(p);\n if (n === null) return null;\n if (n <= 0) return <Badge tone=\"slate\">uitverkocht</Badge>;\n if (n <= 3) return <Badge tone=\"amber\">{n} over</Badge>;\n return null;\n })()}\n <Badge tone={p.status === \"active\" ? \"green\" : p.status === \"draft\" ? \"amber\" : \"slate\"}>\n {STATUS_OPTIONS.find((s) => s.value === p.status)?.label ?? p.status}\n </Badge>\n </button>\n </li>\n ))}\n {filtered.length === 0 && <li className=\"px-3 py-2 text-sm text-slate-500\">Geen producten.</li>}\n </ul>\n </Card>\n {current ? (\n <ProductForm\n key={current.id}\n siteId={siteId}\n product={current}\n categories={cats}\n onDeleted={() => setSelected(null)}\n onDuplicated={(id) => setSelected(id)}\n onToast={toast}\n confirm={confirm}\n />\n ) : (\n <Card>\n <p className=\"text-sm text-slate-500\">Selecteer of maak een product.</p>\n </Card>\n )}\n </div>\n );\n}\n\n/**\n * Het volledige productformulier. Sinds 7.8 ook buiten dit tabblad in gebruik: het tandwiel bij een\n * product op de pagina opent hetzelfde formulier in een zijpaneel. Bewust hergebruik en geen tweede,\n * uitgeklede versie — twee formulieren voor één product lopen gegarandeerd uit elkaar, en dan hangt\n * het van je route af welke velden je kunt bereiken.\n */\n/**\n * Voorraadstand van een product voor de beheerlijst: het laagste getal dat er te melden valt.\n * `null` = niet bijgehouden of onbeperkt. Zonder opties telt `stock`, met opties de varianten.\n */\nfunction voorraadStand(p: Product): number | null {\n if (p.trackStock !== true) return null;\n if ((p.options ?? []).length === 0) return p.stock ?? null;\n const getallen = (p.variants ?? []).filter((v) => v.stock !== undefined && v.active !== false).map((v) => v.stock as number);\n return getallen.length > 0 ? Math.min(...getallen) : null;\n}\n\nexport function ProductForm({\n siteId,\n product,\n categories,\n onDeleted,\n onDuplicated,\n onToast,\n confirm,\n}: {\n siteId: string;\n product: StoredProduct;\n categories: StoredCategory[];\n onDeleted: () => void;\n /** Optioneel: na dupliceren de kopie selecteren (het zijpaneel doet dit niet). */\n onDuplicated?: (id: string) => void;\n onToast: (t: { title: string; description?: string; tone?: \"default\" | \"success\" | \"error\" }) => void;\n confirm: (opts: ConfirmOptions) => Promise<boolean>;\n}) {\n const [def, setDef] = useState<Product>(product);\n /**\n * Waarschuwen bij niet-opgeslagen werk (7.18). Het formulier heeft geen autosave (een product\n * gaat meteen live), dus wie naar een ander product klikte of het tabblad sloot, was zijn\n * wijzigingen kwijt zonder enige melding.\n */\n const vuil = JSON.stringify(def) !== JSON.stringify(product);\n useEffect(() => {\n if (!vuil) return;\n const waarschuw = (e: BeforeUnloadEvent) => e.preventDefault();\n window.addEventListener(\"beforeunload\", waarschuw);\n return () => window.removeEventListener(\"beforeunload\", waarschuw);\n }, [vuil]);\n const [priceInput, setPriceInput] = useState(centsToEuroInput(product.priceCents));\n const [compareInput, setCompareInput] = useState(centsToEuroInput(product.compareAtCents));\n\n const variantKeys = useMemo(() => allVariantKeys(def.options ?? []), [def.options]);\n\n function setOption(i: number, patch: Partial<ProductOption>) {\n setDef((p) => {\n const options = [...(p.options ?? [])];\n options[i] = { ...options[i], ...patch };\n return { ...p, options };\n });\n }\n\n function variantFor(key: string): ProductVariant | undefined {\n return (def.variants ?? []).find((v) => v.key === key);\n }\n\n function patchVariant(key: string, patch: Partial<ProductVariant>) {\n setDef((p) => {\n const variants = [...(p.variants ?? [])];\n const idx = variants.findIndex((v) => v.key === key);\n if (idx >= 0) variants[idx] = { ...variants[idx], ...patch };\n else variants.push({ key, active: true, ...patch });\n return { ...p, variants };\n });\n }\n\n /** Kopie van dit product maken (nieuwe slug + \"(kopie)\"), als concept zodat je rustig aanpast. */\n async function dupliceer() {\n try {\n const kopie: Product = {\n ...def,\n name: `${def.name} (kopie)`,\n slug: `${def.slug}-kopie`.slice(0, 120),\n status: \"draft\",\n order: (def.order ?? 0) + 1,\n };\n const id = await saveProductDoc(siteId, undefined, kopie);\n onToast({ title: \"Product gedupliceerd\", description: \"De kopie staat als concept klaar.\", tone: \"success\" });\n onDuplicated?.(id);\n } catch (e) {\n onToast({ title: \"Dupliceren mislukt\", description: e instanceof Error ? e.message : undefined, tone: \"error\" });\n }\n }\n\n async function save() {\n const db = getDb();\n if (!db) return;\n // Alleen varianten bewaren die iets toevoegen (afwijkende prijs/voorraad/sku of inactief),\n // en geen wees-varianten van verwijderde optiewaarden.\n const validKeys = new Set(variantKeys);\n const cleanVariants = (def.variants ?? []).filter(\n (v) =>\n validKeys.has(v.key) &&\n (v.priceCents !== undefined || v.stock !== undefined || v.sku !== undefined || v.active === false),\n );\n // Onvolledige personalisatie-regels (zonder label) niet opslaan.\n const cleanPersonalization = (def.personalization ?? []).filter((f) => f.label.trim().length > 0);\n const parsed = ProductSchema.safeParse({\n ...def,\n variants: cleanVariants,\n personalization: cleanPersonalization.length > 0 ? cleanPersonalization : undefined,\n });\n if (!parsed.success) {\n onToast({ title: \"Ongeldig product\", description: parsed.error.issues[0]?.message, tone: \"error\" });\n return;\n }\n // Via de callable (7.9): de server valideert, auditeert en — belangrijker — pingt de\n // products-cachetag, zodat de wijziging ook op de LIVE shoppagina's staat en niet alleen in\n // Firestore. Zie productClient.ts, ook voor de terugval op een oudere backend.\n await saveProductDoc(siteId, product.id, parsed.data);\n onToast({ title: \"Product opgeslagen\", tone: \"success\" });\n }\n\n async function remove() {\n const ok = await confirm({\n title: `\"${product.name}\" verwijderen?`,\n description: \"Bestaande bestellingen behouden hun eigen kopie van naam en prijs.\",\n tone: \"danger\",\n });\n if (!ok) return;\n await removeProduct(siteId, product.id);\n onDeleted();\n }\n\n return (\n <Card>\n <div className=\"grid gap-4\">\n <div className=\"grid gap-4 md:grid-cols-2\">\n <Field label=\"Naam\">\n <Input\n value={def.name}\n onChange={(e) => setDef((p) => ({ ...p, name: e.target.value, slug: slugify(e.target.value) || p.slug }))}\n />\n </Field>\n <Field label=\"Slug\" hint=\"Product-URL: /product?p=<slug>\">\n <Input value={def.slug} onChange={(e) => setDef((p) => ({ ...p, slug: slugify(e.target.value) }))} />\n </Field>\n </div>\n\n <div className=\"grid gap-4 md:grid-cols-3\">\n <Field label=\"Prijs (€, incl. btw)\">\n <Input\n value={priceInput}\n onChange={(e) => {\n setPriceInput(e.target.value);\n const c = eurosToCents(e.target.value);\n if (c !== null) setDef((p) => ({ ...p, priceCents: c }));\n }}\n />\n </Field>\n <Field label=\"Van-prijs (€, optioneel)\" hint=\"Doorgestreept naast de prijs\">\n <Input\n value={compareInput}\n onChange={(e) => {\n setCompareInput(e.target.value);\n const c = eurosToCents(e.target.value);\n setDef((p) => ({ ...p, compareAtCents: c ?? undefined }));\n }}\n />\n </Field>\n <Field label=\"Btw-tarief\">\n <Select\n value={String(def.vatRate ?? 21)}\n onValueChange={(v) => setDef((p) => ({ ...p, vatRate: Number(v) as 21 | 9 | 0 }))}\n options={[\n { value: \"21\", label: \"21%\" },\n { value: \"9\", label: \"9%\" },\n { value: \"0\", label: \"0%\" },\n ]}\n />\n </Field>\n </div>\n\n <Field label=\"Omschrijving\">\n <Textarea rows={4} value={def.description ?? \"\"} onChange={(e) => setDef((p) => ({ ...p, description: e.target.value }))} />\n </Field>\n\n <Field label=\"Afbeeldingen\" hint=\"Eerste afbeelding is het hoofdbeeld\">\n <div className=\"flex flex-wrap items-center gap-3\">\n {(def.images ?? []).map((url, i) => (\n <div key={url + i} className=\"relative\">\n <img src={url} alt=\"\" className={`h-16 w-16 rounded-lg object-cover ${i === 0 ? \"ring-2 ring-brand-500\" : \"\"}`} />\n {i === 0 && (\n <span className=\"absolute -bottom-2 left-1/2 -translate-x-1/2 rounded-full bg-brand-500 px-1.5 text-[10px] text-white\">hoofd</span>\n )}\n <button\n type=\"button\"\n aria-label=\"Verwijder afbeelding\"\n className=\"absolute -right-2 -top-2 grid h-5 w-5 place-items-center rounded-full bg-slate-700 text-xs text-white\"\n onClick={() => setDef((p) => ({ ...p, images: (p.images ?? []).filter((_, j) => j !== i) }))}\n >\n ✕\n </button>\n {/* Herschikken: de eerste afbeelding is het hoofdbeeld, en dat was alleen te\n veranderen door alles te wissen en opnieuw te kiezen. */}\n {i > 0 && (\n <button\n type=\"button\"\n aria-label=\"Naar voren verplaatsen\"\n title={i === 1 ? \"Maak hoofdbeeld\" : \"Naar voren\"}\n className=\"absolute -left-2 -top-2 grid h-5 w-5 place-items-center rounded-full bg-slate-700 text-xs text-white\"\n onClick={() =>\n setDef((p) => {\n const imgs = [...(p.images ?? [])];\n [imgs[i - 1], imgs[i]] = [imgs[i], imgs[i - 1]];\n return { ...p, images: imgs };\n })\n }\n >\n ←\n </button>\n )}\n </div>\n ))}\n <MediaPickerButton\n siteId={siteId}\n label=\"+ Afbeelding\"\n onSelect={(m) => setDef((p) => ({ ...p, images: [...(p.images ?? []), m.url] }))}\n />\n </div>\n </Field>\n\n <Field label=\"Categorieën\">\n <div className=\"flex flex-wrap gap-2\">\n {categories.map((c) => {\n const on = (def.categoryIds ?? []).includes(c.id);\n return (\n <button\n key={c.id}\n type=\"button\"\n onClick={() =>\n setDef((p) => ({\n ...p,\n categoryIds: on ? (p.categoryIds ?? []).filter((x) => x !== c.id) : [...(p.categoryIds ?? []), c.id],\n }))\n }\n className={`rounded-full border px-3 py-1 text-sm ${\n on ? \"border-brand-500 bg-brand-50 text-brand-700\" : \"border-slate-200 hover:bg-slate-50\"\n }`}\n >\n {on ? \"✓ \" : \"\"}\n {c.name}\n </button>\n );\n })}\n {categories.length === 0 && <span className=\"text-sm text-slate-500\">Maak eerst een categorie aan.</span>}\n </div>\n </Field>\n\n {/* Opties & varianten */}\n <Field\n label=\"Opties (bv. Maat, Kleur)\"\n hint=\"Max. 3. Waarden gescheiden door komma's. Combinaties kun je hieronder per stuk aanpassen.\"\n >\n <div className=\"grid gap-2\">\n {(def.options ?? []).map((opt, i) => (\n <div key={i} className=\"grid gap-2 rounded-lg border border-slate-100 p-2\">\n <div className=\"flex items-center gap-2\">\n <Input\n className=\"w-36\"\n placeholder=\"Naam (bv. Maat)\"\n value={opt.name}\n onChange={(e) => setOption(i, { name: e.target.value })}\n />\n <Input\n placeholder=\"Waarden, bv: 56, 62, 68, 74\"\n value={opt.values.join(\", \")}\n onChange={(e) =>\n setOption(i, { values: e.target.value.split(\",\").map((s) => s.trim()).filter(Boolean) })\n }\n />\n <Button\n size=\"sm\"\n variant=\"ghost\"\n onClick={() => setDef((p) => ({ ...p, options: (p.options ?? []).filter((_, j) => j !== i) }))}\n >\n ✕\n </Button>\n </div>\n {/* Afbeelding per waarde (5.6) — vooral voor de kleur-as: de detailpagina\n wisselt het hoofdbeeld mee met de gekozen kleur. */}\n {/^kleur|^colou?r/i.test(opt.name) && opt.values.length > 0 && (\n <div className=\"flex flex-wrap items-center gap-2\">\n {opt.values.map((value) => {\n const url = opt.images?.[value];\n return (\n <span key={value} className=\"flex items-center gap-1 rounded-full border border-slate-200 px-2 py-1 text-xs\">\n {url && <img src={url} alt=\"\" className=\"h-6 w-6 rounded object-cover\" />}\n {value}\n <MediaPickerButton\n siteId={siteId}\n label={url ? \"↻\" : \"+ foto\"}\n onSelect={(m) =>\n setOption(i, { images: { ...(opt.images ?? {}), [value]: m.url } })\n }\n />\n {url && (\n <button\n type=\"button\"\n aria-label={`Verwijder foto van ${value}`}\n className=\"text-slate-400 hover:text-slate-700\"\n onClick={() => {\n const images = { ...(opt.images ?? {}) };\n delete images[value];\n setOption(i, { images });\n }}\n >\n ✕\n </button>\n )}\n </span>\n );\n })}\n </div>\n )}\n </div>\n ))}\n {(def.options ?? []).length < 3 && (\n <Button\n size=\"sm\"\n variant=\"secondary\"\n className=\"w-fit\"\n onClick={() => setDef((p) => ({ ...p, options: [...(p.options ?? []), { name: \"\", values: [] }] }))}\n >\n + Optie\n </Button>\n )}\n </div>\n </Field>\n\n {variantKeys.length > 0 && variantKeys.length <= 60 && (\n <Field label=\"Varianten\" hint=\"Leeg = standaardprijs / onbeperkte voorraad\">\n <div className=\"overflow-x-auto\">\n <table className=\"w-full text-sm\">\n <thead>\n <tr className=\"text-left text-xs text-slate-500\">\n <th className=\"py-1 pr-2\">Combinatie</th>\n <th className=\"py-1 pr-2\">Prijs (€)</th>\n {def.trackStock && <th className=\"py-1 pr-2\">Voorraad</th>}\n <th className=\"py-1 pr-2\">Artikelnr.</th>\n <th className=\"py-1 pr-2\">Leverbaar</th>\n </tr>\n </thead>\n <tbody>\n {variantKeys.map((key) => {\n const v = variantFor(key);\n return (\n <tr key={key} className=\"border-t border-slate-100\">\n <td className=\"py-1 pr-2\">{key.split(\"|\").join(\" · \")}</td>\n <td className=\"py-1 pr-2\">\n {/* Native uncontrolled inputs: geen cursor-gedoe bij \"13,\" tijdens typen. */}\n <input\n className=\"w-24 rounded-lg border border-slate-200 px-2 py-1\"\n placeholder={centsToEuroInput(def.priceCents)}\n defaultValue={centsToEuroInput(v?.priceCents)}\n onChange={(e) => {\n const c = eurosToCents(e.target.value);\n patchVariant(key, { priceCents: c ?? undefined });\n }}\n />\n </td>\n {def.trackStock && (\n <td className=\"py-1 pr-2\">\n <input\n className=\"w-20 rounded-lg border border-slate-200 px-2 py-1\"\n type=\"number\"\n min={0}\n placeholder=\"∞\"\n defaultValue={v?.stock ?? \"\"}\n onChange={(e) => {\n const n = e.target.value === \"\" ? undefined : Math.max(0, Number(e.target.value) || 0);\n patchVariant(key, { stock: n });\n }}\n />\n </td>\n )}\n <td className=\"py-1 pr-2\">\n {/* SKU stond al in het schema maar was nergens in te vullen — terwijl\n het juist per variant nodig is (voor je eigen administratie/pakbon). */}\n <input\n className=\"w-28 rounded-lg border border-slate-200 px-2 py-1\"\n placeholder=\"—\"\n defaultValue={v?.sku ?? \"\"}\n onChange={(e) => patchVariant(key, { sku: e.target.value.trim() || undefined })}\n />\n </td>\n <td className=\"py-1 pr-2\">\n <Switch checked={v?.active !== false} onCheckedChange={(on) => patchVariant(key, { active: on })} />\n </td>\n </tr>\n );\n })}\n </tbody>\n </table>\n </div>\n </Field>\n )}\n {variantKeys.length > 60 && (\n <p className=\"text-sm text-amber-600\">\n {variantKeys.length} combinaties — te veel om per stuk te tonen. Beperk het aantal optie-waarden.\n </p>\n )}\n\n {/* Personalisatie (5.6): eigen naam/logo van de klant, met optionele meerprijs.\n Sinds 7.19 met een eigen aan/uit-schakelaar: uitzetten bewaart de velden. */}\n <div className=\"rounded-xl border border-slate-200 p-3\">\n <Switch\n checked={def.personalizationEnabled !== false}\n onCheckedChange={(v) =>\n setDef((p) => ({\n ...p,\n personalizationEnabled: v,\n // Bij het aanzetten meteen één leeg veld klaarzetten, anders staat de klant voor\n // een lege sectie en lijkt de schakelaar niets te doen.\n personalization:\n v && (p.personalization ?? []).length === 0\n ? [{ key: \"naam\", label: \"Naam\", type: \"text\", required: false, maxLength: 20, priceCents: 0 }]\n : p.personalization,\n }))\n }\n label=\"Persoonlijk maken aanzetten voor dit product\"\n />\n {def.personalizationEnabled === false && (def.personalization?.length ?? 0) > 0 && (\n <p className=\"mt-1 text-xs text-slate-500\">\n Staat uit — de klant ziet geen invulveld. Je {def.personalization!.length === 1 ? \"instelling blijft\" : \"instellingen blijven\"} bewaard\n voor als je het weer aanzet.\n </p>\n )}\n\n {def.personalizationEnabled !== false && (\n <Field\n label=\"Personalisatie (eigen tekst of afbeelding)\"\n hint=\"Max. 3 velden. De klant vult dit in op de productpagina; de meerprijs telt automatisch mee.\"\n >\n <div className=\"grid gap-2\">\n {(def.personalization ?? []).map((f, i) => {\n const setPers = (patch: Partial<PersonalizationField>) =>\n setDef((p) => {\n const personalization = [...(p.personalization ?? [])];\n personalization[i] = { ...personalization[i], ...patch };\n return { ...p, personalization };\n });\n return (\n <div key={i} className=\"grid gap-2 rounded-lg border border-slate-100 p-2 md:grid-cols-[1fr_130px_90px_110px_auto]\">\n <Input\n placeholder='Label (bv. \"Naam van het kindje\")'\n value={f.label}\n onChange={(e) =>\n setPers({\n label: e.target.value,\n key: f.key || e.target.value.toLowerCase().replace(/[^a-z0-9]+/g, \"_\").replace(/^_+|_+$/g, \"\").slice(0, 40) || `veld${i + 1}`,\n })\n }\n />\n <Select\n value={f.type}\n onValueChange={(v) => setPers({ type: v as \"text\" | \"image\" })}\n options={[\n { value: \"text\", label: \"Tekst\" },\n { value: \"image\", label: \"Afbeelding\" },\n ]}\n />\n {f.type === \"text\" ? (\n <Input\n type=\"number\"\n title=\"Max. tekens\"\n min={1}\n max={200}\n value={f.maxLength ?? 40}\n onChange={(e) => setPers({ maxLength: Math.max(1, Math.min(200, Number(e.target.value) || 40)) })}\n />\n ) : (\n <span className=\"self-center text-xs text-slate-400\">jpg/png/webp</span>\n )}\n <input\n className=\"rounded-lg border border-slate-200 px-2 py-1\"\n placeholder=\"Meerprijs €\"\n defaultValue={f.priceCents ? centsToEuroInput(f.priceCents) : \"\"}\n onChange={(e) => setPers({ priceCents: eurosToCents(e.target.value) ?? 0 })}\n />\n <div className=\"flex items-center gap-2\">\n <Switch checked={f.required === true} onCheckedChange={(v) => setPers({ required: v })} label=\"Verplicht\" />\n <Button\n size=\"sm\"\n variant=\"ghost\"\n onClick={() => setDef((p) => ({ ...p, personalization: (p.personalization ?? []).filter((_, j) => j !== i) }))}\n >\n ✕\n </Button>\n </div>\n {/* Uitleg onder het veld in de winkel. Bestond al in het schema en werd al\n gerenderd op de productpagina, maar was hier nooit in te vullen. */}\n <input\n className=\"col-span-full rounded-lg border border-slate-200 px-2 py-1 text-sm\"\n placeholder=\"Uitleg voor de klant (optioneel), bv. 'Max. 12 tekens — komt op de voorkant'\"\n value={f.helpText ?? \"\"}\n onChange={(e) => setPers({ helpText: e.target.value })}\n />\n </div>\n );\n })}\n {(def.personalization ?? []).length < 3 && (\n <Button\n size=\"sm\"\n variant=\"secondary\"\n className=\"w-fit\"\n onClick={() =>\n setDef((p) => ({\n ...p,\n personalization: [\n ...(p.personalization ?? []),\n { key: `veld${(p.personalization ?? []).length + 1}`, label: \"\", type: \"text\", required: false, maxLength: 40, priceCents: 0 },\n ],\n }))\n }\n >\n + Personalisatie-veld\n </Button>\n )}\n </div>\n </Field>\n )}\n </div>\n\n <div className=\"flex flex-wrap items-center gap-6\">\n <Field label=\"Status\">\n <Select\n value={def.status ?? \"draft\"}\n onValueChange={(v) => setDef((p) => ({ ...p, status: v as Product[\"status\"] }))}\n options={STATUS_OPTIONS}\n />\n </Field>\n <Field label=\"Volgorde\">\n <Input\n type=\"number\"\n className=\"w-24\"\n value={def.order ?? 0}\n onChange={(e) => setDef((p) => ({ ...p, order: Math.max(0, Number(e.target.value) || 0) }))}\n />\n </Field>\n <Switch checked={def.featured === true} onCheckedChange={(v) => setDef((p) => ({ ...p, featured: v }))} label=\"Uitgelicht\" />\n <Switch checked={def.trackStock === true} onCheckedChange={(v) => setDef((p) => ({ ...p, trackStock: v }))} label=\"Voorraad bijhouden\" />\n {def.trackStock && (def.options ?? []).length === 0 && (\n <Field label=\"Voorraad\">\n <Input\n type=\"number\"\n className=\"w-24\"\n placeholder=\"∞\"\n value={def.stock ?? \"\"}\n onChange={(e) =>\n setDef((p) => ({ ...p, stock: e.target.value === \"\" ? undefined : Math.max(0, Number(e.target.value) || 0) }))\n }\n />\n </Field>\n )}\n </div>\n\n <div className=\"flex items-center justify-between border-t border-slate-100 pt-4\">\n <div className=\"flex items-center gap-3\">\n <Button onClick={save}>Opslaan</Button>\n <span className=\"text-sm text-slate-500\">{formatCents(def.priceCents)}</span>\n {vuil && <span className=\"text-sm font-medium text-amber-700\">• niet opgeslagen</span>}\n </div>\n <div className=\"flex items-center gap-2\">\n {/* Dupliceren: een variant van een bestaand product maken was overtypen. */}\n <Button variant=\"secondary\" onClick={dupliceer}>Dupliceren</Button>\n <Button variant=\"danger\" onClick={remove}>Verwijderen</Button>\n </div>\n </div>\n </div>\n </Card>\n );\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../../src/admin/shop/ProductsTab.tsx"],"sourcesContent":["\"use client\";\n\nimport { useEffect, useMemo, useState } from \"react\";\nimport { collection, doc, onSnapshot } from \"firebase/firestore\";\nimport { removeProduct, saveProductDoc } from \"./productClient\";\nimport { getDb } from \"../../firebase/client\";\nimport {\n ProductSchema,\n formatCents,\n optieHeeftBeeldPerWaarde,\n saneerProductDoc,\n type PersonalizationField,\n type Product,\n type ProductOption,\n type ProductVariant,\n type ShopCategory,\n} from \"../../schema/shop\";\nimport { Badge, Button, Card, Field, Input, Textarea } from \"../ui/primitives\";\nimport { Select } from \"../ui/select\";\nimport { Switch } from \"../ui/switch\";\nimport { useToast } from \"../ui/toast\";\nimport { useConfirm, type ConfirmOptions } from \"../ui/ConfirmDialog\";\nimport { MediaPickerButton } from \"../media/MediaPicker\";\nimport { slugify } from \"../lib/slugify\";\nimport { allVariantKeys, centsToEuroInput, eurosToCents } from \"./lib\";\n\ntype StoredProduct = Product & { id: string };\ntype StoredCategory = ShopCategory & { id: string };\n\nconst STATUS_OPTIONS = [\n { value: \"draft\", label: \"Concept\" },\n { value: \"active\", label: \"Actief\" },\n { value: \"archived\", label: \"Gearchiveerd\" },\n];\n\nexport function ProductsTab({ siteId }: { siteId: string }) {\n const { toast } = useToast();\n const confirm = useConfirm();\n const [products, setProducts] = useState<StoredProduct[]>([]);\n const [cats, setCats] = useState<StoredCategory[]>([]);\n const [selected, setSelected] = useState<string | null>(null);\n const [q, setQ] = useState(\"\");\n\n useEffect(() => {\n const db = getDb();\n if (!db) return;\n const un1 = onSnapshot(collection(db, \"sites\", siteId, \"products\"), (snap) =>\n setProducts(\n snap.docs\n // saneerProductDoc: historische docs dragen null-velden (zie schema/shop.ts) — schoon\n // ze bij het lezen, zodat de form-state ze niet via {...def} opnieuw naar de server lekt.\n .map((d) => ({ id: d.id, ...saneerProductDoc(d.data() as Product) }))\n .sort((a, b) => (a.order ?? 0) - (b.order ?? 0) || a.name.localeCompare(b.name, \"nl\")),\n ),\n );\n const un2 = onSnapshot(collection(db, \"sites\", siteId, \"shopCategories\"), (snap) =>\n setCats(snap.docs.map((d) => ({ id: d.id, ...(d.data() as ShopCategory) }))),\n );\n return () => {\n un1();\n un2();\n };\n }, [siteId]);\n\n async function createProduct() {\n // Ook aanmaken loopt via de callable: een vers product is een draft (onzichtbaar voor de\n // cache), maar zo staat élke productmutatie in de audit-log en is er één pad i.p.v. twee.\n const nieuw = {\n name: \"Nieuw product\",\n slug: `product-${products.length + 1}`,\n description: \"\",\n categoryIds: [],\n images: [],\n priceCents: 0,\n vatRate: 21,\n options: [],\n variants: [],\n trackStock: false,\n status: \"draft\",\n order: products.length,\n featured: false,\n personalizationEnabled: true,\n } satisfies Product;\n try {\n const id = await saveProductDoc(siteId, undefined, nieuw);\n setSelected(id);\n } catch (e) {\n // Zichtbaar falen: dit gooide een tijdlang stil (unhandled) terwijl de knop gewoon niets\n // leek te doen — precies de melding \"bij aanmaken product krijg ik iets met permissions\".\n toast({ title: \"Product aanmaken mislukt\", description: e instanceof Error ? e.message : undefined, tone: \"error\" });\n }\n }\n\n const filtered = useMemo(() => {\n const needle = q.trim().toLowerCase();\n if (!needle) return products;\n return products.filter((p) => p.name.toLowerCase().includes(needle) || p.slug.includes(needle));\n }, [products, q]);\n\n const current = products.find((p) => p.id === selected) ?? null;\n\n return (\n <div className=\"grid gap-4 md:grid-cols-[300px_1fr]\">\n <Card className=\"h-fit\">\n <div className=\"mb-3 flex items-center justify-between\">\n <h3 className=\"text-sm font-semibold\">Producten</h3>\n <Button size=\"sm\" onClick={createProduct}>+ Nieuw</Button>\n </div>\n <Input placeholder=\"Zoeken…\" value={q} onChange={(e) => setQ(e.target.value)} className=\"mb-2\" />\n <ul className=\"grid max-h-[60vh] gap-1 overflow-y-auto\">\n {filtered.map((p) => (\n <li key={p.id}>\n <button\n onClick={() => setSelected(p.id)}\n className={`flex w-full items-center gap-2 rounded-lg px-3 py-2 text-left text-sm ${\n p.id === selected ? \"bg-brand-50 text-brand-700\" : \"hover:bg-slate-100\"\n }`}\n >\n {p.images?.[0] && <img src={p.images[0]} alt=\"\" className=\"h-8 w-8 rounded object-cover\" />}\n {/* Categorie erbij: hetzelfde ontwerp bestaat vaak als rompertje, slabbetje én\n t-shirt, en dan staat dezelfde naam vijf keer onder elkaar in deze lijst.\n Op een eigen regel eronder, want achter de naam is de kolom te smal — dan werd\n juist het onderscheidende deel afgekapt (\"Geschapen! — Korte mouw t-s…\"). */}\n <span className=\"min-w-0 flex-1\">\n <span className=\"block truncate\">{p.name}</span>\n {(() => {\n const cat = cats.find((c) => c.id === (p.categoryIds ?? [])[0]);\n return cat ? <span className=\"block truncate text-xs text-slate-400\">{cat.name}</span> : null;\n })()}\n </span>\n {/* Voorraadsignaal: uitverkocht of bijna op moet je zien zónder elk product te\n openen — anders merk je pas bij een bestelling dat er niets meer is. */}\n {(() => {\n const n = voorraadStand(p);\n if (n === null) return null;\n if (n <= 0) return <Badge tone=\"slate\">uitverkocht</Badge>;\n if (n <= 3) return <Badge tone=\"amber\">{n} over</Badge>;\n return null;\n })()}\n <Badge tone={p.status === \"active\" ? \"green\" : p.status === \"draft\" ? \"amber\" : \"slate\"}>\n {STATUS_OPTIONS.find((s) => s.value === p.status)?.label ?? p.status}\n </Badge>\n </button>\n </li>\n ))}\n {filtered.length === 0 && <li className=\"px-3 py-2 text-sm text-slate-500\">Geen producten.</li>}\n </ul>\n </Card>\n {current ? (\n <ProductForm\n key={current.id}\n siteId={siteId}\n product={current}\n categories={cats}\n onDeleted={() => setSelected(null)}\n onDuplicated={(id) => setSelected(id)}\n onToast={toast}\n confirm={confirm}\n />\n ) : (\n <Card>\n <p className=\"text-sm text-slate-500\">Selecteer of maak een product.</p>\n </Card>\n )}\n </div>\n );\n}\n\n/**\n * Het volledige productformulier. Sinds 7.8 ook buiten dit tabblad in gebruik: het tandwiel bij een\n * product op de pagina opent hetzelfde formulier in een zijpaneel. Bewust hergebruik en geen tweede,\n * uitgeklede versie — twee formulieren voor één product lopen gegarandeerd uit elkaar, en dan hangt\n * het van je route af welke velden je kunt bereiken.\n */\n/**\n * Voorraadstand van een product voor de beheerlijst: het laagste getal dat er te melden valt.\n * `null` = niet bijgehouden of onbeperkt. Zonder opties telt `stock`, met opties de varianten.\n */\nfunction voorraadStand(p: Product): number | null {\n if (p.trackStock !== true) return null;\n if ((p.options ?? []).length === 0) return p.stock ?? null;\n const getallen = (p.variants ?? []).filter((v) => v.stock !== undefined && v.active !== false).map((v) => v.stock as number);\n return getallen.length > 0 ? Math.min(...getallen) : null;\n}\n\nexport function ProductForm({\n siteId,\n product,\n categories,\n onDeleted,\n onDuplicated,\n onToast,\n confirm,\n}: {\n siteId: string;\n product: StoredProduct;\n categories: StoredCategory[];\n onDeleted: () => void;\n /** Optioneel: na dupliceren de kopie selecteren (het zijpaneel doet dit niet). */\n onDuplicated?: (id: string) => void;\n onToast: (t: { title: string; description?: string; tone?: \"default\" | \"success\" | \"error\" }) => void;\n confirm: (opts: ConfirmOptions) => Promise<boolean>;\n}) {\n const [def, setDef] = useState<Product>(product);\n /**\n * Waarschuwen bij niet-opgeslagen werk (7.18). Het formulier heeft geen autosave (een product\n * gaat meteen live), dus wie naar een ander product klikte of het tabblad sloot, was zijn\n * wijzigingen kwijt zonder enige melding.\n */\n const vuil = JSON.stringify(def) !== JSON.stringify(product);\n useEffect(() => {\n if (!vuil) return;\n const waarschuw = (e: BeforeUnloadEvent) => e.preventDefault();\n window.addEventListener(\"beforeunload\", waarschuw);\n return () => window.removeEventListener(\"beforeunload\", waarschuw);\n }, [vuil]);\n const [priceInput, setPriceInput] = useState(centsToEuroInput(product.priceCents));\n const [compareInput, setCompareInput] = useState(centsToEuroInput(product.compareAtCents));\n\n const variantKeys = useMemo(() => allVariantKeys(def.options ?? []), [def.options]);\n\n function setOption(i: number, patch: Partial<ProductOption>) {\n setDef((p) => {\n const options = [...(p.options ?? [])];\n options[i] = { ...options[i], ...patch };\n return { ...p, options };\n });\n }\n\n /**\n * Zet \"eigen foto per waarde\" op één as. Bewust exclusief: het hoofdbeeld op de detailpagina kan\n * maar één keuze volgen, dus aanzetten op de ene as zet hem uit op de andere. Zonder die regel\n * krijg je een product waarbij niemand kan uitleggen welke foto er verschijnt.\n *\n * De vlag wordt op ALLE assen expliciet gezet (ook de uitgezette), zodat de oude naamregel niet\n * alsnog een tweede as aan het beeld hangt.\n */\n function zetBeeldAs(index: number, aan: boolean) {\n setDef((p) => ({\n ...p,\n options: (p.options ?? []).map((o, j) => ({ ...o, imagePerValue: j === index ? aan : false })),\n }));\n }\n\n function variantFor(key: string): ProductVariant | undefined {\n return (def.variants ?? []).find((v) => v.key === key);\n }\n\n function patchVariant(key: string, patch: Partial<ProductVariant>) {\n setDef((p) => {\n const variants = [...(p.variants ?? [])];\n const idx = variants.findIndex((v) => v.key === key);\n if (idx >= 0) variants[idx] = { ...variants[idx], ...patch };\n else variants.push({ key, active: true, ...patch });\n return { ...p, variants };\n });\n }\n\n /** Kopie van dit product maken (nieuwe slug + \"(kopie)\"), als concept zodat je rustig aanpast. */\n async function dupliceer() {\n try {\n const kopie: Product = {\n ...def,\n name: `${def.name} (kopie)`,\n slug: `${def.slug}-kopie`.slice(0, 120),\n status: \"draft\",\n order: (def.order ?? 0) + 1,\n };\n const id = await saveProductDoc(siteId, undefined, kopie);\n onToast({ title: \"Product gedupliceerd\", description: \"De kopie staat als concept klaar.\", tone: \"success\" });\n onDuplicated?.(id);\n } catch (e) {\n onToast({ title: \"Dupliceren mislukt\", description: e instanceof Error ? e.message : undefined, tone: \"error\" });\n }\n }\n\n async function save() {\n const db = getDb();\n if (!db) return;\n // Alleen varianten bewaren die iets toevoegen (afwijkende prijs/voorraad/sku of inactief),\n // en geen wees-varianten van verwijderde optiewaarden.\n const validKeys = new Set(variantKeys);\n const cleanVariants = (def.variants ?? []).filter(\n (v) =>\n validKeys.has(v.key) &&\n (v.priceCents !== undefined || v.stock !== undefined || v.sku !== undefined || v.active === false),\n );\n // Onvolledige personalisatie-regels (zonder label) niet opslaan.\n const cleanPersonalization = (def.personalization ?? []).filter((f) => f.label.trim().length > 0);\n /**\n * Wees-foto's opruimen, net als bij de varianten hierboven: hernoem of verwijder je een\n * optiewaarde, dan bleef de foto onder de oude naam in het document staan. Onzichtbaar, nooit\n * meer getoond, en hij dook weer op zodra iemand toevallig dezelfde waarde opnieuw typte.\n * Alleen de foto's van waarden die nog bestaan blijven over; staat het vinkje uit, dan laten\n * we ze met rust, zodat weer aanzetten je werk teruggeeft.\n */\n const cleanOptions = (def.options ?? []).map((o) => {\n if (!o.images || !optieHeeftBeeldPerWaarde(o)) return o;\n const images = Object.fromEntries(Object.entries(o.images).filter(([waarde]) => o.values.includes(waarde)));\n return { ...o, images: Object.keys(images).length > 0 ? images : undefined };\n });\n const parsed = ProductSchema.safeParse({\n ...def,\n options: cleanOptions,\n variants: cleanVariants,\n personalization: cleanPersonalization.length > 0 ? cleanPersonalization : undefined,\n });\n if (!parsed.success) {\n onToast({ title: \"Ongeldig product\", description: parsed.error.issues[0]?.message, tone: \"error\" });\n return;\n }\n // Via de callable (7.9): de server valideert, auditeert en — belangrijker — pingt de\n // products-cachetag, zodat de wijziging ook op de LIVE shoppagina's staat en niet alleen in\n // Firestore. Zie productClient.ts, ook voor de terugval op een oudere backend.\n await saveProductDoc(siteId, product.id, parsed.data);\n onToast({ title: \"Product opgeslagen\", tone: \"success\" });\n }\n\n async function remove() {\n const ok = await confirm({\n title: `\"${product.name}\" verwijderen?`,\n description: \"Bestaande bestellingen behouden hun eigen kopie van naam en prijs.\",\n tone: \"danger\",\n });\n if (!ok) return;\n await removeProduct(siteId, product.id);\n onDeleted();\n }\n\n return (\n <Card>\n <div className=\"grid gap-4\">\n <div className=\"grid gap-4 md:grid-cols-2\">\n <Field label=\"Naam\">\n <Input\n value={def.name}\n onChange={(e) => setDef((p) => ({ ...p, name: e.target.value, slug: slugify(e.target.value) || p.slug }))}\n />\n </Field>\n <Field label=\"Slug\" hint=\"Product-URL: /product?p=<slug>\">\n <Input value={def.slug} onChange={(e) => setDef((p) => ({ ...p, slug: slugify(e.target.value) }))} />\n </Field>\n </div>\n\n <div className=\"grid gap-4 md:grid-cols-3\">\n <Field label=\"Prijs (€, incl. btw)\">\n <Input\n value={priceInput}\n onChange={(e) => {\n setPriceInput(e.target.value);\n const c = eurosToCents(e.target.value);\n if (c !== null) setDef((p) => ({ ...p, priceCents: c }));\n }}\n />\n </Field>\n <Field label=\"Van-prijs (€, optioneel)\" hint=\"Doorgestreept naast de prijs\">\n <Input\n value={compareInput}\n onChange={(e) => {\n setCompareInput(e.target.value);\n const c = eurosToCents(e.target.value);\n setDef((p) => ({ ...p, compareAtCents: c ?? undefined }));\n }}\n />\n </Field>\n <Field label=\"Btw-tarief\">\n <Select\n value={String(def.vatRate ?? 21)}\n onValueChange={(v) => setDef((p) => ({ ...p, vatRate: Number(v) as 21 | 9 | 0 }))}\n options={[\n { value: \"21\", label: \"21%\" },\n { value: \"9\", label: \"9%\" },\n { value: \"0\", label: \"0%\" },\n ]}\n />\n </Field>\n </div>\n\n <Field label=\"Omschrijving\">\n <Textarea rows={4} value={def.description ?? \"\"} onChange={(e) => setDef((p) => ({ ...p, description: e.target.value }))} />\n </Field>\n\n <Field label=\"Afbeeldingen\" hint=\"Eerste afbeelding is het hoofdbeeld\">\n <div className=\"flex flex-wrap items-center gap-3\">\n {(def.images ?? []).map((url, i) => (\n <div key={url + i} className=\"relative\">\n <img src={url} alt=\"\" className={`h-16 w-16 rounded-lg object-cover ${i === 0 ? \"ring-2 ring-brand-500\" : \"\"}`} />\n {i === 0 && (\n <span className=\"absolute -bottom-2 left-1/2 -translate-x-1/2 rounded-full bg-brand-500 px-1.5 text-[10px] text-white\">hoofd</span>\n )}\n <button\n type=\"button\"\n aria-label=\"Verwijder afbeelding\"\n className=\"absolute -right-2 -top-2 grid h-5 w-5 place-items-center rounded-full bg-slate-700 text-xs text-white\"\n onClick={() => setDef((p) => ({ ...p, images: (p.images ?? []).filter((_, j) => j !== i) }))}\n >\n ✕\n </button>\n {/* Herschikken: de eerste afbeelding is het hoofdbeeld, en dat was alleen te\n veranderen door alles te wissen en opnieuw te kiezen. */}\n {i > 0 && (\n <button\n type=\"button\"\n aria-label=\"Naar voren verplaatsen\"\n title={i === 1 ? \"Maak hoofdbeeld\" : \"Naar voren\"}\n className=\"absolute -left-2 -top-2 grid h-5 w-5 place-items-center rounded-full bg-slate-700 text-xs text-white\"\n onClick={() =>\n setDef((p) => {\n const imgs = [...(p.images ?? [])];\n [imgs[i - 1], imgs[i]] = [imgs[i], imgs[i - 1]];\n return { ...p, images: imgs };\n })\n }\n >\n ←\n </button>\n )}\n </div>\n ))}\n <MediaPickerButton\n siteId={siteId}\n label=\"+ Afbeelding\"\n onSelect={(m) => setDef((p) => ({ ...p, images: [...(p.images ?? []), m.url] }))}\n />\n </div>\n </Field>\n\n <Field label=\"Categorieën\">\n <div className=\"flex flex-wrap gap-2\">\n {categories.map((c) => {\n const on = (def.categoryIds ?? []).includes(c.id);\n return (\n <button\n key={c.id}\n type=\"button\"\n onClick={() =>\n setDef((p) => ({\n ...p,\n categoryIds: on ? (p.categoryIds ?? []).filter((x) => x !== c.id) : [...(p.categoryIds ?? []), c.id],\n }))\n }\n className={`rounded-full border px-3 py-1 text-sm ${\n on ? \"border-brand-500 bg-brand-50 text-brand-700\" : \"border-slate-200 hover:bg-slate-50\"\n }`}\n >\n {on ? \"✓ \" : \"\"}\n {c.name}\n </button>\n );\n })}\n {categories.length === 0 && <span className=\"text-sm text-slate-500\">Maak eerst een categorie aan.</span>}\n </div>\n </Field>\n\n {/* Opties & varianten */}\n <Field\n label=\"Opties (bv. Maat, Kleur)\"\n hint=\"Max. 3. Waarden gescheiden door komma's. Combinaties kun je hieronder per stuk aanpassen.\"\n >\n <div className=\"grid gap-2\">\n {(def.options ?? []).map((opt, i) => (\n <div key={i} className=\"grid gap-2 rounded-lg border border-slate-100 p-2\">\n <div className=\"flex items-center gap-2\">\n <Input\n className=\"w-36\"\n placeholder=\"Naam (bv. Maat)\"\n value={opt.name}\n onChange={(e) => setOption(i, { name: e.target.value })}\n />\n <Input\n placeholder=\"Waarden, bv: 56, 62, 68, 74\"\n value={opt.values.join(\", \")}\n onChange={(e) =>\n setOption(i, { values: e.target.value.split(\",\").map((s) => s.trim()).filter(Boolean) })\n }\n />\n <Button\n size=\"sm\"\n variant=\"ghost\"\n onClick={() => setDef((p) => ({ ...p, options: (p.options ?? []).filter((_, j) => j !== i) }))}\n >\n ✕\n </Button>\n </div>\n {/*\n Foto per waarde (5.6, instelbaar sinds 7.23). Tot 7.23 hing dit aan de naam van\n de as: alleen \"Kleur\" kreeg fotoknoppen. Dat brak zodra iemand zijn as \"Print\" of\n \"Dessin\" noemde, en hernoemen liet de foto's onzichtbaar achter. Nu is het een\n vinkje, en het staat op élke as — met de oude naamregel als beginstand, zodat\n bestaande producten er hetzelfde bijstaan.\n */}\n <label className=\"flex w-fit items-center gap-2 text-xs text-slate-600\">\n <input\n type=\"checkbox\"\n checked={optieHeeftBeeldPerWaarde(opt)}\n onChange={(e) => zetBeeldAs(i, e.target.checked)}\n />\n Elke waarde een eigen foto\n <span className=\"text-slate-400\">— het hoofdbeeld wisselt mee met deze keuze</span>\n </label>\n {optieHeeftBeeldPerWaarde(opt) && opt.values.length > 0 && (\n <div className=\"flex flex-wrap items-center gap-2\">\n {opt.values.map((value) => {\n const url = opt.images?.[value];\n return (\n <span key={value} className=\"flex items-center gap-1 rounded-full border border-slate-200 px-2 py-1 text-xs\">\n {url && <img src={url} alt=\"\" className=\"h-6 w-6 rounded object-cover\" />}\n {value}\n <MediaPickerButton\n siteId={siteId}\n label={url ? \"↻\" : \"+ foto\"}\n onSelect={(m) =>\n setOption(i, { images: { ...(opt.images ?? {}), [value]: m.url } })\n }\n />\n {url && (\n <button\n type=\"button\"\n aria-label={`Verwijder foto van ${value}`}\n className=\"text-slate-400 hover:text-slate-700\"\n onClick={() => {\n const images = { ...(opt.images ?? {}) };\n delete images[value];\n setOption(i, { images });\n }}\n >\n ✕\n </button>\n )}\n </span>\n );\n })}\n </div>\n )}\n </div>\n ))}\n {(def.options ?? []).length < 3 && (\n <Button\n size=\"sm\"\n variant=\"secondary\"\n className=\"w-fit\"\n onClick={() => setDef((p) => ({ ...p, options: [...(p.options ?? []), { name: \"\", values: [] }] }))}\n >\n + Optie\n </Button>\n )}\n </div>\n </Field>\n\n {variantKeys.length > 0 && variantKeys.length <= 60 && (\n <Field label=\"Varianten\" hint=\"Leeg = standaardprijs / onbeperkte voorraad\">\n <div className=\"overflow-x-auto\">\n <table className=\"w-full text-sm\">\n <thead>\n <tr className=\"text-left text-xs text-slate-500\">\n <th className=\"py-1 pr-2\">Combinatie</th>\n <th className=\"py-1 pr-2\">Prijs (€)</th>\n {def.trackStock && <th className=\"py-1 pr-2\">Voorraad</th>}\n <th className=\"py-1 pr-2\">Artikelnr.</th>\n <th className=\"py-1 pr-2\">Leverbaar</th>\n </tr>\n </thead>\n <tbody>\n {variantKeys.map((key) => {\n const v = variantFor(key);\n return (\n <tr key={key} className=\"border-t border-slate-100\">\n <td className=\"py-1 pr-2\">{key.split(\"|\").join(\" · \")}</td>\n <td className=\"py-1 pr-2\">\n {/* Native uncontrolled inputs: geen cursor-gedoe bij \"13,\" tijdens typen. */}\n <input\n className=\"w-24 rounded-lg border border-slate-200 px-2 py-1\"\n placeholder={centsToEuroInput(def.priceCents)}\n defaultValue={centsToEuroInput(v?.priceCents)}\n onChange={(e) => {\n const c = eurosToCents(e.target.value);\n patchVariant(key, { priceCents: c ?? undefined });\n }}\n />\n </td>\n {def.trackStock && (\n <td className=\"py-1 pr-2\">\n <input\n className=\"w-20 rounded-lg border border-slate-200 px-2 py-1\"\n type=\"number\"\n min={0}\n placeholder=\"∞\"\n defaultValue={v?.stock ?? \"\"}\n onChange={(e) => {\n const n = e.target.value === \"\" ? undefined : Math.max(0, Number(e.target.value) || 0);\n patchVariant(key, { stock: n });\n }}\n />\n </td>\n )}\n <td className=\"py-1 pr-2\">\n {/* SKU stond al in het schema maar was nergens in te vullen — terwijl\n het juist per variant nodig is (voor je eigen administratie/pakbon). */}\n <input\n className=\"w-28 rounded-lg border border-slate-200 px-2 py-1\"\n placeholder=\"—\"\n defaultValue={v?.sku ?? \"\"}\n onChange={(e) => patchVariant(key, { sku: e.target.value.trim() || undefined })}\n />\n </td>\n <td className=\"py-1 pr-2\">\n <Switch checked={v?.active !== false} onCheckedChange={(on) => patchVariant(key, { active: on })} />\n </td>\n </tr>\n );\n })}\n </tbody>\n </table>\n </div>\n </Field>\n )}\n {variantKeys.length > 60 && (\n <p className=\"text-sm text-amber-600\">\n {variantKeys.length} combinaties — te veel om per stuk te tonen. Beperk het aantal optie-waarden.\n </p>\n )}\n\n {/* Personalisatie (5.6): eigen naam/logo van de klant, met optionele meerprijs.\n Sinds 7.19 met een eigen aan/uit-schakelaar: uitzetten bewaart de velden. */}\n <div className=\"rounded-xl border border-slate-200 p-3\">\n <Switch\n checked={def.personalizationEnabled !== false}\n onCheckedChange={(v) =>\n setDef((p) => ({\n ...p,\n personalizationEnabled: v,\n // Bij het aanzetten meteen één leeg veld klaarzetten, anders staat de klant voor\n // een lege sectie en lijkt de schakelaar niets te doen.\n personalization:\n v && (p.personalization ?? []).length === 0\n ? [{ key: \"naam\", label: \"Naam\", type: \"text\", required: false, maxLength: 20, priceCents: 0 }]\n : p.personalization,\n }))\n }\n label=\"Persoonlijk maken aanzetten voor dit product\"\n />\n {def.personalizationEnabled === false && (def.personalization?.length ?? 0) > 0 && (\n <p className=\"mt-1 text-xs text-slate-500\">\n Staat uit — de klant ziet geen invulveld. Je {def.personalization!.length === 1 ? \"instelling blijft\" : \"instellingen blijven\"} bewaard\n voor als je het weer aanzet.\n </p>\n )}\n\n {def.personalizationEnabled !== false && (\n <Field\n label=\"Personalisatie (eigen tekst of afbeelding)\"\n hint=\"Max. 3 velden. De klant vult dit in op de productpagina; de meerprijs telt automatisch mee.\"\n >\n <div className=\"grid gap-2\">\n {(def.personalization ?? []).map((f, i) => {\n const setPers = (patch: Partial<PersonalizationField>) =>\n setDef((p) => {\n const personalization = [...(p.personalization ?? [])];\n personalization[i] = { ...personalization[i], ...patch };\n return { ...p, personalization };\n });\n return (\n <div key={i} className=\"grid gap-2 rounded-lg border border-slate-100 p-2 md:grid-cols-[1fr_130px_90px_110px_auto]\">\n <Input\n placeholder='Label (bv. \"Naam van het kindje\")'\n value={f.label}\n onChange={(e) =>\n setPers({\n label: e.target.value,\n key: f.key || e.target.value.toLowerCase().replace(/[^a-z0-9]+/g, \"_\").replace(/^_+|_+$/g, \"\").slice(0, 40) || `veld${i + 1}`,\n })\n }\n />\n <Select\n value={f.type}\n onValueChange={(v) => setPers({ type: v as \"text\" | \"image\" })}\n options={[\n { value: \"text\", label: \"Tekst\" },\n { value: \"image\", label: \"Afbeelding\" },\n ]}\n />\n {f.type === \"text\" ? (\n <Input\n type=\"number\"\n title=\"Max. tekens\"\n min={1}\n max={200}\n value={f.maxLength ?? 40}\n onChange={(e) => setPers({ maxLength: Math.max(1, Math.min(200, Number(e.target.value) || 40)) })}\n />\n ) : (\n <span className=\"self-center text-xs text-slate-400\">jpg/png/webp</span>\n )}\n <input\n className=\"rounded-lg border border-slate-200 px-2 py-1\"\n placeholder=\"Meerprijs €\"\n defaultValue={f.priceCents ? centsToEuroInput(f.priceCents) : \"\"}\n onChange={(e) => setPers({ priceCents: eurosToCents(e.target.value) ?? 0 })}\n />\n <div className=\"flex items-center gap-2\">\n <Switch checked={f.required === true} onCheckedChange={(v) => setPers({ required: v })} label=\"Verplicht\" />\n <Button\n size=\"sm\"\n variant=\"ghost\"\n onClick={() => setDef((p) => ({ ...p, personalization: (p.personalization ?? []).filter((_, j) => j !== i) }))}\n >\n ✕\n </Button>\n </div>\n {/* Uitleg onder het veld in de winkel. Bestond al in het schema en werd al\n gerenderd op de productpagina, maar was hier nooit in te vullen. */}\n <input\n className=\"col-span-full rounded-lg border border-slate-200 px-2 py-1 text-sm\"\n placeholder=\"Uitleg voor de klant (optioneel), bv. 'Max. 12 tekens — komt op de voorkant'\"\n value={f.helpText ?? \"\"}\n onChange={(e) => setPers({ helpText: e.target.value })}\n />\n </div>\n );\n })}\n {(def.personalization ?? []).length < 3 && (\n <Button\n size=\"sm\"\n variant=\"secondary\"\n className=\"w-fit\"\n onClick={() =>\n setDef((p) => ({\n ...p,\n personalization: [\n ...(p.personalization ?? []),\n { key: `veld${(p.personalization ?? []).length + 1}`, label: \"\", type: \"text\", required: false, maxLength: 40, priceCents: 0 },\n ],\n }))\n }\n >\n + Personalisatie-veld\n </Button>\n )}\n </div>\n </Field>\n )}\n </div>\n\n <div className=\"flex flex-wrap items-center gap-6\">\n <Field label=\"Status\">\n <Select\n value={def.status ?? \"draft\"}\n onValueChange={(v) => setDef((p) => ({ ...p, status: v as Product[\"status\"] }))}\n options={STATUS_OPTIONS}\n />\n </Field>\n <Field label=\"Volgorde\">\n <Input\n type=\"number\"\n className=\"w-24\"\n value={def.order ?? 0}\n onChange={(e) => setDef((p) => ({ ...p, order: Math.max(0, Number(e.target.value) || 0) }))}\n />\n </Field>\n <Switch checked={def.featured === true} onCheckedChange={(v) => setDef((p) => ({ ...p, featured: v }))} label=\"Uitgelicht\" />\n <Switch checked={def.trackStock === true} onCheckedChange={(v) => setDef((p) => ({ ...p, trackStock: v }))} label=\"Voorraad bijhouden\" />\n {def.trackStock && (def.options ?? []).length === 0 && (\n <Field label=\"Voorraad\">\n <Input\n type=\"number\"\n className=\"w-24\"\n placeholder=\"∞\"\n value={def.stock ?? \"\"}\n onChange={(e) =>\n setDef((p) => ({ ...p, stock: e.target.value === \"\" ? undefined : Math.max(0, Number(e.target.value) || 0) }))\n }\n />\n </Field>\n )}\n </div>\n\n <div className=\"flex items-center justify-between border-t border-slate-100 pt-4\">\n <div className=\"flex items-center gap-3\">\n <Button onClick={save}>Opslaan</Button>\n <span className=\"text-sm text-slate-500\">{formatCents(def.priceCents)}</span>\n {vuil && <span className=\"text-sm font-medium text-amber-700\">• niet opgeslagen</span>}\n </div>\n <div className=\"flex items-center gap-2\">\n {/* Dupliceren: een variant van een bestaand product maken was overtypen. */}\n <Button variant=\"secondary\" onClick={dupliceer}>Dupliceren</Button>\n <Button variant=\"danger\" onClick={remove}>Verwijderen</Button>\n </div>\n </div>\n </div>\n </Card>\n );\n}\n"],"mappings":"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","names":[]}
@@ -12,7 +12,7 @@ import { useShopStrings } from "../strings.js";
12
12
  import {
13
13
  fetchActiveProducts,
14
14
  fetchProductBySlug,
15
- kleurOptie,
15
+ beeldOptie,
16
16
  readQueryParam,
17
17
  uploadPersonalizationCall,
18
18
  useShopSettings,
@@ -144,19 +144,19 @@ function ProductDetailRender({ block, siteId }) {
144
144
  if (!product) return 0;
145
145
  return productPriceCents(product, variantKey) + persSurcharge;
146
146
  }, [product, variantKey, persSurcharge]);
147
- const kleur = product ? kleurOptie(product) : null;
148
- const kleurIdx = kleur && product ? (product.options ?? []).findIndex((o) => o === kleur) : -1;
149
- const gekozenKleur = kleurIdx >= 0 ? choices[kleurIdx] : void 0;
150
- const kleurImage = gekozenKleur ? kleur?.images?.[gekozenKleur] : void 0;
151
- const mainImage = manualImage ?? kleurImage ?? product?.images?.[0];
147
+ const beeldAs = product ? beeldOptie(product) : null;
148
+ const beeldIdx = beeldAs && product ? (product.options ?? []).findIndex((o) => o === beeldAs) : -1;
149
+ const gekozenWaarde = beeldIdx >= 0 ? choices[beeldIdx] : void 0;
150
+ const waardeImage = gekozenWaarde ? beeldAs?.images?.[gekozenWaarde] : void 0;
151
+ const mainImage = manualImage ?? waardeImage ?? product?.images?.[0];
152
152
  const gallery = useMemo(() => {
153
153
  if (!product) return [];
154
154
  const out = (product.images ?? []).map((url) => ({ url }));
155
- for (const [value, url] of Object.entries(kleur?.images ?? {})) {
155
+ for (const [value, url] of Object.entries(beeldAs?.images ?? {})) {
156
156
  if (!out.some((g) => g.url === url)) out.push({ url, label: value });
157
157
  }
158
158
  return out;
159
- }, [product, kleur]);
159
+ }, [product, beeldAs]);
160
160
  const stockValue = options.length === 0 ? product?.stock : variant?.stock;
161
161
  const outOfStock = Boolean(product?.trackStock && stockValue !== void 0 && stockValue <= 0);
162
162
  const lowStock = Boolean(product?.trackStock && stockValue !== void 0 && stockValue > 0 && stockValue <= 5);
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../src/modules/shop/blocks/ProductDetail.tsx"],"sourcesContent":["\"use client\";\n\nimport { useEffect, useMemo, useState } from \"react\";\nimport type { z } from \"zod\";\nimport { formatCents, productPriceCents, variantKeyFor } from \"../../../schema/shopGeld\";\nimport type { ProductDetailBlockSchema, PersonalizationField } from \"../../../schema/shop\";\nimport { defineBlock, type BlockRenderProps } from \"../../../blocks/registry\";\nimport { useBlockData } from \"../../../blocks/context\";\nimport { Card, Heading, Text } from \"../../../blocks/ui\";\nimport { Selector } from \"@astryxdesign/core/Selector\";\nimport { cartAdd } from \"../cartStore\";\nimport { AantalInvoer } from \"./AantalInvoer\";\nimport { useShopStrings } from \"../strings\";\nimport {\n fetchActiveProducts,\n fetchProductBySlug,\n kleurOptie,\n readQueryParam,\n uploadPersonalizationCall,\n useShopSettings,\n variantLabelFor,\n type StoredProduct,\n} from \"../lib\";\nimport type { ShopPageData } from \"../types\";\nimport { ProductCard } from \"./ProductGrid\";\n\ntype Blk = z.infer<typeof ProductDetailBlockSchema>;\ntype PersValue = Record<string, { text?: string; imagePath?: string; fileName?: string }>;\n\n/** Personalisatie-invoer (eigen tekst of afbeelding) onder de optie-selects. */\nfunction PersonalizationInputs({\n fields,\n siteId,\n productId,\n value,\n currency,\n onChange,\n}: {\n fields: PersonalizationField[];\n siteId: string;\n productId: string;\n value: PersValue;\n currency: string;\n onChange: (next: PersValue) => void;\n}) {\n const t = useShopStrings();\n const [uploading, setUploading] = useState<string | null>(null);\n const [error, setError] = useState<string | null>(null);\n\n return (\n <div className=\"xicms-shop-personalization\" style={{ display: \"grid\", gap: \"0.75rem\" }}>\n {fields.map((f) => {\n const v = value[f.key] ?? {};\n const label = (\n <span>\n {f.label}\n {f.required ? \" *\" : \"\"}\n {f.priceCents > 0 && (\n <span style={{ opacity: 0.7, marginLeft: 6, fontSize: \"0.9em\" }}>\n {t(\"personalizationSurcharge\", { bedrag: formatCents(f.priceCents, currency) })}\n </span>\n )}\n </span>\n );\n if (f.type === \"text\") {\n return (\n <label key={f.key} style={{ display: \"grid\", gap: 4 }}>\n <span style={{ fontSize: \"0.9rem\" }}>{label}</span>\n <input\n className=\"xicms-input\"\n type=\"text\"\n maxLength={f.maxLength}\n value={v.text ?? \"\"}\n onChange={(e) => onChange({ ...value, [f.key]: { text: e.target.value } })}\n />\n <span style={{ fontSize: \"0.75rem\", opacity: 0.6 }}>\n {f.helpText ? `${f.helpText} · ` : \"\"}\n {(v.text ?? \"\").length}/{f.maxLength}\n </span>\n </label>\n );\n }\n return (\n <div key={f.key} style={{ display: \"grid\", gap: 4 }}>\n <span style={{ fontSize: \"0.9rem\" }}>{label}</span>\n <div style={{ display: \"flex\", alignItems: \"center\", gap: 10, flexWrap: \"wrap\" }}>\n <label className=\"xicms-btn xicms-btn--secondary\" style={{ cursor: \"pointer\" }}>\n {uploading === f.key\n ? t(\"personalizationUploading\")\n : v.imagePath\n ? t(\"personalizationUploaded\")\n : t(\"personalizationUpload\")}\n <input\n type=\"file\"\n accept=\"image/jpeg,image/png,image/webp\"\n style={{ display: \"none\" }}\n onChange={async (e) => {\n const file = e.target.files?.[0];\n if (!file) return;\n setError(null);\n setUploading(f.key);\n try {\n const { path } = await uploadPersonalizationCall(siteId, productId, file);\n onChange({ ...value, [f.key]: { imagePath: path, fileName: file.name } });\n } catch (err) {\n setError(err instanceof Error && err.message === \"TOO_BIG\" ? t(\"personalizationTooBig\") : t(\"personalizationFailed\"));\n } finally {\n setUploading(null);\n }\n }}\n />\n </label>\n {v.fileName && <span style={{ fontSize: \"0.85rem\", opacity: 0.75 }}>{v.fileName}</span>}\n </div>\n {f.helpText && <span style={{ fontSize: \"0.75rem\", opacity: 0.6 }}>{f.helpText}</span>}\n </div>\n );\n })}\n {error && <Text role=\"alert\" style={{ color: \"var(--color-error, #c0392b)\" }}>{error}</Text>}\n </div>\n );\n}\n\n/**\n * Productdetail — één generieke pagina met dit block toont elk product via `?p=<slug>`\n * (of een vast product via `block.slug`). Optie-selects met per-kleur-beeldwissel,\n * personalisatie (eigen naam/logo), voorraad-signalen en \"Ook leuk\"-rij.\n */\nfunction ProductDetailRender({ block, siteId }: BlockRenderProps<Blk>) {\n const settings = useShopSettings();\n const t = useShopStrings();\n const serverData = useBlockData<ShopPageData>(\"shop\");\n const [product, setProduct] = useState<StoredProduct | null | undefined>(\n serverData && \"product\" in serverData ? serverData.product : undefined,\n );\n const [related, setRelated] = useState<StoredProduct[]>([]);\n const [choices, setChoices] = useState<Record<number, string>>({});\n const [personalization, setPersonalization] = useState<PersValue>({});\n const [qty, setQty] = useState(1);\n const [manualImage, setManualImage] = useState<string | null>(null);\n const [added, setAdded] = useState(false);\n\n useEffect(() => {\n if (serverData && \"product\" in serverData) return; // SSR-data aanwezig\n const slug = block.slug || readQueryParam(\"p\") || \"\";\n if (!slug) {\n setProduct(null);\n return;\n }\n void fetchProductBySlug(siteId, slug).then(setProduct).catch((e) => {\n console.warn(\"[xicms] product laden mislukt:\", e);\n setProduct(null);\n });\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [siteId, block.slug]);\n\n // \"Ook leuk\": producten uit dezelfde categorie (server-set hergebruiken waar mogelijk).\n useEffect(() => {\n if (!block.showRelated || !product) return;\n const pick = (all: StoredProduct[]) =>\n setRelated(\n all\n .filter((x) => x.id !== product.id && x.categoryIds?.some((c) => product.categoryIds?.includes(c)))\n .slice(0, 4),\n );\n if (serverData?.products) pick(serverData.products);\n else void fetchActiveProducts(siteId).then(pick).catch(() => setRelated([]));\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [product?.id, block.showRelated, siteId]);\n\n const options = product?.options ?? [];\n // Uitgeschakeld op het product? Dan tonen we de velden niet en rekent de server ze ook niet\n // (zie resolvePersonalization) — de configuratie blijft wel bewaard voor later.\n const persFields = product?.personalizationEnabled === false ? [] : (product?.personalization ?? []);\n const allChosen = options.every((_, i) => Boolean(choices[i]));\n const variantKey = product && options.length > 0 && allChosen\n ? variantKeyFor(options.map((_, i) => choices[i]))\n : undefined;\n const variant = variantKey ? product?.variants?.find((v) => v.key === variantKey) : undefined;\n\n /** Meerprijs van ingevulde personalisatie (per stuk). */\n const persSurcharge = useMemo(\n () =>\n persFields.reduce((sum, f) => {\n const v = personalization[f.key];\n return v && (v.text || v.imagePath) ? sum + f.priceCents : sum;\n }, 0),\n [persFields, personalization],\n );\n\n const priceCents = useMemo(() => {\n if (!product) return 0;\n return productPriceCents(product, variantKey) + persSurcharge;\n }, [product, variantKey, persSurcharge]);\n\n /** Hoofdbeeld: handmatige thumbnail-keuze > per-kleur-afbeelding > eerste productfoto. */\n const kleur = product ? kleurOptie(product) : null;\n const kleurIdx = kleur && product ? (product.options ?? []).findIndex((o) => o === kleur) : -1;\n const gekozenKleur = kleurIdx >= 0 ? choices[kleurIdx] : undefined;\n const kleurImage = gekozenKleur ? kleur?.images?.[gekozenKleur] : undefined;\n const mainImage = manualImage ?? kleurImage ?? product?.images?.[0];\n\n /** Alle toonbare afbeeldingen: productfoto's + unieke per-kleur-afbeeldingen. */\n const gallery = useMemo(() => {\n if (!product) return [] as { url: string; label?: string }[];\n const out: { url: string; label?: string }[] = (product.images ?? []).map((url) => ({ url }));\n for (const [value, url] of Object.entries(kleur?.images ?? {})) {\n if (!out.some((g) => g.url === url)) out.push({ url, label: value });\n }\n return out;\n }, [product, kleur]);\n\n const stockValue = options.length === 0 ? product?.stock : variant?.stock;\n const outOfStock = Boolean(product?.trackStock && stockValue !== undefined && stockValue <= 0);\n const lowStock = Boolean(product?.trackStock && stockValue !== undefined && stockValue > 0 && stockValue <= 5);\n const variantInactive = variant !== undefined && variant.active === false;\n\n if (product === undefined) return null;\n if (product === null) {\n return (\n <Card padding={4} variant=\"muted\">\n <Text>\n {t(\"productNotFound\")} <a href={settings.shopPath}>{t(\"backToShop\")}</a>\n </Text>\n </Card>\n );\n }\n\n const persComplete = persFields.every((f) => {\n if (!f.required) return true;\n const v = personalization[f.key];\n return Boolean(v && (v.text?.trim() || v.imagePath));\n });\n\n function addToCart() {\n if (!product) return;\n const filled: PersValue = {};\n for (const f of persFields) {\n const v = personalization[f.key];\n if (v && (v.text?.trim() || v.imagePath)) filled[f.key] = v;\n }\n const persLabel = persFields\n .filter((f) => filled[f.key])\n .map((f) => `${f.label}: ${filled[f.key].text ?? filled[f.key].fileName ?? \"✓\"}`)\n .join(\" · \");\n cartAdd(\n siteId,\n {\n productId: product.id,\n slug: product.slug,\n name: product.name,\n variantKey,\n variantLabel: variantKey ? variantLabelFor(options, variantKey) : undefined,\n unitPriceCents: priceCents,\n imageUrl: mainImage ?? product.images?.[0],\n personalization: Object.keys(filled).length > 0 ? filled : undefined,\n personalizationLabel: persLabel || undefined,\n },\n qty,\n );\n setAdded(true);\n }\n\n const canOrder = (options.length === 0 || allChosen) && !outOfStock && !variantInactive && persComplete;\n\n return (\n <>\n <div\n className=\"xicms-shop-detail\"\n style={{ display: \"grid\", gap: \"2rem\", gridTemplateColumns: \"repeat(auto-fit, minmax(min(320px, 100%), 1fr))\" }}\n >\n <div className=\"xicms-shop-detail__media\">\n {mainImage ? (\n <img\n src={mainImage}\n alt={product.name}\n style={{ width: \"100%\", aspectRatio: \"1/1\", objectFit: \"cover\", borderRadius: \"var(--xicms-radius, 12px)\" }}\n />\n ) : (\n <div\n className=\"xicms-shop-detail__placeholder\"\n aria-hidden=\"true\"\n style={{ width: \"100%\", aspectRatio: \"1/1\", background: \"var(--xicms-muted, #f1f1f1)\", borderRadius: \"var(--xicms-radius, 12px)\" }}\n />\n )}\n {gallery.length > 1 && (\n <div style={{ display: \"flex\", gap: 8, marginTop: 8, flexWrap: \"wrap\" }}>\n {gallery.map((g) => (\n <button\n key={g.url}\n type=\"button\"\n title={g.label}\n onClick={() => setManualImage(g.url)}\n style={{\n padding: 0,\n border: g.url === mainImage ? \"2px solid var(--xicms-primary, #333)\" : \"2px solid transparent\",\n borderRadius: 8,\n cursor: \"pointer\",\n background: \"none\",\n }}\n >\n <img src={g.url} alt={g.label ?? \"\"} style={{ width: 64, height: 64, objectFit: \"cover\", borderRadius: 6, display: \"block\" }} />\n </button>\n ))}\n </div>\n )}\n </div>\n\n {/*\n Haken voor de bewerkmodus. De tekst van een product staat NIET in dit blok — het blok wijst\n alleen een product aan (`slug`) en haalt de rest uit de productencollectie. Zonder deze\n haken kon de editor daarom niets: het tandwiel opende een paneel met twee instellingen en\n het potlood verscheen helemaal niet, want er viel in dit blok niets te bewerken. Terecht,\n en toch precies verkeerd — je stond naar een productnaam te kijken die je niet kon wijzigen.\n `data-xicms-product` zegt om welk product het gaat, `data-xicms-product-edit` welk veld\n eronder ligt; de editor schrijft die rechtstreeks naar het productdocument.\n */}\n <div className=\"xicms-shop-detail__info\" data-xicms-product={product.id}>\n <Heading level={1} data-xicms-product-edit=\"name\">{product.name}</Heading>\n <p className=\"xicms-shop-detail__price\" style={{ fontSize: \"1.35rem\", fontWeight: 700, margin: \"0.5rem 0 1rem\" }}>\n {product.compareAtCents !== undefined && product.compareAtCents > priceCents && (\n <s style={{ opacity: 0.6, marginRight: 8, fontWeight: \"normal\" }}>\n {formatCents(product.compareAtCents, settings.currency)}\n </s>\n )}\n {formatCents(priceCents, settings.currency)}\n </p>\n\n {product.description && (\n <p className=\"xicms-shop-detail__desc\" data-xicms-product-edit=\"description\" style={{ whiteSpace: \"pre-wrap\", margin: 0 }}>{product.description}</p>\n )}\n\n <div style={{ display: \"grid\", gap: \"0.75rem\", maxWidth: 420, marginTop: \"1rem\" }}>\n {options.map((opt, i) => (\n <Selector\n key={opt.name}\n label={opt.name}\n placeholder={t(\"choose\")}\n options={opt.values}\n value={choices[i] ?? \"\"}\n onChange={(v) => {\n setChoices((prev) => ({ ...prev, [i]: (v as string) ?? \"\" }));\n setManualImage(null);\n setAdded(false);\n }}\n />\n ))}\n\n {persFields.length > 0 && (\n <PersonalizationInputs\n fields={persFields}\n siteId={siteId}\n productId={product.id}\n value={personalization}\n currency={settings.currency}\n onChange={(v) => {\n setPersonalization(v);\n setAdded(false);\n }}\n />\n )}\n\n <label className=\"xicms-shop-detail__qty\" style={{ display: \"grid\", gap: 4 }}>\n <span style={{ fontSize: \"0.9rem\" }}>{t(\"qty\")}</span>\n <AantalInvoer value={qty} onChange={setQty} min={1} max={999} style={{ width: 90 }} />\n </label>\n\n {outOfStock && <Text style={{ color: \"var(--color-error, #c0392b)\" }}>{t(\"outOfStock\")}</Text>}\n {variantInactive && <Text style={{ color: \"var(--color-error, #c0392b)\" }}>{t(\"variantUnavailable\")}</Text>}\n {lowStock && !outOfStock && (\n <Text style={{ color: \"var(--xicms-primary, #4d7fb0)\", fontWeight: 600 }}>\n {t(\"stockLeft\", { n: stockValue as number })}\n </Text>\n )}\n\n {added ? (\n <div role=\"status\" style={{ display: \"grid\", gap: 8 }}>\n <Text>{t(\"added\")}</Text>\n <div style={{ display: \"flex\", gap: 12, flexWrap: \"wrap\" }}>\n <a className=\"xicms-btn xicms-btn--primary\" href={settings.cartPath}>{t(\"toCart\")}</a>\n <a className=\"xicms-btn xicms-btn--secondary\" href={settings.shopPath}>{t(\"continueShopping\")}</a>\n </div>\n </div>\n ) : (\n <button\n type=\"button\"\n className=\"xicms-btn xicms-btn--primary\"\n disabled={!canOrder}\n onClick={addToCart}\n style={{ cursor: canOrder ? \"pointer\" : \"not-allowed\", opacity: canOrder ? 1 : 0.5, width: \"fit-content\" }}\n >\n {t(\"addToCart\")}\n </button>\n )}\n </div>\n </div>\n </div>\n\n {block.showRelated && related.length > 0 && (\n <div className=\"xicms-shop-related\" style={{ marginTop: \"3rem\" }}>\n <Heading level={2}>{t(\"related\")}</Heading>\n <div\n className=\"xicms-shop-grid__items\"\n style={{\n display: \"grid\",\n gridTemplateColumns: \"repeat(auto-fill, minmax(min(200px, calc(50% - 1rem)), 1fr))\",\n gap: \"1.25rem\",\n marginTop: \"1rem\",\n }}\n >\n {related.map((p) => (\n <ProductCard\n key={p.id}\n product={p}\n siteId={siteId}\n currency={settings.currency}\n detailPath={settings.detailPath}\n cartPath={settings.cartPath}\n t={t}\n />\n ))}\n </div>\n </div>\n )}\n </>\n );\n}\n\nexport const productDetailBlock = defineBlock({\n type: \"productDetail\",\n Render: ProductDetailRender,\n label: \"Productdetail\",\n icon: \"🏷️\",\n defaults: () => ({ showRelated: true }),\n});\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../../../src/modules/shop/blocks/ProductDetail.tsx"],"sourcesContent":["\"use client\";\n\nimport { useEffect, useMemo, useState } from \"react\";\nimport type { z } from \"zod\";\nimport { formatCents, productPriceCents, variantKeyFor } from \"../../../schema/shopGeld\";\nimport type { ProductDetailBlockSchema, PersonalizationField } from \"../../../schema/shop\";\nimport { defineBlock, type BlockRenderProps } from \"../../../blocks/registry\";\nimport { useBlockData } from \"../../../blocks/context\";\nimport { Card, Heading, Text } from \"../../../blocks/ui\";\nimport { Selector } from \"@astryxdesign/core/Selector\";\nimport { cartAdd } from \"../cartStore\";\nimport { AantalInvoer } from \"./AantalInvoer\";\nimport { useShopStrings } from \"../strings\";\nimport {\n fetchActiveProducts,\n fetchProductBySlug,\n beeldOptie,\n readQueryParam,\n uploadPersonalizationCall,\n useShopSettings,\n variantLabelFor,\n type StoredProduct,\n} from \"../lib\";\nimport type { ShopPageData } from \"../types\";\nimport { ProductCard } from \"./ProductGrid\";\n\ntype Blk = z.infer<typeof ProductDetailBlockSchema>;\ntype PersValue = Record<string, { text?: string; imagePath?: string; fileName?: string }>;\n\n/** Personalisatie-invoer (eigen tekst of afbeelding) onder de optie-selects. */\nfunction PersonalizationInputs({\n fields,\n siteId,\n productId,\n value,\n currency,\n onChange,\n}: {\n fields: PersonalizationField[];\n siteId: string;\n productId: string;\n value: PersValue;\n currency: string;\n onChange: (next: PersValue) => void;\n}) {\n const t = useShopStrings();\n const [uploading, setUploading] = useState<string | null>(null);\n const [error, setError] = useState<string | null>(null);\n\n return (\n <div className=\"xicms-shop-personalization\" style={{ display: \"grid\", gap: \"0.75rem\" }}>\n {fields.map((f) => {\n const v = value[f.key] ?? {};\n const label = (\n <span>\n {f.label}\n {f.required ? \" *\" : \"\"}\n {f.priceCents > 0 && (\n <span style={{ opacity: 0.7, marginLeft: 6, fontSize: \"0.9em\" }}>\n {t(\"personalizationSurcharge\", { bedrag: formatCents(f.priceCents, currency) })}\n </span>\n )}\n </span>\n );\n if (f.type === \"text\") {\n return (\n <label key={f.key} style={{ display: \"grid\", gap: 4 }}>\n <span style={{ fontSize: \"0.9rem\" }}>{label}</span>\n <input\n className=\"xicms-input\"\n type=\"text\"\n maxLength={f.maxLength}\n value={v.text ?? \"\"}\n onChange={(e) => onChange({ ...value, [f.key]: { text: e.target.value } })}\n />\n <span style={{ fontSize: \"0.75rem\", opacity: 0.6 }}>\n {f.helpText ? `${f.helpText} · ` : \"\"}\n {(v.text ?? \"\").length}/{f.maxLength}\n </span>\n </label>\n );\n }\n return (\n <div key={f.key} style={{ display: \"grid\", gap: 4 }}>\n <span style={{ fontSize: \"0.9rem\" }}>{label}</span>\n <div style={{ display: \"flex\", alignItems: \"center\", gap: 10, flexWrap: \"wrap\" }}>\n <label className=\"xicms-btn xicms-btn--secondary\" style={{ cursor: \"pointer\" }}>\n {uploading === f.key\n ? t(\"personalizationUploading\")\n : v.imagePath\n ? t(\"personalizationUploaded\")\n : t(\"personalizationUpload\")}\n <input\n type=\"file\"\n accept=\"image/jpeg,image/png,image/webp\"\n style={{ display: \"none\" }}\n onChange={async (e) => {\n const file = e.target.files?.[0];\n if (!file) return;\n setError(null);\n setUploading(f.key);\n try {\n const { path } = await uploadPersonalizationCall(siteId, productId, file);\n onChange({ ...value, [f.key]: { imagePath: path, fileName: file.name } });\n } catch (err) {\n setError(err instanceof Error && err.message === \"TOO_BIG\" ? t(\"personalizationTooBig\") : t(\"personalizationFailed\"));\n } finally {\n setUploading(null);\n }\n }}\n />\n </label>\n {v.fileName && <span style={{ fontSize: \"0.85rem\", opacity: 0.75 }}>{v.fileName}</span>}\n </div>\n {f.helpText && <span style={{ fontSize: \"0.75rem\", opacity: 0.6 }}>{f.helpText}</span>}\n </div>\n );\n })}\n {error && <Text role=\"alert\" style={{ color: \"var(--color-error, #c0392b)\" }}>{error}</Text>}\n </div>\n );\n}\n\n/**\n * Productdetail — één generieke pagina met dit block toont elk product via `?p=<slug>`\n * (of een vast product via `block.slug`). Optie-selects met per-kleur-beeldwissel,\n * personalisatie (eigen naam/logo), voorraad-signalen en \"Ook leuk\"-rij.\n */\nfunction ProductDetailRender({ block, siteId }: BlockRenderProps<Blk>) {\n const settings = useShopSettings();\n const t = useShopStrings();\n const serverData = useBlockData<ShopPageData>(\"shop\");\n const [product, setProduct] = useState<StoredProduct | null | undefined>(\n serverData && \"product\" in serverData ? serverData.product : undefined,\n );\n const [related, setRelated] = useState<StoredProduct[]>([]);\n const [choices, setChoices] = useState<Record<number, string>>({});\n const [personalization, setPersonalization] = useState<PersValue>({});\n const [qty, setQty] = useState(1);\n const [manualImage, setManualImage] = useState<string | null>(null);\n const [added, setAdded] = useState(false);\n\n useEffect(() => {\n if (serverData && \"product\" in serverData) return; // SSR-data aanwezig\n const slug = block.slug || readQueryParam(\"p\") || \"\";\n if (!slug) {\n setProduct(null);\n return;\n }\n void fetchProductBySlug(siteId, slug).then(setProduct).catch((e) => {\n console.warn(\"[xicms] product laden mislukt:\", e);\n setProduct(null);\n });\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [siteId, block.slug]);\n\n // \"Ook leuk\": producten uit dezelfde categorie (server-set hergebruiken waar mogelijk).\n useEffect(() => {\n if (!block.showRelated || !product) return;\n const pick = (all: StoredProduct[]) =>\n setRelated(\n all\n .filter((x) => x.id !== product.id && x.categoryIds?.some((c) => product.categoryIds?.includes(c)))\n .slice(0, 4),\n );\n if (serverData?.products) pick(serverData.products);\n else void fetchActiveProducts(siteId).then(pick).catch(() => setRelated([]));\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [product?.id, block.showRelated, siteId]);\n\n const options = product?.options ?? [];\n // Uitgeschakeld op het product? Dan tonen we de velden niet en rekent de server ze ook niet\n // (zie resolvePersonalization) — de configuratie blijft wel bewaard voor later.\n const persFields = product?.personalizationEnabled === false ? [] : (product?.personalization ?? []);\n const allChosen = options.every((_, i) => Boolean(choices[i]));\n const variantKey = product && options.length > 0 && allChosen\n ? variantKeyFor(options.map((_, i) => choices[i]))\n : undefined;\n const variant = variantKey ? product?.variants?.find((v) => v.key === variantKey) : undefined;\n\n /** Meerprijs van ingevulde personalisatie (per stuk). */\n const persSurcharge = useMemo(\n () =>\n persFields.reduce((sum, f) => {\n const v = personalization[f.key];\n return v && (v.text || v.imagePath) ? sum + f.priceCents : sum;\n }, 0),\n [persFields, personalization],\n );\n\n const priceCents = useMemo(() => {\n if (!product) return 0;\n return productPriceCents(product, variantKey) + persSurcharge;\n }, [product, variantKey, persSurcharge]);\n\n /**\n * Hoofdbeeld: handmatige thumbnail-keuze > foto van de gekozen waarde > eerste productfoto.\n *\n * Welke as het beeld stuurt is sinds 7.23 een instelling op het product (`imagePerValue`) en\n * niet meer \"de as die toevallig Kleur heet\" — het mag dus net zo goed \"Print\" of \"Dessin\" zijn.\n */\n const beeldAs = product ? beeldOptie(product) : null;\n const beeldIdx = beeldAs && product ? (product.options ?? []).findIndex((o) => o === beeldAs) : -1;\n const gekozenWaarde = beeldIdx >= 0 ? choices[beeldIdx] : undefined;\n const waardeImage = gekozenWaarde ? beeldAs?.images?.[gekozenWaarde] : undefined;\n const mainImage = manualImage ?? waardeImage ?? product?.images?.[0];\n\n /** Alle toonbare afbeeldingen: productfoto's + de unieke foto's per optiewaarde. */\n const gallery = useMemo(() => {\n if (!product) return [] as { url: string; label?: string }[];\n const out: { url: string; label?: string }[] = (product.images ?? []).map((url) => ({ url }));\n for (const [value, url] of Object.entries(beeldAs?.images ?? {})) {\n if (!out.some((g) => g.url === url)) out.push({ url, label: value });\n }\n return out;\n }, [product, beeldAs]);\n\n const stockValue = options.length === 0 ? product?.stock : variant?.stock;\n const outOfStock = Boolean(product?.trackStock && stockValue !== undefined && stockValue <= 0);\n const lowStock = Boolean(product?.trackStock && stockValue !== undefined && stockValue > 0 && stockValue <= 5);\n const variantInactive = variant !== undefined && variant.active === false;\n\n if (product === undefined) return null;\n if (product === null) {\n return (\n <Card padding={4} variant=\"muted\">\n <Text>\n {t(\"productNotFound\")} <a href={settings.shopPath}>{t(\"backToShop\")}</a>\n </Text>\n </Card>\n );\n }\n\n const persComplete = persFields.every((f) => {\n if (!f.required) return true;\n const v = personalization[f.key];\n return Boolean(v && (v.text?.trim() || v.imagePath));\n });\n\n function addToCart() {\n if (!product) return;\n const filled: PersValue = {};\n for (const f of persFields) {\n const v = personalization[f.key];\n if (v && (v.text?.trim() || v.imagePath)) filled[f.key] = v;\n }\n const persLabel = persFields\n .filter((f) => filled[f.key])\n .map((f) => `${f.label}: ${filled[f.key].text ?? filled[f.key].fileName ?? \"✓\"}`)\n .join(\" · \");\n cartAdd(\n siteId,\n {\n productId: product.id,\n slug: product.slug,\n name: product.name,\n variantKey,\n variantLabel: variantKey ? variantLabelFor(options, variantKey) : undefined,\n unitPriceCents: priceCents,\n imageUrl: mainImage ?? product.images?.[0],\n personalization: Object.keys(filled).length > 0 ? filled : undefined,\n personalizationLabel: persLabel || undefined,\n },\n qty,\n );\n setAdded(true);\n }\n\n const canOrder = (options.length === 0 || allChosen) && !outOfStock && !variantInactive && persComplete;\n\n return (\n <>\n <div\n className=\"xicms-shop-detail\"\n style={{ display: \"grid\", gap: \"2rem\", gridTemplateColumns: \"repeat(auto-fit, minmax(min(320px, 100%), 1fr))\" }}\n >\n <div className=\"xicms-shop-detail__media\">\n {mainImage ? (\n <img\n src={mainImage}\n alt={product.name}\n style={{ width: \"100%\", aspectRatio: \"1/1\", objectFit: \"cover\", borderRadius: \"var(--xicms-radius, 12px)\" }}\n />\n ) : (\n <div\n className=\"xicms-shop-detail__placeholder\"\n aria-hidden=\"true\"\n style={{ width: \"100%\", aspectRatio: \"1/1\", background: \"var(--xicms-muted, #f1f1f1)\", borderRadius: \"var(--xicms-radius, 12px)\" }}\n />\n )}\n {gallery.length > 1 && (\n <div style={{ display: \"flex\", gap: 8, marginTop: 8, flexWrap: \"wrap\" }}>\n {gallery.map((g) => (\n <button\n key={g.url}\n type=\"button\"\n title={g.label}\n onClick={() => setManualImage(g.url)}\n style={{\n padding: 0,\n border: g.url === mainImage ? \"2px solid var(--xicms-primary, #333)\" : \"2px solid transparent\",\n borderRadius: 8,\n cursor: \"pointer\",\n background: \"none\",\n }}\n >\n <img src={g.url} alt={g.label ?? \"\"} style={{ width: 64, height: 64, objectFit: \"cover\", borderRadius: 6, display: \"block\" }} />\n </button>\n ))}\n </div>\n )}\n </div>\n\n {/*\n Haken voor de bewerkmodus. De tekst van een product staat NIET in dit blok — het blok wijst\n alleen een product aan (`slug`) en haalt de rest uit de productencollectie. Zonder deze\n haken kon de editor daarom niets: het tandwiel opende een paneel met twee instellingen en\n het potlood verscheen helemaal niet, want er viel in dit blok niets te bewerken. Terecht,\n en toch precies verkeerd — je stond naar een productnaam te kijken die je niet kon wijzigen.\n `data-xicms-product` zegt om welk product het gaat, `data-xicms-product-edit` welk veld\n eronder ligt; de editor schrijft die rechtstreeks naar het productdocument.\n */}\n <div className=\"xicms-shop-detail__info\" data-xicms-product={product.id}>\n <Heading level={1} data-xicms-product-edit=\"name\">{product.name}</Heading>\n <p className=\"xicms-shop-detail__price\" style={{ fontSize: \"1.35rem\", fontWeight: 700, margin: \"0.5rem 0 1rem\" }}>\n {product.compareAtCents !== undefined && product.compareAtCents > priceCents && (\n <s style={{ opacity: 0.6, marginRight: 8, fontWeight: \"normal\" }}>\n {formatCents(product.compareAtCents, settings.currency)}\n </s>\n )}\n {formatCents(priceCents, settings.currency)}\n </p>\n\n {product.description && (\n <p className=\"xicms-shop-detail__desc\" data-xicms-product-edit=\"description\" style={{ whiteSpace: \"pre-wrap\", margin: 0 }}>{product.description}</p>\n )}\n\n <div style={{ display: \"grid\", gap: \"0.75rem\", maxWidth: 420, marginTop: \"1rem\" }}>\n {options.map((opt, i) => (\n <Selector\n key={opt.name}\n label={opt.name}\n placeholder={t(\"choose\")}\n options={opt.values}\n value={choices[i] ?? \"\"}\n onChange={(v) => {\n setChoices((prev) => ({ ...prev, [i]: (v as string) ?? \"\" }));\n setManualImage(null);\n setAdded(false);\n }}\n />\n ))}\n\n {persFields.length > 0 && (\n <PersonalizationInputs\n fields={persFields}\n siteId={siteId}\n productId={product.id}\n value={personalization}\n currency={settings.currency}\n onChange={(v) => {\n setPersonalization(v);\n setAdded(false);\n }}\n />\n )}\n\n <label className=\"xicms-shop-detail__qty\" style={{ display: \"grid\", gap: 4 }}>\n <span style={{ fontSize: \"0.9rem\" }}>{t(\"qty\")}</span>\n <AantalInvoer value={qty} onChange={setQty} min={1} max={999} style={{ width: 90 }} />\n </label>\n\n {outOfStock && <Text style={{ color: \"var(--color-error, #c0392b)\" }}>{t(\"outOfStock\")}</Text>}\n {variantInactive && <Text style={{ color: \"var(--color-error, #c0392b)\" }}>{t(\"variantUnavailable\")}</Text>}\n {lowStock && !outOfStock && (\n <Text style={{ color: \"var(--xicms-primary, #4d7fb0)\", fontWeight: 600 }}>\n {t(\"stockLeft\", { n: stockValue as number })}\n </Text>\n )}\n\n {added ? (\n <div role=\"status\" style={{ display: \"grid\", gap: 8 }}>\n <Text>{t(\"added\")}</Text>\n <div style={{ display: \"flex\", gap: 12, flexWrap: \"wrap\" }}>\n <a className=\"xicms-btn xicms-btn--primary\" href={settings.cartPath}>{t(\"toCart\")}</a>\n <a className=\"xicms-btn xicms-btn--secondary\" href={settings.shopPath}>{t(\"continueShopping\")}</a>\n </div>\n </div>\n ) : (\n <button\n type=\"button\"\n className=\"xicms-btn xicms-btn--primary\"\n disabled={!canOrder}\n onClick={addToCart}\n style={{ cursor: canOrder ? \"pointer\" : \"not-allowed\", opacity: canOrder ? 1 : 0.5, width: \"fit-content\" }}\n >\n {t(\"addToCart\")}\n </button>\n )}\n </div>\n </div>\n </div>\n\n {block.showRelated && related.length > 0 && (\n <div className=\"xicms-shop-related\" style={{ marginTop: \"3rem\" }}>\n <Heading level={2}>{t(\"related\")}</Heading>\n <div\n className=\"xicms-shop-grid__items\"\n style={{\n display: \"grid\",\n gridTemplateColumns: \"repeat(auto-fill, minmax(min(200px, calc(50% - 1rem)), 1fr))\",\n gap: \"1.25rem\",\n marginTop: \"1rem\",\n }}\n >\n {related.map((p) => (\n <ProductCard\n key={p.id}\n product={p}\n siteId={siteId}\n currency={settings.currency}\n detailPath={settings.detailPath}\n cartPath={settings.cartPath}\n t={t}\n />\n ))}\n </div>\n </div>\n )}\n </>\n );\n}\n\nexport const productDetailBlock = defineBlock({\n type: \"productDetail\",\n Render: ProductDetailRender,\n label: \"Productdetail\",\n icon: \"🏷️\",\n defaults: () => ({ showRelated: true }),\n});\n"],"mappings":"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","names":[]}
@@ -2,6 +2,7 @@
2
2
  import { jsx, jsxs } from "react/jsx-runtime";
3
3
  import { useEffect, useMemo, useState } from "react";
4
4
  import { formatCents } from "../../../schema/shopGeld.js";
5
+ import { optieHeeftBeeldPerWaarde } from "../../../schema/shop.js";
5
6
  import { defineBlock } from "../../../blocks/registry.js";
6
7
  import { useBlockData } from "../../../blocks/context.js";
7
8
  import { Card, Heading, Text } from "../../../blocks/ui/index.js";
@@ -33,7 +34,8 @@ function isSoldOut(p) {
33
34
  function ColorSwatches({ product, onHover }) {
34
35
  const optie = kleurOptie(product);
35
36
  if (!optie) return null;
36
- const dots = optie.values.map((v) => ({ v, css: kleurNaarCss(v), img: optie.images?.[v] })).filter((d) => d.css !== null);
37
+ const metBeeld = optieHeeftBeeldPerWaarde(optie);
38
+ const dots = optie.values.map((v) => ({ v, css: kleurNaarCss(v), img: metBeeld ? optie.images?.[v] : void 0 })).filter((d) => d.css !== null);
37
39
  if (dots.length === 0) return null;
38
40
  return /* @__PURE__ */ jsx("span", { className: "xicms-shop-card__swatches", "aria-label": `Kleuren: ${optie.values.join(", ")}`, children: dots.map((d) => /* @__PURE__ */ jsx(
39
41
  "span",
@@ -68,7 +70,8 @@ function ProductCard({
68
70
  const href = `${detailPath}?p=${encodeURIComponent(product.slug)}`;
69
71
  const simple = (product.options ?? []).length === 0 && !(product.personalization ?? []).length;
70
72
  const soldOut = isSoldOut(product);
71
- const filterImage = filterColor ? kleurOptie(product)?.images?.[filterColor] : void 0;
73
+ const kleurAs = filterColor ? kleurOptie(product) : null;
74
+ const filterImage = kleurAs && optieHeeftBeeldPerWaarde(kleurAs) ? kleurAs.images?.[filterColor] : void 0;
72
75
  const image = hoverImage ?? filterImage ?? product.images?.[0];
73
76
  return (
74
77
  // Flex-kolom over de volle kaarthoogte: titel (vast 2 regels) → prijs → swatches →
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../src/modules/shop/blocks/ProductGrid.tsx"],"sourcesContent":["\"use client\";\n\nimport { useEffect, useMemo, useState } from \"react\";\nimport type { z } from \"zod\";\nimport { formatCents } from \"../../../schema/shopGeld\";\nimport type { ProductGridBlockSchema } from \"../../../schema/shop\";\nimport { defineBlock, type BlockRenderProps } from \"../../../blocks/registry\";\nimport { useBlockData } from \"../../../blocks/context\";\nimport { Card, Heading, Text } from \"../../../blocks/ui\";\nimport { cartAdd } from \"../cartStore\";\nimport { useShopStrings, type ShopT } from \"../strings\";\nimport {\n fetchActiveProducts,\n fetchShopCategories,\n kleurNaarCss,\n kleurOptie,\n readQueryParam,\n useShopSettings,\n type StoredProduct,\n type StoredShopCategory,\n} from \"../lib\";\nimport type { ShopPageData } from \"../types\";\n\ntype Blk = z.infer<typeof ProductGridBlockSchema>;\n\n/** Prijsregel met optionele doorgestreepte \"van\"-prijs en \"vanaf\" bij variant-prijzen. */\nexport function PriceLine({ product, currency }: { product: StoredProduct; currency: string }) {\n const variantPrices = (product.variants ?? [])\n .filter((v) => v.priceCents !== undefined)\n .map((v) => v.priceCents as number);\n const min = Math.min(product.priceCents, ...variantPrices);\n const hasRange = variantPrices.some((p) => p !== product.priceCents);\n return (\n <span className=\"xicms-shop-price\">\n {product.compareAtCents !== undefined && product.compareAtCents > min && (\n <s style={{ opacity: 0.6, marginRight: 6 }}>{formatCents(product.compareAtCents, currency)}</s>\n )}\n {hasRange ? `vanaf ${formatCents(min, currency)}` : formatCents(min, currency)}\n </span>\n );\n}\n\n/**\n * Uitverkocht. Zonder opties: de voorraad van het product zelf. MÉT opties: pas uitverkocht als\n * élke variant met een expliciete voorraad op 0 staat — dat was eerder helemaal niet af te lezen,\n * waardoor een volledig uitverkocht t-shirt gewoon als beschikbaar in het grid stond.\n */\nfunction isSoldOut(p: StoredProduct): boolean {\n if (p.trackStock !== true) return false;\n if ((p.options ?? []).length === 0) return p.stock !== undefined && p.stock <= 0;\n const metVoorraad = (p.variants ?? []).filter((v) => v.stock !== undefined && v.active !== false);\n return metVoorraad.length > 0 && metVoorraad.every((v) => (v.stock as number) <= 0);\n}\n\n/** Stof-stipjes; hover wisselt de kaartfoto naar de per-kleur-afbeelding (als die er is). */\nfunction ColorSwatches({ product, onHover }: { product: StoredProduct; onHover?: (url: string | null) => void }) {\n const optie = kleurOptie(product);\n if (!optie) return null;\n const dots = optie.values\n .map((v) => ({ v, css: kleurNaarCss(v), img: optie.images?.[v] }))\n .filter((d): d is { v: string; css: string; img: string | undefined } => d.css !== null);\n if (dots.length === 0) return null;\n return (\n <span className=\"xicms-shop-card__swatches\" aria-label={`Kleuren: ${optie.values.join(\", \")}`}>\n {dots.map((d) => (\n <span\n key={d.v}\n className=\"xicms-shop-card__swatch\"\n title={d.v}\n onMouseEnter={d.img && onHover ? () => onHover(d.img ?? null) : undefined}\n onMouseLeave={d.img && onHover ? () => onHover(null) : undefined}\n style={{\n display: \"inline-block\",\n width: 12,\n height: 12,\n borderRadius: 999,\n background: d.css,\n boxShadow: \"inset 0 0 0 1px rgba(0,0,0,0.18)\",\n }}\n />\n ))}\n </span>\n );\n}\n\nexport function ProductCard({\n product,\n siteId,\n currency,\n detailPath,\n cartPath,\n t,\n filterColor,\n}: {\n product: StoredProduct;\n siteId: string;\n currency: string;\n detailPath: string;\n cartPath: string;\n t: ShopT;\n /** Actief kleurfilter uit het overzicht — toont bij voorkeur de foto van die kleur. */\n filterColor?: string | null;\n}) {\n const [added, setAdded] = useState(false);\n const [hoverImage, setHoverImage] = useState<string | null>(null);\n const href = `${detailPath}?p=${encodeURIComponent(product.slug)}`;\n const simple = (product.options ?? []).length === 0 && !(product.personalization ?? []).length;\n const soldOut = isSoldOut(product);\n const filterImage = filterColor ? kleurOptie(product)?.images?.[filterColor] : undefined;\n const image = hoverImage ?? filterImage ?? product.images?.[0];\n\n return (\n // Flex-kolom over de volle kaarthoogte: titel (vast 2 regels) → prijs → swatches →\n // knop onderaan. Zo lijnen prijs/swatches/knop over álle kaarten in de rij uit, ook\n // als een productnaam één of twee regels beslaat. Alle opmaak in styles.css.\n // `data-xicms-product` maakt de kaart in de bewerkmodus aanwijsbaar als PRODUCT (potlood voor\n // de naam, tandwiel voor het hele formulier). Op de cataloguspagina was dat er niet, en dus was\n // een productnaam daar het enige stuk tekst op de site dat je nergens kon aanklikken.\n <Card padding={0} className=\"xicms-shop-card\" data-xicms-product={product.id}>\n <a className=\"xicms-shop-card__link\" href={href}>\n <div className=\"xicms-shop-card__media\">\n {image ? (\n <img src={image} alt={product.name} loading=\"lazy\" className=\"xicms-shop-card__img\" />\n ) : (\n <div className=\"xicms-shop-card__placeholder\" aria-hidden=\"true\" />\n )}\n {soldOut && <span className=\"xicms-shop-card__soldout\">{t(\"soldOut\")}</span>}\n </div>\n <div className=\"xicms-shop-card__body\">\n <span className=\"xicms-shop-card__name\" data-xicms-product-edit=\"name\">{product.name}</span>\n <span className=\"xicms-shop-card__priceline\">\n <PriceLine product={product} currency={currency} />\n </span>\n <span className=\"xicms-shop-card__swatchrow\">\n <ColorSwatches product={product} onHover={setHoverImage} />\n </span>\n </div>\n </a>\n <div className=\"xicms-shop-card__actions\">\n {simple ? (\n added ? (\n <a className=\"xicms-shop-card__added\" href={cartPath}>\n {t(\"addedGoToCart\")}\n </a>\n ) : (\n <button\n type=\"button\"\n className=\"xicms-btn xicms-btn--primary\"\n disabled={soldOut}\n style={{ cursor: soldOut ? \"not-allowed\" : \"pointer\" }}\n onClick={() => {\n cartAdd(siteId, {\n productId: product.id,\n slug: product.slug,\n name: product.name,\n unitPriceCents: product.priceCents,\n imageUrl: product.images?.[0],\n });\n setAdded(true);\n }}\n >\n {t(\"addToCart\")}\n </button>\n )\n ) : (\n <a className=\"xicms-btn xicms-btn--secondary\" href={href}>\n {t(\"view\")}\n </a>\n )}\n </div>\n </Card>\n );\n}\n\nfunction ProductGridRender({ block, siteId }: BlockRenderProps<Blk>) {\n const settings = useShopSettings();\n const t = useShopStrings();\n const serverData = useBlockData<ShopPageData>(\"shop\");\n const [products, setProducts] = useState<StoredProduct[] | null>(serverData?.products ?? null);\n const [cats, setCats] = useState<StoredShopCategory[]>(serverData?.categories ?? []);\n const [queryCat, setQueryCat] = useState<string | null>(null);\n const [filterColor, setFilterColor] = useState<string | null>(null);\n const [filterSize, setFilterSize] = useState<string | null>(null);\n const [sort, setSort] = useState<Blk[\"sort\"]>(block.sort);\n\n useEffect(() => {\n // Server-data aanwezig (SSR/ISR)? Dan geen client-fetch — geen flits, minder reads.\n if (!serverData?.products) {\n void fetchActiveProducts(siteId).then(setProducts).catch((e) => {\n console.warn(\"[xicms] producten laden mislukt:\", e);\n setProducts([]);\n });\n }\n if (!serverData?.categories) {\n void fetchShopCategories(siteId).then(setCats).catch(() => setCats([]));\n }\n if (block.useQueryCategory && !block.categorySlug) setQueryCat(readQueryParam(\"cat\"));\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [siteId, block.useQueryCategory, block.categorySlug]);\n\n const activeSlug = block.categorySlug || queryCat || null;\n const activeCat = activeSlug ? cats.find((c) => c.slug === activeSlug) ?? null : null;\n\n /** Beschikbare filterwaarden uit de geladen set (client-side; geen extra reads). */\n const filterValues = useMemo(() => {\n if (!block.showFilters || !products) return { colors: [] as string[], sizes: [] as string[] };\n const colors = new Set<string>();\n const sizes = new Set<string>();\n for (const p of products) {\n for (const o of p.options ?? []) {\n if (/^kleur|^colou?r/i.test(o.name)) o.values.forEach((v) => colors.add(v));\n if (/^maat|^size/i.test(o.name)) o.values.forEach((v) => sizes.add(v));\n }\n }\n return { colors: [...colors], sizes: [...sizes].sort((a, b) => Number(a) - Number(b) || a.localeCompare(b)) };\n }, [products, block.showFilters]);\n\n const visible = useMemo(() => {\n if (!products) return [];\n let list = products;\n if (activeCat) list = list.filter((p) => (p.categoryIds ?? []).includes(activeCat.id));\n if (block.featuredOnly) list = list.filter((p) => p.featured);\n if (filterColor) {\n list = list.filter((p) =>\n (p.options ?? []).some((o) => /^kleur|^colou?r/i.test(o.name) && o.values.includes(filterColor)),\n );\n }\n if (filterSize) {\n list = list.filter((p) =>\n (p.options ?? []).some((o) => /^maat|^size/i.test(o.name) && o.values.includes(filterSize)),\n );\n }\n const sorted = [...list];\n if (sort === \"name\") sorted.sort((a, b) => a.name.localeCompare(b.name, \"nl\"));\n else if (sort === \"priceAsc\") sorted.sort((a, b) => a.priceCents - b.priceCents);\n else if (sort === \"priceDesc\") sorted.sort((a, b) => b.priceCents - a.priceCents);\n else sorted.sort((a, b) => (a.order ?? 0) - (b.order ?? 0) || a.name.localeCompare(b.name, \"nl\"));\n /**\n * Uitverkochte artikelen altijd achteraan, ongeacht de gekozen sortering: ze bleven op hun\n * plek staan en duwden zo leverbare producten van de eerste rij. De volgorde binnen elke\n * groep blijft die van de sortering hierboven — een stabiele sort verandert daar niets aan.\n */\n sorted.sort((a, b) => Number(isSoldOut(a)) - Number(isSoldOut(b)));\n return sorted.slice(0, block.limit);\n }, [products, activeCat, block.featuredOnly, block.limit, sort, filterColor, filterSize]);\n\n if (products === null) return null;\n\n const chip = (label: string, active: boolean, onClick: () => void, swatch?: string | null) => (\n <button\n key={label}\n type=\"button\"\n className={`xicms-shop-filterchip${active ? \" xicms-shop-filterchip--active\" : \"\"}`}\n aria-pressed={active}\n onClick={onClick}\n >\n {swatch && (\n <span\n aria-hidden\n style={{ display: \"inline-block\", width: 10, height: 10, borderRadius: 999, background: swatch, boxShadow: \"inset 0 0 0 1px rgba(0,0,0,0.18)\", marginRight: 5 }}\n />\n )}\n {label}\n </button>\n );\n\n return (\n <div className=\"xicms-shop-grid\">\n {(block.heading || activeCat) && (\n <Heading level={2} data-xicms-edit=\"heading\">\n {activeCat?.name ?? block.heading}\n </Heading>\n )}\n {activeCat && (\n <Text size=\"sm\" display=\"block\" style={{ marginTop: 4 }}>\n <a href={settings.shopPath}>← {t(\"allCategories\")}</a>\n </Text>\n )}\n\n {block.showFilters && (filterValues.colors.length > 0 || filterValues.sizes.length > 0) && (\n <div className=\"xicms-shop-filters\">\n {filterValues.colors.length > 0 && (\n <div className=\"xicms-shop-filters__row\" role=\"group\" aria-label=\"Kleur\">\n {chip(t(\"filterAll\"), filterColor === null, () => setFilterColor(null))}\n {filterValues.colors.map((c) =>\n chip(c, filterColor === c, () => setFilterColor(filterColor === c ? null : c), kleurNaarCss(c)),\n )}\n </div>\n )}\n {filterValues.sizes.length > 0 && (\n <div className=\"xicms-shop-filters__row\" role=\"group\" aria-label=\"Maat\">\n {chip(t(\"filterAll\"), filterSize === null, () => setFilterSize(null))}\n {filterValues.sizes.map((s) => chip(s, filterSize === s, () => setFilterSize(filterSize === s ? null : s)))}\n </div>\n )}\n <label className=\"xicms-shop-filters__sort\">\n <span>{t(\"sortLabel\")}</span>\n <select className=\"xicms-input\" value={sort} onChange={(e) => setSort(e.target.value as Blk[\"sort\"])}>\n <option value=\"order\">{t(\"sortDefault\")}</option>\n <option value=\"name\">{t(\"sortName\")}</option>\n <option value=\"priceAsc\">{t(\"sortPriceAsc\")}</option>\n <option value=\"priceDesc\">{t(\"sortPriceDesc\")}</option>\n </select>\n </label>\n </div>\n )}\n\n {visible.length === 0 ? (\n <Card padding={4} variant=\"muted\" style={{ marginTop: \"1rem\" }}>\n <Text>{t(\"noProducts\")}</Text>\n </Card>\n ) : (\n <div\n className=\"xicms-shop-grid__items\"\n data-columns={block.columns}\n style={{\n display: \"grid\",\n gridTemplateColumns: `repeat(auto-fill, minmax(min(240px, calc(100%/${block.columns} - 1rem)), 1fr))`,\n gap: \"1.25rem\",\n marginTop: \"1.25rem\",\n }}\n >\n {visible.map((p) => (\n <ProductCard\n key={p.id}\n product={p}\n siteId={siteId}\n currency={settings.currency}\n detailPath={settings.detailPath}\n cartPath={settings.cartPath}\n t={t}\n filterColor={filterColor}\n />\n ))}\n </div>\n )}\n </div>\n );\n}\n\nexport const productGridBlock = defineBlock({\n type: \"productGrid\",\n Render: ProductGridRender,\n label: \"Producten-overzicht\",\n icon: \"🛍️\",\n defaults: () => ({ useQueryCategory: true, columns: 3, limit: 60, sort: \"order\" }),\n});\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../../../src/modules/shop/blocks/ProductGrid.tsx"],"sourcesContent":["\"use client\";\n\nimport { useEffect, useMemo, useState } from \"react\";\nimport type { z } from \"zod\";\nimport { formatCents } from \"../../../schema/shopGeld\";\nimport type { ProductGridBlockSchema } from \"../../../schema/shop\";\nimport { optieHeeftBeeldPerWaarde } from \"../../../schema/shop\";\nimport { defineBlock, type BlockRenderProps } from \"../../../blocks/registry\";\nimport { useBlockData } from \"../../../blocks/context\";\nimport { Card, Heading, Text } from \"../../../blocks/ui\";\nimport { cartAdd } from \"../cartStore\";\nimport { useShopStrings, type ShopT } from \"../strings\";\nimport {\n fetchActiveProducts,\n fetchShopCategories,\n kleurNaarCss,\n kleurOptie,\n readQueryParam,\n useShopSettings,\n type StoredProduct,\n type StoredShopCategory,\n} from \"../lib\";\nimport type { ShopPageData } from \"../types\";\n\ntype Blk = z.infer<typeof ProductGridBlockSchema>;\n\n/** Prijsregel met optionele doorgestreepte \"van\"-prijs en \"vanaf\" bij variant-prijzen. */\nexport function PriceLine({ product, currency }: { product: StoredProduct; currency: string }) {\n const variantPrices = (product.variants ?? [])\n .filter((v) => v.priceCents !== undefined)\n .map((v) => v.priceCents as number);\n const min = Math.min(product.priceCents, ...variantPrices);\n const hasRange = variantPrices.some((p) => p !== product.priceCents);\n return (\n <span className=\"xicms-shop-price\">\n {product.compareAtCents !== undefined && product.compareAtCents > min && (\n <s style={{ opacity: 0.6, marginRight: 6 }}>{formatCents(product.compareAtCents, currency)}</s>\n )}\n {hasRange ? `vanaf ${formatCents(min, currency)}` : formatCents(min, currency)}\n </span>\n );\n}\n\n/**\n * Uitverkocht. Zonder opties: de voorraad van het product zelf. MÉT opties: pas uitverkocht als\n * élke variant met een expliciete voorraad op 0 staat — dat was eerder helemaal niet af te lezen,\n * waardoor een volledig uitverkocht t-shirt gewoon als beschikbaar in het grid stond.\n */\nfunction isSoldOut(p: StoredProduct): boolean {\n if (p.trackStock !== true) return false;\n if ((p.options ?? []).length === 0) return p.stock !== undefined && p.stock <= 0;\n const metVoorraad = (p.variants ?? []).filter((v) => v.stock !== undefined && v.active !== false);\n return metVoorraad.length > 0 && metVoorraad.every((v) => (v.stock as number) <= 0);\n}\n\n/**\n * Stof-stipjes; hover wisselt de kaartfoto naar de foto van die kleur (als die er is).\n *\n * De stipjes blijven aan de KLEUR-as hangen — een tint tekenen kan alleen als de waarde een\n * kleurnaam is. De foto's komen er alleen bij als die as ook echt beeld per waarde voert: staat\n * dat vinkje uit, dan zijn eventuele oude URL's dode data en moet hover niets doen.\n */\nfunction ColorSwatches({ product, onHover }: { product: StoredProduct; onHover?: (url: string | null) => void }) {\n const optie = kleurOptie(product);\n if (!optie) return null;\n const metBeeld = optieHeeftBeeldPerWaarde(optie);\n const dots = optie.values\n .map((v) => ({ v, css: kleurNaarCss(v), img: metBeeld ? optie.images?.[v] : undefined }))\n .filter((d): d is { v: string; css: string; img: string | undefined } => d.css !== null);\n if (dots.length === 0) return null;\n return (\n <span className=\"xicms-shop-card__swatches\" aria-label={`Kleuren: ${optie.values.join(\", \")}`}>\n {dots.map((d) => (\n <span\n key={d.v}\n className=\"xicms-shop-card__swatch\"\n title={d.v}\n onMouseEnter={d.img && onHover ? () => onHover(d.img ?? null) : undefined}\n onMouseLeave={d.img && onHover ? () => onHover(null) : undefined}\n style={{\n display: \"inline-block\",\n width: 12,\n height: 12,\n borderRadius: 999,\n background: d.css,\n boxShadow: \"inset 0 0 0 1px rgba(0,0,0,0.18)\",\n }}\n />\n ))}\n </span>\n );\n}\n\nexport function ProductCard({\n product,\n siteId,\n currency,\n detailPath,\n cartPath,\n t,\n filterColor,\n}: {\n product: StoredProduct;\n siteId: string;\n currency: string;\n detailPath: string;\n cartPath: string;\n t: ShopT;\n /** Actief kleurfilter uit het overzicht — toont bij voorkeur de foto van die kleur. */\n filterColor?: string | null;\n}) {\n const [added, setAdded] = useState(false);\n const [hoverImage, setHoverImage] = useState<string | null>(null);\n const href = `${detailPath}?p=${encodeURIComponent(product.slug)}`;\n const simple = (product.options ?? []).length === 0 && !(product.personalization ?? []).length;\n const soldOut = isSoldOut(product);\n // Kleurfilter actief? Toon bij voorkeur de foto van die kleur — maar alleen als de kleur-as\n // ook echt beeld per waarde voert (zie ColorSwatches).\n const kleurAs = filterColor ? kleurOptie(product) : null;\n const filterImage = kleurAs && optieHeeftBeeldPerWaarde(kleurAs) ? kleurAs.images?.[filterColor as string] : undefined;\n const image = hoverImage ?? filterImage ?? product.images?.[0];\n\n return (\n // Flex-kolom over de volle kaarthoogte: titel (vast 2 regels) → prijs → swatches →\n // knop onderaan. Zo lijnen prijs/swatches/knop over álle kaarten in de rij uit, ook\n // als een productnaam één of twee regels beslaat. Alle opmaak in styles.css.\n // `data-xicms-product` maakt de kaart in de bewerkmodus aanwijsbaar als PRODUCT (potlood voor\n // de naam, tandwiel voor het hele formulier). Op de cataloguspagina was dat er niet, en dus was\n // een productnaam daar het enige stuk tekst op de site dat je nergens kon aanklikken.\n <Card padding={0} className=\"xicms-shop-card\" data-xicms-product={product.id}>\n <a className=\"xicms-shop-card__link\" href={href}>\n <div className=\"xicms-shop-card__media\">\n {image ? (\n <img src={image} alt={product.name} loading=\"lazy\" className=\"xicms-shop-card__img\" />\n ) : (\n <div className=\"xicms-shop-card__placeholder\" aria-hidden=\"true\" />\n )}\n {soldOut && <span className=\"xicms-shop-card__soldout\">{t(\"soldOut\")}</span>}\n </div>\n <div className=\"xicms-shop-card__body\">\n <span className=\"xicms-shop-card__name\" data-xicms-product-edit=\"name\">{product.name}</span>\n <span className=\"xicms-shop-card__priceline\">\n <PriceLine product={product} currency={currency} />\n </span>\n <span className=\"xicms-shop-card__swatchrow\">\n <ColorSwatches product={product} onHover={setHoverImage} />\n </span>\n </div>\n </a>\n <div className=\"xicms-shop-card__actions\">\n {simple ? (\n added ? (\n <a className=\"xicms-shop-card__added\" href={cartPath}>\n {t(\"addedGoToCart\")}\n </a>\n ) : (\n <button\n type=\"button\"\n className=\"xicms-btn xicms-btn--primary\"\n disabled={soldOut}\n style={{ cursor: soldOut ? \"not-allowed\" : \"pointer\" }}\n onClick={() => {\n cartAdd(siteId, {\n productId: product.id,\n slug: product.slug,\n name: product.name,\n unitPriceCents: product.priceCents,\n imageUrl: product.images?.[0],\n });\n setAdded(true);\n }}\n >\n {t(\"addToCart\")}\n </button>\n )\n ) : (\n <a className=\"xicms-btn xicms-btn--secondary\" href={href}>\n {t(\"view\")}\n </a>\n )}\n </div>\n </Card>\n );\n}\n\nfunction ProductGridRender({ block, siteId }: BlockRenderProps<Blk>) {\n const settings = useShopSettings();\n const t = useShopStrings();\n const serverData = useBlockData<ShopPageData>(\"shop\");\n const [products, setProducts] = useState<StoredProduct[] | null>(serverData?.products ?? null);\n const [cats, setCats] = useState<StoredShopCategory[]>(serverData?.categories ?? []);\n const [queryCat, setQueryCat] = useState<string | null>(null);\n const [filterColor, setFilterColor] = useState<string | null>(null);\n const [filterSize, setFilterSize] = useState<string | null>(null);\n const [sort, setSort] = useState<Blk[\"sort\"]>(block.sort);\n\n useEffect(() => {\n // Server-data aanwezig (SSR/ISR)? Dan geen client-fetch — geen flits, minder reads.\n if (!serverData?.products) {\n void fetchActiveProducts(siteId).then(setProducts).catch((e) => {\n console.warn(\"[xicms] producten laden mislukt:\", e);\n setProducts([]);\n });\n }\n if (!serverData?.categories) {\n void fetchShopCategories(siteId).then(setCats).catch(() => setCats([]));\n }\n if (block.useQueryCategory && !block.categorySlug) setQueryCat(readQueryParam(\"cat\"));\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [siteId, block.useQueryCategory, block.categorySlug]);\n\n const activeSlug = block.categorySlug || queryCat || null;\n const activeCat = activeSlug ? cats.find((c) => c.slug === activeSlug) ?? null : null;\n\n /** Beschikbare filterwaarden uit de geladen set (client-side; geen extra reads). */\n const filterValues = useMemo(() => {\n if (!block.showFilters || !products) return { colors: [] as string[], sizes: [] as string[] };\n const colors = new Set<string>();\n const sizes = new Set<string>();\n for (const p of products) {\n for (const o of p.options ?? []) {\n if (/^kleur|^colou?r/i.test(o.name)) o.values.forEach((v) => colors.add(v));\n if (/^maat|^size/i.test(o.name)) o.values.forEach((v) => sizes.add(v));\n }\n }\n return { colors: [...colors], sizes: [...sizes].sort((a, b) => Number(a) - Number(b) || a.localeCompare(b)) };\n }, [products, block.showFilters]);\n\n const visible = useMemo(() => {\n if (!products) return [];\n let list = products;\n if (activeCat) list = list.filter((p) => (p.categoryIds ?? []).includes(activeCat.id));\n if (block.featuredOnly) list = list.filter((p) => p.featured);\n if (filterColor) {\n list = list.filter((p) =>\n (p.options ?? []).some((o) => /^kleur|^colou?r/i.test(o.name) && o.values.includes(filterColor)),\n );\n }\n if (filterSize) {\n list = list.filter((p) =>\n (p.options ?? []).some((o) => /^maat|^size/i.test(o.name) && o.values.includes(filterSize)),\n );\n }\n const sorted = [...list];\n if (sort === \"name\") sorted.sort((a, b) => a.name.localeCompare(b.name, \"nl\"));\n else if (sort === \"priceAsc\") sorted.sort((a, b) => a.priceCents - b.priceCents);\n else if (sort === \"priceDesc\") sorted.sort((a, b) => b.priceCents - a.priceCents);\n else sorted.sort((a, b) => (a.order ?? 0) - (b.order ?? 0) || a.name.localeCompare(b.name, \"nl\"));\n /**\n * Uitverkochte artikelen altijd achteraan, ongeacht de gekozen sortering: ze bleven op hun\n * plek staan en duwden zo leverbare producten van de eerste rij. De volgorde binnen elke\n * groep blijft die van de sortering hierboven — een stabiele sort verandert daar niets aan.\n */\n sorted.sort((a, b) => Number(isSoldOut(a)) - Number(isSoldOut(b)));\n return sorted.slice(0, block.limit);\n }, [products, activeCat, block.featuredOnly, block.limit, sort, filterColor, filterSize]);\n\n if (products === null) return null;\n\n const chip = (label: string, active: boolean, onClick: () => void, swatch?: string | null) => (\n <button\n key={label}\n type=\"button\"\n className={`xicms-shop-filterchip${active ? \" xicms-shop-filterchip--active\" : \"\"}`}\n aria-pressed={active}\n onClick={onClick}\n >\n {swatch && (\n <span\n aria-hidden\n style={{ display: \"inline-block\", width: 10, height: 10, borderRadius: 999, background: swatch, boxShadow: \"inset 0 0 0 1px rgba(0,0,0,0.18)\", marginRight: 5 }}\n />\n )}\n {label}\n </button>\n );\n\n return (\n <div className=\"xicms-shop-grid\">\n {(block.heading || activeCat) && (\n <Heading level={2} data-xicms-edit=\"heading\">\n {activeCat?.name ?? block.heading}\n </Heading>\n )}\n {activeCat && (\n <Text size=\"sm\" display=\"block\" style={{ marginTop: 4 }}>\n <a href={settings.shopPath}>← {t(\"allCategories\")}</a>\n </Text>\n )}\n\n {block.showFilters && (filterValues.colors.length > 0 || filterValues.sizes.length > 0) && (\n <div className=\"xicms-shop-filters\">\n {filterValues.colors.length > 0 && (\n <div className=\"xicms-shop-filters__row\" role=\"group\" aria-label=\"Kleur\">\n {chip(t(\"filterAll\"), filterColor === null, () => setFilterColor(null))}\n {filterValues.colors.map((c) =>\n chip(c, filterColor === c, () => setFilterColor(filterColor === c ? null : c), kleurNaarCss(c)),\n )}\n </div>\n )}\n {filterValues.sizes.length > 0 && (\n <div className=\"xicms-shop-filters__row\" role=\"group\" aria-label=\"Maat\">\n {chip(t(\"filterAll\"), filterSize === null, () => setFilterSize(null))}\n {filterValues.sizes.map((s) => chip(s, filterSize === s, () => setFilterSize(filterSize === s ? null : s)))}\n </div>\n )}\n <label className=\"xicms-shop-filters__sort\">\n <span>{t(\"sortLabel\")}</span>\n <select className=\"xicms-input\" value={sort} onChange={(e) => setSort(e.target.value as Blk[\"sort\"])}>\n <option value=\"order\">{t(\"sortDefault\")}</option>\n <option value=\"name\">{t(\"sortName\")}</option>\n <option value=\"priceAsc\">{t(\"sortPriceAsc\")}</option>\n <option value=\"priceDesc\">{t(\"sortPriceDesc\")}</option>\n </select>\n </label>\n </div>\n )}\n\n {visible.length === 0 ? (\n <Card padding={4} variant=\"muted\" style={{ marginTop: \"1rem\" }}>\n <Text>{t(\"noProducts\")}</Text>\n </Card>\n ) : (\n <div\n className=\"xicms-shop-grid__items\"\n data-columns={block.columns}\n style={{\n display: \"grid\",\n gridTemplateColumns: `repeat(auto-fill, minmax(min(240px, calc(100%/${block.columns} - 1rem)), 1fr))`,\n gap: \"1.25rem\",\n marginTop: \"1.25rem\",\n }}\n >\n {visible.map((p) => (\n <ProductCard\n key={p.id}\n product={p}\n siteId={siteId}\n currency={settings.currency}\n detailPath={settings.detailPath}\n cartPath={settings.cartPath}\n t={t}\n filterColor={filterColor}\n />\n ))}\n </div>\n )}\n </div>\n );\n}\n\nexport const productGridBlock = defineBlock({\n type: \"productGrid\",\n Render: ProductGridRender,\n label: \"Producten-overzicht\",\n icon: \"🛍️\",\n defaults: () => ({ useQueryCategory: true, columns: 3, limit: 60, sort: \"order\" }),\n});\n"],"mappings":"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","names":[]}
@@ -51,5 +51,21 @@ export declare function getOrderReturnStatusCall(siteId: string, orderId: string
51
51
  * swatch-stipjes op productkaarten. Onbekende namen geven null (dan geen stipje).
52
52
  */
53
53
  export declare function kleurNaarCss(naam: string): string | null;
54
- /** De kleur-optie van een product (naam "Kleur"/"Colour"/"Color"), of null. */
54
+ /**
55
+ * De KLEUR-as van een product (naam "Kleur"/"Colour"/"Color"), of null.
56
+ *
57
+ * Blijft op de naam zoeken, en dat is hier terecht: deze as voedt de gekleurde stipjes op de
58
+ * productkaart en het kleurfilter in het overzicht. Een stipje in de juiste tint kan alleen als
59
+ * de wáárde een kleurnaam is (zie `kleurNaarCss`) — "Print: Olifantje" heeft geen tint.
60
+ */
55
61
  export declare function kleurOptie(product: Pick<Product, "options">): ProductOption | null;
62
+ /**
63
+ * De as waarvan élke waarde een eigen foto heeft — de as die het hoofdbeeld op de detailpagina
64
+ * stuurt. Dit is losgekoppeld van de kleur-as (7.23): de winkelier zet per as een vinkje, dus het
65
+ * mag net zo goed "Print" of "Dessin" zijn. Zonder expliciete keuze valt het terug op de oude
66
+ * kleur-naamregel, zodat bestaande producten onveranderd blijven werken.
67
+ *
68
+ * Maar één as tegelijk: het hoofdbeeld kan niet twee keuzes tegelijk volgen. Staat de vlag per
69
+ * ongeluk toch op meerdere assen (handmatig aangepaste data), dan wint de eerste.
70
+ */
71
+ export declare function beeldOptie(product: Pick<Product, "options">): ProductOption | null;
@@ -7,6 +7,7 @@ async function fnSdk() {
7
7
  const [client, sdk] = await Promise.all([import("../../firebase/client.js"), import("../../firebase/functionsClient.js")]);
8
8
  return { app: client.getFirebaseApp(), ...sdk };
9
9
  }
10
+ import { optieHeeftBeeldPerWaarde } from "../../schema/shop.js";
10
11
  import { useModuleSettings } from "../context.js";
11
12
  import { defaultShopSettings } from "./settings.js";
12
13
  function useShopSettings() {
@@ -123,7 +124,12 @@ function kleurNaarCss(naam) {
123
124
  function kleurOptie(product) {
124
125
  return (product.options ?? []).find((o) => /^kleur|^colou?r/i.test(o.name)) ?? null;
125
126
  }
127
+ function beeldOptie(product) {
128
+ const opties = product.options ?? [];
129
+ return opties.find((o) => o.imagePerValue === true) ?? opties.find((o) => optieHeeftBeeldPerWaarde(o)) ?? null;
130
+ }
126
131
  export {
132
+ beeldOptie,
127
133
  createOrderCall,
128
134
  fetchActiveProducts,
129
135
  fetchProductBySlug,
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/modules/shop/lib.ts"],"sourcesContent":["\"use client\";\n\n/**\n * Client-helpers voor de shop-blocks: producten/categorieën lezen (publieke rules) en de\n * `createOrder`-callable aanroepen. Queries bewust simpel gehouden (één `where` op status,\n * filteren/sorteren client-side) zodat er géén composite-indexes nodig zijn — assortimenten\n * van XiCMS-sites zijn klein (tientallen tot honderden producten).\n */\n\n/**\n * Firestore/Functions worden hier DYNAMISCH geladen. De shop-blocks zitten in de publieke bundel,\n * dus een statische import zette de SDK op élke pagina — ook op sites zonder webshop.\n */\nasync function fsSdk() {\n const [client, sdk] = await Promise.all([import(\"../../firebase/client\"), import(\"firebase/firestore\")]);\n return { db: client.getDb(), ...sdk };\n}\nasync function fnSdk() {\n const [client, sdk] = await Promise.all([import(\"../../firebase/client\"), import(\"../../firebase/functionsClient\")]);\n return { app: client.getFirebaseApp(), ...sdk };\n}\nimport type { CreateOrderInput, Product, ProductOption, ShopCategory } from \"../../schema/shop\";\nimport { useModuleSettings } from \"../context\";\nimport { defaultShopSettings, type ShopSettings } from \"./settings\";\n\n/** Publieke shop-instellingen met defaults (paden, verzendkosten, valuta). */\nexport function useShopSettings(): ShopSettings {\n const raw = useModuleSettings<Partial<ShopSettings>>(\"shop\");\n return { ...defaultShopSettings, ...raw };\n}\n\nexport type StoredProduct = Product & { id: string };\nexport type StoredShopCategory = ShopCategory & { id: string };\n\nexport async function fetchActiveProducts(siteId: string): Promise<StoredProduct[]> {\n const { db, collection, getDocs, limit: fsLimit, query, where } = await fsSdk();\n if (!db) return [];\n const snap = await getDocs(\n query(collection(db, \"sites\", siteId, \"products\"), where(\"status\", \"==\", \"active\"), fsLimit(500)),\n );\n return snap.docs.map((d) => ({ id: d.id, ...(d.data() as Product) }));\n}\n\nexport async function fetchProductBySlug(siteId: string, slug: string): Promise<StoredProduct | null> {\n const { db, collection, getDocs, limit: fsLimit, query, where } = await fsSdk();\n if (!db || !slug) return null;\n // Het status-filter is verplicht: de rules staan publieke reads alleen toe voor actieve\n // producten, en Firestore weigert query's die niet aantoonbaar binnen die regel blijven.\n // Twee equality-filters → geen composite index nodig (index-merging).\n const snap = await getDocs(\n query(\n collection(db, \"sites\", siteId, \"products\"),\n where(\"slug\", \"==\", slug),\n where(\"status\", \"==\", \"active\"),\n fsLimit(1),\n ),\n );\n const doc = snap.docs[0];\n if (!doc) return null;\n return { id: doc.id, ...(doc.data() as Product) };\n}\n\nexport async function fetchShopCategories(siteId: string): Promise<StoredShopCategory[]> {\n const { db, collection, getDocs, limit: fsLimit, query, where } = await fsSdk();\n if (!db) return [];\n const snap = await getDocs(query(collection(db, \"sites\", siteId, \"shopCategories\"), fsLimit(100)));\n return snap.docs\n .map((d) => ({ id: d.id, ...(d.data() as ShopCategory) }))\n .filter((c) => c.visible !== false)\n .sort((a, b) => (a.order ?? 0) - (b.order ?? 0));\n}\n\n/** Start de checkout: order + Stripe-sessie server-side; geeft de redirect-URL terug. */\n/**\n * Verwijdert `undefined`/`null`-velden diep uit een payload vóór een callable-aanroep.\n * De Firebase-callable-SDK serialiseert `undefined` namelijk als `null` op de wire, terwijl de\n * server-side Zod-schema's optionele velden met `.optional()` valideren — dat wijst `null` af en\n * geeft \"Ongeldige invoer\". Door lege velden hier weg te laten komen ze als afwezig binnen (= ok).\n */\nfunction stripEmpty<T>(value: T): T {\n if (Array.isArray(value)) {\n return value.map((v) => stripEmpty(v)) as unknown as T;\n }\n if (value && typeof value === \"object\") {\n const out: Record<string, unknown> = {};\n for (const [k, v] of Object.entries(value as Record<string, unknown>)) {\n if (v === undefined || v === null) continue;\n out[k] = stripEmpty(v);\n }\n return out as T;\n }\n return value;\n}\n\nexport async function createOrderCall(input: CreateOrderInput): Promise<{ url: string; orderNumber: string }> {\n const { app, callable } = await fnSdk();\n if (!app) throw new Error(\"Webwinkel niet beschikbaar.\");\n const fn = callable(app, \"createOrder\");\n const res = await fn(stripEmpty(input));\n return res.data as { url: string; orderNumber: string };\n}\n\n/** Valideert een kortingscode server-side; geeft de verwachte korting terug (preview). */\nexport interface DiscountPreview {\n valid: boolean;\n code?: string;\n discountCents?: number;\n freeShipping?: boolean;\n reason?: string;\n}\nexport async function validateDiscountCall(siteId: string, code: string, itemsCents: number): Promise<DiscountPreview> {\n const { app, callable } = await fnSdk();\n if (!app) throw new Error(\"Webwinkel niet beschikbaar.\");\n const fn = callable(app, \"validateDiscountCode\");\n const res = await fn({ siteId, code, itemsCents });\n return res.data as DiscountPreview;\n}\n\n/**\n * Upload een personalisatie-afbeelding (eigen logo e.d.) via de begrensde callable —\n * geeft het Storage-pad terug dat op de orderregel meegaat. Max 5 MB, jpeg/png/webp.\n */\nexport async function uploadPersonalizationCall(\n siteId: string,\n productId: string,\n file: File,\n): Promise<{ path: string }> {\n const { app, callable } = await fnSdk();\n if (!app) throw new Error(\"Webwinkel niet beschikbaar.\");\n if (file.size > 5 * 1024 * 1024) throw new Error(\"TOO_BIG\");\n const bytesBase64 = await new Promise<string>((resolve, reject) => {\n const reader = new FileReader();\n reader.onload = () => resolve(String(reader.result).split(\",\")[1] ?? \"\");\n reader.onerror = () => reject(new Error(\"Bestand lezen mislukt.\"));\n reader.readAsDataURL(file);\n });\n const fn = callable(app, \"uploadPersonalization\");\n const res = await fn({ siteId, productId, fileName: file.name, contentType: file.type, bytesBase64 });\n return res.data as { path: string };\n}\n\n/** Leesbaar variantlabel: opties [\"Maat\",\"Kleur\"] + key \"68|Roze\" → \"Maat 68 · Roze\". */\nexport function variantLabelFor(options: { name: string; values: string[] }[], key: string): string {\n const parts = key.split(\"|\");\n return options.map((o, i) => `${o.name} ${parts[i] ?? \"?\"}`).join(\" · \");\n}\n\n/** Querystring-parameter client-side lezen (blocks zijn client components). */\nexport function readQueryParam(name: string): string | null {\n if (typeof window === \"undefined\") return null;\n return new URLSearchParams(window.location.search).get(name);\n}\n\n/**\n * Echte orderstatus opvragen (voor de bedankt-pagina). Nodig bij Mollie: die heeft één\n * redirect-URL voor alle uitkomsten, dus de status moet server-side gecheckt worden.\n */\nexport async function getOrderReturnStatusCall(\n siteId: string,\n orderId: string,\n): Promise<{ status: string; number?: string }> {\n const { app, callable } = await fnSdk();\n if (!app) return { status: \"unknown\" };\n const fn = callable(app, \"getOrderReturnStatus\");\n const res = await fn({ siteId, orderId });\n return res.data as { status: string; number?: string };\n}\n\n/* ── Kleur-swatches ──────────────────────────────────────────────────────── */\n\n// Zachte, pastelachtige swatch-tinten — informatief maar rustig (geen felle vlekken op de\n// kaart). Bewust gedempt t.o.v. de echte stofkleuren; de foto toont de precieze kleur.\nconst KLEUR_CSS: [RegExp, string][] = [\n [/fuchsia|fushia/, \"#dd8fb4\"],\n [/roze|pink/, \"#f2c3d3\"],\n [/blauw|blue/, \"#bcd3ea\"],\n [/mint/, \"#bfe0d2\"],\n [/oker|geel|yellow/, \"#eccf90\"],\n [/wit|white/, \"#f6f3ec\"],\n [/creme|crème|ecru|beige|zand/, \"#f0e7d6\"],\n [/grijs|gray|grey/, \"#c7ccd3\"],\n [/groen|green/, \"#b3d1b3\"],\n [/rood|red|bordeaux/, \"#e0a0a0\"],\n [/paars|purple|lila/, \"#cdbce6\"],\n [/oranje|orange/, \"#f2bd94\"],\n [/bruin|brown/, \"#c3a488\"],\n [/zwart|black|navy|donkerblauw/, \"#6b7488\"],\n];\n\n/**\n * Zet een kleurnaam (\"Roze\", \"Korte mouw tshirt (oker)geel\") om naar een CSS-kleur voor de\n * swatch-stipjes op productkaarten. Onbekende namen geven null (dan geen stipje).\n */\nexport function kleurNaarCss(naam: string): string | null {\n const l = naam.toLowerCase();\n for (const [re, css] of KLEUR_CSS) if (re.test(l)) return css;\n return null;\n}\n\n/** De kleur-optie van een product (naam \"Kleur\"/\"Colour\"/\"Color\"), of null. */\nexport function kleurOptie(product: Pick<Product, \"options\">): ProductOption | null {\n return (product.options ?? []).find((o) => /^kleur|^colou?r/i.test(o.name)) ?? null;\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../../src/modules/shop/lib.ts"],"sourcesContent":["\"use client\";\n\n/**\n * Client-helpers voor de shop-blocks: producten/categorieën lezen (publieke rules) en de\n * `createOrder`-callable aanroepen. Queries bewust simpel gehouden (één `where` op status,\n * filteren/sorteren client-side) zodat er géén composite-indexes nodig zijn — assortimenten\n * van XiCMS-sites zijn klein (tientallen tot honderden producten).\n */\n\n/**\n * Firestore/Functions worden hier DYNAMISCH geladen. De shop-blocks zitten in de publieke bundel,\n * dus een statische import zette de SDK op élke pagina — ook op sites zonder webshop.\n */\nasync function fsSdk() {\n const [client, sdk] = await Promise.all([import(\"../../firebase/client\"), import(\"firebase/firestore\")]);\n return { db: client.getDb(), ...sdk };\n}\nasync function fnSdk() {\n const [client, sdk] = await Promise.all([import(\"../../firebase/client\"), import(\"../../firebase/functionsClient\")]);\n return { app: client.getFirebaseApp(), ...sdk };\n}\nimport type { CreateOrderInput, Product, ProductOption, ShopCategory } from \"../../schema/shop\";\nimport { optieHeeftBeeldPerWaarde } from \"../../schema/shop\";\nimport { useModuleSettings } from \"../context\";\nimport { defaultShopSettings, type ShopSettings } from \"./settings\";\n\n/** Publieke shop-instellingen met defaults (paden, verzendkosten, valuta). */\nexport function useShopSettings(): ShopSettings {\n const raw = useModuleSettings<Partial<ShopSettings>>(\"shop\");\n return { ...defaultShopSettings, ...raw };\n}\n\nexport type StoredProduct = Product & { id: string };\nexport type StoredShopCategory = ShopCategory & { id: string };\n\nexport async function fetchActiveProducts(siteId: string): Promise<StoredProduct[]> {\n const { db, collection, getDocs, limit: fsLimit, query, where } = await fsSdk();\n if (!db) return [];\n const snap = await getDocs(\n query(collection(db, \"sites\", siteId, \"products\"), where(\"status\", \"==\", \"active\"), fsLimit(500)),\n );\n return snap.docs.map((d) => ({ id: d.id, ...(d.data() as Product) }));\n}\n\nexport async function fetchProductBySlug(siteId: string, slug: string): Promise<StoredProduct | null> {\n const { db, collection, getDocs, limit: fsLimit, query, where } = await fsSdk();\n if (!db || !slug) return null;\n // Het status-filter is verplicht: de rules staan publieke reads alleen toe voor actieve\n // producten, en Firestore weigert query's die niet aantoonbaar binnen die regel blijven.\n // Twee equality-filters → geen composite index nodig (index-merging).\n const snap = await getDocs(\n query(\n collection(db, \"sites\", siteId, \"products\"),\n where(\"slug\", \"==\", slug),\n where(\"status\", \"==\", \"active\"),\n fsLimit(1),\n ),\n );\n const doc = snap.docs[0];\n if (!doc) return null;\n return { id: doc.id, ...(doc.data() as Product) };\n}\n\nexport async function fetchShopCategories(siteId: string): Promise<StoredShopCategory[]> {\n const { db, collection, getDocs, limit: fsLimit, query, where } = await fsSdk();\n if (!db) return [];\n const snap = await getDocs(query(collection(db, \"sites\", siteId, \"shopCategories\"), fsLimit(100)));\n return snap.docs\n .map((d) => ({ id: d.id, ...(d.data() as ShopCategory) }))\n .filter((c) => c.visible !== false)\n .sort((a, b) => (a.order ?? 0) - (b.order ?? 0));\n}\n\n/** Start de checkout: order + Stripe-sessie server-side; geeft de redirect-URL terug. */\n/**\n * Verwijdert `undefined`/`null`-velden diep uit een payload vóór een callable-aanroep.\n * De Firebase-callable-SDK serialiseert `undefined` namelijk als `null` op de wire, terwijl de\n * server-side Zod-schema's optionele velden met `.optional()` valideren — dat wijst `null` af en\n * geeft \"Ongeldige invoer\". Door lege velden hier weg te laten komen ze als afwezig binnen (= ok).\n */\nfunction stripEmpty<T>(value: T): T {\n if (Array.isArray(value)) {\n return value.map((v) => stripEmpty(v)) as unknown as T;\n }\n if (value && typeof value === \"object\") {\n const out: Record<string, unknown> = {};\n for (const [k, v] of Object.entries(value as Record<string, unknown>)) {\n if (v === undefined || v === null) continue;\n out[k] = stripEmpty(v);\n }\n return out as T;\n }\n return value;\n}\n\nexport async function createOrderCall(input: CreateOrderInput): Promise<{ url: string; orderNumber: string }> {\n const { app, callable } = await fnSdk();\n if (!app) throw new Error(\"Webwinkel niet beschikbaar.\");\n const fn = callable(app, \"createOrder\");\n const res = await fn(stripEmpty(input));\n return res.data as { url: string; orderNumber: string };\n}\n\n/** Valideert een kortingscode server-side; geeft de verwachte korting terug (preview). */\nexport interface DiscountPreview {\n valid: boolean;\n code?: string;\n discountCents?: number;\n freeShipping?: boolean;\n reason?: string;\n}\nexport async function validateDiscountCall(siteId: string, code: string, itemsCents: number): Promise<DiscountPreview> {\n const { app, callable } = await fnSdk();\n if (!app) throw new Error(\"Webwinkel niet beschikbaar.\");\n const fn = callable(app, \"validateDiscountCode\");\n const res = await fn({ siteId, code, itemsCents });\n return res.data as DiscountPreview;\n}\n\n/**\n * Upload een personalisatie-afbeelding (eigen logo e.d.) via de begrensde callable —\n * geeft het Storage-pad terug dat op de orderregel meegaat. Max 5 MB, jpeg/png/webp.\n */\nexport async function uploadPersonalizationCall(\n siteId: string,\n productId: string,\n file: File,\n): Promise<{ path: string }> {\n const { app, callable } = await fnSdk();\n if (!app) throw new Error(\"Webwinkel niet beschikbaar.\");\n if (file.size > 5 * 1024 * 1024) throw new Error(\"TOO_BIG\");\n const bytesBase64 = await new Promise<string>((resolve, reject) => {\n const reader = new FileReader();\n reader.onload = () => resolve(String(reader.result).split(\",\")[1] ?? \"\");\n reader.onerror = () => reject(new Error(\"Bestand lezen mislukt.\"));\n reader.readAsDataURL(file);\n });\n const fn = callable(app, \"uploadPersonalization\");\n const res = await fn({ siteId, productId, fileName: file.name, contentType: file.type, bytesBase64 });\n return res.data as { path: string };\n}\n\n/** Leesbaar variantlabel: opties [\"Maat\",\"Kleur\"] + key \"68|Roze\" → \"Maat 68 · Roze\". */\nexport function variantLabelFor(options: { name: string; values: string[] }[], key: string): string {\n const parts = key.split(\"|\");\n return options.map((o, i) => `${o.name} ${parts[i] ?? \"?\"}`).join(\" · \");\n}\n\n/** Querystring-parameter client-side lezen (blocks zijn client components). */\nexport function readQueryParam(name: string): string | null {\n if (typeof window === \"undefined\") return null;\n return new URLSearchParams(window.location.search).get(name);\n}\n\n/**\n * Echte orderstatus opvragen (voor de bedankt-pagina). Nodig bij Mollie: die heeft één\n * redirect-URL voor alle uitkomsten, dus de status moet server-side gecheckt worden.\n */\nexport async function getOrderReturnStatusCall(\n siteId: string,\n orderId: string,\n): Promise<{ status: string; number?: string }> {\n const { app, callable } = await fnSdk();\n if (!app) return { status: \"unknown\" };\n const fn = callable(app, \"getOrderReturnStatus\");\n const res = await fn({ siteId, orderId });\n return res.data as { status: string; number?: string };\n}\n\n/* ── Kleur-swatches ──────────────────────────────────────────────────────── */\n\n// Zachte, pastelachtige swatch-tinten — informatief maar rustig (geen felle vlekken op de\n// kaart). Bewust gedempt t.o.v. de echte stofkleuren; de foto toont de precieze kleur.\nconst KLEUR_CSS: [RegExp, string][] = [\n [/fuchsia|fushia/, \"#dd8fb4\"],\n [/roze|pink/, \"#f2c3d3\"],\n [/blauw|blue/, \"#bcd3ea\"],\n [/mint/, \"#bfe0d2\"],\n [/oker|geel|yellow/, \"#eccf90\"],\n [/wit|white/, \"#f6f3ec\"],\n [/creme|crème|ecru|beige|zand/, \"#f0e7d6\"],\n [/grijs|gray|grey/, \"#c7ccd3\"],\n [/groen|green/, \"#b3d1b3\"],\n [/rood|red|bordeaux/, \"#e0a0a0\"],\n [/paars|purple|lila/, \"#cdbce6\"],\n [/oranje|orange/, \"#f2bd94\"],\n [/bruin|brown/, \"#c3a488\"],\n [/zwart|black|navy|donkerblauw/, \"#6b7488\"],\n];\n\n/**\n * Zet een kleurnaam (\"Roze\", \"Korte mouw tshirt (oker)geel\") om naar een CSS-kleur voor de\n * swatch-stipjes op productkaarten. Onbekende namen geven null (dan geen stipje).\n */\nexport function kleurNaarCss(naam: string): string | null {\n const l = naam.toLowerCase();\n for (const [re, css] of KLEUR_CSS) if (re.test(l)) return css;\n return null;\n}\n\n/**\n * De KLEUR-as van een product (naam \"Kleur\"/\"Colour\"/\"Color\"), of null.\n *\n * Blijft op de naam zoeken, en dat is hier terecht: deze as voedt de gekleurde stipjes op de\n * productkaart en het kleurfilter in het overzicht. Een stipje in de juiste tint kan alleen als\n * de wáárde een kleurnaam is (zie `kleurNaarCss`) — \"Print: Olifantje\" heeft geen tint.\n */\nexport function kleurOptie(product: Pick<Product, \"options\">): ProductOption | null {\n return (product.options ?? []).find((o) => /^kleur|^colou?r/i.test(o.name)) ?? null;\n}\n\n/**\n * De as waarvan élke waarde een eigen foto heeft — de as die het hoofdbeeld op de detailpagina\n * stuurt. Dit is losgekoppeld van de kleur-as (7.23): de winkelier zet per as een vinkje, dus het\n * mag net zo goed \"Print\" of \"Dessin\" zijn. Zonder expliciete keuze valt het terug op de oude\n * kleur-naamregel, zodat bestaande producten onveranderd blijven werken.\n *\n * Maar één as tegelijk: het hoofdbeeld kan niet twee keuzes tegelijk volgen. Staat de vlag per\n * ongeluk toch op meerdere assen (handmatig aangepaste data), dan wint de eerste.\n */\nexport function beeldOptie(product: Pick<Product, \"options\">): ProductOption | null {\n const opties = product.options ?? [];\n return opties.find((o) => o.imagePerValue === true) ?? opties.find((o) => optieHeeftBeeldPerWaarde(o)) ?? null;\n}\n"],"mappings":"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","names":[]}
@@ -27,8 +27,11 @@ export declare const ProductOptionSchema: z.ZodMiniObject<{
27
27
  name: z.ZodMiniString<string>;
28
28
  values: z.ZodMiniArray<z.ZodMiniString<string>>;
29
29
  images: z.ZodMiniOptional<z.ZodMiniRecord<z.ZodMiniString<string>, z.ZodMiniString<string>>>;
30
+ imagePerValue: z.ZodMiniOptional<z.ZodMiniBoolean<boolean>>;
30
31
  }, z.core.$strip>;
31
32
  export type ProductOption = z.infer<typeof ProductOptionSchema>;
33
+ /** Heeft deze as een eigen foto per waarde? Zonder expliciete keuze: de oude kleur-naamregel. */
34
+ export declare function optieHeeftBeeldPerWaarde(optie: Pick<ProductOption, "name" | "imagePerValue">): boolean;
32
35
  /**
33
36
  * Personalisatie-veld (5.6): de klant geeft eigen tekst of een eigen afbeelding op
34
37
  * (bv. "rompertje met naam"). Meerprijs telt server-side mee in `createOrder`.
@@ -97,6 +100,7 @@ export declare const ProductSchema: z.ZodMiniObject<{
97
100
  name: z.ZodMiniString<string>;
98
101
  values: z.ZodMiniArray<z.ZodMiniString<string>>;
99
102
  images: z.ZodMiniOptional<z.ZodMiniRecord<z.ZodMiniString<string>, z.ZodMiniString<string>>>;
103
+ imagePerValue: z.ZodMiniOptional<z.ZodMiniBoolean<boolean>>;
100
104
  }, z.core.$strip>>>;
101
105
  variants: z.ZodMiniDefault<z.ZodMiniArray<z.ZodMiniObject<{
102
106
  key: z.ZodMiniString<string>;
@@ -12,12 +12,32 @@ const ProductOptionSchema = z.object({
12
12
  name: z.string().check(z.minLength(1)).check(z.maxLength(40)),
13
13
  values: z.array(z.string().check(z.minLength(1)).check(z.maxLength(60))).check(z.minLength(1)).check(z.maxLength(30)),
14
14
  /**
15
- * 5.6: afbeelding per optieWAARDE (praktijk: de kleur-as) — `{ "Roze": "https://…" }`.
16
- * De detailpagina wisselt het hoofdbeeld mee met de keuze; bewust per as en niet per
17
- * variant-combinatie (maat × kleur zou het aantal afbeeldingen laten exploderen).
15
+ * 5.6: afbeelding per optieWAARDE — `{ "Roze": "https://…" }`. De detailpagina wisselt het
16
+ * hoofdbeeld mee met de keuze; bewust per as en niet per variant-combinatie (maat × kleur zou
17
+ * het aantal afbeeldingen laten exploderen).
18
18
  */
19
- images: z.optional(z.record(z.string(), z.string().check(z.maxLength(2e3))))
19
+ images: z.optional(z.record(z.string(), z.string().check(z.maxLength(2e3)))),
20
+ /**
21
+ * 7.23: heeft élke waarde van deze as een eigen foto?
22
+ *
23
+ * Tot nu toe was dat geen keuze maar een naamtruc: alleen een as die "Kleur"/"Color"/"Colour"
24
+ * heette kreeg fotoknoppen. Noemde je hem "Print", "Dessin" of "Uitvoering", dan kon je er geen
25
+ * foto's aan hangen — en hernoemde je een bestaande kleur-as, dan verdwenen de knoppen terwijl
26
+ * de foto's onzichtbaar in het document achterbleven. Nu is het een vinkje in het
27
+ * productformulier en staat de naam er los van.
28
+ *
29
+ * Drie toestanden, met opzet: `true` = aan, `false` = uit, en **afwezig** = "nooit ingesteld",
30
+ * en dan valt `beeldOptie()` terug op de oude naamcheck. Zo blijven alle bestaande producten
31
+ * werken zonder migratie, terwijl één klik de nieuwe regel vastlegt.
32
+ *
33
+ * Eén as tegelijk: het hoofdbeeld kan maar één keuze volgen. Het formulier zet de vlag daarom
34
+ * bij de andere assen uit zodra je hem hier aanzet.
35
+ */
36
+ imagePerValue: z.optional(z.boolean())
20
37
  });
38
+ function optieHeeftBeeldPerWaarde(optie) {
39
+ return optie.imagePerValue ?? /^kleur|^colou?r/i.test(optie.name);
40
+ }
21
41
  const PersonalizationFieldSchema = z.object({
22
42
  key: z.string().check(z.minLength(1)).check(z.maxLength(40)).check(z.regex(/^[a-zA-Z][a-zA-Z0-9_]*$/, "sleutel: letters/cijfers/underscore, beginnend met een letter")),
23
43
  label: z.string().check(z.minLength(1)).check(z.maxLength(120)),
@@ -330,6 +350,7 @@ export {
330
350
  ShopCategoriesBlockSchema,
331
351
  ShopCategorySchema,
332
352
  formatCents,
353
+ optieHeeftBeeldPerWaarde,
333
354
  orderRegelNaam,
334
355
  productPriceCents,
335
356
  saneerProductDoc,
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/schema/shop.ts"],"sourcesContent":["import * as z from \"zod/mini\";\nimport { blockBase } from \"./blockBase\";\n\n/**\n * Shop-schema's (5.5) — webwinkel + donaties. Pure Zod: gedeeld door core, admin en Functions.\n *\n * Datamodel per tenant:\n * sites/{siteId}/shopCategories/{id} — categorieën (editor-write, publiek leesbaar)\n * sites/{siteId}/products/{id} — producten (editor-write, publiek leesbaar als actief)\n * sites/{siteId}/orders/{id} — bestellingen (ALLEEN via Functions; editor leest,\n * en mag alleen status/verzendvelden muteren)\n * sites/{siteId}/private/shop — geheimen (Stripe-key, SMTP) — deny-all, alleen Functions\n *\n * Geld is ALTIJD een integer in centen (nooit floats). Publieke shopconfig (verzendkosten,\n * paden, donatie-grenzen) leeft in de module-settings van de `shop`-module\n * (settings/public.modules.shop.config) — zie modules/shop/module.ts.\n */\n\n/* ── Categorieën ─────────────────────────────────────────────────────────── */\n\nexport const ShopCategorySchema = z.object({\n name: z.string().check(z.minLength(1)).check(z.maxLength(120)),\n slug: z.string().check(z.minLength(1)).check(z.maxLength(120)).check(z.regex(/^[a-z0-9-]+$/, \"slug: alleen kleine letters, cijfers en streepjes\")),\n description: z.optional(z.string().check(z.maxLength(2000))),\n imageUrl: z.optional(z.string().check(z.maxLength(2000))),\n order: z._default(z.int().check(z.minimum(0)).check(z.maximum(9999)), 0),\n visible: z._default(z.boolean(), true),\n});\nexport type ShopCategory = z.infer<typeof ShopCategorySchema>;\n\n/* ── Producten ───────────────────────────────────────────────────────────── */\n\n/** Optie-as, bv. { name: \"Maat\", values: [\"56\",\"62\",\"68\",\"74\"] }. Max 3 assen per product. */\nexport const ProductOptionSchema = z.object({\n name: z.string().check(z.minLength(1)).check(z.maxLength(40)),\n values: z.array(z.string().check(z.minLength(1)).check(z.maxLength(60))).check(z.minLength(1)).check(z.maxLength(30)),\n /**\n * 5.6: afbeelding per optieWAARDE (praktijk: de kleur-as) — `{ \"Roze\": \"https://…\" }`.\n * De detailpagina wisselt het hoofdbeeld mee met de keuze; bewust per as en niet per\n * variant-combinatie (maat × kleur zou het aantal afbeeldingen laten exploderen).\n */\n images: z.optional(z.record(z.string(), z.string().check(z.maxLength(2000)))),\n});\nexport type ProductOption = z.infer<typeof ProductOptionSchema>;\n\n/**\n * Personalisatie-veld (5.6): de klant geeft eigen tekst of een eigen afbeelding op\n * (bv. \"rompertje met naam\"). Meerprijs telt server-side mee in `createOrder`.\n */\nexport const PersonalizationFieldSchema = z.object({\n key: z.string().check(z.minLength(1)).check(z.maxLength(40)).check(z.regex(/^[a-zA-Z][a-zA-Z0-9_]*$/, \"sleutel: letters/cijfers/underscore, beginnend met een letter\")),\n label: z.string().check(z.minLength(1)).check(z.maxLength(120)),\n type: z.enum([\"text\", \"image\"]),\n required: z._default(z.boolean(), false),\n /** Alleen voor type \"text\". */\n maxLength: z._default(z.int().check(z.minimum(1)).check(z.maximum(200)), 40),\n /** Meerprijs in centen (per stuk). */\n priceCents: z._default(z.int().check(z.minimum(0)).check(z.maximum(1_000_000_00)), 0),\n helpText: z.optional(z.string().check(z.maxLength(200))),\n});\nexport type PersonalizationField = z.infer<typeof PersonalizationFieldSchema>;\n\n/** Ingevulde personalisatie op een orderregel/cartregel: per veld-key tekst of upload-pad. */\nexport const PersonalizationValueSchema = z.record(\n z.string(),\n z.object({\n text: z.optional(z.string().check(z.maxLength(200))),\n /** Storage-pad uit de uploadPersonalization-callable (géén vrije URL). */\n imagePath: z.optional(z.string().check(z.maxLength(400))),\n /** Oorspronkelijke bestandsnaam (weergave in beheer/mail). */\n fileName: z.optional(z.string().check(z.maxLength(200))),\n }),\n);\nexport type PersonalizationValue = z.infer<typeof PersonalizationValueSchema>;\n\n/**\n * Variant = één combinatie van optiewaarden. `key` is de waarden gejoined met \"|\" in\n * optie-volgorde (bv. \"68|Roze\"). Alleen aanwezig waar hij afwijkt van de product-defaults.\n */\nexport const ProductVariantSchema = z.object({\n key: z.string().check(z.minLength(1)).check(z.maxLength(200)),\n priceCents: z.optional(z.int().check(z.minimum(0)).check(z.maximum(1_000_000_00))),\n stock: z.optional(z.int().check(z.minimum(0)).check(z.maximum(1_000_000))),\n sku: z.optional(z.string().check(z.maxLength(60))),\n active: z._default(z.boolean(), true),\n});\nexport type ProductVariant = z.infer<typeof ProductVariantSchema>;\n\nexport const ProductStatusSchema = z.enum([\"draft\", \"active\", \"archived\"]);\nexport type ProductStatus = z.infer<typeof ProductStatusSchema>;\n\n/**\n * Verwijdert top-level `null`-velden uit een productdoc vóór validatie (7.17.1).\n *\n * Er liggen productdocs met `personalization: null` (e.d.) in Firestore: de functions-SDK\n * encodeerde een client-side `undefined` als `null` over de draad (dezelfde klasse als de\n * \"+ Nieuw product\"-bug), en de callable schreef dat door. `z.optional()` accepteert een\n * ONTBREKEND veld maar geen `null`, en `parseProduct` op de leeskant slaat een ongeldig doc\n * stil over — waardoor een product dat gewoon \"actief\" in het beheer staat van de live winkel\n * verdween (gemeten op christelijkkraamcadeau: 2 van de 4 \"Maak een vrolijk geluid\"-varianten,\n * precies de twee die vóór de 7.16.2-fix nog eens waren opgeslagen). In dit model betekent\n * `null` nergens iets anders dan \"afwezig\", dus strippen is verliesvrij.\n */\nexport function saneerProductDoc<T>(data: T): T {\n if (typeof data !== \"object\" || data === null || Array.isArray(data)) return data;\n return Object.fromEntries(\n Object.entries(data as Record<string, unknown>).filter(([, v]) => v !== null),\n ) as T;\n}\n\nexport const ProductSchema = z.object({\n name: z.string().check(z.minLength(1)).check(z.maxLength(200)),\n slug: z.string().check(z.minLength(1)).check(z.maxLength(120)).check(z.regex(/^[a-z0-9-]+$/, \"slug: alleen kleine letters, cijfers en streepjes\")),\n /**\n * Beschrijving als platte tekst (regeleinden blijven behouden). Bewust géén HTML:\n * producten worden client-side door editors geschreven (rules-pad, niet via de\n * publish-sanitizer), dus HTML zou hier een XSS-route zijn.\n */\n description: z._default(z.string().check(z.maxLength(20_000)), \"\"),\n categoryIds: z._default(z.array(z.string().check(z.maxLength(80))).check(z.maxLength(20)), []),\n /** Afbeeldings-URL's (via de mediabibliotheek). Eerste = hoofdbeeld. */\n images: z._default(z.array(z.string().check(z.maxLength(2000))).check(z.maxLength(12)), []),\n /** Prijs incl. btw, in centen. */\n priceCents: z._default(z.int().check(z.minimum(0)).check(z.maximum(1_000_000_00)), 0),\n /** Doorgestreepte \"van\"-prijs (optioneel). */\n compareAtCents: z.optional(z.int().check(z.minimum(0)).check(z.maximum(1_000_000_00))),\n /** Btw-tarief (NL): 21, 9 of 0. Prijzen zijn inclusief. */\n vatRate: z._default(z.union([z.literal(21), z.literal(9), z.literal(0)]), 21),\n options: z._default(z.array(ProductOptionSchema).check(z.maxLength(3)), []),\n variants: z._default(z.array(ProductVariantSchema).check(z.maxLength(200)), []),\n /** Voorraad bijhouden? false = altijd bestelbaar (op-maat/print-on-demand). */\n trackStock: z._default(z.boolean(), false),\n /** Voorraad voor producten zónder varianten (met varianten telt de variant-stock). */\n stock: z.optional(z.int().check(z.minimum(0)).check(z.maximum(1_000_000))),\n status: z._default(ProductStatusSchema, \"draft\"),\n /** Sorteervolgorde binnen een categorie (laag = eerst). */\n order: z._default(z.int().check(z.minimum(0)).check(z.maximum(9999)), 0),\n featured: z._default(z.boolean(), false),\n /** 5.6: personalisatie-velden (eigen naam/logo) — max 3 per product. */\n personalization: z.optional(z.array(PersonalizationFieldSchema).check(z.maxLength(3))),\n /**\n * Staat \"persoonlijk maken\" aan voor dit product? (7.19)\n *\n * Voorheen was dit impliciet: velden aanwezig = aan. Wie het tijdelijk wilde uitzetten (te\n * druk, vakantie, leverancier levert de bedrukking niet) moest de velden VERWIJDEREN en dus\n * ook zijn labels, meerprijzen en uitleg kwijtraken. Nu blijft de configuratie staan en gaat\n * alleen de functie uit. Default true zodat bestaande producten met velden niets merken.\n */\n personalizationEnabled: z._default(z.boolean(), true),\n});\nexport type Product = z.infer<typeof ProductSchema>;\n\n/* ── Kortingscodes ───────────────────────────────────────────────────────── */\n\n/** `sites/{siteId}/discountCodes/{CODE}` (doc-id = code in hoofdletters; editor beheert). */\nexport const DiscountCodeSchema = z.object({\n code: z.string().check(z.minLength(3)).check(z.maxLength(30)).check(z.regex(/^[A-Z0-9-]+$/, \"code: hoofdletters, cijfers en streepjes\")),\n type: z.enum([\"percent\", \"fixed\"]),\n /** percent: 1–100; fixed: bedrag in centen. */\n value: z.int().check(z.minimum(1)).check(z.maximum(1_000_000_00)),\n minOrderCents: z._default(z.int().check(z.minimum(0)).check(z.maximum(1_000_000_00)), 0),\n freeShipping: z._default(z.boolean(), false),\n active: z._default(z.boolean(), true),\n /** ISO-datums (yyyy-mm-dd); leeg = geen grens. */\n startsAt: z.optional(z.string().check(z.maxLength(10))),\n endsAt: z.optional(z.string().check(z.maxLength(10))),\n /** 0 = onbeperkt. `usedCount` wordt door de webhook transactioneel opgehoogd. */\n maxUses: z._default(z.int().check(z.minimum(0)).check(z.maximum(1_000_000)), 0),\n usedCount: z._default(z.int().check(z.minimum(0)), 0),\n /**\n * Eén keer per e-mailadres (7.18) — de klassieke welkomstkorting. De server telt de eerder\n * BETAALDE orders van dat adres met deze code; een afgebroken checkout brandt hem dus niet op.\n */\n oncePerCustomer: z._default(z.boolean(), false),\n});\nexport type DiscountCode = z.infer<typeof DiscountCodeSchema>;\n\n/* ── Mail-templates ──────────────────────────────────────────────────────── */\n\n// Mailtemplates zijn site-breed (ook zonder shop) — de canonieke definitie staat in `./mail`.\n// Hier her-geëxporteerd voor backward-compat van bestaande shop-importeurs.\nexport {\n MAIL_TEMPLATE_KEYS,\n MAIL_TEMPLATE_DEFS,\n MailTemplateSchema,\n MailTemplatesSchema,\n type MailTemplateKey,\n type MailTemplates,\n} from \"./mail\";\n\n/* ── Bestellingen ────────────────────────────────────────────────────────── */\n\nexport const OrderStatusSchema = z.enum([\"pending\", \"paid\", \"shipped\", \"cancelled\", \"refunded\"]);\nexport type OrderStatus = z.infer<typeof OrderStatusSchema>;\n\n/** Orderregel: snapshot van naam/prijs op besteldatum — blijft kloppen als het product wijzigt. */\nexport const OrderItemSchema = z.object({\n kind: z._default(z.enum([\"product\", \"donation\"]), \"product\"),\n productId: z.optional(z.string().check(z.maxLength(80))),\n name: z.string().check(z.minLength(1)).check(z.maxLength(240)),\n variantKey: z.optional(z.string().check(z.maxLength(200))),\n qty: z.int().check(z.minimum(1)).check(z.maximum(999)),\n /** Incl. eventuele personalisatie-meerprijs. */\n unitPriceCents: z.int().check(z.minimum(0)).check(z.maximum(1_000_000_00)),\n vatRate: z._default(z.union([z.literal(21), z.literal(9), z.literal(0)]), 21),\n /** 5.6: ingevulde personalisatie (\"Naam: Boaz\", upload-pad). */\n personalization: z.optional(PersonalizationValueSchema),\n /**\n * 7.18: het deel van `unitPriceCents` dat personalisatie-meerprijs is.\n *\n * De meerprijs werkte al end-to-end, maar zat onzichtbaar in de stukprijs: een klant die\n * € 16,95 betaalde voor een product van € 13,95 zag nergens waar die € 3,00 vandaan kwam.\n * Apart opslaan (i.p.v. bij het tonen terugrekenen) houdt de historie kloppend als de\n * meerprijs van het product later verandert.\n */\n personalizationCents: z._default(z.int().check(z.minimum(0)), 0),\n /**\n * 7.22: de categorie waarin dit product verkocht is.\n *\n * Bij een winkel als Christelijk Kraamcadeau is `name` alleen het ontwerp (\"Blij blij\") en zegt\n * de categorie wát het is: rompertje, slabbetje, korte- of lange-mouw t-shirt. Hetzelfde ontwerp\n * bestaat in vijf categorieën, dus zonder dit veld staat er vier keer een identieke regel op de\n * pakbon, de factuur en in de bevestigingsmail — en weet niemand wat er de deur uit moet.\n *\n * Meegeschreven als momentopname, net als `name` en `unitPriceCents`: hernoem je later een\n * categorie, dan blijft een oude bestelling zeggen wat er destijds verkocht is.\n */\n categoryName: z.optional(z.string().check(z.maxLength(120))),\n});\nexport type OrderItem = z.infer<typeof OrderItemSchema>;\n\n/** \"Blij blij — Slabbetje\": de productnaam plus de categorie waarin hij verkocht is. */\nexport function orderRegelNaam(item: { name: string; categoryName?: string }): string {\n return item.categoryName ? `${item.name} — ${item.categoryName}` : item.name;\n}\n\nexport const OrderAddressSchema = z.object({\n address: z.string().check(z.minLength(1)).check(z.maxLength(200)),\n zip: z.string().check(z.minLength(1)).check(z.maxLength(20)),\n city: z.string().check(z.minLength(1)).check(z.maxLength(120)),\n country: z._default(z.string().check(z.minLength(2)).check(z.maxLength(2)), \"NL\"),\n});\nexport type OrderAddress = z.infer<typeof OrderAddressSchema>;\n\nexport const OrderCustomerSchema = z.object({\n email: z.email().check(z.maxLength(200)),\n name: z.string().check(z.minLength(1)).check(z.maxLength(200)),\n phone: z.optional(z.string().check(z.maxLength(40))),\n});\nexport type OrderCustomer = z.infer<typeof OrderCustomerSchema>;\n\n/** Volledige order zoals door Functions geschreven (timestamps komen daarbovenop). */\nexport const OrderSchema = z.object({\n number: z.string().check(z.minLength(1)).check(z.maxLength(40)),\n status: OrderStatusSchema,\n items: z.array(OrderItemSchema).check(z.minLength(1)).check(z.maxLength(50)),\n amount: z.object({\n itemsCents: z.int().check(z.minimum(0)),\n shippingCents: z.int().check(z.minimum(0)),\n /** 5.6: toegepaste korting (0 = geen). */\n discountCents: z._default(z.int().check(z.minimum(0)), 0),\n totalCents: z.int().check(z.minimum(0)),\n /**\n * 7.18: btw-bedrag in de order zelf. Werd eerder alleen op de factuur en in de CSV-export\n * afgeleid — twee losse kopieën van dezelfde som, terwijl dit voor de aangifte hét getal is.\n */\n vatCents: z.optional(z.int().check(z.minimum(0))),\n /** Btw per tarief (\"21\" → centen), voor de aangifte per categorie. */\n vatByRate: z.optional(z.record(z.string(), z.int())),\n }),\n /** 5.6: toegepaste kortingscode (snapshot). */\n discount: z.optional(z.object({\n code: z.string().check(z.maxLength(30)),\n type: z.enum([\"percent\", \"fixed\"]),\n value: z.int(),\n freeShipping: z._default(z.boolean(), false),\n })),\n customer: OrderCustomerSchema,\n shipping: z.optional(OrderAddressSchema),\n note: z.optional(z.string().check(z.maxLength(2000))),\n /** 7.18: interne notitie van de winkelier — de klant ziet hem nooit. */\n adminNote: z.optional(z.string().check(z.maxLength(2000))),\n /** 7.18: doorlopend factuurnummer (eigen reeks, zonder gaten) — pas gezet bij betaling. */\n invoiceNumber: z.optional(z.string().check(z.maxLength(40))),\n /** 7.18: handmatig aangemaakt in /beheer (telefonisch, markt, overschrijving). */\n manual: z.optional(z.boolean()),\n /** 7.18: som van alle (gedeeltelijke) terugbetalingen in centen. */\n refundedCents: z.optional(z.int().check(z.minimum(0))),\n /** 7.18: berichten die de winkelier vanuit /beheer aan de klant stuurde. */\n messages: z.optional(z.array(z.object({\n at: z.string().check(z.maxLength(40)),\n by: z.optional(z.string().check(z.maxLength(200))),\n subject: z.string().check(z.maxLength(200)),\n body: z.string().check(z.maxLength(2000)),\n })).check(z.maxLength(50))),\n /** 5.6: track & trace-link (gezet bij \"verzonden\"). */\n trackingUrl: z.optional(z.string().check(z.maxLength(500))),\n /** 5.6: factuur-pad in Storage (gezet door de webhook). */\n invoicePath: z.optional(z.string().check(z.maxLength(400))),\n stripe: z._default(z.object({\n sessionId: z.optional(z.string().check(z.maxLength(200))),\n paymentIntentId: z.optional(z.string().check(z.maxLength(200))),\n }), {}),\n});\nexport type Order = z.infer<typeof OrderSchema>;\n\n/* ── Callable-input (checkout) ───────────────────────────────────────────── */\n\n/**\n * Input van de `createOrder`-callable. Client-prijzen worden NOOIT vertrouwd: de functie\n * herrekent alles tegen de opgeslagen product-docs. Alleen `donationCents` komt van de\n * client (binnen de per-site ingestelde min/max van het donatie-block).\n */\nexport const CreateOrderItemInputSchema = z.object({\n productId: z.optional(z.string().check(z.minLength(1)).check(z.maxLength(80))),\n variantKey: z.optional(z.string().check(z.maxLength(200))),\n qty: z._default(z.int().check(z.minimum(1)).check(z.maximum(999)), 1),\n /** Alleen voor donaties (zonder productId). */\n donationCents: z.optional(z.int().check(z.minimum(50)).check(z.maximum(1_000_000_00))),\n /** 5.6: personalisatie-invoer (gevalideerd tegen product.personalization). */\n personalization: z.optional(PersonalizationValueSchema),\n});\n\nexport const CreateOrderInputSchema = z.object({\n siteId: z.string().check(z.minLength(1)).check(z.maxLength(80)),\n items: z.array(CreateOrderItemInputSchema).check(z.minLength(1)).check(z.maxLength(50)),\n customer: OrderCustomerSchema,\n shipping: z.optional(OrderAddressSchema),\n note: z.optional(z.string().check(z.maxLength(2000))),\n /** 5.6: kortingscode (server-side gevalideerd en verrekend). */\n discountCode: z.optional(z.string().check(z.maxLength(30))),\n /** Paden op de klantsite waarheen Stripe terug redirect. */\n successPath: z._default(z.string().check(z.maxLength(400)), \"/bedankt\"),\n cancelPath: z._default(z.string().check(z.maxLength(400)), \"/winkelwagen\"),\n /** Honeypot — moet leeg zijn (bot-detectie). */\n _hp: z.optional(z.string().check(z.maxLength(0))),\n});\nexport type CreateOrderInput = z.infer<typeof CreateOrderInputSchema>;\n\n/* ── Block-schema's ──────────────────────────────────────────────────────── */\n\nexport const ShopCategoriesBlockSchema = z.object({\n ...blockBase,\n _type: z.literal(\"shopCategories\"),\n heading: z.optional(z.string().check(z.maxLength(200))),\n columns: z._default(z.int().check(z.minimum(2)).check(z.maximum(4)), 3),\n});\n\nexport const ProductGridBlockSchema = z.object({\n ...blockBase,\n _type: z.literal(\"productGrid\"),\n heading: z.optional(z.string().check(z.maxLength(200))),\n /** Vast categorie-filter (slug). Leeg + useQueryCategory → filter via `?cat=` in de URL. */\n categorySlug: z.optional(z.string().check(z.maxLength(120))),\n useQueryCategory: z._default(z.boolean(), true),\n featuredOnly: z._default(z.boolean(), false),\n columns: z._default(z.int().check(z.minimum(2)).check(z.maximum(4)), 3),\n limit: z._default(z.int().check(z.minimum(1)).check(z.maximum(200)), 60),\n sort: z._default(z.enum([\"order\", \"name\", \"priceAsc\", \"priceDesc\"]), \"order\"),\n /** 5.6: filterchips (kleur/maat) + sorteerkeuze boven het grid. */\n showFilters: z._default(z.boolean(), false),\n});\n\nexport const ProductDetailBlockSchema = z.object({\n ...blockBase,\n _type: z.literal(\"productDetail\"),\n /** Vast product (slug); leeg → product uit `?p=` in de URL (één generieke detailpagina). */\n slug: z.optional(z.string().check(z.maxLength(120))),\n /** 5.6: \"Ook leuk\"-rij met producten uit dezelfde categorie. */\n showRelated: z._default(z.boolean(), true),\n});\n\nexport const CartBlockSchema = z.object({\n ...blockBase,\n _type: z.literal(\"cart\"),\n heading: z.optional(z.string().check(z.maxLength(200))),\n});\n\nexport const CheckoutBlockSchema = z.object({\n ...blockBase,\n _type: z.literal(\"checkout\"),\n heading: z.optional(z.string().check(z.maxLength(200))),\n introText: z.optional(z.string().check(z.maxLength(2000))),\n});\n\nexport const DonationBlockSchema = z.object({\n ...blockBase,\n _type: z.literal(\"donation\"),\n heading: z.optional(z.string().check(z.maxLength(200))),\n introText: z.optional(z.string().check(z.maxLength(2000))),\n /** Voorgestelde bedragen in centen. */\n amountsCents: z._default(z.array(z.int().check(z.minimum(50)).check(z.maximum(1_000_000_00))).check(z.maxLength(8)), [500, 1000, 2500]),\n allowCustom: z._default(z.boolean(), true),\n customMinCents: z._default(z.int().check(z.minimum(50)), 100),\n customMaxCents: z._default(z.int().check(z.maximum(1_000_000_00)), 100_000),\n buttonLabel: z._default(z.string().check(z.maxLength(80)), \"Doneer\"),\n});\n\nexport const OrderStatusBlockSchema = z.object({\n ...blockBase,\n _type: z.literal(\"orderStatus\"),\n successTitle: z._default(z.string().check(z.maxLength(200)), \"Bedankt voor je bestelling!\"),\n successText: z._default(z.string().check(z.maxLength(2000)), \"Je ontvangt zo een bevestiging per e-mail.\"),\n cancelledTitle: z._default(z.string().check(z.maxLength(200)), \"Betaling niet afgerond\"),\n cancelledText: z._default(z.string().check(z.maxLength(2000)), \"Je bestelling is niet betaald. Probeer het gerust opnieuw.\"),\n});\n\n/* ── Helpers (gedeeld client/server) ─────────────────────────────────────── */\n\nexport { variantKeyFor, productPriceCents, shippingCentsFor, formatCents } from \"./shopGeld\";\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../src/schema/shop.ts"],"sourcesContent":["import * as z from \"zod/mini\";\nimport { blockBase } from \"./blockBase\";\n\n/**\n * Shop-schema's (5.5) — webwinkel + donaties. Pure Zod: gedeeld door core, admin en Functions.\n *\n * Datamodel per tenant:\n * sites/{siteId}/shopCategories/{id} — categorieën (editor-write, publiek leesbaar)\n * sites/{siteId}/products/{id} — producten (editor-write, publiek leesbaar als actief)\n * sites/{siteId}/orders/{id} — bestellingen (ALLEEN via Functions; editor leest,\n * en mag alleen status/verzendvelden muteren)\n * sites/{siteId}/private/shop — geheimen (Stripe-key, SMTP) — deny-all, alleen Functions\n *\n * Geld is ALTIJD een integer in centen (nooit floats). Publieke shopconfig (verzendkosten,\n * paden, donatie-grenzen) leeft in de module-settings van de `shop`-module\n * (settings/public.modules.shop.config) — zie modules/shop/module.ts.\n */\n\n/* ── Categorieën ─────────────────────────────────────────────────────────── */\n\nexport const ShopCategorySchema = z.object({\n name: z.string().check(z.minLength(1)).check(z.maxLength(120)),\n slug: z.string().check(z.minLength(1)).check(z.maxLength(120)).check(z.regex(/^[a-z0-9-]+$/, \"slug: alleen kleine letters, cijfers en streepjes\")),\n description: z.optional(z.string().check(z.maxLength(2000))),\n imageUrl: z.optional(z.string().check(z.maxLength(2000))),\n order: z._default(z.int().check(z.minimum(0)).check(z.maximum(9999)), 0),\n visible: z._default(z.boolean(), true),\n});\nexport type ShopCategory = z.infer<typeof ShopCategorySchema>;\n\n/* ── Producten ───────────────────────────────────────────────────────────── */\n\n/** Optie-as, bv. { name: \"Maat\", values: [\"56\",\"62\",\"68\",\"74\"] }. Max 3 assen per product. */\nexport const ProductOptionSchema = z.object({\n name: z.string().check(z.minLength(1)).check(z.maxLength(40)),\n values: z.array(z.string().check(z.minLength(1)).check(z.maxLength(60))).check(z.minLength(1)).check(z.maxLength(30)),\n /**\n * 5.6: afbeelding per optieWAARDE — `{ \"Roze\": \"https://…\" }`. De detailpagina wisselt het\n * hoofdbeeld mee met de keuze; bewust per as en niet per variant-combinatie (maat × kleur zou\n * het aantal afbeeldingen laten exploderen).\n */\n images: z.optional(z.record(z.string(), z.string().check(z.maxLength(2000)))),\n /**\n * 7.23: heeft élke waarde van deze as een eigen foto?\n *\n * Tot nu toe was dat geen keuze maar een naamtruc: alleen een as die \"Kleur\"/\"Color\"/\"Colour\"\n * heette kreeg fotoknoppen. Noemde je hem \"Print\", \"Dessin\" of \"Uitvoering\", dan kon je er geen\n * foto's aan hangen — en hernoemde je een bestaande kleur-as, dan verdwenen de knoppen terwijl\n * de foto's onzichtbaar in het document achterbleven. Nu is het een vinkje in het\n * productformulier en staat de naam er los van.\n *\n * Drie toestanden, met opzet: `true` = aan, `false` = uit, en **afwezig** = \"nooit ingesteld\",\n * en dan valt `beeldOptie()` terug op de oude naamcheck. Zo blijven alle bestaande producten\n * werken zonder migratie, terwijl één klik de nieuwe regel vastlegt.\n *\n * Eén as tegelijk: het hoofdbeeld kan maar één keuze volgen. Het formulier zet de vlag daarom\n * bij de andere assen uit zodra je hem hier aanzet.\n */\n imagePerValue: z.optional(z.boolean()),\n});\nexport type ProductOption = z.infer<typeof ProductOptionSchema>;\n\n/** Heeft deze as een eigen foto per waarde? Zonder expliciete keuze: de oude kleur-naamregel. */\nexport function optieHeeftBeeldPerWaarde(optie: Pick<ProductOption, \"name\" | \"imagePerValue\">): boolean {\n return optie.imagePerValue ?? /^kleur|^colou?r/i.test(optie.name);\n}\n\n/**\n * Personalisatie-veld (5.6): de klant geeft eigen tekst of een eigen afbeelding op\n * (bv. \"rompertje met naam\"). Meerprijs telt server-side mee in `createOrder`.\n */\nexport const PersonalizationFieldSchema = z.object({\n key: z.string().check(z.minLength(1)).check(z.maxLength(40)).check(z.regex(/^[a-zA-Z][a-zA-Z0-9_]*$/, \"sleutel: letters/cijfers/underscore, beginnend met een letter\")),\n label: z.string().check(z.minLength(1)).check(z.maxLength(120)),\n type: z.enum([\"text\", \"image\"]),\n required: z._default(z.boolean(), false),\n /** Alleen voor type \"text\". */\n maxLength: z._default(z.int().check(z.minimum(1)).check(z.maximum(200)), 40),\n /** Meerprijs in centen (per stuk). */\n priceCents: z._default(z.int().check(z.minimum(0)).check(z.maximum(1_000_000_00)), 0),\n helpText: z.optional(z.string().check(z.maxLength(200))),\n});\nexport type PersonalizationField = z.infer<typeof PersonalizationFieldSchema>;\n\n/** Ingevulde personalisatie op een orderregel/cartregel: per veld-key tekst of upload-pad. */\nexport const PersonalizationValueSchema = z.record(\n z.string(),\n z.object({\n text: z.optional(z.string().check(z.maxLength(200))),\n /** Storage-pad uit de uploadPersonalization-callable (géén vrije URL). */\n imagePath: z.optional(z.string().check(z.maxLength(400))),\n /** Oorspronkelijke bestandsnaam (weergave in beheer/mail). */\n fileName: z.optional(z.string().check(z.maxLength(200))),\n }),\n);\nexport type PersonalizationValue = z.infer<typeof PersonalizationValueSchema>;\n\n/**\n * Variant = één combinatie van optiewaarden. `key` is de waarden gejoined met \"|\" in\n * optie-volgorde (bv. \"68|Roze\"). Alleen aanwezig waar hij afwijkt van de product-defaults.\n */\nexport const ProductVariantSchema = z.object({\n key: z.string().check(z.minLength(1)).check(z.maxLength(200)),\n priceCents: z.optional(z.int().check(z.minimum(0)).check(z.maximum(1_000_000_00))),\n stock: z.optional(z.int().check(z.minimum(0)).check(z.maximum(1_000_000))),\n sku: z.optional(z.string().check(z.maxLength(60))),\n active: z._default(z.boolean(), true),\n});\nexport type ProductVariant = z.infer<typeof ProductVariantSchema>;\n\nexport const ProductStatusSchema = z.enum([\"draft\", \"active\", \"archived\"]);\nexport type ProductStatus = z.infer<typeof ProductStatusSchema>;\n\n/**\n * Verwijdert top-level `null`-velden uit een productdoc vóór validatie (7.17.1).\n *\n * Er liggen productdocs met `personalization: null` (e.d.) in Firestore: de functions-SDK\n * encodeerde een client-side `undefined` als `null` over de draad (dezelfde klasse als de\n * \"+ Nieuw product\"-bug), en de callable schreef dat door. `z.optional()` accepteert een\n * ONTBREKEND veld maar geen `null`, en `parseProduct` op de leeskant slaat een ongeldig doc\n * stil over — waardoor een product dat gewoon \"actief\" in het beheer staat van de live winkel\n * verdween (gemeten op christelijkkraamcadeau: 2 van de 4 \"Maak een vrolijk geluid\"-varianten,\n * precies de twee die vóór de 7.16.2-fix nog eens waren opgeslagen). In dit model betekent\n * `null` nergens iets anders dan \"afwezig\", dus strippen is verliesvrij.\n */\nexport function saneerProductDoc<T>(data: T): T {\n if (typeof data !== \"object\" || data === null || Array.isArray(data)) return data;\n return Object.fromEntries(\n Object.entries(data as Record<string, unknown>).filter(([, v]) => v !== null),\n ) as T;\n}\n\nexport const ProductSchema = z.object({\n name: z.string().check(z.minLength(1)).check(z.maxLength(200)),\n slug: z.string().check(z.minLength(1)).check(z.maxLength(120)).check(z.regex(/^[a-z0-9-]+$/, \"slug: alleen kleine letters, cijfers en streepjes\")),\n /**\n * Beschrijving als platte tekst (regeleinden blijven behouden). Bewust géén HTML:\n * producten worden client-side door editors geschreven (rules-pad, niet via de\n * publish-sanitizer), dus HTML zou hier een XSS-route zijn.\n */\n description: z._default(z.string().check(z.maxLength(20_000)), \"\"),\n categoryIds: z._default(z.array(z.string().check(z.maxLength(80))).check(z.maxLength(20)), []),\n /** Afbeeldings-URL's (via de mediabibliotheek). Eerste = hoofdbeeld. */\n images: z._default(z.array(z.string().check(z.maxLength(2000))).check(z.maxLength(12)), []),\n /** Prijs incl. btw, in centen. */\n priceCents: z._default(z.int().check(z.minimum(0)).check(z.maximum(1_000_000_00)), 0),\n /** Doorgestreepte \"van\"-prijs (optioneel). */\n compareAtCents: z.optional(z.int().check(z.minimum(0)).check(z.maximum(1_000_000_00))),\n /** Btw-tarief (NL): 21, 9 of 0. Prijzen zijn inclusief. */\n vatRate: z._default(z.union([z.literal(21), z.literal(9), z.literal(0)]), 21),\n options: z._default(z.array(ProductOptionSchema).check(z.maxLength(3)), []),\n variants: z._default(z.array(ProductVariantSchema).check(z.maxLength(200)), []),\n /** Voorraad bijhouden? false = altijd bestelbaar (op-maat/print-on-demand). */\n trackStock: z._default(z.boolean(), false),\n /** Voorraad voor producten zónder varianten (met varianten telt de variant-stock). */\n stock: z.optional(z.int().check(z.minimum(0)).check(z.maximum(1_000_000))),\n status: z._default(ProductStatusSchema, \"draft\"),\n /** Sorteervolgorde binnen een categorie (laag = eerst). */\n order: z._default(z.int().check(z.minimum(0)).check(z.maximum(9999)), 0),\n featured: z._default(z.boolean(), false),\n /** 5.6: personalisatie-velden (eigen naam/logo) — max 3 per product. */\n personalization: z.optional(z.array(PersonalizationFieldSchema).check(z.maxLength(3))),\n /**\n * Staat \"persoonlijk maken\" aan voor dit product? (7.19)\n *\n * Voorheen was dit impliciet: velden aanwezig = aan. Wie het tijdelijk wilde uitzetten (te\n * druk, vakantie, leverancier levert de bedrukking niet) moest de velden VERWIJDEREN en dus\n * ook zijn labels, meerprijzen en uitleg kwijtraken. Nu blijft de configuratie staan en gaat\n * alleen de functie uit. Default true zodat bestaande producten met velden niets merken.\n */\n personalizationEnabled: z._default(z.boolean(), true),\n});\nexport type Product = z.infer<typeof ProductSchema>;\n\n/* ── Kortingscodes ───────────────────────────────────────────────────────── */\n\n/** `sites/{siteId}/discountCodes/{CODE}` (doc-id = code in hoofdletters; editor beheert). */\nexport const DiscountCodeSchema = z.object({\n code: z.string().check(z.minLength(3)).check(z.maxLength(30)).check(z.regex(/^[A-Z0-9-]+$/, \"code: hoofdletters, cijfers en streepjes\")),\n type: z.enum([\"percent\", \"fixed\"]),\n /** percent: 1–100; fixed: bedrag in centen. */\n value: z.int().check(z.minimum(1)).check(z.maximum(1_000_000_00)),\n minOrderCents: z._default(z.int().check(z.minimum(0)).check(z.maximum(1_000_000_00)), 0),\n freeShipping: z._default(z.boolean(), false),\n active: z._default(z.boolean(), true),\n /** ISO-datums (yyyy-mm-dd); leeg = geen grens. */\n startsAt: z.optional(z.string().check(z.maxLength(10))),\n endsAt: z.optional(z.string().check(z.maxLength(10))),\n /** 0 = onbeperkt. `usedCount` wordt door de webhook transactioneel opgehoogd. */\n maxUses: z._default(z.int().check(z.minimum(0)).check(z.maximum(1_000_000)), 0),\n usedCount: z._default(z.int().check(z.minimum(0)), 0),\n /**\n * Eén keer per e-mailadres (7.18) — de klassieke welkomstkorting. De server telt de eerder\n * BETAALDE orders van dat adres met deze code; een afgebroken checkout brandt hem dus niet op.\n */\n oncePerCustomer: z._default(z.boolean(), false),\n});\nexport type DiscountCode = z.infer<typeof DiscountCodeSchema>;\n\n/* ── Mail-templates ──────────────────────────────────────────────────────── */\n\n// Mailtemplates zijn site-breed (ook zonder shop) — de canonieke definitie staat in `./mail`.\n// Hier her-geëxporteerd voor backward-compat van bestaande shop-importeurs.\nexport {\n MAIL_TEMPLATE_KEYS,\n MAIL_TEMPLATE_DEFS,\n MailTemplateSchema,\n MailTemplatesSchema,\n type MailTemplateKey,\n type MailTemplates,\n} from \"./mail\";\n\n/* ── Bestellingen ────────────────────────────────────────────────────────── */\n\nexport const OrderStatusSchema = z.enum([\"pending\", \"paid\", \"shipped\", \"cancelled\", \"refunded\"]);\nexport type OrderStatus = z.infer<typeof OrderStatusSchema>;\n\n/** Orderregel: snapshot van naam/prijs op besteldatum — blijft kloppen als het product wijzigt. */\nexport const OrderItemSchema = z.object({\n kind: z._default(z.enum([\"product\", \"donation\"]), \"product\"),\n productId: z.optional(z.string().check(z.maxLength(80))),\n name: z.string().check(z.minLength(1)).check(z.maxLength(240)),\n variantKey: z.optional(z.string().check(z.maxLength(200))),\n qty: z.int().check(z.minimum(1)).check(z.maximum(999)),\n /** Incl. eventuele personalisatie-meerprijs. */\n unitPriceCents: z.int().check(z.minimum(0)).check(z.maximum(1_000_000_00)),\n vatRate: z._default(z.union([z.literal(21), z.literal(9), z.literal(0)]), 21),\n /** 5.6: ingevulde personalisatie (\"Naam: Boaz\", upload-pad). */\n personalization: z.optional(PersonalizationValueSchema),\n /**\n * 7.18: het deel van `unitPriceCents` dat personalisatie-meerprijs is.\n *\n * De meerprijs werkte al end-to-end, maar zat onzichtbaar in de stukprijs: een klant die\n * € 16,95 betaalde voor een product van € 13,95 zag nergens waar die € 3,00 vandaan kwam.\n * Apart opslaan (i.p.v. bij het tonen terugrekenen) houdt de historie kloppend als de\n * meerprijs van het product later verandert.\n */\n personalizationCents: z._default(z.int().check(z.minimum(0)), 0),\n /**\n * 7.22: de categorie waarin dit product verkocht is.\n *\n * Bij een winkel als Christelijk Kraamcadeau is `name` alleen het ontwerp (\"Blij blij\") en zegt\n * de categorie wát het is: rompertje, slabbetje, korte- of lange-mouw t-shirt. Hetzelfde ontwerp\n * bestaat in vijf categorieën, dus zonder dit veld staat er vier keer een identieke regel op de\n * pakbon, de factuur en in de bevestigingsmail — en weet niemand wat er de deur uit moet.\n *\n * Meegeschreven als momentopname, net als `name` en `unitPriceCents`: hernoem je later een\n * categorie, dan blijft een oude bestelling zeggen wat er destijds verkocht is.\n */\n categoryName: z.optional(z.string().check(z.maxLength(120))),\n});\nexport type OrderItem = z.infer<typeof OrderItemSchema>;\n\n/** \"Blij blij — Slabbetje\": de productnaam plus de categorie waarin hij verkocht is. */\nexport function orderRegelNaam(item: { name: string; categoryName?: string }): string {\n return item.categoryName ? `${item.name} — ${item.categoryName}` : item.name;\n}\n\nexport const OrderAddressSchema = z.object({\n address: z.string().check(z.minLength(1)).check(z.maxLength(200)),\n zip: z.string().check(z.minLength(1)).check(z.maxLength(20)),\n city: z.string().check(z.minLength(1)).check(z.maxLength(120)),\n country: z._default(z.string().check(z.minLength(2)).check(z.maxLength(2)), \"NL\"),\n});\nexport type OrderAddress = z.infer<typeof OrderAddressSchema>;\n\nexport const OrderCustomerSchema = z.object({\n email: z.email().check(z.maxLength(200)),\n name: z.string().check(z.minLength(1)).check(z.maxLength(200)),\n phone: z.optional(z.string().check(z.maxLength(40))),\n});\nexport type OrderCustomer = z.infer<typeof OrderCustomerSchema>;\n\n/** Volledige order zoals door Functions geschreven (timestamps komen daarbovenop). */\nexport const OrderSchema = z.object({\n number: z.string().check(z.minLength(1)).check(z.maxLength(40)),\n status: OrderStatusSchema,\n items: z.array(OrderItemSchema).check(z.minLength(1)).check(z.maxLength(50)),\n amount: z.object({\n itemsCents: z.int().check(z.minimum(0)),\n shippingCents: z.int().check(z.minimum(0)),\n /** 5.6: toegepaste korting (0 = geen). */\n discountCents: z._default(z.int().check(z.minimum(0)), 0),\n totalCents: z.int().check(z.minimum(0)),\n /**\n * 7.18: btw-bedrag in de order zelf. Werd eerder alleen op de factuur en in de CSV-export\n * afgeleid — twee losse kopieën van dezelfde som, terwijl dit voor de aangifte hét getal is.\n */\n vatCents: z.optional(z.int().check(z.minimum(0))),\n /** Btw per tarief (\"21\" → centen), voor de aangifte per categorie. */\n vatByRate: z.optional(z.record(z.string(), z.int())),\n }),\n /** 5.6: toegepaste kortingscode (snapshot). */\n discount: z.optional(z.object({\n code: z.string().check(z.maxLength(30)),\n type: z.enum([\"percent\", \"fixed\"]),\n value: z.int(),\n freeShipping: z._default(z.boolean(), false),\n })),\n customer: OrderCustomerSchema,\n shipping: z.optional(OrderAddressSchema),\n note: z.optional(z.string().check(z.maxLength(2000))),\n /** 7.18: interne notitie van de winkelier — de klant ziet hem nooit. */\n adminNote: z.optional(z.string().check(z.maxLength(2000))),\n /** 7.18: doorlopend factuurnummer (eigen reeks, zonder gaten) — pas gezet bij betaling. */\n invoiceNumber: z.optional(z.string().check(z.maxLength(40))),\n /** 7.18: handmatig aangemaakt in /beheer (telefonisch, markt, overschrijving). */\n manual: z.optional(z.boolean()),\n /** 7.18: som van alle (gedeeltelijke) terugbetalingen in centen. */\n refundedCents: z.optional(z.int().check(z.minimum(0))),\n /** 7.18: berichten die de winkelier vanuit /beheer aan de klant stuurde. */\n messages: z.optional(z.array(z.object({\n at: z.string().check(z.maxLength(40)),\n by: z.optional(z.string().check(z.maxLength(200))),\n subject: z.string().check(z.maxLength(200)),\n body: z.string().check(z.maxLength(2000)),\n })).check(z.maxLength(50))),\n /** 5.6: track & trace-link (gezet bij \"verzonden\"). */\n trackingUrl: z.optional(z.string().check(z.maxLength(500))),\n /** 5.6: factuur-pad in Storage (gezet door de webhook). */\n invoicePath: z.optional(z.string().check(z.maxLength(400))),\n stripe: z._default(z.object({\n sessionId: z.optional(z.string().check(z.maxLength(200))),\n paymentIntentId: z.optional(z.string().check(z.maxLength(200))),\n }), {}),\n});\nexport type Order = z.infer<typeof OrderSchema>;\n\n/* ── Callable-input (checkout) ───────────────────────────────────────────── */\n\n/**\n * Input van de `createOrder`-callable. Client-prijzen worden NOOIT vertrouwd: de functie\n * herrekent alles tegen de opgeslagen product-docs. Alleen `donationCents` komt van de\n * client (binnen de per-site ingestelde min/max van het donatie-block).\n */\nexport const CreateOrderItemInputSchema = z.object({\n productId: z.optional(z.string().check(z.minLength(1)).check(z.maxLength(80))),\n variantKey: z.optional(z.string().check(z.maxLength(200))),\n qty: z._default(z.int().check(z.minimum(1)).check(z.maximum(999)), 1),\n /** Alleen voor donaties (zonder productId). */\n donationCents: z.optional(z.int().check(z.minimum(50)).check(z.maximum(1_000_000_00))),\n /** 5.6: personalisatie-invoer (gevalideerd tegen product.personalization). */\n personalization: z.optional(PersonalizationValueSchema),\n});\n\nexport const CreateOrderInputSchema = z.object({\n siteId: z.string().check(z.minLength(1)).check(z.maxLength(80)),\n items: z.array(CreateOrderItemInputSchema).check(z.minLength(1)).check(z.maxLength(50)),\n customer: OrderCustomerSchema,\n shipping: z.optional(OrderAddressSchema),\n note: z.optional(z.string().check(z.maxLength(2000))),\n /** 5.6: kortingscode (server-side gevalideerd en verrekend). */\n discountCode: z.optional(z.string().check(z.maxLength(30))),\n /** Paden op de klantsite waarheen Stripe terug redirect. */\n successPath: z._default(z.string().check(z.maxLength(400)), \"/bedankt\"),\n cancelPath: z._default(z.string().check(z.maxLength(400)), \"/winkelwagen\"),\n /** Honeypot — moet leeg zijn (bot-detectie). */\n _hp: z.optional(z.string().check(z.maxLength(0))),\n});\nexport type CreateOrderInput = z.infer<typeof CreateOrderInputSchema>;\n\n/* ── Block-schema's ──────────────────────────────────────────────────────── */\n\nexport const ShopCategoriesBlockSchema = z.object({\n ...blockBase,\n _type: z.literal(\"shopCategories\"),\n heading: z.optional(z.string().check(z.maxLength(200))),\n columns: z._default(z.int().check(z.minimum(2)).check(z.maximum(4)), 3),\n});\n\nexport const ProductGridBlockSchema = z.object({\n ...blockBase,\n _type: z.literal(\"productGrid\"),\n heading: z.optional(z.string().check(z.maxLength(200))),\n /** Vast categorie-filter (slug). Leeg + useQueryCategory → filter via `?cat=` in de URL. */\n categorySlug: z.optional(z.string().check(z.maxLength(120))),\n useQueryCategory: z._default(z.boolean(), true),\n featuredOnly: z._default(z.boolean(), false),\n columns: z._default(z.int().check(z.minimum(2)).check(z.maximum(4)), 3),\n limit: z._default(z.int().check(z.minimum(1)).check(z.maximum(200)), 60),\n sort: z._default(z.enum([\"order\", \"name\", \"priceAsc\", \"priceDesc\"]), \"order\"),\n /** 5.6: filterchips (kleur/maat) + sorteerkeuze boven het grid. */\n showFilters: z._default(z.boolean(), false),\n});\n\nexport const ProductDetailBlockSchema = z.object({\n ...blockBase,\n _type: z.literal(\"productDetail\"),\n /** Vast product (slug); leeg → product uit `?p=` in de URL (één generieke detailpagina). */\n slug: z.optional(z.string().check(z.maxLength(120))),\n /** 5.6: \"Ook leuk\"-rij met producten uit dezelfde categorie. */\n showRelated: z._default(z.boolean(), true),\n});\n\nexport const CartBlockSchema = z.object({\n ...blockBase,\n _type: z.literal(\"cart\"),\n heading: z.optional(z.string().check(z.maxLength(200))),\n});\n\nexport const CheckoutBlockSchema = z.object({\n ...blockBase,\n _type: z.literal(\"checkout\"),\n heading: z.optional(z.string().check(z.maxLength(200))),\n introText: z.optional(z.string().check(z.maxLength(2000))),\n});\n\nexport const DonationBlockSchema = z.object({\n ...blockBase,\n _type: z.literal(\"donation\"),\n heading: z.optional(z.string().check(z.maxLength(200))),\n introText: z.optional(z.string().check(z.maxLength(2000))),\n /** Voorgestelde bedragen in centen. */\n amountsCents: z._default(z.array(z.int().check(z.minimum(50)).check(z.maximum(1_000_000_00))).check(z.maxLength(8)), [500, 1000, 2500]),\n allowCustom: z._default(z.boolean(), true),\n customMinCents: z._default(z.int().check(z.minimum(50)), 100),\n customMaxCents: z._default(z.int().check(z.maximum(1_000_000_00)), 100_000),\n buttonLabel: z._default(z.string().check(z.maxLength(80)), \"Doneer\"),\n});\n\nexport const OrderStatusBlockSchema = z.object({\n ...blockBase,\n _type: z.literal(\"orderStatus\"),\n successTitle: z._default(z.string().check(z.maxLength(200)), \"Bedankt voor je bestelling!\"),\n successText: z._default(z.string().check(z.maxLength(2000)), \"Je ontvangt zo een bevestiging per e-mail.\"),\n cancelledTitle: z._default(z.string().check(z.maxLength(200)), \"Betaling niet afgerond\"),\n cancelledText: z._default(z.string().check(z.maxLength(2000)), \"Je bestelling is niet betaald. Probeer het gerust opnieuw.\"),\n});\n\n/* ── Helpers (gedeeld client/server) ─────────────────────────────────────── */\n\nexport { variantKeyFor, productPriceCents, shippingCentsFor, formatCents } from \"./shopGeld\";\n"],"mappings":"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","names":[]}
package/dist/styles.css CHANGED
@@ -769,21 +769,28 @@
769
769
 
770
770
  /* Zwevende werkbalk voor editors */
771
771
  /*
772
- * De beheerbalk ZWEEFT bovenaan, gecentreerd — en in beide toestanden op precies dezelfde plek.
772
+ * De beheerbalk ZWEEFT ONDERAAN, gecentreerd — en in beide toestanden op precies dezelfde plek.
773
773
  *
774
- * Geschiedenis in twee stappen. Hij zweefde ooit als pilletje onderaan, terwijl de editor zijn
774
+ * Geschiedenis in drie stappen. Hij zweefde ooit als pilletje onderaan, terwijl de editor zijn
775
775
  * eigen balk bovenaan meebracht: overschakelen verplaatste je houvast. 7.8 loste dat op met één
776
776
  * vaste balk van rand tot rand — maar die las als een deel van de site in plaats van als
777
- * gereedschap dat erover heen ligt. 7.16 combineert beide lessen: wéér een zwevend pilletje
778
- * (duidelijk gereedschap, de site blijft zichtbaar), maar bovenaan gecentreerd en in beide
779
- * modi identiek gepositioneerd, zodat het houvast van 7.8 blijft.
777
+ * gereedschap dat erover heen ligt. 7.16 maakte er weer een zwevend pilletje van, nu bovenaan
778
+ * gecentreerd en in beide modi identiek.
780
779
  *
781
- * Onderaan blijft van de publiceerknop (sinds 7.8): die hoort bij je werk, niet bij de navigatie.
780
+ * 7.22 haalt hem terug naar onderen, om één reden: bovenaan lag hij pal over het hoofdmenu van
781
+ * de site. Precies de plek waar elke site zijn navigatie heeft, dus op elke klantsite dekte het
782
+ * gereedschap de menu-items af — en je kon niet meer klikken waar je heen wilde. Onderaan is er
783
+ * bijna nooit iets dat je nodig hebt terwijl je aan het bewerken bent, en de publiceerknop staat
784
+ * daar toch al: al het gereedschap zit nu aan dezelfde kant van het scherm.
785
+ *
786
+ * Publiceren en Verwerpen staan rechts, de balk in het midden — die botsen dus niet. Alleen op
787
+ * smalle schermen wordt de balk zo breed als het scherm; daar schuiven die twee knoppen erboven
788
+ * (zie de media-query bij .xicms-ve-publiceer).
782
789
  */
783
790
  .xicms-editbar,
784
791
  .xicms-ve__bar {
785
792
  position: fixed;
786
- top: 10px;
793
+ bottom: calc(10px + var(--xicms-bottombar, 0px));
787
794
  left: 50%;
788
795
  transform: translateX(-50%);
789
796
  max-width: calc(100vw - 16px);
@@ -804,17 +811,6 @@
804
811
  font-size: 0.8rem;
805
812
  line-height: 1;
806
813
  }
807
- /*
808
- * De balk dekt de bovenkant van de site af; duw de pagina omlaag zolang hij er staat.
809
- *
810
- * Op `:root` en niet op `body`: de padding hoort bij de scroll-container, en `body` is bij
811
- * klantsites veel vaker het element waar eigen CSS aan zit (achtergronden, grid, `min-height`).
812
- * Hoe minder we aan `body` doen, hoe kleiner de kans dat deze balk iets in de weg legt op een
813
- * site die we niet kennen. Layout-technisch is het resultaat hetzelfde.
814
- *
815
- * Padding alleen is niet genoeg: een `position: sticky; top: 0`-header plakt aan het VENSTER en
816
- * schuift hier gewoon onderdoor. Die worden apart opzijgezet — zie components/barOffset.ts.
817
- */
818
814
  /* De balk scrolt op smalle schermen horizontaal; de scrollbalk zelf is dan alleen ruis. */
819
815
  .xicms-editbar,
820
816
  .xicms-ve__bar {
@@ -825,11 +821,11 @@
825
821
  display: none;
826
822
  }
827
823
 
828
- /* GEEN pagina-offset meer: het pilletje ZWEEFT over de site heen, de pagina schuift er gewoon
829
- onderdoor. De vorige vaste balk duwde alles omlaag (padding op :root + sticky-header-correctie
824
+ /* GEEN pagina-offset: het pilletje ZWEEFT over de site heen, de pagina schuift er gewoon
825
+ onderdoor. De vaste balk van 7.8 duwde alles omlaag (padding op :root + sticky-header-correctie
830
826
  in barOffset.ts) — dat hoorde bij een balk van rand tot rand, niet bij zwevend gereedschap.
831
- Bewuste afweging: gecentreerd bovenin kan het pilletje een stukje site-header overlappen; dat
832
- is de prijs van een site die er verder exact zo bijstaat als voor de bezoeker. */
827
+ Nu hij onderaan staat, vervalt ook het sticky-header-probleem: een `position: sticky; top: 0`
828
+ header schoof onder de bovenbalk door, onderaan is er niets om onderdoor te schuiven. */
833
829
  /* Smalle schermen: de balk hield alles en werd twee regels hoog — en de pagina-offset (2.6rem,
834
830
  vast) rekent op één regel, dus de site schoof onder de balk. Het pad en de statustekst zijn op
835
831
  een telefoon ballast; de vier moduskoppen en het gereedschap zijn de balk. */
@@ -3333,6 +3329,17 @@
3333
3329
  z-index: 10006;
3334
3330
  transition: right 0.2s ease;
3335
3331
  }
3332
+ /* Smalle schermen: daar is de beheerbalk geen gecentreerd pilletje meer maar loopt hij van rand
3333
+ tot rand (zie de media-query bij .xicms-editbar), dus rechtsonder botst hij wél met deze twee.
3334
+ Ze schuiven er dan bovenuit: balk 10px + ~40px hoog = 50, plus lucht. */
3335
+ @media (max-width: 720px) {
3336
+ .xicms-ve-publiceer {
3337
+ bottom: calc(66px + var(--xicms-bottombar, 0px));
3338
+ }
3339
+ .xicms-ve-verwerp {
3340
+ bottom: calc(140px + var(--xicms-bottombar, 0px));
3341
+ }
3342
+ }
3336
3343
  /* Lade open → knop schuift mee naar links, anders ligt hij óver het formulier waarin je net
3337
3344
  aan het typen bent. Alleen op schermen waar de lade een zijlade is (op mobiel komt hij van
3338
3345
  onderen en blijft de knop gewoon rechts). */
package/dist/version.js CHANGED
@@ -1,4 +1,4 @@
1
- const CORE_VERSION = true ? "7.22.2" : "0.0.0-dev";
1
+ const CORE_VERSION = true ? "7.23.0" : "0.0.0-dev";
2
2
  const CORE_PACKAGE = "@xiwebdesign/xicms-core";
3
3
  function isNieuwer(beschikbaar, huidig) {
4
4
  const delen = (v) => v.split("-")[0].split(".").map((n) => Number.parseInt(n, 10) || 0);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xiwebdesign/xicms-core",
3
- "version": "7.22.2",
3
+ "version": "7.23.0",
4
4
  "description": "XiCMS engine: gestructureerde content, pagina's, draft/publish, admin (TipTap+Radix), SEO en beveiliging voor klantsites.",
5
5
  "license": "UNLICENSED",
6
6
  "repository": {