feat: #420 reimplement icon picker (#421)

This commit is contained in:
Manuel
2024-05-04 23:00:15 +02:00
committed by GitHub
parent 51aaab2f23
commit 60a35e2583
37 changed files with 2974 additions and 10 deletions

View File

@@ -9,7 +9,8 @@ import { useI18n } from "@homarr/translation/client";
import type { z } from "@homarr/validation";
import { validation } from "@homarr/validation";
// TODO: add icon picker
import { IconPicker } from "~/components/icons/picker/icon-picker";
type FormType = z.infer<typeof validation.app.manage>;
interface AppFormProps {
@@ -38,10 +39,11 @@ export const AppForm = (props: AppFormProps) => {
<form onSubmit={form.onSubmit(handleSubmit)}>
<Stack>
<TextInput {...form.getInputProps("name")} withAsterisk label="Name" />
<TextInput
{...form.getInputProps("iconUrl")}
withAsterisk
label="Icon URL"
<IconPicker
initialValue={initialValues?.iconUrl}
onChange={(iconUrl) => {
form.setFieldValue("iconUrl", iconUrl);
}}
/>
<Textarea {...form.getInputProps("description")} label="Description" />
<TextInput {...form.getInputProps("href")} label="URL" />

View File

@@ -1,6 +1,7 @@
"use client";
import React, { ChangeEvent, useMemo, useState } from "react";
import type { ChangeEvent } from "react";
import React, { useMemo, useState } from "react";
import Link from "next/link";
import { Group, Menu, ScrollArea, Stack, Text, TextInput } from "@mantine/core";
import { IconSearch } from "@tabler/icons-react";

View File

@@ -0,0 +1,118 @@
import { useState } from "react";
import {
Combobox,
Group,
Image,
InputBase,
Skeleton,
Text,
useCombobox,
} from "@mantine/core";
import { clientApi } from "@homarr/api/client";
import { useScopedI18n } from "@homarr/translation/client";
interface IconPickerProps {
initialValue?: string;
onChange: (iconUrl: string) => void;
}
export const IconPicker = ({ initialValue, onChange }: IconPickerProps) => {
const [value, setValue] = useState<string>(initialValue ?? "");
const [search, setSearch] = useState(initialValue ?? "");
const t = useScopedI18n("common");
const { data, isFetching } = clientApi.icon.findIcons.useQuery({
searchText: search,
});
const combobox = useCombobox({
onDropdownClose: () => combobox.resetSelectedOption(),
});
const notNullableData = data?.icons ?? [];
const totalOptions = notNullableData.reduce(
(acc, group) => acc + group.icons.length,
0,
);
const groups = notNullableData.map((group) => {
const options = group.icons.map((item) => (
<Combobox.Option value={item.url} key={item.id}>
<Group>
<Image src={item.url} w={20} h={20} />
<Text>{item.name}</Text>
</Group>
</Combobox.Option>
));
return (
<Combobox.Group label={group.slug} key={group.id}>
{options}
</Combobox.Group>
);
});
return (
<Combobox
onOptionSubmit={(value) => {
setValue(value);
setSearch(value);
onChange(value);
combobox.closeDropdown();
}}
store={combobox}
withinPortal
>
<Combobox.Target>
<InputBase
rightSection={<Combobox.Chevron />}
value={search}
onChange={(event) => {
combobox.openDropdown();
combobox.updateSelectedOptionIndex();
setSearch(event.currentTarget.value);
}}
onClick={() => combobox.openDropdown()}
onFocus={() => combobox.openDropdown()}
onBlur={() => {
combobox.closeDropdown();
setSearch(value || "");
}}
rightSectionPointerEvents="none"
withAsterisk
label="Icon URL"
/>
</Combobox.Target>
<Combobox.Dropdown>
<Combobox.Header>
<Text c="dimmed">
{t("iconPicker.header", { countIcons: data?.countIcons })}
</Text>
</Combobox.Header>
<Combobox.Options mah={350} style={{ overflowY: "auto" }}>
{totalOptions > 0 ? (
groups
) : !isFetching ? (
<Combobox.Empty>{t("search.nothingFound")}</Combobox.Empty>
) : (
Array(15)
.fill(0)
.map((_, index: number) => (
<Combobox.Option
value={`skeleton-${index}`}
key={index}
disabled
>
<Skeleton height={25} visible />
</Combobox.Option>
))
)}
</Combobox.Options>
</Combobox.Dropdown>
</Combobox>
);
};