@instockng/storefront-ui 1.0.75 → 1.0.77
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/dist/components/ProductAddOns.d.ts +3 -1
- package/dist/components/ProductAddOns.d.ts.map +1 -1
- package/dist/components/RecommendedProducts.d.ts.map +1 -1
- package/dist/components/VariantPickerModal.d.ts +28 -0
- package/dist/components/VariantPickerModal.d.ts.map +1 -0
- package/dist/contexts/CartContext.d.ts.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.mjs +104 -102
- package/dist/index10.mjs +162 -121
- package/dist/index100.mjs +20 -69
- package/dist/index101.mjs +55 -34
- package/dist/index102.mjs +32 -42
- package/dist/index103.mjs +2 -2
- package/dist/index104.mjs +2 -28
- package/dist/index105.mjs +2 -18
- package/dist/index106.mjs +26 -213
- package/dist/index107.mjs +13 -175
- package/dist/index108.mjs +210 -17
- package/dist/index109.mjs +174 -17
- package/dist/index11.mjs +116 -88
- package/dist/index110.mjs +14 -25
- package/dist/index111.mjs +17 -150
- package/dist/index112.mjs +26 -13
- package/dist/index113.mjs +149 -24
- package/dist/index114.mjs +13 -77
- package/dist/index115.mjs +20 -27
- package/dist/index116.mjs +76 -137
- package/dist/index117.mjs +33 -50
- package/dist/index118.mjs +141 -19
- package/dist/index119.mjs +50 -22
- package/dist/index12.mjs +86 -90
- package/dist/index120.mjs +14 -14
- package/dist/index121.mjs +20 -18
- package/dist/index122.mjs +16 -14
- package/dist/index123.mjs +17 -14
- package/dist/index124.mjs +12 -12
- package/dist/index125.mjs +15 -58
- package/dist/index126.mjs +14 -11
- package/dist/index127.mjs +56 -30
- package/dist/index128.mjs +9 -15
- package/dist/index129.mjs +31 -26
- package/dist/index13.mjs +83 -185
- package/dist/index130.mjs +13 -16
- package/dist/index131.mjs +26 -11
- package/dist/index132.mjs +18 -12
- package/dist/index133.mjs +12 -40
- package/dist/index134.mjs +14 -16
- package/dist/index135.mjs +42 -263
- package/dist/index136.mjs +15 -62
- package/dist/index137.mjs +267 -7
- package/dist/index138.mjs +70 -2
- package/dist/index139.mjs +8 -2
- package/dist/index14.mjs +196 -94
- package/dist/index140.mjs +2 -33
- package/dist/index141.mjs +2 -2
- package/dist/index142.mjs +33 -2
- package/dist/index143.mjs +2 -21
- package/dist/index144.mjs +19 -54
- package/dist/index145.mjs +55 -28
- package/dist/index146.mjs +27 -5
- package/dist/index147.mjs +6 -51
- package/dist/index148.mjs +51 -5
- package/dist/index149.mjs +6 -12
- package/dist/index15.mjs +82 -713
- package/dist/index150.mjs +12 -7
- package/dist/index151.mjs +6 -27
- package/dist/index152.mjs +28 -2
- package/dist/index153.mjs +2 -70
- package/dist/index154.mjs +67 -164
- package/dist/index155.mjs +167 -2
- package/dist/index159.mjs +1 -1
- package/dist/index16.mjs +722 -53
- package/dist/index160.mjs +1 -1
- package/dist/index161.mjs +1 -1
- package/dist/index162.mjs +1 -1
- package/dist/index163.mjs +1 -1
- package/dist/index165.mjs +3 -3
- package/dist/index166.mjs +1 -1
- package/dist/index168.mjs +3 -3
- package/dist/index169.mjs +1 -1
- package/dist/index17.mjs +58 -60
- package/dist/index174.mjs +2 -2
- package/dist/index176.mjs +1 -1
- package/dist/index178.mjs +2 -2
- package/dist/index179.mjs +5 -5
- package/dist/index18.mjs +60 -22
- package/dist/index182.mjs +1 -1
- package/dist/index185.mjs +1 -1
- package/dist/index188.mjs +3 -3
- package/dist/index189.mjs +1 -1
- package/dist/index19.mjs +22 -107
- package/dist/index193.mjs +2 -2
- package/dist/index195.mjs +2 -2
- package/dist/index199.mjs +1 -1
- package/dist/index2.mjs +2 -2
- package/dist/index20.mjs +105 -38
- package/dist/index201.mjs +1 -1
- package/dist/index21.mjs +37 -37
- package/dist/index210.mjs +2 -2
- package/dist/index212.mjs +1 -1
- package/dist/index213.mjs +1 -1
- package/dist/index214.mjs +2 -31
- package/dist/index215.mjs +30 -10
- package/dist/index216.mjs +10 -3
- package/dist/index217.mjs +3 -3
- package/dist/index218.mjs +4 -13
- package/dist/index219.mjs +13 -7
- package/dist/index22.mjs +42 -71
- package/dist/index220.mjs +7 -12
- package/dist/index221.mjs +11 -4
- package/dist/index222.mjs +5 -33
- package/dist/index223.mjs +33 -31
- package/dist/index224.mjs +29 -26
- package/dist/index225.mjs +26 -59
- package/dist/index226.mjs +61 -2
- package/dist/index227.mjs +2 -108
- package/dist/index228.mjs +2 -2
- package/dist/index229.mjs +2 -2
- package/dist/index23.mjs +67 -28
- package/dist/index230.mjs +2 -37
- package/dist/index231.mjs +108 -2
- package/dist/index232.mjs +2 -2
- package/dist/index233.mjs +2 -2
- package/dist/index234.mjs +2 -2
- package/dist/index236.mjs +2 -2
- package/dist/index237.mjs +2 -244
- package/dist/index238.mjs +37 -2
- package/dist/index239.mjs +2 -33
- package/dist/index24.mjs +29 -6
- package/dist/index240.mjs +2 -65
- package/dist/index241.mjs +243 -24
- package/dist/index242.mjs +2 -2
- package/dist/index243.mjs +33 -2
- package/dist/index244.mjs +65 -2
- package/dist/index245.mjs +25 -2
- package/dist/index246.mjs +2 -2
- package/dist/index247.mjs +2 -2
- package/dist/index248.mjs +2 -2
- package/dist/index249.mjs +2 -2
- package/dist/index25.mjs +8 -21
- package/dist/index250.mjs +2 -2
- package/dist/index251.mjs +2 -2
- package/dist/index253.mjs +2 -2
- package/dist/index254.mjs +2 -2
- package/dist/index255.mjs +4 -2
- package/dist/index256.mjs +2 -4
- package/dist/index258.mjs +3 -2
- package/dist/index259.mjs +2 -18
- package/dist/index26.mjs +19 -34
- package/dist/index260.mjs +2 -47
- package/dist/index261.mjs +17 -2
- package/dist/index262.mjs +13 -2
- package/dist/index263.mjs +6 -2
- package/dist/index264.mjs +30 -2
- package/dist/index265.mjs +2 -91
- package/dist/index266.mjs +2 -3
- package/dist/index267.mjs +18 -2
- package/dist/index268.mjs +47 -2
- package/dist/index269.mjs +2 -17
- package/dist/index27.mjs +34 -37
- package/dist/index270.mjs +2 -13
- package/dist/index271.mjs +2 -6
- package/dist/index272.mjs +2 -30
- package/dist/index273.mjs +91 -2
- package/dist/index274.mjs +2 -2
- package/dist/index276.mjs +2 -2
- package/dist/index277.mjs +5 -0
- package/dist/index28.mjs +28 -103
- package/dist/index29.mjs +104 -20
- package/dist/index3.mjs +90 -90
- package/dist/index30.mjs +29 -9
- package/dist/index31.mjs +9 -9
- package/dist/index32.mjs +11 -116
- package/dist/index33.mjs +116 -25
- package/dist/index34.mjs +25 -81
- package/dist/index35.mjs +81 -112
- package/dist/index36.mjs +109 -8
- package/dist/index37.mjs +9 -33
- package/dist/index38.mjs +27 -20
- package/dist/index39.mjs +26 -9
- package/dist/index4.mjs +1 -1
- package/dist/index40.mjs +10 -122
- package/dist/index41.mjs +114 -380
- package/dist/index42.mjs +384 -20
- package/dist/index43.mjs +24 -31
- package/dist/index44.mjs +32 -7
- package/dist/index45.mjs +6 -1432
- package/dist/index46.mjs +1432 -69
- package/dist/index47.mjs +70 -2
- package/dist/index48.mjs +2 -60
- package/dist/index49.mjs +57 -48
- package/dist/index5.mjs +1 -1
- package/dist/index50.mjs +51 -33
- package/dist/index51.mjs +33 -15
- package/dist/index52.mjs +12 -2260
- package/dist/index53.mjs +2263 -36
- package/dist/index54.mjs +36 -44
- package/dist/index55.mjs +44 -99
- package/dist/index56.mjs +99 -81
- package/dist/index57.mjs +75 -13
- package/dist/index58.mjs +15 -125
- package/dist/index59.mjs +93 -89
- package/dist/index6.mjs +1 -1
- package/dist/index60.mjs +102 -56
- package/dist/index61.mjs +55 -89
- package/dist/index62.mjs +82 -76
- package/dist/index63.mjs +100 -13
- package/dist/index64.mjs +17 -92
- package/dist/index65.mjs +70 -56
- package/dist/index66.mjs +62 -44
- package/dist/index67.mjs +47 -46
- package/dist/index68.mjs +48 -121
- package/dist/index69.mjs +133 -22
- package/dist/index7.mjs +6 -6
- package/dist/index70.mjs +4 -152
- package/dist/index71.mjs +21 -20
- package/dist/index72.mjs +2 -5
- package/dist/index73.mjs +23 -2
- package/dist/index74.mjs +149 -71
- package/dist/index75.mjs +74 -14
- package/dist/index76.mjs +14 -62
- package/dist/index77.mjs +63 -2
- package/dist/index78.mjs +234 -5
- package/dist/index79.mjs +5 -1133
- package/dist/index8.mjs +4 -4
- package/dist/index80.mjs +131 -17
- package/dist/index81.mjs +67 -54
- package/dist/index82.mjs +84 -30
- package/dist/index83.mjs +29 -2
- package/dist/index84.mjs +9 -235
- package/dist/index85.mjs +74 -5
- package/dist/index86.mjs +3 -133
- package/dist/index87.mjs +2 -68
- package/dist/index88.mjs +79 -83
- package/dist/index89.mjs +52 -27
- package/dist/index9.mjs +3 -3
- package/dist/index90.mjs +5 -8
- package/dist/index91.mjs +4 -74
- package/dist/index92.mjs +178 -3
- package/dist/index93.mjs +53 -2
- package/dist/index94.mjs +68 -82
- package/dist/index95.mjs +33 -53
- package/dist/index96.mjs +42 -5
- package/dist/index97.mjs +2 -5
- package/dist/index98.mjs +5 -178
- package/dist/index99.mjs +1134 -53
- package/dist/styles.css +1 -1
- package/package.json +1 -1
- package/src/components/ProductAddOns.stories.tsx +10 -3
- package/src/components/ProductAddOns.tsx +167 -112
- package/src/components/RecommendedProducts.tsx +130 -88
- package/src/components/VariantPickerModal.tsx +154 -0
- package/src/contexts/CartContext.tsx +8 -1
- package/src/index.ts +3 -0
|
@@ -14,6 +14,7 @@ import { formatCurrency, cn } from '../lib/utils';
|
|
|
14
14
|
import { Button } from './ui/button';
|
|
15
15
|
import { ShoppingCart, Loader2, Package } from 'lucide-react';
|
|
16
16
|
import { useState, useEffect } from 'react';
|
|
17
|
+
import { VariantPickerModal, type VariantPickerProduct, type Variant } from './VariantPickerModal';
|
|
17
18
|
|
|
18
19
|
export interface RecommendedProductsProps {
|
|
19
20
|
/** Number of recommendations to fetch */
|
|
@@ -32,6 +33,8 @@ export function RecommendedProducts({
|
|
|
32
33
|
const [fadingProductIds, setFadingProductIds] = useState<string[]>([]);
|
|
33
34
|
const [collapsingProductIds, setCollapsingProductIds] = useState<string[]>([]);
|
|
34
35
|
const [hiddenProductIds, setHiddenProductIds] = useState<string[]>([]);
|
|
36
|
+
const [variantPickerProduct, setVariantPickerProduct] = useState<NonNullable<typeof recommendations>[number] | null>(null);
|
|
37
|
+
const [isAddingFromModal, setIsAddingFromModal] = useState(false);
|
|
35
38
|
|
|
36
39
|
// Reset hidden products when recommendations change (e.g., when cart items are removed)
|
|
37
40
|
useEffect(() => {
|
|
@@ -87,100 +90,139 @@ export function RecommendedProducts({
|
|
|
87
90
|
}
|
|
88
91
|
};
|
|
89
92
|
|
|
93
|
+
const handleAddClick = (product: typeof visibleRecommendations[0]) => {
|
|
94
|
+
// For multi-variant products, open modal
|
|
95
|
+
if (product.variants.length > 1) {
|
|
96
|
+
setVariantPickerProduct(product);
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
// For single-variant, add directly
|
|
100
|
+
const firstVariant = product.variants[0];
|
|
101
|
+
if (firstVariant) {
|
|
102
|
+
handleAddToCart(product, firstVariant);
|
|
103
|
+
}
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
const handleVariantConfirm = async (product: VariantPickerProduct, variant: Variant) => {
|
|
107
|
+
setIsAddingFromModal(true);
|
|
108
|
+
try {
|
|
109
|
+
const fullProduct = visibleRecommendations.find(p => p.id === product.id);
|
|
110
|
+
const fullVariant = fullProduct?.variants.find(v => v.id === variant.id);
|
|
111
|
+
if (fullProduct && fullVariant) {
|
|
112
|
+
await handleAddToCart(fullProduct, fullVariant);
|
|
113
|
+
}
|
|
114
|
+
setVariantPickerProduct(null);
|
|
115
|
+
} finally {
|
|
116
|
+
setIsAddingFromModal(false);
|
|
117
|
+
}
|
|
118
|
+
};
|
|
119
|
+
|
|
90
120
|
return (
|
|
91
|
-
|
|
92
|
-
{
|
|
93
|
-
|
|
94
|
-
<
|
|
95
|
-
|
|
121
|
+
<>
|
|
122
|
+
<div className={cn('pt-2', className)}>
|
|
123
|
+
{/* Heading */}
|
|
124
|
+
<div className="mb-2 px-4">
|
|
125
|
+
<h3 className="text-lg font-semibold text-gray-900">Frequently bought together</h3>
|
|
126
|
+
</div>
|
|
96
127
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
>
|
|
120
|
-
{/* Clickable Product Link */}
|
|
121
|
-
<a
|
|
122
|
-
href={`/product/${product.slug}`}
|
|
123
|
-
className="flex flex-row flex-1 hover:bg-gray-50 transition-colors"
|
|
128
|
+
{/* Horizontal Scrollable Product List */}
|
|
129
|
+
<div className="overflow-x-auto">
|
|
130
|
+
<div className="flex gap-4 pb-2 transition-all duration-500">
|
|
131
|
+
{/* Left spacer */}
|
|
132
|
+
<div className="flex-shrink-0 w-1" />
|
|
133
|
+
|
|
134
|
+
{visibleRecommendations?.map((product) => {
|
|
135
|
+
const firstVariant = product.variants[0];
|
|
136
|
+
if (!firstVariant) return null;
|
|
137
|
+
|
|
138
|
+
const isAdding = addingProductIds.includes(product.id);
|
|
139
|
+
const isFading = fadingProductIds.includes(product.id);
|
|
140
|
+
const isCollapsing = collapsingProductIds.includes(product.id);
|
|
141
|
+
|
|
142
|
+
return (
|
|
143
|
+
<div
|
|
144
|
+
key={product.id}
|
|
145
|
+
className={cn(
|
|
146
|
+
"flex flex-row flex-shrink-0 w-64 h-24 bg-white border border-gray-300 rounded-lg overflow-hidden transition-all duration-500",
|
|
147
|
+
isFading && "opacity-0 blur-sm scale-95 pointer-events-none",
|
|
148
|
+
isCollapsing && "w-0 h-0 mr-[-1rem] overflow-hidden"
|
|
149
|
+
)}
|
|
124
150
|
>
|
|
125
|
-
{/* Product
|
|
126
|
-
<
|
|
127
|
-
{product.
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
</div>
|
|
139
|
-
|
|
140
|
-
{/* Product Info */}
|
|
141
|
-
<div className="p-2 flex flex-col flex-1 justify-between">
|
|
142
|
-
<div>
|
|
143
|
-
<h4 className="text-xs font-medium text-gray-900 line-clamp-2 leading-tight">
|
|
144
|
-
{product.name}
|
|
145
|
-
</h4>
|
|
146
|
-
<p className="text-xs font-semibold text-accent-600 mt-0.5">
|
|
147
|
-
{formatCurrency(firstVariant.price)}
|
|
148
|
-
</p>
|
|
149
|
-
</div>
|
|
150
|
-
|
|
151
|
-
{/* Add to Cart Button */}
|
|
152
|
-
<Button
|
|
153
|
-
size="sm"
|
|
154
|
-
onClick={(e) => {
|
|
155
|
-
e.preventDefault();
|
|
156
|
-
e.stopPropagation();
|
|
157
|
-
handleAddToCart(product, firstVariant);
|
|
158
|
-
}}
|
|
159
|
-
disabled={isAdding}
|
|
160
|
-
className="w-full bg-accent-500 hover:bg-accent-600 text-white text-xs h-6 mt-1"
|
|
161
|
-
>
|
|
162
|
-
{isAdding ? (
|
|
163
|
-
<>
|
|
164
|
-
<Loader2 className="h-3 w-3 animate-spin" />
|
|
165
|
-
Adding...
|
|
166
|
-
</>
|
|
151
|
+
{/* Clickable Product Link */}
|
|
152
|
+
<a
|
|
153
|
+
href={`/product/${product.slug}`}
|
|
154
|
+
className="flex flex-row flex-1 hover:bg-gray-50 transition-colors"
|
|
155
|
+
>
|
|
156
|
+
{/* Product Image */}
|
|
157
|
+
<div className="relative w-20 h-full flex-shrink-0 bg-gray-100">
|
|
158
|
+
{product.thumbnailUrl ? (
|
|
159
|
+
<img
|
|
160
|
+
src={product.thumbnailUrl}
|
|
161
|
+
alt={product.name}
|
|
162
|
+
className="w-full h-full object-cover"
|
|
163
|
+
/>
|
|
167
164
|
) : (
|
|
168
|
-
|
|
169
|
-
<
|
|
170
|
-
|
|
171
|
-
</>
|
|
165
|
+
<div className="w-full h-full flex items-center justify-center">
|
|
166
|
+
<Package className="h-8 w-8 text-gray-300" />
|
|
167
|
+
</div>
|
|
172
168
|
)}
|
|
173
|
-
</
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
169
|
+
</div>
|
|
170
|
+
|
|
171
|
+
{/* Product Info */}
|
|
172
|
+
<div className="p-2 flex flex-col flex-1 justify-between">
|
|
173
|
+
<div>
|
|
174
|
+
<h4 className="text-xs font-medium text-gray-900 line-clamp-2 leading-tight">
|
|
175
|
+
{product.name}
|
|
176
|
+
</h4>
|
|
177
|
+
<p className="text-xs font-semibold text-accent-600 mt-0.5">
|
|
178
|
+
{formatCurrency(firstVariant.price)}
|
|
179
|
+
</p>
|
|
180
|
+
</div>
|
|
181
|
+
|
|
182
|
+
{/* Add to Cart Button */}
|
|
183
|
+
<Button
|
|
184
|
+
size="sm"
|
|
185
|
+
onClick={(e) => {
|
|
186
|
+
e.preventDefault();
|
|
187
|
+
e.stopPropagation();
|
|
188
|
+
handleAddClick(product);
|
|
189
|
+
}}
|
|
190
|
+
disabled={isAdding}
|
|
191
|
+
className="w-full bg-accent-500 hover:bg-accent-600 text-white text-xs h-6 mt-1"
|
|
192
|
+
>
|
|
193
|
+
{isAdding ? (
|
|
194
|
+
<>
|
|
195
|
+
<Loader2 className="h-3 w-3 animate-spin" />
|
|
196
|
+
Adding...
|
|
197
|
+
</>
|
|
198
|
+
) : (
|
|
199
|
+
<>
|
|
200
|
+
<ShoppingCart className="h-3 w-3" />
|
|
201
|
+
Add
|
|
202
|
+
</>
|
|
203
|
+
)}
|
|
204
|
+
</Button>
|
|
205
|
+
</div>
|
|
206
|
+
</a>
|
|
207
|
+
</div>
|
|
208
|
+
);
|
|
209
|
+
})}
|
|
210
|
+
|
|
211
|
+
{/* Right spacer */}
|
|
212
|
+
<div className="flex-shrink-0 w-1" />
|
|
213
|
+
</div>
|
|
182
214
|
</div>
|
|
183
215
|
</div>
|
|
184
|
-
|
|
216
|
+
|
|
217
|
+
{/* Variant Picker Modal */}
|
|
218
|
+
<VariantPickerModal
|
|
219
|
+
isOpen={!!variantPickerProduct}
|
|
220
|
+
onClose={() => setVariantPickerProduct(null)}
|
|
221
|
+
product={variantPickerProduct}
|
|
222
|
+
onConfirm={handleVariantConfirm}
|
|
223
|
+
isLoading={isAddingFromModal}
|
|
224
|
+
/>
|
|
225
|
+
</>
|
|
185
226
|
);
|
|
186
227
|
}
|
|
228
|
+
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* VariantPickerModal Component
|
|
5
|
+
*
|
|
6
|
+
* Modal for selecting a product variant before adding to cart.
|
|
7
|
+
* Used by ProductAddOns and RecommendedProducts for multi-variant products.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { useState } from 'react';
|
|
11
|
+
import { Modal } from './ui/modal';
|
|
12
|
+
import { Button } from './ui/button';
|
|
13
|
+
import { formatCurrency, cn } from '../lib/utils';
|
|
14
|
+
import { ShoppingCart, Loader2, Package } from 'lucide-react';
|
|
15
|
+
|
|
16
|
+
export interface Variant {
|
|
17
|
+
id: string;
|
|
18
|
+
name?: string | null;
|
|
19
|
+
price: number;
|
|
20
|
+
sku: string;
|
|
21
|
+
thumbnailUrl?: string | null;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface VariantPickerProduct {
|
|
25
|
+
id: string;
|
|
26
|
+
name: string;
|
|
27
|
+
slug: string;
|
|
28
|
+
thumbnailUrl?: string | null;
|
|
29
|
+
variants: Variant[];
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface VariantPickerModalProps {
|
|
33
|
+
/** Controls modal visibility */
|
|
34
|
+
isOpen: boolean;
|
|
35
|
+
/** Callback when modal should close */
|
|
36
|
+
onClose: () => void;
|
|
37
|
+
/** Product to select variant for */
|
|
38
|
+
product: VariantPickerProduct | null;
|
|
39
|
+
/** Callback when variant is selected and confirmed */
|
|
40
|
+
onConfirm: (product: VariantPickerProduct, variant: Variant) => void;
|
|
41
|
+
/** Whether add operation is in progress */
|
|
42
|
+
isLoading?: boolean;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function VariantPickerModal({
|
|
46
|
+
isOpen,
|
|
47
|
+
onClose,
|
|
48
|
+
product,
|
|
49
|
+
onConfirm,
|
|
50
|
+
isLoading = false,
|
|
51
|
+
}: VariantPickerModalProps) {
|
|
52
|
+
const [selectedVariantId, setSelectedVariantId] = useState<string | null>(null);
|
|
53
|
+
|
|
54
|
+
// Reset selection when product changes
|
|
55
|
+
const effectiveVariantId = selectedVariantId || product?.variants[0]?.id || null;
|
|
56
|
+
const selectedVariant = product?.variants.find(v => v.id === effectiveVariantId);
|
|
57
|
+
|
|
58
|
+
const handleConfirm = () => {
|
|
59
|
+
if (product && selectedVariant) {
|
|
60
|
+
onConfirm(product, selectedVariant);
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
if (!product) return null;
|
|
65
|
+
|
|
66
|
+
return (
|
|
67
|
+
<Modal
|
|
68
|
+
isOpen={isOpen}
|
|
69
|
+
onClose={onClose}
|
|
70
|
+
title="Select Option"
|
|
71
|
+
size="sm"
|
|
72
|
+
footer={
|
|
73
|
+
<Button
|
|
74
|
+
onClick={handleConfirm}
|
|
75
|
+
disabled={!selectedVariant || isLoading}
|
|
76
|
+
className="w-full bg-accent-500 hover:bg-accent-600 text-white"
|
|
77
|
+
size="lg"
|
|
78
|
+
>
|
|
79
|
+
{isLoading ? (
|
|
80
|
+
<>
|
|
81
|
+
<Loader2 className="h-4 w-4 animate-spin mr-2" />
|
|
82
|
+
Adding...
|
|
83
|
+
</>
|
|
84
|
+
) : (
|
|
85
|
+
<>
|
|
86
|
+
<ShoppingCart className="h-4 w-4 mr-2" />
|
|
87
|
+
Add to Cart - {selectedVariant && formatCurrency(selectedVariant.price)}
|
|
88
|
+
</>
|
|
89
|
+
)}
|
|
90
|
+
</Button>
|
|
91
|
+
}
|
|
92
|
+
>
|
|
93
|
+
<div className="space-y-4">
|
|
94
|
+
{/* Product Info */}
|
|
95
|
+
<div className="flex gap-4">
|
|
96
|
+
{/* Thumbnail - shows variant image if available, otherwise product image */}
|
|
97
|
+
<div className="w-20 h-20 flex-shrink-0 rounded-lg overflow-hidden bg-gray-100">
|
|
98
|
+
{(selectedVariant?.thumbnailUrl || product.thumbnailUrl) ? (
|
|
99
|
+
<img
|
|
100
|
+
src={selectedVariant?.thumbnailUrl || product.thumbnailUrl || ''}
|
|
101
|
+
alt={selectedVariant?.name || product.name}
|
|
102
|
+
className="w-full h-full object-cover"
|
|
103
|
+
/>
|
|
104
|
+
) : (
|
|
105
|
+
<div className="w-full h-full flex items-center justify-center">
|
|
106
|
+
<Package className="h-8 w-8 text-gray-300" />
|
|
107
|
+
</div>
|
|
108
|
+
)}
|
|
109
|
+
</div>
|
|
110
|
+
|
|
111
|
+
{/* Name */}
|
|
112
|
+
<div className="flex-1">
|
|
113
|
+
<h3 className="font-semibold text-gray-900">{product.name}</h3>
|
|
114
|
+
<p className="text-sm text-gray-500 mt-1">
|
|
115
|
+
{product.variants.length} options available
|
|
116
|
+
</p>
|
|
117
|
+
</div>
|
|
118
|
+
</div>
|
|
119
|
+
|
|
120
|
+
{/* Variant Buttons */}
|
|
121
|
+
<div className="space-y-2">
|
|
122
|
+
<p className="text-sm font-medium text-gray-700">Choose an option:</p>
|
|
123
|
+
<div className="flex flex-wrap gap-2">
|
|
124
|
+
{product.variants.map((variant) => {
|
|
125
|
+
const isSelected = variant.id === effectiveVariantId;
|
|
126
|
+
const variantImage = variant.thumbnailUrl || product.thumbnailUrl;
|
|
127
|
+
return (
|
|
128
|
+
<button
|
|
129
|
+
key={variant.id}
|
|
130
|
+
onClick={() => setSelectedVariantId(variant.id)}
|
|
131
|
+
className={cn(
|
|
132
|
+
"flex items-center gap-2 px-3 py-2 rounded-full text-sm font-medium border-2 transition-all",
|
|
133
|
+
isSelected
|
|
134
|
+
? "border-accent-500 bg-accent-50 text-accent-700"
|
|
135
|
+
: "border-gray-200 bg-white text-gray-700 hover:border-gray-300"
|
|
136
|
+
)}
|
|
137
|
+
>
|
|
138
|
+
{variantImage && (
|
|
139
|
+
<img
|
|
140
|
+
src={variantImage}
|
|
141
|
+
alt={variant.name || 'Variant'}
|
|
142
|
+
className="w-6 h-6 rounded-full object-cover flex-shrink-0"
|
|
143
|
+
/>
|
|
144
|
+
)}
|
|
145
|
+
{variant.name || 'Default'} - {formatCurrency(variant.price)}
|
|
146
|
+
</button>
|
|
147
|
+
);
|
|
148
|
+
})}
|
|
149
|
+
</div>
|
|
150
|
+
</div>
|
|
151
|
+
</div>
|
|
152
|
+
</Modal>
|
|
153
|
+
);
|
|
154
|
+
}
|
|
@@ -264,6 +264,7 @@ export function CartProvider({ children, brandSlug, initialCartId, shoppingCartP
|
|
|
264
264
|
|
|
265
265
|
// Add item to cart via API
|
|
266
266
|
// Refresh recommendations only if cart is not visible
|
|
267
|
+
const shouldRefreshRecommendations = !isOpen;
|
|
267
268
|
const updatedCart = await addItemMutation.mutateAsync({
|
|
268
269
|
sku,
|
|
269
270
|
quantity,
|
|
@@ -271,9 +272,15 @@ export function CartProvider({ children, brandSlug, initialCartId, shoppingCartP
|
|
|
271
272
|
fbp,
|
|
272
273
|
ttp,
|
|
273
274
|
ttclid,
|
|
274
|
-
refreshRecommendations:
|
|
275
|
+
refreshRecommendations: shouldRefreshRecommendations
|
|
275
276
|
});
|
|
276
277
|
|
|
278
|
+
// Invalidate recommendations cache only if cart was closed when adding
|
|
279
|
+
// (to avoid refreshing while user is viewing the cart)
|
|
280
|
+
if (shouldRefreshRecommendations && cartId) {
|
|
281
|
+
queryClient.invalidateQueries({ queryKey: queryKeys.public.carts.recommendations(cartId) });
|
|
282
|
+
}
|
|
283
|
+
|
|
277
284
|
// Skip tracking if error response
|
|
278
285
|
if ('error' in updatedCart) return;
|
|
279
286
|
|
package/src/index.ts
CHANGED
|
@@ -37,6 +37,9 @@ export type { ProductAddOnsProps, AddOnProduct } from './components/ProductAddOn
|
|
|
37
37
|
export { RecommendedProducts } from './components/RecommendedProducts';
|
|
38
38
|
export type { RecommendedProductsProps } from './components/RecommendedProducts';
|
|
39
39
|
|
|
40
|
+
export { VariantPickerModal } from './components/VariantPickerModal';
|
|
41
|
+
export type { VariantPickerModalProps, VariantPickerProduct, Variant } from './components/VariantPickerModal';
|
|
42
|
+
|
|
40
43
|
export { CartItem } from './components/CartItem';
|
|
41
44
|
export type { CartItemProps } from './components/CartItem';
|
|
42
45
|
|