{lang === 'ar' ? a.name_ar : a.name_en}
{lang === 'ar' ? a.bio_ar : a.bio_en}
"{lang === 'ar' ? a.quote_ar : a.quote_en}"
// app.jsx — storefront root: state, routing, fly-to-cart animation, Tweaks // NOTE: admin is now a separate file (Admin Dashboard.html) const TWEAK_DEFAULTS = /*EDITMODE-BEGIN*/{ "palette": ["#F3ECE0", "#3A3027", "#B97A5E", "#8A8B6C"], "fontPair": "tajawal-fraunces", "gridCols": 3, "dark": false }/*EDITMODE-END*/; const PALETTES = { 'clay-sand': { name:'Clay', colors:['#F3ECE0','#3A3027','#B97A5E','#8A8B6C'] }, 'oat-olive': { name:'Olive', colors:['#F1EEE3','#33352A','#7E8B5E','#C08A52'] }, 'desert-rose': { name:'Rose', colors:['#F5ECE6','#3B2F2A','#C08274','#9A8466'] }, 'stone-sage': { name:'Stone', colors:['#EFEDE6','#322F29','#9A8C72','#7C8A7A'] }, 'terra-cream': { name:'Terra', colors:['#F4ECE1','#3A2E25','#C46A4B','#8C7B5A'] }, }; const FONT_PAIRS = { 'tajawal-fraunces': { name: 'Tajawal × Fraunces', google: 'Tajawal:wght@300;400;500;700|Fraunces:ital,opsz,wght@0,9..144,400;0,9..144,500;0,9..144,600;1,9..144,400', arHead: '"Tajawal", system-ui, sans-serif', arBody: '"Tajawal", system-ui, sans-serif', enHead: '"Fraunces", Georgia, serif', enBody: '"Tajawal", system-ui, sans-serif', }, 'tajawal-all': { name: 'Tajawal', google: 'Tajawal:wght@300;400;500;700', arHead: '"Tajawal", sans-serif', arBody: '"Tajawal", sans-serif', enHead: '"Tajawal", sans-serif', enBody: '"Tajawal", sans-serif', }, 'rubik-fraunces': { name: 'Rubik × Fraunces', google: 'Rubik:wght@300;400;500;600;700|Fraunces:ital,opsz,wght@0,9..144,400;0,9..144,500;0,9..144,600;1,9..144,400', arHead: '"Rubik", sans-serif', arBody: '"Rubik", sans-serif', enHead: '"Fraunces", serif', enBody: '"Rubik", sans-serif', }, }; const MONO_FONT = '"IBM Plex Mono", ui-monospace, monospace'; function App() { const [t, setTweak] = useTweaks(TWEAK_DEFAULTS); const [route, _setRoute] = React.useState('home'); const [routeData, setRouteData] = React.useState(null); const [lang, setLang] = React.useState('ar'); const [cart, setCart] = React.useState([]); const [pdpProduct, setPdpProduct] = React.useState(null); const [searchOpen, setSearchOpen] = React.useState(false); const [lastOrder, setLastOrder] = React.useState(null); const [booted, setBooted] = React.useState(false); const cartBadgeRef = React.useRef(); // Load live catalog/settings from the API once. React.useEffect(() => { apiGet('bootstrap').then(d => { if (d && Array.isArray(d.products)) { PRODUCTS = d.products; SCENT_FAMILIES = d.families || []; SETTINGS = d.settings || {}; } setBooted(true); }).catch(() => setBooted(true)); }, []); const setRoute = (r, data) => { _setRoute(r); setRouteData(data || null); window.scrollTo({ top: 0, behavior: 'instant' }); }; const openPDP = (p) => { setPdpProduct(p); setRoute('pdp'); }; // ── flying bottle animation ─────────────────── const flyToCart = (sourceEl, color) => { if (!sourceEl || !cartBadgeRef.current) return; const srcRect = sourceEl.getBoundingClientRect(); const tgtRect = cartBadgeRef.current.getBoundingClientRect(); const fly = document.createElement('div'); fly.className = 'fly'; fly.style.background = `linear-gradient(170deg, ${color}, color-mix(in oklab, ${color} 55%, #000))`; fly.style.left = srcRect.left + srcRect.width / 2 - 20 + 'px'; fly.style.top = srcRect.top + srcRect.height / 2 - 25 + 'px'; document.body.appendChild(fly); requestAnimationFrame(() => { fly.style.transform = `translate(${tgtRect.left - srcRect.left - srcRect.width / 2 + 20 + tgtRect.width/2}px, ${tgtRect.top - srcRect.top - srcRect.height / 2 + 25}px) scale(0.2) rotate(360deg)`; fly.style.opacity = '0.3'; }); setTimeout(() => { fly.remove(); const badge = document.querySelector('[data-cart-badge]'); if (badge) { badge.classList.remove('cart-badge-shake'); void badge.offsetWidth; badge.classList.add('cart-badge-shake'); } }, 720); }; const addToCart = (product, sourceEl, size) => { const sz = size || product.sizes[1]; setCart(prev => { const idx = prev.findIndex(i => i.id === product.id && i.size === sz.ml); if (idx >= 0) { const next = [...prev]; next[idx] = { ...next[idx], qty: next[idx].qty + 1 }; return next; } return [...prev, { id: product.id, name_ar: product.name_ar, name_en: product.name_en, tone: product.tone, concentration: product.concentration, size: sz.ml, price: sz.price, qty: 1, }]; }); flyToCart(sourceEl, product.tone); }; // ── theme injection ──────────────────────────── const fontPair = FONT_PAIRS[t.fontPair] || FONT_PAIRS['messiri-cormorant']; React.useEffect(() => { document.documentElement.lang = lang; document.documentElement.dir = lang === 'ar' ? 'rtl' : 'ltr'; }, [lang]); React.useEffect(() => { const id = 'dyn-fonts'; let l = document.getElementById(id); if (!l) { l = document.createElement('link'); l.id = id; l.rel = 'stylesheet'; document.head.appendChild(l); } l.href = `https://fonts.googleapis.com/css2?family=${fontPair.google.replace(/\|/g, '&family=')}&family=JetBrains+Mono:wght@400;500&display=swap`; }, [t.fontPair]); const styleVars = { '--bg': t.palette[0], '--bg-2': `color-mix(in oklab, ${t.palette[0]} 88%, ${t.palette[1]})`, '--paper': `color-mix(in oklab, ${t.palette[0]} 96%, #fff)`, '--ink': t.palette[1], '--ink-2': `color-mix(in oklab, ${t.palette[1]} 75%, ${t.palette[0]})`, '--ink-3': `color-mix(in oklab, ${t.palette[1]} 50%, ${t.palette[0]})`, '--line': `color-mix(in oklab, ${t.palette[1]} 14%, transparent)`, '--line-2': `color-mix(in oklab, ${t.palette[1]} 7%, transparent)`, '--accent': (SETTINGS && SETTINGS.accent) || t.palette[2], '--accent-2': t.palette[3], '--f-ar-head': fontPair.arHead, '--f-ar-body': fontPair.arBody, '--f-en-head': fontPair.enHead, '--f-en-body': fontPair.enBody, }; const cartCount = cart.reduce((s, i) => s + i.qty, 0); const minimalNav = route === 'checkout' || route === 'confirm'; return (
{t('استبرق هي بيت عطور مقرّه دبي، يستورد ويوزّع أرقى العطور المقطّرة يدوياً من بيوت العائلات الهندية. نعمل مع 12 عطّاراً عبر ميسور، آسام، كشمير، ولكنو — كل قطرة تحمل توقيع صانعها.', 'Istabraq is a Dubai-based perfumery, importing and decanting the finest hand-distilled fragrances from Indian family ateliers. We work with 12 attar masters across Mysore, Assam, Kashmir and Lucknow — every drop bears its maker\'s signature.')}
{t('خلف كل زجاجة من استبرق، عطّار يعمل بطرق توارثتها العائلات منذ قرون. هؤلاء بعضٌ منهم.', 'Behind every Istabraq bottle is an attar maker working by methods passed down through families for centuries. These are a few of them.')}
{lang === 'ar' ? a.bio_ar : a.bio_en}
"{lang === 'ar' ? a.quote_ar : a.quote_en}"