Add attempts
This commit is contained in:
13562
package-lock.json
generated
13562
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -201,6 +201,14 @@ interface ContestsState {
|
|||||||
status: Status;
|
status: Status;
|
||||||
error?: string;
|
error?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
fetchParticipating: {
|
||||||
|
contests: Contest[],
|
||||||
|
hasNextPage: boolean,
|
||||||
|
status: Status,
|
||||||
|
error?: string,
|
||||||
|
};
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const emptyContest: Contest = {
|
const emptyContest: Contest = {
|
||||||
@@ -244,6 +252,13 @@ const initialState: ContestsState = {
|
|||||||
|
|
||||||
checkRegistration: { registered: false, status: 'idle' },
|
checkRegistration: { registered: false, status: 'idle' },
|
||||||
fetchUpcomingEligible: { contests: [], status: 'idle' },
|
fetchUpcomingEligible: { contests: [], status: 'idle' },
|
||||||
|
fetchParticipating: {
|
||||||
|
contests: [],
|
||||||
|
hasNextPage: false,
|
||||||
|
status: 'idle',
|
||||||
|
error: undefined,
|
||||||
|
},
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// =====================
|
// =====================
|
||||||
@@ -252,6 +267,29 @@ const initialState: ContestsState = {
|
|||||||
|
|
||||||
// Existing ----------------------------
|
// Existing ----------------------------
|
||||||
|
|
||||||
|
export const fetchParticipatingContests = createAsyncThunk(
|
||||||
|
'contests/fetchParticipating',
|
||||||
|
async (
|
||||||
|
params: { page?: number; pageSize?: number } = {},
|
||||||
|
{ rejectWithValue },
|
||||||
|
) => {
|
||||||
|
try {
|
||||||
|
const { page = 0, pageSize = 10 } = params;
|
||||||
|
const response = await axios.get<ContestsResponse>(
|
||||||
|
'/contests/participating',
|
||||||
|
{ params: { page, pageSize } }
|
||||||
|
);
|
||||||
|
return response.data;
|
||||||
|
} catch (err: any) {
|
||||||
|
return rejectWithValue(
|
||||||
|
err.response?.data?.message || 'Failed to fetch participating contests'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
export const fetchMySubmissions = createAsyncThunk(
|
export const fetchMySubmissions = createAsyncThunk(
|
||||||
'contests/fetchMySubmissions',
|
'contests/fetchMySubmissions',
|
||||||
async (contestId: number, { rejectWithValue }) => {
|
async (contestId: number, { rejectWithValue }) => {
|
||||||
@@ -889,6 +927,26 @@ const contestsSlice = createSlice({
|
|||||||
state.fetchUpcomingEligible.error = action.payload;
|
state.fetchUpcomingEligible.error = action.payload;
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
||||||
|
builder.addCase(fetchParticipatingContests.pending, (state) => {
|
||||||
|
state.fetchParticipating.status = 'loading';
|
||||||
|
});
|
||||||
|
|
||||||
|
builder.addCase(
|
||||||
|
fetchParticipatingContests.fulfilled,
|
||||||
|
(state, action: PayloadAction<ContestsResponse>) => {
|
||||||
|
state.fetchParticipating.status = 'successful';
|
||||||
|
state.fetchParticipating.contests = action.payload.contests;
|
||||||
|
state.fetchParticipating.hasNextPage = action.payload.hasNextPage;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
builder.addCase(fetchParticipatingContests.rejected, (state, action: any) => {
|
||||||
|
state.fetchParticipating.status = 'failed';
|
||||||
|
state.fetchParticipating.error = action.payload;
|
||||||
|
});
|
||||||
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { useEffect } from 'react';
|
|||||||
import { useAppDispatch, useAppSelector } from '../../../redux/hooks';
|
import { useAppDispatch, useAppSelector } from '../../../redux/hooks';
|
||||||
import { setMenuActivePage } from '../../../redux/slices/store';
|
import { setMenuActivePage } from '../../../redux/slices/store';
|
||||||
import { Navigate, Route, Routes, useParams } from 'react-router-dom';
|
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 ContestMissions from './Missions';
|
||||||
import Submissions from './Submissions';
|
import Submissions from './Submissions';
|
||||||
|
|
||||||
@@ -30,6 +30,7 @@ const Contest = () => {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
dispatch(fetchContestById(contestIdNumber));
|
dispatch(fetchContestById(contestIdNumber));
|
||||||
|
dispatch(fetchMyAttemptsInContest(contestIdNumber));
|
||||||
}, [contestIdNumber]);
|
}, [contestIdNumber]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { cn } from '../../../lib/cn';
|
|||||||
import { IconError, IconSuccess } from '../../../assets/icons/missions';
|
import { IconError, IconSuccess } from '../../../assets/icons/missions';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
import { useLocation } from 'react-router-dom';
|
import { useLocation } from 'react-router-dom';
|
||||||
|
import { toastWarning } from '../../../lib/toastNotification';
|
||||||
|
|
||||||
export interface MissionItemProps {
|
export interface MissionItemProps {
|
||||||
contestId: number;
|
contestId: number;
|
||||||
@@ -11,6 +12,7 @@ export interface MissionItemProps {
|
|||||||
memoryLimit?: number;
|
memoryLimit?: number;
|
||||||
type?: 'first' | 'second';
|
type?: 'first' | 'second';
|
||||||
status?: 'success' | 'error';
|
status?: 'success' | 'error';
|
||||||
|
attemptsStarted?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function formatMilliseconds(ms: number): string {
|
export function formatMilliseconds(ms: number): string {
|
||||||
@@ -32,6 +34,7 @@ const MissionItem: React.FC<MissionItemProps> = ({
|
|||||||
memoryLimit = 256 * 1024 * 1024,
|
memoryLimit = 256 * 1024 * 1024,
|
||||||
type,
|
type,
|
||||||
status,
|
status,
|
||||||
|
attemptsStarted,
|
||||||
}) => {
|
}) => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
@@ -50,7 +53,12 @@ const MissionItem: React.FC<MissionItemProps> = ({
|
|||||||
'cursor-pointer brightness-100 hover:brightness-125 transition-all duration-300',
|
'cursor-pointer brightness-100 hover:brightness-125 transition-all duration-300',
|
||||||
)}
|
)}
|
||||||
onClick={() => {
|
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>
|
<div className="text-[18px] font-bold">#{id}</div>
|
||||||
|
|||||||
@@ -1,9 +1,13 @@
|
|||||||
import { FC, useEffect } from 'react';
|
import { FC, useEffect, useState } from 'react';
|
||||||
import MissionItem from './MissionItem';
|
import MissionItem from './MissionItem';
|
||||||
import {
|
import {
|
||||||
Contest,
|
Contest,
|
||||||
|
fetchContestById,
|
||||||
|
fetchContests,
|
||||||
|
fetchMyAttemptsInContest,
|
||||||
fetchMySubmissions,
|
fetchMySubmissions,
|
||||||
setContestStatus,
|
setContestStatus,
|
||||||
|
startContestAttempt,
|
||||||
} from '../../../redux/slices/contests';
|
} from '../../../redux/slices/contests';
|
||||||
import { useAppDispatch, useAppSelector } from '../../../redux/hooks';
|
import { useAppDispatch, useAppSelector } from '../../../redux/hooks';
|
||||||
import { PrimaryButton } from '../../../components/button/PrimaryButton';
|
import { PrimaryButton } from '../../../components/button/PrimaryButton';
|
||||||
@@ -20,6 +24,8 @@ interface ContestMissionsProps {
|
|||||||
contest?: Contest;
|
contest?: Contest;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
const ContestMissions: FC<ContestMissionsProps> = ({ contest }) => {
|
const ContestMissions: FC<ContestMissionsProps> = ({ contest }) => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
@@ -27,6 +33,44 @@ const ContestMissions: FC<ContestMissionsProps> = ({ contest }) => {
|
|||||||
(state) => state.contests.fetchMySubmissions,
|
(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(() => {
|
useEffect(() => {
|
||||||
if (contest) dispatch(fetchMySubmissions(contest.id));
|
if (contest) dispatch(fetchMySubmissions(contest.id));
|
||||||
}, [contest]);
|
}, [contest]);
|
||||||
@@ -53,6 +97,12 @@ const ContestMissions: FC<ContestMissionsProps> = ({ contest }) => {
|
|||||||
|
|
||||||
const totalCount = contest.missions?.length ?? 0;
|
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 (
|
return (
|
||||||
<div className=" h-screen grid grid-rows-[74px,40px,1fr] p-[20px] gap-[20px]">
|
<div className=" h-screen grid grid-rows-[74px,40px,1fr] p-[20px] gap-[20px]">
|
||||||
<div className="">
|
<div className="">
|
||||||
@@ -72,17 +122,34 @@ const ContestMissions: FC<ContestMissionsProps> = ({ contest }) => {
|
|||||||
Контест #{contest.id}
|
Контест #{contest.id}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</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>
|
</div>
|
||||||
<div className="flex justify-between items-center">
|
<div className="flex justify-between items-center">
|
||||||
<div className="text-liquid-white text-[16px] font-bold">{`${solvedCount}/${totalCount} Решено`}</div>
|
<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={() => {
|
onClick={() => {
|
||||||
navigate(`/contest/${contest.id}/submissions`);
|
navigate(`/contest/${contest.id}/submissions`);
|
||||||
}}
|
}}
|
||||||
text="Мои посылки"
|
text="Мои посылки"
|
||||||
/>
|
/></div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="h-full min-h-0 overflow-y-scroll medium-scrollbar flex flex-col gap-[20px]">
|
<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 (
|
return (
|
||||||
<MissionItem
|
<MissionItem
|
||||||
|
attemptsStarted={attemptsStarted}
|
||||||
contestId={contest.id}
|
contestId={contest.id}
|
||||||
key={i}
|
key={i}
|
||||||
id={v.id}
|
id={v.id}
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ const Submissions: FC<SubmissionsProps> = ({ contest }) => {
|
|||||||
(state) => state.contests.fetchMySubmissions
|
(state) => state.contests.fetchMySubmissions
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (contest && contest.id) dispatch(fetchMySubmissions(contest.id));
|
if (contest && contest.id) dispatch(fetchMySubmissions(contest.id));
|
||||||
}, [contest]);
|
}, [contest]);
|
||||||
|
|||||||
@@ -3,6 +3,12 @@ import { Account } from '../../../assets/icons/auth';
|
|||||||
import { PrimaryButton } from '../../../components/button/PrimaryButton';
|
import { PrimaryButton } from '../../../components/button/PrimaryButton';
|
||||||
import { ReverseButton } from '../../../components/button/ReverseButton';
|
import { ReverseButton } from '../../../components/button/ReverseButton';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
import { toastSuccess, 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 {
|
export interface ContestItemProps {
|
||||||
id: number;
|
id: number;
|
||||||
@@ -10,7 +16,6 @@ export interface ContestItemProps {
|
|||||||
startAt: string;
|
startAt: string;
|
||||||
duration: number;
|
duration: number;
|
||||||
members: number;
|
members: number;
|
||||||
statusRegister: 'reg' | 'nonreg';
|
|
||||||
type: 'first' | 'second';
|
type: 'first' | 'second';
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -53,15 +58,41 @@ const ContestItem: React.FC<ContestItemProps> = ({
|
|||||||
startAt,
|
startAt,
|
||||||
duration,
|
duration,
|
||||||
members,
|
members,
|
||||||
statusRegister,
|
|
||||||
type,
|
type,
|
||||||
}) => {
|
}) => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
|
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
|
|
||||||
const waitTime = new Date(startAt).getTime() - now.getTime();
|
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 (
|
return (
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
@@ -73,6 +104,10 @@ const ContestItem: React.FC<ContestItemProps> = ({
|
|||||||
: ' bg-liquid-background',
|
: ' bg-liquid-background',
|
||||||
)}
|
)}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
|
if (myRole == 'None'){
|
||||||
|
toastWarning("Зарегистрируйтесь на контест");
|
||||||
|
return;
|
||||||
|
}
|
||||||
navigate(`/contest/${id}`);
|
navigate(`/contest/${id}`);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@@ -95,15 +130,18 @@ const ContestItem: React.FC<ContestItemProps> = ({
|
|||||||
<img src={Account} className="h-[24px] w-[24px]" />
|
<img src={Account} className="h-[24px] w-[24px]" />
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center justify-end">
|
<div className="flex items-center justify-end">
|
||||||
{statusRegister == 'reg' ? (
|
{myRole == 'None' ? (
|
||||||
<>
|
<>
|
||||||
{' '}
|
{' '}
|
||||||
<PrimaryButton onClick={() => {}} text="Регистрация" />
|
<PrimaryButton onClick={() => {
|
||||||
|
dispatch(addOrUpdateContestMember({contestId: id, member: {userId: Number(userId), role:"Participant"}}))
|
||||||
|
}} text="Регистрация" />
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
{' '}
|
{' '}
|
||||||
<ReverseButton onClick={() => {}} text="Вы записаны" />
|
<ReverseButton onClick={() => {
|
||||||
|
navigate(`/contest/${id}`);}} text="Войти" />
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { cn } from '../../../lib/cn';
|
|||||||
import { useAppDispatch, useAppSelector } from '../../../redux/hooks';
|
import { useAppDispatch, useAppSelector } from '../../../redux/hooks';
|
||||||
import ContestsBlock from './ContestsBlock';
|
import ContestsBlock from './ContestsBlock';
|
||||||
import { setMenuActivePage } from '../../../redux/slices/store';
|
import { setMenuActivePage } from '../../../redux/slices/store';
|
||||||
import { fetchContests } from '../../../redux/slices/contests';
|
import { fetchContests, fetchMyContests, fetchParticipatingContests } from '../../../redux/slices/contests';
|
||||||
import ModalCreateContest from './ModalCreate';
|
import ModalCreateContest from './ModalCreate';
|
||||||
import Filters from './Filter';
|
import Filters from './Filter';
|
||||||
|
|
||||||
@@ -23,10 +23,13 @@ const Contests = () => {
|
|||||||
);
|
);
|
||||||
const error = useAppSelector((state) => state.contests.fetchContests.error);
|
const error = useAppSelector((state) => state.contests.fetchContests.error);
|
||||||
|
|
||||||
|
|
||||||
// При загрузке страницы — выставляем активную вкладку и подгружаем контесты
|
// При загрузке страницы — выставляем активную вкладку и подгружаем контесты
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
dispatch(setMenuActivePage('contests'));
|
dispatch(setMenuActivePage('contests'));
|
||||||
dispatch(fetchContests({}));
|
dispatch(fetchContests({}));
|
||||||
|
dispatch(fetchParticipatingContests({pageSize:100}));
|
||||||
|
dispatch(fetchMyContests());
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { cn } from '../../../lib/cn';
|
|||||||
import { ChevroneDown } from '../../../assets/icons/groups';
|
import { ChevroneDown } from '../../../assets/icons/groups';
|
||||||
import ContestItem from './ContestItem';
|
import ContestItem from './ContestItem';
|
||||||
import { Contest } from '../../../redux/slices/contests';
|
import { Contest } from '../../../redux/slices/contests';
|
||||||
|
import { useAppSelector } from '../../../redux/hooks';
|
||||||
|
|
||||||
interface ContestsBlockProps {
|
interface ContestsBlockProps {
|
||||||
contests: Contest[];
|
contests: Contest[];
|
||||||
@@ -56,7 +57,6 @@ const ContestsBlock: FC<ContestsBlockProps> = ({
|
|||||||
id={v.id}
|
id={v.id}
|
||||||
name={v.name}
|
name={v.name}
|
||||||
startAt={v.startsAt ?? new Date().toString()}
|
startAt={v.startsAt ?? new Date().toString()}
|
||||||
statusRegister={'reg'}
|
|
||||||
duration={
|
duration={
|
||||||
new Date(v.endsAt ?? new Date().toString()).getTime() -
|
new Date(v.endsAt ?? new Date().toString()).getTime() -
|
||||||
new Date(v.startsAt ?? new Date().toString()).getTime()
|
new Date(v.startsAt ?? new Date().toString()).getTime()
|
||||||
|
|||||||
Reference in New Issue
Block a user