import React, { useEffect, useState } from 'react'; import { AlertCircle, AtSign, BadgeCheck, CheckCircle, Loader2 } from 'lucide-react'; import { Button } from '@/components/ui/button'; import { useSkateNip05 } from '@/hooks/useSkateNip05'; import { SKATE_NIP05_DOMAIN, parseSkateNip05, validateSkateNip05Name, } from '@/lib/skateNip05'; import { cn } from '@/lib/utils'; import { useTranslation } from '@/contexts/language'; type FieldStatus = 'idle' | 'checking' | 'available' | 'taken' | 'invalid' | 'error' | 'claimed'; interface SkateNip05InputProps { /** The local part being edited (no `@domain`). */ handle: string; onHandleChange: (handle: string) => void; /** The full `name@skate-spots.org` already claimed, if any. */ claimed?: string | null; /** Fired once a claim succeeds. */ onClaimed?: (nip05: string) => void; disabled?: boolean; autoFocus?: boolean; id?: string; className?: string; } /** * Input for claiming a free `name@skate-spots.org` handle. * * Self-contained: it validates locally, checks availability against the registry * (debounced), and claims on demand. A failure is never fatal — the parent simply * ends up without a handle, which is a perfectly usable account. */ export const SkateNip05Input: React.FC = ({ handle, onHandleChange, claimed, onClaimed, disabled, autoFocus, id = 'skate-nip05', className, }) => { const { checkAvailability, claim } = useSkateNip05(); const { t } = useTranslation(); const [status, setStatus] = useState('idle'); const [message, setMessage] = useState(null); const [claiming, setClaiming] = useState(false); const validation = validateSkateNip05Name(handle); const claimedHandle = parseSkateNip05(claimed); const isClaimed = !!claimedHandle && claimedHandle === validation.name; // Debounced availability check — mirrors the server's rules so we rarely make // a pointless request, and aborts an in-flight one when the input changes. useEffect(() => { if (isClaimed) { setStatus('claimed'); setMessage(null); return; } if (!handle.trim()) { setStatus('idle'); setMessage(null); return; } if (!validation.ok) { setStatus('invalid'); setMessage(validation.reason ?? null); return; } setStatus('checking'); setMessage(null); const controller = new AbortController(); const timer = setTimeout(async () => { const res = await checkAvailability(validation.name, controller.signal); if (controller.signal.aborted) return; setStatus(res.available ? 'available' : 'taken'); setMessage(res.reason); }, 400); return () => { clearTimeout(timer); controller.abort(); }; }, [handle, isClaimed, validation.ok, validation.name, validation.reason, checkAvailability]); const handleClaim = async () => { if (!validation.ok) return; setClaiming(true); const result = await claim(validation.name); setClaiming(false); if (result.ok) { setStatus('claimed'); setMessage(null); onClaimed?.(result.nip05); } else { setStatus('error'); setMessage(result.error); } }; return (
onHandleChange(e.target.value)} placeholder={t('settingsUi.skateNip05.placeholder')} spellCheck={false} autoCapitalize="none" autoComplete="off" disabled={disabled || claiming} autoFocus={autoFocus} className="min-w-0 flex-1 bg-transparent px-3 py-2 text-sm outline-none placeholder:text-muted-foreground" /> {SKATE_NIP05_DOMAIN}
{status === 'claimed' ? (

{claimedHandle}@{SKATE_NIP05_DOMAIN} {t('settingsUi.skateNip05.claimedSuffix')}

) : status === 'checking' ? (

{t('settingsUi.skateNip05.checking')}

) : status === 'available' ? (

{t('settingsUi.skateNip05.available', { name: validation.name, domain: SKATE_NIP05_DOMAIN })}

) : status === 'invalid' || status === 'taken' || status === 'error' ? (

{message}

) : (

{t('settingsUi.skateNip05.hint')}

)}
); };