forked from
daviddao.org/eii-frontend
this repo has no description
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}