import type { FocusEventHandler } from "react"; import { startTransition } from "react"; import { ActionIcon, Box, Card, Combobox, Flex, Group, Image, Indicator, InputBase, Paper, Skeleton, Stack, Text, UnstyledButton, useCombobox, } from "@mantine/core"; import { useDebouncedValue, useUncontrolled } from "@mantine/hooks"; import { IconUpload } from "@tabler/icons-react"; import { clientApi } from "@homarr/api/client"; import { useSession } from "@homarr/auth/client"; import { useScopedI18n } from "@homarr/translation/client"; import { UploadMedia } from "../upload-media/upload-media"; import classes from "./icon-picker.module.css"; interface IconPickerProps { value?: string; onChange: (iconUrl: string) => void; error?: string | null; onFocus?: FocusEventHandler; onBlur?: FocusEventHandler; withAsterisk?: boolean; } export const IconPicker = ({ value: propsValue, onChange, error, onFocus, onBlur, withAsterisk = true, }: IconPickerProps) => { const [value, setValue] = useUncontrolled({ value: propsValue, onChange, }); const [search, setSearch] = useUncontrolled({ value, onChange: (value) => { setValue(value); }, }); const [previewUrl, setPreviewUrl] = useUncontrolled({ value: propsValue ?? null, }); const { data: session } = useSession(); const tCommon = useScopedI18n("common"); const [debouncedSearch] = useDebouncedValue(search, 100); // We use not useSuspenseQuery as it would cause an above Suspense boundary to trigger and so searching for something is bad UX. const { data } = clientApi.icon.findIcons.useQuery({ searchText: debouncedSearch, }); const combobox = useCombobox({ onDropdownClose: () => combobox.resetSelectedOption(), }); const totalOptions = data?.icons.reduce((acc, group) => acc + group.icons.length, 0) ?? 0; const groups = data?.icons.map((group) => { const options = group.icons.map((item) => ( { const value = item.url; startTransition(() => { setPreviewUrl(value); setSearch(value); setValue(value); combobox.closeDropdown(); }); }} > )); return ( {group.slug} {options} ); }); return ( } leftSection={previewUrl ? : null} value={search} onChange={(event) => { combobox.openDropdown(); combobox.updateSelectedOptionIndex(); setSearch(event.currentTarget.value); setValue(event.currentTarget.value); setPreviewUrl(null); }} onClick={() => combobox.openDropdown()} onFocus={(event) => { onFocus?.(event); combobox.openDropdown(); }} onBlur={(event) => { onBlur?.(event); combobox.closeDropdown(); setPreviewUrl(value); setSearch(value || ""); }} rightSectionPointerEvents="none" withAsterisk={withAsterisk} error={error} label={tCommon("iconPicker.label")} placeholder={tCommon("iconPicker.header", { countIcons: String(data?.countIcons ?? 0) })} /> {session?.user.permissions.includes("media-upload") && ( { startTransition(() => { setValue(url); setPreviewUrl(url); setSearch(url); }); }} > {({ onClick, loading }) => ( )} )} {totalOptions > 0 ? ( {groups} ) : ( Array(15) .fill(0) .map((_, index: number) => ( )) )} ); };