add update contest form

This commit is contained in:
Виталий Лавшонок
2025-12-09 16:54:44 +03:00
parent 4391114dc3
commit 6675bd871e
4 changed files with 141 additions and 123 deletions

View File

@@ -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);
}}
>

View File

@@ -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>
);

View File

@@ -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

View File

@@ -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',