Tutoriel guidé (didacticiel) : tour général d'orientation + tour dédié à la correction des métadonnées de recette #42

Closed
opened 2026-09-05 19:02:34 +02:00 by kyuno · 3 comments
Owner

Contexte

Accompagner l'utilisateur à sa première connexion avec un didacticiel :
popovers explicatifs + mise en évidence des menus, sur les écrans clés.
Décisions prises avec l'utilisateur : deux tours distincts — un tour
général d'orientation, et un tour spécifique pour la correction des
métadonnées de recette (technique/ingrédients/ustensiles/température) — tous
les deux rejouables à la demande depuis les paramètres.

Deux explorations ont établi que rien de tout ça n'a de précédent dans le
repo
:

  • Onboarding existant (/onboarding/regime → /foyer → /sources →
    /allergenes) : mécanisme totalement différent, à but data-collection,
    sans aucun flag de complétion persistant nulle part — juste une navigation
    impérative une seule fois après signup() (SignupPage.tsx:55,
    void navigate("/onboarding/regime")). login() ne repasse jamais par
    l'onboarding, quel que soit l'état du profil. Rien à réutiliser tel quel
    pour "a déjà vu le tutoriel", mais un précédent de forme utile existe dans
    le schéma : Ingredient.reviewedAt (DateTime? nullable, "stampé une fois
    fait, null en attente") est la colonne la plus proche de ce dont on a
    besoin.
  • Overlays existants (Dialog.tsx, Tooltip.tsx, WeekNavigator's
    CalendarPopover, TechStepCorrectionPopover) : aucun ne fait de
    suivi de position en temps réel d'un élément arbitraire — chacun est soit
    un <dialog> natif (showModal(), top layer du navigateur), soit ancré en
    CSS à un parent fixe voisin, soit en flux normal. Zéro
    getBoundingClientRect de tracking (le seul hit du repo est un hit-test
    ponctuel dans Dialog.tsx, pas du positionnement), zéro ResizeObserver,
    zéro createPortal, zéro échelle de z-index dans _theme.scss (juste des
    10 ad hoc dans quelques fichiers). Un tour qui doit surligner des
    éléments arbitraires à travers plusieurs pages est une brique d'UI
    entièrement nouvelle
    pour ce repo, pas une extension d'un patron
    existant.
  • Aucun élément (items de sidebar, boutons...) n'a d'id/data-* à cibler
    aujourd'hui — à ajouter partout où le tour doit pointer.

Choix de librairie — recherché en direct (web + vérification GitHub)
plutôt que deviné : driver.js, pas react-joyride. Comparatif :
react-joyride est le plus populaire pour React (~340k téléchargements/
semaine) mais sans publication depuis ~9 mois et incompatible React 19 sans
version instable, ~34 Ko gzippé avec ses dépendances. shepherd.js/
intro.js sont en licence AGPL sauf achat d'une licence commerciale — à
éviter pour ce projet. driver.js : ~5 Ko gzippé, licence MIT, zéro
dépendance, TypeScript vanilla (pas de binding React officiel, mais son API
impérative simple s'appelle très bien depuis un hook/contexte React).

Conception — persistance (pensée pour N tours dès le départ)

Une nouvelle table plutôt qu'un flag unique, puisqu'il y a déjà 2 tours
connus et vraisemblablement d'autres à venir — même convention "clé stable"
que Diet.key/Unit.key/TechStep.key/Source.key :

model TourProgress {
  userProfileId Int      @map("user_profile_id")
  tourKey       String   @map("tour_key") // "general" | "techStepMetadata" | ...
  completedAt   DateTime @default(now()) @map("completed_at")

  userProfile UserProfile @relation(fields: [userProfileId], references: [id], onDelete: Cascade)

  @@id([userProfileId, tourKey])
  @@map("tour_progress")
}

Présence d'une ligne = "vu" (passer un tour marque aussi complété — pas de
re-nag après un skip volontaire). GET /profile/tours →
{ completedTourKeys: string[] }, POST /profile/tours/:tourKey/complete →
upsert la ligne (idempotent).

Ne bénéficie pas du passage "gratuit" par /auth/me — vérifié :
toSafeProfile() (apps/api/src/lib/safe-profile.ts) ne fait que retirer
passwordHash du reste de la ligne Prisma UserProfile, donc une colonne
ajoutée directement là y apparaîtrait automatiquement, sans nouvel endpoint.
Une table séparée comme TourProgress n'a pas ce raccourci (il faut son
propre fetch, comme theme via /preferences aujourd'hui) — mais le gain
d'extensibilité (aucune migration pour un 3e tour futur) l'emporte sur cette
petite latence supplémentaire. Un TourProvider (contexte React, même
position que ThemeProvider dans main.tsx, même patron que
ThemeContext.tsx : effet gardé sur user?.id, pas de localStorage)
charge completedTourKeys une fois la session connue.

Conception — moteur de tour réutilisable

interface TourStep {
  route?: string;   // navigue ici avant cette étape si pas déjà dessus
  target: string;   // sélecteur CSS (attribut data-tour-*)
  titleKey: string; // clé i18n
  contentKey: string;
}
interface TourDefinition {
  key: string;
  steps: TourStep[];
}

useTour() expose startTour(key) / isTourCompleted(key), pilote
driver.js (navigation React Router entre étapes situées sur des routes
différentes ; driver.js gère le surlignage/popover une fois l'élément
monté), et appelle l'endpoint de complétion à la fin de la séquence ou au
clic sur "Passer".

Portée volontairement limitée pour cette itération : le tour ne cible
jamais un élément qui n'existe qu'à l'intérieur d'un <dialog> natif ouvert
(Dialog.tsx) — un <dialog> en showModal() vit dans le top layer du
navigateur, au-dessus de tout, y compris l'overlay de driver.js qui n'est
qu'un élément positionné classique. Cibler l'intérieur d'un dialogue
casserait visuellement l'effet de surlignage (le dialogue s'afficherait
par-dessus le halo/popover du tour). Les deux tours ci-dessous restent donc
sur du contenu de page normale.

