@cartbase/storefront 0.20.0 → 0.21.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (109) hide show
  1. package/LICENSE +21 -21
  2. package/package.json +4 -1
  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 +181 -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/redirects.ts +37 -37
  18. package/src/api/regions.ts +200 -200
  19. package/src/api/search.ts +163 -163
  20. package/src/api/store.ts +56 -35
  21. package/src/api/types.ts +91 -91
  22. package/src/cart-drawer/context.tsx +778 -778
  23. package/src/cart-drawer/cross-sell-carousel.tsx +211 -211
  24. package/src/cart-drawer/cross-sell-sidebar.tsx +158 -158
  25. package/src/cart-drawer/gift-wrap.tsx +82 -82
  26. package/src/cart-drawer/item/index.tsx +162 -162
  27. package/src/cart-drawer/item/upsell.tsx +110 -110
  28. package/src/cart-drawer/labels.ts +123 -123
  29. package/src/cart-drawer/mutation-queue.ts +78 -78
  30. package/src/cart-drawer/notes.tsx +131 -131
  31. package/src/cart-drawer/payment-badges.tsx +36 -96
  32. package/src/cart-drawer/sticky-footer.tsx +73 -73
  33. package/src/cart-drawer/summary-breakdown.tsx +197 -197
  34. package/src/checkout/address-error-copy.ts +117 -117
  35. package/src/checkout/boxnow-locker-selector.tsx +410 -410
  36. package/src/checkout/compare-addresses.ts +40 -40
  37. package/src/checkout/discount-section.tsx +218 -218
  38. package/src/checkout/error-copy-codes.ts +63 -63
  39. package/src/checkout/geocode.ts +154 -154
  40. package/src/checkout/gift-card-section.tsx +224 -224
  41. package/src/checkout/index.ts +72 -72
  42. package/src/checkout/labels.ts +495 -495
  43. package/src/checkout/payment-button.tsx +372 -372
  44. package/src/checkout/payment-error-copy.ts +154 -154
  45. package/src/checkout/promotion-error-copy.ts +91 -91
  46. package/src/common/country-flag.tsx +52 -52
  47. package/src/common/country-select.tsx +11 -11
  48. package/src/common/icons/cartbase-mark.ts +9 -0
  49. package/src/common/icons/payment-marks.ts +73 -0
  50. package/src/common/icons/social-marks.ts +61 -0
  51. package/src/common/index.ts +35 -20
  52. package/src/common/market-select.tsx +57 -57
  53. package/src/common/payment-icons.tsx +54 -0
  54. package/src/common/powered-by-cartbase.tsx +47 -0
  55. package/src/common/social-links.tsx +65 -0
  56. package/src/index.ts +12 -12
  57. package/src/lib/country-name.ts +59 -59
  58. package/src/lib/get-product-price.ts +133 -133
  59. package/src/lib/media-image.tsx +39 -39
  60. package/src/lib/payment-constants.ts +53 -53
  61. package/src/lib/platform.ts +13 -13
  62. package/src/lib/price.tsx +39 -39
  63. package/src/lib/store-api-error.ts +36 -36
  64. package/src/lib/variant-caption.ts +32 -32
  65. package/src/locales/bg.ts +3 -0
  66. package/src/locales/context.ts +37 -37
  67. package/src/locales/en.ts +26 -26
  68. package/src/locales/es.ts +3 -0
  69. package/src/locales/index.ts +19 -19
  70. package/src/locales/provider.tsx +59 -59
  71. package/src/locales/types.ts +77 -77
  72. package/src/order/index.ts +62 -62
  73. package/src/order/labels.ts +79 -79
  74. package/src/order/order-totals.tsx +250 -250
  75. package/src/primitives/select-field.tsx +93 -93
  76. package/src/products/image-gallery.tsx +43 -43
  77. package/src/products/option-select.tsx +67 -67
  78. package/src/products/product-info.tsx +5 -5
  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/purchase-options.tsx +130 -130
  84. package/src/products/sets.ts +110 -110
  85. package/src/products/variant-matching.ts +71 -71
  86. package/src/products/variant-url.ts +74 -74
  87. package/src/reviews-ui/lightbox-state.ts +46 -46
  88. package/src/reviews-ui/review-header.tsx +166 -166
  89. package/src/reviews-ui/review-lightbox.tsx +271 -271
  90. package/src/reviews-ui/review-list.tsx +193 -193
  91. package/src/reviews-ui/review-widget.tsx +219 -218
  92. package/src/reviews-ui/widget-options.ts +55 -55
  93. package/src/store/category-template.tsx +136 -136
  94. package/src/store/index.ts +40 -40
  95. package/src/store/labels.ts +10 -0
  96. package/src/tracking/chatgpt-pixel.tsx +99 -99
  97. package/src/tracking/consent-init.tsx +62 -62
  98. package/src/tracking/events.ts +348 -348
  99. package/src/tracking/ga4.tsx +93 -93
  100. package/src/tracking/google-ads.ts +84 -84
  101. package/src/tracking/gtm.tsx +60 -60
  102. package/src/tracking/inline-script.ts +49 -49
  103. package/src/tracking/oaiq.ts +206 -206
  104. package/src/tracking/tiktok-pixel.tsx +91 -91
  105. package/src/tracking/track-init.tsx +56 -56
  106. package/src/tracking/track-order-purchase.tsx +122 -122
  107. package/src/tracking/ttq.ts +180 -180
  108. package/src/tracking/use-tracking-config.ts +54 -54
  109. package/theme/index.css +25 -25
