import { useEffect, useState } from 'react'; import Header from '../views/articleeditor/Header'; import { PrimaryButton } from '../components/button/PrimaryButton'; import { Input } from '../components/input/Input'; import { useAppDispatch, useAppSelector } from '../redux/hooks'; import { CreateContestBody, deleteContest, fetchContestById, setContestStatus, updateContest, } from '../redux/slices/contests'; import { useQuery } from '../hooks/useQuery'; import { Navigate, useNavigate } from 'react-router-dom'; import { fetchMissionById, fetchMissions } from '../redux/slices/missions'; import { ReverseButton } from '../components/button/ReverseButton'; import { DropDownList, DropDownListItem, } from '../components/input/DropDownList'; import { NumberInput } from '../components/input/NumberInput'; import { cn } from '../lib/cn'; import DateInput from '../components/input/DateInput'; import { fetchMyGroups } from '../redux/slices/groups'; interface Mission { id: number; name: string; } const highlightZ = (name: string, filter: string) => { if (!filter) return name; const s = filter.toLowerCase(); const t = name.toLowerCase(); const n = t.length; const m = s.length; const mark = Array(n).fill(false); // Проходимся с конца и ставим отметки for (let i = n - 1; i >= 0; i--) { if (i + m <= n && t.slice(i, i + m) === s) { for (let j = i; j < i + m; j++) { if (mark[j]) break; mark[j] = true; } } } // === Формируем единые жёлтые блоки === const result: any[] = []; let i = 0; while (i < n) { if (!mark[i]) { // обычный символ result.push(name[i]); i++; } else { // начинаем жёлтый блок let j = i; while (j < n && mark[j]) j++; const chunk = name.slice(i, j); result.push( {chunk} , ); i = j; } } return result; }; function toUtc(localDateTime?: string): string { if (!localDateTime) return ''; // Создаём дату (она автоматически считается как локальная) const date = new Date(localDateTime); // Возвращаем ISO-строку с 'Z' (всегда в UTC) return date.toISOString(); } /** * Страница создания / редактирования контеста */ const ContestEditor = () => { const dispatch = useAppDispatch(); const navigate = useNavigate(); const query = useQuery(); const back = query.get('back') ?? undefined; const contestId = Number(query.get('contestId') ?? undefined); const refactor = !!contestId; if (!refactor) { return ; } const status = useAppSelector( (state) => state.contests.createContest.status, ); const [missionFindInput, setMissionFindInput] = useState(''); 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 visibilityItems: DropDownListItem[] = [ { value: 'Public', text: 'Публичный' }, { value: 'GroupPrivate', text: 'Для группы' }, ]; const scheduleTypeItems: DropDownListItem[] = [ { value: 'AlwaysOpen', text: 'Всегда открыт' }, { value: 'FixedWindow', text: 'Фиксированое окно' }, { value: 'RollingWindow', text: 'Скользящее окно' }, ]; const [contest, setContest] = useState({ name: '', description: '', scheduleType: 'AlwaysOpen', visibility: 'Public', startsAt: toLocal(now), endsAt: toLocal(plus60), attemptDurationMinutes: 60, maxAttempts: 1, allowEarlyFinish: false, missionIds: [], articleIds: [], }); const myname = useAppSelector((state) => state.auth.username); const [missions, setMissions] = useState([]); const statusDelete = useAppSelector( (state) => state.contests.deleteContest.status, ); const statusUpdate = useAppSelector( (state) => state.contests.updateContest.status, ); const { contest: contestById, status: contestByIdstatus } = useAppSelector( (state) => state.contests.fetchContestById, ); const globalMissions = useAppSelector((state) => state.missions.missions); const myGroups = useAppSelector( (state) => state.groups.fetchMyGroups.groups, ).filter((group) => group.members.some( (member) => member.username === myname && member.role.includes('Administrator'), ), ); 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') { } }, [status]); const handleChange = (key: keyof CreateContestBody, value: any) => { setContest((prev) => ({ ...prev, [key]: value })); }; const handleUpdateContest = () => { dispatch( updateContest({ ...contest, endsAt: toUtc(contest.endsAt), startsAt: toUtc(contest.startsAt), contestId, }), ); }; const handleDeleteContest = () => { dispatch(deleteContest(contestId)); }; const addMission = () => { const mission = globalMissions .filter((v) => !contest?.missionIds?.includes(v.id)) .filter((v) => (v.id + ' ' + v.name) .toLocaleLowerCase() .includes(missionFindInput.toLocaleLowerCase()), )[0]; if (!mission) return; const id = mission.id; if (!id || contest.missionIds?.includes(id)) return; dispatch(fetchMissionById(id)) .unwrap() .then((mission) => { setMissions((prev) => [...prev, mission]); setContest((prev) => ({ ...prev, missionIds: [...(prev.missionIds ?? []), id], })); setMissionFindInput(''); }) .catch((err) => { err; }); }; const removeMission = (removeId: number) => { setContest({ ...contest, missionIds: contest.missionIds?.filter((v) => v !== removeId), }); setMissions(missions.filter((v) => v.id != removeId)); }; useEffect(() => { if (statusDelete == 'successful') { dispatch( setContestStatus({ key: 'deleteContest', status: 'idle' }), ); navigate('/home/account/contests'); } }, [statusDelete]); useEffect(() => { if (statusUpdate == 'successful') { dispatch( setContestStatus({ key: 'updateContest', status: 'idle' }), ); navigate('/home/account/contests'); } }, [statusUpdate]); useEffect(() => { if (refactor) { dispatch(fetchContestById(contestId)); dispatch(fetchMyGroups()); dispatch(fetchMissions({})); } }, [refactor]); useEffect(() => { if (refactor && contestByIdstatus == 'successful' && contestById) { setContest({ ...contestById, // groupIds: contestById.groups.map(group => group.groupId), missionIds: contestById.missions?.map((mission) => mission.id), articleIds: contestById.articles?.map( (article) => article.articleId, ), }); setMissions(contestById.missions ?? []); } }, [contestById]); const visibilityDefaultState = visibilityItems.find( (i) => contest && i.value === contest.visibility, ) ?? visibilityItems[0]; const scheduleTypeDefaultState = scheduleTypeItems.find( (i) => contest && i.value === contest.scheduleType, ) ?? scheduleTypeItems[0]; const groupItems = myGroups.map((v) => { return { value: '' + v.id, text: v.name, }; }); const groupIdDefaultState = myGroups.find((g) => g.id == contest?.groupId) ?? myGroups[0] ?? undefined; return (
navigate(back || '/home/contests')} />
{/* Левая панешь */}

