contests update

This commit is contained in:
Виталий Лавшонок
2025-11-06 18:27:28 +03:00
parent 1b39b8c77f
commit 6c92c789d0
5 changed files with 136 additions and 108 deletions

View File

@@ -6,48 +6,71 @@ import { useAppDispatch, useAppSelector } from '../redux/hooks';
import { import {
createContest, createContest,
CreateContestBody, CreateContestBody,
deleteContest,
fetchContestById, fetchContestById,
setContestStatus,
updateContest,
} from '../redux/slices/contests'; } from '../redux/slices/contests';
import DateRangeInput from '../components/input/DateRangeInput'; import DateRangeInput from '../components/input/DateRangeInput';
import { useQuery } from '../hooks/useQuery'; import { useQuery } from '../hooks/useQuery';
import { useNavigate } from 'react-router-dom'; import { Navigate, useNavigate } from 'react-router-dom';
import { fetchMissionById, Mission } from '../redux/slices/missions'; import { fetchMissionById } from '../redux/slices/missions';
import { ReverseButton } from '../components/button/ReverseButton'; import { ReverseButton } from '../components/button/ReverseButton';
interface Mission {
id: number;
name: string;
}
/** /**
* Страница создания / редактирования контеста * Страница создания / редактирования контеста
*/ */
const ContestEditor = () => { const ContestEditor = () => {
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const navigate = useNavigate(); const navigate = useNavigate();
const status = useAppSelector(
(state) => state.contests.createContest.status,
);
const [missionIdInput, setMissionIdInput] = useState<string>('');
const query = useQuery(); const query = useQuery();
const back = query.get('back') ?? undefined; const back = query.get('back') ?? undefined;
const contestId = Number(query.get('contestId') ?? undefined); const contestId = Number(query.get('contestId') ?? undefined);
const refactor = !!contestId; const refactor = !!contestId;
if (!refactor){
return <Navigate to="/home/account/acontest" />
}
const status = useAppSelector(
(state) => state.contests.createContest.status,
);
const [missionIdInput, setMissionIdInput] = useState<string>('');
const [contest, setContest] = useState<CreateContestBody>({ const [contest, setContest] = useState<CreateContestBody>({
name: '', name: '',
description: '', description: '',
scheduleType: 'AlwaysOpen', scheduleType: 'AlwaysOpen',
visibility: 'Public', visibility: 'Public',
startsAt: null, startsAt: '',
endsAt: null, endsAt: '',
attemptDurationMinutes: null, attemptDurationMinutes: 60,
maxAttempts: null, maxAttempts: 1,
allowEarlyFinish: false, allowEarlyFinish: true,
groupId: null, groupIds: [],
missionIds: [], missionIds: [],
articleIds: [], articleIds: [],
}); });
const [missions, setMissions] = useState<Mission[]>([]); const [missions, setMissions] = useState<Mission[]>([]);
const statusDelete = useAppSelector((state) => state.contests.deleteContest.status)
const { contest: contestById, status: contestByIdstatus } = useAppSelector( const { contest: contestById, status: contestByIdstatus } = useAppSelector(
(state) => state.contests.fetchContestById, (state) => state.contests.fetchContestById,
); );
@@ -61,9 +84,15 @@ const ContestEditor = () => {
setContest((prev) => ({ ...prev, [key]: value })); setContest((prev) => ({ ...prev, [key]: value }));
}; };
const handleSubmit = () => { const handleUpdateContest = () => {
dispatch(createContest(contest)); dispatch(updateContest({...contest, contestId}));
}; };
const handleDeleteContest = () => {
dispatch(deleteContest(contestId));
};
const addMission = () => { const addMission = () => {
const id = Number(missionIdInput.trim()); const id = Number(missionIdInput.trim());
@@ -91,6 +120,13 @@ const ContestEditor = () => {
setMissions(missions.filter((v) => v.id != removeId)); setMissions(missions.filter((v) => v.id != removeId));
}; };
useEffect(() => {
if (statusDelete == "successful"){
dispatch(setContestStatus({key: "deleteContest", status: "idle"}))
navigate('/home/account/contests')
}
}, [statusDelete])
useEffect(() => { useEffect(() => {
if (refactor) { if (refactor) {
dispatch(fetchContestById(contestId)); dispatch(fetchContestById(contestId));
@@ -101,10 +137,14 @@ const ContestEditor = () => {
if (refactor && contestByIdstatus == 'successful' && contestById) { if (refactor && contestByIdstatus == 'successful' && contestById) {
setContest({ setContest({
...contestById, ...contestById,
missionIds: [], // groupIds: contestById.groups.map(group => group.groupId),
groupIds: [],
missionIds: contestById.missions?.map(mission => mission.id),
articleIds: contestById.articles?.map(article => article.articleId),
visibility: 'Public', visibility: 'Public',
scheduleType: 'AlwaysOpen', scheduleType: 'AlwaysOpen',
}); });
setMissions(contestById.missions ?? []);
} }
}, [contestById]); }, [contestById]);
@@ -253,27 +293,19 @@ const ContestEditor = () => {
{/* Кнопки */} {/* Кнопки */}
<div className="flex flex-row w-full items-center justify-end mt-[20px] gap-[20px]"> <div className="flex flex-row w-full items-center justify-end mt-[20px] gap-[20px]">
{refactor ? (
<>
<PrimaryButton <PrimaryButton
onClick={handleSubmit} onClick={handleUpdateContest}
text="Сохранить" text="Сохранить"
disabled={status === 'loading'} disabled={status === 'loading'}
/> />
<ReverseButton <ReverseButton
color="error" color="error"
onClick={handleSubmit} onClick={handleDeleteContest}
text="Удалить" text="Удалить"
disabled={status === 'loading'} disabled={statusDelete === 'loading'}
/> />
</>
) : (
<PrimaryButton
onClick={handleSubmit}
text="Создать"
disabled={status === 'loading'}
/>
)}
</div> </div>
</div> </div>
</div> </div>

View File

@@ -11,8 +11,6 @@ export interface Mission {
name: string; name: string;
difficulty: number; difficulty: number;
tags: string[]; tags: string[];
createdAt: string;
updatedAt: string;
timeLimitMilliseconds: number; timeLimitMilliseconds: number;
memoryLimitBytes: number; memoryLimitBytes: number;
statements: string; statements: string;
@@ -32,17 +30,18 @@ export interface Group {
export interface Contest { export interface Contest {
id: number; id: number;
name: string; name: string;
description: string; description?: string;
scheduleType: 'AlwaysOpen' | 'FixedWindow' | 'RollingWindow'; scheduleType: 'AlwaysOpen' | 'FixedWindow' | 'RollingWindow';
startsAt: string; visibility: 'Public' | 'GroupPrivate';
endsAt: string; startsAt?: string;
attemptDurationMinutes: number; endsAt?: string;
maxAttempts: number; attemptDurationMinutes?: number;
allowEarlyFinish: boolean; maxAttempts?: number;
groups: Group[]; allowEarlyFinish?: boolean;
missions: Mission[]; groups?: Group[];
articles: any[]; missions?: Mission[];
members: Member[]; articles?: any[];
members?: Member[];
} }
interface ContestsResponse { interface ContestsResponse {
@@ -52,17 +51,17 @@ interface ContestsResponse {
export interface CreateContestBody { export interface CreateContestBody {
name: string; name: string;
description: string; description?: string;
scheduleType: 'AlwaysOpen' | 'FixedWindow' | 'RollingWindow'; scheduleType: 'AlwaysOpen' | 'FixedWindow' | 'RollingWindow';
visibility: 'Public' | 'GroupPrivate'; visibility: 'Public' | 'GroupPrivate';
startsAt: string; startsAt?: string;
endsAt: string; endsAt?: string;
attemptDurationMinutes: number; attemptDurationMinutes?: number;
maxAttempts: number; maxAttempts?: number;
allowEarlyFinish: boolean; allowEarlyFinish?: boolean;
groupId: number; groupIds?: number[];
missionIds: number[]; missionIds?: number[];
articleIds: number[]; articleIds?: number[];
} }
// ===================== // =====================
@@ -76,38 +75,38 @@ interface ContestsState {
contests: Contest[]; contests: Contest[];
hasNextPage: boolean; hasNextPage: boolean;
status: Status; status: Status;
error: string | null; error?: string;
}; };
fetchContestById: { fetchContestById: {
contest: Contest; contest: Contest;
status: Status; status: Status;
error: string | null; error?: string;
}; };
createContest: { createContest: {
contest: Contest; contest: Contest;
status: Status; status: Status;
error: string | null; error?: string;
}; };
// 🆕 Добавляем updateContest и deleteContest // 🆕 Добавляем updateContest и deleteContest
updateContest: { updateContest: {
contest: Contest; contest: Contest;
status: Status; status: Status;
error: string | null; error?: string;
}; };
deleteContest: { deleteContest: {
status: Status; status: Status;
error: string | null; error?: string;
}; };
fetchMyContests: { fetchMyContests: {
contests: Contest[]; contests: Contest[];
status: Status; status: Status;
error: string | null; error?: string;
}; };
fetchRegisteredContests: { fetchRegisteredContests: {
contests: Contest[]; contests: Contest[];
hasNextPage: boolean; hasNextPage: boolean;
status: Status; status: Status;
error: string | null; error?: string;
}; };
} }
@@ -116,7 +115,7 @@ const initialState: ContestsState = {
contests: [], contests: [],
hasNextPage: false, hasNextPage: false,
status: 'idle', status: 'idle',
error: null, error: undefined,
}, },
fetchContestById: { fetchContestById: {
contest: { contest: {
@@ -124,6 +123,7 @@ const initialState: ContestsState = {
name: '', name: '',
description: '', description: '',
scheduleType: 'AlwaysOpen', scheduleType: 'AlwaysOpen',
visibility: 'Public',
startsAt: '', startsAt: '',
endsAt: '', endsAt: '',
attemptDurationMinutes: 0, attemptDurationMinutes: 0,
@@ -135,7 +135,7 @@ const initialState: ContestsState = {
members: [], members: [],
}, },
status: 'idle', status: 'idle',
error: null, error: undefined,
}, },
createContest: { createContest: {
contest: { contest: {
@@ -143,6 +143,7 @@ const initialState: ContestsState = {
name: '', name: '',
description: '', description: '',
scheduleType: 'AlwaysOpen', scheduleType: 'AlwaysOpen',
visibility: 'Public',
startsAt: '', startsAt: '',
endsAt: '', endsAt: '',
attemptDurationMinutes: 0, attemptDurationMinutes: 0,
@@ -154,7 +155,7 @@ const initialState: ContestsState = {
members: [], members: [],
}, },
status: 'idle', status: 'idle',
error: null, error: undefined,
}, },
updateContest: { updateContest: {
contest: { contest: {
@@ -162,6 +163,7 @@ const initialState: ContestsState = {
name: '', name: '',
description: '', description: '',
scheduleType: 'AlwaysOpen', scheduleType: 'AlwaysOpen',
visibility: 'Public',
startsAt: '', startsAt: '',
endsAt: '', endsAt: '',
attemptDurationMinutes: 0, attemptDurationMinutes: 0,
@@ -173,22 +175,22 @@ const initialState: ContestsState = {
members: [], members: [],
}, },
status: 'idle', status: 'idle',
error: null, error: undefined,
}, },
deleteContest: { deleteContest: {
status: 'idle', status: 'idle',
error: null, error: undefined,
}, },
fetchMyContests: { fetchMyContests: {
contests: [], contests: [],
status: 'idle', status: 'idle',
error: null, error: undefined,
}, },
fetchRegisteredContests: { fetchRegisteredContests: {
contests: [], contests: [],
hasNextPage: false, hasNextPage: false,
status: 'idle', status: 'idle',
error: null, error: undefined,
}, },
}; };
@@ -265,7 +267,7 @@ export const updateContest = createAsyncThunk(
{ rejectWithValue }, { rejectWithValue },
) => { ) => {
try { try {
const response = await axios.patch<Contest>( const response = await axios.put<Contest>(
`/contests/${contestId}`, `/contests/${contestId}`,
contestData, contestData,
); );
@@ -354,7 +356,7 @@ const contestsSlice = createSlice({
// fetchContests // fetchContests
builder.addCase(fetchContests.pending, (state) => { builder.addCase(fetchContests.pending, (state) => {
state.fetchContests.status = 'loading'; state.fetchContests.status = 'loading';
state.fetchContests.error = null; state.fetchContests.error = undefined;
}); });
builder.addCase( builder.addCase(
fetchContests.fulfilled, fetchContests.fulfilled,
@@ -372,7 +374,7 @@ const contestsSlice = createSlice({
// fetchContestById // fetchContestById
builder.addCase(fetchContestById.pending, (state) => { builder.addCase(fetchContestById.pending, (state) => {
state.fetchContestById.status = 'loading'; state.fetchContestById.status = 'loading';
state.fetchContestById.error = null; state.fetchContestById.error = undefined;
}); });
builder.addCase( builder.addCase(
fetchContestById.fulfilled, fetchContestById.fulfilled,
@@ -389,7 +391,7 @@ const contestsSlice = createSlice({
// createContest // createContest
builder.addCase(createContest.pending, (state) => { builder.addCase(createContest.pending, (state) => {
state.createContest.status = 'loading'; state.createContest.status = 'loading';
state.createContest.error = null; state.createContest.error = undefined;
}); });
builder.addCase( builder.addCase(
createContest.fulfilled, createContest.fulfilled,
@@ -406,7 +408,7 @@ const contestsSlice = createSlice({
// 🆕 updateContest // 🆕 updateContest
builder.addCase(updateContest.pending, (state) => { builder.addCase(updateContest.pending, (state) => {
state.updateContest.status = 'loading'; state.updateContest.status = 'loading';
state.updateContest.error = null; state.updateContest.error = undefined;
}); });
builder.addCase( builder.addCase(
updateContest.fulfilled, updateContest.fulfilled,
@@ -423,7 +425,7 @@ const contestsSlice = createSlice({
// 🆕 deleteContest // 🆕 deleteContest
builder.addCase(deleteContest.pending, (state) => { builder.addCase(deleteContest.pending, (state) => {
state.deleteContest.status = 'loading'; state.deleteContest.status = 'loading';
state.deleteContest.error = null; state.deleteContest.error = undefined;
}); });
builder.addCase( builder.addCase(
deleteContest.fulfilled, deleteContest.fulfilled,
@@ -448,7 +450,7 @@ const contestsSlice = createSlice({
// fetchMyContests // fetchMyContests
builder.addCase(fetchMyContests.pending, (state) => { builder.addCase(fetchMyContests.pending, (state) => {
state.fetchMyContests.status = 'loading'; state.fetchMyContests.status = 'loading';
state.fetchMyContests.error = null; state.fetchMyContests.error = undefined;
}); });
builder.addCase( builder.addCase(
fetchMyContests.fulfilled, fetchMyContests.fulfilled,
@@ -465,7 +467,7 @@ const contestsSlice = createSlice({
// fetchRegisteredContests // fetchRegisteredContests
builder.addCase(fetchRegisteredContests.pending, (state) => { builder.addCase(fetchRegisteredContests.pending, (state) => {
state.fetchRegisteredContests.status = 'loading'; state.fetchRegisteredContests.status = 'loading';
state.fetchRegisteredContests.error = null; state.fetchRegisteredContests.error = undefined;
}); });
builder.addCase( builder.addCase(
fetchRegisteredContests.fulfilled, fetchRegisteredContests.fulfilled,

View File

@@ -28,16 +28,6 @@ const Contests = () => {
dispatch(fetchContests({})); dispatch(fetchContests({}));
}, []); }, []);
if (status == 'loading') {
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-[calc(100%+250px)] box-border p-[20px] pt-[20p]"> <div className="h-full w-[calc(100%+250px)] box-border p-[20px] pt-[20p]">
<div className="h-full box-border"> <div className="h-full box-border">
@@ -59,24 +49,29 @@ const Contests = () => {
</div> </div>
<div className="bg-liquid-lighter h-[50px] mb-[20px]" /> <div className="bg-liquid-lighter h-[50px] mb-[20px]" />
{status == 'loading' && <div className="text-liquid-white p-4">Загрузка контестов...</div>}
{status == 'failed' && <div className="text-red-500 p-4">Ошибка: {error}</div>}
{status == 'successful' &&
<>
<ContestsBlock
className="mb-[20px]"
title="Текущие"
contests={contests.filter((contest) => {
const endTime = new Date(contest.endsAt ?? new Date().toDateString()).getTime();
return endTime >= now.getTime();
})}
/>
<ContestsBlock <ContestsBlock
className="mb-[20px]" className="mb-[20px]"
title="Текущие" title="Прошедшие"
contests={contests.filter((contest) => { contests={contests.filter((contest) => {
const endTime = new Date(contest.endsAt).getTime(); const endTime = new Date(contest.endsAt ?? new Date().toDateString()).getTime();
return endTime >= now.getTime(); 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>
<ModalCreateContest <ModalCreateContest

View File

@@ -55,13 +55,13 @@ const ContestsBlock: FC<ContestsBlockProps> = ({
key={i} key={i}
id={v.id} id={v.id}
name={v.name} name={v.name}
startAt={v.startsAt} startAt={v.startsAt ?? new Date().toString()}
statusRegister={'reg'} statusRegister={'reg'}
duration={ duration={
new Date(v.endsAt).getTime() - new Date(v.endsAt ?? new Date().toString()).getTime() -
new Date(v.startsAt).getTime() new Date(v.startsAt ?? new Date().toString()).getTime()
} }
members={v.members.length} members={v.members?.length ?? 0}
type={i % 2 ? 'second' : 'first'} type={i % 2 ? 'second' : 'first'}
/> />
))} ))}

View File

@@ -37,7 +37,7 @@ const ModalCreateContest: FC<ModalCreateContestProps> = ({
attemptDurationMinutes: 0, attemptDurationMinutes: 0,
maxAttempts: 0, maxAttempts: 0,
allowEarlyFinish: false, allowEarlyFinish: false,
groupId: 0, groupIds: [],
missionIds: [], missionIds: [],
articleIds: [], articleIds: [],
}); });
@@ -48,13 +48,12 @@ const ModalCreateContest: FC<ModalCreateContestProps> = ({
useEffect(() => { useEffect(() => {
if (status === 'successful') { if (status === 'successful') {
console.log('navigate');
navigate(
`/contest/create?back=/home/account/contests&contestId=${contest.id}`,
);
dispatch( dispatch(
setContestStatus({ key: 'createContest', status: 'idle' }), setContestStatus({ key: 'createContest', status: 'idle' }),
); );
navigate(
`/contest/create?back=/home/account/contests&contestId=${contest.id}`,
);
} }
}, [status]); }, [status]);