update articles slice

This commit is contained in:
Виталий Лавшонок
2025-11-13 16:32:32 +03:00
parent 18d17f895d
commit ded41ba7f0
8 changed files with 496 additions and 285 deletions

View File

@@ -1,7 +1,9 @@
import { createSlice, createAsyncThunk, PayloadAction } from '@reduxjs/toolkit';
import axios from '../../axios';
// ─── Типы ────────────────────────────────────────────
// =====================
// Типы
// =====================
type Status = 'idle' | 'loading' | 'successful' | 'failed';
@@ -15,39 +17,145 @@ export interface Article {
updatedAt: string;
}
interface ArticlesState {
articles: Article[];
currentArticle?: Article;
interface ArticlesResponse {
hasNextPage: boolean;
statuses: {
create: Status;
update: Status;
delete: Status;
fetchAll: Status;
fetchById: Status;
articles: Article[];
}
// =====================
// Состояние
// =====================
interface ArticlesState {
fetchArticles: {
articles: Article[];
hasNextPage: boolean;
status: Status;
error?: string;
};
fetchArticleById: {
article?: Article;
status: Status;
error?: string;
};
createArticle: {
article?: Article;
status: Status;
error?: string;
};
updateArticle: {
article?: Article;
status: Status;
error?: string;
};
deleteArticle: {
status: Status;
error?: string;
};
fetchMyArticles: {
articles: Article[];
status: Status;
error?: string;
};
error: string | null;
}
const initialState: ArticlesState = {
articles: [],
currentArticle: undefined,
hasNextPage: false,
statuses: {
create: 'idle',
update: 'idle',
delete: 'idle',
fetchAll: 'idle',
fetchById: 'idle',
fetchArticles: {
articles: [],
hasNextPage: false,
status: 'idle',
error: undefined,
},
fetchArticleById: {
article: undefined,
status: 'idle',
error: undefined,
},
createArticle: {
article: undefined,
status: 'idle',
error: undefined,
},
updateArticle: {
article: undefined,
status: 'idle',
error: undefined,
},
deleteArticle: {
status: 'idle',
error: undefined,
},
fetchMyArticles: {
articles: [],
status: 'idle',
error: undefined,
},
error: null,
};
// ─── Async Thunks ─────────────────────────────────────
// =====================
// Async Thunks
// =====================
// POST /articles
// Все статьи
export const fetchArticles = createAsyncThunk(
'articles/fetchArticles',
async (
{
page = 0,
pageSize = 10,
tags,
}: { page?: number; pageSize?: number; tags?: string[] } = {},
{ rejectWithValue },
) => {
try {
const params: any = { page, pageSize };
if (tags && tags.length > 0) params.tags = tags;
const response = await axios.get<ArticlesResponse>('/articles', {
params,
});
return response.data;
} catch (err: any) {
return rejectWithValue(
err.response?.data?.message || 'Ошибка при получении статей',
);
}
},
);
// Мои статьи
export const fetchMyArticles = createAsyncThunk(
'articles/fetchMyArticles',
async (_, { rejectWithValue }) => {
try {
const response = await axios.get<Article[]>('/articles/my');
return response.data;
} catch (err: any) {
return rejectWithValue(
err.response?.data?.message ||
'Ошибка при получении моих статей',
);
}
},
);
// Статья по ID
export const fetchArticleById = createAsyncThunk(
'articles/fetchById',
async (articleId: number, { rejectWithValue }) => {
try {
const response = await axios.get<Article>(`/articles/${articleId}`);
return response.data;
} catch (err: any) {
return rejectWithValue(
err.response?.data?.message || 'Ошибка при получении статьи',
);
}
},
);
// Создание статьи
export const createArticle = createAsyncThunk(
'articles/createArticle',
'articles/create',
async (
{
name,
@@ -57,12 +165,12 @@ export const createArticle = createAsyncThunk(
{ rejectWithValue },
) => {
try {
const response = await axios.post('/articles', {
const response = await axios.post<Article>('/articles', {
name,
content,
tags,
});
return response.data as Article;
return response.data;
} catch (err: any) {
return rejectWithValue(
err.response?.data?.message || 'Ошибка при создании статьи',
@@ -71,9 +179,9 @@ export const createArticle = createAsyncThunk(
},
);
// PUT /articles/{articleId}
// Обновление статьи
export const updateArticle = createAsyncThunk(
'articles/updateArticle',
'articles/update',
async (
{
articleId,
@@ -84,12 +192,15 @@ export const updateArticle = createAsyncThunk(
{ rejectWithValue },
) => {
try {
const response = await axios.put(`/articles/${articleId}`, {
name,
content,
tags,
});
return response.data as Article;
const response = await axios.put<Article>(
`/articles/${articleId}`,
{
name,
content,
tags,
},
);
return response.data;
} catch (err: any) {
return rejectWithValue(
err.response?.data?.message || 'Ошибка при обновлении статьи',
@@ -98,9 +209,9 @@ export const updateArticle = createAsyncThunk(
},
);
// DELETE /articles/{articleId}
// Удаление статьи
export const deleteArticle = createAsyncThunk(
'articles/deleteArticle',
'articles/delete',
async (articleId: number, { rejectWithValue }) => {
try {
await axios.delete(`/articles/${articleId}`);
@@ -113,186 +224,136 @@ export const deleteArticle = createAsyncThunk(
},
);
// GET /articles
export const fetchArticles = createAsyncThunk(
'articles/fetchArticles',
async (
{
page = 0,
pageSize = 10,
tags,
}: { page?: number; pageSize?: number; tags?: string[] },
{ rejectWithValue },
) => {
try {
const params: any = { page, pageSize };
if (tags && tags.length > 0) params.tags = tags;
const response = await axios.get('/articles', { params });
return response.data as {
hasNextPage: boolean;
articles: Article[];
};
} catch (err: any) {
return rejectWithValue(
err.response?.data?.message || 'Ошибка при получении статей',
);
}
},
);
// GET /articles/{articleId}
export const fetchArticleById = createAsyncThunk(
'articles/fetchArticleById',
async (articleId: number, { rejectWithValue }) => {
try {
const response = await axios.get(`/articles/${articleId}`);
return response.data as Article;
} catch (err: any) {
return rejectWithValue(
err.response?.data?.message || 'Ошибка при получении статьи',
);
}
},
);
// ─── Slice ────────────────────────────────────────────
// =====================
// Slice
// =====================
const articlesSlice = createSlice({
name: 'articles',
initialState,
reducers: {
clearCurrentArticle: (state) => {
state.currentArticle = undefined;
},
setArticlesStatus: (
state,
action: PayloadAction<{
key: keyof ArticlesState['statuses'];
status: Status;
}>,
action: PayloadAction<{ key: keyof ArticlesState; status: Status }>,
) => {
const { key, status } = action.payload;
state.statuses[key] = status;
if (state[key]) {
(state[key] as any).status = status;
}
},
},
extraReducers: (builder) => {
// ─── CREATE ARTICLE ───
builder.addCase(createArticle.pending, (state) => {
state.statuses.create = 'loading';
state.error = null;
});
builder.addCase(
createArticle.fulfilled,
(state, action: PayloadAction<Article>) => {
state.statuses.create = 'successful';
state.articles.push(action.payload);
},
);
builder.addCase(
createArticle.rejected,
(state, action: PayloadAction<any>) => {
state.statuses.create = 'failed';
state.error = action.payload;
},
);
// ─── UPDATE ARTICLE ───
builder.addCase(updateArticle.pending, (state) => {
state.statuses.update = 'loading';
state.error = null;
});
builder.addCase(
updateArticle.fulfilled,
(state, action: PayloadAction<Article>) => {
state.statuses.update = 'successful';
const index = state.articles.findIndex(
(a) => a.id === action.payload.id,
);
if (index !== -1) state.articles[index] = action.payload;
if (state.currentArticle?.id === action.payload.id)
state.currentArticle = action.payload;
},
);
builder.addCase(
updateArticle.rejected,
(state, action: PayloadAction<any>) => {
state.statuses.update = 'failed';
state.error = action.payload;
},
);
// ─── DELETE ARTICLE ───
builder.addCase(deleteArticle.pending, (state) => {
state.statuses.delete = 'loading';
state.error = null;
});
builder.addCase(
deleteArticle.fulfilled,
(state, action: PayloadAction<number>) => {
state.statuses.delete = 'successful';
state.articles = state.articles.filter(
(a) => a.id !== action.payload,
);
if (state.currentArticle?.id === action.payload)
state.currentArticle = undefined;
},
);
builder.addCase(
deleteArticle.rejected,
(state, action: PayloadAction<any>) => {
state.statuses.delete = 'failed';
state.error = action.payload;
},
);
// ─── FETCH ARTICLES ───
// fetchArticles
builder.addCase(fetchArticles.pending, (state) => {
state.statuses.fetchAll = 'loading';
state.error = null;
state.fetchArticles.status = 'loading';
state.fetchArticles.error = undefined;
});
builder.addCase(
fetchArticles.fulfilled,
(
state,
action: PayloadAction<{
hasNextPage: boolean;
articles: Article[];
}>,
) => {
state.statuses.fetchAll = 'successful';
state.articles = action.payload.articles;
state.hasNextPage = action.payload.hasNextPage;
},
);
builder.addCase(
fetchArticles.rejected,
(state, action: PayloadAction<any>) => {
state.statuses.fetchAll = 'failed';
state.error = action.payload;
(state, action: PayloadAction<ArticlesResponse>) => {
state.fetchArticles.status = 'successful';
state.fetchArticles.articles = action.payload.articles;
state.fetchArticles.hasNextPage = action.payload.hasNextPage;
},
);
builder.addCase(fetchArticles.rejected, (state, action: any) => {
state.fetchArticles.status = 'failed';
state.fetchArticles.error = action.payload;
});
// ─── FETCH ARTICLE BY ID ───
// fetchMyArticles
builder.addCase(fetchMyArticles.pending, (state) => {
state.fetchMyArticles.status = 'loading';
state.fetchMyArticles.error = undefined;
});
builder.addCase(
fetchMyArticles.fulfilled,
(state, action: PayloadAction<Article[]>) => {
state.fetchMyArticles.status = 'successful';
state.fetchMyArticles.articles = action.payload;
},
);
builder.addCase(fetchMyArticles.rejected, (state, action: any) => {
state.fetchMyArticles.status = 'failed';
state.fetchMyArticles.error = action.payload;
});
// fetchArticleById
builder.addCase(fetchArticleById.pending, (state) => {
state.statuses.fetchById = 'loading';
state.error = null;
state.fetchArticleById.status = 'loading';
state.fetchArticleById.error = undefined;
});
builder.addCase(
fetchArticleById.fulfilled,
(state, action: PayloadAction<Article>) => {
state.statuses.fetchById = 'successful';
state.currentArticle = action.payload;
state.fetchArticleById.status = 'successful';
state.fetchArticleById.article = action.payload;
},
);
builder.addCase(fetchArticleById.rejected, (state, action: any) => {
state.fetchArticleById.status = 'failed';
state.fetchArticleById.error = action.payload;
});
// createArticle
builder.addCase(createArticle.pending, (state) => {
state.createArticle.status = 'loading';
state.createArticle.error = undefined;
});
builder.addCase(
fetchArticleById.rejected,
(state, action: PayloadAction<any>) => {
state.statuses.fetchById = 'failed';
state.error = action.payload;
createArticle.fulfilled,
(state, action: PayloadAction<Article>) => {
state.createArticle.status = 'successful';
state.createArticle.article = action.payload;
},
);
builder.addCase(createArticle.rejected, (state, action: any) => {
state.createArticle.status = 'failed';
state.createArticle.error = action.payload;
});
// updateArticle
builder.addCase(updateArticle.pending, (state) => {
state.updateArticle.status = 'loading';
state.updateArticle.error = undefined;
});
builder.addCase(
updateArticle.fulfilled,
(state, action: PayloadAction<Article>) => {
state.updateArticle.status = 'successful';
state.updateArticle.article = action.payload;
},
);
builder.addCase(updateArticle.rejected, (state, action: any) => {
state.updateArticle.status = 'failed';
state.updateArticle.error = action.payload;
});
// deleteArticle
builder.addCase(deleteArticle.pending, (state) => {
state.deleteArticle.status = 'loading';
state.deleteArticle.error = undefined;
});
builder.addCase(
deleteArticle.fulfilled,
(state, action: PayloadAction<number>) => {
state.deleteArticle.status = 'successful';
state.fetchArticles.articles =
state.fetchArticles.articles.filter(
(a) => a.id !== action.payload,
);
state.fetchMyArticles.articles =
state.fetchMyArticles.articles.filter(
(a) => a.id !== action.payload,
);
},
);
builder.addCase(deleteArticle.rejected, (state, action: any) => {
state.deleteArticle.status = 'failed';
state.deleteArticle.error = action.payload;
});
},
});
export const { clearCurrentArticle, setArticlesStatus } = articlesSlice.actions;
export const { setArticlesStatus } = articlesSlice.actions;
export const articlesReducer = articlesSlice.reducer;