Capitalisation complète — app-alexandrie & app-template-resto (23-03-2026)

Intègre ~50 entrées depuis 95_a_capitaliser.md vers les fichiers validés :
- backend risques : +15 (GET sans authz, TOCTOU tenantId, TTL UTC, AdminRoleGuard, P3014...)
- backend patterns : P2002 amendé (create+update) + 10 nouveaux (Decimal, URL safe, EN enforcement...)
- frontend risques : +21 (defaultValue/key, useTransition global, consent state, Tailwind invalide...)
- frontend patterns : +6 (click-to-load, toggle optimiste, Server Action retourne entité...)
- debug/postmortem : export{fn} ne crée pas de binding local

95_a_capitaliser.md remis à l'état initial vide.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
MaksTinyWorkshop
2026-03-23 15:02:14 +01:00
parent 2e6ed9d374
commit e61e3d5ea8
6 changed files with 1485 additions and 1288 deletions

View File

@@ -12,7 +12,7 @@ Il sert de **mémoire durable** pour éviter :
- de redélibérer éternellement sur des sujets déjà tranchés,
- de propager des “bonnes pratiques” théoriques non éprouvées.
Dernière mise à jour : 20-03-2026
Dernière mise à jour : 23-03-2026
---
@@ -29,6 +29,11 @@ Dernière mise à jour : 20-03-2026
- [Tests de styles React Native sans renderer JSX](#pattern-tests-styles-sans-renderer)
- [Export des styles de composant pour réutilisation partielle](#pattern-export-styles-composant)
- [Token typography par usage sémantique (React Native)](#pattern-token-typography-semantique)
- [Click-to-load strict pour les embeds tiers (iframe/widget)](#pattern-click-to-load-embeds-tiers)
- [Toggle optimiste avec rollback (React Server Action)](#pattern-toggle-optimiste-rollback)
- [Server Action retournant l'entité — élimination de `router.refresh()` sur create/edit](#pattern-server-action-retourne-entite)
- [ESLint flat config avec presets Next.js (`eslint.config.mjs`)](#pattern-eslint-flat-config-nextjs)
- [Grilles 2 colonnes FR/EN — mobile-first](#pattern-grilles-2-colonnes-mobile-first)
---
@@ -634,3 +639,192 @@ mediumText12: { fontSize: 12, fontWeight: 500 }, // ambigu, réutilisé
- on met à jour la date
- on précise le nouveau contexte
- En cas de doute → le pattern nentre pas encore ici
---
<a id="pattern-click-to-load-embeds-tiers"></a>
## Pattern : Click-to-load strict pour les embeds tiers (iframe/widget)
### Synthèse
- **Objectif** : ne charger aucun service tiers sans action explicite de lutilisateur (performance + consentement implicite).
- **Contexte** : site/webapp avec modules de réservation, map, chat ou tout embed iframe à la demande.
- **Quand lutiliser** : dès quun embed tiers est chargé à la demande (pas au premier rendu).
- **Quand léviter** : si lembed est central à la page et doit être visible immédiatement.
### Analyse
- **Avantages** :
- LCP non pollué par des tiers (performance-first)
- Aucun tiers ne reçoit de données utilisateur sans action volontaire (consentement implicite)
- Fallback toujours disponible en cas derreur iframe
- **Limites / vigilance** :
- Le fallback (lien externe + `tel:`) doit être actionnable même si lembed échoue
### Validation
- Validé le : 21-03-2026
- Contexte technique : React / Next.js — app-template-resto
### Implémentation
```tsx
const [loaded, setLoaded] = useState(false);
const [errored, setErrored] = useState(false);
if (errored) return <a href={url}>Ouvrir {label}</a>;
return (
<>
{!loaded && <button onClick={() => setLoaded(true)}>Charger {label}</button>}
{loaded && <iframe src={url} onError={() => setErrored(true)} />}
</>
);
```
---
<a id="pattern-toggle-optimiste-rollback"></a>
## Pattern : Toggle optimiste avec rollback (React Server Action)
### Synthèse
- **Objectif** : masquer la latence serveur sur un toggle boolean en mettant à jour lUI immédiatement, avec rollback en cas derreur.
- **Contexte** : toggles boolean (visibilité, disponibilité, settings) où la latence doit être masquée.
- **Quand lutiliser** : toggles sans besoin de re-fetcher lentité entière après mutation.
- **Quand léviter** : mutations qui retournent des données complexes → préférer le pattern "Server Action retournant lentité".
### Validation
- Validé le : 21-03-2026
- Contexte technique : React / Next.js App Router — app-template-resto
### Implémentation
```tsx
const [optimistic, setOptimistic] = useState(initialValue);
async function handleToggle() {
const prev = optimistic;
setOptimistic(!prev); // update immédiat
try {
await toggleAction(!prev);
router.refresh(); // synchronise le Server Component parent
} catch {
setOptimistic(prev); // rollback si erreur
}
}
```
---
<a id="pattern-server-action-retourne-entite"></a>
## Pattern : Server Action retournant lentité — élimination de `router.refresh()` sur create/edit
### Synthèse
- **Objectif** : mettre à jour létat local directement avec les données réelles retournées par le serveur, sans round-trip SSR supplémentaire.
- **Contexte** : liste ditems managée côté client (`useState`) avec création et modification via Server Actions.
- **Quand lutiliser** : create et edit dentités dans une liste. Plus performant que toggle optimiste + `router.refresh()`.
- **Quand léviter** : simples toggles boolean → le pattern optimiste avec rollback suffit.
### Analyse
- **Avantages vs toggle optimiste + `router.refresh()` :**
- Zéro aller-retour SSR supplémentaire (~500ms2s économisés sur mobile)
- État local garanti cohérent avec la DB (données réelles, pas calculées localement)
- Pas de flash de rechargement
- **Limites / vigilance** :
- `revalidatePath` reste nécessaire pour invalider le cache des pages publiques
### Validation
- Validé le : 22-03-2026
- Contexte technique : React / Next.js App Router — app-template-resto story 3.8
### Implémentation
```typescript
// Repository — retourne lentité complète
export async function createItem(tenantId: string, data: Input): Promise<ItemRow> {
return prisma.item.create({ data: { tenantId, ...data }, select: { ...fullSelect } });
}
// Action — retourne la donnée au client
export async function createItemAction(formData: FormData): Promise<ItemRow> {
const actor = await requireOwner();
const item = await createItem(actor.tenantId, input);
revalidatePath("/dashboard/..."); // invalider cache pages publiques
return item; // ← clé : retourner lentité
}
// Client — mise à jour locale sans round-trip SSR
const created = await createItemAction(formData);
setItems((prev) => [...prev, created]); // pas de router.refresh()
```
**Pour les entités avec relations :** utiliser un helper `findItemById(tenantId, id)` appelé après la mutation pour retourner la forme complète avec les relations résolues.
---
<a id="pattern-eslint-flat-config-nextjs"></a>
## Pattern : ESLint flat config avec presets Next.js (`eslint.config.mjs`)
### Synthèse
- **Objectif** : éviter les bugs de compatibilité de lancien `.eslintrc` avec Next.js récent.
- **Contexte** : projet Next.js récent utilisant déjà le flat config ESLint.
- **Quand lutiliser** : nouveau projet Next.js ou migration ESLint.
- **Quand léviter** : si le projet doit rester compatible avec des outils legacy ESLint.
### Validation
- Validé le : 16-03-2026
- Contexte technique : Next.js 16+ / ESLint flat config — app-template-resto
### Implémentation
```javascript
// eslint.config.mjs
import nextPlugin from "@next/eslint-plugin-next";
export default [
...nextPlugin.configs["core-web-vitals"],
...nextPlugin.configs["typescript"],
{
rules: {
// overrides ciblés ici
},
},
];
```
---
<a id="pattern-grilles-2-colonnes-mobile-first"></a>
## Pattern : Grilles 2 colonnes FR/EN — mobile-first
### Synthèse
- **Objectif** : afficher les champs FR + EN côte à côte sur desktop, en colonne unique sur mobile.
- **Contexte** : formulaires dashboard avec champs bilingues FR/EN côte à côte.
- **Quand lutiliser** : tout formulaire avec colonnes parallèles sur un projet mobile-first.
- **Quand léviter** : si les champs sont indépendants et nont pas de relation visuelle FR/EN.
### Validation
- Validé le : 22-03-2026
- Contexte technique : Tailwind CSS / React — app-template-resto
### Implémentation
```html
<!-- ✅ Mobile-first — colonne unique sur < 640px, 2 colonnes sur ≥ 640px -->
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
<input placeholder="Nom (FR)" />
<input placeholder="Name (EN)" />
</div>
<!-- ❌ À éviter — 2 colonnes trop étroites sur mobile -->
<div class="grid grid-cols-2 gap-4">...</div>
```