From 5c1a7ffa015d24b60bfdaf521d318ee3285e2002 Mon Sep 17 00:00:00 2001 From: IK Date: Mon, 21 Sep 2026 19:40:35 +0700 Subject: [PATCH] feat: add honest activity map mode --- apps/web/src/components/ActivityMap.astro | 46 +++++++++++++++++++++++ apps/web/src/pages/index.astro | 5 ++- apps/web/src/styles/dashboard-polish.css | 2 + docs/ROADMAP.md | 4 +- 4 files changed, 54 insertions(+), 3 deletions(-) create mode 100644 apps/web/src/components/ActivityMap.astro diff --git a/apps/web/src/components/ActivityMap.astro b/apps/web/src/components/ActivityMap.astro new file mode 100644 index 0000000..70dd22a --- /dev/null +++ b/apps/web/src/components/ActivityMap.astro @@ -0,0 +1,46 @@ +--- +import { spotPath, type Activity } from "../lib/api"; + +const { items } = Astro.props as { items: Activity[] }; +const xValues = items.map((item) => item.x); +const yValues = items.map((item) => item.y); +const minX = Math.min(...xValues, 0); +const maxX = Math.max(...xValues, 1); +const minY = Math.min(...yValues, 0); +const maxY = Math.max(...yValues, 1); +const xRange = Math.max(1, maxX - minX); +const yRange = Math.max(1, maxY - minY); +const column = (value: number) => Math.min(20, Math.max(1, Math.round(((value - minX) / xRange) * 18) + 1)); +const row = (value: number) => Math.min(20, Math.max(1, Math.round((1 - (value - minY) / yRange) * 18) + 1)); +--- +
+
+
Режим схемы

Горячие точки на координатной сетке

+

Показаны подтверждённые координаты текущей выборки. Это схема `x:y`, а не географическая карта водоёма.

