Files
mystuff_frontend/src/components/Sidebar.tsx
T
Lars Behrends 073c8a6c5d 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.
2026-04-26 02:18:01 +02:00

414 lines
15 KiB
TypeScript

import { useState } from 'react';
import { NavLink, useLocation, useNavigate } from 'react-router-dom';
import {
LayoutDashboard,
Library,
Users,
FolderKanban,
Database,
Settings,
Sun,
LogOut,
Menu,
X,
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';
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,
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 navigate = useNavigate();
const toggleTheme = () => {
setTheme(theme === 'dark' ? 'light' : 'dark');
};
const handleLogout = () => {
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 */}
<button
onClick={() => setIsMobileOpen(!isMobileOpen)}
className="lg:hidden fixed top-4 left-4 z-50 p-2 bg-card rounded-lg border border-border/50 hover:bg-muted transition-colors"
>
{isMobileOpen ? <X size={20} /> : <Menu size={20} />}
</button>
{/* Overlay for mobile */}
{isMobileOpen && (
<div
className="lg:hidden fixed inset-0 bg-black/50 z-40"
onClick={() => setIsMobileOpen(false)}
/>
)}
{/* Sidebar */}
<aside
className={cn(
'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-5">
<div className="flex items-center gap-3">
<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-lg font-bold text-white">{pageTitle || 'MediaVault'}</span>
</div>
</div>
{/* Navigation */}
<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-3 border-t border-white/5 space-y-1">
<button
onClick={toggleTheme}
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={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>
</>
);
}