add filters
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
import { cn } from '../../../../lib/cn';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { Edit } from '../../../../assets/icons/input';
|
||||
import { Trash } from '../../../../assets/icons/input';
|
||||
import { useAppSelector } from '../../../../redux/hooks';
|
||||
|
||||
export interface MissionItemProps {
|
||||
@@ -83,7 +83,7 @@ const MissionItem: React.FC<MissionItemProps> = ({
|
||||
</div>
|
||||
<div className="h-[24px] w-[24px]">
|
||||
<img
|
||||
src={Edit}
|
||||
src={Trash}
|
||||
className={cn(
|
||||
'hover:bg-liquid-light rounded-[8px] transition-all duration-300',
|
||||
deleteStatus == 'loading' &&
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { cn } from '../../../lib/cn';
|
||||
import { useAppSelector } from '../../../redux/hooks';
|
||||
|
||||
export interface ArticleItemProps {
|
||||
id: number;
|
||||
@@ -9,6 +10,65 @@ export interface ArticleItemProps {
|
||||
|
||||
const ArticleItem: React.FC<ArticleItemProps> = ({ id, name, tags }) => {
|
||||
const navigate = useNavigate();
|
||||
|
||||
const filterTags = useAppSelector(
|
||||
(state) => state.store.articles.articleTagFilter,
|
||||
);
|
||||
const nameFilter = useAppSelector(
|
||||
(state) => state.store.articles.filterName,
|
||||
);
|
||||
|
||||
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;
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
@@ -26,7 +86,7 @@ const ArticleItem: React.FC<ArticleItemProps> = ({ id, name, tags }) => {
|
||||
#{id}
|
||||
</div>
|
||||
<div className="text-[18px] font-bold flex items-center bg-red-400r">
|
||||
{name}
|
||||
{highlightZ(name, nameFilter)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-[14px] flex text-liquid-light gap-[10px] mt-[10px]">
|
||||
@@ -36,6 +96,8 @@ const ArticleItem: React.FC<ArticleItemProps> = ({ id, name, tags }) => {
|
||||
className={cn(
|
||||
'rounded-full px-[16px] py-[8px] bg-liquid-lighter',
|
||||
v == 'Sertificated' && 'text-liquid-green',
|
||||
filterTags.includes(v) &&
|
||||
'border-liquid-brightmain border-[1px] border-solid text-liquid-brightmain',
|
||||
)}
|
||||
>
|
||||
{v}
|
||||
|
||||
@@ -2,7 +2,11 @@ import { useEffect } from 'react';
|
||||
import { SecondaryButton } from '../../../components/button/SecondaryButton';
|
||||
import { useAppDispatch, useAppSelector } from '../../../redux/hooks';
|
||||
import ArticleItem from './ArticleItem';
|
||||
import { setMenuActivePage } from '../../../redux/slices/store';
|
||||
import {
|
||||
setArticlesNameFilter,
|
||||
setArticlesTagFilter,
|
||||
setMenuActivePage,
|
||||
} from '../../../redux/slices/store';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { fetchArticles } from '../../../redux/slices/articles';
|
||||
import Filters from './Filter';
|
||||
@@ -15,39 +19,22 @@ const Articles = () => {
|
||||
const articles = useAppSelector(
|
||||
(state) => state.articles.fetchArticles.articles,
|
||||
);
|
||||
const status = useAppSelector(
|
||||
(state) => state.articles.fetchArticles.status,
|
||||
const tagsFilter = useAppSelector(
|
||||
(state) => state.store.articles.articleTagFilter,
|
||||
);
|
||||
const nameFilter = useAppSelector(
|
||||
(state) => state.store.articles.filterName,
|
||||
);
|
||||
const error = useAppSelector((state) => state.articles.fetchArticles.error);
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(setMenuActivePage('articles'));
|
||||
dispatch(fetchArticles({}));
|
||||
}, [dispatch]);
|
||||
dispatch(fetchArticles({ tags: tagsFilter }));
|
||||
}, []);
|
||||
|
||||
// ========================
|
||||
// Состояния загрузки / ошибки
|
||||
// ========================
|
||||
if (status === 'loading') {
|
||||
return (
|
||||
<div className="h-full w-full flex items-center justify-center text-liquid-light text-[18px]">
|
||||
Загрузка статей...
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (status === 'failed') {
|
||||
return (
|
||||
<div className="h-full w-full flex flex-col items-center justify-center text-liquid-red text-[18px]">
|
||||
Ошибка при загрузке статей
|
||||
{error && (
|
||||
<div className="text-liquid-light text-[14px] mt-2">
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
const filterTagsHandler = (value: string[]) => {
|
||||
dispatch(setArticlesTagFilter(value));
|
||||
dispatch(fetchArticles({ tags: value }));
|
||||
};
|
||||
|
||||
// ========================
|
||||
// Основной контент
|
||||
@@ -68,7 +55,14 @@ const Articles = () => {
|
||||
</div>
|
||||
|
||||
{/* Фильтры */}
|
||||
<Filters />
|
||||
<Filters
|
||||
onChangeTags={(value: string[]) => {
|
||||
filterTagsHandler(value);
|
||||
}}
|
||||
onChangeName={(value: string) => {
|
||||
dispatch(setArticlesNameFilter(value));
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* Список статей */}
|
||||
<div className="mt-[20px]">
|
||||
@@ -77,14 +71,15 @@ const Articles = () => {
|
||||
Пока нет статей
|
||||
</div>
|
||||
) : (
|
||||
articles.map((v) => <ArticleItem key={v.id} {...v} />)
|
||||
articles
|
||||
.filter((v) =>
|
||||
v.name
|
||||
.toLocaleLowerCase()
|
||||
.includes(nameFilter.toLocaleLowerCase()),
|
||||
)
|
||||
.map((v) => <ArticleItem key={v.id} {...v} />)
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Пагинация (пока заглушка) */}
|
||||
<div className="mt-[20px] text-liquid-light text-[14px]">
|
||||
pages
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,51 +1,24 @@
|
||||
import {
|
||||
FilterDropDown,
|
||||
FilterItem,
|
||||
} from '../../../components/filters/Filter';
|
||||
import { SorterDropDown } from '../../../components/filters/Sorter';
|
||||
import { FC } from 'react';
|
||||
import { TagFilter } from '../../../components/filters/TagFilter';
|
||||
import { SearchInput } from '../../../components/input/SearchInput';
|
||||
|
||||
const Filters = () => {
|
||||
const items: FilterItem[] = [
|
||||
{ text: 'React', value: 'react' },
|
||||
{ text: 'Vue', value: 'vue' },
|
||||
{ text: 'Angular', value: 'angular' },
|
||||
{ text: 'Svelte', value: 'svelte' },
|
||||
{ text: 'Next.js', value: 'next' },
|
||||
{ text: 'Nuxt', value: 'nuxt' },
|
||||
{ text: 'Solid', value: 'solid' },
|
||||
{ text: 'Qwik', value: 'qwik' },
|
||||
];
|
||||
interface ArticleFiltersProps {
|
||||
onChangeTags: (value: string[]) => void;
|
||||
onChangeName: (value: string) => void;
|
||||
}
|
||||
|
||||
const Filters: FC<ArticleFiltersProps> = ({ onChangeTags, onChangeName }) => {
|
||||
return (
|
||||
<div className=" h-[50px] mb-[20px] flex gap-[20px] items-center">
|
||||
<SearchInput onChange={() => {}} placeholder="Поиск задачи" />
|
||||
|
||||
<SorterDropDown
|
||||
items={[
|
||||
{
|
||||
value: '1',
|
||||
text: 'Сложность',
|
||||
},
|
||||
{
|
||||
value: '2',
|
||||
text: 'Дата создания',
|
||||
},
|
||||
{
|
||||
value: '3',
|
||||
text: 'ID',
|
||||
},
|
||||
]}
|
||||
onChange={(v) => {
|
||||
v;
|
||||
<SearchInput
|
||||
onChange={(value: string) => {
|
||||
onChangeName(value);
|
||||
}}
|
||||
placeholder="Поиск статьи"
|
||||
/>
|
||||
|
||||
<FilterDropDown
|
||||
items={items}
|
||||
defaultState={[]}
|
||||
onChange={(values) => {
|
||||
values;
|
||||
<TagFilter
|
||||
onChange={(value: string[]) => {
|
||||
onChangeTags(value);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -3,7 +3,10 @@ import { SecondaryButton } from '../../../components/button/SecondaryButton';
|
||||
import { cn } from '../../../lib/cn';
|
||||
import { useAppDispatch, useAppSelector } from '../../../redux/hooks';
|
||||
import ContestsBlock from './ContestsBlock';
|
||||
import { setMenuActivePage } from '../../../redux/slices/store';
|
||||
import {
|
||||
setContestsNameFilter,
|
||||
setMenuActivePage,
|
||||
} from '../../../redux/slices/store';
|
||||
import {
|
||||
fetchContests,
|
||||
fetchMyContests,
|
||||
@@ -21,6 +24,10 @@ const Contests = () => {
|
||||
(state) => state.contests.fetchContests,
|
||||
);
|
||||
|
||||
const nameFilter = useAppSelector(
|
||||
(state) => state.store.contests.filterName,
|
||||
);
|
||||
|
||||
// При загрузке страницы — выставляем активную вкладку и подгружаем контесты
|
||||
useEffect(() => {
|
||||
dispatch(setMenuActivePage('contests'));
|
||||
@@ -49,7 +56,11 @@ const Contests = () => {
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Filters />
|
||||
<Filters
|
||||
onChangeName={(v: string) => {
|
||||
dispatch(setContestsNameFilter(v));
|
||||
}}
|
||||
/>
|
||||
{status == 'loading' && (
|
||||
<div className="text-liquid-white p-4">
|
||||
Загрузка контестов...
|
||||
@@ -60,18 +71,30 @@ const Contests = () => {
|
||||
<ContestsBlock
|
||||
className="mb-[20px]"
|
||||
title="Текущие"
|
||||
contests={contests.filter(
|
||||
(c) => c.scheduleType != 'AlwaysOpen',
|
||||
)}
|
||||
contests={contests
|
||||
.filter((v) =>
|
||||
v.name
|
||||
.toLocaleLowerCase()
|
||||
.includes(
|
||||
nameFilter.toLocaleLowerCase(),
|
||||
),
|
||||
)
|
||||
.filter((c) => c.scheduleType != 'AlwaysOpen')}
|
||||
type="upcoming"
|
||||
/>
|
||||
|
||||
<ContestsBlock
|
||||
className="mb-[20px]"
|
||||
title="Постоянные"
|
||||
contests={contests.filter(
|
||||
(c) => c.scheduleType == 'AlwaysOpen',
|
||||
)}
|
||||
contests={contests
|
||||
.filter((v) =>
|
||||
v.name
|
||||
.toLocaleLowerCase()
|
||||
.includes(
|
||||
nameFilter.toLocaleLowerCase(),
|
||||
),
|
||||
)
|
||||
.filter((c) => c.scheduleType == 'AlwaysOpen')}
|
||||
type="past"
|
||||
/>
|
||||
</>
|
||||
|
||||
@@ -1,49 +1,18 @@
|
||||
import { FilterDropDown, FilterItem } from '../../../components/filters/Filter';
|
||||
import { SorterDropDown } from '../../../components/filters/Sorter';
|
||||
import { FC } from 'react';
|
||||
import { SearchInput } from '../../../components/input/SearchInput';
|
||||
|
||||
const Filters = () => {
|
||||
const items: FilterItem[] = [
|
||||
{ text: 'React', value: 'react' },
|
||||
{ text: 'Vue', value: 'vue' },
|
||||
{ text: 'Angular', value: 'angular' },
|
||||
{ text: 'Svelte', value: 'svelte' },
|
||||
{ text: 'Next.js', value: 'next' },
|
||||
{ text: 'Nuxt', value: 'nuxt' },
|
||||
{ text: 'Solid', value: 'solid' },
|
||||
{ text: 'Qwik', value: 'qwik' },
|
||||
];
|
||||
interface ContestFiltersProps {
|
||||
onChangeName: (value: string) => void;
|
||||
}
|
||||
|
||||
const Filters: FC<ContestFiltersProps> = ({ onChangeName }) => {
|
||||
return (
|
||||
<div className=" h-[50px] mb-[20px] flex gap-[20px] items-center">
|
||||
<SearchInput onChange={() => {}} placeholder="Поиск задачи" />
|
||||
|
||||
<SorterDropDown
|
||||
items={[
|
||||
{
|
||||
value: '1',
|
||||
text: 'Сложность',
|
||||
},
|
||||
{
|
||||
value: '2',
|
||||
text: 'Дата создания',
|
||||
},
|
||||
{
|
||||
value: '3',
|
||||
text: 'ID',
|
||||
},
|
||||
]}
|
||||
onChange={(v) => {
|
||||
v;
|
||||
}}
|
||||
/>
|
||||
|
||||
<FilterDropDown
|
||||
items={items}
|
||||
defaultState={[]}
|
||||
onChange={(values) => {
|
||||
values;
|
||||
<SearchInput
|
||||
onChange={(value: string) => {
|
||||
onChangeName(value);
|
||||
}}
|
||||
placeholder="Поиск контеста"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -14,7 +14,7 @@ import { NumberInput } from '../../../components/input/NumberInput';
|
||||
import {
|
||||
DropDownList,
|
||||
DropDownListItem,
|
||||
} from '../../../components/filters/DropDownList';
|
||||
} from '../../../components/input/DropDownList';
|
||||
import DateInput from '../../../components/input/DateInput';
|
||||
import { cn } from '../../../lib/cn';
|
||||
|
||||
|
||||
@@ -83,6 +83,61 @@ const PastContestItem: React.FC<PastContestItemProps> = ({
|
||||
(state) => state.contests.fetchParticipating,
|
||||
);
|
||||
|
||||
const nameFilter = useAppSelector(
|
||||
(state) => state.store.contests.filterName,
|
||||
);
|
||||
|
||||
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;
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
setRole(
|
||||
(() => {
|
||||
@@ -119,7 +174,9 @@ const PastContestItem: React.FC<PastContestItemProps> = ({
|
||||
navigate(`/contest/${contestId}?${params}`);
|
||||
}}
|
||||
>
|
||||
<div className="text-left font-bold text-[18px]">{name}</div>
|
||||
<div className="text-left font-bold text-[18px]">
|
||||
{highlightZ(name, nameFilter)}
|
||||
</div>
|
||||
<div className="text-center text-liquid-brightmain font-normal flex items-center justify-center">
|
||||
{username}
|
||||
</div>
|
||||
|
||||
@@ -98,6 +98,61 @@ const UpcoingContestItem: React.FC<UpcoingContestItemProps> = ({
|
||||
(state) => state.contests.fetchParticipating,
|
||||
);
|
||||
|
||||
const nameFilter = useAppSelector(
|
||||
(state) => state.store.contests.filterName,
|
||||
);
|
||||
|
||||
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;
|
||||
};
|
||||
|
||||
const query = useQuery();
|
||||
const username = query.get('username') ?? myname ?? '';
|
||||
|
||||
@@ -146,7 +201,9 @@ const UpcoingContestItem: React.FC<UpcoingContestItemProps> = ({
|
||||
navigate(`/contest/${contestId}?${params}`);
|
||||
}}
|
||||
>
|
||||
<div className="text-left font-bold text-[18px]">{name}</div>
|
||||
<div className="text-left font-bold text-[18px]">
|
||||
{highlightZ(name, nameFilter)}
|
||||
</div>
|
||||
<div className="text-center text-liquid-brightmain font-normal flex items-center justify-center">
|
||||
{username}
|
||||
</div>
|
||||
|
||||
@@ -1,51 +1,24 @@
|
||||
import {
|
||||
FilterDropDown,
|
||||
FilterItem,
|
||||
} from '../../../components/filters/Filter';
|
||||
import { SorterDropDown } from '../../../components/filters/Sorter';
|
||||
import { FC } from 'react';
|
||||
import { TagFilter } from '../../../components/filters/TagFilter';
|
||||
import { SearchInput } from '../../../components/input/SearchInput';
|
||||
|
||||
const Filters = () => {
|
||||
const items: FilterItem[] = [
|
||||
{ text: 'React', value: 'react' },
|
||||
{ text: 'Vue', value: 'vue' },
|
||||
{ text: 'Angular', value: 'angular' },
|
||||
{ text: 'Svelte', value: 'svelte' },
|
||||
{ text: 'Next.js', value: 'next' },
|
||||
{ text: 'Nuxt', value: 'nuxt' },
|
||||
{ text: 'Solid', value: 'solid' },
|
||||
{ text: 'Qwik', value: 'qwik' },
|
||||
];
|
||||
interface MissionFiltersProps {
|
||||
onChangeTags: (value: string[]) => void;
|
||||
onChangeName: (value: string) => void;
|
||||
}
|
||||
|
||||
const Filters: FC<MissionFiltersProps> = ({ onChangeTags, onChangeName }) => {
|
||||
return (
|
||||
<div className=" h-[50px] mb-[20px] flex gap-[20px] items-center">
|
||||
<SearchInput onChange={() => {}} placeholder="Поиск задачи" />
|
||||
|
||||
<SorterDropDown
|
||||
items={[
|
||||
{
|
||||
value: '1',
|
||||
text: 'Сложность',
|
||||
},
|
||||
{
|
||||
value: '2',
|
||||
text: 'Дата создания',
|
||||
},
|
||||
{
|
||||
value: '3',
|
||||
text: 'ID',
|
||||
},
|
||||
]}
|
||||
onChange={(v) => {
|
||||
v;
|
||||
<SearchInput
|
||||
onChange={(value: string) => {
|
||||
onChangeName(value);
|
||||
}}
|
||||
placeholder="Поиск задачи"
|
||||
/>
|
||||
|
||||
<FilterDropDown
|
||||
items={items}
|
||||
defaultState={[]}
|
||||
onChange={(values) => {
|
||||
values;
|
||||
<TagFilter
|
||||
onChange={(value: string[]) => {
|
||||
onChangeTags(value);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { cn } from '../../../lib/cn';
|
||||
import { IconError, IconSuccess } from '../../../assets/icons/missions';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useAppSelector } from '../../../redux/hooks';
|
||||
|
||||
export interface MissionItemProps {
|
||||
id: number;
|
||||
@@ -38,6 +39,61 @@ const MissionItem: React.FC<MissionItemProps> = ({
|
||||
}) => {
|
||||
const navigate = useNavigate();
|
||||
|
||||
const nameFilter = useAppSelector(
|
||||
(state) => state.store.missions.filterName,
|
||||
);
|
||||
|
||||
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;
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
@@ -55,7 +111,9 @@ const MissionItem: React.FC<MissionItemProps> = ({
|
||||
}}
|
||||
>
|
||||
<div className="text-[18px] font-bold">#{id}</div>
|
||||
<div className="text-[18px] font-bold">{name}</div>
|
||||
<div className="text-[18px] font-bold">
|
||||
{highlightZ(name, nameFilter)}
|
||||
</div>
|
||||
<div className="text-[12px] text-right">
|
||||
стандартный ввод/вывод {formatMilliseconds(timeLimit)},{' '}
|
||||
{formatBytesToMB(memoryLimit)}
|
||||
|
||||
@@ -2,7 +2,11 @@ import MissionItem from './MissionItem';
|
||||
import { SecondaryButton } from '../../../components/button/SecondaryButton';
|
||||
import { useAppDispatch, useAppSelector } from '../../../redux/hooks';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { setMenuActivePage } from '../../../redux/slices/store';
|
||||
import {
|
||||
setMenuActivePage,
|
||||
setMissionsNameFilter,
|
||||
setMissionsTagFilter,
|
||||
} from '../../../redux/slices/store';
|
||||
import { fetchMissions } from '../../../redux/slices/missions';
|
||||
import ModalCreate from './ModalCreate';
|
||||
import Filters from './Filter';
|
||||
@@ -25,10 +29,21 @@ const Missions = () => {
|
||||
|
||||
const missions = useAppSelector((state) => state.missions.missions);
|
||||
|
||||
const nameFilter = useAppSelector(
|
||||
(state) => state.store.missions.filterName,
|
||||
);
|
||||
const tagsFilter = useAppSelector(
|
||||
(state) => state.store.articles.articleTagFilter,
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(setMenuActivePage('missions'));
|
||||
dispatch(fetchMissions({}));
|
||||
dispatch(fetchMissions({ tags: tagsFilter }));
|
||||
}, []);
|
||||
const filterTagsHandler = (value: string[]) => {
|
||||
dispatch(setMissionsTagFilter(value));
|
||||
dispatch(fetchMissions({ tags: value }));
|
||||
};
|
||||
|
||||
return (
|
||||
<div className=" h-full w-full box-border p-[20px] pt-[20px]">
|
||||
@@ -46,28 +61,39 @@ const Missions = () => {
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Filters />
|
||||
<Filters
|
||||
onChangeTags={(value: string[]) => {
|
||||
filterTagsHandler(value);
|
||||
}}
|
||||
onChangeName={(value: string) => {
|
||||
dispatch(setMissionsNameFilter(value));
|
||||
}}
|
||||
/>
|
||||
|
||||
<div>
|
||||
{missions.map((v, i) => (
|
||||
<MissionItem
|
||||
key={i}
|
||||
id={v.id}
|
||||
authorId={v.authorId}
|
||||
name={v.name}
|
||||
difficulty={'Easy'}
|
||||
tags={v.tags}
|
||||
timeLimit={1000}
|
||||
memoryLimit={256 * 1024 * 1024}
|
||||
createdAt={v.createdAt}
|
||||
updatedAt={v.updatedAt}
|
||||
type={i % 2 == 0 ? 'first' : 'second'}
|
||||
status={'empty'}
|
||||
/>
|
||||
))}
|
||||
{missions
|
||||
.filter((v) =>
|
||||
v.name
|
||||
.toLowerCase()
|
||||
.includes(nameFilter.toLocaleLowerCase()),
|
||||
)
|
||||
.map((v, i) => (
|
||||
<MissionItem
|
||||
key={i}
|
||||
id={v.id}
|
||||
authorId={v.authorId}
|
||||
name={v.name}
|
||||
difficulty={'Easy'}
|
||||
tags={v.tags}
|
||||
timeLimit={1000}
|
||||
memoryLimit={256 * 1024 * 1024}
|
||||
createdAt={v.createdAt}
|
||||
updatedAt={v.updatedAt}
|
||||
type={i % 2 == 0 ? 'first' : 'second'}
|
||||
status={'empty'}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div>pages</div>
|
||||
</div>
|
||||
|
||||
<ModalCreate setActive={setModalActive} active={modalActive} />
|
||||
|
||||
@@ -14,7 +14,7 @@ import ConfirmModal from '../../../../components/modal/ConfirmModal';
|
||||
import {
|
||||
DropDownList,
|
||||
DropDownListItem,
|
||||
} from '../../../../components/filters/DropDownList';
|
||||
} from '../../../../components/input/DropDownList';
|
||||
import { ReverseButton } from '../../../../components/button/ReverseButton';
|
||||
|
||||
interface ModalUpdateProps {
|
||||
|
||||
Reference in New Issue
Block a user