add update contest form
This commit is contained in:
@@ -32,8 +32,6 @@ export const DropDownList: React.FC<DropDownListProps> = ({
|
||||
);
|
||||
const [active, setActive] = React.useState<boolean>(false);
|
||||
|
||||
React.useEffect(() => onChange(value.value), [value]);
|
||||
|
||||
const ref = React.useRef<HTMLDivElement>(null);
|
||||
|
||||
useClickOutside(ref, () => {
|
||||
@@ -63,7 +61,7 @@ export const DropDownList: React.FC<DropDownListProps> = ({
|
||||
<img
|
||||
src={chevroneDropDownList}
|
||||
className={cn(
|
||||
' absolute right-[16px] h-[24px] w-[24px] top-[8.5px] rotate-0 transition-all duration-300 pointer-events-none',
|
||||
' absolute right-[16px] h-[24px] w-[24px] top-[8.5px] rotate-0 transition-all duration-300 pointer-events-none select-none',
|
||||
active && ' rotate-180',
|
||||
)}
|
||||
/>
|
||||
@@ -78,7 +76,7 @@ export const DropDownList: React.FC<DropDownListProps> = ({
|
||||
: 'grid-rows-[0fr] opacity-0',
|
||||
)}
|
||||
>
|
||||
<div className=" overflow-hidden p-[8px]">
|
||||
<div className=" overflow-hidden p-[8px] border-liquid-background border-solid border-[1px] rounded-[10px]">
|
||||
<div
|
||||
className={cn(
|
||||
' overflow-y-scroll max-h-[200px] thin-scrollbar pr-[8px] ',
|
||||
@@ -97,6 +95,7 @@ export const DropDownList: React.FC<DropDownListProps> = ({
|
||||
)}
|
||||
onClick={() => {
|
||||
setValue(v);
|
||||
onChange(v.value);
|
||||
setActive(false);
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -18,7 +18,7 @@ const DateInput: React.FC<DateInputProps> = ({
|
||||
}) => {
|
||||
return (
|
||||
<div className={`flex flex-col gap-1 ${className}`}>
|
||||
<label className="block text-sm font-medium text-gray-700">
|
||||
<label className="block text-sm font-medium text-liquid-white">
|
||||
{label}
|
||||
</label>
|
||||
<input
|
||||
@@ -26,7 +26,7 @@ const DateInput: React.FC<DateInputProps> = ({
|
||||
value={value}
|
||||
defaultValue={defaultValue}
|
||||
onChange={(e) => onChange(e.target.value)}
|
||||
className="mt-1 block w-full rounded-md border-gray-300 focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm"
|
||||
className="mt-1 block w-full rounded-[10px] sm:text-sm outline-none p-[8px] text-liquid-white cursor-text bg-liquid-lighter"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -10,11 +10,17 @@ import {
|
||||
setContestStatus,
|
||||
updateContest,
|
||||
} from '../redux/slices/contests';
|
||||
import DateRangeInput from '../components/input/DateRangeInput';
|
||||
import { useQuery } from '../hooks/useQuery';
|
||||
import { Navigate, useNavigate } from 'react-router-dom';
|
||||
import { fetchMissionById } from '../redux/slices/missions';
|
||||
import { ReverseButton } from '../components/button/ReverseButton';
|
||||
import {
|
||||
DropDownList,
|
||||
DropDownListItem,
|
||||
} from '../components/filters/DropDownList';
|
||||
import { NumberInput } from '../components/input/NumberInput';
|
||||
import { cn } from '../lib/cn';
|
||||
import DateInput from '../components/input/DateInput';
|
||||
|
||||
interface Mission {
|
||||
id: number;
|
||||
@@ -52,6 +58,17 @@ const ContestEditor = () => {
|
||||
return local.toISOString().slice(0, 16);
|
||||
};
|
||||
|
||||
const visibilityItems: DropDownListItem[] = [
|
||||
{ value: 'Public', text: 'Публичный' },
|
||||
{ value: 'GroupPrivate', text: 'Для группы' },
|
||||
];
|
||||
|
||||
const scheduleTypeItems: DropDownListItem[] = [
|
||||
{ value: 'AlwaysOpen', text: 'Всегда открыт' },
|
||||
{ value: 'FixedWindow', text: 'Фиксированое окно' },
|
||||
{ value: 'RollingWindow', text: 'Скользящее окно' },
|
||||
];
|
||||
|
||||
const [contest, setContest] = useState<CreateContestBody>({
|
||||
name: '',
|
||||
description: '',
|
||||
@@ -78,6 +95,18 @@ const ContestEditor = () => {
|
||||
const { contest: contestById, status: contestByIdstatus } = useAppSelector(
|
||||
(state) => state.contests.fetchContestById,
|
||||
);
|
||||
|
||||
function toLocalInputValue(utcString: string) {
|
||||
const d = new Date(utcString);
|
||||
|
||||
const pad = (n: number) => n.toString().padStart(2, '0');
|
||||
|
||||
return (
|
||||
`${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}` +
|
||||
`T${pad(d.getHours())}:${pad(d.getMinutes())}`
|
||||
);
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (status === 'successful') {
|
||||
}
|
||||
@@ -154,13 +183,23 @@ const ContestEditor = () => {
|
||||
articleIds: contestById.articles?.map(
|
||||
(article) => article.articleId,
|
||||
),
|
||||
visibility: 'Public',
|
||||
scheduleType: 'AlwaysOpen',
|
||||
});
|
||||
setMissions(contestById.missions ?? []);
|
||||
}
|
||||
}, [contestById]);
|
||||
|
||||
console.log(contest);
|
||||
|
||||
const visibilityDefaultState =
|
||||
visibilityItems.find(
|
||||
(i) => contest && i.value === contest.visibility,
|
||||
) ?? visibilityItems[0];
|
||||
|
||||
const scheduleTypeDefaultState =
|
||||
scheduleTypeItems.find(
|
||||
(i) => contest && i.value === contest.scheduleType,
|
||||
) ?? scheduleTypeItems[0];
|
||||
|
||||
return (
|
||||
<div className="h-screen grid grid-rows-[60px,1fr] text-liquid-white">
|
||||
<Header backClick={() => navigate(back || '/home/contests')} />
|
||||
@@ -201,73 +240,113 @@ const ContestEditor = () => {
|
||||
<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>
|
||||
|
||||
<DropDownList
|
||||
items={scheduleTypeItems}
|
||||
defaultState={scheduleTypeDefaultState}
|
||||
onChange={(v) => {
|
||||
handleChange('scheduleType', v);
|
||||
}}
|
||||
weight="w-full"
|
||||
/>
|
||||
</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>
|
||||
<DropDownList
|
||||
items={visibilityItems}
|
||||
onChange={(v) => {
|
||||
handleChange('visibility', v);
|
||||
}}
|
||||
defaultState={visibilityDefaultState}
|
||||
weight="w-full"
|
||||
/>
|
||||
</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
|
||||
className={cn(
|
||||
' grid grid-flow-row grid-rows-[0fr] opacity-0 transition-all duration-200',
|
||||
contest.visibility == 'GroupPrivate' &&
|
||||
'grid-rows-[1fr] opacity-100',
|
||||
)}
|
||||
>
|
||||
<div className="overflow-hidden">
|
||||
<div className="grid grid-cols-2 gap-[10px] mt-[10px]">
|
||||
<NumberInput
|
||||
defaultState={contest.groupId ?? 1}
|
||||
name="groupId"
|
||||
label="Id группы"
|
||||
placeholder="Например: 3"
|
||||
minValue={1}
|
||||
maxValue={1000000000000000}
|
||||
onChange={(v) =>
|
||||
handleChange(
|
||||
'groupId',
|
||||
Number(v),
|
||||
)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Даты */}
|
||||
<div
|
||||
className={cn(
|
||||
' grid grid-flow-row grid-rows-[0fr] opacity-0 transition-all duration-200',
|
||||
contest.scheduleType != 'AlwaysOpen' &&
|
||||
'grid-rows-[1fr] opacity-100',
|
||||
)}
|
||||
>
|
||||
<div className="overflow-hidden">
|
||||
<div className="grid grid-cols-2 gap-[10px] mt-[10px]">
|
||||
<DateInput
|
||||
label="Дата начала"
|
||||
value={
|
||||
contest.startsAt
|
||||
? toLocalInputValue(
|
||||
contest.startsAt,
|
||||
)
|
||||
: ''
|
||||
}
|
||||
onChange={(v) =>
|
||||
handleChange('startsAt', v)
|
||||
}
|
||||
/>
|
||||
|
||||
<DateInput
|
||||
label="Дата окончания"
|
||||
value={
|
||||
contest.endsAt
|
||||
? toLocalInputValue(
|
||||
contest.endsAt,
|
||||
)
|
||||
: ''
|
||||
}
|
||||
onChange={(v) =>
|
||||
handleChange('endsAt', v)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Продолжительность и лимиты */}
|
||||
<div className="grid grid-cols-2 gap-[10px] mt-[10px]">
|
||||
<Input
|
||||
<NumberInput
|
||||
defaultState={
|
||||
contest.attemptDurationMinutes
|
||||
}
|
||||
name="attemptDurationMinutes"
|
||||
type="number"
|
||||
label="Длительность попытки (мин)"
|
||||
placeholder="Например: 60"
|
||||
minValue={1}
|
||||
maxValue={365 * 24 * 60}
|
||||
onChange={(v) =>
|
||||
handleChange(
|
||||
'attemptDurationMinutes',
|
||||
@@ -275,35 +354,19 @@ const ContestEditor = () => {
|
||||
)
|
||||
}
|
||||
/>
|
||||
<Input
|
||||
<NumberInput
|
||||
defaultState={contest.maxAttempts}
|
||||
name="maxAttempts"
|
||||
type="number"
|
||||
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={!!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]">
|
||||
<PrimaryButton
|
||||
|
||||
@@ -149,26 +149,6 @@ const ModalCreateContest: FC<ModalCreateContestProps> = ({
|
||||
}}
|
||||
weight="w-full"
|
||||
/>
|
||||
|
||||
{/* <select
|
||||
className="w-full p-2 rounded-md bg-liquid-darker border border-liquid-lighter"
|
||||
value={form.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>
|
||||
@@ -180,33 +160,9 @@ const ModalCreateContest: FC<ModalCreateContestProps> = ({
|
||||
}}
|
||||
weight="w-full"
|
||||
/>
|
||||
{/* <select
|
||||
className="w-full p-2 rounded-md bg-liquid-darker border border-liquid-lighter"
|
||||
value={form.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={form.startsAt || ''}
|
||||
endValue={form.endsAt || ''}
|
||||
onChange={handleChange}
|
||||
className="mt-[10px]"
|
||||
/>
|
||||
</div> */}
|
||||
|
||||
<div
|
||||
className={cn(
|
||||
' grid grid-flow-row grid-rows-[0fr] opacity-0 transition-all duration-200',
|
||||
|
||||
Reference in New Issue
Block a user