@xiwebdesign/xicms-core 7.22.3 → 7.24.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.
Files changed (107) hide show
  1. package/dist/admin/AdminApp.js +2 -1
  2. package/dist/admin/AdminApp.js.map +1 -1
  3. package/dist/admin/CoreVersion.d.ts +6 -0
  4. package/dist/admin/CoreVersion.js +42 -1
  5. package/dist/admin/CoreVersion.js.map +1 -1
  6. package/dist/admin/SuperadminPanel.js +19 -3
  7. package/dist/admin/SuperadminPanel.js.map +1 -1
  8. package/dist/admin/dashboard/DashboardPanel.js +46 -9
  9. package/dist/admin/dashboard/DashboardPanel.js.map +1 -1
  10. package/dist/admin/forms/FormsPanel.d.ts +9 -0
  11. package/dist/admin/forms/FormsPanel.js +102 -16
  12. package/dist/admin/forms/FormsPanel.js.map +1 -1
  13. package/dist/admin/forms/gezien.d.ts +3 -0
  14. package/dist/admin/forms/gezien.js +22 -0
  15. package/dist/admin/forms/gezien.js.map +1 -0
  16. package/dist/admin/lib/kleur.d.ts +32 -0
  17. package/dist/admin/lib/kleur.js +60 -0
  18. package/dist/admin/lib/kleur.js.map +1 -0
  19. package/dist/admin/lib/pagesClient.d.ts +32 -0
  20. package/dist/admin/lib/pagesClient.js +60 -1
  21. package/dist/admin/lib/pagesClient.js.map +1 -1
  22. package/dist/admin/lib/seoHulp.d.ts +28 -0
  23. package/dist/admin/lib/seoHulp.js +33 -0
  24. package/dist/admin/lib/seoHulp.js.map +1 -0
  25. package/dist/admin/media/MediaManager.js +133 -51
  26. package/dist/admin/media/MediaManager.js.map +1 -1
  27. package/dist/admin/pages/PageEditor.d.ts +14 -0
  28. package/dist/admin/pages/PageEditor.js +192 -123
  29. package/dist/admin/pages/PageEditor.js.map +1 -1
  30. package/dist/admin/pages/PagesPanel.js +88 -40
  31. package/dist/admin/pages/PagesPanel.js.map +1 -1
  32. package/dist/admin/settings/SettingsPanel.js +100 -19
  33. package/dist/admin/settings/SettingsPanel.js.map +1 -1
  34. package/dist/admin/shop/ProductsTab.js +27 -2
  35. package/dist/admin/shop/ProductsTab.js.map +1 -1
  36. package/dist/admin/shop/ShopPanel.js +5 -2
  37. package/dist/admin/shop/ShopPanel.js.map +1 -1
  38. package/dist/admin/stats/StatsPanel.js +30 -2
  39. package/dist/admin/stats/StatsPanel.js.map +1 -1
  40. package/dist/admin/stats/lib.d.ts +14 -1
  41. package/dist/admin/stats/lib.js +46 -2
  42. package/dist/admin/stats/lib.js.map +1 -1
  43. package/dist/admin/visual/VisualEditor.js +80 -4
  44. package/dist/admin/visual/VisualEditor.js.map +1 -1
  45. package/dist/admin.css +1 -1
  46. package/dist/blocks/builtins/FormBlockRender.js +8 -2
  47. package/dist/blocks/builtins/FormBlockRender.js.map +1 -1
  48. package/dist/blocks/builtins/lib/submitForm.d.ts +3 -1
  49. package/dist/blocks/builtins/lib/submitForm.js +2 -2
  50. package/dist/blocks/builtins/lib/submitForm.js.map +1 -1
  51. package/dist/components/EditBar.d.ts +3 -1
  52. package/dist/components/EditBar.js +30 -4
  53. package/dist/components/EditBar.js.map +1 -1
  54. package/dist/components/News.js +8 -4
  55. package/dist/components/News.js.map +1 -1
  56. package/dist/components/PageView.d.ts +5 -0
  57. package/dist/components/PageView.js +24 -2
  58. package/dist/components/PageView.js.map +1 -1
  59. package/dist/components/ResponsiveImage.d.ts +1 -1
  60. package/dist/components/ResponsiveImage.js +3 -1
  61. package/dist/components/ResponsiveImage.js.map +1 -1
  62. package/dist/components/site/SiteNav.d.ts +29 -0
  63. package/dist/components/site/SiteNav.js +82 -0
  64. package/dist/components/site/SiteNav.js.map +1 -0
  65. package/dist/content/types.d.ts +8 -0
  66. package/dist/index.d.ts +3 -1
  67. package/dist/index.js +8 -1
  68. package/dist/index.js.map +1 -1
  69. package/dist/media/ImageContext.d.ts +1 -6
  70. package/dist/media/ImageContext.js +6 -1
  71. package/dist/media/ImageContext.js.map +1 -1
  72. package/dist/modules/shop/blocks/ProductDetail.js +8 -8
  73. package/dist/modules/shop/blocks/ProductDetail.js.map +1 -1
  74. package/dist/modules/shop/blocks/ProductGrid.js +5 -2
  75. package/dist/modules/shop/blocks/ProductGrid.js.map +1 -1
  76. package/dist/modules/shop/lib.d.ts +17 -1
  77. package/dist/modules/shop/lib.js +6 -0
  78. package/dist/modules/shop/lib.js.map +1 -1
  79. package/dist/releaseNotes.d.ts +15 -0
  80. package/dist/releaseNotes.js +28 -0
  81. package/dist/releaseNotes.js.map +1 -0
  82. package/dist/schema/settings.d.ts +105 -0
  83. package/dist/schema/settings.js +45 -5
  84. package/dist/schema/settings.js.map +1 -1
  85. package/dist/schema/shop.d.ts +4 -0
  86. package/dist/schema/shop.js +25 -4
  87. package/dist/schema/shop.js.map +1 -1
  88. package/dist/schema/siteConfig.d.ts +9 -0
  89. package/dist/server/blockData.js +6 -2
  90. package/dist/server/blockData.js.map +1 -1
  91. package/dist/server/draft.d.ts +11 -0
  92. package/dist/server/draft.js +20 -1
  93. package/dist/server/draft.js.map +1 -1
  94. package/dist/server/index.d.ts +3 -1
  95. package/dist/server/index.js +7 -1
  96. package/dist/server/index.js.map +1 -1
  97. package/dist/server/pages.js +4 -1
  98. package/dist/server/pages.js.map +1 -1
  99. package/dist/server/previewToken.d.ts +34 -0
  100. package/dist/server/previewToken.js +44 -0
  101. package/dist/server/previewToken.js.map +1 -0
  102. package/dist/server/redirects.d.ts +15 -0
  103. package/dist/server/redirects.js +15 -0
  104. package/dist/server/redirects.js.map +1 -0
  105. package/dist/styles.css +110 -0
  106. package/dist/version.js +1 -1
  107. package/package.json +1 -1
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../src/modules/shop/blocks/ProductGrid.tsx"],"sourcesContent":["\"use client\";\n\nimport { useEffect, useMemo, useState } from \"react\";\nimport type { z } from \"zod\";\nimport { formatCents } from \"../../../schema/shopGeld\";\nimport type { ProductGridBlockSchema } from \"../../../schema/shop\";\nimport { defineBlock, type BlockRenderProps } from \"../../../blocks/registry\";\nimport { useBlockData } from \"../../../blocks/context\";\nimport { Card, Heading, Text } from \"../../../blocks/ui\";\nimport { cartAdd } from \"../cartStore\";\nimport { useShopStrings, type ShopT } from \"../strings\";\nimport {\n fetchActiveProducts,\n fetchShopCategories,\n kleurNaarCss,\n kleurOptie,\n readQueryParam,\n useShopSettings,\n type StoredProduct,\n type StoredShopCategory,\n} from \"../lib\";\nimport type { ShopPageData } from \"../types\";\n\ntype Blk = z.infer<typeof ProductGridBlockSchema>;\n\n/** Prijsregel met optionele doorgestreepte \"van\"-prijs en \"vanaf\" bij variant-prijzen. */\nexport function PriceLine({ product, currency }: { product: StoredProduct; currency: string }) {\n const variantPrices = (product.variants ?? [])\n .filter((v) => v.priceCents !== undefined)\n .map((v) => v.priceCents as number);\n const min = Math.min(product.priceCents, ...variantPrices);\n const hasRange = variantPrices.some((p) => p !== product.priceCents);\n return (\n <span className=\"xicms-shop-price\">\n {product.compareAtCents !== undefined && product.compareAtCents > min && (\n <s style={{ opacity: 0.6, marginRight: 6 }}>{formatCents(product.compareAtCents, currency)}</s>\n )}\n {hasRange ? `vanaf ${formatCents(min, currency)}` : formatCents(min, currency)}\n </span>\n );\n}\n\n/**\n * Uitverkocht. Zonder opties: de voorraad van het product zelf. MÉT opties: pas uitverkocht als\n * élke variant met een expliciete voorraad op 0 staat — dat was eerder helemaal niet af te lezen,\n * waardoor een volledig uitverkocht t-shirt gewoon als beschikbaar in het grid stond.\n */\nfunction isSoldOut(p: StoredProduct): boolean {\n if (p.trackStock !== true) return false;\n if ((p.options ?? []).length === 0) return p.stock !== undefined && p.stock <= 0;\n const metVoorraad = (p.variants ?? []).filter((v) => v.stock !== undefined && v.active !== false);\n return metVoorraad.length > 0 && metVoorraad.every((v) => (v.stock as number) <= 0);\n}\n\n/** Stof-stipjes; hover wisselt de kaartfoto naar de per-kleur-afbeelding (als die er is). */\nfunction ColorSwatches({ product, onHover }: { product: StoredProduct; onHover?: (url: string | null) => void }) {\n const optie = kleurOptie(product);\n if (!optie) return null;\n const dots = optie.values\n .map((v) => ({ v, css: kleurNaarCss(v), img: optie.images?.[v] }))\n .filter((d): d is { v: string; css: string; img: string | undefined } => d.css !== null);\n if (dots.length === 0) return null;\n return (\n <span className=\"xicms-shop-card__swatches\" aria-label={`Kleuren: ${optie.values.join(\", \")}`}>\n {dots.map((d) => (\n <span\n key={d.v}\n className=\"xicms-shop-card__swatch\"\n title={d.v}\n onMouseEnter={d.img && onHover ? () => onHover(d.img ?? null) : undefined}\n onMouseLeave={d.img && onHover ? () => onHover(null) : undefined}\n style={{\n display: \"inline-block\",\n width: 12,\n height: 12,\n borderRadius: 999,\n background: d.css,\n boxShadow: \"inset 0 0 0 1px rgba(0,0,0,0.18)\",\n }}\n />\n ))}\n </span>\n );\n}\n\nexport function ProductCard({\n product,\n siteId,\n currency,\n detailPath,\n cartPath,\n t,\n filterColor,\n}: {\n product: StoredProduct;\n siteId: string;\n currency: string;\n detailPath: string;\n cartPath: string;\n t: ShopT;\n /** Actief kleurfilter uit het overzicht — toont bij voorkeur de foto van die kleur. */\n filterColor?: string | null;\n}) {\n const [added, setAdded] = useState(false);\n const [hoverImage, setHoverImage] = useState<string | null>(null);\n const href = `${detailPath}?p=${encodeURIComponent(product.slug)}`;\n const simple = (product.options ?? []).length === 0 && !(product.personalization ?? []).length;\n const soldOut = isSoldOut(product);\n const filterImage = filterColor ? kleurOptie(product)?.images?.[filterColor] : undefined;\n const image = hoverImage ?? filterImage ?? product.images?.[0];\n\n return (\n // Flex-kolom over de volle kaarthoogte: titel (vast 2 regels) → prijs → swatches →\n // knop onderaan. Zo lijnen prijs/swatches/knop over álle kaarten in de rij uit, ook\n // als een productnaam één of twee regels beslaat. Alle opmaak in styles.css.\n // `data-xicms-product` maakt de kaart in de bewerkmodus aanwijsbaar als PRODUCT (potlood voor\n // de naam, tandwiel voor het hele formulier). Op de cataloguspagina was dat er niet, en dus was\n // een productnaam daar het enige stuk tekst op de site dat je nergens kon aanklikken.\n <Card padding={0} className=\"xicms-shop-card\" data-xicms-product={product.id}>\n <a className=\"xicms-shop-card__link\" href={href}>\n <div className=\"xicms-shop-card__media\">\n {image ? (\n <img src={image} alt={product.name} loading=\"lazy\" className=\"xicms-shop-card__img\" />\n ) : (\n <div className=\"xicms-shop-card__placeholder\" aria-hidden=\"true\" />\n )}\n {soldOut && <span className=\"xicms-shop-card__soldout\">{t(\"soldOut\")}</span>}\n </div>\n <div className=\"xicms-shop-card__body\">\n <span className=\"xicms-shop-card__name\" data-xicms-product-edit=\"name\">{product.name}</span>\n <span className=\"xicms-shop-card__priceline\">\n <PriceLine product={product} currency={currency} />\n </span>\n <span className=\"xicms-shop-card__swatchrow\">\n <ColorSwatches product={product} onHover={setHoverImage} />\n </span>\n </div>\n </a>\n <div className=\"xicms-shop-card__actions\">\n {simple ? (\n added ? (\n <a className=\"xicms-shop-card__added\" href={cartPath}>\n {t(\"addedGoToCart\")}\n </a>\n ) : (\n <button\n type=\"button\"\n className=\"xicms-btn xicms-btn--primary\"\n disabled={soldOut}\n style={{ cursor: soldOut ? \"not-allowed\" : \"pointer\" }}\n onClick={() => {\n cartAdd(siteId, {\n productId: product.id,\n slug: product.slug,\n name: product.name,\n unitPriceCents: product.priceCents,\n imageUrl: product.images?.[0],\n });\n setAdded(true);\n }}\n >\n {t(\"addToCart\")}\n </button>\n )\n ) : (\n <a className=\"xicms-btn xicms-btn--secondary\" href={href}>\n {t(\"view\")}\n </a>\n )}\n </div>\n </Card>\n );\n}\n\nfunction ProductGridRender({ block, siteId }: BlockRenderProps<Blk>) {\n const settings = useShopSettings();\n const t = useShopStrings();\n const serverData = useBlockData<ShopPageData>(\"shop\");\n const [products, setProducts] = useState<StoredProduct[] | null>(serverData?.products ?? null);\n const [cats, setCats] = useState<StoredShopCategory[]>(serverData?.categories ?? []);\n const [queryCat, setQueryCat] = useState<string | null>(null);\n const [filterColor, setFilterColor] = useState<string | null>(null);\n const [filterSize, setFilterSize] = useState<string | null>(null);\n const [sort, setSort] = useState<Blk[\"sort\"]>(block.sort);\n\n useEffect(() => {\n // Server-data aanwezig (SSR/ISR)? Dan geen client-fetch — geen flits, minder reads.\n if (!serverData?.products) {\n void fetchActiveProducts(siteId).then(setProducts).catch((e) => {\n console.warn(\"[xicms] producten laden mislukt:\", e);\n setProducts([]);\n });\n }\n if (!serverData?.categories) {\n void fetchShopCategories(siteId).then(setCats).catch(() => setCats([]));\n }\n if (block.useQueryCategory && !block.categorySlug) setQueryCat(readQueryParam(\"cat\"));\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [siteId, block.useQueryCategory, block.categorySlug]);\n\n const activeSlug = block.categorySlug || queryCat || null;\n const activeCat = activeSlug ? cats.find((c) => c.slug === activeSlug) ?? null : null;\n\n /** Beschikbare filterwaarden uit de geladen set (client-side; geen extra reads). */\n const filterValues = useMemo(() => {\n if (!block.showFilters || !products) return { colors: [] as string[], sizes: [] as string[] };\n const colors = new Set<string>();\n const sizes = new Set<string>();\n for (const p of products) {\n for (const o of p.options ?? []) {\n if (/^kleur|^colou?r/i.test(o.name)) o.values.forEach((v) => colors.add(v));\n if (/^maat|^size/i.test(o.name)) o.values.forEach((v) => sizes.add(v));\n }\n }\n return { colors: [...colors], sizes: [...sizes].sort((a, b) => Number(a) - Number(b) || a.localeCompare(b)) };\n }, [products, block.showFilters]);\n\n const visible = useMemo(() => {\n if (!products) return [];\n let list = products;\n if (activeCat) list = list.filter((p) => (p.categoryIds ?? []).includes(activeCat.id));\n if (block.featuredOnly) list = list.filter((p) => p.featured);\n if (filterColor) {\n list = list.filter((p) =>\n (p.options ?? []).some((o) => /^kleur|^colou?r/i.test(o.name) && o.values.includes(filterColor)),\n );\n }\n if (filterSize) {\n list = list.filter((p) =>\n (p.options ?? []).some((o) => /^maat|^size/i.test(o.name) && o.values.includes(filterSize)),\n );\n }\n const sorted = [...list];\n if (sort === \"name\") sorted.sort((a, b) => a.name.localeCompare(b.name, \"nl\"));\n else if (sort === \"priceAsc\") sorted.sort((a, b) => a.priceCents - b.priceCents);\n else if (sort === \"priceDesc\") sorted.sort((a, b) => b.priceCents - a.priceCents);\n else sorted.sort((a, b) => (a.order ?? 0) - (b.order ?? 0) || a.name.localeCompare(b.name, \"nl\"));\n /**\n * Uitverkochte artikelen altijd achteraan, ongeacht de gekozen sortering: ze bleven op hun\n * plek staan en duwden zo leverbare producten van de eerste rij. De volgorde binnen elke\n * groep blijft die van de sortering hierboven — een stabiele sort verandert daar niets aan.\n */\n sorted.sort((a, b) => Number(isSoldOut(a)) - Number(isSoldOut(b)));\n return sorted.slice(0, block.limit);\n }, [products, activeCat, block.featuredOnly, block.limit, sort, filterColor, filterSize]);\n\n if (products === null) return null;\n\n const chip = (label: string, active: boolean, onClick: () => void, swatch?: string | null) => (\n <button\n key={label}\n type=\"button\"\n className={`xicms-shop-filterchip${active ? \" xicms-shop-filterchip--active\" : \"\"}`}\n aria-pressed={active}\n onClick={onClick}\n >\n {swatch && (\n <span\n aria-hidden\n style={{ display: \"inline-block\", width: 10, height: 10, borderRadius: 999, background: swatch, boxShadow: \"inset 0 0 0 1px rgba(0,0,0,0.18)\", marginRight: 5 }}\n />\n )}\n {label}\n </button>\n );\n\n return (\n <div className=\"xicms-shop-grid\">\n {(block.heading || activeCat) && (\n <Heading level={2} data-xicms-edit=\"heading\">\n {activeCat?.name ?? block.heading}\n </Heading>\n )}\n {activeCat && (\n <Text size=\"sm\" display=\"block\" style={{ marginTop: 4 }}>\n <a href={settings.shopPath}>← {t(\"allCategories\")}</a>\n </Text>\n )}\n\n {block.showFilters && (filterValues.colors.length > 0 || filterValues.sizes.length > 0) && (\n <div className=\"xicms-shop-filters\">\n {filterValues.colors.length > 0 && (\n <div className=\"xicms-shop-filters__row\" role=\"group\" aria-label=\"Kleur\">\n {chip(t(\"filterAll\"), filterColor === null, () => setFilterColor(null))}\n {filterValues.colors.map((c) =>\n chip(c, filterColor === c, () => setFilterColor(filterColor === c ? null : c), kleurNaarCss(c)),\n )}\n </div>\n )}\n {filterValues.sizes.length > 0 && (\n <div className=\"xicms-shop-filters__row\" role=\"group\" aria-label=\"Maat\">\n {chip(t(\"filterAll\"), filterSize === null, () => setFilterSize(null))}\n {filterValues.sizes.map((s) => chip(s, filterSize === s, () => setFilterSize(filterSize === s ? null : s)))}\n </div>\n )}\n <label className=\"xicms-shop-filters__sort\">\n <span>{t(\"sortLabel\")}</span>\n <select className=\"xicms-input\" value={sort} onChange={(e) => setSort(e.target.value as Blk[\"sort\"])}>\n <option value=\"order\">{t(\"sortDefault\")}</option>\n <option value=\"name\">{t(\"sortName\")}</option>\n <option value=\"priceAsc\">{t(\"sortPriceAsc\")}</option>\n <option value=\"priceDesc\">{t(\"sortPriceDesc\")}</option>\n </select>\n </label>\n </div>\n )}\n\n {visible.length === 0 ? (\n <Card padding={4} variant=\"muted\" style={{ marginTop: \"1rem\" }}>\n <Text>{t(\"noProducts\")}</Text>\n </Card>\n ) : (\n <div\n className=\"xicms-shop-grid__items\"\n data-columns={block.columns}\n style={{\n display: \"grid\",\n gridTemplateColumns: `repeat(auto-fill, minmax(min(240px, calc(100%/${block.columns} - 1rem)), 1fr))`,\n gap: \"1.25rem\",\n marginTop: \"1.25rem\",\n }}\n >\n {visible.map((p) => (\n <ProductCard\n key={p.id}\n product={p}\n siteId={siteId}\n currency={settings.currency}\n detailPath={settings.detailPath}\n cartPath={settings.cartPath}\n t={t}\n filterColor={filterColor}\n />\n ))}\n </div>\n )}\n </div>\n );\n}\n\nexport const productGridBlock = defineBlock({\n type: \"productGrid\",\n Render: ProductGridRender,\n label: \"Producten-overzicht\",\n icon: \"🛍️\",\n defaults: () => ({ useQueryCategory: true, columns: 3, limit: 60, sort: \"order\" }),\n});\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../../../src/modules/shop/blocks/ProductGrid.tsx"],"sourcesContent":["\"use client\";\n\nimport { useEffect, useMemo, useState } from \"react\";\nimport type { z } from \"zod\";\nimport { formatCents } from \"../../../schema/shopGeld\";\nimport type { ProductGridBlockSchema } from \"../../../schema/shop\";\nimport { optieHeeftBeeldPerWaarde } from \"../../../schema/shop\";\nimport { defineBlock, type BlockRenderProps } from \"../../../blocks/registry\";\nimport { useBlockData } from \"../../../blocks/context\";\nimport { Card, Heading, Text } from \"../../../blocks/ui\";\nimport { cartAdd } from \"../cartStore\";\nimport { useShopStrings, type ShopT } from \"../strings\";\nimport {\n fetchActiveProducts,\n fetchShopCategories,\n kleurNaarCss,\n kleurOptie,\n readQueryParam,\n useShopSettings,\n type StoredProduct,\n type StoredShopCategory,\n} from \"../lib\";\nimport type { ShopPageData } from \"../types\";\n\ntype Blk = z.infer<typeof ProductGridBlockSchema>;\n\n/** Prijsregel met optionele doorgestreepte \"van\"-prijs en \"vanaf\" bij variant-prijzen. */\nexport function PriceLine({ product, currency }: { product: StoredProduct; currency: string }) {\n const variantPrices = (product.variants ?? [])\n .filter((v) => v.priceCents !== undefined)\n .map((v) => v.priceCents as number);\n const min = Math.min(product.priceCents, ...variantPrices);\n const hasRange = variantPrices.some((p) => p !== product.priceCents);\n return (\n <span className=\"xicms-shop-price\">\n {product.compareAtCents !== undefined && product.compareAtCents > min && (\n <s style={{ opacity: 0.6, marginRight: 6 }}>{formatCents(product.compareAtCents, currency)}</s>\n )}\n {hasRange ? `vanaf ${formatCents(min, currency)}` : formatCents(min, currency)}\n </span>\n );\n}\n\n/**\n * Uitverkocht. Zonder opties: de voorraad van het product zelf. MÉT opties: pas uitverkocht als\n * élke variant met een expliciete voorraad op 0 staat — dat was eerder helemaal niet af te lezen,\n * waardoor een volledig uitverkocht t-shirt gewoon als beschikbaar in het grid stond.\n */\nfunction isSoldOut(p: StoredProduct): boolean {\n if (p.trackStock !== true) return false;\n if ((p.options ?? []).length === 0) return p.stock !== undefined && p.stock <= 0;\n const metVoorraad = (p.variants ?? []).filter((v) => v.stock !== undefined && v.active !== false);\n return metVoorraad.length > 0 && metVoorraad.every((v) => (v.stock as number) <= 0);\n}\n\n/**\n * Stof-stipjes; hover wisselt de kaartfoto naar de foto van die kleur (als die er is).\n *\n * De stipjes blijven aan de KLEUR-as hangen — een tint tekenen kan alleen als de waarde een\n * kleurnaam is. De foto's komen er alleen bij als die as ook echt beeld per waarde voert: staat\n * dat vinkje uit, dan zijn eventuele oude URL's dode data en moet hover niets doen.\n */\nfunction ColorSwatches({ product, onHover }: { product: StoredProduct; onHover?: (url: string | null) => void }) {\n const optie = kleurOptie(product);\n if (!optie) return null;\n const metBeeld = optieHeeftBeeldPerWaarde(optie);\n const dots = optie.values\n .map((v) => ({ v, css: kleurNaarCss(v), img: metBeeld ? optie.images?.[v] : undefined }))\n .filter((d): d is { v: string; css: string; img: string | undefined } => d.css !== null);\n if (dots.length === 0) return null;\n return (\n <span className=\"xicms-shop-card__swatches\" aria-label={`Kleuren: ${optie.values.join(\", \")}`}>\n {dots.map((d) => (\n <span\n key={d.v}\n className=\"xicms-shop-card__swatch\"\n title={d.v}\n onMouseEnter={d.img && onHover ? () => onHover(d.img ?? null) : undefined}\n onMouseLeave={d.img && onHover ? () => onHover(null) : undefined}\n style={{\n display: \"inline-block\",\n width: 12,\n height: 12,\n borderRadius: 999,\n background: d.css,\n boxShadow: \"inset 0 0 0 1px rgba(0,0,0,0.18)\",\n }}\n />\n ))}\n </span>\n );\n}\n\nexport function ProductCard({\n product,\n siteId,\n currency,\n detailPath,\n cartPath,\n t,\n filterColor,\n}: {\n product: StoredProduct;\n siteId: string;\n currency: string;\n detailPath: string;\n cartPath: string;\n t: ShopT;\n /** Actief kleurfilter uit het overzicht — toont bij voorkeur de foto van die kleur. */\n filterColor?: string | null;\n}) {\n const [added, setAdded] = useState(false);\n const [hoverImage, setHoverImage] = useState<string | null>(null);\n const href = `${detailPath}?p=${encodeURIComponent(product.slug)}`;\n const simple = (product.options ?? []).length === 0 && !(product.personalization ?? []).length;\n const soldOut = isSoldOut(product);\n // Kleurfilter actief? Toon bij voorkeur de foto van die kleur — maar alleen als de kleur-as\n // ook echt beeld per waarde voert (zie ColorSwatches).\n const kleurAs = filterColor ? kleurOptie(product) : null;\n const filterImage = kleurAs && optieHeeftBeeldPerWaarde(kleurAs) ? kleurAs.images?.[filterColor as string] : undefined;\n const image = hoverImage ?? filterImage ?? product.images?.[0];\n\n return (\n // Flex-kolom over de volle kaarthoogte: titel (vast 2 regels) → prijs → swatches →\n // knop onderaan. Zo lijnen prijs/swatches/knop over álle kaarten in de rij uit, ook\n // als een productnaam één of twee regels beslaat. Alle opmaak in styles.css.\n // `data-xicms-product` maakt de kaart in de bewerkmodus aanwijsbaar als PRODUCT (potlood voor\n // de naam, tandwiel voor het hele formulier). Op de cataloguspagina was dat er niet, en dus was\n // een productnaam daar het enige stuk tekst op de site dat je nergens kon aanklikken.\n <Card padding={0} className=\"xicms-shop-card\" data-xicms-product={product.id}>\n <a className=\"xicms-shop-card__link\" href={href}>\n <div className=\"xicms-shop-card__media\">\n {image ? (\n <img src={image} alt={product.name} loading=\"lazy\" className=\"xicms-shop-card__img\" />\n ) : (\n <div className=\"xicms-shop-card__placeholder\" aria-hidden=\"true\" />\n )}\n {soldOut && <span className=\"xicms-shop-card__soldout\">{t(\"soldOut\")}</span>}\n </div>\n <div className=\"xicms-shop-card__body\">\n <span className=\"xicms-shop-card__name\" data-xicms-product-edit=\"name\">{product.name}</span>\n <span className=\"xicms-shop-card__priceline\">\n <PriceLine product={product} currency={currency} />\n </span>\n <span className=\"xicms-shop-card__swatchrow\">\n <ColorSwatches product={product} onHover={setHoverImage} />\n </span>\n </div>\n </a>\n <div className=\"xicms-shop-card__actions\">\n {simple ? (\n added ? (\n <a className=\"xicms-shop-card__added\" href={cartPath}>\n {t(\"addedGoToCart\")}\n </a>\n ) : (\n <button\n type=\"button\"\n className=\"xicms-btn xicms-btn--primary\"\n disabled={soldOut}\n style={{ cursor: soldOut ? \"not-allowed\" : \"pointer\" }}\n onClick={() => {\n cartAdd(siteId, {\n productId: product.id,\n slug: product.slug,\n name: product.name,\n unitPriceCents: product.priceCents,\n imageUrl: product.images?.[0],\n });\n setAdded(true);\n }}\n >\n {t(\"addToCart\")}\n </button>\n )\n ) : (\n <a className=\"xicms-btn xicms-btn--secondary\" href={href}>\n {t(\"view\")}\n </a>\n )}\n </div>\n </Card>\n );\n}\n\nfunction ProductGridRender({ block, siteId }: BlockRenderProps<Blk>) {\n const settings = useShopSettings();\n const t = useShopStrings();\n const serverData = useBlockData<ShopPageData>(\"shop\");\n const [products, setProducts] = useState<StoredProduct[] | null>(serverData?.products ?? null);\n const [cats, setCats] = useState<StoredShopCategory[]>(serverData?.categories ?? []);\n const [queryCat, setQueryCat] = useState<string | null>(null);\n const [filterColor, setFilterColor] = useState<string | null>(null);\n const [filterSize, setFilterSize] = useState<string | null>(null);\n const [sort, setSort] = useState<Blk[\"sort\"]>(block.sort);\n\n useEffect(() => {\n // Server-data aanwezig (SSR/ISR)? Dan geen client-fetch — geen flits, minder reads.\n if (!serverData?.products) {\n void fetchActiveProducts(siteId).then(setProducts).catch((e) => {\n console.warn(\"[xicms] producten laden mislukt:\", e);\n setProducts([]);\n });\n }\n if (!serverData?.categories) {\n void fetchShopCategories(siteId).then(setCats).catch(() => setCats([]));\n }\n if (block.useQueryCategory && !block.categorySlug) setQueryCat(readQueryParam(\"cat\"));\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [siteId, block.useQueryCategory, block.categorySlug]);\n\n const activeSlug = block.categorySlug || queryCat || null;\n const activeCat = activeSlug ? cats.find((c) => c.slug === activeSlug) ?? null : null;\n\n /** Beschikbare filterwaarden uit de geladen set (client-side; geen extra reads). */\n const filterValues = useMemo(() => {\n if (!block.showFilters || !products) return { colors: [] as string[], sizes: [] as string[] };\n const colors = new Set<string>();\n const sizes = new Set<string>();\n for (const p of products) {\n for (const o of p.options ?? []) {\n if (/^kleur|^colou?r/i.test(o.name)) o.values.forEach((v) => colors.add(v));\n if (/^maat|^size/i.test(o.name)) o.values.forEach((v) => sizes.add(v));\n }\n }\n return { colors: [...colors], sizes: [...sizes].sort((a, b) => Number(a) - Number(b) || a.localeCompare(b)) };\n }, [products, block.showFilters]);\n\n const visible = useMemo(() => {\n if (!products) return [];\n let list = products;\n if (activeCat) list = list.filter((p) => (p.categoryIds ?? []).includes(activeCat.id));\n if (block.featuredOnly) list = list.filter((p) => p.featured);\n if (filterColor) {\n list = list.filter((p) =>\n (p.options ?? []).some((o) => /^kleur|^colou?r/i.test(o.name) && o.values.includes(filterColor)),\n );\n }\n if (filterSize) {\n list = list.filter((p) =>\n (p.options ?? []).some((o) => /^maat|^size/i.test(o.name) && o.values.includes(filterSize)),\n );\n }\n const sorted = [...list];\n if (sort === \"name\") sorted.sort((a, b) => a.name.localeCompare(b.name, \"nl\"));\n else if (sort === \"priceAsc\") sorted.sort((a, b) => a.priceCents - b.priceCents);\n else if (sort === \"priceDesc\") sorted.sort((a, b) => b.priceCents - a.priceCents);\n else sorted.sort((a, b) => (a.order ?? 0) - (b.order ?? 0) || a.name.localeCompare(b.name, \"nl\"));\n /**\n * Uitverkochte artikelen altijd achteraan, ongeacht de gekozen sortering: ze bleven op hun\n * plek staan en duwden zo leverbare producten van de eerste rij. De volgorde binnen elke\n * groep blijft die van de sortering hierboven — een stabiele sort verandert daar niets aan.\n */\n sorted.sort((a, b) => Number(isSoldOut(a)) - Number(isSoldOut(b)));\n return sorted.slice(0, block.limit);\n }, [products, activeCat, block.featuredOnly, block.limit, sort, filterColor, filterSize]);\n\n if (products === null) return null;\n\n const chip = (label: string, active: boolean, onClick: () => void, swatch?: string | null) => (\n <button\n key={label}\n type=\"button\"\n className={`xicms-shop-filterchip${active ? \" xicms-shop-filterchip--active\" : \"\"}`}\n aria-pressed={active}\n onClick={onClick}\n >\n {swatch && (\n <span\n aria-hidden\n style={{ display: \"inline-block\", width: 10, height: 10, borderRadius: 999, background: swatch, boxShadow: \"inset 0 0 0 1px rgba(0,0,0,0.18)\", marginRight: 5 }}\n />\n )}\n {label}\n </button>\n );\n\n return (\n <div className=\"xicms-shop-grid\">\n {(block.heading || activeCat) && (\n <Heading level={2} data-xicms-edit=\"heading\">\n {activeCat?.name ?? block.heading}\n </Heading>\n )}\n {activeCat && (\n <Text size=\"sm\" display=\"block\" style={{ marginTop: 4 }}>\n <a href={settings.shopPath}>← {t(\"allCategories\")}</a>\n </Text>\n )}\n\n {block.showFilters && (filterValues.colors.length > 0 || filterValues.sizes.length > 0) && (\n <div className=\"xicms-shop-filters\">\n {filterValues.colors.length > 0 && (\n <div className=\"xicms-shop-filters__row\" role=\"group\" aria-label=\"Kleur\">\n {chip(t(\"filterAll\"), filterColor === null, () => setFilterColor(null))}\n {filterValues.colors.map((c) =>\n chip(c, filterColor === c, () => setFilterColor(filterColor === c ? null : c), kleurNaarCss(c)),\n )}\n </div>\n )}\n {filterValues.sizes.length > 0 && (\n <div className=\"xicms-shop-filters__row\" role=\"group\" aria-label=\"Maat\">\n {chip(t(\"filterAll\"), filterSize === null, () => setFilterSize(null))}\n {filterValues.sizes.map((s) => chip(s, filterSize === s, () => setFilterSize(filterSize === s ? null : s)))}\n </div>\n )}\n <label className=\"xicms-shop-filters__sort\">\n <span>{t(\"sortLabel\")}</span>\n <select className=\"xicms-input\" value={sort} onChange={(e) => setSort(e.target.value as Blk[\"sort\"])}>\n <option value=\"order\">{t(\"sortDefault\")}</option>\n <option value=\"name\">{t(\"sortName\")}</option>\n <option value=\"priceAsc\">{t(\"sortPriceAsc\")}</option>\n <option value=\"priceDesc\">{t(\"sortPriceDesc\")}</option>\n </select>\n </label>\n </div>\n )}\n\n {visible.length === 0 ? (\n <Card padding={4} variant=\"muted\" style={{ marginTop: \"1rem\" }}>\n <Text>{t(\"noProducts\")}</Text>\n </Card>\n ) : (\n <div\n className=\"xicms-shop-grid__items\"\n data-columns={block.columns}\n style={{\n display: \"grid\",\n gridTemplateColumns: `repeat(auto-fill, minmax(min(240px, calc(100%/${block.columns} - 1rem)), 1fr))`,\n gap: \"1.25rem\",\n marginTop: \"1.25rem\",\n }}\n >\n {visible.map((p) => (\n <ProductCard\n key={p.id}\n product={p}\n siteId={siteId}\n currency={settings.currency}\n detailPath={settings.detailPath}\n cartPath={settings.cartPath}\n t={t}\n filterColor={filterColor}\n />\n ))}\n </div>\n )}\n </div>\n );\n}\n\nexport const productGridBlock = defineBlock({\n type: \"productGrid\",\n Render: ProductGridRender,\n label: \"Producten-overzicht\",\n icon: \"🛍️\",\n defaults: () => ({ useQueryCategory: true, columns: 3, limit: 60, sort: \"order\" }),\n});\n"],"mappings":"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","names":[]}
@@ -51,5 +51,21 @@ export declare function getOrderReturnStatusCall(siteId: string, orderId: string
51
51
  * swatch-stipjes op productkaarten. Onbekende namen geven null (dan geen stipje).
52
52
  */
53
53
  export declare function kleurNaarCss(naam: string): string | null;
54
- /** De kleur-optie van een product (naam "Kleur"/"Colour"/"Color"), of null. */
54
+ /**
55
+ * De KLEUR-as van een product (naam "Kleur"/"Colour"/"Color"), of null.
56
+ *
57
+ * Blijft op de naam zoeken, en dat is hier terecht: deze as voedt de gekleurde stipjes op de
58
+ * productkaart en het kleurfilter in het overzicht. Een stipje in de juiste tint kan alleen als
59
+ * de wáárde een kleurnaam is (zie `kleurNaarCss`) — "Print: Olifantje" heeft geen tint.
60
+ */
55
61
  export declare function kleurOptie(product: Pick<Product, "options">): ProductOption | null;
62
+ /**
63
+ * De as waarvan élke waarde een eigen foto heeft — de as die het hoofdbeeld op de detailpagina
64
+ * stuurt. Dit is losgekoppeld van de kleur-as (7.23): de winkelier zet per as een vinkje, dus het
65
+ * mag net zo goed "Print" of "Dessin" zijn. Zonder expliciete keuze valt het terug op de oude
66
+ * kleur-naamregel, zodat bestaande producten onveranderd blijven werken.
67
+ *
68
+ * Maar één as tegelijk: het hoofdbeeld kan niet twee keuzes tegelijk volgen. Staat de vlag per
69
+ * ongeluk toch op meerdere assen (handmatig aangepaste data), dan wint de eerste.
70
+ */
71
+ export declare function beeldOptie(product: Pick<Product, "options">): ProductOption | null;
@@ -7,6 +7,7 @@ async function fnSdk() {
7
7
  const [client, sdk] = await Promise.all([import("../../firebase/client.js"), import("../../firebase/functionsClient.js")]);
8
8
  return { app: client.getFirebaseApp(), ...sdk };
9
9
  }
10
+ import { optieHeeftBeeldPerWaarde } from "../../schema/shop.js";
10
11
  import { useModuleSettings } from "../context.js";
11
12
  import { defaultShopSettings } from "./settings.js";
12
13
  function useShopSettings() {
@@ -123,7 +124,12 @@ function kleurNaarCss(naam) {
123
124
  function kleurOptie(product) {
124
125
  return (product.options ?? []).find((o) => /^kleur|^colou?r/i.test(o.name)) ?? null;
125
126
  }
127
+ function beeldOptie(product) {
128
+ const opties = product.options ?? [];
129
+ return opties.find((o) => o.imagePerValue === true) ?? opties.find((o) => optieHeeftBeeldPerWaarde(o)) ?? null;
130
+ }
126
131
  export {
132
+ beeldOptie,
127
133
  createOrderCall,
128
134
  fetchActiveProducts,
129
135
  fetchProductBySlug,
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/modules/shop/lib.ts"],"sourcesContent":["\"use client\";\n\n/**\n * Client-helpers voor de shop-blocks: producten/categorieën lezen (publieke rules) en de\n * `createOrder`-callable aanroepen. Queries bewust simpel gehouden (één `where` op status,\n * filteren/sorteren client-side) zodat er géén composite-indexes nodig zijn — assortimenten\n * van XiCMS-sites zijn klein (tientallen tot honderden producten).\n */\n\n/**\n * Firestore/Functions worden hier DYNAMISCH geladen. De shop-blocks zitten in de publieke bundel,\n * dus een statische import zette de SDK op élke pagina — ook op sites zonder webshop.\n */\nasync function fsSdk() {\n const [client, sdk] = await Promise.all([import(\"../../firebase/client\"), import(\"firebase/firestore\")]);\n return { db: client.getDb(), ...sdk };\n}\nasync function fnSdk() {\n const [client, sdk] = await Promise.all([import(\"../../firebase/client\"), import(\"../../firebase/functionsClient\")]);\n return { app: client.getFirebaseApp(), ...sdk };\n}\nimport type { CreateOrderInput, Product, ProductOption, ShopCategory } from \"../../schema/shop\";\nimport { useModuleSettings } from \"../context\";\nimport { defaultShopSettings, type ShopSettings } from \"./settings\";\n\n/** Publieke shop-instellingen met defaults (paden, verzendkosten, valuta). */\nexport function useShopSettings(): ShopSettings {\n const raw = useModuleSettings<Partial<ShopSettings>>(\"shop\");\n return { ...defaultShopSettings, ...raw };\n}\n\nexport type StoredProduct = Product & { id: string };\nexport type StoredShopCategory = ShopCategory & { id: string };\n\nexport async function fetchActiveProducts(siteId: string): Promise<StoredProduct[]> {\n const { db, collection, getDocs, limit: fsLimit, query, where } = await fsSdk();\n if (!db) return [];\n const snap = await getDocs(\n query(collection(db, \"sites\", siteId, \"products\"), where(\"status\", \"==\", \"active\"), fsLimit(500)),\n );\n return snap.docs.map((d) => ({ id: d.id, ...(d.data() as Product) }));\n}\n\nexport async function fetchProductBySlug(siteId: string, slug: string): Promise<StoredProduct | null> {\n const { db, collection, getDocs, limit: fsLimit, query, where } = await fsSdk();\n if (!db || !slug) return null;\n // Het status-filter is verplicht: de rules staan publieke reads alleen toe voor actieve\n // producten, en Firestore weigert query's die niet aantoonbaar binnen die regel blijven.\n // Twee equality-filters → geen composite index nodig (index-merging).\n const snap = await getDocs(\n query(\n collection(db, \"sites\", siteId, \"products\"),\n where(\"slug\", \"==\", slug),\n where(\"status\", \"==\", \"active\"),\n fsLimit(1),\n ),\n );\n const doc = snap.docs[0];\n if (!doc) return null;\n return { id: doc.id, ...(doc.data() as Product) };\n}\n\nexport async function fetchShopCategories(siteId: string): Promise<StoredShopCategory[]> {\n const { db, collection, getDocs, limit: fsLimit, query, where } = await fsSdk();\n if (!db) return [];\n const snap = await getDocs(query(collection(db, \"sites\", siteId, \"shopCategories\"), fsLimit(100)));\n return snap.docs\n .map((d) => ({ id: d.id, ...(d.data() as ShopCategory) }))\n .filter((c) => c.visible !== false)\n .sort((a, b) => (a.order ?? 0) - (b.order ?? 0));\n}\n\n/** Start de checkout: order + Stripe-sessie server-side; geeft de redirect-URL terug. */\n/**\n * Verwijdert `undefined`/`null`-velden diep uit een payload vóór een callable-aanroep.\n * De Firebase-callable-SDK serialiseert `undefined` namelijk als `null` op de wire, terwijl de\n * server-side Zod-schema's optionele velden met `.optional()` valideren — dat wijst `null` af en\n * geeft \"Ongeldige invoer\". Door lege velden hier weg te laten komen ze als afwezig binnen (= ok).\n */\nfunction stripEmpty<T>(value: T): T {\n if (Array.isArray(value)) {\n return value.map((v) => stripEmpty(v)) as unknown as T;\n }\n if (value && typeof value === \"object\") {\n const out: Record<string, unknown> = {};\n for (const [k, v] of Object.entries(value as Record<string, unknown>)) {\n if (v === undefined || v === null) continue;\n out[k] = stripEmpty(v);\n }\n return out as T;\n }\n return value;\n}\n\nexport async function createOrderCall(input: CreateOrderInput): Promise<{ url: string; orderNumber: string }> {\n const { app, callable } = await fnSdk();\n if (!app) throw new Error(\"Webwinkel niet beschikbaar.\");\n const fn = callable(app, \"createOrder\");\n const res = await fn(stripEmpty(input));\n return res.data as { url: string; orderNumber: string };\n}\n\n/** Valideert een kortingscode server-side; geeft de verwachte korting terug (preview). */\nexport interface DiscountPreview {\n valid: boolean;\n code?: string;\n discountCents?: number;\n freeShipping?: boolean;\n reason?: string;\n}\nexport async function validateDiscountCall(siteId: string, code: string, itemsCents: number): Promise<DiscountPreview> {\n const { app, callable } = await fnSdk();\n if (!app) throw new Error(\"Webwinkel niet beschikbaar.\");\n const fn = callable(app, \"validateDiscountCode\");\n const res = await fn({ siteId, code, itemsCents });\n return res.data as DiscountPreview;\n}\n\n/**\n * Upload een personalisatie-afbeelding (eigen logo e.d.) via de begrensde callable —\n * geeft het Storage-pad terug dat op de orderregel meegaat. Max 5 MB, jpeg/png/webp.\n */\nexport async function uploadPersonalizationCall(\n siteId: string,\n productId: string,\n file: File,\n): Promise<{ path: string }> {\n const { app, callable } = await fnSdk();\n if (!app) throw new Error(\"Webwinkel niet beschikbaar.\");\n if (file.size > 5 * 1024 * 1024) throw new Error(\"TOO_BIG\");\n const bytesBase64 = await new Promise<string>((resolve, reject) => {\n const reader = new FileReader();\n reader.onload = () => resolve(String(reader.result).split(\",\")[1] ?? \"\");\n reader.onerror = () => reject(new Error(\"Bestand lezen mislukt.\"));\n reader.readAsDataURL(file);\n });\n const fn = callable(app, \"uploadPersonalization\");\n const res = await fn({ siteId, productId, fileName: file.name, contentType: file.type, bytesBase64 });\n return res.data as { path: string };\n}\n\n/** Leesbaar variantlabel: opties [\"Maat\",\"Kleur\"] + key \"68|Roze\" → \"Maat 68 · Roze\". */\nexport function variantLabelFor(options: { name: string; values: string[] }[], key: string): string {\n const parts = key.split(\"|\");\n return options.map((o, i) => `${o.name} ${parts[i] ?? \"?\"}`).join(\" · \");\n}\n\n/** Querystring-parameter client-side lezen (blocks zijn client components). */\nexport function readQueryParam(name: string): string | null {\n if (typeof window === \"undefined\") return null;\n return new URLSearchParams(window.location.search).get(name);\n}\n\n/**\n * Echte orderstatus opvragen (voor de bedankt-pagina). Nodig bij Mollie: die heeft één\n * redirect-URL voor alle uitkomsten, dus de status moet server-side gecheckt worden.\n */\nexport async function getOrderReturnStatusCall(\n siteId: string,\n orderId: string,\n): Promise<{ status: string; number?: string }> {\n const { app, callable } = await fnSdk();\n if (!app) return { status: \"unknown\" };\n const fn = callable(app, \"getOrderReturnStatus\");\n const res = await fn({ siteId, orderId });\n return res.data as { status: string; number?: string };\n}\n\n/* ── Kleur-swatches ──────────────────────────────────────────────────────── */\n\n// Zachte, pastelachtige swatch-tinten — informatief maar rustig (geen felle vlekken op de\n// kaart). Bewust gedempt t.o.v. de echte stofkleuren; de foto toont de precieze kleur.\nconst KLEUR_CSS: [RegExp, string][] = [\n [/fuchsia|fushia/, \"#dd8fb4\"],\n [/roze|pink/, \"#f2c3d3\"],\n [/blauw|blue/, \"#bcd3ea\"],\n [/mint/, \"#bfe0d2\"],\n [/oker|geel|yellow/, \"#eccf90\"],\n [/wit|white/, \"#f6f3ec\"],\n [/creme|crème|ecru|beige|zand/, \"#f0e7d6\"],\n [/grijs|gray|grey/, \"#c7ccd3\"],\n [/groen|green/, \"#b3d1b3\"],\n [/rood|red|bordeaux/, \"#e0a0a0\"],\n [/paars|purple|lila/, \"#cdbce6\"],\n [/oranje|orange/, \"#f2bd94\"],\n [/bruin|brown/, \"#c3a488\"],\n [/zwart|black|navy|donkerblauw/, \"#6b7488\"],\n];\n\n/**\n * Zet een kleurnaam (\"Roze\", \"Korte mouw tshirt (oker)geel\") om naar een CSS-kleur voor de\n * swatch-stipjes op productkaarten. Onbekende namen geven null (dan geen stipje).\n */\nexport function kleurNaarCss(naam: string): string | null {\n const l = naam.toLowerCase();\n for (const [re, css] of KLEUR_CSS) if (re.test(l)) return css;\n return null;\n}\n\n/** De kleur-optie van een product (naam \"Kleur\"/\"Colour\"/\"Color\"), of null. */\nexport function kleurOptie(product: Pick<Product, \"options\">): ProductOption | null {\n return (product.options ?? []).find((o) => /^kleur|^colou?r/i.test(o.name)) ?? null;\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../../src/modules/shop/lib.ts"],"sourcesContent":["\"use client\";\n\n/**\n * Client-helpers voor de shop-blocks: producten/categorieën lezen (publieke rules) en de\n * `createOrder`-callable aanroepen. Queries bewust simpel gehouden (één `where` op status,\n * filteren/sorteren client-side) zodat er géén composite-indexes nodig zijn — assortimenten\n * van XiCMS-sites zijn klein (tientallen tot honderden producten).\n */\n\n/**\n * Firestore/Functions worden hier DYNAMISCH geladen. De shop-blocks zitten in de publieke bundel,\n * dus een statische import zette de SDK op élke pagina — ook op sites zonder webshop.\n */\nasync function fsSdk() {\n const [client, sdk] = await Promise.all([import(\"../../firebase/client\"), import(\"firebase/firestore\")]);\n return { db: client.getDb(), ...sdk };\n}\nasync function fnSdk() {\n const [client, sdk] = await Promise.all([import(\"../../firebase/client\"), import(\"../../firebase/functionsClient\")]);\n return { app: client.getFirebaseApp(), ...sdk };\n}\nimport type { CreateOrderInput, Product, ProductOption, ShopCategory } from \"../../schema/shop\";\nimport { optieHeeftBeeldPerWaarde } from \"../../schema/shop\";\nimport { useModuleSettings } from \"../context\";\nimport { defaultShopSettings, type ShopSettings } from \"./settings\";\n\n/** Publieke shop-instellingen met defaults (paden, verzendkosten, valuta). */\nexport function useShopSettings(): ShopSettings {\n const raw = useModuleSettings<Partial<ShopSettings>>(\"shop\");\n return { ...defaultShopSettings, ...raw };\n}\n\nexport type StoredProduct = Product & { id: string };\nexport type StoredShopCategory = ShopCategory & { id: string };\n\nexport async function fetchActiveProducts(siteId: string): Promise<StoredProduct[]> {\n const { db, collection, getDocs, limit: fsLimit, query, where } = await fsSdk();\n if (!db) return [];\n const snap = await getDocs(\n query(collection(db, \"sites\", siteId, \"products\"), where(\"status\", \"==\", \"active\"), fsLimit(500)),\n );\n return snap.docs.map((d) => ({ id: d.id, ...(d.data() as Product) }));\n}\n\nexport async function fetchProductBySlug(siteId: string, slug: string): Promise<StoredProduct | null> {\n const { db, collection, getDocs, limit: fsLimit, query, where } = await fsSdk();\n if (!db || !slug) return null;\n // Het status-filter is verplicht: de rules staan publieke reads alleen toe voor actieve\n // producten, en Firestore weigert query's die niet aantoonbaar binnen die regel blijven.\n // Twee equality-filters → geen composite index nodig (index-merging).\n const snap = await getDocs(\n query(\n collection(db, \"sites\", siteId, \"products\"),\n where(\"slug\", \"==\", slug),\n where(\"status\", \"==\", \"active\"),\n fsLimit(1),\n ),\n );\n const doc = snap.docs[0];\n if (!doc) return null;\n return { id: doc.id, ...(doc.data() as Product) };\n}\n\nexport async function fetchShopCategories(siteId: string): Promise<StoredShopCategory[]> {\n const { db, collection, getDocs, limit: fsLimit, query, where } = await fsSdk();\n if (!db) return [];\n const snap = await getDocs(query(collection(db, \"sites\", siteId, \"shopCategories\"), fsLimit(100)));\n return snap.docs\n .map((d) => ({ id: d.id, ...(d.data() as ShopCategory) }))\n .filter((c) => c.visible !== false)\n .sort((a, b) => (a.order ?? 0) - (b.order ?? 0));\n}\n\n/** Start de checkout: order + Stripe-sessie server-side; geeft de redirect-URL terug. */\n/**\n * Verwijdert `undefined`/`null`-velden diep uit een payload vóór een callable-aanroep.\n * De Firebase-callable-SDK serialiseert `undefined` namelijk als `null` op de wire, terwijl de\n * server-side Zod-schema's optionele velden met `.optional()` valideren — dat wijst `null` af en\n * geeft \"Ongeldige invoer\". Door lege velden hier weg te laten komen ze als afwezig binnen (= ok).\n */\nfunction stripEmpty<T>(value: T): T {\n if (Array.isArray(value)) {\n return value.map((v) => stripEmpty(v)) as unknown as T;\n }\n if (value && typeof value === \"object\") {\n const out: Record<string, unknown> = {};\n for (const [k, v] of Object.entries(value as Record<string, unknown>)) {\n if (v === undefined || v === null) continue;\n out[k] = stripEmpty(v);\n }\n return out as T;\n }\n return value;\n}\n\nexport async function createOrderCall(input: CreateOrderInput): Promise<{ url: string; orderNumber: string }> {\n const { app, callable } = await fnSdk();\n if (!app) throw new Error(\"Webwinkel niet beschikbaar.\");\n const fn = callable(app, \"createOrder\");\n const res = await fn(stripEmpty(input));\n return res.data as { url: string; orderNumber: string };\n}\n\n/** Valideert een kortingscode server-side; geeft de verwachte korting terug (preview). */\nexport interface DiscountPreview {\n valid: boolean;\n code?: string;\n discountCents?: number;\n freeShipping?: boolean;\n reason?: string;\n}\nexport async function validateDiscountCall(siteId: string, code: string, itemsCents: number): Promise<DiscountPreview> {\n const { app, callable } = await fnSdk();\n if (!app) throw new Error(\"Webwinkel niet beschikbaar.\");\n const fn = callable(app, \"validateDiscountCode\");\n const res = await fn({ siteId, code, itemsCents });\n return res.data as DiscountPreview;\n}\n\n/**\n * Upload een personalisatie-afbeelding (eigen logo e.d.) via de begrensde callable —\n * geeft het Storage-pad terug dat op de orderregel meegaat. Max 5 MB, jpeg/png/webp.\n */\nexport async function uploadPersonalizationCall(\n siteId: string,\n productId: string,\n file: File,\n): Promise<{ path: string }> {\n const { app, callable } = await fnSdk();\n if (!app) throw new Error(\"Webwinkel niet beschikbaar.\");\n if (file.size > 5 * 1024 * 1024) throw new Error(\"TOO_BIG\");\n const bytesBase64 = await new Promise<string>((resolve, reject) => {\n const reader = new FileReader();\n reader.onload = () => resolve(String(reader.result).split(\",\")[1] ?? \"\");\n reader.onerror = () => reject(new Error(\"Bestand lezen mislukt.\"));\n reader.readAsDataURL(file);\n });\n const fn = callable(app, \"uploadPersonalization\");\n const res = await fn({ siteId, productId, fileName: file.name, contentType: file.type, bytesBase64 });\n return res.data as { path: string };\n}\n\n/** Leesbaar variantlabel: opties [\"Maat\",\"Kleur\"] + key \"68|Roze\" → \"Maat 68 · Roze\". */\nexport function variantLabelFor(options: { name: string; values: string[] }[], key: string): string {\n const parts = key.split(\"|\");\n return options.map((o, i) => `${o.name} ${parts[i] ?? \"?\"}`).join(\" · \");\n}\n\n/** Querystring-parameter client-side lezen (blocks zijn client components). */\nexport function readQueryParam(name: string): string | null {\n if (typeof window === \"undefined\") return null;\n return new URLSearchParams(window.location.search).get(name);\n}\n\n/**\n * Echte orderstatus opvragen (voor de bedankt-pagina). Nodig bij Mollie: die heeft één\n * redirect-URL voor alle uitkomsten, dus de status moet server-side gecheckt worden.\n */\nexport async function getOrderReturnStatusCall(\n siteId: string,\n orderId: string,\n): Promise<{ status: string; number?: string }> {\n const { app, callable } = await fnSdk();\n if (!app) return { status: \"unknown\" };\n const fn = callable(app, \"getOrderReturnStatus\");\n const res = await fn({ siteId, orderId });\n return res.data as { status: string; number?: string };\n}\n\n/* ── Kleur-swatches ──────────────────────────────────────────────────────── */\n\n// Zachte, pastelachtige swatch-tinten — informatief maar rustig (geen felle vlekken op de\n// kaart). Bewust gedempt t.o.v. de echte stofkleuren; de foto toont de precieze kleur.\nconst KLEUR_CSS: [RegExp, string][] = [\n [/fuchsia|fushia/, \"#dd8fb4\"],\n [/roze|pink/, \"#f2c3d3\"],\n [/blauw|blue/, \"#bcd3ea\"],\n [/mint/, \"#bfe0d2\"],\n [/oker|geel|yellow/, \"#eccf90\"],\n [/wit|white/, \"#f6f3ec\"],\n [/creme|crème|ecru|beige|zand/, \"#f0e7d6\"],\n [/grijs|gray|grey/, \"#c7ccd3\"],\n [/groen|green/, \"#b3d1b3\"],\n [/rood|red|bordeaux/, \"#e0a0a0\"],\n [/paars|purple|lila/, \"#cdbce6\"],\n [/oranje|orange/, \"#f2bd94\"],\n [/bruin|brown/, \"#c3a488\"],\n [/zwart|black|navy|donkerblauw/, \"#6b7488\"],\n];\n\n/**\n * Zet een kleurnaam (\"Roze\", \"Korte mouw tshirt (oker)geel\") om naar een CSS-kleur voor de\n * swatch-stipjes op productkaarten. Onbekende namen geven null (dan geen stipje).\n */\nexport function kleurNaarCss(naam: string): string | null {\n const l = naam.toLowerCase();\n for (const [re, css] of KLEUR_CSS) if (re.test(l)) return css;\n return null;\n}\n\n/**\n * De KLEUR-as van een product (naam \"Kleur\"/\"Colour\"/\"Color\"), of null.\n *\n * Blijft op de naam zoeken, en dat is hier terecht: deze as voedt de gekleurde stipjes op de\n * productkaart en het kleurfilter in het overzicht. Een stipje in de juiste tint kan alleen als\n * de wáárde een kleurnaam is (zie `kleurNaarCss`) — \"Print: Olifantje\" heeft geen tint.\n */\nexport function kleurOptie(product: Pick<Product, \"options\">): ProductOption | null {\n return (product.options ?? []).find((o) => /^kleur|^colou?r/i.test(o.name)) ?? null;\n}\n\n/**\n * De as waarvan élke waarde een eigen foto heeft — de as die het hoofdbeeld op de detailpagina\n * stuurt. Dit is losgekoppeld van de kleur-as (7.23): de winkelier zet per as een vinkje, dus het\n * mag net zo goed \"Print\" of \"Dessin\" zijn. Zonder expliciete keuze valt het terug op de oude\n * kleur-naamregel, zodat bestaande producten onveranderd blijven werken.\n *\n * Maar één as tegelijk: het hoofdbeeld kan niet twee keuzes tegelijk volgen. Staat de vlag per\n * ongeluk toch op meerdere assen (handmatig aangepaste data), dan wint de eerste.\n */\nexport function beeldOptie(product: Pick<Product, \"options\">): ProductOption | null {\n const opties = product.options ?? [];\n return opties.find((o) => o.imagePerValue === true) ?? opties.find((o) => optieHeeftBeeldPerWaarde(o)) ?? null;\n}\n"],"mappings":"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","names":[]}
@@ -0,0 +1,15 @@
1
+ /**
2
+ * "Wat is nieuw" (7.24) — korte, klantgerichte notities per release, getoond in /beheer.
3
+ *
4
+ * Features die je bouwt zonder dat de klant het hoort, bestaan voor die klant niet. Dit is de
5
+ * enige plek waar dat verteld wordt; `CoreVersion` toont de notitie van de huidige minor één keer.
6
+ * Alleen wat de KLANT merkt, in zijn woorden — geen interne refactors.
7
+ */
8
+ export interface ReleaseNote {
9
+ /** major.minor — patches tellen niet als "nieuw". */
10
+ versie: string;
11
+ punten: string[];
12
+ }
13
+ export declare const RELEASE_NOTES: ReleaseNote[];
14
+ /** De notitie die bij deze engine-versie hoort (op major.minor). */
15
+ export declare function releaseNoteVoor(versie: string): ReleaseNote | null;
@@ -0,0 +1,28 @@
1
+ const RELEASE_NOTES = [
2
+ {
3
+ versie: "7.24",
4
+ punten: [
5
+ "Het menu dat je onder Menu instelt staat nu \xE9cht op de site \u2014 met submenu's.",
6
+ "Bij Pagina's zie je meteen welke pagina's wijzigingen hebben die nog niet live staan.",
7
+ "Een pagina kun je offline halen (en later weer publiceren) of naar de prullenbak doen \u2014 30 dagen te herstellen.",
8
+ "Verander je het adres van een pagina, dan verwijst het oude adres automatisch door.",
9
+ "Deel een preview-link van een werkversie met iemand zonder account.",
10
+ "Kies een focuspunt op een foto zodat het juiste deel in beeld blijft op mobiel.",
11
+ "Formulieren: inzendingen exporteren als CSV, verwijderen, en zien wat nieuw is.",
12
+ "Statistieken tonen nu ook waar bezoekers vandaan komen en op welk apparaat."
13
+ ]
14
+ },
15
+ {
16
+ versie: "7.23",
17
+ punten: ["Per productoptie (bijvoorbeeld Print of Kleur) kun je aangeven dat elke waarde een eigen foto heeft."]
18
+ }
19
+ ];
20
+ function releaseNoteVoor(versie) {
21
+ const mm = versie.split(".").slice(0, 2).join(".");
22
+ return RELEASE_NOTES.find((n) => n.versie === mm) ?? null;
23
+ }
24
+ export {
25
+ RELEASE_NOTES,
26
+ releaseNoteVoor
27
+ };
28
+ //# sourceMappingURL=releaseNotes.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/releaseNotes.ts"],"sourcesContent":["/**\n * \"Wat is nieuw\" (7.24) — korte, klantgerichte notities per release, getoond in /beheer.\n *\n * Features die je bouwt zonder dat de klant het hoort, bestaan voor die klant niet. Dit is de\n * enige plek waar dat verteld wordt; `CoreVersion` toont de notitie van de huidige minor één keer.\n * Alleen wat de KLANT merkt, in zijn woorden — geen interne refactors.\n */\nexport interface ReleaseNote {\n /** major.minor — patches tellen niet als \"nieuw\". */\n versie: string;\n punten: string[];\n}\n\nexport const RELEASE_NOTES: ReleaseNote[] = [\n {\n versie: \"7.24\",\n punten: [\n \"Het menu dat je onder Menu instelt staat nu écht op de site — met submenu's.\",\n \"Bij Pagina's zie je meteen welke pagina's wijzigingen hebben die nog niet live staan.\",\n \"Een pagina kun je offline halen (en later weer publiceren) of naar de prullenbak doen — 30 dagen te herstellen.\",\n \"Verander je het adres van een pagina, dan verwijst het oude adres automatisch door.\",\n \"Deel een preview-link van een werkversie met iemand zonder account.\",\n \"Kies een focuspunt op een foto zodat het juiste deel in beeld blijft op mobiel.\",\n \"Formulieren: inzendingen exporteren als CSV, verwijderen, en zien wat nieuw is.\",\n \"Statistieken tonen nu ook waar bezoekers vandaan komen en op welk apparaat.\",\n ],\n },\n {\n versie: \"7.23\",\n punten: [\"Per productoptie (bijvoorbeeld Print of Kleur) kun je aangeven dat elke waarde een eigen foto heeft.\"],\n },\n];\n\n/** De notitie die bij deze engine-versie hoort (op major.minor). */\nexport function releaseNoteVoor(versie: string): ReleaseNote | null {\n const mm = versie.split(\".\").slice(0, 2).join(\".\");\n return RELEASE_NOTES.find((n) => n.versie === mm) ?? null;\n}\n"],"mappings":"AAaO,MAAM,gBAA+B;AAAA,EAC1C;AAAA,IACE,QAAQ;AAAA,IACR,QAAQ;AAAA,MACN;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,IACF;AAAA,EACF;AAAA,EACA;AAAA,IACE,QAAQ;AAAA,IACR,QAAQ,CAAC,sGAAsG;AAAA,EACjH;AACF;AAGO,SAAS,gBAAgB,QAAoC;AAClE,QAAM,KAAK,OAAO,MAAM,GAAG,EAAE,MAAM,GAAG,CAAC,EAAE,KAAK,GAAG;AACjD,SAAO,cAAc,KAAK,CAAC,MAAM,EAAE,WAAW,EAAE,KAAK;AACvD;","names":[]}
@@ -11,6 +11,15 @@ import * as z from "zod/mini";
11
11
  * die uit de pas kan lopen met de href. Het beheerscherm mag de gebruiker wél die keuze voorleggen;
12
12
  * wat er wordt opgeslagen is één ondubbelzinnig adres.
13
13
  */
14
+ declare const NavLeafSchema: z.ZodMiniObject<{
15
+ label: z.ZodMiniString<string>;
16
+ href: z.ZodMiniString<string>;
17
+ target: z.ZodMiniOptional<z.ZodMiniEnum<{
18
+ _blank: "_blank";
19
+ _embed: "_embed";
20
+ _self: "_self";
21
+ }>>;
22
+ }, z.core.$strip>;
14
23
  export declare const NavItemSchema: z.ZodMiniObject<{
15
24
  label: z.ZodMiniString<string>;
16
25
  href: z.ZodMiniString<string>;
@@ -19,10 +28,43 @@ export declare const NavItemSchema: z.ZodMiniObject<{
19
28
  _embed: "_embed";
20
29
  _self: "_self";
21
30
  }>>;
31
+ children: z.ZodMiniOptional<z.ZodMiniArray<z.ZodMiniObject<{
32
+ label: z.ZodMiniString<string>;
33
+ href: z.ZodMiniString<string>;
34
+ target: z.ZodMiniOptional<z.ZodMiniEnum<{
35
+ _blank: "_blank";
36
+ _embed: "_embed";
37
+ _self: "_self";
38
+ }>>;
39
+ }, z.core.$strip>>>;
22
40
  }, z.core.$strip>;
23
41
  export type NavItem = z.infer<typeof NavItemSchema>;
42
+ export type NavLeaf = z.infer<typeof NavLeafSchema>;
24
43
  /** Verwijst dit item naar buiten? Afgeleid uit de href, zodat er geen tweede waarheid ontstaat. */
25
44
  export declare function isExterneLink(href: string): boolean;
45
+ /**
46
+ * Eén doorverwijzing (7.24): een oud pad dat permanent naar een nieuw pad wijst.
47
+ *
48
+ * Ontstaat vanzelf wanneer een klant het pad van een pagina wijzigt — de oude URL staat in
49
+ * zoekmachines, in mails en op visitekaartjes, en zonder redirect wordt dat een 404. Alleen
50
+ * interne paden (beginnen met `/`): een open redirect naar buiten is een phishing-hulpmiddel.
51
+ */
52
+ export declare const RedirectSchema: z.ZodMiniObject<{
53
+ from: z.ZodMiniString<string>;
54
+ to: z.ZodMiniString<string>;
55
+ }, z.core.$strip>;
56
+ export type Redirect = z.infer<typeof RedirectSchema>;
57
+ /**
58
+ * Normaliseert een pad zoals de catch-all dat doet ("/over-ons/" → "/over-ons", "" → "/"), zodat
59
+ * een redirect matcht ongeacht een afsluitende slash.
60
+ */
61
+ export declare function normaliseerPad(pad: string): string;
62
+ /**
63
+ * Zoekt de bestemming voor een pad in de redirect-lijst. Volgt ketens (a→b, b→c) tot een diepte
64
+ * van vijf en weigert lussen: een klant die eerst /a naar /b hernoemt en later terug, mag daar
65
+ * geen eindeloze redirect aan overhouden. `null` = geen doorverwijzing.
66
+ */
67
+ export declare function zoekRedirect(pad: string, redirects: Redirect[] | undefined): string | null;
26
68
  /** Het pad waarop een ingekaderde externe pagina wordt getoond. */
27
69
  export declare const EMBED_PAD = "/extern";
28
70
  /**
@@ -115,6 +157,15 @@ export declare const FooterColumnSchema: z.ZodMiniObject<{
115
157
  _embed: "_embed";
116
158
  _self: "_self";
117
159
  }>>;
160
+ children: z.ZodMiniOptional<z.ZodMiniArray<z.ZodMiniObject<{
161
+ label: z.ZodMiniString<string>;
162
+ href: z.ZodMiniString<string>;
163
+ target: z.ZodMiniOptional<z.ZodMiniEnum<{
164
+ _blank: "_blank";
165
+ _embed: "_embed";
166
+ _self: "_self";
167
+ }>>;
168
+ }, z.core.$strip>>>;
118
169
  }, z.core.$strip>>>;
119
170
  }, z.core.$strip>;
120
171
  /** Bewerkbare footer (klant past dit aan in /beheer; core levert de unstyled `SiteFooter`). */
@@ -129,6 +180,15 @@ export declare const FooterSettingsSchema: z.ZodMiniObject<{
129
180
  _embed: "_embed";
130
181
  _self: "_self";
131
182
  }>>;
183
+ children: z.ZodMiniOptional<z.ZodMiniArray<z.ZodMiniObject<{
184
+ label: z.ZodMiniString<string>;
185
+ href: z.ZodMiniString<string>;
186
+ target: z.ZodMiniOptional<z.ZodMiniEnum<{
187
+ _blank: "_blank";
188
+ _embed: "_embed";
189
+ _self: "_self";
190
+ }>>;
191
+ }, z.core.$strip>>>;
132
192
  }, z.core.$strip>>>;
133
193
  }, z.core.$strip>>>;
