/* NEKTAR v2 — Sections (Hero, Histoire, Sélection, Livraison, Lieu, Suivre, Footer) */ const { useState, useEffect, useRef, useCallback, useContext, createContext } = React; const D = window.NEKTAR_DATA; /* ============================================================ LangContext — partagé par toute l'app ============================================================ */ const LangContext = createContext({ lang: 'fr', setLang: () => {} }); window.LangContext = LangContext; function useCopy() { const { lang } = useContext(LangContext); return D.COPY[lang] || D.COPY.fr; } window.useCopy = useCopy; /* ============================================================ HERO ============================================================ */ function HeroSection() { const c = useCopy(); const imgRef = useRef(null); const [loaded, setLoaded] = useState(false); useEffect(() => { const img = imgRef.current; if (img && img.complete && img.naturalWidth > 0) setLoaded(true); }, []); return (
setLoaded(true)} loading="eager" decoding="async" />
{c.hero.eyebrow}

{c.hero.word}

{c.hero.cta}  →
{c.hero.addrLine}
{c.hero.hoursLine}
{c.hero.discover}
); } /* ============================================================ NOTRE HISTOIRE ============================================================ */ function HistoireSection() { const c = useCopy(); return (

{c.histoire.aria}

Un verre de vin — Nektar

{c.histoire.signaturePrefix} {c.histoire.signatureWord}

); } /* ============================================================ SÉLECTION — bulles physiques ============================================================ */ function formatName(name) { const parts = name.split(' '); if (parts.length <= 1) return name; if (parts.length === 2) return parts[0] + '\n' + parts[1]; const mid = Math.ceil(parts.length / 2); return parts.slice(0, mid).join(' ') + '\n' + parts.slice(mid).join(' '); } function makeSrand(seed) { let s = seed; return () => { s = (s * 9301 + 49297) % 233280; return s / 233280; }; } function initPhysics({ count, r, w, h }) { const rand = makeSrand(99); const gap = 10; const minD = r * 2 + gap; const pad = r + 8; const ctaR = Math.min(w, h) * 0.17; const bubbles = []; const shuffled = [...D.DOMAINS].sort(() => rand() - 0.5); for (const name of shuffled) { if (bubbles.length >= count) break; for (let attempt = 0; attempt < 400; attempt++) { const cx = pad + rand() * (w - pad * 2); const cy = pad + rand() * (h - pad * 2); const dx0 = cx - w / 2, dy0 = cy - h / 2; if (dx0 * dx0 + dy0 * dy0 < (ctaR + r) * (ctaR + r)) continue; const clash = bubbles.some(b => { const dx = b.cx - cx, dy = b.cy - cy; return dx * dx + dy * dy < minD * minD; }); if (clash) continue; const angle = rand() * Math.PI * 2; const speed = 0.25 + rand() * 0.35; bubbles.push({ name, formatted: formatName(name), cx, cy, vx: Math.cos(angle) * speed, vy: Math.sin(angle) * speed, el: null, }); break; } } return bubbles; } function tickPhysics(bubbles, r, w, h) { const ctaR = Math.min(w, h) * 0.17; const cx0 = w / 2, cy0 = h / 2; for (const b of bubbles) { b.cx += b.vx; b.cy += b.vy; if (b.cx - r < 0) { b.cx = r; b.vx = Math.abs(b.vx); } if (b.cx + r > w) { b.cx = w - r; b.vx = -Math.abs(b.vx); } if (b.cy - r < 0) { b.cy = r; b.vy = Math.abs(b.vy); } if (b.cy + r > h) { b.cy = h - r; b.vy = -Math.abs(b.vy); } const dxC = b.cx - cx0, dyC = b.cy - cy0; const distC = Math.sqrt(dxC * dxC + dyC * dyC); const minC = ctaR + r + 6; if (distC < minC) { const nx = dxC / (distC || 1), ny = dyC / (distC || 1); const dot = b.vx * nx + b.vy * ny; if (dot < 0) { b.vx -= 2 * dot * nx; b.vy -= 2 * dot * ny; } b.cx += nx * (minC - distC); b.cy += ny * (minC - distC); } } for (let i = 0; i < bubbles.length; i++) { for (let j = i + 1; j < bubbles.length; j++) { const a = bubbles[i], b = bubbles[j]; const dx = b.cx - a.cx, dy = b.cy - a.cy; const dist = Math.sqrt(dx * dx + dy * dy); const minD = r * 2 + 4; if (dist < minD && dist > 0) { const nx = dx / dist, ny = dy / dist; const dvx = a.vx - b.vx, dvy = a.vy - b.vy; const dot = dvx * nx + dvy * ny; if (dot > 0) { a.vx -= dot * nx; a.vy -= dot * ny; b.vx += dot * nx; b.vy += dot * ny; } const push = (minD - dist) / 2; a.cx -= nx * push; a.cy -= ny * push; b.cx += nx * push; b.cy += ny * push; } } } } function paramsFor(w) { if (w >= 1200) return { size: 138, count: 12 }; if (w >= 768) return { size: 116, count: 8 }; return { size: 96, count: 6 }; } function SelectionSection() { const c = useCopy(); const containerRef = useRef(null); const physRef = useRef(null); const [ready, setReady] = useState(false); const [version, setVersion] = useState(0); useEffect(() => { let animId = null; function init() { if (animId) cancelAnimationFrame(animId); const el = containerRef.current; if (!el) return; const rect = el.getBoundingClientRect(); const w = rect.width; const h = rect.height; if (h < 200 || w < 200) return; const { size, count } = paramsFor(window.innerWidth); const r = size / 2; const bubbles = initPhysics({ count, r, w, h }); physRef.current = { bubbles, r, w, h }; setReady(true); setVersion(v => v + 1); const loop = () => { const ph = physRef.current; if (!ph) return; tickPhysics(ph.bubbles, ph.r, ph.w, ph.h); for (const b of ph.bubbles) { if (b.el) { b.el.style.left = (b.cx - ph.r).toFixed(1) + 'px'; b.el.style.top = (b.cy - ph.r).toFixed(1) + 'px'; } } animId = requestAnimationFrame(loop); }; animId = requestAnimationFrame(loop); } init(); let resizeTimer; const onResize = () => { clearTimeout(resizeTimer); resizeTimer = setTimeout(init, 80); }; window.addEventListener('resize', onResize); // ResizeObserver sur la section pour re-init au changement de hauteur let ro; if (window.ResizeObserver && containerRef.current) { ro = new ResizeObserver(onResize); ro.observe(containerRef.current); } return () => { if (animId) cancelAnimationFrame(animId); window.removeEventListener('resize', onResize); clearTimeout(resizeTimer); if (ro) ro.disconnect(); }; }, []); const ph = physRef.current; const bubbles = ph ? ph.bubbles : []; const r = ph ? ph.r : 69; return (

{c.selection.cta}

{ready && bubbles.map(b => (
{ b.el = el; }} aria-hidden="true" style={{ left: (b.cx - r).toFixed(1) + 'px', top: (b.cy - r).toFixed(1) + 'px', width: r * 2 + 'px', height: r * 2 + 'px', }}> {b.formatted}
))}
); } /* ============================================================ LIVRAISON — formulaire Formspree ============================================================ */ function DrawerForm({ open, onClose }) { const c = useCopy(); const lang = c.lang; const liv = c.livraison; const [sent, setSent] = useState(false); const [errors, setErrors] = useState({}); const [submitError, setSubmitError] = useState(false); const [submitting, setSubmitting] = useState(false); const [form, setForm] = useState({ nom: '', tel: '', email: '', date: '', type: '', message: '' }); const set = (k, v) => setForm(f => ({ ...f, [k]: v })); const today = new Date().toISOString().split('T')[0]; const validate = () => { const e = {}; if (!form.nom.trim()) e.nom = true; if (!form.tel.trim()) e.tel = true; if (!form.email.trim() || !/^[^@]+@[^@]+\.[^@]+$/.test(form.email)) e.email = true; if (!form.date || form.date < today) e.date = true; if (!form.message.trim()) e.message = true; return e; }; const handleSubmit = async (e) => { e.preventDefault(); const errs = validate(); if (Object.keys(errs).length) { setErrors(errs); return; } setErrors({}); setSubmitError(false); setSubmitting(true); try { const res = await fetch(D.FORMSPREE_URL, { method: 'POST', headers: { 'Content-Type': 'application/json', 'Accept': 'application/json' }, body: JSON.stringify(form), }); if (res.ok) setSent(true); else setSubmitError(true); } catch (err) { setSubmitError(true); } finally { setSubmitting(false); } }; return ( <>