From 7c276ae865dded57436d130acb457801ce3f310b Mon Sep 17 00:00:00 2001 From: C00LGIRL Date: Fri, 12 Jun 2026 11:41:19 +0300 Subject: [PATCH] feat: after-match overlay window, which shows the result --- src/pages/MatchListener.jsx | 11 ++- src/pages/MatchResultOverlay.css | 121 +++++++++++++++++++++++++++++++ src/pages/MatchResultOverlay.jsx | 56 ++++++++++++++ src/pages/WorkspacePage.jsx | 12 +++ src/pages/useMatchStore.js | 25 +++---- 5 files changed, 210 insertions(+), 15 deletions(-) create mode 100644 src/pages/MatchResultOverlay.css create mode 100644 src/pages/MatchResultOverlay.jsx diff --git a/src/pages/MatchListener.jsx b/src/pages/MatchListener.jsx index 886dd21..2e91cbf 100644 --- a/src/pages/MatchListener.jsx +++ b/src/pages/MatchListener.jsx @@ -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; }; \ No newline at end of file diff --git a/src/pages/MatchResultOverlay.css b/src/pages/MatchResultOverlay.css new file mode 100644 index 0000000..eb4761b --- /dev/null +++ b/src/pages/MatchResultOverlay.css @@ -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); } +} diff --git a/src/pages/MatchResultOverlay.jsx b/src/pages/MatchResultOverlay.jsx new file mode 100644 index 0000000..d6725aa --- /dev/null +++ b/src/pages/MatchResultOverlay.jsx @@ -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 ( +
+ +
+ {isWin ? 'ПОБЕДА!' : 'ПОРАЖЕНИЕ'} +
+ +
+ {isWin ? 'RESPECT +' : 'WASTED'} +
+ +
+ Текущий рейтинг: + {newRating} + + ({deltaSign}{ratingDelta}) + +
+ +
+ Переход на страницу поиска через {secondsLeft} сек… +
+
+ ); +} diff --git a/src/pages/WorkspacePage.jsx b/src/pages/WorkspacePage.jsx index d678729..e58fb85 100644 --- a/src/pages/WorkspacePage.jsx +++ b/src/pages/WorkspacePage.jsx @@ -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(); @@ -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'); @@ -66,6 +70,14 @@ function WorkspacePage() { workspaceConfig={{ isSwapped, setIsSwapped }} /> + {matchResult && ( + + )} + ); } diff --git a/src/pages/useMatchStore.js b/src/pages/useMatchStore.js index e2c4b62..85c294a 100644 --- a/src/pages/useMatchStore.js +++ b/src/pages/useMatchStore.js @@ -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 }), })); \ No newline at end of file