// pages-shop.jsx — Home, Shop, PDP — all wired to FF_API
const { useState: useStateP, useEffect: useEffectP, useMemo: useMemoP } = React;
// ─── HOME PAGE ────────────────────────────────────────────────────────────────
function HomePage({ goto, addToCart, cardStyle, wishIds, toggleWishlist }) {
const [viral, setViral] = useStateP([]);
const [featured, setFeatured] = useStateP([]);
const [cats, setCats] = useStateP([]);
const [loading, setLoading] = useStateP(true);
useEffectP(() => {
if (typeof FF_API === 'undefined') { setLoading(false); return; }
Promise.all([
FF_API.products.list({ tag: 'viral', limit: 4 }),
FF_API.products.list({ sort: 'sold', limit: 8 }),
FF_API.products.categories(),
]).then(([v, f, c]) => {
setViral(v.products || []);
setFeatured(f.products || []);
setCats(c || []);
}).catch(console.error)
.finally(() => setLoading(false));
}, []);
// Collage uses first 4 featured products
const col = featured.slice(0, 4);
return (
{/* ── HERO — mobile-first, product-first ── */}
{/* Mobile hero — full bleed product strip + hook */}
{/* Product image strip — scrollable, shows goods immediately */}
{col.length > 0 && (
{[...col, ...viral.slice(0, 4)].filter(Boolean).filter((p, i, a) => a.findIndex(x => x.id === p.id) === i).slice(0, 6).map((p, i) => (
goto({ page: 'pdp', id: p.id })}
style={{
flexShrink: 0, width: 180, borderRadius: 16, overflow: 'hidden',
position: 'relative', cursor: 'pointer',
boxShadow: '0 4px 20px rgba(0,0,0,.12)',
transform: i % 2 === 0 ? 'rotate(-1deg)' : 'rotate(1deg)',
}}>
{/* Price badge */}
{fmtINR(p.price)}
{/* Discount badge */}
{p.compare > p.price && (
-{Math.round((1 - p.price / p.compare) * 100)}%
)}
))}
{/* Trailing CTA card */}
goto({ page: 'shop' })}
style={{
flexShrink: 0, width: 140, borderRadius: 16,
background: 'var(--ff-accent)', cursor: 'pointer',
display: 'flex', flexDirection: 'column',
alignItems: 'center', justifyContent: 'center',
gap: 8, padding: 20, color: '#fff', height: 220,
}}>
✦
See all finds
)}
{/* Hook text + CTA */}
{/* Trust badges row */}
{[
{ icon: '⚡', text: '48h shipping' },
{ icon: '❤️', text: 'With love' },
{ icon: '💳', text: 'Secure checkout' },
{ icon: '⭐', text: '4.8 avg rating' },
].map(b => (
{b.icon} {b.text}
))}
The internet's
most obsessed-over
products. Delivered.
Viral finds from across the web — tested, curated, shipped to your door in 48h.
goto({ page: 'shop' })}>
Shop the Finds ✦
goto({ page: 'shop', filter: 'viral' })}>
🔥 Viral
{/* Desktop hero — original layout, improved */}
{[
{ icon: '⚡', text: '48h shipping' },
{ icon: '❤️', text: 'With love' },
{ icon: '💳', text: 'Secure checkout' },
{ icon: '⭐', text: '4.8 avg rating' },
].map(b => (
{b.icon} {b.text}
))}
The internet's
most obsessed-over
products. Delivered.
We hunt viral finds from across the internet, test them, and ship the good ones to your door in 48 hours. No fluff.
goto({ page: 'shop' })}>Shop the Finds
goto({ page: 'shop', filter: 'viral' })}>🔥 What's viral
{/* Desktop collage */}
{col[0] &&
}
{col[1] &&
}
{col[2] &&
}
{col[3] &&
}
As seen 6M+ times ✦
{/* Press marquee */}
{[...FF_PRESS, ...FF_PRESS, ...FF_PRESS].map((p, i) => (
✦ AS SEEN ON {p}
))}
{/* Viral row */}
TRENDING NOW
The viral wall ✦
goto({ page: 'shop', filter: 'viral' })}>See all viral
{loading ? : (
{viral.map(p =>
goto({ page: 'pdp', id: p.id })} cardStyle={cardStyle} wishlisted={wishIds?.has(p.id)} onWishlist={toggleWishlist} />)}
)}
{/* ── Social proof bar ── */}
{[
{ n: '127K+', l: 'Happy customers', icon: '😊' },
{ n: '4.8★', l: 'Average rating', icon: '⭐' },
{ n: '48h', l: 'Ship time', icon: '⚡' },
{ n: 'COD', l: 'Cash on delivery', icon: '💳' },
{ n: 'Easy', l: 'Returns', icon: '🔄' },
].map((s, i) => (
))}
{/* ── UGC strip — customer photos & videos ── */}
{/* Category grid — from API */}
Pick your rabbit hole
{cats.filter(c => c.id !== 'all').map((c, i) => {
const catColors = ['var(--ff-lime)', 'var(--ff-lav)', 'var(--ff-sky)', 'var(--ff-sun)', 'var(--ff-pink)', 'var(--ff-mint)'];
return (
goto({ page: 'shop', cat: c.id })} className="ff-card" style={{
background: catColors[i % catColors.length], padding: '22px 20px', textAlign: 'left', minHeight: 130,
display: 'flex', flexDirection: 'column', justifyContent: 'space-between',
}}>
{c.label}
Shop now
);
})}
{/* Blog strip */}
{/* How we work */}
{[
{ t: 'We hunt', d: "Our team scrolls so you don't have to. 800+ products screened every week." },
{ t: 'We test', d: 'Every item we list is put through 30 days of real use. Fails get cut.' },
{ t: 'We ship', d: 'Fulfilled from our warehouse in 48 hours. Free over ₹999.' },
].map((f, i) => (
))}
{/* All products — live from DB */}
Everything in stock
goto({ page: 'shop' })}>Browse all
{loading ? : (
{featured.slice(0, 8).map(p =>
goto({ page: 'pdp', id: p.id })} cardStyle={cardStyle} wishlisted={wishIds?.has(p.id)} onWishlist={toggleWishlist} />)}
)}
{/* Reviews strip — static, marketing copy */}
Real reviews, fussy people
{[
{ q: "My sister thought I spent a fortune. I did not.", n: 'Priya · Bangalore', s: 5 },
{ q: 'They said 48h shipping. It was 31.', n: 'Rahul · Delhi', s: 5 },
{ q: "Finally a site that doesn't lie about sizes.", n: 'Aisha · Mumbai', s: 4 },
].map((r, i) => (
))}
);
}
function Stat({ n, l }) {
return (
);
}
// Loading grid skeleton
function LoadingGrid({ count = 4 }) {
return (
{Array.from({ length: count }).map((_, i) => (
))}
);
}
// ─── SHOP PAGE ────────────────────────────────────────────────────────────────
// ── ReviewForm — text only (no uploads) ─────────────────────────────────────
function ReviewForm({ productId, onDone }) {
const [name, setName] = useStateP('');
const [stars, setStars] = useStateP(5);
const [title, setTitle] = useStateP('');
const [body, setBody] = useStateP('');
const [saving, setSaving] = useStateP(false);
const [error, setError] = useStateP('');
const submit = async () => {
if (!name.trim() || !body.trim()) { setError('Name and review are required'); return; }
setSaving(true); setError('');
const { ok } = await FF_API.products.addReview(productId, {
name: name.trim(), stars, title: title.trim(), body: body.trim(),
});
setSaving(false);
if (ok) onDone(true);
else setError('Could not submit. Please try again.');
};
return (
{error &&
{error}
}
setName(e.target.value)} style={{ marginBottom: 10 }} />
setTitle(e.target.value)} style={{ marginBottom: 10 }} />
{[1, 2, 3, 4, 5].map(s => (
setStars(s)}
style={{ fontSize: 30, color: s <= stars ? '#F5A623' : 'var(--ff-line)', background: 'none', border: 'none', cursor: 'pointer', padding: 0, lineHeight: 1 }}>
★
))}
{['', 'Terrible', 'Poor', 'OK', 'Good', 'Excellent!'][stars]}
);
}
// ── Helpers for YouTube/video URLs ───────────────────────────────────────────
function getYouTubeId(url) {
if (!url) return null;
const m = url.match(/(?:youtube\.com\/(?:watch\?v=|embed\/|shorts\/)|youtu\.be\/)([\w-]{11})/);
return m ? m[1] : null;
}
function isYouTube(url) { return !!getYouTubeId(url); }
function isVideoFile(url) { return /\.(mp4|mov|webm|ogg)$/i.test(url || ''); }
// ── SpotlightModal — fullscreen lightbox with media + optional product link ──
function SpotlightModal({ p, onClose, goto }) {
const ytId = getYouTubeId(p.media_url);
const isVid = p.media_type === 'video' || isYouTube(p.media_url) || isVideoFile(p.media_url);
// Clean product ID — strip any URL prefix
const rawPid = p.product_id || '';
const pid = rawPid.replace(/^.*\/product\//, '').replace(/^https?:\/\/[^/]+\//, '');
React.useEffect(() => {
const handler = e => e.key === 'Escape' && onClose();
document.addEventListener('keydown', handler);
return () => document.removeEventListener('keydown', handler);
}, []);
return (
e.target === e.currentTarget && onClose()}
style={{
position: 'fixed', inset: 0, background: 'rgba(0,0,0,.92)', zIndex: 800,
display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', padding: 16
}}>
{/* Close */}
✕
{/* Media */}
{ytId ? (
VIDEO
) : isVid ? (
) : (
)}
{/* Caption + CTA */}
{(p.caption || p.reviewer_name || pid) && (
{p.caption &&
{p.caption}
}
{p.reviewer_name &&
— {p.reviewer_name}
}
{pid && (
{ onClose(); goto({ page: 'pdp', id: pid }); }}
style={{
background: '#fff', color: 'var(--ff-ink)', border: 'none', borderRadius: 10,
padding: '10px 20px', fontWeight: 700, fontSize: 14, cursor: 'pointer', flexShrink: 0
}}>
View product →
)}
)}
);
}
// ── SpotlightCard — thumbnail card, opens modal on tap ───────────────────────
function SpotlightCard({ p, onOpen }) {
const ytId = getYouTubeId(p.media_url);
const isVid = p.media_type === 'video' || isYouTube(p.media_url) || isVideoFile(p.media_url);
// Best thumbnail: admin-set > YouTube auto-thumb > video poster
const thumb = p.thumbnail ||
(ytId ? 'https://img.youtube.com/vi/' + ytId + '/hqdefault.jpg' : null) ||
(p.media_type === 'photo' ? p.media_url : null);
return (
e.currentTarget.style.transform = 'scale(1.02)'}
onMouseLeave={e => e.currentTarget.style.transform = 'scale(1)'}>
{/* Thumbnail image */}
{thumb
?
:
{isVid ? '🎬' : '📷'}
}
{/* Play button for videos */}
{isVid && (
)}
{/* Gradient + caption */}
{p.caption &&
{p.caption}
}
{p.reviewer_name &&
— {p.reviewer_name}
}
);
}
// ── UGCStrip — "Spotted in the wild" horizontal scroll ────────────────────────
function UGCStrip({ goto }) {
const [posts, setPosts] = useStateP([]);
const [modal, setModal] = useStateP(null);
useEffectP(() => {
if (typeof FF_API === 'undefined') return;
FF_API.content?.spotlight({ featured: 1, limit: 6 }).then(d => setPosts(Array.isArray(d) ? d : []));
}, []);
if (!posts.length) return null;
return (
<>
{modal && setModal(null)} goto={goto} />}
Real customers
Spotted in the wild ✦
Real moments, real people
See all →
{posts.map(p => (
setModal(p)} />
))}
>
);
}
// ── BlogStrip — tips/articles section ────────────────────────────────────────
function BlogStrip() {
const [posts, setPosts] = useStateP([]);
useEffectP(() => {
if (typeof FF_API === 'undefined') return;
FF_API.content?.blogList({ limit: 3 }).then(d => setPosts(Array.isArray(d) ? d : []));
}, []);
if (!posts.length) return null;
return (
Tips & ideas
From the Fussy Finds team
{posts.map(p => (
{p.cover_image &&
}
{p.tag &&
{p.tag} }
{p.title}
{p.excerpt &&
{p.excerpt}
}
Read more
))}
);
}
function ShopPage({ goto, addToCart, cardStyle, initialFilter, initialCat, initialQuery, wishIds, toggleWishlist }) {
const [cat, setCat] = useStateP(initialCat || 'all');
const [tag, setTag] = useStateP(initialFilter || 'all');
const [sort, setSort] = useStateP('sold');
const [q, setQ] = useStateP(initialQuery || '');
const [price, setPrice] = useStateP(5000); // in rupees
const [products, setProducts] = useStateP([]);
const [total, setTotal] = useStateP(0);
const [page, setPage] = useStateP(1);
const [cats, setCats] = useStateP([]);
const [loading, setLoading] = useStateP(true);
// Load categories once
useEffectP(() => {
if (typeof FF_API === 'undefined') return;
FF_API.products.categories().then(setCats).catch(console.error);
}, []);
// Reload products when filters change
useEffectP(() => {
if (typeof FF_API === 'undefined') { setLoading(false); return; }
setLoading(true);
const params = { page, limit: 20, sort };
if (cat !== 'all') params.cat = cat;
if (tag !== 'all') params.tag = tag;
if (q) params.q = q;
// price filter is client-side (API doesn't have max_price param)
FF_API.products.list(params)
.then(r => { setProducts(r.products || []); setTotal(r.total || 0); })
.catch(console.error)
.finally(() => setLoading(false));
}, [cat, tag, sort, q, page]);
// Client-side price filter
// product.price is in paise, price state is in rupees — convert for comparison
const items = useMemoP(() => products.filter(p => (parseInt(p.price) || 0) / 100 <= price), [products, price]);
const catLabel = cats.find(c => c.id === cat)?.label || '';
return (
{tag === 'viral' ? 'The viral wall ✦' : tag === 'new' ? 'New drops ✦' : cat !== 'all' ? catLabel : 'All the finds'}
{total} products · updated daily
{/* Sidebar */}
Search
{ setQ(e.target.value); setPage(1); }} placeholder="Find something…" className="ff-input" style={{ padding: '8px 12px' }} />
Category
{cats.map(c => (
{ setCat(c.id); setPage(1); }} style={{
textAlign: 'left', padding: '7px 10px', borderRadius: 8,
background: cat === c.id ? 'var(--ff-ink)' : 'transparent',
color: cat === c.id ? '#fff' : 'inherit',
fontWeight: cat === c.id ? 600 : 500, fontSize: 14,
}}>{c.label}
))}
Tags
{[{ k: 'all', l: 'All' }, { k: 'viral', l: 'Viral ✦' }, { k: 'new', l: 'New' }, { k: 'bestseller', l: 'Best' }].map(t => (
{ setTag(t.k); setPage(1); }} className="ff-chip" style={{
background: tag === t.k ? 'var(--ff-ink)' : '#fff',
color: tag === t.k ? '#fff' : 'inherit',
}}>{t.l}
))}
Max price: {fmtINR(price)}
setPrice(+e.target.value)} style={{ width: '100%' }} />
{/* Results */}
{cat !== 'all' && setCat('all')}>✕ {catLabel} }
{tag !== 'all' && setTag('all')}>✕ {tag} }
{q && setQ('')}>✕ "{q}" }
Sort:
{ setSort(e.target.value); setPage(1); }} className="ff-input" style={{ padding: '8px 12px', width: 'auto' }}>
Most popular
Highest rated
Price: Low → High
Price: High → Low
{loading ?
: items.length === 0 ? (
🥲
Nothing matches yet
Try clearing a filter or searching something broader.
) : (
{items.map(p =>
goto({ page: 'pdp', id: p.id })} cardStyle={cardStyle} wishlisted={wishIds?.has(p.id)} onWishlist={toggleWishlist} />)}
)}
{/* Pagination */}
{!loading && total > 20 && (
setPage(p => p - 1)}>← Prev
Page {page} of {Math.ceil(total / 20)}
= total} onClick={() => setPage(p => p + 1)}>Next →
)}
);
}
// ─── DESCRIPTION BLOCK — formats product description text ────────────────────
// Handles: – bullet points, **bold**, ALL CAPS HEADERS:, plain paragraphs
function DescriptionBlock({ text }) {
if (!text) return null;
// Split on newlines and also on "– " bullet markers inline
// Some descriptions use – for bullet points inline in one long string
const raw = text
.replace(/\s*[–—-]\s+/g, '\n– ') // normalize bullet chars to newlines
.replace(/\n{3,}/g, '\n\n') // max 2 newlines
.trim();
const lines = raw.split('\n').filter(l => l.trim());
const renderLine = (line, key) => {
const trimmed = line.trim();
// Bullet point
if (trimmed.startsWith('– ') || trimmed.startsWith('- ') || trimmed.startsWith('• ')) {
const content = trimmed.replace(/^[–\-•]\s*/, '');
return (
✦
{renderInline(content)}
);
}
// ALL CAPS HEADER: pattern (e.g. "PREMIUM STAINLESS STEEL:")
if (/^[A-Z][A-Z\s&,\-]{4,}:/.test(trimmed)) {
const [head, ...rest] = trimmed.split(':');
return (
0 ? 12 : 0 }}>
{head.trim()}
{rest.join(':').trim() && {renderInline(rest.join(':').trim())} }
);
}
// Empty line = spacer
if (!trimmed) return
;
// Normal paragraph
return (
{renderInline(trimmed)}
);
};
// Inline: handle **bold** markers
const renderInline = (text) => {
if (!text.includes('**')) return text;
const parts = text.split(/\*\*(.*?)\*\*/g);
return parts.map((part, i) => i % 2 === 1 ? {part} : part);
};
return (
{lines.map((line, i) => renderLine(line, i))}
);
}
// ─── PRODUCT DETAIL PAGE ─────────────────────────────────────────────────────
function ProductPage({ id, goto, addToCart, cardStyle, cart, wishIds, toggleWishlist }) {
const [product, setProduct] = useStateP(null);
const [related, setRelated] = useStateP([]);
const [qty, setQty] = useStateP(1);
// How many already in cart
const cartQty = React.useMemo(() =>
(cart || []).find(i => i.product_id === id)?.qty || 0,
[cart, id]
);
const [loading, setLoading] = useStateP(true);
const [revName, setRevName] = useStateP('');
const [revStars, setRevStars] = useStateP(5);
const [revBody, setRevBody] = useStateP('');
const [revSent, setRevSent] = useStateP(false);
const [revModal, setRevModal] = useStateP(null); // media lightbox
const [activeImg, setActiveImg] = useStateP(0);
useEffectP(() => {
if (!id || typeof FF_API === 'undefined') return;
setLoading(true);
FF_API.products.get(id)
.then(p => {
setProduct(p);
document.title = p.name + ' · Fussy Finds';
if (window.FF_TRACK) window.FF_TRACK('product_view', { product_id: p.id, name: p.name, price: p.price });
// Update OG tags dynamically for WhatsApp/social sharing
const setMeta = (prop, val, attr = 'property') => {
let el = document.querySelector(`meta[${attr}="${prop}"]`);
if (!el) { el = document.createElement('meta'); el.setAttribute(attr, prop); document.head.appendChild(el); }
el.setAttribute('content', val);
};
setMeta('og:title', p.name + ' · Fussy Finds');
setMeta('og:description', p.tagline || 'Shop ' + p.name + ' on Fussy Finds');
setMeta('og:url', window.location.href);
if (Array.isArray(p.images) && p.images[0]) setMeta('og:image', p.images[0]);
setMeta('name', 'description', 'name');
// Save to recently viewed
try {
const rv = JSON.parse(localStorage.getItem('ff_recently_viewed') || '[]');
const filtered = rv.filter(x => x.id !== p.id);
filtered.unshift({ id: p.id, name: p.name, price: p.price, swatch: p.swatch, image: Array.isArray(p.images) ? p.images[0] : null });
localStorage.setItem('ff_recently_viewed', JSON.stringify(filtered.slice(0, 8)));
} catch { }
return FF_API.products.list({ cat: p.cat, limit: 5 });
})
.then(r => setRelated((r.products || []).filter(p => p.id !== id).slice(0, 4)))
.catch(console.error)
.finally(() => setLoading(false));
}, [id]);
if (loading) return Loading…
;
if (!product) return Product not found.
;
// Variants are separate products — use product data directly
const activeImages = Array.isArray(product.images) ? product.images : [];
const activePrice = product.price;
const activeStock = product.stock;
const discount = product.compare > product.price ? Math.round((1 - product.price / product.compare) * 100) : 0;
const reviews = product.review_list || [];
const submitReview = async () => {
if (!revName || !revBody) return;
const { ok } = await FF_API.products.addReview(product.id, { name: revName, stars: revStars, title: '', body: revBody });
if (ok) { setRevSent(true); setRevName(''); setRevBody(''); }
};
return (
{/* ── GALLERY with vertical thumbnail strip + carousel ── */}
{(() => {
const imgs = Array.isArray(product.images) && product.images.length > 0 ? product.images : null;
const total = imgs ? imgs.length : 0;
const prev = () => setActiveImg(i => (i - 1 + total) % total);
const next = () => setActiveImg(i => (i + 1) % total);
return (
1 ? '72px 1fr' : '1fr', gap: 10 }}>
{/* Vertical thumbnail strip (only if >1 image) */}
{imgs && total > 1 && (
{imgs.map((url, i) => (
setActiveImg(i)} style={{
width: 68, height: 68, borderRadius: 10, overflow: 'hidden', flexShrink: 0,
cursor: 'pointer', border: activeImg === i ? '2.5px solid var(--ff-accent)' : '2px solid rgba(0,0,0,.1)',
opacity: activeImg === i ? 1 : 0.7, transition: 'all .15s',
}}>
))}
)}
{/* Main image + carousel arrows */}
{ if (imgs && total > 1) e.currentTarget._tx = e.touches[0].clientX; }}
onTouchEnd={e => {
if (!imgs || total <= 1 || !e.currentTarget._tx) return;
const dx = e.changedTouches[0].clientX - e.currentTarget._tx;
if (dx < -40) next();
else if (dx > 40) prev();
}}
>
{imgs ? (
{ e.target.style.display = 'none'; }}
/>
) : (
)}
{/* Carousel arrows */}
{imgs && total > 1 && (
<>
‹
›
>
)}
{/* Dot indicators */}
{imgs && total > 1 && (
{imgs.map((_, i) => (
setActiveImg(i)} style={{
width: activeImg === i ? 20 : 8, height: 8, borderRadius: 999,
background: activeImg === i ? 'var(--ff-accent)' : 'rgba(255,255,255,.7)',
border: '1.5px solid rgba(0,0,0,.2)', padding: 0, cursor: 'pointer',
transition: 'width .2s',
}} />
))}
)}
{/* Image counter badge */}
{imgs && total > 1 && (
{activeImg + 1} / {total}
)}
);
})()}
{/* ── PRODUCT INFO ── */}
{(Array.isArray(product.tags) ? product.tags : []).includes('viral') && VIRAL ✦ }
{(Array.isArray(product.tags) ? product.tags : []).includes('new') && NEW }
{(Array.isArray(product.tags) ? product.tags : []).includes('bestseller') && BESTSELLER }
{product.name}
{product.tagline}
{/* ── Formatted description ── */}
{product.description && (
)}
{parseFloat(product.rating) || 0} · {Number(product.reviews || 0).toLocaleString()} reviews · {Number(product.sold || 0).toLocaleString()} sold
{fmtINR(activePrice)}
{discount > 0 && <>
{fmtINR(product.compare)}
Save {discount}%
>}
Inclusive of all taxes · Free shipping over ₹999
{/* Variants — each is a full product, clicking navigates to it */}
{product.variants && product.variants.length > 1 && (
Also available in
{product.variants.map(v => {
const isCurrent = v.is_current || v.id === product.id;
const outOfStock = (v.stock ?? 1) === 0;
const thumb = v.images?.[0] || null;
return (
{ if (!isCurrent && !outOfStock) location.href = '/product/' + v.id; }}
title={(v.variant_label || v.name) + (outOfStock ? ' — Out of stock' : '')}
style={{
display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 5,
border: isCurrent ? '2px solid var(--ff-ink)' : '1.5px solid var(--ff-line)',
borderRadius: 10, padding: 8,
background: 'var(--ff-card)',
cursor: (isCurrent || outOfStock) ? 'default' : 'pointer',
opacity: outOfStock ? .45 : 1,
boxShadow: isCurrent ? '0 0 0 2px var(--ff-bg),0 0 0 4px var(--ff-ink)' : 'none',
transition: 'all .15s',
minWidth: 64, maxWidth: 80,
}}>
{/* Image thumbnail */}
{thumb &&
}
{/* Label — show variant_label, or 'Original' for root (no parent_id) */}
{v.variant_label || (!v.parent_id ? 'Original' : v.name)}
{outOfStock && (
SOLD OUT
)}
);
})}
)}
{/* Media lightbox */}
{revModal && (
setRevModal(null)} style={{
position: 'fixed', inset: 0, background: 'rgba(0,0,0,.88)', zIndex: 600,
display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', padding: 20,
}}>
e.stopPropagation()} style={{ maxWidth: 520, width: '100%' }}>
{revModal.type === 'video'
?
:
}
{revModal.body && (
{[1, 2, 3, 4, 5].map(s => ★ )}
{revModal.body}
— {revModal.reviewer}
)}
setRevModal(null)} style={{ position: 'fixed', top: 20, right: 20, background: 'rgba(255,255,255,.15)', border: 'none', color: '#fff', fontSize: 20, width: 40, height: 40, borderRadius: 999, cursor: 'pointer' }}>✕
)}
{/* Qty + add */}
{/* Qty selector — locked at stock - cartQty */}
{(() => {
const maxCanAdd = Math.max(0, (activeStock || 0) - cartQty);
const atMax = qty >= maxCanAdd;
const canIncrease = !atMax && maxCanAdd > 0;
return (<>
setQty(Math.max(1, qty - 1))} disabled={qty <= 1}
style={{ padding: '8px 10px', opacity: qty <= 1 ? .4 : 1 }}>
{qty}
{
if (!canIncrease) return;
setQty(qty + 1);
// Warn when approaching limit
if (qty + 1 >= maxCanAdd && maxCanAdd <= 3) {
window.FF_TOAST && window.FF_TOAST(`Only ${maxCanAdd} available — that's all we have!`);
}
}}
disabled={!canIncrease}
title={!canIncrease ? `Max ${maxCanAdd} available` : ''}
style={{ padding: '8px 10px', opacity: canIncrease ? 1 : .35, cursor: canIncrease ? 'pointer' : 'not-allowed' }}>
>);
})()}
addToCart(product, qty)}>Add to cart · {fmtINR(product.price * qty)}
toggleWishlist && toggleWishlist(product.id)} style={{
width: 52, height: 52, borderRadius: 12, border: '1.5px solid var(--ff-line)',
background: wishIds?.has(product.id) ? 'var(--ff-accent)' : 'var(--ff-card)',
color: wishIds?.has(product.id) ? '#fff' : 'var(--ff-ink)',
fontSize: 22, cursor: 'pointer', flexShrink: 0, display: 'flex', alignItems: 'center', justifyContent: 'center',
transition: 'all .18s',
}} title={wishIds?.has(product.id) ? 'Remove from wishlist' : 'Save to wishlist'}>
{wishIds?.has(product.id) ? '♥' : '♡'}
{ addToCart(product, qty); goto({ page: 'checkout' }); }}>Buy now
{/* Share */}
{
const url = location.origin + '/product/' + product.id;
if (navigator.share) {
navigator.share({ title: product.name, text: product.tagline || '', url })
.then(() => typeof FF_API !== 'undefined' && FF_API.content?.trackShare(product.id, 'native'));
} else {
navigator.clipboard.writeText(url);
window.FF_TOAST && window.FF_TOAST('Link copied ✦');
typeof FF_API !== 'undefined' && FF_API.content?.trackShare(product.id, 'copy');
}
}} style={{ flex: 1, display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 6, fontSize: 13, color: 'var(--ff-muted)', background: 'none', border: '1.5px solid var(--ff-line)', borderRadius: 8, padding: '9px 0', cursor: 'pointer' }}>
↗ Share
{
const url = 'https://wa.me/?text=' + encodeURIComponent(product.name + ' — ' + location.origin + '/product/' + product.id);
window.open(url, '_blank');
typeof FF_API !== 'undefined' && FF_API.content?.trackShare(product.id, 'whatsapp');
}} style={{ flex: 1, display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 6, fontSize: 13, color: '#25D366', background: 'none', border: '1.5px solid #25D366', borderRadius: 8, padding: '9px 0', cursor: 'pointer' }}>
💬 WhatsApp
{/* Trust strip */}
All sales final
Contact us for defects
{product.stock <= 10 && product.stock > 0 && (() => {
const remaining = product.stock - cartQty;
const isCritical = remaining <= 3;
if (remaining <= 0) return null; // fully in cart
return (
{isCritical ? '🔥' : '⚡'}
{isCritical ? `Only ${remaining} left — almost gone!` : `Only ${remaining} left in stock`}
{cartQty > 0 ? `${cartQty} already in your cart.` : (isCritical ? 'Order now before it sells out.' : 'Selling fast — order soon.')}
);
})()}
{product.stock === 0 && (
Out of stock
We're restocking soon. Check back shortly.
)}
{/* ── Reviews section ── */}
{/* Rating summary */}
Customer reviews
{[1, 2, 3, 4, 5].map(s => (
★
))}
{product.rating || 0}
· {Number(product.reviews || 0).toLocaleString()} reviews
document.getElementById('review-form')?.scrollIntoView({ behavior: 'smooth' })}>
✍ Write a review
{/* Review cards */}
{reviews.map((r, i) => (
{[1, 2, 3, 4, 5].map(s => (
★
))}
{r.verified == 1 && (
✓ Verified
)}
{r.title &&
{r.title}
}
{r.body}
{r.name} · {r.created_at?.slice(0, 10)}
FF_API.products.reviewHelpful(r.id)}
style={{ fontSize: 11, color: 'var(--ff-muted)', background: 'none', border: 'none', cursor: 'pointer', padding: 0 }}>
👍 Helpful {r.helpful > 0 ? '(' + r.helpful + ')' : ''}
))}
{!reviews.length && (
No reviews yet — be the first to review this product!
)}
{/* Write a review form */}
{revSent ? '✦ Thanks for your review!' : 'Write a review'}
{!revSent && (
{ if (ok) setRevSent(true); }} />
)}
{/* Recently Viewed */}
{/* Related */}
{related.length > 0 && (
People also grabbed
{related.map(p =>
goto({ page: 'pdp', id: p.id })} cardStyle={cardStyle} wishlisted={wishIds?.has(p.id)} onWishlist={toggleWishlist} />)}
)}
);
}
// ─── RECENTLY VIEWED ─────────────────────────────────────────────────────────
function RecentlyViewed({ currentId, goto, addToCart, cardStyle, wishIds, toggleWishlist }) {
const [items, setItems] = useStateP([]);
useStateP(() => { }); // dummy to keep hook count stable
useEffectP(() => {
try {
const rv = JSON.parse(localStorage.getItem('ff_recently_viewed') || '[]');
setItems(rv.filter(p => p.id !== currentId).slice(0, 4));
} catch { }
}, [currentId]);
if (items.length === 0) return null;
return (
Recently viewed
{items.map(p => (
goto({ page: 'pdp', id: p.id })} className="ff-card"
style={{ cursor: 'pointer', overflow: 'hidden', display: 'flex', flexDirection: 'column' }}>
{p.image
?
:
{p.name}
}
{p.name}
{fmtINR(p.price)}
))}
);
}
// SearchPage — ShopPage with search pre-filled from URL
function SearchPage({ goto, addToCart, cardStyle, wishIds, toggleWishlist }) {
const q = new URLSearchParams(location.search).get('q') || '';
return ;
}
// CategoryPage — ShopPage with category pre-selected
function CategoryPage({ goto, addToCart, cardStyle, wishIds, toggleWishlist }) {
const slug = FF_PATH_ID() || new URLSearchParams(location.search).get('slug') || '';
return ;
}
Object.assign(window, { HomePage, ShopPage, ProductPage, LoadingGrid, SearchPage, CategoryPage });