// ═══════════════════════════════════════════════════════════════ // Aurelle Bride — app.jsx // Public-facing site. Depends on components.jsx loading first. // ═══════════════════════════════════════════════════════════════ // ── reCAPTCHA v3 helper ─────────────────────────────────────── const RECAPTCHA_SITE_KEY = '6LeWlucsAAAAAOMkcMc9P5IlDD6YqHUM7XZThNIL'; function getRecaptchaToken(action) { return new Promise((resolve) => { if (typeof grecaptcha === 'undefined') { resolve(''); return; } grecaptcha.ready(() => { grecaptcha.execute(RECAPTCHA_SITE_KEY, { action }) .then(resolve) .catch(() => resolve('')); }); }); } // ── Cursor follower ─────────────────────────────────────────── (function () { const dot = document.getElementById('cursor'); if (!dot) return; document.addEventListener('mousemove', e => { dot.style.transform = `translate(${e.clientX}px, ${e.clientY}px) translate(-50%,-50%)`; }); document.querySelectorAll('a,button,[role=button]').forEach(el => { el.addEventListener('mouseenter', () => dot.classList.add('hover')); el.addEventListener('mouseleave', () => dot.classList.remove('hover')); }); })(); // ────────────────────────────────────────────────────────────── // SITE NAV // ────────────────────────────────────────────────────────────── // ── Phone-number input with auto-detected country dial code ── const DIAL_CODES = [ // ── Popular / most likely Aurelle customers first ── { code: 'PK', dial: '+92', flag: '🇵🇰', name: 'Pakistan' }, { code: 'AE', dial: '+971', flag: '🇦🇪', name: 'United Arab Emirates' }, { code: 'SA', dial: '+966', flag: '🇸🇦', name: 'Saudi Arabia' }, { code: 'GB', dial: '+44', flag: '🇬🇧', name: 'United Kingdom' }, { code: 'US', dial: '+1', flag: '🇺🇸', name: 'United States' }, { code: 'CA', dial: '+1', flag: '🇨🇦', name: 'Canada' }, { code: 'AU', dial: '+61', flag: '🇦🇺', name: 'Australia' }, { code: 'IN', dial: '+91', flag: '🇮🇳', name: 'India' }, { code: 'QA', dial: '+974', flag: '🇶🇦', name: 'Qatar' }, { code: 'KW', dial: '+965', flag: '🇰🇼', name: 'Kuwait' }, { code: 'BH', dial: '+973', flag: '🇧🇭', name: 'Bahrain' }, { code: 'OM', dial: '+968', flag: '🇴🇲', name: 'Oman' }, { code: 'MY', dial: '+60', flag: '🇲🇾', name: 'Malaysia' }, { code: 'SG', dial: '+65', flag: '🇸🇬', name: 'Singapore' }, { code: 'BD', dial: '+880', flag: '🇧🇩', name: 'Bangladesh' }, // ── A ── { code: 'AF', dial: '+93', flag: '🇦🇫', name: 'Afghanistan' }, { code: 'AL', dial: '+355', flag: '🇦🇱', name: 'Albania' }, { code: 'DZ', dial: '+213', flag: '🇩🇿', name: 'Algeria' }, { code: 'AD', dial: '+376', flag: '🇦🇩', name: 'Andorra' }, { code: 'AO', dial: '+244', flag: '🇦🇴', name: 'Angola' }, { code: 'AG', dial: '+1268',flag: '🇦🇬', name: 'Antigua & Barbuda' }, { code: 'AR', dial: '+54', flag: '🇦🇷', name: 'Argentina' }, { code: 'AM', dial: '+374', flag: '🇦🇲', name: 'Armenia' }, { code: 'AT', dial: '+43', flag: '🇦🇹', name: 'Austria' }, { code: 'AZ', dial: '+994', flag: '🇦🇿', name: 'Azerbaijan' }, // ── B ── { code: 'BS', dial: '+1242',flag: '🇧🇸', name: 'Bahamas' }, { code: 'BB', dial: '+1246',flag: '🇧🇧', name: 'Barbados' }, { code: 'BY', dial: '+375', flag: '🇧🇾', name: 'Belarus' }, { code: 'BE', dial: '+32', flag: '🇧🇪', name: 'Belgium' }, { code: 'BZ', dial: '+501', flag: '🇧🇿', name: 'Belize' }, { code: 'BJ', dial: '+229', flag: '🇧🇯', name: 'Benin' }, { code: 'BT', dial: '+975', flag: '🇧🇹', name: 'Bhutan' }, { code: 'BO', dial: '+591', flag: '🇧🇴', name: 'Bolivia' }, { code: 'BA', dial: '+387', flag: '🇧🇦', name: 'Bosnia & Herzegovina' }, { code: 'BW', dial: '+267', flag: '🇧🇼', name: 'Botswana' }, { code: 'BR', dial: '+55', flag: '🇧🇷', name: 'Brazil' }, { code: 'BN', dial: '+673', flag: '🇧🇳', name: 'Brunei' }, { code: 'BG', dial: '+359', flag: '🇧🇬', name: 'Bulgaria' }, { code: 'BF', dial: '+226', flag: '🇧🇫', name: 'Burkina Faso' }, { code: 'BI', dial: '+257', flag: '🇧🇮', name: 'Burundi' }, // ── C ── { code: 'CV', dial: '+238', flag: '🇨🇻', name: 'Cabo Verde' }, { code: 'KH', dial: '+855', flag: '🇰🇭', name: 'Cambodia' }, { code: 'CM', dial: '+237', flag: '🇨🇲', name: 'Cameroon' }, { code: 'CF', dial: '+236', flag: '🇨🇫', name: 'Central African Rep.' }, { code: 'TD', dial: '+235', flag: '🇹🇩', name: 'Chad' }, { code: 'CL', dial: '+56', flag: '🇨🇱', name: 'Chile' }, { code: 'CN', dial: '+86', flag: '🇨🇳', name: 'China' }, { code: 'CO', dial: '+57', flag: '🇨🇴', name: 'Colombia' }, { code: 'KM', dial: '+269', flag: '🇰🇲', name: 'Comoros' }, { code: 'CD', dial: '+243', flag: '🇨🇩', name: 'Congo (DRC)' }, { code: 'CG', dial: '+242', flag: '🇨🇬', name: 'Congo (Republic)' }, { code: 'CR', dial: '+506', flag: '🇨🇷', name: 'Costa Rica' }, { code: 'HR', dial: '+385', flag: '🇭🇷', name: 'Croatia' }, { code: 'CU', dial: '+53', flag: '🇨🇺', name: 'Cuba' }, { code: 'CY', dial: '+357', flag: '🇨🇾', name: 'Cyprus' }, { code: 'CZ', dial: '+420', flag: '🇨🇿', name: 'Czech Republic' }, // ── D ── { code: 'DK', dial: '+45', flag: '🇩🇰', name: 'Denmark' }, { code: 'DJ', dial: '+253', flag: '🇩🇯', name: 'Djibouti' }, { code: 'DM', dial: '+1767',flag: '🇩🇲', name: 'Dominica' }, { code: 'DO', dial: '+1809',flag: '🇩🇴', name: 'Dominican Republic' }, // ── E ── { code: 'EC', dial: '+593', flag: '🇪🇨', name: 'Ecuador' }, { code: 'EG', dial: '+20', flag: '🇪🇬', name: 'Egypt' }, { code: 'SV', dial: '+503', flag: '🇸🇻', name: 'El Salvador' }, { code: 'GQ', dial: '+240', flag: '🇬🇶', name: 'Equatorial Guinea' }, { code: 'ER', dial: '+291', flag: '🇪🇷', name: 'Eritrea' }, { code: 'EE', dial: '+372', flag: '🇪🇪', name: 'Estonia' }, { code: 'SZ', dial: '+268', flag: '🇸🇿', name: 'Eswatini' }, { code: 'ET', dial: '+251', flag: '🇪🇹', name: 'Ethiopia' }, // ── F ── { code: 'FJ', dial: '+679', flag: '🇫🇯', name: 'Fiji' }, { code: 'FI', dial: '+358', flag: '🇫🇮', name: 'Finland' }, { code: 'FR', dial: '+33', flag: '🇫🇷', name: 'France' }, { code: 'GA', dial: '+241', flag: '🇬🇦', name: 'Gabon' }, { code: 'GM', dial: '+220', flag: '🇬🇲', name: 'Gambia' }, // ── G ── { code: 'GE', dial: '+995', flag: '🇬🇪', name: 'Georgia' }, { code: 'DE', dial: '+49', flag: '🇩🇪', name: 'Germany' }, { code: 'GH', dial: '+233', flag: '🇬🇭', name: 'Ghana' }, { code: 'GR', dial: '+30', flag: '🇬🇷', name: 'Greece' }, { code: 'GD', dial: '+1473',flag: '🇬🇩', name: 'Grenada' }, { code: 'GT', dial: '+502', flag: '🇬🇹', name: 'Guatemala' }, { code: 'GN', dial: '+224', flag: '🇬🇳', name: 'Guinea' }, { code: 'GW', dial: '+245', flag: '🇬🇼', name: 'Guinea-Bissau' }, { code: 'GY', dial: '+592', flag: '🇬🇾', name: 'Guyana' }, // ── H ── { code: 'HT', dial: '+509', flag: '🇭🇹', name: 'Haiti' }, { code: 'HN', dial: '+504', flag: '🇭🇳', name: 'Honduras' }, { code: 'HK', dial: '+852', flag: '🇭🇰', name: 'Hong Kong' }, { code: 'HU', dial: '+36', flag: '🇭🇺', name: 'Hungary' }, // ── I ── { code: 'IS', dial: '+354', flag: '🇮🇸', name: 'Iceland' }, { code: 'ID', dial: '+62', flag: '🇮🇩', name: 'Indonesia' }, { code: 'IR', dial: '+98', flag: '🇮🇷', name: 'Iran' }, { code: 'IQ', dial: '+964', flag: '🇮🇶', name: 'Iraq' }, { code: 'IE', dial: '+353', flag: '🇮🇪', name: 'Ireland' }, { code: 'IL', dial: '+972', flag: '🇮🇱', name: 'Israel' }, { code: 'IT', dial: '+39', flag: '🇮🇹', name: 'Italy' }, // ── J ── { code: 'JM', dial: '+1876',flag: '🇯🇲', name: 'Jamaica' }, { code: 'JP', dial: '+81', flag: '🇯🇵', name: 'Japan' }, { code: 'JO', dial: '+962', flag: '🇯🇴', name: 'Jordan' }, // ── K ── { code: 'KZ', dial: '+7', flag: '🇰🇿', name: 'Kazakhstan' }, { code: 'KE', dial: '+254', flag: '🇰🇪', name: 'Kenya' }, { code: 'KI', dial: '+686', flag: '🇰🇮', name: 'Kiribati' }, { code: 'KP', dial: '+850', flag: '🇰🇵', name: 'North Korea' }, { code: 'KR', dial: '+82', flag: '🇰🇷', name: 'South Korea' }, { code: 'XK', dial: '+383', flag: '🇽🇰', name: 'Kosovo' }, { code: 'KG', dial: '+996', flag: '🇰🇬', name: 'Kyrgyzstan' }, // ── L ── { code: 'LA', dial: '+856', flag: '🇱🇦', name: 'Laos' }, { code: 'LV', dial: '+371', flag: '🇱🇻', name: 'Latvia' }, { code: 'LB', dial: '+961', flag: '🇱🇧', name: 'Lebanon' }, { code: 'LS', dial: '+266', flag: '🇱🇸', name: 'Lesotho' }, { code: 'LR', dial: '+231', flag: '🇱🇷', name: 'Liberia' }, { code: 'LY', dial: '+218', flag: '🇱🇾', name: 'Libya' }, { code: 'LI', dial: '+423', flag: '🇱🇮', name: 'Liechtenstein' }, { code: 'LT', dial: '+370', flag: '🇱🇹', name: 'Lithuania' }, { code: 'LU', dial: '+352', flag: '🇱🇺', name: 'Luxembourg' }, // ── M ── { code: 'MO', dial: '+853', flag: '🇲🇴', name: 'Macau' }, { code: 'MG', dial: '+261', flag: '🇲🇬', name: 'Madagascar' }, { code: 'MW', dial: '+265', flag: '🇲🇼', name: 'Malawi' }, { code: 'MV', dial: '+960', flag: '🇲🇻', name: 'Maldives' }, { code: 'ML', dial: '+223', flag: '🇲🇱', name: 'Mali' }, { code: 'MT', dial: '+356', flag: '🇲🇹', name: 'Malta' }, { code: 'MH', dial: '+692', flag: '🇲🇭', name: 'Marshall Islands' }, { code: 'MR', dial: '+222', flag: '🇲🇷', name: 'Mauritania' }, { code: 'MU', dial: '+230', flag: '🇲🇺', name: 'Mauritius' }, { code: 'MX', dial: '+52', flag: '🇲🇽', name: 'Mexico' }, { code: 'FM', dial: '+691', flag: '🇫🇲', name: 'Micronesia' }, { code: 'MD', dial: '+373', flag: '🇲🇩', name: 'Moldova' }, { code: 'MC', dial: '+377', flag: '🇲🇨', name: 'Monaco' }, { code: 'MN', dial: '+976', flag: '🇲🇳', name: 'Mongolia' }, { code: 'ME', dial: '+382', flag: '🇲🇪', name: 'Montenegro' }, { code: 'MA', dial: '+212', flag: '🇲🇦', name: 'Morocco' }, { code: 'MZ', dial: '+258', flag: '🇲🇿', name: 'Mozambique' }, { code: 'MM', dial: '+95', flag: '🇲🇲', name: 'Myanmar' }, // ── N ── { code: 'NA', dial: '+264', flag: '🇳🇦', name: 'Namibia' }, { code: 'NR', dial: '+674', flag: '🇳🇷', name: 'Nauru' }, { code: 'NP', dial: '+977', flag: '🇳🇵', name: 'Nepal' }, { code: 'NL', dial: '+31', flag: '🇳🇱', name: 'Netherlands' }, { code: 'NZ', dial: '+64', flag: '🇳🇿', name: 'New Zealand' }, { code: 'NI', dial: '+505', flag: '🇳🇮', name: 'Nicaragua' }, { code: 'NE', dial: '+227', flag: '🇳🇪', name: 'Niger' }, { code: 'NG', dial: '+234', flag: '🇳🇬', name: 'Nigeria' }, { code: 'MK', dial: '+389', flag: '🇲🇰', name: 'North Macedonia' }, { code: 'NO', dial: '+47', flag: '🇳🇴', name: 'Norway' }, // ── O–P ── { code: 'PW', dial: '+680', flag: '🇵🇼', name: 'Palau' }, { code: 'PS', dial: '+970', flag: '🇵🇸', name: 'Palestine' }, { code: 'PA', dial: '+507', flag: '🇵🇦', name: 'Panama' }, { code: 'PG', dial: '+675', flag: '🇵🇬', name: 'Papua New Guinea' }, { code: 'PY', dial: '+595', flag: '🇵🇾', name: 'Paraguay' }, { code: 'PE', dial: '+51', flag: '🇵🇪', name: 'Peru' }, { code: 'PH', dial: '+63', flag: '🇵🇭', name: 'Philippines' }, { code: 'PL', dial: '+48', flag: '🇵🇱', name: 'Poland' }, { code: 'PT', dial: '+351', flag: '🇵🇹', name: 'Portugal' }, // ── R ── { code: 'RO', dial: '+40', flag: '🇷🇴', name: 'Romania' }, { code: 'RU', dial: '+7', flag: '🇷🇺', name: 'Russia' }, { code: 'RW', dial: '+250', flag: '🇷🇼', name: 'Rwanda' }, // ── S ── { code: 'KN', dial: '+1869',flag: '🇰🇳', name: 'Saint Kitts & Nevis' }, { code: 'LC', dial: '+1758',flag: '🇱🇨', name: 'Saint Lucia' }, { code: 'VC', dial: '+1784',flag: '🇻🇨', name: 'Saint Vincent' }, { code: 'WS', dial: '+685', flag: '🇼🇸', name: 'Samoa' }, { code: 'SM', dial: '+378', flag: '🇸🇲', name: 'San Marino' }, { code: 'ST', dial: '+239', flag: '🇸🇹', name: 'São Tomé & Príncipe' }, { code: 'SN', dial: '+221', flag: '🇸🇳', name: 'Senegal' }, { code: 'RS', dial: '+381', flag: '🇷🇸', name: 'Serbia' }, { code: 'SC', dial: '+248', flag: '🇸🇨', name: 'Seychelles' }, { code: 'SL', dial: '+232', flag: '🇸🇱', name: 'Sierra Leone' }, { code: 'SK', dial: '+421', flag: '🇸🇰', name: 'Slovakia' }, { code: 'SI', dial: '+386', flag: '🇸🇮', name: 'Slovenia' }, { code: 'SB', dial: '+677', flag: '🇸🇧', name: 'Solomon Islands' }, { code: 'SO', dial: '+252', flag: '🇸🇴', name: 'Somalia' }, { code: 'ZA', dial: '+27', flag: '🇿🇦', name: 'South Africa' }, { code: 'SS', dial: '+211', flag: '🇸🇸', name: 'South Sudan' }, { code: 'ES', dial: '+34', flag: '🇪🇸', name: 'Spain' }, { code: 'LK', dial: '+94', flag: '🇱🇰', name: 'Sri Lanka' }, { code: 'SD', dial: '+249', flag: '🇸🇩', name: 'Sudan' }, { code: 'SR', dial: '+597', flag: '🇸🇷', name: 'Suriname' }, { code: 'SE', dial: '+46', flag: '🇸🇪', name: 'Sweden' }, { code: 'CH', dial: '+41', flag: '🇨🇭', name: 'Switzerland' }, { code: 'SY', dial: '+963', flag: '🇸🇾', name: 'Syria' }, // ── T ── { code: 'TW', dial: '+886', flag: '🇹🇼', name: 'Taiwan' }, { code: 'TJ', dial: '+992', flag: '🇹🇯', name: 'Tajikistan' }, { code: 'TZ', dial: '+255', flag: '🇹🇿', name: 'Tanzania' }, { code: 'TH', dial: '+66', flag: '🇹🇭', name: 'Thailand' }, { code: 'TL', dial: '+670', flag: '🇹🇱', name: 'Timor-Leste' }, { code: 'TG', dial: '+228', flag: '🇹🇬', name: 'Togo' }, { code: 'TO', dial: '+676', flag: '🇹🇴', name: 'Tonga' }, { code: 'TT', dial: '+1868',flag: '🇹🇹', name: 'Trinidad & Tobago' }, { code: 'TN', dial: '+216', flag: '🇹🇳', name: 'Tunisia' }, { code: 'TR', dial: '+90', flag: '🇹🇷', name: 'Turkey' }, { code: 'TM', dial: '+993', flag: '🇹🇲', name: 'Turkmenistan' }, { code: 'TV', dial: '+688', flag: '🇹🇻', name: 'Tuvalu' }, // ── U ── { code: 'UG', dial: '+256', flag: '🇺🇬', name: 'Uganda' }, { code: 'UA', dial: '+380', flag: '🇺🇦', name: 'Ukraine' }, { code: 'UY', dial: '+598', flag: '🇺🇾', name: 'Uruguay' }, { code: 'UZ', dial: '+998', flag: '🇺🇿', name: 'Uzbekistan' }, // ── V ── { code: 'VU', dial: '+678', flag: '🇻🇺', name: 'Vanuatu' }, { code: 'VE', dial: '+58', flag: '🇻🇪', name: 'Venezuela' }, { code: 'VN', dial: '+84', flag: '🇻🇳', name: 'Vietnam' }, // ── Y–Z ── { code: 'YE', dial: '+967', flag: '🇾🇪', name: 'Yemen' }, { code: 'ZM', dial: '+260', flag: '🇿🇲', name: 'Zambia' }, { code: 'ZW', dial: '+263', flag: '🇿🇼', name: 'Zimbabwe' }, ]; // Map currency.jsx country_code → dial entry const COUNTRY_TO_DIAL = {}; DIAL_CODES.forEach(d => { COUNTRY_TO_DIAL[d.code] = d; }); function useAutoDialCode() { const [dialEntry, setDialEntry] = React.useState(DIAL_CODES[0]); // default PK React.useEffect(() => { try { const cached = JSON.parse(localStorage.getItem('ab_geo_v2') || '{}'); const cc = cached.country_code; if (cc && COUNTRY_TO_DIAL[cc]) setDialEntry(COUNTRY_TO_DIAL[cc]); } catch {} }, []); return [dialEntry, setDialEntry]; } function PhoneInput({ onChange, required = false, label = 'Phone Number' }) { const [dialEntry, setDialEntry] = useAutoDialCode(); const [open, setOpen] = React.useState(false); const [local, setLocal] = React.useState(''); const [search, setSearch] = React.useState(''); const filtered = React.useMemo(() => { const q = search.toLowerCase().trim(); if (!q) return DIAL_CODES; return DIAL_CODES.filter(d => d.name.toLowerCase().includes(q) || d.dial.includes(q) || d.code.toLowerCase().includes(q) ); }, [search]); const pickCountry = (d) => { setDialEntry(d); setOpen(false); setSearch(''); if (onChange) onChange(d.dial + local); }; const handleType = (e) => { const raw = e.target.value.replace(/[^\d\s\-().]/g, ''); setLocal(raw); if (onChange) onChange(dialEntry.dial + raw); }; // Close dropdown on outside click const wrapRef = React.useRef(null); React.useEffect(() => { if (!open) return; const handler = (e) => { if (wrapRef.current && !wrapRef.current.contains(e.target)) { setOpen(false); setSearch(''); } }; document.addEventListener('mousedown', handler); return () => document.removeEventListener('mousedown', handler); }, [open]); return (
{/* Country code selector */}
{open && (
{/* Search box */}
setSearch(e.target.value)} style={{ width: '100%', padding: '6px 10px', fontSize: 13, background: 'var(--surface-2, #1a1a1a)', border: '1px solid var(--line)', borderRadius: 6, color: 'var(--ink-1)', outline: 'none', }} />
{/* Country list */}
{filtered.length === 0 && (
No results
)} {filtered.map(d => ( ))}
)}
{/* Number input */}
); } function SiteNav({ page, setPage, theme, setTheme }) { const [menuOpen, setMenuOpen] = useState(false); const links = [ { key: 'home', label: 'Home', path: '/' }, { key: 'collection', label: 'Collection', path: '/collection' }, { key: 'custom', label: 'Custom Order', path: '/custom' }, { key: 'contact', label: 'Contact', path: '/contact' }, ]; return ( ); } // ────────────────────────────────────────────────────────────── // WHATSAPP FAB // ────────────────────────────────────────────────────────────── function WhatsAppFAB() { const wa = (brand.whatsapp || '').replace(/\D/g, ''); const msg = encodeURIComponent('Hi Aurelle Bride! I\'d love to learn more about your collection.'); return (
Need help? Chat with us WhatsApp
); } // ────────────────────────────────────────────────────────────── // HERO // ────────────────────────────────────────────────────────────── function HeroSection({ setPage }) { return (

