360 lines
10 KiB
TypeScript
360 lines
10 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 ArticlesResponse {
|
||
hasNextPage: boolean;
|
||
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;
|
||
};
|
||
}
|
||
|
||
const initialState: ArticlesState = {
|
||
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,
|
||
},
|
||
};
|
||
|
||
// =====================
|
||
// Async Thunks
|
||
// =====================
|
||
|
||
// Все статьи
|
||
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/create',
|
||
async (
|
||
{
|
||
name,
|
||
content,
|
||
tags,
|
||
}: { name: string; content: string; tags: string[] },
|
||
{ rejectWithValue },
|
||
) => {
|
||
try {
|
||
const response = await axios.post<Article>('/articles', {
|
||
name,
|
||
content,
|
||
tags,
|
||
});
|
||
return response.data;
|
||
} catch (err: any) {
|
||
return rejectWithValue(
|
||
err.response?.data?.message || 'Ошибка при создании статьи',
|
||
);
|
||
}
|
||
},
|
||
);
|
||
|
||
// Обновление статьи
|
||
export const updateArticle = createAsyncThunk(
|
||
'articles/update',
|
||
async (
|
||
{
|
||
articleId,
|
||
name,
|
||
content,
|
||
tags,
|
||
}: { articleId: number; name: string; content: string; tags: string[] },
|
||
{ rejectWithValue },
|
||
) => {
|
||
try {
|
||
const response = await axios.put<Article>(
|
||
`/articles/${articleId}`,
|
||
{
|
||
name,
|
||
content,
|
||
tags,
|
||
},
|
||
);
|
||
return response.data;
|
||
} catch (err: any) {
|
||
return rejectWithValue(
|
||
err.response?.data?.message || 'Ошибка при обновлении статьи',
|
||
);
|
||
}
|
||
},
|
||
);
|
||
|
||
// Удаление статьи
|
||
export const deleteArticle = createAsyncThunk(
|
||
'articles/delete',
|
||
async (articleId: number, { rejectWithValue }) => {
|
||
try {
|
||
await axios.delete(`/articles/${articleId}`);
|
||
return articleId;
|
||
} catch (err: any) {
|
||
return rejectWithValue(
|
||
err.response?.data?.message || 'Ошибка при удалении статьи',
|
||
);
|
||
}
|
||
},
|
||
);
|
||
|
||
// =====================
|
||
// Slice
|
||
// =====================
|
||
|
||
const articlesSlice = createSlice({
|
||
name: 'articles',
|
||
initialState,
|
||
reducers: {
|
||
setArticlesStatus: (
|
||
state,
|
||
action: PayloadAction<{ key: keyof ArticlesState; status: Status }>,
|
||
) => {
|
||
const { key, status } = action.payload;
|
||
if (state[key]) {
|
||
(state[key] as any).status = status;
|
||
}
|
||
},
|
||
},
|
||
extraReducers: (builder) => {
|
||
// fetchArticles
|
||
builder.addCase(fetchArticles.pending, (state) => {
|
||
state.fetchArticles.status = 'loading';
|
||
state.fetchArticles.error = undefined;
|
||
});
|
||
builder.addCase(
|
||
fetchArticles.fulfilled,
|
||
(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;
|
||
});
|
||
|
||
// 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.fetchArticleById.status = 'loading';
|
||
state.fetchArticleById.error = undefined;
|
||
});
|
||
builder.addCase(
|
||
fetchArticleById.fulfilled,
|
||
(state, action: PayloadAction<Article>) => {
|
||
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(
|
||
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 { setArticlesStatus } = articlesSlice.actions;
|
||
export const articlesReducer = articlesSlice.reducer;
|