feat: product page layout toggle (flat/card), fix email shortcode rendering
- Add layout_style setting (flat default) to product appearance
- AppearanceController: sanitize & persist layout_style, add to default settings
- Admin SPA: Layout Style select in Appearance > Product
- Customer SPA: useEffect targets <main> bg-white in flat mode (full-width),
card mode uses per-section white floating cards on gray background
- Accordion sections styled per mode: flat=border-t dividers, card=white cards
- Fix email shortcode gaps (EmailRenderer, EmailManager)
- Add missing variables: return_url, contact_url, account_url (alias),
payment_error_reason, order_items_list (alias for order_items_table)
- Fix customer_note extra_data key mismatch (note → customer_note)
- Pass low_stock_threshold via extra_data in low_stock email send
This commit is contained in:
@@ -29,6 +29,20 @@ export default function Product() {
|
||||
const { isEnabled: wishlistEnabled, isInWishlist, toggleWishlist, isLoggedIn } = useWishlist();
|
||||
const { isEnabled: isModuleEnabled } = useModules();
|
||||
|
||||
// Apply white background to <main> in flat mode so the full viewport width is white
|
||||
useEffect(() => {
|
||||
const main = document.querySelector('main');
|
||||
if (!main) return;
|
||||
if (layout.layout_style === 'flat') {
|
||||
(main as HTMLElement).style.backgroundColor = '#ffffff';
|
||||
} else {
|
||||
(main as HTMLElement).style.backgroundColor = '';
|
||||
}
|
||||
return () => {
|
||||
(main as HTMLElement).style.backgroundColor = '';
|
||||
};
|
||||
}, [layout.layout_style]);
|
||||
|
||||
// Fetch product details by slug
|
||||
const { data: product, isLoading, error } = useQuery<ProductType | null>({
|
||||
queryKey: ['product', slug],
|
||||
@@ -94,10 +108,16 @@ export default function Product() {
|
||||
// Find matching variation when attributes change
|
||||
useEffect(() => {
|
||||
if (product?.type === 'variable' && product.variations && Object.keys(selectedAttributes).length > 0) {
|
||||
const variation = (product.variations as any[]).find(v => {
|
||||
if (!v.attributes) return false;
|
||||
let bestMatch: any = null;
|
||||
let highestScore = -1;
|
||||
|
||||
return Object.entries(selectedAttributes).every(([attrName, attrValue]) => {
|
||||
(product.variations as any[]).forEach(v => {
|
||||
if (!v.attributes) return;
|
||||
|
||||
let isMatch = true;
|
||||
let score = 0;
|
||||
|
||||
const attributesMatch = Object.entries(selectedAttributes).every(([attrName, attrValue]) => {
|
||||
const normalizedSelectedValue = attrValue.toLowerCase().trim();
|
||||
const attrNameLower = attrName.toLowerCase();
|
||||
|
||||
@@ -108,17 +128,11 @@ export default function Product() {
|
||||
// Try to find a matching key in the variation attributes
|
||||
let variationValue: string | undefined = undefined;
|
||||
|
||||
// Check for common WooCommerce attribute key formats
|
||||
// 1. Check strict slug format (attribute_7-days-...)
|
||||
if (`attribute_${attrSlug}` in v.attributes) {
|
||||
variationValue = v.attributes[`attribute_${attrSlug}`];
|
||||
}
|
||||
// 2. Check pa_ format (attribute_pa_color)
|
||||
else if (`attribute_pa_${attrSlug}` in v.attributes) {
|
||||
} else if (`attribute_pa_${attrSlug}` in v.attributes) {
|
||||
variationValue = v.attributes[`attribute_pa_${attrSlug}`];
|
||||
}
|
||||
// 3. Fallback to name-based checks (legacy)
|
||||
else if (`attribute_${attrNameLower}` in v.attributes) {
|
||||
} else if (`attribute_${attrNameLower}` in v.attributes) {
|
||||
variationValue = v.attributes[`attribute_${attrNameLower}`];
|
||||
} else if (`attribute_pa_${attrNameLower}` in v.attributes) {
|
||||
variationValue = v.attributes[`attribute_pa_${attrNameLower}`];
|
||||
@@ -126,23 +140,34 @@ export default function Product() {
|
||||
variationValue = v.attributes[attrNameLower];
|
||||
}
|
||||
|
||||
// If key is undefined/missing in variation, it means "Any" -> Match
|
||||
// If key is undefined/missing in variation, it means "Any" -> Match with score 0
|
||||
if (variationValue === undefined || variationValue === null) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// If empty string, it also means "Any" -> Match
|
||||
// If empty string, it also means "Any" -> Match with score 0
|
||||
const normalizedVarValue = String(variationValue).toLowerCase().trim();
|
||||
if (normalizedVarValue === '') {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Otherwise, values must match
|
||||
return normalizedVarValue === normalizedSelectedValue;
|
||||
// Exact match gets a higher score
|
||||
if (normalizedVarValue === normalizedSelectedValue) {
|
||||
score += 1;
|
||||
return true;
|
||||
}
|
||||
|
||||
// Value mismatch
|
||||
return false;
|
||||
});
|
||||
|
||||
if (attributesMatch && score > highestScore) {
|
||||
highestScore = score;
|
||||
bestMatch = v;
|
||||
}
|
||||
});
|
||||
|
||||
setSelectedVariation(variation || null);
|
||||
setSelectedVariation(bestMatch || null);
|
||||
} else if (product?.type !== 'variable') {
|
||||
setSelectedVariation(null);
|
||||
}
|
||||
@@ -317,357 +342,364 @@ export default function Product() {
|
||||
availability: stockStatus === 'instock' ? 'in stock' : 'out of stock',
|
||||
}}
|
||||
/>
|
||||
<div className="max-w-6xl mx-auto py-8">
|
||||
{/* Breadcrumb */}
|
||||
{elements.breadcrumbs && (
|
||||
<nav className="mb-6 text-sm">
|
||||
<Link to="/shop" className="text-gray-600 hover:text-gray-900">
|
||||
Shop
|
||||
</Link>
|
||||
<span className="mx-2 text-gray-400">/</span>
|
||||
<span className="text-gray-900">{product.name}</span>
|
||||
</nav>
|
||||
)}
|
||||
{/* Flat: entire Container is bg-white. Card: per-section white cards on gray. */}
|
||||
<div className="max-w-6xl mx-auto">
|
||||
{/* Top section: flat = no card wrapper, card = white card */}
|
||||
<div className={layout.layout_style === 'card' ? 'bg-white rounded-2xl shadow-sm border border-gray-100 p-6 lg:p-8 xl:p-10 mb-8' : 'mb-8'}>
|
||||
{/* Breadcrumb */}
|
||||
{elements.breadcrumbs && (
|
||||
<nav className="mb-6 text-sm">
|
||||
<Link to="/shop" className="text-gray-600 hover:text-gray-900">
|
||||
Shop
|
||||
</Link>
|
||||
<span className="mx-2 text-gray-400">/</span>
|
||||
<span className="text-gray-900">{product.name}</span>
|
||||
</nav>
|
||||
)}
|
||||
|
||||
<div className={`grid gap-6 lg:gap-12 ${layout.image_position === 'right' ? 'lg:grid-cols-[42%_58%]' : 'lg:grid-cols-[58%_42%]'}`}>
|
||||
{/* Product Images */}
|
||||
<div className={`lg:sticky lg:top-8 lg:self-start ${layout.image_position === 'right' ? 'lg:order-2' : ''}`}>
|
||||
{/* Main Image - ENHANCED */}
|
||||
<div className="relative w-full aspect-square rounded-2xl overflow-hidden bg-gray-50 mb-6">
|
||||
{selectedImage ? (
|
||||
<img
|
||||
src={selectedImage}
|
||||
alt={product.name}
|
||||
className="w-full !h-full object-contain p-8"
|
||||
/>
|
||||
) : (
|
||||
<div className="!h-full flex items-center justify-center text-gray-400">
|
||||
<div className="text-center">
|
||||
<svg className="w-24 h-24 mx-auto mb-4 text-gray-300" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z" />
|
||||
</svg>
|
||||
<p className="text-sm">No image available</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{/* Sale Badge on Image */}
|
||||
{isOnSale && (
|
||||
<div className="absolute top-6 left-6 bg-red-500 text-white px-4 py-2 rounded-full font-bold text-xs uppercase tracking-wider shadow-xl">
|
||||
Sale
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Dots Navigation - Show based on gallery_style */}
|
||||
{allImages && allImages.length > 1 && layout.gallery_style === 'dots' && (
|
||||
<div className="flex justify-center gap-2 mt-4">
|
||||
<div className="flex gap-2">
|
||||
{allImages.map((img, index) => (
|
||||
<button
|
||||
key={index}
|
||||
onClick={() => setSelectedImage(img)}
|
||||
className={`w-2 h-2 rounded-full transition-all ${selectedImage === img
|
||||
? 'bg-primary w-6'
|
||||
: 'bg-gray-300 hover:bg-gray-400'
|
||||
}`}
|
||||
aria-label={`View image ${index + 1}`}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Thumbnail Slider - Show based on gallery_style */}
|
||||
{allImages && allImages.length > 1 && layout.gallery_style === 'thumbnails' && (
|
||||
<div className="relative w-full overflow-hidden">
|
||||
{/* Left Arrow */}
|
||||
{allImages.length > 4 && (
|
||||
<button
|
||||
onClick={() => scrollThumbnails('left')}
|
||||
className="absolute left-0 top-1/2 -translate-y-1/2 z-10 bg-white shadow-xl rounded-full p-2.5 hover:bg-gray-50 transition-all border-2 border-gray-200"
|
||||
>
|
||||
<ChevronLeft className="h-5 w-5" />
|
||||
</button>
|
||||
)}
|
||||
|
||||
{/* Scrollable Thumbnails */}
|
||||
<div
|
||||
ref={thumbnailsRef}
|
||||
className="flex gap-3 overflow-x-auto scroll-smooth scrollbar-hide px-10"
|
||||
style={{ scrollbarWidth: 'none', msOverflowStyle: 'none' }}
|
||||
>
|
||||
{allImages.map((img, index) => (
|
||||
<button
|
||||
key={index}
|
||||
onClick={() => setSelectedImage(img)}
|
||||
className={`flex-shrink-0 w-24 h-24 md:w-28 md:h-28 rounded-lg overflow-hidden border-2 transition-all shadow-md hover:shadow-lg ${selectedImage === img
|
||||
? 'border-primary ring-4 ring-primary ring-offset-2'
|
||||
: 'border-gray-300 hover:border-gray-400'
|
||||
}`}
|
||||
>
|
||||
<img
|
||||
src={img}
|
||||
alt={`${product.name} ${index + 1}`}
|
||||
className="w-full !h-full object-cover"
|
||||
/>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Right Arrow */}
|
||||
{allImages.length > 4 && (
|
||||
<button
|
||||
onClick={() => scrollThumbnails('right')}
|
||||
className="absolute right-0 top-1/2 -translate-y-1/2 z-10 bg-white shadow-xl rounded-full p-2.5 hover:bg-gray-50 transition-all border-2 border-gray-200"
|
||||
>
|
||||
<ChevronRight className="h-5 w-5" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Product Info */}
|
||||
<div>
|
||||
{/* Product Title - PRIMARY HIERARCHY - SERIF FONT */}
|
||||
<h1 className="text-2xl md:text-3xl lg:text-4xl font-serif font-light mb-4 leading-tight text-gray-900">{product.name}</h1>
|
||||
|
||||
{/* Price - SECONDARY (per UI/UX Guide) */}
|
||||
<div className="mb-6">
|
||||
{isOnSale && regularPrice ? (
|
||||
<div className="flex items-center gap-3 flex-wrap">
|
||||
<span className="text-3xl font-bold text-gray-900">
|
||||
{formatPrice(currentPrice)}
|
||||
</span>
|
||||
<span className="text-xl text-gray-400 line-through ml-3">
|
||||
{formatPrice(regularPrice)}
|
||||
</span>
|
||||
<span className="inline-block bg-red-50 text-red-600 px-3 py-1 rounded-md text-sm font-semibold ml-3">
|
||||
Save {Math.round((1 - parseFloat(currentPrice) / parseFloat(regularPrice)) * 100)}%
|
||||
</span>
|
||||
</div>
|
||||
) : (
|
||||
<span className="text-3xl font-bold text-gray-900">{formatPrice(currentPrice)}</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Stock Status Badge */}
|
||||
<div className="mb-6">
|
||||
{stockStatus === 'instock' ? (
|
||||
<div className="inline-flex items-center gap-2 text-green-700 text-sm font-medium">
|
||||
<svg className="w-5 h-5" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fillRule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clipRule="evenodd" />
|
||||
</svg>
|
||||
<span>In Stock • Ships Today</span>
|
||||
</div>
|
||||
) : (
|
||||
<div className="inline-flex items-center gap-2 text-red-700 text-sm font-medium">
|
||||
<svg className="w-5 h-5" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fillRule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clipRule="evenodd" />
|
||||
</svg>
|
||||
<span>Out of Stock</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Short Description */}
|
||||
{product.short_description && (
|
||||
<div
|
||||
className="prose prose-sm text-gray-600 leading-relaxed mb-6 border-l-4 border-gray-200 pl-4"
|
||||
dangerouslySetInnerHTML={{ __html: product.short_description }}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Variation Selector - PILLS (per UI/UX Guide) */}
|
||||
{product.type === 'variable' && product.attributes && product.attributes.length > 0 && (
|
||||
<div className="mb-6 space-y-4">
|
||||
{product.attributes.map((attr: any, index: number) => (
|
||||
attr.variation && (
|
||||
<div key={index}>
|
||||
<label className="block font-medium mb-3 text-sm text-gray-700 uppercase tracking-wider">{attr.name}</label>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{attr.options && attr.options.map((option: string, optIndex: number) => {
|
||||
const isSelected = selectedAttributes[attr.name] === option;
|
||||
return (
|
||||
<button
|
||||
key={optIndex}
|
||||
onClick={() => handleAttributeChange(attr.name, option)}
|
||||
className={`min-w-[48px] min-h-[48px] px-5 py-3 rounded-xl border-2 font-medium transition-all ${isSelected
|
||||
? 'bg-gray-900 text-white border-gray-900 shadow-lg'
|
||||
: 'bg-white text-gray-700 border-gray-200 hover:border-gray-400 hover:shadow-md'
|
||||
}`}
|
||||
>
|
||||
{option}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<div className={`grid gap-6 lg:gap-8 ${layout.image_position === 'right' ? 'lg:grid-cols-[5fr_7fr]' : 'lg:grid-cols-[7fr_5fr]'}`}>
|
||||
{/* Product Images */}
|
||||
<div className={`lg:sticky lg:top-8 lg:self-start ${layout.image_position === 'right' ? 'lg:order-2' : ''}`}>
|
||||
{/* Main Image - ENHANCED */}
|
||||
<div className="relative w-full aspect-square rounded-2xl overflow-hidden bg-gray-50 mb-6">
|
||||
{selectedImage ? (
|
||||
<img
|
||||
src={selectedImage}
|
||||
alt={product.name}
|
||||
className="w-full !h-full object-contain p-8"
|
||||
/>
|
||||
) : (
|
||||
<div className="!h-full flex items-center justify-center text-gray-400">
|
||||
<div className="text-center">
|
||||
<svg className="w-24 h-24 mx-auto mb-4 text-gray-300" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z" />
|
||||
</svg>
|
||||
<p className="text-sm">No image available</p>
|
||||
</div>
|
||||
)
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Quantity & Add to Cart */}
|
||||
{stockStatus === 'instock' && (
|
||||
<div className="space-y-4 mb-6">
|
||||
{/* Quantity Selector */}
|
||||
<div className="flex items-center gap-3">
|
||||
<span className="text-sm font-medium text-gray-700 uppercase tracking-wider">Quantity</span>
|
||||
<div className="flex items-center border-2 border-gray-200 rounded-xl">
|
||||
<button
|
||||
onClick={() => setQuantity(Math.max(1, quantity - 1))}
|
||||
className="p-2.5 hover:bg-gray-100 transition-colors rounded-l-md"
|
||||
>
|
||||
<Minus className="h-4 w-4" />
|
||||
</button>
|
||||
<input
|
||||
type="number"
|
||||
min="1"
|
||||
value={quantity}
|
||||
onChange={(e) => setQuantity(Math.max(1, parseInt(e.target.value) || 1))}
|
||||
className="w-14 text-center border-x-2 border-gray-200 focus:outline-none font-semibold"
|
||||
/>
|
||||
<button
|
||||
onClick={() => setQuantity(quantity + 1)}
|
||||
className="p-2.5 hover:bg-gray-100 transition-colors rounded-r-md"
|
||||
>
|
||||
<Plus className="h-4 w-4" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Action Buttons - PROMINENT */}
|
||||
{/* Add to Cart Button */}
|
||||
<button
|
||||
onClick={handleAddToCart}
|
||||
className="w-full h-14 flex items-center justify-center gap-2 bg-gray-900 text-white rounded-xl font-semibold text-base hover:bg-gray-800 transition-all shadow-lg hover:shadow-xl"
|
||||
>
|
||||
<ShoppingCart className="h-5 w-5" />
|
||||
Add to Cart
|
||||
</button>
|
||||
{isModuleEnabled('wishlist') && wishlistEnabled && (
|
||||
<button
|
||||
onClick={() => product && toggleWishlist(product.id)}
|
||||
className={`w-full h-14 flex items-center justify-center gap-2 rounded-xl font-semibold text-base border-2 transition-all ${product && isInWishlist(product.id)
|
||||
? 'bg-red-50 text-red-600 border-red-200 hover:border-red-400'
|
||||
: 'bg-white text-gray-900 border-gray-200 hover:border-gray-400'
|
||||
}`}
|
||||
>
|
||||
<Heart className={`h-5 w-5 ${product && isInWishlist(product.id) ? 'fill-red-500' : ''
|
||||
}`} />
|
||||
{product && isInWishlist(product.id) ? 'Remove from Wishlist' : 'Add to Wishlist'}
|
||||
</button>
|
||||
)}
|
||||
{/* Sale Badge on Image */}
|
||||
{isOnSale && (
|
||||
<div className="absolute top-6 left-6 bg-red-500 text-white px-4 py-2 rounded-full font-bold text-xs uppercase tracking-wider shadow-xl">
|
||||
Sale
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Trust Badges - REDESIGNED */}
|
||||
<div className="grid grid-cols-3 gap-4 py-6 border-y border-gray-200">
|
||||
{/* Free Shipping */}
|
||||
<div className="flex flex-col items-center text-center">
|
||||
<div className="w-12 h-12 rounded-full bg-green-50 flex items-center justify-center mb-2">
|
||||
<svg className="w-6 h-6 text-green-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 8h14M5 8a2 2 0 110-4h14a2 2 0 110 4M5 8v10a2 2 0 002 2h10a2 2 0 002-2V8m-9 4h4" />
|
||||
</svg>
|
||||
{/* Dots Navigation - Show based on gallery_style */}
|
||||
{allImages && allImages.length > 1 && layout.gallery_style === 'dots' && (
|
||||
<div className="flex justify-center gap-2 mt-4">
|
||||
<div className="flex gap-2">
|
||||
{allImages.map((img, index) => (
|
||||
<button
|
||||
key={index}
|
||||
onClick={() => setSelectedImage(img)}
|
||||
className={`w-2 h-2 rounded-full transition-all ${selectedImage === img
|
||||
? 'bg-primary w-6'
|
||||
: 'bg-gray-300 hover:bg-gray-400'
|
||||
}`}
|
||||
aria-label={`View image ${index + 1}`}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<p className="font-medium text-sm text-gray-900">Free Shipping</p>
|
||||
<p className="text-xs text-gray-500 mt-1">On orders over $50</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Returns */}
|
||||
<div className="flex flex-col items-center text-center">
|
||||
<div className="w-12 h-12 rounded-full bg-blue-50 flex items-center justify-center mb-2">
|
||||
<svg className="w-6 h-6 text-blue-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
|
||||
</svg>
|
||||
</div>
|
||||
<p className="font-medium text-sm text-gray-900">Easy Returns</p>
|
||||
<p className="text-xs text-gray-500 mt-1">30-day guarantee</p>
|
||||
</div>
|
||||
{/* Thumbnail Slider - Show based on gallery_style */}
|
||||
{allImages && allImages.length > 1 && layout.gallery_style === 'thumbnails' && (
|
||||
<div className="relative w-full overflow-hidden">
|
||||
{/* Left Arrow */}
|
||||
{allImages.length > 4 && (
|
||||
<button
|
||||
onClick={() => scrollThumbnails('left')}
|
||||
className="absolute left-0 top-1/2 -translate-y-1/2 z-10 bg-white shadow-xl rounded-full p-2.5 hover:bg-gray-50 transition-all border-2 border-gray-200"
|
||||
>
|
||||
<ChevronLeft className="h-5 w-5" />
|
||||
</button>
|
||||
)}
|
||||
|
||||
{/* Secure */}
|
||||
<div className="flex flex-col items-center text-center">
|
||||
<div className="w-12 h-12 rounded-full bg-purple-50 flex items-center justify-center mb-2">
|
||||
<svg className="w-6 h-6 text-purple-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z" />
|
||||
</svg>
|
||||
{/* Scrollable Thumbnails */}
|
||||
<div
|
||||
ref={thumbnailsRef}
|
||||
className="flex gap-3 overflow-x-auto scroll-smooth scrollbar-hide px-10"
|
||||
style={{ scrollbarWidth: 'none', msOverflowStyle: 'none' }}
|
||||
>
|
||||
{allImages.map((img, index) => (
|
||||
<button
|
||||
key={index}
|
||||
onClick={() => setSelectedImage(img)}
|
||||
className={`flex-shrink-0 w-24 h-24 md:w-28 md:h-28 rounded-lg overflow-hidden border-2 transition-all shadow-md hover:shadow-lg ${selectedImage === img
|
||||
? 'border-primary ring-4 ring-primary ring-offset-2'
|
||||
: 'border-gray-300 hover:border-gray-400'
|
||||
}`}
|
||||
>
|
||||
<img
|
||||
src={img}
|
||||
alt={`${product.name} ${index + 1}`}
|
||||
className="w-full !h-full object-cover"
|
||||
/>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Right Arrow */}
|
||||
{allImages.length > 4 && (
|
||||
<button
|
||||
onClick={() => scrollThumbnails('right')}
|
||||
className="absolute right-0 top-1/2 -translate-y-1/2 z-10 bg-white shadow-xl rounded-full p-2.5 hover:bg-gray-50 transition-all border-2 border-gray-200"
|
||||
>
|
||||
<ChevronRight className="h-5 w-5" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
<p className="font-medium text-sm text-gray-900">Secure Payment</p>
|
||||
<p className="text-xs text-gray-500 mt-1">SSL encrypted</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Product Meta */}
|
||||
{elements.product_meta && (
|
||||
<div className="space-y-2 text-sm border-t pt-4 border-gray-200">
|
||||
{product.sku && (
|
||||
<div className="flex gap-2">
|
||||
<span className="text-gray-600">SKU:</span>
|
||||
<span className="font-medium">{product.sku}</span>
|
||||
</div>
|
||||
)}
|
||||
{product.categories && product.categories.length > 0 && (
|
||||
<div className="flex gap-2">
|
||||
<span className="text-gray-600">Categories:</span>
|
||||
<span className="font-medium">
|
||||
{product.categories.map((cat: any) => cat.name).join(', ')}
|
||||
{/* Product Info */}
|
||||
<div>
|
||||
{/* Product Title - PRIMARY HIERARCHY - SERIF FONT */}
|
||||
<h1 className="text-2xl md:text-3xl lg:text-4xl font-serif font-light mb-4 leading-tight text-gray-900">{product.name}</h1>
|
||||
|
||||
{/* Price - SECONDARY (per UI/UX Guide) */}
|
||||
<div className="mb-6">
|
||||
{isOnSale && regularPrice ? (
|
||||
<div className="flex items-center gap-3 flex-wrap">
|
||||
<span className="text-3xl font-bold text-gray-900">
|
||||
{formatPrice(currentPrice)}
|
||||
</span>
|
||||
<span className="text-xl text-gray-400 line-through ml-3">
|
||||
{formatPrice(regularPrice)}
|
||||
</span>
|
||||
<span className="inline-block bg-red-50 text-red-600 px-3 py-1 rounded-md text-sm font-semibold ml-3">
|
||||
Save {Math.round((1 - parseFloat(currentPrice) / parseFloat(regularPrice)) * 100)}%
|
||||
</span>
|
||||
</div>
|
||||
) : (
|
||||
<span className="text-3xl font-bold text-gray-900">{formatPrice(currentPrice)}</span>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Share Buttons */}
|
||||
{elements.share_buttons && (
|
||||
<div className="flex items-center gap-3 pt-4 border-t border-gray-200">
|
||||
<span className="text-sm text-gray-600 font-medium">Share:</span>
|
||||
<div className="flex gap-2">
|
||||
{/* Stock Status Badge */}
|
||||
<div className="mb-6">
|
||||
{stockStatus === 'instock' ? (
|
||||
<div className="inline-flex items-center gap-2 text-green-700 text-sm font-medium">
|
||||
<svg className="w-5 h-5" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fillRule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clipRule="evenodd" />
|
||||
</svg>
|
||||
<span>In Stock • Ships Today</span>
|
||||
</div>
|
||||
) : (
|
||||
<div className="inline-flex items-center gap-2 text-red-700 text-sm font-medium">
|
||||
<svg className="w-5 h-5" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fillRule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clipRule="evenodd" />
|
||||
</svg>
|
||||
<span>Out of Stock</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Short Description */}
|
||||
{product.short_description && (
|
||||
<div
|
||||
className="prose prose-sm text-gray-600 leading-relaxed mb-6 border-l-4 border-gray-200 pl-4"
|
||||
dangerouslySetInnerHTML={{ __html: product.short_description }}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Variation Selector - PILLS (per UI/UX Guide) */}
|
||||
{product.type === 'variable' && product.attributes && product.attributes.length > 0 && (
|
||||
<div className="mb-6 space-y-4">
|
||||
{product.attributes.map((attr: any, index: number) => (
|
||||
attr.variation && (
|
||||
<div key={index}>
|
||||
<label className="block font-medium mb-3 text-sm text-gray-700 uppercase tracking-wider">{attr.name}</label>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{attr.options && attr.options.map((option: string, optIndex: number) => {
|
||||
const isSelected = selectedAttributes[attr.name] === option;
|
||||
return (
|
||||
<button
|
||||
key={optIndex}
|
||||
onClick={() => handleAttributeChange(attr.name, option)}
|
||||
className={`min-w-[48px] min-h-[48px] px-5 py-3 rounded-xl border-2 font-medium transition-all ${isSelected
|
||||
? 'bg-gray-900 text-white border-gray-900 shadow-lg'
|
||||
: 'bg-white text-gray-700 border-gray-200 hover:border-gray-400 hover:shadow-md'
|
||||
}`}
|
||||
>
|
||||
{option}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Quantity & Add to Cart */}
|
||||
{stockStatus === 'instock' && (
|
||||
<div className="space-y-4 mb-6">
|
||||
{/* Quantity Selector */}
|
||||
<div className="flex items-center gap-3">
|
||||
<span className="text-sm font-medium text-gray-700 uppercase tracking-wider">Quantity</span>
|
||||
<div className="flex items-center border-2 border-gray-200 rounded-xl">
|
||||
<button
|
||||
onClick={() => setQuantity(Math.max(1, quantity - 1))}
|
||||
className="p-2.5 hover:bg-gray-100 transition-colors rounded-l-md"
|
||||
>
|
||||
<Minus className="h-4 w-4" />
|
||||
</button>
|
||||
<input
|
||||
type="number"
|
||||
min="1"
|
||||
value={quantity}
|
||||
onChange={(e) => setQuantity(Math.max(1, parseInt(e.target.value) || 1))}
|
||||
className="w-14 text-center border-x-2 border-gray-200 focus:outline-none font-semibold"
|
||||
/>
|
||||
<button
|
||||
onClick={() => setQuantity(quantity + 1)}
|
||||
className="p-2.5 hover:bg-gray-100 transition-colors rounded-r-md"
|
||||
>
|
||||
<Plus className="h-4 w-4" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Action Buttons - PROMINENT */}
|
||||
{/* Add to Cart Button */}
|
||||
<button
|
||||
onClick={() => {
|
||||
const url = encodeURIComponent(window.location.href);
|
||||
window.open(`https://www.facebook.com/sharer/sharer.php?u=${url}`, '_blank', 'width=600,height=400');
|
||||
}}
|
||||
className="w-9 h-9 rounded-full bg-blue-600 hover:bg-blue-700 text-white flex items-center justify-center transition-colors"
|
||||
title="Share on Facebook"
|
||||
onClick={handleAddToCart}
|
||||
className="w-full h-14 flex items-center justify-center gap-2 bg-gray-900 text-white rounded-xl font-semibold text-base hover:bg-gray-800 transition-all shadow-lg hover:shadow-xl"
|
||||
>
|
||||
<svg className="w-4 h-4" fill="currentColor" viewBox="0 0 24 24"><path d="M24 12.073c0-6.627-5.373-12-12-12s-12 5.373-12 12c0 5.99 4.388 10.954 10.125 11.854v-8.385H7.078v-3.47h3.047V9.43c0-3.007 1.792-4.669 4.533-4.669 1.312 0 2.686.235 2.686.235v2.953H15.83c-1.491 0-1.956.925-1.956 1.874v2.25h3.328l-.532 3.47h-2.796v8.385C19.612 23.027 24 18.062 24 12.073z" /></svg>
|
||||
</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
const url = encodeURIComponent(window.location.href);
|
||||
const text = encodeURIComponent(product.name);
|
||||
window.open(`https://twitter.com/intent/tweet?url=${url}&text=${text}`, '_blank', 'width=600,height=400');
|
||||
}}
|
||||
className="w-9 h-9 rounded-full bg-sky-500 hover:bg-sky-600 text-white flex items-center justify-center transition-colors"
|
||||
title="Share on Twitter"
|
||||
>
|
||||
<svg className="w-4 h-4" fill="currentColor" viewBox="0 0 24 24"><path d="M23.953 4.57a10 10 0 01-2.825.775 4.958 4.958 0 002.163-2.723c-.951.555-2.005.959-3.127 1.184a4.92 4.92 0 00-8.384 4.482C7.69 8.095 4.067 6.13 1.64 3.162a4.822 4.822 0 00-.666 2.475c0 1.71.87 3.213 2.188 4.096a4.904 4.904 0 01-2.228-.616v.06a4.923 4.923 0 003.946 4.827 4.996 4.996 0 01-2.212.085 4.936 4.936 0 004.604 3.417 9.867 9.867 0 01-6.102 2.105c-.39 0-.779-.023-1.17-.067a13.995 13.995 0 007.557 2.209c9.053 0 13.998-7.496 13.998-13.985 0-.21 0-.42-.015-.63A9.935 9.935 0 0024 4.59z" /></svg>
|
||||
</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
const url = encodeURIComponent(window.location.href);
|
||||
const text = encodeURIComponent(product.name);
|
||||
window.open(`https://wa.me/?text=${text}%20${url}`, '_blank');
|
||||
}}
|
||||
className="w-9 h-9 rounded-full bg-green-600 hover:bg-green-700 text-white flex items-center justify-center transition-colors"
|
||||
title="Share on WhatsApp"
|
||||
>
|
||||
<svg className="w-4 h-4" fill="currentColor" viewBox="0 0 24 24"><path d="M17.472 14.382c-.297-.149-1.758-.867-2.03-.967-.273-.099-.471-.148-.67.15-.197.297-.767.966-.94 1.164-.173.199-.347.223-.644.075-.297-.15-1.255-.463-2.39-1.475-.883-.788-1.48-1.761-1.653-2.059-.173-.297-.018-.458.13-.606.134-.133.298-.347.446-.52.149-.174.198-.298.298-.497.099-.198.05-.371-.025-.52-.075-.149-.669-1.612-.916-2.207-.242-.579-.487-.5-.669-.51-.173-.008-.371-.01-.57-.01-.198 0-.52.074-.792.372-.272.297-1.04 1.016-1.04 2.479 0 1.462 1.065 2.875 1.213 3.074.149.198 2.096 3.2 5.077 4.487.709.306 1.262.489 1.694.625.712.227 1.36.195 1.871.118.571-.085 1.758-.719 2.006-1.413.248-.694.248-1.289.173-1.413-.074-.124-.272-.198-.57-.347m-5.421 7.403h-.004a9.87 9.87 0 01-5.031-1.378l-.361-.214-3.741.982.998-3.648-.235-.374a9.86 9.86 0 01-1.51-5.26c.001-5.45 4.436-9.884 9.888-9.884 2.64 0 5.122 1.03 6.988 2.898a9.825 9.825 0 012.893 6.994c-.003 5.45-4.437 9.884-9.885 9.884m8.413-18.297A11.815 11.815 0 0012.05 0C5.495 0 .16 5.335.157 11.892c0 2.096.547 4.142 1.588 5.945L.057 24l6.305-1.654a11.882 11.882 0 005.683 1.448h.005c6.554 0 11.89-5.335 11.893-11.893a11.821 11.821 0 00-3.48-8.413Z" /></svg>
|
||||
<ShoppingCart className="h-5 w-5" />
|
||||
Add to Cart
|
||||
</button>
|
||||
{isModuleEnabled('wishlist') && wishlistEnabled && (
|
||||
<button
|
||||
onClick={() => product && toggleWishlist(product.id)}
|
||||
className={`w-full h-14 flex items-center justify-center gap-2 rounded-xl font-semibold text-base border-2 transition-all ${product && isInWishlist(product.id)
|
||||
? 'bg-red-50 text-red-600 border-red-200 hover:border-red-400'
|
||||
: 'bg-white text-gray-900 border-gray-200 hover:border-gray-400'
|
||||
}`}
|
||||
>
|
||||
<Heart className={`h-5 w-5 ${product && isInWishlist(product.id) ? 'fill-red-500' : ''
|
||||
}`} />
|
||||
{product && isInWishlist(product.id) ? 'Remove from Wishlist' : 'Add to Wishlist'}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Trust Badges - REDESIGNED */}
|
||||
<div className="grid grid-cols-3 gap-4 py-6 border-y border-gray-200">
|
||||
{/* Free Shipping */}
|
||||
<div className="flex flex-col items-center text-center">
|
||||
<div className="w-12 h-12 rounded-full bg-green-50 flex items-center justify-center mb-2">
|
||||
<svg className="w-6 h-6 text-green-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 8h14M5 8a2 2 0 110-4h14a2 2 0 110 4M5 8v10a2 2 0 002 2h10a2 2 0 002-2V8m-9 4h4" />
|
||||
</svg>
|
||||
</div>
|
||||
<p className="font-medium text-sm text-gray-900">Free Shipping</p>
|
||||
<p className="text-xs text-gray-500 mt-1">On orders over $50</p>
|
||||
</div>
|
||||
|
||||
{/* Returns */}
|
||||
<div className="flex flex-col items-center text-center">
|
||||
<div className="w-12 h-12 rounded-full bg-blue-50 flex items-center justify-center mb-2">
|
||||
<svg className="w-6 h-6 text-blue-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
|
||||
</svg>
|
||||
</div>
|
||||
<p className="font-medium text-sm text-gray-900">Easy Returns</p>
|
||||
<p className="text-xs text-gray-500 mt-1">30-day guarantee</p>
|
||||
</div>
|
||||
|
||||
{/* Secure */}
|
||||
<div className="flex flex-col items-center text-center">
|
||||
<div className="w-12 h-12 rounded-full bg-purple-50 flex items-center justify-center mb-2">
|
||||
<svg className="w-6 h-6 text-purple-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z" />
|
||||
</svg>
|
||||
</div>
|
||||
<p className="font-medium text-sm text-gray-900">Secure Payment</p>
|
||||
<p className="text-xs text-gray-500 mt-1">SSL encrypted</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Product Meta */}
|
||||
{elements.product_meta && (
|
||||
<div className="space-y-2 text-sm border-t pt-4 border-gray-200">
|
||||
{product.sku && (
|
||||
<div className="flex gap-2">
|
||||
<span className="text-gray-600">SKU:</span>
|
||||
<span className="font-medium">{product.sku}</span>
|
||||
</div>
|
||||
)}
|
||||
{product.categories && product.categories.length > 0 && (
|
||||
<div className="flex gap-2">
|
||||
<span className="text-gray-600">Categories:</span>
|
||||
<span className="font-medium">
|
||||
{product.categories.map((cat: any) => cat.name).join(', ')}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Share Buttons */}
|
||||
{elements.share_buttons && (
|
||||
<div className="flex items-center gap-3 pt-4 border-t border-gray-200">
|
||||
<span className="text-sm text-gray-600 font-medium">Share:</span>
|
||||
<div className="flex gap-2">
|
||||
<button
|
||||
onClick={() => {
|
||||
const url = encodeURIComponent(window.location.href);
|
||||
window.open(`https://www.facebook.com/sharer/sharer.php?u=${url}`, '_blank', 'width=600,height=400');
|
||||
}}
|
||||
className="w-9 h-9 rounded-full bg-blue-600 hover:bg-blue-700 text-white flex items-center justify-center transition-colors"
|
||||
title="Share on Facebook"
|
||||
>
|
||||
<svg className="w-4 h-4" fill="currentColor" viewBox="0 0 24 24"><path d="M24 12.073c0-6.627-5.373-12-12-12s-12 5.373-12 12c0 5.99 4.388 10.954 10.125 11.854v-8.385H7.078v-3.47h3.047V9.43c0-3.007 1.792-4.669 4.533-4.669 1.312 0 2.686.235 2.686.235v2.953H15.83c-1.491 0-1.956.925-1.956 1.874v2.25h3.328l-.532 3.47h-2.796v8.385C19.612 23.027 24 18.062 24 12.073z" /></svg>
|
||||
</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
const url = encodeURIComponent(window.location.href);
|
||||
const text = encodeURIComponent(product.name);
|
||||
window.open(`https://twitter.com/intent/tweet?url=${url}&text=${text}`, '_blank', 'width=600,height=400');
|
||||
}}
|
||||
className="w-9 h-9 rounded-full bg-sky-500 hover:bg-sky-600 text-white flex items-center justify-center transition-colors"
|
||||
title="Share on Twitter"
|
||||
>
|
||||
<svg className="w-4 h-4" fill="currentColor" viewBox="0 0 24 24"><path d="M23.953 4.57a10 10 0 01-2.825.775 4.958 4.958 0 002.163-2.723c-.951.555-2.005.959-3.127 1.184a4.92 4.92 0 00-8.384 4.482C7.69 8.095 4.067 6.13 1.64 3.162a4.822 4.822 0 00-.666 2.475c0 1.71.87 3.213 2.188 4.096a4.904 4.904 0 01-2.228-.616v.06a4.923 4.923 0 003.946 4.827 4.996 4.996 0 01-2.212.085 4.936 4.936 0 004.604 3.417 9.867 9.867 0 01-6.102 2.105c-.39 0-.779-.023-1.17-.067a13.995 13.995 0 007.557 2.209c9.053 0 13.998-7.496 13.998-13.985 0-.21 0-.42-.015-.63A9.935 9.935 0 0024 4.59z" /></svg>
|
||||
</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
const url = encodeURIComponent(window.location.href);
|
||||
const text = encodeURIComponent(product.name);
|
||||
window.open(`https://wa.me/?text=${text}%20${url}`, '_blank');
|
||||
}}
|
||||
className="w-9 h-9 rounded-full bg-green-600 hover:bg-green-700 text-white flex items-center justify-center transition-colors"
|
||||
title="Share on WhatsApp"
|
||||
>
|
||||
<svg className="w-4 h-4" fill="currentColor" viewBox="0 0 24 24"><path d="M17.472 14.382c-.297-.149-1.758-.867-2.03-.967-.273-.099-.471-.148-.67.15-.197.297-.767.966-.94 1.164-.173.199-.347.223-.644.075-.297-.15-1.255-.463-2.39-1.475-.883-.788-1.48-1.761-1.653-2.059-.173-.297-.018-.458.13-.606.134-.133.298-.347.446-.52.149-.174.198-.298.298-.497.099-.198.05-.371-.025-.52-.075-.149-.669-1.612-.916-2.207-.242-.579-.487-.5-.669-.51-.173-.008-.371-.01-.57-.01-.198 0-.52.074-.792.372-.272.297-1.04 1.016-1.04 2.479 0 1.462 1.065 2.875 1.213 3.074.149.198 2.096 3.2 5.077 4.487.709.306 1.262.489 1.694.625.712.227 1.36.195 1.871.118.571-.085 1.758-.719 2.006-1.413.248-.694.248-1.289.173-1.413-.074-.124-.272-.198-.57-.347m-5.421 7.403h-.004a9.87 9.87 0 01-5.031-1.378l-.361-.214-3.741.982.998-3.648-.235-.374a9.86 9.86 0 01-1.51-5.26c.001-5.45 4.436-9.884 9.888-9.884 2.64 0 5.122 1.03 6.988 2.898a9.825 9.825 0 012.893 6.994c-.003 5.45-4.437 9.884-9.885 9.884m8.413-18.297A11.815 11.815 0 0012.05 0C5.495 0 .16 5.335.157 11.892c0 2.096.547 4.142 1.588 5.945L.057 24l6.305-1.654a11.882 11.882 0 005.683 1.448h.005c6.554 0 11.89-5.335 11.893-11.893a11.821 11.821 0 00-3.48-8.413Z" /></svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Product Information - VERTICAL SECTIONS (Research: 27% overlook tabs) */}
|
||||
<div className="mt-12 space-y-6">
|
||||
<div className="space-y-6">
|
||||
{/* Description Section */}
|
||||
<div className="border border-gray-200 rounded-lg overflow-hidden">
|
||||
<div className={layout.layout_style === 'card'
|
||||
? 'bg-white rounded-2xl shadow-sm border border-gray-100 overflow-hidden'
|
||||
: 'border-t border-gray-200 overflow-hidden'
|
||||
}>
|
||||
<button
|
||||
onClick={() => setActiveTab(activeTab === 'description' ? '' : 'description')}
|
||||
className="w-full flex items-center justify-between p-5 bg-gray-50 hover:bg-gray-100 transition-colors"
|
||||
className="w-full flex items-center justify-between p-5 hover:bg-gray-50 transition-colors bg-transparent"
|
||||
>
|
||||
<h2 className="text-xl font-bold text-gray-900">Product Description</h2>
|
||||
<svg
|
||||
@@ -694,10 +726,13 @@ export default function Product() {
|
||||
</div>
|
||||
|
||||
{/* Specifications Section - SCANNABLE TABLE */}
|
||||
<div className="border border-gray-200 rounded-lg overflow-hidden">
|
||||
<div className={layout.layout_style === 'card'
|
||||
? 'bg-white rounded-2xl shadow-sm border border-gray-100 overflow-hidden'
|
||||
: 'border-t border-gray-200 overflow-hidden'
|
||||
}>
|
||||
<button
|
||||
onClick={() => setActiveTab(activeTab === 'additional' ? '' : 'additional')}
|
||||
className="w-full flex items-center justify-between p-5 bg-gray-50 hover:bg-gray-100 transition-colors"
|
||||
className="w-full flex items-center justify-between p-5 hover:bg-gray-50 transition-colors bg-transparent"
|
||||
>
|
||||
<h2 className="text-xl font-bold text-gray-900">Specifications</h2>
|
||||
<svg
|
||||
|
||||
Reference in New Issue
Block a user