add contest mission input

This commit is contained in:
Виталий Лавшонок
2025-12-10 03:11:27 +03:00
parent c761f337b1
commit 0c41cc59b9

View File

@@ -12,7 +12,7 @@ import {
} from '../redux/slices/contests'; } from '../redux/slices/contests';
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, fetchMissions } from '../redux/slices/missions';
import { ReverseButton } from '../components/button/ReverseButton'; import { ReverseButton } from '../components/button/ReverseButton';
import { import {
DropDownList, DropDownList,
@@ -28,6 +28,54 @@ interface Mission {
name: string; 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(
<span key={i} className="bg-yellow-400 text-black rounded px-1">
{chunk}
</span>,
);
i = j;
}
}
return result;
};
function toUtc(localDateTime?: string): string { function toUtc(localDateTime?: string): string {
if (!localDateTime) return ''; if (!localDateTime) return '';
@@ -58,7 +106,7 @@ const ContestEditor = () => {
(state) => state.contests.createContest.status, (state) => state.contests.createContest.status,
); );
const [missionIdInput, setMissionIdInput] = useState<string>(''); const [missionFindInput, setMissionFindInput] = useState<string>('');
const now = new Date(); const now = new Date();
const plus60 = new Date(now.getTime() + 60 * 60 * 1000); const plus60 = new Date(now.getTime() + 60 * 60 * 1000);
@@ -107,6 +155,8 @@ const ContestEditor = () => {
(state) => state.contests.fetchContestById, (state) => state.contests.fetchContestById,
); );
const globalMissions = useAppSelector((state) => state.missions.missions);
const myGroups = useAppSelector( const myGroups = useAppSelector(
(state) => state.groups.fetchMyGroups.groups, (state) => state.groups.fetchMyGroups.groups,
).filter((group) => ).filter((group) =>
@@ -153,7 +203,15 @@ const ContestEditor = () => {
}; };
const addMission = () => { const addMission = () => {
const id = Number(missionIdInput.trim()); 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; if (!id || contest.missionIds?.includes(id)) return;
dispatch(fetchMissionById(id)) dispatch(fetchMissionById(id))
.unwrap() .unwrap()
@@ -163,7 +221,7 @@ const ContestEditor = () => {
...prev, ...prev,
missionIds: [...(prev.missionIds ?? []), id], missionIds: [...(prev.missionIds ?? []), id],
})); }));
setMissionIdInput(''); setMissionFindInput('');
}) })
.catch((err) => { .catch((err) => {
err; err;
@@ -199,12 +257,13 @@ const ContestEditor = () => {
useEffect(() => { useEffect(() => {
if (refactor) { if (refactor) {
dispatch(fetchContestById(contestId)); dispatch(fetchContestById(contestId));
dispatch(fetchMyGroups());
dispatch(fetchMissions({}));
} }
}, [refactor]); }, [refactor]);
useEffect(() => { useEffect(() => {
if (refactor && contestByIdstatus == 'successful' && contestById) { if (refactor && contestByIdstatus == 'successful' && contestById) {
dispatch(fetchMyGroups());
setContest({ setContest({
...contestById, ...contestById,
// groupIds: contestById.groups.map(group => group.groupId), // groupIds: contestById.groups.map(group => group.groupId),
@@ -445,24 +504,22 @@ const ContestEditor = () => {
</div> </div>
{/* Правая панель */} {/* Правая панель */}
<div className="overflow-y-auto min-h-0 overflow-hidden"> <div className="min-h-0 ">
<div className="p-4 border-r border-gray-700 flex flex-col h-full"> <div className="p-4 border-r border-gray-700 flex flex-col h-full">
<h2 className="text-lg font-semibold mb-3 text-gray-100"></h2>
{/* Блок для тегов */} {/* Блок для тегов */}
<div className="mt-[20px] max-w-[600px]"> <div className="mt-[20px] max-w-[600px] relative">
<div className="grid grid-cols-[1fr,140px] items-end gap-2"> <div className="grid grid-cols-[1fr,140px] items-end gap-2">
<Input <Input
name="missionId" name="missionId"
autocomplete="missionId" autocomplete="missionId"
className="mt-[20px] max-w-[600px]" className="mt-[20px] max-w-[600px]"
type="number" label="Введите название или ID миссии"
label="ID миссии" type="text"
onChange={(v) => { onChange={(v) => {
setMissionIdInput(v); setMissionFindInput(v);
}} }}
defaultState={missionIdInput} defaultState={missionFindInput}
placeholder="458" placeholder={`Наприме: \"458\" или \"Поиск наименьшего\"`}
onKeyDown={(e) => { onKeyDown={(e) => {
if (e.key == 'Enter') addMission(); if (e.key == 'Enter') addMission();
}} }}
@@ -472,18 +529,70 @@ const ContestEditor = () => {
text="Добавить" text="Добавить"
className="h-[40px] w-[140px]" className="h-[40px] w-[140px]"
/> />
{/* Выпадающие задачи */}
<div
className={cn(
'absolute rounded-[10px] bg-liquid-background w-[590px] left-0 top-[100px] z-50 transition-all duration-300',
'grid overflow-hidden border-liquid-lighter border-[3px] border-solid',
missionFindInput
? 'grid-rows-[1fr] opacity-100'
: 'grid-rows-[0fr] opacity-0 pointer-events-none',
)}
>
<div className="overflow-hidden p-[8px]">
<div className="overflow-y-scroll max-h-[250px] thin-scrollbar grid gap-[20px]">
{globalMissions
.filter(
(v) =>
!contest?.missionIds?.includes(
v.id,
),
)
.filter((v) =>
(v.id + ' ' + v.name)
.toLocaleLowerCase()
.includes(
missionFindInput.toLocaleLowerCase(),
),
)
.map((v, i) => (
<div
key={i}
className="hover:bg-liquid-lighter rounded-[10px] px-[12px] py-[4px] transition-colors duration-300 cursor-pointer"
onClick={() => {
setMissionFindInput(
v.id +
' ' +
v.name,
);
addMission();
}}
>
{highlightZ(
'#' +
v.id +
' ' +
v.name,
missionFindInput,
)}
</div>
))}
</div>
</div>
</div>
</div> </div>
<div className="flex flex-wrap gap-[10px] mt-2"> <div className="gap-[10px] mt-[20px]">
{missions.map((v, i) => ( {missions.map((v, i) => (
<div <div
key={i} key={i}
className="flex items-center gap-1 bg-liquid-lighter px-3 py-1 rounded-full" className="grid grid-cols-[60px,1fr,24px] gap-1 bg-liquid-lighter px-[16px] py-[8px] rounded-[10px] relative mb-[10px] items-center"
> >
<span>{v.id}</span> <div>{'#' + v.id}</div>
<span>{v.name}</span> <div>{v.name}</div>
<button <button
onClick={() => removeMission(v.id)} onClick={() => removeMission(v.id)}
className="text-liquid-red font-bold ml-[5px]" className="text-liquid-red font-bold ml-[5px] absolute right-[16px]"
> >
× ×
</button> </button>