// Page sections for HOS /* ───────────── Announcement bar (marquee) ───────────── */ function Announcement() { const { t } = useLang(); const messages = [ t('New · Summer Édition is now online', 'Nouveau · l’Édition Été est en ligne'), t('Become a member — early access to drops', 'Devenez membre — accès anticipé aux drops'), t('Follow @houseofsheylas', 'Suivez @houseofsheylas'), t('Discover the Summer lookbook', 'Découvrez le lookbook Été'), ]; const row = [...messages, ...messages]; // duplicate for seamless marquee return (
{row.map((m, i) => ( {m} ))}
); } /* ───────────── Nav ───────────── */ function SearchOverlay({ onClose }) { const { t } = useLang(); const { siteData } = useSiteData(); const products = siteData.products; const [q, setQ] = useState(''); const inputRef = useRef(null); useEffect(() => { inputRef.current?.focus(); const onKey = (e) => { if (e.key === 'Escape') onClose(); }; window.addEventListener('keydown', onKey); return () => window.removeEventListener('keydown', onKey); }, [onClose]); const results = q.trim().length >= 2 ? products.filter(p => p.name.toLowerCase().includes(q.toLowerCase()) || (p.category || '').toLowerCase().includes(q.toLowerCase())).slice(0, 8) : []; return (
setQ(e.target.value)} placeholder={t('Search pieces…','Rechercher des pièces…')} className="flex-1 bg-transparent font-serif text-deep text-[22px] outline-none placeholder:text-deep/35" />
{results.length > 0 && (
)} {q.trim().length >= 2 && results.length === 0 && (

{t('No results for','Aucun résultat pour')} “{q}”

)}
); } function Nav({ onOpenMenu, route = 'home' }) { const cart = useCart(); const { t } = useLang(); const [scrolled, setScrolled] = useState(false); const [searchOpen, setSearchOpen] = useState(false); useEffect(() => { const onScroll = () => setScrolled(window.scrollY > 24); window.addEventListener('scroll', onScroll, { passive: true }); return () => { window.removeEventListener('scroll', onScroll); }; }, []); // Light (ivory) treatment ONLY while sitting transparently over the dark video hero on home. // The real route is passed from App so this stays correct under pathname-based navigation. const head = String(route).split('/')[0]; const overHero = head === 'home' || head === ''; const light = overHero && !scrolled; const ink = light ? 'text-ivory' : 'text-deep'; const hov = light ? 'hover:text-blush' : 'hover:text-rose'; return (
{/* Left: Menu button */}
{/* Logo center */}
HOS HOUSE OF SHEYLAS
{/* Right: icons */}
{searchOpen && setSearchOpen(false)}/>}
); } /* ───────────── Hero ───────────── Cinematic full-bleed video. French editorial: a woman in a flowing gown walking through a glass garden pergola. Nav floats over it in ivory. */ function Hero() { const { t, lang } = useLang(); const { siteData } = useSiteData(); const v = HERO_VIDEOS[0]; // Homepage CMS: read editable hero content (bilingual) with the original copy as fallback. const hp = (siteData.homepage && siteData.homepage.hero) || {}; const hv = (field, en, fr) => { const node = hp[field]; if (node && typeof node === 'object') return node[lang] || node.en || t(en, fr); if (typeof node === 'string' && node.trim()) return node; return t(en, fr); }; const videoSrc = (typeof hp.videoUrl === 'string' && hp.videoUrl.trim()) ? hp.videoUrl : '/uploads/hero.mp4'; return ( <>
{/* Background video — House of Sheylas brand film (single source, no foreign poster flash) */}
{/* faint film grain */}
{/* Content */}
{/* Meta row, clearing the nav */}
{hv('metaLeft', 'Boutique · Paris · Est. 2025', 'Boutique · Paris · Depuis 2025')}
{hv('metaRight', 'N°08 · Summer', 'N°08 · Été')}
{/* Bottom editorial block */}
{hv('eyebrow', 'Summer · Édition 2026', 'Édition Été 2026')}

{hv('titleLine', 'Soft elegance', 'Élégance douce')}
{hv('titleAccent', 'for every woman.', 'pour chaque femme.')}

{hv('description', 'A curated edit of feminine pieces — selected to make you feel confident, graceful and unforgettable.', 'Une sélection de pièces féminines — choisies pour vous sentir confiante, gracieuse et inoubliable.' )}

{/* Floating featured product — glass card */}
{/* Scroll cue */}
{t('Scroll','Défiler')}
{/* Press strip — light band easing from the dark hero */}
{t('As featured in', 'Vu dans')} {PRESS.slice(0, 5).map(p => ( {p.name} ))}
); } const PRESS = [ { name: 'VOGUE', size: 18, tracking: '.32em' }, { name: 'ELLE', size: 22, tracking: '.36em' }, { name: "HARPER'S BAZAAR", size: 15, tracking: '.22em' }, { name: 'Vanity Fair', size: 17, italic: true, tracking: '.06em' }, { name: 'PORTER', size: 18, tracking: '.32em' }, ]; function Sparkle({ className = '', ...rest }) { return ( ); } /* ClipWord — animated word reveal via clip-path mask + translate */ function ClipWord({ children, delay = 0, className = '', style = {}, as: As = 'span' }) { const ref = useRef(null); const [shown, setShown] = useState(false); useEffect(() => { const el = ref.current; if (!el) return; const io = new IntersectionObserver(([e]) => { if (e.isIntersecting) { setTimeout(() => setShown(true), delay); io.unobserve(el); } }, { threshold: .2 }); io.observe(el); return () => io.disconnect(); }, [delay]); return ( {children} ); } /* ───────────── Categories ───────────── */ const CAT_FR = { dresses:'Robes', sets:'Ensembles', tops:'Hauts', accessories:'Accessoires', bottoms:'Bas' }; // Horizontal scroll carousel with arrow controls — reused by Categories / arrivals / best sellers. function Carousel({ children, itemWidth = 'w-[78%] sm:w-[300px]' }) { const trackRef = useRef(null); const scroll = (dir) => { const el = trackRef.current; if (!el) return; el.scrollBy({ left: dir * (el.clientWidth * 0.85), behavior: 'smooth' }); }; return (
{React.Children.map(children, (child) => (
{child}
))}
); } // Read an editable homepage section heading (bilingual) with a fallback. function homepageSectionGetter(siteData, lang, t) { const sec = (siteData.homepage && siteData.homepage.sections) || {}; return (field, en, fr) => { const n = sec[field]; if (n && typeof n === 'object') return n[lang] || n.en || t(en, fr); return (typeof n === 'string' && n.trim()) ? n : t(en, fr); }; } function Categories() { const { t, lang } = useLang(); const { siteData } = useSiteData(); const sv = homepageSectionGetter(siteData, lang, t); const categories = (siteData.categories || []).filter((c) => c && c.name); if (!categories.length) return null; return (
{sv('categoriesEyebrow', 'Curated for her', 'Sélection pour elle')}

