239 lines
6.4 KiB
TypeScript
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;
|