Add attempts

This commit is contained in:
Виталий Лавшонок
2025-12-03 13:33:59 +03:00
parent 95f7479375
commit 8f337e6f7b
9 changed files with 6971 additions and 6794 deletions

View File

@@ -2,7 +2,7 @@ import { useEffect } from 'react';
import { useAppDispatch, useAppSelector } from '../../../redux/hooks';
import { setMenuActivePage } from '../../../redux/slices/store';
import { Navigate, Route, Routes, useParams } from 'react-router-dom';
import { fetchContestById } from '../../../redux/slices/contests';
import { fetchContestById, fetchMyAttemptsInContest } from '../../../redux/slices/contests';
import ContestMissions from './Missions';
import Submissions from './Submissions';
@@ -30,6 +30,7 @@ const Contest = () => {
useEffect(() => {
dispatch(fetchContestById(contestIdNumber));
dispatch(fetchMyAttemptsInContest(contestIdNumber));
}, [contestIdNumber]);
return (

View File

@@ -2,6 +2,7 @@ import { cn } from '../../../lib/cn';
import { IconError, IconSuccess } from '../../../assets/icons/missions';
import { useNavigate } from 'react-router-dom';
import { useLocation } from 'react-router-dom';
import { toastWarning } from '../../../lib/toastNotification';
export interface MissionItemProps {
contestId: number;
@@ -11,6 +12,7 @@ export interface MissionItemProps {
memoryLimit?: number;
type?: 'first' | 'second';
status?: 'success' | 'error';
attemptsStarted?: boolean;
}
export function formatMilliseconds(ms: number): string {
@@ -32,6 +34,7 @@ const MissionItem: React.FC<MissionItemProps> = ({
memoryLimit = 256 * 1024 * 1024,
type,
status,
attemptsStarted,
}) => {
const navigate = useNavigate();
const location = useLocation();
@@ -50,7 +53,12 @@ const MissionItem: React.FC<MissionItemProps> = ({
'cursor-pointer brightness-100 hover:brightness-125 transition-all duration-300',
)}
onClick={() => {
navigate(`/mission/${id}?back=${path}&contestId=${contestId}`);
if (attemptsStarted){
navigate(`/mission/${id}?back=${path}&contestId=${contestId}`);
}
else{
toastWarning("Нужно начать попытку")
}
}}
>
<div className="text-[18px] font-bold">#{id}</div>

View File

@@ -1,9 +1,13 @@
import { FC, useEffect } from 'react';
import { FC, useEffect, useState } from 'react';
import MissionItem from './MissionItem';
import {
Contest,
fetchContestById,
fetchContests,
fetchMyAttemptsInContest,
fetchMySubmissions,
setContestStatus,
startContestAttempt,
} from '../../../redux/slices/contests';
import { useAppDispatch, useAppSelector } from '../../../redux/hooks';
import { PrimaryButton } from '../../../components/button/PrimaryButton';
@@ -20,6 +24,8 @@ interface ContestMissionsProps {
contest?: Contest;
}
const ContestMissions: FC<ContestMissionsProps> = ({ contest }) => {
const navigate = useNavigate();
const dispatch = useAppDispatch();
@@ -27,6 +33,44 @@ const ContestMissions: FC<ContestMissionsProps> = ({ contest }) => {
(state) => state.contests.fetchMySubmissions,
);
const attempts = useAppSelector((state) => state.contests.fetchMyAttemptsInContest.attempts);
const [attemptsStarted, setAttemptsStarted] = useState<boolean>(false);
const [time, setTime] = useState(0);
useEffect(() => {
const calc = (time: string) => {
return time != "" && new Date() <= new Date(time);
}
if (attempts.length && calc(attempts[0].expiresAt)){
setAttemptsStarted(true);
const diffMs = new Date(attempts[0].expiresAt).getTime() - new Date().getTime();
const seconds = Math.floor((diffMs / 1000));
setTime(seconds)
const interval = setInterval(() => {
setTime((t) => {
if (t <= 1) {
clearInterval(interval); // остановка таймера
setAttemptsStarted(false); // можно закрыть попытку или уведомить пользователя
return 0;
}
return t - 1;
});
}, 1000);
return () => clearInterval(interval);
}
else
setAttemptsStarted(false);
}, [attempts])
useEffect(() => {
if (contest) dispatch(fetchMySubmissions(contest.id));
}, [contest]);
@@ -53,6 +97,12 @@ const ContestMissions: FC<ContestMissionsProps> = ({ contest }) => {
const totalCount = contest.missions?.length ?? 0;
// форматирование: mm:ss
const minutes = String(Math.floor(time / 60)).padStart(2, "0");
const seconds = String(time % 60).padStart(2, "0");
return (
<div className=" h-screen grid grid-rows-[74px,40px,1fr] p-[20px] gap-[20px]">
<div className="">
@@ -72,17 +122,34 @@ const ContestMissions: FC<ContestMissionsProps> = ({ contest }) => {
Контест #{contest.id}
</span>
</div>
<div>{contest.attemptDurationMinutes ?? 0} минут</div>
<div className="text-liquid-light font-bold text-[18px]">
{attemptsStarted
? `${minutes}:${seconds}`
: `Длительность попытки: ${contest.attemptDurationMinutes ?? 0} минут`}
</div>
</div>
</div>
<div className="flex justify-between items-center">
<div className="text-liquid-white text-[16px] font-bold">{`${solvedCount}/${totalCount} Решено`}</div>
<PrimaryButton
<div className='flex gap-[20px]'>
{ attempts.length == 0 || !attemptsStarted ? <PrimaryButton
onClick={() => {
dispatch(startContestAttempt(contest.id)).unwrap().then(() =>
{
dispatch(fetchMyAttemptsInContest(contest.id));
}
);
}}
text="Начать попытку"
/> : <></>
} <PrimaryButton
onClick={() => {
navigate(`/contest/${contest.id}/submissions`);
}}
text="Мои посылки"
/>
/></div>
</div>
<div className="h-full min-h-0 overflow-y-scroll medium-scrollbar flex flex-col gap-[20px]">
@@ -106,6 +173,7 @@ const ContestMissions: FC<ContestMissionsProps> = ({ contest }) => {
return (
<MissionItem
attemptsStarted={attemptsStarted}
contestId={contest.id}
key={i}
id={v.id}

View File

@@ -33,6 +33,7 @@ const Submissions: FC<SubmissionsProps> = ({ contest }) => {
(state) => state.contests.fetchMySubmissions
);
useEffect(() => {
if (contest && contest.id) dispatch(fetchMySubmissions(contest.id));
}, [contest]);