From 7efdb1a16a4f4fd9e5299ec61f857124d347d7ff Mon Sep 17 00:00:00 2001 From: IK Date: Sun, 20 Sep 2026 19:55:33 +0700 Subject: [PATCH] feat: clarify home query context --- apps/web/src/layouts/Layout.astro | 1 + apps/web/src/pages/index.astro | 5 +++-- apps/web/src/styles/query-context.css | 7 +++++++ apps/web/tests/query-journey.spec.ts | 26 ++++++++++++++++++++++++++ docs/ROADMAP.md | 4 ++-- 5 files changed, 39 insertions(+), 4 deletions(-) create mode 100644 apps/web/src/styles/query-context.css create mode 100644 apps/web/tests/query-journey.spec.ts diff --git a/apps/web/src/layouts/Layout.astro b/apps/web/src/layouts/Layout.astro index 2d4e05e..4908665 100644 --- a/apps/web/src/layouts/Layout.astro +++ b/apps/web/src/layouts/Layout.astro @@ -14,6 +14,7 @@ import "../styles/signal-pagination.css"; import "../styles/pagination.css"; import "../styles/dashboard-polish.css"; import "../styles/loading-states.css"; +import "../styles/query-context.css"; import "../styles/theme.css"; import "../styles/media-catalog.css"; import FishingIcon from "../components/FishingIcon.astro"; diff --git a/apps/web/src/pages/index.astro b/apps/web/src/pages/index.astro index cff15dc..e0be486 100644 --- a/apps/web/src/pages/index.astro +++ b/apps/web/src/pages/index.astro @@ -69,6 +69,7 @@ const selectedFish = fishes.find(item => item.slug === fish)?.name_ru ?? "Люб const periodLabel = { "6": "6 часов", "12": "12 часов", "24": "24 часа", "72": "72 часа" }[hours] ?? hours; const sortLabel = { activity: "Сначала активные", confidence: "Сначала надёжные", freshness: "Сначала свежие" }[sort] ?? sort; const filtersChanged = Boolean(waterbody || fish || hours !== "24" || sort !== "activity"); +const queryTitle = `${selectedFish} · ${selectedWaterbody}`; const moreSignalParams = new URLSearchParams(params); moreSignalParams.set("signals", String(Math.min(48, signalLimit + 12))); const moreSignalsHref = `/?${moreSignalParams.toString()}#signals-title`; @@ -92,8 +93,8 @@ const datasetJsonLd = { -
{selectedWaterbody}{selectedFish}{periodLabel}{sortLabel}{filtersChanged && Сбросить}
-
За выбранный период

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

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

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

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

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

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

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

}
+
Ваш запрос

{queryTitle}

{periodLabel} · {sortLabel}

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

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

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

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

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

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

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

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

