Pizzima — Étape 4 : le panier et la commande
Séance 9. Objectif : la première fonctionnalité qui traverse toute l’application. Le client compose un panier, l’envoie, et l’API l’enregistre.
C’est l’étape où votre projet devient une vraie application plutôt qu’un catalogue.
1. Côté API : accepter une commande
Créez backend/data/orders.json avec pour seul contenu [], puis backend/src/orders.ts :
import { createStore } from "./store.js";
export type OrderLine = {
pizzaId: number;
quantity: number;
};
export type Order = {
id: number;
customer: string;
lines: OrderLine[];
total: number;
createdAt: string;
};
const store = createStore<Order>("orders.json");
export function findAllOrders(): Order[] { /* à vous */ }
export function findOrderById(id: number): Order | undefined { /* à vous */ }
export function createOrder(customer: string, lines: OrderLine[]): Order { /* à vous */ }
⚠️ Le total se calcule sur le serveur, à partir des prix du référentiel — jamais à partir d’un montant envoyé par le client. Sinon n’importe qui commande à 0 €. On en reparlera en séance sécurité.
Les routes :
app.post("/api/orders", ...); // 201 + la commande créée
app.get("/api/orders", ...); // la liste (pour le BDE, étape 5)
app.get("/api/orders/:id", ...); // le récapitulatif
Validez le corps comme à l’étape 2 : un customer non vide, au moins une ligne, des quantity entières et positives, et des pizzaId qui existent vraiment.
✅ Point de contrôle intermédiaire :
curl -X POST http://localhost:8080/api/orders \
-H "Content-Type: application/json" \
-d '{"customer":"Julien","lines":[{"pizzaId":1,"quantity":2},{"pizzaId":3,"quantity":1}]}'
# 201, et total = 2 × 8.50 + 11.50 = 28.50
curl -i -X POST http://localhost:8080/api/orders \
-H "Content-Type: application/json" \
-d '{"customer":"Julien","lines":[{"pizzaId":999,"quantity":1}]}'
# 400 : la pizza n'existe pas
2. Côté front : le panier
Le panier est un état qui vit dans App.tsx, parce que plusieurs composants en dépendent : les cartes ajoutent dedans, le récapitulatif le lit.
const [cart, setCart] = useState<OrderLine[]>([]);
Les opérations à écrire :
- ajouter une pizza (si elle est déjà dans le panier, incrémenter sa quantité)
- changer la quantité d’une ligne
- retirer une ligne
- vider le panier
⚠️ Sans jamais muter cart. cart.push(...) ne déclenchera aucun rendu : React compare les références (revoyez l’immutabilité et le shallow compare). Vos outils sont map(), filter() et le spread [...cart, nouvelle].
Puis l’affichage : un composant Cart qui liste les lignes avec leur quantité, le total, et un bouton « Commander ». Le total affiché se recalcule à partir de cart et des prix — ce n’est pas un état, c’est une valeur dérivée.
3. Envoyer la commande
Un champ pour le nom du client, un bouton qui appelle :
await sendJson<Order>("/api/orders", "POST", { customer, lines: cart });
sendJson est fourni dans frontend/src/api.ts.
Pendant l’envoi : le bouton est désactivé. Après : le panier est vidé et le récapitulatif de la commande s’affiche (numéro, lignes, total). En cas d’erreur : le message est affiché et le panier est conservé — on ne fait pas perdre son travail à l’utilisateur.
✅ Point de contrôle
Le parcours complet, dans le navigateur, sans toucher au terminal :
- J’ajoute deux Margherita et une Quatre fromages.
- Le panier affiche 3 pizzas et 28,50 €.
- Je change la quantité, le total suit.
- Je saisis mon nom, je commande.
- Je vois le récapitulatif avec un numéro de commande.
backend/data/orders.jsoncontient ma commande.- Je recharge la page : le catalogue est là, le panier est vide. C’est normal — on en fait un bonus.
🆘 Erreurs fréquentes
- J’ajoute au panier, rien ne s’affiche — vous avez muté le tableau (
push) au lieu d’en créer un nouveau. Le grand classique. - La quantité passe à
NaN— la valeur d’un<input>est toujours une chaîne :Number(event.target.value). - Le total est faux d’un centime — les flottants,
0.1 + 0.2 !== 0.3. Arrondissez à l’affichage avectoFixed(2). 400 Bad Requestsans savoir pourquoi — faites renvoyer par votre API quel champ est invalide, et affichez ce message côté front.- Deux commandes ont le même id — vous partez de
orders.length + 1: après une suppression, ça se recoupe. Basez-vous sur le plus grand id existant.
🎁 Bonus
- Conservez le panier dans
localStoragepour qu’il survive au rechargement. - Affichez une pastille avec le nombre d’articles à côté du titre.
- Ajoutez un
statusà la commande ("en préparation","livrée") — vous en aurez besoin à l’étape 5.