function Compose() { const { m, draft, setSize, add, removeAt, randomFill, clearDraft, commitDraft, avail, open } = useShop(); const { size, items } = draft; const left = size - items.length; const boxW = m ? 330 : (size >= 10 ? 600 : size === 5 ? 560 : 520); return (
Compose ta box choisis la taille, puis sélectionne tes cookies
{SIZES.map(n => ( ))}
{size > 8 ? (
{size} cookies
) : null}
add(id, 1, true)} />
Les parfums {items.length}/{size}
{FLAVORS.map(f => { const n = avail(f.id); const dis = !n || !open || !left; return ( ); })}
{boxLabel(size)}{euro(sizePrice(size))}
{items.length ? summarize(items).join(' · ') : 'Ta box est vide.'} Clique sur un cookie dans la box pour le retirer
); } function ComposeBar() { const { m, draft, open, commitDraft, cart, pieces, go, removeBox, total } = useShop(); const { size, items } = draft; const left = size - items.length; const full = open && left === 0; return (
{cart.length && m ? ( ) : cart.length ? (
Dans ton panier {cart.map(b => ( = 8 ? 92 : 66} /> {boxLabel(b.size)} ))} {pieces} cookies · {euro(total)}
) : null}
{boxLabel(size)} · {full ? complète ✓ : `${items.length}/${size}`} {euro(sizePrice(size))}
{full ? (
) : cart.length && !items.length ? : }
); } function Rule({ ok, children }) { return
{ok ? '✓' : '–'}{children}
; } function Cart() { const { m, cart, go, editBox, removeBox, pieces, open, total } = useShop(); if (!cart.length) return (
Ta box est vide. Ça se règle en deux minutes.
); return (
Panier
{cart.map(b => (
= 8 ? 150 : 120) : 240} />
{boxLabel(b.size)}{euro(boxPrice(b))}
{summarize(b.items).join(' · ')}
))}
Récap
{pieces} cookies{euro(total)}
= CFG.minPieces}>Minimum {CFG.minPieces} cookies = CFG.deliveryMin}>{pieces >= CFG.deliveryMin ? 'Livraison disponible*' : 'Livraison disponible dès ' + CFG.deliveryMin + ' pièces*'} Paiement à la remise
* Livraison autour de {CFG.delivery.label} : {dlvText()}, calculés selon ton adresse. En dessous de {CFG.deliveryMin} pièces, retrait uniquement.
Total{euro(total)}
); } const dlvText = () => { const t = (CFG.delivery && CFG.delivery.tiers) || []; return t.map((x, i) => (x.fee > 0 ? euroShort(x.fee) : 'gratuit') + ' jusqu’à ' + x.km + ' km').join(' · '); }; function Field({ label, error, children }) { return ; } function Checkout() { const { m, cart, pieces, go, placeOrder, total } = useShop(); const canDeliver = pieces >= CFG.deliveryMin; const [mode, setMode] = useState('pickup'); const [day, setDay] = useState(DAYS[0].id); const slot = SLOTS[mode]; const [f, setF] = useState({ first: '', last: '', email: '', phone: '', street: '', zip: '', city: '', note: '' }); const [q, setQ] = useState({ st: 'idle', fee: 0, km: 0, err: '' }); const [ok, setOk] = useState(false); const [tried, setTried] = useState(false); const [busy, setBusy] = useState(false); const [srvErr, setSrvErr] = useState(''); const addrKey = mode === 'delivery' && f.street.trim() && /^\d{4}$/.test(f.zip.trim()) && f.city.trim() ? [f.street, f.zip, f.city].map(x => x.trim()).join('|') : ''; useEffect(() => { if (!addrKey) { setQ({ st: 'idle', fee: 0, km: 0, err: '' }); return; } setQ({ st: 'load', fee: 0, km: 0, err: '' }); let dead = false; const t = setTimeout(() => { fetch(CFG.delivery.api, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ street: f.street, zip: f.zip, city: f.city }) }) .then(r => r.json().then(j => ({ r, j }))) .then(({ r, j }) => { if (dead) return; setQ(r.ok ? { st: 'ok', fee: j.fee, km: j.km, err: '' } : { st: 'err', fee: 0, km: 0, err: j.message || 'Adresse non valide.' }); }) .catch(() => { if (!dead) setQ({ st: 'err', fee: 0, km: 0, err: 'Connexion impossible, réessaie.' }); }); }, 700); return () => { dead = true; clearTimeout(t); }; }, [addrKey]); if (!cart.length) { setTimeout(() => go('cart'), 0); return null; } const set = k => e => setF({ ...f, [k]: e.target.value }); const fee = mode === 'delivery' && q.st === 'ok' ? q.fee : 0; const grand = total + fee; const errs = {}; if (!f.first.trim()) errs.first = 'Ton prénom'; if (!f.last.trim()) errs.last = 'Ton nom'; if (!/^\S+@\S+\.\S+$/.test(f.email)) errs.email = 'Email invalide'; if (f.phone.replace(/\D/g, '').length < 9) errs.phone = 'Numéro invalide'; if (mode === 'delivery') { if (!f.street.trim()) errs.street = 'Adresse requise'; if (!/^\d{4}$/.test(f.zip.trim())) errs.zip = 'Code postal à 4 chiffres'; if (!f.city.trim()) errs.city = 'Commune requise'; if (q.st === 'err') errs.street = q.err; else if (q.st !== 'ok' && !errs.street && !errs.zip && !errs.city) errs.street = 'Calcul de la livraison en cours…'; } if (!ok) errs.ok = 'À cocher'; const E = k => (tried ? errs[k] : null); const submit = () => { setTried(true); setSrvErr(''); if (busy || Object.keys(errs).length) return; setBusy(true); placeOrder({ mode, day, slot, ...f }).then(r => { setBusy(false); if (!r.ok) setSrvErr(r.error); }); }; const H = ({ n, t }) =>
{n}{t}
; return (
Presque fini.
{DAYS.map(d => )}
{mode === 'pickup' ? 'Retrait · ' + SLOTS.pickup : 'Livraison · ' + SLOTS.delivery} L’heure exacte te sera confirmée par mail.
{mode === 'delivery' ? <>
{q.st === 'load' ? 'Calcul des frais de livraison…' : q.st === 'ok' ? `${q.km.toString().replace('.', ',')} km de ${CFG.delivery.label} · livraison ${q.fee > 0 ? euro(q.fee) : 'gratuite'}` : 'Entre ton adresse complète pour calculer la livraison.'}
: null}
Rien à payer en ligneTu règles {euro(grand)}{mode === 'delivery' ? ' (livraison incluse) à la livraison' : ' au retrait'} de ta commande.
Ta commande {cart.map(b =>
{boxLabel(b.size)}{euro(boxPrice(b))}
{summarize(b.items).join(' · ')}
)}
{mode === 'pickup' ? 'Retrait' : 'Livraison'} · {DAYS.find(d => d.id === day).label} · {slot} {mode === 'delivery' ?
Frais de livraison{q.st === 'ok' ? (q.fee > 0 ? euro(q.fee) : 'Gratuite') : '—'}
: null}
À payer à la remise{euro(grand)}
{srvErr ? {srvErr} : null} {mode === 'delivery' ? Frais de livraison calculés selon la distance depuis {CFG.delivery.label} : {dlvText()}. : null} {tried && Object.keys(errs).length ? Il manque quelques infos. : null}
); } function Confirm() { const { m, order, go } = useShop(); if (!order) { setTimeout(() => go('home'), 0); return null; } const d = DAYS.find(x => x.id === order.day); const when = `${d.label} ${d.date} · ${order.slot}`; const dash = ; return (
{`Merci, ${order.first}.`} Ta pré-commande est enregistrée.
CRAVE.
COOKIES & PASTRIES · BRUXELLES
{dash}
PRÉ-COMMANDE{order.no}
{order.mode === 'pickup' ? 'RETRAIT' : 'LIVRAISON'}{d.label.toUpperCase()}
CRÉNEAU{order.slot}
HEURE EXACTEPAR MAIL
{dash} {order.boxes.map(b =>
{boxLabel(b.size).toUpperCase()}{euro(boxPrice(b))}
{summarize(b.items).map(s =>
{s.toUpperCase()}
)}
)} {order.mode === 'delivery' ?
LIVRAISON{order.fee > 0 ? euro(order.fee) : 'GRATUITE'}
: null} {dash}
À PAYER À LA REMISE{euro(order.grand)}
TOTALWORTH IT.
{dash}
Mail automatique envoyé
De CRAVE.À {order.email}Objet Ta pré-commande {order.no} est confirmée
CRAVE. Hello {order.first}, C’est noté : {order.pieces} cookies pour {order.mode === 'pickup' ? 'un retrait' : 'une livraison'} le {when} (heure exacte confirmée dans ce mail). {order.mode === 'pickup' ? 'L’adresse du point de retrait arrive dans ce mail.' : `On livre au ${order.street}, ${order.zip} ${order.city}.`} Tu paies {euro(order.grand)} à la remise{order.mode === 'delivery' ? ' (livraison incluse)' : ''}. Une question ? Réponds à ce mail ou DM {CFG.instagram}. See you this weekend.
); } Object.assign(window, { Compose, Cart, Checkout, Confirm });