mirror of
https://github.com/jakejarvis/sofa.git
synced 2026-08-29 03:55:38 -04:00
Replace title mutation API routes with server actions
Move all title page mutations (status, rating, movie watch, episode watch/unwatch, season watch/unwatch) from API route handlers to server actions called directly from TitleInteractionProvider. Same optimistic update pattern, no behavioral change — just eliminates the fetch() round-trip through 5 API routes. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,76 @@
|
||||
"use server";
|
||||
|
||||
import { eq } from "drizzle-orm";
|
||||
import { headers } from "next/headers";
|
||||
import { auth } from "@/lib/auth/server";
|
||||
import { db } from "@/lib/db/client";
|
||||
import { episodes } from "@/lib/db/schema";
|
||||
import {
|
||||
logEpisodeWatch,
|
||||
logMovieWatch,
|
||||
rateTitleStars,
|
||||
removeTitleStatus,
|
||||
setTitleStatus,
|
||||
unwatchEpisode,
|
||||
unwatchSeason,
|
||||
} from "@/lib/services/tracking";
|
||||
|
||||
async function getSessionUserId() {
|
||||
const session = await auth.api.getSession({ headers: await headers() });
|
||||
if (!session) throw new Error("Unauthorized");
|
||||
return session.user.id;
|
||||
}
|
||||
|
||||
export async function updateTitleStatus(
|
||||
titleId: string,
|
||||
status: string | null,
|
||||
) {
|
||||
const userId = await getSessionUserId();
|
||||
if (status === null || status === undefined) {
|
||||
await removeTitleStatus(userId, titleId);
|
||||
} else {
|
||||
await setTitleStatus(
|
||||
userId,
|
||||
titleId,
|
||||
status as "watchlist" | "in_progress" | "completed",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export async function updateTitleRating(titleId: string, ratingStars: number) {
|
||||
const userId = await getSessionUserId();
|
||||
if (ratingStars < 0 || ratingStars > 5) throw new Error("Invalid rating");
|
||||
await rateTitleStars(userId, titleId, ratingStars);
|
||||
}
|
||||
|
||||
export async function watchMovie(titleId: string) {
|
||||
const userId = await getSessionUserId();
|
||||
await logMovieWatch(userId, titleId);
|
||||
}
|
||||
|
||||
export async function watchEpisode(episodeId: string) {
|
||||
const userId = await getSessionUserId();
|
||||
await logEpisodeWatch(userId, episodeId);
|
||||
}
|
||||
|
||||
export async function unwatchEpisodeAction(episodeId: string) {
|
||||
const userId = await getSessionUserId();
|
||||
await unwatchEpisode(userId, episodeId);
|
||||
}
|
||||
|
||||
export async function watchSeason(seasonId: string) {
|
||||
const userId = await getSessionUserId();
|
||||
const seasonEps = await db
|
||||
.select()
|
||||
.from(episodes)
|
||||
.where(eq(episodes.seasonId, seasonId))
|
||||
.all();
|
||||
for (const ep of seasonEps) {
|
||||
await logEpisodeWatch(userId, ep.id);
|
||||
}
|
||||
}
|
||||
|
||||
export async function unwatchSeasonAction(seasonId: string) {
|
||||
const userId = await getSessionUserId();
|
||||
await unwatchSeason(userId, seasonId);
|
||||
}
|
||||
@@ -9,6 +9,15 @@ import {
|
||||
} from "react";
|
||||
import { toast } from "sonner";
|
||||
import type { Season } from "@/lib/types/title";
|
||||
import {
|
||||
unwatchEpisodeAction,
|
||||
unwatchSeasonAction,
|
||||
updateTitleRating,
|
||||
updateTitleStatus,
|
||||
watchEpisode,
|
||||
watchMovie,
|
||||
watchSeason,
|
||||
} from "./actions";
|
||||
|
||||
interface TitleInteractionState {
|
||||
titleId: string;
|
||||
@@ -74,12 +83,7 @@ export function TitleInteractionProvider({
|
||||
const prev = userStatus;
|
||||
setUserStatus(status);
|
||||
try {
|
||||
const res = await fetch(`/api/titles/${titleId}/status`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ status }),
|
||||
});
|
||||
if (!res.ok) throw new Error();
|
||||
await updateTitleStatus(titleId, status);
|
||||
const label =
|
||||
status === "watchlist"
|
||||
? "Added to watchlist"
|
||||
@@ -102,12 +106,7 @@ export function TitleInteractionProvider({
|
||||
const prev = userRating;
|
||||
setUserRating(ratingStars);
|
||||
try {
|
||||
const res = await fetch(`/api/titles/${titleId}/rating`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ ratingStars }),
|
||||
});
|
||||
if (!res.ok) throw new Error();
|
||||
await updateTitleRating(titleId, ratingStars);
|
||||
toast.success(
|
||||
ratingStars > 0
|
||||
? `Rated ${ratingStars} star${ratingStars > 1 ? "s" : ""}`
|
||||
@@ -125,10 +124,7 @@ export function TitleInteractionProvider({
|
||||
const prev = userStatus;
|
||||
setUserStatus("completed");
|
||||
try {
|
||||
const res = await fetch(`/api/movies/${titleId}/watch`, {
|
||||
method: "POST",
|
||||
});
|
||||
if (!res.ok) throw new Error();
|
||||
await watchMovie(titleId);
|
||||
toast.success(`Marked "${titleName}" as watched`);
|
||||
} catch {
|
||||
setUserStatus(prev);
|
||||
@@ -148,10 +144,7 @@ export function TitleInteractionProvider({
|
||||
setEpisodeWatches((w) => w.filter((id) => id !== episodeId));
|
||||
setUserStatus((s) => (s === "completed" ? "in_progress" : s));
|
||||
try {
|
||||
const res = await fetch(`/api/episodes/${episodeId}/watch`, {
|
||||
method: "DELETE",
|
||||
});
|
||||
if (!res.ok) throw new Error();
|
||||
await unwatchEpisodeAction(episodeId);
|
||||
toast.success(`Unwatched S${seasonNum} E${epNum}`);
|
||||
} catch {
|
||||
setEpisodeWatches((w) =>
|
||||
@@ -165,10 +158,7 @@ export function TitleInteractionProvider({
|
||||
);
|
||||
setUserStatus((s) => s ?? "in_progress");
|
||||
try {
|
||||
const res = await fetch(`/api/episodes/${episodeId}/watch`, {
|
||||
method: "POST",
|
||||
});
|
||||
if (!res.ok) throw new Error();
|
||||
await watchEpisode(episodeId);
|
||||
toast.success(`Watched S${seasonNum} E${epNum}`);
|
||||
} catch {
|
||||
setEpisodeWatches((w) => w.filter((id) => id !== episodeId));
|
||||
@@ -194,10 +184,7 @@ export function TitleInteractionProvider({
|
||||
});
|
||||
|
||||
try {
|
||||
const res = await fetch(`/api/seasons/${season.id}/watch`, {
|
||||
method: "POST",
|
||||
});
|
||||
if (!res.ok) throw new Error();
|
||||
await watchSeason(season.id);
|
||||
toast.success(
|
||||
`Watched all of ${season.name ?? `Season ${season.seasonNumber}`}`,
|
||||
);
|
||||
@@ -215,10 +202,7 @@ export function TitleInteractionProvider({
|
||||
setUserStatus((s) => (s === "completed" ? "in_progress" : s));
|
||||
|
||||
try {
|
||||
const res = await fetch(`/api/seasons/${season.id}/watch`, {
|
||||
method: "DELETE",
|
||||
});
|
||||
if (!res.ok) throw new Error();
|
||||
await unwatchSeasonAction(season.id);
|
||||
toast.success(
|
||||
`Unwatched all of ${season.name ?? `Season ${season.seasonNumber}`}`,
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user