// HOS — secondary pages (Shop / Collections / Product / About / Contact) /* ───────────── Shared ───────────── */ function PageHero({ eyebrow, title, subtitle, crumbs, tone = 'blush', tall = false }) { const bg = tone === 'champagne' ? 'bg-champagne/60' : tone === 'ivory' ? 'bg-ivory grain' : 'bg-blush'; return (
{crumbs && (
{crumbs.map((c, i) => ( {i > 0 && /} {c.href ? {c.label} : {c.label}} ))}
)}
{eyebrow}

{title}

{subtitle && (

{subtitle}

)}
); } /* ───────────── SHOP / PLP ───────────── */ const CAT_FR_PAGE = { dresses:'Robes', sets:'Ensembles', tops:'Hauts', bottoms:'Bas', accessories:'Accessoires' }; function ShopPage({ subPath }) { const { t, lang } = useLang(); const { siteData } = useSiteData(); const liveProducts = siteData.products.length > 0 ? siteData.products : ALL_PRODUCTS; // Determine filter from URL: shop, shop/new, shop/bestsellers, shop/ const filterKey = subPath || 'all'; const titleMap = { all: { eyebrow: t('Shop everything','Tout voir'), title: t('The full edit','Le vestiaire complet'), sub: t('Every piece from House of Sheylas — curated, considered, and made to be worn for years.','Toutes les pièces House of Sheylas — sélectionnées, pensées et faites pour durer.') }, new: { eyebrow: t('Just landed','Tout juste arrivé'), title: t('New arrivals','Nouveautés'), sub: t('The latest arrivals — freshly added to the edit.','Les dernières arrivées — tout juste ajoutées à la sélection.') }, bestsellers: { eyebrow: t('Always in rotation','Toujours en rotation'), title: t('Best sellers','Meilleures ventes'), sub: 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.') }, dresses: { eyebrow: t('Edit','Sélection'), title: t('Dresses','Robes'), sub: t('From slip silks to flowing maxis — silhouettes designed to drape, move, and stay with you.','Du satin léger aux maxis fluides — des silhouettes pensées pour draper, bouger et durer.') }, sets: { eyebrow: t('Edit','Sélection'), title: t('Sets','Ensembles'), sub: t('Effortless coordinates — wear together or apart.','Coordonnés sans effort — ensemble ou séparément.') }, tops: { eyebrow: t('Edit','Sélection'), title: t('Tops','Hauts'), sub: t('Soft blouses, knits, and tailoring — the foundations of a quietly elegant wardrobe.','Blouses douces, mailles et tailoring — les fondations d’un vestiaire discrètement élégant.') }, bottoms: { eyebrow: t('Edit','Sélection'), title: t('Bottoms','Bas'), sub: t('Tailored trousers, flowing skirts, and softly worn denims.','Pantalons cintrés, jupes fluides et denims doucement portés.') }, accessories: { eyebrow: t('Edit','Sélection'), title: t('Accessories','Accessoires'),sub: t('The finishing touches — small, sentimental, made to last.','Les touches finales — petites, sentimentales, faites pour durer.') }, }; // Resolve the page heading — known filters use the curated copy above; any other // category (including ones created in the admin) falls back to its own name/description. const catMatch = (siteData.categories || []).find((c) => c.id === filterKey); const meta = titleMap[filterKey] || (catMatch ? { eyebrow: t('Edit', 'Sélection'), title: lang === 'fr' ? (CAT_FR_PAGE[catMatch.id] || catMatch.name) : catMatch.name, sub: catMatch.description || t('Browse the edit.', 'Parcourez la sélection.'), } : titleMap.all); // Filter products — uses live Supabase data once loaded const products = useMemo(() => { if (filterKey === 'all') return liveProducts; if (filterKey === 'new') return liveProducts.filter(p => p.isNew); if (filterKey === 'bestsellers') return liveProducts.filter(p => p.isBest); return liveProducts.filter(p => p.category === filterKey); }, [filterKey, liveProducts]); // Filter controls const [sort, setSort] = useState('featured'); const [colorF, setColorF] = useState(null); const [sizeF, setSizeF] = useState(null); const [priceMax, setPriceMax] = useState(null); // null = no cap (show all prices) const [view, setView] = useState('grid'); // grid | dense const [filtersOpen, setFiltersOpen] = useState(false); const activeFilters = (colorF ? 1 : 0) + (sizeF ? 1 : 0) + (priceMax != null ? 1 : 0); // Slider ceiling adapts to the real catalogue so high-priced pieces are never hidden. const priceCeiling = useMemo(() => { const top = Math.max(400, ...liveProducts.map(p => Math.ceil(Number(p.price) || 0))); return Math.ceil(top / 50) * 50; }, [liveProducts]); const sorted = useMemo(() => { let arr = [...products]; if (colorF) arr = arr.filter(p => p.colors?.some(c => c.name === colorF)); if (sizeF) arr = arr.filter(p => (p.sizes || SIZES).includes(sizeF)); arr = arr.filter(p => priceMax == null || p.price <= priceMax); if (sort === 'price-asc') arr.sort((a,b) => a.price - b.price); if (sort === 'price-desc') arr.sort((a,b) => b.price - a.price); if (sort === 'new') arr.sort((a,b) => (b.isNew?1:0) - (a.isNew?1:0)); return arr; }, [products, sort, colorF, sizeF, priceMax]); const allColors = useMemo(() => { const s = new Set(); liveProducts.forEach(p => p.colors?.forEach(c => s.add(JSON.stringify(c)))); return [...s].map(s => JSON.parse(s)); }, [liveProducts]); // Quick filter chips — All / New / Best sellers, then every real category from the DB // (so a new admin category like "Swimwear" shows up here automatically). const chips = [ { key:'all', label: t('All','Tout'), href:'#shop' }, { key:'new', label: t('New In','Nouveautés'), href:'#shop/new' }, { key:'bestsellers', label: t('Best Sellers','Meilleures ventes'), href:'#shop/bestsellers' }, ...(siteData.categories || []).filter((c) => c && c.name).map((c) => ({ key: c.id, label: lang === 'fr' ? (CAT_FR_PAGE[c.id] || c.name) : c.name, href: `#shop/${c.id}`, })), ]; const catBreadcrumb = filterKey==='all' ? null : { label: meta.title }; return ( <> {/* Sticky filter rail */}
{/* Chips */}
{chips.map(c => ( {c.label} ))}
{/* Sort + view */}
{/* Toolbar: Filters button + result count */}
{sorted.length} {t('pieces','pièces')}
{/* Grid */} {sorted.length === 0 ? (
{t('No matches','Aucun résultat')}

{t('Adjust your filters to see more pieces.','Modifiez vos filtres pour voir plus de pièces.')}

) : (
{sorted.map((p, i) => )}
)}
{/* Filter drawer (slides in from the right, all breakpoints) */} {filtersOpen && (
setFiltersOpen(false)}/>
)}
); } /* ───────────── COLLECTIONS ───────────── */ function CollectionsPage() { const { t, lang } = useLang(); const SEASON_FR = { 'SS 2026':'PE 2026', 'AW 2026':'AH 2026', 'Resort':'Croisière', 'Capsule':'Capsule' }; const COL_FR = { 'spring-bloom':'Édition Été', 'summer-edit':'Édition Été', 'soft-noir':'Noir doux', 'champagne':'Heure du Champagne', 'atelier':'Édition sélectionnée' }; return ( <> Collections & éditions : <>Collections & édits} subtitle={t('Each collection is a curated edit — selected and styled with intention.','Chaque collection est une sélection — choisie et stylée avec intention.')} crumbs={[{label:t('Home','Accueil'), href:'#home'}, {label:t('Collections','Collections')}]} tone="champagne" /> {/* Featured collection — large editorial */}
{t('Featured · SS 2026','En vedette · PE 2026')}
{t('Summer Edit','Édition Été')}
{t('Discover','Découvrir')}
{t('Chapter 01','Chapitre 01')}

{t('A garden in','Un jardin en')} {t('silk.','soie.')}

{t( 'A selection inspired by Parisian flower markets at dawn — feather-light silks, embroidered detail, and soft tailoring, hand-picked for the season.', 'Une sélection inspirée des marchés aux fleurs parisiens à l’aube — soies légères, détails brodés et tailoring doux, choisis pour la saison.' )}

{[ ['32', t('Pieces','Pièces')], ['08', t('Looks','Looks')], ['100%', t('Silk & linen','Soie & lin')] ].map(([k,v]) => (
{k}
))}
{/* Other collections grid */}
{t('Édits','Éditions')}

{t('More collections','Autres collections')}

{t( 'Explore past chapters and capsules — many pieces remain available in limited quantities.', 'Explorez les chapitres et capsules passés — de nombreuses pièces restent disponibles en quantités limitées.' )}

); } /* ───────────── PRODUCT DETAIL ───────────── */ const COPY_FR = { description: 'Coupée dans une soie satinée légère, cette pièce drape sans effort grâce à une silhouette en biais qui suit vos mouvements. Bretelles réglables et encolure dégagée — élégante pour le soir comme superposée à une maille.', fabric: '100 % soie de mûrier · Nettoyage à sec uniquement', fit: 'Taille normale · Mannequin 1,75 m portant un S', shipping: 'Livraison internationale · Calculée à la caisse', }; function ProductPage({ productId }) { const { siteData } = useSiteData(); const liveProducts = siteData.products.length > 0 ? siteData.products : ALL_PRODUCTS; const p = liveProducts.find(x => x.id === productId || x.slug === productId) || liveProducts[0]; const cart = useCart(); const { t, lang } = useLang(); const { fmt, priceOf, fmtRaw } = useCurrency(); const COPY_L = lang === 'fr' ? COPY_FR : COPY; const soldOut = p.stock === 0 || p.stock === '0'; // Real product images only — no more random hero/story photos. const gallery = (Array.isArray(p.gallery) && p.gallery.length ? p.gallery : [p.img]).filter(Boolean).slice(0, 6); // Real sizes from the product's variants, falling back to the standard run. const sizeOptions = (Array.isArray(p.sizes) && p.sizes.length) ? p.sizes : SIZES; const [size, setSize] = useState(sizeOptions[0]); const [colorIx, setColorIx] = useState(0); const [qty, setQty] = useState(1); // Selected-variant pricing: a variant's own price/overrides win, else the product's. const selColor = (Array.isArray(p.colors) && p.colors[colorIx]) ? p.colors[colorIx] : {}; const priceBase = (selColor.price !== undefined && selColor.price !== null && selColor.price !== '') ? Number(selColor.price) : p.price; const priceOv = (selColor.priceOverrides && Object.keys(selColor.priceOverrides).length) ? selColor.priceOverrides : p.priceOverrides; const [tab, setTab] = useState('description'); const [mainImg, setMainImg] = useState(p.img); const [guideOpen, setGuideOpen] = useState(false); useEffect(() => { setMainImg(p.img); setColorIx(0); setSize(sizeOptions[0]); setQty(1); }, [productId]); // "You may also love" — admin-chosen pairings first (p.related), then same-category fill. const related = useMemo(() => { const picks = (Array.isArray(p.related) ? p.related : []) .map((ref) => liveProducts.find((x) => x.slug === ref || x.id === ref)) .filter(Boolean); const ids = new Set(picks.map((x) => x.id)); const fill = liveProducts.filter((x) => x.category === p.category && x.id !== p.id && !ids.has(x.id)); return [...picks, ...fill].filter((x) => x.id !== p.id).slice(0, 4); }, [p, liveProducts]); // Real description + universal, professional product info (no placeholder silk-satin copy). const productDesc = p.description || t('A considered piece, selected for the House of Sheylas edit.', 'Une pièce choisie avec soin pour la sélection House of Sheylas.'); const TAB_LABELS = { description: t('Details','Détails'), fabric: t('Fabric & care','Tissu & entretien'), fit: t('Fit','Coupe'), shipping: t('Shipping','Livraison'), }; const TAB_CONTENT = { description: productDesc, fabric: p.material || t('Cool, gentle wash or dry clean. Reshape and lay flat to dry. Cool iron if needed.','Lavage doux à froid ou nettoyage à sec. Remettre en forme et sécher à plat. Repasser à froid si besoin.'), fit: p.fit || t('True to size. Between sizes? Size up for a relaxed fit.','Taille normale. Entre deux tailles ? Prenez la taille au-dessus pour un tombé ample.'), shipping: t('Worldwide shipping, calculated at checkout. Ships within 1–2 business days. Easy 14-day returns.','Livraison mondiale, calculée au paiement. Expédition sous 1 à 2 jours ouvrés. Retours faciles sous 14 jours.'), }; const catLabel = lang === 'fr' ? (CAT_FR_PAGE[p.category] || p.category) : p.category; return ( <>
{/* Gallery */}
{gallery.map((g, i) => ( ))}
{p.name} {soldOut ? ( {t('Sold out','Épuisé')} ) : p.badge ? ( {lang==='fr' ? (TAG_FR[p.badge] || p.badge) : p.badge} ) : p.tag ? ( {lang==='fr' ? (TAG_FR[p.tag] || p.tag) : p.tag} ) : null}
{/* Mobile thumbs */}
{gallery.map((g, i) => ( ))}
{/* Details */}
{catLabel} · {t('Summer Édition','Édition Été')}

{p.name}

{p.reviews > 0 && (
{[0,1,2,3,4].map(i => )} ({p.reviews})
)}

{productDesc}

{/* Color */} {p.colors && (

{t('Color','Couleur')}

{p.colors[colorIx].name}
{p.colors.map((c, i) => (
)} {/* Size */}

{t('Size','Taille')}

{sizeOptions.map(s => ( ))}
{/* Qty + add */}
{qty}
{/* Trust strip */}
{[ ['✿', t('Hand-picked','Sélection soignée')], ['✦', t('Curated edit','Édit sélectionné')], ['◆', t('Limited stock','Stock limité')], ].map(([icon, label]) => (
{icon}
{label}
))}
{/* Tabs */}
{Object.keys(TAB_LABELS).map(k => ( ))}

{TAB_CONTENT[tab]}

{/* You may also love */} {related.length > 0 && (
{t('Pair it with','À associer avec')}

{t('You may also love','Vous aimerez aussi')}

{related.map((rp, i) => )}
)} {/* Universal size guide */} {guideOpen && (
setGuideOpen(false)} style={{ position:'fixed', inset:0, zIndex:300, background:'rgba(74,45,51,0.5)', display:'flex', alignItems:'center', justifyContent:'center', padding:'1rem' }}>
e.stopPropagation()} style={{ background:'#FFF8F5', maxWidth:540, width:'100%', borderRadius:6, padding:'1.75rem', boxShadow:'0 24px 70px rgba(74,45,51,0.3)' }}>
{t('Size guide','Guide des tailles')}

{t('Find your fit','Trouvez votre taille')}

{t('Measurements in centimetres. If you are between sizes, we recommend sizing up.','Mesures en centimètres. Entre deux tailles, nous conseillons la taille au-dessus.')}

{[ ['XS','78–82','60–64','86–90'], ['S','82–86','64–68','90–94'], ['M','86–90','68–72','94–98'], ['L','90–95','72–78','98–103'], ['XL','95–100','78–84','103–109'], ].map((row) => ( ))}
{t('Size','Taille')} {t('Bust','Poitrine')} {t('Waist','Taille')} {t('Hips','Hanches')}
{row[0]} {row[1]} {row[2]} {row[3]}

{t('A general guide — fit can vary slightly by piece.','Un guide général — la coupe peut varier légèrement selon la pièce.')}

)} ); } /* ───────────── ABOUT ───────────── */ function AboutPage() { const { t, lang } = useLang(); const ROLE_FR = { Studio: 'Studio', Operations: 'Opérations', Office: 'Bureau', Atelier: 'Atelier' }; const NOTE_FR = { 'Where it all began — 2025.': 'Là où tout a commencé — 2025.', 'Serving our North American community.': 'Au service de notre communauté nord-américaine.', 'Part of our team, behind the scenes.': 'Une partie de notre équipe, en coulisses.', }; const trRole = (r) => (lang === 'fr' ? (ROLE_FR[r] || r) : r); const trNote = (n) => (lang === 'fr' ? (NOTE_FR[n] || n) : n); const VALUES = [ { id: '01', title: t('Curated selection', 'Sélection pointue'), description: t('Every piece is hand-picked. We edit so you don’t have to.', 'Chaque pièce est choisie à la main. Nous sélectionnons pour vous.') }, { id: '02', title: t('Elevated quality', 'Qualité élevée'), description: t('Silk, linen, cashmere — fabrics that breathe, drape, and live with you.', 'Soie, lin, cachemire — des matières qui respirent, drapent et vous accompagnent.') }, { id: '03', title: t('Feminine by design', 'Féminin par essence'), description: t('Soft silhouettes and refined details, chosen for how they make you feel.', 'Des silhouettes douces et des détails raffinés, choisis pour ce qu’ils vous inspirent.') }, { id: '04', title: t('A quiet ethos', 'Une approche discrète'), description: t('No trends, no noise. We select for the woman, not the season.', 'Pas de tendances, pas de bruit. Nous sélectionnons pour la femme, pas pour la saison.') }, ]; return ( <> {t('Quietly made,', 'Pensée avec soin,')}
{t('always intentional.', 'toujours avec intention.')} } subtitle={t( 'House of Sheylas is an independent womenswear boutique — a curated edit of feminine, elevated pieces selected for women who dress for themselves.', 'House of Sheylas est une boutique de mode féminine indépendante — une sélection de pièces féminines et raffinées, choisies pour les femmes qui s’habillent pour elles.' )} crumbs={[{ label: t('Home', 'Accueil'), href: '#home' }, { label: t('About', 'À propos') }]} tone="ivory" tall />
{t('What we believe', 'Ce que nous croyons')}

{t('Our values', 'Nos valeurs')}

{VALUES.map((value, index) => (
{value.id}

{value.title}

{value.description}

))}
{t('Where we are', 'Nos présences')}

{t('Three places,', 'Trois lieux,')} {t('one team.', 'une équipe.')}

{LOCATIONS.map((place, index) => (
{trRole(place.role)}
{place.city}

{trNote(place.note)}

))}
); } /* ───────────── CONTACT ───────────── */ function ContactPage() { const { t } = useLang(); const [form, setForm] = useState({ name:'', email:'', subject:'general', message:'' }); const [state, setState] = useState('idle'); // idle | sending | sent | error const [touched, setTouched] = useState({}); const valid = { name: form.name.trim().length >= 2, email: /^\S+@\S+\.\S+$/.test(form.email), message: form.message.trim().length >= 8, }; const allValid = Object.values(valid).every(Boolean); const submit = async (e) => { e.preventDefault(); setTouched({ name:true, email:true, message:true }); if (!allValid) return setState('error'); setState('sending'); try { const res = await fetch('/api/contact', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ name: form.name, email: form.email, subject: form.subject, message: form.message }), }); const data = await res.json(); if (data.ok) { setState('sent'); } else { setState('error'); } } catch { setState('error'); } }; const onField = (id, v) => setForm({...form, [id]: v}); const onBlur = (id) => setTouched({...touched, [id]: true}); const fieldCls = (id) => `lux-input ${touched[id] && !valid[id] ? '!border-rose' : ''}`; return ( <> {CONTACT_CONTENT.title} {CONTACT_CONTENT.accent}} subtitle={CONTACT_CONTENT.subtitle} crumbs={[{label:t('Home','Accueil'), href:'#home'}, {label:t('Contact','Contact')}]} tone="champagne" />
{/* Form */}
{t('Send us a note','Écrivez-nous')}

{t('Write to us','Écrivez-nous')}

{state === 'sent' ? (
{t('Thank you,','Merci,')} {form.name.split(' ')[0]}.

{t('Your message is on its way to our team. We’ll be in touch shortly at','Votre message est en route vers notre équipe. Nous reviendrons vers vous sous peu à')} {form.email}.

) : (
{t('Subject','Sujet')}
{[ ...CONTACT_CONTENT.topics, ].map((topic) => ( ))}