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';
import { useQuery } from '../hooks/useQuery';
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 {
DropDownList,
@@ -28,6 +28,54 @@ interface Mission {
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 {
if (!localDateTime) return '';
@@ -58,7 +106,7 @@ const ContestEditor = () => {
(state) => state.contests.createContest.status,
);
const [missionIdInput, setMissionIdInput] = useState<string>('');
const [missionFindInput, setMissionFindInput] = useState<string>('');
const now = new Date();
const plus60 = new Date(now.getTime() + 60 * 60 * 1000);
@@ -107,6 +155,8 @@ const ContestEditor = () => {
(state) => state.contests.fetchContestById,
);
const globalMissions = useAppSelector((state) => state.missions.missions);
const myGroups = useAppSelector(
(state) => state.groups.fetchMyGroups.groups,
).filter((group) =>
@@ -153,7 +203,15 @@ const ContestEditor = () => {
};
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;
dispatch(fetchMissionById(id))
.unwrap()
@@ -163,7 +221,7 @@ const ContestEditor = () => {
...prev,
missionIds: [...(prev.missionIds ?? []), id],
}));
setMissionIdInput('');
setMissionFindInput('');
})
.catch((err) => {
err;
@@ -199,12 +257,13 @@ const ContestEditor = () => {
useEffect(() => {
if (refactor) {
dispatch(fetchContestById(contestId));
dispatch(fetchMyGroups());
dispatch(fetchMissions({}));
}
}, [refactor]);
useEffect(() => {
if (refactor && contestByIdstatus == 'successful' && contestById) {
dispatch(fetchMyGroups());
setContest({
...contestById,
// groupIds: contestById.groups.map(group => group.groupId),
@@ -445,24 +504,22 @@ const ContestEditor = () => {
</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">
<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">
<Input
name="missionId"
autocomplete="missionId"
className="mt-[20px] max-w-[600px]"
type="number"
label="ID миссии"
label="Введите название или ID миссии"
type="text"
onChange={(v) => {
setMissionIdInput(v);
setMissionFindInput(v);
}}
defaultState={missionIdInput}
placeholder="458"
defaultState={missionFindInput}
placeholder={`Наприме: \"458\" или \"Поиск наименьшего\"`}
onKeyDown={(e) => {
if (e.key == 'Enter') addMission();
}}
@@ -472,18 +529,70 @@ const ContestEditor = () => {
text="Добавить"
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 className="flex flex-wrap gap-[10px] mt-2">
<div className="gap-[10px] mt-[20px]">
{missions.map((v, i) => (
<div
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>
<span>{v.name}</span>
<div>{'#' + v.id}</div>
<div>{v.name}</div>
<button
onClick={() => removeMission(v.id)}
className="text-liquid-red font-bold ml-[5px]"
className="text-liquid-red font-bold ml-[5px] absolute right-[16px]"
>
×
</button>