Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/App.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -46,6 +47,7 @@ function App() {
<Route path="/profile" element={<ProfilePage/>}/>
<Route path="/battle" element={<BattlePage/>}/>
<Route path="/leaderboard" element={<LeaderboardPage/>}/>
<Route path="/history" element={<MatchHistoryPage/>}/>
<Route path="/match/:matchId" element={<MatchLayout/>}>
<Route path="workspace" element={<WorkspacePage/>}/>
<Route path="draft" element={<DraftPage/>}/>
Expand Down
127 changes: 127 additions & 0 deletions src/pages/MatchHistoryPage.css
Original file line number Diff line number Diff line change
@@ -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;
}
109 changes: 109 additions & 0 deletions src/pages/MatchHistoryPage.jsx
Original file line number Diff line number Diff line change
@@ -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 (
<div className={`mh-row ${won ? 'mh-row--win' : 'mh-row--lose'}`}>
<div className="mh-result">
<span className={`mh-badge ${won ? 'mh-badge--win' : 'mh-badge--lose'}`}>
{won ? 'Победа' : 'Поражение'}
</span>
</div>
<div className="mh-opponent">
<span className="mh-vs">против</span>
<span className="mh-nickname">{entry.opponentNickname}</span>
{entry.finishedAt && <span className="mh-date">{formatDate(entry.finishedAt)}</span>}
</div>
<div className={`mh-rating ${won ? 'mh-rating--win' : 'mh-rating--lose'}`}>
<Arrow size={18} />
<span>{entry.rating ?? '—'}</span>
</div>
</div>
);
}

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 (
<div className="layout-container">
<Header onSettingsClick={() => setShowSettings(true)} />

<main className="mh-main">
<h1 className="mh-title">История матчей</h1>
<p className="mh-subtitle">Ваши прошедшие битвы</p>

{isLoading ? (
<div className="mh-loading">Загрузка истории...</div>
) : !matches || matches.length === 0 ? (
<div className="mh-empty">Пока что вы не сыграли ни одного матча</div>
) : (
<div className="mh-board">
{matches.map((entry) => (
<MatchRow key={entry.matchId} entry={entry} />
))}
</div>
)}
</main>

<Footer />

<SettingsModal
isOpen={showSettings}
onClose={() => setShowSettings(false)}
onLogout={handleLogout}
/>
</div>
);
}
8 changes: 8 additions & 0 deletions src/pages/WorkspacePage.test.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,14 @@ vi.mock('./components/workspace/LeftWorkspace', () => ({ default: () => <div />
vi.mock('./components/workspace/RightWorkspace', () => ({ default: () => <div /> }));
vi.mock('./components/layout/Footer', () => ({ default: () => <div /> }));

vi.mock('./useMatchSession.js', () => ({
useMatchSession: () => ({ userSubmissions: [] }),
}));

vi.mock('./useMatchStore.js', () => ({
useMatchStore: (selector) => selector({ matchResult: null }),
}));

describe('WorkspacePage', () => {
const mockLogout = vi.fn();

Expand Down
5 changes: 4 additions & 1 deletion src/pages/components/layout/Header.jsx
Original file line number Diff line number Diff line change
@@ -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";

Expand Down Expand Up @@ -52,6 +52,9 @@ export default function Header({ onSettingsClick, onNavClick }) {
</span>
<span onClick={() => { if (user) navigate('/leaderboard'); else navigate('/login'); }} style={{ display: 'flex', alignItems: 'center', gap: '6px', cursor: 'pointer' }}>
<Trophy size={18} /> Рейтинг
</span>
<span onClick={() => { if (user) navigate('/history'); else navigate('/login'); }} style={{ display: 'flex', alignItems: 'center', gap: '6px', cursor: 'pointer' }}>
<History size={18} /> История
</span>
</nav>
</div>
Expand Down
Loading