From adb2bd3fc17c9a9f883dc9c962af6d6ad0f45fb8 Mon Sep 17 00:00:00 2001 From: Arnaud Date: Thu, 28 May 2026 23:07:59 +0200 Subject: [PATCH] feat: galerie d'images projet (upload disque) + dashboard /admin MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ==== Galerie d'images ==== Backend - Migration 003 : table project_images (FK CASCADE sur projects) - ImagesController : CRUD complet, deux modes de création * URL externe via JSON body * Upload disque via multipart/form-data avec : - validation MIME RÉELLE via finfo (jpeg/png/webp/gif) - taille max 5 Mo, vérifiée explicitement (au-delà du php.ini) - vérification UPLOAD_ERR_* avec messages clairs - stockage dans public/uploads/projects/:slug/. - dimensions image extraites via getimagesize (best-effort) - row DB avec mime_type / size_bytes / width / height - Request : ajout isMultipart(), formField(), file() - public/index.php : 3 routes (index public, store/update/destroy admin) Proxy - caddy.conf : /uploads/* ajouté au reverse_proxy comme /api/* - vite.config.ts : pareil pour le dev local Frontend - Type ProjectImage (api.ts) avec url + storage_path + dimensions - uploadFile() helper dans client.ts (FormData + Bearer) - ImageGallery.vue : grille thumbnails + lightbox plein-écran avec nav clavier (← → Esc), bouton supprimer par image en mode admin - ImageUploader.vue : drag-drop + file picker, multi-fichiers en série, validation taille côté client, légende commune optionnelle - ProjectDetailPage : nouvel onglet Images entre Roadmap et Discussions ==== Dashboard /admin ==== 5 nouvelles pages sous /admin : - /admin : KPI + répartition catégorie + flux activité - /admin/projects : table CRUD (recherche + filtres status/categorie), actions Éditer / Archiver / Supprimer par ligne - /admin/projects/:slug/edit : form complet d'édition (envoie un diff au PATCH) - /admin/members : annuaire avec recherche + Supprimer - /admin/categories : table avec compte de projets ; Supprimer désactivé si la catégorie est utilisée - AdminSubNav.vue : 4 onglets en haut de chaque page admin Garde d'auth : - beforeEnter sur les routes /admin/* (sauf /admin/login) - Redirige vers /admin/login?next= si pas de token - Après login, retour automatique vers next= (sinon /admin) Sidebar : - Nouvelle section "Administration" visible UNIQUEMENT quand le token est en localStorage, avec un lien vers le dashboard Vérifié localement - Build vue-tsc : 91 modules, OK - Upload disque : fichier PNG stocké dans public/uploads/, accessible via /uploads/ - GET /api/projects/nos-ecoles/images : 2 entrées renvoyées (1 URL + 1 disque) - Toutes les routes /admin/* répondent 200 via le proxy Vite Co-Authored-By: Claude Opus 4.7 (1M context) --- caddy.conf | 9 +- frontend/src/api/client.ts | 42 +++ frontend/src/components/AdminSubNav.vue | 41 +++ frontend/src/components/AppSidebar.vue | 27 ++ frontend/src/components/ImageGallery.vue | 195 +++++++++++++ frontend/src/components/ImageUploader.vue | 137 +++++++++ frontend/src/pages/AdminLoginPage.vue | 23 +- frontend/src/pages/ProjectDetailPage.vue | 16 +- .../src/pages/admin/AdminCategoriesPage.vue | 110 +++++++ frontend/src/pages/admin/AdminMembersPage.vue | 128 ++++++++ .../src/pages/admin/AdminOverviewPage.vue | 99 +++++++ .../src/pages/admin/AdminProjectEditPage.vue | 275 ++++++++++++++++++ .../src/pages/admin/AdminProjectsPage.vue | 199 +++++++++++++ frontend/src/router/index.ts | 21 ++ frontend/src/types/api.ts | 16 + frontend/vite.config.ts | 5 + migrations/003_add_project_images.sql | 27 ++ public/index.php | 6 + src/Controllers/ImagesController.php | 271 +++++++++++++++++ src/Request.php | 36 +++ 20 files changed, 1671 insertions(+), 12 deletions(-) create mode 100644 frontend/src/components/AdminSubNav.vue create mode 100644 frontend/src/components/ImageGallery.vue create mode 100644 frontend/src/components/ImageUploader.vue create mode 100644 frontend/src/pages/admin/AdminCategoriesPage.vue create mode 100644 frontend/src/pages/admin/AdminMembersPage.vue create mode 100644 frontend/src/pages/admin/AdminOverviewPage.vue create mode 100644 frontend/src/pages/admin/AdminProjectEditPage.vue create mode 100644 frontend/src/pages/admin/AdminProjectsPage.vue create mode 100644 migrations/003_add_project_images.sql create mode 100644 src/Controllers/ImagesController.php diff --git a/caddy.conf b/caddy.conf index 3d93398..b650460 100644 --- a/caddy.conf +++ b/caddy.conf @@ -15,9 +15,12 @@ infolab.ledonut-marseille.org { -Server } - # API JSON -> conteneur PHP (sur réseau Docker local, exposé en 127.0.0.1:8003) - @api path /api/* - handle @api { + # API JSON et images uploadées -> conteneur PHP (sur donut-net, exposé 127.0.0.1:8003). + # /uploads/* contient les images de la galerie projet stockées sur disque par + # l'admin via POST /api/projects/:slug/images (multipart). Apache du conteneur + # les sert en static — pas besoin de PHP juste pour servir un fichier. + @apiOrUploads path /api/* /uploads/* + handle @apiOrUploads { reverse_proxy 127.0.0.1:8003 { header_up Host {host} header_up X-Real-IP {remote_host} diff --git a/frontend/src/api/client.ts b/frontend/src/api/client.ts index cf1d2a0..c20f801 100644 --- a/frontend/src/api/client.ts +++ b/frontend/src/api/client.ts @@ -116,3 +116,45 @@ export const api = { patch: (path: string, body: Json, opts?: RequestOptions) => request('PATCH', path, body, opts), delete: (path: string, opts?: RequestOptions) => request('DELETE', path, undefined, opts), } + +/** + * Upload multipart/form-data — utilisé par la galerie projet. + * + * Le navigateur fixe lui-même le Content-Type avec le bon boundary, on ne le + * définit donc PAS à la main (sinon le boundary serait absent et le backend ne + * saurait pas découper le body). + * + * @param path path vers l'endpoint, ex: '/api/projects/nos-ecoles/images' + * @param fields champs texte additionnels (caption, sort_order, ...) + * @param file le fichier (issu d'un ou drop) + * @param fileFieldName nom du champ côté serveur (défaut 'file') + */ +export async function uploadFile( + path: string, + file: File, + fields: Record = {}, + fileFieldName = 'file', +): Promise { + const form = new FormData() + form.append(fileFieldName, file, file.name) + for (const [k, v] of Object.entries(fields)) { + if (v !== undefined && v !== null && v !== '') { + form.append(k, String(v)) + } + } + + const headers: Record = { Accept: 'application/json' } + const token = getAdminToken() + if (token) headers['Authorization'] = `Bearer ${token}` + + const res = await fetch(path, { method: 'POST', headers, body: form }) + + if (res.status === 204) return undefined as T + const text = await res.text() + const data = text === '' ? null : (JSON.parse(text) as unknown) + if (!res.ok) { + const payload = (data ?? { error: 'internal_server_error' }) as ApiErrorPayload + throw new ApiError(res.status, payload.error ?? `http_${res.status}`, payload) + } + return data as T +} diff --git a/frontend/src/components/AdminSubNav.vue b/frontend/src/components/AdminSubNav.vue new file mode 100644 index 0000000..86088a4 --- /dev/null +++ b/frontend/src/components/AdminSubNav.vue @@ -0,0 +1,41 @@ + + + diff --git a/frontend/src/components/AppSidebar.vue b/frontend/src/components/AppSidebar.vue index 81955de..5cc80af 100644 --- a/frontend/src/components/AppSidebar.vue +++ b/frontend/src/components/AppSidebar.vue @@ -121,6 +121,33 @@ const navItems: NavLink[] = [ {{ item.label }} + + + diff --git a/frontend/src/components/ImageGallery.vue b/frontend/src/components/ImageGallery.vue new file mode 100644 index 0000000..81b1606 --- /dev/null +++ b/frontend/src/components/ImageGallery.vue @@ -0,0 +1,195 @@ + + + diff --git a/frontend/src/components/ImageUploader.vue b/frontend/src/components/ImageUploader.vue new file mode 100644 index 0000000..622a01f --- /dev/null +++ b/frontend/src/components/ImageUploader.vue @@ -0,0 +1,137 @@ + + + diff --git a/frontend/src/pages/AdminLoginPage.vue b/frontend/src/pages/AdminLoginPage.vue index 61287c4..128d9f3 100644 --- a/frontend/src/pages/AdminLoginPage.vue +++ b/frontend/src/pages/AdminLoginPage.vue @@ -1,11 +1,22 @@ + + diff --git a/frontend/src/pages/admin/AdminMembersPage.vue b/frontend/src/pages/admin/AdminMembersPage.vue new file mode 100644 index 0000000..db6147b --- /dev/null +++ b/frontend/src/pages/admin/AdminMembersPage.vue @@ -0,0 +1,128 @@ + + + diff --git a/frontend/src/pages/admin/AdminOverviewPage.vue b/frontend/src/pages/admin/AdminOverviewPage.vue new file mode 100644 index 0000000..f89c54d --- /dev/null +++ b/frontend/src/pages/admin/AdminOverviewPage.vue @@ -0,0 +1,99 @@ + + + diff --git a/frontend/src/pages/admin/AdminProjectEditPage.vue b/frontend/src/pages/admin/AdminProjectEditPage.vue new file mode 100644 index 0000000..c0dec10 --- /dev/null +++ b/frontend/src/pages/admin/AdminProjectEditPage.vue @@ -0,0 +1,275 @@ + + +