{refactor ? `Редактирвоание контеста #${contestId} \"${contestById?.name}\"` : 'Создать контест'}
handleChange('name', v)} defaultState={contest.name ?? ''} /> handleChange('description', v)} defaultState={contest.description ?? ''} />
{ handleChange('scheduleType', v); }} weight="w-full" />
{ handleChange('visibility', v); }} defaultState={visibilityDefaultState} weight="w-full" />
{groupIdDefaultState ? (
{ handleChange( 'groupId', Number(v), ); }} weight="w-full" />
) : (
У вас нет группы вкоторой вы являетесь Администратором!
)}
{/* Даты */}
handleChange('startsAt', v) } /> handleChange('endsAt', v) } />
{/* Продолжительность и лимиты */}
handleChange( 'attemptDurationMinutes', Number(v), ) } /> handleChange('maxAttempts', Number(v)) } />
{/* Кнопки */}
{/* Правая панель */}
{/* Блок для тегов */}
{ setMissionFindInput(v); }} defaultState={missionFindInput} placeholder={`Наприме: \"458\" или \"Поиск наименьшего\"`} onKeyDown={(e) => { if (e.key == 'Enter') addMission(); }} /> {/* Выпадающие задачи */}
{globalMissions .filter( (v) => !contest?.missionIds?.includes( v.id, ), ) .filter((v) => (v.id + ' ' + v.name) .toLocaleLowerCase() .includes( missionFindInput.toLocaleLowerCase(), ), ) .map((v, i) => (
{ setMissionFindInput( v.id + ' ' + v.name, ); addMission(); }} > {highlightZ( '#' + v.id + ' ' + v.name, missionFindInput, )}
))}
{missions.map((v, i) => (
{'#' + v.id}
{v.name}
))}
); }; export default ContestEditor;