@xiwebdesign/xicms-core 7.17.0 → 7.17.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/admin/shop/ProductsTab.js +3 -2
- package/dist/admin/shop/ProductsTab.js.map +1 -1
- package/dist/schema/shop.d.ts +13 -0
- package/dist/schema/shop.js +7 -0
- package/dist/schema/shop.js.map +1 -1
- package/dist/server/shop.js +3 -2
- package/dist/server/shop.js.map +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
|
@@ -6,7 +6,8 @@ import { removeProduct, saveProductDoc } from "./productClient.js";
|
|
|
6
6
|
import { getDb } from "../../firebase/client.js";
|
|
7
7
|
import {
|
|
8
8
|
ProductSchema,
|
|
9
|
-
formatCents
|
|
9
|
+
formatCents,
|
|
10
|
+
saneerProductDoc
|
|
10
11
|
} from "../../schema/shop.js";
|
|
11
12
|
import { Badge, Button, Card, Field, Input, Textarea } from "../ui/primitives.js";
|
|
12
13
|
import { Select } from "../ui/select.js";
|
|
@@ -34,7 +35,7 @@ function ProductsTab({ siteId }) {
|
|
|
34
35
|
const un1 = onSnapshot(
|
|
35
36
|
collection(db, "sites", siteId, "products"),
|
|
36
37
|
(snap) => setProducts(
|
|
37
|
-
snap.docs.map((d) => ({ id: d.id, ...d.data() })).sort((a, b) => (a.order ?? 0) - (b.order ?? 0) || a.name.localeCompare(b.name, "nl"))
|
|
38
|
+
snap.docs.map((d) => ({ id: d.id, ...saneerProductDoc(d.data()) })).sort((a, b) => (a.order ?? 0) - (b.order ?? 0) || a.name.localeCompare(b.name, "nl"))
|
|
38
39
|
)
|
|
39
40
|
);
|
|
40
41
|
const un2 = onSnapshot(
|
|
@@ -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 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 .map((d) => ({ id: d.id, ...(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 } 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 <span className=\"min-w-0 flex-1 truncate\">{p.name}</span>\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 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 */\nexport function ProductForm({\n siteId,\n product,\n categories,\n onDeleted,\n onToast,\n confirm,\n}: {\n siteId: string;\n product: StoredProduct;\n categories: StoredCategory[];\n onDeleted: () => 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 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 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\" />\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 </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\">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 <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 <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 </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 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 </div>\n <Button variant=\"danger\" onClick={remove}>Verwijderen</Button>\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 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 } 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 <span className=\"min-w-0 flex-1 truncate\">{p.name}</span>\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 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 */\nexport function ProductForm({\n siteId,\n product,\n categories,\n onDeleted,\n onToast,\n confirm,\n}: {\n siteId: string;\n product: StoredProduct;\n categories: StoredCategory[];\n onDeleted: () => 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 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 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\" />\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 </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\">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 <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 <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 </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 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 </div>\n <Button variant=\"danger\" onClick={remove}>Verwijderen</Button>\n </div>\n </div>\n </Card>\n );\n}\n"],"mappings":";AAsGQ,SACE,KADF;AApGR,SAAS,WAAW,SAAS,gBAAgB;AAC7C,SAAS,YAAiB,kBAAkB;AAC5C,SAAS,eAAe,sBAAsB;AAC9C,SAAS,aAAa;AACtB;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,OAMK;AACP,SAAS,OAAO,QAAQ,MAAM,OAAO,OAAO,gBAAgB;AAC5D,SAAS,cAAc;AACvB,SAAS,cAAc;AACvB,SAAS,gBAAgB;AACzB,SAAS,kBAAuC;AAChD,SAAS,yBAAyB;AAClC,SAAS,eAAe;AACxB,SAAS,gBAAgB,kBAAkB,oBAAoB;AAK/D,MAAM,iBAAiB;AAAA,EACrB,EAAE,OAAO,SAAS,OAAO,UAAU;AAAA,EACnC,EAAE,OAAO,UAAU,OAAO,SAAS;AAAA,EACnC,EAAE,OAAO,YAAY,OAAO,eAAe;AAC7C;AAEO,SAAS,YAAY,EAAE,OAAO,GAAuB;AAC1D,QAAM,EAAE,MAAM,IAAI,SAAS;AAC3B,QAAM,UAAU,WAAW;AAC3B,QAAM,CAAC,UAAU,WAAW,IAAI,SAA0B,CAAC,CAAC;AAC5D,QAAM,CAAC,MAAM,OAAO,IAAI,SAA2B,CAAC,CAAC;AACrD,QAAM,CAAC,UAAU,WAAW,IAAI,SAAwB,IAAI;AAC5D,QAAM,CAAC,GAAG,IAAI,IAAI,SAAS,EAAE;AAE7B,YAAU,MAAM;AACd,UAAM,KAAK,MAAM;AACjB,QAAI,CAAC,GAAI;AACT,UAAM,MAAM;AAAA,MAAW,WAAW,IAAI,SAAS,QAAQ,UAAU;AAAA,MAAG,CAAC,SACnE;AAAA,QACE,KAAK,KAGF,IAAI,CAAC,OAAO,EAAE,IAAI,EAAE,IAAI,GAAG,iBAAiB,EAAE,KAAK,CAAY,EAAE,EAAE,EACnE,KAAK,CAAC,GAAG,OAAO,EAAE,SAAS,MAAM,EAAE,SAAS,MAAM,EAAE,KAAK,cAAc,EAAE,MAAM,IAAI,CAAC;AAAA,MACzF;AAAA,IACF;AACA,UAAM,MAAM;AAAA,MAAW,WAAW,IAAI,SAAS,QAAQ,gBAAgB;AAAA,MAAG,CAAC,SACzE,QAAQ,KAAK,KAAK,IAAI,CAAC,OAAO,EAAE,IAAI,EAAE,IAAI,GAAI,EAAE,KAAK,EAAmB,EAAE,CAAC;AAAA,IAC7E;AACA,WAAO,MAAM;AACX,UAAI;AACJ,UAAI;AAAA,IACN;AAAA,EACF,GAAG,CAAC,MAAM,CAAC;AAEX,iBAAe,gBAAgB;AAG7B,UAAM,QAAQ;AAAA,MACZ,MAAM;AAAA,MACN,MAAM,WAAW,SAAS,SAAS,CAAC;AAAA,MACpC,aAAa;AAAA,MACb,aAAa,CAAC;AAAA,MACd,QAAQ,CAAC;AAAA,MACT,YAAY;AAAA,MACZ,SAAS;AAAA,MACT,SAAS,CAAC;AAAA,MACV,UAAU,CAAC;AAAA,MACX,YAAY;AAAA,MACZ,QAAQ;AAAA,MACR,OAAO,SAAS;AAAA,MAChB,UAAU;AAAA,IACZ;AACA,QAAI;AACF,YAAM,KAAK,MAAM,eAAe,QAAQ,QAAW,KAAK;AACxD,kBAAY,EAAE;AAAA,IAChB,SAAS,GAAG;AAGV,YAAM,EAAE,OAAO,4BAA4B,aAAa,aAAa,QAAQ,EAAE,UAAU,QAAW,MAAM,QAAQ,CAAC;AAAA,IACrH;AAAA,EACF;AAEA,QAAM,WAAW,QAAQ,MAAM;AAC7B,UAAM,SAAS,EAAE,KAAK,EAAE,YAAY;AACpC,QAAI,CAAC,OAAQ,QAAO;AACpB,WAAO,SAAS,OAAO,CAAC,MAAM,EAAE,KAAK,YAAY,EAAE,SAAS,MAAM,KAAK,EAAE,KAAK,SAAS,MAAM,CAAC;AAAA,EAChG,GAAG,CAAC,UAAU,CAAC,CAAC;AAEhB,QAAM,UAAU,SAAS,KAAK,CAAC,MAAM,EAAE,OAAO,QAAQ,KAAK;AAE3D,SACE,qBAAC,SAAI,WAAU,uCACb;AAAA,yBAAC,QAAK,WAAU,SACd;AAAA,2BAAC,SAAI,WAAU,0CACb;AAAA,4BAAC,QAAG,WAAU,yBAAwB,uBAAS;AAAA,QAC/C,oBAAC,UAAO,MAAK,MAAK,SAAS,eAAe,qBAAO;AAAA,SACnD;AAAA,MACA,oBAAC,SAAM,aAAY,gBAAU,OAAO,GAAG,UAAU,CAAC,MAAM,KAAK,EAAE,OAAO,KAAK,GAAG,WAAU,QAAO;AAAA,MAC/F,qBAAC,QAAG,WAAU,2CACX;AAAA,iBAAS,IAAI,CAAC,MACb,oBAAC,QACC;AAAA,UAAC;AAAA;AAAA,YACC,SAAS,MAAM,YAAY,EAAE,EAAE;AAAA,YAC/B,WAAW,yEACT,EAAE,OAAO,WAAW,+BAA+B,oBACrD;AAAA,YAEC;AAAA,gBAAE,SAAS,CAAC,KAAK,oBAAC,SAAI,KAAK,EAAE,OAAO,CAAC,GAAG,KAAI,IAAG,WAAU,gCAA+B;AAAA,cACzF,oBAAC,UAAK,WAAU,2BAA2B,YAAE,MAAK;AAAA,cAClD,oBAAC,SAAM,MAAM,EAAE,WAAW,WAAW,UAAU,EAAE,WAAW,UAAU,UAAU,SAC7E,yBAAe,KAAK,CAAC,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,SAAS,EAAE,QAChE;AAAA;AAAA;AAAA,QACF,KAZO,EAAE,EAaX,CACD;AAAA,QACA,SAAS,WAAW,KAAK,oBAAC,QAAG,WAAU,oCAAmC,6BAAe;AAAA,SAC5F;AAAA,OACF;AAAA,IACC,UACC;AAAA,MAAC;AAAA;AAAA,QAEC;AAAA,QACA,SAAS;AAAA,QACT,YAAY;AAAA,QACZ,WAAW,MAAM,YAAY,IAAI;AAAA,QACjC,SAAS;AAAA,QACT;AAAA;AAAA,MANK,QAAQ;AAAA,IAOf,IAEA,oBAAC,QACC,8BAAC,OAAE,WAAU,0BAAyB,4CAA8B,GACtE;AAAA,KAEJ;AAEJ;AAQO,SAAS,YAAY;AAAA,EAC1B;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,GAOG;AACD,QAAM,CAAC,KAAK,MAAM,IAAI,SAAkB,OAAO;AAC/C,QAAM,CAAC,YAAY,aAAa,IAAI,SAAS,iBAAiB,QAAQ,UAAU,CAAC;AACjF,QAAM,CAAC,cAAc,eAAe,IAAI,SAAS,iBAAiB,QAAQ,cAAc,CAAC;AAEzF,QAAM,cAAc,QAAQ,MAAM,eAAe,IAAI,WAAW,CAAC,CAAC,GAAG,CAAC,IAAI,OAAO,CAAC;AAElF,WAAS,UAAU,GAAW,OAA+B;AAC3D,WAAO,CAAC,MAAM;AACZ,YAAM,UAAU,CAAC,GAAI,EAAE,WAAW,CAAC,CAAE;AACrC,cAAQ,CAAC,IAAI,EAAE,GAAG,QAAQ,CAAC,GAAG,GAAG,MAAM;AACvC,aAAO,EAAE,GAAG,GAAG,QAAQ;AAAA,IACzB,CAAC;AAAA,EACH;AAEA,WAAS,WAAW,KAAyC;AAC3D,YAAQ,IAAI,YAAY,CAAC,GAAG,KAAK,CAAC,MAAM,EAAE,QAAQ,GAAG;AAAA,EACvD;AAEA,WAAS,aAAa,KAAa,OAAgC;AACjE,WAAO,CAAC,MAAM;AACZ,YAAM,WAAW,CAAC,GAAI,EAAE,YAAY,CAAC,CAAE;AACvC,YAAM,MAAM,SAAS,UAAU,CAAC,MAAM,EAAE,QAAQ,GAAG;AACnD,UAAI,OAAO,EAAG,UAAS,GAAG,IAAI,EAAE,GAAG,SAAS,GAAG,GAAG,GAAG,MAAM;AAAA,UACtD,UAAS,KAAK,EAAE,KAAK,QAAQ,MAAM,GAAG,MAAM,CAAC;AAClD,aAAO,EAAE,GAAG,GAAG,SAAS;AAAA,IAC1B,CAAC;AAAA,EACH;AAEA,iBAAe,OAAO;AACpB,UAAM,KAAK,MAAM;AACjB,QAAI,CAAC,GAAI;AAGT,UAAM,YAAY,IAAI,IAAI,WAAW;AACrC,UAAM,iBAAiB,IAAI,YAAY,CAAC,GAAG;AAAA,MACzC,CAAC,MACC,UAAU,IAAI,EAAE,GAAG,MAClB,EAAE,eAAe,UAAa,EAAE,UAAU,UAAa,EAAE,QAAQ,UAAa,EAAE,WAAW;AAAA,IAChG;AAEA,UAAM,wBAAwB,IAAI,mBAAmB,CAAC,GAAG,OAAO,CAAC,MAAM,EAAE,MAAM,KAAK,EAAE,SAAS,CAAC;AAChG,UAAM,SAAS,cAAc,UAAU;AAAA,MACrC,GAAG;AAAA,MACH,UAAU;AAAA,MACV,iBAAiB,qBAAqB,SAAS,IAAI,uBAAuB;AAAA,IAC5E,CAAC;AACD,QAAI,CAAC,OAAO,SAAS;AACnB,cAAQ,EAAE,OAAO,oBAAoB,aAAa,OAAO,MAAM,OAAO,CAAC,GAAG,SAAS,MAAM,QAAQ,CAAC;AAClG;AAAA,IACF;AAIA,UAAM,eAAe,QAAQ,QAAQ,IAAI,OAAO,IAAI;AACpD,YAAQ,EAAE,OAAO,sBAAsB,MAAM,UAAU,CAAC;AAAA,EAC1D;AAEA,iBAAe,SAAS;AACtB,UAAM,KAAK,MAAM,QAAQ;AAAA,MACvB,OAAO,IAAI,QAAQ,IAAI;AAAA,MACvB,aAAa;AAAA,MACb,MAAM;AAAA,IACR,CAAC;AACD,QAAI,CAAC,GAAI;AACT,UAAM,cAAc,QAAQ,QAAQ,EAAE;AACtC,cAAU;AAAA,EACZ;AAEA,SACE,oBAAC,QACC,+BAAC,SAAI,WAAU,cACb;AAAA,yBAAC,SAAI,WAAU,6BACb;AAAA,0BAAC,SAAM,OAAM,QACX;AAAA,QAAC;AAAA;AAAA,UACC,OAAO,IAAI;AAAA,UACX,UAAU,CAAC,MAAM,OAAO,CAAC,OAAO,EAAE,GAAG,GAAG,MAAM,EAAE,OAAO,OAAO,MAAM,QAAQ,EAAE,OAAO,KAAK,KAAK,EAAE,KAAK,EAAE;AAAA;AAAA,MAC1G,GACF;AAAA,MACA,oBAAC,SAAM,OAAM,QAAO,MAAK,kCACvB,8BAAC,SAAM,OAAO,IAAI,MAAM,UAAU,CAAC,MAAM,OAAO,CAAC,OAAO,EAAE,GAAG,GAAG,MAAM,QAAQ,EAAE,OAAO,KAAK,EAAE,EAAE,GAAG,GACrG;AAAA,OACF;AAAA,IAEA,qBAAC,SAAI,WAAU,6BACb;AAAA,0BAAC,SAAM,OAAM,6BACX;AAAA,QAAC;AAAA;AAAA,UACC,OAAO;AAAA,UACP,UAAU,CAAC,MAAM;AACf,0BAAc,EAAE,OAAO,KAAK;AAC5B,kBAAM,IAAI,aAAa,EAAE,OAAO,KAAK;AACrC,gBAAI,MAAM,KAAM,QAAO,CAAC,OAAO,EAAE,GAAG,GAAG,YAAY,EAAE,EAAE;AAAA,UACzD;AAAA;AAAA,MACF,GACF;AAAA,MACA,oBAAC,SAAM,OAAM,iCAA2B,MAAK,gCAC3C;AAAA,QAAC;AAAA;AAAA,UACC,OAAO;AAAA,UACP,UAAU,CAAC,MAAM;AACf,4BAAgB,EAAE,OAAO,KAAK;AAC9B,kBAAM,IAAI,aAAa,EAAE,OAAO,KAAK;AACrC,mBAAO,CAAC,OAAO,EAAE,GAAG,GAAG,gBAAgB,KAAK,OAAU,EAAE;AAAA,UAC1D;AAAA;AAAA,MACF,GACF;AAAA,MACA,oBAAC,SAAM,OAAM,cACX;AAAA,QAAC;AAAA;AAAA,UACC,OAAO,OAAO,IAAI,WAAW,EAAE;AAAA,UAC/B,eAAe,CAAC,MAAM,OAAO,CAAC,OAAO,EAAE,GAAG,GAAG,SAAS,OAAO,CAAC,EAAgB,EAAE;AAAA,UAChF,SAAS;AAAA,YACP,EAAE,OAAO,MAAM,OAAO,MAAM;AAAA,YAC5B,EAAE,OAAO,KAAK,OAAO,KAAK;AAAA,YAC1B,EAAE,OAAO,KAAK,OAAO,KAAK;AAAA,UAC5B;AAAA;AAAA,MACF,GACF;AAAA,OACF;AAAA,IAEA,oBAAC,SAAM,OAAM,gBACX,8BAAC,YAAS,MAAM,GAAG,OAAO,IAAI,eAAe,IAAI,UAAU,CAAC,MAAM,OAAO,CAAC,OAAO,EAAE,GAAG,GAAG,aAAa,EAAE,OAAO,MAAM,EAAE,GAAG,GAC5H;AAAA,IAEA,oBAAC,SAAM,OAAM,gBAAe,MAAK,uCAC/B,+BAAC,SAAI,WAAU,qCACX;AAAA,WAAI,UAAU,CAAC,GAAG,IAAI,CAAC,KAAK,MAC5B,qBAAC,SAAkB,WAAU,YAC3B;AAAA,4BAAC,SAAI,KAAK,KAAK,KAAI,IAAG,WAAU,qCAAoC;AAAA,QACpE;AAAA,UAAC;AAAA;AAAA,YACC,MAAK;AAAA,YACL,cAAW;AAAA,YACX,WAAU;AAAA,YACV,SAAS,MAAM,OAAO,CAAC,OAAO,EAAE,GAAG,GAAG,SAAS,EAAE,UAAU,CAAC,GAAG,OAAO,CAAC,GAAG,MAAM,MAAM,CAAC,EAAE,EAAE;AAAA,YAC5F;AAAA;AAAA,QAED;AAAA,WATQ,MAAM,CAUhB,CACD;AAAA,MACD;AAAA,QAAC;AAAA;AAAA,UACC;AAAA,UACA,OAAM;AAAA,UACN,UAAU,CAAC,MAAM,OAAO,CAAC,OAAO,EAAE,GAAG,GAAG,QAAQ,CAAC,GAAI,EAAE,UAAU,CAAC,GAAI,EAAE,GAAG,EAAE,EAAE;AAAA;AAAA,MACjF;AAAA,OACF,GACF;AAAA,IAEA,oBAAC,SAAM,OAAM,kBACX,+BAAC,SAAI,WAAU,wBACZ;AAAA,iBAAW,IAAI,CAAC,MAAM;AACrB,cAAM,MAAM,IAAI,eAAe,CAAC,GAAG,SAAS,EAAE,EAAE;AAChD,eACE;AAAA,UAAC;AAAA;AAAA,YAEC,MAAK;AAAA,YACL,SAAS,MACP,OAAO,CAAC,OAAO;AAAA,cACb,GAAG;AAAA,cACH,aAAa,MAAM,EAAE,eAAe,CAAC,GAAG,OAAO,CAAC,MAAM,MAAM,EAAE,EAAE,IAAI,CAAC,GAAI,EAAE,eAAe,CAAC,GAAI,EAAE,EAAE;AAAA,YACrG,EAAE;AAAA,YAEJ,WAAW,yCACT,KAAK,gDAAgD,oCACvD;AAAA,YAEC;AAAA,mBAAK,YAAO;AAAA,cACZ,EAAE;AAAA;AAAA;AAAA,UAbE,EAAE;AAAA,QAcT;AAAA,MAEJ,CAAC;AAAA,MACA,WAAW,WAAW,KAAK,oBAAC,UAAK,WAAU,0BAAyB,2CAA6B;AAAA,OACpG,GACF;AAAA,IAGA;AAAA,MAAC;AAAA;AAAA,QACC,OAAM;AAAA,QACN,MAAK;AAAA,QAEL,+BAAC,SAAI,WAAU,cACX;AAAA,eAAI,WAAW,CAAC,GAAG,IAAI,CAAC,KAAK,MAC7B,qBAAC,SAAY,WAAU,qDACrB;AAAA,iCAAC,SAAI,WAAU,2BACb;AAAA;AAAA,gBAAC;AAAA;AAAA,kBACC,WAAU;AAAA,kBACV,aAAY;AAAA,kBACZ,OAAO,IAAI;AAAA,kBACX,UAAU,CAAC,MAAM,UAAU,GAAG,EAAE,MAAM,EAAE,OAAO,MAAM,CAAC;AAAA;AAAA,cACxD;AAAA,cACA;AAAA,gBAAC;AAAA;AAAA,kBACC,aAAY;AAAA,kBACZ,OAAO,IAAI,OAAO,KAAK,IAAI;AAAA,kBAC3B,UAAU,CAAC,MACT,UAAU,GAAG,EAAE,QAAQ,EAAE,OAAO,MAAM,MAAM,GAAG,EAAE,IAAI,CAAC,MAAM,EAAE,KAAK,CAAC,EAAE,OAAO,OAAO,EAAE,CAAC;AAAA;AAAA,cAE3F;AAAA,cACA;AAAA,gBAAC;AAAA;AAAA,kBACC,MAAK;AAAA,kBACL,SAAQ;AAAA,kBACR,SAAS,MAAM,OAAO,CAAC,OAAO,EAAE,GAAG,GAAG,UAAU,EAAE,WAAW,CAAC,GAAG,OAAO,CAAC,GAAG,MAAM,MAAM,CAAC,EAAE,EAAE;AAAA,kBAC9F;AAAA;AAAA,cAED;AAAA,eACF;AAAA,YAGC,mBAAmB,KAAK,IAAI,IAAI,KAAK,IAAI,OAAO,SAAS,KACxD,oBAAC,SAAI,WAAU,qCACZ,cAAI,OAAO,IAAI,CAAC,UAAU;AACzB,oBAAM,MAAM,IAAI,SAAS,KAAK;AAC9B,qBACE,qBAAC,UAAiB,WAAU,kFACzB;AAAA,uBAAO,oBAAC,SAAI,KAAK,KAAK,KAAI,IAAG,WAAU,gCAA+B;AAAA,gBACtE;AAAA,gBACD;AAAA,kBAAC;AAAA;AAAA,oBACC;AAAA,oBACA,OAAO,MAAM,WAAM;AAAA,oBACnB,UAAU,CAAC,MACT,UAAU,GAAG,EAAE,QAAQ,EAAE,GAAI,IAAI,UAAU,CAAC,GAAI,CAAC,KAAK,GAAG,EAAE,IAAI,EAAE,CAAC;AAAA;AAAA,gBAEtE;AAAA,gBACC,OACC;AAAA,kBAAC;AAAA;AAAA,oBACC,MAAK;AAAA,oBACL,cAAY,sBAAsB,KAAK;AAAA,oBACvC,WAAU;AAAA,oBACV,SAAS,MAAM;AACb,4BAAM,SAAS,EAAE,GAAI,IAAI,UAAU,CAAC,EAAG;AACvC,6BAAO,OAAO,KAAK;AACnB,gCAAU,GAAG,EAAE,OAAO,CAAC;AAAA,oBACzB;AAAA,oBACD;AAAA;AAAA,gBAED;AAAA,mBAtBO,KAwBX;AAAA,YAEJ,CAAC,GACH;AAAA,eAzDM,CA2DV,CACD;AAAA,WACC,IAAI,WAAW,CAAC,GAAG,SAAS,KAC5B;AAAA,YAAC;AAAA;AAAA,cACC,MAAK;AAAA,cACL,SAAQ;AAAA,cACR,WAAU;AAAA,cACV,SAAS,MAAM,OAAO,CAAC,OAAO,EAAE,GAAG,GAAG,SAAS,CAAC,GAAI,EAAE,WAAW,CAAC,GAAI,EAAE,MAAM,IAAI,QAAQ,CAAC,EAAE,CAAC,EAAE,EAAE;AAAA,cACnG;AAAA;AAAA,UAED;AAAA,WAEJ;AAAA;AAAA,IACF;AAAA,IAEC,YAAY,SAAS,KAAK,YAAY,UAAU,MAC/C,oBAAC,SAAM,OAAM,aAAY,MAAK,+CAC5B,8BAAC,SAAI,WAAU,mBACb,+BAAC,WAAM,WAAU,kBACf;AAAA,0BAAC,WACC,+BAAC,QAAG,WAAU,oCACZ;AAAA,4BAAC,QAAG,WAAU,aAAY,wBAAU;AAAA,QACpC,oBAAC,QAAG,WAAU,aAAY,4BAAS;AAAA,QAClC,IAAI,cAAc,oBAAC,QAAG,WAAU,aAAY,sBAAQ;AAAA,QACrD,oBAAC,QAAG,WAAU,aAAY,uBAAS;AAAA,SACrC,GACF;AAAA,MACA,oBAAC,WACE,sBAAY,IAAI,CAAC,QAAQ;AACxB,cAAM,IAAI,WAAW,GAAG;AACxB,eACE,qBAAC,QAAa,WAAU,6BACtB;AAAA,8BAAC,QAAG,WAAU,aAAa,cAAI,MAAM,GAAG,EAAE,KAAK,QAAK,GAAE;AAAA,UACtD,oBAAC,QAAG,WAAU,aAEZ;AAAA,YAAC;AAAA;AAAA,cACC,WAAU;AAAA,cACV,aAAa,iBAAiB,IAAI,UAAU;AAAA,cAC5C,cAAc,iBAAiB,GAAG,UAAU;AAAA,cAC5C,UAAU,CAAC,MAAM;AACf,sBAAM,IAAI,aAAa,EAAE,OAAO,KAAK;AACrC,6BAAa,KAAK,EAAE,YAAY,KAAK,OAAU,CAAC;AAAA,cAClD;AAAA;AAAA,UACF,GACF;AAAA,UACC,IAAI,cACH,oBAAC,QAAG,WAAU,aACZ;AAAA,YAAC;AAAA;AAAA,cACC,WAAU;AAAA,cACV,MAAK;AAAA,cACL,KAAK;AAAA,cACL,aAAY;AAAA,cACZ,cAAc,GAAG,SAAS;AAAA,cAC1B,UAAU,CAAC,MAAM;AACf,sBAAM,IAAI,EAAE,OAAO,UAAU,KAAK,SAAY,KAAK,IAAI,GAAG,OAAO,EAAE,OAAO,KAAK,KAAK,CAAC;AACrF,6BAAa,KAAK,EAAE,OAAO,EAAE,CAAC;AAAA,cAChC;AAAA;AAAA,UACF,GACF;AAAA,UAEF,oBAAC,QAAG,WAAU,aACZ,8BAAC,UAAO,SAAS,GAAG,WAAW,OAAO,iBAAiB,CAAC,OAAO,aAAa,KAAK,EAAE,QAAQ,GAAG,CAAC,GAAG,GACpG;AAAA,aA/BO,GAgCT;AAAA,MAEJ,CAAC,GACH;AAAA,OACF,GACF,GACF;AAAA,IAED,YAAY,SAAS,MACpB,qBAAC,OAAE,WAAU,0BACV;AAAA,kBAAY;AAAA,MAAO;AAAA,OACtB;AAAA,IAIF;AAAA,MAAC;AAAA;AAAA,QACC,OAAM;AAAA,QACN,MAAK;AAAA,QAEL,+BAAC,SAAI,WAAU,cACX;AAAA,eAAI,mBAAmB,CAAC,GAAG,IAAI,CAAC,GAAG,MAAM;AACzC,kBAAM,UAAU,CAAC,UACf,OAAO,CAAC,MAAM;AACZ,oBAAM,kBAAkB,CAAC,GAAI,EAAE,mBAAmB,CAAC,CAAE;AACrD,8BAAgB,CAAC,IAAI,EAAE,GAAG,gBAAgB,CAAC,GAAG,GAAG,MAAM;AACvD,qBAAO,EAAE,GAAG,GAAG,gBAAgB;AAAA,YACjC,CAAC;AACH,mBACE,qBAAC,SAAY,WAAU,8FACrB;AAAA;AAAA,gBAAC;AAAA;AAAA,kBACC,aAAY;AAAA,kBACZ,OAAO,EAAE;AAAA,kBACT,UAAU,CAAC,MACT,QAAQ;AAAA,oBACN,OAAO,EAAE,OAAO;AAAA,oBAChB,KAAK,EAAE,OAAO,EAAE,OAAO,MAAM,YAAY,EAAE,QAAQ,eAAe,GAAG,EAAE,QAAQ,YAAY,EAAE,EAAE,MAAM,GAAG,EAAE,KAAK,OAAO,IAAI,CAAC;AAAA,kBAC7H,CAAC;AAAA;AAAA,cAEL;AAAA,cACA;AAAA,gBAAC;AAAA;AAAA,kBACC,OAAO,EAAE;AAAA,kBACT,eAAe,CAAC,MAAM,QAAQ,EAAE,MAAM,EAAsB,CAAC;AAAA,kBAC7D,SAAS;AAAA,oBACP,EAAE,OAAO,QAAQ,OAAO,QAAQ;AAAA,oBAChC,EAAE,OAAO,SAAS,OAAO,aAAa;AAAA,kBACxC;AAAA;AAAA,cACF;AAAA,cACC,EAAE,SAAS,SACV;AAAA,gBAAC;AAAA;AAAA,kBACC,MAAK;AAAA,kBACL,OAAM;AAAA,kBACN,KAAK;AAAA,kBACL,KAAK;AAAA,kBACL,OAAO,EAAE,aAAa;AAAA,kBACtB,UAAU,CAAC,MAAM,QAAQ,EAAE,WAAW,KAAK,IAAI,GAAG,KAAK,IAAI,KAAK,OAAO,EAAE,OAAO,KAAK,KAAK,EAAE,CAAC,EAAE,CAAC;AAAA;AAAA,cAClG,IAEA,oBAAC,UAAK,WAAU,sCAAqC,0BAAY;AAAA,cAEnE;AAAA,gBAAC;AAAA;AAAA,kBACC,WAAU;AAAA,kBACV,aAAY;AAAA,kBACZ,cAAc,EAAE,aAAa,iBAAiB,EAAE,UAAU,IAAI;AAAA,kBAC9D,UAAU,CAAC,MAAM,QAAQ,EAAE,YAAY,aAAa,EAAE,OAAO,KAAK,KAAK,EAAE,CAAC;AAAA;AAAA,cAC5E;AAAA,cACA,qBAAC,SAAI,WAAU,2BACb;AAAA,oCAAC,UAAO,SAAS,EAAE,aAAa,MAAM,iBAAiB,CAAC,MAAM,QAAQ,EAAE,UAAU,EAAE,CAAC,GAAG,OAAM,aAAY;AAAA,gBAC1G;AAAA,kBAAC;AAAA;AAAA,oBACC,MAAK;AAAA,oBACL,SAAQ;AAAA,oBACR,SAAS,MAAM,OAAO,CAAC,OAAO,EAAE,GAAG,GAAG,kBAAkB,EAAE,mBAAmB,CAAC,GAAG,OAAO,CAAC,GAAG,MAAM,MAAM,CAAC,EAAE,EAAE;AAAA,oBAC9G;AAAA;AAAA,gBAED;AAAA,iBACF;AAAA,iBA9CQ,CA+CV;AAAA,UAEJ,CAAC;AAAA,WACC,IAAI,mBAAmB,CAAC,GAAG,SAAS,KACpC;AAAA,YAAC;AAAA;AAAA,cACC,MAAK;AAAA,cACL,SAAQ;AAAA,cACR,WAAU;AAAA,cACV,SAAS,MACP,OAAO,CAAC,OAAO;AAAA,gBACb,GAAG;AAAA,gBACH,iBAAiB;AAAA,kBACf,GAAI,EAAE,mBAAmB,CAAC;AAAA,kBAC1B,EAAE,KAAK,QAAQ,EAAE,mBAAmB,CAAC,GAAG,SAAS,CAAC,IAAI,OAAO,IAAI,MAAM,QAAQ,UAAU,OAAO,WAAW,IAAI,YAAY,EAAE;AAAA,gBAC/H;AAAA,cACF,EAAE;AAAA,cAEL;AAAA;AAAA,UAED;AAAA,WAEJ;AAAA;AAAA,IACF;AAAA,IAEA,qBAAC,SAAI,WAAU,qCACb;AAAA,0BAAC,SAAM,OAAM,UACX;AAAA,QAAC;AAAA;AAAA,UACC,OAAO,IAAI,UAAU;AAAA,UACrB,eAAe,CAAC,MAAM,OAAO,CAAC,OAAO,EAAE,GAAG,GAAG,QAAQ,EAAuB,EAAE;AAAA,UAC9E,SAAS;AAAA;AAAA,MACX,GACF;AAAA,MACA,oBAAC,SAAM,OAAM,YACX;AAAA,QAAC;AAAA;AAAA,UACC,MAAK;AAAA,UACL,WAAU;AAAA,UACV,OAAO,IAAI,SAAS;AAAA,UACpB,UAAU,CAAC,MAAM,OAAO,CAAC,OAAO,EAAE,GAAG,GAAG,OAAO,KAAK,IAAI,GAAG,OAAO,EAAE,OAAO,KAAK,KAAK,CAAC,EAAE,EAAE;AAAA;AAAA,MAC5F,GACF;AAAA,MACA,oBAAC,UAAO,SAAS,IAAI,aAAa,MAAM,iBAAiB,CAAC,MAAM,OAAO,CAAC,OAAO,EAAE,GAAG,GAAG,UAAU,EAAE,EAAE,GAAG,OAAM,cAAa;AAAA,MAC3H,oBAAC,UAAO,SAAS,IAAI,eAAe,MAAM,iBAAiB,CAAC,MAAM,OAAO,CAAC,OAAO,EAAE,GAAG,GAAG,YAAY,EAAE,EAAE,GAAG,OAAM,sBAAqB;AAAA,MACtI,IAAI,eAAe,IAAI,WAAW,CAAC,GAAG,WAAW,KAChD,oBAAC,SAAM,OAAM,YACX;AAAA,QAAC;AAAA;AAAA,UACC,MAAK;AAAA,UACL,WAAU;AAAA,UACV,aAAY;AAAA,UACZ,OAAO,IAAI,SAAS;AAAA,UACpB,UAAU,CAAC,MACT,OAAO,CAAC,OAAO,EAAE,GAAG,GAAG,OAAO,EAAE,OAAO,UAAU,KAAK,SAAY,KAAK,IAAI,GAAG,OAAO,EAAE,OAAO,KAAK,KAAK,CAAC,EAAE,EAAE;AAAA;AAAA,MAEjH,GACF;AAAA,OAEJ;AAAA,IAEA,qBAAC,SAAI,WAAU,oEACb;AAAA,2BAAC,SAAI,WAAU,2BACb;AAAA,4BAAC,UAAO,SAAS,MAAM,qBAAO;AAAA,QAC9B,oBAAC,UAAK,WAAU,0BAA0B,sBAAY,IAAI,UAAU,GAAE;AAAA,SACxE;AAAA,MACA,oBAAC,UAAO,SAAQ,UAAS,SAAS,QAAQ,yBAAW;AAAA,OACvD;AAAA,KACF,GACF;AAEJ;","names":[]}
|
package/dist/schema/shop.d.ts
CHANGED
|
@@ -71,6 +71,19 @@ export declare const ProductStatusSchema: z.ZodMiniEnum<{
|
|
|
71
71
|
draft: "draft";
|
|
72
72
|
}>;
|
|
73
73
|
export type ProductStatus = z.infer<typeof ProductStatusSchema>;
|
|
74
|
+
/**
|
|
75
|
+
* Verwijdert top-level `null`-velden uit een productdoc vóór validatie (7.17.1).
|
|
76
|
+
*
|
|
77
|
+
* Er liggen productdocs met `personalization: null` (e.d.) in Firestore: de functions-SDK
|
|
78
|
+
* encodeerde een client-side `undefined` als `null` over de draad (dezelfde klasse als de
|
|
79
|
+
* "+ Nieuw product"-bug), en de callable schreef dat door. `z.optional()` accepteert een
|
|
80
|
+
* ONTBREKEND veld maar geen `null`, en `parseProduct` op de leeskant slaat een ongeldig doc
|
|
81
|
+
* stil over — waardoor een product dat gewoon "actief" in het beheer staat van de live winkel
|
|
82
|
+
* verdween (gemeten op christelijkkraamcadeau: 2 van de 4 "Maak een vrolijk geluid"-varianten,
|
|
83
|
+
* precies de twee die vóór de 7.16.2-fix nog eens waren opgeslagen). In dit model betekent
|
|
84
|
+
* `null` nergens iets anders dan "afwezig", dus strippen is verliesvrij.
|
|
85
|
+
*/
|
|
86
|
+
export declare function saneerProductDoc<T>(data: T): T;
|
|
74
87
|
export declare const ProductSchema: z.ZodMiniObject<{
|
|
75
88
|
name: z.ZodMiniString<string>;
|
|
76
89
|
slug: z.ZodMiniString<string>;
|
package/dist/schema/shop.js
CHANGED
|
@@ -47,6 +47,12 @@ const ProductVariantSchema = z.object({
|
|
|
47
47
|
active: z._default(z.boolean(), true)
|
|
48
48
|
});
|
|
49
49
|
const ProductStatusSchema = z.enum(["draft", "active", "archived"]);
|
|
50
|
+
function saneerProductDoc(data) {
|
|
51
|
+
if (typeof data !== "object" || data === null || Array.isArray(data)) return data;
|
|
52
|
+
return Object.fromEntries(
|
|
53
|
+
Object.entries(data).filter(([, v]) => v !== null)
|
|
54
|
+
);
|
|
55
|
+
}
|
|
50
56
|
const ProductSchema = z.object({
|
|
51
57
|
name: z.string().check(z.minLength(1)).check(z.maxLength(200)),
|
|
52
58
|
slug: z.string().check(z.minLength(1)).check(z.maxLength(120)).check(z.regex(/^[a-z0-9-]+$/, "slug: alleen kleine letters, cijfers en streepjes")),
|
|
@@ -265,6 +271,7 @@ export {
|
|
|
265
271
|
ShopCategorySchema,
|
|
266
272
|
formatCents,
|
|
267
273
|
productPriceCents,
|
|
274
|
+
saneerProductDoc,
|
|
268
275
|
shippingCentsFor,
|
|
269
276
|
variantKeyFor
|
|
270
277
|
};
|
package/dist/schema/shop.js.map
CHANGED
|
@@ -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\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});\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});\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});\nexport type OrderItem = z.infer<typeof OrderItemSchema>;\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 /** 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 /** 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 (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});\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});\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});\nexport type OrderItem = z.infer<typeof OrderItemSchema>;\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 /** 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 /** 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/server/shop.js
CHANGED
|
@@ -5,10 +5,11 @@ import { cachedRead } from "./cached.js";
|
|
|
5
5
|
import {
|
|
6
6
|
ProductSchema,
|
|
7
7
|
ShopCategorySchema,
|
|
8
|
-
productPriceCents
|
|
8
|
+
productPriceCents,
|
|
9
|
+
saneerProductDoc
|
|
9
10
|
} from "../schema/shop.js";
|
|
10
11
|
function parseProduct(id, data) {
|
|
11
|
-
const res = ProductSchema.safeParse(data);
|
|
12
|
+
const res = ProductSchema.safeParse(saneerProductDoc(data));
|
|
12
13
|
if (!res.success) {
|
|
13
14
|
console.warn(`[xicms] product "${id}" faalt schema-validatie en wordt overgeslagen:`, res.error.issues[0]);
|
|
14
15
|
return null;
|
package/dist/server/shop.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/server/shop.ts"],"sourcesContent":["import \"server-only\";\n\nimport { cache } from \"react\";\nimport type { Metadata } from \"next\";\n\nimport { getAdminDb } from \"../firebase/admin\";\nimport { cachedRead } from \"./cached\";\nimport {\n ProductSchema,\n ShopCategorySchema,\n productPriceCents,\n type Product,\n type ShopCategory,\n} from \"../schema/shop\";\n\n/**\n * Server-readers voor de shop (5.6) — zelfde contract als server/pages.ts: Admin SDK als die\n * er is, anders keyless Firestore-REST met de publieke web-apiKey (de rules staan publieke\n * reads op actieve producten/categorieën toe). Reads zijn per-request gededupt via cache();\n * route-level ISR (`revalidate`) dempt de kosten tussen requests.\n *\n * Gebruik in de klantsite (catch-all):\n * const blockData = { shop: await getShopPageData(siteId, path, searchParams) };\n * <PublishedPage page={page} siteId={siteId} blockData={blockData} />\n */\n\nexport type ServerProduct = Product & { id: string };\nexport type ServerShopCategory = ShopCategory & { id: string };\n\n// Gedeelde vorm met de blocks (useBlockData(\"shop\")) — één bron van waarheid.\nexport type { ShopPageData } from \"../modules/shop/types\";\nimport type { ShopPageData } from \"../modules/shop/types\";\n\nfunction parseProduct(id: string, data: unknown): ServerProduct | null {\n const res = ProductSchema.safeParse(data);\n if (!res.success) {\n console.warn(`[xicms] product \"${id}\" faalt schema-validatie en wordt overgeslagen:`, res.error.issues[0]);\n return null;\n }\n return { id, ...res.data };\n}\n\nfunction parseCategory(id: string, data: unknown): ServerShopCategory | null {\n const res = ShopCategorySchema.safeParse(data);\n if (!res.success) {\n console.warn(`[xicms] categorie \"${id}\" faalt schema-validatie en wordt overgeslagen:`, res.error.issues[0]);\n return null;\n }\n return { id, ...res.data };\n}\n\n/* ── keyless REST (zelfde aanpak als server/pages.ts) ────────────────────── */\n\ntype FsValue = Record<string, unknown>;\nfunction decode(v: FsValue): unknown {\n if (v == null) return null;\n if (\"stringValue\" in v) return v.stringValue;\n if (\"integerValue\" in v) return Number(v.integerValue);\n if (\"doubleValue\" in v) return v.doubleValue;\n if (\"booleanValue\" in v) return v.booleanValue;\n if (\"nullValue\" in v) return null;\n if (\"timestampValue\" in v) return v.timestampValue;\n if (\"arrayValue\" in v) return (((v.arrayValue as FsValue)?.values as FsValue[]) ?? []).map(decode);\n if (\"mapValue\" in v) return decodeFields(((v.mapValue as FsValue)?.fields as Record<string, FsValue>) ?? {});\n return null;\n}\nfunction decodeFields(fields: Record<string, FsValue>): Record<string, unknown> {\n return Object.fromEntries(Object.entries(fields).map(([k, x]) => [k, decode(x)]));\n}\n\ninterface RestDoc {\n id: string;\n data: Record<string, unknown>;\n}\n\nasync function restQuery(\n siteId: string,\n collectionId: string,\n filters: { fieldPath: string; value: string }[],\n limit?: number,\n): Promise<RestDoc[]> {\n const apiKey = process.env.NEXT_PUBLIC_FIREBASE_API_KEY;\n const projectId = process.env.NEXT_PUBLIC_FIREBASE_PROJECT_ID;\n if (!apiKey || !projectId) throw new Error(\"Firebase REST-config ontbreekt (apiKey/projectId)\");\n const url = `https://firestore.googleapis.com/v1/projects/${projectId}/databases/(default)/documents/sites/${siteId}:runQuery?key=${apiKey}`;\n const fieldFilters = filters.map((f) => ({\n fieldFilter: { field: { fieldPath: f.fieldPath }, op: \"EQUAL\", value: { stringValue: f.value } },\n }));\n const where =\n fieldFilters.length === 0\n ? undefined\n : fieldFilters.length === 1\n ? fieldFilters[0]\n : { compositeFilter: { op: \"AND\", filters: fieldFilters } };\n const structuredQuery: Record<string, unknown> = { from: [{ collectionId }] };\n if (where) structuredQuery.where = where;\n if (limit) structuredQuery.limit = limit;\n try {\n const res = await fetch(url, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({ structuredQuery }),\n // Zelfde vangnet-logica als pages.ts: de tag is het normale pad, de timer dekt een\n // gemiste revalidate-push. Shop-content wijzigt zelden; elke 5 minuten opnieuw tellen was\n // pure verspilling.\n next: { revalidate: Number(process.env.XICMS_ISR_FALLBACK) || 3600, tags: [`xicms:${siteId}:${collectionId}`] },\n });\n if (!res.ok) throw new Error(`Firestore REST-query (shop) ${res.status}`);\n const rows = (await res.json()) as { document?: { name: string; fields: Record<string, FsValue> } }[];\n return rows\n .filter((r) => r.document)\n .map((r) => ({ id: r.document!.name.split(\"/\").pop()!, data: decodeFields(r.document!.fields ?? {}) }));\n } catch (err) {\n // Door naar de aanroeper: uitkomsten worden gecached, fouten niet (zie server/cached.ts).\n throw err instanceof Error ? err : new Error(String(err));\n }\n}\n\n/* ── Readers ─────────────────────────────────────────────────────────────── */\n\nasync function getActiveProductsUncached(siteId: string): Promise<ServerProduct[]> {\n const adb = getAdminDb();\n let docs: RestDoc[];\n if (adb) {\n try {\n const snap = await adb.collection(`sites/${siteId}/products`).where(\"status\", \"==\", \"active\").limit(500).get();\n docs = snap.docs.map((d) => ({ id: d.id, data: d.data() }));\n } catch (e) {\n // Gooien, niet [] — een lege uitkomst zou door de servercache een uur worden vastgehouden.\n throw new Error(`Admin-read producten mislukt: ${(e as Error)?.message ?? e}`);\n }\n } else {\n docs = await restQuery(siteId, \"products\", [{ fieldPath: \"status\", value: \"active\" }], 500);\n }\n return docs\n .map((d) => parseProduct(d.id, d.data))\n .filter((p): p is ServerProduct => p !== null)\n .sort((a, b) => (a.order ?? 0) - (b.order ?? 0) || a.name.localeCompare(b.name, \"nl\"));\n}\n\nasync function getShopCategoriesUncached(siteId: string): Promise<ServerShopCategory[]> {\n const adb = getAdminDb();\n let docs: RestDoc[];\n if (adb) {\n try {\n const snap = await adb.collection(`sites/${siteId}/shopCategories`).limit(100).get();\n docs = snap.docs.map((d) => ({ id: d.id, data: d.data() }));\n } catch (e) {\n throw new Error(`Admin-read categorieën mislukt: ${(e as Error)?.message ?? e}`);\n }\n } else {\n docs = await restQuery(siteId, \"shopCategories\", [], 100);\n }\n return docs\n .map((d) => parseCategory(d.id, d.data))\n .filter((c): c is ServerShopCategory => c !== null && c.visible !== false)\n .sort((a, b) => (a.order ?? 0) - (b.order ?? 0));\n}\n\nasync function getProductBySlugUncached(siteId: string, slug: string): Promise<ServerProduct | null> {\n if (!slug) return null;\n const adb = getAdminDb();\n let docs: RestDoc[];\n if (adb) {\n try {\n const snap = await adb\n .collection(`sites/${siteId}/products`)\n .where(\"status\", \"==\", \"active\")\n .where(\"slug\", \"==\", slug)\n .limit(1)\n .get();\n docs = snap.docs.map((d) => ({ id: d.id, data: d.data() }));\n } catch (e) {\n throw new Error(`Admin-read product mislukt: ${(e as Error)?.message ?? e}`);\n }\n } else {\n docs = await restQuery(\n siteId,\n \"products\",\n [\n { fieldPath: \"status\", value: \"active\" },\n { fieldPath: \"slug\", value: slug },\n ],\n 1,\n );\n }\n const d = docs[0];\n return d ? parseProduct(d.id, d.data) : null;\n}\n\n/**\n * Tagged servercache (zie server/cached.ts) om élk leespad (Admin én REST): de `products`-ping van\n * `saveProduct`/`deleteProduct` maakt dit binnen seconden vers; tot die tijd kost een render nul\n * Firestore-reads. React `cache()` blijft eromheen voor de per-request-dedup (metadata + render).\n */\nexport const getActiveProducts = cache((siteId: string) =>\n cachedRead([\"xicms\", \"shop\", \"products\", siteId], [`xicms:${siteId}:products`], () => getActiveProductsUncached(siteId)),\n);\nexport const getShopCategories = cache((siteId: string) =>\n cachedRead([\"xicms\", \"shop\", \"categories\", siteId], [`xicms:${siteId}:shopCategories`], () => getShopCategoriesUncached(siteId)),\n);\nexport const getProductBySlug = cache((siteId: string, slug: string) =>\n cachedRead([\"xicms\", \"shop\", \"product\", siteId, slug], [`xicms:${siteId}:products`], () => getProductBySlugUncached(siteId, slug)),\n);\n\n/**\n * Verzamelt de shop-startdata voor één pagina. `searchParams.p` (detailpagina) bepaalt of er\n * ook een enkel product geladen wordt. Geef het resultaat door als\n * `<PublishedPage blockData={{ shop: data }}>`.\n */\nexport async function getShopPageData(\n siteId: string,\n searchParams?: { p?: string | string[]; [k: string]: unknown },\n): Promise<ShopPageData> {\n const slugParam = searchParams?.p;\n const slug = Array.isArray(slugParam) ? slugParam[0] : slugParam;\n const [products, categories, product] = await Promise.all([\n getActiveProducts(siteId),\n getShopCategories(siteId),\n slug ? getProductBySlug(siteId, slug) : Promise.resolve(undefined),\n ]);\n const data: ShopPageData = { products, categories };\n if (slug !== undefined) data.product = product ?? null;\n return data;\n}\n\n/* ── SEO ─────────────────────────────────────────────────────────────────── */\n\n/** Metadata voor een productdetailpagina (title/description/OG-beeld). */\nexport function buildProductMetadata(\n product: ServerProduct | null,\n site: { name: string; domain?: string },\n): Metadata {\n if (!product) return { title: `Product — ${site.name}` };\n const description = (product.description || `${product.name} — ${site.name}`)\n .replace(/\\s+/g, \" \")\n .slice(0, 300);\n const image = product.images?.[0];\n return {\n title: `${product.name} — ${site.name}`,\n description,\n openGraph: {\n title: product.name,\n description,\n type: \"website\",\n ...(image ? { images: [{ url: image }] } : {}),\n },\n };\n}\n\n/** schema.org Product/Offer-JSON-LD met echte prijs en beschikbaarheid. */\nexport function buildProductJsonLd(\n product: ServerProduct,\n site: { name: string; domain?: string },\n currency = \"EUR\",\n): Record<string, unknown> {\n const soldOut =\n product.trackStock === true &&\n (product.options ?? []).length === 0 &&\n product.stock !== undefined &&\n product.stock <= 0;\n return {\n \"@context\": \"https://schema.org\",\n \"@type\": \"Product\",\n name: product.name,\n ...(product.description ? { description: product.description.slice(0, 500) } : {}),\n ...(product.images?.length ? { image: product.images } : {}),\n offers: {\n \"@type\": \"Offer\",\n price: (productPriceCents(product) / 100).toFixed(2),\n priceCurrency: currency,\n availability: soldOut ? \"https://schema.org/OutOfStock\" : \"https://schema.org/InStock\",\n ...(site.domain ? { url: `https://${site.domain}/product?p=${encodeURIComponent(product.slug)}` } : {}),\n },\n brand: { \"@type\": \"Brand\", name: site.name },\n };\n}\n"],"mappings":"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","names":["d"]}
|
|
1
|
+
{"version":3,"sources":["../../src/server/shop.ts"],"sourcesContent":["import \"server-only\";\n\nimport { cache } from \"react\";\nimport type { Metadata } from \"next\";\n\nimport { getAdminDb } from \"../firebase/admin\";\nimport { cachedRead } from \"./cached\";\nimport {\n ProductSchema,\n ShopCategorySchema,\n productPriceCents,\n saneerProductDoc,\n type Product,\n type ShopCategory,\n} from \"../schema/shop\";\n\n/**\n * Server-readers voor de shop (5.6) — zelfde contract als server/pages.ts: Admin SDK als die\n * er is, anders keyless Firestore-REST met de publieke web-apiKey (de rules staan publieke\n * reads op actieve producten/categorieën toe). Reads zijn per-request gededupt via cache();\n * route-level ISR (`revalidate`) dempt de kosten tussen requests.\n *\n * Gebruik in de klantsite (catch-all):\n * const blockData = { shop: await getShopPageData(siteId, path, searchParams) };\n * <PublishedPage page={page} siteId={siteId} blockData={blockData} />\n */\n\nexport type ServerProduct = Product & { id: string };\nexport type ServerShopCategory = ShopCategory & { id: string };\n\n// Gedeelde vorm met de blocks (useBlockData(\"shop\")) — één bron van waarheid.\nexport type { ShopPageData } from \"../modules/shop/types\";\nimport type { ShopPageData } from \"../modules/shop/types\";\n\nfunction parseProduct(id: string, data: unknown): ServerProduct | null {\n // Null-velden strippen vóór de validatie: zie saneerProductDoc — zonder dit verdween een\n // actief product stil van de live winkel terwijl het in /beheer gewoon zichtbaar was.\n const res = ProductSchema.safeParse(saneerProductDoc(data));\n if (!res.success) {\n console.warn(`[xicms] product \"${id}\" faalt schema-validatie en wordt overgeslagen:`, res.error.issues[0]);\n return null;\n }\n return { id, ...res.data };\n}\n\nfunction parseCategory(id: string, data: unknown): ServerShopCategory | null {\n const res = ShopCategorySchema.safeParse(data);\n if (!res.success) {\n console.warn(`[xicms] categorie \"${id}\" faalt schema-validatie en wordt overgeslagen:`, res.error.issues[0]);\n return null;\n }\n return { id, ...res.data };\n}\n\n/* ── keyless REST (zelfde aanpak als server/pages.ts) ────────────────────── */\n\ntype FsValue = Record<string, unknown>;\nfunction decode(v: FsValue): unknown {\n if (v == null) return null;\n if (\"stringValue\" in v) return v.stringValue;\n if (\"integerValue\" in v) return Number(v.integerValue);\n if (\"doubleValue\" in v) return v.doubleValue;\n if (\"booleanValue\" in v) return v.booleanValue;\n if (\"nullValue\" in v) return null;\n if (\"timestampValue\" in v) return v.timestampValue;\n if (\"arrayValue\" in v) return (((v.arrayValue as FsValue)?.values as FsValue[]) ?? []).map(decode);\n if (\"mapValue\" in v) return decodeFields(((v.mapValue as FsValue)?.fields as Record<string, FsValue>) ?? {});\n return null;\n}\nfunction decodeFields(fields: Record<string, FsValue>): Record<string, unknown> {\n return Object.fromEntries(Object.entries(fields).map(([k, x]) => [k, decode(x)]));\n}\n\ninterface RestDoc {\n id: string;\n data: Record<string, unknown>;\n}\n\nasync function restQuery(\n siteId: string,\n collectionId: string,\n filters: { fieldPath: string; value: string }[],\n limit?: number,\n): Promise<RestDoc[]> {\n const apiKey = process.env.NEXT_PUBLIC_FIREBASE_API_KEY;\n const projectId = process.env.NEXT_PUBLIC_FIREBASE_PROJECT_ID;\n if (!apiKey || !projectId) throw new Error(\"Firebase REST-config ontbreekt (apiKey/projectId)\");\n const url = `https://firestore.googleapis.com/v1/projects/${projectId}/databases/(default)/documents/sites/${siteId}:runQuery?key=${apiKey}`;\n const fieldFilters = filters.map((f) => ({\n fieldFilter: { field: { fieldPath: f.fieldPath }, op: \"EQUAL\", value: { stringValue: f.value } },\n }));\n const where =\n fieldFilters.length === 0\n ? undefined\n : fieldFilters.length === 1\n ? fieldFilters[0]\n : { compositeFilter: { op: \"AND\", filters: fieldFilters } };\n const structuredQuery: Record<string, unknown> = { from: [{ collectionId }] };\n if (where) structuredQuery.where = where;\n if (limit) structuredQuery.limit = limit;\n try {\n const res = await fetch(url, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({ structuredQuery }),\n // Zelfde vangnet-logica als pages.ts: de tag is het normale pad, de timer dekt een\n // gemiste revalidate-push. Shop-content wijzigt zelden; elke 5 minuten opnieuw tellen was\n // pure verspilling.\n next: { revalidate: Number(process.env.XICMS_ISR_FALLBACK) || 3600, tags: [`xicms:${siteId}:${collectionId}`] },\n });\n if (!res.ok) throw new Error(`Firestore REST-query (shop) ${res.status}`);\n const rows = (await res.json()) as { document?: { name: string; fields: Record<string, FsValue> } }[];\n return rows\n .filter((r) => r.document)\n .map((r) => ({ id: r.document!.name.split(\"/\").pop()!, data: decodeFields(r.document!.fields ?? {}) }));\n } catch (err) {\n // Door naar de aanroeper: uitkomsten worden gecached, fouten niet (zie server/cached.ts).\n throw err instanceof Error ? err : new Error(String(err));\n }\n}\n\n/* ── Readers ─────────────────────────────────────────────────────────────── */\n\nasync function getActiveProductsUncached(siteId: string): Promise<ServerProduct[]> {\n const adb = getAdminDb();\n let docs: RestDoc[];\n if (adb) {\n try {\n const snap = await adb.collection(`sites/${siteId}/products`).where(\"status\", \"==\", \"active\").limit(500).get();\n docs = snap.docs.map((d) => ({ id: d.id, data: d.data() }));\n } catch (e) {\n // Gooien, niet [] — een lege uitkomst zou door de servercache een uur worden vastgehouden.\n throw new Error(`Admin-read producten mislukt: ${(e as Error)?.message ?? e}`);\n }\n } else {\n docs = await restQuery(siteId, \"products\", [{ fieldPath: \"status\", value: \"active\" }], 500);\n }\n return docs\n .map((d) => parseProduct(d.id, d.data))\n .filter((p): p is ServerProduct => p !== null)\n .sort((a, b) => (a.order ?? 0) - (b.order ?? 0) || a.name.localeCompare(b.name, \"nl\"));\n}\n\nasync function getShopCategoriesUncached(siteId: string): Promise<ServerShopCategory[]> {\n const adb = getAdminDb();\n let docs: RestDoc[];\n if (adb) {\n try {\n const snap = await adb.collection(`sites/${siteId}/shopCategories`).limit(100).get();\n docs = snap.docs.map((d) => ({ id: d.id, data: d.data() }));\n } catch (e) {\n throw new Error(`Admin-read categorieën mislukt: ${(e as Error)?.message ?? e}`);\n }\n } else {\n docs = await restQuery(siteId, \"shopCategories\", [], 100);\n }\n return docs\n .map((d) => parseCategory(d.id, d.data))\n .filter((c): c is ServerShopCategory => c !== null && c.visible !== false)\n .sort((a, b) => (a.order ?? 0) - (b.order ?? 0));\n}\n\nasync function getProductBySlugUncached(siteId: string, slug: string): Promise<ServerProduct | null> {\n if (!slug) return null;\n const adb = getAdminDb();\n let docs: RestDoc[];\n if (adb) {\n try {\n const snap = await adb\n .collection(`sites/${siteId}/products`)\n .where(\"status\", \"==\", \"active\")\n .where(\"slug\", \"==\", slug)\n .limit(1)\n .get();\n docs = snap.docs.map((d) => ({ id: d.id, data: d.data() }));\n } catch (e) {\n throw new Error(`Admin-read product mislukt: ${(e as Error)?.message ?? e}`);\n }\n } else {\n docs = await restQuery(\n siteId,\n \"products\",\n [\n { fieldPath: \"status\", value: \"active\" },\n { fieldPath: \"slug\", value: slug },\n ],\n 1,\n );\n }\n const d = docs[0];\n return d ? parseProduct(d.id, d.data) : null;\n}\n\n/**\n * Tagged servercache (zie server/cached.ts) om élk leespad (Admin én REST): de `products`-ping van\n * `saveProduct`/`deleteProduct` maakt dit binnen seconden vers; tot die tijd kost een render nul\n * Firestore-reads. React `cache()` blijft eromheen voor de per-request-dedup (metadata + render).\n */\nexport const getActiveProducts = cache((siteId: string) =>\n cachedRead([\"xicms\", \"shop\", \"products\", siteId], [`xicms:${siteId}:products`], () => getActiveProductsUncached(siteId)),\n);\nexport const getShopCategories = cache((siteId: string) =>\n cachedRead([\"xicms\", \"shop\", \"categories\", siteId], [`xicms:${siteId}:shopCategories`], () => getShopCategoriesUncached(siteId)),\n);\nexport const getProductBySlug = cache((siteId: string, slug: string) =>\n cachedRead([\"xicms\", \"shop\", \"product\", siteId, slug], [`xicms:${siteId}:products`], () => getProductBySlugUncached(siteId, slug)),\n);\n\n/**\n * Verzamelt de shop-startdata voor één pagina. `searchParams.p` (detailpagina) bepaalt of er\n * ook een enkel product geladen wordt. Geef het resultaat door als\n * `<PublishedPage blockData={{ shop: data }}>`.\n */\nexport async function getShopPageData(\n siteId: string,\n searchParams?: { p?: string | string[]; [k: string]: unknown },\n): Promise<ShopPageData> {\n const slugParam = searchParams?.p;\n const slug = Array.isArray(slugParam) ? slugParam[0] : slugParam;\n const [products, categories, product] = await Promise.all([\n getActiveProducts(siteId),\n getShopCategories(siteId),\n slug ? getProductBySlug(siteId, slug) : Promise.resolve(undefined),\n ]);\n const data: ShopPageData = { products, categories };\n if (slug !== undefined) data.product = product ?? null;\n return data;\n}\n\n/* ── SEO ─────────────────────────────────────────────────────────────────── */\n\n/** Metadata voor een productdetailpagina (title/description/OG-beeld). */\nexport function buildProductMetadata(\n product: ServerProduct | null,\n site: { name: string; domain?: string },\n): Metadata {\n if (!product) return { title: `Product — ${site.name}` };\n const description = (product.description || `${product.name} — ${site.name}`)\n .replace(/\\s+/g, \" \")\n .slice(0, 300);\n const image = product.images?.[0];\n return {\n title: `${product.name} — ${site.name}`,\n description,\n openGraph: {\n title: product.name,\n description,\n type: \"website\",\n ...(image ? { images: [{ url: image }] } : {}),\n },\n };\n}\n\n/** schema.org Product/Offer-JSON-LD met echte prijs en beschikbaarheid. */\nexport function buildProductJsonLd(\n product: ServerProduct,\n site: { name: string; domain?: string },\n currency = \"EUR\",\n): Record<string, unknown> {\n const soldOut =\n product.trackStock === true &&\n (product.options ?? []).length === 0 &&\n product.stock !== undefined &&\n product.stock <= 0;\n return {\n \"@context\": \"https://schema.org\",\n \"@type\": \"Product\",\n name: product.name,\n ...(product.description ? { description: product.description.slice(0, 500) } : {}),\n ...(product.images?.length ? { image: product.images } : {}),\n offers: {\n \"@type\": \"Offer\",\n price: (productPriceCents(product) / 100).toFixed(2),\n priceCurrency: currency,\n availability: soldOut ? \"https://schema.org/OutOfStock\" : \"https://schema.org/InStock\",\n ...(site.domain ? { url: `https://${site.domain}/product?p=${encodeURIComponent(product.slug)}` } : {}),\n },\n brand: { \"@type\": \"Brand\", name: site.name },\n };\n}\n"],"mappings":"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","names":["d"]}
|
package/dist/version.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
const CORE_VERSION = true ? "7.17.
|
|
1
|
+
const CORE_VERSION = true ? "7.17.1" : "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.17.
|
|
3
|
+
"version": "7.17.1",
|
|
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": {
|