profile contests

This commit is contained in:
Виталий Лавшонок
2025-11-05 23:54:40 +03:00
parent c6303758e1
commit 4a65aa4b53
7 changed files with 273 additions and 32 deletions

View File

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

View File

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

View File

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

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

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

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

View File

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