formatting

This commit is contained in:
Виталий Лавшонок
2025-11-04 15:04:59 +03:00
parent 3cd8e14288
commit 4972836164
60 changed files with 3604 additions and 2916 deletions

View File

@@ -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;

View File

@@ -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;
},
);
},
});
// =====================

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;