{sv('categoriesTitle', 'Shop by category', 'Par catégorie')}

{t( 'From everyday softness to occasion-ready silhouettes — find the piece your wardrobe is missing.', 'De la douceur du quotidien aux silhouettes pour les grandes occasions — trouvez la pièce qui manque à votre vestiaire.' )}

{categories.map((c) => (
{c.img ? {c.name} :
); } /* ───────────── New arrivals ───────────── */ // Products flagged "New" (by badge or the API's isNew). Capped to 10 for the homepage. function selectNew(products = []) { const newish = products.filter((p) => /new|just|nouvea|arriv/i.test(String(p.badge || p.tag || '')) || p.isNew); return (newish.length ? newish : products).slice(0, 10); } function NewArrivals() { const { t, lang } = useLang(); const { siteData } = useSiteData(); const sv = homepageSectionGetter(siteData, lang, t); const items = selectNew(siteData.products || []); if (!items.length) return null; return (
{sv('newEyebrow', 'Just landed', 'Tout juste arrivé')}

{sv('newTitle', 'New arrivals', 'Nouveautés')}

{t('View all →', 'Voir tout →')}
{items.map((p, i) => )}
); } /* ───────────── Brand story ───────────── */ // Read an editable homepage group field (bilingual {en,fr} or plain string) with a fallback. function homepageGroupGetter(siteData, lang, t, group) { const g = (siteData.homepage && siteData.homepage[group]) || {}; return { val: (field, en, fr) => { const n = g[field]; if (n && typeof n === 'object') return n[lang] || n.en || t(en, fr); return (typeof n === 'string' && n.trim()) ? n : t(en, fr); }, raw: (field, fallback) => { const n = g[field]; return (typeof n === 'string' && n.trim()) ? n : fallback; }, }; } function BrandStory() { const { t, lang } = useLang(); const { siteData } = useSiteData(); const s = homepageGroupGetter(siteData, lang, t, 'story'); const imgPrimary = s.raw('imagePrimary', STORY_IMG_1); const imgSecondary = s.raw('imageSecondary', STORY_IMG_2); return (
{/* Texture */}
{/* Image collage */}
{/* Copy */}
{s.val('eyebrow', 'Our story', 'Notre histoire')}

