feat: expose confirmed fishing approach
This commit is contained in:
@@ -31,6 +31,8 @@ const level = activity ? activityLevel(activity.activity_score) : null;
|
||||
const methodLabels: Record<string, string> = { spinning: "Спиннинг", bottom: "Донная", float: "Поплавочная" };
|
||||
const method = catches.map(item => item.fishing_method).find(Boolean) ?? "";
|
||||
const retrieve = catches.map(item => item.retrieve_method).find(Boolean) ?? "";
|
||||
const confirmedMethods = [...new Set(catches.map(item => item.fishing_method).filter((value): value is string => Boolean(value)))].map(value => methodLabels[value] ?? value);
|
||||
const confirmedRetrieves = [...new Set(catches.map(item => item.retrieve_method).filter((value): value is string => Boolean(value)))];
|
||||
const risk = activity ? activity.catches < 3 ? "Малая выборка" : activity.confidence_score < 50 ? "Низкая уверенность" : "Подтверждено" : "Нет оценки";
|
||||
const spotDescription = spot ? `Свежие уловы и активность на точке ${spot.x}:${spot.y}, ${spot.waterbody}: рыба, вес, приманки и источники данных.` : "Данные точки ловли Russian Fishing 4.";
|
||||
const breadcrumbs = spot ? { "@context": "https://schema.org", "@type": "BreadcrumbList", itemListElement: [
|
||||
@@ -50,16 +52,25 @@ const breadcrumbs = spot ? { "@context": "https://schema.org", "@type": "Breadcr
|
||||
<div class="section-heading"><h2>Последние уловы</h2></div>
|
||||
{catches.length ? <CatchList catches={catches}/> : <div class="state compact-state"><h2>Уловов пока нет</h2><p>Для этой точки нет одобренных наблюдений.</p></div>}
|
||||
</div>
|
||||
<aside><span class="eyebrow">Оценка за 24 часа</span>{activity && level ? <><h2>{activity.activity_score} / 100</h2><strong data-activity-level={level.short}>{level.description}</strong><p>{activity.explanation}</p><DataPassport sources={activity.sources} observedAt={activity.last_confirmed_at} confidence={activity.confidence_score} sampleSize={activity.catches} independentPlayers={activity.unique_players} coordinatePrecision={activity.coordinate_precision} coordinateSources={activity.coordinate_sources} sourceConflicts={activity.source_conflicts} status={activity.catches < 3 ? "insufficient" : "verified"}/></> : <p>За последние 24 часа данных для расчёта нет.</p>}<h3 class="detail-action-heading">Что взять</h3>{spot.top_baits.length ? <ol>{spot.top_baits.map(name => <li><span class="tackle-label"><TackleGlyph name={name} size={24}/><span>{name}</span></span></li>)}</ol> : <p>Недостаточно данных.</p>}<p class="note">Учитываются только одобренные наблюдения.</p></aside>
|
||||
<aside><span class="eyebrow">Оценка за 24 часа</span>{activity && level ? <><h2>{activity.activity_score} / 100</h2><strong data-activity-level={level.short}>{level.description}</strong><p>{activity.explanation}</p><DataPassport sources={activity.sources} observedAt={activity.last_confirmed_at} confidence={activity.confidence_score} sampleSize={activity.catches} independentPlayers={activity.unique_players} coordinatePrecision={activity.coordinate_precision} coordinateSources={activity.coordinate_sources} sourceConflicts={activity.source_conflicts} status={activity.catches < 3 ? "insufficient" : "verified"}/></> : <p>За последние 24 часа данных для расчёта нет.</p>}<h3 class="detail-action-heading">Что взять</h3>{spot.top_baits.length ? <ol>{spot.top_baits.map(name => <li><span class="tackle-label"><TackleGlyph name={name} size={24}/><span>{name}</span></span></li>)}</ol> : <p>Недостаточно данных.</p>}<section class="confirmed-approach" aria-label="Подтверждённый метод и проводка"><h3>Подтверждённый подход</h3>{confirmedMethods.length || confirmedRetrieves.length ? <dl>{confirmedMethods.length > 0 && <div><dt>Метод</dt><dd>{confirmedMethods.join(", ")}</dd></div>}{confirmedRetrieves.length > 0 && <div><dt>Проводка</dt><dd>{confirmedRetrieves.join(", ")}</dd></div>}</dl> : <p>Метод и проводка в уловах не указаны.</p>}<small>По одобренным уловам этой точки.</small></section><p class="note">Учитываются только одобренные наблюдения.</p></aside>
|
||||
</section>
|
||||
</>}
|
||||
</Layout>
|
||||
<script>
|
||||
const copyText = async (value: string) => {
|
||||
if (navigator.clipboard?.writeText) { await navigator.clipboard.writeText(value); return; }
|
||||
const field = document.createElement("textarea");
|
||||
field.value = value; field.setAttribute("readonly", ""); field.style.position = "fixed"; field.style.opacity = "0";
|
||||
document.body.append(field); field.select();
|
||||
const copied = document.execCommand("copy");
|
||||
field.remove();
|
||||
if (!copied) throw new Error("Clipboard is unavailable");
|
||||
};
|
||||
document.querySelectorAll<HTMLButtonElement>("[data-copy-coordinates]").forEach((button) => {
|
||||
button.addEventListener("click", async () => {
|
||||
const value = button.dataset.copyCoordinates || "";
|
||||
const status = button.parentElement?.querySelector<HTMLElement>(".copy-status");
|
||||
try { await navigator.clipboard.writeText(value); if (status) status.textContent = "Скопировано"; }
|
||||
try { await copyText(value); if (status) status.textContent = "Скопировано"; }
|
||||
catch { if (status) status.textContent = value; }
|
||||
});
|
||||
});
|
||||
@@ -88,3 +99,12 @@ const breadcrumbs = spot ? { "@context": "https://schema.org", "@type": "Breadcr
|
||||
});
|
||||
});
|
||||
</script>
|
||||
<style>
|
||||
.confirmed-approach { margin-top: 22px; padding-top: 18px; border-top: 1px solid var(--border); }
|
||||
.confirmed-approach h3 { margin: 0 0 12px; }
|
||||
.confirmed-approach dl { display: grid; gap: 8px; margin: 0 0 8px; }
|
||||
.confirmed-approach dl div { display: grid; grid-template-columns: 92px 1fr; gap: 8px; }
|
||||
.confirmed-approach dt { color: var(--text-subtle); font-size: 11px; font-weight: 750; text-transform: uppercase; }
|
||||
.confirmed-approach dd { margin: 0; color: var(--text-secondary); }
|
||||
.confirmed-approach p, .confirmed-approach small { color: var(--text-muted); }
|
||||
</style>
|
||||
|
||||
@@ -1,16 +1,20 @@
|
||||
---
|
||||
import ActivityCard from "../../../components/ActivityCard.astro";
|
||||
import ActivityPeriodFilter from "../../../components/ActivityPeriodFilter.astro";
|
||||
import AtlasBreadcrumbs from "../../../components/AtlasBreadcrumbs.astro";
|
||||
import PageHero from "../../../components/PageHero.astro";
|
||||
import StatePanel from "../../../components/StatePanel.astro";
|
||||
import Layout from "../../../layouts/Layout.astro";
|
||||
import { api, plural, type Activity, type DictionaryItem, type PaginatedActivity } from "../../../lib/api";
|
||||
const { slug, fish: fishSlug } = Astro.params;
|
||||
const requestedHours = Astro.url.searchParams.get("hours") ?? "72";
|
||||
const hours = ["24", "72", "168"].includes(requestedHours) ? requestedHours : "72";
|
||||
const periodLabel = { "24": "24 часа", "72": "72 часа", "168": "7 дней" }[hours];
|
||||
let water: DictionaryItem | undefined, fish: DictionaryItem | undefined, items: Activity[] = [], unavailable = false;
|
||||
try {
|
||||
const [waters, fishes] = await Promise.all([api<DictionaryItem[]>("/api/v1/waterbodies?limit=500"), api<DictionaryItem[]>("/api/v1/fishes?limit=500")]);
|
||||
water = waters.find(item => item.slug === slug); fish = fishes.find(item => item.slug === fishSlug);
|
||||
if (water && fish) { const paginated = await api<PaginatedActivity>(`/api/v1/activity?hours=72&waterbody=${encodeURIComponent(water.slug)}&fish=${encodeURIComponent(fish.slug)}&limit=100`); items = paginated.items; }
|
||||
if (water && fish) { const paginated = await api<PaginatedActivity>(`/api/v1/activity?hours=${hours}&waterbody=${encodeURIComponent(water.slug)}&fish=${encodeURIComponent(fish.slug)}&limit=100`); items = paginated.items; }
|
||||
} catch { unavailable = true; }
|
||||
if (unavailable) {
|
||||
Astro.response.status = 503;
|
||||
@@ -23,6 +27,7 @@ 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}>
|
||||
<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 ? `${items.length} ${plural(items.length,["свежая точка","свежие точки","свежих точек"])} за последние 72 часа.` : undefined} variant={valid ? "pair" : undefined} identity={fish?.name_ru} secondaryIdentity={water?.slug} />
|
||||
{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="72 часа"/>) : <StatePanel contained={false} title="Свежих точек пока нет" description="Данные появятся после новых подтверждённых наблюдений." />}</div></section>}
|
||||
<PageHero eyebrow={water?.name_ru ?? "Каталог RF4"} title={fish?.name_ru ?? "Данные не найдены"} description={valid ? `${items.length} ${plural(items.length,["свежая точка","свежие точки","свежих точек"])} за ${periodLabel}.` : undefined} variant={valid ? "pair" : undefined} identity={fish?.name_ru} secondaryIdentity={water?.slug} />
|
||||
{valid && <ActivityPeriodFilter hours={hours} />}
|
||||
{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>}
|
||||
</Layout>
|
||||
|
||||
Reference in New Issue
Block a user