diff --git a/src/pages/ContestEditor.tsx b/src/pages/ContestEditor.tsx index aa46a50..03d06b0 100644 --- a/src/pages/ContestEditor.tsx +++ b/src/pages/ContestEditor.tsx @@ -61,7 +61,6 @@ const ContestEditor = () => { attemptDurationMinutes: 60, maxAttempts: 1, allowEarlyFinish: true, - groupIds: [], missionIds: [], articleIds: [], }); @@ -70,6 +69,7 @@ const ContestEditor = () => { const statusDelete = useAppSelector((state) => state.contests.deleteContest.status) + const statusUpdate = useAppSelector((state) => state.contests.updateContest.status); const { contest: contestById, status: contestByIdstatus } = useAppSelector( (state) => state.contests.fetchContestById, @@ -127,6 +127,14 @@ const ContestEditor = () => { } }, [statusDelete]) + + useEffect(() => { + if (statusUpdate == "successful"){ + dispatch(setContestStatus({key: "updateContest", status: "idle"})) + navigate('/home/account/contests') + } + }, [statusUpdate]) + useEffect(() => { if (refactor) { dispatch(fetchContestById(contestId)); @@ -138,7 +146,6 @@ const ContestEditor = () => { setContest({ ...contestById, // groupIds: contestById.groups.map(group => group.groupId), - groupIds: [], missionIds: contestById.missions?.map(mission => mission.id), articleIds: contestById.articles?.map(article => article.articleId), visibility: 'Public', diff --git a/src/pages/Mission.tsx b/src/pages/Mission.tsx index b09c25a..161d516 100644 --- a/src/pages/Mission.tsx +++ b/src/pages/Mission.tsx @@ -20,6 +20,7 @@ const Mission = () => { const query = useQuery(); const back = query.get('back') ?? undefined; + const contestId = Number(query.get('contestId') ?? undefined); if (!missionId || isNaN(missionIdNumber)) { if (back) return ; @@ -179,13 +180,14 @@ const Mission = () => { { + console.log(contestId); await dispatch( submitMission({ missionId: missionIdNumber, language: language, languageVersion: 'latest', sourceCode: code, - contestId: null, + contestId: contestId, }), ).unwrap(); dispatch( @@ -198,7 +200,7 @@ const Mission = () => {
- +
diff --git a/src/redux/slices/contests.ts b/src/redux/slices/contests.ts index f6e9d4c..1e2970c 100644 --- a/src/redux/slices/contests.ts +++ b/src/redux/slices/contests.ts @@ -5,6 +5,36 @@ import axios from '../../axios'; // Типы // ===================== +// ===================== +// Типы для посылок +// ===================== + +export interface Solution { + id: number; + missionId: number; + language: string; + languageVersion: string; + sourceCode: string; + status: string; + time: string; + testerState: string; + testerErrorCode: string; + testerMessage: string; + currentTest: number; + amountOfTests: number; +} + +export interface Submission { + id: number; + userId: number; + solution: Solution; + contestId: number; + contestName: string; + sourceType: string; +} + + + export interface Mission { id: number; authorId: number; @@ -38,7 +68,8 @@ export interface Contest { attemptDurationMinutes?: number; maxAttempts?: number; allowEarlyFinish?: boolean; - groups?: Group[]; + groupId?: number; + groupName?: string; missions?: Mission[]; articles?: any[]; members?: Member[]; @@ -59,7 +90,8 @@ export interface CreateContestBody { attemptDurationMinutes?: number; maxAttempts?: number; allowEarlyFinish?: boolean; - groupIds?: number[]; + groupId?: number; + groupName?: string; missionIds?: number[]; articleIds?: number[]; } @@ -87,6 +119,12 @@ interface ContestsState { status: Status; error?: string; }; + fetchMySubmissions: { + submissions: Submission[]; + status: Status; + error?: string; + }; + // 🆕 Добавляем updateContest и deleteContest updateContest: { contest: Contest; @@ -129,7 +167,8 @@ const initialState: ContestsState = { attemptDurationMinutes: 0, maxAttempts: 0, allowEarlyFinish: false, - groups: [], + groupId: undefined, + groupName: undefined, missions: [], articles: [], members: [], @@ -137,6 +176,12 @@ const initialState: ContestsState = { status: 'idle', error: undefined, }, + fetchMySubmissions: { + submissions: [], + status: 'idle', + error: undefined, + }, + createContest: { contest: { id: 0, @@ -149,7 +194,8 @@ const initialState: ContestsState = { attemptDurationMinutes: 0, maxAttempts: 0, allowEarlyFinish: false, - groups: [], + groupId: undefined, + groupName: undefined, missions: [], articles: [], members: [], @@ -169,7 +215,8 @@ const initialState: ContestsState = { attemptDurationMinutes: 0, maxAttempts: 0, allowEarlyFinish: false, - groups: [], + groupId: undefined, + groupName: undefined, missions: [], articles: [], members: [], @@ -198,6 +245,24 @@ const initialState: ContestsState = { // Async Thunks // ===================== +// Мои посылки в контесте +export const fetchMySubmissions = createAsyncThunk( + 'contests/fetchMySubmissions', + async (contestId: number, { rejectWithValue }) => { + try { + const response = await axios.get( + `/contests/${contestId}/submissions/my`, + ); + return response.data; + } catch (err: any) { + return rejectWithValue( + err.response?.data?.message || 'Failed to fetch my submissions', + ); + } + }, +); + + // Все контесты export const fetchContests = createAsyncThunk( 'contests/fetchAll', @@ -353,6 +418,25 @@ const contestsSlice = createSlice({ }, }, extraReducers: (builder) => { + // 🆕 fetchMySubmissions + builder.addCase(fetchMySubmissions.pending, (state) => { + state.fetchMySubmissions.status = 'loading'; + state.fetchMySubmissions.error = undefined; + }); + builder.addCase( + fetchMySubmissions.fulfilled, + (state, action: PayloadAction) => { + state.fetchMySubmissions.status = 'successful'; + state.fetchMySubmissions.submissions = action.payload; + }, + ); + builder.addCase(fetchMySubmissions.rejected, (state, action: any) => { + state.fetchMySubmissions.status = 'failed'; + state.fetchMySubmissions.error = action.payload; + }); + + + // fetchContests builder.addCase(fetchContests.pending, (state) => { state.fetchContests.status = 'loading'; diff --git a/src/redux/slices/submit.ts b/src/redux/slices/submit.ts index 21522b1..1b627f2 100644 --- a/src/redux/slices/submit.ts +++ b/src/redux/slices/submit.ts @@ -56,6 +56,7 @@ const initialState: SubmitState = { export const submitMission = createAsyncThunk( 'submit/submitMission', async (submitData: Submit, { rejectWithValue }) => { + console.log(submitData); try { const response = await axios.post('/submits', submitData); return response.data; diff --git a/src/views/home/account/contests/ContestsBlock.tsx b/src/views/home/account/contests/ContestsBlock.tsx index 879c1a3..f732ffe 100644 --- a/src/views/home/account/contests/ContestsBlock.tsx +++ b/src/views/home/account/contests/ContestsBlock.tsx @@ -59,12 +59,12 @@ const ContestsBlock: FC = ({ key={i} id={v.id} name={v.name} - startAt={v.startsAt} + startAt={v.startsAt ?? ''} duration={ - new Date(v.endsAt).getTime() - - new Date(v.startsAt).getTime() + new Date(v.endsAt ?? '').getTime() - + new Date(v.startsAt ?? '').getTime() } - members={v.members.length} + members={(v.members??[]).length} type={i % 2 ? 'second' : 'first'} /> ) : ( @@ -72,13 +72,13 @@ const ContestsBlock: FC = ({ key={i} id={v.id} name={v.name} - startAt={v.startsAt} + startAt={v.startsAt ?? ''} statusRegister={'reg'} duration={ - new Date(v.endsAt).getTime() - - new Date(v.startsAt).getTime() + new Date(v.endsAt ?? '').getTime() - + new Date(v.startsAt ?? '').getTime() } - members={v.members.length} + members={(v.members??[]).length} type={i % 2 ? 'second' : 'first'} /> ); diff --git a/src/views/home/contest/Contest.tsx b/src/views/home/contest/Contest.tsx index d184dd4..79e7fcf 100644 --- a/src/views/home/contest/Contest.tsx +++ b/src/views/home/contest/Contest.tsx @@ -1,9 +1,11 @@ import { useEffect } from 'react'; import { useAppDispatch, useAppSelector } from '../../../redux/hooks'; import { setMenuActivePage } from '../../../redux/slices/store'; -import { Navigate, Route, Routes, useParams } from 'react-router-dom'; +import { Navigate, Route, Routes, useNavigate, useParams } from 'react-router-dom'; import { fetchContestById } from '../../../redux/slices/contests'; import ContestMissions from './Missions'; +import { PrimaryButton } from '../../../components/button/PrimaryButton'; +import Submissions from './Submissions'; export interface Article { id: number; @@ -12,14 +14,15 @@ export interface Article { } const Contest = () => { + const navigate = useNavigate(); const { contestId } = useParams<{ contestId: string }>(); const contestIdNumber = contestId && /^\d+$/.test(contestId) ? parseInt(contestId, 10) : null; - if (contestIdNumber === null) { + if (!contestIdNumber) { return ; } const dispatch = useAppDispatch(); - const contest = useAppSelector((state) => state.contests.selectedContest); + const contest = useAppSelector((state) => state.contests.fetchContestById.contest); useEffect(() => { dispatch(setMenuActivePage('contest')); @@ -31,12 +34,19 @@ const Contest = () => { return (
+ {navigate(`/contest/${contestIdNumber}/submissions`)}} text='Мои посылки' /> + + } + /> } /> +
); }; diff --git a/src/views/home/contest/MissionItem.tsx b/src/views/home/contest/MissionItem.tsx index ee9579b..8f4e6cb 100644 --- a/src/views/home/contest/MissionItem.tsx +++ b/src/views/home/contest/MissionItem.tsx @@ -4,6 +4,7 @@ import { useNavigate } from 'react-router-dom'; import { useLocation } from 'react-router-dom'; export interface MissionItemProps { + contestId: number; id: number; name: string; timeLimit?: number; @@ -24,6 +25,7 @@ export function formatBytesToMB(bytes: number): string { } const MissionItem: React.FC = ({ + contestId, id, name, timeLimit = 1000, @@ -48,7 +50,7 @@ const MissionItem: React.FC = ({ 'cursor-pointer brightness-100 hover:brightness-125 transition-all duration-300', )} onClick={() => { - navigate(`/mission/${id}?back=${path}`); + navigate(`/mission/${id}?back=${path}&contestId=${contestId}`); }} >
#{id}
diff --git a/src/views/home/contest/Missions.tsx b/src/views/home/contest/Missions.tsx index 535cf05..f18511a 100644 --- a/src/views/home/contest/Missions.tsx +++ b/src/views/home/contest/Missions.tsx @@ -9,7 +9,7 @@ export interface Article { } interface ContestMissionsProps { - contest: Contest | null; + contest?: Contest; } const ContestMissions: FC = ({ contest }) => { @@ -25,8 +25,10 @@ const ContestMissions: FC = ({ contest }) => { {contest?.name} {contest.id}
- {contest.missions.map((v, i) => ( + {(contest.missions ?? []).map((v, i) => ( = ({ + id, + language, + time, + verdict, + type, + status, +}) => { + // const navigate = useNavigate(); + + return ( +
{}} + > +
#{id}
+
+ {formatDate(time)} +
+
{language}
+
+ {verdict} +
+
+ ); +}; + +export default SubmissionItem; diff --git a/src/views/home/contest/Submissions.tsx b/src/views/home/contest/Submissions.tsx index e69de29..24024a2 100644 --- a/src/views/home/contest/Submissions.tsx +++ b/src/views/home/contest/Submissions.tsx @@ -0,0 +1,73 @@ +import SubmissionItem from './SubmissionItem'; +import { useAppDispatch, useAppSelector } from '../../../redux/hooks'; +import { FC, useEffect } from 'react'; +import { fetchMySubmissions, setContestStatus } from '../../../redux/slices/contests'; + +export interface Mission { + id: number; + authorId: number; + name: string; + difficulty: 'Easy' | 'Medium' | 'Hard'; + tags: string[]; + timeLimit: number; + memoryLimit: number; + createdAt: string; + updatedAt: string; +} + +interface SubmissionsProps { + contestId: number; +} + +const Submissions: FC = ({ contestId }) => { + const dispatch = useAppDispatch(); + + const {submissions, status} = useAppSelector( + (state) => state.contests.fetchMySubmissions + ); + + useEffect(() => { + dispatch(fetchMySubmissions(contestId)); + }, [contestId]); + + useEffect(() => { + if (status == "successful"){ + dispatch(setContestStatus({key:"fetchMySubmissions", status: "idle"})); + } + }, [status]) + + const checkStatus = (status: string) => { + if (status == 'IncorrectAnswer') return 'wronganswer'; + if (status == 'TimeLimitError') return 'timelimit'; + return undefined; + }; + + return ( +
+ {submissions && + submissions.map((v, i) => ( + + ))} +
+ ); +}; + +export default Submissions; diff --git a/src/views/home/contests/ModalCreate.tsx b/src/views/home/contests/ModalCreate.tsx index 6863b0d..0c30465 100644 --- a/src/views/home/contests/ModalCreate.tsx +++ b/src/views/home/contests/ModalCreate.tsx @@ -37,7 +37,6 @@ const ModalCreateContest: FC = ({ attemptDurationMinutes: 0, maxAttempts: 0, allowEarlyFinish: false, - groupIds: [], missionIds: [], articleIds: [], }); diff --git a/src/views/mission/statement/MissionSubmissions.tsx b/src/views/mission/statement/MissionSubmissions.tsx index cdd725d..9d8db13 100644 --- a/src/views/mission/statement/MissionSubmissions.tsx +++ b/src/views/mission/statement/MissionSubmissions.tsx @@ -1,6 +1,7 @@ import SubmissionItem from './SubmissionItem'; -import { useAppSelector } from '../../../redux/hooks'; +import { useAppDispatch, useAppSelector } from '../../../redux/hooks'; import { FC, useEffect } from 'react'; +import { fetchMySubmissions } from '../../../redux/slices/contests'; export interface Mission { id: number; @@ -16,13 +17,18 @@ export interface Mission { interface MissionSubmissionsProps { missionId: number; + contestId?: number; } -const MissionSubmissions: FC = ({ missionId }) => { +const MissionSubmissions: FC = ({ missionId, contestId }) => { + const dispatch = useAppDispatch(); const submissions = useAppSelector( (state) => state.submin.submitsById[missionId], ); + + const {submissions: contestSubmission, status: contestStatus} = useAppSelector((state) => state.contests.fetchMySubmissions); + useEffect(() => {}, []); const checkStatus = (status: string) => { @@ -31,9 +37,40 @@ const MissionSubmissions: FC = ({ missionId }) => { return undefined; }; + + useEffect(() => { + if (contestId){ + dispatch(fetchMySubmissions(contestId)); + } + }, [contestId, missionId]) return (
- {submissions && + +{contestId ? +contestSubmission && + contestSubmission.filter(v => v.solution.missionId == missionId).map((v, i) => ( + + )) +: + submissions && submissions.map((v, i) => ( = ({ missionId }) => { : checkStatus(v.solution.testerErrorCode) } /> - ))} + )) + }
); };