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 @@ + + +