CI / pruefen (pull_request) Canceled after 0s
War ein Film einmal als gesehen markiert, gab es keinen Weg zurueck: im
Zustand "gesehen" stand statt eines Knopfes ein <p> — ein Etikett ohne
Klickziel. Der Statuswechsel konnte das nicht heilen, weil "gesehen" aus den
Sichtungen abgeleitet wird und setStatus() die absichtlich nicht anfasst.
Und eine Gegenfunktion fuer Filme gab es nicht: unmarkEpisodes() und
unmarkSeason() waren da, ein unmarkMovie() nie geschrieben.
unmarkMovie() ist auf run: entry.currentRun und episodeId: null eingegrenzt.
Wer einen Film dreimal gesehen hat, hat drei Sichtungen — ohne die
Eingrenzung loeschte das Zuruecknehmen alle drei und verfaelschte
rueckwirkend die Statistik.
Die gruene Plakette bleibt stehen, ist jetzt aber klickbar, so wie sich
Episodenhaekchen auch abwaehlen lassen. Ueber die Schnittstelle geht es mit
DELETE {"action":"movie"} als Gegenstueck zum gleichnamigen POST, ohne dass
man die watchIds kennen muss.
Drei Tests: Zuruecknehmen samt erneutem Abhaken, die Eingrenzung auf den
laufenden Durchlauf, und dass Serien abgewiesen werden. Klickweg und
Endpunkt gegen einen Produktionsbau geprueft.
Der im Issue vermutete Nebenpunkt trifft nicht zu: report() blendet das
Zuruecknehmen nur bei ids.length > 0 ein, ein wirkungsloses Angebot gab es
also nie.
Refs #5
146 lines
4.5 KiB
TypeScript
146 lines
4.5 KiB
TypeScript
"use client";
|
|
|
|
import { useTransition } from "react";
|
|
import { useRouter } from "next/navigation";
|
|
import {
|
|
markMovieAction,
|
|
undoAction,
|
|
unmarkMovieAction,
|
|
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();
|
|
}
|
|
|
|
// Im Zustand „gesehen" bleibt die grüne Plakette stehen, ist aber klickbar —
|
|
// so wie sich Episodenhäkchen auch wieder abwählen lassen. Vorher stand hier
|
|
// ein <p>: ein Etikett ohne Klickziel, aus dem es kein Zurück gab.
|
|
if (watched) {
|
|
return (
|
|
<button
|
|
type="button"
|
|
disabled={pending}
|
|
title="Markierung wieder entfernen"
|
|
onClick={() =>
|
|
startTransition(async () => {
|
|
report(await unmarkMovieAction(entryId));
|
|
router.refresh();
|
|
})
|
|
}
|
|
className="group 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)] transition-colors hover:border-[color-mix(in_oklab,var(--fl-success)_70%,transparent)] hover:bg-[color-mix(in_oklab,var(--fl-success)_20%,transparent)] disabled:opacity-60"
|
|
>
|
|
<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
|
|
<span className="text-xs text-[color-mix(in_oklab,var(--fl-success)_75%,var(--fl-muted))]">
|
|
— Markierung entfernen
|
|
</span>
|
|
</button>
|
|
);
|
|
}
|
|
|
|
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>
|
|
);
|
|
}
|