299 lines
9.2 KiB
TypeScript
299 lines
9.2 KiB
TypeScript
import { createSlice, createAsyncThunk, PayloadAction } from '@reduxjs/toolkit';
|
|
import axios from '../../axios';
|
|
|
|
// ─── Типы ────────────────────────────────────────────
|
|
|
|
type Status = 'idle' | 'loading' | 'successful' | 'failed';
|
|
|
|
export interface Article {
|
|
id: number;
|
|
authorId: number;
|
|
name: string;
|
|
content: string;
|
|
tags: string[];
|
|
createdAt: string;
|
|
updatedAt: string;
|
|
}
|
|
|
|
interface ArticlesState {
|
|
articles: Article[];
|
|
currentArticle?: Article;
|
|
hasNextPage: boolean;
|
|
statuses: {
|
|
create: Status;
|
|
update: Status;
|
|
delete: Status;
|
|
fetchAll: Status;
|
|
fetchById: Status;
|
|
};
|
|
error: string | null;
|
|
}
|
|
|
|
const initialState: ArticlesState = {
|
|
articles: [],
|
|
currentArticle: undefined,
|
|
hasNextPage: false,
|
|
statuses: {
|
|
create: 'idle',
|
|
update: 'idle',
|
|
delete: 'idle',
|
|
fetchAll: 'idle',
|
|
fetchById: 'idle',
|
|
},
|
|
error: null,
|
|
};
|
|
|
|
// ─── Async Thunks ─────────────────────────────────────
|
|
|
|
// POST /articles
|
|
export const createArticle = createAsyncThunk(
|
|
'articles/createArticle',
|
|
async (
|
|
{
|
|
name,
|
|
content,
|
|
tags,
|
|
}: { name: string; content: string; tags: string[] },
|
|
{ rejectWithValue },
|
|
) => {
|
|
try {
|
|
const response = await axios.post('/articles', {
|
|
name,
|
|
content,
|
|
tags,
|
|
});
|
|
return response.data as Article;
|
|
} catch (err: any) {
|
|
return rejectWithValue(
|
|
err.response?.data?.message || 'Ошибка при создании статьи',
|
|
);
|
|
}
|
|
},
|
|
);
|
|
|
|
// PUT /articles/{articleId}
|
|
export const updateArticle = createAsyncThunk(
|
|
'articles/updateArticle',
|
|
async (
|
|
{
|
|
articleId,
|
|
name,
|
|
content,
|
|
tags,
|
|
}: { articleId: number; name: string; content: string; tags: string[] },
|
|
{ rejectWithValue },
|
|
) => {
|
|
try {
|
|
const response = await axios.put(`/articles/${articleId}`, {
|
|
name,
|
|
content,
|
|
tags,
|
|
});
|
|
return response.data as Article;
|
|
} catch (err: any) {
|
|
return rejectWithValue(
|
|
err.response?.data?.message || 'Ошибка при обновлении статьи',
|
|
);
|
|
}
|
|
},
|
|
);
|
|
|
|
// DELETE /articles/{articleId}
|
|
export const deleteArticle = createAsyncThunk(
|
|
'articles/deleteArticle',
|
|
async (articleId: number, { rejectWithValue }) => {
|
|
try {
|
|
await axios.delete(`/articles/${articleId}`);
|
|
return articleId;
|
|
} catch (err: any) {
|
|
return rejectWithValue(
|
|
err.response?.data?.message || 'Ошибка при удалении статьи',
|
|
);
|
|
}
|
|
},
|
|
);
|
|
|
|
// 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 ────────────────────────────────────────────
|
|
|
|
const articlesSlice = createSlice({
|
|
name: 'articles',
|
|
initialState,
|
|
reducers: {
|
|
clearCurrentArticle: (state) => {
|
|
state.currentArticle = undefined;
|
|
},
|
|
setArticlesStatus: (
|
|
state,
|
|
action: PayloadAction<{
|
|
key: keyof ArticlesState['statuses'];
|
|
status: Status;
|
|
}>,
|
|
) => {
|
|
const { key, status } = action.payload;
|
|
state.statuses[key] = 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 ───
|
|
builder.addCase(fetchArticles.pending, (state) => {
|
|
state.statuses.fetchAll = 'loading';
|
|
state.error = null;
|
|
});
|
|
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;
|
|
},
|
|
);
|
|
|
|
// ─── FETCH ARTICLE BY ID ───
|
|
builder.addCase(fetchArticleById.pending, (state) => {
|
|
state.statuses.fetchById = 'loading';
|
|
state.error = null;
|
|
});
|
|
builder.addCase(
|
|
fetchArticleById.fulfilled,
|
|
(state, action: PayloadAction<Article>) => {
|
|
state.statuses.fetchById = 'successful';
|
|
state.currentArticle = action.payload;
|
|
},
|
|
);
|
|
builder.addCase(
|
|
fetchArticleById.rejected,
|
|
(state, action: PayloadAction<any>) => {
|
|
state.statuses.fetchById = 'failed';
|
|
state.error = action.payload;
|
|
},
|
|
);
|
|
},
|
|
});
|
|
|
|
export const { clearCurrentArticle, setArticlesStatus } = articlesSlice.actions;
|
|
export const articlesReducer = articlesSlice.reducer;
|