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);
|
const [active, setActive] = React.useState<boolean>(false);
|
||||||
|
|
||||||
React.useEffect(() => onChange(value.value), [value]);
|
|
||||||
|
|
||||||
const ref = React.useRef<HTMLDivElement>(null);
|
const ref = React.useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
useClickOutside(ref, () => {
|
useClickOutside(ref, () => {
|
||||||
@@ -63,7 +61,7 @@ export const DropDownList: React.FC<DropDownListProps> = ({
|
|||||||
<img
|
<img
|
||||||
src={chevroneDropDownList}
|
src={chevroneDropDownList}
|
||||||
className={cn(
|
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',
|
active && ' rotate-180',
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
@@ -78,7 +76,7 @@ export const DropDownList: React.FC<DropDownListProps> = ({
|
|||||||
: 'grid-rows-[0fr] opacity-0',
|
: '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
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
' overflow-y-scroll max-h-[200px] thin-scrollbar pr-[8px] ',
|
' overflow-y-scroll max-h-[200px] thin-scrollbar pr-[8px] ',
|
||||||
@@ -97,6 +95,7 @@ export const DropDownList: React.FC<DropDownListProps> = ({
|
|||||||
)}
|
)}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setValue(v);
|
setValue(v);
|
||||||
|
onChange(v.value);
|
||||||
setActive(false);
|
setActive(false);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ const DateInput: React.FC<DateInputProps> = ({
|
|||||||
}) => {
|
}) => {
|
||||||
return (
|
return (
|
||||||
<div className={`flex flex-col gap-1 ${className}`}>
|
<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}
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
@@ -26,7 +26,7 @@ const DateInput: React.FC<DateInputProps> = ({
|
|||||||
value={value}
|
value={value}
|
||||||
defaultValue={defaultValue}
|
defaultValue={defaultValue}
|
||||||
onChange={(e) => onChange(e.target.value)}
|
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>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -10,11 +10,17 @@ import {
|
|||||||
setContestStatus,
|
setContestStatus,
|
||||||
updateContest,
|
updateContest,
|
||||||
} from '../redux/slices/contests';
|
} from '../redux/slices/contests';
|
||||||
import DateRangeInput from '../components/input/DateRangeInput';
|
|
||||||
import { useQuery } from '../hooks/useQuery';
|
import { useQuery } from '../hooks/useQuery';
|
||||||
import { Navigate, useNavigate } from 'react-router-dom';
|
import { Navigate, useNavigate } from 'react-router-dom';
|
||||||
import { fetchMissionById } from '../redux/slices/missions';
|
import { fetchMissionById } from '../redux/slices/missions';
|
||||||
import { ReverseButton } from '../components/button/ReverseButton';
|
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 {
|
interface Mission {
|
||||||
id: number;
|
id: number;
|
||||||
@@ -52,6 +58,17 @@ const ContestEditor = () => {
|
|||||||
return local.toISOString().slice(0, 16);
|
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>({
|
const [contest, setContest] = useState<CreateContestBody>({
|
||||||
name: '',
|
name: '',
|
||||||
description: '',
|
description: '',
|
||||||
@@ -78,6 +95,18 @@ const ContestEditor = () => {
|
|||||||
const { contest: contestById, status: contestByIdstatus } = useAppSelector(
|
const { contest: contestById, status: contestByIdstatus } = useAppSelector(
|
||||||
(state) => state.contests.fetchContestById,
|
(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(() => {
|
useEffect(() => {
|
||||||
if (status === 'successful') {
|
if (status === 'successful') {
|
||||||
}
|
}
|
||||||
@@ -154,13 +183,23 @@ const ContestEditor = () => {
|
|||||||
articleIds: contestById.articles?.map(
|
articleIds: contestById.articles?.map(
|
||||||
(article) => article.articleId,
|
(article) => article.articleId,
|
||||||
),
|
),
|
||||||
visibility: 'Public',
|
|
||||||
scheduleType: 'AlwaysOpen',
|
|
||||||
});
|
});
|
||||||
setMissions(contestById.missions ?? []);
|
setMissions(contestById.missions ?? []);
|
||||||
}
|
}
|
||||||
}, [contestById]);
|
}, [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 (
|
return (
|
||||||
<div className="h-screen grid grid-rows-[60px,1fr] text-liquid-white">
|
<div className="h-screen grid grid-rows-[60px,1fr] text-liquid-white">
|
||||||
<Header backClick={() => navigate(back || '/home/contests')} />
|
<Header backClick={() => navigate(back || '/home/contests')} />
|
||||||
@@ -201,73 +240,113 @@ const ContestEditor = () => {
|
|||||||
<div className="grid grid-cols-2 gap-[10px] mt-[10px]">
|
<div className="grid grid-cols-2 gap-[10px] mt-[10px]">
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm mb-1">
|
<label className="block text-sm mb-1">
|
||||||
Тип расписания
|
Тип контеста
|
||||||
</label>
|
</label>
|
||||||
<select
|
|
||||||
className="w-full p-2 rounded-md bg-liquid-darker border border-liquid-lighter"
|
<DropDownList
|
||||||
value={contest.scheduleType}
|
items={scheduleTypeItems}
|
||||||
onChange={(e) =>
|
defaultState={scheduleTypeDefaultState}
|
||||||
handleChange(
|
onChange={(v) => {
|
||||||
'scheduleType',
|
handleChange('scheduleType', v);
|
||||||
e.target
|
}}
|
||||||
.value as CreateContestBody['scheduleType'],
|
weight="w-full"
|
||||||
)
|
/>
|
||||||
}
|
|
||||||
>
|
|
||||||
<option value="AlwaysOpen">
|
|
||||||
Всегда открыт
|
|
||||||
</option>
|
|
||||||
<option value="FixedWindow">
|
|
||||||
Фиксированные даты
|
|
||||||
</option>
|
|
||||||
<option value="RollingWindow">
|
|
||||||
Скользящее окно
|
|
||||||
</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm mb-1">
|
<label className="block text-sm mb-1">
|
||||||
Видимость
|
Видимость
|
||||||
</label>
|
</label>
|
||||||
<select
|
<DropDownList
|
||||||
className="w-full p-2 rounded-md bg-liquid-darker border border-liquid-lighter"
|
items={visibilityItems}
|
||||||
value={contest.visibility}
|
onChange={(v) => {
|
||||||
onChange={(e) =>
|
handleChange('visibility', v);
|
||||||
handleChange(
|
}}
|
||||||
'visibility',
|
defaultState={visibilityDefaultState}
|
||||||
e.target
|
weight="w-full"
|
||||||
.value as CreateContestBody['visibility'],
|
/>
|
||||||
)
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<option value="Public">
|
|
||||||
Публичный
|
|
||||||
</option>
|
|
||||||
<option value="GroupPrivate">
|
|
||||||
Групповой
|
|
||||||
</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Даты начала и конца */}
|
<div
|
||||||
<div className="grid grid-cols-2 gap-[10px] mt-[10px]">
|
className={cn(
|
||||||
<DateRangeInput
|
' grid grid-flow-row grid-rows-[0fr] opacity-0 transition-all duration-200',
|
||||||
startValue={contest.startsAt || ''}
|
contest.visibility == 'GroupPrivate' &&
|
||||||
endValue={contest.endsAt || ''}
|
'grid-rows-[1fr] opacity-100',
|
||||||
onChange={handleChange}
|
)}
|
||||||
className="mt-[10px]"
|
>
|
||||||
/>
|
<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>
|
||||||
|
|
||||||
{/* Продолжительность и лимиты */}
|
{/* Продолжительность и лимиты */}
|
||||||
<div className="grid grid-cols-2 gap-[10px] mt-[10px]">
|
<div className="grid grid-cols-2 gap-[10px] mt-[10px]">
|
||||||
<Input
|
<NumberInput
|
||||||
|
defaultState={
|
||||||
|
contest.attemptDurationMinutes
|
||||||
|
}
|
||||||
name="attemptDurationMinutes"
|
name="attemptDurationMinutes"
|
||||||
type="number"
|
|
||||||
label="Длительность попытки (мин)"
|
label="Длительность попытки (мин)"
|
||||||
placeholder="Например: 60"
|
placeholder="Например: 60"
|
||||||
|
minValue={1}
|
||||||
|
maxValue={365 * 24 * 60}
|
||||||
onChange={(v) =>
|
onChange={(v) =>
|
||||||
handleChange(
|
handleChange(
|
||||||
'attemptDurationMinutes',
|
'attemptDurationMinutes',
|
||||||
@@ -275,35 +354,19 @@ const ContestEditor = () => {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<Input
|
<NumberInput
|
||||||
|
defaultState={contest.maxAttempts}
|
||||||
name="maxAttempts"
|
name="maxAttempts"
|
||||||
type="number"
|
|
||||||
label="Макс. попыток"
|
label="Макс. попыток"
|
||||||
placeholder="Например: 3"
|
placeholder="Например: 3"
|
||||||
|
minValue={1}
|
||||||
|
maxValue={100}
|
||||||
onChange={(v) =>
|
onChange={(v) =>
|
||||||
handleChange('maxAttempts', Number(v))
|
handleChange('maxAttempts', Number(v))
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</div>
|
</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]">
|
<div className="flex flex-row w-full items-center justify-end mt-[20px] gap-[20px]">
|
||||||
<PrimaryButton
|
<PrimaryButton
|
||||||
|
|||||||
@@ -149,26 +149,6 @@ const ModalCreateContest: FC<ModalCreateContestProps> = ({
|
|||||||
}}
|
}}
|
||||||
weight="w-full"
|
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>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
@@ -180,33 +160,9 @@ const ModalCreateContest: FC<ModalCreateContestProps> = ({
|
|||||||
}}
|
}}
|
||||||
weight="w-full"
|
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>
|
</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
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
' grid grid-flow-row grid-rows-[0fr] opacity-0 transition-all duration-200',
|
' grid grid-flow-row grid-rows-[0fr] opacity-0 transition-all duration-200',
|
||||||
|
|||||||
Reference in New Issue
Block a user