SPB Git

spb/phi-conseil

Public

PHI Conseil : site Next.js 16 fidèle à la maquette (accueil 7 sections, pages internes, contenu JSON validé, formulaire de contact sécurisé, tests, docs)

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Simon-Pierre Boucher committed (Nov 8, 2026) parent 0d135ae Browse files

90 changed files +5,294 −139

90 changed files
added .github/workflows/ci.yml +37 −0
@@ -0,0 +1,37 @@
1+name: CI
2+
3+on:
4+ push:
5+ branches: [main]
6+ pull_request:
7+
8+jobs:
9+ check:
10+ runs-on: ubuntu-latest
11+ env:
12+ CONTACT_TRANSPORT: log
13+ NEXT_PUBLIC_SITE_URL: http://127.0.0.1:3100
14+ steps:
15+ - uses: actions/checkout@v4
16+ - uses: pnpm/action-setup@v4
17+ with:
18+ version: 11
19+ - uses: actions/setup-node@v4
20+ with:
21+ node-version: 22
22+ cache: pnpm
23+ - run: pnpm install --frozen-lockfile
24+ - run: pnpm lint
25+ - run: pnpm typecheck
26+ - run: pnpm test
27+ - run: pnpm build
28+ - run: pnpm exec playwright install --with-deps chromium webkit
29+ - run: pnpm test:e2e
30+ - uses: actions/upload-artifact@v4
31+ if: always()
32+ with:
33+ name: playwright-report
34+ path: |
35+ playwright-report
36+ docs/screenshots
37+ retention-days: 14
modified .gitignore +7 −0
@@ -39,3 +39,10 @@ yarn-error.log*
39 # typescript 39 # typescript
40 *.tsbuildinfo 40 *.tsbuildinfo
41 next-env.d.ts 41 next-env.d.ts
42+
43+# tests & captures
44+/test-results/
45+/playwright-report/
46+/docs/screenshots/*.png
47+!/docs/screenshots/.gitkeep
48+.env.local
added .prettierignore +6 −0
@@ -0,0 +1,6 @@
1+.next
2+node_modules
3+pnpm-lock.yaml
4+test-results
5+playwright-report
6+docs/screenshots
added .prettierrc +7 −0
@@ -0,0 +1,7 @@
1+{
2+ "semi": true,
3+ "singleQuote": false,
4+ "trailingComma": "all",
5+ "printWidth": 100,
6+ "plugins": ["prettier-plugin-tailwindcss"]
7+}
modified README.md +41 −22
@@ -1,36 +1,55 @@
1−This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app). 1+# PHI Conseil — site web
2 2
3−## Getting Started 3+Site vitrine de PHI Conseil (« Stratégies · Financement · Réussite »), reproduit fidèlement à partir de la maquette client (`references/phi-conseil-homepage.jpeg`) et conçu comme un produit de production : Next.js 16 (App Router), React 19, TypeScript strict, Tailwind CSS 4, contenu administrable validé par Zod, formulaire de contact sécurisé, tests unitaires et de bout en bout.
4 4
5−First, run the development server: 5+## Démarrage
6 6
7 ```bash 7 ```bash
8−npm run dev 8+pnpm install
9−# or 9+cp .env.example .env.local # ajuster au besoin (transport courriel, URL)
10−yarn dev 10+pnpm dev # http://localhost:3000
11−# or
12−pnpm dev
13−# or
14−bun dev
15 ``` 11 ```
16 12
17−Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. 13+Prérequis : Node.js ≥ 20 (testé avec 26), pnpm ≥ 10.
18 14
19−You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. 15+## Scripts
20 16
21−This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel. 17+| Commande | Rôle |
18+| --------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
19+| `pnpm dev` | serveur de développement |
20+| `pnpm build` / `pnpm start` | build de production / serveur de production (`-p <port>`) |
21+| `pnpm lint` | ESLint (config Next + règles React Compiler) |
22+| `pnpm typecheck` | `tsc --noEmit` |
23+| `pnpm test` | tests unitaires Vitest (schémas, validation, sécurité, API, composants) |
24+| `pnpm test:e2e` | Playwright : navigation, menu mobile, formulaire, débordements, axe, captures (`docs/screenshots/`) ; nécessite `pnpm build` au préalable et `pnpm exec playwright install chromium webkit` la première fois |
25+| `pnpm check` | lint + typecheck + tests + build |
26+| `pnpm icons` | régénère favicon / icônes à partir du logo (spirale dorée) |
27+| `pnpm format` | Prettier |
22 28
23−## Learn More 29+## Structure
24 30
25−To learn more about Next.js, take a look at the following resources: 31+```
26− 32+content/ contenu administrable (site.json, home.json, pages.json) — voir docs/CLIENT_GUIDE.md
27−- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. 33+src/app/ routes (accueil, services, a-propos, temoignages, nous-joindre, confidentialite, api/contact)
28−- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. 34+src/components/ layout/ sections/ ui/ graphics/ forms/
35+src/lib/ content.ts (schémas + chargeur), env.ts, validation.ts, security.ts, mail.ts, seo.ts, spiral.ts
36+src/styles/ tokens.css (design tokens : couleurs échantillonnées, typo, espacements…)
37+tests/unit, tests/e2e
38+docs/ plan, déploiement, guide client, rapport QA, registre des actifs, captures
39+references/ maquette client
40+```
29 41
30−You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome! 42+## Principes
31 43
32−## Deploy on Vercel 44+- **Fidélité** : couleurs échantillonnées sur la maquette, structure DOM = ordre visuel sur tous les écrans, motifs (spirale de Fibonacci, vagues, feuillage) en SVG.
45+- **Honnêteté du contenu** : aucune coordonnée, témoignage ou réalisation inventés. Les témoignages ne sont publiés que s'ils sont `approved: true` et la section `visible: true` ; les photos de la maquette sont remplacées par des illustrations vectorielles tant que le client n'a pas fourni d'images sous licence (`docs/ASSET_REGISTER.md`).
46+- **Sécurité** : CSP, en-têtes durcis, validation Zod client + serveur, honeypot, délai anti-robot, limitation de débit, dédoublonnage, journalisation sans donnée personnelle, secrets uniquement côté serveur.
47+- **Accessibilité** : WCAG 2.2 AA visé (axe sans violation sur toutes les pages), navigation clavier, `prefers-reduced-motion`, lien d'évitement, états focus visibles.
33 48
34−The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. 49+## Documentation
35 50
36−Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details. 51+- `docs/IMPLEMENTATION_PLAN.md` — audit, architecture, décisions, critères d'acceptation
52+- `docs/DEPLOYMENT.md` — hébergement, DNS, HTTPS, variables, sauvegardes, rollback
53+- `docs/CLIENT_GUIDE.md` — modifier textes, services, témoignages, images, coordonnées
54+- `docs/QA_REPORT.md` — résultats réels des vérifications, limites, points à valider
55+- `docs/ASSET_REGISTER.md` — provenance, licence et statut de chaque actif
added content/home.json +80 −0
@@ -0,0 +1,80 @@
1+{
2+ "hero": {
3+ "titleLines": ["Transformer", "vos idées en projets", "concrets"],
4+ "subtitle": "Votre partenaire en recherche de subventions, en rédaction de demandes et en reddition de comptes.",
5+ "cta": { "label": "Découvrir nos services", "href": "/services" },
6+ "image": {
7+ "src": "",
8+ "alt": "Bureau lumineux avec une plante, une tasse et un ordinateur portable"
9+ }
10+ },
11+ "services": {
12+ "visible": true,
13+ "title": "Nos services",
14+ "items": [
15+ {
16+ "slug": "recherche-de-subventions",
17+ "icon": "search",
18+ "title": "Recherche de subventions",
19+ "summary": "Trouvez les programmes adaptés à vos projets.",
20+ "description": ""
21+ },
22+ {
23+ "slug": "redaction-et-revision-de-demandes",
24+ "icon": "document-edit",
25+ "title": "Rédaction et révision de demandes",
26+ "summary": "Présentez des projets solides et convaincants.",
27+ "description": ""
28+ },
29+ {
30+ "slug": "redaction-et-revision-de-rapports",
31+ "icon": "chart",
32+ "title": "Rédaction et révision de rapports",
33+ "summary": "Communiquez efficacement vos résultats.",
34+ "description": ""
35+ },
36+ {
37+ "slug": "reddition-de-comptes",
38+ "icon": "check-circle",
39+ "title": "Reddition de comptes",
40+ "summary": "Assurez une gestion rigoureuse et conforme.",
41+ "description": ""
42+ }
43+ ]
44+ },
45+ "about": {
46+ "visible": true,
47+ "overtitle": "À propos de PHI Conseil",
48+ "title": "Une approche stratégique pour maximiser vos opportunités",
49+ "paragraph": "Forte d'une expertise en financement et en gestion de projets, Phi Conseil vous accompagne avec rigueur, créativité et bienveillance, pour donner vie à vos ambitions.",
50+ "cta": { "label": "En savoir plus", "href": "/a-propos" },
51+ "benefits": [
52+ "Une expertise reconnue",
53+ "Une approche personnalisée",
54+ "Un accompagnement de A à Z",
55+ "Un engagement envers votre réussite"
56+ ],
57+ "image": {
58+ "src": "",
59+ "alt": "Vase transparent avec feuillage posé sur des livres, lumière douce"
60+ }
61+ },
62+ "testimonials": {
63+ "visible": false,
64+ "previewOnly": true,
65+ "items": [
66+ {
67+ "quote": "« Grâce à l'accompagnement de Phi Conseil, nous avons obtenu un financement majeur pour notre projet. Leur professionnalisme et leur disponibilité font toute la différence. »",
68+ "author": "Exemple de maquette (non vérifié)",
69+ "role": "Témoignage fictif de la capture — à remplacer par un avis autorisé",
70+ "approved": false
71+ }
72+ ]
73+ },
74+ "callToAction": {
75+ "visible": true,
76+ "title": "Parlons de vos projets !",
77+ "subtitle": "Contactez-nous dès aujourd'hui pour discuter de vos besoins.",
78+ "cta": { "label": "Nous joindre", "href": "/nous-joindre" }
79+ }
80+}
added content/pages.json +42 −0
@@ -0,0 +1,42 @@
1+{
2+ "services": {
3+ "title": "Nos services",
4+ "intro": "Quatre services complémentaires pour financer, documenter et rendre compte de vos projets.",
5+ "seoDescription": "Recherche de subventions, rédaction et révision de demandes et de rapports, reddition de comptes : les services de PHI Conseil."
6+ },
7+ "about": {
8+ "title": "À propos",
9+ "intro": "Une approche stratégique pour maximiser vos opportunités.",
10+ "seoDescription": "Découvrez l'approche de PHI Conseil : expertise, personnalisation, accompagnement de A à Z et engagement envers votre réussite.",
11+ "body": [
12+ "Forte d'une expertise en financement et en gestion de projets, Phi Conseil vous accompagne avec rigueur, créativité et bienveillance, pour donner vie à vos ambitions."
13+ ]
14+ },
15+ "testimonials": {
16+ "title": "Témoignages",
17+ "intro": "Les témoignages publiés sur cette page sont ceux de clients ayant autorisé leur diffusion.",
18+ "emptyMessage": "Aucun témoignage n'est publié pour le moment.",
19+ "seoDescription": "Témoignages de clients de PHI Conseil."
20+ },
21+ "contact": {
22+ "title": "Nous joindre",
23+ "intro": "Décrivez-nous votre projet ou votre besoin : nous vous répondrons dans les meilleurs délais.",
24+ "seoDescription": "Contactez PHI Conseil pour discuter de vos besoins en recherche de subventions, rédaction de demandes et reddition de comptes.",
25+ "subjects": [
26+ "Recherche de subventions",
27+ "Rédaction ou révision d'une demande",
28+ "Rédaction ou révision d'un rapport",
29+ "Reddition de comptes",
30+ "Autre demande"
31+ ],
32+ "consentLabel": "J'accepte que mes renseignements soient utilisés pour traiter ma demande, conformément à la politique de confidentialité.",
33+ "successTitle": "Message envoyé",
34+ "successMessage": "Merci ! Votre message a bien été transmis. Nous vous répondrons dans les meilleurs délais."
35+ },
36+ "privacy": {
37+ "title": "Politique de confidentialité et mentions légales",
38+ "seoDescription": "Politique de confidentialité et mentions légales de PHI Conseil.",
39+ "lastUpdated": "2026-11-08",
40+ "reviewed": false
41+ }
42+}
added content/site.json +36 −0
@@ -0,0 +1,36 @@
1+{
2+ "brand": {
3+ "name": "PHI Conseil",
4+ "tagline": "STRATÉGIES · FINANCEMENT · RÉUSSITE",
5+ "footerSignature": "DES STRATÉGIES QUI DONNENT VIE À VOS PROJETS.",
6+ "legalName": "Phi Conseil"
7+ },
8+ "navigation": [
9+ { "label": "Accueil", "href": "/" },
10+ { "label": "Nos services", "href": "/services" },
11+ { "label": "À propos", "href": "/a-propos" },
12+ { "label": "Témoignages", "href": "/temoignages" },
13+ { "label": "Nous joindre", "href": "/nous-joindre" }
14+ ],
15+ "headerCta": { "label": "Contactez-nous", "href": "/nous-joindre" },
16+ "contact": {
17+ "email": "",
18+ "phone": "",
19+ "address": "",
20+ "region": "Québec, Canada"
21+ },
22+ "social": {
23+ "linkedin": ""
24+ },
25+ "legalLinks": [
26+ { "label": "Confidentialité", "href": "/confidentialite" },
27+ { "label": "Mentions légales", "href": "/confidentialite#mentions-legales" }
28+ ],
29+ "seo": {
30+ "siteUrl": "https://www.example.invalid",
31+ "defaultTitle": "PHI Conseil — Stratégies, financement, réussite",
32+ "titleTemplate": "%s | PHI Conseil",
33+ "defaultDescription": "Votre partenaire en recherche de subventions, en rédaction de demandes et en reddition de comptes.",
34+ "locale": "fr_CA"
35+ }
36+}
added docs/ASSET_REGISTER.md +18 −0
@@ -0,0 +1,18 @@
1+# Registre des actifs — PHI Conseil
2+
3+| Actif | Emplacement | Provenance | Licence | Statut |
4+| ---------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------- | ---------------------------------------------------------------------------- |
5+| Maquette de l'accueil | `references/phi-conseil-homepage.jpeg` | fournie par le client (IMG_1079.JPG, 1024 × 1536) | usage interne de référence | référence, non publiée |
6+| Logo (spirale dorée + rectangle d'or) | `src/components/graphics/Logo.tsx`, `src/lib/spiral.ts`, `public/logo-mark.svg` | **recréé en SVG** d'après la maquette (construction Fibonacci 1-1-2-3-5-8-13-21) | œuvre dérivée de l'identité du client | ⚠️ à valider par le client ; remplacer par le vectoriel officiel s'il existe |
7+| Favicon / icônes | `src/app/icon.svg`, `src/app/favicon.ico`, `src/app/apple-icon.png`, `public/icon-192.png`, `public/icon-512.png` | générés depuis le logo par `scripts/generate-icons.mts` | idem logo | dérivés du logo, à régénérer si le logo change |
8+| Image Open Graph | `src/app/opengraph-image.tsx` | générée au build (texte + spirale) | — | OK |
9+| Photo du hero (bureau, plante, tasse, ordinateur) | `src/components/graphics/Placeholders.tsx` (`DeskScene`) | **illustration vectorielle de remplacement** créée pour le projet ; la photo de la maquette n'a pas été reprise (droits inconnus) | créée pour le projet | ⚠️ placeholder — fournir une photo sous licence |
10+| Photo « À propos » (vase, feuillage, livres) | `src/components/graphics/Placeholders.tsx` (`VaseScene`) | idem | créée pour le projet | ⚠️ placeholder — fournir une photo sous licence |
11+| Motifs (vagues, feuillage, spirales décoratives) | `src/components/graphics/*` | créés pour le projet | — | OK |
12+| Icônes de services et d'interface | `src/components/ui/ServiceIcon.tsx`, `Icons.tsx` | dessinées pour le projet (trait 1,5) | — | OK |
13+| Police titres **EB Garamond** | `next/font/google` (auto-hébergée au build) | Georg Duffner / Octavio Pardo, Google Fonts | SIL Open Font License 1.1 | OK ; remplacer par la police officielle du client si elle existe |
14+| Police corps **Figtree** | `next/font/google` | Erik Kennedy, Google Fonts | SIL Open Font License 1.1 | OK |
15+| Témoignage « Marie-Claude Deschamps, Directrice générale » | `content/home.json` (`approved: false`) | texte de la maquette, **identité non vérifiée** | — | ⚠️ NON PUBLIÉ ; obtenir un avis réel et une autorisation écrite |
16+| Textes marketing (hero, services, à propos, bandeau) | `content/*.json` | repris mot pour mot de la maquette | client | à confirmer par le client |
17+| Textes juridiques (confidentialité, mentions légales) | `src/app/(site)/confidentialite/page.tsx`, `pages.json` | gabarit rédigé pour le projet, décrit les pratiques réelles du site | — | ⚠️ à faire valider (`privacy.reviewed=false`) |
18+| Coordonnées, LinkedIn | `content/site.json` | **vides** (non fournies) | — | à compléter par le client |
added docs/CLIENT_GUIDE.md +47 −0
@@ -0,0 +1,47 @@
1+# Guide de gestion des contenus — PHI Conseil
2+
3+Tous les textes, services, avantages, témoignages, coordonnées et paramètres SEO se trouvent dans trois fichiers du dossier `content/`. Ils se modifient avec n'importe quel éditeur de texte (ou l'interface web du dépôt Git) ; aucune modification du code n'est nécessaire. À chaque mise à jour, le site est reconstruit et vérifié : une faute de structure (guillemet manquant, champ oublié) bloque la publication avec un message précis au lieu de casser le site.
4+
5+> Règle d'or : conservez les guillemets `"…"` et les virgules ; modifiez uniquement ce qui se trouve entre les guillemets, ou ajoutez/retirez des éléments complets dans les listes.
6+
7+## `content/site.json` — identité et coordonnées
8+
9+| Champ | Usage |
10+| -------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------- |
11+| `brand.name`, `brand.tagline`, `brand.footerSignature`, `brand.legalName` | nom, signature de l'en-tête, phrase du pied de page, raison sociale du copyright |
12+| `navigation[]` | liens du menu (ordre = ordre affiché) |
13+| `headerCta` | bouton « Contactez-nous → » |
14+| `contact.email`, `contact.phone`, `contact.address`, `contact.region` | coordonnées ; **laissez `""` pour masquer** l'icône courriel / la ligne correspondante |
15+| `social.linkedin` | URL complète du profil LinkedIn ; `""` = icône masquée |
16+| `legalLinks[]` | liens juridiques du pied de page |
17+| `seo.siteUrl`, `seo.defaultTitle`, `seo.titleTemplate`, `seo.defaultDescription` | métadonnées par défaut |
18+
19+## `content/home.json` — page d'accueil
20+
21+- `hero.titleLines` : les lignes du grand titre (1 à 4). `hero.subtitle`, `hero.cta`.
22+- `hero.image.src` : chemin d'une photo placée dans `public/images/` (ex. `"/images/hero.jpg"`). Vide = illustration vectorielle de remplacement. Toujours renseigner `alt` (description pour les lecteurs d'écran).
23+- `services.items[]` : `slug` (identifiant URL, lettres minuscules et tirets), `icon` (`search`, `document-edit`, `chart`, `check-circle`), `title`, `summary` (accueil), `description` (page Services, facultatif). L'ordre du tableau = ordre d'affichage.
24+- `about` : `overtitle`, `title`, `paragraph`, `cta`, `benefits[]` (coches dorées), `image`.
25+- `testimonials.visible` : `true` pour afficher la section. **Un témoignage n'apparaît que si `approved: true`** (autorisation écrite de la personne citée). `previewOnly` permet d'afficher les exemples non approuvés uniquement sur un site de démonstration.
26+- `callToAction` : bandeau bleu final.
27+- Chaque section possède `visible` pour la masquer sans la supprimer.
28+
29+## `content/pages.json` — pages internes
30+
31+Titres, introductions, descriptions SEO des pages Services, À propos, Témoignages, Nous joindre (liste `subjects` du menu déroulant, texte de consentement, message de confirmation) et Confidentialité (`lastUpdated` au format AAAA-MM-JJ ; passez `reviewed` à `true` une fois le texte validé juridiquement pour retirer l'avertissement).
32+
33+## Images
34+
35+1. Préparer un JPEG/WebP de bonne qualité (hero : ≥ 1600 px de large ; À propos : format vertical ≥ 1000 × 1250 px).
36+2. Le déposer dans `public/images/`.
37+3. Renseigner `image.src` et `image.alt` dans `home.json`.
38+4. Noter la provenance et la licence dans `docs/ASSET_REGISTER.md`.
39+ Le site convertit et redimensionne automatiquement (AVIF/WebP, tailles adaptées).
40+
41+## Logo et favicon
42+
43+Le logo actuel est une recréation SVG de la spirale dorée. Pour utiliser le logo officiel : remplacer le composant `src/components/graphics/Logo.tsx` par le SVG fourni, puis `pnpm icons` pour régénérer favicon et icônes (ou fournir directement `src/app/icon.svg`, `src/app/apple-icon.png`).
44+
45+## Publier une modification
46+
47+Sur le serveur d'hébergement : `pnpm build && pm2 restart phi-conseil` (voir `docs/DEPLOYMENT.md`), puis `git commit` pour conserver l'historique. Une erreur de contenu s'affiche au build sous la forme `Contenu invalide dans content/home.json : hero.subtitle : …`.
added docs/DEPLOYMENT.md +68 −0
@@ -0,0 +1,68 @@
1+# Déploiement — PHI Conseil
2+
3+## Cible
4+
5+Application Node.js (`next start`) derrière un reverse proxy TLS. Aucune base de données. Le build est statique pour toutes les pages ; seule `POST /api/contact` s'exécute à la demande.
6+
7+| Besoin | Valeur |
8+| -------- | ---------------------------------------------------------------- |
9+| Node.js | ≥ 20 (testé 26) |
10+| RAM | ~150 Mo au repos |
11+| Port | `PORT` (défaut 3000) |
12+| Fichiers | `.next/`, `public/`, `content/`, `node_modules/`, `package.json` |
13+
14+## Variables d'environnement
15+
16+Voir `.env.example`. En production :
17+
18+- `NEXT_PUBLIC_SITE_URL` — URL publique (canonical, sitemap, OG). **Inlinée au build** : rebuild si elle change.
19+- `NEXT_PUBLIC_CONTENT_PREVIEW` — `0` en production. `1` affiche les témoignages de maquette avec un bandeau « exemple non vérifié » (sites de démonstration uniquement ; inlinée au build).
20+- `CONTACT_TRANSPORT=smtp` + `CONTACT_TO`, `CONTACT_FROM`, `SMTP_HOST`, `SMTP_PORT`, `SMTP_SECURE`, `SMTP_USER`, `SMTP_PASS` — sans ces valeurs le formulaire fonctionne mais **n'envoie aucun courriel** (`CONTACT_TRANSPORT=log`) et l'utilisateur en est informé dans l'écran de confirmation.
21+- `CONTACT_RATE_LIMIT` / `CONTACT_RATE_WINDOW_S` — anti-abus (défaut 5 / 600 s par IP). Le limiteur est en mémoire : valable pour une instance unique (PM2 `fork`). Pour plusieurs instances, brancher Redis dans `src/lib/security.ts`.
22+
23+Le proxy doit transmettre `X-Forwarded-For` (Caddy et nginx le font par défaut) pour que la limitation par IP soit correcte.
24+
25+## Procédure générique (VPS / serveur Node)
26+
27+```bash
28+git clone <dépôt> phi-conseil && cd phi-conseil
29+pnpm install --frozen-lockfile
30+cp .env.example .env.local && $EDITOR .env.local
31+pnpm build
32+pm2 start pnpm --name phi-conseil -- start -p 3000 # ou systemd
33+```
34+
35+Reverse proxy (exemple Caddy) : `phiconseil.example { reverse_proxy 127.0.0.1:3000 }` — Caddy obtient et renouvelle le certificat TLS automatiquement. HSTS est déjà émis par l'application.
36+
37+### Vercel (alternative)
38+
39+Importer le dépôt, définir les variables d'environnement, framework Next.js détecté automatiquement. Le limiteur en mémoire n'est pas fiable en serverless : remplacer par Upstash/Redis ou le middleware de limitation du fournisseur.
40+
41+## Procédure MacLustr (`mld`) — site de démonstration test.spboucher.ai
42+
43+1. Manifeste `M1M32:~/dispatch/apps/phi-conseil.json` (dir `~/apps/phi-conseil`, PM2 `pnpm start -p 8520`, `tunnel.domain = test.spboucher.ai`, hooks `post_sync` : `pnpm install --frozen-lockfile && pnpm build`).
44+2. `mld stage ~/Desktop/phi-conseil phi-conseil` puis `mld deploy phi-conseil`.
45+3. DNS GoDaddy `A test.spboucher.ai → 51.161.112.61` (passerelle BHS64) — déjà en place ; `mld` pose la route Caddy et vérifie la santé publique.
46+4. Mises à jour : modifier **sur le nœud** (clone git), `pnpm build && pm2 restart phi-conseil`, puis `git commit/push` depuis le nœud (règle remote-first MacLustr).
47+
48+## Sauvegardes et restauration
49+
50+- Le code et le contenu (`content/*.json`) sont versionnés dans Git : la sauvegarde est le dépôt distant. Restaurer = `git checkout <tag>` + `pnpm install && pnpm build`.
51+- Les messages du formulaire ne sont pas stockés par le site : ils vivent dans la boîte courriel destinataire (sauvegarde relevant du fournisseur de messagerie).
52+- Secrets (`.env.local`) : conserver une copie chiffrée hors du dépôt (gestionnaire de mots de passe).
53+
54+## Retour arrière
55+
56+1. `git log` → identifier la dernière version saine (chaque mise en production est taguée `vX.Y.Z`).
57+2. `git checkout vX.Y.Z && pnpm install --frozen-lockfile && pnpm build && pm2 restart phi-conseil`.
58+3. Vérifier `GET /` (200) et `GET /api/contact` (405 attendu) ; sur MacLustr, `mld status --live phi-conseil`.
59+
60+## Surveillance minimale
61+
62+- Santé : `GET /` doit répondre 200 (healthcheck PM2/mld).
63+- Journaux : `pm2 logs phi-conseil` — les lignes `[contact]` ne contiennent qu'une empreinte d'IP, jamais le contenu des messages.
64+- Erreurs de rendu : digest côté serveur (`console.error`) corrélable à l'écran d'erreur client. Un service d'erreurs (Sentry…) peut être ajouté dans `src/app/error.tsx` et `instrumentation.ts` ; non inclus par défaut pour éviter tout traceur non consenti.
65+
66+## Mise à jour des dépendances
67+
68+`pnpm outdated` puis `pnpm update --latest` en suivant les notes de migration de Next.js ; relancer `pnpm check` et `pnpm test:e2e` avant mise en production. Audit : `pnpm audit`.
added docs/IMPLEMENTATION_PLAN.md +87 −0
@@ -0,0 +1,87 @@
1+# Plan d'implémentation — PHI Conseil
2+
3+Date : 2026-11-08. Référence visuelle : `references/phi-conseil-homepage.jpeg` (1024 × 1536 px, capture desktop fournie par le client).
4+
5+## 1. Audit initial
6+
7+| Élément | État constaté |
8+| ------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
9+| Dépôt | Vide à l'origine (créé aujourd'hui). Scaffold `create-next-app` 16.4.5, TypeScript strict, Tailwind 4, ESLint 9, pnpm 11. |
10+| Capture | Présente et inspectée. Couleurs échantillonnées par région (médiane / mode / moyenne des pixels sombres) — voir `src/styles/tokens.css`. |
11+| Actifs client | **Aucun** fichier original fourni : pas de logo vectoriel, pas de photos sous licence, pas de polices, pas de coordonnées, pas de textes juridiques, pas de témoignage autorisé. |
12+| Hébergement | Non précisé. Cible par défaut : Node 20+ (`next start` derrière un reverse proxy TLS). Un déploiement sur le cluster MacLustr (`mld`) ou sur Vercel est documenté dans `docs/DEPLOYMENT.md`. |
13+
14+### Inconnues bloquantes (à valider par le client)
15+
16+1. Logo vectoriel officiel (sinon le SVG recréé `src/components/graphics/Logo.tsx` doit être approuvé).
17+2. Photos sous licence pour le hero et la section À propos (les illustrations livrées sont des **placeholders vectoriels** dans la palette, marqués comme tels dans `docs/ASSET_REGISTER.md`).
18+3. Coordonnées réelles (courriel, téléphone, adresse, LinkedIn) : laissées vides, les icônes/liens correspondants sont masqués tant que les champs du contenu sont vides.
19+4. Témoignage(s) autorisé(s) : la section est **masquée en production** (`testimonials.visible=false`) ; le texte de la maquette est conservé uniquement comme exemple de prévisualisation marqué « non vérifié ».
20+5. Textes juridiques (confidentialité, mentions légales) : gabarit fourni, à faire valider par un conseiller juridique.
21+6. Fournisseur de messagerie transactionnelle (SMTP) et adresses expéditeur/destinataire.
22+7. Choix CMS : voir §4.
23+
24+## 2. Architecture
25+
26+```
27+content/ ← contenu administrable (JSON validé par Zod au build et à l'exécution)
28+ site.json ← identité, navigation, coordonnées, liens sociaux, SEO par défaut
29+ home.json ← hero, services, à-propos, témoignages, bandeau, visibilité des sections
30+ pages.json ← pages internes (services, à propos, témoignages, nous joindre, confidentialité)
31+src/
32+ app/
33+ layout.tsx ← polices, métadonnées par défaut, Header/Footer
34+ (site)/page.tsx ← accueil (7 sections)
35+ (site)/services|a-propos|temoignages|nous-joindre|confidentialite/page.tsx
36+ api/contact/route.ts ← POST : validation Zod, honeypot, limitation de débit, envoi SMTP
37+ sitemap.ts, robots.ts, icon.svg, opengraph-image.tsx
38+ components/
39+ layout/ Header, MobileNav, Footer
40+ sections/ Hero, Services, About, Testimonials, CallToAction
41+ graphics/ Logo, GoldenSpiral, WaveDivider, BotanicalMotif, placeholders photo
42+ ui/ Button, Container, SectionTitle, Icon…
43+ forms/ ContactForm (client)
44+ lib/ content.ts (chargeur + schémas), env.ts, validation.ts, security.ts (rate limit, sanitisation), mail.ts, seo.ts
45+ styles/ tokens.css (design tokens), globals.css
46+tests/
47+ unit/ Vitest + Testing Library (schémas, validation, rate limit, composants)
48+ e2e/ Playwright (navigation, menu mobile, formulaire, liens, axe, captures)
49+docs/ IMPLEMENTATION_PLAN, DEPLOYMENT, CLIENT_GUIDE, QA_REPORT, ASSET_REGISTER
50+```
51+
52+## 3. Design system
53+
54+- Tokens CSS (`src/styles/tokens.css`) : couleurs échantillonnées, échelle typographique `clamp()`, espacements, rayons, ombres, mouvement. Exposés à Tailwind 4 via `@theme`.
55+- Titres : **EB Garamond** (OFL) ; corps : **Figtree** (OFL). Chargées par `next/font/google` (auto-hébergées au build, `display: swap`). À remplacer par les polices officielles du client si elles existent.
56+- Motifs : spirale de Fibonacci (carrés + arcs de quart de cercle, ratio 1,618 approché par la suite 1-1-2-3-5-8-13-21), vagues, feuillage — tous en SVG inline.
57+
58+## 4. Contenu administrable — décision
59+
60+Le cahier des charges recommande un CMS maintenu (Payload / Sanity) tout en demandant d'éviter de surdimensionner si une solution statique convient et de **demander validation**.
61+
62+Choix livré : **contenu structuré en fichiers JSON versionnés** (`content/*.json`), validé par Zod, modifiable sans toucher au code applicatif, avec guide client. Raisons : aucun hébergement de base de données ni de compte tiers à gérer, pas de surface d'authentification à sécuriser, sauvegarde = historique Git, coût nul, déploiement statique possible.
63+
64+Compromis documentés : l'édition exige un éditeur de texte ou l'interface web Git ; pas d'aperçu WYSIWYG ni de gestion de médias par glisser-déposer. Chemin d'évolution : `src/lib/content.ts` est l'unique point d'accès au contenu ; brancher Payload (SQLite/Postgres, intégré à Next) ou Sanity consiste à remplacer ce module en conservant les schémas Zod. **Décision à valider par le client.**
65+
66+## 5. Étapes
67+
68+1. Audit + plan (ce document). ✔
69+2. Design system : tokens, polices, UI de base, logo et motifs SVG.
70+3. Accueil fidèle : 7 sections, comparaison Playwright à 1024 px (largeur de la capture) puis 1440 px.
71+4. Pages internes + chargeur de contenu + visibilité des sections.
72+5. Formulaire de contact + API + messagerie SMTP configurable + protection antispam.
73+6. Validation : lint, typecheck, Vitest, Playwright (6 largeurs, axe), build de production.
74+7. Livraison : README, .env.example, docs/DEPLOYMENT, CLIENT_GUIDE, QA_REPORT, ASSET_REGISTER.
75+
76+## 6. Critères d'acceptation par section (checklist)
77+
78+- [ ] En-tête : logo + nom + signature, 5 liens, CTA bleu arrondi « Contactez-nous → », lien actif souligné, menu mobile accessible, en-tête collant sobre.
79+- [ ] Hero : fond ivoire, H1 serif 3 lignes, sous-titre, bouton bleu, illustration à droite avec spirale dorée superposée, vagues ivoire/bleu pâle en bas.
80+- [ ] Services : titre centré entre filets dorés, 4 cartes (disque bleu-gris + icône au trait), textes exacts.
81+- [ ] À propos : image verticale à gauche avec courbe ivoire, surtitre doré, H2, paragraphe, bouton contour, 4 bénéfices avec coches dorées séparés par un filet vertical.
82+- [ ] Témoignages : encadré bleu pâle arrondi, guillemets dorés, serif italique, pagination ; slider accessible ; masqué en prod sans avis validé.
83+- [ ] Bandeau : fond bleu ardoise, titre blanc, bouton ivoire, spirale + feuillage dorés à droite.
84+- [ ] Pied de page : logo + nom, liens, icônes LinkedIn/courriel (si fournis), copyright dynamique, signature, liens légaux.
85+- [ ] Responsive 360/390/768/1024/1440/1920 sans débordement horizontal.
86+- [ ] Formulaire : validation client/serveur, honeypot, rate limit, SMTP configurable, états succès/erreur, tests.
87+- [ ] Lint, typecheck, tests, build, axe : verts ou écarts documentés dans `docs/QA_REPORT.md`.
added docs/screenshots/.gitkeep +0 −0
modified next.config.ts +9 −1
@@ -1,9 +1,17 @@
1 import type { NextConfig } from "next"; 1 import type { NextConfig } from "next";
2+import { securityHeaders } from "./src/lib/security";
2 3
3 const nextConfig: NextConfig = { 4 const nextConfig: NextConfig = {
4− /* config options here */
5 cacheComponents: true, 5 cacheComponents: true,
6 partialPrefetching: true, 6 partialPrefetching: true,
7+ poweredByHeader: false,
8+ reactStrictMode: true,
9+ images: {
10+ formats: ["image/avif", "image/webp"],
11+ },
12+ async headers() {
13+ return [{ source: "/(.*)", headers: securityHeaders }];
14+ },
7 turbopack: { 15 turbopack: {
8 rules: { 16 rules: {
9 "*.css": { 17 "*.css": {
modified package.json +27 −3
@@ -6,22 +6,46 @@
6 "dev": "next dev", 6 "dev": "next dev",
7 "build": "next build", 7 "build": "next build",
8 "start": "next start", 8 "start": "next start",
9− "lint": "eslint" 9+ "lint": "eslint",
10+ "typecheck": "tsc --noEmit",
11+ "format": "prettier --write .",
12+ "format:check": "prettier --check .",
13+ "test": "vitest run",
14+ "test:watch": "vitest",
15+ "test:e2e": "playwright test",
16+ "icons": "node --experimental-strip-types scripts/generate-icons.mts",
17+ "check": "pnpm lint && pnpm typecheck && pnpm test && pnpm build"
10 }, 18 },
11 "dependencies": { 19 "dependencies": {
20+ "@hookform/resolvers": "^7.0.3",
12 "next": "16.4.5", 21 "next": "16.4.5",
22+ "nodemailer": "^10.3.1",
13 "react": "19.3.0", 23 "react": "19.3.0",
14− "react-dom": "19.3.0" 24+ "react-dom": "19.3.0",
25+ "react-hook-form": "^7.91.0",
26+ "sharp": "^0.35.5",
27+ "zod": "^4.6.7"
15 }, 28 },
16 "devDependencies": { 29 "devDependencies": {
30+ "@axe-core/playwright": "^4.14.0",
31+ "@playwright/test": "^1.65.0",
17 "@tailwindcss/turbopack": "^4", 32 "@tailwindcss/turbopack": "^4",
33+ "@testing-library/jest-dom": "^7.0.1",
34+ "@testing-library/react": "^16.3.3",
35+ "@testing-library/user-event": "^14.6.8",
18 "@types/node": "^20", 36 "@types/node": "^20",
37+ "@types/nodemailer": "^8.0.2",
19 "@types/react": "^19", 38 "@types/react": "^19",
20 "@types/react-dom": "^19", 39 "@types/react-dom": "^19",
40+ "@vitejs/plugin-react": "^6.2.1",
21 "eslint": "^9", 41 "eslint": "^9",
22 "eslint-config-next": "16.4.5", 42 "eslint-config-next": "16.4.5",
43+ "jsdom": "^31.0.0",
44+ "prettier": "^3.9.10",
45+ "prettier-plugin-tailwindcss": "^0.8.1",
23 "tailwindcss": "^4", 46 "tailwindcss": "^4",
24− "typescript": "^5" 47+ "typescript": "^5",
48+ "vitest": "^5.1.0"
25 }, 49 },
26 "packageManager": "pnpm@11.1.2" 50 "packageManager": "pnpm@11.1.2"
27 } 51 }
added playwright.config.ts +26 −0
@@ -0,0 +1,26 @@
1+import { defineConfig, devices } from "@playwright/test";
2+
3+const PORT = process.env.PW_PORT ?? "3100";
4+const baseURL = `http://127.0.0.1:${PORT}`;
5+
6+export default defineConfig({
7+ testDir: "./tests/e2e",
8+ fullyParallel: true,
9+ retries: process.env.CI ? 1 : 0,
10+ reporter: process.env.CI ? [["list"], ["html", { open: "never" }]] : "list",
11+ use: { baseURL, trace: "retain-on-failure", locale: "fr-CA" },
12+ webServer: {
13+ command: `pnpm start -p ${PORT}`,
14+ url: baseURL,
15+ reuseExistingServer: !process.env.CI,
16+ timeout: 120_000,
17+ env: { CONTACT_TRANSPORT: "log", NEXT_PUBLIC_SITE_URL: baseURL },
18+ },
19+ projects: [
20+ {
21+ name: "desktop",
22+ use: { ...devices["Desktop Chrome"], viewport: { width: 1440, height: 900 } },
23+ },
24+ { name: "mobile", use: { ...devices["iPhone 13"] } },
25+ ],
26+});
modified pnpm-lock.yaml +1098 −5
Large diff (1,740 lines) is not loaded by default to keep the page fast.Load diff
deleted public/file.svg +0 −1
Large diff (2 lines) is not loaded by default to keep the page fast.Load diff
deleted public/globe.svg +0 −1
Large diff (2 lines) is not loaded by default to keep the page fast.Load diff
added public/icon-192.png +0 −0

Binary file not shown.

added public/icon-512.png +0 −0

Binary file not shown.

added public/logo-mark.svg +4 −0
Large diff (5 lines) is not loaded by default to keep the page fast.Load diff
deleted public/next.svg +0 −1
Large diff (2 lines) is not loaded by default to keep the page fast.Load diff
deleted public/vercel.svg +0 −1
Large diff (2 lines) is not loaded by default to keep the page fast.Load diff
deleted public/window.svg +0 −1
Large diff (2 lines) is not loaded by default to keep the page fast.Load diff
added references/phi-conseil-homepage.jpeg +0 −0

Binary file not shown.

added scripts/generate-icons.mts +58 −0
Large diff (58 lines) is not loaded by default to keep the page fast.Load diff
added scripts/overflow.mjs +18 −0
Large diff (18 lines) is not loaded by default to keep the page fast.Load diff
added scripts/shot.mjs +9 −0
Large diff (9 lines) is not loaded by default to keep the page fast.Load diff
added src/app/(site)/a-propos/page.tsx +65 −0
Large diff (65 lines) is not loaded by default to keep the page fast.Load diff
added src/app/(site)/confidentialite/page.tsx +99 −0
Large diff (99 lines) is not loaded by default to keep the page fast.Load diff
added src/app/(site)/nous-joindre/page.tsx +77 −0
Large diff (77 lines) is not loaded by default to keep the page fast.Load diff
added src/app/(site)/page.tsx +22 −0
Large diff (22 lines) is not loaded by default to keep the page fast.Load diff
added src/app/(site)/services/page.tsx +52 −0
Large diff (52 lines) is not loaded by default to keep the page fast.Load diff
added src/app/(site)/temoignages/page.tsx +50 −0
Large diff (50 lines) is not loaded by default to keep the page fast.Load diff
added src/app/api/contact/route.ts +96 −0
Large diff (96 lines) is not loaded by default to keep the page fast.Load diff
added src/app/apple-icon.png +0 −0

Binary file not shown.

added src/app/error.tsx +35 −0
Large diff (35 lines) is not loaded by default to keep the page fast.Load diff
modified src/app/favicon.ico +0 −0

Binary file not shown.

modified src/app/globals.css +153 −16
Large diff (179 lines) is not loaded by default to keep the page fast.Load diff
added src/app/icon.svg +4 −0
Large diff (5 lines) is not loaded by default to keep the page fast.Load diff
modified src/app/layout.tsx +48 −16
Large diff (77 lines) is not loaded by default to keep the page fast.Load diff
added src/app/manifest.ts +18 −0
Large diff (18 lines) is not loaded by default to keep the page fast.Load diff
added src/app/not-found.tsx +21 −0
Large diff (21 lines) is not loaded by default to keep the page fast.Load diff
added src/app/opengraph-image.tsx +65 −0
Large diff (65 lines) is not loaded by default to keep the page fast.Load diff
deleted src/app/page.tsx +0 −69
Large diff (69 lines) is not loaded by default to keep the page fast.Load diff
added src/app/robots.ts +9 −0
Large diff (9 lines) is not loaded by default to keep the page fast.Load diff
added src/app/sitemap.ts +19 −0
Large diff (19 lines) is not loaded by default to keep the page fast.Load diff
added src/components/forms/ContactForm.tsx +275 −0
Large diff (275 lines) is not loaded by default to keep the page fast.Load diff
added src/components/graphics/BotanicalMotif.tsx +34 −0
Large diff (34 lines) is not loaded by default to keep the page fast.Load diff
added src/components/graphics/GoldenSpiral.tsx +66 −0
Large diff (66 lines) is not loaded by default to keep the page fast.Load diff
added src/components/graphics/Logo.tsx +46 −0
Large diff (46 lines) is not loaded by default to keep the page fast.Load diff
added src/components/graphics/Placeholders.tsx +168 −0
Large diff (168 lines) is not loaded by default to keep the page fast.Load diff
added src/components/graphics/WaveDivider.tsx +28 −0
Large diff (28 lines) is not loaded by default to keep the page fast.Load diff
added src/components/layout/Footer.tsx +102 −0
Large diff (102 lines) is not loaded by default to keep the page fast.Load diff
added src/components/layout/Header.tsx +30 −0
Large diff (30 lines) is not loaded by default to keep the page fast.Load diff
added src/components/layout/MobileNav.tsx +114 −0
Large diff (114 lines) is not loaded by default to keep the page fast.Load diff
added src/components/layout/NavLinks.tsx +47 −0
Large diff (47 lines) is not loaded by default to keep the page fast.Load diff
added src/components/sections/About.tsx +78 −0
Large diff (78 lines) is not loaded by default to keep the page fast.Load diff
added src/components/sections/CallToAction.tsx +46 −0
Large diff (46 lines) is not loaded by default to keep the page fast.Load diff
added src/components/sections/Hero.tsx +55 −0
Large diff (55 lines) is not loaded by default to keep the page fast.Load diff
added src/components/sections/Services.tsx +34 −0
Large diff (34 lines) is not loaded by default to keep the page fast.Load diff
added src/components/sections/TestimonialSlider.tsx +106 −0
Large diff (106 lines) is not loaded by default to keep the page fast.Load diff
added src/components/sections/Testimonials.tsx +27 −0
Large diff (27 lines) is not loaded by default to keep the page fast.Load diff
added src/components/ui/Button.tsx +88 −0
Large diff (88 lines) is not loaded by default to keep the page fast.Load diff
added src/components/ui/Icons.tsx +63 −0
Large diff (63 lines) is not loaded by default to keep the page fast.Load diff
added src/components/ui/SectionTitle.tsx +22 −0
Large diff (22 lines) is not loaded by default to keep the page fast.Load diff
added src/components/ui/ServiceIcon.tsx +57 −0
Large diff (57 lines) is not loaded by default to keep the page fast.Load diff
added src/lib/content.ts +184 −0
Large diff (184 lines) is not loaded by default to keep the page fast.Load diff
added src/lib/env.ts +62 −0
Large diff (62 lines) is not loaded by default to keep the page fast.Load diff
added src/lib/mail.ts +85 −0
Large diff (85 lines) is not loaded by default to keep the page fast.Load diff
added src/lib/security.ts +93 −0
Large diff (93 lines) is not loaded by default to keep the page fast.Load diff
added src/lib/seo.ts +39 −0
Large diff (39 lines) is not loaded by default to keep the page fast.Load diff
added src/lib/spiral.ts +81 −0
Large diff (81 lines) is not loaded by default to keep the page fast.Load diff
added src/lib/time.ts +8 −0
Large diff (8 lines) is not loaded by default to keep the page fast.Load diff
added src/lib/validation.ts +64 −0
Large diff (64 lines) is not loaded by default to keep the page fast.Load diff
added src/styles/tokens.css +100 −0
Large diff (100 lines) is not loaded by default to keep the page fast.Load diff
added tests/e2e/a11y.spec.ts +25 −0
Large diff (25 lines) is not loaded by default to keep the page fast.Load diff
added tests/e2e/contact.spec.ts +40 −0
Large diff (40 lines) is not loaded by default to keep the page fast.Load diff
added tests/e2e/mobile-menu.spec.ts +37 −0
Large diff (37 lines) is not loaded by default to keep the page fast.Load diff
added tests/e2e/navigation.spec.ts +85 −0
Large diff (85 lines) is not loaded by default to keep the page fast.Load diff
added tests/e2e/screenshots.spec.ts +22 −0
Large diff (22 lines) is not loaded by default to keep the page fast.Load diff
added tests/unit/components.test.tsx +43 −0
Large diff (43 lines) is not loaded by default to keep the page fast.Load diff
added tests/unit/contact-route.test.ts +108 −0
Large diff (108 lines) is not loaded by default to keep the page fast.Load diff
added tests/unit/content.test.ts +39 −0
Large diff (39 lines) is not loaded by default to keep the page fast.Load diff
added tests/unit/security.test.ts +51 −0
Large diff (51 lines) is not loaded by default to keep the page fast.Load diff
added tests/unit/setup.ts +20 −0
Large diff (20 lines) is not loaded by default to keep the page fast.Load diff
added tests/unit/validation.test.ts +47 −0
Large diff (47 lines) is not loaded by default to keep the page fast.Load diff
modified tsconfig.json +3 −2
Large diff (15 lines) is not loaded by default to keep the page fast.Load diff
added vitest.config.mts +14 −0
Large diff (15 lines) is not loaded by default to keep the page fast.Load diff
Menu

Theme