Update DetailView.tsx
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
import { Media, Staff } from '@/types';
|
import { Media, Staff } from '@/types';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
import { useState } from 'react';
|
import { useState, useMemo, useEffect } from 'react';
|
||||||
import {
|
import {
|
||||||
Play,
|
Play,
|
||||||
Bookmark,
|
Bookmark,
|
||||||
@@ -27,6 +27,44 @@ export default function DetailView({ media, onPersonClick }: DetailViewProps) {
|
|||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const [castLimit, setCastLimit] = useState(6);
|
const [castLimit, setCastLimit] = useState(6);
|
||||||
const [showAllCast, setShowAllCast] = useState(false);
|
const [showAllCast, setShowAllCast] = useState(false);
|
||||||
|
const [expandedSeasons, setExpandedSeasons] = useState<Set<number>>(new Set());
|
||||||
|
|
||||||
|
// Group episodes by season
|
||||||
|
const episodesBySeason = useMemo(() => {
|
||||||
|
if (!media.episodes) return {};
|
||||||
|
const grouped: Record<number, typeof media.episodes> = {};
|
||||||
|
media.episodes.forEach(episode => {
|
||||||
|
if (!grouped[episode.season]) {
|
||||||
|
grouped[episode.season] = [];
|
||||||
|
}
|
||||||
|
grouped[episode.season].push(episode);
|
||||||
|
});
|
||||||
|
// Sort episodes within each season by episode number
|
||||||
|
Object.keys(grouped).forEach(season => {
|
||||||
|
grouped[Number(season)].sort((a, b) => a.episode_number - b.episode_number);
|
||||||
|
});
|
||||||
|
return grouped;
|
||||||
|
}, [media.episodes]);
|
||||||
|
|
||||||
|
// Expand first season by default on mount
|
||||||
|
useEffect(() => {
|
||||||
|
const seasons = Object.keys(episodesBySeason).map(Number).sort((a, b) => a - b);
|
||||||
|
if (seasons.length > 0) {
|
||||||
|
setExpandedSeasons(new Set([seasons[0]]));
|
||||||
|
}
|
||||||
|
}, [episodesBySeason]);
|
||||||
|
|
||||||
|
const toggleSeason = (season: number) => {
|
||||||
|
setExpandedSeasons(prev => {
|
||||||
|
const newSet = new Set(prev);
|
||||||
|
if (newSet.has(season)) {
|
||||||
|
newSet.delete(season);
|
||||||
|
} else {
|
||||||
|
newSet.add(season);
|
||||||
|
}
|
||||||
|
return newSet;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
const displayedCast = showAllCast ? media.staff : (media.staff?.slice(0, castLimit) || []);
|
const displayedCast = showAllCast ? media.staff : (media.staff?.slice(0, castLimit) || []);
|
||||||
const hasMoreCast = (media.staff?.length || 0) > castLimit;
|
const hasMoreCast = (media.staff?.length || 0) > castLimit;
|
||||||
@@ -254,6 +292,9 @@ export default function DetailView({ media, onPersonClick }: DetailViewProps) {
|
|||||||
<div className="flex items-center gap-2 text-[#6d28d9] font-black text-xl">
|
<div className="flex items-center gap-2 text-[#6d28d9] font-black text-xl">
|
||||||
<span className="opacity-40">{media.episodes.length}</span> Episode{media.episodes.length !== 1 ? 's' : ''}
|
<span className="opacity-40">{media.episodes.length}</span> Episode{media.episodes.length !== 1 ? 's' : ''}
|
||||||
</div>
|
</div>
|
||||||
|
<div className="text-sm font-bold text-muted-foreground">
|
||||||
|
{Object.keys(episodesBySeason).length} Season{Object.keys(episodesBySeason).length !== 1 ? 's' : ''}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
@@ -269,29 +310,57 @@ export default function DetailView({ media, onPersonClick }: DetailViewProps) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-6">
|
<div className="space-y-4">
|
||||||
{media.episodes.map(episode => (
|
{Object.keys(episodesBySeason)
|
||||||
<div key={episode.id} className="group cursor-pointer">
|
.map(Number)
|
||||||
<div className="flex flex-col md:flex-row gap-6">
|
.sort((a, b) => a - b)
|
||||||
<div className="w-full md:w-[240px] shrink-0 aspect-video rounded-xl overflow-hidden shadow-sm relative">
|
.map(season => (
|
||||||
<img src={episode.thumbnail} alt={episode.title} className="w-full h-full object-cover transition-transform duration-500 group-hover:scale-105" referrerPolicy="no-referrer" />
|
<div key={season} className="border border-border rounded-2xl overflow-hidden">
|
||||||
<div className="absolute inset-0 bg-black/0 group-hover:bg-black/20 transition-colors" />
|
<button
|
||||||
</div>
|
onClick={() => toggleSeason(season)}
|
||||||
<div className="flex-1 py-1">
|
className="w-full flex items-center justify-between p-6 bg-card hover:bg-muted/50 transition-colors"
|
||||||
<div className="flex items-center justify-between mb-2">
|
>
|
||||||
<h3 className="font-black text-foreground group-hover:text-[#6d28d9] transition-colors">
|
<div className="flex items-center gap-4">
|
||||||
S{episode.season}:E{episode.episode_number} • {episode.title}
|
<h3 className="text-2xl font-black text-foreground">Season {season}</h3>
|
||||||
</h3>
|
<Badge variant="outline" className="border-[#6d28d9]/30 text-[#6d28d9] font-bold">
|
||||||
<span className="text-xs font-bold text-muted-foreground">{episode.air_date} • {episode.duration}m</span>
|
{episodesBySeason[season].length} Episode{episodesBySeason[season].length !== 1 ? 's' : ''}
|
||||||
|
</Badge>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-sm text-muted-foreground leading-relaxed line-clamp-3">
|
<ChevronDown
|
||||||
{episode.description}
|
size={24}
|
||||||
</p>
|
className={`transition-transform duration-300 text-muted-foreground ${
|
||||||
</div>
|
expandedSeasons.has(season) ? 'rotate-180' : ''
|
||||||
|
}`}
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
{expandedSeasons.has(season) && (
|
||||||
|
<div className="p-6 pt-0 space-y-6">
|
||||||
|
{episodesBySeason[season].map(episode => (
|
||||||
|
<div key={episode.id} className="group cursor-pointer">
|
||||||
|
<div className="flex flex-col md:flex-row gap-6">
|
||||||
|
<div className="w-full md:w-[240px] shrink-0 aspect-video rounded-xl overflow-hidden shadow-sm relative">
|
||||||
|
<img src={episode.thumbnail} alt={episode.title} className="w-full h-full object-cover transition-transform duration-500 group-hover:scale-105" referrerPolicy="no-referrer" />
|
||||||
|
<div className="absolute inset-0 bg-black/0 group-hover:bg-black/20 transition-colors" />
|
||||||
|
</div>
|
||||||
|
<div className="flex-1 py-1">
|
||||||
|
<div className="flex items-center justify-between mb-2">
|
||||||
|
<h3 className="font-black text-foreground group-hover:text-[#6d28d9] transition-colors">
|
||||||
|
E{episode.episode_number} • {episode.title}
|
||||||
|
</h3>
|
||||||
|
<span className="text-xs font-bold text-muted-foreground">{episode.air_date} • {episode.duration}m</span>
|
||||||
|
</div>
|
||||||
|
<p className="text-sm text-muted-foreground leading-relaxed line-clamp-3">
|
||||||
|
{episode.description}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Separator className="mt-6 bg-border" />
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<Separator className="mt-6 bg-border" />
|
))}
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
)}
|
)}
|
||||||
|
|||||||
Reference in New Issue
Block a user