Reorganise la navigation de l'administration
This commit is contained in:
1 parent
317095fa88
commit
9df9c0d2ae
4 files changed
+153
-39
No files matched your search
+80
-34
@@ -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}
|
||||
</button>
|
||||
<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
@@ -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. */
|
||||
|
||||
Reference in new issue
Block a user