add filters

This commit is contained in:
Виталий Лавшонок
2025-12-10 00:04:20 +03:00
parent 14d2f5cbf1
commit 02de330034
23 changed files with 639 additions and 212 deletions

View File

@@ -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}

View File

@@ -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>
);

View File

@@ -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>