diff --git a/app/(pages)/titles/[id]/_components/actions.ts b/app/(pages)/titles/[id]/_components/actions.ts new file mode 100644 index 0000000..c2e9685 --- /dev/null +++ b/app/(pages)/titles/[id]/_components/actions.ts @@ -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); +} diff --git a/app/(pages)/titles/[id]/_components/title-interaction-provider.tsx b/app/(pages)/titles/[id]/_components/title-interaction-provider.tsx index 914769c..d565984 100644 --- a/app/(pages)/titles/[id]/_components/title-interaction-provider.tsx +++ b/app/(pages)/titles/[id]/_components/title-interaction-provider.tsx @@ -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}`}`, ); diff --git a/app/api/episodes/[id]/watch/route.ts b/app/api/episodes/[id]/watch/route.ts deleted file mode 100644 index 6b4ddd7..0000000 --- a/app/api/episodes/[id]/watch/route.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { headers } from "next/headers"; -import type { NextRequest } from "next/server"; -import { NextResponse } from "next/server"; -import { auth } from "@/lib/auth/server"; -import { logEpisodeWatch, unwatchEpisode } from "@/lib/services/tracking"; - -export async function POST( - _req: NextRequest, - { params }: { params: Promise<{ id: string }> }, -) { - const session = await auth.api.getSession({ - headers: await headers(), - }); - if (!session) - return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); - - const { id } = await params; - await logEpisodeWatch(session.user.id, id); - return NextResponse.json({ ok: true }); -} - -export async function DELETE( - _req: NextRequest, - { params }: { params: Promise<{ id: string }> }, -) { - const session = await auth.api.getSession({ - headers: await headers(), - }); - if (!session) - return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); - - const { id } = await params; - await unwatchEpisode(session.user.id, id); - return NextResponse.json({ ok: true }); -} diff --git a/app/api/movies/[id]/watch/route.ts b/app/api/movies/[id]/watch/route.ts deleted file mode 100644 index 46cbcf4..0000000 --- a/app/api/movies/[id]/watch/route.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { headers } from "next/headers"; -import type { NextRequest } from "next/server"; -import { NextResponse } from "next/server"; -import { auth } from "@/lib/auth/server"; -import { logMovieWatch } from "@/lib/services/tracking"; - -export async function POST( - _req: NextRequest, - { params }: { params: Promise<{ id: string }> }, -) { - const session = await auth.api.getSession({ - headers: await headers(), - }); - if (!session) - return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); - - const { id } = await params; - await logMovieWatch(session.user.id, id); - return NextResponse.json({ ok: true }); -} diff --git a/app/api/seasons/[id]/watch/route.ts b/app/api/seasons/[id]/watch/route.ts deleted file mode 100644 index 3609596..0000000 --- a/app/api/seasons/[id]/watch/route.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { eq } from "drizzle-orm"; -import { headers } from "next/headers"; -import type { NextRequest } from "next/server"; -import { NextResponse } from "next/server"; -import { auth } from "@/lib/auth/server"; -import { db } from "@/lib/db/client"; -import { episodes } from "@/lib/db/schema"; -import { logEpisodeWatch, unwatchSeason } from "@/lib/services/tracking"; - -export async function POST( - _req: NextRequest, - { params }: { params: Promise<{ id: string }> }, -) { - const session = await auth.api.getSession({ - headers: await headers(), - }); - if (!session) - return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); - - const { id } = await params; - const seasonEps = await db - .select() - .from(episodes) - .where(eq(episodes.seasonId, id)) - .all(); - - for (const ep of seasonEps) { - await logEpisodeWatch(session.user.id, ep.id); - } - - return NextResponse.json({ ok: true }); -} - -export async function DELETE( - _req: NextRequest, - { params }: { params: Promise<{ id: string }> }, -) { - const session = await auth.api.getSession({ - headers: await headers(), - }); - if (!session) - return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); - - const { id } = await params; - await unwatchSeason(session.user.id, id); - return NextResponse.json({ ok: true }); -} diff --git a/app/api/titles/[id]/rating/route.ts b/app/api/titles/[id]/rating/route.ts deleted file mode 100644 index 7d9d395..0000000 --- a/app/api/titles/[id]/rating/route.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { headers } from "next/headers"; -import type { NextRequest } from "next/server"; -import { NextResponse } from "next/server"; -import { auth } from "@/lib/auth/server"; -import { rateTitleStars } from "@/lib/services/tracking"; - -export async function POST( - req: NextRequest, - { params }: { params: Promise<{ id: string }> }, -) { - const session = await auth.api.getSession({ - headers: await headers(), - }); - if (!session) - return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); - - const { id } = await params; - const body = await req.json(); - const { ratingStars } = body; - - if (typeof ratingStars !== "number" || ratingStars < 0 || ratingStars > 5) { - return NextResponse.json( - { error: "ratingStars must be 0-5" }, - { status: 400 }, - ); - } - - await rateTitleStars(session.user.id, id, ratingStars); - return NextResponse.json({ ok: true }); -} diff --git a/app/api/titles/[id]/status/route.ts b/app/api/titles/[id]/status/route.ts deleted file mode 100644 index 27ab3df..0000000 --- a/app/api/titles/[id]/status/route.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { headers } from "next/headers"; -import type { NextRequest } from "next/server"; -import { NextResponse } from "next/server"; -import { auth } from "@/lib/auth/server"; -import { - getUserTitleInfo, - removeTitleStatus, - setTitleStatus, -} from "@/lib/services/tracking"; - -export async function GET( - _req: NextRequest, - { params }: { params: Promise<{ id: string }> }, -) { - const session = await auth.api.getSession({ - headers: await headers(), - }); - if (!session) - return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); - - const { id } = await params; - const info = await getUserTitleInfo(session.user.id, id); - return NextResponse.json(info); -} - -export async function POST( - req: NextRequest, - { params }: { params: Promise<{ id: string }> }, -) { - const session = await auth.api.getSession({ - headers: await headers(), - }); - if (!session) - return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); - - const { id } = await params; - const body = await req.json(); - const { status } = body; - - if (status === null || status === undefined) { - await removeTitleStatus(session.user.id, id); - } else { - await setTitleStatus(session.user.id, id, status); - } - - return NextResponse.json({ ok: true }); -}