feat: Bedienelemente für Bibliothek und Episodenfortschritt
Server-Aktionen prüfen die Sitzung jeweils selbst — auf Angaben aus dem Formular ist beim Zugriffsschutz kein Verlass. Häkchen reagieren sofort und werden zurückgedreht, wenn der Server widerspricht: Abhaken soll sich anfühlen wie ein Schalter, nicht wie ein Formular. Jede Sichtungsaktion meldet sich unten kurz zurück und lässt sich von dort zurücknehmen — die Wirkung passiert weit weg vom Klick.
This commit is contained in:
@@ -0,0 +1,123 @@
|
||||
"use client";
|
||||
|
||||
import { useTransition } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { markMovieAction, undoAction, watchNextAction } from "@/app/actions/library";
|
||||
import { useToasts } from "@/components/Toasts";
|
||||
import { episodeCode, formatDate, isUpcoming } from "@/lib/labels";
|
||||
|
||||
/**
|
||||
* „Als Nächstes dran" mit dem Schnellknopf.
|
||||
*
|
||||
* Das ist der meistbenutzte Knopf der ganzen Anwendung — deshalb steht er
|
||||
* oben, ist groß, und sagt vorher, was er abhaken wird.
|
||||
*/
|
||||
export function NextEpisodeCard({
|
||||
entryId,
|
||||
next,
|
||||
}: {
|
||||
entryId: string;
|
||||
next: {
|
||||
id: string;
|
||||
name: string;
|
||||
seasonNumber: number;
|
||||
episodeNumber: number;
|
||||
airDate: string | null;
|
||||
runtime: number | null;
|
||||
};
|
||||
}) {
|
||||
const router = useRouter();
|
||||
const { report } = useToasts();
|
||||
const [pending, startTransition] = useTransition();
|
||||
|
||||
const upcoming = isUpcoming(next.airDate);
|
||||
const code = episodeCode(next.seasonNumber, next.episodeNumber);
|
||||
|
||||
async function undo(watchIds: string[]) {
|
||||
await undoAction(entryId, watchIds);
|
||||
router.refresh();
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-wrap items-center gap-4 rounded-xl border border-[var(--fl-border)] bg-[var(--fl-surface-2)] p-4">
|
||||
<div className="min-w-0 flex-1">
|
||||
<p className="text-xs font-semibold uppercase tracking-wide text-[var(--fl-faint)]">
|
||||
Als Nächstes dran
|
||||
</p>
|
||||
<p className="mt-1 truncate font-medium">
|
||||
<span className="fl-gradient-text font-mono font-semibold">{code}</span>{" "}
|
||||
<span className="text-[var(--fl-text)]">{next.name}</span>
|
||||
</p>
|
||||
<p className="mt-0.5 text-xs text-[var(--fl-faint)]">
|
||||
{upcoming
|
||||
? `Erscheint erst am ${formatDate(next.airDate)}`
|
||||
: [formatDate(next.airDate), next.runtime ? `${next.runtime} min` : null]
|
||||
.filter(Boolean)
|
||||
.join(" · ") || "Kein Ausstrahlungsdatum hinterlegt"}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
disabled={pending}
|
||||
onClick={() =>
|
||||
startTransition(async () => {
|
||||
const result = await watchNextAction(entryId);
|
||||
report(result, (ids) => undo(ids));
|
||||
router.refresh();
|
||||
})
|
||||
}
|
||||
className="fl-btn fl-btn-primary"
|
||||
title={`${code} als gesehen markieren`}
|
||||
>
|
||||
+1 Episode
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/** Sichtungsknopf für Filme. */
|
||||
export function WatchMovieButton({
|
||||
entryId,
|
||||
watched,
|
||||
}: {
|
||||
entryId: string;
|
||||
watched: boolean;
|
||||
}) {
|
||||
const router = useRouter();
|
||||
const { report } = useToasts();
|
||||
const [pending, startTransition] = useTransition();
|
||||
|
||||
async function undo(watchIds: string[]) {
|
||||
await undoAction(entryId, watchIds);
|
||||
router.refresh();
|
||||
}
|
||||
|
||||
if (watched) {
|
||||
return (
|
||||
<p className="inline-flex items-center gap-2 rounded-xl border border-[color-mix(in_oklab,var(--fl-success)_45%,transparent)] bg-[color-mix(in_oklab,var(--fl-success)_12%,transparent)] px-3 py-2 text-sm text-[var(--fl-success)]">
|
||||
<svg viewBox="0 0 24 24" className="h-4 w-4" fill="none" stroke="currentColor" strokeWidth="3" aria-hidden>
|
||||
<path d="m5 13 4.5 4.5L19 7" strokeLinecap="round" strokeLinejoin="round" />
|
||||
</svg>
|
||||
Gesehen
|
||||
</p>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
disabled={pending}
|
||||
onClick={() =>
|
||||
startTransition(async () => {
|
||||
const result = await markMovieAction(entryId);
|
||||
report(result, (ids) => undo(ids));
|
||||
router.refresh();
|
||||
})
|
||||
}
|
||||
className="fl-btn fl-btn-primary"
|
||||
>
|
||||
Als gesehen markieren
|
||||
</button>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user