// ============================================================================
// 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 (
);
}
// ---------- 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
?

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]]
?

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 => (
{p.brand}
{p.model}
|
))}
{rows.map(row => (
| {row.label} |
{products.map(p => (
{row.get(p)}
|
))}
))}
);
}
Object.assign(window, { ProductCard, ProductModal, CompareModal, Icon, fmtINR, buildWaUrl, PlaceholderArt, variantsOf, VariantPills });