import React, { useState, useEffect, useRef } from 'react'; 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 { useWishlist } from '@/hooks/useWishlist'; import { useModules } from '@/hooks/useModules'; 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'; import SEOHead from '@/components/SEOHead'; 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 [selectedImage, setSelectedImage] = useState(); const [selectedVariation, setSelectedVariation] = useState(null); const [selectedAttributes, setSelectedAttributes] = useState>({}); const thumbnailsRef = useRef(null); const { addItem } = useCartStore(); const { isEnabled: wishlistEnabled, isInWishlist, toggleWishlist, isLoggedIn } = useWishlist(); const { isEnabled: isModuleEnabled } = useModules(); // Apply white background to
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({ queryKey: ['product', slug], queryFn: async () => { const response = await apiClient.get(`/shop/products?slug=${slug}`); return response.products?.[0] || null; }, enabled: !!slug, }); // Fetch related products const { data: relatedProducts } = useQuery({ queryKey: ['related-products', product?.id], queryFn: async () => { if (!product) return []; try { if (product.related_ids && product.related_ids.length > 0) { const ids = product.related_ids.slice(0, 4).join(','); const response = await apiClient.get(`/shop/products?include=${ids}`); return response.products || []; } const categoryId = product.categories?.[0]?.term_id || product.categories?.[0]?.id; if (categoryId) { const response = await apiClient.get(`/shop/products?category=${categoryId}&per_page=4&exclude=${product.id}`); return response.products || []; } return []; } catch (error) { console.error('Failed to fetch related products:', error); return []; } }, enabled: !!product?.id && elements.related_products, }); // Set initial image when product loads useEffect(() => { if (product && !selectedImage) { setSelectedImage(product.image || product.images?.[0]); } }, [product]); // AUTO-SELECT FIRST VARIATION (Issue #2 from report) useEffect(() => { if (product?.type === 'variable' && product.attributes && Object.keys(selectedAttributes).length === 0) { const initialAttributes: Record = {}; 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) { let bestMatch: any = null; let highestScore = -1; (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(); // Find the attribute definition to get the slug const attrDef = product.attributes?.find((a: any) => a.name === attrName); const attrSlug = attrDef?.slug || attrNameLower.replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, ''); // Try to find a matching key in the variation attributes let variationValue: string | undefined = undefined; if (`attribute_${attrSlug}` in v.attributes) { variationValue = v.attributes[`attribute_${attrSlug}`]; } else if (`attribute_pa_${attrSlug}` in v.attributes) { variationValue = v.attributes[`attribute_pa_${attrSlug}`]; } 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}`]; } else if (attrNameLower in v.attributes) { variationValue = v.attributes[attrNameLower]; } // 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 with score 0 const normalizedVarValue = String(variationValue).toLowerCase().trim(); if (normalizedVarValue === '') { return true; } // 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(bestMatch || null); } else if (product?.type !== 'variable') { setSelectedVariation(null); } }, [selectedAttributes, product]); // Auto-switch image when variation selected useEffect(() => { if (selectedVariation && selectedVariation.image) { setSelectedImage(selectedVariation.image); } }, [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) { const scrollAmount = 200; thumbnailsRef.current.scrollBy({ left: direction === 'left' ? -scrollAmount : scrollAmount, behavior: 'smooth' }); } }; const handleAttributeChange = (attributeName: string, value: string) => { setSelectedAttributes(prev => ({ ...prev, [attributeName]: value })); }; const handleAddToCart = async () => { if (!product) return; // Validate variation selection for variable products if (product.type === 'variable') { if (!selectedVariation) { toast.error('Please select all product options'); return; } } // Construct variation params using keys from the matched variation // but filling in values from user selection (handles "Any" variations with empty values) let variation_params: Record = {}; if (product.type === 'variable' && selectedVariation?.attributes) { // Get keys from the variation's attributes (these are the correct WooCommerce keys) Object.keys(selectedVariation.attributes).forEach(key => { // Key format is like "attribute_7-days-auto-closing-variation-plan" // Extract the slug part after "attribute_" const slug = key.replace(/^attribute_/, ''); // Find the matching user-selected value by attribute name const attrDef = product.attributes?.find((a: any) => a.slug === slug || a.name.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '') === slug ); if (attrDef && selectedAttributes[attrDef.name]) { variation_params[key] = selectedAttributes[attrDef.name]; } else { // Fallback to stored value if no user selection variation_params[key] = selectedVariation.attributes[key]; } }); } try { await apiClient.post(apiClient.endpoints.cart.add, { product_id: product.id, quantity, variation_id: selectedVariation?.id || 0, variation: variation_params, }); 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!`, { action: { label: 'View Cart', onClick: () => navigate('/cart'), }, }); } catch (error) { toast.error('Failed to add to cart'); console.error(error); } }; if (isLoading) { return (
); } if (error || !product) { return (

Product Not Found

The product you're looking for doesn't exist.

); } // Price calculation - FIXED const currentPrice = selectedVariation?.price || product.price; const regularPrice = selectedVariation?.regular_price || product.regular_price; const isOnSale = regularPrice && currentPrice && parseFloat(currentPrice) < parseFloat(regularPrice); const stockStatus = selectedVariation?.in_stock !== undefined ? (selectedVariation.in_stock ? 'instock' : 'outofstock') : product.stock_status; return ( {/* SEO Meta Tags for Social Sharing */} ]+>/g, '').slice(0, 160) || product.description?.replace(/<[^>]+>/g, '').slice(0, 160)} image={product.image || product.images?.[0]} type="product" product={{ price: currentPrice, currency: (window as any).woonoowCustomer?.currency?.code || 'USD', availability: stockStatus === 'instock' ? 'in stock' : 'out of stock', }} /> {/* Flat: entire Container is bg-white. Card: per-section white cards on gray. */}
{/* Top section: flat = no card wrapper, card = white card */}
{/* Breadcrumb */} {elements.breadcrumbs && ( )}
{/* Product Images */}
{/* Main Image - ENHANCED */}
{selectedImage ? ( {product.name} ) : (

No image available

)} {/* Sale Badge on Image */} {isOnSale && (
Sale
)}
{/* Dots Navigation - Show based on gallery_style */} {allImages && allImages.length > 1 && layout.gallery_style === 'dots' && (
{allImages.map((img, index) => (
)} {/* Thumbnail Slider - Show based on gallery_style */} {allImages && allImages.length > 1 && layout.gallery_style === 'thumbnails' && (
{/* Left Arrow */} {allImages.length > 4 && ( )} {/* Scrollable Thumbnails */}
{allImages.map((img, index) => ( ))}
{/* Right Arrow */} {allImages.length > 4 && ( )}
)}
{/* Product Info */}
{/* Product Title - PRIMARY HIERARCHY - SERIF FONT */}

{product.name}

{/* Price - SECONDARY (per UI/UX Guide) */}
{isOnSale && regularPrice ? (
{formatPrice(currentPrice)} {formatPrice(regularPrice)} Save {Math.round((1 - parseFloat(currentPrice) / parseFloat(regularPrice)) * 100)}%
) : ( {formatPrice(currentPrice)} )}
{/* Stock Status Badge */}
{stockStatus === 'instock' ? (
In Stock • Ships Today
) : (
Out of Stock
)}
{/* Short Description */} {product.short_description && (
)} {/* Variation Selector - PILLS (per UI/UX Guide) */} {product.type === 'variable' && product.attributes && product.attributes.length > 0 && (
{product.attributes.map((attr: any, index: number) => ( attr.variation && (
{attr.options && attr.options.map((option: string, optIndex: number) => { const isSelected = selectedAttributes[attr.name] === option; return ( ); })}
) ))}
)} {/* Quantity & Add to Cart */} {stockStatus === 'instock' && (
{/* Quantity Selector */}
Quantity
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" />
{/* Action Buttons - PROMINENT */} {/* Add to Cart Button */} {isModuleEnabled('wishlist') && wishlistEnabled && ( )}
)} {/* Trust Badges - REDESIGNED */}
{/* Free Shipping */}

Free Shipping

On orders over $50

{/* Returns */}

Easy Returns

30-day guarantee

{/* Secure */}

Secure Payment

SSL encrypted

{/* Product Meta */} {elements.product_meta && (
{product.sku && (
SKU: {product.sku}
)} {product.categories && product.categories.length > 0 && (
Categories: {product.categories.map((cat: any) => cat.name).join(', ')}
)}
)} {/* Share Buttons */} {elements.share_buttons && (
Share:
)}
{/* Product Information - VERTICAL SECTIONS (Research: 27% overlook tabs) */}
{/* Description Section */}
{activeTab === 'description' && (
{product.description ? (
) : (

No description available.

)}
)}
{/* Specifications Section - SCANNABLE TABLE */}
{activeTab === 'additional' && (
{product.attributes && product.attributes.length > 0 ? ( {product.attributes.map((attr: any, index: number) => ( ))}
{attr.name} {Array.isArray(attr.options) ? attr.options.join(', ') : attr.options}
) : (

No specifications available.

)}
)}
{/* 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)) && (
{activeTab === 'reviews' && (
{/* Review Summary */}
5.0
{[1, 2, 3, 4, 5].map((star) => ( ))}
Based on 128 reviews
{[5, 4, 3, 2, 1].map((rating) => (
{rating} ★
{rating === 5 ? '122' : rating === 4 ? '5' : '1'}
))}
{/* Sample Reviews */}
{/* Review 1 */}
JD
John Doe • 2 days ago Verified Purchase
{[1, 2, 3, 4, 5].map((star) => ( ))}

Absolutely love this product! The quality exceeded my expectations and it arrived quickly. The packaging was also very professional. Highly recommend!

{/* Review 2 */}
SM
Sarah Miller • 1 week ago Verified Purchase
{[1, 2, 3, 4, 5].map((star) => ( ))}

Great value for money. Works exactly as described. Customer service was also very responsive when I had questions before purchasing.

{/* Review 3 */}
MJ
Michael Johnson • 2 weeks ago Verified Purchase
{[1, 2, 3, 4, 5].map((star) => ( ))}

Perfect! This is my third purchase and I keep coming back. The consistency in quality is impressive. Will definitely buy again.

)}
))}
{/* Related Products */} {elements.related_products && relatedProducts && relatedProducts.length > 0 && (

{relatedProductsSettings.title}

{relatedProducts.map((relatedProduct) => ( ))}
)}
{/* Sticky CTA Bar */} {layout.sticky_add_to_cart && stockStatus === 'instock' && (
{/* Show selected variation for variable products */} {product.type === 'variable' && Object.keys(selectedAttributes).length > 0 && (
{Object.entries(selectedAttributes).map(([key, value], index) => ( {value} {index < Object.keys(selectedAttributes).length - 1 && } ))}
)}
{formatPrice(currentPrice)}
)} ); }