feat: Oberfläche für Universen
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.
This commit is contained in:
@@ -0,0 +1,193 @@
|
||||
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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user