Files
LiquidCode_Frontend/src/redux/slices/contests.ts
Виталий Лавшонок dfc2985209 auth + groups invite
2025-11-15 17:37:47 +03:00

575 lines
16 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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<Submission[]>(
`/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<ContestsResponse>('/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<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',
);
}
},
);
// 🆕 Обновление контеста
export const updateContest = createAsyncThunk(
'contests/update',
async (
{
contestId,
...contestData
}: { contestId: number } & CreateContestBody,
{ rejectWithValue },
) => {
try {
const response = await axios.put<Contest>(
`/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<Contest[]>('/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<ContestsResponse>(
'/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<Submission[]>) => {
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<ContestsResponse>) => {
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<Contest>) => {
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<Contest>) => {
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<Contest>) => {
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<number>) => {
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<Contest[]>) => {
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<ContestsResponse>) => {
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;