diff --git a/.gitignore b/.gitignore index d55cdc6..5ec0827 100644 --- a/.gitignore +++ b/.gitignore @@ -26,3 +26,7 @@ pnpm-debug.log* # temp *.log + +# runtime data (uploads + JSON content edited via /admin) +# kept on the server, not deployed via FTP +data/ diff --git a/CLAUDE.md b/CLAUDE.md index 5075e88..8794ec2 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -22,7 +22,23 @@ Site vitrine personnel en **Astro 6 hybrid** (SSG public + SSR admin), identité ## Points critiques à connaître ### Content -Tout le contenu du site est dans `public/content/*.json` (7 fichiers). Chaque fichier est validé par un schéma Zod dans `src/content/schemas/`. Édition possible via `/admin` ou directement en éditant le JSON. +Tout le contenu du site est dans **`data/content/*.json`** (7 fichiers, **gitignored**). Chaque fichier est validé par un schéma Zod dans `src/content/schemas/`. Édition possible via `/admin` ou directement en éditant le JSON. Au premier run, les JSON sont copiés depuis `public/content/*.json` (seeds versionnés en repo) si `data/content/` est vide — voir `src/lib/content.ts`. + +### Persistance prod (Plesk) +Le déploiement Gitea CI/CD FTP **rebuilt `public/` à chaque push**. C'est pour ça que tout le contenu dynamique est dans **`data/`** (gitignored, conservé entre déploiements) : +- `data/content/*.json` — JSON édités via `/admin` +- `data/uploads/img/{folder}/*` — images uploadées +- `data/uploads/assets/{folder}/*` — PDF (CV) + +Les fichiers de `data/uploads/` sont servis via la route **`/api/files/[...path]`** (Astro SSR) — donc les chemins en JSON sont du type `/api/files/img/pp/foo.png` ou `/api/files/assets/cv.pdf`. + +**Init prod (premier déploiement post-refonte)** : +1. Sur le serveur Plesk, créer `data/{content,uploads/img,uploads/assets}` avec les bons droits (utilisateur Node). +2. Copier les seeds : `cp public/content/*.json data/content/` et `cp -r public/img/* data/uploads/img/` et `cp public/assets/cv.pdf data/uploads/assets/`. +3. Vérifier que la CI Gitea **n'écrase pas** `data/` (l'exclure du déploiement FTP, ou s'assurer qu'il ne fait pas partie du payload). +4. Tester un upload via `/admin` → vérifier que le fichier arrive bien dans `data/uploads/...` et que `/api/files/...` le sert. + +**Sécurité** : `/api/files/[...path].ts` protège contre le path traversal (résolu via `resolve()` + check préfixe). Les uploads passent par `/api/upload` qui est JWT-gated dans `src/middleware.ts`. ### Admin Protégé par mot de passe bcrypt + JWT cookie httpOnly 7j. Middleware dans `src/middleware.ts`. Routes `/admin/*` et `/api/*` ont `export const prerender = false`. @@ -72,7 +88,7 @@ Tous les composants animés **doivent** respecter `prefers-reduced-motion`. Les ## Workflow d'édition 1. Lire `docs/README.md` puis `docs/01-analyse-et-plan.md` + `docs/02-moodboard-freeparty.md` pour la direction artistique -2. Pour un changement de contenu → éditer `public/content/*.json` (ou via `/admin`) +2. Pour un changement de contenu → éditer `data/content/*.json` (ou via `/admin`). Les seeds dans `public/content/*.json` ne sont utilisés qu'au premier run (bootstrap) 3. Pour un changement de design → modifier `src/styles/global.css` + composants 4. Pour un changement structurel → modifier schéma Zod d'abord, puis composant, puis JSON diff --git a/package-lock.json b/package-lock.json index 814bacc..54a978a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,15 +1,16 @@ { "name": "portfolio", - "version": "0.0.1", + "version": "1.0.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "portfolio", - "version": "0.0.1", + "version": "1.0.0", "dependencies": { "@astrojs/node": "^10.0.5", "@astrojs/react": "^5.0.3", + "@hookform/resolvers": "^5.2.2", "@iconify-icon/react": "^3.0.3", "@react-three/drei": "^10.7.7", "@react-three/fiber": "^9.6.0", @@ -25,6 +26,7 @@ "lucide-react": "^1.8.0", "react": "^19.2.5", "react-dom": "^19.2.5", + "react-hook-form": "^7.75.0", "tailwindcss": "^4.2.2", "three": "^0.184.0", "zod": "^4.3.6" @@ -1084,6 +1086,18 @@ "node": ">=18" } }, + "node_modules/@hookform/resolvers": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/@hookform/resolvers/-/resolvers-5.2.2.tgz", + "integrity": "sha512-A/IxlMLShx3KjV/HeTcTfaMxdwy690+L/ZADoeaTltLx+CVuzkeVIPuybK3jrRfw7YZnmdKsVVHAlEPIAEUNlA==", + "license": "MIT", + "dependencies": { + "@standard-schema/utils": "^0.3.0" + }, + "peerDependencies": { + "react-hook-form": "^7.55.0" + } + }, "node_modules/@iconify-icon/react": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/@iconify-icon/react/-/react-3.0.3.tgz", @@ -2181,6 +2195,12 @@ "integrity": "sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg==", "license": "MIT" }, + "node_modules/@standard-schema/utils": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@standard-schema/utils/-/utils-0.3.0.tgz", + "integrity": "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==", + "license": "MIT" + }, "node_modules/@tailwindcss/node": { "version": "4.2.2", "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.2.2.tgz", @@ -6045,6 +6065,22 @@ "react": "^19.2.5" } }, + "node_modules/react-hook-form": { + "version": "7.75.0", + "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.75.0.tgz", + "integrity": "sha512-Ovv94H+0p3sJ7B9B5QxPuCP1u8V/cHuVGyH55cSwodYDtoJwK+fqk3vjfIgSX59I2U/bU4z0nRJ9HMLpNiWEmw==", + "license": "MIT", + "engines": { + "node": ">=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/react-hook-form" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17 || ^18 || ^19" + } + }, "node_modules/react-refresh": { "version": "0.18.0", "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.18.0.tgz", diff --git a/package.json b/package.json index 61681ff..173916e 100644 --- a/package.json +++ b/package.json @@ -17,6 +17,7 @@ "dependencies": { "@astrojs/node": "^10.0.5", "@astrojs/react": "^5.0.3", + "@hookform/resolvers": "^5.2.2", "@iconify-icon/react": "^3.0.3", "@react-three/drei": "^10.7.7", "@react-three/fiber": "^9.6.0", @@ -32,6 +33,7 @@ "lucide-react": "^1.8.0", "react": "^19.2.5", "react-dom": "^19.2.5", + "react-hook-form": "^7.75.0", "tailwindcss": "^4.2.2", "three": "^0.184.0", "zod": "^4.3.6" diff --git a/public/content/experiences.json b/public/content/experiences.json index 4f44e8e..4627b15 100644 --- a/public/content/experiences.json +++ b/public/content/experiences.json @@ -3,7 +3,7 @@ { "id": "almeria", "company": "Almeria", - "logo": "/img/company/almeria.png", + "logo": "/api/files/img/company/almeria.png", "role": "Développeur", "period": "Depuis 2023", "duration": "3 ans", @@ -20,7 +20,7 @@ { "id": "cerema", "company": "CEREMA", - "logo": "/img/company/cerema.png", + "logo": "/api/files/img/company/cerema.png", "role": "Technicien évaluation mesure trafic & mobilité", "period": "2021 - 2023", "duration": "2 ans", @@ -37,7 +37,7 @@ { "id": "sncf", "company": "Réseau SNCF", - "logo": "/img/company/sncf.png", + "logo": "/api/files/img/company/sncf.png", "role": "Agent télécom", "period": "2019 - 2021", "duration": "2 ans", @@ -54,7 +54,7 @@ { "id": "novoferm", "company": "Novoferm", - "logo": "/img/company/novoferm.png", + "logo": "/api/files/img/company/novoferm.png", "role": "Stage développeur", "period": "2019", "duration": "7 semaines", @@ -67,7 +67,7 @@ { "id": "pivotpoint", "company": "Pivot Point", - "logo": "/img/company/pivotpoint.png", + "logo": "/api/files/img/company/pivotpoint.png", "role": "Stage développeur", "period": "2018", "duration": "1 semaine", @@ -80,7 +80,7 @@ { "id": "actemium", "company": "Actemium", - "logo": "/img/company/actemium.png", + "logo": "/api/files/img/company/actemium.png", "role": "Stage développeur", "period": "2018", "duration": "1 semaine", diff --git a/public/content/profile.json b/public/content/profile.json index a96161a..8c1f9cc 100644 --- a/public/content/profile.json +++ b/public/content/profile.json @@ -1,18 +1,26 @@ { "name": "Johan Leroy", - "title": "Développeur Fullstack", + "title": "Développeur", "tagline": "Dev le jour, DJ la nuit", "bio": "Développeur fullstack passionné par les technos modernes. J'aime créer des expériences web performantes, propres et scalables — et accessoirement mixer de la tekno quand l'écran s'éteint.", "available": true, "availableLabel": "Ouvert aux opportunités", "hero": { - "photo": "/img/pp/pp3.png", - "photos": ["/img/pp/pp1.png", "/img/pp/pp2.png", "/img/pp/pp3.png"], - "roles": ["Dev", "DJ", "Dev"], + "photo": "/api/files/img/pp/pp3.png", + "photos": [ + "/api/files/img/pp/pp1.png", + "/api/files/img/pp/pp2.png", + "/api/files/img/pp/pp3.png" + ], + "roles": [ + "Dev", + "DJ", + "Dev" + ], "cta": [ { "label": "Télécharger mon CV", - "href": "/assets/cv.pdf", + "href": "/api/files/assets/cv.pdf", "variant": "primary", "external": true }, @@ -28,7 +36,8 @@ "email": "contact@johanleroy.fr", "github": "https://github.com/JohanLeroy", "gitea": "https://git.lidge.fr/johanleroy", + "linkedin": "https://www.linkedin.com/in/johan-leroy-472409180/", "website": "https://johanleroy.fr" }, - "cv": "/assets/cv.pdf" + "cv": "/api/files/assets/cv.pdf" } diff --git a/public/content/projects.json b/public/content/projects.json index 4574227..5f24ce4 100644 --- a/public/content/projects.json +++ b/public/content/projects.json @@ -5,7 +5,7 @@ "title": "Techos", "subtitle": "Plateforme hébergement & domaines", "description": "Plateforme client + admin de gestion d'hébergement web et de domaines pour une association, construite par-dessus WHMCS. Design system Liquid Glass dark + light mode.", - "image": "/img/projet/techos.png", + "image": "/api/files/img/projet/techos.png", "category": "web", "status": "live", "url": "https://techos-asso.fr", @@ -50,7 +50,7 @@ "title": "Liberty Pour La Vie", "subtitle": "Site associatif (archivé)", "description": "Développement d'un site web complet pour l'association Liberty Pour La Vie. Projet fermé, plus en ligne.", - "image": "/img/projet/lplv.png", + "image": "/api/files/img/projet/lplv.png", "category": "web", "status": "archived", "stack": ["Java Spring Boot", "Thymeleaf", "Bootstrap", "MariaDB"], @@ -61,7 +61,7 @@ "title": "Mon Voisin Geek", "subtitle": "Site vitrine (archivé)", "description": "Développement d'un site web vitrine simple pour Mon Voisin Geek. Projet fermé, plus en ligne.", - "image": "/img/projet/monvoisingeek.png", + "image": "/api/files/img/projet/monvoisingeek.png", "category": "web", "status": "archived", "stack": ["Angular", "Tailwind CSS", "Express.js", "MariaDB"], diff --git a/public/content/site.json b/public/content/site.json index df28dee..e9d0c33 100644 --- a/public/content/site.json +++ b/public/content/site.json @@ -16,7 +16,7 @@ "freelance", "nantes" ], - "ogImage": "/img/logo/android-chrome-512x512.png", + "ogImage": "/api/files/img/logo/android-chrome-512x512.png", "features": { "hero3D": false, "smoothScroll": true, diff --git a/src/components/Footer.astro b/src/components/Footer.astro index b688e27..49203b8 100644 --- a/src/components/Footer.astro +++ b/src/components/Footer.astro @@ -116,6 +116,20 @@ const year = new Date().getFullYear(); ) } + { + profile.socials.linkedin && ( +