Ajoute deux entrées au menu d’administration. Les campagnes s’affichent dans un tableau vide honnête. Les statistiques disent clairement qu’aucune mesure n’est encore branchée, sans afficher de zéro trompeur.
244 lines
8.6 KiB
TypeScript
244 lines
8.6 KiB
TypeScript
'use client'
|
|
|
|
import { usePathname } from 'next/navigation'
|
|
import { useSession } from 'next-auth/react'
|
|
import { performSignOut } from '@/lib/auth-client'
|
|
import {
|
|
LayoutDashboard,
|
|
Users,
|
|
Shield,
|
|
Settings,
|
|
StickyNote,
|
|
ArrowLeft,
|
|
User,
|
|
LogOut,
|
|
Brain,
|
|
CreditCard,
|
|
Mail,
|
|
BarChart3,
|
|
} from 'lucide-react'
|
|
import { cn } from '@/lib/utils'
|
|
import { useLanguage } from '@/lib/i18n'
|
|
import { NotificationPanel } from '@/components/notification-panel'
|
|
import {
|
|
DropdownMenu,
|
|
DropdownMenuContent,
|
|
DropdownMenuItem,
|
|
DropdownMenuSeparator,
|
|
DropdownMenuTrigger,
|
|
} from '@/components/ui/dropdown-menu'
|
|
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'
|
|
|
|
const ADMIN_NAV_ITEMS = [
|
|
{
|
|
titleKey: 'admin.sidebar.dashboard',
|
|
href: '/admin',
|
|
icon: LayoutDashboard,
|
|
},
|
|
{
|
|
titleKey: 'admin.sidebar.users',
|
|
href: '/admin/users',
|
|
icon: Users,
|
|
},
|
|
{
|
|
titleKey: 'admin.sidebar.aiManagement',
|
|
href: '/admin/ai',
|
|
icon: Brain,
|
|
},
|
|
{
|
|
titleKey: 'admin.sidebar.billing',
|
|
href: '/admin/billing',
|
|
icon: CreditCard,
|
|
},
|
|
{
|
|
titleKey: 'admin.sidebar.marketing',
|
|
href: '/admin/marketing',
|
|
icon: Mail,
|
|
},
|
|
{
|
|
titleKey: 'admin.sidebar.stats',
|
|
href: '/admin/stats',
|
|
icon: BarChart3,
|
|
},
|
|
{
|
|
titleKey: 'admin.sidebar.published',
|
|
href: '/admin/published',
|
|
icon: Shield,
|
|
},
|
|
{
|
|
titleKey: 'admin.sidebar.settings',
|
|
href: '/admin/settings',
|
|
icon: Settings,
|
|
},
|
|
] as const
|
|
|
|
function navItemIsActive(pathname: string | null, href: string): boolean {
|
|
if (!pathname) return false
|
|
if (href === '/admin') return pathname === '/admin'
|
|
if (href === '/admin/ai') return pathname === '/admin/ai' || pathname.startsWith('/admin/ai')
|
|
return pathname === href || pathname.startsWith(`${href}/`)
|
|
}
|
|
|
|
/**
|
|
* Barre latérale administration — même vocabulaire visuel que {@link Sidebar}
|
|
* (fond bureau, panneau vitré, navigation arrondie). Liens en <a> pour
|
|
* éviter les transitions RSC qui déclenchent React #310 entre groupes de routes.
|
|
*/
|
|
export function AdminSidebar({ className }: { className?: string }) {
|
|
const pathname = usePathname()
|
|
const { t } = useLanguage()
|
|
const { data: session } = useSession()
|
|
const user = session?.user
|
|
const initial = user?.name
|
|
? user.name.charAt(0).toUpperCase()
|
|
: user?.email?.[0]?.toUpperCase() ?? '?'
|
|
|
|
return (
|
|
<aside
|
|
className={cn(
|
|
'flex h-full min-h-0 w-64 shrink-0 flex-col sm:w-72 lg:w-80',
|
|
'border-e border-border/40 bg-white/30 backdrop-blur-md sidebar-shadow dark:border-border/30 dark:bg-sidebar/90',
|
|
className
|
|
)}
|
|
>
|
|
{/* Marque + retour app */}
|
|
<div className="flex flex-col gap-4 p-6 pb-4">
|
|
<div className="flex items-center justify-between gap-2">
|
|
<DropdownMenu>
|
|
<DropdownMenuTrigger asChild>
|
|
<button
|
|
type="button"
|
|
className="shrink-0 rounded-full outline-none ring-offset-background transition-shadow hover:ring-2 hover:ring-primary/30 focus-visible:ring-2 focus-visible:ring-ring"
|
|
aria-label={t('sidebar.accountMenu') || 'Account menu'}
|
|
>
|
|
<div className="flex size-10 items-center justify-center overflow-hidden rounded-full border border-border bg-muted shadow-sm">
|
|
<Avatar className="size-10 ring-1 ring-border/60">
|
|
<AvatarImage src={(user as { image?: string } | undefined)?.image} alt="" />
|
|
<AvatarFallback className="bg-primary/10 font-memento-serif text-lg font-semibold text-primary">
|
|
{initial}
|
|
</AvatarFallback>
|
|
</Avatar>
|
|
</div>
|
|
</button>
|
|
</DropdownMenuTrigger>
|
|
<DropdownMenuContent align="start" className="w-52 bg-popover border-border">
|
|
<DropdownMenuItem asChild className="cursor-pointer">
|
|
<a href="/settings/profile" className="flex items-center gap-2">
|
|
<User className="h-4 w-4" />
|
|
{t('settings.profile')}
|
|
</a>
|
|
</DropdownMenuItem>
|
|
<DropdownMenuItem asChild className="cursor-pointer">
|
|
<a href="/settings" className="flex items-center gap-2">
|
|
<Settings className="h-4 w-4" />
|
|
{t('nav.settings')}
|
|
</a>
|
|
</DropdownMenuItem>
|
|
<DropdownMenuSeparator />
|
|
<DropdownMenuItem
|
|
className="cursor-pointer text-destructive focus:text-destructive"
|
|
onClick={() => performSignOut('/login')}
|
|
>
|
|
<LogOut className="mr-2 h-4 w-4" />
|
|
{t('auth.signOut')}
|
|
</DropdownMenuItem>
|
|
</DropdownMenuContent>
|
|
</DropdownMenu>
|
|
|
|
<a
|
|
href="/home"
|
|
className={cn(
|
|
'flex min-w-0 flex-1 items-center gap-2 rounded-xl px-2 py-1.5 transition-colors',
|
|
'hover:bg-white/40 dark:hover:bg-white/10'
|
|
)}
|
|
>
|
|
<div className="flex size-8 shrink-0 items-center justify-center rounded-lg bg-primary text-primary-foreground shadow-sm">
|
|
<StickyNote className="size-4" />
|
|
</div>
|
|
<div className="min-w-0 text-left">
|
|
<p className="truncate font-memento-serif text-[13px] font-semibold tracking-tight text-foreground">
|
|
MEMENTO
|
|
</p>
|
|
<p className="truncate text-[10px] font-medium uppercase tracking-wider text-muted-foreground">
|
|
{t('admin.adminConsole')}
|
|
</p>
|
|
</div>
|
|
</a>
|
|
</div>
|
|
|
|
<a
|
|
href="/home"
|
|
className={cn(
|
|
'flex items-center gap-3 rounded-xl px-4 py-2.5 text-[13px] font-medium transition-all',
|
|
'text-muted-foreground hover:bg-white/40 hover:text-foreground dark:hover:bg-white/10'
|
|
)}
|
|
>
|
|
<div className="flex size-8 shrink-0 items-center justify-center rounded-full border border-border bg-white/60 dark:bg-white/10">
|
|
<ArrowLeft className="size-4" />
|
|
</div>
|
|
<span>{t('admin.backToApp')}</span>
|
|
</a>
|
|
|
|
<div className="flex items-center gap-2 rounded-full bg-primary/10 px-3 py-1.5 text-xs font-semibold text-primary dark:bg-primary/20">
|
|
<Shield className="size-3.5 shrink-0" />
|
|
<span>{t('admin.title')}</span>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Navigation */}
|
|
<div className="flex-1 overflow-y-auto px-4 pb-4 custom-scrollbar">
|
|
<p className="mb-3 px-4 text-[10px] font-bold uppercase tracking-widest text-muted-foreground">
|
|
{t('admin.navSection')}
|
|
</p>
|
|
<nav className="space-y-1">
|
|
{ADMIN_NAV_ITEMS.map((item) => {
|
|
const Icon = item.icon
|
|
const active = navItemIsActive(pathname, item.href)
|
|
return (
|
|
<a
|
|
key={item.href}
|
|
href={item.href}
|
|
className={cn(
|
|
'flex items-center gap-3 rounded-xl px-4 py-3 transition-all duration-300',
|
|
active
|
|
? 'memento-active-nav text-foreground'
|
|
: 'text-muted-foreground hover:bg-white/40 hover:text-foreground dark:hover:bg-white/10'
|
|
)}
|
|
>
|
|
<div
|
|
className={cn(
|
|
'flex size-8 shrink-0 items-center justify-center rounded-full border transition-colors',
|
|
active
|
|
? 'border-foreground bg-foreground text-background'
|
|
: 'border-border bg-white/60 dark:bg-white/10'
|
|
)}
|
|
>
|
|
<Icon className="size-4" />
|
|
</div>
|
|
<span className="text-[13px] font-medium">{t(item.titleKey)}</span>
|
|
</a>
|
|
)
|
|
})}
|
|
</nav>
|
|
</div>
|
|
|
|
{/* Pied */}
|
|
<div className="space-y-1 border-t border-border p-5 pt-4">
|
|
<div className="flex items-center gap-3 px-4 py-2">
|
|
<NotificationPanel />
|
|
<span className="text-[13px] font-medium text-muted-foreground">
|
|
{t('notification.notifications')}
|
|
</span>
|
|
</div>
|
|
<a
|
|
href="/settings"
|
|
className="flex items-center gap-3 rounded-lg px-4 py-2 text-[13px] font-medium text-muted-foreground transition-colors hover:bg-white/30 hover:text-foreground dark:hover:bg-white/10"
|
|
>
|
|
<Settings className="size-4" />
|
|
<span>{t('nav.settings')}</span>
|
|
</a>
|
|
</div>
|
|
</aside>
|
|
)
|
|
}
|