this repo has no description
0
fork

Configure Feed

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

at change-requests 248 lines 8.4 kB view raw
1'use client' 2 3import { useState, useEffect, useRef } from 'react' 4import { LogIn, LogOut, User, FolderOpen } from 'lucide-react' 5import Link from 'next/link' 6 7interface UserProfile { 8 did: string 9 handle: string 10 displayName?: string 11 avatar?: string 12 description?: string 13} 14 15interface LoginButtonProps { 16 className?: string 17} 18 19export default function LoginButton({ className }: LoginButtonProps) { 20 const [isLoggedIn, setIsLoggedIn] = useState(false) 21 const [profile, setProfile] = useState<UserProfile | null>(null) 22 const [handle, setHandle] = useState('') 23 const [showLoginForm, setShowLoginForm] = useState(false) 24 const [showProfileMenu, setShowProfileMenu] = useState(false) 25 const [loading, setLoading] = useState(false) 26 const [error, setError] = useState<string | null>(null) 27 const profileMenuRef = useRef<HTMLDivElement>(null) 28 const loginFormRef = useRef<HTMLDivElement>(null) 29 30 useEffect(() => { 31 checkLoginStatus() 32 }, []) 33 34 useEffect(() => { 35 const handleClickOutside = (event: MouseEvent) => { 36 if (profileMenuRef.current && !profileMenuRef.current.contains(event.target as Node)) { 37 setShowProfileMenu(false) 38 } 39 if (loginFormRef.current && !loginFormRef.current.contains(event.target as Node)) { 40 setShowLoginForm(false) 41 } 42 } 43 44 document.addEventListener('mousedown', handleClickOutside) 45 return () => document.removeEventListener('mousedown', handleClickOutside) 46 }, []) 47 48 const checkLoginStatus = async () => { 49 try { 50 const response = await fetch('/api/status') 51 if (response.ok) { 52 const data = await response.json() 53 if (data.did) { 54 setIsLoggedIn(true) 55 await fetchProfile() 56 } else { 57 setIsLoggedIn(false) 58 setProfile(null) 59 } 60 } 61 } catch (err) { 62 console.error('Error checking login status:', err) 63 } 64 } 65 66 const fetchProfile = async () => { 67 try { 68 const response = await fetch('/api/profile') 69 if (response.ok) { 70 const profileData = await response.json() 71 setProfile(profileData) 72 } 73 } catch (err) { 74 console.error('Error fetching profile:', err) 75 } 76 } 77 78 const handleLogin = async (e: React.FormEvent) => { 79 e.preventDefault() 80 if (!handle.trim()) return 81 82 setLoading(true) 83 setError(null) 84 85 try { 86 const response = await fetch('/api/login', { 87 method: 'POST', 88 headers: { 89 'Content-Type': 'application/json', 90 }, 91 body: JSON.stringify({ handle: handle.trim() }), 92 }) 93 94 if (response.ok) { 95 const data = await response.json() 96 window.location.href = data.redirectUrl 97 } else { 98 const errorData = await response.json() 99 setError(errorData.error || 'Login failed') 100 } 101 } catch (err) { 102 setError('Network error') 103 console.error('Login failed:', err) 104 } finally { 105 setLoading(false) 106 } 107 } 108 109 const handleLogout = async () => { 110 try { 111 await fetch('/api/logout', { method: 'POST' }) 112 setIsLoggedIn(false) 113 setProfile(null) 114 setShowProfileMenu(false) 115 window.location.reload() 116 } catch (err) { 117 console.error('Logout failed:', err) 118 } 119 } 120 121 if (isLoggedIn && profile) { 122 return ( 123 <div className="relative" ref={profileMenuRef}> 124 <button 125 onClick={() => setShowProfileMenu(!showProfileMenu)} 126 className={`flex items-center space-x-2 px-2 py-2 text-sm font-medium text-secondary hover:text-primary transition-colors rounded-lg ${className}`} 127 > 128 {profile.avatar ? ( 129 <img 130 src={profile.avatar} 131 alt={profile.displayName || profile.handle} 132 className="h-7 w-7 rounded-full border-2 border-transparent hover:border-accent transition-colors" 133 /> 134 ) : ( 135 <div className="h-7 w-7 rounded-full bg-accent-light flex items-center justify-center border-2 border-transparent hover:border-accent transition-colors"> 136 <User className="h-4 w-4 text-accent" /> 137 </div> 138 )} 139 <span className="hidden lg:inline"> 140 {profile.displayName || profile.handle} 141 </span> 142 </button> 143 144 {showProfileMenu && ( 145 <div className="absolute right-0 top-full mt-2 w-64 p-4 bg-surface border border-border rounded-lg shadow-lg z-50"> 146 <div className="flex items-center space-x-3 mb-3"> 147 {profile.avatar ? ( 148 <img 149 src={profile.avatar} 150 alt={profile.displayName || profile.handle} 151 className="h-10 w-10 rounded-full" 152 /> 153 ) : ( 154 <div className="h-10 w-10 rounded-full bg-accent-light flex items-center justify-center"> 155 <User className="h-6 w-6 text-accent" /> 156 </div> 157 )} 158 <div className="flex-1 min-w-0"> 159 <p className="text-sm font-medium text-primary truncate"> 160 {profile.displayName || profile.handle} 161 </p> 162 <p className="text-xs text-secondary truncate"> 163 @{profile.handle} 164 </p> 165 </div> 166 </div> 167 168 {profile.description && ( 169 <p className="text-xs text-secondary mb-3 line-clamp-2"> 170 {profile.description} 171 </p> 172 )} 173 174 <div className="space-y-2"> 175 <Link 176 href={`/project/${encodeURIComponent(profile.did)}`} 177 className="w-full flex items-center justify-center space-x-2 px-3 py-2 text-xs font-medium text-secondary hover:text-primary border border-border rounded-md transition-colors" 178 onClick={() => setShowProfileMenu(false)} 179 > 180 <FolderOpen className="h-3 w-3" /> 181 <span>My Project</span> 182 </Link> 183 184 <button 185 onClick={handleLogout} 186 className="w-full flex items-center justify-center space-x-2 px-3 py-2 text-xs font-medium text-secondary hover:text-primary border border-border rounded-md transition-colors" 187 > 188 <LogOut className="h-3 w-3" /> 189 <span>Sign out</span> 190 </button> 191 </div> 192 </div> 193 )} 194 </div> 195 ) 196 } 197 198 if (showLoginForm) { 199 return ( 200 <div className="relative" ref={loginFormRef}> 201 <div className="absolute right-0 top-full mt-2 w-72 p-4 bg-surface border border-border rounded-lg shadow-lg z-50"> 202 <form onSubmit={handleLogin} className="space-y-3"> 203 <div> 204 <input 205 type="text" 206 placeholder="Enter your Bluesky handle" 207 value={handle} 208 onChange={(e) => setHandle(e.target.value)} 209 className="w-full px-3 py-2 text-sm border border-border rounded-md bg-surface text-primary focus:outline-none focus:ring-2 focus:ring-accent focus:border-transparent" 210 required 211 disabled={loading} 212 /> 213 </div> 214 {error && ( 215 <p className="text-xs text-red-500">{error}</p> 216 )} 217 <div className="flex space-x-2"> 218 <button 219 type="submit" 220 disabled={loading} 221 className="flex-1 px-3 py-2 text-xs font-medium text-white bg-accent hover:bg-accent-hover disabled:opacity-50 rounded-md transition-colors" 222 > 223 {loading ? 'Signing in...' : 'Sign in'} 224 </button> 225 <button 226 type="button" 227 onClick={() => setShowLoginForm(false)} 228 className="px-3 py-2 text-xs font-medium text-secondary hover:text-primary border border-border rounded-md transition-colors" 229 > 230 Cancel 231 </button> 232 </div> 233 </form> 234 </div> 235 </div> 236 ) 237 } 238 239 return ( 240 <button 241 onClick={() => setShowLoginForm(true)} 242 className={`flex items-center space-x-2 px-2 py-2 text-sm font-medium text-secondary hover:text-primary transition-colors rounded-lg ${className}`} 243 > 244 <LogIn className="h-4 w-4" /> 245 <span className="hidden lg:inline">Sign In</span> 246 </button> 247 ) 248}