Layout admin : - src/app/admin/layout.tsx : route protégée requireRole(ADMIN), sidebar + topbar + breadcrumbs, data-admin sur racine pour theme sobre indépendant du theme public - Sidebar : 12 sections groupées (Vue d'ensemble, Catalogue, Activité, Membres, Contenu, Système), highlight de la route courante - TopBar : prompt ⌘K, lien vers site public, email admin - Breadcrumbs : auto depuis pathname - CommandPalette : ⌘K / Ctrl K, navigation ↑↓ + Entrée, recherche live debounced 150ms Dashboard : - 7 KPI cards avec tone neutral/ok/warn/info (réservations semaine, confirmées 30j, revenus reversés, occupation, nouveaux users, carbets publiés, avis à modérer) - Section raccourcis fréquents Theme admin : - globals.css : [data-admin] override le background+font, neutralise les borders sépia/papier teinté du theme aquarelle, garantit lisibilité permanente Recherche globale : - lib/admin/search.ts : query parallèle sur Carbet, User, Booking, ContentPage, PirogueProvider (5 résultats par catégorie, LIKE insensitive) - api/admin/search?q=… route handler avec requireRole KPI : - lib/admin/kpis.ts : 7 métriques live (cache 0), Promise.all, helper formatEur Pas de dépendance externe ajoutée (cmdk, shadcn) — composants custom Tailwind pour rester léger.
198 lines
6.7 KiB
TypeScript
198 lines
6.7 KiB
TypeScript
"use client";
|
|
|
|
import Link from "next/link";
|
|
import { usePathname } from "next/navigation";
|
|
import type { ReactNode } from "react";
|
|
|
|
type NavItem = {
|
|
href: string;
|
|
label: string;
|
|
icon: ReactNode;
|
|
badge?: number;
|
|
};
|
|
|
|
type NavGroup = {
|
|
label: string;
|
|
items: NavItem[];
|
|
};
|
|
|
|
const ICONS = {
|
|
dashboard: (
|
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
|
<rect x="3" y="3" width="7" height="7" rx="1" />
|
|
<rect x="14" y="3" width="7" height="7" rx="1" />
|
|
<rect x="3" y="14" width="7" height="7" rx="1" />
|
|
<rect x="14" y="14" width="7" height="7" rx="1" />
|
|
</svg>
|
|
),
|
|
carbets: (
|
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
|
<path d="M3 21 L12 6 L21 21 Z" />
|
|
<path d="M9 21 V13 H15 V21" />
|
|
</svg>
|
|
),
|
|
bookings: (
|
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
|
<rect x="3" y="5" width="18" height="16" rx="2" />
|
|
<path d="M3 10 H21" />
|
|
<path d="M8 3 V7" />
|
|
<path d="M16 3 V7" />
|
|
</svg>
|
|
),
|
|
users: (
|
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
|
<circle cx="9" cy="8" r="3" />
|
|
<path d="M3 21 Q3 14 9 14 Q15 14 15 21" />
|
|
<circle cx="17" cy="9" r="2.5" />
|
|
<path d="M14 16 Q17 13 21 16" />
|
|
</svg>
|
|
),
|
|
organizations: (
|
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
|
<rect x="4" y="6" width="16" height="14" rx="1" />
|
|
<path d="M4 10 H20" />
|
|
<path d="M9 10 V20" />
|
|
<path d="M15 10 V20" />
|
|
</svg>
|
|
),
|
|
pirogue: (
|
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round">
|
|
<path d="M3 14 Q12 22 21 14" />
|
|
<path d="M5 12 H19" />
|
|
<path d="M9 12 V8" />
|
|
<path d="M15 12 V8" />
|
|
</svg>
|
|
),
|
|
reviews: (
|
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
|
<path d="M12 2 L14.5 8.5 L21 9 L16 14 L17.5 21 L12 17.5 L6.5 21 L8 14 L3 9 L9.5 8.5 Z" />
|
|
</svg>
|
|
),
|
|
media: (
|
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
|
<rect x="3" y="3" width="18" height="18" rx="2" />
|
|
<circle cx="9" cy="9" r="2" />
|
|
<path d="M3 17 L9 11 L21 19" />
|
|
</svg>
|
|
),
|
|
pages: (
|
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
|
<path d="M5 3 H15 L19 7 V21 H5 Z" />
|
|
<path d="M14 3 V8 H19" />
|
|
<path d="M9 13 H15" />
|
|
<path d="M9 17 H13" />
|
|
</svg>
|
|
),
|
|
plugins: (
|
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
|
<rect x="3" y="3" width="7" height="7" rx="1" />
|
|
<rect x="14" y="3" width="7" height="7" rx="1" />
|
|
<rect x="3" y="14" width="7" height="7" rx="1" />
|
|
<path d="M17 14 V21 M14 17 H20" />
|
|
</svg>
|
|
),
|
|
settings: (
|
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
|
<circle cx="12" cy="12" r="3" />
|
|
<path d="M12 1 V5 M12 19 V23 M4.2 4.2 L7 7 M17 17 L19.8 19.8 M1 12 H5 M19 12 H23 M4.2 19.8 L7 17 M17 7 L19.8 4.2" />
|
|
</svg>
|
|
),
|
|
audit: (
|
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
|
<path d="M4 4 H20 V20 H4 Z" />
|
|
<path d="M8 9 H16 M8 13 H16 M8 17 H12" />
|
|
</svg>
|
|
),
|
|
};
|
|
|
|
const GROUPS: NavGroup[] = [
|
|
{
|
|
label: "Vue d'ensemble",
|
|
items: [{ href: "/admin", label: "Dashboard", icon: ICONS.dashboard }],
|
|
},
|
|
{
|
|
label: "Catalogue",
|
|
items: [
|
|
{ href: "/admin/carbets", label: "Carbets", icon: ICONS.carbets },
|
|
{ href: "/admin/pirogue-providers", label: "Prestataires pirogue", icon: ICONS.pirogue },
|
|
{ href: "/admin/media", label: "Médias", icon: ICONS.media },
|
|
],
|
|
},
|
|
{
|
|
label: "Activité",
|
|
items: [
|
|
{ href: "/admin/bookings", label: "Réservations", icon: ICONS.bookings },
|
|
{ href: "/admin/reviews", label: "Avis & modération", icon: ICONS.reviews },
|
|
],
|
|
},
|
|
{
|
|
label: "Membres",
|
|
items: [
|
|
{ href: "/admin/users", label: "Utilisateurs", icon: ICONS.users },
|
|
{ href: "/admin/organizations", label: "Organisations CE", icon: ICONS.organizations },
|
|
],
|
|
},
|
|
{
|
|
label: "Contenu",
|
|
items: [{ href: "/admin/content-pages", label: "Pages éditoriales", icon: ICONS.pages }],
|
|
},
|
|
{
|
|
label: "Système",
|
|
items: [
|
|
{ href: "/admin/plugins", label: "Plugins", icon: ICONS.plugins },
|
|
{ href: "/admin/settings", label: "Paramètres", icon: ICONS.settings },
|
|
{ href: "/admin/audit", label: "Audit log", icon: ICONS.audit },
|
|
],
|
|
},
|
|
];
|
|
|
|
export function Sidebar() {
|
|
const pathname = usePathname();
|
|
|
|
return (
|
|
<nav className="flex h-full w-60 shrink-0 flex-col gap-1 border-r border-zinc-200 bg-[#f7f5f0] py-4">
|
|
<Link
|
|
href="/admin"
|
|
className="mx-3 mb-3 flex items-center gap-2 rounded px-2 py-1.5 text-sm font-semibold text-zinc-900"
|
|
>
|
|
<span className="inline-block h-2.5 w-2.5 rounded-full bg-[#8c3d18]" />
|
|
Karbé · Admin
|
|
</Link>
|
|
|
|
{GROUPS.map((group) => (
|
|
<section key={group.label} className="mb-2 px-3">
|
|
<h3 className="mb-1 px-2 text-[10px] font-semibold uppercase tracking-wider text-zinc-500">
|
|
{group.label}
|
|
</h3>
|
|
<ul className="space-y-0.5">
|
|
{group.items.map((item) => {
|
|
const active = pathname === item.href || (item.href !== "/admin" && pathname.startsWith(item.href));
|
|
return (
|
|
<li key={item.href}>
|
|
<Link
|
|
href={item.href}
|
|
className={`flex items-center justify-between rounded px-2 py-1.5 text-sm transition ${
|
|
active
|
|
? "bg-white text-zinc-900 shadow-sm ring-1 ring-zinc-200"
|
|
: "text-zinc-600 hover:bg-white/60 hover:text-zinc-900"
|
|
}`}
|
|
>
|
|
<span className="flex items-center gap-2">
|
|
<span className={active ? "text-[#8c3d18]" : "text-zinc-400"}>{item.icon}</span>
|
|
{item.label}
|
|
</span>
|
|
{item.badge !== undefined ? (
|
|
<span className="rounded-full bg-zinc-200 px-1.5 py-0.5 text-[10px] font-medium text-zinc-700">
|
|
{item.badge}
|
|
</span>
|
|
) : null}
|
|
</Link>
|
|
</li>
|
|
);
|
|
})}
|
|
</ul>
|
|
</section>
|
|
))}
|
|
</nav>
|
|
);
|
|
}
|