42 lines
4.3 KiB
Plaintext
42 lines
4.3 KiB
Plaintext
---
|
|
import AtlasBreadcrumbs from "../../../components/AtlasBreadcrumbs.astro";
|
|
import DataPassport from "../../../components/DataPassport.astro";
|
|
import Layout from "../../../layouts/Layout.astro";
|
|
import PageHero from "../../../components/PageHero.astro";
|
|
import StatePanel from "../../../components/StatePanel.astro";
|
|
import TackleGlyph from "../../../components/TackleGlyph.astro";
|
|
import { ApiError, api, type TackleItem } from "../../../lib/api";
|
|
import { safeHttpUrl } from "../../../lib/urls";
|
|
|
|
const { id } = Astro.params;
|
|
let item: TackleItem | undefined;
|
|
let unavailable = false;
|
|
try {
|
|
item = await api<TackleItem>(`/api/v1/tackle/items/${encodeURIComponent(id ?? "")}`);
|
|
} catch (error) {
|
|
unavailable = !(error instanceof ApiError) || error.status >= 500;
|
|
}
|
|
if (!item && !unavailable) Astro.response.status = 404;
|
|
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 missingLabels: Record<string, string> = { subcategory: "подкатегория", brand: "бренд", family: "семейство", unlock_level: "уровень открытия", source_url: "ссылка на источник", source_checked_at: "дата проверки" };
|
|
const missing = item?.missing_fields.map((field) => missingLabels[field] ?? field) ?? [];
|
|
const sourceHref = safeHttpUrl(item?.source_url);
|
|
---
|
|
<Layout title={item ? `${item.name} — снасти RF4` : "Снасть не найдена — RF4 Spotter"} description={item ? `Подтверждённая карточка ${item.name} в каталоге снастей RF4 с источником и отметками полноты.` : "Такой карточки нет в каталоге снастей RF4."} noindex={!item || unavailable} errorPage={!item || unavailable}>
|
|
<AtlasBreadcrumbs items={[{ label: "Снасти", href: "/tackle" }, { label: item?.name ?? "Не найдено" }]} />
|
|
<PageHero eyebrow={item ? categoryLabels[item.category] ?? "Каталог RF4" : "Каталог RF4"} title={item?.name ?? (unavailable ? "Каталог недоступен" : "Снасть не найдена")} description={item ? "Канонические характеристики и происхождение без догадок." : undefined} variant={item ? "fish" : undefined} identity={item?.name} />
|
|
{unavailable ? <StatePanel tone="unavailable" title="Карточка временно недоступна" description="Каталог не ответил. Непроверенные характеристики не показываем." /> : !item ? <StatePanel tone="error" title="Такой карточки нет в каталоге" actionHref="/tackle" actionLabel="Открыть каталог" /> : <>
|
|
<section class="tackle-detail content-grid" aria-label="Характеристики снасти">
|
|
<div class="tackle-detail__identity"><TackleGlyph name={item.name} size={72} /><span class="overline">{categoryLabels[item.category] ?? item.category}</span><h2>{item.name}</h2><p>{[item.brand, item.family].filter(Boolean).join(" · ") || "Бренд и семейство не указаны."}</p></div>
|
|
<div><dl><div><dt>Подкатегория</dt><dd>{item.subcategory ?? "Не указана"}</dd></div><div><dt>Уровень открытия</dt><dd>{item.unlock_level ?? "Не указан"}</dd></div><div><dt>Источник</dt><dd>{item.source_system ?? "Не указан"}</dd></div></dl>{missing.length > 0 && <p class="tackle-detail__missing"><strong>Не хватает:</strong> {missing.join(", ")}.</p>}{sourceHref && <a data-action="secondary" href={sourceHref} rel="noreferrer">Открыть первоисточник</a>}</div>
|
|
</section>
|
|
<DataPassport sources={item.source_system ? [item.source_system] : []} sourceUrl={item.source_url} observedAt={item.source_checked_at} completeness={missing.length ? null : 100} status={missing.length ? "incomplete" : item.source_checked_at ? "verified" : "unverified"} />
|
|
</>}
|
|
</Layout>
|