SPB Git

Repository Public

AdminNav.tsx 5.1 KB 145 lines tsx

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
Files main
Menu

Theme