Replace server actions with API routes and add SWR data-fetching hooks

- Convert discover, stats, status, and system-health server actions to
  proper API route handlers under `app/api/`; delete `lib/actions/explore.ts`,
  `lib/actions/settings.ts`, and `lib/actions/setup.ts`
- Add `use-discover`, `use-stats`, and `use-system-health` SWR hooks
  that call the new routes; update `command-palette`, `title-card`,
  `update-toast`, and `stats-display` to consume them
- Lift auth centering wrapper from individual login/register pages into
  `(auth)/layout.tsx`; switch both pages from `auth.api.getSession` to
  the cached `getSession()` helper
- Relocate setup wizard from `app/(auth)/setup/` to `app/setup/` (outside
  auth group) with dedicated `copy-button` and `refresh-button` client
  components
- Move `not-found.tsx` and `error.tsx` to app root so they apply
  globally instead of only within the pages route group
This commit is contained in:
2026-03-08 19:09:21 -04:00
parent 1f3e5e976f
commit 271069cc0c
32 changed files with 616 additions and 618 deletions
+83
View File
@@ -0,0 +1,83 @@
import { type NextRequest, NextResponse } from "next/server";
import { getSession } from "@/lib/auth/session";
import { isTmdbConfigured } from "@/lib/config";
import {
getEpisodeProgressByTmdbIds,
getUserStatusesByTmdbIds,
} from "@/lib/services/tracking";
import { discover } from "@/lib/tmdb/client";
import { tmdbImageUrl } from "@/lib/tmdb/image";
export async function GET(req: NextRequest) {
const session = await getSession();
if (!session) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}
if (!isTmdbConfigured()) {
return NextResponse.json(
{ error: "TMDB API key is not configured." },
{ status: 503 },
);
}
const mediaType = req.nextUrl.searchParams.get("mediaType");
const genreId = req.nextUrl.searchParams.get("genreId");
if (mediaType !== "movie" && mediaType !== "tv") {
return NextResponse.json(
{ error: "mediaType must be movie or tv" },
{ status: 400 },
);
}
const parsedGenreId = Number(genreId);
if (!genreId || !Number.isFinite(parsedGenreId)) {
return NextResponse.json(
{ error: "genreId must be a number" },
{ status: 400 },
);
}
try {
const results = await discover(mediaType, {
sort_by: "popularity.desc",
"vote_count.gte": "50",
with_genres: String(parsedGenreId),
});
type DiscoverResult = NonNullable<typeof results.results>[number] & {
title?: string;
name?: string;
release_date?: string;
first_air_date?: string;
};
const items = ((results.results ?? []) as DiscoverResult[])
.filter((r) => r.poster_path)
.map((r) => ({
tmdbId: r.id,
type: mediaType,
title: r.title ?? r.name ?? "",
posterPath: tmdbImageUrl(r.poster_path ?? null, "posters"),
releaseDate: r.release_date ?? r.first_air_date ?? null,
voteAverage: r.vote_average,
}));
const lookups = items.map((r) => ({ tmdbId: r.tmdbId, type: r.type }));
const [userStatuses, episodeProgress] =
lookups.length > 0
? await Promise.all([
getUserStatusesByTmdbIds(session.user.id, lookups),
getEpisodeProgressByTmdbIds(session.user.id, lookups),
])
: [{}, {}];
return NextResponse.json({ items, userStatuses, episodeProgress });
} catch {
return NextResponse.json(
{ error: "Failed to fetch discover results" },
{ status: 502 },
);
}
}