♻️ refactor: project search bar

This commit is contained in:
Wahyu Kurniawan 2024-03-06 10:30:26 +07:00
parent b03200c256
commit 6ddf44cddd
No known key found for this signature in database
GPG Key ID: 040A1549143A8E33
6 changed files with 181 additions and 43 deletions

View File

@ -6,15 +6,14 @@ import { useDebounce } from 'usehooks-ts';
import SearchBar from 'components/SearchBar'; import SearchBar from 'components/SearchBar';
import { useGQLClient } from 'context/GQLClientContext'; import { useGQLClient } from 'context/GQLClientContext';
import { cn } from 'utils/classnames'; import { cn } from 'utils/classnames';
import { InfoRoundFilledIcon } from 'components/shared/CustomIcon'; import { ProjectSearchBarItem } from './ProjectSearchBarItem';
import { Avatar } from 'components/shared/Avatar'; import { ProjectSearchBarEmpty } from './ProjectSearchBarEmpty';
import { getInitials } from 'utils/geInitials';
interface ProjectsSearchProps { interface ProjectSearchBarProps {
onChange?: (data: Project) => void; onChange?: (data: Project) => void;
} }
const ProjectSearchBar = ({ onChange }: ProjectsSearchProps) => { export const ProjectSearchBar = ({ onChange }: ProjectSearchBarProps) => {
const [items, setItems] = useState<Project[]>([]); const [items, setItems] = useState<Project[]>([]);
const [selectedItem, setSelectedItem] = useState<Project | null>(null); const [selectedItem, setSelectedItem] = useState<Project | null>(null);
const client = useGQLClient(); const client = useGQLClient();
@ -60,7 +59,7 @@ const ProjectSearchBar = ({ onChange }: ProjectsSearchProps) => {
}, [fetchProjects, debouncedInputValue]); }, [fetchProjects, debouncedInputValue]);
return ( return (
<div className="relative"> <div className="relative w-full lg:w-fit">
<SearchBar {...getInputProps()} /> <SearchBar {...getInputProps()} />
<div <div
{...getMenuProps()} {...getMenuProps()}
@ -77,46 +76,18 @@ const ProjectSearchBar = ({ onChange }: ProjectsSearchProps) => {
</p> </p>
</div> </div>
{items.map((item, index) => ( {items.map((item, index) => (
<div <ProjectSearchBarItem
{...getItemProps({ item, index })} {...getItemProps({ item, index })}
key={item.id} key={item.id}
className={cn( item={item}
'px-2 py-2 flex items-center gap-3 rounded-lg hover:bg-base-bg-emphasized', active={highlightedIndex === index || selectedItem === item}
{
'bg-base-bg-emphasized':
highlightedIndex === index || selectedItem === item,
},
)}
>
<Avatar
size={32}
imageSrc={item.icon}
initials={getInitials(item.name)}
/> />
<div className="flex flex-col flex-1">
<p className="text-sm tracking-[-0.006em] text-elements-high-em">
{item.name}
</p>
<p className="text-xs text-elements-low-em">
{item.organization.name}
</p>
</div>
</div>
))} ))}
</> </>
) : ( ) : (
<div className="flex items-center px-2 py-2 gap-3"> <ProjectSearchBarEmpty />
<div className="w-8 h-8 rounded-lg flex items-center justify-center bg-orange-50 text-elements-warning">
<InfoRoundFilledIcon size={16} />
</div>
<p className="text-elements-low-em text-sm tracking-[-0.006em]">
No projects matching this name
</p>
</div>
)} )}
</div> </div>
</div> </div>
); );
}; };
export default ProjectSearchBar;

View File

@ -0,0 +1,84 @@
import React, { useCallback, useEffect, useState } from 'react';
import * as Dialog from '@radix-ui/react-dialog';
import { Button } from 'components/shared/Button';
import { CrossIcon, SearchIcon } from 'components/shared/CustomIcon';
import { Input } from 'components/shared/Input';
import { useGQLClient } from 'context/GQLClientContext';
import { Project } from 'gql-client';
import { useDebounce } from 'usehooks-ts';
import { ProjectSearchBarItem } from './ProjectSearchBarItem';
import { ProjectSearchBarEmpty } from './ProjectSearchBarEmpty';
interface ProjectSearchBarDialogProps extends Dialog.DialogProps {
onClose?: () => void;
}
export const ProjectSearchBarDialog = ({
onClose,
...props
}: ProjectSearchBarDialogProps) => {
const [items, setItems] = useState<Project[]>([]);
const [inputValue, setInputValue] = useState<string>('');
const client = useGQLClient();
const debouncedInputValue = useDebounce<string>(inputValue, 500);
const fetchProjects = useCallback(
async (inputValue: string) => {
const { searchProjects } = await client.searchProjects(inputValue);
setItems(searchProjects);
},
[client],
);
useEffect(() => {
if (debouncedInputValue) {
fetchProjects(debouncedInputValue);
}
}, [fetchProjects, debouncedInputValue]);
const handleClose = () => {
setInputValue('');
setItems([]);
onClose?.();
};
console.log(items);
return (
<Dialog.Root {...props}>
<Dialog.Portal>
<div className="bg-base-bg fixed inset-0 md:hidden overflow-y-auto">
<Dialog.Content>
<div className="py-2.5 px-4 w-full flex items-center justify-between border-b border-border-separator/[0.06]">
<Input
leftIcon={<SearchIcon />}
placeholder="Search"
appearance="borderless"
value={inputValue}
autoFocus
onChange={(e) => setInputValue(e.target.value)}
/>
<Button iconOnly variant="ghost" onClick={handleClose}>
<CrossIcon size={16} />
</Button>
</div>
{/* Content */}
<div className="flex flex-col gap-1 px-2 py-2">
{items.length > 0
? items.map((item) => (
<>
<div className="px-2 py-2">
<p className="text-elements-mid-em text-xs font-medium">
Suggestions
</p>
</div>
<ProjectSearchBarItem key={item.id} item={item} />
</>
))
: inputValue && <ProjectSearchBarEmpty />}
</div>
</Dialog.Content>
</div>
</Dialog.Portal>
</Dialog.Root>
);
};

