From 69655dda8212181a8a6cd99f28d56a548d2b34d9 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: Fri, 7 Nov 2025 19:04:45 +0300 Subject: [PATCH] contests --- .../home/account/missions/MissionsBlock.tsx | 69 +++++-- src/views/home/contest/Contest.tsx | 8 +- src/views/home/contest/MissionItem.tsx | 2 +- src/views/home/contest/Missions.tsx | 143 +++++++++++---- src/views/home/contest/SubmissionItem.tsx | 23 ++- src/views/home/contest/Submissions.tsx | 172 ++++++++++++------ .../mission/statement/MissionSubmissions.tsx | 95 +++------- 7 files changed, 334 insertions(+), 178 deletions(-) diff --git a/src/views/home/account/missions/MissionsBlock.tsx b/src/views/home/account/missions/MissionsBlock.tsx index c20a241..8cd10b0 100644 --- a/src/views/home/account/missions/MissionsBlock.tsx +++ b/src/views/home/account/missions/MissionsBlock.tsx @@ -1,19 +1,66 @@ -import { useEffect } from 'react'; -import { useAppDispatch } from '../../../../redux/hooks'; -import { setMenuActiveProfilePage } from '../../../../redux/slices/store'; +import { FC, useEffect } from "react"; +import { useAppDispatch } from "../../../../redux/hooks"; +import { setMenuActiveProfilePage } from "../../../../redux/slices/store"; +import { cn } from "../../../../lib/cn"; + + +interface ItemProps { + count: number; + totalCount: number; + title: string; + color?: "default" | "red" | "green" | "orange"; +} + +const Item: FC = ({count, totalCount, title, color = "default"}) => { + + return
+
{count}/{totalCount}
+
{title}
+
+}; const MissionsBlock = () => { - const dispatch = useAppDispatch(); + const dispatch = useAppDispatch(); - useEffect(() => { - dispatch(setMenuActiveProfilePage('missions')); - }, []); + useEffect(() => { + dispatch(setMenuActiveProfilePage("missions")); + }, []); - return ( -
- Пока пусто :( + return ( +
+
+
+
Решенные задачи
+
+ +
+ +
+
+ + + +
+ +
+
Компетенции
+ +
+ + + +
- ); +
Недавиние задачи
+
Мои задачи
+
+
+ ); }; export default MissionsBlock; diff --git a/src/views/home/contest/Contest.tsx b/src/views/home/contest/Contest.tsx index 79e7fcf..b30de29 100644 --- a/src/views/home/contest/Contest.tsx +++ b/src/views/home/contest/Contest.tsx @@ -24,6 +24,7 @@ const Contest = () => { const dispatch = useAppDispatch(); const contest = useAppSelector((state) => state.contests.fetchContestById.contest); + useEffect(() => { dispatch(setMenuActivePage('contest')); }, []); @@ -33,17 +34,16 @@ const Contest = () => { }, [contestIdNumber]); return ( -
- {navigate(`/contest/${contestIdNumber}/submissions`)}} text='Мои посылки' /> +
} + element={} /> } + element={} /> diff --git a/src/views/home/contest/MissionItem.tsx b/src/views/home/contest/MissionItem.tsx index 8f4e6cb..bf5f29e 100644 --- a/src/views/home/contest/MissionItem.tsx +++ b/src/views/home/contest/MissionItem.tsx @@ -10,7 +10,7 @@ export interface MissionItemProps { timeLimit?: number; memoryLimit?: number; type?: 'first' | 'second'; - status?: 'empty' | 'success' | 'error'; + status?: 'success' | 'error'; } export function formatMilliseconds(ms: number): string { diff --git a/src/views/home/contest/Missions.tsx b/src/views/home/contest/Missions.tsx index f18511a..1893df9 100644 --- a/src/views/home/contest/Missions.tsx +++ b/src/views/home/contest/Missions.tsx @@ -1,45 +1,124 @@ -import { FC } from 'react'; -import MissionItem from './MissionItem'; -import { Contest } from '../../../redux/slices/contests'; +import { FC, useEffect } from "react"; +import MissionItem from "./MissionItem"; +import { + Contest, + fetchMySubmissions, + setContestStatus, +} from "../../../redux/slices/contests"; +import { useAppDispatch, useAppSelector } from "../../../redux/hooks"; +import { PrimaryButton } from "../../../components/button/PrimaryButton"; +import { useNavigate } from "react-router-dom"; +import { arrowLeft } from "../../../assets/icons/header"; export interface Article { - id: number; - name: string; - tags: string[]; + id: number; + name: string; + tags: string[]; } interface ContestMissionsProps { - contest?: Contest; + contest?: Contest; } const ContestMissions: FC = ({ contest }) => { - if (!contest) { - return <>; - } + const navigate = useNavigate(); + const dispatch = useAppDispatch(); + const { submissions, status } = useAppSelector( + (state) => state.contests.fetchMySubmissions + ); - return ( -
-
-
-
- {contest?.name} {contest.id} -
-
- {(contest.missions ?? []).map((v, i) => ( - - ))} -
-
+ useEffect(() => { + if (contest) dispatch(fetchMySubmissions(contest.id)); + }, [contest]); + + useEffect(() => { + if (status == "successful") { + dispatch(setContestStatus({ key: "fetchMySubmissions", status: "idle" })); + } + }, [status]); + + if (!contest) { + return <>; + } + + 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}
- ); +
+
+ { + navigate(`/home/contests`); + }} + /> + + Контест #{contest.id} + +
+
{contest.attemptDurationMinutes ?? 0} минут
+
+
+
+
{`${solvedCount}/${totalCount} Решено`}
+ { + navigate(`/contest/${contest.id}/submissions`); + }} + text="Мои посылки" + /> +
+ +
+
+ {(contest.missions ?? []).map((v, i) => { + const missionSubmissions = submissions.filter( + (s) => s.solution.missionId === v.id + ); + + const hasSuccess = missionSubmissions.some( + (s) => s.solution.status == "Accepted: All tests passed" + ); + + console.log(missionSubmissions); + + const status = hasSuccess + ? "success" + : missionSubmissions.length > 0 + ? "error" + : undefined; + + return ( + + ); + })} +
+
+
+ ); }; export default ContestMissions; diff --git a/src/views/home/contest/SubmissionItem.tsx b/src/views/home/contest/SubmissionItem.tsx index c4b86a3..6d90537 100644 --- a/src/views/home/contest/SubmissionItem.tsx +++ b/src/views/home/contest/SubmissionItem.tsx @@ -4,9 +4,12 @@ import { cn } from '../../../lib/cn'; export interface SubmissionItemProps { id: number; + datetime: string; + missionId: number; language: string; - time: string; verdict: string; + duration: number; + memory: number; type: 'first' | 'second'; status?: 'success' | 'wronganswer' | 'timelimit'; } @@ -37,20 +40,23 @@ function formatDate(dateString: string): string { const SubmissionItem: React.FC = ({ id, + datetime, + missionId, language, - time, verdict, + duration, + memory, type, - status, + status }) => { // const navigate = useNavigate(); return (
= ({ >
#{id}
- {formatDate(time)} + {formatDate(datetime)}
+
{missionId}
{language}
= ({ )} > {verdict} +
+
{formatMilliseconds(duration)}
+
+ {formatBytesToMB(memory)}
); diff --git a/src/views/home/contest/Submissions.tsx b/src/views/home/contest/Submissions.tsx index 24024a2..7143099 100644 --- a/src/views/home/contest/Submissions.tsx +++ b/src/views/home/contest/Submissions.tsx @@ -1,73 +1,129 @@ -import SubmissionItem from './SubmissionItem'; -import { useAppDispatch, useAppSelector } from '../../../redux/hooks'; -import { FC, useEffect } from 'react'; -import { fetchMySubmissions, setContestStatus } from '../../../redux/slices/contests'; +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"; 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 { - contestId: number; + contest: Contest; } -const Submissions: FC = ({ contestId }) => { - const dispatch = useAppDispatch(); - - const {submissions, status} = useAppSelector( - (state) => state.contests.fetchMySubmissions - ); +const Submissions: FC = ({ contest }) => { + const dispatch = useAppDispatch(); + const navigate = useNavigate(); - useEffect(() => { - dispatch(fetchMySubmissions(contestId)); - }, [contestId]); + const { submissions, status } = useAppSelector( + (state) => state.contests.fetchMySubmissions + ); - useEffect(() => { - if (status == "successful"){ - dispatch(setContestStatus({key:"fetchMySubmissions", status: "idle"})); - } - }, [status]) + useEffect(() => { + if (contest && contest.id) dispatch(fetchMySubmissions(contest.id)); + }, [contest]); - const checkStatus = (status: string) => { - if (status == 'IncorrectAnswer') return 'wronganswer'; - if (status == 'TimeLimitError') return 'timelimit'; - return undefined; - }; + useEffect(() => { + if (status == "successful") { + dispatch(setContestStatus({ key: "fetchMySubmissions", status: "idle" })); + } + }, [status]); - return ( -
- {submissions && - submissions.map((v, i) => ( - - ))} + 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}
- ); +
+
+ { + navigate(`/contest/${contest.id}`); + }} + /> + + Контест #{contest.id} + +
+
{`${solvedCount}/${totalCount} Решено`}
+
+
+ +
+
+
Посылка
+
Когда
+
Задача
+
Язык
+
Вердикт
+
Время
+
Память
+
+ + {!submissions || submissions.length == 0 ? ( +
Вы еще ничего не отсылали
+ ) : ( + <> + {submissions.map((v, i) => ( + + ))} + + )} +
+
+ ); }; export default Submissions; diff --git a/src/views/mission/statement/MissionSubmissions.tsx b/src/views/mission/statement/MissionSubmissions.tsx index 9d8db13..04dca84 100644 --- a/src/views/mission/statement/MissionSubmissions.tsx +++ b/src/views/mission/statement/MissionSubmissions.tsx @@ -1,7 +1,6 @@ import SubmissionItem from './SubmissionItem'; -import { useAppDispatch, useAppSelector } from '../../../redux/hooks'; -import { FC, useEffect } from 'react'; -import { fetchMySubmissions } from '../../../redux/slices/contests'; +import { useAppSelector } from '../../../redux/hooks'; +import { FC } from 'react'; export interface Mission { id: number; @@ -21,78 +20,42 @@ interface MissionSubmissionsProps { } const MissionSubmissions: FC = ({ missionId, contestId }) => { - const dispatch = useAppDispatch(); const submissions = useAppSelector( - (state) => state.submin.submitsById[missionId], + (state) => state.submin.submitsById[missionId] || [] ); - - const {submissions: contestSubmission, status: contestStatus} = useAppSelector((state) => state.contests.fetchMySubmissions); - - useEffect(() => {}, []); - const checkStatus = (status: string) => { - if (status == 'IncorrectAnswer') return 'wronganswer'; - if (status == 'TimeLimitError') return 'timelimit'; + if (status === 'IncorrectAnswer') return 'wronganswer'; + if (status === 'TimeLimitError') return 'timelimit'; return undefined; }; + // Если contestId передан, фильтруем по нему, иначе показываем все + const filteredSubmissions = contestId + ? submissions.filter((v) => v.contestId === contestId) + : submissions; - useEffect(() => { - if (contestId){ - dispatch(fetchMySubmissions(contestId)); - } - }, [contestId, missionId]) return ( -
- -{contestId ? -contestSubmission && - contestSubmission.filter(v => v.solution.missionId == missionId).map((v, i) => ( - - )) -: - submissions && - submissions.map((v, i) => ( - - )) - } +
+ {filteredSubmissions.map((v, i) => ( + + ))}
); };