diff --git a/package-lock.json b/package-lock.json index ba128eb..c0f017c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2094,14 +2094,14 @@ "license": "MIT" }, "node_modules/axios": { - "version": "1.13.6", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.6.tgz", - "integrity": "sha512-ChTCHMouEe2kn713WHbQGcuYrr6fXTBiu460OTwWrWob16g1bXn4vtz07Ope7ewMozJAnEquLk5lWQWtBig9DQ==", + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.15.0.tgz", + "integrity": "sha512-wWyJDlAatxk30ZJer+GeCWS209sA42X+N5jU2jy6oHTp7ufw8uzUTVFBX9+wTfAlhiJXGS0Bq7X6efruWjuK9Q==", "license": "MIT", "dependencies": { "follow-redirects": "^1.15.11", "form-data": "^4.0.5", - "proxy-from-env": "^1.1.0" + "proxy-from-env": "^2.1.0" } }, "node_modules/balanced-match": { @@ -2135,9 +2135,9 @@ } }, "node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "version": "1.1.14", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz", + "integrity": "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==", "dev": true, "license": "MIT", "dependencies": { @@ -2900,16 +2900,16 @@ } }, "node_modules/flatted": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", - "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz", + "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==", "dev": true, "license": "ISC" }, "node_modules/follow-redirects": { - "version": "1.15.11", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", - "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.0.tgz", + "integrity": "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==", "funding": [ { "type": "individual", @@ -3683,9 +3683,9 @@ "license": "ISC" }, "node_modules/picomatch": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", - "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", "dev": true, "license": "MIT", "engines": { @@ -3765,10 +3765,13 @@ } }, "node_modules/proxy-from-env": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", - "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", - "license": "MIT" + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-2.1.0.tgz", + "integrity": "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==", + "license": "MIT", + "engines": { + "node": ">=10" + } }, "node_modules/punycode": { "version": "2.3.1", @@ -4191,9 +4194,9 @@ } }, "node_modules/undici": { - "version": "7.22.0", - "resolved": "https://registry.npmjs.org/undici/-/undici-7.22.0.tgz", - "integrity": "sha512-RqslV2Us5BrllB+JeiZnK4peryVTndy9Dnqq62S3yYRRTj0tFQCwEniUy2167skdGOy3vqRzEvl1Dm4sV2ReDg==", + "version": "7.25.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.25.0.tgz", + "integrity": "sha512-xXnp4kTyor2Zq+J1FfPI6Eq3ew5h6Vl0F/8d9XU5zZQf1tX9s2Su1/3PiMmUANFULpmksxkClamIZcaUqryHsQ==", "dev": true, "license": "MIT", "engines": { @@ -4242,9 +4245,9 @@ } }, "node_modules/vite": { - "version": "7.3.1", - "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.1.tgz", - "integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==", + "version": "7.3.2", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.2.tgz", + "integrity": "sha512-Bby3NOsna2jsjfLVOHKes8sGwgl4TT0E6vvpYgnAYDIF/tie7MRaFthmKuHx1NSXjiTueXH3do80FMQgvEktRg==", "dev": true, "license": "MIT", "dependencies": { diff --git a/src/api/axiosConfig.js b/src/api/axiosConfig.js index d9724f6..daf3814 100644 --- a/src/api/axiosConfig.js +++ b/src/api/axiosConfig.js @@ -5,6 +5,7 @@ const BASE_URL = 'http://localhost:8080'; const commonConfig = { baseURL: BASE_URL, withCredentials: true, + validateStatus: (status) => (status >= 200 && status < 300) || status === 304 }; const attachInterceptors = (instance) => { @@ -34,4 +35,4 @@ export const api = attachInterceptors(axios.create({ export const multipartApi = attachInterceptors(axios.create({ ...commonConfig, -})); \ No newline at end of file +})); diff --git a/src/pages/WorkspacePage.css b/src/pages/WorkspacePage.css index 2e219a9..e5a23f7 100644 --- a/src/pages/WorkspacePage.css +++ b/src/pages/WorkspacePage.css @@ -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; } \ No newline at end of file diff --git a/src/pages/WorkspacePage.jsx b/src/pages/WorkspacePage.jsx index 70f6d90..882980d 100644 --- a/src/pages/WorkspacePage.jsx +++ b/src/pages/WorkspacePage.jsx @@ -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'; @@ -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) { @@ -36,7 +38,7 @@ function WorkspacePage() {
{isSwapped ? ( - + ) : ( )} @@ -48,7 +50,7 @@ function WorkspacePage() { {isSwapped ? ( ) : ( - + )}
diff --git a/src/pages/components/workspace/RightWorkspace.jsx b/src/pages/components/workspace/RightWorkspace.jsx index 35bd49d..cea51a7 100644 --- a/src/pages/components/workspace/RightWorkspace.jsx +++ b/src/pages/components/workspace/RightWorkspace.jsx @@ -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 ( - -
-

Заголовок задачи

-

Текст условия задачи будет загружаться сюда...

+
+
setActiveTab('description')} + > + + Условие задачи +
+ +
setActiveTab('submissions')} + > + + Сабмиты +
+ +
+ {activeTab === 'description' ? ( +
+

Заголовок задачи

+

Текст условия задачи будет загружаться сюда...

+
+ ) : ( +
+ {(!submissions || !Array.isArray(submissions) || submissions.length === 0) ? ( +

Посылок пока нет или данные загружаются...

+ ) : ( +
+ {submissions.map((sub, index) => ( +
+
+ {sub.status.replace(/_/g, ' ')} +
+
+ ID: {sub.id} | Язык: {LANGUAGE_MAP[sub.languageId] || sub.languageId} | Время: {formatDate(sub.createdAt)} +
+
+ ))} +
+ )} +
+ )} +
diff --git a/src/pages/useParcelPolling.js b/src/pages/useParcelPolling.js new file mode 100644 index 0000000..ec8a0ed --- /dev/null +++ b/src/pages/useParcelPolling.js @@ -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 }; +};