Fix explore paging for non-date sorts (#24392)

* fix explore paging for non-date sorts

Explore paged every sort by passing the last row's `start_time` as a `before` or `after` cursor, which only works when rows are ordered by `start_time`. For score, speed, and relevance sorts, each page dropped every match newer than that row and repeated older rows from earlier pages, so infinite scroll stopped after a few pages. `/events` and `/events/search` now accept `offset`, and Explore pages non-date sorts by offset. Date sorts keep the cursor because `useSWRInfinite` only revalidates the first page, and cursor keys for later pages follow it while offset keys don't. Score and speed sorts on `/events` break ties on `id` so offset pages stay stable.

* order search ties by id and reject negative offsets

`/events/search` sorted in Python over a query with no `ORDER BY`, so tied scores, speeds, or distances kept whatever order SQLite returned, which isn't guaranteed to match across page requests. The query is now ordered by id and the stable sorts keep that order for ties. `offset` also accepted negative values, which sliced from the end of the search results.
This commit is contained in:
Josh Hawkins
2026-09-17 11:14:20 -06:00
committed by GitHub
parent d69107de33
commit 06967fec91
6 changed files with 162 additions and 8 deletions
+26 -6
View File
@@ -198,7 +198,19 @@ export default function Explore() {
const [url, params] = searchQuery;
const isAscending = params.sort?.includes("date_asc");
// a start_time cursor only works when rows are ordered by start_time,
// so every other sort pages by offset
const isDateSort =
params.sort === "date_asc" ||
params.sort === "date_desc" ||
(!params.sort && url === "events");
if (pageIndex > 0 && !isDateSort) {
return [
url,
{ ...params, offset: pageIndex * API_LIMIT, limit: API_LIMIT },
];
}
if (pageIndex > 0 && previousPageData) {
const lastDate = previousPageData[previousPageData.length - 1].start_time;
@@ -206,7 +218,8 @@ export default function Explore() {
url,
{
...params,
[isAscending ? "after" : "before"]: lastDate.toString(),
[params.sort === "date_asc" ? "after" : "before"]:
lastDate.toString(),
limit: API_LIMIT,
},
];
@@ -238,10 +251,17 @@ export default function Explore() {
},
});
const searchResults = useMemo(
() => (data ? ([] as SearchResult[]).concat(...data) : []),
[data],
);
// offset pages can overlap when results shift between page fetches
const searchResults = useMemo(() => {
if (!data) return [];
const seen = new Set<string>();
return data.flat().filter((result) => {
if (seen.has(result.id)) return false;
seen.add(result.id);
return true;
});
}, [data]);
const isLoadingInitialData = !data && !isValidating;
const isLoadingMore =
isLoadingInitialData ||
+1
View File
@@ -109,6 +109,7 @@ export type SearchQueryParams = {
max_speed?: number;
search_type?: string;
limit?: number;
offset?: number;
in_progress?: number;
include_thumbnails?: number;
query?: string;