auth + groups invite
This commit is contained in:
@@ -1,7 +1,9 @@
|
||||
import { createSlice, createAsyncThunk, PayloadAction } from '@reduxjs/toolkit';
|
||||
import axios from '../../axios';
|
||||
|
||||
// ─── Типы ────────────────────────────────────────────
|
||||
// =====================
|
||||
// Типы
|
||||
// =====================
|
||||
|
||||
type Status = 'idle' | 'loading' | 'successful' | 'failed';
|
||||
|
||||
@@ -19,39 +21,106 @@ export interface Group {
|
||||
contests: any[];
|
||||
}
|
||||
|
||||
// =====================
|
||||
// Состояние
|
||||
// =====================
|
||||
|
||||
interface GroupsState {
|
||||
groups: Group[];
|
||||
currentGroup: Group | null;
|
||||
statuses: {
|
||||
create: Status;
|
||||
update: Status;
|
||||
delete: Status;
|
||||
fetchMy: Status;
|
||||
fetchById: Status;
|
||||
addMember: Status;
|
||||
removeMember: Status;
|
||||
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;
|
||||
};
|
||||
error: string | null;
|
||||
}
|
||||
|
||||
const initialState: GroupsState = {
|
||||
groups: [],
|
||||
currentGroup: null,
|
||||
statuses: {
|
||||
create: 'idle',
|
||||
update: 'idle',
|
||||
delete: 'idle',
|
||||
fetchMy: 'idle',
|
||||
fetchById: 'idle',
|
||||
addMember: 'idle',
|
||||
removeMember: 'idle',
|
||||
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,
|
||||
},
|
||||
error: null,
|
||||
};
|
||||
|
||||
// ─── Async Thunks ─────────────────────────────────────
|
||||
// =====================
|
||||
// Async Thunks
|
||||
// =====================
|
||||
|
||||
// POST /groups
|
||||
export const createGroup = createAsyncThunk(
|
||||
'groups/createGroup',
|
||||
async (
|
||||
@@ -69,7 +138,6 @@ export const createGroup = createAsyncThunk(
|
||||
},
|
||||
);
|
||||
|
||||
// PUT /groups/{groupId}
|
||||
export const updateGroup = createAsyncThunk(
|
||||
'groups/updateGroup',
|
||||
async (
|
||||
@@ -94,7 +162,6 @@ export const updateGroup = createAsyncThunk(
|
||||
},
|
||||
);
|
||||
|
||||
// DELETE /groups/{groupId}
|
||||
export const deleteGroup = createAsyncThunk(
|
||||
'groups/deleteGroup',
|
||||
async (groupId: number, { rejectWithValue }) => {
|
||||
@@ -109,7 +176,6 @@ export const deleteGroup = createAsyncThunk(
|
||||
},
|
||||
);
|
||||
|
||||
// GET /groups/my
|
||||
export const fetchMyGroups = createAsyncThunk(
|
||||
'groups/fetchMyGroups',
|
||||
async (_, { rejectWithValue }) => {
|
||||
@@ -124,7 +190,6 @@ export const fetchMyGroups = createAsyncThunk(
|
||||
},
|
||||
);
|
||||
|
||||
// GET /groups/{groupId}
|
||||
export const fetchGroupById = createAsyncThunk(
|
||||
'groups/fetchGroupById',
|
||||
async (groupId: number, { rejectWithValue }) => {
|
||||
@@ -139,16 +204,22 @@ export const fetchGroupById = createAsyncThunk(
|
||||
},
|
||||
);
|
||||
|
||||
// POST /groups/members
|
||||
export const addGroupMember = createAsyncThunk(
|
||||
'groups/addGroupMember',
|
||||
async (
|
||||
{ userId, role }: { userId: number; role: string },
|
||||
{
|
||||
groupId,
|
||||
userId,
|
||||
role,
|
||||
}: { groupId: number; userId: number; role: string },
|
||||
{ rejectWithValue },
|
||||
) => {
|
||||
try {
|
||||
await axios.post('/groups/members', { userId, role });
|
||||
return { userId, role };
|
||||
const response = await axios.post(`/groups/${groupId}/members`, {
|
||||
userId,
|
||||
role,
|
||||
});
|
||||
return response.data;
|
||||
} catch (err: any) {
|
||||
return rejectWithValue(
|
||||
err.response?.data?.message ||
|
||||
@@ -158,7 +229,6 @@ export const addGroupMember = createAsyncThunk(
|
||||
},
|
||||
);
|
||||
|
||||
// DELETE /groups/{groupId}/members/{memberId}
|
||||
export const removeGroupMember = createAsyncThunk(
|
||||
'groups/removeGroupMember',
|
||||
async (
|
||||
@@ -176,147 +246,169 @@ export const removeGroupMember = createAsyncThunk(
|
||||
},
|
||||
);
|
||||
|
||||
// ─── Slice ────────────────────────────────────────────
|
||||
// =====================
|
||||
// Новые 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?.message ||
|
||||
'Ошибка при получении ссылки для присоединения',
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
// Присоединение к группе по токену приглашения
|
||||
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?.message ||
|
||||
'Ошибка при присоединении к группе по ссылке',
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
// =====================
|
||||
// Slice
|
||||
// =====================
|
||||
|
||||
const groupsSlice = createSlice({
|
||||
name: 'groups',
|
||||
initialState,
|
||||
reducers: {
|
||||
clearCurrentGroup: (state) => {
|
||||
state.currentGroup = null;
|
||||
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) => {
|
||||
// ─── 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 ───
|
||||
// fetchMyGroups
|
||||
builder.addCase(fetchMyGroups.pending, (state) => {
|
||||
state.statuses.fetchMy = 'loading';
|
||||
state.error = null;
|
||||
state.fetchMyGroups.status = 'loading';
|
||||
});
|
||||
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;
|
||||
state.fetchMyGroups.status = 'successful';
|
||||
state.fetchMyGroups.groups = action.payload;
|
||||
},
|
||||
);
|
||||
builder.addCase(fetchMyGroups.rejected, (state, action: any) => {
|
||||
state.fetchMyGroups.status = 'failed';
|
||||
state.fetchMyGroups.error = action.payload;
|
||||
});
|
||||
|
||||
// ─── FETCH GROUP BY ID ───
|
||||
// fetchGroupById
|
||||
builder.addCase(fetchGroupById.pending, (state) => {
|
||||
state.statuses.fetchById = 'loading';
|
||||
state.error = null;
|
||||
state.fetchGroupById.status = 'loading';
|
||||
});
|
||||
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;
|
||||
state.fetchGroupById.status = 'successful';
|
||||
state.fetchGroupById.group = action.payload;
|
||||
},
|
||||
);
|
||||
builder.addCase(fetchGroupById.rejected, (state, action: any) => {
|
||||
state.fetchGroupById.status = 'failed';
|
||||
state.fetchGroupById.error = action.payload;
|
||||
});
|
||||
|
||||
// ─── ADD MEMBER ───
|
||||
// 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';
|
||||
state.createGroup.error = action.payload;
|
||||
});
|
||||
|
||||
// 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';
|
||||
state.updateGroup.error = action.payload;
|
||||
});
|
||||
|
||||
// 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';
|
||||
state.deleteGroup.error = action.payload;
|
||||
});
|
||||
|
||||
// addGroupMember
|
||||
builder.addCase(addGroupMember.pending, (state) => {
|
||||
state.statuses.addMember = 'loading';
|
||||
state.error = null;
|
||||
state.addGroupMember.status = 'loading';
|
||||
});
|
||||
builder.addCase(addGroupMember.fulfilled, (state) => {
|
||||
state.statuses.addMember = 'successful';
|
||||
state.addGroupMember.status = 'successful';
|
||||
});
|
||||
builder.addCase(addGroupMember.rejected, (state, action: any) => {
|
||||
state.addGroupMember.status = 'failed';
|
||||
state.addGroupMember.error = action.payload;
|
||||
});
|
||||
builder.addCase(
|
||||
addGroupMember.rejected,
|
||||
(state, action: PayloadAction<any>) => {
|
||||
state.statuses.addMember = 'failed';
|
||||
state.error = action.payload;
|
||||
},
|
||||
);
|
||||
|
||||
// ─── REMOVE MEMBER ───
|
||||
// removeGroupMember
|
||||
builder.addCase(removeGroupMember.pending, (state) => {
|
||||
state.statuses.removeMember = 'loading';
|
||||
state.error = null;
|
||||
state.removeGroupMember.status = 'loading';
|
||||
});
|
||||
builder.addCase(
|
||||
removeGroupMember.fulfilled,
|
||||
@@ -324,27 +416,60 @@ const groupsSlice = createSlice({
|
||||
state,
|
||||
action: PayloadAction<{ groupId: number; memberId: number }>,
|
||||
) => {
|
||||
state.statuses.removeMember = 'successful';
|
||||
state.removeGroupMember.status = 'successful';
|
||||
if (
|
||||
state.currentGroup &&
|
||||
state.currentGroup.id === action.payload.groupId
|
||||
state.fetchGroupById.group &&
|
||||
state.fetchGroupById.group.id === action.payload.groupId
|
||||
) {
|
||||
state.currentGroup.members =
|
||||
state.currentGroup.members.filter(
|
||||
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';
|
||||
state.removeGroupMember.error = action.payload;
|
||||
});
|
||||
|
||||
// fetchGroupJoinLink
|
||||
builder.addCase(fetchGroupJoinLink.pending, (state) => {
|
||||
state.fetchGroupJoinLink.status = 'loading';
|
||||
});
|
||||
builder.addCase(
|
||||
removeGroupMember.rejected,
|
||||
(state, action: PayloadAction<any>) => {
|
||||
state.statuses.removeMember = 'failed';
|
||||
state.error = action.payload;
|
||||
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';
|
||||
state.fetchGroupJoinLink.error = action.payload;
|
||||
});
|
||||
|
||||
// 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';
|
||||
state.joinGroupByToken.error = action.payload;
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
export const { clearCurrentGroup } = groupsSlice.actions;
|
||||
export const { setGroupsStatus } = groupsSlice.actions;
|
||||
export const groupsReducer = groupsSlice.reducer;
|
||||
|
||||
Reference in New Issue
Block a user