feat: add activity evidence summaries
This commit is contained in:
@@ -0,0 +1,26 @@
|
||||
---
|
||||
import DataPassport from "./DataPassport.astro";
|
||||
import type { Activity } from "../lib/api";
|
||||
const { items, totalPoints, periodLabel } = Astro.props as { items: Activity[]; totalPoints: number; periodLabel: string };
|
||||
const sources = [...new Set(items.flatMap(item => item.sources))];
|
||||
const conflicts = [...new Set(items.flatMap(item => item.source_conflicts))];
|
||||
const observations = items.reduce((sum, item) => sum + item.catches, 0);
|
||||
const confidence = items.length ? Math.round(items.reduce((sum, item) => sum + item.confidence_score, 0) / items.length) : null;
|
||||
const observedAt = items.map(item => item.last_confirmed_at).filter(Boolean).sort().at(-1) ?? null;
|
||||
const status = observations < 3 ? "insufficient" : "verified";
|
||||
---
|
||||
<section class="activity-evidence content-grid" aria-labelledby="activity-evidence-title">
|
||||
<div>
|
||||
<span class="overline">Доверие к выборке</span>
|
||||
<h2 id="activity-evidence-title">Паспорт активности</h2>
|
||||
<p>{totalPoints} {totalPoints === 1 ? "точка" : totalPoints < 5 ? "точки" : "точек"} за {periodLabel}. Сводка построена по подтверждённым карточкам этого списка.</p>
|
||||
</div>
|
||||
<DataPassport sources={sources} observedAt={observedAt} periodLabel={periodLabel} confidence={confidence} sampleSize={observations} sourceConflicts={conflicts} status={status} />
|
||||
</section>
|
||||
|
||||
<style>
|
||||
.activity-evidence { display: grid; grid-template-columns: minmax(0, 1fr) minmax(320px, 1.1fr); gap: 24px; align-items: start; margin-block: 26px; }
|
||||
.activity-evidence h2 { margin: 6px 0 8px; }
|
||||
.activity-evidence p { max-width: 470px; margin: 0; color: var(--text-muted); line-height: 1.6; }
|
||||
@media (max-width: 720px) { .activity-evidence { grid-template-columns: 1fr; } }
|
||||
</style>
|
||||
@@ -1,5 +1,6 @@
|
||||
---
|
||||
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";
|
||||
@@ -46,6 +47,7 @@ const schema = fish ? { "@context":"https://schema.org", "@type":"CollectionPage
|
||||
<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>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
---
|
||||
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";
|
||||
@@ -53,6 +54,7 @@ const schema = water ? { "@context":"https://schema.org", "@type":"CollectionPag
|
||||
{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 && <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 && <ActivityEvidenceSummary items={items} totalPoints={totalItems} periodLabel={periodLabel} />}
|
||||
{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>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
---
|
||||
import ActivityCard from "../../../components/ActivityCard.astro";
|
||||
import ActivityEvidenceSummary from "../../../components/ActivityEvidenceSummary.astro";
|
||||
import ActivityPeriodFilter from "../../../components/ActivityPeriodFilter.astro";
|
||||
import AtlasBreadcrumbs from "../../../components/AtlasBreadcrumbs.astro";
|
||||
import PageHero from "../../../components/PageHero.astro";
|
||||
@@ -40,6 +41,7 @@ const schema = valid ? { "@type":"CollectionPage", name:`${fish!.name_ru} на $
|
||||
<AtlasBreadcrumbs items={[{ label: "Водоёмы", href: "/waterbodies" }, { label: water?.name_ru ?? "Не найдено", href: water ? `/waterbodies/${water.slug}` : undefined }, { label: fish?.name_ru ?? "Не найдено" }]} />
|
||||
<PageHero eyebrow={water?.name_ru ?? "Каталог RF4"} title={fish?.name_ru ?? "Данные не найдены"} description={valid ? `${totalItems} ${plural(totalItems,["свежая точка","свежие точки","свежих точек"])} за ${periodLabel}.` : undefined} variant={valid ? "pair" : undefined} identity={fish?.name_ru} secondaryIdentity={water?.slug} />
|
||||
{valid && <ActivityPeriodFilter hours={hours} />}
|
||||
{valid && <ActivityEvidenceSummary items={items} totalPoints={totalItems} periodLabel={periodLabel} />}
|
||||
{unavailable ? <StatePanel tone="unavailable" title="Данные временно недоступны" description="Свежие наблюдения сейчас не получены. Попробуйте позднее." /> : !valid ? <StatePanel tone="error" title="Такого сочетания нет в справочнике" actionHref="/" actionLabel="Вернуться на главную" /> : <section class="catalog-results catalog-results--single content-grid"><div>{items.length ? items.map(item => <ActivityCard item={item} periodLabel={periodLabel}/>) : <StatePanel contained={false} title="Свежих точек пока нет" description="Данные появятся после новых подтверждённых наблюдений." />}</div></section>}
|
||||
{valid && <Pagination path={`/waterbodies/${water!.slug}/${fish!.slug}`} params={new URLSearchParams({ hours })} total={totalItems} limit={activityLimit} offset={offset} itemLabel="точек" />}
|
||||
</Layout>
|
||||
|
||||
Reference in New Issue
Block a user