@instockng/storefront-ui 1.0.74 → 1.0.76
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/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 +20 -17
- package/dist/index106.mjs +54 -213
- package/dist/index107.mjs +28 -179
- package/dist/index108.mjs +6 -21
- package/dist/index109.mjs +50 -21
- package/dist/index11.mjs +116 -88
- package/dist/index110.mjs +6 -33
- package/dist/index111.mjs +11 -155
- package/dist/index112.mjs +7 -20
- package/dist/index113.mjs +28 -31
- package/dist/index114.mjs +2 -84
- package/dist/index115.mjs +68 -36
- package/dist/index116.mjs +163 -141
- package/dist/index117.mjs +2 -55
- package/dist/index118.mjs +26 -21
- package/dist/index119.mjs +11 -20
- package/dist/index12.mjs +86 -90
- package/dist/index120.mjs +211 -19
- package/dist/index121.mjs +174 -19
- package/dist/index122.mjs +15 -14
- package/dist/index123.mjs +16 -15
- package/dist/index124.mjs +26 -14
- package/dist/index125.mjs +147 -56
- package/dist/index126.mjs +10 -8
- package/dist/index127.mjs +22 -30
- package/dist/index128.mjs +77 -17
- package/dist/index129.mjs +30 -26
- package/dist/index13.mjs +83 -185
- package/dist/index130.mjs +140 -22
- package/dist/index131.mjs +50 -14
- package/dist/index132.mjs +17 -15
- package/dist/index133.mjs +18 -38
- package/dist/index134.mjs +17 -17
- package/dist/index135.mjs +19 -262
- package/dist/index136.mjs +13 -62
- package/dist/index137.mjs +21 -7
- package/dist/index138.mjs +21 -2
- package/dist/index139.mjs +65 -2
- package/dist/index14.mjs +196 -94
- package/dist/index140.mjs +12 -27
- package/dist/index141.mjs +39 -2
- package/dist/index142.mjs +24 -2
- package/dist/index143.mjs +33 -20
- package/dist/index144.mjs +25 -54
- package/dist/index145.mjs +18 -28
- package/dist/index146.mjs +20 -6
- package/dist/index147.mjs +44 -49
- package/dist/index148.mjs +23 -6
- package/dist/index149.mjs +267 -11
- package/dist/index15.mjs +82 -713
- package/dist/index150.mjs +70 -7
- package/dist/index151.mjs +8 -28
- package/dist/index152.mjs +2 -2
- package/dist/index153.mjs +2 -70
- package/dist/index154.mjs +30 -164
- package/dist/index155.mjs +2 -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 +2 -2
- 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 +31 -2
- package/dist/index215.mjs +11 -2
- package/dist/index216.mjs +4 -2
- package/dist/index217.mjs +4 -2
- package/dist/index218.mjs +13 -108
- package/dist/index219.mjs +7 -2
- package/dist/index22.mjs +42 -71
- package/dist/index220.mjs +12 -2
- package/dist/index221.mjs +4 -36
- package/dist/index222.mjs +33 -2
- package/dist/index223.mjs +31 -2
- package/dist/index224.mjs +28 -2
- package/dist/index225.mjs +61 -2
- package/dist/index226.mjs +2 -2
- package/dist/index227.mjs +2 -2
- package/dist/index228.mjs +2 -2
- package/dist/index229.mjs +2 -244
- package/dist/index23.mjs +67 -28
- package/dist/index231.mjs +2 -33
- package/dist/index232.mjs +2 -65
- package/dist/index233.mjs +31 -19
- package/dist/index234.mjs +2 -2
- package/dist/index235.mjs +2 -2
- package/dist/index237.mjs +244 -2
- package/dist/index238.mjs +2 -2
- package/dist/index239.mjs +33 -2
- package/dist/index24.mjs +29 -6
- package/dist/index240.mjs +65 -2
- package/dist/index241.mjs +25 -2
- package/dist/index243.mjs +2 -2
- package/dist/index244.mjs +108 -4
- package/dist/index245.mjs +2 -31
- package/dist/index246.mjs +2 -11
- package/dist/index247.mjs +2 -4
- package/dist/index248.mjs +2 -4
- package/dist/index249.mjs +2 -13
- package/dist/index25.mjs +8 -21
- package/dist/index250.mjs +2 -7
- package/dist/index251.mjs +2 -12
- package/dist/index252.mjs +2 -5
- package/dist/index253.mjs +2 -33
- package/dist/index254.mjs +2 -31
- package/dist/index255.mjs +2 -28
- package/dist/index256.mjs +2 -61
- package/dist/index257.mjs +4 -2
- 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 -2
- package/dist/index267.mjs +18 -3
- package/dist/index268.mjs +47 -2
- package/dist/index269.mjs +2 -2
- package/dist/index27.mjs +34 -37
- package/dist/index270.mjs +2 -17
- package/dist/index271.mjs +2 -13
- package/dist/index272.mjs +91 -6
- package/dist/index273.mjs +2 -30
- 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 +5 -5
- 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 +24 -2
- package/dist/index71.mjs +2 -153
- package/dist/index72.mjs +4 -22
- package/dist/index73.mjs +22 -4
- 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/ShoppingCart.tsx +1 -1
- package/src/components/VariantPickerModal.tsx +154 -0
- 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
|
+
|
|
@@ -153,7 +153,7 @@ export function ShoppingCart({
|
|
|
153
153
|
|
|
154
154
|
return (
|
|
155
155
|
<div className="space-y-2">
|
|
156
|
-
<p className="text-sm font-
|
|
156
|
+
<p className="text-sm font-bold text-center">
|
|
157
157
|
{isFreeShipping ? (
|
|
158
158
|
<span className="text-green-600">You've unlocked FREE delivery! 🎉</span>
|
|
159
159
|
) : (
|
|
@@ -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
|
+
}
|
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
|
|