forked from cerc-io/snowballtools-base
♻️ refactor: project search bar
This commit is contained in:
parent
b03200c256
commit
6ddf44cddd
@ -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;
|
|
@ -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>
|
||||||
|
);
|
||||||
|
};
|
@ -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>
|
||||||
|
);
|
||||||
|
};
|
@ -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 };
|
@ -0,0 +1,2 @@
|
|||||||
|
export * from './ProjectSearchBar';
|
||||||
|
export * from './ProjectSearchBarDialog';
|
@ -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();
|
||||||
|
Loading…
Reference in New Issue
Block a user