Repository Public
1"use client";2// Auteur : Simon-Pierre Boucher — contact@spboucher.ai3// Navigation du panel — latérale (desktop) / barre repliable (mobile).4// Le badge de non-lus se rafraîchit par sondage léger (45 s) sur5// /api/admin/badge — l'architecture notifications pourra brancher6// courriel/push/Slack sur le même compteur.7import { useEffect, useState } from "react";8import { usePathname, useRouter } from "next/navigation";9import { ADMIN_ROLES } from "@/lib/comms/categories";1011const LINKS = [12 { href: "/admin", label: "Dashboard", exact: true },13 { href: "/admin/inbox", label: "Inbox", badge: "inbox" },14 { href: "/admin/courriel", label: "Courriel", badge: "mail" },15 { href: "/admin/candidatures", label: "Candidatures" },16 { href: "/admin/equipe", label: "Équipe & accès" },17];1819export default function AdminNav({20 displayName,21 username,22 roles,23 initialUnread,24 initialMail,25}: {26 displayName: string;27 username: string;28 roles: string[];29 initialUnread: number;30 initialMail: number;31}) {32 const pathname = usePathname();33 const router = useRouter();34 const [unread, setUnread] = useState(initialUnread);35 const [mail, setMail] = useState(initialMail);36 const [open, setOpen] = useState(false);3738 useEffect(() => {39 setUnread(initialUnread);40 setMail(initialMail);41 }, [initialUnread, initialMail]);4243 useEffect(() => {44 const t = setInterval(async () => {45 try {46 const res = await fetch("/api/admin/badge");47 if (res.ok) {48 const data = (await res.json()) as { unread: number; mail?: number };49 setUnread(data.unread);50 setMail(data.mail ?? 0);51 }52 } catch {53 /* réseau : on garde la dernière valeur */54 }55 }, 45000);56 return () => clearInterval(t);57 }, []);5859 async function logout() {60 await fetch("/api/admin/logout", { method: "POST" }).catch(() => {});61 router.replace("/admin/connexion");62 router.refresh();63 }6465 const nav = (66 <>67 <nav className="mt-6 flex flex-col gap-1" aria-label="Navigation admin">68 {LINKS.map((l) => {69 const active = l.exact ? pathname === l.href : pathname.startsWith(l.href);70 return (71 <a72 key={l.href}73 href={l.href}74 className={`adm-nav-link ${active ? "adm-nav-link--active" : ""}`}75 >76 {l.label}77 {(() => {78 const n =79 l.badge === "inbox" ? unread : l.badge === "mail" ? mail : 0;80 return n > 0 ? (81 <span82 className={`gk-mono rounded-full px-2 py-[1px] text-[10.5px] font-bold ${83 active ? "bg-ink text-lime" : "bg-lime text-ink"84 }`}85 >86 {n}87 </span>88 ) : null;89 })()}90 </a>91 );92 })}93 </nav>94 <div className="mt-auto border-t border-[rgba(245,243,238,0.14)] pt-4">95 <p className="gk-display text-[14px] font-bold text-paper">{displayName}</p>96 <p className="gk-mono mt-[2px] text-[10px] tracking-[0.08em] text-[rgba(245,243,238,0.5)] uppercase">97 @{username} · {roles.map((r) => ADMIN_ROLES[r] ?? r).join(" · ")}98 </p>99 <div className="mt-3 flex items-center gap-4">100 <a101 href="/contact"102 target="_blank"103 className="gk-mono text-[10px] font-bold tracking-[0.08em] text-[rgba(245,243,238,0.55)] uppercase underline-offset-4 hover:text-lime hover:underline"104 >105 Voir /contact ↗106 </a>107 <button108 onClick={logout}109 className="gk-mono cursor-pointer text-[10px] font-bold tracking-[0.08em] text-[rgba(245,243,238,0.55)] uppercase underline-offset-4 hover:text-lime hover:underline"110 >111 Déconnexion112 </button>113 </div>114 </div>115 </>116 );117118 return (119 <aside className="flex-none bg-ink lg:sticky lg:top-0 lg:flex lg:h-[100dvh] lg:w-[248px] lg:flex-col lg:px-5 lg:py-6">120 {/* barre mobile */}121 <div className="flex items-center justify-between px-4 py-4 lg:px-0 lg:py-0">122 <a href="/admin" className="gk-display text-[19px] font-bold tracking-[-0.03em] text-paper no-underline">123 Groupe{" "}124 <span className="inline-block -rotate-2 rounded-md bg-lime px-[6px] pb-[2px] text-ink">125 KA126 </span>{" "}127 <span className="gk-mono ml-1 align-middle text-[9px] font-bold tracking-[0.24em] text-[rgba(245,243,238,0.5)] uppercase">128 Admin129 </span>130 </a>131 <button132 className="gk-mono cursor-pointer rounded-lg border border-[rgba(245,243,238,0.3)] px-3 py-2 text-[10px] font-bold tracking-[0.1em] text-paper uppercase lg:hidden"133 onClick={() => setOpen((o) => !o)}134 aria-expanded={open}135 >136 Menu{unread + mail > 0 ? ` · ${unread + mail}` : ""}137 </button>138 </div>139 <div className={`${open ? "flex" : "hidden"} flex-col px-4 pb-6 lg:flex lg:flex-1 lg:px-0 lg:pb-0`}>140 {nav}141 </div>142 </aside>143 );144}145