Aller au contenu principal

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 ?

Composant

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

info
  • 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

  1. Créer un composant FormulaireJoueur
  2. Afficher un formulaire avec Nom, Prénom
  3. Ajouter un bouton Commencer le quiz
  4. À la soumission, insérer un nouveau joueur dans Supabase
  5. Stocker l’ID du joueur dans le localStorage (ou dans un useState global 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 / outilUtilité
useStateGé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


Quel est le rôle principal d’un composant React ?


Quel hook permet de stocker des données dans un composant React ?


Où faut-il créer les composants dans un projet Next.js App Router ?


Quelle méthode permet d’ajouter un joueur dans Supabase ?


Pourquoi utilise-t-on `preventDefault()` dans un formulaire ?



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

  1. Créer une RLS
  2. Créer un composant FormulaireJoueur.tsx
  3. Afficher un formulaire avec un champ Nom et prénom obligatoire
  4. Ajouter un bouton Commencer le quiz
  5. À la soumission :
    • insérer un joueur dans Supabase (dans la table joueur)
    • stocker l’ID du joueur dans localStorage
    • afficher un message de confirmation
  6. Afficher ce composant dans app/page.tsx avant le quiz
  7. 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

02_erreur.png

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 setNom permet de modifier la valeur de nom.

  • message : contient un message d’erreur ou de confirmation à afficher.

    La fonction setMessage permet de modifier la valeur de message.

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 son id)

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’id du joueur dans le localStorage → 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.
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 nom avec setNom

Bouton de validation
<Button type="submit" className="w-full">Commencer le quiz</Button>
  • C’est un bouton de type submit, donc il déclenche handleSubmit
  • 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

ÉtapeDescription
1L’utilisateur voit un champ pour entrer son nom
2Il clique sur "Commencer le quiz"
3Le formulaire est traité sans recharger la page
4Le joueur est ajouté dans la base Supabase
5Son ID est stocké dans le navigateur
6Le 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 joueurPret est false, 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>
);
}