add contest mission input
This commit is contained in:
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user