Retro Bulletin Board Systems on atproto. Web app and TUI. lazy mirror of alyraffauf/atbbs atbbs.xyz
forums python tui atproto bbs
3
fork

Configure Feed

Select the types of activity you want to include in your feed.

web: revamp login page with bsky actor search

+162 -21
+31
web/src/hooks/useHandleSearch.ts
··· 1 + /** Debounced handle typeahead using Bluesky's public API. */ 2 + 3 + import { useEffect, useState } from "react"; 4 + import { searchHandles, type HandleMatch } from "../lib/bsky"; 5 + 6 + const DEBOUNCE_MS = 300; 7 + 8 + export function useHandleSearch(query: string): HandleMatch[] { 9 + const [matches, setMatches] = useState<HandleMatch[]>([]); 10 + 11 + useEffect(() => { 12 + const trimmed = query.trim(); 13 + if (trimmed.length < 2) { 14 + setMatches([]); 15 + return; 16 + } 17 + 18 + let cancelled = false; 19 + const timeout = setTimeout(async () => { 20 + const results = await searchHandles(trimmed); 21 + if (!cancelled) setMatches(results); 22 + }, DEBOUNCE_MS); 23 + 24 + return () => { 25 + cancelled = true; 26 + clearTimeout(timeout); 27 + }; 28 + }, [query]); 29 + 30 + return matches; 31 + }
+32
web/src/lib/bsky.ts
··· 1 + /** Bluesky public API helpers. */ 2 + 3 + const BSKY_PUBLIC = "https://public.api.bsky.app"; 4 + 5 + export interface HandleMatch { 6 + handle: string; 7 + displayName: string; 8 + avatar?: string; 9 + } 10 + 11 + export async function searchHandles( 12 + query: string, 13 + limit = 5, 14 + ): Promise<HandleMatch[]> { 15 + const url = 16 + `${BSKY_PUBLIC}/xrpc/app.bsky.actor.searchActorsTypeahead` + 17 + `?q=${encodeURIComponent(query)}&limit=${limit}`; 18 + const resp = await fetch(url); 19 + if (!resp.ok) return []; 20 + const data = (await resp.json()) as { 21 + actors: { 22 + handle: string; 23 + displayName?: string; 24 + avatar?: string; 25 + }[]; 26 + }; 27 + return data.actors.map((actor) => ({ 28 + handle: actor.handle, 29 + displayName: actor.displayName ?? actor.handle, 30 + avatar: actor.avatar, 31 + })); 32 + }
+99 -21
web/src/pages/Login.tsx
··· 1 - import { useState, type SyntheticEvent } from "react"; 1 + import { useRef, useState, type SyntheticEvent } from "react"; 2 2 import { useAuth } from "../lib/auth"; 3 3 import { usePageTitle } from "../hooks/usePageTitle"; 4 + import { useHandleSearch } from "../hooks/useHandleSearch"; 4 5 import HandleInput from "../components/form/HandleInput"; 5 6 import { Button } from "../components/form/Form"; 6 7 ··· 9 10 const [handle, setHandle] = useState(""); 10 11 const [error, setError] = useState<string | null>(null); 11 12 const [busy, setBusy] = useState(false); 13 + const [focused, setFocused] = useState(false); 14 + const blurTimeout = useRef<ReturnType<typeof setTimeout>>(undefined); 15 + const matches = useHandleSearch(handle); 12 16 usePageTitle("Login — atbbs"); 13 17 14 - async function onSubmit(e: SyntheticEvent) { 15 - e.preventDefault(); 18 + async function onSubmit(event: SyntheticEvent) { 19 + event.preventDefault(); 16 20 setError(null); 17 21 setBusy(true); 18 22 try { ··· 23 27 } 24 28 } 25 29 30 + function selectHandle(selected: string) { 31 + setHandle(selected); 32 + setFocused(false); 33 + } 34 + 35 + function onFocus() { 36 + clearTimeout(blurTimeout.current); 37 + setFocused(true); 38 + } 39 + 40 + function onBlur() { 41 + blurTimeout.current = setTimeout(() => setFocused(false), 150); 42 + } 43 + 26 44 return ( 27 - <> 28 - <h1 className="text-lg text-neutral-200 mb-1">log in</h1> 29 - <p className="text-neutral-500 mb-6"> 30 - Sign in with your atproto handle to post threads and replies. 45 + <div className="h-full flex flex-col justify-center overflow-hidden max-w-md mx-auto"> 46 + <div className="text-center mb-8"> 47 + <picture> 48 + <source 49 + srcSet="/hero-dark.svg" 50 + media="(prefers-color-scheme: dark)" 51 + /> 52 + <img 53 + src="/hero.svg" 54 + alt="@bbs" 55 + className="mx-auto mb-4" 56 + style={{ width: 180, imageRendering: "pixelated" }} 57 + /> 58 + </picture> 59 + <h1 className="text-lg text-neutral-200 mb-2">Log in to atbbs</h1> 60 + <p className="text-neutral-500">Use any atproto account.</p> 61 + </div> 62 + 63 + {error && <p className="text-red-400 mb-4 text-center">{error}</p>} 64 + 65 + <div onFocus={onFocus} onBlur={onBlur} className="mb-6"> 66 + <form onSubmit={onSubmit} className="flex gap-2"> 67 + <HandleInput 68 + name="handle" 69 + value={handle} 70 + onChange={setHandle} 71 + required 72 + className="flex-1" 73 + /> 74 + <Button type="submit" disabled={busy}> 75 + {busy ? "..." : "log in"} 76 + </Button> 77 + </form> 78 + {focused && matches.length > 0 && ( 79 + <div className="relative"> 80 + <div className="absolute left-0 right-0 mt-1 bg-neutral-900 border border-neutral-800 rounded shadow-lg z-10"> 81 + {matches.map((match) => ( 82 + <button 83 + key={match.handle} 84 + type="button" 85 + onClick={() => selectHandle(match.handle)} 86 + className="flex items-center gap-3 w-full px-3 py-2 text-left hover:bg-neutral-800 first:rounded-t last:rounded-b" 87 + > 88 + {match.avatar && ( 89 + <img 90 + src={match.avatar} 91 + alt="" 92 + className="w-6 h-6 rounded-full shrink-0" 93 + /> 94 + )} 95 + <div className="min-w-0"> 96 + <div className="text-sm text-neutral-200 truncate"> 97 + {match.displayName} 98 + </div> 99 + <div className="text-xs text-neutral-500 truncate"> 100 + {match.handle} 101 + </div> 102 + </div> 103 + </button> 104 + ))} 105 + </div> 106 + </div> 107 + )} 108 + </div> 109 + 110 + <div className="text-neutral-500 text-xs space-y-2"> 111 + <p>Once signed in, you can:</p> 112 + <ul className="list-disc list-inside space-y-1 text-neutral-600"> 113 + <li>Post threads and replies</li> 114 + <li>Pin boards you like</li> 115 + <li>Set up a profile</li> 116 + <li>Start your own community</li> 117 + </ul> 118 + </div> 119 + 120 + <p className="text-neutral-600 text-xs mt-6"> 121 + You'll be redirected to your hosting provider to continue. 31 122 </p> 32 - {error && <p className="text-red-400 mb-4">{error}</p>} 33 - <form onSubmit={onSubmit} className="flex gap-2 max-w-md"> 34 - <HandleInput 35 - name="handle" 36 - value={handle} 37 - onChange={setHandle} 38 - required 39 - className="flex-1" 40 - /> 41 - <Button type="submit" disabled={busy}> 42 - {busy ? "..." : "log in"} 43 - </Button> 44 - </form> 45 - </> 123 + </div> 46 124 ); 47 125 }