From 6c92c789d091cdac5040d4480953dd40c289947f 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: Thu, 6 Nov 2025 18:27:28 +0300 Subject: [PATCH] contests update --- src/pages/ContestEditor.tsx | 90 +++++++++++++++-------- src/redux/slices/contests.ts | 88 +++++++++++----------- src/views/home/contests/Contests.tsx | 49 ++++++------ src/views/home/contests/ContestsBlock.tsx | 8 +- src/views/home/contests/ModalCreate.tsx | 9 +-- 5 files changed, 136 insertions(+), 108 deletions(-) diff --git a/src/pages/ContestEditor.tsx b/src/pages/ContestEditor.tsx index 669adc7..aa46a50 100644 --- a/src/pages/ContestEditor.tsx +++ b/src/pages/ContestEditor.tsx @@ -6,48 +6,71 @@ import { useAppDispatch, useAppSelector } from '../redux/hooks'; import { createContest, CreateContestBody, + deleteContest, fetchContestById, + setContestStatus, + updateContest, } from '../redux/slices/contests'; import DateRangeInput from '../components/input/DateRangeInput'; import { useQuery } from '../hooks/useQuery'; -import { useNavigate } from 'react-router-dom'; -import { fetchMissionById, Mission } from '../redux/slices/missions'; +import { Navigate, useNavigate } from 'react-router-dom'; +import { fetchMissionById } from '../redux/slices/missions'; import { ReverseButton } from '../components/button/ReverseButton'; + +interface Mission { + id: number; + name: string; +} + /** * Страница создания / редактирования контеста */ const ContestEditor = () => { + const dispatch = useAppDispatch(); const navigate = useNavigate(); - const status = useAppSelector( - (state) => state.contests.createContest.status, - ); - - const [missionIdInput, setMissionIdInput] = useState(''); const query = useQuery(); const back = query.get('back') ?? undefined; const contestId = Number(query.get('contestId') ?? undefined); const refactor = !!contestId; + if (!refactor){ + return + } + + + + const status = useAppSelector( + (state) => state.contests.createContest.status, + ); + + + + const [missionIdInput, setMissionIdInput] = useState(''); + + const [contest, setContest] = useState({ name: '', description: '', scheduleType: 'AlwaysOpen', visibility: 'Public', - startsAt: null, - endsAt: null, - attemptDurationMinutes: null, - maxAttempts: null, - allowEarlyFinish: false, - groupId: null, + startsAt: '', + endsAt: '', + attemptDurationMinutes: 60, + maxAttempts: 1, + allowEarlyFinish: true, + groupIds: [], missionIds: [], articleIds: [], }); const [missions, setMissions] = useState([]); + + const statusDelete = useAppSelector((state) => state.contests.deleteContest.status) + const { contest: contestById, status: contestByIdstatus } = useAppSelector( (state) => state.contests.fetchContestById, ); @@ -61,9 +84,15 @@ const ContestEditor = () => { setContest((prev) => ({ ...prev, [key]: value })); }; - const handleSubmit = () => { - dispatch(createContest(contest)); + const handleUpdateContest = () => { + dispatch(updateContest({...contest, contestId})); }; + + const handleDeleteContest = () => { + dispatch(deleteContest(contestId)); + }; + + const addMission = () => { const id = Number(missionIdInput.trim()); @@ -91,6 +120,13 @@ const ContestEditor = () => { setMissions(missions.filter((v) => v.id != removeId)); }; + useEffect(() => { + if (statusDelete == "successful"){ + dispatch(setContestStatus({key: "deleteContest", status: "idle"})) + navigate('/home/account/contests') + } + }, [statusDelete]) + useEffect(() => { if (refactor) { dispatch(fetchContestById(contestId)); @@ -101,10 +137,14 @@ const ContestEditor = () => { if (refactor && contestByIdstatus == 'successful' && contestById) { setContest({ ...contestById, - missionIds: [], + // groupIds: contestById.groups.map(group => group.groupId), + groupIds: [], + missionIds: contestById.missions?.map(mission => mission.id), + articleIds: contestById.articles?.map(article => article.articleId), visibility: 'Public', scheduleType: 'AlwaysOpen', }); + setMissions(contestById.missions ?? []); } }, [contestById]); @@ -253,27 +293,19 @@ const ContestEditor = () => { {/* Кнопки */}
- {refactor ? ( - <> + - - ) : ( - - )} +
diff --git a/src/redux/slices/contests.ts b/src/redux/slices/contests.ts index a5c0656..f6e9d4c 100644 --- a/src/redux/slices/contests.ts +++ b/src/redux/slices/contests.ts @@ -11,8 +11,6 @@ export interface Mission { name: string; difficulty: number; tags: string[]; - createdAt: string; - updatedAt: string; timeLimitMilliseconds: number; memoryLimitBytes: number; statements: string; @@ -32,17 +30,18 @@ export interface Group { export interface Contest { id: number; name: string; - description: string; + description?: string; scheduleType: 'AlwaysOpen' | 'FixedWindow' | 'RollingWindow'; - startsAt: string; - endsAt: string; - attemptDurationMinutes: number; - maxAttempts: number; - allowEarlyFinish: boolean; - groups: Group[]; - missions: Mission[]; - articles: any[]; - members: Member[]; + visibility: 'Public' | 'GroupPrivate'; + startsAt?: string; + endsAt?: string; + attemptDurationMinutes?: number; + maxAttempts?: number; + allowEarlyFinish?: boolean; + groups?: Group[]; + missions?: Mission[]; + articles?: any[]; + members?: Member[]; } interface ContestsResponse { @@ -52,17 +51,17 @@ interface ContestsResponse { export interface CreateContestBody { name: string; - description: string; + description?: string; scheduleType: 'AlwaysOpen' | 'FixedWindow' | 'RollingWindow'; visibility: 'Public' | 'GroupPrivate'; - startsAt: string; - endsAt: string; - attemptDurationMinutes: number; - maxAttempts: number; - allowEarlyFinish: boolean; - groupId: number; - missionIds: number[]; - articleIds: number[]; + startsAt?: string; + endsAt?: string; + attemptDurationMinutes?: number; + maxAttempts?: number; + allowEarlyFinish?: boolean; + groupIds?: number[]; + missionIds?: number[]; + articleIds?: number[]; } // ===================== @@ -76,38 +75,38 @@ interface ContestsState { contests: Contest[]; hasNextPage: boolean; status: Status; - error: string | null; + error?: string; }; fetchContestById: { contest: Contest; status: Status; - error: string | null; + error?: string; }; createContest: { contest: Contest; status: Status; - error: string | null; + error?: string; }; // 🆕 Добавляем updateContest и deleteContest updateContest: { contest: Contest; status: Status; - error: string | null; + error?: string; }; deleteContest: { status: Status; - error: string | null; + error?: string; }; fetchMyContests: { contests: Contest[]; status: Status; - error: string | null; + error?: string; }; fetchRegisteredContests: { contests: Contest[]; hasNextPage: boolean; status: Status; - error: string | null; + error?: string; }; } @@ -116,7 +115,7 @@ const initialState: ContestsState = { contests: [], hasNextPage: false, status: 'idle', - error: null, + error: undefined, }, fetchContestById: { contest: { @@ -124,6 +123,7 @@ const initialState: ContestsState = { name: '', description: '', scheduleType: 'AlwaysOpen', + visibility: 'Public', startsAt: '', endsAt: '', attemptDurationMinutes: 0, @@ -135,7 +135,7 @@ const initialState: ContestsState = { members: [], }, status: 'idle', - error: null, + error: undefined, }, createContest: { contest: { @@ -143,6 +143,7 @@ const initialState: ContestsState = { name: '', description: '', scheduleType: 'AlwaysOpen', + visibility: 'Public', startsAt: '', endsAt: '', attemptDurationMinutes: 0, @@ -154,7 +155,7 @@ const initialState: ContestsState = { members: [], }, status: 'idle', - error: null, + error: undefined, }, updateContest: { contest: { @@ -162,6 +163,7 @@ const initialState: ContestsState = { name: '', description: '', scheduleType: 'AlwaysOpen', + visibility: 'Public', startsAt: '', endsAt: '', attemptDurationMinutes: 0, @@ -173,22 +175,22 @@ const initialState: ContestsState = { members: [], }, status: 'idle', - error: null, + error: undefined, }, deleteContest: { status: 'idle', - error: null, + error: undefined, }, fetchMyContests: { contests: [], status: 'idle', - error: null, + error: undefined, }, fetchRegisteredContests: { contests: [], hasNextPage: false, status: 'idle', - error: null, + error: undefined, }, }; @@ -265,7 +267,7 @@ export const updateContest = createAsyncThunk( { rejectWithValue }, ) => { try { - const response = await axios.patch( + const response = await axios.put( `/contests/${contestId}`, contestData, ); @@ -354,7 +356,7 @@ const contestsSlice = createSlice({ // fetchContests builder.addCase(fetchContests.pending, (state) => { state.fetchContests.status = 'loading'; - state.fetchContests.error = null; + state.fetchContests.error = undefined; }); builder.addCase( fetchContests.fulfilled, @@ -372,7 +374,7 @@ const contestsSlice = createSlice({ // fetchContestById builder.addCase(fetchContestById.pending, (state) => { state.fetchContestById.status = 'loading'; - state.fetchContestById.error = null; + state.fetchContestById.error = undefined; }); builder.addCase( fetchContestById.fulfilled, @@ -389,7 +391,7 @@ const contestsSlice = createSlice({ // createContest builder.addCase(createContest.pending, (state) => { state.createContest.status = 'loading'; - state.createContest.error = null; + state.createContest.error = undefined; }); builder.addCase( createContest.fulfilled, @@ -406,7 +408,7 @@ const contestsSlice = createSlice({ // 🆕 updateContest builder.addCase(updateContest.pending, (state) => { state.updateContest.status = 'loading'; - state.updateContest.error = null; + state.updateContest.error = undefined; }); builder.addCase( updateContest.fulfilled, @@ -423,7 +425,7 @@ const contestsSlice = createSlice({ // 🆕 deleteContest builder.addCase(deleteContest.pending, (state) => { state.deleteContest.status = 'loading'; - state.deleteContest.error = null; + state.deleteContest.error = undefined; }); builder.addCase( deleteContest.fulfilled, @@ -448,7 +450,7 @@ const contestsSlice = createSlice({ // fetchMyContests builder.addCase(fetchMyContests.pending, (state) => { state.fetchMyContests.status = 'loading'; - state.fetchMyContests.error = null; + state.fetchMyContests.error = undefined; }); builder.addCase( fetchMyContests.fulfilled, @@ -465,7 +467,7 @@ const contestsSlice = createSlice({ // fetchRegisteredContests builder.addCase(fetchRegisteredContests.pending, (state) => { state.fetchRegisteredContests.status = 'loading'; - state.fetchRegisteredContests.error = null; + state.fetchRegisteredContests.error = undefined; }); builder.addCase( fetchRegisteredContests.fulfilled, diff --git a/src/views/home/contests/Contests.tsx b/src/views/home/contests/Contests.tsx index 7af6f32..999ee29 100644 --- a/src/views/home/contests/Contests.tsx +++ b/src/views/home/contests/Contests.tsx @@ -28,16 +28,6 @@ const Contests = () => { dispatch(fetchContests({})); }, []); - if (status == 'loading') { - return ( -
Загрузка контестов...
- ); - } - - if (error) { - return
Ошибка: {error}
; - } - return (
@@ -59,24 +49,29 @@ const Contests = () => {
+ {status == 'loading' &&
Загрузка контестов...
} + {status == 'failed' &&
Ошибка: {error}
} + {status == 'successful' && + <> + { + const endTime = new Date(contest.endsAt ?? new Date().toDateString()).getTime(); + return endTime >= now.getTime(); + })} + /> - { - const endTime = new Date(contest.endsAt).getTime(); - return endTime >= now.getTime(); - })} - /> - - { - const endTime = new Date(contest.endsAt).getTime(); - return endTime < now.getTime(); - })} - /> + { + const endTime = new Date(contest.endsAt ?? new Date().toDateString()).getTime(); + return endTime < now.getTime(); + })} + /> + + }
= ({ key={i} id={v.id} name={v.name} - startAt={v.startsAt} + startAt={v.startsAt ?? new Date().toString()} statusRegister={'reg'} duration={ - new Date(v.endsAt).getTime() - - new Date(v.startsAt).getTime() + new Date(v.endsAt ?? new Date().toString()).getTime() - + new Date(v.startsAt ?? new Date().toString()).getTime() } - members={v.members.length} + members={v.members?.length ?? 0} type={i % 2 ? 'second' : 'first'} /> ))} diff --git a/src/views/home/contests/ModalCreate.tsx b/src/views/home/contests/ModalCreate.tsx index 39d9998..6863b0d 100644 --- a/src/views/home/contests/ModalCreate.tsx +++ b/src/views/home/contests/ModalCreate.tsx @@ -37,7 +37,7 @@ const ModalCreateContest: FC = ({ attemptDurationMinutes: 0, maxAttempts: 0, allowEarlyFinish: false, - groupId: 0, + groupIds: [], missionIds: [], articleIds: [], }); @@ -48,13 +48,12 @@ const ModalCreateContest: FC = ({ useEffect(() => { if (status === 'successful') { - console.log('navigate'); - navigate( - `/contest/create?back=/home/account/contests&contestId=${contest.id}`, - ); dispatch( setContestStatus({ key: 'createContest', status: 'idle' }), ); + navigate( + `/contest/create?back=/home/account/contests&contestId=${contest.id}`, + ); } }, [status]);