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
11 changes: 10 additions & 1 deletion src/pages/MatchListener.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,12 +29,21 @@ export const MatchListener = ({ matchId, children }) => {
}
});

const resultSub = subscribe('/user/queue/match-result', (msg) => {
try {
handleIncomingMessage(JSON.parse(msg.body));
} catch (err) {
console.error(err);
}
});

return () => {
if (sessionSub) sessionSub.unsubscribe();
if (errorSub) errorSub.unsubscribe();
if (resultSub) resultSub.unsubscribe();
resetStore();
};
}, [isConnected, matchId, handleIncomingMessage, resetStore, subscribe]);
}, [isConnected, matchId, handleIncomingMessage, handleErrorMessage, resetStore, subscribe]);

return children;
};
121 changes: 121 additions & 0 deletions src/pages/MatchResultOverlay.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
/* Шрифт из GTA SA — Pricedown (точный аналог надписей WASTED/RESPECT) */
@import url('https://fonts.googleapis.com/css2?family=Pricedown:wght@400&display=swap');
/*
Если Pricedown не подгрузится из Google Fonts (он там есть как 'Pricedown Bl'),
как запасной вариант используем Impact — выглядит близко.
*/

.overlay-root {
position: fixed;
inset: 0;
z-index: 9999;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 24px;
/* Анимация появления */
animation: overlay-fade-in 0.6s ease forwards;
}

