@cartbase/storefront 0.1.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/package.json +226 -0
- package/src/api/auth.ts +108 -0
- package/src/api/carts.ts +506 -0
- package/src/api/categories.ts +184 -0
- package/src/api/checkout.ts +440 -0
- package/src/api/collections.ts +130 -0
- package/src/api/consent.ts +75 -0
- package/src/api/content.ts +125 -0
- package/src/api/customers.ts +307 -0
- package/src/api/gift-cards.ts +112 -0
- package/src/api/http.ts +122 -0
- package/src/api/index.ts +29 -0
- package/src/api/integrations.ts +130 -0
- package/src/api/menus.ts +77 -0
- package/src/api/metaobjects.ts +136 -0
- package/src/api/orders.ts +290 -0
- package/src/api/products.ts +303 -0
- package/src/api/redirects.ts +37 -0
- package/src/api/regions.ts +141 -0
- package/src/api/reviews.ts +259 -0
- package/src/api/search.ts +133 -0
- package/src/api/types.ts +91 -0
- package/src/cart-drawer/cart-drawer.tsx +86 -0
- package/src/cart-drawer/context.tsx +569 -0
- package/src/cart-drawer/continue-shopping.tsx +28 -0
- package/src/cart-drawer/cross-sell-carousel.tsx +212 -0
- package/src/cart-drawer/cross-sell-sidebar.tsx +158 -0
- package/src/cart-drawer/empty.tsx +52 -0
- package/src/cart-drawer/free-gift.tsx +71 -0
- package/src/cart-drawer/gift-wrap.tsx +83 -0
- package/src/cart-drawer/header.tsx +52 -0
- package/src/cart-drawer/index.ts +69 -0
- package/src/cart-drawer/item/index.tsx +164 -0
- package/src/cart-drawer/item/quantity.tsx +100 -0
- package/src/cart-drawer/item/upsell.tsx +110 -0
- package/src/cart-drawer/item/variant.tsx +46 -0
- package/src/cart-drawer/labels-bg.ts +72 -0
- package/src/cart-drawer/labels.ts +119 -0
- package/src/cart-drawer/notes.tsx +131 -0
- package/src/cart-drawer/payment-badges.tsx +96 -0
- package/src/cart-drawer/promo-banner.tsx +43 -0
- package/src/cart-drawer/rewards-points.tsx +78 -0
- package/src/cart-drawer/sticky-footer.tsx +73 -0
- package/src/cart-drawer/summary-breakdown.tsx +196 -0
- package/src/cart-drawer/template.tsx +225 -0
- package/src/cart-drawer/tiered-progress.tsx +168 -0
- package/src/checkout/address-error-copy.ts +119 -0
- package/src/checkout/address-form.tsx +224 -0
- package/src/checkout/address-select.tsx +79 -0
- package/src/checkout/boxnow-locker-selector.tsx +410 -0
- package/src/checkout/checkout-client.tsx +222 -0
- package/src/checkout/company-details.tsx +94 -0
- package/src/checkout/compare-addresses.ts +40 -0
- package/src/checkout/context.tsx +76 -0
- package/src/checkout/discount-section.tsx +218 -0
- package/src/checkout/econt-office-selector.tsx +332 -0
- package/src/checkout/error-message.tsx +25 -0
- package/src/checkout/geocode.ts +154 -0
- package/src/checkout/gift-card-section.tsx +224 -0
- package/src/checkout/index.ts +74 -0
- package/src/checkout/labels-bg.ts +128 -0
- package/src/checkout/labels.ts +263 -0
- package/src/checkout/line-item-card.tsx +152 -0
- package/src/checkout/order-summary.tsx +524 -0
- package/src/checkout/payment-button.tsx +373 -0
- package/src/checkout/payment-error-copy.ts +269 -0
- package/src/checkout/payment-method-list.tsx +365 -0
- package/src/checkout/payment-wrapper.tsx +102 -0
- package/src/checkout/promotion-error-copy.ts +124 -0
- package/src/checkout/shipping-method-list.tsx +335 -0
- package/src/checkout/stripe-wrapper.tsx +165 -0
- package/src/checkout/use-checkout-orchestration.ts +1504 -0
- package/src/common/cart-button-client.tsx +39 -0
- package/src/common/cart-button.tsx +28 -0
- package/src/common/country-select.tsx +65 -0
- package/src/common/delete-button.tsx +66 -0
- package/src/common/index.ts +17 -0
- package/src/common/language-select.tsx +78 -0
- package/src/common/localized-link.tsx +45 -0
- package/src/common/skeleton.tsx +29 -0
- package/src/index.ts +12 -0
- package/src/lib/cart-helpers.ts +113 -0
- package/src/lib/dual-price.tsx +73 -0
- package/src/lib/get-percentage-diff.ts +5 -0
- package/src/lib/get-product-price.ts +133 -0
- package/src/lib/hooks/use-intersection.ts +30 -0
- package/src/lib/hooks/use-toggle-state.ts +25 -0
- package/src/lib/money.ts +73 -0
- package/src/lib/payment-constants.ts +66 -0
- package/src/lib/product.ts +22 -0
- package/src/lib/sort-products.ts +63 -0
- package/src/lib/store-api-error.ts +36 -0
- package/src/lib/utils.ts +16 -0
- package/src/order/context.tsx +32 -0
- package/src/order/index.ts +63 -0
- package/src/order/labels-bg.ts +39 -0
- package/src/order/labels.ts +79 -0
- package/src/order/order-address-card.tsx +47 -0
- package/src/order/order-completed-template.tsx +165 -0
- package/src/order/order-confirmation-header.tsx +65 -0
- package/src/order/order-delivery-card.tsx +258 -0
- package/src/order/order-help-section.tsx +47 -0
- package/src/order/order-item.tsx +201 -0
- package/src/order/order-items-list.tsx +52 -0
- package/src/order/order-payment-card.tsx +95 -0
- package/src/order/order-timeline.tsx +141 -0
- package/src/order/order-totals.tsx +245 -0
- package/src/primitives/field.tsx +125 -0
- package/src/primitives/select-field.tsx +77 -0
- package/src/primitives/ui/accordion.tsx +61 -0
- package/src/primitives/ui/button.tsx +68 -0
- package/src/primitives/ui/collapsible.tsx +16 -0
- package/src/primitives/ui/dialog.tsx +112 -0
- package/src/primitives/ui/input.tsx +30 -0
- package/src/primitives/ui/label.tsx +31 -0
- package/src/primitives/ui/popover.tsx +38 -0
- package/src/primitives/ui/select.tsx +163 -0
- package/src/primitives/ui/sheet.tsx +131 -0
- package/src/primitives/ui/tabs.tsx +62 -0
- package/src/products/context.tsx +34 -0
- package/src/products/image-gallery.tsx +43 -0
- package/src/products/index.ts +44 -0
- package/src/products/labels-bg.ts +35 -0
- package/src/products/labels.ts +57 -0
- package/src/products/mobile-actions.tsx +180 -0
- package/src/products/option-select.tsx +67 -0
- package/src/products/preview-price.tsx +36 -0
- package/src/products/product-actions-wrapper.tsx +58 -0
- package/src/products/product-actions.tsx +217 -0
- package/src/products/product-info.tsx +43 -0
- package/src/products/product-preview.tsx +49 -0
- package/src/products/product-price.tsx +69 -0
- package/src/products/product-tabs.tsx +169 -0
- package/src/products/product-template.tsx +114 -0
- package/src/products/purchase-options.tsx +130 -0
- package/src/products/related-products.tsx +86 -0
- package/src/products/thumbnail.tsx +71 -0
- package/src/products/variant-matching.ts +71 -0
- package/src/reviews-ui/helpers.ts +174 -0
- package/src/reviews-ui/index.ts +74 -0
- package/src/reviews-ui/labels-bg.ts +91 -0
- package/src/reviews-ui/labels.ts +199 -0
- package/src/reviews-ui/photo-upload.tsx +345 -0
- package/src/reviews-ui/review-list.tsx +249 -0
- package/src/reviews-ui/review-widget.tsx +224 -0
- package/src/reviews-ui/review-wizard.tsx +560 -0
- package/src/reviews-ui/star-badge.tsx +104 -0
- package/src/reviews-ui/wizard-state.ts +81 -0
- package/src/store/category-template.tsx +129 -0
- package/src/store/collection-template.tsx +139 -0
- package/src/store/index.ts +41 -0
- package/src/store/labels-bg.ts +22 -0
- package/src/store/labels.ts +52 -0
- package/src/store/paginated-products.tsx +116 -0
- package/src/store/pagination.tsx +103 -0
- package/src/store/search-params.ts +256 -0
- package/src/store/search-template.tsx +249 -0
- package/src/store/skeleton-product-grid.tsx +26 -0
- package/src/store/sort-select.tsx +81 -0
- package/src/store/store-template.tsx +65 -0
- package/src/tracking/attribution.ts +418 -0
- package/src/tracking/consent-banner.tsx +355 -0
- package/src/tracking/consent-init.tsx +44 -0
- package/src/tracking/consent.ts +243 -0
- package/src/tracking/fbq.ts +168 -0
- package/src/tracking/ga4.tsx +49 -0
- package/src/tracking/get-tracking-attribution.ts +224 -0
- package/src/tracking/get-tracking-config.ts +50 -0
- package/src/tracking/gtag.ts +200 -0
- package/src/tracking/index.ts +133 -0
- package/src/tracking/meta-pixel.tsx +166 -0
- package/src/tracking/rybbit-events.ts +242 -0
- package/src/tracking/rybbit.tsx +40 -0
- package/src/tracking/types.ts +185 -0
- package/src/tracking/use-engagement-time.ts +58 -0
- package/tailwind-preset.cjs +72 -0
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import Image from "next/image"
|
|
4
|
+
import Link from "next/link"
|
|
5
|
+
import { useRef, useState } from "react"
|
|
6
|
+
|
|
7
|
+
import { DualPrice } from "../lib/dual-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
|
+
* Barter 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
|
+
<DualPrice
|
|
182
|
+
amount={product.price}
|
|
183
|
+
currencyCode={product.currencyCode}
|
|
184
|
+
className="text-[13px] font-bold text-foreground block mt-0.5"
|
|
185
|
+
bgnClassName="text-muted-foreground text-[10px] ml-1"
|
|
186
|
+
/>
|
|
187
|
+
</div>
|
|
188
|
+
|
|
189
|
+
<button
|
|
190
|
+
type="button"
|
|
191
|
+
onClick={() => onAdd?.(product.id, product.variantId)}
|
|
192
|
+
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"
|
|
193
|
+
aria-label={`${labels.addToCart} ${product.title}`}
|
|
194
|
+
>
|
|
195
|
+
<svg
|
|
196
|
+
width="14"
|
|
197
|
+
height="14"
|
|
198
|
+
viewBox="0 0 12 12"
|
|
199
|
+
fill="none"
|
|
200
|
+
stroke="currentColor"
|
|
201
|
+
strokeWidth="1.5"
|
|
202
|
+
strokeLinecap="round"
|
|
203
|
+
>
|
|
204
|
+
<path d="M6 2.5v7M2.5 6h7" />
|
|
205
|
+
</svg>
|
|
206
|
+
</button>
|
|
207
|
+
</div>
|
|
208
|
+
))}
|
|
209
|
+
</div>
|
|
210
|
+
</div>
|
|
211
|
+
)
|
|
212
|
+
}
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import Image from "next/image"
|
|
4
|
+
import Link from "next/link"
|
|
5
|
+
|
|
6
|
+
import type { StoreProduct } from "../api/products"
|
|
7
|
+
import { convertToLocale } from "../lib/money"
|
|
8
|
+
import { useCartDrawer } from "./context"
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* CartCrossSellSidebar — vertical product-card list for desktop cart
|
|
12
|
+
* drawer's left sidebar.
|
|
13
|
+
*
|
|
14
|
+
* Ported from `@1click/ui/src/cart-drawer/cross-sell-sidebar.tsx` (v2.3.1).
|
|
15
|
+
* Barter data seam: source stores fed this from Medusa product fetches —
|
|
16
|
+
* feed it from `@cartbase/storefront/api/products` `listProducts` (curated
|
|
17
|
+
* collection/tag) or `@cartbase/storefront/api/search` `listRelatedProducts`
|
|
18
|
+
* (anchor-product complements), mapped through `toCrossSellProduct` below.
|
|
19
|
+
* Wire `onAdd` to the provider's `addItem(variantId)`. Prices are EUR
|
|
20
|
+
* major units and come server-computed via `calculated_price` — never
|
|
21
|
+
* recompute client-side.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
export type CrossSellSidebarProduct = {
|
|
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
|
+
/**
|
|
35
|
+
* Map a store-API product (products/search/related responses all reuse the
|
|
36
|
+
* canonical `StoreProduct` shape) to a cross-sell card. Picks the first
|
|
37
|
+
* variant carrying a server-computed `calculated_price` for the current
|
|
38
|
+
* pricing context; returns null when no variant is priced (pass
|
|
39
|
+
* `currency_code`/`region_id` on the listing call, and skip null results).
|
|
40
|
+
* Pure — exported for unit tests and for building `crossSell.loader`s.
|
|
41
|
+
*/
|
|
42
|
+
export function toCrossSellProduct(
|
|
43
|
+
product: StoreProduct
|
|
44
|
+
): CrossSellSidebarProduct | null {
|
|
45
|
+
const variant = (product.variants ?? []).find((v) => v.calculated_price)
|
|
46
|
+
const price = variant?.calculated_price
|
|
47
|
+
if (!variant || !price) return null
|
|
48
|
+
return {
|
|
49
|
+
id: product.id,
|
|
50
|
+
title: product.title,
|
|
51
|
+
handle: product.handle,
|
|
52
|
+
thumbnail: product.thumbnail,
|
|
53
|
+
price: price.calculated_amount,
|
|
54
|
+
currencyCode: price.currency_code,
|
|
55
|
+
variantId: variant.id,
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
type CartCrossSellSidebarProps = {
|
|
60
|
+
label?: string
|
|
61
|
+
products: CrossSellSidebarProduct[]
|
|
62
|
+
onAdd?: (productId: string, variantId?: string) => void
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function CartCrossSellSidebar({
|
|
66
|
+
label,
|
|
67
|
+
products,
|
|
68
|
+
onAdd,
|
|
69
|
+
}: CartCrossSellSidebarProps) {
|
|
70
|
+
const { labels, hrefs } = useCartDrawer()
|
|
71
|
+
|
|
72
|
+
if (!products.length) return null
|
|
73
|
+
|
|
74
|
+
return (
|
|
75
|
+
<div className="h-full flex flex-col">
|
|
76
|
+
<div className="px-5 pt-6 pb-4">
|
|
77
|
+
<h3 className="text-xs font-bold text-foreground uppercase tracking-widest">
|
|
78
|
+
{label ?? labels.youMightAlsoLike}
|
|
79
|
+
</h3>
|
|
80
|
+
</div>
|
|
81
|
+
<div className="flex-1 overflow-y-auto no-scrollbar px-4 pb-6 space-y-3">
|
|
82
|
+
{products.map((product) => (
|
|
83
|
+
<div
|
|
84
|
+
key={product.id}
|
|
85
|
+
className="bg-card rounded-[2px] p-3.5 transition-shadow hover:shadow-md"
|
|
86
|
+
style={{
|
|
87
|
+
boxShadow: "0 1px 4px rgba(0,0,0,0.06), 0 0 1px rgba(0,0,0,0.08)",
|
|
88
|
+
}}
|
|
89
|
+
>
|
|
90
|
+
<Link href={`${hrefs.productPrefix}/${product.handle}`}>
|
|
91
|
+
<div className="w-full aspect-square rounded-xl overflow-hidden bg-muted relative mb-3">
|
|
92
|
+
{product.thumbnail ? (
|
|
93
|
+
<Image
|
|
94
|
+
src={product.thumbnail}
|
|
95
|
+
alt={product.title}
|
|
96
|
+
fill
|
|
97
|
+
className="object-cover"
|
|
98
|
+
sizes="200px"
|
|
99
|
+
/>
|
|
100
|
+
) : (
|
|
101
|
+
<div className="w-full h-full flex items-center justify-center">
|
|
102
|
+
<svg
|
|
103
|
+
width="32"
|
|
104
|
+
height="32"
|
|
105
|
+
viewBox="0 0 24 24"
|
|
106
|
+
fill="none"
|
|
107
|
+
stroke="currentColor"
|
|
108
|
+
className="text-muted-foreground"
|
|
109
|
+
strokeWidth="1.2"
|
|
110
|
+
>
|
|
111
|
+
<rect x="3" y="3" width="18" height="18" rx="3" />
|
|
112
|
+
<circle cx="8.5" cy="8.5" r="1.5" />
|
|
113
|
+
<path
|
|
114
|
+
d="M21 15l-5-5L5 21"
|
|
115
|
+
strokeLinecap="round"
|
|
116
|
+
strokeLinejoin="round"
|
|
117
|
+
/>
|
|
118
|
+
</svg>
|
|
119
|
+
</div>
|
|
120
|
+
)}
|
|
121
|
+
</div>
|
|
122
|
+
</Link>
|
|
123
|
+
<Link
|
|
124
|
+
href={`${hrefs.productPrefix}/${product.handle}`}
|
|
125
|
+
className="text-[13px] font-semibold text-foreground leading-tight line-clamp-2 hover:text-muted-foreground transition-colors block"
|
|
126
|
+
>
|
|
127
|
+
{product.title}
|
|
128
|
+
</Link>
|
|
129
|
+
<p className="text-sm font-semibold text-foreground mt-1.5">
|
|
130
|
+
{convertToLocale({
|
|
131
|
+
amount: product.price,
|
|
132
|
+
currency_code: product.currencyCode,
|
|
133
|
+
})}
|
|
134
|
+
</p>
|
|
135
|
+
<button
|
|
136
|
+
type="button"
|
|
137
|
+
onClick={() => onAdd?.(product.id, product.variantId)}
|
|
138
|
+
className="mt-3 w-full h-10 text-xs font-semibold text-foreground bg-card border border-border rounded-xl hover:bg-foreground hover:text-card hover:border-text-base transition-all flex items-center justify-center gap-1.5 active:scale-[0.97]"
|
|
139
|
+
>
|
|
140
|
+
{labels.addToCart}
|
|
141
|
+
<svg
|
|
142
|
+
width="12"
|
|
143
|
+
height="12"
|
|
144
|
+
viewBox="0 0 12 12"
|
|
145
|
+
fill="none"
|
|
146
|
+
stroke="currentColor"
|
|
147
|
+
strokeWidth="1.5"
|
|
148
|
+
strokeLinecap="round"
|
|
149
|
+
>
|
|
150
|
+
<path d="M2.5 6h7M7 3.5L9.5 6 7 8.5" />
|
|
151
|
+
</svg>
|
|
152
|
+
</button>
|
|
153
|
+
</div>
|
|
154
|
+
))}
|
|
155
|
+
</div>
|
|
156
|
+
</div>
|
|
157
|
+
)
|
|
158
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import Link from "next/link"
|
|
4
|
+
|
|
5
|
+
import { useCartDrawer } from "./context"
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* CartEmpty — empty-state for the cart drawer.
|
|
9
|
+
*
|
|
10
|
+
* Ported from `@1click/ui/src/cart-drawer/empty.tsx` (v2.3.1);
|
|
11
|
+
* originally extracted from mindpages-storefront
|
|
12
|
+
* src/modules/cart-drawer/cart-empty.tsx.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
export function CartEmpty() {
|
|
16
|
+
const { close, labels, hrefs } = useCartDrawer()
|
|
17
|
+
|
|
18
|
+
return (
|
|
19
|
+
<div className="flex-1 flex flex-col items-center justify-center px-8 py-16">
|
|
20
|
+
<div className="w-16 h-16 rounded-full bg-muted flex items-center justify-center mb-5">
|
|
21
|
+
<svg
|
|
22
|
+
width="28"
|
|
23
|
+
height="28"
|
|
24
|
+
viewBox="0 0 28 28"
|
|
25
|
+
fill="none"
|
|
26
|
+
stroke="currentColor"
|
|
27
|
+
className="text-muted-foreground"
|
|
28
|
+
strokeWidth="1.5"
|
|
29
|
+
strokeLinecap="round"
|
|
30
|
+
strokeLinejoin="round"
|
|
31
|
+
>
|
|
32
|
+
<path d="M6 8h16l-1.5 14H7.5L6 8z" />
|
|
33
|
+
<path d="M10 8V6a4 4 0 018 0v2" />
|
|
34
|
+
</svg>
|
|
35
|
+
</div>
|
|
36
|
+
<h3 className="text-lg font-semibold text-foreground mb-1.5">
|
|
37
|
+
{labels.emptyCartTitle}
|
|
38
|
+
</h3>
|
|
39
|
+
<p className="text-sm text-muted-foreground text-center mb-6 max-w-[240px]">
|
|
40
|
+
{labels.emptyCartMessage}
|
|
41
|
+
</p>
|
|
42
|
+
<Link href={hrefs.browse} onClick={close}>
|
|
43
|
+
<button
|
|
44
|
+
type="button"
|
|
45
|
+
className="px-6 py-2.5 bg-foreground text-card text-sm font-medium rounded-xl hover:bg-foreground/90 transition-colors active:scale-[0.97]"
|
|
46
|
+
>
|
|
47
|
+
{labels.browseProducts}
|
|
48
|
+
</button>
|
|
49
|
+
</Link>
|
|
50
|
+
</div>
|
|
51
|
+
)
|
|
52
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import Image from "next/image"
|
|
4
|
+
|
|
5
|
+
import { convertToLocale } from "../lib/money"
|
|
6
|
+
import { useCartDrawer } from "./context"
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* CartFreeGift — a "free gift" promo card shown inside the cart drawer
|
|
10
|
+
* when the cart total exceeds a qualifying threshold.
|
|
11
|
+
*
|
|
12
|
+
* Ported from `@1click/ui/src/cart-drawer/free-gift.tsx` (v2.3.1);
|
|
13
|
+
* originally extracted from mindpages-storefront
|
|
14
|
+
* src/modules/cart-drawer/cart-free-gift.tsx. `originalPrice` is EUR
|
|
15
|
+
* major units.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
type CartFreeGiftProps = {
|
|
19
|
+
title: string
|
|
20
|
+
description?: string
|
|
21
|
+
originalPrice: number
|
|
22
|
+
currencyCode: string
|
|
23
|
+
thumbnail?: string | null
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function CartFreeGift({
|
|
27
|
+
title,
|
|
28
|
+
description,
|
|
29
|
+
originalPrice,
|
|
30
|
+
currencyCode,
|
|
31
|
+
thumbnail,
|
|
32
|
+
}: CartFreeGiftProps) {
|
|
33
|
+
const { labels } = useCartDrawer()
|
|
34
|
+
|
|
35
|
+
return (
|
|
36
|
+
<div className="mx-5 my-3 p-4 rounded-xl border border-success/20 bg-success/5">
|
|
37
|
+
<div className="flex items-start gap-3">
|
|
38
|
+
{thumbnail && (
|
|
39
|
+
<div className="w-12 h-12 rounded-lg overflow-hidden bg-card flex-shrink-0 relative">
|
|
40
|
+
<Image
|
|
41
|
+
src={thumbnail}
|
|
42
|
+
alt={title}
|
|
43
|
+
fill
|
|
44
|
+
className="object-cover"
|
|
45
|
+
sizes="48px"
|
|
46
|
+
/>
|
|
47
|
+
</div>
|
|
48
|
+
)}
|
|
49
|
+
<div className="flex-1 min-w-0">
|
|
50
|
+
<div className="flex items-center gap-2">
|
|
51
|
+
<span className="text-sm font-semibold text-foreground">{title}</span>
|
|
52
|
+
<span className="text-xs text-muted-foreground line-through">
|
|
53
|
+
{convertToLocale({
|
|
54
|
+
amount: originalPrice,
|
|
55
|
+
currency_code: currencyCode,
|
|
56
|
+
})}
|
|
57
|
+
</span>
|
|
58
|
+
<span className="text-xs font-bold text-success bg-success/10 px-1.5 py-0.5 rounded">
|
|
59
|
+
{labels.free}
|
|
60
|
+
</span>
|
|
61
|
+
</div>
|
|
62
|
+
{description && (
|
|
63
|
+
<p className="text-xs text-muted-foreground mt-1 leading-relaxed">
|
|
64
|
+
{description}
|
|
65
|
+
</p>
|
|
66
|
+
)}
|
|
67
|
+
</div>
|
|
68
|
+
</div>
|
|
69
|
+
</div>
|
|
70
|
+
)
|
|
71
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import { useState } from "react"
|
|
4
|
+
|
|
5
|
+
import { DualPrice } from "../lib/dual-price"
|
|
6
|
+
import { cn } from "../lib/utils"
|
|
7
|
+
import { useCartDrawer } from "./context"
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* CartGiftWrap — inline toggle row for adding gift wrapping to the cart.
|
|
11
|
+
*
|
|
12
|
+
* Ported from `@1click/ui/src/cart-drawer/gift-wrap.tsx` (v2.3.1);
|
|
13
|
+
* originally extracted from mindpages-storefront
|
|
14
|
+
* src/modules/cart-drawer/cart-gift-wrap.tsx. `price` is EUR major units.
|
|
15
|
+
* The store owns the effect: wire `onToggle` to add/remove the store's
|
|
16
|
+
* gift-wrap variant via the provider's `addItem` / `removeItem`.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
type CartGiftWrapProps = {
|
|
20
|
+
price: number
|
|
21
|
+
currencyCode: string
|
|
22
|
+
enabled?: boolean
|
|
23
|
+
onToggle?: (enabled: boolean) => void
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function CartGiftWrap({
|
|
27
|
+
price,
|
|
28
|
+
currencyCode,
|
|
29
|
+
enabled = false,
|
|
30
|
+
onToggle,
|
|
31
|
+
}: CartGiftWrapProps) {
|
|
32
|
+
const { labels } = useCartDrawer()
|
|
33
|
+
const [checked, setChecked] = useState(enabled)
|
|
34
|
+
|
|
35
|
+
const handleToggle = () => {
|
|
36
|
+
const next = !checked
|
|
37
|
+
setChecked(next)
|
|
38
|
+
onToggle?.(next)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return (
|
|
42
|
+
<div className="px-5 sm:px-6 py-2">
|
|
43
|
+
<button
|
|
44
|
+
type="button"
|
|
45
|
+
onClick={handleToggle}
|
|
46
|
+
className="flex items-center gap-3 w-full text-left py-2 min-h-[44px] group"
|
|
47
|
+
>
|
|
48
|
+
<div
|
|
49
|
+
className={cn(
|
|
50
|
+
"w-5 h-5 rounded border-[1.5px] flex items-center justify-center transition-all flex-shrink-0",
|
|
51
|
+
checked
|
|
52
|
+
? "bg-foreground border-text-base"
|
|
53
|
+
: "border-border group-hover:border-text-muted group-active:border-text-muted"
|
|
54
|
+
)}
|
|
55
|
+
>
|
|
56
|
+
{checked && (
|
|
57
|
+
<svg
|
|
58
|
+
width="10"
|
|
59
|
+
height="10"
|
|
60
|
+
viewBox="0 0 10 10"
|
|
61
|
+
fill="none"
|
|
62
|
+
stroke="white"
|
|
63
|
+
strokeWidth="2"
|
|
64
|
+
strokeLinecap="round"
|
|
65
|
+
strokeLinejoin="round"
|
|
66
|
+
>
|
|
67
|
+
<path d="M2 5l2 2 4-4" />
|
|
68
|
+
</svg>
|
|
69
|
+
)}
|
|
70
|
+
</div>
|
|
71
|
+
<span className="text-[13px] text-foreground flex-1">
|
|
72
|
+
{labels.addGiftWrap}
|
|
73
|
+
</span>
|
|
74
|
+
<DualPrice
|
|
75
|
+
amount={price}
|
|
76
|
+
currencyCode={currencyCode}
|
|
77
|
+
className="text-[13px] text-muted-foreground font-medium"
|
|
78
|
+
bgnClassName="text-muted-foreground text-[10px] ml-1"
|
|
79
|
+
/>
|
|
80
|
+
</button>
|
|
81
|
+
</div>
|
|
82
|
+
)
|
|
83
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import { productItemCount } from "../lib/cart-helpers"
|
|
4
|
+
import { useCartDrawer } from "./context"
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Cart drawer header — store title, item count badge, close button.
|
|
8
|
+
*
|
|
9
|
+
* Ported from `@1click/ui/src/cart-drawer/header.tsx` (v2.3.1). Barter
|
|
10
|
+
* adjustment: the badge counts PRODUCT lines via the shared
|
|
11
|
+
* `productItemCount` predicate (the source summed all lines) so a
|
|
12
|
+
* backend-injected COD-fee line can never inflate the item count —
|
|
13
|
+
* consistent with the provider's auto-open counter.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
export function CartDrawerHeader() {
|
|
17
|
+
const { close, cart, labels } = useCartDrawer()
|
|
18
|
+
const totalItems = productItemCount(cart?.items)
|
|
19
|
+
|
|
20
|
+
return (
|
|
21
|
+
<div className="flex items-center justify-between px-5 sm:px-6 py-4 sm:py-5 flex-shrink-0 bg-card">
|
|
22
|
+
<div className="flex items-center gap-2.5">
|
|
23
|
+
<h2 className="text-base sm:text-[17px] font-bold text-foreground tracking-tight">
|
|
24
|
+
{labels.yourCart}
|
|
25
|
+
</h2>
|
|
26
|
+
{totalItems > 0 && (
|
|
27
|
+
<span className="text-[11px] font-semibold text-muted-foreground bg-muted rounded-full px-2.5 py-1 leading-none">
|
|
28
|
+
{totalItems} {totalItems === 1 ? labels.item : labels.items}
|
|
29
|
+
</span>
|
|
30
|
+
)}
|
|
31
|
+
</div>
|
|
32
|
+
<button
|
|
33
|
+
onClick={close}
|
|
34
|
+
className="w-11 h-11 -mr-1.5 flex items-center justify-center rounded-full hover:bg-muted active:bg-muted transition-colors text-muted-foreground hover:text-foreground"
|
|
35
|
+
aria-label={labels.closeCart}
|
|
36
|
+
>
|
|
37
|
+
<svg
|
|
38
|
+
width="18"
|
|
39
|
+
height="18"
|
|
40
|
+
viewBox="0 0 16 16"
|
|
41
|
+
fill="none"
|
|
42
|
+
stroke="currentColor"
|
|
43
|
+
strokeWidth="2"
|
|
44
|
+
strokeLinecap="round"
|
|
45
|
+
aria-hidden="true"
|
|
46
|
+
>
|
|
47
|
+
<path d="M4 4l8 8M12 4l-8 8" />
|
|
48
|
+
</svg>
|
|
49
|
+
</button>
|
|
50
|
+
</div>
|
|
51
|
+
)
|
|
52
|
+
}
|