@xiwebdesign/xicms-core 7.18.0 → 7.21.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/admin/settings/SettingsPanel.js +8 -1
- package/dist/admin/settings/SettingsPanel.js.map +1 -1
- package/dist/admin/shop/OrdersTab.js +141 -4
- package/dist/admin/shop/OrdersTab.js.map +1 -1
- package/dist/admin/shop/ProductsTab.d.ts +3 -7
- package/dist/admin/shop/ProductsTab.js +190 -97
- package/dist/admin/shop/ProductsTab.js.map +1 -1
- package/dist/admin/shop/ShopSettingsTab.js +57 -2
- package/dist/admin/shop/ShopSettingsTab.js.map +1 -1
- package/dist/admin.css +1 -1
- package/dist/modules/shop/blocks/Cart.js +8 -2
- package/dist/modules/shop/blocks/Cart.js.map +1 -1
- package/dist/modules/shop/blocks/Checkout.js +32 -8
- package/dist/modules/shop/blocks/Checkout.js.map +1 -1
- package/dist/modules/shop/blocks/ProductDetail.js +1 -1
- package/dist/modules/shop/blocks/ProductDetail.js.map +1 -1
- package/dist/modules/shop/blocks/ProductGrid.js +5 -1
- package/dist/modules/shop/blocks/ProductGrid.js.map +1 -1
- package/dist/modules/shop/settings.d.ts +9 -0
- package/dist/modules/shop/settings.js +27 -1
- package/dist/modules/shop/settings.js.map +1 -1
- package/dist/modules/shop/strings.d.ts +2 -0
- package/dist/modules/shop/strings.js +4 -0
- package/dist/modules/shop/strings.js.map +1 -1
- package/dist/schema/shop.d.ts +5 -0
- package/dist/schema/shop.js +22 -2
- package/dist/schema/shop.js.map +1 -1
- package/dist/schema/shopGeld.d.ts +30 -3
- package/dist/schema/shopGeld.js +19 -4
- package/dist/schema/shopGeld.js.map +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
|
@@ -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 const persFields = 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 { 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":[]}
|
|
@@ -25,7 +25,10 @@ function PriceLine({ product, currency }) {
|
|
|
25
25
|
] });
|
|
26
26
|
}
|
|
27
27
|
function isSoldOut(p) {
|
|
28
|
-
|
|
28
|
+
if (p.trackStock !== true) return false;
|
|
29
|
+
if ((p.options ?? []).length === 0) return p.stock !== void 0 && p.stock <= 0;
|
|
30
|
+
const metVoorraad = (p.variants ?? []).filter((v) => v.stock !== void 0 && v.active !== false);
|
|
31
|
+
return metVoorraad.length > 0 && metVoorraad.every((v) => v.stock <= 0);
|
|
29
32
|
}
|
|
30
33
|
function ColorSwatches({ product, onHover }) {
|
|
31
34
|
const optie = kleurOptie(product);
|
|
@@ -165,6 +168,7 @@ function ProductGridRender({ block, siteId }) {
|
|
|
165
168
|
else if (sort === "priceAsc") sorted.sort((a, b) => a.priceCents - b.priceCents);
|
|
166
169
|
else if (sort === "priceDesc") sorted.sort((a, b) => b.priceCents - a.priceCents);
|
|
167
170
|
else sorted.sort((a, b) => (a.order ?? 0) - (b.order ?? 0) || a.name.localeCompare(b.name, "nl"));
|
|
171
|
+
sorted.sort((a, b) => Number(isSoldOut(a)) - Number(isSoldOut(b)));
|
|
168
172
|
return sorted.slice(0, block.limit);
|
|
169
173
|
}, [products, activeCat, block.featuredOnly, block.limit, sort, filterColor, filterSize]);
|
|
170
174
|
if (products === null) return null;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../src/modules/shop/blocks/ProductGrid.tsx"],"sourcesContent":["\"use client\";\n\nimport { useEffect, useMemo, useState } from \"react\";\nimport type { z } from \"zod\";\nimport { formatCents } from \"../../../schema/shopGeld\";\nimport type { ProductGridBlockSchema } from \"../../../schema/shop\";\nimport { defineBlock, type BlockRenderProps } from \"../../../blocks/registry\";\nimport { useBlockData } from \"../../../blocks/context\";\nimport { Card, Heading, Text } from \"../../../blocks/ui\";\nimport { cartAdd } from \"../cartStore\";\nimport { useShopStrings, type ShopT } from \"../strings\";\nimport {\n fetchActiveProducts,\n fetchShopCategories,\n kleurNaarCss,\n kleurOptie,\n readQueryParam,\n useShopSettings,\n type StoredProduct,\n type StoredShopCategory,\n} from \"../lib\";\nimport type { ShopPageData } from \"../types\";\n\ntype Blk = z.infer<typeof ProductGridBlockSchema>;\n\n/** Prijsregel met optionele doorgestreepte \"van\"-prijs en \"vanaf\" bij variant-prijzen. */\nexport function PriceLine({ product, currency }: { product: StoredProduct; currency: string }) {\n const variantPrices = (product.variants ?? [])\n .filter((v) => v.priceCents !== undefined)\n .map((v) => v.priceCents as number);\n const min = Math.min(product.priceCents, ...variantPrices);\n const hasRange = variantPrices.some((p) => p !== product.priceCents);\n return (\n <span className=\"xicms-shop-price\">\n {product.compareAtCents !== undefined && product.compareAtCents > min && (\n <s style={{ opacity: 0.6, marginRight: 6 }}>{formatCents(product.compareAtCents, currency)}</s>\n )}\n {hasRange ? `vanaf ${formatCents(min, currency)}` : formatCents(min, currency)}\n </span>\n );\n}\n\n/** Uitverkocht (alleen betrouwbaar vast te stellen zonder opties: expliciete voorraad 0). */\nfunction isSoldOut(p: StoredProduct): boolean {\n return p.trackStock === true && (p.options ?? []).length === 0 && p.stock !== undefined && p.stock <= 0;\n}\n\n/** Stof-stipjes; hover wisselt de kaartfoto naar de per-kleur-afbeelding (als die er is). */\nfunction ColorSwatches({ product, onHover }: { product: StoredProduct; onHover?: (url: string | null) => void }) {\n const optie = kleurOptie(product);\n if (!optie) return null;\n const dots = optie.values\n .map((v) => ({ v, css: kleurNaarCss(v), img: optie.images?.[v] }))\n .filter((d): d is { v: string; css: string; img: string | undefined } => d.css !== null);\n if (dots.length === 0) return null;\n return (\n <span className=\"xicms-shop-card__swatches\" aria-label={`Kleuren: ${optie.values.join(\", \")}`}>\n {dots.map((d) => (\n <span\n key={d.v}\n className=\"xicms-shop-card__swatch\"\n title={d.v}\n onMouseEnter={d.img && onHover ? () => onHover(d.img ?? null) : undefined}\n onMouseLeave={d.img && onHover ? () => onHover(null) : undefined}\n style={{\n display: \"inline-block\",\n width: 12,\n height: 12,\n borderRadius: 999,\n background: d.css,\n boxShadow: \"inset 0 0 0 1px rgba(0,0,0,0.18)\",\n }}\n />\n ))}\n </span>\n );\n}\n\nexport function ProductCard({\n product,\n siteId,\n currency,\n detailPath,\n cartPath,\n t,\n filterColor,\n}: {\n product: StoredProduct;\n siteId: string;\n currency: string;\n detailPath: string;\n cartPath: string;\n t: ShopT;\n /** Actief kleurfilter uit het overzicht — toont bij voorkeur de foto van die kleur. */\n filterColor?: string | null;\n}) {\n const [added, setAdded] = useState(false);\n const [hoverImage, setHoverImage] = useState<string | null>(null);\n const href = `${detailPath}?p=${encodeURIComponent(product.slug)}`;\n const simple = (product.options ?? []).length === 0 && !(product.personalization ?? []).length;\n const soldOut = isSoldOut(product);\n const filterImage = filterColor ? kleurOptie(product)?.images?.[filterColor] : undefined;\n const image = hoverImage ?? filterImage ?? product.images?.[0];\n\n return (\n // Flex-kolom over de volle kaarthoogte: titel (vast 2 regels) → prijs → swatches →\n // knop onderaan. Zo lijnen prijs/swatches/knop over álle kaarten in de rij uit, ook\n // als een productnaam één of twee regels beslaat. Alle opmaak in styles.css.\n // `data-xicms-product` maakt de kaart in de bewerkmodus aanwijsbaar als PRODUCT (potlood voor\n // de naam, tandwiel voor het hele formulier). Op de cataloguspagina was dat er niet, en dus was\n // een productnaam daar het enige stuk tekst op de site dat je nergens kon aanklikken.\n <Card padding={0} className=\"xicms-shop-card\" data-xicms-product={product.id}>\n <a className=\"xicms-shop-card__link\" href={href}>\n <div className=\"xicms-shop-card__media\">\n {image ? (\n <img src={image} alt={product.name} loading=\"lazy\" className=\"xicms-shop-card__img\" />\n ) : (\n <div className=\"xicms-shop-card__placeholder\" aria-hidden=\"true\" />\n )}\n {soldOut && <span className=\"xicms-shop-card__soldout\">{t(\"soldOut\")}</span>}\n </div>\n <div className=\"xicms-shop-card__body\">\n <span className=\"xicms-shop-card__name\" data-xicms-product-edit=\"name\">{product.name}</span>\n <span className=\"xicms-shop-card__priceline\">\n <PriceLine product={product} currency={currency} />\n </span>\n <span className=\"xicms-shop-card__swatchrow\">\n <ColorSwatches product={product} onHover={setHoverImage} />\n </span>\n </div>\n </a>\n <div className=\"xicms-shop-card__actions\">\n {simple ? (\n added ? (\n <a className=\"xicms-shop-card__added\" href={cartPath}>\n {t(\"addedGoToCart\")}\n </a>\n ) : (\n <button\n type=\"button\"\n className=\"xicms-btn xicms-btn--primary\"\n disabled={soldOut}\n style={{ cursor: soldOut ? \"not-allowed\" : \"pointer\" }}\n onClick={() => {\n cartAdd(siteId, {\n productId: product.id,\n slug: product.slug,\n name: product.name,\n unitPriceCents: product.priceCents,\n imageUrl: product.images?.[0],\n });\n setAdded(true);\n }}\n >\n {t(\"addToCart\")}\n </button>\n )\n ) : (\n <a className=\"xicms-btn xicms-btn--secondary\" href={href}>\n {t(\"view\")}\n </a>\n )}\n </div>\n </Card>\n );\n}\n\nfunction ProductGridRender({ block, siteId }: BlockRenderProps<Blk>) {\n const settings = useShopSettings();\n const t = useShopStrings();\n const serverData = useBlockData<ShopPageData>(\"shop\");\n const [products, setProducts] = useState<StoredProduct[] | null>(serverData?.products ?? null);\n const [cats, setCats] = useState<StoredShopCategory[]>(serverData?.categories ?? []);\n const [queryCat, setQueryCat] = useState<string | null>(null);\n const [filterColor, setFilterColor] = useState<string | null>(null);\n const [filterSize, setFilterSize] = useState<string | null>(null);\n const [sort, setSort] = useState<Blk[\"sort\"]>(block.sort);\n\n useEffect(() => {\n // Server-data aanwezig (SSR/ISR)? Dan geen client-fetch — geen flits, minder reads.\n if (!serverData?.products) {\n void fetchActiveProducts(siteId).then(setProducts).catch((e) => {\n console.warn(\"[xicms] producten laden mislukt:\", e);\n setProducts([]);\n });\n }\n if (!serverData?.categories) {\n void fetchShopCategories(siteId).then(setCats).catch(() => setCats([]));\n }\n if (block.useQueryCategory && !block.categorySlug) setQueryCat(readQueryParam(\"cat\"));\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [siteId, block.useQueryCategory, block.categorySlug]);\n\n const activeSlug = block.categorySlug || queryCat || null;\n const activeCat = activeSlug ? cats.find((c) => c.slug === activeSlug) ?? null : null;\n\n /** Beschikbare filterwaarden uit de geladen set (client-side; geen extra reads). */\n const filterValues = useMemo(() => {\n if (!block.showFilters || !products) return { colors: [] as string[], sizes: [] as string[] };\n const colors = new Set<string>();\n const sizes = new Set<string>();\n for (const p of products) {\n for (const o of p.options ?? []) {\n if (/^kleur|^colou?r/i.test(o.name)) o.values.forEach((v) => colors.add(v));\n if (/^maat|^size/i.test(o.name)) o.values.forEach((v) => sizes.add(v));\n }\n }\n return { colors: [...colors], sizes: [...sizes].sort((a, b) => Number(a) - Number(b) || a.localeCompare(b)) };\n }, [products, block.showFilters]);\n\n const visible = useMemo(() => {\n if (!products) return [];\n let list = products;\n if (activeCat) list = list.filter((p) => (p.categoryIds ?? []).includes(activeCat.id));\n if (block.featuredOnly) list = list.filter((p) => p.featured);\n if (filterColor) {\n list = list.filter((p) =>\n (p.options ?? []).some((o) => /^kleur|^colou?r/i.test(o.name) && o.values.includes(filterColor)),\n );\n }\n if (filterSize) {\n list = list.filter((p) =>\n (p.options ?? []).some((o) => /^maat|^size/i.test(o.name) && o.values.includes(filterSize)),\n );\n }\n const sorted = [...list];\n if (sort === \"name\") sorted.sort((a, b) => a.name.localeCompare(b.name, \"nl\"));\n else if (sort === \"priceAsc\") sorted.sort((a, b) => a.priceCents - b.priceCents);\n else if (sort === \"priceDesc\") sorted.sort((a, b) => b.priceCents - a.priceCents);\n else sorted.sort((a, b) => (a.order ?? 0) - (b.order ?? 0) || a.name.localeCompare(b.name, \"nl\"));\n return sorted.slice(0, block.limit);\n }, [products, activeCat, block.featuredOnly, block.limit, sort, filterColor, filterSize]);\n\n if (products === null) return null;\n\n const chip = (label: string, active: boolean, onClick: () => void, swatch?: string | null) => (\n <button\n key={label}\n type=\"button\"\n className={`xicms-shop-filterchip${active ? \" xicms-shop-filterchip--active\" : \"\"}`}\n aria-pressed={active}\n onClick={onClick}\n >\n {swatch && (\n <span\n aria-hidden\n style={{ display: \"inline-block\", width: 10, height: 10, borderRadius: 999, background: swatch, boxShadow: \"inset 0 0 0 1px rgba(0,0,0,0.18)\", marginRight: 5 }}\n />\n )}\n {label}\n </button>\n );\n\n return (\n <div className=\"xicms-shop-grid\">\n {(block.heading || activeCat) && (\n <Heading level={2} data-xicms-edit=\"heading\">\n {activeCat?.name ?? block.heading}\n </Heading>\n )}\n {activeCat && (\n <Text size=\"sm\" display=\"block\" style={{ marginTop: 4 }}>\n <a href={settings.shopPath}>← {t(\"allCategories\")}</a>\n </Text>\n )}\n\n {block.showFilters && (filterValues.colors.length > 0 || filterValues.sizes.length > 0) && (\n <div className=\"xicms-shop-filters\">\n {filterValues.colors.length > 0 && (\n <div className=\"xicms-shop-filters__row\" role=\"group\" aria-label=\"Kleur\">\n {chip(t(\"filterAll\"), filterColor === null, () => setFilterColor(null))}\n {filterValues.colors.map((c) =>\n chip(c, filterColor === c, () => setFilterColor(filterColor === c ? null : c), kleurNaarCss(c)),\n )}\n </div>\n )}\n {filterValues.sizes.length > 0 && (\n <div className=\"xicms-shop-filters__row\" role=\"group\" aria-label=\"Maat\">\n {chip(t(\"filterAll\"), filterSize === null, () => setFilterSize(null))}\n {filterValues.sizes.map((s) => chip(s, filterSize === s, () => setFilterSize(filterSize === s ? null : s)))}\n </div>\n )}\n <label className=\"xicms-shop-filters__sort\">\n <span>{t(\"sortLabel\")}</span>\n <select className=\"xicms-input\" value={sort} onChange={(e) => setSort(e.target.value as Blk[\"sort\"])}>\n <option value=\"order\">{t(\"sortDefault\")}</option>\n <option value=\"name\">{t(\"sortName\")}</option>\n <option value=\"priceAsc\">{t(\"sortPriceAsc\")}</option>\n <option value=\"priceDesc\">{t(\"sortPriceDesc\")}</option>\n </select>\n </label>\n </div>\n )}\n\n {visible.length === 0 ? (\n <Card padding={4} variant=\"muted\" style={{ marginTop: \"1rem\" }}>\n <Text>{t(\"noProducts\")}</Text>\n </Card>\n ) : (\n <div\n className=\"xicms-shop-grid__items\"\n data-columns={block.columns}\n style={{\n display: \"grid\",\n gridTemplateColumns: `repeat(auto-fill, minmax(min(240px, calc(100%/${block.columns} - 1rem)), 1fr))`,\n gap: \"1.25rem\",\n marginTop: \"1.25rem\",\n }}\n >\n {visible.map((p) => (\n <ProductCard\n key={p.id}\n product={p}\n siteId={siteId}\n currency={settings.currency}\n detailPath={settings.detailPath}\n cartPath={settings.cartPath}\n t={t}\n filterColor={filterColor}\n />\n ))}\n </div>\n )}\n </div>\n );\n}\n\nexport const productGridBlock = defineBlock({\n type: \"productGrid\",\n Render: ProductGridRender,\n label: \"Producten-overzicht\",\n icon: \"🛍️\",\n defaults: () => ({ useQueryCategory: true, columns: 3, limit: 60, sort: \"order\" }),\n});\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../../../src/modules/shop/blocks/ProductGrid.tsx"],"sourcesContent":["\"use client\";\n\nimport { useEffect, useMemo, useState } from \"react\";\nimport type { z } from \"zod\";\nimport { formatCents } from \"../../../schema/shopGeld\";\nimport type { ProductGridBlockSchema } from \"../../../schema/shop\";\nimport { defineBlock, type BlockRenderProps } from \"../../../blocks/registry\";\nimport { useBlockData } from \"../../../blocks/context\";\nimport { Card, Heading, Text } from \"../../../blocks/ui\";\nimport { cartAdd } from \"../cartStore\";\nimport { useShopStrings, type ShopT } from \"../strings\";\nimport {\n fetchActiveProducts,\n fetchShopCategories,\n kleurNaarCss,\n kleurOptie,\n readQueryParam,\n useShopSettings,\n type StoredProduct,\n type StoredShopCategory,\n} from \"../lib\";\nimport type { ShopPageData } from \"../types\";\n\ntype Blk = z.infer<typeof ProductGridBlockSchema>;\n\n/** Prijsregel met optionele doorgestreepte \"van\"-prijs en \"vanaf\" bij variant-prijzen. */\nexport function PriceLine({ product, currency }: { product: StoredProduct; currency: string }) {\n const variantPrices = (product.variants ?? [])\n .filter((v) => v.priceCents !== undefined)\n .map((v) => v.priceCents as number);\n const min = Math.min(product.priceCents, ...variantPrices);\n const hasRange = variantPrices.some((p) => p !== product.priceCents);\n return (\n <span className=\"xicms-shop-price\">\n {product.compareAtCents !== undefined && product.compareAtCents > min && (\n <s style={{ opacity: 0.6, marginRight: 6 }}>{formatCents(product.compareAtCents, currency)}</s>\n )}\n {hasRange ? `vanaf ${formatCents(min, currency)}` : formatCents(min, currency)}\n </span>\n );\n}\n\n/**\n * Uitverkocht. Zonder opties: de voorraad van het product zelf. MÉT opties: pas uitverkocht als\n * élke variant met een expliciete voorraad op 0 staat — dat was eerder helemaal niet af te lezen,\n * waardoor een volledig uitverkocht t-shirt gewoon als beschikbaar in het grid stond.\n */\nfunction isSoldOut(p: StoredProduct): boolean {\n if (p.trackStock !== true) return false;\n if ((p.options ?? []).length === 0) return p.stock !== undefined && p.stock <= 0;\n const metVoorraad = (p.variants ?? []).filter((v) => v.stock !== undefined && v.active !== false);\n return metVoorraad.length > 0 && metVoorraad.every((v) => (v.stock as number) <= 0);\n}\n\n/** Stof-stipjes; hover wisselt de kaartfoto naar de per-kleur-afbeelding (als die er is). */\nfunction ColorSwatches({ product, onHover }: { product: StoredProduct; onHover?: (url: string | null) => void }) {\n const optie = kleurOptie(product);\n if (!optie) return null;\n const dots = optie.values\n .map((v) => ({ v, css: kleurNaarCss(v), img: optie.images?.[v] }))\n .filter((d): d is { v: string; css: string; img: string | undefined } => d.css !== null);\n if (dots.length === 0) return null;\n return (\n <span className=\"xicms-shop-card__swatches\" aria-label={`Kleuren: ${optie.values.join(\", \")}`}>\n {dots.map((d) => (\n <span\n key={d.v}\n className=\"xicms-shop-card__swatch\"\n title={d.v}\n onMouseEnter={d.img && onHover ? () => onHover(d.img ?? null) : undefined}\n onMouseLeave={d.img && onHover ? () => onHover(null) : undefined}\n style={{\n display: \"inline-block\",\n width: 12,\n height: 12,\n borderRadius: 999,\n background: d.css,\n boxShadow: \"inset 0 0 0 1px rgba(0,0,0,0.18)\",\n }}\n />\n ))}\n </span>\n );\n}\n\nexport function ProductCard({\n product,\n siteId,\n currency,\n detailPath,\n cartPath,\n t,\n filterColor,\n}: {\n product: StoredProduct;\n siteId: string;\n currency: string;\n detailPath: string;\n cartPath: string;\n t: ShopT;\n /** Actief kleurfilter uit het overzicht — toont bij voorkeur de foto van die kleur. */\n filterColor?: string | null;\n}) {\n const [added, setAdded] = useState(false);\n const [hoverImage, setHoverImage] = useState<string | null>(null);\n const href = `${detailPath}?p=${encodeURIComponent(product.slug)}`;\n const simple = (product.options ?? []).length === 0 && !(product.personalization ?? []).length;\n const soldOut = isSoldOut(product);\n const filterImage = filterColor ? kleurOptie(product)?.images?.[filterColor] : undefined;\n const image = hoverImage ?? filterImage ?? product.images?.[0];\n\n return (\n // Flex-kolom over de volle kaarthoogte: titel (vast 2 regels) → prijs → swatches →\n // knop onderaan. Zo lijnen prijs/swatches/knop over álle kaarten in de rij uit, ook\n // als een productnaam één of twee regels beslaat. Alle opmaak in styles.css.\n // `data-xicms-product` maakt de kaart in de bewerkmodus aanwijsbaar als PRODUCT (potlood voor\n // de naam, tandwiel voor het hele formulier). Op de cataloguspagina was dat er niet, en dus was\n // een productnaam daar het enige stuk tekst op de site dat je nergens kon aanklikken.\n <Card padding={0} className=\"xicms-shop-card\" data-xicms-product={product.id}>\n <a className=\"xicms-shop-card__link\" href={href}>\n <div className=\"xicms-shop-card__media\">\n {image ? (\n <img src={image} alt={product.name} loading=\"lazy\" className=\"xicms-shop-card__img\" />\n ) : (\n <div className=\"xicms-shop-card__placeholder\" aria-hidden=\"true\" />\n )}\n {soldOut && <span className=\"xicms-shop-card__soldout\">{t(\"soldOut\")}</span>}\n </div>\n <div className=\"xicms-shop-card__body\">\n <span className=\"xicms-shop-card__name\" data-xicms-product-edit=\"name\">{product.name}</span>\n <span className=\"xicms-shop-card__priceline\">\n <PriceLine product={product} currency={currency} />\n </span>\n <span className=\"xicms-shop-card__swatchrow\">\n <ColorSwatches product={product} onHover={setHoverImage} />\n </span>\n </div>\n </a>\n <div className=\"xicms-shop-card__actions\">\n {simple ? (\n added ? (\n <a className=\"xicms-shop-card__added\" href={cartPath}>\n {t(\"addedGoToCart\")}\n </a>\n ) : (\n <button\n type=\"button\"\n className=\"xicms-btn xicms-btn--primary\"\n disabled={soldOut}\n style={{ cursor: soldOut ? \"not-allowed\" : \"pointer\" }}\n onClick={() => {\n cartAdd(siteId, {\n productId: product.id,\n slug: product.slug,\n name: product.name,\n unitPriceCents: product.priceCents,\n imageUrl: product.images?.[0],\n });\n setAdded(true);\n }}\n >\n {t(\"addToCart\")}\n </button>\n )\n ) : (\n <a className=\"xicms-btn xicms-btn--secondary\" href={href}>\n {t(\"view\")}\n </a>\n )}\n </div>\n </Card>\n );\n}\n\nfunction ProductGridRender({ block, siteId }: BlockRenderProps<Blk>) {\n const settings = useShopSettings();\n const t = useShopStrings();\n const serverData = useBlockData<ShopPageData>(\"shop\");\n const [products, setProducts] = useState<StoredProduct[] | null>(serverData?.products ?? null);\n const [cats, setCats] = useState<StoredShopCategory[]>(serverData?.categories ?? []);\n const [queryCat, setQueryCat] = useState<string | null>(null);\n const [filterColor, setFilterColor] = useState<string | null>(null);\n const [filterSize, setFilterSize] = useState<string | null>(null);\n const [sort, setSort] = useState<Blk[\"sort\"]>(block.sort);\n\n useEffect(() => {\n // Server-data aanwezig (SSR/ISR)? Dan geen client-fetch — geen flits, minder reads.\n if (!serverData?.products) {\n void fetchActiveProducts(siteId).then(setProducts).catch((e) => {\n console.warn(\"[xicms] producten laden mislukt:\", e);\n setProducts([]);\n });\n }\n if (!serverData?.categories) {\n void fetchShopCategories(siteId).then(setCats).catch(() => setCats([]));\n }\n if (block.useQueryCategory && !block.categorySlug) setQueryCat(readQueryParam(\"cat\"));\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [siteId, block.useQueryCategory, block.categorySlug]);\n\n const activeSlug = block.categorySlug || queryCat || null;\n const activeCat = activeSlug ? cats.find((c) => c.slug === activeSlug) ?? null : null;\n\n /** Beschikbare filterwaarden uit de geladen set (client-side; geen extra reads). */\n const filterValues = useMemo(() => {\n if (!block.showFilters || !products) return { colors: [] as string[], sizes: [] as string[] };\n const colors = new Set<string>();\n const sizes = new Set<string>();\n for (const p of products) {\n for (const o of p.options ?? []) {\n if (/^kleur|^colou?r/i.test(o.name)) o.values.forEach((v) => colors.add(v));\n if (/^maat|^size/i.test(o.name)) o.values.forEach((v) => sizes.add(v));\n }\n }\n return { colors: [...colors], sizes: [...sizes].sort((a, b) => Number(a) - Number(b) || a.localeCompare(b)) };\n }, [products, block.showFilters]);\n\n const visible = useMemo(() => {\n if (!products) return [];\n let list = products;\n if (activeCat) list = list.filter((p) => (p.categoryIds ?? []).includes(activeCat.id));\n if (block.featuredOnly) list = list.filter((p) => p.featured);\n if (filterColor) {\n list = list.filter((p) =>\n (p.options ?? []).some((o) => /^kleur|^colou?r/i.test(o.name) && o.values.includes(filterColor)),\n );\n }\n if (filterSize) {\n list = list.filter((p) =>\n (p.options ?? []).some((o) => /^maat|^size/i.test(o.name) && o.values.includes(filterSize)),\n );\n }\n const sorted = [...list];\n if (sort === \"name\") sorted.sort((a, b) => a.name.localeCompare(b.name, \"nl\"));\n else if (sort === \"priceAsc\") sorted.sort((a, b) => a.priceCents - b.priceCents);\n else if (sort === \"priceDesc\") sorted.sort((a, b) => b.priceCents - a.priceCents);\n else sorted.sort((a, b) => (a.order ?? 0) - (b.order ?? 0) || a.name.localeCompare(b.name, \"nl\"));\n /**\n * Uitverkochte artikelen altijd achteraan, ongeacht de gekozen sortering: ze bleven op hun\n * plek staan en duwden zo leverbare producten van de eerste rij. De volgorde binnen elke\n * groep blijft die van de sortering hierboven — een stabiele sort verandert daar niets aan.\n */\n sorted.sort((a, b) => Number(isSoldOut(a)) - Number(isSoldOut(b)));\n return sorted.slice(0, block.limit);\n }, [products, activeCat, block.featuredOnly, block.limit, sort, filterColor, filterSize]);\n\n if (products === null) return null;\n\n const chip = (label: string, active: boolean, onClick: () => void, swatch?: string | null) => (\n <button\n key={label}\n type=\"button\"\n className={`xicms-shop-filterchip${active ? \" xicms-shop-filterchip--active\" : \"\"}`}\n aria-pressed={active}\n onClick={onClick}\n >\n {swatch && (\n <span\n aria-hidden\n style={{ display: \"inline-block\", width: 10, height: 10, borderRadius: 999, background: swatch, boxShadow: \"inset 0 0 0 1px rgba(0,0,0,0.18)\", marginRight: 5 }}\n />\n )}\n {label}\n </button>\n );\n\n return (\n <div className=\"xicms-shop-grid\">\n {(block.heading || activeCat) && (\n <Heading level={2} data-xicms-edit=\"heading\">\n {activeCat?.name ?? block.heading}\n </Heading>\n )}\n {activeCat && (\n <Text size=\"sm\" display=\"block\" style={{ marginTop: 4 }}>\n <a href={settings.shopPath}>← {t(\"allCategories\")}</a>\n </Text>\n )}\n\n {block.showFilters && (filterValues.colors.length > 0 || filterValues.sizes.length > 0) && (\n <div className=\"xicms-shop-filters\">\n {filterValues.colors.length > 0 && (\n <div className=\"xicms-shop-filters__row\" role=\"group\" aria-label=\"Kleur\">\n {chip(t(\"filterAll\"), filterColor === null, () => setFilterColor(null))}\n {filterValues.colors.map((c) =>\n chip(c, filterColor === c, () => setFilterColor(filterColor === c ? null : c), kleurNaarCss(c)),\n )}\n </div>\n )}\n {filterValues.sizes.length > 0 && (\n <div className=\"xicms-shop-filters__row\" role=\"group\" aria-label=\"Maat\">\n {chip(t(\"filterAll\"), filterSize === null, () => setFilterSize(null))}\n {filterValues.sizes.map((s) => chip(s, filterSize === s, () => setFilterSize(filterSize === s ? null : s)))}\n </div>\n )}\n <label className=\"xicms-shop-filters__sort\">\n <span>{t(\"sortLabel\")}</span>\n <select className=\"xicms-input\" value={sort} onChange={(e) => setSort(e.target.value as Blk[\"sort\"])}>\n <option value=\"order\">{t(\"sortDefault\")}</option>\n <option value=\"name\">{t(\"sortName\")}</option>\n <option value=\"priceAsc\">{t(\"sortPriceAsc\")}</option>\n <option value=\"priceDesc\">{t(\"sortPriceDesc\")}</option>\n </select>\n </label>\n </div>\n )}\n\n {visible.length === 0 ? (\n <Card padding={4} variant=\"muted\" style={{ marginTop: \"1rem\" }}>\n <Text>{t(\"noProducts\")}</Text>\n </Card>\n ) : (\n <div\n className=\"xicms-shop-grid__items\"\n data-columns={block.columns}\n style={{\n display: \"grid\",\n gridTemplateColumns: `repeat(auto-fill, minmax(min(240px, calc(100%/${block.columns} - 1rem)), 1fr))`,\n gap: \"1.25rem\",\n marginTop: \"1.25rem\",\n }}\n >\n {visible.map((p) => (\n <ProductCard\n key={p.id}\n product={p}\n siteId={siteId}\n currency={settings.currency}\n detailPath={settings.detailPath}\n cartPath={settings.cartPath}\n t={t}\n filterColor={filterColor}\n />\n ))}\n </div>\n )}\n </div>\n );\n}\n\nexport const productGridBlock = defineBlock({\n type: \"productGrid\",\n Render: ProductGridRender,\n label: \"Producten-overzicht\",\n icon: \"🛍️\",\n defaults: () => ({ useQueryCategory: true, columns: 3, limit: 60, sort: \"order\" }),\n});\n"],"mappings":"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","names":[]}
|
|
@@ -15,13 +15,22 @@ export declare const ShopSettingsSchema: z.ZodMiniObject<{
|
|
|
15
15
|
currency: z.ZodMiniDefault<z.ZodMiniString<string>>;
|
|
16
16
|
shippingFlatCents: z.ZodMiniDefault<z.ZodMiniNumberFormat>;
|
|
17
17
|
shippingFreeFromCents: z.ZodMiniDefault<z.ZodMiniNumberFormat>;
|
|
18
|
+
shippingZones: z.ZodMiniDefault<z.ZodMiniArray<z.ZodMiniObject<{
|
|
19
|
+
naam: z.ZodMiniString<string>;
|
|
20
|
+
landen: z.ZodMiniDefault<z.ZodMiniArray<z.ZodMiniString<string>>>;
|
|
21
|
+
flatCents: z.ZodMiniDefault<z.ZodMiniNumberFormat>;
|
|
22
|
+
freeFromCents: z.ZodMiniDefault<z.ZodMiniNumberFormat>;
|
|
23
|
+
}, z.core.$strip>>>;
|
|
18
24
|
orderPrefix: z.ZodMiniDefault<z.ZodMiniString<string>>;
|
|
25
|
+
invoicePrefix: z.ZodMiniDefault<z.ZodMiniString<string>>;
|
|
19
26
|
businessName: z.ZodMiniDefault<z.ZodMiniString<string>>;
|
|
20
27
|
businessAddress: z.ZodMiniDefault<z.ZodMiniString<string>>;
|
|
21
28
|
businessZip: z.ZodMiniDefault<z.ZodMiniString<string>>;
|
|
22
29
|
businessCity: z.ZodMiniDefault<z.ZodMiniString<string>>;
|
|
23
30
|
businessKvk: z.ZodMiniDefault<z.ZodMiniString<string>>;
|
|
24
31
|
businessBtw: z.ZodMiniDefault<z.ZodMiniString<string>>;
|
|
32
|
+
businessEmail: z.ZodMiniDefault<z.ZodMiniString<string>>;
|
|
33
|
+
businessPhone: z.ZodMiniDefault<z.ZodMiniString<string>>;
|
|
25
34
|
}, z.core.$strip>;
|
|
26
35
|
export type ShopSettings = z.infer<typeof ShopSettingsSchema>;
|
|
27
36
|
export declare const defaultShopSettings: ShopSettings;
|
|
@@ -8,14 +8,40 @@ const ShopSettingsSchema = z.object({
|
|
|
8
8
|
currency: z._default(z.string().check(z.minLength(3)).check(z.maxLength(3)), "EUR").register(z.globalRegistry, { description: "Valuta (ISO-code)" }),
|
|
9
9
|
shippingFlatCents: z._default(z.int().check(z.minimum(0)).check(z.maximum(1e5)), 495).register(z.globalRegistry, { description: "Verzendkosten (centen)" }),
|
|
10
10
|
shippingFreeFromCents: z._default(z.int().check(z.minimum(0)).check(z.maximum(1e8)), 0).register(z.globalRegistry, { description: "Gratis verzending vanaf (centen, 0 = nooit)" }),
|
|
11
|
+
/**
|
|
12
|
+
* Verzendzones (7.20): tarief per (groep) land(en). Leeg = het enkele tarief hierboven voor
|
|
13
|
+
* alles — het gedrag van vóór 7.20, dus bestaande winkels merken niets. Een zone ZONDER landen
|
|
14
|
+
* is de "overige landen"-zone; staat die er niet, dan verzendt de winkel alleen naar de landen
|
|
15
|
+
* die wél in een zone staan (en toont de checkout precies die keuze).
|
|
16
|
+
*/
|
|
17
|
+
shippingZones: z._default(z.array(z.object({
|
|
18
|
+
naam: z.string().check(z.minLength(1)).check(z.maxLength(60)),
|
|
19
|
+
landen: z._default(z.array(z.string().check(z.maxLength(2))).check(z.maxLength(60)), []),
|
|
20
|
+
flatCents: z._default(z.int().check(z.minimum(0)).check(z.maximum(1e5)), 0),
|
|
21
|
+
freeFromCents: z._default(z.int().check(z.minimum(0)).check(z.maximum(1e8)), 0)
|
|
22
|
+
})).check(z.maxLength(10)), []).register(z.globalRegistry, { description: "Verzendzones (tarief per land)" }),
|
|
11
23
|
orderPrefix: z._default(z.string().check(z.maxLength(12)), "ORDER").register(z.globalRegistry, { description: "Ordernummer-prefix" }),
|
|
24
|
+
/**
|
|
25
|
+
* Prefix van de FACTUURreeks (7.18). Leeg = dezelfde als het ordernummer. De factuurreeks is
|
|
26
|
+
* een eigen doorlopende nummering die alleen bij betaling ophoogt en per jaar herstart —
|
|
27
|
+
* het ordernummer mag gaten hebben (afgebroken checkouts), een factuurreeks niet.
|
|
28
|
+
*/
|
|
29
|
+
invoicePrefix: z._default(z.string().check(z.maxLength(12)), "").register(z.globalRegistry, { description: "Factuurnummer-prefix (leeg = zelfde als ordernummer)" }),
|
|
12
30
|
/* Bedrijfsgegevens — komen op de factuur-PDF (5.6). */
|
|
13
31
|
businessName: z._default(z.string().check(z.maxLength(120)), "").register(z.globalRegistry, { description: "Bedrijfsnaam (factuur)" }),
|
|
14
32
|
businessAddress: z._default(z.string().check(z.maxLength(200)), "").register(z.globalRegistry, { description: "Adres (factuur)" }),
|
|
15
33
|
businessZip: z._default(z.string().check(z.maxLength(20)), "").register(z.globalRegistry, { description: "Postcode (factuur)" }),
|
|
16
34
|
businessCity: z._default(z.string().check(z.maxLength(120)), "").register(z.globalRegistry, { description: "Plaats (factuur)" }),
|
|
17
35
|
businessKvk: z._default(z.string().check(z.maxLength(20)), "").register(z.globalRegistry, { description: "KVK-nummer (factuur)" }),
|
|
18
|
-
businessBtw: z._default(z.string().check(z.maxLength(30)), "").register(z.globalRegistry, { description: "Btw-nummer (factuur)" })
|
|
36
|
+
businessBtw: z._default(z.string().check(z.maxLength(30)), "").register(z.globalRegistry, { description: "Btw-nummer (factuur)" }),
|
|
37
|
+
/**
|
|
38
|
+
* Contactgegevens in de mailfooter en op de factuur (7.21). `businessEmail` werd al door
|
|
39
|
+
* `mailLayout.ts` en `invoice.ts` UITGELEZEN, maar bestond nergens als instelling — een dood
|
|
40
|
+
* veld op drie plekken, waardoor de klant in zijn bestelbevestiging geen enkel adres zag om
|
|
41
|
+
* op te reageren.
|
|
42
|
+
*/
|
|
43
|
+
businessEmail: z._default(z.string().check(z.maxLength(200)), "").register(z.globalRegistry, { description: "Contact-e-mail (factuur + mailfooter)" }),
|
|
44
|
+
businessPhone: z._default(z.string().check(z.maxLength(40)), "").register(z.globalRegistry, { description: "Telefoon (factuur + mailfooter)" })
|
|
19
45
|
});
|
|
20
46
|
const defaultShopSettings = ShopSettingsSchema.parse({});
|
|
21
47
|
export {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/modules/shop/settings.ts"],"sourcesContent":["import * as z from \"zod/mini\";\n\n/**\n * Publieke shop-instellingen (per site, via `settings/public.modules.shop.config` of\n * `SiteConfig.modules.shop.config`). Server-safe: alleen Zod. De SettingsPanel rendert hier\n * automatisch een formulier voor (moduleSettingsForm); de Functions lezen dezelfde config\n * server-side. Geheimen (Stripe-key, SMTP) horen hier NIET — die staan in\n * `sites/{siteId}/private/shop` en lopen via callables.\n */\nexport const ShopSettingsSchema = z.object({\n shopPath: z._default(z.string().check(z.maxLength(200)), \"/webwinkel\").register(z.globalRegistry, { description: \"Pad van de winkelpagina\" }),\n detailPath: z._default(z.string().check(z.maxLength(200)), \"/product\").register(z.globalRegistry, { description: \"Pad van de productpagina\" }),\n cartPath: z._default(z.string().check(z.maxLength(200)), \"/winkelwagen\").register(z.globalRegistry, { description: \"Pad van de winkelwagen\" }),\n checkoutPath: z._default(z.string().check(z.maxLength(200)), \"/afrekenen\").register(z.globalRegistry, { description: \"Pad van de afrekenpagina\" }),\n successPath: z._default(z.string().check(z.maxLength(200)), \"/bedankt\").register(z.globalRegistry, { description: \"Pad van de bedankt-pagina\" }),\n currency: z._default(z.string().check(z.minLength(3)).check(z.maxLength(3)), \"EUR\").register(z.globalRegistry, { description: \"Valuta (ISO-code)\" }),\n shippingFlatCents: z._default(z.int().check(z.minimum(0)).check(z.maximum(100_000)), 495).register(z.globalRegistry, { description: \"Verzendkosten (centen)\" }),\n shippingFreeFromCents: z._default(z.int().check(z.minimum(0)).check(z.maximum(1_000_000_00)), 0).register(z.globalRegistry, { description: \"Gratis verzending vanaf (centen, 0 = nooit)\" }),\n orderPrefix: z._default(z.string().check(z.maxLength(12)), \"ORDER\").register(z.globalRegistry, { description: \"Ordernummer-prefix\" }),\n /* Bedrijfsgegevens — komen op de factuur-PDF (5.6). */\n businessName: z._default(z.string().check(z.maxLength(120)), \"\").register(z.globalRegistry, { description: \"Bedrijfsnaam (factuur)\" }),\n businessAddress: z._default(z.string().check(z.maxLength(200)), \"\").register(z.globalRegistry, { description: \"Adres (factuur)\" }),\n businessZip: z._default(z.string().check(z.maxLength(20)), \"\").register(z.globalRegistry, { description: \"Postcode (factuur)\" }),\n businessCity: z._default(z.string().check(z.maxLength(120)), \"\").register(z.globalRegistry, { description: \"Plaats (factuur)\" }),\n businessKvk: z._default(z.string().check(z.maxLength(20)), \"\").register(z.globalRegistry, { description: \"KVK-nummer (factuur)\" }),\n businessBtw: z._default(z.string().check(z.maxLength(30)), \"\").register(z.globalRegistry, { description: \"Btw-nummer (factuur)\" }),\n});\nexport type ShopSettings = z.infer<typeof ShopSettingsSchema>;\n\nexport const defaultShopSettings: ShopSettings = ShopSettingsSchema.parse({});\n"],"mappings":"AAAA,YAAY,OAAO;AASZ,MAAM,qBAAqB,EAAE,OAAO;AAAA,EACzC,UAAU,EAAE,SAAS,EAAE,OAAO,EAAE,MAAM,EAAE,UAAU,GAAG,CAAC,GAAG,YAAY,EAAE,SAAS,EAAE,gBAAgB,EAAE,aAAa,0BAA0B,CAAC;AAAA,EAC5I,YAAY,EAAE,SAAS,EAAE,OAAO,EAAE,MAAM,EAAE,UAAU,GAAG,CAAC,GAAG,UAAU,EAAE,SAAS,EAAE,gBAAgB,EAAE,aAAa,2BAA2B,CAAC;AAAA,EAC7I,UAAU,EAAE,SAAS,EAAE,OAAO,EAAE,MAAM,EAAE,UAAU,GAAG,CAAC,GAAG,cAAc,EAAE,SAAS,EAAE,gBAAgB,EAAE,aAAa,yBAAyB,CAAC;AAAA,EAC7I,cAAc,EAAE,SAAS,EAAE,OAAO,EAAE,MAAM,EAAE,UAAU,GAAG,CAAC,GAAG,YAAY,EAAE,SAAS,EAAE,gBAAgB,EAAE,aAAa,2BAA2B,CAAC;AAAA,EACjJ,aAAa,EAAE,SAAS,EAAE,OAAO,EAAE,MAAM,EAAE,UAAU,GAAG,CAAC,GAAG,UAAU,EAAE,SAAS,EAAE,gBAAgB,EAAE,aAAa,4BAA4B,CAAC;AAAA,EAC/I,UAAU,EAAE,SAAS,EAAE,OAAO,EAAE,MAAM,EAAE,UAAU,CAAC,CAAC,EAAE,MAAM,EAAE,UAAU,CAAC,CAAC,GAAG,KAAK,EAAE,SAAS,EAAE,gBAAgB,EAAE,aAAa,oBAAoB,CAAC;AAAA,EACnJ,mBAAmB,EAAE,SAAS,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,CAAC,CAAC,EAAE,MAAM,EAAE,QAAQ,GAAO,CAAC,GAAG,GAAG,EAAE,SAAS,EAAE,gBAAgB,EAAE,aAAa,yBAAyB,CAAC;AAAA,EAC9J,uBAAuB,EAAE,SAAS,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,CAAC,CAAC,EAAE,MAAM,EAAE,QAAQ,GAAY,CAAC,GAAG,CAAC,EAAE,SAAS,EAAE,gBAAgB,EAAE,aAAa,8CAA8C,CAAC;AAAA,
|
|
1
|
+
{"version":3,"sources":["../../../src/modules/shop/settings.ts"],"sourcesContent":["import * as z from \"zod/mini\";\n\n/**\n * Publieke shop-instellingen (per site, via `settings/public.modules.shop.config` of\n * `SiteConfig.modules.shop.config`). Server-safe: alleen Zod. De SettingsPanel rendert hier\n * automatisch een formulier voor (moduleSettingsForm); de Functions lezen dezelfde config\n * server-side. Geheimen (Stripe-key, SMTP) horen hier NIET — die staan in\n * `sites/{siteId}/private/shop` en lopen via callables.\n */\nexport const ShopSettingsSchema = z.object({\n shopPath: z._default(z.string().check(z.maxLength(200)), \"/webwinkel\").register(z.globalRegistry, { description: \"Pad van de winkelpagina\" }),\n detailPath: z._default(z.string().check(z.maxLength(200)), \"/product\").register(z.globalRegistry, { description: \"Pad van de productpagina\" }),\n cartPath: z._default(z.string().check(z.maxLength(200)), \"/winkelwagen\").register(z.globalRegistry, { description: \"Pad van de winkelwagen\" }),\n checkoutPath: z._default(z.string().check(z.maxLength(200)), \"/afrekenen\").register(z.globalRegistry, { description: \"Pad van de afrekenpagina\" }),\n successPath: z._default(z.string().check(z.maxLength(200)), \"/bedankt\").register(z.globalRegistry, { description: \"Pad van de bedankt-pagina\" }),\n currency: z._default(z.string().check(z.minLength(3)).check(z.maxLength(3)), \"EUR\").register(z.globalRegistry, { description: \"Valuta (ISO-code)\" }),\n shippingFlatCents: z._default(z.int().check(z.minimum(0)).check(z.maximum(100_000)), 495).register(z.globalRegistry, { description: \"Verzendkosten (centen)\" }),\n shippingFreeFromCents: z._default(z.int().check(z.minimum(0)).check(z.maximum(1_000_000_00)), 0).register(z.globalRegistry, { description: \"Gratis verzending vanaf (centen, 0 = nooit)\" }),\n /**\n * Verzendzones (7.20): tarief per (groep) land(en). Leeg = het enkele tarief hierboven voor\n * alles — het gedrag van vóór 7.20, dus bestaande winkels merken niets. Een zone ZONDER landen\n * is de \"overige landen\"-zone; staat die er niet, dan verzendt de winkel alleen naar de landen\n * die wél in een zone staan (en toont de checkout precies die keuze).\n */\n shippingZones: z._default(z.array(z.object({\n naam: z.string().check(z.minLength(1)).check(z.maxLength(60)),\n landen: z._default(z.array(z.string().check(z.maxLength(2))).check(z.maxLength(60)), []),\n flatCents: z._default(z.int().check(z.minimum(0)).check(z.maximum(100_000)), 0),\n freeFromCents: z._default(z.int().check(z.minimum(0)).check(z.maximum(1_000_000_00)), 0),\n })).check(z.maxLength(10)), []).register(z.globalRegistry, { description: \"Verzendzones (tarief per land)\" }),\n orderPrefix: z._default(z.string().check(z.maxLength(12)), \"ORDER\").register(z.globalRegistry, { description: \"Ordernummer-prefix\" }),\n /**\n * Prefix van de FACTUURreeks (7.18). Leeg = dezelfde als het ordernummer. De factuurreeks is\n * een eigen doorlopende nummering die alleen bij betaling ophoogt en per jaar herstart —\n * het ordernummer mag gaten hebben (afgebroken checkouts), een factuurreeks niet.\n */\n invoicePrefix: z._default(z.string().check(z.maxLength(12)), \"\").register(z.globalRegistry, { description: \"Factuurnummer-prefix (leeg = zelfde als ordernummer)\" }),\n /* Bedrijfsgegevens — komen op de factuur-PDF (5.6). */\n businessName: z._default(z.string().check(z.maxLength(120)), \"\").register(z.globalRegistry, { description: \"Bedrijfsnaam (factuur)\" }),\n businessAddress: z._default(z.string().check(z.maxLength(200)), \"\").register(z.globalRegistry, { description: \"Adres (factuur)\" }),\n businessZip: z._default(z.string().check(z.maxLength(20)), \"\").register(z.globalRegistry, { description: \"Postcode (factuur)\" }),\n businessCity: z._default(z.string().check(z.maxLength(120)), \"\").register(z.globalRegistry, { description: \"Plaats (factuur)\" }),\n businessKvk: z._default(z.string().check(z.maxLength(20)), \"\").register(z.globalRegistry, { description: \"KVK-nummer (factuur)\" }),\n businessBtw: z._default(z.string().check(z.maxLength(30)), \"\").register(z.globalRegistry, { description: \"Btw-nummer (factuur)\" }),\n /**\n * Contactgegevens in de mailfooter en op de factuur (7.21). `businessEmail` werd al door\n * `mailLayout.ts` en `invoice.ts` UITGELEZEN, maar bestond nergens als instelling — een dood\n * veld op drie plekken, waardoor de klant in zijn bestelbevestiging geen enkel adres zag om\n * op te reageren.\n */\n businessEmail: z._default(z.string().check(z.maxLength(200)), \"\").register(z.globalRegistry, { description: \"Contact-e-mail (factuur + mailfooter)\" }),\n businessPhone: z._default(z.string().check(z.maxLength(40)), \"\").register(z.globalRegistry, { description: \"Telefoon (factuur + mailfooter)\" }),\n});\nexport type ShopSettings = z.infer<typeof ShopSettingsSchema>;\n\nexport const defaultShopSettings: ShopSettings = ShopSettingsSchema.parse({});\n"],"mappings":"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","names":[]}
|
|
@@ -36,6 +36,8 @@ const NL = {
|
|
|
36
36
|
fieldAddress: "Straat + huisnummer",
|
|
37
37
|
fieldZip: "Postcode",
|
|
38
38
|
fieldCity: "Plaats",
|
|
39
|
+
fieldCountry: "Land",
|
|
40
|
+
shippingFromCountry: "(bij bezorging in Nederland \u2014 kies je land bij het afrekenen)",
|
|
39
41
|
fieldNote: "Opmerking (optioneel)",
|
|
40
42
|
choose: "Kies\u2026",
|
|
41
43
|
discountCode: "Kortingscode",
|
|
@@ -96,6 +98,8 @@ const EN = {
|
|
|
96
98
|
fieldAddress: "Street + number",
|
|
97
99
|
fieldZip: "Postal code",
|
|
98
100
|
fieldCity: "City",
|
|
101
|
+
fieldCountry: "Country",
|
|
102
|
+
shippingFromCountry: "(for delivery in the Netherlands \u2014 choose your country at checkout)",
|
|
99
103
|
fieldNote: "Note (optional)",
|
|
100
104
|
choose: "Choose\u2026",
|
|
101
105
|
discountCode: "Discount code",
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/modules/shop/strings.ts"],"sourcesContent":["\"use client\";\n\n/**\n * UI-teksten van de shop-blocks, gelokaliseerd op `SiteConfig.lang` (nl = default, en).\n * Interpolatie via `{naam}`-placeholders. Bewust een platte tabel — geen i18n-framework;\n * de shop-strings zijn de enige plek in de blocks met vaste UI-tekst.\n */\nimport { useSiteConfig } from \"../../config\";\n\nconst NL = {\n addToCart: \"In winkelwagen\",\n added: \"✓ Toegevoegd aan je winkelwagen.\",\n addedGoToCart: \"✓ In winkelwagen — bekijk\",\n view: \"Bekijk\",\n toCart: \"Naar winkelwagen\",\n continueShopping: \"Verder winkelen\",\n checkout: \"Afrekenen\",\n cartEmpty: \"Je winkelwagen is leeg.\",\n toShop: \"Naar de winkel\",\n subtotal: \"Subtotaal\",\n shippingCosts: \"Verzendkosten\",\n free: \"gratis\",\n total: \"Totaal\",\n perPiece: \"p/st\",\n qty: \"Aantal\",\n outOfStock: \"Uitverkocht in deze uitvoering.\",\n variantUnavailable: \"Deze combinatie is niet leverbaar.\",\n stockLeft: \"Nog {n} beschikbaar\",\n soldOut: \"Uitverkocht\",\n productNotFound: \"Product niet gevonden.\",\n backToShop: \"Terug naar de winkel\",\n noProducts: \"Geen producten gevonden.\",\n noCategories: \"Nog geen categorieën.\",\n allCategories: \"Alle categorieën\",\n pay: \"Betalen — {bedrag}\",\n busy: \"Bezig…\",\n checkoutFailed: \"Afrekenen is niet gelukt. Controleer je gegevens en probeer het opnieuw.\",\n secureNote: \"Je rekent veilig af via een beveiligde betaalpagina (o.a. iDEAL). Na betaling ontvang je een bevestiging per e-mail.\",\n yourOrder: \"Je bestelling\",\n fieldName: \"Naam\",\n fieldEmail: \"E-mail\",\n fieldPhone: \"Telefoon (optioneel)\",\n fieldAddress: \"Straat + huisnummer\",\n fieldZip: \"Postcode\",\n fieldCity: \"Plaats\",\n fieldNote: \"Opmerking (optioneel)\",\n choose: \"Kies…\",\n discountCode: \"Kortingscode\",\n discountApply: \"Toepassen\",\n discount: \"Korting\",\n discountInvalid: \"Deze code is niet (meer) geldig.\",\n discountApplied: \"Korting toegepast: {code}\",\n related: \"Ook leuk\",\n filterAll: \"Alles\",\n sortLabel: \"Sorteren\",\n sortDefault: \"Aanbevolen\",\n sortName: \"Naam\",\n sortPriceAsc: \"Prijs (laag-hoog)\",\n sortPriceDesc: \"Prijs (hoog-laag)\",\n personalizationUpload: \"Kies bestand…\",\n personalizationUploading: \"Uploaden…\",\n personalizationUploaded: \"✓ Geüpload\",\n personalizationTooBig: \"Bestand te groot (max 5 MB).\",\n personalizationFailed: \"Uploaden mislukt — probeer het opnieuw.\",\n personalizationSurcharge: \"+ {bedrag}\",\n donationFailed: \"Doneren is niet gelukt. Probeer het later opnieuw.\",\n donationCustomAmount: \"Ander bedrag\",\n donationAmountLabel: \"Bedrag in euro's (min. {min})\",\n};\n\nconst EN: typeof NL = {\n addToCart: \"Add to cart\",\n added: \"✓ Added to your cart.\",\n addedGoToCart: \"✓ In cart — view\",\n view: \"View\",\n toCart: \"Go to cart\",\n continueShopping: \"Continue shopping\",\n checkout: \"Checkout\",\n cartEmpty: \"Your cart is empty.\",\n toShop: \"Go to the shop\",\n subtotal: \"Subtotal\",\n shippingCosts: \"Shipping\",\n free: \"free\",\n total: \"Total\",\n perPiece: \"each\",\n qty: \"Quantity\",\n outOfStock: \"Sold out in this variant.\",\n variantUnavailable: \"This combination is unavailable.\",\n stockLeft: \"Only {n} left\",\n soldOut: \"Sold out\",\n productNotFound: \"Product not found.\",\n backToShop: \"Back to the shop\",\n noProducts: \"No products found.\",\n noCategories: \"No categories yet.\",\n allCategories: \"All categories\",\n pay: \"Pay — {bedrag}\",\n busy: \"Working…\",\n checkoutFailed: \"Checkout failed. Check your details and try again.\",\n secureNote: \"You pay securely via a protected checkout page (incl. iDEAL). You will receive a confirmation email after payment.\",\n yourOrder: \"Your order\",\n fieldName: \"Name\",\n fieldEmail: \"Email\",\n fieldPhone: \"Phone (optional)\",\n fieldAddress: \"Street + number\",\n fieldZip: \"Postal code\",\n fieldCity: \"City\",\n fieldNote: \"Note (optional)\",\n choose: \"Choose…\",\n discountCode: \"Discount code\",\n discountApply: \"Apply\",\n discount: \"Discount\",\n discountInvalid: \"This code is not (or no longer) valid.\",\n discountApplied: \"Discount applied: {code}\",\n related: \"You may also like\",\n filterAll: \"All\",\n sortLabel: \"Sort\",\n sortDefault: \"Recommended\",\n sortName: \"Name\",\n sortPriceAsc: \"Price (low-high)\",\n sortPriceDesc: \"Price (high-low)\",\n personalizationUpload: \"Choose file…\",\n personalizationUploading: \"Uploading…\",\n personalizationUploaded: \"✓ Uploaded\",\n personalizationTooBig: \"File too large (max 5 MB).\",\n personalizationFailed: \"Upload failed — please try again.\",\n personalizationSurcharge: \"+ {bedrag}\",\n donationFailed: \"Donation failed. Please try again later.\",\n donationCustomAmount: \"Custom amount\",\n donationAmountLabel: \"Amount in euros (min. {min})\",\n};\n\nexport type ShopStringKey = keyof typeof NL;\nconst TABLES: Record<string, typeof NL> = { nl: NL, en: EN };\n\nexport type ShopT = (key: ShopStringKey, vars?: Record<string, string | number>) => string;\n\n/** Vertaalfunctie voor shop-blocks: `t(\"addToCart\")`, `t(\"stockLeft\", { n: 3 })`. */\nexport function useShopStrings(): ShopT {\n const lang = (useSiteConfig().lang ?? \"nl\").slice(0, 2).toLowerCase();\n const table = TABLES[lang] ?? NL;\n return (key, vars) => {\n let s = table[key] ?? NL[key] ?? key;\n if (vars) for (const [k, v] of Object.entries(vars)) s = s.replaceAll(`{${k}}`, String(v));\n return s;\n };\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../../src/modules/shop/strings.ts"],"sourcesContent":["\"use client\";\n\n/**\n * UI-teksten van de shop-blocks, gelokaliseerd op `SiteConfig.lang` (nl = default, en).\n * Interpolatie via `{naam}`-placeholders. Bewust een platte tabel — geen i18n-framework;\n * de shop-strings zijn de enige plek in de blocks met vaste UI-tekst.\n */\nimport { useSiteConfig } from \"../../config\";\n\nconst NL = {\n addToCart: \"In winkelwagen\",\n added: \"✓ Toegevoegd aan je winkelwagen.\",\n addedGoToCart: \"✓ In winkelwagen — bekijk\",\n view: \"Bekijk\",\n toCart: \"Naar winkelwagen\",\n continueShopping: \"Verder winkelen\",\n checkout: \"Afrekenen\",\n cartEmpty: \"Je winkelwagen is leeg.\",\n toShop: \"Naar de winkel\",\n subtotal: \"Subtotaal\",\n shippingCosts: \"Verzendkosten\",\n free: \"gratis\",\n total: \"Totaal\",\n perPiece: \"p/st\",\n qty: \"Aantal\",\n outOfStock: \"Uitverkocht in deze uitvoering.\",\n variantUnavailable: \"Deze combinatie is niet leverbaar.\",\n stockLeft: \"Nog {n} beschikbaar\",\n soldOut: \"Uitverkocht\",\n productNotFound: \"Product niet gevonden.\",\n backToShop: \"Terug naar de winkel\",\n noProducts: \"Geen producten gevonden.\",\n noCategories: \"Nog geen categorieën.\",\n allCategories: \"Alle categorieën\",\n pay: \"Betalen — {bedrag}\",\n busy: \"Bezig…\",\n checkoutFailed: \"Afrekenen is niet gelukt. Controleer je gegevens en probeer het opnieuw.\",\n secureNote: \"Je rekent veilig af via een beveiligde betaalpagina (o.a. iDEAL). Na betaling ontvang je een bevestiging per e-mail.\",\n yourOrder: \"Je bestelling\",\n fieldName: \"Naam\",\n fieldEmail: \"E-mail\",\n fieldPhone: \"Telefoon (optioneel)\",\n fieldAddress: \"Straat + huisnummer\",\n fieldZip: \"Postcode\",\n fieldCity: \"Plaats\",\n fieldCountry: \"Land\",\n shippingFromCountry: \"(bij bezorging in Nederland — kies je land bij het afrekenen)\",\n fieldNote: \"Opmerking (optioneel)\",\n choose: \"Kies…\",\n discountCode: \"Kortingscode\",\n discountApply: \"Toepassen\",\n discount: \"Korting\",\n discountInvalid: \"Deze code is niet (meer) geldig.\",\n discountApplied: \"Korting toegepast: {code}\",\n related: \"Ook leuk\",\n filterAll: \"Alles\",\n sortLabel: \"Sorteren\",\n sortDefault: \"Aanbevolen\",\n sortName: \"Naam\",\n sortPriceAsc: \"Prijs (laag-hoog)\",\n sortPriceDesc: \"Prijs (hoog-laag)\",\n personalizationUpload: \"Kies bestand…\",\n personalizationUploading: \"Uploaden…\",\n personalizationUploaded: \"✓ Geüpload\",\n personalizationTooBig: \"Bestand te groot (max 5 MB).\",\n personalizationFailed: \"Uploaden mislukt — probeer het opnieuw.\",\n personalizationSurcharge: \"+ {bedrag}\",\n donationFailed: \"Doneren is niet gelukt. Probeer het later opnieuw.\",\n donationCustomAmount: \"Ander bedrag\",\n donationAmountLabel: \"Bedrag in euro's (min. {min})\",\n};\n\nconst EN: typeof NL = {\n addToCart: \"Add to cart\",\n added: \"✓ Added to your cart.\",\n addedGoToCart: \"✓ In cart — view\",\n view: \"View\",\n toCart: \"Go to cart\",\n continueShopping: \"Continue shopping\",\n checkout: \"Checkout\",\n cartEmpty: \"Your cart is empty.\",\n toShop: \"Go to the shop\",\n subtotal: \"Subtotal\",\n shippingCosts: \"Shipping\",\n free: \"free\",\n total: \"Total\",\n perPiece: \"each\",\n qty: \"Quantity\",\n outOfStock: \"Sold out in this variant.\",\n variantUnavailable: \"This combination is unavailable.\",\n stockLeft: \"Only {n} left\",\n soldOut: \"Sold out\",\n productNotFound: \"Product not found.\",\n backToShop: \"Back to the shop\",\n noProducts: \"No products found.\",\n noCategories: \"No categories yet.\",\n allCategories: \"All categories\",\n pay: \"Pay — {bedrag}\",\n busy: \"Working…\",\n checkoutFailed: \"Checkout failed. Check your details and try again.\",\n secureNote: \"You pay securely via a protected checkout page (incl. iDEAL). You will receive a confirmation email after payment.\",\n yourOrder: \"Your order\",\n fieldName: \"Name\",\n fieldEmail: \"Email\",\n fieldPhone: \"Phone (optional)\",\n fieldAddress: \"Street + number\",\n fieldZip: \"Postal code\",\n fieldCity: \"City\",\n fieldCountry: \"Country\",\n shippingFromCountry: \"(for delivery in the Netherlands — choose your country at checkout)\",\n fieldNote: \"Note (optional)\",\n choose: \"Choose…\",\n discountCode: \"Discount code\",\n discountApply: \"Apply\",\n discount: \"Discount\",\n discountInvalid: \"This code is not (or no longer) valid.\",\n discountApplied: \"Discount applied: {code}\",\n related: \"You may also like\",\n filterAll: \"All\",\n sortLabel: \"Sort\",\n sortDefault: \"Recommended\",\n sortName: \"Name\",\n sortPriceAsc: \"Price (low-high)\",\n sortPriceDesc: \"Price (high-low)\",\n personalizationUpload: \"Choose file…\",\n personalizationUploading: \"Uploading…\",\n personalizationUploaded: \"✓ Uploaded\",\n personalizationTooBig: \"File too large (max 5 MB).\",\n personalizationFailed: \"Upload failed — please try again.\",\n personalizationSurcharge: \"+ {bedrag}\",\n donationFailed: \"Donation failed. Please try again later.\",\n donationCustomAmount: \"Custom amount\",\n donationAmountLabel: \"Amount in euros (min. {min})\",\n};\n\nexport type ShopStringKey = keyof typeof NL;\nconst TABLES: Record<string, typeof NL> = { nl: NL, en: EN };\n\nexport type ShopT = (key: ShopStringKey, vars?: Record<string, string | number>) => string;\n\n/** Vertaalfunctie voor shop-blocks: `t(\"addToCart\")`, `t(\"stockLeft\", { n: 3 })`. */\nexport function useShopStrings(): ShopT {\n const lang = (useSiteConfig().lang ?? \"nl\").slice(0, 2).toLowerCase();\n const table = TABLES[lang] ?? NL;\n return (key, vars) => {\n let s = table[key] ?? NL[key] ?? key;\n if (vars) for (const [k, v] of Object.entries(vars)) s = s.replaceAll(`{${k}}`, String(v));\n return s;\n };\n}\n"],"mappings":"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","names":[]}
|
package/dist/schema/shop.d.ts
CHANGED
|
@@ -126,6 +126,7 @@ export declare const ProductSchema: z.ZodMiniObject<{
|
|
|
126
126
|
priceCents: z.ZodMiniDefault<z.ZodMiniNumberFormat>;
|
|
127
127
|
helpText: z.ZodMiniOptional<z.ZodMiniString<string>>;
|
|
128
128
|
}, z.core.$strip>>>;
|
|
129
|
+
personalizationEnabled: z.ZodMiniDefault<z.ZodMiniBoolean<boolean>>;
|
|
129
130
|
}, z.core.$strip>;
|
|
130
131
|
export type Product = z.infer<typeof ProductSchema>;
|
|
131
132
|
/** `sites/{siteId}/discountCodes/{CODE}` (doc-id = code in hoofdletters; editor beheert). */
|
|
@@ -221,6 +222,8 @@ export declare const OrderSchema: z.ZodMiniObject<{
|
|
|
221
222
|
shippingCents: z.ZodMiniNumberFormat;
|
|
222
223
|
discountCents: z.ZodMiniDefault<z.ZodMiniNumberFormat>;
|
|
223
224
|
totalCents: z.ZodMiniNumberFormat;
|
|
225
|
+
vatCents: z.ZodMiniOptional<z.ZodMiniNumberFormat>;
|
|
226
|
+
vatByRate: z.ZodMiniOptional<z.ZodMiniRecord<z.ZodMiniString<string>, z.ZodMiniNumberFormat>>;
|
|
224
227
|
}, z.core.$strip>;
|
|
225
228
|
discount: z.ZodMiniOptional<z.ZodMiniObject<{
|
|
226
229
|
code: z.ZodMiniString<string>;
|
|
@@ -244,6 +247,8 @@ export declare const OrderSchema: z.ZodMiniObject<{
|
|
|
244
247
|
}, z.core.$strip>>;
|
|
245
248
|
note: z.ZodMiniOptional<z.ZodMiniString<string>>;
|
|
246
249
|
adminNote: z.ZodMiniOptional<z.ZodMiniString<string>>;
|
|
250
|
+
invoiceNumber: z.ZodMiniOptional<z.ZodMiniString<string>>;
|
|
251
|
+
manual: z.ZodMiniOptional<z.ZodMiniBoolean<boolean>>;
|
|
247
252
|
refundedCents: z.ZodMiniOptional<z.ZodMiniNumberFormat>;
|
|
248
253
|
messages: z.ZodMiniOptional<z.ZodMiniArray<z.ZodMiniObject<{
|
|
249
254
|
at: z.ZodMiniString<string>;
|
package/dist/schema/shop.js
CHANGED
|
@@ -82,7 +82,16 @@ const ProductSchema = z.object({
|
|
|
82
82
|
order: z._default(z.int().check(z.minimum(0)).check(z.maximum(9999)), 0),
|
|
83
83
|
featured: z._default(z.boolean(), false),
|
|
84
84
|
/** 5.6: personalisatie-velden (eigen naam/logo) — max 3 per product. */
|
|
85
|
-
personalization: z.optional(z.array(PersonalizationFieldSchema).check(z.maxLength(3)))
|
|
85
|
+
personalization: z.optional(z.array(PersonalizationFieldSchema).check(z.maxLength(3))),
|
|
86
|
+
/**
|
|
87
|
+
* Staat "persoonlijk maken" aan voor dit product? (7.19)
|
|
88
|
+
*
|
|
89
|
+
* Voorheen was dit impliciet: velden aanwezig = aan. Wie het tijdelijk wilde uitzetten (te
|
|
90
|
+
* druk, vakantie, leverancier levert de bedrukking niet) moest de velden VERWIJDEREN en dus
|
|
91
|
+
* ook zijn labels, meerprijzen en uitleg kwijtraken. Nu blijft de configuratie staan en gaat
|
|
92
|
+
* alleen de functie uit. Default true zodat bestaande producten met velden niets merken.
|
|
93
|
+
*/
|
|
94
|
+
personalizationEnabled: z._default(z.boolean(), true)
|
|
86
95
|
});
|
|
87
96
|
const DiscountCodeSchema = z.object({
|
|
88
97
|
code: z.string().check(z.minLength(3)).check(z.maxLength(30)).check(z.regex(/^[A-Z0-9-]+$/, "code: hoofdletters, cijfers en streepjes")),
|
|
@@ -152,7 +161,14 @@ const OrderSchema = z.object({
|
|
|
152
161
|
shippingCents: z.int().check(z.minimum(0)),
|
|
153
162
|
/** 5.6: toegepaste korting (0 = geen). */
|
|
154
163
|
discountCents: z._default(z.int().check(z.minimum(0)), 0),
|
|
155
|
-
totalCents: z.int().check(z.minimum(0))
|
|
164
|
+
totalCents: z.int().check(z.minimum(0)),
|
|
165
|
+
/**
|
|
166
|
+
* 7.18: btw-bedrag in de order zelf. Werd eerder alleen op de factuur en in de CSV-export
|
|
167
|
+
* afgeleid — twee losse kopieën van dezelfde som, terwijl dit voor de aangifte hét getal is.
|
|
168
|
+
*/
|
|
169
|
+
vatCents: z.optional(z.int().check(z.minimum(0))),
|
|
170
|
+
/** Btw per tarief ("21" → centen), voor de aangifte per categorie. */
|
|
171
|
+
vatByRate: z.optional(z.record(z.string(), z.int()))
|
|
156
172
|
}),
|
|
157
173
|
/** 5.6: toegepaste kortingscode (snapshot). */
|
|
158
174
|
discount: z.optional(z.object({
|
|
@@ -166,6 +182,10 @@ const OrderSchema = z.object({
|
|
|
166
182
|
note: z.optional(z.string().check(z.maxLength(2e3))),
|
|
167
183
|
/** 7.18: interne notitie van de winkelier — de klant ziet hem nooit. */
|
|
168
184
|
adminNote: z.optional(z.string().check(z.maxLength(2e3))),
|
|
185
|
+
/** 7.18: doorlopend factuurnummer (eigen reeks, zonder gaten) — pas gezet bij betaling. */
|
|
186
|
+
invoiceNumber: z.optional(z.string().check(z.maxLength(40))),
|
|
187
|
+
/** 7.18: handmatig aangemaakt in /beheer (telefonisch, markt, overschrijving). */
|
|
188
|
+
manual: z.optional(z.boolean()),
|
|
169
189
|
/** 7.18: som van alle (gedeeltelijke) terugbetalingen in centen. */
|
|
170
190
|
refundedCents: z.optional(z.int().check(z.minimum(0))),
|
|
171
191
|
/** 7.18: berichten die de winkelier vanuit /beheer aan de klant stuurde. */
|