Améliorer l’expérience client
Crée une expérience client complète : navbar role-aware, panier, historique de commandes, profil sécurisé et sessions gérées.
Améliorer l’expérience client
Dans cette leçon nous passons d’un simple panier à une vraie interface client :
- refonte de la navbar pour distinguer admin et client ;
- ajout des pages : commandes, détail d’une commande, profil ;
- factorisation des composants d’affichage des statuts ;
- mise à jour du profil (nom + mot de passe) et gestion des sessions.
Chaque étape s’appuie sur les fichiers créés dans la vidéo, sans rien ajouter de nouveau.
Refonte de la navbar
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 ?
- Un seul composant gère le fond blanc et la version mobile.
- Le bouton panier reste accessible à tout moment – aligné via
flex justify-between. - Pour un admin, on garde l’accès au back-office.
Liste des commandes client
Route dynamique
1export async function loader({ request }) {2const user = await requireUser(request) // redirige si non connecté3const orders = await getUserOrders(user.id)4return { orders, user }5}
Composant carte réutilisable
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>
Factorisation des badges
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.
Profil client : mise à jour sécurisée
Validation Zod
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})
Action serveur
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 ?
- On délègue le hash à Better Auth.
- L’action retourne un message de succès, capturé par Conform pour afficher un toast.
Gestion des sessions utilisateur
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/>
- Un client supprime sa session courante → déconnexion immédiate.
- Un admin peut lister les sessions d’un utilisateur (PRISMA + rôle).
Résultat final
- Navbar responsive, role-aware ;
- Historique de commandes complet, badge lisible ;
- Profil éditable (nom + mot de passe) ;
- Gestion fine des sessions ;
- Aucune duplication de mapping ou de type.
À toi de jouer !
- Ajoute un lien « Factures » dans le dropdown et recycle la logique de
orders.$orderIdpour le PDF. - Active les mails transactionnels (module Stripe + Resend) pour prévenir le client après changement de mot de passe.
- Vérifie le flux mobile : menu burger, listing commandes, formulaire profil.
Prochaine leçon : optimiser l’expérience admin (tableau commandes + recherche live).
Comprendre les concepts fondamentaux
Quelle est la principale différence entre les composants client et serveur dans React ?
Optimisation des performances
Quelle technique est recommandée pour éviter les rendus inutiles dans React ?
Architecture des données
Quel hook permet de gérer les effets de bord dans un composant React ?