Add catch-up prompt when marking episodes with unwatched predecessors

When marking an episode as watched, if earlier episodes are unwatched,
the success toast now includes a "Catch up" action button that offers
to mark all previous episodes as watched in one click.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-03 16:19:09 -05:00
co-authored by Claude Opus 4.6
parent a940e61d75
commit e376367fe9
2 changed files with 66 additions and 2 deletions
@@ -76,3 +76,10 @@ export async function unwatchSeasonAction(seasonId: string) {
const userId = await getSessionUserId();
await unwatchSeason(userId, seasonId);
}
export async function batchWatchEpisodes(episodeIds: string[]) {
const userId = await getSessionUserId();
for (const id of episodeIds) {
await logEpisodeWatch(userId, id);
}
}
@@ -10,6 +10,7 @@ import {
import { toast } from "sonner";
import type { Season } from "@/lib/types/title";
import {
batchWatchEpisodes,
markAllWatchedAction,
unwatchEpisodeAction,
unwatchSeasonAction,
@@ -133,6 +134,33 @@ export function TitleInteractionProvider({
}
}, [titleId, titleName, userStatus]);
const handleCatchUp = useCallback(
async (episodeIds: string[]) => {
setEpisodeWatches((w) => {
const set = new Set(w);
for (const id of episodeIds) set.add(id);
return [...set];
});
// Check if all episodes are now watched
setEpisodeWatches((w) => {
const allEpIds = seasons.flatMap((s) => s.episodes.map((ep) => ep.id));
if (allEpIds.every((id) => w.includes(id))) {
setUserStatus("completed");
}
return w;
});
try {
await batchWatchEpisodes(episodeIds);
toast.success(
`Caught up — marked ${episodeIds.length} episode${episodeIds.length > 1 ? "s" : ""} as watched`,
);
} catch {
toast.error("Failed to catch up");
}
},
[seasons],
);
const handleWatchEpisode = useCallback(
async (
episodeId: string,
@@ -162,7 +190,36 @@ export function TitleInteractionProvider({
);
try {
await watchEpisode(episodeId);
// Find unwatched episodes before this one
const previousUnwatched: string[] = [];
for (const s of seasons) {
for (const ep of s.episodes) {
if (
s.seasonNumber < seasonNum ||
(s.seasonNumber === seasonNum && ep.episodeNumber < epNum)
) {
// Use the latest episodeWatches state
if (!episodeWatches.includes(ep.id) && ep.id !== episodeId) {
previousUnwatched.push(ep.id);
}
}
}
}
if (previousUnwatched.length > 0) {
const count = previousUnwatched.length;
toast.success(`Watched S${seasonNum} E${epNum}`, {
description: `${count} earlier episode${count > 1 ? "s" : ""} unwatched`,
action: {
label: "Catch up",
onClick: () => handleCatchUp(previousUnwatched),
},
duration: 8000,
});
} else {
toast.success(`Watched S${seasonNum} E${epNum}`);
}
} catch {
setEpisodeWatches((w) => w.filter((id) => id !== episodeId));
toast.error("Failed to mark episode");
@@ -170,7 +227,7 @@ export function TitleInteractionProvider({
}
setWatchingEp(null);
},
[],
[seasons, episodeWatches, handleCatchUp],
);
const handleMarkSeason = useCallback(