134
194
  social: z.ZodMiniOptional<z.ZodMiniArray<z.ZodMiniObject<{
@@ -157,6 +217,15 @@ export declare const SiteSettingsSchema: z.ZodMiniObject<{
157
217
  _embed: "_embed";
158
218
  _self: "_self";
159
219
  }>>;
220
+ children: z.ZodMiniOptional<z.ZodMiniArray<z.ZodMiniObject<{
221
+ label: z.ZodMiniString<string>;
222
+ href: z.ZodMiniString<string>;
223
+ target: z.ZodMiniOptional<z.ZodMiniEnum<{
224
+ _blank: "_blank";
225
+ _embed: "_embed";
226
+ _self: "_self";
227
+ }>>;
228
+ }, z.core.$strip>>>;
160
229
  }, z.core.$strip>>>;
161
230
  theme: z.ZodMiniOptional<z.ZodMiniObject<{
162
231
  primary: z.ZodMiniString<string>;
@@ -223,6 +292,15 @@ export declare const SiteSettingsSchema: z.ZodMiniObject<{
223
292
  _embed: "_embed";
224
293
  _self: "_self";
225
294
  }>>;
295
+ children: z.ZodMiniOptional<z.ZodMiniArray<z.ZodMiniObject<{
296
+ label: z.ZodMiniString<string>;
297
+ href: z.ZodMiniString<string>;
298
+ target: z.ZodMiniOptional<z.ZodMiniEnum<{
299
+ _blank: "_blank";
300
+ _embed: "_embed";
301
+ _self: "_self";
302
+ }>>;
303
+ }, z.core.$strip>>>;
226
304
  }, z.core.$strip>>>;
