Files
LiquidCode_Frontend/src/redux/slices/groups.ts
Виталий Лавшонок d1a46435c4 add error toasts
2025-12-10 01:33:16 +03:00

510 lines
15 KiB
TypeScript

import { createSlice, createAsyncThunk, PayloadAction } from '@reduxjs/toolkit';
import axios from '../../axios';
import { toastError } from '../../lib/toastNotification';
// =====================
// Типы
// =====================
type Status = 'idle' | 'loading' | 'successful' | 'failed';
export interface GroupMember {
userId: number;
username: string;
role: string;
}
export interface Group {
id: number;
name: string;
description: string;
members: GroupMember[];
contests: any[];
}
// =====================
// Состояние
// =====================
interface GroupsState {
fetchMyGroups: {
groups: Group[];
status: Status;
error?: string;
};
fetchGroupById: {
group?: Group;
status: Status;
error?: string;
};
createGroup: {
group?: Group;
status: Status;
error?: string;
};
updateGroup: {
group?: Group;
status: Status;
error?: string;
};
deleteGroup: {
deletedId?: number;
status: Status;
error?: string;
};
addGroupMember: {
status: Status;
error?: string;
};
removeGroupMember: {
status: Status;
error?: string;
};
fetchGroupJoinLink: {
joinLink?: { token: string; expiresAt: string };
status: Status;
error?: string;
};
joinGroupByToken: {
group?: Group;
status: Status;
error?: string;
};
}
const initialState: GroupsState = {
fetchMyGroups: {
groups: [],
status: 'idle',
error: undefined,
},
fetchGroupById: {
group: undefined,
status: 'idle',
error: undefined,
},
createGroup: {
group: undefined,
status: 'idle',
error: undefined,
},
updateGroup: {
group: undefined,
status: 'idle',
error: undefined,
},
deleteGroup: {
deletedId: undefined,
status: 'idle',
error: undefined,
},
addGroupMember: {
status: 'idle',
error: undefined,
},
removeGroupMember: {
status: 'idle',
error: undefined,
},
fetchGroupJoinLink: {
joinLink: undefined,
status: 'idle',
error: undefined,
},
joinGroupByToken: {
group: undefined,
status: 'idle',
error: undefined,
},
};
// =====================
// Async Thunks
// =====================
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);
}
},
);
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);
}
},
);
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);
}
},
);
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);
}
},
);
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);
}
},
);
export const addGroupMember = createAsyncThunk(
'groups/addGroupMember',
async (
{
groupId,
userId,
role,
}: { groupId: number; userId: number; role: string },
{ rejectWithValue },
) => {
try {
const response = await axios.post(`/groups/${groupId}/members`, {
userId,
role,
});
return response.data;
} catch (err: any) {
return rejectWithValue(err.response?.data);
}
},
);
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);
}
},
);
// =====================
// Новые Async Thunks
// =====================
// Получение актуальной ссылки для присоединения к группе
export const fetchGroupJoinLink = createAsyncThunk(
'groups/fetchGroupJoinLink',
async (groupId: number, { rejectWithValue }) => {
try {
const response = await axios.get(`/groups/${groupId}/join-link`);
return response.data as { token: string; expiresAt: string };
} catch (err: any) {
return rejectWithValue(err.response?.data);
}
},
);
// Присоединение к группе по токену приглашения
export const joinGroupByToken = createAsyncThunk(
'groups/joinGroupByToken',
async (token: string, { rejectWithValue }) => {
try {
const response = await axios.post(`/groups/join/${token}`);
return response.data as Group;
} catch (err: any) {
return rejectWithValue(err.response?.data);
}
},
);
// =====================
// Slice
// =====================
const groupsSlice = createSlice({
name: 'groups',
initialState,
reducers: {
setGroupsStatus: (
state,
action: PayloadAction<{ key: keyof GroupsState; status: Status }>,
) => {
const { key, status } = action.payload;
if (state[key]) {
(state[key] as any).status = status;
}
},
},
extraReducers: (builder) => {
// fetchMyGroups
builder.addCase(fetchMyGroups.pending, (state) => {
state.fetchMyGroups.status = 'loading';
});
builder.addCase(
fetchMyGroups.fulfilled,
(state, action: PayloadAction<Group[]>) => {
state.fetchMyGroups.status = 'successful';
state.fetchMyGroups.groups = action.payload;
},
);
builder.addCase(fetchMyGroups.rejected, (state, action: any) => {
state.fetchMyGroups.status = 'failed';
const errors = action.payload.errors as Record<string, string[]>;
Object.values(errors).forEach((messages) => {
messages.forEach((msg) => {
toastError(msg);
});
});
});
// fetchGroupById
builder.addCase(fetchGroupById.pending, (state) => {
state.fetchGroupById.status = 'loading';
});
builder.addCase(
fetchGroupById.fulfilled,
(state, action: PayloadAction<Group>) => {
state.fetchGroupById.status = 'successful';
state.fetchGroupById.group = action.payload;
},
);
builder.addCase(fetchGroupById.rejected, (state, action: any) => {
state.fetchGroupById.status = 'failed';
const errors = action.payload.errors as Record<string, string[]>;
Object.values(errors).forEach((messages) => {
messages.forEach((msg) => {
toastError(msg);
});
});
});
// createGroup
builder.addCase(createGroup.pending, (state) => {
state.createGroup.status = 'loading';
});
builder.addCase(
createGroup.fulfilled,
(state, action: PayloadAction<Group>) => {
state.createGroup.status = 'successful';
state.createGroup.group = action.payload;
state.fetchMyGroups.groups.push(action.payload);
},
);
builder.addCase(createGroup.rejected, (state, action: any) => {
state.createGroup.status = 'failed';
const errors = action.payload.errors as Record<string, string[]>;
Object.values(errors).forEach((messages) => {
messages.forEach((msg) => {
toastError(msg);
});
});
});
// updateGroup
builder.addCase(updateGroup.pending, (state) => {
state.updateGroup.status = 'loading';
});
builder.addCase(
updateGroup.fulfilled,
(state, action: PayloadAction<Group>) => {
state.updateGroup.status = 'successful';
state.updateGroup.group = action.payload;
const index = state.fetchMyGroups.groups.findIndex(
(g) => g.id === action.payload.id,
);
if (index !== -1)
state.fetchMyGroups.groups[index] = action.payload;
if (state.fetchGroupById.group?.id === action.payload.id)
state.fetchGroupById.group = action.payload;
},
);
builder.addCase(updateGroup.rejected, (state, action: any) => {
state.updateGroup.status = 'failed';
const errors = action.payload.errors as Record<string, string[]>;
Object.values(errors).forEach((messages) => {
messages.forEach((msg) => {
toastError(msg);
});
});
});
// deleteGroup
builder.addCase(deleteGroup.pending, (state) => {
state.deleteGroup.status = 'loading';
});
builder.addCase(
deleteGroup.fulfilled,
(state, action: PayloadAction<number>) => {
state.deleteGroup.status = 'successful';
state.deleteGroup.deletedId = action.payload;
state.fetchMyGroups.groups = state.fetchMyGroups.groups.filter(
(g) => g.id !== action.payload,
);
if (state.fetchGroupById.group?.id === action.payload)
state.fetchGroupById.group = undefined;
},
);
builder.addCase(deleteGroup.rejected, (state, action: any) => {
state.deleteGroup.status = 'failed';
const errors = action.payload.errors as Record<string, string[]>;
Object.values(errors).forEach((messages) => {
messages.forEach((msg) => {
toastError(msg);
});
});
});
// addGroupMember
builder.addCase(addGroupMember.pending, (state) => {
state.addGroupMember.status = 'loading';
});
builder.addCase(addGroupMember.fulfilled, (state) => {
state.addGroupMember.status = 'successful';
});
builder.addCase(addGroupMember.rejected, (state, action: any) => {
state.addGroupMember.status = 'failed';
const errors = action.payload.errors as Record<string, string[]>;
Object.values(errors).forEach((messages) => {
messages.forEach((msg) => {
toastError(msg);
});
});
});
// removeGroupMember
builder.addCase(removeGroupMember.pending, (state) => {
state.removeGroupMember.status = 'loading';
});
builder.addCase(
removeGroupMember.fulfilled,
(
state,
action: PayloadAction<{ groupId: number; memberId: number }>,
) => {
state.removeGroupMember.status = 'successful';
if (
state.fetchGroupById.group &&
state.fetchGroupById.group.id === action.payload.groupId
) {
state.fetchGroupById.group.members =
state.fetchGroupById.group.members.filter(
(m) => m.userId !== action.payload.memberId,
);
}
},
);
builder.addCase(removeGroupMember.rejected, (state, action: any) => {
state.removeGroupMember.status = 'failed';
const errors = action.payload.errors as Record<string, string[]>;
Object.values(errors).forEach((messages) => {
messages.forEach((msg) => {
toastError(msg);
});
});
});
// fetchGroupJoinLink
builder.addCase(fetchGroupJoinLink.pending, (state) => {
state.fetchGroupJoinLink.status = 'loading';
});
builder.addCase(
fetchGroupJoinLink.fulfilled,
(
state,
action: PayloadAction<{ token: string; expiresAt: string }>,
) => {
state.fetchGroupJoinLink.status = 'successful';
state.fetchGroupJoinLink.joinLink = action.payload;
},
);
builder.addCase(fetchGroupJoinLink.rejected, (state, action: any) => {
state.fetchGroupJoinLink.status = 'failed';
const errors = action.payload.errors as Record<string, string[]>;
Object.values(errors).forEach((messages) => {
messages.forEach((msg) => {
toastError(msg);
});
});
});
// joinGroupByToken
builder.addCase(joinGroupByToken.pending, (state) => {
state.joinGroupByToken.status = 'loading';
});
builder.addCase(
joinGroupByToken.fulfilled,
(state, action: PayloadAction<Group>) => {
state.joinGroupByToken.status = 'successful';
state.joinGroupByToken.group = action.payload;
state.fetchMyGroups.groups.push(action.payload); // добавим новую группу в список
},
);
builder.addCase(joinGroupByToken.rejected, (state, action: any) => {
state.joinGroupByToken.status = 'failed';
const errors = action.payload.errors as Record<string, string[]>;
Object.values(errors).forEach((messages) => {
messages.forEach((msg) => {
toastError(msg);
});
});
});
},
});
export const { setGroupsStatus } = groupsSlice.actions;
export const groupsReducer = groupsSlice.reducer;