318 lines
12 KiB
TypeScript
318 lines
12 KiB
TypeScript
import { FC, useEffect, useState } from 'react';
|
||
import { Modal } from '../../../components/modal/Modal';
|
||
import { PrimaryButton } from '../../../components/button/PrimaryButton';
|
||
import { SecondaryButton } from '../../../components/button/SecondaryButton';
|
||
import { Input } from '../../../components/input/Input';
|
||
import { useAppDispatch, useAppSelector } from '../../../redux/hooks';
|
||
import {
|
||
createContest,
|
||
setContestStatus,
|
||
} from '../../../redux/slices/contests';
|
||
import { CreateContestBody } from '../../../redux/slices/contests';
|
||
import { useNavigate } from 'react-router-dom';
|
||
import { NumberInput } from '../../../components/input/NumberInput';
|
||
import {
|
||
DropDownList,
|
||
DropDownListItem,
|
||
} from '../../../components/input/DropDownList';
|
||
import DateInput from '../../../components/input/DateInput';
|
||
import { cn } from '../../../lib/cn';
|
||
import { fetchMyGroups } from '../../../redux/slices/groups';
|
||
|
||
function toUtc(localDateTime?: string): string {
|
||
if (!localDateTime) return '';
|
||
|
||
// Создаём дату (она автоматически считается как локальная)
|
||
const date = new Date(localDateTime);
|
||
|
||
// Возвращаем ISO-строку с 'Z' (всегда в UTC)
|
||
return date.toISOString();
|
||
}
|
||
|
||
interface ModalCreateContestProps {
|
||
active: boolean;
|
||
setActive: (value: boolean) => void;
|
||
}
|
||
|
||
const ModalCreateContest: FC<ModalCreateContestProps> = ({
|
||
active,
|
||
setActive,
|
||
}) => {
|
||
const dispatch = useAppDispatch();
|
||
const navigate = useNavigate();
|
||
const status = useAppSelector(
|
||
(state) => state.contests.createContest.status,
|
||
);
|
||
|
||
const visibilityItems: DropDownListItem[] = [
|
||
{ value: 'Public', text: 'Публичный' },
|
||
{ value: 'GroupPrivate', text: 'Для группы' },
|
||
];
|
||
|
||
const scheduleTypeItems: DropDownListItem[] = [
|
||
{ value: 'AlwaysOpen', text: 'Всегда открыт' },
|
||
{ value: 'FixedWindow', text: 'Фиксированое окно' },
|
||
{ value: 'RollingWindow', text: 'Скользящее окно' },
|
||
];
|
||
|
||
const now = new Date();
|
||
const plus60 = new Date(now.getTime() + 60 * 60 * 1000);
|
||
|
||
const toLocal = (d: Date) => {
|
||
const off = d.getTimezoneOffset();
|
||
const local = new Date(d.getTime() - off * 60000);
|
||
return local.toISOString().slice(0, 16);
|
||
};
|
||
|
||
const [form, setForm] = useState<CreateContestBody>({
|
||
name: '',
|
||
description: '',
|
||
scheduleType: 'AlwaysOpen',
|
||
visibility: 'Public',
|
||
startsAt: toLocal(now),
|
||
endsAt: toLocal(plus60),
|
||
attemptDurationMinutes: 60,
|
||
maxAttempts: 1,
|
||
allowEarlyFinish: false,
|
||
missionIds: [],
|
||
articleIds: [],
|
||
});
|
||
|
||
const contest = useAppSelector(
|
||
(state) => state.contests.createContest.contest,
|
||
);
|
||
const myname = useAppSelector((state) => state.auth.username);
|
||
const myGroups = useAppSelector(
|
||
(state) => state.groups.fetchMyGroups.groups,
|
||
).filter((group) =>
|
||
group.members.some(
|
||
(member) =>
|
||
member.username === myname &&
|
||
member.role.includes('Administrator'),
|
||
),
|
||
);
|
||
|
||
useEffect(() => {
|
||
if (status === 'successful') {
|
||
dispatch(
|
||
setContestStatus({ key: 'createContest', status: 'idle' }),
|
||
);
|
||
navigate(
|
||
`/contest/create?back=/home/account/contests&contestId=${contest.id}`,
|
||
);
|
||
}
|
||
}, [status]);
|
||
|
||
useEffect(() => {
|
||
if (active) {
|
||
dispatch(fetchMyGroups());
|
||
}
|
||
}, [active]);
|
||
|
||
const handleChange = (key: keyof CreateContestBody, value: any) => {
|
||
setForm((prev) => ({ ...prev, [key]: value }));
|
||
};
|
||
|
||
const handleSubmit = () => {
|
||
dispatch(
|
||
createContest({
|
||
...form,
|
||
endsAt: toUtc(form.endsAt),
|
||
startsAt: toUtc(form.startsAt),
|
||
}),
|
||
);
|
||
};
|
||
const groupItems = myGroups.map((v) => {
|
||
return {
|
||
value: '' + v.id,
|
||
text: v.name,
|
||
};
|
||
});
|
||
const groupIdDefaultState =
|
||
myGroups.find((g) => g.id == form?.groupId) ?? myGroups[0] ?? undefined;
|
||
|
||
console.log(groupItems, myGroups, groupIdDefaultState);
|
||
return (
|
||
<Modal
|
||
className="bg-liquid-background border-liquid-lighter border-[2px] p-[25px] rounded-[20px] text-liquid-white"
|
||
onOpenChange={setActive}
|
||
open={active}
|
||
backdrop="blur"
|
||
>
|
||
<div className="w-[550px]">
|
||
<div className="font-bold text-[30px] mb-[10px]">
|
||
Создать контест
|
||
</div>
|
||
|
||
<Input
|
||
name="name"
|
||
type="text"
|
||
label="Название"
|
||
className="mt-[10px]"
|
||
placeholder="Введите название"
|
||
onChange={(v) => handleChange('name', v)}
|
||
/>
|
||
|
||
<Input
|
||
name="description"
|
||
type="text"
|
||
label="Описание"
|
||
className="mt-[10px]"
|
||
placeholder="Введите описание"
|
||
onChange={(v) => handleChange('description', v)}
|
||
/>
|
||
|
||
<div className="grid grid-cols-2 gap-[10px] mt-[10px]">
|
||
<div>
|
||
<label className="block text-sm mb-1">
|
||
Тип контеста
|
||
</label>
|
||
|
||
<DropDownList
|
||
items={scheduleTypeItems}
|
||
onChange={(v) => {
|
||
handleChange('scheduleType', v);
|
||
}}
|
||
weight="w-full"
|
||
/>
|
||
</div>
|
||
|
||
<div>
|
||
<label className="block text-sm mb-1">Видимость</label>
|
||
<DropDownList
|
||
items={visibilityItems}
|
||
onChange={(v) => {
|
||
handleChange('visibility', v);
|
||
}}
|
||
weight="w-full"
|
||
/>
|
||
</div>
|
||
</div>
|
||
|
||
<div
|
||
className={cn(
|
||
' grid grid-flow-row grid-rows-[0fr] opacity-0 transition-all duration-200 mb-[10px]',
|
||
form.visibility == 'GroupPrivate' &&
|
||
'grid-rows-[1fr] opacity-100',
|
||
)}
|
||
>
|
||
{groupIdDefaultState ? (
|
||
<div
|
||
className={cn(
|
||
form.visibility != 'GroupPrivate' &&
|
||
'overflow-hidden',
|
||
)}
|
||
>
|
||
<div>
|
||
<label className="block text-sm mb-2 mt-[10px]">
|
||
Группа для привязки
|
||
</label>
|
||
|
||
<DropDownList
|
||
items={groupItems}
|
||
defaultState={{
|
||
value: '' + groupIdDefaultState.id,
|
||
text: groupIdDefaultState.name,
|
||
}}
|
||
onChange={(v) => {
|
||
handleChange('groupId', Number(v));
|
||
}}
|
||
weight="w-full"
|
||
/>
|
||
</div>
|
||
</div>
|
||
) : (
|
||
<div className="overflow-hidden">
|
||
<div className="text-liquid-red my-[20px]">
|
||
У вас нет группы вкоторой вы являетесь
|
||
Администратором!
|
||
</div>
|
||
</div>
|
||
)}
|
||
</div>
|
||
|
||
{/* Даты */}
|
||
<div
|
||
className={cn(
|
||
' grid grid-flow-row grid-rows-[0fr] opacity-0 transition-all duration-200',
|
||
form.scheduleType != 'AlwaysOpen' &&
|
||
'grid-rows-[1fr] opacity-100',
|
||
)}
|
||
>
|
||
<div className="overflow-hidden">
|
||
<div className="grid grid-cols-2 gap-[10px] mt-[10px]">
|
||
<DateInput
|
||
label="Дата начала"
|
||
value={form.startsAt}
|
||
onChange={(v) => handleChange('startsAt', v)}
|
||
/>
|
||
|
||
<DateInput
|
||
label="Дата окончания"
|
||
value={form.endsAt}
|
||
onChange={(v) => handleChange('endsAt', v)}
|
||
/>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Продолжительность и лимиты */}
|
||
<div className="grid grid-cols-2 gap-[10px] mt-[10px]">
|
||
<NumberInput
|
||
defaultState={form.attemptDurationMinutes}
|
||
name="attemptDurationMinutes"
|
||
label="Длительность попытки (мин)"
|
||
placeholder="Например: 60"
|
||
minValue={1}
|
||
maxValue={365 * 24 * 60}
|
||
onChange={(v) =>
|
||
handleChange('attemptDurationMinutes', Number(v))
|
||
}
|
||
/>
|
||
<NumberInput
|
||
defaultState={form.maxAttempts}
|
||
name="maxAttempts"
|
||
label="Макс. попыток"
|
||
placeholder="Например: 3"
|
||
minValue={1}
|
||
maxValue={100}
|
||
onChange={(v) => handleChange('maxAttempts', Number(v))}
|
||
/>
|
||
</div>
|
||
|
||
{/* Разрешить раннее завершение */}
|
||
{/* <div className="flex items-center gap-[10px] mt-[15px]">
|
||
<input
|
||
id="allowEarlyFinish"
|
||
type="checkbox"
|
||
checked={!!form.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]">
|
||
<PrimaryButton
|
||
onClick={() => {
|
||
handleSubmit();
|
||
}}
|
||
text="Создать"
|
||
disabled={status === 'loading'}
|
||
/>
|
||
<SecondaryButton
|
||
onClick={() => setActive(false)}
|
||
text="Отмена"
|
||
/>
|
||
</div>
|
||
</div>
|
||
</Modal>
|
||
);
|
||
};
|
||
|
||
export default ModalCreateContest;
|