feat: show reviewed media on tackle details
This commit is contained in:
@@ -1,21 +1,25 @@
|
||||
---
|
||||
import AtlasBreadcrumbs from "../../../components/AtlasBreadcrumbs.astro";
|
||||
import DataPassport from "../../../components/DataPassport.astro";
|
||||
import EntityMedia from "../../../components/EntityMedia.astro";
|
||||
import Layout from "../../../layouts/Layout.astro";
|
||||
import PageHero from "../../../components/PageHero.astro";
|
||||
import StatePanel from "../../../components/StatePanel.astro";
|
||||
import TackleGlyph from "../../../components/TackleGlyph.astro";
|
||||
import { ApiError, api, type TackleItem } from "../../../lib/api";
|
||||
import { ApiError, api, type MediaAsset, type TackleItem } from "../../../lib/api";
|
||||
import { findMediaByLabel, mediaRoleLabel } from "../../../lib/media";
|
||||
import { safeHttpUrl } from "../../../lib/urls";
|
||||
|
||||
const { id } = Astro.params;
|
||||
let item: TackleItem | undefined;
|
||||
let item: TackleItem | undefined, media: MediaAsset[] = [];
|
||||
let unavailable = false;
|
||||
try {
|
||||
item = await api<TackleItem>(`/api/v1/tackle/items/${encodeURIComponent(id ?? "")}`);
|
||||
} catch (error) {
|
||||
unavailable = !(error instanceof ApiError) || error.status >= 500;
|
||||
}
|
||||
const [itemResult, mediaResult] = await Promise.allSettled([
|
||||
api<TackleItem>(`/api/v1/tackle/items/${encodeURIComponent(id ?? "")}`),
|
||||
api<MediaAsset[]>("/api/v1/media/catalog?entity_type=tackle"),
|
||||
]);
|
||||
if (itemResult.status === "fulfilled") item = itemResult.value;
|
||||
else { const error = itemResult.reason; unavailable = !(error instanceof ApiError) || error.status >= 500; }
|
||||
if (mediaResult.status === "fulfilled") media = mediaResult.value;
|
||||
if (!item && !unavailable) Astro.response.status = 404;
|
||||
if (unavailable) {
|
||||
Astro.response.status = 503;
|
||||
@@ -27,6 +31,7 @@ const categoryLabels: Record<string, string> = { bait: "Наживка", lure: "
|
||||
const missingLabels: Record<string, string> = { subcategory: "подкатегория", brand: "бренд", family: "семейство", unlock_level: "уровень открытия", source_url: "ссылка на источник", source_checked_at: "дата проверки" };
|
||||
const missing = item?.missing_fields.map((field) => missingLabels[field] ?? field) ?? [];
|
||||
const sourceHref = safeHttpUrl(item?.source_url);
|
||||
const mediaCandidates = item ? (["tackle_card", "tackle_detail"] as const).map(role => findMediaByLabel(media, item!.name, [role])).filter((asset): asset is MediaAsset => Boolean(asset)) : [];
|
||||
---
|
||||
<Layout title={item ? `${item.name} — снасти RF4` : "Снасть не найдена — RF4 Spotter"} description={item ? `Подтверждённая карточка ${item.name} в каталоге снастей RF4 с источником и отметками полноты.` : "Такой карточки нет в каталоге снастей RF4."} noindex={!item || unavailable} errorPage={!item || unavailable}>
|
||||
<AtlasBreadcrumbs items={[{ label: "Снасти", href: "/tackle" }, { label: item?.name ?? "Не найдено" }]} />
|
||||
@@ -37,5 +42,6 @@ const sourceHref = safeHttpUrl(item?.source_url);
|
||||
<div><dl><div><dt>Подкатегория</dt><dd>{item.subcategory ?? "Не указана"}</dd></div><div><dt>Уровень открытия</dt><dd>{item.unlock_level ?? "Не указан"}</dd></div><div><dt>Источник</dt><dd>{item.source_system ?? "Не указан"}</dd></div></dl>{missing.length > 0 && <p class="tackle-detail__missing"><strong>Не хватает:</strong> {missing.join(", ")}.</p>}{sourceHref && <a data-action="secondary" href={sourceHref} rel="noreferrer">Открыть первоисточник</a>}</div>
|
||||
</section>
|
||||
<DataPassport sources={item.source_system ? [item.source_system] : []} sourceUrl={item.source_url} observedAt={item.source_checked_at} completeness={missing.length ? null : 100} status={missing.length ? "incomplete" : item.source_checked_at ? "verified" : "unverified"} />
|
||||
{mediaCandidates.length > 0 && <section class="catalog-media-grid content-grid" aria-label={`Изображения снасти: ${item.name}`}>{mediaCandidates.map(asset => <article><span class="overline">{mediaRoleLabel(asset.media_role ?? "")}</span><EntityMedia asset={asset} sourceLink /></article>)}</section>}
|
||||
</>}
|
||||
</Layout>
|
||||
|
||||
@@ -1,21 +1,25 @@
|
||||
---
|
||||
import AtlasBreadcrumbs from "../../../components/AtlasBreadcrumbs.astro";
|
||||
import DataPassport from "../../../components/DataPassport.astro";
|
||||
import EntityMedia from "../../../components/EntityMedia.astro";
|
||||
import Layout from "../../../layouts/Layout.astro";
|
||||
import PageHero from "../../../components/PageHero.astro";
|
||||
import StatePanel from "../../../components/StatePanel.astro";
|
||||
import TackleGlyph from "../../../components/TackleGlyph.astro";
|
||||
import { ApiError, api, type Rig } from "../../../lib/api";
|
||||
import { ApiError, api, type MediaAsset, type Rig } from "../../../lib/api";
|
||||
import { findMediaByLabel, mediaRoleLabel } from "../../../lib/media";
|
||||
import { safeHttpUrl } from "../../../lib/urls";
|
||||
|
||||
const { id } = Astro.params;
|
||||
let rig: Rig | undefined;
|
||||
let rig: Rig | undefined, media: MediaAsset[] = [];
|
||||
let unavailable = false;
|
||||
try {
|
||||
rig = await api<Rig>(`/api/v1/tackle/rigs/${encodeURIComponent(id ?? "")}`);
|
||||
} catch (error) {
|
||||
unavailable = !(error instanceof ApiError) || error.status >= 500;
|
||||
}
|
||||
const [rigResult, mediaResult] = await Promise.allSettled([
|
||||
api<Rig>(`/api/v1/tackle/rigs/${encodeURIComponent(id ?? "")}`),
|
||||
api<MediaAsset[]>("/api/v1/media/catalog?entity_type=tackle"),
|
||||
]);
|
||||
if (rigResult.status === "fulfilled") rig = rigResult.value;
|
||||
else { const error = rigResult.reason; unavailable = !(error instanceof ApiError) || error.status >= 500; }
|
||||
if (mediaResult.status === "fulfilled") media = mediaResult.value;
|
||||
if (!rig && !unavailable) Astro.response.status = 404;
|
||||
if (unavailable) {
|
||||
Astro.response.status = 503;
|
||||
@@ -27,6 +31,7 @@ const roleLabels: Record<string, string> = { lure: "Приманка", bait: "Н
|
||||
const missingLabels: Record<string, string> = { source_url: "ссылка на источник", source_checked_at: "дата проверки" };
|
||||
const missing = rig?.missing_fields.map((field) => missingLabels[field] ?? field) ?? [];
|
||||
const sourceHref = safeHttpUrl(rig?.source_url);
|
||||
const mediaCandidates = rig ? [findMediaByLabel(media, rig.name, ["rig_diagram"])].filter((asset): asset is MediaAsset => Boolean(asset)) : [];
|
||||
---
|
||||
<Layout title={rig ? `${rig.name} — сборки RF4` : "Сборка не найдена — RF4 Spotter"} description={rig ? `Подтверждённая карточка сборки ${rig.name} в каталоге RF4 с составом и источником.` : "Такой карточки сборки нет в каталоге RF4."} noindex={!rig || unavailable} errorPage={!rig || unavailable}>
|
||||
<AtlasBreadcrumbs items={[{ label: "Снасти", href: "/tackle" }, { label: "Сборки" }, { label: rig?.name ?? "Не найдено" }]} />
|
||||
@@ -37,6 +42,7 @@ const sourceHref = safeHttpUrl(rig?.source_url);
|
||||
<div><span class="overline">Компоненты</span><ol class="rig-components">{rig.components.length ? rig.components.map(component => <li><span>{roleLabels[component.role] ?? component.role}</span>{component.tackle_item_id ? <a href={`/tackle/items/${component.tackle_item_id}`}>{component.raw_value ?? "Карточка снасти"}</a> : <span>{component.raw_value ?? "Значение не указано"}</span>}</li>) : <li>Состав не указан.</li>}</ol>{missing.length > 0 && <p class="tackle-detail__missing"><strong>Не хватает:</strong> {missing.join(", ")}.</p>}{sourceHref && <a data-action="secondary" href={sourceHref} rel="noreferrer">Открыть первоисточник</a>}</div>
|
||||
</section>
|
||||
<DataPassport sources={rig.source_system ? [rig.source_system] : []} sourceUrl={rig.source_url} observedAt={rig.source_checked_at} completeness={missing.length ? null : 100} status={missing.length ? "incomplete" : rig.source_checked_at ? "verified" : "unverified"} />
|
||||
{mediaCandidates.length > 0 && <section class="catalog-media-grid content-grid" aria-label={`Изображения сборки: ${rig.name}`}>{mediaCandidates.map(asset => <article><span class="overline">{mediaRoleLabel(asset.media_role ?? "")}</span><EntityMedia asset={asset} sourceLink /></article>)}</section>}
|
||||
</>}
|
||||
</Layout>
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
.entity-media{margin:0;min-width:0}.entity-media__frame{position:relative;display:grid;place-items:center;overflow:hidden;min-height:190px;padding:18px;border:1px solid var(--border-soft);border-radius:16px;background:radial-gradient(circle at 50% 46%,color-mix(in srgb,var(--lime) 15%,var(--surface)) 0 18%,var(--surface-soft) 62%)}.entity-media__frame:after{content:"";position:absolute;inset:12px;border:1px solid color-mix(in srgb,var(--border) 55%,transparent);border-radius:11px;pointer-events:none}.entity-media img{position:relative;z-index:1;display:block;width:100%;height:180px;object-fit:contain;filter:drop-shadow(0 12px 18px #08222624);image-rendering:auto}.entity-media figcaption{display:flex;align-items:center;justify-content:space-between;gap:10px;margin-top:9px;color:var(--text-muted);font-size:11px}.entity-media figcaption>span:last-child{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.entity-media--compact{grid-column:2;grid-row:1/4;width:118px}.entity-media--compact .entity-media__frame{min-height:88px;height:88px;padding:10px;border:0;background:color-mix(in srgb,var(--lime) 8%,var(--surface-soft))}.entity-media--compact img{height:72px}.entity-media--compact figcaption{justify-content:flex-end}.entity-media--compact figcaption>span:last-child{display:none}.entity-media--compact .source-chip{transform:scale(.9);transform-origin:right center}.media-library{padding:34px 0 100px}.media-library__intro{display:flex;justify-content:space-between;align-items:center;gap:20px;margin-bottom:24px;color:var(--text-muted)}.media-library__grid{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:16px}.media-library__card{padding:12px;border:1px solid var(--border-soft);border-radius:18px;background:var(--surface)}.media-library__card .entity-media__frame{min-height:170px}.media-library__card h2{margin:12px 3px 3px;font:400 19px Georgia,serif}.media-library__card>span{margin-left:3px;color:var(--text-subtle);font-size:10px;text-transform:uppercase;letter-spacing:.09em}@media(max-width:1050px){.media-library__grid{grid-template-columns:repeat(3,1fr)}}@media(max-width:720px){.media-library__grid{grid-template-columns:repeat(2,1fr)}.entity-media--compact{width:90px}.entity-media--compact .entity-media__frame{height:76px}.media-library__intro{display:block}}@media(max-width:440px){.media-library__grid{grid-template-columns:1fr}}
|
||||
.entity-feature{display:grid;grid-template-columns:minmax(280px,420px) 1fr;align-items:center;gap:48px;padding-block:36px;border-bottom:1px solid var(--border)}.entity-feature .entity-media__frame{min-height:260px}.entity-feature .entity-media img{height:230px}.entity-feature h2{margin:10px 0 12px;font:400 clamp(34px,4vw,58px)/.95 Georgia,serif}.entity-feature p{max-width:620px;color:var(--text-muted);line-height:1.6}@media(max-width:720px){.entity-feature{grid-template-columns:1fr;gap:24px;padding-block:26px}}
|
||||
.waterbody-media-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:18px;padding-block:28px}.waterbody-media-grid article{min-width:0;padding:18px;border:1px solid var(--border-soft);border-radius:16px;background:var(--surface-soft)}.waterbody-media-grid article>.overline{display:block;margin-bottom:10px}.waterbody-media-grid .entity-media__frame{min-height:220px}@media(max-width:720px){.waterbody-media-grid{grid-template-columns:1fr;padding-block:20px}.waterbody-media-grid .entity-media__frame{min-height:190px}}
|
||||
.catalog-media-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(min(100%,320px),1fr));gap:18px;padding-block:28px 70px}.catalog-media-grid article{min-width:0;padding:18px;border:1px solid var(--border-soft);border-radius:16px;background:var(--surface-soft)}.catalog-media-grid article>.overline{display:block;margin-bottom:10px}.catalog-media-grid .entity-media__frame{min-height:220px}
|
||||
|
||||
.media-role-filter{display:flex;align-items:end;gap:10px;margin:0 0 22px;padding:14px 16px;border:1px solid var(--border-soft);border-radius:12px;background:var(--surface-soft)}.media-role-filter label{display:grid;gap:6px;color:var(--text-muted);font-size:11px;font-weight:750;text-transform:uppercase;letter-spacing:.08em}.media-role-filter select{min-width:220px;height:40px;padding:0 10px;border:1px solid var(--border);border-radius:8px;background:var(--surface);color:var(--text)}.media-role-filter a{padding:11px 4px;color:var(--text-muted);font-size:12px}@media(max-width:720px){.media-role-filter{align-items:stretch;flex-wrap:wrap}.media-role-filter label{flex:1 1 100%}.media-role-filter select{width:100%}}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user