54 lines
5.8 KiB
Plaintext
54 lines
5.8 KiB
Plaintext
---
|
|
import ActivityCard from "../../components/ActivityCard.astro";
|
|
import ActivityEvidenceSummary from "../../components/ActivityEvidenceSummary.astro";
|
|
import ActivityPeriodFilter from "../../components/ActivityPeriodFilter.astro";
|
|
import EntityMedia from "../../components/EntityMedia.astro";
|
|
import AtlasBreadcrumbs from "../../components/AtlasBreadcrumbs.astro";
|
|
import AtlasEntityLink from "../../components/AtlasEntityLink.astro";
|
|
import PageHero from "../../components/PageHero.astro";
|
|
import StatePanel from "../../components/StatePanel.astro";
|
|
import Layout from "../../layouts/Layout.astro";
|
|
import Pagination from "../../components/Pagination.astro";
|
|
import { api, plural, type Activity, type DictionaryItem, type MediaAsset, type PaginatedActivity } from "../../lib/api";
|
|
import { findMediaByLabel } from "../../lib/media";
|
|
const { slug } = Astro.params;
|
|
const requestedHours = Astro.url.searchParams.get("hours") ?? "72";
|
|
const hours = ["24", "72", "168"].includes(requestedHours) ? requestedHours : "72";
|
|
const periodLabel = { "24": "24 часа", "72": "72 часа", "168": "7 дней" }[hours];
|
|
const requestedOffset = Number(Astro.url.searchParams.get("offset") ?? 0);
|
|
let offset = Number.isInteger(requestedOffset) && requestedOffset >= 0 ? requestedOffset : 0;
|
|
const activityLimit = 24;
|
|
let fish: DictionaryItem | undefined, items: Activity[] = [], media: MediaAsset[] = [], totalItems = 0, unavailable = false;
|
|
try {
|
|
const [fishes, fishMedia] = await Promise.all([api<DictionaryItem[]>("/api/v1/fishes?limit=500"), api<MediaAsset[]>("/api/v1/media/catalog?entity_type=fish")]);
|
|
media = fishMedia;
|
|
fish = fishes.find(item => item.slug === slug);
|
|
if (fish) {
|
|
let paginated = await api<PaginatedActivity>(`/api/v1/activity?hours=${hours}&fish=${encodeURIComponent(fish.slug)}&limit=${activityLimit}&offset=${offset}`);
|
|
if (paginated.total > 0 && offset >= paginated.total) {
|
|
offset = Math.floor((paginated.total - 1) / activityLimit) * activityLimit;
|
|
paginated = await api<PaginatedActivity>(`/api/v1/activity?hours=${hours}&fish=${encodeURIComponent(fish.slug)}&limit=${activityLimit}&offset=${offset}`);
|
|
}
|
|
items = paginated.items; totalItems = paginated.total;
|
|
}
|
|
} catch { unavailable = true; }
|
|
if (unavailable) {
|
|
Astro.response.status = 503;
|
|
Astro.response.headers.set("Retry-After", "60");
|
|
Astro.response.headers.set("Cache-Control", "no-store");
|
|
}
|
|
if (!fish && !unavailable) Astro.response.status = 404;
|
|
const waters = [...new Map(items.map(item => [item.waterbody_slug, item.waterbody])).entries()];
|
|
const image = fish ? findMediaByLabel(media, fish.name_ru) : undefined;
|
|
const schema = fish ? { "@context":"https://schema.org", "@type":"CollectionPage", name:`Где ловить ${fish.name_ru} в RF4`, url:`https://rf4spotter.ru/fish/${fish.slug}` } : null;
|
|
---
|
|
<Layout title={fish ? `Где ловить ${fish.name_ru} в RF4 — свежие точки` : "Рыба не найдена — RF4 Spotter"} description={fish ? `Свежие точки ловли ${fish.name_ru} в Russian Fishing 4: водоёмы, координаты, приманки, активность и источники наблюдений.` : "Такого вида рыбы нет в каталоге RF4 Spotter."} noindex={!fish || unavailable} structuredData={schema} errorPage={!fish || unavailable}>
|
|
<AtlasBreadcrumbs items={[{ label: "Рыбы", href: "/fish" }, { label: fish?.name_ru ?? "Не найдено" }]} />
|
|
<PageHero eyebrow={`Свежие данные за ${periodLabel}`} title={fish?.name_ru ?? "Рыба не найдена"} description={fish ? `${totalItems} ${plural(totalItems,["активная точка","активные точки","активных точек"])} на ${waters.length} ${plural(waters.length,["водоёме","водоёмах","водоёмах"])}.` : undefined} variant={fish ? "fish" : undefined} identity={fish?.name_ru} />
|
|
{fish && <ActivityPeriodFilter hours={hours} />}
|
|
{image && <section class="entity-feature content-grid" aria-label={`Изображение: ${fish!.name_ru}`}><EntityMedia asset={image} sourceLink /><div><span class="overline">Визуальный справочник</span><h2>{fish!.name_ru}</h2><p>Изображение опубликовано с прямой ссылкой на источник. Оно помогает отличить вид, а актуальные точки ниже остаются отдельными наблюдениями игроков.</p></div></section>}
|
|
{fish && <ActivityEvidenceSummary items={items} totalPoints={totalItems} periodLabel={periodLabel} />}
|
|
{unavailable ? <StatePanel tone="unavailable" title="Данные временно недоступны" description="Каталог сохранён, но свежие наблюдения сейчас не получены." /> : !fish ? <StatePanel tone="error" title="Такой рыбы нет в справочнике" actionHref="/fish" actionLabel="Открыть каталог" /> : <section class="catalog-results content-grid"><aside><span class="overline">Водоёмы</span>{waters.length ? <nav>{waters.map(([waterSlug,name]) => <AtlasEntityLink href={`/waterbodies/${waterSlug}/${fish!.slug}?hours=${hours}`} label={name} kind="water" identity={waterSlug} />)}</nav> : <p>Свежих подтверждённых водоёмов пока нет.</p>}</aside><div>{items.length ? items.map(item => <ActivityCard item={item} periodLabel={periodLabel}/>) : <StatePanel contained={false} title="Свежих точек пока нет" description="Проверьте позже или посмотрите полевые сигналы на главной." />}</div></section>}
|
|
{fish && <Pagination path={`/fish/${fish.slug}`} params={new URLSearchParams({ hours })} total={totalItems} limit={activityLimit} offset={offset} itemLabel="точек" />}
|
|
</Layout>
|