Aller au contenu principal

Enregistrement du joueur

Créer un composant React pour permettre au joueur de s’enregistrer de manière sécurisée avant de commencer le quiz


Objectifs de la séance

  • Comprendre ce qu’est un composant React
  • Mettre en place une authentification sécurisée avec Supabase
  • Utiliser un trigger pour lier un utilisateur authentifié à un joueur
  • Créer des politiques RLS (Row Level Security) pour protéger les données
  • Créer un formulaire d’enregistrement pour demander le pseudo du 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 de manière sécurisée

  1. Utiliser l'authentification anonyme de Supabase pour créer un utilisateur sécurisé.
  2. Créer un trigger qui se déclenche à la création de l'utilisateur pour l'insérer automatiquement dans la table joueur.
  3. Créer une politique RLS pour que chaque utilisateur ne puisse voir et modifier que ses propres données.
  4. Créer un composant FormulaireJoueur qui appelle signInAnonymously.
  5. Stocker l'ID de l'utilisateur Supabase dans le localStorage.
  6. Utiliser cet ID pour lier les futures actions (comme le score) au joueur.

Table joueur dans Supabase

La table joueur doit être modifiée pour être liée à la table auth.users de Supabase.

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,
user_id uuid REFERENCES auth.users(id) ON DELETE CASCADE,
CONSTRAINT joueur_pkey PRIMARY KEY (id),
CONSTRAINT joueur_user_id_unique UNIQUE (user_id)
);

Nous utiliserons le champ pseudo pour stocker le nom du joueur et le champ user_id pour le lier de manière sécurisée à un compte Supabase.


Sécurité : Authentification et RLS

1. Activer l'authentification anonyme

Dans la console Supabase, allez dans Authentication > Settings et activez Enable anonymous sign-in.

2. Créer un trigger pour créer le joueur automatiquement

Pour garantir que chaque utilisateur authentifié a une entrée correspondante dans la table joueur, nous créons une fonction et un trigger.

-- La fonction qui sera exécutée par le trigger
CREATE OR REPLACE FUNCTION public.handle_new_user()
RETURNS TRIGGER AS $$
BEGIN
-- On récupère le pseudo depuis les métadonnées envoyées lors de l'inscription
DECLARE new_pseudo TEXT;
new_pseudo := NEW.raw_user_meta_data->>'pseudo';

-- On lève une erreur explicite si le pseudo est manquant ou vide
IF new_pseudo IS NULL OR trim(new_pseudo) = '' THEN
RAISE EXCEPTION 'Le pseudo est obligatoire pour créer un joueur.';
END IF;

-- Si le pseudo est présent, on insère la nouvelle ligne dans la table joueur
INSERT INTO public.joueur (user_id, pseudo, date_inscription)
VALUES (
NEW.id,
new_pseudo,
CURRENT_DATE
);

RETURN NEW;
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;

-- Le trigger qui déclenche la fonction après qu'un nouvel utilisateur soit créé dans auth.users
CREATE TRIGGER on_auth_user_created
AFTER INSERT ON auth.users
FOR EACH ROW EXECUTE FUNCTION public.handle_new_user();

3. Créer une politique RLS sécurisée

Activez les RLS sur la table joueur et créez une politique qui n'autorise l'accès qu'au propriétaire de la ligne.

-- Activer RLS
ALTER TABLE public.joueur ENABLE ROW LEVEL SECURITY;

-- Créer la politique sécurisée
CREATE POLICY "A user can view and modify their own player data"
ON public.joueur
FOR ALL
USING (auth.uid() = user_id)
WITH CHECK (auth.uid() = user_id);

Cette politique est beaucoup plus sûre car elle s'assure qu'un utilisateur ne peut voir ou modifier que la ligne de la table joueur qui lui est directement liée via son user_id.


Quelques méthodes à connaître

Méthode / outilUtilité
useStateGérer des champs de formulaire
supabase.auth.signInAnonymously()Créer un utilisateur anonyme sécurisé
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 ?


Pourquoi utiliser l'authentification anonyme de Supabase ici ?


Quel mécanisme permet de créer automatiquement un joueur dans la table `joueur` lors de l'inscription ?


Dans une politique RLS sécurisée, à quoi correspond `auth.uid()` ?



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 de manière sécurisée en le liant au système d'authentification de Supabase.

Lorsque le joueur a saisi son nom et clique sur le bouton Commencer le quiz :

  • authentifier l'utilisateur anonymement
  • insérer un joueur dans Supabase (via un trigger)
  • stocker l’ID de l'utilisateur Supabase dans localStorage

