mirror of
https://github.com/jakejarvis/sofa.git
synced 2026-08-29 06:15:39 -04:00
- Add composite indexes on userEpisodeWatches and userMovieWatches for hot queries - Batch episode tracking: wrap season/batch watches in single transaction (~8 queries vs 8*N) - Fix N+1 patterns in credits, recommendations, and filmography with batch prefetch+insert - Stream TV season hydration via Suspense instead of blocking page render - Optimize webhook logs (per-connection LIMIT 10) and system health queries - Merge genre filter waterfalls into single Promise.all fetch - Migrate deprecated Jotai loadable() to unwrap() - Replace isolated createStore()+Provider with useHydrateAtoms on root store - Remove unnecessary atomWithStorage SSR guards (handled by Jotai internally) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
62 lines
1.9 KiB
TypeScript
62 lines
1.9 KiB
TypeScript
import { atom } from "jotai";
|
|
import { unwrap } from "jotai/utils";
|
|
import {
|
|
fetchEpisodeProgress,
|
|
fetchUserStatuses,
|
|
} from "@/lib/actions/watchlist";
|
|
|
|
type TitleStatus = "watchlist" | "in_progress" | "completed";
|
|
|
|
interface TitleRowItem {
|
|
tmdbId: number;
|
|
type: "movie" | "tv";
|
|
title: string;
|
|
posterPath: string | null;
|
|
releaseDate: string | null;
|
|
voteAverage: number;
|
|
}
|
|
|
|
export const selectedGenreAtom = atom<number | null>(null);
|
|
export const mediaTypeAtom = atom<"movie" | "tv">("movie");
|
|
export const defaultItemsAtom = atom<TitleRowItem[]>([]);
|
|
export const initialUserStatusesAtom = atom<Record<string, TitleStatus>>({});
|
|
export const initialEpisodeProgressAtom = atom<
|
|
Record<string, { watched: number; total: number }>
|
|
>({});
|
|
|
|
const genreResultsAsyncAtom = atom(async (get) => {
|
|
const genre = get(selectedGenreAtom);
|
|
const mediaType = get(mediaTypeAtom);
|
|
if (genre === null) return null;
|
|
const res = await fetch(
|
|
`/api/explore/discover?type=${mediaType}&genre=${genre}&sort_by=popularity.desc`,
|
|
);
|
|
const data = await res.json();
|
|
return (data.results ?? []) as TitleRowItem[];
|
|
});
|
|
|
|
export const genreResultsAtom = unwrap(genreResultsAsyncAtom);
|
|
|
|
interface GenreEnrichments {
|
|
statuses: Record<string, TitleStatus>;
|
|
progress: Record<string, { watched: number; total: number }>;
|
|
}
|
|
|
|
const genreEnrichmentsAsyncAtom = atom(
|
|
async (get): Promise<GenreEnrichments | null> => {
|
|
const genre = get(selectedGenreAtom);
|
|
if (genre === null) return null;
|
|
const genreResults = await get(genreResultsAsyncAtom);
|
|
if (!genreResults || genreResults.length === 0)
|
|
return { statuses: {}, progress: {} };
|
|
const items = genreResults.map((r) => ({ tmdbId: r.tmdbId, type: r.type }));
|
|
const [statuses, progress] = await Promise.all([
|
|
fetchUserStatuses(items),
|
|
fetchEpisodeProgress(items),
|
|
]);
|
|
return { statuses, progress };
|
|
},
|
|
);
|
|
|
|
export const genreEnrichmentsAtom = unwrap(genreEnrichmentsAsyncAtom);
|