@reblu/site-blocks 0.4.4 → 0.5.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/CHANGELOG.md +57 -0
- package/README.md +90 -0
- package/dist/index.cjs +191 -2
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +108 -2
- package/dist/index.d.ts +108 -2
- package/dist/index.js +187 -1
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/index.d.cts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
2
|
-
import { PageDetail, PageListItem, ShopProductListItem } from '@reblu/site-contracts';
|
|
2
|
+
import { PageDetail, PageListItem, ShopProductListItem, ShopFacets } from '@reblu/site-contracts';
|
|
3
3
|
|
|
4
4
|
declare function readingTime(content: string | null | undefined): number | null;
|
|
5
5
|
|
|
@@ -277,4 +277,110 @@ interface ProductCardProps {
|
|
|
277
277
|
}
|
|
278
278
|
declare function ProductCard({ product, href, currencyCode, locale, className }: ProductCardProps): react_jsx_runtime.JSX.Element;
|
|
279
279
|
|
|
280
|
-
|
|
280
|
+
/**
|
|
281
|
+
* `ShopFilters` — plug-and-play SITE filter chrome over `GET /api/site/shop/facets`
|
|
282
|
+
* (REB-809). Which axes appear, their order and labels come entirely from
|
|
283
|
+
* `facets` — nexus config, not this component — so no SITE hand-builds shop
|
|
284
|
+
* filter UI again (the gap this issue closes: dermamedicin's SITE hardcoded
|
|
285
|
+
* `BRAND_AXIS = 'brand'` while the tenant's real axis key is `marca`).
|
|
286
|
+
*
|
|
287
|
+
* RSC-safe by construction (REB-647 lesson): every option always renders a real
|
|
288
|
+
* `<a href>` built by the `hrefs` prop, so importing and rendering this from an
|
|
289
|
+
* actual Server Component — no `onChange`, navigation only — never crashes and
|
|
290
|
+
* never attaches an event handler. `onChange` is strictly additive: when
|
|
291
|
+
* provided, a click is intercepted (`preventDefault` + call `onChange`) instead
|
|
292
|
+
* of navigating. Attaching a function prop to a host element only works when
|
|
293
|
+
* the call site itself is (or is inside) a Client Component — pass `onChange`
|
|
294
|
+
* only from a `'use client'` boundary; from a Server Component, omit it and let
|
|
295
|
+
* `hrefs` alone drive navigation (a real page load / your router's own Link).
|
|
296
|
+
*
|
|
297
|
+
* ── Single-select everywhere, on purpose ──────────────────────────────────
|
|
298
|
+
* `facets.taxonomies[].selectionMode` (REB-809/REB-801) reflects how many terms
|
|
299
|
+
* a PRODUCT may carry on that axis — it is NOT how many the storefront filter
|
|
300
|
+
* may select at once. `GET /api/site/shop/products`'s `taxonomy[<key>]` query
|
|
301
|
+
* param accepts exactly one term slug per axis today (`parseListParams` in
|
|
302
|
+
* base-api collapses any repeat), so every taxonomy axis renders single-select
|
|
303
|
+
* filter UI here regardless of `selectionMode` — clicking the active term's own
|
|
304
|
+
* link clears the axis, clicking another replaces it. Wiring `selectionMode`
|
|
305
|
+
* into a true multi-value taxonomy filter needs the query contract to grow an
|
|
306
|
+
* array form first; that is out of this component's scope.
|
|
307
|
+
*
|
|
308
|
+
* Attribute facets (`facets.attributes`) are genuinely multi-select already —
|
|
309
|
+
* `attr[<key>]` repeats to OR-match several values — so those render as a
|
|
310
|
+
* checkbox-shaped list where more than one option may be active at once.
|
|
311
|
+
*
|
|
312
|
+
* Tokens only: no brand aesthetic, every color/spacing class is a design
|
|
313
|
+
* token so the same component renders correctly for every tenant theme.
|
|
314
|
+
*/
|
|
315
|
+
/** One price band. `min`/`max` are integer cents; either may be omitted for an open end. */
|
|
316
|
+
interface ShopFilterPriceBand {
|
|
317
|
+
min?: number;
|
|
318
|
+
max?: number;
|
|
319
|
+
/** Overrides the auto-generated `formatPrice`-based label. */
|
|
320
|
+
label?: string;
|
|
321
|
+
}
|
|
322
|
+
/** Sensible EUR defaults — override via the `priceBands` prop for another currency/locale. */
|
|
323
|
+
declare const DEFAULT_SHOP_PRICE_BANDS: ShopFilterPriceBand[];
|
|
324
|
+
/** The filter selection currently reflected in the URL (or component state). */
|
|
325
|
+
interface ShopFiltersActive {
|
|
326
|
+
/** axis key → active term slug. */
|
|
327
|
+
taxonomy?: Record<string, string>;
|
|
328
|
+
/** field key → active value(s). */
|
|
329
|
+
attr?: Record<string, string | string[]>;
|
|
330
|
+
priceMin?: number;
|
|
331
|
+
priceMax?: number;
|
|
332
|
+
onSale?: boolean;
|
|
333
|
+
}
|
|
334
|
+
/**
|
|
335
|
+
* Href generators — the RSC-safe navigation seam. A consumer typically supplies
|
|
336
|
+
* these from `@reblu/site-next`'s shop URL helpers (`parseShopSearchParams` /
|
|
337
|
+
* `serializeShopParams`), which preserve every other current param and reset
|
|
338
|
+
* pagination on change.
|
|
339
|
+
*/
|
|
340
|
+
interface ShopFiltersHrefs {
|
|
341
|
+
/** `value` undefined clears the axis (renders the "all" option's href). */
|
|
342
|
+
taxonomy: (axisKey: string, value: string | undefined) => string;
|
|
343
|
+
/** Href toggling `value` in/out of `fieldKey`'s active value set. */
|
|
344
|
+
attr: (fieldKey: string, value: string) => string;
|
|
345
|
+
price: (min: number | undefined, max: number | undefined) => string;
|
|
346
|
+
onSale: (nextActive: boolean) => string;
|
|
347
|
+
clearAll: () => string;
|
|
348
|
+
}
|
|
349
|
+
interface ShopFiltersLabels {
|
|
350
|
+
/** The "show every term of this axis" option. */
|
|
351
|
+
allTerms: string;
|
|
352
|
+
clearAll: string;
|
|
353
|
+
onSale: string;
|
|
354
|
+
price: string;
|
|
355
|
+
/** Open-ended "up to X" band, e.g. `(formatted) => \`Under ${formatted}\`` (the default). */
|
|
356
|
+
priceUnder: (formattedMax: string) => string;
|
|
357
|
+
/** Open-ended "X or more" band, e.g. `(formatted) => \`${formatted}+\`` (the default). */
|
|
358
|
+
priceOver: (formattedMin: string) => string;
|
|
359
|
+
/** Bounded band, e.g. `(min, max) => \`${min} – ${max}\`` (the default). */
|
|
360
|
+
priceRange: (formattedMin: string, formattedMax: string) => string;
|
|
361
|
+
}
|
|
362
|
+
interface ShopFiltersProps {
|
|
363
|
+
facets: ShopFacets;
|
|
364
|
+
hrefs: ShopFiltersHrefs;
|
|
365
|
+
active?: ShopFiltersActive;
|
|
366
|
+
/** Client-only: intercepts navigation with a callback instead. See the module doc. */
|
|
367
|
+
onChange?: (patch: Partial<ShopFiltersActive>) => void;
|
|
368
|
+
priceBands?: ShopFilterPriceBand[];
|
|
369
|
+
labels?: Partial<ShopFiltersLabels>;
|
|
370
|
+
/** Forwarded to `formatPrice` for price-band labels. Defaults match `ProductCard`'s (EUR/es-ES). */
|
|
371
|
+
currencyCode?: string;
|
|
372
|
+
locale?: string;
|
|
373
|
+
className?: string;
|
|
374
|
+
}
|
|
375
|
+
/**
|
|
376
|
+
* The exact "would `ShopFilters` render anything at all" predicate — exported
|
|
377
|
+
* so a consuming page can decide whether to render a wrapping `<aside>` (or
|
|
378
|
+
* similar landmark) around it WITHOUT hand-copying this logic (a copy that
|
|
379
|
+
* can silently drift from the component's own decision). True when `facets`
|
|
380
|
+
* has at least one populated dimension, OR `active` has a selection to clear
|
|
381
|
+
* (see R3-null-render-strands-active-filters above).
|
|
382
|
+
*/
|
|
383
|
+
declare function shouldRenderShopFilters(facets: ShopFacets, active?: ShopFiltersActive): boolean;
|
|
384
|
+
declare function ShopFilters({ facets, hrefs, active, onChange, priceBands, labels, currencyCode, locale, className, }: ShopFiltersProps): react_jsx_runtime.JSX.Element | null;
|
|
385
|
+
|
|
386
|
+
export { ALLOWED_YOUTUBE_HOSTS, DEFAULT_SHOP_PRICE_BANDS, MarkdownRenderer, type MarkdownRendererProps, ProductCard, type ProductCardProps, ProfileCard, type ProfileCardProps, SafeHtml, type SafeHtmlProps, type ShareUrls, type ShopFilterPriceBand, ShopFilters, type ShopFiltersActive, type ShopFiltersHrefs, type ShopFiltersLabels, type ShopFiltersProps, Specialty, type SpecialtyProps, Treatment, type TreatmentProps, buildShareUrls, formatPrice, isAllowedYouTubeHost, markdownToSafeHtml, readingTime, sanitizeHtml, shouldRenderShopFilters };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
2
|
-
import { PageDetail, PageListItem, ShopProductListItem } from '@reblu/site-contracts';
|
|
2
|
+
import { PageDetail, PageListItem, ShopProductListItem, ShopFacets } from '@reblu/site-contracts';
|
|
3
3
|
|
|
4
4
|
declare function readingTime(content: string | null | undefined): number | null;
|
|
5
5
|
|
|
@@ -277,4 +277,110 @@ interface ProductCardProps {
|
|
|
277
277
|
}
|
|
278
278
|
declare function ProductCard({ product, href, currencyCode, locale, className }: ProductCardProps): react_jsx_runtime.JSX.Element;
|
|
279
279
|
|
|
280
|
-
|
|
280
|
+
/**
|
|
281
|
+
* `ShopFilters` — plug-and-play SITE filter chrome over `GET /api/site/shop/facets`
|
|
282
|
+
* (REB-809). Which axes appear, their order and labels come entirely from
|
|
283
|
+
* `facets` — nexus config, not this component — so no SITE hand-builds shop
|
|
284
|
+
* filter UI again (the gap this issue closes: dermamedicin's SITE hardcoded
|
|
285
|
+
* `BRAND_AXIS = 'brand'` while the tenant's real axis key is `marca`).
|
|
286
|
+
*
|
|
287
|
+
* RSC-safe by construction (REB-647 lesson): every option always renders a real
|
|
288
|
+
* `<a href>` built by the `hrefs` prop, so importing and rendering this from an
|
|
289
|
+
* actual Server Component — no `onChange`, navigation only — never crashes and
|
|
290
|
+
* never attaches an event handler. `onChange` is strictly additive: when
|
|
291
|
+
* provided, a click is intercepted (`preventDefault` + call `onChange`) instead
|
|
292
|
+
* of navigating. Attaching a function prop to a host element only works when
|
|
293
|
+
* the call site itself is (or is inside) a Client Component — pass `onChange`
|
|
294
|
+
* only from a `'use client'` boundary; from a Server Component, omit it and let
|
|
295
|
+
* `hrefs` alone drive navigation (a real page load / your router's own Link).
|
|
296
|
+
*
|
|
297
|
+
* ── Single-select everywhere, on purpose ──────────────────────────────────
|
|
298
|
+
* `facets.taxonomies[].selectionMode` (REB-809/REB-801) reflects how many terms
|
|
299
|
+
* a PRODUCT may carry on that axis — it is NOT how many the storefront filter
|
|
300
|
+
* may select at once. `GET /api/site/shop/products`'s `taxonomy[<key>]` query
|
|
301
|
+
* param accepts exactly one term slug per axis today (`parseListParams` in
|
|
302
|
+
* base-api collapses any repeat), so every taxonomy axis renders single-select
|
|
303
|
+
* filter UI here regardless of `selectionMode` — clicking the active term's own
|
|
304
|
+
* link clears the axis, clicking another replaces it. Wiring `selectionMode`
|
|
305
|
+
* into a true multi-value taxonomy filter needs the query contract to grow an
|
|
306
|
+
* array form first; that is out of this component's scope.
|
|
307
|
+
*
|
|
308
|
+
* Attribute facets (`facets.attributes`) are genuinely multi-select already —
|
|
309
|
+
* `attr[<key>]` repeats to OR-match several values — so those render as a
|
|
310
|
+
* checkbox-shaped list where more than one option may be active at once.
|
|
311
|
+
*
|
|
312
|
+
* Tokens only: no brand aesthetic, every color/spacing class is a design
|
|
313
|
+
* token so the same component renders correctly for every tenant theme.
|
|
314
|
+
*/
|
|
315
|
+
/** One price band. `min`/`max` are integer cents; either may be omitted for an open end. */
|
|
316
|
+
interface ShopFilterPriceBand {
|
|
317
|
+
min?: number;
|
|
318
|
+
max?: number;
|
|
319
|
+
/** Overrides the auto-generated `formatPrice`-based label. */
|
|
320
|
+
label?: string;
|
|
321
|
+
}
|
|
322
|
+
/** Sensible EUR defaults — override via the `priceBands` prop for another currency/locale. */
|
|
323
|
+
declare const DEFAULT_SHOP_PRICE_BANDS: ShopFilterPriceBand[];
|
|
324
|
+
/** The filter selection currently reflected in the URL (or component state). */
|
|
325
|
+
interface ShopFiltersActive {
|
|
326
|
+
/** axis key → active term slug. */
|
|
327
|
+
taxonomy?: Record<string, string>;
|
|
328
|
+
/** field key → active value(s). */
|
|
329
|
+
attr?: Record<string, string | string[]>;
|
|
330
|
+
priceMin?: number;
|
|
331
|
+
priceMax?: number;
|
|
332
|
+
onSale?: boolean;
|
|
333
|
+
}
|
|
334
|
+
/**
|
|
335
|
+
* Href generators — the RSC-safe navigation seam. A consumer typically supplies
|
|
336
|
+
* these from `@reblu/site-next`'s shop URL helpers (`parseShopSearchParams` /
|
|
337
|
+
* `serializeShopParams`), which preserve every other current param and reset
|
|
338
|
+
* pagination on change.
|
|
339
|
+
*/
|
|
340
|
+
interface ShopFiltersHrefs {
|
|
341
|
+
/** `value` undefined clears the axis (renders the "all" option's href). */
|
|
342
|
+
taxonomy: (axisKey: string, value: string | undefined) => string;
|
|
343
|
+
/** Href toggling `value` in/out of `fieldKey`'s active value set. */
|
|
344
|
+
attr: (fieldKey: string, value: string) => string;
|
|
345
|
+
price: (min: number | undefined, max: number | undefined) => string;
|
|
346
|
+
onSale: (nextActive: boolean) => string;
|
|
347
|
+
clearAll: () => string;
|
|
348
|
+
}
|
|
349
|
+
interface ShopFiltersLabels {
|
|
350
|
+
/** The "show every term of this axis" option. */
|
|
351
|
+
allTerms: string;
|
|
352
|
+
clearAll: string;
|
|
353
|
+
onSale: string;
|
|
354
|
+
price: string;
|
|
355
|
+
/** Open-ended "up to X" band, e.g. `(formatted) => \`Under ${formatted}\`` (the default). */
|
|
356
|
+
priceUnder: (formattedMax: string) => string;
|
|
357
|
+
/** Open-ended "X or more" band, e.g. `(formatted) => \`${formatted}+\`` (the default). */
|
|
358
|
+
priceOver: (formattedMin: string) => string;
|
|
359
|
+
/** Bounded band, e.g. `(min, max) => \`${min} – ${max}\`` (the default). */
|
|
360
|
+
priceRange: (formattedMin: string, formattedMax: string) => string;
|
|
361
|
+
}
|
|
362
|
+
interface ShopFiltersProps {
|
|
363
|
+
facets: ShopFacets;
|
|
364
|
+
hrefs: ShopFiltersHrefs;
|
|
365
|
+
active?: ShopFiltersActive;
|
|
366
|
+
/** Client-only: intercepts navigation with a callback instead. See the module doc. */
|
|
367
|
+
onChange?: (patch: Partial<ShopFiltersActive>) => void;
|
|
368
|
+
priceBands?: ShopFilterPriceBand[];
|
|
369
|
+
labels?: Partial<ShopFiltersLabels>;
|
|
370
|
+
/** Forwarded to `formatPrice` for price-band labels. Defaults match `ProductCard`'s (EUR/es-ES). */
|
|
371
|
+
currencyCode?: string;
|
|
372
|
+
locale?: string;
|
|
373
|
+
className?: string;
|
|
374
|
+
}
|
|
375
|
+
/**
|
|
376
|
+
* The exact "would `ShopFilters` render anything at all" predicate — exported
|
|
377
|
+
* so a consuming page can decide whether to render a wrapping `<aside>` (or
|
|
378
|
+
* similar landmark) around it WITHOUT hand-copying this logic (a copy that
|
|
379
|
+
* can silently drift from the component's own decision). True when `facets`
|
|
380
|
+
* has at least one populated dimension, OR `active` has a selection to clear
|
|
381
|
+
* (see R3-null-render-strands-active-filters above).
|
|
382
|
+
*/
|
|
383
|
+
declare function shouldRenderShopFilters(facets: ShopFacets, active?: ShopFiltersActive): boolean;
|
|
384
|
+
declare function ShopFilters({ facets, hrefs, active, onChange, priceBands, labels, currencyCode, locale, className, }: ShopFiltersProps): react_jsx_runtime.JSX.Element | null;
|
|
385
|
+
|
|
386
|
+
export { ALLOWED_YOUTUBE_HOSTS, DEFAULT_SHOP_PRICE_BANDS, MarkdownRenderer, type MarkdownRendererProps, ProductCard, type ProductCardProps, ProfileCard, type ProfileCardProps, SafeHtml, type SafeHtmlProps, type ShareUrls, type ShopFilterPriceBand, ShopFilters, type ShopFiltersActive, type ShopFiltersHrefs, type ShopFiltersLabels, type ShopFiltersProps, Specialty, type SpecialtyProps, Treatment, type TreatmentProps, buildShareUrls, formatPrice, isAllowedYouTubeHost, markdownToSafeHtml, readingTime, sanitizeHtml, shouldRenderShopFilters };
|
package/dist/index.js
CHANGED
|
@@ -13401,12 +13401,197 @@ function ProductCard({ product, href, currencyCode = "EUR", locale, className })
|
|
|
13401
13401
|
}
|
|
13402
13402
|
);
|
|
13403
13403
|
}
|
|
13404
|
+
|
|
13405
|
+
// src/components/shop-filters.tsx
|
|
13406
|
+
import { cn as cn7 } from "@reblu/site-ui/utils";
|
|
13407
|
+
import { jsx as jsx7, jsxs as jsxs6 } from "react/jsx-runtime";
|
|
13408
|
+
var DEFAULT_SHOP_PRICE_BANDS = [
|
|
13409
|
+
{ max: 2500 },
|
|
13410
|
+
{ min: 2500, max: 5e3 },
|
|
13411
|
+
{ min: 5e3, max: 1e4 },
|
|
13412
|
+
{ min: 1e4 }
|
|
13413
|
+
];
|
|
13414
|
+
var DEFAULT_LABELS = {
|
|
13415
|
+
allTerms: "All",
|
|
13416
|
+
clearAll: "Clear filters",
|
|
13417
|
+
onSale: "On sale",
|
|
13418
|
+
price: "Price",
|
|
13419
|
+
priceUnder: (formattedMax) => `Under ${formattedMax}`,
|
|
13420
|
+
priceOver: (formattedMin) => `${formattedMin}+`,
|
|
13421
|
+
priceRange: (formattedMin, formattedMax) => `${formattedMin} \u2013 ${formattedMax}`
|
|
13422
|
+
};
|
|
13423
|
+
function toValueArray(value) {
|
|
13424
|
+
if (value === void 0) return [];
|
|
13425
|
+
return Array.isArray(value) ? value : [value];
|
|
13426
|
+
}
|
|
13427
|
+
function hasAnyActiveFilter(active) {
|
|
13428
|
+
return Object.keys(active.taxonomy ?? {}).length > 0 || Object.keys(active.attr ?? {}).length > 0 || active.priceMin !== void 0 || active.priceMax !== void 0 || Boolean(active.onSale);
|
|
13429
|
+
}
|
|
13430
|
+
function shouldRenderShopFilters(facets, active = {}) {
|
|
13431
|
+
const hasFacetContent = facets.taxonomies.some((axis) => axis.terms.length > 0) || facets.attributes.some((attr) => attr.options.length > 0) || facets.price.max > facets.price.min || facets.onSale.count > 0;
|
|
13432
|
+
return hasFacetContent || hasAnyActiveFilter(active);
|
|
13433
|
+
}
|
|
13434
|
+
function priceBandLabel(band, t, currencyCode, locale) {
|
|
13435
|
+
if (band.label) return band.label;
|
|
13436
|
+
if (band.min === void 0 && band.max !== void 0) {
|
|
13437
|
+
return t.priceUnder(formatPrice(band.max, currencyCode, locale));
|
|
13438
|
+
}
|
|
13439
|
+
if (band.max === void 0 && band.min !== void 0) {
|
|
13440
|
+
return t.priceOver(formatPrice(band.min, currencyCode, locale));
|
|
13441
|
+
}
|
|
13442
|
+
if (band.min !== void 0 && band.max !== void 0) {
|
|
13443
|
+
return t.priceRange(formatPrice(band.min, currencyCode, locale), formatPrice(band.max, currencyCode, locale));
|
|
13444
|
+
}
|
|
13445
|
+
return "";
|
|
13446
|
+
}
|
|
13447
|
+
function OptionLink({ href, active, label, onActivate }) {
|
|
13448
|
+
return /* @__PURE__ */ jsx7(
|
|
13449
|
+
"a",
|
|
13450
|
+
{
|
|
13451
|
+
href,
|
|
13452
|
+
"aria-current": active ? "true" : void 0,
|
|
13453
|
+
onClick: onActivate ? (event) => {
|
|
13454
|
+
event.preventDefault();
|
|
13455
|
+
onActivate();
|
|
13456
|
+
} : void 0,
|
|
13457
|
+
className: cn7(
|
|
13458
|
+
"inline-flex items-center rounded-full border px-3 py-1 text-sm transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
|
|
13459
|
+
active ? "border-transparent bg-primary text-primary-foreground" : "border-border text-muted-foreground hover:bg-muted hover:text-foreground"
|
|
13460
|
+
),
|
|
13461
|
+
children: label
|
|
13462
|
+
}
|
|
13463
|
+
);
|
|
13464
|
+
}
|
|
13465
|
+
function FilterFieldset({ legend, children: children2 }) {
|
|
13466
|
+
return /* @__PURE__ */ jsxs6("fieldset", { className: "space-y-2", children: [
|
|
13467
|
+
/* @__PURE__ */ jsx7("legend", { className: "text-sm font-medium text-foreground", children: legend }),
|
|
13468
|
+
/* @__PURE__ */ jsx7("div", { className: "flex flex-wrap gap-2", children: children2 })
|
|
13469
|
+
] });
|
|
13470
|
+
}
|
|
13471
|
+
function ShopFilters({
|
|
13472
|
+
facets,
|
|
13473
|
+
hrefs,
|
|
13474
|
+
active = {},
|
|
13475
|
+
onChange,
|
|
13476
|
+
priceBands = DEFAULT_SHOP_PRICE_BANDS,
|
|
13477
|
+
labels,
|
|
13478
|
+
currencyCode,
|
|
13479
|
+
locale,
|
|
13480
|
+
className
|
|
13481
|
+
}) {
|
|
13482
|
+
const t = { ...DEFAULT_LABELS, ...labels };
|
|
13483
|
+
const visibleTaxonomies = facets.taxonomies.filter((axis) => axis.terms.length > 0);
|
|
13484
|
+
const visibleAttributes = facets.attributes.filter((attr) => attr.options.length > 0);
|
|
13485
|
+
const hasPriceSpread = facets.price.max > facets.price.min;
|
|
13486
|
+
const hasOnSale = facets.onSale.count > 0;
|
|
13487
|
+
if (!shouldRenderShopFilters(facets, active)) {
|
|
13488
|
+
return null;
|
|
13489
|
+
}
|
|
13490
|
+
return /* @__PURE__ */ jsxs6("div", { className: cn7("space-y-6", className), "data-slot": "shop-filters", children: [
|
|
13491
|
+
visibleTaxonomies.map((axis) => {
|
|
13492
|
+
const activeSlug = active.taxonomy?.[axis.key];
|
|
13493
|
+
return /* @__PURE__ */ jsxs6(FilterFieldset, { legend: axis.label, children: [
|
|
13494
|
+
/* @__PURE__ */ jsx7(
|
|
13495
|
+
OptionLink,
|
|
13496
|
+
{
|
|
13497
|
+
href: hrefs.taxonomy(axis.key, void 0),
|
|
13498
|
+
active: activeSlug === void 0,
|
|
13499
|
+
label: t.allTerms,
|
|
13500
|
+
onActivate: onChange ? () => {
|
|
13501
|
+
const next2 = { ...active.taxonomy };
|
|
13502
|
+
delete next2[axis.key];
|
|
13503
|
+
onChange({ taxonomy: next2 });
|
|
13504
|
+
} : void 0
|
|
13505
|
+
}
|
|
13506
|
+
),
|
|
13507
|
+
axis.terms.map((term) => {
|
|
13508
|
+
const isActive = activeSlug === term.slug;
|
|
13509
|
+
return /* @__PURE__ */ jsx7(
|
|
13510
|
+
OptionLink,
|
|
13511
|
+
{
|
|
13512
|
+
href: hrefs.taxonomy(axis.key, isActive ? void 0 : term.slug),
|
|
13513
|
+
active: isActive,
|
|
13514
|
+
label: `${term.name} (${term.count})`,
|
|
13515
|
+
onActivate: onChange ? () => {
|
|
13516
|
+
const next2 = { ...active.taxonomy };
|
|
13517
|
+
if (isActive) delete next2[axis.key];
|
|
13518
|
+
else next2[axis.key] = term.slug;
|
|
13519
|
+
onChange({ taxonomy: next2 });
|
|
13520
|
+
} : void 0
|
|
13521
|
+
},
|
|
13522
|
+
term.slug
|
|
13523
|
+
);
|
|
13524
|
+
})
|
|
13525
|
+
] }, axis.key);
|
|
13526
|
+
}),
|
|
13527
|
+
visibleAttributes.map((attr) => {
|
|
13528
|
+
const activeValues = toValueArray(active.attr?.[attr.key]);
|
|
13529
|
+
return /* @__PURE__ */ jsx7(FilterFieldset, { legend: attr.label, children: attr.options.map((opt) => {
|
|
13530
|
+
const isActive = activeValues.includes(opt.value);
|
|
13531
|
+
return /* @__PURE__ */ jsx7(
|
|
13532
|
+
OptionLink,
|
|
13533
|
+
{
|
|
13534
|
+
href: hrefs.attr(attr.key, opt.value),
|
|
13535
|
+
active: isActive,
|
|
13536
|
+
label: `${opt.label} (${opt.count})`,
|
|
13537
|
+
onActivate: onChange ? () => {
|
|
13538
|
+
const next2 = isActive ? activeValues.filter((v2) => v2 !== opt.value) : [...activeValues, opt.value];
|
|
13539
|
+
const attrNext = { ...active.attr };
|
|
13540
|
+
if (next2.length === 0) delete attrNext[attr.key];
|
|
13541
|
+
else attrNext[attr.key] = next2;
|
|
13542
|
+
onChange({ attr: attrNext });
|
|
13543
|
+
} : void 0
|
|
13544
|
+
},
|
|
13545
|
+
opt.value
|
|
13546
|
+
);
|
|
13547
|
+
}) }, attr.key);
|
|
13548
|
+
}),
|
|
13549
|
+
hasPriceSpread && /* @__PURE__ */ jsx7(FilterFieldset, { legend: t.price, children: priceBands.map((band, i) => {
|
|
13550
|
+
const isActive = active.priceMin === band.min && active.priceMax === band.max;
|
|
13551
|
+
const nextMin = isActive ? void 0 : band.min;
|
|
13552
|
+
const nextMax = isActive ? void 0 : band.max;
|
|
13553
|
+
return /* @__PURE__ */ jsx7(
|
|
13554
|
+
OptionLink,
|
|
13555
|
+
{
|
|
13556
|
+
href: hrefs.price(nextMin, nextMax),
|
|
13557
|
+
active: isActive,
|
|
13558
|
+
label: priceBandLabel(band, t, currencyCode, locale),
|
|
13559
|
+
onActivate: onChange ? () => onChange({ priceMin: nextMin, priceMax: nextMax }) : void 0
|
|
13560
|
+
},
|
|
13561
|
+
i
|
|
13562
|
+
);
|
|
13563
|
+
}) }),
|
|
13564
|
+
hasOnSale && /* @__PURE__ */ jsx7(
|
|
13565
|
+
OptionLink,
|
|
13566
|
+
{
|
|
13567
|
+
href: hrefs.onSale(!active.onSale),
|
|
13568
|
+
active: Boolean(active.onSale),
|
|
13569
|
+
label: `${t.onSale} (${facets.onSale.count})`,
|
|
13570
|
+
onActivate: onChange ? () => onChange({ onSale: !active.onSale }) : void 0
|
|
13571
|
+
}
|
|
13572
|
+
),
|
|
13573
|
+
/* @__PURE__ */ jsx7(
|
|
13574
|
+
"a",
|
|
13575
|
+
{
|
|
13576
|
+
href: hrefs.clearAll(),
|
|
13577
|
+
onClick: onChange ? (event) => {
|
|
13578
|
+
event.preventDefault();
|
|
13579
|
+
onChange({ taxonomy: {}, attr: {}, priceMin: void 0, priceMax: void 0, onSale: false });
|
|
13580
|
+
} : void 0,
|
|
13581
|
+
className: "text-sm font-medium text-muted-foreground underline-offset-4 hover:text-foreground hover:underline",
|
|
13582
|
+
children: t.clearAll
|
|
13583
|
+
}
|
|
13584
|
+
)
|
|
13585
|
+
] });
|
|
13586
|
+
}
|
|
13404
13587
|
export {
|
|
13405
13588
|
ALLOWED_YOUTUBE_HOSTS,
|
|
13589
|
+
DEFAULT_SHOP_PRICE_BANDS,
|
|
13406
13590
|
MarkdownRenderer,
|
|
13407
13591
|
ProductCard,
|
|
13408
13592
|
ProfileCard,
|
|
13409
13593
|
SafeHtml,
|
|
13594
|
+
ShopFilters,
|
|
13410
13595
|
Specialty,
|
|
13411
13596
|
Treatment,
|
|
13412
13597
|
buildShareUrls,
|
|
@@ -13414,6 +13599,7 @@ export {
|
|
|
13414
13599
|
isAllowedYouTubeHost,
|
|
13415
13600
|
markdownToSafeHtml,
|
|
13416
13601
|
readingTime,
|
|
13417
|
-
sanitizeHtml
|
|
13602
|
+
sanitizeHtml,
|
|
13603
|
+
shouldRenderShopFilters
|
|
13418
13604
|
};
|
|
13419
13605
|
//# sourceMappingURL=index.js.map
|