feat: add tackle catalog and recommendation analytics

This commit is contained in:
ik
2026-09-20 18:26:53 +07:00
parent 4f0c2d23de
commit e94d247096
11 changed files with 350 additions and 11 deletions
+40
View File
@@ -0,0 +1,40 @@
---
import Layout from "../../layouts/Layout.astro";
import PageHero from "../../components/PageHero.astro";
import StatePanel from "../../components/StatePanel.astro";
import Pagination from "../../components/Pagination.astro";
import TackleGlyph from "../../components/TackleGlyph.astro";
import { api, type PaginatedTackleItems } from "../../lib/api";
const params = Astro.url.searchParams;
const category = params.get("category") ?? "";
const brand = params.get("brand") ?? "";
const family = params.get("family") ?? "";
const requestedOffset = Number(params.get("offset") ?? 0);
const limit = 48;
const offset = Number.isInteger(requestedOffset) && requestedOffset >= 0 ? requestedOffset : 0;
const query = new URLSearchParams({ limit: String(limit), offset: String(offset) });
if (category) query.set("category", category);
if (brand) query.set("brand", brand);
if (family) query.set("family", family);
let result: PaginatedTackleItems = { items: [], total: 0, limit, offset }, unavailable = false;
try { result = await api<PaginatedTackleItems>(`/api/v1/tackle/items?${query}`); } 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 categoryLabels: Record<string, string> = { bait: "Наживка", lure: "Приманка", rod: "Удилище", reel: "Катушка", line: "Леска", hook: "Крючок", rig: "Монтаж", float: "Поплавок", sinker: "Груз", other: "Другое" };
const filterParams = new URLSearchParams();
if (category) filterParams.set("category", category);
if (brand) filterParams.set("brand", brand);
if (family) filterParams.set("family", family);
---
<Layout title="Снасти и приманки RF4 — RF4 Spotter" description="Канонический каталог снастей, приманок и монтажей Russian Fishing 4 с источниками и отметками неполноты.">
<PageHero eyebrow="Канонический справочник" title="Снасти и приманки" description="Показываем только подтверждённые карточки. Пустые поля явно отмечены и не заменяются догадками." variant="fish" count={result.total} />
<form class="record-filters" method="get" aria-label="Фильтры каталога снастей">
<label>Категория<select name="category"><option value="">Все категории</option>{Object.entries(categoryLabels).map(([value, label]) => <option value={value} selected={category === value}>{label}</option>)}</select></label>
<label>Бренд<input name="brand" value={brand} maxlength="100" placeholder="Например, RF4" /></label>
<label>Семейство<input name="family" value={family} maxlength="100" placeholder="Название семейства" /></label>
<button data-action="primary">Фильтровать</button>
{(category || brand || family) && <a data-action="quiet" href="/tackle">Сбросить</a>}
</form>
{unavailable ? <StatePanel tone="unavailable" title="Каталог временно недоступен" description="Не показываем непроверенные карточки. Попробуйте обновить страницу позже." /> : result.items.length ? <section class="catalog-grid content-grid" aria-label="Карточки снастей">{result.items.map(item => <a href={`/tackle/items/${item.id}`}><TackleGlyph name={item.name} size={32} /><span>{categoryLabels[item.category] ?? item.category}</span><strong>{item.name}</strong><small>{[item.brand, item.family].filter(Boolean).join(" · ") || "Характеристики уточняются"}</small></a>)}</section> : <StatePanel title="Подтверждённых карточек пока нет" description="Каталог заполнится после разрешённой загрузки и ручной проверки источников." />}
{!unavailable && <Pagination path="/tackle" params={filterParams} total={result.total} limit={limit} offset={offset} itemLabel="карточек" />}
</Layout>