// ═══════════════════════════════════════════════════════════════ // Aurelle Bride — components.jsx // Shared utilities, hooks, and UI components // Loaded first; assigns to window.* for cross-file access // ═══════════════════════════════════════════════════════════════ window.Logo = function Logo({ size = 28, mark = false }) { return (
Aurelle {!mark && Bride}
); }; window.I = { arrow: () => , sun: () => , moon: () => , plus: () => , close: () => , cog: () => , upload: () => , }; window.Placeholder = function Placeholder({ label = '', tone = 'default' }) { return (
{label || 'No Image'}
); }; window.useApp = function useApp() { return { theme: document.documentElement.dataset.theme || 'dark', toggleTheme: () => { const current = document.documentElement.dataset.theme || 'dark'; document.documentElement.dataset.theme = current === 'dark' ? 'light' : 'dark'; }, navigate: (path) => { window.location.hash = '#/' + path; }, showToast: (msg) => alert(msg), formatPrice: window.formatPKR }; }; const { useState, useEffect, useRef, useCallback, useMemo } = React; // ── Config ──────────────────────────────────────────────────── window.cfg = window.__SITE_CONFIG__ || {}; window.brand = window.cfg.brand || { brandName: 'Aurelle Bride', whatsapp: '+923358154775' }; window.seo = window.cfg.seo || {}; // ── API ─────────────────────────────────────────────────────── window.API_BASE = '/api'; window.apiFetch = async function(path, opts = {}) { const token = localStorage.getItem('ab_token'); const headers = { 'Content-Type': 'application/json', ...(opts.headers || {}) }; if (token) headers['Authorization'] = `Bearer ${token}`; // Auto-append .php if path has no extension and no query string starting segment const fullPath = path.replace(/^(\/(admin\/)?[a-z_]+)(\/|$|\?)/, (m, seg, adm, rest) => { return seg + '.php' + rest; }); const res = await fetch(window.API_BASE + fullPath, { ...opts, headers }); if (!res.ok) throw new Error(`API ${res.status}: ${await res.text()}`); return res.json(); }; // ── Formatting ──────────────────────────────────────────────── // formatPKR: admin-only, always shows raw PKR. Public prices use . window.formatPKR = function(val) { if (!val) return '—'; return '₨ ' + Number(val).toLocaleString(); }; // Alias kept so any legacy code doesn't break during migration window.formatPrice = window.formatPKR; window.formatDate = function(d) { if (!d) return '—'; return new Date(d).toLocaleDateString('en-GB', { day: 'numeric', month: 'short', year: 'numeric' }); }; window.timeAgo = function(d) { if (!d) return ''; const diff = Date.now() - new Date(d).getTime(); const mins = Math.floor(diff / 60000); if (mins < 60) return `${mins}m ago`; const hrs = Math.floor(mins / 60); if (hrs < 24) return `${hrs}h ago`; return Math.floor(hrs / 24) + 'd ago'; }; window.readingTime = function(html) { const words = (html || '').replace(/<[^>]+>/g, '').split(/\s+/).length; return Math.max(1, Math.round(words / 200)); }; // ── Seed / Fallback Data ────────────────────────────────────── window.SEED_PRODUCTS = [ { id: 8, name: 'Saffron Veil Lehenga', col: 'Lehenga', status: 'AVAILABLE', priceValue: 268000, tone: 'gold', desc: 'Saffron lehenga with hand-couched gold dabka and a sheer silk veil.', fabric_detail: 'Raw silk base with gold dabka threadwork. Sheer chiffon dupatta with scalloped border.', care: 'Dry clean only. Store flat, avoid direct sunlight.', delivery_weeks: 8, tags: 'bridal,lehenga,saffron,gold', image_url: 'https://aurellebride.com/uploads/177c6a178b39a0b6_Lehnga.jpg', date: '2026-04-10', gallery: [{ url: 'https://aurellebride.com/uploads/177c6a178b39a0b6_Lehnga.jpg', label: 'On model' }, { url: 'https://aurellebride.com/uploads/8bfc7416e2aa0556_ChatGPT Image May 2, 2026, 01_42_12 PM.png', label: 'Embroidery detail' }] }, { id: 9, name: 'Bridal Gown', col: 'Gown', status: '1 OF 1', priceValue: 298000, tone: 'default', desc: 'Black silk-satin couture gown for the modern reception bride.', fabric_detail: 'Heavyweight silk-satin with hand-set crystal embellishments on bodice.', care: 'Professional dry clean only.', delivery_weeks: 10, tags: 'bridal,gown,black,reception', image_url: 'https://aurellebride.com/uploads/7a08b8243982c91a_bridal gown.jpg', date: '2026-04-12', gallery: [{ url: 'https://aurellebride.com/uploads/7a08b8243982c91a_bridal gown.jpg', label: 'On model' }] }, { id: 1777709176119, name: 'Bridal Mexi', col: 'Bridal Mexi', status: '1 OF 1', priceValue: 250000, tone: 'rich', desc: 'A richly embroidered mexi silhouette in deep blush.', delivery_weeks: 8, tags: 'bridal,mexi,blush', image_url: 'https://aurellebride.com/uploads/14d49814aaea6b7f_bridal mexi.jpg', date: '2026-05-02', gallery: [{ url: 'https://aurellebride.com/uploads/14d49814aaea6b7f_bridal mexi.jpg', label: 'On model' }] }, { id: 1777711526393, name: 'Sharara Pink', col: 'Sharara', status: '1 OF 1', priceValue: 150000, tone: 'rich', desc: 'Blush-pink sharara with fine kiran border and organza kameez.', delivery_weeks: 7, tags: 'bridal,sharara,pink', image_url: 'https://aurellebride.com/uploads/2f586c5dcc02e4b7_Sharara.jpg', date: '2026-05-02', gallery: [{ url: 'https://aurellebride.com/uploads/2f586c5dcc02e4b7_Sharara.jpg', label: 'On model' }] }, { id: 1777711611120, name: 'Anarkali', col: 'Anarkali', status: '1 OF 1', priceValue: 225000, tone: 'rich', desc: 'Floor-length anarkali with mirror-work yoke and flared skirt.', delivery_weeks: 9, tags: 'bridal,anarkali', image_url: 'https://aurellebride.com/uploads/9bc496a9d070d6fc_Anarkali.jpg', date: '2026-05-02', gallery: [{ url: 'https://aurellebride.com/uploads/9bc496a9d070d6fc_Anarkali.jpg', label: 'On model' }] }, { id: 1777711644161, name: 'New Garara', col: 'Gharara', status: '1 OF 1', priceValue: 175000, tone: 'rich', desc: 'Ivory gharara with vintage gota patti work and raw silk kameez.', delivery_weeks: 8, tags: 'bridal,gharara,ivory', image_url: 'https://aurellebride.com/uploads/b17c151b9d328450_garara.jpg', date: '2026-05-02', gallery: [{ url: 'https://aurellebride.com/uploads/b17c151b9d328450_garara.jpg', label: 'On model' }] }, ]; window.SEED_REVIEWS = [ { id: 1, name: 'Sana M.', location: 'Dubai, UAE', occasion: 'Wedding · Jan 2026', rating: 5, title: 'Worth every penny', body: 'I wore the Saffron Veil Lehenga for my walima and received compliments all night. The craftsmanship is extraordinary — you can feel the hand-work in every inch.', featured: 1, status: 'approved' }, { id: 2, name: 'Ayesha K.', location: 'London, UK', occasion: 'Engagement · Dec 2025', rating: 5, title: 'A dream come true', body: 'Aurelle created my custom lehenga from scratch. The team was patient with my measurements and the final result was beyond what I imagined.', featured: 1, status: 'approved' }, { id: 3, name: 'Zara R.', location: 'Karachi, PK', occasion: 'Barat · Feb 2026', rating: 5, title: 'Truly one of a kind', body: 'The detail on the embroidery is something I have never seen from any other atelier. Every stitch feels intentional. My mother cried when she saw me.', featured: 1, status: 'approved' }, ]; // ══════════════════════════════════════════════════════════════ // HOOKS // ══════════════════════════════════════════════════════════════ window.useToast = function() { const [toast, setToast] = useState(null); const show = useCallback((msg, type = 'success') => { setToast({ msg, type }); setTimeout(() => setToast(null), 3400); }, []); return { toast, show }; }; window.useReveal = function(ref) { useEffect(() => { if (!ref.current) return; const obs = new IntersectionObserver( entries => entries.forEach(e => e.isIntersecting && e.target.classList.add('in')), { threshold: 0.08 } ); ref.current.querySelectorAll('.reveal').forEach(el => obs.observe(el)); return () => obs.disconnect(); }, [ref]); }; window.useProducts = function() { const [products, setProducts] = useState(window.SEED_PRODUCTS); const [loading, setLoading] = useState(false); useEffect(() => { setLoading(true); fetch('/api/products.php') .then(r => r.json()) .then(d => { if (Array.isArray(d) && d.length) setProducts(d); }) .catch(() => {}) .finally(() => setLoading(false)); }, []); return { products, setProducts, loading }; }; // ══════════════════════════════════════════════════════════════ // UI PRIMITIVES // ══════════════════════════════════════════════════════════════ window.Spinner = function Spinner({ size = 20 }) { return ( ); }; window.Toast = function Toast({ toast }) { if (!toast) return null; return
{toast.msg}
; }; window.Modal = function Modal({ open, onClose, title, children, maxW = '620px' }) { useEffect(() => { document.body.style.overflow = open ? 'hidden' : ''; return () => { document.body.style.overflow = ''; }; }, [open]); if (!open) return null; return (
e.target === e.currentTarget && onClose()}>

{title}

{children}
); }; window.Field = function Field({ label, required, error, hint, children, style }) { return (
{label && ( )} {children} {error && ⚠ {error}}
); }; window.StatusPill = function StatusPill({ status }) { const s = (status || '').toLowerCase().replace(/\s+/g, '-'); return {status}; }; window.StarRating = function StarRating({ value = 5, onChange, size = 20 }) { const [hover, setHover] = useState(0); return (
{[1, 2, 3, 4, 5].map(n => ( onChange && onChange(n)} onMouseEnter={() => onChange && setHover(n)} onMouseLeave={() => onChange && setHover(0)} style={{ fontSize: size, color: n <= (hover || value) ? 'var(--gold)' : 'var(--line-2)', cursor: onChange ? 'pointer' : 'default', transition: 'color .15s', }}>★ ))}
); }; window.VideoEmbed = function VideoEmbed({ provider, videoId, videoUrl, thumbnail, title = '' }) { const src = { vimeo: `https://player.vimeo.com/video/${videoId}?dnt=1&byline=0&portrait=0&title=0`, youtube: `https://www.youtube-nocookie.com/embed/${videoId}?rel=0`, bunny: videoUrl, self: videoUrl, }[provider] || videoUrl; if (provider === 'bunny' || provider === 'self') { return (
); } return (