@cartbase/storefront 0.19.0 → 0.20.1
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/LICENSE +21 -21
- package/package.json +258 -258
- package/src/api/auth.ts +108 -108
- package/src/api/carts.ts +523 -523
- package/src/api/categories.ts +184 -184
- package/src/api/checkout.ts +526 -526
- package/src/api/collections.ts +130 -130
- package/src/api/consent.ts +75 -75
- package/src/api/content.ts +125 -125
- package/src/api/customers.ts +303 -303
- package/src/api/gift-cards.ts +112 -112
- package/src/api/http.ts +180 -180
- package/src/api/index.ts +30 -30
- package/src/api/menus.ts +77 -77
- package/src/api/metaobjects.ts +136 -136
- package/src/api/orders.ts +290 -290
- package/src/api/redirects.ts +37 -37
- package/src/api/regions.ts +200 -200
- package/src/api/reviews.ts +22 -1
- package/src/api/search.ts +163 -163
- package/src/api/store.ts +35 -35
- package/src/api/types.ts +91 -91
- package/src/cart-drawer/context.tsx +778 -778
- package/src/cart-drawer/cross-sell-carousel.tsx +211 -211
- package/src/cart-drawer/cross-sell-sidebar.tsx +158 -158
- package/src/cart-drawer/gift-wrap.tsx +82 -82
- package/src/cart-drawer/item/index.tsx +162 -162
- package/src/cart-drawer/item/upsell.tsx +110 -110
- package/src/cart-drawer/labels.ts +123 -123
- package/src/cart-drawer/mutation-queue.ts +78 -78
- package/src/cart-drawer/notes.tsx +131 -131
- package/src/cart-drawer/sticky-footer.tsx +73 -73
- package/src/cart-drawer/summary-breakdown.tsx +197 -197
- package/src/checkout/address-error-copy.ts +117 -117
- package/src/checkout/boxnow-locker-selector.tsx +410 -410
- package/src/checkout/compare-addresses.ts +40 -40
- package/src/checkout/discount-section.tsx +218 -218
- package/src/checkout/error-copy-codes.ts +63 -63
- package/src/checkout/geocode.ts +154 -154
- package/src/checkout/gift-card-section.tsx +224 -224
- package/src/checkout/index.ts +72 -72
- package/src/checkout/labels.ts +495 -495
- package/src/checkout/payment-button.tsx +372 -372
- package/src/checkout/payment-error-copy.ts +154 -154
- package/src/checkout/promotion-error-copy.ts +91 -91
- package/src/common/country-flag.tsx +52 -52
- package/src/common/country-select.tsx +11 -11
- package/src/common/index.ts +20 -20
- package/src/common/market-select.tsx +57 -57
- package/src/index.ts +12 -12
- package/src/lib/country-name.ts +59 -59
- package/src/lib/get-product-price.ts +133 -133
- package/src/lib/media-image.tsx +39 -39
- package/src/lib/payment-constants.ts +53 -53
- package/src/lib/platform.ts +13 -13
- package/src/lib/price.tsx +39 -39
- package/src/lib/store-api-error.ts +36 -36
- package/src/lib/variant-caption.ts +32 -32
- package/src/locales/bg.ts +3 -2
- package/src/locales/context.ts +37 -37
- package/src/locales/en.ts +26 -26
- package/src/locales/es.ts +3 -2
- package/src/locales/index.ts +19 -19
- package/src/locales/provider.tsx +59 -59
- package/src/locales/types.ts +77 -77
- package/src/order/index.ts +62 -62
- package/src/order/labels.ts +79 -79
- package/src/order/order-totals.tsx +250 -250
- package/src/primitives/select-field.tsx +93 -93
- package/src/products/image-gallery.tsx +43 -43
- package/src/products/option-select.tsx +67 -67
- package/src/products/product-info.tsx +5 -5
- package/src/products/product-price.tsx +69 -69
- package/src/products/product-promises.tsx +61 -61
- package/src/products/product-specs.tsx +65 -65
- package/src/products/product-tabs.tsx +123 -123
- package/src/products/purchase-options.tsx +130 -130
- package/src/products/sets.ts +110 -110
- package/src/products/variant-matching.ts +71 -71
- package/src/products/variant-url.ts +74 -74
- package/src/reviews-ui/index.ts +12 -0
- package/src/reviews-ui/labels.ts +5 -2
- package/src/reviews-ui/lightbox-state.ts +46 -46
- package/src/reviews-ui/review-header.tsx +166 -0
- package/src/reviews-ui/review-lightbox.tsx +271 -271
- package/src/reviews-ui/review-list.tsx +193 -191
- package/src/reviews-ui/review-widget.tsx +63 -71
- package/src/reviews-ui/star-badge.tsx +6 -4
- package/src/reviews-ui/widget-options.ts +55 -0
- package/src/store/category-template.tsx +136 -136
- package/src/store/index.ts +40 -40
- package/src/tracking/chatgpt-pixel.tsx +99 -99
- package/src/tracking/consent-init.tsx +62 -62
- package/src/tracking/events.ts +348 -348
- package/src/tracking/ga4.tsx +93 -93
- package/src/tracking/google-ads.ts +84 -84
- package/src/tracking/gtm.tsx +60 -60
- package/src/tracking/inline-script.ts +49 -49
- package/src/tracking/oaiq.ts +206 -206
- package/src/tracking/tiktok-pixel.tsx +91 -91
- package/src/tracking/track-init.tsx +56 -56
- package/src/tracking/track-order-purchase.tsx +122 -122
- package/src/tracking/ttq.ts +180 -180
- package/src/tracking/use-tracking-config.ts +54 -54
- package/theme/index.css +25 -25
|
@@ -1,211 +1,211 @@
|
|
|
1
|
-
"use client"
|
|
2
|
-
|
|
3
|
-
import { StoreImage as Image } from "../lib/media-image"
|
|
4
|
-
import Link from "next/link"
|
|
5
|
-
import { useRef, useState } from "react"
|
|
6
|
-
|
|
7
|
-
import { Price } from "../lib/price"
|
|
8
|
-
import { cn } from "../lib/utils"
|
|
9
|
-
import { useCartDrawer } from "./context"
|
|
10
|
-
|
|
11
|
-
/**
|
|
12
|
-
* CartCrossSellCarousel — horizontal scrollable product strip with left/right
|
|
13
|
-
* arrow controls. Used as mobile alternative to the sidebar or as a second
|
|
14
|
-
* cross-sell zone below the line items on desktop.
|
|
15
|
-
*
|
|
16
|
-
* Ported from `@1click/ui/src/cart-drawer/cross-sell-carousel.tsx` (v2.3.1).
|
|
17
|
-
* Cartbase data seam: same as the sidebar — feed from
|
|
18
|
-
* `@cartbase/storefront/api/products` `listProducts` or `api/search`
|
|
19
|
-
* `listRelatedProducts` via `toCrossSellProduct`
|
|
20
|
-
* (`./cross-sell-sidebar`; the card shapes are identical), wire `onAdd`
|
|
21
|
-
* to the provider's `addItem(variantId)`. Prices EUR major units.
|
|
22
|
-
*/
|
|
23
|
-
|
|
24
|
-
export type CrossSellCarouselProduct = {
|
|
25
|
-
id: string
|
|
26
|
-
title: string
|
|
27
|
-
handle: string
|
|
28
|
-
thumbnail: string | null
|
|
29
|
-
price: number
|
|
30
|
-
currencyCode: string
|
|
31
|
-
variantId?: string
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
type CartCrossSellCarouselProps = {
|
|
35
|
-
label?: string
|
|
36
|
-
products: CrossSellCarouselProduct[]
|
|
37
|
-
onAdd?: (productId: string, variantId?: string) => void
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
export function CartCrossSellCarousel({
|
|
41
|
-
label,
|
|
42
|
-
products,
|
|
43
|
-
onAdd,
|
|
44
|
-
}: CartCrossSellCarouselProps) {
|
|
45
|
-
const { labels, hrefs } = useCartDrawer()
|
|
46
|
-
const scrollRef = useRef<HTMLDivElement>(null)
|
|
47
|
-
const [canScrollLeft, setCanScrollLeft] = useState(false)
|
|
48
|
-
const [canScrollRight, setCanScrollRight] = useState(true)
|
|
49
|
-
|
|
50
|
-
const checkScroll = () => {
|
|
51
|
-
if (!scrollRef.current) return
|
|
52
|
-
const { scrollLeft, scrollWidth, clientWidth } = scrollRef.current
|
|
53
|
-
setCanScrollLeft(scrollLeft > 4)
|
|
54
|
-
setCanScrollRight(scrollLeft < scrollWidth - clientWidth - 4)
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
const scroll = (dir: "left" | "right") => {
|
|
58
|
-
if (!scrollRef.current) return
|
|
59
|
-
scrollRef.current.scrollBy({
|
|
60
|
-
left: dir === "left" ? -260 : 260,
|
|
61
|
-
behavior: "smooth",
|
|
62
|
-
})
|
|
63
|
-
setTimeout(checkScroll, 350)
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
if (!products.length) return null
|
|
67
|
-
|
|
68
|
-
return (
|
|
69
|
-
<div className="border-t border-border bg-muted/50 py-4">
|
|
70
|
-
<div className="flex items-center justify-between px-5 sm:px-6 mb-3">
|
|
71
|
-
<h3 className="text-sm font-semibold text-foreground">
|
|
72
|
-
{label ?? labels.youllLoveThis}
|
|
73
|
-
</h3>
|
|
74
|
-
<div className="hidden sm:flex items-center gap-1">
|
|
75
|
-
<button
|
|
76
|
-
type="button"
|
|
77
|
-
onClick={() => scroll("left")}
|
|
78
|
-
disabled={!canScrollLeft}
|
|
79
|
-
className="w-8 h-8 flex items-center justify-center rounded-full border border-border text-muted-foreground hover:border-text-muted hover:text-foreground disabled:opacity-30 disabled:cursor-not-allowed transition-all"
|
|
80
|
-
aria-label="Scroll left"
|
|
81
|
-
>
|
|
82
|
-
<svg
|
|
83
|
-
width="14"
|
|
84
|
-
height="14"
|
|
85
|
-
viewBox="0 0 14 14"
|
|
86
|
-
fill="none"
|
|
87
|
-
stroke="currentColor"
|
|
88
|
-
strokeWidth="1.5"
|
|
89
|
-
strokeLinecap="round"
|
|
90
|
-
strokeLinejoin="round"
|
|
91
|
-
>
|
|
92
|
-
<path d="M8.5 3L4.5 7l4 4" />
|
|
93
|
-
</svg>
|
|
94
|
-
</button>
|
|
95
|
-
<button
|
|
96
|
-
type="button"
|
|
97
|
-
onClick={() => scroll("right")}
|
|
98
|
-
disabled={!canScrollRight}
|
|
99
|
-
className="w-8 h-8 flex items-center justify-center rounded-full border border-border text-muted-foreground hover:border-text-muted hover:text-foreground disabled:opacity-30 disabled:cursor-not-allowed transition-all"
|
|
100
|
-
aria-label="Scroll right"
|
|
101
|
-
>
|
|
102
|
-
<svg
|
|
103
|
-
width="14"
|
|
104
|
-
height="14"
|
|
105
|
-
viewBox="0 0 14 14"
|
|
106
|
-
fill="none"
|
|
107
|
-
stroke="currentColor"
|
|
108
|
-
strokeWidth="1.5"
|
|
109
|
-
strokeLinecap="round"
|
|
110
|
-
strokeLinejoin="round"
|
|
111
|
-
>
|
|
112
|
-
<path d="M5.5 3l4 4-4 4" />
|
|
113
|
-
</svg>
|
|
114
|
-
</button>
|
|
115
|
-
</div>
|
|
116
|
-
</div>
|
|
117
|
-
|
|
118
|
-
<div
|
|
119
|
-
ref={scrollRef}
|
|
120
|
-
onScroll={checkScroll}
|
|
121
|
-
className="flex gap-2.5 overflow-x-auto no-scrollbar"
|
|
122
|
-
style={{
|
|
123
|
-
WebkitOverflowScrolling: "touch",
|
|
124
|
-
paddingLeft: "1.25rem",
|
|
125
|
-
paddingRight: "1.25rem",
|
|
126
|
-
}}
|
|
127
|
-
>
|
|
128
|
-
{products.map((product) => (
|
|
129
|
-
<div
|
|
130
|
-
key={product.id}
|
|
131
|
-
className={cn(
|
|
132
|
-
"flex-shrink-0 w-[260px] bg-card rounded-xl border border-border p-2.5",
|
|
133
|
-
"flex gap-3 items-center snap-start"
|
|
134
|
-
)}
|
|
135
|
-
style={{ boxShadow: "0 1px 3px rgba(0,0,0,0.04)" }}
|
|
136
|
-
>
|
|
137
|
-
<Link
|
|
138
|
-
href={`${hrefs.productPrefix}/${product.handle}`}
|
|
139
|
-
className="flex-shrink-0"
|
|
140
|
-
>
|
|
141
|
-
<div className="w-14 h-14 rounded-lg overflow-hidden bg-muted relative">
|
|
142
|
-
{product.thumbnail ? (
|
|
143
|
-
<Image
|
|
144
|
-
src={product.thumbnail}
|
|
145
|
-
alt={product.title}
|
|
146
|
-
fill
|
|
147
|
-
className="object-cover"
|
|
148
|
-
sizes="56px"
|
|
149
|
-
/>
|
|
150
|
-
) : (
|
|
151
|
-
<div className="w-full h-full flex items-center justify-center">
|
|
152
|
-
<svg
|
|
153
|
-
width="16"
|
|
154
|
-
height="16"
|
|
155
|
-
viewBox="0 0 24 24"
|
|
156
|
-
fill="none"
|
|
157
|
-
stroke="currentColor"
|
|
158
|
-
className="text-muted-foreground"
|
|
159
|
-
strokeWidth="1.2"
|
|
160
|
-
>
|
|
161
|
-
<rect x="3" y="3" width="18" height="18" rx="3" />
|
|
162
|
-
<circle cx="8.5" cy="8.5" r="1.5" />
|
|
163
|
-
<path
|
|
164
|
-
d="M21 15l-5-5L5 21"
|
|
165
|
-
strokeLinecap="round"
|
|
166
|
-
strokeLinejoin="round"
|
|
167
|
-
/>
|
|
168
|
-
</svg>
|
|
169
|
-
</div>
|
|
170
|
-
)}
|
|
171
|
-
</div>
|
|
172
|
-
</Link>
|
|
173
|
-
|
|
174
|
-
<div className="flex-1 min-w-0">
|
|
175
|
-
<Link
|
|
176
|
-
href={`${hrefs.productPrefix}/${product.handle}`}
|
|
177
|
-
className="text-[12px] font-semibold text-foreground line-clamp-1 leading-snug hover:text-muted-foreground transition-colors block"
|
|
178
|
-
>
|
|
179
|
-
{product.title}
|
|
180
|
-
</Link>
|
|
181
|
-
<Price
|
|
182
|
-
amount={product.price}
|
|
183
|
-
currencyCode={product.currencyCode}
|
|
184
|
-
className="text-[13px] font-bold text-foreground block mt-0.5"
|
|
185
|
-
/>
|
|
186
|
-
</div>
|
|
187
|
-
|
|
188
|
-
<button
|
|
189
|
-
type="button"
|
|
190
|
-
onClick={() => onAdd?.(product.id, product.variantId)}
|
|
191
|
-
className="w-8 h-8 flex-shrink-0 flex items-center justify-center rounded-full border border-border text-muted-foreground hover:border-text-base hover:text-foreground active:bg-muted transition-all"
|
|
192
|
-
aria-label={`${labels.addToCart} ${product.title}`}
|
|
193
|
-
>
|
|
194
|
-
<svg
|
|
195
|
-
width="14"
|
|
196
|
-
height="14"
|
|
197
|
-
viewBox="0 0 12 12"
|
|
198
|
-
fill="none"
|
|
199
|
-
stroke="currentColor"
|
|
200
|
-
strokeWidth="1.5"
|
|
201
|
-
strokeLinecap="round"
|
|
202
|
-
>
|
|
203
|
-
<path d="M6 2.5v7M2.5 6h7" />
|
|
204
|
-
</svg>
|
|
205
|
-
</button>
|
|
206
|
-
</div>
|
|
207
|
-
))}
|
|
208
|
-
</div>
|
|
209
|
-
</div>
|
|
210
|
-
)
|
|
211
|
-
}
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import { StoreImage as Image } from "../lib/media-image"
|
|
4
|
+
import Link from "next/link"
|
|
5
|
+
import { useRef, useState } from "react"
|
|
6
|
+
|
|
7
|
+
import { Price } from "../lib/price"
|
|
8
|
+
import { cn } from "../lib/utils"
|
|
9
|
+
import { useCartDrawer } from "./context"
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* CartCrossSellCarousel — horizontal scrollable product strip with left/right
|
|
13
|
+
* arrow controls. Used as mobile alternative to the sidebar or as a second
|
|
14
|
+
* cross-sell zone below the line items on desktop.
|
|
15
|
+
*
|
|
16
|
+
* Ported from `@1click/ui/src/cart-drawer/cross-sell-carousel.tsx` (v2.3.1).
|
|
17
|
+
* Cartbase data seam: same as the sidebar — feed from
|
|
18
|
+
* `@cartbase/storefront/api/products` `listProducts` or `api/search`
|
|
19
|
+
* `listRelatedProducts` via `toCrossSellProduct`
|
|
20
|
+
* (`./cross-sell-sidebar`; the card shapes are identical), wire `onAdd`
|
|
21
|
+
* to the provider's `addItem(variantId)`. Prices EUR major units.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
export type CrossSellCarouselProduct = {
|
|
25
|
+
id: string
|
|
26
|
+
title: string
|
|
27
|
+
handle: string
|
|
28
|
+
thumbnail: string | null
|
|
29
|
+
price: number
|
|
30
|
+
currencyCode: string
|
|
31
|
+
variantId?: string
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
type CartCrossSellCarouselProps = {
|
|
35
|
+
label?: string
|
|
36
|
+
products: CrossSellCarouselProduct[]
|
|
37
|
+
onAdd?: (productId: string, variantId?: string) => void
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function CartCrossSellCarousel({
|
|
41
|
+
label,
|
|
42
|
+
products,
|
|
43
|
+
onAdd,
|
|
44
|
+
}: CartCrossSellCarouselProps) {
|
|
45
|
+
const { labels, hrefs } = useCartDrawer()
|
|
46
|
+
const scrollRef = useRef<HTMLDivElement>(null)
|
|
47
|
+
const [canScrollLeft, setCanScrollLeft] = useState(false)
|
|
48
|
+
const [canScrollRight, setCanScrollRight] = useState(true)
|
|
49
|
+
|
|
50
|
+
const checkScroll = () => {
|
|
51
|
+
if (!scrollRef.current) return
|
|
52
|
+
const { scrollLeft, scrollWidth, clientWidth } = scrollRef.current
|
|
53
|
+
setCanScrollLeft(scrollLeft > 4)
|
|
54
|
+
setCanScrollRight(scrollLeft < scrollWidth - clientWidth - 4)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const scroll = (dir: "left" | "right") => {
|
|
58
|
+
if (!scrollRef.current) return
|
|
59
|
+
scrollRef.current.scrollBy({
|
|
60
|
+
left: dir === "left" ? -260 : 260,
|
|
61
|
+
behavior: "smooth",
|
|
62
|
+
})
|
|
63
|
+
setTimeout(checkScroll, 350)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (!products.length) return null
|
|
67
|
+
|
|
68
|
+
return (
|
|
69
|
+
<div className="border-t border-border bg-muted/50 py-4">
|
|
70
|
+
<div className="flex items-center justify-between px-5 sm:px-6 mb-3">
|
|
71
|
+
<h3 className="text-sm font-semibold text-foreground">
|
|
72
|
+
{label ?? labels.youllLoveThis}
|
|
73
|
+
</h3>
|
|
74
|
+
<div className="hidden sm:flex items-center gap-1">
|
|
75
|
+
<button
|
|
76
|
+
type="button"
|
|
77
|
+
onClick={() => scroll("left")}
|
|
78
|
+
disabled={!canScrollLeft}
|
|
79
|
+
className="w-8 h-8 flex items-center justify-center rounded-full border border-border text-muted-foreground hover:border-text-muted hover:text-foreground disabled:opacity-30 disabled:cursor-not-allowed transition-all"
|
|
80
|
+
aria-label="Scroll left"
|
|
81
|
+
>
|
|
82
|
+
<svg
|
|
83
|
+
width="14"
|
|
84
|
+
height="14"
|
|
85
|
+
viewBox="0 0 14 14"
|
|
86
|
+
fill="none"
|
|
87
|
+
stroke="currentColor"
|
|
88
|
+
strokeWidth="1.5"
|
|
89
|
+
strokeLinecap="round"
|
|
90
|
+
strokeLinejoin="round"
|
|
91
|
+
>
|
|
92
|
+
<path d="M8.5 3L4.5 7l4 4" />
|
|
93
|
+
</svg>
|
|
94
|
+
</button>
|
|
95
|
+
<button
|
|
96
|
+
type="button"
|
|
97
|
+
onClick={() => scroll("right")}
|
|
98
|
+
disabled={!canScrollRight}
|
|
99
|
+
className="w-8 h-8 flex items-center justify-center rounded-full border border-border text-muted-foreground hover:border-text-muted hover:text-foreground disabled:opacity-30 disabled:cursor-not-allowed transition-all"
|
|
100
|
+
aria-label="Scroll right"
|
|
101
|
+
>
|
|
102
|
+
<svg
|
|
103
|
+
width="14"
|
|
104
|
+
height="14"
|
|
105
|
+
viewBox="0 0 14 14"
|
|
106
|
+
fill="none"
|
|
107
|
+
stroke="currentColor"
|
|
108
|
+
strokeWidth="1.5"
|
|
109
|
+
strokeLinecap="round"
|
|
110
|
+
strokeLinejoin="round"
|
|
111
|
+
>
|
|
112
|
+
<path d="M5.5 3l4 4-4 4" />
|
|
113
|
+
</svg>
|
|
114
|
+
</button>
|
|
115
|
+
</div>
|
|
116
|
+
</div>
|
|
117
|
+
|
|
118
|
+
<div
|
|
119
|
+
ref={scrollRef}
|
|
120
|
+
onScroll={checkScroll}
|
|
121
|
+
className="flex gap-2.5 overflow-x-auto no-scrollbar"
|
|
122
|
+
style={{
|
|
123
|
+
WebkitOverflowScrolling: "touch",
|
|
124
|
+
paddingLeft: "1.25rem",
|
|
125
|
+
paddingRight: "1.25rem",
|
|
126
|
+
}}
|
|
127
|
+
>
|
|
128
|
+
{products.map((product) => (
|
|
129
|
+
<div
|
|
130
|
+
key={product.id}
|
|
131
|
+
className={cn(
|
|
132
|
+
"flex-shrink-0 w-[260px] bg-card rounded-xl border border-border p-2.5",
|
|
133
|
+
"flex gap-3 items-center snap-start"
|
|
134
|
+
)}
|
|
135
|
+
style={{ boxShadow: "0 1px 3px rgba(0,0,0,0.04)" }}
|
|
136
|
+
>
|
|
137
|
+
<Link
|
|
138
|
+
href={`${hrefs.productPrefix}/${product.handle}`}
|
|
139
|
+
className="flex-shrink-0"
|
|
140
|
+
>
|
|
141
|
+
<div className="w-14 h-14 rounded-lg overflow-hidden bg-muted relative">
|
|
142
|
+
{product.thumbnail ? (
|
|
143
|
+
<Image
|
|
144
|
+
src={product.thumbnail}
|
|
145
|
+
alt={product.title}
|
|
146
|
+
fill
|
|
147
|
+
className="object-cover"
|
|
148
|
+
sizes="56px"
|
|
149
|
+
/>
|
|
150
|
+
) : (
|
|
151
|
+
<div className="w-full h-full flex items-center justify-center">
|
|
152
|
+
<svg
|
|
153
|
+
width="16"
|
|
154
|
+
height="16"
|
|
155
|
+
viewBox="0 0 24 24"
|
|
156
|
+
fill="none"
|
|
157
|
+
stroke="currentColor"
|
|
158
|
+
className="text-muted-foreground"
|
|
159
|
+
strokeWidth="1.2"
|
|
160
|
+
>
|
|
161
|
+
<rect x="3" y="3" width="18" height="18" rx="3" />
|
|
162
|
+
<circle cx="8.5" cy="8.5" r="1.5" />
|
|
163
|
+
<path
|
|
164
|
+
d="M21 15l-5-5L5 21"
|
|
165
|
+
strokeLinecap="round"
|
|
166
|
+
strokeLinejoin="round"
|
|
167
|
+
/>
|
|
168
|
+
</svg>
|
|
169
|
+
</div>
|
|
170
|
+
)}
|
|
171
|
+
</div>
|
|
172
|
+
</Link>
|
|
173
|
+
|
|
174
|
+
<div className="flex-1 min-w-0">
|
|
175
|
+
<Link
|
|
176
|
+
href={`${hrefs.productPrefix}/${product.handle}`}
|
|
177
|
+
className="text-[12px] font-semibold text-foreground line-clamp-1 leading-snug hover:text-muted-foreground transition-colors block"
|
|
178
|
+
>
|
|
179
|
+
{product.title}
|
|
180
|
+
</Link>
|
|
181
|
+
<Price
|
|
182
|
+
amount={product.price}
|
|
183
|
+
currencyCode={product.currencyCode}
|
|
184
|
+
className="text-[13px] font-bold text-foreground block mt-0.5"
|
|
185
|
+
/>
|
|
186
|
+
</div>
|
|
187
|
+
|
|
188
|
+
<button
|
|
189
|
+
type="button"
|
|
190
|
+
onClick={() => onAdd?.(product.id, product.variantId)}
|
|
191
|
+
className="w-8 h-8 flex-shrink-0 flex items-center justify-center rounded-full border border-border text-muted-foreground hover:border-text-base hover:text-foreground active:bg-muted transition-all"
|
|
192
|
+
aria-label={`${labels.addToCart} ${product.title}`}
|
|
193
|
+
>
|
|
194
|
+
<svg
|
|
195
|
+
width="14"
|
|
196
|
+
height="14"
|
|
197
|
+
viewBox="0 0 12 12"
|
|
198
|
+
fill="none"
|
|
199
|
+
stroke="currentColor"
|
|
200
|
+
strokeWidth="1.5"
|
|
201
|
+
strokeLinecap="round"
|
|
202
|
+
>
|
|
203
|
+
<path d="M6 2.5v7M2.5 6h7" />
|
|
204
|
+
</svg>
|
|
205
|
+
</button>
|
|
206
|
+
</div>
|
|
207
|
+
))}
|
|
208
|
+
</div>
|
|
209
|
+
</div>
|
|
210
|
+
)
|
|
211
|
+
}
|