Version: 1.0 Date: Novembre 2024 Application: Progressive Web Application (PWA) de Quiz Nom du projet: CodeMaster (Quiz Master)
CodeMaster est une application web permettant aux utilisateurs de :
- S'entraîner via des quiz sur des sujets techniques (TypeScript, React, Next.js, Node.js, CSS, JavaScript, Entretiens)
- Progresser en suivant leurs statistiques sur 30 jours
- Débloquer des badges pour récompenser les jalons d'apprentissage
- Gérer les données en important des questions depuis des fichiers JSON personnalisés
- Organiser le contenu en créant et en gérant des catégories de questions
Utilisateur final / Apprenant
- Accès unique, pas de système d'authentification
- Toutes les données sont locales (IndexedDB) et non synchronisées
- Une session par appareil / navigateur
- Pas de notion d'utilisateur distinct
-
Système de quiz
- Sélection de catégories
- Choix de difficulté (facile, moyen, difficile, aléatoire)
- Sélection du nombre de questions (5, 10, 20)
- Navigation dans les questions (suivant, passer)
- Réponses aux questions avec feedback explicatif
-
Gestion des catégories et des questions
- Import de questions depuis fichiers JSON
- Création et édition de catégories personnalisées
- Suppression de catégories et questions associées
- Association des questions à des catégories
-
Système de statistiques
- Moyenne des scores (pourcentage)
- Meilleur score atteint
- Streak actuel (jours consécutifs)
- Total de quizzes complétés
- Évolution sur 30 jours (graphique)
-
Système de badges
- 6 badges distincts avec conditions de déblocage
- Notifications lors du déblocage
- Historique du déblocage (date)
-
Persistance des données
- Toutes les données stockées localement dans IndexedDB
- Reprise automatique d'une session non terminée
- Aucun serveur ni synchronisation cloud
| Aspect | Technologie | Version |
|---|---|---|
| Framework | Vue.js | 3.5.22 |
| Language | TypeScript | 5.9 |
| Build | Vite | 7.1.11 |
| State Management | Pinia | 3.0.3 |
| Routing | Vue Router | 4.6.3 |
| Styling | Tailwind CSS | v4 (via @tailwindcss/postcss) |
| Icons | Phosphor Icons | (CDN unpkg) |
| Charts | Chart.js | 4.5.1 |
| Markdown | marked | 17.0.1 |
Pattern: Monolithique frontend-only (aucun serveur backend)
Structure des dossiers:
src/
├── main.ts # Point d'entrée Vue + Pinia + Router
├── App.vue # Root component avec transitions
├── style.css # Tailwind + custom components
├── types/
│ ├── models.ts # Interfaces TypeScript
│ └── constants.ts # Constantes et données par défaut
├── db/
│ ├── config.ts # IndexedDB setup et opérations
│ └── repositories.ts # Repository pattern (CRUD)
├── stores/ # 3 stores Pinia
│ ├── useDataStore.ts # Questions, badges, catégories
│ ├── useQuizStore.ts # Session de quiz active
│ └── useStatsStore.ts # Statistiques et badges
├── router/
│ └── index.ts # Configuration Vue Router (8 routes)
├── views/ # Pages (10 fichiers Vue)
│ ├── quiz/
│ │ ├── Home.vue
│ │ ├── Difficulty.vue
│ │ ├── Count.vue
│ │ ├── RandomConfig.vue
│ │ ├── Active.vue
│ │ └── Summary.vue
│ ├── stats/
│ │ └── Index.vue
│ └── settings/
│ ├── Import.vue
│ ├── SelectCategory.vue
│ ├── Categories.vue
│ └── CategoryEdit.vue
├── components/ # Composants réutilisables
│ ├── layout/
│ │ ├── AppHeader.vue # En-tête + navigation
│ │ └── AppLayout.vue # Layout principal
│ ├── quiz/
│ │ ├── QuestionCard.vue # Affichage question
│ │ ├── AnswerOption.vue # Bouton réponse
│ │ └── ProgressBar.vue # Barre de progression
│ ├── stats/
│ │ ├── StatCard.vue # Carte KPI
│ │ ├── EvolutionChart.vue # Graphique 30j
│ │ └── BadgesGrid.vue # Grille de badges
│ └── common/
│ ├── BaseButton.vue # Bouton réutilisable
│ ├── BaseModal.vue # Modal
│ ├── LoadingSpinner.vue # Spinner
│ └── MarkdownText.vue # Rendu Markdown
└── fixtures/
└── questions.ts # Données de test (30 questions)
Base de données: quiz-master-db (version 3)
Stores (Object Stores) et indices:
| Store | Key Path | Indices | Rôle |
|---|---|---|---|
questions |
id |
countApparition (non-unique) |
Questions et métadonnées |
sessions |
sessionId |
dateFin (non-unique) |
Historique des quiz |
meta |
id |
aucun | Badges, métadonnées globales |
categories |
id |
label (unique) |
Catégories de questions |
Schéma relationnel:
- Une
Questionappartient à uneCategory(viacategorie: string= category ID) - Une
QuizSessioncontient plusieursSessionQuestion(questions modifiées pour le quiz) - Un
Badgeest indépendant, lié auxQuizSessionvia logique métier
Authentification: Aucune (app locale)
Formulaires: Vue standard (réactivité via ref())
Data fetching: IndexedDB direct (repositories pattern)
Validation: Logique imperméative dans les stores et composants
Routing: Vue Router 4 avec navigation par nom de route
Internationalisation: Aucune (application en français)
Tests:
- Unitaires: Vitest 3.2.4 + @vue/test-utils 2.4.6
- E2E: Playwright 1.56.1
Toutes les routes sont imbriquées sous / avec AppLayout comme composant parent.
/
├── /home → HomeView
├── /quiz/difficulty → DifficultyView
├── /quiz/count → CountView
├── /quiz/randomconfig → RandomConfigView
├── /quiz/active → QuizActiveView
├── /quiz/summary → SummaryView
├── /stats → StatsView
├── /settings/import → ImportView
├── /settings/select-category → SelectCategoryView
├── /settings/categories → CategoriesView
└── /settings/categories/edit → CategoryEditView (create ou edit mode)
Redirection: / → /home
Composant: src/views/quiz/Home.vue
Rôle: Page d'accueil et sélection de catégorie pour un quiz
Utilisateurs autorisés: Tous
Actions principales:
- Afficher la liste des catégories disposant de questions
- Permettre la sélection d'une catégorie → parcours
Difficulté → Nombre de questions → Quiz - Ouvrir le mode aléatoire (multi-catégories) → parcours
RandomConfig → Difficulté → Nombre de questions → Quiz - Naviguer vers l'import de données si aucune catégorie disponible
Données affichées:
- Grille de catégories (labels, icônes colorées)
- Bouton "Mode Aléatoire"
- Alerte si aucune catégorie disponible
- Lien d'accès à l'import de données
API / Stores:
useDataStore:questions,allCategories,reloadQuestions()useQuizStore:selectCategory(),openRandomConfig()
Navigation sortante:
selectCategory(label)→/quiz/difficultyopenRandomConfig()→/quiz/randomconfiggoToImport()→/settings/import
Composant: src/views/quiz/Difficulty.vue
Rôle: Sélection du niveau de difficulté
Utilisateurs autorisés: Tous
Actions principales:
- Afficher 4 boutons (Facile, Moyen, Difficile, Aléatoire)
- Enregistrer la sélection dans le store
- Naviguer vers la sélection du nombre de questions
Données affichées:
- Boutons avec labels de difficulté
- Description des points accordés par niveau
API / Stores:
useQuizStore:selectDifficulty(difficulty)
Navigation sortante:
- Après sélection →
/quiz/count
Composant: src/views/quiz/Count.vue
Rôle: Sélection du nombre de questions pour le quiz
Utilisateurs autorisés: Tous
Actions principales:
- Afficher 3 options (5, 10, 20 questions)
- Créer une session de quiz avec :
- Catégories sélectionnées (Home ou RandomConfig)
- Difficulté sélectionnée (Difficulty)
- Nombre de questions choisi
- Naviguer vers le quiz actif
Données affichées:
- 3 boutons de sélection (5/10/20)
API / Stores:
useQuizStore:createQuizSession(categories, difficulty, count)
Navigation sortante:
- Après création →
/quiz/active
Composant: src/views/quiz/RandomConfig.vue
Rôle: Configuration du mode aléatoire (sélection multi-catégories)
Utilisateurs autorisés: Tous
Actions principales:
- Afficher toutes les catégories avec questions
- Multi-sélection de catégories (checkboxes)
- Valider la sélection (minimum 1 catégorie)
- Enregistrer la sélection
- Naviguer vers le choix de difficulté
Données affichées:
- Liste de catégories sélectionnables
- Nombre de questions par catégorie (optionnel)
API / Stores:
useDataStore:allCategories,questionsuseQuizStore:randomCategoriesSelection,validateRandomSelection()
Validation:
- Au moins 1 catégorie doit être sélectionnée
Navigation sortante:
- Après validation →
/quiz/difficulty
Composant: src/views/quiz/Active.vue
Rôle: Interface de quiz en cours (page principale de jeu)
Utilisateurs autorisés: Tous (si session active)
Actions principales:
- Afficher la question courante avec :
- Énoncé (peut contenir du Markdown)
- Catégorie et difficulté de la question
- 4 réponses mélangées
- Numéro de la question et progression
- Permettre à l'utilisateur de :
- Sélectionner une réponse → valider (
submitAnswer()) - Passer la question (
skipQuestion())
- Sélectionner une réponse → valider (
- Afficher l'explication après réponse
- Progresser vers la question suivante ou terminer le quiz
Données affichées:
- Barre de progression (%)
- Numéro question / total
- Texte de la question
- 4 boutons réponse
- Badge difficulté et catégorie
- Explication (après réponse)
API / Stores:
useQuizStore:activeSession,currentQuestion,selectedAnswerIndex,hasAnsweredsubmitAnswer(index),skipQuestion(),nextQuestion()progressPercent,isLastQuestion,isQuizFinished
Comportements métier:
- Les réponses sont mélangées aléatoirement à chaque question (
ordreReponses) - Les réponses correctes incrémentent
countBonneReponsede la question originale - Les apparitions incrémentent
countApparition - Si la dernière question → appeler
finishQuiz()
Navigation sortante:
- Après quiz terminé →
/quiz/summary
Composant: src/views/quiz/Summary.vue
Rôle: Résumé et résultats du quiz
Utilisateurs autorisés: Tous (après avoir terminé un quiz)
Actions principales:
- Afficher le score obtenu (%)
- Afficher le nombre de bonnes réponses
- Comparer avec la moyenne globale
- Afficher le streak courant
- Afficher les badges nouvellement débloqués
- Permettre de :
- Retourner à l'accueil
- Refaire le même quiz (replay)
Données affichées:
- Score en cercle de progression (0-100%)
- Bonnes réponses / total
- Moyenne globale et comparaison
- Streak actuel
- Badges débloqués (nom, icône, description)
- Message personnalisé selon performance
API / Stores:
useQuizStore:activeSession,getReplayParams()useStatsStore:globalStats,newlyUnlockedBadges,loadStats()
Calculs métier:
- Score en % = (correct answers / total questions) × 100
- Score pondéré = somme des points selon difficulté des bonnes réponses
- Comparaison à la moyenne globale
Navigation sortante:
goHome()→/homereplayQuiz()→/quiz/countavec paramètres précédents
Composant: src/views/stats/Index.vue
Rôle: Tableau de bord statistiques
Utilisateurs autorisés: Tous
Actions principales:
- Afficher 4 KPI cards :
- Moyenne globale (%)
- Meilleur score (%)
- Streak actuel (jours)
- Total de quizzes complétés
- Afficher graphique d'évolution 30 jours (moyenne quotidienne)
- Afficher grille de badges (vérouillés / débloqués)
- Afficher détails badge au clic (modal)
Données affichées:
- 4 cartes de statistiques
- Graphique Chart.js (30 jours)
- Grille 3 colonnes de badges
- Détails badge au hover/clic
API / Stores:
useStatsStore:globalStats(moyenne, meilleur score, streak, sessions)calculateDailyAverages(sessions)pour le graphique
useDataStore:badges
Calculs métier:
- Moyenne globale = moyenne de tous les scores en %
- Meilleur score = max des scores
- Streak = jours consécutifs avec au moins 1 quiz
- Historique 30 jours = groupé par date
Navigation sortante:
- Aucune (page de consultation)
Composant: src/views/settings/Import.vue
Rôle: Gestion des données et import de questions
Utilisateurs autorisés: Tous
Actions principales:
- Afficher liste des catégories pré-chargées avec statut :
- "Non chargé", "En cours", "Chargé", "Erreur"
- Charger catégories individuelles (via fichiers JSON internes)
- Charger toutes les catégories (avec progression globale)
- Réessayer une catégorie échouée
- Upload personnalisé : sélectionner fichier JSON →
SelectCategory - Réinitialiser les statistiques (danger zone)
- Annuler opération en cours
Données affichées:
- Tableau / liste des catégories avec statut de chargement
- Barre de progression (par catégorie et globale)
- Boutons d'action
- Zone de danger (reset stats)
API / Stores:
useDataStore:importQuestions(),reloadQuestions()useStatsStore: pour reset statssessionRepository,questionRepositorypour réinitialisation
Comportements métier:
- Les fichiers JSON doivent être dans
questions/du repo (pré-chargés) - Format JSON attendu : array de { intitule, reponses[], indexBonneReponse, difficulte, explication?, categorie? }
- Upload personnalisé stocke en
sessionStoragepuis bascule versSelectCategory - Reset stats réinitialise les sessions mais garde les questions
Navigation sortante:
- Upload →
/settings/select-category(via sessionStorage)
Composant: src/views/settings/SelectCategory.vue
Rôle: Sélection ou création de catégorie pour import personnalisé
Utilisateurs autorisés: Tous (après upload)
Actions principales:
- Afficher catégories existantes (sélection)
- Formulaire pour créer nouvelle catégorie :
- Label (texte, validation d'unicité)
- Icône (24 options)
- Couleur (14 options)
- Valider et importer les questions vers la catégorie sélectionnée
- Annuler (retour)
Données affichées:
- Liste des catégories existantes (boutons radio)
- Formulaire de création catégorie
- Sélection icône (grid 24 icônes)
- Sélection couleur (grid 14 couleurs)
API / Stores:
useDataStore:allCategories,getCategoryByLabel()addCategory(),importQuestions(category, json)
sessionStoragepour récupérer le JSON upload
Validation:
- Label requis et unique
- Icône requise
- Couleur requise
Navigation sortante:
- Après succès →
/home(après 2s) - Annuler →
router.back()
Composant: src/views/settings/Categories.vue
Rôle: Liste et gestion des catégories
Utilisateurs autorisés: Tous
Actions principales:
- Afficher liste de toutes les catégories avec nombre de questions
- Créer nouvelle catégorie (FAB → CategoryEdit)
- Éditer catégorie existante (clic → CategoryEdit)
- Supprimer catégorie (avec confirmation) + cascader suppression questions
Données affichées:
- Tableau / liste des catégories
- Nombre de questions par catégorie
- Bouton FAB création
- Menu/boutons édition/suppression
API / Stores:
useDataStore:allCategories,questionsdeleteCategory(id)(cascading)
Navigation sortante:
- Créer →
/settings/categories/edit(sans params) - Éditer →
/settings/categories/edit?id=<categoryId> - Après action → reste sur même page
Composant: src/views/settings/CategoryEdit.vue
Rôle: Formulaire création/édition de catégorie
Utilisateurs autorisés: Tous
Actions principales:
- Détecter mode (création vs édition) via route query param
id - En mode édition : pré-remplir le formulaire
- Afficher formulaire :
- Label (input text avec validation d'unicité)
- Icône (grid de 24 icônes sélectionnables)
- Couleur (grid de 14 couleurs sélectionnables)
- Valider et sauvegarder (création ou mise à jour)
- En cas de changement de label → mettre à jour toutes les questions associées
- Annuler → retour à la liste
Données affichées:
- Formulaire avec 3 champs
- Grids de sélection (icônes, couleurs)
- Boutons Save/Cancel
API / Stores:
useDataStore:allCategoriesaddCategory(),updateCategory()- Logique de cascade pour label changement
Validation:
- Label requis, unique (sauf si édition de la même catégorie)
- Icône requise
- Couleur requise
Erreurs métier:
- Doublon de label → message d'erreur
Navigation sortante:
- Après succès →
/settings/categories - Annuler →
/settings/categories
interface Category {
id: string // Unique ID (ex: 'cat_typescript')
label: string // Nom affiché (ex: 'TypeScript')
icon: string // Nom icône Phosphor (ex: 'Code')
color: TailwindColor // Couleur Tailwind (ex: 'blue')
}Rôle: Organiseur de questions. Les catégories peuvent être modifiées et supprimées. Chaque question apartient à une catégorie.
7 catégories pré-définies:
- TypeScript (blue)
- React (cyan)
- Next.js (slate)
- Node.js (green)
- CSS (purple)
- JavaScript (yellow)
- Entretiens (indigo)
interface Question {
id: string // Unique ID (ex: 'q-1', 'imported-123-0')
intitule: string // Énoncé (peut avoir du Markdown)
reponses: string[] // [answer0, answer1, answer2, answer3]
indexBonneReponse: number // Index de la bonne réponse (0-3)
explication: string // Texte d'explication (Markdown)
categorie: string // ID de la catégorie
difficulte: 'facile' | 'moyen' | 'difficile'
countApparition: number // Nombre de fois affichée
countBonneReponse: number // Nombre de fois correctement répondue
}Rôle: Donnée brute d'une question de quiz. Immuable après ajout au système (sauf métadonnées de comptage).
Points par difficulté:
- Facile (1 point)
- Moyen (2 points)
- Difficile (3 points)
Importation:
- Via JSON array (propriétés requises: intitule, reponses, indexBonneReponse, difficulte)
- Propriétés optionnelles: id, explication, categorie
- Validation au import avec messages d'erreur clairs
interface SessionQuestion extends Question {
ordreReponses: number[] // [0, 2, 3, 1] - réponses mélangées
estSkippe: boolean // true si l'utilisateur a passé
estCorrecte: boolean | null // true/false après réponse, null avant
}Rôle: Snapshot de Question lors d'un quiz, avec état de réponse et ordre mélangé.
interface QuizSession {
sessionId: string // UUID unique
dateDebut: string // ISO string
dateFin: string | null // ISO string (null si en cours)
questions: SessionQuestion[] // Array des questions
indexQuestionCourante: number
nbQuestions: number
scorePondere: number // Somme des points
scorePondereMax: number // Max possible
notePourcentage: number // 0-100
difficulteChoisie: Difficulty
categories: string[] // Labels des catégories
dateJour?: string // Format YYYY-MM-DD pour stats
}Rôle: Session de quiz, persistée en IndexedDB. Peut être en cours ou complétée.
Cycle de vie:
- Création (dateFin = null)
- Progression (nextQuestion())
- Terminaison (finishQuiz() → dateFin défini, scores calculés)
- Suppression (abandon)
interface Badge {
id: string // Unique ID (ex: 'first_quiz', 'perfect_score')
nom: string // Nom (ex: 'Premier Pas')
description: string // Description du but
statut: 'verrouille' | 'debloque'
icon?: string // Emoji (ex: '🐣')
dateDebloque?: string | null // ISO string du déblocage
}Rôle: Récompenses pour jalons d'apprentissage.
6 badges pré-définis:
| ID | Nom | Description | Condition |
|---|---|---|---|
| first_quiz | Premier Pas | Terminer un premier quiz | Complétée ≥ 1 session |
| perfect_score | Perfection | Obtenir 100% à un quiz | Score = 100% sur une session |
| streak_3 | Habitué | 3 jours de suite | Streak ≥ 3 jours |
| streak_7 | Accro | 7 jours de suite | Streak ≥ 7 jours |
| marathon | Marathonien | Faire 20 quiz au total | Total complétées ≥ 20 |
| math_expert | Boss des Maths | 5 quiz de Maths terminés | 5 sessions monocatégorie 'Maths' |
Règle métier: Un badge verrouillé devient débloqué après qu'une condition soit remplie. Permanent (ne peut pas se re-verrouiller).
interface GlobalStats {
moyenneGlobale: number // Moyenne des scores (%)
meilleurScore: number // Max score (%)
streakActuel: number // Jours consécutifs
totalSessions: number // Sessions complétées
historiqueSessions: QuizSession[] // Toutes sessions
}Rôle: Statistiques agrégées globales.
- Streak = nombre de jours consécutifs avec au moins 1 quiz complété
- Se réinitialise à 0 si > 1 jour sans quiz
- Calculé en comparant les dateJour des sessions
- Utilisé pour les badges streak_3 et streak_7
- Score en % = (nombre de bonnes réponses / total questions) × 100
- Score pondéré = Σ(points_difficulte_si_correct)
Où: facile=1, moyen=2, difficile=3
- Max score pondéré = Σ(points_difficulte_toutes_questions)
À la création de la session:
- Générer ordreReponses = [0, 1, 2, 3] mélangé aléatoirement
- Afficher reponses[ordreReponses[0]], [ordreReponses[1]], etc.
- Comparer l'index sélectionné avec indexBonneReponse (original)
1. Filtrer par catégories sélectionnées
2. Filtrer par difficulté (sauf si 'random')
3. Trier par countApparition (ascending) + aléatoire pour égalité
4. Prendre les N premières
5. Lancer exception si < N questions disponibles
Après chaque réponse:
- Incrémenter countApparition
- Si correcte: incrémenter countBonneReponse
- Persister en IndexedDB
Après finishQuiz():
- Vérifier first_quiz: totalSessions >= 1
- Vérifier perfect_score: score == 100%
- Vérifier streak_3/7: calculer streak
- Vérifier marathon: totalSessions >= 20
- Vérifier math_expert: 5+ sessions monocatégorie 'Maths'
- Changer statut 'verrouille' → 'debloque' pour nouveaux
- Enregistrer dateDebloque = maintenant
- Ajouter à newlyUnlockedBadges pour notification
Au mount de App.vue:
- Rechercher QuizSession avec dateFin == null
- Si trouvée: afficher modal
- "Reprendre" → router.push('/quiz/active')
- "Abandonner" → sessionRepository.delete(sessionId)
Organisation par domaine fonctionnel:
layout/- Structure principale (AppHeader, AppLayout)quiz/- Composants de quiz (Questions, réponses, progression)stats/- Affichage statistiques (KPI cards, graphiques, badges)settings/- Gestion catégories et import (formulaires, listes)common/- Composants réutilisables (modal, bouton, spinner)
Total: 13 composants (6 pages principales via views/, 13 composants réutilisables)
Rôle: Wrapper principal de l'application
Props: Aucune
Enfants:
- AppHeader (header fixe)
- router-view (contenu principal)
- BaseModal (reprise de session)
Comportements:
- Affiche modal de reprise si
quizStore.showResumeModalest vrai - Boutons actions:
- Reprendre →
resumePreviousSession()+ navigate - Abandonner →
abandonSession()
- Reprendre →
Rôle: En-tête de navigation
Props: Aucune
Affichage:
- Logo + titre "CodeMaster" (cliquable → home)
- Bouton settings (icône →
/settings/categories) - Bouton stats (icône + badge notification) →
/stats
Comportements:
- Indicateur visuel (badge rouge) si badges non lus (
badgesNonLusdu statsStore)
Props:
interface Props {
question: SessionQuestion
questionNumber: number
totalQuestions: number
selectedAnswerIndex: number | null
hasAnswered: boolean
}Emits:
answer-selected(answerIndex: number)
Affichage:
- Numéro question / total
- Badges catégorie et difficulté
- Texte de la question (avec Markdown)
- 4 boutons AnswerOption
- Explication (après réponse)
Comportements:
- Affiche explication seulement après réponse (
hasAnswered && !estSkippe) - Désactive les boutons après réponse
- Les réponses sont dans
question.ordreReponses(mélangées)
Props:
interface Props {
text: string
isCorrect: boolean
isSelected: boolean
hasAnswered: boolean
disabled: boolean
}Emits:
click()- click du bouton
Affichage:
- Bouton avec texte réponse
- Classes dynamiques selon état :
- Normal (enabled, non-répondu)
- Correct (vert si sélectionné et correct)
- Incorrect (rouge si sélectionné et faux)
- Correct-non-sélectionné (vert faible si pas sélectionné mais correct)
Props:
interface Props {
progress: number // 0-100
}Affichage:
- Barre de progression horizontale
- Pourcentage du côté
Props:
interface Props {
label: string
value: number | string
icon: string
color?: string
}Affichage:
- Carte avec label, icône et valeur
- Couleur de fond selon type
Props:
interface Props {
data: Record<string, { sum: number; count: number }>
}Comportement:
- Graphique Chart.js linéaire
- X-axis: 30 jours passés
- Y-axis: moyenne score (%)
- Montre l'évolution quotidienne
Props:
interface Props {
badges: Badge[]
}Affichage:
- Grille 3 colonnes
- Chaque badge affiche :
- Icône emoji
- Nom
- Statut (verrouillé/débloqué)
- Date déblocage (si applicable)
- Description au hover
Props:
interface Props {
variant?: 'primary' | 'secondary' | 'danger' | 'ghost'
size?: 'sm' | 'md' | 'lg'
disabled?: boolean
loading?: boolean
fullWidth?: boolean
type?: 'button' | 'submit' | 'reset'
}Défauts: variant='primary', size='md', disabled=false, loading=false, fullWidth=false, type='button'
Slots:
- default - contenu du bouton
Comportements:
- Affiche spinner (⌛) si
loading - Disabled opacity et cursor si
disabledouloading - Active scale 95% au clic
- 4 variants pré-définis:
primary: Indigo bg, white text, hover darker, shadowsecondary: Light slate bg, slate text, hover darkerdanger: Light red bg, red text, border rougeghost: No bg, slate text, hover light slate bg
- 3 tailles avec padding différent
- Optional fullWidth utilise w-full
Props:
interface Props {
title: string // Modal title header
show: boolean // Controls visibility (v-if)
}Slots:
default- contenu du modal (body)actions- boutons d'action (footer)
Comportement:
- Conditional rendering: affiche seulement si
showest vrai - Overlay full-screen avec backdrop blur (position absolute z-50)
- Centré au milieu de l'écran
- Titre en header distinct
- Actions dans footer (template slot)
- Responsive padding
- Box shadow d'élévation
Comportement:
- Composant pur (aucune prop, aucun état)
- Affiche spinner SVG centré
- Animation rotation continue
Props:
interface Props {
text: string // Raw markdown text to render
}Comportement:
-
Parsing Markdown:
- Utilise
markedlibrary v10+ avec options:breaks: true- convertit sauts de ligne en<br>gfm: true- GitHub Flavored Markdown
- Watcher sur
textprop → async parsing - Gestion erreurs: fallback au texte brut
- Utilise
-
Optimisations:
- Retire
<p>tags wrapper si présents (inline rendering) - Gère texte vide sans erreur
- Retire
-
Formatage HTML:
- Code inline: fond gris clair, monospace, petit
- Code blocks: scrollable, monospace, indentation
- Strong: bold, indigo color
- Em: italic
- Support: headings, listes, liens, tables
-
Sécurité:
- Utilise
v-html(marked escaped par défaut) - Pas de risque XSS si source de confiance
- Utilise
Side Effects:
- Render HTML via
v-htmldirective
Props:
interface Props {
categorie: Category | null // Category to edit (null = create new)
allCategories: Category[] // All existing for validation
}Emits:
submit: [category: Category]- Form soumis avec catégorie validecancel: []- Utilisateur annule
Données locales:
form: ref<{ label: string; icon: string; color: string }>
errors: ref<Record<string, string>>Fonctionnalités:
-
Initialisation:
- Watcher sur
categorieprop - Mode édition: pré-remplir form avec données existantes
- Mode création: form vide (defaults: icon='Code', color='blue')
- Watcher sur
-
Sélection d'icône:
- 24 icônes disponibles: Code, Rocket, Cpu, Palette, Database, Chat, Calculator, Microscope, Globe, Lightning, Book, Moon, Bug, Wine, Sparkle, Lightbulb, Gear, Wrench, Hammer, Square, Star, Heart, Flag, Target
- Grid display avec sélection
-
Sélection de couleur:
- 14 couleurs Tailwind: slate, red, orange, amber, yellow, lime, green, emerald, teal, cyan, blue, indigo, purple, pink
- Grid display avec sélection
-
Validation:
- Label: requis, non-vide après trim
- Label unique: vérifier contre toutes catégories (sauf la courante en édition)
- Icon: requis
- Color: requis
- Messages d'erreur affichés sous champs
-
Soumission:
- Valide avant émission
- Génère ID pour nouvelles catégories:
cat_${timestamp} - Émet
Categorycomplète
Dépendances:
- Vue composition API (ref, watch, computed, toRaw)
- Type Category
Props:
interface Props {
categories: Category[] // Categories to list
questions: Question[] // For counting per category
}Emits:
category-click: [categoryId: string]- Row cliquédelete: [categoryId: string]- Delete confirmé (après swipe)
Fonctionnalités:
-
Swipe-to-Delete (mobile/touch):
handleTouchStart(categoryId): Enregistre position X initialehandleTouchMove(categoryId, e): Calcule distance swipe, limite à -80px maxhandleTouchEnd(categoryId): Snap behavior- Si > 60px swiped: lock à -80px (reveal delete button)
- Sinon: reset à 0
- Hints pour swipe affichés sur mobile
-
Affichage catégories:
- Icône avec badge fond coloré
- Label de catégorie
- Nombre de questions calculé dynamiquement
- Chevron indicator
-
États vides:
- Message si aucune catégorie
-
Fonction helper:
getQuestionCountForCategory(categoryId: string): number // Compte questions avec categorie === categoryId
-
Mappages couleurs:
- Full-color: 500 intensity
- Light badges: 100 intensity
- 14 couleurs supportées
Dépendances:
- Vue composition API (ref, computed)
- Touch events API
Props:
interface Props {
isOpen: boolean // Modal visibility
categories: Category[] // Existing categories
}Emits:
select: [categoryLabel: string]- Catégorie sélectionnéecancel: []- Utilisateur annule
Données locales:
selectedCategory: ref<string>('') // Selected label
newCategory: ref({ // New category form
label: string
icon: string // default: 'Code'
color: string // default: 'blue'
})Fonctionnalités:
-
Mode Sélection (existantes):
- Boutons pour chaque catégorie existante
- Click → sélectionne label
-
Mode Création (nouvelle):
- Formulaire label + icon + color picker
- Validation: label non-vide requis
createAndSelect(): valide, définit comme sélectionnée, reset form- Mêmes 24 icônes et 14 couleurs que FormCategorie
-
Soumission:
handleSelect(): émet label sélectionné (existing ou new)- Bouton disabled si rien sélectionné
Dépendances:
- Vue composition API (ref, computed)
- Type Category
Props:
interface Props {
label: string // Card title (uppercase)
value: string | number // Main value to display
icon?: string // Optional Phosphor icon name
color?: 'primary' | 'green' | 'orange' | 'slate' // Default: 'primary'
}Comportement:
- Affiche label (petit, uppercase, espaced)
- Affiche icône + valeur (gros)
- Couleurs pré-définies:
primary: indigo-600green: green-600orange: orange-500slate: slate-700
- Fond blanc, ombre, border subtle
Utilisation dans Stats page:
// 4 cards pour KPIs
;[
{ label: 'Moyenne', value: globalStats.moyenneGlobale.toFixed(1) + '%', color: 'primary' },
{ label: 'Meilleur Score', value: globalStats.meilleurScore.toFixed(1) + '%', color: 'green' },
{ label: 'Streak', value: globalStats.streakActuel + 'j', color: 'orange' },
{ label: 'Total Quizzes', value: globalStats.totalSessions, color: 'slate' },
]Props:
interface Props {
sessions: QuizSession[] // Sessions to chart
}Comportement:
-
Lifecycle:
onMounted: AppellerenderChart()aprèsnextTick()onUnmounted: Destroy chart instance, cleanup
-
Préparation données:
- Initialise map 30 jours avec
{ sum: 0, count: 0 } - Agrège sessions par
dateJour(YYYY-MM-DD) - Calcule moyenne quotidienne:
sum notePourcentage / count - Remplit labels dates (MM-DD format)
- Initialise map 30 jours avec
-
Rendu Chart.js:
- Type: Line chart
- X-axis: Derniers 30 jours (MM-DD format)
- Y-axis: Pourcentage 0-100
- Ligne indigo (#4F46E5) avec area semi-transparente
- Tension 0.3 (smooth curves)
- Fill: true
- spanGaps: true (relie à travers jours manquants)
- Responsive: true
- Aspect ratio: auto
- Legend: hidden
-
État vide:
- Message si pas de sessions
Canvas Reference:
- HTML element avec ID
evolutionChart - Chart.js référence cet ID
Dépendances:
- Chart.js library
- Vue lifecycle (onMounted, onUnmounted, nextTick)
Props:
interface Props {
badges: Badge[] // All badges to display
}Emits:
badge-click: [badge: Badge]- Badge cliqué
Fonctionnalités:
-
Affichage grille:
- 3 colonnes responsive
- Carré aspect ratio pour chaque badge
- Contenu: emoji icon + nom
-
États badge:
- Débloqué (
statut === 'debloque'):- Fond blanc, border jaune
- Opacité 100%, sans grayscale
- Hover effects, clickable
- Verrouillé (
statut === 'verrouille'):- Fond gris sombre, opacité réduite (60%)
- Grayscale filter
- Non-clickable
- Débloqué (
-
Contenu:
- Icône emoji:
badge.icon(ex: '🐣', '🎯', '🔥') - Nom:
badge.nom(max 2 lignes avec ellipsis) - Date déblocage: affichée si disponible
- Icône emoji:
Intégration Stats page:
- Crée BadgesGrid avec
dataStore.badges - Écoute
badge-clickpour afficher détails (modal optionnel)
Props:
interface Props {
text: string // Answer text
isCorrect: boolean | null // null = unanswered, true/false = answered
isSelected: boolean // User selected this option
hasAnswered: boolean // Quiz state: answered
disabled: boolean // Button disabled
}Emits:
click: []- Bouton cliqué
Comportement dynamique:
| État | Classe | Icône |
|---|---|---|
| Avant réponse | White border, hover | None |
| Sélectionné correct | Green bg | ✓ check-circle |
| Sélectionné incorrect | Red bg reduced opacity | ✗ x-circle |
| Non-sélectionné correct (après) | Green tint light | - |
| Non-sélectionné incorrect | Gray out | - |
Interactions:
- Cliquable tant que
!disabled - Transitions smooth entre états
- Feedback visuel clair pour correct/incorrect
Props:
interface Props {
progress: number // 0-100 percentage
}Affichage:
- Barre horizontale remplie progressivement
- Pourcentage affiché à côté ou dans la barre
- Couleur indigo
Props:
interface Props {
question: SessionQuestion
questionNumber: number
totalQuestions: number
selectedAnswerIndex: number | null
hasAnswered: boolean
}Emits:
answer-selected: [answerIndex: number]
Sections affichées:
-
Header question:
- Numéro: "Question X/Y"
- Badge catégorie: Affiche label + couleur category
- Badge difficulté: Affiche niveau (facile/moyen/difficile)
-
Texte question:
- Rendu avec MarkdownText (support Markdown complet)
- Font grande et lisible
-
4 réponses:
- Appelle AnswerOption pour chaque réponse
- Indice affiché =
question.ordreReponses[idx] - Index sélectionné comparé à
question.indexBonneReponse - Disabled après réponse
-
Explication (conditionnelle):
- Affichée seulement si
hasAnswered && !question.estSkippe - Fond bleu clair, border bleu
- Titre "Explication" avec icône
- Rendu avec MarkdownText
- Affichée seulement si
Logique réponses mélangées:
// Affichage dans template:
v-for="(answerIndex, idx) in question.ordreReponses"
// answerIndex = 0,1,2,3 (original index dans reponses[])
:text="question.reponses[answerIndex]"
:is-correct="answerIndex === question.indexBonneReponse"
// Comparaison après réponse:
selectedAnswerIndex === question.indexBonneReponse → correctRôle: Gestion persistante de l'état de chargement des catégories via localStorage
Interface de données:
interface LoadedCategory {
categoryId: string // UUID de la catégorie (ex: 'cat_react')
categoryLabel: string // Label affichable
loaded: boolean // État chargement
questionCount: number // Total questions chargées
loadedAt?: string // ISO timestamp du chargement
error?: string // Message d'erreur si échec
}Fonctions principales:
-
getLoadedCategoriesState(): Record<string, LoadedCategory>
- Récupère depuis localStorage clé
'quiz-master-loaded-categories' - Retourne état initial si pas trouvé
- Récupère depuis localStorage clé
-
getInitialState(): Record<string, LoadedCategory>
- Crée état par défaut pour 7 catégories pré-définies
- Mapping: react → cat_react, typescript → cat_typescript, etc.
- Initial:
loaded: false, questionCount: 0pour chaque
-
saveLoadedCategoriesState(state: Record<string, LoadedCategory>)
- Persiste en localStorage
- Try-catch pour erreurs
-
markCategoryAsLoaded(categoryLabel: string, count: number)
- Met à jour catégorie à
loaded: true - Enregistre
questionCountetloadedAt - Lance erreur si catégorie non trouvée
- Met à jour catégorie à
-
markCategoryAsError(categoryLabel: string, error: string)
- Enregistre message d'erreur
- Préserve autres champs
-
getLoadedCategoryCount(): number
- Compte catégories avec
loaded: true
- Compte catégories avec
-
getTotalQuestionsLoaded(): number
- Somme des
questionCounttoutes catégories
- Somme des
Persistance:
- localStorage key:
'quiz-master-loaded-categories' - Format JSON sérialisé
- Survit aux fermetures/rechargements
Erreurs: Try-catch silencieux, fallback à initial state
Rôle: Chargement asynchrone de fichiers JSON contenant questions depuis public/questions/
Mapping catégories → fichiers:
const CATEGORY_FILE_MAPPING = {
react: 'cat_react',
typescript: 'cat_typescript',
nodejs: 'cat_nodejs',
nextjs: 'cat_nextjs',
css: 'cat_css',
javascript: 'cat_javascript',
entretiens: 'cat_entretiens',
}Type de callback:
type ProgressCallback = (loaded: number, total: number) => void
// Appelé pour chaque question chargée (pour barre de progression)Fonctions principales:
-
normalizeCategoryName(input: string): string
- Normalise noms de catégories vers IDs
- Lowercases + trim
- Variations gérées: "Node.js" → "nodejs", "Entretien" → "entretiens", etc.
- Mapping 13+ variations
- Fallback:
cat_${normalized} - Exemple:
normalizeCategoryName('React') → 'cat_react' normalizeCategoryName('Node.js') → 'cat_nodejs'
-
async loadQuestionsFromJsonFile(category: string, onProgress?: ProgressCallback): Promise<Question[]>
- Charge JSON depuis
/questions/{category}.json - Normalise chaque question:
- Ajoute defaults:
explication: '',countApparition: 0,countBonneReponse: 0 - Convertit difficulte (excluant 'random')
- Normalise categorie via
normalizeCategoryName()
- Ajoute defaults:
- Appelle
onProgress(index, total)pour chaque question - Retourne array normalisé
- Fetch 404 → empty array (warning log)
- Fetch erreur → throw (error log)
- Extensive debug logging
- Charge JSON depuis
-
async loadAllQuestionsFromJsonParallel(onProgress?: ProgressCallback): Promise<Question[]>
- Charge toutes catégories en parallèle via
Promise.all() - Flatten résultats
- Log total questions loaded
- Appelle
onProgresspour chaque question
- Charge toutes catégories en parallèle via
Gestion erreurs:
- HTTP 404 → logged as warning, empty array retourné
- Network errors → thrown, propagé à caller
- Progress callback errors → propagated
Dépendances:
- Fetch API (natif browser)
- Question type
Rôle: Questions de test/fixture pour développement (20 questions)
Export: ADDITIONAL_QUESTIONS: Question[]
Contenu par catégorie (5 questions chaque):
-
Maths (IDs 11-15):
- 15 × 12 = ? (moyen)
- 7³ = ? (difficile)
- Nombre premier (moyen)
- Côtés d'hexagone (facile)
- 45/180 en % (moyen)
-
Géographie (IDs 16-20):
- Plus haute montagne (facile)
- Plus grand océan (moyen)
- Capitale du Japon (facile)
- Pays avec plus d'îles (difficile)
- Plus grand désert (moyen)
-
Science (IDs 21-25):
- Symbole du fer (moyen)
- Chromosomes humains (difficile)
- Gaz atmosphere (moyen)
- Groupe sanguin ABO (moyen)
- Production insuline (difficile)
-
Art (IDs 26-30):
- Van Gogh oreille (difficile)
- Starry Night artiste (moyen)
- Mouvement Picasso (difficile)
- Olympia année (difficile)
- Nationalité Schiele (difficile)
Structure question:
{
id: string // Unique ID
intitule: string // Énoncé
reponses: string[] // 4 réponses
indexBonneReponse: number // Index correct (0-3)
explication: string // Explication
categorie: string // Nom catégorie (ex: 'Maths')
difficulte: Difficulty // facile | moyen | difficile
countApparition: 0 // Always 0 (fixture)
countBonneReponse: 0 // Always 0 (fixture)
}Utilisation: Développement sans dépendre du chargement JSON
3 stores Pinia distincts:
État:
const questions = ref<Question[]>([]) // Toutes les questions
const badges = ref<Badge[]>([]) // Tous les badges
const categories = ref<Category[]>([]) // Toutes les catégories
const isLoading = ref(false) // État chargement global
const error = ref<string | null>(null) // Erreur globaleActions (asynchrones):
initData()- Charger questions, badges, catégories depuis IndexedDBimportQuestions(json, targetCategory?)- Importer depuis fichier JSONresetBadges()- Réinitialiser tous les badges à 'verrouille'updateBadges(newBadges)- Sauvegarder badges modifiésloadCategories()- Recharger catégoriesaddCategory(category)- Créer nouvelle catégorieupdateCategory(category)- Modifier catégorie (cascading label)deleteCategory(categoryId)- Supprimer + supprimer questionsgetCategoryByLabel(label)- LookupresetCategories()- Réinitialiser à valeurs par défautreloadQuestions()- Recharger questions depuis IndexedDB
Getters:
allCategories- Computed categories.value
État:
const activeSession = ref<QuizSession | null>(null)
const selectedAnswerIndex = ref<number | null>(null)
const hasAnswered = ref(false)
const showResumeModal = ref(false)
// Sélections temporaires
const selectedCategories = ref<string[]>([])
const randomCategoriesSelection = ref<string[]>([])
const selectedDifficulty = ref<Difficulty | null>(null)Computed:
currentQuestion- Question actuellecurrentQuestionIndex- IndexprogressPercent- 0-100isLastQuestion- booléenisQuizFinished- booléen
Actions (asynchrones):
checkResumableSession()- Chercher session en cours au mountresumePreviousSession()- Reprendre session en coursabandonSession()- Supprimer session en courscreateQuizSession(categories, difficulty, count)- Créer nouvelle sessionsubmitAnswer(answerIndex)- Enregistrer réponseskipQuestion()- Passer questionnextQuestion()- Aller question suivante ou finirsaveCurrentSession()- Persister session courantefinishQuiz()- Terminer quiz + calculer scores + mettre à jour stats
Actions (synchrones):
-
clearActiveSession()- Vider l'état -
selectCategory(category)- Enregistrer sélection -
openRandomConfig(availableCategories)- Pré-remplir -
validateRandomSelection()- Valider sélection aléatoire -
selectDifficulty(difficulty)- Enregistrer difficulté -
getReplayParams()- Retourner paramètres pour replay
Comportements clés:
- Questions triées par countApparition (moins vues en premier)
- Réponses mélangées aléatoirement
- Sauvegarde après chaque action (submitAnswer, nextQuestion)
- Appelle statsStore après finishQuiz()
État:
const globalStats = ref<GlobalStats>({
moyenneGlobale: 0,
meilleurScore: 0,
streakActuel: 0,
totalSessions: 0,
historiqueSessions: [],
})
const previousStats = ref<ComparisonStats>({ average: 0 })
const newlyUnlockedBadges = ref<Badge[]>([])Computed:
badgesNonLus- Vrai si newlyUnlockedBadges non vide
Actions (asynchrones):
loadStats()- Charger et calculer stats depuis IndexedDBupdateStatsAndBadges(session)- Après finishQuiz(), vérifier badgescalculateDailyAverages(sessions)- Calculer moyennes par jour (30j)
Actions (synchrones):
checkAndUnlockBadges(session, completedSessions, currentStreak, badges)- Logique déblocagecalculateCurrentStreak(sessions)- Calculer streak actuel
Calculs:
- Moyenne = moyenne des notePourcentage
- Meilleur = max notePourcentage
- Streak = jours consécutifs avec ≥1 session
- Historique = sessions.filter(s => s.dateFin !== null)
Initialisation (App.vue mount):
dataStore.initData()- Charge questions, badges, catégoriesquizStore.checkResumableSession()- Vérifie session en cours
Flux quiz:
- Sélection catégorie (Home)
- Sélection difficulté (Difficulty)
- Sélection compte (Count)
createQuizSession()- Crée session en IndexedDB- Chaque réponse:
submitAnswer()→ sauvegarde - Fin:
finishQuiz()→ calcul scores + appellestatsStore.updateStatsAndBadges() - Stats recalculées et badges mis à jour
Import de données:
- Upload JSON → stocke en sessionStorage
- SelectCategory → sélectionne/crée catégorie
importQuestions(json, categoryLabel)→ valide, normalise, sauvegardereloadQuestions()pour mettre à jour l'état
Gestion des erreurs:
- Try-catch dans chaque action async
- Erreurs enregistrées dans store.error
- Messages affichés à l'utilisateur
L'application est 100% frontend (client-side only). Toutes les données sont stockées localement dans IndexedDB. Il n'y a pas de serveur backend, pas d'API REST, pas de synchronisation cloud.
Les opérations base de données sont abstraites via des repositories:
interface QuestionRepository {
getAll(): Promise<Question[]>
getById(id: string): Promise<Question | undefined>
save(question: Question): Promise<void>
saveMany(questions: Question[]): Promise<void>
clear(): Promise<void>
incrementApparition(id: string): Promise<void>
incrementCorrect(id: string): Promise<void>
}Implémentation: Transactions IndexedDB readwrite/readonly
interface SessionRepository {
getAll(): Promise<QuizSession[]>
getById(sessionId: string): Promise<QuizSession | undefined>
save(session: QuizSession): Promise<void>
delete(sessionId: string): Promise<void>
getPendingSession(): Promise<QuizSession | undefined>
getCompleted(): Promise<QuizSession[]>
clear(): Promise<void>
}interface MetaRepository {
get(key: string): Promise<any>
save(key: string, data: any): Promise<void>
getBadges(): Promise<Badge[]>
saveBadges(badges: Badge[]): Promise<void>
}interface CategoryRepository {
getAll(): Promise<Category[]>
getById(id: string): Promise<Category | undefined>
getByLabel(label: string): Promise<Category | undefined>
save(category: Category): Promise<void>
update(category: Category): Promise<void>
delete(id: string): Promise<void>
deleteByLabel(label: string): Promise<void>
saveMany(categories: Category[]): Promise<void>
clear(): Promise<void>
}Requis:
[
{
"intitule": "Qu'est-ce que TypeScript ?",
"reponses": ["Un langage", "Une lib", "Un framework", "Un IDE"],
"indexBonneReponse": 0,
"difficulte": "facile"
},
...
]Optionnels:
{
"id": "q-custom-1",
"explication": "TypeScript est un **sur-ensemble** de JavaScript",
"categorie": "TypeScript"
}Validation:
- Array requis
- Chaque item doit avoir: intitule, reponses (array 4 éléments), indexBonneReponse (0-3), difficulte
- Si categorie absent: 'Sans catégorie'
- Si explication absent: ''
- Si id absent: généré comme
imported-${timestamp}-${index}
L'application n'a pas de système d'authentification. Il n'y a pas de:
- Login / signup
- Tokens JWT
- Sessions utilisateur
- Rôles / permissions
Toutes les pages sont publiques. N'importe quel utilisateur peut y accéder directement.
Restriction fonctionnelle:
/quiz/activene fonctionne que s'il existe uneactiveSessionen store- Si pas de session: composant vide ou redirection implicite vers
/home
- Si pas de session: composant vide ou redirection implicite vers
/quiz/summarynécessite une session terminée (isQuizFinished)
Chaque navigateur/appareil a ses propres données (IndexedDB isolée par origin).
Il n'y a pas de partage de données entre appareils / utilisateurs.
Situation: Utilisateur crée un quiz avec catégories/difficulté où il n'y a aucune question.
Comportement:
createQuizSession()lance une exception: "Pas assez de questions disponibles pour cette sélection"- Utilisateur reste sur la page et voit le message d'erreur
Prévention:
- Home affiche alerte si aucune catégorie disponible
- Encourage l'import
Situation: Utilisateur ferme le navigateur pendant un quiz.
Comportement:
- Session non terminée persiste en IndexedDB (
dateFin === null) - Au prochain mount de l'app: modal "Quiz en cours" propose reprendre ou abandonner
- Si reprendre: active la session et navigue vers
/quiz/active
Situation: Utilisateur supprime une catégorie qui contient des questions.
Comportement (cascading):
- Toutes les questions de cette catégorie sont supprimées
- Les sessions historiques gardent les snapshots de questions
- Aucun impact sur les statistiques (basées sur historiqueSessions)
Situation: Utilisateur change le label d'une catégorie (ex: 'TypeScript' → 'TS').
Comportement:
- Toutes les questions ayant
categorie: 'TypeScript'sont mises à jour vers'TS' - Sauvegarder en IndexedDB
- Garder les sessions historiques inchangées (dateJour/categories referant aux labels d'époque)
Situation: Utilisateur importe des questions dans une catégorie qui existe déjà.
Comportement:
- Les questions s'ajoutent aux existantes
questionRepository.saveMany()utilisestore.put()(insert or update)- Si ID collide: l'ancien est écrasé
Situation: Utilisateur obtient 100% à un quiz.
Comportement:
perfect_scorebadge est débloqué (si pas déjà débloqué)- Ajouté à
newlyUnlockedBadgespour notification - Message personnalisé "Parfait ! Vous êtes un expert !"
Situation: Utilisateur ne fait pas de quiz pendant 2+ jours.
Comportement:
calculateCurrentStreak()calcule jours consécutifs- Si dernière session < 2 jours: streak continue
- Si > 1 jour: streak = 0
- Affichage dans Summary et Stats reflète le streak actuel
Situation: Questions ou explications contiennent du Markdown.
Composant: MarkdownText.vue utilise marked library.
Comportement:
- Markdown parsé et rendu en HTML
- Support: bold, italic,
code, # headers, listes, liens - Pas de risque XSS (HTML échappé sauf si balisé)
- Questions par quiz: 5, 10 ou 20 seulement (choix limité)
- Catégories: 7 par défaut, illimitées en ajout manuel
- Difficultés: 3 (facile/moyen/difficile) + random
- Reponses par question: Exactement 4
- IndexedDB a des limites par origin (typiquement 50MB+)
- Pas de gestion de quota
- Dépassement → erreur implicite
- Toutes les questions chargées en mémoire
- Grille de badges chargée complètement
- Pas de lazy-loading
- Questions vides: pas de création (validation)
- Categories vides: interdites (validation label)
- Sessions supprimées: continuent d'exister en historique
- Badges sans date:
dateDebloque = nullsi verrouille
Les stores fournissent:
isLoadingpour signaler un chargementerrorpour un message d'erreur- Pas de loading détaillé par opération (global uniquement)
- Submit answer:
hasAnsweredempêche double soumission - Créer catégorie: pas de protection (peut causer duplication en double-clic rapide)
Optimisations observées:
- Lazy loading questions: Questions filtrées et triées en mémoire (pas d'indexation avancée)
- Tri efficace:
countApparitionindex en IndexedDB - Minimal state: Seule la session active en mémoire (autres restent en IndexedDB)
- No re-renders coûteux: Pinia réactivité fine-grained
- Mélange aléatoire: Fait à la création (O(n), pas à chaque affichage)
Goulots potentiels:
- Import de 1000+ questions peut être lent (parsing + sauvegarde)
- Calcul de streak sur 30j: linéaire sur toutes les sessions
- Graphique 30j: recalculé à chaque loadStats()
Aspects de sécurité applicatifs:
-
Validation des entrées
- Label catégorie: validation d'unicité
- JSON import: validation structure et propriétés
- Difficulte: enum (Exclude<Difficulty, 'random'>)
-
Prévention XSS
- Markdown rendu via
marked(escaped par défaut) - Pas de
v-htmlbrut (sauf MarkdownText qui parse) - Props TypeScript fortement typées
- Markdown rendu via
-
Confidentialité des données
- Toutes les données locales (IndexedDB client-side only)
- Aucune synchronisation, aucun serveur
- Chaque appareil/navigateur isolé
- Pas de traçage utilisateur
-
Pas de sécurité entre utilisateurs
- Mono-utilisateur par appareil
- Pas de chiffrement local
- Données en plaintext dans IndexedDB
Éléments observés (non-visuels):
-
Labels:
- Questions ont numéro de question (contexte)
- Badges ont descriptions (pour lecteur d'écran)
-
Focus:
- Boutons navigables au clavier
- Pas d'ordre de tabulation explicite observé
-
Modaux:
- BaseModal avec titre
- Boutons d'action distincts
-
Feedback:
- Spinner pour chargement
- Messages d'erreur texte
- États de réponse (correct/incorrect) explicites
Observations:
- Pas d'ARIA labels détaillés
- Pas de gestionnaire de focus modal
- Pas de skip-to-content
- Couleurs pour difficulté (risque pour daltoniens)
Patterns appliqués:
-
Repository Pattern
- Abstraction IndexedDB
- Échange facile (changer vers LocalStorage, HTTP, etc.)
-
Store Pinia
- Séparation claire: Data / Quiz / Stats
- Logique métier centralisée
-
Composants atomiques
- BaseButton, BaseModal réutilisables
- QuestionCard = logique quiz isolée
-
Types TypeScript
- Interfaces pour toutes entités
- Exclude<> pour types conditionnels
-
Logs de debug
- [DataStore], [QuizStore], [Repository] préfixes
- Facilite trace des opérations
Absence de patterns:
- Pas de tests unitaires couverts (test files existent mais vides)
- Pas de E2E complets
- Pas de storybook
- Pas de documentation API (JSDoc minimal)
export default defineConfig({
plugins: [
vue(), // Support .vue files
vueJsx(), // JSX support
vueDevTools(), // Vue DevTools plugin
],
resolve: {
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url)),
},
},
})Points clés:
- Target ES2020
- Alias
@poursrc/ - Vite v7 (full ESM)
{
"extends": "@vue/tsconfig/tsconfig.dom.json",
"include": ["env.d.ts", "src/**/*", "src/**/*.vue"],
"exclude": ["src/**/__tests__/*"],
"compilerOptions": {
"paths": { "@/*": ["./src/*"] }
}
}Points clés:
- Strict mode activé (hérité de @vue/tsconfig)
- Include fichiers .vue
- Alias path matching
// tailwind.config.js
export default {
content: ['./index.html', './src/**/*.{vue,js,ts}'],
theme: {
extend: {
colors: {
// Material Design 3 colors customisés
},
},
},
}Intégration:
- @tailwindcss/postcss (PostCSS plugin)
- src/style.css:
@import "tailwindcss" - CSS compilé et tree-shaken
export default {
plugins: {
'@tailwindcss/postcss': {},
},
}Intégration simple Tailwind.
Lint avec:
- Oxlint (1.23) - correctness
- ESLint 9.37 - rules
- Vue plugin
- TypeScript plugin
- Vitest plugin (pour tests)
- Prettier plugin (formatting)
Configuration de formatage (indentation 2 spaces, semi, etc.)
npm run dev # Vite dev server (http://localhost:5174)
npm run build # type-check + vite build (production)
npm run preview # Prévisualiser prod localement
npm run test:unit # Vitest
npm run test:e2e # Playwright
npm run lint # Oxlint + ESLint avec auto-fix
npm run format # Prettier formatProduction:
dist/- bundles optimisés minifiésdist/assets/- CSS, JS, chunksdist/index.html- entry point
Taille estimée:
- CSS: ~32kB (~6.3kB gzipped)
- JS: Dépend de bundle splitting
- Node.js: ^20.19.0 || >=22.12.0
- Package manager: npm (package-lock.json)
| Chemin | Nom | Type | Rôle principal |
|---|---|---|---|
layout/AppLayout.vue |
AppLayout | Layout | Wrapper principal avec header et modal |
layout/AppHeader.vue |
AppHeader | Navigation | En-tête avec logo et accès stats |
quiz/QuestionCard.vue |
QuestionCard | Présentation | Affichage question + 4 réponses |
quiz/AnswerOption.vue |
AnswerOption | Bouton | Réponse cliquable avec feedback état |
quiz/ProgressBar.vue |
ProgressBar | Indicateur | Barre progression 0-100% |
stats/StatCard.vue |
StatCard | KPI | Card statistique (moyenne, streak, etc.) |
stats/EvolutionChart.vue |
EvolutionChart | Graphique | Chart.js 30-day trend line |
stats/BadgesGrid.vue |
BadgesGrid | Grille | Affichage 3-col badges (vérouillé/débloqué) |
settings/FormCategorie.vue |
FormCategorie | Formulaire | Create/edit catégorie (label, icon, color) |
settings/ListeCategories.vue |
ListeCategories | Liste | Swipe-to-delete catégories avec counts |
settings/ModalSelectCategory.vue |
ModalSelectCategory | Modal | Sélection catégorie existante ou création |
common/BaseButton.vue |
BaseButton | Composant | Bouton réutilisable (4 variants, 3 sizes) |
common/BaseModal.vue |
BaseModal | Layout | Modal wrapper générique |
common/LoadingSpinner.vue |
LoadingSpinner | Indicateur | Spinner SVG pur |
common/MarkdownText.vue |
MarkdownText | Rendu | Parse Markdown vers HTML |
| Item | Type | Rôle |
|---|---|---|
questions |
ref | Array questions depuis IndexedDB |
badges |
ref | Array badges 6 pré-définis |
categories |
ref | Array catégories |
isLoading |
ref | Flag état chargement |
error |
ref | Message erreur global |
initData() |
action | Load q, badges, cats au mount |
importQuestions(json, cat) |
action | Validate + save JSON import |
addCategory(cat) |
action | Créer catégorie (avec unicité) |
updateCategory(cat) |
action | Edit + cascade label change |
deleteCategory(id) |
action | Delete + cascade questions |
resetBadges() |
action | Set all à 'verrouille' |
updateBadges(badges) |
action | Save badges state |
| Item | Type | Rôle |
|---|---|---|
activeSession |
ref | QuizSession en cours ou null |
selectedAnswerIndex |
ref | Index réponse sélectionnée |
hasAnswered |
ref | Flag question répondue |
showResumeModal |
ref | Affiche modal reprise |
selectedCategories |
ref | Categories sélectionnées |
selectedDifficulty |
ref | Difficulty sélectionnée |
currentQuestion |
computed | SessionQuestion courante |
progressPercent |
computed | 0-100 progression |
isLastQuestion |
computed | Dernière question flag |
createQuizSession(cats, diff, count) |
action | Create + save session |
submitAnswer(idx) |
action | Enregistrer réponse |
skipQuestion() |
action | Passer question |
nextQuestion() |
action | Question suivante ou finish |
finishQuiz() |
action | Terminer + calc scores + badges |
checkResumableSession() |
action | Find pending session |
| Item | Type | Rôle |
|---|---|---|
globalStats |
ref | Agrégation stats (moyenne, streak, etc.) |
newlyUnlockedBadges |
ref | Badges débloqués cette session |
badgesNonLus |
computed | True si badges nouveaux |
loadStats() |
action | Calculate global stats |
updateStatsAndBadges(session) |
action | After finishQuiz + check badges |
calculateDailyAverages(sessions) |
action | 30-day map for chart |
calculateCurrentStreak(sessions) |
action | Days consécutifs |
(Rappel: App 100% client-side, pas d'API backend)
GET / → IndexedDB (dbPromise)
POST /quiz/create-session → QuizSession save (sessionRepository)
PUT /quiz/session/{id} → Session update (sessionRepository)
DELETE /quiz/session/{id} → Session delete (sessionRepository)
GET /quiz/sessions → getAll (sessionRepository)
POST /questions/import → saveMany (questionRepository)
GET /statistics → calculateStats (sessionRepository + Pinia)
// Requis:
- intitule: string (non-vide)
- reponses: string[] (exactement 4 éléments)
- indexBonneReponse: number (0-3)
- difficulte: 'facile' | 'moyen' | 'difficile'
// Optionnels (avec défauts):
- id: string (default: `imported-${timestamp}-${idx}`)
- explication: string (default: '')
- categorie: string (default: 'Sans catégorie')
- countApparition: number (default: 0)
- countBonneReponse: number (default: 0)// Tous requis:
- label: string (non-vide, unique)
- icon: string (parmi 24 icônes)
- color: TailwindColor (parmi 14 couleurs)
// Généré:
- id: string (user-created ou timestamp-based)first_quiz: completed >= 1 session
perfect_score: score == 100% this session
streak_3: currentStreak >= 3 days
streak_7: currentStreak >= 7 days
marathon: totalCompleted >= 20
math_expert: 5+ sessions monocatégorie 'Maths'
Localisation: public/questions/{category}.json
Exemples de fichiers:
cat_react.json- Questions Reactcat_typescript.json- Questions TypeScriptcat_nodejs.json- Questions Node.jscat_nextjs.json- Questions Next.jscat_css.json- Questions CSScat_javascript.json- Questions JavaScriptcat_entretiens.json- Questions entretiens techniques
Format JSON:
[
{
"intitule": "Qu'est-ce que React ?",
"reponses": [
"Une librairie JavaScript",
"Un framework backend",
"Une base de données",
"Un serveur"
],
"indexBonneReponse": 0,
"difficulte": "facile",
"explication": "React est une librairie JavaScript développée par Facebook pour construire des UI avec composants réutilisables.",
"categorie": "React"
},
...
]Question
├─ categorie (string) ──→ Category.id
├─ difficulte: 'facile' | 'moyen' | 'difficile'
├─ countApparition (métrique usage)
└─ countBonneReponse (métrique succès)
Category
├─ id (unique)
├─ label (unique, changeable)
├─ icon (24 options)
└─ color (14 Tailwind colors)
QuizSession
├─ sessionId (UUID)
├─ dateDebut, dateFin (ISO strings)
├─ questions: SessionQuestion[] (snapshots)
├─ categories: string[] (labels of selected)
├─ difficulteChoisie: Difficulty
├─ scorePondere (weighted points)
├─ notePourcentage (0-100)
└─ dateJour (YYYY-MM-DD, streak calc)
Badge
├─ id (unique, predefined)
├─ nom, description
├─ statut: 'verrouille' | 'debloque'
├─ icon (emoji)
└─ dateDebloque (ISO timestamp)
GlobalStats
├─ moyenneGlobale (avg %)
├─ meilleurScore (max %)
├─ streakActuel (consecutive days)
├─ totalSessions (count)
└─ historiqueSessions: QuizSession[] (for aggregation)
- vue@3.5.22
- pinia@3.0.3
- vue-router@4.6.3
- chart.js@4.5.1
- marked@17.0.1
- @tailwindcss/postcss@4.1.17
- vite@7.1.11
- typescript@~5.9.0
- @vitejs/plugin-vue@6.0.1
- @vue/test-utils@2.4.6
- vitest@3.2.4
- @playwright/test@1.56.1
- oxlint@~1.23.0
- eslint@9.37.0
CodeMaster est une Progressive Web Application (PWA) de quiz 100% client-side. Elle permet aux utilisateurs de:
- Faire des quiz catégorisés avec difficulté variable
- Suivre leurs progrès via statistiques et badges
- Importer des questions personnalisées
- Gérer des catégories
- Frontend Only: Aucun serveur, pas d'API, IndexedDB seul
- 3 Stores Pinia: Données (questions/badges), Quiz (session), Stats (agrégation)
- Repository Pattern: Abstraction IndexedDB pour maintenabilité
- 8 Routes: Flux quiz + import + stats + catégories
- TypeScript Strict: Types pour toutes entités
- Tailwind CSS v4: Styling avec custom components
- Chart.js & Markdown: Visualisations et contenu riche
src/stores/- Logique métier (3 stores)src/db/- Persistance (IndexedDB)src/views/- Pages (10 fichiers)src/router/- Navigationsrc/types/- Models TypeScript
Fin de la spécification fonctionnelle