chore: initial commit
No files matched your search
@@ -0,0 +1,2 @@
|
|||||||
|
# Variables exposées au client (préfixées NEXT_PUBLIC_)
|
||||||
|
# NEXT_PUBLIC_API_URL=http://localhost:3000
|
||||||
@@ -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
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
node_modules
|
||||||
|
.next
|
||||||
|
out
|
||||||
|
coverage
|
||||||
|
next-env.d.ts
|
||||||
|
pnpm-lock.yaml
|
||||||
|
public/*.png
|
||||||
|
public/*.ico
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"semi": false,
|
||||||
|
"singleQuote": true,
|
||||||
|
"trailingComma": "all",
|
||||||
|
"printWidth": 100
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
|
||||||
@@ -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.
|
||||||
@@ -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).
|
||||||
@@ -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)
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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),-,-,-,-
|
||||||
|
@@ -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
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
import { withSerwist } from '@serwist/turbopack'
|
||||||
|
|
||||||
|
export default withSerwist({
|
||||||
|
// Configuration Next.js (Turbopack activé par défaut dans Next 16)
|
||||||
|
})
|
||||||
@@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
allowBuilds:
|
||||||
|
sharp: true
|
||||||
|
'@tailwindcss/oxide': true
|
||||||
|
'@swc/core': true
|
||||||
|
esbuild: true
|
||||||
|
unrs-resolver: true
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
const config = {
|
||||||
|
plugins: {
|
||||||
|
'@tailwindcss/postcss': {},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export default config
|
||||||
|
After Width: | Height: | Size: 782 B |
|
After Width: | Height: | Size: 483 B |
@@ -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 |
|
After Width: | Height: | Size: 2.7 KiB |
|
After Width: | Height: | Size: 1017 B |
|
After Width: | Height: | Size: 3.0 KiB |
|
After Width: | Height: | Size: 455 B |
|
After Width: | Height: | Size: 25 KiB |
@@ -0,0 +1 @@
|
|||||||
|
@import 'tailwindcss';
|
||||||
@@ -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>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -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',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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()
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
export default function Home() {
|
||||||
|
return (
|
||||||
|
<main>
|
||||||
|
<h1>Hello world</h1>
|
||||||
|
</main>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -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,
|
||||||
|
},
|
||||||
|
)
|
||||||
@@ -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()
|
||||||
@@ -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>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -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,
|
||||||
|
}),
|
||||||
|
})
|
||||||
@@ -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"]
|
||||||
|
}
|
||||||
@@ -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/**',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||