Add account and articles updater

This commit is contained in:
Виталий Лавшонок
2025-11-05 11:43:18 +03:00
parent aeab03d35c
commit c6303758e1
22 changed files with 581 additions and 124 deletions

View File

@@ -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>

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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={() => {

View File

@@ -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 {

View File

@@ -24,7 +24,7 @@ const MenuItem: React.FC<MenuItemProps> = ({
<Link
to={href}
className={`
flex items-center gap-3 p-[16px] rounded-[10px\] h-[40px] text-[18px] font-bold
flex items-center gap-3 p-[16px] rounded-[10px] h-[40px] text-[18px] font-bold
transition-all duration-300 text-liquid-white mt-[20px]
active:scale-95
${