This commit is contained in:
Виталий Лавшонок
2025-11-08 15:11:53 +03:00
parent b12a3acf1d
commit f7924cd564
3 changed files with 110 additions and 115 deletions

View File

@@ -1,124 +1,120 @@
import React from 'react'; import React from 'react';
import { cn } from '../../lib/cn'; import { cn } from '../../lib/cn';
import { checkMark, chevroneDropDownList } from '../../assets/icons/input'; import { checkMark } from '../../assets/icons/input';
import { useClickOutside } from '../../hooks/useClickOutside'; import { useClickOutside } from '../../hooks/useClickOutside';
import { Sort, SortActive } from '../../assets/icons/filters';
export interface FilterItem { export interface FilterItem {
text: string; text: string;
value: string; value: string;
} }
interface FilterProps { interface FilterProps {
disabled?: boolean; disabled?: boolean;
className?: string; className?: string;
onChange: (state: string[]) => void; // теперь массив выбранных значений onChange: (items: FilterItem[]) => void;
defaultState?: FilterItem[]; defaultState?: FilterItem[];
items: FilterItem[]; items: FilterItem[];
} }
export const Filter: React.FC<FilterProps> = ({ export const Filter: React.FC<FilterProps> = ({
className = '', disabled = false,
onChange, className = '',
defaultState = [], onChange,
items = [{ text: '', value: '' }], defaultState = [],
items = [],
}) => { }) => {
if (items.length === 0) items.push({ text: '', value: '' }); const [value, setValue] = React.useState<FilterItem[]>(defaultState);
const [active, setActive] = React.useState(false);
const [selectedValues, setSelectedValues] = const ref = React.useRef<HTMLDivElement>(null);
React.useState<FilterItem[]>(defaultState);
const [active, setActive] = React.useState<boolean>(false);
React.useEffect(() => { useClickOutside(ref, () => {
onChange(selectedValues.map((v) => v.value)); setActive(false);
}, [selectedValues]); });
const ref = React.useRef<HTMLDivElement>(null); React.useEffect(() => {
useClickOutside(ref, () => setActive(false)); onChange(value);
}, [value]);
const toggleItem = (item: FilterItem) => { const toggleItem = (item: FilterItem) => {
const exists = selectedValues.some((v) => v.value === item.value); const exists = value.some((val) => val.value === item.value);
if (exists) { if (exists) {
setSelectedValues( setValue(value.filter((val) => val.value !== item.value));
selectedValues.filter((v) => v.value !== item.value), } else {
); setValue([...value, item]);
} else { }
setSelectedValues([...selectedValues, item]); };
}
};
const displayText = return (
selectedValues.length > 0 <div className={cn('relative', className)} ref={ref}>
? selectedValues.map((v) => v.text).join(', ') <div
: 'Выберите...'; className={cn(
'items-center h-[40px] rounded-full bg-liquid-lighter w-[40px] flex',
return ( 'text-[18px] font-bold cursor-pointer select-none',
<div className={cn('relative', className)} ref={ref}> 'overflow-hidden',
<div (active || value.length > 0) && 'w-fit border-liquid-brightmain border-[1px] border-solid',
className={cn( )}
'flex items-center h-[40px] rounded-[10px] bg-liquid-lighter px-[16px] w-[220px]', onClick={() => {
'text-[16px] font-bold cursor-pointer select-none', if (!disabled) setActive(!active);
'transition-all active:scale-95 duration-300 overflow-hidden whitespace-nowrap text-ellipsis', }}
)} >
onClick={() => setActive(!active)} <div className="text-liquid-brightmain pl-[42px] pr-[16px] w-fit">
title={displayText} {value.length}
>
{displayText}
</div>
<img
src={chevroneDropDownList}
className={cn(
'absolute right-[16px] h-[24px] w-[24px] top-[8.5px] rotate-0 transition-all duration-300 pointer-events-none',
active && 'rotate-180',
)}
/>
<div
className={cn(
'absolute rounded-[10px] bg-liquid-lighter w-[220px] left-0 top-[48px] z-50 transition-all duration-300',
'grid overflow-hidden',
active
? 'grid-rows-[1fr] opacity-100'
: 'grid-rows-[0fr] opacity-0',
)}
>
<div className="overflow-hidden p-[8px]">
<div
className={cn(
'overflow-y-scroll max-h-[220px] thin-scrollbar pr-[8px]',
'grid grid-cols-2 gap-[4px]',
)}
>
{items.map((v, i) => {
const isSelected = selectedValues.some(
(sel) => sel.value === v.value,
);
return (
<div
key={i}
className={cn(
'cursor-pointer h-[36px] relative transition-all duration-300 flex items-center pl-[8px] pr-[24px] rounded-[6px]',
'text-[14px] font-medium select-none',
'hover:bg-liquid-background',
isSelected &&
'bg-liquid-background font-semibold',
)}
onClick={() => toggleItem(v)}
>
{v.text}
{isSelected && (
<img
src={checkMark}
className="absolute right-[6px] w-[16px] h-[16px]"
/>
)}
</div>
);
})}
</div>
</div>
</div>
</div> </div>
); </div>
{/* Sort icons */}
<img
src={Sort}
className={cn(
'absolute left-[8px] top-[8px] h-[24px] w-[24px] rotate-0 transition-all duration-300 pointer-events-none',
)}
/>
<img
src={SortActive}
className={cn(
'absolute left-[8px] top-[8px] h-[24px] w-[24px] rotate-0 transition-all duration-300 pointer-events-none opacity-0',
(active || value.length > 0) && 'opacity-100',
)}
/>
{/* Dropdown */}
<div
className={cn(
'absolute rounded-[10px] bg-liquid-lighter w-[460px] left-0 top-[48px] z-50 transition-all duration-300',
'grid overflow-hidden',
active ? 'grid-rows-[1fr] opacity-100' : 'grid-rows-[0fr] opacity-0',
)}
>
<div className="overflow-hidden p-[8px]">
<div className="overflow-y-scroll max-h-[200px] thin-scrollbar pr-[8px] grid grid-cols-2 gap-[20px]">
{items.map((v) => {
const selected = value.some((val) => val.value === v.value);
return (
<div
key={v.value}
className={cn(
'cursor-pointer h-[36px] relative transition-all duration-300',
'text-[16px] font-medium select-none flex items-center pl-[8px]',
'hover:bg-liquid-background rounded-[10px]',
selected && 'bg-liquid-background/50',
)}
onClick={() => toggleItem(v)}
>
{v.text}
{selected && (
<img
src={checkMark}
className="absolute right-[8px] h-[20px] w-[20px]"
/>
)}
</div>
);
})}
</div>
</div>
</div>
</div>
);
}; };

