Add account and articles updater
This commit is contained in:
@@ -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 (
|
||||
<div className="h-full w-[calc(100%+250px)] box-border grid grid-cols-[1fr,520px] relative">
|
||||
<div className="h-full w-[calc(100%+250px)] box-border grid grid-cols-[1fr,520px] relative divide-x-[1px] divide-liquid-lighter">
|
||||
<div className=" h-full min-h-0 flex flex-col">
|
||||
<div className=" h-full grid grid-rows-[80px,1fr] ">
|
||||
<div className="">
|
||||
<div className=" h-full grid grid-rows-[80px,1fr] ">
|
||||
<div className="h-full w-full">
|
||||
<AccountMenu />
|
||||
</div>
|
||||
<div className="h-full min-h-0 overflow-y-scroll medium-scrollbar flex flex-col gap-[20px] ">
|
||||
<Routes>
|
||||
<Route
|
||||
path="/home/account/missions"
|
||||
path="missions"
|
||||
element={<MissionsBlock />}
|
||||
/>
|
||||
<Route
|
||||
path="/home/account/articles"
|
||||
path="articles"
|
||||
element={<ArticlesBlock />}
|
||||
/>
|
||||
<Route
|
||||
path="/home/account/contests"
|
||||
path="contests"
|
||||
element={<ContestsBlock />}
|
||||
/>
|
||||
<Route path="*" element={<MissionsBlock />} />
|
||||
<Route
|
||||
path="*"
|
||||
element={
|
||||
<Navigate to="/home/account/missions" />
|
||||
}
|
||||
/>
|
||||
</Routes>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -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<MenuItemProps> = ({
|
||||
icon,
|
||||
text = '',
|
||||
href = '',
|
||||
active = false,
|
||||
page = '',
|
||||
profilePage = '',
|
||||
}) => {
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
return (
|
||||
<Link
|
||||
to={href}
|
||||
className={`
|
||||
flex items-center gap-3 p-[16px] rounded-[10px] h-[40px] text-[18px] font-bold
|
||||
transition-all duration-300 text-liquid-white
|
||||
active:scale-95 hover:bg-liquid-lighter hover:ring-[1px] hover:ring-liquid-light hover:ring-inset
|
||||
${active && 'bg-liquid-lighter '}
|
||||
`}
|
||||
onClick={() => {
|
||||
dispatch(setMenuActivePage(page));
|
||||
dispatch(setMenuActiveProfilePage(profilePage));
|
||||
}}
|
||||
>
|
||||
<img src={icon} />
|
||||
<span>{text}</span>
|
||||
</Link>
|
||||
);
|
||||
};
|
||||
|
||||
const AccountMenu = () => {
|
||||
return <div className="h-full w-full relative "></div>;
|
||||
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 (
|
||||
<div className="h-full w-full relative flex p-[20px] gap-[10px]">
|
||||
{menuItems.map((v, i) => (
|
||||
<MenuItem
|
||||
{...v}
|
||||
key={i}
|
||||
active={activeProfilePage == v.profilePage}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default AccountMenu;
|
||||
|
||||
@@ -1,5 +1,124 @@
|
||||
const ArticlesBlock = () => {
|
||||
return <div className="h-full w-full relative "></div>;
|
||||
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<ArticleItemProps> = ({ id, name, tags }) => {
|
||||
const navigate = useNavigate();
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'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 ',
|
||||
'border-b-[1px] border-b-liquid-lighter cursor-pointer hover:bg-liquid-lighter transition-all duration-300',
|
||||
)}
|
||||
onClick={() => {
|
||||
navigate(`/article/${id}?back=/home/account/articles`);
|
||||
}}
|
||||
>
|
||||
<div className="h-[23px] flex ">
|
||||
<div className="text-[18px] font-bold w-[60px] mr-[20px] flex items-center">
|
||||
#{id}
|
||||
</div>
|
||||
<div className="text-[18px] font-bold flex items-center">
|
||||
{name}
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-[14px] flex text-liquid-light gap-[10px] mt-[10px]">
|
||||
{tags.map((v, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className={cn(
|
||||
'rounded-full px-[16px] py-[8px] bg-liquid-lighter',
|
||||
v == 'Sertificated' && 'text-liquid-green',
|
||||
)}
|
||||
>
|
||||
{v}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<img
|
||||
className=" absolute right-[10px] top-[10px] h-[24px] w-[24px] hover:bg-liquid-light rounded-[5px] transition-all duration-300"
|
||||
src={Edit}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
navigate(
|
||||
`/article/create?back=/home/account/articles&articleId=${id}`,
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
interface ArticlesBlockProps {
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const ArticlesBlock: FC<ArticlesBlockProps> = ({ className = '' }) => {
|
||||
const dispatch = useAppDispatch();
|
||||
const articles = useAppSelector((state) => state.articles.articles);
|
||||
const [active, setActive] = useState<boolean>(true);
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(setMenuActiveProfilePage('articles'));
|
||||
dispatch(fetchArticles({}));
|
||||
}, []);
|
||||
return (
|
||||
<div className="h-full w-full relative p-[20px]">
|
||||
<div
|
||||
className={cn(
|
||||
' border-b-[1px] border-b-liquid-lighter rounded-[10px]',
|
||||
className,
|
||||
)}
|
||||
>
|
||||
<div
|
||||
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',
|
||||
active && ' border-b-liquid-lighter',
|
||||
)}
|
||||
onClick={() => {
|
||||
setActive(!active);
|
||||
}}
|
||||
>
|
||||
<span>Мои статьи</span>
|
||||
<img
|
||||
src={ChevroneDown}
|
||||
className={cn(
|
||||
'transition-all duration-300',
|
||||
active && 'rotate-180',
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className={cn(
|
||||
' grid grid-flow-row grid-rows-[0fr] opacity-0 transition-all duration-300',
|
||||
active && 'grid-rows-[1fr] opacity-100',
|
||||
)}
|
||||
>
|
||||
<div className="overflow-hidden">
|
||||
<div className="grid gap-[20px] pt-[20px] pb-[20px] box-border">
|
||||
{articles.map((v, i) => (
|
||||
<ArticleItem key={i} {...v} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ArticlesBlock;
|
||||
|
||||
@@ -1,5 +1,18 @@
|
||||
import { useEffect } from 'react';
|
||||
import { useAppDispatch } from '../../../redux/hooks';
|
||||
import { setMenuActiveProfilePage } from '../../../redux/slices/store';
|
||||
|
||||
const ContestsBlock = () => {
|
||||
return <div className="h-full w-full relative bg-fuchsia-600"></div>;
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(setMenuActiveProfilePage('contests'));
|
||||
}, []);
|
||||
return (
|
||||
<div className="h-full w-full relative flex items-center justify-center text-[60px] font-bold">
|
||||
Пока пусто :(
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ContestsBlock;
|
||||
|
||||
@@ -1,5 +1,19 @@
|
||||
import { useEffect } from 'react';
|
||||
import { useAppDispatch } from '../../../redux/hooks';
|
||||
import { setMenuActiveProfilePage } from '../../../redux/slices/store';
|
||||
|
||||
const MissionsBlock = () => {
|
||||
return <div className="h-full w-full relative "></div>;
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(setMenuActiveProfilePage('missions'));
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="h-full w-full relative flex items-center justify-center text-[60px] font-bold">
|
||||
Пока пусто :(
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default MissionsBlock;
|
||||
|
||||
@@ -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<StatisticItemProps> = ({
|
||||
title,
|
||||
icon,
|
||||
count = 0,
|
||||
countLastWeek = 0,
|
||||
}) => {
|
||||
return (
|
||||
<div className="h-[53px] grid grid-cols-[36px,1fr] gap-[20px] text-liquid-white">
|
||||
<img src={icon} />
|
||||
<div className="h-full flex flex-col justify-between">
|
||||
<div className="flex gap-[20px] text-[18px] font-bold leading-[23px]">
|
||||
<div>{title}</div>
|
||||
<div>{count}</div>
|
||||
</div>
|
||||
<div className="text-[16px] font-medium leading-[20px]">
|
||||
{'За 7 дней '}
|
||||
<span className="text-liquid-light">{countLastWeek}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const RightPanel = () => {
|
||||
const dispatch = useAppDispatch();
|
||||
const name = useAppSelector((state) => state.auth.username);
|
||||
const email = useAppSelector((state) => state.auth.email);
|
||||
return (
|
||||
<div className="h-full w-full relative p-[20px]">
|
||||
<div>{name}</div>
|
||||
<div>{email}</div>
|
||||
<div className="h-full w-full relative flex flex-col p-[20px] pt-[35px] gap-[20px]">
|
||||
<div className="grid grid-cols-[150px,1fr] h-[150px] gap-[20px]">
|
||||
<div className="-hfull w-full bg-[#B8B8B8] rounded-[10px]"></div>
|
||||
<div className=" relative">
|
||||
<div className="text-liquid-white text-[24px] leading-[30px] font-bold">
|
||||
{name}
|
||||
</div>
|
||||
<div className="text-liquid-light text-[18px] leading-[23px] font-medium">
|
||||
{email}
|
||||
</div>
|
||||
<div className=" absolute bottom-0 text-liquid-light text-[24px] leading-[30px] font-bold">
|
||||
Топ 50%
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<PrimaryButton
|
||||
onClick={() => {}}
|
||||
text="Редактировать"
|
||||
className="w-full"
|
||||
/>
|
||||
|
||||
<div className="h-[1px] w-full bg-liquid-lighter"></div>
|
||||
|
||||
<div className="text-liquid-white text-[24px] leading-[30px] font-bold">
|
||||
{'Статистика решений'}
|
||||
</div>
|
||||
|
||||
<StatisticItem
|
||||
icon={Clipboard}
|
||||
title={'Задачи'}
|
||||
count={14}
|
||||
countLastWeek={5}
|
||||
/>
|
||||
<StatisticItem
|
||||
icon={Cup}
|
||||
title={'Контесты'}
|
||||
count={8}
|
||||
countLastWeek={2}
|
||||
/>
|
||||
|
||||
<div className="text-liquid-white text-[24px] leading-[30px] font-bold">
|
||||
{'Статистика созданий'}
|
||||
</div>
|
||||
|
||||
<StatisticItem
|
||||
icon={Clipboard}
|
||||
title={'Задачи'}
|
||||
count={4}
|
||||
countLastWeek={2}
|
||||
/>
|
||||
<StatisticItem
|
||||
icon={OpenBook}
|
||||
title={'Статьи'}
|
||||
count={12}
|
||||
countLastWeek={4}
|
||||
/>
|
||||
<StatisticItem
|
||||
icon={Cup}
|
||||
title={'Контесты'}
|
||||
count={2}
|
||||
countLastWeek={0}
|
||||
/>
|
||||
|
||||
<ReverseButton
|
||||
className="absolute bottom-[20px] right-[20px]"
|
||||
onClick={() => {
|
||||
|
||||
Reference in New Issue
Block a user