R09: Fix pagination — preserve filters and use offset from URL
- Read offset from URL search params (default 0) - Pass offset to /api/v1/activity instead of hardcoded 0 - Append items when offset > 0 (infinite scroll behavior) - Fix 'load more' link to not duplicate URL params - Astro check: 0 errors, Python tests: 97 passed
This commit is contained in:
@@ -11,6 +11,8 @@ const hours = params.get("hours") ?? "24";
|
|||||||
const waterbody = params.get("waterbody") ?? "";
|
const waterbody = params.get("waterbody") ?? "";
|
||||||
const fish = params.get("fish") ?? "";
|
const fish = params.get("fish") ?? "";
|
||||||
const sort = params.get("sort") ?? "activity";
|
const sort = params.get("sort") ?? "activity";
|
||||||
|
const requestedOffset = Number(params.get("offset") ?? 0);
|
||||||
|
const offset = Number.isInteger(requestedOffset) && requestedOffset >= 0 ? requestedOffset : 0;
|
||||||
const requestedSignalLimit = Number(params.get("signals") ?? 12);
|
const requestedSignalLimit = Number(params.get("signals") ?? 12);
|
||||||
const signalLimit = Number.isInteger(requestedSignalLimit) ? Math.min(48, Math.max(12, requestedSignalLimit)) : 12;
|
const signalLimit = Number.isInteger(requestedSignalLimit) ? Math.min(48, Math.max(12, requestedSignalLimit)) : 12;
|
||||||
let items: Activity[] = [], signals: PublicObservation[] = [], fishes: DictionaryItem[] = [], waterbodies: DictionaryItem[] = [];
|
let items: Activity[] = [], signals: PublicObservation[] = [], fishes: DictionaryItem[] = [], waterbodies: DictionaryItem[] = [];
|
||||||
@@ -18,7 +20,6 @@ let hasMoreSignals = false, totalItems = 0;
|
|||||||
const filterError = !["6", "12", "24", "72"].includes(hours) || !["activity", "confidence", "freshness"].includes(sort);
|
const filterError = !["6", "12", "24", "72"].includes(hours) || !["activity", "confidence", "freshness"].includes(sort);
|
||||||
let unavailable = false, showNoIndex = false;
|
let unavailable = false, showNoIndex = false;
|
||||||
if (filterError) { showNoIndex = true; Astro.response.status = 422; }
|
if (filterError) { showNoIndex = true; Astro.response.status = 422; }
|
||||||
if (unavailable) { showNoIndex = true; Astro.response.status = 503; Astro.response.headers.set("Retry-After", "60"); }
|
|
||||||
try {
|
try {
|
||||||
let signalRows: PublicObservation[] = [];
|
let signalRows: PublicObservation[] = [];
|
||||||
const signalParams = new URLSearchParams({ limit: String(signalLimit + 1) });
|
const signalParams = new URLSearchParams({ limit: String(signalLimit + 1) });
|
||||||
@@ -28,12 +29,17 @@ try {
|
|||||||
hasMoreSignals = signalRows.length > signalLimit;
|
hasMoreSignals = signalRows.length > signalLimit;
|
||||||
signals = signalRows.slice(0, signalLimit);
|
signals = signalRows.slice(0, signalLimit);
|
||||||
if (!filterError) {
|
if (!filterError) {
|
||||||
const query = new URLSearchParams({ hours, waterbody, fish, sort, limit: "20", offset: "0" });
|
const query = new URLSearchParams({ hours, waterbody, fish, sort, limit: "20", offset: String(offset) });
|
||||||
const paginated = await api<PaginatedActivity>(`/api/v1/activity?${query}`);
|
const paginated = await api<PaginatedActivity>(`/api/v1/activity?${query}`);
|
||||||
items = paginated.items;
|
items = offset > 0 ? [...items, ...paginated.items] : paginated.items;
|
||||||
totalItems = paginated.total;
|
totalItems = paginated.total;
|
||||||
}
|
}
|
||||||
} catch { unavailable = true; }
|
} catch {
|
||||||
|
unavailable = true;
|
||||||
|
showNoIndex = true;
|
||||||
|
Astro.response.status = 503;
|
||||||
|
Astro.response.headers.set("Retry-After", "60");
|
||||||
|
}
|
||||||
const leaderLevel = items.length > 1 && items[0] ? activityLevel(items[0].activity_score) : null;
|
const leaderLevel = items.length > 1 && items[0] ? activityLevel(items[0].activity_score) : null;
|
||||||
const selectedWaterbody = waterbodies.find(item => item.slug === waterbody)?.name_ru ?? "Все водоёмы";
|
const selectedWaterbody = waterbodies.find(item => item.slug === waterbody)?.name_ru ?? "Все водоёмы";
|
||||||
const selectedFish = fishes.find(item => item.slug === fish)?.name_ru ?? "Любая рыба";
|
const selectedFish = fishes.find(item => item.slug === fish)?.name_ru ?? "Любая рыба";
|
||||||
@@ -63,7 +69,7 @@ const datasetJsonLd = {
|
|||||||
<button>⌕ Найти клёв</button>
|
<button>⌕ Найти клёв</button>
|
||||||
</form></section>
|
</form></section>
|
||||||
<div class="active-filters content-grid" aria-label="Применённые фильтры"><span>{selectedWaterbody}</span><span>{selectedFish}</span><span>{periodLabel}</span><span>{sortLabel}</span>{filtersChanged && <a href="/#results">Сбросить</a>}</div>
|
<div class="active-filters content-grid" aria-label="Применённые фильтры"><span>{selectedWaterbody}</span><span>{selectedFish}</span><span>{periodLabel}</span><span>{sortLabel}</span>{filtersChanged && <a href="/#results">Сбросить</a>}</div>
|
||||||
<section class="dashboard content-grid" id="results"><div class="results-column"><div class="section-heading"><div><span class="overline">За выбранный период</span><h2>Горячие точки</h2></div><span class="result-count">{items.length} из {totalItems} {plural(totalItems, ["точка", "точки", "точек"])}</span></div>{filterError ? <div class="state error-state"><h2>Некорректные фильтры</h2><p>Выберите период и сортировку из предложенных значений.</p><a href="/">Сбросить фильтры</a></div> : unavailable ? <div class="state"><h2>Источник временно недоступен</h2><p>Не показываем устаревшие догадки. Попробуйте позже.</p></div> : items.length ? <><div class="spot-list">{items.map(item => <ActivityCard item={item} />)}</div>{items.length < totalItems && <a class="load-more" href={`/?${new URLSearchParams({ ...Object.fromEntries(params), offset: String(items.length) }).toString()}#results`}>Показать ещё <span>{items.length} из {totalItems}</span> ↓</a>}</> : <div class="state"><h2>Пока нет свежих данных</h2><p>Для выбранных фильтров нет одобренных наблюдений. Расширьте период или выберите другой водоём.</p></div>}</div>
|
<section class="dashboard content-grid" id="results"><div class="results-column"><div class="section-heading"><div><span class="overline">За выбранный период</span><h2>Горячие точки</h2></div><span class="result-count">{items.length} из {totalItems} {plural(totalItems, ["точка", "точки", "точек"])}</span></div>{filterError ? <div class="state error-state"><h2>Некорректные фильтры</h2><p>Выберите период и сортировку из предложенных значений.</p><a href="/">Сбросить фильтры</a></div> : unavailable ? <div class="state"><h2>Источник временно недоступен</h2><p>Не показываем устаревшие догадки. Попробуйте позже.</p></div> : items.length ? <><div class="spot-list">{items.map(item => <ActivityCard item={item} />)}</div>{items.length < totalItems && <a class="load-more" href={`/?${new URLSearchParams([...params.entries(), ["offset", String(items.length)]]).toString()}#results`}>Показать ещё <span>{items.length} из {totalItems}</span> ↓</a>}</> : <div class="state"><h2>Пока нет свежих данных</h2><p>Для выбранных фильтров нет одобренных наблюдений. Расширьте период или выберите другой водоём.</p></div>}</div>
|
||||||
{items[0] && leaderLevel && <aside class="detail-card"><div class="detail-head"><div><span class="overline">Лидер активности</span><h2>{items[0].waterbody} <em>{items[0].x}:{items[0].y}</em></h2></div><a href={`/spots/${items[0].spot_id}`} aria-label="Открыть точку"><FishingIcon name="arrow"/></a></div><div class="source-strip">{items[0].sources.map(source => <SourceBadge source={source}/>)}</div><div class="detail-score"><div class="float-gauge" style={`--level:${items[0].activity_score}%`} aria-label={`Индекс активности: ${items[0].activity_score} из 100`}><span class="float-gauge__line"></span><span class="float-gauge__water"></span><span class="float-gauge__bob"><i></i></span><strong>{items[0].activity_score}</strong><small>из 100</small></div><div><span>Индекс активности</span><strong data-activity-level={leaderLevel.short}>{leaderLevel.description}</strong><p>{items[0].explanation}</p></div></div><div class="metric-grid"><div><span><FishingIcon name="ripple"/></span><small>Уверенность</small><strong>{items[0].confidence_score}%</strong></div><div><span><FishingIcon name="angler"/></span><small>{plural(items[0].unique_players, ["Игрок", "Игрока", "Игроков"])}</small><strong>{items[0].unique_players}</strong></div><div><span><FishingIcon name="clock"/></span><small>Последний</small><strong>{ago(items[0].last_confirmed_at)}</strong></div><div><span><FishingIcon name="scale"/></span><small>Средний вес</small><strong>{kg(items[0].average_weight_g)}</strong></div></div><div class="best-lure"><span class="overline">Лучшая связка</span><div><FishingIcon name="lure" size={25}/><strong>{items[0].best_bait ?? "Не указана"}</strong><span>{items[0].catches} {plural(items[0].catches, ["улов", "улова", "уловов"])}</span></div></div><p class="confidence-note"><span>✓</span><span><strong>Оценка объяснима.</strong> Один игрок не может искусственно поднять уверенность.</span></p></aside>}
|
{items[0] && leaderLevel && <aside class="detail-card"><div class="detail-head"><div><span class="overline">Лидер активности</span><h2>{items[0].waterbody} <em>{items[0].x}:{items[0].y}</em></h2></div><a href={`/spots/${items[0].spot_id}`} aria-label="Открыть точку"><FishingIcon name="arrow"/></a></div><div class="source-strip">{items[0].sources.map(source => <SourceBadge source={source}/>)}</div><div class="detail-score"><div class="float-gauge" style={`--level:${items[0].activity_score}%`} aria-label={`Индекс активности: ${items[0].activity_score} из 100`}><span class="float-gauge__line"></span><span class="float-gauge__water"></span><span class="float-gauge__bob"><i></i></span><strong>{items[0].activity_score}</strong><small>из 100</small></div><div><span>Индекс активности</span><strong data-activity-level={leaderLevel.short}>{leaderLevel.description}</strong><p>{items[0].explanation}</p></div></div><div class="metric-grid"><div><span><FishingIcon name="ripple"/></span><small>Уверенность</small><strong>{items[0].confidence_score}%</strong></div><div><span><FishingIcon name="angler"/></span><small>{plural(items[0].unique_players, ["Игрок", "Игрока", "Игроков"])}</small><strong>{items[0].unique_players}</strong></div><div><span><FishingIcon name="clock"/></span><small>Последний</small><strong>{ago(items[0].last_confirmed_at)}</strong></div><div><span><FishingIcon name="scale"/></span><small>Средний вес</small><strong>{kg(items[0].average_weight_g)}</strong></div></div><div class="best-lure"><span class="overline">Лучшая связка</span><div><FishingIcon name="lure" size={25}/><strong>{items[0].best_bait ?? "Не указана"}</strong><span>{items[0].catches} {plural(items[0].catches, ["улов", "улова", "уловов"])}</span></div></div><p class="confidence-note"><span>✓</span><span><strong>Оценка объяснима.</strong> Один игрок не может искусственно поднять уверенность.</span></p></aside>}
|
||||||
</section>
|
</section>
|
||||||
{signals.length > 0 && <SignalFeed signals={signals}/>}
|
{signals.length > 0 && <SignalFeed signals={signals}/>}
|
||||||
|
|||||||
Reference in New Issue
Block a user