41 lines
4.2 KiB
Plaintext
41 lines
4.2 KiB
Plaintext
---
|
|
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>
|