510 lines
15 KiB
TypeScript
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;
|