Leader Board
Objectif : Afficher un classement des meilleurs scores des joueurs, à partir des données stockées dans la table
joueur.

Pourquoi afficher un classement ?
Afficher un classement permet de :
- Valoriser les meilleurs joueurs
- Créer un esprit de compétition
- Donner envie de rejouer pour améliorer son score
- Visualiser les performances de tous les participants
Où sont stockés les scores ?
Dans ce TP, les scores sont enregistrés dans la table joueur, avec les colonnes suivantes :
| Colonne | Type | Description |
|---|---|---|
nom | text | Le nom du joueur |
meilleur_score | bigint | Le meilleur score obtenu |
meilleur_temps | bigint | Le temps associé au meilleur score |
date_meilleur_score | date | La date du meilleur score |
Nous allons utiliser ces données pour afficher un tableau de classement des joueurs.
Étapes du TP
Objectif
Créer un composant React qui affiche le classement des joueurs, trié par score décroissant.
Étape 1 – Créer un composant Classement.tsx
Dans le dossier components, créez un fichier Classement.tsx.
Ce composant va :
- Récupérer les joueurs depuis Supabase
- Trier par score décroissant
- Afficher les résultats dans un tableau
Solution
// components/Classement.tsx
"use client";
import { useEffect, useState } from "react";
import { supabase } from "../lib/supabaseClient";
type Joueur = {
id: number;
pseudo: string;
meilleur_score: number;
meilleur_temps: number;
date_meilleur_score: string;
};
export default function Classement() {
const [joueurs, setJoueurs] = useState<Joueur[]>([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
async function fetchClassement() {
const { data, error } = await supabase
.from("joueur")
.select("id, pseudo, meilleur_score, meilleur_temps, date_meilleur_score")
.order("meilleur_score", { ascending: false })
.limit(10);
if (error) {
console.log("Erreur récupération classement :", error);
} else {
setJoueurs(data || []);
}
setLoading(false);
}
fetchClassement();
}, []);
if (loading) {
return <p>Chargement du classement...</p>;
}
return (
<div className="max-w-3xl mx-auto mt-10">
<h2 className="text-3xl font-bold mb-6 text-center">Classement</h2>
<table className="w-full border-collapse border border-gray-300">
<thead>
<tr className="bg-gray-100">
<th className="border border-gray-300 px-4 py-2 text-left">#</th>
<th className="border border-gray-300 px-4 py-2 text-left">Pseudo</th>
<th className="border border-gray-300 px-4 py-2 text-left">Score</th>
<th className="border border-gray-300 px-4 py-2 text-left">Temps</th>
<th className="border border-gray-300 px-4 py-2 text-left">Date</th>
</tr>
</thead>
<tbody>
{joueurs.map((joueur, index) => (
<tr key={joueur.id} className="hover:bg-gray-50">
<td className="border border-gray-300 px-4 py-2">{index + 1}</td>
<td className="border border-gray-300 px-4 py-2">{joueur.pseudo}</td>
<td className="border border-gray-300 px-4 py-2">{joueur.meilleur_score}</td>
<td className="border border-gray-300 px-4 py-2">{joueur.meilleur_temps ?? "-"}</td>
<td className="border border-gray-300 px-4 py-2">{joueur.date_meilleur_score ?? "-"}</td>
</tr>
))}
</tbody>
</table>
</div>
);
}

Étape 2 – Afficher le composant dans la page principale
Dans app/page.tsx ou une autre page de votre choix, importez et affichez le composant Classement.
Solution
import Classement from "@/components/Classement";
// Dans le JSX :
<Classement />
Placez-le par exemple sous Merci pour votre participation !, à la fin du quiz.
...
</Card>
<div className="mt-8">
<p className="text-lg mb-4">
Merci {joueurNom} pour votre participation !
</p>
</div>
<Classement />
</div>

