formatting
This commit is contained in:
@@ -1,188 +1,260 @@
|
||||
import { createSlice, createAsyncThunk, PayloadAction } from "@reduxjs/toolkit";
|
||||
import axios from "../../axios";
|
||||
import { createSlice, createAsyncThunk, PayloadAction } from '@reduxjs/toolkit';
|
||||
import axios from '../../axios';
|
||||
|
||||
// Типы данных
|
||||
interface AuthState {
|
||||
jwt: string | null;
|
||||
refreshToken: string | null;
|
||||
username: string | null;
|
||||
status: "idle" | "loading" | "successful" | "failed";
|
||||
error: string | null;
|
||||
jwt: string | null;
|
||||
refreshToken: string | null;
|
||||
username: string | null;
|
||||
status: 'idle' | 'loading' | 'successful' | 'failed';
|
||||
error: string | null;
|
||||
}
|
||||
|
||||
// Инициализация состояния
|
||||
const initialState: AuthState = {
|
||||
jwt: null,
|
||||
refreshToken: null,
|
||||
username: null,
|
||||
status: "idle",
|
||||
error: null,
|
||||
jwt: null,
|
||||
refreshToken: null,
|
||||
username: null,
|
||||
status: 'idle',
|
||||
error: null,
|
||||
};
|
||||
|
||||
// AsyncThunk: Регистрация
|
||||
export const registerUser = createAsyncThunk(
|
||||
"auth/register",
|
||||
async (
|
||||
{ username, email, password }: { username: string; email: string; password: string },
|
||||
{ rejectWithValue }
|
||||
) => {
|
||||
try {
|
||||
const response = await axios.post("/authentication/register", { username, email, password });
|
||||
return response.data; // { jwt, refreshToken }
|
||||
} catch (err: any) {
|
||||
return rejectWithValue(err.response?.data?.message || "Registration failed");
|
||||
}
|
||||
}
|
||||
'auth/register',
|
||||
async (
|
||||
{
|
||||
username,
|
||||
email,
|
||||
password,
|
||||
}: { username: string; email: string; password: string },
|
||||
{ rejectWithValue },
|
||||
) => {
|
||||
try {
|
||||
const response = await axios.post('/authentication/register', {
|
||||
username,
|
||||
email,
|
||||
password,
|
||||
});
|
||||
return response.data; // { jwt, refreshToken }
|
||||
} catch (err: any) {
|
||||
return rejectWithValue(
|
||||
err.response?.data?.message || 'Registration failed',
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
// AsyncThunk: Логин
|
||||
export const loginUser = createAsyncThunk(
|
||||
"auth/login",
|
||||
async (
|
||||
{ username, password }: { username: string; password: string },
|
||||
{ rejectWithValue }
|
||||
) => {
|
||||
try {
|
||||
const response = await axios.post("/authentication/login", { username, password });
|
||||
return response.data; // { jwt, refreshToken }
|
||||
} catch (err: any) {
|
||||
return rejectWithValue(err.response?.data?.message || "Login failed");
|
||||
}
|
||||
}
|
||||
'auth/login',
|
||||
async (
|
||||
{ username, password }: { username: string; password: string },
|
||||
{ rejectWithValue },
|
||||
) => {
|
||||
try {
|
||||
const response = await axios.post('/authentication/login', {
|
||||
username,
|
||||
password,
|
||||
});
|
||||
return response.data; // { jwt, refreshToken }
|
||||
} catch (err: any) {
|
||||
return rejectWithValue(
|
||||
err.response?.data?.message || 'Login failed',
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
// AsyncThunk: Обновление токена
|
||||
export const refreshToken = createAsyncThunk(
|
||||
"auth/refresh",
|
||||
async ({ refreshToken }: { refreshToken: string }, { rejectWithValue }) => {
|
||||
try {
|
||||
const response = await axios.post("/authentication/refresh", { refreshToken });
|
||||
return response.data; // { username }
|
||||
} catch (err: any) {
|
||||
return rejectWithValue(err.response?.data?.message || "Refresh token failed");
|
||||
}
|
||||
}
|
||||
'auth/refresh',
|
||||
async ({ refreshToken }: { refreshToken: string }, { rejectWithValue }) => {
|
||||
try {
|
||||
const response = await axios.post('/authentication/refresh', {
|
||||
refreshToken,
|
||||
});
|
||||
return response.data; // { username }
|
||||
} catch (err: any) {
|
||||
return rejectWithValue(
|
||||
err.response?.data?.message || 'Refresh token failed',
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
// AsyncThunk: Получение информации о пользователе
|
||||
export const fetchWhoAmI = createAsyncThunk(
|
||||
"auth/whoami",
|
||||
async (_, { rejectWithValue }) => {
|
||||
try {
|
||||
const response = await axios.get("/authentication/whoami");
|
||||
return response.data; // { username }
|
||||
} catch (err: any) {
|
||||
return rejectWithValue(err.response?.data?.message || "Failed to fetch user info");
|
||||
}
|
||||
}
|
||||
'auth/whoami',
|
||||
async (_, { rejectWithValue }) => {
|
||||
try {
|
||||
const response = await axios.get('/authentication/whoami');
|
||||
return response.data; // { username }
|
||||
} catch (err: any) {
|
||||
return rejectWithValue(
|
||||
err.response?.data?.message || 'Failed to fetch user info',
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
// AsyncThunk: Загрузка токенов из localStorage
|
||||
export const loadTokensFromLocalStorage = createAsyncThunk(
|
||||
"auth/loadTokens",
|
||||
async (_, { }) => {
|
||||
const jwt = localStorage.getItem("jwt");
|
||||
const refreshToken = localStorage.getItem("refreshToken");
|
||||
'auth/loadTokens',
|
||||
async (_, {}) => {
|
||||
const jwt = localStorage.getItem('jwt');
|
||||
const refreshToken = localStorage.getItem('refreshToken');
|
||||
|
||||
if (jwt && refreshToken) {
|
||||
axios.defaults.headers.common['Authorization'] = `Bearer ${jwt}`;
|
||||
return { jwt, refreshToken };
|
||||
} else {
|
||||
return { jwt: null, refreshToken: null };
|
||||
}
|
||||
}
|
||||
if (jwt && refreshToken) {
|
||||
axios.defaults.headers.common['Authorization'] = `Bearer ${jwt}`;
|
||||
return { jwt, refreshToken };
|
||||
} else {
|
||||
return { jwt: null, refreshToken: null };
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
// Slice
|
||||
const authSlice = createSlice({
|
||||
name: "auth",
|
||||
initialState,
|
||||
reducers: {
|
||||
logout: (state) => {
|
||||
state.jwt = null;
|
||||
state.refreshToken = null;
|
||||
state.username = null;
|
||||
state.status = "idle";
|
||||
state.error = null;
|
||||
localStorage.removeItem("jwt");
|
||||
localStorage.removeItem("refreshToken");
|
||||
delete axios.defaults.headers.common['Authorization'];
|
||||
name: 'auth',
|
||||
initialState,
|
||||
reducers: {
|
||||
logout: (state) => {
|
||||
state.jwt = null;
|
||||
state.refreshToken = null;
|
||||
state.username = null;
|
||||
state.status = 'idle';
|
||||
state.error = null;
|
||||
localStorage.removeItem('jwt');
|
||||
localStorage.removeItem('refreshToken');
|
||||
delete axios.defaults.headers.common['Authorization'];
|
||||
},
|
||||
},
|
||||
},
|
||||
extraReducers: (builder) => {
|
||||
// Регистрация
|
||||
builder.addCase(registerUser.pending, (state) => {
|
||||
state.status = "loading";
|
||||
state.error = null;
|
||||
});
|
||||
builder.addCase(registerUser.fulfilled, (state, action: PayloadAction<{ jwt: string; refreshToken: string }>) => {
|
||||
state.status = "successful";
|
||||
state.jwt = action.payload.jwt;
|
||||
state.refreshToken = action.payload.refreshToken;
|
||||
axios.defaults.headers.common['Authorization'] = `Bearer ${action.payload.jwt}`;
|
||||
localStorage.setItem("jwt", action.payload.jwt);
|
||||
localStorage.setItem("refreshToken", action.payload.refreshToken);
|
||||
});
|
||||
builder.addCase(registerUser.rejected, (state, action: PayloadAction<any>) => {
|
||||
state.status = "failed";
|
||||
state.error = action.payload;
|
||||
});
|
||||
extraReducers: (builder) => {
|
||||
// Регистрация
|
||||
builder.addCase(registerUser.pending, (state) => {
|
||||
state.status = 'loading';
|
||||
state.error = null;
|
||||
});
|
||||
builder.addCase(
|
||||
registerUser.fulfilled,
|
||||
(
|
||||
state,
|
||||
action: PayloadAction<{ jwt: string; refreshToken: string }>,
|
||||
) => {
|
||||
state.status = 'successful';
|
||||
state.jwt = action.payload.jwt;
|
||||
state.refreshToken = action.payload.refreshToken;
|
||||
axios.defaults.headers.common[
|
||||
'Authorization'
|
||||
] = `Bearer ${action.payload.jwt}`;
|
||||
localStorage.setItem('jwt', action.payload.jwt);
|
||||
localStorage.setItem(
|
||||
'refreshToken',
|
||||
action.payload.refreshToken,
|
||||
);
|
||||
},
|
||||
);
|
||||
builder.addCase(
|
||||
registerUser.rejected,
|
||||
(state, action: PayloadAction<any>) => {
|
||||
state.status = 'failed';
|
||||
state.error = action.payload;
|
||||
},
|
||||
);
|
||||
|
||||
// Логин
|
||||
builder.addCase(loginUser.pending, (state) => {
|
||||
state.status = "loading";
|
||||
state.error = null;
|
||||
});
|
||||
builder.addCase(loginUser.fulfilled, (state, action: PayloadAction<{ jwt: string; refreshToken: string }>) => {
|
||||
state.status = "successful";
|
||||
state.jwt = action.payload.jwt;
|
||||
state.refreshToken = action.payload.refreshToken;
|
||||
axios.defaults.headers.common['Authorization'] = `Bearer ${action.payload.jwt}`;
|
||||
localStorage.setItem("jwt", action.payload.jwt);
|
||||
localStorage.setItem("refreshToken", action.payload.refreshToken);
|
||||
});
|
||||
builder.addCase(loginUser.rejected, (state, action: PayloadAction<any>) => {
|
||||
state.status = "failed";
|
||||
state.error = action.payload;
|
||||
});
|
||||
// Логин
|
||||
builder.addCase(loginUser.pending, (state) => {
|
||||
state.status = 'loading';
|
||||
state.error = null;
|
||||
});
|
||||
builder.addCase(
|
||||
loginUser.fulfilled,
|
||||
(
|
||||
state,
|
||||
action: PayloadAction<{ jwt: string; refreshToken: string }>,
|
||||
) => {
|
||||
state.status = 'successful';
|
||||
state.jwt = action.payload.jwt;
|
||||
state.refreshToken = action.payload.refreshToken;
|
||||
axios.defaults.headers.common[
|
||||
'Authorization'
|
||||
] = `Bearer ${action.payload.jwt}`;
|
||||
localStorage.setItem('jwt', action.payload.jwt);
|
||||
localStorage.setItem(
|
||||
'refreshToken',
|
||||
action.payload.refreshToken,
|
||||
);
|
||||
},
|
||||
);
|
||||
builder.addCase(
|
||||
loginUser.rejected,
|
||||
(state, action: PayloadAction<any>) => {
|
||||
state.status = 'failed';
|
||||
state.error = action.payload;
|
||||
},
|
||||
);
|
||||
|
||||
// Обновление токена
|
||||
builder.addCase(refreshToken.pending, (state) => {
|
||||
state.status = "loading";
|
||||
state.error = null;
|
||||
});
|
||||
builder.addCase(refreshToken.fulfilled, (state, action: PayloadAction<{ username: string }>) => {
|
||||
state.status = "successful";
|
||||
state.username = action.payload.username;
|
||||
});
|
||||
builder.addCase(refreshToken.rejected, (state, action: PayloadAction<any>) => {
|
||||
state.status = "failed";
|
||||
state.error = action.payload;
|
||||
});
|
||||
// Обновление токена
|
||||
builder.addCase(refreshToken.pending, (state) => {
|
||||
state.status = 'loading';
|
||||
state.error = null;
|
||||
});
|
||||
builder.addCase(
|
||||
refreshToken.fulfilled,
|
||||
(state, action: PayloadAction<{ username: string }>) => {
|
||||
state.status = 'successful';
|
||||
state.username = action.payload.username;
|
||||
},
|
||||
);
|
||||
builder.addCase(
|
||||
refreshToken.rejected,
|
||||
(state, action: PayloadAction<any>) => {
|
||||
state.status = 'failed';
|
||||
state.error = action.payload;
|
||||
},
|
||||
);
|
||||
|
||||
// Получение информации о пользователе
|
||||
builder.addCase(fetchWhoAmI.pending, (state) => {
|
||||
state.status = "loading";
|
||||
state.error = null;
|
||||
});
|
||||
builder.addCase(fetchWhoAmI.fulfilled, (state, action: PayloadAction<{ username: string }>) => {
|
||||
state.status = "successful";
|
||||
state.username = action.payload.username;
|
||||
});
|
||||
builder.addCase(fetchWhoAmI.rejected, (state, action: PayloadAction<any>) => {
|
||||
state.status = "failed";
|
||||
state.error = action.payload;
|
||||
});
|
||||
// Получение информации о пользователе
|
||||
builder.addCase(fetchWhoAmI.pending, (state) => {
|
||||
state.status = 'loading';
|
||||
state.error = null;
|
||||
});
|
||||
builder.addCase(
|
||||
fetchWhoAmI.fulfilled,
|
||||
(state, action: PayloadAction<{ username: string }>) => {
|
||||
state.status = 'successful';
|
||||
state.username = action.payload.username;
|
||||
},
|
||||
);
|
||||
builder.addCase(
|
||||
fetchWhoAmI.rejected,
|
||||
(state, action: PayloadAction<any>) => {
|
||||
state.status = 'failed';
|
||||
state.error = action.payload;
|
||||
},
|
||||
);
|
||||
|
||||
// Загрузка токенов из localStorage
|
||||
builder.addCase(loadTokensFromLocalStorage.fulfilled, (state, action: PayloadAction<{ jwt: string | null; refreshToken: string | null }>) => {
|
||||
state.jwt = action.payload.jwt;
|
||||
state.refreshToken = action.payload.refreshToken;
|
||||
if (action.payload.jwt) {
|
||||
axios.defaults.headers.common['Authorization'] = `Bearer ${action.payload.jwt}`;
|
||||
}
|
||||
});
|
||||
},
|
||||
// Загрузка токенов из localStorage
|
||||
builder.addCase(
|
||||
loadTokensFromLocalStorage.fulfilled,
|
||||
(
|
||||
state,
|
||||
action: PayloadAction<{
|
||||
jwt: string | null;
|
||||
refreshToken: string | null;
|
||||
}>,
|
||||
) => {
|
||||
state.jwt = action.payload.jwt;
|
||||
state.refreshToken = action.payload.refreshToken;
|
||||
if (action.payload.jwt) {
|
||||
axios.defaults.headers.common[
|
||||
'Authorization'
|
||||
] = `Bearer ${action.payload.jwt}`;
|
||||
}
|
||||
},
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
export const { logout } = authSlice.actions;
|
||||
|
||||
@@ -1,58 +1,58 @@
|
||||
import { createSlice, createAsyncThunk, PayloadAction } from "@reduxjs/toolkit";
|
||||
import axios from "../../axios";
|
||||
import { createSlice, createAsyncThunk, PayloadAction } from '@reduxjs/toolkit';
|
||||
import axios from '../../axios';
|
||||
|
||||
// =====================
|
||||
// Типы
|
||||
// =====================
|
||||
|
||||
export interface Mission {
|
||||
missionId: number;
|
||||
name: string;
|
||||
sortOrder: number;
|
||||
missionId: number;
|
||||
name: string;
|
||||
sortOrder: number;
|
||||
}
|
||||
|
||||
export interface Member {
|
||||
userId: number;
|
||||
username: string;
|
||||
role: string;
|
||||
userId: number;
|
||||
username: string;
|
||||
role: string;
|
||||
}
|
||||
|
||||
export interface Contest {
|
||||
id: number;
|
||||
name: string;
|
||||
description: string;
|
||||
scheduleType: string;
|
||||
startsAt: string;
|
||||
endsAt: string;
|
||||
availableFrom: string | null;
|
||||
availableUntil: string | null;
|
||||
attemptDurationMinutes: number | null;
|
||||
groupId: number | null;
|
||||
groupName: string | null;
|
||||
missions: Mission[];
|
||||
articles: any[];
|
||||
members: Member[];
|
||||
id: number;
|
||||
name: string;
|
||||
description: string;
|
||||
scheduleType: string;
|
||||
startsAt: string;
|
||||
endsAt: string;
|
||||
availableFrom: string | null;
|
||||
availableUntil: string | null;
|
||||
attemptDurationMinutes: number | null;
|
||||
groupId: number | null;
|
||||
groupName: string | null;
|
||||
missions: Mission[];
|
||||
articles: any[];
|
||||
members: Member[];
|
||||
}
|
||||
|
||||
interface ContestsResponse {
|
||||
hasNextPage: boolean;
|
||||
contests: Contest[];
|
||||
hasNextPage: boolean;
|
||||
contests: Contest[];
|
||||
}
|
||||
|
||||
export interface CreateContestBody {
|
||||
name: string;
|
||||
description: string;
|
||||
scheduleType: "FixedWindow" | "Flexible";
|
||||
startsAt: string;
|
||||
endsAt: string;
|
||||
availableFrom: string | null;
|
||||
availableUntil: string | null;
|
||||
attemptDurationMinutes: number | null;
|
||||
groupId: number | null;
|
||||
missionIds: number[];
|
||||
articleIds: number[];
|
||||
participantIds: number[];
|
||||
organizerIds: number[];
|
||||
name: string;
|
||||
description: string;
|
||||
scheduleType: 'FixedWindow' | 'Flexible';
|
||||
startsAt: string;
|
||||
endsAt: string;
|
||||
availableFrom: string | null;
|
||||
availableUntil: string | null;
|
||||
attemptDurationMinutes: number | null;
|
||||
groupId: number | null;
|
||||
missionIds: number[];
|
||||
articleIds: number[];
|
||||
participantIds: number[];
|
||||
organizerIds: number[];
|
||||
}
|
||||
|
||||
// =====================
|
||||
@@ -60,19 +60,19 @@ export interface CreateContestBody {
|
||||
// =====================
|
||||
|
||||
interface ContestsState {
|
||||
contests: Contest[];
|
||||
selectedContest: Contest | null;
|
||||
hasNextPage: boolean;
|
||||
status: "idle" | "loading" | "successful" | "failed";
|
||||
error: string | null;
|
||||
contests: Contest[];
|
||||
selectedContest: Contest | null;
|
||||
hasNextPage: boolean;
|
||||
status: 'idle' | 'loading' | 'successful' | 'failed';
|
||||
error: string | null;
|
||||
}
|
||||
|
||||
const initialState: ContestsState = {
|
||||
contests: [],
|
||||
selectedContest: null,
|
||||
hasNextPage: false,
|
||||
status: "idle",
|
||||
error: null,
|
||||
contests: [],
|
||||
selectedContest: null,
|
||||
hasNextPage: false,
|
||||
status: 'idle',
|
||||
error: null,
|
||||
};
|
||||
|
||||
// =====================
|
||||
@@ -81,47 +81,60 @@ const initialState: ContestsState = {
|
||||
|
||||
// Получение списка контестов
|
||||
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");
|
||||
}
|
||||
}
|
||||
'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");
|
||||
}
|
||||
}
|
||||
'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");
|
||||
}
|
||||
}
|
||||
'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',
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
// =====================
|
||||
@@ -129,57 +142,75 @@ export const createContest = createAsyncThunk(
|
||||
// =====================
|
||||
|
||||
const contestsSlice = createSlice({
|
||||
name: "contests",
|
||||
initialState,
|
||||
reducers: {
|
||||
clearSelectedContest: (state) => {
|
||||
state.selectedContest = null;
|
||||
name: 'contests',
|
||||
initialState,
|
||||
reducers: {
|
||||
clearSelectedContest: (state) => {
|
||||
state.selectedContest = null;
|
||||
},
|
||||
},
|
||||
},
|
||||
extraReducers: (builder) => {
|
||||
// fetchContests
|
||||
builder.addCase(fetchContests.pending, (state) => {
|
||||
state.status = "loading";
|
||||
state.error = null;
|
||||
});
|
||||
builder.addCase(fetchContests.fulfilled, (state, action: PayloadAction<ContestsResponse>) => {
|
||||
state.status = "successful";
|
||||
state.contests = action.payload.contests;
|
||||
state.hasNextPage = action.payload.hasNextPage;
|
||||
});
|
||||
builder.addCase(fetchContests.rejected, (state, action: PayloadAction<any>) => {
|
||||
state.status = "failed";
|
||||
state.error = action.payload;
|
||||
});
|
||||
extraReducers: (builder) => {
|
||||
// fetchContests
|
||||
builder.addCase(fetchContests.pending, (state) => {
|
||||
state.status = 'loading';
|
||||
state.error = null;
|
||||
});
|
||||
builder.addCase(
|
||||
fetchContests.fulfilled,
|
||||
(state, action: PayloadAction<ContestsResponse>) => {
|
||||
state.status = 'successful';
|
||||
state.contests = action.payload.contests;
|
||||
state.hasNextPage = action.payload.hasNextPage;
|
||||
},
|
||||
);
|
||||
builder.addCase(
|
||||
fetchContests.rejected,
|
||||
(state, action: PayloadAction<any>) => {
|
||||
state.status = 'failed';
|
||||
state.error = action.payload;
|
||||
},
|
||||
);
|
||||
|
||||
// fetchContestById
|
||||
builder.addCase(fetchContestById.pending, (state) => {
|
||||
state.status = "loading";
|
||||
state.error = null;
|
||||
});
|
||||
builder.addCase(fetchContestById.fulfilled, (state, action: PayloadAction<Contest>) => {
|
||||
state.status = "successful";
|
||||
state.selectedContest = action.payload;
|
||||
});
|
||||
builder.addCase(fetchContestById.rejected, (state, action: PayloadAction<any>) => {
|
||||
state.status = "failed";
|
||||
state.error = action.payload;
|
||||
});
|
||||
// fetchContestById
|
||||
builder.addCase(fetchContestById.pending, (state) => {
|
||||
state.status = 'loading';
|
||||
state.error = null;
|
||||
});
|
||||
builder.addCase(
|
||||
fetchContestById.fulfilled,
|
||||
(state, action: PayloadAction<Contest>) => {
|
||||
state.status = 'successful';
|
||||
state.selectedContest = action.payload;
|
||||
},
|
||||
);
|
||||
builder.addCase(
|
||||
fetchContestById.rejected,
|
||||
(state, action: PayloadAction<any>) => {
|
||||
state.status = 'failed';
|
||||
state.error = action.payload;
|
||||
},
|
||||
);
|
||||
|
||||
// createContest
|
||||
builder.addCase(createContest.pending, (state) => {
|
||||
state.status = "loading";
|
||||
state.error = null;
|
||||
});
|
||||
builder.addCase(createContest.fulfilled, (state, action: PayloadAction<Contest>) => {
|
||||
state.status = "successful";
|
||||
state.contests.unshift(action.payload);
|
||||
});
|
||||
builder.addCase(createContest.rejected, (state, action: PayloadAction<any>) => {
|
||||
state.status = "failed";
|
||||
state.error = action.payload;
|
||||
});
|
||||
},
|
||||
// createContest
|
||||
builder.addCase(createContest.pending, (state) => {
|
||||
state.status = 'loading';
|
||||
state.error = null;
|
||||
});
|
||||
builder.addCase(
|
||||
createContest.fulfilled,
|
||||
(state, action: PayloadAction<Contest>) => {
|
||||
state.status = 'successful';
|
||||
state.contests.unshift(action.payload);
|
||||
},
|
||||
);
|
||||
builder.addCase(
|
||||
createContest.rejected,
|
||||
(state, action: PayloadAction<any>) => {
|
||||
state.status = 'failed';
|
||||
state.error = action.payload;
|
||||
},
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
// =====================
|
||||
|
||||
@@ -1,282 +1,349 @@
|
||||
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";
|
||||
type Status = 'idle' | 'loading' | 'successful' | 'failed';
|
||||
|
||||
export interface GroupMember {
|
||||
userId: number;
|
||||
username: string;
|
||||
role: string;
|
||||
userId: number;
|
||||
username: string;
|
||||
role: string;
|
||||
}
|
||||
|
||||
export interface Group {
|
||||
id: number;
|
||||
name: string;
|
||||
description: string;
|
||||
members: GroupMember[];
|
||||
contests: any[];
|
||||
id: number;
|
||||
name: string;
|
||||
description: string;
|
||||
members: GroupMember[];
|
||||
contests: any[];
|
||||
}
|
||||
|
||||
interface GroupsState {
|
||||
groups: Group[];
|
||||
currentGroup: Group | null;
|
||||
statuses: {
|
||||
create: Status;
|
||||
update: Status;
|
||||
delete: Status;
|
||||
fetchMy: Status;
|
||||
fetchById: Status;
|
||||
addMember: Status;
|
||||
removeMember: Status;
|
||||
};
|
||||
error: string | null;
|
||||
groups: Group[];
|
||||
currentGroup: Group | null;
|
||||
statuses: {
|
||||
create: Status;
|
||||
update: Status;
|
||||
delete: Status;
|
||||
fetchMy: Status;
|
||||
fetchById: Status;
|
||||
addMember: Status;
|
||||
removeMember: Status;
|
||||
};
|
||||
error: string | null;
|
||||
}
|
||||
|
||||
const initialState: GroupsState = {
|
||||
groups: [],
|
||||
currentGroup: null,
|
||||
statuses: {
|
||||
create: "idle",
|
||||
update: "idle",
|
||||
delete: "idle",
|
||||
fetchMy: "idle",
|
||||
fetchById: "idle",
|
||||
addMember: "idle",
|
||||
removeMember: "idle",
|
||||
},
|
||||
error: null,
|
||||
groups: [],
|
||||
currentGroup: null,
|
||||
statuses: {
|
||||
create: 'idle',
|
||||
update: 'idle',
|
||||
delete: 'idle',
|
||||
fetchMy: 'idle',
|
||||
fetchById: 'idle',
|
||||
addMember: 'idle',
|
||||
removeMember: 'idle',
|
||||
},
|
||||
error: null,
|
||||
};
|
||||
|
||||
|
||||
// ─── Async Thunks ─────────────────────────────────────
|
||||
|
||||
// POST /groups
|
||||
export const createGroup = createAsyncThunk(
|
||||
"groups/createGroup",
|
||||
async (
|
||||
{ name, description }: { name: string; description: string },
|
||||
{ rejectWithValue }
|
||||
) => {
|
||||
try {
|
||||
const response = await axios.post("/groups", { name, description });
|
||||
return response.data as Group;
|
||||
} catch (err: any) {
|
||||
return rejectWithValue(err.response?.data?.message || "Ошибка при создании группы");
|
||||
}
|
||||
}
|
||||
'groups/createGroup',
|
||||
async (
|
||||
{ name, description }: { name: string; description: string },
|
||||
{ rejectWithValue },
|
||||
) => {
|
||||
try {
|
||||
const response = await axios.post('/groups', { name, description });
|
||||
return response.data as Group;
|
||||
} catch (err: any) {
|
||||
return rejectWithValue(
|
||||
err.response?.data?.message || 'Ошибка при создании группы',
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
// PUT /groups/{groupId}
|
||||
export const updateGroup = createAsyncThunk(
|
||||
"groups/updateGroup",
|
||||
async (
|
||||
{ groupId, name, description }: { groupId: number; name: string; description: string },
|
||||
{ rejectWithValue }
|
||||
) => {
|
||||
try {
|
||||
const response = await axios.put(`/groups/${groupId}`, { name, description });
|
||||
return response.data as Group;
|
||||
} catch (err: any) {
|
||||
return rejectWithValue(err.response?.data?.message || "Ошибка при обновлении группы");
|
||||
}
|
||||
}
|
||||
'groups/updateGroup',
|
||||
async (
|
||||
{
|
||||
groupId,
|
||||
name,
|
||||
description,
|
||||
}: { groupId: number; name: string; description: string },
|
||||
{ rejectWithValue },
|
||||
) => {
|
||||
try {
|
||||
const response = await axios.put(`/groups/${groupId}`, {
|
||||
name,
|
||||
description,
|
||||
});
|
||||
return response.data as Group;
|
||||
} catch (err: any) {
|
||||
return rejectWithValue(
|
||||
err.response?.data?.message || 'Ошибка при обновлении группы',
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
// DELETE /groups/{groupId}
|
||||
export const deleteGroup = createAsyncThunk(
|
||||
"groups/deleteGroup",
|
||||
async (groupId: number, { rejectWithValue }) => {
|
||||
try {
|
||||
await axios.delete(`/groups/${groupId}`);
|
||||
return groupId;
|
||||
} catch (err: any) {
|
||||
return rejectWithValue(err.response?.data?.message || "Ошибка при удалении группы");
|
||||
}
|
||||
}
|
||||
'groups/deleteGroup',
|
||||
async (groupId: number, { rejectWithValue }) => {
|
||||
try {
|
||||
await axios.delete(`/groups/${groupId}`);
|
||||
return groupId;
|
||||
} catch (err: any) {
|
||||
return rejectWithValue(
|
||||
err.response?.data?.message || 'Ошибка при удалении группы',
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
// GET /groups/my
|
||||
export const fetchMyGroups = createAsyncThunk(
|
||||
"groups/fetchMyGroups",
|
||||
async (_, { rejectWithValue }) => {
|
||||
try {
|
||||
const response = await axios.get("/groups/my");
|
||||
return response.data.groups as Group[];
|
||||
} catch (err: any) {
|
||||
return rejectWithValue(err.response?.data?.message || "Ошибка при получении групп");
|
||||
}
|
||||
}
|
||||
'groups/fetchMyGroups',
|
||||
async (_, { rejectWithValue }) => {
|
||||
try {
|
||||
const response = await axios.get('/groups/my');
|
||||
return response.data.groups as Group[];
|
||||
} catch (err: any) {
|
||||
return rejectWithValue(
|
||||
err.response?.data?.message || 'Ошибка при получении групп',
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
// GET /groups/{groupId}
|
||||
export const fetchGroupById = createAsyncThunk(
|
||||
"groups/fetchGroupById",
|
||||
async (groupId: number, { rejectWithValue }) => {
|
||||
try {
|
||||
const response = await axios.get(`/groups/${groupId}`);
|
||||
return response.data as Group;
|
||||
} catch (err: any) {
|
||||
return rejectWithValue(err.response?.data?.message || "Ошибка при получении группы");
|
||||
}
|
||||
}
|
||||
'groups/fetchGroupById',
|
||||
async (groupId: number, { rejectWithValue }) => {
|
||||
try {
|
||||
const response = await axios.get(`/groups/${groupId}`);
|
||||
return response.data as Group;
|
||||
} catch (err: any) {
|
||||
return rejectWithValue(
|
||||
err.response?.data?.message || 'Ошибка при получении группы',
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
// POST /groups/members
|
||||
export const addGroupMember = createAsyncThunk(
|
||||
"groups/addGroupMember",
|
||||
async ({ userId, role }: { userId: number; role: string }, { rejectWithValue }) => {
|
||||
try {
|
||||
await axios.post("/groups/members", { userId, role });
|
||||
return { userId, role };
|
||||
} catch (err: any) {
|
||||
return rejectWithValue(err.response?.data?.message || "Ошибка при добавлении участника");
|
||||
}
|
||||
}
|
||||
'groups/addGroupMember',
|
||||
async (
|
||||
{ userId, role }: { userId: number; role: string },
|
||||
{ rejectWithValue },
|
||||
) => {
|
||||
try {
|
||||
await axios.post('/groups/members', { userId, role });
|
||||
return { userId, role };
|
||||
} catch (err: any) {
|
||||
return rejectWithValue(
|
||||
err.response?.data?.message ||
|
||||
'Ошибка при добавлении участника',
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
// DELETE /groups/{groupId}/members/{memberId}
|
||||
export const removeGroupMember = createAsyncThunk(
|
||||
"groups/removeGroupMember",
|
||||
async (
|
||||
{ groupId, memberId }: { groupId: number; memberId: number },
|
||||
{ rejectWithValue }
|
||||
) => {
|
||||
try {
|
||||
await axios.delete(`/groups/${groupId}/members/${memberId}`);
|
||||
return { groupId, memberId };
|
||||
} catch (err: any) {
|
||||
return rejectWithValue(err.response?.data?.message || "Ошибка при удалении участника");
|
||||
}
|
||||
}
|
||||
'groups/removeGroupMember',
|
||||
async (
|
||||
{ groupId, memberId }: { groupId: number; memberId: number },
|
||||
{ rejectWithValue },
|
||||
) => {
|
||||
try {
|
||||
await axios.delete(`/groups/${groupId}/members/${memberId}`);
|
||||
return { groupId, memberId };
|
||||
} catch (err: any) {
|
||||
return rejectWithValue(
|
||||
err.response?.data?.message || 'Ошибка при удалении участника',
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
// ─── Slice ────────────────────────────────────────────
|
||||
|
||||
const groupsSlice = createSlice({
|
||||
name: "groups",
|
||||
initialState,
|
||||
reducers: {
|
||||
clearCurrentGroup: (state) => {
|
||||
state.currentGroup = null;
|
||||
name: 'groups',
|
||||
initialState,
|
||||
reducers: {
|
||||
clearCurrentGroup: (state) => {
|
||||
state.currentGroup = null;
|
||||
},
|
||||
},
|
||||
},
|
||||
extraReducers: (builder) => {
|
||||
// ─── CREATE GROUP ───
|
||||
builder.addCase(createGroup.pending, (state) => {
|
||||
state.statuses.create = "loading";
|
||||
state.error = null;
|
||||
});
|
||||
builder.addCase(createGroup.fulfilled, (state, action: PayloadAction<Group>) => {
|
||||
state.statuses.create = "successful";
|
||||
state.groups.push(action.payload);
|
||||
});
|
||||
builder.addCase(createGroup.rejected, (state, action: PayloadAction<any>) => {
|
||||
state.statuses.create = "failed";
|
||||
state.error = action.payload;
|
||||
});
|
||||
|
||||
|
||||
// ─── UPDATE GROUP ───
|
||||
builder.addCase(updateGroup.pending, (state) => {
|
||||
state.statuses.update = "loading";
|
||||
state.error = null;
|
||||
});
|
||||
builder.addCase(updateGroup.fulfilled, (state, action: PayloadAction<Group>) => {
|
||||
state.statuses.update = "successful";
|
||||
const index = state.groups.findIndex((g) => g.id === action.payload.id);
|
||||
if (index !== -1) state.groups[index] = action.payload;
|
||||
if (state.currentGroup?.id === action.payload.id) {
|
||||
state.currentGroup = action.payload;
|
||||
}
|
||||
});
|
||||
builder.addCase(updateGroup.rejected, (state, action: PayloadAction<any>) => {
|
||||
state.statuses.update = "failed";
|
||||
state.error = action.payload;
|
||||
});
|
||||
|
||||
|
||||
|
||||
// ─── DELETE GROUP ───
|
||||
builder.addCase(deleteGroup.pending, (state) => {
|
||||
state.statuses.delete = "loading";
|
||||
state.error = null;
|
||||
});
|
||||
builder.addCase(deleteGroup.fulfilled, (state, action: PayloadAction<number>) => {
|
||||
state.statuses.delete = "successful";
|
||||
state.groups = state.groups.filter((g) => g.id !== action.payload);
|
||||
if (state.currentGroup?.id === action.payload) state.currentGroup = null;
|
||||
});
|
||||
builder.addCase(deleteGroup.rejected, (state, action: PayloadAction<any>) => {
|
||||
state.statuses.delete = "failed";
|
||||
state.error = action.payload;
|
||||
});
|
||||
|
||||
|
||||
// ─── FETCH MY GROUPS ───
|
||||
builder.addCase(fetchMyGroups.pending, (state) => {
|
||||
state.statuses.fetchMy = "loading";
|
||||
state.error = null;
|
||||
});
|
||||
builder.addCase(fetchMyGroups.fulfilled, (state, action: PayloadAction<Group[]>) => {
|
||||
state.statuses.fetchMy = "successful";
|
||||
state.groups = action.payload;
|
||||
});
|
||||
builder.addCase(fetchMyGroups.rejected, (state, action: PayloadAction<any>) => {
|
||||
state.statuses.fetchMy = "failed";
|
||||
state.error = action.payload;
|
||||
});
|
||||
|
||||
|
||||
// ─── FETCH GROUP BY ID ───
|
||||
builder.addCase(fetchGroupById.pending, (state) => {
|
||||
state.statuses.fetchById = "loading";
|
||||
state.error = null;
|
||||
});
|
||||
builder.addCase(fetchGroupById.fulfilled, (state, action: PayloadAction<Group>) => {
|
||||
state.statuses.fetchById = "successful";
|
||||
state.currentGroup = action.payload;
|
||||
});
|
||||
builder.addCase(fetchGroupById.rejected, (state, action: PayloadAction<any>) => {
|
||||
state.statuses.fetchById = "failed";
|
||||
state.error = action.payload;
|
||||
});
|
||||
|
||||
|
||||
// ─── ADD MEMBER ───
|
||||
builder.addCase(addGroupMember.pending, (state) => {
|
||||
state.statuses.addMember = "loading";
|
||||
state.error = null;
|
||||
});
|
||||
builder.addCase(addGroupMember.fulfilled, (state) => {
|
||||
state.statuses.addMember = "successful";
|
||||
});
|
||||
builder.addCase(addGroupMember.rejected, (state, action: PayloadAction<any>) => {
|
||||
state.statuses.addMember = "failed";
|
||||
state.error = action.payload;
|
||||
});
|
||||
|
||||
|
||||
// ─── REMOVE MEMBER ───
|
||||
builder.addCase(removeGroupMember.pending, (state) => {
|
||||
state.statuses.removeMember = "loading";
|
||||
state.error = null;
|
||||
});
|
||||
builder.addCase(removeGroupMember.fulfilled, (state, action: PayloadAction<{ groupId: number; memberId: number }>) => {
|
||||
state.statuses.removeMember = "successful";
|
||||
if (state.currentGroup && state.currentGroup.id === action.payload.groupId) {
|
||||
state.currentGroup.members = state.currentGroup.members.filter(
|
||||
(m) => m.userId !== action.payload.memberId
|
||||
extraReducers: (builder) => {
|
||||
// ─── CREATE GROUP ───
|
||||
builder.addCase(createGroup.pending, (state) => {
|
||||
state.statuses.create = 'loading';
|
||||
state.error = null;
|
||||
});
|
||||
builder.addCase(
|
||||
createGroup.fulfilled,
|
||||
(state, action: PayloadAction<Group>) => {
|
||||
state.statuses.create = 'successful';
|
||||
state.groups.push(action.payload);
|
||||
},
|
||||
);
|
||||
builder.addCase(
|
||||
createGroup.rejected,
|
||||
(state, action: PayloadAction<any>) => {
|
||||
state.statuses.create = 'failed';
|
||||
state.error = action.payload;
|
||||
},
|
||||
);
|
||||
}
|
||||
});
|
||||
builder.addCase(removeGroupMember.rejected, (state, action: PayloadAction<any>) => {
|
||||
state.statuses.removeMember = "failed";
|
||||
state.error = action.payload;
|
||||
});
|
||||
|
||||
},
|
||||
// ─── UPDATE GROUP ───
|
||||
builder.addCase(updateGroup.pending, (state) => {
|
||||
state.statuses.update = 'loading';
|
||||
state.error = null;
|
||||
});
|
||||
builder.addCase(
|
||||
updateGroup.fulfilled,
|
||||
(state, action: PayloadAction<Group>) => {
|
||||
state.statuses.update = 'successful';
|
||||
const index = state.groups.findIndex(
|
||||
(g) => g.id === action.payload.id,
|
||||
);
|
||||
if (index !== -1) state.groups[index] = action.payload;
|
||||
if (state.currentGroup?.id === action.payload.id) {
|
||||
state.currentGroup = action.payload;
|
||||
}
|
||||
},
|
||||
);
|
||||
builder.addCase(
|
||||
updateGroup.rejected,
|
||||
(state, action: PayloadAction<any>) => {
|
||||
state.statuses.update = 'failed';
|
||||
state.error = action.payload;
|
||||
},
|
||||
);
|
||||
|
||||
// ─── DELETE GROUP ───
|
||||
builder.addCase(deleteGroup.pending, (state) => {
|
||||
state.statuses.delete = 'loading';
|
||||
state.error = null;
|
||||
});
|
||||
builder.addCase(
|
||||
deleteGroup.fulfilled,
|
||||
(state, action: PayloadAction<number>) => {
|
||||
state.statuses.delete = 'successful';
|
||||
state.groups = state.groups.filter(
|
||||
(g) => g.id !== action.payload,
|
||||
);
|
||||
if (state.currentGroup?.id === action.payload)
|
||||
state.currentGroup = null;
|
||||
},
|
||||
);
|
||||
builder.addCase(
|
||||
deleteGroup.rejected,
|
||||
(state, action: PayloadAction<any>) => {
|
||||
state.statuses.delete = 'failed';
|
||||
state.error = action.payload;
|
||||
},
|
||||
);
|
||||
|
||||
// ─── FETCH MY GROUPS ───
|
||||
builder.addCase(fetchMyGroups.pending, (state) => {
|
||||
state.statuses.fetchMy = 'loading';
|
||||
state.error = null;
|
||||
});
|
||||
builder.addCase(
|
||||
fetchMyGroups.fulfilled,
|
||||
(state, action: PayloadAction<Group[]>) => {
|
||||
state.statuses.fetchMy = 'successful';
|
||||
state.groups = action.payload;
|
||||
},
|
||||
);
|
||||
builder.addCase(
|
||||
fetchMyGroups.rejected,
|
||||
(state, action: PayloadAction<any>) => {
|
||||
state.statuses.fetchMy = 'failed';
|
||||
state.error = action.payload;
|
||||
},
|
||||
);
|
||||
|
||||
// ─── FETCH GROUP BY ID ───
|
||||
builder.addCase(fetchGroupById.pending, (state) => {
|
||||
state.statuses.fetchById = 'loading';
|
||||
state.error = null;
|
||||
});
|
||||
builder.addCase(
|
||||
fetchGroupById.fulfilled,
|
||||
(state, action: PayloadAction<Group>) => {
|
||||
state.statuses.fetchById = 'successful';
|
||||
state.currentGroup = action.payload;
|
||||
},
|
||||
);
|
||||
builder.addCase(
|
||||
fetchGroupById.rejected,
|
||||
(state, action: PayloadAction<any>) => {
|
||||
state.statuses.fetchById = 'failed';
|
||||
state.error = action.payload;
|
||||
},
|
||||
);
|
||||
|
||||
// ─── ADD MEMBER ───
|
||||
builder.addCase(addGroupMember.pending, (state) => {
|
||||
state.statuses.addMember = 'loading';
|
||||
state.error = null;
|
||||
});
|
||||
builder.addCase(addGroupMember.fulfilled, (state) => {
|
||||
state.statuses.addMember = 'successful';
|
||||
});
|
||||
builder.addCase(
|
||||
addGroupMember.rejected,
|
||||
(state, action: PayloadAction<any>) => {
|
||||
state.statuses.addMember = 'failed';
|
||||
state.error = action.payload;
|
||||
},
|
||||
);
|
||||
|
||||
// ─── REMOVE MEMBER ───
|
||||
builder.addCase(removeGroupMember.pending, (state) => {
|
||||
state.statuses.removeMember = 'loading';
|
||||
state.error = null;
|
||||
});
|
||||
builder.addCase(
|
||||
removeGroupMember.fulfilled,
|
||||
(
|
||||
state,
|
||||
action: PayloadAction<{ groupId: number; memberId: number }>,
|
||||
) => {
|
||||
state.statuses.removeMember = 'successful';
|
||||
if (
|
||||
state.currentGroup &&
|
||||
state.currentGroup.id === action.payload.groupId
|
||||
) {
|
||||
state.currentGroup.members =
|
||||
state.currentGroup.members.filter(
|
||||
(m) => m.userId !== action.payload.memberId,
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
builder.addCase(
|
||||
removeGroupMember.rejected,
|
||||
(state, action: PayloadAction<any>) => {
|
||||
state.statuses.removeMember = 'failed';
|
||||
state.error = action.payload;
|
||||
},
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
export const { clearCurrentGroup } = groupsSlice.actions;
|
||||
|
||||
@@ -1,29 +1,28 @@
|
||||
import { createSlice, PayloadAction} from "@reduxjs/toolkit";
|
||||
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
|
||||
|
||||
// Типы данных
|
||||
interface StorState {
|
||||
menu: {
|
||||
activePage: string;
|
||||
}
|
||||
menu: {
|
||||
activePage: string;
|
||||
};
|
||||
}
|
||||
|
||||
// Инициализация состояния
|
||||
const initialState: StorState = {
|
||||
menu: {
|
||||
activePage: "",
|
||||
}
|
||||
activePage: '',
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
// Slice
|
||||
const storeSlice = createSlice({
|
||||
name: "store",
|
||||
initialState,
|
||||
reducers: {
|
||||
setMenuActivePage: (state, activePage: PayloadAction<string>) => {
|
||||
state.menu.activePage = activePage.payload;
|
||||
name: 'store',
|
||||
initialState,
|
||||
reducers: {
|
||||
setMenuActivePage: (state, activePage: PayloadAction<string>) => {
|
||||
state.menu.activePage = activePage.payload;
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const { setMenuActivePage } = storeSlice.actions;
|
||||
|
||||
@@ -1,184 +1,224 @@
|
||||
import { createSlice, createAsyncThunk, PayloadAction } from "@reduxjs/toolkit";
|
||||
import axios from "../../axios";
|
||||
import { createSlice, createAsyncThunk, PayloadAction } from '@reduxjs/toolkit';
|
||||
import axios from '../../axios';
|
||||
|
||||
// Типы данных
|
||||
export interface Submit {
|
||||
id?: number;
|
||||
missionId: number;
|
||||
language: string;
|
||||
languageVersion: string;
|
||||
sourceCode: string;
|
||||
contestId: number | null;
|
||||
id?: number;
|
||||
missionId: number;
|
||||
language: string;
|
||||
languageVersion: string;
|
||||
sourceCode: string;
|
||||
contestId: number | null;
|
||||
}
|
||||
|
||||
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;
|
||||
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 MissionSubmit {
|
||||
id: number;
|
||||
userId: number;
|
||||
solution: Solution;
|
||||
contestId: number | null;
|
||||
contestName: string | null;
|
||||
sourceType: string;
|
||||
id: number;
|
||||
userId: number;
|
||||
solution: Solution;
|
||||
contestId: number | null;
|
||||
contestName: string | null;
|
||||
sourceType: string;
|
||||
}
|
||||
|
||||
interface SubmitState {
|
||||
submits: Submit[];
|
||||
submitsById: Record<number, MissionSubmit[]>; // ✅ добавлено
|
||||
currentSubmit?: Submit;
|
||||
status: "idle" | "loading" | "successful" | "failed";
|
||||
error: string | null;
|
||||
submits: Submit[];
|
||||
submitsById: Record<number, MissionSubmit[]>; // ✅ добавлено
|
||||
currentSubmit?: Submit;
|
||||
status: 'idle' | 'loading' | 'successful' | 'failed';
|
||||
error: string | null;
|
||||
}
|
||||
|
||||
// Начальное состояние
|
||||
const initialState: SubmitState = {
|
||||
submits: [],
|
||||
submitsById: {}, // ✅ инициализация
|
||||
currentSubmit: undefined,
|
||||
status: "idle",
|
||||
error: null,
|
||||
submits: [],
|
||||
submitsById: {}, // ✅ инициализация
|
||||
currentSubmit: undefined,
|
||||
status: 'idle',
|
||||
error: null,
|
||||
};
|
||||
|
||||
// AsyncThunk: Отправка решения
|
||||
export const submitMission = createAsyncThunk(
|
||||
"submit/submitMission",
|
||||
async (submitData: Submit, { rejectWithValue }) => {
|
||||
try {
|
||||
const response = await axios.post("/submits", submitData);
|
||||
return response.data;
|
||||
} catch (err: any) {
|
||||
return rejectWithValue(err.response?.data?.message || "Submit failed");
|
||||
}
|
||||
}
|
||||
'submit/submitMission',
|
||||
async (submitData: Submit, { rejectWithValue }) => {
|
||||
try {
|
||||
const response = await axios.post('/submits', submitData);
|
||||
return response.data;
|
||||
} catch (err: any) {
|
||||
return rejectWithValue(
|
||||
err.response?.data?.message || 'Submit failed',
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
// AsyncThunk: Получить все свои отправки
|
||||
export const fetchMySubmits = createAsyncThunk(
|
||||
"submit/fetchMySubmits",
|
||||
async (_, { rejectWithValue }) => {
|
||||
try {
|
||||
const response = await axios.get("/submits/my");
|
||||
return response.data as Submit[];
|
||||
} catch (err: any) {
|
||||
return rejectWithValue(err.response?.data?.message || "Failed to fetch submits");
|
||||
}
|
||||
}
|
||||
'submit/fetchMySubmits',
|
||||
async (_, { rejectWithValue }) => {
|
||||
try {
|
||||
const response = await axios.get('/submits/my');
|
||||
return response.data as Submit[];
|
||||
} catch (err: any) {
|
||||
return rejectWithValue(
|
||||
err.response?.data?.message || 'Failed to fetch submits',
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
// AsyncThunk: Получить конкретную отправку по ID
|
||||
export const fetchSubmitById = createAsyncThunk(
|
||||
"submit/fetchSubmitById",
|
||||
async (id: number, { rejectWithValue }) => {
|
||||
try {
|
||||
const response = await axios.get(`/submits/${id}`);
|
||||
return response.data as Submit;
|
||||
} catch (err: any) {
|
||||
return rejectWithValue(err.response?.data?.message || "Failed to fetch submit");
|
||||
}
|
||||
}
|
||||
'submit/fetchSubmitById',
|
||||
async (id: number, { rejectWithValue }) => {
|
||||
try {
|
||||
const response = await axios.get(`/submits/${id}`);
|
||||
return response.data as Submit;
|
||||
} catch (err: any) {
|
||||
return rejectWithValue(
|
||||
err.response?.data?.message || 'Failed to fetch submit',
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
// ✅ AsyncThunk: Получить отправки для конкретной миссии (новая структура)
|
||||
export const fetchMySubmitsByMission = createAsyncThunk(
|
||||
"submit/fetchMySubmitsByMission",
|
||||
async (missionId: number, { rejectWithValue }) => {
|
||||
try {
|
||||
const response = await axios.get(`/submits/my/mission/${missionId}`);
|
||||
return { missionId, data: response.data as MissionSubmit[] };
|
||||
} catch (err: any) {
|
||||
return rejectWithValue(err.response?.data?.message || "Failed to fetch mission submits");
|
||||
}
|
||||
}
|
||||
'submit/fetchMySubmitsByMission',
|
||||
async (missionId: number, { rejectWithValue }) => {
|
||||
try {
|
||||
const response = await axios.get(
|
||||
`/submits/my/mission/${missionId}`,
|
||||
);
|
||||
return { missionId, data: response.data as MissionSubmit[] };
|
||||
} catch (err: any) {
|
||||
return rejectWithValue(
|
||||
err.response?.data?.message ||
|
||||
'Failed to fetch mission submits',
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
// Slice
|
||||
const submitSlice = createSlice({
|
||||
name: "submit",
|
||||
initialState,
|
||||
reducers: {
|
||||
clearCurrentSubmit: (state) => {
|
||||
state.currentSubmit = undefined;
|
||||
state.status = "idle";
|
||||
state.error = null;
|
||||
name: 'submit',
|
||||
initialState,
|
||||
reducers: {
|
||||
clearCurrentSubmit: (state) => {
|
||||
state.currentSubmit = undefined;
|
||||
state.status = 'idle';
|
||||
state.error = null;
|
||||
},
|
||||
clearSubmitsByMission: (state, action: PayloadAction<number>) => {
|
||||
delete state.submitsById[action.payload];
|
||||
},
|
||||
},
|
||||
clearSubmitsByMission: (state, action: PayloadAction<number>) => {
|
||||
delete state.submitsById[action.payload];
|
||||
extraReducers: (builder) => {
|
||||
// Отправка решения
|
||||
builder.addCase(submitMission.pending, (state) => {
|
||||
state.status = 'loading';
|
||||
state.error = null;
|
||||
});
|
||||
builder.addCase(
|
||||
submitMission.fulfilled,
|
||||
(state, action: PayloadAction<Submit>) => {
|
||||
state.status = 'successful';
|
||||
state.submits.push(action.payload);
|
||||
},
|
||||
);
|
||||
builder.addCase(
|
||||
submitMission.rejected,
|
||||
(state, action: PayloadAction<any>) => {
|
||||
state.status = 'failed';
|
||||
state.error = action.payload;
|
||||
},
|
||||
);
|
||||
|
||||
// Получить все свои отправки
|
||||
builder.addCase(fetchMySubmits.pending, (state) => {
|
||||
state.status = 'loading';
|
||||
state.error = null;
|
||||
});
|
||||
builder.addCase(
|
||||
fetchMySubmits.fulfilled,
|
||||
(state, action: PayloadAction<Submit[]>) => {
|
||||
state.status = 'successful';
|
||||
state.submits = action.payload;
|
||||
},
|
||||
);
|
||||
builder.addCase(
|
||||
fetchMySubmits.rejected,
|
||||
(state, action: PayloadAction<any>) => {
|
||||
state.status = 'failed';
|
||||
state.error = action.payload;
|
||||
},
|
||||
);
|
||||
|
||||
// Получить отправку по ID
|
||||
builder.addCase(fetchSubmitById.pending, (state) => {
|
||||
state.status = 'loading';
|
||||
state.error = null;
|
||||
});
|
||||
builder.addCase(
|
||||
fetchSubmitById.fulfilled,
|
||||
(state, action: PayloadAction<Submit>) => {
|
||||
state.status = 'successful';
|
||||
state.currentSubmit = action.payload;
|
||||
},
|
||||
);
|
||||
builder.addCase(
|
||||
fetchSubmitById.rejected,
|
||||
(state, action: PayloadAction<any>) => {
|
||||
state.status = 'failed';
|
||||
state.error = action.payload;
|
||||
},
|
||||
);
|
||||
|
||||
// ✅ Получить отправки по миссии
|
||||
builder.addCase(fetchMySubmitsByMission.pending, (state) => {
|
||||
state.status = 'loading';
|
||||
state.error = null;
|
||||
});
|
||||
builder.addCase(
|
||||
fetchMySubmitsByMission.fulfilled,
|
||||
(
|
||||
state,
|
||||
action: PayloadAction<{
|
||||
missionId: number;
|
||||
data: MissionSubmit[];
|
||||
}>,
|
||||
) => {
|
||||
state.status = 'successful';
|
||||
state.submitsById[action.payload.missionId] =
|
||||
action.payload.data;
|
||||
},
|
||||
);
|
||||
builder.addCase(
|
||||
fetchMySubmitsByMission.rejected,
|
||||
(state, action: PayloadAction<any>) => {
|
||||
state.status = 'failed';
|
||||
state.error = action.payload;
|
||||
},
|
||||
);
|
||||
},
|
||||
},
|
||||
extraReducers: (builder) => {
|
||||
// Отправка решения
|
||||
builder.addCase(submitMission.pending, (state) => {
|
||||
state.status = "loading";
|
||||
state.error = null;
|
||||
});
|
||||
builder.addCase(submitMission.fulfilled, (state, action: PayloadAction<Submit>) => {
|
||||
state.status = "successful";
|
||||
state.submits.push(action.payload);
|
||||
});
|
||||
builder.addCase(submitMission.rejected, (state, action: PayloadAction<any>) => {
|
||||
state.status = "failed";
|
||||
state.error = action.payload;
|
||||
});
|
||||
|
||||
// Получить все свои отправки
|
||||
builder.addCase(fetchMySubmits.pending, (state) => {
|
||||
state.status = "loading";
|
||||
state.error = null;
|
||||
});
|
||||
builder.addCase(fetchMySubmits.fulfilled, (state, action: PayloadAction<Submit[]>) => {
|
||||
state.status = "successful";
|
||||
state.submits = action.payload;
|
||||
});
|
||||
builder.addCase(fetchMySubmits.rejected, (state, action: PayloadAction<any>) => {
|
||||
state.status = "failed";
|
||||
state.error = action.payload;
|
||||
});
|
||||
|
||||
// Получить отправку по ID
|
||||
builder.addCase(fetchSubmitById.pending, (state) => {
|
||||
state.status = "loading";
|
||||
state.error = null;
|
||||
});
|
||||
builder.addCase(fetchSubmitById.fulfilled, (state, action: PayloadAction<Submit>) => {
|
||||
state.status = "successful";
|
||||
state.currentSubmit = action.payload;
|
||||
});
|
||||
builder.addCase(fetchSubmitById.rejected, (state, action: PayloadAction<any>) => {
|
||||
state.status = "failed";
|
||||
state.error = action.payload;
|
||||
});
|
||||
|
||||
// ✅ Получить отправки по миссии
|
||||
builder.addCase(fetchMySubmitsByMission.pending, (state) => {
|
||||
state.status = "loading";
|
||||
state.error = null;
|
||||
});
|
||||
builder.addCase(
|
||||
fetchMySubmitsByMission.fulfilled,
|
||||
(state, action: PayloadAction<{ missionId: number; data: MissionSubmit[] }>) => {
|
||||
state.status = "successful";
|
||||
state.submitsById[action.payload.missionId] = action.payload.data;
|
||||
}
|
||||
);
|
||||
builder.addCase(fetchMySubmitsByMission.rejected, (state, action: PayloadAction<any>) => {
|
||||
state.status = "failed";
|
||||
state.error = action.payload;
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
export const { clearCurrentSubmit, clearSubmitsByMission } = submitSlice.actions;
|
||||
export const { clearCurrentSubmit, clearSubmitsByMission } =
|
||||
submitSlice.actions;
|
||||
export const submitReducer = submitSlice.reducer;
|
||||
|
||||
Reference in New Issue
Block a user