Skip to content
Open
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
55 changes: 29 additions & 26 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 3 additions & 4 deletions src/api/axiosConfig.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,8 @@ import axios from 'axios';
const api = axios.create({
baseURL: '',
withCredentials: true,
headers: {
'Content-Type': 'application/json',
},
headers: { 'Content-Type': 'application/json' },
validateStatus: (status) => (status >= 200 && status < 300) || status === 304
});


Expand Down Expand Up @@ -35,4 +34,4 @@ api.interceptors.response.use(
}
);

export default api;
export default api;
62 changes: 62 additions & 0 deletions src/pages/WorkspacePage.css
Original file line number Diff line number Diff line change
Expand Up @@ -196,4 +196,66 @@
.collapsed-text svg {
display: block;
flex-shrink: 0;
}

.tabs-header {
gap: 20px;
user-select: none;
}

.tab-button {
display: flex;
align-items: center;
gap: 8px;
cursor: pointer;
color: var(--text-secondary);
transition: color 0.2s ease;
}

.tab-button.active {
color: var(--text-primary);
}

.tab-button span {
font-weight: 600;
font-size: 0.85rem;
}

.task-description h3 {
margin-top: 0;
}

.empty-submissions {
color: var(--text-secondary);
}

.submissions-list {
display: flex;
flex-direction: column;
gap: 8px;
}

.submission-item {
padding: 10px;
border-radius: 6px;
background-color: var(--bg-main);
border: 1px solid var(--border-color);
}

.submission-status {
font-weight: bold;
}

.submission-status.accepted {
color: #4CAF50;
}

.submission-status.rejected {
color: #f87171;
}

