Enregistrement du joueur
Créer un composant React pour permettre au joueur de s’enregistrer avant de commencer le quiz

Objectifs de la séance
- Comprendre ce qu’est un composant React
- Créer un formulaire d’enregistrement pour demander le nom et le prénom du joueur
- Enregistrer ce joueur dans la base de données Supabase (table
joueur) - Préparer le terrain pour enregistrer son score plus tard
Notions théoriques
Qu’est-ce qu’un composant React ?

Un composant est un bloc de code réutilisable.
Un composant peut contenir :
- du HTML (rendu à l’écran)
- du JavaScript (logique de l’interface)
- des états (
useState) pour gérer les données internes - des props (paramètres) pour personnaliser son comportement
Pourquoi créer des composants ?
- Pour organiser le code en petites parties lisibles
- Pour réutiliser des éléments (ex. : une carte, un bouton, un formulaire)
- Pour séparer les responsabilités : chaque composant a un rôle précis
Exemple simple de composant
// app/components/Bonjour.tsx
export default function Bonjour() {
return <p>Bonjour !</p>;
}
Et dans app/page.tsx :
import Bonjour from "./components/Bonjour";
export default function Home() {
return (
<div>
<Bonjour />
</div>
);
}
Composant avec état (useState)
"use client";
import { useState } from "react";
export default function Compteur() {
const [nombre, setNombre] = useState(0);
return (
<div>
<p>Vous avez cliqué {nombre} fois</p>
<button onClick={() => setNombre(nombre + 1)}>Cliquez ici</button>
</div>
);
}
Où créer les composants ?
Dans un projet Next.js, vous pouvez créer un dossier pour vos composants :
/app/components/
Chaque composant est un fichier .tsx (TypeScript + JSX).
- Le TypeScript est une extension de JavaScript qui ajoute la gestion des types.
- JSX est une syntaxe qui permet de mélanger du JavaScript et du HTML.
Étapes pour enregistrer un joueur
- Créer un composant
FormulaireJoueur - Afficher un formulaire avec
Nom,Prénom - Ajouter un bouton Commencer le quiz
- À la soumission, insérer un nouveau joueur dans Supabase
- Stocker l’ID du joueur dans le
localStorage(ou dans unuseStateglobal temporairement)
Table joueur dans Supabase
La table joueur est déjà créée :
CREATE TABLE public.joueur (
id bigint GENERATED ALWAYS AS IDENTITY NOT NULL,
created_at timestamp with time zone NOT NULL DEFAULT now(),
pseudo character varying NOT NULL,
email character varying,
date_inscription date NOT NULL,
CONSTRAINT joueur_pkey PRIMARY KEY (id)
);
Nous allons utiliser uniquement le champ pseudo pour y stocker le nom complet du joueur.
Sécurité : RLS
-
Vérifiez que les RLS sont activées sur la table
joueur:-- Activer RLS
ALTER TABLE joueur ENABLE ROW LEVEL SECURITY; -
Autorisez l'insertion publique :
-- Autoriser l'insertion pour tout le monde
CREATE POLICY "Allow insert for all users"
ON joueur
FOR INSERT
TO public
WITH CHECK (true); -
Autorisez la lecture publique :
-- Autoriser la lecture pour tout le monde
CREATE POLICY "Enable read access for all users"
ON joueur
TO public
USING (
true
);
Quelques méthodes à connaître
| Méthode / outil | Utilité |
|---|---|
useState | G érer des champs de formulaire |
supabase.from(...).insert(...) | Ajouter une ligne dans une table |
localStorage.setItem(...) | Mémoriser un ID dans le navigateur |
onSubmit={handleSubmit} | Gérer l’envoi d’un formulaire |
preventDefault() | Empêcher le rechargement de la page |
Test de mémorisation / compréhension
TP pour réfléchir et résoudre des problèmes
Objectif
Créer un formulaire de début de partie et enregistrer le joueur
Étapes du TP
- Créer une RLS
- Créer un composant
FormulaireJoueur.tsx - Afficher un formulaire avec un champ
Nom et prénomobligatoire - Ajouter un bouton Commencer le quiz
- À la soumission :
- insérer un joueur dans Supabase (dans la table
joueur) - stocker l’ID du joueur dans
localStorage - afficher un message de confirmation
- insérer un joueur dans Supabase (dans la table
- Afficher ce composant dans
app/page.tsxavant le quiz - N’autoriser l’accès au quiz que si un joueur est enregistré
Étape 1. Créer les RLS
-
Vérifiez que les RLS sont activées sur la table
joueur:-- Activer RLS
ALTER TABLE joueur ENABLE ROW LEVEL SECURITY; -
Autorisez l'insertion publique :
-- Autoriser l'insertion pour tout le monde
CREATE POLICY "Allow insert for all users"
ON joueur
FOR INSERT
TO public
WITH CHECK (true);
-
Autorisez la lecture publique :
-- Autoriser la lecture pour tout le monde
CREATE POLICY "Enable read access for all users"
ON joueur
TO public
USING (
true
);
Étape 2 : Ajoutez les valeurs par défaut
-- Ajouter une valeur par défaut pour le champ created_at
ALTER TABLE joueur
ALTER COLUMN created_at SET DEFAULT now();
-- Ajouter une valeur par défaut pour le champ date_inscription
ALTER TABLE joueur
ALTER COLUMN date_inscription SET DEFAULT now();

Étape 3. Créer FormulaireJoueur.tsx
Dans le dossier app/components/, créez le fichier FormulaireJoueur.tsx avec le code suivant :
"use client";
import { useState } from "react";
import { supabase } from "@/lib/supabaseClient";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
export default function FormulaireJoueur({ onJoueurCree }: { onJoueurCree: () => void }) {
const [nom, setNom] = useState("");
const [message, setMessage] = useState("");
async function handleSubmit(e: React.FormEvent) {
e.preventDefault();
if (!nom.trim()) {
setMessage("Veuillez entrer votre nom.");
return;
}
const { data, error } = await supabase.from("joueur").insert({
pseudo: nom,
}).select().single();
if (error) {
setMessage("Erreur lors de l'enregistrement.");
console.error(error);
} else {
localStorage.setItem("joueur_id", data.id);
setMessage("Bienvenue " + nom + " !");
onJoueurCree();
}
}
return (
<form onSubmit={handleSubmit} className="max-w-md mx-auto mt-10 space-y-4">
<label className="block text-lg font-semibold">Nom et prénom</label>
<Input
type="text"
value={nom}
onChange={(e) => setNom(e.target.value)}
placeholder="Votre nom complet"
/>
<Button type="submit" className="w-full">Commencer le quiz</Button>
{message && (
<Alert className="mt-4">
<AlertTitle>Info</AlertTitle>
<AlertDescription>{message}</AlertDescription>
</Alert>
)}
</form>
);
}
Impossible de localiser le module '@/components/ui/input'
Si l'erreur Impossible de localiser le module '@/components/ui/input' s'affiche, c'est que le composant Input n'existe pas dans votre projet.
Il suffit d'installer le composant Input de Shadcn, tel qu'indiqué sur https://ui.shadcn.com/docs/components/input.
Utilisez la commande suivante dans votre terminal, à la racine du projet :
npx shadcn@latest add input

Décryptage du code
Le composant FormulaireJoueur.tsx a pour objectif d’afficher un formulaire permettant à un joueur de saisir son nom et prénom, puis de l’enregistrer dans la base de données Supabase.
Une fois le joueur enregistré, son identifiant est mémorisé dans le navigateur (dans le localStorage) et le quiz démarre.
Fonction principale du composant
export default function FormulaireJoueur({ onJoueurCree }: { onJoueurCree: () => void }) {
- Il s'agit d’un composant fonctionnel.
- Il reçoit une propriété (prop) appelée
onJoueurCree: c’est une fonction que le parent (dans notre cas,page.tsx) va lui transmettre. - Cette fonction sera appelée quand le joueur est bien enregistré, afin d’indiquer que le quiz peut commencer.
Déclaration des états
const [nom, setNom] = useState("");
const [message, setMessage] = useState("");
nom: contient le texte saisi par l’utilisateur dans le champ de formulaire.La fonction
setNompermet de modifier la valeur denom.message: contient un message d’erreur ou de confirmation à afficher.La fonction
setMessagepermet de modifier la valeur demessage.
Fonction de traitement du formulaire
async function handleSubmit(e: React.FormEvent) {
e.preventDefault();
- Cette fonction est appelée lorsque l’utilisateur soumet le formulaire.
e.preventDefault()empêche le comportement par défaut du formulaire (qui est de recharger la page).
Vérification du champ
if (!nom.trim()) {
setMessage("Veuillez entrer votre nom.");
return;
}
- Si le champ est vide ou contient uniquement des espaces, on affiche un message d’erreur.
Insertion dans Supabase
const { data, error } = await supabase
.from("joueur")
.insert({
pseudo: nom,
})
.select()
.single();
- On insère une nouvelle ligne dans la table
joueur.pseudo: on y met le nom saisi par l’utilisateur.
- La méthode
insert()retourne un objet avec deux propriétés :data: contient les données de la ligne insérée (si tout s’est bien passé)error: contient l’erreur (si une erreur est survenue)
.select().single(): permet de récupérer immédiatement la ligne insérée (y compris sonid)
Traitement du résultat
if (error) {
setMessage("Erreur lors de l'enregistrement.");
console.error(error);
} else {
localStorage.setItem("joueur_id", data.id);
setMessage("Bienvenue " + nom + " !");
onJoueurCree();
}
- Si une erreur survient, on l’affiche dans la console et on prévient l’utilisateur.
- Sinon :
- On stocke l’
iddu joueur dans lelocalStorage→ cela permet de s’en souvenir plus tard (ex : pour enregistrer son score) - On affiche un message de bienvenue
- On appelle la fonction
onJoueurCree()pour signaler au parent que tout est prêt.
- On stocke l’
Affichage du formulaire
<form onSubmit={handleSubmit} className="max-w-md mx-auto mt-10 space-y-4">
- Le formulaire est centré (
mx-auto) et limité à une largeur (max-w-md). onSubmit={handleSubmit}: quand l’utilisateur valide, on appelle notre fonction.
Champ de saisie
<label className="block text-lg font-semibold">Nom et prénom</label>
<Input
type="text"
value={nom}
onChange={(e) => setNom(e.target.value)}
placeholder="Votre nom complet"
/>
- Le champ affiche le contenu de
nom - À chaque frappe de l’utilisateur, on met à jour
nomavecsetNom
Bouton de validation
<Button type="submit" className="w-full">Commencer le quiz</Button>
- C’est un bouton de type
submit, donc il déclenchehandleSubmit - Il est large (
w-full) pour une meilleure ergonomie
Affichage du message
{message && (
<Alert className="mt-4">
<AlertTitle>Info</AlertTitle>
<AlertDescription>{message}</AlertDescription>
</Alert>
)}
- Si un message est défini (erreur ou confirmation), on l’affiche dans un composant
Alert.
Résumé du fonctionnement
| Étape | Description |
|---|---|
| 1 | L’utilisateur voit un champ pour entrer son nom |
| 2 | Il clique sur "Commencer le quiz" |
| 3 | Le formulaire est traité sans recharger la page |
| 4 | Le joueur est ajouté dans la base Supabase |
| 5 | Son ID est stocké dans le navigateur |
| 6 | Le quiz démarre |
Étape 4. Modifier app/page.tsx
Modifiez le fichier app/page.tsx pour afficher le formulaire avant le quiz, et n’autoriser l’accès au quiz que si un joueur est enregistré.
En haut du fichier :
import FormulaireJoueur from "@/components/FormulaireJoueur";
Dans le composant Home :
const [joueurPret, setJoueurPret] = useState(false);
useEffect(() => {
const joueurId = localStorage.getItem("joueur_id");
if (joueurId) {
setJoueurPret(true);
}
}, []);
Et dans le return :
{!joueurPret ? (
<FormulaireJoueur onJoueurCree={() => setJoueurPret(true)} />
) : (
// ... le quiz existant
)}
Décryptage du code
Que se passe-t-il dans page.tsx ?
Dans le fichier app/page.tsx, on affiche ce composant seulement si aucun joueur n’est encore enregistré :
const [joueurPret, setJoueurPret] = useState(false);
useEffect(() => {
const joueurId = localStorage.getItem("joueur_id");
if (joueurId) {
setJoueurPret(true);
}
}, []);
Et dans le return :
{!joueurPret ? (
<FormulaireJoueur onJoueurCree={() => setJoueurPret(true)} />
) : (
// Affichage du quiz
)}
- Si
joueurPretestfalse, on affiche le formulaire. - Quand le joueur est créé, on appelle
setJoueurPret(true)pour afficher le quiz.
Résultat attendu
- Le joueur saisit son nom
- Il est enregistré dans Supabase
- Son ID est stocké dans
localStorage - Le quiz démarre uniquement après l’enregistrement

Une solution
Code complet SQL
-- Activer RLS
ALTER TABLE joueur ENABLE ROW LEVEL SECURITY;
-- Autoriser l'insertion pour tout le monde
CREATE POLICY "Allow insert for all users"
ON joueur
FOR INSERT
TO public
WITH CHECK (true);
-- Ajouter une valeur par défaut pour le champ created_at
ALTER TABLE joueur
ALTER COLUMN created_at SET DEFAULT now();
Code complet de app/components/FormulaireJoueur.tsx
"use client";
import { useState } from "react";
import { supabase } from "@/lib/supabaseClient";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
export default function FormulaireJoueur({ onJoueurCree }: { onJoueurCree: () => void }) {
const [nom, setNom] = useState("");
const [message, setMessage] = useState("");
async function handleSubmit(e: React.FormEvent) {
e.preventDefault();
if (!nom.trim()) {
setMessage("Veuillez entrer votre nom.");
return;
}
const { data, error } = await supabase.from("joueur").insert({
pseudo: nom,
}).select().single();
if (error) {
setMessage("Erreur lors de l'enregistrement.");
console.error(error);
} else {
localStorage.setItem("joueur_id", data.id);
setMessage("Bienvenue " + nom + " !");
onJoueurCree();
}
}
return (
<form onSubmit={handleSubmit} className="max-w-md mx-auto mt-10 space-y-4">
<label className="block text-lg font-semibold">Nom et prénom</label>
<Input
type="text"
value={nom}
onChange={(e) => setNom(e.target.value)}
placeholder="Votre nom complet"
/>
<Button type="submit" className="w-full">Commencer le quiz</Button>
{message && (
<Alert className="mt-4">
<AlertTitle>Info</AlertTitle>
<AlertDescription>{message}</AlertDescription>
</Alert>
)}
</form>
);
}
Code complet de app/page.tsx
"use client";
import { useEffect, useState } from "react";
import { supabase } from "../lib/supabaseClient";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Alert, AlertTitle, AlertDescription } from "@/components/ui/alert";
import { Button } from "@/components/ui/button";
import FormulaireJoueur from "@/components/FormulaireJoueur";
import Image from "next/image";
import Link from "next/link";
export default function Home() {
const [questions, setQuestions] = useState<any[]>([]);
const [questionIndex, setQuestionIndex] = useState(0);
const [explication, setExplication] = useState("");
const [afficherExplication, setAfficherExplication] = useState(false);
const question = questions[questionIndex];
const [joueurPret, setJoueurPret] = useState(false);
useEffect(() => {
const joueurId = localStorage.getItem("joueur_id");
if (joueurId) {
setJoueurPret(true);
}
}, []);
useEffect(() => {
async function fetchQuestion() {
const { data, error } = await supabase
.from("question")
.select(`
id,
texte,
image_url,
image_credit_nom,
image_credit_url,
explication,
reponses:reponse (
id,
texte,
est_correcte
)
`)
.order("id", { ascending: true });
console.log("Données récupérées :", data);
if (error) {
console.error("Erreur Supabase :", error);
} else {
setQuestions(data || []); // On prend toutes les questions récupérées
}
}
fetchQuestion();
}, []);
function handleClick(reponse: any) {
if (!question || afficherExplication) return;
const estBonneReponse = reponse.est_correcte;
const message = estBonneReponse
? "Bonne réponse !"
: "Mauvaise réponse.";
const explicationTexte = question.explication || message;
setExplication(explicationTexte);
setAfficherExplication(true);
setTimeout(() => {
setAfficherExplication(false);
setExplication("");
setQuestionIndex((prev) => prev + 1);
}, 5000);
}
if (!question) {
return (
<div className="text-center mt-10">
<h2 className="text-2xl font-bold">Quiz terminé !</h2>
<p className="mt-4 text-muted-foreground">Merci d’avoir participé.</p>
</div>
);
}
return (
<div>
{!joueurPret ? (
<FormulaireJoueur onJoueurCree={() => setJoueurPret(true)} />
) : (
<div>
<Alert className="bg-blue-50 border-blue-300 text-blue-800 max-w-xl mx-auto mt-6">
<AlertTitle className="text-xl font-semibold">Bienvenue sur CyberQuiz</AlertTitle>
<AlertDescription>
Un quiz pour tester vos connaissances en cybersécurité.
</AlertDescription>
</Alert>
{questions.length > 0 ? (
<Card className="max-w-4xl mx-auto mt-6">
<div className="flex flex-col md:flex-row">
{/* Colonne gauche : image + crédit */}
<div className="w-full md:w-1/2 p-4">
{question.image_url ? (
<Image
src={question.image_url}
alt="Illustration de la question"
width={400}
height={300}
className="rounded"
/>
) : (
<div className="w-full h-[300px] bg-gray-100 flex items-center justify-center text-sm text-gray-500 rounded">
Aucune image disponible
</div>
)}
{question.image_credit_nom && question.image_credit_url && (
<Alert className="mt-4 text-sm text-muted-foreground">
<AlertDescription>
<span className="inline">
Image :{" "}
<Link
href={question.image_credit_url}
target="_blank"
rel="noopener noreferrer"
className="underline underline-offset-2 hover:text-primary"
>
{question.image_credit_nom}
</Link>
</span>
</AlertDescription>
</Alert>
)}
</div>
{/* Colonne droite : question + réponses */}
<div className="w-full md:w-1/2 p-4">
<CardHeader className="p-0 mb-4">
<CardTitle>Question</CardTitle>
</CardHeader>
<CardContent className="p-0">
<p className="text-lg font-semibold mb-4">{question.texte}</p>
{question.reponses.map((reponse: any) => (
<Button
key={reponse.id}
onClick={() => handleClick(reponse)}
disabled={afficherExplication}
className="w-full justify-start mt-2 whitespace-normal text-left"
variant="outline"
>
{reponse.texte}
</Button>
))}
</CardContent>
{afficherExplication && (
<Alert className="mt-6 bg-yellow-50 border-yellow-300 text-yellow-800">
<AlertTitle>Explication</AlertTitle>
<AlertDescription>{explication}</AlertDescription>
</Alert>
)}
</div>
</div>
</Card>
) : (
<p className="text-center mt-6">Chargement de la question...</p>
)}
</div>
)}
</div>
);
}
Bonus : Afficher le nom du joueur
Comment afficher le nom du joueur dans la page du quiz ?

Une solution
Maintenant que le joueur est enregistré et que son ID est stocké dans le
localStorage, nous allons récupérer son nom depuis Supabase et l’afficher dans l’interface du quiz, par exemple dans l’en-tête ou dans une alerte de bienvenue.
Étape 1 : Ajouter un nouvel état joueurNom
Dans le composant Home (app/page.tsx), ajoutez un nouvel état pour stocker le nom du joueur :
const [joueurNom, setJoueurNom] = useState("");
Étape 2 : Récupérer le nom du joueur depuis Supabase
Dans le useEffect qui vérifie si un joueur est prêt, ajoutez une requête Supabase pour récupérer le nom :
useEffect(() => {
const joueurId = localStorage.getItem("joueur_id");
if (joueurId) {
setJoueurPret(true);
// Requête Supabase pour récupérer le nom du joueur
supabase
.from("joueur")
.select("pseudo")
.eq("id", joueurId)
.single()
.then(({ data, error }) => {
if (error) {
console.error("Erreur lors de la récupération du joueur :", error);
} else if (data) {
setJoueurNom(data.pseudo);
}
});
}
}, []);
Étape 3 : Afficher le nom du joueur dans l'interface
Juste au-dessus du quiz, vous pouvez afficher une alerte personnalisée avec le nom du joueur :
<Alert className="bg-green-50 border-green-300 text-green-800 max-w-xl mx-auto mt-6">
<AlertTitle className="text-xl font-semibold">
Bienvenue {joueurNom} !
</AlertTitle>
<AlertDescription>
Préparez-vous à tester vos connaissances en cybersécurité.
</AlertDescription>
</Alert>
Nouveau code complet de app/page.tsx
"use client";
import { useEffect, useState } from "react";
import { supabase } from "../lib/supabaseClient";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Alert, AlertTitle, AlertDescription } from "@/components/ui/alert";
import { Button } from "@/components/ui/button";
import FormulaireJoueur from "@/components/FormulaireJoueur";
import Image from "next/image";
import Link from "next/link";
export default function Home() {
const [questions, setQuestions] = useState<any[]>([]);
const [questionIndex, setQuestionIndex] = useState(0);
const [explication, setExplication] = useState("");
const [afficherExplication, setAfficherExplication] = useState(false);
const [joueurNom, setJoueurNom] = useState("");
const question = questions[questionIndex];
const [joueurPret, setJoueurPret] = useState(false);
useEffect(() => {
const joueurId = localStorage.getItem("joueur_id");
if (joueurId) {
setJoueurPret(true);
// Requête Supabase pour récupérer le nom du joueur
supabase
.from("joueur")
.select("pseudo")
.eq("id", joueurId)
.single()
.then(({ data, error }) => {
if (error) {
console.error("Erreur lors de la récupération du joueur :", error);
} else if (data) {
setJoueurNom(data.pseudo);
}
});
}
}, []);
useEffect(() => {
async function fetchQuestion() {
const { data, error } = await supabase
.from("question")
.select(`
id,
texte,
image_url,
image_credit_nom,
image_credit_url,
explication,
reponses:reponse (
id,
texte,
est_correcte
)
`)
.order("id", { ascending: true });
console.log("Données récupérées :", data);
if (error) {
console.error("Erreur Supabase :", error);
} else {
setQuestions(data || []); // On prend toutes les questions récupérées
}
}
fetchQuestion();
}, []);
function handleClick(reponse: any) {
if (!question || afficherExplication) return;
const estBonneReponse = reponse.est_correcte;
const message = estBonneReponse
? "Bonne réponse !"
: "Mauvaise réponse.";
const explicationTexte = question.explication || message;
setExplication(explicationTexte);
setAfficherExplication(true);
setTimeout(() => {
setAfficherExplication(false);
setExplication("");
setQuestionIndex((prev) => prev + 1);
}, 5000);
}
if (!question) {
return (
<div className="text-center mt-10">
<h2 className="text-2xl font-bold">Quiz terminé !</h2>
<p className="mt-4 text-muted-foreground">Merci d’avoir participé.</p>
</div>
);
}
return (
<div>
{!joueurPret ? (
<FormulaireJoueur onJoueurCree={() => setJoueurPret(true)} />
) : (
<div>
{joueurNom && (
<Alert className="bg-blue-50 border-blue-300 text-blue-800 max-w-xl mx-auto mt-6">
<AlertTitle className="text-xl font-semibold">
Bienvenue {joueurNom} !
</AlertTitle>
<AlertDescription>
Préparez-vous à tester vos connaissances en cybersécurité.
</AlertDescription>
</Alert>
)}
{questions.length > 0 ? (
<Card className="max-w-4xl mx-auto mt-6">
<div className="flex flex-col md:flex-row">
{/* Colonne gauche : image + crédit */}
<div className="w-full md:w-1/2 p-4">
{question.image_url ? (
<Image
src={question.image_url}
alt="Illustration de la question"
width={400}
height={300}
className="rounded"
/>
) : (
<div className="w-full h-[300px] bg-gray-100 flex items-center justify-center text-sm text-gray-500 rounded">
Aucune image disponible
</div>
)}
{question.image_credit_nom && question.image_credit_url && (
<Alert className="mt-4 text-sm text-muted-foreground">
<AlertDescription>
<span className="inline">
Image :{" "}
<Link
href={question.image_credit_url}
target="_blank"
rel="noopener noreferrer"
className="underline underline-offset-2 hover:text-primary"
>
{question.image_credit_nom}
</Link>
</span>
</AlertDescription>
</Alert>
)}
</div>
{/* Colonne droite : question + réponses */}
<div className="w-full md:w-1/2 p-4">
<CardHeader className="p-0 mb-4">
<CardTitle>Question</CardTitle>
</CardHeader>
<CardContent className="p-0">
<p className="text-lg font-semibold mb-4">{question.texte}</p>
{question.reponses.map((reponse: any) => (
<Button
key={reponse.id}
onClick={() => handleClick(reponse)}
disabled={afficherExplication}
className="w-full justify-start mt-2 whitespace-normal text-left"
variant="outline"
>
{reponse.texte}
</Button>
))}
</CardContent>
{afficherExplication && (
<Alert className="mt-6 bg-yellow-50 border-yellow-300 text-yellow-800">
<AlertTitle>Explication</AlertTitle>
<AlertDescription>{explication}</AlertDescription>
</Alert>
)}
</div>
</div>
</Card>
) : (
<p className="text-center mt-6">Chargement de la question...</p>
)}
</div>
)}
</div>
);
}