Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Pecunia

C’est la documentation de l’application Pecunia pour la promo 2024/2025 pour le titre RNCP de niveau 6 (Bac+3/+4) Concepteur Développeur d’Applications de la WILD CODE SCHOOL // SIMPLON.

Ce wiki est la ressources principale du groupe, sur tout ce qui concerne la documentation technique ou la documentation théorique demander pour le titre.

C’est quoi Pecunia ?

Pecunia est une application de gestion de budget conçue pour permettre aux utilisateurs de suivre et de gérer leurs finances personnelles de manière simple et flexible.

En permettant une personnalisation avancée des catégories de dépenses. Elle vise à offrir une approche sur mesure adaptée à des besoins spécifiques. Contrairement aux applications bancaires traditionnelles ou aux applications de budget qui vous obligent à lier vos comptes bancaires, Pecunia permet aux utilisateurs de TOUT gérer en entrant manuellement toutes leurs transactions.

Cette approche permet une personnalisation complète et un meilleur contrôle de ses finances personnelles.

Installation

Dans cette section, nous allons voir comment installer les outils nécessaires que ce soit pour le front ou back-end. Pour l’ensemble, nous allons utilisé pre-commit et commitizen pour formater correctement nos commits avant de les pousser sur nos branches.

Par ailleurs pour le front, la dépendance husky sera là pour nous faciliter la mise en place de pre-commit. On va surtout se focaliser sur les outils pour l’écosystème Java que Javascript ici.

Nous allons avoir besoin pour

le front :

le back :

Dans les prochaines sous-sections, vous allez pouvoir suivre l’installation de ses outils pour IDEA de Jetbrains.

Pre-commit // Commitizen

Ce sont deux outils permettant pour l’un de créer des git hooks que ce soit au moment du push ou au moment d’écrire un message de commit.

Pour l’instant, nous avons fait le choix d’utiliser qu’un git hook pour les messages de commit.

Installation: Pre-Commit

