dont work
This commit is contained in:
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;
|
||||
Reference in New Issue
Block a user