feat: paginate waterbody activity detail
This commit is contained in:
@@ -8,6 +8,7 @@ import PageHero from "../../components/PageHero.astro";
|
|||||||
import StatePanel from "../../components/StatePanel.astro";
|
import StatePanel from "../../components/StatePanel.astro";
|
||||||
import DataPassport from "../../components/DataPassport.astro";
|
import DataPassport from "../../components/DataPassport.astro";
|
||||||
import Layout from "../../layouts/Layout.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 { api, plural, type Activity, type DictionaryItem, type MediaAsset, type PaginatedActivity } from "../../lib/api";
|
||||||
import { findMediaByLabel, mediaRoleLabel } from "../../lib/media";
|
import { findMediaByLabel, mediaRoleLabel } from "../../lib/media";
|
||||||
import { safeHttpUrl } from "../../lib/urls";
|
import { safeHttpUrl } from "../../lib/urls";
|
||||||
@@ -15,12 +16,15 @@ const { slug } = Astro.params;
|
|||||||
const requestedHours = Astro.url.searchParams.get("hours") ?? "72";
|
const requestedHours = Astro.url.searchParams.get("hours") ?? "72";
|
||||||
const hours = ["24", "72", "168"].includes(requestedHours) ? requestedHours : "72";
|
const hours = ["24", "72", "168"].includes(requestedHours) ? requestedHours : "72";
|
||||||
const periodLabel = { "24": "24 часа", "72": "72 часа", "168": "7 дней" }[hours];
|
const periodLabel = { "24": "24 часа", "72": "72 часа", "168": "7 дней" }[hours];
|
||||||
let water: DictionaryItem | undefined, items: Activity[] = [], media: MediaAsset[] = [], unavailable = false;
|
const requestedOffset = Number(Astro.url.searchParams.get("offset") ?? 0);
|
||||||
|
const offset = Number.isInteger(requestedOffset) && requestedOffset >= 0 ? requestedOffset : 0;
|
||||||
|
const activityLimit = 24;
|
||||||
|
let water: DictionaryItem | undefined, items: Activity[] = [], media: MediaAsset[] = [], totalItems = 0, unavailable = false;
|
||||||
try {
|
try {
|
||||||
const [waters, waterMedia] = await Promise.all([api<DictionaryItem[]>("/api/v1/waterbodies?limit=500"), api<MediaAsset[]>("/api/v1/media/catalog?entity_type=waterbody")]);
|
const [waters, waterMedia] = await Promise.all([api<DictionaryItem[]>("/api/v1/waterbodies?limit=500"), api<MediaAsset[]>("/api/v1/media/catalog?entity_type=waterbody")]);
|
||||||
media = waterMedia;
|
media = waterMedia;
|
||||||
water = waters.find(item => item.slug === slug);
|
water = waters.find(item => item.slug === slug);
|
||||||
if (water) { const paginated = await api<PaginatedActivity>(`/api/v1/activity?hours=${hours}&waterbody=${encodeURIComponent(water.slug)}&limit=100`); items = paginated.items; }
|
if (water) { const paginated = await api<PaginatedActivity>(`/api/v1/activity?hours=${hours}&waterbody=${encodeURIComponent(water.slug)}&limit=${activityLimit}&offset=${offset}`); items = paginated.items; totalItems = paginated.total; }
|
||||||
} catch { unavailable = true; }
|
} catch { unavailable = true; }
|
||||||
if (unavailable) {
|
if (unavailable) {
|
||||||
Astro.response.status = 503;
|
Astro.response.status = 503;
|
||||||
@@ -36,11 +40,12 @@ const schema = water ? { "@context":"https://schema.org", "@type":"CollectionPag
|
|||||||
---
|
---
|
||||||
<Layout title={water ? `${water.name_ru} в RF4 — рыба и свежие точки` : "Водоём не найден — RF4 Spotter"} description={water ? `${water.name_ru} в Russian Fishing 4: свежие координаты, активные виды рыб, приманки и источники наблюдений.` : "Такого водоёма нет в каталоге RF4 Spotter."} noindex={!water || unavailable} structuredData={schema} errorPage={!water || unavailable}>
|
<Layout title={water ? `${water.name_ru} в RF4 — рыба и свежие точки` : "Водоём не найден — RF4 Spotter"} description={water ? `${water.name_ru} в Russian Fishing 4: свежие координаты, активные виды рыб, приманки и источники наблюдений.` : "Такого водоёма нет в каталоге RF4 Spotter."} noindex={!water || unavailable} structuredData={schema} errorPage={!water || unavailable}>
|
||||||
<AtlasBreadcrumbs items={[{ label: "Водоёмы", href: "/waterbodies" }, { label: water?.name_ru ?? "Не найдено" }]} />
|
<AtlasBreadcrumbs items={[{ label: "Водоёмы", href: "/waterbodies" }, { label: water?.name_ru ?? "Не найдено" }]} />
|
||||||
<PageHero eyebrow={`Свежие данные за ${periodLabel}`} title={water?.name_ru ?? "Водоём не найден"} description={water ? `${items.length} ${plural(items.length,["активная точка","активные точки","активных точек"])} для ${fishes.length} ${plural(fishes.length,["вида рыбы","видов рыб","видов рыб"])}.` : undefined} variant={water ? "water" : undefined} identity={water?.slug} />
|
<PageHero eyebrow={`Свежие данные за ${periodLabel}`} title={water?.name_ru ?? "Водоём не найден"} description={water ? `${totalItems} ${plural(totalItems,["активная точка","активные точки","активных точек"])} для ${fishes.length} ${plural(fishes.length,["вида рыбы","видов рыб","видов рыб"])}.` : undefined} variant={water ? "water" : undefined} identity={water?.slug} />
|
||||||
{water && <ActivityPeriodFilter hours={hours} />}
|
{water && <ActivityPeriodFilter hours={hours} />}
|
||||||
{coverImage && <section class="entity-feature content-grid" aria-label={`Заставка: ${water!.name_ru}`}><EntityMedia asset={coverImage} sourceLink /><div><span class="overline">{mediaRoleLabel("waterbody_cover")}</span><h2>{water!.name_ru}</h2><p>Материал показан с прямой атрибуцией. Координаты активных точек ниже относятся к данным наблюдений, а не к геометрии изображения.</p></div></section>}
|
{coverImage && <section class="entity-feature content-grid" aria-label={`Заставка: ${water!.name_ru}`}><EntityMedia asset={coverImage} sourceLink /><div><span class="overline">{mediaRoleLabel("waterbody_cover")}</span><h2>{water!.name_ru}</h2><p>Материал показан с прямой атрибуцией. Координаты активных точек ниже относятся к данным наблюдений, а не к геометрии изображения.</p></div></section>}
|
||||||
{supportingMedia.length > 0 && <section class="waterbody-media-grid content-grid" aria-label={`Карты: ${water!.name_ru}`}>{supportingMedia.map(asset => <article><span class="overline">{mediaRoleLabel(asset.media_role ?? "")}</span><EntityMedia asset={asset} sourceLink /></article>)}</section>}
|
{supportingMedia.length > 0 && <section class="waterbody-media-grid content-grid" aria-label={`Карты: ${water!.name_ru}`}>{supportingMedia.map(asset => <article><span class="overline">{mediaRoleLabel(asset.media_role ?? "")}</span><EntityMedia asset={asset} sourceLink /></article>)}</section>}
|
||||||
{water && (water.description || water.unlock_level !== undefined || water.source_url || water.source_fish_species) && <section class="data-passport content-grid" aria-label="Паспорт водоёма"><div><span class="overline">Канонические сведения</span><h2>{water.name_ru}</h2>{water.description && <p>{water.description}</p>}{water.unlock_level !== undefined && water.unlock_level !== null && <p>Открывается с уровня: <strong>{water.unlock_level}</strong></p>}{water.source_fish_species?.length && <p>В карточке источника указано видов рыб: <strong>{water.source_fish_species.length}</strong>.</p>}{water.source_aliases?.length && <p>Алиасы источника: {water.source_aliases.join(", ")}.</p>}</div><div><span class="overline">Источник</span>{sourceHref ? <p><a href={sourceHref} rel="noreferrer">{water.source_system ?? "Внешний источник"} · исходная карточка</a></p> : <p>Источник для описания ещё не подтверждён.</p>}{water.source_point_urls?.length && <p>Сохранено ссылок на точки: <strong>{water.source_point_urls.length}</strong>.</p>}{water.source_image_urls?.length && <p>Изображений-кандидатов: <strong>{water.source_image_urls.length}</strong>; публикация требует отдельной проверки.</p>}{water.source_checked_at && <p>Проверено: {new Date(water.source_checked_at).toLocaleDateString("ru-RU")}</p>}</div></section>}
|
{water && (water.description || water.unlock_level !== undefined || water.source_url || water.source_fish_species) && <section class="data-passport content-grid" aria-label="Паспорт водоёма"><div><span class="overline">Канонические сведения</span><h2>{water.name_ru}</h2>{water.description && <p>{water.description}</p>}{water.unlock_level !== undefined && water.unlock_level !== null && <p>Открывается с уровня: <strong>{water.unlock_level}</strong></p>}{water.source_fish_species?.length && <p>В карточке источника указано видов рыб: <strong>{water.source_fish_species.length}</strong>.</p>}{water.source_aliases?.length && <p>Алиасы источника: {water.source_aliases.join(", ")}.</p>}</div><div><span class="overline">Источник</span>{sourceHref ? <p><a href={sourceHref} rel="noreferrer">{water.source_system ?? "Внешний источник"} · исходная карточка</a></p> : <p>Источник для описания ещё не подтверждён.</p>}{water.source_point_urls?.length && <p>Сохранено ссылок на точки: <strong>{water.source_point_urls.length}</strong>.</p>}{water.source_image_urls?.length && <p>Изображений-кандидатов: <strong>{water.source_image_urls.length}</strong>; публикация требует отдельной проверки.</p>}{water.source_checked_at && <p>Проверено: {new Date(water.source_checked_at).toLocaleDateString("ru-RU")}</p>}</div></section>}
|
||||||
{water && <DataPassport sources={water.source_system ? [water.source_system] : []} sourceUrl={water.source_url} observedAt={water.source_checked_at} status={water.source_checked_at ? "verified" : "unverified"}/>}
|
{water && <DataPassport sources={water.source_system ? [water.source_system] : []} sourceUrl={water.source_url} observedAt={water.source_checked_at} status={water.source_checked_at ? "verified" : "unverified"}/>}
|
||||||
{unavailable ? <StatePanel tone="unavailable" title="Данные временно недоступны" description="Каталог сохранён, но свежие наблюдения сейчас не получены." /> : !water ? <StatePanel tone="error" title="Такого водоёма нет в справочнике" actionHref="/waterbodies" actionLabel="Открыть каталог" /> : <section class="catalog-results content-grid"><aside><span class="overline">Рыбы</span>{fishes.length ? <nav>{fishes.map(([fishSlug,name]) => <AtlasEntityLink href={`/waterbodies/${water!.slug}/${fishSlug}?hours=${hours}`} label={name} kind="fish" identity={name} />)}</nav> : <p>Свежих подтверждённых видов пока нет.</p>}</aside><div>{items.length ? items.map(item => <ActivityCard item={item} periodLabel={periodLabel}/>) : <StatePanel contained={false} title="Свежих точек пока нет" description="Свежих подтверждённых наблюдений пока нет." />}</div></section>}
|
{unavailable ? <StatePanel tone="unavailable" title="Данные временно недоступны" description="Каталог сохранён, но свежие наблюдения сейчас не получены." /> : !water ? <StatePanel tone="error" title="Такого водоёма нет в справочнике" actionHref="/waterbodies" actionLabel="Открыть каталог" /> : <section class="catalog-results content-grid"><aside><span class="overline">Рыбы</span>{fishes.length ? <nav>{fishes.map(([fishSlug,name]) => <AtlasEntityLink href={`/waterbodies/${water!.slug}/${fishSlug}?hours=${hours}`} label={name} kind="fish" identity={name} />)}</nav> : <p>Свежих подтверждённых видов пока нет.</p>}</aside><div>{items.length ? items.map(item => <ActivityCard item={item} periodLabel={periodLabel}/>) : <StatePanel contained={false} title="Свежих точек пока нет" description="Свежих подтверждённых наблюдений пока нет." />}</div></section>}
|
||||||
|
{water && <Pagination path={`/waterbodies/${water.slug}`} params={new URLSearchParams({ hours })} total={totalItems} limit={activityLimit} offset={offset} itemLabel="точек" />}
|
||||||
</Layout>
|
</Layout>
|
||||||
|
|||||||
Reference in New Issue
Block a user