add contests
This commit is contained in:
@@ -1,165 +0,0 @@
|
||||
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 { toastWarning } from '../../../lib/toastNotification';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useAppDispatch, useAppSelector } from '../../../redux/hooks';
|
||||
import { addOrUpdateContestMember } from '../../../redux/slices/contests';
|
||||
|
||||
export type Role = 'None' | 'Participant' | 'Organizer';
|
||||
|
||||
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 dispatch = useAppDispatch();
|
||||
|
||||
const now = new Date();
|
||||
|
||||
const waitTime = new Date(startAt).getTime() - now.getTime();
|
||||
|
||||
const [myRole, setMyRole] = useState<Role>('None');
|
||||
|
||||
const userId = useAppSelector((state) => state.auth.id);
|
||||
const { contests: contestsRegistered } = useAppSelector(
|
||||
(state) => state.contests.fetchParticipating,
|
||||
);
|
||||
const { contests: contestsMy } = useAppSelector(
|
||||
(state) => state.contests.fetchMyContests,
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (!contestsRegistered || contestsRegistered.length === 0) {
|
||||
setMyRole('None');
|
||||
return;
|
||||
}
|
||||
|
||||
const reg = contestsRegistered.find((c) => c.id === id);
|
||||
const my = contestsMy.find((c) => c.id === id);
|
||||
|
||||
if (my) setMyRole('Organizer');
|
||||
else if (reg) setMyRole('Participant');
|
||||
else setMyRole('None');
|
||||
}, [contestsRegistered]);
|
||||
|
||||
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={() => {
|
||||
if (myRole == 'None') {
|
||||
toastWarning('Зарегистрируйтесь на контест');
|
||||
return;
|
||||
}
|
||||
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">
|
||||
{myRole == 'None' ? (
|
||||
<>
|
||||
{' '}
|
||||
<PrimaryButton
|
||||
onClick={() => {
|
||||
dispatch(
|
||||
addOrUpdateContestMember({
|
||||
contestId: id,
|
||||
member: {
|
||||
userId: Number(userId),
|
||||
role: 'Participant',
|
||||
},
|
||||
}),
|
||||
);
|
||||
}}
|
||||
text="Регистрация"
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
{' '}
|
||||
<ReverseButton
|
||||
onClick={() => {
|
||||
navigate(`/contest/${id}`);
|
||||
}}
|
||||
text="Войти"
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ContestItem;
|
||||
@@ -4,31 +4,28 @@ import { cn } from '../../../lib/cn';
|
||||
import { useAppDispatch, useAppSelector } from '../../../redux/hooks';
|
||||
import ContestsBlock from './ContestsBlock';
|
||||
import { setMenuActivePage } from '../../../redux/slices/store';
|
||||
import { fetchContests, fetchMyContests, fetchParticipatingContests } from '../../../redux/slices/contests';
|
||||
import {
|
||||
fetchContests,
|
||||
fetchMyContests,
|
||||
fetchParticipatingContests,
|
||||
} from '../../../redux/slices/contests';
|
||||
import ModalCreateContest from './ModalCreate';
|
||||
import Filters from './Filter';
|
||||
|
||||
const Contests = () => {
|
||||
const dispatch = useAppDispatch();
|
||||
const now = new Date();
|
||||
|
||||
const [modalActive, setModalActive] = useState<boolean>(false);
|
||||
|
||||
// Берём данные из Redux
|
||||
const contests = useAppSelector(
|
||||
(state) => state.contests.fetchContests.contests,
|
||||
const { contests, status } = useAppSelector(
|
||||
(state) => state.contests.fetchContests,
|
||||
);
|
||||
const status = useAppSelector(
|
||||
(state) => state.contests.fetchContests.status,
|
||||
);
|
||||
const error = useAppSelector((state) => state.contests.fetchContests.error);
|
||||
|
||||
|
||||
// При загрузке страницы — выставляем активную вкладку и подгружаем контесты
|
||||
useEffect(() => {
|
||||
dispatch(setMenuActivePage('contests'));
|
||||
dispatch(fetchContests({}));
|
||||
dispatch(fetchParticipatingContests({pageSize:100}));
|
||||
dispatch(fetchParticipatingContests({ pageSize: 100 }));
|
||||
dispatch(fetchMyContests());
|
||||
}, []);
|
||||
|
||||
@@ -58,31 +55,24 @@ const Contests = () => {
|
||||
Загрузка контестов...
|
||||
</div>
|
||||
)}
|
||||
{status == 'failed' && (
|
||||
<div className="text-red-500 p-4">Ошибка: {error}</div>
|
||||
)}
|
||||
{status == 'successful' && (
|
||||
<>
|
||||
<ContestsBlock
|
||||
className="mb-[20px]"
|
||||
title="Текущие"
|
||||
contests={contests.filter((contest) => {
|
||||
const endTime = new Date(
|
||||
contest.endsAt ?? new Date().toDateString(),
|
||||
).getTime();
|
||||
return endTime >= now.getTime();
|
||||
})}
|
||||
contests={contests.filter(
|
||||
(c) => c.scheduleType != 'AlwaysOpen',
|
||||
)}
|
||||
type="upcoming"
|
||||
/>
|
||||
|
||||
<ContestsBlock
|
||||
className="mb-[20px]"
|
||||
title="Прошедшие"
|
||||
contests={contests.filter((contest) => {
|
||||
const endTime = new Date(
|
||||
contest.endsAt ?? new Date().toDateString(),
|
||||
).getTime();
|
||||
return endTime < now.getTime();
|
||||
})}
|
||||
title="Постоянные"
|
||||
contests={contests.filter(
|
||||
(c) => c.scheduleType == 'AlwaysOpen',
|
||||
)}
|
||||
type="past"
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
@@ -1,19 +1,22 @@
|
||||
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';
|
||||
import PastContestItem from './PastContestItem';
|
||||
import UpcoingContestItem from './UpcomingContestItem';
|
||||
|
||||
interface ContestsBlockProps {
|
||||
contests: Contest[];
|
||||
title: string;
|
||||
className?: string;
|
||||
type: 'upcoming' | 'past';
|
||||
}
|
||||
|
||||
const ContestsBlock: FC<ContestsBlockProps> = ({
|
||||
contests,
|
||||
title,
|
||||
className,
|
||||
type,
|
||||
}) => {
|
||||
const [active, setActive] = useState<boolean>(title != 'Скрытые');
|
||||
|
||||
@@ -33,11 +36,11 @@ const ContestsBlock: FC<ContestsBlockProps> = ({
|
||||
setActive(!active);
|
||||
}}
|
||||
>
|
||||
<span>{title}</span>
|
||||
<span className=" select-none">{title}</span>
|
||||
<img
|
||||
src={ChevroneDown}
|
||||
className={cn(
|
||||
'transition-all duration-300',
|
||||
'transition-all duration-300 select-none',
|
||||
active && 'rotate-180',
|
||||
)}
|
||||
/>
|
||||
@@ -50,24 +53,51 @@ 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 ?? new Date().toString()}
|
||||
duration={
|
||||
new Date(
|
||||
v.endsAt ?? new Date().toString(),
|
||||
).getTime() -
|
||||
new Date(
|
||||
v.startsAt ?? new Date().toString(),
|
||||
).getTime()
|
||||
}
|
||||
members={v.members?.length ?? 0}
|
||||
type={i % 2 ? 'second' : 'first'}
|
||||
/>
|
||||
))}
|
||||
{contests.map((v, i) => {
|
||||
if (type == 'past') {
|
||||
return (
|
||||
<PastContestItem
|
||||
key={i}
|
||||
contestId={v.id}
|
||||
scheduleType={v.scheduleType}
|
||||
name={v.name}
|
||||
startsAt={
|
||||
v.startsAt ?? new Date().toString()
|
||||
}
|
||||
endsAt={
|
||||
v.endsAt ?? new Date().toString()
|
||||
}
|
||||
attemptDurationMinutes={
|
||||
v.attemptDurationMinutes ?? 0
|
||||
}
|
||||
type={i % 2 ? 'second' : 'first'}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (type == 'upcoming') {
|
||||
return (
|
||||
<UpcoingContestItem
|
||||
key={i}
|
||||
contestId={v.id}
|
||||
scheduleType={v.scheduleType}
|
||||
name={v.name}
|
||||
startsAt={
|
||||
v.startsAt ?? new Date().toString()
|
||||
}
|
||||
endsAt={
|
||||
v.endsAt ?? new Date().toString()
|
||||
}
|
||||
attemptDurationMinutes={
|
||||
v.attemptDurationMinutes ?? 0
|
||||
}
|
||||
type={i % 2 ? 'second' : 'first'}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return <></>;
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
189
src/views/home/contests/PastContestItem.tsx
Normal file
189
src/views/home/contests/PastContestItem.tsx
Normal file
@@ -0,0 +1,189 @@
|
||||
import { cn } from '../../../lib/cn';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useAppDispatch, useAppSelector } from '../../../redux/hooks';
|
||||
import { useQuery } from '../../../hooks/useQuery';
|
||||
import { ReverseButton } from '../../../components/button/ReverseButton';
|
||||
import { PrimaryButton } from '../../../components/button/PrimaryButton';
|
||||
import { toastWarning } from '../../../lib/toastNotification';
|
||||
import { useEffect, useState } from 'react';
|
||||
import {
|
||||
addOrUpdateContestMember,
|
||||
fetchParticipatingContests,
|
||||
} from '../../../redux/slices/contests';
|
||||
|
||||
export interface PastContestItemProps {
|
||||
name: string;
|
||||
contestId: number;
|
||||
scheduleType: 'AlwaysOpen' | 'FixedWindow' | 'RollingWindow';
|
||||
startsAt: string;
|
||||
endsAt: string;
|
||||
attemptDurationMinutes: 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 formatDurationTime(minutes: number): string {
|
||||
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} мин`;
|
||||
}
|
||||
}
|
||||
|
||||
type Role = 'None' | 'Participant' | 'Organizer';
|
||||
|
||||
const PastContestItem: React.FC<PastContestItemProps> = ({
|
||||
name,
|
||||
contestId,
|
||||
scheduleType,
|
||||
startsAt,
|
||||
endsAt,
|
||||
attemptDurationMinutes,
|
||||
type,
|
||||
}) => {
|
||||
const navigate = useNavigate();
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const [role, setRole] = useState<Role>('None');
|
||||
|
||||
const myname = useAppSelector((state) => state.auth.username);
|
||||
|
||||
const userId = useAppSelector((state) => state.auth.id);
|
||||
|
||||
const query = useQuery();
|
||||
const username = query.get('username') ?? myname ?? '';
|
||||
|
||||
const { contests: myContests } = useAppSelector(
|
||||
(state) => state.contests.fetchMyContests,
|
||||
);
|
||||
const { contests: participantContests } = useAppSelector(
|
||||
(state) => state.contests.fetchParticipating,
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
setRole(
|
||||
(() => {
|
||||
if (myContests?.some((c) => c.id === contestId)) {
|
||||
return 'Organizer';
|
||||
}
|
||||
if (participantContests?.some((c) => c.id === contestId)) {
|
||||
return 'Participant';
|
||||
}
|
||||
return 'None';
|
||||
})(),
|
||||
);
|
||||
}, [myContests, participantContests]);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'w-full box-border relative rounded-[10px] px-[20px] py-[14px] text-liquid-white text-[16px] leading-[20px] cursor-pointer grid items-center font-bold border-transparent hover:border-liquid-darkmain border-solid border-[1px] transition-all duration-300',
|
||||
userId
|
||||
? 'grid-cols-[1fr,150px,190px,120px,150px,150px]'
|
||||
: 'grid-cols-[1fr,150px,190px,120px,150px]',
|
||||
type == 'first'
|
||||
? ' bg-liquid-lighter'
|
||||
: ' bg-liquid-background',
|
||||
)}
|
||||
onClick={() => {
|
||||
if (role == 'None') {
|
||||
toastWarning('Нужно зарегистрироваться на контест');
|
||||
return;
|
||||
}
|
||||
const params = new URLSearchParams({
|
||||
back: '/home/contests',
|
||||
});
|
||||
navigate(`/contest/${contestId}?${params}`);
|
||||
}}
|
||||
>
|
||||
<div className="text-left font-bold text-[18px]">{name}</div>
|
||||
<div className="text-center text-liquid-brightmain font-normal flex items-center justify-center">
|
||||
{username}
|
||||
</div>
|
||||
{scheduleType == 'AlwaysOpen' ? (
|
||||
<div className="text-center text-nowrap whitespace-pre-line text-[14px]">
|
||||
Всегда открыт
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex items-center gap-[5px] text-[14px]">
|
||||
<div className="text-center text-nowrap whitespace-pre-line">
|
||||
{formatDate(startsAt)}
|
||||
</div>
|
||||
<div>-</div>
|
||||
<div className="text-center text-nowrap whitespace-pre-line">
|
||||
{formatDate(endsAt)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="text-center">
|
||||
{formatDurationTime(attemptDurationMinutes)}
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-center text-liquid-brightmain font-normal">
|
||||
{scheduleType == 'AlwaysOpen' ? 'Открыт' : 'Завершен'}
|
||||
</div>
|
||||
{userId && (
|
||||
<div className="flex items-center justify-center">
|
||||
{role == 'Organizer' || role == 'Participant' ? (
|
||||
<ReverseButton
|
||||
onClick={() => {
|
||||
const params = new URLSearchParams({
|
||||
back: '/home/contests',
|
||||
});
|
||||
navigate(`/contest/${contestId}?${params}`);
|
||||
}}
|
||||
text="Войти"
|
||||
/>
|
||||
) : (
|
||||
<PrimaryButton
|
||||
onClick={() => {
|
||||
dispatch(
|
||||
addOrUpdateContestMember({
|
||||
contestId: contestId,
|
||||
member: {
|
||||
userId: Number(userId),
|
||||
role: 'Participant',
|
||||
},
|
||||
}),
|
||||
)
|
||||
.unwrap()
|
||||
.then(() =>
|
||||
dispatch(
|
||||
fetchParticipatingContests({}),
|
||||
),
|
||||
);
|
||||
}}
|
||||
text="Регистрация"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default PastContestItem;
|
||||
233
src/views/home/contests/UpcomingContestItem.tsx
Normal file
233
src/views/home/contests/UpcomingContestItem.tsx
Normal file
@@ -0,0 +1,233 @@
|
||||
import { cn } from '../../../lib/cn';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useAppDispatch, useAppSelector } from '../../../redux/hooks';
|
||||
import { useQuery } from '../../../hooks/useQuery';
|
||||
import { toastWarning } from '../../../lib/toastNotification';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { ReverseButton } from '../../../components/button/ReverseButton';
|
||||
import { PrimaryButton } from '../../../components/button/PrimaryButton';
|
||||
import {
|
||||
addOrUpdateContestMember,
|
||||
fetchParticipatingContests,
|
||||
} from '../../../redux/slices/contests';
|
||||
|
||||
type Role = 'None' | 'Participant' | 'Organizer';
|
||||
|
||||
export interface UpcoingContestItemProps {
|
||||
name: string;
|
||||
contestId: number;
|
||||
scheduleType: 'AlwaysOpen' | 'FixedWindow' | 'RollingWindow';
|
||||
startsAt: string;
|
||||
endsAt: string;
|
||||
attemptDurationMinutes: 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 formatDurationTime(minutes: number): string {
|
||||
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} мин`;
|
||||
}
|
||||
}
|
||||
|
||||
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 UpcoingContestItem: React.FC<UpcoingContestItemProps> = ({
|
||||
name,
|
||||
contestId,
|
||||
scheduleType,
|
||||
startsAt,
|
||||
endsAt,
|
||||
attemptDurationMinutes,
|
||||
type,
|
||||
}) => {
|
||||
const navigate = useNavigate();
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const [role, setRole] = useState<Role>('None');
|
||||
|
||||
const myname = useAppSelector((state) => state.auth.username);
|
||||
|
||||
const { contests: myContests } = useAppSelector(
|
||||
(state) => state.contests.fetchMyContests,
|
||||
);
|
||||
const { contests: participantContests } = useAppSelector(
|
||||
(state) => state.contests.fetchParticipating,
|
||||
);
|
||||
|
||||
const query = useQuery();
|
||||
const username = query.get('username') ?? myname ?? '';
|
||||
|
||||
const userId = useAppSelector((state) => state.auth.id);
|
||||
|
||||
const started = new Date(startsAt) <= new Date();
|
||||
const finished = new Date(endsAt) <= new Date();
|
||||
const waitTime = !started
|
||||
? new Date(startsAt).getTime() - new Date().getTime()
|
||||
: new Date(endsAt).getTime() - new Date().getTime();
|
||||
|
||||
useEffect(() => {
|
||||
setRole(
|
||||
(() => {
|
||||
if (myContests?.some((c) => c.id === contestId)) {
|
||||
return 'Organizer';
|
||||
}
|
||||
if (participantContests?.some((c) => c.id === contestId)) {
|
||||
return 'Participant';
|
||||
}
|
||||
return 'None';
|
||||
})(),
|
||||
);
|
||||
}, [myContests, participantContests]);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'w-full box-border relative rounded-[10px] px-[20px] py-[14px] text-liquid-white text-[16px] leading-[20px] cursor-pointer grid items-center font-bold border-transparent hover:border-liquid-darkmain border-solid border-[1px] transition-all duration-300',
|
||||
userId
|
||||
? 'grid-cols-[1fr,1fr,220px,130px,130px,140px,150px]'
|
||||
: 'grid-cols-[1fr,1fr,220px,130px,130px,130px]',
|
||||
type == 'first'
|
||||
? ' bg-liquid-lighter'
|
||||
: ' bg-liquid-background',
|
||||
)}
|
||||
onClick={() => {
|
||||
if (!started) {
|
||||
toastWarning('Контест еще не начался');
|
||||
return;
|
||||
}
|
||||
|
||||
const params = new URLSearchParams({
|
||||
back: '/home/contests',
|
||||
});
|
||||
navigate(`/contest/${contestId}?${params}`);
|
||||
}}
|
||||
>
|
||||
<div className="text-left font-bold text-[18px]">{name}</div>
|
||||
<div className="text-center text-liquid-brightmain font-normal flex items-center justify-center">
|
||||
{username}
|
||||
</div>
|
||||
{scheduleType == 'AlwaysOpen' ? (
|
||||
<div className="text-center text-nowrap whitespace-pre-line text-[14px]">
|
||||
Всегда открыт
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex items-center gap-[5px] text-[14px]">
|
||||
<div className="text-center text-nowrap whitespace-pre-line">
|
||||
{formatDate(startsAt)}
|
||||
</div>
|
||||
<div>-</div>
|
||||
<div className="text-center text-nowrap whitespace-pre-line">
|
||||
{formatDate(endsAt)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="text-center">
|
||||
{formatDurationTime(attemptDurationMinutes)}
|
||||
</div>
|
||||
|
||||
{!started ? (
|
||||
<div className="text-center whitespace-pre-line ">
|
||||
{'До начала\n' + formatWaitTime(waitTime)}
|
||||
</div>
|
||||
) : (
|
||||
!finished && (
|
||||
<div className="text-center whitespace-pre-line ">
|
||||
{'До конца\n' + formatWaitTime(waitTime)}
|
||||
</div>
|
||||
)
|
||||
)}
|
||||
|
||||
<div className="flex items-center justify-center text-liquid-brightmain font-normal">
|
||||
{new Date() < new Date(startsAt) ? (
|
||||
<>{'Не начался'}</>
|
||||
) : (
|
||||
<>{scheduleType == 'AlwaysOpen' ? 'Открыт' : 'Идет'}</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{userId && (
|
||||
<div className="flex items-center justify-center">
|
||||
{role == 'Organizer' || role == 'Participant' ? (
|
||||
<ReverseButton
|
||||
onClick={() => {
|
||||
const params = new URLSearchParams({
|
||||
back: '/home/contests',
|
||||
});
|
||||
navigate(`/contest/${contestId}?${params}`);
|
||||
}}
|
||||
text="Войти"
|
||||
/>
|
||||
) : (
|
||||
<PrimaryButton
|
||||
onClick={() => {
|
||||
dispatch(
|
||||
addOrUpdateContestMember({
|
||||
contestId: contestId,
|
||||
member: {
|
||||
userId: Number(userId),
|
||||
role: 'Participant',
|
||||
},
|
||||
}),
|
||||
)
|
||||
.unwrap()
|
||||
.then(() =>
|
||||
dispatch(
|
||||
fetchParticipatingContests({}),
|
||||
),
|
||||
);
|
||||
}}
|
||||
text="Регистрация"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default UpcoingContestItem;
|
||||
Reference in New Issue
Block a user