227
305
  }, z.core.$strip>>>;
228
306
  social: z.ZodMiniOptional<z.ZodMiniArray<z.ZodMiniObject<{
@@ -240,6 +318,10 @@ export declare const SiteSettingsSchema: z.ZodMiniObject<{
240
318
  label: z.ZodMiniString<string>;
241
319
  href: z.ZodMiniString<string>;
242
320
  }, z.core.$strip>>>;
321
+ redirects: z.ZodMiniOptional<z.ZodMiniArray<z.ZodMiniObject<{
322
+ from: z.ZodMiniString<string>;
323
+ to: z.ZodMiniString<string>;
324
+ }, z.core.$strip>>>;
243
325
  }, z.core.$strip>;
244
326
  export type SiteSettings = z.infer<typeof SiteSettingsSchema>;
245
327
  /**
@@ -257,6 +339,15 @@ export declare const PublicSiteSettingsSchema: z.ZodMiniObject<{
257
339
  _embed: "_embed";
258
340
  _self: "_self";
259
341
  }>>;
342
+ children: z.ZodMiniOptional<z.ZodMiniArray<z.ZodMiniObject<{
343
+ label: z.ZodMiniString<string>;
344
+ href: z.ZodMiniString<string>;
345
+ target: z.ZodMiniOptional<z.ZodMiniEnum<{
346
+ _blank: "_blank";
347
+ _embed: "_embed";
348
+ _self: "_self";
349
+ }>>;
350
+ }, z.core.$strip>>>;
260
351
  }, z.core.$strip>>>;
261
352
  theme: z.ZodMiniOptional<z.ZodMiniObject<{
262
353
  primary: z.ZodMiniString<string>;
@@ -314,6 +405,15 @@ export declare const PublicSiteSettingsSchema: z.ZodMiniObject<{
314
405
  _embed: "_embed";
315
406
  _self: "_self";
316
407
  }>>;
408
+ children: z.ZodMiniOptional<z.ZodMiniArray<z.ZodMiniObject<{
409
+ label: z.ZodMiniString<string>;
410
+ href: z.ZodMiniString<string>;
411
+ target: z.ZodMiniOptional<z.ZodMiniEnum<{
412
+ _blank: "_blank";
413
+ _embed: "_embed";
414
+ _self: "_self";
415
+ }>>;
416
+ }, z.core.$strip>>>;
317
417
  }, z.core.$strip>>>;
318
418
  }, z.core.$strip>>>;
319
419
  social: z.ZodMiniOptional<z.ZodMiniArray<z.ZodMiniObject<{
@@ -327,7 +427,12 @@ export declare const PublicSiteSettingsSchema: z.ZodMiniObject<{
327
427
  enabled: z.ZodMiniOptional<z.ZodMiniBoolean<boolean>>;
328
428
  config: z.ZodMiniOptional<z.ZodMiniRecord<z.ZodMiniString<string>, z.ZodMiniUnknown>>;
329
429
  }, z.core.$strip>]>>>;
430
+ redirects: z.ZodMiniOptional<z.ZodMiniArray<z.ZodMiniObject<{
431
+ from: z.ZodMiniString<string>;
432
+ to: z.ZodMiniString<string>;
433
+ }, z.core.$strip>>>;
330
434
  }, z.core.$strip>;
331
435
  export type PublicSiteSettings = z.infer<typeof PublicSiteSettingsSchema>;
332
436
  /** Haalt de publiek-veilige subset uit de volledige settings (voor het `settings/public`-doc). */
