Add Sidebar, restructure App and DetailView

Add a new Sidebar component and integrate it into App.tsx (replacing Header), updating overall layout to a two-column flex layout and moving/footer adjustments. Substantially refactor DetailView: new responsive layout, progress bar, tabbed navigation (Overview, Cast, Tracks, Seasons, etc.), improved cast and tracks UI, various icon and metadata display tweaks, and several UX/responsiveness fixes. Also add AGENTS.md (project development guide) and minor related imports/cleanup across changed files.
This commit is contained in:
Lars Behrends
2026-04-16 13:51:08 +02:00
parent b57b22c30b
commit a407b57006
4 changed files with 735 additions and 230 deletions
+217
View File
@@ -0,0 +1,217 @@
import { useState } from 'react';
import { NavLink, useLocation } from 'react-router-dom';
import {
LayoutDashboard,
BookOpen,
Film,
Tv,
Gamepad2,
Users,
Tag,
Music as MusicIcon,
Monitor,
Eye,
Dumbbell,
Calendar,
FolderKanban,
Settings,
Sun,
LogOut,
ChevronDown,
ChevronRight,
Menu,
X
} 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;
}
export default function Sidebar({ enabledCategories, onToggleCategory }: SidebarProps) {
const [isMediaExpanded, setIsMediaExpanded] = useState(true);
const [isMobileOpen, setIsMobileOpen] = useState(false);
const { theme, setTheme } = useTheme();
const location = useLocation();
const categoryPaths: Record<MediaCategory, string> = {
'Anime': 'anime',
'Movies': 'movies',
'TV Series': 'tv-series',
'Music': 'music',
'Books': 'books',
'Games': 'games',
'Consoles': 'consoles',
'Adult': 'adult'
};
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: <Settings size={18} />, label: 'Settings', path: '/settings' }
];
const toggleTheme = () => {
setTheme(theme === 'dark' ? 'light' : 'dark');
};
const handleLogout = () => {
console.log('Logout clicked');
};
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-72 bg-card border-r border-border/50 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="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>
<span className="text-xl font-black text-foreground">kyoo</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>
)}
</div>
))}
</nav>
{/* Bottom section */}
<div className="p-4 border-t border-border/50 space-y-2">
<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"
>
<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>
</button>
</div>
</aside>
</>
);
}