-
{
- setSelectedOrgSlug(value!);
- navigate(`/${value}`);
- }}
- selected={(_, index) => (
-
-
-
-
- {organizations[index!]?.name}
-
-
Organization
-
-
- )}
- arrow={
-
- }
- >
- {/* // TODO: Show label organization and manage in option */}
- {renderOrganizations}
-
+
{renderMenu}
diff --git a/packages/frontend/src/components/shared/UserSelect/UserSelect.theme.ts b/packages/frontend/src/components/shared/UserSelect/UserSelect.theme.ts
new file mode 100644
index 0000000..bca34a1
--- /dev/null
+++ b/packages/frontend/src/components/shared/UserSelect/UserSelect.theme.ts
@@ -0,0 +1,57 @@
+import { tv, VariantProps } from 'tailwind-variants';
+
+export const userSelectTheme = tv({
+ slots: {
+ container: ['flex', 'flex-col', 'relative', 'gap-2'],
+ inputWrapper: [
+ 'relative',
+ 'flex',
+ 'flex-wrap',
+ 'gap-1',
+ 'min-w-[200px]',
+ 'w-full',
+ 'rounded-lg',
+ 'bg-transparent',
+ 'text-elements-mid-em',
+ 'shadow-sm',
+ 'border',
+ 'border-border-interactive',
+ 'focus-ring',
+ 'disabled:shadow-none',
+ 'disabled:border-none',
+ ],
+ input: ['outline-none'],
+ popover: [
+ 'mt-12',
+ 'z-20',
+ 'absolute',
+ 'flex-col',
+ 'gap-0.5',
+ 'min-w-full',
+ 'bg-surface-floating',
+ 'shadow-dropdown',
+ 'w-auto',
+ 'max-h-60',
+ 'overflow-auto',
+ 'border',
+ 'border-gray-200',
+ 'rounded-xl',
+ ],
+ popoverItemWrapper: ['flex', 'flex-col', 'px-2', 'py-2', 'gap-1'],
+ },
+ variants: {
+ isOpen: {
+ true: {
+ popover: ['flex'],
+ },
+ false: {
+ popover: ['hidden'],
+ },
+ },
+ hasValue: {
+ true: '',
+ },
+ },
+});
+
+export type UserSelectTheme = VariantProps
;
diff --git a/packages/frontend/src/components/shared/UserSelect/UserSelect.tsx b/packages/frontend/src/components/shared/UserSelect/UserSelect.tsx
new file mode 100644
index 0000000..81ef792
--- /dev/null
+++ b/packages/frontend/src/components/shared/UserSelect/UserSelect.tsx
@@ -0,0 +1,208 @@
+import React, {
+ useState,
+ ComponentPropsWithoutRef,
+ useRef,
+ useEffect,
+} from 'react';
+import { useNavigate } from 'react-router-dom';
+import { useSelect } from 'downshift';
+import { UserSelectTheme, userSelectTheme } from './UserSelect.theme';
+import { EmptyUserSelectItem, UserSelectItem } from './UserSelectItem';
+import {
+ BuildingIcon,
+ ChevronUpDown,
+ SettingsSlidersIcon,
+} from 'components/shared/CustomIcon';
+import { WavyBorder } from 'components/shared/WavyBorder';
+import { cn } from 'utils/classnames';
+
+export type UserSelectOption = {
+ value: string;
+ label: string;
+ imgSrc?: string;
+};
+
+interface UserSelectProps
+ extends Omit, 'value' | 'onChange'>,
+ UserSelectTheme {
+ options: UserSelectOption[];
+ value?: UserSelectOption;
+}
+
+export const UserSelect = ({ options, value }: UserSelectProps) => {
+ const theme = userSelectTheme();
+ const navigate = useNavigate();
+
+ const [selectedItem, setSelectedItem] = useState(
+ (value as UserSelectOption) || null,
+ );
+ const [dropdownOpen, setDropdownOpen] = useState(false);
+ const [dropdownPosition, setDropdownPosition] = useState<'top' | 'bottom'>(
+ 'bottom',
+ );
+ const popoverRef = useRef(null); // Ref for the popover
+ const inputWrapperRef = useRef(null); // Ref for the input wrapper
+
+ // Calculate and update popover position
+ useEffect(() => {
+ if (dropdownOpen && popoverRef.current && inputWrapperRef.current) {
+ const popover = popoverRef.current;
+ // @ts-expect-error – we know it's not null lol
+ const input = inputWrapperRef.current.getBoundingClientRect();
+ const spaceBelow = window.innerHeight - input.bottom;
+ const spaceAbove = input.top;
+ // @ts-expect-error – we know it's not null lol
+ const popoverHeight = popover.offsetHeight;
+
+ // Determine if there's enough space below
+ if (spaceBelow >= popoverHeight) {
+ setDropdownPosition('bottom');
+ } else if (spaceAbove >= popoverHeight) {
+ setDropdownPosition('top');
+ } else {
+ // Default to bottom if neither has enough space, but you could also set logic to choose the side with more space
+ setDropdownPosition('bottom');
+ }
+ }
+ }, [dropdownOpen]); // Re-calculate whenever the dropdown is opened
+
+ useEffect(() => {
+ setSelectedItem(value as UserSelectOption);
+ }, [value]);
+
+ const handleSelectedItemChange = (selectedItem: UserSelectOption | null) => {
+ setSelectedItem(selectedItem);
+ navigate(`/${selectedItem?.value}`);
+ };
+
+ const isSelected = (item: UserSelectOption) =>
+ selectedItem?.value === item.value;
+
+ const {
+ isOpen,
+ getMenuProps,
+ getToggleButtonProps,
+ highlightedIndex,
+ getItemProps,
+ openMenu,
+ } = useSelect({
+ items: options,
+ // @ts-expect-error – there are two params but we don't need the second one
+ isItemDisabled: (item) => item.disabled,
+ onSelectedItemChange: ({ selectedItem }) => {
+ if (selectedItem) {
+ handleSelectedItemChange(selectedItem);
+ }
+ },
+ onIsOpenChange: ({ isOpen }) => {
+ setDropdownOpen(isOpen ?? false);
+ },
+ itemToString: (item) => (item ? item.label : ''),
+ });
+
+ const handleManage = () => {
+ //TODO: implement manage handler
+ };
+
+ return (
+
+ {/* Input */}
+
!dropdownOpen && openMenu()}
+ className="cursor-pointer relative py-2 pl-2 pr-4 flex min-w-[200px] w-full items-center justify-between rounded-xl bg-surface-card shadow-sm"
+ >
+
+
+
+ {selectedItem?.label ? (
+
+ {selectedItem?.label}
+
+ ) : (
+
+ )}
+
Organization
+
+
+
+
+
+
+
+
+ {/* Popover */}
+
+
+ {/* Settings header */}
+
+
+ {/* Organization */}
+ {isOpen && options.length !== 0 ? (
+ options.map((item, index) => (
+
+ ))
+ ) : (
+
+ )}
+
+
+ {/* WavyBorder */}
+ {/* //TODO:remove if personal dont exist */}
+
+
+
+ {/* //TODO:Personal (replace options with Personal Options) */}
+ {isOpen && options.length !== 0 ? (
+ options.map((item, index) => (
+
+ ))
+ ) : (
+
+ )}
+
+
+
+ );
+};
diff --git a/packages/frontend/src/components/shared/UserSelect/UserSelectItem/UserSelectItem.theme.ts b/packages/frontend/src/components/shared/UserSelect/UserSelectItem/UserSelectItem.theme.ts
new file mode 100644
index 0000000..17f28bd
--- /dev/null
+++ b/packages/frontend/src/components/shared/UserSelect/UserSelectItem/UserSelectItem.theme.ts
@@ -0,0 +1,28 @@
+import { tv, VariantProps } from 'tailwind-variants';
+
+export const userSelectItemTheme = tv({
+ slots: {
+ wrapper: [
+ 'p-2',
+ 'gap-3',
+ 'flex',
+ 'items-center',
+ 'justify-between',
+ 'rounded-lg',
+ 'cursor-pointer',
+ ],
+ content: ['flex', 'gap-3', 'items-center'],
+ img: ['h-10', 'w-10', 'rounded-lg'],
+ selectedIcon: ['h-5', 'w-5', 'text-controls-primary'],
+ label: ['text-sm', 'text-elements-high-em', 'tracking-[-0.006em]'],
+ },
+ variants: {
+ active: {
+ true: {
+ wrapper: ['bg-base-bg-emphasized'],
+ },
+ },
+ },
+});
+
+export type UserSelectItemTheme = VariantProps;
diff --git a/packages/frontend/src/components/shared/UserSelect/UserSelectItem/UserSelectItem.tsx b/packages/frontend/src/components/shared/UserSelect/UserSelectItem/UserSelectItem.tsx
new file mode 100644
index 0000000..4885d6c
--- /dev/null
+++ b/packages/frontend/src/components/shared/UserSelect/UserSelectItem/UserSelectItem.tsx
@@ -0,0 +1,66 @@
+import React, { forwardRef, ComponentPropsWithoutRef } from 'react';
+import { Overwrite, UseComboboxGetItemPropsReturnValue } from 'downshift';
+import {
+ userSelectItemTheme,
+ UserSelectItemTheme,
+} from './UserSelectItem.theme';
+import { CheckRadioIcon } from 'components/shared/CustomIcon';
+import { UserSelectOption } from 'components/shared/UserSelect';
+import { OmitCommon } from 'types/common';
+
+/**
+ * Represents a type that merges ComponentPropsWithoutRef<'li'> with certain exclusions.
+ * @type {MergedComponentPropsWithoutRef}
+ */
+type MergedComponentPropsWithoutRef = OmitCommon<
+ ComponentPropsWithoutRef<'li'>,
+ Omit<
+ Overwrite,
+ 'index' | 'item'
+ >
+>;
+
+export interface UserSelectItemProps
+ extends MergedComponentPropsWithoutRef,
+ UserSelectItemTheme {
+ selected: boolean;
+ option: UserSelectOption;
+ hovered?: boolean;
+}
+
+const UserSelectItem = forwardRef(
+ ({ className, selected, hovered, option, ...props }, ref) => {
+ const theme = userSelectItemTheme();
+
+ const { value, label, imgSrc = './logo.svg' } = option;
+
+ return (
+
+
+
+
{label}
+
+ {selected && }
+
+ );
+ },
+);
+
+export const EmptyUserSelectItem = () => {
+ const theme = userSelectItemTheme();
+ return (
+
+
+
+ );
+};
+
+UserSelectItem.displayName = 'UserSelectItem';
+
+export { UserSelectItem };
diff --git a/packages/frontend/src/components/shared/UserSelect/UserSelectItem/index.ts b/packages/frontend/src/components/shared/UserSelect/UserSelectItem/index.ts
new file mode 100644
index 0000000..57e76fc
--- /dev/null
+++ b/packages/frontend/src/components/shared/UserSelect/UserSelectItem/index.ts
@@ -0,0 +1,2 @@
+export * from './UserSelectItem';
+export * from './UserSelectItem.theme';
diff --git a/packages/frontend/src/components/shared/UserSelect/index.ts b/packages/frontend/src/components/shared/UserSelect/index.ts
new file mode 100644
index 0000000..8225a33
--- /dev/null
+++ b/packages/frontend/src/components/shared/UserSelect/index.ts
@@ -0,0 +1,2 @@
+export * from './UserSelect';
+export * from './UserSelect.theme';