Étapes du TP

  1. Modifier la table joueur
  2. Créer un trigger
  3. Créer une RLS
  4. Ajouter les valeurs par défaut
  5. Créer un composant FormulaireJoueur.tsx pour afficher un formulaire avec un champ Pseudo obligatoire
  6. Modifier app/page.tsx pour afficher ce formulaire avant le quiz pour afficher ce composant dans app/page.tsx avant le quiz et n'autoriser l’accès au quiz que si un joueur est enregistré

Étape 1 : Modifier la table joueur

Utiliser l'authentification de Supabase pour lier chaque joueur à un utilisateur sécurisé

astuce

Supabase offre une authentification anonyme qui crée un utilisateur temporaire dans la table auth.users.

Cette authentification fournie par Supabase vous donne un auth.uid() fiable et sécurisé à utiliser dans vos politiques RLS.

Nous allons lier chaque joueur à un utilisateur de la table auth.users.

Qu'est-ce que la table auth.users ?

La table auth.users est une table spéciale gérée par Supabase qui stocke les informations des utilisateurs authentifiés. Elle contient des champs comme id, email, created_at, etc. Le champ id est un identifiant unique pour chaque utilisateur, souvent de type uuid.

Pour gérer les utilisateurs, dans Supabase, il suffit d'aller dans l'onglet Authentication > Users.

La meilleure façon de lier les joueurs à la table auth.users, c'est :

  • d'ajouter une colonne user_id de type uuid et d'y mettre une clé étrangère

    -- Ajouter une colonne pour lier chaque joueur à un utilisateur Supabase
    ALTER TABLE public.joueur
    ADD COLUMN user_id uuid REFERENCES auth.users(id) ON DELETE CASCADE;

  • de la rendre colonne user_id unique pour garantir une relation 1-pour-1

    -- Rendre cette colonne unique pour garantir la relation 1-pour-1
    ALTER TABLE public.joueur ADD CONSTRAINT joueur_user_id_unique UNIQUE (user_id);


Étape 2 : Créer un trigger

Créer un trigger pour créer le joueur automatiquement

Pour que le processus soit fluide et sécurisé, on va créer un trigger qui, dès qu'un utilisateur s'inscrit (anonymement), crée automatiquement une ligne correspondante dans la table joueur.

info

Nous apprendrons plus tard à créer des triggers

Voici le code SQL pour créer la fonction qui sera exécutée par le trigger :

-- Fonction qui sera exécutée par le trigger
-- On utilise CREATE OR REPLACE pour mettre à jour la fonction existante
CREATE OR REPLACE FUNCTION public.handle_new_user()
RETURNS TRIGGER AS $$ DECLARE
-- La déclaration des variables doit se faire AVANT le bloc BEGIN
new_pseudo TEXT;
BEGIN
-- On récupère le pseudo depuis les métadonnées envoyées lors de l'inscription
new_pseudo := NEW.raw_user_meta_data->>'pseudo';

-- On lève une erreur explicite si le pseudo est manquant ou vide
IF new_pseudo IS NULL OR trim(new_pseudo) = '' THEN
RAISE EXCEPTION 'Le pseudo est obligatoire pour créer un joueur.';
END IF;

-- Si le pseudo est présent, on insère la nouvelle ligne dans la table joueur
INSERT INTO public.joueur (user_id, pseudo, date_inscription)
VALUES (
NEW.id,
new_pseudo,
CURRENT_DATE
);

RETURN NEW;
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;

Voici le code SQL pour créer le trigger qui utilise cette fonction :

-- Trigger qui déclenche la fonction après qu'un nouvel utilisateur soit créé dans auth.users
CREATE TRIGGER on_auth_user_created
AFTER INSERT ON auth.users
FOR EACH ROW EXECUTE FUNCTION public.handle_new_user();


Étape 3. 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;

  • Créez une politique sécurisée qui n'autorise l'accès qu'au propriétaire des données. C'est beaucoup plus sûr que d'autoriser l'accès public.

    Voici le code SQL pour la politique RLS sécurisée

    -- Créer la politique sécurisée
    CREATE POLICY "A user can view and modify their own player data"
    ON public.joueur
    FOR ALL
    USING (auth.uid() = user_id)
    WITH CHECK (auth.uid() = user_id);

    astuce

    Cette politique garantit qu'un utilisateur ne peut modifier que la ligne de la table joueur qui lui appartient (user_id).


Étape 4 : Ajouter les valeurs par défaut

Pour conserver la date de création et d’inscription du joueur, nous allons ajouter des valeurs par défaut dans la table joueur, pour les champs created_at et date_inscription.

  • Voici le code SQL à exécuter dans Supabase :

    -- 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 5. Créer FormulaireJoueur.tsx

Dans le dossier app/components/, créez le fichier FormulaireJoueur.tsx.

