Files
LiquidCode_Frontend/src/redux/slices/missions.ts
Roman Pytkov 79b146786b
All checks were successful
Build and Push Docker Image / build (push) Successful in 53s
Изменена модалка добавления миссии
2025-12-25 16:47:48 +03:00

432 lines
13 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';
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;