Files
rf4-spotter/design-reference/app/page.tsx
T
2026-09-03 08:18:35 +07:00

102 lines
16 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"use client";
import { FormEvent, useMemo, useState } from "react";
import { ArrowUpRight, CheckCircle2, ChevronRight, Clock3, FishSymbol, Gauge, MapPin, Plus, Search, Send, Sparkles, Trophy, Users, Waves } from "lucide-react";
import { Button } from "@/components/ui/button";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
type Spot = { id: number; water: string; fish: string; coords: string; bait: string; method: string; score: number; confidence: number; reports: number; anglers: number; last: string; avg: string; peak: string; trend: number[]; note: string };
const spots: Spot[] = [
{ id: 1, water: "Вьюнок", fish: "Щука обыкновенная", coords: "110:103", bait: "Spiker #2 01-015", method: "Вращение · 25", score: 92, confidence: 87, reports: 27, anglers: 11, last: "34 мин", avg: "1,84 кг", peak: "18:0022:00", trend: [28,42,36,57,51,72,84,92], note: "Стабильная серия вечерних уловов. Лучше работает проводка поперёк русла." },
{ id: 2, water: "Куори", fish: "Форель озёрная", coords: "117:92", bait: "Hornet 1-002", method: "Равномерная · 30", score: 78, confidence: 72, reports: 14, anglers: 7, last: "1 ч 12 мин", avg: "2,31 кг", peak: "05:0009:00", trend: [31,47,44,62,69,61,74,78], note: "Утреннее окно держится второй день. Данных меньше, чем по Вьюнку." },
{ id: 3, water: "Белая", fish: "Голавль", coords: "72:58", bait: "Aikkila 4 г 006", method: "Против течения · 20", score: 64, confidence: 81, reports: 18, anglers: 9, last: "2 ч 08 мин", avg: "0,92 кг", peak: "11:0015:00", trend: [72,68,57,49,55,62,61,64], note: "Клёв ровный, но без всплеска. Точка хорошо подтверждена разными игроками." },
{ id: 4, water: "Лосиное озеро", fish: "Окунь", coords: "44:51", bait: "Express Spinner 2-004", method: "Stop&Go · 18", score: 49, confidence: 58, reports: 8, anglers: 4, last: "3 ч 41 мин", avg: "0,41 кг", peak: "07:0011:00", trend: [58,62,55,44,39,46,52,49], note: "Свежих сообщений немного. Подходит скорее для опыта, чем для заработка." },
];
const records = [
["Щука обыкновенная", "18,642 кг", "Ладожское озеро", "Hunter 2-012", "SibirianFox", "сегодня, 08:14"],
["Форель озёрная", "14,081 кг", "Куори", "Hornet 1-002", "Taimen", "сегодня, 06:47"],
["Голавль", "7,923 кг", "Белая", "Aikkila 4 г 006", "Vega_Altai", "вчера, 21:18"],
["Окунь", "4,218 кг", "Вьюнок", "Spiker #2 01-015", "Rybachok", "вчера, 16:03"],
];
function ScoreRing({ value }: { value: number }) {
return <div className="score-ring" style={{ "--score": `${value * 3.6}deg` } as React.CSSProperties}><div><strong>{value}</strong><span>из 100</span></div></div>;
}
function Sparkline({ values }: { values: number[] }) {
const points = values.map((value, index) => `${index * (100 / (values.length - 1))},${48 - value * 0.38}`).join(" ");
return <svg className="sparkline" viewBox="0 0 100 52" preserveAspectRatio="none" aria-label="Динамика активности"><path d={`M0,52 L${points} L100,52 Z`} className="spark-area" /><polyline points={points} className="spark-line" /></svg>;
}
function Brand() {
return <div className="brand" aria-label="Ни хвоста, ни чешуи"><div className="brand-mark"><FishSymbol /></div><div className="brand-name"><span>Ни хвоста,</span><strong>ни чешуи</strong></div></div>;
}
export default function Home() {
const [water, setWater] = useState("all");
const [fish, setFish] = useState("all");
const [period, setPeriod] = useState("24");
const [selectedId, setSelectedId] = useState(1);
const [sent, setSent] = useState(false);
const visibleSpots = useMemo(() => spots.filter((spot) => (water === "all" || spot.water === water) && (fish === "all" || spot.fish === fish)), [water, fish]);
const selected = spots.find((spot) => spot.id === selectedId) ?? spots[0];
function submitCatch(event: FormEvent<HTMLFormElement>) { event.preventDefault(); setSent(true); }
return (
<Tabs defaultValue="pulse" className="site-shell">
<header className="topbar">
<Brand />
<TabsList variant="line" className="main-nav" aria-label="Разделы сайта">
<TabsTrigger value="pulse"><Waves /> Сейчас клюёт</TabsTrigger>
<TabsTrigger value="records"><Trophy /> Рекорды</TabsTrigger>
<TabsTrigger value="submit"><Plus /> Добавить улов</TabsTrigger>
</TabsList>
<div className="live-badge"><span /> Данные обновлены 4 мин назад</div>
</header>
<TabsContent value="pulse"><main>
<section className="intro content-grid">
<div className="intro-copy"><div className="eyebrow"><span>RF4</span> Живая карта клёва</div><h1>Выбирай место,<br /><em>пока клюёт.</em></h1><p>Свежие точки, рабочие приманки и честная оценка данных от игроков.</p></div>
<div className="lake-card"><img src="/lake-dawn.png" alt="Туманное озеро на рассвете с поплавком" /><div className="lake-overlay"><div><span className="overline">Пульс водоёмов</span><strong>3 точки набирают активность</strong></div><div className="pulse-orb"><span /></div></div></div>
</section>
<section className="filters-wrap"><div className="filters content-grid">
<label className="filter-field"><span>Водоём</span><Select value={water} onValueChange={setWater}><SelectTrigger><SelectValue /></SelectTrigger><SelectContent><SelectItem value="all">Все доступные</SelectItem><SelectItem value="Вьюнок">Вьюнок</SelectItem><SelectItem value="Куори">Куори</SelectItem><SelectItem value="Белая">Белая</SelectItem><SelectItem value="Лосиное озеро">Лосиное озеро</SelectItem></SelectContent></Select></label>
<label className="filter-field"><span>Рыба</span><Select value={fish} onValueChange={setFish}><SelectTrigger><SelectValue /></SelectTrigger><SelectContent><SelectItem value="all">Любая рыба</SelectItem><SelectItem value="Щука обыкновенная">Щука</SelectItem><SelectItem value="Форель озёрная">Форель озёрная</SelectItem><SelectItem value="Голавль">Голавль</SelectItem><SelectItem value="Окунь">Окунь</SelectItem></SelectContent></Select></label>
<label className="filter-field"><span>Период</span><Select value={period} onValueChange={setPeriod}><SelectTrigger><SelectValue /></SelectTrigger><SelectContent><SelectItem value="6">Последние 6 часов</SelectItem><SelectItem value="12">Последние 12 часов</SelectItem><SelectItem value="24">Последние сутки</SelectItem><SelectItem value="72">Последние 3 дня</SelectItem></SelectContent></Select></label>
<Button className="search-button"><Search /> Найти клёв</Button>
</div></section>
<section className="dashboard content-grid">
<div className="results-column"><div className="section-heading"><div><span className="overline">За {period === "24" ? "последние сутки" : `${period} часов`}</span><h2>Горячие точки</h2></div><div className="result-count">{visibleSpots.length} {visibleSpots.length === 1 ? "точка" : "точки"}</div></div>
<div className="spot-list">{visibleSpots.map((spot, index) => (
<button key={spot.id} className={`spot-card ${selected.id === spot.id ? "selected" : ""}`} onClick={() => setSelectedId(spot.id)}>
<div className="spot-rank">0{index + 1}</div><div className="spot-main"><div className="spot-topline"><span>{spot.water}</span><span className={`activity-pill level-${Math.ceil(spot.score / 25)}`}><i /> {spot.score >= 80 ? "Очень высокий" : spot.score >= 60 ? "Высокий" : "Средний"}</span></div><h3>{spot.fish}</h3><div className="spot-meta"><span><MapPin /> {spot.coords}</span><span><Clock3 /> {spot.last} назад</span></div><div className="bait-line"><FishSymbol /><div><span>Работает сейчас</span><strong>{spot.bait}</strong></div></div></div>
<div className="spot-stats"><div><strong>{spot.reports}</strong><span>уловов</span></div><div><strong>{spot.anglers}</strong><span>игроков</span></div><Sparkline values={spot.trend} /></div><ChevronRight className="card-arrow" />
</button>
))}{visibleSpots.length === 0 && <div className="empty-state"><Search /><h3>Пока нет свежих данных</h3><p>Попробуйте другой водоём или рыбу.</p></div>}</div>
</div>
<aside className="detail-card"><div className="detail-head"><div><span className="overline">Подробно о точке</span><h2>{selected.water} <em>{selected.coords}</em></h2></div><button className="icon-button" aria-label="Открыть полную карточку"><ArrowUpRight /></button></div>
<div className="detail-score"><ScoreRing value={selected.score} /><div><span>Индекс активности</span><strong>{selected.score >= 80 ? "Клёв отличный" : selected.score >= 60 ? "Клёв хороший" : "Клёв умеренный"}</strong><p>{selected.note}</p></div></div>
<div className="metric-grid"><div><Gauge /><span>Уверенность</span><strong>{selected.confidence}%</strong></div><div><Users /><span>Игроков</span><strong>{selected.anglers}</strong></div><div><Clock3 /><span>Пик клёва</span><strong>{selected.peak}</strong></div><div><FishSymbol /><span>Средний вес</span><strong>{selected.avg}</strong></div></div>
<div className="detail-section"><span className="overline">Лучшая связка</span><div className="lure-chip"><div className="lure-dot" /><div><strong>{selected.bait}</strong><span>{selected.method}</span></div><span>43%</span></div><div className="lure-chip"><div className="lure-dot secondary" /><div><strong>Zeiman Spinner 3-008</strong><span>Равномерная · 22</span></div><span>21%</span></div></div>
<div className="confidence-note"><CheckCircle2 /><span><strong>Данные надёжные.</strong> Точку подтвердили несколько разных игроков.</span></div>
</aside>
</section>
<section className="how-it-works content-grid"><div><span className="overline">Как читать данные</span><h2>Не обещаем рыбу.<br />Показываем факты.</h2></div><div className="principles"><article><span>01</span><h3>Свежесть</h3><p>Чем старше сообщение, тем меньше оно влияет на активность.</p></article><article><span>02</span><h3>Разные игроки</h3><p>Десять уловов одного человека не равны десяти подтверждениям.</p></article><article><span>03</span><h3>Уверенность</h3><p>Каждая оценка объясняет, сколько данных за ней стоит.</p></article></div></section>
</main></TabsContent>
<TabsContent value="records"><main className="subpage content-grid"><div className="subpage-title"><div><span className="eyebrow"><span>RF4</span> Официальные данные</span><h1>Рекорды <em>недели</em></h1></div><p>Таблицы обновляются автоматически. Координаты точек в официальных рекордах не публикуются.</p></div><div className="record-toolbar"><div className="record-stat"><Trophy /><div><strong>428</strong><span>рекордов за неделю</span></div></div><div className="record-stat"><Sparkles /><div><strong>37</strong><span>новых сегодня</span></div></div><span className="source-label">Источник: официальный сайт RF4 · демоданные</span></div><div className="records-table-wrap"><table className="records-table"><thead><tr><th>Рыба</th><th>Вес</th><th>Водоём</th><th>Наживка / приманка</th><th>Игрок</th><th>Дата</th></tr></thead><tbody>{records.map((row) => <tr key={row.join("-")}>{row.map((cell, index) => <td key={cell} data-label={["Рыба", "Вес", "Водоём", "Приманка", "Игрок", "Дата"][index]}>{index === 0 && <FishSymbol />}{cell}</td>)}</tr>)}</tbody></table></div></main></TabsContent>
<TabsContent value="submit"><main className="subpage submit-page content-grid"><div className="submit-intro"><span className="eyebrow"><span>+1</span> Помочь сообществу</span><h1>Добавить<br /><em>свой улов</em></h1><p>Полминуты и рабочая точка появится в общей статистике после проверки.</p><div className="submit-tip"><FishSymbol /><div><strong>Скриншот повышает доверие</strong><span>Можно загрузить экран улова или оставить ссылку на публикацию.</span></div></div></div>
<form className="catch-form" onSubmit={submitCatch}>{sent ? <div className="success-state"><CheckCircle2 /><span className="overline">Улов отправлен</span><h2>Ни хвоста!</h2><p>Спасибо. Запись появится в статистике после проверки.</p><Button type="button" onClick={() => setSent(false)}>Добавить ещё один</Button></div> : <><div className="form-heading"><span>Новый улов</span><small>Поля со звёздочкой обязательны</small></div><div className="form-grid"><label><span>Рыба *</span><input required placeholder="Например, щука" /></label><label><span>Вес *</span><div className="input-suffix"><input required inputMode="decimal" placeholder="2,846" /><i>кг</i></div></label><label><span>Водоём *</span><input required placeholder="Вьюнок" /></label><label><span>Координаты *</span><input required pattern="[0-9]+:[0-9]+" placeholder="110:103" /></label><label className="wide"><span>Приманка или наживка</span><input placeholder="Spiker #2 01-015" /></label><label><span>Проводка</span><input placeholder="Вращение" /></label><label><span>Скорость</span><input inputMode="numeric" placeholder="25" /></label><label className="wide"><span>Комментарий</span><textarea rows={3} placeholder="Время клёва, глубина, особенности точки…" /></label></div><div className="form-actions"><label className="upload-button"><Plus /> Добавить скриншот<input type="file" accept="image/png,image/jpeg,image/webp" /></label><Button type="submit" className="submit-button"><Send /> Отправить на проверку</Button></div></>}</form>
</main></TabsContent>
<footer><Brand /><p>Неофициальный проект для игроков Russian Fishing 4. Все данные на макете демонстрационные.</p><span>НХНЧ · 2026</span></footer>
</Tabs>
);
}