"use client"; import React, { useState, useEffect, useRef, useMemo } from 'react'; import Image from 'next/image'; import { useRouter, useSearchParams } from 'next/navigation'; import { useCartStore } from '@/store/cartStore'; import { catalogService, ProductResponse } from '@/services/api/catalogService'; import { formatCurrency, getCatalogPriceInfo, getImageUrl, parseMoney } from '@/lib/utils'; import { addToCompareAndNavigate } from '@/lib/compare'; import { ShieldCheck, Truck, ShoppingCart, Heart, Check, Eye, Coins, Share2, HelpCircle, GitCompare, ChevronUp, ChevronDown } from 'lucide-react'; interface ProductInteractiveGridProps { product: ProductResponse; initialReviewsCount: number; relatedProducts?: ProductResponse[]; } const FALLBACK_TECH_IMAGES: string[] = ['/images/placeholder-product.webp']; function looksLikeBrandId(value?: string | null): boolean { if (!value) return true; if (value.length > 18 && /[0-9]/.test(value)) return true; if (/^[0-9A-Z]{16,}$/i.test(value.replace(/[_-]/g, ''))) return true; return false; } function getSortedImages(images?: any[] | null): any[] { if (!images || !Array.isArray(images) || images.length === 0) return []; return [...images].sort((a, b) => { const aPri = a.is_primary || a.is_banner ? 1 : 0; const bPri = b.is_primary || b.is_banner ? 1 : 0; if (aPri !== bPri) return bPri - aPri; return (a.sort_order ?? 0) - (b.sort_order ?? 0); }); } function getPrimaryImageUrl(images?: any[] | null): string | null { const sorted = getSortedImages(images); return sorted[0]?.image_url || null; } function buildGalleryUrls(images?: any[] | null): string[] { const sorted = getSortedImages(images); const urls = sorted .map((img: any) => getImageUrl(img?.image_url)) .filter((url) => url && url !== '/images/placeholder-product.webp'); return urls.length > 0 ? urls : FALLBACK_TECH_IMAGES; } function resolveInitialImage(product: ProductResponse, variantIndex: number, failedImages?: Set): string { const variant = product.variants?.[variantIndex]; const varImgs = getSortedImages(variant?.images) .map((i: any) => getImageUrl(i?.image_url)) .filter((u): u is string => Boolean(u) && !failedImages?.has(u!)); if (varImgs.length > 0) return varImgs[0]; const mainImgs = getSortedImages(product.images) .map((i: any) => getImageUrl(i?.image_url)) .filter((u): u is string => Boolean(u) && !failedImages?.has(u!)); if (mainImgs.length > 0) return mainImgs[0]; return FALLBACK_TECH_IMAGES[0]; } export const ProductInteractiveGrid: React.FC = ({ product, initialReviewsCount, relatedProducts = [], }) => { const searchParams = useSearchParams(); const router = useRouter(); const [failedImages, setFailedImages] = useState>(new Set()); const [selectedImage, setSelectedImage] = useState(() => resolveInitialImage(product, 0)); const [selectedVariantIndex, setSelectedVariantIndex] = useState(0); const [quantity, setQuantity] = useState(1); const [added, setAdded] = useState(false); const [selections, setSelections] = useState>({}); const [showStickyBar, setShowStickyBar] = useState(false); const [addonQty, setAddonQty] = useState>({}); const [brandName, setBrandName] = useState(''); const thumbRailRef = useRef(null); // Zoom magnifier lens coordinates state const [zoomPos, setZoomPos] = useState({ x: 0, y: 0 }); const [isZooming, setIsZooming] = useState(false); const imageRef = useRef(null); const addToCart = useCartStore((state) => state.addToCart); const toggleWishlist = useCartStore((state) => state.toggleWishlist); const wishlist = useCartStore((state) => state.wishlist) || []; const compareList = useCartStore((state) => state.compareList) || []; const lookupIndex = useRef<{ variantAttrsMap: Map>; validValuesByDimension: Map>; } | null>(null); // Initialize lookup maps and default state on mount/product change useEffect(() => { if (!product) return; const variantAttrsMap = new Map>(); const validValuesByDimension = new Map>(); product.variants.forEach((variant: any) => { const attrMap = new Map(); (variant.attributes || []).forEach((a: any) => { attrMap.set(a.attribute_code, a.attribute_value); if (!validValuesByDimension.has(a.attribute_code)) { validValuesByDimension.set(a.attribute_code, new Set()); } validValuesByDimension.get(a.attribute_code)!.add(a.attribute_value); }); variantAttrsMap.set(variant.variant_id, attrMap); }); lookupIndex.current = { variantAttrsMap, validValuesByDimension }; // Select variant based on URL params or default to first index const urlVariantId = searchParams.get('variant'); const urlSku = searchParams.get('sku'); let activeIdx = product.variants.findIndex((v: any) => v.variant_id === urlVariantId || v.sku === urlSku); if (activeIdx === -1) { activeIdx = 0; } setSelectedVariantIndex(activeIdx); setSelectedImage(resolveInitialImage(product, activeIdx, failedImages)); const initial: Record = {}; const activeVar = product.variants[activeIdx]; if (activeVar && activeVar.attributes) { activeVar.attributes.forEach((a: any) => { initial[a.attribute_code] = a.attribute_value; }); } setSelections(initial); }, [product, searchParams]); useEffect(() => { const named = (product as ProductResponse & { brand_name?: string }).brand_name; if (named && !looksLikeBrandId(named)) { setBrandName(named); return; } catalogService.getBrands().then((brands) => { const match = brands.find((b) => b.brand_id === product.brand_id || b.slug === product.brand_id); setBrandName(match?.name || 'Gallery'); }); }, [product]); // Scroll listener for mobile sticky CTA bar useEffect(() => { const handleScroll = () => { if (window.scrollY > 450) { setShowStickyBar(true); } else { setShowStickyBar(false); } }; window.addEventListener('scroll', handleScroll); return () => window.removeEventListener('scroll', handleScroll); }, []); const handleMouseMove = (e: React.MouseEvent) => { if (!imageRef.current) return; const { left, top, width, height } = imageRef.current.getBoundingClientRect(); const x = ((e.clientX - left) / width) * 100; const y = ((e.clientY - top) / height) * 100; setZoomPos({ x, y }); }; const selectedVariant = product.variants && product.variants.length > 0 ? product.variants[selectedVariantIndex] || product.variants.find((v: any) => { if (!lookupIndex.current) return false; const attrMap = lookupIndex.current.variantAttrsMap.get(v.variant_id); if (!attrMap || attrMap.size === 0) return false; return ( Object.entries(selections).length > 0 && Object.entries(selections).every(([k, vVal]) => attrMap.get(k) === vVal) ); }) || product.variants[0] : null; useEffect(() => { if (!selectedVariant) return; setSelectedImage(resolveInitialImage(product, selectedVariantIndex, failedImages)); }, [selectedVariantIndex, selectedVariant?.variant_id, product, failedImages]); const availableStock = selectedVariant?.available_stock; const outOfStock = typeof availableStock === 'number' && availableStock <= 0; const maxQty = typeof availableStock === 'number' ? Math.max(0, availableStock) : 99; const isInWishlist = wishlist.some((item) => item.product_id === product.product_id); const isInCompare = compareList.some((item) => item.product_id === product.product_id); const galleryImages = useMemo(() => { const variantImgs = selectedVariant?.images || []; const validVariantImgs = variantImgs.filter( (img: any) => !failedImages.has(getImageUrl(img?.image_url)) ); const validProductImgs = (product.images || []).filter( (img: any) => !failedImages.has(getImageUrl(img?.image_url)) ); const sortedVariant = getSortedImages(validVariantImgs); const sortedProduct = getSortedImages(validProductImgs); const rawImages = sortedVariant.length > 0 ? sortedVariant : sortedProduct; const urls: string[] = []; const seen = new Set(); rawImages.forEach((img: any) => { const url = getImageUrl(img?.image_url); if (url && !failedImages.has(url) && !seen.has(url)) { seen.add(url); urls.push(url); } }); return urls.length > 0 ? urls : FALLBACK_TECH_IMAGES; }, [selectedVariant, product.images, failedImages]); useEffect(() => { if (galleryImages.length > 0) { if (!selectedImage || !galleryImages.includes(selectedImage) || failedImages.has(selectedImage)) { const firstValid = galleryImages.find((img) => !failedImages.has(img)) || galleryImages[0]; setSelectedImage(firstValid); } } }, [galleryImages, selectedImage, failedImages, selectedVariantIndex, selectedVariant?.variant_id]); const handleAddToCart = () => { if (selectedVariant) { const available = selectedVariant.available_stock; if (typeof available === 'number' && (available <= 0 || quantity > available)) { return; } addToCart(product, selectedVariant, quantity); Object.entries(addonQty).forEach(([productId, qty]) => { if (qty <= 0) return; const addon = relatedProducts.find((p) => p.product_id === productId); const variant = addon?.variants?.[0]; if (addon && variant) void addToCart(addon, variant, qty); }); setAdded(true); window.dispatchEvent(new Event('wishlistUpdated')); setTimeout(() => setAdded(false), 2500); } }; const { price: minPrice, priceMax: maxPrice } = getCatalogPriceInfo(product); const selectedPrice = parseMoney(selectedVariant?.price); const selectedCompare = parseMoney(selectedVariant?.compare_price); const priceLabel = selectedVariant ? formatCurrency(selectedPrice) : minPrice && maxPrice && minPrice !== maxPrice ? `${formatCurrency(minPrice)} – ${formatCurrency(maxPrice)}` : formatCurrency(minPrice); const discountPct = selectedCompare > selectedPrice ? Math.round(((selectedCompare - selectedPrice) / selectedCompare) * 100) : 0; const cleanDesc = (product.description || '') .replace(/</gi, '<') .replace(/>/gi, '>') .replace(/&/gi, '&') .replace(/ /gi, ' ') .replace(/<[^>]*>/g, ' ') .replace(/\s+/g, ' ') .trim(); const featureBullets = cleanDesc .split(/[.\n]/) .map((s) => s.trim()) .filter((s) => s.length > 8 && s.length < 60 && !/[<>]/.test(s)) .slice(0, 3); const fallbackFeatures = ['Certified OEM quality', '12-month replacement warranty', 'Fast dispatch from warehouse']; const highlights = featureBullets.length > 0 ? featureBullets : fallbackFeatures; const addonItems = relatedProducts.slice(0, 3); const scrollThumbs = (dir: -1 | 1) => { thumbRailRef.current?.scrollBy({ top: dir * 80, behavior: 'smooth' }); }; return ( <>
{/* Left Column: Image Gallery */}
{galleryImages.map((img, idx) => { const thumbSrc = failedImages.has(img) ? '/images/placeholder-product.webp' : img; return ( ); })}
setIsZooming(true)} onMouseLeave={() => setIsZooming(false)} onMouseMove={handleMouseMove} className="relative bg-white border border-gray-200 rounded-sm p-6 h-[360px] md:h-[420px] flex items-center justify-center overflow-hidden cursor-crosshair group select-none w-full" > {(() => { const validGallery = galleryImages.filter((img) => !failedImages.has(img)); const effectiveImage = selectedImage && !failedImages.has(selectedImage) && validGallery.includes(selectedImage) ? selectedImage : validGallery[0] || FALLBACK_TECH_IMAGES[0]; const mainSrc = failedImages.has(effectiveImage) ? FALLBACK_TECH_IMAGES[0] : effectiveImage; return ( {product.name} { if (effectiveImage && effectiveImage !== FALLBACK_TECH_IMAGES[0]) { setFailedImages((prev) => new Set(prev).add(effectiveImage)); } }} /> ); })()} {isZooming && (() => { const zoomSrc = failedImages.has(selectedImage) ? '/images/placeholder-product.webp' : selectedImage; return zoomSrc ? (
) : null; })()}
{/* Right Column */}
{discountPct > 0 && ( -{discountPct}% )}
Brand:{' '} {brandName || 'Gallery'}

