Crée une expérience client complète : navbar role-aware, panier, historique de commandes, profil sécurisé et sessions gérées.
avec React Router 7
Posez vos questions 24/7 à notre IA experte en React Router 7
Validez vos acquis avec des quiz personnalisés et un feedback instantané
Dans cette leçon nous passons d’un simple panier à une vraie interface client :
Chaque étape s’appuie sur les fichiers créés dans la vidéo, sans rien ajouter de nouveau.
Objectif : – aucune icône « Dashboard » pour un client simple, – un dropdown compact avec : Profil, Commandes, Factures, Déconnexion, – badge panier aligné à droite.
1import { Link } from "react-router"2import { ShoppingCart, User } from "lucide-react"3import { useOptionalUser } from "~/root"4import { Button } from "~/components/ui/button"56export function Navbar() {7const user = useOptionalUser()89return (10<nav className="border-b bg-white">11<div className="container flex h-16 items-center justify-between">12{/* logo */}13<Link to="/">NEXUS</Link>1415{/* menu client */}16<div className="flex items-center gap-4">17<Link to="/cart" className="relative">18<ShoppingCart className="size-5" />19{/* badge quantité ajouté via context du panier */}20</Link>2122{user ? (23<DropdownMenu>24<DropdownMenuTrigger asChild>25<Button variant="ghost" size="icon">26<User className="size-5" />27</Button>28</DropdownMenuTrigger>2930<DropdownMenuContent align="end">31{user.role === "administrator" && (32<DropdownMenuItem asChild>33<Link to="/admin">Dashboard admin</Link>34</DropdownMenuItem>35)}36<DropdownMenuItem asChild>37<Link to="/profile">Mon profil</Link>38</DropdownMenuItem>39<DropdownMenuItem asChild>40<Link to="/orders">Mes commandes</Link>41</DropdownMenuItem>42<DropdownMenuSeparator />43<DropdownMenuItem asChild>44<form action="/logout" method="post">45<button>Se déconnecter</button>46</form>47</DropdownMenuItem>48</DropdownMenuContent>49</DropdownMenu>50) : (51<Link to="/login" className="text-sm">52Connexion53</Link>54)}55</div>56</div>57</nav>58)59}
Pourquoi ?
flex justify-between
.1export async function loader({ request }) {2const user = await requireUser(request) // redirige si non connecté3const orders = await getUserOrders(user.id)4return { orders, user }5}
1<Card>2<CardHeader className="flex justify-between">3<div>4<CardTitle>Commande #{order.id.slice(-8)}</CardTitle>5<p className="text-sm text-muted-foreground">6{new Date(order.createdAt).toLocaleDateString("fr-FR")}7</p>8</div>9<div className="flex gap-2">10<OrderStatusBadge status={order.orderStatus} />11<PaymentStatusBadge status={order.paymentStatus} />12</div>13</CardHeader>1415<CardContent>16{order.items.slice(0, 3).map((it) => (17<OrderItemComponent key={it.id} item={it} />18))}19{order.items.length > 3 && (20<p className="text-xs text-gray-500 text-center">21…et {order.items.length - 3} autre(s)22</p>23)}2425<Link26to={href("/orders/:orderId", { orderId: order.id })}27className="block pt-3 text-sm text-blue-600"28>29Voir les détails →30</Link>31</CardContent>32</Card>
1import type { OrderStatus, PaymentStatus } from "generated/prisma/enums"23const orderMap = {4DRAFT: { label: "Brouillon", variant: "secondary" },5PENDING: { label: "En attente", variant: "secondary" },6PAID: { label: "Confirmée", variant: "default" },7FULFILLED: { label: "Livrée", variant: "default" },8CANCELED: { label: "Annulée", variant: "destructive" },9REFUNDED: { label: "Remboursée", variant: "secondary" },10} as const1112export function OrderStatusBadge({ status }: { status: OrderStatus }) {13const s = orderMap[status]14return <Badge variant={s.variant}>{s.label}</Badge>15}
Les mappings sont tipés par l’enum Prisma → plus de as any
.
1const UpdatePasswordSchema = z2.object({3currentPassword: z.string().min(1),4newPassword: z5.string()6.min(8)7.regex(/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)/),8confirmPassword: z.string(),9})10.superRefine((d, ctx) => {11if (d.newPassword !== d.confirmPassword) {12ctx.addIssue({ path: ["confirmPassword"], code: "custom",13message: "Les mots de passe ne correspondent pas" })14}15})
1export async function updateUserPassword(2request: Request,3currentPassword: string,4newPassword: string,5) {6const session = await auth.api.getSession({ headers: request.headers })7if (!session?.user) throw new Error("User not authenticated")89await auth.api.changePassword({10body: { currentPassword, newPassword, revokeOtherSessions: false },11headers: request.headers,12})13return { success: true }14}
Pourquoi ?
1const sessions = await prisma.session.findMany({ where: { userId: user.id } })23/* … */4<details className="text-xs">5<summary>User Agent</summary>6<p className="font-mono break-all">{session.userAgent}</p>7</details>89<Button10variant="ghost"11size="sm"12onClick={() => {13if (confirm("Supprimer cette session ?")) {14const fd = new FormData()15fd.set("intent", "delete-session")16fd.set("sessionId", session.id)17fetcher.submit(fd, { method: "POST" })18}19}}20/>
orders.$orderId
pour le PDF.Prochaine leçon : optimiser l’expérience admin (tableau commandes + recherche live).
Quelle est la principale différence entre les composants client et serveur dans React ?
Quelle technique est recommandée pour éviter les rendus inutiles dans React ?
Quel hook permet de gérer les effets de bord dans un composant React ?
Comment implémenter la gestion des erreurs pour les requêtes API dans React ?
Quelle est la meilleure pratique pour déployer une application React en production ?