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:
+21
-25
@@ -6,7 +6,7 @@
|
||||
import { useState, useMemo, useEffect } from 'react';
|
||||
import { LayoutGroup } from 'motion/react';
|
||||
import { BrowserRouter, Routes, Route, useNavigate, useSearchParams, useParams, useLocation } from 'react-router-dom';
|
||||
import Header from './components/Header';
|
||||
import Sidebar from './components/Sidebar';
|
||||
import BrowseView from './components/BrowseView';
|
||||
import DashboardView from './components/DashboardView';
|
||||
import DetailView from './components/DetailView';
|
||||
@@ -338,17 +338,13 @@ function AppContent() {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-background font-sans selection:bg-[#6d28d9]/20 selection:text-[#6d28d9]">
|
||||
<Header
|
||||
onSearch={handleSearch}
|
||||
activeCategory={activeCategory}
|
||||
onCategoryChange={handleCategoryChange}
|
||||
<div className="min-h-screen bg-background font-sans selection:bg-[#6d28d9]/20 selection:text-[#6d28d9] flex">
|
||||
<Sidebar
|
||||
enabledCategories={enabledCategories}
|
||||
onToggleCategory={toggleCategory}
|
||||
transparent={location.pathname.startsWith('/media/') || location.pathname.startsWith('/cast/')}
|
||||
/>
|
||||
|
||||
<main>
|
||||
<main className="flex-1 lg:ml-72 flex flex-col">
|
||||
<LayoutGroup>
|
||||
<Routes>
|
||||
<Route path="/" element={
|
||||
@@ -493,25 +489,25 @@ function AppContent() {
|
||||
} />
|
||||
</Routes>
|
||||
</LayoutGroup>
|
||||
</main>
|
||||
|
||||
{/* Footer */}
|
||||
<footer className="py-8 px-6 border-t border-border/50 bg-muted/30 backdrop-blur-sm">
|
||||
<div className="max-w-[1920px] mx-auto flex flex-col md:flex-row items-center justify-between gap-4">
|
||||
<div className="flex items-center gap-2 text-lg font-black text-muted-foreground">
|
||||
<div className="w-5 h-5 bg-gradient-to-br from-[#6d28d9] to-[#8b5cf6] rounded-full" />
|
||||
<span className="bg-clip-text text-transparent bg-gradient-to-r from-foreground to-foreground/70">kyoo</span>
|
||||
{/* Footer */}
|
||||
<footer className="py-8 px-6 border-t border-border/50 bg-muted/30 backdrop-blur-sm mt-auto">
|
||||
<div className="max-w-[1920px] mx-auto flex flex-col md:flex-row items-center justify-between gap-4">
|
||||
<div className="flex items-center gap-2 text-lg font-black text-muted-foreground">
|
||||
<div className="w-5 h-5 bg-gradient-to-br from-[#6d28d9] to-[#8b5cf6] rounded-full" />
|
||||
<span className="bg-clip-text text-transparent bg-gradient-to-r from-foreground to-foreground/70">kyoo</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-6 text-sm font-bold text-muted-foreground">
|
||||
<a href="#" className="hover:text-[#6d28d9] transition-colors duration-300">Terms</a>
|
||||
<a href="#" className="hover:text-[#6d28d9] transition-colors duration-300">Privacy</a>
|
||||
<a href="#" className="hover:text-[#6d28d9] transition-colors duration-300">Contact</a>
|
||||
</div>
|
||||
<p className="text-xs font-medium text-muted-foreground">
|
||||
© 2026 Kyoo Media Discovery. All rights reserved.
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-6 text-sm font-bold text-muted-foreground">
|
||||
<a href="#" className="hover:text-[#6d28d9] transition-colors duration-300">Terms</a>
|
||||
<a href="#" className="hover:text-[#6d28d9] transition-colors duration-300">Privacy</a>
|
||||
<a href="#" className="hover:text-[#6d28d9] transition-colors duration-300">Contact</a>
|
||||
</div>
|
||||
<p className="text-xs font-medium text-muted-foreground">
|
||||
© 2026 Kyoo Media Discovery. All rights reserved.
|
||||
</p>
|
||||
</div>
|
||||
</footer>
|
||||
</footer>
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
+159
-205
@@ -10,7 +10,10 @@ import {
|
||||
ChevronRight,
|
||||
Search,
|
||||
ListFilter,
|
||||
ChevronDown
|
||||
ChevronDown,
|
||||
Calendar,
|
||||
Clock,
|
||||
Eye
|
||||
} from 'lucide-react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
@@ -28,6 +31,24 @@ export default function DetailView({ media, onPersonClick }: DetailViewProps) {
|
||||
const [castLimit, setCastLimit] = useState(6);
|
||||
const [showAllCast, setShowAllCast] = useState(false);
|
||||
const [expandedSeasons, setExpandedSeasons] = useState<Set<number>>(new Set());
|
||||
const [progress, setProgress] = useState(70.8);
|
||||
|
||||
const hasEpisodes = media.episodes && media.episodes.length > 0;
|
||||
const hasTracks = media.tracks && media.tracks.length > 0;
|
||||
const hasCast = media.staff && media.staff.length > 0;
|
||||
const tabs = [
|
||||
'Overview',
|
||||
...(hasCast ? ['Cast'] : []),
|
||||
'Actions',
|
||||
'History',
|
||||
...(hasEpisodes ? ['Seasons'] : []),
|
||||
...(hasTracks ? ['Tracks'] : []),
|
||||
'Reviews',
|
||||
'Suggestions',
|
||||
'Watch On'
|
||||
];
|
||||
|
||||
const [activeTab, setActiveTab] = useState(tabs[0]);
|
||||
|
||||
// Group episodes by season
|
||||
const episodesBySeason = useMemo(() => {
|
||||
@@ -68,31 +89,32 @@ export default function DetailView({ media, onPersonClick }: DetailViewProps) {
|
||||
|
||||
const displayedCast = showAllCast ? media.staff : (media.staff?.slice(0, castLimit) || []);
|
||||
const hasMoreCast = (media.staff?.length || 0) > castLimit;
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-background">
|
||||
{/* Banner */}
|
||||
<div className="relative h-[450px] w-full overflow-hidden">
|
||||
<img
|
||||
src={media.banner || media.poster}
|
||||
<img
|
||||
src={media.banner || media.poster}
|
||||
alt={media.title}
|
||||
className="w-full h-full object-cover"
|
||||
referrerPolicy="no-referrer"
|
||||
/>
|
||||
<div className="absolute inset-0 bg-gradient-to-t from-background via-background/50 to-transparent" />
|
||||
|
||||
<button
|
||||
|
||||
<button
|
||||
onClick={() => navigate(-1)}
|
||||
className="absolute top-24 left-6 p-3 bg-black/30 hover:bg-black/50 backdrop-blur-md text-white rounded-2xl transition-all duration-300 hover:scale-110 z-10 border border-white/20"
|
||||
className="absolute top-24 left-6 p-3 bg-black/30 hover:bg-black/50 backdrop-blur-md text-white rounded-2xl transition-all duration-300 hover:scale-110 z-10 border border-white/20 lg:left-80"
|
||||
>
|
||||
<ChevronLeft size={24} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<div className="max-w-[1920px] mx-auto px-6 -mt-32 relative z-10 pb-24">
|
||||
<div className="flex flex-col md:flex-row gap-8">
|
||||
{/* Left Column: Poster + Metadata */}
|
||||
<div className="w-full md:w-[320px] shrink-0">
|
||||
<div className="max-w-[1920px] mx-auto px-6 py-8 pb-24 -mt-32 relative z-10">
|
||||
<div className="flex flex-col lg:flex-row gap-8">
|
||||
{/* Left Column: Cover Image */}
|
||||
<div className="w-full lg:w-[400px] shrink-0">
|
||||
<motion.div
|
||||
layoutId={`media-${media.id}`}
|
||||
className={`rounded-2xl overflow-hidden shadow-2xl bg-card border border-border/50 ${
|
||||
@@ -108,184 +130,129 @@ export default function DetailView({ media, onPersonClick }: DetailViewProps) {
|
||||
referrerPolicy="no-referrer"
|
||||
/>
|
||||
</motion.div>
|
||||
|
||||
{/* Compact metadata under poster */}
|
||||
<div className="mt-4 space-y-2">
|
||||
{media.studios && media.studios.length > 0 && (
|
||||
<p className="text-xs font-bold text-muted-foreground">
|
||||
<span className="text-muted-foreground/70 uppercase tracking-widest mr-2">Studios:</span>
|
||||
{media.studios.join(', ')}
|
||||
</p>
|
||||
)}
|
||||
{media.developers && media.developers.length > 0 && (
|
||||
<div className="flex items-center gap-2 flex-wrap">
|
||||
<span className="text-xs font-bold text-muted-foreground uppercase tracking-widest">Developers:</span>
|
||||
{media.developers.map(dev => (
|
||||
<Badge key={dev} variant="secondary" className="bg-muted text-foreground hover:bg-muted/80 border-none px-2 py-0.5 font-bold text-[10px]">
|
||||
{dev}
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
{media.platforms && media.platforms.length > 0 && (
|
||||
<div className="flex items-center gap-2 flex-wrap">
|
||||
<span className="text-xs font-bold text-muted-foreground uppercase tracking-widest">Platforms:</span>
|
||||
{media.platforms.map(platform => (
|
||||
<Badge key={platform} variant="secondary" className="bg-muted text-foreground hover:bg-muted/80 border-none px-2 py-0.5 font-bold text-[10px]">
|
||||
{platform}
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
{media.categories && media.categories.length > 0 && (
|
||||
<div className="flex items-center gap-2 flex-wrap">
|
||||
<span className="text-xs font-bold text-muted-foreground uppercase tracking-widest">Categories:</span>
|
||||
{media.categories.map(category => (
|
||||
<Badge key={category} variant="secondary" className="bg-muted text-foreground hover:bg-muted/80 border-none px-2 py-0.5 font-bold text-[10px]">
|
||||
{category}
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
{media.completionStatus && (
|
||||
<p className="text-xs font-bold text-muted-foreground">
|
||||
<span className="text-muted-foreground/70 uppercase tracking-widest mr-2">Completion:</span>
|
||||
{media.completionStatus}
|
||||
</p>
|
||||
)}
|
||||
{media.source && (
|
||||
<p className="text-xs font-bold text-muted-foreground">
|
||||
<span className="text-muted-foreground/70 uppercase tracking-widest mr-2">Source:</span>
|
||||
{media.source}
|
||||
</p>
|
||||
)}
|
||||
{media.playCount !== undefined && media.playCount !== null && (
|
||||
<p className="text-xs font-bold text-muted-foreground">
|
||||
<span className="text-muted-foreground/70 uppercase tracking-widest mr-2">Play Count:</span>
|
||||
{media.playCount}
|
||||
</p>
|
||||
)}
|
||||
{media.playtime !== undefined && media.playtime !== null && media.playtime > 0 && (
|
||||
<p className="text-xs font-bold text-muted-foreground">
|
||||
<span className="text-muted-foreground/70 uppercase tracking-widest mr-2">Playtime:</span>
|
||||
{media.playtime}h
|
||||
</p>
|
||||
)}
|
||||
{media.lastActivity && (
|
||||
<p className="text-xs font-bold text-muted-foreground">
|
||||
<span className="text-muted-foreground/70 uppercase tracking-widest mr-2">Last Activity:</span>
|
||||
{media.lastActivity}
|
||||
</p>
|
||||
)}
|
||||
<div className="flex items-center gap-3">
|
||||
<span className="text-xs font-bold text-muted-foreground uppercase tracking-widest">Links:</span>
|
||||
<Button variant="link" className="p-0 h-auto text-[#6d28d9] font-bold text-xs">Tvdb</Button>
|
||||
<Button variant="link" className="p-0 h-auto text-[#6d28d9] font-bold text-xs">AniDb</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Right Column: Info */}
|
||||
<div className="flex-1 pt-4 md:pt-8">
|
||||
<div className="flex flex-wrap items-end justify-between gap-4 mb-8">
|
||||
<div>
|
||||
<h1 className="text-5xl font-black text-foreground mb-3">
|
||||
{media.title} <span className="text-muted-foreground font-medium">({media.year})</span>
|
||||
</h1>
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<Button size="icon" className="rounded-full bg-gradient-to-br from-[#6d28d9] to-[#8b5cf6] hover:from-[#5b21b6] hover:to-[#7c3aed] shadow-lg shadow-[#6d28d9]/30 transition-all duration-300 hover:scale-110">
|
||||
<Play size={20} fill="currentColor" />
|
||||
</Button>
|
||||
<Button size="icon" variant="outline" className="rounded-full border-border hover:border-[#6d28d9]/50 transition-all duration-300 hover:scale-110">
|
||||
<Bookmark size={20} />
|
||||
</Button>
|
||||
<Button size="icon" variant="outline" className="rounded-full border-border hover:border-[#6d28d9]/50 transition-all duration-300 hover:scale-110">
|
||||
<MoreHorizontal size={20} />
|
||||
</Button>
|
||||
</div>
|
||||
<div className="flex items-center gap-1 text-foreground font-bold">
|
||||
<Star size={20} className="text-yellow-500 fill-yellow-500" />
|
||||
{media.rating} / 10
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="hidden lg:block text-right">
|
||||
<h3 className="text-xs font-black text-[#6d28d9] uppercase tracking-wider mb-2">Genres</h3>
|
||||
<div className="flex flex-col items-end gap-1">
|
||||
{media.genres?.map(genre => (
|
||||
<span key={genre} className="text-sm font-bold text-foreground hover:text-[#6d28d9] cursor-pointer transition-colors duration-300">
|
||||
• {genre}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className="text-foreground leading-relaxed mb-6 max-w-3xl prose prose-sm dark:prose-invert"
|
||||
dangerouslySetInnerHTML={{ __html: media.description || '' }}
|
||||
/>
|
||||
|
||||
{/* Tags */}
|
||||
<div className="flex flex-wrap gap-2 mb-4">
|
||||
{media.tags?.map(tag => (
|
||||
<Badge key={tag} variant="secondary" className="bg-[#6d28d9]/10 text-[#6d28d9] hover:bg-[#6d28d9]/20 border-none px-3 py-1 font-bold text-[10px] uppercase tracking-wider">
|
||||
{tag}
|
||||
<div className="flex-1">
|
||||
{/* Header with tags */}
|
||||
<div className="flex flex-wrap items-center gap-3 mb-4">
|
||||
<h1 className="text-4xl lg:text-5xl font-black text-foreground">
|
||||
{media.title}
|
||||
</h1>
|
||||
{media.status && (
|
||||
<Badge className={
|
||||
media.status === 'watching' || media.status === 'reading' || media.status === 'listening' || media.status === 'playing'
|
||||
? 'bg-green-500/20 text-green-400 border-green-500/30 font-bold'
|
||||
: media.status === 'completed'
|
||||
? 'bg-blue-500/20 text-blue-400 border-blue-500/30 font-bold'
|
||||
: 'bg-gray-500/20 text-gray-400 border-gray-500/30 font-bold'
|
||||
}>
|
||||
{media.status.toUpperCase()}
|
||||
</Badge>
|
||||
))}
|
||||
)}
|
||||
{media.completionStatus && (
|
||||
<Badge className="bg-purple-500/20 text-purple-400 border-purple-500/30 font-bold">{media.completionStatus.toUpperCase()}</Badge>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Staff Section - Only show if staff data exists */}
|
||||
{media.staff && media.staff.length > 0 && (
|
||||
<section className="mt-20">
|
||||
<div className="flex items-center justify-between mb-8">
|
||||
<h2 className="text-3xl font-black text-foreground">Cast & Crew</h2>
|
||||
<div className="flex items-center gap-4">
|
||||
<span className="text-sm font-bold text-muted-foreground">
|
||||
{showAllCast ? media.staff.length : displayedCast.length} / {media.staff.length}
|
||||
</span>
|
||||
{hasMoreCast && (
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => setShowAllCast(!showAllCast)}
|
||||
className="rounded-xl border-border font-bold hover:border-[#6d28d9]/50 transition-all duration-300"
|
||||
>
|
||||
{showAllCast ? 'Show Less' : 'Show All'}
|
||||
<ChevronDown size={16} className={`ml-2 transition-transform ${showAllCast ? 'rotate-180' : ''}`} />
|
||||
</Button>
|
||||
)}
|
||||
{/* Show Details */}
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 lg:grid-cols-6 gap-4 mb-6">
|
||||
<div className="flex items-center gap-2 text-sm text-muted-foreground">
|
||||
<Calendar size={16} />
|
||||
<span>{media.year}</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 text-sm text-muted-foreground">
|
||||
<span>{media.status ? media.status.charAt(0).toUpperCase() + media.status.slice(1) : 'Unknown'}</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 text-sm text-muted-foreground">
|
||||
<Clock size={16} />
|
||||
<span>{media.playtime ? `${media.playtime}h` : '12h 30m'}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
{displayedCast.map(person => (
|
||||
|
||||
{/* Progress Bar */}
|
||||
<div className="mb-6">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<span className="text-sm font-bold text-foreground">Progress</span>
|
||||
<span className="text-sm font-bold text-[#6d28d9]">{progress}%</span>
|
||||
</div>
|
||||
<div className="w-full h-2 bg-muted rounded-full overflow-hidden">
|
||||
<div
|
||||
key={person.id}
|
||||
className="flex items-center gap-4 bg-card p-4 rounded-2xl shadow-sm border border-border/50 hover:shadow-xl hover:border-[#6d28d9]/30 transition-all duration-300 cursor-pointer group"
|
||||
onClick={() => onPersonClick(person)}
|
||||
className="h-full bg-gradient-to-r from-[#6d28d9] to-[#8b5cf6] transition-all duration-500"
|
||||
style={{ width: `${progress}%` }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Navigation Tabs */}
|
||||
<div className="flex flex-wrap gap-2 mb-6 border-b border-border/50 pb-4">
|
||||
{tabs.map(tab => (
|
||||
<button
|
||||
key={tab}
|
||||
onClick={() => setActiveTab(tab)}
|
||||
className={`px-4 py-2 rounded-lg text-sm font-medium transition-colors ${
|
||||
activeTab === tab
|
||||
? 'bg-[#6d28d9]/10 text-[#6d28d9]'
|
||||
: 'text-muted-foreground hover:text-foreground hover:bg-muted/50'
|
||||
}`}
|
||||
>
|
||||
<div className="w-16 h-20 rounded-xl overflow-hidden shrink-0 border border-border/30">
|
||||
<img src={person.photo} alt={person.name} className="w-full h-full object-cover group-hover:scale-105 transition-transform duration-300" referrerPolicy="no-referrer" />
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<h4 className="font-bold text-foreground truncate group-hover:text-[#6d28d9] transition-colors duration-300">{person.name}</h4>
|
||||
<p className="text-xs text-muted-foreground truncate">{person.characterName || person.role}</p>
|
||||
</div>
|
||||
<div className="w-16 h-20 rounded-xl overflow-hidden shrink-0 bg-muted border border-border/30">
|
||||
<img src={person.characterImage} alt={person.characterName} className="w-full h-full object-contain" referrerPolicy="no-referrer" />
|
||||
</div>
|
||||
</div>
|
||||
{tab}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
|
||||
{/* Episodes Section - Only show if episodes data exists */}
|
||||
{media.episodes && media.episodes.length > 0 && (
|
||||
{/* Genre Tags */}
|
||||
{activeTab === 'Overview' && (
|
||||
<div className="flex flex-wrap gap-2 mb-6">
|
||||
{media.genres?.map(genre => (
|
||||
<Badge key={genre} variant="secondary" className="bg-muted/50 text-foreground hover:bg-muted/80 border border-border/50 px-3 py-1 font-bold text-sm">
|
||||
{genre}
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Description */}
|
||||
{activeTab === 'Overview' && (
|
||||
<div
|
||||
className="text-foreground leading-relaxed mb-8 max-w-4xl prose prose-sm dark:prose-invert"
|
||||
dangerouslySetInnerHTML={{ __html: media.description || '' }}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Acting Section - Horizontal Scrollable */}
|
||||
{media.staff && media.staff.length > 0 && activeTab === 'Cast' && (
|
||||
<section className="mt-12">
|
||||
<h2 className="text-2xl font-black text-foreground mb-6">Acting</h2>
|
||||
<div className="flex gap-4 overflow-x-auto pb-4 scrollbar-thin scrollbar-thumb-border scrollbar-track-transparent">
|
||||
{displayedCast.map(person => (
|
||||
<div
|
||||
key={person.id}
|
||||
className="flex-shrink-0 w-48 bg-card p-4 rounded-2xl shadow-sm border border-border/50 hover:shadow-xl hover:border-[#6d28d9]/30 transition-all duration-300 cursor-pointer group"
|
||||
onClick={() => onPersonClick(person)}
|
||||
>
|
||||
<div className="w-full h-56 rounded-xl overflow-hidden mb-3 border border-border/30">
|
||||
<img src={person.photo} alt={person.name} className="w-full h-full object-cover group-hover:scale-105 transition-transform duration-300" referrerPolicy="no-referrer" />
|
||||
</div>
|
||||
<h4 className="font-bold text-foreground truncate group-hover:text-[#6d28d9] transition-colors duration-300">{person.name}</h4>
|
||||
<p className="text-xs text-muted-foreground truncate">{person.characterName || person.role}</p>
|
||||
</div>
|
||||
))}
|
||||
{hasMoreCast && (
|
||||
<button
|
||||
onClick={() => setShowAllCast(!showAllCast)}
|
||||
className="flex-shrink-0 w-48 bg-card p-4 rounded-2xl shadow-sm border border-border/50 hover:shadow-xl hover:border-[#6d28d9]/30 transition-all duration-300 flex items-center justify-center"
|
||||
>
|
||||
<span className="font-bold text-[#6d28d9]">
|
||||
{showAllCast ? 'Show Less' : `+${media.staff!.length - castLimit} more`}
|
||||
</span>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
{/* Episodes Section - Only show if episodes data exists and Seasons tab is active */}
|
||||
{media.episodes && media.episodes.length > 0 && activeTab === 'Seasons' && (
|
||||
<section className="mt-20">
|
||||
<div className="flex flex-col md:flex-row md:items-center justify-between gap-4 mb-8">
|
||||
<div className="flex items-center gap-6">
|
||||
@@ -365,8 +332,8 @@ export default function DetailView({ media, onPersonClick }: DetailViewProps) {
|
||||
</section>
|
||||
)}
|
||||
|
||||
{/* Tracks Section - Only show if tracks data exists (Music) */}
|
||||
{media.tracks && media.tracks.length > 0 && (
|
||||
{/* Tracks Section - Only show if tracks data exists and Tracks tab is active */}
|
||||
{media.tracks && media.tracks.length > 0 && activeTab === 'Tracks' && (
|
||||
<section className="mt-20">
|
||||
<div className="flex flex-col md:flex-row md:items-center justify-between gap-4 mb-8">
|
||||
<div className="flex items-center gap-6">
|
||||
@@ -388,37 +355,24 @@ export default function DetailView({ media, onPersonClick }: DetailViewProps) {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="border border-border/50 rounded-2xl overflow-hidden bg-card/50 backdrop-blur-sm">
|
||||
<div className="divide-y divide-border/50">
|
||||
{media.tracks
|
||||
.sort((a, b) => a.track_number - b.track_number)
|
||||
.map((track, index) => (
|
||||
<div key={track.id} className="group cursor-pointer hover:bg-muted/50 transition-colors duration-300">
|
||||
<div className="flex items-center gap-4 p-4">
|
||||
<div className="w-8 h-8 rounded-full bg-muted flex items-center justify-center text-xs font-bold text-muted-foreground group-hover:bg-gradient-to-br group-hover:from-[#6d28d9] group-hover:to-[#8b5cf6] group-hover:text-white transition-all duration-300">
|
||||
{track.track_number}
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<h3 className="font-bold text-foreground group-hover:text-[#6d28d9] transition-colors duration-300 truncate">
|
||||
{track.title}
|
||||
</h3>
|
||||
<p className="text-sm text-muted-foreground">{track.artist}</p>
|
||||
</div>
|
||||
{track.duration && (
|
||||
<span className="text-xs font-bold text-muted-foreground">
|
||||
{track.duration}s
|
||||
</span>
|
||||
)}
|
||||
<Button size="icon" variant="ghost" className="text-muted-foreground opacity-0 group-hover:opacity-100 transition-opacity hover:bg-[#6d28d9]/10 hover:text-[#6d28d9] rounded-xl">
|
||||
<Play size={18} />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
{media.tracks.map(track => (
|
||||
<div key={track.id} className="group cursor-pointer flex items-center gap-4 p-4 rounded-2xl hover:bg-muted/50 transition-colors duration-300 border border-transparent hover:border-border/30">
|
||||
<span className="text-sm font-bold text-muted-foreground w-8">{track.track_number}</span>
|
||||
<div className="flex-1">
|
||||
<h3 className="font-black text-foreground group-hover:text-[#6d28d9] transition-colors duration-300">
|
||||
{track.title}
|
||||
</h3>
|
||||
<p className="text-sm text-muted-foreground">{track.artist}</p>
|
||||
</div>
|
||||
<span className="text-xs font-bold text-muted-foreground">{track.duration ? `${track.duration}m` : '-'}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user