refactor: unify public state panels
This commit is contained in:
@@ -0,0 +1,8 @@
|
|||||||
|
---
|
||||||
|
const { title, description, actionHref, actionLabel, tone = "empty", compact = false, contained = true } = Astro.props as { title: string; description?: string; actionHref?: string; actionLabel?: string; tone?: "empty" | "error" | "unavailable"; compact?: boolean; contained?: boolean };
|
||||||
|
---
|
||||||
|
<div class:list={["state", { "content-grid": contained, "compact-state": compact, "error-state": tone === "error", "unavailable-state": tone === "unavailable" }]} role={tone === "empty" ? "status" : "alert"}>
|
||||||
|
<h2>{title}</h2>
|
||||||
|
{description && <p>{description}</p>}
|
||||||
|
{actionHref && actionLabel && <a href={actionHref}>{actionLabel}</a>}
|
||||||
|
</div>
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
---
|
---
|
||||||
import ActivityCard from "../../components/ActivityCard.astro";
|
import ActivityCard from "../../components/ActivityCard.astro";
|
||||||
import PageHero from "../../components/PageHero.astro";
|
import PageHero from "../../components/PageHero.astro";
|
||||||
|
import StatePanel from "../../components/StatePanel.astro";
|
||||||
import Layout from "../../layouts/Layout.astro";
|
import Layout from "../../layouts/Layout.astro";
|
||||||
import { api, plural, type Activity, type DictionaryItem, type PaginatedActivity } from "../../lib/api";
|
import { api, plural, type Activity, type DictionaryItem, type PaginatedActivity } from "../../lib/api";
|
||||||
const { slug } = Astro.params;
|
const { slug } = Astro.params;
|
||||||
@@ -22,5 +23,5 @@ const schema = fish ? { "@context":"https://schema.org", "@type":"CollectionPage
|
|||||||
<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}>
|
<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}>
|
||||||
<a class="back" href="/fish">← Все виды рыб</a>
|
<a class="back" href="/fish">← Все виды рыб</a>
|
||||||
<PageHero eyebrow="Свежие данные за 72 часа" title={fish?.name_ru ?? "Рыба не найдена"} description={fish ? `${items.length} ${plural(items.length,["активная точка","активные точки","активных точек"])} на ${waters.length} ${plural(waters.length,["водоёме","водоёмах","водоёмах"])}.` : undefined} />
|
<PageHero eyebrow="Свежие данные за 72 часа" title={fish?.name_ru ?? "Рыба не найдена"} description={fish ? `${items.length} ${plural(items.length,["активная точка","активные точки","активных точек"])} на ${waters.length} ${plural(waters.length,["водоёме","водоёмах","водоёмах"])}.` : undefined} />
|
||||||
{unavailable ? <div class="state content-grid"><h2>Данные временно недоступны</h2></div> : !fish ? <div class="state content-grid"><h2>Такой рыбы нет в справочнике</h2><a href="/fish">Открыть каталог</a></div> : <section class="catalog-results content-grid"><aside><span class="overline">Водоёмы</span>{waters.length ? <nav>{waters.map(([waterSlug,name]) => <a href={`/waterbodies/${waterSlug}/${fish!.slug}`}>{name}</a>)}</nav> : <p>Свежих подтверждённых водоёмов пока нет.</p>}</aside><div>{items.length ? items.map(item => <ActivityCard item={item}/>) : <div class="state"><h2>Свежих точек пока нет</h2><p>Проверьте позже или посмотрите полевые сигналы на главной.</p></div>}</div></section>}
|
{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]) => <a href={`/waterbodies/${waterSlug}/${fish!.slug}`}>{name}</a>)}</nav> : <p>Свежих подтверждённых водоёмов пока нет.</p>}</aside><div>{items.length ? items.map(item => <ActivityCard item={item}/>) : <StatePanel contained={false} title="Свежих точек пока нет" description="Проверьте позже или посмотрите полевые сигналы на главной." />}</div></section>}
|
||||||
</Layout>
|
</Layout>
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
import Layout from "../../layouts/Layout.astro";
|
import Layout from "../../layouts/Layout.astro";
|
||||||
import FishSilhouette from "../../components/FishSilhouette.astro";
|
import FishSilhouette from "../../components/FishSilhouette.astro";
|
||||||
import PageHero from "../../components/PageHero.astro";
|
import PageHero from "../../components/PageHero.astro";
|
||||||
|
import StatePanel from "../../components/StatePanel.astro";
|
||||||
import { api, type DictionaryItem } from "../../lib/api";
|
import { api, type DictionaryItem } from "../../lib/api";
|
||||||
let fishes: DictionaryItem[] = [], unavailable = false;
|
let fishes: DictionaryItem[] = [], unavailable = false;
|
||||||
try { fishes = await api<DictionaryItem[]>("/api/v1/fishes?limit=500"); } catch { unavailable = true; }
|
try { fishes = await api<DictionaryItem[]>("/api/v1/fishes?limit=500"); } catch { unavailable = true; }
|
||||||
@@ -13,5 +14,5 @@ if (unavailable) {
|
|||||||
---
|
---
|
||||||
<Layout title="Все виды рыб Russian Fishing 4 — RF4 Spotter" description="Каталог рыб RF4 со свежими точками, уловами, приманками и прозрачными источниками данных.">
|
<Layout title="Все виды рыб Russian Fishing 4 — RF4 Spotter" description="Каталог рыб RF4 со свежими точками, уловами, приманками и прозрачными источниками данных.">
|
||||||
<PageHero eyebrow="Справочник RF4" title="Рыбы" description="Выберите вид, чтобы увидеть свежие подтверждённые точки и полевые сигналы." />
|
<PageHero eyebrow="Справочник RF4" title="Рыбы" description="Выберите вид, чтобы увидеть свежие подтверждённые точки и полевые сигналы." />
|
||||||
{unavailable ? <div class="state content-grid"><h2>Каталог временно недоступен</h2></div> : <nav class="catalog-grid content-grid" aria-label="Виды рыб">{fishes.map(fish => <a href={`/fish/${fish.slug}`}><span>Вид рыбы</span><strong>{fish.name_ru}</strong><i>→</i><FishSilhouette name={fish.name_ru} size={76}/></a>)}</nav>}
|
{unavailable ? <StatePanel tone="unavailable" title="Каталог временно недоступен" description="Не показываем непроверенный список. Попробуйте обновить страницу позже." /> : <nav class="catalog-grid content-grid" aria-label="Виды рыб">{fishes.map(fish => <a href={`/fish/${fish.slug}`}><span>Вид рыбы</span><strong>{fish.name_ru}</strong><i>→</i><FishSilhouette name={fish.name_ru} size={76}/></a>)}</nav>}
|
||||||
</Layout>
|
</Layout>
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
import Layout from "../layouts/Layout.astro";
|
import Layout from "../layouts/Layout.astro";
|
||||||
import SourceBadge from "../components/SourceBadge.astro";
|
import SourceBadge from "../components/SourceBadge.astro";
|
||||||
import SectionHeading from "../components/SectionHeading.astro";
|
import SectionHeading from "../components/SectionHeading.astro";
|
||||||
|
import StatePanel from "../components/StatePanel.astro";
|
||||||
import { api, kg, type DictionaryItem, type ImportRun, type OfficialRecord, type PaginatedOfficialRecord } from "../lib/api";
|
import { api, kg, type DictionaryItem, type ImportRun, type OfficialRecord, type PaginatedOfficialRecord } from "../lib/api";
|
||||||
const params = Astro.url.searchParams;
|
const params = Astro.url.searchParams;
|
||||||
const fish = params.get("fish") ?? "";
|
const fish = params.get("fish") ?? "";
|
||||||
@@ -23,9 +24,9 @@ const last = runs[0];
|
|||||||
<section class="records-hero"><div><span class="eyebrow">Публичные данные RF4</span><h1>Официальные<br/><em>рекорды</em></h1></div><div class="source-status"><span class:list={["status-dot", last?.status]}></span><strong>{last ? `Импорт: ${last.status}` : "Импорт ещё не запускался"}</strong>{last?.finished_at && <small>{new Date(last.finished_at).toLocaleString("ru-RU")} · {last.rows_seen} строк</small>}</div></section>
|
<section class="records-hero"><div><span class="eyebrow">Публичные данные RF4</span><h1>Официальные<br/><em>рекорды</em></h1></div><div class="source-status"><span class:list={["status-dot", last?.status]}></span><strong>{last ? `Импорт: ${last.status}` : "Импорт ещё не запускался"}</strong>{last?.finished_at && <small>{new Date(last.finished_at).toLocaleString("ru-RU")} · {last.rows_seen} строк</small>}</div></section>
|
||||||
<form class="record-filters" method="get"><label>Рыба<select name="fish"><option value="">Любая рыба</option>{fishes.map(item => <option value={item.slug} selected={fish === item.slug}>{item.name_ru}</option>)}</select></label><label>Водоём<select name="waterbody"><option value="">Все водоёмы</option>{waterbodies.map(item => <option value={item.slug} selected={waterbody === item.slug}>{item.name_ru}</option>)}</select></label><button>Фильтровать</button>{(fish || waterbody) && <a href="/records">Сбросить</a>}</form>
|
<form class="record-filters" method="get"><label>Рыба<select name="fish"><option value="">Любая рыба</option>{fishes.map(item => <option value={item.slug} selected={fish === item.slug}>{item.name_ru}</option>)}</select></label><label>Водоём<select name="waterbody"><option value="">Все водоёмы</option>{waterbodies.map(item => <option value={item.slug} selected={waterbody === item.slug}>{item.name_ru}</option>)}</select></label><button>Фильтровать</button>{(fish || waterbody) && <a href="/records">Сбросить</a>}</form>
|
||||||
<div class="content-grid"><SectionHeading eyebrow="Официальный источник" title="Последние записи" count={`${items.length} из ${totalRecords} записей`} /></div>
|
<div class="content-grid"><SectionHeading eyebrow="Официальный источник" title="Последние записи" count={`${items.length} из ${totalRecords} записей`} /></div>
|
||||||
{unavailable && <div class="state content-grid"><h2>Источник временно недоступен</h2></div>}
|
{unavailable && <StatePanel tone="unavailable" title="Источник временно недоступен" description="Сохраняем ранее импортированные данные, но не показываем их как свежее обновление." />}
|
||||||
{!unavailable && items.length && <div class="record-table"><div class="record-row record-head"><span>Рыба</span><span>Вес</span><span>Водоём</span><span>Приманка</span><span>Игрок</span><span>Дата и источник</span></div>{items.map(record => <article class="record-row"><strong data-label="Рыба">{record.fish}</strong><strong data-label="Вес">{kg(record.weight_g)}</strong><span data-label="Водоём">{record.waterbody}</span><span data-label="Приманка">{record.bait ?? "—"}</span><span data-label="Игрок">{record.player_name ?? "—"}</span><span data-label="Дата и источник" class="record-provenance"><time>{record.record_date ? new Date(record.record_date).toLocaleDateString("ru-RU") : "—"}</time><SourceBadge source={record.source_system} href={record.source_url}/></span></article>)}</div>}
|
{!unavailable && items.length && <div class="record-table"><div class="record-row record-head"><span>Рыба</span><span>Вес</span><span>Водоём</span><span>Приманка</span><span>Игрок</span><span>Дата и источник</span></div>{items.map(record => <article class="record-row"><strong data-label="Рыба">{record.fish}</strong><strong data-label="Вес">{kg(record.weight_g)}</strong><span data-label="Водоём">{record.waterbody}</span><span data-label="Приманка">{record.bait ?? "—"}</span><span data-label="Игрок">{record.player_name ?? "—"}</span><span data-label="Дата и источник" class="record-provenance"><time>{record.record_date ? new Date(record.record_date).toLocaleDateString("ru-RU") : "—"}</time><SourceBadge source={record.source_system} href={record.source_url}/></span></article>)}</div>}
|
||||||
{!unavailable && items.length && offset + items.length < totalRecords && <a class="load-more" href={`/records?${(() => { const p = new URLSearchParams(params); p.delete("offset"); p.set("offset", String(offset + items.length)); return p.toString(); })()}`}>Показать ещё <span>{offset + items.length} из {totalRecords}</span> ↓</a>}
|
{!unavailable && items.length && offset + items.length < totalRecords && <a class="load-more" href={`/records?${(() => { const p = new URLSearchParams(params); p.delete("offset"); p.set("offset", String(offset + items.length)); return p.toString(); })()}`}>Показать ещё <span>{offset + items.length} из {totalRecords}</span> ↓</a>}
|
||||||
{!unavailable && !items.length && <div class="state content-grid"><h2>Рекорды ещё не импортированы</h2><p>Для выбранных условий записей пока нет.</p></div>}
|
{!unavailable && !items.length && <StatePanel title="Рекорды ещё не импортированы" description="Для выбранных условий записей пока нет." actionHref="/records" actionLabel="Сбросить фильтры" />}
|
||||||
<p class="official-note">Источник: <a href="https://rf4game.de/records/region/RU/" rel="noreferrer">официальный сайт Russian Fishing 4</a>. Координаты в официальных таблицах отсутствуют.</p>
|
<p class="official-note">Источник: <a href="https://rf4game.de/records/region/RU/" rel="noreferrer">официальный сайт Russian Fishing 4</a>. Координаты в официальных таблицах отсутствуют.</p>
|
||||||
</Layout>
|
</Layout>
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import Layout from "../layouts/Layout.astro";
|
|||||||
import SourceBadge from "../components/SourceBadge.astro";
|
import SourceBadge from "../components/SourceBadge.astro";
|
||||||
import DataLegend from "../components/DataLegend.astro";
|
import DataLegend from "../components/DataLegend.astro";
|
||||||
import PageHero from "../components/PageHero.astro";
|
import PageHero from "../components/PageHero.astro";
|
||||||
|
import StatePanel from "../components/StatePanel.astro";
|
||||||
import { ago, api, plural, type SourceStatus } from "../lib/api";
|
import { ago, api, plural, type SourceStatus } from "../lib/api";
|
||||||
let sources: SourceStatus[] = [], unavailable = false;
|
let sources: SourceStatus[] = [], unavailable = false;
|
||||||
try { sources = await api<SourceStatus[]>("/api/v1/source-status"); } catch { unavailable = true; }
|
try { sources = await api<SourceStatus[]>("/api/v1/source-status"); } catch { unavailable = true; }
|
||||||
@@ -10,6 +11,6 @@ const labels = { healthy:"Актуален", stale:"Данные устарел
|
|||||||
---
|
---
|
||||||
<Layout title="Состояние источников — RF4 Spotter" description="Текущее безопасное состояние источников данных RF4 Spotter.">
|
<Layout title="Состояние источников — RF4 Spotter" description="Текущее безопасное состояние источников данных RF4 Spotter.">
|
||||||
<PageHero eyebrow="Прозрачность данных" title="Статус источников" description="Пауза или изменение страницы не удаляют уже опубликованные данные." />
|
<PageHero eyebrow="Прозрачность данных" title="Статус источников" description="Пауза или изменение страницы не удаляют уже опубликованные данные." />
|
||||||
{unavailable ? <div class="state content-grid"><h2>Статус временно недоступен</h2></div> : <section class="source-health-grid content-grid">{sources.map(source => <article data-status={source.status}><div><SourceBadge source={source.source_system}/><span class="health-state"><i></i>{labels[source.status]}</span></div><h2>{source.name}</h2><strong>{source.observations} {plural(source.observations,["наблюдение","наблюдения","наблюдений"])}</strong><p>{source.last_success_at ? `Последнее успешное обновление ${ago(source.last_success_at)}` : "Успешных запусков пока нет"}</p></article>)}</section>}
|
{unavailable ? <StatePanel tone="unavailable" title="Статус временно недоступен" description="Данные о состоянии источников не получены. Публичные наблюдения при этом не изменяются." /> : <section class="source-health-grid content-grid">{sources.map(source => <article data-status={source.status}><div><SourceBadge source={source.source_system}/><span class="health-state"><i></i>{labels[source.status]}</span></div><h2>{source.name}</h2><strong>{source.observations} {plural(source.observations,["наблюдение","наблюдения","наблюдений"])}</strong><p>{source.last_success_at ? `Последнее успешное обновление ${ago(source.last_success_at)}` : "Успешных запусков пока нет"}</p></article>)}</section>}
|
||||||
<DataLegend />
|
<DataLegend />
|
||||||
</Layout>
|
</Layout>
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
---
|
---
|
||||||
import ActivityCard from "../../components/ActivityCard.astro";
|
import ActivityCard from "../../components/ActivityCard.astro";
|
||||||
import PageHero from "../../components/PageHero.astro";
|
import PageHero from "../../components/PageHero.astro";
|
||||||
|
import StatePanel from "../../components/StatePanel.astro";
|
||||||
import Layout from "../../layouts/Layout.astro";
|
import Layout from "../../layouts/Layout.astro";
|
||||||
import { api, plural, type Activity, type DictionaryItem, type PaginatedActivity } from "../../lib/api";
|
import { api, plural, type Activity, type DictionaryItem, type PaginatedActivity } from "../../lib/api";
|
||||||
const { slug } = Astro.params;
|
const { slug } = Astro.params;
|
||||||
@@ -22,5 +23,5 @@ 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}>
|
||||||
<a class="back" href="/waterbodies">← Все водоёмы</a>
|
<a class="back" href="/waterbodies">← Все водоёмы</a>
|
||||||
<PageHero eyebrow="Свежие данные за 72 часа" title={water?.name_ru ?? "Водоём не найден"} description={water ? `${items.length} ${plural(items.length,["активная точка","активные точки","активных точек"])} для ${fishes.length} ${plural(fishes.length,["вида рыбы","видов рыб","видов рыб"])}.` : undefined} />
|
<PageHero eyebrow="Свежие данные за 72 часа" title={water?.name_ru ?? "Водоём не найден"} description={water ? `${items.length} ${plural(items.length,["активная точка","активные точки","активных точек"])} для ${fishes.length} ${plural(fishes.length,["вида рыбы","видов рыб","видов рыб"])}.` : undefined} />
|
||||||
{unavailable ? <div class="state content-grid"><h2>Данные временно недоступны</h2></div> : !water ? <div class="state content-grid"><h2>Такого водоёма нет в справочнике</h2><a href="/waterbodies">Открыть каталог</a></div> : <section class="catalog-results content-grid"><aside><span class="overline">Рыбы</span>{fishes.length ? <nav>{fishes.map(([fishSlug,name]) => <a href={`/waterbodies/${water!.slug}/${fishSlug}`}>{name}</a>)}</nav> : <p>Свежих подтверждённых видов пока нет.</p>}</aside><div>{items.length ? items.map(item => <ActivityCard item={item}/>) : <div class="state"><h2>Свежих точек пока нет</h2><p>Проверьте позже или посмотрите полевые сигналы на главной.</p></div>}</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]) => <a href={`/waterbodies/${water!.slug}/${fishSlug}`}>{name}</a>)}</nav> : <p>Свежих подтверждённых видов пока нет.</p>}</aside><div>{items.length ? items.map(item => <ActivityCard item={item}/>) : <StatePanel contained={false} title="Свежих точек пока нет" description="Проверьте позже или посмотрите полевые сигналы на главной." />}</div></section>}
|
||||||
</Layout>
|
</Layout>
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
---
|
---
|
||||||
import ActivityCard from "../../../components/ActivityCard.astro";
|
import ActivityCard from "../../../components/ActivityCard.astro";
|
||||||
import PageHero from "../../../components/PageHero.astro";
|
import PageHero from "../../../components/PageHero.astro";
|
||||||
|
import StatePanel from "../../../components/StatePanel.astro";
|
||||||
import Layout from "../../../layouts/Layout.astro";
|
import Layout from "../../../layouts/Layout.astro";
|
||||||
import { api, plural, type Activity, type DictionaryItem, type PaginatedActivity } from "../../../lib/api";
|
import { api, plural, type Activity, type DictionaryItem, type PaginatedActivity } from "../../../lib/api";
|
||||||
const { slug, fish: fishSlug } = Astro.params;
|
const { slug, fish: fishSlug } = Astro.params;
|
||||||
@@ -22,5 +23,5 @@ const schema = valid ? { "@type":"CollectionPage", name:`${fish!.name_ru} на $
|
|||||||
<Layout title={valid ? `${fish!.name_ru} на ${water!.name_ru} в RF4 — точки и приманки` : "Страница не найдена — RF4 Spotter"} description={valid ? `Где ловить ${fish!.name_ru} на ${water!.name_ru} в Russian Fishing 4: свежие координаты, приманки, активность и источники.` : "Такого сочетания рыбы и водоёма нет в каталоге."} noindex={!valid || unavailable} structuredData={schema} errorPage={!valid || unavailable}>
|
<Layout title={valid ? `${fish!.name_ru} на ${water!.name_ru} в RF4 — точки и приманки` : "Страница не найдена — RF4 Spotter"} description={valid ? `Где ловить ${fish!.name_ru} на ${water!.name_ru} в Russian Fishing 4: свежие координаты, приманки, активность и источники.` : "Такого сочетания рыбы и водоёма нет в каталоге."} noindex={!valid || unavailable} structuredData={schema} errorPage={!valid || unavailable}>
|
||||||
<a class="back" href={water ? `/waterbodies/${water.slug}` : "/waterbodies"}>← К водоёму</a>
|
<a class="back" href={water ? `/waterbodies/${water.slug}` : "/waterbodies"}>← К водоёму</a>
|
||||||
<PageHero eyebrow={water?.name_ru ?? "Каталог RF4"} title={fish?.name_ru ?? "Данные не найдены"} description={valid ? `${items.length} ${plural(items.length,["свежая точка","свежие точки","свежих точек"])} за последние 72 часа.` : undefined} />
|
<PageHero eyebrow={water?.name_ru ?? "Каталог RF4"} title={fish?.name_ru ?? "Данные не найдены"} description={valid ? `${items.length} ${plural(items.length,["свежая точка","свежие точки","свежих точек"])} за последние 72 часа.` : undefined} />
|
||||||
{unavailable ? <div class="state content-grid"><h2>Данные временно недоступны</h2></div> : !valid ? <div class="state content-grid"><h2>Такого сочетания нет в справочнике</h2><a href="/">Вернуться на главную</a></div> : <section class="catalog-results catalog-results--single content-grid"><div>{items.length ? items.map(item => <ActivityCard item={item}/>) : <div class="state"><h2>Свежих точек пока нет</h2><p>Данные появятся после новых подтверждённых наблюдений.</p></div>}</div></section>}
|
{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}/>) : <StatePanel contained={false} title="Свежих точек пока нет" description="Данные появятся после новых подтверждённых наблюдений." />}</div></section>}
|
||||||
</Layout>
|
</Layout>
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
---
|
---
|
||||||
import Layout from "../../layouts/Layout.astro";
|
import Layout from "../../layouts/Layout.astro";
|
||||||
import PageHero from "../../components/PageHero.astro";
|
import PageHero from "../../components/PageHero.astro";
|
||||||
|
import StatePanel from "../../components/StatePanel.astro";
|
||||||
import { api, type DictionaryItem } from "../../lib/api";
|
import { api, type DictionaryItem } from "../../lib/api";
|
||||||
let waters: DictionaryItem[] = [], unavailable = false;
|
let waters: DictionaryItem[] = [], unavailable = false;
|
||||||
try { waters = await api<DictionaryItem[]>("/api/v1/waterbodies?limit=500"); } catch { unavailable = true; }
|
try { waters = await api<DictionaryItem[]>("/api/v1/waterbodies?limit=500"); } catch { unavailable = true; }
|
||||||
@@ -12,5 +13,5 @@ if (unavailable) {
|
|||||||
---
|
---
|
||||||
<Layout title="Все водоёмы Russian Fishing 4 — RF4 Spotter" description="Каталог водоёмов RF4 со свежими точками, рыбами, приманками и прозрачными источниками данных.">
|
<Layout title="Все водоёмы Russian Fishing 4 — RF4 Spotter" description="Каталог водоёмов RF4 со свежими точками, рыбами, приманками и прозрачными источниками данных.">
|
||||||
<PageHero eyebrow="Карта водоёмов RF4" title="Водоёмы" description="Откройте водоём, чтобы увидеть активные виды рыб и последние подтверждённые точки." />
|
<PageHero eyebrow="Карта водоёмов RF4" title="Водоёмы" description="Откройте водоём, чтобы увидеть активные виды рыб и последние подтверждённые точки." />
|
||||||
{unavailable ? <div class="state content-grid"><h2>Каталог временно недоступен</h2></div> : <nav class="catalog-grid content-grid" aria-label="Водоёмы">{waters.map(water => <a href={`/waterbodies/${water.slug}`}><span>Водоём</span><strong>{water.name_ru}</strong><i>→</i></a>)}</nav>}
|
{unavailable ? <StatePanel tone="unavailable" title="Каталог временно недоступен" description="Не показываем непроверенный список. Попробуйте обновить страницу позже." /> : <nav class="catalog-grid content-grid" aria-label="Водоёмы">{waters.map(water => <a href={`/waterbodies/${water.slug}`}><span>Водоём</span><strong>{water.name_ru}</strong><i>→</i></a>)}</nav>}
|
||||||
</Layout>
|
</Layout>
|
||||||
|
|||||||
@@ -47,3 +47,6 @@ footer{min-height:118px;background:var(--deep);color:#dbe4df;padding:28px max(32
|
|||||||
|
|
||||||
/* Shared editorial signature for data sections. */
|
/* Shared editorial signature for data sections. */
|
||||||
.section-heading{gap:24px}.section-heading>div{min-width:0}.section-heading p{max-width:700px;margin:10px 0 0;color:var(--text-muted);line-height:1.55}.section-heading--editorial{padding-top:68px;border-top:1px solid var(--border);margin-bottom:25px}.section-heading--editorial h2{font-size:46px;line-height:.95}.section-heading--editorial .result-count{align-self:end}@media(max-width:720px){.section-heading--editorial{display:block;padding-top:45px}.section-heading--editorial .result-count{display:inline-block;margin-top:14px}}
|
.section-heading{gap:24px}.section-heading>div{min-width:0}.section-heading p{max-width:700px;margin:10px 0 0;color:var(--text-muted);line-height:1.55}.section-heading--editorial{padding-top:68px;border-top:1px solid var(--border);margin-bottom:25px}.section-heading--editorial h2{font-size:46px;line-height:.95}.section-heading--editorial .result-count{align-self:end}@media(max-width:720px){.section-heading--editorial{display:block;padding-top:45px}.section-heading--editorial .result-count{display:inline-block;margin-top:14px}}
|
||||||
|
|
||||||
|
/* One semantic treatment for empty, unavailable and invalid public states. */
|
||||||
|
.state.unavailable-state{border-style:solid;border-color:color-mix(in srgb,var(--warning) 35%,var(--border));background:color-mix(in srgb,var(--warning-soft) 35%,var(--surface))}.state.error-state{background:color-mix(in srgb,var(--danger-soft) 28%,var(--surface))}.state>a{margin-top:8px;font-weight:750;text-underline-offset:4px}
|
||||||
|
|||||||
+1
-1
@@ -23,7 +23,7 @@
|
|||||||
- [x] **B01 · Иерархия имени.** RF4 Spotter — единое имя продукта в UI, metadata, manifest и документации; «Ни хвоста, ни чешуи» — поддерживающий слоган. Обновлены wordmark в header/footer и подпись выпуска.
|
- [x] **B01 · Иерархия имени.** RF4 Spotter — единое имя продукта в UI, metadata, manifest и документации; «Ни хвоста, ни чешуи» — поддерживающий слоган. Обновлены wordmark в header/footer и подпись выпуска.
|
||||||
- [x] **B02 · Дизайн-токены.** Добавлены семантические роли поверхностей, текста, границ, фокуса, success/warning/danger и всех источников. На токены переведены паспорта данных, status-карточки, skeleton и focus; контрастные пары зафиксированы в brand system.
|
- [x] **B02 · Дизайн-токены.** Добавлены семантические роли поверхностей, текста, границ, фокуса, success/warning/danger и всех источников. На токены переведены паспорта данных, status-карточки, skeleton и focus; контрастные пары зафиксированы в brand system.
|
||||||
- [x] **B03 · Графическая грамматика.** Мотивы закреплены за функциями: крючок — бренд, поплавок — активность/ожидание, леска — время, радар — координаты, силуэт — сущность рыбы. Убраны ложная рябь неполных сигналов и дублирующие радар круги карточки лидера; неполнота теперь подчёркнута спокойной полевой меткой.
|
- [x] **B03 · Графическая грамматика.** Мотивы закреплены за функциями: крючок — бренд, поплавок — активность/ожидание, леска — время, радар — координаты, силуэт — сущность рыбы. Убраны ложная рябь неполных сигналов и дублирующие радар круги карточки лидера; неполнота теперь подчёркнута спокойной полевой меткой.
|
||||||
- [ ] **B04 · Компонентная подпись.** В работе: общие `SectionHeading` и `PageHero` унифицируют рекорды, сигналы, каталоги и статус; далее унифицировать CTA и системные состояния публичных/admin-страниц.
|
- [ ] **B04 · Компонентная подпись.** В работе: `SectionHeading`, `PageHero` и `StatePanel` унифицируют заголовки, hero и состояния публичных каталогов, статуса и рекордов; далее унифицировать CTA и состояния admin-страниц.
|
||||||
- [ ] **B05 · Motion-система.** Оставить только смысловые микроанимации поклёвки/пульса/перехода, задать общие длительности и полностью поддержать reduced motion.
|
- [ ] **B05 · Motion-система.** Оставить только смысловые микроанимации поклёвки/пульса/перехода, задать общие длительности и полностью поддержать reduced motion.
|
||||||
- [ ] **B06 · Brand QA.** Проверить favicon, maskable icons, manifest, OG 1200×630, светлый/тёмный фон, 16/32 px и mobile/desktop screenshots; записать правила в [brand-system.md](brand-system.md).
|
- [ ] **B06 · Brand QA.** Проверить favicon, maskable icons, manifest, OG 1200×630, светлый/тёмный фон, 16/32 px и mobile/desktop screenshots; записать правила в [brand-system.md](brand-system.md).
|
||||||
|
|
||||||
|
|||||||
@@ -42,6 +42,8 @@
|
|||||||
|
|
||||||
## Компонентная подпись
|
## Компонентная подпись
|
||||||
|
|
||||||
|
`StatePanel` — единая рамка для пустых, недоступных и ошибочных состояний. Пустое состояние имеет роль `status`, недоступное и ошибочное — `alert`; цвет сообщает смысл, а не заменяет заголовок и пояснение. На вложенных карточках компонент используется без внешнего `content-grid`.
|
||||||
|
|
||||||
Секции данных используют `SectionHeading`: короткий uppercase eyebrow задаёт контекст, антиквенный заголовок называет сущность, необязательное описание объясняет правила, а счётчик всегда расположен справа или под текстом на мобильном. Так число результатов не конкурирует с названием раздела и одинаково читается в рекордах и полевых сигналах.
|
Секции данных используют `SectionHeading`: короткий uppercase eyebrow задаёт контекст, антиквенный заголовок называет сущность, необязательное описание объясняет правила, а счётчик всегда расположен справа или под текстом на мобильном. Так число результатов не конкурирует с названием раздела и одинаково читается в рекордах и полевых сигналах.
|
||||||
|
|
||||||
Каталоги, detail-выборки и страницы прозрачности используют `PageHero`: один контекст, один `h1`, одно короткое пояснение. Это сохраняет одинаковый вертикальный ритм и SEO-иерархию независимо от наличия данных.
|
Каталоги, detail-выборки и страницы прозрачности используют `PageHero`: один контекст, один `h1`, одно короткое пояснение. Это сохраняет одинаковый вертикальный ритм и SEO-иерархию независимо от наличия данных.
|
||||||
|
|||||||
Reference in New Issue
Block a user