This commit is contained in:
Виталий Лавшонок
2025-11-03 11:57:24 +03:00
parent fbe441c654
commit db8828e32b
9 changed files with 368 additions and 130 deletions

View File

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

View File

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

View File

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