369 lines
10 KiB
TypeScript
369 lines
10 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 Post {
|
|
id: number;
|
|
groupId: number;
|
|
authorId: number;
|
|
authorUsername: string;
|
|
name: string;
|
|
content: string;
|
|
createdAt: string;
|
|
updatedAt: string;
|
|
}
|
|
|
|
export interface PostsPage {
|
|
items: Post[];
|
|
hasNext: boolean;
|
|
}
|
|
|
|
// =====================
|
|
// Состояние
|
|
// =====================
|
|
|
|
interface PostsState {
|
|
fetchPosts: {
|
|
pages: Record<number, PostsPage>; // страница => данные
|
|
status: Status;
|
|
error?: string;
|
|
};
|
|
fetchPostById: {
|
|
post?: Post;
|
|
status: Status;
|
|
error?: string;
|
|
};
|
|
createPost: {
|
|
post?: Post;
|
|
status: Status;
|
|
error?: string;
|
|
};
|
|
updatePost: {
|
|
post?: Post;
|
|
status: Status;
|
|
error?: string;
|
|
};
|
|
deletePost: {
|
|
deletedId?: number;
|
|
status: Status;
|
|
error?: string;
|
|
};
|
|
}
|
|
|
|
const initialState: PostsState = {
|
|
fetchPosts: {
|
|
pages: {},
|
|
status: 'idle',
|
|
error: undefined,
|
|
},
|
|
fetchPostById: {
|
|
post: undefined,
|
|
status: 'idle',
|
|
error: undefined,
|
|
},
|
|
createPost: {
|
|
post: undefined,
|
|
status: 'idle',
|
|
error: undefined,
|
|
},
|
|
updatePost: {
|
|
post: undefined,
|
|
status: 'idle',
|
|
error: undefined,
|
|
},
|
|
deletePost: {
|
|
deletedId: undefined,
|
|
status: 'idle',
|
|
error: undefined,
|
|
},
|
|
};
|
|
|
|
// =====================
|
|
// Async Thunks
|
|
// =====================
|
|
|
|
// Получить посты группы (пагинация)
|
|
export const fetchGroupPosts = createAsyncThunk(
|
|
'posts/fetchGroupPosts',
|
|
async (
|
|
{
|
|
groupId,
|
|
page = 0,
|
|
pageSize = 100,
|
|
}: { groupId: number; page?: number; pageSize?: number },
|
|
{ rejectWithValue },
|
|
) => {
|
|
try {
|
|
const response = await axios.get(
|
|
`/groups/${groupId}/feed?page=${page}&pageSize=${pageSize}`,
|
|
);
|
|
return { page, data: response.data as PostsPage };
|
|
} catch (err: any) {
|
|
return rejectWithValue(err.response?.data);
|
|
}
|
|
},
|
|
);
|
|
|
|
// Получить один пост
|
|
export const fetchPostById = createAsyncThunk(
|
|
'posts/fetchPostById',
|
|
async (
|
|
{ groupId, postId }: { groupId: number; postId: number },
|
|
{ rejectWithValue },
|
|
) => {
|
|
try {
|
|
const response = await axios.get(
|
|
`/groups/${groupId}/feed/${postId}`,
|
|
);
|
|
return response.data as Post;
|
|
} catch (err: any) {
|
|
return rejectWithValue(err.response?.data);
|
|
}
|
|
},
|
|
);
|
|
|
|
// Создать пост
|
|
export const createPost = createAsyncThunk(
|
|
'posts/createPost',
|
|
async (
|
|
{
|
|
groupId,
|
|
name,
|
|
content,
|
|
}: { groupId: number; name: string; content: string },
|
|
{ rejectWithValue },
|
|
) => {
|
|
try {
|
|
const response = await axios.post(`/groups/${groupId}/feed`, {
|
|
name,
|
|
content,
|
|
});
|
|
return response.data as Post;
|
|
} catch (err: any) {
|
|
return rejectWithValue(err.response?.data);
|
|
}
|
|
},
|
|
);
|
|
|
|
// Обновить пост
|
|
export const updatePost = createAsyncThunk(
|
|
'posts/updatePost',
|
|
async (
|
|
{
|
|
groupId,
|
|
postId,
|
|
name,
|
|
content,
|
|
}: {
|
|
groupId: number;
|
|
postId: number;
|
|
name: string;
|
|
content: string;
|
|
},
|
|
{ rejectWithValue },
|
|
) => {
|
|
try {
|
|
const response = await axios.put(
|
|
`/groups/${groupId}/feed/${postId}`,
|
|
{
|
|
name,
|
|
content,
|
|
},
|
|
);
|
|
return response.data as Post;
|
|
} catch (err: any) {
|
|
return rejectWithValue(err.response?.data);
|
|
}
|
|
},
|
|
);
|
|
|
|
// Удалить пост
|
|
export const deletePost = createAsyncThunk(
|
|
'posts/deletePost',
|
|
async (
|
|
{ groupId, postId }: { groupId: number; postId: number },
|
|
{ rejectWithValue },
|
|
) => {
|
|
try {
|
|
await axios.delete(`/groups/${groupId}/feed/${postId}`);
|
|
return postId;
|
|
} catch (err: any) {
|
|
return rejectWithValue(err.response?.data);
|
|
}
|
|
},
|
|
);
|
|
|
|
// =====================
|
|
// Slice
|
|
// =====================
|
|
|
|
const postsSlice = createSlice({
|
|
name: 'posts',
|
|
initialState,
|
|
reducers: {
|
|
setGroupFeedStatus: (
|
|
state,
|
|
action: PayloadAction<{ key: keyof PostsState; status: Status }>,
|
|
) => {
|
|
const { key, status } = action.payload;
|
|
if (state[key]) {
|
|
(state[key] as any).status = status;
|
|
}
|
|
},
|
|
},
|
|
extraReducers: (builder) => {
|
|
// fetchGroupPosts
|
|
builder.addCase(fetchGroupPosts.pending, (state) => {
|
|
state.fetchPosts.status = 'loading';
|
|
});
|
|
builder.addCase(
|
|
fetchGroupPosts.fulfilled,
|
|
(
|
|
state,
|
|
action: PayloadAction<{ page: number; data: PostsPage }>,
|
|
) => {
|
|
const { page, data } = action.payload;
|
|
state.fetchPosts.status = 'successful';
|
|
state.fetchPosts.pages[page] = data;
|
|
},
|
|
);
|
|
builder.addCase(fetchGroupPosts.rejected, (state, action: any) => {
|
|
state.fetchPosts.status = 'failed';
|
|
|
|
const errors = action.payload.errors as Record<string, string[]>;
|
|
Object.values(errors).forEach((messages) => {
|
|
messages.forEach((msg) => {
|
|
toastError(msg);
|
|
});
|
|
});
|
|
});
|
|
|
|
// fetchPostById
|
|
builder.addCase(fetchPostById.pending, (state) => {
|
|
state.fetchPostById.status = 'loading';
|
|
});
|
|
builder.addCase(
|
|
fetchPostById.fulfilled,
|
|
(state, action: PayloadAction<Post>) => {
|
|
state.fetchPostById.status = 'successful';
|
|
state.fetchPostById.post = action.payload;
|
|
},
|
|
);
|
|
builder.addCase(fetchPostById.rejected, (state, action: any) => {
|
|
state.fetchPostById.status = 'failed';
|
|
|
|
const errors = action.payload.errors as Record<string, string[]>;
|
|
Object.values(errors).forEach((messages) => {
|
|
messages.forEach((msg) => {
|
|
toastError(msg);
|
|
});
|
|
});
|
|
});
|
|
|
|
// createPost
|
|
builder.addCase(createPost.pending, (state) => {
|
|
state.createPost.status = 'loading';
|
|
});
|
|
builder.addCase(
|
|
createPost.fulfilled,
|
|
(state, action: PayloadAction<Post>) => {
|
|
state.createPost.status = 'successful';
|
|
state.createPost.post = action.payload;
|
|
|
|
// добавляем сразу в первую страницу (page = 0)
|
|
if (state.fetchPosts.pages[0]) {
|
|
state.fetchPosts.pages[0].items.unshift(action.payload);
|
|
}
|
|
},
|
|
);
|
|
builder.addCase(createPost.rejected, (state, action: any) => {
|
|
state.createPost.status = 'failed';
|
|
|
|
const errors = action.payload.errors as Record<string, string[]>;
|
|
Object.values(errors).forEach((messages) => {
|
|
messages.forEach((msg) => {
|
|
toastError(msg);
|
|
});
|
|
});
|
|
});
|
|
|
|
// updatePost
|
|
builder.addCase(updatePost.pending, (state) => {
|
|
state.updatePost.status = 'loading';
|
|
});
|
|
builder.addCase(
|
|
updatePost.fulfilled,
|
|
(state, action: PayloadAction<Post>) => {
|
|
state.updatePost.status = 'successful';
|
|
state.updatePost.post = action.payload;
|
|
|
|
// обновим в списках
|
|
for (const page of Object.values(state.fetchPosts.pages)) {
|
|
const index = page.items.findIndex(
|
|
(p) => p.id === action.payload.id,
|
|
);
|
|
if (index !== -1) page.items[index] = action.payload;
|
|
}
|
|
|
|
// обновим если открыт одиночный пост
|
|
if (state.fetchPostById.post?.id === action.payload.id) {
|
|
state.fetchPostById.post = action.payload;
|
|
}
|
|
},
|
|
);
|
|
builder.addCase(updatePost.rejected, (state, action: any) => {
|
|
state.updatePost.status = 'failed';
|
|
|
|
const errors = action.payload.errors as Record<string, string[]>;
|
|
Object.values(errors).forEach((messages) => {
|
|
messages.forEach((msg) => {
|
|
toastError(msg);
|
|
});
|
|
});
|
|
});
|
|
|
|
// deletePost
|
|
builder.addCase(deletePost.pending, (state) => {
|
|
state.deletePost.status = 'loading';
|
|
});
|
|
builder.addCase(
|
|
deletePost.fulfilled,
|
|
(state, action: PayloadAction<number>) => {
|
|
state.deletePost.status = 'successful';
|
|
state.deletePost.deletedId = action.payload;
|
|
|
|
// удалить из всех страниц
|
|
for (const page of Object.values(state.fetchPosts.pages)) {
|
|
page.items = page.items.filter(
|
|
(p) => p.id !== action.payload,
|
|
);
|
|
}
|
|
|
|
// если открыт индивидуальный пост
|
|
if (state.fetchPostById.post?.id === action.payload) {
|
|
state.fetchPostById.post = undefined;
|
|
}
|
|
},
|
|
);
|
|
builder.addCase(deletePost.rejected, (state, action: any) => {
|
|
state.deletePost.status = 'failed';
|
|
|
|
const errors = action.payload.errors as Record<string, string[]>;
|
|
Object.values(errors).forEach((messages) => {
|
|
messages.forEach((msg) => {
|
|
toastError(msg);
|
|
});
|
|
});
|
|
});
|
|
},
|
|
});
|
|
|
|
export const { setGroupFeedStatus } = postsSlice.actions;
|
|
export const groupFeedReducer = postsSlice.reducer;
|