{s.val('titleLine1', 'For women who love', 'Pour les femmes qui aiment')}
{s.val('titleLine2', 'elegance, confidence,', 'l’élégance, la confiance,')}
{s.val('titleLine3', 'and timeless femininity.', 'et la féminité intemporelle.')}

{s.val('body', 'House of Sheylas is a curated boutique, bringing together feminine, elevated pieces — chosen with care for the women who wear them.', 'House of Sheylas est une boutique qui rassemble des pièces féminines et raffinées — choisies avec soin pour les femmes qui les portent.' )}

{[ [t('Curated','Sélection'), t('Hand-picked pieces', 'Pièces choisies')], [t('Edit','Édit'), t('Refined & feminine', 'Raffinées & féminines')], [t('For her','Pour elle'), t('Women who dress for themselves','Pour les femmes qui s’habillent pour elles')], ].map(([k,v]) => (
{k}
{v}
))}
); } /* ───────────── Best sellers ───────────── */ // Best sellers = ONLY products the admin badges as best-seller / hot / most-loved / // restocked. Featured ("Hero") products are intentionally excluded so a New-badged hero // piece doesn't leak in here. Capped to 10. function selectBest(products = []) { return products .filter((p) => /best|most|loved|restock|réassort|seller|hot/i.test(String(p.badge || ''))) .slice(0, 10); } function BestSellers() { const cart = useCart(); const { t, lang } = useLang(); const { siteData } = useSiteData(); const sv = homepageSectionGetter(siteData, lang, t); const items = selectBest(siteData.products || []); if (!items.length) return null; return (
{sv('bestEyebrow', 'Always in rotation', 'Toujours en rotation')}

{sv('bestTitle', 'Best sellers', 'Meilleures ventes')}

{t( 'The pieces our community returns for — restocked, re-loved, and rarely in stock for long.', 'Les pièces que notre communauté redemande — réassorties, ré-adorées, et rarement en stock longtemps.' )}