/* Фон победы — тёмно-зелёный дым, как в GTA SA при Respect+ */
.overlay-win {
background: radial-gradient(ellipse at center, #0d2b0d 0%, #050f05 60%, #000 100%);
}

/* Фон поражения — тёмно-красный, как WASTED */
.overlay-lose {
background: radial-gradient(ellipse at center, #2b0000 0%, #110000 60%, #000 100%);
}

/* Главная надпись — ПОБЕДА / ПОРАЖЕНИЕ */
.overlay-headline {
font-family: 'Pricedown Bl', 'Impact', 'Arial Black', sans-serif;
font-size: clamp(72px, 12vw, 160px);
font-weight: 900;
text-transform: uppercase;
letter-spacing: 0.04em;
line-height: 1;
/* Победа — жёлто-золотой с зелёным свечением, поражение — белый с красным */
}
.overlay-win .overlay-headline {
color: #f5d000;
text-shadow:
0 0 20px #f5d00088,
0 0 60px #8ec90066,
3px 3px 0 #7a6600,
-1px -1px 0 #000;
animation: headline-pulse 1.8s ease-in-out infinite alternate;
}
.overlay-lose .overlay-headline {
color: #ffffff;
text-shadow:
0 0 20px #ff000099,
0 0 60px #cc000055,
3px 3px 0 #660000,
-1px -1px 0 #000;
animation: headline-pulse 1.8s ease-in-out infinite alternate;
}

/* "RESPECT +" / "WASTED" — атмосферная подпись */
.overlay-subtitle {
font-family: 'Pricedown Bl', 'Impact', 'Arial Black', sans-serif;
font-size: clamp(28px, 4vw, 52px);
font-weight: 700;
letter-spacing: 0.12em;
text-transform: uppercase;
opacity: 0.75;
}
.overlay-win .overlay-subtitle { color: #8ec900; }
.overlay-lose .overlay-subtitle { color: #cc2200; }

/* Карточка рейтинга */
.overlay-rating-card {
display: flex;
align-items: baseline;
gap: 10px;
background: rgba(0, 0, 0, 0.55);
border: 1px solid rgba(255, 255, 255, 0.12);
border-radius: 10px;
padding: 16px 32px;
backdrop-filter: blur(6px);
font-family: 'Inter', sans-serif;
margin-top: 8px;
}
.rating-label {
font-size: 1rem;
color: #aaa;
letter-spacing: 0.03em;
}
.rating-value {
font-size: 2rem;
font-weight: 700;
color: #fff;
}
.rating-delta {
font-size: 1.4rem;
font-weight: 700;
}
.delta-positive { color: #4cef70; }
.delta-negative { color: #ff4444; }

/* Таймер редиректа */
.overlay-timer {
font-family: 'Inter', sans-serif;
font-size: 0.9rem;
color: rgba(255, 255, 255, 0.4);
letter-spacing: 0.04em;
margin-top: 8px;
}

/* Появление */
@keyframes overlay-fade-in {
from { opacity: 0; transform: scale(1.04); }
to { opacity: 1; transform: scale(1); }
}

/* Пульсация заголовка */
@keyframes headline-pulse {
from { filter: brightness(1); }
to { filter: brightness(1.18); }
}
56 changes: 56 additions & 0 deletions src/pages/MatchResultOverlay.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import { useEffect, useState, useRef } from 'react';
import { useNavigate } from 'react-router-dom';
import './MatchResultOverlay.css';

const REDIRECT_DELAY_MS = 30_000;

export default function MatchResultOverlay({ outcome, newRating, ratingDelta }) {
const navigate = useNavigate();
const [secondsLeft, setSecondsLeft] = useState(Math.round(REDIRECT_DELAY_MS / 1000));
const intervalRef = useRef(null);

const isWin = outcome === 'WIN';

useEffect(() => {
intervalRef.current = setInterval(() => {
setSecondsLeft((s) => {
if (s <= 1) {
clearInterval(intervalRef.current);
navigate('/battle');
return 0;
}
return s - 1;
});
}, 1000);

return () => clearInterval(intervalRef.current);
}, [navigate]);

const deltaSign = isWin ? '+' : '−';
const deltaClass = isWin ? 'delta-positive' : 'delta-negative';

return (
<div className={`overlay-root ${isWin ? 'overlay-win' : 'overlay-lose'}`}>

<div className="overlay-headline">
{isWin ? 'ПОБЕДА!' : 'ПОРАЖЕНИЕ'}
</div>

<div className="overlay-subtitle">
{isWin ? 'RESPECT +' : 'WASTED'}
</div>

<div className="overlay-rating-card">
<span className="rating-label">Текущий рейтинг:</span>
<span className="rating-value">{newRating}</span>
<span className={`rating-delta ${deltaClass}`}>
({deltaSign}{ratingDelta})
</span>
</div>

<div className="overlay-timer">
Переход на страницу поиска через {secondsLeft} сек…
</div>
</div>
);
}
12 changes: 12 additions & 0 deletions src/pages/WorkspacePage.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import Footer from './components/layout/Footer';
import LeftWorkspace from './components/workspace/LeftWorkspace';
import RightWorkspace from './components/workspace/RightWorkspace';
import './WorkspacePage.css';
import MatchResultOverlay from './MatchResultOverlay';
import { useMatchStore } from "./useMatchStore.js";

function WorkspacePage() {
const {matchId} = useParams();
Expand All @@ -19,6 +21,8 @@ function WorkspacePage() {
const [showSettings, setShowSettings] = useState(false);
const { submissions } = useParcelPolling();

const matchResult = useMatchStore((state) => state.matchResult);

useEffect(() => {
if (isDarkMode) {
document.body.classList.remove('light-theme');
Expand Down Expand Up @@ -66,6 +70,14 @@ function WorkspacePage() {
workspaceConfig={{ isSwapped, setIsSwapped }}
/>

{matchResult && (
<MatchResultOverlay
outcome={matchResult.outcome}
newRating={matchResult.newRating}
ratingDelta={matchResult.ratingDelta}
/>
)}

</div>
);
}
Expand Down
25 changes: 11 additions & 14 deletions src/pages/useMatchStore.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,30 +3,27 @@ import { create } from 'zustand';
export const useMatchStore = create((set) => ({

draftSessionDTO: null,

error: {stage : null, message: null},

error: { stage: null, message: null },
matchStarted: false,

matchResult: null,

handleIncomingMessage: (payload) => {

switch (payload.status) {
case "MATCH_STARTED_REDIRECT":
set({matchStarted : true});
case 'MATCH_STARTED_REDIRECT':
set({ matchStarted: true });
break;
case "DRAFT":
set({draftSessionDTO: payload.payload});
case 'DRAFT':
set({ draftSessionDTO: payload.payload });
break;
case 'MATCH_FINISHED':
set({ matchResult: payload.payload });
break;
}
},


handleErrorMessage: (payload) => {

set({ error: {stage: null, message: payload.message} });
set({ error: { stage: null, message: payload.message } });
},


resetStore: () => set({ matchStarted: null, error: null })
resetStore: () => set({ matchStarted: null, error: null, matchResult: null }),
}));
Loading