All checks were successful
Build and Push Docker Image / build (push) Successful in 53s
432 lines
13 KiB
TypeScript
432 lines
13 KiB
TypeScript
import { createSlice, createAsyncThunk, PayloadAction } from '@reduxjs/toolkit';
|
||
import axios from '../../axios';
|
||
import { toastError } from '../../lib/toastNotification';
|
||
import {
|
||
buildUploadMissionFormData,
|
||
getProblemXmlMissingNameMessage,
|
||
UploadMissionRequest,
|
||
} from '../../api/missionsUpload';
|
||
|
||
// ─── Типы ────────────────────────────────────────────
|
||
|
||
type Status = 'idle' | 'loading' | 'successful' | 'failed';
|
||
|
||
interface Statement {
|
||
id: number;
|
||
language: string;
|
||
statementTexts: Record<string, string>;
|
||
mediaFiles?: { id: number; fileName: string; mediaUrl: string }[];
|
||
}
|
||
|
||
export interface Mission {
|
||
id: number;
|
||
authorId: number;
|
||
name: string;
|
||
difficulty: number;
|
||
tags: string[];
|
||
createdAt: string;
|
||
updatedAt: string;
|
||
timeLimit: number;
|
||
memoryLimit: number;
|
||
statements?: Statement[];
|
||
}
|
||
|
||
interface MissionsState {
|
||
missions: Mission[];
|
||
newMissions: Mission[];
|
||
currentMission: Mission | null;
|
||
hasNextPage: boolean;
|
||
create: {
|
||
errors?: Record<string, string[]>;
|
||
};
|
||
statuses: {
|
||
fetchList: Status;
|
||
fetchById: Status;
|
||
upload: Status;
|
||
fetchMy: Status;
|
||
delete: Status;
|
||
};
|
||
error: string | null;
|
||
}
|
||
|
||
// ─── Инициализация состояния ──────────────────────────
|
||
|
||
const initialState: MissionsState = {
|
||
missions: [],
|
||
newMissions: [],
|
||
currentMission: null,
|
||
hasNextPage: false,
|
||
create: {},
|
||
statuses: {
|
||
fetchList: 'idle',
|
||
fetchById: 'idle',
|
||
upload: 'idle',
|
||
fetchMy: 'idle',
|
||
delete: 'idle',
|
||
},
|
||
error: null,
|
||
};
|
||
|
||
// ─── Async Thunks ─────────────────────────────────────
|
||
|
||
// GET /missions
|
||
export const fetchMissions = createAsyncThunk(
|
||
'missions/fetchMissions',
|
||
async (
|
||
{
|
||
page = 0,
|
||
pageSize = 100,
|
||
tags = [],
|
||
}: { page?: number; pageSize?: number; tags?: string[] },
|
||
{ rejectWithValue },
|
||
) => {
|
||
try {
|
||
const params: any = { page, pageSize };
|
||
if (tags.length) params.tags = tags;
|
||
const response = await axios.get('/missions', {
|
||
params,
|
||
paramsSerializer: {
|
||
indexes: null,
|
||
},
|
||
});
|
||
return response.data; // { missions, hasNextPage }
|
||
} catch (err: any) {
|
||
return rejectWithValue(err.response?.data);
|
||
}
|
||
},
|
||
);
|
||
|
||
// GET /missions
|
||
export const fetchNewMissions = createAsyncThunk(
|
||
'missions/fetchNewMissions',
|
||
async (
|
||
{
|
||
page = 0,
|
||
pageSize = 10,
|
||
tags = [],
|
||
}: { page?: number; pageSize?: number; tags?: string[] },
|
||
{ rejectWithValue },
|
||
) => {
|
||
try {
|
||
const params: any = { page, pageSize };
|
||
if (tags.length) params.tags = tags;
|
||
const response = await axios.get('/missions', {
|
||
params,
|
||
paramsSerializer: {
|
||
indexes: null,
|
||
},
|
||
});
|
||
return response.data; // { missions, hasNextPage }
|
||
} catch (err: any) {
|
||
return rejectWithValue(err.response?.data);
|
||
}
|
||
},
|
||
);
|
||
|
||
// GET /missions/{id}
|
||
export const fetchMissionById = createAsyncThunk(
|
||
'missions/fetchMissionById',
|
||
async (id: number, { rejectWithValue }) => {
|
||
try {
|
||
const response = await axios.get(`/missions/${id}`);
|
||
return response.data; // Mission
|
||
} catch (err: any) {
|
||
return rejectWithValue(err.response?.data);
|
||
}
|
||
},
|
||
);
|
||
|
||
// ✅ GET /missions/my
|
||
export const fetchMyMissions = createAsyncThunk(
|
||
'missions/fetchMyMissions',
|
||
async (_, { rejectWithValue }) => {
|
||
try {
|
||
const response = await axios.get('/missions/my');
|
||
return response.data as Mission[]; // массив миссий пользователя
|
||
} catch (err: any) {
|
||
return rejectWithValue(err.response?.data);
|
||
}
|
||
},
|
||
);
|
||
|
||
// POST /missions/upload
|
||
export const uploadMission = createAsyncThunk(
|
||
'missions/uploadMission',
|
||
async (
|
||
request: UploadMissionRequest,
|
||
{ rejectWithValue },
|
||
) => {
|
||
try {
|
||
const formData = buildUploadMissionFormData(request);
|
||
|
||
const response = await axios.post('/missions/upload', formData, {
|
||
headers: { 'Content-Type': 'multipart/form-data' },
|
||
});
|
||
return response.data; // Mission
|
||
} catch (err: any) {
|
||
const status = err?.response?.status;
|
||
const responseData = err?.response?.data;
|
||
|
||
if (status === 400) {
|
||
const msg = getProblemXmlMissingNameMessage(responseData);
|
||
if (msg) {
|
||
return rejectWithValue({
|
||
errors: {
|
||
missionFile: [msg],
|
||
},
|
||
});
|
||
}
|
||
}
|
||
|
||
if (responseData?.errors) {
|
||
return rejectWithValue(responseData);
|
||
}
|
||
|
||
const fallback =
|
||
typeof responseData === 'string'
|
||
? responseData
|
||
: 'Не удалось загрузить миссию';
|
||
|
||
return rejectWithValue({
|
||
errors: {
|
||
general: [fallback],
|
||
},
|
||
});
|
||
}
|
||
},
|
||
);
|
||
|
||
// DELETE /missions/{id}
|
||
export const deleteMission = createAsyncThunk(
|
||
'missions/deleteMission',
|
||
async (id: number, { rejectWithValue }) => {
|
||
try {
|
||
await axios.delete(`/missions/${id}`);
|
||
return id; // возвращаем id удалённой миссии
|
||
} catch (err: any) {
|
||
return rejectWithValue(err.response?.data);
|
||
}
|
||
},
|
||
);
|
||
|
||
// ─── Slice ────────────────────────────────────────────
|
||
|
||
const missionsSlice = createSlice({
|
||
name: 'missions',
|
||
initialState,
|
||
reducers: {
|
||
setMissionsStatus: (
|
||
state,
|
||
action: PayloadAction<{
|
||
key: keyof MissionsState['statuses'];
|
||
status: Status;
|
||
}>,
|
||
) => {
|
||
const { key, status } = action.payload;
|
||
state.statuses[key] = status;
|
||
},
|
||
},
|
||
extraReducers: (builder) => {
|
||
// ─── FETCH MISSIONS ───
|
||
builder.addCase(fetchMissions.pending, (state) => {
|
||
state.statuses.fetchList = 'loading';
|
||
state.error = null;
|
||
});
|
||
builder.addCase(
|
||
fetchMissions.fulfilled,
|
||
(
|
||
state,
|
||
action: PayloadAction<{
|
||
missions: Mission[];
|
||
hasNextPage: boolean;
|
||
}>,
|
||
) => {
|
||
state.statuses.fetchList = 'successful';
|
||
state.missions = action.payload.missions;
|
||
state.hasNextPage = action.payload.hasNextPage;
|
||
},
|
||
);
|
||
builder.addCase(
|
||
fetchMissions.rejected,
|
||
(state, action: PayloadAction<any>) => {
|
||
state.statuses.fetchList = 'failed';
|
||
|
||
const errors = action.payload.errors as Record<
|
||
string,
|
||
string[]
|
||
>;
|
||
Object.values(errors).forEach((messages) => {
|
||
messages.forEach((msg) => {
|
||
toastError(msg);
|
||
});
|
||
});
|
||
},
|
||
);
|
||
|
||
// ─── FETCH NEW MISSIONS ───
|
||
builder.addCase(fetchNewMissions.pending, (state) => {
|
||
state.statuses.fetchList = 'loading';
|
||
state.error = null;
|
||
});
|
||
builder.addCase(
|
||
fetchNewMissions.fulfilled,
|
||
(
|
||
state,
|
||
action: PayloadAction<{
|
||
missions: Mission[];
|
||
hasNextPage: boolean;
|
||
}>,
|
||
) => {
|
||
state.statuses.fetchList = 'successful';
|
||
state.newMissions = action.payload.missions;
|
||
state.hasNextPage = action.payload.hasNextPage;
|
||
},
|
||
);
|
||
builder.addCase(
|
||
fetchNewMissions.rejected,
|
||
(state, action: PayloadAction<any>) => {
|
||
state.statuses.fetchList = 'failed';
|
||
|
||
const errors = action.payload.errors as Record<
|
||
string,
|
||
string[]
|
||
>;
|
||
Object.values(errors).forEach((messages) => {
|
||
messages.forEach((msg) => {
|
||
toastError(msg);
|
||
});
|
||
});
|
||
},
|
||
);
|
||
|
||
// ─── FETCH MISSION BY ID ───
|
||
builder.addCase(fetchMissionById.pending, (state) => {
|
||
state.statuses.fetchById = 'loading';
|
||
state.error = null;
|
||
});
|
||
builder.addCase(
|
||
fetchMissionById.fulfilled,
|
||
(state, action: PayloadAction<Mission>) => {
|
||
state.statuses.fetchById = 'successful';
|
||
state.currentMission = action.payload;
|
||
},
|
||
);
|
||
builder.addCase(
|
||
fetchMissionById.rejected,
|
||
(state, action: PayloadAction<any>) => {
|
||
state.statuses.fetchById = 'failed';
|
||
|
||
const errors = action.payload.errors as Record<
|
||
string,
|
||
string[]
|
||
>;
|
||
Object.values(errors).forEach((messages) => {
|
||
messages.forEach((msg) => {
|
||
toastError(msg);
|
||
});
|
||
});
|
||
},
|
||
);
|
||
|
||
// ✅ FETCH MY MISSIONS ───
|
||
builder.addCase(fetchMyMissions.pending, (state) => {
|
||
state.statuses.fetchMy = 'loading';
|
||
state.error = null;
|
||
});
|
||
builder.addCase(
|
||
fetchMyMissions.fulfilled,
|
||
(state, action: PayloadAction<Mission[]>) => {
|
||
state.statuses.fetchMy = 'successful';
|
||
state.missions = action.payload;
|
||
},
|
||
);
|
||
builder.addCase(
|
||
fetchMyMissions.rejected,
|
||
(state, action: PayloadAction<any>) => {
|
||
state.statuses.fetchMy = 'failed';
|
||
|
||
const errors = action.payload.errors as Record<
|
||
string,
|
||
string[]
|
||
>;
|
||
Object.values(errors).forEach((messages) => {
|
||
messages.forEach((msg) => {
|
||
toastError(msg);
|
||
});
|
||
});
|
||
},
|
||
);
|
||
|
||
// ─── UPLOAD MISSION ───
|
||
builder.addCase(uploadMission.pending, (state) => {
|
||
state.statuses.upload = 'loading';
|
||
state.error = null;
|
||
});
|
||
builder.addCase(
|
||
uploadMission.fulfilled,
|
||
(state, action: PayloadAction<Mission>) => {
|
||
state.statuses.upload = 'successful';
|
||
state.missions.unshift(action.payload);
|
||
},
|
||
);
|
||
builder.addCase(
|
||
uploadMission.rejected,
|
||
(state, action: PayloadAction<any>) => {
|
||
state.statuses.upload = 'failed';
|
||
|
||
const errors = action.payload?.errors as
|
||
| Record<string, string[]>
|
||
| undefined;
|
||
if (errors) {
|
||
Object.values(errors).forEach((messages) => {
|
||
messages.forEach((msg) => {
|
||
toastError(msg);
|
||
});
|
||
});
|
||
state.create.errors = errors;
|
||
} else {
|
||
toastError('Не удалось загрузить миссию');
|
||
}
|
||
},
|
||
);
|
||
|
||
// ─── DELETE MISSION ───
|
||
builder.addCase(deleteMission.pending, (state) => {
|
||
state.statuses.delete = 'loading';
|
||
state.error = null;
|
||
});
|
||
builder.addCase(
|
||
deleteMission.fulfilled,
|
||
(state, action: PayloadAction<number>) => {
|
||
state.statuses.delete = 'successful';
|
||
state.missions = state.missions.filter(
|
||
(m) => m.id !== action.payload,
|
||
);
|
||
|
||
if (state.currentMission?.id === action.payload) {
|
||
state.currentMission = null;
|
||
}
|
||
},
|
||
);
|
||
builder.addCase(
|
||
deleteMission.rejected,
|
||
(state, action: PayloadAction<any>) => {
|
||
state.statuses.delete = 'failed';
|
||
|
||
const errors = action.payload.errors as Record<
|
||
string,
|
||
string[]
|
||
>;
|
||
Object.values(errors).forEach((messages) => {
|
||
messages.forEach((msg) => {
|
||
toastError(msg);
|
||
});
|
||
});
|
||
},
|
||
);
|
||
},
|
||
});
|
||
|
||
export const { setMissionsStatus } = missionsSlice.actions;
|
||
export const missionsReducer = missionsSlice.reducer;
|