{filtersChanged && Показать все данные}
}
{items[0] && leaderLevel && }
{!filterError && !activityUnavailable && } diff --git a/apps/web/src/styles/query-context.css b/apps/web/src/styles/query-context.css new file mode 100644 index 0000000..b107bb3 --- /dev/null +++ b/apps/web/src/styles/query-context.css @@ -0,0 +1,7 @@ +.query-summary{display:flex;justify-content:space-between;align-items:end;gap:24px;padding-top:20px} +.query-summary h2{margin:5px 0 4px;font:400 27px Georgia,serif;letter-spacing:-.035em} +.query-summary p{margin:0;color:var(--text-muted);font-size:13px} +.query-summary__filters{display:flex;align-items:center;justify-content:flex-end;flex-wrap:wrap;gap:8px} +.query-summary__filters span{padding:6px 10px;border:1px solid #c8d3cb;border-radius:20px;background:#f8faf5;color:#526662;font-size:12px} +.query-summary__filters a{margin-left:4px;color:#4d625e;font-size:13px;text-underline-offset:3px} +@media(max-width:720px){.query-summary{display:block;padding:16px 14px 0;width:100%}.query-summary h2{font-size:23px}.query-summary__filters{justify-content:flex-start;flex-wrap:nowrap;overflow-x:auto;padding-top:11px;scroll-snap-type:x proximity;scrollbar-width:none}.query-summary__filters::-webkit-scrollbar{display:none}.query-summary__filters span{flex:0 0 auto;scroll-snap-align:start}.query-summary__filters a{position:sticky;right:0;flex:0 0 auto;padding:6px 10px;background:var(--paper)}} diff --git a/apps/web/tests/query-journey.spec.ts b/apps/web/tests/query-journey.spec.ts new file mode 100644 index 0000000..597663f --- /dev/null +++ b/apps/web/tests/query-journey.spec.ts @@ -0,0 +1,26 @@ +import { expect, test } from "@playwright/test"; + +test("home makes the selected query context explicit and shareable", async ({ page }) => { + await page.setViewportSize({ width: 390, height: 844 }); + await page.goto("/?waterbody=kuori&fish=lake-trout&hours=72&sort=freshness"); + + await expect(page.getByRole("heading", { name: "Озёрная форель · Куори" })).toBeVisible(); + await expect(page.getByText("72 часа · Сначала свежие")).toBeVisible(); + await expect(page.getByRole("link", { name: "Сбросить запрос" })).toHaveAttribute("href", "/#results"); + await expect(page.getByLabel("Водоём")).toHaveValue("kuori"); + await expect(page.getByLabel("Рыба")).toHaveValue("lake-trout"); + await expect(page.getByLabel("Период")).toHaveValue("72"); + await expect(page.getByLabel("Сначала")).toHaveValue("freshness"); + + const overflow = await page.evaluate(() => document.documentElement.scrollWidth - document.documentElement.clientWidth); + expect(overflow).toBeLessThanOrEqual(0); +}); + +test("empty filtered result offers a clear way back to the full dataset", async ({ page }) => { + await page.goto("/?waterbody=kuori&fish=lake-trout&hours=6&sort=freshness"); + + const emptyState = page.getByRole("heading", { name: "Пока нет свежих данных" }); + if (await emptyState.isVisible()) { + await expect(page.getByRole("link", { name: "Показать все данные" })).toHaveAttribute("href", "/#results"); + } +}); diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index f309f66..3ee649b 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.** Зафиксировать task-based навигацию, смысловой порядок ответа, словарь статусов (`fresh`, `stale`, `insufficient_data`, `blocked`, `temporary_error`) и единый scorecard: time-to-first-useful-answer, filter-to-result steps, completion rate, overflow, axe и keyboard regression. Сверить каждый публичный маршрут с контрактом; не добавлять фичи, которые не улучшают решение игрока. -- [ ] **U02 · Главный сценарий «рыба → водоём → точка → снасть».** Перестроить home/filter journey так, чтобы один URL сохранял рыбу, водоём, период, сортировку и режим list/map; показать 3–5 вариантов с понятным CTA и сохранить текущие empty/error/partial states. Acceptance: первый полезный вариант виден без регистрации, back/refresh сохраняют контекст, mobile не теряет фильтры. +- [ ] **U02 · Главный сценарий «рыба → водоём → точка → снасть».** Главная уже сохраняет рыбу, водоём, период и сортировку в shareable URL и после поиска явно показывает контекст запроса, включая мобильный режим; empty-state предлагает вернуться к полному набору данных. Осталось показать 3–5 вариантов с понятным CTA и добавить режим map. Acceptance: первый полезный вариант виден без регистрации, back/refresh сохраняют контекст, mobile не теряет фильтры. - [ ] **U03 · Evidence/trust card.** Для activity-карточек добавлен первый общий evidence-контракт: freshness, confidence, число наблюдений, независимые игроки, `coordinate_precision`, source badges и явный статус `Неполные данные` при малой выборке; targeted E2E проверяет эти поля. Осталось протянуть тот же контракт на spot, waterbody и tackle, добавить период выборки, completeness, конфликт источников и правило минимальной выборки для любых рекомендаций. Нельзя показывать «лучшую точку» или AI-like рекомендацию без объяснения и минимальной выборки. -- [ ] **U04 · List/map и progressive disclosure.** Добавить единый контракт list/map: одинаковые фильтры, сортировка, URL-состояние и карточки; на mobile список остаётся базовым режимом, карта открывается отдельным действием. Не перегружать первый экран: вторичные raw/provenance-поля раскрываются по запросу, но не исчезают. +- [ ] **U04 · List/map и progressive disclosure.** List остаётся честным базовым режимом: фильтры, сортировка, URL-состояние и evidence-карточки уже работают без имитации координатной карты. Следующий шаг — единый list/map-контракт после подтверждения геометрии; на mobile карта должна открываться отдельным действием. Вторичные raw/provenance-поля не исчезают и раскрываются по запросу. - [ ] **U05 · Mobile-first и сохранённый план рыбалки.** Добавить лёгкий сценарий сравнения и сохранения 3–5 вариантов без обязательного аккаунта: рыба, водоём, координаты, метод, наживка, свежесть, доверие и риск. Acceptance: shareable URL/print-карточка, 320/390 px, восстановление после reload, удаление локального плана без внешнего сервиса. - [ ] **U06 · Контентная и визуальная иерархия.** Сократить конкурирующие цифры на карточках, привести badges/source labels к одной семантике, сделать «что делать» главным текстом, а provenance — вторичным раскрываемым слоем. Провести ручной review на 5 ключевых маршрутах и проверить, что цвет не является единственным носителем статуса. - [ ] **U07 · UX-приёмка и измерения.** Добавить Playwright-контракты для полного journey, list/map, evidence states, saved plan, stale/blocked/insufficient data и share/print. Зафиксировать baseline Lighthouse/axe/CLS/INP и time-to-first-useful-answer; закрывать U01–U06 только по evidence, screenshots и обратной связи пилота.