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

View File

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

View File

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

View File

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