Couture,
made for you.

{brand.heroSub || 'A future-forward bridal atelier.'} Every piece is constructed for one bride — your measurements, your vision, your moment.

{[['6+', 'Silhouettes'], ['100%', 'Made to measure'], ['Worldwide', 'Shipping']].map(([n, l]) => (
{n}
{l}
))}
Saffron Veil Lehenga — Aurelle Bride
Latest drop Saffron Veil Lehenga ·
); } // ────────────────────────────────────────────────────────────── // MARQUEE // ────────────────────────────────────────────────────────────── function MarqueeStrip() { const items = ['Lehenga', 'Sharara', 'Gharara', 'Bridal Gown', 'Anarkali', 'Bridal Mexi', 'Custom Couture']; const doubled = [...items, ...items]; return ( ); } // ────────────────────────────────────────────────────────────── // COLLECTION // ────────────────────────────────────────────────────────────── function CollectionSection({ onSelectProduct, limit }) { const { products, loading } = useProducts(); const [filter, setFilter] = useState('All'); const sectionRef = useRef(null); useReveal(sectionRef); // Listen for filter events from footer navigation links useEffect(() => { const handler = (e) => setFilter(e.detail || 'All'); window.addEventListener('set-collection-filter', handler); return () => window.removeEventListener('set-collection-filter', handler); }, []); const categories = useMemo(() => ['All', ...new Set(products.map(p => p.col))], [products]); const filtered = filter === 'All' ? products : products.filter(p => p.col === filter); const display = limit ? filtered.slice(0, limit) : filtered; return (
The Collection

