Création et protection de la route /history

Protège la route, récupère l’historique personnalisé de l’utilisateur.

5 min read

Accéder gratuitement à cette formation

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 :

  1. Créer la route /history avec l’App Router de Next.js 15
  2. Protéger l’accès via une fonction requireUser()
  3. Définir le modèle Prisma et lancer la migration
  4. Coder la logique serveur pour récupérer les items
  5. Construire un composant client optimiste avec useOptimistic

Prêt·e ? C’est parti ! 🚀

1. Déclarer la page et la protection

On commence par créer le fichier de page dans le dossier app/history.

app/history/page.tsx
1
'use server';
2
import { requireUser } from '../server/session';
3
import { getHistory } from '../server/history';
4
import { HistoryList } from './HistoryList';
5
6
export default async function History() {
7
// 1. On force la présence d'un utilisateur
8
const user = await requireUser();
9
// 2. On récupère les items
10
const initialHistoryItems = await getHistory();
11
// 3. On rend notre composant client
12
return (
13
<HistoryList
14
user={user}
15
initialHistoryItems={initialHistoryItems}
16
/>
17
);
18
}

Léger rappel : requireUser() fait un redirect('/login') sous le capot si l’utilisateur n’est pas connecté.

2. Ajouter le lien dans la navigation

Pour que la route soit accessible, on ajoute un lien dans le layout global :

app/layout.tsx
1
<nav className="flex items-center space-x-4">
2
<Link href="/">Home</Link>
3
- {/* autres liens */}
4
+ <Link
5
+ href="/history"
6
+ className="text-gray-700 hover:text-blue-600"
7
+ >
8
+ History
9
+ </Link>
10
</nav>

3. Créer le modèle Prisma et migrer

Dans prisma/schema.prisma, on définit un modèle pour stocker chaque action de l’utilisateur :

prisma/schema.prisma
1
model HistoryItem {
2
id Int @id @default(autoincrement())
3
user User @relation(fields: [userId], references: [id])
4
userId Int
5
movieId Int?
6
serieId Int?
7
createdAt DateTime @default(now())
8
}

Ensuite, lance une migration :

Terminal
1
npx prisma migrate dev --name add-history-item

Le fichier SQLite dev.db sera mis à jour automatiquement.

4. Logique serveur : récupérer l’historique

On place le code dans app/server/history.ts :

app/server/history.ts
1
'use server';
2
import { prisma } from './db';
3
import { requireUser } from './session';
4
import { getMovie, getSerie } from './movies';
5
6
export async function getHistoryQuery({ email }: { email: string }) {
7
// Tous les items pour cet email, triés par date
8
const items = await prisma.historyItem.findMany({
9
where: { user: { email } },
10
orderBy: { createdAt: 'desc' },
11
});
12
13
// On enrichit chaque item par les données du movie/serie
14
return Promise.all(
15
items.map(async item => ({
16
...item,
17
movie: item.movieId
18
? await getMovie({ movieId: item.movieId })
19
: null,
20
serie: item.serieId
21
? await getSerie({ serieId: item.serieId })
22
: null,
23
}))
24
);
25
}
26
27
export async function getHistory() {
28
// Sécurise la route
29
const user = await requireUser();
30
// Requête enrichie
31
return getHistoryQuery({ email: user.email });
32
}

5. Construire un composant client optimiste

On veut supprimer un item et mettre à jour l’UI sans rechargement.
Dans app/history/HistoryList.tsx :

app/history/HistoryList.tsx
1
'use client';
2
import { useOptimistic } from 'react';
3
import { HistoryMediaItem } from './HistoryMediaItem';
4
import type { HistoryItem } from '../server/history';
5
6
export function HistoryList({
7
user,
8
initialHistoryItems,
9
}: {
10
user: { email: string };
11
initialHistoryItems: HistoryItem[];
12
}) {
13
// Optimistic UI : on supprime localement avant la mutation
14
const [historyItems, removeOptimistic] = useOptimistic<
15
HistoryItem[],
16
number
17
>(initialHistoryItems, (list, id) =>
18
list.filter(item => item.id !== id)
19
);
20
21
// Regroupement par date (facultatif)
22
// ...
23
24
return (
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
<HistoryMediaItem
33
key={item.id}
34
item={item}
35
user={user}
36
removeHistoryItemOptimisticly={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 :

app/components/RemoveHistoryItemButton.tsx
1
'use client';
2
import { useTransition } from 'react';
3
import { removeHistoryItem } from '../server/movies';
4
5
export function RemoveHistoryItemButton({
6
historyItemId,
7
removeHistoryItemOptimisticly,
8
}: {
9
historyItemId: number;
10
removeHistoryItemOptimisticly: (id: number) => void;
11
}) {
12
const [isPending, start] = useTransition();
13
14
return (
15
<button
16
disabled={isPending}
17
onClick={() => {
18
start(() => {
19
// 1. Update UI localement
20
removeHistoryItemOptimisticly(historyItemId);
21
// 2. Lancer la mutation serveur
22
removeHistoryItem({ historyItemId });
23
});
24
}}
25
className="bg-red-600 text-white px-4 py-2 rounded"
26
>
27
{isPending ? 'Removing…' : 'Remove'}
28
</button>
29
);
30
}

Enfin, la fonction serveur removeHistoryItem :

app/server/movies.ts
1
export async function removeHistoryItem({
2
historyItemId,
3
}: {
4
historyItemId: number;
5
}) {
6
await requireUser();
7
await prisma.historyItem.delete({
8
where: { id: historyItemId },
9
});
10
revalidatePath('/history');
11
}

Exercices

  1. Redirection
    Modifie requireUser() pour rediriger vers /login avec un message flash
    plutôt que de lancer une erreur.

  2. Groupement par date
    Dans HistoryList, affiche les items regroupés par année et jour
    (resp. 2024 → « 12 April », …).

  3. 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.