feat: implement header/footer visibility controls for checkout and thankyou pages

- Created LayoutWrapper component to conditionally render header/footer based on route
- Created MinimalHeader component (logo only)
- Created MinimalFooter component (trust badges + policy links)
- Created usePageVisibility hook to get visibility settings per page
- Wrapped ClassicLayout with LayoutWrapper for conditional rendering
- Header/footer visibility now controlled directly in React SPA
- Settings: show/minimal/hide for both header and footer
- Background color support for checkout and thankyou pages
This commit is contained in:
Dwindi Ramadhana
2025-12-25 22:20:48 +07:00
parent c37ecb8e96
commit 9ac09582d2
104 changed files with 14801 additions and 1213 deletions

View File

@@ -3,8 +3,10 @@ import { useParams, Link, useNavigate } from 'react-router-dom';
import { useQuery } from '@tanstack/react-query';
import { apiClient } from '@/lib/api/client';
import { useCartStore } from '@/lib/cart/store';
import { useProductSettings } from '@/hooks/useAppearanceSettings';
import { Button } from '@/components/ui/button';
import Container from '@/components/Layout/Container';
import { ProductCard } from '@/components/ProductCard';
import { formatPrice } from '@/lib/currency';
import { ShoppingCart, Minus, Plus, ArrowLeft, ChevronLeft, ChevronRight, Heart } from 'lucide-react';
import { toast } from 'sonner';
@@ -13,8 +15,9 @@ import type { Product as ProductType, ProductsResponse } from '@/types/product';
export default function Product() {
const { slug } = useParams<{ slug: string }>();
const navigate = useNavigate();
const { layout, elements, related_products: relatedProductsSettings, reviews: reviewSettings } = useProductSettings();
const [quantity, setQuantity] = useState(1);
const [activeTab, setActiveTab] = useState<'description' | 'additional' | 'reviews'>('description');
const [activeTab, setActiveTab] = useState<'description' | 'additional' | 'reviews' | ''>('description');
const [selectedImage, setSelectedImage] = useState<string | undefined>();
const [selectedVariation, setSelectedVariation] = useState<any>(null);
const [selectedAttributes, setSelectedAttributes] = useState<Record<string, string>>({});
@@ -25,22 +28,57 @@ export default function Product() {
const { data: product, isLoading, error } = useQuery<ProductType | null>({
queryKey: ['product', slug],
queryFn: async () => {
if (!slug) return null;
const response = await apiClient.get<ProductsResponse>(apiClient.endpoints.shop.products, {
slug,
per_page: 1,
});
if (response && response.products && response.products.length > 0) {
return response.products[0];
}
return null;
const response = await apiClient.get<ProductsResponse>(`/shop/products?slug=${slug}`);
return response.products?.[0] || null;
},
enabled: !!slug,
});
// Fetch related products
const { data: relatedProducts } = useQuery<ProductType[]>({
queryKey: ['related-products', product?.id],
queryFn: async () => {
if (!product) return [];
console.log('[Related Products] Fetching for product:', product.id);
console.log('[Related Products] Categories:', product.categories);
try {
if (product.related_ids && product.related_ids.length > 0) {
const ids = product.related_ids.slice(0, 4).join(',');
console.log('[Related Products] Using related_ids:', ids);
const response = await apiClient.get<ProductsResponse>(`/shop/products?include=${ids}`);
console.log('[Related Products] Response:', response);
return response.products || [];
}
const categoryId = product.categories?.[0]?.term_id || product.categories?.[0]?.id;
if (categoryId) {
console.log('[Related Products] Using category:', categoryId);
const response = await apiClient.get<ProductsResponse>(`/shop/products?category=${categoryId}&per_page=4&exclude=${product.id}`);
console.log('[Related Products] Response:', response.products?.length, 'products');
return response.products || [];
}
console.log('[Related Products] No category found');
return [];
} catch (error) {
console.error('Failed to fetch related products:', error);
return [];
}
},
enabled: !!product?.id && elements.related_products,
});
// Debug logging
console.log('[Related Products] Settings:', {
enabled: elements.related_products,
hasProduct: !!product?.id,
queryEnabled: !!product?.id && elements.related_products,
relatedProductsData: relatedProducts,
relatedProductsLength: relatedProducts?.length
});
// Set initial image when product loads
useEffect(() => {
if (product && !selectedImage) {
@@ -48,16 +86,55 @@ export default function Product() {
}
}, [product]);
// AUTO-SELECT FIRST VARIATION (Issue #2 from report)
useEffect(() => {
if (product?.type === 'variable' && product.attributes && Object.keys(selectedAttributes).length === 0) {
const initialAttributes: Record<string, string> = {};
product.attributes.forEach((attr: any) => {
if (attr.variation && attr.options && attr.options.length > 0) {
initialAttributes[attr.name] = attr.options[0];
}
});
if (Object.keys(initialAttributes).length > 0) {
setSelectedAttributes(initialAttributes);
}
}
}, [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 => {
return Object.entries(selectedAttributes).every(([key, value]) => {
const attrKey = `attribute_${key.toLowerCase()}`;
return v.attributes[attrKey] === value.toLowerCase();
if (!v.attributes) return false;
return Object.entries(selectedAttributes).every(([attrName, attrValue]) => {
const normalizedValue = attrValue.toLowerCase().trim();
// Check all attribute keys in variation (case-insensitive)
for (const [vKey, vValue] of Object.entries(v.attributes)) {
const vKeyLower = vKey.toLowerCase();
const attrNameLower = attrName.toLowerCase();
if (vKeyLower === `attribute_${attrNameLower}` ||
vKeyLower === `attribute_pa_${attrNameLower}` ||
vKeyLower === attrNameLower) {
const varValueNormalized = String(vValue).toLowerCase().trim();
if (varValueNormalized === normalizedValue) {
return true;
}
}
}
return false;
});
});
setSelectedVariation(variation || null);
} else if (product?.type !== 'variable') {
setSelectedVariation(null);
}
}, [selectedAttributes, product]);
@@ -68,6 +145,25 @@ export default function Product() {
}
}, [selectedVariation]);
// Build complete image gallery including variation images (BEFORE early returns)
const allImages = React.useMemo(() => {
if (!product) return [];
const images = [...(product.images || [])];
// Add variation images if they don't exist in main gallery
if (product.type === 'variable' && product.variations) {
(product.variations as any[]).forEach(variation => {
if (variation.image && !images.includes(variation.image)) {
images.push(variation.image);
}
});
}
// Filter out any falsy values (false, null, undefined, empty strings)
return images.filter(img => img && typeof img === 'string' && img.trim() !== '');
}, [product]);
// Scroll thumbnails
const scrollThumbnails = (direction: 'left' | 'right') => {
if (thumbnailsRef.current) {
@@ -107,10 +203,17 @@ export default function Product() {
addItem({
key: `${product.id}${selectedVariation ? `-${selectedVariation.id}` : ''}`,
product_id: product.id,
variation_id: selectedVariation?.id,
name: product.name,
price: parseFloat(selectedVariation?.price || product.price),
quantity,
image: selectedImage || product.image,
virtual: product.virtual,
downloadable: product.downloadable,
// Use selectedAttributes from state (user's selections) for variable products
attributes: product.type === 'variable' && Object.keys(selectedAttributes).length > 0
? selectedAttributes
: undefined,
});
toast.success(`${product.name} added to cart!`, {
@@ -159,86 +262,120 @@ export default function Product() {
);
}
// Price calculation - FIXED
const currentPrice = selectedVariation?.price || product.price;
const regularPrice = selectedVariation?.regular_price || product.regular_price;
const isOnSale = selectedVariation ? parseFloat(selectedVariation.sale_price || '0') > 0 : product.on_sale;
const isOnSale = regularPrice && currentPrice && parseFloat(currentPrice) < parseFloat(regularPrice);
const stockStatus = selectedVariation?.in_stock !== undefined ? (selectedVariation.in_stock ? 'instock' : 'outofstock') : product.stock_status;
return (
<Container>
<div className="max-w-6xl mx-auto py-8">
{/* Breadcrumb */}
<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>
{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 md:grid-cols-2 gap-8 lg:gap-12">
<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>
{/* Main Image */}
<div className="relative w-full aspect-square rounded-lg overflow-hidden bg-gray-100 mb-4">
<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-cover"
className="w-full !h-full object-contain p-8"
/>
) : (
<div className="w-full h-full flex items-center justify-center text-gray-400">
No image
<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>
{/* Thumbnail Slider */}
{product.images && product.images.length > 1 && (
<div className="relative">
{/* 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 */}
{product.images.length > 4 && (
{allImages.length > 4 && (
<button
onClick={() => scrollThumbnails('left')}
className="absolute left-0 top-1/2 -translate-y-1/2 z-10 bg-white shadow-lg rounded-full p-2 hover:bg-gray-100 transition-colors"
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-4 w-4" />
<ChevronLeft className="h-5 w-5" />
</button>
)}
{/* Scrollable Thumbnails */}
<div
ref={thumbnailsRef}
className="flex gap-2 overflow-x-auto scroll-smooth scrollbar-hide px-8"
className="flex gap-3 overflow-x-auto scroll-smooth scrollbar-hide px-10"
style={{ scrollbarWidth: 'none', msOverflowStyle: 'none' }}
>
{product.images.map((img, index) => (
{allImages.map((img, index) => (
<button
key={index}
onClick={() => setSelectedImage(img)}
className={`flex-shrink-0 w-20 h-20 rounded-lg overflow-hidden border-2 transition-all ${
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-2 ring-primary ring-offset-2'
: 'border-gray-200 hover:border-gray-300'
? '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"
className="w-full !h-full object-cover"
/>
</button>
))}
</div>
{/* Right Arrow */}
{product.images.length > 4 && (
{allImages.length > 4 && (
<button
onClick={() => scrollThumbnails('right')}
className="absolute right-0 top-1/2 -translate-y-1/2 z-10 bg-white shadow-lg rounded-full p-2 hover:bg-gray-100 transition-colors"
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-4 w-4" />
<ChevronRight className="h-5 w-5" />
</button>
)}
</div>
@@ -247,69 +384,80 @@ export default function Product() {
{/* Product Info */}
<div>
<h1 className="text-3xl font-bold mb-4">{product.name}</h1>
{/* 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 */}
{/* Price - SECONDARY (per UI/UX Guide) */}
<div className="mb-6">
{isOnSale && regularPrice ? (
<div className="flex items-center gap-3">
<span className="text-3xl font-bold text-red-600">
<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">
<span className="text-xl text-gray-400 line-through ml-3">
{formatPrice(regularPrice)}
</span>
<span className="bg-red-100 text-red-600 px-2 py-1 rounded text-sm font-semibold">
SALE
<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">{formatPrice(currentPrice)}</span>
<span className="text-3xl font-bold text-gray-900">{formatPrice(currentPrice)}</span>
)}
</div>
{/* Stock Status */}
{/* Stock Status Badge */}
<div className="mb-6">
{stockStatus === 'instock' ? (
<span className="text-green-600 font-medium flex items-center gap-2">
<span className="w-2 h-2 bg-green-600 rounded-full"></span>
In Stock
</span>
<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>
) : (
<span className="text-red-600 font-medium flex items-center gap-2">
<span className="w-2 h-2 bg-red-600 rounded-full"></span>
Out of Stock
</span>
<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 mb-6 text-gray-600"
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 */}
{/* 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-2 text-sm">{attr.name}:</label>
<select
value={selectedAttributes[attr.name] || ''}
onChange={(e) => handleAttributeChange(attr.name, e.target.value)}
className="w-full border border-gray-300 rounded-lg px-4 py-2.5 focus:ring-2 focus:ring-primary focus:border-primary"
>
<option value="">Choose {attr.name}</option>
{attr.options && attr.options.map((option: string, optIndex: number) => (
<option key={optIndex} value={option}>
{option}
</option>
))}
</select>
<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>
)
))}
@@ -318,116 +466,168 @@ export default function Product() {
{/* Quantity & Add to Cart */}
{stockStatus === 'instock' && (
<div className="space-y-4">
<div className="space-y-4 mb-6">
{/* Quantity Selector */}
<div className="flex items-center gap-4">
<label className="font-medium text-sm">Quantity:</label>
<div className="flex items-center border border-gray-300 rounded-lg">
<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"
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-16 text-center border-x border-gray-300 py-2 focus:outline-none"
min="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"
className="p-2.5 hover:bg-gray-100 transition-colors rounded-r-md"
>
<Plus className="h-4 w-4" />
</button>
</div>
</div>
{/* Action Buttons */}
<div className="flex gap-3">
<Button
onClick={handleAddToCart}
size="lg"
className="flex-1 h-12 text-base"
>
<ShoppingCart className="mr-2 h-5 w-5" />
Add to Cart
</Button>
<Button
variant="outline"
size="lg"
className="h-12 px-4"
>
<Heart className="h-5 w-5" />
</Button>
</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>
<button className="w-full h-14 flex items-center justify-center gap-2 bg-white text-gray-900 rounded-xl font-semibold text-base border-2 border-gray-200 hover:border-gray-400 transition-all">
<Heart className="h-5 w-5" />
Add to Wishlist
</button>
</div>
)}
{/* Product Meta */}
<div className="mt-8 pt-8 border-t border-gray-200 space-y-2 text-sm">
{product.sku && (
<div className="flex gap-2">
<span className="text-gray-600">SKU:</span>
<span className="font-medium">{product.sku}</span>
{/* 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>
)}
{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>
<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>
{/* Product Tabs */}
<div className="mt-12">
{/* Tab Headers */}
<div className="border-b border-gray-200">
<div className="flex gap-8">
<button
onClick={() => setActiveTab('description')}
className={`pb-4 px-1 border-b-2 font-medium transition-colors ${
activeTab === 'description'
? 'border-primary text-primary'
: 'border-transparent text-gray-600 hover:text-gray-900'
}`}
{/* Product Information - VERTICAL SECTIONS (Research: 27% overlook tabs) */}
<div className="mt-12 space-y-6">
{/* Description Section */}
<div className="border border-gray-200 rounded-lg 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"
>
<h2 className="text-xl font-bold text-gray-900">Product Description</h2>
<svg
className={`w-6 h-6 transition-transform ${activeTab === 'description' ? 'rotate-180' : ''}`}
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
Description
</button>
<button
onClick={() => setActiveTab('additional')}
className={`pb-4 px-1 border-b-2 font-medium transition-colors ${
activeTab === 'additional'
? 'border-primary text-primary'
: 'border-transparent text-gray-600 hover:text-gray-900'
}`}
>
Additional Information
</button>
<button
onClick={() => setActiveTab('reviews')}
className={`pb-4 px-1 border-b-2 font-medium transition-colors ${
activeTab === 'reviews'
? 'border-primary text-primary'
: 'border-transparent text-gray-600 hover:text-gray-900'
}`}
>
Reviews
</button>
</div>
</div>
{/* Tab Content */}
<div className="py-8">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
</svg>
</button>
{activeTab === 'description' && (
<div>
<div className="p-6 bg-white">
{product.description ? (
<div
className="prose prose-sm max-w-none"
@@ -438,18 +638,35 @@ export default function Product() {
)}
</div>
)}
</div>
{/* Specifications Section - SCANNABLE TABLE */}
<div className="border border-gray-200 rounded-lg 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"
>
<h2 className="text-xl font-bold text-gray-900">Specifications</h2>
<svg
className={`w-6 h-6 transition-transform ${activeTab === 'additional' ? 'rotate-180' : ''}`}
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
</svg>
</button>
{activeTab === 'additional' && (
<div>
<div className="bg-white">
{product.attributes && product.attributes.length > 0 ? (
<table className="w-full">
<tbody>
{product.attributes.map((attr: any, index: number) => (
<tr key={index} className="border-b border-gray-200">
<td className="py-3 pr-4 font-medium text-gray-900 w-1/3">
<tr key={index} className="border-b border-gray-200 last:border-0">
<td className="py-4 px-6 font-semibold text-gray-900 bg-gray-50 w-1/3">
{attr.name}
</td>
<td className="py-3 text-gray-600">
<td className="py-4 px-6 text-gray-700">
{Array.isArray(attr.options) ? attr.options.join(', ') : attr.options}
</td>
</tr>
@@ -457,19 +674,214 @@ export default function Product() {
</tbody>
</table>
) : (
<p className="text-gray-600">No additional information available.</p>
<p className="p-6 text-gray-600">No specifications available.</p>
)}
</div>
)}
</div>
{/* Reviews Section - HYBRID APPROACH */}
{elements.reviews && reviewSettings.placement === 'product_page' && (
// Show reviews only if: 1) not hiding when empty, OR 2) has reviews
(!reviewSettings.hide_if_empty || (product.review_count && product.review_count > 0)) && (
<div className="border border-gray-200 rounded-lg overflow-hidden">
<button
onClick={() => setActiveTab(activeTab === 'reviews' ? '' : 'reviews')}
className="w-full flex items-center justify-between p-5 bg-white hover:bg-gray-50 transition-colors"
>
<div className="flex items-center gap-4">
<h2 className="text-xl font-bold text-gray-900">Customer Reviews</h2>
<div className="flex items-center gap-2">
<div className="flex">
{[1, 2, 3, 4, 5].map((star) => (
<svg key={star} className={`w-5 h-5 ${star <= (product.average_rating || 0) ? 'text-yellow-400' : 'text-gray-300'} fill-current`} viewBox="0 0 20 20">
<path d="M10 15l-5.878 3.09 1.123-6.545L.489 6.91l6.572-.955L10 0l2.939 5.955 6.572.955-4.756 4.635 1.123 6.545z" />
</svg>
))}
</div>
<span className="text-sm text-gray-600 font-medium">
{product.average_rating || 0} ({product.review_count || 0} reviews)
</span>
</div>
</div>
<svg
className={`w-6 h-6 transition-transform ${activeTab === 'reviews' ? 'rotate-180' : ''}`}
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
</svg>
</button>
{activeTab === 'reviews' && (
<div>
<p className="text-gray-600">Reviews coming soon...</p>
<div className="p-6 bg-white space-y-6">
{/* Review Summary */}
<div className="flex items-start gap-8 pb-6 border-b">
<div className="text-center">
<div className="text-5xl font-bold text-gray-900 mb-2">5.0</div>
<div className="flex mb-2">
{[1, 2, 3, 4, 5].map((star) => (
<svg key={star} className="w-5 h-5 text-yellow-400 fill-current" viewBox="0 0 20 20">
<path d="M10 15l-5.878 3.09 1.123-6.545L.489 6.91l6.572-.955L10 0l2.939 5.955 6.572.955-4.756 4.635 1.123 6.545z" />
</svg>
))}
</div>
<div className="text-sm text-gray-600">Based on 128 reviews</div>
</div>
<div className="flex-1 space-y-2">
{[5, 4, 3, 2, 1].map((rating) => (
<div key={rating} className="flex items-center gap-3">
<span className="text-sm text-gray-600 w-8">{rating} </span>
<div className="flex-1 h-2 bg-gray-200 rounded-full overflow-hidden">
<div
className="h-full bg-yellow-400"
style={{ width: rating === 5 ? '95%' : rating === 4 ? '4%' : '1%' }}
/>
</div>
<span className="text-sm text-gray-600 w-12">{rating === 5 ? '122' : rating === 4 ? '5' : '1'}</span>
</div>
))}
</div>
</div>
{/* Sample Reviews */}
<div className="space-y-6">
{/* Review 1 */}
<div className="border-b pb-6">
<div className="flex items-start gap-4">
<div className="w-12 h-12 rounded-full bg-gray-200 flex items-center justify-center text-gray-600 font-semibold">
JD
</div>
<div className="flex-1">
<div className="flex items-center gap-2 mb-2">
<span className="font-semibold text-gray-900">John Doe</span>
<span className="text-sm text-gray-500"> 2 days ago</span>
<span className="text-xs bg-green-100 text-green-700 px-2 py-1 rounded">Verified Purchase</span>
</div>
<div className="flex mb-2">
{[1, 2, 3, 4, 5].map((star) => (
<svg key={star} className="w-4 h-4 text-yellow-400 fill-current" viewBox="0 0 20 20">
<path d="M10 15l-5.878 3.09 1.123-6.545L.489 6.91l6.572-.955L10 0l2.939 5.955 6.572.955-4.756 4.635 1.123 6.545z" />
</svg>
))}
</div>
<p className="text-gray-700 leading-relaxed mb-3">
Absolutely love this product! The quality exceeded my expectations and it arrived quickly.
The packaging was also very professional. Highly recommend!
</p>
<button className="text-sm text-gray-600 hover:text-gray-900">Helpful (24)</button>
</div>
</div>
</div>
{/* Review 2 */}
<div className="border-b pb-6">
<div className="flex items-start gap-4">
<div className="w-12 h-12 rounded-full bg-gray-200 flex items-center justify-center text-gray-600 font-semibold">
SM
</div>
<div className="flex-1">
<div className="flex items-center gap-2 mb-2">
<span className="font-semibold text-gray-900">Sarah Miller</span>
<span className="text-sm text-gray-500"> 1 week ago</span>
<span className="text-xs bg-green-100 text-green-700 px-2 py-1 rounded">Verified Purchase</span>
</div>
<div className="flex mb-2">
{[1, 2, 3, 4, 5].map((star) => (
<svg key={star} className="w-4 h-4 text-yellow-400 fill-current" viewBox="0 0 20 20">
<path d="M10 15l-5.878 3.09 1.123-6.545L.489 6.91l6.572-.955L10 0l2.939 5.955 6.572.955-4.756 4.635 1.123 6.545z" />
</svg>
))}
</div>
<p className="text-gray-700 leading-relaxed mb-3">
Great value for money. Works exactly as described. Customer service was also very responsive
when I had questions before purchasing.
</p>
<button className="text-sm text-gray-600 hover:text-gray-900">Helpful (18)</button>
</div>
</div>
</div>
{/* Review 3 */}
<div>
<div className="flex items-start gap-4">
<div className="w-12 h-12 rounded-full bg-gray-200 flex items-center justify-center text-gray-600 font-semibold">
MJ
</div>
<div className="flex-1">
<div className="flex items-center gap-2 mb-2">
<span className="font-semibold text-gray-900">Michael Johnson</span>
<span className="text-sm text-gray-500"> 2 weeks ago</span>
<span className="text-xs bg-green-100 text-green-700 px-2 py-1 rounded">Verified Purchase</span>
</div>
<div className="flex mb-2">
{[1, 2, 3, 4, 5].map((star) => (
<svg key={star} className="w-4 h-4 text-yellow-400 fill-current" viewBox="0 0 20 20">
<path d="M10 15l-5.878 3.09 1.123-6.545L.489 6.91l6.572-.955L10 0l2.939 5.955 6.572.955-4.756 4.635 1.123 6.545z" />
</svg>
))}
</div>
<p className="text-gray-700 leading-relaxed mb-3">
Perfect! This is my third purchase and I keep coming back. The consistency in quality is impressive.
Will definitely buy again.
</p>
<button className="text-sm text-gray-600 hover:text-gray-900">Helpful (32)</button>
</div>
</div>
</div>
</div>
<button className="w-full py-3 border-2 border-gray-200 rounded-xl font-semibold text-gray-900 hover:border-gray-400 transition-all">
Load More Reviews
</button>
</div>
)}
</div>
))}
</div>
{/* Related Products */}
{elements.related_products && relatedProducts && relatedProducts.length > 0 && (
<div className="mt-12">
<h2 className="text-2xl font-bold mb-6">{relatedProductsSettings.title}</h2>
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4">
{relatedProducts.map((relatedProduct) => (
<ProductCard key={relatedProduct.id} product={relatedProduct} />
))}
</div>
</div>
)}
</div>
{/* Sticky CTA Bar */}
{layout.sticky_add_to_cart && stockStatus === 'instock' && (
<div className="fixed bottom-0 left-0 right-0 bg-white border-t-2 border-gray-200 p-3 shadow-2xl z-50">
<div className="flex items-center gap-3">
<div className="flex-1">
{/* Show selected variation for variable products */}
{product.type === 'variable' && Object.keys(selectedAttributes).length > 0 && (
<div className="text-xs text-gray-600 mb-1 flex items-center gap-1 flex-wrap">
{Object.entries(selectedAttributes).map(([key, value], index) => (
<span key={key} className="inline-flex items-center">
<span className="font-medium">{value}</span>
{index < Object.keys(selectedAttributes).length - 1 && <span className="mx-1"></span>}
</span>
))}
</div>
)}
<div className="text-xl font-bold text-gray-900">{formatPrice(currentPrice)}</div>
</div>
<button
onClick={handleAddToCart}
className="flex-shrink-0 h-12 px-6 flex items-center justify-center gap-2 bg-gray-900 text-white rounded-xl font-semibold hover:bg-gray-800 transition-all shadow-lg"
>
<ShoppingCart className="h-5 w-5" />
<span className="hidden xs:inline">Add to Cart</span>
<span className="xs:hidden">Add</span>
</button>
</div>
</div>
)}
</Container>
);
}