diff --git a/src/App.jsx b/src/App.jsx index 2f5af89..dd282f7 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -2,6 +2,7 @@ import {BrowserRouter as Router, Routes, Route, Navigate, useParams, Outlet} fro import LoginPage from './pages/LoginPage'; import BattlePage from './pages/BattlePage'; import LeaderboardPage from './pages/LeaderboardPage'; +import MatchHistoryPage from './pages/MatchHistoryPage'; import WorkspacePage from './pages/WorkspacePage'; import './App.css'; import {ProfilePage} from "./pages/ProfilePage.jsx"; @@ -46,6 +47,7 @@ function App() { }/> }/> }/> + }/> }> }/> }/> diff --git a/src/pages/MatchHistoryPage.css b/src/pages/MatchHistoryPage.css new file mode 100644 index 0000000..9bc62e5 --- /dev/null +++ b/src/pages/MatchHistoryPage.css @@ -0,0 +1,127 @@ +.mh-main { + flex-grow: 1; + display: flex; + flex-direction: column; + align-items: center; + padding: 40px 20px 60px; + background: radial-gradient(circle at center, #1a1a2e 0%, var(--bg-main) 100%); + color: var(--text-primary); +} + +.mh-title { + margin-bottom: 6px; + font-family: var(--font-ui); + font-size: 2.5rem; + text-shadow: 0 4px 10px rgba(0, 0, 0, 0.5); +} + +.mh-subtitle { + margin-bottom: 40px; + color: var(--text-secondary); + font-size: 1.1rem; +} + +.mh-loading, +.mh-empty { + margin-top: 40px; + color: var(--text-secondary); + font-size: 1.1rem; + text-align: center; +} + +.mh-board { + width: 100%; + max-width: 640px; + display: flex; + flex-direction: column; + gap: 8px; +} + +.mh-row { + display: grid; + grid-template-columns: 130px 1fr 110px; + align-items: center; + padding: 14px 20px; + background: rgba(255, 255, 255, 0.04); + border: 1px solid rgba(255, 255, 255, 0.08); + border-left-width: 4px; + border-radius: 10px; + transition: transform 0.12s ease, background 0.12s ease; +} + +.mh-row:hover { + background: rgba(255, 255, 255, 0.07); + transform: translateX(2px); +} + +.mh-row--win { + border-left-color: #2ecc71; +} + +.mh-row--lose { + border-left-color: #e74c3c; +} + +.mh-badge { + display: inline-block; + padding: 4px 12px; + font-size: 0.8rem; + font-weight: 700; + text-transform: uppercase; + letter-spacing: 0.5px; + border-radius: 6px; +} + +.mh-badge--win { + background: rgba(46, 204, 113, 0.15); + color: #2ecc71; +} + +.mh-badge--lose { + background: rgba(231, 76, 60, 0.15); + color: #e74c3c; +} + +.mh-opponent { + display: flex; + align-items: center; + gap: 8px; + min-width: 0; +} + +.mh-vs { + color: var(--text-secondary); + font-size: 0.9rem; +} + +.mh-nickname { + font-weight: 600; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.mh-date { + margin-left: auto; + color: var(--text-secondary); + font-size: 0.8rem; + white-space: nowrap; +} + +.mh-rating { + display: flex; + align-items: center; + justify-content: flex-end; + gap: 6px; + font-family: var(--font-ui); + font-size: 1.2rem; + font-weight: 700; +} + +.mh-rating--win { + color: #2ecc71; +} + +.mh-rating--lose { + color: #e74c3c; +} diff --git a/src/pages/MatchHistoryPage.jsx b/src/pages/MatchHistoryPage.jsx new file mode 100644 index 0000000..b0ab5b8 --- /dev/null +++ b/src/pages/MatchHistoryPage.jsx @@ -0,0 +1,109 @@ +import { useState, useEffect } from 'react'; +import { useNavigate } from 'react-router-dom'; +import { ArrowDown, ArrowUp } from 'lucide-react'; +import { useUser } from '../context/UserContext'; +import { api } from '../api/axiosConfig'; +import Header from './components/layout/Header'; +import Footer from './components/layout/Footer'; +import SettingsModal from './components/ui/SettingsModal'; +import toast from 'react-hot-toast'; +import './MatchHistoryPage.css'; + +function formatDate(iso) { + if (!iso) return ''; + try { + return new Date(iso).toLocaleString('ru-RU', { + day: '2-digit', + month: '2-digit', + year: 'numeric', + hour: '2-digit', + minute: '2-digit', + }); + } catch { + return ''; + } +} + +function MatchRow({ entry }) { + const won = entry.won; + const Arrow = won ? ArrowUp : ArrowDown; + return ( +
+
+ + {won ? 'Победа' : 'Поражение'} + +
+
+ против + {entry.opponentNickname} + {entry.finishedAt && {formatDate(entry.finishedAt)}} +
+
+ + {entry.rating ?? '—'} +
+
+ ); +} + +export default function MatchHistoryPage() { + const navigate = useNavigate(); + const { logout } = useUser(); + const [showSettings, setShowSettings] = useState(false); + + const [matches, setMatches] = useState(null); + const [isLoading, setIsLoading] = useState(true); + + useEffect(() => { + let cancelled = false; + (async () => { + try { + const res = await api.get('/match/history'); + if (!cancelled) setMatches(res.data ?? []); + } catch (err) { + if (!cancelled) toast.error('Не удалось загрузить историю матчей'); + console.error('[MatchHistory] load error:', err); + } finally { + if (!cancelled) setIsLoading(false); + } + })(); + return () => { cancelled = true; }; + }, []); + + const handleLogout = () => { + logout(); + navigate('/login'); + }; + + return ( +
+
setShowSettings(true)} /> + +
+

История матчей

+

Ваши прошедшие битвы

+ + {isLoading ? ( +
Загрузка истории...
+ ) : !matches || matches.length === 0 ? ( +
Пока что вы не сыграли ни одного матча
+ ) : ( +
+ {matches.map((entry) => ( + + ))} +
+ )} +
+ +
+ + setShowSettings(false)} + onLogout={handleLogout} + /> +
+ ); +} diff --git a/src/pages/WorkspacePage.test.jsx b/src/pages/WorkspacePage.test.jsx index 9d5f418..e06aa7a 100644 --- a/src/pages/WorkspacePage.test.jsx +++ b/src/pages/WorkspacePage.test.jsx @@ -30,6 +30,14 @@ vi.mock('./components/workspace/LeftWorkspace', () => ({ default: () =>
vi.mock('./components/workspace/RightWorkspace', () => ({ default: () =>
})); vi.mock('./components/layout/Footer', () => ({ default: () =>
})); +vi.mock('./useMatchSession.js', () => ({ + useMatchSession: () => ({ userSubmissions: [] }), +})); + +vi.mock('./useMatchStore.js', () => ({ + useMatchStore: (selector) => selector({ matchResult: null }), +})); + describe('WorkspacePage', () => { const mockLogout = vi.fn(); diff --git a/src/pages/components/layout/Header.jsx b/src/pages/components/layout/Header.jsx index 29882c1..9531fc3 100644 --- a/src/pages/components/layout/Header.jsx +++ b/src/pages/components/layout/Header.jsx @@ -1,5 +1,5 @@ import { useUser } from '../../../context/UserContext'; -import { User, Settings, Swords, LogIn, Trophy } from 'lucide-react'; +import { User, Settings, Swords, LogIn, Trophy, History } from 'lucide-react'; import { useNavigate, useLocation } from 'react-router-dom'; import {useState} from "react"; @@ -52,6 +52,9 @@ export default function Header({ onSettingsClick, onNavClick }) { { if (user) navigate('/leaderboard'); else navigate('/login'); }} style={{ display: 'flex', alignItems: 'center', gap: '6px', cursor: 'pointer' }}> Рейтинг + + { if (user) navigate('/history'); else navigate('/login'); }} style={{ display: 'flex', alignItems: 'center', gap: '6px', cursor: 'pointer' }}> + История