articles view
This commit is contained in:
@@ -7,6 +7,7 @@ import { Route, Routes } from 'react-router-dom';
|
|||||||
import Home from './pages/Home';
|
import Home from './pages/Home';
|
||||||
import Mission from './pages/Mission';
|
import Mission from './pages/Mission';
|
||||||
import ArticleEditor from './pages/ArticleEditor';
|
import ArticleEditor from './pages/ArticleEditor';
|
||||||
|
import Article from './pages/Article';
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
return (
|
return (
|
||||||
@@ -19,6 +20,7 @@ function App() {
|
|||||||
path="/article/create/*"
|
path="/article/create/*"
|
||||||
element={<ArticleEditor />}
|
element={<ArticleEditor />}
|
||||||
/>
|
/>
|
||||||
|
<Route path="/article/:articleId" element={<Article />} />
|
||||||
<Route path="*" element={<Home />} />
|
<Route path="*" element={<Home />} />
|
||||||
</Routes>
|
</Routes>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
67
src/pages/Article.tsx
Normal file
67
src/pages/Article.tsx
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
import { useParams, Navigate } from 'react-router-dom';
|
||||||
|
import { useAppDispatch, useAppSelector } from '../redux/hooks';
|
||||||
|
import Header from '../views/article/Header';
|
||||||
|
import { useEffect } from 'react';
|
||||||
|
import { fetchArticleById } from '../redux/slices/articles';
|
||||||
|
import MarkdownPreview from '../views/articleeditor/MarckDownPreview';
|
||||||
|
|
||||||
|
const Article = () => {
|
||||||
|
// Получаем параметры из URL
|
||||||
|
const { articleId } = useParams<{ articleId: string }>();
|
||||||
|
const articleIdNumber = Number(articleId);
|
||||||
|
if (!articleId || isNaN(articleIdNumber)) {
|
||||||
|
return <Navigate to="/home" replace />;
|
||||||
|
}
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
|
const article = useAppSelector((state) => state.articles.currentArticle);
|
||||||
|
const status = useAppSelector((state) => state.articles.statuses.fetchById);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
dispatch(fetchArticleById(articleIdNumber));
|
||||||
|
}, [articleIdNumber]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="grid grid-cols-[1fr,250px] divide-x-[1px] divide-liquid-lighter">
|
||||||
|
<div className="h-screen grid grid-rows-[60px,1fr] relative">
|
||||||
|
<div className="">
|
||||||
|
<Header articleId={articleIdNumber} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{status == 'loading' || !article ? (
|
||||||
|
<div>Загрузка...</div>
|
||||||
|
) : (
|
||||||
|
<div className="h-full min-h-0 gap-[20px] overflow-y-scroll flex flex-col medium-scrollbar ">
|
||||||
|
<div>
|
||||||
|
<div className="text-[40px] font-bold leading-[50px] text-liquid-white">
|
||||||
|
{article.name}
|
||||||
|
</div>
|
||||||
|
<div className="text-[18px] font-bold leading-[23px] text-liquid-light">
|
||||||
|
#{article.id}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{article.tags.length && (
|
||||||
|
<div className="flex gap-[10px]">
|
||||||
|
{article.tags.map((v, i) => (
|
||||||
|
<div
|
||||||
|
key={i}
|
||||||
|
className="py-[8px] px-[16px] rounded-[20px] text-liquid-light bg-liquid-lighter text-[14px] font-normal w-fit"
|
||||||
|
>
|
||||||
|
{v}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<MarkdownPreview
|
||||||
|
content={article!.content}
|
||||||
|
className="bg-transparent"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className=""></div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Article;
|
||||||
@@ -1,19 +1,25 @@
|
|||||||
import { Route, Routes, useNavigate } from 'react-router-dom';
|
import { Route, Routes, useNavigate } from 'react-router-dom';
|
||||||
import Header from '../views/articleeditor/Header';
|
import Header from '../views/articleeditor/Header';
|
||||||
import MarkdownEditor from '../views/articleeditor/Editor';
|
import MarkdownEditor from '../views/articleeditor/Editor';
|
||||||
import { useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { PrimaryButton } from '../components/button/PrimaryButton';
|
import { PrimaryButton } from '../components/button/PrimaryButton';
|
||||||
import MarkdownPreview from '../views/articleeditor/MarckDownPreview';
|
import MarkdownPreview from '../views/articleeditor/MarckDownPreview';
|
||||||
import { Input } from '../components/input/Input';
|
import { Input } from '../components/input/Input';
|
||||||
|
import { useAppDispatch, useAppSelector } from '../redux/hooks';
|
||||||
|
import { createArticle, setArticlesStatus } from '../redux/slices/articles';
|
||||||
|
|
||||||
const ArticleEditor = () => {
|
const ArticleEditor = () => {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
|
|
||||||
const [code, setCode] = useState<string>('');
|
const [code, setCode] = useState<string>('');
|
||||||
const [name, setName] = useState<string>('');
|
const [name, setName] = useState<string>('');
|
||||||
const navigate = useNavigate();
|
|
||||||
|
|
||||||
const [tagInput, setTagInput] = useState<string>('');
|
const [tagInput, setTagInput] = useState<string>('');
|
||||||
const [tags, setTags] = useState<string[]>([]);
|
const [tags, setTags] = useState<string[]>([]);
|
||||||
|
|
||||||
|
const status = useAppSelector((state) => state.articles.statuses.create);
|
||||||
|
|
||||||
const addTag = () => {
|
const addTag = () => {
|
||||||
const newTag = tagInput.trim();
|
const newTag = tagInput.trim();
|
||||||
if (newTag && !tags.includes(newTag)) {
|
if (newTag && !tags.includes(newTag)) {
|
||||||
@@ -26,6 +32,13 @@ const ArticleEditor = () => {
|
|||||||
setTags(tags.filter((tag) => tag !== tagToRemove));
|
setTags(tags.filter((tag) => tag !== tagToRemove));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (status == 'successful') {
|
||||||
|
dispatch(setArticlesStatus({ key: 'create', status: 'idle' }));
|
||||||
|
navigate('/home/articles');
|
||||||
|
}
|
||||||
|
}, [status]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="h-screen grid grid-rows-[60px,1fr]">
|
<div className="h-screen grid grid-rows-[60px,1fr]">
|
||||||
<Routes>
|
<Routes>
|
||||||
@@ -39,7 +52,12 @@ const ArticleEditor = () => {
|
|||||||
<Routes>
|
<Routes>
|
||||||
<Route
|
<Route
|
||||||
path="editor"
|
path="editor"
|
||||||
element={<MarkdownEditor onChange={setCode} />}
|
element={
|
||||||
|
<MarkdownEditor
|
||||||
|
onChange={setCode}
|
||||||
|
defaultValue={code}
|
||||||
|
/>
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
<Route
|
<Route
|
||||||
path="*"
|
path="*"
|
||||||
@@ -51,17 +69,21 @@ const ArticleEditor = () => {
|
|||||||
|
|
||||||
<PrimaryButton
|
<PrimaryButton
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
console.log({
|
dispatch(
|
||||||
name: name,
|
createArticle({
|
||||||
tags: tags,
|
name,
|
||||||
text: code,
|
tags,
|
||||||
});
|
content: code,
|
||||||
|
}),
|
||||||
|
);
|
||||||
}}
|
}}
|
||||||
text="Опубликовать"
|
text="Опубликовать"
|
||||||
className="mt-[20px]"
|
className="mt-[20px]"
|
||||||
|
disabled={status == 'loading'}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Input
|
<Input
|
||||||
|
defaultState={name}
|
||||||
name="articleName"
|
name="articleName"
|
||||||
autocomplete="articleName"
|
autocomplete="articleName"
|
||||||
className="mt-[20px] max-w-[600px]"
|
className="mt-[20px] max-w-[600px]"
|
||||||
|
|||||||
298
src/redux/slices/articles.ts
Normal file
298
src/redux/slices/articles.ts
Normal file
@@ -0,0 +1,298 @@
|
|||||||
|
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;
|
||||||
@@ -5,6 +5,7 @@ import { missionsReducer } from './slices/missions';
|
|||||||
import { submitReducer } from './slices/submit';
|
import { submitReducer } from './slices/submit';
|
||||||
import { contestsReducer } from './slices/contests';
|
import { contestsReducer } from './slices/contests';
|
||||||
import { groupsReducer } from './slices/groups';
|
import { groupsReducer } from './slices/groups';
|
||||||
|
import { articlesReducer } from './slices/articles';
|
||||||
|
|
||||||
// использование
|
// использование
|
||||||
// import { useAppDispatch, useAppSelector } from '../redux/hooks';
|
// import { useAppDispatch, useAppSelector } from '../redux/hooks';
|
||||||
@@ -23,6 +24,7 @@ export const store = configureStore({
|
|||||||
submin: submitReducer,
|
submin: submitReducer,
|
||||||
contests: contestsReducer,
|
contests: contestsReducer,
|
||||||
groups: groupsReducer,
|
groups: groupsReducer,
|
||||||
|
articles: articlesReducer,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
60
src/views/article/Header.tsx
Normal file
60
src/views/article/Header.tsx
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import {
|
||||||
|
chevroneLeft,
|
||||||
|
chevroneRight,
|
||||||
|
arrowLeft,
|
||||||
|
} from '../../assets/icons/header';
|
||||||
|
import { Logo } from '../../assets/logos';
|
||||||
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
|
||||||
|
interface HeaderProps {
|
||||||
|
articleId: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Header: React.FC<HeaderProps> = ({ articleId }) => {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
return (
|
||||||
|
<header className="w-full h-[60px] flex items-center px-4 gap-[20px]">
|
||||||
|
<img
|
||||||
|
src={Logo}
|
||||||
|
alt="Logo"
|
||||||
|
className="h-[28px] w-auto cursor-pointer"
|
||||||
|
onClick={() => {
|
||||||
|
navigate('/home');
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<img
|
||||||
|
src={arrowLeft}
|
||||||
|
alt="back"
|
||||||
|
className="h-[24px] w-[24px] cursor-pointer"
|
||||||
|
onClick={() => {
|
||||||
|
navigate('/home/articles');
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="flex gap-[10px]">
|
||||||
|
<img
|
||||||
|
src={chevroneLeft}
|
||||||
|
alt="back"
|
||||||
|
className="h-[24px] w-[24px] cursor-pointer"
|
||||||
|
onClick={() => {
|
||||||
|
if (articleId > 1)
|
||||||
|
navigate(`/article/${articleId - 1}`);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<span className="text-[18px] font-bold">#{articleId}</span>
|
||||||
|
<img
|
||||||
|
src={chevroneRight}
|
||||||
|
alt="back"
|
||||||
|
className="h-[24px] w-[24px] cursor-pointer"
|
||||||
|
onClick={() => {
|
||||||
|
navigate(`/article/${articleId + 1}`);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Header;
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { useNavigate } from 'react-router-dom';
|
||||||
import { cn } from '../../../lib/cn';
|
import { cn } from '../../../lib/cn';
|
||||||
|
|
||||||
export interface ArticleItemProps {
|
export interface ArticleItemProps {
|
||||||
@@ -7,14 +8,18 @@ export interface ArticleItemProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const ArticleItem: React.FC<ArticleItemProps> = ({ id, name, tags }) => {
|
const ArticleItem: React.FC<ArticleItemProps> = ({ id, name, tags }) => {
|
||||||
|
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",
|
// 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',
|
'border-b-[1px] border-b-liquid-lighter cursor-pointer hover:bg-liquid-lighter transition-all duration-300',
|
||||||
)}
|
)}
|
||||||
|
onClick={() => {
|
||||||
|
navigate(`/article/${id}`);
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<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">
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
import { useEffect } from 'react';
|
import { useEffect } from 'react';
|
||||||
import { SecondaryButton } from '../../../components/button/SecondaryButton';
|
import { SecondaryButton } from '../../../components/button/SecondaryButton';
|
||||||
import { useAppDispatch } from '../../../redux/hooks';
|
import { useAppDispatch, useAppSelector } from '../../../redux/hooks';
|
||||||
import ArticleItem from './ArticleItem';
|
import ArticleItem from './ArticleItem';
|
||||||
import { setMenuActivePage } from '../../../redux/slices/store';
|
import { setMenuActivePage } from '../../../redux/slices/store';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
import { fetchArticles } from '../../../redux/slices/articles';
|
||||||
|
|
||||||
export interface Article {
|
export interface Article {
|
||||||
id: number;
|
id: number;
|
||||||
@@ -15,123 +16,16 @@ const Articles = () => {
|
|||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
const articles: Article[] = [
|
const articles = useAppSelector((state) => state.articles.articles);
|
||||||
{
|
const status = useAppSelector((state) => state.articles.statuses.fetchAll);
|
||||||
id: 1,
|
|
||||||
name: 'Todo List App',
|
|
||||||
tags: ['Sertificated', 'state', 'list'],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 2,
|
|
||||||
name: 'Search Filter Component',
|
|
||||||
tags: ['filter', 'props', 'hooks'],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 3,
|
|
||||||
name: 'User Card List',
|
|
||||||
tags: ['components', 'props', 'array'],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 4,
|
|
||||||
name: 'Theme Switcher',
|
|
||||||
tags: ['Sertificated', 'theme', 'hooks'],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 2,
|
|
||||||
name: 'Search Filter Component',
|
|
||||||
tags: ['filter', 'props', 'hooks'],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 3,
|
|
||||||
name: 'User Card List',
|
|
||||||
tags: ['components', 'props', 'array'],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 4,
|
|
||||||
name: 'Theme Switcher',
|
|
||||||
tags: ['Sertificated', 'theme', 'hooks'],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 2,
|
|
||||||
name: 'Search Filter Component',
|
|
||||||
tags: ['filter', 'props', 'hooks'],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 3,
|
|
||||||
name: 'User Card List',
|
|
||||||
tags: ['components', 'props', 'array'],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 4,
|
|
||||||
name: 'Theme Switcher',
|
|
||||||
tags: ['Sertificated', 'theme', 'hooks'],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 2,
|
|
||||||
name: 'Search Filter Component',
|
|
||||||
tags: ['filter', 'props', 'hooks'],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 3,
|
|
||||||
name: 'User Card List',
|
|
||||||
tags: ['components', 'props', 'array'],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 4,
|
|
||||||
name: 'Theme Switcher',
|
|
||||||
tags: ['Sertificated', 'theme', 'hooks'],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 2,
|
|
||||||
name: 'Search Filter Component',
|
|
||||||
tags: ['filter', 'props', 'hooks'],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 3,
|
|
||||||
name: 'User Card List',
|
|
||||||
tags: ['components', 'props', 'array'],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 4,
|
|
||||||
name: 'Theme Switcher',
|
|
||||||
tags: ['Sertificated', 'theme', 'hooks'],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 2,
|
|
||||||
name: 'Search Filter Component',
|
|
||||||
tags: ['filter', 'props', 'hooks'],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 3,
|
|
||||||
name: 'User Card List',
|
|
||||||
tags: ['components', 'props', 'array'],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 4,
|
|
||||||
name: 'Theme Switcher',
|
|
||||||
tags: ['Sertificated', 'theme', 'hooks'],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 2,
|
|
||||||
name: 'Search Filter Component',
|
|
||||||
tags: ['filter', 'props', 'hooks'],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 3,
|
|
||||||
name: 'User Card List',
|
|
||||||
tags: ['components', 'props', 'array'],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 4,
|
|
||||||
name: 'Theme Switcher',
|
|
||||||
tags: ['Sertificated', 'theme', 'hooks'],
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
dispatch(setMenuActivePage('articles'));
|
dispatch(setMenuActivePage('articles'));
|
||||||
|
dispatch(fetchArticles({}));
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
if (status == 'loading') return <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] pt-[20px]">
|
||||||
<div className="h-full box-border">
|
<div className="h-full box-border">
|
||||||
|
|||||||
Reference in New Issue
Block a user