Protège la route, récupère l’historique personnalisé de l’utilisateur.
Renseigne ton email pour débloquer immédiatement cette formation gratuite.
Dans ce chapitre, tu vas découvrir comment mettre en place une page dédiée à l’historique de visionnage et la sécuriser pour les utilisateurs connectés seulement. On va :
/history
avec l’App Router de Next.js 15requireUser()
useOptimistic
Prêt·e ? C’est parti ! 🚀
On commence par créer le fichier de page dans le dossier app/history
.
1'use server';2import { requireUser } from '../server/session';3import { getHistory } from '../server/history';4import { HistoryList } from './HistoryList';56export default async function History() {7// 1. On force la présence d'un utilisateur8const user = await requireUser();9// 2. On récupère les items10const initialHistoryItems = await getHistory();11// 3. On rend notre composant client12return (13<HistoryList14user={user}15initialHistoryItems={initialHistoryItems}16/>17);18}
Léger rappel :
requireUser()
fait unredirect('/login')
sous le capot si l’utilisateur n’est pas connecté.
Pour que la route soit accessible, on ajoute un lien dans le layout global :
1<nav className="flex items-center space-x-4">2<Link href="/">Home</Link>3- {/* autres liens */}4+ <Link5+ href="/history"6+ className="text-gray-700 hover:text-blue-600"7+ >8+ History9+ </Link>10</nav>
Mets à jour ton fichier layout.tsx
pour voir le lien apparaître immédiatement.
Dans prisma/schema.prisma
, on définit un modèle pour stocker chaque action de l’utilisateur :
1model HistoryItem {2id Int @id @default(autoincrement())3user User @relation(fields: [userId], references: [id])4userId Int5movieId Int?6serieId Int?7createdAt DateTime @default(now())8}
Ensuite, lance une migration :
1npx prisma migrate dev --name add-history-item
Le fichier SQLite
dev.db
sera mis à jour automatiquement.
On place le code dans app/server/history.ts
:
1'use server';2import { prisma } from './db';3import { requireUser } from './session';4import { getMovie, getSerie } from './movies';56export async function getHistoryQuery({ email }: { email: string }) {7// Tous les items pour cet email, triés par date8const items = await prisma.historyItem.findMany({9where: { user: { email } },10orderBy: { createdAt: 'desc' },11});1213// On enrichit chaque item par les données du movie/serie14return Promise.all(15items.map(async item => ({16...item,17movie: item.movieId18? await getMovie({ movieId: item.movieId })19: null,20serie: item.serieId21? await getSerie({ serieId: item.serieId })22: null,23}))24);25}2627export async function getHistory() {28// Sécurise la route29const user = await requireUser();30// Requête enrichie31return getHistoryQuery({ email: user.email });32}
Appelle getHistoryQuery
uniquement après avoir validé l’authentification,
sinon tu pourrais exposer des données sensibles.
On veut supprimer un item et mettre à jour l’UI sans rechargement.
Dans app/history/HistoryList.tsx
:
1'use client';2import { useOptimistic } from 'react';3import { HistoryMediaItem } from './HistoryMediaItem';4import type { HistoryItem } from '../server/history';56export function HistoryList({7user,8initialHistoryItems,9}: {10user: { email: string };11initialHistoryItems: HistoryItem[];12}) {13// Optimistic UI : on supprime localement avant la mutation14const [historyItems, removeOptimistic] = useOptimistic<15HistoryItem[],16number17>(initialHistoryItems, (list, id) =>18list.filter(item => item.id !== id)19);2021// Regroupement par date (facultatif)22// ...2324return (25<div className="p-8">26<h1 className="text-3xl mb-6">Watch history</h1>27{historyItems.length === 0 ? (28<p>No history yet</p>29) : (30<ul className="space-y-4">31{historyItems.map(item => (32<HistoryMediaItem33key={item.id}34item={item}35user={user}36removeHistoryItemOptimisticly={removeOptimistic}37/>38))}39</ul>40)}41</div>42);43}
Le composant HistoryMediaItem
importe un bouton de suppression qui déclenche la mutation serveur et la mise à jour optimiste :
1'use client';2import { useTransition } from 'react';3import { removeHistoryItem } from '../server/movies';45export function RemoveHistoryItemButton({6historyItemId,7removeHistoryItemOptimisticly,8}: {9historyItemId: number;10removeHistoryItemOptimisticly: (id: number) => void;11}) {12const [isPending, start] = useTransition();1314return (15<button16disabled={isPending}17onClick={() => {18start(() => {19// 1. Update UI localement20removeHistoryItemOptimisticly(historyItemId);21// 2. Lancer la mutation serveur22removeHistoryItem({ historyItemId });23});24}}25className="bg-red-600 text-white px-4 py-2 rounded"26>27{isPending ? 'Removing…' : 'Remove'}28</button>29);30}
Enfin, la fonction serveur removeHistoryItem
:
1export async function removeHistoryItem({2historyItemId,3}: {4historyItemId: number;5}) {6await requireUser();7await prisma.historyItem.delete({8where: { id: historyItemId },9});10revalidatePath('/history');11}
Redirection
Modifie requireUser()
pour rediriger vers /login
avec un message flash
plutôt que de lancer une erreur.
Groupement par date
Dans HistoryList
, affiche les items regroupés par année et jour
(resp. 2024 → « 12 April », …).
Ajout optimiste
Crée un bouton « Add to history » dans MovieCard
qui ajoute
un HistoryItem
et met à jour la liste de manière optimiste.
Bon travail ! 🎉 Continue sur ta lancée et peaufine ton UI pour la rendre ultra-agréable.