+
+
+ + + {items.map((item) => {item.x}:{item.y}{item.fish})} +
+
+ + diff --git a/apps/web/src/pages/index.astro b/apps/web/src/pages/index.astro index 95ecb40..e3c9bbf 100644 --- a/apps/web/src/pages/index.astro +++ b/apps/web/src/pages/index.astro @@ -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 = {
Ваш запрос

{queryTitle}

{periodLabel} · {sortLabel}

{selectedWaterbody}{selectedFish}{periodLabel}{sortLabel}{filtersChanged && Сбросить запрос}
-
За выбранный период

Горячие точки

{items.length} на странице · всего {totalItems}
{filterError ?

Некорректные фильтры

Выберите период и сортировку из предложенных значений.

Сбросить фильтры
: activityUnavailable ? : items.length ?
{items.map(item => )}
:

Пока нет свежих данных

Для выбранных фильтров нет одобренных наблюдений. Расширьте период или выберите другой водоём.

{filtersChanged && Показать все данные}
}
+
За выбранный период

Горячие точки

{items.length} на странице · всего {totalItems}
{filterError ?

Некорректные фильтры

Выберите период и сортировку из предложенных значений.

Сбросить фильтры
: activityUnavailable ? : items.length ? (view === "map" ? :
{items.map(item => )}
) :

Пока нет свежих данных

Для выбранных фильтров нет одобренных наблюдений. Расширьте период или выберите другой водоём.

{filtersChanged && Показать все данные}
}
{items[0] && leaderLevel && }
{!filterError && !activityUnavailable && } diff --git a/apps/web/src/styles/dashboard-polish.css b/apps/web/src/styles/dashboard-polish.css index cb8b144..4ed7899 100644 --- a/apps/web/src/styles/dashboard-polish.css +++ b/apps/web/src/styles/dashboard-polish.css @@ -1 +1,3 @@ .dashboard:has(>.results-column:only-child){grid-template-columns:1fr} +.result-tools{display:flex;align-items:center;gap:10px}.view-toggle{display:flex;padding:3px;border:1px solid var(--border);border-radius:999px;background:var(--surface-soft)}.view-toggle a{padding:7px 10px;border-radius:999px;color:var(--text-muted);font-size:11px;font-weight:750;text-decoration:none}.view-toggle a:hover,.view-toggle__active{background:var(--surface);color:var(--text)!important;box-shadow:0 2px 8px color-mix(in srgb,var(--deep) 10%,transparent)} +@media(max-width:720px){.section-heading{align-items:start}.result-tools{display:grid;justify-items:end;gap:7px}.view-toggle a{padding:6px 9px}} diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index 9a6c420..13ac567 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -93,9 +93,9 @@ Результаты сравнительного аудита и ссылки сохранены в [ux-competitive-audit.md](ux-competitive-audit.md). Целевой критерий проекта: RF4 Spotter должен быстрее и честнее отвечать на вопрос «куда идти, на что ловить и почему этому можно доверять», чем каталог или лента community-постов. - [ ] **U01 · UX-контракт и scorecard.** Контракт маршрутов, смысловой порядок ответа, словарь статусов и целевые метрики зафиксированы в [ux-contract.md](ux-contract.md); для home, spot, waterbody, plan и tackle описаны первый ответ и обязательное объяснение. Сквозной Playwright task journey проверяет home → spot → «Что взять» → plan и возврат query-контекста, включая source/evidence-поля первой карточки; ручной сценарий и критерии зафиксированы в [ux-task-review-2026-09-21.md](ux-task-review-2026-09-21.md). Осталось провести review с участниками и собрать completion rate на пилоте. -- [ ] **U02 · Главный сценарий «рыба → водоём → точка → снасть».** Главная сохраняет рыбу, водоём, период и сортировку в shareable URL, явно показывает контекст запроса и даёт текстовый CTA «Открыть точку» на каждой карточке, включая mobile; первый экран ограничен пятью вариантами, а остальные доступны через сохраняющую query-контекст серверную пагинацию; empty-state предлагает вернуться к полному набору данных. Осталось добавить режим map. Acceptance: первый полезный вариант виден без регистрации, back/refresh сохраняют контекст, mobile не теряет фильтры. +- [ ] **U02 · Главный сценарий «рыба → водоём → точка → снасть».** Главная сохраняет рыбу, водоём, период и сортировку в shareable URL, явно показывает контекст запроса и даёт текстовый CTA «Открыть точку» на каждой карточке, включая mobile; первый экран ограничен пятью вариантами, а остальные доступны через сохраняющую query-контекст серверную пагинацию; empty-state предлагает вернуться к полному набору данных. Добавлен URL-сохраняющий режим `view=map` с отдельным mobile-friendly действием: он показывает только подтверждённые `x:y` на честной координатной схеме и ведёт в detail точки, без имитации геометрии водоёма. Acceptance: первый полезный вариант виден без регистрации, back/refresh сохраняют контекст, mobile не теряет фильтры. - [ ] **U03 · Evidence/trust card.** Общий evidence-контракт используется на activity-карточках, detail точки, водоёма и карточках снастей: freshness с текстовым `Свежо`/`Устарело`, явный период расчёта, completeness, confidence/статус, source badges и доступные доменные поля; при выборке меньше 3 явно показано отдельное состояние `Недостаточно данных`, ограниченный источник не смешивается с неполными полями, а явно переданные provenance-конфликты видны текстом. Источники координат доступны в раскрываемом блоке паспорта, тогда как точность остаётся видимой в основном ответе. Targeted E2E и API-тест проверяют публичные пути, период, 48-часовой порог и конфликт источников; единый `passportDisplayStatus` дополнительно закрепляет приоритет `conflict` → `stale` → исходный статус unit-регрессией. Все публичные provenance links проходят credential-free HTTP(S) allowlist. -- [ ] **U04 · List/map и progressive disclosure.** List остаётся честным базовым режимом: фильтры, сортировка, URL-состояние и evidence-карточки уже работают без имитации координатной карты. Источники координат в activity/spot evidence-паспорте и многокомпонентный комплект снастей в списке уловов раскрываются по запросу, не скрывая основную точность, статус и источник; canonical-компоненты сохраняют переходы в каталог. Следующий шаг — единый list/map-контракт после подтверждения геометрии; на mobile карта должна открываться отдельным действием. Вторичные raw/provenance-поля не исчезают и раскрываются по запросу. +- [ ] **U04 · List/map и progressive disclosure.** List остаётся честным базовым режимом: фильтры, сортировка, URL-состояние и evidence-карточки уже работают без имитации координатной карты. Источники координат в activity/spot evidence-паспорте и многокомпонентный комплект снастей в списке уловов раскрываются по запросу, не скрывая основную точность, статус и источник; canonical-компоненты сохраняют переходы в каталог. Режим `view=map` использует единый URL-контракт, на mobile открывается отдельным действием и прямо маркирован как координатная схема, пока геометрия водоёма не подтверждена. Вторичные raw/provenance-поля не исчезают и раскрываются по запросу. - [ ] **U05 · Mobile-first и сохранённый план рыбалки.** `/plan` поддерживает список до 5 уникальных локальных вариантов, удаление, очистку, переход к точке, print/PDF и восстановление из shareable URL; кнопка «Поделиться планом» использует native share или clipboard fallback и явно сообщает, если план пуст или share-ссылка повреждена. Detail-кнопка сохраняет данные с `aria-pressed` и восстанавливается после reload. Print/mobile-контракт проверяет 320/390 px, лимит импорта и отсутствие горизонтального overflow; импорт дедуплицирует точки, нормализует длины локальных текстовых полей, принимает confidence только в диапазоне `0..100` и не теряет реальное значение `0`, а карточка плана сравнивает подтверждённые метод/проводку и явно показывает риск по выборке/уверенности. - [ ] **U06 · Контентная и визуальная иерархия.** Для detail точки действие «Что взять» выделено отдельным заголовком, provenance и качество собраны в общем паспорте данных, а подтверждённые метод/проводка видны рядом с последними уловами; статусы дополнительно передаются текстом, малая выборка не маркируется как готовая рекомендация, а дублирующие catches/players/confidence убраны из вторичной колонки activity-карточки. Mobile route-contract проверяет первый ответ и действие на home, spot, waterbody, plan и tackle, а task regression требует source/evidence-поля; остаётся ручной review визуальных деталей по [ux-task-review-2026-09-21.md](ux-task-review-2026-09-21.md). - [ ] **U07 · UX-приёмка и измерения.** Playwright-контракты уже покрывают query journey, сквозной task journey, evidence states, saved plan, share/print, accessibility и visual matrix; unit-контракт добавил детерминированные stale- и blocked-подписи, а insufficient-data теперь отделён от incomplete в публичном паспорте. `/status` явно сообщает, что при ограничении источника опубликованные данные сохранены. Visual-matrix regression теперь проверяет `252` комбинации, включая valid/error spot, plan и tackle analytics; representative review зафиксирован в `docs/ux-reference-review-2026-09-21.md`. Локальный Lighthouse baseline обновлён: performance 98 и accessibility 100. Остаётся получить production Lighthouse/CLS/INP/time-to-first-useful-answer после пилота и принять решение о долговременном хранении screenshots.