Hey is a decentralized and permissionless social media app built with Lens Protocol 馃尶
1import {
2 Listbox,
3 ListboxButton,
4 ListboxOption,
5 ListboxOptions,
6 Transition
7} from "@headlessui/react";
8import { MagnifyingGlassIcon } from "@heroicons/react/24/outline";
9import { CheckCircleIcon, ChevronDownIcon } from "@heroicons/react/24/solid";
10import { Fragment, memo, type ReactNode, useState } from "react";
11import { Input } from "@/components/Shared/UI";
12import cn from "@/helpers/cn";
13
14interface SelectProps {
15 className?: string;
16 defaultValue?: string;
17 iconClassName?: string;
18 onChange: (value: any) => any;
19 options?: {
20 disabled?: boolean;
21 helper?: string;
22 icon?: string;
23 label: string;
24 htmlLabel?: ReactNode;
25 selected?: boolean;
26 value: number | string;
27 }[];
28 showSearch?: boolean;
29}
30
31const Select = ({
32 className,
33 defaultValue,
34 iconClassName,
35 onChange,
36 options,
37 showSearch = false
38}: SelectProps) => {
39 const [searchValue, setSearchValue] = useState("");
40 const selected = options?.find((option) => option.selected) || options?.[0];
41
42 return (
43 <Listbox onChange={onChange} value={defaultValue || selected?.value}>
44 <div className="relative">
45 <ListboxButton
46 className={cn(
47 "flex w-full items-center justify-between space-x-3 rounded-xl border border-gray-300 bg-white px-3 py-2 text-left outline-hidden focus:border-gray-500 focus:ring-gray-400 dark:border-gray-700 dark:bg-gray-800",
48 className
49 )}
50 >
51 <span className="flex items-center space-x-2">
52 {selected?.icon && (
53 <img
54 alt={selected?.label}
55 className={iconClassName}
56 src={selected?.icon}
57 />
58 )}
59 <span>{selected?.htmlLabel || selected?.label}</span>
60 </span>
61 <ChevronDownIcon className="mr-1 size-5 text-gray-400" />
62 </ListboxButton>
63 <Transition
64 as={Fragment}
65 enter="transition ease-out duration-100"
66 enterFrom="transform opacity-0 scale-95"
67 enterTo="transform opacity-100 scale-100"
68 leave="transition ease-in duration-75"
69 leaveFrom="transform opacity-100 scale-100"
70 leaveTo="transform opacity-0 scale-95"
71 >
72 <ListboxOptions className="no-scrollbar absolute z-[5] mt-2 max-h-60 w-full overflow-auto rounded-xl border border-gray-200 bg-white shadow-xs focus:outline-hidden dark:border-gray-700 dark:bg-gray-900">
73 {showSearch ? (
74 <div className="mx-4 mt-4">
75 <Input
76 className="w-full"
77 iconLeft={<MagnifyingGlassIcon />}
78 onChange={(event) => {
79 setSearchValue(event.target.value);
80 }}
81 placeholder="Search"
82 type="text"
83 value={searchValue}
84 />
85 </div>
86 ) : null}
87 {options
88 ?.filter((option) =>
89 option.label.toLowerCase().includes(searchValue.toLowerCase())
90 )
91 .map((option, id) => (
92 <ListboxOption
93 className={({ focus }: { focus: boolean }) =>
94 cn(
95 { "dropdown-active": focus },
96 "m-2 cursor-pointer rounded-lg"
97 )
98 }
99 disabled={option.disabled}
100 key={id}
101 value={option.value}
102 >
103 {({ selected }) => (
104 <div className="mx-2 flex flex-col space-y-0 py-1.5">
105 <span className="flex w-full items-center justify-between space-x-3 text-gray-700 dark:text-gray-200">
106 <span className="flex items-center space-x-2">
107 {option.icon && (
108 <img
109 alt={option.label}
110 className={iconClassName}
111 src={option.icon}
112 />
113 )}
114 <span className="block truncate">
115 {option.htmlLabel || option.label}
116 </span>
117 </span>
118 {selected ? (
119 <CheckCircleIcon className="size-5" />
120 ) : null}
121 </span>
122 {option.helper ? (
123 <span className="text-gray-500 text-xs dark:text-gray-200">
124 {option.helper}
125 </span>
126 ) : null}
127 </div>
128 )}
129 </ListboxOption>
130 ))}
131 </ListboxOptions>
132 </Transition>
133 </div>
134 </Listbox>
135 );
136};
137
138export default memo(Select);