La fiche projet publique /projects/:slug avait déjà des actions inline
en mode admin (Supprimer, ajouter étape, ajouter ressource, etc.), mais
pas de raccourci pour basculer vers le form d'édition complet où on
peut changer titre, slug, catégorie, status, descriptions, progression.
Ajout d'un bouton "Éditer" à côté de "Supprimer" en haut à droite (admin
only) qui pointe vers /admin/projects/:slug/edit. Style outline blanc
pour ne pas concurrencer visuellement le rouge de Supprimer.
Cohérent avec les autres tableaux admin où le clic sur le titre ouvre
maintenant l'édition.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Cohérence finale des 3 tableaux admin : le clic sur l'élément principal
de la 1re colonne pointe maintenant sur l'écran d'édition admin
correspondant pour les projets, les membres et les catégories.
Avant : project-detail (fiche publique). Maintenant : admin-project-edit.
Le lien "Éditer" en bout de ligne reste pour la découvrabilité, et la
fiche publique reste accessible depuis la nav globale ou /projects.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Cohérence avec /admin/members et /admin/projects : le nom dans la
1re colonne est maintenant un RouterLink vers `admin-category-edit`.
Le bouton "Éditer" en bout de ligne reste pour la découvrabilité.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Le lien sur display_name dans la liste /admin/members pointait sur
`project-detail` avec le slug du MEMBRE comme :slug -> aboutissait sur
une 404 ou la mauvaise fiche projet selon les collisions de slug.
Maintenant : pointe sur `admin-member-edit`. Plus cohérent avec le pattern
utilisé pour les projets (clic sur titre -> fiche) et avec ce que l'admin
attend dans un tableau d'admin (clic = édition).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Nouvelle page /admin/members/new, jumeau de l'écran d'édition :
- Form complet : display_name, slug (auto), initials (auto), kind, bio,
email, website, avatar URL
- Avatar preview live (initiales / URL d'avatar mis à jour à la saisie)
- Slug auto-généré depuis display_name (NFD + lowercase + tirets)
tant que l'utilisateur n'a pas édité manuellement (flag slugDirty)
- Initiales auto-suggérées (1re lettre de chaque mot, max 3, upper),
flag initialsDirty distinct -> tu peux laisser le slug auto mais
forcer des initiales manuelles ou vice-versa
- Body construit en omettant les champs vides -> l'API génère ses propres
initiales si vides, slug auto-uniqueifie sur collision (-2/-3...)
- 201 -> redirige vers /admin/members/:slug/edit pour voir le résultat
- 422/409/401 gérés comme les autres écrans admin
UI
- AdminMembersPage : CTA "Nouveau membre" en haut à droite (à côté de
la recherche, justify-between sur le wrap parent)
- Router : route nommée admin-member-new avec guard admin
Backend
- Aucune nouvelle route : POST /api/members existait déjà
- Le contrôleur valide display_name (required), kind enum, email format,
URLs ; auto-génère initiales si non fournies (1res lettres de chaque mot)
- Slug auto avec suffixe en cas de collision (cf. Slugger::unique)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Nouvelle page /admin/categories/new, jumeau de l'écran d'édition :
- Form complet : name, slug (auto), description, color_hex, icon,
dimension, sort_order
- Preview live identique à l'éditeur (top border + folder tinté + badge)
- Slug auto-généré depuis le nom (normalisation NFD + lowercase + dashes)
tant que l'utilisateur n'a pas touché manuellement au champ. Flag
slugDirty mis à true sur premier @input du slug -> on arrête l'auto.
- Validation color_hex /^#RRGGBB$/ côté client + normalisation upper-case
- Body construit en omettant les champs vides -> l'API applique ses
défauts (color_hex='#888888', dimension='transversal', sort_order=0)
- Sur 201 : redirige vers /admin/categories/:slug/edit pour que l'admin
voie tout de suite sa nouvelle catégorie et puisse ajuster
- 422/409/401 gérés comme sur les autres écrans admin
UI
- AdminCategoriesPage : CTA "Nouvelle catégorie" en haut à droite
- Router : route nommée admin-category-new avec guard admin
Backend
- Aucune nouvelle route : POST /api/categories existait déjà
- Le contrôleur valide name (required, max 128), dimension enum, color
hex format, slug auto-unique
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Nouvelle page /admin/categories/:slug/edit, même pattern que members :
- Preview live de la carte catégorie (top border colorée + folder icon
tinté + badge "X projets" + label dimension)
- Form complet : name, slug, description, color_hex, icon, dimension, sort_order
- Color picker natif <input type="color"> + champ HEX texte synchronisés,
validation /^#[0-9A-Fa-f]{6}$/ côté client
- Diff PATCH (champs vides -> null), upper-case normalisé pour color_hex
- Si le slug change -> router.replace vers la nouvelle URL
- Bouton Supprimer en header désactivé si projects_count > 0 (FK RESTRICT)
- 422 -> fieldErrors, 409 -> slug collision, 401 -> session expirée
- projects_count récupéré direct depuis GET /api/categories/:slug
(la réponse contient ce champ pour la route détail, cf. controller PHP)
UI
- AdminCategoriesPage : nouveau lien "Éditer" par ligne du tableau
- Router : route nommée admin-category-edit avec guard admin
Backend
- Aucune nouvelle route : PATCH /api/categories/:slug existait déjà
- Validation côté serveur : in dimension enum, colorHex format,
integer sort_order >= 0, slug auto-unique avec suffixe -2/-3...
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Nouvelle page /admin/members/:slug/edit avec :
- Preview avatar live (initiales/URL mis à jour à la saisie)
- Form complet : display_name, slug, initials, kind, bio, email, website, avatar
- Envoie un diff au PATCH (champs vides -> null, pas chaîne vide)
- Si le slug change, redirige automatiquement vers la nouvelle URL
- Encart "Projets attachés" en lecture (avec lien vers chaque fiche)
pour rappeler l'impact d'une suppression ou d'un changement de slug
- Bouton Supprimer en header avec confirm qui mentionne le nb de projets
- Banner vert "Modifications enregistrées" sur succès
- Gestion erreurs : 422 -> fieldErrors, 401, 409 (slug collision), réseau
Backend
- Aucune nouvelle route : PATCH /api/members/:slug existait déjà
- Le contrôleur gère déjà slug auto-unique, initials, kind enum, etc.
UI
- AdminMembersPage : nouveau lien "Éditer" par ligne, à côté de Supprimer
- Router : route nommée admin-member-edit avec guard admin
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Backend
- ProjectMembersController::update : nouveau PATCH /api/projects/:slug/members/:member_slug
pour changer le rôle (admin/contributor/observer). 404 si pivot absent,
no-op idempotent si déjà même rôle, 422 si rôle invalide. Route câblée
dans public/index.php.
Frontend
- JoinProjectModal : 2 onglets (Membre existant / Nouveau membre)
* "existant" : <select> de l'annuaire filtré sur excludeSlugs (membres
déjà sur le projet), preview avatar+bio du membre sélectionné
* "nouveau" : flow d'origine (créer le membre puis l'attacher)
* sélecteur de rôle visible dans les 2 modes
* onJoined remonte un Member complet (au lieu de {id,slug,name})
-> ProjectDetailPage peut pousser un ProjectMemberEntry avec ses
vraies initials/avatar/kind dans la liste optimiste
- ProjectDetailPage panneau Équipe (mode admin uniquement) :
* Rôle inline éditable via <select> stylé minimal -> PATCH optimiste
avec rollback en cas d'échec
* Bouton retirer-membre par ligne (DELETE) avec confirm
* Le badge "Vous" / "Admin connecté" ne change pas
Code-review (high effort, --fix appliqué) :
- loadDirectory affiche maintenant une vraie erreur réseau au lieu de
silencieusement présenter "tous les membres déjà sur ce projet"
- loadDirectory est lazy : on ne fetche l'annuaire que si l'onglet
"existant" est actif (économise 1 requête si admin saute direct à "nouveau")
- Aperçu du membre sélectionné : computed `selectedMember` au lieu de
<template v-for><template v-if>
- Bouton retirer-membre : opacity-60 sur mobile (sm: et plus garde le
behavior hover-to-reveal) -> reste atteignable sur touch
- + label `aria-label` pour l'accessibilité
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
==== 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>
Les noms étaient tout à l'extérieur (R=275), au-delà des limites planétaires.
L'utilisateur veut "sur le cercle intermédiaire entre les camemberts des
catégories et les limites planétaires" — c'est-à-dire SUR l'anneau gris du
plafond écologique qui sépare la zone sûre (où vivent les wedges colorés)
des limites planétaires.
- R_CAT_LABEL : 275 → 212 (milieu de l'anneau gris 200..225)
- viewBox : retour à 560×560 (plus besoin d'étendre à R=275)
- Cream background : 295 → 270 (idem)
- Font-size : 11 → 10 (plus discret sur ring)
- paint-order + stroke #FAFAF6 sur le texte : "halo" cream autour des
lettres pour rester lisibles malgré le pattern strié du ring gris.
Plus simple qu'un mask et fonctionne dans tous les browsers modernes.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Les libellés des 6 catégories thématiques (Data, Démocratie, Habitat,
Territoire, Écologie, Éducation) étaient posés HORIZONTAUX au milieu
de leur wedge dans la zone sûre. Ils chevauchaient les points-projets
quand on en avait beaucoup et compliquaient la lecture.
Déplacement à R=275, ARC autour du donut, dans la couleur de la catégorie :
- Nouvelle constante R_CAT_LABEL (la plus extérieure : > planétaires à 252)
- viewBox 600x600 (vs 560x560) pour caser la nouvelle frontière
- Fond cream étendu à R=295 pour soutenir les labels jusqu'au bord
- Font-size 11 + weight 700 + lettre-espacement 0.5px pour lisibilité
- Réutilisation du helper arcLabelPaths (halfSpan 28° = quasi-pleine
largeur du secteur de 60°)
- Couleur récupérée par index depuis la liste categories réactive
Suppression du rendu in-wedge des labels catégorie : redondance avec les
arcs extérieurs + meilleure lisibilité des points-projets dans la zone sûre.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Beaucoup de dimensions Raworth dépassent l'arc disponible à R=88 :
« Revenu & travail », « Liens sociaux », « Égalité de genre »,
« Équité sociale », « Voix politique », « Paix & justice ». Posées en
une seule ligne, le texte débordait ou était illisible.
Solution : permettre 2 lignes par libellé, chaque ligne ayant son propre
arc à un rayon décalé. Du coup ces 6 dimensions sont séparées en 2 mots
empilés radialement. Les 6 autres (Alimentation, Santé, Éducation, Eau,
Énergie, Logement) restent sur une seule ligne.
Refactor :
- Type `Label = readonly string[]` (1..N lignes)
- PLANETARY_BOUNDARIES et SOCIAL_FOUNDATIONS unifiés au format tableau
de tableaux (planétaires restent toutes à 1 ligne, sociales mixtes)
- arcLabelPaths() gère le multi-lignes : une ArcLine par ligne, avec
décalage radial de ±lineSpacing/2
- lineSpacing = 9u (légèrement > font-size 8 pour éviter le chevauchement)
- Inversion du décalage radial selon la moitié du donut pour que la
ligne "primaire" reste toujours du côté lecteur (haut de l'écran) :
top : ligne 0 = R+offset (extérieur)
bot : ligne 0 = R-offset (intérieur)
- Aplatissement via flatMap pour rendre comme une flat-list dans le
SVG (pas de `<template>` imbriqué)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Le plancher social et ses 12 dimensions étaient à l'étroit autour de
R=67 (anneau intérieur 78..100). Maintenant :
- R_SOCIAL_LABEL passe de 67 à 88
-> circonférence du cercle où sont posés les labels = 2π·88 ≈ 553u
-> chaque label dispose de ~46u d'arc (contre ~35 avant)
- R_FLOOR_INNER passe de 78 à 102
- R_FLOOR_OUTER passe de 100 à 125
-> l'anneau orange du plancher social est visuellement plus présent
- R_CENTER passe de 50 à 55 (pour ne pas écraser le "LE DONUT")
- Font-size des labels sociaux : 6 → 8 (plus lisible avec l'arc élargi)
L'espace sûr (R_FLOOR_OUTER..R_CEILING_INNER) passe de 100u à 75u : les
trois bandes de positionnement des projets (social / transversal /
écologique) sont recalibrées en conséquence sans chevauchement.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Limites planétaires + dimensions sociales : labels qui épousent vraiment la
courbure de l'anneau au lieu d'être posés en ligne droite tangente. Implémenté
avec SVG <textPath> + un <path> caché par label.
Petit piège : pour qu'un label en bas du donut reste lisible (pas écrit à
l'envers ou de droite à gauche), il faut INVERSER la direction du path selon
la moitié.
Helper arcLabelPaths() :
- Moitié haute (angles ~0°) : path va de θ-δ vers θ+δ, sweep=1
- Moitié basse (angles ~180°) : path va de θ+δ vers θ-δ, sweep=0
→ la direction du chemin au point central du label est toujours "horizontale
à gauche-droite" du point de vue d'un lecteur fixe.
Ajouts :
- 12 dimensions du plancher social Raworth (Alimentation, Santé, Éducation,
Revenu & travail, Eau, Énergie, Liens sociaux, Logement, Égalité de genre,
Équité sociale, Voix politique, Paix & justice) placées en arc entre le
centre et l'anneau intérieur (rayon 67)
- R_CENTER passé de 56 à 50 pour libérer 6 unités au profit des labels
sociaux sans toucher au "LE DONUT / X projets" au centre
- Suppression des textes centrés "PLAFOND ÉCOLOGIQUE" et "PLANCHER SOCIAL"
sur les anneaux (redondants avec les labels qui les détaillent)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Le pattern <RouterLink v-slot="..."><a @click.prevent>...</a></RouterLink>
sans le prop `custom` rend DEUX <a> imbriqués : celui de RouterLink (qui gère
la navigation) + celui du slot. Notre @click.prevent sur le <a> interne
bloquait le clic avant qu'il remonte au <a> externe. Résultat : clic mort.
Fix : ajout du prop `custom` (RouterLink ne rend plus son <a>) +
@click="navigate" (fonction fournie par le v-slot qui fait le router.push
en gérant les modifier keys, le ctrl-clic, etc.).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1. Jitter au survol (bug visible) :
`transform: scale(1.5)` en SVG prend pour origine le (0,0) du viewBox,
pas le centre du <circle>. Le scale "déplaçait" donc le cercle vers le centre
de la viewport → la souris n'était plus dessus → mouseleave → retour →
mouseenter → re-déplacé. Boucle infinie de tremblement.
Fix : classe `.dot-circle` avec
transform-box: fill-box;
transform-origin: center;
Le scale prend désormais pour pivot le centre de la forme. Stable.
2. Couleurs des secteurs thématiques (par la maquette d'origine) :
Réintroduction des wedges colorés dans la zone sûre, opacité 10 % sur
le fill + 20 % sur le stroke. Reste sobre, garde la lisibilité.
3. Limites planétaires (de Kate Raworth) :
Les 9 limites planétaires (Changement climatique, Acidification des
océans, Biodiversité, Eau douce, Cycles N&P, Pollution chimique,
Pollution de l'air, Couche d'ozone, Usage des sols) sont placées en
étiquettes tangentes autour de l'anneau extérieur, avec flip 180° en
moitié basse pour rester lisibles.
Cosmétique :
- viewBox 560×560 (au lieu de 520) pour caser les labels extérieurs
- Labels catégorie déplacés AU MILIEU de leur wedge (sur la zone sûre)
plutôt qu'à l'extérieur (où ils chevauchaient les limites planétaires)
- Séparateurs radiaux passés en blanc pour mieux contraster avec les
wedges colorés
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Donut (DonutPage.vue) — alignement avec la maquette "Capture 2026-05-28 205310" :
- Suppression des secteurs en couleur vive : 6 secteurs séparés par des
lignes radiales en pointillé gris léger
- 2 anneaux gris texturés (pattern diagonal) : "Plafond écologique" (extérieur)
et "Plancher social" (intérieur), avec leurs labels
- Espace sûr cream entre les deux où vivent les projets
- Positionnement des projets :
angle : secteur de la catégorie + jitter stable (hash slug)
rayon : bande définie par la dimension de la catégorie
(social -> 108..140, ecologique -> 160..192, transversal -> 142..158)
puis modulé par progress_percent (plus avancé = plus éloigné du centre)
- Projets archivés rendus en POINTS CREUX (stroke only, sans fill)
- Tooltip survol -> RouterLink cliquable vers la fiche projet
- Légende droite réécrite avec les phrases exactes de la maquette
("L'anneau intérieur représente le plancher social, ..." etc.)
- Liste des catégories par dimension avec compte de projets cliquables
Jeu d'essai (tests/seed-demo.sh) :
- 10 actifs / 6 réalisés / 4 archivés (= 20 projets), réalistes Marseille
- 10 membres (4 collectives/orgs, 4 personnes, 2 collectives + AMU)
- 26 liens project_members, 27 étapes de roadmap (mix done/à faire)
- 6 discussions, 8 ressources, 8 events d'activité antérieurs
- Lit DB_* depuis .env, force --default-character-set=utf8mb4
- TRUNCATE+INSERT idempotent (la table categories est préservée)
- Hors /migrations/ pour ne PAS être appliqué automatiquement par apply.sh
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Le client mysql sous Windows + Git Bash utilise cp850 comme
`character_set_client` par défaut. Quand on applique 002_seed_categories.sql
avec un simple `mysql < ...`, le client réinterprète les bytes UTF-8 du fichier
via cp850 AVANT de les insérer : `é` (0xC3 0xA9) devient `├®` (E2949C C2AE)
stocké en clair dans la base, et apparaît tel quel dans l'API + le SPA.
Le `SET NAMES utf8mb4` côté serveur ne corrige pas ça parce que la conversion
est déjà faite côté client. Il faut le flag `--default-character-set=utf8mb4`
sur la ligne de commande mysql.
- README.md : doc de la commande correcte + commande de vérif HEX
- 002_seed_categories.sql : commentaire d'alerte + SET NAMES utf8mb4 (no-op
côté connexion mais documente l'intention)
Côté prod (apply.sh + docker exec mariadb) le client interne utilise utf8mb4
par défaut, donc pas de souci là-bas.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>