import { createSlice, createAsyncThunk, PayloadAction } from '@reduxjs/toolkit'; 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; name: string; difficulty: number; tags: string[]; timeLimitMilliseconds: number; memoryLimitBytes: number; statements: string; } export interface Member { userId: number; username: string; role: string; } export interface Group { groupId: number; groupName: string; } export interface Contest { id: number; name: string; description?: string; scheduleType: 'AlwaysOpen' | 'FixedWindow' | 'RollingWindow'; visibility: 'Public' | 'GroupPrivate'; startsAt?: string; endsAt?: string; attemptDurationMinutes?: number; maxAttempts?: number; allowEarlyFinish?: boolean; groupId?: number; groupName?: string; missions?: Mission[]; articles?: any[]; members?: Member[]; } interface ContestsResponse { hasNextPage: boolean; contests: Contest[]; } export interface CreateContestBody { name: string; description?: string; scheduleType: 'AlwaysOpen' | 'FixedWindow' | 'RollingWindow'; visibility: 'Public' | 'GroupPrivate'; startsAt?: string; endsAt?: string; attemptDurationMinutes?: number; maxAttempts?: number; allowEarlyFinish?: boolean; groupId?: number; groupName?: string; missionIds?: number[]; articleIds?: number[]; } // ===================== // Состояние // ===================== type Status = 'idle' | 'loading' | 'successful' | 'failed'; interface ContestsState { fetchContests: { contests: Contest[]; hasNextPage: boolean; status: Status; error?: string; }; fetchContestById: { contest: Contest; status: Status; error?: string; }; createContest: { contest: Contest; status: Status; error?: string; }; fetchMySubmissions: { submissions: Submission[]; status: Status; error?: string; }; updateContest: { contest: Contest; status: Status; error?: string; }; deleteContest: { status: Status; error?: string; }; fetchMyContests: { contests: Contest[]; status: Status; error?: string; }; fetchRegisteredContests: { contests: Contest[]; hasNextPage: boolean; status: Status; error?: string; }; } const initialState: ContestsState = { fetchContests: { contests: [], hasNextPage: false, status: 'idle', error: undefined, }, fetchContestById: { contest: { id: 0, name: '', description: '', scheduleType: 'AlwaysOpen', visibility: 'Public', startsAt: '', endsAt: '', attemptDurationMinutes: 0, maxAttempts: 0, allowEarlyFinish: false, groupId: undefined, groupName: undefined, missions: [], articles: [], members: [], }, status: 'idle', error: undefined, }, fetchMySubmissions: { submissions: [], status: 'idle', error: undefined, }, createContest: { contest: { id: 0, name: '', description: '', scheduleType: 'AlwaysOpen', visibility: 'Public', startsAt: '', endsAt: '', attemptDurationMinutes: 0, maxAttempts: 0, allowEarlyFinish: false, groupId: undefined, groupName: undefined, missions: [], articles: [], members: [], }, status: 'idle', error: undefined, }, updateContest: { contest: { id: 0, name: '', description: '', scheduleType: 'AlwaysOpen', visibility: 'Public', startsAt: '', endsAt: '', attemptDurationMinutes: 0, maxAttempts: 0, allowEarlyFinish: false, groupId: undefined, groupName: undefined, missions: [], articles: [], members: [], }, status: 'idle', error: undefined, }, deleteContest: { status: 'idle', error: undefined, }, fetchMyContests: { contests: [], status: 'idle', error: undefined, }, fetchRegisteredContests: { contests: [], hasNextPage: false, status: 'idle', error: undefined, }, }; // ===================== // 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', async ( params: { page?: number; pageSize?: number; groupId?: number | null; } = {}, { rejectWithValue }, ) => { try { const { page = 0, pageSize = 10, groupId } = params; const response = await axios.get('/contests', { params: { page, pageSize, groupId }, }); return response.data; } catch (err: any) { return rejectWithValue( err.response?.data?.message || 'Failed to fetch contests', ); } }, ); // Контест по ID export const fetchContestById = createAsyncThunk( 'contests/fetchById', async (id: number, { rejectWithValue }) => { try { const response = await axios.get(`/contests/${id}`); return response.data; } catch (err: any) { return rejectWithValue( err.response?.data?.message || 'Failed to fetch contest', ); } }, ); // Создание контеста export const createContest = createAsyncThunk( 'contests/create', async (contestData: CreateContestBody, { rejectWithValue }) => { try { const response = await axios.post( '/contests', contestData, ); return response.data; } catch (err: any) { return rejectWithValue( err.response?.data?.message || 'Failed to create contest', ); } }, ); // 🆕 Обновление контеста export const updateContest = createAsyncThunk( 'contests/update', async ( { contestId, ...contestData }: { contestId: number } & CreateContestBody, { rejectWithValue }, ) => { try { const response = await axios.put( `/contests/${contestId}`, contestData, ); return response.data; } catch (err: any) { return rejectWithValue( err.response?.data?.message || 'Failed to update contest', ); } }, ); // 🆕 Удаление контеста export const deleteContest = createAsyncThunk( 'contests/delete', async (contestId: number, { rejectWithValue }) => { try { await axios.delete(`/contests/${contestId}`); return contestId; } catch (err: any) { return rejectWithValue( err.response?.data?.message || 'Failed to delete contest', ); } }, ); // Контесты, созданные мной export const fetchMyContests = createAsyncThunk( 'contests/fetchMyContests', async (_, { rejectWithValue }) => { try { const response = await axios.get('/contests/my'); return response.data; } catch (err: any) { return rejectWithValue( err.response?.data?.message || 'Failed to fetch my contests', ); } }, ); // Контесты, где я зарегистрирован export const fetchRegisteredContests = createAsyncThunk( 'contests/fetchRegisteredContests', async ( params: { page?: number; pageSize?: number } = {}, { rejectWithValue }, ) => { try { const { page = 0, pageSize = 10 } = params; const response = await axios.get( '/contests/registered', { params: { page, pageSize } }, ); return response.data; } catch (err: any) { return rejectWithValue( err.response?.data?.message || 'Failed to fetch registered contests', ); } }, ); // ===================== // Slice // ===================== const contestsSlice = createSlice({ name: 'contests', initialState, reducers: { // 🆕 Сброс статусов setContestStatus: ( state, action: PayloadAction<{ key: keyof ContestsState; status: Status }>, ) => { const { key, status } = action.payload; if (state[key]) { (state[key] as any).status = status; } }, }, 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'; state.fetchContests.error = undefined; }); builder.addCase( fetchContests.fulfilled, (state, action: PayloadAction) => { state.fetchContests.status = 'successful'; state.fetchContests.contests = action.payload.contests; state.fetchContests.hasNextPage = action.payload.hasNextPage; }, ); builder.addCase(fetchContests.rejected, (state, action: any) => { state.fetchContests.status = 'failed'; state.fetchContests.error = action.payload; }); // fetchContestById builder.addCase(fetchContestById.pending, (state) => { state.fetchContestById.status = 'loading'; state.fetchContestById.error = undefined; }); builder.addCase( fetchContestById.fulfilled, (state, action: PayloadAction) => { state.fetchContestById.status = 'successful'; state.fetchContestById.contest = action.payload; }, ); builder.addCase(fetchContestById.rejected, (state, action: any) => { state.fetchContestById.status = 'failed'; state.fetchContestById.error = action.payload; }); // createContest builder.addCase(createContest.pending, (state) => { state.createContest.status = 'loading'; state.createContest.error = undefined; }); builder.addCase( createContest.fulfilled, (state, action: PayloadAction) => { state.createContest.status = 'successful'; state.createContest.contest = action.payload; }, ); builder.addCase(createContest.rejected, (state, action: any) => { state.createContest.status = 'failed'; state.createContest.error = action.payload; }); // 🆕 updateContest builder.addCase(updateContest.pending, (state) => { state.updateContest.status = 'loading'; state.updateContest.error = undefined; }); builder.addCase( updateContest.fulfilled, (state, action: PayloadAction) => { state.updateContest.status = 'successful'; state.updateContest.contest = action.payload; }, ); builder.addCase(updateContest.rejected, (state, action: any) => { state.updateContest.status = 'failed'; state.updateContest.error = action.payload; }); // 🆕 deleteContest builder.addCase(deleteContest.pending, (state) => { state.deleteContest.status = 'loading'; state.deleteContest.error = undefined; }); builder.addCase( deleteContest.fulfilled, (state, action: PayloadAction) => { state.deleteContest.status = 'successful'; // Удалим контест из списков state.fetchContests.contests = state.fetchContests.contests.filter( (c) => c.id !== action.payload, ); state.fetchMyContests.contests = state.fetchMyContests.contests.filter( (c) => c.id !== action.payload, ); }, ); builder.addCase(deleteContest.rejected, (state, action: any) => { state.deleteContest.status = 'failed'; state.deleteContest.error = action.payload; }); // fetchMyContests builder.addCase(fetchMyContests.pending, (state) => { state.fetchMyContests.status = 'loading'; state.fetchMyContests.error = undefined; }); builder.addCase( fetchMyContests.fulfilled, (state, action: PayloadAction) => { state.fetchMyContests.status = 'successful'; state.fetchMyContests.contests = action.payload; }, ); builder.addCase(fetchMyContests.rejected, (state, action: any) => { state.fetchMyContests.status = 'failed'; state.fetchMyContests.error = action.payload; }); // fetchRegisteredContests builder.addCase(fetchRegisteredContests.pending, (state) => { state.fetchRegisteredContests.status = 'loading'; state.fetchRegisteredContests.error = undefined; }); builder.addCase( fetchRegisteredContests.fulfilled, (state, action: PayloadAction) => { state.fetchRegisteredContests.status = 'successful'; state.fetchRegisteredContests.contests = action.payload.contests; state.fetchRegisteredContests.hasNextPage = action.payload.hasNextPage; }, ); builder.addCase( fetchRegisteredContests.rejected, (state, action: any) => { state.fetchRegisteredContests.status = 'failed'; state.fetchRegisteredContests.error = action.payload; }, ); }, }); // ===================== // Экспорты // ===================== export const { setContestStatus } = contestsSlice.actions; export const contestsReducer = contestsSlice.reducer;