contests
This commit is contained in:
@@ -1,11 +1,12 @@
|
||||
import { cn } from "../../../lib/cn";
|
||||
import { Account } from "../../../assets/icons/auth";
|
||||
import { registerUser } from "../../../redux/slices/auth";
|
||||
import { PrimaryButton } from "../../../components/button/PrimaryButton";
|
||||
import { ReverseButton } from "../../../components/button/ReverseButton";
|
||||
|
||||
export interface ContestItemProps {
|
||||
id: number;
|
||||
name: string;
|
||||
authors: string[];
|
||||
startAt: string;
|
||||
registerAt: string;
|
||||
duration: number;
|
||||
members: number;
|
||||
statusRegister: "reg" | "nonreg";
|
||||
@@ -25,44 +26,69 @@ function formatDate(dateString: string): string {
|
||||
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, authors, startAt, registerAt, duration, members, statusRegister, type
|
||||
name, startAt, duration, members, statusRegister, type
|
||||
}) => {
|
||||
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",
|
||||
<div className={cn("w-full box-border relative rounded-[10px] px-[20px] py-[10px] text-liquid-white text-[16px] leading-[20px]",
|
||||
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"
|
||||
)}>
|
||||
<div className="text-left">
|
||||
<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>)}
|
||||
<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">
|
||||
<div className="text-center text-nowrap whitespace-pre-line">
|
||||
{formatDate(startAt)}
|
||||
</div>
|
||||
<div className="text-center">
|
||||
{duration}
|
||||
{formatWaitTime(duration)}
|
||||
</div>
|
||||
{
|
||||
waitTime > 0 &&
|
||||
<div className="text-center">
|
||||
{waitTime}
|
||||
<div className="text-center whitespace-pre-line ">
|
||||
|
||||
{"До начала\n" + formatWaitTime(waitTime)}
|
||||
</div>
|
||||
}
|
||||
<div className="text-center">
|
||||
{members}
|
||||
<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="text-center">
|
||||
{statusRegister}
|
||||
<div className="flex items-center justify-end">
|
||||
{
|
||||
statusRegister == "reg" ?
|
||||
<> <PrimaryButton onClick={() => {}} text="Регистрация"/></>
|
||||
:
|
||||
<> <ReverseButton onClick={() => {}} text="Вы записаны"/></>
|
||||
}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
@@ -1,128 +1,69 @@
|
||||
import { useEffect } from "react";
|
||||
import { SecondaryButton } from "../../../components/button/SecondaryButton";
|
||||
import { cn } from "../../../lib/cn";
|
||||
import { useAppDispatch } from "../../../redux/hooks";
|
||||
import { useAppDispatch, useAppSelector } from "../../../redux/hooks";
|
||||
import ContestsBlock from "./ContestsBlock";
|
||||
import { setMenuActivePage } from "../../../redux/slices/store";
|
||||
|
||||
|
||||
interface Contest {
|
||||
id: number;
|
||||
name: string;
|
||||
authors: string[];
|
||||
startAt: string;
|
||||
registerAt: string;
|
||||
duration: number;
|
||||
members: number;
|
||||
statusRegister: "reg" | "nonreg";
|
||||
}
|
||||
|
||||
|
||||
import { fetchContests } from "../../../redux/slices/contests";
|
||||
|
||||
const Contests = () => {
|
||||
|
||||
const dispatch = useAppDispatch();
|
||||
const now = new Date();
|
||||
const contests: Contest[] = [
|
||||
// === Прошедшие контесты ===
|
||||
{
|
||||
id: 1,
|
||||
name: "Code Marathon 2025",
|
||||
authors: ["tourist", "Petr", "Semen", "Rotar"],
|
||||
startAt: "2025-09-15T10:00:00.000Z",
|
||||
registerAt: "2025-09-10T10:00:00.000Z",
|
||||
duration: 180,
|
||||
members: 4821,
|
||||
statusRegister: "reg",
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: "Autumn Cup 2025",
|
||||
authors: ["awoo", "Benq"],
|
||||
startAt: "2025-09-25T17:00:00.000Z",
|
||||
registerAt: "2025-09-20T17:00:00.000Z",
|
||||
duration: 150,
|
||||
members: 3670,
|
||||
statusRegister: "nonreg",
|
||||
},
|
||||
|
||||
// === Контесты, которые сейчас идут ===
|
||||
{
|
||||
id: 3,
|
||||
name: "Halloween Challenge",
|
||||
authors: ["Errichto", "Radewoosh"],
|
||||
startAt: "2025-10-29T10:00:00.000Z", // начался сегодня
|
||||
registerAt: "2025-10-25T10:00:00.000Z",
|
||||
duration: 240,
|
||||
members: 5123,
|
||||
statusRegister: "reg",
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
name: "October Blitz",
|
||||
authors: ["neal", "Um_nik"],
|
||||
startAt: "2025-10-29T12:00:00.000Z",
|
||||
registerAt: "2025-10-24T12:00:00.000Z",
|
||||
duration: 300,
|
||||
members: 2890,
|
||||
statusRegister: "nonreg",
|
||||
},
|
||||
|
||||
// === Контесты, которые еще не начались ===
|
||||
{
|
||||
id: 5,
|
||||
name: "Winter Warmup",
|
||||
authors: ["tourist", "rng_58"],
|
||||
startAt: "2025-11-05T18:00:00.000Z",
|
||||
registerAt: "2025-11-01T18:00:00.000Z",
|
||||
duration: 180,
|
||||
members: 2100,
|
||||
statusRegister: "reg",
|
||||
},
|
||||
{
|
||||
id: 6,
|
||||
name: "Global Coding Cup",
|
||||
authors: ["maroonrk", "kostka"],
|
||||
startAt: "2025-11-12T15:00:00.000Z",
|
||||
registerAt: "2025-11-08T15:00:00.000Z",
|
||||
duration: 240,
|
||||
members: 1520,
|
||||
statusRegister: "nonreg",
|
||||
},
|
||||
];
|
||||
// Берём данные из Redux
|
||||
const contests = useAppSelector((state) => state.contests.contests);
|
||||
const loading = useAppSelector((state) => state.contests.status);
|
||||
const error = useAppSelector((state) => state.contests.error);
|
||||
|
||||
// При загрузке страницы — выставляем активную вкладку и подгружаем контесты
|
||||
useEffect(() => {
|
||||
dispatch(setMenuActivePage("contests"))
|
||||
dispatch(setMenuActivePage("contests"));
|
||||
dispatch(fetchContests({}));
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className=" h-full w-[calc(100%+250px)] box-border p-[20px] pt-[20p]">
|
||||
<div className="h-full box-border">
|
||||
if (loading == "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-[calc(100%+250px)] box-border p-[20px] pt-[20p]">
|
||||
<div className="h-full box-border">
|
||||
<div className="relative flex items-center mb-[20px]">
|
||||
<div className={cn("h-[50px] text-[40px] font-bold text-liquid-white flex items-center")}>
|
||||
Контесты
|
||||
</div>
|
||||
<SecondaryButton
|
||||
onClick={() => { }}
|
||||
onClick={() => {}}
|
||||
text="Создать группу"
|
||||
className="absolute right-0"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="bg-liquid-lighter h-[50px] mb-[20px]">
|
||||
<div className="bg-liquid-lighter h-[50px] mb-[20px]" />
|
||||
|
||||
</div>
|
||||
<ContestsBlock
|
||||
className="mb-[20px]"
|
||||
title="Текущие"
|
||||
contests={contests.filter((contest) => {
|
||||
const endTime =
|
||||
new Date(contest.endsAt).getTime()
|
||||
return endTime >= now.getTime();
|
||||
})}
|
||||
/>
|
||||
|
||||
|
||||
<ContestsBlock className="mb-[20px]" title="Текущие" contests={contests.filter(contest => {
|
||||
const endTime = new Date(contest.startAt).getTime() + contest.duration * 60 * 1000;
|
||||
return endTime >= now.getTime();
|
||||
})} />
|
||||
<ContestsBlock className="mb-[20px]" title="Прошедшие" contests={contests.filter(contest => {
|
||||
const endTime = new Date(contest.startAt).getTime() + contest.duration * 60 * 1000;
|
||||
return endTime < now.getTime();
|
||||
})} />
|
||||
<ContestsBlock
|
||||
className="mb-[20px]"
|
||||
title="Прошедшие"
|
||||
contests={contests.filter((contest) => {
|
||||
const endTime =
|
||||
new Date(contest.endsAt).getTime()
|
||||
return endTime < now.getTime();
|
||||
})}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -2,27 +2,19 @@ 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 Contest {
|
||||
id: number;
|
||||
name: string;
|
||||
authors: string[];
|
||||
startAt: string;
|
||||
registerAt: string;
|
||||
duration: number;
|
||||
members: number;
|
||||
statusRegister: "reg" | "nonreg";
|
||||
}
|
||||
|
||||
interface GroupsBlockProps {
|
||||
|
||||
interface ContestsBlockProps {
|
||||
contests: Contest[];
|
||||
title: string;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
|
||||
const GroupsBlock: FC<GroupsBlockProps> = ({ contests, title, className }) => {
|
||||
const ContestsBlock: FC<ContestsBlockProps> = ({ contests, title, className }) => {
|
||||
|
||||
|
||||
const [active, setActive] = useState<boolean>(title != "Скрытые");
|
||||
@@ -50,7 +42,14 @@ const GroupsBlock: FC<GroupsBlockProps> = ({ contests, title, className }) => {
|
||||
<div className="overflow-hidden">
|
||||
<div className="pb-[10px] pt-[20px]">
|
||||
{
|
||||
contests.map((v, i) => <ContestItem key={i} {...v} type={i % 2 ? "second" : "first"} />)
|
||||
contests.map((v, i) => <ContestItem
|
||||
key={i}
|
||||
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>
|
||||
|
||||
@@ -60,4 +59,4 @@ const GroupsBlock: FC<GroupsBlockProps> = ({ contests, title, className }) => {
|
||||
);
|
||||
};
|
||||
|
||||
export default GroupsBlock;
|
||||
export default ContestsBlock;
|
||||
|
||||
Reference in New Issue
Block a user