@cartbase/storefront 0.17.0 → 0.18.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/LICENSE +21 -21
  2. package/package.json +258 -256
  3. package/src/api/auth.ts +108 -108
  4. package/src/api/carts.ts +523 -523
  5. package/src/api/categories.ts +184 -184
  6. package/src/api/checkout.ts +526 -526
  7. package/src/api/collections.ts +130 -130
  8. package/src/api/consent.ts +75 -75
  9. package/src/api/content.ts +125 -125
  10. package/src/api/customers.ts +303 -303
  11. package/src/api/gift-cards.ts +112 -112
  12. package/src/api/http.ts +180 -180
  13. package/src/api/index.ts +30 -30
  14. package/src/api/menus.ts +77 -77
  15. package/src/api/metaobjects.ts +136 -136
  16. package/src/api/orders.ts +290 -290
  17. package/src/api/products.ts +43 -1
  18. package/src/api/redirects.ts +37 -37
  19. package/src/api/regions.ts +200 -200
  20. package/src/api/reviews.ts +259 -259
  21. package/src/api/search.ts +163 -163
  22. package/src/api/store.ts +35 -35
  23. package/src/api/types.ts +91 -91
  24. package/src/cart-drawer/context.tsx +277 -164
  25. package/src/cart-drawer/cross-sell-carousel.tsx +211 -211
  26. package/src/cart-drawer/cross-sell-sidebar.tsx +158 -158
  27. package/src/cart-drawer/gift-wrap.tsx +82 -82
  28. package/src/cart-drawer/index.ts +71 -68
  29. package/src/cart-drawer/item/index.tsx +162 -162
  30. package/src/cart-drawer/item/quantity.tsx +8 -14
  31. package/src/cart-drawer/item/upsell.tsx +110 -110
  32. package/src/cart-drawer/labels.ts +123 -123
  33. package/src/cart-drawer/mutation-queue.ts +78 -0
  34. package/src/cart-drawer/notes.tsx +131 -131
  35. package/src/cart-drawer/sticky-footer.tsx +73 -73
  36. package/src/cart-drawer/summary-breakdown.tsx +197 -197
  37. package/src/checkout/address-error-copy.ts +117 -117
  38. package/src/checkout/boxnow-locker-selector.tsx +410 -410
  39. package/src/checkout/compare-addresses.ts +40 -40
  40. package/src/checkout/discount-section.tsx +218 -218
  41. package/src/checkout/error-copy-codes.ts +63 -63
  42. package/src/checkout/geocode.ts +154 -154
  43. package/src/checkout/gift-card-section.tsx +224 -224
  44. package/src/checkout/index.ts +72 -72
  45. package/src/checkout/labels.ts +495 -495
  46. package/src/checkout/payment-button.tsx +372 -372
  47. package/src/checkout/payment-error-copy.ts +154 -154
  48. package/src/checkout/promotion-error-copy.ts +91 -91
  49. package/src/checkout/use-checkout-orchestration.ts +1592 -1592
  50. package/src/common/country-flag.tsx +52 -52
  51. package/src/common/country-select.tsx +11 -11
  52. package/src/common/index.ts +20 -20
  53. package/src/common/market-select.tsx +57 -57
  54. package/src/index.ts +12 -12
  55. package/src/lib/country-name.ts +59 -59
  56. package/src/lib/get-product-price.ts +133 -133
  57. package/src/lib/media-image.tsx +39 -39
  58. package/src/lib/payment-constants.ts +53 -53
  59. package/src/lib/platform.ts +13 -13
  60. package/src/lib/price.tsx +39 -39
  61. package/src/lib/store-api-error.ts +36 -36
  62. package/src/lib/variant-caption.ts +32 -32
  63. package/src/locales/bg.ts +467 -467
  64. package/src/locales/context.ts +37 -37
  65. package/src/locales/en.ts +26 -26
  66. package/src/locales/es.ts +466 -466
  67. package/src/locales/index.ts +19 -19
  68. package/src/locales/provider.tsx +59 -59
  69. package/src/locales/types.ts +77 -77
  70. package/src/order/index.ts +62 -62
  71. package/src/order/labels.ts +79 -79
  72. package/src/order/order-totals.tsx +250 -250
  73. package/src/primitives/select-field.tsx +93 -93
  74. package/src/products/image-gallery.tsx +43 -43
  75. package/src/products/index.ts +68 -59
  76. package/src/products/option-select.tsx +67 -67
  77. package/src/products/product-actions-wrapper.tsx +63 -62
  78. package/src/products/product-actions.tsx +151 -150
  79. package/src/products/product-price.tsx +69 -69
  80. package/src/products/product-promises.tsx +61 -61
  81. package/src/products/product-specs.tsx +65 -65
  82. package/src/products/product-tabs.tsx +123 -123
  83. package/src/products/product-template.tsx +163 -162
  84. package/src/products/purchase-options.tsx +130 -130
  85. package/src/products/sets.ts +110 -0
  86. package/src/products/use-product-actions.ts +199 -164
  87. package/src/products/variant-matching.ts +71 -71
  88. package/src/products/variant-url.ts +74 -74
  89. package/src/reviews-ui/index.ts +73 -73
  90. package/src/store/category-template.tsx +136 -136
  91. package/src/store/index.ts +40 -40
  92. package/src/tracking/chatgpt-pixel.tsx +99 -99
  93. package/src/tracking/consent-init.tsx +62 -62
  94. package/src/tracking/events.ts +348 -348
  95. package/src/tracking/ga4.tsx +93 -93
  96. package/src/tracking/google-ads.ts +84 -84
  97. package/src/tracking/gtm.tsx +60 -60
  98. package/src/tracking/inline-script.ts +49 -49
  99. package/src/tracking/oaiq.ts +206 -206
  100. package/src/tracking/tiktok-pixel.tsx +91 -91
  101. package/src/tracking/track-init.tsx +56 -56
  102. package/src/tracking/track-order-purchase.tsx +122 -122
  103. package/src/tracking/ttq.ts +180 -180
  104. package/src/tracking/use-tracking-config.ts +54 -54
  105. package/theme/index.css +25 -25
  106. package/theme/theme.css +150 -150
  107. package/theme/tokens.css +106 -106