333
437
  export declare function toPublicSettings(s: SiteSettings): PublicSiteSettings;
438
+ export {};
@@ -1,6 +1,6 @@
1
1
  import * as z from "zod/mini";
2
2
  import { SeoSchema } from "./seo.js";
3
- const NavItemSchema = z.object({
3
+ const NavLeafSchema = z.object({
4
4
  label: z.string().check(z.minLength(1)).check(z.maxLength(80)),
5
5
  href: z.string().check(z.minLength(1)).check(z.maxLength(2e3)),
6
6
  /**
@@ -9,9 +9,42 @@ const NavItemSchema = z.object({
9
9
  */
10
10
  target: z.optional(z.enum(["_self", "_blank", "_embed"]))
11
11
  });
12
+ const NavItemSchema = z.object({
13
+ ...NavLeafSchema.shape,
14
+ /**
15
+ * Submenu (7.24): één niveau diep, bewust niet recursief. "Diensten ▾ Tuinaanleg / Onderhoud"
16
+ * dekt vrijwel elke bedrijfssite; een tweede niveau is op mobiel niet meer te bedienen en op
17
+ * desktop een hover-doolhof. Een item mét kinderen blijft zelf klikbaar (href = de
18
+ * overzichtspagina), zodat het menu ook zonder JavaScript en met het toetsenbord werkt.
19
+ */
20
+ children: z.optional(z.array(NavLeafSchema).check(z.maxLength(20)))
21
+ });
12
22
  function isExterneLink(href) {
13
23
  return /^https?:\/\//i.test(href);
14
24
  }
25
+ const RedirectSchema = z.object({
26
+ from: z.string().check(z.minLength(1)).check(z.maxLength(400)).check(z.regex(/^\/[^\s?#]*$/, "een pad begint met / en bevat geen spaties")),
27
+ to: z.string().check(z.minLength(1)).check(z.maxLength(400)).check(z.regex(/^\/[^\s]*$/, "een pad begint met /"))
28
+ });
29
+ function normaliseerPad(pad) {
30
+ const p = pad.split("?")[0].split("#")[0].replace(/\/+$/, "");
31
+ return p === "" ? "/" : p.startsWith("/") ? p : `/${p}`;
32
+ }
33
+ function zoekRedirect(pad, redirects) {
34
+ if (!redirects?.length) return null;
35
+ const kaart = new Map(redirects.map((r) => [normaliseerPad(r.from), normaliseerPad(r.to)]));
36
+ let huidig = normaliseerPad(pad);
37
+ const gezien = /* @__PURE__ */ new Set([huidig]);
38
+ let doel = null;
39
+ for (let i = 0; i < 5; i++) {
40
+ const volgende = kaart.get(huidig);
41
+ if (!volgende || gezien.has(volgende)) break;
42
+ gezien.add(volgende);
43
+ doel = volgende;
44
+ huidig = volgende;
45
+ }
46
+ return doel;
47
+ }
15
48
  const EMBED_PAD = "/extern";
16
49
  function navHref(item) {
17
50
  if (item.target === "_embed" && isExterneLink(item.href)) {
@@ -130,7 +163,9 @@ const SiteSettingsSchema = z.object({
130
163
  dashboardLinks: z.optional(z.array(z.object({
131
164
  label: z.string().check(z.minLength(1)).check(z.maxLength(80)),
132
165
  href: z.string().check(z.minLength(1)).check(z.maxLength(2e3))
133
- })).check(z.maxLength(12)))
166
+ })).check(z.maxLength(12))),
167
+ /** Doorverwijzingen van oude naar nieuwe paden (7.24). Publiek: de catch-all leest ze. */
168
+ redirects: z.optional(z.array(RedirectSchema).check(z.maxLength(200)))
134
169
  });
135
170
  const PublicSiteSettingsSchema = z.object({
136
171
  name: z.optional(z.string().check(z.maxLength(160))),
@@ -138,7 +173,8 @@ const PublicSiteSettingsSchema = z.object({
138
173
  theme: z.optional(ThemeSchema),
139
174
  seoDefaults: z.optional(SeoSchema),
140
175
  footer: z.optional(FooterSettingsSchema),
141
- modules: z.optional(ModulesSettingsSchema)
176
+ modules: z.optional(ModulesSettingsSchema),
177
+ redirects: z.optional(z.array(RedirectSchema).check(z.maxLength(200)))
142
178
  });
143
179
  function toPublicSettings(s) {
144
180
  return {
@@ -147,7 +183,8 @@ function toPublicSettings(s) {
147
183
  theme: s.theme,
148
184
  seoDefaults: s.seoDefaults,
149
185
  footer: s.footer,
150
- modules: s.modules
186
+ modules: s.modules,
187
+ redirects: s.redirects
151
188
  };
152
189
  }
153
190
  export {
@@ -158,6 +195,7 @@ export {
158
195
  ModulesSettingsSchema,
159
196
  NavItemSchema,
160
197
  PublicSiteSettingsSchema,
198
+ RedirectSchema,
161
199
  SiteSettingsSchema,
162
200
  SocialLinkSchema,
163
201
  ThemeSchema,
@@ -165,6 +203,8 @@ export {
165
203
  isToegestaneEmbed,
166
204
  mergeThemeTokens,
167
205
  navHref,
168
- toPublicSettings
206
+ normaliseerPad,
207
+ toPublicSettings,
208
+ zoekRedirect
169
209
  };
170
210
  //# sourceMappingURL=settings.js.map