From 2502dab05c1d9399bbe552aabf7fa036ba511277 Mon Sep 17 00:00:00 2001 From: julien rata Date: Thu, 18 Jun 2026 08:11:09 +0200 Subject: [PATCH] =?UTF-8?q?feat:=20ajout=20de=20l'outil=20p=C3=A9dagogique?= =?UTF-8?q?=20=C2=AB=20Clean=20Code=20=C2=BB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Hub /clean-code (grille de cartes) + détail /clean-code/:chapitre, calqués sur SOLID : modèle CleanCodePrinciple, contenu statique CLEAN_CODE_PRINCIPLES (8 chapitres), routes lazy, entrée navLinks et registre Tool (seed, order 6). Co-Authored-By: Claude Opus 4.8 (1M context) --- DECISIONS.md | 28 ++ backend/seed/data.js | 10 + frontend/src/app/app.component.ts | 1 + frontend/src/app/app.routes.ts | 14 + .../app/core/data/clean-code-principles.ts | 361 ++++++++++++++++++ .../core/models/clean-code-principle.model.ts | 32 ++ .../clean-code-detail.component.html | 90 +++++ .../clean-code-detail.component.scss | 121 ++++++ .../clean-code-detail.component.ts | 88 +++++ .../clean-code/clean-code.component.html | 55 +++ .../clean-code/clean-code.component.scss | 160 ++++++++ .../clean-code/clean-code.component.ts | 42 ++ 12 files changed, 1002 insertions(+) create mode 100644 frontend/src/app/core/data/clean-code-principles.ts create mode 100644 frontend/src/app/core/models/clean-code-principle.model.ts create mode 100644 frontend/src/app/features/clean-code-detail/clean-code-detail.component.html create mode 100644 frontend/src/app/features/clean-code-detail/clean-code-detail.component.scss create mode 100644 frontend/src/app/features/clean-code-detail/clean-code-detail.component.ts create mode 100644 frontend/src/app/features/clean-code/clean-code.component.html create mode 100644 frontend/src/app/features/clean-code/clean-code.component.scss create mode 100644 frontend/src/app/features/clean-code/clean-code.component.ts diff --git a/DECISIONS.md b/DECISIONS.md index cbbeeb2..cfb59f4 100644 --- a/DECISIONS.md +++ b/DECISIONS.md @@ -30,6 +30,34 @@ Entrées **antéchronologiques** (la plus récente en haut). La date au format ` --- +## 2026-06-18 — Ajout de l'outil pédagogique « Clean Code » + +- **Contexte** : enrichir le catalogue d'un outil sur les chapitres-clés de + *Clean Code* (Robert C. Martin), sur le même modèle que SOLID (hub + détail), + sans refactor du cœur. +- **Décision** : suivre ADD-A-TOOL.md à la lettre en calquant SOLID. Nouveau + modèle `CleanCodePrinciple` (champs techniques anglais `slug`/`icon`, contenu + FR), constante statique `CLEAN_CODE_PRINCIPLES` (8 chapitres : noms, fonctions, + commentaires, mise en forme, gestion des erreurs, limites, classes, tests), + hub `/clean-code` (grille de cartes) + détail `/clean-code/:chapitre` + (résolution réactive du param, repli vers le hub si slug inconnu, réutilise + `SequentialNavComponent` et `neighborSlug`). Routes lazy, entrée `navLinks` + après SOLID, entrée seed `Tool` `order: 6`. Contenu pédagogique **statique + côté front** ; seul le registre `Tool` passe par l'API (cf. STATE-AND-DATA.md). +- **Options écartées** : (1) réutiliser tel quel le modèle `SolidPrinciple` + (rejeté — le champ `lettre` est propre à l'acronyme SOLID ; on le remplace par + `numero` de chapitre, tout en gardant `nomEn`/`nomFr` qui restent pertinents) ; + (2) un composant de détail générique partagé SOLID ↔ Clean Code (rejeté — même + raison que la factorisation des détails du 2026-06-17 : abstraction « au cas + où », templates de contenu trop proches mais pas identiques) ; (3) servir le + contenu via l'API/seed (rejeté — le contenu enseigné reste statique). +- **Pourquoi** : extensibilité par addition, cohérence visuelle (DA festive, + tokens `--cc-*`, mixins `shared/styles/detail`) et structurelle avec les outils + existants, sans dépendance ni refactor. +- **Trace** : branche `feat/clean-code` — `npm run check` vert (front : lint + + build ; back : lint + 9 tests). Re-seed (`npm run seed`) non lancé (écrit en + base) — à exécuter au déploiement. + ## 2026-06-17 — Toolbar (shell) alignée sur la DA festive - **Contexte** : la toolbar gardait l'esthétique « atelier » sobre (fond diff --git a/backend/seed/data.js b/backend/seed/data.js index 928350a..f146556 100644 --- a/backend/seed/data.js +++ b/backend/seed/data.js @@ -54,6 +54,16 @@ const tools = [ available: true, order: 5, }, + { + slug: 'clean-code', + name: 'Clean Code', + description: + 'Les huit chapitres-clés de Robert C. Martin — noms, fonctions, commentaires, erreurs, classes, tests — pour un code qu’on relit sans effort.', + icon: 'cleaning_services', + route: '/clean-code', + available: true, + order: 6, + }, ]; const checklist = [ diff --git a/frontend/src/app/app.component.ts b/frontend/src/app/app.component.ts index cbee776..ba6bca5 100644 --- a/frontend/src/app/app.component.ts +++ b/frontend/src/app/app.component.ts @@ -32,6 +32,7 @@ export class AppComponent { { label: 'Checklist Code Review', link: '/code-review' }, { label: 'Bonnes pratiques', link: '/bonnes-pratiques' }, { label: 'Principes SOLID', link: '/solid' }, + { label: 'Clean Code', link: '/clean-code' }, { label: 'Design Patterns', link: '/design-patterns' }, { label: 'Claude Code', link: '/claude-code-setup' }, ]; diff --git a/frontend/src/app/app.routes.ts b/frontend/src/app/app.routes.ts index cd8e4ea..aead52f 100644 --- a/frontend/src/app/app.routes.ts +++ b/frontend/src/app/app.routes.ts @@ -58,6 +58,20 @@ export const routes: Routes = [ (m) => m.DesignPatternDetailComponent ), }, + { + path: 'clean-code', + loadComponent: () => + import('./features/clean-code/clean-code.component').then( + (m) => m.CleanCodeComponent + ), + }, + { + path: 'clean-code/:chapitre', + loadComponent: () => + import('./features/clean-code-detail/clean-code-detail.component').then( + (m) => m.CleanCodeDetailComponent + ), + }, { path: 'claude-code-setup', loadComponent: () => diff --git a/frontend/src/app/core/data/clean-code-principles.ts b/frontend/src/app/core/data/clean-code-principles.ts new file mode 100644 index 0000000..2a7dff2 --- /dev/null +++ b/frontend/src/app/core/data/clean-code-principles.ts @@ -0,0 +1,361 @@ +import { CleanCodePrinciple } from '../models/clean-code-principle.model'; + +/** + * Source unique du contenu pédagogique « Clean Code » : huit chapitres-clés + * de l'ouvrage de Robert C. Martin, déclinés en principes pratiques, dans + * l'ordre du parcours. Chaque chapitre porte un `slug` stable qui sert à la + * fois de paramètre de route (page de détail `/clean-code/:chapitre`) et + * d'identifiant pour le parcours précédent/suivant. + * + * Consommé par la page d'aperçu (clean-code, hub) et par les pages de détail + * (clean-code-detail). Contenu pédagogique : définition, problème, pourquoi, + * exemples de code à éviter / à préférer, et comment respecter le principe. + */ +export const CLEAN_CODE_PRINCIPLES: CleanCodePrinciple[] = [ + { + numero: '1', + slug: 'meaningful-names', + icon: 'label', + nomEn: 'Meaningful Names', + nomFr: 'Noms significatifs', + definition: + 'Un nom doit révéler l’intention : pourquoi cette chose existe, ce qu’elle fait et comment on l’utilise.', + probleme: + 'Des noms vagues comme `d`, `tmp`, `data` ou `manager` obligent le lecteur à deviner. Le code se lit alors comme une énigme : il faut tout relire pour comprendre une seule ligne.', + pourquoi: [ + 'Lisibilité : un nom juste rend souvent le commentaire inutile.', + 'Maintenance : on retrouve et modifie le bon endroit sans relire toute la fonction.', + 'Communication : le code parle le langage du métier, pas celui de la machine.', + ], + exempleAvant: { + legende: 'Non conforme — des noms qui cachent l’intention', + code: `function getThem(list) { + const l = []; + for (const x of list) { + if (x[0] === 4) l.push(x); + } + return l; +}`, + }, + exempleApres: { + legende: 'Conforme — les noms disent le quoi et le pourquoi', + code: `const STATUS_FLAGGED = 4; + +function getFlaggedCells(board) { + return board.filter((cell) => cell.status === STATUS_FLAGGED); +}`, + }, + commentRespecter: [ + 'Choisis des noms prononçables et cherchables, quitte à les rallonger.', + 'Bannis les abréviations obscures et les noms fourre-tout (`data`, `info`, `manager`).', + 'Nomme dans le langage du domaine, pas dans celui de l’implémentation.', + 'Si un nom a besoin d’un commentaire pour s’expliquer, change le nom.', + ], + }, + { + numero: '2', + slug: 'functions', + icon: 'functions', + nomEn: 'Functions', + nomFr: 'Fonctions', + definition: + 'Une fonction doit être courte, faire une seule chose, et la faire à un seul niveau d’abstraction.', + probleme: + 'Une fonction longue qui mêle l’orchestration et les détails, multiplie les arguments et les effets de bord, devient impossible à lire d’un coup d’œil — et tout aussi pénible à tester.', + pourquoi: [ + 'Lisibilité : une petite fonction se lit comme un paragraphe.', + 'Testabilité : une responsabilité unique se teste sans échafaudage.', + 'Réutilisation : extraire une fonction nomme un concept et le rend réemployable.', + ], + exempleAvant: { + legende: 'Non conforme — la fonction fait tout, plusieurs niveaux mêlés', + code: `function sendReport(users) { + for (const u of users) { + if (u.active && u.email) { + const body = 'Bonjour ' + u.name + '...'; + smtp.connect(); + smtp.send(u.email, body); + smtp.close(); + } + } +}`, + }, + exempleApres: { + legende: 'Conforme — chaque fonction à un seul niveau, un seul rôle', + code: `function sendReport(users) { + users.filter(canReceiveReport).forEach(sendReportTo); +} + +function canReceiveReport(user) { + return user.active && Boolean(user.email); +} + +function sendReportTo(user) { + mailer.send(user.email, buildReportBody(user)); +}`, + }, + commentRespecter: [ + 'Vise des fonctions courtes ; si tu hésites à extraire, extrais.', + 'Une fonction = une seule chose = un seul niveau d’abstraction.', + 'Limite les arguments (de zéro à trois) ; regroupe-les en objet au-delà.', + 'Évite les effets de bord cachés et les drapeaux booléens en paramètre.', + ], + }, + { + numero: '3', + slug: 'comments', + icon: 'comment', + nomEn: 'Comments', + nomFr: 'Commentaires', + definition: + 'Le bon commentaire explique un « pourquoi » que le code ne peut pas dire ; le meilleur est celui qu’on a rendu inutile.', + probleme: + 'Un commentaire qui paraphrase le code finit par mentir en vieillissant, et compense souvent un code obscur au lieu de le clarifier. Le bruit cache alors le peu de signal qui comptait.', + pourquoi: [ + 'Vérité : un code clair ne ment pas, contrairement à un commentaire qui dérive.', + 'Intention : réserver le commentaire au « pourquoi » le rend précieux.', + 'Sobriété : moins de bruit, plus de signal.', + ], + exempleAvant: { + legende: 'Non conforme — le commentaire répète le code', + code: `// incrémente i de 1 +i = i + 1; + +// vérifie si l'utilisateur est majeur +if (user.age >= 18) { /* ... */ }`, + }, + exempleApres: { + legende: + 'Conforme — le code se passe de commentaire, le « pourquoi » reste', + code: `i = i + 1; + +if (isAdult(user)) { /* ... */ } + +// Le fournisseur limite à 50 req/s : on temporise pour éviter le 429. +await sleep(RATE_LIMIT_DELAY_MS);`, + }, + commentRespecter: [ + 'Avant de commenter, demande-toi si un meilleur nom rendrait le commentaire inutile.', + 'Réserve les commentaires au « pourquoi » : décision, contrainte, piège.', + 'Supprime le code commenté : l’historique git le garde pour toi.', + 'Méfie-toi des commentaires qui vieillissent — un commentaire faux est pire qu’aucun.', + ], + }, + { + numero: '4', + slug: 'formatting', + icon: 'format_align_left', + nomEn: 'Formatting', + nomFr: 'Mise en forme', + definition: + 'La mise en forme est une communication : un code aéré et cohérent se lit avant même d’être compris.', + probleme: + 'Une indentation erratique, des fichiers fourre-tout et des éléments liés éloignés exigent un effort de lecture constant — et chaque modification produit un diff bruyant.', + pourquoi: [ + 'Lisibilité : l’œil suit la structure quand l’espacement la révèle.', + 'Cohésion d’équipe : un style commun supprime les débats et les diffs cosmétiques.', + 'Proximité : ce qui se lit ensemble doit rester ensemble.', + ], + exempleAvant: { + legende: 'Non conforme — densité et incohérence brouillent la lecture', + code: `function price(items){let t=0;for(const i of items){t+=i.qty*i.price} +if(t>100){t=t*0.9} +return t}`, + }, + exempleApres: { + legende: 'Conforme — espacement régulier, intentions séparées', + code: `const BULK_THRESHOLD = 100; +const BULK_DISCOUNT = 0.9; + +function price(items) { + const total = items.reduce((sum, i) => sum + i.qty * i.price, 0); + + return total > BULK_THRESHOLD ? total * BULK_DISCOUNT : total; +}`, + }, + commentRespecter: [ + 'Confie le style à un formateur automatique (Prettier) plutôt qu’à la discipline.', + 'Garde proches les choses liées ; sépare par une ligne vide les idées distinctes.', + 'Vise des fichiers courts, lus de haut en bas comme un article.', + 'Adopte le style de l’équipe, même si ce n’est pas ton préféré.', + ], + }, + { + numero: '5', + slug: 'error-handling', + icon: 'error', + nomEn: 'Error Handling', + nomFr: 'Gestion des erreurs', + definition: + 'Gérer les erreurs ne doit pas noyer la logique : préfère les exceptions aux codes de retour, et ne renvoie jamais `null` en douce.', + probleme: + 'Des codes d’erreur vérifiés partout, un `null` qui se propage et des blocs `catch` vides finissent par noyer le chemin nominal sous la défense — et laissent passer des bugs en silence.', + pourquoi: [ + 'Lisibilité : séparer le chemin nominal du traitement d’erreur clarifie les deux.', + 'Robustesse : une exception non avalée remonte là où on peut la traiter.', + 'Sûreté : bannir `null` supprime une classe entière de bugs.', + ], + exempleAvant: { + legende: 'Non conforme — codes de retour et null mêlés à la logique', + code: `function totalDue(id) { + const user = findUser(id); + if (user === null) return -1; + const cart = user.cart; + if (cart === null) return -1; + return cart.total; +}`, + }, + exempleApres: { + legende: 'Conforme — exceptions et valeurs sûres, logique dégagée', + code: `function totalDue(id) { + return findUser(id).cart.total; // panier garanti non null +} + +function findUser(id) { + const user = repo.byId(id); + if (!user) throw new UserNotFoundError(id); + return user; +}`, + }, + commentRespecter: [ + 'Préfère les exceptions aux codes de retour qui polluent chaque appelant.', + 'Ne renvoie ni n’accepte `null` : renvoie un objet vide, une valeur optionnelle, ou lève.', + 'N’avale jamais une exception dans un `catch` vide ; au minimum, journalise et relance.', + 'Donne du contexte à tes erreurs : quoi, où, avec quelle donnée.', + ], + }, + { + numero: '6', + slug: 'boundaries', + icon: 'fence', + nomEn: 'Boundaries', + nomFr: 'Limites', + definition: + 'Aux frontières avec du code tiers, isole la dépendance derrière une interface qui t’appartient.', + probleme: + 'Appeler partout l’API d’une librairie externe soude ton code à un détail que tu ne contrôles pas : une montée de version casse tout, et tes tests dépendent du vrai service.', + pourquoi: [ + 'Découplage : le métier dépend de ton interface, pas de la librairie.', + 'Testabilité : tu remplaces la frontière par un double en test.', + 'Évolutivité : changer de fournisseur ne touche qu’un seul adaptateur.', + ], + exempleAvant: { + legende: 'Non conforme — l’API tierce fuit dans tout le métier', + code: `import Stripe from 'stripe'; +const stripe = new Stripe(KEY); + +async function checkout(order) { + // chaque appel dépend de la forme exacte de l'API Stripe + await stripe.charges.create({ amount: order.total, currency: 'eur' }); +}`, + }, + exempleApres: { + legende: 'Conforme — une frontière qui t’appartient enveloppe le tiers', + code: `interface PaymentGateway { + charge(amountCents: number): Promise; +} + +class StripeGateway implements PaymentGateway { + charge(amountCents: number) { + return this.stripe.charges.create({ amount: amountCents, currency: 'eur' }); + } +} + +async function checkout(order, gateway: PaymentGateway) { + await gateway.charge(order.total); +}`, + }, + commentRespecter: [ + 'Enveloppe chaque dépendance externe derrière une interface que tu définis.', + 'Convertis les types du tiers en tes propres types dès la frontière.', + 'Écris des « tests d’apprentissage » pour comprendre et verrouiller le comportement du tiers.', + 'Garde le code tiers loin du cœur : un seul endroit à changer s’il évolue.', + ], + }, + { + numero: '7', + slug: 'classes', + icon: 'category', + nomEn: 'Classes', + nomFr: 'Classes', + definition: + 'Une classe doit être petite, cohésive, et n’avoir qu’une seule raison de changer.', + probleme: + 'Une classe « dieu » accumule champs et méthodes sans lien : sa cohésion s’effondre, on ne sait plus ce qu’elle représente, et la moindre évolution la fait changer.', + pourquoi: [ + 'Lisibilité : une petite classe cohésive s’explique par son nom.', + 'Maintenance : une seule raison de changer limite l’onde de choc.', + 'Cohésion : des méthodes qui partagent les mêmes champs forment un vrai concept.', + ], + exempleAvant: { + legende: 'Non conforme — une classe fourre-tout, faiblement cohésive', + code: `class User { + name; email; passwordHash; + validateEmail() { /* ... */ } + hashPassword() { /* ... */ } + saveToDb() { /* ... */ } + renderProfileHtml() { /* ... */ } + sendNewsletter() { /* ... */ } +}`, + }, + exempleApres: { + legende: 'Conforme — des classes petites, chacune un seul rôle', + code: `class User { + constructor(public name: string, public email: Email) {} +} +class UserRepository { save(user: User) { /* ... */ } } +class ProfileView { render(user: User): string { /* ... */ } } +class Newsletter { sendTo(user: User) { /* ... */ } }`, + }, + commentRespecter: [ + 'Garde tes classes petites ; mesure-les en responsabilités, pas en lignes.', + 'Si un sous-groupe de méthodes n’utilise qu’un sous-groupe de champs, extrais une classe.', + 'Une classe = une seule raison de changer (le « S » de SOLID).', + 'Sépare ce qui varie pour des raisons différentes : métier, persistance, présentation.', + ], + }, + { + numero: '8', + slug: 'unit-tests', + icon: 'science', + nomEn: 'Unit Tests', + nomFr: 'Tests unitaires', + definition: + 'Un test doit être aussi propre que le code de production, et suivre les règles F.I.R.S.T.', + probleme: + 'Des tests fragiles, lents, dépendants les uns des autres et qui vérifient dix choses à la fois finissent par perdre la confiance de l’équipe — qui cesse alors de les maintenir.', + pourquoi: [ + 'Confiance : une suite propre autorise le changement sans peur.', + 'Documentation : un test lisible montre comment utiliser le code.', + 'Conception : du code testable est du code faiblement couplé.', + ], + exempleAvant: { + legende: 'Non conforme — un test qui vérifie tout, illisible', + code: `test('user', () => { + const u = create(); + u.name = 'A'; save(u); const r = load(u.id); + expect(r.name).toBe('A'); expect(r.active).toBe(true); + expect(db.count()).toBe(1); expect(mailer.sent).toBe(1); +});`, + }, + exempleApres: { + legende: 'Conforme — un concept par test, arrangé-agi-attendu', + code: `test('recharge un utilisateur enregistré', () => { + const user = aUser({ name: 'Ada' }); // Arrange + repo.save(user); // Act + expect(repo.byId(user.id).name).toBe('Ada'); // Assert +}); + +test('créer un utilisateur déclenche un mail de bienvenue', () => { + service.create(aUser()); + expect(mailer.sentTo).toHaveLength(1); +});`, + }, + commentRespecter: [ + 'Traite tes tests comme du code de production : lisibles, nommés, refactorés.', + 'Un seul concept vérifié par test ; structure en Arrange-Act-Assert.', + 'Suis F.I.R.S.T. : Fast, Independent, Repeatable, Self-validating, Timely.', + 'Rends chaque test indépendant : aucun ordre, aucun état partagé.', + ], + }, +]; diff --git a/frontend/src/app/core/models/clean-code-principle.model.ts b/frontend/src/app/core/models/clean-code-principle.model.ts new file mode 100644 index 0000000..7ad11e0 --- /dev/null +++ b/frontend/src/app/core/models/clean-code-principle.model.ts @@ -0,0 +1,32 @@ +/** Un bloc de code illustratif (affiché tel quel, jamais exécuté). */ +export interface CleanCodeExample { + /** Légende du bloc (ex. « Version qui cache l'intention »). */ + legende: string; + /** Le code — TypeScript / pseudo-code illustratif. */ + code: string; +} + +/** Un chapitre de Clean Code (Robert C. Martin) décliné en principe pratique. */ +export interface CleanCodePrinciple { + /** Numéro de chapitre (ordre du parcours, ex. « 1 »). */ + numero: string; + /** Slug court pour la route de détail et l'ancre (ex. `meaningful-names`). */ + slug: string; + icon: string; + /** Titre en anglais (ex. « Meaningful Names »). */ + nomEn: string; + /** Titre en français (ex. « Noms significatifs »). */ + nomFr: string; + /** Définition courte (carte d'aperçu + en-tête de la page de détail). */ + definition: string; + /** Le problème que le principe résout (carte d'aperçu). */ + probleme: string; + /** Pourquoi ce principe compte — points développés (page de détail). */ + pourquoi: string[]; + /** Exemple de code à éviter (ignore le principe). */ + exempleAvant: CleanCodeExample; + /** Exemple de code à préférer (respecte le principe). */ + exempleApres: CleanCodeExample; + /** Techniques / bonnes pratiques pour respecter le principe. */ + commentRespecter: string[]; +} diff --git a/frontend/src/app/features/clean-code-detail/clean-code-detail.component.html b/frontend/src/app/features/clean-code-detail/clean-code-detail.component.html new file mode 100644 index 0000000..b61389b --- /dev/null +++ b/frontend/src/app/features/clean-code-detail/clean-code-detail.component.html @@ -0,0 +1,90 @@ +@if (principle(); as p) { + + +
+ +
+ +

{{ p.nomFr }}

+

{{ p.nomEn }}

+
+
+ +

{{ p.definition }}

+ + +
+

+ + Le problème résolu +

+

{{ p.probleme }}

+
+ + +
+

+ + Pourquoi l'appliquer +

+
    + @for (point of p.pourquoi; track point) { +
  • {{ point }}
  • + } +
+
+ + +
+

+ + En pratique +

+
+ + +

+ + À éviter +

+

{{ p.exempleAvant.legende }}

+
{{ p.exempleAvant.code }}
+
+
+ + + +

+ + À préférer +

+

{{ p.exempleApres.legende }}

+
{{ p.exempleApres.code }}
+
+
+
+
+ + +
+

+ + Comment le respecter +

+
    + @for (conseil of p.commentRespecter; track conseil) { +
  • {{ conseil }}
  • + } +
+
+ + + +} diff --git a/frontend/src/app/features/clean-code-detail/clean-code-detail.component.scss b/frontend/src/app/features/clean-code-detail/clean-code-detail.component.scss new file mode 100644 index 0000000..19dec8a --- /dev/null +++ b/frontend/src/app/features/clean-code-detail/clean-code-detail.component.scss @@ -0,0 +1,121 @@ +@use '../../shared/styles/detail' as *; + +:host { + display: block; + max-width: 960px; + margin: 0 auto; + padding: var(--cc-space-5) var(--cc-space-4) var(--cc-space-8); + // Entrée de page : apparition fluide à chaque navigation. + animation: cc-rise var(--cc-dur) var(--cc-ease-smooth) both; +} + +.ccd-header { + display: flex; + align-items: center; + gap: var(--cc-space-4); + margin-bottom: var(--cc-space-5); + + &__numero { + display: inline-flex; + align-items: center; + justify-content: center; + width: 3rem; + height: 3rem; + flex-shrink: 0; + border-radius: var(--cc-radius-md); + background: var(--cc-gradient-festive); + color: var(--cc-on-primary); + font-size: 1.6rem; + font-weight: 700; + box-shadow: var(--cc-shadow-glow); + } + + &__title { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: var(--cc-space-2) var(--cc-space-3); + + h1 { + margin: 0; + color: var(--cc-ink); + font-size: clamp(1.5rem, 3.5vw, 2rem); + font-weight: 700; + line-height: var(--cc-lh-tight); + } + } + + &__icon { + color: var(--cc-accent); + } + + &__en { + width: 100%; + margin: 0; + color: var(--cc-ink-soft); + font-size: var(--cc-fs-small); + font-style: italic; + } +} + +.ccd-definition { + margin: 0 0 var(--cc-space-6); + max-width: var(--cc-measure); + color: var(--cc-ink); + font-size: 1.05rem; + line-height: var(--cc-lh-body); +} + +.ccd-block { + @include detail-block; + + > p { + margin: 0; + max-width: var(--cc-measure); + color: var(--cc-ink-soft); + line-height: var(--cc-lh-body); + } +} + +.ccd-list { + @include detail-list; +} + +/* Exemples « à éviter / à préférer » avec blocs de code */ +.examples { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); + gap: var(--cc-space-4); +} + +.example { + @include detail-example; + + &__title { + display: flex; + align-items: center; + gap: var(--cc-space-2); + margin: 0 0 var(--cc-space-2); + font-size: var(--cc-fs-h3); + font-weight: 600; + } + + // Indices doublés (icône + couleur de bord), jamais la couleur seule. + &--avoid { + border-left: 4px solid var(--cc-warn); + + .example__title { + color: var(--cc-warn); + } + } + + &--prefer { + border-left: 4px solid var(--cc-success); + + .example__title { + color: var(--cc-success); + } + } +} + +@include detail-code; diff --git a/frontend/src/app/features/clean-code-detail/clean-code-detail.component.ts b/frontend/src/app/features/clean-code-detail/clean-code-detail.component.ts new file mode 100644 index 0000000..08bf885 --- /dev/null +++ b/frontend/src/app/features/clean-code-detail/clean-code-detail.component.ts @@ -0,0 +1,88 @@ +import { Component, computed, inject, signal } from '@angular/core'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; +import { ActivatedRoute, Router } from '@angular/router'; +import { MatCardModule } from '@angular/material/card'; +import { MatIconModule } from '@angular/material/icon'; + +import { CLEAN_CODE_PRINCIPLES } from '../../core/data/clean-code-principles'; +import { CleanCodePrinciple } from '../../core/models/clean-code-principle.model'; +import { + BreadcrumbComponent, + BreadcrumbItem, +} from '../../shared/components/breadcrumb/breadcrumb.component'; +import { + SequentialNavComponent, + SequentialNavItem, +} from '../../shared/components/sequential-nav/sequential-nav.component'; +import { neighborSlug } from '../../core/utils/sequential-nav'; + +/** Ordre du parcours — dérivé de l'unique source CLEAN_CODE_PRINCIPLES. */ +const PRINCIPLE_SLUGS = CLEAN_CODE_PRINCIPLES.map((p) => p.slug); + +/** + * Page de détail d'un chapitre Clean Code : définition, pourquoi, exemples de + * code à éviter / à préférer, et comment le respecter. + * + * Le chapitre est résolu depuis le paramètre de route `:chapitre`. Un slug + * inconnu redirige vers le hub `/clean-code`. Le suivi réactif du paramètre + * couvre la navigation précédent/suivant entre routes sœurs (Angular réutilise + * alors le composant) — même pattern que la page de détail SOLID. + */ +@Component({ + selector: 'app-clean-code-detail', + imports: [ + BreadcrumbComponent, + SequentialNavComponent, + MatCardModule, + MatIconModule, + ], + templateUrl: './clean-code-detail.component.html', + styleUrl: './clean-code-detail.component.scss', +}) +export class CleanCodeDetailComponent { + private route = inject(ActivatedRoute); + private router = inject(Router); + + /** Slug courant, suivi de façon réactive (cf. constructeur). */ + private readonly slug = signal(''); + + /** Chapitre courant (undefined si slug inconnu → redirection). */ + readonly principle = signal(undefined); + + /** Fil d'Ariane : Accueil › Clean Code › {nom du chapitre}. */ + readonly breadcrumb = computed(() => [ + { label: 'Accueil', link: '/' }, + { label: 'Clean Code', link: '/clean-code' }, + { label: this.principle()?.nomFr ?? '' }, + ]); + + /** Chapitre précédent / suivant du parcours (undefined aux extrémités). */ + readonly prev = computed(() => + this.neighbor(-1, 'précédent') + ); + readonly next = computed(() => + this.neighbor(1, 'suivant') + ); + + constructor() { + this.route.paramMap.pipe(takeUntilDestroyed()).subscribe((params) => { + const slug = params.get('chapitre') ?? ''; + const principle = CLEAN_CODE_PRINCIPLES.find((p) => p.slug === slug); + if (!principle) { + this.router.navigate(['/clean-code']); + return; + } + this.slug.set(slug); + this.principle.set(principle); + }); + } + + /** Chapitre voisin dans PRINCIPLE_SLUGS (delta -1 = précédent, +1 = suivant). */ + private neighbor(delta: number, sens: string): SequentialNavItem | undefined { + const slug = neighborSlug(PRINCIPLE_SLUGS, this.slug(), delta); + if (!slug) return undefined; + const nomFr = + CLEAN_CODE_PRINCIPLES.find((p) => p.slug === slug)?.nomFr ?? ''; + return { slug, label: nomFr, ariaLabel: `Chapitre ${sens} : ${nomFr}` }; + } +} diff --git a/frontend/src/app/features/clean-code/clean-code.component.html b/frontend/src/app/features/clean-code/clean-code.component.html new file mode 100644 index 0000000..5fc6fde --- /dev/null +++ b/frontend/src/app/features/clean-code/clean-code.component.html @@ -0,0 +1,55 @@ + + +
+

Clean Code

+

+ Huit chapitres-clés de l'ouvrage de Robert C. Martin — du nom de variable au + test unitaire — pour écrire un code qu'on relit sans effort et qu'on fait + évoluer sans peur. +

+
+ + +
+ +

+ Le code propre n'est pas un luxe : c'est ce qui rend le changement bon + marché. +

+

+ On lit le code bien plus souvent qu'on ne l'écrit. Ces principes, + popularisés par Robert C. Martin, guident chaque décision du quotidien — + nommer, découper, commenter, gérer l'erreur. Choisissez un chapitre + ci-dessous pour le découvrir en détail : définition, problème résolu, + exemples de code à éviter puis à préférer, et comment l'appliquer. +

+
+ + + diff --git a/frontend/src/app/features/clean-code/clean-code.component.scss b/frontend/src/app/features/clean-code/clean-code.component.scss new file mode 100644 index 0000000..f01fdf3 --- /dev/null +++ b/frontend/src/app/features/clean-code/clean-code.component.scss @@ -0,0 +1,160 @@ +:host { + display: block; + max-width: 960px; + margin: 0 auto; + padding: var(--cc-space-5) var(--cc-space-4) var(--cc-space-8); + // Entrée de page : apparition fluide à chaque navigation. + animation: cc-rise var(--cc-dur) var(--cc-ease-smooth) both; +} + +.clean-code-header { + margin-bottom: var(--cc-space-5); + + h1 { + margin: 0; + color: var(--cc-ink); + font-size: clamp(1.6rem, 4vw, 2.25rem); + font-weight: 700; + line-height: var(--cc-lh-tight); + } + + .subtitle { + margin: var(--cc-space-2) 0 0; + max-width: var(--cc-measure); + color: var(--cc-ink-soft); + line-height: var(--cc-lh-body); + } +} + +/* Hero — donne le ton festif (framboise → violet) */ +.hero { + position: relative; + z-index: 0; // contexte d'empilement pour les confettis décoratifs + overflow: hidden; + background: var(--cc-gradient-festive); + color: var(--cc-on-primary); + border-radius: var(--cc-radius-lg); + padding: var(--cc-space-6); + margin-bottom: var(--cc-space-6); + box-shadow: var(--cc-shadow-2); + + &__lead { + margin: 0 0 var(--cc-space-3); + font-size: clamp(1.15rem, 2.5vw, 1.5rem); + font-weight: 600; + line-height: var(--cc-lh-tight); + } + + &__body { + margin: 0; + max-width: var(--cc-measure); + color: rgba(255, 255, 255, 0.92); + line-height: var(--cc-lh-body); + } +} + +/* Grille de cartes-chapitres (le hub) */ +.chapters { + list-style: none; + margin: 0; + padding: 0; + display: grid; + grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); + gap: var(--cc-space-4); + // Apparition en cascade : utilitaire `.cc-stagger` (styles.scss), posé sur + // ce conteneur dans le template (cible les `
  • `, enfants directs). +} + +.chapter-card { + display: block; + height: 100%; + text-decoration: none; + color: inherit; + border-radius: var(--cc-radius-md); + + &__inner { + height: 100%; + border-radius: var(--cc-radius-md); + transition: + transform var(--cc-transition-bounce), + box-shadow var(--cc-transition), + border-color var(--cc-transition); + } + + &:hover &__inner, + &:focus-visible &__inner { + transform: translateY(-6px) scale(1.015); + box-shadow: var(--cc-shadow-2); + border-color: var(--cc-primary); + } + + // Le numéro du chapitre « pétille » au survol. + &:hover &__numero, + &:focus-visible &__numero { + transform: rotate(-6deg) scale(1.08); + } + + &__top { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: var(--cc-space-3); + } + + &__numero { + display: inline-flex; + align-items: center; + justify-content: center; + width: 2.75rem; + height: 2.75rem; + border-radius: var(--cc-radius-md); + background: var(--cc-gradient-festive); + color: var(--cc-on-primary); + font-size: 1.5rem; + font-weight: 700; + box-shadow: var(--cc-shadow-glow); + transition: transform var(--cc-transition-bounce); + } + + &__icon { + color: var(--cc-accent); + } + + &__title { + margin: 0; + font-size: var(--cc-fs-h3); + font-weight: 600; + color: var(--cc-ink); + line-height: var(--cc-lh-tight); + } + + &__en { + margin: var(--cc-space-1) 0 0; + color: var(--cc-ink-soft); + font-size: var(--cc-fs-small); + font-style: italic; + } + + &__def { + margin: var(--cc-space-3) 0 0; + color: var(--cc-ink-soft); + font-size: 0.9rem; + line-height: 1.5; + } + + &__cta { + display: inline-flex; + align-items: center; + gap: var(--cc-space-1); + margin-top: var(--cc-space-4); + color: var(--cc-primary); + font-size: var(--cc-fs-small); + font-weight: 600; + + mat-icon { + font-size: 1.1rem; + width: 1.1rem; + height: 1.1rem; + } + } +} diff --git a/frontend/src/app/features/clean-code/clean-code.component.ts b/frontend/src/app/features/clean-code/clean-code.component.ts new file mode 100644 index 0000000..96df1d3 --- /dev/null +++ b/frontend/src/app/features/clean-code/clean-code.component.ts @@ -0,0 +1,42 @@ +import { Component } from '@angular/core'; +import { RouterLink } from '@angular/router'; +import { MatCardModule } from '@angular/material/card'; +import { MatIconModule } from '@angular/material/icon'; + +import { CLEAN_CODE_PRINCIPLES } from '../../core/data/clean-code-principles'; +import { CleanCodePrinciple } from '../../core/models/clean-code-principle.model'; +import { + BreadcrumbComponent, + BreadcrumbItem, +} from '../../shared/components/breadcrumb/breadcrumb.component'; +import { ConfettiComponent } from '../../shared/components/confetti/confetti.component'; + +/** + * Page pédagogique « Clean Code ». + * + * Hub des huit chapitres-clés : une carte par chapitre, cliquable vers sa + * page de détail (`/clean-code/:chapitre`). Le contenu provient de la source + * unique CLEAN_CODE_PRINCIPLES. + */ +@Component({ + selector: 'app-clean-code', + imports: [ + RouterLink, + BreadcrumbComponent, + ConfettiComponent, + MatCardModule, + MatIconModule, + ], + templateUrl: './clean-code.component.html', + styleUrl: './clean-code.component.scss', +}) +export class CleanCodeComponent { + /** Fil d'Ariane : Accueil › Clean Code (page courante). */ + readonly breadcrumb: BreadcrumbItem[] = [ + { label: 'Accueil', link: '/' }, + { label: 'Clean Code' }, + ]; + + /** Les huit chapitres, dans l'ordre du parcours. */ + readonly principles: CleanCodePrinciple[] = CLEAN_CODE_PRINCIPLES; +}