Reorganise la navigation de l'administration

This commit is contained in:
Michael committed 2026-07-17 01:17:51 +02:00
1 parent 317095fa88
commit 9df9c0d2ae
4 files changed
+152 -38

No files matched your search

+10
View File
@@ -8,6 +8,7 @@
"name": "sbc-web",
"version": "1.0.0",
"dependencies": {
"lucide-react": "^1.24.0",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-router-dom": "^6.26.0"
@@ -1407,6 +1408,15 @@
"yallist": "^3.0.2"
}
},
"node_modules/lucide-react": {
"version": "1.24.0",
"resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-1.24.0.tgz",
"integrity": "sha512-YT6mBD8lGKkg4nM39enlm94/sfJIiW0YKUT60fBy4YK8tai31ylg1VhGNWxkpSKHo9UagfnZqwIff3HTDQwXeA==",
"license": "ISC",
"peerDependencies": {
"react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0"
}
},
"node_modules/ms": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+1
View File
@@ -9,6 +9,7 @@
"preview": "vite preview"
},
"dependencies": {
"lucide-react": "^1.24.0",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-router-dom": "^6.26.0"
+79 -33
View File
@@ -1,5 +1,22 @@
import { useEffect, useState } from 'react';
import { Link, Navigate } from 'react-router-dom';
import {
ArrowLeft,
CalendarCheck2,
CalendarDays,
ClipboardList,
LayoutDashboard,
LogOut,
Mail,
PanelsTopLeft,
ReceiptText,
Settings2,
ShieldCheck,
Tags,
UserCog,
UserRoundPlus,
Users,
} from 'lucide-react';
import { useAuth } from '../lib/AuthContext.jsx';
import { api, dateParts, statutLabel } from '../lib/api.js';
import { MembersTab, RencontresTab, InscriptionsTab } from '../components/admin/AdminTabs.jsx';
@@ -11,24 +28,31 @@ import { RequestsTab } from '../components/admin/AdminRequests.jsx';
import { BillingTab } from '../components/admin/AdminBilling.jsx';
import { RgpdTab } from '../components/admin/AdminRgpd.jsx';
// Moderators get members/membership requests/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).
// Staff members share the operational sections. Administrators also manage
// content and settings, while treasurers additionally access billing. The
// same permissions are enforced here and by the server 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', 'treasurer'] },
{ key: 'demandes', icon: '◌', label: 'Demandes', title: "Demandes d’adhésion", roles: ['admin', 'moderator', 'treasurer'] },
{ key: 'rencontres', icon: '◈', label: 'Rencontres', title: 'Rencontres', roles: ['admin', 'moderator', 'treasurer'] },
{ key: 'passees', icon: '▦', label: 'Rencontres passées', title: 'Rencontres passées', roles: ['admin', 'moderator', 'treasurer'] },
{ key: 'inscriptions', icon: '✎', label: 'Inscriptions', title: 'Inscriptions', roles: ['admin', 'moderator', 'treasurer'] },
{ key: 'facturation', icon: '€', label: 'Facturation', title: 'Facturation des adhésions', roles: ['admin', 'treasurer'] },
{ key: 'categories', icon: '☲', label: 'Catégories', title: 'Catégories', roles: ['admin'] },
{ key: 'contenu', icon: '▧', label: 'Contenu du site', title: 'Contenu du site', roles: ['admin'] },
{ key: 'parametres', icon: '⚙', label: 'Paramètres', title: "Paramètres de l'association", roles: ['admin'] },
{ key: 'rgpd', icon: '⚖', label: 'RGPD', title: 'Conformité RGPD', roles: ['admin'] },
{ key: 'emails', icon: '✉', label: "Créateur d'e-mail", title: "Créateur d'e-mail", roles: ['admin'] },
{ key: 'utilisateurs', icon: '⚿', label: 'Utilisateurs', title: 'Administrateurs, trésoriers & modérateurs', roles: ['admin'] },
{ key: 'dashboard', group: 'overview', icon: LayoutDashboard, label: 'Tableau de bord', title: 'Tableau de bord', roles: ['admin'] },
{ key: 'demandes', group: 'members', icon: UserRoundPlus, label: 'Demandes', title: "Demandes d’adhésion", roles: ['admin', 'moderator', 'treasurer'] },
{ key: 'membres', group: 'members', icon: Users, label: 'Membres', title: 'Gestion des membres', roles: ['admin', 'moderator', 'treasurer'] },
{ key: 'facturation', group: 'members', icon: ReceiptText, label: 'Facturation', title: 'Facturation des adhésions', roles: ['admin', 'treasurer'] },
{ key: 'rencontres', group: 'events', icon: CalendarDays, label: 'Rencontres', title: 'Rencontres', roles: ['admin', 'moderator', 'treasurer'] },
{ key: 'inscriptions', group: 'events', icon: ClipboardList, label: 'Inscriptions', title: 'Inscriptions', roles: ['admin', 'moderator', 'treasurer'] },
{ key: 'passees', group: 'events', icon: CalendarCheck2, label: 'Rencontres passées', title: 'Rencontres passées', roles: ['admin', 'moderator', 'treasurer'] },
{ key: 'emails', group: 'communication', icon: Mail, label: "Créateur d'e-mail", title: "Créateur d'e-mail", roles: ['admin'] },
{ key: 'contenu', group: 'communication', icon: PanelsTopLeft, label: 'Contenu du site', title: 'Contenu du site', roles: ['admin'] },
{ key: 'categories', group: 'communication', icon: Tags, label: 'Catégories', title: 'Catégories', roles: ['admin'] },
{ key: 'parametres', group: 'settings', icon: Settings2, label: 'Association', title: "Paramètres de l'association", roles: ['admin'] },
{ key: 'utilisateurs', group: 'settings', icon: UserCog, label: 'Utilisateurs', title: 'Administrateurs, trésoriers & modérateurs', roles: ['admin'] },
{ key: 'rgpd', group: 'settings', icon: ShieldCheck, label: 'RGPD', title: 'Conformité RGPD', roles: ['admin'] },
];
const NAV_GROUPS = [
{ key: 'overview', label: "Vue d'ensemble" },
{ key: 'members', label: 'Adhérents' },
{ key: 'events', label: 'Rencontres' },
{ key: 'communication', label: 'Communication' },
{ key: 'settings', label: 'Configuration' },
];
function Dashboard() {
@@ -108,6 +132,10 @@ function Dashboard() {
export function AdminShell() {
const { user, logout } = useAuth();
const tabs = TABS.filter((t) => t.roles.includes(user.role));
const navGroups = NAV_GROUPS.map((group) => ({
...group,
tabs: tabs.filter((item) => item.group === group.key),
})).filter((group) => group.tabs.length > 0);
const [tab, setTab] = useState(tabs[0].key);
const current = tabs.find((t) => t.key === tab) || tabs[0];
@@ -116,27 +144,45 @@ export function AdminShell() {
return (
<div className="admin-shell" style={{ display: 'flex', minHeight: '100vh', background: 'var(--admin-bg)' }}>
<aside className="admin-sidebar" style={{ width: 250, flexShrink: 0, background: 'var(--dark)', color: '#fff', display: 'flex', flexDirection: 'column', padding: '26px 0', position: 'sticky', top: 0, height: '100vh' }}>
<div style={{ padding: '0 24px 26px', borderBottom: '1px solid rgba(255,255,255,.1)', display: 'flex', alignItems: 'center', gap: 12 }}>
<img src="/assets/logo.jpg" alt="SLUC" style={{ height: 40, width: 40, objectFit: 'cover', borderRadius: 4, background: '#fff' }} />
<div style={{ lineHeight: 1.1 }}>
<div className="serif" style={{ fontSize: 16, fontWeight: 600 }}>
{roleLabel}
</div>
<div style={{ fontSize: 10, letterSpacing: '.14em', textTransform: 'uppercase', color: '#8A8279' }}>Business Club</div>
<aside className="admin-sidebar">
<div className="admin-sidebar-header">
<img src="/assets/logo.jpg" alt="SLUC" className="admin-sidebar-logo" />
<div className="admin-sidebar-brand">
<div className="serif">Administration</div>
<span>{roleLabel}</span>
</div>
</div>
<nav style={{ padding: '20px 14px', display: 'flex', flexDirection: 'column', gap: 4, flex: 1 }}>
{tabs.map((t) => (
<button key={t.key} className={`admin-nav-btn${tab === t.key ? ' active' : ''}`} onClick={() => setTab(t.key)}>
{t.icon} {t.label}
<nav className="admin-nav" aria-label="Navigation de l'administration">
{navGroups.map((group) => (
<div className="admin-nav-group" key={group.key}>
<div className="admin-nav-group-label">{group.label}</div>
<div className="admin-nav-group-items">
{group.tabs.map((item) => {
const Icon = item.icon;
return (
<button
key={item.key}
className={`admin-nav-btn${tab === item.key ? ' active' : ''}`}
onClick={() => setTab(item.key)}
aria-current={tab === item.key ? 'page' : undefined}
>
<Icon size={18} strokeWidth={1.8} aria-hidden="true" />
<span>{item.label}</span>
</button>
);
})}
</div>
</div>
))}
</nav>
<div style={{ padding: '14px 24px 0', borderTop: '1px solid rgba(255,255,255,.1)', display: 'flex', flexDirection: 'column', gap: 4 }}>
<Link to="/" style={{ color: '#B7AFA6', fontSize: 13.5, fontWeight: 500, padding: '6px 0', textDecoration: 'none' }}>← Retour au site</Link>
<button onClick={logout} style={{ background: 'none', border: 'none', color: '#B7AFA6', fontSize: 13.5, fontWeight: 500, padding: '6px 0', textAlign: 'left' }}>
Se déconnecter
<div className="admin-sidebar-footer">
<Link to="/" className="admin-sidebar-action">
<ArrowLeft size={17} aria-hidden="true" />
<span>Retour au site</span>
</Link>
<button onClick={logout} className="admin-sidebar-action">
<LogOut size={17} aria-hidden="true" />
<span>Se déconnecter</span>
</button>
</div>
</aside>
+62 -5
View File
@@ -117,12 +117,60 @@ input:focus, textarea:focus, select:focus { outline: none; border-color: var(--r
}
/* Admin sidebar */
.admin-nav-btn {
display: block; width: 100%; text-align: left; background: none; color: #B7AFA6;
border: none; border-left: 3px solid transparent; font-size: 14px; font-weight: 500;
padding: 12px 14px; border-radius: 3px;
.admin-sidebar {
width: 272px; flex-shrink: 0; background: var(--dark); color: #fff;
display: flex; flex-direction: column; position: sticky; top: 0; height: 100vh;
border-right: 1px solid rgba(255, 255, 255, .06);
}
.admin-nav-btn.active { background: rgba(193, 39, 45, .18); color: #fff; border-left-color: var(--red); font-weight: 600; }
.admin-sidebar-header {
min-height: 84px; padding: 18px 20px; border-bottom: 1px solid rgba(255, 255, 255, .1);
display: flex; align-items: center; gap: 13px; flex-shrink: 0;
}
.admin-sidebar-logo {
width: 44px; height: 44px; object-fit: cover; border-radius: 4px; background: #fff;
}
.admin-sidebar-brand { min-width: 0; line-height: 1.15; }
.admin-sidebar-brand .serif { font-size: 17px; font-weight: 600; }
.admin-sidebar-brand span {
display: inline-block; margin-top: 6px; color: #D8D1C9; font-size: 10px; font-weight: 700;
letter-spacing: .08em; text-transform: uppercase;
}
.admin-nav {
flex: 1; min-height: 0; overflow-y: auto; padding: 14px 12px 16px;
scrollbar-width: thin; scrollbar-color: rgba(255, 255, 255, .18) transparent;
}
.admin-nav-group + .admin-nav-group { margin-top: 14px; }
.admin-nav-group-label {
margin: 0 12px 6px; color: #817A73; font-size: 10px; font-weight: 700;
letter-spacing: .1em; text-transform: uppercase;
}
.admin-nav-group-items { display: flex; flex-direction: column; gap: 2px; }
.admin-nav-btn {
display: flex; align-items: center; gap: 11px; width: 100%; min-height: 38px;
text-align: left; background: transparent; color: #B7AFA6;
border: 1px solid transparent; font-size: 13.5px; font-weight: 500;
padding: 8px 12px; border-radius: 4px;
transition: background-color .15s ease, color .15s ease, border-color .15s ease;
}
.admin-nav-btn svg { flex: 0 0 auto; color: #8F8880; }
.admin-nav-btn:hover { background: rgba(255, 255, 255, .055); color: #fff; }
.admin-nav-btn:hover svg { color: #D9D2CA; }
.admin-nav-btn:focus-visible { outline: 2px solid var(--red-soft); outline-offset: 1px; }
.admin-nav-btn.active {
background: rgba(193, 39, 45, .2); color: #fff; border-color: rgba(224, 119, 122, .18); font-weight: 600;
}
.admin-nav-btn.active svg { color: var(--red-soft); }
.admin-sidebar-footer {
flex-shrink: 0; padding: 10px 12px 12px; border-top: 1px solid rgba(255, 255, 255, .1);
display: flex; flex-direction: column; gap: 3px;
}
.admin-sidebar-action {
display: flex; align-items: center; gap: 10px; width: 100%; min-height: 36px;
padding: 8px 12px; background: transparent; border: 0; border-radius: 4px;
color: #9F9890; font-size: 13px; font-weight: 500; text-align: left; text-decoration: none;
}
.admin-sidebar-action:hover { background: rgba(255, 255, 255, .055); color: #fff; }
.admin-sidebar-action:focus-visible { outline: 2px solid var(--red-soft); outline-offset: 1px; }
/* Membership request workflow */
.requests-layout { display: flex; flex-direction: column; gap: 20px; }
@@ -190,6 +238,12 @@ input:focus, textarea:focus, select:focus { outline: none; border-color: var(--r
@media (max-width: 860px) {
.admin-shell { flex-direction: column !important; }
.admin-sidebar { width: 100% !important; height: auto !important; position: static !important; }
.admin-sidebar-header { min-height: 72px; padding: 14px 20px; }
.admin-sidebar-logo { width: 40px; height: 40px; }
.admin-nav { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 24px 16px; overflow: visible; padding: 18px 20px 22px; }
.admin-nav-group + .admin-nav-group { margin-top: 0; }
.admin-sidebar-footer { flex-direction: row; padding: 12px 20px; }
.admin-sidebar-action { width: auto; }
.admin-content-header { padding: 16px 20px !important; }
.admin-content-body { padding: 20px !important; }
.requests-summary { grid-template-columns: 1fr; }
@@ -200,6 +254,9 @@ input:focus, textarea:focus, select:focus { outline: none; border-color: var(--r
@media (max-width: 640px) {
.container { padding-left: 20px; padding-right: 20px; }
.admin-nav { grid-template-columns: 1fr; gap: 20px; }
.admin-sidebar-footer { align-items: stretch; flex-direction: column; }
.admin-sidebar-action { width: 100%; }
/* Header nav: tighter spacing so the extra links (added alongside the
new "rencontres passées" page) wrap without feeling cramped. */