feat: Bibliotheksseite, Papierkorb und manuelles Anlegen

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.
This commit is contained in:
Florian Wilde
2026-09-01 19:24:51 +02:00
parent f613136e8a
commit f9ce2f7021
7 changed files with 557 additions and 220 deletions
+71
View File
@@ -0,0 +1,71 @@
import Link from "next/link";
import type { Metadata } from "next";
import { requireUser } from "@/lib/auth";
import { getSettings } from "@/lib/settings";
import { getTrash } from "@/lib/queries";
import { AppShell } from "@/components/AppShell";
import { Poster } from "@/components/Poster";
import { TrashActions } from "@/components/library/TrashActions";
import { formatRelative, TYPE_LABEL } from "@/lib/labels";
export const metadata: Metadata = { title: "Papierkorb" };
/** Nach dieser Frist räumt der Hintergrundlauf endgültig auf. */
const RETENTION_DAYS = 30;
export default async function TrashPage() {
const user = await requireUser("/papierkorb");
const settings = await getSettings();
const entries = await getTrash(user.id);
return (
<AppShell user={user} instanceName={settings.instanceName}>
<header className="mb-5">
<h1 className="text-2xl font-semibold tracking-tight sm:text-3xl">Papierkorb</h1>
<p className="mt-1 text-sm text-[var(--fl-muted)]">
Entfernte Titel bleiben {RETENTION_DAYS} Tage liegen. Danach werden sie samt ihrer
Sichtungen endgültig gelöscht.
</p>
</header>
{entries.length === 0 ? (
<div className="fl-card p-8 text-center">
<p className="text-[var(--fl-muted)]">Der Papierkorb ist leer.</p>
<Link href="/bibliothek" className="fl-btn fl-btn-ghost mt-4">
Zur Bibliothek
</Link>
</div>
) : (
<ul className="space-y-3">
{entries.map((entry) => {
const deletedAt = entry.deletedAt!;
const purgeAt = new Date(deletedAt.getTime() + RETENTION_DAYS * 86_400_000);
return (
<li key={entry.id} className="fl-card flex flex-wrap items-center gap-4 p-3 sm:p-4">
<div className="w-16 shrink-0">
<Poster path={entry.title.posterPath} alt={entry.title.title} size="w154" />
</div>
<div className="min-w-0 flex-1">
<p className="font-medium">{entry.title.title}</p>
<p className="text-sm text-[var(--fl-faint)]">
{TYPE_LABEL[entry.title.type]}
{entry.title.releaseDate ? ` · ${entry.title.releaseDate.getFullYear()}` : ""}
{" · entfernt "}
{formatRelative(deletedAt)}
</p>
<p className="mt-0.5 text-xs text-[var(--fl-warning)]">
Wird {formatRelative(purgeAt)} endgültig gelöscht
</p>
</div>
<TrashActions entryId={entry.id} title={entry.title.title} />
</li>
);
})}
</ul>
)}
</AppShell>
);
}