feat: paginate and label media library
This commit is contained in:
@@ -1,18 +1,23 @@
|
|||||||
---
|
---
|
||||||
import EntityMedia from "../components/EntityMedia.astro";
|
import EntityMedia from "../components/EntityMedia.astro";
|
||||||
|
import Pagination from "../components/Pagination.astro";
|
||||||
import PageHero from "../components/PageHero.astro";
|
import PageHero from "../components/PageHero.astro";
|
||||||
import StatePanel from "../components/StatePanel.astro";
|
import StatePanel from "../components/StatePanel.astro";
|
||||||
import Layout from "../layouts/Layout.astro";
|
import Layout from "../layouts/Layout.astro";
|
||||||
import { api, type MediaAsset } from "../lib/api";
|
import { api, type MediaAsset } from "../lib/api";
|
||||||
|
import { mediaRoleLabel } from "../lib/media";
|
||||||
const allowed = new Set(["fish", "waterbody", "tackle", "reference"]);
|
const allowed = new Set(["fish", "waterbody", "tackle", "reference"]);
|
||||||
const requested = Astro.url.searchParams.get("type") ?? "fish";
|
const requested = Astro.url.searchParams.get("type") ?? "fish";
|
||||||
const type = allowed.has(requested) ? requested : "fish";
|
const type = allowed.has(requested) ? requested : "fish";
|
||||||
const requestedRole = Astro.url.searchParams.get("role") ?? "";
|
const requestedRole = Astro.url.searchParams.get("role") ?? "";
|
||||||
const search = Astro.url.searchParams.get("q")?.trim() ?? "";
|
const search = Astro.url.searchParams.get("q")?.trim() ?? "";
|
||||||
|
const requestedOffset = Number(Astro.url.searchParams.get("offset") ?? 0);
|
||||||
|
let offset = Number.isInteger(requestedOffset) && requestedOffset >= 0 ? requestedOffset : 0;
|
||||||
|
const pageLimit = 48;
|
||||||
const roleOptions: Record<string, Array<[string, string]>> = { waterbody: [["waterbody_cover", "Заставки"], ["waterbody_map", "Карты"], ["waterbody_depth_map", "Карты глубин"], ["waterbody_screenshot", "Скриншоты"]], tackle: [["tackle_card", "Карточки снастей"], ["tackle_detail", "Детали снастей"], ["rig_diagram", "Схемы сборок"], ["tackle_screenshot", "Скриншоты снастей"]] };
|
const roleOptions: Record<string, Array<[string, string]>> = { waterbody: [["waterbody_cover", "Заставки"], ["waterbody_map", "Карты"], ["waterbody_depth_map", "Карты глубин"], ["waterbody_screenshot", "Скриншоты"]], tackle: [["tackle_card", "Карточки снастей"], ["tackle_detail", "Детали снастей"], ["rig_diagram", "Схемы сборок"], ["tackle_screenshot", "Скриншоты снастей"]] };
|
||||||
const selectedRole = (roleOptions[type] ?? []).some(([value]) => value === requestedRole) ? requestedRole : "";
|
const selectedRole = (roleOptions[type] ?? []).some(([value]) => value === requestedRole) ? requestedRole : "";
|
||||||
let assets: MediaAsset[] = [], unavailable = false;
|
let assets: MediaAsset[] = [], allAssets: MediaAsset[] = [], unavailable = false;
|
||||||
try { const query = new URLSearchParams({ entity_type: type }); if (selectedRole) query.set("media_role", selectedRole); assets = await api<MediaAsset[]>(`/api/v1/media/catalog?${query}`); if (search) { const needle = search.toLocaleLowerCase("ru-RU"); assets = assets.filter(asset => (asset.label ?? "").toLocaleLowerCase("ru-RU").includes(needle)); } } catch { unavailable = true; }
|
try { const query = new URLSearchParams({ entity_type: type }); if (selectedRole) query.set("media_role", selectedRole); allAssets = await api<MediaAsset[]>(`/api/v1/media/catalog?${query}`); if (search) { const needle = search.toLocaleLowerCase("ru-RU"); allAssets = allAssets.filter(asset => (asset.label ?? "").toLocaleLowerCase("ru-RU").includes(needle)); } if (allAssets.length > 0 && offset >= allAssets.length) offset = Math.floor((allAssets.length - 1) / pageLimit) * pageLimit; assets = allAssets.slice(offset, offset + pageLimit); } catch { unavailable = true; }
|
||||||
if (unavailable) {
|
if (unavailable) {
|
||||||
Astro.response.status = 503;
|
Astro.response.status = 503;
|
||||||
Astro.response.headers.set("Retry-After", "60");
|
Astro.response.headers.set("Retry-After", "60");
|
||||||
@@ -21,10 +26,11 @@ if (unavailable) {
|
|||||||
const labels: Record<string,string> = {fish:"Рыбы",waterbody:"Водоёмы",tackle:"Снасти и приманки",reference:"Справочные материалы"};
|
const labels: Record<string,string> = {fish:"Рыбы",waterbody:"Водоёмы",tackle:"Снасти и приманки",reference:"Справочные материалы"};
|
||||||
---
|
---
|
||||||
<Layout title={`${labels[type]} RF4 — медиатека RF4 Spotter`} description="Изображения рыб, водоёмов и снастей Russian Fishing 4 с обязательной атрибуцией каждого источника." noindex={unavailable} errorPage={unavailable}>
|
<Layout title={`${labels[type]} RF4 — медиатека RF4 Spotter`} description="Изображения рыб, водоёмов и снастей Russian Fishing 4 с обязательной атрибуцией каждого источника." noindex={unavailable} errorPage={unavailable}>
|
||||||
<PageHero eyebrow="Визуальный справочник" title="Медиатека" description="Собранные материалы RF4 с источником у каждого изображения. Каталог пополняется по мере импорта и проверки." variant="fish" count={assets.length} />
|
<PageHero eyebrow="Визуальный справочник" title="Медиатека" description="Собранные материалы RF4 с источником у каждого изображения. Каталог пополняется по мере импорта и проверки." variant="fish" count={allAssets.length} />
|
||||||
<section class="media-library content-grid">
|
<section class="media-library content-grid">
|
||||||
<nav class="media-library__intro" aria-label="Разделы медиатеки"><strong>{labels[type]} · {assets.length}</strong><span><a href="/media?type=fish">Рыбы</a> · <a href="/media?type=waterbody">Водоёмы</a> · <a href="/media?type=tackle">Снасти</a> · <a href="/media?type=reference">Справка</a></span></nav>
|
<nav class="media-library__intro" aria-label="Разделы медиатеки"><strong>{labels[type]} · {allAssets.length}</strong><span><a href="/media?type=fish">Рыбы</a> · <a href="/media?type=waterbody">Водоёмы</a> · <a href="/media?type=tackle">Снасти</a> · <a href="/media?type=reference">Справка</a></span></nav>
|
||||||
<form class="media-role-filter" method="get"><input type="hidden" name="type" value={type} /><label>Поиск по подписи<input name="q" value={search} maxlength="100" placeholder="Например, щука" /></label>{roleOptions[type]?.length > 0 && <label>Тип материала<select name="role"><option value="">Все роли</option>{roleOptions[type].map(([value, label]) => <option value={value} selected={selectedRole === value}>{label}</option>)}</select></label>}<button data-action="secondary">Показать</button>{(selectedRole || search) && <a data-action="quiet" href={`/media?type=${type}`}>Сбросить</a>}</form>
|
<form class="media-role-filter" method="get"><input type="hidden" name="type" value={type} /><label>Поиск по подписи<input name="q" value={search} maxlength="100" placeholder="Например, щука" /></label>{roleOptions[type]?.length > 0 && <label>Тип материала<select name="role"><option value="">Все роли</option>{roleOptions[type].map(([value, label]) => <option value={value} selected={selectedRole === value}>{label}</option>)}</select></label>}<button data-action="secondary">Показать</button>{(selectedRole || search) && <a data-action="quiet" href={`/media?type=${type}`}>Сбросить</a>}</form>
|
||||||
{unavailable ? <StatePanel tone="unavailable" title="Медиатека временно недоступна" /> : assets.length ? <div class="media-library__grid">{assets.map(asset => <article class="media-library__card"><EntityMedia asset={asset} sourceLink /><h2>{asset.label ?? "Без подписи"}</h2><span>{labels[asset.entity_type]}</span></article>)}</div> : <StatePanel title="В этом разделе пока нет изображений" description="Материалы появятся после следующего импорта." />}
|
{unavailable ? <StatePanel tone="unavailable" title="Медиатека временно недоступна" /> : assets.length ? <div class="media-library__grid">{assets.map(asset => <article class="media-library__card"><EntityMedia asset={asset} sourceLink /><h2>{asset.label ?? "Без подписи"}</h2><span>{labels[asset.entity_type]} · {mediaRoleLabel(asset.media_role ?? "")}</span><small>Источник: {asset.source_system}</small></article>)}</div> : <StatePanel title={search ? "Ничего не найдено" : "В этом разделе пока нет изображений"} description="Материалы появятся после следующего импорта." />}
|
||||||
|
{!unavailable && <Pagination path="/media" params={new URLSearchParams({ type, ...(selectedRole ? { role: selectedRole } : {}), ...(search ? { q: search } : {}) })} total={allAssets.length} limit={pageLimit} offset={offset} itemLabel="материалов" />}
|
||||||
</section>
|
</section>
|
||||||
</Layout>
|
</Layout>
|
||||||
|
|||||||
Reference in New Issue
Block a user