// === Shared UI components for Tour Eiffel 2000 === const { useState, useEffect, useMemo, useRef } = React; // SVG icons const Icon = { Search: () => (), Cart: () => (), Sun: () => (), Moon: () => (), Arrow: () => (), Close: () => (), Plus: () => (), Minus: () => (), }; // === Cart store (localStorage) === const CART_KEY = 'te2000_cart'; function readCart() { try { return JSON.parse(localStorage.getItem(CART_KEY) || '[]'); } catch { return []; } } function writeCart(items) { localStorage.setItem(CART_KEY, JSON.stringify(items)); window.dispatchEvent(new CustomEvent('cart-updated')); } function useCart() { const [items, setItems] = useState(readCart()); useEffect(() => { const sync = () => setItems(readCart()); window.addEventListener('cart-updated', sync); window.addEventListener('storage', sync); return () => { window.removeEventListener('cart-updated', sync); window.removeEventListener('storage', sync); }; }, []); return { items, add: (item) => { const existing = items.find(i => i.key === item.key); const next = existing ? items.map(i => i.key === item.key ? { ...i, qty: i.qty + 1 } : i) : [...items, { ...item, qty: 1 }]; writeCart(next); }, remove: (key) => writeCart(items.filter(i => i.key !== key)), setQty: (key, qty) => writeCart(items.map(i => i.key === key ? { ...i, qty } : i)), clear: () => writeCart([]), total: items.reduce((s, i) => s + i.price * i.qty, 0), count: items.reduce((s, i) => s + i.qty, 0), }; } // === Theme === function useTheme() { const [theme, setTheme] = useState(() => localStorage.getItem('te2000_theme') || 'light'); useEffect(() => { document.documentElement.setAttribute('data-theme', theme); localStorage.setItem('te2000_theme', theme); }, [theme]); return [theme, setTheme]; } // === Header === function Header({ active }) { const cart = useCart(); const [theme, setTheme] = useTheme(); return (
Tour Eiffel 2000 Jean-Paul Lubliner · 365 jours
{cart.count > 0 && {cart.count}}
); } // === Footer === function Footer() { return ( ); } // === Photo card (with lazy image) === function PhotoCard({ photo, ratio = '3/4', showMeta = true }) { return (
{`Tour
{showMeta && (
J-{photo.j} {photo.dateLabel}
)}
); } // === Search overlay === function SearchOverlay() { const [open, setOpen] = useState(false); const [q, setQ] = useState(''); const inputRef = useRef(null); useEffect(() => { const onOpen = () => { setOpen(true); setTimeout(() => inputRef.current?.focus(), 60); }; const onKey = (e) => { if (e.key === 'Escape') setOpen(false); }; document.addEventListener('open-search', onOpen); document.addEventListener('keydown', onKey); return () => { document.removeEventListener('open-search', onOpen); document.removeEventListener('keydown', onKey); }; }, []); if (!open) return null; const num = parseInt(q.replace(/\D/g, ''), 10); const results = q ? PHOTOS.filter(p => (num && p.j === num) || p.dateLabel.toLowerCase().includes(q.toLowerCase()) || p.season.includes(q.toLowerCase()) || p.light.includes(q.toLowerCase()) || p.mood.includes(q.toLowerCase()) ).slice(0, 8) : PHOTOS.slice(0, 8); return (
e.target === e.currentTarget && setOpen(false)}>
setQ(e.target.value)} placeholder="Chercher un jour, une saison, une lumière, un numéro J-…" />
{results.map(p => (
J-{p.j} · {p.dateLabel}
{p.mood} · {p.light}
))}
); } Object.assign(window, { Icon, Header, Footer, PhotoCard, SearchOverlay, useCart, useTheme });