dont work
This commit is contained in:
@@ -8,6 +8,7 @@ import Home from './pages/Home';
|
|||||||
import Mission from './pages/Mission';
|
import Mission from './pages/Mission';
|
||||||
import ArticleEditor from './pages/ArticleEditor';
|
import ArticleEditor from './pages/ArticleEditor';
|
||||||
import Article from './pages/Article';
|
import Article from './pages/Article';
|
||||||
|
import ContestEditor from './pages/ContestEditor';
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
return (
|
return (
|
||||||
@@ -20,6 +21,10 @@ function App() {
|
|||||||
path="/article/create/*"
|
path="/article/create/*"
|
||||||
element={<ArticleEditor />}
|
element={<ArticleEditor />}
|
||||||
/>
|
/>
|
||||||
|
<Route
|
||||||
|
path="/contest/create/*"
|
||||||
|
element={<ContestEditor />}
|
||||||
|
/>
|
||||||
<Route path="/article/:articleId" element={<Article />} />
|
<Route path="/article/:articleId" element={<Article />} />
|
||||||
<Route path="*" element={<Home />} />
|
<Route path="*" element={<Home />} />
|
||||||
</Routes>
|
</Routes>
|
||||||
|
|||||||
336
src/pages/ContestEditor.tsx
Normal file
336
src/pages/ContestEditor.tsx
Normal file
@@ -0,0 +1,336 @@
|
|||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
import Header from '../views/articleeditor/Header';
|
||||||
|
import { PrimaryButton } from '../components/button/PrimaryButton';
|
||||||
|
import { Input } from '../components/input/Input';
|
||||||
|
import { useAppDispatch, useAppSelector } from '../redux/hooks';
|
||||||
|
import {
|
||||||
|
createContest,
|
||||||
|
CreateContestBody,
|
||||||
|
fetchContestById,
|
||||||
|
} 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 { ReverseButton } from '../components/button/ReverseButton';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Страница создания / редактирования контеста
|
||||||
|
*/
|
||||||
|
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;
|
||||||
|
|
||||||
|
const [contest, setContest] = useState<CreateContestBody>({
|
||||||
|
name: '',
|
||||||
|
description: '',
|
||||||
|
scheduleType: 'AlwaysOpen',
|
||||||
|
visibility: 'Public',
|
||||||
|
startsAt: null,
|
||||||
|
endsAt: null,
|
||||||
|
attemptDurationMinutes: null,
|
||||||
|
maxAttempts: null,
|
||||||
|
allowEarlyFinish: false,
|
||||||
|
groupId: null,
|
||||||
|
missionIds: [],
|
||||||
|
articleIds: [],
|
||||||
|
});
|
||||||
|
|
||||||
|
const [missions, setMissions] = useState<Mission[]>([]);
|
||||||
|
|
||||||
|
const { contest: contestById, status: contestByIdstatus } = useAppSelector(
|
||||||
|
(state) => state.contests.fetchContestById,
|
||||||
|
);
|
||||||
|
console.log(contestByIdstatus, contestById);
|
||||||
|
useEffect(() => {
|
||||||
|
if (status === 'successful') {
|
||||||
|
}
|
||||||
|
}, [status]);
|
||||||
|
|
||||||
|
const handleChange = (key: keyof CreateContestBody, value: any) => {
|
||||||
|
setContest((prev) => ({ ...prev, [key]: value }));
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSubmit = () => {
|
||||||
|
dispatch(createContest(contest));
|
||||||
|
};
|
||||||
|
|
||||||
|
const addMission = () => {
|
||||||
|
const id = Number(missionIdInput.trim());
|
||||||
|
if (!id || contest.missionIds?.includes(id)) return;
|
||||||
|
dispatch(fetchMissionById(id))
|
||||||
|
.unwrap()
|
||||||
|
.then((mission) => {
|
||||||
|
setMissions((prev) => [...prev, mission]);
|
||||||
|
setContest((prev) => ({
|
||||||
|
...prev,
|
||||||
|
missionIds: [...(prev.missionIds ?? []), id],
|
||||||
|
}));
|
||||||
|
setMissionIdInput('');
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
console.error('Ошибка при загрузке миссии:', err);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const removeMission = (removeId: number) => {
|
||||||
|
setContest({
|
||||||
|
...contest,
|
||||||
|
missionIds: contest.missionIds?.filter((v) => v !== removeId),
|
||||||
|
});
|
||||||
|
setMissions(missions.filter((v) => v.id != removeId));
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (refactor) {
|
||||||
|
dispatch(fetchContestById(contestId));
|
||||||
|
}
|
||||||
|
}, [refactor]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (refactor && contestByIdstatus == 'successful' && contestById) {
|
||||||
|
setContest({
|
||||||
|
...contestById,
|
||||||
|
missionIds: [],
|
||||||
|
visibility: 'Public',
|
||||||
|
scheduleType: 'AlwaysOpen',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [contestById]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="h-screen grid grid-rows-[60px,1fr] text-liquid-white">
|
||||||
|
<Header backClick={() => navigate(back || '/home/contests')} />
|
||||||
|
|
||||||
|
<div className="grid grid-cols-2 h-full min-h-0">
|
||||||
|
{/* Левая панешь */}
|
||||||
|
<div className="overflow-y-auto min-h-0 overflow-hidden">
|
||||||
|
<div className="p-4 border-r border-gray-700 flex flex-col h-full">
|
||||||
|
<h2 className="text-lg font-semibold mb-3 text-gray-100"></h2>
|
||||||
|
|
||||||
|
<div className="">
|
||||||
|
<div className="font-bold text-[30px] mb-[10px]">
|
||||||
|
{refactor
|
||||||
|
? `Редактирвоание контеста #${contestId} \"${contestById?.name}\"`
|
||||||
|
: 'Создать контест'}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Input
|
||||||
|
name="name"
|
||||||
|
type="text"
|
||||||
|
label="Название"
|
||||||
|
className="mt-[10px]"
|
||||||
|
placeholder="Введите название"
|
||||||
|
onChange={(v) => handleChange('name', v)}
|
||||||
|
defaultState={contest.name ?? ''}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Input
|
||||||
|
name="description"
|
||||||
|
type="text"
|
||||||
|
label="Описание"
|
||||||
|
className="mt-[10px]"
|
||||||
|
placeholder="Введите описание"
|
||||||
|
onChange={(v) => handleChange('description', v)}
|
||||||
|
defaultState={contest.description ?? ''}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-2 gap-[10px] mt-[10px]">
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm mb-1">
|
||||||
|
Тип расписания
|
||||||
|
</label>
|
||||||
|
<select
|
||||||
|
className="w-full p-2 rounded-md bg-liquid-darker border border-liquid-lighter"
|
||||||
|
value={contest.scheduleType}
|
||||||
|
onChange={(e) =>
|
||||||
|
handleChange(
|
||||||
|
'scheduleType',
|
||||||
|
e.target
|
||||||
|
.value as CreateContestBody['scheduleType'],
|
||||||
|
)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<option value="AlwaysOpen">
|
||||||
|
Всегда открыт
|
||||||
|
</option>
|
||||||
|
<option value="FixedWindow">
|
||||||
|
Фиксированные даты
|
||||||
|
</option>
|
||||||
|
<option value="RollingWindow">
|
||||||
|
Скользящее окно
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm mb-1">
|
||||||
|
Видимость
|
||||||
|
</label>
|
||||||
|
<select
|
||||||
|
className="w-full p-2 rounded-md bg-liquid-darker border border-liquid-lighter"
|
||||||
|
value={contest.visibility}
|
||||||
|
onChange={(e) =>
|
||||||
|
handleChange(
|
||||||
|
'visibility',
|
||||||
|
e.target
|
||||||
|
.value as CreateContestBody['visibility'],
|
||||||
|
)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<option value="Public">
|
||||||
|
Публичный
|
||||||
|
</option>
|
||||||
|
<option value="GroupPrivate">
|
||||||
|
Групповой
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Даты начала и конца */}
|
||||||
|
<div className="grid grid-cols-2 gap-[10px] mt-[10px]">
|
||||||
|
<DateRangeInput
|
||||||
|
startValue={contest.startsAt || ''}
|
||||||
|
endValue={contest.endsAt || ''}
|
||||||
|
onChange={handleChange}
|
||||||
|
className="mt-[10px]"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Продолжительность и лимиты */}
|
||||||
|
<div className="grid grid-cols-2 gap-[10px] mt-[10px]">
|
||||||
|
<Input
|
||||||
|
name="attemptDurationMinutes"
|
||||||
|
type="number"
|
||||||
|
label="Длительность попытки (мин)"
|
||||||
|
placeholder="Например: 60"
|
||||||
|
onChange={(v) =>
|
||||||
|
handleChange(
|
||||||
|
'attemptDurationMinutes',
|
||||||
|
Number(v),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
name="maxAttempts"
|
||||||
|
type="number"
|
||||||
|
label="Макс. попыток"
|
||||||
|
placeholder="Например: 3"
|
||||||
|
onChange={(v) =>
|
||||||
|
handleChange('maxAttempts', Number(v))
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Разрешить раннее завершение */}
|
||||||
|
<div className="flex items-center gap-[10px] mt-[15px]">
|
||||||
|
<input
|
||||||
|
id="allowEarlyFinish"
|
||||||
|
type="checkbox"
|
||||||
|
checked={!!contest.allowEarlyFinish}
|
||||||
|
onChange={(e) =>
|
||||||
|
handleChange(
|
||||||
|
'allowEarlyFinish',
|
||||||
|
e.target.checked,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<label htmlFor="allowEarlyFinish">
|
||||||
|
Разрешить раннее завершение
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Кнопки */}
|
||||||
|
<div className="flex flex-row w-full items-center justify-end mt-[20px] gap-[20px]">
|
||||||
|
{refactor ? (
|
||||||
|
<>
|
||||||
|
<PrimaryButton
|
||||||
|
onClick={handleSubmit}
|
||||||
|
text="Сохранить"
|
||||||
|
disabled={status === 'loading'}
|
||||||
|
/>
|
||||||
|
<ReverseButton
|
||||||
|
color="error"
|
||||||
|
onClick={handleSubmit}
|
||||||
|
text="Удалить"
|
||||||
|
disabled={status === 'loading'}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<PrimaryButton
|
||||||
|
onClick={handleSubmit}
|
||||||
|
text="Создать"
|
||||||
|
disabled={status === 'loading'}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Правая панель */}
|
||||||
|
<div className="overflow-y-auto min-h-0 overflow-hidden">
|
||||||
|
<div className="p-4 border-r border-gray-700 flex flex-col h-full">
|
||||||
|
<h2 className="text-lg font-semibold mb-3 text-gray-100"></h2>
|
||||||
|
|
||||||
|
{/* Блок для тегов */}
|
||||||
|
<div className="mt-[20px] max-w-[600px]">
|
||||||
|
<div className="grid grid-cols-[1fr,140px] items-end gap-2">
|
||||||
|
<Input
|
||||||
|
name="missionId"
|
||||||
|
autocomplete="missionId"
|
||||||
|
className="mt-[20px] max-w-[600px]"
|
||||||
|
type="number"
|
||||||
|
label="ID миссии"
|
||||||
|
onChange={(v) => {
|
||||||
|
setMissionIdInput(v);
|
||||||
|
}}
|
||||||
|
defaultState={missionIdInput}
|
||||||
|
placeholder="458"
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
if (e.key == 'Enter') addMission();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<PrimaryButton
|
||||||
|
onClick={addMission}
|
||||||
|
text="Добавить"
|
||||||
|
className="h-[40px] w-[140px]"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-wrap gap-[10px] mt-2">
|
||||||
|
{missions.map((v, i) => (
|
||||||
|
<div
|
||||||
|
key={i}
|
||||||
|
className="flex items-center gap-1 bg-liquid-lighter px-3 py-1 rounded-full"
|
||||||
|
>
|
||||||
|
<span>{v.id}</span>
|
||||||
|
<span>{v.name}</span>
|
||||||
|
<button
|
||||||
|
onClick={() => removeMission(v.id)}
|
||||||
|
className="text-liquid-red font-bold ml-[5px]"
|
||||||
|
>
|
||||||
|
×
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ContestEditor;
|
||||||
@@ -15,7 +15,7 @@ export interface Mission {
|
|||||||
updatedAt: string;
|
updatedAt: string;
|
||||||
timeLimitMilliseconds: number;
|
timeLimitMilliseconds: number;
|
||||||
memoryLimitBytes: number;
|
memoryLimitBytes: number;
|
||||||
statements: null;
|
statements: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Member {
|
export interface Member {
|
||||||
@@ -24,18 +24,22 @@ export interface Member {
|
|||||||
role: string;
|
role: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface Group {
|
||||||
|
groupId: number;
|
||||||
|
groupName: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface Contest {
|
export interface Contest {
|
||||||
id: number;
|
id: number;
|
||||||
name: string;
|
name: string;
|
||||||
description: string;
|
description: string;
|
||||||
scheduleType: string;
|
scheduleType: 'AlwaysOpen' | 'FixedWindow' | 'RollingWindow';
|
||||||
startsAt: string;
|
startsAt: string;
|
||||||
endsAt: string;
|
endsAt: string;
|
||||||
attemptDurationMinutes: number | null;
|
attemptDurationMinutes: number;
|
||||||
maxAttempts: number | null;
|
maxAttempts: number;
|
||||||
allowEarlyFinish: boolean | null;
|
allowEarlyFinish: boolean;
|
||||||
groupId: number | null;
|
groups: Group[];
|
||||||
groupName: string | null;
|
|
||||||
missions: Mission[];
|
missions: Mission[];
|
||||||
articles: any[];
|
articles: any[];
|
||||||
members: Member[];
|
members: Member[];
|
||||||
@@ -47,20 +51,18 @@ interface ContestsResponse {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface CreateContestBody {
|
export interface CreateContestBody {
|
||||||
name?: string | null;
|
name: string;
|
||||||
description?: string | null;
|
description: string;
|
||||||
scheduleType: 'AlwaysOpen' | 'FixedWindow' | 'RollingWindow';
|
scheduleType: 'AlwaysOpen' | 'FixedWindow' | 'RollingWindow';
|
||||||
visibility: 'Public' | 'GroupPrivate';
|
visibility: 'Public' | 'GroupPrivate';
|
||||||
startsAt?: string | null;
|
startsAt: string;
|
||||||
endsAt?: string | null;
|
endsAt: string;
|
||||||
attemptDurationMinutes?: number | null;
|
attemptDurationMinutes: number;
|
||||||
maxAttempts?: number | null;
|
maxAttempts: number;
|
||||||
allowEarlyFinish?: boolean | null;
|
allowEarlyFinish: boolean;
|
||||||
groupId?: number | null;
|
groupId: number;
|
||||||
missionIds?: number[] | null;
|
missionIds: number[];
|
||||||
articleIds?: number[] | null;
|
articleIds: number[];
|
||||||
participantIds?: number[] | null;
|
|
||||||
organizerIds?: number[] | null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// =====================
|
// =====================
|
||||||
@@ -77,12 +79,22 @@ interface ContestsState {
|
|||||||
error: string | null;
|
error: string | null;
|
||||||
};
|
};
|
||||||
fetchContestById: {
|
fetchContestById: {
|
||||||
contest: Contest | null;
|
contest: Contest;
|
||||||
status: Status;
|
status: Status;
|
||||||
error: string | null;
|
error: string | null;
|
||||||
};
|
};
|
||||||
createContest: {
|
createContest: {
|
||||||
contest: Contest | null;
|
contest: Contest;
|
||||||
|
status: Status;
|
||||||
|
error: string | null;
|
||||||
|
};
|
||||||
|
// 🆕 Добавляем updateContest и deleteContest
|
||||||
|
updateContest: {
|
||||||
|
contest: Contest;
|
||||||
|
status: Status;
|
||||||
|
error: string | null;
|
||||||
|
};
|
||||||
|
deleteContest: {
|
||||||
status: Status;
|
status: Status;
|
||||||
error: string | null;
|
error: string | null;
|
||||||
};
|
};
|
||||||
@@ -107,12 +119,63 @@ const initialState: ContestsState = {
|
|||||||
error: null,
|
error: null,
|
||||||
},
|
},
|
||||||
fetchContestById: {
|
fetchContestById: {
|
||||||
contest: null,
|
contest: {
|
||||||
|
id: 0,
|
||||||
|
name: '',
|
||||||
|
description: '',
|
||||||
|
scheduleType: 'AlwaysOpen',
|
||||||
|
startsAt: '',
|
||||||
|
endsAt: '',
|
||||||
|
attemptDurationMinutes: 0,
|
||||||
|
maxAttempts: 0,
|
||||||
|
allowEarlyFinish: false,
|
||||||
|
groups: [],
|
||||||
|
missions: [],
|
||||||
|
articles: [],
|
||||||
|
members: [],
|
||||||
|
},
|
||||||
status: 'idle',
|
status: 'idle',
|
||||||
error: null,
|
error: null,
|
||||||
},
|
},
|
||||||
createContest: {
|
createContest: {
|
||||||
contest: null,
|
contest: {
|
||||||
|
id: 0,
|
||||||
|
name: '',
|
||||||
|
description: '',
|
||||||
|
scheduleType: 'AlwaysOpen',
|
||||||
|
startsAt: '',
|
||||||
|
endsAt: '',
|
||||||
|
attemptDurationMinutes: 0,
|
||||||
|
maxAttempts: 0,
|
||||||
|
allowEarlyFinish: false,
|
||||||
|
groups: [],
|
||||||
|
missions: [],
|
||||||
|
articles: [],
|
||||||
|
members: [],
|
||||||
|
},
|
||||||
|
status: 'idle',
|
||||||
|
error: null,
|
||||||
|
},
|
||||||
|
updateContest: {
|
||||||
|
contest: {
|
||||||
|
id: 0,
|
||||||
|
name: '',
|
||||||
|
description: '',
|
||||||
|
scheduleType: 'AlwaysOpen',
|
||||||
|
startsAt: '',
|
||||||
|
endsAt: '',
|
||||||
|
attemptDurationMinutes: 0,
|
||||||
|
maxAttempts: 0,
|
||||||
|
allowEarlyFinish: false,
|
||||||
|
groups: [],
|
||||||
|
missions: [],
|
||||||
|
articles: [],
|
||||||
|
members: [],
|
||||||
|
},
|
||||||
|
status: 'idle',
|
||||||
|
error: null,
|
||||||
|
},
|
||||||
|
deleteContest: {
|
||||||
status: 'idle',
|
status: 'idle',
|
||||||
error: null,
|
error: null,
|
||||||
},
|
},
|
||||||
@@ -191,13 +254,51 @@ export const createContest = createAsyncThunk(
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// 🆕 Обновление контеста
|
||||||
|
export const updateContest = createAsyncThunk(
|
||||||
|
'contests/update',
|
||||||
|
async (
|
||||||
|
{
|
||||||
|
contestId,
|
||||||
|
...contestData
|
||||||
|
}: { contestId: number } & CreateContestBody,
|
||||||
|
{ rejectWithValue },
|
||||||
|
) => {
|
||||||
|
try {
|
||||||
|
const response = await axios.patch<Contest>(
|
||||||
|
`/contests/${contestId}`,
|
||||||
|
contestData,
|
||||||
|
);
|
||||||
|
return response.data;
|
||||||
|
} catch (err: any) {
|
||||||
|
return rejectWithValue(
|
||||||
|
err.response?.data?.message || 'Failed to update contest',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
// 🆕 Удаление контеста
|
||||||
|
export const deleteContest = createAsyncThunk(
|
||||||
|
'contests/delete',
|
||||||
|
async (contestId: number, { rejectWithValue }) => {
|
||||||
|
try {
|
||||||
|
await axios.delete(`/contests/${contestId}`);
|
||||||
|
return contestId;
|
||||||
|
} catch (err: any) {
|
||||||
|
return rejectWithValue(
|
||||||
|
err.response?.data?.message || 'Failed to delete contest',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
// Контесты, созданные мной
|
// Контесты, созданные мной
|
||||||
export const fetchMyContests = createAsyncThunk(
|
export const fetchMyContests = createAsyncThunk(
|
||||||
'contests/fetchMyContests',
|
'contests/fetchMyContests',
|
||||||
async (_, { rejectWithValue }) => {
|
async (_, { rejectWithValue }) => {
|
||||||
try {
|
try {
|
||||||
const response = await axios.get<Contest[]>('/contests/my');
|
const response = await axios.get<Contest[]>('/contests/my');
|
||||||
// Возвращаем просто массив контестов
|
|
||||||
return response.data;
|
return response.data;
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
return rejectWithValue(
|
return rejectWithValue(
|
||||||
@@ -238,8 +339,15 @@ const contestsSlice = createSlice({
|
|||||||
name: 'contests',
|
name: 'contests',
|
||||||
initialState,
|
initialState,
|
||||||
reducers: {
|
reducers: {
|
||||||
clearSelectedContest: (state) => {
|
// 🆕 Сброс статусов
|
||||||
state.fetchContestById.contest = null;
|
setContestStatus: (
|
||||||
|
state,
|
||||||
|
action: PayloadAction<{ key: keyof ContestsState; status: Status }>,
|
||||||
|
) => {
|
||||||
|
const { key, status } = action.payload;
|
||||||
|
if (state[key]) {
|
||||||
|
(state[key] as any).status = status;
|
||||||
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
extraReducers: (builder) => {
|
extraReducers: (builder) => {
|
||||||
@@ -295,7 +403,48 @@ const contestsSlice = createSlice({
|
|||||||
state.createContest.error = action.payload;
|
state.createContest.error = action.payload;
|
||||||
});
|
});
|
||||||
|
|
||||||
// fetchMyContests
|
// 🆕 updateContest
|
||||||
|
builder.addCase(updateContest.pending, (state) => {
|
||||||
|
state.updateContest.status = 'loading';
|
||||||
|
state.updateContest.error = null;
|
||||||
|
});
|
||||||
|
builder.addCase(
|
||||||
|
updateContest.fulfilled,
|
||||||
|
(state, action: PayloadAction<Contest>) => {
|
||||||
|
state.updateContest.status = 'successful';
|
||||||
|
state.updateContest.contest = action.payload;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
builder.addCase(updateContest.rejected, (state, action: any) => {
|
||||||
|
state.updateContest.status = 'failed';
|
||||||
|
state.updateContest.error = action.payload;
|
||||||
|
});
|
||||||
|
|
||||||
|
// 🆕 deleteContest
|
||||||
|
builder.addCase(deleteContest.pending, (state) => {
|
||||||
|
state.deleteContest.status = 'loading';
|
||||||
|
state.deleteContest.error = null;
|
||||||
|
});
|
||||||
|
builder.addCase(
|
||||||
|
deleteContest.fulfilled,
|
||||||
|
(state, action: PayloadAction<number>) => {
|
||||||
|
state.deleteContest.status = 'successful';
|
||||||
|
// Удалим контест из списков
|
||||||
|
state.fetchContests.contests =
|
||||||
|
state.fetchContests.contests.filter(
|
||||||
|
(c) => c.id !== action.payload,
|
||||||
|
);
|
||||||
|
state.fetchMyContests.contests =
|
||||||
|
state.fetchMyContests.contests.filter(
|
||||||
|
(c) => c.id !== action.payload,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
builder.addCase(deleteContest.rejected, (state, action: any) => {
|
||||||
|
state.deleteContest.status = 'failed';
|
||||||
|
state.deleteContest.error = action.payload;
|
||||||
|
});
|
||||||
|
|
||||||
// fetchMyContests
|
// fetchMyContests
|
||||||
builder.addCase(fetchMyContests.pending, (state) => {
|
builder.addCase(fetchMyContests.pending, (state) => {
|
||||||
state.fetchMyContests.status = 'loading';
|
state.fetchMyContests.status = 'loading';
|
||||||
@@ -342,5 +491,5 @@ const contestsSlice = createSlice({
|
|||||||
// Экспорты
|
// Экспорты
|
||||||
// =====================
|
// =====================
|
||||||
|
|
||||||
export const { clearSelectedContest } = contestsSlice.actions;
|
export const { setContestStatus } = contestsSlice.actions;
|
||||||
export const contestsReducer = contestsSlice.reducer;
|
export const contestsReducer = contestsSlice.reducer;
|
||||||
|
|||||||
@@ -60,7 +60,6 @@ const ContestsBlock: FC<ContestsBlockProps> = ({
|
|||||||
id={v.id}
|
id={v.id}
|
||||||
name={v.name}
|
name={v.name}
|
||||||
startAt={v.startsAt}
|
startAt={v.startsAt}
|
||||||
statusRegister={'reg'}
|
|
||||||
duration={
|
duration={
|
||||||
new Date(v.endsAt).getTime() -
|
new Date(v.endsAt).getTime() -
|
||||||
new Date(v.startsAt).getTime()
|
new Date(v.startsAt).getTime()
|
||||||
|
|||||||
@@ -93,7 +93,7 @@ const ContestItem: React.FC<ContestItemProps> = ({
|
|||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
navigate(
|
navigate(
|
||||||
`/contest/editor?back=/home/account/articles&articleId=${id}`,
|
`/contest/create?back=/home/account/contests&contestId=${id}`,
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -4,9 +4,13 @@ import { PrimaryButton } from '../../../components/button/PrimaryButton';
|
|||||||
import { SecondaryButton } from '../../../components/button/SecondaryButton';
|
import { SecondaryButton } from '../../../components/button/SecondaryButton';
|
||||||
import { Input } from '../../../components/input/Input';
|
import { Input } from '../../../components/input/Input';
|
||||||
import { useAppDispatch, useAppSelector } from '../../../redux/hooks';
|
import { useAppDispatch, useAppSelector } from '../../../redux/hooks';
|
||||||
import { createContest } from '../../../redux/slices/contests';
|
import {
|
||||||
|
createContest,
|
||||||
|
setContestStatus,
|
||||||
|
} from '../../../redux/slices/contests';
|
||||||
import { CreateContestBody } from '../../../redux/slices/contests';
|
import { CreateContestBody } from '../../../redux/slices/contests';
|
||||||
import DateRangeInput from '../../../components/input/DateRangeInput';
|
import DateRangeInput from '../../../components/input/DateRangeInput';
|
||||||
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
|
||||||
interface ModalCreateContestProps {
|
interface ModalCreateContestProps {
|
||||||
active: boolean;
|
active: boolean;
|
||||||
@@ -18,6 +22,7 @@ const ModalCreateContest: FC<ModalCreateContestProps> = ({
|
|||||||
setActive,
|
setActive,
|
||||||
}) => {
|
}) => {
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
|
const navigate = useNavigate();
|
||||||
const status = useAppSelector(
|
const status = useAppSelector(
|
||||||
(state) => state.contests.createContest.status,
|
(state) => state.contests.createContest.status,
|
||||||
);
|
);
|
||||||
@@ -27,21 +32,29 @@ const ModalCreateContest: FC<ModalCreateContestProps> = ({
|
|||||||
description: '',
|
description: '',
|
||||||
scheduleType: 'AlwaysOpen',
|
scheduleType: 'AlwaysOpen',
|
||||||
visibility: 'Public',
|
visibility: 'Public',
|
||||||
startsAt: null,
|
startsAt: '',
|
||||||
endsAt: null,
|
endsAt: '',
|
||||||
attemptDurationMinutes: null,
|
attemptDurationMinutes: 0,
|
||||||
maxAttempts: null,
|
maxAttempts: 0,
|
||||||
allowEarlyFinish: false,
|
allowEarlyFinish: false,
|
||||||
groupId: null,
|
groupId: 0,
|
||||||
missionIds: null,
|
missionIds: [],
|
||||||
articleIds: null,
|
articleIds: [],
|
||||||
participantIds: null,
|
|
||||||
organizerIds: null,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const contest = useAppSelector(
|
||||||
|
(state) => state.contests.createContest.contest,
|
||||||
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (status === 'successful') {
|
if (status === 'successful') {
|
||||||
setActive(false);
|
console.log('navigate');
|
||||||
|
navigate(
|
||||||
|
`/contest/create?back=/home/account/contests&contestId=${contest.id}`,
|
||||||
|
);
|
||||||
|
dispatch(
|
||||||
|
setContestStatus({ key: 'createContest', status: 'idle' }),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}, [status]);
|
}, [status]);
|
||||||
|
|
||||||
@@ -176,7 +189,9 @@ const ModalCreateContest: FC<ModalCreateContestProps> = ({
|
|||||||
{/* Кнопки */}
|
{/* Кнопки */}
|
||||||
<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]">
|
||||||
<PrimaryButton
|
<PrimaryButton
|
||||||
onClick={handleSubmit}
|
onClick={() => {
|
||||||
|
handleSubmit();
|
||||||
|
}}
|
||||||
text="Создать"
|
text="Создать"
|
||||||
disabled={status === 'loading'}
|
disabled={status === 'loading'}
|
||||||
/>
|
/>
|
||||||
|
|||||||
Reference in New Issue
Block a user