Le composant FormulaireJoueur va appeler la fonction signInAnonymously (il ne va pas insérer lui même les données).

  • Voici le code complet du fichier 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("");
    const [isLoading, setIsLoading] = useState(false);

    async function handleSubmit(e: React.FormEvent) {
    e.preventDefault();
    setMessage("");
    setIsLoading(true);

    if (!nom.trim()) {
    setMessage("Veuillez entrer un pseudo.");
    setIsLoading(false);
    return;
    }

    // On s'authentifie anonymement en passant le pseudo dans les métadonnées
    const { data: authData, error: authError } = await supabase.auth.signInAnonymously({
    options: {
    data: {
    pseudo: nom,
    },
    },
    });

    if (authError) {
    setMessage(`Erreur: ${authError.message}`);
    console.error(authError);
    } else if (authData.user) {
    // Si l'authentification réussit, le trigger a déjà créé le joueur.
    // On stocke l'ID de l'utilisateur Supabase pour les futures requêtes.
    localStorage.setItem("supabase_user_id", authData.user.id);
    setMessage(`Bienvenue ${nom} !`);
    onJoueurCree();
    }

    setIsLoading(false);
    }

    return (
    <form onSubmit={handleSubmit} className="max-w-md mx-auto mt-10 space-y-4">
    <label className="block text-lg font-semibold">Pseudo</label>
    <Input
    type="text"
    value={nom}
    onChange={(e) => setNom(e.target.value)}
    placeholder="Choisissez un pseudo"
    disabled={isLoading}
    />
    <Button type="submit" className="w-full" disabled={isLoading}>
    {isLoading ? "Connexion..." : "Commencer le quiz"}
    </Button>

    {message && (
    <Alert className="mt-4">
    <AlertTitle>Info</AlertTitle>
    <AlertDescription>{message}</AlertDescription>
    </Alert>
    )}
    </form>
    );
    }

    Si erreur 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

    Si une erreur s'affiche

    Veuillez vérifier votre trigger handle_new_user().

Décryptage du code du composant

Le composant FormulaireJoueur.tsx a pour objectif d’afficher un formulaire permettant à un joueur de saisir son pseudo, puis de l’authentifier de manière anonyme. Le trigger se charge ensuite de créer l'entrée dans la table joueur.

Une fois l'utilisateur authentifié, son identifiant Supabase 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("");
const [isLoading, setIsLoading] = useState(false);
  • nom : contient le texte saisi par l’utilisateur dans le champ de formulaire.
  • message : contient un message d’erreur ou de confirmation à afficher.
  • isLoading : un état pour gérer l'affichage d'un indicateur de chargement et désactiver le bouton pendant l'appel à l'API.
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 un pseudo.");
return;
}
  • Si le champ est vide ou contient uniquement des espaces, on affiche un message d’erreur.

Authentification et création automatique
  const { data: authData, error: authError } = await supabase.auth.signInAnonymously({
options: {
data: {
pseudo: nom,
},
},
});
  • On appelle la fonction signInAnonymously de Supabase.
  • On passe le pseudo dans les options.data. Ces données seront accessibles dans le trigger via NEW.raw_user_meta_data.
  • La méthode retourne un objet avec data (contenant l'utilisateur créé) et error.

Traitement du résultat
  if (authError) {
setMessage(`Erreur: ${authError.message}`);
console.error(authError);
} else if (authData.user) {
localStorage.setItem("supabase_user_id", authData.user.id);
setMessage(`Bienvenue ${nom} !`);
onJoueurCree();
}
  • Si une erreur survient, on l'affiche.
  • Sinon :
    • On stocke l’id de l'utilisateur Supabase (pas l'ID du joueur) dans le localStorage.
    • 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">Pseudo</label>
<Input
type="text"
value={nom}
onChange={(e) => setNom(e.target.value)}
placeholder="Choisissez un pseudo"
disabled={isLoading}
/>
  • Le champ affiche le contenu de nom
  • À chaque frappe de l’utilisateur, on met à jour nom avec setNom
  • Il est désactivé pendant le chargement.

Bouton de validation
<Button type="submit" className="w-full" disabled={isLoading}>
{isLoading ? "Connexion..." : "Commencer le quiz"}
</Button>
  • Le texte du bouton change pour indiquer l'état de chargement.

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 un pseudo
2Il clique sur "Commencer le quiz"
3Le formulaire appelle signInAnonymously sans recharger la page
4Le trigger crée le joueur dans la base Supabase
5L'ID de l'utilisateur Supabase est stocké dans le navigateur
6Le quiz démarre

Étape 6. 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é.

