tangled
alpha
login
or
join now
graham.systems
/
statusphere-react
forked from
samuel.fm/statusphere-react
0
fork
atom
the statusphere demo reworked into a vite/react app in a monorepo
0
fork
atom
overview
issues
pulls
pipelines
better form
samuel.fm
1 year ago
52e6f7b5
53c09364
+71
-18
2 changed files
expand all
collapse all
unified
split
packages
client
src
hooks
useAuth.tsx
pages
LoginPage.tsx
+9
-1
packages/client/src/hooks/useAuth.tsx
···
75
75
setError(null)
76
76
77
77
try {
78
78
-
const result = await api.login(handle)
78
78
+
// Add a small artificial delay for UX purposes
79
79
+
const loginPromise = api.login(handle)
80
80
+
81
81
+
// Ensure the loading state shows for at least 800ms for better UX
82
82
+
const result = await Promise.all([
83
83
+
loginPromise,
84
84
+
new Promise(resolve => setTimeout(resolve, 800))
85
85
+
]).then(([loginResult]) => loginResult)
86
86
+
79
87
return result
80
88
} catch (err) {
81
89
const message = err instanceof Error ? err.message : 'Login failed'
+62
-17
packages/client/src/pages/LoginPage.tsx
···
1
1
import { useState } from 'react'
2
2
import { Link } from 'react-router-dom'
3
3
+
import { useMutation } from '@tanstack/react-query'
3
4
4
5
import Header from '#/components/Header'
5
6
import { useAuth } from '#/hooks/useAuth'
···
7
8
const LoginPage = () => {
8
9
const [handle, setHandle] = useState('')
9
10
const [error, setError] = useState<string | null>(null)
10
10
-
const { login, loading } = useAuth()
11
11
+
const { login } = useAuth()
11
12
12
12
-
const handleSubmit = async (e: React.FormEvent) => {
13
13
+
const mutation = useMutation({
14
14
+
mutationFn: async (handleValue: string) => {
15
15
+
const result = await login(handleValue)
16
16
+
17
17
+
// Add a small delay before redirecting for better UX
18
18
+
await new Promise((resolve) => setTimeout(resolve, 500))
19
19
+
20
20
+
return result
21
21
+
},
22
22
+
onSuccess: (data) => {
23
23
+
// Redirect to ATProto OAuth flow
24
24
+
window.location.href = data.redirectUrl
25
25
+
},
26
26
+
onError: (err) => {
27
27
+
const message = err instanceof Error ? err.message : 'Login failed'
28
28
+
setError(message)
29
29
+
},
30
30
+
})
31
31
+
32
32
+
const handleSubmit = (e: React.FormEvent) => {
13
33
e.preventDefault()
34
34
+
setError(null)
14
35
15
36
if (!handle.trim()) {
16
37
setError('Handle cannot be empty')
17
38
return
18
39
}
19
40
20
20
-
try {
21
21
-
const { redirectUrl } = await login(handle)
22
22
-
// Redirect to ATProto OAuth flow
23
23
-
window.location.href = redirectUrl
24
24
-
} catch (err) {
25
25
-
const message = err instanceof Error ? err.message : 'Login failed'
26
26
-
setError(message)
27
27
-
}
41
41
+
mutation.mutate(handle)
28
42
}
43
43
+
44
44
+
// count success as also pending, since the browser should be redirecting
45
45
+
const pending = mutation.isPending || mutation.isSuccess
29
46
30
47
return (
31
48
<div className="flex flex-col gap-8">
···
42
59
43
60
<form onSubmit={handleSubmit}>
44
61
<div className="mb-4">
45
45
-
<label htmlFor="handle" className="block mb-2 text-gray-700 dark:text-gray-300">
62
62
+
<label
63
63
+
htmlFor="handle"
64
64
+
className="block mb-2 text-gray-700 dark:text-gray-300"
65
65
+
>
46
66
Enter your Bluesky handle:
47
67
</label>
48
68
<input
···
51
71
value={handle}
52
72
onChange={(e) => setHandle(e.target.value)}
53
73
placeholder="example.bsky.social"
54
54
-
disabled={loading}
55
55
-
className="w-full p-3 border border-gray-300 dark:border-gray-700 bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-300 dark:focus:ring-blue-500"
74
74
+
disabled={pending}
75
75
+
className="w-full p-3 border border-gray-300 dark:border-gray-700 bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-300 dark:focus:ring-blue-500 transition-colors"
56
76
/>
57
77
</div>
58
78
59
79
<button
60
80
type="submit"
61
61
-
disabled={loading}
62
62
-
className={`w-full px-4 py-2 rounded-md bg-blue-500 dark:bg-blue-600 text-white font-medium hover:bg-blue-600 dark:hover:bg-blue-700 transition-colors focus:outline-none focus:ring-2 focus:ring-blue-300 dark:focus:ring-blue-500 ${
63
63
-
loading ? 'opacity-70 cursor-not-allowed' : ''
81
81
+
disabled={pending}
82
82
+
className={`w-full px-4 py-2 rounded-md bg-blue-500 dark:bg-blue-600 text-white font-medium hover:bg-blue-600 dark:hover:bg-blue-700 transition-colors focus:outline-none focus:ring-2 focus:ring-blue-300 dark:focus:ring-blue-500 relative ${
83
83
+
pending ? 'opacity-90 cursor-not-allowed' : ''
64
84
}`}
65
85
>
66
66
-
{loading ? 'Logging in...' : 'Login'}
86
86
+
<span className={pending ? 'invisible' : ''}>Login</span>
87
87
+
{pending && (
88
88
+
<span className="absolute inset-0 flex items-center justify-center">
89
89
+
<svg
90
90
+
className="animate-spin -ml-1 mr-2 h-5 w-5 text-white"
91
91
+
xmlns="http://www.w3.org/2000/svg"
92
92
+
fill="none"
93
93
+
viewBox="0 0 24 24"
94
94
+
>
95
95
+
<circle
96
96
+
className="opacity-25"
97
97
+
cx="12"
98
98
+
cy="12"
99
99
+
r="10"
100
100
+
stroke="currentColor"
101
101
+
strokeWidth="4"
102
102
+
></circle>
103
103
+
<path
104
104
+
className="opacity-75"
105
105
+
fill="currentColor"
106
106
+
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
107
107
+
></path>
108
108
+
</svg>
109
109
+
<span>Connecting...</span>
110
110
+
</span>
111
111
+
)}
67
112
</button>
68
113
</form>
69
114