// Shared UI primitives for Fussy Finds
const { useState, useEffect, useRef, useMemo, useCallback } = React;
const fmtINR = (n) => '₹' + Number(n).toLocaleString('en-IN', { minimumFractionDigits: 0, maximumFractionDigits: 2 });
// Star row (outline + fill)
function Stars({ value = 5, size = 14 }) {
const full = Math.round(value);
return (
{[1, 2, 3, 4, 5].map(i => (
))}
);
}
// Placeholder product "image" — striped with a mono label
function ProductArt({ product, ratio = '1 / 1', size = 'md' }) {
const labelMap = { home: 'home', kitchen: 'kitchen', beauty: 'beauty', tech: 'tech', wellness: 'wellness', desk: 'desk' };
// Use real image if available
const imgs = Array.isArray(product.images) ? product.images : [];
const mainImg = imgs.find(u => u && u.trim()) || null;
if (mainImg) {
return (

{ e.target.style.display = 'none'; e.target.parentNode.style.background = product.swatch || '#FFC7A8'; }}
/>
);
}
// Fallback: coloured placeholder with product name
return (
{product.name.split(' ').map((w, i) => (
{w}
))}
product · {labelMap[product.cat] || product.cat}
);
}
// Product card (2 styles toggled via CSS class)
function ProductCard({ product, onAdd, onOpen, cardStyle = 'sticker', wishlisted = false, onWishlist }) {
// Safe guards — API may return null/string for these fields
const tags = Array.isArray(product.tags) ? product.tags : [];
const price = parseInt(product.price) || 0;
const compare = parseInt(product.compare) || 0;
const reviews = parseInt(product.reviews) || 0;
const rating = parseFloat(product.rating) || 0;
const discount = compare > price ? Math.round((1 - price / compare) * 100) : 0;
const outOfStock = product.stock === 0 || product.stock === '0';
const tagEl = outOfStock ? OUT OF STOCK
: tags.includes('viral') ? VIRAL ✦
: tags.includes('bestseller') ? BESTSELLER
: tags.includes('new') ? NEW
: null;
const HeartBtn = () => (
);
if (cardStyle === 'minimal') {
return (
e.currentTarget.style.transform = 'translateY(-3px)'}
onMouseOut={e => e.currentTarget.style.transform = 'translateY(0)'}
>
{tagEl &&
{tagEl}
}
{discount > 0 && (
−{discount}%
)}
{product.name}
{fmtINR(product.price)}
{rating}
{outOfStock
? Out of stock
:
}
);
}
// "sticker" default — bold border + shadow + colored footer
return (
{ e.currentTarget.style.transform = 'translateY(-3px)'; e.currentTarget.style.boxShadow = 'var(--ff-shadow-lg)'; }}
onMouseOut={e => { e.currentTarget.style.transform = 'translateY(0)'; e.currentTarget.style.boxShadow = 'var(--ff-shadow)'; }}
>
{tagEl &&
{tagEl}
}
{discount > 0 && (
−{discount}%
)}
{product.name}
{product.tagline}
{rating} · {reviews.toLocaleString()}
{fmtINR(product.price)}
{compare > price && (
{fmtINR(product.compare)}
)}
{outOfStock
?
Out of stock
:
}
);
}
// ─── Theme Switcher — Myntra-style floating palette ─────────────────────────
const FF_THEMES = {
warm: { name: 'Warm', sub: 'Fraunces · DM Sans', file: 'theme-warm.css', preview: ['#D4007A', '#FAFAF8', '#181510'] },
editorial: { name: 'Editorial', sub: 'Cormorant · Inter', file: 'theme-editorial.css', preview: ['#F9F8F5', '#111110', '#9B9590'] },
bold: { name: 'Bold', sub: 'Space Grotesk · Energetic', file: 'theme-bold.css', preview: ['#FFF8EC', '#E4439C', '#D4FF4D'] },
mono: { name: 'Mono', sub: 'Grayscale · Minimal', file: 'theme-mono.css', preview: ['#FFFFFF', '#000000', '#888888'] },
myntra: { name: 'Myntra', sub: 'Indian Commerce · Bold Pink', file: 'theme-myntra.css', preview: ['#F4F4F4', '#FF3F6C', '#282C3F'] },
};
// Resolve base path (works from both root and /pages/ subdirectory)
const _themePath = () => {
const p = window.location.pathname;
return p.includes('/pages/') ? '../' : './';
};
function ThemeSwitcher() {
const [open, setOpen] = React.useState(false);
const [activeTheme, setActiveTheme] = React.useState(() => localStorage.getItem('ff_theme') || 'warm');
React.useEffect(() => {
injectTheme(activeTheme);
}, []);
const injectTheme = (key) => {
const theme = FF_THEMES[key];
if (!theme) return;
// Remove existing theme link if any
const existing = document.getElementById('ff-theme-link');
if (existing) existing.remove();
// Inject new theme CSS
const link = document.createElement('link');
link.id = 'ff-theme-link';
link.rel = 'stylesheet';
link.href = _themePath() + theme.file;
document.head.appendChild(link);
localStorage.setItem('ff_theme', key);
setActiveTheme(key);
};
return (
{open && (
Site theme
{Object.entries(FF_THEMES).map(([key, theme]) => (
))}
Saved for your visits
)}
);
}
// Icon helpers
const Icon = {
Search: (p) => ,
Bag: (p) => ,
User: (p) => ,
Close: (p) => ,
Menu: (p) => ,
Arrow: (p) => ,
Heart: (p) => ,
Check: (p) => ,
Box: (p) => ,
Truck: (p) => ,
Tag: (p) => ,
Filter: (p) => ,
Plus: (p) => ,
Minus: (p) => ,
Trash: (p) => ,
Chart: (p) => ,
Grid: (p) => ,
Star: (p) => ,
};
// Header
function Header({ route, goto, cart, openCart, user, onLogin, search, setSearch }) {
const count = (cart || []).reduce((s, i) => s + i.qty, 0);
const [mopen, setMopen] = useState(false);
return (
{/* Announcement bar */}
FREE SHIPPING OVER ₹999 ✦ ALL SALES FINAL ✦ COD AVAILABLE
✦
Fussy Finds
setSearch(e.target.value)}
onKeyDown={e => { if (e.key === 'Enter' && search) location.href = FF_URLS.search(search); }}
placeholder="Search 2,000+ viral finds…"
className="ff-input"
style={{ paddingLeft: 42, paddingRight: 16, borderRadius: 999 }}
/>
{user && (
♥
)}
{mopen && (
)}
);
}
// Footer
function Footer() {
const base = (() => { const p = location.pathname; return p.includes('/pages/') ? '../' : './'; })();
const [email, setEmail] = React.useState('');
const [joined, setJoined] = React.useState(false);
const [subLoading, setSubLoading] = React.useState(false);
const [subError, setSubError] = React.useState('');
const handleJoin = async () => {
if (!email || !email.includes('@')) { setSubError('Enter a valid email'); return; }
setSubLoading(true); setSubError('');
try {
// Get referral code from URL if present
const ref = new URLSearchParams(location.search).get('ref') || '';
const res = await fetch('/api/newsletter.php?action=subscribe', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email, ref }),
});
const data = await res.json();
if (data.ok) {
setJoined(true);
} else {
setSubError(data.error || 'Something went wrong');
}
} catch (e) {
setSubError('Could not subscribe. Try again.');
} finally {
setSubLoading(false);
}
};
const SHOP_LINKS = [
{ l: 'All Finds', href: base + '/shop' },
{ l: 'Viral ✦', href: base + '/shop?filter=viral' },
{ l: 'New Drops', href: base + '/shop?filter=new' },
{ l: 'Bestsellers', href: base + '/shop?filter=bestseller' },
{ l: 'Blog', href: base + '/blog' },
{ l: 'Recipes', href: base + '/recipes' },
{ l: 'Under ₹999', href: base + '/shop?sort=price_asc' },
];
const HELP_LINKS = [
{ l: 'Shipping Info', href: base + '/shipping-info' },
{ l: 'Track Order', href: base + '/account?tab=orders' },
{ l: 'Contact Us', href: 'mailto:hello@fussyfind.com' },
{ l: 'FAQs', href: base + '/faq' },
];
const BRAND_LINKS = [
{ l: 'Our Story', href: base + '/about' },
{ l: 'Journal', href: '/about' },
{ l: 'Creator Program', href: 'mailto:collab@fussyfind.com' },
{ l: 'Press', href: 'mailto:press@fussyfind.com' },
];
const LinkCol = ({ title, links }) => (
);
return (
);
}
// Toast system
function Toast({ toast, onDismiss }) {
useEffect(() => {
if (!toast) return;
const t = setTimeout(onDismiss, 2800);
return () => clearTimeout(t);
}, [toast]);
if (!toast) return null;
return (
✦ {toast}
);
}
Object.assign(window, { Stars, ProductArt, ProductCard, Icon, Header, Footer, Toast, fmtINR, ThemeSwitcher, FF_THEMES });