// ============================================================================ // Sun Systems Catalogue — React Components // ============================================================================ const { useState, useEffect, useMemo, useRef, useCallback } = React; // ---------- Currency formatter ---------- const fmtINR = (n) => '₹' + Number(n).toLocaleString('en-IN'); // ---------- Brand-tinted SVG placeholder ---------- function PlaceholderArt({ seed = 0, label = '' }) { // Build a soft, abstract laptop placeholder — paper-tone gradient + subtle laptop glyph const hueShift = (seed * 37) % 60; return ( {/* Laptop glyph */} ); } // ---------- Icons ---------- const Icon = { Search: (p) => (), Filter: (p) => (), Cpu: (p) => (), Memory: (p) => (), Hdd: (p) => (), Display: (p) => (), WhatsApp: (p) => (), Compare: (p) => (), Eye: (p) => (), X: (p) => (), Check: (p) => (), Phone: (p) => (), MapPin: (p) => (), Upload: (p) => (), Sun: (p) => (), Mail: (p) => (), }; // ---------- WhatsApp link builder ---------- // `sel` (optional) is the chosen variant: { ram, storage, price } function buildWaUrl(phone, product, sel) { let msg; if (product && sel) { msg = `Hi Sun Systems! I'd like to know more about the *${product.model}* (${product.processor}, ${sel.ram}GB / ${sel.storage}GB SSD) listed at ${fmtINR(sel.price)}. Is it available?`; } else if (product) { msg = `Hi Sun Systems! I'd like to know more about the *${product.model}* (${product.processor}). Is it available?`; } else { msg = `Hi Sun Systems! I'd like to know more about your laptops.`; } return `https://wa.me/91${phone}?text=${encodeURIComponent(msg)}`; } // ---------- Variant engine (base price + RAM/SSD upgrades) ---------- // Reads baseRam/baseStorage/basePrice/addRam/addSsd and produces the list of // 1–4 configurations plus a price function. Upgrade targets are 16GB and 512GB. function variantsOf(p) { const baseRam = Number(p.baseRam) || 0; const baseStorage = Number(p.baseStorage) || 0; const basePrice = Number(p.basePrice) || 0; const addRam = (Number(p.addRam) > 0 && baseRam < 16) ? Number(p.addRam) : 0; const addSsd = (Number(p.addSsd) > 0 && baseStorage < 512) ? Number(p.addSsd) : 0; const ramOptions = addRam ? [baseRam, 16] : [baseRam]; const storageOptions = addSsd ? [baseStorage, 512] : [baseStorage]; const priceFor = (ram, storage) => basePrice + (ram > baseRam ? addRam : 0) + (storage > baseStorage ? addSsd : 0); const prices = []; ramOptions.forEach(r => storageOptions.forEach(s => prices.push(priceFor(r, s)))); return { baseRam, baseStorage, basePrice, addRam, addSsd, ramOptions, storageOptions, priceFor, hasVariants: ramOptions.length > 1 || storageOptions.length > 1, priceMin: Math.min(...prices), priceMax: Math.max(...prices), }; } // ---------- Variant pill selector ---------- function VariantPills({ label, options, value, unit, onChange }) { return (
{label}
{options.map(o => ( ))}
); } // ---------- Image resolver — supports `image` (single) or `images` (array) ---------- function getImages(product) { if (Array.isArray(product.images) && product.images.length) return product.images; if (product.image) return [product.image]; return []; } function getCover(product) { const imgs = getImages(product); return imgs[0] || null; } // ---------- Card ---------- function ProductCard({ product, variant, showPrice, onOpen, onCompare, isComparing, waPhone }) { const v = variantsOf(product); const [sel, setSel] = useState({ ram: v.ramOptions[0], storage: v.storageOptions[0] }); const price = v.priceFor(sel.ram, sel.storage); const ramLabel = `${sel.ram}GB`; const storageLabel = `${sel.storage}GB SSD`; const [imgError, setImgError] = useState(false); const cover = getCover(product); const showImg = cover && !imgError; return (
onOpen(product)} >
{showImg ? {product.model} setImgError(true)} /> : }
{product.stock === 'in-stock' ? 'In stock' : 'Sold out'} {product.featured && Featured}
{!showImg && variant !== 'minimal' && photo coming soon}
{variant === 'minimal' && (
{product.stock === 'in-stock' ? 'In stock' : 'Sold out'} {product.featured && Featured}
)}
{product.brand} · {product.series}

{product.model}

