adb2bd3fc1
==== 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/<hash>.<ext>
- 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=<url> 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) <noreply@anthropic.com>
57 lines
2.9 KiB
TypeScript
57 lines
2.9 KiB
TypeScript
/**
|
|
* Routes de l'app. AppLayout englobe toutes les pages "public" (sidebar visible).
|
|
* /admin/login a son propre layout vide (pas de sidebar tant qu'on n'est pas loggé).
|
|
*/
|
|
|
|
import { createRouter, createWebHistory, type RouteRecordRaw } from 'vue-router'
|
|
import AppLayout from '@/layouts/AppLayout.vue'
|
|
import { getAdminToken } from '@/api/client'
|
|
|
|
/**
|
|
* Guard pour les routes /admin/* (hors /admin/login). Si pas de token Bearer
|
|
* en localStorage, redirige vers /admin/login (en mémorisant la destination
|
|
* via query.next pour qu'on y retourne après auth).
|
|
*/
|
|
function requireAdmin(to: { fullPath: string }) {
|
|
if (!getAdminToken()) {
|
|
return { name: 'admin-login', query: { next: to.fullPath } }
|
|
}
|
|
return true
|
|
}
|
|
|
|
const routes: RouteRecordRaw[] = [
|
|
{
|
|
path: '/',
|
|
component: AppLayout,
|
|
children: [
|
|
{ path: '', name: 'home', component: () => import('@/pages/HomePage.vue') },
|
|
{ path: 'dashboard', name: 'dashboard', component: () => import('@/pages/DashboardPage.vue') },
|
|
{ path: 'donut', name: 'donut', component: () => import('@/pages/DonutPage.vue') },
|
|
{ path: 'projects', name: 'projects', component: () => import('@/pages/ProjectsPage.vue') },
|
|
{ path: 'projects/new', name: 'project-new', component: () => import('@/pages/ProjectNewPage.vue') },
|
|
{ path: 'projects/:slug', name: 'project-detail', component: () => import('@/pages/ProjectDetailPage.vue'), props: true },
|
|
{ path: 'archives', name: 'archives', component: () => import('@/pages/ArchivesPage.vue') },
|
|
{ path: 'categories', name: 'categories', component: () => import('@/pages/CategoriesPage.vue') },
|
|
|
|
// --- Admin dashboard (token Bearer requis pour toutes les sous-pages sauf /admin/login) ---
|
|
{ path: 'admin', name: 'admin-overview', component: () => import('@/pages/admin/AdminOverviewPage.vue'), beforeEnter: requireAdmin },
|
|
{ path: 'admin/projects', name: 'admin-projects', component: () => import('@/pages/admin/AdminProjectsPage.vue'), beforeEnter: requireAdmin },
|
|
{ path: 'admin/projects/:slug/edit', name: 'admin-project-edit', component: () => import('@/pages/admin/AdminProjectEditPage.vue'), props: true, beforeEnter: requireAdmin },
|
|
{ path: 'admin/members', name: 'admin-members', component: () => import('@/pages/admin/AdminMembersPage.vue'), beforeEnter: requireAdmin },
|
|
{ path: 'admin/categories', name: 'admin-categories', component: () => import('@/pages/admin/AdminCategoriesPage.vue'), beforeEnter: requireAdmin },
|
|
|
|
{ path: 'admin/login', name: 'admin-login', component: () => import('@/pages/AdminLoginPage.vue'), meta: { hideForAuthed: true } },
|
|
{ path: ':pathMatch(.*)*', name: 'not-found', component: () => import('@/pages/NotFoundPage.vue') },
|
|
],
|
|
},
|
|
]
|
|
|
|
export const router = createRouter({
|
|
history: createWebHistory(),
|
|
routes,
|
|
scrollBehavior() {
|
|
// Scroll en haut à chaque navigation (UX par défaut pour une SPA de contenu).
|
|
return { top: 0 }
|
|
},
|
|
})
|