import { createSlice, createAsyncThunk, PayloadAction } from '@reduxjs/toolkit'; import axios from '../../axios'; // ===================== // Типы // ===================== export interface Mission { missionId: number; name: string; sortOrder: number; } 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 { contests: Contest[]; selectedContest: Contest | null; hasNextPage: boolean; statuses: { fetchList: Status; fetchById: Status; create: Status; }; error: string | null; } const initialState: ContestsState = { contests: [], selectedContest: null, hasNextPage: false, statuses: { fetchList: 'idle', fetchById: 'idle', create: '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', ); } }, ); 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', ); } }, ); // ===================== // Slice // ===================== const contestsSlice = createSlice({ name: 'contests', initialState, reducers: { clearSelectedContest: (state) => { state.selectedContest = null; }, setContestStatus: ( state, action: PayloadAction<{ key: keyof ContestsState['statuses']; status: Status; }>, ) => { state.statuses[action.payload.key] = action.payload.status; }, }, extraReducers: (builder) => { // fetchContests builder.addCase(fetchContests.pending, (state) => { state.statuses.fetchList = 'loading'; state.error = null; }); builder.addCase( fetchContests.fulfilled, (state, action: PayloadAction) => { state.statuses.fetchList = 'successful'; state.contests = action.payload.contests; state.hasNextPage = action.payload.hasNextPage; }, ); builder.addCase( fetchContests.rejected, (state, action: PayloadAction) => { state.statuses.fetchList = 'failed'; state.error = action.payload; }, ); // fetchContestById builder.addCase(fetchContestById.pending, (state) => { state.statuses.fetchById = 'loading'; state.error = null; }); builder.addCase( fetchContestById.fulfilled, (state, action: PayloadAction) => { state.statuses.fetchById = 'successful'; state.selectedContest = action.payload; }, ); builder.addCase( fetchContestById.rejected, (state, action: PayloadAction) => { state.statuses.fetchById = 'failed'; state.error = action.payload; }, ); // createContest builder.addCase(createContest.pending, (state) => { state.statuses.create = 'loading'; state.error = null; }); builder.addCase( createContest.fulfilled, (state, action: PayloadAction) => { state.statuses.create = 'successful'; state.contests.unshift(action.payload); }, ); builder.addCase( createContest.rejected, (state, action: PayloadAction) => { state.statuses.create = 'failed'; state.error = action.payload; }, ); }, }); // ===================== // Экспорты // ===================== export const { clearSelectedContest, setContestStatus } = contestsSlice.actions; export const contestsReducer = contestsSlice.reducer;