Files
LiquidCode_Frontend/src/views/home/missions/ModalCreate.tsx
Виталий Лавшонок 106510e3c9 Main branch changes
2025-12-25 17:35:56 +03:00

265 lines
10 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { FC, useEffect, useState } from 'react';
import { Modal } from '../../../components/modal/Modal';
import { PrimaryButton } from '../../../components/button/PrimaryButton';
import { SecondaryButton } from '../../../components/button/SecondaryButton';
import { Input } from '../../../components/input/Input';
import { useAppDispatch, useAppSelector } from '../../../redux/hooks';
import {
setMissionsStatus,
uploadMission,
} from '../../../redux/slices/missions';
import { toastSuccess } from '../../../lib/toastNotification';
import { cn } from '../../../lib/cn';
import { Link } from 'react-router-dom';
import { NumberInput } from '../../../components/input/NumberInput';
import { DropDownList, DropDownListItem } from '../../../components/input/DropDownList';
interface ModalCreateProps {
active: boolean;
setActive: (value: boolean) => void;
}
const ModalCreate: FC<ModalCreateProps> = ({ active, setActive }) => {
type TagsMode = 'fromArchive' | 'none' | 'custom';
const [difficulty, setDifficulty] = useState<number>(1);
const [file, setFile] = useState<File | null>(null);
const [tagInput, setTagInput] = useState<string>('');
const [tags, setTags] = useState<string[]>([]);
const [tagsMode, setTagsMode] = useState<TagsMode>('fromArchive');
const status = useAppSelector((state) => state.missions.statuses.upload);
const dispatch = useAppDispatch();
const [clickSubmit, setClickSubmit] = useState<boolean>(false);
const normalizeTags = (raw: string[]): string[] => {
const result: string[] = [];
const seen = new Set<string>();
for (const t of raw) {
const trimmed = t.trim();
if (!trimmed) continue;
const key = trimmed.toLowerCase();
if (seen.has(key)) continue;
seen.add(key);
result.push(trimmed);
}
return result;
};
const addTag = () => {
const parts = tagInput.split(',').map((v) => v.trim());
const next = normalizeTags([...tags, ...parts]);
setTags(next);
setTagInput('');
};
const removeTag = (tagToRemove: string) => {
setTags(tags.filter((tag) => tag !== tagToRemove));
};
const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
if (e.target.files && e.target.files[0]) {
setFile(e.target.files[0]);
}
};
const handleSubmit = async () => {
setClickSubmit(true);
if (!file) return;
const payloadTags =
tagsMode === 'custom' ? normalizeTags(tags) : undefined;
dispatch(
uploadMission({
missionFile: file,
difficulty,
tags: payloadTags,
}),
);
};
useEffect(() => {
if (status === 'successful') {
toastSuccess('Миссия создана!');
setDifficulty(1);
setTags([]);
setFile(null);
setTagInput('');
setTagsMode('fromArchive');
dispatch(setMissionsStatus({ key: 'upload', status: 'idle' }));
setActive(false);
}
}, [status]);
useEffect(() => {
if (active == true) {
setClickSubmit(false);
}
dispatch(setMissionsStatus({ key: 'upload', status: 'idle' }));
}, [active]);
const getDifficultyErrorMessage = (): string => {
if (!clickSubmit) return '';
if (!Number.isFinite(difficulty) || difficulty < 1)
return 'Укажите сложность (минимум 1)';
return '';
};
const tagsModeItems: DropDownListItem[] = [
{ text: 'Теги: из архива', value: 'fromArchive' },
{ text: 'Теги: без тегов', value: 'none' },
{ text: 'Теги: свои', value: 'custom' },
];
const isTagsMode = (v: string): v is TagsMode =>
v === 'fromArchive' || v === 'none' || v === 'custom';
const currentTagsModeItem =
tagsModeItems.find((i) => i.value === tagsMode) ?? tagsModeItems[0];
useEffect(() => {
if (tagsMode === 'none') {
setTags([]);
setTagInput('');
}
}, [tagsMode]);
return (
<Modal
className="bg-liquid-background border-liquid-lighter border-[2px] p-[25px] rounded-[20px] text-liquid-white"
onOpenChange={setActive}
open={active}
backdrop="blur"
>
<div className="w-[500px]">
<div className="font-bold text-[30px]">Добавить задачу</div>
<NumberInput
name="difficulty"
className="mt-[10px]"
label="Сложность"
defaultState={difficulty}
minValue={1}
maxValue={3500}
onChange={(v) => setDifficulty(v)}
placeholder="1"
error={getDifficultyErrorMessage()}
/>
<div className="mt-4">
<label className="block mb-2">Файл задачи</label>
<label className="cursor-pointer inline-flex items-center justify-center px-4 py-2 bg-liquid-lighter hover:bg-liquid-dark transition-colors rounded-[10px] text-liquid-white font-medium">
{file ? file.name : 'Выбрать файл'}
<input
type="file"
onChange={handleFileChange}
accept=".zip"
className="hidden"
/>
</label>
{
<div
className={cn(
'text-liquid-red text-[14px] h-auto text-left mt-[5px] whitespace-pre-line overflow-hidden ',
(!clickSubmit || file) && 'h-0 mt-0',
)}
>
Необходимо выбрать файл задачи
</div>
}
</div>
{/* Теги */}
<div className="mb-[50px] max-w-[600px]">
<div className="mt-[20px]">
<div className="text-[18px] text-liquid-white font-medium h-[23px] mb-[10px]">
Режим тегов
</div>
<DropDownList
items={tagsModeItems}
defaultState={currentTagsModeItem}
onChange={(v) => {
if (isTagsMode(v)) setTagsMode(v);
}}
weight="w-full"
/>
{tagsMode === 'none' && (
<div className="text-liquid-red text-[14px] mt-[8px] whitespace-pre-line">
Пустой список тегов сейчас не поддерживается в multipart.
Будет использован режим из архива.
</div>
)}
</div>
{tagsMode === 'custom' && (
<>
<div className="grid grid-cols-[1fr,140px] items-end gap-2">
<Input
name="missionTags"
autocomplete="missionTags"
className="mt-[20px] max-w-[600px]"
type="text"
label="Теги (через запятую)"
onChange={(v) => setTagInput(v)}
defaultState={tagInput}
placeholder="arrays, dp"
onKeyDown={(e) => {
if (e.key === 'Enter') addTag();
}}
/>
<PrimaryButton
onClick={addTag}
text="Добавить"
className="h-[40px] w-[140px]"
/>
</div>
<div className="flex flex-wrap gap-[10px] mt-2">
{tags.map((tag) => (
<div
key={tag}
className="flex items-center gap-1 bg-liquid-lighter px-3 py-1 rounded-full"
>
<span>{tag}</span>
<button
onClick={() => removeTag(tag)}
className="text-liquid-red font-bold ml-[5px]"
>
×
</button>
</div>
))}
</div>
</>
)}
</div>
<div>
Создать пакет задачи можно на платформе{' '}
<Link
to={'https://polygon.codeforces.com'}
target="_blank"
className="text-[#7489ff] hover:text-[#8c9dfd] transition-color duration-300"
>
polygon
</Link>
</div>
<div className="flex flex-row w-full items-center justify-end mt-[20px] gap-[20px]">
<PrimaryButton
onClick={handleSubmit}
text={status === 'loading' ? 'Загрузка...' : 'Создать'}
disabled={status === 'loading'}
/>
<SecondaryButton
onClick={() => setActive(false)}
text="Отмена"
/>
</div>
</div>
</Modal>
);
};
export default ModalCreate;