diff --git a/packages/forms-collection/src/new-app/_form.tsx b/packages/forms-collection/src/new-app/_form.tsx index b47869f3d..146ed5151 100644 --- a/packages/forms-collection/src/new-app/_form.tsx +++ b/packages/forms-collection/src/new-app/_form.tsx @@ -1,17 +1,19 @@ "use client"; import type { ChangeEventHandler } from "react"; -import { useRef } from "react"; +import { useEffect, useRef } from "react"; import Link from "next/link"; import { Button, Checkbox, Collapse, Group, Stack, Textarea, TextInput } from "@mantine/core"; -import { useDisclosure } from "@mantine/hooks"; +import { useDebouncedValue, useDisclosure } from "@mantine/hooks"; import type { z } from "zod"; +import { clientApi } from "@homarr/api/client"; import { useZodForm } from "@homarr/form"; import { useI18n } from "@homarr/translation/client"; import { validation } from "@homarr/validation"; import { IconPicker } from "../icon-picker/icon-picker"; +import { findBestIconMatch } from "./icon-matcher"; type FormType = z.infer; @@ -45,6 +47,9 @@ export const AppForm = ({ }, }); + // Debounce the name value with 200ms delay + const [debouncedName] = useDebouncedValue(form.values.name, 200); + const shouldCreateAnother = useRef(false); const handleSubmit = (values: FormType) => { const redirect = !shouldCreateAnother.current; @@ -68,6 +73,25 @@ export const AppForm = ({ } }; + // Auto-select icon based on app name with debounced search + const { data: iconsData } = clientApi.icon.findIcons.useQuery( + { + searchText: debouncedName, + }, + { + enabled: debouncedName.length > 3, + }, + ); + + useEffect(() => { + if (debouncedName && !form.values.iconUrl && iconsData?.icons) { + const bestMatch = findBestIconMatch(debouncedName, iconsData.icons); + if (bestMatch) { + form.setFieldValue("iconUrl", bestMatch); + } + } + }, [debouncedName, iconsData]); + return (
diff --git a/packages/forms-collection/src/new-app/icon-matcher.ts b/packages/forms-collection/src/new-app/icon-matcher.ts new file mode 100644 index 000000000..bbd611977 --- /dev/null +++ b/packages/forms-collection/src/new-app/icon-matcher.ts @@ -0,0 +1,30 @@ +import type { inferRouterOutputs } from "@trpc/server"; + +import type { AppRouter } from "@homarr/api"; + +type RouterOutput = inferRouterOutputs; +type IconGroupsOutput = RouterOutput["icon"]["findIcons"]["icons"]; + +export const findBestIconMatch = (searchTerm: string, iconGroups: IconGroupsOutput): string | null => { + const nameLower = searchTerm.toLowerCase(); + const allIcons = iconGroups.flatMap((group) => group.icons); + + const getIconPriority = (iconUrl: string) => { + const fileName = iconUrl.toLowerCase().split("/").pop()?.split(".")[0]; + if (!fileName) return -1; + + const isSvg = iconUrl.endsWith(".svg"); + const isExactMatch = fileName === nameLower; + + if (isExactMatch) return isSvg ? 0 : 1; + if (fileName.includes(nameLower)) return isSvg ? 2 : 3; + return -1; + }; + + for (let priority = 0; priority <= 3; priority++) { + const match = allIcons.find((icon) => getIconPriority(icon.url) === priority); + if (match) return match.url; + } + + return null; +};