{product.name}

{priceLabel} {selectedCompare > selectedPrice && ( {formatCurrency(selectedCompare)} )}
{/* Interactive Variant Attribute Selectors */} {product.variants && product.variants.length > 0 && (
{(() => { const dimensions = new Map }>(); (product.variants || []).forEach((variant: any) => { (variant.attributes || []).forEach((attr: any) => { const code = String(attr.attribute_code || '').trim(); const val = String(attr.attribute_value || '').trim(); if (!code || !val) return; if (!dimensions.has(code)) { const rawName = String(attr.attribute_name || '').trim(); let label = rawName && !looksLikeBrandId(rawName) ? rawName : code.replace(/[_-]+/g, ' ').replace(/\b\w/g, (c) => c.toUpperCase()); const normKey = code.toLowerCase(); const normLabel = label.toLowerCase(); if (normKey === 'qul' || normKey === 'qual' || normLabel === 'qul' || normLabel === 'qual') { label = 'Quality'; } else if (normKey === 'clr' || normLabel === 'clr') { label = 'Color'; } else if (normKey === 'str' || normLabel === 'str') { label = 'Storage'; } else if (normKey === 'ram' || normLabel === 'ram') { label = 'RAM'; } dimensions.set(code, { code, label, values: new Set() }); } dimensions.get(code)!.values.add(val); }); }); const dims = Array.from(dimensions.values()).map((d) => ({ code: d.code, label: d.label, values: Array.from(d.values), })); if (dims.length > 0) { return dims.map((dim) => { const currentVal = selections[dim.code] || ''; return (
{dim.values.map((val) => { const isSelected = currentVal === val; return ( ); })}
); }); } if (product.variants.length > 1) { return (
{product.variants.map((v: any, idx: number) => { const isSelected = selectedVariantIndex === idx; return ( ); })}
); } return null; })()}
)}
{/* Sticky Bottom Purchase bar matching iOS Environment indicators */} {showStickyBar && (
{(() => { const barSrc = failedImages.has(selectedImage) ? '/images/placeholder-product.webp' : (selectedImage || '/images/placeholder-product.webp'); return ( { if (selectedImage) { setFailedImages((prev) => new Set(prev).add(selectedImage)); } }} /> ); })()}

{product.name}

{selectedVariant ? formatCurrency(selectedPrice) : 'N/A'}
)} ); };