Something went wrong. Try again.
A Social Deduction / Deception Game
Something went wrong. Try again.
4.4 kB · 123 lines
TSX
at main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124import { useEffect, useRef, useState } from 'react'
type Suggestion = { did: string handle: string displayName?: string avatar?: string}
export function LoginForm() { const [handle, setHandle] = useState('') const [suggestions, setSuggestions] = useState<Suggestion[]>([]) const [open, setOpen] = useState(false) const [submitting, setSubmitting] = useState(false) const [error, setError] = useState<string | null>(null) const abortRef = useRef<AbortController | null>(null)
useEffect(() => { const q = handle.trim() if (q.length < 1) { setSuggestions([]) return } abortRef.current?.abort() const ctrl = new AbortController() abortRef.current = ctrl const timer = setTimeout(async () => { try { const url = `https://public.api.bsky.app/xrpc/app.bsky.actor.searchActorsTypeahead?q=${encodeURIComponent(q)}&limit=8` const res = await fetch(url, { signal: ctrl.signal }) if (!res.ok) return const data = (await res.json()) as { actors: Suggestion[] } setSuggestions(data.actors ?? []) } catch (err) { if ((err as Error).name !== 'AbortError') { console.error('typeahead failed', err) } } }, 200) return () => { clearTimeout(timer) ctrl.abort() } }, [handle])
function onSubmit(e: React.FormEvent) { e.preventDefault() const value = handle.trim() if (!value) return setSubmitting(true) setError(null) // Hand off to the server's OAuth flow: it redirects to the PDS and lands back on /oauth/callback. window.location.href = `/oauth/login?handle=${encodeURIComponent(value)}` }
return ( <form onSubmit={onSubmit} className="w-full max-w-sm space-y-3"> <label className="block"> <span className="block text-xs uppercase tracking-widest text-zinc-500 mb-2"> handle </span> <div className="relative"> <input type="text" autoComplete="off" autoCorrect="off" autoCapitalize="off" spellCheck={false} autoFocus value={handle} onChange={(e) => { setHandle(e.target.value) setOpen(true) }} onFocus={() => setOpen(true)} onBlur={() => setTimeout(() => setOpen(false), 150)} placeholder="alice.bsky.social" className="w-full rounded-md border border-zinc-800 bg-zinc-900 px-3 py-2 text-zinc-100 placeholder:text-zinc-600 focus:border-zinc-500 focus:outline-none" disabled={submitting} /> {open && suggestions.length > 0 && ( <ul className="absolute z-10 mt-1 w-full rounded-md border border-zinc-800 bg-zinc-900 divide-y divide-zinc-800 overflow-hidden shadow-lg"> {suggestions.map((s) => ( <li key={s.did}> <button type="button" onMouseDown={(e) => e.preventDefault()} onClick={() => { setHandle(s.handle) setSuggestions([]) setOpen(false) }} className="w-full flex items-center gap-3 px-3 py-2 text-left hover:bg-zinc-800" > {s.avatar ? ( <img src={s.avatar} alt="" className="w-8 h-8 rounded-full" /> ) : ( <div className="w-8 h-8 rounded-full bg-zinc-800" /> )} <div className="min-w-0 flex-1"> <div className="text-sm text-zinc-100 truncate"> {s.displayName || s.handle} </div> <div className="text-xs text-zinc-500 truncate">@{s.handle}</div> </div> </button> </li> ))} </ul> )} </div> </label> <button type="submit" disabled={submitting || !handle.trim()} className="w-full rounded-md bg-zinc-100 px-3 py-2 text-sm font-medium text-zinc-900 hover:bg-white disabled:opacity-50 disabled:hover:bg-zinc-100" > {submitting ? 'redirecting…' : 'continue to pds'} </button> {error && <p className="text-sm text-red-400">{error}</p>} </form> )}