add filter

This commit is contained in:
Виталий Лавшонок
2025-11-08 15:54:49 +03:00
parent f7924cd564
commit 1cbd2dc0b3
14 changed files with 373 additions and 134 deletions

View File

@@ -2,119 +2,124 @@ import React from 'react';
import { cn } from '../../lib/cn';
import { checkMark } from '../../assets/icons/input';
import { useClickOutside } from '../../hooks/useClickOutside';
import { Sort, SortActive } from '../../assets/icons/filters';
import { iconFilter, iconFilterActive } from '../../assets/icons/filters';
export interface FilterItem {
text: string;
value: string;
text: string;
value: string;
}
interface FilterProps {
disabled?: boolean;
className?: string;
onChange: (items: FilterItem[]) => void;
defaultState?: FilterItem[];
items: FilterItem[];
disabled?: boolean;
className?: string;
onChange: (items: FilterItem[]) => void;
defaultState?: FilterItem[];
items: FilterItem[];
}
export const Filter: React.FC<FilterProps> = ({
disabled = false,
className = '',
onChange,
defaultState = [],
items = [],
export const FilterDropDown: React.FC<FilterProps> = ({
disabled = false,
className = '',
onChange,
defaultState = [],
items = [],
}) => {
const [value, setValue] = React.useState<FilterItem[]>(defaultState);
const [active, setActive] = React.useState(false);
const [value, setValue] = React.useState<FilterItem[]>(defaultState);
const [active, setActive] = React.useState(false);
const ref = React.useRef<HTMLDivElement>(null);
const ref = React.useRef<HTMLDivElement>(null);
useClickOutside(ref, () => {
setActive(false);
});
useClickOutside(ref, () => {
setActive(false);
});
React.useEffect(() => {
onChange(value);
}, [value]);
React.useEffect(() => {
onChange(value);
}, [value]);
const toggleItem = (item: FilterItem) => {
const exists = value.some((val) => val.value === item.value);
if (exists) {
setValue(value.filter((val) => val.value !== item.value));
} else {
setValue([...value, item]);
}
};
const toggleItem = (item: FilterItem) => {
const exists = value.some((val) => val.value === item.value);
if (exists) {
setValue(value.filter((val) => val.value !== item.value));
} else {
setValue([...value, item]);
}
};
return (
<div className={cn('relative', className)} ref={ref}>
<div
className={cn(
'items-center h-[40px] rounded-full bg-liquid-lighter w-[40px] flex',
'text-[18px] font-bold cursor-pointer select-none',
'overflow-hidden',
(active || value.length > 0) && 'w-fit border-liquid-brightmain border-[1px] border-solid',
)}
onClick={() => {
if (!disabled) setActive(!active);
}}
>
<div className="text-liquid-brightmain pl-[42px] pr-[16px] w-fit">
{value.length}
</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]"
/>
)}
return (
<div className={cn('relative', className)} ref={ref}>
<div
className={cn(
'items-center h-[40px] rounded-full bg-liquid-lighter w-[40px] flex',
'text-[18px] font-bold cursor-pointer select-none',
'overflow-hidden',
(active || value.length > 0) &&
'w-fit border-liquid-brightmain border-[1px] border-solid',
)}
onClick={() => {
if (!disabled) setActive(!active);
}}
>
<div className="text-liquid-brightmain pl-[42px] pr-[16px] w-fit">
{value.length}
</div>
);
})}
</div>
</div>
{/* Filter icons */}
<img
src={iconFilter}
className={cn(
'absolute left-[8px] top-[8px] h-[24px] w-[24px] rotate-0 transition-all duration-300 pointer-events-none',
)}
/>
<img
src={iconFilterActive}
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>
</div>
</div>
);
);
};

View File

