Umsortieren geht per Ziehen und über Pfeilknöpfe: reines Drag & Drop wäre mit Tastatur nicht bedienbar und auf dem Handy fummelig — die Knöpfe sind der verlässliche Weg, das Ziehen der bequeme. Die Erscheinungsreihenfolge lässt sich gar nicht erst anfassen, weil sie sich aus den Daten ergibt.
194 lines
6.7 KiB
TypeScript
194 lines
6.7 KiB
TypeScript
import Link from "next/link";
|
|
import { notFound } from "next/navigation";
|
|
import type { Metadata } from "next";
|
|
import { requireUser } from "@/lib/auth";
|
|
import { getSettings } from "@/lib/settings";
|
|
import { prisma } from "@/lib/db";
|
|
import { getUniverse, suggestCollections } from "@/lib/universes";
|
|
import { formatMinutes } from "@/lib/labels";
|
|
import { AppShell } from "@/components/AppShell";
|
|
import { ProgressBar } from "@/components/ProgressBar";
|
|
import { OrderedList } from "@/components/universes/OrderedList";
|
|
import {
|
|
AddTitles,
|
|
DeleteUniverse,
|
|
OrderTabs,
|
|
SharePanel,
|
|
} from "@/components/universes/UniverseTools";
|
|
|
|
type Params = Promise<{ id: string }>;
|
|
type Search = Promise<{ reihenfolge?: string }>;
|
|
|
|
export async function generateMetadata({ params }: { params: Params }): Promise<Metadata> {
|
|
const { id } = await params;
|
|
const universe = await prisma.universe.findUnique({ where: { id }, select: { name: true } });
|
|
return { title: universe?.name ?? "Universum" };
|
|
}
|
|
|
|
export default async function UniversePage({
|
|
params,
|
|
searchParams,
|
|
}: {
|
|
params: Params;
|
|
searchParams: Search;
|
|
}) {
|
|
const user = await requireUser();
|
|
const settings = await getSettings();
|
|
const { id } = await params;
|
|
const { reihenfolge } = await searchParams;
|
|
|
|
const universe = await getUniverse(user.id, id, reihenfolge);
|
|
if (!universe) notFound();
|
|
|
|
const activeOrder = universe.orders.find((order) => order.id === universe.activeOrderId);
|
|
const inUniverse = new Set(universe.entries.map((entry) => entry.titleId));
|
|
|
|
// Vorschläge zum Aufnehmen: alles aus der eigenen Bibliothek, was noch fehlt.
|
|
const [libraryTitles, collections] = universe.canWrite
|
|
? await Promise.all([
|
|
prisma.libraryEntry.findMany({
|
|
where: { userId: user.id, deletedAt: null },
|
|
orderBy: { title: { title: "asc" } },
|
|
select: {
|
|
title: { select: { id: true, title: true, type: true, releaseDate: true } },
|
|
},
|
|
}),
|
|
suggestCollections(user.id),
|
|
])
|
|
: [[], []];
|
|
|
|
const candidates = libraryTitles
|
|
.map((entry) => entry.title)
|
|
.filter((title) => !inUniverse.has(title.id))
|
|
.map((title) => ({
|
|
id: title.id,
|
|
title: title.title,
|
|
type: title.type,
|
|
year: title.releaseDate?.getFullYear() ?? null,
|
|
}));
|
|
|
|
return (
|
|
<AppShell user={user} instanceName={settings.instanceName}>
|
|
<header className="mb-5">
|
|
<p className="text-xs font-semibold uppercase tracking-wide text-[var(--fl-faint)]">
|
|
Universum
|
|
{!universe.isOwner && ` · geteilt von ${universe.ownerName}`}
|
|
{!universe.canWrite && " · nur ansehen"}
|
|
</p>
|
|
<h1 className="mt-1 text-2xl font-semibold tracking-tight sm:text-3xl">{universe.name}</h1>
|
|
{universe.description && (
|
|
<p className="mt-1 max-w-2xl text-sm text-[var(--fl-muted)]">{universe.description}</p>
|
|
)}
|
|
</header>
|
|
|
|
<section className="fl-card mb-5 p-5" aria-label="Gesamtfortschritt">
|
|
<div className="flex flex-wrap items-end justify-between gap-4">
|
|
<div>
|
|
<p className="text-2xl font-semibold">
|
|
{universe.progress.completed}{" "}
|
|
<span className="text-base font-normal text-[var(--fl-muted)]">
|
|
von {universe.progress.total} abgeschlossen
|
|
</span>
|
|
</p>
|
|
<p className="mt-1 text-sm text-[var(--fl-muted)]">
|
|
{formatMinutes(universe.progress.watchedMinutes)} gesehen
|
|
{universe.progress.remainingMinutes > 0 && (
|
|
<>
|
|
{" · noch "}
|
|
<span className="text-[var(--fl-text)]">
|
|
{formatMinutes(universe.progress.remainingMinutes)}
|
|
</span>
|
|
{" bis zum Ende des Universums"}
|
|
</>
|
|
)}
|
|
</p>
|
|
</div>
|
|
|
|
{universe.nextUp && (
|
|
<div className="text-right">
|
|
<p className="text-xs font-semibold uppercase tracking-wide text-[var(--fl-faint)]">
|
|
Als Nächstes dran
|
|
</p>
|
|
<Link
|
|
href={`/titel/${universe.nextUp.titleId}`}
|
|
className="fl-gradient-text text-lg font-semibold"
|
|
>
|
|
{universe.nextUp.title}
|
|
</Link>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
<div className="mt-4">
|
|
<ProgressBar
|
|
percent={universe.progress.percent}
|
|
label={`${universe.progress.percent} Prozent des Universums abgeschlossen`}
|
|
/>
|
|
</div>
|
|
</section>
|
|
|
|
<div className="mb-4">
|
|
<OrderTabs universe={universe} canWrite={universe.canWrite} />
|
|
{activeOrder?.kind === "RELEASE" ? (
|
|
<p className="mt-2 text-xs text-[var(--fl-faint)]">
|
|
Diese Reihenfolge ergibt sich aus den Erscheinungsdaten und bleibt damit immer aktuell.
|
|
</p>
|
|
) : universe.canWrite ? (
|
|
<p className="mt-2 text-xs text-[var(--fl-faint)]">
|
|
Ziehen oder die Pfeile benutzen, um umzusortieren — gespeichert wird sofort.
|
|
</p>
|
|
) : null}
|
|
</div>
|
|
|
|
<div className="grid gap-6 lg:grid-cols-[minmax(0,1fr)_20rem]">
|
|
<div className="order-2 lg:order-1">
|
|
<OrderedList
|
|
universeId={universe.id}
|
|
orderId={universe.activeOrderId}
|
|
entries={universe.entries}
|
|
sortable={activeOrder?.kind === "CUSTOM"}
|
|
canWrite={universe.canWrite}
|
|
nextUpItemId={universe.nextUp?.itemId ?? null}
|
|
/>
|
|
</div>
|
|
|
|
<aside className="order-1 space-y-5 lg:order-2">
|
|
{universe.canWrite && (
|
|
<div className="fl-card p-4">
|
|
<h2 className="mb-3 text-sm font-semibold uppercase tracking-wide text-[var(--fl-faint)]">
|
|
Titel aufnehmen
|
|
</h2>
|
|
<AddTitles
|
|
universeId={universe.id}
|
|
candidates={candidates}
|
|
collections={collections}
|
|
/>
|
|
</div>
|
|
)}
|
|
|
|
{universe.isOwner && (
|
|
<>
|
|
<div className="fl-card p-4">
|
|
<h2 className="mb-3 text-sm font-semibold uppercase tracking-wide text-[var(--fl-faint)]">
|
|
Teilen
|
|
</h2>
|
|
<SharePanel universe={universe} />
|
|
</div>
|
|
|
|
<div className="fl-card p-4">
|
|
<DeleteUniverse universeId={universe.id} name={universe.name} />
|
|
</div>
|
|
</>
|
|
)}
|
|
</aside>
|
|
</div>
|
|
|
|
<p className="mt-8 text-sm">
|
|
<Link href="/universen" className="text-[var(--fl-faint)] hover:text-[var(--fl-muted)]">
|
|
← Alle Universen
|
|
</Link>
|
|
</p>
|
|
</AppShell>
|
|
);
|
|
}
|