import { createSlice, createAsyncThunk, PayloadAction } from '@reduxjs/toolkit'; import axios from '../../axios'; // ===================== // Типы // ===================== 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; // страница => данные 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?.message || 'Ошибка загрузки постов', ); } }, ); // Получить один пост 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?.message || 'Ошибка загрузки поста', ); } }, ); // Создать пост 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?.message || 'Ошибка создания поста', ); } }, ); // Обновить пост 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?.message || 'Ошибка обновления поста', ); } }, ); // Удалить пост 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?.message || 'Ошибка удаления поста', ); } }, ); // ===================== // 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'; state.fetchPosts.error = action.payload; }); // fetchPostById builder.addCase(fetchPostById.pending, (state) => { state.fetchPostById.status = 'loading'; }); builder.addCase( fetchPostById.fulfilled, (state, action: PayloadAction) => { state.fetchPostById.status = 'successful'; state.fetchPostById.post = action.payload; }, ); builder.addCase(fetchPostById.rejected, (state, action: any) => { state.fetchPostById.status = 'failed'; state.fetchPostById.error = action.payload; }); // createPost builder.addCase(createPost.pending, (state) => { state.createPost.status = 'loading'; }); builder.addCase( createPost.fulfilled, (state, action: PayloadAction) => { 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'; state.createPost.error = action.payload; }); // updatePost builder.addCase(updatePost.pending, (state) => { state.updatePost.status = 'loading'; }); builder.addCase( updatePost.fulfilled, (state, action: PayloadAction) => { 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'; state.updatePost.error = action.payload; }); // deletePost builder.addCase(deletePost.pending, (state) => { state.deletePost.status = 'loading'; }); builder.addCase( deletePost.fulfilled, (state, action: PayloadAction) => { 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'; state.deletePost.error = action.payload; }); }, }); export const { setGroupFeedStatus } = postsSlice.actions; export const groupFeedReducer = postsSlice.reducer;