@xiwebdesign/xicms-core 7.22.0 → 7.22.2
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 -6
- package/dist/admin/shop/ProductsTab.js.map +1 -1
- package/dist/modules/shop/blocks/AantalInvoer.d.ts +26 -0
- package/dist/modules/shop/blocks/AantalInvoer.js +50 -0
- package/dist/modules/shop/blocks/AantalInvoer.js.map +1 -0
- package/dist/modules/shop/blocks/Cart.js +6 -7
- package/dist/modules/shop/blocks/Cart.js.map +1 -1
- package/dist/modules/shop/blocks/ProductDetail.js +2 -12
- package/dist/modules/shop/blocks/ProductDetail.js.map +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
|
@@ -92,14 +92,11 @@ function ProductsTab({ siteId }) {
|
|
|
92
92
|
className: `flex w-full items-center gap-2 rounded-lg px-3 py-2 text-left text-sm ${p.id === selected ? "bg-brand-50 text-brand-700" : "hover:bg-slate-100"}`,
|
|
93
93
|
children: [
|
|
94
94
|
p.images?.[0] && /* @__PURE__ */ jsx("img", { src: p.images[0], alt: "", className: "h-8 w-8 rounded object-cover" }),
|
|
95
|
-
/* @__PURE__ */ jsxs("span", { className: "min-w-0 flex-1
|
|
96
|
-
p.name,
|
|
95
|
+
/* @__PURE__ */ jsxs("span", { className: "min-w-0 flex-1", children: [
|
|
96
|
+
/* @__PURE__ */ jsx("span", { className: "block truncate", children: p.name }),
|
|
97
97
|
(() => {
|
|
98
98
|
const cat = cats.find((c) => c.id === (p.categoryIds ?? [])[0]);
|
|
99
|
-
return cat ? /* @__PURE__ */
|
|
100
|
-
" \u2014 ",
|
|
101
|
-
cat.name
|
|
102
|
-
] }) : null;
|
|
99
|
+
return cat ? /* @__PURE__ */ jsx("span", { className: "block truncate text-xs text-slate-400", children: cat.name }) : null;
|
|
103
100
|
})()
|
|
104
101
|
] }),
|
|
105
102
|
(() => {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/admin/shop/ProductsTab.tsx"],"sourcesContent":["\"use client\";\n\nimport { useEffect, useMemo, useState } from \"react\";\nimport { collection, doc, onSnapshot } from \"firebase/firestore\";\nimport { removeProduct, saveProductDoc } from \"./productClient\";\nimport { getDb } from \"../../firebase/client\";\nimport {\n ProductSchema,\n formatCents,\n saneerProductDoc,\n type PersonalizationField,\n type Product,\n type ProductOption,\n type ProductVariant,\n type ShopCategory,\n} from \"../../schema/shop\";\nimport { Badge, Button, Card, Field, Input, Textarea } from \"../ui/primitives\";\nimport { Select } from \"../ui/select\";\nimport { Switch } from \"../ui/switch\";\nimport { useToast } from \"../ui/toast\";\nimport { useConfirm, type ConfirmOptions } from \"../ui/ConfirmDialog\";\nimport { MediaPickerButton } from \"../media/MediaPicker\";\nimport { slugify } from \"../lib/slugify\";\nimport { allVariantKeys, centsToEuroInput, eurosToCents } from \"./lib\";\n\ntype StoredProduct = Product & { id: string };\ntype StoredCategory = ShopCategory & { id: string };\n\nconst STATUS_OPTIONS = [\n { value: \"draft\", label: \"Concept\" },\n { value: \"active\", label: \"Actief\" },\n { value: \"archived\", label: \"Gearchiveerd\" },\n];\n\nexport function ProductsTab({ siteId }: { siteId: string }) {\n const { toast } = useToast();\n const confirm = useConfirm();\n const [products, setProducts] = useState<StoredProduct[]>([]);\n const [cats, setCats] = useState<StoredCategory[]>([]);\n const [selected, setSelected] = useState<string | null>(null);\n const [q, setQ] = useState(\"\");\n\n useEffect(() => {\n const db = getDb();\n if (!db) return;\n const un1 = onSnapshot(collection(db, \"sites\", siteId, \"products\"), (snap) =>\n setProducts(\n snap.docs\n // saneerProductDoc: historische docs dragen null-velden (zie schema/shop.ts) — schoon\n // ze bij het lezen, zodat de form-state ze niet via {...def} opnieuw naar de server lekt.\n .map((d) => ({ id: d.id, ...saneerProductDoc(d.data() as Product) }))\n .sort((a, b) => (a.order ?? 0) - (b.order ?? 0) || a.name.localeCompare(b.name, \"nl\")),\n ),\n );\n const un2 = onSnapshot(collection(db, \"sites\", siteId, \"shopCategories\"), (snap) =>\n setCats(snap.docs.map((d) => ({ id: d.id, ...(d.data() as ShopCategory) }))),\n );\n return () => {\n un1();\n un2();\n };\n }, [siteId]);\n\n async function createProduct() {\n // Ook aanmaken loopt via de callable: een vers product is een draft (onzichtbaar voor de\n // cache), maar zo staat élke productmutatie in de audit-log en is er één pad i.p.v. twee.\n const nieuw = {\n name: \"Nieuw product\",\n slug: `product-${products.length + 1}`,\n description: \"\",\n categoryIds: [],\n images: [],\n priceCents: 0,\n vatRate: 21,\n options: [],\n variants: [],\n trackStock: false,\n status: \"draft\",\n order: products.length,\n featured: false,\n personalizationEnabled: true,\n } satisfies Product;\n try {\n const id = await saveProductDoc(siteId, undefined, nieuw);\n setSelected(id);\n } catch (e) {\n // Zichtbaar falen: dit gooide een tijdlang stil (unhandled) terwijl de knop gewoon niets\n // leek te doen — precies de melding \"bij aanmaken product krijg ik iets met permissions\".\n toast({ title: \"Product aanmaken mislukt\", description: e instanceof Error ? e.message : undefined, tone: \"error\" });\n }\n }\n\n const filtered = useMemo(() => {\n const needle = q.trim().toLowerCase();\n if (!needle) return products;\n return products.filter((p) => p.name.toLowerCase().includes(needle) || p.slug.includes(needle));\n }, [products, q]);\n\n const current = products.find((p) => p.id === selected) ?? null;\n\n return (\n <div className=\"grid gap-4 md:grid-cols-[300px_1fr]\">\n <Card className=\"h-fit\">\n <div className=\"mb-3 flex items-center justify-between\">\n <h3 className=\"text-sm font-semibold\">Producten</h3>\n <Button size=\"sm\" onClick={createProduct}>+ Nieuw</Button>\n </div>\n <Input placeholder=\"Zoeken…\" value={q} onChange={(e) => setQ(e.target.value)} className=\"mb-2\" />\n <ul className=\"grid max-h-[60vh] gap-1 overflow-y-auto\">\n {filtered.map((p) => (\n <li key={p.id}>\n <button\n onClick={() => setSelected(p.id)}\n className={`flex w-full items-center gap-2 rounded-lg px-3 py-2 text-left text-sm ${\n p.id === selected ? \"bg-brand-50 text-brand-700\" : \"hover:bg-slate-100\"\n }`}\n >\n {p.images?.[0] && <img src={p.images[0]} alt=\"\" className=\"h-8 w-8 rounded object-cover\" />}\n {/* Categorie erbij: hetzelfde ontwerp bestaat vaak als rompertje, slabbetje én\n t-shirt, en dan staat dezelfde naam vijf keer onder elkaar in deze lijst. */}\n <span className=\"min-w-0 flex-1 truncate\">\n {p.name}\n {(() => {\n const cat = cats.find((c) => c.id === (p.categoryIds ?? [])[0]);\n return cat ? <span className=\"text-slate-400\"> — {cat.name}</span> : null;\n })()}\n </span>\n {/* Voorraadsignaal: uitverkocht of bijna op moet je zien zónder elk product te\n openen — anders merk je pas bij een bestelling dat er niets meer is. */}\n {(() => {\n const n = voorraadStand(p);\n if (n === null) return null;\n if (n <= 0) return <Badge tone=\"slate\">uitverkocht</Badge>;\n if (n <= 3) return <Badge tone=\"amber\">{n} over</Badge>;\n return null;\n })()}\n <Badge tone={p.status === \"active\" ? \"green\" : p.status === \"draft\" ? \"amber\" : \"slate\"}>\n {STATUS_OPTIONS.find((s) => s.value === p.status)?.label ?? p.status}\n </Badge>\n </button>\n </li>\n ))}\n {filtered.length === 0 && <li className=\"px-3 py-2 text-sm text-slate-500\">Geen producten.</li>}\n </ul>\n </Card>\n {current ? (\n <ProductForm\n key={current.id}\n siteId={siteId}\n product={current}\n categories={cats}\n onDeleted={() => setSelected(null)}\n onDuplicated={(id) => setSelected(id)}\n onToast={toast}\n confirm={confirm}\n />\n ) : (\n <Card>\n <p className=\"text-sm text-slate-500\">Selecteer of maak een product.</p>\n </Card>\n )}\n </div>\n );\n}\n\n/**\n * Het volledige productformulier. Sinds 7.8 ook buiten dit tabblad in gebruik: het tandwiel bij een\n * product op de pagina opent hetzelfde formulier in een zijpaneel. Bewust hergebruik en geen tweede,\n * uitgeklede versie — twee formulieren voor één product lopen gegarandeerd uit elkaar, en dan hangt\n * het van je route af welke velden je kunt bereiken.\n */\n/**\n * Voorraadstand van een product voor de beheerlijst: het laagste getal dat er te melden valt.\n * `null` = niet bijgehouden of onbeperkt. Zonder opties telt `stock`, met opties de varianten.\n */\nfunction voorraadStand(p: Product): number | null {\n if (p.trackStock !== true) return null;\n if ((p.options ?? []).length === 0) return p.stock ?? null;\n const getallen = (p.variants ?? []).filter((v) => v.stock !== undefined && v.active !== false).map((v) => v.stock as number);\n return getallen.length > 0 ? Math.min(...getallen) : null;\n}\n\nexport function ProductForm({\n siteId,\n product,\n categories,\n onDeleted,\n onDuplicated,\n onToast,\n confirm,\n}: {\n siteId: string;\n product: StoredProduct;\n categories: StoredCategory[];\n onDeleted: () => void;\n /** Optioneel: na dupliceren de kopie selecteren (het zijpaneel doet dit niet). */\n onDuplicated?: (id: string) => void;\n onToast: (t: { title: string; description?: string; tone?: \"default\" | \"success\" | \"error\" }) => void;\n confirm: (opts: ConfirmOptions) => Promise<boolean>;\n}) {\n const [def, setDef] = useState<Product>(product);\n /**\n * Waarschuwen bij niet-opgeslagen werk (7.18). Het formulier heeft geen autosave (een product\n * gaat meteen live), dus wie naar een ander product klikte of het tabblad sloot, was zijn\n * wijzigingen kwijt zonder enige melding.\n */\n const vuil = JSON.stringify(def) !== JSON.stringify(product);\n useEffect(() => {\n if (!vuil) return;\n const waarschuw = (e: BeforeUnloadEvent) => e.preventDefault();\n window.addEventListener(\"beforeunload\", waarschuw);\n return () => window.removeEventListener(\"beforeunload\", waarschuw);\n }, [vuil]);\n const [priceInput, setPriceInput] = useState(centsToEuroInput(product.priceCents));\n const [compareInput, setCompareInput] = useState(centsToEuroInput(product.compareAtCents));\n\n const variantKeys = useMemo(() => allVariantKeys(def.options ?? []), [def.options]);\n\n function setOption(i: number, patch: Partial<ProductOption>) {\n setDef((p) => {\n const options = [...(p.options ?? [])];\n options[i] = { ...options[i], ...patch };\n return { ...p, options };\n });\n }\n\n function variantFor(key: string): ProductVariant | undefined {\n return (def.variants ?? []).find((v) => v.key === key);\n }\n\n function patchVariant(key: string, patch: Partial<ProductVariant>) {\n setDef((p) => {\n const variants = [...(p.variants ?? [])];\n const idx = variants.findIndex((v) => v.key === key);\n if (idx >= 0) variants[idx] = { ...variants[idx], ...patch };\n else variants.push({ key, active: true, ...patch });\n return { ...p, variants };\n });\n }\n\n /** Kopie van dit product maken (nieuwe slug + \"(kopie)\"), als concept zodat je rustig aanpast. */\n async function dupliceer() {\n try {\n const kopie: Product = {\n ...def,\n name: `${def.name} (kopie)`,\n slug: `${def.slug}-kopie`.slice(0, 120),\n status: \"draft\",\n order: (def.order ?? 0) + 1,\n };\n const id = await saveProductDoc(siteId, undefined, kopie);\n onToast({ title: \"Product gedupliceerd\", description: \"De kopie staat als concept klaar.\", tone: \"success\" });\n onDuplicated?.(id);\n } catch (e) {\n onToast({ title: \"Dupliceren mislukt\", description: e instanceof Error ? e.message : undefined, tone: \"error\" });\n }\n }\n\n async function save() {\n const db = getDb();\n if (!db) return;\n // Alleen varianten bewaren die iets toevoegen (afwijkende prijs/voorraad/sku of inactief),\n // en geen wees-varianten van verwijderde optiewaarden.\n const validKeys = new Set(variantKeys);\n const cleanVariants = (def.variants ?? []).filter(\n (v) =>\n validKeys.has(v.key) &&\n (v.priceCents !== undefined || v.stock !== undefined || v.sku !== undefined || v.active === false),\n );\n // Onvolledige personalisatie-regels (zonder label) niet opslaan.\n const cleanPersonalization = (def.personalization ?? []).filter((f) => f.label.trim().length > 0);\n const parsed = ProductSchema.safeParse({\n ...def,\n variants: cleanVariants,\n personalization: cleanPersonalization.length > 0 ? cleanPersonalization : undefined,\n });\n if (!parsed.success) {\n onToast({ title: \"Ongeldig product\", description: parsed.error.issues[0]?.message, tone: \"error\" });\n return;\n }\n // Via de callable (7.9): de server valideert, auditeert en — belangrijker — pingt de\n // products-cachetag, zodat de wijziging ook op de LIVE shoppagina's staat en niet alleen in\n // Firestore. Zie productClient.ts, ook voor de terugval op een oudere backend.\n await saveProductDoc(siteId, product.id, parsed.data);\n onToast({ title: \"Product opgeslagen\", tone: \"success\" });\n }\n\n async function remove() {\n const ok = await confirm({\n title: `\"${product.name}\" verwijderen?`,\n description: \"Bestaande bestellingen behouden hun eigen kopie van naam en prijs.\",\n tone: \"danger\",\n });\n if (!ok) return;\n await removeProduct(siteId, product.id);\n onDeleted();\n }\n\n return (\n <Card>\n <div className=\"grid gap-4\">\n <div className=\"grid gap-4 md:grid-cols-2\">\n <Field label=\"Naam\">\n <Input\n value={def.name}\n onChange={(e) => setDef((p) => ({ ...p, name: e.target.value, slug: slugify(e.target.value) || p.slug }))}\n />\n </Field>\n <Field label=\"Slug\" hint=\"Product-URL: /product?p=<slug>\">\n <Input value={def.slug} onChange={(e) => setDef((p) => ({ ...p, slug: slugify(e.target.value) }))} />\n </Field>\n </div>\n\n <div className=\"grid gap-4 md:grid-cols-3\">\n <Field label=\"Prijs (€, incl. btw)\">\n <Input\n value={priceInput}\n onChange={(e) => {\n setPriceInput(e.target.value);\n const c = eurosToCents(e.target.value);\n if (c !== null) setDef((p) => ({ ...p, priceCents: c }));\n }}\n />\n </Field>\n <Field label=\"Van-prijs (€, optioneel)\" hint=\"Doorgestreept naast de prijs\">\n <Input\n value={compareInput}\n onChange={(e) => {\n setCompareInput(e.target.value);\n const c = eurosToCents(e.target.value);\n setDef((p) => ({ ...p, compareAtCents: c ?? undefined }));\n }}\n />\n </Field>\n <Field label=\"Btw-tarief\">\n <Select\n value={String(def.vatRate ?? 21)}\n onValueChange={(v) => setDef((p) => ({ ...p, vatRate: Number(v) as 21 | 9 | 0 }))}\n options={[\n { value: \"21\", label: \"21%\" },\n { value: \"9\", label: \"9%\" },\n { value: \"0\", label: \"0%\" },\n ]}\n />\n </Field>\n </div>\n\n <Field label=\"Omschrijving\">\n <Textarea rows={4} value={def.description ?? \"\"} onChange={(e) => setDef((p) => ({ ...p, description: e.target.value }))} />\n </Field>\n\n <Field label=\"Afbeeldingen\" hint=\"Eerste afbeelding is het hoofdbeeld\">\n <div className=\"flex flex-wrap items-center gap-3\">\n {(def.images ?? []).map((url, i) => (\n <div key={url + i} className=\"relative\">\n <img src={url} alt=\"\" className={`h-16 w-16 rounded-lg object-cover ${i === 0 ? \"ring-2 ring-brand-500\" : \"\"}`} />\n {i === 0 && (\n <span className=\"absolute -bottom-2 left-1/2 -translate-x-1/2 rounded-full bg-brand-500 px-1.5 text-[10px] text-white\">hoofd</span>\n )}\n <button\n type=\"button\"\n aria-label=\"Verwijder afbeelding\"\n className=\"absolute -right-2 -top-2 grid h-5 w-5 place-items-center rounded-full bg-slate-700 text-xs text-white\"\n onClick={() => setDef((p) => ({ ...p, images: (p.images ?? []).filter((_, j) => j !== i) }))}\n >\n ✕\n </button>\n {/* Herschikken: de eerste afbeelding is het hoofdbeeld, en dat was alleen te\n veranderen door alles te wissen en opnieuw te kiezen. */}\n {i > 0 && (\n <button\n type=\"button\"\n aria-label=\"Naar voren verplaatsen\"\n title={i === 1 ? \"Maak hoofdbeeld\" : \"Naar voren\"}\n className=\"absolute -left-2 -top-2 grid h-5 w-5 place-items-center rounded-full bg-slate-700 text-xs text-white\"\n onClick={() =>\n setDef((p) => {\n const imgs = [...(p.images ?? [])];\n [imgs[i - 1], imgs[i]] = [imgs[i], imgs[i - 1]];\n return { ...p, images: imgs };\n })\n }\n >\n ←\n </button>\n )}\n </div>\n ))}\n <MediaPickerButton\n siteId={siteId}\n label=\"+ Afbeelding\"\n onSelect={(m) => setDef((p) => ({ ...p, images: [...(p.images ?? []), m.url] }))}\n />\n </div>\n </Field>\n\n <Field label=\"Categorieën\">\n <div className=\"flex flex-wrap gap-2\">\n {categories.map((c) => {\n const on = (def.categoryIds ?? []).includes(c.id);\n return (\n <button\n key={c.id}\n type=\"button\"\n onClick={() =>\n setDef((p) => ({\n ...p,\n categoryIds: on ? (p.categoryIds ?? []).filter((x) => x !== c.id) : [...(p.categoryIds ?? []), c.id],\n }))\n }\n className={`rounded-full border px-3 py-1 text-sm ${\n on ? \"border-brand-500 bg-brand-50 text-brand-700\" : \"border-slate-200 hover:bg-slate-50\"\n }`}\n >\n {on ? \"✓ \" : \"\"}\n {c.name}\n </button>\n );\n })}\n {categories.length === 0 && <span className=\"text-sm text-slate-500\">Maak eerst een categorie aan.</span>}\n </div>\n </Field>\n\n {/* Opties & varianten */}\n <Field\n label=\"Opties (bv. Maat, Kleur)\"\n hint=\"Max. 3. Waarden gescheiden door komma's. Combinaties kun je hieronder per stuk aanpassen.\"\n >\n <div className=\"grid gap-2\">\n {(def.options ?? []).map((opt, i) => (\n <div key={i} className=\"grid gap-2 rounded-lg border border-slate-100 p-2\">\n <div className=\"flex items-center gap-2\">\n <Input\n className=\"w-36\"\n placeholder=\"Naam (bv. Maat)\"\n value={opt.name}\n onChange={(e) => setOption(i, { name: e.target.value })}\n />\n <Input\n placeholder=\"Waarden, bv: 56, 62, 68, 74\"\n value={opt.values.join(\", \")}\n onChange={(e) =>\n setOption(i, { values: e.target.value.split(\",\").map((s) => s.trim()).filter(Boolean) })\n }\n />\n <Button\n size=\"sm\"\n variant=\"ghost\"\n onClick={() => setDef((p) => ({ ...p, options: (p.options ?? []).filter((_, j) => j !== i) }))}\n >\n ✕\n </Button>\n </div>\n {/* Afbeelding per waarde (5.6) — vooral voor de kleur-as: de detailpagina\n wisselt het hoofdbeeld mee met de gekozen kleur. */}\n {/^kleur|^colou?r/i.test(opt.name) && opt.values.length > 0 && (\n <div className=\"flex flex-wrap items-center gap-2\">\n {opt.values.map((value) => {\n const url = opt.images?.[value];\n return (\n <span key={value} className=\"flex items-center gap-1 rounded-full border border-slate-200 px-2 py-1 text-xs\">\n {url && <img src={url} alt=\"\" className=\"h-6 w-6 rounded object-cover\" />}\n {value}\n <MediaPickerButton\n siteId={siteId}\n label={url ? \"↻\" : \"+ foto\"}\n onSelect={(m) =>\n setOption(i, { images: { ...(opt.images ?? {}), [value]: m.url } })\n }\n />\n {url && (\n <button\n type=\"button\"\n aria-label={`Verwijder foto van ${value}`}\n className=\"text-slate-400 hover:text-slate-700\"\n onClick={() => {\n const images = { ...(opt.images ?? {}) };\n delete images[value];\n setOption(i, { images });\n }}\n >\n ✕\n </button>\n )}\n </span>\n );\n })}\n </div>\n )}\n </div>\n ))}\n {(def.options ?? []).length < 3 && (\n <Button\n size=\"sm\"\n variant=\"secondary\"\n className=\"w-fit\"\n onClick={() => setDef((p) => ({ ...p, options: [...(p.options ?? []), { name: \"\", values: [] }] }))}\n >\n + Optie\n </Button>\n )}\n </div>\n </Field>\n\n {variantKeys.length > 0 && variantKeys.length <= 60 && (\n <Field label=\"Varianten\" hint=\"Leeg = standaardprijs / onbeperkte voorraad\">\n <div className=\"overflow-x-auto\">\n <table className=\"w-full text-sm\">\n <thead>\n <tr className=\"text-left text-xs text-slate-500\">\n <th className=\"py-1 pr-2\">Combinatie</th>\n <th className=\"py-1 pr-2\">Prijs (€)</th>\n {def.trackStock && <th className=\"py-1 pr-2\">Voorraad</th>}\n <th className=\"py-1 pr-2\">Artikelnr.</th>\n <th className=\"py-1 pr-2\">Leverbaar</th>\n </tr>\n </thead>\n <tbody>\n {variantKeys.map((key) => {\n const v = variantFor(key);\n return (\n <tr key={key} className=\"border-t border-slate-100\">\n <td className=\"py-1 pr-2\">{key.split(\"|\").join(\" · \")}</td>\n <td className=\"py-1 pr-2\">\n {/* Native uncontrolled inputs: geen cursor-gedoe bij \"13,\" tijdens typen. */}\n <input\n className=\"w-24 rounded-lg border border-slate-200 px-2 py-1\"\n placeholder={centsToEuroInput(def.priceCents)}\n defaultValue={centsToEuroInput(v?.priceCents)}\n onChange={(e) => {\n const c = eurosToCents(e.target.value);\n patchVariant(key, { priceCents: c ?? undefined });\n }}\n />\n </td>\n {def.trackStock && (\n <td className=\"py-1 pr-2\">\n <input\n className=\"w-20 rounded-lg border border-slate-200 px-2 py-1\"\n type=\"number\"\n min={0}\n placeholder=\"∞\"\n defaultValue={v?.stock ?? \"\"}\n onChange={(e) => {\n const n = e.target.value === \"\" ? undefined : Math.max(0, Number(e.target.value) || 0);\n patchVariant(key, { stock: n });\n }}\n />\n </td>\n )}\n <td className=\"py-1 pr-2\">\n {/* SKU stond al in het schema maar was nergens in te vullen — terwijl\n het juist per variant nodig is (voor je eigen administratie/pakbon). */}\n <input\n className=\"w-28 rounded-lg border border-slate-200 px-2 py-1\"\n placeholder=\"—\"\n defaultValue={v?.sku ?? \"\"}\n onChange={(e) => patchVariant(key, { sku: e.target.value.trim() || undefined })}\n />\n </td>\n <td className=\"py-1 pr-2\">\n <Switch checked={v?.active !== false} onCheckedChange={(on) => patchVariant(key, { active: on })} />\n </td>\n </tr>\n );\n })}\n </tbody>\n </table>\n </div>\n </Field>\n )}\n {variantKeys.length > 60 && (\n <p className=\"text-sm text-amber-600\">\n {variantKeys.length} combinaties — te veel om per stuk te tonen. Beperk het aantal optie-waarden.\n </p>\n )}\n\n {/* Personalisatie (5.6): eigen naam/logo van de klant, met optionele meerprijs.\n Sinds 7.19 met een eigen aan/uit-schakelaar: uitzetten bewaart de velden. */}\n <div className=\"rounded-xl border border-slate-200 p-3\">\n <Switch\n checked={def.personalizationEnabled !== false}\n onCheckedChange={(v) =>\n setDef((p) => ({\n ...p,\n personalizationEnabled: v,\n // Bij het aanzetten meteen één leeg veld klaarzetten, anders staat de klant voor\n // een lege sectie en lijkt de schakelaar niets te doen.\n personalization:\n v && (p.personalization ?? []).length === 0\n ? [{ key: \"naam\", label: \"Naam\", type: \"text\", required: false, maxLength: 20, priceCents: 0 }]\n : p.personalization,\n }))\n }\n label=\"Persoonlijk maken aanzetten voor dit product\"\n />\n {def.personalizationEnabled === false && (def.personalization?.length ?? 0) > 0 && (\n <p className=\"mt-1 text-xs text-slate-500\">\n Staat uit — de klant ziet geen invulveld. Je {def.personalization!.length === 1 ? \"instelling blijft\" : \"instellingen blijven\"} bewaard\n voor als je het weer aanzet.\n </p>\n )}\n\n {def.personalizationEnabled !== false && (\n <Field\n label=\"Personalisatie (eigen tekst of afbeelding)\"\n hint=\"Max. 3 velden. De klant vult dit in op de productpagina; de meerprijs telt automatisch mee.\"\n >\n <div className=\"grid gap-2\">\n {(def.personalization ?? []).map((f, i) => {\n const setPers = (patch: Partial<PersonalizationField>) =>\n setDef((p) => {\n const personalization = [...(p.personalization ?? [])];\n personalization[i] = { ...personalization[i], ...patch };\n return { ...p, personalization };\n });\n return (\n <div key={i} className=\"grid gap-2 rounded-lg border border-slate-100 p-2 md:grid-cols-[1fr_130px_90px_110px_auto]\">\n <Input\n placeholder='Label (bv. \"Naam van het kindje\")'\n value={f.label}\n onChange={(e) =>\n setPers({\n label: e.target.value,\n key: f.key || e.target.value.toLowerCase().replace(/[^a-z0-9]+/g, \"_\").replace(/^_+|_+$/g, \"\").slice(0, 40) || `veld${i + 1}`,\n })\n }\n />\n <Select\n value={f.type}\n onValueChange={(v) => setPers({ type: v as \"text\" | \"image\" })}\n options={[\n { value: \"text\", label: \"Tekst\" },\n { value: \"image\", label: \"Afbeelding\" },\n ]}\n />\n {f.type === \"text\" ? (\n <Input\n type=\"number\"\n title=\"Max. tekens\"\n min={1}\n max={200}\n value={f.maxLength ?? 40}\n onChange={(e) => setPers({ maxLength: Math.max(1, Math.min(200, Number(e.target.value) || 40)) })}\n />\n ) : (\n <span className=\"self-center text-xs text-slate-400\">jpg/png/webp</span>\n )}\n <input\n className=\"rounded-lg border border-slate-200 px-2 py-1\"\n placeholder=\"Meerprijs €\"\n defaultValue={f.priceCents ? centsToEuroInput(f.priceCents) : \"\"}\n onChange={(e) => setPers({ priceCents: eurosToCents(e.target.value) ?? 0 })}\n />\n <div className=\"flex items-center gap-2\">\n <Switch checked={f.required === true} onCheckedChange={(v) => setPers({ required: v })} label=\"Verplicht\" />\n <Button\n size=\"sm\"\n variant=\"ghost\"\n onClick={() => setDef((p) => ({ ...p, personalization: (p.personalization ?? []).filter((_, j) => j !== i) }))}\n >\n ✕\n </Button>\n </div>\n {/* Uitleg onder het veld in de winkel. Bestond al in het schema en werd al\n gerenderd op de productpagina, maar was hier nooit in te vullen. */}\n <input\n className=\"col-span-full rounded-lg border border-slate-200 px-2 py-1 text-sm\"\n placeholder=\"Uitleg voor de klant (optioneel), bv. 'Max. 12 tekens — komt op de voorkant'\"\n value={f.helpText ?? \"\"}\n onChange={(e) => setPers({ helpText: e.target.value })}\n />\n </div>\n );\n })}\n {(def.personalization ?? []).length < 3 && (\n <Button\n size=\"sm\"\n variant=\"secondary\"\n className=\"w-fit\"\n onClick={() =>\n setDef((p) => ({\n ...p,\n personalization: [\n ...(p.personalization ?? []),\n { key: `veld${(p.personalization ?? []).length + 1}`, label: \"\", type: \"text\", required: false, maxLength: 40, priceCents: 0 },\n ],\n }))\n }\n >\n + Personalisatie-veld\n </Button>\n )}\n </div>\n </Field>\n )}\n </div>\n\n <div className=\"flex flex-wrap items-center gap-6\">\n <Field label=\"Status\">\n <Select\n value={def.status ?? \"draft\"}\n onValueChange={(v) => setDef((p) => ({ ...p, status: v as Product[\"status\"] }))}\n options={STATUS_OPTIONS}\n />\n </Field>\n <Field label=\"Volgorde\">\n <Input\n type=\"number\"\n className=\"w-24\"\n value={def.order ?? 0}\n onChange={(e) => setDef((p) => ({ ...p, order: Math.max(0, Number(e.target.value) || 0) }))}\n />\n </Field>\n <Switch checked={def.featured === true} onCheckedChange={(v) => setDef((p) => ({ ...p, featured: v }))} label=\"Uitgelicht\" />\n <Switch checked={def.trackStock === true} onCheckedChange={(v) => setDef((p) => ({ ...p, trackStock: v }))} label=\"Voorraad bijhouden\" />\n {def.trackStock && (def.options ?? []).length === 0 && (\n <Field label=\"Voorraad\">\n <Input\n type=\"number\"\n className=\"w-24\"\n placeholder=\"∞\"\n value={def.stock ?? \"\"}\n onChange={(e) =>\n setDef((p) => ({ ...p, stock: e.target.value === \"\" ? undefined : Math.max(0, Number(e.target.value) || 0) }))\n }\n />\n </Field>\n )}\n </div>\n\n <div className=\"flex items-center justify-between border-t border-slate-100 pt-4\">\n <div className=\"flex items-center gap-3\">\n <Button onClick={save}>Opslaan</Button>\n <span className=\"text-sm text-slate-500\">{formatCents(def.priceCents)}</span>\n {vuil && <span className=\"text-sm font-medium text-amber-700\">• niet opgeslagen</span>}\n </div>\n <div className=\"flex items-center gap-2\">\n {/* Dupliceren: een variant van een bestaand product maken was overtypen. */}\n <Button variant=\"secondary\" onClick={dupliceer}>Dupliceren</Button>\n <Button variant=\"danger\" onClick={remove}>Verwijderen</Button>\n </div>\n </div>\n </div>\n </Card>\n );\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../../src/admin/shop/ProductsTab.tsx"],"sourcesContent":["\"use client\";\n\nimport { useEffect, useMemo, useState } from \"react\";\nimport { collection, doc, onSnapshot } from \"firebase/firestore\";\nimport { removeProduct, saveProductDoc } from \"./productClient\";\nimport { getDb } from \"../../firebase/client\";\nimport {\n ProductSchema,\n formatCents,\n saneerProductDoc,\n type PersonalizationField,\n type Product,\n type ProductOption,\n type ProductVariant,\n type ShopCategory,\n} from \"../../schema/shop\";\nimport { Badge, Button, Card, Field, Input, Textarea } from \"../ui/primitives\";\nimport { Select } from \"../ui/select\";\nimport { Switch } from \"../ui/switch\";\nimport { useToast } from \"../ui/toast\";\nimport { useConfirm, type ConfirmOptions } from \"../ui/ConfirmDialog\";\nimport { MediaPickerButton } from \"../media/MediaPicker\";\nimport { slugify } from \"../lib/slugify\";\nimport { allVariantKeys, centsToEuroInput, eurosToCents } from \"./lib\";\n\ntype StoredProduct = Product & { id: string };\ntype StoredCategory = ShopCategory & { id: string };\n\nconst STATUS_OPTIONS = [\n { value: \"draft\", label: \"Concept\" },\n { value: \"active\", label: \"Actief\" },\n { value: \"archived\", label: \"Gearchiveerd\" },\n];\n\nexport function ProductsTab({ siteId }: { siteId: string }) {\n const { toast } = useToast();\n const confirm = useConfirm();\n const [products, setProducts] = useState<StoredProduct[]>([]);\n const [cats, setCats] = useState<StoredCategory[]>([]);\n const [selected, setSelected] = useState<string | null>(null);\n const [q, setQ] = useState(\"\");\n\n useEffect(() => {\n const db = getDb();\n if (!db) return;\n const un1 = onSnapshot(collection(db, \"sites\", siteId, \"products\"), (snap) =>\n setProducts(\n snap.docs\n // saneerProductDoc: historische docs dragen null-velden (zie schema/shop.ts) — schoon\n // ze bij het lezen, zodat de form-state ze niet via {...def} opnieuw naar de server lekt.\n .map((d) => ({ id: d.id, ...saneerProductDoc(d.data() as Product) }))\n .sort((a, b) => (a.order ?? 0) - (b.order ?? 0) || a.name.localeCompare(b.name, \"nl\")),\n ),\n );\n const un2 = onSnapshot(collection(db, \"sites\", siteId, \"shopCategories\"), (snap) =>\n setCats(snap.docs.map((d) => ({ id: d.id, ...(d.data() as ShopCategory) }))),\n );\n return () => {\n un1();\n un2();\n };\n }, [siteId]);\n\n async function createProduct() {\n // Ook aanmaken loopt via de callable: een vers product is een draft (onzichtbaar voor de\n // cache), maar zo staat élke productmutatie in de audit-log en is er één pad i.p.v. twee.\n const nieuw = {\n name: \"Nieuw product\",\n slug: `product-${products.length + 1}`,\n description: \"\",\n categoryIds: [],\n images: [],\n priceCents: 0,\n vatRate: 21,\n options: [],\n variants: [],\n trackStock: false,\n status: \"draft\",\n order: products.length,\n featured: false,\n personalizationEnabled: true,\n } satisfies Product;\n try {\n const id = await saveProductDoc(siteId, undefined, nieuw);\n setSelected(id);\n } catch (e) {\n // Zichtbaar falen: dit gooide een tijdlang stil (unhandled) terwijl de knop gewoon niets\n // leek te doen — precies de melding \"bij aanmaken product krijg ik iets met permissions\".\n toast({ title: \"Product aanmaken mislukt\", description: e instanceof Error ? e.message : undefined, tone: \"error\" });\n }\n }\n\n const filtered = useMemo(() => {\n const needle = q.trim().toLowerCase();\n if (!needle) return products;\n return products.filter((p) => p.name.toLowerCase().includes(needle) || p.slug.includes(needle));\n }, [products, q]);\n\n const current = products.find((p) => p.id === selected) ?? null;\n\n return (\n <div className=\"grid gap-4 md:grid-cols-[300px_1fr]\">\n <Card className=\"h-fit\">\n <div className=\"mb-3 flex items-center justify-between\">\n <h3 className=\"text-sm font-semibold\">Producten</h3>\n <Button size=\"sm\" onClick={createProduct}>+ Nieuw</Button>\n </div>\n <Input placeholder=\"Zoeken…\" value={q} onChange={(e) => setQ(e.target.value)} className=\"mb-2\" />\n <ul className=\"grid max-h-[60vh] gap-1 overflow-y-auto\">\n {filtered.map((p) => (\n <li key={p.id}>\n <button\n onClick={() => setSelected(p.id)}\n className={`flex w-full items-center gap-2 rounded-lg px-3 py-2 text-left text-sm ${\n p.id === selected ? \"bg-brand-50 text-brand-700\" : \"hover:bg-slate-100\"\n }`}\n >\n {p.images?.[0] && <img src={p.images[0]} alt=\"\" className=\"h-8 w-8 rounded object-cover\" />}\n {/* Categorie erbij: hetzelfde ontwerp bestaat vaak als rompertje, slabbetje én\n t-shirt, en dan staat dezelfde naam vijf keer onder elkaar in deze lijst.\n Op een eigen regel eronder, want achter de naam is de kolom te smal — dan werd\n juist het onderscheidende deel afgekapt (\"Geschapen! — Korte mouw t-s…\"). */}\n <span className=\"min-w-0 flex-1\">\n <span className=\"block truncate\">{p.name}</span>\n {(() => {\n const cat = cats.find((c) => c.id === (p.categoryIds ?? [])[0]);\n return cat ? <span className=\"block truncate text-xs text-slate-400\">{cat.name}</span> : null;\n })()}\n </span>\n {/* Voorraadsignaal: uitverkocht of bijna op moet je zien zónder elk product te\n openen — anders merk je pas bij een bestelling dat er niets meer is. */}\n {(() => {\n const n = voorraadStand(p);\n if (n === null) return null;\n if (n <= 0) return <Badge tone=\"slate\">uitverkocht</Badge>;\n if (n <= 3) return <Badge tone=\"amber\">{n} over</Badge>;\n return null;\n })()}\n <Badge tone={p.status === \"active\" ? \"green\" : p.status === \"draft\" ? \"amber\" : \"slate\"}>\n {STATUS_OPTIONS.find((s) => s.value === p.status)?.label ?? p.status}\n </Badge>\n </button>\n </li>\n ))}\n {filtered.length === 0 && <li className=\"px-3 py-2 text-sm text-slate-500\">Geen producten.</li>}\n </ul>\n </Card>\n {current ? (\n <ProductForm\n key={current.id}\n siteId={siteId}\n product={current}\n categories={cats}\n onDeleted={() => setSelected(null)}\n onDuplicated={(id) => setSelected(id)}\n onToast={toast}\n confirm={confirm}\n />\n ) : (\n <Card>\n <p className=\"text-sm text-slate-500\">Selecteer of maak een product.</p>\n </Card>\n )}\n </div>\n );\n}\n\n/**\n * Het volledige productformulier. Sinds 7.8 ook buiten dit tabblad in gebruik: het tandwiel bij een\n * product op de pagina opent hetzelfde formulier in een zijpaneel. Bewust hergebruik en geen tweede,\n * uitgeklede versie — twee formulieren voor één product lopen gegarandeerd uit elkaar, en dan hangt\n * het van je route af welke velden je kunt bereiken.\n */\n/**\n * Voorraadstand van een product voor de beheerlijst: het laagste getal dat er te melden valt.\n * `null` = niet bijgehouden of onbeperkt. Zonder opties telt `stock`, met opties de varianten.\n */\nfunction voorraadStand(p: Product): number | null {\n if (p.trackStock !== true) return null;\n if ((p.options ?? []).length === 0) return p.stock ?? null;\n const getallen = (p.variants ?? []).filter((v) => v.stock !== undefined && v.active !== false).map((v) => v.stock as number);\n return getallen.length > 0 ? Math.min(...getallen) : null;\n}\n\nexport function ProductForm({\n siteId,\n product,\n categories,\n onDeleted,\n onDuplicated,\n onToast,\n confirm,\n}: {\n siteId: string;\n product: StoredProduct;\n categories: StoredCategory[];\n onDeleted: () => void;\n /** Optioneel: na dupliceren de kopie selecteren (het zijpaneel doet dit niet). */\n onDuplicated?: (id: string) => void;\n onToast: (t: { title: string; description?: string; tone?: \"default\" | \"success\" | \"error\" }) => void;\n confirm: (opts: ConfirmOptions) => Promise<boolean>;\n}) {\n const [def, setDef] = useState<Product>(product);\n /**\n * Waarschuwen bij niet-opgeslagen werk (7.18). Het formulier heeft geen autosave (een product\n * gaat meteen live), dus wie naar een ander product klikte of het tabblad sloot, was zijn\n * wijzigingen kwijt zonder enige melding.\n */\n const vuil = JSON.stringify(def) !== JSON.stringify(product);\n useEffect(() => {\n if (!vuil) return;\n const waarschuw = (e: BeforeUnloadEvent) => e.preventDefault();\n window.addEventListener(\"beforeunload\", waarschuw);\n return () => window.removeEventListener(\"beforeunload\", waarschuw);\n }, [vuil]);\n const [priceInput, setPriceInput] = useState(centsToEuroInput(product.priceCents));\n const [compareInput, setCompareInput] = useState(centsToEuroInput(product.compareAtCents));\n\n const variantKeys = useMemo(() => allVariantKeys(def.options ?? []), [def.options]);\n\n function setOption(i: number, patch: Partial<ProductOption>) {\n setDef((p) => {\n const options = [...(p.options ?? [])];\n options[i] = { ...options[i], ...patch };\n return { ...p, options };\n });\n }\n\n function variantFor(key: string): ProductVariant | undefined {\n return (def.variants ?? []).find((v) => v.key === key);\n }\n\n function patchVariant(key: string, patch: Partial<ProductVariant>) {\n setDef((p) => {\n const variants = [...(p.variants ?? [])];\n const idx = variants.findIndex((v) => v.key === key);\n if (idx >= 0) variants[idx] = { ...variants[idx], ...patch };\n else variants.push({ key, active: true, ...patch });\n return { ...p, variants };\n });\n }\n\n /** Kopie van dit product maken (nieuwe slug + \"(kopie)\"), als concept zodat je rustig aanpast. */\n async function dupliceer() {\n try {\n const kopie: Product = {\n ...def,\n name: `${def.name} (kopie)`,\n slug: `${def.slug}-kopie`.slice(0, 120),\n status: \"draft\",\n order: (def.order ?? 0) + 1,\n };\n const id = await saveProductDoc(siteId, undefined, kopie);\n onToast({ title: \"Product gedupliceerd\", description: \"De kopie staat als concept klaar.\", tone: \"success\" });\n onDuplicated?.(id);\n } catch (e) {\n onToast({ title: \"Dupliceren mislukt\", description: e instanceof Error ? e.message : undefined, tone: \"error\" });\n }\n }\n\n async function save() {\n const db = getDb();\n if (!db) return;\n // Alleen varianten bewaren die iets toevoegen (afwijkende prijs/voorraad/sku of inactief),\n // en geen wees-varianten van verwijderde optiewaarden.\n const validKeys = new Set(variantKeys);\n const cleanVariants = (def.variants ?? []).filter(\n (v) =>\n validKeys.has(v.key) &&\n (v.priceCents !== undefined || v.stock !== undefined || v.sku !== undefined || v.active === false),\n );\n // Onvolledige personalisatie-regels (zonder label) niet opslaan.\n const cleanPersonalization = (def.personalization ?? []).filter((f) => f.label.trim().length > 0);\n const parsed = ProductSchema.safeParse({\n ...def,\n variants: cleanVariants,\n personalization: cleanPersonalization.length > 0 ? cleanPersonalization : undefined,\n });\n if (!parsed.success) {\n onToast({ title: \"Ongeldig product\", description: parsed.error.issues[0]?.message, tone: \"error\" });\n return;\n }\n // Via de callable (7.9): de server valideert, auditeert en — belangrijker — pingt de\n // products-cachetag, zodat de wijziging ook op de LIVE shoppagina's staat en niet alleen in\n // Firestore. Zie productClient.ts, ook voor de terugval op een oudere backend.\n await saveProductDoc(siteId, product.id, parsed.data);\n onToast({ title: \"Product opgeslagen\", tone: \"success\" });\n }\n\n async function remove() {\n const ok = await confirm({\n title: `\"${product.name}\" verwijderen?`,\n description: \"Bestaande bestellingen behouden hun eigen kopie van naam en prijs.\",\n tone: \"danger\",\n });\n if (!ok) return;\n await removeProduct(siteId, product.id);\n onDeleted();\n }\n\n return (\n <Card>\n <div className=\"grid gap-4\">\n <div className=\"grid gap-4 md:grid-cols-2\">\n <Field label=\"Naam\">\n <Input\n value={def.name}\n onChange={(e) => setDef((p) => ({ ...p, name: e.target.value, slug: slugify(e.target.value) || p.slug }))}\n />\n </Field>\n <Field label=\"Slug\" hint=\"Product-URL: /product?p=<slug>\">\n <Input value={def.slug} onChange={(e) => setDef((p) => ({ ...p, slug: slugify(e.target.value) }))} />\n </Field>\n </div>\n\n <div className=\"grid gap-4 md:grid-cols-3\">\n <Field label=\"Prijs (€, incl. btw)\">\n <Input\n value={priceInput}\n onChange={(e) => {\n setPriceInput(e.target.value);\n const c = eurosToCents(e.target.value);\n if (c !== null) setDef((p) => ({ ...p, priceCents: c }));\n }}\n />\n </Field>\n <Field label=\"Van-prijs (€, optioneel)\" hint=\"Doorgestreept naast de prijs\">\n <Input\n value={compareInput}\n onChange={(e) => {\n setCompareInput(e.target.value);\n const c = eurosToCents(e.target.value);\n setDef((p) => ({ ...p, compareAtCents: c ?? undefined }));\n }}\n />\n </Field>\n <Field label=\"Btw-tarief\">\n <Select\n value={String(def.vatRate ?? 21)}\n onValueChange={(v) => setDef((p) => ({ ...p, vatRate: Number(v) as 21 | 9 | 0 }))}\n options={[\n { value: \"21\", label: \"21%\" },\n { value: \"9\", label: \"9%\" },\n { value: \"0\", label: \"0%\" },\n ]}\n />\n </Field>\n </div>\n\n <Field label=\"Omschrijving\">\n <Textarea rows={4} value={def.description ?? \"\"} onChange={(e) => setDef((p) => ({ ...p, description: e.target.value }))} />\n </Field>\n\n <Field label=\"Afbeeldingen\" hint=\"Eerste afbeelding is het hoofdbeeld\">\n <div className=\"flex flex-wrap items-center gap-3\">\n {(def.images ?? []).map((url, i) => (\n <div key={url + i} className=\"relative\">\n <img src={url} alt=\"\" className={`h-16 w-16 rounded-lg object-cover ${i === 0 ? \"ring-2 ring-brand-500\" : \"\"}`} />\n {i === 0 && (\n <span className=\"absolute -bottom-2 left-1/2 -translate-x-1/2 rounded-full bg-brand-500 px-1.5 text-[10px] text-white\">hoofd</span>\n )}\n <button\n type=\"button\"\n aria-label=\"Verwijder afbeelding\"\n className=\"absolute -right-2 -top-2 grid h-5 w-5 place-items-center rounded-full bg-slate-700 text-xs text-white\"\n onClick={() => setDef((p) => ({ ...p, images: (p.images ?? []).filter((_, j) => j !== i) }))}\n >\n ✕\n </button>\n {/* Herschikken: de eerste afbeelding is het hoofdbeeld, en dat was alleen te\n veranderen door alles te wissen en opnieuw te kiezen. */}\n {i > 0 && (\n <button\n type=\"button\"\n aria-label=\"Naar voren verplaatsen\"\n title={i === 1 ? \"Maak hoofdbeeld\" : \"Naar voren\"}\n className=\"absolute -left-2 -top-2 grid h-5 w-5 place-items-center rounded-full bg-slate-700 text-xs text-white\"\n onClick={() =>\n setDef((p) => {\n const imgs = [...(p.images ?? [])];\n [imgs[i - 1], imgs[i]] = [imgs[i], imgs[i - 1]];\n return { ...p, images: imgs };\n })\n }\n >\n ←\n </button>\n )}\n </div>\n ))}\n <MediaPickerButton\n siteId={siteId}\n label=\"+ Afbeelding\"\n onSelect={(m) => setDef((p) => ({ ...p, images: [...(p.images ?? []), m.url] }))}\n />\n </div>\n </Field>\n\n <Field label=\"Categorieën\">\n <div className=\"flex flex-wrap gap-2\">\n {categories.map((c) => {\n const on = (def.categoryIds ?? []).includes(c.id);\n return (\n <button\n key={c.id}\n type=\"button\"\n onClick={() =>\n setDef((p) => ({\n ...p,\n categoryIds: on ? (p.categoryIds ?? []).filter((x) => x !== c.id) : [...(p.categoryIds ?? []), c.id],\n }))\n }\n className={`rounded-full border px-3 py-1 text-sm ${\n on ? \"border-brand-500 bg-brand-50 text-brand-700\" : \"border-slate-200 hover:bg-slate-50\"\n }`}\n >\n {on ? \"✓ \" : \"\"}\n {c.name}\n </button>\n );\n })}\n {categories.length === 0 && <span className=\"text-sm text-slate-500\">Maak eerst een categorie aan.</span>}\n </div>\n </Field>\n\n {/* Opties & varianten */}\n <Field\n label=\"Opties (bv. Maat, Kleur)\"\n hint=\"Max. 3. Waarden gescheiden door komma's. Combinaties kun je hieronder per stuk aanpassen.\"\n >\n <div className=\"grid gap-2\">\n {(def.options ?? []).map((opt, i) => (\n <div key={i} className=\"grid gap-2 rounded-lg border border-slate-100 p-2\">\n <div className=\"flex items-center gap-2\">\n <Input\n className=\"w-36\"\n placeholder=\"Naam (bv. Maat)\"\n value={opt.name}\n onChange={(e) => setOption(i, { name: e.target.value })}\n />\n <Input\n placeholder=\"Waarden, bv: 56, 62, 68, 74\"\n value={opt.values.join(\", \")}\n onChange={(e) =>\n setOption(i, { values: e.target.value.split(\",\").map((s) => s.trim()).filter(Boolean) })\n }\n />\n <Button\n size=\"sm\"\n variant=\"ghost\"\n onClick={() => setDef((p) => ({ ...p, options: (p.options ?? []).filter((_, j) => j !== i) }))}\n >\n ✕\n </Button>\n </div>\n {/* Afbeelding per waarde (5.6) — vooral voor de kleur-as: de detailpagina\n wisselt het hoofdbeeld mee met de gekozen kleur. */}\n {/^kleur|^colou?r/i.test(opt.name) && opt.values.length > 0 && (\n <div className=\"flex flex-wrap items-center gap-2\">\n {opt.values.map((value) => {\n const url = opt.images?.[value];\n return (\n <span key={value} className=\"flex items-center gap-1 rounded-full border border-slate-200 px-2 py-1 text-xs\">\n {url && <img src={url} alt=\"\" className=\"h-6 w-6 rounded object-cover\" />}\n {value}\n <MediaPickerButton\n siteId={siteId}\n label={url ? \"↻\" : \"+ foto\"}\n onSelect={(m) =>\n setOption(i, { images: { ...(opt.images ?? {}), [value]: m.url } })\n }\n />\n {url && (\n <button\n type=\"button\"\n aria-label={`Verwijder foto van ${value}`}\n className=\"text-slate-400 hover:text-slate-700\"\n onClick={() => {\n const images = { ...(opt.images ?? {}) };\n delete images[value];\n setOption(i, { images });\n }}\n >\n ✕\n </button>\n )}\n </span>\n );\n })}\n </div>\n )}\n </div>\n ))}\n {(def.options ?? []).length < 3 && (\n <Button\n size=\"sm\"\n variant=\"secondary\"\n className=\"w-fit\"\n onClick={() => setDef((p) => ({ ...p, options: [...(p.options ?? []), { name: \"\", values: [] }] }))}\n >\n + Optie\n </Button>\n )}\n </div>\n </Field>\n\n {variantKeys.length > 0 && variantKeys.length <= 60 && (\n <Field label=\"Varianten\" hint=\"Leeg = standaardprijs / onbeperkte voorraad\">\n <div className=\"overflow-x-auto\">\n <table className=\"w-full text-sm\">\n <thead>\n <tr className=\"text-left text-xs text-slate-500\">\n <th className=\"py-1 pr-2\">Combinatie</th>\n <th className=\"py-1 pr-2\">Prijs (€)</th>\n {def.trackStock && <th className=\"py-1 pr-2\">Voorraad</th>}\n <th className=\"py-1 pr-2\">Artikelnr.</th>\n <th className=\"py-1 pr-2\">Leverbaar</th>\n </tr>\n </thead>\n <tbody>\n {variantKeys.map((key) => {\n const v = variantFor(key);\n return (\n <tr key={key} className=\"border-t border-slate-100\">\n <td className=\"py-1 pr-2\">{key.split(\"|\").join(\" · \")}</td>\n <td className=\"py-1 pr-2\">\n {/* Native uncontrolled inputs: geen cursor-gedoe bij \"13,\" tijdens typen. */}\n <input\n className=\"w-24 rounded-lg border border-slate-200 px-2 py-1\"\n placeholder={centsToEuroInput(def.priceCents)}\n defaultValue={centsToEuroInput(v?.priceCents)}\n onChange={(e) => {\n const c = eurosToCents(e.target.value);\n patchVariant(key, { priceCents: c ?? undefined });\n }}\n />\n </td>\n {def.trackStock && (\n <td className=\"py-1 pr-2\">\n <input\n className=\"w-20 rounded-lg border border-slate-200 px-2 py-1\"\n type=\"number\"\n min={0}\n placeholder=\"∞\"\n defaultValue={v?.stock ?? \"\"}\n onChange={(e) => {\n const n = e.target.value === \"\" ? undefined : Math.max(0, Number(e.target.value) || 0);\n patchVariant(key, { stock: n });\n }}\n />\n </td>\n )}\n <td className=\"py-1 pr-2\">\n {/* SKU stond al in het schema maar was nergens in te vullen — terwijl\n het juist per variant nodig is (voor je eigen administratie/pakbon). */}\n <input\n className=\"w-28 rounded-lg border border-slate-200 px-2 py-1\"\n placeholder=\"—\"\n defaultValue={v?.sku ?? \"\"}\n onChange={(e) => patchVariant(key, { sku: e.target.value.trim() || undefined })}\n />\n </td>\n <td className=\"py-1 pr-2\">\n <Switch checked={v?.active !== false} onCheckedChange={(on) => patchVariant(key, { active: on })} />\n </td>\n </tr>\n );\n })}\n </tbody>\n </table>\n </div>\n </Field>\n )}\n {variantKeys.length > 60 && (\n <p className=\"text-sm text-amber-600\">\n {variantKeys.length} combinaties — te veel om per stuk te tonen. Beperk het aantal optie-waarden.\n </p>\n )}\n\n {/* Personalisatie (5.6): eigen naam/logo van de klant, met optionele meerprijs.\n Sinds 7.19 met een eigen aan/uit-schakelaar: uitzetten bewaart de velden. */}\n <div className=\"rounded-xl border border-slate-200 p-3\">\n <Switch\n checked={def.personalizationEnabled !== false}\n onCheckedChange={(v) =>\n setDef((p) => ({\n ...p,\n personalizationEnabled: v,\n // Bij het aanzetten meteen één leeg veld klaarzetten, anders staat de klant voor\n // een lege sectie en lijkt de schakelaar niets te doen.\n personalization:\n v && (p.personalization ?? []).length === 0\n ? [{ key: \"naam\", label: \"Naam\", type: \"text\", required: false, maxLength: 20, priceCents: 0 }]\n : p.personalization,\n }))\n }\n label=\"Persoonlijk maken aanzetten voor dit product\"\n />\n {def.personalizationEnabled === false && (def.personalization?.length ?? 0) > 0 && (\n <p className=\"mt-1 text-xs text-slate-500\">\n Staat uit — de klant ziet geen invulveld. Je {def.personalization!.length === 1 ? \"instelling blijft\" : \"instellingen blijven\"} bewaard\n voor als je het weer aanzet.\n </p>\n )}\n\n {def.personalizationEnabled !== false && (\n <Field\n label=\"Personalisatie (eigen tekst of afbeelding)\"\n hint=\"Max. 3 velden. De klant vult dit in op de productpagina; de meerprijs telt automatisch mee.\"\n >\n <div className=\"grid gap-2\">\n {(def.personalization ?? []).map((f, i) => {\n const setPers = (patch: Partial<PersonalizationField>) =>\n setDef((p) => {\n const personalization = [...(p.personalization ?? [])];\n personalization[i] = { ...personalization[i], ...patch };\n return { ...p, personalization };\n });\n return (\n <div key={i} className=\"grid gap-2 rounded-lg border border-slate-100 p-2 md:grid-cols-[1fr_130px_90px_110px_auto]\">\n <Input\n placeholder='Label (bv. \"Naam van het kindje\")'\n value={f.label}\n onChange={(e) =>\n setPers({\n label: e.target.value,\n key: f.key || e.target.value.toLowerCase().replace(/[^a-z0-9]+/g, \"_\").replace(/^_+|_+$/g, \"\").slice(0, 40) || `veld${i + 1}`,\n })\n }\n />\n <Select\n value={f.type}\n onValueChange={(v) => setPers({ type: v as \"text\" | \"image\" })}\n options={[\n { value: \"text\", label: \"Tekst\" },\n { value: \"image\", label: \"Afbeelding\" },\n ]}\n />\n {f.type === \"text\" ? (\n <Input\n type=\"number\"\n title=\"Max. tekens\"\n min={1}\n max={200}\n value={f.maxLength ?? 40}\n onChange={(e) => setPers({ maxLength: Math.max(1, Math.min(200, Number(e.target.value) || 40)) })}\n />\n ) : (\n <span className=\"self-center text-xs text-slate-400\">jpg/png/webp</span>\n )}\n <input\n className=\"rounded-lg border border-slate-200 px-2 py-1\"\n placeholder=\"Meerprijs €\"\n defaultValue={f.priceCents ? centsToEuroInput(f.priceCents) : \"\"}\n onChange={(e) => setPers({ priceCents: eurosToCents(e.target.value) ?? 0 })}\n />\n <div className=\"flex items-center gap-2\">\n <Switch checked={f.required === true} onCheckedChange={(v) => setPers({ required: v })} label=\"Verplicht\" />\n <Button\n size=\"sm\"\n variant=\"ghost\"\n onClick={() => setDef((p) => ({ ...p, personalization: (p.personalization ?? []).filter((_, j) => j !== i) }))}\n >\n ✕\n </Button>\n </div>\n {/* Uitleg onder het veld in de winkel. Bestond al in het schema en werd al\n gerenderd op de productpagina, maar was hier nooit in te vullen. */}\n <input\n className=\"col-span-full rounded-lg border border-slate-200 px-2 py-1 text-sm\"\n placeholder=\"Uitleg voor de klant (optioneel), bv. 'Max. 12 tekens — komt op de voorkant'\"\n value={f.helpText ?? \"\"}\n onChange={(e) => setPers({ helpText: e.target.value })}\n />\n </div>\n );\n })}\n {(def.personalization ?? []).length < 3 && (\n <Button\n size=\"sm\"\n variant=\"secondary\"\n className=\"w-fit\"\n onClick={() =>\n setDef((p) => ({\n ...p,\n personalization: [\n ...(p.personalization ?? []),\n { key: `veld${(p.personalization ?? []).length + 1}`, label: \"\", type: \"text\", required: false, maxLength: 40, priceCents: 0 },\n ],\n }))\n }\n >\n + Personalisatie-veld\n </Button>\n )}\n </div>\n </Field>\n )}\n </div>\n\n <div className=\"flex flex-wrap items-center gap-6\">\n <Field label=\"Status\">\n <Select\n value={def.status ?? \"draft\"}\n onValueChange={(v) => setDef((p) => ({ ...p, status: v as Product[\"status\"] }))}\n options={STATUS_OPTIONS}\n />\n </Field>\n <Field label=\"Volgorde\">\n <Input\n type=\"number\"\n className=\"w-24\"\n value={def.order ?? 0}\n onChange={(e) => setDef((p) => ({ ...p, order: Math.max(0, Number(e.target.value) || 0) }))}\n />\n </Field>\n <Switch checked={def.featured === true} onCheckedChange={(v) => setDef((p) => ({ ...p, featured: v }))} label=\"Uitgelicht\" />\n <Switch checked={def.trackStock === true} onCheckedChange={(v) => setDef((p) => ({ ...p, trackStock: v }))} label=\"Voorraad bijhouden\" />\n {def.trackStock && (def.options ?? []).length === 0 && (\n <Field label=\"Voorraad\">\n <Input\n type=\"number\"\n className=\"w-24\"\n placeholder=\"∞\"\n value={def.stock ?? \"\"}\n onChange={(e) =>\n setDef((p) => ({ ...p, stock: e.target.value === \"\" ? undefined : Math.max(0, Number(e.target.value) || 0) }))\n }\n />\n </Field>\n )}\n </div>\n\n <div className=\"flex items-center justify-between border-t border-slate-100 pt-4\">\n <div className=\"flex items-center gap-3\">\n <Button onClick={save}>Opslaan</Button>\n <span className=\"text-sm text-slate-500\">{formatCents(def.priceCents)}</span>\n {vuil && <span className=\"text-sm font-medium text-amber-700\">• niet opgeslagen</span>}\n </div>\n <div className=\"flex items-center gap-2\">\n {/* Dupliceren: een variant van een bestaand product maken was overtypen. */}\n <Button variant=\"secondary\" onClick={dupliceer}>Dupliceren</Button>\n <Button variant=\"danger\" onClick={remove}>Verwijderen</Button>\n </div>\n </div>\n </div>\n </Card>\n );\n}\n"],"mappings":"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","names":[]}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Aantal-veld voor de winkel (productpagina én winkelwagen).
|
|
3
|
+
*
|
|
4
|
+
* Waarom een eigen component en niet gewoon een `<input type="number">`? Omdat een getal-invoer
|
|
5
|
+
* die rechtstreeks aan een getal-state hangt op twee manieren stukgaat, en klanten liepen tegen
|
|
6
|
+
* allebei aan:
|
|
7
|
+
*
|
|
8
|
+
* 1. **Je kunt niets intypen.** Om "2" te typen wis je eerst de "1". Dat wissen levert een lege
|
|
9
|
+
* string op; `Number("") || 1` maakte daar meteen weer 1 van, React zette de 1 terug in het
|
|
10
|
+
* veld en de cursor sprong. Het aantal veranderen kon dus alleen nog met de pijltjes.
|
|
11
|
+
* 2. **In de winkelwagen verdween het product.** Daar werd een lege string 0, en `cartSetQty`
|
|
12
|
+
* gooit een regel met aantal 0 uit de wagen. Eén keer backspace en je bestelling was weg.
|
|
13
|
+
*
|
|
14
|
+
* De oplossing is dat het veld tijdens het typen even leeg mág zijn: de ruwe tekst staat in een
|
|
15
|
+
* eigen state en er wordt pas iets doorgegeven als er een geldig getal staat. Verlaat je het veld
|
|
16
|
+
* leeg of ongeldig, dan komt de laatste geldige waarde terug. Weghalen doe je met het kruisje,
|
|
17
|
+
* niet door een veld leeg te maken — dat is een bewuste handeling en geen typfout.
|
|
18
|
+
*/
|
|
19
|
+
export declare function AantalInvoer({ value, onChange, min, max, ariaLabel, style, }: {
|
|
20
|
+
value: number;
|
|
21
|
+
onChange: (n: number) => void;
|
|
22
|
+
min?: number;
|
|
23
|
+
max?: number;
|
|
24
|
+
ariaLabel?: string;
|
|
25
|
+
style?: React.CSSProperties;
|
|
26
|
+
}): import("react").JSX.Element;
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx } from "react/jsx-runtime";
|
|
3
|
+
import { useEffect, useState } from "react";
|
|
4
|
+
function AantalInvoer({
|
|
5
|
+
value,
|
|
6
|
+
onChange,
|
|
7
|
+
min = 1,
|
|
8
|
+
max = 999,
|
|
9
|
+
ariaLabel,
|
|
10
|
+
style
|
|
11
|
+
}) {
|
|
12
|
+
const [tekst, setTekst] = useState(String(value));
|
|
13
|
+
useEffect(() => {
|
|
14
|
+
setTekst((huidig) => Number(huidig) === value ? huidig : String(value));
|
|
15
|
+
}, [value]);
|
|
16
|
+
return /* @__PURE__ */ jsx(
|
|
17
|
+
"input",
|
|
18
|
+
{
|
|
19
|
+
className: "xicms-input",
|
|
20
|
+
type: "number",
|
|
21
|
+
inputMode: "numeric",
|
|
22
|
+
min,
|
|
23
|
+
max,
|
|
24
|
+
value: tekst,
|
|
25
|
+
"aria-label": ariaLabel,
|
|
26
|
+
onChange: (e) => {
|
|
27
|
+
const rauw = e.target.value;
|
|
28
|
+
setTekst(rauw);
|
|
29
|
+
if (rauw.trim() === "") return;
|
|
30
|
+
const n = Number(rauw);
|
|
31
|
+
if (!Number.isFinite(n)) return;
|
|
32
|
+
const begrensd = Math.max(min, Math.min(max, Math.floor(n)));
|
|
33
|
+
if (begrensd !== value) onChange(begrensd);
|
|
34
|
+
if (n > max) setTekst(String(begrensd));
|
|
35
|
+
},
|
|
36
|
+
onBlur: () => {
|
|
37
|
+
const n = Number(tekst);
|
|
38
|
+
const geldig = tekst.trim() !== "" && Number.isFinite(n) && n >= min;
|
|
39
|
+
const eind = geldig ? Math.max(min, Math.min(max, Math.floor(n))) : value;
|
|
40
|
+
setTekst(String(eind));
|
|
41
|
+
if (eind !== value) onChange(eind);
|
|
42
|
+
},
|
|
43
|
+
style
|
|
44
|
+
}
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
export {
|
|
48
|
+
AantalInvoer
|
|
49
|
+
};
|
|
50
|
+
//# sourceMappingURL=AantalInvoer.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../src/modules/shop/blocks/AantalInvoer.tsx"],"sourcesContent":["\"use client\";\n\nimport { useEffect, useState } from \"react\";\n\n/**\n * Aantal-veld voor de winkel (productpagina én winkelwagen).\n *\n * Waarom een eigen component en niet gewoon een `<input type=\"number\">`? Omdat een getal-invoer\n * die rechtstreeks aan een getal-state hangt op twee manieren stukgaat, en klanten liepen tegen\n * allebei aan:\n *\n * 1. **Je kunt niets intypen.** Om \"2\" te typen wis je eerst de \"1\". Dat wissen levert een lege\n * string op; `Number(\"\") || 1` maakte daar meteen weer 1 van, React zette de 1 terug in het\n * veld en de cursor sprong. Het aantal veranderen kon dus alleen nog met de pijltjes.\n * 2. **In de winkelwagen verdween het product.** Daar werd een lege string 0, en `cartSetQty`\n * gooit een regel met aantal 0 uit de wagen. Eén keer backspace en je bestelling was weg.\n *\n * De oplossing is dat het veld tijdens het typen even leeg mág zijn: de ruwe tekst staat in een\n * eigen state en er wordt pas iets doorgegeven als er een geldig getal staat. Verlaat je het veld\n * leeg of ongeldig, dan komt de laatste geldige waarde terug. Weghalen doe je met het kruisje,\n * niet door een veld leeg te maken — dat is een bewuste handeling en geen typfout.\n */\nexport function AantalInvoer({\n value,\n onChange,\n min = 1,\n max = 999,\n ariaLabel,\n style,\n}: {\n value: number;\n onChange: (n: number) => void;\n min?: number;\n max?: number;\n ariaLabel?: string;\n style?: React.CSSProperties;\n}) {\n const [tekst, setTekst] = useState(String(value));\n\n // Verandert het aantal buiten dit veld om (andere regel aangepast, cart hersteld), dan volgt\n // het veld mee — maar niet terwijl de bezoeker zelf aan het typen is: dan zou de tekst\n // onder zijn handen vandaan springen.\n useEffect(() => {\n setTekst((huidig) => (Number(huidig) === value ? huidig : String(value)));\n }, [value]);\n\n return (\n <input\n className=\"xicms-input\"\n type=\"number\"\n inputMode=\"numeric\"\n min={min}\n max={max}\n value={tekst}\n aria-label={ariaLabel}\n onChange={(e) => {\n const rauw = e.target.value;\n setTekst(rauw);\n if (rauw.trim() === \"\") return; // leeg mag tijdens het typen; nog niets doorgeven\n const n = Number(rauw);\n if (!Number.isFinite(n)) return;\n const begrensd = Math.max(min, Math.min(max, Math.floor(n)));\n if (begrensd !== value) onChange(begrensd);\n // Alleen de tekst corrigeren als de invoer écht buiten de grenzen viel; anders blijft\n // \"0\" tijdens het typen van \"0\" → onmogelijk om bv. \"10\" te typen op een leeg veld.\n if (n > max) setTekst(String(begrensd));\n }}\n onBlur={() => {\n const n = Number(tekst);\n const geldig = tekst.trim() !== \"\" && Number.isFinite(n) && n >= min;\n const eind = geldig ? Math.max(min, Math.min(max, Math.floor(n))) : value;\n setTekst(String(eind));\n if (eind !== value) onChange(eind);\n }}\n style={style}\n />\n );\n}\n"],"mappings":";AA+CI;AA7CJ,SAAS,WAAW,gBAAgB;AAoB7B,SAAS,aAAa;AAAA,EAC3B;AAAA,EACA;AAAA,EACA,MAAM;AAAA,EACN,MAAM;AAAA,EACN;AAAA,EACA;AACF,GAOG;AACD,QAAM,CAAC,OAAO,QAAQ,IAAI,SAAS,OAAO,KAAK,CAAC;AAKhD,YAAU,MAAM;AACd,aAAS,CAAC,WAAY,OAAO,MAAM,MAAM,QAAQ,SAAS,OAAO,KAAK,CAAE;AAAA,EAC1E,GAAG,CAAC,KAAK,CAAC;AAEV,SACE;AAAA,IAAC;AAAA;AAAA,MACC,WAAU;AAAA,MACV,MAAK;AAAA,MACL,WAAU;AAAA,MACV;AAAA,MACA;AAAA,MACA,OAAO;AAAA,MACP,cAAY;AAAA,MACZ,UAAU,CAAC,MAAM;AACf,cAAM,OAAO,EAAE,OAAO;AACtB,iBAAS,IAAI;AACb,YAAI,KAAK,KAAK,MAAM,GAAI;AACxB,cAAM,IAAI,OAAO,IAAI;AACrB,YAAI,CAAC,OAAO,SAAS,CAAC,EAAG;AACzB,cAAM,WAAW,KAAK,IAAI,KAAK,KAAK,IAAI,KAAK,KAAK,MAAM,CAAC,CAAC,CAAC;AAC3D,YAAI,aAAa,MAAO,UAAS,QAAQ;AAGzC,YAAI,IAAI,IAAK,UAAS,OAAO,QAAQ,CAAC;AAAA,MACxC;AAAA,MACA,QAAQ,MAAM;AACZ,cAAM,IAAI,OAAO,KAAK;AACtB,cAAM,SAAS,MAAM,KAAK,MAAM,MAAM,OAAO,SAAS,CAAC,KAAK,KAAK;AACjE,cAAM,OAAO,SAAS,KAAK,IAAI,KAAK,KAAK,IAAI,KAAK,KAAK,MAAM,CAAC,CAAC,CAAC,IAAI;AACpE,iBAAS,OAAO,IAAI,CAAC;AACrB,YAAI,SAAS,MAAO,UAAS,IAAI;AAAA,MACnC;AAAA,MACA;AAAA;AAAA,EACF;AAEJ;","names":[]}
|
|
@@ -4,6 +4,7 @@ import { formatCents, shippingCentsFor } from "../../../schema/shopGeld.js";
|
|
|
4
4
|
import { defineBlock } from "../../../blocks/registry.js";
|
|
5
5
|
import { Card, Heading, Text } from "../../../blocks/ui/index.js";
|
|
6
6
|
import { cartLineKey, cartRemove, cartSetQty, useCart } from "../cartStore.js";
|
|
7
|
+
import { AantalInvoer } from "./AantalInvoer.js";
|
|
7
8
|
import { useShopStrings } from "../strings.js";
|
|
8
9
|
import { useShopSettings } from "../lib.js";
|
|
9
10
|
function CartRender({ block, siteId }) {
|
|
@@ -41,15 +42,13 @@ function CartRender({ block, siteId }) {
|
|
|
41
42
|
] }),
|
|
42
43
|
/* @__PURE__ */ jsxs("div", { style: { display: "flex", alignItems: "center", gap: 8 }, children: [
|
|
43
44
|
/* @__PURE__ */ jsx(
|
|
44
|
-
|
|
45
|
+
AantalInvoer,
|
|
45
46
|
{
|
|
46
|
-
className: "xicms-input",
|
|
47
|
-
type: "number",
|
|
48
|
-
min: 0,
|
|
49
|
-
max: 999,
|
|
50
47
|
value: l.qty,
|
|
51
|
-
|
|
52
|
-
|
|
48
|
+
onChange: (n) => cartSetQty(siteId, l, n),
|
|
49
|
+
min: 1,
|
|
50
|
+
max: 999,
|
|
51
|
+
ariaLabel: `${t("qty")} ${l.name}`,
|
|
53
52
|
style: { width: 72 }
|
|
54
53
|
}
|
|
55
54
|
),
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../src/modules/shop/blocks/Cart.tsx"],"sourcesContent":["\"use client\";\n\nimport type { z } from \"zod\";\nimport { formatCents, shippingCentsFor } from \"../../../schema/shopGeld\";\nimport type { CartBlockSchema } from \"../../../schema/shop\";\nimport { defineBlock, type BlockRenderProps } from \"../../../blocks/registry\";\nimport { Card, Heading, Text } from \"../../../blocks/ui\";\nimport { cartLineKey, cartRemove, cartSetQty, useCart } from \"../cartStore\";\nimport { useShopStrings } from \"../strings\";\nimport { useShopSettings } from \"../lib\";\n\ntype Blk = z.infer<typeof CartBlockSchema>;\n\nfunction CartRender({ block, siteId }: BlockRenderProps<Blk>) {\n const settings = useShopSettings();\n const t = useShopStrings();\n const { lines, itemsCents } = useCart(siteId);\n /**\n * Verzendkosten in de winkelwagen zijn een RICHTBEDRAG voor het standaardland: de klant kiest\n * zijn land (en een eventuele kortingscode) pas bij het afrekenen. Met zones staat dat er nu\n * ook bij, zodat \"€ 7,25\" niet als een belofte leest voor een Belgische bezoeker.\n */\n const shippingCents = shippingCentsFor(\n itemsCents,\n {\n shippingFlatCents: settings.shippingFlatCents,\n shippingFreeFromCents: settings.shippingFreeFromCents || undefined,\n shippingZones: settings.shippingZones,\n },\n lines.length > 0,\n );\n const meerdereZones = (settings.shippingZones ?? []).length > 1;\n\n return (\n <div className=\"xicms-shop-cart\">\n {block.heading && <Heading level={1} data-xicms-edit=\"heading\">{block.heading}</Heading>}\n\n {lines.length === 0 ? (\n <Card padding={4} variant=\"muted\" style={{ marginTop: \"1rem\" }}>\n <Text>\n {t(\"cartEmpty\")} <a href={settings.shopPath}>{t(\"toShop\")}</a>\n </Text>\n </Card>\n ) : (\n <div style={{ display: \"grid\", gap: \"1rem\", marginTop: \"1rem\" }}>\n {lines.map((l) => (\n <Card key={cartLineKey(l)} padding={3}>\n <div style={{ display: \"flex\", gap: \"1rem\", alignItems: \"center\", flexWrap: \"wrap\" }}>\n {l.imageUrl && (\n <img src={l.imageUrl} alt=\"\" style={{ width: 72, height: 72, objectFit: \"cover\", borderRadius: 8 }} />\n )}\n <div style={{ flex: \"1 1 200px\" }}>\n <a href={`${settings.detailPath}?p=${encodeURIComponent(l.slug)}`} style={{ textDecoration: \"none\", color: \"inherit\" }}>\n <Text weight=\"bold\" display=\"block\">{l.name}</Text>\n </a>\n {l.variantLabel && <Text size=\"sm\" display=\"block\" style={{ opacity: 0.75 }}>{l.variantLabel}</Text>}\n {l.personalizationLabel && (\n <Text size=\"sm\" display=\"block\" style={{ opacity: 0.75 }}>{l.personalizationLabel}</Text>\n )}\n <Text size=\"sm\" display=\"block\" style={{ marginTop: 2 }}>\n {formatCents(l.unitPriceCents, settings.currency)} {t(\"perPiece\")}\n </Text>\n </div>\n <div style={{ display: \"flex\", alignItems: \"center\", gap: 8 }}>\n
|
|
1
|
+
{"version":3,"sources":["../../../../src/modules/shop/blocks/Cart.tsx"],"sourcesContent":["\"use client\";\n\nimport type { z } from \"zod\";\nimport { formatCents, shippingCentsFor } from \"../../../schema/shopGeld\";\nimport type { CartBlockSchema } from \"../../../schema/shop\";\nimport { defineBlock, type BlockRenderProps } from \"../../../blocks/registry\";\nimport { Card, Heading, Text } from \"../../../blocks/ui\";\nimport { cartLineKey, cartRemove, cartSetQty, useCart } from \"../cartStore\";\nimport { AantalInvoer } from \"./AantalInvoer\";\nimport { useShopStrings } from \"../strings\";\nimport { useShopSettings } from \"../lib\";\n\ntype Blk = z.infer<typeof CartBlockSchema>;\n\nfunction CartRender({ block, siteId }: BlockRenderProps<Blk>) {\n const settings = useShopSettings();\n const t = useShopStrings();\n const { lines, itemsCents } = useCart(siteId);\n /**\n * Verzendkosten in de winkelwagen zijn een RICHTBEDRAG voor het standaardland: de klant kiest\n * zijn land (en een eventuele kortingscode) pas bij het afrekenen. Met zones staat dat er nu\n * ook bij, zodat \"€ 7,25\" niet als een belofte leest voor een Belgische bezoeker.\n */\n const shippingCents = shippingCentsFor(\n itemsCents,\n {\n shippingFlatCents: settings.shippingFlatCents,\n shippingFreeFromCents: settings.shippingFreeFromCents || undefined,\n shippingZones: settings.shippingZones,\n },\n lines.length > 0,\n );\n const meerdereZones = (settings.shippingZones ?? []).length > 1;\n\n return (\n <div className=\"xicms-shop-cart\">\n {block.heading && <Heading level={1} data-xicms-edit=\"heading\">{block.heading}</Heading>}\n\n {lines.length === 0 ? (\n <Card padding={4} variant=\"muted\" style={{ marginTop: \"1rem\" }}>\n <Text>\n {t(\"cartEmpty\")} <a href={settings.shopPath}>{t(\"toShop\")}</a>\n </Text>\n </Card>\n ) : (\n <div style={{ display: \"grid\", gap: \"1rem\", marginTop: \"1rem\" }}>\n {lines.map((l) => (\n <Card key={cartLineKey(l)} padding={3}>\n <div style={{ display: \"flex\", gap: \"1rem\", alignItems: \"center\", flexWrap: \"wrap\" }}>\n {l.imageUrl && (\n <img src={l.imageUrl} alt=\"\" style={{ width: 72, height: 72, objectFit: \"cover\", borderRadius: 8 }} />\n )}\n <div style={{ flex: \"1 1 200px\" }}>\n <a href={`${settings.detailPath}?p=${encodeURIComponent(l.slug)}`} style={{ textDecoration: \"none\", color: \"inherit\" }}>\n <Text weight=\"bold\" display=\"block\">{l.name}</Text>\n </a>\n {l.variantLabel && <Text size=\"sm\" display=\"block\" style={{ opacity: 0.75 }}>{l.variantLabel}</Text>}\n {l.personalizationLabel && (\n <Text size=\"sm\" display=\"block\" style={{ opacity: 0.75 }}>{l.personalizationLabel}</Text>\n )}\n <Text size=\"sm\" display=\"block\" style={{ marginTop: 2 }}>\n {formatCents(l.unitPriceCents, settings.currency)} {t(\"perPiece\")}\n </Text>\n </div>\n <div style={{ display: \"flex\", alignItems: \"center\", gap: 8 }}>\n {/* min 1: een leeg veld mag nooit de regel uit de wagen gooien — weghalen doe\n je met het kruisje hiernaast. */}\n <AantalInvoer\n value={l.qty}\n onChange={(n) => cartSetQty(siteId, l, n)}\n min={1}\n max={999}\n ariaLabel={`${t(\"qty\")} ${l.name}`}\n style={{ width: 72 }}\n />\n <Text weight=\"bold\" style={{ minWidth: 80, textAlign: \"right\" }}>\n {formatCents(l.unitPriceCents * l.qty, settings.currency)}\n </Text>\n <button\n type=\"button\"\n aria-label={`Verwijder ${l.name}`}\n onClick={() => cartRemove(siteId, l)}\n style={{ background: \"none\", border: \"none\", cursor: \"pointer\", fontSize: \"1.1rem\", opacity: 0.6 }}\n >\n ✕\n </button>\n </div>\n </div>\n </Card>\n ))}\n\n <Card padding={3} variant=\"muted\">\n <div style={{ display: \"grid\", gap: 4, justifyContent: \"end\", textAlign: \"right\" }}>\n <Text size=\"sm\">{t(\"subtotal\")}: {formatCents(itemsCents, settings.currency)}</Text>\n <Text size=\"sm\">\n {t(\"shippingCosts\")}: {shippingCents === 0 ? t(\"free\") : formatCents(shippingCents, settings.currency)}\n {meerdereZones ? ` ${t(\"shippingFromCountry\")}` : \"\"}\n </Text>\n <Text weight=\"bold\">{t(\"total\")}: {formatCents(itemsCents + shippingCents, settings.currency)}</Text>\n </div>\n </Card>\n\n <div style={{ display: \"flex\", gap: 12, flexWrap: \"wrap\" }}>\n <a className=\"xicms-btn xicms-btn--primary\" href={settings.checkoutPath}>{t(\"checkout\")}</a>\n <a className=\"xicms-btn xicms-btn--secondary\" href={settings.shopPath}>{t(\"continueShopping\")}</a>\n </div>\n </div>\n )}\n </div>\n );\n}\n\nexport const cartBlock = defineBlock({\n type: \"cart\",\n Render: CartRender,\n label: \"Winkelwagen\",\n icon: \"🛒\",\n defaults: () => ({ heading: \"Winkelwagen\" }),\n});\n"],"mappings":"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","names":[]}
|
|
@@ -7,6 +7,7 @@ import { useBlockData } from "../../../blocks/context.js";
|
|
|
7
7
|
import { Card, Heading, Text } from "../../../blocks/ui/index.js";
|
|
8
8
|
import { Selector } from "@astryxdesign/core/Selector";
|
|
9
9
|
import { cartAdd } from "../cartStore.js";
|
|
10
|
+
import { AantalInvoer } from "./AantalInvoer.js";
|
|
10
11
|
import { useShopStrings } from "../strings.js";
|
|
11
12
|
import {
|
|
12
13
|
fetchActiveProducts,
|
|
@@ -279,18 +280,7 @@ function ProductDetailRender({ block, siteId }) {
|
|
|
279
280
|
),
|
|
280
281
|
/* @__PURE__ */ jsxs("label", { className: "xicms-shop-detail__qty", style: { display: "grid", gap: 4 }, children: [
|
|
281
282
|
/* @__PURE__ */ jsx("span", { style: { fontSize: "0.9rem" }, children: t("qty") }),
|
|
282
|
-
/* @__PURE__ */ jsx(
|
|
283
|
-
"input",
|
|
284
|
-
{
|
|
285
|
-
className: "xicms-input",
|
|
286
|
-
type: "number",
|
|
287
|
-
min: 1,
|
|
288
|
-
max: 999,
|
|
289
|
-
value: qty,
|
|
290
|
-
onChange: (e) => setQty(Math.max(1, Math.min(999, Number(e.target.value) || 1))),
|
|
291
|
-
style: { width: 90 }
|
|
292
|
-
}
|
|
293
|
-
)
|
|
283
|
+
/* @__PURE__ */ jsx(AantalInvoer, { value: qty, onChange: setQty, min: 1, max: 999, style: { width: 90 } })
|
|
294
284
|
] }),
|
|
295
285
|
outOfStock && /* @__PURE__ */ jsx(Text, { style: { color: "var(--color-error, #c0392b)" }, children: t("outOfStock") }),
|
|
296
286
|
variantInactive && /* @__PURE__ */ jsx(Text, { style: { color: "var(--color-error, #c0392b)" }, children: t("variantUnavailable") }),
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../src/modules/shop/blocks/ProductDetail.tsx"],"sourcesContent":["\"use client\";\n\nimport { useEffect, useMemo, useState } from \"react\";\nimport type { z } from \"zod\";\nimport { formatCents, productPriceCents, variantKeyFor } from \"../../../schema/shopGeld\";\nimport type { ProductDetailBlockSchema, PersonalizationField } from \"../../../schema/shop\";\nimport { defineBlock, type BlockRenderProps } from \"../../../blocks/registry\";\nimport { useBlockData } from \"../../../blocks/context\";\nimport { Card, Heading, Text } from \"../../../blocks/ui\";\nimport { Selector } from \"@astryxdesign/core/Selector\";\nimport { cartAdd } from \"../cartStore\";\nimport { useShopStrings } from \"../strings\";\nimport {\n fetchActiveProducts,\n fetchProductBySlug,\n kleurOptie,\n readQueryParam,\n uploadPersonalizationCall,\n useShopSettings,\n variantLabelFor,\n type StoredProduct,\n} from \"../lib\";\nimport type { ShopPageData } from \"../types\";\nimport { ProductCard } from \"./ProductGrid\";\n\ntype Blk = z.infer<typeof ProductDetailBlockSchema>;\ntype PersValue = Record<string, { text?: string; imagePath?: string; fileName?: string }>;\n\n/** Personalisatie-invoer (eigen tekst of afbeelding) onder de optie-selects. */\nfunction PersonalizationInputs({\n fields,\n siteId,\n productId,\n value,\n currency,\n onChange,\n}: {\n fields: PersonalizationField[];\n siteId: string;\n productId: string;\n value: PersValue;\n currency: string;\n onChange: (next: PersValue) => void;\n}) {\n const t = useShopStrings();\n const [uploading, setUploading] = useState<string | null>(null);\n const [error, setError] = useState<string | null>(null);\n\n return (\n <div className=\"xicms-shop-personalization\" style={{ display: \"grid\", gap: \"0.75rem\" }}>\n {fields.map((f) => {\n const v = value[f.key] ?? {};\n const label = (\n <span>\n {f.label}\n {f.required ? \" *\" : \"\"}\n {f.priceCents > 0 && (\n <span style={{ opacity: 0.7, marginLeft: 6, fontSize: \"0.9em\" }}>\n {t(\"personalizationSurcharge\", { bedrag: formatCents(f.priceCents, currency) })}\n </span>\n )}\n </span>\n );\n if (f.type === \"text\") {\n return (\n <label key={f.key} style={{ display: \"grid\", gap: 4 }}>\n <span style={{ fontSize: \"0.9rem\" }}>{label}</span>\n <input\n className=\"xicms-input\"\n type=\"text\"\n maxLength={f.maxLength}\n value={v.text ?? \"\"}\n onChange={(e) => onChange({ ...value, [f.key]: { text: e.target.value } })}\n />\n <span style={{ fontSize: \"0.75rem\", opacity: 0.6 }}>\n {f.helpText ? `${f.helpText} · ` : \"\"}\n {(v.text ?? \"\").length}/{f.maxLength}\n </span>\n </label>\n );\n }\n return (\n <div key={f.key} style={{ display: \"grid\", gap: 4 }}>\n <span style={{ fontSize: \"0.9rem\" }}>{label}</span>\n <div style={{ display: \"flex\", alignItems: \"center\", gap: 10, flexWrap: \"wrap\" }}>\n <label className=\"xicms-btn xicms-btn--secondary\" style={{ cursor: \"pointer\" }}>\n {uploading === f.key\n ? t(\"personalizationUploading\")\n : v.imagePath\n ? t(\"personalizationUploaded\")\n : t(\"personalizationUpload\")}\n <input\n type=\"file\"\n accept=\"image/jpeg,image/png,image/webp\"\n style={{ display: \"none\" }}\n onChange={async (e) => {\n const file = e.target.files?.[0];\n if (!file) return;\n setError(null);\n setUploading(f.key);\n try {\n const { path } = await uploadPersonalizationCall(siteId, productId, file);\n onChange({ ...value, [f.key]: { imagePath: path, fileName: file.name } });\n } catch (err) {\n setError(err instanceof Error && err.message === \"TOO_BIG\" ? t(\"personalizationTooBig\") : t(\"personalizationFailed\"));\n } finally {\n setUploading(null);\n }\n }}\n />\n </label>\n {v.fileName && <span style={{ fontSize: \"0.85rem\", opacity: 0.75 }}>{v.fileName}</span>}\n </div>\n {f.helpText && <span style={{ fontSize: \"0.75rem\", opacity: 0.6 }}>{f.helpText}</span>}\n </div>\n );\n })}\n {error && <Text role=\"alert\" style={{ color: \"var(--color-error, #c0392b)\" }}>{error}</Text>}\n </div>\n );\n}\n\n/**\n * Productdetail — één generieke pagina met dit block toont elk product via `?p=<slug>`\n * (of een vast product via `block.slug`). Optie-selects met per-kleur-beeldwissel,\n * personalisatie (eigen naam/logo), voorraad-signalen en \"Ook leuk\"-rij.\n */\nfunction ProductDetailRender({ block, siteId }: BlockRenderProps<Blk>) {\n const settings = useShopSettings();\n const t = useShopStrings();\n const serverData = useBlockData<ShopPageData>(\"shop\");\n const [product, setProduct] = useState<StoredProduct | null | undefined>(\n serverData && \"product\" in serverData ? serverData.product : undefined,\n );\n const [related, setRelated] = useState<StoredProduct[]>([]);\n const [choices, setChoices] = useState<Record<number, string>>({});\n const [personalization, setPersonalization] = useState<PersValue>({});\n const [qty, setQty] = useState(1);\n const [manualImage, setManualImage] = useState<string | null>(null);\n const [added, setAdded] = useState(false);\n\n useEffect(() => {\n if (serverData && \"product\" in serverData) return; // SSR-data aanwezig\n const slug = block.slug || readQueryParam(\"p\") || \"\";\n if (!slug) {\n setProduct(null);\n return;\n }\n void fetchProductBySlug(siteId, slug).then(setProduct).catch((e) => {\n console.warn(\"[xicms] product laden mislukt:\", e);\n setProduct(null);\n });\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [siteId, block.slug]);\n\n // \"Ook leuk\": producten uit dezelfde categorie (server-set hergebruiken waar mogelijk).\n useEffect(() => {\n if (!block.showRelated || !product) return;\n const pick = (all: StoredProduct[]) =>\n setRelated(\n all\n .filter((x) => x.id !== product.id && x.categoryIds?.some((c) => product.categoryIds?.includes(c)))\n .slice(0, 4),\n );\n if (serverData?.products) pick(serverData.products);\n else void fetchActiveProducts(siteId).then(pick).catch(() => setRelated([]));\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [product?.id, block.showRelated, siteId]);\n\n const options = product?.options ?? [];\n // Uitgeschakeld op het product? Dan tonen we de velden niet en rekent de server ze ook niet\n // (zie resolvePersonalization) — de configuratie blijft wel bewaard voor later.\n const persFields = product?.personalizationEnabled === false ? [] : (product?.personalization ?? []);\n const allChosen = options.every((_, i) => Boolean(choices[i]));\n const variantKey = product && options.length > 0 && allChosen\n ? variantKeyFor(options.map((_, i) => choices[i]))\n : undefined;\n const variant = variantKey ? product?.variants?.find((v) => v.key === variantKey) : undefined;\n\n /** Meerprijs van ingevulde personalisatie (per stuk). */\n const persSurcharge = useMemo(\n () =>\n persFields.reduce((sum, f) => {\n const v = personalization[f.key];\n return v && (v.text || v.imagePath) ? sum + f.priceCents : sum;\n }, 0),\n [persFields, personalization],\n );\n\n const priceCents = useMemo(() => {\n if (!product) return 0;\n return productPriceCents(product, variantKey) + persSurcharge;\n }, [product, variantKey, persSurcharge]);\n\n /** Hoofdbeeld: handmatige thumbnail-keuze > per-kleur-afbeelding > eerste productfoto. */\n const kleur = product ? kleurOptie(product) : null;\n const kleurIdx = kleur && product ? (product.options ?? []).findIndex((o) => o === kleur) : -1;\n const gekozenKleur = kleurIdx >= 0 ? choices[kleurIdx] : undefined;\n const kleurImage = gekozenKleur ? kleur?.images?.[gekozenKleur] : undefined;\n const mainImage = manualImage ?? kleurImage ?? product?.images?.[0];\n\n /** Alle toonbare afbeeldingen: productfoto's + unieke per-kleur-afbeeldingen. */\n const gallery = useMemo(() => {\n if (!product) return [] as { url: string; label?: string }[];\n const out: { url: string; label?: string }[] = (product.images ?? []).map((url) => ({ url }));\n for (const [value, url] of Object.entries(kleur?.images ?? {})) {\n if (!out.some((g) => g.url === url)) out.push({ url, label: value });\n }\n return out;\n }, [product, kleur]);\n\n const stockValue = options.length === 0 ? product?.stock : variant?.stock;\n const outOfStock = Boolean(product?.trackStock && stockValue !== undefined && stockValue <= 0);\n const lowStock = Boolean(product?.trackStock && stockValue !== undefined && stockValue > 0 && stockValue <= 5);\n const variantInactive = variant !== undefined && variant.active === false;\n\n if (product === undefined) return null;\n if (product === null) {\n return (\n <Card padding={4} variant=\"muted\">\n <Text>\n {t(\"productNotFound\")} <a href={settings.shopPath}>{t(\"backToShop\")}</a>\n </Text>\n </Card>\n );\n }\n\n const persComplete = persFields.every((f) => {\n if (!f.required) return true;\n const v = personalization[f.key];\n return Boolean(v && (v.text?.trim() || v.imagePath));\n });\n\n function addToCart() {\n if (!product) return;\n const filled: PersValue = {};\n for (const f of persFields) {\n const v = personalization[f.key];\n if (v && (v.text?.trim() || v.imagePath)) filled[f.key] = v;\n }\n const persLabel = persFields\n .filter((f) => filled[f.key])\n .map((f) => `${f.label}: ${filled[f.key].text ?? filled[f.key].fileName ?? \"✓\"}`)\n .join(\" · \");\n cartAdd(\n siteId,\n {\n productId: product.id,\n slug: product.slug,\n name: product.name,\n variantKey,\n variantLabel: variantKey ? variantLabelFor(options, variantKey) : undefined,\n unitPriceCents: priceCents,\n imageUrl: mainImage ?? product.images?.[0],\n personalization: Object.keys(filled).length > 0 ? filled : undefined,\n personalizationLabel: persLabel || undefined,\n },\n qty,\n );\n setAdded(true);\n }\n\n const canOrder = (options.length === 0 || allChosen) && !outOfStock && !variantInactive && persComplete;\n\n return (\n <>\n <div\n className=\"xicms-shop-detail\"\n style={{ display: \"grid\", gap: \"2rem\", gridTemplateColumns: \"repeat(auto-fit, minmax(min(320px, 100%), 1fr))\" }}\n >\n <div className=\"xicms-shop-detail__media\">\n {mainImage ? (\n <img\n src={mainImage}\n alt={product.name}\n style={{ width: \"100%\", aspectRatio: \"1/1\", objectFit: \"cover\", borderRadius: \"var(--xicms-radius, 12px)\" }}\n />\n ) : (\n <div\n className=\"xicms-shop-detail__placeholder\"\n aria-hidden=\"true\"\n style={{ width: \"100%\", aspectRatio: \"1/1\", background: \"var(--xicms-muted, #f1f1f1)\", borderRadius: \"var(--xicms-radius, 12px)\" }}\n />\n )}\n {gallery.length > 1 && (\n <div style={{ display: \"flex\", gap: 8, marginTop: 8, flexWrap: \"wrap\" }}>\n {gallery.map((g) => (\n <button\n key={g.url}\n type=\"button\"\n title={g.label}\n onClick={() => setManualImage(g.url)}\n style={{\n padding: 0,\n border: g.url === mainImage ? \"2px solid var(--xicms-primary, #333)\" : \"2px solid transparent\",\n borderRadius: 8,\n cursor: \"pointer\",\n background: \"none\",\n }}\n >\n <img src={g.url} alt={g.label ?? \"\"} style={{ width: 64, height: 64, objectFit: \"cover\", borderRadius: 6, display: \"block\" }} />\n </button>\n ))}\n </div>\n )}\n </div>\n\n {/*\n Haken voor de bewerkmodus. De tekst van een product staat NIET in dit blok — het blok wijst\n alleen een product aan (`slug`) en haalt de rest uit de productencollectie. Zonder deze\n haken kon de editor daarom niets: het tandwiel opende een paneel met twee instellingen en\n het potlood verscheen helemaal niet, want er viel in dit blok niets te bewerken. Terecht,\n en toch precies verkeerd — je stond naar een productnaam te kijken die je niet kon wijzigen.\n `data-xicms-product` zegt om welk product het gaat, `data-xicms-product-edit` welk veld\n eronder ligt; de editor schrijft die rechtstreeks naar het productdocument.\n */}\n <div className=\"xicms-shop-detail__info\" data-xicms-product={product.id}>\n <Heading level={1} data-xicms-product-edit=\"name\">{product.name}</Heading>\n <p className=\"xicms-shop-detail__price\" style={{ fontSize: \"1.35rem\", fontWeight: 700, margin: \"0.5rem 0 1rem\" }}>\n {product.compareAtCents !== undefined && product.compareAtCents > priceCents && (\n <s style={{ opacity: 0.6, marginRight: 8, fontWeight: \"normal\" }}>\n {formatCents(product.compareAtCents, settings.currency)}\n </s>\n )}\n {formatCents(priceCents, settings.currency)}\n </p>\n\n {product.description && (\n <p className=\"xicms-shop-detail__desc\" data-xicms-product-edit=\"description\" style={{ whiteSpace: \"pre-wrap\", margin: 0 }}>{product.description}</p>\n )}\n\n <div style={{ display: \"grid\", gap: \"0.75rem\", maxWidth: 420, marginTop: \"1rem\" }}>\n {options.map((opt, i) => (\n <Selector\n key={opt.name}\n label={opt.name}\n placeholder={t(\"choose\")}\n options={opt.values}\n value={choices[i] ?? \"\"}\n onChange={(v) => {\n setChoices((prev) => ({ ...prev, [i]: (v as string) ?? \"\" }));\n setManualImage(null);\n setAdded(false);\n }}\n />\n ))}\n\n {persFields.length > 0 && (\n <PersonalizationInputs\n fields={persFields}\n siteId={siteId}\n productId={product.id}\n value={personalization}\n currency={settings.currency}\n onChange={(v) => {\n setPersonalization(v);\n setAdded(false);\n }}\n />\n )}\n\n <label className=\"xicms-shop-detail__qty\" style={{ display: \"grid\", gap: 4 }}>\n <span style={{ fontSize: \"0.9rem\" }}>{t(\"qty\")}</span>\n <input\n className=\"xicms-input\"\n type=\"number\"\n min={1}\n max={999}\n value={qty}\n onChange={(e) => setQty(Math.max(1, Math.min(999, Number(e.target.value) || 1)))}\n style={{ width: 90 }}\n />\n </label>\n\n {outOfStock && <Text style={{ color: \"var(--color-error, #c0392b)\" }}>{t(\"outOfStock\")}</Text>}\n {variantInactive && <Text style={{ color: \"var(--color-error, #c0392b)\" }}>{t(\"variantUnavailable\")}</Text>}\n {lowStock && !outOfStock && (\n <Text style={{ color: \"var(--xicms-primary, #4d7fb0)\", fontWeight: 600 }}>\n {t(\"stockLeft\", { n: stockValue as number })}\n </Text>\n )}\n\n {added ? (\n <div role=\"status\" style={{ display: \"grid\", gap: 8 }}>\n <Text>{t(\"added\")}</Text>\n <div style={{ display: \"flex\", gap: 12, flexWrap: \"wrap\" }}>\n <a className=\"xicms-btn xicms-btn--primary\" href={settings.cartPath}>{t(\"toCart\")}</a>\n <a className=\"xicms-btn xicms-btn--secondary\" href={settings.shopPath}>{t(\"continueShopping\")}</a>\n </div>\n </div>\n ) : (\n <button\n type=\"button\"\n className=\"xicms-btn xicms-btn--primary\"\n disabled={!canOrder}\n onClick={addToCart}\n style={{ cursor: canOrder ? \"pointer\" : \"not-allowed\", opacity: canOrder ? 1 : 0.5, width: \"fit-content\" }}\n >\n {t(\"addToCart\")}\n </button>\n )}\n </div>\n </div>\n </div>\n\n {block.showRelated && related.length > 0 && (\n <div className=\"xicms-shop-related\" style={{ marginTop: \"3rem\" }}>\n <Heading level={2}>{t(\"related\")}</Heading>\n <div\n className=\"xicms-shop-grid__items\"\n style={{\n display: \"grid\",\n gridTemplateColumns: \"repeat(auto-fill, minmax(min(200px, calc(50% - 1rem)), 1fr))\",\n gap: \"1.25rem\",\n marginTop: \"1rem\",\n }}\n >\n {related.map((p) => (\n <ProductCard\n key={p.id}\n product={p}\n siteId={siteId}\n currency={settings.currency}\n detailPath={settings.detailPath}\n cartPath={settings.cartPath}\n t={t}\n />\n ))}\n </div>\n </div>\n )}\n </>\n );\n}\n\nexport const productDetailBlock = defineBlock({\n type: \"productDetail\",\n Render: ProductDetailRender,\n label: \"Productdetail\",\n icon: \"🏷️\",\n defaults: () => ({ showRelated: true }),\n});\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../../../src/modules/shop/blocks/ProductDetail.tsx"],"sourcesContent":["\"use client\";\n\nimport { useEffect, useMemo, useState } from \"react\";\nimport type { z } from \"zod\";\nimport { formatCents, productPriceCents, variantKeyFor } from \"../../../schema/shopGeld\";\nimport type { ProductDetailBlockSchema, PersonalizationField } from \"../../../schema/shop\";\nimport { defineBlock, type BlockRenderProps } from \"../../../blocks/registry\";\nimport { useBlockData } from \"../../../blocks/context\";\nimport { Card, Heading, Text } from \"../../../blocks/ui\";\nimport { Selector } from \"@astryxdesign/core/Selector\";\nimport { cartAdd } from \"../cartStore\";\nimport { AantalInvoer } from \"./AantalInvoer\";\nimport { useShopStrings } from \"../strings\";\nimport {\n fetchActiveProducts,\n fetchProductBySlug,\n kleurOptie,\n readQueryParam,\n uploadPersonalizationCall,\n useShopSettings,\n variantLabelFor,\n type StoredProduct,\n} from \"../lib\";\nimport type { ShopPageData } from \"../types\";\nimport { ProductCard } from \"./ProductGrid\";\n\ntype Blk = z.infer<typeof ProductDetailBlockSchema>;\ntype PersValue = Record<string, { text?: string; imagePath?: string; fileName?: string }>;\n\n/** Personalisatie-invoer (eigen tekst of afbeelding) onder de optie-selects. */\nfunction PersonalizationInputs({\n fields,\n siteId,\n productId,\n value,\n currency,\n onChange,\n}: {\n fields: PersonalizationField[];\n siteId: string;\n productId: string;\n value: PersValue;\n currency: string;\n onChange: (next: PersValue) => void;\n}) {\n const t = useShopStrings();\n const [uploading, setUploading] = useState<string | null>(null);\n const [error, setError] = useState<string | null>(null);\n\n return (\n <div className=\"xicms-shop-personalization\" style={{ display: \"grid\", gap: \"0.75rem\" }}>\n {fields.map((f) => {\n const v = value[f.key] ?? {};\n const label = (\n <span>\n {f.label}\n {f.required ? \" *\" : \"\"}\n {f.priceCents > 0 && (\n <span style={{ opacity: 0.7, marginLeft: 6, fontSize: \"0.9em\" }}>\n {t(\"personalizationSurcharge\", { bedrag: formatCents(f.priceCents, currency) })}\n </span>\n )}\n </span>\n );\n if (f.type === \"text\") {\n return (\n <label key={f.key} style={{ display: \"grid\", gap: 4 }}>\n <span style={{ fontSize: \"0.9rem\" }}>{label}</span>\n <input\n className=\"xicms-input\"\n type=\"text\"\n maxLength={f.maxLength}\n value={v.text ?? \"\"}\n onChange={(e) => onChange({ ...value, [f.key]: { text: e.target.value } })}\n />\n <span style={{ fontSize: \"0.75rem\", opacity: 0.6 }}>\n {f.helpText ? `${f.helpText} · ` : \"\"}\n {(v.text ?? \"\").length}/{f.maxLength}\n </span>\n </label>\n );\n }\n return (\n <div key={f.key} style={{ display: \"grid\", gap: 4 }}>\n <span style={{ fontSize: \"0.9rem\" }}>{label}</span>\n <div style={{ display: \"flex\", alignItems: \"center\", gap: 10, flexWrap: \"wrap\" }}>\n <label className=\"xicms-btn xicms-btn--secondary\" style={{ cursor: \"pointer\" }}>\n {uploading === f.key\n ? t(\"personalizationUploading\")\n : v.imagePath\n ? t(\"personalizationUploaded\")\n : t(\"personalizationUpload\")}\n <input\n type=\"file\"\n accept=\"image/jpeg,image/png,image/webp\"\n style={{ display: \"none\" }}\n onChange={async (e) => {\n const file = e.target.files?.[0];\n if (!file) return;\n setError(null);\n setUploading(f.key);\n try {\n const { path } = await uploadPersonalizationCall(siteId, productId, file);\n onChange({ ...value, [f.key]: { imagePath: path, fileName: file.name } });\n } catch (err) {\n setError(err instanceof Error && err.message === \"TOO_BIG\" ? t(\"personalizationTooBig\") : t(\"personalizationFailed\"));\n } finally {\n setUploading(null);\n }\n }}\n />\n </label>\n {v.fileName && <span style={{ fontSize: \"0.85rem\", opacity: 0.75 }}>{v.fileName}</span>}\n </div>\n {f.helpText && <span style={{ fontSize: \"0.75rem\", opacity: 0.6 }}>{f.helpText}</span>}\n </div>\n );\n })}\n {error && <Text role=\"alert\" style={{ color: \"var(--color-error, #c0392b)\" }}>{error}</Text>}\n </div>\n );\n}\n\n/**\n * Productdetail — één generieke pagina met dit block toont elk product via `?p=<slug>`\n * (of een vast product via `block.slug`). Optie-selects met per-kleur-beeldwissel,\n * personalisatie (eigen naam/logo), voorraad-signalen en \"Ook leuk\"-rij.\n */\nfunction ProductDetailRender({ block, siteId }: BlockRenderProps<Blk>) {\n const settings = useShopSettings();\n const t = useShopStrings();\n const serverData = useBlockData<ShopPageData>(\"shop\");\n const [product, setProduct] = useState<StoredProduct | null | undefined>(\n serverData && \"product\" in serverData ? serverData.product : undefined,\n );\n const [related, setRelated] = useState<StoredProduct[]>([]);\n const [choices, setChoices] = useState<Record<number, string>>({});\n const [personalization, setPersonalization] = useState<PersValue>({});\n const [qty, setQty] = useState(1);\n const [manualImage, setManualImage] = useState<string | null>(null);\n const [added, setAdded] = useState(false);\n\n useEffect(() => {\n if (serverData && \"product\" in serverData) return; // SSR-data aanwezig\n const slug = block.slug || readQueryParam(\"p\") || \"\";\n if (!slug) {\n setProduct(null);\n return;\n }\n void fetchProductBySlug(siteId, slug).then(setProduct).catch((e) => {\n console.warn(\"[xicms] product laden mislukt:\", e);\n setProduct(null);\n });\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [siteId, block.slug]);\n\n // \"Ook leuk\": producten uit dezelfde categorie (server-set hergebruiken waar mogelijk).\n useEffect(() => {\n if (!block.showRelated || !product) return;\n const pick = (all: StoredProduct[]) =>\n setRelated(\n all\n .filter((x) => x.id !== product.id && x.categoryIds?.some((c) => product.categoryIds?.includes(c)))\n .slice(0, 4),\n );\n if (serverData?.products) pick(serverData.products);\n else void fetchActiveProducts(siteId).then(pick).catch(() => setRelated([]));\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [product?.id, block.showRelated, siteId]);\n\n const options = product?.options ?? [];\n // Uitgeschakeld op het product? Dan tonen we de velden niet en rekent de server ze ook niet\n // (zie resolvePersonalization) — de configuratie blijft wel bewaard voor later.\n const persFields = product?.personalizationEnabled === false ? [] : (product?.personalization ?? []);\n const allChosen = options.every((_, i) => Boolean(choices[i]));\n const variantKey = product && options.length > 0 && allChosen\n ? variantKeyFor(options.map((_, i) => choices[i]))\n : undefined;\n const variant = variantKey ? product?.variants?.find((v) => v.key === variantKey) : undefined;\n\n /** Meerprijs van ingevulde personalisatie (per stuk). */\n const persSurcharge = useMemo(\n () =>\n persFields.reduce((sum, f) => {\n const v = personalization[f.key];\n return v && (v.text || v.imagePath) ? sum + f.priceCents : sum;\n }, 0),\n [persFields, personalization],\n );\n\n const priceCents = useMemo(() => {\n if (!product) return 0;\n return productPriceCents(product, variantKey) + persSurcharge;\n }, [product, variantKey, persSurcharge]);\n\n /** Hoofdbeeld: handmatige thumbnail-keuze > per-kleur-afbeelding > eerste productfoto. */\n const kleur = product ? kleurOptie(product) : null;\n const kleurIdx = kleur && product ? (product.options ?? []).findIndex((o) => o === kleur) : -1;\n const gekozenKleur = kleurIdx >= 0 ? choices[kleurIdx] : undefined;\n const kleurImage = gekozenKleur ? kleur?.images?.[gekozenKleur] : undefined;\n const mainImage = manualImage ?? kleurImage ?? product?.images?.[0];\n\n /** Alle toonbare afbeeldingen: productfoto's + unieke per-kleur-afbeeldingen. */\n const gallery = useMemo(() => {\n if (!product) return [] as { url: string; label?: string }[];\n const out: { url: string; label?: string }[] = (product.images ?? []).map((url) => ({ url }));\n for (const [value, url] of Object.entries(kleur?.images ?? {})) {\n if (!out.some((g) => g.url === url)) out.push({ url, label: value });\n }\n return out;\n }, [product, kleur]);\n\n const stockValue = options.length === 0 ? product?.stock : variant?.stock;\n const outOfStock = Boolean(product?.trackStock && stockValue !== undefined && stockValue <= 0);\n const lowStock = Boolean(product?.trackStock && stockValue !== undefined && stockValue > 0 && stockValue <= 5);\n const variantInactive = variant !== undefined && variant.active === false;\n\n if (product === undefined) return null;\n if (product === null) {\n return (\n <Card padding={4} variant=\"muted\">\n <Text>\n {t(\"productNotFound\")} <a href={settings.shopPath}>{t(\"backToShop\")}</a>\n </Text>\n </Card>\n );\n }\n\n const persComplete = persFields.every((f) => {\n if (!f.required) return true;\n const v = personalization[f.key];\n return Boolean(v && (v.text?.trim() || v.imagePath));\n });\n\n function addToCart() {\n if (!product) return;\n const filled: PersValue = {};\n for (const f of persFields) {\n const v = personalization[f.key];\n if (v && (v.text?.trim() || v.imagePath)) filled[f.key] = v;\n }\n const persLabel = persFields\n .filter((f) => filled[f.key])\n .map((f) => `${f.label}: ${filled[f.key].text ?? filled[f.key].fileName ?? \"✓\"}`)\n .join(\" · \");\n cartAdd(\n siteId,\n {\n productId: product.id,\n slug: product.slug,\n name: product.name,\n variantKey,\n variantLabel: variantKey ? variantLabelFor(options, variantKey) : undefined,\n unitPriceCents: priceCents,\n imageUrl: mainImage ?? product.images?.[0],\n personalization: Object.keys(filled).length > 0 ? filled : undefined,\n personalizationLabel: persLabel || undefined,\n },\n qty,\n );\n setAdded(true);\n }\n\n const canOrder = (options.length === 0 || allChosen) && !outOfStock && !variantInactive && persComplete;\n\n return (\n <>\n <div\n className=\"xicms-shop-detail\"\n style={{ display: \"grid\", gap: \"2rem\", gridTemplateColumns: \"repeat(auto-fit, minmax(min(320px, 100%), 1fr))\" }}\n >\n <div className=\"xicms-shop-detail__media\">\n {mainImage ? (\n <img\n src={mainImage}\n alt={product.name}\n style={{ width: \"100%\", aspectRatio: \"1/1\", objectFit: \"cover\", borderRadius: \"var(--xicms-radius, 12px)\" }}\n />\n ) : (\n <div\n className=\"xicms-shop-detail__placeholder\"\n aria-hidden=\"true\"\n style={{ width: \"100%\", aspectRatio: \"1/1\", background: \"var(--xicms-muted, #f1f1f1)\", borderRadius: \"var(--xicms-radius, 12px)\" }}\n />\n )}\n {gallery.length > 1 && (\n <div style={{ display: \"flex\", gap: 8, marginTop: 8, flexWrap: \"wrap\" }}>\n {gallery.map((g) => (\n <button\n key={g.url}\n type=\"button\"\n title={g.label}\n onClick={() => setManualImage(g.url)}\n style={{\n padding: 0,\n border: g.url === mainImage ? \"2px solid var(--xicms-primary, #333)\" : \"2px solid transparent\",\n borderRadius: 8,\n cursor: \"pointer\",\n background: \"none\",\n }}\n >\n <img src={g.url} alt={g.label ?? \"\"} style={{ width: 64, height: 64, objectFit: \"cover\", borderRadius: 6, display: \"block\" }} />\n </button>\n ))}\n </div>\n )}\n </div>\n\n {/*\n Haken voor de bewerkmodus. De tekst van een product staat NIET in dit blok — het blok wijst\n alleen een product aan (`slug`) en haalt de rest uit de productencollectie. Zonder deze\n haken kon de editor daarom niets: het tandwiel opende een paneel met twee instellingen en\n het potlood verscheen helemaal niet, want er viel in dit blok niets te bewerken. Terecht,\n en toch precies verkeerd — je stond naar een productnaam te kijken die je niet kon wijzigen.\n `data-xicms-product` zegt om welk product het gaat, `data-xicms-product-edit` welk veld\n eronder ligt; de editor schrijft die rechtstreeks naar het productdocument.\n */}\n <div className=\"xicms-shop-detail__info\" data-xicms-product={product.id}>\n <Heading level={1} data-xicms-product-edit=\"name\">{product.name}</Heading>\n <p className=\"xicms-shop-detail__price\" style={{ fontSize: \"1.35rem\", fontWeight: 700, margin: \"0.5rem 0 1rem\" }}>\n {product.compareAtCents !== undefined && product.compareAtCents > priceCents && (\n <s style={{ opacity: 0.6, marginRight: 8, fontWeight: \"normal\" }}>\n {formatCents(product.compareAtCents, settings.currency)}\n </s>\n )}\n {formatCents(priceCents, settings.currency)}\n </p>\n\n {product.description && (\n <p className=\"xicms-shop-detail__desc\" data-xicms-product-edit=\"description\" style={{ whiteSpace: \"pre-wrap\", margin: 0 }}>{product.description}</p>\n )}\n\n <div style={{ display: \"grid\", gap: \"0.75rem\", maxWidth: 420, marginTop: \"1rem\" }}>\n {options.map((opt, i) => (\n <Selector\n key={opt.name}\n label={opt.name}\n placeholder={t(\"choose\")}\n options={opt.values}\n value={choices[i] ?? \"\"}\n onChange={(v) => {\n setChoices((prev) => ({ ...prev, [i]: (v as string) ?? \"\" }));\n setManualImage(null);\n setAdded(false);\n }}\n />\n ))}\n\n {persFields.length > 0 && (\n <PersonalizationInputs\n fields={persFields}\n siteId={siteId}\n productId={product.id}\n value={personalization}\n currency={settings.currency}\n onChange={(v) => {\n setPersonalization(v);\n setAdded(false);\n }}\n />\n )}\n\n <label className=\"xicms-shop-detail__qty\" style={{ display: \"grid\", gap: 4 }}>\n <span style={{ fontSize: \"0.9rem\" }}>{t(\"qty\")}</span>\n <AantalInvoer value={qty} onChange={setQty} min={1} max={999} style={{ width: 90 }} />\n </label>\n\n {outOfStock && <Text style={{ color: \"var(--color-error, #c0392b)\" }}>{t(\"outOfStock\")}</Text>}\n {variantInactive && <Text style={{ color: \"var(--color-error, #c0392b)\" }}>{t(\"variantUnavailable\")}</Text>}\n {lowStock && !outOfStock && (\n <Text style={{ color: \"var(--xicms-primary, #4d7fb0)\", fontWeight: 600 }}>\n {t(\"stockLeft\", { n: stockValue as number })}\n </Text>\n )}\n\n {added ? (\n <div role=\"status\" style={{ display: \"grid\", gap: 8 }}>\n <Text>{t(\"added\")}</Text>\n <div style={{ display: \"flex\", gap: 12, flexWrap: \"wrap\" }}>\n <a className=\"xicms-btn xicms-btn--primary\" href={settings.cartPath}>{t(\"toCart\")}</a>\n <a className=\"xicms-btn xicms-btn--secondary\" href={settings.shopPath}>{t(\"continueShopping\")}</a>\n </div>\n </div>\n ) : (\n <button\n type=\"button\"\n className=\"xicms-btn xicms-btn--primary\"\n disabled={!canOrder}\n onClick={addToCart}\n style={{ cursor: canOrder ? \"pointer\" : \"not-allowed\", opacity: canOrder ? 1 : 0.5, width: \"fit-content\" }}\n >\n {t(\"addToCart\")}\n </button>\n )}\n </div>\n </div>\n </div>\n\n {block.showRelated && related.length > 0 && (\n <div className=\"xicms-shop-related\" style={{ marginTop: \"3rem\" }}>\n <Heading level={2}>{t(\"related\")}</Heading>\n <div\n className=\"xicms-shop-grid__items\"\n style={{\n display: \"grid\",\n gridTemplateColumns: \"repeat(auto-fill, minmax(min(200px, calc(50% - 1rem)), 1fr))\",\n gap: \"1.25rem\",\n marginTop: \"1rem\",\n }}\n >\n {related.map((p) => (\n <ProductCard\n key={p.id}\n product={p}\n siteId={siteId}\n currency={settings.currency}\n detailPath={settings.detailPath}\n cartPath={settings.cartPath}\n t={t}\n />\n ))}\n </div>\n </div>\n )}\n </>\n );\n}\n\nexport const productDetailBlock = defineBlock({\n type: \"productDetail\",\n Render: ProductDetailRender,\n label: \"Productdetail\",\n icon: \"🏷️\",\n defaults: () => ({ showRelated: true }),\n});\n"],"mappings":"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","names":[]}
|
package/dist/version.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
const CORE_VERSION = true ? "7.22.
|
|
1
|
+
const CORE_VERSION = true ? "7.22.2" : "0.0.0-dev";
|
|
2
2
|
const CORE_PACKAGE = "@xiwebdesign/xicms-core";
|
|
3
3
|
function isNieuwer(beschikbaar, huidig) {
|
|
4
4
|
const delen = (v) => v.split("-")[0].split(".").map((n) => Number.parseInt(n, 10) || 0);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xiwebdesign/xicms-core",
|
|
3
|
-
"version": "7.22.
|
|
3
|
+
"version": "7.22.2",
|
|
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": {
|