contests update
This commit is contained in:
@@ -6,48 +6,71 @@ import { useAppDispatch, useAppSelector } from '../redux/hooks';
|
||||
import {
|
||||
createContest,
|
||||
CreateContestBody,
|
||||
deleteContest,
|
||||
fetchContestById,
|
||||
setContestStatus,
|
||||
updateContest,
|
||||
} from '../redux/slices/contests';
|
||||
import DateRangeInput from '../components/input/DateRangeInput';
|
||||
import { useQuery } from '../hooks/useQuery';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { fetchMissionById, Mission } from '../redux/slices/missions';
|
||||
import { Navigate, useNavigate } from 'react-router-dom';
|
||||
import { fetchMissionById } from '../redux/slices/missions';
|
||||
import { ReverseButton } from '../components/button/ReverseButton';
|
||||
|
||||
|
||||
interface Mission {
|
||||
id: number;
|
||||
name: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Страница создания / редактирования контеста
|
||||
*/
|
||||
const ContestEditor = () => {
|
||||
|
||||
const dispatch = useAppDispatch();
|
||||
const navigate = useNavigate();
|
||||
const status = useAppSelector(
|
||||
(state) => state.contests.createContest.status,
|
||||
);
|
||||
|
||||
const [missionIdInput, setMissionIdInput] = useState<string>('');
|
||||
|
||||
const query = useQuery();
|
||||
const back = query.get('back') ?? undefined;
|
||||
const contestId = Number(query.get('contestId') ?? undefined);
|
||||
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>({
|
||||
name: '',
|
||||
description: '',
|
||||
scheduleType: 'AlwaysOpen',
|
||||
visibility: 'Public',
|
||||
startsAt: null,
|
||||
endsAt: null,
|
||||
attemptDurationMinutes: null,
|
||||
maxAttempts: null,
|
||||
allowEarlyFinish: false,
|
||||
groupId: null,
|
||||
startsAt: '',
|
||||
endsAt: '',
|
||||
attemptDurationMinutes: 60,
|
||||
maxAttempts: 1,
|
||||
allowEarlyFinish: true,
|
||||
groupIds: [],
|
||||
missionIds: [],
|
||||
articleIds: [],
|
||||
});
|
||||
|
||||
const [missions, setMissions] = useState<Mission[]>([]);
|
||||
|
||||
|
||||
const statusDelete = useAppSelector((state) => state.contests.deleteContest.status)
|
||||
|
||||
const { contest: contestById, status: contestByIdstatus } = useAppSelector(
|
||||
(state) => state.contests.fetchContestById,
|
||||
);
|
||||
@@ -61,10 +84,16 @@ const ContestEditor = () => {
|
||||
setContest((prev) => ({ ...prev, [key]: value }));
|
||||
};
|
||||
|
||||
const handleSubmit = () => {
|
||||
dispatch(createContest(contest));
|
||||
const handleUpdateContest = () => {
|
||||
dispatch(updateContest({...contest, contestId}));
|
||||
};
|
||||
|
||||
const handleDeleteContest = () => {
|
||||
dispatch(deleteContest(contestId));
|
||||
};
|
||||
|
||||
|
||||
|
||||
const addMission = () => {
|
||||
const id = Number(missionIdInput.trim());
|
||||
if (!id || contest.missionIds?.includes(id)) return;
|
||||
@@ -91,6 +120,13 @@ const ContestEditor = () => {
|
||||
setMissions(missions.filter((v) => v.id != removeId));
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (statusDelete == "successful"){
|
||||
dispatch(setContestStatus({key: "deleteContest", status: "idle"}))
|
||||
navigate('/home/account/contests')
|
||||
}
|
||||
}, [statusDelete])
|
||||
|
||||
useEffect(() => {
|
||||
if (refactor) {
|
||||
dispatch(fetchContestById(contestId));
|
||||
@@ -101,10 +137,14 @@ const ContestEditor = () => {
|
||||
if (refactor && contestByIdstatus == 'successful' && contestById) {
|
||||
setContest({
|
||||
...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',
|
||||
scheduleType: 'AlwaysOpen',
|
||||
});
|
||||
setMissions(contestById.missions ?? []);
|
||||
}
|
||||
}, [contestById]);
|
||||
|
||||
@@ -253,27 +293,19 @@ const ContestEditor = () => {
|
||||
|
||||
{/* Кнопки */}
|
||||
<div className="flex flex-row w-full items-center justify-end mt-[20px] gap-[20px]">
|
||||
{refactor ? (
|
||||
<>
|
||||
|
||||
<PrimaryButton
|
||||
onClick={handleSubmit}
|
||||
onClick={handleUpdateContest}
|
||||
text="Сохранить"
|
||||
disabled={status === 'loading'}
|
||||
/>
|
||||
<ReverseButton
|
||||
color="error"
|
||||
onClick={handleSubmit}
|
||||
onClick={handleDeleteContest}
|
||||
text="Удалить"
|
||||
disabled={status === 'loading'}
|
||||
disabled={statusDelete === 'loading'}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<PrimaryButton
|
||||
onClick={handleSubmit}
|
||||
text="Создать"
|
||||
disabled={status === 'loading'}
|
||||
/>
|
||||
)}
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -11,8 +11,6 @@ export interface Mission {
|
||||
name: string;
|
||||
difficulty: number;
|
||||
tags: string[];
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
timeLimitMilliseconds: number;
|
||||
memoryLimitBytes: number;
|
||||
statements: string;
|
||||
@@ -32,17 +30,18 @@ export interface Group {
|
||||
export interface Contest {
|
||||
id: number;
|
||||
name: string;
|
||||
description: string;
|
||||
description?: string;
|
||||
scheduleType: 'AlwaysOpen' | 'FixedWindow' | 'RollingWindow';
|
||||
startsAt: string;
|
||||
endsAt: string;
|
||||
attemptDurationMinutes: number;
|
||||
maxAttempts: number;
|
||||
allowEarlyFinish: boolean;
|
||||
groups: Group[];
|
||||
missions: Mission[];
|
||||
articles: any[];
|
||||
members: Member[];
|
||||
visibility: 'Public' | 'GroupPrivate';
|
||||
startsAt?: string;
|
||||
endsAt?: string;
|
||||
attemptDurationMinutes?: number;
|
||||
maxAttempts?: number;
|
||||
allowEarlyFinish?: boolean;
|
||||
groups?: Group[];
|
||||
missions?: Mission[];
|
||||
articles?: any[];
|
||||
members?: Member[];
|
||||
}
|
||||
|
||||
interface ContestsResponse {
|
||||
@@ -52,17 +51,17 @@ interface ContestsResponse {
|
||||
|
||||
export interface CreateContestBody {
|
||||
name: string;
|
||||
description: string;
|
||||
description?: string;
|
||||
scheduleType: 'AlwaysOpen' | 'FixedWindow' | 'RollingWindow';
|
||||
visibility: 'Public' | 'GroupPrivate';
|
||||
startsAt: string;
|
||||
endsAt: string;
|
||||
attemptDurationMinutes: number;
|
||||
maxAttempts: number;
|
||||
allowEarlyFinish: boolean;
|
||||
groupId: number;
|
||||
missionIds: number[];
|
||||
articleIds: number[];
|
||||
startsAt?: string;
|
||||
endsAt?: string;
|
||||
attemptDurationMinutes?: number;
|
||||
maxAttempts?: number;
|
||||
allowEarlyFinish?: boolean;
|
||||
groupIds?: number[];
|
||||
missionIds?: number[];
|
||||
articleIds?: number[];
|
||||
}
|
||||
|
||||
// =====================
|
||||
@@ -76,38 +75,38 @@ interface ContestsState {
|
||||
contests: Contest[];
|
||||
hasNextPage: boolean;
|
||||
status: Status;
|
||||
error: string | null;
|
||||
error?: string;
|
||||
};
|
||||
fetchContestById: {
|
||||
contest: Contest;
|
||||
status: Status;
|
||||
error: string | null;
|
||||
error?: string;
|
||||
};
|
||||
createContest: {
|
||||
contest: Contest;
|
||||
status: Status;
|
||||
error: string | null;
|
||||
error?: string;
|
||||
};
|
||||
// 🆕 Добавляем updateContest и deleteContest
|
||||
updateContest: {
|
||||
contest: Contest;
|
||||
status: Status;
|
||||
error: string | null;
|
||||
error?: string;
|
||||
};
|
||||
deleteContest: {
|
||||
status: Status;
|
||||
error: string | null;
|
||||
error?: string;
|
||||
};
|
||||
fetchMyContests: {
|
||||
contests: Contest[];
|
||||
status: Status;
|
||||
error: string | null;
|
||||
error?: string;
|
||||
};
|
||||
fetchRegisteredContests: {
|
||||
contests: Contest[];
|
||||
hasNextPage: boolean;
|
||||
status: Status;
|
||||
error: string | null;
|
||||
error?: string;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -116,7 +115,7 @@ const initialState: ContestsState = {
|
||||
contests: [],
|
||||
hasNextPage: false,
|
||||
status: 'idle',
|
||||
error: null,
|
||||
error: undefined,
|
||||
},
|
||||
fetchContestById: {
|
||||
contest: {
|
||||
@@ -124,6 +123,7 @@ const initialState: ContestsState = {
|
||||
name: '',
|
||||
description: '',
|
||||
scheduleType: 'AlwaysOpen',
|
||||
visibility: 'Public',
|
||||
startsAt: '',
|
||||
endsAt: '',
|
||||
attemptDurationMinutes: 0,
|
||||
@@ -135,7 +135,7 @@ const initialState: ContestsState = {
|
||||
members: [],
|
||||
},
|
||||
status: 'idle',
|
||||
error: null,
|
||||
error: undefined,
|
||||
},
|
||||
createContest: {
|
||||
contest: {
|
||||
@@ -143,6 +143,7 @@ const initialState: ContestsState = {
|
||||
name: '',
|
||||
description: '',
|
||||
scheduleType: 'AlwaysOpen',
|
||||
visibility: 'Public',
|
||||
startsAt: '',
|
||||
endsAt: '',
|
||||
attemptDurationMinutes: 0,
|
||||
@@ -154,7 +155,7 @@ const initialState: ContestsState = {
|
||||
members: [],
|
||||
},
|
||||
status: 'idle',
|
||||
error: null,
|
||||
error: undefined,
|
||||
},
|
||||
updateContest: {
|
||||
contest: {
|
||||
@@ -162,6 +163,7 @@ const initialState: ContestsState = {
|
||||
name: '',
|
||||
description: '',
|
||||
scheduleType: 'AlwaysOpen',
|
||||
visibility: 'Public',
|
||||
startsAt: '',
|
||||
endsAt: '',
|
||||
attemptDurationMinutes: 0,
|
||||
@@ -173,22 +175,22 @@ const initialState: ContestsState = {
|
||||
members: [],
|
||||
},
|
||||
status: 'idle',
|
||||
error: null,
|
||||
error: undefined,
|
||||
},
|
||||
deleteContest: {
|
||||
status: 'idle',
|
||||
error: null,
|
||||
error: undefined,
|
||||
},
|
||||
fetchMyContests: {
|
||||
contests: [],
|
||||
status: 'idle',
|
||||
error: null,
|
||||
error: undefined,
|
||||
},
|
||||
fetchRegisteredContests: {
|
||||
contests: [],
|
||||
hasNextPage: false,
|
||||
status: 'idle',
|
||||
error: null,
|
||||
error: undefined,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -265,7 +267,7 @@ export const updateContest = createAsyncThunk(
|
||||
{ rejectWithValue },
|
||||
) => {
|
||||
try {
|
||||
const response = await axios.patch<Contest>(
|
||||
const response = await axios.put<Contest>(
|
||||
`/contests/${contestId}`,
|
||||
contestData,
|
||||
);
|
||||
@@ -354,7 +356,7 @@ const contestsSlice = createSlice({
|
||||
// fetchContests
|
||||
builder.addCase(fetchContests.pending, (state) => {
|
||||
state.fetchContests.status = 'loading';
|
||||
state.fetchContests.error = null;
|
||||
state.fetchContests.error = undefined;
|
||||
});
|
||||
builder.addCase(
|
||||
fetchContests.fulfilled,
|
||||
@@ -372,7 +374,7 @@ const contestsSlice = createSlice({
|
||||
// fetchContestById
|
||||
builder.addCase(fetchContestById.pending, (state) => {
|
||||
state.fetchContestById.status = 'loading';
|
||||
state.fetchContestById.error = null;
|
||||
state.fetchContestById.error = undefined;
|
||||
});
|
||||
builder.addCase(
|
||||
fetchContestById.fulfilled,
|
||||
@@ -389,7 +391,7 @@ const contestsSlice = createSlice({
|
||||
// createContest
|
||||
builder.addCase(createContest.pending, (state) => {
|
||||
state.createContest.status = 'loading';
|
||||
state.createContest.error = null;
|
||||
state.createContest.error = undefined;
|
||||
});
|
||||
builder.addCase(
|
||||
createContest.fulfilled,
|
||||
@@ -406,7 +408,7 @@ const contestsSlice = createSlice({
|
||||
// 🆕 updateContest
|
||||
builder.addCase(updateContest.pending, (state) => {
|
||||
state.updateContest.status = 'loading';
|
||||
state.updateContest.error = null;
|
||||
state.updateContest.error = undefined;
|
||||
});
|
||||
builder.addCase(
|
||||
updateContest.fulfilled,
|
||||
@@ -423,7 +425,7 @@ const contestsSlice = createSlice({
|
||||
// 🆕 deleteContest
|
||||
builder.addCase(deleteContest.pending, (state) => {
|
||||
state.deleteContest.status = 'loading';
|
||||
state.deleteContest.error = null;
|
||||
state.deleteContest.error = undefined;
|
||||
});
|
||||
builder.addCase(
|
||||
deleteContest.fulfilled,
|
||||
@@ -448,7 +450,7 @@ const contestsSlice = createSlice({
|
||||
// fetchMyContests
|
||||
builder.addCase(fetchMyContests.pending, (state) => {
|
||||
state.fetchMyContests.status = 'loading';
|
||||
state.fetchMyContests.error = null;
|
||||
state.fetchMyContests.error = undefined;
|
||||
});
|
||||
builder.addCase(
|
||||
fetchMyContests.fulfilled,
|
||||
@@ -465,7 +467,7 @@ const contestsSlice = createSlice({
|
||||
// fetchRegisteredContests
|
||||
builder.addCase(fetchRegisteredContests.pending, (state) => {
|
||||
state.fetchRegisteredContests.status = 'loading';
|
||||
state.fetchRegisteredContests.error = null;
|
||||
state.fetchRegisteredContests.error = undefined;
|
||||
});
|
||||
builder.addCase(
|
||||
fetchRegisteredContests.fulfilled,
|
||||
|
||||
@@ -28,16 +28,6 @@ const Contests = () => {
|
||||
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 (
|
||||
<div className="h-full w-[calc(100%+250px)] box-border p-[20px] pt-[20p]">
|
||||
<div className="h-full box-border">
|
||||
@@ -59,12 +49,15 @@ const Contests = () => {
|
||||
</div>
|
||||
|
||||
<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).getTime();
|
||||
const endTime = new Date(contest.endsAt ?? new Date().toDateString()).getTime();
|
||||
return endTime >= now.getTime();
|
||||
})}
|
||||
/>
|
||||
@@ -73,10 +66,12 @@ const Contests = () => {
|
||||
className="mb-[20px]"
|
||||
title="Прошедшие"
|
||||
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();
|
||||
})}
|
||||
/>
|
||||
</>
|
||||
}
|
||||
</div>
|
||||
|
||||
<ModalCreateContest
|
||||
|
||||
@@ -55,13 +55,13 @@ const ContestsBlock: FC<ContestsBlockProps> = ({
|
||||
key={i}
|
||||
id={v.id}
|
||||
name={v.name}
|
||||
startAt={v.startsAt}
|
||||
startAt={v.startsAt ?? new Date().toString()}
|
||||
statusRegister={'reg'}
|
||||
duration={
|
||||
new Date(v.endsAt).getTime() -
|
||||
new Date(v.startsAt).getTime()
|
||||
new Date(v.endsAt ?? new Date().toString()).getTime() -
|
||||
new Date(v.startsAt ?? new Date().toString()).getTime()
|
||||
}
|
||||
members={v.members.length}
|
||||
members={v.members?.length ?? 0}
|
||||
type={i % 2 ? 'second' : 'first'}
|
||||
/>
|
||||
))}
|
||||
|
||||
@@ -37,7 +37,7 @@ const ModalCreateContest: FC<ModalCreateContestProps> = ({
|
||||
attemptDurationMinutes: 0,
|
||||
maxAttempts: 0,
|
||||
allowEarlyFinish: false,
|
||||
groupId: 0,
|
||||
groupIds: [],
|
||||
missionIds: [],
|
||||
articleIds: [],
|
||||
});
|
||||
@@ -48,13 +48,12 @@ const ModalCreateContest: FC<ModalCreateContestProps> = ({
|
||||
|
||||
useEffect(() => {
|
||||
if (status === 'successful') {
|
||||
console.log('navigate');
|
||||
navigate(
|
||||
`/contest/create?back=/home/account/contests&contestId=${contest.id}`,
|
||||
);
|
||||
dispatch(
|
||||
setContestStatus({ key: 'createContest', status: 'idle' }),
|
||||
);
|
||||
navigate(
|
||||
`/contest/create?back=/home/account/contests&contestId=${contest.id}`,
|
||||
);
|
||||
}
|
||||
}, [status]);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user