mirror of
https://github.com/jakejarvis/sofa.git
synced 2026-08-29 01:35:39 -04:00
feat(core): add pagination to explore, search, person, and library endpoints
Convert trending, search, people.detail, and dashboard.library to page-based responses (`page` / `totalPages`) so clients can load results incrementally. Update `@sofa/core` discovery and person services to accept a `page` / `limit` input and slice results accordingly. Add a new DB migration to persist the data needed to back paginated filmography queries. On the web, introduce a `useInfiniteScroll` hook and wire up `useInfiniteQuery` in the explore, person detail, and filterable title row components with an intersection-observer sentinel. On native, switch the same screens from `useQuery` to `useInfiniteQuery` with `onEndReached` / `ListFooterComponent` loading indicators. Also replace remaining `FlatList` usages with `FlashList` in the home and title detail screens.
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
import {
|
||||
getContinueWatchingFeed,
|
||||
getNewAvailableFeed,
|
||||
getLibraryFeed,
|
||||
getRecommendationsFeed,
|
||||
getUserStats,
|
||||
getWatchCount,
|
||||
@@ -44,9 +44,14 @@ export const continueWatching = os.dashboard.continueWatching
|
||||
|
||||
export const library = os.dashboard.library
|
||||
.use(authed)
|
||||
.handler(({ context }) => {
|
||||
const feed = getNewAvailableFeed(context.user.id);
|
||||
const items = feed.slice(0, 10).map((t) => ({
|
||||
.handler(({ input, context }) => {
|
||||
const {
|
||||
items: feed,
|
||||
page,
|
||||
totalPages,
|
||||
totalResults,
|
||||
} = getLibraryFeed(context.user.id, input.page, input.limit);
|
||||
const items = feed.map((t) => ({
|
||||
id: t.titleId,
|
||||
tmdbId: t.tmdbId,
|
||||
type: t.type,
|
||||
@@ -58,7 +63,7 @@ export const library = os.dashboard.library
|
||||
voteAverage: t.voteAverage,
|
||||
userStatus: t.userStatus,
|
||||
}));
|
||||
return { items };
|
||||
return { items, page, totalPages, totalResults };
|
||||
});
|
||||
|
||||
export const recommendations = os.dashboard.recommendations
|
||||
|
||||
@@ -22,11 +22,15 @@ export const discover = os.discover
|
||||
});
|
||||
}
|
||||
|
||||
const results = await discoverTmdb(input.type, {
|
||||
sort_by: "popularity.desc",
|
||||
"vote_count.gte": "50",
|
||||
with_genres: String(input.genreId),
|
||||
});
|
||||
const results = await discoverTmdb(
|
||||
input.type,
|
||||
{
|
||||
sort_by: "popularity.desc",
|
||||
"vote_count.gte": "50",
|
||||
with_genres: String(input.genreId),
|
||||
},
|
||||
input.page,
|
||||
);
|
||||
|
||||
type DiscoverResult = NonNullable<typeof results.results>[number] & {
|
||||
title?: string;
|
||||
@@ -61,5 +65,12 @@ export const discover = os.discover
|
||||
]
|
||||
: [{}, {}];
|
||||
|
||||
return { items, userStatuses, episodeProgress };
|
||||
return {
|
||||
items,
|
||||
userStatuses,
|
||||
episodeProgress,
|
||||
page: results.page ?? input.page,
|
||||
totalPages: results.total_pages ?? 1,
|
||||
totalResults: results.total_results ?? 0,
|
||||
};
|
||||
});
|
||||
|
||||
@@ -26,7 +26,7 @@ export const trending = os.explore.trending
|
||||
.handler(async ({ input, context }) => {
|
||||
requireTmdb();
|
||||
|
||||
const data = await getTrending(input.type, "day");
|
||||
const data = await getTrending(input.type, "day", input.page);
|
||||
const results = (data.results ?? []) as Record<string, unknown>[];
|
||||
|
||||
const baseItems = results
|
||||
@@ -83,7 +83,15 @@ export const trending = os.explore.trending
|
||||
]
|
||||
: [{}, {}];
|
||||
|
||||
return { items, hero, userStatuses, episodeProgress };
|
||||
return {
|
||||
items,
|
||||
hero,
|
||||
userStatuses,
|
||||
episodeProgress,
|
||||
page: (data as { page?: number }).page ?? input.page,
|
||||
totalPages: (data as { total_pages?: number }).total_pages ?? 1,
|
||||
totalResults: (data as { total_results?: number }).total_results ?? 0,
|
||||
};
|
||||
});
|
||||
|
||||
export const popular = os.explore.popular
|
||||
@@ -91,7 +99,7 @@ export const popular = os.explore.popular
|
||||
.handler(async ({ input, context }) => {
|
||||
requireTmdb();
|
||||
|
||||
const data = await getPopular(input.type);
|
||||
const data = await getPopular(input.type, input.page);
|
||||
const baseItems = ((data.results ?? []) as Record<string, unknown>[])
|
||||
.filter((r) => r.poster_path)
|
||||
.map((r) => ({
|
||||
@@ -118,7 +126,14 @@ export const popular = os.explore.popular
|
||||
]
|
||||
: [{}, {}];
|
||||
|
||||
return { items, userStatuses, episodeProgress };
|
||||
return {
|
||||
items,
|
||||
userStatuses,
|
||||
episodeProgress,
|
||||
page: data.page ?? input.page,
|
||||
totalPages: data.total_pages ?? 1,
|
||||
totalResults: data.total_results ?? 0,
|
||||
};
|
||||
});
|
||||
|
||||
export const genres = os.explore.genres
|
||||
|
||||
@@ -15,13 +15,24 @@ export const detail = os.people.detail
|
||||
if (!person)
|
||||
throw new ORPCError("NOT_FOUND", { message: "Person not found" });
|
||||
|
||||
const filmography = await fetchFullFilmography(person.id);
|
||||
const allCredits = await fetchFullFilmography(person.id);
|
||||
|
||||
const start = (input.page - 1) * input.limit;
|
||||
const pageCredits = allCredits.slice(start, start + input.limit);
|
||||
|
||||
const userStatuses = getUserStatusesByTitleIds(
|
||||
context.user.id,
|
||||
filmography.map((c) => c.titleId),
|
||||
pageCredits.map((c) => c.titleId),
|
||||
);
|
||||
|
||||
return { person, filmography, userStatuses };
|
||||
return {
|
||||
person,
|
||||
filmography: pageCredits,
|
||||
userStatuses,
|
||||
page: input.page,
|
||||
totalPages: Math.max(1, Math.ceil(allCredits.length / input.limit)),
|
||||
totalResults: allCredits.length,
|
||||
};
|
||||
});
|
||||
|
||||
export const resolve = os.people.resolve
|
||||
|
||||
@@ -19,12 +19,12 @@ export const search = os.search.use(authed).handler(async ({ input }) => {
|
||||
|
||||
const query = input.query.trim();
|
||||
if (!query) {
|
||||
return { results: [] };
|
||||
return { results: [], page: 1, totalPages: 0, totalResults: 0 };
|
||||
}
|
||||
const type = input.type ?? null;
|
||||
|
||||
if (type === "person") {
|
||||
const personResults = await searchPerson(query);
|
||||
const personResults = await searchPerson(query, input.page);
|
||||
return {
|
||||
results: (personResults.results ?? []).map((r) => ({
|
||||
tmdbId: r.id,
|
||||
@@ -43,15 +43,18 @@ export const search = os.search.use(authed).handler(async ({ input }) => {
|
||||
.map((k) => k.title ?? (k as { name?: string }).name)
|
||||
.filter((s): s is string => !!s) as string[]) ?? null,
|
||||
})),
|
||||
page: personResults.page ?? input.page,
|
||||
totalPages: personResults.total_pages ?? 1,
|
||||
totalResults: personResults.total_results ?? 0,
|
||||
};
|
||||
}
|
||||
|
||||
const raw =
|
||||
type === "movie"
|
||||
? await searchMovies(query)
|
||||
? await searchMovies(query, input.page)
|
||||
: type === "tv"
|
||||
? await searchTv(query)
|
||||
: await searchMulti(query);
|
||||
? await searchTv(query, input.page)
|
||||
: await searchMulti(query, input.page);
|
||||
|
||||
type SearchResult = {
|
||||
id: number;
|
||||
@@ -105,5 +108,10 @@ export const search = os.search.use(authed).handler(async ({ input }) => {
|
||||
})
|
||||
.filter((r): r is NonNullable<typeof r> => r !== null);
|
||||
|
||||
return { results: mapped };
|
||||
return {
|
||||
results: mapped,
|
||||
page: raw.page ?? input.page,
|
||||
totalPages: raw.total_pages ?? 1,
|
||||
totalResults: raw.total_results ?? 0,
|
||||
};
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user