feat(frontend): library page, components, and schedule/add-to-block dialogs (tasks 11-14)

This commit is contained in:
2026-03-20 00:35:40 +01:00
parent 49c7f7abd7
commit 91271bd83c
8 changed files with 591 additions and 0 deletions

View File

@@ -0,0 +1,91 @@
"use client";
import { useCollections, useGenres } from "@/hooks/use-library";
import type { LibrarySearchParams } from "@/hooks/use-library-search";
import { Input } from "@/components/ui/input";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { Badge } from "@/components/ui/badge";
interface Props {
filter: LibrarySearchParams;
onFilterChange: (next: Partial<LibrarySearchParams>) => void;
}
const CONTENT_TYPES = [
{ value: "", label: "All types" },
{ value: "movie", label: "Movies" },
{ value: "episode", label: "Episodes" },
{ value: "short", label: "Shorts" },
];
export function LibrarySidebar({ filter, onFilterChange }: Props) {
const { data: collections } = useCollections(filter.provider);
const { data: genres } = useGenres(filter.type, { provider: filter.provider });
return (
<aside className="w-56 shrink-0 border-r border-zinc-800 bg-zinc-950 p-4 flex flex-col gap-4">
<div>
<p className="mb-1.5 text-xs font-medium uppercase tracking-wider text-zinc-500">Search</p>
<Input
placeholder="Search…"
value={filter.q ?? ""}
onChange={e => onFilterChange({ q: e.target.value || undefined })}
className="h-8 text-xs"
/>
</div>
<div>
<p className="mb-1.5 text-xs font-medium uppercase tracking-wider text-zinc-500">Type</p>
<Select value={filter.type ?? ""} onValueChange={v => onFilterChange({ type: v || undefined })}>
<SelectTrigger className="h-8 text-xs"><SelectValue /></SelectTrigger>
<SelectContent>
{CONTENT_TYPES.map(ct => (
<SelectItem key={ct.value} value={ct.value}>{ct.label}</SelectItem>
))}
</SelectContent>
</Select>
</div>
{collections && collections.length > 0 && (
<div>
<p className="mb-1.5 text-xs font-medium uppercase tracking-wider text-zinc-500">Collection</p>
<Select value={filter.collection ?? ""} onValueChange={v => onFilterChange({ collection: v || undefined })}>
<SelectTrigger className="h-8 text-xs"><SelectValue placeholder="All" /></SelectTrigger>
<SelectContent>
<SelectItem value="">All</SelectItem>
{collections.map(c => (
<SelectItem key={c.id} value={c.id}>{c.name}</SelectItem>
))}
</SelectContent>
</Select>
</div>
)}
{genres && genres.length > 0 && (
<div>
<p className="mb-1.5 text-xs font-medium uppercase tracking-wider text-zinc-500">Genre</p>
<div className="flex flex-wrap gap-1">
{genres.map(g => {
const active = filter.genres?.includes(g) ?? false;
return (
<Badge
key={g}
variant={active ? "default" : "outline"}
className="cursor-pointer text-xs"
onClick={() => {
const current = filter.genres ?? [];
onFilterChange({
genres: active ? current.filter(x => x !== g) : [...current, g],
});
}}
>
{g}
</Badge>
);
})}
</div>
</div>
)}
</aside>
);
}