Aller au contenu principal

L'écran générateur de quiz

Laisser le joueur choisir un thème, un niveau et un nombre de questions, sans jamais lui permettre un réglage impossible.

Objectifs de la séance​

  • étendre les types et la fonction filtrerQuestions avec la valeur spéciale "tous" ;
  • créer un composant de choix (une « puce ») accessible aux lecteurs d'écran ;
  • calculer le nombre de questions disponibles et désactiver les réglages impossibles ;
  • naviguer avec des paramètres : router.push({ pathname, params }) ;
  • comprendre que les paramètres d'une adresse sont toujours des chaînes de caractères.

Petite vérification​

Avant de commencer, vérifiez si vous savez...

  1. cliquer sur « Jouer » dans le navigateur et lire l'adresse (route) affichée dans la barre d'adresse
  2. cliquer sur le bouton retour de la barre du haut et dire quel écran s'affiche
  3. dire dans quel fichier est défini le titre « Partie en cours »
📌 Réponse attendue

TP pour réfléchir et résoudre des problèmes​

Votre mission : créer l'écran generateur où le joueur règle sa partie, puis y accéder depuis l'accueil. Pour l'instant, l'écran partie ignore encore ces réglages : vous les exploiterez à la séance suivante.

Étape 1 — Des types pour les réglages​

Dans src/types/quiz.ts, ajoutez les types du générateur : un thème précis ou « tous », un niveau précis ou « tous », et l'ensemble des réglages d'une partie.


Bonne pratique - Une valeur spéciale plutôt qu'un cas à part

Ajouter "tous" à l'union permet de traiter « pas de filtre » avec le même type que les vrais choix, sans variable supplémentaire du genre utiliserFiltre: boolean.

Étape 2 — Étendre la fonction de filtrage​

La fonction filtrerQuestions du tutoriel 2 accepte maintenant "tous" pour le thème ou le niveau.

