import { createSlice, createAsyncThunk, PayloadAction } from '@reduxjs/toolkit'; import axios from '../../axios'; // ===================== // Типы // ===================== export interface Mission { id: number; authorId: number; name: string; difficulty: number; tags: string[]; createdAt: string; updatedAt: string; timeLimitMilliseconds: number; memoryLimitBytes: number; statements: null; } export interface Member { userId: number; username: string; role: string; } export interface Contest { id: number; name: string; description: string; scheduleType: string; startsAt: string; endsAt: string; attemptDurationMinutes: number | null; maxAttempts: number | null; allowEarlyFinish: boolean | null; groupId: number | null; groupName: string | null; missions: Mission[]; articles: any[]; members: Member[]; } interface ContestsResponse { hasNextPage: boolean; contests: Contest[]; } export interface CreateContestBody { name?: string | null; description?: string | null; scheduleType: 'AlwaysOpen' | 'FixedWindow' | 'RollingWindow'; visibility: 'Public' | 'GroupPrivate'; startsAt?: string | null; endsAt?: string | null; attemptDurationMinutes?: number | null; maxAttempts?: number | null; allowEarlyFinish?: boolean | null; groupId?: number | null; missionIds?: number[] | null; articleIds?: number[] | null; participantIds?: number[] | null; organizerIds?: number[] | null; } // ===================== // Состояние // ===================== type Status = 'idle' | 'loading' | 'successful' | 'failed'; interface ContestsState { fetchContests: { contests: Contest[]; hasNextPage: boolean; status: Status; error: string | null; }; fetchContestById: { contest: Contest | null; status: Status; error: string | null; }; createContest: { contest: Contest | null; status: Status; error: string | null; }; fetchMyContests: { contests: Contest[]; status: Status; error: string | null; }; fetchRegisteredContests: { contests: Contest[]; hasNextPage: boolean; status: Status; error: string | null; }; } const initialState: ContestsState = { fetchContests: { contests: [], hasNextPage: false, status: 'idle', error: null, }, fetchContestById: { contest: null, status: 'idle', error: null, }, createContest: { contest: null, status: 'idle', error: null, }, fetchMyContests: { contests: [], status: 'idle', error: null, }, fetchRegisteredContests: { contests: [], hasNextPage: false, status: 'idle', error: null, }, }; // ===================== // Async Thunks // ===================== // Все контесты 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 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: { clearSelectedContest: (state) => { state.fetchContestById.contest = null; }, }, extraReducers: (builder) => { // fetchContests builder.addCase(fetchContests.pending, (state) => { state.fetchContests.status = 'loading'; state.fetchContests.error = null; }); 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 = null; }); 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 = null; }); 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; }); // fetchMyContests // fetchMyContests builder.addCase(fetchMyContests.pending, (state) => { state.fetchMyContests.status = 'loading'; state.fetchMyContests.error = null; }); 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 = null; }); 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 { clearSelectedContest } = contestsSlice.actions; export const contestsReducer = contestsSlice.reducer;