upload mission modal

This commit is contained in:
Виталий Лавшонок
2025-11-04 14:59:45 +03:00
parent 2e3a8779fc
commit 3cd8e14288
7 changed files with 380 additions and 250 deletions

View File

@@ -1,146 +1,215 @@
import { createSlice, createAsyncThunk, PayloadAction } from "@reduxjs/toolkit";
import axios from "../../axios";
import { createSlice, createAsyncThunk, PayloadAction } from '@reduxjs/toolkit';
import axios from '../../axios';
// ─── Типы ────────────────────────────────────────────
type Status = 'idle' | 'loading' | 'successful' | 'failed';
// Типы данных
interface Statement {
id: number;
language: string;
statementTexts: Record<string, string>;
mediaFiles?: { id: number; fileName: string; mediaUrl: string }[];
id: number;
language: string;
statementTexts: Record<string, string>;
mediaFiles?: { id: number; fileName: string; mediaUrl: string }[];
}
interface Mission {
id: number;
authorId: number;
name: string;
difficulty: number;
tags: string[];
createdAt: string;
updatedAt: string;
statements?: Statement[];
export interface Mission {
id: number;
authorId: number;
name: string;
difficulty: number;
tags: string[];
createdAt: string;
updatedAt: string;
statements?: Statement[];
}
interface MissionsState {
missions: Mission[];
currentMission: Mission | null;
hasNextPage: boolean;
status: "idle" | "loading" | "successful" | "failed";
error: string | null;
missions: Mission[];
currentMission: Mission | null;
hasNextPage: boolean;
statuses: {
fetchList: Status;
fetchById: Status;
upload: Status;
};
error: string | null;
}
// Инициализация состояния
// ─── Инициализация состояния ──────────────────────────
const initialState: MissionsState = {
missions: [],
currentMission: null,
hasNextPage: false,
status: "idle",
error: null,
missions: [],
currentMission: null,
hasNextPage: false,
statuses: {
fetchList: 'idle',
fetchById: 'idle',
upload: 'idle',
},
error: null,
};
// AsyncThunk: Получение списка миссий
// ─── Async Thunks ─────────────────────────────────────
// GET /missions
export const fetchMissions = createAsyncThunk(
"missions/fetchMissions",
async (
{ page = 0, pageSize = 10, tags = [] }: { page?: number; pageSize?: number; tags?: string[] },
{ rejectWithValue }
) => {
try {
const params: any = { page, pageSize };
if (tags) params.tags = tags;
const response = await axios.get("/missions", { params });
return response.data; // { hasNextPage, missions }
} catch (err: any) {
return rejectWithValue(err.response?.data?.message || "Failed to fetch missions");
}
}
'missions/fetchMissions',
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 });
return response.data; // { missions, hasNextPage }
} catch (err: any) {
return rejectWithValue(
err.response?.data?.message || 'Ошибка при получении миссий',
);
}
},
);
// AsyncThunk: Получение миссии по id
// 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?.message || "Failed to fetch mission");
}
}
'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?.message || 'Ошибка при получении миссии',
);
}
},
);
// AsyncThunk: Загрузка миссии
// POST /missions/upload
export const uploadMission = createAsyncThunk(
"missions/uploadMission",
async (
{ file, name, difficulty, tags }: { file: File; name: string; difficulty: number; tags: string[] },
{ rejectWithValue }
) => {
try {
const formData = new FormData();
formData.append("MissionFile", file);
formData.append("Name", name);
formData.append("Difficulty", difficulty.toString());
tags.forEach(tag => formData.append("Tags", tag));
'missions/uploadMission',
async (
{
file,
name,
difficulty,
tags,
}: { file: File; name: string; difficulty: number; tags: string[] },
{ rejectWithValue },
) => {
try {
const formData = new FormData();
formData.append('MissionFile', file);
formData.append('Name', name);
formData.append('Difficulty', difficulty.toString());
tags.forEach((tag) => formData.append('Tags', tag));
const response = await axios.post("/missions/upload", formData, {
headers: { "Content-Type": "multipart/form-data" },
});
return response.data; // Mission
} catch (err: any) {
return rejectWithValue(err.response?.data?.message || "Failed to upload mission");
}
}
const response = await axios.post('/missions/upload', formData, {
headers: { 'Content-Type': 'multipart/form-data' },
});
return response.data; // Mission
} catch (err: any) {
return rejectWithValue(
err.response?.data?.message || 'Ошибка при загрузке миссии',
);
}
},
);
// Slice
// ─── Slice ────────────────────────────────────────────
const missionsSlice = createSlice({
name: "missions",
initialState,
reducers: {},
extraReducers: (builder) => {
// fetchMissions
builder.addCase(fetchMissions.pending, (state) => {
state.status = "loading";
state.error = null;
});
builder.addCase(fetchMissions.fulfilled, (state, action: PayloadAction<{ missions: Mission[]; hasNextPage: boolean }>) => {
state.status = "successful";
state.missions = action.payload.missions;
state.hasNextPage = action.payload.hasNextPage;
});
builder.addCase(fetchMissions.rejected, (state, action: PayloadAction<any>) => {
state.status = "failed";
state.error = action.payload;
});
name: 'missions',
initialState,
reducers: {
clearCurrentMission: (state) => {
state.currentMission = null;
},
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';
state.error = action.payload;
},
);
// fetchMissionById
builder.addCase(fetchMissionById.pending, (state) => {
state.status = "loading";
state.error = null;
});
builder.addCase(fetchMissionById.fulfilled, (state, action: PayloadAction<Mission>) => {
state.status = "successful";
state.currentMission = action.payload;
});
builder.addCase(fetchMissionById.rejected, (state, action: PayloadAction<any>) => {
state.status = "failed";
state.error = action.payload;
});
// ─── 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';
state.error = action.payload;
},
);
// uploadMission
builder.addCase(uploadMission.pending, (state) => {
state.status = "loading";
state.error = null;
});
builder.addCase(uploadMission.fulfilled, (state, action: PayloadAction<Mission>) => {
state.status = "successful";
state.missions.unshift(action.payload); // Добавляем новую миссию в начало списка
});
builder.addCase(uploadMission.rejected, (state, action: PayloadAction<any>) => {
state.status = "failed";
state.error = action.payload;
});
},
// ─── 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';
state.error = action.payload;
},
);
},
});
export const { clearCurrentMission, setMissionsStatus } = missionsSlice.actions;
export const missionsReducer = missionsSlice.reducer;