veza/apps/web/src/components/layout/Sidebar.tsx

242 lines
10 KiB
TypeScript
Raw Normal View History

import React from 'react';
import { useNavigate, useLocation, Link } from 'react-router-dom';
import {
Home, Users, Disc, Radio, Settings, LogOut, ShoppingBag,
GraduationCap, BarChart2, Shield, Box, MessageSquare, Cloud,
Layers, Cpu, Heart, ListMusic, CreditCard, DollarSign, Terminal,
ChevronLeft, ChevronRight,
} from 'lucide-react';
import { NavItem } from '../../types';
import { useAuthStore } from '@/features/auth/store/authStore';
import { useUIStore } from '@/stores/ui';
import { cn } from '@/lib/utils';
import { Button } from '@/components/ui/button';
interface SidebarProps {
currentView?: string;
onNavigate?: (viewId: string) => void;
onLogout?: () => void;
}
const navItems: { section: string; items: NavItem[] }[] = [
{
section: 'My Studio',
items: [
{ id: 'dashboard', label: 'Command Center', icon: <Home className="w-4 h-4" /> },
{ id: 'studio', label: 'Cloud Files', icon: <Cloud className="w-4 h-4" /> },
{ id: 'tracks', label: 'Projects', icon: <Layers className="w-4 h-4" /> },
{ id: 'gear', label: 'Gear Locker', icon: <Box className="w-4 h-4" /> },
{ id: 'analytics', label: 'Performance', icon: <BarChart2 className="w-4 h-4" /> },
],
},
{
section: 'Veza Network',
items: [
{ id: 'social', label: 'Community Feed', icon: <Users className="w-4 h-4" /> },
{ id: 'marketplace', label: 'Marketplace', icon: <ShoppingBag className="w-4 h-4" /> },
{ id: 'live', label: 'Live Sessions', icon: <Radio className="w-4 h-4" />, badge: 3 },
{ id: 'chat', label: 'Channels', icon: <MessageSquare className="w-4 h-4" />, badge: 12 },
{ id: 'education', label: 'Academy', icon: <GraduationCap className="w-4 h-4" /> },
],
},
{
section: 'Commerce',
items: [
{ id: 'sell', label: 'Seller Dashboard', icon: <DollarSign className="w-4 h-4" /> },
{ id: 'wishlist', label: 'Wishlist', icon: <Heart className="w-4 h-4" /> },
{ id: 'purchases', label: 'Purchases', icon: <CreditCard className="w-4 h-4" /> },
],
},
{
section: 'Library',
items: [
{ id: 'playlists', label: 'Playlists', icon: <ListMusic className="w-4 h-4" /> },
{ id: 'queue', label: 'Play Queue', icon: <Disc className="w-4 h-4" /> },
],
},
{
section: 'System',
items: [
{ id: 'developer', label: 'Developer API', icon: <Terminal className="w-4 h-4" /> },
{ id: 'admin', label: 'Admin Panel', icon: <Shield className="w-4 h-4" /> },
],
},
];
const routeMap: Record<string, string> = {
dashboard: '/dashboard', studio: '/library', tracks: '/library', gear: '/gear',
analytics: '/analytics', social: '/social', marketplace: '/marketplace', live: '/live',
chat: '/chat', education: '/education', sell: '/sell', wishlist: '/wishlist',
purchases: '/purchases', playlists: '/playlists', queue: '/queue', developer: '/developer',
admin: '/admin', settings: '/settings',
};
export const Sidebar: React.FC<SidebarProps> = ({ currentView, onNavigate, onLogout }) => {
const navigate = useNavigate();
const location = useLocation();
const { logout } = useAuthStore();
const { sidebarOpen, setSidebarOpen } = useUIStore();
const handleMobileNav = () => {
if (window.innerWidth < 1024) setSidebarOpen(false);
};
const activeView = currentView || Object.keys(routeMap).find((key) => routeMap[key] === location.pathname) || 'dashboard';
const handleLogout = () => {
logout();
navigate('/login');
onLogout?.();
};
return (
<>
{sidebarOpen && (
<div
className="fixed inset-0 bg-black/60 backdrop-blur-sm lg:hidden z-[90]"
onClick={() => setSidebarOpen(false)}
aria-hidden="true"
/>
)}
<aside
className={cn(
'fixed left-6 bottom-6 top-24 rounded-xl flex flex-col transition-all duration-500 ease-[cubic-bezier(0.33,1,0.68,1)] z-[95] overflow-hidden border border-white/5 bg-[var(--sidebar)] backdrop-blur-md',
sidebarOpen ? 'w-64 translate-x-0 opacity-100' : '-translate-x-full lg:translate-x-0 lg:opacity-100 lg:w-20'
)}
>
{/* Header */}
<div className="p-6 border-b border-white/5 flex items-center gap-4 relative">
<div className="w-8 h-8 rounded-lg bg-muted/20 flex items-center justify-center border border-white/10 shadow-[0_0_15px_rgba(0,0,0,0.5)] flex-shrink-0">
<Cpu className="w-4 h-4 text-muted-foreground" />
</div>
2026-01-22 16:23:11 +00:00
<div className={cn("transition-all duration-300 overflow-hidden", sidebarOpen ? "w-auto opacity-100" : "w-0 opacity-0")}>
<h2 className="text-xs font-display font-bold text-foreground tracking-widest uppercase whitespace-nowrap">
2026-01-22 16:23:11 +00:00
System Hub
</h2>
<div className="flex items-center gap-1.5 mt-0.5">
<span className="w-1.5 h-1.5 rounded-full bg-primary animate-pulse shadow-[0_0_8px_var(--color-primary)]" />
<span className="text-[9px] font-mono text-primary opacity-90 uppercase whitespace-nowrap tracking-wider">
Online
2026-01-22 16:23:11 +00:00
</span>
</div>
2026-01-22 16:23:11 +00:00
</div>
<Button
variant="ghost"
size="icon"
onClick={() => setSidebarOpen(!sidebarOpen)}
2026-01-22 16:23:11 +00:00
className={cn(
"ml-auto text-muted-foreground hover:text-white hidden lg:flex hover:bg-white/5",
!sidebarOpen && "absolute left-1/2 -translate-x-1/2 top-1/2 -translate-y-1/2"
2026-01-22 16:23:11 +00:00
)}
>
{sidebarOpen ? <ChevronLeft className="w-4 h-4" /> : <ChevronRight className="w-4 h-4" />}
</Button>
</div>
{/* Nav Content — audit §4.2: more space between sections */}
<div className="flex-1 overflow-y-auto custom-scrollbar p-3 space-y-8">
{navItems.map((group, idx) => (
<div key={idx}>
2026-01-22 16:23:11 +00:00
<h3 className={cn(
"text-[9px] font-mono font-bold text-muted-foreground/50 uppercase tracking-[0.2em] mb-3 mt-1 px-3 transition-all duration-300",
!sidebarOpen && "opacity-0 h-0 overflow-hidden mb-0 mt-0"
2026-01-22 16:23:11 +00:00
)}>
{group.section}
</h3>
<div className="space-y-1">
{group.items.map((item) => {
const route = routeMap[item.id] || '/dashboard';
const isActive = activeView === item.id;
return (
<Link
key={item.id}
to={route}
onClick={() => {
handleMobileNav();
onNavigate?.(item.id);
}}
className={cn(
'w-full flex items-center px-3 py-2.5 rounded-xl text-sm font-medium transition-all duration-[var(--duration-immersive)] ease-in-out group relative overflow-hidden',
isActive
? 'bg-primary/10 text-foreground shadow-[inset_2px_0_0_0_var(--color-primary)]'
: 'text-muted-foreground/60 hover:text-primary hover:bg-white/5',
!sidebarOpen && "justify-center px-0"
)}
title={!sidebarOpen ? item.label : undefined}
>
{/* Glow for active state */}
{isActive && (
<div className="absolute inset-0 bg-gradient-to-r from-primary/10 to-transparent opacity-50 pointer-events-none" />
)}
<div className={cn("flex items-center gap-3 relative z-10", !sidebarOpen && "justify-center")}>
<span className={cn(
'transition-colors duration-[var(--duration-immersive)] ease-in-out',
isActive ? 'text-primary' : 'text-muted-foreground/60 group-hover:text-primary'
)}>
{item.icon}
</span>
2026-01-22 16:23:11 +00:00
<span className={cn(
"transition-all duration-300 whitespace-nowrap origin-left font-sans text-[13px]",
2026-01-22 16:23:11 +00:00
sidebarOpen ? "w-auto opacity-100 scale-100" : "w-0 opacity-0 scale-95 hidden"
)}>
{item.label}
</span>
</div>
{/* Badges — primary (teal) only, no magenta (audit: cohérence des accents) */}
{item.badge && sidebarOpen && (
<span className="ml-auto bg-primary/20 text-primary text-[9px] px-1.5 py-0.5 rounded font-mono font-bold border border-primary/30">
{item.badge}
</span>
)}
2026-01-22 16:23:11 +00:00
{item.badge && !sidebarOpen && (
<span className="absolute top-2 right-2 w-1.5 h-1.5 rounded-full bg-primary shadow-[0_0_5px_var(--color-primary)]" />
2026-01-22 16:23:11 +00:00
)}
</Link>
);
})}
</div>
</div>
))}
</div>
{/* Footer */}
<div className="p-3 border-t border-white/5 bg-black/20 backdrop-blur-sm">
<Link
to="/settings"
onClick={() => { handleMobileNav(); onNavigate?.('settings'); }}
className={cn(
'flex items-center gap-3 px-3 py-2.5 rounded-xl text-sm transition-all duration-[var(--duration-immersive)] ease-in-out',
activeView === 'settings' ? 'bg-primary/10 text-primary' : 'text-muted-foreground/60 hover:text-primary hover:bg-white/5',
2026-01-22 16:23:11 +00:00
!sidebarOpen && "justify-center"
)}
>
<Settings className="w-4 h-4" />
<span className={cn("transition-all duration-300 whitespace-nowrap", sidebarOpen ? "opacity-100" : "w-0 opacity-0 hidden")}>Settings</span>
</Link>
2026-01-22 16:23:11 +00:00
<Button
variant="ghost"
onClick={handleLogout}
2026-01-22 16:23:11 +00:00
className={cn(
"w-full text-destructive/80 hover:text-destructive hover:bg-destructive/10 mt-1 gap-3 justify-start",
!sidebarOpen && "justify-center px-0"
)}
2026-01-22 16:23:11 +00:00
>
<LogOut className="w-4 h-4" />
<span className={cn("transition-all duration-300 whitespace-nowrap", sidebarOpen ? "opacity-100" : "w-0 opacity-0 hidden")}>Sign Out</span>
</Button>
</div>
</aside>
</>
);
};