add contest mission input
This commit is contained in:
@@ -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 className="flex flex-wrap gap-[10px] mt-2">
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<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>
|
||||
|
||||
Reference in New Issue
Block a user