import React, { useState, useEffect, useRef } from "react"; import { Link, useSearchParams, Navigate } from "react-router-dom"; import { AtSign } from "lucide-react"; import { BlueskyIcon, MarginIcon } from "../../components/common/Icons"; import SignUpModal from "../../components/modals/SignUpModal"; import { searchActors, startLogin, type ActorSearchItem, } from "../../api/client"; import { Avatar } from "../../components/ui"; import { useStore } from "@nanostores/react"; import { $theme } from "../../store/theme"; import { $user } from "../../store/auth"; export default function Login() { useStore($theme); // ensure theme is applied on this page const user = useStore($user); const [searchParams] = useSearchParams(); const [handle, setHandle] = useState(""); const [suggestions, setSuggestions] = useState([]); const [showSuggestions, setShowSuggestions] = useState(false); const [loading, setLoading] = useState(false); const [error, setError] = useState( searchParams.get("error") || null, ); const [selectedIndex, setSelectedIndex] = useState(-1); const [showSignUp, setShowSignUp] = useState(false); const inputRef = useRef(null); const suggestionsRef = useRef(null); const isSelectionRef = useRef(false); const [providerIndex, setProviderIndex] = useState(0); const [morphClass, setMorphClass] = useState( "opacity-100 translate-y-0 blur-0", ); const providers = [ "AT Protocol", "Margin", "Bluesky", "Blacksky", "Tangled", "Northsky", "witchcraft.systems", "tophhie.social", "altq.net", ]; useEffect(() => { const cycleText = () => { setMorphClass("opacity-0 translate-y-2 blur-sm"); setTimeout(() => { setProviderIndex((prev) => (prev + 1) % providers.length); setMorphClass("opacity-100 translate-y-0 blur-0"); }, 400); }; const interval = setInterval(cycleText, 3000); return () => clearInterval(interval); }, [providers.length]); useEffect(() => { if (handle.length >= 3) { if (isSelectionRef.current) { isSelectionRef.current = false; return; } const timer = setTimeout(async () => { try { if (!handle.includes(".")) { const data = await searchActors(handle); setSuggestions(data.actors || []); setShowSuggestions(true); setSelectedIndex(-1); } } catch (e) { console.error("Search failed:", e); } }, 300); return () => clearTimeout(timer); } }, [handle]); useEffect(() => { const handleClickOutside = (e: MouseEvent) => { if ( suggestionsRef.current && !suggestionsRef.current.contains(e.target as Node) && inputRef.current && !inputRef.current.contains(e.target as Node) ) { setShowSuggestions(false); } }; document.addEventListener("mousedown", handleClickOutside); return () => document.removeEventListener("mousedown", handleClickOutside); }, []); const handleKeyDown = (e: React.KeyboardEvent) => { if (!showSuggestions || suggestions.length === 0) return; if (e.key === "ArrowDown") { e.preventDefault(); setSelectedIndex((prev) => Math.min(prev + 1, suggestions.length - 1)); } else if (e.key === "ArrowUp") { e.preventDefault(); setSelectedIndex((prev) => Math.max(prev - 1, -1)); } else if (e.key === "Enter" && selectedIndex >= 0) { e.preventDefault(); selectSuggestion(suggestions[selectedIndex]); } else if (e.key === "Escape") { setShowSuggestions(false); } }; const selectSuggestion = (actor: ActorSearchItem) => { isSelectionRef.current = true; setHandle(actor.handle); setSuggestions([]); setShowSuggestions(false); inputRef.current?.blur(); }; const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); if (!handle.trim()) return; setLoading(true); setError(null); try { const result = await startLogin(handle.trim()); if (result.authorizationUrl) { window.location.href = result.authorizationUrl; } if (result.authorizationUrl) { window.location.href = result.authorizationUrl; } } catch (err) { const message = err instanceof Error ? err.message : "Unknown error"; setError(message || "Failed to initiate login. Please try again."); setLoading(false); } }; if (user) { return ; } return (
×

Sign in with your
{providers[providerIndex]} {" "} handle

{ const val = e.target.value; setHandle(val); if (val.length < 3) { setSuggestions([]); setShowSuggestions(false); } }} onKeyDown={handleKeyDown} onFocus={() => handle.length >= 3 && suggestions.length > 0 && !handle.includes(".") && setShowSuggestions(true) } placeholder="handle.bsky.social" className="w-full pl-12 pr-4 py-3.5 bg-white dark:bg-surface-900 border border-surface-200 dark:border-surface-700 rounded-xl shadow-sm outline-none focus:border-primary-500 dark:focus:border-primary-400 focus:ring-4 focus:ring-primary-500/10 transition-all font-medium text-lg text-surface-900 dark:text-white placeholder:text-surface-400 dark:placeholder:text-surface-500" autoCapitalize="none" autoCorrect="off" autoComplete="off" spellCheck={false} disabled={loading} /> {showSuggestions && suggestions.length > 0 && (
{suggestions.map((actor, index) => ( ))}
)}
{error && (
{error}
)}

By signing in, you agree to our{" "} Terms of Service {" "} and{" "} Privacy Policy .

or
{showSignUp && setShowSignUp(false)} />}
); }