Die Detailseite hängt jetzt an der internen Titel-ID statt an der TMDB-ID — nur so bekommen manuell angelegte Titel dieselbe Seite. Treffer aus der Suche laufen über /tmdb/<art>/<id>, das den Titel holt und weiterleitet. Der Fortschritt einer ganzen Liste wird in einer Abfrage gezählt statt je Eintrag; bei 300 Titeln wären das sonst 300 Rundreisen zur Datenbank. Entfernte Titel wandern für 30 Tage in den Papierkorb und behalten ihren Fortschritt.
94 lines
3.5 KiB
TypeScript
94 lines
3.5 KiB
TypeScript
import Link from "next/link";
|
|
import type { Metadata } from "next";
|
|
import { requireUser } from "@/lib/auth";
|
|
import { getSettings } from "@/lib/settings";
|
|
import { getLibrary, getLibraryFacets, type LibraryFilters as Filters, type LibrarySort } from "@/lib/queries";
|
|
import { AppShell } from "@/components/AppShell";
|
|
import { LibraryCard } from "@/components/library/LibraryCard";
|
|
import { LibraryFilters } from "@/components/library/LibraryFilters";
|
|
import type { EntryStatus, TitleType } from "@/generated/prisma/client";
|
|
|
|
export const metadata: Metadata = { title: "Bibliothek" };
|
|
|
|
const STATUSES = ["WATCHING", "PAUSED", "COMPLETED", "DROPPED", "WATCHLIST"];
|
|
const SORTS = ["aktualisiert", "hinzugefuegt", "titel", "bewertung", "jahr"];
|
|
|
|
export default async function LibraryPage({
|
|
searchParams,
|
|
}: {
|
|
searchParams: Promise<Record<string, string | undefined>>;
|
|
}) {
|
|
const user = await requireUser("/bibliothek");
|
|
const settings = await getSettings();
|
|
const params = await searchParams;
|
|
|
|
const filters: Filters = {
|
|
query: params.q?.trim() || undefined,
|
|
status: STATUSES.includes(params.status ?? "") ? (params.status as EntryStatus) : undefined,
|
|
type: params.typ === "SERIES" || params.typ === "MOVIE" ? (params.typ as TitleType) : undefined,
|
|
genre: params.genre || undefined,
|
|
tag: params.tag || undefined,
|
|
provider: params.dienst || undefined,
|
|
sort: SORTS.includes(params.sortierung ?? "") ? (params.sortierung as LibrarySort) : undefined,
|
|
};
|
|
|
|
const [items, facets] = await Promise.all([
|
|
getLibrary(user.id, filters),
|
|
getLibraryFacets(user.id),
|
|
]);
|
|
|
|
const hasFilters = Object.values(filters).some(Boolean);
|
|
|
|
return (
|
|
<AppShell user={user} instanceName={settings.instanceName}>
|
|
<header className="mb-5 flex flex-wrap items-end justify-between gap-3">
|
|
<div>
|
|
<h1 className="text-2xl font-semibold tracking-tight sm:text-3xl">Bibliothek</h1>
|
|
<p className="mt-1 text-sm text-[var(--fl-muted)]">
|
|
{facets.counts.ALLE === 0
|
|
? "Noch nichts aufgenommen."
|
|
: `${facets.counts.ALLE} ${facets.counts.ALLE === 1 ? "Titel" : "Titel"} — davon ${facets.counts.WATCHING} gerade am Laufen.`}
|
|
</p>
|
|
</div>
|
|
<Link href="/suche" className="fl-btn fl-btn-primary">
|
|
Titel hinzufügen
|
|
</Link>
|
|
</header>
|
|
|
|
{facets.counts.ALLE > 0 && <LibraryFilters facets={facets} />}
|
|
|
|
<div className="mt-6">
|
|
{items.length === 0 ? (
|
|
<div className="fl-card p-8 text-center">
|
|
<p className="text-[var(--fl-muted)]">
|
|
{facets.counts.ALLE === 0
|
|
? "Deine Bibliothek ist noch leer."
|
|
: hasFilters
|
|
? "Zu dieser Auswahl passt nichts."
|
|
: "Hier ist nichts."}
|
|
</p>
|
|
{facets.counts.ALLE === 0 && (
|
|
<Link href="/suche" className="fl-btn fl-btn-primary mt-4">
|
|
Ersten Titel suchen
|
|
</Link>
|
|
)}
|
|
</div>
|
|
) : (
|
|
<>
|
|
<p className="mb-4 text-sm text-[var(--fl-faint)]">
|
|
{items.length} {items.length === 1 ? "Titel" : "Titel"}
|
|
</p>
|
|
<ul className="grid grid-cols-2 gap-4 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-6">
|
|
{items.map((item) => (
|
|
<li key={item.entryId}>
|
|
<LibraryCard item={item} />
|
|
</li>
|
|
))}
|
|
</ul>
|
|
</>
|
|
)}
|
|
</div>
|
|
</AppShell>
|
|
);
|
|
}
|