storefront-layouts 1.0.9 → 1.0.13
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/shipping/ProductDeliveryInfoSection.d.ts.map +1 -1
- package/dist/components/shipping/ProductDeliveryInfoSection.js +10 -3
- package/dist/components/ui/address-selector.d.ts.map +1 -1
- package/dist/components/ui/address-selector.js +4 -3
- package/dist/components/ui/button.js +1 -1
- package/dist/components/ui/checkout-button.d.ts.map +1 -1
- package/dist/components/ui/checkout-button.js +2 -1
- package/dist/components/ui/product-card.d.ts.map +1 -1
- package/dist/components/ui/product-card.js +15 -4
- package/dist/components/ui/route-skeleton-loader.d.ts.map +1 -1
- package/dist/components/ui/route-skeleton-loader.js +6 -2
- package/dist/components/ui/sheet.d.ts.map +1 -1
- package/dist/components/ui/sheet.js +2 -2
- package/dist/components/ui/skeletons/checkout-skeleton.js +1 -1
- package/dist/editor/blocks/defaults/clothing-minimal-home.js +1 -1
- package/dist/editor/blocks/migrate-clothing-minimal.js +1 -1
- package/dist/hooks/use-pdp-variation.d.ts +57 -0
- package/dist/hooks/use-pdp-variation.d.ts.map +1 -0
- package/dist/hooks/use-pdp-variation.js +69 -0
- package/dist/hooks/use-product-by-slug.d.ts.map +1 -1
- package/dist/hooks/use-product-by-slug.js +0 -5
- package/dist/hooks/use-product-variation-selection.js +1 -1
- package/dist/hooks/use-shipping-quote.d.ts +1 -0
- package/dist/hooks/use-shipping-quote.d.ts.map +1 -1
- package/dist/hooks/use-shipping-quote.js +3 -0
- package/dist/layouts/booking/pages/ProductDetailPage.d.ts.map +1 -1
- package/dist/layouts/booking/pages/ProductDetailPage.js +130 -223
- package/dist/layouts/booking-agenda/pages/ProductDetailPage.d.ts.map +1 -1
- package/dist/layouts/booking-agenda/pages/ProductDetailPage.js +130 -223
- package/dist/layouts/clothing/components/ClothingHomePage.d.ts.map +1 -1
- package/dist/layouts/clothing/components/ClothingHomePage.js +40 -2
- package/dist/layouts/clothing/pages/ProductDetailPage.d.ts.map +1 -1
- package/dist/layouts/clothing/pages/ProductDetailPage.js +127 -209
- package/dist/layouts/clothing/pages/ProductsPage.d.ts.map +1 -1
- package/dist/layouts/clothing/pages/ProductsPage.js +2 -5
- package/dist/layouts/clothing-minimal/components/ClothingHomePageMinimal.d.ts.map +1 -1
- package/dist/layouts/clothing-minimal/components/ClothingHomePageMinimal.js +5 -3
- package/dist/layouts/clothing-minimal/components/clothing-minimal-block-render.d.ts.map +1 -1
- package/dist/layouts/clothing-minimal/components/clothing-minimal-block-render.js +13 -4
- package/dist/layouts/clothing-minimal/pages/ProductDetailPage.d.ts.map +1 -1
- package/dist/layouts/clothing-minimal/pages/ProductDetailPage.js +130 -223
- package/dist/layouts/clothing-minimal/pages/ProductsPage.d.ts.map +1 -1
- package/dist/layouts/clothing-minimal/pages/ProductsPage.js +2 -5
- package/dist/layouts/electronics/components/ElectronicsGridProductCard.js +1 -1
- package/dist/layouts/electronics/components/ElectronicsStoreHeader.d.ts.map +1 -1
- package/dist/layouts/electronics/components/ElectronicsStoreHeader.js +10 -9
- package/dist/layouts/electronics/pages/ProductDetailPage.d.ts.map +1 -1
- package/dist/layouts/electronics/pages/ProductDetailPage.js +130 -223
- package/dist/layouts/electronics/pages/ProductsPage.d.ts.map +1 -1
- package/dist/layouts/electronics/pages/ProductsPage.js +2 -5
- package/dist/layouts/electronics-grid/components/ElectronicsGridProductCard.js +1 -1
- package/dist/layouts/electronics-grid/components/ElectronicsHomePageGrid.d.ts.map +1 -1
- package/dist/layouts/electronics-grid/components/ElectronicsHomePageGrid.js +4 -5
- package/dist/layouts/electronics-grid/pages/ProductDetailPage.d.ts.map +1 -1
- package/dist/layouts/electronics-grid/pages/ProductDetailPage.js +130 -223
- package/dist/layouts/electronics-grid/pages/ProductsPage.d.ts.map +1 -1
- package/dist/layouts/electronics-grid/pages/ProductsPage.js +2 -5
- package/dist/layouts/food/pages/MenuPage.js +1 -1
- package/dist/layouts/food/pages/ProductDetailPage.d.ts.map +1 -1
- package/dist/layouts/food/pages/ProductDetailPage.js +37 -140
- package/dist/layouts/food/pages/ProductsPage.d.ts.map +1 -1
- package/dist/layouts/food/pages/ProductsPage.js +2 -5
- package/dist/layouts/food-modern/pages/MenuPage.js +1 -1
- package/dist/layouts/food-modern/pages/ProductDetailPage.d.ts.map +1 -1
- package/dist/layouts/food-modern/pages/ProductDetailPage.js +130 -223
- package/dist/layouts/food-modern/pages/ProductsPage.d.ts.map +1 -1
- package/dist/layouts/food-modern/pages/ProductsPage.js +2 -5
- package/dist/layouts/motivational-speaker/pages/ProductsPage.d.ts.map +1 -1
- package/dist/layouts/motivational-speaker/pages/ProductsPage.js +2 -5
- package/dist/layouts/shared/components/PdpColorSwatch.d.ts +11 -0
- package/dist/layouts/shared/components/PdpColorSwatch.d.ts.map +1 -0
- package/dist/layouts/shared/components/PdpColorSwatch.js +29 -0
- package/dist/layouts/shared/components/PdpVariationSelectors.d.ts +13 -0
- package/dist/layouts/shared/components/PdpVariationSelectors.d.ts.map +1 -0
- package/dist/layouts/shared/components/PdpVariationSelectors.js +65 -0
- package/dist/layouts/shared/components/StoreCartSheet.d.ts +8 -0
- package/dist/layouts/shared/components/StoreCartSheet.d.ts.map +1 -0
- package/dist/layouts/shared/components/StoreCartSheet.js +16 -0
- package/dist/layouts/shared/components/StoreHeader.d.ts.map +1 -1
- package/dist/layouts/shared/components/StoreHeader.js +13 -12
- package/dist/layouts/shared/components/headers/ClothingMinimalHeader.d.ts.map +1 -1
- package/dist/layouts/shared/components/headers/ClothingMinimalHeader.js +5 -7
- package/dist/layouts/shared/pages/CartPage.d.ts.map +1 -1
- package/dist/layouts/shared/pages/CartPage.js +6 -5
- package/dist/layouts/shared/pages/CheckoutPage.d.ts.map +1 -1
- package/dist/layouts/shared/pages/CheckoutPage.js +209 -107
- package/dist/layouts/shared/product-category-filter-chip.d.ts +2 -0
- package/dist/layouts/shared/product-category-filter-chip.d.ts.map +1 -0
- package/dist/layouts/shared/product-category-filter-chip.js +6 -0
- package/dist/lib/pdp-cart-actions.d.ts +5 -0
- package/dist/lib/pdp-cart-actions.d.ts.map +1 -0
- package/dist/lib/pdp-cart-actions.js +19 -0
- package/dist/lib/product-variation-utils.d.ts +17 -0
- package/dist/lib/product-variation-utils.d.ts.map +1 -1
- package/dist/lib/product-variation-utils.js +193 -14
- package/dist/lib/services/checkout.service.d.ts +11 -0
- package/dist/lib/services/checkout.service.d.ts.map +1 -1
- package/dist/lib/services/shipping.service.d.ts +3 -0
- package/dist/lib/services/shipping.service.d.ts.map +1 -1
- package/dist/lib/store-config-utils.d.ts.map +1 -1
- package/dist/lib/store-config-utils.js +104 -11
- package/dist/lib/store-context.d.ts.map +1 -1
- package/dist/lib/store-context.js +23 -8
- package/dist/lib/utils/asset-helpers.d.ts +2 -0
- package/dist/lib/utils/asset-helpers.d.ts.map +1 -1
- package/dist/lib/utils/asset-helpers.js +24 -0
- package/dist/styles/index.css +143 -45
- package/package.json +8 -2
|
@@ -5,8 +5,8 @@ import { useProductReviews } from '../../../hooks/use-product-reviews.js';
|
|
|
5
5
|
import { Button } from '../../../components/ui/button';
|
|
6
6
|
import { Breadcrumbs } from '../../../components/ui/breadcrumbs';
|
|
7
7
|
import { Modal } from '../../../components/ui/modal';
|
|
8
|
-
import { ShoppingCart, Heart, Star, CheckCircle, ThumbsUp, User, Minus, Plus,
|
|
9
|
-
import { useState, useMemo, useEffect } from 'react';
|
|
8
|
+
import { ShoppingCart, Heart, Star, CheckCircle, ThumbsUp, User, Minus, Plus, Share2, Info } from 'lucide-react';
|
|
9
|
+
import { useState, useMemo, useEffect, useCallback } from 'react';
|
|
10
10
|
import { useStore } from '../../../lib/store-context';
|
|
11
11
|
import { useToast } from '../../../components/ui/toast';
|
|
12
12
|
import { useRouter } from 'next/navigation';
|
|
@@ -23,8 +23,12 @@ import { ButtonLoader } from '../../../components/ui/page-loader';
|
|
|
23
23
|
import { extractImageUrls } from '../../../lib/store-config-utils';
|
|
24
24
|
import { OutOfStockOverlay } from '../../../components/ui/out-of-stock-overlay';
|
|
25
25
|
import { cn, formatCurrency, filterActiveProducts, isEditorPreviewMode, resolvePdpProduct } from '../../../lib/utils';
|
|
26
|
+
import { usePdpVariation } from '../../../hooks/use-pdp-variation';
|
|
27
|
+
import { buildPdpBuyNowItem } from '../../../lib/pdp-cart-actions';
|
|
28
|
+
import { assertVariationSelected } from '../../../lib/product-variation-utils';
|
|
26
29
|
import { shippingService } from '../../../lib/services/shipping.service';
|
|
27
30
|
import { ProductDeliveryInfoSection } from '../../../components/shipping/ProductDeliveryInfoSection';
|
|
31
|
+
import { PdpVariationSelectors } from '../../shared/components/PdpVariationSelectors';
|
|
28
32
|
import { formatDateSafe } from '../../../utils/date';
|
|
29
33
|
// Helper function to format date
|
|
30
34
|
const formatDate = (dateString) => {
|
|
@@ -117,21 +121,11 @@ export function ProductDetailPage({ storeConfig: initialConfig, productSlug }) {
|
|
|
117
121
|
addToast('Failed to update wishlist', 'error');
|
|
118
122
|
}
|
|
119
123
|
};
|
|
120
|
-
|
|
121
|
-
const colorVariants = product?.variants?.filter(v => v.name === 'Color') || [];
|
|
122
|
-
const sizeVariants = product?.variants?.filter(v => v.name === 'Size') || [];
|
|
123
|
-
// Initialize state with defaults
|
|
124
|
+
const { optionGroups, selectedAttributes, setAttribute, isOptionAvailable, selectedVariation, galleryImages, displayPrice, displayCompareAt, displaySKU, isOutOfStock, getOptionImage, } = usePdpVariation(product);
|
|
124
125
|
const [selectedImage, setSelectedImage] = useState(0);
|
|
125
|
-
// Ensure selectedImage is within bounds when images change
|
|
126
126
|
useEffect(() => {
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
const timer = setTimeout(() => setSelectedImage(0), 0);
|
|
130
|
-
return () => clearTimeout(timer);
|
|
131
|
-
}
|
|
132
|
-
}, [product, selectedImage]);
|
|
133
|
-
const [selectedColor, setSelectedColor] = useState(colorVariants.length > 0 ? colorVariants[0].value : null);
|
|
134
|
-
const [selectedSize, setSelectedSize] = useState(sizeVariants.length > 0 ? sizeVariants[0].value : null);
|
|
127
|
+
setSelectedImage(0);
|
|
128
|
+
}, [selectedVariation?.id, galleryImages.join('|')]);
|
|
135
129
|
const [quantity, setQuantity] = useState(1);
|
|
136
130
|
const [showAllReviews, setShowAllReviews] = useState(false);
|
|
137
131
|
const [isReviewModalOpen, setIsReviewModalOpen] = useState(false);
|
|
@@ -152,6 +146,108 @@ export function ProductDetailPage({ storeConfig: initialConfig, productSlug }) {
|
|
|
152
146
|
const [shippingError, setShippingError] = useState(null);
|
|
153
147
|
const [freeShippingEligible, setFreeShippingEligible] = useState(false);
|
|
154
148
|
const [freeShippingThreshold, setFreeShippingThreshold] = useState(undefined);
|
|
149
|
+
const calculateShipping = useCallback(async () => {
|
|
150
|
+
if (!product || !shippingAddress.country || !shippingAddress.city) {
|
|
151
|
+
setShippingError('Please provide country and city to calculate shipping');
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
setIsCalculatingShipping(true);
|
|
155
|
+
setShippingError(null);
|
|
156
|
+
try {
|
|
157
|
+
const currency = product.currency || storeConfig.settings?.currency || 'USD';
|
|
158
|
+
const subtotal = displayPrice * quantity;
|
|
159
|
+
const result = await shippingService.calculateShipping({
|
|
160
|
+
storeId: storeConfig.id,
|
|
161
|
+
address: {
|
|
162
|
+
country: shippingAddress.country,
|
|
163
|
+
city: shippingAddress.city,
|
|
164
|
+
zipCode: shippingAddress.zipCode,
|
|
165
|
+
},
|
|
166
|
+
items: [
|
|
167
|
+
{
|
|
168
|
+
productId: product.id,
|
|
169
|
+
quantity,
|
|
170
|
+
},
|
|
171
|
+
],
|
|
172
|
+
subtotal,
|
|
173
|
+
currency,
|
|
174
|
+
});
|
|
175
|
+
setShippingMethods(result.methods || []);
|
|
176
|
+
setFreeShippingEligible(result.freeShippingEligible || false);
|
|
177
|
+
setFreeShippingThreshold(result.freeShippingThreshold);
|
|
178
|
+
}
|
|
179
|
+
catch (error) {
|
|
180
|
+
console.error('Shipping calculation error:', error);
|
|
181
|
+
setShippingError(error instanceof Error ? error.message : 'Failed to calculate shipping. Please try again.');
|
|
182
|
+
setShippingMethods([]);
|
|
183
|
+
}
|
|
184
|
+
finally {
|
|
185
|
+
setIsCalculatingShipping(false);
|
|
186
|
+
}
|
|
187
|
+
}, [
|
|
188
|
+
product,
|
|
189
|
+
shippingAddress.country,
|
|
190
|
+
shippingAddress.city,
|
|
191
|
+
shippingAddress.zipCode,
|
|
192
|
+
quantity,
|
|
193
|
+
displayPrice,
|
|
194
|
+
storeConfig.id,
|
|
195
|
+
storeConfig.settings?.currency,
|
|
196
|
+
]);
|
|
197
|
+
useEffect(() => {
|
|
198
|
+
const loadAddresses = async () => {
|
|
199
|
+
if (isAuthenticated) {
|
|
200
|
+
try {
|
|
201
|
+
const addresses = await customerService.getAddresses();
|
|
202
|
+
setSavedAddresses(addresses);
|
|
203
|
+
if (addresses.length > 0) {
|
|
204
|
+
const defaultAddress = addresses.find((a) => a.isDefault) || addresses[0];
|
|
205
|
+
if (defaultAddress) {
|
|
206
|
+
setShippingAddress({
|
|
207
|
+
country: defaultAddress.country || '',
|
|
208
|
+
city: defaultAddress.city || '',
|
|
209
|
+
zipCode: defaultAddress.zipCode || '',
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
catch (error) {
|
|
215
|
+
console.error('Failed to load addresses:', error);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
};
|
|
219
|
+
loadAddresses();
|
|
220
|
+
}, [isAuthenticated]);
|
|
221
|
+
useEffect(() => {
|
|
222
|
+
if (!shippingAddress.country || !shippingAddress.city || !product) {
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
const timer = setTimeout(() => {
|
|
226
|
+
calculateShipping();
|
|
227
|
+
}, 500);
|
|
228
|
+
return () => clearTimeout(timer);
|
|
229
|
+
}, [
|
|
230
|
+
shippingAddress.country,
|
|
231
|
+
shippingAddress.city,
|
|
232
|
+
shippingAddress.zipCode,
|
|
233
|
+
quantity,
|
|
234
|
+
product?.id,
|
|
235
|
+
displayPrice,
|
|
236
|
+
calculateShipping,
|
|
237
|
+
]);
|
|
238
|
+
useEffect(() => {
|
|
239
|
+
if (product && storeConfig?.id) {
|
|
240
|
+
trackEvent({
|
|
241
|
+
eventType: 'product_view',
|
|
242
|
+
metadata: {
|
|
243
|
+
productId: product.id,
|
|
244
|
+
productName: product.name,
|
|
245
|
+
categoryId: product.categoryId,
|
|
246
|
+
price: displayPrice,
|
|
247
|
+
},
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
}, [product?.id, storeConfig?.id, displayPrice, trackEvent]);
|
|
155
251
|
const handleSubmitReview = async (e) => {
|
|
156
252
|
e.preventDefault();
|
|
157
253
|
if (!product?.id)
|
|
@@ -190,8 +286,6 @@ export function ProductDetailPage({ storeConfig: initialConfig, productSlug }) {
|
|
|
190
286
|
if (!product) {
|
|
191
287
|
return (_jsx("div", { className: "min-h-screen bg-white flex items-center justify-center p-8", children: _jsxs("div", { className: "text-center", children: [_jsx("h1", { className: "text-2xl font-bold mb-2", children: "Product Not Found" }), _jsx("p", { className: "text-gray-600 mb-4", children: "The product you're looking for doesn't exist." })] }) }));
|
|
192
288
|
}
|
|
193
|
-
// Use SKU for display, fallback to generated SKU from slug if not available
|
|
194
|
-
const displaySKU = product.sku || product.slug?.toUpperCase().replace(/-/g, '').substring(0, 12).padEnd(8, 'X') || 'N/A';
|
|
195
289
|
// Build category breadcrumb path with all parent categories
|
|
196
290
|
const buildCategoryPath = (categoryId) => {
|
|
197
291
|
if (!categoryId)
|
|
@@ -222,35 +316,7 @@ export function ProductDetailPage({ storeConfig: initialConfig, productSlug }) {
|
|
|
222
316
|
// Add product name
|
|
223
317
|
{ label: product.name, href: `/${storeConfig.slug}/products/${product.slug}` },
|
|
224
318
|
];
|
|
225
|
-
|
|
226
|
-
// but handle edge cases where it might still be in object format
|
|
227
|
-
// Use the comprehensive extraction utility from store-config-utils for consistency
|
|
228
|
-
const productImages = (() => {
|
|
229
|
-
// Debug: Log the raw product.images to understand its format
|
|
230
|
-
if (process.env.NODE_ENV === 'development') {
|
|
231
|
-
console.log('[ProductDetailPage] Raw product.images:', {
|
|
232
|
-
productId: product.id,
|
|
233
|
-
productName: product.name,
|
|
234
|
-
images: product.images,
|
|
235
|
-
imagesType: typeof product.images,
|
|
236
|
-
imagesIsArray: Array.isArray(product.images),
|
|
237
|
-
imagesLength: Array.isArray(product.images) ? product.images.length : 'N/A',
|
|
238
|
-
firstItem: Array.isArray(product.images) && product.images.length > 0 ? product.images[0] : 'N/A',
|
|
239
|
-
firstItemType: Array.isArray(product.images) && product.images.length > 0 ? typeof product.images[0] : 'N/A'
|
|
240
|
-
});
|
|
241
|
-
}
|
|
242
|
-
// If already an array of strings (normal case after transformation)
|
|
243
|
-
if (Array.isArray(product.images) && product.images.length > 0) {
|
|
244
|
-
const firstItem = product.images[0];
|
|
245
|
-
if (typeof firstItem === 'string') {
|
|
246
|
-
const filtered = product.images.filter((img) => typeof img === 'string' && img.trim().length > 0);
|
|
247
|
-
return filtered;
|
|
248
|
-
}
|
|
249
|
-
}
|
|
250
|
-
// Use the comprehensive extraction utility for any other format (objects, nested structures, etc.)
|
|
251
|
-
const extracted = extractImageUrls(product.images);
|
|
252
|
-
return extracted;
|
|
253
|
-
})();
|
|
319
|
+
const productImages = galleryImages.length > 0 ? galleryImages : extractImageUrls(product.images);
|
|
254
320
|
// For gallery, use actual images only (don't duplicate to fill 5 slots)
|
|
255
321
|
const displayImages = productImages.slice(0, 5);
|
|
256
322
|
const hasMoreImages = productImages.length > 5;
|
|
@@ -275,20 +341,6 @@ export function ProductDetailPage({ storeConfig: initialConfig, productSlug }) {
|
|
|
275
341
|
});
|
|
276
342
|
}
|
|
277
343
|
}
|
|
278
|
-
// Color swatch mapping
|
|
279
|
-
const colorSwatches = {
|
|
280
|
-
'White': '#FFFFFF',
|
|
281
|
-
'Black': '#000000',
|
|
282
|
-
'Gray': '#808080',
|
|
283
|
-
'Grey': '#808080',
|
|
284
|
-
'Blue': '#3B82F6',
|
|
285
|
-
'Red': '#EF4444',
|
|
286
|
-
'Green': '#10B981',
|
|
287
|
-
'Yellow': '#FBBF24',
|
|
288
|
-
'Brown': '#92400E',
|
|
289
|
-
'Navy': '#1E3A8A',
|
|
290
|
-
'Beige': '#F5F5DC',
|
|
291
|
-
};
|
|
292
344
|
// Reviews loaded from API (product payload does not include review rows)
|
|
293
345
|
const displayedReviews = showAllReviews ? reviews : reviews.slice(0, 3);
|
|
294
346
|
const hasMoreReviews = reviews.length > 3;
|
|
@@ -296,8 +348,6 @@ export function ProductDetailPage({ storeConfig: initialConfig, productSlug }) {
|
|
|
296
348
|
const averageRating = reviews.length > 0
|
|
297
349
|
? reviews.reduce((sum, review) => sum + review.rating, 0) / reviews.length
|
|
298
350
|
: Number(product.rating) || 0;
|
|
299
|
-
// Out of stock check
|
|
300
|
-
const isOutOfStock = !product.inStock;
|
|
301
351
|
// Calculate rating distribution
|
|
302
352
|
const ratingDistribution = [5, 4, 3, 2, 1].map(rating => {
|
|
303
353
|
const count = reviews.filter(r => Math.round(r.rating) === rating).length;
|
|
@@ -307,29 +357,19 @@ export function ProductDetailPage({ storeConfig: initialConfig, productSlug }) {
|
|
|
307
357
|
percentage: reviews.length > 0 ? (count / reviews.length) * 100 : 0,
|
|
308
358
|
};
|
|
309
359
|
});
|
|
310
|
-
const
|
|
311
|
-
if (!product.variants?.length)
|
|
312
|
-
return undefined;
|
|
313
|
-
// Only match when the shopper has chosen options — do not treat
|
|
314
|
-
// "no selection" as a wildcard that silently picks a lone variation.
|
|
315
|
-
if (!selectedColor && !selectedSize)
|
|
316
|
-
return undefined;
|
|
317
|
-
return product.variants.find((variation) => {
|
|
318
|
-
const attrs = variation.attributes || {};
|
|
319
|
-
const colorMatch = !selectedColor || attrs.color === selectedColor;
|
|
320
|
-
const sizeMatch = !selectedSize || attrs.size === selectedSize;
|
|
321
|
-
return colorMatch && sizeMatch;
|
|
322
|
-
});
|
|
323
|
-
};
|
|
324
|
-
const handleAddToCart = () => {
|
|
360
|
+
const handleAddToCart = async () => {
|
|
325
361
|
if (isOutOfStock) {
|
|
326
362
|
addToast('This product is currently out of stock', 'error');
|
|
327
363
|
return;
|
|
328
364
|
}
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
365
|
+
try {
|
|
366
|
+
assertVariationSelected(product, selectedVariation, selectedAttributes);
|
|
367
|
+
await addToCart(product, quantity, selectedVariation?.id);
|
|
368
|
+
addToast(`${product.name} added to cart`, 'success');
|
|
369
|
+
}
|
|
370
|
+
catch (error) {
|
|
371
|
+
addToast(error instanceof Error ? error.message : 'Could not add to cart', 'error');
|
|
372
|
+
}
|
|
333
373
|
};
|
|
334
374
|
const handleBuyNow = async () => {
|
|
335
375
|
if (isOutOfStock) {
|
|
@@ -341,32 +381,14 @@ export function ProductDetailPage({ storeConfig: initialConfig, productSlug }) {
|
|
|
341
381
|
}
|
|
342
382
|
setIsBuyNowLoading(true);
|
|
343
383
|
try {
|
|
344
|
-
const
|
|
345
|
-
const variantId = matchingVariation?.id;
|
|
346
|
-
const variantName = matchingVariation
|
|
347
|
-
? (Object.values(matchingVariation.attributes || {}).filter(Boolean).join(' / ') ||
|
|
348
|
-
matchingVariation.name ||
|
|
349
|
-
matchingVariation.value)
|
|
350
|
-
: undefined;
|
|
351
|
-
// Create buy now item (don't add to cart) — price must match what's shown on PDP
|
|
352
|
-
const buyNowItemId = variantId ? `${product.id}-${variantId}` : product.id;
|
|
353
|
-
const buyNowItem = {
|
|
354
|
-
id: buyNowItemId,
|
|
355
|
-
productId: product.id,
|
|
356
|
-
quantity,
|
|
357
|
-
variantId,
|
|
358
|
-
variantName: variantName || variantId,
|
|
359
|
-
price: matchingVariation?.price ?? product.price,
|
|
360
|
-
product
|
|
361
|
-
};
|
|
362
|
-
// Set buy now item and navigate directly to checkout
|
|
384
|
+
const buyNowItem = buildPdpBuyNowItem(product, quantity, selectedVariation, displayPrice, selectedAttributes);
|
|
363
385
|
setBuyNowItem(buyNowItem);
|
|
364
386
|
// Navigate to checkout page for direct checkout
|
|
365
387
|
router.push(`/${storeConfig.slug}/checkout`);
|
|
366
388
|
}
|
|
367
389
|
catch (error) {
|
|
368
390
|
console.error('Buy Now error:', error);
|
|
369
|
-
addToast('Failed to proceed to checkout. Please try again.', 'error');
|
|
391
|
+
addToast(error instanceof Error ? error.message : 'Failed to proceed to checkout. Please try again.', 'error');
|
|
370
392
|
}
|
|
371
393
|
finally {
|
|
372
394
|
setIsBuyNowLoading(false);
|
|
@@ -379,128 +401,24 @@ export function ProductDetailPage({ storeConfig: initialConfig, productSlug }) {
|
|
|
379
401
|
// Note: This is now only used for other flows, not buy now
|
|
380
402
|
// Buy now now redirects to checkout page
|
|
381
403
|
};
|
|
382
|
-
// Calculate shipping when address is provided
|
|
383
|
-
const calculateShipping = async () => {
|
|
384
|
-
if (!product || !shippingAddress.country || !shippingAddress.city) {
|
|
385
|
-
setShippingError('Please provide country and city to calculate shipping');
|
|
386
|
-
return;
|
|
387
|
-
}
|
|
388
|
-
setIsCalculatingShipping(true);
|
|
389
|
-
setShippingError(null);
|
|
390
|
-
try {
|
|
391
|
-
const currency = product.currency || storeConfig.settings?.currency || 'USD';
|
|
392
|
-
const subtotal = product.price * quantity;
|
|
393
|
-
const result = await shippingService.calculateShipping({
|
|
394
|
-
storeId: storeConfig.id,
|
|
395
|
-
address: {
|
|
396
|
-
country: shippingAddress.country,
|
|
397
|
-
city: shippingAddress.city,
|
|
398
|
-
zipCode: shippingAddress.zipCode,
|
|
399
|
-
},
|
|
400
|
-
items: [{
|
|
401
|
-
productId: product.id,
|
|
402
|
-
quantity: quantity,
|
|
403
|
-
}],
|
|
404
|
-
subtotal: subtotal,
|
|
405
|
-
currency: currency,
|
|
406
|
-
});
|
|
407
|
-
setShippingMethods(result.methods || []);
|
|
408
|
-
setFreeShippingEligible(result.freeShippingEligible || false);
|
|
409
|
-
setFreeShippingThreshold(result.freeShippingThreshold);
|
|
410
|
-
}
|
|
411
|
-
catch (error) {
|
|
412
|
-
console.error('Shipping calculation error:', error);
|
|
413
|
-
setShippingError(error.message || 'Failed to calculate shipping. Please try again.');
|
|
414
|
-
setShippingMethods([]);
|
|
415
|
-
}
|
|
416
|
-
finally {
|
|
417
|
-
setIsCalculatingShipping(false);
|
|
418
|
-
}
|
|
419
|
-
};
|
|
420
|
-
// Load saved addresses for authenticated users and auto-populate shipping address
|
|
421
|
-
useEffect(() => {
|
|
422
|
-
const loadAddresses = async () => {
|
|
423
|
-
if (isAuthenticated) {
|
|
424
|
-
try {
|
|
425
|
-
const addresses = await customerService.getAddresses();
|
|
426
|
-
setSavedAddresses(addresses);
|
|
427
|
-
// Auto-populate with default address or first address if available
|
|
428
|
-
if (addresses.length > 0) {
|
|
429
|
-
const defaultAddress = addresses.find(a => a.isDefault) || addresses[0];
|
|
430
|
-
if (defaultAddress) {
|
|
431
|
-
setShippingAddress({
|
|
432
|
-
country: defaultAddress.country || '',
|
|
433
|
-
city: defaultAddress.city || '',
|
|
434
|
-
zipCode: defaultAddress.zipCode || '',
|
|
435
|
-
});
|
|
436
|
-
}
|
|
437
|
-
}
|
|
438
|
-
}
|
|
439
|
-
catch (error) {
|
|
440
|
-
console.error('Failed to load addresses:', error);
|
|
441
|
-
// Don't show error toast - addresses are optional
|
|
442
|
-
// Users can still manually enter shipping address
|
|
443
|
-
}
|
|
444
|
-
}
|
|
445
|
-
};
|
|
446
|
-
loadAddresses();
|
|
447
|
-
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
448
|
-
}, [isAuthenticated]);
|
|
449
|
-
// Auto-calculate shipping when address fields are filled (debounced)
|
|
450
|
-
useEffect(() => {
|
|
451
|
-
if (!shippingAddress.country || !shippingAddress.city || !product) {
|
|
452
|
-
return;
|
|
453
|
-
}
|
|
454
|
-
const timer = setTimeout(() => {
|
|
455
|
-
calculateShipping();
|
|
456
|
-
}, 500); // Debounce 500ms
|
|
457
|
-
return () => clearTimeout(timer);
|
|
458
|
-
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
459
|
-
}, [shippingAddress.country, shippingAddress.city, shippingAddress.zipCode, quantity, product?.id, product?.price]);
|
|
460
|
-
// Track product view
|
|
461
|
-
useEffect(() => {
|
|
462
|
-
if (product && storeConfig?.id) {
|
|
463
|
-
trackEvent({
|
|
464
|
-
eventType: 'product_view',
|
|
465
|
-
metadata: {
|
|
466
|
-
productId: product.id,
|
|
467
|
-
productName: product.name,
|
|
468
|
-
categoryId: product.categoryId,
|
|
469
|
-
price: product.price,
|
|
470
|
-
},
|
|
471
|
-
});
|
|
472
|
-
}
|
|
473
|
-
}, [product?.id, storeConfig?.id, trackEvent]);
|
|
474
404
|
return (_jsxs("div", { className: "min-h-screen bg-white", children: [_jsxs("div", { "data-content-ready": true, className: "container mx-auto px-4 sm:px-6 lg:px-8 py-8 lg:py-12 max-w-7xl", children: [_jsx("div", { className: "mb-6", children: _jsx(Breadcrumbs, { items: breadcrumbItems }) }), _jsxs("div", { className: "grid grid-cols-1 lg:grid-cols-2 gap-8 lg:gap-16 mb-20", children: [_jsxs("div", { className: "relative lg:sticky lg:top-24 self-start z-0", children: [_jsxs("div", { className: "relative aspect-square bg-gray-50 rounded-xl overflow-hidden mb-4 border border-gray-100 shadow-sm group", children: [_jsx(ImageWithFallback, { "data-product-image": true, src: displayImages[selectedImage] || displayImages[0] || productImages[0], alt: product.name, className: cn("w-full h-full object-cover transition-transform duration-300 group-hover:scale-105", isOutOfStock && "grayscale"), skeletonAspectRatio: "square" }), isOutOfStock && (_jsx(OutOfStockOverlay, { badgePosition: "center" }))] }), displayImages.length > 0 && (_jsxs("div", { className: "grid grid-cols-5 gap-2", children: [displayImages.map((image, index) => (_jsx("button", { onClick: () => setSelectedImage(Math.min(index, displayImages.length - 1)), className: `aspect-square rounded-lg overflow-hidden border-2 transition-all ${selectedImage === index
|
|
475
405
|
? 'border-gray-900 ring-2 ring-gray-900 ring-offset-2'
|
|
476
406
|
: 'border-gray-200 hover:border-gray-300'}`, children: _jsx(ImageWithFallback, { src: image, alt: `${product.name} view ${index + 1}`, className: cn("w-full h-full object-cover", isOutOfStock && "grayscale"), skeletonAspectRatio: "square" }) }, index))), hasMoreImages && (_jsxs("div", { className: "aspect-square rounded-lg border-2 border-gray-200 bg-gray-50 flex items-center justify-center text-xs text-gray-500 font-medium", children: ["+", productImages.length - 5, " more"] }))] }))] }), _jsxs("div", { className: "flex flex-col", children: [_jsxs("div", { className: "flex items-center justify-between mb-2", children: [_jsx("span", { className: "text-sm font-semibold text-gray-700 uppercase tracking-wide", children: (() => {
|
|
477
407
|
const brandMatch = product.name.match(/\b(Nike|Adidas|Puma|Reebok|New Balance|Vans|Converse|Zara|H&M|Gucci|Prada|Levi's|Calvin Klein)\b/i);
|
|
478
408
|
return brandMatch ? brandMatch[1] : storeConfig.name;
|
|
479
|
-
})() }), _jsx("button", { onClick: handleShare, className: "text-gray-400 hover:text-gray-900 transition-colors p-2 rounded-full hover:bg-gray-100", title: "Share this product", children: _jsx(Share2, { className: "h-5 w-5" }) })] }), _jsx("h1", { "data-product-name": true, className: "text-3xl sm:text-4xl lg:text-5xl font-bold text-gray-900 mb-3 leading-tight tracking-tight", children: product.name }), _jsxs("p", { className: "text-sm text-gray-500 mb-6 font-mono", children: ["SKU: ", displaySKU] }), _jsx("div", { className: "mb-4 flex items-center gap-2", children:
|
|
480
|
-
const colorValue = variant.value;
|
|
481
|
-
const swatchColor = colorSwatches[colorValue] || '#CCCCCC';
|
|
482
|
-
const isSelected = selectedColor === colorValue;
|
|
483
|
-
return (_jsx("button", { onClick: () => setSelectedColor(colorValue), className: `relative w-12 h-12 rounded-full border-2 transition-all ${isSelected
|
|
484
|
-
? 'border-gray-900 ring-2 ring-gray-900 ring-offset-2 scale-105'
|
|
485
|
-
: 'border-gray-200 hover:border-gray-300'}`, style: { backgroundColor: swatchColor }, title: colorValue, children: isSelected && (_jsx("div", { className: "absolute inset-0 flex items-center justify-center", children: _jsx(CheckCircle, { className: `h-5 w-5 ${['White', 'Beige', 'Yellow'].includes(colorValue) ? 'text-gray-900' : 'text-white'} drop-shadow-sm` }) })) }, variant.id));
|
|
486
|
-
}) })] })), sizeVariants.length > 0 && (_jsxs("div", { className: "mb-8", children: [_jsxs("div", { className: "flex items-center justify-between mb-4", children: [_jsxs("div", { className: "flex items-center gap-2", children: [_jsx("span", { className: "text-sm font-semibold text-gray-900", children: "Size:" }), _jsx("span", { className: "text-sm text-gray-600", children: selectedSize || 'Select Size' })] }), productDetailConfig?.showSizeGuide !== false && (_jsxs("a", { href: `/${storeConfig.slug}/size-guide`, target: "_blank", rel: "noopener noreferrer", className: "flex items-center gap-1.5 text-xs font-medium text-gray-600 hover:text-gray-900 underline decoration-gray-400 underline-offset-4", children: [_jsx(Ruler, { className: "h-3.5 w-3.5" }), " Size guide"] }))] }), _jsx("div", { className: "grid grid-cols-4 sm:grid-cols-5 gap-2.5", children: sizeVariants.map((variant) => {
|
|
487
|
-
const isSelected = selectedSize === variant.value;
|
|
488
|
-
return (_jsx("button", { onClick: () => setSelectedSize(variant.value), className: `px-4 py-3 rounded-lg border-2 text-sm font-semibold transition-all ${isSelected
|
|
489
|
-
? 'bg-gray-900 text-white border-gray-900 shadow-md transform -translate-y-0.5'
|
|
490
|
-
: 'bg-white text-gray-700 border-gray-200 hover:border-gray-300 hover:bg-gray-50'}`, children: variant.value }, variant.id));
|
|
491
|
-
}) })] })), _jsxs("div", { className: "flex flex-col gap-4 mb-8", children: [_jsxs("div", { className: "flex items-center gap-3", children: [_jsx("span", { className: "text-sm font-semibold text-gray-900", children: "Quantity" }), _jsxs("div", { className: "flex items-center border border-gray-300 rounded-lg", children: [_jsx("button", { className: "p-3 text-gray-600 hover:bg-gray-100 rounded-l-lg disabled:opacity-50", onClick: () => setQuantity(Math.max(1, quantity - 1)), disabled: quantity <= 1, children: _jsx(Minus, { className: "h-4 w-4" }) }), _jsx("span", { className: "w-12 text-center font-semibold text-gray-900", children: quantity }), _jsx("button", { className: "p-3 text-gray-600 hover:bg-gray-100 rounded-r-lg", onClick: () => setQuantity(quantity + 1), children: _jsx(Plus, { className: "h-4 w-4" }) })] })] }), _jsxs("div", { className: "flex flex-row items-stretch gap-2 sm:gap-3 mt-2 w-full", children: [productDetailConfig?.showAddToCart !== false && (_jsx(Button, { "data-add-to-cart": true, size: "lg", onClick: handleAddToCart, disabled: isCartLoading || isOutOfStock, className: "flex-1 min-w-0 bg-white border-2 border-gray-900 text-gray-900 hover:bg-gray-50 rounded-full h-12 sm:h-14 px-3 sm:px-6 text-sm sm:text-base font-bold shadow-sm transition-all disabled:opacity-50 disabled:cursor-not-allowed [&_svg]:text-gray-900", children: isCartLoading ? (_jsxs(_Fragment, { children: [_jsx(ButtonLoader, { className: "mr-1.5 sm:mr-2" }), "Adding..."] })) : (_jsxs(_Fragment, { children: [_jsx(ShoppingCart, { className: "h-4 w-4 sm:h-5 sm:w-5 mr-1.5 sm:mr-2 shrink-0 text-gray-900" }), "Add to Cart"] })) })), productDetailConfig?.showAddToCart !== false && (_jsx(Button, { size: "lg", onClick: handleBuyNow, disabled: isOutOfStock || isBuyNowLoading || isCartLoading, className: "flex-1 min-w-0 bg-gray-900 hover:bg-gray-800 text-white rounded-full h-12 sm:h-14 px-3 sm:px-6 text-sm sm:text-base font-bold shadow-lg hover:shadow-xl transition-all disabled:opacity-50 disabled:cursor-not-allowed", children: isBuyNowLoading || isCartLoading ? (_jsxs(_Fragment, { children: [_jsx(ButtonLoader, { className: "mr-1.5 sm:mr-2" }), "Adding..."] })) : ('Buy Now') })), storeConfig.features.wishlist && productDetailConfig?.showWishlist !== false && (_jsx(Button, { variant: "outline", size: "lg", onClick: handleWishlistClick, disabled: isWishlistLoading || !product, className: cn("h-12 w-12 sm:h-14 sm:w-14 shrink-0 rounded-full border-2 transition-all disabled:opacity-50 disabled:cursor-not-allowed", product && isInWishlist(product.id)
|
|
409
|
+
})() }), _jsx("button", { onClick: handleShare, className: "text-gray-400 hover:text-gray-900 transition-colors p-2 rounded-full hover:bg-gray-100", title: "Share this product", children: _jsx(Share2, { className: "h-5 w-5" }) })] }), _jsx("h1", { "data-product-name": true, className: "text-3xl sm:text-4xl lg:text-5xl font-bold text-gray-900 mb-3 leading-tight tracking-tight", children: product.name }), _jsxs("p", { className: "text-sm text-gray-500 mb-6 font-mono", children: ["SKU: ", displaySKU] }), _jsx("div", { className: "mb-4 flex items-center gap-2", children: !isOutOfStock ? (_jsxs("span", { className: "inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-green-100 text-green-800", children: [_jsx(CheckCircle, { className: "w-3 h-3 mr-1" }), "In Stock"] })) : (_jsxs("span", { className: "inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-red-100 text-red-800", children: [_jsx(Info, { className: "w-3 h-3 mr-1" }), "Out of Stock"] })) }), _jsxs("div", { className: "mb-8", children: [_jsxs("div", { className: "flex items-baseline gap-4", children: [_jsx("span", { "data-product-price": true, className: "text-4xl lg:text-5xl font-bold text-gray-900", children: formatCurrency(displayPrice, product.currency || 'USD') }), displayCompareAt != null && displayCompareAt > displayPrice && (_jsx("span", { className: "text-2xl text-gray-500 line-through", children: formatCurrency(displayCompareAt, product.currency || 'USD') }))] }), displayCompareAt != null && displayCompareAt > displayPrice && (_jsxs("span", { className: "inline-block mt-2 bg-red-100 text-red-700 text-xs font-bold px-2 py-1 rounded", children: ["SAVE ", Math.round(((displayCompareAt - displayPrice) / displayCompareAt) * 100), "%"] }))] }), _jsx(PdpVariationSelectors, { optionGroups: optionGroups, selectedAttributes: selectedAttributes, setAttribute: setAttribute, isOptionAvailable: isOptionAvailable, getOptionImage: getOptionImage, storeSlug: storeConfig.slug, showSizeGuide: productDetailConfig?.showSizeGuide !== false, onAfterClearAttribute: () => setQuantity(1) }), _jsxs("div", { className: "flex flex-col gap-4 mb-8", children: [_jsxs("div", { className: "flex items-center gap-3", children: [_jsx("span", { className: "text-sm font-semibold text-gray-900", children: "Quantity" }), _jsxs("div", { className: "flex items-center border border-gray-300 rounded-lg", children: [_jsx("button", { className: "p-3 text-gray-600 hover:bg-gray-100 rounded-l-lg disabled:opacity-50", onClick: () => setQuantity(Math.max(1, quantity - 1)), disabled: quantity <= 1, children: _jsx(Minus, { className: "h-4 w-4" }) }), _jsx("span", { className: "w-12 text-center font-semibold text-gray-900", children: quantity }), _jsx("button", { className: "p-3 text-gray-600 hover:bg-gray-100 rounded-r-lg", onClick: () => setQuantity(quantity + 1), children: _jsx(Plus, { className: "h-4 w-4" }) })] })] }), _jsxs("div", { className: "flex flex-row items-stretch gap-2 sm:gap-3 mt-2 w-full", children: [productDetailConfig?.showAddToCart !== false && (_jsx(Button, { "data-add-to-cart": true, variant: "outline", size: "lg", onClick: handleAddToCart, disabled: isCartLoading || isOutOfStock, className: "flex-1 min-w-0 bg-white border-2 border-gray-900 text-gray-900 hover:bg-gray-50 rounded-full h-12 sm:h-14 px-3 sm:px-6 text-sm sm:text-base font-bold shadow-sm transition-all disabled:opacity-50 disabled:cursor-not-allowed [&_svg]:text-gray-900", children: isCartLoading ? (_jsxs(_Fragment, { children: [_jsx(ButtonLoader, { className: "mr-1.5 sm:mr-2" }), "Adding..."] })) : (_jsxs(_Fragment, { children: [_jsx(ShoppingCart, { className: "h-4 w-4 sm:h-5 sm:w-5 mr-1.5 sm:mr-2 shrink-0 text-gray-900" }), "Add to Cart"] })) })), productDetailConfig?.showAddToCart !== false && (_jsx(Button, { size: "lg", onClick: handleBuyNow, disabled: isOutOfStock || isBuyNowLoading || isCartLoading, className: "flex-1 min-w-0 bg-gray-900 hover:bg-gray-800 text-white rounded-full h-12 sm:h-14 px-3 sm:px-6 text-sm sm:text-base font-bold shadow-lg hover:shadow-xl transition-all disabled:opacity-50 disabled:cursor-not-allowed", children: isBuyNowLoading || isCartLoading ? (_jsxs(_Fragment, { children: [_jsx(ButtonLoader, { className: "mr-1.5 sm:mr-2" }), "Adding..."] })) : ('Buy Now') })), storeConfig.features.wishlist && productDetailConfig?.showWishlist !== false && (_jsx(Button, { variant: "outline", size: "lg", onClick: handleWishlistClick, disabled: isWishlistLoading || !product, className: cn("h-12 w-12 sm:h-14 sm:w-14 shrink-0 rounded-full border-2 transition-all disabled:opacity-50 disabled:cursor-not-allowed", product && isInWishlist(product.id)
|
|
492
410
|
? "border-red-500 bg-red-50 text-red-500 hover:bg-red-100"
|
|
493
|
-
: "border-gray-200 hover:border-gray-300 hover:bg-gray-50"), children: isWishlistLoading ? (_jsx(ButtonLoader, { className: "h-5 w-5" })) : (_jsx(Heart, { className: cn("h-5 w-5", product && isInWishlist(product.id) && "fill-current") })) }))] })] }), _jsx(ProductDeliveryInfoSection, { storeConfig: storeConfig, currency: storeConfig.settings?.currency || 'USD', productCurrency: product.currency, productPrice:
|
|
411
|
+
: "border-gray-200 hover:border-gray-300 hover:bg-gray-50"), children: isWishlistLoading ? (_jsx(ButtonLoader, { className: "h-5 w-5" })) : (_jsx(Heart, { className: cn("h-5 w-5", product && isInWishlist(product.id) && "fill-current") })) }))] })] }), _jsx(ProductDeliveryInfoSection, { storeConfig: storeConfig, currency: storeConfig.settings?.currency || 'USD', productCurrency: product.currency, productPrice: displayPrice, quantity: quantity, shippingAddress: shippingAddress, updateShippingAddress: (patch) => setShippingAddress((prev) => ({ ...prev, ...patch })), shippingMethods: shippingMethods, isCalculatingShipping: isCalculatingShipping, shippingError: shippingError, freeShippingEligible: freeShippingEligible, freeShippingThreshold: freeShippingThreshold, onToast: addToast }), product.description && (_jsxs("div", { className: "mb-8", children: [_jsx("h2", { className: "text-lg font-semibold text-gray-900 mb-3", children: "Description" }), _jsxs("div", { className: "prose prose-sm max-w-none text-gray-600 leading-relaxed", children: [_jsx(SafeRichHtml, { value: product.description, fallbackPlain: "", as: "div" }), _jsxs("ul", { className: "list-disc pl-5 space-y-1 mt-2", children: [_jsx("li", { children: "Premium quality material" }), _jsx("li", { children: "Comfortable fit for all-day wear" }), _jsx("li", { children: "Durable stitching and finish" })] })] })] })), product.specifications && Object.keys(product.specifications).length > 0 && (_jsxs("div", { className: "mb-8", children: [_jsx("h2", { className: "text-lg font-semibold text-gray-900 mb-4", children: "Specifications" }), _jsx("dl", { className: "grid grid-cols-1 sm:grid-cols-2 gap-x-4 gap-y-4", children: Object.entries(product.specifications).map(([key, value]) => (_jsxs("div", { className: "flex flex-col p-3 bg-gray-50 rounded-lg", children: [_jsx("dt", { className: "text-xs text-gray-500 font-medium uppercase tracking-wider mb-1", children: key }), _jsx("dd", { className: "text-sm font-bold text-gray-900", children: value })] }, key))) })] }))] })] }), productDetailConfig?.showReviews !== false && (_jsx("div", { className: "mt-20 pt-16 border-t border-gray-200", children: _jsxs("div", { className: "max-w-4xl", children: [_jsxs("div", { className: "flex flex-col sm:flex-row items-start sm:items-center justify-between gap-6 mb-10", children: [_jsxs("div", { className: "flex-1", children: [_jsx("h2", { className: "text-2xl lg:text-3xl font-bold text-gray-900 mb-4", children: "Customer Reviews" }), reviewCount > 0 && averageRating > 0 && (_jsxs("div", { className: "flex items-center gap-4", children: [_jsxs("div", { className: "flex items-center gap-2", children: [_jsx(StarRating, { rating: averageRating, size: "lg" }), _jsx("span", { className: "text-2xl font-bold text-gray-900", children: averageRating.toFixed(1) }), _jsx("span", { className: "text-lg text-gray-500", children: "out of 5" })] }), _jsx("span", { className: "text-gray-300", children: "|" }), _jsxs("span", { className: "text-lg text-gray-600", children: [reviewCount, " ", reviewCount === 1 ? 'review' : 'reviews'] })] }))] }), reviewCount > 0 && (_jsx(Button, { onClick: () => {
|
|
494
412
|
if (!isAuthenticated) {
|
|
495
413
|
openLogin({ onSuccess: () => setIsReviewModalOpen(true) });
|
|
496
414
|
return;
|
|
497
415
|
}
|
|
498
416
|
setIsReviewModalOpen(true);
|
|
499
|
-
}, className: "rounded-full shrink-0 bg-gray-900 hover:bg-gray-800 text-white", children:
|
|
417
|
+
}, className: "rounded-full shrink-0 bg-gray-900 hover:bg-gray-800 text-white", children: "Write a Review" }))] }), ratingDistribution.some(r => r.count > 0) && (_jsxs("div", { className: "mb-10 p-6 bg-gray-50 rounded-xl border border-gray-100", children: [_jsx("h3", { className: "text-sm font-semibold text-gray-900 mb-5", children: "Rating Breakdown" }), _jsx("div", { className: "space-y-3", children: ratingDistribution.map(({ rating, count, percentage }) => (_jsxs("div", { className: "flex items-center gap-4", children: [_jsxs("div", { className: "flex items-center gap-1.5 w-20", children: [_jsx("span", { className: "text-sm font-semibold text-gray-900", children: rating }), _jsx(Star, { className: "h-4 w-4 fill-gray-900 text-gray-900" })] }), _jsx("div", { className: "flex-1 h-2.5 bg-gray-200 rounded-full overflow-hidden", children: _jsx("div", { className: "h-full bg-gradient-to-r from-gray-700 to-gray-900 rounded-full transition-all duration-500", style: { width: `${percentage}%` } }) }), _jsx("span", { className: "text-sm font-medium text-gray-700 w-12 text-right", children: count })] }, rating))) })] })), reviews.length > 0 ? (_jsxs(_Fragment, { children: [_jsx("div", { className: "space-y-6", children: displayedReviews.map((review) => (_jsx(ReviewCard, { review: review }, review.id))) }), hasMoreReviews && !showAllReviews && (_jsx("div", { className: "mt-8 text-center", children: _jsxs(Button, { variant: "outline", onClick: () => setShowAllReviews(true), className: "px-8 rounded-full h-12 border-gray-300", children: ["Show All ", reviews.length, " Reviews"] }) }))] })) : (_jsxs("div", { className: "text-center py-12 bg-gray-50 rounded-xl border border-dashed border-gray-300", children: [_jsx("div", { className: "inline-flex items-center justify-center w-12 h-12 rounded-full bg-white shadow-sm mb-4", children: _jsx(Star, { className: "h-6 w-6 text-gray-400" }) }), _jsx("h3", { className: "text-lg font-semibold text-gray-900 mb-2", children: "No Reviews Yet" }), _jsx("p", { className: "text-gray-500 max-w-sm mx-auto mb-6", children: "Be the first to review this product and help other customers make informed decisions." }), _jsx(Button, { onClick: () => {
|
|
500
418
|
if (!isAuthenticated) {
|
|
501
419
|
openLogin({ onSuccess: () => setIsReviewModalOpen(true) });
|
|
502
420
|
return;
|
|
503
421
|
}
|
|
504
422
|
setIsReviewModalOpen(true);
|
|
505
|
-
}, className: "bg-gray-900 hover:bg-gray-800 text-white", children: "Write
|
|
423
|
+
}, className: "bg-gray-900 hover:bg-gray-800 text-white", children: "Write a Review" })] }))] }) })), productDetailConfig?.showRelatedProducts !== false && relatedProducts.length > 0 && (_jsxs("div", { className: "mt-20 pt-16 border-t border-gray-200", children: [_jsx("h2", { className: "text-2xl lg:text-3xl font-bold text-gray-900 mb-8", children: "You Might Also Like" }), _jsx("div", { className: "grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-6", children: relatedProducts.map((relatedProduct) => (_jsx("div", { className: "h-[400px]", children: _jsx(ProductCard, { product: relatedProduct, storeSlug: storeConfig.slug, onAddToCart: (p) => addToCart(p, 1), onQuickView: handleQuickView, className: "h-full" }) }, relatedProduct.id))) })] }))] }), _jsx(Modal, { isOpen: isReviewModalOpen, onClose: () => setIsReviewModalOpen(false), title: "Write a Review", children: _jsxs("form", { onSubmit: handleSubmitReview, className: "space-y-4", children: [_jsxs("div", { children: [_jsx("label", { className: "block text-sm font-medium text-gray-700 mb-2", children: "Rating" }), _jsxs("div", { className: "flex items-center gap-1", children: [[1, 2, 3, 4, 5].map((star) => (_jsxs("div", { className: "relative group", children: [_jsx("button", { type: "button", onClick: () => setNewReview({ ...newReview, rating: star - 0.5 }), className: "absolute top-0 left-0 w-1/2 h-full z-20 focus:outline-none", title: `${star - 0.5} stars` }), _jsx("button", { type: "button", onClick: () => setNewReview({ ...newReview, rating: star }), className: "absolute top-0 right-0 w-1/2 h-full z-20 focus:outline-none", title: `${star} stars` }), _jsxs("div", { className: "relative pointer-events-none transition-transform group-hover:scale-110", children: [_jsx(Star, { className: "h-8 w-8 text-gray-200 fill-gray-200" }), newReview.rating >= star - 0.5 && (_jsx("div", { className: "absolute inset-0 overflow-hidden", style: { width: newReview.rating === star - 0.5 ? '50%' : '100%' }, children: _jsx(Star, { className: "h-8 w-8 text-gray-900 fill-gray-900" }) }))] })] }, star))), _jsx("span", { className: "ml-3 text-lg font-medium text-gray-900 w-12", children: newReview.rating })] })] }), _jsxs("div", { children: [_jsx("label", { htmlFor: "title", className: "block text-sm font-medium text-gray-700 mb-1", children: "Review Title" }), _jsx("input", { type: "text", id: "title", required: true, className: "w-full rounded-md border border-gray-300 px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-black", placeholder: "Summarize your thoughts", value: newReview.title, onChange: (e) => setNewReview({ ...newReview, title: e.target.value }) })] }), _jsxs("div", { children: [_jsx("label", { htmlFor: "comment", className: "block text-sm font-medium text-gray-700 mb-1", children: "Review" }), _jsx("textarea", { id: "comment", required: true, rows: 4, className: "w-full rounded-md border border-gray-300 px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-black", placeholder: "What did you like or dislike?", value: newReview.comment, onChange: (e) => setNewReview({ ...newReview, comment: e.target.value }) })] }), _jsxs("div", { className: "pt-4 flex justify-end gap-3", children: [_jsx(Button, { type: "button", variant: "outline", onClick: () => setIsReviewModalOpen(false), disabled: isSubmittingReview, children: "Cancel" }), _jsx(Button, { type: "submit", className: "bg-black text-white hover:bg-gray-800", disabled: isSubmittingReview, children: isSubmittingReview ? 'Submitting…' : 'Submit Review' })] })] }) }), _jsx(GuestCheckoutModal, { isOpen: showGuestModal, onClose: () => setShowGuestModal(false), onSubmit: handleGuestInfoSubmit, initialData: getGuestUserInfo() || undefined })] }));
|
|
506
424
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ProductsPage.d.ts","sourceRoot":"","sources":["../../../../src/layouts/clothing/pages/ProductsPage.tsx"],"names":[],"mappings":"AAEA,OAAO,EAAE,WAAW,EAAgB,MAAM,0BAA0B,CAAC;
|
|
1
|
+
{"version":3,"file":"ProductsPage.d.ts","sourceRoot":"","sources":["../../../../src/layouts/clothing/pages/ProductsPage.tsx"],"names":[],"mappings":"AAEA,OAAO,EAAE,WAAW,EAAgB,MAAM,0BAA0B,CAAC;AAkCrE,UAAU,iBAAiB;IACzB,WAAW,EAAE,WAAW,CAAC;CAC1B;AA4uBD,wBAAgB,YAAY,CAAC,KAAK,EAAE,iBAAiB,+BAMpD"}
|
|
@@ -20,6 +20,7 @@ import { filterActiveProducts } from '../../../lib/utils';
|
|
|
20
20
|
import { getAllCategoryIds, flattenCategoryTree, buildCategoryTree } from '../../../lib/utils/category-tree';
|
|
21
21
|
import { getBannerImage, getLayoutText } from '../../../lib/utils/asset-helpers';
|
|
22
22
|
import { shouldUseAPI } from '../../../lib/utils/demo-detection';
|
|
23
|
+
import { productCategoryFilterChipClass } from '../../shared/product-category-filter-chip';
|
|
23
24
|
import { useCatalogPriceRange } from '../../../hooks/use-catalog-price-range.js';
|
|
24
25
|
import { getApiPriceFilterActive, isWithinActivePriceRange } from '../../../lib/utils/catalog-price-bounds.js';
|
|
25
26
|
import { buildFilteredProductQuery, getFilteredProductQueryKey, productMatchesCategoryIds, productMatchesCategorySlug, } from '../../../lib/utils/filtered-product-query.js';
|
|
@@ -302,11 +303,7 @@ function ProductsPageContent({ storeConfig: initialConfig }) {
|
|
|
302
303
|
element?.scrollIntoView({ behavior: 'smooth' });
|
|
303
304
|
}, children: productsHeaderConfig?.primaryButtonText || primaryButtonText })), (productsHeaderConfig?.secondaryButtonText || secondaryButtonText) && (productsHeaderConfig?.secondaryButtonText || secondaryButtonText).trim() !== '' && (_jsx(Button, { className: `h-12 px-8 rounded-full text-base font-medium shadow-lg hover:shadow-xl transition-all duration-300 transform hover:-translate-y-1 ${(productsHeaderConfig?.image || categoryHeaderImage) ? 'bg-white/10 backdrop-blur-md text-white border border-white/30 hover:bg-white/20' : 'bg-gray-100 text-gray-900 hover:bg-gray-200 border border-gray-200'}`, onClick: () => {
|
|
304
305
|
window.location.href = `/${storeConfig.slug}/categories`;
|
|
305
|
-
}, children: productsHeaderConfig?.secondaryButtonText || secondaryButtonText }))] })] }) }) })] })), _jsxs("div", { "data-content-ready": true, className: "container mx-auto px-4 sm:px-6 lg:px-8 py-8", children: [_jsxs("div", { className: "flex gap-3 mb-8 overflow-x-auto pb-2 scrollbar-hide", children: [_jsx("button", { onClick: () => setSelectedCategoryFilter(null), className: `
|
|
306
|
-
? 'bg-gray-900 text-white hover:bg-blue-600'
|
|
307
|
-
: 'bg-gray-100 text-gray-700 hover:bg-blue-50 hover:text-blue-600'}`, children: "All" }), categories.map((category) => (_jsx("button", { onClick: () => setSelectedCategoryFilter(selectedCategoryFilter === category.slug ? null : category.slug), className: `px-6 py-2.5 rounded-lg text-sm font-semibold whitespace-nowrap transition-all ${selectedCategoryFilter === category.slug
|
|
308
|
-
? 'bg-gray-900 text-white hover:bg-blue-600'
|
|
309
|
-
: 'bg-gray-100 text-gray-700 hover:bg-blue-50 hover:text-blue-600'}`, children: category.name }, category.id)))] }), _jsxs("div", { id: "product-grid", className: "grid grid-cols-1 lg:grid-cols-4 gap-8", children: [_jsxs("aside", { className: "hidden lg:block col-span-1 space-y-8 sticky top-24 h-fit max-h-[calc(100vh-8rem)] overflow-y-auto pr-2 scrollbar-thin scrollbar-thumb-gray-200 scrollbar-track-transparent", children: [_jsxs("div", { className: "flex items-center justify-between pb-4 border-b border-gray-100", children: [_jsx("h2", { className: "text-xl font-bold text-gray-900", children: "Filters" }), hasActiveFilters && (_jsxs("button", { onClick: clearAllFilters, className: "text-xs font-medium text-red-500 hover:text-red-700 flex items-center gap-1", children: [_jsx(RotateCcw, { className: "h-3 w-3" }), " Reset"] }))] }), _jsxs("div", { children: [_jsx("h3", { className: "text-sm font-semibold text-gray-900 uppercase tracking-wide mb-4", children: "Availability" }), _jsxs("label", { className: "flex items-center gap-3 cursor-pointer group p-2 -ml-2 rounded-lg hover:bg-gray-50 transition-colors", children: [_jsx("div", { className: `w-5 h-5 rounded border flex items-center justify-center transition-colors ${showInStockOnly ? 'bg-black border-black' : 'border-gray-300 group-hover:border-black'}`, children: showInStockOnly && _jsx(Check, { className: "h-3 w-3 text-white" }) }), _jsx("input", { type: "checkbox", className: "hidden", checked: showInStockOnly, onChange: () => setShowInStockOnly(!showInStockOnly) }), _jsx("span", { className: "text-sm text-gray-700 font-medium", children: "In Stock Only" })] })] }), _jsxs("div", { children: [_jsxs("div", { className: "flex items-center justify-between mb-4", children: [_jsx("h3", { className: "text-sm font-semibold text-gray-900 uppercase tracking-wide", children: "Brand" }), selectedBrands.length > 0 && (_jsx("button", { onClick: () => setSelectedBrands([]), className: "text-xs text-gray-600 hover:text-blue-600 font-medium transition-colors", children: "Clear" }))] }), brands.length > 0 ? (_jsxs("div", { className: "space-y-2", children: [(showMoreBrands ? brands : brands.slice(0, 6)).map((brand) => (_jsx("label", { className: "flex items-center justify-between cursor-pointer group hover:bg-blue-50 hover:text-blue-600 p-2 rounded-lg -ml-2 -mr-2 transition-colors", children: _jsxs("div", { className: "flex items-center gap-3", children: [_jsx("input", { type: "checkbox", checked: selectedBrands.includes(brand), onChange: () => setSelectedBrands(prev => prev.includes(brand) ? prev.filter(b => b !== brand) : [...prev, brand]), className: "w-4 h-4 text-blue-600 border-gray-300 rounded focus:ring-blue-500" }), _jsx("span", { className: "text-sm text-gray-700 font-medium group-hover:text-blue-600", children: brand })] }) }, brand))), brands.length > 6 && (_jsx("button", { onClick: () => setShowMoreBrands(!showMoreBrands), className: "text-sm text-gray-600 hover:text-blue-600 font-medium mt-2 flex items-center gap-1 transition-colors", children: showMoreBrands ? _jsxs(_Fragment, { children: ["Show less ", _jsx(ChevronUp, { className: "h-4 w-4" })] }) : _jsxs(_Fragment, { children: ["Show more ", _jsx(ChevronDown, { className: "h-4 w-4" })] }) }))] })) : (_jsx("p", { className: "text-xs text-gray-500 italic", children: "No brands available" }))] }), _jsxs("div", { children: [_jsx("h3", { className: "text-sm font-semibold text-gray-900 uppercase tracking-wide mb-4", children: "Category" }), _jsx(CategoryTree, { categories: categories, selectedCategoryIds: selectedCategories, onCategoryToggle: (categoryId, isSelected) => {
|
|
306
|
+
}, children: productsHeaderConfig?.secondaryButtonText || secondaryButtonText }))] })] }) }) })] })), _jsxs("div", { "data-content-ready": true, className: "container mx-auto px-4 sm:px-6 lg:px-8 py-8", children: [_jsxs("div", { className: "flex gap-3 mb-8 overflow-x-auto pb-2 scrollbar-hide", children: [_jsx("button", { onClick: () => setSelectedCategoryFilter(null), className: productCategoryFilterChipClass(!selectedCategoryFilter), children: "All" }), categories.map((category) => (_jsx("button", { onClick: () => setSelectedCategoryFilter(selectedCategoryFilter === category.slug ? null : category.slug), className: productCategoryFilterChipClass(selectedCategoryFilter === category.slug), children: category.name }, category.id)))] }), _jsxs("div", { id: "product-grid", className: "grid grid-cols-1 lg:grid-cols-4 gap-8", children: [_jsxs("aside", { className: "hidden lg:block col-span-1 space-y-8 sticky top-24 h-fit max-h-[calc(100vh-8rem)] overflow-y-auto pr-2 scrollbar-thin scrollbar-thumb-gray-200 scrollbar-track-transparent", children: [_jsxs("div", { className: "flex items-center justify-between pb-4 border-b border-gray-100", children: [_jsx("h2", { className: "text-xl font-bold text-gray-900", children: "Filters" }), hasActiveFilters && (_jsxs("button", { onClick: clearAllFilters, className: "text-xs font-medium text-red-500 hover:text-red-700 flex items-center gap-1", children: [_jsx(RotateCcw, { className: "h-3 w-3" }), " Reset"] }))] }), _jsxs("div", { children: [_jsx("h3", { className: "text-sm font-semibold text-gray-900 uppercase tracking-wide mb-4", children: "Availability" }), _jsxs("label", { className: "flex items-center gap-3 cursor-pointer group p-2 -ml-2 rounded-lg hover:bg-gray-50 transition-colors", children: [_jsx("div", { className: `w-5 h-5 rounded border flex items-center justify-center transition-colors ${showInStockOnly ? 'bg-black border-black' : 'border-gray-300 group-hover:border-black'}`, children: showInStockOnly && _jsx(Check, { className: "h-3 w-3 text-white" }) }), _jsx("input", { type: "checkbox", className: "hidden", checked: showInStockOnly, onChange: () => setShowInStockOnly(!showInStockOnly) }), _jsx("span", { className: "text-sm text-gray-700 font-medium", children: "In Stock Only" })] })] }), _jsxs("div", { children: [_jsxs("div", { className: "flex items-center justify-between mb-4", children: [_jsx("h3", { className: "text-sm font-semibold text-gray-900 uppercase tracking-wide", children: "Brand" }), selectedBrands.length > 0 && (_jsx("button", { onClick: () => setSelectedBrands([]), className: "text-xs text-gray-600 hover:text-blue-600 font-medium transition-colors", children: "Clear" }))] }), brands.length > 0 ? (_jsxs("div", { className: "space-y-2", children: [(showMoreBrands ? brands : brands.slice(0, 6)).map((brand) => (_jsx("label", { className: "flex items-center justify-between cursor-pointer group hover:bg-blue-50 hover:text-blue-600 p-2 rounded-lg -ml-2 -mr-2 transition-colors", children: _jsxs("div", { className: "flex items-center gap-3", children: [_jsx("input", { type: "checkbox", checked: selectedBrands.includes(brand), onChange: () => setSelectedBrands(prev => prev.includes(brand) ? prev.filter(b => b !== brand) : [...prev, brand]), className: "w-4 h-4 text-blue-600 border-gray-300 rounded focus:ring-blue-500" }), _jsx("span", { className: "text-sm text-gray-700 font-medium group-hover:text-blue-600", children: brand })] }) }, brand))), brands.length > 6 && (_jsx("button", { onClick: () => setShowMoreBrands(!showMoreBrands), className: "text-sm text-gray-600 hover:text-blue-600 font-medium mt-2 flex items-center gap-1 transition-colors", children: showMoreBrands ? _jsxs(_Fragment, { children: ["Show less ", _jsx(ChevronUp, { className: "h-4 w-4" })] }) : _jsxs(_Fragment, { children: ["Show more ", _jsx(ChevronDown, { className: "h-4 w-4" })] }) }))] })) : (_jsx("p", { className: "text-xs text-gray-500 italic", children: "No brands available" }))] }), _jsxs("div", { children: [_jsx("h3", { className: "text-sm font-semibold text-gray-900 uppercase tracking-wide mb-4", children: "Category" }), _jsx(CategoryTree, { categories: categories, selectedCategoryIds: selectedCategories, onCategoryToggle: (categoryId, isSelected) => {
|
|
310
307
|
setSelectedCategories(prev => isSelected
|
|
311
308
|
? [...prev, categoryId]
|
|
312
309
|
: prev.filter(id => id !== categoryId));
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ClothingHomePageMinimal.d.ts","sourceRoot":"","sources":["../../../../src/layouts/clothing-minimal/components/ClothingHomePageMinimal.tsx"],"names":[],"mappings":"AAIA,OAAO,EAAE,WAAW,EAAgB,MAAM,0BAA0B,CAAC;AAkBrE,UAAU,4BAA4B;IACpC,WAAW,EAAE,WAAW,CAAC;CAC1B;AAED,wBAAgB,uBAAuB,CAAC,EAAE,WAAW,EAAE,aAAa,EAAE,EAAE,4BAA4B,+
|
|
1
|
+
{"version":3,"file":"ClothingHomePageMinimal.d.ts","sourceRoot":"","sources":["../../../../src/layouts/clothing-minimal/components/ClothingHomePageMinimal.tsx"],"names":[],"mappings":"AAIA,OAAO,EAAE,WAAW,EAAgB,MAAM,0BAA0B,CAAC;AAkBrE,UAAU,4BAA4B;IACpC,WAAW,EAAE,WAAW,CAAC;CAC1B;AAED,wBAAgB,uBAAuB,CAAC,EAAE,WAAW,EAAE,aAAa,EAAE,EAAE,4BAA4B,+BAkWnG"}
|