profile contests
This commit is contained in:
@@ -1,9 +1,9 @@
|
|||||||
import { Navigate, Route, Routes } from 'react-router-dom';
|
import { Navigate, Route, Routes } from 'react-router-dom';
|
||||||
import AccountMenu from './AccoutMenu';
|
import AccountMenu from './AccoutMenu';
|
||||||
import RightPanel from './RightPanel';
|
import RightPanel from './RightPanel';
|
||||||
import MissionsBlock from './MissionsBlock';
|
import MissionsBlock from './missions/MissionsBlock';
|
||||||
import ContestsBlock from './ContestsBlock';
|
import Contests from './contests/Contests';
|
||||||
import ArticlesBlock from './ArticlesBlock';
|
import ArticlesBlock from './articles/ArticlesBlock';
|
||||||
import { useAppDispatch } from '../../../redux/hooks';
|
import { useAppDispatch } from '../../../redux/hooks';
|
||||||
import { useEffect } from 'react';
|
import { useEffect } from 'react';
|
||||||
import { setMenuActivePage } from '../../../redux/slices/store';
|
import { setMenuActivePage } from '../../../redux/slices/store';
|
||||||
@@ -32,10 +32,7 @@ const Account = () => {
|
|||||||
path="articles"
|
path="articles"
|
||||||
element={<ArticlesBlock />}
|
element={<ArticlesBlock />}
|
||||||
/>
|
/>
|
||||||
<Route
|
<Route path="contests" element={<Contests />} />
|
||||||
path="contests"
|
|
||||||
element={<ContestsBlock />}
|
|
||||||
/>
|
|
||||||
<Route
|
<Route
|
||||||
path="*"
|
path="*"
|
||||||
element={
|
element={
|
||||||
|
|||||||
@@ -1,18 +0,0 @@
|
|||||||
import { useEffect } from 'react';
|
|
||||||
import { useAppDispatch } from '../../../redux/hooks';
|
|
||||||
import { setMenuActiveProfilePage } from '../../../redux/slices/store';
|
|
||||||
|
|
||||||
const ContestsBlock = () => {
|
|
||||||
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,9 +1,9 @@
|
|||||||
import { FC, useEffect, useState } from 'react';
|
import { FC, useEffect, useState } from 'react';
|
||||||
import { useAppDispatch, useAppSelector } from '../../../redux/hooks';
|
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 { fetchArticles } from '../../../../redux/slices/articles';
|
||||||
|
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
|
||||||
124
src/views/home/account/contests/ContestItem.tsx
Normal file
124
src/views/home/account/contests/ContestItem.tsx
Normal file
@@ -0,0 +1,124 @@
|
|||||||
|
import { cn } from '../../../../lib/cn';
|
||||||
|
import { Account } from '../../../../assets/icons/auth';
|
||||||
|
import { PrimaryButton } from '../../../../components/button/PrimaryButton';
|
||||||
|
import { ReverseButton } from '../../../../components/button/ReverseButton';
|
||||||
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
|
||||||
|
export interface ContestItemProps {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
startAt: string;
|
||||||
|
duration: number;
|
||||||
|
members: number;
|
||||||
|
statusRegister: 'reg' | 'nonreg';
|
||||||
|
type: 'first' | 'second';
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatDate(dateString: string): string {
|
||||||
|
const date = new Date(dateString);
|
||||||
|
|
||||||
|
const day = date.getDate().toString().padStart(2, '0');
|
||||||
|
const month = (date.getMonth() + 1).toString().padStart(2, '0');
|
||||||
|
const year = date.getFullYear();
|
||||||
|
|
||||||
|
const hours = date.getHours().toString().padStart(2, '0');
|
||||||
|
const minutes = date.getMinutes().toString().padStart(2, '0');
|
||||||
|
|
||||||
|
return `${day}/${month}/${year}\n${hours}:${minutes}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatWaitTime(ms: number): string {
|
||||||
|
const minutes = Math.floor(ms / 60000);
|
||||||
|
const hours = Math.floor(minutes / 60);
|
||||||
|
const days = Math.floor(hours / 24);
|
||||||
|
|
||||||
|
if (days > 0) {
|
||||||
|
const remainder = days % 10;
|
||||||
|
let suffix = 'дней';
|
||||||
|
if (remainder === 1 && days !== 11) suffix = 'день';
|
||||||
|
else if (remainder >= 2 && remainder <= 4 && (days < 10 || days > 20))
|
||||||
|
suffix = 'дня';
|
||||||
|
return `${days} ${suffix}`;
|
||||||
|
} else if (hours > 0) {
|
||||||
|
const mins = minutes % 60;
|
||||||
|
return mins > 0 ? `${hours} ч ${mins} мин` : `${hours} ч`;
|
||||||
|
} else {
|
||||||
|
return `${minutes} мин`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const ContestItem: React.FC<ContestItemProps> = ({
|
||||||
|
id,
|
||||||
|
name,
|
||||||
|
startAt,
|
||||||
|
duration,
|
||||||
|
members,
|
||||||
|
statusRegister,
|
||||||
|
type,
|
||||||
|
}) => {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
const now = new Date();
|
||||||
|
|
||||||
|
const waitTime = new Date(startAt).getTime() - now.getTime();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
'w-full box-border relative rounded-[10px] px-[20px] py-[10px] text-liquid-white text-[16px] leading-[20px] cursor-pointer',
|
||||||
|
waitTime <= 0 ? 'grid grid-cols-6' : 'grid grid-cols-7',
|
||||||
|
'items-center font-bold text-liquid-white',
|
||||||
|
type == 'first'
|
||||||
|
? ' bg-liquid-lighter'
|
||||||
|
: ' bg-liquid-background',
|
||||||
|
)}
|
||||||
|
onClick={() => {
|
||||||
|
navigate(`/contest/${id}`);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className="text-left font-bold text-[18px]">{name}</div>
|
||||||
|
<div className="text-center text-liquid-brightmain font-normal ">
|
||||||
|
{/* {authors.map((v, i) => <p key={i}>{v}</p>)} */}
|
||||||
|
valavshonok
|
||||||
|
</div>
|
||||||
|
<div className="text-center text-nowrap whitespace-pre-line">
|
||||||
|
{formatDate(startAt)}
|
||||||
|
</div>
|
||||||
|
<div className="text-center">{formatWaitTime(duration)}</div>
|
||||||
|
{waitTime > 0 && (
|
||||||
|
<div className="text-center whitespace-pre-line ">
|
||||||
|
{'До начала\n' + formatWaitTime(waitTime)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className="items-center justify-center flex gap-[10px] flex-row w-full">
|
||||||
|
<div>{members}</div>
|
||||||
|
<img src={Account} className="h-[24px] w-[24px]" />
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center justify-end">
|
||||||
|
{statusRegister == 'reg' ? (
|
||||||
|
<>
|
||||||
|
{' '}
|
||||||
|
<PrimaryButton
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
}}
|
||||||
|
text="Регистрация"
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
{' '}
|
||||||
|
<ReverseButton
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
}}
|
||||||
|
text="Вы записаны"
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ContestItem;
|
||||||
62
src/views/home/account/contests/Contests.tsx
Normal file
62
src/views/home/account/contests/Contests.tsx
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
import { useAppDispatch, useAppSelector } from '../../../../redux/hooks';
|
||||||
|
import { setMenuActiveProfilePage } from '../../../../redux/slices/store';
|
||||||
|
import { fetchContests } from '../../../../redux/slices/contests';
|
||||||
|
import ContestsBlock from './ContestsBlock';
|
||||||
|
|
||||||
|
const Contests = () => {
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
|
const now = new Date();
|
||||||
|
|
||||||
|
const [modalActive, setModalActive] = useState<boolean>(false);
|
||||||
|
|
||||||
|
// Берём данные из Redux
|
||||||
|
const contests = useAppSelector((state) => state.contests.contests);
|
||||||
|
const status = useAppSelector((state) => state.contests.statuses.create);
|
||||||
|
const error = useAppSelector((state) => state.contests.error);
|
||||||
|
|
||||||
|
// При загрузке страницы — выставляем активную вкладку и подгружаем контесты
|
||||||
|
useEffect(() => {
|
||||||
|
dispatch(fetchContests({}));
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
dispatch(setMenuActiveProfilePage('contests'));
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
if (status == 'loading') {
|
||||||
|
return (
|
||||||
|
<div className="text-liquid-white p-4">Загрузка контестов...</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
return <div className="text-red-500 p-4">Ошибка: {error}</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="h-full w-full relative flex flex-col text-[60px] font-bold p-[20px]">
|
||||||
|
<ContestsBlock
|
||||||
|
className="mb-[20px]"
|
||||||
|
type="reg"
|
||||||
|
title="Предстоящие контесты"
|
||||||
|
contests={contests.filter((contest) => {
|
||||||
|
const endTime = new Date(contest.endsAt).getTime();
|
||||||
|
return endTime >= now.getTime();
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<ContestsBlock
|
||||||
|
className="mb-[20px]"
|
||||||
|
title="Мои контесты"
|
||||||
|
type="my"
|
||||||
|
contests={contests.filter((contest) => {
|
||||||
|
const endTime = new Date(contest.endsAt).getTime();
|
||||||
|
return endTime < now.getTime();
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Contests;
|
||||||
76
src/views/home/account/contests/ContestsBlock.tsx
Normal file
76
src/views/home/account/contests/ContestsBlock.tsx
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
import { useState, FC } from 'react';
|
||||||
|
import { cn } from '../../../../lib/cn';
|
||||||
|
import { ChevroneDown } from '../../../../assets/icons/groups';
|
||||||
|
import ContestItem from './ContestItem';
|
||||||
|
import { Contest } from '../../../../redux/slices/contests';
|
||||||
|
|
||||||
|
interface ContestsBlockProps {
|
||||||
|
contests: Contest[];
|
||||||
|
title: string;
|
||||||
|
className?: string;
|
||||||
|
type?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ContestsBlock: FC<ContestsBlockProps> = ({
|
||||||
|
contests,
|
||||||
|
title,
|
||||||
|
className,
|
||||||
|
}) => {
|
||||||
|
const [active, setActive] = useState<boolean>(title != 'Скрытые');
|
||||||
|
|
||||||
|
return (
|
||||||
|
<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] items-center cursor-pointer border-b-[1px] border-b-transparent transition-all duration-300',
|
||||||
|
active && 'border-b-liquid-lighter',
|
||||||
|
)}
|
||||||
|
onClick={() => {
|
||||||
|
setActive(!active);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span>{title}</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="pb-[10px] pt-[20px]">
|
||||||
|
{contests.map((v, i) => (
|
||||||
|
<ContestItem
|
||||||
|
key={i}
|
||||||
|
id={v.id}
|
||||||
|
name={v.name}
|
||||||
|
startAt={v.startsAt}
|
||||||
|
statusRegister={'reg'}
|
||||||
|
duration={
|
||||||
|
new Date(v.endsAt).getTime() -
|
||||||
|
new Date(v.startsAt).getTime()
|
||||||
|
}
|
||||||
|
members={v.members.length}
|
||||||
|
type={i % 2 ? 'second' : 'first'}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ContestsBlock;
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import { useEffect } from 'react';
|
import { useEffect } from 'react';
|
||||||
import { useAppDispatch } from '../../../redux/hooks';
|
import { useAppDispatch } from '../../../../redux/hooks';
|
||||||
import { setMenuActiveProfilePage } from '../../../redux/slices/store';
|
import { setMenuActiveProfilePage } from '../../../../redux/slices/store';
|
||||||
|
|
||||||
const MissionsBlock = () => {
|
const MissionsBlock = () => {
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
Reference in New Issue
Block a user