@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,193 +1,193 @@
1
- "use client"
2
-
3
- import { useState } from "react"
4
- import { CircleCheck } from "lucide-react"
5
- import type { PublicReview } from "../api/reviews"
6
- import { useLocaleArea } from "../locales/context"
7
- import { defaultReviewsUiLabels, type ReviewsUiLabels } from "./labels"
8
- import { formatReviewDate, reviewDisplayName } from "./helpers"
9
- import { lightboxMedia, type LightboxPosition } from "./lightbox-state"
10
- import { ReviewLightbox } from "./review-lightbox"
11
- import { StarRow } from "./star-badge"
12
-
13
- /**
14
- * Review cards (masonry) + the review lightbox. Ported from the Alenika PDP
15
- * reviews section (src/components/product/reviews-list.tsx, the card
16
- * grid), presentational half: the fetch/sort/paging orchestration lives in
17
- * `ReviewWidget`. Data: `api/reviews` `PublicReview[]` (hidden media is
18
- * filtered out server-side, the client just renders what it gets).
19
- *
20
- * A card (Loox's, as evoo.bg runs it): the first photo on top at its own
21
- * height, then the reviewer with the verified badge, the date, the stars and
22
- * the text. A click anywhere on a card opens that review in
23
- * `ReviewLightbox`; a click on one of its photos opens it on that photo.
24
- *
25
- * Verified-purchaser badge is unconditional — every Cartbase review comes
26
- * from a tokenized post-purchase email, so the badge is a tautology of
27
- * the system, not an opt-in flag.
28
- *
29
- * Masonry: CSS multi-column so photo reviews (tall) and text-only reviews
30
- * (short) pack together without empty gaps; `break-inside-avoid` keeps
31
- * each card whole. Images-first ordering comes from the API's default
32
- * sort, so photo cards lead the flow. `layout="list"` (the store's
33
- * widget_layout setting) renders a single column instead.
34
- */
35
- export interface ReviewListProps {
36
- reviews: PublicReview[]
37
- /** Store display option (`getWidget().options.layout`). Default masonry. */
38
- layout?: "masonry" | "list"
39
- /** The date on each card (`getWidget().options.show_date`). Default on. */
40
- showDate?: boolean
41
- labels?: Partial<ReviewsUiLabels>
42
- className?: string
43
- }
44
-
45
- export function ReviewList({
46
- reviews,
47
- layout = "masonry",
48
- showDate = true,
49
- labels,
50
- className,
51
- }: ReviewListProps) {
52
- const l = { ...useLocaleArea("reviews", defaultReviewsUiLabels), ...labels }
53
- const [open, setOpen] = useState<LightboxPosition | null>(null)
54
-
55
- return (
56
- <>
57
- <ul
58
- className={
59
- className ??
60
- (layout === "list"
61
- ? "columns-1 [column-gap:1rem]"
62
- : "columns-1 sm:columns-2 lg:columns-3 xl:columns-4 [column-gap:1rem]")
63
- }
64
- >
65
- {reviews.map((r, index) => {
66
- const media = lightboxMedia(r)
67
- const hero = media[0]
68
- const rest = media.slice(1)
69
- return (
70
- <li
71
- key={r.id}
72
- className="relative mb-4 break-inside-avoid overflow-hidden rounded-lg bg-card shadow-card"
73
- >
74
- {/* The whole card is the button; the photos sit above it. */}
75
- <button
76
- type="button"
77
- onClick={() => setOpen({ review: index, media: 0 })}
78
- aria-label={`${l.openReview}: ${reviewDisplayName(r.customer_name)}`}
79
- className="absolute inset-0 z-0 cursor-pointer"
80
- />
81
-
82
- {/* Hero media on top */}
83
- {hero && (
84
- <button
85
- type="button"
86
- onClick={() => setOpen({ review: index, media: 0 })}
87
- aria-label={hero.type === "image" ? l.showPhoto : l.playVideo}
88
- className="relative z-10 block w-full group"
89
- >
90
- {hero.type === "image" ? (
91
- <img
92
- src={hero.thumb || hero.url}
93
- alt=""
94
- loading="lazy"
95
- className="block w-full h-auto object-cover transition-transform group-hover:scale-[1.02]"
96
- />
97
- ) : (
98
- <>
99
- {/* `#t=0.001`: iOS Safari paints no first frame from
100
- `preload="metadata"` alone; the seek makes it. */}
101
- <video
102
- src={`${hero.url}#t=0.001`}
103
- muted
104
- playsInline
105
- preload="metadata"
106
- className="block w-full h-auto object-cover bg-black"
107
- />
108
- <span className="absolute inset-0 flex items-center justify-center">
109
- <span className="w-11 h-11 rounded-full bg-background/80 flex items-center justify-center">
110
- <span
111
- aria-hidden
112
- className="w-0 h-0 border-y-[7px] border-y-transparent border-l-[12px] border-l-foreground ml-0.5"
113
- />
114
- </span>
115
- </span>
116
- </>
117
- )}
118
- </button>
119
- )}
120
-
121
- <div className="p-4">
122
- <div className="flex flex-wrap items-center gap-x-2 gap-y-0.5">
123
- <span className="text-body font-semibold text-foreground">
124
- {reviewDisplayName(r.customer_name)}
125
- </span>
126
- <span
127
- className="inline-flex items-center gap-1 text-caption text-foreground"
128
- title={l.verifiedTitle}
129
- >
130
- <CircleCheck aria-hidden className="size-4 fill-foreground text-background" />
131
- {l.verified}
132
- </span>
133
- </div>
134
- {showDate ? (
135
- <p className="mt-0.5 text-caption text-muted-foreground">
136
- {formatReviewDate(r.created_at, l)}
137
- </p>
138
- ) : null}
139
- <StarRow rating={r.rating} size="md" className="mt-2" />
140
-
141
- {r.title && (
142
- <h3 className="mt-2 text-h6 text-foreground">{r.title}</h3>
143
- )}
144
- {r.body && (
145
- <p className="mt-2 whitespace-pre-wrap text-body text-foreground">{r.body}</p>
146
- )}
147
-
148
- {/* Extra media beyond the hero */}
149
- {rest.length > 0 && (
150
- <div className="relative z-10 mt-3 grid grid-cols-4 gap-2">
151
- {rest.map((m, i) => (
152
- <button
153
- key={i}
154
- type="button"
155
- onClick={() => setOpen({ review: index, media: i + 1 })}
156
- aria-label={m.type === "image" ? l.showPhoto : l.playVideo}
157
- className="relative aspect-square overflow-hidden rounded-md bg-muted"
158
- >
159
- {m.type === "image" ? (
160
- <img
161
- src={m.thumb || m.url}
162
- alt=""
163
- loading="lazy"
164
- className="absolute inset-0 w-full h-full object-cover"
165
- />
166
- ) : (
167
- <video
168
- src={`${m.url}#t=0.001`}
169
- muted
170
- playsInline
171
- preload="metadata"
172
- className="absolute inset-0 w-full h-full object-cover bg-black"
173
- />
174
- )}
175
- </button>
176
- ))}
177
- </div>
178
- )}
179
- </div>
180
- </li>
181
- )
182
- })}
183
- </ul>
184
-
185
- <ReviewLightbox
186
- reviews={reviews}
187
- position={open}
188
- onPositionChange={setOpen}
189
- labels={labels}
190
- />
191
- </>
192
- )
193
- }
1
+ "use client"
2
+
3
+ import { useState } from "react"
4
+ import { CircleCheck } from "lucide-react"
5
+ import type { PublicReview } from "../api/reviews"
6
+ import { useLocaleArea } from "../locales/context"
7
+ import { defaultReviewsUiLabels, type ReviewsUiLabels } from "./labels"
8
+ import { formatReviewDate, reviewDisplayName } from "./helpers"
9
+ import { lightboxMedia, type LightboxPosition } from "./lightbox-state"
10
+ import { ReviewLightbox } from "./review-lightbox"
11
+ import { StarRow } from "./star-badge"
12
+
13
+ /**
14
+ * Review cards (masonry) + the review lightbox. Ported from the Alenika PDP
15
+ * reviews section (src/components/product/reviews-list.tsx, the card
16
+ * grid), presentational half: the fetch/sort/paging orchestration lives in
17
+ * `ReviewWidget`. Data: `api/reviews` `PublicReview[]` (hidden media is
18
+ * filtered out server-side, the client just renders what it gets).
19
+ *
20
+ * A card (Loox's, as evoo.bg runs it): the first photo on top at its own
21
+ * height, then the reviewer with the verified badge, the date, the stars and
22
+ * the text. A click anywhere on a card opens that review in
23
+ * `ReviewLightbox`; a click on one of its photos opens it on that photo.
24
+ *
25
+ * Verified-purchaser badge is unconditional — every Cartbase review comes
26
+ * from a tokenized post-purchase email, so the badge is a tautology of
27
+ * the system, not an opt-in flag.
28
+ *
29
+ * Masonry: CSS multi-column so photo reviews (tall) and text-only reviews
30
+ * (short) pack together without empty gaps; `break-inside-avoid` keeps
31
+ * each card whole. Images-first ordering comes from the API's default
32
+ * sort, so photo cards lead the flow. `layout="list"` (the store's
33
+ * widget_layout setting) renders a single column instead.
34
+ */
35
+ export interface ReviewListProps {
36
+ reviews: PublicReview[]
37
+ /** Store display option (`getWidget().options.layout`). Default masonry. */
38
+ layout?: "masonry" | "list"
39
+ /** The date on each card (`getWidget().options.show_date`). Default on. */
40
+ showDate?: boolean
41
+ labels?: Partial<ReviewsUiLabels>
42
+ className?: string
43
+ }
44
+
45
+ export function ReviewList({
46
+ reviews,
47
+ layout = "masonry",
48
+ showDate = true,
49
+ labels,
50
+ className,
51
+ }: ReviewListProps) {
52
+ const l = { ...useLocaleArea("reviews", defaultReviewsUiLabels), ...labels }
53
+ const [open, setOpen] = useState<LightboxPosition | null>(null)
54
+
55
+ return (
56
+ <>
57
+ <ul
58
+ className={
59
+ className ??
60
+ (layout === "list"
61
+ ? "columns-1 [column-gap:1rem]"
62
+ : "columns-2 sm:columns-[14rem] [column-gap:1rem]")
63
+ }
64
+ >
65
+ {reviews.map((r, index) => {
66
+ const media = lightboxMedia(r)
67
+ const hero = media[0]
68
+ const rest = media.slice(1)
69
+ return (
70
+ <li
71
+ key={r.id}
72
+ className="relative mb-4 break-inside-avoid overflow-hidden rounded-lg bg-card shadow-card"
73
+ >
74
+ {/* The whole card is the button; the photos sit above it. */}
75
+ <button
76
+ type="button"
77
+ onClick={() => setOpen({ review: index, media: 0 })}
78
+ aria-label={`${l.openReview}: ${reviewDisplayName(r.customer_name)}`}
79
+ className="absolute inset-0 z-0 cursor-pointer"
80
+ />
81
+
82
+ {/* Hero media on top */}
83
+ {hero && (
84
+ <button
85
+ type="button"
86
+ onClick={() => setOpen({ review: index, media: 0 })}
87
+ aria-label={hero.type === "image" ? l.showPhoto : l.playVideo}
88
+ className="relative z-10 block w-full group"
89
+ >
90
+ {hero.type === "image" ? (
91
+ <img
92
+ src={hero.thumb || hero.url}
93
+ alt=""
94
+ loading="lazy"
95
+ className="block w-full h-auto object-cover transition-transform group-hover:scale-[1.02]"
96
+ />
97
+ ) : (
98
+ <>
99
+ {/* `#t=0.001`: iOS Safari paints no first frame from
100
+ `preload="metadata"` alone; the seek makes it. */}
101
+ <video
102
+ src={`${hero.url}#t=0.001`}
103
+ muted
104
+ playsInline
105
+ preload="metadata"
106
+ className="block w-full h-auto object-cover bg-black"
107
+ />
108
+ <span className="absolute inset-0 flex items-center justify-center">
109
+ <span className="w-11 h-11 rounded-full bg-background/80 flex items-center justify-center">
110
+ <span
111
+ aria-hidden
112
+ className="w-0 h-0 border-y-[7px] border-y-transparent border-l-[12px] border-l-foreground ml-0.5"
113
+ />
114
+ </span>
115
+ </span>
116
+ </>
117
+ )}
118
+ </button>
119
+ )}
120
+
121
+ <div className="p-4">
122
+ <div className="flex flex-wrap items-center gap-x-2 gap-y-0.5">
123
+ <span className="text-body font-semibold text-foreground">
124
+ {reviewDisplayName(r.customer_name)}
125
+ </span>
126
+ <span
127
+ className="inline-flex items-center gap-1 text-caption text-foreground"
128
+ title={l.verifiedTitle}
129
+ >
130
+ <CircleCheck aria-hidden className="size-4 fill-foreground text-background" />
131
+ {l.verified}
132
+ </span>
133
+ </div>
134
+ {showDate ? (
135
+ <p className="mt-0.5 text-caption text-muted-foreground">
136
+ {formatReviewDate(r.created_at, l)}
137
+ </p>
138
+ ) : null}
139
+ <StarRow rating={r.rating} size="md" className="mt-2" />
140
+
141
+ {r.title && (
142
+ <h3 className="mt-2 text-h6 text-foreground">{r.title}</h3>
143
+ )}
144
+ {r.body && (
145
+ <p className="mt-2 whitespace-pre-wrap text-body text-foreground">{r.body}</p>
146
+ )}
147
+
148
+ {/* Extra media beyond the hero */}
149
+ {rest.length > 0 && (
150
+ <div className="relative z-10 mt-3 grid grid-cols-4 gap-2">
151
+ {rest.map((m, i) => (
152
+ <button
153
+ key={i}
154
+ type="button"
155
+ onClick={() => setOpen({ review: index, media: i + 1 })}
156
+ aria-label={m.type === "image" ? l.showPhoto : l.playVideo}
157
+ className="relative aspect-square overflow-hidden rounded-md bg-muted"
158
+ >
159
+ {m.type === "image" ? (
160
+ <img
161
+ src={m.thumb || m.url}
162
+ alt=""
163
+ loading="lazy"
164
+ className="absolute inset-0 w-full h-full object-cover"
165
+ />
166
+ ) : (
167
+ <video
168
+ src={`${m.url}#t=0.001`}
169
+ muted
170
+ playsInline
171
+ preload="metadata"
172
+ className="absolute inset-0 w-full h-full object-cover bg-black"
173
+ />
174
+ )}
175
+ </button>
176
+ ))}
177
+ </div>
178
+ )}
179
+ </div>
180
+ </li>
181
+ )
182
+ })}
183
+ </ul>
184
+
185
+ <ReviewLightbox
186
+ reviews={reviews}
187
+ position={open}
188
+ onPositionChange={setOpen}
189
+ labels={labels}
190
+ />
191
+ </>
192
+ )
193
+ }