diff --git a/src/assets/icons/account/clipboard.svg b/src/assets/icons/account/clipboard.svg
new file mode 100644
index 0000000..fb6e81b
--- /dev/null
+++ b/src/assets/icons/account/clipboard.svg
@@ -0,0 +1,3 @@
+
diff --git a/src/assets/icons/account/cup.svg b/src/assets/icons/account/cup.svg
new file mode 100644
index 0000000..330aeb0
--- /dev/null
+++ b/src/assets/icons/account/cup.svg
@@ -0,0 +1,3 @@
+
diff --git a/src/assets/icons/account/index.ts b/src/assets/icons/account/index.ts
new file mode 100644
index 0000000..5fa91fb
--- /dev/null
+++ b/src/assets/icons/account/index.ts
@@ -0,0 +1,5 @@
+import Clipboard from './clipboard.svg';
+import Cup from './cup.svg';
+import OpenBook from './openbook.svg';
+
+export { Clipboard, Cup, OpenBook };
diff --git a/src/assets/icons/account/openbook.svg b/src/assets/icons/account/openbook.svg
new file mode 100644
index 0000000..db632c4
--- /dev/null
+++ b/src/assets/icons/account/openbook.svg
@@ -0,0 +1,3 @@
+
diff --git a/src/assets/icons/input/edit.svg b/src/assets/icons/input/edit.svg
new file mode 100644
index 0000000..27910a5
--- /dev/null
+++ b/src/assets/icons/input/edit.svg
@@ -0,0 +1,3 @@
+
diff --git a/src/assets/icons/input/index.ts b/src/assets/icons/input/index.ts
index 4888d41..08cfb70 100644
--- a/src/assets/icons/input/index.ts
+++ b/src/assets/icons/input/index.ts
@@ -4,8 +4,10 @@ import googleLogo from './google-logo.svg';
import upload from './upload.svg';
import chevroneDropDownList from './chevron-drop-down.svg';
import checkMark from './check-mark.svg';
+import Edit from './edit.svg';
export {
+ Edit,
eyeClosed,
eyeOpen,
googleLogo,
diff --git a/src/pages/Article.tsx b/src/pages/Article.tsx
index e6e8ad7..44b910a 100644
--- a/src/pages/Article.tsx
+++ b/src/pages/Article.tsx
@@ -4,12 +4,18 @@ import Header from '../views/article/Header';
import { useEffect } from 'react';
import { fetchArticleById } from '../redux/slices/articles';
import MarkdownPreview from '../views/articleeditor/MarckDownPreview';
+import { useQuery } from '../hooks/useQuery';
const Article = () => {
// Получаем параметры из URL
const { articleId } = useParams<{ articleId: string }>();
const articleIdNumber = Number(articleId);
+
+ const query = useQuery();
+ const back = query.get('back') ?? undefined;
+
if (!articleId || isNaN(articleIdNumber)) {
+ if (back) return ;
return ;
}
const dispatch = useAppDispatch();
@@ -24,7 +30,7 @@ const Article = () => {
-
+
{status == 'loading' || !article ? (
diff --git a/src/pages/ArticleEditor.tsx b/src/pages/ArticleEditor.tsx
index 10b99f3..79002b8 100644
--- a/src/pages/ArticleEditor.tsx
+++ b/src/pages/ArticleEditor.tsx
@@ -1,4 +1,4 @@
-import { Route, Routes, useNavigate } from 'react-router-dom';
+import { useNavigate } from 'react-router-dom';
import Header from '../views/articleeditor/Header';
import MarkdownEditor from '../views/articleeditor/Editor';
import { useEffect, useState } from 'react';
@@ -6,19 +6,42 @@ import { PrimaryButton } from '../components/button/PrimaryButton';
import MarkdownPreview from '../views/articleeditor/MarckDownPreview';
import { Input } from '../components/input/Input';
import { useAppDispatch, useAppSelector } from '../redux/hooks';
-import { createArticle, setArticlesStatus } from '../redux/slices/articles';
+import {
+ createArticle,
+ deleteArticle,
+ fetchArticleById,
+ setArticlesStatus,
+ updateArticle,
+} from '../redux/slices/articles';
+import { useQuery } from '../hooks/useQuery';
+import { ReverseButton } from '../components/button/ReverseButton';
const ArticleEditor = () => {
const navigate = useNavigate();
const dispatch = useAppDispatch();
- const [code, setCode] = useState
('');
- const [name, setName] = useState('');
+ const query = useQuery();
+ const back = query.get('back') ?? undefined;
+ const articleId = Number(query.get('articleId') ?? undefined);
+ const article = useAppSelector((state) => state.articles.currentArticle);
+ const refactor = articleId != undefined && !isNaN(articleId);
+ const [code, setCode] = useState(article?.content || '');
+ const [name, setName] = useState(article?.name || '');
const [tagInput, setTagInput] = useState('');
- const [tags, setTags] = useState([]);
+ const [tags, setTags] = useState(article?.tags || []);
- const status = useAppSelector((state) => state.articles.statuses.create);
+ const [activeEditor, setActiveEditor] = useState(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 newTag = tagInput.trim();
@@ -33,40 +56,92 @@ const ArticleEditor = () => {
};
useEffect(() => {
- if (status == 'successful') {
+ if (statusCreate == 'successful') {
dispatch(setArticlesStatus({ key: 'create', status: 'idle' }));
- navigate('/home/articles');
+ navigate(back ? back : '/home/articles');
}
- }, [status]);
+ }, [statusCreate]);
+
+ useEffect(() => {
+ if (statusDelete == 'successful') {
+ dispatch(setArticlesStatus({ key: 'delete', status: 'idle' }));
+ navigate(back ? back : '/home/articles');
+ }
+ }, [statusDelete]);
+
+ useEffect(() => {
+ if (statusUpdate == 'successful') {
+ dispatch(setArticlesStatus({ key: 'update', status: 'idle' }));
+ navigate(back ? back : '/home/articles');
+ }
+ }, [statusUpdate]);
+
+ useEffect(() => {
+ if (articleId) {
+ dispatch(fetchArticleById(articleId));
+ }
+ }, [articleId]);
+
+ useEffect(() => {
+ if (article && refactor) {
+ setCode(article?.content || '');
+ setName(article?.name || '');
+ setTags(article?.tags || []);
+ }
+ }, [article]);
return (
-
- }
+ {activeEditor ? (
+ {
+ setActiveEditor(false);
+ }}
/>
- } />
-
+ ) : (
+
navigate(back ? back : '/home/articles')}
+ />
+ )}
-
-
- }
- />
-
-
- Создание статьи
+ {activeEditor ? (
+
+ ) : (
+
+
+ {refactor
+ ? `Редактирование статьи: \"${article?.name}\"`
+ : 'Создание статьи'}
+
+
+ {refactor ? (
+
+
{
+ dispatch(
+ updateArticle({
+ articleId,
+ name,
+ tags,
+ content: code,
+ }),
+ );
+ }}
+ text="Обновить"
+ className="mt-[20px]"
+ disabled={statusUpdate == 'loading'}
+ />
+ {
+ dispatch(deleteArticle(articleId));
+ }}
+ color="error"
+ text="Удалить"
+ className="mt-[20px]"
+ disabled={statusDelete == 'loading'}
+ />
-
+ ) : (
{
dispatch(
@@ -79,78 +154,78 @@ const ArticleEditor = () => {
}}
text="Опубликовать"
className="mt-[20px]"
- disabled={status == 'loading'}
+ disabled={statusCreate == 'loading'}
/>
+ )}
+
+
{
+ setName(v);
+ }}
+ placeholder="Новая статья"
+ />
+
+ {/* Блок для тегов */}
+
+
{
- setName(v);
+ setTagInput(v);
+ }}
+ defaultState={tagInput}
+ placeholder="arrays"
+ onKeyDown={(e) => {
+ console.log(e.key);
+ if (e.key == 'Enter') addTag();
}}
- placeholder="Новая статья"
/>
-
- {/* Блок для тегов */}
-
-
-
{
- setTagInput(v);
- }}
- defaultState={tagInput}
- placeholder="arrays"
- onKeyDown={(e) => {
- console.log(e.key);
- if (e.key == 'Enter') addTag();
- }}
- />
-
-
-
- {tags.map((tag) => (
-
- {tag}
-
-
- ))}
-
-
-
navigate('editor')}
- text="Редактировать текст"
- className="mt-[20px]"
- />
-
- }
- />
-
+
+ {tags.map((tag) => (
+
+ {tag}
+
+
+ ))}
+
+
+
+
setActiveEditor(true)}
+ text="Редактировать текст"
+ className="mt-[20px]"
+ />
+
+
+ )}
);
};
diff --git a/src/pages/Mission.tsx b/src/pages/Mission.tsx
index 66f1191..b09c25a 100644
--- a/src/pages/Mission.tsx
+++ b/src/pages/Mission.tsx
@@ -22,6 +22,7 @@ const Mission = () => {
const back = query.get('back') ?? undefined;
if (!missionId || isNaN(missionIdNumber)) {
+ if (back) return ;
return ;
}
diff --git a/src/redux/slices/auth.ts b/src/redux/slices/auth.ts
index 50f6523..9b04ff6 100644
--- a/src/redux/slices/auth.ts
+++ b/src/redux/slices/auth.ts
@@ -1,4 +1,4 @@
-import { createSlice, createAsyncThunk, PayloadAction } from '@reduxjs/toolkit';
+import { createSlice, createAsyncThunk } from '@reduxjs/toolkit';
import axios from '../../axios';
// 🔹 Декодирование JWT
diff --git a/src/redux/slices/store.ts b/src/redux/slices/store.ts
index d67bb2b..bd54ba6 100644
--- a/src/redux/slices/store.ts
+++ b/src/redux/slices/store.ts
@@ -4,6 +4,7 @@ import { createSlice, PayloadAction } from '@reduxjs/toolkit';
interface StorState {
menu: {
activePage: string;
+ activeProfilePage: string;
};
}
@@ -11,6 +12,7 @@ interface StorState {
const initialState: StorState = {
menu: {
activePage: '',
+ activeProfilePage: '',
},
};
@@ -22,8 +24,15 @@ const storeSlice = createSlice({
setMenuActivePage: (state, activePage: PayloadAction) => {
state.menu.activePage = activePage.payload;
},
+ setMenuActiveProfilePage: (
+ state,
+ activeProfilePage: PayloadAction,
+ ) => {
+ state.menu.activeProfilePage = activeProfilePage.payload;
+ },
},
});
-export const { setMenuActivePage } = storeSlice.actions;
+export const { setMenuActivePage, setMenuActiveProfilePage } =
+ storeSlice.actions;
export const storeReducer = storeSlice.reducer;
diff --git a/src/views/article/Header.tsx b/src/views/article/Header.tsx
index 93920fe..dad0203 100644
--- a/src/views/article/Header.tsx
+++ b/src/views/article/Header.tsx
@@ -9,9 +9,10 @@ import { useNavigate } from 'react-router-dom';
interface HeaderProps {
articleId: number;
+ back?: string;
}
-const Header: React.FC = ({ articleId }) => {
+const Header: React.FC = ({ articleId, back }) => {
const navigate = useNavigate();
return (
@@ -29,7 +30,7 @@ const Header: React.FC = ({ articleId }) => {
alt="back"
className="h-[24px] w-[24px] cursor-pointer"
onClick={() => {
- navigate('/home/articles');
+ navigate(back ? back : '/home/articles');
}}
/>
@@ -39,8 +40,11 @@ const Header: React.FC = ({ articleId }) => {
alt="back"
className="h-[24px] w-[24px] cursor-pointer"
onClick={() => {
- if (articleId > 1)
- navigate(`/article/${articleId - 1}`);
+ if (articleId <= 1) return;
+
+ if (back)
+ navigate(`/article/${articleId - 1}?back=${back}`);
+ else navigate(`/article/${articleId - 1}`);
}}
/>
#{articleId}
@@ -49,7 +53,9 @@ const Header: React.FC = ({ articleId }) => {
alt="back"
className="h-[24px] w-[24px] cursor-pointer"
onClick={() => {
- navigate(`/article/${articleId + 1}`);
+ if (back)
+ navigate(`/article/${articleId + 1}?back=${back}`);
+ else navigate(`/article/${articleId + 1}`);
}}
/>
diff --git a/src/views/articleeditor/Header.tsx b/src/views/articleeditor/Header.tsx
index 384972f..7109c5d 100644
--- a/src/views/articleeditor/Header.tsx
+++ b/src/views/articleeditor/Header.tsx
@@ -4,10 +4,10 @@ import { Logo } from '../../assets/logos';
import { useNavigate } from 'react-router-dom';
interface HeaderProps {
- backUrl?: string;
+ backClick?: () => void;
}
-const Header: React.FC = ({ backUrl = '/home/articles' }) => {
+const Header: React.FC = ({ backClick }) => {
const navigate = useNavigate();
return (
@@ -25,7 +25,7 @@ const Header: React.FC = ({ backUrl = '/home/articles' }) => {
alt="back"
className="h-[24px] w-[24px] cursor-pointer"
onClick={() => {
- navigate(backUrl);
+ if (backClick) backClick();
}}
/>
diff --git a/src/views/home/account/Account.tsx b/src/views/home/account/Account.tsx
index b508835..7dbf7af 100644
--- a/src/views/home/account/Account.tsx
+++ b/src/views/home/account/Account.tsx
@@ -1,33 +1,47 @@
-import { Route, Routes } from 'react-router-dom';
+import { Navigate, Route, Routes } from 'react-router-dom';
import AccountMenu from './AccoutMenu';
import RightPanel from './RightPanel';
import MissionsBlock from './MissionsBlock';
import ContestsBlock from './ContestsBlock';
import ArticlesBlock from './ArticlesBlock';
+import { useAppDispatch } from '../../../redux/hooks';
+import { useEffect } from 'react';
+import { setMenuActivePage } from '../../../redux/slices/store';
const Account = () => {
+ const dispatch = useAppDispatch();
+
+ useEffect(() => {
+ dispatch(setMenuActivePage('account'));
+ }, []);
+
return (
-
+
-
-
+
+
}
/>
}
/>
}
/>
- } />
+
+ }
+ />
diff --git a/src/views/home/account/AccoutMenu.tsx b/src/views/home/account/AccoutMenu.tsx
index 204bf1b..f8a51b1 100644
--- a/src/views/home/account/AccoutMenu.tsx
+++ b/src/views/home/account/AccoutMenu.tsx
@@ -1,5 +1,94 @@
+import { Openbook, Cup, Clipboard } from '../../../assets/icons/menu';
+
+import React from 'react';
+import { Link } from 'react-router-dom';
+import { useAppDispatch, useAppSelector } from '../../../redux/hooks';
+import {
+ setMenuActivePage,
+ setMenuActiveProfilePage,
+} from '../../../redux/slices/store';
+
+interface MenuItemProps {
+ icon: string;
+ text: string;
+ href: string;
+ page: string;
+ profilePage: string;
+ active?: boolean;
+}
+
+const MenuItem: React.FC
= ({
+ icon,
+ text = '',
+ href = '',
+ active = false,
+ page = '',
+ profilePage = '',
+}) => {
+ const dispatch = useAppDispatch();
+
+ return (
+ {
+ dispatch(setMenuActivePage(page));
+ dispatch(setMenuActiveProfilePage(profilePage));
+ }}
+ >
+
+ {text}
+
+ );
+};
+
const AccountMenu = () => {
- return ;
+ const menuItems = [
+ {
+ text: 'Задачи',
+ href: '/home/account/missions',
+ icon: Clipboard,
+ page: 'account',
+ profilePage: 'missions',
+ },
+ {
+ text: 'Статьи',
+ href: '/home/account/articles',
+ icon: Openbook,
+ page: 'account',
+ profilePage: 'articles',
+ },
+ {
+ text: 'Контесты',
+ href: '/home/account/contests',
+ icon: Cup,
+ page: 'account',
+ profilePage: 'contests',
+ },
+ ];
+
+ const activeProfilePage = useAppSelector(
+ (state) => state.store.menu.activeProfilePage,
+ );
+
+ console.log('active', [activeProfilePage]);
+
+ return (
+
+ {menuItems.map((v, i) => (
+
+ ))}
+
+ );
};
export default AccountMenu;
diff --git a/src/views/home/account/ArticlesBlock.tsx b/src/views/home/account/ArticlesBlock.tsx
index a0ec79c..1d4f141 100644
--- a/src/views/home/account/ArticlesBlock.tsx
+++ b/src/views/home/account/ArticlesBlock.tsx
@@ -1,5 +1,124 @@
-const ArticlesBlock = () => {
- return ;
+import { FC, useEffect, useState } from 'react';
+import { useAppDispatch, useAppSelector } from '../../../redux/hooks';
+import { setMenuActiveProfilePage } from '../../../redux/slices/store';
+import { cn } from '../../../lib/cn';
+import { ChevroneDown, Edit } from '../../../assets/icons/groups';
+import { fetchArticles } from '../../../redux/slices/articles';
+
+import { useNavigate } from 'react-router-dom';
+
+export interface ArticleItemProps {
+ id: number;
+ name: string;
+ tags: string[];
+}
+
+const ArticleItem: React.FC = ({ id, name, tags }) => {
+ const navigate = useNavigate();
+ return (
+ {
+ navigate(`/article/${id}?back=/home/account/articles`);
+ }}
+ >
+
+
+ #{id}
+
+
+ {name}
+
+
+
+ {tags.map((v, i) => (
+
+ {v}
+
+ ))}
+
+
+

{
+ e.stopPropagation();
+ navigate(
+ `/article/create?back=/home/account/articles&articleId=${id}`,
+ );
+ }}
+ />
+
+ );
+};
+
+interface ArticlesBlockProps {
+ className?: string;
+}
+
+const ArticlesBlock: FC = ({ className = '' }) => {
+ const dispatch = useAppDispatch();
+ const articles = useAppSelector((state) => state.articles.articles);
+ const [active, setActive] = useState(true);
+
+ useEffect(() => {
+ dispatch(setMenuActiveProfilePage('articles'));
+ dispatch(fetchArticles({}));
+ }, []);
+ return (
+
+
+
{
+ setActive(!active);
+ }}
+ >
+
Мои статьи
+

+
+
+
+
+ {articles.map((v, i) => (
+
+ ))}
+
+
+
+
+
+ );
};
export default ArticlesBlock;
diff --git a/src/views/home/account/ContestsBlock.tsx b/src/views/home/account/ContestsBlock.tsx
index ef6d1f9..91b13ac 100644
--- a/src/views/home/account/ContestsBlock.tsx
+++ b/src/views/home/account/ContestsBlock.tsx
@@ -1,5 +1,18 @@
+import { useEffect } from 'react';
+import { useAppDispatch } from '../../../redux/hooks';
+import { setMenuActiveProfilePage } from '../../../redux/slices/store';
+
const ContestsBlock = () => {
- return ;
+ const dispatch = useAppDispatch();
+
+ useEffect(() => {
+ dispatch(setMenuActiveProfilePage('contests'));
+ }, []);
+ return (
+
+ Пока пусто :(
+
+ );
};
export default ContestsBlock;
diff --git a/src/views/home/account/MissionsBlock.tsx b/src/views/home/account/MissionsBlock.tsx
index b439d81..1fce2a8 100644
--- a/src/views/home/account/MissionsBlock.tsx
+++ b/src/views/home/account/MissionsBlock.tsx
@@ -1,5 +1,19 @@
+import { useEffect } from 'react';
+import { useAppDispatch } from '../../../redux/hooks';
+import { setMenuActiveProfilePage } from '../../../redux/slices/store';
+
const MissionsBlock = () => {
- return ;
+ const dispatch = useAppDispatch();
+
+ useEffect(() => {
+ dispatch(setMenuActiveProfilePage('missions'));
+ }, []);
+
+ return (
+
+ Пока пусто :(
+
+ );
};
export default MissionsBlock;
diff --git a/src/views/home/account/RightPanel.tsx b/src/views/home/account/RightPanel.tsx
index effd006..b1b63d8 100644
--- a/src/views/home/account/RightPanel.tsx
+++ b/src/views/home/account/RightPanel.tsx
@@ -1,15 +1,108 @@
+import { PrimaryButton } from '../../../components/button/PrimaryButton';
import { ReverseButton } from '../../../components/button/ReverseButton';
import { useAppDispatch, useAppSelector } from '../../../redux/hooks';
import { logout } from '../../../redux/slices/auth';
+import { OpenBook, Clipboard, Cup } from '../../../assets/icons/account';
+import { FC } from 'react';
+
+interface StatisticItemProps {
+ icon: string;
+ title: string;
+ count?: number;
+ countLastWeek?: number;
+}
+const StatisticItem: FC = ({
+ title,
+ icon,
+ count = 0,
+ countLastWeek = 0,
+}) => {
+ return (
+
+

+
+
+
+ {'За 7 дней '}
+ {countLastWeek}
+
+
+
+ );
+};
const RightPanel = () => {
const dispatch = useAppDispatch();
const name = useAppSelector((state) => state.auth.username);
const email = useAppSelector((state) => state.auth.email);
return (
-
-
{name}
-
{email}
+
+
+
+
+
+ {name}
+
+
+ {email}
+
+
+ Топ 50%
+
+
+
+
+
{}}
+ text="Редактировать"
+ className="w-full"
+ />
+
+
+
+
+ {'Статистика решений'}
+
+
+
+
+
+
+ {'Статистика созданий'}
+
+
+
+
+
+
{
diff --git a/src/views/home/contest/Missions.tsx b/src/views/home/contest/Missions.tsx
index 00d9824..535cf05 100644
--- a/src/views/home/contest/Missions.tsx
+++ b/src/views/home/contest/Missions.tsx
@@ -1,6 +1,5 @@
import { FC } from 'react';
-import { useAppDispatch } from '../../../redux/hooks';
-import MissionItem, { MissionItemProps } from './MissionItem';
+import MissionItem from './MissionItem';
import { Contest } from '../../../redux/slices/contests';
export interface Article {
diff --git a/src/views/home/menu/MenuItem.tsx b/src/views/home/menu/MenuItem.tsx
index 501b3c0..db112df 100644
--- a/src/views/home/menu/MenuItem.tsx
+++ b/src/views/home/menu/MenuItem.tsx
@@ -24,7 +24,7 @@ const MenuItem: React.FC = ({