add contests

This commit is contained in:
Виталий Лавшонок
2025-12-05 23:42:18 +03:00
parent 358c7def78
commit fd34761745
36 changed files with 2518 additions and 710 deletions

View File

@@ -1,9 +1,17 @@
import { SearchInput } from '../../../components/input/SearchInput';
import { useAppDispatch } from '../../../redux/hooks';
import { setGroupFilter } from '../../../redux/slices/store';
const Filters = () => {
const dispatch = useAppDispatch();
return (
<div className=" h-[50px] mb-[20px] flex gap-[20px] items-center">
<SearchInput onChange={() => {}} placeholder="Поиск группы" />
<SearchInput
onChange={(v: string) => {
dispatch(setGroupFilter(v));
}}
placeholder="Поиск группы"
/>
</div>
);
};

View File

@@ -1,18 +1,12 @@
import { cn } from '../../../lib/cn';
import {
Book,
UserAdd,
Edit,
EyeClosed,
EyeOpen,
} from '../../../assets/icons/groups';
import { Book, UserAdd, Edit } from '../../../assets/icons/groups';
import { useNavigate } from 'react-router-dom';
import { GroupInvite, GroupUpdate } from './Groups';
import { useAppSelector } from '../../../redux/hooks';
export interface GroupItemProps {
id: number;
role: 'menager' | 'member' | 'owner' | 'viewer';
visible: boolean;
name: string;
description: string;
setUpdateActive: (value: any) => void;
@@ -43,7 +37,6 @@ const IconComponent: React.FC<IconComponentProps> = ({ src, onClick }) => {
const GroupItem: React.FC<GroupItemProps> = ({
id,
name,
visible,
description,
setUpdateGroup,
setUpdateActive,
@@ -53,6 +46,61 @@ const GroupItem: React.FC<GroupItemProps> = ({
}) => {
const navigate = useNavigate();
const filter = useAppSelector(
(state) => state.store.group.groupFilter,
).toLowerCase();
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(
@@ -66,7 +114,10 @@ const GroupItem: React.FC<GroupItemProps> = ({
className="bg-liquid-brightmain rounded-[10px]"
/>
<div className="grid grid-flow-row grid-rows-[1fr,24px]">
<div className="text-[18px] font-bold">{name}</div>
<div className="text-[18px] font-bold">
{highlightZ(name, filter)}
</div>
<div className=" flex gap-[10px]">
{type == 'manage' && (
<IconComponent
@@ -86,8 +137,6 @@ const GroupItem: React.FC<GroupItemProps> = ({
}}
/>
)}
{visible == false && <IconComponent src={EyeOpen} />}
{visible == true && <IconComponent src={EyeClosed} />}
</div>
</div>
</div>

View File

@@ -4,7 +4,7 @@ import { cn } from '../../../lib/cn';
import { useAppDispatch, useAppSelector } from '../../../redux/hooks';
import GroupsBlock from './GroupsBlock';
import { setMenuActivePage } from '../../../redux/slices/store';
import { fetchMyGroups } from '../../../redux/slices/groups';
import { fetchMyGroups, Group } from '../../../redux/slices/groups';
import ModalCreate from './ModalCreate';
import ModalUpdate from './ModalUpdate';
import Filters from './Filter';
@@ -45,6 +45,7 @@ const Groups = () => {
const groupsError = useAppSelector(
(store) => store.groups.fetchMyGroups.error,
);
const filter = useAppSelector((state) => state.store.group.groupFilter);
// Берём текущего пользователя
const currentUserName = useAppSelector((store) => store.auth.username);
@@ -54,17 +55,21 @@ const Groups = () => {
dispatch(fetchMyGroups());
}, [dispatch]);
// Разделяем группы
const { managedGroups, currentGroups, hiddenGroups } = useMemo(() => {
const applyFilter = (groups: Group[], filter: string) => {
if (!filter || filter.trim() === '') return groups;
const normalized = filter.toLowerCase();
return groups.filter((g) => g.name.toLowerCase().includes(normalized));
};
const { managedGroups, currentGroups } = useMemo(() => {
if (!groups || !currentUserName) {
return { managedGroups: [], currentGroups: [], hiddenGroups: [] };
return { managedGroups: [], currentGroups: [] };
}
const managed: typeof groups = [];
const current: typeof groups = [];
const hidden: typeof groups = []; // пока пустые, без логики
groups.forEach((group) => {
applyFilter(groups, filter).forEach((group) => {
const me = group.members.find(
(m) => m.username === currentUserName,
);
@@ -80,9 +85,8 @@ const Groups = () => {
return {
managedGroups: managed,
currentGroups: current,
hiddenGroups: hidden,
};
}, [groups, currentUserName]);
}, [groups, currentUserName, filter]);
return (
<div className="h-full w-[calc(100%+250px)] box-border p-[20px] pt-[20px]">
@@ -137,16 +141,6 @@ const Groups = () => {
setInviteGroup={setInviteGroup}
type="member"
/>
<GroupsBlock
className="mb-[20px]"
title="Скрытые"
groups={hiddenGroups} // пока пусто
setUpdateActive={setModalUpdateActive}
setUpdateGroup={setUpdateGroup}
setInviteActive={setModalInviteActive}
setInviteGroup={setInviteGroup}
type="member"
/>
</>
)}
</div>

View File

@@ -65,7 +65,6 @@ const GroupsBlock: FC<GroupsBlockProps> = ({
<GroupItem
key={i}
id={v.id}
visible={true}
description={v.description}
setUpdateActive={setUpdateActive}
setUpdateGroup={setUpdateGroup}