@@ -1,8 +1,8 @@
import React from 'react';
import { FC, useEffect, useRef, useState } from 'react';
import { cn } from '../../lib/cn';
import { checkMark } from '../../assets/icons/input';
import { useClickOutside } from '../../hooks/useClickOutside';
import { Sort, SortActive } from '../../assets/icons/filters';
import { iconSort, iconSortActive } from '../../assets/icons/filters';
export interface SorterItem {
text: string;
@@ -17,7 +17,7 @@ interface SorterProps {
items: SorterItem[];
}
export const Sorter: React.FC<SorterProps> = ({
export const SorterDropDown: FC<SorterProps> = ({
// disabled = false,
className = '',
onChange,
@@ -26,14 +26,15 @@ export const Sorter: React.FC<SorterProps> = ({
}) => {
if (items.length == 0) items.push({ text: '', value: '' });
const [value, setValue] = React.useState<SorterItem>(
const [value, setValue] = useState<SorterItem>(
defaultState != undefined ? defaultState : items[0],
);
const [active, setActive] = React.useState<boolean>(false);
const [active, setActive] = useState<boolean>(false);
const [activate, setActivate] = useState<Boolean>(false);
React.useEffect(() => onChange(value.value), [value]);
useEffect(() => onChange(value.value), [value]);
const ref = React.useRef<HTMLDivElement>(null);
const ref = useRef<HTMLDivElement>(null);
useClickOutside(ref, () => {
setActive(false);
@@ -46,7 +47,8 @@ export const Sorter: React.FC<SorterProps> = ({
'grid items-center h-[40px] rounded-full bg-liquid-lighter grid-cols-[40px]',
'text-[18px] font-bold cursor-pointer select-none',
'overflow-hidden',
active && ' grid-cols-[1fr] border-liquid-brightmain border-[1px] border-solid',
(active || activate) &&
' grid-cols-[1fr] border-liquid-brightmain border-[1px] border-solid',
)}
onClick={() => {
setActive(!active);
@@ -63,18 +65,17 @@ export const Sorter: React.FC<SorterProps> = ({
</div>
</div>
<div className="h-[24px] w-[24px]"></div>
<img
src={Sort}
src={iconSort}
className={cn(
' absolute right-[16px] h-[24px] w-[24px] top-[8px] left-[8px] rotate-0 transition-all duration-300 pointer-events-none',
)}
/>
<img
src={SortActive}
src={iconSortActive}
className={cn(
' absolute right-[16px] h-[24px] w-[24px] top-[8px] left-[8px] rotate-0 transition-all duration-300 pointer-events-none opacity-0',
active && ' opacity-100',
(active || activate) && ' opacity-100',
)}
/>
@@ -107,6 +108,7 @@ export const Sorter: React.FC<SorterProps> = ({
onClick={() => {
setValue(v);
setActive(false);
setActivate(true);
}}
>
{v.text}

View File

@@ -0,0 +1,59 @@
import React from 'react';
import { cn } from '../../lib/cn';
import { iconSearch } from '../../assets/icons/filters';
interface searchInputProps {
name?: string;
error?: string;
disabled?: boolean;
required?: boolean;
label?: string;
placeholder?: string;
className?: string;
onChange: (state: string) => void;
defaultState?: string;
autocomplete?: string;
onKeyDown?: (e: React.KeyboardEvent<HTMLInputElement>) => void;
}
export const SearchInput: React.FC<searchInputProps> = ({
placeholder = '',
className = '',
onChange,
defaultState = '',
name = '',
autocomplete = '',
onKeyDown,
}) => {
const [value, setValue] = React.useState<string>(defaultState);
React.useEffect(() => onChange(value), [value]);
React.useEffect(() => setValue(defaultState), [defaultState]);
return (
<label
className={cn(
'relative bg-liquid-lighter w-[200px] h-[40px] rounded-full px-[16px] pl-[50px] py-[8px] cursor-text',
className,
)}
>
<input
className={cn(
'placeholder:text-liquid-light w-full bg-transparent outline-none text-liquid-white',
)}
value={value}
name={name}
autoComplete={autocomplete}
type="text"
placeholder={placeholder}
onChange={(e) => {
setValue(e.target.value);
}}
onKeyDown={(e: React.KeyboardEvent<HTMLInputElement>) => {
if (onKeyDown) onKeyDown(e);
}}
/>
<img src={iconSearch} className=" absolute top-[8px] left-[16px]" />
</label>
);
};