profile contests
This commit is contained in:
@@ -1,9 +1,9 @@
|
||||
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 MissionsBlock from './missions/MissionsBlock';
|
||||
import Contests from './contests/Contests';
|
||||
import ArticlesBlock from './articles/ArticlesBlock';
|
||||
import { useAppDispatch } from '../../../redux/hooks';
|
||||
import { useEffect } from 'react';
|
||||
import { setMenuActivePage } from '../../../redux/slices/store';
|
||||
@@ -32,10 +32,7 @@ const Account = () => {
|
||||
path="articles"
|
||||
element={<ArticlesBlock />}
|
||||
/>
|
||||
<Route
|
||||
path="contests"
|
||||
element={<ContestsBlock />}
|
||||
/>
|
||||
<Route path="contests" element={<Contests />} />
|
||||
<Route
|
||||
path="*"
|
||||
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 { 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 { 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';
|
||||
|
||||
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 { useAppDispatch } from '../../../redux/hooks';
|
||||
import { setMenuActiveProfilePage } from '../../../redux/slices/store';
|
||||
import { useAppDispatch } from '../../../../redux/hooks';
|
||||
import { setMenuActiveProfilePage } from '../../../../redux/slices/store';
|
||||
|
||||
const MissionsBlock = () => {
|
||||
const dispatch = useAppDispatch();
|
||||
Reference in New Issue
Block a user