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:
Lars Behrends
2026-04-26 02:18:01 +02:00
parent 9a72ba3064
commit 073c8a6c5d
37 changed files with 6306 additions and 1593 deletions
+118 -66
View File
@@ -1,10 +1,11 @@
import { Episode } from '@/types';
import { useState, useMemo, useEffect } from 'react';
import { Search, MoreHorizontal, ListFilter, ChevronDown } from 'lucide-react';
import { Search, Play, Clock, Calendar, ChevronDown, Tv } from 'lucide-react';
import { Button } from '@/components/ui/button';
import { Badge } from '@/components/ui/badge';
import { Input } from '@/components/ui/input';
import { Separator } from '@/components/ui/separator';
import { Card, CardContent, CardHeader } from '@/components/ui/card';
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@/components/ui/collapsible';
interface SeasonsTabProps {
episodes: Episode[];
@@ -51,82 +52,133 @@ export default function SeasonsTab({ episodes }: SeasonsTabProps) {
};
return (
<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">
<div className="flex items-center gap-2 text-[#6d28d9] font-black text-2xl">
<span className="opacity-40">{episodes.length}</span> Episode{episodes.length !== 1 ? 's' : ''}
</div>
<div className="text-sm font-bold text-muted-foreground">
{Object.keys(episodesBySeason).length} Season{Object.keys(episodesBySeason).length !== 1 ? 's' : ''}
</div>
</div>
<div className="space-y-4">
{/* Header */}
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4">
<div className="flex items-center gap-2">
<div className="relative">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 text-muted-foreground" size={16} />
<Input placeholder="Search" className="pl-10 w-[200px] bg-muted/50 backdrop-blur-sm border-none rounded-full h-9 text-sm" />
<div className="w-6 h-6 rounded bg-primary/10 flex items-center justify-center">
<Tv className="w-4 h-4 text-primary" />
</div>
<Button variant="ghost" size="icon" className="text-muted-foreground hover:bg-muted/50 rounded-xl transition-all duration-300">
<MoreHorizontal size={20} />
</Button>
<Button variant="ghost" size="icon" className="text-muted-foreground hover:bg-muted/50 rounded-xl transition-all duration-300">
<ListFilter size={20} />
</Button>
<h2 className="text-sm font-medium uppercase tracking-wide text-muted-foreground">
Episodes
</h2>
<Badge variant="secondary" className="text-xs">
{episodes.length}
</Badge>
<span className="text-xs text-muted-foreground">
{Object.keys(episodesBySeason).length} Season{Object.keys(episodesBySeason).length !== 1 ? 's' : ''}
</span>
</div>
<div className="relative">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 text-muted-foreground w-4 h-4" />
<Input
placeholder="Search episodes..."
className="pl-9 w-full sm:w-[200px] bg-muted/50 border-none rounded-lg h-9 text-sm"
/>
</div>
</div>
<div className="space-y-4">
{/* Seasons */}
<div className="space-y-3">
{Object.keys(episodesBySeason)
.map(Number)
.sort((a, b) => a - b)
.map(season => (
<div key={season} className="border border-border/50 rounded-2xl overflow-hidden bg-card/50 backdrop-blur-sm">
<button
onClick={() => toggleSeason(season)}
className="w-full flex items-center justify-between p-6 bg-card/50 hover:bg-muted/50 transition-colors duration-300"
>
<div className="flex items-center gap-4">
<h3 className="text-2xl font-black text-foreground">Season {season}</h3>
<Badge variant="outline" className="border-[#6d28d9]/30 text-[#6d28d9] font-bold">
{episodesBySeason[season].length} Episode{episodesBySeason[season].length !== 1 ? 's' : ''}
</Badge>
</div>
<ChevronDown
size={24}
className={`transition-transform duration-300 text-muted-foreground ${
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-2xl overflow-hidden shadow-sm relative border border-border/30">
<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 duration-300" />
</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 duration-300">
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>
<Collapsible
key={season}
open={expandedSeasons.has(season)}
onOpenChange={() => toggleSeason(season)}
>
<Card className="border-border/60 overflow-hidden">
<CollapsibleTrigger asChild>
<CardHeader className="py-3 px-4 cursor-pointer hover:bg-muted/30 transition-colors">
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<h3 className="font-semibold text-foreground">Season {season}</h3>
<Badge variant="outline" className="text-xs border-primary/30 text-primary">
{episodesBySeason[season].length} Episode{episodesBySeason[season].length !== 1 ? 's' : ''}
</Badge>
</div>
<Separator className="mt-6 bg-border/50" />
<ChevronDown
className={`w-5 h-5 text-muted-foreground transition-transform duration-200 ${
expandedSeasons.has(season) ? 'rotate-180' : ''
}`}
/>
</div>
))}
</div>
)}
</div>
</CardHeader>
</CollapsibleTrigger>
<CollapsibleContent>
<CardContent className="p-0">
<div className="divide-y divide-border/50">
{episodesBySeason[season].map((episode, index) => (
<div
key={episode.id}
className="group p-4 hover:bg-muted/30 transition-colors cursor-pointer"
>
<div className="flex flex-col sm:flex-row gap-4">
{/* Thumbnail */}
<div className="w-full sm:w-[160px] shrink-0 aspect-video rounded-lg overflow-hidden relative bg-muted border border-border/30">
{episode.thumbnail ? (
<img
src={episode.thumbnail}
alt={episode.title}
className="w-full h-full object-cover transition-transform duration-300 group-hover:scale-105"
referrerPolicy="no-referrer"
/>
) : (
<div className="w-full h-full flex items-center justify-center">
<Play className="w-8 h-8 text-muted-foreground" />
</div>
)}
<div className="absolute inset-0 bg-black/0 group-hover:bg-black/20 transition-colors flex items-center justify-center">
<div className="w-10 h-10 rounded-full bg-primary/90 text-primary-foreground flex items-center justify-center opacity-0 group-hover:opacity-100 transition-opacity shadow-lg">
<Play className="w-5 h-5 fill-current ml-0.5" />
</div>
</div>
</div>
{/* Info */}
<div className="flex-1 min-w-0">
<div className="flex items-start justify-between gap-4">
<div className="flex-1 min-w-0">
<p className="text-xs text-muted-foreground mb-1">
Episode {episode.episode_number}
</p>
<h4 className="font-medium text-foreground group-hover:text-primary transition-colors truncate">
{episode.title}
</h4>
{episode.description && (
<p className="text-sm text-muted-foreground line-clamp-2 mt-1">
{episode.description}
</p>
)}
</div>
<div className="flex items-center gap-3 text-xs text-muted-foreground shrink-0">
{episode.duration > 0 && (
<div className="flex items-center gap-1">
<Clock className="w-3 h-3" />
<span>{episode.duration}m</span>
</div>
)}
{episode.air_date && (
<div className="flex items-center gap-1">
<Calendar className="w-3 h-3" />
<span>{episode.air_date}</span>
</div>
)}
</div>
</div>
</div>
</div>
</div>
))}
</div>
</CardContent>
</CollapsibleContent>
</Card>
</Collapsible>
))}
</div>
</section>
</div>
);
}