feat(admin): création de membre via dashboard

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>
This commit is contained in:
2026-05-29 13:47:42 +02:00
parent dcf99f011a
commit 97c68531ed
3 changed files with 254 additions and 1 deletions
+10 -1
View File
@@ -62,13 +62,22 @@ const KIND_LABEL = {
</header>
<AdminSubNav />
<div class="mt-6 flex items-center gap-2">
<div class="mt-6 flex items-center justify-between gap-2 flex-wrap">
<input
v-model="searchQ"
type="search"
placeholder="Rechercher un membre…"
class="bg-donut-surface border border-donut-border rounded-lg px-3 py-2 text-sm outline-none focus:border-donut-text w-64"
/>
<RouterLink
:to="{ name: 'admin-member-new' }"
class="flex items-center gap-1.5 bg-donut-text text-white rounded-lg px-3.5 py-2 text-sm font-medium hover:bg-black transition-colors"
>
<svg viewBox="0 0 24 24" class="w-4 h-4" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round">
<path d="M12 5v14M5 12h14" />
</svg>
Nouveau membre
</RouterLink>
</div>
<LoadingState v-if="loading" class="mt-8" />