@xiwebdesign/xicms-core 7.16.1 → 7.16.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.
|
@@ -62,8 +62,12 @@ function ProductsTab({ siteId }) {
|
|
|
62
62
|
order: products.length,
|
|
63
63
|
featured: false
|
|
64
64
|
};
|
|
65
|
-
|
|
66
|
-
|
|
65
|
+
try {
|
|
66
|
+
const id = await saveProductDoc(siteId, void 0, nieuw);
|
|
67
|
+
setSelected(id);
|
|
68
|
+
} catch (e) {
|
|
69
|
+
toast({ title: "Product aanmaken mislukt", description: e instanceof Error ? e.message : void 0, tone: "error" });
|
|
70
|
+
}
|
|
67
71
|
}
|
|
68
72
|
const filtered = useMemo(() => {
|
|
69
73
|
const needle = q.trim().toLowerCase();
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/admin/shop/ProductsTab.tsx"],"sourcesContent":["\"use client\";\n\nimport { useEffect, useMemo, useState } from \"react\";\nimport { collection, doc, onSnapshot } from \"firebase/firestore\";\nimport { removeProduct, saveProductDoc } from \"./productClient\";\nimport { getDb } from \"../../firebase/client\";\nimport {\n ProductSchema,\n formatCents,\n type PersonalizationField,\n type Product,\n type ProductOption,\n type ProductVariant,\n type ShopCategory,\n} from \"../../schema/shop\";\nimport { Badge, Button, Card, Field, Input, Textarea } from \"../ui/primitives\";\nimport { Select } from \"../ui/select\";\nimport { Switch } from \"../ui/switch\";\nimport { useToast } from \"../ui/toast\";\nimport { useConfirm, type ConfirmOptions } from \"../ui/ConfirmDialog\";\nimport { MediaPickerButton } from \"../media/MediaPicker\";\nimport { slugify } from \"../lib/slugify\";\nimport { allVariantKeys, centsToEuroInput, eurosToCents } from \"./lib\";\n\ntype StoredProduct = Product & { id: string };\ntype StoredCategory = ShopCategory & { id: string };\n\nconst STATUS_OPTIONS = [\n { value: \"draft\", label: \"Concept\" },\n { value: \"active\", label: \"Actief\" },\n { value: \"archived\", label: \"Gearchiveerd\" },\n];\n\nexport function ProductsTab({ siteId }: { siteId: string }) {\n const { toast } = useToast();\n const confirm = useConfirm();\n const [products, setProducts] = useState<StoredProduct[]>([]);\n const [cats, setCats] = useState<StoredCategory[]>([]);\n const [selected, setSelected] = useState<string | null>(null);\n const [q, setQ] = useState(\"\");\n\n useEffect(() => {\n const db = getDb();\n if (!db) return;\n const un1 = onSnapshot(collection(db, \"sites\", siteId, \"products\"), (snap) =>\n setProducts(\n snap.docs\n .map((d) => ({ id: d.id, ...(d.data() as Product) }))\n .sort((a, b) => (a.order ?? 0) - (b.order ?? 0) || a.name.localeCompare(b.name, \"nl\")),\n ),\n );\n const un2 = onSnapshot(collection(db, \"sites\", siteId, \"shopCategories\"), (snap) =>\n setCats(snap.docs.map((d) => ({ id: d.id, ...(d.data() as ShopCategory) }))),\n );\n return () => {\n un1();\n un2();\n };\n }, [siteId]);\n\n async function createProduct() {\n // Ook aanmaken loopt via de callable: een vers product is een draft (onzichtbaar voor de\n // cache), maar zo staat élke productmutatie in de audit-log en is er één pad i.p.v. twee.\n const nieuw = {\n name: \"Nieuw product\",\n slug: `product-${products.length + 1}`,\n description: \"\",\n categoryIds: [],\n images: [],\n priceCents: 0,\n vatRate: 21,\n options: [],\n variants: [],\n trackStock: false,\n status: \"draft\",\n order: products.length,\n featured: false,\n } satisfies Product;\n const id = await saveProductDoc(siteId, undefined, nieuw);\n setSelected(id);\n }\n\n const filtered = useMemo(() => {\n const needle = q.trim().toLowerCase();\n if (!needle) return products;\n return products.filter((p) => p.name.toLowerCase().includes(needle) || p.slug.includes(needle));\n }, [products, q]);\n\n const current = products.find((p) => p.id === selected) ?? null;\n\n return (\n <div className=\"grid gap-4 md:grid-cols-[300px_1fr]\">\n <Card className=\"h-fit\">\n <div className=\"mb-3 flex items-center justify-between\">\n <h3 className=\"text-sm font-semibold\">Producten</h3>\n <Button size=\"sm\" onClick={createProduct}>+ Nieuw</Button>\n </div>\n <Input placeholder=\"Zoeken…\" value={q} onChange={(e) => setQ(e.target.value)} className=\"mb-2\" />\n <ul className=\"grid max-h-[60vh] gap-1 overflow-y-auto\">\n {filtered.map((p) => (\n <li key={p.id}>\n <button\n onClick={() => setSelected(p.id)}\n className={`flex w-full items-center gap-2 rounded-lg px-3 py-2 text-left text-sm ${\n p.id === selected ? \"bg-brand-50 text-brand-700\" : \"hover:bg-slate-100\"\n }`}\n >\n {p.images?.[0] && <img src={p.images[0]} alt=\"\" className=\"h-8 w-8 rounded object-cover\" />}\n <span className=\"min-w-0 flex-1 truncate\">{p.name}</span>\n <Badge tone={p.status === \"active\" ? \"green\" : p.status === \"draft\" ? \"amber\" : \"slate\"}>\n {STATUS_OPTIONS.find((s) => s.value === p.status)?.label ?? p.status}\n </Badge>\n </button>\n </li>\n ))}\n {filtered.length === 0 && <li className=\"px-3 py-2 text-sm text-slate-500\">Geen producten.</li>}\n </ul>\n </Card>\n {current ? (\n <ProductForm\n key={current.id}\n siteId={siteId}\n product={current}\n categories={cats}\n onDeleted={() => setSelected(null)}\n onToast={toast}\n confirm={confirm}\n />\n ) : (\n <Card>\n <p className=\"text-sm text-slate-500\">Selecteer of maak een product.</p>\n </Card>\n )}\n </div>\n );\n}\n\n/**\n * Het volledige productformulier. Sinds 7.8 ook buiten dit tabblad in gebruik: het tandwiel bij een\n * product op de pagina opent hetzelfde formulier in een zijpaneel. Bewust hergebruik en geen tweede,\n * uitgeklede versie — twee formulieren voor één product lopen gegarandeerd uit elkaar, en dan hangt\n * het van je route af welke velden je kunt bereiken.\n */\nexport function ProductForm({\n siteId,\n product,\n categories,\n onDeleted,\n onToast,\n confirm,\n}: {\n siteId: string;\n product: StoredProduct;\n categories: StoredCategory[];\n onDeleted: () => void;\n onToast: (t: { title: string; description?: string; tone?: \"default\" | \"success\" | \"error\" }) => void;\n confirm: (opts: ConfirmOptions) => Promise<boolean>;\n}) {\n const [def, setDef] = useState<Product>(product);\n const [priceInput, setPriceInput] = useState(centsToEuroInput(product.priceCents));\n const [compareInput, setCompareInput] = useState(centsToEuroInput(product.compareAtCents));\n\n const variantKeys = useMemo(() => allVariantKeys(def.options ?? []), [def.options]);\n\n function setOption(i: number, patch: Partial<ProductOption>) {\n setDef((p) => {\n const options = [...(p.options ?? [])];\n options[i] = { ...options[i], ...patch };\n return { ...p, options };\n });\n }\n\n function variantFor(key: string): ProductVariant | undefined {\n return (def.variants ?? []).find((v) => v.key === key);\n }\n\n function patchVariant(key: string, patch: Partial<ProductVariant>) {\n setDef((p) => {\n const variants = [...(p.variants ?? [])];\n const idx = variants.findIndex((v) => v.key === key);\n if (idx >= 0) variants[idx] = { ...variants[idx], ...patch };\n else variants.push({ key, active: true, ...patch });\n return { ...p, variants };\n });\n }\n\n async function save() {\n const db = getDb();\n if (!db) return;\n // Alleen varianten bewaren die iets toevoegen (afwijkende prijs/voorraad/sku of inactief),\n // en geen wees-varianten van verwijderde optiewaarden.\n const validKeys = new Set(variantKeys);\n const cleanVariants = (def.variants ?? []).filter(\n (v) =>\n validKeys.has(v.key) &&\n (v.priceCents !== undefined || v.stock !== undefined || v.sku !== undefined || v.active === false),\n );\n // Onvolledige personalisatie-regels (zonder label) niet opslaan.\n const cleanPersonalization = (def.personalization ?? []).filter((f) => f.label.trim().length > 0);\n const parsed = ProductSchema.safeParse({\n ...def,\n variants: cleanVariants,\n personalization: cleanPersonalization.length > 0 ? cleanPersonalization : undefined,\n });\n if (!parsed.success) {\n onToast({ title: \"Ongeldig product\", description: parsed.error.issues[0]?.message, tone: \"error\" });\n return;\n }\n // Via de callable (7.9): de server valideert, auditeert en — belangrijker — pingt de\n // products-cachetag, zodat de wijziging ook op de LIVE shoppagina's staat en niet alleen in\n // Firestore. Zie productClient.ts, ook voor de terugval op een oudere backend.\n await saveProductDoc(siteId, product.id, parsed.data);\n onToast({ title: \"Product opgeslagen\", tone: \"success\" });\n }\n\n async function remove() {\n const ok = await confirm({\n title: `\"${product.name}\" verwijderen?`,\n description: \"Bestaande bestellingen behouden hun eigen kopie van naam en prijs.\",\n tone: \"danger\",\n });\n if (!ok) return;\n await removeProduct(siteId, product.id);\n onDeleted();\n }\n\n return (\n <Card>\n <div className=\"grid gap-4\">\n <div className=\"grid gap-4 md:grid-cols-2\">\n <Field label=\"Naam\">\n <Input\n value={def.name}\n onChange={(e) => setDef((p) => ({ ...p, name: e.target.value, slug: slugify(e.target.value) || p.slug }))}\n />\n </Field>\n <Field label=\"Slug\" hint=\"Product-URL: /product?p=<slug>\">\n <Input value={def.slug} onChange={(e) => setDef((p) => ({ ...p, slug: slugify(e.target.value) }))} />\n </Field>\n </div>\n\n <div className=\"grid gap-4 md:grid-cols-3\">\n <Field label=\"Prijs (€, incl. btw)\">\n <Input\n value={priceInput}\n onChange={(e) => {\n setPriceInput(e.target.value);\n const c = eurosToCents(e.target.value);\n if (c !== null) setDef((p) => ({ ...p, priceCents: c }));\n }}\n />\n </Field>\n <Field label=\"Van-prijs (€, optioneel)\" hint=\"Doorgestreept naast de prijs\">\n <Input\n value={compareInput}\n onChange={(e) => {\n setCompareInput(e.target.value);\n const c = eurosToCents(e.target.value);\n setDef((p) => ({ ...p, compareAtCents: c ?? undefined }));\n }}\n />\n </Field>\n <Field label=\"Btw-tarief\">\n <Select\n value={String(def.vatRate ?? 21)}\n onValueChange={(v) => setDef((p) => ({ ...p, vatRate: Number(v) as 21 | 9 | 0 }))}\n options={[\n { value: \"21\", label: \"21%\" },\n { value: \"9\", label: \"9%\" },\n { value: \"0\", label: \"0%\" },\n ]}\n />\n </Field>\n </div>\n\n <Field label=\"Omschrijving\">\n <Textarea rows={4} value={def.description ?? \"\"} onChange={(e) => setDef((p) => ({ ...p, description: e.target.value }))} />\n </Field>\n\n <Field label=\"Afbeeldingen\" hint=\"Eerste afbeelding is het hoofdbeeld\">\n <div className=\"flex flex-wrap items-center gap-3\">\n {(def.images ?? []).map((url, i) => (\n <div key={url + i} className=\"relative\">\n <img src={url} alt=\"\" className=\"h-16 w-16 rounded-lg object-cover\" />\n <button\n type=\"button\"\n aria-label=\"Verwijder afbeelding\"\n className=\"absolute -right-2 -top-2 grid h-5 w-5 place-items-center rounded-full bg-slate-700 text-xs text-white\"\n onClick={() => setDef((p) => ({ ...p, images: (p.images ?? []).filter((_, j) => j !== i) }))}\n >\n ✕\n </button>\n </div>\n ))}\n <MediaPickerButton\n siteId={siteId}\n label=\"+ Afbeelding\"\n onSelect={(m) => setDef((p) => ({ ...p, images: [...(p.images ?? []), m.url] }))}\n />\n </div>\n </Field>\n\n <Field label=\"Categorieën\">\n <div className=\"flex flex-wrap gap-2\">\n {categories.map((c) => {\n const on = (def.categoryIds ?? []).includes(c.id);\n return (\n <button\n key={c.id}\n type=\"button\"\n onClick={() =>\n setDef((p) => ({\n ...p,\n categoryIds: on ? (p.categoryIds ?? []).filter((x) => x !== c.id) : [...(p.categoryIds ?? []), c.id],\n }))\n }\n className={`rounded-full border px-3 py-1 text-sm ${\n on ? \"border-brand-500 bg-brand-50 text-brand-700\" : \"border-slate-200 hover:bg-slate-50\"\n }`}\n >\n {on ? \"✓ \" : \"\"}\n {c.name}\n </button>\n );\n })}\n {categories.length === 0 && <span className=\"text-sm text-slate-500\">Maak eerst een categorie aan.</span>}\n </div>\n </Field>\n\n {/* Opties & varianten */}\n <Field\n label=\"Opties (bv. Maat, Kleur)\"\n hint=\"Max. 3. Waarden gescheiden door komma's. Combinaties kun je hieronder per stuk aanpassen.\"\n >\n <div className=\"grid gap-2\">\n {(def.options ?? []).map((opt, i) => (\n <div key={i} className=\"grid gap-2 rounded-lg border border-slate-100 p-2\">\n <div className=\"flex items-center gap-2\">\n <Input\n className=\"w-36\"\n placeholder=\"Naam (bv. Maat)\"\n value={opt.name}\n onChange={(e) => setOption(i, { name: e.target.value })}\n />\n <Input\n placeholder=\"Waarden, bv: 56, 62, 68, 74\"\n value={opt.values.join(\", \")}\n onChange={(e) =>\n setOption(i, { values: e.target.value.split(\",\").map((s) => s.trim()).filter(Boolean) })\n }\n />\n <Button\n size=\"sm\"\n variant=\"ghost\"\n onClick={() => setDef((p) => ({ ...p, options: (p.options ?? []).filter((_, j) => j !== i) }))}\n >\n ✕\n </Button>\n </div>\n {/* Afbeelding per waarde (5.6) — vooral voor de kleur-as: de detailpagina\n wisselt het hoofdbeeld mee met de gekozen kleur. */}\n {/^kleur|^colou?r/i.test(opt.name) && opt.values.length > 0 && (\n <div className=\"flex flex-wrap items-center gap-2\">\n {opt.values.map((value) => {\n const url = opt.images?.[value];\n return (\n <span key={value} className=\"flex items-center gap-1 rounded-full border border-slate-200 px-2 py-1 text-xs\">\n {url && <img src={url} alt=\"\" className=\"h-6 w-6 rounded object-cover\" />}\n {value}\n <MediaPickerButton\n siteId={siteId}\n label={url ? \"↻\" : \"+ foto\"}\n onSelect={(m) =>\n setOption(i, { images: { ...(opt.images ?? {}), [value]: m.url } })\n }\n />\n {url && (\n <button\n type=\"button\"\n aria-label={`Verwijder foto van ${value}`}\n className=\"text-slate-400 hover:text-slate-700\"\n onClick={() => {\n const images = { ...(opt.images ?? {}) };\n delete images[value];\n setOption(i, { images });\n }}\n >\n ✕\n </button>\n )}\n </span>\n );\n })}\n </div>\n )}\n </div>\n ))}\n {(def.options ?? []).length < 3 && (\n <Button\n size=\"sm\"\n variant=\"secondary\"\n className=\"w-fit\"\n onClick={() => setDef((p) => ({ ...p, options: [...(p.options ?? []), { name: \"\", values: [] }] }))}\n >\n + Optie\n </Button>\n )}\n </div>\n </Field>\n\n {variantKeys.length > 0 && variantKeys.length <= 60 && (\n <Field label=\"Varianten\" hint=\"Leeg = standaardprijs / onbeperkte voorraad\">\n <div className=\"overflow-x-auto\">\n <table className=\"w-full text-sm\">\n <thead>\n <tr className=\"text-left text-xs text-slate-500\">\n <th className=\"py-1 pr-2\">Combinatie</th>\n <th className=\"py-1 pr-2\">Prijs (€)</th>\n {def.trackStock && <th className=\"py-1 pr-2\">Voorraad</th>}\n <th className=\"py-1 pr-2\">Leverbaar</th>\n </tr>\n </thead>\n <tbody>\n {variantKeys.map((key) => {\n const v = variantFor(key);\n return (\n <tr key={key} className=\"border-t border-slate-100\">\n <td className=\"py-1 pr-2\">{key.split(\"|\").join(\" · \")}</td>\n <td className=\"py-1 pr-2\">\n {/* Native uncontrolled inputs: geen cursor-gedoe bij \"13,\" tijdens typen. */}\n <input\n className=\"w-24 rounded-lg border border-slate-200 px-2 py-1\"\n placeholder={centsToEuroInput(def.priceCents)}\n defaultValue={centsToEuroInput(v?.priceCents)}\n onChange={(e) => {\n const c = eurosToCents(e.target.value);\n patchVariant(key, { priceCents: c ?? undefined });\n }}\n />\n </td>\n {def.trackStock && (\n <td className=\"py-1 pr-2\">\n <input\n className=\"w-20 rounded-lg border border-slate-200 px-2 py-1\"\n type=\"number\"\n min={0}\n placeholder=\"∞\"\n defaultValue={v?.stock ?? \"\"}\n onChange={(e) => {\n const n = e.target.value === \"\" ? undefined : Math.max(0, Number(e.target.value) || 0);\n patchVariant(key, { stock: n });\n }}\n />\n </td>\n )}\n <td className=\"py-1 pr-2\">\n <Switch checked={v?.active !== false} onCheckedChange={(on) => patchVariant(key, { active: on })} />\n </td>\n </tr>\n );\n })}\n </tbody>\n </table>\n </div>\n </Field>\n )}\n {variantKeys.length > 60 && (\n <p className=\"text-sm text-amber-600\">\n {variantKeys.length} combinaties — te veel om per stuk te tonen. Beperk het aantal optie-waarden.\n </p>\n )}\n\n {/* Personalisatie (5.6): eigen naam/logo van de klant, met optionele meerprijs. */}\n <Field\n label=\"Personalisatie (eigen tekst of afbeelding)\"\n hint=\"Max. 3 velden. De klant vult dit in op de productpagina; de meerprijs telt automatisch mee.\"\n >\n <div className=\"grid gap-2\">\n {(def.personalization ?? []).map((f, i) => {\n const setPers = (patch: Partial<PersonalizationField>) =>\n setDef((p) => {\n const personalization = [...(p.personalization ?? [])];\n personalization[i] = { ...personalization[i], ...patch };\n return { ...p, personalization };\n });\n return (\n <div key={i} className=\"grid gap-2 rounded-lg border border-slate-100 p-2 md:grid-cols-[1fr_130px_90px_110px_auto]\">\n <Input\n placeholder='Label (bv. \"Naam van het kindje\")'\n value={f.label}\n onChange={(e) =>\n setPers({\n label: e.target.value,\n key: f.key || e.target.value.toLowerCase().replace(/[^a-z0-9]+/g, \"_\").replace(/^_+|_+$/g, \"\").slice(0, 40) || `veld${i + 1}`,\n })\n }\n />\n <Select\n value={f.type}\n onValueChange={(v) => setPers({ type: v as \"text\" | \"image\" })}\n options={[\n { value: \"text\", label: \"Tekst\" },\n { value: \"image\", label: \"Afbeelding\" },\n ]}\n />\n {f.type === \"text\" ? (\n <Input\n type=\"number\"\n title=\"Max. tekens\"\n min={1}\n max={200}\n value={f.maxLength ?? 40}\n onChange={(e) => setPers({ maxLength: Math.max(1, Math.min(200, Number(e.target.value) || 40)) })}\n />\n ) : (\n <span className=\"self-center text-xs text-slate-400\">jpg/png/webp</span>\n )}\n <input\n className=\"rounded-lg border border-slate-200 px-2 py-1\"\n placeholder=\"Meerprijs €\"\n defaultValue={f.priceCents ? centsToEuroInput(f.priceCents) : \"\"}\n onChange={(e) => setPers({ priceCents: eurosToCents(e.target.value) ?? 0 })}\n />\n <div className=\"flex items-center gap-2\">\n <Switch checked={f.required === true} onCheckedChange={(v) => setPers({ required: v })} label=\"Verplicht\" />\n <Button\n size=\"sm\"\n variant=\"ghost\"\n onClick={() => setDef((p) => ({ ...p, personalization: (p.personalization ?? []).filter((_, j) => j !== i) }))}\n >\n ✕\n </Button>\n </div>\n </div>\n );\n })}\n {(def.personalization ?? []).length < 3 && (\n <Button\n size=\"sm\"\n variant=\"secondary\"\n className=\"w-fit\"\n onClick={() =>\n setDef((p) => ({\n ...p,\n personalization: [\n ...(p.personalization ?? []),\n { key: `veld${(p.personalization ?? []).length + 1}`, label: \"\", type: \"text\", required: false, maxLength: 40, priceCents: 0 },\n ],\n }))\n }\n >\n + Personalisatie-veld\n </Button>\n )}\n </div>\n </Field>\n\n <div className=\"flex flex-wrap items-center gap-6\">\n <Field label=\"Status\">\n <Select\n value={def.status ?? \"draft\"}\n onValueChange={(v) => setDef((p) => ({ ...p, status: v as Product[\"status\"] }))}\n options={STATUS_OPTIONS}\n />\n </Field>\n <Field label=\"Volgorde\">\n <Input\n type=\"number\"\n className=\"w-24\"\n value={def.order ?? 0}\n onChange={(e) => setDef((p) => ({ ...p, order: Math.max(0, Number(e.target.value) || 0) }))}\n />\n </Field>\n <Switch checked={def.featured === true} onCheckedChange={(v) => setDef((p) => ({ ...p, featured: v }))} label=\"Uitgelicht\" />\n <Switch checked={def.trackStock === true} onCheckedChange={(v) => setDef((p) => ({ ...p, trackStock: v }))} label=\"Voorraad bijhouden\" />\n {def.trackStock && (def.options ?? []).length === 0 && (\n <Field label=\"Voorraad\">\n <Input\n type=\"number\"\n className=\"w-24\"\n placeholder=\"∞\"\n value={def.stock ?? \"\"}\n onChange={(e) =>\n setDef((p) => ({ ...p, stock: e.target.value === \"\" ? undefined : Math.max(0, Number(e.target.value) || 0) }))\n }\n />\n </Field>\n )}\n </div>\n\n <div className=\"flex items-center justify-between border-t border-slate-100 pt-4\">\n <div className=\"flex items-center gap-3\">\n <Button onClick={save}>Opslaan</Button>\n <span className=\"text-sm text-slate-500\">{formatCents(def.priceCents)}</span>\n </div>\n <Button variant=\"danger\" onClick={remove}>Verwijderen</Button>\n </div>\n </div>\n </Card>\n );\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../../src/admin/shop/ProductsTab.tsx"],"sourcesContent":["\"use client\";\n\nimport { useEffect, useMemo, useState } from \"react\";\nimport { collection, doc, onSnapshot } from \"firebase/firestore\";\nimport { removeProduct, saveProductDoc } from \"./productClient\";\nimport { getDb } from \"../../firebase/client\";\nimport {\n ProductSchema,\n formatCents,\n type PersonalizationField,\n type Product,\n type ProductOption,\n type ProductVariant,\n type ShopCategory,\n} from \"../../schema/shop\";\nimport { Badge, Button, Card, Field, Input, Textarea } from \"../ui/primitives\";\nimport { Select } from \"../ui/select\";\nimport { Switch } from \"../ui/switch\";\nimport { useToast } from \"../ui/toast\";\nimport { useConfirm, type ConfirmOptions } from \"../ui/ConfirmDialog\";\nimport { MediaPickerButton } from \"../media/MediaPicker\";\nimport { slugify } from \"../lib/slugify\";\nimport { allVariantKeys, centsToEuroInput, eurosToCents } from \"./lib\";\n\ntype StoredProduct = Product & { id: string };\ntype StoredCategory = ShopCategory & { id: string };\n\nconst STATUS_OPTIONS = [\n { value: \"draft\", label: \"Concept\" },\n { value: \"active\", label: \"Actief\" },\n { value: \"archived\", label: \"Gearchiveerd\" },\n];\n\nexport function ProductsTab({ siteId }: { siteId: string }) {\n const { toast } = useToast();\n const confirm = useConfirm();\n const [products, setProducts] = useState<StoredProduct[]>([]);\n const [cats, setCats] = useState<StoredCategory[]>([]);\n const [selected, setSelected] = useState<string | null>(null);\n const [q, setQ] = useState(\"\");\n\n useEffect(() => {\n const db = getDb();\n if (!db) return;\n const un1 = onSnapshot(collection(db, \"sites\", siteId, \"products\"), (snap) =>\n setProducts(\n snap.docs\n .map((d) => ({ id: d.id, ...(d.data() as Product) }))\n .sort((a, b) => (a.order ?? 0) - (b.order ?? 0) || a.name.localeCompare(b.name, \"nl\")),\n ),\n );\n const un2 = onSnapshot(collection(db, \"sites\", siteId, \"shopCategories\"), (snap) =>\n setCats(snap.docs.map((d) => ({ id: d.id, ...(d.data() as ShopCategory) }))),\n );\n return () => {\n un1();\n un2();\n };\n }, [siteId]);\n\n async function createProduct() {\n // Ook aanmaken loopt via de callable: een vers product is een draft (onzichtbaar voor de\n // cache), maar zo staat élke productmutatie in de audit-log en is er één pad i.p.v. twee.\n const nieuw = {\n name: \"Nieuw product\",\n slug: `product-${products.length + 1}`,\n description: \"\",\n categoryIds: [],\n images: [],\n priceCents: 0,\n vatRate: 21,\n options: [],\n variants: [],\n trackStock: false,\n status: \"draft\",\n order: products.length,\n featured: false,\n } satisfies Product;\n try {\n const id = await saveProductDoc(siteId, undefined, nieuw);\n setSelected(id);\n } catch (e) {\n // Zichtbaar falen: dit gooide een tijdlang stil (unhandled) terwijl de knop gewoon niets\n // leek te doen — precies de melding \"bij aanmaken product krijg ik iets met permissions\".\n toast({ title: \"Product aanmaken mislukt\", description: e instanceof Error ? e.message : undefined, tone: \"error\" });\n }\n }\n\n const filtered = useMemo(() => {\n const needle = q.trim().toLowerCase();\n if (!needle) return products;\n return products.filter((p) => p.name.toLowerCase().includes(needle) || p.slug.includes(needle));\n }, [products, q]);\n\n const current = products.find((p) => p.id === selected) ?? null;\n\n return (\n <div className=\"grid gap-4 md:grid-cols-[300px_1fr]\">\n <Card className=\"h-fit\">\n <div className=\"mb-3 flex items-center justify-between\">\n <h3 className=\"text-sm font-semibold\">Producten</h3>\n <Button size=\"sm\" onClick={createProduct}>+ Nieuw</Button>\n </div>\n <Input placeholder=\"Zoeken…\" value={q} onChange={(e) => setQ(e.target.value)} className=\"mb-2\" />\n <ul className=\"grid max-h-[60vh] gap-1 overflow-y-auto\">\n {filtered.map((p) => (\n <li key={p.id}>\n <button\n onClick={() => setSelected(p.id)}\n className={`flex w-full items-center gap-2 rounded-lg px-3 py-2 text-left text-sm ${\n p.id === selected ? \"bg-brand-50 text-brand-700\" : \"hover:bg-slate-100\"\n }`}\n >\n {p.images?.[0] && <img src={p.images[0]} alt=\"\" className=\"h-8 w-8 rounded object-cover\" />}\n <span className=\"min-w-0 flex-1 truncate\">{p.name}</span>\n <Badge tone={p.status === \"active\" ? \"green\" : p.status === \"draft\" ? \"amber\" : \"slate\"}>\n {STATUS_OPTIONS.find((s) => s.value === p.status)?.label ?? p.status}\n </Badge>\n </button>\n </li>\n ))}\n {filtered.length === 0 && <li className=\"px-3 py-2 text-sm text-slate-500\">Geen producten.</li>}\n </ul>\n </Card>\n {current ? (\n <ProductForm\n key={current.id}\n siteId={siteId}\n product={current}\n categories={cats}\n onDeleted={() => setSelected(null)}\n onToast={toast}\n confirm={confirm}\n />\n ) : (\n <Card>\n <p className=\"text-sm text-slate-500\">Selecteer of maak een product.</p>\n </Card>\n )}\n </div>\n );\n}\n\n/**\n * Het volledige productformulier. Sinds 7.8 ook buiten dit tabblad in gebruik: het tandwiel bij een\n * product op de pagina opent hetzelfde formulier in een zijpaneel. Bewust hergebruik en geen tweede,\n * uitgeklede versie — twee formulieren voor één product lopen gegarandeerd uit elkaar, en dan hangt\n * het van je route af welke velden je kunt bereiken.\n */\nexport function ProductForm({\n siteId,\n product,\n categories,\n onDeleted,\n onToast,\n confirm,\n}: {\n siteId: string;\n product: StoredProduct;\n categories: StoredCategory[];\n onDeleted: () => void;\n onToast: (t: { title: string; description?: string; tone?: \"default\" | \"success\" | \"error\" }) => void;\n confirm: (opts: ConfirmOptions) => Promise<boolean>;\n}) {\n const [def, setDef] = useState<Product>(product);\n const [priceInput, setPriceInput] = useState(centsToEuroInput(product.priceCents));\n const [compareInput, setCompareInput] = useState(centsToEuroInput(product.compareAtCents));\n\n const variantKeys = useMemo(() => allVariantKeys(def.options ?? []), [def.options]);\n\n function setOption(i: number, patch: Partial<ProductOption>) {\n setDef((p) => {\n const options = [...(p.options ?? [])];\n options[i] = { ...options[i], ...patch };\n return { ...p, options };\n });\n }\n\n function variantFor(key: string): ProductVariant | undefined {\n return (def.variants ?? []).find((v) => v.key === key);\n }\n\n function patchVariant(key: string, patch: Partial<ProductVariant>) {\n setDef((p) => {\n const variants = [...(p.variants ?? [])];\n const idx = variants.findIndex((v) => v.key === key);\n if (idx >= 0) variants[idx] = { ...variants[idx], ...patch };\n else variants.push({ key, active: true, ...patch });\n return { ...p, variants };\n });\n }\n\n async function save() {\n const db = getDb();\n if (!db) return;\n // Alleen varianten bewaren die iets toevoegen (afwijkende prijs/voorraad/sku of inactief),\n // en geen wees-varianten van verwijderde optiewaarden.\n const validKeys = new Set(variantKeys);\n const cleanVariants = (def.variants ?? []).filter(\n (v) =>\n validKeys.has(v.key) &&\n (v.priceCents !== undefined || v.stock !== undefined || v.sku !== undefined || v.active === false),\n );\n // Onvolledige personalisatie-regels (zonder label) niet opslaan.\n const cleanPersonalization = (def.personalization ?? []).filter((f) => f.label.trim().length > 0);\n const parsed = ProductSchema.safeParse({\n ...def,\n variants: cleanVariants,\n personalization: cleanPersonalization.length > 0 ? cleanPersonalization : undefined,\n });\n if (!parsed.success) {\n onToast({ title: \"Ongeldig product\", description: parsed.error.issues[0]?.message, tone: \"error\" });\n return;\n }\n // Via de callable (7.9): de server valideert, auditeert en — belangrijker — pingt de\n // products-cachetag, zodat de wijziging ook op de LIVE shoppagina's staat en niet alleen in\n // Firestore. Zie productClient.ts, ook voor de terugval op een oudere backend.\n await saveProductDoc(siteId, product.id, parsed.data);\n onToast({ title: \"Product opgeslagen\", tone: \"success\" });\n }\n\n async function remove() {\n const ok = await confirm({\n title: `\"${product.name}\" verwijderen?`,\n description: \"Bestaande bestellingen behouden hun eigen kopie van naam en prijs.\",\n tone: \"danger\",\n });\n if (!ok) return;\n await removeProduct(siteId, product.id);\n onDeleted();\n }\n\n return (\n <Card>\n <div className=\"grid gap-4\">\n <div className=\"grid gap-4 md:grid-cols-2\">\n <Field label=\"Naam\">\n <Input\n value={def.name}\n onChange={(e) => setDef((p) => ({ ...p, name: e.target.value, slug: slugify(e.target.value) || p.slug }))}\n />\n </Field>\n <Field label=\"Slug\" hint=\"Product-URL: /product?p=<slug>\">\n <Input value={def.slug} onChange={(e) => setDef((p) => ({ ...p, slug: slugify(e.target.value) }))} />\n </Field>\n </div>\n\n <div className=\"grid gap-4 md:grid-cols-3\">\n <Field label=\"Prijs (€, incl. btw)\">\n <Input\n value={priceInput}\n onChange={(e) => {\n setPriceInput(e.target.value);\n const c = eurosToCents(e.target.value);\n if (c !== null) setDef((p) => ({ ...p, priceCents: c }));\n }}\n />\n </Field>\n <Field label=\"Van-prijs (€, optioneel)\" hint=\"Doorgestreept naast de prijs\">\n <Input\n value={compareInput}\n onChange={(e) => {\n setCompareInput(e.target.value);\n const c = eurosToCents(e.target.value);\n setDef((p) => ({ ...p, compareAtCents: c ?? undefined }));\n }}\n />\n </Field>\n <Field label=\"Btw-tarief\">\n <Select\n value={String(def.vatRate ?? 21)}\n onValueChange={(v) => setDef((p) => ({ ...p, vatRate: Number(v) as 21 | 9 | 0 }))}\n options={[\n { value: \"21\", label: \"21%\" },\n { value: \"9\", label: \"9%\" },\n { value: \"0\", label: \"0%\" },\n ]}\n />\n </Field>\n </div>\n\n <Field label=\"Omschrijving\">\n <Textarea rows={4} value={def.description ?? \"\"} onChange={(e) => setDef((p) => ({ ...p, description: e.target.value }))} />\n </Field>\n\n <Field label=\"Afbeeldingen\" hint=\"Eerste afbeelding is het hoofdbeeld\">\n <div className=\"flex flex-wrap items-center gap-3\">\n {(def.images ?? []).map((url, i) => (\n <div key={url + i} className=\"relative\">\n <img src={url} alt=\"\" className=\"h-16 w-16 rounded-lg object-cover\" />\n <button\n type=\"button\"\n aria-label=\"Verwijder afbeelding\"\n className=\"absolute -right-2 -top-2 grid h-5 w-5 place-items-center rounded-full bg-slate-700 text-xs text-white\"\n onClick={() => setDef((p) => ({ ...p, images: (p.images ?? []).filter((_, j) => j !== i) }))}\n >\n ✕\n </button>\n </div>\n ))}\n <MediaPickerButton\n siteId={siteId}\n label=\"+ Afbeelding\"\n onSelect={(m) => setDef((p) => ({ ...p, images: [...(p.images ?? []), m.url] }))}\n />\n </div>\n </Field>\n\n <Field label=\"Categorieën\">\n <div className=\"flex flex-wrap gap-2\">\n {categories.map((c) => {\n const on = (def.categoryIds ?? []).includes(c.id);\n return (\n <button\n key={c.id}\n type=\"button\"\n onClick={() =>\n setDef((p) => ({\n ...p,\n categoryIds: on ? (p.categoryIds ?? []).filter((x) => x !== c.id) : [...(p.categoryIds ?? []), c.id],\n }))\n }\n className={`rounded-full border px-3 py-1 text-sm ${\n on ? \"border-brand-500 bg-brand-50 text-brand-700\" : \"border-slate-200 hover:bg-slate-50\"\n }`}\n >\n {on ? \"✓ \" : \"\"}\n {c.name}\n </button>\n );\n })}\n {categories.length === 0 && <span className=\"text-sm text-slate-500\">Maak eerst een categorie aan.</span>}\n </div>\n </Field>\n\n {/* Opties & varianten */}\n <Field\n label=\"Opties (bv. Maat, Kleur)\"\n hint=\"Max. 3. Waarden gescheiden door komma's. Combinaties kun je hieronder per stuk aanpassen.\"\n >\n <div className=\"grid gap-2\">\n {(def.options ?? []).map((opt, i) => (\n <div key={i} className=\"grid gap-2 rounded-lg border border-slate-100 p-2\">\n <div className=\"flex items-center gap-2\">\n <Input\n className=\"w-36\"\n placeholder=\"Naam (bv. Maat)\"\n value={opt.name}\n onChange={(e) => setOption(i, { name: e.target.value })}\n />\n <Input\n placeholder=\"Waarden, bv: 56, 62, 68, 74\"\n value={opt.values.join(\", \")}\n onChange={(e) =>\n setOption(i, { values: e.target.value.split(\",\").map((s) => s.trim()).filter(Boolean) })\n }\n />\n <Button\n size=\"sm\"\n variant=\"ghost\"\n onClick={() => setDef((p) => ({ ...p, options: (p.options ?? []).filter((_, j) => j !== i) }))}\n >\n ✕\n </Button>\n </div>\n {/* Afbeelding per waarde (5.6) — vooral voor de kleur-as: de detailpagina\n wisselt het hoofdbeeld mee met de gekozen kleur. */}\n {/^kleur|^colou?r/i.test(opt.name) && opt.values.length > 0 && (\n <div className=\"flex flex-wrap items-center gap-2\">\n {opt.values.map((value) => {\n const url = opt.images?.[value];\n return (\n <span key={value} className=\"flex items-center gap-1 rounded-full border border-slate-200 px-2 py-1 text-xs\">\n {url && <img src={url} alt=\"\" className=\"h-6 w-6 rounded object-cover\" />}\n {value}\n <MediaPickerButton\n siteId={siteId}\n label={url ? \"↻\" : \"+ foto\"}\n onSelect={(m) =>\n setOption(i, { images: { ...(opt.images ?? {}), [value]: m.url } })\n }\n />\n {url && (\n <button\n type=\"button\"\n aria-label={`Verwijder foto van ${value}`}\n className=\"text-slate-400 hover:text-slate-700\"\n onClick={() => {\n const images = { ...(opt.images ?? {}) };\n delete images[value];\n setOption(i, { images });\n }}\n >\n ✕\n </button>\n )}\n </span>\n );\n })}\n </div>\n )}\n </div>\n ))}\n {(def.options ?? []).length < 3 && (\n <Button\n size=\"sm\"\n variant=\"secondary\"\n className=\"w-fit\"\n onClick={() => setDef((p) => ({ ...p, options: [...(p.options ?? []), { name: \"\", values: [] }] }))}\n >\n + Optie\n </Button>\n )}\n </div>\n </Field>\n\n {variantKeys.length > 0 && variantKeys.length <= 60 && (\n <Field label=\"Varianten\" hint=\"Leeg = standaardprijs / onbeperkte voorraad\">\n <div className=\"overflow-x-auto\">\n <table className=\"w-full text-sm\">\n <thead>\n <tr className=\"text-left text-xs text-slate-500\">\n <th className=\"py-1 pr-2\">Combinatie</th>\n <th className=\"py-1 pr-2\">Prijs (€)</th>\n {def.trackStock && <th className=\"py-1 pr-2\">Voorraad</th>}\n <th className=\"py-1 pr-2\">Leverbaar</th>\n </tr>\n </thead>\n <tbody>\n {variantKeys.map((key) => {\n const v = variantFor(key);\n return (\n <tr key={key} className=\"border-t border-slate-100\">\n <td className=\"py-1 pr-2\">{key.split(\"|\").join(\" · \")}</td>\n <td className=\"py-1 pr-2\">\n {/* Native uncontrolled inputs: geen cursor-gedoe bij \"13,\" tijdens typen. */}\n <input\n className=\"w-24 rounded-lg border border-slate-200 px-2 py-1\"\n placeholder={centsToEuroInput(def.priceCents)}\n defaultValue={centsToEuroInput(v?.priceCents)}\n onChange={(e) => {\n const c = eurosToCents(e.target.value);\n patchVariant(key, { priceCents: c ?? undefined });\n }}\n />\n </td>\n {def.trackStock && (\n <td className=\"py-1 pr-2\">\n <input\n className=\"w-20 rounded-lg border border-slate-200 px-2 py-1\"\n type=\"number\"\n min={0}\n placeholder=\"∞\"\n defaultValue={v?.stock ?? \"\"}\n onChange={(e) => {\n const n = e.target.value === \"\" ? undefined : Math.max(0, Number(e.target.value) || 0);\n patchVariant(key, { stock: n });\n }}\n />\n </td>\n )}\n <td className=\"py-1 pr-2\">\n <Switch checked={v?.active !== false} onCheckedChange={(on) => patchVariant(key, { active: on })} />\n </td>\n </tr>\n );\n })}\n </tbody>\n </table>\n </div>\n </Field>\n )}\n {variantKeys.length > 60 && (\n <p className=\"text-sm text-amber-600\">\n {variantKeys.length} combinaties — te veel om per stuk te tonen. Beperk het aantal optie-waarden.\n </p>\n )}\n\n {/* Personalisatie (5.6): eigen naam/logo van de klant, met optionele meerprijs. */}\n <Field\n label=\"Personalisatie (eigen tekst of afbeelding)\"\n hint=\"Max. 3 velden. De klant vult dit in op de productpagina; de meerprijs telt automatisch mee.\"\n >\n <div className=\"grid gap-2\">\n {(def.personalization ?? []).map((f, i) => {\n const setPers = (patch: Partial<PersonalizationField>) =>\n setDef((p) => {\n const personalization = [...(p.personalization ?? [])];\n personalization[i] = { ...personalization[i], ...patch };\n return { ...p, personalization };\n });\n return (\n <div key={i} className=\"grid gap-2 rounded-lg border border-slate-100 p-2 md:grid-cols-[1fr_130px_90px_110px_auto]\">\n <Input\n placeholder='Label (bv. \"Naam van het kindje\")'\n value={f.label}\n onChange={(e) =>\n setPers({\n label: e.target.value,\n key: f.key || e.target.value.toLowerCase().replace(/[^a-z0-9]+/g, \"_\").replace(/^_+|_+$/g, \"\").slice(0, 40) || `veld${i + 1}`,\n })\n }\n />\n <Select\n value={f.type}\n onValueChange={(v) => setPers({ type: v as \"text\" | \"image\" })}\n options={[\n { value: \"text\", label: \"Tekst\" },\n { value: \"image\", label: \"Afbeelding\" },\n ]}\n />\n {f.type === \"text\" ? (\n <Input\n type=\"number\"\n title=\"Max. tekens\"\n min={1}\n max={200}\n value={f.maxLength ?? 40}\n onChange={(e) => setPers({ maxLength: Math.max(1, Math.min(200, Number(e.target.value) || 40)) })}\n />\n ) : (\n <span className=\"self-center text-xs text-slate-400\">jpg/png/webp</span>\n )}\n <input\n className=\"rounded-lg border border-slate-200 px-2 py-1\"\n placeholder=\"Meerprijs €\"\n defaultValue={f.priceCents ? centsToEuroInput(f.priceCents) : \"\"}\n onChange={(e) => setPers({ priceCents: eurosToCents(e.target.value) ?? 0 })}\n />\n <div className=\"flex items-center gap-2\">\n <Switch checked={f.required === true} onCheckedChange={(v) => setPers({ required: v })} label=\"Verplicht\" />\n <Button\n size=\"sm\"\n variant=\"ghost\"\n onClick={() => setDef((p) => ({ ...p, personalization: (p.personalization ?? []).filter((_, j) => j !== i) }))}\n >\n ✕\n </Button>\n </div>\n </div>\n );\n })}\n {(def.personalization ?? []).length < 3 && (\n <Button\n size=\"sm\"\n variant=\"secondary\"\n className=\"w-fit\"\n onClick={() =>\n setDef((p) => ({\n ...p,\n personalization: [\n ...(p.personalization ?? []),\n { key: `veld${(p.personalization ?? []).length + 1}`, label: \"\", type: \"text\", required: false, maxLength: 40, priceCents: 0 },\n ],\n }))\n }\n >\n + Personalisatie-veld\n </Button>\n )}\n </div>\n </Field>\n\n <div className=\"flex flex-wrap items-center gap-6\">\n <Field label=\"Status\">\n <Select\n value={def.status ?? \"draft\"}\n onValueChange={(v) => setDef((p) => ({ ...p, status: v as Product[\"status\"] }))}\n options={STATUS_OPTIONS}\n />\n </Field>\n <Field label=\"Volgorde\">\n <Input\n type=\"number\"\n className=\"w-24\"\n value={def.order ?? 0}\n onChange={(e) => setDef((p) => ({ ...p, order: Math.max(0, Number(e.target.value) || 0) }))}\n />\n </Field>\n <Switch checked={def.featured === true} onCheckedChange={(v) => setDef((p) => ({ ...p, featured: v }))} label=\"Uitgelicht\" />\n <Switch checked={def.trackStock === true} onCheckedChange={(v) => setDef((p) => ({ ...p, trackStock: v }))} label=\"Voorraad bijhouden\" />\n {def.trackStock && (def.options ?? []).length === 0 && (\n <Field label=\"Voorraad\">\n <Input\n type=\"number\"\n className=\"w-24\"\n placeholder=\"∞\"\n value={def.stock ?? \"\"}\n onChange={(e) =>\n setDef((p) => ({ ...p, stock: e.target.value === \"\" ? undefined : Math.max(0, Number(e.target.value) || 0) }))\n }\n />\n </Field>\n )}\n </div>\n\n <div className=\"flex items-center justify-between border-t border-slate-100 pt-4\">\n <div className=\"flex items-center gap-3\">\n <Button onClick={save}>Opslaan</Button>\n <span className=\"text-sm text-slate-500\">{formatCents(def.priceCents)}</span>\n </div>\n <Button variant=\"danger\" onClick={remove}>Verwijderen</Button>\n </div>\n </div>\n </Card>\n );\n}\n"],"mappings":"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","names":[]}
|
|
@@ -2,6 +2,9 @@ import { getFunctions, httpsCallable, connectFunctionsEmulator } from "firebase/
|
|
|
2
2
|
import { functionsRegion } from "./region.js";
|
|
3
3
|
import { emulatorHosts } from "./emulators.js";
|
|
4
4
|
const verbonden = /* @__PURE__ */ new WeakSet();
|
|
5
|
+
function zonderUndefined(data) {
|
|
6
|
+
return data === void 0 ? void 0 : JSON.parse(JSON.stringify(data));
|
|
7
|
+
}
|
|
5
8
|
function callable(app, naam) {
|
|
6
9
|
const fns = getFunctions(app, functionsRegion());
|
|
7
10
|
if (!verbonden.has(fns)) {
|
|
@@ -9,7 +12,8 @@ function callable(app, naam) {
|
|
|
9
12
|
const em = emulatorHosts();
|
|
10
13
|
if (em) connectFunctionsEmulator(fns, em.host, em.functions);
|
|
11
14
|
}
|
|
12
|
-
|
|
15
|
+
const fn = httpsCallable(fns, naam);
|
|
16
|
+
return ((data) => fn(zonderUndefined(data)));
|
|
13
17
|
}
|
|
14
18
|
export {
|
|
15
19
|
callable
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/firebase/functionsClient.ts"],"sourcesContent":["/**\n * Eén plek waar een Cloud-Function-callable vandaan komt.\n *\n * Hiervoor stond op eenentwintig plekken hetzelfde regeltje\n * `httpsCallable(getFunctions(app, functionsRegion()), \"naam\")`. Dat was al duplicatie, maar het\n * werd een risico toen de emulators erbij kwamen: verbind je Firestore lokaal en laat je de\n * callables ongemoeid, dan lees je uit de emulator terwijl je SCHRIJFT naar het echte project —\n * en dat merk je pas als de productiedata al veranderd is. Door de callables hier te maken kan\n * die scheefstand niet ontstaan.\n *\n * Dit bestand importeert `firebase/functions` statisch, en wordt daarom zélf overal dynamisch\n * geïmporteerd waar het publieke pad langskomt (PageView, submitForm) — zo blijft de SDK uit de\n * bundel van een gewone bezoeker.\n */\nimport type { FirebaseApp } from \"firebase/app\";\nimport { getFunctions, httpsCallable, connectFunctionsEmulator, type HttpsCallable } from \"firebase/functions\";\nimport { functionsRegion } from \"./region\";\nimport { emulatorHosts } from \"./emulators\";\n\n// `getFunctions(app, regio)` geeft per app+regio dezelfde instantie terug, dus één verbinding volstaat.\nconst verbonden = new WeakSet<object>();\n\n/** Een getypeerde callable voor `naam`, in de juiste regio en (lokaal) tegen de emulator. */\nexport function callable<Req = unknown, Res = unknown>(app: FirebaseApp, naam: string): HttpsCallable<Req, Res> {\n const fns = getFunctions(app, functionsRegion());\n if (!verbonden.has(fns)) {\n verbonden.add(fns);\n const em = emulatorHosts();\n if (em) connectFunctionsEmulator(fns, em.host, em.functions);\n }\n
|
|
1
|
+
{"version":3,"sources":["../../src/firebase/functionsClient.ts"],"sourcesContent":["/**\n * Eén plek waar een Cloud-Function-callable vandaan komt.\n *\n * Hiervoor stond op eenentwintig plekken hetzelfde regeltje\n * `httpsCallable(getFunctions(app, functionsRegion()), \"naam\")`. Dat was al duplicatie, maar het\n * werd een risico toen de emulators erbij kwamen: verbind je Firestore lokaal en laat je de\n * callables ongemoeid, dan lees je uit de emulator terwijl je SCHRIJFT naar het echte project —\n * en dat merk je pas als de productiedata al veranderd is. Door de callables hier te maken kan\n * die scheefstand niet ontstaan.\n *\n * Dit bestand importeert `firebase/functions` statisch, en wordt daarom zélf overal dynamisch\n * geïmporteerd waar het publieke pad langskomt (PageView, submitForm) — zo blijft de SDK uit de\n * bundel van een gewone bezoeker.\n */\nimport type { FirebaseApp } from \"firebase/app\";\nimport { getFunctions, httpsCallable, connectFunctionsEmulator, type HttpsCallable } from \"firebase/functions\";\nimport { functionsRegion } from \"./region\";\nimport { emulatorHosts } from \"./emulators\";\n\n// `getFunctions(app, regio)` geeft per app+regio dezelfde instantie terug, dus één verbinding volstaat.\nconst verbonden = new WeakSet<object>();\n\n/**\n * `undefined`-velden strippen vóór verzenden.\n *\n * De functions-SDK encodeert `undefined` als `null` over de draad, en de zod-schema's op de\n * server zeggen `.optional()` — dat accepteert een ONTBREKEND veld, maar geen `null`. Elke\n * payload met een `veld: kanLeegZijn ? waarde : undefined`-patroon werd daardoor server-side\n * geweigerd: zo faalde \"+ Nieuw product\" ALTIJD (\"Ongeldige productgegevens\" — `productId:\n * undefined` kwam als `null` aan, live bewezen op christelijkkraamcadeau met een handmatige\n * call: zónder het veld 200, mét `null` 400) en \"als verzonden markeren\" zonder tracking-URL\n * net zo. Eén JSON-roundtrip hier dicht die hele klasse voor álle callables: de payloads zijn\n * plain JSON (geen Timestamps of klasse-instanties), dus dit is verliesvrij.\n */\nfunction zonderUndefined(data: unknown): unknown {\n return data === undefined ? undefined : JSON.parse(JSON.stringify(data));\n}\n\n/** Een getypeerde callable voor `naam`, in de juiste regio en (lokaal) tegen de emulator. */\nexport function callable<Req = unknown, Res = unknown>(app: FirebaseApp, naam: string): HttpsCallable<Req, Res> {\n const fns = getFunctions(app, functionsRegion());\n if (!verbonden.has(fns)) {\n verbonden.add(fns);\n const em = emulatorHosts();\n if (em) connectFunctionsEmulator(fns, em.host, em.functions);\n }\n const fn = httpsCallable<Req, Res>(fns, naam);\n return ((data?: Req) => fn(zonderUndefined(data) as Req)) as HttpsCallable<Req, Res>;\n}\n"],"mappings":"AAeA,SAAS,cAAc,eAAe,gCAAoD;AAC1F,SAAS,uBAAuB;AAChC,SAAS,qBAAqB;AAG9B,MAAM,YAAY,oBAAI,QAAgB;AActC,SAAS,gBAAgB,MAAwB;AAC/C,SAAO,SAAS,SAAY,SAAY,KAAK,MAAM,KAAK,UAAU,IAAI,CAAC;AACzE;AAGO,SAAS,SAAuC,KAAkB,MAAuC;AAC9G,QAAM,MAAM,aAAa,KAAK,gBAAgB,CAAC;AAC/C,MAAI,CAAC,UAAU,IAAI,GAAG,GAAG;AACvB,cAAU,IAAI,GAAG;AACjB,UAAM,KAAK,cAAc;AACzB,QAAI,GAAI,0BAAyB,KAAK,GAAG,MAAM,GAAG,SAAS;AAAA,EAC7D;AACA,QAAM,KAAK,cAAwB,KAAK,IAAI;AAC5C,UAAQ,CAAC,SAAe,GAAG,gBAAgB,IAAI,CAAQ;AACzD;","names":[]}
|
package/dist/version.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
const CORE_VERSION = true ? "7.16.
|
|
1
|
+
const CORE_VERSION = true ? "7.16.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.16.
|
|
3
|
+
"version": "7.16.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": {
|