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,
ComboboxTrigger,
} from "@/components/ui/shadcn-io/combobox"
import { pb } from "@/lib/pb"
import { useExistingIconNames } from "@/hooks/use-submissions"
interface IconNameComboboxProps {
value: string
@@ -21,45 +21,25 @@ interface IconNameComboboxProps {
}
export function IconNameCombobox({ value, onValueChange, onIsExisting }: IconNameComboboxProps) {
const [existingIcons, setExistingIcons] = useState<Array<{ label: string; value: string }>>([])
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()
}, [])
const { data: existingIcons = [], isLoading: loading } = useExistingIconNames()
const [previewValue, setPreviewValue] = useState("")
// Check if current value is existing
useEffect(() => {
const isExisting = existingIcons.some((icon) => icon.value === value)
onIsExisting(isExisting)
}, [value, existingIcons, onIsExisting])
const handleCreateNew = (inputValue: string) => {
const sanitizedValue = inputValue
const sanitizeIconName = (inputValue: string): string => {
return inputValue
.toLowerCase()
.trim()
.replace(/\s+/g, "-")
.replace(/[^a-z0-9-]/g, "")
}
const handleCreateNew = (inputValue: string) => {
const sanitizedValue = sanitizeIconName(inputValue)
onValueChange(sanitizedValue)
}
@@ -75,14 +55,37 @@ export function IconNameCombobox({ value, onValueChange, onIsExisting }: IconNam
<span className="font-mono">{value}</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>
<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>
<ComboboxEmpty>No existing icon found.</ComboboxEmpty>
{existingIcons.length > 0 && (
{!loading && existingIcons.length > 0 && (
<ComboboxGroup heading="Existing Icons">
{existingIcons.map((icon) => (
<ComboboxItem key={icon.value} value={icon.value}>
@@ -91,20 +94,6 @@ export function IconNameCombobox({ value, onValueChange, onIsExisting }: IconNam
))}
</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>
</ComboboxContent>
</Combobox>