From db361051e61443b9780c60713e9a673bf14b19ae Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 25 Jul 2026 00:25:56 +0000 Subject: [PATCH] =?UTF-8?q?Menu=20d'administration=20regroup=C3=A9=20et=20?= =?UTF-8?q?version=20mobile=20du=20site?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Le menu empilait douze liens sous un seul intitulé, avec des carrés, ronds et losanges qui ne portaient aucun sens. Sur téléphone, la barre latérale se dépliait en ligne et devenait un pavé de boutons inutilisable. Navigation - Six rubriques au lieu d'une pile : Pilotage, Vie du réseau, Rencontres, Communication, Configuration, Côté adhérent. Le quotidien est ainsi séparé de ce qu'on ne touche qu'occasionnellement. - Une icône par entrée (`BackendIcons.tsx`) à la place des formes génériques. - Le menu devient une table de données filtrée par capacité, au lieu de douze blocs JSX répétant les mêmes appels à `can()`. Mobile - Sous 1024px la barre latérale devient un tiroir : voile de fond, fermeture par Échap, par clic hors zone et à la navigation, défilement de la page bloqué, actions Voir le site / Déconnexion rapatriées dans le tiroir. - `min-height: 0` sur la zone de navigation : sans lui l'enfant flex refusait de rétrécir et la carte utilisateur recouvrait les derniers liens, interceptant les clics. - En-tête public repensé : logo toujours présent sur téléphone (seul retour à l'accueil), accès adhérent sur la même ligne, navigation répartie en dessous au lieu de se replier en escalier. - Liste des adhérents : le tableau à cinq colonnes devient une fiche par adhérent, sur deux lignes constantes, sans défilement horizontal. - Compteurs du tableau de bord en deux colonnes plutôt qu'empilés. Vérifié : aucun débordement horizontal sur les 23 pages en 320, 390 et 768 px ; tiroir et cibles tactiles testés au doigt sur toutes les interactions. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01QXNXRC4j5VLfKvpyyisrnb --- src/app/backend/BackendShell.tsx | 382 ++++++++++++++--------------- src/app/backend/adherents/page.tsx | 25 +- src/app/backend/page.tsx | 8 +- src/app/globals.css | 369 +++++++++++++++++++++++++++- src/components/BackendIcons.tsx | 134 ++++++++++ src/components/SiteHeader.tsx | 57 ++--- 6 files changed, 719 insertions(+), 256 deletions(-) create mode 100644 src/components/BackendIcons.tsx diff --git a/src/app/backend/BackendShell.tsx b/src/app/backend/BackendShell.tsx index 15d95ec..800a33e 100644 --- a/src/app/backend/BackendShell.tsx +++ b/src/app/backend/BackendShell.tsx @@ -2,7 +2,8 @@ import Link from "next/link"; import { usePathname } from "next/navigation"; -import type { ReactNode } from "react"; +import { useEffect, useState, type ReactNode } from "react"; +import { BackendIcon, type BackendIconName } from "@/components/BackendIcons"; import { can, ROLE_LABELS } from "@/lib/rbac"; import type { AppRole } from "@/types/next-auth"; import { doSignOut } from "./actions"; @@ -19,6 +20,7 @@ const TITLES: Record = { "/backend/inscriptions": ["Inscriptions", "Gérer les participants aux rencontres"], "/backend/rencontres-passees": ["Rencontres passées", "Publier les archives et leurs photos"], "/backend/emails": ["Création d'e-mails", "Composer des messages aux couleurs de Plein R"], + "/backend/reseaux": ["Réseaux sociaux", "Connecter les pages Facebook et LinkedIn"], "/backend/administrateurs": ["Administrateurs", "Gérer les accès à l'administration"], "/backend/categories": ["Catégories", "Gérer les métiers de l'annuaire"], "/backend/parametres": ["Paramètres du site", "Configurer l'association et les mentions légales"], @@ -26,6 +28,67 @@ const TITLES: Record = { "/backend/changer-mot-de-passe": ["Mot de passe", "Sécurisez votre compte"], }; +type Capability = Parameters[1]; +type BadgeKey = "inbox" | "promos"; + +type NavItem = { + href: string; + label: string; + icon: BackendIconName; + capability?: Capability; + badge?: BadgeKey; + /** Les fiches adhérent sont des sous-routes : l'entrée doit rester active. */ + prefix?: boolean; +}; + +type NavSection = { label: string; items: NavItem[] }; + +/** + * Menu groupé par usage plutôt qu'en une seule pile : on distingue le quotidien + * (réseau, rencontres) de ce qu'on ne touche qu'occasionnellement (configuration). + */ +const SECTIONS: NavSection[] = [ + { + label: "Pilotage", + items: [{ href: "/backend", label: "Tableau de bord", icon: "dashboard", capability: "viewDashboard" }], + }, + { + label: "Vie du réseau", + items: [ + { href: "/backend/adherents", label: "Adhérents", icon: "members", capability: "manageMembers", prefix: true }, + { href: "/backend/demandes", label: "Demandes & messages", icon: "inbox", capability: "manageMembers", badge: "inbox" }, + { href: "/backend/promotions", label: "Promotions", icon: "promos", capability: "moderatePromos", badge: "promos" }, + ], + }, + { + label: "Rencontres", + items: [ + { href: "/backend/rencontres", label: "Prochaines dates", icon: "meetings", capability: "manageMeetings" }, + { href: "/backend/inscriptions", label: "Inscriptions", icon: "registrations", capability: "manageMeetings" }, + { href: "/backend/rencontres-passees", label: "Archives & photos", icon: "gallery", capability: "manageMeetings" }, + ], + }, + { + label: "Communication", + items: [ + { href: "/backend/emails", label: "E-mails", icon: "emails", capability: "manageEmails" }, + { href: "/backend/reseaux", label: "Réseaux sociaux", icon: "social", capability: "manageSettings" }, + ], + }, + { + label: "Configuration", + items: [ + { href: "/backend/categories", label: "Catégories", icon: "categories", capability: "manageCategories" }, + { href: "/backend/parametres", label: "Paramètres du site", icon: "settings", capability: "manageSettings" }, + { href: "/backend/administrateurs", label: "Administrateurs", icon: "admins", capability: "manageAdmins" }, + ], + }, + { + label: "Côté adhérent", + items: [{ href: "/backend/espace", label: "Mon espace", icon: "space" }], + }, +]; + function initialsOf(name: string) { return name .split(/\s+/) @@ -35,19 +98,21 @@ function initialsOf(name: string) { .toUpperCase(); } -function NavLink({ - href, - active, - children, -}: { - href: string; - active: boolean; - children: ReactNode; -}) { +function Badge({ value }: { value: number }) { return ( - - {children} - + + {value} + ); } @@ -63,149 +128,116 @@ export function BackendShell({ children: ReactNode; }) { const pathname = usePathname(); + const [menuOpen, setMenuOpen] = useState(false); + const [title, sub] = TITLES[pathname] ?? (pathname.startsWith("/backend/adherents") ? TITLES["/backend/adherents"] : ["Administration", "Plein R"]); - const dot = ( - + // Le tiroir se referme dès qu'on navigue. + useEffect(() => { + setMenuOpen(false); + }, [pathname]); + + // Pas de défilement de la page derrière le tiroir ouvert. + useEffect(() => { + if (!menuOpen) return; + const previous = document.body.style.overflow; + document.body.style.overflow = "hidden"; + return () => { + document.body.style.overflow = previous; + }; + }, [menuOpen]); + + useEffect(() => { + if (!menuOpen) return; + const onKey = (event: KeyboardEvent) => { + if (event.key === "Escape") setMenuOpen(false); + }; + window.addEventListener("keydown", onKey); + return () => window.removeEventListener("keydown", onKey); + }, [menuOpen]); + + const badges: Record = { inbox: inboxCount, promos: pendingCount }; + + const sections = SECTIONS.map((section) => ({ + ...section, + items: section.items.filter((item) => !item.capability || can(user.role, item.capability)), + })).filter((section) => section.items.length > 0); + + const siteLink = ( + + ← Voir le site + ); - const dotRound = ( - - ); - const dotDiamond = ( - + const signOut = ( +
+ +
); return (
- {/* sidebar */} -
+ + {/* Sur téléphone, l'entête n'a pas la place : les actions vivent ici. */} +
+ {siteLink} + {signOut} +
- {/* main */}
-
-
-

- {title} -

-
{sub}
+
+ +
+

{title}

+
{sub}
-
- - ← Voir le site - -
- -
+
+ {siteLink} + {signOut}
-
{children}
+
{children}
); } - -const sectionLabel = { - fontSize: 10.5, - letterSpacing: "0.14em", - textTransform: "uppercase" as const, - color: "#5f7d97", - fontWeight: 700, - padding: "20px 10px 8px", -}; diff --git a/src/app/backend/adherents/page.tsx b/src/app/backend/adherents/page.tsx index a50b472..78e947b 100644 --- a/src/app/backend/adherents/page.tsx +++ b/src/app/backend/adherents/page.tsx @@ -65,8 +65,6 @@ export default async function AdherentsPage({ redirect("/backend/adherents"); } - const COLS = "1.6fr 1fr 1.2fr 0.9fr 0.8fr"; - return (
{missingAccounts > 0 && ( @@ -89,8 +87,7 @@ export default async function AdherentsPage({ -
-
+
@@ -104,9 +101,8 @@ export default async function AdherentsPage({
{ const active = m.status === "active"; return ( -
-
+
+
{initialsOf(m.name)}
{m.name}
-
{m.email}
+
{m.email}
-
{m.categoryLabel ?? "—"}
-
{m.city ?? "—"}
+
+ {m.categoryLabel ?? "—"} +
+
+ {m.city ?? "—"} +
{active ? "Actif" : "En attente"}
-
+
Modifier @@ -157,7 +157,6 @@ export default async function AdherentsPage({ ); })}
-
); } diff --git a/src/app/backend/page.tsx b/src/app/backend/page.tsx index f999801..ffdd800 100644 --- a/src/app/backend/page.tsx +++ b/src/app/backend/page.tsx @@ -23,12 +23,12 @@ function StatCard({ hintColor?: string; }) { return ( -
-
{label}
-
+
+
{label}
+
{value}
-
{hint}
+
{hint}
); } diff --git a/src/app/globals.css b/src/app/globals.css index 22f4ef9..0aa8d81 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -237,6 +237,87 @@ button { align-items: start; } +/* ---- En-tête public ---- */ +.site-header { + display: flex; + align-items: center; + gap: 20px; + padding: 18px clamp(16px, 5vw, 56px); +} +.site-header__brand { + display: inline-flex; + align-items: center; + margin-right: auto; +} +.site-header__brand.is-desktop-hidden { + display: none; +} +.site-header__brand img { + display: block; + width: auto; + height: 46px; +} +.site-header__nav { + display: flex; + align-items: center; + gap: 30px; + margin-left: auto; +} +.site-header__link { + color: var(--muted); + font-size: 15px; + font-weight: 500; + text-decoration: none; + white-space: nowrap; +} +.site-header__link.is-active { + color: var(--ink-soft); + font-weight: 600; +} +.site-header__cta { + padding: 10px 18px; + border-radius: 999px; + background: var(--brown); + color: #fff; + font-size: 14.5px; + font-weight: 600; + text-decoration: none; + white-space: nowrap; +} + +@media (max-width: 720px) { + /* Ligne 1 : logo + accès adhérent. Ligne 2 : la navigation, répartie sur + toute la largeur plutôt que repliée en escalier. */ + .site-header { + flex-wrap: wrap; + gap: 10px 12px; + padding: 12px 16px; + } + .site-header__brand, + .site-header__brand.is-desktop-hidden { + display: inline-flex; + } + .site-header__brand img { + height: 38px; + } + .site-header__nav { + order: 3; + flex: 1 1 100%; + margin-left: 0; + gap: 6px; + justify-content: space-between; + border-top: 1px solid #e2d6bd; + padding-top: 10px; + } + .site-header__link { + font-size: 14px; + } + .site-header__cta { + padding: 9px 15px; + font-size: 13.5px; + } +} + /* ---- Public footer & legal pages ---- */ .footer-grid { display: grid; @@ -333,10 +414,15 @@ button { } @media (max-width: 640px) { .grid-6, - .grid-4, .grid-3 { grid-template-columns: 1fr; } + /* Les compteurs du tableau de bord restent sur deux colonnes : empilés, ils + imposaient un écran entier de défilement pour quatre chiffres. */ + .grid-4 { + grid-template-columns: 1fr 1fr; + gap: 12px; + } .container { padding: 0 16px; } @@ -363,6 +449,98 @@ img { -webkit-overflow-scrolling: touch; } +/* ---- Compteurs du tableau de bord ---- */ +.stat-card { + padding: 20px; +} +.stat-card__label { + margin-bottom: 8px; + color: var(--muted-3); + font-size: 13px; +} +.stat-card__value { + font-size: 32px; + font-weight: 800; +} +.stat-card__hint { + margin-top: 6px; + font-size: 12px; + font-weight: 600; +} + +@media (max-width: 640px) { + .stat-card { + padding: 14px; + } + .stat-card__label { + margin-bottom: 4px; + font-size: 12px; + } + .stat-card__value { + font-size: 26px; + } + .stat-card__hint { + margin-top: 3px; + font-size: 11px; + } +} + +/* ---- Liste des adhérents : tableau sur grand écran, fiches sur téléphone ---- */ +.member-row { + display: grid; + grid-template-columns: 1.6fr 1fr 1.2fr 0.9fr 0.8fr; + align-items: center; + gap: 12px; +} +.member-row__identity { + display: flex; + align-items: center; + gap: 12px; + min-width: 0; +} +.member-row__mail { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} +.member-row__action { + text-align: right; +} + +@media (max-width: 760px) { + /* Un tableau à 5 colonnes est illisible sur un téléphone : chaque adhérent + devient une fiche qui se replie, sans défilement horizontal. */ + .member-row { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 9px 10px; + } + /* Doit rester après `.member-row` : même spécificité, c'est l'ordre qui tranche. */ + .member-row.member-head { + display: none; + } + .member-row__identity { + flex: 1 1 100%; + } + /* Base 0 : les métadonnées ne réclament pas leur largeur de contenu, donc la + ligne ne se replie pas et c'est le texte qui se rogne. Chaque fiche tient + ainsi sur deux lignes, toujours de la même façon. */ + .member-row__meta { + flex: 1 1 0; + min-width: 0; + overflow: hidden; + font-size: 13px !important; + text-overflow: ellipsis; + white-space: nowrap; + } + .member-row__action { + flex: 0 0 auto; + margin-left: auto; + text-align: right; + } +} + /* ---- Backend layout ---- */ .backend { display: flex; @@ -381,6 +559,61 @@ img { top: 0; height: 100vh; } +.sidebar-brand { + display: flex; + align-items: center; + gap: 11px; + padding: 6px 8px 18px; +} +.sidebar-close { + display: none; + align-items: center; + justify-content: center; + /* 40px : cible tactile confortable au pouce. */ + width: 40px; + height: 40px; + flex-shrink: 0; + border: 0; + border-radius: 10px; + background: rgba(255, 255, 255, 0.08); + color: #cfe0ee; + cursor: pointer; +} +.sidebar-nav { + flex: 1 1 auto; + min-width: 0; + /* Sans min-height:0 un enfant flex refuse de rétrécir : la zone débordait + sous la carte utilisateur, qui interceptait les clics sur les derniers + liens au lieu de laisser défiler le menu. */ + min-height: 0; + overflow-y: auto; +} +.sidebar-section { + padding: 16px 10px 7px; + color: #5f7d97; + font-size: 10.5px; + font-weight: 700; + letter-spacing: 0.14em; + text-transform: uppercase; +} +.sidebar-nav > div:first-child .sidebar-section { + padding-top: 4px; +} +.sidebar-user { + display: flex; + align-items: center; + gap: 11px; + margin-top: 14px; + padding: 11px 12px; + border-radius: 12px; + background: rgba(255, 255, 255, 0.06); +} +/* Sur grand écran les actions vivent dans l'entête, pas dans la barre. */ +.sidebar-actions { + display: none; + gap: 9px; + margin-top: 12px; +} .nav-btn { display: flex; align-items: center; @@ -409,6 +642,75 @@ img { color: #fff; box-shadow: inset 3px 0 0 var(--gold); } +.sidebar-backdrop { + display: none; + position: fixed; + inset: 0; + z-index: 40; + border: 0; + padding: 0; + background: rgba(19, 50, 79, 0.5); +} +.backend-header { + display: flex; + align-items: center; + gap: 16px; + padding: 18px 32px; + background: var(--cream); + border-bottom: 1px solid #e2d6bd; + position: sticky; + top: 0; + z-index: 5; +} +.backend-header__title { + margin: 0; + color: var(--ink-strong); + font-size: 22px; + font-weight: 800; +} +.backend-header__sub { + margin-top: 2px; + color: var(--muted-3); + font-size: 13px; +} +.backend-header__actions { + display: flex; + align-items: center; + gap: 12px; +} +.backend-burger { + display: none; + padding: 9px; + border: 1px solid #d8cdb4; + border-radius: 10px; + background: #fff; + color: var(--ink-soft); + cursor: pointer; +} +.backend-action { + display: inline-block; + border: 1px solid #d8cdb4; + border-radius: 10px; + padding: 9px 15px; + background: transparent; + color: var(--muted); + font-family: inherit; + font-size: 13.5px; + font-weight: 600; + text-decoration: none; + white-space: nowrap; + cursor: pointer; +} +.backend-action--strong { + background: #fff; + color: var(--brown); + font-weight: 700; +} +.backend-content { + flex: 1; + padding: 28px 32px 44px; + min-width: 0; +} ::-webkit-scrollbar { width: 10px; @@ -419,17 +721,70 @@ img { border-radius: 6px; } -@media (max-width: 860px) { +/* Sous 1024px la barre latérale devient un tiroir : l'empiler en ligne rendait + la navigation illisible sur téléphone. */ +@media (max-width: 1024px) { .backend { flex-direction: column; } .sidebar { + position: fixed; + top: 0; + left: 0; + z-index: 50; + width: min(304px, 86vw); + height: 100dvh; + padding: 18px 14px; + transform: translateX(-100%); + transition: transform 0.25s ease; + box-shadow: 0 0 40px rgba(19, 50, 79, 0.35); + } + .sidebar.is-open { + transform: translateX(0); + } + .sidebar-close, + .sidebar-actions { + display: flex; + } + .sidebar-actions .backend-action { + flex: 1; + border-color: rgba(255, 255, 255, 0.22); + color: #cfe0ee; + text-align: center; + } + .sidebar-actions form { + flex: 1; + } + .sidebar-actions .backend-action--strong { width: 100%; - height: auto; - position: static; - flex-direction: row; - flex-wrap: wrap; - align-items: center; + background: var(--gold); + border-color: var(--gold); + color: var(--ink); + } + .sidebar-backdrop { + display: block; + } + .backend-burger { + display: inline-flex; + } + .backend-header__actions { + display: none; + } + .backend-header { + padding: 13px 16px; + gap: 12px; + } + .backend-header__title { + font-size: 18px; + } + .backend-header__sub { + font-size: 12px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + .backend-content { + padding: 18px 16px 40px; } } diff --git a/src/components/BackendIcons.tsx b/src/components/BackendIcons.tsx new file mode 100644 index 0000000..dcc2ded --- /dev/null +++ b/src/components/BackendIcons.tsx @@ -0,0 +1,134 @@ +import type { ReactNode } from "react"; + +/** + * Icônes de la navigation d'administration. Les anciens carrés / ronds / + * losanges ne portaient aucun sens : une icône par rubrique rend le menu + * lisible d'un coup d'œil. + */ +export type BackendIconName = + | "dashboard" + | "members" + | "inbox" + | "promos" + | "meetings" + | "registrations" + | "gallery" + | "emails" + | "social" + | "categories" + | "settings" + | "admins" + | "space" + | "menu" + | "close"; + +const PATHS: Record = { + dashboard: ( + <> + + + + + + ), + members: ( + <> + + + + + ), + inbox: ( + <> + + + + ), + promos: ( + <> + + + + ), + meetings: ( + <> + + + + ), + registrations: ( + <> + + + + + ), + gallery: ( + <> + + + + + ), + emails: ( + <> + + + ), + social: ( + <> + + + + + + ), + categories: ( + <> + + + + + + ), + settings: ( + <> + + + + + ), + admins: ( + <> + + + + ), + space: ( + <> + + + + ), + menu: , + close: , +}; + +export function BackendIcon({ name, size = 18 }: { name: BackendIconName; size?: number }) { + return ( + + ); +} diff --git a/src/components/SiteHeader.tsx b/src/components/SiteHeader.tsx index e5086da..e05681b 100644 --- a/src/components/SiteHeader.tsx +++ b/src/components/SiteHeader.tsx @@ -9,56 +9,37 @@ const NAV = [ export function SiteHeader({ active, logo = false }: { active?: string; logo?: boolean }) { return ( -
- {logo && ( - - {/* eslint-disable-next-line @next/next/no-img-element */} - Plein R - - )} -