Integrate shadcn UI & add UI primitives
Integrates the shadcn/ui design system across the app and adds a collection of reusable UI primitives and layout components. Adds new UI atoms/molecules (avatar, card, collapsible, progress, select, sheet, sidebar, skeleton, table, tabs, toggles, tooltip), app sidebar, media filters, MediaTable, and a mobile hook; updates many views/components to use the new UI. Updates AGENTS.md with styling, layout, accessibility and design standards (Tailwind/shadcn guidance) and adds a registry entry to components.json. Also updates dependencies/lockfile to align shadcn and related packages.
This commit is contained in:
+336
-135
@@ -1,86 +1,58 @@
|
||||
import { useState } from 'react';
|
||||
import { NavLink, useLocation } from 'react-router-dom';
|
||||
import { NavLink, useLocation, useNavigate } from 'react-router-dom';
|
||||
import {
|
||||
LayoutDashboard,
|
||||
BookOpen,
|
||||
Film,
|
||||
Tv,
|
||||
Gamepad2,
|
||||
Library,
|
||||
Users,
|
||||
Tag,
|
||||
Music as MusicIcon,
|
||||
Monitor,
|
||||
Eye,
|
||||
Dumbbell,
|
||||
Calendar,
|
||||
FolderKanban,
|
||||
Database,
|
||||
Settings,
|
||||
Sun,
|
||||
LogOut,
|
||||
ChevronDown,
|
||||
ChevronRight,
|
||||
Menu,
|
||||
X,
|
||||
Plus
|
||||
Plus,
|
||||
Film,
|
||||
Tv,
|
||||
Gamepad2,
|
||||
Heart,
|
||||
Eye,
|
||||
Flame,
|
||||
Clock,
|
||||
ChevronRight
|
||||
} from 'lucide-react';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { useTheme } from '@/contexts/ThemeContext';
|
||||
import { MediaCategory } from '@/types';
|
||||
import { CATEGORY_PATHS } from '@/constants';
|
||||
|
||||
interface SidebarProps {
|
||||
enabledCategories: MediaCategory[];
|
||||
onToggleCategory: (category: MediaCategory) => void;
|
||||
pageTitle?: string;
|
||||
mediaCounts?: {
|
||||
all: number;
|
||||
movies: number;
|
||||
series: number;
|
||||
games: number;
|
||||
adult: number;
|
||||
favorites: number;
|
||||
};
|
||||
activeFilter?: string;
|
||||
onFilterChange?: (filter: string) => void;
|
||||
}
|
||||
|
||||
export default function Sidebar({ enabledCategories, onToggleCategory, pageTitle }: SidebarProps) {
|
||||
const [isMediaExpanded, setIsMediaExpanded] = useState(true);
|
||||
export default function Sidebar({
|
||||
enabledCategories,
|
||||
onToggleCategory,
|
||||
pageTitle,
|
||||
mediaCounts = { all: 24, movies: 8, series: 6, games: 6, adult: 4, favorites: 11 },
|
||||
activeFilter = 'all',
|
||||
onFilterChange
|
||||
}: SidebarProps) {
|
||||
const [isMobileOpen, setIsMobileOpen] = useState(false);
|
||||
const { theme, setTheme } = useTheme();
|
||||
const location = useLocation();
|
||||
|
||||
|
||||
const categoryIcons: Record<string, any> = {
|
||||
'Audio Book': <BookOpen size={18} />,
|
||||
'Book': <BookOpen size={18} />,
|
||||
'Movie': <Film size={18} />,
|
||||
'Music': <MusicIcon size={18} />,
|
||||
'Show': <Tv size={18} />,
|
||||
'Video Game': <Gamepad2 size={18} />,
|
||||
'Consoles': <Monitor size={18} />,
|
||||
'Adult': <Eye size={18} />,
|
||||
'Groups': <Users size={18} />,
|
||||
'People': <Users size={18} />,
|
||||
'Genres': <Tag size={18} />
|
||||
};
|
||||
|
||||
const navItems = [
|
||||
{ icon: <LayoutDashboard size={18} />, label: 'Dashboard', path: '/' },
|
||||
{
|
||||
icon: <Film size={18} />,
|
||||
label: 'Media',
|
||||
hasSubmenu: true,
|
||||
submenu: [
|
||||
...(enabledCategories.includes('Anime') ? [{ label: 'Anime', path: '/anime' }] : []),
|
||||
...(enabledCategories.includes('Books') ? [{ label: 'Book', path: '/books' }] : []),
|
||||
...(enabledCategories.includes('Movies') ? [{ label: 'Movie', path: '/movies' }] : []),
|
||||
...(enabledCategories.includes('Music') ? [{ label: 'Music', path: '/music' }] : []),
|
||||
...(enabledCategories.includes('TV Series') ? [{ label: 'Show', path: '/tv-series' }] : []),
|
||||
...(enabledCategories.includes('Games') ? [{ label: 'Video Game', path: '/games' }] : []),
|
||||
...(enabledCategories.includes('Consoles') ? [{ label: 'Consoles', path: '/consoles' }] : []),
|
||||
...(enabledCategories.includes('Adult') ? [{ label: 'Adult', path: '/adult' }] : []),
|
||||
{ label: 'People', path: '/cast' },
|
||||
{ label: 'Genres', path: '/browse' }
|
||||
].filter(Boolean)
|
||||
},
|
||||
//{ icon: <Dumbbell size={18} />, label: 'Fitness', path: '/fitness' },
|
||||
//{ icon: <Calendar size={18} />, label: 'Calendar', path: '/calendar' },
|
||||
//{ icon: <FolderKanban size={18} />, label: 'Collections', path: '/collections' },
|
||||
{ icon: <Plus size={18} />, label: 'Add Media', path: '/add' },
|
||||
{ icon: <Settings size={18} />, label: 'Settings', path: '/settings' },
|
||||
{ icon: <FolderKanban size={18} />, label: 'Import', path: '/import' }
|
||||
];
|
||||
const navigate = useNavigate();
|
||||
|
||||
const toggleTheme = () => {
|
||||
setTheme(theme === 'dark' ? 'light' : 'dark');
|
||||
@@ -90,6 +62,36 @@ export default function Sidebar({ enabledCategories, onToggleCategory, pageTitle
|
||||
console.log('Logout clicked');
|
||||
};
|
||||
|
||||
const handleFilterClick = (filter: string) => {
|
||||
onFilterChange?.(filter);
|
||||
if (filter === 'all') {
|
||||
navigate('/browse');
|
||||
} else if (filter === 'movies') {
|
||||
navigate('/movies');
|
||||
} else if (filter === 'series') {
|
||||
navigate('/tv-series');
|
||||
} else if (filter === 'games') {
|
||||
navigate('/games');
|
||||
} else if (filter === 'adult') {
|
||||
navigate('/adult');
|
||||
} else if (filter === 'favorites') {
|
||||
navigate('/browse?favorites=true');
|
||||
}
|
||||
};
|
||||
|
||||
const handleQuickFilter = (filter: string) => {
|
||||
if (filter === 'most-played') {
|
||||
navigate('/browse?sort=plays');
|
||||
} else if (filter === 'recently-added') {
|
||||
navigate('/browse?sort=recent');
|
||||
}
|
||||
};
|
||||
|
||||
const isActive = (path: string) => {
|
||||
if (path === '/') return location.pathname === '/';
|
||||
return location.pathname.startsWith(path);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* Mobile menu button */}
|
||||
@@ -111,100 +113,299 @@ export default function Sidebar({ enabledCategories, onToggleCategory, pageTitle
|
||||
{/* Sidebar */}
|
||||
<aside
|
||||
className={cn(
|
||||
'fixed left-0 top-0 bottom-0 w-72 bg-card border-r border-border/50 z-50 flex flex-col transition-transform duration-300',
|
||||
'fixed left-0 top-0 bottom-0 w-64 bg-[#0d0f14] border-r border-white/5 z-50 flex flex-col transition-transform duration-300',
|
||||
isMobileOpen ? 'translate-x-0' : '-translate-x-full lg:translate-x-0'
|
||||
)}
|
||||
>
|
||||
{/* Logo */}
|
||||
<div className="p-6 border-b border-border/50">
|
||||
<div className="p-5">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-10 h-10 bg-gradient-to-br from-[#6d28d9] to-[#8b5cf6] rounded-xl flex items-center justify-center shadow-lg shadow-[#6d28d9]/30">
|
||||
<div className="w-5 h-5 rounded-full bg-white" />
|
||||
<div className="w-8 h-8 bg-gradient-to-br from-[#e8466c] to-[#f47298] rounded-lg flex items-center justify-center">
|
||||
<svg viewBox="0 0 24 24" className="w-5 h-5 text-white" fill="currentColor">
|
||||
<path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-2 15l-5-5 1.41-1.41L10 14.17l7.59-7.59L19 8l-9 9z"/>
|
||||
</svg>
|
||||
</div>
|
||||
<span className="text-xl font-black text-foreground">{pageTitle || 'omnyx'}</span>
|
||||
<span className="text-lg font-bold text-white">{pageTitle || 'MediaVault'}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Navigation */}
|
||||
<nav className="flex-1 overflow-y-auto p-4 space-y-2">
|
||||
{navItems.map((item) => (
|
||||
<div key={item.label}>
|
||||
{item.hasSubmenu ? (
|
||||
<div>
|
||||
<button
|
||||
onClick={() => setIsMediaExpanded(!isMediaExpanded)}
|
||||
className="w-full flex items-center justify-between px-4 py-3 rounded-xl hover:bg-muted/50 transition-colors group"
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="text-muted-foreground group-hover:text-foreground transition-colors">
|
||||
{item.icon}
|
||||
</div>
|
||||
<span className="font-bold text-foreground">{item.label}</span>
|
||||
</div>
|
||||
{isMediaExpanded ? <ChevronDown size={16} /> : <ChevronRight size={16} />}
|
||||
</button>
|
||||
{isMediaExpanded && item.submenu && (
|
||||
<div className="ml-4 mt-1 space-y-1">
|
||||
{item.submenu.map((subItem) => (
|
||||
<NavLink
|
||||
key={subItem.label}
|
||||
to={subItem.path}
|
||||
onClick={() => setIsMobileOpen(false)}
|
||||
className={({ isActive }) =>
|
||||
cn(
|
||||
'flex items-center gap-3 px-4 py-2.5 rounded-xl text-sm font-medium transition-colors',
|
||||
isActive
|
||||
? 'bg-[#6d28d9]/10 text-[#6d28d9]'
|
||||
: 'text-muted-foreground hover:text-foreground hover:bg-muted/50'
|
||||
)
|
||||
}
|
||||
>
|
||||
{categoryIcons[subItem.label]}
|
||||
{subItem.label}
|
||||
</NavLink>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<NavLink
|
||||
to={item.path}
|
||||
onClick={() => setIsMobileOpen(false)}
|
||||
className={({ isActive }) =>
|
||||
cn(
|
||||
'flex items-center gap-3 px-4 py-3 rounded-xl transition-colors group',
|
||||
isActive
|
||||
? 'bg-[#6d28d9]/10 text-[#6d28d9]'
|
||||
: 'text-muted-foreground hover:text-foreground hover:bg-muted/50'
|
||||
)
|
||||
}
|
||||
>
|
||||
<div className={cn('transition-colors', location.pathname === item.path ? 'text-[#6d28d9]' : 'group-hover:text-foreground')}>
|
||||
{item.icon}
|
||||
</div>
|
||||
<span className="font-bold">{item.label}</span>
|
||||
</NavLink>
|
||||
)}
|
||||
<nav className="flex-1 overflow-y-auto px-3 py-2 space-y-1">
|
||||
{/* Main Navigation */}
|
||||
<NavLink
|
||||
to="/"
|
||||
onClick={() => setIsMobileOpen(false)}
|
||||
className={cn(
|
||||
'flex items-center gap-3 px-3 py-2.5 rounded-lg transition-colors group',
|
||||
isActive('/')
|
||||
? 'bg-[#e8466c]/10 text-[#e8466c]'
|
||||
: 'text-gray-400 hover:text-white hover:bg-white/5'
|
||||
)}
|
||||
>
|
||||
<LayoutDashboard size={18} />
|
||||
<span className="font-medium text-sm">Dashboard</span>
|
||||
</NavLink>
|
||||
|
||||
<NavLink
|
||||
to="/browse"
|
||||
onClick={() => setIsMobileOpen(false)}
|
||||
className={cn(
|
||||
'flex items-center gap-3 px-3 py-2.5 rounded-lg transition-colors group',
|
||||
isActive('/browse') || isActive('/movies') || isActive('/tv-series') || isActive('/games') || isActive('/adult')
|
||||
? 'bg-[#e8466c]/10 text-[#e8466c]'
|
||||
: 'text-gray-400 hover:text-white hover:bg-white/5'
|
||||
)}
|
||||
>
|
||||
<Library size={18} />
|
||||
<span className="font-medium text-sm">Library</span>
|
||||
</NavLink>
|
||||
|
||||
<NavLink
|
||||
to="/cast"
|
||||
onClick={() => setIsMobileOpen(false)}
|
||||
className={cn(
|
||||
'flex items-center gap-3 px-3 py-2.5 rounded-lg transition-colors group',
|
||||
isActive('/cast')
|
||||
? 'bg-[#e8466c]/10 text-[#e8466c]'
|
||||
: 'text-gray-400 hover:text-white hover:bg-white/5'
|
||||
)}
|
||||
>
|
||||
<Users size={18} />
|
||||
<span className="font-medium text-sm">Actors</span>
|
||||
</NavLink>
|
||||
|
||||
<NavLink
|
||||
to="/collections"
|
||||
onClick={() => setIsMobileOpen(false)}
|
||||
className={cn(
|
||||
'flex items-center gap-3 px-3 py-2.5 rounded-lg transition-colors group',
|
||||
isActive('/collections')
|
||||
? 'bg-[#e8466c]/10 text-[#e8466c]'
|
||||
: 'text-gray-400 hover:text-white hover:bg-white/5'
|
||||
)}
|
||||
>
|
||||
<FolderKanban size={18} />
|
||||
<span className="font-medium text-sm">Collections</span>
|
||||
</NavLink>
|
||||
|
||||
<NavLink
|
||||
to="/sources"
|
||||
onClick={() => setIsMobileOpen(false)}
|
||||
className={cn(
|
||||
'flex items-center gap-3 px-3 py-2.5 rounded-lg transition-colors group',
|
||||
isActive('/sources')
|
||||
? 'bg-[#e8466c]/10 text-[#e8466c]'
|
||||
: 'text-gray-400 hover:text-white hover:bg-white/5'
|
||||
)}
|
||||
>
|
||||
<Database size={18} />
|
||||
<span className="font-medium text-sm">Sources</span>
|
||||
</NavLink>
|
||||
|
||||
<NavLink
|
||||
to="/settings"
|
||||
onClick={() => setIsMobileOpen(false)}
|
||||
className={cn(
|
||||
'flex items-center gap-3 px-3 py-2.5 rounded-lg transition-colors group',
|
||||
isActive('/settings')
|
||||
? 'bg-[#e8466c]/10 text-[#e8466c]'
|
||||
: 'text-gray-400 hover:text-white hover:bg-white/5'
|
||||
)}
|
||||
>
|
||||
<Settings size={18} />
|
||||
<span className="font-medium text-sm">Settings</span>
|
||||
</NavLink>
|
||||
|
||||
{/* MEDIA TYPE Section */}
|
||||
<div className="mt-6">
|
||||
<div className="px-3 mb-2">
|
||||
<span className="text-xs font-semibold text-gray-500 uppercase tracking-wider">Media Type</span>
|
||||
</div>
|
||||
))}
|
||||
|
||||
<button
|
||||
onClick={() => handleFilterClick('all')}
|
||||
className={cn(
|
||||
'w-full flex items-center justify-between px-3 py-2 rounded-lg transition-colors group',
|
||||
activeFilter === 'all'
|
||||
? 'bg-[#e8466c]/10 text-[#e8466c]'
|
||||
: 'text-gray-400 hover:text-white hover:bg-white/5'
|
||||
)}
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
<Library size={16} />
|
||||
<span className="text-sm">All</span>
|
||||
</div>
|
||||
<span className={cn(
|
||||
'text-xs px-2 py-0.5 rounded-full',
|
||||
activeFilter === 'all' ? 'bg-[#e8466c]/20 text-[#e8466c]' : 'bg-white/10 text-gray-500'
|
||||
)}>
|
||||
{mediaCounts.all}
|
||||
</span>
|
||||
</button>
|
||||
|
||||
{enabledCategories.includes('Movies') && (
|
||||
<button
|
||||
onClick={() => handleFilterClick('movies')}
|
||||
className={cn(
|
||||
'w-full flex items-center justify-between px-3 py-2 rounded-lg transition-colors group',
|
||||
activeFilter === 'movies' || location.pathname === '/movies'
|
||||
? 'bg-[#e8466c]/10 text-[#e8466c]'
|
||||
: 'text-gray-400 hover:text-white hover:bg-white/5'
|
||||
)}
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
<Film size={16} />
|
||||
<span className="text-sm">Movies</span>
|
||||
</div>
|
||||
<span className={cn(
|
||||
'text-xs px-2 py-0.5 rounded-full',
|
||||
activeFilter === 'movies' || location.pathname === '/movies' ? 'bg-[#e8466c]/20 text-[#e8466c]' : 'bg-white/10 text-gray-500'
|
||||
)}>
|
||||
{mediaCounts.movies}
|
||||
</span>
|
||||
</button>
|
||||
)}
|
||||
|
||||
{enabledCategories.includes('TV Series') && (
|
||||
<button
|
||||
onClick={() => handleFilterClick('series')}
|
||||
className={cn(
|
||||
'w-full flex items-center justify-between px-3 py-2 rounded-lg transition-colors group',
|
||||
activeFilter === 'series' || location.pathname === '/tv-series'
|
||||
? 'bg-[#e8466c]/10 text-[#e8466c]'
|
||||
: 'text-gray-400 hover:text-white hover:bg-white/5'
|
||||
)}
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
<Tv size={16} />
|
||||
<span className="text-sm">Series</span>
|
||||
</div>
|
||||
<span className={cn(
|
||||
'text-xs px-2 py-0.5 rounded-full',
|
||||
activeFilter === 'series' || location.pathname === '/tv-series' ? 'bg-[#e8466c]/20 text-[#e8466c]' : 'bg-white/10 text-gray-500'
|
||||
)}>
|
||||
{mediaCounts.series}
|
||||
</span>
|
||||
</button>
|
||||
)}
|
||||
|
||||
{enabledCategories.includes('Games') && (
|
||||
<button
|
||||
onClick={() => handleFilterClick('games')}
|
||||
className={cn(
|
||||
'w-full flex items-center justify-between px-3 py-2 rounded-lg transition-colors group',
|
||||
activeFilter === 'games' || location.pathname === '/games'
|
||||
? 'bg-[#e8466c]/10 text-[#e8466c]'
|
||||
: 'text-gray-400 hover:text-white hover:bg-white/5'
|
||||
)}
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
<Gamepad2 size={16} />
|
||||
<span className="text-sm">Games</span>
|
||||
</div>
|
||||
<span className={cn(
|
||||
'text-xs px-2 py-0.5 rounded-full',
|
||||
activeFilter === 'games' || location.pathname === '/games' ? 'bg-[#e8466c]/20 text-[#e8466c]' : 'bg-white/10 text-gray-500'
|
||||
)}>
|
||||
{mediaCounts.games}
|
||||
</span>
|
||||
</button>
|
||||
)}
|
||||
|
||||
{enabledCategories.includes('Adult') && (
|
||||
<button
|
||||
onClick={() => handleFilterClick('adult')}
|
||||
className={cn(
|
||||
'w-full flex items-center justify-between px-3 py-2 rounded-lg transition-colors group',
|
||||
activeFilter === 'adult' || location.pathname === '/adult'
|
||||
? 'bg-[#e8466c]/10 text-[#e8466c]'
|
||||
: 'text-gray-400 hover:text-white hover:bg-white/5'
|
||||
)}
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
<Eye size={16} />
|
||||
<span className="text-sm">Adult</span>
|
||||
</div>
|
||||
<span className={cn(
|
||||
'text-xs px-2 py-0.5 rounded-full',
|
||||
activeFilter === 'adult' || location.pathname === '/adult' ? 'bg-[#e8466c]/20 text-[#e8466c]' : 'bg-white/10 text-gray-500'
|
||||
)}>
|
||||
{mediaCounts.adult}
|
||||
</span>
|
||||
</button>
|
||||
)}
|
||||
|
||||
<button
|
||||
onClick={() => handleFilterClick('favorites')}
|
||||
className={cn(
|
||||
'w-full flex items-center justify-between px-3 py-2 rounded-lg transition-colors group',
|
||||
activeFilter === 'favorites'
|
||||
? 'bg-[#e8466c]/10 text-[#e8466c]'
|
||||
: 'text-gray-400 hover:text-white hover:bg-white/5'
|
||||
)}
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
<Heart size={16} />
|
||||
<span className="text-sm">Favorites</span>
|
||||
</div>
|
||||
<span className={cn(
|
||||
'text-xs px-2 py-0.5 rounded-full',
|
||||
activeFilter === 'favorites' ? 'bg-[#e8466c]/20 text-[#e8466c]' : 'bg-white/10 text-gray-500'
|
||||
)}>
|
||||
{mediaCounts.favorites}
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* QUICK FILTER Section */}
|
||||
<div className="mt-6">
|
||||
<div className="px-3 mb-2">
|
||||
<span className="text-xs font-semibold text-gray-500 uppercase tracking-wider">Quick Filter</span>
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={() => handleQuickFilter('most-played')}
|
||||
className="w-full flex items-center gap-3 px-3 py-2 rounded-lg text-gray-400 hover:text-white hover:bg-white/5 transition-colors group"
|
||||
>
|
||||
<Flame size={16} className="text-orange-500" />
|
||||
<span className="text-sm">Most Played</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={() => handleQuickFilter('recently-added')}
|
||||
className="w-full flex items-center gap-3 px-3 py-2 rounded-lg text-gray-400 hover:text-white hover:bg-white/5 transition-colors group"
|
||||
>
|
||||
<Clock size={16} className="text-cyan-500" />
|
||||
<span className="text-sm">Recently Added</span>
|
||||
</button>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
{/* Bottom section */}
|
||||
<div className="p-4 border-t border-border/50 space-y-2">
|
||||
<div className="p-3 border-t border-white/5 space-y-1">
|
||||
<button
|
||||
onClick={toggleTheme}
|
||||
className="w-full flex items-center gap-3 px-4 py-3 rounded-xl text-muted-foreground hover:text-foreground hover:bg-muted/50 transition-colors"
|
||||
className="w-full flex items-center gap-3 px-3 py-2 rounded-lg text-gray-400 hover:text-white hover:bg-white/5 transition-colors"
|
||||
>
|
||||
<Sun size={18} />
|
||||
<span className="font-medium">{theme === 'dark' ? 'Light theme' : 'Dark theme'}</span>
|
||||
</button>
|
||||
<button
|
||||
onClick={handleLogout}
|
||||
className="w-full flex items-center gap-3 px-4 py-3 rounded-xl text-muted-foreground hover:text-foreground hover:bg-muted/50 transition-colors"
|
||||
>
|
||||
<LogOut size={18} />
|
||||
<span className="font-medium">Logout</span>
|
||||
<Sun size={16} />
|
||||
<span className="text-sm font-medium">{theme === 'dark' ? 'Light theme' : 'Dark theme'}</span>
|
||||
</button>
|
||||
|
||||
{/* User avatar */}
|
||||
<div className="flex items-center gap-3 px-3 py-3 mt-2">
|
||||
<div className="w-8 h-8 rounded-full bg-gradient-to-br from-[#e8466c] to-[#f47298] flex items-center justify-center text-white text-sm font-bold">
|
||||
N
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="text-sm font-medium text-white truncate">User</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={handleLogout}
|
||||
className="text-gray-400 hover:text-white transition-colors"
|
||||
>
|
||||
<ChevronRight size={16} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
</>
|
||||
|
||||
Reference in New Issue
Block a user