feat: add honest activity map mode

This commit is contained in:
ik
2026-09-21 19:40:35 +07:00
parent b9aecb75f5
commit 5c1a7ffa01
4 changed files with 54 additions and 3 deletions
+4 -1
View File
@@ -1,6 +1,7 @@
---
import Layout from "../layouts/Layout.astro";
import ActivityCard from "../components/ActivityCard.astro";
import ActivityMap from "../components/ActivityMap.astro";
import FishingIcon from "../components/FishingIcon.astro";
import SourceBadge from "../components/SourceBadge.astro";
import TackleGlyph from "../components/TackleGlyph.astro";
@@ -14,6 +15,7 @@ const hours = params.get("hours") ?? "24";
const waterbody = params.get("waterbody") ?? "";
const fish = params.get("fish") ?? "";
const sort = params.get("sort") ?? "activity";
const view = params.get("view") === "map" ? "map" : "list";
const activityLimit = 5;
const requestedOffset = Number(params.get("offset") ?? 0);
let offset = Number.isInteger(requestedOffset) && requestedOffset >= 0 ? requestedOffset : 0;
@@ -73,6 +75,7 @@ const queryTitle = `${selectedFish} · ${selectedWaterbody}`;
const moreSignalParams = new URLSearchParams(params);
moreSignalParams.set("signals", String(Math.min(48, signalLimit + 12)));
const moreSignalsHref = `/?${moreSignalParams.toString()}#signals-title`;
const viewHref = (next: "list" | "map") => { const query = new URLSearchParams(params); query.set("view", next); query.delete("offset"); return `/?${query.toString()}#results`; };
const datasetJsonLd = {
"@context": "https://schema.org", "@type": "Dataset",
name: "RF4 Spotter — наблюдения об активности рыбы",
@@ -94,7 +97,7 @@ const datasetJsonLd = {
<button data-action="primary">⌕ Найти клёв</button>
</form></section>
<section class="query-summary content-grid" aria-label="Контекст запроса" aria-live="polite"><div><span class="overline">Ваш запрос</span><h2 id="query-summary-title">{queryTitle}</h2><p>{periodLabel} · {sortLabel}</p></div><div class="query-summary__filters" aria-label="Применённые фильтры"><span>{selectedWaterbody}</span><span>{selectedFish}</span><span>{periodLabel}</span><span>{sortLabel}</span>{filtersChanged && <a href="/#results">Сбросить запрос</a>}</div></section>
<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}</span></div>{filterError ? <div class="state error-state"><h2>Некорректные фильтры</h2><p>Выберите период и сортировку из предложенных значений.</p><a data-action="secondary" href="/">Сбросить фильтры</a></div> : activityUnavailable ? <StatePanel contained={false} tone="unavailable" title="Горячие точки временно недоступны" description="Полевые сигналы и справочники продолжают работать независимо." /> : items.length ? <div class="spot-list">{items.map(item => <ActivityCard item={item} periodLabel={periodLabel} />)}</div> : <div class="state"><h2>Пока нет свежих данных</h2><p>Для выбранных фильтров нет одобренных наблюдений. Расширьте период или выберите другой водоём.</p>{filtersChanged && <a data-action="secondary" href="/#results">Показать все данные</a>}</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><div class="result-tools"><span class="result-count">{items.length} на странице · всего {totalItems}</span><nav class="view-toggle" aria-label="Режим отображения"><a class:list={{ "view-toggle__active": view === "list" }} href={viewHref("list")} aria-current={view === "list" ? "page" : undefined}>Список</a><a class:list={{ "view-toggle__active": view === "map" }} href={viewHref("map")} aria-current={view === "map" ? "page" : undefined}>Схема</a></nav></div></div>{filterError ? <div class="state error-state"><h2>Некорректные фильтры</h2><p>Выберите период и сортировку из предложенных значений.</p><a data-action="secondary" href="/">Сбросить фильтры</a></div> : activityUnavailable ? <StatePanel contained={false} tone="unavailable" title="Горячие точки временно недоступны" description="Полевые сигналы и справочники продолжают работать независимо." /> : items.length ? (view === "map" ? <ActivityMap items={items} /> : <div class="spot-list">{items.map(item => <ActivityCard item={item} periodLabel={periodLabel} />)}</div>) : <div class="state"><h2>Пока нет свежих данных</h2><p>Для выбранных фильтров нет одобренных наблюдений. Расширьте период или выберите другой водоём.</p>{filtersChanged && <a data-action="secondary" href="/#results">Показать все данные</a>}</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:list={["float-gauge", `meter-level-${leaderMeter}`]} 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><TackleGlyph name={items[0].best_bait}/><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>
{!filterError && !activityUnavailable && <Pagination path="/" params={params} total={totalItems} limit={activityLimit} offset={offset} anchor="#results" itemLabel="точек" />}