.submission-details {
font-size: 12px;
color: var(--text-secondary);
margin-top: 4px;
}
6 changes: 4 additions & 2 deletions src/pages/WorkspacePage.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import SettingsModal from './components/ui/SettingsModal';
import { useState, useEffect } from 'react';
import { PanelGroup, PanelResizeHandle } from "react-resizable-panels";
import { useNavigate } from 'react-router-dom';
import { useParcelPolling } from './useParcelPolling';
import Header from './components/layout/Header';
import Footer from './components/layout/Footer';
import LeftWorkspace from './components/workspace/LeftWorkspace';
Expand All @@ -15,6 +16,7 @@ function WorkspacePage() {
const [isDarkMode, setIsDarkMode] = useState(true);
const [isSwapped, setIsSwapped] = useState(false);
const [showSettings, setShowSettings] = useState(false);
const { submissions } = useParcelPolling();

useEffect(() => {
if (isDarkMode) {
Expand All @@ -36,7 +38,7 @@ function WorkspacePage() {
<main className="workspace">
<PanelGroup direction="horizontal">
{isSwapped ? (
<RightWorkspace position="left" />
<RightWorkspace position="left" submissions={submissions} />
) : (
<LeftWorkspace isDarkMode={isDarkMode} position="left" />
)}
Expand All @@ -48,7 +50,7 @@ function WorkspacePage() {
{isSwapped ? (
<LeftWorkspace isDarkMode={isDarkMode} position="right" />
) : (
<RightWorkspace position="right" />
<RightWorkspace position="right" submissions={submissions} />
)}
</PanelGroup>
</main>
Expand Down
71 changes: 65 additions & 6 deletions src/pages/components/workspace/RightWorkspace.jsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,31 @@
import { useState, useRef } from 'react';
import { Panel, PanelGroup, PanelResizeHandle } from "react-resizable-panels";
import { FileText, BarChart2 } from 'lucide-react';
import { FileText, BarChart2, List } from 'lucide-react';
import PanelHeader from '../ui/PanelHeader';

export default function RightWorkspace({ position = 'right' }) {
const LANGUAGE_MAP = {
54: 'C++',
71: 'Python',
63: 'JavaScript'
};

export default function RightWorkspace({ position = 'right', submissions = [] }) {
const [isCollapsed, setIsCollapsed] = useState(false);
const [activeTab, setActiveTab] = useState('description');
const panelRef = useRef(null);

const formatDate = (dateString) => {
if (!dateString) return '';
const date = new Date(dateString);
return date.toLocaleString('ru-RU', {
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
day: '2-digit',
month: '2-digit'
});
};

return (
<Panel
ref={panelRef}
Expand All @@ -31,11 +50,51 @@ export default function RightWorkspace({ position = 'right' }) {
) : (
<PanelGroup direction="vertical">
<Panel defaultSize={50} minSize={15} className="panel">
<PanelHeader title="Условие задачи" Icon={FileText} />
<div className="panel-content">
<h3 style={{ marginTop: 0 }}>Заголовок задачи</h3>
<p>Текст условия задачи будет загружаться сюда...</p>
<div className="panel-header tabs-header">
<div
className={`tab-button ${activeTab === 'description' ? 'active' : ''}`}
onClick={() => setActiveTab('description')}
>
<FileText size={16} />
<span>Условие задачи</span>
</div>

<div
className={`tab-button ${activeTab === 'submissions' ? 'active' : ''}`}
onClick={() => setActiveTab('submissions')}
>
<List size={16} />
<span>Сабмиты</span>
</div>
</div>

<div className="panel-content">
{activeTab === 'description' ? (
<div className="task-description">
<h3>Заголовок задачи</h3>
<p>Текст условия задачи будет загружаться сюда...</p>
</div>
) : (
<div className="submissions-container">
{(!submissions || !Array.isArray(submissions) || submissions.length === 0) ? (
<p className="empty-submissions">Посылок пока нет или данные загружаются...</p>
) : (
<div className="submissions-list">
{submissions.map((sub, index) => (
<div key={sub.id || index} className="submission-item">
<div className={`submission-status ${sub.status === 'ACCEPTED' ? 'accepted' : 'rejected'}`}>
{sub.status.replace(/_/g, ' ')}
</div>
<div className="submission-details">
ID: {sub.id} | Язык: {LANGUAGE_MAP[sub.languageId] || sub.languageId} | Время: {formatDate(sub.createdAt)}
</div>
</div>
))}
</div>
)}
</div>
)}
</div>
</Panel>

<PanelResizeHandle className="resizer-horizontal">
Expand Down
73 changes: 73 additions & 0 deletions src/pages/useParcelPolling.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import { useState, useEffect, useRef } from 'react'
import api from '../api/axiosConfig'

export const useParcelPolling = () => {
const [submissions, setSubmissions] = useState([]);
const [hasError, setHasError] = useState(false);
const isMountedRef = useRef(true);
const lastUpdateRef = useRef(null);

useEffect(() => {
isMountedRef.current = true;
let timeoutId = null;

const poll = async () => {
if (!isMountedRef.current) return;

const controller = new AbortController();

try {
const params = {};
if (lastUpdateRef.current) {
params.lastUpdate = lastUpdateRef.current;
}

const response = await api.get('/my-submissions', {
signal: controller.signal,
params,
timeout: 25000
});


if (!isMountedRef.current) return;

if (response.status === 200) {
setSubmissions(response.data);
setHasError(false);

if (response.data.length > 0) {
const latest = response.data.reduce((max, sub) =>
sub.updatedAt > max ? sub.updatedAt : max,
response.data[0].updatedAt
);
lastUpdateRef.current = latest.endsWith('Z') ? latest : latest + 'Z';
} else if (!lastUpdateRef.current) {
lastUpdateRef.current = new Date().toISOString();
}
}

if (isMountedRef.current) {
poll();
}

} catch (error) {
if (error.name === 'CanceledError' || error.code === 'ERR_CANCELED') return;

if (isMountedRef.current) {
console.error('Polling error:', error);
setHasError(true);
timeoutId = setTimeout(poll, 3000);
}
}
};

poll();

return () => {
isMountedRef.current = false;
if (timeoutId) clearTimeout(timeoutId);
};
}, []);

return { submissions, hasError };
};