mirror of
https://github.com/walkxcode/dashboard-icons.git
synced 2025-10-27 21:49:04 +08:00
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:
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user