feat: add fishing iconography and float gauge

This commit is contained in:
ik
2026-09-06 13:37:03 +07:00
parent 366ff83a60
commit b45ba38a00
7 changed files with 40 additions and 6 deletions
+3 -2
View File
@@ -1,12 +1,13 @@
---
import type { Activity } from "../lib/api";
import { activityLevel, ago, kg, plural } from "../lib/api";
import FishingIcon from "./FishingIcon.astro";
const { item } = Astro.props as { item: Activity };
const level = activityLevel(item.activity_score);
const limited = item.catches < 3;
---
<a class="spot-card" data-testid={`spot-${item.x}-${item.y}`} href={`/spots/${item.spot_id}`}>
<span class="spot-rank">{String(item.activity_score).padStart(2,"0")}</span>
<div class="spot-main"><div class="spot-topline"><span>{item.waterbody}</span><span class="activity-pill" data-activity-level={level.short}><i></i>{level.short}</span>{limited && <span class="data-quality">Данных мало</span>}</div><h3>{item.fish}</h3><div class="spot-meta"><span>⌖ {item.x}:{item.y}</span><span>◷ {ago(item.last_confirmed_at)}</span></div><p class="data-note">{item.explanation}</p><div class="bait-line"><span class="lure-dot"></span><div><span>Работает сейчас</span><strong>{item.best_bait ?? "не указана"}</strong></div></div></div>
<div class="spot-stats"><div><strong>{item.catches}</strong><span>{plural(item.catches, ["улов", "улова", "уловов"])}</span></div><div><strong>{item.unique_players}</strong><span>{plural(item.unique_players, ["игрок", "игрока", "игроков"])}</span></div><div><strong>{kg(item.average_weight_g)}</strong><span>средний вес</span></div><div><strong>{item.confidence_score}%</strong><span>уверенность</span></div></div><span class="card-arrow"></span>
<div class="spot-main"><div class="spot-topline"><span>{item.waterbody}</span><span class="activity-pill" data-activity-level={level.short}><i></i>{level.short}</span>{limited && <span class="data-quality">Данных мало</span>}</div><h3>{item.fish}</h3><div class="spot-meta"><span><FishingIcon name="pin" size={14}/> {item.x}:{item.y}</span><span><FishingIcon name="clock" size={14}/> {ago(item.last_confirmed_at)}</span></div><p class="data-note">{item.explanation}</p><div class="bait-line"><FishingIcon name="lure" size={25}/><div><span>Работает сейчас</span><strong>{item.best_bait ?? "не указана"}</strong></div></div></div>
<div class="spot-stats"><div><strong>{item.catches}</strong><span>{plural(item.catches, ["улов", "улова", "уловов"])}</span></div><div><strong>{item.unique_players}</strong><span>{plural(item.unique_players, ["игрок", "игрока", "игроков"])}</span></div><div><strong>{kg(item.average_weight_g)}</strong><span>средний вес</span></div><div><strong>{item.confidence_score}%</strong><span>уверенность</span></div></div><span class="card-arrow"><FishingIcon name="arrow" size={22}/></span>
</a>
+17
View File
@@ -0,0 +1,17 @@
---
type IconName = "hook" | "float" | "trophy" | "plus" | "pin" | "clock" | "ripple" | "angler" | "scale" | "lure" | "arrow";
const { name, size = 18, label } = Astro.props as { name: IconName; size?: number; label?: string };
---
<svg class={`fish-icon fish-icon--${name}`} width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden={label ? undefined : "true"} role={label ? "img" : undefined} aria-label={label}>
{name === "hook" && <><path d="M15 3v11a6 6 0 1 1-6-6"/><path d="m6 9 3-1 1 3"/><path d="M15 3h3"/></>}
{name === "float" && <><path d="M12 2v3M12 19v3"/><path d="M12 5c2.2 0 3 2.8 3 6s-.8 8-3 8-3-4.8-3-8 .8-6 3-6Z"/><path d="M9.2 11h5.6"/></>}
{name === "trophy" && <><path d="M8 4h8v4a4 4 0 0 1-8 0V4Z"/><path d="M8 6H5c0 3 1.5 5 4.5 5M16 6h3c0 3-1.5 5-4.5 5M12 12v5M8 20h8M9 17h6"/></>}
{name === "plus" && <><path d="M12 5v14M5 12h14"/><circle cx="12" cy="12" r="9"/></>}
{name === "pin" && <><path d="M12 21s6-6.1 6-12a6 6 0 1 0-12 0c0 5.9 6 12 6 12Z"/><circle cx="12" cy="9" r="2"/></>}
{name === "clock" && <><circle cx="12" cy="12" r="9"/><path d="M12 7v5l3 2"/></>}
{name === "ripple" && <><circle cx="12" cy="12" r="2"/><path d="M6.6 8.4A6.5 6.5 0 0 0 5.5 12c0 1.3.4 2.5 1.1 3.6M17.4 8.4a6.5 6.5 0 0 1 1.1 3.6c0 1.3-.4 2.5-1.1 3.6M3.5 5.7A10 10 0 0 0 2 12c0 2.4.8 4.6 2.1 6.3M20.5 5.7A10 10 0 0 1 22 12c0 2.4-.8 4.6-2.1 6.3"/></>}
{name === "angler" && <><circle cx="9" cy="6" r="2"/><path d="m8 9-2 5 4 2 2 5M8 12l5 2M13 14l5-8M17 6c2 3 3 6 3 10"/></>}
{name === "scale" && <><path d="M12 3v18M7 6h10M6 6l-3 6h6L6 6ZM18 6l-3 6h6l-3-6ZM8 21h8"/></>}
{name === "lure" && <><path d="M9 4c4 1 7 5 6 10s-5 7-8 4 1-7 2-14Z"/><path d="m14 8 4-2M8 18c0 3 4 3 4 0"/></>}
{name === "arrow" && <><path d="M5 12h14M14 7l5 5-5 5"/></>}
</svg>
+4 -3
View File
@@ -1,5 +1,6 @@
---
import "../styles/global.css";
import FishingIcon from "../components/FishingIcon.astro";
const { title = "RF4 Spotter" } = Astro.props;
const path = Astro.url.pathname;
---
@@ -8,11 +9,11 @@ const path = Astro.url.pathname;
<head><meta charset="UTF-8" /><meta name="viewport" content="width=device-width" /><meta name="description" content="Свежие точки и статистика клёва Russian Fishing 4" /><link rel="icon" href="/favicon.svg" /><title>{title}</title></head>
<body>
<header class="topbar">
<a href="/" class="brand" aria-label="Ни хвоста, ни чешуи"><span class="brand-mark" aria-hidden="true"></span><span class="brand-name"><span>Ни хвоста,</span><strong>ни чешуи</strong></span></a>
<nav aria-label="Разделы сайта"><a class:list={{active:path === "/"}} href="/"> <span>Сейчас клюёт</span></a><a class:list={{active:path.startsWith("/records")}} href="/records"> <span>Рекорды</span></a><a class:list={{active:path.startsWith("/report")}} href="/report"> <span>Добавить улов</span></a></nav>
<a href="/" class="brand" aria-label="Ни хвоста, ни чешуи"><span class="brand-mark" aria-hidden="true"><FishingIcon name="hook" size={24}/></span><span class="brand-name"><span>Ни хвоста,</span><strong>ни чешуи</strong></span></a>
<nav aria-label="Разделы сайта"><a class:list={{active:path === "/"}} href="/"><FishingIcon name="float"/> <span>Сейчас клюёт</span></a><a class:list={{active:path.startsWith("/records")}} href="/records"><FishingIcon name="trophy"/> <span>Рекорды</span></a><a class:list={{active:path.startsWith("/report")}} href="/report"><FishingIcon name="plus"/> <span>Добавить улов</span></a></nav>
<p class="live-badge"><span></span> Свежие данные и честная оценка</p>
</header>
<main><slot /></main>
<footer><a href="/" class="brand"><span class="brand-mark"></span><span class="brand-name"><span>Ни хвоста,</span><strong>ни чешуи</strong></span></a><p>Неофициальный проект для игроков Russian Fishing 4. Данные seed демонстрационные.</p><span>НХНЧ · 2026</span></footer>
<footer><a href="/" class="brand"><span class="brand-mark"><FishingIcon name="hook" size={24}/></span><span class="brand-name"><span>Ни хвоста,</span><strong>ни чешуи</strong></span></a><p>Неофициальный проект для игроков Russian Fishing 4. Данные seed демонстрационные.</p><span>НХНЧ · 2026</span></footer>
</body>
</html>
+2 -1
View File
@@ -1,6 +1,7 @@
---
import Layout from "../layouts/Layout.astro";
import ActivityCard from "../components/ActivityCard.astro";
import FishingIcon from "../components/FishingIcon.astro";
import { activityLevel, ago, api, kg, plural, type Activity, type DictionaryItem } from "../lib/api";
const params = Astro.url.searchParams;
@@ -30,7 +31,7 @@ const leaderLevel = items[0] ? activityLevel(items[0].activity_score) : null;
<button>⌕ Найти клёв</button>
</form></section>
<section class="dashboard content-grid"><div class="results-column"><div class="section-heading"><div><span class="overline">За выбранный период</span><h2>Горячие точки</h2></div><span class="result-count">{items.length} {plural(items.length, ["точка", "точки", "точек"])}</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> : <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="Открыть точку"></a></div><div class="detail-score"><div class="score-ring" style={`--score:${items[0].activity_score * 3.6}deg`}><div><strong>{items[0].activity_score}</strong><span>из 100</span></div></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></span><small>Уверенность</small><strong>{items[0].confidence_score}%</strong></div><div><span></span><small>{plural(items[0].unique_players, ["Игрок", "Игрока", "Игроков"])}</small><strong>{items[0].unique_players}</strong></div><div><span></span><small>Последний</small><strong>{ago(items[0].last_confirmed_at)}</strong></div><div><span></span><small>Средний вес</small><strong>{kg(items[0].average_weight_g)}</strong></div></div><div class="best-lure"><span class="overline">Лучшая связка</span><div><i></i><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="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 class="how-it-works content-grid"><div><span class="overline">Как читать данные</span><h2>Не обещаем рыбу.<br/>Показываем факты.</h2></div><div class="principles"><article><span>01</span><h3>Свежесть</h3><p>Чем старше сообщение, тем меньше оно влияет на активность.</p></article><article><span>02</span><h3>Разные игроки</h3><p>Десять уловов одного человека не равны десяти подтверждениям.</p></article><article><span>03</span><h3>Уверенность</h3><p>Каждая оценка объясняет, сколько данных за ней стоит.</p></article></div></section>
</Layout>
+6
View File
@@ -15,3 +15,9 @@ footer{min-height:118px;background:var(--deep);color:#dbe4df;padding:28px max(32
@media(max-width:720px){.moderation-app{width:calc(100% - 28px)}.moderation-card{grid-template-columns:1fr}.moderation-proof{grid-row:2}.moderation-actions{grid-column:1;display:block}.moderation-actions>div{margin-top:12px}.admin-login{display:block}.admin-login button{width:100%;margin-top:12px}}
@media(max-width:480px){.filters{grid-template-columns:1fr}.spot-stats strong{font-size:16px}.topbar nav{gap:4px}.topbar nav a{padding:0 5px}.brand-name{font-size:14px}.brand-name strong{font-size:17px}.moderation-actions>div{display:grid}.moderation-actions button{width:100%}}
@media(prefers-reduced-motion:reduce){*{scroll-behavior:auto!important;transition-duration:.01ms!important}}
/* RF4 field-guide iconography and float activity gauge */
.fish-icon{display:inline-block;flex:none;vertical-align:-.2em}.brand-mark .fish-icon{transform:rotate(-12deg)}.topbar nav a.active .fish-icon{color:#6b9229}.spot-meta span{display:flex;align-items:center;gap:4px}.bait-line>.fish-icon,.best-lure .fish-icon{color:#e19a43;transform:rotate(14deg)}.card-arrow{font-size:0}.detail-head>a{transition:background .2s ease,color .2s ease}.detail-head>a:hover{background:var(--lime);color:var(--deep)}
.float-gauge{width:122px;height:142px;position:relative;display:flex;flex-direction:column;align-items:center;justify-content:flex-end;padding-bottom:8px;isolation:isolate}.float-gauge:before,.float-gauge:after{content:"";position:absolute;left:3px;right:3px;border:1px solid #ffffff1d;border-radius:50%}.float-gauge:before{height:28px;bottom:25px;box-shadow:0 0 0 10px #ffffff08}.float-gauge:after{height:15px;bottom:32px}.float-gauge__line{position:absolute;left:50%;top:0;width:1px;height:105px;background:linear-gradient(#ffffff55,#ffffff12);z-index:-1}.float-gauge__water{position:absolute;left:11px;right:11px;bottom:39px;height:calc(var(--level)*.62);max-height:62px;background:linear-gradient(180deg,#c9f45b08,#c9f45b36);border-top:1px solid var(--lime);clip-path:polygon(0 6px,18% 0,36% 6px,54% 0,72% 6px,90% 0,100% 4px,100% 100%,0 100%)}.float-gauge__bob{position:absolute;left:50%;bottom:calc(39px + var(--level)*.62);width:18px;height:54px;transform:translate(-50%,50%);border:1px solid #e6f8ad;border-radius:50% 50% 42% 42%;background:linear-gradient(180deg,var(--lime) 0 35%,#f4f7ed 36% 58%,#e47e3b 59%);box-shadow:0 5px 18px #c9f45b38}.float-gauge__bob i{position:absolute;left:50%;top:-24px;width:1px;height:25px;background:#dfe9dc}.float-gauge strong{position:relative;z-index:2;font:400 31px/1 Georgia,serif;text-shadow:0 2px 8px var(--deep)}.float-gauge small{position:relative;z-index:2;color:#a9b8b5;font-size:9px;text-transform:uppercase}.best-lure>div{grid-template-columns:25px 1fr auto}
@media(max-width:720px){.float-gauge{width:100px}}
@media(prefers-reduced-motion:no-preference){.pulse-orb span{animation:ripple-pulse 2.4s ease-out infinite}.float-gauge__bob{animation:bob 2.8s ease-in-out infinite}}@keyframes ripple-pulse{50%{box-shadow:0 0 0 15px #c9f45b08}}@keyframes bob{50%{translate:0 -3px}}
+1
View File
@@ -71,6 +71,7 @@
- [ ] Провести Lighthouse-проверку основных страниц и устранить критические проблемы производительности.
- [x] Провести UI/UX-аудит desktop/mobile и сформировать приоритетный план (`docs/UI_UX_AUDIT.md`).
- [x] Исправить единую шкалу активности и русские числительные во всех публичных представлениях (пакет A UI/UX-аудита; unit-тест границ и E2E согласованности).
- [x] Дополнить визуальный язык авторскими рыболовными SVG-иконками и шкалой активности в форме поплавка; анимации учитывают `prefers-reduced-motion`.
- [ ] Сократить мобильный путь до результатов и улучшить фильтры (пакет B UI/UX-аудита).
- [ ] Упростить форму улова, добавить помощь форматов и сохранение значений при ошибке (пакет C UI/UX-аудита).
- [ ] Заменить slug-фильтры и улучшить мобильное представление рекордов/detail (пакет D UI/UX-аудита).
+7
View File
@@ -34,6 +34,13 @@
## План работ
### Визуальный язык — выполнено
- [x] Добавлен единый набор тонких рыболовных SVG-иконок без внешней библиотеки.
- [x] Unicode-символы навигации, метрик, координат, времени и приманки заменены тематическими пиктограммами.
- [x] Кольцевая диаграмма лидера заменена авторской шкалой-поплавком: высота поплавка и воды связана с индексом активности.
- [x] Добавлена деликатная рябь и покачивание с учётом `prefers-reduced-motion`.
### Пакет A — достоверность интерфейса
- [x] Вынести шкалу активности и пользовательские формулировки в общий helper.