Each piece, singular.

One-of-one silhouettes and bespoke couture — no two brides wear the same dress.

{/* Filter tabs */}
{categories.map(cat => ( ))}
{loading ?
: (
{display.map((p, i) => ( ))}
)}
); } // ────────────────────────────────────────────────────────────── // PRODUCT DETAIL MODAL // ────────────────────────────────────────────────────────────── function ProductModal({ product, onClose, onOrder }) { const [imgIdx, setImgIdx] = useState(0); const [lightbox, setLightbox] = useState(false); const gallery = useMemo(() => { if (!product) return []; const g = typeof product.gallery === 'string' ? JSON.parse(product.gallery || '[]') : (product.gallery || []); return g.length ? g : [{ url: product.image_url, label: 'Main' }]; }, [product]); useEffect(() => { setImgIdx(0); setLightbox(false); }, [product]); // Escape key closes lightbox useEffect(() => { if (!lightbox) return; const handler = (e) => { if (e.key === 'Escape') setLightbox(false); }; window.addEventListener('keydown', handler); return () => window.removeEventListener('keydown', handler); }, [lightbox]); // ── Dynamic Product Schema injection for Google rich results ── useEffect(() => { if (!product) return; const slug = product.name .toLowerCase() .replace(/[^a-z0-9]+/g, '-') .replace(/^-|-$/g, ''); const productUrl = 'https://aurellebride.com/collection#' + slug; const images = gallery.map(function(g) { return g.url; }).filter(Boolean); const schema = { "@context": "https://schema.org", "@type": "Product", "name": product.name, "description": product.desc || (product.name + ' — luxury bridal couture by Aurelle Bride. Made to your measurements, shipped worldwide.'), "image": images.length ? images : [product.image_url], "brand": { "@type": "Brand", "name": "Aurelle Bride" }, "sku": 'AB-' + product.id, "category": product.col || "Bridal Couture", "url": productUrl, "offers": { "@type": "Offer", "priceCurrency": "PKR", "price": String(product.priceValue || 0), "availability": product.status === 'AVAILABLE' ? "https://schema.org/InStock" : "https://schema.org/LimitedAvailability", "itemCondition": "https://schema.org/NewCondition", "seller": { "@type": "Organization", "name": "Aurelle Bride" }, "url": productUrl } }; var existing = document.getElementById('product-schema-dynamic'); if (existing) existing.remove(); var el = document.createElement('script'); el.type = 'application/ld+json'; el.id = 'product-schema-dynamic'; el.text = JSON.stringify(schema); document.head.appendChild(el); var prevTitle = document.title; document.title = product.name + ' — Aurelle Bride'; return function() { var s = document.getElementById('product-schema-dynamic'); if (s) s.remove(); document.title = prevTitle; }; }, [product]); if (!product) return null; const wa = (brand.whatsapp || '').replace(/\D/g, ''); const waMsg = encodeURIComponent(`Hi! I'm interested in: ${product.name} (₨ ${Number(product.priceValue).toLocaleString()} PKR)`); return ( <> {/* ── Lightbox ── */} {lightbox && (
setLightbox(false)} style={{ position: 'fixed', inset: 0, zIndex: 99999, background: 'rgba(0,0,0,0.92)', display: 'flex', alignItems: 'center', justifyContent: 'center', cursor: 'zoom-out', }} > {/* Close button */} {/* Prev / Next arrows */} {gallery.length > 1 && ( <> )} {/* Full image */} {gallery[imgIdx]?.label e.stopPropagation()} style={{ maxWidth: '92vw', maxHeight: '92vh', objectFit: 'contain', borderRadius: 4, boxShadow: '0 8px 48px rgba(0,0,0,0.6)', cursor: 'default', }} /> {/* Caption */} {gallery[imgIdx]?.label && (
{gallery[imgIdx].label}
)}
)}
e.target === e.currentTarget && onClose()}>
{/* Gallery */}
{gallery[imgIdx]?.label setLightbox(true)} style={{ width: '100%', height: '100%', objectFit: 'cover', cursor: 'zoom-in' }} /> {/* Zoom hint */}
setLightbox(true)} style={{ position: 'absolute', bottom: 10, right: 10, cursor: 'zoom-in', background: 'rgba(0,0,0,0.55)', borderRadius: 6, padding: '4px 8px', fontSize: 11, color: '#fff', letterSpacing: '.06em', userSelect: 'none', }}>🔍 tap to zoom
{product.status}
{gallery.length > 1 && (
{gallery.map((img, i) => ( ))}
)}
{/* Info */}
{product.col}