@@ -1,74 +1,74 @@
1
- /**
2
- * The product page contract, part one: THE ADDRESS IS THE STATE.
3
- *
4
- * A product page names its chosen variant in the address, the way Shopify
5
- * does: `/products/<handle>?variant=<id>`. One parameter name for every
6
- * store, so a link in an email, an ad or a chat opens on the variant it was
7
- * copied from, and a refresh never forgets a choice.
8
- *
9
- * The value is the id's DIGITS, never the prefixed key: the platform's law
10
- * for every id in a URL (entity-numbers-everywhere card). `pvar_1000683784068`
11
- * travels as `1000683784068`; reading accepts both forms, so an old link
12
- * with the whole key still resolves.
13
- *
14
- * The default, when the address names nothing, is THE MERCHANT'S: the
15
- * variant made of each option's first value, in the option order the admin
16
- * shows. Not the first variant of the wire, which is whatever the database
17
- * scanned first (2026-09-15, evoo: the carton before the bottle on a product
18
- * whose option says bottle first). Pure module, no React.
19
- */
20
- import type { StoreProduct, StoreProductVariant } from "../api/products"
21
- import { findMatchingVariant } from "./variant-matching"
22
-
23
- /** Shopify's parameter name, and ours. */
24
- export const VARIANT_PARAM = "variant"
25
-
26
- /** The digits of a platform id; an id without a prefix travels whole. */
27
- export function variantParamValue(variantId: string): string {
28
- const underscore = variantId.indexOf("_")
29
- return underscore >= 0 ? variantId.slice(underscore + 1) : variantId
30
- }
31
-
32
- /** The variant an address names, by its digits or by its whole id. */
33
- export function findVariantByParam<V extends { id: string }>(
34
- variants: V[] | null | undefined,
35
- value: string | null | undefined
36
- ): V | undefined {
37
- if (!value) return undefined
38
- return variants?.find((v) => v.id === value || variantParamValue(v.id) === value)
39
- }
40
-
41
- /**
42
- * The merchant's default variant: each option's first value, together;
43
- * failing that, the lowest rank; failing that, the first of the wire.
44
- */
45
- export function defaultVariant(
46
- product: Pick<StoreProduct, "options" | "variants">
47
- ): StoreProductVariant | undefined {
48
- const variants = product.variants ?? []
49
- if (!variants.length) return undefined
50
- if (variants.length === 1) return variants[0]
51
-
52
- const firstValues: Record<string, string> = {}
53
- for (const option of product.options ?? []) {
54
- const first = option.values?.[0]?.value
55
- if (first) firstValues[option.id] = first
56
- }
57
- const byOptions = Object.keys(firstValues).length
58
- ? findMatchingVariant(variants, firstValues)
59
- : undefined
60
- if (byOptions) return byOptions
61
-
62
- return [...variants].sort((a, b) => (a.variant_rank ?? 0) - (b.variant_rank ?? 0))[0]
63
- }
64
-
65
- /** The page's address with the variant written in, every other parameter kept. */
66
- export function variantHref(
67
- pathname: string,
68
- search: string | URLSearchParams | null | undefined,
69
- variantId: string
70
- ): string {
71
- const params = new URLSearchParams(search ?? "")
72
- params.set(VARIANT_PARAM, variantParamValue(variantId))
73
- return `${pathname}?${params.toString()}`
74
- }
1
+ /**
2
+ * The product page contract, part one: THE ADDRESS IS THE STATE.
3
+ *
4
+ * A product page names its chosen variant in the address, the way Shopify
5
+ * does: `/products/<handle>?variant=<id>`. One parameter name for every
6
+ * store, so a link in an email, an ad or a chat opens on the variant it was
7
+ * copied from, and a refresh never forgets a choice.
8
+ *
9
+ * The value is the id's DIGITS, never the prefixed key: the platform's law
10
+ * for every id in a URL (entity-numbers-everywhere card). `pvar_1000683784068`
11
+ * travels as `1000683784068`; reading accepts both forms, so an old link
12
+ * with the whole key still resolves.
13
+ *
14
+ * The default, when the address names nothing, is THE MERCHANT'S: the
15
+ * variant made of each option's first value, in the option order the admin
16
+ * shows. Not the first variant of the wire, which is whatever the database
17
+ * scanned first (2026-09-15, evoo: the carton before the bottle on a product
18
+ * whose option says bottle first). Pure module, no React.
19
+ */
20
+ import type { StoreProduct, StoreProductVariant } from "../api/products"
21
+ import { findMatchingVariant } from "./variant-matching"
22
+
23
+ /** Shopify's parameter name, and ours. */
24
+ export const VARIANT_PARAM = "variant"
25
+
26
+ /** The digits of a platform id; an id without a prefix travels whole. */
27
+ export function variantParamValue(variantId: string): string {
28
+ const underscore = variantId.indexOf("_")
29
+ return underscore >= 0 ? variantId.slice(underscore + 1) : variantId
30
+ }
31
+
32
+ /** The variant an address names, by its digits or by its whole id. */
33
+ export function findVariantByParam<V extends { id: string }>(
34
+ variants: V[] | null | undefined,
35
+ value: string | null | undefined
36
+ ): V | undefined {
37
+ if (!value) return undefined
38
+ return variants?.find((v) => v.id === value || variantParamValue(v.id) === value)
39
+ }
40
+
41
+ /**
42
+ * The merchant's default variant: each option's first value, together;
43
+ * failing that, the lowest rank; failing that, the first of the wire.
44
+ */
45
+ export function defaultVariant(
46
+ product: Pick<StoreProduct, "options" | "variants">
47
+ ): StoreProductVariant | undefined {
48
+ const variants = product.variants ?? []
49
+ if (!variants.length) return undefined
50
+ if (variants.length === 1) return variants[0]
51
+
52
+ const firstValues: Record<string, string> = {}
53
+ for (const option of product.options ?? []) {
54
+ const first = option.values?.[0]?.value
55
+ if (first) firstValues[option.id] = first
56
+ }
57
+ const byOptions = Object.keys(firstValues).length
58
+ ? findMatchingVariant(variants, firstValues)
59
+ : undefined
60
+ if (byOptions) return byOptions
61
+
62
+ return [...variants].sort((a, b) => (a.variant_rank ?? 0) - (b.variant_rank ?? 0))[0]
63
+ }
64
+
65
+ /** The page's address with the variant written in, every other parameter kept. */
66
+ export function variantHref(
67
+ pathname: string,
68
+ search: string | URLSearchParams | null | undefined,
69
+ variantId: string
70
+ ): string {
71
+ const params = new URLSearchParams(search ?? "")
72
+ params.set(VARIANT_PARAM, variantParamValue(variantId))
73
+ return `${pathname}?${params.toString()}`
74
+ }
@@ -1,73 +1,73 @@
1
- /**
2
- * @cartbase/storefront/reviews-ui — verified-purchase review components.
3
- * NEW family (no @1click/ui counterpart): ported/adapted from the Alenika
4
- * production storefront (src/components/product/reviews-list.tsx + the
5
- * /review/[token] page + review-form.tsx) over the batch-9 SDK
6
- * (`api/reviews` — DTOs mirror alenika/src/lib/reviews-api.ts verbatim).
7
- * Contracts: docs/storefront/components.md + docs/storefront/reviews.md.
8
- */
9
- export {
10
- defaultReviewsUiLabels,
11
- type ReviewsUiLabels,
12
- } from "./labels"
13
-
14
- export {
15
- formatLabel,
16
- reviewDisplayName,
17
- firstNameOf,
18
- formatReviewDate,
19
- sortParamsFor,
20
- reviewCountLabel,
21
- checkFileForUpload,
22
- MAX_IMAGES,
23
- MAX_VIDEOS,
24
- MAX_BODY,
25
- MAX_IMAGE_BYTES,
26
- MAX_VIDEO_BYTES,
27
- MAX_VIDEO_SECONDS,
28
- IMAGE_MIME,
29
- VIDEO_MIME,
30
- UPLOAD_ACCEPT,
31
- type ReviewSortKey,
32
- type FileCheckInput,
33
- type FileCheckResult,
34
- } from "./helpers"
35
-
36
- export {
37
- resolveWizardEntry,
38
- submitErrorKeyFor,
39
- canSubmitRating,
40
- type WizardEntry,
41
- type WizardStep,
42
- type SubmitErrorKey,
43
- } from "./wizard-state"
44
-
45
- export {
46
- StarRow,
47
- StarBadge,
48
- RatingDistribution,
49
- type StarBadgeProps,
50
- type RatingDistributionProps,
51
- } from "./star-badge"
52
-
53
- export {
54
- ReviewList,
55
- ReviewLightbox,
56
- type ReviewListProps,
57
- } from "./review-list"
58
-
59
- export { ReviewWidget, type ReviewWidgetProps } from "./review-widget"
60
-
61
- export {
62
- ReviewWizard,
63
- ReviewWizardForm,
64
- type ReviewWizardProps,
65
- } from "./review-wizard"
66
-
67
- export {
68
- ReviewPhotoUpload,
69
- readyMedia,
70
- putToSignedUrl,
71
- type LocalMedia,
72
- type ReviewPhotoUploadProps,
73
- } from "./photo-upload"
1
+ /**
2
+ * @cartbase/storefront/reviews-ui — verified-purchase review components.
3
+ * NEW family (no @1click/ui counterpart): ported/adapted from the Alenika
4
+ * production storefront (src/components/product/reviews-list.tsx + the
5
+ * /review/[token] page + review-form.tsx) over the batch-9 SDK
6
+ * (`api/reviews` — DTOs mirror alenika/src/lib/reviews-api.ts verbatim).
7
+ * Contracts: docs/storefront/components.md + docs/storefront/reviews.md.
8
+ */
9
+ export {
10
+ defaultReviewsUiLabels,
11
+ type ReviewsUiLabels,
12
+ } from "./labels"
13
+
14
+ export {
15
+ formatLabel,
16
+ reviewDisplayName,
17
+ firstNameOf,
18
+ formatReviewDate,
19
+ sortParamsFor,
20
+ reviewCountLabel,
21
+ checkFileForUpload,
22
+ MAX_IMAGES,
23
+ MAX_VIDEOS,
24
+ MAX_BODY,
25
+ MAX_IMAGE_BYTES,
26
+ MAX_VIDEO_BYTES,
27
+ MAX_VIDEO_SECONDS,
28
+ IMAGE_MIME,
29
+ VIDEO_MIME,
30
+ UPLOAD_ACCEPT,
31
+ type ReviewSortKey,
32
+ type FileCheckInput,
33
+ type FileCheckResult,
34
+ } from "./helpers"
35
+
36
+ export {
37
+ resolveWizardEntry,
38
+ submitErrorKeyFor,
39
+ canSubmitRating,
40
+ type WizardEntry,
41
+ type WizardStep,
42
+ type SubmitErrorKey,
43
+ } from "./wizard-state"
44
+
45
+ export {
46
+ StarRow,
47
+ StarBadge,
48
+ RatingDistribution,
49
+ type StarBadgeProps,
50
+ type RatingDistributionProps,
51
+ } from "./star-badge"
52
+
53
+ export {
54
+ ReviewList,
55
+ ReviewLightbox,
56
+ type ReviewListProps,
57
+ } from "./review-list"
58
+
59
+ export { ReviewWidget, type ReviewWidgetProps } from "./review-widget"
60
+
61
+ export {
62
+ ReviewWizard,
63
+ ReviewWizardForm,
64
+ type ReviewWizardProps,
65
+ } from "./review-wizard"
66
+
67
+ export {
68
+ ReviewPhotoUpload,
69
+ readyMedia,
70
+ putToSignedUrl,
71
+ type LocalMedia,
72
+ type ReviewPhotoUploadProps,
73
+ } from "./photo-upload"
@@ -1,136 +1,136 @@
1
- /**
2
- * Category page — ported from
3
- * `@1click/ui/src/store/category-template.tsx` (v2.3.1). Data seam:
4
- * `HttpTypes.StoreProductCategory` → Cartbase `StoreProductCategory`
5
- * (`@cartbase/storefront/api/categories`); products come from the plain
6
- * listing filtered by `category_id` (membership), via `PaginatedProducts`.
7
- *
8
- * Breadcrumbs need `parent_category` chains and the child list needs
9
- * `category_children` — fetch the category with `include_ancestors_tree` +
10
- * `include_descendants_tree` (`retrieveCategory` query flags), otherwise
11
- * those sections simply don't render. Skeleton count is the fixed default
12
- * (Cartbase categories don't embed a products array).
13
- */
14
- import { Suspense } from "react"
15
- import { notFound } from "next/navigation"
16
- import Link from "next/link"
17
- import type { StorefrontClient } from "../api/http"
18
- import type { PricingContextQuery } from "../api/types"
19
- import type { StoreProductCategory } from "../api/categories"
20
- import type { SortOptions } from "../lib/sort-products"
21
- import type { StoreLabels } from "./labels"
22
- import { SortSelect } from "./sort-select"
23
- import { PaginatedProducts, type ProductCardComponent } from "./paginated-products"
24
- import { SkeletonProductGrid } from "./skeleton-product-grid"
25
-
26
- type CategoryTemplateProps = {
27
- client: StorefrontClient
28
- category: StoreProductCategory
29
- sortBy?: SortOptions
30
- /** From the `page` query param. */
31
- page?: string
32
- pricingContext?: PricingContextQuery
33
- /**
34
- * The store's pack for this area, REQUIRED: this is a server component and
35
- * cannot read `StorefrontLocaleProvider`, so the page passes
36
- * `STORE_LOCALE.store` (`en.store` for an English store). Required since
37
- * 2026-09-14 so that forgetting it fails the build instead of shipping
38
- * silent English.
39
- */
40
- labels: StoreLabels
41
- /** Override the default ProductPreview with a store-specific card. */
42
- renderProduct?: ProductCardComponent
43
- }
44
-
45
- export function CategoryTemplate({
46
- client,
47
- category,
48
- sortBy,
49
- page,
50
- pricingContext,
51
- labels,
52
- renderProduct,
53
- }: CategoryTemplateProps) {
54
- const pageNumber = page ? parseInt(page) : 1
55
- const sort = sortBy || "created_at"
56
-
57
- if (!category) notFound()
58
-
59
- const parents: StoreProductCategory[] = []
60
- const getParents = (cat: StoreProductCategory) => {
61
- if (cat.parent_category) {
62
- parents.push(cat.parent_category)
63
- getParents(cat.parent_category)
64
- }
65
- }
66
- getParents(category)
67
-
68
- return (
69
- <div
70
- className="flex flex-col sm:flex-row sm:items-start py-6 max-w-7xl mx-auto px-4"
71
- data-testid="category-container"
72
- >
73
- <div className="flex sm:flex-col gap-12 py-4 mb-8 sm:px-0 pl-6 sm:min-w-[250px]">
74
- <SortSelect
75
- sortBy={sort}
76
- labels={labels}
77
- data-testid="sort-by-container"
78
- />
79
- </div>
80
- <div className="w-full">
81
- <div className="flex flex-row mb-8 gap-4">
82
- {parents.map((parent) => (
83
- <span key={parent.id} className="text-muted-foreground text-2xl">
84
- <Link
85
- className="mr-4 hover:text-foreground"
86
- href={`/categories/${parent.handle}`}
87
- >
88
- {parent.name}
89
- </Link>
90
- /
91
- </span>
92
- ))}
93
- <h1
94
- className="text-2xl font-semibold text-foreground"
95
- data-testid="category-page-title"
96
- >
97
- {category.name}
98
- </h1>
99
- </div>
100
- {category.description && (
101
- <div className="mb-8 text-sm text-muted-foreground">
102
- <p>{category.description}</p>
103
- </div>
104
- )}
105
- {category.category_children && (
106
- <div className="mb-8">
107
- <ul className="grid grid-cols-1 gap-2">
108
- {category.category_children.map((c) => (
109
- <li key={c.id}>
110
- <Link
111
- href={`/categories/${c.handle}`}
112
- className="text-primary hover:underline"
113
- >
114
- {c.name}
115
- </Link>
116
- </li>
117
- ))}
118
- </ul>
119
- </div>
120
- )}
121
- <Suspense fallback={<SkeletonProductGrid />}>
122
- <PaginatedProducts
123
- client={client}
124
- sortBy={sort}
125
- page={pageNumber}
126
- categoryId={category.id}
127
- pricingContext={pricingContext}
128
- renderProduct={renderProduct}
129
- />
130
- </Suspense>
131
- </div>
132
- </div>
133
- )
134
- }
135
-
136
- export { type CategoryTemplateProps }
1
+ /**
2
+ * Category page — ported from
3
+ * `@1click/ui/src/store/category-template.tsx` (v2.3.1). Data seam:
4
+ * `HttpTypes.StoreProductCategory` → Cartbase `StoreProductCategory`
5
+ * (`@cartbase/storefront/api/categories`); products come from the plain
6
+ * listing filtered by `category_id` (membership), via `PaginatedProducts`.
7
+ *
8
+ * Breadcrumbs need `parent_category` chains and the child list needs
9
+ * `category_children` — fetch the category with `include_ancestors_tree` +
10
+ * `include_descendants_tree` (`retrieveCategory` query flags), otherwise
11
+ * those sections simply don't render. Skeleton count is the fixed default
12
+ * (Cartbase categories don't embed a products array).
13
+ */
14
+ import { Suspense } from "react"
15
+ import { notFound } from "next/navigation"
16
+ import Link from "next/link"
17
+ import type { StorefrontClient } from "../api/http"
18
+ import type { PricingContextQuery } from "../api/types"
19
+ import type { StoreProductCategory } from "../api/categories"
20
+ import type { SortOptions } from "../lib/sort-products"
21
+ import type { StoreLabels } from "./labels"
22
+ import { SortSelect } from "./sort-select"
23
+ import { PaginatedProducts, type ProductCardComponent } from "./paginated-products"
24
+ import { SkeletonProductGrid } from "./skeleton-product-grid"
25
+
26
+ type CategoryTemplateProps = {
27
+ client: StorefrontClient
28
+ category: StoreProductCategory
29
+ sortBy?: SortOptions
30
+ /** From the `page` query param. */
31
+ page?: string
32
+ pricingContext?: PricingContextQuery
33
+ /**
34
+ * The store's pack for this area, REQUIRED: this is a server component and
35
+ * cannot read `StorefrontLocaleProvider`, so the page passes
36
+ * `STORE_LOCALE.store` (`en.store` for an English store). Required since
37
+ * 2026-09-14 so that forgetting it fails the build instead of shipping
38
+ * silent English.
39
+ */
40
+ labels: StoreLabels
41
+ /** Override the default ProductPreview with a store-specific card. */
42
+ renderProduct?: ProductCardComponent
43
+ }
44
+
45
+ export function CategoryTemplate({
46
+ client,
47
+ category,
48
+ sortBy,
49
+ page,
50
+ pricingContext,
51
+ labels,
52
+ renderProduct,
53
+ }: CategoryTemplateProps) {
54
+ const pageNumber = page ? parseInt(page) : 1
55
+ const sort = sortBy || "created_at"
56
+
57
+ if (!category) notFound()
58
+
59
+ const parents: StoreProductCategory[] = []
60
+ const getParents = (cat: StoreProductCategory) => {
61
+ if (cat.parent_category) {
62
+ parents.push(cat.parent_category)
63
+ getParents(cat.parent_category)
64
+ }
65
+ }
66
+ getParents(category)
67
+
68
+ return (
69
+ <div
70
+ className="flex flex-col sm:flex-row sm:items-start py-6 max-w-7xl mx-auto px-4"
71
+ data-testid="category-container"
72
+ >
73
+ <div className="flex sm:flex-col gap-12 py-4 mb-8 sm:px-0 pl-6 sm:min-w-[250px]">
74
+ <SortSelect
75
+ sortBy={sort}
76
+ labels={labels}
77
+ data-testid="sort-by-container"
78
+ />
79
+ </div>
80
+ <div className="w-full">
81
+ <div className="flex flex-row mb-8 gap-4">
82
+ {parents.map((parent) => (
83
+ <span key={parent.id} className="text-muted-foreground text-2xl">
84
+ <Link
85
+ className="mr-4 hover:text-foreground"
86
+ href={`/categories/${parent.handle}`}
87
+ >
88
+ {parent.name}
89
+ </Link>
90
+ /
91
+ </span>
92
+ ))}
93
+ <h1
94
+ className="text-2xl font-semibold text-foreground"
95
+ data-testid="category-page-title"
96
+ >
97
+ {category.name}
98
+ </h1>
99
+ </div>
100
+ {category.description && (
101
+ <div className="mb-8 text-sm text-muted-foreground">
102
+ <p>{category.description}</p>
103
+ </div>
104
+ )}
105
+ {category.category_children && (
106
+ <div className="mb-8">
107
+ <ul className="grid grid-cols-1 gap-2">
108
+ {category.category_children.map((c) => (
109
+ <li key={c.id}>
110
+ <Link
111
+ href={`/categories/${c.handle}`}
112
+ className="text-primary hover:underline"
113
+ >
114
+ {c.name}
115
+ </Link>
116
+ </li>
117
+ ))}
118
+ </ul>
119
+ </div>
120
+ )}
121
+ <Suspense fallback={<SkeletonProductGrid />}>
122
+ <PaginatedProducts
123
+ client={client}
124
+ sortBy={sort}
125
+ page={pageNumber}
126
+ categoryId={category.id}
127
+ pricingContext={pricingContext}
128
+ renderProduct={renderProduct}
129
+ />
130
+ </Suspense>
131
+ </div>
132
+ </div>
133
+ )
134
+ }
135
+
136
+ export { type CategoryTemplateProps }
@@ -1,40 +1,40 @@
1
- /**
2
- * @cartbase/storefront — store (listing) family barrel. Prefer subpath
3
- * imports in apps; the barrel exists for convenience and for the extra pure
4
- * module (search-params).
5
- */
6
- export {
7
- defaultStoreLabels,
8
- sortOptionLabelKeys,
9
- type StoreLabels,
10
- } from "./labels"
11
-
12
- export { Pagination } from "./pagination"
13
- export { SortSelect, type SortSelectProps } from "./sort-select"
14
- export { PaginatedProducts, type ProductCardComponent } from "./paginated-products"
15
- export { SkeletonProductGrid } from "./skeleton-product-grid"
16
- export { StoreTemplate, type StoreTemplateProps } from "./store-template"
17
- export {
18
- CollectionTemplate,
19
- type CollectionTemplateProps,
20
- } from "./collection-template"
21
- export {
22
- CategoryTemplate,
23
- type CategoryTemplateProps,
24
- } from "./category-template"
25
- export { SearchTemplate, type SearchTemplateProps } from "./search-template"
26
-
27
- export {
28
- parseSearchParams,
29
- buildSearchQueryString,
30
- fromQueryString,
31
- toggleFacetSelection,
32
- isFacetSelected,
33
- clearFilters,
34
- toSearchQuery,
35
- type SearchPageState,
36
- type RawSearchParams,
37
- type FacetBucketRef,
38
- } from "./search-params"
39
-
40
- export { type SortOptions } from "../lib/sort-products"
1
+ /**
2
+ * @cartbase/storefront — store (listing) family barrel. Prefer subpath
3
+ * imports in apps; the barrel exists for convenience and for the extra pure
4
+ * module (search-params).
5
+ */
6
+ export {
7
+ defaultStoreLabels,
8
+ sortOptionLabelKeys,
9
+ type StoreLabels,
10
+ } from "./labels"
11
+
12
+ export { Pagination } from "./pagination"
13
+ export { SortSelect, type SortSelectProps } from "./sort-select"
14
+ export { PaginatedProducts, type ProductCardComponent } from "./paginated-products"
15
+ export { SkeletonProductGrid } from "./skeleton-product-grid"
16
+ export { StoreTemplate, type StoreTemplateProps } from "./store-template"
17
+ export {
18
+ CollectionTemplate,
19
+ type CollectionTemplateProps,
20
+ } from "./collection-template"
21
+ export {
22
+ CategoryTemplate,
23
+ type CategoryTemplateProps,
24
+ } from "./category-template"
25
+ export { SearchTemplate, type SearchTemplateProps } from "./search-template"
26
+
27
+ export {
28
+ parseSearchParams,
29
+ buildSearchQueryString,
30
+ fromQueryString,
31
+ toggleFacetSelection,
32
+ isFacetSelected,
33
+ clearFilters,
34
+ toSearchQuery,
35
+ type SearchPageState,
36
+ type RawSearchParams,
37
+ type FacetBucketRef,
38
+ } from "./search-params"
39
+
40
+ export { type SortOptions } from "../lib/sort-products"