feat(home): Pagination-Tiefe per Snapshot, scroll-restore deep-scroll-fest
Some checks failed
Build & Publish Docker Image / build-and-push (push) Has been cancelled
Some checks failed
Build & Publish Docker Image / build-and-push (push) Has been cancelled
Bei tiefer Endless-Scroll (z. B. 60 nachgeladene Items) versagte der generische scroll-restore: nach dem Back-Mount lud onMount() nur die initialen 10 Items via loadAllMore(), das Dokument blieb ~1000px hoch und der rAF-Poll konnte die gespeicherte scrollY (z. B. 4000) nie erreichen — Best-Effort scrollTo clampte auf die erreichbare Hoehe. Fix per SvelteKit-Snapshot, derselbe Mechanismus den die Page bereits fuer SearchStore nutzt: Capture nimmt zusaetzlich allRecipes.length, allSort und allExhausted mit. Restore setzt sort sofort und parkt die Tiefe in pendingPagination. onMount sieht das Pending und ruft statt loadAllMore() ein einmaliges rehydrateAll(sort, count, exhausted) — ein Fetch mit limit=count rehydriert die ganze Liste atomar. Danach hat das Dokument die Originalhoehe und der Layout-Restore-Poll laesst die scrollY genau dort landen, wo der User vorher war. API-Cap (src/routes/api/recipes/all/+server.ts) von 50 auf 200 angehoben — Recipe-Metadaten sind klein (~200 B/Stueck), 200er- Response ~40 KB. Cap deckt realistische Scroll-Tiefen. Reload (Cmd-R) behaelt das alte Verhalten: ohne Snapshot greift der sort-aus-localStorage-Pfad, lade-Sequenz startet wieder bei 10. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
1
.claude/scheduled_tasks.lock
Normal file
1
.claude/scheduled_tasks.lock
Normal file
@@ -0,0 +1 @@
|
|||||||
|
{"sessionId":"30c25419-6cff-4007-9cdc-dfbb83eda4c5","pid":31160,"acquiredAt":1776842935855}
|
||||||
1443
ci-log.txt
Normal file
1443
ci-log.txt
Normal file
File diff suppressed because it is too large
Load Diff
@@ -42,10 +42,51 @@
|
|||||||
let allChips: HTMLElement | undefined = $state();
|
let allChips: HTMLElement | undefined = $state();
|
||||||
let allObserver: IntersectionObserver | null = null;
|
let allObserver: IntersectionObserver | null = null;
|
||||||
|
|
||||||
export const snapshot: Snapshot<SearchSnapshot> = {
|
// Snapshot persists across history navigation. We capture not only the
|
||||||
capture: () => store.captureSnapshot(),
|
// search store, but also the pagination depth ("user had loaded 60
|
||||||
restore: (s) => store.restoreSnapshot(s)
|
// recipes via infinite scroll") so on back-nav we can re-hydrate the
|
||||||
|
// full list in one fetch — otherwise the document is too short and
|
||||||
|
// scroll-restore can't reach the saved Y position.
|
||||||
|
type HomeSnapshot = SearchSnapshot & {
|
||||||
|
allLoaded: number;
|
||||||
|
allSort: AllSort;
|
||||||
|
allExhausted: boolean;
|
||||||
};
|
};
|
||||||
|
let pendingPagination: { count: number; sort: AllSort; exhausted: boolean } | null = null;
|
||||||
|
|
||||||
|
export const snapshot: Snapshot<HomeSnapshot> = {
|
||||||
|
capture: () => ({
|
||||||
|
...store.captureSnapshot(),
|
||||||
|
allLoaded: allRecipes.length,
|
||||||
|
allSort,
|
||||||
|
allExhausted
|
||||||
|
}),
|
||||||
|
restore: (s) => {
|
||||||
|
store.restoreSnapshot(s);
|
||||||
|
if (s.allLoaded > 0) {
|
||||||
|
pendingPagination = {
|
||||||
|
count: s.allLoaded,
|
||||||
|
sort: s.allSort,
|
||||||
|
exhausted: s.allExhausted
|
||||||
|
};
|
||||||
|
allSort = s.allSort;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
async function rehydrateAll(sort: AllSort, count: number, exhausted: boolean) {
|
||||||
|
allLoading = true;
|
||||||
|
try {
|
||||||
|
const res = await fetch(`/api/recipes/all?sort=${sort}&limit=${count}&offset=0`);
|
||||||
|
if (!res.ok) return;
|
||||||
|
const body = await res.json();
|
||||||
|
const hits = body.hits as SearchHit[];
|
||||||
|
allRecipes = hits;
|
||||||
|
allExhausted = exhausted || hits.length < count;
|
||||||
|
} finally {
|
||||||
|
allLoading = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function loadRecent() {
|
async function loadRecent() {
|
||||||
const res = await fetch('/api/recipes/search');
|
const res = await fetch('/api/recipes/search');
|
||||||
@@ -120,11 +161,20 @@
|
|||||||
if (urlQ) store.query = urlQ;
|
if (urlQ) store.query = urlQ;
|
||||||
void loadRecent();
|
void loadRecent();
|
||||||
void searchFilterStore.load();
|
void searchFilterStore.load();
|
||||||
const saved = localStorage.getItem('kochwas.allSort');
|
if (pendingPagination) {
|
||||||
if (saved && ['name', 'rating', 'cooked', 'created'].includes(saved)) {
|
// Back-navigation: restore the full loaded depth in one shot so
|
||||||
allSort = saved as AllSort;
|
// the document is tall enough for scroll-restore to land. Sort
|
||||||
|
// already came from the snapshot — don't read localStorage here.
|
||||||
|
const p = pendingPagination;
|
||||||
|
pendingPagination = null;
|
||||||
|
void rehydrateAll(p.sort, p.count, p.exhausted);
|
||||||
|
} else {
|
||||||
|
const saved = localStorage.getItem('kochwas.allSort');
|
||||||
|
if (saved && ['name', 'rating', 'cooked', 'created'].includes(saved)) {
|
||||||
|
allSort = saved as AllSort;
|
||||||
|
}
|
||||||
|
void loadAllMore();
|
||||||
}
|
}
|
||||||
void loadAllMore();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// IntersectionObserver an den Sentinel hängen — wenn sichtbar, nachladen.
|
// IntersectionObserver an den Sentinel hängen — wenn sichtbar, nachladen.
|
||||||
|
|||||||
@@ -11,7 +11,11 @@ const VALID_SORTS = new Set<AllRecipesSort>(['name', 'rating', 'cooked', 'create
|
|||||||
export const GET: RequestHandler = async ({ url }) => {
|
export const GET: RequestHandler = async ({ url }) => {
|
||||||
const sortRaw = (url.searchParams.get('sort') ?? 'name') as AllRecipesSort;
|
const sortRaw = (url.searchParams.get('sort') ?? 'name') as AllRecipesSort;
|
||||||
if (!VALID_SORTS.has(sortRaw)) error(400, { message: 'Invalid sort' });
|
if (!VALID_SORTS.has(sortRaw)) error(400, { message: 'Invalid sort' });
|
||||||
const limit = Math.min(50, Math.max(1, Number(url.searchParams.get('limit') ?? 10)));
|
// Cap is 200 (not 10's typical paging step) to support snapshot-based
|
||||||
|
// pagination restore on /+page.svelte: when the user navigates back
|
||||||
|
// after deep infinite-scroll, we re-hydrate the full loaded count in
|
||||||
|
// one round-trip so document height matches and scroll-restore lands.
|
||||||
|
const limit = Math.min(200, Math.max(1, Number(url.searchParams.get('limit') ?? 10)));
|
||||||
const offset = Math.max(0, Number(url.searchParams.get('offset') ?? 0));
|
const offset = Math.max(0, Number(url.searchParams.get('offset') ?? 0));
|
||||||
const hits = listAllRecipesPaginated(getDb(), sortRaw, limit, offset);
|
const hits = listAllRecipesPaginated(getDb(), sortRaw, limit, offset);
|
||||||
return json({ sort: sortRaw, limit, offset, hits });
|
return json({ sort: sortRaw, limit, offset, hits });
|
||||||
|
|||||||
Reference in New Issue
Block a user