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) => (
))
)}
);
};