my contests
This commit is contained in:
@@ -70,33 +70,70 @@ export interface CreateContestBody {
|
|||||||
type Status = 'idle' | 'loading' | 'successful' | 'failed';
|
type Status = 'idle' | 'loading' | 'successful' | 'failed';
|
||||||
|
|
||||||
interface ContestsState {
|
interface ContestsState {
|
||||||
contests: Contest[];
|
fetchContests: {
|
||||||
selectedContest: Contest | null;
|
contests: Contest[];
|
||||||
hasNextPage: boolean;
|
hasNextPage: boolean;
|
||||||
statuses: {
|
status: Status;
|
||||||
fetchList: Status;
|
error: string | null;
|
||||||
fetchById: Status;
|
};
|
||||||
create: Status;
|
fetchContestById: {
|
||||||
|
contest: Contest | null;
|
||||||
|
status: Status;
|
||||||
|
error: string | null;
|
||||||
|
};
|
||||||
|
createContest: {
|
||||||
|
contest: Contest | null;
|
||||||
|
status: Status;
|
||||||
|
error: string | null;
|
||||||
|
};
|
||||||
|
fetchMyContests: {
|
||||||
|
contests: Contest[];
|
||||||
|
status: Status;
|
||||||
|
error: string | null;
|
||||||
|
};
|
||||||
|
fetchRegisteredContests: {
|
||||||
|
contests: Contest[];
|
||||||
|
hasNextPage: boolean;
|
||||||
|
status: Status;
|
||||||
|
error: string | null;
|
||||||
};
|
};
|
||||||
error: string | null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const initialState: ContestsState = {
|
const initialState: ContestsState = {
|
||||||
contests: [],
|
fetchContests: {
|
||||||
selectedContest: null,
|
contests: [],
|
||||||
hasNextPage: false,
|
hasNextPage: false,
|
||||||
statuses: {
|
status: 'idle',
|
||||||
fetchList: 'idle',
|
error: null,
|
||||||
fetchById: 'idle',
|
},
|
||||||
create: 'idle',
|
fetchContestById: {
|
||||||
|
contest: null,
|
||||||
|
status: 'idle',
|
||||||
|
error: null,
|
||||||
|
},
|
||||||
|
createContest: {
|
||||||
|
contest: null,
|
||||||
|
status: 'idle',
|
||||||
|
error: null,
|
||||||
|
},
|
||||||
|
fetchMyContests: {
|
||||||
|
contests: [],
|
||||||
|
status: 'idle',
|
||||||
|
error: null,
|
||||||
|
},
|
||||||
|
fetchRegisteredContests: {
|
||||||
|
contests: [],
|
||||||
|
hasNextPage: false,
|
||||||
|
status: 'idle',
|
||||||
|
error: null,
|
||||||
},
|
},
|
||||||
error: null,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// =====================
|
// =====================
|
||||||
// Async Thunks
|
// Async Thunks
|
||||||
// =====================
|
// =====================
|
||||||
|
|
||||||
|
// Все контесты
|
||||||
export const fetchContests = createAsyncThunk(
|
export const fetchContests = createAsyncThunk(
|
||||||
'contests/fetchAll',
|
'contests/fetchAll',
|
||||||
async (
|
async (
|
||||||
@@ -121,6 +158,7 @@ export const fetchContests = createAsyncThunk(
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Контест по ID
|
||||||
export const fetchContestById = createAsyncThunk(
|
export const fetchContestById = createAsyncThunk(
|
||||||
'contests/fetchById',
|
'contests/fetchById',
|
||||||
async (id: number, { rejectWithValue }) => {
|
async (id: number, { rejectWithValue }) => {
|
||||||
@@ -135,6 +173,7 @@ export const fetchContestById = createAsyncThunk(
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Создание контеста
|
||||||
export const createContest = createAsyncThunk(
|
export const createContest = createAsyncThunk(
|
||||||
'contests/create',
|
'contests/create',
|
||||||
async (contestData: CreateContestBody, { rejectWithValue }) => {
|
async (contestData: CreateContestBody, { rejectWithValue }) => {
|
||||||
@@ -152,6 +191,45 @@ export const createContest = createAsyncThunk(
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Контесты, созданные мной
|
||||||
|
export const fetchMyContests = createAsyncThunk(
|
||||||
|
'contests/fetchMyContests',
|
||||||
|
async (_, { rejectWithValue }) => {
|
||||||
|
try {
|
||||||
|
const response = await axios.get<Contest[]>('/contests/my');
|
||||||
|
// Возвращаем просто массив контестов
|
||||||
|
return response.data;
|
||||||
|
} catch (err: any) {
|
||||||
|
return rejectWithValue(
|
||||||
|
err.response?.data?.message || 'Failed to fetch my contests',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
// Контесты, где я зарегистрирован
|
||||||
|
export const fetchRegisteredContests = createAsyncThunk(
|
||||||
|
'contests/fetchRegisteredContests',
|
||||||
|
async (
|
||||||
|
params: { page?: number; pageSize?: number } = {},
|
||||||
|
{ rejectWithValue },
|
||||||
|
) => {
|
||||||
|
try {
|
||||||
|
const { page = 0, pageSize = 10 } = params;
|
||||||
|
const response = await axios.get<ContestsResponse>(
|
||||||
|
'/contests/registered',
|
||||||
|
{ params: { page, pageSize } },
|
||||||
|
);
|
||||||
|
return response.data;
|
||||||
|
} catch (err: any) {
|
||||||
|
return rejectWithValue(
|
||||||
|
err.response?.data?.message ||
|
||||||
|
'Failed to fetch registered contests',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
// =====================
|
// =====================
|
||||||
// Slice
|
// Slice
|
||||||
// =====================
|
// =====================
|
||||||
@@ -161,77 +239,100 @@ const contestsSlice = createSlice({
|
|||||||
initialState,
|
initialState,
|
||||||
reducers: {
|
reducers: {
|
||||||
clearSelectedContest: (state) => {
|
clearSelectedContest: (state) => {
|
||||||
state.selectedContest = null;
|
state.fetchContestById.contest = null;
|
||||||
},
|
|
||||||
setContestStatus: (
|
|
||||||
state,
|
|
||||||
action: PayloadAction<{
|
|
||||||
key: keyof ContestsState['statuses'];
|
|
||||||
status: Status;
|
|
||||||
}>,
|
|
||||||
) => {
|
|
||||||
state.statuses[action.payload.key] = action.payload.status;
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
extraReducers: (builder) => {
|
extraReducers: (builder) => {
|
||||||
// fetchContests
|
// fetchContests
|
||||||
builder.addCase(fetchContests.pending, (state) => {
|
builder.addCase(fetchContests.pending, (state) => {
|
||||||
state.statuses.fetchList = 'loading';
|
state.fetchContests.status = 'loading';
|
||||||
state.error = null;
|
state.fetchContests.error = null;
|
||||||
});
|
});
|
||||||
builder.addCase(
|
builder.addCase(
|
||||||
fetchContests.fulfilled,
|
fetchContests.fulfilled,
|
||||||
(state, action: PayloadAction<ContestsResponse>) => {
|
(state, action: PayloadAction<ContestsResponse>) => {
|
||||||
state.statuses.fetchList = 'successful';
|
state.fetchContests.status = 'successful';
|
||||||
state.contests = action.payload.contests;
|
state.fetchContests.contests = action.payload.contests;
|
||||||
state.hasNextPage = action.payload.hasNextPage;
|
state.fetchContests.hasNextPage = action.payload.hasNextPage;
|
||||||
},
|
|
||||||
);
|
|
||||||
builder.addCase(
|
|
||||||
fetchContests.rejected,
|
|
||||||
(state, action: PayloadAction<any>) => {
|
|
||||||
state.statuses.fetchList = 'failed';
|
|
||||||
state.error = action.payload;
|
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
builder.addCase(fetchContests.rejected, (state, action: any) => {
|
||||||
|
state.fetchContests.status = 'failed';
|
||||||
|
state.fetchContests.error = action.payload;
|
||||||
|
});
|
||||||
|
|
||||||
// fetchContestById
|
// fetchContestById
|
||||||
builder.addCase(fetchContestById.pending, (state) => {
|
builder.addCase(fetchContestById.pending, (state) => {
|
||||||
state.statuses.fetchById = 'loading';
|
state.fetchContestById.status = 'loading';
|
||||||
state.error = null;
|
state.fetchContestById.error = null;
|
||||||
});
|
});
|
||||||
builder.addCase(
|
builder.addCase(
|
||||||
fetchContestById.fulfilled,
|
fetchContestById.fulfilled,
|
||||||
(state, action: PayloadAction<Contest>) => {
|
(state, action: PayloadAction<Contest>) => {
|
||||||
state.statuses.fetchById = 'successful';
|
state.fetchContestById.status = 'successful';
|
||||||
state.selectedContest = action.payload;
|
state.fetchContestById.contest = action.payload;
|
||||||
},
|
|
||||||
);
|
|
||||||
builder.addCase(
|
|
||||||
fetchContestById.rejected,
|
|
||||||
(state, action: PayloadAction<any>) => {
|
|
||||||
state.statuses.fetchById = 'failed';
|
|
||||||
state.error = action.payload;
|
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
builder.addCase(fetchContestById.rejected, (state, action: any) => {
|
||||||
|
state.fetchContestById.status = 'failed';
|
||||||
|
state.fetchContestById.error = action.payload;
|
||||||
|
});
|
||||||
|
|
||||||
// createContest
|
// createContest
|
||||||
builder.addCase(createContest.pending, (state) => {
|
builder.addCase(createContest.pending, (state) => {
|
||||||
state.statuses.create = 'loading';
|
state.createContest.status = 'loading';
|
||||||
state.error = null;
|
state.createContest.error = null;
|
||||||
});
|
});
|
||||||
builder.addCase(
|
builder.addCase(
|
||||||
createContest.fulfilled,
|
createContest.fulfilled,
|
||||||
(state, action: PayloadAction<Contest>) => {
|
(state, action: PayloadAction<Contest>) => {
|
||||||
state.statuses.create = 'successful';
|
state.createContest.status = 'successful';
|
||||||
state.contests.unshift(action.payload);
|
state.createContest.contest = action.payload;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
builder.addCase(createContest.rejected, (state, action: any) => {
|
||||||
|
state.createContest.status = 'failed';
|
||||||
|
state.createContest.error = action.payload;
|
||||||
|
});
|
||||||
|
|
||||||
|
// fetchMyContests
|
||||||
|
// fetchMyContests
|
||||||
|
builder.addCase(fetchMyContests.pending, (state) => {
|
||||||
|
state.fetchMyContests.status = 'loading';
|
||||||
|
state.fetchMyContests.error = null;
|
||||||
|
});
|
||||||
|
builder.addCase(
|
||||||
|
fetchMyContests.fulfilled,
|
||||||
|
(state, action: PayloadAction<Contest[]>) => {
|
||||||
|
state.fetchMyContests.status = 'successful';
|
||||||
|
state.fetchMyContests.contests = action.payload;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
builder.addCase(fetchMyContests.rejected, (state, action: any) => {
|
||||||
|
state.fetchMyContests.status = 'failed';
|
||||||
|
state.fetchMyContests.error = action.payload;
|
||||||
|
});
|
||||||
|
|
||||||
|
// fetchRegisteredContests
|
||||||
|
builder.addCase(fetchRegisteredContests.pending, (state) => {
|
||||||
|
state.fetchRegisteredContests.status = 'loading';
|
||||||
|
state.fetchRegisteredContests.error = null;
|
||||||
|
});
|
||||||
|
builder.addCase(
|
||||||
|
fetchRegisteredContests.fulfilled,
|
||||||
|
(state, action: PayloadAction<ContestsResponse>) => {
|
||||||
|
state.fetchRegisteredContests.status = 'successful';
|
||||||
|
state.fetchRegisteredContests.contests =
|
||||||
|
action.payload.contests;
|
||||||
|
state.fetchRegisteredContests.hasNextPage =
|
||||||
|
action.payload.hasNextPage;
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
builder.addCase(
|
builder.addCase(
|
||||||
createContest.rejected,
|
fetchRegisteredContests.rejected,
|
||||||
(state, action: PayloadAction<any>) => {
|
(state, action: any) => {
|
||||||
state.statuses.create = 'failed';
|
state.fetchRegisteredContests.status = 'failed';
|
||||||
state.error = action.payload;
|
state.fetchRegisteredContests.error = action.payload;
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@@ -241,5 +342,5 @@ const contestsSlice = createSlice({
|
|||||||
// Экспорты
|
// Экспорты
|
||||||
// =====================
|
// =====================
|
||||||
|
|
||||||
export const { clearSelectedContest, setContestStatus } = contestsSlice.actions;
|
export const { clearSelectedContest } = contestsSlice.actions;
|
||||||
export const contestsReducer = contestsSlice.reducer;
|
export const contestsReducer = contestsSlice.reducer;
|
||||||
|
|||||||
@@ -1,60 +1,64 @@
|
|||||||
import { useEffect, useState } from 'react';
|
import { useEffect } from 'react';
|
||||||
import { useAppDispatch, useAppSelector } from '../../../../redux/hooks';
|
import { useAppDispatch, useAppSelector } from '../../../../redux/hooks';
|
||||||
import { setMenuActiveProfilePage } from '../../../../redux/slices/store';
|
import { setMenuActiveProfilePage } from '../../../../redux/slices/store';
|
||||||
import { fetchContests } from '../../../../redux/slices/contests';
|
import {
|
||||||
|
fetchMyContests,
|
||||||
|
fetchRegisteredContests,
|
||||||
|
} from '../../../../redux/slices/contests';
|
||||||
import ContestsBlock from './ContestsBlock';
|
import ContestsBlock from './ContestsBlock';
|
||||||
|
|
||||||
const Contests = () => {
|
const Contests = () => {
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const now = new Date();
|
|
||||||
|
|
||||||
const [modalActive, setModalActive] = useState<boolean>(false);
|
// Redux-состояния
|
||||||
|
const myContestsState = useAppSelector(
|
||||||
// Берём данные из Redux
|
(state) => state.contests.fetchMyContests,
|
||||||
const contests = useAppSelector((state) => state.contests.contests);
|
);
|
||||||
const status = useAppSelector((state) => state.contests.statuses.create);
|
const regContestsState = useAppSelector(
|
||||||
const error = useAppSelector((state) => state.contests.error);
|
(state) => state.contests.fetchRegisteredContests,
|
||||||
|
);
|
||||||
// При загрузке страницы — выставляем активную вкладку и подгружаем контесты
|
|
||||||
useEffect(() => {
|
|
||||||
dispatch(fetchContests({}));
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
|
// При загрузке страницы — выставляем вкладку и подгружаем контесты
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
dispatch(setMenuActiveProfilePage('contests'));
|
dispatch(setMenuActiveProfilePage('contests'));
|
||||||
|
dispatch(fetchMyContests());
|
||||||
|
dispatch(fetchRegisteredContests({}));
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
if (status == 'loading') {
|
console.log(myContestsState);
|
||||||
return (
|
|
||||||
<div className="text-liquid-white p-4">Загрузка контестов...</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (error) {
|
|
||||||
return <div className="text-red-500 p-4">Ошибка: {error}</div>;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="h-full w-full relative flex flex-col text-[60px] font-bold p-[20px]">
|
<div className="h-full w-full relative flex flex-col text-[60px] font-bold p-[20px] gap-[20px]">
|
||||||
<ContestsBlock
|
{/* Контесты, в которых я участвую */}
|
||||||
className="mb-[20px]"
|
<div>
|
||||||
type="reg"
|
<ContestsBlock
|
||||||
title="Предстоящие контесты"
|
className="mb-[20px]"
|
||||||
contests={contests.filter((contest) => {
|
title="Предстоящие контесты"
|
||||||
const endTime = new Date(contest.endsAt).getTime();
|
type="reg"
|
||||||
return endTime >= now.getTime();
|
// contests={regContestsState.contests}
|
||||||
})}
|
contests={[]}
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
<ContestsBlock
|
{/* Контесты, которые я создал */}
|
||||||
className="mb-[20px]"
|
<div>
|
||||||
title="Мои контесты"
|
{myContestsState.status === 'loading' ? (
|
||||||
type="my"
|
<div className="text-liquid-white p-4 text-[24px]">
|
||||||
contests={contests.filter((contest) => {
|
Загрузка ваших контестов...
|
||||||
const endTime = new Date(contest.endsAt).getTime();
|
</div>
|
||||||
return endTime < now.getTime();
|
) : myContestsState.error ? (
|
||||||
})}
|
<div className="text-red-500 p-4 text-[24px]">
|
||||||
/>
|
Ошибка: {myContestsState.error}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<ContestsBlock
|
||||||
|
className="mb-[20px]"
|
||||||
|
title="Мои контесты"
|
||||||
|
type="my"
|
||||||
|
contests={myContestsState.contests}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,20 +1,22 @@
|
|||||||
import { useState, FC } from 'react';
|
import { useState, FC } from 'react';
|
||||||
import { cn } from '../../../../lib/cn';
|
import { cn } from '../../../../lib/cn';
|
||||||
import { ChevroneDown } from '../../../../assets/icons/groups';
|
import { ChevroneDown } from '../../../../assets/icons/groups';
|
||||||
import ContestItem from './ContestItem';
|
import MyContestItem from './MyContestItem';
|
||||||
|
import RegisterContestItem from './RegisterContestItem';
|
||||||
import { Contest } from '../../../../redux/slices/contests';
|
import { Contest } from '../../../../redux/slices/contests';
|
||||||
|
|
||||||
interface ContestsBlockProps {
|
interface ContestsBlockProps {
|
||||||
contests: Contest[];
|
contests: Contest[];
|
||||||
title: string;
|
title: string;
|
||||||
className?: string;
|
className?: string;
|
||||||
type?: string;
|
type?: 'my' | 'reg';
|
||||||
}
|
}
|
||||||
|
|
||||||
const ContestsBlock: FC<ContestsBlockProps> = ({
|
const ContestsBlock: FC<ContestsBlockProps> = ({
|
||||||
contests,
|
contests,
|
||||||
title,
|
title,
|
||||||
className,
|
className,
|
||||||
|
type = 'my',
|
||||||
}) => {
|
}) => {
|
||||||
const [active, setActive] = useState<boolean>(title != 'Скрытые');
|
const [active, setActive] = useState<boolean>(title != 'Скрытые');
|
||||||
|
|
||||||
@@ -51,21 +53,37 @@ const ContestsBlock: FC<ContestsBlockProps> = ({
|
|||||||
>
|
>
|
||||||
<div className="overflow-hidden">
|
<div className="overflow-hidden">
|
||||||
<div className="pb-[10px] pt-[20px]">
|
<div className="pb-[10px] pt-[20px]">
|
||||||
{contests.map((v, i) => (
|
{contests.map((v, i) => {
|
||||||
<ContestItem
|
return type == 'my' ? (
|
||||||
key={i}
|
<MyContestItem
|
||||||
id={v.id}
|
key={i}
|
||||||
name={v.name}
|
id={v.id}
|
||||||
startAt={v.startsAt}
|
name={v.name}
|
||||||
statusRegister={'reg'}
|
startAt={v.startsAt}
|
||||||
duration={
|
statusRegister={'reg'}
|
||||||
new Date(v.endsAt).getTime() -
|
duration={
|
||||||
new Date(v.startsAt).getTime()
|
new Date(v.endsAt).getTime() -
|
||||||
}
|
new Date(v.startsAt).getTime()
|
||||||
members={v.members.length}
|
}
|
||||||
type={i % 2 ? 'second' : 'first'}
|
members={v.members.length}
|
||||||
/>
|
type={i % 2 ? 'second' : 'first'}
|
||||||
))}
|
/>
|
||||||
|
) : (
|
||||||
|
<RegisterContestItem
|
||||||
|
key={i}
|
||||||
|
id={v.id}
|
||||||
|
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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
104
src/views/home/account/contests/MyContestItem.tsx
Normal file
104
src/views/home/account/contests/MyContestItem.tsx
Normal file
@@ -0,0 +1,104 @@
|
|||||||
|
import { cn } from '../../../../lib/cn';
|
||||||
|
import { Account } from '../../../../assets/icons/auth';
|
||||||
|
import { PrimaryButton } from '../../../../components/button/PrimaryButton';
|
||||||
|
import { ReverseButton } from '../../../../components/button/ReverseButton';
|
||||||
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
import { Edit } from '../../../../assets/icons/input';
|
||||||
|
|
||||||
|
export interface ContestItemProps {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
startAt: string;
|
||||||
|
duration: number;
|
||||||
|
members: number;
|
||||||
|
type: 'first' | 'second';
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatDate(dateString: string): string {
|
||||||
|
const date = new Date(dateString);
|
||||||
|
|
||||||
|
const day = date.getDate().toString().padStart(2, '0');
|
||||||
|
const month = (date.getMonth() + 1).toString().padStart(2, '0');
|
||||||
|
const year = date.getFullYear();
|
||||||
|
|
||||||
|
const hours = date.getHours().toString().padStart(2, '0');
|
||||||
|
const minutes = date.getMinutes().toString().padStart(2, '0');
|
||||||
|
|
||||||
|
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,
|
||||||
|
startAt,
|
||||||
|
duration,
|
||||||
|
members,
|
||||||
|
type,
|
||||||
|
}) => {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
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 text-[16px] leading-[20px] cursor-pointer grid grid-cols-[1fr,1fr,110px,110px,110px,24px] items-center font-bold',
|
||||||
|
type == 'first'
|
||||||
|
? ' bg-liquid-lighter'
|
||||||
|
: ' bg-liquid-background',
|
||||||
|
)}
|
||||||
|
onClick={() => {
|
||||||
|
navigate(`/contest/${id}`);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<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>)} */}
|
||||||
|
valavshonok
|
||||||
|
</div>
|
||||||
|
<div className="text-center text-nowrap whitespace-pre-line">
|
||||||
|
{formatDate(startAt)}
|
||||||
|
</div>
|
||||||
|
<div className="text-center">{formatWaitTime(duration)}</div>
|
||||||
|
<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>
|
||||||
|
|
||||||
|
<img
|
||||||
|
className=" h-[24px] w-[24px] hover:bg-liquid-light rounded-[5px] transition-all duration-300"
|
||||||
|
src={Edit}
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
navigate(
|
||||||
|
`/contest/editor?back=/home/account/articles&articleId=${id}`,
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ContestItem;
|
||||||
@@ -14,9 +14,13 @@ const Contests = () => {
|
|||||||
const [modalActive, setModalActive] = useState<boolean>(false);
|
const [modalActive, setModalActive] = useState<boolean>(false);
|
||||||
|
|
||||||
// Берём данные из Redux
|
// Берём данные из Redux
|
||||||
const contests = useAppSelector((state) => state.contests.contests);
|
const contests = useAppSelector(
|
||||||
const status = useAppSelector((state) => state.contests.statuses.create);
|
(state) => state.contests.fetchContests.contests,
|
||||||
const error = useAppSelector((state) => state.contests.error);
|
);
|
||||||
|
const status = useAppSelector(
|
||||||
|
(state) => state.contests.fetchContests.status,
|
||||||
|
);
|
||||||
|
const error = useAppSelector((state) => state.contests.fetchContests.error);
|
||||||
|
|
||||||
// При загрузке страницы — выставляем активную вкладку и подгружаем контесты
|
// При загрузке страницы — выставляем активную вкладку и подгружаем контесты
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|||||||
@@ -18,7 +18,9 @@ const ModalCreateContest: FC<ModalCreateContestProps> = ({
|
|||||||
setActive,
|
setActive,
|
||||||
}) => {
|
}) => {
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const status = useAppSelector((state) => state.contests.statuses.create);
|
const status = useAppSelector(
|
||||||
|
(state) => state.contests.createContest.status,
|
||||||
|
);
|
||||||
|
|
||||||
const [form, setForm] = useState<CreateContestBody>({
|
const [form, setForm] = useState<CreateContestBody>({
|
||||||
name: '',
|
name: '',
|
||||||
|
|||||||
Reference in New Issue
Block a user