Files
LiquidCode_Frontend/src/redux/slices/contests.ts
Виталий Лавшонок cdb5595769 contests
2025-11-04 19:33:47 +03:00

239 lines
6.4 KiB
TypeScript

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<ContestsResponse>('/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<Contest>(`/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<Contest>(
'/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<ContestsResponse>) => {
state.statuses.fetchList = 'successful';
state.contests = action.payload.contests;
state.hasNextPage = action.payload.hasNextPage;
},
);
builder.addCase(
fetchContests.rejected,
(state, action: PayloadAction<any>) => {
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<Contest>) => {
state.statuses.fetchById = 'successful';
state.selectedContest = action.payload;
},
);
builder.addCase(
fetchContestById.rejected,
(state, action: PayloadAction<any>) => {
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<Contest>) => {
state.statuses.create = 'successful';
state.contests.unshift(action.payload);
},
);
builder.addCase(
createContest.rejected,
(state, action: PayloadAction<any>) => {
state.statuses.create = 'failed';
state.error = action.payload;
},
);
},
});
// =====================
// Экспорты
// =====================
export const { clearSelectedContest, setContestStatus } = contestsSlice.actions;
export const contestsReducer = contestsSlice.reducer;