feat: implement infinite scrolling for public entity lists

Add cursor-based pagination to backend list queries
Introduce LoadMoreSentinel for intersection-based loading
Replace manual load more buttons with infinite scroll sentinel
This commit is contained in:
2026-05-06 08:33:08 +08:00
parent 91a001e3f9
commit c821e9ebba
16 changed files with 619 additions and 103 deletions

View File

@@ -109,6 +109,19 @@ export interface ProjectUpdatesParams {
limit?: number;
}
export interface ListPage<T> {
items: T[];
nextCursor: string | null;
hasMore: boolean;
}
export interface PublicListParams {
cursor?: string | null;
limit?: number;
}
export type PublicListQueryParams = Record<string, string | number | boolean | null | undefined> & PublicListParams;
export interface EntityImage {
path: string;
url: string;
@@ -997,11 +1010,11 @@ export interface RateLimitSettingsPayload {
policies: Record<RateLimitPolicyKey, RateLimitPolicySettings>;
}
export function buildQuery(params: Record<string, string | number | boolean | undefined>): string {
export function buildQuery(params: Record<string, string | number | boolean | null | undefined>): string {
const search = new URLSearchParams();
Object.entries(params).forEach(([key, value]) => {
if (value !== undefined && value !== '') {
if (value !== undefined && value !== null && value !== '') {
search.set(key, String(value));
}
});
@@ -1279,6 +1292,13 @@ export const api = {
deletePermission: (id: string | number) => deleteJson(`/api/admin/permissions/${id}`),
options: () => getJson<Options>('/api/options'),
dailyChecklist: () => getJson<DailyChecklistItem[]>('/api/daily-checklist'),
dailyChecklistPage: (params: PublicListParams = {}) =>
getJson<ListPage<DailyChecklistItem>>(
`/api/daily-checklist${buildQuery({
cursor: params.cursor ?? undefined,
limit: params.limit
})}`
),
lifePosts: (params: LifePostsParams = {}) =>
getJson<LifePostsPage>(
`/api/life-posts${buildQuery({
@@ -1395,6 +1415,14 @@ export const api = {
deleteConfig: (type: ConfigType, id: number) => deleteJson(`/api/admin/config/${type}/${id}`),
pokemon: (params: Record<string, string | number | boolean | undefined>) =>
getJson<Pokemon[]>(`/api/pokemon${buildQuery(params)}`),
pokemonPage: (params: PublicListQueryParams) =>
getJson<ListPage<Pokemon>>(
`/api/pokemon${buildQuery({
...params,
cursor: params.cursor ?? undefined,
limit: params.limit
})}`
),
pokemonDetail: (id: string | number) => getJson<PokemonDetail>(`/api/pokemon/${id}`),
pokemonFetchOptions: (search: string, signal?: AbortSignal, all = false) =>
getJson<PokemonFetchOption[]>(
@@ -1411,6 +1439,14 @@ export const api = {
reorderPokemon: (ids: number[]) => sendJson<Pokemon[]>('/api/admin/pokemon/order', 'PUT', { ids }),
habitats: (params: Record<string, string | number | boolean | undefined> = {}) =>
getJson<Habitat[]>(`/api/habitats${buildQuery(params)}`),
habitatsPage: (params: PublicListQueryParams = {}) =>
getJson<ListPage<Habitat>>(
`/api/habitats${buildQuery({
...params,
cursor: params.cursor ?? undefined,
limit: params.limit
})}`
),
habitatDetail: (id: string | number) => getJson<HabitatDetail>(`/api/habitats/${id}`),
createHabitat: (payload: HabitatPayload) => sendJson<HabitatDetail>('/api/habitats', 'POST', payload),
updateHabitat: (id: string | number, payload: HabitatPayload) =>
@@ -1419,6 +1455,14 @@ export const api = {
reorderHabitats: (ids: number[]) => sendJson<Habitat[]>('/api/admin/habitats/order', 'PUT', { ids }),
items: (params: Record<string, string | number | boolean | undefined>) =>
getJson<Item[]>(`/api/items${buildQuery(params)}`),
itemsPage: (params: PublicListQueryParams) =>
getJson<ListPage<Item>>(
`/api/items${buildQuery({
...params,
cursor: params.cursor ?? undefined,
limit: params.limit
})}`
),
itemDetail: (id: string | number) => getJson<ItemDetail>(`/api/items/${id}`),
createItem: (payload: ItemPayload) => sendJson<ItemDetail>('/api/items', 'POST', payload),
updateItem: (id: string | number, payload: ItemPayload) => sendJson<ItemDetail>(`/api/items/${id}`, 'PUT', payload),
@@ -1426,6 +1470,14 @@ export const api = {
reorderItems: (ids: number[]) => sendJson<Item[]>('/api/admin/items/order', 'PUT', { ids }),
ancientArtifacts: (params: Record<string, string | number | undefined> = {}) =>
getJson<AncientArtifact[]>(`/api/ancient-artifacts${buildQuery(params)}`),
ancientArtifactsPage: (params: PublicListQueryParams = {}) =>
getJson<ListPage<AncientArtifact>>(
`/api/ancient-artifacts${buildQuery({
...params,
cursor: params.cursor ?? undefined,
limit: params.limit
})}`
),
ancientArtifactDetail: (id: string | number) => getJson<AncientArtifactDetail>(`/api/ancient-artifacts/${id}`),
createAncientArtifact: (payload: AncientArtifactPayload) =>
sendJson<AncientArtifactDetail>('/api/ancient-artifacts', 'POST', payload),
@@ -1436,6 +1488,14 @@ export const api = {
sendJson<AncientArtifact[]>('/api/admin/ancient-artifacts/order', 'PUT', { ids }),
recipes: (params: Record<string, string | number | undefined> = {}) =>
getJson<Recipe[]>(`/api/recipes${buildQuery(params)}`),
recipesPage: (params: PublicListQueryParams = {}) =>
getJson<ListPage<Recipe>>(
`/api/recipes${buildQuery({
...params,
cursor: params.cursor ?? undefined,
limit: params.limit
})}`
),
recipeDetail: (id: string | number) => getJson<RecipeDetail>(`/api/recipes/${id}`),
createRecipe: (payload: RecipePayload) => sendJson<RecipeDetail>('/api/recipes', 'POST', payload),
updateRecipe: (id: string | number, payload: RecipePayload) =>