View File

@ -0,0 +1,24 @@
import { InfoRoundFilledIcon } from 'components/shared/CustomIcon';
import React, { ComponentPropsWithoutRef } from 'react';
import { cn } from 'utils/classnames';
interface ProjectSearchBarEmptyProps extends ComponentPropsWithoutRef<'div'> {}
export const ProjectSearchBarEmpty = ({
className,
...props
}: ProjectSearchBarEmptyProps) => {
return (
<div
{...props}
className={cn('flex items-center px-2 py-2 gap-3', className)}
>
<div className="w-8 h-8 rounded-lg flex items-center justify-center bg-orange-50 text-elements-warning">
<InfoRoundFilledIcon size={16} />
</div>
<p className="text-elements-low-em text-sm tracking-[-0.006em]">
No projects matching this name
</p>
</div>
);
};

View File

@ -0,0 +1,59 @@
import { Avatar } from 'components/shared/Avatar';
import { Overwrite, UseComboboxGetItemPropsReturnValue } from 'downshift';
import { Project } from 'gql-client';
import React, { ComponentPropsWithoutRef, forwardRef } from 'react';
import { OmitCommon } from 'types/common';
import { cn } from 'utils/classnames';
import { getInitials } from 'utils/geInitials';
/**
* Represents a type that merges ComponentPropsWithoutRef<'li'> with certain exclusions.
* @type {MergedComponentPropsWithoutRef}
*/
type MergedComponentPropsWithoutRef = OmitCommon<
ComponentPropsWithoutRef<'div'>,
Omit<
Overwrite<UseComboboxGetItemPropsReturnValue, Project[]>,
'index' | 'item'
>
>;
interface ProjectSearchBarItemProps extends MergedComponentPropsWithoutRef {
item: Project;
active?: boolean;
}
const ProjectSearchBarItem = forwardRef<
HTMLDivElement,
ProjectSearchBarItemProps
>(({ item, active, ...props }, ref) => {
return (
<div
{...props}
ref={ref}
key={item.id}
className={cn(
'px-2 py-2 flex items-center gap-3 rounded-lg hover:bg-base-bg-emphasized',
{
'bg-base-bg-emphasized': active,
},
)}
>
<Avatar
size={32}
imageSrc={item.icon}
initials={getInitials(item.name)}
/>
<div className="flex flex-col flex-1">
<p className="text-sm tracking-[-0.006em] text-elements-high-em">
{item.name}
</p>
<p className="text-xs text-elements-low-em">{item.organization.name}</p>
</div>
</div>
);
});
ProjectSearchBarItem.displayName = 'ProjectSearchBarItem';
export { ProjectSearchBarItem };

View File

@ -0,0 +1,2 @@
export * from './ProjectSearchBar';
export * from './ProjectSearchBarDialog';

View File

@ -2,16 +2,14 @@ import React, { useCallback, useEffect, useState } from 'react';
import { Outlet, useNavigate } from 'react-router-dom'; import { Outlet, useNavigate } from 'react-router-dom';
import { User } from 'gql-client'; import { User } from 'gql-client';
// import { Tooltip } from '@material-tailwind/react';
import HorizontalLine from 'components/HorizontalLine'; import HorizontalLine from 'components/HorizontalLine';
import ProjectSearchBar from 'components/projects/ProjectSearchBar';
import { useGQLClient } from 'context/GQLClientContext'; import { useGQLClient } from 'context/GQLClientContext';
import { NotificationBellIcon, PlusIcon } from 'components/shared/CustomIcon'; import { NotificationBellIcon, PlusIcon } from 'components/shared/CustomIcon';
import { Button } from 'components/shared/Button'; import { Button } from 'components/shared/Button';
import { Avatar } from 'components/shared/Avatar'; import { Avatar } from 'components/shared/Avatar';
import { getInitials } from 'utils/geInitials'; import { getInitials } from 'utils/geInitials';
import { formatAddress } from '../utils/format'; import { formatAddress } from 'utils/format';
import { ProjectSearchBar } from 'components/projects/ProjectSearchBar';
const ProjectSearch = () => { const ProjectSearch = () => {
const navigate = useNavigate(); const navigate = useNavigate();