contests
This commit is contained in:
3
src/assets/icons/auth/account.svg
Normal file
3
src/assets/icons/auth/account.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M15.7105 12.71C16.6909 11.9387 17.4065 10.8809 17.7577 9.68394C18.109 8.48697 18.0784 7.21027 17.6703 6.03147C17.2621 4.85267 16.4967 3.83039 15.4806 3.10686C14.4644 2.38332 13.2479 1.99451 12.0005 1.99451C10.753 1.99451 9.5366 2.38332 8.52041 3.10686C7.50423 3.83039 6.73883 4.85267 6.3307 6.03147C5.92257 7.21027 5.892 8.48697 6.24325 9.68394C6.59449 10.8809 7.31009 11.9387 8.29048 12.71C6.61056 13.383 5.14477 14.4994 4.04938 15.9399C2.95398 17.3805 2.27005 19.0913 2.07048 20.89C2.05604 21.0213 2.0676 21.1542 2.10451 21.2811C2.14142 21.4079 2.20295 21.5263 2.2856 21.6293C2.4525 21.8375 2.69527 21.9708 2.96049 22C3.2257 22.0292 3.49164 21.9518 3.69981 21.7849C3.90798 21.618 4.04131 21.3752 4.07049 21.11C4.29007 19.1552 5.22217 17.3498 6.6887 16.0388C8.15524 14.7278 10.0534 14.003 12.0205 14.003C13.9876 14.003 15.8857 14.7278 17.3523 16.0388C18.8188 17.3498 19.7509 19.1552 19.9705 21.11C19.9977 21.3557 20.1149 21.5827 20.2996 21.747C20.4843 21.9114 20.7233 22.0015 20.9705 22H21.0805C21.3426 21.9698 21.5822 21.8373 21.747 21.6313C21.9119 21.4252 21.9886 21.1624 21.9605 20.9C21.76 19.0962 21.0724 17.381 19.9713 15.9382C18.8703 14.4954 17.3974 13.3795 15.7105 12.71ZM12.0005 12C11.2094 12 10.436 11.7654 9.7782 11.3259C9.12041 10.8864 8.60772 10.2616 8.30497 9.53074C8.00222 8.79983 7.923 7.99557 8.07734 7.21964C8.23168 6.44372 8.61265 5.73099 9.17206 5.17158C9.73147 4.61217 10.4442 4.2312 11.2201 4.07686C11.996 3.92252 12.8003 4.00173 13.5312 4.30448C14.2621 4.60724 14.8868 5.11993 15.3264 5.77772C15.7659 6.43552 16.0005 7.20888 16.0005 8C16.0005 9.06087 15.5791 10.0783 14.8289 10.8284C14.0788 11.5786 13.0614 12 12.0005 12Z" fill="#EDF6F7"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.7 KiB |
@@ -1,3 +1,4 @@
|
||||
import Balloon from "./balloon.svg";
|
||||
import Account from "./account.svg"
|
||||
|
||||
export {Balloon};
|
||||
export {Balloon, Account};
|
||||
70
src/components/button/ReverseButton.tsx
Normal file
70
src/components/button/ReverseButton.tsx
Normal file
@@ -0,0 +1,70 @@
|
||||
import React from "react";
|
||||
import { cn } from "../../lib/cn";
|
||||
|
||||
interface ButtonProps {
|
||||
disabled?: boolean;
|
||||
text?: string;
|
||||
className?: string;
|
||||
onClick: () => void;
|
||||
children?: React.ReactNode;
|
||||
}
|
||||
|
||||
export const ReverseButton: React.FC<ButtonProps> = ({
|
||||
disabled = false,
|
||||
text = "",
|
||||
className,
|
||||
onClick,
|
||||
children,
|
||||
}) => {
|
||||
return (
|
||||
<label
|
||||
className={cn(
|
||||
"grid relative cursor-pointer select-none group w-fit box-border",
|
||||
disabled && "pointer-events-none",
|
||||
className
|
||||
)}
|
||||
>
|
||||
{/* Основной контейнер, */}
|
||||
<div
|
||||
className={cn(
|
||||
"group-active:scale-90 flex items-center justify-center box-border z-10 relative transition-all duration-300",
|
||||
"rounded-[10px]",
|
||||
"group-hover:bg-liquid-darkmain ",
|
||||
"px-[16px] py-[8px]",
|
||||
"bg-liquid-lighter ring-[1px] ring-liquid-darkmain ring-inset",
|
||||
disabled && "bg-liquid-lighter"
|
||||
)}
|
||||
>
|
||||
{/* Скрытый button */}
|
||||
<button
|
||||
className={cn(
|
||||
"absolute opacity-0 -z-10 h-0 w-0",
|
||||
"[&:focus-visible+*]:outline-liquid-brightmain",
|
||||
)}
|
||||
disabled={disabled}
|
||||
onClick={() => { onClick() }}
|
||||
/>
|
||||
|
||||
{/* Граница при выделении через tab */}
|
||||
<div
|
||||
className={cn(
|
||||
"absolute outline-offset-[2.5px] border-[2px] border-transparent outline-[2.5px] outline outline-transparent transition-all duration-300 text-transparent box-border text-[18px] font-bold p-0 ,m-0 leading-[23px]",
|
||||
"rounded-[10px]",
|
||||
"px-[16px] py-[8px]",
|
||||
)}
|
||||
>
|
||||
{children || text}
|
||||
</div>
|
||||
<div
|
||||
className={cn(
|
||||
"transition-all duration-300 text-liquid-brightmain text-[18px] font-bold p-0 m-0 leading-[23px]",
|
||||
"group-hover:text-liquid-white ",
|
||||
disabled && "text-liquid-light"
|
||||
)}
|
||||
>
|
||||
{children || text}
|
||||
</div>
|
||||
</div>
|
||||
</label>
|
||||
);
|
||||
};
|
||||
@@ -36,7 +36,14 @@ const Home = () => {
|
||||
<Route path="articles/*" element={<Articles/>} />
|
||||
<Route path="groups/*" element={<Groups/>} />
|
||||
<Route path="contests/*" element={<Contests/>} />
|
||||
<Route path="*" element={<>{name}<PrimaryButton onClick={() => {dispatch(logout())}}>выйти</PrimaryButton></>} />
|
||||
<Route path="*" element={<>
|
||||
<p>{jwt}</p>
|
||||
<PrimaryButton onClick={() => {if (jwt) navigator.clipboard.writeText(jwt);}} text="скопировать токен" className="pt-[20px]"/>
|
||||
<p className="py-[20px]">{name}</p>
|
||||
<PrimaryButton onClick={() => {dispatch(logout())}}>выйти</PrimaryButton>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
</Routes>
|
||||
</div>
|
||||
{
|
||||
|
||||
189
src/redux/slices/contests.ts
Normal file
189
src/redux/slices/contests.ts
Normal file
@@ -0,0 +1,189 @@
|
||||
import { createSlice, createAsyncThunk, PayloadAction } from "@reduxjs/toolkit";
|
||||
import axios from "../../axios";
|
||||
|
||||
// =====================
|
||||
// Типы
|
||||
// =====================
|
||||
|
||||
export interface Mission {
|
||||
missionId: number;
|
||||
name: string;
|
||||
sortOrder: number;
|
||||
}
|
||||
|
||||
export interface Member {
|
||||
userId: number;
|
||||
username: string;
|
||||
role: string;
|
||||
}
|
||||
|
||||
export interface Contest {
|
||||
id: number;
|
||||
name: string;
|
||||
description: string;
|
||||
scheduleType: string;
|
||||
startsAt: string;
|
||||
endsAt: string;
|
||||
availableFrom: string | null;
|
||||
availableUntil: string | null;
|
||||
attemptDurationMinutes: number | null;
|
||||
groupId: number | null;
|
||||
groupName: string | null;
|
||||
missions: Mission[];
|
||||
articles: any[];
|
||||
members: Member[];
|
||||
}
|
||||
|
||||
interface ContestsResponse {
|
||||
hasNextPage: boolean;
|
||||
contests: Contest[];
|
||||
}
|
||||
|
||||
export interface CreateContestBody {
|
||||
name: string;
|
||||
description: string;
|
||||
scheduleType: "FixedWindow" | "Flexible";
|
||||
startsAt: string;
|
||||
endsAt: string;
|
||||
availableFrom: string | null;
|
||||
availableUntil: string | null;
|
||||
attemptDurationMinutes: number | null;
|
||||
groupId: number | null;
|
||||
missionIds: number[];
|
||||
articleIds: number[];
|
||||
participantIds: number[];
|
||||
organizerIds: number[];
|
||||
}
|
||||
|
||||
// =====================
|
||||
// Состояние
|
||||
// =====================
|
||||
|
||||
interface ContestsState {
|
||||
contests: Contest[];
|
||||
selectedContest: Contest | null;
|
||||
hasNextPage: boolean;
|
||||
status: "idle" | "loading" | "successful" | "failed";
|
||||
error: string | null;
|
||||
}
|
||||
|
||||
const initialState: ContestsState = {
|
||||
contests: [],
|
||||
selectedContest: null,
|
||||
hasNextPage: false,
|
||||
status: "idle",
|
||||
error: null,
|
||||
};
|
||||
|
||||
// =====================
|
||||
// Async Thunks
|
||||
// =====================
|
||||
|
||||
// Получение списка контестов
|
||||
export const fetchContests = createAsyncThunk(
|
||||
"contests/fetchAll",
|
||||
async (
|
||||
params: { page?: number; pageSize?: number; groupId?: number | null } = {},
|
||||
{ rejectWithValue }
|
||||
) => {
|
||||
try {
|
||||
const { page = 0, pageSize = 10, groupId } = params;
|
||||
const response = await axios.get<ContestsResponse>("/contests", {
|
||||
params: { page, pageSize, groupId },
|
||||
});
|
||||
return response.data;
|
||||
} catch (err: any) {
|
||||
return rejectWithValue(err.response?.data?.message || "Failed to fetch contests");
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// Получение одного контеста по ID
|
||||
export const fetchContestById = createAsyncThunk(
|
||||
"contests/fetchById",
|
||||
async (id: number, { rejectWithValue }) => {
|
||||
try {
|
||||
const response = await axios.get<Contest>(`/contests/${id}`);
|
||||
return response.data;
|
||||
} catch (err: any) {
|
||||
return rejectWithValue(err.response?.data?.message || "Failed to fetch contest");
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// Создание нового контеста
|
||||
export const createContest = createAsyncThunk(
|
||||
"contests/create",
|
||||
async (contestData: CreateContestBody, { rejectWithValue }) => {
|
||||
try {
|
||||
const response = await axios.post<Contest>("/contests", contestData);
|
||||
return response.data;
|
||||
} catch (err: any) {
|
||||
return rejectWithValue(err.response?.data?.message || "Failed to create contest");
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// =====================
|
||||
// Slice
|
||||
// =====================
|
||||
|
||||
const contestsSlice = createSlice({
|
||||
name: "contests",
|
||||
initialState,
|
||||
reducers: {
|
||||
clearSelectedContest: (state) => {
|
||||
state.selectedContest = null;
|
||||
},
|
||||
},
|
||||
extraReducers: (builder) => {
|
||||
// fetchContests
|
||||
builder.addCase(fetchContests.pending, (state) => {
|
||||
state.status = "loading";
|
||||
state.error = null;
|
||||
});
|
||||
builder.addCase(fetchContests.fulfilled, (state, action: PayloadAction<ContestsResponse>) => {
|
||||
state.status = "successful";
|
||||
state.contests = action.payload.contests;
|
||||
state.hasNextPage = action.payload.hasNextPage;
|
||||
});
|
||||
builder.addCase(fetchContests.rejected, (state, action: PayloadAction<any>) => {
|
||||
state.status = "failed";
|
||||
state.error = action.payload;
|
||||
});
|
||||
|
||||
// fetchContestById
|
||||
builder.addCase(fetchContestById.pending, (state) => {
|
||||
state.status = "loading";
|
||||
state.error = null;
|
||||
});
|
||||
builder.addCase(fetchContestById.fulfilled, (state, action: PayloadAction<Contest>) => {
|
||||
state.status = "successful";
|
||||
state.selectedContest = action.payload;
|
||||
});
|
||||
builder.addCase(fetchContestById.rejected, (state, action: PayloadAction<any>) => {
|
||||
state.status = "failed";
|
||||
state.error = action.payload;
|
||||
});
|
||||
|
||||
// createContest
|
||||
builder.addCase(createContest.pending, (state) => {
|
||||
state.status = "loading";
|
||||
state.error = null;
|
||||
});
|
||||
builder.addCase(createContest.fulfilled, (state, action: PayloadAction<Contest>) => {
|
||||
state.status = "successful";
|
||||
state.contests.unshift(action.payload);
|
||||
});
|
||||
builder.addCase(createContest.rejected, (state, action: PayloadAction<any>) => {
|
||||
state.status = "failed";
|
||||
state.error = action.payload;
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
// =====================
|
||||
// Экспорты
|
||||
// =====================
|
||||
export const { clearSelectedContest } = contestsSlice.actions;
|
||||
export const contestsReducer = contestsSlice.reducer;
|
||||
@@ -3,6 +3,7 @@ import { authReducer } from "./slices/auth";
|
||||
import { storeReducer } from "./slices/store";
|
||||
import { missionsReducer } from "./slices/missions";
|
||||
import { submitReducer } from "./slices/submit";
|
||||
import { contestsReducer } from "./slices/contests";
|
||||
|
||||
|
||||
// использование
|
||||
@@ -21,6 +22,7 @@ export const store = configureStore({
|
||||
store: storeReducer,
|
||||
missions: missionsReducer,
|
||||
submin: submitReducer,
|
||||
contests: contestsReducer,
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -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