View File

@@ -45,8 +45,8 @@ export const Sorter: React.FC<SorterProps> = ({
className={cn( className={cn(
'grid items-center h-[40px] rounded-full bg-liquid-lighter grid-cols-[40px]', 'grid items-center h-[40px] rounded-full bg-liquid-lighter grid-cols-[40px]',
'text-[18px] font-bold cursor-pointer select-none', 'text-[18px] font-bold cursor-pointer select-none',
'transitin-all active:scale-95 duration-300 overflow-hidden', 'overflow-hidden',
active && ' grid-cols-[1fr]', active && ' grid-cols-[1fr] border-liquid-brightmain border-[1px] border-solid',
)} )}
onClick={() => { onClick={() => {
setActive(!active); setActive(!active);
@@ -54,7 +54,7 @@ export const Sorter: React.FC<SorterProps> = ({
> >
<div <div
className={cn( className={cn(
'text-liquid-brightmain pl-[40px] pr-[16px]', 'text-liquid-brightmain pl-[42px] pr-[16px]',
active && '', active && '',
)} )}
> >
@@ -63,7 +63,7 @@ export const Sorter: React.FC<SorterProps> = ({
</div> </div>
</div> </div>
<div className="h-[24px] w-[24px] bg-red-500"></div> <div className="h-[24px] w-[24px]"></div>
<img <img
src={Sort} src={Sort}
className={cn( className={cn(

View File

@@ -14,7 +14,7 @@ const Filters = () => {
]; ];
return ( return (
<div className=" h-[50px] mb-[20px] flex"> <div className=" h-[50px] mb-[20px] flex gap-[20px]">
<div></div> <div></div>
<Sorter <Sorter
@@ -35,12 +35,11 @@ const Filters = () => {
onChange={(v) => console.log(v)} onChange={(v) => console.log(v)}
/> />
{/* <Filter <Filter
items={items} items={items}
defaultState={[items[0], items[3]]} // начальные выбранные элементы defaultState={[]}
onChange={(values) => console.log(values)} // обработчик изменения onChange={(values) => console.log(values)}
className="w-[240px]" />
/> */}
</div> </div>
); );
}; };