Remplacez aussi, dans la signature, les types CodeTheme et Niveau par FiltreTheme et FiltreNiveau, et questions: Question[] par questions: readonly Question[] (le tableau n'est pas modifié).


Bonne pratique - Les parenthèses lèvent le doute

Mélanger || et && sans parenthèses donne souvent un résultat inattendu : && est prioritaire sur ||. Avec des parenthèses, la lecture est évidente.

Étape 3 — Tester le filtrage avec « tous »​

Dans tests/filtrerQuestions.test.ts, ajoutez un test. Il utilise des chiffres que vous connaissez : 27 questions au total, 9 par thème, 9 par niveau.

Lancez npm test : tests 30 et pass 30.


Bonne pratique - Tester avec des chiffres connus

Un test qui utilise des valeurs que l'on peut vérifier à la main (27, 9, 9) est facile à relire et à croire.

Étape 4 — Les constantes du générateur​

Créez src/constants/generateur.ts : la liste des nombres de questions proposés et celle des niveaux avec leur libellé.

Au-dessus, ajoutez la ligne export const NOMBRES_QUESTIONS = [3, 5, 10]; et l'importation du type (import type { FiltreNiveau } from "@/types/quiz";).


Bonne pratique - Pas de nombres « magiques » dans les écrans

Les valeurs de réglage sont regroupées dans un fichier de constantes : pour proposer 20 questions, on modifie une ligne à un seul endroit, pas plusieurs écrans.

Étape 5 — Un composant de choix accessible​

Créez src/components/ChoixOption.tsx : une « puce » qui affiche un libellé et indique si elle est sélectionnée.


Bonne pratique - Penser aux lecteurs d'écran

Un utilisateur aveugle ne voit pas la couleur de la puce. accessibilityState lui permet d'entendre « sélectionné » ou « désactivé ». Cela coûte une ligne et rend l'application utilisable par plus de monde.

Étape 6 — L'écran générateur : états et nombre disponible​

Créez src/app/generateur.tsx. Trois useState mémorisent les réglages. Le nombre de questions disponibles n'est pas un état : il se calcule à partir du thème et du niveau.


Bonne pratique - Calculer plutôt que stocker (encore)

C'est la règle vue à la séance sur le score. Si disponibles était un état, il faudrait penser à le mettre à jour à chaque changement de thème ou de niveau, et l'oublier un jour.

Étape 7 — Naviguer avec des paramètres​

Au clic sur « Générer le quiz », on ouvre l'écran partie en lui transmettant les réglages dans l'adresse (par exemple /partie?theme=reseaux&niveau=2&nombre=3).


Bonne pratique - Garder le garde-fou dans le gestionnaire

Le bouton est désactivé quand les réglages sont invalides, mais la fonction vérifie quand même if (!reglagesValides) return;. L'interface n'est jamais l'unique barrière.

Étape 8 — Désactiver les réglages impossibles​

Parmi les nombres proposés (3, 5, 10), ceux qui dépassent le nombre de questions disponibles doivent être grisés. Complétez la puce correspondante.

Essayez : thème « Réseaux et Wi-Fi » et niveau « Expert » : il n'y a que 3 questions, les puces 5 et 10 sont grisées. Vous pouvez même obtenir un message d'erreur et un bouton désactivé si le nombre sélectionné (5 par défaut) est devenu trop grand.


Bonne pratique - Rendre les réglages impossibles... impossibles

Il vaut mieux empêcher une erreur que l'expliquer après coup. Le message « Pas assez de questions » n'est que le filet de sécurité, pour le cas où le nombre déjà sélectionné devient trop grand après un changement de thème.

Étape 9 — Y accéder depuis l'accueil​

Dans src/app/index.tsx, le bouton ouvre maintenant le générateur. Dans _layout.tsx, ajoutez <Stack.Screen name="generateur" options={{ title: "Générateur de quiz" }} /> entre l'accueil et la partie.


Bonne pratique - Vérifier le parcours de bout en bout

Testez le parcours complet : accueil, générateur, puis « Générer le quiz ». Regardez l'adresse dans la barre du navigateur : elle contient vos réglages (?theme=...&niveau=...&nombre=...).

Étape 10 — Enregistrer votre travail​


Bonne pratique - Un commit qui raconte la fonctionnalité

Le message décrit ce que l'utilisateur peut faire désormais, pas les fichiers modifiés.

📌 Une solution

Notions théoriques​

Une valeur spéciale : « tous »​

Le thème d'une question est l'un des trois codes ("mots-de-passe", "hameconnage", "reseaux"). Pour le générateur, il faut un quatrième choix : tous les thèmes. On l'ajoute à l'union :

export type FiltreTheme = CodeTheme | "tous";

La fonction filtrerQuestions traite alors "tous" comme « ne pas filtrer sur ce critère » :

(theme === "tous" || question.theme === theme) && (niveau === "tous" || question.niveau === niveau)

Lisez-la ainsi : « le thème convient si l'on accepte tous les thèmes ou si c'est exactement celui demandé ; et le niveau convient de la même façon ».

Pourquoi pas null ?

On aurait pu utiliser null pour « tous ». La chaîne "tous" a l'avantage de s'écrire telle quelle dans une adresse (?theme=tous) et de se lire facilement dans le code. Plus tard, quand les questions viendront de la base de données, on traduira "tous" en null à la frontière entre l'application et la base.

Un composant de choix : la « puce »​

ChoixOption reçoit un libelle, un booléen estSelectionne, et une prop facultative desactive. Son style est un tableau de styles, dont certains sont conditionnels :

style={[
styles.puce,
estSelectionne && styles.selectionnee,
desactive && styles.desactivee,
]}

Si la condition est fausse, false est ignoré par React Native. Le style de base puce est toujours appliqué ; selectionnee et desactivee s'y ajoutent quand c'est utile.

Calculer le nombre disponible, bloquer les réglages impossibles​

Le joueur peut combiner thème, niveau et nombre. Certaines combinaisons sont impossibles : on ne peut pas demander 10 questions quand il n'y en a que 3. Trois protections :

  1. les puces trop grandes sont désactivées (desactive={n > disponibles}) ;
  2. un message explique le problème si le nombre déjà choisi devient trop grand ;
  3. le bouton « Générer le quiz » est désactivé, et la fonction genererLeQuiz refuse de continuer si reglagesValides est faux.
router.push({
pathname: "/partie",
params: { theme, niveau: String(niveau), nombre: String(nombre) },
});

L'adresse obtenue est /partie?theme=reseaux&niveau=2&nombre=3. Après le point d'interrogation figurent les paramètres (clé=valeur, séparés par &).

Les paramètres sont toujours des chaînes de caractères

Dans une adresse, tout est texte : nombre=3 est la chaîne "3", pas le nombre 3. C'est pourquoi on écrit String(niveau) et String(nombre). Et surtout : n'importe qui peut modifier l'adresse à la main (nombre=999, theme=pirates). Le prochain tutoriel vous apprend à les vérifier avant de les utiliser.


Test de mémorisation/compréhension​


Pourquoi le type FiltreTheme est-il défini comme CodeTheme | "tous" ?


Que fait la condition (theme === "tous" || question.theme === theme) ?


Pourquoi écrit-on String(niveau) dans params ?


Pourquoi disponibles n'est-il pas déclaré avec useState ?


À quoi sert accessibilityState={{ selected: ..., disabled: ... }} ?


Quelle est l'adresse obtenue avec thème reseaux, niveau 2 et 3 questions ?



Exercices complémentaires​

Exercice A : proposer 20 questions​

Ajoutez 20 à NOMBRES_QUESTIONS dans src/constants/generateur.ts. Que se passe-t-il pour la puce « 20 » avec le thème « Tous » et le niveau « Tous » (27 questions) ? Et avec un thème précis ?

Exercice B : un test supplémentaire​

Dans tests/filtrerQuestions.test.ts, ajoutez un test qui vérifie que filtrerQuestions(QUESTIONS_LOCALES, "mots-de-passe", 1) renvoie 3 questions (3 thèmes, 3 niveaux, 3 questions chacun).


Ce qu'il faut retenir​

NotionRésumé
"tous"Valeur spéciale ajoutée à l'union des thèmes et des niveaux : « pas de filtre ».
Valeur calculéeLe nombre de questions disponibles se calcule, il ne se stocke pas.
Réglages impossiblesPuces grisées, message d'erreur, bouton désactivé et garde-fou dans la fonction.
router.push({ pathname, params })Navigue vers une route en lui transmettant des paramètres dans l'adresse.
Paramètres d'URLToujours des chaînes de caractères, et modifiables par n'importe qui.
accessibilityStateInforme les lecteurs d'écran de l'état d'un composant.

Aperçu de la prochaine séance​

Pour l'instant, l'écran partie ne regarde pas les paramètres reçus : il tire toujours 5 questions. Dans la prochaine séance, vous allez lire ces paramètres avec useLocalSearchParams, puis surtout les valider, car n'importe qui peut taper ?nombre=999 ou ?theme=pirates dans la barre d'adresse.