44 lines
5.4 KiB
Plaintext
44 lines
5.4 KiB
Plaintext
---
|
|
import Layout from "../../layouts/Layout.astro";
|
|
import PageHero from "../../components/PageHero.astro";
|
|
import StatePanel from "../../components/StatePanel.astro";
|
|
import { ago, api, type DictionaryItem, type TackleCombination } from "../../lib/api";
|
|
|
|
const params = Astro.url.searchParams;
|
|
const waterbody = params.get("waterbody") ?? "";
|
|
const fish = params.get("fish") ?? "";
|
|
const method = params.get("method") ?? "";
|
|
const role = params.get("role") ?? "";
|
|
const hours = [24, 72, 168].includes(Number(params.get("hours"))) ? Number(params.get("hours")) : 72;
|
|
const query = new URLSearchParams({ hours: String(hours), min_samples: "3", min_players: "2", limit: "100" });
|
|
if (waterbody) query.set("waterbody", waterbody);
|
|
if (fish) query.set("fish", fish);
|
|
if (method) query.set("method", method);
|
|
if (role) query.set("role", role);
|
|
let combinations: TackleCombination[] = [], waters: DictionaryItem[] = [], fishes: DictionaryItem[] = [], unavailable = false;
|
|
try {
|
|
[combinations, waters, fishes] = await Promise.all([
|
|
api<TackleCombination[]>(`/api/v1/analytics/tackle?${query}`),
|
|
api<DictionaryItem[]>("/api/v1/waterbodies?limit=500"),
|
|
api<DictionaryItem[]>("/api/v1/fishes?limit=500"),
|
|
]);
|
|
} catch { unavailable = true; }
|
|
if (unavailable) { Astro.response.status = 503; Astro.response.headers.set("Retry-After", "60"); Astro.response.headers.set("Cache-Control", "no-store"); }
|
|
const methodLabels: Record<string, string> = { spinning: "Спиннинг", bottom: "Донная", float: "Поплавочная" };
|
|
const roleLabels: Record<string, string> = { lure: "Приманка", bait: "Наживка", rig: "Сборка", rod: "Удилище", reel: "Катушка", line: "Леска", hook: "Крючок", float: "Поплавок", sinker: "Груз" };
|
|
---
|
|
<Layout title="Сочетания снастей RF4 — RF4 Spotter" description="Подтверждённые сочетания снастей RF4 по одобренным наблюдениям, с выборкой и независимыми игроками." noindex={unavailable} errorPage={unavailable}>
|
|
<PageHero eyebrow="Только одобренные наблюдения" title="Сочетания снастей" description="Факт использования отделён от рекомендации: малой выборке нельзя приписывать эффективность." variant="fish" />
|
|
<form class="record-filters" method="get" aria-label="Фильтры сочетаний снастей">
|
|
<label>Водоём<select name="waterbody"><option value="">Все водоёмы</option>{waters.map(item => <option value={item.slug} selected={waterbody === item.slug}>{item.name_ru}</option>)}</select></label>
|
|
<label>Рыба<select name="fish"><option value="">Все виды</option>{fishes.map(item => <option value={item.slug} selected={fish === item.slug}>{item.name_ru}</option>)}</select></label>
|
|
<label>Метод<select name="method"><option value="">Все методы</option>{Object.entries(methodLabels).map(([value, label]) => <option value={value} selected={method === value}>{label}</option>)}</select></label>
|
|
<label>Роль<select name="role"><option value="">Все роли</option>{Object.entries(roleLabels).map(([value, label]) => <option value={value} selected={role === value}>{label}</option>)}</select></label>
|
|
<label>Период<select name="hours">{[24, 72, 168].map(value => <option value={value} selected={hours === value}>{value} ч</option>)}</select></label>
|
|
<button data-action="primary">Показать</button>
|
|
{(waterbody || fish || method || role || hours !== 72) && <a data-action="quiet" href="/tackle/analytics">Сбросить</a>}
|
|
</form>
|
|
<p class="privacy content-grid">Порог рекомендации: минимум 3 наблюдения от 2 независимых игроков. Это не рейтинг снасти и не гарантия улова.</p>
|
|
{unavailable ? <StatePanel tone="unavailable" title="Аналитика временно недоступна" description="Не показываем непроверенные сочетания. Попробуйте позже." /> : combinations.length ? <section class="signal-grid content-grid" aria-label="Сочетания снастей" data-analytics-results>{combinations.map(item => { const canonicalHref = item.rig_id ? `/tackle/rigs/${item.rig_id}` : item.tackle_item_id ? `/tackle/items/${item.tackle_item_id}` : null; return <article class="signal-card" data-analytics-status={item.status}><div class="signal-card__top"><span class="source-chip"><span>{roleLabels[item.role] ?? item.role}</span></span><span class="quality-chip">{item.status === "recommendation" ? "Рекомендация" : "Недостаточно данных"}</span></div><h2>{canonicalHref ? <a href={canonicalHref}>{item.value}</a> : item.value}</h2><dl><div><dt>Наблюдения</dt><dd>{item.catches}</dd></div><div><dt>Игроки</dt><dd>{item.unique_players}</dd></div><div><dt>Свежесть выборки</dt><dd>{item.freshness_score}%</dd></div><div><dt>Последнее</dt><dd>{ago(item.last_seen_at)}</dd></div></dl><p>{item.explanation}</p></article>})}</section> : <div data-analytics-results><StatePanel title="Подтверждённых сочетаний пока нет" description="Сочетания появятся после новых одобренных наблюдений с указанием компонентов." /></div>}
|
|
</Layout>
|