Files
LiquidCode_Frontend/src/views/home/contests/ModalCreate.tsx
Виталий Лавшонок c761f337b1 group drop down list
2025-12-10 02:37:16 +03:00

318 lines
12 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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;