URLField.tsx•5.06 kB
'use client'
import { Badge } from '@/src/components/ui/badge';
import { Input } from '@/src/components/ui/input';
import { splitUrl } from '@/src/lib/client-utils';
import { cn, getSimpleIcon } from '@/src/lib/utils';
import { integrations } from '@superglue/shared';
import { Link } from 'lucide-react';
import { forwardRef, useCallback, useEffect, useImperativeHandle, useRef, useState } from 'react';
interface URLFieldProps {
url: string
onUrlChange: (urlHost: string, urlPath: string, queryParams: Record<string, string>) => void
placeholder?: string
className?: string
error?: boolean
required?: boolean
}
export interface URLFieldHandle {
commit: () => void
}
export const URLField = forwardRef<URLFieldHandle, URLFieldProps>(function URLField(
{ url: initialUrl, onUrlChange, placeholder = "https://api.example.com/v1", className, error = false, required = false },
ref
) {
const [url, setUrl] = useState(initialUrl || '')
const [isValid, setIsValid] = useState<boolean | null>(null)
const [iconName, setIconName] = useState<string | null>(null)
const debounceTimerRef = useRef<NodeJS.Timeout | null>(null)
const previousUrlRef = useRef(initialUrl);
const validateUrl = (url: string): boolean => {
if (!url) return false
try {
new URL(url)
return true
} catch {
return false
}
}
const getIconForUrl = (url: string): string | null => {
if (!url) return null
try {
const urlObj = new URL(url)
const fullPath = `${urlObj.hostname}${urlObj.pathname}`
for (const [name, integration] of Object.entries(integrations)) {
const regex = new RegExp(integration.regex)
if (regex.test(fullPath)) {
return integration.icon as string
}
}
return null
} catch {
return null
}
}
const extractQueryParams = (url: string): Record<string, string> => {
try {
const urlObj = new URL(url);
const params: Record<string, string> = {};
urlObj.searchParams.forEach((value, key) => {
params[key] = value;
});
return params;
} catch {
return {};
}
}
const commitUrl = useCallback((rawUrl: string) => {
let newUrl = rawUrl
if (newUrl && !newUrl.includes('://') && newUrl.includes('.')) {
newUrl = `https://${newUrl}`
setUrl(newUrl)
}
setIconName(getIconForUrl(newUrl))
const { urlHost, urlPath } = splitUrl(newUrl)
const queryParams = extractQueryParams(newUrl)
onUrlChange(urlHost, urlPath, queryParams)
}, [onUrlChange])
const handleBlur = useCallback(() => {
commitUrl(url)
}, [url, commitUrl])
const handleInputChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
const newUrl = e.target.value
setUrl(newUrl)
}, [onUrlChange]);
const handleClear = useCallback(() => {
setUrl('')
setIsValid(null)
setIconName(null)
onUrlChange('', '', {})
}, [onUrlChange])
useEffect(() => {
// Only update internal state when prop actually changes
if (initialUrl !== previousUrlRef.current) {
setUrl(initialUrl || '')
const valid = validateUrl(initialUrl)
setIsValid(valid)
if (valid) {
setIconName(getIconForUrl(initialUrl))
}
previousUrlRef.current = initialUrl;
}
}, [initialUrl])
useEffect(() => {
return () => {
if (debounceTimerRef.current) {
clearTimeout(debounceTimerRef.current)
}
}
}, [])
// Get the simple-icon for the current URL if available
const simpleIcon = iconName ? getSimpleIcon(iconName) : null
useImperativeHandle(ref, () => ({
commit: () => commitUrl(url)
}))
return (
<div className={className}>
<div className="relative flex items-center gap-2">
<div className="relative flex-1">
<Input
value={url}
onChange={handleInputChange}
onBlur={handleBlur}
placeholder={placeholder}
className={cn(
"pr-28",
error && "border-destructive focus-visible:ring-destructive"
)}
required={required}
/>
<div className="absolute right-2 top-1/2 -translate-y-1/2 flex items-center gap-1">
<Badge variant="outline">
{simpleIcon ? (
<div className="flex items-center">
<svg
width="12"
height="12"
viewBox="0 0 24 24"
fill={`#${simpleIcon.hex}`}
className="mr-1"
>
<path d={simpleIcon.path} />
</svg>
<span>{String(simpleIcon.title || "URL").charAt(0).toUpperCase() + String(simpleIcon.title || "URL").slice(1)}</span>
</div>
) : (
<>
<Link className="h-3 w-3 mr-1" /> URL
</>
)}
</Badge>
</div>
</div>
</div>
</div>
)
})