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 => (
setSize(n)}>
{n} {euroShort(sizePrice(n))}
))}
8 ? ' on' : '')} onClick={() => setSize(size > 8 ? size : 10)}>
{size > 8 ? size : '+'} {size > 8 ? euroShort(sizePrice(size)) : 'plus'}
{size > 8 ? (
setSize(Math.max(9, size - 1))} aria-label="Moins">−
{size} cookies
setSize(Math.min(MAX_BOX, size + 1))} aria-label="Plus">+
) : null}
add(id, 1, true)} />
Au hasard
Vider
Les parfums
{items.length}/{size}
{FLAVORS.map(f => {
const n = avail(f.id); const dis = !n || !open || !left;
return (
e.dataTransfer.setData('text/plain', f.id)} onClick={() => add(f.id, 1, true)}>
{f.name}
{!m ? {f.desc} : null}
);
})}
{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 ? (
go('cart')} style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', minHeight: 32, fontSize: 10, color: '#E2C07A', borderBottom: '1px solid rgba(244,240,231,.1)', paddingBottom: 8 }}>{cart.length} box dans ton panier · {euro(total)} Voir →
) : cart.length ? (
Dans ton panier
{cart.map(b => (
= 8 ? 92 : 66} />
{boxLabel(b.size)}
removeBox(b.id)} aria-label="Retirer">×
))}
{pieces} cookies · {euro(total)}
) : null}
{boxLabel(size)} · {full ? complète ✓ : `${items.length}/${size}`}
{euro(sizePrice(size))}
{full ? (
commitDraft(false)}>Ajouter au panier →
commitDraft(true)}>+ Ajouter et composer une autre box
) : cart.length && !items.length ?
go('cart')}>Voir le panier · {euro(total)} →
:
{!open ? 'Commandes fermées' : `Encore ${left} cookie${left > 1 ? 's' : ''}`} }
);
}
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.
go('compose')}>Compose ta box
);
return (
Panier
{cart.map(b => (
= 8 ? 150 : 120) : 240} />
{boxLabel(b.size)} {euro(boxPrice(b))}
{summarize(b.items).join(' · ')}
editBox(b.id)}>Modifier removeBox(b.id)}>Supprimer
))}
go('compose')}>+ Ajouter une autre box
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)}
go('checkout')}>Passer commande
);
}
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 {label} {children}{error ? {error} : null} ;
}
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.
setMode('pickup')}>Retrait Point de retrait communiqué par mail
setMode('delivery')}>Livraison {canDeliver ? 'Autour de ' + CFG.delivery.label + ' · ' + dlvText() : `Disponible dès ${CFG.deliveryMin} pièces* (tu en as ${pieces})`}
{DAYS.map(d => setDay(d.id)}>{d.label} {d.date} )}
{mode === 'pickup' ? 'Retrait · ' + SLOTS.pickup : 'Livraison · ' + SLOTS.delivery}
L’heure exacte te sera confirmée par mail.
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)}
{busy ? 'Envoi en cours…' : 'Confirmer ma pré-commande'}
{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.
COOKIES & PASTRIES · BRUXELLES
{dash}
PRÉ-COMMANDE {order.no}
{order.mode === 'pickup' ? 'RETRAIT' : 'LIVRAISON'} {d.label.toUpperCase()}
CRÉNEAU {order.slot}
HEURE EXACTE PAR 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)}
TOTAL WORTH IT.
{dash}
Mail automatique envoyé
De CRAVE.À {order.email}Objet Ta pré-commande {order.no} est confirmée
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.
go('home')}>Retour à l’accueil go('menu')}>Le menu
);
}
Object.assign(window, { Compose, Cart, Checkout, Confirm });