update articles slice
This commit is contained in:
@@ -5,6 +5,7 @@ import { useEffect } from 'react';
|
|||||||
import { fetchArticleById } from '../redux/slices/articles';
|
import { fetchArticleById } from '../redux/slices/articles';
|
||||||
import MarkdownPreview from '../views/articleeditor/MarckDownPreview';
|
import MarkdownPreview from '../views/articleeditor/MarckDownPreview';
|
||||||
import { useQuery } from '../hooks/useQuery';
|
import { useQuery } from '../hooks/useQuery';
|
||||||
|
import { ArticlesRightPanel } from '../views/home/rightpanel/Articles';
|
||||||
|
|
||||||
const Article = () => {
|
const Article = () => {
|
||||||
// Получаем параметры из URL
|
// Получаем параметры из URL
|
||||||
@@ -19,8 +20,12 @@ const Article = () => {
|
|||||||
return <Navigate to="/home" replace />;
|
return <Navigate to="/home" replace />;
|
||||||
}
|
}
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const article = useAppSelector((state) => state.articles.currentArticle);
|
const article = useAppSelector(
|
||||||
const status = useAppSelector((state) => state.articles.statuses.fetchById);
|
(state) => state.articles.fetchArticleById.article,
|
||||||
|
);
|
||||||
|
const status = useAppSelector(
|
||||||
|
(state) => state.articles.fetchArticleById.status,
|
||||||
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
dispatch(fetchArticleById(articleIdNumber));
|
dispatch(fetchArticleById(articleIdNumber));
|
||||||
@@ -65,7 +70,7 @@ const Article = () => {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className=""></div>
|
<ArticlesRightPanel />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -23,26 +23,33 @@ const ArticleEditor = () => {
|
|||||||
const query = useQuery();
|
const query = useQuery();
|
||||||
const back = query.get('back') ?? undefined;
|
const back = query.get('back') ?? undefined;
|
||||||
const articleId = Number(query.get('articleId') ?? undefined);
|
const articleId = Number(query.get('articleId') ?? undefined);
|
||||||
const article = useAppSelector((state) => state.articles.currentArticle);
|
const refactor = articleId && !isNaN(articleId);
|
||||||
const refactor = articleId != undefined && !isNaN(articleId);
|
|
||||||
|
|
||||||
|
// Достаём данные из redux
|
||||||
|
const article = useAppSelector(
|
||||||
|
(state) => state.articles.fetchArticleById.article,
|
||||||
|
);
|
||||||
|
|
||||||
|
const statusCreate = useAppSelector(
|
||||||
|
(state) => state.articles.createArticle.status,
|
||||||
|
);
|
||||||
|
const statusUpdate = useAppSelector(
|
||||||
|
(state) => state.articles.updateArticle.status,
|
||||||
|
);
|
||||||
|
const statusDelete = useAppSelector(
|
||||||
|
(state) => state.articles.deleteArticle.status,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Локальные состояния
|
||||||
const [code, setCode] = useState<string>(article?.content || '');
|
const [code, setCode] = useState<string>(article?.content || '');
|
||||||
const [name, setName] = useState<string>(article?.name || '');
|
const [name, setName] = useState<string>(article?.name || '');
|
||||||
const [tagInput, setTagInput] = useState<string>('');
|
const [tagInput, setTagInput] = useState<string>('');
|
||||||
const [tags, setTags] = useState<string[]>(article?.tags || []);
|
const [tags, setTags] = useState<string[]>(article?.tags || []);
|
||||||
|
|
||||||
const [activeEditor, setActiveEditor] = useState<boolean>(false);
|
const [activeEditor, setActiveEditor] = useState<boolean>(false);
|
||||||
|
|
||||||
const statusCreate = useAppSelector(
|
// ==========================
|
||||||
(state) => state.articles.statuses.create,
|
// Теги
|
||||||
);
|
// ==========================
|
||||||
const statusUpdate = useAppSelector(
|
|
||||||
(state) => state.articles.statuses.update,
|
|
||||||
);
|
|
||||||
const statusDelete = useAppSelector(
|
|
||||||
(state) => state.articles.statuses.delete,
|
|
||||||
);
|
|
||||||
|
|
||||||
const addTag = () => {
|
const addTag = () => {
|
||||||
const newTag = tagInput.trim();
|
const newTag = tagInput.trim();
|
||||||
if (newTag && !tags.includes(newTag)) {
|
if (newTag && !tags.includes(newTag)) {
|
||||||
@@ -55,53 +62,63 @@ const ArticleEditor = () => {
|
|||||||
setTags(tags.filter((tag) => tag !== tagToRemove));
|
setTags(tags.filter((tag) => tag !== tagToRemove));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// ==========================
|
||||||
|
// Эффекты по статусам
|
||||||
|
// ==========================
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (statusCreate == 'successful') {
|
if (statusCreate === 'successful') {
|
||||||
dispatch(setArticlesStatus({ key: 'create', status: 'idle' }));
|
dispatch(
|
||||||
navigate(back ? back : '/home/articles');
|
setArticlesStatus({ key: 'createArticle', status: 'idle' }),
|
||||||
|
);
|
||||||
|
navigate(back ?? '/home/articles');
|
||||||
}
|
}
|
||||||
}, [statusCreate]);
|
}, [statusCreate]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (statusDelete == 'successful') {
|
if (statusUpdate === 'successful') {
|
||||||
dispatch(setArticlesStatus({ key: 'delete', status: 'idle' }));
|
dispatch(
|
||||||
navigate(back ? back : '/home/articles');
|
setArticlesStatus({ key: 'updateArticle', status: 'idle' }),
|
||||||
}
|
);
|
||||||
}, [statusDelete]);
|
navigate(back ?? '/home/articles');
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (statusUpdate == 'successful') {
|
|
||||||
dispatch(setArticlesStatus({ key: 'update', status: 'idle' }));
|
|
||||||
navigate(back ? back : '/home/articles');
|
|
||||||
}
|
}
|
||||||
}, [statusUpdate]);
|
}, [statusUpdate]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (statusDelete === 'successful') {
|
||||||
|
dispatch(
|
||||||
|
setArticlesStatus({ key: 'deleteArticle', status: 'idle' }),
|
||||||
|
);
|
||||||
|
navigate(back ?? '/home/articles');
|
||||||
|
}
|
||||||
|
}, [statusDelete]);
|
||||||
|
|
||||||
|
// ==========================
|
||||||
|
// Получение статьи
|
||||||
|
// ==========================
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (articleId) {
|
if (articleId) {
|
||||||
dispatch(fetchArticleById(articleId));
|
dispatch(fetchArticleById(articleId));
|
||||||
}
|
}
|
||||||
}, [articleId]);
|
}, [articleId]);
|
||||||
|
|
||||||
|
// Обновление локального состояния после загрузки статьи
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (article && refactor) {
|
if (article && refactor) {
|
||||||
setCode(article?.content || '');
|
setCode(article.content || '');
|
||||||
setName(article?.name || '');
|
setName(article.name || '');
|
||||||
setTags(article?.tags || []);
|
setTags(article.tags || []);
|
||||||
}
|
}
|
||||||
}, [article]);
|
}, [article]);
|
||||||
|
|
||||||
|
// ==========================
|
||||||
|
// Рендер
|
||||||
|
// ==========================
|
||||||
return (
|
return (
|
||||||
<div className="h-screen grid grid-rows-[60px,1fr]">
|
<div className="h-screen grid grid-rows-[60px,1fr]">
|
||||||
{activeEditor ? (
|
{activeEditor ? (
|
||||||
<Header
|
<Header backClick={() => setActiveEditor(false)} />
|
||||||
backClick={() => {
|
|
||||||
setActiveEditor(false);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
) : (
|
) : (
|
||||||
<Header
|
<Header backClick={() => navigate(back ?? '/home/articles')} />
|
||||||
backClick={() => navigate(back ? back : '/home/articles')}
|
|
||||||
/>
|
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{activeEditor ? (
|
{activeEditor ? (
|
||||||
@@ -113,6 +130,8 @@ const ArticleEditor = () => {
|
|||||||
? `Редактирование статьи: \"${article?.name}\"`
|
? `Редактирование статьи: \"${article?.name}\"`
|
||||||
: 'Создание статьи'}
|
: 'Создание статьи'}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Кнопки действий */}
|
||||||
<div>
|
<div>
|
||||||
{refactor ? (
|
{refactor ? (
|
||||||
<div className="flex gap-[20px]">
|
<div className="flex gap-[20px]">
|
||||||
@@ -129,16 +148,16 @@ const ArticleEditor = () => {
|
|||||||
}}
|
}}
|
||||||
text="Обновить"
|
text="Обновить"
|
||||||
className="mt-[20px]"
|
className="mt-[20px]"
|
||||||
disabled={statusUpdate == 'loading'}
|
disabled={statusUpdate === 'loading'}
|
||||||
/>
|
/>
|
||||||
<ReverseButton
|
<ReverseButton
|
||||||
onClick={() => {
|
onClick={() =>
|
||||||
dispatch(deleteArticle(articleId));
|
dispatch(deleteArticle(articleId))
|
||||||
}}
|
}
|
||||||
color="error"
|
color="error"
|
||||||
text="Удалить"
|
text="Удалить"
|
||||||
className="mt-[20px]"
|
className="mt-[20px]"
|
||||||
disabled={statusDelete == 'loading'}
|
disabled={statusDelete === 'loading'}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
@@ -154,11 +173,12 @@ const ArticleEditor = () => {
|
|||||||
}}
|
}}
|
||||||
text="Опубликовать"
|
text="Опубликовать"
|
||||||
className="mt-[20px]"
|
className="mt-[20px]"
|
||||||
disabled={statusCreate == 'loading'}
|
disabled={statusCreate === 'loading'}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Название */}
|
||||||
<Input
|
<Input
|
||||||
defaultState={name}
|
defaultState={name}
|
||||||
name="articleName"
|
name="articleName"
|
||||||
@@ -166,13 +186,11 @@ const ArticleEditor = () => {
|
|||||||
className="mt-[20px] max-w-[600px]"
|
className="mt-[20px] max-w-[600px]"
|
||||||
type="text"
|
type="text"
|
||||||
label="Название"
|
label="Название"
|
||||||
onChange={(v) => {
|
onChange={setName}
|
||||||
setName(v);
|
|
||||||
}}
|
|
||||||
placeholder="Новая статья"
|
placeholder="Новая статья"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Блок для тегов */}
|
{/* Теги */}
|
||||||
<div className="mt-[20px] max-w-[600px]">
|
<div className="mt-[20px] max-w-[600px]">
|
||||||
<div className="grid grid-cols-[1fr,140px] items-end gap-2">
|
<div className="grid grid-cols-[1fr,140px] items-end gap-2">
|
||||||
<Input
|
<Input
|
||||||
@@ -181,14 +199,11 @@ const ArticleEditor = () => {
|
|||||||
className="mt-[20px] max-w-[600px]"
|
className="mt-[20px] max-w-[600px]"
|
||||||
type="text"
|
type="text"
|
||||||
label="Теги"
|
label="Теги"
|
||||||
onChange={(v) => {
|
onChange={setTagInput}
|
||||||
setTagInput(v);
|
|
||||||
}}
|
|
||||||
defaultState={tagInput}
|
defaultState={tagInput}
|
||||||
placeholder="arrays"
|
placeholder="arrays"
|
||||||
onKeyDown={(e) => {
|
onKeyDown={(e) => {
|
||||||
console.log(e.key);
|
if (e.key === 'Enter') addTag();
|
||||||
if (e.key == 'Enter') addTag();
|
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<PrimaryButton
|
<PrimaryButton
|
||||||
@@ -215,6 +230,7 @@ const ArticleEditor = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Просмотр и переход в редактор */}
|
||||||
<PrimaryButton
|
<PrimaryButton
|
||||||
onClick={() => setActiveEditor(true)}
|
onClick={() => setActiveEditor(true)}
|
||||||
text="Редактировать текст"
|
text="Редактировать текст"
|
||||||
@@ -222,7 +238,7 @@ const ArticleEditor = () => {
|
|||||||
/>
|
/>
|
||||||
<MarkdownPreview
|
<MarkdownPreview
|
||||||
content={code}
|
content={code}
|
||||||
className="bg-transparent border-liquid-lighter border-[3px] rounder-[20px] mt-[20px]"
|
className="bg-transparent border-liquid-lighter border-[3px] rounded-[20px] mt-[20px]"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -11,12 +11,13 @@ import Articles from '../views/home/articles/Articles';
|
|||||||
import Groups from '../views/home/groups/Groups';
|
import Groups from '../views/home/groups/Groups';
|
||||||
import Contests from '../views/home/contests/Contests';
|
import Contests from '../views/home/contests/Contests';
|
||||||
import { PrimaryButton } from '../components/button/PrimaryButton';
|
import { PrimaryButton } from '../components/button/PrimaryButton';
|
||||||
import Group from '../views/home/groups/Group';
|
import Group from '../views/home/group/Group';
|
||||||
import Contest from '../views/home/contest/Contest';
|
import Contest from '../views/home/contest/Contest';
|
||||||
import Account from '../views/home/account/Account';
|
import Account from '../views/home/account/Account';
|
||||||
import ProtectedRoute from '../components/router/ProtectedRoute';
|
import ProtectedRoute from '../components/router/ProtectedRoute';
|
||||||
import { MissionsRightPanel } from '../views/home/rightpanel/Missions';
|
import { MissionsRightPanel } from '../views/home/rightpanel/Missions';
|
||||||
import { ArticlesRightPanel } from '../views/home/rightpanel/Articles';
|
import { ArticlesRightPanel } from '../views/home/rightpanel/Articles';
|
||||||
|
import { GroupRightPanel } from '../views/home/rightpanel/Group';
|
||||||
|
|
||||||
const Home = () => {
|
const Home = () => {
|
||||||
const name = useAppSelector((state) => state.auth.username);
|
const name = useAppSelector((state) => state.auth.username);
|
||||||
@@ -78,6 +79,10 @@ const Home = () => {
|
|||||||
<Routes>
|
<Routes>
|
||||||
<Route path="articles/*" element={<ArticlesRightPanel />} />
|
<Route path="articles/*" element={<ArticlesRightPanel />} />
|
||||||
<Route path="missions/*" element={<MissionsRightPanel />} />
|
<Route path="missions/*" element={<MissionsRightPanel />} />
|
||||||
|
<Route
|
||||||
|
path="group/:groupId"
|
||||||
|
element={<GroupRightPanel />}
|
||||||
|
/>
|
||||||
</Routes>
|
</Routes>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
import { createSlice, createAsyncThunk, PayloadAction } from '@reduxjs/toolkit';
|
import { createSlice, createAsyncThunk, PayloadAction } from '@reduxjs/toolkit';
|
||||||
import axios from '../../axios';
|
import axios from '../../axios';
|
||||||
|
|
||||||
// ─── Типы ────────────────────────────────────────────
|
// =====================
|
||||||
|
// Типы
|
||||||
|
// =====================
|
||||||
|
|
||||||
type Status = 'idle' | 'loading' | 'successful' | 'failed';
|
type Status = 'idle' | 'loading' | 'successful' | 'failed';
|
||||||
|
|
||||||
@@ -15,39 +17,145 @@ export interface Article {
|
|||||||
updatedAt: string;
|
updatedAt: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ArticlesState {
|
interface ArticlesResponse {
|
||||||
articles: Article[];
|
|
||||||
currentArticle?: Article;
|
|
||||||
hasNextPage: boolean;
|
hasNextPage: boolean;
|
||||||
statuses: {
|
articles: Article[];
|
||||||
create: Status;
|
}
|
||||||
update: Status;
|
|
||||||
delete: Status;
|
// =====================
|
||||||
fetchAll: Status;
|
// Состояние
|
||||||
fetchById: Status;
|
// =====================
|
||||||
|
|
||||||
|
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 = {
|
const initialState: ArticlesState = {
|
||||||
articles: [],
|
fetchArticles: {
|
||||||
currentArticle: undefined,
|
articles: [],
|
||||||
hasNextPage: false,
|
hasNextPage: false,
|
||||||
statuses: {
|
status: 'idle',
|
||||||
create: 'idle',
|
error: undefined,
|
||||||
update: 'idle',
|
},
|
||||||
delete: 'idle',
|
fetchArticleById: {
|
||||||
fetchAll: 'idle',
|
article: undefined,
|
||||||
fetchById: 'idle',
|
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(
|
export const createArticle = createAsyncThunk(
|
||||||
'articles/createArticle',
|
'articles/create',
|
||||||
async (
|
async (
|
||||||
{
|
{
|
||||||
name,
|
name,
|
||||||
@@ -57,12 +165,12 @@ export const createArticle = createAsyncThunk(
|
|||||||
{ rejectWithValue },
|
{ rejectWithValue },
|
||||||
) => {
|
) => {
|
||||||
try {
|
try {
|
||||||
const response = await axios.post('/articles', {
|
const response = await axios.post<Article>('/articles', {
|
||||||
name,
|
name,
|
||||||
content,
|
content,
|
||||||
tags,
|
tags,
|
||||||
});
|
});
|
||||||
return response.data as Article;
|
return response.data;
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
return rejectWithValue(
|
return rejectWithValue(
|
||||||
err.response?.data?.message || 'Ошибка при создании статьи',
|
err.response?.data?.message || 'Ошибка при создании статьи',
|
||||||
@@ -71,9 +179,9 @@ export const createArticle = createAsyncThunk(
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
// PUT /articles/{articleId}
|
// Обновление статьи
|
||||||
export const updateArticle = createAsyncThunk(
|
export const updateArticle = createAsyncThunk(
|
||||||
'articles/updateArticle',
|
'articles/update',
|
||||||
async (
|
async (
|
||||||
{
|
{
|
||||||
articleId,
|
articleId,
|
||||||
@@ -84,12 +192,15 @@ export const updateArticle = createAsyncThunk(
|
|||||||
{ rejectWithValue },
|
{ rejectWithValue },
|
||||||
) => {
|
) => {
|
||||||
try {
|
try {
|
||||||
const response = await axios.put(`/articles/${articleId}`, {
|
const response = await axios.put<Article>(
|
||||||
name,
|
`/articles/${articleId}`,
|
||||||
content,
|
{
|
||||||
tags,
|
name,
|
||||||
});
|
content,
|
||||||
return response.data as Article;
|
tags,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
return response.data;
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
return rejectWithValue(
|
return rejectWithValue(
|
||||||
err.response?.data?.message || 'Ошибка при обновлении статьи',
|
err.response?.data?.message || 'Ошибка при обновлении статьи',
|
||||||
@@ -98,9 +209,9 @@ export const updateArticle = createAsyncThunk(
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
// DELETE /articles/{articleId}
|
// Удаление статьи
|
||||||
export const deleteArticle = createAsyncThunk(
|
export const deleteArticle = createAsyncThunk(
|
||||||
'articles/deleteArticle',
|
'articles/delete',
|
||||||
async (articleId: number, { rejectWithValue }) => {
|
async (articleId: number, { rejectWithValue }) => {
|
||||||
try {
|
try {
|
||||||
await axios.delete(`/articles/${articleId}`);
|
await axios.delete(`/articles/${articleId}`);
|
||||||
@@ -113,186 +224,136 @@ export const deleteArticle = createAsyncThunk(
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
// GET /articles
|
// =====================
|
||||||
export const fetchArticles = createAsyncThunk(
|
// Slice
|
||||||
'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({
|
const articlesSlice = createSlice({
|
||||||
name: 'articles',
|
name: 'articles',
|
||||||
initialState,
|
initialState,
|
||||||
reducers: {
|
reducers: {
|
||||||
clearCurrentArticle: (state) => {
|
|
||||||
state.currentArticle = undefined;
|
|
||||||
},
|
|
||||||
setArticlesStatus: (
|
setArticlesStatus: (
|
||||||
state,
|
state,
|
||||||
action: PayloadAction<{
|
action: PayloadAction<{ key: keyof ArticlesState; status: Status }>,
|
||||||
key: keyof ArticlesState['statuses'];
|
|
||||||
status: Status;
|
|
||||||
}>,
|
|
||||||
) => {
|
) => {
|
||||||
const { key, status } = action.payload;
|
const { key, status } = action.payload;
|
||||||
state.statuses[key] = status;
|
if (state[key]) {
|
||||||
|
(state[key] as any).status = status;
|
||||||
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
extraReducers: (builder) => {
|
extraReducers: (builder) => {
|
||||||
// ─── CREATE ARTICLE ───
|
// fetchArticles
|
||||||
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) => {
|
builder.addCase(fetchArticles.pending, (state) => {
|
||||||
state.statuses.fetchAll = 'loading';
|
state.fetchArticles.status = 'loading';
|
||||||
state.error = null;
|
state.fetchArticles.error = undefined;
|
||||||
});
|
});
|
||||||
builder.addCase(
|
builder.addCase(
|
||||||
fetchArticles.fulfilled,
|
fetchArticles.fulfilled,
|
||||||
(
|
(state, action: PayloadAction<ArticlesResponse>) => {
|
||||||
state,
|
state.fetchArticles.status = 'successful';
|
||||||
action: PayloadAction<{
|
state.fetchArticles.articles = action.payload.articles;
|
||||||
hasNextPage: boolean;
|
state.fetchArticles.hasNextPage = action.payload.hasNextPage;
|
||||||
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;
|
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
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) => {
|
builder.addCase(fetchArticleById.pending, (state) => {
|
||||||
state.statuses.fetchById = 'loading';
|
state.fetchArticleById.status = 'loading';
|
||||||
state.error = null;
|
state.fetchArticleById.error = undefined;
|
||||||
});
|
});
|
||||||
builder.addCase(
|
builder.addCase(
|
||||||
fetchArticleById.fulfilled,
|
fetchArticleById.fulfilled,
|
||||||
(state, action: PayloadAction<Article>) => {
|
(state, action: PayloadAction<Article>) => {
|
||||||
state.statuses.fetchById = 'successful';
|
state.fetchArticleById.status = 'successful';
|
||||||
state.currentArticle = action.payload;
|
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(
|
builder.addCase(
|
||||||
fetchArticleById.rejected,
|
createArticle.fulfilled,
|
||||||
(state, action: PayloadAction<any>) => {
|
(state, action: PayloadAction<Article>) => {
|
||||||
state.statuses.fetchById = 'failed';
|
state.createArticle.status = 'successful';
|
||||||
state.error = action.payload;
|
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;
|
export const articlesReducer = articlesSlice.reducer;
|
||||||
|
|||||||
@@ -3,8 +3,7 @@ import { useAppDispatch, useAppSelector } from '../../../../redux/hooks';
|
|||||||
import { setMenuActiveProfilePage } from '../../../../redux/slices/store';
|
import { setMenuActiveProfilePage } from '../../../../redux/slices/store';
|
||||||
import { cn } from '../../../../lib/cn';
|
import { cn } from '../../../../lib/cn';
|
||||||
import { ChevroneDown, Edit } from '../../../../assets/icons/groups';
|
import { ChevroneDown, Edit } from '../../../../assets/icons/groups';
|
||||||
import { fetchArticles } from '../../../../redux/slices/articles';
|
import { fetchMyArticles } from '../../../../redux/slices/articles';
|
||||||
|
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
|
||||||
export interface ArticleItemProps {
|
export interface ArticleItemProps {
|
||||||
@@ -13,21 +12,21 @@ export interface ArticleItemProps {
|
|||||||
tags: string[];
|
tags: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
const ArticleItem: React.FC<ArticleItemProps> = ({ id, name, tags }) => {
|
const ArticleItem: FC<ArticleItemProps> = ({ id, name, tags }) => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
'w-full relative rounded-[10px] text-liquid-white mb-[20px]',
|
'w-full relative rounded-[10px] text-liquid-white mb-[20px]',
|
||||||
// type == "first" ? "bg-liquid-lighter" : "bg-liquid-background",
|
'gap-[20px] px-[20px] py-[10px] box-border',
|
||||||
'gap-[20px] px-[20px] py-[10px] box-border ',
|
|
||||||
'border-b-[1px] border-b-liquid-lighter cursor-pointer hover:bg-liquid-lighter transition-all duration-300',
|
'border-b-[1px] border-b-liquid-lighter cursor-pointer hover:bg-liquid-lighter transition-all duration-300',
|
||||||
)}
|
)}
|
||||||
onClick={() => {
|
onClick={() =>
|
||||||
navigate(`/article/${id}?back=/home/account/articles`);
|
navigate(`/article/${id}?back=/home/account/articles`)
|
||||||
}}
|
}
|
||||||
>
|
>
|
||||||
<div className="h-[23px] flex ">
|
<div className="h-[23px] flex">
|
||||||
<div className="text-[18px] font-bold w-[60px] mr-[20px] flex items-center">
|
<div className="text-[18px] font-bold w-[60px] mr-[20px] flex items-center">
|
||||||
#{id}
|
#{id}
|
||||||
</div>
|
</div>
|
||||||
@@ -35,13 +34,14 @@ const ArticleItem: React.FC<ArticleItemProps> = ({ id, name, tags }) => {
|
|||||||
{name}
|
{name}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="text-[14px] flex text-liquid-light gap-[10px] mt-[10px]">
|
<div className="text-[14px] flex text-liquid-light gap-[10px] mt-[10px]">
|
||||||
{tags.map((v, i) => (
|
{tags.map((v, i) => (
|
||||||
<div
|
<div
|
||||||
key={i}
|
key={i}
|
||||||
className={cn(
|
className={cn(
|
||||||
'rounded-full px-[16px] py-[8px] bg-liquid-lighter',
|
'rounded-full px-[16px] py-[8px] bg-liquid-lighter',
|
||||||
v == 'Sertificated' && 'text-liquid-green',
|
v === 'Sertificated' && 'text-liquid-green',
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{v}
|
{v}
|
||||||
@@ -50,8 +50,9 @@ const ArticleItem: React.FC<ArticleItemProps> = ({ id, name, tags }) => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<img
|
<img
|
||||||
className=" absolute right-[10px] top-[10px] h-[24px] w-[24px] hover:bg-liquid-light rounded-[5px] transition-all duration-300"
|
className="absolute right-[10px] top-[10px] h-[24px] w-[24px] hover:bg-liquid-light rounded-[5px] transition-all duration-300"
|
||||||
src={Edit}
|
src={Edit}
|
||||||
|
alt="Редактировать"
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
navigate(
|
navigate(
|
||||||
@@ -69,49 +70,79 @@ interface ArticlesBlockProps {
|
|||||||
|
|
||||||
const ArticlesBlock: FC<ArticlesBlockProps> = ({ className = '' }) => {
|
const ArticlesBlock: FC<ArticlesBlockProps> = ({ className = '' }) => {
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const articles = useAppSelector((state) => state.articles.articles);
|
|
||||||
const [active, setActive] = useState<boolean>(true);
|
const [active, setActive] = useState<boolean>(true);
|
||||||
|
|
||||||
|
// ✅ Берём только "мои статьи"
|
||||||
|
const articles = useAppSelector(
|
||||||
|
(state) => state.articles.fetchMyArticles.articles,
|
||||||
|
);
|
||||||
|
const status = useAppSelector(
|
||||||
|
(state) => state.articles.fetchMyArticles.status,
|
||||||
|
);
|
||||||
|
const error = useAppSelector(
|
||||||
|
(state) => state.articles.fetchMyArticles.error,
|
||||||
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
dispatch(setMenuActiveProfilePage('articles'));
|
dispatch(setMenuActiveProfilePage('articles'));
|
||||||
dispatch(fetchArticles({}));
|
dispatch(fetchMyArticles());
|
||||||
}, []);
|
}, [dispatch]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="h-full w-full relative p-[20px]">
|
<div className="h-full w-full relative p-[20px]">
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
' border-b-[1px] border-b-liquid-lighter rounded-[10px]',
|
'border-b-[1px] border-b-liquid-lighter rounded-[10px]',
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
|
{/* Заголовок */}
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
' h-[40px] text-[24px] font-bold flex gap-[10px] border-b-[1px] border-b-transparent items-center cursor-pointer transition-all duration-300',
|
'h-[40px] text-[24px] font-bold flex gap-[10px] border-b-[1px] border-b-transparent items-center cursor-pointer transition-all duration-300',
|
||||||
active && ' border-b-liquid-lighter',
|
active && 'border-b-liquid-lighter',
|
||||||
)}
|
)}
|
||||||
onClick={() => {
|
onClick={() => setActive(!active)}
|
||||||
setActive(!active);
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<span>Мои статьи</span>
|
<span>Мои статьи</span>
|
||||||
<img
|
<img
|
||||||
src={ChevroneDown}
|
src={ChevroneDown}
|
||||||
|
alt="toggle"
|
||||||
className={cn(
|
className={cn(
|
||||||
'transition-all duration-300',
|
'transition-all duration-300',
|
||||||
active && 'rotate-180',
|
active && 'rotate-180',
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Контент */}
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
' grid grid-flow-row grid-rows-[0fr] opacity-0 transition-all duration-300',
|
'grid grid-flow-row grid-rows-[0fr] opacity-0 transition-all duration-300',
|
||||||
active && 'grid-rows-[1fr] opacity-100',
|
active && 'grid-rows-[1fr] opacity-100',
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div className="overflow-hidden">
|
<div className="overflow-hidden">
|
||||||
<div className="grid gap-[20px] pt-[20px] pb-[20px] box-border">
|
<div className="grid gap-[20px] pt-[20px] pb-[20px] box-border">
|
||||||
{articles.map((v, i) => (
|
{status === 'loading' && (
|
||||||
<ArticleItem key={i} {...v} />
|
<div className="text-liquid-light">
|
||||||
|
Загрузка статей...
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{status === 'failed' && (
|
||||||
|
<div className="text-liquid-red">
|
||||||
|
Ошибка:{' '}
|
||||||
|
{error || 'Не удалось загрузить статьи'}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{status === 'successful' &&
|
||||||
|
articles.length === 0 && (
|
||||||
|
<div className="text-liquid-light">
|
||||||
|
У вас пока нет статей
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{articles.map((v) => (
|
||||||
|
<ArticleItem key={v.id} {...v} />
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -7,51 +7,84 @@ import { useNavigate } from 'react-router-dom';
|
|||||||
import { fetchArticles } from '../../../redux/slices/articles';
|
import { fetchArticles } from '../../../redux/slices/articles';
|
||||||
import Filters from './Filter';
|
import Filters from './Filter';
|
||||||
|
|
||||||
export interface Article {
|
|
||||||
id: number;
|
|
||||||
name: string;
|
|
||||||
tags: string[];
|
|
||||||
}
|
|
||||||
|
|
||||||
const Articles = () => {
|
const Articles = () => {
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
const articles = useAppSelector((state) => state.articles.articles);
|
// ✅ Берём данные из нового состояния
|
||||||
const status = useAppSelector((state) => state.articles.statuses.fetchAll);
|
const articles = useAppSelector(
|
||||||
|
(state) => state.articles.fetchArticles.articles,
|
||||||
|
);
|
||||||
|
const status = useAppSelector(
|
||||||
|
(state) => state.articles.fetchArticles.status,
|
||||||
|
);
|
||||||
|
const error = useAppSelector((state) => state.articles.fetchArticles.error);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
dispatch(setMenuActivePage('articles'));
|
dispatch(setMenuActivePage('articles'));
|
||||||
dispatch(fetchArticles({}));
|
dispatch(fetchArticles({}));
|
||||||
}, []);
|
}, [dispatch]);
|
||||||
|
|
||||||
if (status == 'loading') return <div>Загрузка...</div>;
|
// ========================
|
||||||
|
// Состояния загрузки / ошибки
|
||||||
|
// ========================
|
||||||
|
if (status === 'loading') {
|
||||||
|
return (
|
||||||
|
<div className="h-full w-full flex items-center justify-center text-liquid-light text-[18px]">
|
||||||
|
Загрузка статей...
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (status === 'failed') {
|
||||||
|
return (
|
||||||
|
<div className="h-full w-full flex flex-col items-center justify-center text-liquid-red text-[18px]">
|
||||||
|
Ошибка при загрузке статей
|
||||||
|
{error && (
|
||||||
|
<div className="text-liquid-light text-[14px] mt-2">
|
||||||
|
{error}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========================
|
||||||
|
// Основной контент
|
||||||
|
// ========================
|
||||||
return (
|
return (
|
||||||
<div className=" h-full w-full box-border p-[20px] pt-[20px]">
|
<div className="h-full w-full box-border p-[20px]">
|
||||||
<div className="h-full box-border">
|
<div className="h-full box-border">
|
||||||
|
{/* Заголовок */}
|
||||||
<div className="relative flex items-center mb-[20px]">
|
<div className="relative flex items-center mb-[20px]">
|
||||||
<div className="h-[50px] text-[40px] font-bold text-liquid-white flex items-center">
|
<div className="h-[50px] text-[40px] font-bold text-liquid-white flex items-center">
|
||||||
Статьи
|
Статьи
|
||||||
</div>
|
</div>
|
||||||
<SecondaryButton
|
<SecondaryButton
|
||||||
onClick={() => {
|
onClick={() => navigate('/article/create')}
|
||||||
navigate('/article/create');
|
|
||||||
}}
|
|
||||||
text="Создать статью"
|
text="Создать статью"
|
||||||
className="absolute right-0"
|
className="absolute right-0"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Фильтры */}
|
||||||
<Filters />
|
<Filters />
|
||||||
|
|
||||||
<div>
|
{/* Список статей */}
|
||||||
{articles.map((v, i) => (
|
<div className="mt-[20px]">
|
||||||
<ArticleItem key={i} {...v} />
|
{articles.length === 0 ? (
|
||||||
))}
|
<div className="text-liquid-light text-[16px]">
|
||||||
|
Пока нет статей
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
articles.map((v) => <ArticleItem key={v.id} {...v} />)
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>pages</div>
|
{/* Пагинация (пока заглушка) */}
|
||||||
|
<div className="mt-[20px] text-liquid-light text-[14px]">
|
||||||
|
pages
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
60
src/views/home/rightpanel/Group.tsx
Normal file
60
src/views/home/rightpanel/Group.tsx
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
import { FC } from 'react';
|
||||||
|
|
||||||
|
export const GroupRightPanel: FC = () => {
|
||||||
|
const items = [
|
||||||
|
{
|
||||||
|
name: 'Игнат Герасименко',
|
||||||
|
role: 'Администратор',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Алиса Макаренко',
|
||||||
|
role: 'Модератор',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Федор Картман',
|
||||||
|
role: 'Модератор',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Карина Механаджанович',
|
||||||
|
role: 'Участник',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Михаил Ангрский',
|
||||||
|
role: 'Участник',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'newuser',
|
||||||
|
role: 'Участник (Вы)',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
return (
|
||||||
|
<div className="h-screen w-full overflow-y-scroll thin-dark-scrollbar p-[20px] gap-[5px] flex flex-col">
|
||||||
|
<div className="text-liquid-white font-bold text-[18px]">
|
||||||
|
Пользователи
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{items.map((v, i) => {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{
|
||||||
|
<div className="text-liquid-light text-[16px] grid grid-cols-[40px,1fr] gap-[10px] items-center cursor-pointer hover:bg-liquid-lighter transition-all duration-300 rounded-[10px] p-[5px]">
|
||||||
|
<div className="h-[40px] w-[40px] rounded-[10px] bg-[#D9D9D9]"></div>
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<div className="text-liquid-white font-bold text-[16px] leading-5">
|
||||||
|
{v.name}
|
||||||
|
</div>
|
||||||
|
<div className="text-liquid-light font-normal text-[16px] leading-5">
|
||||||
|
{v.role}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
{i + 1 != items.length && (
|
||||||
|
<div className="h-[1px] w-full bg-liquid-lighter"></div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user