Votre fichier app/page.tsx doit :

  • chercher supabase_user_id dans le localStorage au chargement

  • utiliser user_id pour les requêtes SQL dans Supabase

  • si l’ID est trouvé, afficher le quiz

  • sinon, afficher le composant FormulaireJoueur

  • une fois le joueur créé, passer à l’affichage du quiz

  • stocker le nom du joueur pour l’afficher

  • Dans le fichier app/page.tsx, on affiche ce composant seulement si aucun joueur n’est encore enregistré :

    const [joueurPret, setJoueurPret] = useState(false);

    useEffect(() => {
    // On ne lance la récupération que si joueurPret est passé à 'true'
    if (joueurPret) {
    const userId = localStorage.getItem("supabase_user_id");
    if (userId) {
    supabase
    .from("joueur")
    .select("pseudo")
    .eq("user_id", userId)
    .single()
    .then(({ data, error }) => {
    if (error) {
    console.error("Erreur lors de la récupération du joueur :", error);
    } else if (data) {
    setJoueurNom(data.pseudo);
    }
    });
    }
    }
    }, []);

    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.
    • La logique pour récupérer le nom du joueur utilise maintenant supabase_user_id et filtre sur la colonne user_id de la table joueur.

    Voici une solution possible du code complet de app/page.tsx après modification
    "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(() => {
    // On ne lance la récupération que si joueurPret est passé à 'true'
    if (joueurPret) {
    const userId = localStorage.getItem("supabase_user_id");
    if (userId) {
    supabase
    .from("joueur")
    .select("pseudo")
    .eq("user_id", userId)
    .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 = message + " " + 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)}
    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>
    );
    }

    Résultat attendu

    • Le joueur saisit un pseudo
    • Il est authentifié anonymement et enregistré dans Supabase
    • L'ID de l'utilisateur Supabase est stocké dans localStorage
    • Le quiz démarre uniquement après l'enregistrement

    Si vous avez une erreur Anonymous sign-ins are disabled lors de l'insertion du joueur

    Cela signifie que l'authentification anonyme n'est pas activée dans votre projet Supabase.

    Pour l'activer, il vous suffit de vous rendre dans la console Supabase et d'activer cette option.

    • Ouvrez le dashboard de votre projet dans Supabase

    • Dans le menu de gauche, allez dans la section Authentication.

    • Cliquez sur Sign In / Providers.

    • Dans la section User Signups.

    • Trouvez le paramètre Allow anonymous sign-ins et activez-le en basculant l'interrupteur sur ON.

    • Cliquez sur Save changes.

    Pourquoi cette étape est-elle nécessaire ?

    Par défaut, Supabase ne permet pas les connexions anonymes pour des raisons de sécurité. C'est une fonctionnalité que vous devez explicitement activer. En l'activant, vous autorisez votre application à créer des utilisateurs temporaires via la fonction supabase.auth.signInAnonymously(), ce qui est exactement ce que nous avons besoin pour notre application.

    Une fois cette option activée et sauvegardée, retournez dans votre application et réessayez de soumettre le formulaire. L'erreur devrait avoir disparu et le joueur sera correctement créé.


Bonus : Afficher le nom du joueur

Comment afficher le nom du joueur dans la page du quiz ?

Maintenant que le joueur est enregistré, que son ID d'utilisateur Supabase est stocké dans le localStorage, et que nous récupérons son nom depuis Supabase nous pouvons afficher son nom dans l’interface du quiz, par exemple dans l’en-tête ou dans une alerte de bienvenue.

Dans le app/page.tsx, 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>

Le nom du joueur ne s’affiche pas

attention

Le nom du joueur ne s’affiche pas lors de l’affichage de la 1ère question.

Il faut rafraichir le navigateur, ce n'est pas satisfaisant !

Lorsque le composant page.tsx est monté pour la première fois, il exécute l'effet useEffect qui vérifie si un supabase_user_id est stocké dans le localStorage. Si c'est le cas, il définit joueurPret sur true et lance une requête pour récupérer le nom du joueur depuis la table joueur. Cependant, cette requête est asynchrone, ce qui signifie qu'elle ne bloque pas le rendu initial du composant. Ainsi, lors du premier rendu, le nom du joueur n'est pas encore disponible, car la requête pour le récupérer est toujours en cours.

La solution la plus simple à mettre en place, consiste à forcer la ré-exécution de l'effet useEffect lorsque joueurPret change.

Il suffit d'ajouter joueurPret comme dépendance au useEffect, afin de lui dire de se ré-exécuter chaque fois que l'état joueurPret change :

// Dans app/page.tsx

useEffect(() => {
// On ne lance la récupération que si joueurPret est passé à 'true'
if (joueurPret) {
const userId = localStorage.getItem("supabase_user_id");
if (userId) {
supabase
.from("joueur")
.select("pseudo")
.eq("user_id", userId)
.single()
.then(({ data, error }) => {
if (error) {
console.error("Erreur lors de la récupération du joueur :", error);
} else if (data) {
setJoueurNom(data.pseudo);
}
});
}
}
}, [joueurPret]); // <-- On ajoute joueurPret comme dépendance
Une proposition de solution du code complet de app/page.tsx