Etant donné que c’est un package Python, je conseille de passer par l’outil pipx, qui fonctionne comme les outils de versionning pour Node (volta, nvs, etc), ca facilitera les installations pour la suite.

  1. pipx install pre-commit dans un terminal.
  2. pre-commit install dans le dossier de votre projet (ici on se positionne dans pecunia-api/ afin que cela installe les git hooks correspondants étant donné que nous ne commitions pas le dossier .git

C’est tout pour Pre-Commit, le fichier de configuration .pre-commit-config.yaml est normalement déjà présent.

Installation: Commitizen

Normalement vous avez pipx sur votre poste de travail.

1.pipx ensurepath

2. pipx install commitizen

3. pipx upgrade commitizen

Si vous avez un retour en faisant cz -h c’est que vous avez bien installé Commitizen. Bravo !

Comment combiner les deux ?

Quand vous effectué un changement dans les fichers et que vous voulez commit votre travail :

Vous n’avez qu’à ajouter comme d’habitude

git add . ou git add <des fichiers>

puis afin de respecter la nomenclature vous faites

cz : cela va vous ouvrir une boite de dialogue dans votre terminal, vous avez juste à choisir ce que vous voulez faire comme dans cette petite vidéo:

Using commitizen cli

et si vous voulez tout de meme passer par un git commit classique. Il faudra respecter la nomenclature parfaitement sinon…

Conclusion

Avec ses deux outils, le project sera cohérent au niveau de ses commits et de ses attentions derrière.

De plus Commitizen permet de gérer la version de nos applications avec cz bump en utilisant semantic versionning et permettant aussi de créer des changelogs

source: https://pre-commit.com/#install

source: https://commitizen-tools.github.io/commitizen/

source: https://semver.org/

Google Java Format

Afin de nous aider à satisfaire notre outil d’aide au développement qu’est CheckStyle, on va avoir besoin d’un formatteur qui va automatiser les conventions de code simple à mettre en place dans notre projet.

Avant de commencer le tutorial, je vous invite à lire le README de Google Java Format.

Installation IDE

INTELIJ IDEA

Pour installer le formater, on va devoir installer un plugin :

Settings -> Plugins -> On clique sur marketplace -> On cherche “google-java-format”

On devrait retrouver ce plugin

Ensuite vous avez juste à suivre cette partie dans le README du projet :

config_formater

[!WARNING] Avant de passer à la suite je vous invite fortement à prendre cette config ci-dessous que vous retrouverez dans IDEA : Settings -> Tools -> Actions On Save

code_on_save

Checkstyle

Checkstyle est un outil de développement qui permet d’avoir des standards de code en Java avec comme deux configurations par défaut:

  • Sun Code Conventions qui n’a plus été mis à jour depuis 1999
  • Google Java Styledernière mis à jour en 2022 et qui est assez strict et ajout des standards d’indentation

C’est ce dernier que nous avons choisis pour notre projet.

Installation Projet

Checkstyle est présent dans notre projet grace à un plugin mis dans le pom.xml

      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-checkstyle-plugin</artifactId>
        <version>3.6.0</version>
        <configuration>
          <configLocation>google_checks.xml</configLocation>
          <consoleOutput>true</consoleOutput>
          <failsOnError>true</failsOnError>
          <linkXRef>false</linkXRef>
        </configuration>
        <executions>
          <execution>
            <id>validate</id>
            <phase>validate</phase>
            <goals>
              <goal>check</goal>
            </goals>
          </execution>
        </executions>
      </plugin>

Avec ce plugin maven, on pourra utiliser les commandes suivantes :

  • mvn checkstyle:check
    Cette comande anaylyse et va compter les violations directement dans la console. Cela peut faire échouer la compilation (ce qu’on veut ici)
  • mvn checkstyle:checkstyle
  • la meme chose qu’au dessus sauf qu’ici ca va générer un reporting en forme de page html avec toutes les violations du projet.On doit lancer à la main la page HTML qui se trouve dans target/site/checkstyle.html

Pour avoir ce dossier site/ , il faut éxécuter la commande dans le terminal :

mvn site

Extensions IDE

INTELIJ IDEA

Tout d’abord, on télécharge le plugin directement dans son IDE

Settings -> Plugins -> On clique sur marketplace -> On cherche “CheckStyle-IDEA” On devrait retrouver ce plugin

Ensuite on va aller dans : Settings -> Tools -> Checkstyle et on va cocher comme ci-dessous

checkstyle_config

Organiser correctement les imports

Ensuite, on va modifier comment IDEA organise ses imports car on veut pas d’import en étoiles car ca peut porter à confusion avec d’autres packages quand une méthode à le même nom :

Encore une fois on va aller dans :

Settings -> Editor -> Code Style -> Java Puis vous allez prendre cette configuration ci-dessous :

part1

[!WARNING] Il ne doit rien avoir dans la section “Packages to Use Import with ‘*’

part2 part3

Désactiver l’auto indentation de IDEA

Settings -> General -> Smart Keys Puis on utilise cette configuration :

indent

Vous pouvez aussi utiliser ce tutorial si besoin :

https://checkstyle.org/idea.html

VS CODE

  1. Installer l’extension Checkstyle For Java
  2. Ctrl+alt+P => Checkstyle: Set the Checkstyle Configuration Style
  3. Choisir /google_checks.xml
  4. Normalement c’est bon, vous êtes en accord avec le projet

source : https://github.com/redhat-developer/vscode-java/wiki/Formatter-settings source: https://checkstyle.org/

Javadoc

Comme en Javascript avec la JSDoc, il extiste en Java, le Javadoc qui permet de documenter notre codebase et générer une page HTML avec toutes nos classes et méthodes.

En lien avec le checkstyle de Google, on devra document nos méthodes et classes afin de rendre claire notre code et qu’il puisse être compréhensible par d’autres développeur.euses.

Installation

Un plugin est disponible avec maven :

<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-javadoc-plugin</artifactId>
<version>3.6.2</version>
<configuration>
<source>1.8</source>
<target>1.8</target>
</configuration>
</plugin>

Avec ce plugin on a notre disposition 16 commandes mais on va en utliser que quelques une normalement.

  • mvn javadoc:javadoc va générer la documentation dans le dossier target/site encore
  • mvn javadoc:fix va formatter correctement les commentaires javadoc de notre projet (Attention ca affecte l’ENSEMBLE du projet)

A modifier dans le futur afin de lancer pendant la CI et éviter des duplications d’éxécution pour la phase de generate-ressources

source : https://maven.apache.org/plugins/maven-javadoc-plugin/

🔄 Workflow

1. Main Branch

  • main: Code de production
  • dev: Branche d'intégration pour les nouvelles features

2. Feature workflow

  • Créez une branche : git checkout -b feat/your-feature depuis la branche main
  • Poussez votre travail et ouvrez une pull request pour la branche dev
  • Merge Rebase, si vous pouvez, après que 2 personnes ait validé votre travail

Branching strategy schema

Branching Strategy

Commit Messages Conventions

Pourquoi utiliser une format de commit structuré ?

Un format de commit clair et cohérent améliorer la lisibilité, le suivi de l’historique et l’automatisation (par exemple, les changelogs, les notes de version, etc)

Conventionnal commit format

<type>(<scope>): <message>

🔷 Type de commits
TypeUsages
featUne nouvelle fonctionnalité. Corrélation avec MINOR dans SemVer
fixCorrection d’un bug. Corrélation avec PATCH dans SemVer
docsChangements dans la documentation uniquement
testAjouter des tests manquants ou corriger des tests existants
buildChangements qui affectent le système de construction ou les dépendances externes (exemple : pip, docker, npm)
refactorUne modification du code qui ne corrige pas un bogue et n’ajoute pas de fonctionnalité.
styleChangements qui n’affectent pas la signification du code (espaces blancs, formatage, points-virgules manquants, etc.)
perfUne modification du code qui améliore les performances
ciModifications relatifs à la CI/CD

📌 Exemples

feat(auth): add JWT authentication middlewares
fix(ui): resolve navbar rendering issue
build(angular): update angular17 to angular19
docs(readme): update installation guide

Git Branch Strategy

📌 Conventions de naming des branches

PrefixUsage
feat/Développement de nouvelles fonctionnalités
fix/Correction de bugs
hotfix/Corrections de productions critiques
docs/Mise à jour de la documentation
test/Travaux liés aux tests
refactor/Code refactoring
chore/Maintenance et les tools
poc/Preuve de concept pour les essais de faisabilité

🔷 Examples

feat/<num-ticket-taiga>user-authentication
fix/<num-ticket-taiga>navbar-alignment
docs/<num-ticket-taiga>project-guidelines
chore/<num-ticket-taiga>update-dependencies

Informations et Aides

Aide pour le titre et infos complémentaires :


Ce sont des ressources pour la Méthode Merise et des informations sur la conception de BDD en général :


Ce sont des ressources pour la Méthode Merise et des informations sur la conception de BDD en général :

Documents obligatoires

Résumé Cahier des Charges

Daniel Cahier des charges projet

Thomas Cahier des Charges

Objectif du projet

L’objectif principal du projet est de développer une application dédiée à la gestion de budget, axée sur les concepts de simplicité et de contrôle financier.

La vision du projet vise à créer un outil pour les utilisateurs souhaitant mieux gérer leurs finances personnelles ou familiales.

L’application doit se distinguer par sa facilité d’utilisation, offrant une expérience fluide aussi bien lors de la planification budgétaire que lors du suivi des dépenses quotidiennes.

L’application permettra à un utilisateur connecté de gérer un portefeuille et les transactions associées.

Une attention particulière est portée à l’intuitivité de l’application, garantissant une saisie aisée des transactions et des catégories budgétaires par les utilisateurs.

Les choix de couleurs s’aligneront sur une charte graphique propre à l’univers de la finance, tout en étant pensés pour être accessibles aux personnes daltoniennes. De même, les polices d’écriture seront choisies pour leur simplicité et leur accessibilité aux personnes atteintes de dyslexie.

L’approche méthodologique adoptée est celle de l’organisation SCRUM, avec des sprints hebdomadaires intégrant le principe d’intégration continue. Cette méthodologie agile permet une gestion efficace du développement, favorisant la flexibilité et la collaboration au sein de l’équipe de travail.

Fonctionnalités clés

La fonctionnalité centrale du projet, le Minimum Viable Product (MVP), consiste en la mise en œuvre du CRUD (Create, Read, Update, Delete) pour les transactions.

L’accès à ces fonctionnalités sera restreint aux utilisateurs connectés et inclura les actions suivantes :

  • Création d’une transaction
  • Consultation d’une transaction
  • Modification d’une transaction
  • Suppression d’une transaction

De plus, l’application inclura les fonctionnalités suivantes :

  • CRUD des catégories avec gestion des icônes
  • CRUD sur les fournisseurs
  • Modification du profil utilisateur

L’utilisation de l’application en tant qu’utilisateur connecté nécessitera les actions supplémentaires suivantes :

  • Formulaire d’inscription
  • Formulaire de connexion

Technologies et architecture

Développement de l’application avec le langage de programmation Java via le framework Spring, ce qui induit une architecture Modèle Vue Contrôleur.

Développement Backend :

  • Langage de Programmation : Java
  • Framework : Spring (Modèle Vue Contrôleur)
  • Système de Gestion de Base de Données : MySQL
  • ORM : Hibernate (Gestion de la base de données avec Spring)
  • Correcteur de code : Checkstyle

Développement Frontend :

  • Framework : Angular
  • Bibliothèque CSS : Bootstrap
  • Préprocesseur CSS : SASS (pour la cohérence avec la maquette visuelle)
  • Langage de Template : HTML5, CSS3

Sécurité

La sécurité est une priorité dans le développement de cette application de gestion de budget.

Nous utilisons des DTO (Data Transfer Objects) pour sécuriser les échanges de données entre les couches de l’application.

Les communications entre le client et le serveur sont protégées par des protocoles de chiffrement, et les informations sensibles sont stockées de manière sécurisée dans la base de données.

Des pratiques de développement sécurisé, telles que la validation des entrées utilisateur et la prévention des injections SQL, sont rigoureusement appliquées.

L’authentification des utilisateurs est renforcée par l’utilisation de JWT (JSON Web Tokens) pour les sessions utilisateur, assurant ainsi une gestion sécurisée des accès.

🗺️ Sitemap

Dans un souci d’expérience utilisateur fluide, nous avons conçu un parcours clair et structuré à l’aide de l’outil Gloomaps.

sitemap

Product Backlog

Product Backlog

Conception de BDD + UML :


MLD + MODÈLE PHYSIQUE DE DONNÉES + Diagramme de classe + Diagramme séquence :

Drawio : https://app.diagrams.net/?src=about#G1yuIcwr5naRDbpJvKWywAGntifsswMZHg#%7B%22pageId%22%3A%22-F2r_zDhWsfIRe05eEPR%22%7D

Diagramme de cas d’usage :

LucidChart : https://lucid.app/lucidchart/c0417efa-c71c-479d-9d3f-c88048240679/edit?viewport_loc=-266%2C901%2C416%2C205%2C.Q4MUjXso07N&invitationId=inv_b4b420aa-d55a-4500-b1de-c1563b326c42

UML - Diagramme cas utilisation


Dictionnaire de données :

Google sheets : https://docs.google.com/spreadsheets/d/1p_G5ZhDzxSyXcsKZC1ZgbwwwVrPcOlQ_fioW7h5U2kQ/edit?gid=0#gid=0


MODÈLE CONCEPTUEL DE DONNÉES :

Début de MCD : lien

// MOOCODO MCD

UTILISATEUR: email_utilisateur, mot de passe, nom, prénom, photo de profil
APPARTENIR, 01 UTILISATEUR, 11 PORTEFEUILLE
PORTEFEUILLE: nom_portefeuille, devise, solde initia
COMPOSER, 0N PORTEFEUILLE, 11 TRANSACTION
FOURNISSEUR: nom

CREER, 1N UTILISATEUR, 1N CATEGORIE
CATEGORIE:nom_categorie, icone, couleur, type, est_global
REUTILISER, 1N PORTEFEUILLE, 0N CATEGORIE
TRANSACTION: montant, type, note, date de la transaction
ASSOCIER, 1N FOURNISSEUR, 01 TRANSACTION

:
RATTACHER, 11 CATEGORIE, 0N ICONE
COMPORTER, 0N CATEGORIE, 11 TRANSACTION
CONTENIR, 0N ETIQUETTE, 01 TRANSACTION
:

:
ICONE:nom_icone, url
:
ETIQUETTE: nom_etiquette
:

:
:
COMPOSER, 0N PORTEFEUILLE, 11 TRANSACTION
:
:
PORTEFEUILLE: nom_portefeuille, devise, solde initial
:
:
ASSOCIER, 0N FOURNISSEUR, 01 TRANSACTION
TRANSACTION: montant, type, note, date de la transaction
CONTENIR, 0N TAG, 0N TRANSACTION

APPARTENIR, 01 UTILISATEUR, 11 PORTEFEUILLE
UTILISATEUR: email_utilisateur, mot de passe, nom, prénom, photo de profil
CREER, 11 FOURNISSEUR, 0N UTILISATEUR
FOURNISSEUR: nom
:
TAG: nom_tag

:
MODIFIER, 0N UTILISATEUR, 11 CATEGORIE
CATEGORIE:nom_categorie, icone, couleur, type, est_global
COMPORTER, 0N CATEGORIE, 11 TRANSACTION
:
:

:
:
RATTACHER, 11 CATEGORIE, 0N ICONE
ICONE:nom_icone, url
:
:

:
:
CREATE, 11 PROVIDER, 0N USER
PROVIDER: name
ASSOCIATE, 0N PROVIDER, 01 TRANSACTION
TAG: tag_name

USER_ROLE: name
ATTACH, 11 USER, 01 USER_ROLE
USER: email, password, last_name, first_name, profile_picture
BELONG_TO, 01 USER, 11 WALLET
:
CONTAIN, 0N TAG, 0N TRANSACTION

:
:
MODIFY, 0N USER, 11 CATEGORY
WALLET: name, currency, initial_balance
COMPOSE, 0N WALLET, 11 TRANSACTION
TRANSACTION: amount, type, note, transaction_date

ICON: icon_name, url
LINK, 11 CATEGORY, 0N ICON
CATEGORY: name, icon, color, type, is_global
INCLUDE, 0N CATEGORY, 11 TRANSACTION
:
:

:
:
CREATE, 11 PROVIDER, 0N USER
PROVIDER: name
ASSOCIATE, 0N PROVIDER, 01 TRANSACTION
TAG: tag_name

USER_ROLE: name
ATTACH, 11 USER, 01 USER_ROLE
USER: email, password, last_name, first_name, BELONG_TO, 01 USER, 11 WALLET
:
CONTAIN, 0N TAG, 0N TRANSACTION

PROFILE_PICTURE: file, LOAD, 11 USER, 11 PROFILE_PICTURE :
:
MODIFY, 0N USER, 11 CATEGORY
WALLET: name, currency, initial_balance
COMPOSE, 0N WALLET, 11 TRANSACTION
TRANSACTION: amount, type, note, transaction_date

ICON: icon_name, url
LINK, 11 CATEGORY, 0N ICON
CATEGORY: name, icon, color, type, is_global
INCLUDE, 0N CATEGORY, 11 TRANSACTION
:
:


MODÈLE LOGIQUE DE DONNÉES :

CATEGORIE ( nom_categorie, icone, couleur, type, est_global, #nom_icone )

CREER ( #email_utilisateur, #nom_categorie )

ICONE ( nom_icone, url )

PORTEFEUILLE ( nom_portefeuille, devise, solde_initia, #email_utilisateur )

REUTILISER ( #nom_portefeuille, #nom_categorie )

TRANSACTION ( montant, type, note, date_de_la_transaction, #nom_portefeuille, nom, #nom_categorie, nom_etiquette )

UTILISATEUR ( email_utilisateur, mot_de_passe, nom, prénom, photo_de_profil )

Diagramme de séquence :

Nous avons utilisé le site sequencediagram

Voici un CRUD pour les transactions avec les scripts pour la génération des diagrammes sur le site

Créer une transaction

diagramme:


script:
title Création d'une transaction

participant Utilisateur
participant Frontend
participant API
participant DB

Utilisateur->Frontend: Remplit formulaire "Nouvelle transaction"
Frontend->API: POST /api/transactions (montant, catégorie, date, etc.)

alt Utilisateur authentifié
    API->API: Vérifie la validité des données (contrôles métiers et format)
    alt Données valides et droits OK
        API->DB: INSERT INTO transactions (...)
        DB-->API: Retour nouvel ID
        API-->Frontend: HTTP 201 + transaction JSON
    else Données invalides
        API-->Frontend: HTTP 400 Bad Request + message d'erreur
    else Droits insuffisants
        API-->Frontend: HTTP 403 Forbidden
    else Erreur technique
        API-->Frontend: HTTP 500 Internal Server Error
    end
else Non authentifié
    API-->Frontend: HTTP 403 Forbidden
end

Frontend-->Utilisateur: Affiche message de confirmation ou erreur

Consulter la liste des transactions

diagramme:


script:
title Consultation de la liste des transactions

participant Utilisateur
participant Frontend
participant API
participant DB

Utilisateur->Frontend: Accède au dashboard
Frontend->API: GET /api/transactions (filtres éventuels)

alt Utilisateur authentifié
    API->API: Vérifie les droits d'accès (appartenance utilisateur)
    alt Droits OK
        API->DB: SELECT * FROM transactions WHERE user_id = ? [et filtres éventuels]
        DB-->API: Liste des transactions
        API-->Frontend: HTTP 200 + JSON (liste des transactions)
    else Droits insuffisants
        API-->Frontend: HTTP 403 Forbidden
    else Erreur technique
        API-->Frontend: HTTP 500 Internal Server Error
    end
else Non authentifié
    API-->Frontend: HTTP 403 Forbidden
end

Frontend-->Utilisateur: Affiche la liste ou message d'erreur


Consulter une transaction avec un id

diagramme:


script:
title Consultation d'une transaction par ID

participant Utilisateur
participant Frontend
participant API
participant DB

Utilisateur->Frontend: Accède à la page d'une transaction (id)
Frontend->API: GET /api/transactions/:id

alt Utilisateur authentifié
    API->API: Vérifie l'existence de la transaction et les droits d'accès
    alt Transaction existe et droits OK
        API->DB: SELECT * FROM transactions WHERE id = ?
        DB-->API: Données de la transaction
        API-->Frontend: HTTP 200 + JSON (transaction)
    else Transaction inexistante ou droits insuffisants
        API-->Frontend: HTTP 403 Forbidden ou 404 Not Found
    end
    alt Erreur technique
        API-->Frontend: HTTP 500 Internal Server Error
    end
else Non authentifié
    API-->Frontend: HTTP 403 Forbidden
end
Frontend-->Utilisateur: Affiche les détails ou message d'erreur


Modifier une transaction

diagramme:


script:
title Modification d'une transaction

participant Utilisateur
participant Frontend
participant API
participant DB

Utilisateur->Frontend: Ouvre formulaire d'édition (transaction id)
Frontend->API: PUT /api/transactions/:id (nouveaux champs)

alt Utilisateur authentifié
    API->API: Vérifie l'existence de la transaction et les droits d'accès
    alt Transaction existe et droits OK
        API->API: Valide les nouvelles données
        alt Données valides
            API->DB: UPDATE transactions SET ... WHERE id = ?
            DB-->API: OK ou nombre lignes modifiées
            API-->Frontend: HTTP 200 + transaction mise à jour
        else Données invalides
            API-->Frontend: HTTP 400 Bad Request + message d'erreur
        else Erreur technique
            API-->Frontend: HTTP 500 Internal Server Error
        end
    else Transaction inexistante ou droits insuffisants
        API-->Frontend: HTTP 403 Forbidden ou 404 Not Found
    end
else Non authentifié
    API-->Frontend: HTTP 403 Forbidden
end

Frontend-->Utilisateur: Affiche message de validation ou d’erreur


Supprimer une transaction

diagramme:


script:
title Suppression d'une transaction

participant Utilisateur
participant Frontend
participant API
participant DB

Utilisateur->Frontend: Clique "Supprimer transaction"
Frontend->API: DELETE /api/transactions/:id

alt Utilisateur authentifié
    API->API: Vérifie l'existence et l'appartenance de la transaction
    alt Transaction existe et droits OK
        API->DB: DELETE FROM transactions WHERE id = ?
        DB-->API: OK ou nombre de lignes supprimées
        API-->Frontend: HTTP 204 No Content
    else Transaction inexistante ou droits insuffisants
        API-->Frontend: HTTP 403 Forbidden ou 404 Not Found
    end
    alt Erreur technique
        API-->Frontend: HTTP 500 Internal Server Error
    end
else Non authentifié
    API-->Frontend: HTTP 403 Forbidden
end

Frontend-->Utilisateur: Affiche message "Transaction supprimée" ou erreur


Introduction

Ce wiki centralise toutes les conventions, tokens, outils SCSS ainsi que les composants UI de l’application

Ici, on peut trouver:

  • Toutes les couleurs, espacements et typos officielles (code + exemples)
  • Les mixins et outils SCSS réutilisables
  • la documentation des composants UI ainsi que leurs propriétés et les bonnes pratiques d’accessibilité

Objectif

Mettre en place une base SCSS pour avoir des styles cohérents et faciles à maintenir dans Angular 19. On utilise des “Design Tokens” (variables globales) pour que tout soit centralisé.


📁 Arborescence SCSS

src/
└── styles/
    ├── styles.scss                # Entrée globale Angular
    ├── abstracts/
    │   └── _breakpoints.scss       # Mixin mq()
    │   └── _layout.scss       # Mixin padding, margin, radius, flexbox
    │   └── _shadows.scss       # Mixin sur les box-shadows
    │   └── _typography.scss       # Mixin pour appliquer les fonts

    ├── base/
    │   └── _reset.scss             # Reset CSS de base
    ├── fonts/
    │   └── _font-face.scss             # déclaration des fonts
    ├── tokens/
    │   ├── _variables-light.scss   # Thème clair
    │   ├── _variables-dark.scss    # Thème sombre
    │   ├── _variables-desktop.scss # Tailles desktop
    │   └── _variables-mobile.scss  # Tailles mobile
    └── themes/
        ├── _tokens.scss            # Fonctions `themed()` / `themed-block()`
        └── _tokens.map.scss        # Généré automatiquement (voir doc design-tokens)

Gestion des Etats avec Angular

Les Signals

Quand utiliser les Signals en Angular

1. Idéal pour les composants UI simples et réactifs

Exemples :

  • Boutons
  • Icônes
  • inputs
  • Dropdowns

Pourquoi ?

  • Mise à jour immédiate sans re-render global
  • Pas de gestion d’abonnement / désabonnement
  • Code plus simple et lisible
  • Très performant pour l’état local et synchrone

2. 🌐 Observables (RxJS) pour les flux asynchrones

Exemples :

  • Requêtes HTTP
  • WebSocket / EventSource
  • Flux d’événements utilisateur complexes

Pourquoi ?

  • Puissance de composition (mergeMap, combineLatest, etc.)
  • Gestion d’événements multiples ou dépendants
  • Contrôle du temps (debounce, delay, etc.)

🔁 Ces observables peuvent être convertis en signals dans les composants avec toSignal().


3. Classes pour les composants complexes avec logique métier

Exemples :

  • Formulaires avancés avec validations dynamiques
  • Menus interactifs et contextuels
  • Wizards, éditeurs, dashboards

Pourquoi ?

  • Encapsulation claire avec décorateurs (@Input, @Output, DI…)
  • Bonne séparation des responsabilités
  • Combinable avec des signals pour une logique plus réactive

Combinaison des trois

Un code moderne Angular typique combine :

  • RxJS dans les services (pour les flux de données externes)
  • Signals dans les composants (pour l’état local réactif)
  • Classes Angular (comme conteneur logique et structure de composant)

Résumé rapide

TâcheOutil recommandé
UI léger et très réactifsignal()
Données asynchrones (HTTP, etc.)Observable (RxJS)
Formulaire / menu / logique métierclass (avec Signals)
Coordination entre composantsObservable, @Input

Paradigme de Gestion: Signals vs Classes

1. Gestion d’état classique dans Angular (avant Signals)

En Angular traditionnel (pré-Signals), les composants sont des classes avec :

  • Des attributs d’état (class fields).
  • Du templating via interpolation et bindings ({{ value }}, [prop]="value", (event)…).
  • Et parfois des observables RxJS pour les flux de données.

Exemple :

@Component({ ... })
export class MyComponent {
  count = 0;

  increment() {
    this.count++;
  }
}

Problème :

  • Le mécanisme de détection des changements d’Angular (basée sur Zone.js) doit scruter les changements potentiels partout dans le template et la hiérarchie.
  • Pour détecter un changement de count, Angular déclenche une boucle de vérification (digestion), typiquement à chaque click, HTTP, ou setTimeout.

👉 C’est une approche pull-based : Angular vérifie l’état pour savoir s’il a changé.


2. Les Signaux Angular (Angular v16+)

Les Signaux (signal()) sont une abstraction réactive qui permet de déclarer l’état comme une source de données auto-réactive.

Définition abstraction réactive:

En informatique, une abstraction réactive désigne un modèle ou une couche logicielle qui permet de gérer et de réagir automatiquement aux changements de données ou d’état, sans avoir à écrire explicitement le code de synchronisation ou de propagation de ces changements.

Dans le cadre des signaux avec Angular :

Un signal est une primitive réactive introduite dans Angular pour gérer l’état de façon déclarative et réactive. Une abstraction réactive avec les signaux signifie que l’on peut manipuler des valeurs (états, données) via des objets ou fonctions qui “savent” notifier automatiquement les parties de l’application qui en dépendent dès qu’il y a un changement. Cela évite d’avoir à gérer manuellement les subscriptions, le changement de détection, ou les flux d’événements : on déclare des dépendances, et Angular s’occupe de réagir aux changements.

Exemple équivalent :

import { signal } from '@angular/core';

@Component({ ... })
export class MyComponent {
  count = signal(0);

  increment() {
    this.count.update(c => c + 1);
  }
}

En template :

<p>Count: {{ count() }}</p>

Remarque : count() lit la valeur du signal. Toute dépendance à count() est automatiquement suivie.

✅ Ce qui change :

  • Aucune vérification globale nécessaire.
  • Seul le DOM ou le composant qui dépend de count est mis à jour.
  • Angular sait exactement quoi mettre à jour, sans scruter toute l’arborescence.

C’est une approche push-based : le signal pousse la mise à jour aux endroits concernés.


Push-based vs Pull-based — Résumé conceptuel

ParadigmePull-basedPush-based
DéclenchementOn vérifie si quelque chose a changéLe changement déclenche une mise à jour
ExemplesZone.js, @Input() bindings, ngOnChanges, ChangeDetectorRef.detectChanges()Signals, RxJS (partiellement), Observables
PerformanceMoins prévisible, dépend d’une boucleHaute performance, ciblé et réactif
RéactivitéImplicite, globaleExplicite, fine-grainée
Modèle mental“Quelque chose a peut-être chang锓Je sais que ça a changé”

Bonus : Signaux vs RxJS

Même si RxJS est aussi push-based, la principale différence est :

  • RxJS = asynchrone, orienté streams d’événements
  • Signals = synchrone, orienté état actuel (comparable à un BehaviorSubject readonly)

En Angular moderne : RxJS pour les flux (ex: HTTP, websocket) + Signals pour l’état local réactif = combo idéal.


En résumé

  • Les signaux Angular changent fondamentalement la façon de penser l’état.
  • On passe d’un modèle “passif” (je déclare un champ) à un modèle réactif et auto-propagé.
  • Le passage du pull au push améliore :
    • ✅ les perfs (moins de vérifications inutiles),
    • ✅ la clarté du code (moins d’effets de bord),
    • ✅ la maintenabilité (les dépendances sont explicites).

Composants UI: pattern signal, input, computed

1. Le pattern moderne : signal + @Input + computed

Ce pattern est idéal pour les composants réutilisables, simples et très réactifs, comme les icônes, boutons, dropdowns.

🔧 Exemple :

import { Component, Input, computed, signal } from '@angular/core';

@Component({
  selector: 'app-icon',
  template: `<i [class]="iconClass()"></i>`
})
export class IconComponent {
  private _name = signal('default');
  private _size = signal('md');

  @Input()
  set name(value: string) {
    this._name.set(value);
  }

  @Input()
  set size(value: string) {
    this._size.set(value);
  }

  readonly iconClass = computed(() => {
    return \`icon-\${this._name()} icon-size-\${this._size()}\`;
  });
}

Avantages :

  • Réactivité automatique sans ngOnChanges
  • Code plus clair et déclaratif
  • Pas de risque de fuite mémoire
  • Pas de logique dans le template
  • Très performant pour des composants UI

2. Le modèle classique : @Input() seul + ngOnChanges

Si tu utilises @Input() sans signal, alors tu dois gérer manuellement les effets des changements.

Exemple :

import { Component, Input, OnChanges, SimpleChanges } from '@angular/core';

@Component({
  selector: 'app-greeting',
  template: `<p>{{ greeting }}</p>`
})
export class GreetingComponent implements OnChanges {
  @Input() name = '';
  greeting = '';

  ngOnChanges(changes: SimpleChanges): void {
    if (changes['name']) {
      this.greeting = \`Bonjour \${this.name}\`;
    }
  }
}

Inconvénients :

  • Plus verbeux
  • Moins déclaratif
  • Moins performant si utilisé massivement
  • Besoin de SimpleChanges pour gérer les cas complexes

Comparatif résumé

ApprocheRéactivitéSimplicitéPerformanceBesoin de ngOnChanges
@Input() seul❌ ManuelleMoyenneMoyenne✅ Oui
signal() + @Input() + computed()✅ Automatique✅ Haute✅ Excellente❌ Non

Conclusion

Utiliser signal() avec @Input() et computed() te permet de créer des composants plus déclaratifs, performants et faciles à maintenir. La méthode ngOnChanges reste utile pour les cas où tu ne peux pas ou ne veux pas utiliser les signals.

Tokens et Variables

🎨 Intégrations des tokens Figma

📌 Objectif

Ce document explique comment importer et synchroniser les Design Tokens Figma (couleurs, espacements, typographies, radius…) dans l’application Angular pecunia-front, jusqu’à la génération du mapping SCSS, en gardant un process automatisé, fiable et maintenable.


🔧 Outil de design utilisé

  • Figma + plugin Variables Import/Export
    Ce plugin permet d’exporter toutes les variables Figma sous forme de fichiers JSON normalisés.

🧩 Pipeline d’import et de mapping

1. Export Figma

  • Le designer exporte les variables Figma via le plugin, ce qui génère les fichiers JSON dans tokens/import.

2. Génération des SCSS et du mapping avec Style Dictionary

  • La commande suivante automatise la création des fichiers SCSS (un par thème ou plateforme) et du mapping SCSS :

    npm run build-color-token
    
  • Cette commande fait tout le travail pour toi :

    • Elle transforme les fichiers JSON de Figma en fichiers SCSS utilisables dans Angular.
    • Elle crée un fichier de mapping (src/styles/themes/_tokens.map.scss) qui relie chaque nom de variable à sa valeur pour chaque thème (clair/sombre).
  • Pourquoi faire comme ça ?

    • Pour être sûr que les couleurs et autres variables sont toujours à jour entre Figma et le code.
    • Pour éviter de devoir tout refaire à la main à chaque changement.
    • Pour ne pas se tromper ou oublier une variable.

📁 Arborescence des tokens

Fichiers exportés depuis Figma :

tokens/
└── import/
    ├── primitives.json
    ├── colors.light.json
    ├── colors.dark.json
    ├── size.desktop.json
    └── size.mobile.json

Fichiers générés pour Angular :

src/styles/tokens/
├── _variables-light.scss
├── _variables-dark.scss
├── _variables-desktop.scss
└── _variables-mobile.scss
src/styles/themes/
└── _tokens.map.scss

🔄 Mise à jour des tokens Figma → Angular

Étapes à suivre à chaque modification des tokens dans Figma :

  1. Exporter les nouveaux JSON depuis Figma dans tokens/import.
  2. Générer les SCSS et le mapping à jour :
    npm run build-color-token
    
  3. Vérifier que les fichiers dans src/styles/tokens et src/styles/themes sont bien à jour.
  4. Relancer l’application Angular si besoin.

Exemple de token map générée

@use '../tokens/variables-light' as light;
@use '../tokens/variables-dark' as dark;

$tokens: (
  'background-neutral-primary': (
    light: light.$background-neutral-primary,
    dark: dark.$background-neutral-primary,
  ),
  'text-neutral-default': (
    light: light.$text-neutral-default,
    dark: dark.$text-neutral-default,
  ),
  // ...autres tokens
);

Explication

Ce mapping permet de retrouver la bonne valeur d’une variable (token) selon le thème (clair ou sombre).
Exemple : si tu veux la couleur de fond pour le thème “dark”, tu demandes 'background-neutral-primary' et tu obtiens la bonne couleur pour “dark”.


Configuration dans package.json

"scripts": {
  "build-tokens": "node scripts/build-tokens.mjs",
  "generate-token-map": "node scripts/generate-token-map.mjs",
  "build-color-tokens": "npm run build-tokens && npm run generate-token-map"
}

🎯 Pourquoi ce pipeline ?

  • Automatisation : Moins d’erreurs humaines, process reproductible.
  • Cohérence : Les tokens Figma sont la source de vérité, le code Angular reflète toujours le design.
  • Scalabilité : Facile d’ajouter de nouveaux thèmes ou plateformes (ex : mobile/desktop).
  • DRY/SOLID : Centralisation, factorisation, séparation des responsabilités.

📚 Résumé pédagogique

  • Chaque étape du pipeline a une responsabilité claire (S de SOLID).
  • Le mapping évite la duplication et permet de changer de thème dynamiquement sans toucher à tous les styles (DRY).
  • Les scripts automatisent la synchronisation entre Figma et Angular, pour un design system robuste et maintenable.

Table de correspondance des couleurs

TokenLightCouleurDarkCouleur
background-alert-default-low#fee2e2color#fee2e2color
background-alert-default-medium#dc2626color#dc2626color
background-alert-disabled#fee2e2color#fecacacolor
background-alert-focus-low#fca5a5color#fca5a5color
background-alert-focus-medium#af0404color#f87171color
background-alert-hover-low#fca5a5color#fca5a5color
background-alert-hover-medium#af0404color#f87171color
background-badge-blue-low#0056a0color#38bdf8color
background-badge-blue-medium#075985color#0ea5e9color
background-badge-green-low#047857color#6ee7b7color
background-badge-green-medium#065f46color#10b981color
background-badge-grey#374151color#d1d5dbcolor
background-badge-orange-low#c2410ccolor#fdba74color
background-badge-orange-medium#9a3412color#f97316color
background-badge-pink-low#be185dcolor#f9a8d4color
background-badge-pink-medium#9d174dcolor#ec4899color
background-badge-red-low#dc2626color#fca5a5color
background-badge-red-medium#af0404color#f87171color
background-badge-violet-low#7c3aedcolor#c4b5fdcolor
background-badge-violet-medium#6d28d9color#8b5cf6color
background-badge-yellow-low#a16207color#fde047color
background-badge-yellow-medium#854d0ecolor#eab308color
background-neutral-primary#f9fafbcolor#030712color
background-neutral-primary-inverse#030712color#f9fafbcolor
background-neutral-primary-static#f9fafbcolor#f9fafbcolor
background-neutral-primary-static-inverse#030712color#030712color
background-neutral-secondary#f6f8f7color#111827color
background-neutral-secondary-inverse#111827color#f6f8f7color
background-neutral-secondary-static#f6f8f7color#f6f8f7color
background-neutral-secondary-static-inverse#111827color#111827color
background-neutral-tertiary#eef6f3color#111827color
background-primary-default#085e41color#42f0b6color
background-primary-disabled#d0fbedcolor#d0fbedcolor
background-primary-focus#0fbd83color#d0fbedcolor
background-primary-hover#0fbd83color#d0fbedcolor
background-primary-hover-ghost#d0fbedcolor#d0fbedcolor
background-secondary-default#244238color#7ab8a2color
background-secondary-disabled#deede8color#deede8color
background-secondary-focus#7ab8a2color#deede8color
background-secondary-hover#7ab8a2color#deede8color
background-secondary-hover-ghost#deede8color#deede8color
background-success-default-low#d1fae5color#d1fae5color
background-success-default-medium#047857color#34d399color
background-success-default-medium-static#047857color#047857color
background-success-focus-low#6ee7b7color#6ee7b7color
background-success-focus-medium#065f46color#34d399color
background-success-hover-low#6ee7b7color#6ee7b7color
background-success-hover-medium#065f46color#34d399color
border-alert-default-low#fee2e2color#fee2e2color
border-alert-default-medium#dc2626color#fca5a5color
border-alert-default-medium-static#dc2626color#dc2626color
border-alert-disabled#fee2e2color#fee2e2color
border-alert-focus-low#fca5a5color#fca5a5color
border-alert-focus-medium#af0404color#f87171color
border-alert-hover-low#fca5a5color#fca5a5color
border-alert-hover-medium#af0404color#f87171color
border-neutral-primary#f9fafbcolor#030712color
border-neutral-primary-inverse#030712color#f9fafbcolor
border-neutral-primary-static#f9fafbcolor#f9fafbcolor
border-neutral-primary-static-inverse#030712color#030712color
border-neutral-secondary#f6f8f7color#111827color
border-neutral-secondary-inverse#d1d5dbcolor#4b5563color
border-neutral-secondary-static#f6f8f7color#f6f8f7color
border-neutral-secondary-static-inverse#111827color#111827color
border-primary-default#085e41color#13eca4color
border-primary-disabled#d0fbedcolor#d0fbedcolor
border-primary-focus#0fbd83color#d0fbedcolor
border-primary-hover#0fbd83color#d0fbedcolor
border-primary-hover-ghost#d0fbedcolor#d0fbedcolor
border-secondary-default#244238color#7ab8a2color
border-secondary-disabled#deede8color#deede8color
border-secondary-focus#7ab8a2color#deede8color
border-secondary-hover#7ab8a2color#deede8color
border-secondary-hover-ghost#deede8color#deede8color
border-success-default-low#d1fae5color#d1fae5color
border-success-default-medium#047857color#6ee7b7color
border-success-default-medium-static#047857color#047857color
border-success-focus-low#6ee7b7color#6ee7b7color
border-success-focus-medium#065f46color#34d399color
border-success-hover-low#6ee7b7color#6ee7b7color
border-success-hover-medium#065f46color#34d399color
common-danger-hight#af0404color#fca5a5color
common-danger-highter#7f1d1dcolor#fee2e2color
common-danger-hightest#450a0acolor#fef2f2color
common-danger-low#fca5a5color#dc2626color
common-danger-lower#fee2e2color#991b1bcolor
common-danger-lowest#fef2f2color#450a0acolor
common-danger-medium#ef4444color#f87171color
common-information-hight#0369a1color#7dd3fccolor
common-information-highter#0c4a6ecolor#e0f2fecolor
common-information-hightest#082f49color#f0f9ffcolor
common-information-low#7dd3fccolor#0056a0color
common-information-lower#e0f2fecolor#075985color
common-information-lowest#f0f9ffcolor#082f49color
common-information-medium#0ea5e9color#38bdf8color
common-neutral-hight#4b5563color#d1d5dbcolor
common-neutral-highter#374151color#f6f8f7color
common-neutral-hightest#19190bcolor#ffffffcolor
common-neutral-low#d1d5dbcolor#4b5563color
common-neutral-lower#f6f8f7color#1f2937color
common-neutral-lowest#ffffffcolor#19190bcolor
common-neutral-medium#9ca3afcolor#6b7280color
common-primary-high#085e41color#42f0b6color
common-primary-higher#13201ccolor#d0fbedcolor
common-primary-highest#0f1110color#e7fdf6color
common-primary-low#71f4c8color#085e41color
common-primary-lower#d0fbedcolor#13201ccolor
common-primary-lowest#e7fdf6color#0f1110color
common-primary-medium#0fbd83color#0fbd83color
common-secondary-high#244238color#7ab8a2color
common-secondary-higher#12211ccolor#deede8color
common-secondary-highest#09110ecolor#eef6f3color
common-secondary-low#9cc9b9color#244238color
common-secondary-lower#deede8color#12211ccolor
common-secondary-lowest#eef6f3color#09110ecolor
common-secondary-medium#486b5fcolor#47856fcolor
common-success-hight#047857color#6ee7b7color
common-success-highter#064e3bcolor#d1fae5color
common-success-hightest#022c22color#ecfdf5color
common-success-low#6ee7b7color#14936bcolor
common-success-lower#d1fae5color#065f46color
common-success-lowest#ecfdf5color#022c22color
common-success-medium#10b981color#34d399color
common-warning-hight#c2410ccolor#fdba74color
common-warning-highter#7c2d12color#ffedd5color
common-warning-hightest#431407color#fff7edcolor
common-warning-low#fdba74color#d65a00color
common-warning-lower#ffedd5color#9a3412color
common-warning-lowest#fff7edcolor#431407color
common-warning-medium#f97316color#fb923ccolor
icon-text-alert-default#dc2626color#fca5a5color
icon-text-alert-default-darker#af0404color#af0404color
icon-text-alert-default-lighter#ef4444color#dc2626color
icon-text-default#030712color#f9fafbcolor
icon-text-default-inverse#f9fafbcolor#030712color
icon-text-default-static#030712color#030712color
icon-text-default-static-inverse#f9fafbcolor#f9fafbcolor
icon-text-neutral-secondary#4b5563color#d1d5dbcolor
icon-text-primary-default#085e41color#42f0b6color
icon-text-primary-focus#0fbd83color#d0fbedcolor
icon-text-primary-hover#0fbd83color#d0fbedcolor
icon-text-secondary-default#244238color#7ab8a2color
icon-text-secondary-focus#7ab8a2color#deede8color
icon-text-secondary-hover#7ab8a2color#deede8color
icon-text-success-default#14936bcolor#6ee7b7color
icon-text-success-default-darker#047857color#047857color
icon-text-success-default-lighter#10b981color#14936bcolor
overlay-100#09090bcolor#ffffffcolor
overlay-200#09090bcolor#ffffffcolor
overlay-300#09090bcolor#ffffffcolor
overlay-50#09090bcolor#ffffffcolor
overlay-500#09090bcolor#09090bcolor
text-alert-default#dc2626color#fca5a5color
text-alert-default-darker#af0404color#af0404color
text-alert-default-lighter#ef4444color#dc2626color
text-neutral-default#030712color#f9fafbcolor
text-neutral-default-inverse#f9fafbcolor#030712color
text-neutral-default-static#030712color#030712color
text-neutral-default-static-inverse#f9fafbcolor#f9fafbcolor
text-neutral-secondary#4b5563color#d1d5dbcolor
text-primary-default#085e41color#13eca4color
text-primary-default-darker#13201ccolor#0fbd83color
text-primary-default-lighter#0b8e62color#42f0b6color
text-primary-disabled#085e41color#085e41color
text-secondary-default#244238color#59a68bcolor
text-secondary-default-darker#12211ccolor#47856fcolor
text-secondary-default-lighter#486b5fcolor#7ab8a2color
text-secondary-disabled#486b5fcolor#486b5fcolor
text-success-default#047857color#6ee7b7color
text-success-default-darker#065f46color#047857color
text-success-default-lighter#10b981color#14936bcolor

Mixins : gérer les couleurs des themes

Le thème actif est appliqué via l’attribut data-theme sur la balise <html> :

<html data-theme="light">
  ou
  <html data-theme="dark"></html>
</html>

Un système centralisé permet de faire correspondre un token logique à la bonne valeur du thème :

// styles/themes/tokens.scss
@use 'sass:map';
@use 'tokens.map' as tokens-map;

@function themed($key, $theme-name) {
  $entry: map.get(tokens-map.$tokens, $key);
  @if $entry == null {
    @return null;
  }
  $value: map.get($entry, $theme-name);
  @if $value == null {
    @return null;
  }
  @return $value;
}

@mixin themed-block($props-map) {
  @each $theme-name in $theme-names {
    :host-context([data-theme='#{$theme-name}']) & {
      @each $prop, $token in $props-map {
        #{$prop}: themed($token, $theme-name);
      }
    }
  }
}

Explication simplifiée

  • themed($key, $theme-name) :
    Cette fonction va chercher la bonne valeur d’une variable (token) selon le thème (clair ou sombre).
    Exemple : si tu demandes la couleur de fond pour le thème “dark”, elle te donne la bonne couleur.

  • themed-block($props-map) :
    Ce mixin applique plusieurs propriétés CSS selon le thème actif.
    Il les applique automatiquement pour chaque thème une liste de propriété simple qu’on lui donne

  • On utilise :host-context([data-theme='#{$theme-name}']) pour que le style change tout seul quand le thème change, sans toucher au code du composant.

  • Ça évite de recopier la logique de thème partout (DRY) et chaque fonction/mixin a un but précis (S de SOLID).


✅ Exemple avec un bouton

@use '../../../styles/themes/tokens' as theme;
@use '../../../styles/tokens/variables-mobile' as *;

.btn-switch {
  //style classique
  padding: 10px;
  border-radius: 8px;
  cursor: pointer;

  // application des variables issues des tokens sur les 2 thèmes
  // mixin themed-block à utiliser
  @include theme.themed-block(
    (
      background-color: 'background-primary-default',
      color: 'text-neutral-default-inverse',
    )
  );

  &:hover {
    @include theme.themed-block(
      (
        background-color: 'background-primary-hover',
      )
    );

    // 🎯 Application uniquement pour le light
    // utiliser fonction themed avec :host-context([data-theme='light'])
    :host-context([data-theme='light']) & {
      color: theme.themed('text-neutral-default', 'light');
    }
  }
}

🎯 Pourquoi cette organisation ?

  • Chaque fichier ou fonction a un rôle précis (S de SOLID)
  • Tout est centralisé : on ne répète pas les valeurs (DRY)
  • Facile à faire évoluer : ajouter un thème ou changer une couleur est simple
  • Lisible : tout le monde comprend où et comment utiliser les outils du design system

⚠️ Limite connue : propriétés CSS complexes (ex : linear-gradient)

la mixin themed-block remplace chaque propriété CSS du map par la valeur du token pour chaque thème.

Mais elle ne sait pas parser une fonction CSS complexe (ex: linear-gradient(…)) : elle attend un token simple.

Si tu fais ça :


@include theme.themed-block((
 background: linear-gradient(
   117deg,
   'background-neutral-primary' 50.11%,
   'common-neutral-low' 100%
 )
));

→ $token = toute la string linear-gradient(…) → La fonction themed() ne sait pas quoi faire de cette string qui mélange tokens et CSS.

Sass ne peut pas analyser et “remplacer” chaque nom de token à l’intérieur d’une string complexe.

Il faudrait parser la string, reconnaître les tokens, et appeler themed() sur chaque.

Il faut donc le faire à la main, c’est la limite naturelle du SCSS “classique”

→ Soit on passe par une mixin/fonction custom encore plus complexe (peu utile ici), → Soit on écrit le gradient manuellement pour chaque thème, comme tu as fait :

.main-wrapper {
  background: linear-gradient(
    117deg,
    #{themed('background-neutral-primary', 'light')} 50.11%,
    #{themed('common-neutral-low', 'light')} 100%
  );
}

:host-context([data-theme='dark']) .main-wrapper {
  background: linear-gradient(
    117deg,
    #{themed('background-neutral-primary', 'dark')} 50.11%,
    #{themed('common-neutral-low', 'dark')} 100%
  );
}

La mixin themed-block fonctionne parfaitement pour remplacer des propriétés simples (color, background-color, border-color, etc.), mais par conception, elle ne peut pas parser ni remplacer automatiquement chaque nom de token à l’intérieur d’une fonction CSS complexe comme un linear-gradient.

Dans ces cas, on utilise directement la fonction themed dans la string de gradient, pour garantir la cohérence DS, tout en restant explicite.

Typographies

Gestions des Polices

Nous utilisons Open Sans dans différentes variantes pour couvrir tous les styles du projet (Display, Heading, Text).

📦 Organisation des fichiers

les fichiers de polices sont placées dans le dossier src/assets/fonts

les déclarations sont définies dans le dossier src/styles/fonts/_font-face.scss

Les variables typographiques (tailles, poids, interlignes) et la mixin utilitaire sont dans src/styles/abstracts/_typography.scss

Mixin

@mixin text-style($size-key, $weight-key: regular, $mode: desktop) {
  $sizes: if($mode == desktop, $sizes-desktop, $sizes-mobile);

  font-family: $font-family-base;
  font-size: map.get($sizes, $size-key);
  line-height: map.get($sizes, $size-key);
  font-weight: map.get($font-weights, $weight-key);
}

Cette mixin permet d’appliquer une règle typographique complète (police, taille, interligne, poids) à partir de clés logiques comme heading-h2, text-sm, etc.

Le paramètre $mode permet de basculer dynamiquement entre mobile et desktop.

les clés sont dans typography.scss

✅ Exemple d’utilisation d’une font


@use '../../../styles/abstracts/typography' as typo;

h2 {
  @include typo.text-style(heading-h2, extrabold, mobile);
}

Cet exemple applique :

  • la police “Open Sans”
  • une taille adaptée à un titre de niveau 2
  • un poids fort (extrabold)
  • un interligne cohérent avec la maquette``

Documentation des paramètres typographiques

Paramètres

ParamètreTypeValeurs possiblesDéfautDescription
$size-keystringVoir tableaux ci-dessousClé de taille (définit font-size & line-height)
$weight-keystringregular, semibold, bold, extraboldregularPoids de police
$modestringdesktop, mobiledesktopMode d’affichage (pour responsive)

Tailles disponibles (en px)

Desktop

Cléfont-size / line-height
display-h180px
display-h260px
display-h348px
display-h436px
display-h524px
display-h620px
heading-h130px
heading-h224px
heading-h320px
heading-h418px
heading-h516px
heading-h614px
text-lg18px
text-md16px
text-sm14px
text-xs12px
text-xxs10px

Mobile

Cléfont-size / line-height
display-h148px
display-h236px
display-h328px
display-h424px
display-h518px
display-h616px
heading-h124px
heading-h220px
heading-h318px
heading-h416px
heading-h514px
heading-h612px
text-lg16px
text-md14px
text-sm12px
text-xs10px
text-xxs9px

Poids disponibles

CléValeur CSS
regular400
semibold600
bold700
extrabold800

Layout

Mixin, Media Query et Breakpoint

abstracts/_breakpoints.scss

mq = media query

Explication

Le mixin mq sert à écrire facilement des media-queries (pour adapter le style selon la taille d’écran).
Au lieu de répéter les tailles partout, on utilise des noms comme mobile, tablet, desktop.
Si on veut changer une taille, il suffit de modifier la map en haut du fichier.

Cela évite de recopier les mêmes valeurs partout (DRY) et chaque fichier a un rôle précis (S de SOLID).

@use 'sass:map';

$breakpoints: (
  mobile: 390px,
  tablet: 768px,
  desktop: 1024px,
);

@mixin mq($breakpoint) {
  $value: map.get($breakpoints, $breakpoint);
  @if $value {
    @media screen and (min-width: $value) {
      @content;
    }
  } @else {
    @warn "Breakpoint #{$breakpoint} non défini.";
  }
}

🔄 Exemple d’utilisation du mixin

h1 {
  font-size: 18px;

  @include mq(tablet) {
    font-size: 24px;
  }

  @include mq(desktop) {
    font-size: 32px;
  }
}

Espacements, margin, padding et radius

abstracts/_Layout.scss

le fichier layout.scss contient les variables pour les espacement et les radius utilisables directement dans le scss

$space-2: px-to-rem(2); // 2px
$space-4: px-to-rem(4); // 4px
$space-6: px-to-rem(6); // 6px
$space-8: px-to-rem(8); // 8px
$space-10: px-to-rem(10); // 10px
$space-12: px-to-rem(12); // 12px
$space-14: px-to-rem(14); // 14px
$space-16: px-to-rem(16); // 16px
$space-24: px-to-rem(24); // 24px
$space-32: px-to-rem(32); // 32px
$space-48: px-to-rem(48); // 48px
$space-64: px-to-rem(64); // 64px
$space-80: px-to-rem(80); // 80px
$space-96: px-to-rem(96); // 96px
$space-128: px-to-rem(128); // 128px

  
$radius-sm: 4px;
$radius-md: 8px;
$radius-lg: 16px;
$radius-xl: 24px;
$radius-pill: 9999px;

la fonction px-to-rem permet de convertir une valeur numérique en unité rem

Plusieurs mixins utilitaires sont aussi présentes dans layout pour gérer les margin, padding, radius et les flexbox

padding et margin

@mixin margin($top, $right: null, $bottom: null, $left: null) {
  margin-top: size($top);
  margin-right: if($right != null, size($right), size($top));
  margin-bottom: if($bottom != null, size($bottom), size($top));
  margin-left: if($left != null, size($left), if($right != null, size($right), size($top)));
}
@mixin padding($top, $right: null, $bottom: null, $left: null) {
  padding-top: size($top);
  padding-right: if($right != null, size($right), size($top));
  padding-bottom: if($bottom != null, size($bottom), size($top));
  padding-left: if($left != null, size($left), if($right != null, size($right), size($top)));
}

Ces deux mixins simplifient l’écriture des marges et des padding dans l’application. Elles fonctionnent exactement comme les propriétés CSS margin et padding, mais avec plus de flexibilité.

Les mixins acceptent de 1 à 4 paramètres, tout comme en CSS standard :

@include  margin($top, $right, $bottom, $left);
@include  padding($top, $right, $bottom, $left);

On peut passer e 1 à 4 valeur et les mixins s’adaptent de la façon suivante:

  1. Un seul paramètre ($top) : Appliqué aux quatre côtés
margin-top: $top;
margin-right: $top;     // Même valeur que top
margin-bottom: $top;    // Même valeur que top
margin-left: $top;      // Même valeur que top
  1. Deux paramètres ($top, $right) : Vertical et horizontal
margin-top: $top;
margin-right: $right;
margin-bottom: $top;     // Même valeur que top
margin-left: $right;     // Même valeur que right
  1. Trois paramètres ($top, $right, $bottom) : Comme CSS standard
margin-top: $top;
margin-right: $right;
margin-bottom: $bottom;
margin-left: $right;     // Même valeur que right
  1. Quatre paramètres ($top, $right, $bottom, $left) : Contrôle complet
margin-top: $top;
margin-right: $right;
margin-bottom: $bottom;
margin-left: $left;

Exemples concrets

Exemple 1 : Une valeur (même espacement partout)

.card {
  @include padding(space-8);
}

// Généré en CSS :
.card {
  padding-top: 0.5rem;
  padding-right: 0.5rem;
  padding-bottom: 0.5rem;
  padding-left: 0.5rem;
}

Exemple 2 : Deux valeurs (vertical/horizontal)

.button {
  @include padding(space-4, space-8);
}

// Généré en CSS :
.button {
  padding-top: 0.25rem;     // $space-4
  padding-right: 0.5rem;    // $space-8
  padding-bottom: 0.25rem;  // $space-4
  padding-left: 0.5rem;     // $space-8
}

Exemple 3 : Valeurs spécifiques pour chaque côté

.header {
  @include margin(space-16, space-8, space-4, space-8);
}

// Généré en CSS :
.header {
  margin-top: 1rem;       // $space-16
  margin-right: 0.5rem;   // $space-8
  margin-bottom: 0.25rem; // $space-4
  margin-left: 0.5rem;    // $space-8
}

Exemple 4 : Valeurs nulles pour omettre certains côtés

.section {
  @include padding(space-8, null, space-16);
}

// Généré en CSS :
.section {
  padding-top: 0.5rem;     // $space-8
  padding-right: 0.5rem;   // $space-8 (valeur par défaut = $top)
  padding-bottom: 1rem;    // $space-16
  padding-left: 0.5rem;    // $space-8 (valeur par défaut = $right = $top)
}

Mixin radius – Guide d’utilisation

Ce mixin permet d’appliquer rapidement un border-radius cohérent avec le design system Pecunia, en choisissant une valeur prédéfinie.


Définition SCSS

$radii: (
  sm: 4px,
  md: 8px,
  lg: 16px,
  xl: 24px,
  pill: 9999px,
);

@mixin radius($key: md) {
  $radius: map.get($radii, $key);
  @if $radius {
    border-radius: $radius;
  } @else {
    @warn "Radius `#{$key}` non trouvé dans la map $radii.";
  }
}

Paramètres

ParamètreTypeValeurs possiblesDéfautDescription
$keystringsm, md, lg, xl, pillmdClé du rayon à appliquer

Valeurs disponibles

CléValeur pxUtilisation recommandée
sm4pxPetits éléments, badges
md8pxBoutons, inputs, cartes
lg16pxCartes, modales, sections
xl24pxGrands conteneurs, illustrations
pill9999pxEffet “pilule” (boutons ronds)

Exemples d’utilisation

// Bord arrondi moyen (par défaut)
.card {
  @include radius();
}

// Bord arrondi large
.modal {
  @include radius(lg);
}

// Effet pilule (pour un bouton rond)
.button-pill {
  @include radius(pill);
}

Flexbox

Les mixins flex et flex-center sont là pour simplifier ton code tout en gardant toute la puissance de flexbox.

La mixin flex-centerest un raccourci pour un élément horizontalement et verticalement. C’est l’une des opérations les plus courantes en CSS.

@mixin  flex-center {
  display: flex;
  flex-direction: column;
  justify-content: center;
  align-items: center;
  gap: $space-16;
}

Exemple d’utilisation

.content {
@include  flex-center;
height: 100vh; // Hauteur de l'écran complet
}

La mixin flex

C’est un mixin flexible qui permet de configurer n’importe quelle propriété flexbox. On peut spécifier une seule ou toutes les propriétés.

@mixin flex($dir: null, $wrap: null, $justify: null, $align: null, $gap: null) {
  display: flex;

  @if $dir != null {
    flex-direction: $dir;
  }
  @if $wrap != null {
    flex-wrap: $wrap;
  }
  @if $justify != null {
    justify-content: $justify;
  }
  @if $align != null {
    align-items: $align;
  }
  @if $gap != null {
    gap: size($gap);
  }
}

Les paramètres en détail

  • $dir : Direction des éléments

    • row (défaut) : éléments alignés horizontalement
    • column : éléments alignés verticalement
    • row-reverse, column-reverse : ordre inversé
  • $wrap : Comment les éléments se comportent quand il n’y a plus de place

    • nowrap (défaut) : reste sur une seule ligne, peut déborder
    • wrap : passe à la ligne suivante si besoin
    • wrap-reverse : passe à la ligne du bas vers le haut
  • $justify : Alignement horizontal (sur l’axe principal)

    • flex-start (défaut) : éléments au début
    • center : éléments au centre
    • flex-end : éléments à la fin
    • space-between : espacés avec les extrémités collées aux bords
    • space-around : espacés avec espace autour de chaque élément
    • space-evenly : espacés uniformément
  • $align : Alignement vertical (sur l’axe secondaire)

    • stretch (défaut) : étirés pour occuper tout l’espace
    • center : centrés
    • flex-start : en haut/au début
    • flex-end : en bas/à la fin
    • baseline : alignés sur la ligne de base du texte
  • $gap : Espace entre les éléments (utilise une clé du map spacing comme ‘space-8’, ou une valeur CSS)

    • Exemple: space-8 pour 8px d’espacement
@include flex($gap: 'space-8');    // 8px (en rem)
@include flex($gap: 1.25rem);  
  1. Une barre de navigation horizontale avec espace entre les éléments
.navbar {
  @include flex($justify: space-between, $align: center);
  padding: space-4 space-8;
}
  1. Une liste verticale d’éléments espacés
.menu-items {
  @include flex($dir: column, $gap: space-8);
}
  1. Une grille d’images qui se réorganise automatiquement
.image-gallery {
  @include flex($wrap: wrap, $gap: space-16, $justify: center);
}
  1. Un formulaire avec labels et champs alignés
.form-group {
  @include flex($dir: column, $gap: space-4);
  
  @include mq(tablet) {
    // Change en horizontal sur tablette et +
    @include flex($dir: row, $align: center, $gap: space-8);
  }
}

L’avantage du null

En utilisant null comme valeur par défaut, on peut spécifier uniquement les propriétés dont on a besoin. Les propriétés non spécifiées n’apparaîtront pas dans le CSS final, ce qui donne un code plus léger.

// Seulement direction et gap
.sidebar {
  @include flex($dir: column, $gap: space-16);
}

// Génère seulement :
.sidebar {
  display: flex;
  flex-direction: column;
  gap: 1rem;
}

Shadows

abstracts/_shadows.scss

le fichier gère le système des ombres

Il reprend les éléments du DS sous figma

image

@use 'sass:map';

$shadows: (
  light: (
    xs: (0px 1px 2px rgba(9, 9, 11, 0.05)),
    sm: (0px 1px 3px rgba(9, 9, 11, 0.10), 0px 1px 2px rgba(9, 9, 11, 0.10)),
    md: (0px 4px 6px rgba(9, 9, 11, 0.10), 0px 0px 4px rgba(9, 9, 11, 0.10)),
    lg: (0px 4px 6px rgba(9, 9, 11, 0.10), 0px 4px 4px rgba(9, 9, 11, 0.20)),
    xl: (0px 20px 25px rgba(9, 9, 11, 0.10), 0px 8px 10px rgba(9, 9, 11, 0.10)),
    2xl: (0px 25px 50px rgba(9, 9, 11, 0.20)),
    inner: (inset 0px 2px 4px 0px rgba(9, 9, 11, 0.05))
  ),
  dark: (
    xs: (0px 1px 2px rgba(255, 255, 255, 0.05)),
    sm: (0px 1px 3px rgba(255, 255, 255, 0.10), 0px 1px 2px rgba(255, 255, 255, 0.10)),
    md: (0px 4px 6px rgba(255, 255, 255, 0.10), 0px 0px 4px rgba(255, 255, 255, 0.10)),
    lg: (0px 4px 6px rgba(255, 255, 255, 0.10), 0px 4px 4px rgba(255, 255, 255, 0.20)),
    xl: (0px 20px 25px rgba(255, 255, 255, 0.10), 0px 8px 10px rgba(255, 255, 255, 0.10)),
    2xl: (0px 25px 50px rgba(255, 255, 255, 0.20)),
    inner: (inset 0px 2px 4px 0px rgba(255, 255, 255, 0.05))
  )
);

@mixin shadow($key, $theme: light) {
  $theme-map: map.get($shadows, $theme);
  $shadow: map.get($theme-map, $key);

  @if $shadow {
    box-shadow: $shadow;
  } @else {
    @warn "Shadow `#{$key}` not found for theme `#{$theme}`.";
  }
}

La map $shadows

C’est une structure de données organisée en trois niveaux:

  1. Premier niveau: Les thèmes (light et dark)
  2. Deuxième niveau: Les tailles d’ombre (xs, sm, md, lg, xl, 2xl, inner)
  3. Troisième niveau: Les valeurs d’ombres CSS (entre parenthèses)

Le mixin shadow

C’est une fonction réutilisable qui:

  1. Prend deux paramètres:
    • $key: La taille de l’ombre (xs, sm, md, etc.)
    • $theme: Le thème à utiliser (light ou dark, avec light par défaut)
  2. Récupère la bonne valeur d’ombre depuis la structure imbriquée
  3. L’applique comme box-shadow si elle existe
  4. Affiche un avertissement si l’ombre demandée n’existe pas

Exemple de base

.card {
  // Ombre légère (sm) en thème clair (par défaut)
  @include shadow(sm);
}

Exemple avec thème spécifique

.card-dark {
  // Ombre moyenne (md) en thème sombre
  @include shadow(md, dark);
}

Exemple avec adaptation au thème actif

// Pour un élément qui s'adapte au thème de l'application
.adaptive-card {
  // Style de base
  background-color: white;
  
  // En thème clair
  :host-context([data-theme='light']) & {
    @include shadow(md, light);
  }
  
  // En thème sombre
  :host-context([data-theme='dark']) & {
    background-color: #333;
    @include shadow(md, dark);
  }
}

Exemple: Un Bouton

exemple dans le bouton switch du theme avec les mixins theme, layout, typo et shadows

Pensez bien aux imports

@use  '../../../styles/themes/tokens'  as  theme;
@use  '../../../styles/abstracts/typography'  as  typo;
@use  '../../../styles/abstracts/layout'  as  layout;
@use  '../../../styles/abstracts/shadows'  as  shadows;

.btn-switch {

// Utilisation des variables de thème

  @include layout.flex($dir: row, $align: center, $justify: space-between, $gap : layout.$space-8);
  @include layout.padding(layout.$space-8, layout.$space-12);
  @include layout.margin(layout.$space-8, layout.$space-12);
  @include layout.radius(md);
  @include shadows.shadow(lg, dark);
  border: none;
  cursor: pointer;
  font-family: 'open-sans', sans-serif;

  @include typo.text-style(text-md, regular);

  //style identique pour les deux thèmes
  @include theme.themed-block(
    (
      background-color: 'background-primary-default',
      color: 'text-neutral-default-inverse',
    )
  );

  // hover séparé
  &:hover {
    @include theme.themed-block(
      (
        background-color: 'background-primary-hover',
      )
    );
  }
}

// 🎯 Exception uniquement pour le light
:host-context([data-theme='light']) .btn-switch {
  @include shadows.shadow(lg, light);
  &:hover {
    color: #{theme.themed('text-neutral-default', 'light')};
  }
}

Composants UI

Icons

Objectif

Le composant IconComponent est un composant Angular standalone, conçu pour afficher des icônes SVG issues de la librairie Lucide (stockées en local dans assets/icons/lucide/).
Il est utilisé dans le design system Pecunia, pour gérer toutes les icônes d’interface : boutons, menus, statuts, badges, etc.


Définition

export class IconComponent {
  private readonly http = inject(HttpClient);

  //déclaration des signaux => mini varialbles observables
  readonly _name = signal<string>('');
  readonly _size = signal<IconSize>('md');
  readonly _ariaLabel = signal<string>('');
  readonly _isDecorative = signal<boolean>(false);

  //setter pour mettre à jour les inputs avec des signaux
  @Input({ required: true }) set name(value: string) {
    this._name.set(value);
  }
  @Input() set size(value: IconSize) {
    this._size.set(value);
  }
  @Input() set ariaLabel(value: string) {
    this._ariaLabel.set(value);
  }
  @Input() set isDecorative(value: boolean) {
    this._isDecorative.set(value);
  }

Ce composant est totalement réactif (via signal() et computed()), encapsulé, accessible et personnalisable.


Propriétés disponibles (@Input())

PropTypeObligatoireDescription
namestring✅ OuiNom du fichier SVG (ex: "plus", "arrow-left")
size'xs' | 'sm' | 'md' | 'lg'❌ NonTaille logique, applique une classe CSS (icon-size-md par défaut)
ariaLabelstring❌ NonTexte alternatif (accessibilité), utilisé si isDecorative = false
isDecorativeboolean❌ NonSi true, l’icône est masquée des lecteurs d’écran

Cas d’usages typiques

  • Icône seule dans un bouton d’action (delete, edit…)
  • Icône dans une puce, un badge ou une ligne de tableau
  • Icône décorative dans une UI (à cacher aux lecteurs d’écran)

Icônes décoratives vs informatives

Une icône est décorative si :

  • Elle n’ajoute aucune information essentielle
  • Elle accompagne un texte déjà explicite
  • Elle est utilisée uniquement pour améliorer l’esthétique

Exemples décoratifs :

  • Un 🔒 à côté du mot « Connexion »
  • Une icône 🛒 dans un bouton « Ajouter au panier »
  • Un pictogramme 🎯 dans une carte qui a déjà un titre

➡️ Accessibilité :

  • Utiliser [isDecorative]="true" dans Pecunia (ce qui appliquera aria-hidden="true")
  • Pas besoin de ariaLabel pour les icônes décoratives

Une icône est informative si :

  • Elle remplace un texte
  • Elle transmet une information visuelle (état, action)
  • Elle est la seule information visible

Exemples informatifs :

  • Une 🗑️ seule dans un bouton ➜ signifie “Supprimer”
  • Une icône ❗ dans un message ➜ signifie “Erreur”
  • Une 👁️ dans un champ ➜ signifie “Afficher le mot de passe”

➡️ Accessibilité :

  • Option 1 : Fournir un ariaLabel="Description" directement sur l’icône
  • Option 2 : Mettre aria-label sur l’élément parent (si l’icône est dans un élément interactif)

📝 Règle simple à retenir

Si l’icône peut être retirée sans perte d’information, elle est décorative.
Sinon, elle est informative et doit être accessible aux lecteurs d’écran.


Exemples concrets

  • Icône significative – Option 1 (label sur l’icône)
<app-ui-icon name="trash" ariaLabel="Supprimer la transaction"></app-ui-icon>
  • Icône significative – Option 2 (label sur le parent)
<button aria-label="Supprimer la transaction">
  <app-ui-icon name="trash" [isDecorative]="true"></app-ui-icon>
</button>
  • Icône décorative
<button>
  <app-ui-icon name="trash" [isDecorative]="true"></app-ui-icon>
  Supprimer la transaction
</button>

Cas d’usage dans le IconComponent

Situationaria labeldecorative
Icône seule dans un bouton"Supprimer"false
Icône accompagnée d’un texte""true
Icône de statut (succès, erreur…)"Succès"false
Icône purement esthétique""true

Comment ça marche ?

Le composant :

  • Résout le chemin SVG via un computed() :
    assets/icons/lucide/${name}.svg
  • Applique une classe de taille (icon-size-md, icon-size-lg, etc.)
  • Réagit à une erreur de chargement avec un fallback (alert-circle.svg)

Bonnes pratiques d’intégration

  • Toujours utiliser ce composant plutôt que des balises <img> ou <svg> brutes
  • Ne pas hardcoder le chemin de l’icône dans les composants parents
  • Préférer les tailles logiques (sm, md, etc.) au lieu de fixer les pixels
  • Respecter la séparation : le style (.scss) gère la taille réelle

Gestion couleur de l’icon

L’icône est affichée via un <span> contenant une mask-image SVG.
La couleur est appliquée via background-color: currentColor en CSS.

Pourquoi utiliser mask-image au lieu de img?

L’utilisation de mask-image offre plusieurs avantages importants :

  • Héritage de couleur : Une icône avec mask-image peut hériter de la couleur du texte parent via currentColor, ce qui est difficile avec des images SVG classiques.
  • Performance : Les masks CSS sont plus performants que les SVG injectés dans le DOM pour de nombreuses icônes.
  • Flexibilité : On peut changer la couleur dynamiquement sans modifier le fichier SVG source

Important : coloration pour les icônes sans texte

Pour les boutons ou éléments ne contenant que des icônes, il est impératif de définir la propriété CSS color dans le parent :

<!-- Composant bouton icône sans texte -->
<button class="icon-only-button">
  <app-icon name="trash" ariaLabel="Supprimer" />
</button>
// SCSS du composant parent
.icon-only-button {
  // IMPORTANT : définir la couleur même sans texte !
  @include theme.themed-block(
    (
      color: 'text-neutral-default',
      // Couleur pour l'icône
    )
  );
}

Pour le dossier CDA

Le composant IconComponent respecte les principes du design system :

  • réutilisable, autonome, testable
  • conforme aux bonnes pratiques d’accessibilité
  • basé sur la nouvelle API Angular signal() / computed() pour plus de lisibilité

Il est centralisé dans shared/ et documenté pour permettre son adoption par l’ensemble de l’équipe.

🎨 Listes des Icons

Ce tableau présente les icônes disponibles dans notre application, basées sur la librairie Lucide.

Nom logique (Angular)Aperçu
albumalbum
arrow-leftarrow-left
arrow-rightarrow-right
babybaby
badge-dollar-signbadge-dollar-sign
badge-eurobadge-euro
banknotebanknote
boxbox
briefcase-businessbriefcase-business
busbus
bus-frontbus-front
cakecake
calendar-dayscalendar-days
carcar
carrotcarrot
chart-column-increasingchart-column-increasing
checkcheck
chevron-downchevron-down
chevron-leftchevron-left
chevron-rightchevron-right
chevron-upchevron-up
circle-question-markcircle-question-mark
clipboard-listclipboard-list
dramadrama
dribbbledribble
drilldrill
dumbbelldumbbell
factoryfactory
file-textfile-text
fishfish
flower-2flower-2
fuelfuel
gamepad-2gamepad-2
gemgem
giftgift
guitarguitar
hamham
hamburgerhamburger
hammerhammer
hospitalhospital
househouse
landmarklandmark
layout-gridlayout-grid
locklock
luggageluggage
mailmail
martinimartini
musicmusic
palettepalette
party-popperparty-popper
phonephone
pillpill
pizzapizza
planeplane
plusplus
popcornpopcorn
searchsearch
settingssettings
shield-banshield-ban
shipship
shirtshirt
shopping-bagshopping-bag
shopping-cartshopping-cart
storestore
sunsun
sun-moonsun-moon
tagtag
traffic-conetraffic-cone
trash-2trash-2
useruser
users-roundusers-round
utensils-crossedutensils-crossed
volleyballvolleyball
walletwallet
wand-sparkleswand-sparkles
waveswaves
wifiwifi
zoom-inzoom-in
zoom-outzoom-out

Accessibilité

Composant IconComponent – Documentation d’usage

Objectif

Le composant IconComponent est un composant Angular standalone, conçu pour afficher des icônes SVG issues de la librairie Lucide (stockées en local dans assets/icons/lucide/).
Il est utilisé dans le design system Pecunia, pour gérer toutes les icônes d’interface : boutons, menus, statuts, badges, etc.


Définition

export class IconComponent {
  private readonly http = inject(HttpClient);

  //déclaration des signaux => mini varialbles observables
  readonly _name = signal<string>('');
  readonly _size = signal<IconSize>('md');
  readonly _ariaLabel = signal<string>('');
  readonly _isDecorative = signal<boolean>(false);

  //setter pour mettre à jour les inputs avec des signaux
  @Input({ required: true }) set name(value: string) {
    this._name.set(value);
  }
  @Input() set size(value: IconSize) {
    this._size.set(value);
  }
  @Input() set ariaLabel(value: string) {
    this._ariaLabel.set(value);
  }
  @Input() set isDecorative(value: boolean) {
    this._isDecorative.set(value);
  }

Ce composant est totalement réactif (via signal() et computed()), encapsulé, accessible et personnalisable.


Propriétés disponibles (@Input())

PropTypeObligatoireDescription
namestring✅ OuiNom du fichier SVG (ex: "plus", "arrow-left")
size'xs' | 'sm' | 'md' | 'lg'❌ NonTaille logique, applique une classe CSS (icon-size-md par défaut)
ariaLabelstring❌ NonTexte alternatif (accessibilité), utilisé si isDecorative = false
isDecorativeboolean❌ NonSi true, l’icône est masquée des lecteurs d’écran

Cas d’usages typiques

  • Icône seule dans un bouton d’action (delete, edit…)
  • Icône dans une puce, un badge ou une ligne de tableau
  • Icône décorative dans une UI (à cacher aux lecteurs d’écran)

Icônes décoratives vs informatives

Une icône est décorative si :

  • Elle n’ajoute aucune information essentielle
  • Elle accompagne un texte déjà explicite
  • Elle est utilisée uniquement pour améliorer l’esthétique

Exemples décoratifs :

  • Un 🔒 à côté du mot « Connexion »
  • Une icône 🛒 dans un bouton « Ajouter au panier »
  • Un pictogramme 🎯 dans une carte qui a déjà un titre

➡️ Accessibilité :

  • Utiliser [isDecorative]="true" dans Pecunia (ce qui appliquera aria-hidden="true")
  • Pas besoin de ariaLabel pour les icônes décoratives

Une icône est informative si :

  • Elle remplace un texte
  • Elle transmet une information visuelle (état, action)
  • Elle est la seule information visible

Exemples informatifs :

  • Une 🗑️ seule dans un bouton ➜ signifie “Supprimer”
  • Une icône ❗ dans un message ➜ signifie “Erreur”
  • Une 👁️ dans un champ ➜ signifie “Afficher le mot de passe”

➡️ Accessibilité :

  • Option 1 : Fournir un ariaLabel="Description" directement sur l’icône
  • Option 2 : Mettre aria-label sur l’élément parent (si l’icône est dans un élément interactif)

📝 Règle simple à retenir

Si l’icône peut être retirée sans perte d’information, elle est décorative.
Sinon, elle est informative et doit être accessible aux lecteurs d’écran.


Exemples concrets

  • Icône significative – Option 1 (label sur l’icône)
<app-ui-icon name="trash" ariaLabel="Supprimer la transaction"></app-ui-icon>
  • Icône significative – Option 2 (label sur le parent)
<button aria-label="Supprimer la transaction">
  <app-ui-icon name="trash" [isDecorative]="true"></app-ui-icon>
</button>
  • Icône décorative
<button>
  <app-ui-icon name="trash" [isDecorative]="true"></app-ui-icon>
  Supprimer la transaction
</button>

Cas d’usage dans le IconComponent

Situationaria labeldecorative
Icône seule dans un bouton"Supprimer"false
Icône accompagnée d’un texte""true
Icône de statut (succès, erreur…)"Succès"false
Icône purement esthétique""true

Comment ça marche ?

Le composant :

  • Résout le chemin SVG via un computed() :
    assets/icons/lucide/${name}.svg
  • Applique une classe de taille (icon-size-md, icon-size-lg, etc.)
  • Réagit à une erreur de chargement avec un fallback (alert-circle.svg)

Bonnes pratiques d’intégration

  • Toujours utiliser ce composant plutôt que des balises <img> ou <svg> brutes
  • Ne pas hardcoder le chemin de l’icône dans les composants parents
  • Préférer les tailles logiques (sm, md, etc.) au lieu de fixer les pixels
  • Respecter la séparation : le style (.scss) gère la taille réelle

Gestion couleur de l’icon

L’icône est affichée via un <span> contenant une mask-image SVG.
La couleur est appliquée via background-color: currentColor en CSS.

Pourquoi utiliser mask-image au lieu de img?

L’utilisation de mask-image offre plusieurs avantages importants :

  • Héritage de couleur : Une icône avec mask-image peut hériter de la couleur du texte parent via currentColor, ce qui est difficile avec des images SVG classiques.
  • Performance : Les masks CSS sont plus performants que les SVG injectés dans le DOM pour de nombreuses icônes.
  • Flexibilité : On peut changer la couleur dynamiquement sans modifier le fichier SVG source

Important : coloration pour les icônes sans texte

Pour les boutons ou éléments ne contenant que des icônes, il est impératif de définir la propriété CSS color dans le parent :

<!-- Composant bouton icône sans texte -->
<button class="icon-only-button">
  <app-icon name="trash" ariaLabel="Supprimer" />
</button>
// SCSS du composant parent
.icon-only-button {
  // IMPORTANT : définir la couleur même sans texte !
  @include theme.themed-block(
    (
      color: 'text-neutral-default',
      // Couleur pour l'icône
    )
  );
}

Pour le dossier CDA

Le composant IconComponent respecte les principes du design system :

  • réutilisable, autonome, testable
  • conforme aux bonnes pratiques d’accessibilité
  • basé sur la nouvelle API Angular signal() / computed() pour plus de lisibilité

Il est centralisé dans shared/ et documenté pour permettre son adoption par l’ensemble de l’équipe.

Composant <app-ui-button>

Description

Le composant <app-ui-button> centralise tous les usages de bouton de l’application :

  • Actions principales ou secondaires
  • Boutons de formulaire
  • Boutons “icon only” (accessibles)
  • Variantes, tailles, arrondis, largeur

Il applique automatiquement les styles du Design System (tokens, radius, typographie, responsive…) et garantit l’accessibilité.


Propriétés (props)

PropTypeValeurs possiblesDefaultDescription
variantstring (VariantType)primary, secondary, alert, success, ghostprimaryStyle visuel du bouton (couleur, usage)
typestring (ButtonType)button, submit, resetbuttonType natif HTML du bouton
sizestring (ButtonSize)medium, largemediumTaille du bouton (padding et font-size adaptés)
radiusstring (ButtonRadius)medium, pillmediumRayon de bordure (arrondi standard ou pill = très arrondi)
widthstring (ButtonWidth)auto, fullautoLargeur auto (adaptée au contenu) ou block (100%)
minWidthnumber | string | nullex : 160, '10rem', '60%', nullnullLargeur minimale du bouton (en px si number, ou unité CSS)
maxWidthnumber | string | nullex : 320, '20rem', '100%', nullnullLargeur maximale du bouton (en px si number, ou unité CSS)
disabledbooleantrue, falsefalseDésactive le bouton
ariaLabelstring– (libre, recommandé pour bouton icône seule)Label accessibilité, obligatoire pour “icon only”
buttonClickEventEmitter<Event> (output)Événement émis lors du clic

Détail des types :

variant

  • primary : Bouton principal (bleu/DS)
  • secondary : Bouton secondaire (neutre)
  • alert : Bouton d’alerte (rouge/orange)
  • success : Bouton de validation (vert)
  • ghost : Fond transparent, contour coloré (pour actions secondaires)

type

  • button : Bouton simple (défaut)
  • submit : Pour validation de formulaire
  • reset : Réinitialisation de formulaire

size

  • medium : Par défaut (hauteur et padding standard)
  • large : Plus grand, texte plus gros

radius

  • medium : Arrondi standard (4px, 8px selon tokens)
  • pill : Arrondi “capsule” (max, pour bouton très arrondi)

width

  • auto : Largeur s’adapte au contenu (défaut)
  • full : Largeur 100% du parent (responsive)

min-width

  • minWidth : valeur numérique (ex: 160 pour 160px) ou string avec unité CSS ('10rem', '60%').
    Si non renseigné (null), aucune min-width n’est appliquée.

max-width

  • maxWidth : valeur numérique (ex: 320 pour 320px) ou string avec unité CSS ('100%', '20rem').
    Si non renseigné (null), aucune max-width n’est appliquée.

Exemples d’utilisation

Bouton primaire (par défaut)

<app-ui-button (buttonClick)="onValider()">
  Valider
</app-ui-button>

Bouton secondaire, large, arrondi “pill”

<app-ui-button
  variant="secondary"
  size="large"
  radius="pill"
  (buttonClick)="onRetour()"
>
  Retour
</app-ui-button>

Bouton “alert” désactivé

<app-ui-button
  variant="alert"
  [disabled]="true"
>
  Supprimer
</app-ui-button>

Bouton “icon only” (accessibilité obligatoire)

<app-ui-button
  variant="custom-icon"
  ariaLabel="Fermer la fenêtre"
  (buttonClick)="onClose()"
>
  <app-ui-icon name="x" size="md" [isDecorative]="false" />
</app-ui-button>

Bouton ghost, block (100% largeur)

<app-ui-button
  variant="ghost"
  width="block"
  (buttonClick)="onAction()"
>
  Action secondaire
</app-ui-button>

Bouton avec min/max en pixels

<app-ui-button [minWidth]="160" [maxWidth]="320">
  Confirmer
</app-ui-button>

Bouton avec min/max responsive (en rem et %)

<app-ui-button minWidth="10rem" maxWidth="80%">
  S'inscrire
</app-ui-button>

Bouton full width dans un parent, avec min/max personnalisés

<div style="width:400px;">
  <app-ui-button width="full" minWidth="200" maxWidth="360">
    Continuer
  </app-ui-button>
</div>

Accessibilité : bonnes pratiques

  • Pas besoin d’ariaLabel si le bouton a un texte visible.
  • Obligatoire de fournir ariaLabel si bouton “icon only” (ou texte ambigu)
  • Utilise le slot <ng-content> pour injecter texte, icônes, loader, etc.
  • Ne jamais mettre de <button> à l’intérieur d’un <app-ui-button>.

Composant <app-ui-input>

Description

Le composant <app-ui-input> centralise tous les usages de champ texte du Design System :

  • Champ texte, email, mot de passe, number, tel, url
  • Prise en charge des labels, placeholders, état error/success, helper
  • Largeurs personnalisables (full ou auto, min/max)
  • Slots pour icônes à gauche/droite
  • Compatible accessibilité (label, id, name, etc.)

Typages

export type inputType =
  | 'text'
  | 'email'
  | 'password'
  | 'number'
  | 'tel'
  | 'url';

export type inputStatus = 'error' | 'success' | null;

export type inputWidth = 'full' | 'auto';

export type inputMinWidth = number | string | null;

export type inputMaxWidth = number | string | null;

Propriétés (@Input)

PropTypeValeurs possiblesDefaultDescription
typeinputType'text', 'email', 'password', 'number', 'tel', 'url''text'Type natif HTML de l’input
labelstring''Label affiché au-dessus de l’input
placeholderstring''Placeholder natif
valuestring''Valeur contrôlée du champ
helperstring''Message d’aide ou d’erreur sous le champ
statusinputStatus'error', 'success', nullnullÉtat visuel du champ
widthinputWidth'auto', 'full''auto'Largeur du champ
minWidthinputMinWidthnumber, string, nullnullLargeur minimale (ex: 200, '10rem')
maxWidthinputMaxWidthnumber, string, nullnullLargeur maximale (ex: 392, '100%')
requiredbooleantrue, falsefalseChamp obligatoire
disabledbooleantrue, falsefalseDésactive le champ
namestringnullAttribut name natif
idstringnullAttribut id natif
ariaLabelstring | nullnullLabel accessibilité alternatif (si pas de label)
ariaDescribedBystring | nullnullId de description pour l’accessibilité
overideClassstring''Classe(s) additionnelles

Événements (@Output)

EventTypeDescription
InputValueChangeEventEmitter<string>Émis à chaque modification de valeur
inputBlurEventEmitter<Event>Émis à la perte de focus
inputFocusEventEmitter<Event>Émis à la prise de focus

Slots (ng-content)

  • Icône à gauche : <ng-content select="[icon-left]"></ng-content>
  • Icône à droite : <ng-content select="[icon-right]"></ng-content>

Exemples d’utilisation

Champ email avec icône

<app-ui-input
  label="Email"
  placeholder="ex: xxx@xxx.com"
  width="full"
  [minWidth]="200"
  [maxWidth]="392"
  [required]="true"
  type="email"
  id="email-input"
  name="email"
>
  <app-ui-icon name="mail" icon-left />
</app-ui-input>

Champ mot de passe avec icône à gauche

<app-ui-input
  label="Mot de passe"
  placeholder="password"
  width="full"
  [minWidth]="200"
  [maxWidth]="392"
  [required]="true"
  type="password"
  id="password-input"
  name="password"
>
  <app-ui-icon name="lock" icon-left />
</app-ui-input>

Champ en erreur (avec helper)

<app-ui-input
  label="Prénom"
  placeholder="Prénom"
  width="full"
  [minWidth]="200"
  [maxWidth]="392"
  [required]="true"
  type="text"
  id="firstname-input"
  name="firstname"
  status="error"
  helper="3 caractères minimum, 1 majuscule, 1 minuscule, pas de chiffres ni de caractères spéciaux"
>
  <app-ui-icon name="user" icon-left />
</app-ui-input>

Champ success + icône à droite

<app-ui-input
  label="Nom de famille"
  placeholder="Nom de famille"
  width="full"
  [minWidth]="200"
  [maxWidth]="392"
  [required]="true"
  type="text"
  id="lastname-input"
  name="lastname"
  [status]="statusLastName"
>
  <app-ui-icon name="user" icon-left />
  @if (statusLastName === 'success') {
    <app-ui-icon name="check" icon-right />
  }
</app-ui-input>

Bonnes pratiques

  • Toujours renseigner label OU ariaLabel (jamais de champ sans label accessible)
  • Le parent gère l’état du champ (status) selon la validation métier
  • Le composant est réactif et flexible (émets les events, slots pour icônes)
  • Pour l’accessibilité : on peut enrichir avec des props ARIA à mesure des besoins

Compatibilité Angular Forms : pourquoi et comment ControlValueAccessor

Pourquoi ?

Pour que <app-ui-input> soit utilisable avec Angular Reactive Forms (formControlName, etc.)
— comme un <input> natif — il doit implémenter l’interface ControlValueAccessor.

Cela permet :

  • de lier le composant à un FormControl ou FormGroup
  • de synchroniser automatiquement la value, l’état disabled, les changements (input), etc.
  • de faire en sorte qu’Angular puisse contrôler la value, l’état touched, etc.

Comment ?

  1. on déclare que le composant implémente ControlValueAccessor
    et on ajoute le provider NG_VALUE_ACCESSOR dans le décorateur :
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
import { forwardRef } from '@angular/core';

@Component({
  // ...
  providers: [{
    provide: NG_VALUE_ACCESSOR,
    useExisting: forwardRef(() => InputComponent),
    multi: true,
  }]
})
export class InputComponent implements ControlValueAccessor {
  // ...
}
  • forwardRef permet à Angular d’utiliser la référence du composant même avant sa déclaration complète.
  • multi: true permet à plusieurs valeurs d’être ajoutées à la même injection.

  1. Implémenter les méthodes de l’interface dans le composant :
// Pour transmettre les changements à Angular Forms
private onChange: (value: string | number) => void = () => { /* intentionally empty */ };

// Pour signaler à Angular que le champ a été “touché”
private onTouched: () => void = () => { /* intentionally empty */ };

// Synchronise la value externe (FormControl → composant)
writeValue(value: string | number): void {
  this._value.set(value ?? '');
}

// Enregistre le callback à appeler quand la value change (composant → FormControl)
registerOnChange(fn: (value: string | number) => void): void {
  this.onChange = fn;
}

// Enregistre le callback à appeler quand le champ est touché (blur, etc.)
registerOnTouched(fn: () => void): void {
  this.onTouched = fn;
}

// Gère l’état disabled transmis par Angular
setDisabledState(isDisabled: boolean): void {
  this._disabled.set(isDisabled);
}
  • writeValue : appelée quand la value du FormControl change (ex : reset, patchValue…)
  • registerOnChange : Angular injecte ici la fonction à appeler sur changement (input)
  • registerOnTouched : Angular injecte ici la fonction à appeler quand l’input est “touché” (focus perdu)
  • setDisabledState : Angular te notifie si le champ doit être désactivé

Grâce à cette interface, le composant est 100% compatible Reactive Forms,
et se comporte comme un <input> natif dans les formulaires.

Composant <app-ui-link>

Description

Le composant <app-ui-link> centralise tous les usages de liens internes du Design System :

  • Lien de navigation SPA avec Angular [routerLink]
  • Variantes visuelles : couleur
  • Accessibilité, focus, désactivation

Propriétés (props)

PropTypeValeurs possiblesDefaultDescription
routerLinkstring | string[] | nullex : '/register', ['/register']– (requis)Chemin de navigation SPA interne
variantstring (LinkVariant)primary, secondary, alert, neutralprimaryStyle visuel du lien (couleur DS)
widthstring (LinkWidth)auto, fullautoLargeur auto ou 100% parent
minWidthnumber | string | nullex : 160, '10rem', '60%', nullnullLargeur minimale du lien (en px ou unité CSS)
maxWidthnumber | string | nullex : 320, '20rem', '100%', nullnullLargeur maximale du lien (en px ou unité CSS)
disabledbooleantrue, falsefalseDésactive la navigation et l’accessibilité
ariaLabelstring | nullnullLabel accessibilité alternatif (si pas de texte)
tabIndexnumber0, -10Ordre focus clavier (désactivé : -1)

Détail des types :

variant

  • primary : Lien principal (couleur DS)
  • secondary : Lien secondaire (gris/neutre)
  • alert : Lien “alerte” (rouge/orange)
  • neutral : Couleur texte neutre

width

  • auto : Largeur adaptée au contenu
  • full : Largeur 100% du parent (block, centré)

minWidth / maxWidth

  • Valeur numérique (px), string avec unité ('10rem', '100%'), ou null (pas de contrainte)

Exemples d’utilisation

Lien principal vers une page de création de compte

<app-ui-link [routerLink]="['/register']" variant="primary">
  Créer un compte
</app-ui-link>

Lien secondaire “mot de passe oublié”, avec accessibilité

<app-ui-link
  [routerLink]="['/forgot-password']"
  variant="secondary"
  ariaLabel="Réinitialiser le mot de passe"
>
  Mot de passe oublié ?
</app-ui-link>

Introduction

Le but de cette documentation est de présenter étape par étape comment mettre en place le déploiement en production de notre application.

Definition

Ce sont des pratiques DevOps, qui désigne l’intégration et la distribution ou le déploiement continu.

Cela a pour objectif de rationaliser et d’accélérer le cycle de vie de développement des logiciels.

CI/CD schema

Ce processus ont pour but de faire gagner du temps aux développeurs dans leur travail en automatisant un maximum de processus, tout en en garantissant une meilleure robustesse et maintenabilité du code.

Continuous Integration

L’intégration Continu (CI) correspond à la mise en place de tests automatiques avant de pouvoir intégrer de nouvels modifications au code source en ligne.

Ce processus d’automatisation permet de détecter rapidement les problèmes d’intégrations et les régressions.

Les Github Actions permettent une homogénéité de notre codebase.

Que ce soit en front ou dans le back, nous utilisons des workflows afin de :

  • Lancer nos tests automatisés (Unitaires > Intégrations > End-to-End)
  • De respecter les conventions de qualité de code à l’aide des linters (Eslint pour Angular - Checkstyle pour Spring)
  • De pouvoir vérifier si le projet compile correctement

Cela permet de garantir la fiabilité des modifications du code fusionné tout le long de la vide d’un projet.

Moins de bugs et des corrections plus rapides en production.

Continuous Delivery

CD est l’acronyme de “Continuous Delivery” ou le déploiement continu.

C’est une pratique DevOps consitant à tranférer automatiquement les modifications depuis le référentiel vers l’environnement de production, où elles peuvent être utilisées par les utilisateurs.

Par rapport à la facon traditionnel de la mise en production, cela permet réduire la surcharge des équipes d’administration systèmes qui mettent en production de facon manuel et qui ralentissaient la distribution des applications.

Cela permet d’offrir une version à jour de facon régulière du projet en ligne.

La pipeline CI

Pour mettre en place une CI côté backend ou frontend, on commence par créer un ficher de workflows qui seront automatiquement reconnu par Github.

fichier workflows github

On va y définir un workflow chargé de lancer tous les tests du backend ou du frontend.

Frontend

C’est un simple fichier qui réunit toutes les étapes :

name: CI - Lint // Format // Test

on:
  pull_request:
    branches: [dev, main]
  push:
    branches: [dev, main]

jobs:
  build-and-test:
    runs-on: ubuntu-latest

    steps:
      - name: Checkout Code
        uses: actions/Checkout@v4

      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: latest

      - name: Install dependencies
        run: npm ci

      - name: Run unit test
        run: npm run test:ci

      - name: Format with prettier
        run: npm run format

      - name: Lint with eslint
        run: npm run lint

      - name: Build application
        run: npm run build

Cela verifie bien que notre application respecte :

  • les conventions de code
  • les tests automatisées passent au vert
  • la compilation est ok

Backend

Au niveau de notre api spring, nous avons séparés les actions afin de ne pas surcharger un fichier et d’améliorer la lisibilité.

Linting

Avec Checkstyle, nous vérifions d’abord avec la commande mvn checkstyle:check si il y a des violations de notre convention de code, qui suit celle mis en place par Google.

Ensuite, nous créons un reporting via mvn checkstyle:checkstyle.

name: CI - Lint with Checkstyle

on:
  pull_request:
    branches: [dev, main]
  push:
    branches: [dev, main]
jobs:
  checkstyle:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout Code
        uses: actions/checkout@v4
      - name: Set up JDK 21
        uses: actions/setup-java@v4
        with:
          java-version: "21"
          distribution: "temurin"
          cache: maven
      - name: Run Checkstyle
        run: mvn checkstyle:check
      - name: Generate report
        run: mvn checkstyle:checkstyle
      - name: Upload Checkstyle report
        uses: actions/upload-artifact@v4
        with:
          name: checkstyle-report
          path: target/checkstyle/*
          retention-days: 15

Formatting

On utilise ici un formatter en adéquation avec la convention de Google avec ‘google-java-format’.

name: CI - Format with Google Java Format
permissions:
  contents: write

on:
  pull_request:
    branches: [dev, main]
  push:
    branches: [dev, main]

jobs:
  formatting:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          token: ${{ secrets.GITHUB_TOKEN }}
          # Pour les push events, utiliser la branche actuelle
          ref: ${{ github.event_name == 'push' && github.ref || github.head_ref }}
      - uses: axel-op/googlejavaformat-action@v4
        with:
          args: "--replace"
          commit-message: "style(ci): format"
          github-token: ${{ secrets.GITHUB_TOKEN }}```

Compile and Test

On va voir si l’application compile et en même temps maven nous permet de lancer nos tests automatisées.

Si de ces deux outils est rouge, ca ne passe pas. La compilation doit passer ET les tests doit passer au vert.

permissions:
  contents: read

name: CI - Build and Test
on:
  pull_request:
    branches: [dev, main]
  push:
    branches: [dev, main]

jobs:
  build-and-test:
    name: Build and Test Application
    runs-on: ubuntu-latest
    steps:
      - name: Checkout Code
        uses: actions/checkout@v4

      - name: Set Up JDK 21
        uses: actions/setup-java@v4
        with:
          java-version: "21"
          distribution: "temurin"
          cache: maven
      - name: Build and Run Tests
        env:
          SPRING_PROFILES_ACTIVE: test
        run: ./mvnw clean test --batch-mode --no-transfer-progress

La pipeline CD

Pour savoir comment a été gérer le serveur dédié (vps ou serveur maison) pour ce projet, vous pouvez aller sur cette page.

Pour le projet, nous utilisons la containerisation avec Docker et afin d’orchestrer nos containeurs, nous utilisons docker compose.

Pourquoi containeriser nos applications ?

  • Environnement isolé et consistant
  • Déploiement d’application rapide (bien avec le workflow CI/CD)
  • Flexible et Scalable
  • La transférabilité d’un serveur à un autre
  • Cost Effective
  • Permet de rollback facilement avec le Version Control System (VCS) interne des containers

Mise en place sur le serveur - CD

Une fois la CI passée, les modifications de code sont ajoutés au dépôt. On veut donc récupérer ce code automatiquement sur notre serveur et re-déployer notre application.

Pour cela, on va utiliser un nouveau workflow qui sera chargé de pousser nos images Docker avec les modifications de code à jour sur DockerHub.

Avec un “container registry” comme DockerHub, on signalera l’arrivé de nouveau code au serveur sur lequel est déployé notre application pour le récupérer.

Configuration du serveur

Il y a plusieurs configurations à faire sur le serveur.

UFW

On commencer par installer ufw qui est un firewall qui permet d’autoriser ou de bloquer certains ports et protocoles.

Ici, on va vouloir autoriser les ports concernant :

  • 22/tcp Ipv4 et Ipv6, utiliser pour la connection en SSH
  • 443 Ipv4 et Ipv6, utiliser pour la connection https

source: How to config ufw

Fail2ban

Fail2ban est service qui permet de bloquer les multiples connexions parasites. Typiquement, un nombre élevé et répété de tentatives infructueuses de connexion provenant d’une même machine.

Lorsqu’une correspondance à un filtre est détecté, l’adresse IP sera banni.“

source : How to config fail2ban

Workflow de mise en ligne

A nos workflows précédents, on va créer un nouveau workflow sous certaines conditions.

on:
  workflow_run:
    workflows: ["CI - Build and Test"]
    types:
      - completed

jobs:
  deploy:
    if: |
      github.event.workflow_run.conclusion == 'success' &&
      github.event.workflow_run.head_branch == 'main'

D’après les conditions définies dans la section on de ce fichier, on voit que le workflow chargé de lancer le déploiement se déclenche **quand le workflow “CI- Build and Test” est terminé. De plus, dans la section jobs on va rajouter deux conditions afin de d’être plus granulaire dans notre workflow. On va vouloir que notre workflow de la CI soit au vert et qu’il se soit passé sur main ou dev (suivant l’environnement où vous êtes)

Les Images sur DockerHub

A notre fichier précédent, on va rajouter une étape pour créer une image Docker et la pousser en ligne sur DockerHub.

      - name: Log in Docker Hub
        uses: docker/login-action@v3
        with:
          username: ${{ secrets.DOCKERHUB_USERNAME }}
          password: ${{ secrets.DOCKERHUB_PASSWORD }}
      - name: Build and Push Docker Image
        run: |
          docker build -t txrigxn/pecunia-api:latest . || exit 1
          docker push txrigxn/pecunia-api:latest

De facon synthetique, cette étape permet de créer une image docker du code récupérer sur le dépôt, de se connecter à DockerHub et de pousser le tout en ligne.

Avec cette étape, on remarque que les images sont poussées vers notre profil DockerHub:

Dockerhub

Se connecter au Serveur

On va rajouter une étape à notre fichier afin de se connecter en Ssh à notre serveur distant pour exécuter un script de déploiement.

      - name: Deploy to Server
        env:
          SSHKEY: ${{ secrets.SSHKEY }}
          VPS_USER: ${{ secrets.VPS_USER }}
          VPS_HOST: ${{ secrets.VPS_HOST }}
          SSH_KNOWN_HOSTS: ${{ secrets.SSH_KNOWN_HOSTS }}
        run: |
          # SSH KEY CONFIG
          mkdir -p ~/.ssh
          echo "$SSHKEY" > ~/.ssh/id_ed25519
          chmod 600 ~/.ssh/id_ed25519
          echo "$SSH_KNOWN_HOSTS" > ~/.ssh/known_hosts
          ssh -i ~/.ssh/id_ed25519 ${VPS_USER}@${VPS_HOST} "/home/pecunia/pecunia/deploy.sh"

Cette étape permet :

  • de configurer temporairement la clée SSH et les hôtes connus,
  • de se connecter automatiquement au serveur via SSH
  • et d’exécuter un script deploy.sh situé sur le serveur pour lancer pour le déploiement de l’application

Grâce à cette automatisation, le déploiement se fait directement après la construction ou la mise à jour de l’image, sans intervention manuelle.

Docker Compose sur le serveur

Après avoir configurer notre serveur, on doit configurer les éléments restants pour le déploiement de notre application en couche.

Comme nous utilisons Docker pour la containeurisation de nos applications pour le déploiement, nous avons besoin d’un orchestrateur de containeur. Nous avons choisis d’utiliser docker compose par sa simplicité d’utilisation.

infra

Le docker compose de deploiement

Afin d’orchestrer nos containers, nous avons plusieurs services dans notre fichier.

Tout d’abord, nous avons choisi une image d’un reverse proxy comme Traefik, qui nous permet à la fois de rediriger correctement les requetes entre nos services et aussi d’avoir des certificats SSL automatiquement et gratuitement.

services:
  traefik:
    image: traefik:v3.4
    container_name: traefik
    restart: unless-stopped
    command:
      - "--api.insecure=true"
      - "--providers.docker.true"
      - "--providers.docker.exposedbyDefault=false"
      - "--entryPoints.web.address=:80"
      - "--entryPoints.websecure.address=:443"
      - "--certificatesresolvers.myresolver.acme.httpchallenge=true"
      - "--certificatesresolvers.myresolver.acme.httpchallenge.entrypoint=web"
      - "--certificatesresolvers.myresolver.acme.email=mysubscriptions@tuta.com"
      "--certificatesresolvers.myresolver.acme.storage=/letsencrypt/acme.json"
    ports:
      - "80:80"
      - "443:443"
      - "8080:8080"
    volumes:
      - "./letsencrypt:/letsencrypt
      - "/var/run/docker.sock:/var/run/docker.sock:ro"

De plus la configuration est simple. On configure Traefik directement dans le fichier afin de rediriger les requêtes http et https vers notre nom de domaine que nous verrons dans nos services de l’application

 frontend:
  image: txrigxn/pecunia-front:latest
  container_name: pecunia-front
  env_file: ".env"
  labels:
    - "traefik.enable=true"
    - "traefik.http.routers.frontend.rule=Host(`pecuniapp.com`)"
    - "traefik.http.routers.frontend.entrypoints=websecure"
    - "traefik.http.routers.frontend.tls.certresolver=myresolver"

Le reverse proxy est configuré pour rediriger les requêtes venu de pecuniapp.com vers notre application frontend, qui ensuite lui appelera notre api.

 backend:
  image: txrigxn/pecunia-api:latest
  container_name: "pecunia-api"
  ports:
    - ${SPRING_LOCAL_PORT}:${SPRING_DOCKER_PORT}
  env_file: ".env"
  volumes:
    - ./src/:/app/src
  depends_on:
    db:
      condition: service_healthy
  labels:
  - "traefik.enable=true"
  - "traefik.http.routers.backend.rule=Host(`api.pecuniapp.com`)"
  - "traefik.http.routers.backend.entrypoints=websecure"
  - "traefik.http.routers.backend.tls.certificatesresolvers=myresolver"

Après notre service front, le backend est mis en place aussi avec l’utilisation du protocole https avec le sous-domaine api.pecuniapp.com mais cette api est privé.

Enfin, comme le service backend dépend de la bonne condition de notre base de donnée avec la ligne depends_on, on ajouter un nouveau service :

  db:
    image: mysql:8
    restart: always
    ports:
      - ${DB_LOCAL_PORT}:${DB_DOCKER_PORT}
    env_file: ".env"
    healthcheck:
      test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
      interval: 10s
      timeout: 5s
      retries: 20
    volumes:
    - db_data:/var/lib/mysql

volumes:
  db_data:

Un volume est créé pour l’image mysql officiel pour que les données enregistrer via l’api soit persistant pour les utilisateurs.

Script de déploiement

Ce script est déclencher par le GithubAction de déploiement. Voici le script en question, qui est chargé de d’éteindre les containers du projet, de récupérer les images à jour qui vient d’arriver sur DockerHub, et de redéployer le projet avec cette nouvelle image:

#!/bin/bash

set -e

echo "→ Changing to project directory..."
cd /home/pecunia/pecunia

echo "→ Pulling latest Docker image..."
docker compose pull backend frontend

echo "→ Stopping running containers..."
docker compose down

echo "→ Starting new containers..."
docker compose up -d

echo "✅ Deployment completed."

Récapitulatif du fonctionnement de la CI / CD

La CI permet d’automatiser l’éxecution de tests lors de l’ajout de code sur le dépôt distant commun, afin de détecter immédiatement des problèmes d’intégrations.

La CD permet d’automatiser la mise en ligne et mise à jour du projet lorsque du code est ajouté sur le dépôt distant commun, afin d’apporter régulièrement des modifications en production fiable.

Ces deux concepts DevOps utilisent l’automatisation des tâches, pour permettre des processus fluides et rapides.

Concernant Pecunia, un schéma récapitulatif de tout le processus CI / CD est disponible ci-dessous :

CICD_schema

Introduction

Ce chaptire présente l’Application Programming Interface Pecunia dans son ensemble :

  • L’environnement de dévelopement
  • Les différentes couches de l’application
  • Les tests
  • La documentation avec Swagger

Objectif

Mise à disposition d’une API sécurisée pour client avec le framework spring.

🌱 Spring

Présentation

Spring est un framework Java open source, largement utilisé pour développer des applications d’entreprise.
Il fournit une infrastructure robuste, modulaire et extensible, qui permet de créer des applications maintenables, testables et scalables.


✅ Avantages

📈 Popularité et maturité du framework

Spring est l’un des frameworks Java les plus répandus. Il est :

  • Massivement documenté
  • Activement maintenu
  • Soutenu par une large communauté

👉 Cela facilite l’adoption de bonnes pratiques, l’intégration de bibliothèques fiables et la résolution rapide des problèmes.


🧱 Architecture en couches

L’application suit une architecture en couches, favorisant la séparation des responsabilités et la maintenabilité.
Chaque couche a un rôle spécifique :


📂 controller

Gère les requêtes HTTP (REST) et renvoie les réponses appropriées.
Utilise les annotations comme @RestController, @GetMapping, @PostMapping, etc.

Exemples : UserController, AuthController

Responsabilités :

  • Déléguer la logique métier aux services
  • Valider les entrées utilisateur
  • Retourner les statuts HTTP adaptés (200, 404, etc.)

📂 dto (Data Transfer Object)

Permet de transporter les données entre les couches sans exposer les entités.

Exemples : UserDTO, UserRegistrationDTO

Responsabilités :

  • Structurer les données entrantes/sortantes
  • Contrôler ce qui est exposé à l’API
  • Sécuriser et simplifier les échanges

📂 exception

Centralise la gestion des erreurs.

Exemples : ResourceNotFoundException, GlobalExceptionHandler

Responsabilités :

  • Définir des exceptions personnalisées
  • Gérer globalement les erreurs (@ControllerAdvice)
  • Fournir des réponses claires aux utilisateurs

📂 mapper

Convertit les données entre les entités (model) et les DTOs (dto).

Exemple : UserMapper

Responsabilités :

  • Séparer la logique de transformation
  • Faciliter les conversions bidirectionnelles

📂 model

Contient les entités JPA qui représentent les tables de la base de données.

Exemples : User, Transaction

Responsabilités :

  • Définir les champs persistés
  • Spécifier les relations JPA
  • Ajouter les contraintes de validation

📂 repository

Gère l’accès aux données avec Spring Data JPA.

Exemple : UserRepository extends JpaRepository<User, Long>

Responsabilités :

  • Fournir les opérations CRUD
  • Définir des requêtes personnalisées (findByEmail, etc.)

📂 security

Configure la sécurité de l’application : authentification, autorisation, filtres.

Exemples : SecurityConfig, JwtAuthenticationFilter, CustomUserDetailsService

Responsabilités :

  • Définir les règles d’accès
  • Gérer le token JWT
  • Sécuriser les endpoints

📂 service

Contient la logique métier.

Exemples : UserService, TransactionService

Responsabilités :

  • Orchestrer les appels aux repositories
  • Implémenter les règles métier
  • Fournir une API métier aux controller

♻️ Injection de dépendances (IoC)

Spring utilise l’inversion de contrôle (IoC) et l’injection de dépendances, ce qui permet :

  • 🔁 De réduire le couplage entre les composants
  • 🧪 De faciliter les tests unitaires
  • 🧠 De centraliser la configuration des beans

Exemple de dépendances Maven :

<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-security</artifactId>
</dependency>

🌐 Orientation RESTful

Spring facilite le développement d’API REST, devenues essentielles dans les architectures modernes, notamment les applications web SPA (Single Page Application) ou mobiles. L’utilisation des annotations comme @RestController, @GetMapping, @PostMapping, etc., rend le développement rapide et clair.

🗄️ Gestion simplifiée de la base de données

Nous utilisons Hibernate comme implémentation de la spécification JPA (Java Persistence API), couplé à Spring Data JPA. Cette combinaison nous permet de simplifier considérablement l’accès aux données et la gestion des entités.

Grâce à Spring Data JPA, nous pouvons :

  • Définir des interfaces Repository qui permettent d’effectuer des opérations CRUD (Create, Read, Update, Delete) sans avoir à écrire de requêtes SQL ou HQL manuellement.
  • Créer des requêtes personnalisées simplement en nommant les méthodes de façon explicite, par exemple :
      Optional<User> findByEmail(String email);
    
      boolean existsByEmail(String email);
    
      List<User> findByFirstname(String firstname);
    
      List<User> findByLastname(String lastname);
    

🧪 Facilité de tests

Spring propose de nombreux outils et annotations pour les tests unitaires et tests d’intégration (@SpringBootTest, @MockBean, etc.), ce qui garantit la qualité logicielle du projet.


🗂️ Arborescence du projet Spring Boot

src
└── main
    ├── java
    │   └── com
    │       └── pecunia
    │           ├── controller
    │           │   └── UserController.java
    │           │
    │           ├── dto
    │           │   ├── UserDTO.java
    │           │   └── UserLoginDTO.java
    │           │   └── UserRegistrationDTO.java
    │           │   └── UserUpdateDTO.jav
    │           │
    │           ├── exception
    │           │   └── GlobalExceptionHandler.java
    │           │   └──ResourceNotFoundException.java
    │           │
    │           ├── mapper
    │           │   └── UserMapper.java
    │           │
    │           ├── model
    │           │   └── User.java
    │           │
    │           ├── repository
    │           │   └── UserRepository.java
    │           │
    │           ├── security
    │           │   ├── AuthenticationService.java
    │           │   ├── JwtAuthenticationFilter.java
    │           │   ├── JwtService.java
    │           │   ├── PasswordMatchersValidator.java
    │           │   ├── PasswordMatches.java
    │           │   ├── SecurityConfig.java
    │           │   └── TokenBlackList.java
    │           │
    │           ├── service
    │           │   ├── CustomUserDetailsService.java
    │           │   └── UserService.java
    │           │
    │           └── MonProjetApplication.java
    │
    └── resources
        ├── staapplication.propertiesc/
        └── application-dev.properties

🔄 Résumé du flux de données

[Client HTTP]
     ↓
[Controller] → [DTO] → [Service] → [Mapper] → [Model] → [Repository] → [Database]
                                       ↑
                                    [Exception]
                                       ↓
                                [Security Layer]

Swagger UI

Permet de voir tous les endpoints disponibles, de les tester directement depuis le navigateur, et de voir les réponses des requêtes.

Endpoints de l’API

Voici quelques-uns des endpoints disponibles dans notre API :

  • GET /users : Obtenir une liste d’utilsateurs.
  • POST /auth/register : Créer un nouvel utilisateur.
  • PUT /users/{id} : Mettre à jour un uitilisateur existant.
  • DELETE /users/{id} : Supprimer un utilisateur.

Mise en place de l’environement

  • Variables dans votre .env :
#spring doc
DOC_PATH=/pecunia-docs
SORT=method

Exemples d’Utilisation

Voici quelques exemples d’utilisation de notre API avec Swagger UI :

  1. Créer un nouvel utilisateur :

    • Trouvez l’endpoint POST /auth/register.
    • Cliquez sur le bouton “Try it out”.
    • Entrez les données nécessaires dans le corps de la requête :
    {
        "firstname": "John",
        "lastname": "Doe",
        "email": "johndoe@gmail.com",
        "password": "ValidPassword123@?!",
        "confirmPassword": "ValidPassword123@?!"
    }   
    
    • Cliquez sur le bouton “Execute” pour envoyer la requête.
    • Vous devriez voir la réponse avec les données créées.
    • Vous pouvez voir l’utilisateur dans la response body.
  2. Authentifiez-vous :

    • Trouvez l’endpoint POST /auth/login.
    • Cliquez sur le bouton “Try it out”.
    • Entrez les données nécessaires dans le corps de la requête :
    {
        "email": "johndoe@gmail.com",
        "password": "ValidPassword123@?!"
    }
    
    • Cliquez sur le bouton “Execute” pour envoyer la requête.
    • Vous pouvezvoir le token d’authentification dans la response body.

Swagger est un outil puissant pour documenter et tester les APIs. En utilisant Swagger UI, vous pouvez facilement explorer et interagir avec les endpoints de notre API.