From 0a9d700144249bf037d28ea6be02b3b85b4dd1bc Mon Sep 17 00:00:00 2001 From: Thomas Camlong Date: Thu, 2 Oct 2025 10:52:06 +0200 Subject: [PATCH] feat: add icon name combobox component - Create combobox for icon name selection and creation - Integrate with PocketBase to fetch existing icon names - Add input sanitization for icon IDs (lowercase, hyphens only) - Implement existing icon detection and validation - Support both selection from existing icons and creation of new ones - Provide visual feedback for new vs existing icon submissions --- web/src/components/icon-name-combobox.tsx | 113 ++++++++++++++++++++++ 1 file changed, 113 insertions(+) create mode 100644 web/src/components/icon-name-combobox.tsx diff --git a/web/src/components/icon-name-combobox.tsx b/web/src/components/icon-name-combobox.tsx new file mode 100644 index 00000000..43aaee07 --- /dev/null +++ b/web/src/components/icon-name-combobox.tsx @@ -0,0 +1,113 @@ +"use client" + +import { useEffect, useState } from "react" +import { + Combobox, + ComboboxContent, + ComboboxCreateNew, + ComboboxEmpty, + ComboboxGroup, + ComboboxInput, + ComboboxItem, + ComboboxList, + ComboboxTrigger, +} from "@/components/ui/shadcn-io/combobox" +import { pb } from "@/lib/pb" + +interface IconNameComboboxProps { + value: string + onValueChange: (value: string) => void + onIsExisting: (isExisting: boolean) => void +} + +export function IconNameCombobox({ value, onValueChange, onIsExisting }: IconNameComboboxProps) { + const [existingIcons, setExistingIcons] = useState>([]) + const [loading, setLoading] = useState(true) + + useEffect(() => { + async function fetchExistingIcons() { + try { + const records = await pb.collection("community_gallery").getFullList({ + fields: "name", + sort: "name", + }) + + const uniqueNames = Array.from(new Set(records.map((r) => r.name))) + const formattedIcons = uniqueNames.map((name) => ({ + label: name, + value: name, + })) + + setExistingIcons(formattedIcons) + } catch (error) { + console.error("Failed to fetch existing icons:", error) + } finally { + setLoading(false) + } + } + + fetchExistingIcons() + }, []) + + useEffect(() => { + const isExisting = existingIcons.some((icon) => icon.value === value) + onIsExisting(isExisting) + }, [value, existingIcons, onIsExisting]) + + const handleCreateNew = (inputValue: string) => { + const sanitizedValue = inputValue + .toLowerCase() + .trim() + .replace(/\s+/g, "-") + .replace(/[^a-z0-9-]/g, "") + onValueChange(sanitizedValue) + } + + const handleValueChange = (newValue: string) => { + onValueChange(newValue) + } + + return ( + + + {value ? ( + + {value} + + ) : ( + Select or create icon ID... + )} + + + + + No existing icon found. + {existingIcons.length > 0 && ( + + {existingIcons.map((icon) => ( + + {icon.label} + + ))} + + )} + + {(inputValue) => ( +
+ Create new icon: + + {inputValue + .toLowerCase() + .trim() + .replace(/\s+/g, "-") + .replace(/[^a-z0-9-]/g, "")} + +
+ )} +
+
+
+
+ ) +} +