Étape 3 – Ajouter un style
Nous allons améliorer l’apparence du tableau de classement avec un style professionnel, responsive, et des pictogrammes pour les 3 premiers joueurs (une médaille d’or, d’argent ou de bronze).
Pour les pictogrammes, il faut installer la bibliothèque react-icons :
npm install react-icons
Voici le composant Classement.tsx avec tous les styles ajoutés :
// components/Classement.tsx
"use client";
import { useEffect, useState } from "react";
import { supabase } from "@/lib/supabaseClient";
import { FaMedal } from "react-icons/fa"; // npm install react-icons
type Joueur = {
id: number;
pseudo: string;
meilleur_score: number;
meilleur_temps: number;
date_meilleur_score: string;
};
export default function Classement() {
const [joueurs, setJoueurs] = useState<Joueur[]>([]);
const [loading, setLoading] = useState(true);
const [refreshing, setRefreshing] = useState(false);
const fetchClassement = async () => {
setRefreshing(true);
const { data, error } = await supabase
.from("joueur")
.select("id, pseudo, meilleur_score, meilleur_temps, date_meilleur_score")
.order("meilleur_score", { ascending: false })
.limit(10);
if (error) {
console.error("Erreur récupération classement :", error);
} else {
setJoueurs(data || []);
}
setLoading(false);
setRefreshing(false);
};
useEffect(() => {
fetchClassement();
}, []);
const getMedal = (index: number) => {
const medals = [
{ color: "text-yellow-500", title: "Or" },
{ color: "text-gray-400", title: "Argent" },
{ color: "text-orange-500", title: "Bronze" },
];
if (index < 3) {
return (
<FaMedal
className={`w-5 h-5 ${medals[index].color}`}
title={`Médaille ${medals[index].title}`}
/>
);
}
return <span>{index + 1}</span>;
};
if (loading) {
return <p className="text-center mt-10 text-gray-500">Chargement du classement...</p>;
}
return (
<div className="mx-auto max-w-4xl px-4 sm:px-6 lg:px-8 mt-12">
<div className="flex flex-col sm:flex-row items-center justify-between mb-6 gap-4">
<h2 className="text-3xl font-bold text-gray-800">Classement</h2>
</div>
<div className="overflow-x-auto shadow rounded-lg">
<table className="min-w-full bg-white border border-gray-200">
<thead className="bg-gray-100">
<tr>
<th className="px-4 py-3 text-left text-sm font-semibold text-gray-700">Rang</th>
<th className="px-4 py-3 text-left text-sm font-semibold text-gray-700">Pseudo</th>
<th className="px-4 py-3 text-left text-sm font-semibold text-gray-700">Score</th>
<th className="px-4 py-3 text-left text-sm font-semibold text-gray-700">Temps (s)</th>
<th className="px-4 py-3 text-left text-sm font-semibold text-gray-700">Date</th>
</tr>
</thead>
<tbody>
{joueurs.map((joueur, index) => (
<tr
key={joueur.id}
className="border-t border-gray-200 hover:bg-gray-50 transition"
>
<td className="px-4 py-3 text-sm text-gray-900 font-medium">
{getMedal(index)}
</td>
<td className="px-4 py-3 text-sm text-gray-800">{joueur.pseudo}</td>
<td className="px-4 py-3 text-sm text-gray-800">{joueur.meilleur_score}</td>
<td className="px-4 py-3 text-sm text-gray-800">
{joueur.meilleur_temps ?? "-"}
</td>
<td className="px-4 py-3 text-sm text-gray-800">
{joueur.date_meilleur_score
? new Date(joueur.date_meilleur_score).toLocaleDateString("fr-FR")
: "-"}
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
);
}

Étape 4 – Trier en cas d’égalité
Par défaut, les joueurs sont triés par meilleur_score décroissant.
En cas d’égalité, on peut trier par meilleur_temps croissant (le plus rapide en premier).
Solution (modification dans la requête Supabase)
const { data, error } = await supabase
.from("joueur")
.select("id, pseudo, meilleur_score, meilleur_temps, date_meilleur_score")
.order("meilleur_score", { ascending: false })
.order("meilleur_temps", { ascending: true }) // tri secondaire
.limit(10);

Étape 5 – N’afficher que les joueurs ayant un score
Certains joueurs peuvent ne pas avoir encore joué.
On peut filtrer pour n’afficher que ceux dont meilleur_score est supérieur à 0.
Solution
const { data, error } = await supabase
.from("joueur")
.select("id, pseudo, meilleur_score, meilleur_temps, date_meilleur_score")
.gt("meilleur_score", 0) // score > 0
.order("meilleur_score", { ascending: false })
.order("meilleur_temps", { ascending: true })
.limit(10);

Étape 6 – Ajouter un bouton "Rafraîchir"
Nous allons :
- Extraire la fonction
fetchClassementpour pouvoir la réutiliser - Ajouter un bouton qui déclenche cette fonction lorsqu'on clique dessus
- Afficher un état de chargement pendant la mise à jour
Voici le composant complet avec le bouton "Rafraîchir" intégré :
// components/Classement.tsx
"use client";
import { useEffect, useState } from "react";
import { supabase } from "@/lib/supabaseClient";
import { FaMedal } from "react-icons/fa"; // npm install react-icons
type Joueur = {
id: number;
pseudo: string;
meilleur_score: number;
meilleur_temps: number;
date_meilleur_score: string;
};
export default function Classement() {
const [joueurs, setJoueurs] = useState<Joueur[]>([]);
const [loading, setLoading] = useState(true);
const [refreshing, setRefreshing] = useState(false);
const fetchClassement = async () => {
setRefreshing(true);
const { data, error } = await supabase
.from("joueur")
.select("id, pseudo, meilleur_score, meilleur_temps, date_meilleur_score")
.gt("meilleur_score", 0) // score > 0
.order("meilleur_score", { ascending: false })
.order("meilleur_temps", { ascending: true })
.limit(10);
if (error) {
console.error("Erreur récupération classement :", error);
} else {
setJoueurs(data || []);
}
setLoading(false);
setRefreshing(false);
};
useEffect(() => {
fetchClassement();
}, []);
const getMedal = (index: number) => {
const medals = [
{ color: "text-yellow-500", title: "Or" },
{ color: "text-gray-400", title: "Argent" },
{ color: "text-orange-500", title: "Bronze" },
];
if (index < 3) {
return (
<FaMedal
className={`w-5 h-5 ${medals[index].color}`}
title={`Médaille ${medals[index].title}`}
/>
);
}
return <span>{index + 1}</span>;
};
if (loading) {
return <p className="text-center mt-10 text-gray-500">Chargement du classement...</p>;
}
return (
<div className="mx-auto max-w-4xl px-4 sm:px-6 lg:px-8 mt-12">
<div className="flex flex-col sm:flex-row items-center justify-between mb-6 gap-4">
<h2 className="text-3xl font-bold text-gray-800">Classement</h2>
<button
onClick={fetchClassement}
disabled={refreshing}
className="px-5 py-2 bg-blue-600 text-white font-medium rounded hover:bg-blue-700 disabled:opacity-50 transition"
>
{refreshing ? "Rafraîchissement..." : "Rafraîchir"}
</button>
</div>
<div className="overflow-x-auto shadow rounded-lg">
<table className="min-w-full bg-white border border-gray-200">
<thead className="bg-gray-100">
<tr>
<th className="px-4 py-3 text-left text-sm font-semibold text-gray-700">Rang</th>
<th className="px-4 py-3 text-left text-sm font-semibold text-gray-700">Pseudo</th>
<th className="px-4 py-3 text-left text-sm font-semibold text-gray-700">Score</th>
<th className="px-4 py-3 text-left text-sm font-semibold text-gray-700">Temps (s)</th>
<th className="px-4 py-3 text-left text-sm font-semibold text-gray-700">Date</th>
</tr>
</thead>
<tbody>
{joueurs.map((joueur, index) => (
<tr
key={joueur.id}
className="border-t border-gray-200 hover:bg-gray-50 transition"
>
<td className="px-4 py-3 text-sm text-gray-900 font-medium">
{getMedal(index)}
</td>
<td className="px-4 py-3 text-sm text-gray-800">{joueur.pseudo}</td>
<td className="px-4 py-3 text-sm text-gray-800">{joueur.meilleur_score}</td>
<td className="px-4 py-3 text-sm text-gray-800">
{joueur.meilleur_temps ?? "-"}
</td>
<td className="px-4 py-3 text-sm text-gray-800">
{joueur.date_meilleur_score
? new Date(joueur.date_meilleur_score).toLocaleDateString("fr-FR")
: "-"}
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
);
}

Bonus (facultatif)
Si vous voulez aller plus loin, vous pouvez :
- Ajouter un rafraîchissement automatique toutes les 30 secondes
- Ajouter un toast pour indiquer que le classement a été mis à jour