From 358c7def78ccc04e5192e934c172bb32899b0c00 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=92=D0=B8=D1=82=D0=B0=D0=BB=D0=B8=D0=B9=20=D0=9B=D0=B0?= =?UTF-8?q?=D0=B2=D1=88=D0=BE=D0=BD=D0=BE=D0=BA?= <114582703+valavshonok@users.noreply.github.com> Date: Wed, 3 Dec 2025 21:15:42 +0300 Subject: [PATCH] Add ettempts in contests --- src/pages/Mission.tsx | 17 +- src/redux/slices/contests.ts | 64 +++---- src/redux/slices/submit.ts | 2 +- .../home/account/missions/MyMissionItem.tsx | 4 +- src/views/home/auth/Login.tsx | 2 - src/views/home/auth/Register.tsx | 2 - src/views/home/contest/Missions.tsx | 130 +++++++------ src/views/home/contest/Submissions.tsx | 177 +++++++----------- src/views/home/contest/SubmissionsBlock.tsx | 75 ++++++++ src/views/home/contests/ContestItem.tsx | 65 ++++--- src/views/home/contests/ContestsBlock.tsx | 9 +- src/views/home/groups/Filter.tsx | 16 -- src/views/mission/statement/Header.tsx | 59 +++++- .../mission/statement/MissionSubmissions.tsx | 15 +- 14 files changed, 377 insertions(+), 260 deletions(-) create mode 100644 src/views/home/contest/SubmissionsBlock.tsx diff --git a/src/pages/Mission.tsx b/src/pages/Mission.tsx index b36b4a6..4200496 100644 --- a/src/pages/Mission.tsx +++ b/src/pages/Mission.tsx @@ -9,6 +9,7 @@ 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(); @@ -20,6 +21,10 @@ const Mission = () => { const missionStatus = useAppSelector( (state) => state.missions.statuses.fetchById, ); + const attempt = useAppSelector( + (state) => state.contests.fetchMyAttemptsInContest.attempts[0], + ); + const missionIdNumber = Number(missionId); const query = useQuery(); @@ -44,6 +49,9 @@ const Mission = () => { if (pollingRef.current) return; pollingRef.current = setInterval(async () => { + if (contestId) { + dispatch(fetchMyAttemptsInContest(contestId)); + } dispatch(fetchMySubmitsByMission(missionIdNumber)); const hasWaiting = submissionsRef.current.some( @@ -63,6 +71,12 @@ const Mission = () => { }, 5000); // 10 секунд }; + useEffect(() => { + if (contestId) { + dispatch(fetchMyAttemptsInContest(contestId)); + } + }, [contestId]); + useEffect(() => { dispatch(fetchMissionById(missionIdNumber)); dispatch(fetchMySubmitsByMission(missionIdNumber)); @@ -194,7 +208,8 @@ const Mission = () => { language: language, languageVersion: 'latest', sourceCode: code, - contestId: contestId, + contestAttemptId: + attempt?.attemptId, }), ).unwrap(); dispatch( diff --git a/src/redux/slices/contests.ts b/src/redux/slices/contests.ts index 34fc0fc..cf9ca08 100644 --- a/src/redux/slices/contests.ts +++ b/src/redux/slices/contests.ts @@ -75,6 +75,7 @@ export interface Attempt { startedAt: string; expiresAt: string; finished: boolean; + submissions?: Submission[]; results?: any[]; } @@ -203,12 +204,11 @@ interface ContestsState { }; fetchParticipating: { - contests: Contest[], - hasNextPage: boolean, - status: Status, - error?: string, + contests: Contest[]; + hasNextPage: boolean; + status: Status; + error?: string; }; - } const emptyContest: Contest = { @@ -253,12 +253,11 @@ const initialState: ContestsState = { checkRegistration: { registered: false, status: 'idle' }, fetchUpcomingEligible: { contests: [], status: 'idle' }, fetchParticipating: { - contests: [], - hasNextPage: false, - status: 'idle', - error: undefined, -}, - + contests: [], + hasNextPage: false, + status: 'idle', + error: undefined, + }, }; // ===================== @@ -277,19 +276,18 @@ export const fetchParticipatingContests = createAsyncThunk( const { page = 0, pageSize = 10 } = params; const response = await axios.get( '/contests/participating', - { params: { page, pageSize } } + { params: { page, pageSize } }, ); return response.data; } catch (err: any) { return rejectWithValue( - err.response?.data?.message || 'Failed to fetch participating contests' + err.response?.data?.message || + 'Failed to fetch participating contests', ); } - } + }, ); - - export const fetchMySubmissions = createAsyncThunk( 'contests/fetchMySubmissions', async (contestId: number, { rejectWithValue }) => { @@ -928,25 +926,27 @@ const contestsSlice = createSlice({ }, ); - builder.addCase(fetchParticipatingContests.pending, (state) => { - state.fetchParticipating.status = 'loading'; -}); + state.fetchParticipating.status = 'loading'; + }); -builder.addCase( - fetchParticipatingContests.fulfilled, - (state, action: PayloadAction) => { - state.fetchParticipating.status = 'successful'; - state.fetchParticipating.contests = action.payload.contests; - state.fetchParticipating.hasNextPage = action.payload.hasNextPage; - } -); - -builder.addCase(fetchParticipatingContests.rejected, (state, action: any) => { - state.fetchParticipating.status = 'failed'; - state.fetchParticipating.error = action.payload; -}); + builder.addCase( + fetchParticipatingContests.fulfilled, + (state, action: PayloadAction) => { + state.fetchParticipating.status = 'successful'; + state.fetchParticipating.contests = action.payload.contests; + state.fetchParticipating.hasNextPage = + action.payload.hasNextPage; + }, + ); + builder.addCase( + fetchParticipatingContests.rejected, + (state, action: any) => { + state.fetchParticipating.status = 'failed'; + state.fetchParticipating.error = action.payload; + }, + ); }, }); diff --git a/src/redux/slices/submit.ts b/src/redux/slices/submit.ts index 21522b1..6cf53d7 100644 --- a/src/redux/slices/submit.ts +++ b/src/redux/slices/submit.ts @@ -8,7 +8,7 @@ export interface Submit { language: string; languageVersion: string; sourceCode: string; - contestId?: number; + contestAttemptId?: number; } export interface Solution { diff --git a/src/views/home/account/missions/MyMissionItem.tsx b/src/views/home/account/missions/MyMissionItem.tsx index 556820b..a882bb0 100644 --- a/src/views/home/account/missions/MyMissionItem.tsx +++ b/src/views/home/account/missions/MyMissionItem.tsx @@ -1,8 +1,7 @@ import { cn } from '../../../../lib/cn'; import { useNavigate } from 'react-router-dom'; import { Edit } from '../../../../assets/icons/input'; -import { useAppDispatch, useAppSelector } from '../../../../redux/hooks'; -import { deleteMission } from '../../../../redux/slices/missions'; +import { useAppSelector } from '../../../../redux/hooks'; export interface MissionItemProps { id: number; @@ -43,7 +42,6 @@ const MissionItem: React.FC = ({ setDeleteModalActive, }) => { const navigate = useNavigate(); - const dispatch = useAppDispatch(); const difficultyItems = ['Easy', 'Medium', 'Hard']; const difficultyString = difficultyItems[Math.min(Math.max(0, difficulty - 1), 2)]; diff --git a/src/views/home/auth/Login.tsx b/src/views/home/auth/Login.tsx index 4ca776b..6ed61d1 100644 --- a/src/views/home/auth/Login.tsx +++ b/src/views/home/auth/Login.tsx @@ -8,8 +8,6 @@ import { loginUser } from '../../../redux/slices/auth'; // import { cn } from "../../../lib/cn"; import { setMenuActivePage } from '../../../redux/slices/store'; import { Balloon } from '../../../assets/icons/auth'; -import { SecondaryButton } from '../../../components/button/SecondaryButton'; -import { googleLogo } from '../../../assets/icons/input'; const Login = () => { const dispatch = useAppDispatch(); diff --git a/src/views/home/auth/Register.tsx b/src/views/home/auth/Register.tsx index be971e7..1b5f1c5 100644 --- a/src/views/home/auth/Register.tsx +++ b/src/views/home/auth/Register.tsx @@ -9,9 +9,7 @@ import { registerUser } from '../../../redux/slices/auth'; import { setMenuActivePage } from '../../../redux/slices/store'; import { Balloon } from '../../../assets/icons/auth'; import { Link } from 'react-router-dom'; -import { SecondaryButton } from '../../../components/button/SecondaryButton'; import { Checkbox } from '../../../components/checkbox/Checkbox'; -import { googleLogo } from '../../../assets/icons/input'; function isValidEmail(email: string): boolean { const pattern = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/; diff --git a/src/views/home/contest/Missions.tsx b/src/views/home/contest/Missions.tsx index def6cdd..80b4867 100644 --- a/src/views/home/contest/Missions.tsx +++ b/src/views/home/contest/Missions.tsx @@ -2,8 +2,6 @@ import { FC, useEffect, useState } from 'react'; import MissionItem from './MissionItem'; import { Contest, - fetchContestById, - fetchContests, fetchMyAttemptsInContest, fetchMySubmissions, setContestStatus, @@ -24,52 +22,54 @@ interface ContestMissionsProps { contest?: Contest; } - - const ContestMissions: FC = ({ contest }) => { const navigate = useNavigate(); const dispatch = useAppDispatch(); - const { submissions, status } = useAppSelector( + const { status } = useAppSelector( (state) => state.contests.fetchMySubmissions, ); - const attempts = useAppSelector((state) => state.contests.fetchMyAttemptsInContest.attempts); + const attempts = useAppSelector( + (state) => state.contests.fetchMyAttemptsInContest.attempts, + ); + const submissions = useAppSelector( + (state) => + state.contests.fetchMyAttemptsInContest.attempts[0]?.submissions, + ); + const [attemptsStarted, setAttemptsStarted] = useState(false); - const [time, setTime] = useState(0); useEffect(() => { - const calc = (time: string) => { - return time != "" && new Date() <= new Date(time); - } - if (attempts.length && calc(attempts[0].expiresAt)){ - - setAttemptsStarted(true); - - const diffMs = new Date(attempts[0].expiresAt).getTime() - new Date().getTime(); - - const seconds = Math.floor((diffMs / 1000)); + return time != '' && new Date() <= new Date(time); + }; + if (attempts.length && calc(attempts[0].expiresAt)) { + setAttemptsStarted(true); - setTime(seconds) + const diffMs = + new Date(attempts[0].expiresAt).getTime() - + new Date().getTime(); + + const seconds = Math.floor(diffMs / 1000); + + setTime(seconds); const interval = setInterval(() => { setTime((t) => { if (t <= 1) { - clearInterval(interval); // остановка таймера - setAttemptsStarted(false); // можно закрыть попытку или уведомить пользователя - return 0; - } - return t - 1; + clearInterval(interval); // остановка таймера + setAttemptsStarted(false); // можно закрыть попытку или уведомить пользователя + return 0; + } + return t - 1; }); }, 1000); return () => clearInterval(interval); - } - else - setAttemptsStarted(false); - }, [attempts]) + } else setAttemptsStarted(false); + }, [attempts]); useEffect(() => { if (contest) dispatch(fetchMySubmissions(contest.id)); @@ -88,7 +88,7 @@ const ContestMissions: FC = ({ contest }) => { } const solvedCount = (contest.missions ?? []).filter((mission) => - submissions.some( + submissions?.some( (s) => s.solution.missionId === mission.id && s.solution.status === 'Accepted: All tests passed', @@ -97,11 +97,9 @@ const ContestMissions: FC = ({ contest }) => { const totalCount = contest.missions?.length ?? 0; - - // форматирование: mm:ss - const minutes = String(Math.floor(time / 60)).padStart(2, "0"); - const seconds = String(time % 60).padStart(2, "0"); - + // форматирование: mm:ss + const minutes = String(Math.floor(time / 60)).padStart(2, '0'); + const seconds = String(time % 60).padStart(2, '0'); return (
@@ -123,43 +121,60 @@ const ContestMissions: FC = ({ contest }) => {
- {attemptsStarted + {attemptsStarted ? `${minutes}:${seconds}` - : `Длительность попытки: ${contest.attemptDurationMinutes ?? 0} минут`} + : `Длительность попытки: ${ + contest.attemptDurationMinutes ?? 0 + } минут. Осталось попыток ${ + (contest.maxAttempts ?? 0) - + (attempts?.length ?? 0) + }/${contest.maxAttempts ?? 0}`}
-
{`${solvedCount}/${totalCount} Решено`}
-
- { attempts.length == 0 || !attemptsStarted ? { - dispatch(startContestAttempt(contest.id)).unwrap().then(() => - { - dispatch(fetchMyAttemptsInContest(contest.id)); - } - ); - }} - text="Начать попытку" - /> : <> - } { - navigate(`/contest/${contest.id}/submissions`); - }} - text="Мои посылки" - />
- +
+ {attempts.length == 0 || !attemptsStarted ? ( + { + dispatch(startContestAttempt(contest.id)) + .unwrap() + .then(() => { + dispatch( + fetchMyAttemptsInContest( + contest.id, + ), + ); + }); + }} + text="Начать попытку" + disabled={ + (contest.maxAttempts ?? 0) - + (attempts?.length ?? 0) <= + 0 + } + /> + ) : ( + <> + )}{' '} + { + navigate(`/contest/${contest.id}/submissions`); + }} + text="Мои посылки" + /> +
{(contest.missions ?? []).map((v, i) => { - const missionSubmissions = submissions.filter( + const missionSubmissions = submissions?.filter( (s) => s.solution.missionId === v.id, ); - const hasSuccess = missionSubmissions.some( + const hasSuccess = missionSubmissions?.some( (s) => s.solution.status == 'Accepted: All tests passed', @@ -167,7 +182,8 @@ const ContestMissions: FC = ({ contest }) => { const status = hasSuccess ? 'success' - : missionSubmissions.length > 0 + : missionSubmissions?.length && + missionSubmissions.length > 0 ? 'error' : undefined; diff --git a/src/views/home/contest/Submissions.tsx b/src/views/home/contest/Submissions.tsx index d7f31dc..5cb56ea 100644 --- a/src/views/home/contest/Submissions.tsx +++ b/src/views/home/contest/Submissions.tsx @@ -1,130 +1,81 @@ -import SubmissionItem from "./SubmissionItem"; -import { useAppDispatch, useAppSelector } from "../../../redux/hooks"; -import { FC, useEffect } from "react"; -import { - Contest, - fetchMySubmissions, - setContestStatus, -} from "../../../redux/slices/contests"; -import { arrowLeft } from "../../../assets/icons/header"; -import { useNavigate } from "react-router-dom"; +import { useAppDispatch, useAppSelector } from '../../../redux/hooks'; +import { FC, useEffect } from 'react'; +import { Contest, fetchMySubmissions } from '../../../redux/slices/contests'; +import { arrowLeft } from '../../../assets/icons/header'; +import { useNavigate } from 'react-router-dom'; +import SubmissionsBlock from './SubmissionsBlock'; export interface Mission { - id: number; - authorId: number; - name: string; - difficulty: "Easy" | "Medium" | "Hard"; - tags: string[]; - timeLimit: number; - memoryLimit: number; - createdAt: string; - updatedAt: string; + id: number; + authorId: number; + name: string; + difficulty: 'Easy' | 'Medium' | 'Hard'; + tags: string[]; + timeLimit: number; + memoryLimit: number; + createdAt: string; + updatedAt: string; } interface SubmissionsProps { - contest: Contest; + contest: Contest; } const Submissions: FC = ({ contest }) => { - const dispatch = useAppDispatch(); - const navigate = useNavigate(); + const dispatch = useAppDispatch(); + const navigate = useNavigate(); - const { submissions, status } = useAppSelector( - (state) => state.contests.fetchMySubmissions - ); + const attempts = useAppSelector( + (state) => state.contests.fetchMyAttemptsInContest.attempts, + ); + const submissions = useAppSelector( + (state) => + state.contests.fetchMyAttemptsInContest.attempts[0]?.submissions, + ); + useEffect(() => { + if (contest && contest.id) dispatch(fetchMySubmissions(contest.id)); + }, [contest]); - useEffect(() => { - if (contest && contest.id) dispatch(fetchMySubmissions(contest.id)); - }, [contest]); + const solvedCount = (contest.missions ?? []).filter((mission) => + submissions?.some( + (s) => + s.solution.missionId === mission.id && + s.solution.status === 'Accepted: All tests passed', + ), + ).length; - useEffect(() => { - if (status == "successful") { - dispatch(setContestStatus({ key: "fetchMySubmissions", status: "idle" })); - } - }, [status]); + const totalCount = contest.missions?.length ?? 0; - const checkStatus = (status: string) => { - if (status == "IncorrectAnswer") return "wronganswer"; - if (status == "TimeLimitError") return "timelimit"; - return undefined; - }; - - const solvedCount = (contest.missions ?? []).filter((mission) => - submissions.some( - (s) => - s.solution.missionId === mission.id && - s.solution.status === "Accepted: All tests passed" - ) - ).length; - - const totalCount = contest.missions?.length ?? 0; - - return ( -
-
-
- {contest.name} + return ( +
+
+
+ {contest.name} +
+
+
+ { + navigate(`/contest/${contest.id}`); + }} + /> + + Контест #{contest.id} + +
+
{`${solvedCount}/${totalCount} Решено`}
+
+
+
+ {attempts?.map((v, i) => ( + + ))} +
-
-
- { - navigate(`/contest/${contest.id}`); - }} - /> - - Контест #{contest.id} - -
-
{`${solvedCount}/${totalCount} Решено`}
-
-
- -
-
-
Посылка
-
Когда
-
Задача
-
Язык
-
Вердикт
-
Время
-
Память
-
- - {!submissions || submissions.length == 0 ? ( -
Вы еще ничего не отсылали
- ) : ( - <> - {submissions.map((v, i) => ( - - ))} - - )} -
-
- ); + ); }; export default Submissions; diff --git a/src/views/home/contest/SubmissionsBlock.tsx b/src/views/home/contest/SubmissionsBlock.tsx new file mode 100644 index 0000000..32e15b0 --- /dev/null +++ b/src/views/home/contest/SubmissionsBlock.tsx @@ -0,0 +1,75 @@ +import SubmissionItem from './SubmissionItem'; +import { FC } from 'react'; +import { Attempt } from '../../../redux/slices/contests'; + +interface SubmissionsBlockProps { + attempt: Attempt; +} + +const SubmissionsBlock: FC = ({ attempt }) => { + const submissions = attempt?.submissions; + const isFinished = new Date(attempt.expiresAt) < new Date(); + + const checkStatus = (status: string) => { + if (status == 'IncorrectAnswer') return 'wronganswer'; + if (status == 'TimeLimitError') return 'timelimit'; + return undefined; + }; + + return ( +
+
{`Попытка #${attempt.attemptId}`}
+ {!submissions || submissions.length == 0 ? ( + <> + ) : ( +
+
Посылка
+
Когда
+
Задача
+
Язык
+
Вердикт
+
Время
+
Память
+
+ )} + + {!submissions || submissions.length == 0 ? ( +
+ {isFinished + ? 'Вы ничего не посылали в этот сеанс' + : 'Вы еще ничего не отсылали'} +
+ ) : ( + <> + {submissions.map((v, i) => ( + + ))} + + )} +
+
+ ); +}; + +export default SubmissionsBlock; diff --git a/src/views/home/contests/ContestItem.tsx b/src/views/home/contests/ContestItem.tsx index 4e14122..d4e1a8c 100644 --- a/src/views/home/contests/ContestItem.tsx +++ b/src/views/home/contests/ContestItem.tsx @@ -3,12 +3,12 @@ import { Account } from '../../../assets/icons/auth'; import { PrimaryButton } from '../../../components/button/PrimaryButton'; import { ReverseButton } from '../../../components/button/ReverseButton'; import { useNavigate } from 'react-router-dom'; -import { toastSuccess, toastWarning } from '../../../lib/toastNotification'; +import { toastWarning } from '../../../lib/toastNotification'; import { useEffect, useState } from 'react'; import { useAppDispatch, useAppSelector } from '../../../redux/hooks'; import { addOrUpdateContestMember } from '../../../redux/slices/contests'; -export type Role = "None" | "Participant" | "Organizer"; +export type Role = 'None' | 'Participant' | 'Organizer'; export interface ContestItemProps { id: number; @@ -67,31 +67,29 @@ const ContestItem: React.FC = ({ const waitTime = new Date(startAt).getTime() - now.getTime(); - const [myRole, setMyRole] = useState("None"); + const [myRole, setMyRole] = useState('None'); const userId = useAppSelector((state) => state.auth.id); - const {contests: contestsRegistered} = useAppSelector((state) => state.contests.fetchParticipating); - const {contests: contestsMy} = useAppSelector((state) => state.contests.fetchMyContests); - + const { contests: contestsRegistered } = useAppSelector( + (state) => state.contests.fetchParticipating, + ); + const { contests: contestsMy } = useAppSelector( + (state) => state.contests.fetchMyContests, + ); useEffect(() => { if (!contestsRegistered || contestsRegistered.length === 0) { - setMyRole("None"); + setMyRole('None'); return; } - - const reg = contestsRegistered.find(c => c.id === id); - const my = contestsMy.find(c => c.id === id); - - if (my) - setMyRole("Organizer"); - else if (reg) - setMyRole("Participant"); - else - setMyRole("None"); - }, [contestsRegistered]) + const reg = contestsRegistered.find((c) => c.id === id); + const my = contestsMy.find((c) => c.id === id); + if (my) setMyRole('Organizer'); + else if (reg) setMyRole('Participant'); + else setMyRole('None'); + }, [contestsRegistered]); return (
= ({ : ' bg-liquid-background', )} onClick={() => { - if (myRole == 'None'){ - toastWarning("Зарегистрируйтесь на контест"); - return; + if (myRole == 'None') { + toastWarning('Зарегистрируйтесь на контест'); + return; } navigate(`/contest/${id}`); }} @@ -133,15 +131,30 @@ const ContestItem: React.FC = ({ {myRole == 'None' ? ( <> {' '} - { - dispatch(addOrUpdateContestMember({contestId: id, member: {userId: Number(userId), role:"Participant"}})) - }} text="Регистрация" /> + { + dispatch( + addOrUpdateContestMember({ + contestId: id, + member: { + userId: Number(userId), + role: 'Participant', + }, + }), + ); + }} + text="Регистрация" + /> ) : ( <> {' '} - { - navigate(`/contest/${id}`);}} text="Войти" /> + { + navigate(`/contest/${id}`); + }} + text="Войти" + /> )}
diff --git a/src/views/home/contests/ContestsBlock.tsx b/src/views/home/contests/ContestsBlock.tsx index 359734c..7b642b5 100644 --- a/src/views/home/contests/ContestsBlock.tsx +++ b/src/views/home/contests/ContestsBlock.tsx @@ -3,7 +3,6 @@ import { cn } from '../../../lib/cn'; import { ChevroneDown } from '../../../assets/icons/groups'; import ContestItem from './ContestItem'; import { Contest } from '../../../redux/slices/contests'; -import { useAppSelector } from '../../../redux/hooks'; interface ContestsBlockProps { contests: Contest[]; @@ -58,8 +57,12 @@ const ContestsBlock: FC = ({ name={v.name} startAt={v.startsAt ?? new Date().toString()} duration={ - new Date(v.endsAt ?? new Date().toString()).getTime() - - new Date(v.startsAt ?? new Date().toString()).getTime() + new Date( + v.endsAt ?? new Date().toString(), + ).getTime() - + new Date( + v.startsAt ?? new Date().toString(), + ).getTime() } members={v.members?.length ?? 0} type={i % 2 ? 'second' : 'first'} diff --git a/src/views/home/groups/Filter.tsx b/src/views/home/groups/Filter.tsx index d319f55..8d9d105 100644 --- a/src/views/home/groups/Filter.tsx +++ b/src/views/home/groups/Filter.tsx @@ -1,22 +1,6 @@ -import { - FilterDropDown, - FilterItem, -} from '../../../components/drop-down-list/Filter'; -import { SorterDropDown } from '../../../components/drop-down-list/Sorter'; import { SearchInput } from '../../../components/input/SearchInput'; const Filters = () => { - const items: FilterItem[] = [ - { text: 'React', value: 'react' }, - { text: 'Vue', value: 'vue' }, - { text: 'Angular', value: 'angular' }, - { text: 'Svelte', value: 'svelte' }, - { text: 'Next.js', value: 'next' }, - { text: 'Nuxt', value: 'nuxt' }, - { text: 'Solid', value: 'solid' }, - { text: 'Qwik', value: 'qwik' }, - ]; - return (
{}} placeholder="Поиск группы" /> diff --git a/src/views/mission/statement/Header.tsx b/src/views/mission/statement/Header.tsx index 979957e..c7f2510 100644 --- a/src/views/mission/statement/Header.tsx +++ b/src/views/mission/statement/Header.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { useEffect, useState } from 'react'; import { chevroneLeft, chevroneRight, @@ -6,6 +6,9 @@ import { } from '../../../assets/icons/header'; import { Logo } from '../../../assets/logos'; import { useNavigate } from 'react-router-dom'; +import { useAppDispatch, useAppSelector } from '../../../redux/hooks'; +import { useQuery } from '../../../hooks/useQuery'; +import { fetchMyAttemptsInContest } from '../../../redux/slices/contests'; interface HeaderProps { missionId: number; @@ -14,6 +17,57 @@ interface HeaderProps { const Header: React.FC = ({ missionId, back }) => { const navigate = useNavigate(); + const dispatch = useAppDispatch(); + + const query = useQuery(); + const contestId = Number(query.get('contestId') ?? undefined); + + const attempt = useAppSelector( + (state) => state.contests.fetchMyAttemptsInContest.attempts[0], + ); + + const [time, setTime] = useState(0); + + useEffect(() => { + if (!contestId) return; + const calc = (time: string) => { + return time != '' && new Date() <= new Date(time); + }; + if (attempt) { + if (!calc(attempt.expiresAt)) { + navigate('/home/contests'); + } + const diffMs = + new Date(attempt.expiresAt).getTime() - new Date().getTime(); + + const seconds = Math.floor(diffMs / 1000); + + setTime(seconds); + + const interval = setInterval(() => { + setTime((t) => { + if (t <= 1) { + clearInterval(interval); + navigate('/home/contests'); + return 0; + } + return t - 1; + }); + }, 1000); + + return () => clearInterval(interval); + } + }, [attempt]); + + useEffect(() => { + if (contestId) { + dispatch(fetchMyAttemptsInContest(contestId)); + } + }, [contestId]); + + const minutes = String(Math.floor(time / 60)).padStart(2, '0'); + const seconds = String(time % 60).padStart(2, '0'); + return (
= ({ missionId, back }) => { }} />
+ {!!contestId && !!attempt && ( +
{`${minutes}:${seconds}`}
+ )} ); }; diff --git a/src/views/mission/statement/MissionSubmissions.tsx b/src/views/mission/statement/MissionSubmissions.tsx index 04dca84..6868432 100644 --- a/src/views/mission/statement/MissionSubmissions.tsx +++ b/src/views/mission/statement/MissionSubmissions.tsx @@ -19,9 +19,16 @@ interface MissionSubmissionsProps { contestId?: number; } -const MissionSubmissions: FC = ({ missionId, contestId }) => { +const MissionSubmissions: FC = ({ + missionId, + contestId, +}) => { const submissions = useAppSelector( - (state) => state.submin.submitsById[missionId] || [] + (state) => state.submin.submitsById[missionId] || [], + ); + + const attempt = useAppSelector( + (state) => state.contests.fetchMyAttemptsInContest.attempts[0], ); const checkStatus = (status: string) => { @@ -32,7 +39,9 @@ const MissionSubmissions: FC = ({ missionId, contestId // Если contestId передан, фильтруем по нему, иначе показываем все const filteredSubmissions = contestId - ? submissions.filter((v) => v.contestId === contestId) + ? attempt?.submissions?.filter( + (v) => v.solution.missionId == missionId, + ) ?? [] : submissions; return (