Gestion des rencontres passées + interdiction des actions destructrices aux modérateurs
Rencontres passées (section « Ils y étaient » de l'accueil) : - Nouvel onglet « Rencontres passées » (admin + modérateur) : création, édition, jusqu'à 3 photos par rencontre (1 grande + 2 petites, upload après enregistrement — même schéma que la photo des rencontres à venir) - Nouvelles colonnes rencontres_passees.image_path/_2/_3 (migration ALTER TABLE IF NOT EXISTS) - Home.jsx affiche désormais les vraies photos uploadées (repli sur le motif placeholder si absentes) ; ordre passé de ORDER BY id à ORDER BY id DESC (les plus récentes en premier, seed réordonné en conséquence pour préserver l'affichage actuel) - Suppression réservée aux administrateurs Modérateurs : plus aucune action destructrice possible, imposé côté serveur (adminOnly) et masqué côté interface : - Validation/suspension d'un membre (toggle-valide) - Suppression d'une rencontre, d'une rencontre passée, d'une inscription Vérifié : 34 + 21 + 26 tests existants toujours au vert, 21 nouveaux tests (CRUD rencontres passées, upload des 3 emplacements photo, ordre d'affichage, restrictions modérateur sur chaque action désormais admin- only), parcours navigateur complet (upload des 3 photos, rendu réel sur la page d'accueil). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Fkg15RCxNgUys4ru73He2o
This commit is contained in:
11 files changed
+426
-55
No files matched your search
@@ -4,15 +4,18 @@ import { useAuth } from '../lib/AuthContext.jsx';
|
||||
import { api, dateParts, statutLabel } from '../lib/api.js';
|
||||
import { MembersTab, RencontresTab, InscriptionsTab } from '../components/admin/AdminTabs.jsx';
|
||||
import { CategoriesTab, ContenuTab } from '../components/admin/AdminContent.jsx';
|
||||
import { PastEventsTab } from '../components/admin/AdminPast.jsx';
|
||||
import { UsersTab } from '../components/admin/AdminUsers.jsx';
|
||||
|
||||
// Moderators only get members/rencontres/inscriptions — everything else
|
||||
// (dashboard, taxonomy, site content, staff account management) is
|
||||
// admin-only, enforced both here (sidebar) and server-side (routes).
|
||||
// Moderators only get members/rencontres/inscriptions/rencontres passées
|
||||
// (create + edit, never delete) — everything else (dashboard, taxonomy,
|
||||
// site content, staff account management) is admin-only, enforced both
|
||||
// here (sidebar) and server-side (routes).
|
||||
const TABS = [
|
||||
{ key: 'dashboard', icon: '◧', label: 'Tableau de bord', title: 'Tableau de bord', roles: ['admin'] },
|
||||
{ key: 'membres', icon: '▤', label: 'Membres', title: 'Gestion des membres', roles: ['admin', 'moderator'] },
|
||||
{ key: 'rencontres', icon: '◈', label: 'Rencontres', title: 'Rencontres', roles: ['admin', 'moderator'] },
|
||||
{ key: 'passees', icon: '▦', label: 'Rencontres passées', title: 'Rencontres passées', roles: ['admin', 'moderator'] },
|
||||
{ key: 'inscriptions', icon: '✎', label: 'Inscriptions', title: 'Inscriptions', roles: ['admin', 'moderator'] },
|
||||
{ key: 'categories', icon: '☲', label: 'Catégories', title: 'Catégories', roles: ['admin'] },
|
||||
{ key: 'contenu', icon: '▧', label: 'Contenu du site', title: 'Contenu du site', roles: ['admin'] },
|
||||
@@ -143,6 +146,7 @@ export function AdminShell() {
|
||||
{tab === 'dashboard' && <Dashboard />}
|
||||
{tab === 'membres' && <MembersTab />}
|
||||
{tab === 'rencontres' && <RencontresTab />}
|
||||
{tab === 'passees' && <PastEventsTab />}
|
||||
{tab === 'inscriptions' && <InscriptionsTab />}
|
||||
{tab === 'categories' && <CategoriesTab />}
|
||||
{tab === 'contenu' && <ContenuTab />}
|
||||
|
||||
+13
-4
@@ -213,7 +213,10 @@ export default function Home() {
|
||||
const restantes = Math.max(0, e.places - e.inscrits);
|
||||
return (
|
||||
<div key={e.id} className="card" style={{ overflow: 'hidden', display: 'flex', flexDirection: 'column' }}>
|
||||
<div className="placeholder-pattern" style={{ height: 170, position: 'relative' }}>
|
||||
<div className={e.image_path ? '' : 'placeholder-pattern'} style={{ height: 170, position: 'relative' }}>
|
||||
{e.image_path && (
|
||||
<img src={e.image_path} alt="" style={{ position: 'absolute', inset: 0, width: '100%', height: '100%', objectFit: 'cover' }} />
|
||||
)}
|
||||
<div style={{ position: 'absolute', top: 16, left: 16, background: 'var(--dark)', color: '#fff', borderRadius: 3, padding: '8px 12px', textAlign: 'center', lineHeight: 1.05 }}>
|
||||
<div className="serif" style={{ fontSize: 24, fontWeight: 600 }}>{jour}</div>
|
||||
<div style={{ fontSize: 10, letterSpacing: '.12em', textTransform: 'uppercase', color: '#B7AFA6' }}>{mois}</div>
|
||||
@@ -262,9 +265,15 @@ export default function Home() {
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gridTemplateRows: '1fr 1fr', gap: 10, height: 240 }}>
|
||||
<div className="placeholder-pattern" style={{ gridRow: 'span 2', borderRadius: 3 }} />
|
||||
<div className="placeholder-pattern" style={{ borderRadius: 3 }} />
|
||||
<div className="placeholder-pattern" style={{ borderRadius: 3 }} />
|
||||
<div className={p.image_path ? '' : 'placeholder-pattern'} style={{ gridRow: 'span 2', borderRadius: 3, overflow: 'hidden' }}>
|
||||
{p.image_path && <img src={p.image_path} alt="" style={{ width: '100%', height: '100%', objectFit: 'cover' }} />}
|
||||
</div>
|
||||
<div className={p.image_path_2 ? '' : 'placeholder-pattern'} style={{ borderRadius: 3, overflow: 'hidden' }}>
|
||||
{p.image_path_2 && <img src={p.image_path_2} alt="" style={{ width: '100%', height: '100%', objectFit: 'cover' }} />}
|
||||
</div>
|
||||
<div className={p.image_path_3 ? '' : 'placeholder-pattern'} style={{ borderRadius: 3, overflow: 'hidden' }}>
|
||||
{p.image_path_3 && <img src={p.image_path_3} alt="" style={{ width: '100%', height: '100%', objectFit: 'cover' }} />}
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
))}
|
||||
|
||||
Reference in new issue
Block a user