{product.cpuTier}{product.generation ? ` · ${product.generation}th gen` : ''} {v.ramOptions.length === 1 && {ramLabel}} {v.storageOptions.length === 1 && {storageLabel}} {product.displayLabel}
{v.hasVariants && (
e.stopPropagation()}> {v.ramOptions.length > 1 && ( setSel(s => ({ ...s, ram: o }))} /> )} {v.storageOptions.length > 1 && ( setSel(s => ({ ...s, storage: o }))} /> )}
)}
{showPrice ? (
{fmtINR(price)} incl. of all taxes
) : (
Ask for price
)}
e.stopPropagation()}>
); } // ---------- Detail modal ---------- function ProductModal({ product, onClose, showPrice, waPhone }) { const images = getImages(product); const [imgIdx, setImgIdx] = useState(0); const [failed, setFailed] = useState({}); const v = variantsOf(product); const [sel, setSel] = useState({ ram: v.ramOptions[0], storage: v.storageOptions[0] }); const price = v.priceFor(sel.ram, sel.storage); const go = (dir) => setImgIdx(i => (i + dir + images.length) % images.length); // Touch swipe for mobile gallery const touchX = useRef(null); const onTouchStart = (e) => { touchX.current = e.touches[0].clientX; }; const onTouchEnd = (e) => { if (touchX.current == null || images.length < 2) return; const dx = e.changedTouches[0].clientX - touchX.current; if (Math.abs(dx) > 40) go(dx < 0 ? 1 : -1); touchX.current = null; }; useEffect(() => { const onKey = (e) => { if (e.key === 'Escape') onClose(); if (e.key === 'ArrowRight' && images.length > 1) setImgIdx(i => (i + 1) % images.length); if (e.key === 'ArrowLeft' && images.length > 1) setImgIdx(i => (i - 1 + images.length) % images.length); }; window.addEventListener('keydown', onKey); document.body.style.overflow = 'hidden'; return () => { window.removeEventListener('keydown', onKey); document.body.style.overflow = ''; }; }, [onClose, images.length]); if (!product) return null; const priceStr = fmtINR(price); return (
e.stopPropagation()}>
{images[imgIdx] && !failed[images[imgIdx]] ? {product.model} setFailed(f => ({ ...f, [images[imgIdx]]: true }))} /> : } {images.length > 1 && ( <>
{images.map((_, i) => (
)}
{product.stock === 'in-stock' ? 'In stock' : 'Sold out'}
{product.brand} · {product.series}

{product.model}

{showPrice && (
{priceStr} INR
)} {v.hasVariants && (
{v.ramOptions.length > 1 && ( setSel(s => ({ ...s, ram: o }))} /> )} {v.storageOptions.length > 1 && ( setSel(s => ({ ...s, storage: o }))} /> )}
)}
Processor
{product.processor}
{v.ramOptions.length === 1 && (<>
Memory
{sel.ram}GB
)} {v.storageOptions.length === 1 && (<>
Storage
{sel.storage}GB SSD
)}
Display
{product.displayLabel}
Graphics
{product.graphics || 'Integrated'}
{product.notes && (<>
Notes
{product.notes}
)}
Ask on WhatsApp

Pre-owned business laptops · Tested & cleaned · 1-month testing + 6-month service warranty

); } // ---------- Compare modal ---------- function CompareModal({ products, onClose, onRemove, showPrice }) { useEffect(() => { const onKey = (e) => { if (e.key === 'Escape') onClose(); }; window.addEventListener('keydown', onKey); document.body.style.overflow = 'hidden'; return () => { window.removeEventListener('keydown', onKey); document.body.style.overflow = ''; }; }, [onClose]); // Highlight best in each numeric row const best = useMemo(() => { const ramMax = Math.max(...products.map(p => p.ram)); const stoMax = Math.max(...products.map(p => p.storage)); const priceMin = Math.min(...products.map(p => p.priceMin)); return { ramMax, stoMax, priceMin }; }, [products]); const rows = [ { label: 'Brand', get: p => p.brand }, { label: 'Series', get: p => p.series }, { label: 'Processor', get: p => p.processor }, { label: 'Generation', get: p => p.generation ? `${p.generation}th gen` : '—' }, { label: 'RAM', get: p => p.ramLabel, highlight: p => p.ram === best.ramMax }, { label: 'Storage', get: p => p.storageLabel, highlight: p => p.storage === best.stoMax }, { label: 'Display', get: p => p.displayLabel }, { label: 'Graphics', get: p => p.graphics || 'Integrated' }, { label: 'Stock', get: p => p.stock === 'in-stock' ? 'In stock' : 'Sold out' }, ]; if (showPrice) rows.push({ label: 'Price (INR)', get: p => p.priceMin === p.priceMax ? fmtINR(p.priceMin) : `${fmtINR(p.priceMin)}–${fmtINR(p.priceMax)}`, highlight: p => p.priceMin === best.priceMin }); return (
e.stopPropagation()}>

Side-by-side

{products.map(p => ( ))} {rows.map(row => ( {products.map(p => ( ))} ))}
{p.brand}

{p.model}

{row.label} {row.get(p)}
); } Object.assign(window, { ProductCard, ProductModal, CompareModal, Icon, fmtINR, buildWaUrl, PlaceholderArt, variantsOf, VariantPills });