// src/components/Login.tsx import AtpAgent, { Agent } from "@atproto/api"; import { useAtom } from "jotai"; import React, { useEffect, useRef, useState } from "react"; import { useAuth } from "~/providers/UnifiedAuthProvider"; import { imgCDNAtom } from "~/utils/atoms"; import { useQueryIdentity, useQueryProfile } from "~/utils/useQuery"; // --- 1. The Main Component (Orchestrator with `compact` prop) --- export default function Login({ compact = false, popup = false, }: { compact?: boolean; popup?: boolean; }) { const { status, agent, logout } = useAuth(); // Loading state can be styled differently based on the prop if (status === "loading") { return (
); } // --- LOGGED IN STATE --- if (status === "signedIn") { // Large view if (!compact) { return (

You are logged in!

); } // Compact view return (
); } // --- LOGGED OUT STATE --- if (!compact) { // Large view renders the form directly in the card return (
); } // Compact view renders a button that toggles the form in a dropdown return ; } // --- 2. The Reusable, Self-Contained Login Form Component --- export function UnifiedLoginForm() { const [mode, setMode] = useState<"oauth" | "password">("oauth"); return (
setMode("oauth")} /> setMode("password")} />
{mode === "oauth" ? : }
); } // --- 3. Helper components for layouts, forms, and UI --- // A new component to contain the logic for the compact dropdown const CompactLoginButton = ({ popup }: { popup?: boolean }) => { const [showForm, setShowForm] = useState(false); const formRef = useRef(null); useEffect(() => { function handleClickOutside(event: MouseEvent) { if (formRef.current && !formRef.current.contains(event.target as Node)) { setShowForm(false); } } if (showForm) { document.addEventListener("mousedown", handleClickOutside); } return () => { document.removeEventListener("mousedown", handleClickOutside); }; }, [showForm]); return (
{showForm && (
)}
); }; const TabButton = ({ label, active, onClick, }: { label: string; active: boolean; onClick: () => void; }) => ( ); const OAuthForm = () => { const { loginWithOAuth } = useAuth(); const [handle, setHandle] = useState(""); useEffect(() => { const lastHandle = localStorage.getItem("lastHandle"); if (lastHandle) setHandle(lastHandle); }, []); const handleSubmit = (e: React.FormEvent) => { e.preventDefault(); if (handle.trim()) { localStorage.setItem("lastHandle", handle); loginWithOAuth(handle); } }; return (

Sign in with AT. Your password is never shared.

{/* setHandle(e.target.value)} className="px-3 py-2 rounded border border-gray-300 dark:border-gray-700 bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 text-sm focus:outline-none focus:ring-2 focus:ring-gray-500" /> */}
setHandle(e.target.value)} />
); }; const PasswordForm = () => { const { loginWithPassword } = useAuth(); const [user, setUser] = useState(""); const [password, setPassword] = useState(""); const [serviceURL, setServiceURL] = useState("bsky.social"); const [error, setError] = useState(null); useEffect(() => { const lastHandle = localStorage.getItem("lastHandle"); if (lastHandle) setUser(lastHandle); }, []); const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); setError(null); try { localStorage.setItem("lastHandle", user); await loginWithPassword(user, password, `https://${serviceURL}`); } catch (err) { setError("Login failed. Check your handle and App Password."); } }; return (

Warning: Less secure. Use an App Password.

{/* setUser(e.target.value)} className="px-3 py-2 rounded border border-gray-300 dark:border-gray-700 bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 text-sm focus:outline-none focus:ring-2 focus:ring-gray-500" autoComplete="username" /> setPassword(e.target.value)} className="px-3 py-2 rounded border border-gray-300 dark:border-gray-700 bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 text-sm focus:outline-none focus:ring-2 focus:ring-gray-500" autoComplete="current-password" /> setServiceURL(e.target.value)} className="px-3 py-2 rounded border border-gray-300 dark:border-gray-700 bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 text-sm focus:outline-none focus:ring-2 focus:ring-gray-500" /> */}
setUser(e.target.value)} />
setPassword(e.target.value)} />
setServiceURL(e.target.value)} />
{error &&

{error}

}
); }; // --- Profile Component (now supports a `large` prop for styling) --- export const ProfileThing = ({ agent, large = false, }: { agent: Agent | null; large?: boolean; }) => { const did = ((agent as AtpAgent)?.session?.did ?? (agent as AtpAgent)?.assertDid ?? agent?.did) as string | undefined; const { data: identity } = useQueryIdentity(did); const { data: profiledata } = useQueryProfile( `at://${did}/app.bsky.actor.profile/self` ); const profile = profiledata?.value; const [imgcdn] = useAtom(imgCDNAtom) function getAvatarUrl(p: typeof profile) { const link = p?.avatar?.ref?.["$link"]; if (!link || !did) return null; return `https://${imgcdn}/img/avatar/plain/${did}/${link}@jpeg`; } if (!profiledata) { return ( // Skeleton loader
); } return (
avatar
{profile?.displayName}
@{identity?.handle}
); };