Skip to content
6 changes: 6 additions & 0 deletions src/App.css
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,12 @@ html, body, #root {
-webkit-font-smoothing: antialiased;
}

.draft-container {
flex-grow: 1;
overflow-y: auto;
padding: 20px;
}

.page-header {
display: flex;
align-items: center;
Expand Down
322 changes: 197 additions & 125 deletions src/pages/DraftPage.jsx

Large diffs are not rendered by default.

153 changes: 89 additions & 64 deletions src/pages/WorkspacePage.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,76 +10,101 @@ import RightWorkspace from './components/workspace/RightWorkspace';
import './WorkspacePage.css';
import MatchResultOverlay from './MatchResultOverlay';
import { useMatchStore } from "./useMatchStore.js";
import {useMatchSession} from "./useMatchSession.js";
import {useMatchSession} from "./useMatchSession.jsx";
import {api} from "../api/axiosConfig.js";

function WorkspacePage() {
const {matchId} = useParams();
const { logout } = useUser();
const navigate = useNavigate();
const [isDarkMode, setIsDarkMode] = useState(true);
const [isSwapped, setIsSwapped] = useState(false);
const [showSettings, setShowSettings] = useState(false);
const { userSubmissions } = useMatchSession();

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

useEffect(() => {
if (isDarkMode) {
document.body.classList.remove('light-theme');
} else {
document.body.classList.add('light-theme');
}
}, [isDarkMode]);

const handleLogout = () => {
logout();
navigate('/login');
};

return (
<div className="layout-container">
<Header onSettingsClick={() => setShowSettings(true)} />

<main className="workspace">
<PanelGroup direction="horizontal">
{isSwapped ? (
<RightWorkspace position="left" submissions={userSubmissions} />
) : (
<LeftWorkspace isDarkMode={isDarkMode} position="left" submissions = {userSubmissions} matchId = {matchId} />
)}
const {matchId} = useParams();
const { logout } = useUser();
const navigate = useNavigate();
const [isDarkMode, setIsDarkMode] = useState(true);
const [isSwapped, setIsSwapped] = useState(false);
const [showSettings, setShowSettings] = useState(false);
const { userSubmissions } = useMatchSession(matchId);
const [matchOptions, setMatchOptions] = useState(null);
const matchResult = useMatchStore((state) => state.matchResult);

<PanelResizeHandle className="resizer-vertical">
<div className="resizer-line-vertical"></div>
</PanelResizeHandle>
useEffect(() => {
if (isDarkMode) {
document.body.classList.remove('light-theme');
} else {
document.body.classList.add('light-theme');
}
}, [isDarkMode]);

{isSwapped ? (
<LeftWorkspace isDarkMode={isDarkMode} position="right" />
) : (
<RightWorkspace position="right" submissions={userSubmissions} />
)}
</PanelGroup>
</main>

<Footer />

<SettingsModal
isOpen={showSettings}
onClose={() => setShowSettings(false)}
onLogout={handleLogout}
themeConfig={{ isDarkMode, setIsDarkMode }}
workspaceConfig={{ isSwapped, setIsSwapped }}
/>

{matchResult && (
<MatchResultOverlay
outcome={matchResult.outcome}
newRating={matchResult.newRating}
ratingDelta={matchResult.ratingDelta}
useEffect(() => {
let ignore = false;

const fetchOptions = async () => {
try {
const response = await api.get(`/match/${matchId}/options`);
if (!ignore) {
console.log(`options : ${response.data.statement}`);
setMatchOptions(response.data);
}
} catch (error) {
console.error("Ошибка при загрузке опций матча:", error.message);
}
};

if (matchId) {
fetchOptions();
}

return () => {
ignore = true;
};
}, [matchId]);

const handleLogout = () => {
logout();
navigate('/login');
};

return (
<div className="layout-container">
<Header onSettingsClick={() => setShowSettings(true)} />

<main className="workspace">
<PanelGroup direction="horizontal">
{isSwapped ? (
<RightWorkspace position="left" submissions={userSubmissions} matchOptions = {matchOptions} />
) : (
<LeftWorkspace isDarkMode={isDarkMode} position="left" submissions = {userSubmissions} matchId = {matchId} matchOptions = {matchOptions}/>
)}

<PanelResizeHandle className="resizer-vertical">
<div className="resizer-line-vertical"></div>
</PanelResizeHandle>

{isSwapped ? (
<LeftWorkspace isDarkMode={isDarkMode} position="right" matchOptions = {matchOptions} />
) : (
<RightWorkspace position="right" submissions={userSubmissions} matchOptions = {matchOptions} />
)}
</PanelGroup>
</main>

<Footer />

<SettingsModal
isOpen={showSettings}
onClose={() => setShowSettings(false)}
onLogout={handleLogout}
themeConfig={{ isDarkMode, setIsDarkMode }}
workspaceConfig={{ isSwapped, setIsSwapped }}
/>
)}

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

</div>
);
}

export default WorkspacePage;
14 changes: 10 additions & 4 deletions src/pages/WorkspacePage.test.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,12 +30,18 @@ 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('../context/WebSocketContext', () => ({
useWebSocket: () => ({
isConnected: true,
publish: vi.fn(),
}),
}));

vi.mock('./useMatchStore.js', () => ({
useMatchStore: (selector) => selector({ matchResult: null }),
vi.mock('../context/UserContext', () => ({
useUser: () => ({
user: { id: 'test-user-id', nickname: 'CodzillaPro' },
logout: vi.fn(),
}),
}));

describe('WorkspacePage', () => {
Expand Down
37 changes: 37 additions & 0 deletions src/pages/components/ui/ActionTimer.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
export default function ActionTimer({ timeLeft, totalDuration = 20, isMyTurn = true }) {

const percentage = Math.max(0, Math.min(100, (timeLeft / totalDuration) * 100));


const getBarColor = () => {
if (isMyTurn) {

if (percentage > 50) return '#22c55e';
if (percentage > 20) return '#eab308';
return '#f97316';
} else {

if (percentage > 50) return '#a855f7';
if (percentage > 20) return '#b91c1c';
return '#ef4444';
}
};

return (
<div style={{
width: '100%',
height: '5px',
backgroundColor: 'rgba(0, 0, 0, 0.3)',
position: 'relative',
overflow: 'hidden',
zIndex: 10
}}>
<div style={{
width: `${percentage}%`,
height: '100%',
backgroundColor: getBarColor(),
transition: 'width 0.1s linear, background-color 0.5s ease',
}} />
</div>
);
}
85 changes: 57 additions & 28 deletions src/pages/components/workspace/LeftWorkspace.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,23 +10,25 @@ import {multipartApi} from "../../../api/axiosConfig.js";

const DEFAULT_CODE = "# Напишите ваш код здесь...\n";

// const languages = [
//
// { id: 71, name: "Python" },
//
// { id: 54, name: "C++" },
//
// { id: 63, name: "JavaScript" }
//
// ];
//
// const idToLanguage = {
// 71 : 'python',
// 54 : 'cpp',
// 63 : 'javascript'
// };
const enumToLanguage = {
'PY': 'python',
'CPP': 'cpp',
'JAVA': 'java'
};

export default function LeftWorkspace({isDarkMode, position = 'left', matchId, submissions = []}) {
const complexityStyles = {
'EASY': {label: 'Easy', color: '#22c55e', bg: 'rgba(34, 197, 94, 0.1)'},
'MEDIUM': {label: 'Medium', color: '#f97316', bg: 'rgba(249, 115, 22, 0.1)'},
'HARD': {label: 'Hard', color: '#ef4444', bg: 'rgba(239, 68, 68, 0.1)'}
};

const displayLanguages = {
'PY': 'Python',
'CPP': 'C++',
'JAVA': 'Java'
};

export default function LeftWorkspace({isDarkMode, position = 'left', matchId, submissions = [], matchOptions = null}) {
// const [languageId, setLanguageId] = useState(71);
const [code, setCode] = useState(DEFAULT_CODE);
const [showResetModal, setShowResetModal] = useState(false);
Expand Down Expand Up @@ -125,17 +127,43 @@ export default function LeftWorkspace({isDarkMode, position = 'left', matchId, s
<RunButton/>
<SubmitButton onClick={handleSubmit} disabled={isSubmitting}/>
<Timer/>
{/*<select*/}
{/* className="language-select"*/}
{/* value={languageId}*/}
{/* onChange={(e) => setLanguageId(e.target.value)}*/}
{/*>*/}
{/* {languages.map((lang) => (*/}
{/* <option key={lang.id} value={lang.id}>*/}
{/* {lang.name}*/}
{/* </option>*/}
{/* ))}*/}
{/*</select>*/}
{matchOptions && (
<div style={{
display: 'flex',
gap: '8px',
alignItems: 'center',
marginLeft: 'auto',
marginRight: '12px'
}}>
{/* Бейдж Языка */}
<span style={{
fontSize: '12px',
fontWeight: '600',
padding: '4px 8px',
borderRadius: '4px',
backgroundColor: 'rgba(255, 255, 255, 0.08)',
color: '#cbd5e1',
border: '1px solid rgba(255, 255, 255, 0.1)'
}}>
{displayLanguages[matchOptions.language] || matchOptions.language}
</span>

{/* Бейдж Сложности */}
{matchOptions.problemLevel && (
<span style={{
fontSize: '12px',
fontWeight: '600',
padding: '4px 8px',
borderRadius: '4px',
backgroundColor: complexityStyles[matchOptions.problemLevel.toUpperCase()]?.bg || 'rgba(255,255,255,0.05)',
color: complexityStyles[matchOptions.problemLevel.toUpperCase()]?.color || '#fff',
border: `1px solid ${complexityStyles[matchOptions.problemLevel.toUpperCase()]?.color || 'transparent'}`
}}>
{complexityStyles[matchOptions.problemLevel.toUpperCase()]?.label || matchOptions.problemLevel}
</span>
)}
</div>
)}
<button className="btn icon-btn" onClick={() => setShowResetModal(true)}
title="Сбросить код">
<RotateCcw size={16}/>
Expand All @@ -157,6 +185,7 @@ export default function LeftWorkspace({isDarkMode, position = 'left', matchId, s
height="100%"
theme={isDarkMode ? "vs-dark" : "light"}
value={code}
language={enumToLanguage[matchOptions?.language] || 'python'}
onChange={(value) => setCode(value)}
options={{
fontSize: 15,
Expand All @@ -177,7 +206,7 @@ export default function LeftWorkspace({isDarkMode, position = 'left', matchId, s
{testResults ? (
<pre
className={`test-text ${testResults.includes('Accepted') ? 'success' : ''}`}
style={{ whiteSpace: 'pre-wrap' }}
style={{whiteSpace: 'pre-wrap'}}
>
{testResults}
</pre>
Expand Down
Loading
Loading