Tour 1 — Général (orientation)

Déclenché automatiquement à l'arrivée sur / si
!isTourCompleted("general") — couvre aussi bien la sortie du wizard
d'onboarding que la prochaine connexion d'un utilisateur déjà existant qui
ne l'a jamais vu (rien à distinguer entre les deux cas, la condition est la
même). Étapes (contenu réel à affiner en revue de PR) :

  1. Sidebar — Planning, Recettes, Liste de courses (chaque NavLink de
    AppLayout.tsx's NAV_ITEMS gagne un data-tour-nav-<key>).
  2. Grille du planning (PlanningPage) — comment lire la semaine.
  3. Bouton "+" d'un créneau — juste montrer le bouton qui ouvre
    RecipePickerDialog, sans l'ouvrir (limite du <dialog> ci-dessus).
  4. Bouton "Cuisiner" de l'en-tête du planning.
  5. Aperçu de la liste de courses.

Tour 2 — Correction des métadonnées de recette

Déclenché la première fois que StepDescription's activeCorrection
devient non-null en mode éditable
(apps/web/src/features/recipes/steps/StepDescription.tsx) si
!isTourCompleted("techStepMetadata").

Dépendance à noter avant de définir les étapes précises :
l'issue #36 (le
popover de correction se ferme au lieu de rester ouvert pendant la sélection
d'un ingrédient/ustensile/température) est encore ouverte — ce tour ne
devrait pas apprendre un geste actuellement cassé. Soit #36 est corrigé
d'abord, soit ce tour se limite pour l'instant à la partie qui fonctionne
déjà (sélectionner du texte, choisir une technique) sans couvrir l'ajout
d'ingrédient/ustensile/température tant que #36 n'est pas réglé.

Conception — rejouer un tour depuis les paramètres

Nouvelle petite section (page dédiée /parametres/aide, ou bloc ajouté à
AccountSettingsPage.tsx — à trancher en implémentation) listant chaque
tour connu avec un bouton "Revoir" — conçue pour scaler à un 3e tour futur
sans changement de structure (simple map sur le registre de
TourDefinition, pas une entrée codée en dur par tour).

Fichiers à toucher

  • apps/api/prisma/schema.prisma — modèle TourProgress + migration.
  • apps/api/src/modules/profile/ — routes/service GET/POST /profile/tours*.
  • apps/web/src/features/tour/ (nouveau) — TourContext.tsx, définitions
    des 2 tours, wrapper autour de driver.js.
  • apps/web/src/layouts/AppLayout.tsx, pages/planning/PlanningPage.tsx,
    pages/shopping-list/ShoppingListPage.tsx — attributs data-tour-* +
    déclenchement du tour général.
  • apps/web/src/features/recipes/steps/StepDescription.tsx — déclenchement
    du tour métadonnées.
  • apps/web/src/pages/settings/ — section "Revoir un tutoriel".
  • apps/web/package.json — ajoute driver.js.
  • apps/web/src/locales/fr/translation.json — namespace tour.*.

Tests

  • Mocha : GET/POST /profile/tours/:key/complete (upsert idempotent,
    ne casse rien si rappelé plusieurs fois).
  • Cypress e2e : un nouvel utilisateur voit le tour général démarrer sur
    / et peut le terminer/passer ; une fois complété, aucun second
    déclenchement automatique (isTourCompleted) ; le lien "Revoir" dans les
    paramètres relance la séquence à la demande indépendamment de l'état
    "déjà vu".

Hors scope

  • Tout tour au-delà des deux définis ici (import de recettes, session de
    cuisine, foyer partagé...) — le registre est conçu pour en accueillir
    facilement plus tard, aucun n'est écrit dans cette itération.
  • Cibler un élément à l'intérieur d'un <dialog> ouvert (limite du top
    layer expliquée plus haut).
  • Rédaction finale du contenu (titres/textes exacts de chaque étape) — la
    structure et les cibles sont posées ici, le texte se peaufine en revue de
    PR.

🤖 Filed by Claude Code

## Contexte Accompagner l'utilisateur à sa première connexion avec un didacticiel : popovers explicatifs + mise en évidence des menus, sur les écrans clés. Décisions prises avec l'utilisateur : **deux tours distincts** — un tour général d'orientation, et un tour spécifique pour la correction des métadonnées de recette (technique/ingrédients/ustensiles/température) — tous les deux **rejouables à la demande** depuis les paramètres. Deux explorations ont établi que **rien de tout ça n'a de précédent dans le repo** : - **Onboarding existant** (`/onboarding/regime` → `/foyer` → `/sources` → `/allergenes`) : mécanisme totalement différent, à but data-collection, sans aucun flag de complétion persistant nulle part — juste une navigation impérative une seule fois après `signup()` (`SignupPage.tsx:55`, `void navigate("/onboarding/regime")`). `login()` ne repasse jamais par l'onboarding, quel que soit l'état du profil. Rien à réutiliser tel quel pour "a déjà vu le tutoriel", mais un précédent de forme utile existe dans le schéma : `Ingredient.reviewedAt` (`DateTime?` nullable, "stampé une fois fait, `null` en attente") est la colonne la plus proche de ce dont on a besoin. - **Overlays existants** (`Dialog.tsx`, `Tooltip.tsx`, `WeekNavigator`'s `CalendarPopover`, `TechStepCorrectionPopover`) : **aucun** ne fait de suivi de position en temps réel d'un élément arbitraire — chacun est soit un `<dialog>` natif (`showModal()`, top layer du navigateur), soit ancré en CSS à un parent fixe voisin, soit en flux normal. Zéro `getBoundingClientRect` de tracking (le seul hit du repo est un hit-test ponctuel dans `Dialog.tsx`, pas du positionnement), zéro `ResizeObserver`, zéro `createPortal`, zéro échelle de z-index dans `_theme.scss` (juste des `10` ad hoc dans quelques fichiers). Un tour qui doit surligner des éléments arbitraires à travers plusieurs pages est une **brique d'UI entièrement nouvelle** pour ce repo, pas une extension d'un patron existant. - Aucun élément (items de sidebar, boutons...) n'a d'`id`/`data-*` à cibler aujourd'hui — à ajouter partout où le tour doit pointer. **Choix de librairie** — recherché en direct (web + vérification GitHub) plutôt que deviné : **`driver.js`**, pas `react-joyride`. Comparatif : `react-joyride` est le plus populaire pour React (~340k téléchargements/ semaine) mais sans publication depuis ~9 mois et incompatible React 19 sans version instable, ~34 Ko gzippé avec ses dépendances. `shepherd.js`/ `intro.js` sont en licence **AGPL** sauf achat d'une licence commerciale — à éviter pour ce projet. `driver.js` : ~5 Ko gzippé, licence **MIT**, zéro dépendance, TypeScript vanilla (pas de binding React officiel, mais son API impérative simple s'appelle très bien depuis un hook/contexte React). ## Conception — persistance (pensée pour N tours dès le départ) Une nouvelle table plutôt qu'un flag unique, puisqu'il y a déjà 2 tours connus et vraisemblablement d'autres à venir — même convention "clé stable" que `Diet.key`/`Unit.key`/`TechStep.key`/`Source.key` : ```prisma model TourProgress { userProfileId Int @map("user_profile_id") tourKey String @map("tour_key") // "general" | "techStepMetadata" | ... completedAt DateTime @default(now()) @map("completed_at") userProfile UserProfile @relation(fields: [userProfileId], references: [id], onDelete: Cascade) @@id([userProfileId, tourKey]) @@map("tour_progress") } ``` Présence d'une ligne = "vu" (passer un tour marque aussi complété — pas de re-nag après un skip volontaire). `GET /profile/tours` → `{ completedTourKeys: string[] }`, `POST /profile/tours/:tourKey/complete` → upsert la ligne (idempotent). Ne bénéficie pas du passage "gratuit" par `/auth/me` — vérifié : `toSafeProfile()` (`apps/api/src/lib/safe-profile.ts`) ne fait que retirer `passwordHash` du reste de la ligne Prisma `UserProfile`, donc une colonne ajoutée directement là y apparaîtrait automatiquement, sans nouvel endpoint. Une table séparée comme `TourProgress` n'a pas ce raccourci (il faut son propre fetch, comme `theme` via `/preferences` aujourd'hui) — mais le gain d'extensibilité (aucune migration pour un 3e tour futur) l'emporte sur cette petite latence supplémentaire. Un `TourProvider` (contexte React, même position que `ThemeProvider` dans `main.tsx`, même patron que `ThemeContext.tsx` : effet gardé sur `user?.id`, pas de `localStorage`) charge `completedTourKeys` une fois la session connue. ## Conception — moteur de tour réutilisable ```ts interface TourStep { route?: string; // navigue ici avant cette étape si pas déjà dessus target: string; // sélecteur CSS (attribut data-tour-*) titleKey: string; // clé i18n contentKey: string; } interface TourDefinition { key: string; steps: TourStep[]; } ``` `useTour()` expose `startTour(key)` / `isTourCompleted(key)`, pilote `driver.js` (navigation React Router entre étapes situées sur des routes différentes ; `driver.js` gère le surlignage/popover une fois l'élément monté), et appelle l'endpoint de complétion à la fin de la séquence ou au clic sur "Passer". **Portée volontairement limitée pour cette itération** : le tour ne cible jamais un élément qui n'existe qu'à l'intérieur d'un `<dialog>` natif ouvert (`Dialog.tsx`) — un `<dialog>` en `showModal()` vit dans le top layer du navigateur, au-dessus de tout, y compris l'overlay de `driver.js` qui n'est qu'un élément positionné classique. Cibler l'intérieur d'un dialogue casserait visuellement l'effet de surlignage (le dialogue s'afficherait par-dessus le halo/popover du tour). Les deux tours ci-dessous restent donc sur du contenu de page normale. ## Tour 1 — Général (orientation) Déclenché automatiquement à l'arrivée sur `/` si `!isTourCompleted("general")` — couvre aussi bien la sortie du wizard d'onboarding que la prochaine connexion d'un utilisateur déjà existant qui ne l'a jamais vu (rien à distinguer entre les deux cas, la condition est la même). Étapes (contenu réel à affiner en revue de PR) : 1. Sidebar — Planning, Recettes, Liste de courses (chaque `NavLink` de `AppLayout.tsx`'s `NAV_ITEMS` gagne un `data-tour-nav-<key>`). 2. Grille du planning (`PlanningPage`) — comment lire la semaine. 3. Bouton "+" d'un créneau — juste montrer le bouton qui ouvre `RecipePickerDialog`, sans l'ouvrir (limite du `<dialog>` ci-dessus). 4. Bouton "Cuisiner" de l'en-tête du planning. 5. Aperçu de la liste de courses. ## Tour 2 — Correction des métadonnées de recette Déclenché la première fois que `StepDescription`'s `activeCorrection` devient non-null en mode éditable (`apps/web/src/features/recipes/steps/StepDescription.tsx`) si `!isTourCompleted("techStepMetadata")`. **Dépendance à noter avant de définir les étapes précises** : [l'issue #36](https://git.dev.kyuno.fr/kyuno/batchCooking/issues/36) (le popover de correction se ferme au lieu de rester ouvert pendant la sélection d'un ingrédient/ustensile/température) est encore ouverte — ce tour ne devrait pas apprendre un geste actuellement cassé. Soit #36 est corrigé d'abord, soit ce tour se limite pour l'instant à la partie qui fonctionne déjà (sélectionner du texte, choisir une technique) sans couvrir l'ajout d'ingrédient/ustensile/température tant que #36 n'est pas réglé. ## Conception — rejouer un tour depuis les paramètres Nouvelle petite section (page dédiée `/parametres/aide`, ou bloc ajouté à `AccountSettingsPage.tsx` — à trancher en implémentation) listant chaque tour connu avec un bouton "Revoir" — conçue pour scaler à un 3e tour futur sans changement de structure (simple `map` sur le registre de `TourDefinition`, pas une entrée codée en dur par tour). ## Fichiers à toucher - `apps/api/prisma/schema.prisma` — modèle `TourProgress` + migration. - `apps/api/src/modules/profile/` — routes/service `GET`/`POST /profile/tours*`. - `apps/web/src/features/tour/` (nouveau) — `TourContext.tsx`, définitions des 2 tours, wrapper autour de `driver.js`. - `apps/web/src/layouts/AppLayout.tsx`, `pages/planning/PlanningPage.tsx`, `pages/shopping-list/ShoppingListPage.tsx` — attributs `data-tour-*` + déclenchement du tour général. - `apps/web/src/features/recipes/steps/StepDescription.tsx` — déclenchement du tour métadonnées. - `apps/web/src/pages/settings/` — section "Revoir un tutoriel". - `apps/web/package.json` — ajoute `driver.js`. - `apps/web/src/locales/fr/translation.json` — namespace `tour.*`. ## Tests - **Mocha** : `GET`/`POST /profile/tours/:key/complete` (upsert idempotent, ne casse rien si rappelé plusieurs fois). - **Cypress e2e** : un nouvel utilisateur voit le tour général démarrer sur `/` et peut le terminer/passer ; une fois complété, aucun second déclenchement automatique (`isTourCompleted`) ; le lien "Revoir" dans les paramètres relance la séquence à la demande indépendamment de l'état "déjà vu". ## Hors scope - Tout tour au-delà des deux définis ici (import de recettes, session de cuisine, foyer partagé...) — le registre est conçu pour en accueillir facilement plus tard, aucun n'est écrit dans cette itération. - Cibler un élément à l'intérieur d'un `<dialog>` ouvert (limite du top layer expliquée plus haut). - Rédaction finale du contenu (titres/textes exacts de chaque étape) — la structure et les cibles sont posées ici, le texte se peaufine en revue de PR. --- 🤖 Filed by [Claude Code](https://claude.com/claude-code)
kyuno added the
enhancement
planned
labels 2026-09-05 19:02:34 +02:00
kyuno added the
Low
label 2026-09-05 19:32:02 +02:00
Author
Owner

Révisé par issue #44 : le registre TourDefinition[] statique en TypeScript (avec titres/contenus en clés i18n) décrit ci-dessus est remplacé par un modèle en base (TourScenario/TourScenarioStep/TourTarget, texte libre plutôt que clés i18n) éditable depuis l'admin. Implémenter #44 en premier (ou en même temps) — le moteur de tour (useTour(), driver.js) doit consommer GET /reference/tours plutôt que le registre statique décrit ici.

🤖 Filed by Claude Code

Révisé par [issue #44](https://git.dev.kyuno.fr/kyuno/batchCooking/issues/44) : le registre `TourDefinition[]` statique en TypeScript (avec titres/contenus en clés i18n) décrit ci-dessus est remplacé par un modèle en base (`TourScenario`/`TourScenarioStep`/`TourTarget`, texte libre plutôt que clés i18n) éditable depuis l'admin. Implémenter #44 en premier (ou en même temps) — le moteur de tour (`useTour()`, `driver.js`) doit consommer `GET /reference/tours` plutôt que le registre statique décrit ici. 🤖 Filed by [Claude Code](https://claude.com/claude-code)
Author
Owner

Livré en 4 PR (avec #44, comme prévu par son corps) :

  • #58 — modèle de données (TourTarget / TourScenario / TourScenarioStep / TourProgress), migration, seed des 2 scénarios + 9 cibles, GET /reference/tours, GET/POST /profile/tours* (idempotent, tour.completed / tour.skipped en analytics).
  • #59 — CRUD admin /admin/api/tours*.
  • #60 — moteur front : dépendance driver.js, useTour() / TourProvider, wrapper tour-engine.ts (navigation inter-routes + issue unique), attributs data-tour-*, déclenchement auto (tour général sur /, tour métadonnées à l'ouverture du popover de correction), section « Revoir un tutoriel » dans les préférences utilisateur.
  • #61 — page admin /admin/didacticiels.

Tests : tour.feature (5 scénarios) + admin-tours.cy.ts (4) + Mocha (/reference/tours, /profile/tours, CRUD admin).

Réserve : le tour techStepMetadata ne fait pour l'instant que 2 étapes (sélectionner du texte, choisir une technique) — l'ajout d'ingrédient/ustensile/température attend la correction de #36 (le popover se ferme en pleine sélection), exactement comme ce ticket l'anticipait. À rouvrir/compléter une fois #36 réglé, ou à fermer si la v1 convient.

🤖 via Claude Code

Livré en 4 PR (avec #44, comme prévu par son corps) : - **#58** — modèle de données (`TourTarget` / `TourScenario` / `TourScenarioStep` / `TourProgress`), migration, seed des 2 scénarios + 9 cibles, `GET /reference/tours`, `GET`/`POST /profile/tours*` (idempotent, `tour.completed` / `tour.skipped` en analytics). - **#59** — CRUD admin `/admin/api/tours*`. - **#60** — moteur front : dépendance `driver.js`, `useTour()` / `TourProvider`, wrapper `tour-engine.ts` (navigation inter-routes + issue unique), attributs `data-tour-*`, déclenchement auto (tour général sur `/`, tour métadonnées à l'ouverture du popover de correction), section « Revoir un tutoriel » dans les préférences utilisateur. - **#61** — page admin `/admin/didacticiels`. Tests : `tour.feature` (5 scénarios) + `admin-tours.cy.ts` (4) + Mocha (`/reference/tours`, `/profile/tours`, CRUD admin). **Réserve** : le tour `techStepMetadata` ne fait pour l'instant que 2 étapes (sélectionner du texte, choisir une technique) — l'ajout d'ingrédient/ustensile/température attend la correction de #36 (le popover se ferme en pleine sélection), exactement comme ce ticket l'anticipait. À rouvrir/compléter une fois #36 réglé, ou à fermer si la v1 convient. 🤖 via [Claude Code](https://claude.com/claude-code)
Author
Owner

Fermeture tardive — le travail est en réalité déjà livré depuis le 2026-09-06 via 4 PR mergées sur main : #58 (socle données + API), #59 (CRUD admin API), #60 (moteur driver.js), #61 (UI admin CRUD). Les deux tours fonctionnent réellement (TourContext/tour-engine.ts, déclenchement auto sur PlanningPage et StepDescription, rejouable depuis /parametres/preferences-utilisateur). Ce ticket était juste resté ouvert par oubli.

Fermeture tardive — le travail est en réalité déjà livré depuis le 2026-09-06 via 4 PR mergées sur `main` : #58 (socle données + API), #59 (CRUD admin API), #60 (moteur driver.js), #61 (UI admin CRUD). Les deux tours fonctionnent réellement (`TourContext`/`tour-engine.ts`, déclenchement auto sur `PlanningPage` et `StepDescription`, rejouable depuis `/parametres/preferences-utilisateur`). Ce ticket était juste resté ouvert par oubli.
kyuno closed this issue 2026-09-07 11:35:39 +02:00
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference: kyuno/batchCooking#42
No description provided.