--- import Layout from "../layouts/Layout.astro"; import ActivityCard from "../components/ActivityCard.astro"; import FishingIcon from "../components/FishingIcon.astro"; import SourceBadge from "../components/SourceBadge.astro"; import TackleGlyph from "../components/TackleGlyph.astro"; import SignalFeed from "../components/SignalFeed.astro"; import { activityLevel, ago, api, kg, plural, type Activity, type DictionaryItem, type PaginatedActivity, type PublicObservation } from "../lib/api"; const params = Astro.url.searchParams; const hours = params.get("hours") ?? "24"; const waterbody = params.get("waterbody") ?? ""; const fish = params.get("fish") ?? ""; const sort = params.get("sort") ?? "activity"; const requestedOffset = Number(params.get("offset") ?? 0); const offset = Number.isInteger(requestedOffset) && requestedOffset >= 0 ? requestedOffset : 0; const requestedSignalLimit = Number(params.get("signals") ?? 12); const signalLimit = Number.isInteger(requestedSignalLimit) ? Math.min(48, Math.max(12, requestedSignalLimit)) : 12; let items: Activity[] = [], signals: PublicObservation[] = [], fishes: DictionaryItem[] = [], waterbodies: DictionaryItem[] = []; let hasMoreSignals = false, totalItems = 0; const filterError = !["6", "12", "24", "72"].includes(hours) || !["activity", "confidence", "freshness"].includes(sort); let unavailable = false, showNoIndex = false; if (filterError) { showNoIndex = true; Astro.response.status = 422; } try { let signalRows: PublicObservation[] = []; const signalParams = new URLSearchParams({ limit: String(signalLimit + 1) }); if (waterbody) signalParams.set("waterbody", waterbody); if (fish) signalParams.set("fish", fish); [fishes, waterbodies, signalRows] = await Promise.all([api("/api/v1/fishes"), api("/api/v1/waterbodies"), api(`/api/v1/community-observations?${signalParams}`)]); hasMoreSignals = signalRows.length > signalLimit; signals = signalRows.slice(0, signalLimit); if (!filterError) { const query = new URLSearchParams({ hours, waterbody, fish, sort, limit: "20", offset: String(offset) }); const paginated = await api(`/api/v1/activity?${query}`); items = offset > 0 ? [...items, ...paginated.items] : paginated.items; totalItems = paginated.total; } } catch { unavailable = true; showNoIndex = true; Astro.response.status = 503; Astro.response.headers.set("Retry-After", "60"); } const leaderLevel = items.length > 1 && items[0] ? activityLevel(items[0].activity_score) : null; const selectedWaterbody = waterbodies.find(item => item.slug === waterbody)?.name_ru ?? "Все водоёмы"; const selectedFish = fishes.find(item => item.slug === fish)?.name_ru ?? "Любая рыба"; const periodLabel = { "6": "6 часов", "12": "12 часов", "24": "24 часа", "72": "72 часа" }[hours] ?? hours; const sortLabel = { activity: "Сначала активные", confidence: "Сначала надёжные", freshness: "Сначала свежие" }[sort] ?? sort; const filtersChanged = Boolean(waterbody || fish || hours !== "24" || sort !== "activity"); const moreSignalParams = new URLSearchParams(params); moreSignalParams.set("signals", String(Math.min(48, signalLimit + 12))); const moreSignalsHref = `/?${moreSignalParams.toString()}#signals-title`; const datasetJsonLd = { "@context": "https://schema.org", "@type": "Dataset", name: "RF4 Spotter — наблюдения об активности рыбы", description: "Агрегированные и исходные наблюдения по точкам ловли в Russian Fishing 4 с указанием происхождения и полноты данных.", url: "https://rf4spotter.ru/", inLanguage: "ru", creator: { "@type": "Organization", name: "RF4 Spotter" }, isAccessibleForFree: true, }; ---
RF4 Живая карта клёва

Выбирай место,
пока клюёт.

Свежие точки, рабочие приманки и честная оценка данных от игроков.

Туманное озеро на рассвете
Пульс водоёмов{items.length} {plural(items.length, ["точка показывает", "точки показывают", "точек показывают"])} активность
Период и порядок
{selectedWaterbody}{selectedFish}{periodLabel}{sortLabel}{filtersChanged && Сбросить}
За выбранный период

Горячие точки

{items.length} из {totalItems} {plural(totalItems, ["точка", "точки", "точек"])}
{filterError ?

Некорректные фильтры

Выберите период и сортировку из предложенных значений.

Сбросить фильтры
: unavailable ?

Источник временно недоступен

Не показываем устаревшие догадки. Попробуйте позже.

: items.length ? <>
{items.map(item => )}
{offset + items.length < totalItems && { const p = new URLSearchParams(params); p.delete("offset"); p.set("offset", String(offset + items.length)); return p.toString(); })()}#results`}>Показать ещё {offset + items.length} из {totalItems}} :

Пока нет свежих данных

Для выбранных фильтров нет одобренных наблюдений. Расширьте период или выберите другой водоём.

}
{items[0] && leaderLevel && }
{signals.length > 0 && } {hasMoreSignals && signalLimit < 48 && Показать ещё {signalLimit} из доступных}
Как читать данные

Не обещаем рыбу.
Показываем факты.

01

Свежесть

Чем старше сообщение, тем меньше оно влияет на активность.

02

Разные игроки

Десять уловов одного человека не равны десяти подтверждениям.

03

Уверенность

Каждая оценка объясняет, сколько данных за ней стоит.