refactor(ui): improve icon name combobox with custom hook and better UX

- Replace direct PocketBase calls with useExistingIconNames hook
- Add loading states and better error handling
- Improve create new icon flow with preview functionality
- Extract icon name sanitization logic into reusable function
- Enhance empty state with loading indicator
- Reorganize component structure for better maintainability
This commit is contained in:
Thomas Camlong
2025-10-02 15:20:11 +02:00
parent f4819acc7c
commit f0215627d7

View File

@@ -12,7 +12,7 @@ import {
ComboboxList, ComboboxList,
ComboboxTrigger, ComboboxTrigger,
} from "@/components/ui/shadcn-io/combobox" } from "@/components/ui/shadcn-io/combobox"
import { pb } from "@/lib/pb" import { useExistingIconNames } from "@/hooks/use-submissions"
interface IconNameComboboxProps { interface IconNameComboboxProps {
value: string value: string
@@ -21,45 +21,25 @@ interface IconNameComboboxProps {
} }
export function IconNameCombobox({ value, onValueChange, onIsExisting }: IconNameComboboxProps) { export function IconNameCombobox({ value, onValueChange, onIsExisting }: IconNameComboboxProps) {
const [existingIcons, setExistingIcons] = useState<Array<{ label: string; value: string }>>([]) const { data: existingIcons = [], isLoading: loading } = useExistingIconNames()
const [loading, setLoading] = useState(true) const [previewValue, setPreviewValue] = useState("")
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()
}, [])
// Check if current value is existing
useEffect(() => { useEffect(() => {
const isExisting = existingIcons.some((icon) => icon.value === value) const isExisting = existingIcons.some((icon) => icon.value === value)
onIsExisting(isExisting) onIsExisting(isExisting)
}, [value, existingIcons, onIsExisting]) }, [value, existingIcons, onIsExisting])
const handleCreateNew = (inputValue: string) => { const sanitizeIconName = (inputValue: string): string => {
const sanitizedValue = inputValue return inputValue
.toLowerCase() .toLowerCase()
.trim() .trim()
.replace(/\s+/g, "-") .replace(/\s+/g, "-")
.replace(/[^a-z0-9-]/g, "") .replace(/[^a-z0-9-]/g, "")
}
const handleCreateNew = (inputValue: string) => {
const sanitizedValue = sanitizeIconName(inputValue)
onValueChange(sanitizedValue) onValueChange(sanitizedValue)
} }
@@ -75,14 +55,37 @@ export function IconNameCombobox({ value, onValueChange, onIsExisting }: IconNam
<span className="font-mono">{value}</span> <span className="font-mono">{value}</span>
</span> </span>
) : ( ) : (
<span className="text-muted-foreground">Select or create icon ID...</span> <span className="text-muted-foreground">
{loading ? "Loading icons..." : "Select or create icon ID..."}
</span>
)} )}
</ComboboxTrigger> </ComboboxTrigger>
<ComboboxContent> <ComboboxContent>
<ComboboxInput placeholder="Search or type new icon ID..." /> <ComboboxInput
placeholder="Search or type new icon ID..."
onValueChange={setPreviewValue}
/>
<ComboboxEmpty>
{loading ? (
"Loading..."
) : (
<ComboboxCreateNew onCreateNew={handleCreateNew}>
{(inputValue) => {
const sanitized = sanitizeIconName(inputValue)
return (
<div className="flex items-center gap-2 py-2">
<span className="text-muted-foreground">Create new icon:</span>
<span className="font-mono font-semibold text-foreground">
{sanitized}
</span>
</div>
)
}}
</ComboboxCreateNew>
)}
</ComboboxEmpty>
<ComboboxList> <ComboboxList>
<ComboboxEmpty>No existing icon found.</ComboboxEmpty> {!loading && existingIcons.length > 0 && (
{existingIcons.length > 0 && (
<ComboboxGroup heading="Existing Icons"> <ComboboxGroup heading="Existing Icons">
{existingIcons.map((icon) => ( {existingIcons.map((icon) => (
<ComboboxItem key={icon.value} value={icon.value}> <ComboboxItem key={icon.value} value={icon.value}>
@@ -91,20 +94,6 @@ export function IconNameCombobox({ value, onValueChange, onIsExisting }: IconNam
))} ))}
</ComboboxGroup> </ComboboxGroup>
)} )}
<ComboboxCreateNew onCreateNew={handleCreateNew}>
{(inputValue) => (
<div className="flex items-start gap-2">
<span className="text-muted-foreground">Create new icon:</span>
<span className="font-mono font-semibold">
{inputValue
.toLowerCase()
.trim()
.replace(/\s+/g, "-")
.replace(/[^a-z0-9-]/g, "")}
</span>
</div>
)}
</ComboboxCreateNew>
</ComboboxList> </ComboboxList>
</ComboboxContent> </ComboboxContent>
</Combobox> </Combobox>