238 lines
8.7 KiB
TypeScript
238 lines
8.7 KiB
TypeScript
import { useParams, Navigate, useNavigate } from 'react-router-dom';
|
||
import CodeEditor from '../views/mission/codeeditor/CodeEditor';
|
||
import Statement from '../views/mission/statement/Statement';
|
||
import { PrimaryButton } from '../components/button/PrimaryButton';
|
||
import { useEffect, useRef, useState } from 'react';
|
||
import { useAppDispatch, useAppSelector } from '../redux/hooks';
|
||
import { fetchMySubmitsByMission, submitMission } from '../redux/slices/submit';
|
||
import { fetchMissionById, setMissionsStatus } from '../redux/slices/missions';
|
||
import Header from '../views/mission/statement/Header';
|
||
import MissionSubmissions from '../views/mission/statement/MissionSubmissions';
|
||
import { useQuery } from '../hooks/useQuery';
|
||
import { fetchMyAttemptsInContest } from '../redux/slices/contests';
|
||
|
||
const Mission = () => {
|
||
const dispatch = useAppDispatch();
|
||
const navigate = useNavigate();
|
||
|
||
// Получаем параметры из URL
|
||
const { missionId } = useParams<{ missionId: string }>();
|
||
const mission = useAppSelector((state) => state.missions.currentMission);
|
||
const missionStatus = useAppSelector(
|
||
(state) => state.missions.statuses.fetchById,
|
||
);
|
||
const attempt = useAppSelector(
|
||
(state) => state.contests.fetchMyAttemptsInContest.attempts[0],
|
||
);
|
||
|
||
const missionIdNumber = Number(missionId);
|
||
|
||
const query = useQuery();
|
||
const back = query.get('back') ?? undefined;
|
||
const contestId = Number(query.get('contestId') ?? undefined);
|
||
|
||
if (!missionId || isNaN(missionIdNumber)) {
|
||
if (back) return <Navigate to={back} replace />;
|
||
return <Navigate to="/home" replace />;
|
||
}
|
||
|
||
const [code, setCode] = useState<string>('');
|
||
const [language, setLanguage] = useState<string>('');
|
||
|
||
const pollingRef = useRef<number | null>(null);
|
||
const submissions = useAppSelector(
|
||
(state) => state.submin.submitsById[missionIdNumber] || [],
|
||
);
|
||
const submissionsRef = useRef(submissions);
|
||
|
||
const startPolling = () => {
|
||
if (pollingRef.current) return;
|
||
|
||
pollingRef.current = setInterval(async () => {
|
||
if (contestId) {
|
||
dispatch(fetchMyAttemptsInContest(contestId));
|
||
}
|
||
dispatch(fetchMySubmitsByMission(missionIdNumber));
|
||
|
||
const hasWaiting = submissionsRef.current.some(
|
||
(s: any) =>
|
||
s.solution.status == 'Waiting' ||
|
||
s.solution.testerState === 'Waiting' ||
|
||
s.solution.status === 'Compiling' ||
|
||
s.solution.testerState === 'Compiling',
|
||
);
|
||
if (!hasWaiting) {
|
||
// Всё проверено — стоп
|
||
if (pollingRef.current) {
|
||
clearInterval(pollingRef.current);
|
||
pollingRef.current = null;
|
||
}
|
||
}
|
||
}, 5000); // 10 секунд
|
||
};
|
||
|
||
useEffect(() => {
|
||
if (contestId) {
|
||
dispatch(fetchMyAttemptsInContest(contestId));
|
||
}
|
||
}, [contestId]);
|
||
|
||
useEffect(() => {
|
||
dispatch(fetchMissionById(missionIdNumber));
|
||
dispatch(fetchMySubmitsByMission(missionIdNumber));
|
||
}, [missionIdNumber]);
|
||
|
||
useEffect(() => {}, [submissions]);
|
||
|
||
useEffect(() => {
|
||
return () => {
|
||
if (pollingRef.current) {
|
||
clearInterval(pollingRef.current);
|
||
pollingRef.current = null;
|
||
}
|
||
};
|
||
}, []);
|
||
useEffect(() => {
|
||
if (missionStatus == 'failed') {
|
||
setMissionsStatus({ key: 'fetchById', status: 'idle' });
|
||
navigate(back ?? '/home/missions');
|
||
}
|
||
}, [missionStatus]);
|
||
|
||
useEffect(() => {
|
||
submissionsRef.current = submissions;
|
||
|
||
if (submissions.length) {
|
||
const hasWaiting = submissions.some(
|
||
(s) =>
|
||
s.solution.status === 'Waiting' ||
|
||
s.solution.testerState === 'Waiting' ||
|
||
s.solution.status === 'Compiling' ||
|
||
s.solution.testerState === 'Compiling',
|
||
);
|
||
|
||
if (hasWaiting) {
|
||
startPolling();
|
||
}
|
||
}
|
||
}, [submissions]);
|
||
|
||
if (!mission || !mission.statements || mission.statements.length === 0) {
|
||
return <div>Загрузка...</div>;
|
||
}
|
||
|
||
interface StatementData {
|
||
id: number;
|
||
legend?: string;
|
||
timeLimit?: number;
|
||
output?: string;
|
||
input?: string;
|
||
sampleTests?: any[];
|
||
name?: string;
|
||
memoryLimit?: number;
|
||
tags?: string[];
|
||
notes?: string;
|
||
html?: string;
|
||
mediaFiles?: any[];
|
||
}
|
||
|
||
let statementData: StatementData = { id: mission.id };
|
||
|
||
try {
|
||
// 1. Берём первый statement с форматом Latex и языком russian
|
||
const latexStatement = mission.statements.find(
|
||
(stmt: any) =>
|
||
stmt && stmt.language === 'russian' && stmt.format === 'Latex',
|
||
);
|
||
|
||
// 2. Берём первый statement с форматом Html и языком russian
|
||
const htmlStatement = mission.statements.find(
|
||
(stmt: any) =>
|
||
stmt && stmt.language === 'russian' && stmt.format === 'Html',
|
||
);
|
||
|
||
if (!latexStatement) throw new Error('Не найден блок Latex на русском');
|
||
if (!htmlStatement) throw new Error('Не найден блок Html на русском');
|
||
|
||
// 3. Парсим данные из problem-properties.json
|
||
const statementTexts = JSON.parse(
|
||
latexStatement.statementTexts['problem-properties.json'],
|
||
);
|
||
|
||
statementData = {
|
||
id: missionIdNumber,
|
||
legend: statementTexts.legend,
|
||
timeLimit: statementTexts.timeLimit,
|
||
output: statementTexts.output,
|
||
input: statementTexts.input,
|
||
sampleTests: statementTexts.sampleTests,
|
||
name: statementTexts.name,
|
||
memoryLimit: statementTexts.memoryLimit,
|
||
tags: mission.tags,
|
||
notes: statementTexts.notes,
|
||
html: htmlStatement.statementTexts['problem.html'],
|
||
mediaFiles: latexStatement.mediaFiles,
|
||
};
|
||
} catch (err) {}
|
||
|
||
return (
|
||
<div className="h-screen grid grid-rows-[60px,1fr]">
|
||
<div className="">
|
||
<Header missionId={missionIdNumber} back={back} />
|
||
</div>
|
||
|
||
<div className="grid grid-cols-2 h-full min-h-0 gap-[20px]">
|
||
<div className="overflow-y-auto min-h-0 overflow-hidden">
|
||
<Statement {...statementData} />
|
||
</div>
|
||
|
||
<div className="overflow-y-auto min-h-0 overflow-hidden pb-[20px]">
|
||
<div className=" grid grid-rows-[1fr,45px,230px] grid-flow-row h-full w-full gap-[20px] ">
|
||
<div className="w-full relative ">
|
||
<CodeEditor
|
||
onChange={(value: string) => {
|
||
setCode(value);
|
||
}}
|
||
onChangeLanguage={(value: string) => {
|
||
setLanguage(value);
|
||
}}
|
||
/>
|
||
</div>
|
||
<div>
|
||
<PrimaryButton
|
||
text="Отправить"
|
||
onClick={async () => {
|
||
await dispatch(
|
||
submitMission({
|
||
missionId: missionIdNumber,
|
||
language: language,
|
||
languageVersion: 'latest',
|
||
sourceCode: code,
|
||
contestAttemptId:
|
||
attempt?.attemptId,
|
||
}),
|
||
).unwrap();
|
||
dispatch(
|
||
fetchMySubmitsByMission(
|
||
missionIdNumber,
|
||
),
|
||
);
|
||
}}
|
||
/>
|
||
</div>
|
||
|
||
<div className="h-full w-full ">
|
||
<MissionSubmissions
|
||
missionId={missionIdNumber}
|
||
contestId={contestId}
|
||
/>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
);
|
||
};
|
||
|
||
export default Mission;
|