my contests

This commit is contained in:
Виталий Лавшонок
2025-11-06 00:41:01 +03:00
parent 4a65aa4b53
commit dc6df1480e
7 changed files with 354 additions and 121 deletions

View File

@@ -1,60 +1,64 @@
import { useEffect, useState } from 'react';
import { useEffect } from 'react';
import { useAppDispatch, useAppSelector } from '../../../../redux/hooks';
import { setMenuActiveProfilePage } from '../../../../redux/slices/store';
import { fetchContests } from '../../../../redux/slices/contests';
import {
fetchMyContests,
fetchRegisteredContests,
} 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({}));
}, []);
// Redux-состояния
const myContestsState = useAppSelector(
(state) => state.contests.fetchMyContests,
);
const regContestsState = useAppSelector(
(state) => state.contests.fetchRegisteredContests,
);
// При загрузке страницы — выставляем вкладку и подгружаем контесты
useEffect(() => {
dispatch(setMenuActiveProfilePage('contests'));
dispatch(fetchMyContests());
dispatch(fetchRegisteredContests({}));
}, []);
if (status == 'loading') {
return (
<div className="text-liquid-white p-4">Загрузка контестов...</div>
);
}
if (error) {
return <div className="text-red-500 p-4">Ошибка: {error}</div>;
}
console.log(myContestsState);
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();
})}
/>
<div className="h-full w-full relative flex flex-col text-[60px] font-bold p-[20px] gap-[20px]">
{/* Контесты, в которых я участвую */}
<div>
<ContestsBlock
className="mb-[20px]"
title="Предстоящие контесты"
type="reg"
// contests={regContestsState.contests}
contests={[]}
/>
</div>
<ContestsBlock
className="mb-[20px]"
title="Мои контесты"
type="my"
contests={contests.filter((contest) => {
const endTime = new Date(contest.endsAt).getTime();
return endTime < now.getTime();
})}
/>
{/* Контесты, которые я создал */}
<div>
{myContestsState.status === 'loading' ? (
<div className="text-liquid-white p-4 text-[24px]">
Загрузка ваших контестов...
</div>
) : myContestsState.error ? (
<div className="text-red-500 p-4 text-[24px]">
Ошибка: {myContestsState.error}
</div>
) : (
<ContestsBlock
className="mb-[20px]"
title="Мои контесты"
type="my"
contests={myContestsState.contests}
/>
)}
</div>
</div>
);
};

View File

@@ -1,20 +1,22 @@
import { useState, FC } from 'react';
import { cn } from '../../../../lib/cn';
import { ChevroneDown } from '../../../../assets/icons/groups';
import ContestItem from './ContestItem';
import MyContestItem from './MyContestItem';
import RegisterContestItem from './RegisterContestItem';
import { Contest } from '../../../../redux/slices/contests';
interface ContestsBlockProps {
contests: Contest[];
title: string;
className?: string;
type?: string;
type?: 'my' | 'reg';
}
const ContestsBlock: FC<ContestsBlockProps> = ({
contests,
title,
className,
type = 'my',
}) => {
const [active, setActive] = useState<boolean>(title != 'Скрытые');
@@ -51,21 +53,37 @@ const ContestsBlock: FC<ContestsBlockProps> = ({
>
<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'}
/>
))}
{contests.map((v, i) => {
return type == 'my' ? (
<MyContestItem
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'}
/>
) : (
<RegisterContestItem
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>

View File

@@ -0,0 +1,104 @@
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';
import { Edit } from '../../../../assets/icons/input';
export interface ContestItemProps {
id: number;
name: string;
startAt: string;
duration: number;
members: number;
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,
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 grid grid-cols-[1fr,1fr,110px,110px,110px,24px] items-center font-bold',
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>
<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>
<img
className=" h-[24px] w-[24px] hover:bg-liquid-light rounded-[5px] transition-all duration-300"
src={Edit}
onClick={(e) => {
e.stopPropagation();
navigate(
`/contest/editor?back=/home/account/articles&articleId=${id}`,
);
}}
/>
</div>
);
};
export default ContestItem;