chore: initial commit

This commit is contained in:
devcodetools committed 2026-09-21 17:28:26 +02:00
commit 03f03f7ad2
39 files changed
+7292

No files matched your search

+2
View File
@@ -0,0 +1,2 @@
# Variables exposées au client (préfixées NEXT_PUBLIC_)
# NEXT_PUBLIC_API_URL=http://localhost:3000
+51
View File
@@ -0,0 +1,51 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.*
.yarn/*
!.yarn/patches
!.yarn/plugins
!.yarn/releases
!.yarn/versions
# testing
/coverage
# next.js
/.next/
/out/
# production
/build
# misc
.DS_Store
*.pem
# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.pnpm-debug.log*
# env files (can opt-in for committing if needed)
.env*
# vercel
.vercel
# typescript
*.tsbuildinfo
next-env.d.ts
# Environnement (jamais committer les secrets)
.env
.env.*
!.env.example
.env.local
.env.*.local
# Couverture de tests
coverage
+8
View File
@@ -0,0 +1,8 @@
node_modules
.next
out
coverage
next-env.d.ts
pnpm-lock.yaml
public/*.png
public/*.ico
+6
View File
@@ -0,0 +1,6 @@
{
"semi": false,
"singleQuote": true,
"trailingComma": "all",
"printWidth": 100
}
+37
View File
@@ -0,0 +1,37 @@
# Application web Flashcards
Flashcards est application web SPA PWA permet d'afficher des cartes sur des sujets de code, de développement ou de culture tech.
## Principe du jeu
Le jeu consiste à afficher une carte au hasard.
une question ou un terme apparaît. La réponse est cachée. Si l'utilisateur souhaite consuletr la réponse, il peut faire dérouler la réponse.
Il est néanmoins possible de sélectionner une carte au hasard sur un thème plus précis : code, langage, etc...
L'utilisateur peut créer, modifier ou supprimer une carte (CRUD).
L'utilisatuer peut afficher la liste des cartes ou consulter une carte spécifique.
## Stack technique
- Framework : Next Js
- Typescript
- Tailwind
- Eslint, Prettier
- Test unitaires : Vitest avec coverage
### Base de données
Bien qu'il n'y ait réellement pas besoin de 2 SGBD, dans un but pédagogique, il faudra gérer les 2 bases de données simultanément.
Les composants d'accès aux données devront être le plus génériques possibles.
- Mysql (conteneur Docker)
- Mongo Db (conteneur Docker)
### Déploiement
- Docker via docker-compose
- Volumes managés
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) $(date +%Y)
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+37
View File
@@ -0,0 +1,37 @@
# flashcards
Application web PWA (Next.js 16 + React + TypeScript + Tailwind CSS 4 + Serwist).
## Prérequis
- Node.js >= 20.19.0 (ou >= 22.12.0)
- pnpm >= 9
## Installation
```bash
pnpm install
```
## Commandes
| Commande | Description |
| -------------------- | ------------------------------------------------------------- |
| `pnpm dev` | Serveur de développement (Turbopack) |
| `pnpm build` | Build de production + service worker (Serwist) |
| `pnpm start` | Prévisualisation du build en local |
| `pnpm test` | Tests unitaires (Vitest) |
| `pnpm test:watch` | Tests en mode watch |
| `pnpm test:coverage` | Tests + rapport de couverture (coverage/) |
| `pnpm lint` | Lint ESLint |
| `pnpm format` | Formatage Prettier |
| `pnpm typecheck` | Vérification TypeScript |
| `pnpm check` | Chaîne qualité : lint → format → typecheck → tests → coverage |
## Environnement
Copiez `.env.example` vers `.env` pour définir les variables (préfixe `NEXT_PUBLIC_`).
## Licence
MIT — voir [LICENSE](LICENSE).
+179
View File
@@ -0,0 +1,179 @@
# TODO — Application web Flashcards (développement assisté par IA)
Document de spécifications et checklist destiné à guider un agent IA dans le développement complet de l'application. Il complète le GOAL.md en le rendant actionnable.
---
## 1. Contexte produit
Application web **SPA + PWA** : Flashcards affiche des cartes (questions/réponses) sur des sujets de code, de développement ou de culture tech.
### Principe du jeu
- Affichage d'une **carte au hasard** : une question ou un terme apparaît, la réponse est cachée.
- L'utilisateur peut **révéler la réponse** à la demande.
- Possibilité de tirer une carte au hasard sur un **thème précis** (code, langage, etc.).
### Gestion des cartes
- L'utilisateur peut **créer, modifier ou supprimer** une carte (CRUD complet).
- L'utilisateur peut **afficher la liste** des cartes ou **consulter une carte spécifique**.
---
## 2. Exigences fonctionnelles
> Chaque exigence doit être satisfaite avant de considérer la feature terminée.
| Ref | Exigence | Critère d'acceptation |
|-----|----------|------------------------|
| F-01 | Afficher une carte au hasard | Un clic sur « Jouer » affiche UNE carte tirée aléatoirement de l'ensemble des cartes |
| F-02 | Dans un tirage aléatoire, la question est visible et la réponse cachée | Le terme/question s'affiche ; la réponse ne s'affiche pas |
| F-03 | Révéler la réponse | Un clic (ou action) déroule/affiche la réponse cachée ; un second clic la masque |
| F-04 | Tirer une carte au hasard **par thème** | L'utilisateur choisit un thème ; le tirage se fait uniquement parmi les cartes de ce thème |
| F-05 | Créer une carte | Formulaire de création avec question, réponse et thème ; la carte est persistée |
| F-06 | Modifier une carte | Formulaire pré-rempli ; la modification est persistée |
| F-07 | Supprimer une carte | Confirmation avant suppression ; la carte disparaît de la liste |
| F-08 | Lister les cartes | La liste complète des cartes s'affiche (avec au minimum question + thème) |
| F-09 | Consulter une carte spécifique | L'accès direct à une carte (via sa route/id) affiche son détail complet |
| F-10 | Thèmes | L'utilisateur peut choisir un thème reflétant les cartes existantes |
---
## 3. Modélisation de données (proposition à valider)
### Entité `Card`
| Champ | Type | Contrainte | Description |
|-------------|------------|-------------------|------------------------------------------|
| `id` | string/INT | unique (PK) | Identifiant stable de la carte |
| `question` | string | requis | Terme ou question affiché |
| `response` | string | requis | Réponse cachée puis révélée |
| `theme` | string | requis | Thème/catégorie (ex. « code », « javascript ») |
| `tags` | string[] | optionnel | Mots-clés additionnels |
| `createdAt` | datetime | auto | Date de création |
| `updatedAt` | datetime | auto | Date de dernière modification |
> La liste des thèmes peut être dérivée dynamiquement des valeurs `theme` distinctes présentes en base (simplicité) ou gérée en entité `Theme` dédiée (robustesse). Choix à figer en phase de conception.
### Représentation duale SGBD
- **MySQL** : table `cards`
- **MongoDB** : collection `cards`
Le même `id` doit permettre de retrouver une carte dans les deux SGBD (l'id métier n'est pas nécessairement l'id technique Mongo).
---
## 4. Exigences techniques et architecture
### Stack
- Framework : **Next.js**
- Langage : **TypeScript** (strict)
- Styles : **Tailwind**
- Qualité : **ESLint**, **Prettier**
- Tests unitaires : **Vitest** avec **coverage**
### Base de données — double SGBD simultané (contrainte pédagogique)
Bien qu'il n'y ait pas réellement besoin de 2 SGBD, dans un but pédagogique les **deux bases doivent être gérées simultanément** :
- **MySQL** (conteneur Docker)
- **MongoDB** (conteneur Docker)
### Exigence architecturale majeure : couche d'accès aux données GÉNÉRIQUE
Les composants d'accès aux données doivent être **le plus générique possible**. Concrètement :
- Définir une **abstraction commune** (interface/repository générique, ex. `CardRepository`) exposant les opérations : `findAll`, `findById`, `create`, `update`, `delete`, `findRandom`, `findRandomByTheme`, `findThemes`.
- Fournir **deux implémentations** de cette abstraction : une pour MySQL, une pour MongoDB.
- **Aucun composant métier (service, route, UI) ne doit dépendre d'un SGBD précis** : il consomme uniquement l'abstraction.
- Les deux implémentations doivent être utilisables et activables (par config/DI/injection), et les tests doivent couvrir les deux.
### Déploiement
- **Docker** via **docker-compose**
- **Volumes nommés** pour la persistance des données des 2 SGBD
- Services au minimum : `web` (Next.js), `mysql`, `mongo`
---
## 5. Définition de fait (Definition of Done)
Une feature est « faite » si et seulement si :
- [ ] Les tests unitaires (Vitest) passent, **coverage** généré et au-dessus du seuil défini
- [ ] `lint` (ESLint) et `format` (Prettier — check) sans erreur
- [ ] TypeScript strict compile sans erreur
- [ ] L'implémentation est branchée sur la **couche générique d'accès aux données**
- [ ] Les **deux SGBD** (MySQL + MongoDB) fonctionnent simultanément en local
- [ ] Le comportement correspond aux critères d'acceptation de la section 2
- [ ] Construit et lançable via docker-compose avec volumes persistants
---
## 6. Checklist de développement par phases
> Ordre d'exécution recommandé. Passer à la phase suivante uniquement si la DoD de la phase courante est remplie.
### Phase 0 — Scaffolding
- [ ] Initialiser le projet Next.js + TypeScript (strict)
- [ ] Configurer Tailwind
- [ ] Configurer ESLint + Prettier
- [ ] Configurer Vitest + rapport de coverage
- [ ] Fixer le seuil de coverage (à définir, ex. 80%)
- [ ] DoD : `lint`, `format`, `typecheck`, `test` s'exécutent sans erreur sur un squelette
### Phase 1 — Données et couche d'accès générique
- [ ] Schéma MySQL (table `cards`) + migration/seed
- [ ] Schéma MongoDB (collection `cards`) + seed
- [ ] Interface générique `CardRepository` (contrat complet)
- [ ] Implémentation `MysqlCardRepository`
- [ ] Implémentation `MongoCardRepository`
- [ ] Mécanisme d'activation/sélection de l'implémentation (config)
- [ ] Tests unitaires des deux implémentations
- [ ] DoD : phase 0 + les 2 repositories testés et interchangeables
### Phase 2 — API / services métier
- [ ] Service métier `CardService` reposant uniquement sur l'abstraction repository
- [ ] Routes API : liste, détail, création, modification, suppression
- [ ] Routes API : tirage aléatoire simple + tirage aléatoire par thème + liste des thèmes
- [ ] Validation des entrées (contrats TypeScript / schémas)
- [ ] Gestion d'erreurs cohérente (404 carte inconnue, 400 payload invalide…)
- [ ] Tests unitaires du service (mock du repository)
- [ ] DoD : phases précédentes + contexte de MCP de l'API
### Phase 3 — Interface utilisateur
- [ ] Page/vue **liste des cartes** (F-08)
- [ ] Page/vue **détail d'une carte** (F-09)
- [ ] Formulaire **création** (F-05)
- [ ] Formulaire **édition** (F-06)
- [ ] **Suppression** avec confirmation (F-07)
- [ ] Vue **jeu** : tirage aléatoire global + sélecteur de thème (F-01, F-02, F-03, F-04, F-10)
- [ ] Navigation/flux SPA cohérent (routes Next.js)
- [ ] DoD : phases précédentes + parcours utilisateur complet fonctionnel
### Phase 4 — Tests
- [ ] Tests des composants UI (composants critiques)
- [ ] Couverture des critères F-01 à F-10
- [ ] Coverage au seuil défini (Phase 0)
- [ ] DoD : toutes phases précédentes vertes + coverage ok
### Phase 5 — PWA, déploiement
- [ ] Manifest PWA + service worker (installable, hors-ligne) — la stack SPA le permet
- [ ] `docker-compose.yml` : `web`, `mysql`, `mongo`
- [ ] Volumes nommés pour l'ensemble des données
- [ ] Variables/env de connexion aux 2 SGBD selon l'implémentation sélectionnée
- [ ] Construction de l'image web et démarrage complet via `docker compose up`
- [ ] Test de persistance après redémarrage des conteneurs
- [ ] DoD finale pleinement vérifiée
---
## 7. Points à trancher / en attente de décision
- [ ] Couverture : seuil de coverage exact
- [ ] Selon cas : `Theme` en table dédiée vs dérivation des `theme` distincts
- [ ] Choix par défaut de l'implémentation repository pour la prod (MySQL ou Mongo)
- [ ] Authentification éventuelle (hors périmètre README, à confirmer si nécessaire)
+70
View File
@@ -0,0 +1,70 @@
@startuml c4-container
' C4 modèle — Niveau 2 : Conteneurs (syntaxe native PlantUML, compatible anciennes versions)
' Application web Flashcards (SPA/PWA)
hide stereotype
skinparam backgroundColor #FFFFFF
' ===== Styles par type =====
skinparam rectangle<<person>> {
BackgroundColor #08427B
BorderColor #073B6F
FontColor #FFFFFF
}
skinparam rectangle<<system>> {
'BackgroundColor #1168BD
BorderColor #3C7FC0
FontColor #FFFFFF
}
skinparam rectangle<<container>> {
BackgroundColor #438DD5
BorderColor #135F97
FontColor #FFFFFF
}
skinparam rectangle<<external_system>> {
BackgroundColor #999999
BorderColor #8A8A8A
FontColor #FFFFFF
}
skinparam arrow {
Color #707070
}
title Conteneurs — Flashcards (Niveau 2)
' ===== Personne =====
rectangle "Utilisateur\nNavigateur / mobile\n(accès au jeu et au CRUD)" <<person>> as User
' ===== Système Flashcards et ses conteneurs =====
rectangle "Application Flashcards [Système]\nSPA/PWA Next.js — double persistance simultanée" <<system>> as Flashcards {
rectangle "SPA React (Next.js)\n[Conteneur : Web Browser]\nRendu de l'UI, état local, navigation" <<container>> as Spa
rectangle "API Next.js / App Router\n[Conteneur : Node.js]\nCardController > CardService :\nliste, CRUD, tirage aléatoire, thèmes\n(seul conteneur qui accède aux données)" <<container>> as Api
rectangle "CardRepository (générique)\n[Abstraction de données]\nContracts : findAll, findById,\nfindRandom, findRandomByTheme,\nfindThemes, create, update, delete" <<container>> as Repo
rectangle "Implémentation MySQL\n[Conteneur : librairie]\nImplémentation SQL du repository" <<container>> as RepoMysql
rectangle "Implémentation MongoDB\n[Conteneur : librairie]\nImplémentation Mongo du repository" <<container>> as RepoMongo
}
' ===== Systèmes externes =====
rectangle "Base MySQL\n[Conteneur externe : SGBD]\nPersistance des cartes (table cards)\n+ stockage Docker avec volume" <<external_system>> as MySql
rectangle "Base MongoDB\n[Conteneur externe : SGBD]\nPersistance des cartes (collection cards)\n+ stockage Docker avec volume" <<external_system>> as Mongo
'rectangle "Docker / docker-compose\n[infrastructure]\nOrchestration des conteneurs\nweb + bases + volumes" <<external_system>> as Docker
' ===== Relations =====
User -[#707070]-> Spa : "Navigue et interagit (HTTP/HTTPS)"
Spa -[#707070]-> Api : "Appels API JSON (dialogues, CRUD,\ntirage, révélation de réponse)"
Api -[#707070]down-> Repo : "Utilise uniquement l'abstraction"
Repo -[#707070]-> RepoMysql : "implémente"
Repo -[#707070]-> RepoMongo : "implémente"
Api -[#707070]down-> MySql : "Tables SQL via repository\n(selectionné par config/driver)"
Api -[#707070]down-> Mongo : "Requêtes Mongo via repository\n(selectionné par config/driver)"
'Docker -[#707070]-> MySql : "Démarre / arrête (docker-compose)"
'Docker -[#707070]-> Mongo : "Démarre / arrête (docker-compose)"
@enduml
+55
View File
@@ -0,0 +1,55 @@
@startuml c4-context
' C4 modèle — Niveau 1 : Contexte système (syntaxe native PlantUML, compatible anciennes versions)
' Application web Flashcards (SPA/PWA)
hide stereotype
skinparam backgroundColor #FFFFFF
' ===== Styles par type =====
skinparam rectangle<<person>> {
BackgroundColor #08427B
BorderColor #073B6F
FontColor #FFFFFF
}
skinparam rectangle<<external_person>> {
BackgroundColor #686868
BorderColor #8A8A8A
FontColor #FFFFFF
}
skinparam rectangle<<system>> {
BackgroundColor #1168BD
BorderColor #3C7FC0
FontColor #FFFFFF
}
skinparam rectangle<<external_system>> {
BackgroundColor #999999
BorderColor #8A8A8A
FontColor #FFFFFF
}
skinparam arrow {
Color #707070
}
title Contexte système — Flashcards (Niveau 1)
' ===== Personnes =====
rectangle "Utilisateur\nJoueur et contributeur de cartes\n(CRUD) : code, dev, culture tech" <<person>> as User
' ===== Système à modéliser =====
rectangle "App Flashcards\nSPA/PWA Next.js — tirage de cartes au\nhasard, tirage par thème, CRUD des cartes.\nDouble persistance simultanée\nMySQL + MongoDB via couche générique." <<system>> as Flashcards
' ===== Systèmes externes =====
rectangle "MySQL\nBase relationnelle\n(conteneur Docker)" <<external_system>> as MySql
rectangle "MongoDB\nBase documentaire\n(conteneur Docker)" <<external_system>> as Mongo
'rectangle "Docker / docker-compose\nOrchestration des conteneurs\n(web, bases) + volumes persistants" <<external_system>> as Docker
User -[#707070]-> Flashcards : Joue, crée, modifie, supprime,\nconsulte et révèle des cartes
Flashcards -[#707070]-> MySql : Lit / écrit (CardRepository)
Flashcards -[#707070]-> Mongo : Lit / écrit (CardRepository)
'Docker -[#707070]-> MySql : Démarre et gère
'Docker -[#707070]-> Mongo : Démarre et gère
@enduml
+105
View File
@@ -0,0 +1,105 @@
@startuml
' Diagramme de classes — Application web Flashcards (Architecture : couche d'accès générique)
skinparam classAttributeIconSize 0
skinparam classFontStyle bold
' ===== Domaine =====
class Card {
- id : string
- question : string
- response : string
- theme : string
- tags : string[]
- createdAt : Date
- updatedAt : Date
+ Card(id, question, response, theme, tags)
+ validate() : void
}
' ===== Abstraction générique d'accès aux données =====
interface CardRepository {
+ findAll() : Promise<Card[]>
+ findById(id : string) : Promise<Card | null>
+ findRandom() : Promise<Card | null>
+ findRandomByTheme(theme : string) : Promise<Card | null>
+ findThemes() : Promise<string[]>
+ create(card : Card) : Promise<Card>
+ update(card : Card) : Promise<Card>
+ delete(id : string) : Promise<boolean>
}
' ===== Implémentations SGBD =====
class MysqlCardRepository {
- connection : MysqlConnection
+ MysqlCardRepository(connection)
}
class MongoCardRepository {
- collection : MongoCollection
+ MongoCardRepository(collection)
}
class "MysqlConnection" as ConnMysql {
- host : string
- port : number
- user : string
- password : string
- database : string
+ query(sql : string, params : unknown[]) : Promise<unknown>
}
class "MongoCollection" as ColMongo {
+ find(filter : object) : Promise<unknown[]>
+ insertOne(doc : object) : Promise<unknown>
+ updateOne(filter : object, update : object) : Promise<unknown>
+ deleteOne(filter : object) : Promise<unknown>
}
class DatabaseConfig {
- driver : string
+ getActiveDriver() : string
}
' ===== Service métier (dépend uniquement de l'abstraction) =====
class CardService {
- repository : CardRepository
+ CardService(repository : CardRepository)
+ listCards() : Promise<Card[]>
+ getCard(id : string) : Promise<Card>
+ createCard(payload : NewCardPayload) : Promise<Card>
+ updateCard(id : string, payload : UpdateCardPayload) : Promise<Card>
+ deleteCard(id : string) : Promise<void>
+ playRandom(theme? : string) : Promise<Card>
+ listThemes() : Promise<string[]>
}
' ===== Couche exposition (routes / contrôleur) =====
class CardController {
+ handleList(req, res) : Promise<void>
+ handleGetById(req, res) : Promise<void>
+ handleCreate(req, res) : Promise<void>
+ handleUpdate(req, res) : Promise<void>
+ handleDelete(req, res) : Promise<void>
+ handlePlayRandom(req, res) : Promise<void>
+ handleThemes(req, res) : Promise<void>
}
class Factory {
{static} buildCardRepository(config : DatabaseConfig) : CardRepository
}
' ===== Relations =====
CardRepository <|.. MysqlCardRepository
CardRepository <|.. MongoCardRepository
MysqlCardRepository *-- ConnMysql : utilise >
MongoCardRepository *-- ColMongo : utilise >
CardService *-- CardRepository : <<injecté>> utilise >
CardService ..> Card : < crée/retourne
CardController --> CardService : utilise
Factory ..> CardRepository : <<instance de>> construit
Factory ..> CardService : construit
@enduml
+74
View File
@@ -0,0 +1,74 @@
@startuml
' Diagramme de séquence — "Afficher une carte" puis "Révéler la réponse"
' Application web Flashcards (SPA/PWA, couche d'accès générique MySQL/Mongo)
hide footbox
title Jeu : afficher une carte au hasard puis révéler la réponse
actor Utilisateur as User
participant "SPA React\n(UI)" as SPA
participant "CardController\n(API Next.js)" as Api
participant "CardService" as Service
participant "CardRepository\n(abstraction générique)" as Repo
participant "SGBD\n(MySQL OU MongoDB)" as DB
== Afficher une carte au hasard ==
User -> SPA : clique « Jouer »
SPA -> SPA : un thème est-il déjà sélectionné ?
alt Thème précis sélectionné (F-04)
User -> SPA : sélectionne un thème (ex. javascript)
SPA -> Api : GET /api/cards/random?theme=javascript
else Tirage global (F-01)
SPA -> Api : GET /api/cards/random
end
Api -> Service : playRandom(theme?)
activate Service
alt theme fourni
Service -> Repo : findRandomByTheme(theme)
else sans thème
Service -> Repo : findRandom()
end
activate Repo
Repo -> DB : Requête aléatoire (ORDER BY RAND() / $sample)
activate DB
DB --> Repo : Enregistrement(s) : question, réponse, thème
deactivate DB
Repo --> Service : Card
deactivate Repo
Service --> Api : Card (réponse non renvoyée par sécurité)
deactivate Service
Api --> SPA : 200 JSON { id, question, theme }
SPA -> SPA : affiche la question\nla réponse reste masquée
== Révéler la réponse ==
User -> SPA : clique « Voir la réponse » (F-03)
SPA -> Api : GET /api/cards/{id}/reveal
activate Api
Api -> Repo : findById(id)
activate Repo
Repo -> DB : SELECT par id / findOne
activate DB
DB --> Repo : Carte complète
deactivate DB
Repo --> Api : Card { response }
deactivate Repo
Api --> SPA : 200 JSON { response }
deactivate Api
SPA --> SPA : déroule et affiche la réponse
User -> SPA : reclique (masquer/réafficher) — boucle libre
loop tant que joueur le souhaite
SPA --> User : réponse visible / masquée
end
note over Repo : L'implémentation concrète (MySQL ou MongoDB)\nest fournie par la Factory selon DatabaseConfig\nsans impact sur Service ni Api (générique).
@enduml
+45
View File
@@ -0,0 +1,45 @@
@startuml Usecase
' Diagramme de cas d'utilisation — Application web Flashcards (SPA/PWA)
' Le CRUD et le jeu sont volontairement isolés : le jeu ne dépend d'aucun cas du catalogue.
left to right direction
skinparam actorStyle awesome
skinparam packageStyle rectangle
actor "Utilisateur" as User
rectangle "Jeu — révision" {
usecase "Afficher une carte au hasard" as UC_Jouer
usecase "Tirer des cartes d'un thème" as UC_Theme
usecase "Révéler la réponse" as UC_Revele
}
rectangle "Gestion des cartes (CRUD)" {
usecase "Lister les cartes" as UC_Liste
usecase "Consulter une carte" as UC_Detail
usecase "Créer une carte" as UC_Create
usecase "Modifier une carte" as UC_Update
usecase "Supprimer une carte" as UC_Delete
}
' --- Jeu ---
User --> UC_Jouer
UC_Jouer ..> UC_Theme : <<extend>>
UC_Jouer --> UC_Revele : <<include>>
' --- CRUD : la liste est l'entrée unique, le détail est le point d'ancrage des actions ---
User --> UC_Liste
UC_Liste ..> UC_Detail : <<extend>>
UC_Liste ..> UC_Create : <<extend>>
UC_Detail ..> UC_Update : <<extend>>
UC_Detail ..> UC_Delete : <<extend>>
note bottom of UC_Delete
Suppression avec confirmation
end note
note bottom of UC_Revele
La réponse est cachée tant que
l'utilisateur ne la déroule pas
end note
@enduml
+13
View File
@@ -0,0 +1,13 @@
Élément,Entité/Table,Champ,Type TypeScript,Type MySQL,Type MongoDB,Description,Contraintes,Obligatoire,Exemple
Carte,cards,id,string,VARCHAR(36) (UUID),string (champ c_id),Identifiant métier stable de la carte,UNIQUE / PRIMARY KEY,Oui,carte_123e4567-e89b-12d3-a456-426614174000
Carte,cards,question,string,TEXT,string,Terme ou question affichée à l'utilisateur lors du tirage jeu,Non vide,Oui,a2f3
Carte,cards,response,string,TEXT,string,"Réponse cachée, révélée à la demande par le joueur",Non vide,Oui,Asynchrone et non bloquant (Event Loop)
Carte,cards,theme,string,VARCHAR(50),string,Thème / catégorie de la carte,Non vide + index,Oui,javascript
Carte,cards,tags,string[],JSON (type JSON),string[],Mots-clés additionnels pour la recherche,Optionnel,Non,"[""frontend"",""DOM""]"
Carte,cards,createdAt,Date,DATETIME / TIMESTAMP,Date,Date de création de la carte,Automatique à l'insertion,Oui,9/16/26 8:00
Carte,cards,updatedAt,Date,DATETIME / TIMESTAMP,Date,Date de dernière modification,Automatique à chaque modification,Oui,9/16/26 9:30
Thème,themes,id,string,VARCHAR(36) (UUID),string,Identifiant unique du thème (si entité dédiée),UNIQUE / PRIMARY KEY,Oui,thm_2026-0001
Thème,themes,name,string,VARCHAR(50) UNIQUE,string,Nom du thème,UNIQUE,Oui,javascript
Liste des types,"Option ""recommandation""",liste des thèmes,-,-,-,Alternative : dériver dynamiquement les thèmes depuis les valeurs DISTINCT de cards.theme,-,-,-
Index recommandé MySQL,idx_theme_sur_cards.theme,-,-,INDEX (theme),-,-,-,-,-
Index recommandé MongoDB,index_sur_cards.theme,-,-,-,index(theme),-,-,-,-
1 Élément Entité/Table Champ Type TypeScript Type MySQL Type MongoDB Description Contraintes Obligatoire Exemple
2 Carte cards id string VARCHAR(36) (UUID) string (champ c_id) Identifiant métier stable de la carte UNIQUE / PRIMARY KEY Oui carte_123e4567-e89b-12d3-a456-426614174000
3 Carte cards question string TEXT string Terme ou question affichée à l'utilisateur lors du tirage jeu Non vide Oui a2f3
4 Carte cards response string TEXT string Réponse cachée, révélée à la demande par le joueur Non vide Oui Asynchrone et non bloquant (Event Loop)
5 Carte cards theme string VARCHAR(50) string Thème / catégorie de la carte Non vide + index Oui javascript
6 Carte cards tags string[] JSON (type JSON) string[] Mots-clés additionnels pour la recherche Optionnel Non ["frontend","DOM"]
7 Carte cards createdAt Date DATETIME / TIMESTAMP Date Date de création de la carte Automatique à l'insertion Oui 9/16/26 8:00
8 Carte cards updatedAt Date DATETIME / TIMESTAMP Date Date de dernière modification Automatique à chaque modification Oui 9/16/26 9:30
9 Thème themes id string VARCHAR(36) (UUID) string Identifiant unique du thème (si entité dédiée) UNIQUE / PRIMARY KEY Oui thm_2026-0001
10 Thème themes name string VARCHAR(50) UNIQUE string Nom du thème UNIQUE Oui javascript
11 Liste des types Option "recommandation" liste des thèmes - - - Alternative : dériver dynamiquement les thèmes depuis les valeurs DISTINCT de cards.theme - - -
12 Index recommandé MySQL idx_theme_sur_cards.theme - - INDEX (theme) - - - - -
13 Index recommandé MongoDB index_sur_cards.theme - - - index(theme) - - - -
+13
View File
@@ -0,0 +1,13 @@
import { defineConfig, globalIgnores } from 'eslint/config'
import nextVitals from 'eslint-config-next/core-web-vitals'
import nextTs from 'eslint-config-next/typescript'
import prettier from 'eslint-config-prettier/flat'
const eslintConfig = defineConfig([
...nextVitals,
...nextTs,
prettier,
globalIgnores(['.next/**', 'out/**', 'build/**', 'coverage/**', 'next-env.d.ts']),
])
export default eslintConfig
+5
View File
@@ -0,0 +1,5 @@
import { withSerwist } from '@serwist/turbopack'
export default withSerwist({
// Configuration Next.js (Turbopack activé par défaut dans Next 16)
})
+52
View File
@@ -0,0 +1,52 @@
{
"name": "flashcards",
"version": "0.0.0",
"private": true,
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"test": "vitest run",
"test:watch": "vitest",
"test:coverage": "vitest run --coverage",
"typecheck": "next typegen && tsc --noEmit",
"lint": "eslint .",
"lint:fix": "eslint . --fix",
"format": "prettier --write .",
"format:check": "prettier --check .",
"check": "pnpm lint && pnpm format && pnpm format:check && pnpm typecheck && pnpm test && pnpm test:coverage",
"pwa:assets": "pwa-assets-generator --preset minimal-2023 public/icon.svg"
},
"dependencies": {
"next": "16.3.5",
"react": "19.2.8",
"react-dom": "19.2.8"
},
"devDependencies": {
"@serwist/turbopack": "^9.5.12",
"@tailwindcss/postcss": "^4",
"@testing-library/jest-dom": "^7.0.1",
"@testing-library/react": "^16.3.3",
"@types/node": "^20",
"@types/react": "^19",
"@types/react-dom": "^19",
"@vite-pwa/assets-generator": "^2.0.0",
"@vitejs/plugin-react": "^6.1.1",
"@vitest/coverage-v8": "^5.0.1",
"esbuild": "^0.28.2",
"eslint": "^9",
"eslint-config-next": "16.3.5",
"eslint-config-prettier": "^10.1.8",
"jsdom": "^30.1.0",
"prettier": "^3.9.8",
"serwist": "^9.5.12",
"tailwindcss": "^4",
"typescript": "^5",
"vitest": "^5.0.1"
},
"packageManager": "pnpm@12.4.2",
"type": "module",
"engines": {
"node": ">=20.19.0"
}
}
+6244
View File
File diff suppressed because it is too large. Load diff
+6
View File
@@ -0,0 +1,6 @@
allowBuilds:
sharp: true
'@tailwindcss/oxide': true
'@swc/core': true
esbuild: true
unrs-resolver: true
+7
View File
@@ -0,0 +1,7 @@
const config = {
plugins: {
'@tailwindcss/postcss': {},
},
}
export default config
Binary file not shown.

After

Width:  |  Height:  |  Size: 782 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 483 B

+7
View File
@@ -0,0 +1,7 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024" width="1024" height="1024">
<rect width="1024" height="1024" rx="200" fill="#0f172a"/>
<g fill="none" stroke="#38bdf8" stroke-width="64" stroke-linecap="round" stroke-linejoin="round">
<path d="M384 256 L256 512 L384 768"/>
<path d="M640 256 L768 512 L640 768"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 353 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1017 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 455 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

+1
View File
@@ -0,0 +1 @@
@import 'tailwindcss';
+63
View File
@@ -0,0 +1,63 @@
import { SerwistProvider } from '@serwist/turbopack/react'
import type { Metadata, Viewport } from 'next'
import type { ReactNode } from 'react'
import './globals.css'
const APP_NAME = 'flashcards'
const APP_DESCRIPTION = 'Application web PWA installable et utilisable hors connexion.'
const APP_TITLE_TEMPLATE = '%s · flashcards'
export const metadata: Metadata = {
applicationName: APP_NAME,
title: {
default: APP_NAME,
template: APP_TITLE_TEMPLATE,
},
description: APP_DESCRIPTION,
manifest: '/manifest.webmanifest',
appleWebApp: {
capable: true,
statusBarStyle: 'default',
title: APP_NAME,
},
formatDetection: {
telephone: false,
},
icons: {
icon: '/icon.svg',
apple: '/apple-touch-icon-180x180.png',
},
openGraph: {
type: 'website',
siteName: APP_NAME,
title: {
default: APP_NAME,
template: APP_TITLE_TEMPLATE,
},
description: APP_DESCRIPTION,
},
twitter: {
card: 'summary',
title: {
default: APP_NAME,
template: APP_TITLE_TEMPLATE,
},
description: APP_DESCRIPTION,
},
}
export const viewport: Viewport = {
themeColor: '#0f172a',
}
export default function RootLayout({ children }: { children: ReactNode }) {
return (
<html lang="fr">
<body>
<SerwistProvider swUrl="/serwist/sw.js" disable={process.env.NODE_ENV === 'development'}>
{children}
</SerwistProvider>
</body>
</html>
)
}
+27
View File
@@ -0,0 +1,27 @@
import type { MetadataRoute } from 'next'
export default function manifest(): MetadataRoute.Manifest {
return {
name: 'flashcards',
short_name: 'flashcards',
description: 'Application web PWA installable et utilisable hors connexion.',
lang: 'fr',
start_url: '/',
scope: '/',
display: 'standalone',
background_color: '#0f172a',
theme_color: '#0f172a',
orientation: 'portrait',
icons: [
{ src: '/pwa-64x64.png', sizes: '64x64', type: 'image/png' },
{ src: '/pwa-192x192.png', sizes: '192x192', type: 'image/png' },
{ src: '/pwa-512x512.png', sizes: '512x512', type: 'image/png' },
{
src: '/maskable-icon-512x512.png',
sizes: '512x512',
type: 'image/png',
purpose: 'maskable',
},
],
}
}
+10
View File
@@ -0,0 +1,10 @@
import { render, screen } from '@testing-library/react'
import { describe, expect, it } from 'vitest'
import Home from './page'
describe('Home', () => {
it('affiche le contenu minimal', () => {
render(<Home />)
expect(screen.getByRole('heading', { level: 1, name: 'Hello world' })).toBeInTheDocument()
})
})
+7
View File
@@ -0,0 +1,7 @@
export default function Home() {
return (
<main>
<h1>Hello world</h1>
</main>
)
}
+14
View File
@@ -0,0 +1,14 @@
import { spawnSync } from 'node:child_process'
import { createSerwistRoute } from '@serwist/turbopack'
// Révision du cache : change à chaque commit pour invalider le precache.
const revision =
spawnSync('git', ['rev-parse', 'HEAD'], { encoding: 'utf-8' }).stdout ?? crypto.randomUUID()
export const { dynamic, dynamicParams, revalidate, generateStaticParams, GET } = createSerwistRoute(
{
additionalPrecacheEntries: [{ url: '/~offline', revision }],
swSrc: 'src/app/sw.ts',
useNativeEsbuild: true,
},
)
+41
View File
@@ -0,0 +1,41 @@
/// <reference lib="webworker" />
import { defaultCache } from '@serwist/turbopack/worker'
import type { PrecacheEntry, RuntimeCaching, SerwistGlobalConfig } from 'serwist'
import { NetworkOnly, Serwist } from 'serwist'
declare global {
interface WorkerGlobalScope extends SerwistGlobalConfig {
__SW_MANIFEST: (PrecacheEntry | string)[] | undefined
}
}
declare const self: ServiceWorkerGlobalScope
// Ne jamais mettre en cache les appels API
const runtimeCaching: RuntimeCaching[] = [
{
matcher: ({ sameOrigin, url }) => sameOrigin && url.pathname.startsWith('/api/'),
handler: new NetworkOnly(),
},
...defaultCache,
]
const serwist = new Serwist({
precacheEntries: self.__SW_MANIFEST,
skipWaiting: true,
clientsClaim: true,
navigationPreload: true,
runtimeCaching,
fallbacks: {
entries: [
{
url: '/~offline',
matcher({ request }) {
return request.mode === 'navigate'
},
},
],
},
})
serwist.addEventListeners()
+11
View File
@@ -0,0 +1,11 @@
import Link from 'next/link'
export default function OfflinePage() {
return (
<main>
<h1>Vous êtes hors connexion</h1>
<p>Reconnectez-vous pour continuer.</p>
<Link href="/">Accueil</Link>
</main>
)
}
+19
View File
@@ -0,0 +1,19 @@
import '@testing-library/jest-dom/vitest'
import { cleanup } from '@testing-library/react'
import { afterEach } from 'vitest'
afterEach(() => cleanup())
Object.defineProperty(window, 'matchMedia', {
writable: true,
value: (query: string) => ({
matches: false,
media: query,
onchange: null,
addListener: () => {},
removeListener: () => {},
addEventListener: () => {},
removeEventListener: () => {},
dispatchEvent: () => false,
}),
})
+34
View File
@@ -0,0 +1,34 @@
{
"compilerOptions": {
"target": "ES2017",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "react-jsx",
"incremental": true,
"plugins": [
{
"name": "next"
}
],
"paths": {
"@/*": ["./src/*"]
}
},
"include": [
"next-env.d.ts",
"**/*.ts",
"**/*.tsx",
".next/types/**/*.ts",
".next/dev/types/**/*.ts",
"**/*.mts"
],
"exclude": ["node_modules"]
}
+28
View File
@@ -0,0 +1,28 @@
import react from '@vitejs/plugin-react'
import { defineConfig } from 'vitest/config'
export default defineConfig({
plugins: [react()],
resolve: {
tsconfigPaths: true,
},
test: {
globals: true,
environment: 'jsdom',
setupFiles: ['./src/test/setup.ts'],
include: ['src/**/*.test.{ts,tsx}'],
coverage: {
provider: 'v8',
reporter: ['text', 'html', 'lcov'],
include: ['src/**/*.{ts,tsx}'],
exclude: [
'src/**/*.test.{ts,tsx}',
'src/app/layout.tsx',
'src/app/manifest.ts',
'src/app/sw.ts',
'src/app/serwist/**',
'src/test/**',
],
},
},
})