sync
This commit is contained in:
@@ -0,0 +1,101 @@
|
||||
"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:00–22: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:00–09: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:00–15: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:00–11: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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user