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:
@@ -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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user