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.

5 min read
Déverrouillez votre potentiel

avec React Router 7

Vous en avez marre de...

❌ perdre du temps à chercher des informations éparpillées
❌ ne pas avoir de retour sur votre progression
Assistant IA spécialisé

Posez vos questions 24/7 à notre IA experte en React Router 7

Quiz interactifs

Validez vos acquis avec des quiz personnalisés et un feedback instantané

9 modules
72 leçons
Accès à vie
299.49
-35%

Améliorer l’expérience client

Dans cette leçon nous passons d’un simple panier à une vraie interface client :

  1. refonte de la navbar pour distinguer admin et client ;
  2. ajout des pages : commandes, détail d’une commande, profil ;
  3. factorisation des composants d’affichage des statuts ;
  4. 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.

app/components/layout/navbar.tsx
1
import { Link } from "react-router"
2
import { ShoppingCart, User } from "lucide-react"
3
import { useOptionalUser } from "~/root"
4
import { Button } from "~/components/ui/button"
5
6
export function Navbar() {
7
const user = useOptionalUser()
8
9
return (
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>
14
15
{/* 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>
21
22
{user ? (
23
<DropdownMenu>
24
<DropdownMenuTrigger asChild>
25
<Button variant="ghost" size="icon">
26
<User className="size-5" />
27
</Button>
28
</DropdownMenuTrigger>
29
30
<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">
52
Connexion
53
</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

app/routes/_public+/_customer+/orders.index.tsx
1
export async function loader({ request }) {
2
const user = await requireUser(request) // redirige si non connecté
3
const orders = await getUserOrders(user.id)
4
return { orders, user }
5
}

Composant carte réutilisable

app/components/order/order-card.tsx
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>
14
15
<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
)}
24
25
<Link
26
to={href("/orders/:orderId", { orderId: order.id })}
27
className="block pt-3 text-sm text-blue-600"
28
>
29
Voir les détails →
30
</Link>
31
</CardContent>
32
</Card>

Factorisation des badges

app/components/order/order-status-badge.tsx
1
import type { OrderStatus, PaymentStatus } from "generated/prisma/enums"
2
3
const orderMap = {
4
DRAFT: { label: "Brouillon", variant: "secondary" },
5
PENDING: { label: "En attente", variant: "secondary" },
6
PAID: { label: "Confirmée", variant: "default" },
7
FULFILLED: { label: "Livrée", variant: "default" },
8
CANCELED: { label: "Annulée", variant: "destructive" },
9
REFUNDED: { label: "Remboursée", variant: "secondary" },
10
} as const
11
12
export function OrderStatusBadge({ status }: { status: OrderStatus }) {
13
const s = orderMap[status]
14
return <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

profile.tsx {73}
1
const UpdatePasswordSchema = z
2
.object({
3
currentPassword: z.string().min(1),
4
newPassword: z
5
.string()
6
.min(8)
7
.regex(/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)/),
8
confirmPassword: z.string(),
9
})
10
.superRefine((d, ctx) => {
11
if (d.newPassword !== d.confirmPassword) {
12
ctx.addIssue({ path: ["confirmPassword"], code: "custom",
13
message: "Les mots de passe ne correspondent pas" })
14
}
15
})

Action serveur

app/server/auth.server.ts {181-209}
1
export async function updateUserPassword(
2
request: Request,
3
currentPassword: string,
4
newPassword: string,
5
) {
6
const session = await auth.api.getSession({ headers: request.headers })
7
if (!session?.user) throw new Error("User not authenticated")
8
9
await auth.api.changePassword({
10
body: { currentPassword, newPassword, revokeOtherSessions: false },
11
headers: request.headers,
12
})
13
return { 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

app/routes/_public+/_customer+/profile.tsx {234-267}
1
const sessions = await prisma.session.findMany({ where: { userId: user.id } })
2
3
/* … */
4
<details className="text-xs">
5
<summary>User Agent</summary>
6
<p className="font-mono break-all">{session.userAgent}</p>
7
</details>
8
9
<Button
10
variant="ghost"
11
size="sm"
12
onClick={() => {
13
if (confirm("Supprimer cette session ?")) {
14
const fd = new FormData()
15
fd.set("intent", "delete-session")
16
fd.set("sessionId", session.id)
17
fetcher.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 !

  1. Ajoute un lien « Factures » dans le dropdown et recycle la logique de orders.$orderId pour le PDF.
  2. Active les mails transactionnels (module Stripe + Resend) pour prévenir le client après changement de mot de passe.
  3. Vérifie le flux mobile : menu burger, listing commandes, formulaire profil.

Prochaine leçon : optimiser l’expérience admin (tableau commandes + recherche live).

Premium
Quiz interactif
Testez vos connaissances et validez votre compréhension du module avec notre quiz interactif.
1

Comprendre les concepts fondamentaux

Quelle est la principale différence entre les composants client et serveur dans React ?

Les composants client s'exécutent uniquement dans le navigateur
Les composants serveur peuvent utiliser useState
Les composants client sont plus rapides
Il n'y a aucune différence significative
2

Optimisation des performances

Quelle technique est recommandée pour éviter les rendus inutiles dans React ?

Utiliser React.memo pour les composants fonctionnels
Ajouter plus d'états locaux
Éviter d'utiliser les props
Toujours utiliser les class components
3

Architecture des données

Quel hook permet de gérer les effets de bord dans un composant React ?

useEffect
useState
useMemo
useContext
4

Gestion des erreurs

Comment implémenter la gestion des erreurs pour les requêtes API dans React ?

Utiliser try/catch avec async/await
Ignorer les erreurs
Toujours afficher un message d'erreur
Rediriger l'utilisateur
5

Déploiement et CI/CD

Quelle est la meilleure pratique pour déployer une application React en production ?

Utiliser un service CI/CD comme GitHub Actions
Copier les fichiers manuellement via FTP
Envoyer le code source complet
Ne jamais mettre à jour l'application

Débloquez ce quiz et tous les autres contenus premium en achetant ce cours