@@ -1,218 +1,219 @@
1
- "use client"
2
-
3
- import { useCallback, useEffect, useRef, useState } from "react"
4
- import type { StorefrontClient } from "../api/http"
5
- import {
6
- getWidget,
7
- listReviews,
8
- type PublicReview,
9
- type ReviewWidgetPayload,
10
- } from "../api/reviews"
11
- import { cn } from "../lib/utils"
12
- import { useLocaleArea } from "../locales/context"
13
- import { defaultReviewsUiLabels, type ReviewsUiLabels } from "./labels"
14
- import { sortParamsFor, type ReviewSortKey } from "./helpers"
15
- import { ReviewSummaryHeader } from "./review-header"
16
- import { ReviewList } from "./review-list"
17
- import { reviewWidgetOptions } from "./widget-options"
18
-
19
- /**
20
- * The product reviews widget, the reviews app's grid for the bottom of a
21
- * product page (Loox's Reviews Widget): a header, the review cards, photo
22
- * reviews first, and "Show more reviews" under them; a card opens its
23
- * review in the lightbox. Ported from the Alenika PDP section
24
- * (src/components/product/reviews-list.tsx) with the Cartbase data seam:
25
- *
26
- * - Bootstrap is ONE call — `api/reviews.getWidget(client, productId)`
27
- * (aggregate + first page + the store's widget settings; edge-cached).
28
- * Server-fetch it and pass `initialData` (recommended — no client
29
- * waterfall); when omitted the widget fetches it on mount.
30
- * - The store's settings (admin, Reviews, Widgets) arrive in
31
- * `options` and shape it: the header (`minimal`, `compact`,
32
- * `expanded`, and what the minimal one shows), the rating breakdown,
33
- * the sort menu, the date on the cards, masonry or list, and the page
34
- * size. A field an older platform does not send takes Loox's default
35
- * (`reviewWidgetOptions`).
36
- * - Sort changes and "Show more reviews" re-fetch client-side via
37
- * `listReviews` (the API orders; the default sort is photo reviews
38
- * first, then newest).
39
- * - Aggregate fallback preserved (production fix): if the aggregate is
40
- * missing but reviews loaded, derive avg + distribution from the
41
- * loaded page rather than showing a misleading "0.0" next to a filled
42
- * star. The whole section renders null only when there are genuinely
43
- * zero reviews.
44
- */
45
- export interface ReviewWidgetProps {
46
- client: StorefrontClient
47
- productId: string
48
- /** Server-fetched `getWidget` payload (recommended). */
49
- initialData?: ReviewWidgetPayload | null
50
- labels?: Partial<ReviewsUiLabels>
51
- className?: string
52
- }
53
-
54
- export function ReviewWidget({
55
- client,
56
- productId,
57
- initialData,
58
- labels,
59
- className,
60
- }: ReviewWidgetProps) {
61
- // The mounted language, then the store's overrides (2026-09-14): a store
62
- // that mounts StorefrontLocaleProvider passes nothing here.
63
- const l = { ...useLocaleArea("reviews", defaultReviewsUiLabels), ...labels }
64
- const [data, setData] = useState<ReviewWidgetPayload | null>(initialData ?? null)
65
- // The first page is photo reviews first unless the store turned that off.
66
- const [sort, setSort] = useState<ReviewSortKey>(
67
- initialData?.options.photo_first === false ? "date" : "default"
68
- )
69
- const [reviews, setReviews] = useState<PublicReview[]>(initialData?.reviews ?? [])
70
- const [count, setCount] = useState<number>(initialData?.count ?? 0)
71
- const [hasMore, setHasMore] = useState<boolean>(initialData?.has_more ?? false)
72
- const [loading, setLoading] = useState(false)
73
- const sortChanged = useRef(false)
74
-
75
- const options = reviewWidgetOptions(data?.options)
76
- const pageSize = options.pageSize
77
-
78
- // No server-provided bootstrap → fetch it on mount.
79
- useEffect(() => {
80
- if (initialData) return
81
- let cancelled = false
82
- ;(async () => {
83
- try {
84
- const payload = await getWidget(client, productId)
85
- if (cancelled) return
86
- setData(payload)
87
- setReviews(payload.reviews)
88
- setCount(payload.count)
89
- setHasMore(payload.has_more)
90
- if (payload.options.photo_first === false) setSort("date")
91
- } catch {
92
- /* widget is decorative — a failed bootstrap renders nothing */
93
- }
94
- })()
95
- return () => {
96
- cancelled = true
97
- }
98
- // eslint-disable-next-line react-hooks/exhaustive-deps
99
- }, [client, productId])
100
-
101
- // Re-fetch the first page when the shopper picks a sort (the bootstrap
102
- // page already is the first sort, so the first run is skipped).
103
- useEffect(() => {
104
- if (!sortChanged.current) return
105
- let cancelled = false
106
- setLoading(true)
107
- ;(async () => {
108
- try {
109
- const res = await listReviews(client, {
110
- product_id: productId,
111
- ...sortParamsFor(sort),
112
- limit: pageSize,
113
- offset: 0,
114
- })
115
- if (cancelled) return
116
- setReviews(res.reviews)
117
- setCount(res.count)
118
- setHasMore(res.has_more)
119
- } finally {
120
- if (!cancelled) setLoading(false)
121
- }
122
- })()
123
- return () => {
124
- cancelled = true
125
- }
126
- // eslint-disable-next-line react-hooks/exhaustive-deps
127
- }, [sort, productId, pageSize])
128
-
129
- const chooseSort = (next: ReviewSortKey) => {
130
- sortChanged.current = true
131
- setSort(next)
132
- }
133
-
134
- const loadMore = useCallback(async () => {
135
- setLoading(true)
136
- try {
137
- const res = await listReviews(client, {
138
- product_id: productId,
139
- ...sortParamsFor(sort),
140
- limit: pageSize,
141
- offset: reviews.length,
142
- })
143
- setReviews((prev) => [...prev, ...res.reviews])
144
- setCount(res.count)
145
- setHasMore(res.has_more)
146
- } finally {
147
- setLoading(false)
148
- }
149
- }, [client, productId, sort, pageSize, reviews.length])
150
-
151
- // Header summary uses the full aggregate when available; else derive
152
- // from the loaded reviews (see the header comment).
153
- const aggregate = data?.aggregate ?? null
154
- const total = aggregate?.count ?? count
155
- const avg =
156
- aggregate?.avg_rating ??
157
- (reviews.length ? reviews.reduce((s, r) => s + r.rating, 0) / reviews.length : 0)
158
- const distribution =
159
- aggregate?.distribution ??
160
- reviews.reduce(
161
- (d, r) => {
162
- const key = String(r.rating) as "1" | "2" | "3" | "4" | "5"
163
- if (key in d) d[key]++
164
- return d
165
- },
166
- { "1": 0, "2": 0, "3": 0, "4": 0, "5": 0 } as Record<"1" | "2" | "3" | "4" | "5", number>
167
- )
168
-
169
- if (total === 0) {
170
- return null // Hide the section entirely when there are no reviews
171
- }
172
-
173
- return (
174
- <section
175
- id="reviews"
176
- className={className ?? "max-w-[1440px] mx-auto px-4 sm:px-6 lg:px-10 my-16 sm:my-24"}
177
- >
178
- <ReviewSummaryHeader
179
- layout={options.header}
180
- content={options.headerContent}
181
- total={total}
182
- average={avg}
183
- distribution={distribution}
184
- showDistribution={options.showDistribution}
185
- showSort={options.showSort}
186
- sort={sort}
187
- onSortChange={chooseSort}
188
- labels={l}
189
- />
190
-
191
- <ReviewList
192
- reviews={reviews}
193
- layout={options.layout}
194
- showDate={options.showDate}
195
- labels={l}
196
- className={cn(
197
- options.layout === "list"
198
- ? "columns-1 [column-gap:1rem]"
199
- : "columns-1 sm:columns-2 lg:columns-3 xl:columns-4 [column-gap:1rem]",
200
- loading && "opacity-60 transition-opacity"
201
- )}
202
- />
203
-
204
- {hasMore && (
205
- <div className="mt-8 text-center">
206
- <button
207
- type="button"
208
- onClick={loadMore}
209
- disabled={loading}
210
- className="inline-flex items-center justify-center rounded-md border border-border bg-background px-4 py-2 text-body-small text-foreground transition-colors hover:bg-muted disabled:cursor-not-allowed disabled:opacity-50"
211
- >
212
- {loading ? l.loading : l.loadMore}
213
- </button>
214
- </div>
215
- )}
216
- </section>
217
- )
218
- }
1
+ "use client"
2
+
3
+ import { useCallback, useEffect, useRef, useState } from "react"
4
+ import type { StorefrontClient } from "../api/http"
5
+ import {
6
+ getWidget,
7
+ listReviews,
8
+ type PublicReview,
9
+ type ReviewWidgetPayload,
10
+ } from "../api/reviews"
11
+ import { cn } from "../lib/utils"
12
+ import { useLocaleArea } from "../locales/context"
13
+ import { defaultReviewsUiLabels, type ReviewsUiLabels } from "./labels"
14
+ import { sortParamsFor, type ReviewSortKey } from "./helpers"
15
+ import { ReviewSummaryHeader } from "./review-header"
16
+ import { ReviewList } from "./review-list"
17
+ import { reviewWidgetOptions } from "./widget-options"
18
+
19
+ /**
20
+ * The product reviews widget, the reviews app's grid for the bottom of a
21
+ * product page (Loox's Reviews Widget): a header, the review cards, photo
22
+ * reviews first, and "Show more reviews" under them; a card opens its
23
+ * review in the lightbox. Ported from the Alenika PDP section
24
+ * (src/components/product/reviews-list.tsx) with the Cartbase data seam:
25
+ *
26
+ * - Bootstrap is ONE call — `api/reviews.getWidget(client, productId)`
27
+ * (aggregate + first page + the store's widget settings; edge-cached).
28
+ * Server-fetch it and pass `initialData` (recommended — no client
29
+ * waterfall); when omitted the widget fetches it on mount.
30
+ * - The store's settings (admin, Reviews, Widgets) arrive in
31
+ * `options` and shape it: the header (`minimal`, `compact`,
32
+ * `expanded`, and what the minimal one shows), the rating breakdown,
33
+ * the sort menu, the date on the cards, masonry or list, and the page
34
+ * size. A field an older platform does not send takes Loox's default
35
+ * (`reviewWidgetOptions`).
36
+ * - Sort changes and "Show more reviews" re-fetch client-side via
37
+ * `listReviews` (the API orders; the default sort is photo reviews
38
+ * first, then newest).
39
+ * - Aggregate fallback preserved (production fix): if the aggregate is
40
+ * missing but reviews loaded, derive avg + distribution from the
41
+ * loaded page rather than showing a misleading "0.0" next to a filled
42
+ * star. The whole section renders null only when there are genuinely
43
+ * zero reviews.
44
+ */
45
+ export interface ReviewWidgetProps {
46
+ client: StorefrontClient
47
+ productId: string
48
+ /** Server-fetched `getWidget` payload (recommended). */
49
+ initialData?: ReviewWidgetPayload | null
50
+ labels?: Partial<ReviewsUiLabels>
51
+ className?: string
52
+ }
53
+
54
+ export function ReviewWidget({
55
+ client,
56
+ productId,
57
+ initialData,
58
+ labels,
59
+ className,
60
+ }: ReviewWidgetProps) {
61
+ // The mounted language, then the store's overrides (2026-09-14): a store
62
+ // that mounts StorefrontLocaleProvider passes nothing here.
63
+ const l = { ...useLocaleArea("reviews", defaultReviewsUiLabels), ...labels }
64
+ const [data, setData] = useState<ReviewWidgetPayload | null>(initialData ?? null)
65
+ // The first page is photo reviews first unless the store turned that off.
66
+ const [sort, setSort] = useState<ReviewSortKey>(
67
+ initialData?.options.photo_first === false ? "date" : "default"
68
+ )
69
+ const [reviews, setReviews] = useState<PublicReview[]>(initialData?.reviews ?? [])
70
+ const [count, setCount] = useState<number>(initialData?.count ?? 0)
71
+ const [hasMore, setHasMore] = useState<boolean>(initialData?.has_more ?? false)
72
+ const [loading, setLoading] = useState(false)
73
+ const sortChanged = useRef(false)
74
+
75
+ const options = reviewWidgetOptions(data?.options)
76
+ const pageSize = options.pageSize
77
+
78
+ // No server-provided bootstrap → fetch it on mount.
79
+ useEffect(() => {
80
+ if (initialData) return
81
+ let cancelled = false
82
+ ;(async () => {
83
+ try {
84
+ const payload = await getWidget(client, productId)
85
+ if (cancelled) return
86
+ setData(payload)
87
+ setReviews(payload.reviews)
88
+ setCount(payload.count)
89
+ setHasMore(payload.has_more)
90
+ if (payload.options.photo_first === false) setSort("date")
91
+ } catch {
92
+ /* widget is decorative — a failed bootstrap renders nothing */
93
+ }
94
+ })()
95
+ return () => {
96
+ cancelled = true
97
+ }
98
+ // eslint-disable-next-line react-hooks/exhaustive-deps
99
+ }, [client, productId])
100
+
101
+ // Re-fetch the first page when the shopper picks a sort (the bootstrap
102
+ // page already is the first sort, so the first run is skipped).
103
+ useEffect(() => {
104
+ if (!sortChanged.current) return
105
+ let cancelled = false
106
+ setLoading(true)
107
+ ;(async () => {
108
+ try {
109
+ const res = await listReviews(client, {
110
+ product_id: productId,
111
+ ...sortParamsFor(sort),
112
+ limit: pageSize,
113
+ offset: 0,
114
+ })
115
+ if (cancelled) return
116
+ setReviews(res.reviews)
117
+ setCount(res.count)
118
+ setHasMore(res.has_more)
119
+ } finally {
120
+ if (!cancelled) setLoading(false)
121
+ }
122
+ })()
123
+ return () => {
124
+ cancelled = true
125
+ }
126
+ // eslint-disable-next-line react-hooks/exhaustive-deps
127
+ }, [sort, productId, pageSize])
128
+
129
+ const chooseSort = (next: ReviewSortKey) => {
130
+ sortChanged.current = true
131
+ setSort(next)
132
+ }
133
+
134
+ const loadMore = useCallback(async () => {
135
+ setLoading(true)
136
+ try {
137
+ const res = await listReviews(client, {
138
+ product_id: productId,
139
+ ...sortParamsFor(sort),
140
+ limit: pageSize,
141
+ offset: reviews.length,
142
+ })
143
+ setReviews((prev) => [...prev, ...res.reviews])
144
+ setCount(res.count)
145
+ setHasMore(res.has_more)
146
+ } finally {
147
+ setLoading(false)
148
+ }
149
+ }, [client, productId, sort, pageSize, reviews.length])
150
+
151
+ // Header summary uses the full aggregate when available; else derive
152
+ // from the loaded reviews (see the header comment).
153
+ const aggregate = data?.aggregate ?? null
154
+ const total = aggregate?.count ?? count
155
+ const avg =
156
+ aggregate?.avg_rating ??
157
+ (reviews.length ? reviews.reduce((s, r) => s + r.rating, 0) / reviews.length : 0)
158
+ const distribution =
159
+ aggregate?.distribution ??
160
+ reviews.reduce(
161
+ (d, r) => {
162
+ const key = String(r.rating) as "1" | "2" | "3" | "4" | "5"
163
+ if (key in d) d[key]++
164
+ return d
165
+ },
166
+ { "1": 0, "2": 0, "3": 0, "4": 0, "5": 0 } as Record<"1" | "2" | "3" | "4" | "5", number>
167
+ )
168
+
169
+ if (total === 0) {
170
+ return null // Hide the section entirely when there are no reviews
171
+ }
172
+
173
+ return (
174
+ <section
175
+ id="reviews"
176
+ className={className ?? "max-w-[1440px] mx-auto px-4 sm:px-6 lg:px-10 my-16 sm:my-24"}
177
+ >
178
+ <ReviewSummaryHeader
179
+ layout={options.header}
180
+ content={options.headerContent}
181
+ total={total}
182
+ average={avg}
183
+ distribution={distribution}
184
+ showDistribution={options.showDistribution}
185
+ showSort={options.showSort}
186
+ sort={sort}
187
+ onSortChange={chooseSort}
188
+ labels={l}
189
+ />
190
+
191
+ <ReviewList
192
+ reviews={reviews}
193
+ layout={options.layout}
194
+ showDate={options.showDate}
195
+ labels={l}
196
+ className={cn(
197
+ options.layout === "list"
198
+ ? "columns-1 [column-gap:1rem]"
199
+ : // Two on a phone, then as many ~14rem columns as the page is wide.
200
+ "columns-2 sm:columns-[14rem] [column-gap:1rem]",
201
+ loading && "opacity-60 transition-opacity"
202
+ )}
203
+ />
204
+
205
+ {hasMore && (
206
+ <div className="mt-8 text-center">
207
+ <button
208
+ type="button"
209
+ onClick={loadMore}
210
+ disabled={loading}
211
+ className="inline-flex items-center justify-center rounded-md border border-border bg-background px-4 py-2 text-body-small text-foreground transition-colors hover:bg-muted disabled:cursor-not-allowed disabled:opacity-50"
212
+ >
213
+ {loading ? l.loading : l.loadMore}
214
+ </button>
215
+ </div>
216
+ )}
217
+ </section>
218
+ )
219
+ }
@@ -1,55 +1,55 @@
1
- /**
2
- * The reviews widget's display options with every default filled, from the
3
- * payload the platform serves (`getWidget().options`) or from nothing. A
4
- * platform older than a field leaves it out; the widget then shows what
5
- * Loox shows by default. Pure. Unit-tested in
6
- * tests/unit/storefront-review-lightbox.test.ts.
7
- */
8
- import type {
9
- ReviewWidgetHeader,
10
- ReviewWidgetHeaderContent,
11
- ReviewWidgetPayload,
12
- } from "../api/reviews"
13
-
14
- export interface ReviewWidgetOptions {
15
- layout: "masonry" | "list"
16
- pageSize: number
17
- header: ReviewWidgetHeader
18
- headerContent: ReviewWidgetHeaderContent
19
- showDistribution: boolean
20
- showSort: boolean
21
- showDate: boolean
22
- }
23
-
24
- export const DEFAULT_REVIEW_WIDGET_OPTIONS: ReviewWidgetOptions = {
25
- layout: "masonry",
26
- pageSize: 10,
27
- header: "minimal",
28
- headerContent: "count",
29
- showDistribution: true,
30
- showSort: true,
31
- showDate: true,
32
- }
33
-
34
- export function reviewWidgetOptions(
35
- options: Partial<ReviewWidgetPayload["options"]> | null | undefined
36
- ): ReviewWidgetOptions {
37
- const d = DEFAULT_REVIEW_WIDGET_OPTIONS
38
- if (!options) return { ...d }
39
- return {
40
- layout: options.layout === "list" ? "list" : d.layout,
41
- pageSize:
42
- typeof options.page_size === "number" && options.page_size > 0
43
- ? Math.trunc(options.page_size)
44
- : d.pageSize,
45
- header:
46
- options.header === "compact" || options.header === "expanded" ? options.header : d.header,
47
- headerContent:
48
- options.header_content === "average" || options.header_content === "stars"
49
- ? options.header_content
50
- : d.headerContent,
51
- showDistribution: options.show_distribution ?? d.showDistribution,
52
- showSort: options.show_sort ?? d.showSort,
53
- showDate: options.show_date ?? d.showDate,
54
- }
55
- }
1
+ /**
2
+ * The reviews widget's display options with every default filled, from the
3
+ * payload the platform serves (`getWidget().options`) or from nothing. A
4
+ * platform older than a field leaves it out; the widget then shows what
5
+ * Loox shows by default. Pure. Unit-tested in
6
+ * tests/unit/storefront-review-lightbox.test.ts.
7
+ */
8
+ import type {
9
+ ReviewWidgetHeader,
10
+ ReviewWidgetHeaderContent,
11
+ ReviewWidgetPayload,
12
+ } from "../api/reviews"
13
+
14
+ export interface ReviewWidgetOptions {
15
+ layout: "masonry" | "list"
16
+ pageSize: number
17
+ header: ReviewWidgetHeader
18
+ headerContent: ReviewWidgetHeaderContent
19
+ showDistribution: boolean
20
+ showSort: boolean
21
+ showDate: boolean
22
+ }
23
+
24
+ export const DEFAULT_REVIEW_WIDGET_OPTIONS: ReviewWidgetOptions = {
25
+ layout: "masonry",
26
+ pageSize: 20,
27
+ header: "minimal",
28
+ headerContent: "count",
29
+ showDistribution: true,
30
+ showSort: true,
31
+ showDate: true,
32
+ }
33
+
34
+ export function reviewWidgetOptions(
35
+ options: Partial<ReviewWidgetPayload["options"]> | null | undefined
36
+ ): ReviewWidgetOptions {
37
+ const d = DEFAULT_REVIEW_WIDGET_OPTIONS
38
+ if (!options) return { ...d }
39
+ return {
40
+ layout: options.layout === "list" ? "list" : d.layout,
41
+ pageSize:
42
+ typeof options.page_size === "number" && options.page_size > 0
43
+ ? Math.trunc(options.page_size)
44
+ : d.pageSize,
45
+ header:
46
+ options.header === "compact" || options.header === "expanded" ? options.header : d.header,
47
+ headerContent:
48
+ options.header_content === "average" || options.header_content === "stars"
49
+ ? options.header_content
50
+ : d.headerContent,
51
+ showDistribution: options.show_distribution ?? d.showDistribution,
52
+ showSort: options.show_sort ?? d.showSort,
53
+ showDate: options.show_date ?? d.showDate,
54
+ }
55
+ }