Files
Momento/memento-note/components/admin-sidebar.tsx
Antigravity 6fa12f6e13
All checks were successful
CI / Lint, Unit Tests & Build (push) Successful in 7m32s
CI / Deploy production (on server) (push) Successful in 27s
feat(admin): pages Marketing et Statistiques (sans envoi)
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.
2026-09-06 08:55:43 +00:00

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>
)
}