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/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 && ( +