{items.map((p, i) => (
{p.name} {lang === 'fr' ? (TAG_FR[p.badge] || p.badge || 'Best-seller') : (p.badge || 'Best seller')}

{p.name}

{[0,1,2,3,4].map(i => )} ({p.reviews})
))}
); } /* ───────────── Instagram / lifestyle ───────────── */ function Instagram() { const { t, lang } = useLang(); const { siteData } = useSiteData(); const ig = homepageGroupGetter(siteData, lang, t, 'instagram'); const cmsImages = (siteData.homepage && siteData.homepage.instagram && Array.isArray(siteData.homepage.instagram.images)) ? siteData.homepage.instagram.images : []; const images = [0, 1, 2, 3].map((i) => (cmsImages[i] && cmsImages[i].trim()) ? cmsImages[i] : INSTAGRAM[i]); const settings = siteData.settings || {}; const igUrl = ig.raw('url', settings.instagramUrl || '#') || '#'; return (
{ig.raw('handle', '@houseofsheylas')}

{ig.val('title', 'From our community', 'Notre communauté')}

{ig.val('subtitle', 'Tag #houseofsheylas to be featured.', 'Tagguez #houseofsheylas pour être à l’honneur.')}

{images.map((src, i) => (
))}
{ig.val('ctaLabel', 'Follow us on Instagram →', 'Suivez-nous sur Instagram →')}
); } /* ───────────── Newsletter ───────────── */ function Newsletter() { const { t } = useLang(); const [email, setEmail] = useState(''); const [state, setState] = useState('idle'); // idle | loading | success | error const [touched, setTouched] = useState(false); const valid = /^\S+@\S+\.\S+$/.test(email); const onSubmit = async (e) => { e.preventDefault(); setTouched(true); if (!valid) return setState('error'); setState('loading'); try { const res = await fetch('/api/newsletter', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ email }), }); const data = await res.json(); if (data.ok) { setState('success'); } else if (data.error === 'already_subscribed') { setState('success'); // treat existing subscriber as success } else { setState('error'); } } catch { setState('error'); } }; return (
{/* Decorative motifs */}
{t('Become a member', 'Devenir membre')}

{t('Join', 'Rejoignez')} {t('us','-nous')}

{t( 'Get early access to new drops, private sales, and styling inspiration delivered with care to your inbox.', 'Accès anticipé aux nouvelles collections, ventes privées et inspirations de style — livré dans votre boîte mail avec soin.' )}

{state !== 'success' ? ( <>
{ setEmail(e.target.value); if (state==='error') setState('idle'); }} placeholder={t('Enter your email address', 'Votre adresse email')} className="lux-input" style={{ color: '#FFF8F5', borderBottom: 'none' }} aria-label={t('Email','Email')} disabled={state==='loading'} />
{touched && !valid && (
{t('Please enter a valid email.','Veuillez saisir un email valide.')}
)}

{t( 'By subscribing you agree to our privacy policy. Unsubscribe any time.', 'En vous inscrivant, vous acceptez notre politique de confidentialité. Désinscription à tout moment.' )}

) : (
{t('Welcome home.','Bienvenue chez vous.')}

{t('A 10% welcome code is on its way to','Un code de bienvenue de 10 % arrive bientôt à')} {email}. {t('See you in your inbox.','À très vite dans votre boîte mail.')}

)}
); } /* ───────────── Footer ───────────── */ function Footer() { const { t } = useLang(); const { siteData } = useSiteData(); const s = (siteData && siteData.settings) || SITE_SETTINGS; const socials = [ [I.insta, s.instagramUrl], [I.tiktok, s.tiktokUrl], [I.facebook, s.facebookUrl], ].filter(([, href]) => href && String(href).trim()); const cols = [ { title: t('Shop', 'Boutique'), items: [ [t('New In', 'Nouveautés'), '#shop/new'], [t('Dresses', 'Robes'), '#shop/dresses'], [t('Sets', 'Ensembles'), '#shop/sets'], [t('Tops', 'Hauts'), '#shop/tops'], [t('Accessories', 'Accessoires'), '#shop/accessories'], [t('Best Sellers', 'Meilleures ventes'), '#shop/bestsellers'], ], }, { title: t('Care', 'Service'), items: SERVICES.map((service) => [service.title, service.href]), }, { title: t('Boutique', 'La boutique'), items: [ [t('Our Story', 'Notre histoire'), '#about'], [t('Collections', 'Collections'), '#collections'], [t('Contact', 'Contact'), '#contact'], [t('Admin', 'Admin'), '#admin'], ], }, ]; return ( ); } /* ───────────── Cart Drawer ───────────── */ /* ───────────── Checkout Drawer ───────────── */ function CheckoutDrawer({ onClose }) { const cart = useCart(); const { t } = useLang(); const { currency, base, rates, priceOf, fmtRaw } = useCurrency(); // Subtotal in the visitor's currency, honouring any per-zone pinned prices per item. const cartSubtotal = cart.items.reduce((s, it) => s + priceOf(it.price, it.overrides) * it.qty, 0); const [step, setStep] = useState('form'); // form | success | error const [loading, setLoading] = useState(false); const [orderNumber, setOrderNumber] = useState(''); const [errorMsg, setErrorMsg] = useState(''); const [form, setForm] = useState({ name:'', email:'', phone:'', country:'FR', city:'', address:'', notes:'' }); const [touched, setTouched] = useState({}); const set = (k, v) => setForm(f => ({...f, [k]: v})); const blur = (k) => setTouched(t => ({...t, [k]: true})); const valid = { name: form.name.trim().length >= 2, email: /^\S+@\S+\.\S+$/.test(form.email), phone: form.phone.trim().length >= 6, city: form.city.trim().length >= 2, address: form.address.trim().length >= 5, }; const allValid = Object.values(valid).every(Boolean); const isCOD = form.country === 'TN'; const submit = async (e) => { e.preventDefault(); setTouched({ name:true, email:true, phone:true, city:true, address:true }); if (!allValid) return; setLoading(true); try { // Best-effort bot-protection token (null if reCAPTCHA isn't available — never blocks). const recaptchaToken = (typeof window !== 'undefined' && window.getRecaptchaToken) ? await window.getRecaptchaToken('checkout') : null; const res = await fetch('/api/orders', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ currency, recaptchaToken, items: cart.items.map(it => ({ id:it.id, name:it.name, price: Math.round(effectivePrice(it.price, it.overrides, { currency, base, rates })), qty:it.qty, color:it.color, size:it.size })), customer: { name:form.name, email:form.email, phone:form.phone, country:form.country, city:form.city, address:form.address, notes:form.notes }, }), }); const data = await res.json(); if (data.ok) { setOrderNumber(data.order?.order_number || ''); const ids = cart.items.map(it => it.id); ids.forEach(id => cart.remove(id)); setStep('success'); } else { setErrorMsg(data.error || t('Something went wrong. Please try again.','Une erreur est survenue. Veuillez réessayer.')); setStep('error'); } } catch { setErrorMsg(t('Network error — please check your connection.','Erreur réseau — vérifiez votre connexion.')); setStep('error'); } finally { setLoading(false); } }; const fc = (k) => `lux-input${touched[k] && !valid[k] ? ' !border-rose' : ''}`; useEffect(() => { document.body.classList.add('no-scroll'); return () => document.body.classList.remove('no-scroll'); }, []); return (