{product.name}

{product.desc &&

{product.desc}

} {product.fabric_detail && (
Fabric & Craft

{product.fabric_detail}

)} {/* Details grid */}
{product.delivery_weeks && (
Delivery
~{product.delivery_weeks} weeks
)}
Sizing
Made to measure
{/* Shipping — always inquire, never fixed fee */} {product.care && (
Care: {product.care}
)}

Made to your measurements

Every garment is stitched fresh. We collect your measurements before production begins.

WhatsApp
); } // ────────────────────────────────────────────────────────────── // CUSTOM ORDER — multi-step form // ────────────────────────────────────────────────────────────── // ── Helper sub-components for Custom Order ──────────────────── function OrderStep({ n, t, children }) { return (
STEP {n} · {t}
{children}
); } function OrderTile({ active, onClick, children, sub, img }) { return ( ); } // ────────────────────────────────────────────────────────────── // CUSTOM ORDER — original visual builder with sticky summary // ────────────────────────────────────────────────────────────── function CustomOrderSection({ preProduct, onSuccess }) { const { toast, show } = useToast(); const { convert, currency, rate } = useCurrency(); const [type, setType] = useState('lehenga'); const [palette, setPalette] = useState('classic'); const [fabric, setFabric] = useState('silk'); const [embs, setEmbs] = useState([]); const [neckline, setNeckline] = useState('sweetheart'); const [sleeves, setSleeves] = useState('full'); const [files, setFiles] = useState([]); const [contact, setContact] = useState({ name: '', email: '', phone: '', phone_whatsapp: true, notes: '', occasion: 'walima' }); const [submitting, setSubmitting] = useState(false); // Measurements const [measurements, setMeasurements] = useState({ unit: 'cm', bust:'', waist:'', hips:'', height:'', shoulder:'', sleeve_length:'', shirt_length:'', trouser_length:'', measurement_note:'regular', skip: false }); const types = [ { id: 'lehenga', label: 'Lehenga', img: '/uploads/silhouettes/lehenga.jpg' }, { id: 'sharara', label: 'Sharara', img: '/uploads/silhouettes/sharara.jpg' }, { id: 'gharara', label: 'Gharara', img: '/uploads/silhouettes/gharara.jpg' }, { id: 'anarkali', label: 'Anarkali', img: '/uploads/silhouettes/anarkali.jpg' }, { id: 'gown', label: 'Bridal Gown', img: '/uploads/silhouettes/gown.jpg' }, { id: 'mexi', label: 'Bridal Mexi', img: '/uploads/silhouettes/mexi.jpg' }, ]; // Map product col → garment type id when coming from product modal const COL_TO_TYPE = { 'lehenga': 'lehenga', 'sharara': 'sharara', 'gharara': 'gharara', 'anarkali': 'anarkali', 'bridal gown': 'gown', 'gown': 'gown', 'bridal mexi': 'mexi', 'mexi': 'mexi', }; useEffect(() => { if (!preProduct) return; const col = (preProduct.col || preProduct.garment_type || '').toLowerCase().trim(); const matched = COL_TO_TYPE[col]; if (matched) setType(matched); // Pre-fill name/email if product has a customer field (unlikely but safe) }, [preProduct]); const palettes = [ { id: 'classic', name: 'Classic Bridal', c: ['#8B0000','#FFD700','#FFFFFF','#C9A961'] }, { id: 'royal', name: 'Royal Sapphire', c: ['#0a2240','#3556a8','#C0C0C0','#E6E6FA'] }, { id: 'sunset', name: 'Golden Hour', c: ['#a9772a','#d3a35b','#f3d8a0','#FFF8DC'] }, { id: 'emerald', name: 'Emerald Garden', c: ['#0f4d3a','#1f8060','#9bc7a3','#ecf3e0'] }, { id: 'rose', name: 'Rose Atelier', c: ['#7a2030','#c47083','#e9c3c8','#fff4f1'] }, { id: 'onyx', name: 'Onyx Luxe', c: ['#0E0E10','#3A3A3F','#777','#C9A961'] }, ]; const fabricList = [ { id: 'silk', label: 'Silk', price: 25000 }, { id: 'velvet', label: 'Velvet', price: 35000 }, { id: 'georgette', label: 'Georgette', price: 18000 }, { id: 'organza', label: 'Organza', price: 22000 }, { id: 'net', label: 'Net', price: 15000 }, { id: 'brocade', label: 'Brocade', price: 42000 }, ]; const embOpts = [ { id: 'zari', label: 'Zari Work', price: 15000 }, { id: 'mirror', label: 'Mirror Work', price: 12000 }, { id: 'thread', label: 'Thread Embroidery', price: 18000 }, { id: 'stone', label: 'Stone Work', price: 22000 }, { id: 'pearl', label: 'Pearl Beading', price: 14000 }, { id: 'gota', label: 'Gota Patti', price: 11000 }, ]; const necklines = [['sweetheart','Sweetheart'],['vneck','V-Neck'],['boat','Boat'],['scoop','Scoop'],['halter','Halter']]; const sleeveOpts = [['full','Full'],['threequarter','¾'],['cap','Cap'],['sleeveless','Sleeveless']]; const basePrices = { lehenga:150000, sharara:120000, gharara:110000, anarkali:95000, gown:135000, mexi:115000 }; const breakdown = useMemo(() => { const b = basePrices[type] || 0; const f = fabricList.find(x => x.id === fabric)?.price || 0; const e = embs.reduce((s, id) => s + (embOpts.find(o => o.id === id)?.price || 0), 0); return { base: b, fabric: f, embs: e, total: b + f + e }; }, [type, fabric, embs]); // Show price in detected currency const showPrice = (pkr) => { if (!rate || currency === 'PKR') return '₨ ' + Number(pkr).toLocaleString(); return convert(pkr) || ('₨ ' + Number(pkr).toLocaleString()); }; const onUpload = (e) => { const list = Array.from(e.target.files || []); if (files.length + list.length > 8) { show('Max 8 files', 'error'); return; } setFiles(prev => [...prev, ...list.map(f => ({ name: f.name, size: (f.size / 1024 / 1024).toFixed(2) + ' MB', url: f.type.startsWith('image/') ? URL.createObjectURL(f) : null, _raw: f, }))]); }; const submit = async () => { if (!contact.name || !contact.email) { show('Please share your name and email', 'error'); return; } if (!contact.phone) { show('Please add your phone number so we can reach you', 'error'); return; } setSubmitting(true); const payload = { name: contact.name, email: contact.email, phone: contact.phone, phone_whatsapp: contact.phone_whatsapp, occasion: contact.occasion, notes: contact.notes, garment_type: type, palette, fabric, embellishments: embs.join(','), neckline, sleeves, total_estimate: breakdown.total, summary: `${type} · ${palette} · ${fabric} · ${embs.length} emb.`, // measurements unit: measurements.unit, bust: measurements.bust, waist: measurements.waist, hips: measurements.hips, height: measurements.height, shoulder: measurements.shoulder, sleeve_length: measurements.sleeve_length, shirt_length: measurements.shirt_length, trouser_length: measurements.trouser_length, measurement_note: measurements.measurement_note, }; try { const recaptchaToken = await getRecaptchaToken('custom_order'); const r = await fetch('/api/customs.php', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ ...payload, recaptcha_token: recaptchaToken }) }); if (!r.ok) throw new Error(); const { id } = await r.json(); if (files.length) { const fd = new FormData(); files.forEach(f => f._raw && fd.append('files[]', f._raw)); fd.append('parent_table', 'customs'); fd.append('parent_id', id); await fetch('/api/upload.php', { method: 'POST', body: fd }); } show("Brief received — we'll email you within 24 hours.", 'success'); setContact({ name: '', email: '', notes: '', occasion: 'walima' }); setFiles([]); setEmbs([]); } catch { show('Could not submit. Please try again.', 'error'); } finally { setSubmitting(false); } }; return (
THE BUILDER

Design your dream.

Build a brief in steps. Attach inspiration. We respond within 24 hours.

{/* Step 01 — Silhouette */}
{types.map(o => ( setType(o.id)} img={o.img} sub={`base ${showPrice(basePrices[o.id])}`}> {o.label} ))}
{/* Step 02 — Palette */}
{palettes.map(p => ( ))}
{/* Step 03 — Fabric */}
{fabricList.map(f => ( setFabric(f.id)} sub={`+${showPrice(f.price)}`}> {f.label} ))}
{/* Step 04 — Details */}
NECKLINE
{necklines.map(([id, l]) => setNeckline(id)}>{l})}
SLEEVES
{sleeveOpts.map(([id, l]) => setSleeves(id)}>{l})}
{/* Step 05 — Embellishments */}
{embOpts.map(o => ( ))}
{/* Step 06 — Measurements */} {/* Step 07 — Inspiration upload */} {files.length > 0 && (
{files.map((f, i) => (
{f.url ? :
{f.name}
}
))}
)}
{/* Step 08 — Your details */}
setContact({ ...contact, name: e.target.value })} /> setContact({ ...contact, email: e.target.value })} />
setContact({ ...contact, phone: full })} />