flowlink-auth 2.7.4 → 2.7.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/SignUp.jsx CHANGED
@@ -1,260 +1,747 @@
1
- // src/signup.jsx
1
+ // // src/signup.jsx
2
+ // 'use client'
3
+ // import React, { useState } from 'react'
4
+ // import { useAuth } from './provider.js'
5
+
6
+ // export default function SignUp() {
7
+ // const {
8
+ // publishableKey,
9
+ // baseUrl,
10
+ // redirect,
11
+ // redirectTo,
12
+ // user,
13
+ // loadingUser,
14
+ // fetchMe,
15
+ // setUser
16
+ // } = useAuth()
17
+
18
+ // const [name, setName] = useState('')
19
+ // const [email, setEmail] = useState('')
20
+ // const [password, setPassword] = useState('')
21
+ // const [loading, setLoading] = useState(false)
22
+ // const [error, setError] = useState(null)
23
+ // const [message, setMessage] = useState(null)
24
+
25
+ // if (loadingUser) return null
26
+
27
+ // if (user && redirect) {
28
+ // if (typeof redirectTo === 'function') redirectTo(redirect)
29
+ // else if (typeof window !== 'undefined') window.location.assign(redirect)
30
+ // return null
31
+ // }
32
+
33
+ // async function submit(e) {
34
+ // e.preventDefault()
35
+ // setError(null)
36
+ // setMessage(null)
37
+ // setLoading(true)
38
+
39
+ // const url = `${(baseUrl || '').replace(/\/+$/, '')}/api/sdk/signup`
40
+
41
+ // try {
42
+ // const res = await fetch(url, {
43
+ // method: 'POST',
44
+ // credentials: 'include',
45
+ // headers: {
46
+ // 'Content-Type': 'application/json',
47
+ // 'x-publishable-key': publishableKey || ''
48
+ // },
49
+ // body: JSON.stringify({ name, email, password })
50
+ // })
51
+
52
+ // const data = await res.json().catch(() => ({}))
53
+ // if (!res.ok) throw new Error(data.error || 'Signup failed')
54
+
55
+ // if (data.user && typeof setUser === 'function') setUser(data.user)
56
+ // if (typeof fetchMe === 'function') await fetchMe()
57
+
58
+ // setMessage('Account created. Redirecting…')
59
+
60
+ // if (redirect) {
61
+ // setTimeout(() => {
62
+ // if (typeof redirectTo === 'function') redirectTo(redirect)
63
+ // else if (typeof window !== 'undefined') window.location.assign(redirect)
64
+ // }, 300)
65
+ // }
66
+ // } catch (err) {
67
+ // setError(err?.message ?? 'Network error')
68
+ // console.error('Signup error:', err)
69
+ // } finally {
70
+ // setLoading(false)
71
+ // }
72
+ // }
73
+
74
+ // async function startOAuthFlow(provider) {
75
+ // // prevent double clicks
76
+ // setError(null)
77
+ // setLoading(true)
78
+
79
+ // try {
80
+ // const rid =
81
+ // (typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function')
82
+ // ? crypto.randomUUID()
83
+ // : `${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}`
84
+
85
+ // // callback must match what your SDK start expects and what Google console allows
86
+ // const callbackUrl = encodeURIComponent(`${window.location.origin}/signup`)
87
+
88
+ // // build start URL (server returns { oauthUrl })
89
+ // const sdkBase = (typeof process !== 'undefined' && process.env && process.env.NEXT_PUBLIC_FLOWLINK_BASE_URL)
90
+ // || baseUrl || window.location.origin.replace(/\/+$/, '')
91
+ // const startUrl = `${sdkBase}/sdk/auth/start?rid=${rid}&source=${encodeURIComponent(provider)}&callbackUrl=${callbackUrl}`
92
+
93
+ // // ensure publishableKey exists
94
+ // if (!publishableKey) {
95
+ // throw new Error('Missing publishable key (client side). Set NEXT_PUBLIC_FLOWLINK_PUBLISHABLE_KEY or provide publishableKey in provider.')
96
+ // }
97
+
98
+ // const res = await fetch(startUrl, {
99
+ // method: 'GET',
100
+ // headers: {
101
+ // 'x-publishable-key': publishableKey
102
+ // }
103
+ // })
104
+
105
+ // const data = await res.json().catch(() => null)
106
+ // if (!res.ok) {
107
+ // throw new Error(data?.error || `OAuth start failed (${res.status})`)
108
+ // }
109
+ // if (!data?.oauthUrl) {
110
+ // throw new Error('SDK start did not return oauthUrl')
111
+ // }
112
+
113
+ // // navigate to provider (Google/GitHub)
114
+ // window.location.href = data.oauthUrl
115
+ // } catch (err) {
116
+ // console.error('OAuth start error:', err)
117
+ // setError(err?.message || 'OAuth start failed')
118
+ // setLoading(false)
119
+ // }
120
+ // }
121
+
122
+ // const handleGoogle = (e) => {
123
+ // if (e && typeof e.preventDefault === 'function') e.preventDefault()
124
+ // startOAuthFlow('google')
125
+ // }
126
+
127
+ // const handleGithub = (e) => {
128
+ // if (e && typeof e.preventDefault === 'function') e.preventDefault()
129
+ // startOAuthFlow('github')
130
+ // }
131
+ // return (
132
+ // <div style={overlay}>
133
+ // <div style={modal}>
134
+ // <h2 style={title}>Create account</h2>
135
+
136
+ // <form onSubmit={submit}>
137
+ // <label style={label}>Full name</label>
138
+ // <input
139
+ // name="name"
140
+ // style={input}
141
+ // value={name}
142
+ // onChange={e => setName(e.target.value)}
143
+ // autoComplete="name"
144
+ // />
145
+
146
+ // <label style={label}>Email</label>
147
+ // <input
148
+ // name="email"
149
+ // type="email"
150
+ // style={input}
151
+ // value={email}
152
+ // onChange={e => setEmail(e.target.value)}
153
+ // required
154
+ // autoComplete="email"
155
+ // />
156
+
157
+ // <label style={label}>Password</label>
158
+ // <input
159
+ // name="password"
160
+ // type="password"
161
+ // style={input}
162
+ // value={password}
163
+ // onChange={e => setPassword(e.target.value)}
164
+ // required
165
+ // autoComplete="new-password"
166
+ // />
167
+
168
+ // <div style={{ marginTop: 12 }}>
169
+ // <button style={button} type="submit" disabled={loading}>
170
+ // {loading ? 'Creating...' : 'Create account'}
171
+ // </button>
172
+ // </div>
173
+
174
+ // <div style={{ display: 'flex', gap: 8, marginTop: 16 }}>
175
+ // <button
176
+ // type="button"
177
+ // onClick={handleGoogle}
178
+ // style={oauthButtonGoogle}
179
+ // >
180
+ // Continue with Google
181
+ // </button>
182
+
183
+ // <button
184
+ // type="button"
185
+ // onClick={handleGithub}
186
+ // style={oauthButtonGithub}
187
+ // >
188
+ // Continue with GitHub
189
+ // </button>
190
+ // </div>
191
+
192
+ // {error && <div style={errorBox}>{error}</div>}
193
+ // {message && <div style={successBox}>{message}</div>}
194
+ // </form>
195
+ // </div>
196
+ // </div>
197
+ // )
198
+ // }
199
+
200
+ // const overlay = {
201
+ // position: 'fixed',
202
+ // inset: 0,
203
+ // background: 'rgba(0,0,0,0.5)',
204
+ // display: 'flex',
205
+ // justifyContent: 'center',
206
+ // alignItems: 'center',
207
+ // padding: 20,
208
+ // zIndex: 9999
209
+ // }
210
+ // const modal = {
211
+ // width: '100%',
212
+ // maxWidth: 420,
213
+ // background: '#0f1724',
214
+ // borderRadius: 12,
215
+ // padding: 24,
216
+ // color: '#fff'
217
+ // }
218
+ // const title = { margin: 0, fontSize: 20, fontWeight: 600 }
219
+ // const label = { marginTop: 10, display: 'block', fontSize: 13 }
220
+ // const input = {
221
+ // width: '100%',
222
+ // padding: '10px 12px',
223
+ // borderRadius: 8,
224
+ // background: '#0b1220',
225
+ // color: '#fff',
226
+ // border: '1px solid #1e293b',
227
+ // marginTop: 6
228
+ // }
229
+ // const button = {
230
+ // marginTop: 15,
231
+ // width: '100%',
232
+ // padding: '10px 12px',
233
+ // borderRadius: 8,
234
+ // background: '#2563eb',
235
+ // border: 'none',
236
+ // color: '#fff',
237
+ // fontWeight: 600,
238
+ // cursor: 'pointer'
239
+ // }
240
+ // const oauthButtonGoogle = {
241
+ // flex: 1,
242
+ // padding: '10px 12px',
243
+ // borderRadius: 8,
244
+ // background: '#db4437',
245
+ // color: '#fff',
246
+ // border: 'none',
247
+ // cursor: 'pointer'
248
+ // }
249
+ // const oauthButtonGithub = {
250
+ // flex: 1,
251
+ // padding: '10px 12px',
252
+ // borderRadius: 8,
253
+ // background: '#24292f',
254
+ // color: '#fff',
255
+ // border: 'none',
256
+ // cursor: 'pointer'
257
+ // }
258
+ // const errorBox = { marginTop: 10, color: '#ffb4b4', fontSize: 13 }
259
+ // const successBox = { marginTop: 10, color: '#bef264', fontSize: 13 }
260
+
2
261
  'use client'
3
- import React, { useState } from 'react'
262
+ import React, { useState, useRef, useEffect } from 'react'
263
+ import { ToastContainer, toast } from 'react-toastify'
264
+ import 'react-toastify/dist/ReactToastify.css'
4
265
  import { useAuth } from './provider.js'
5
266
 
6
267
  export default function SignUp() {
7
- const {
8
- publishableKey,
9
- baseUrl,
10
- redirect,
11
- redirectTo,
12
- user,
13
- loadingUser,
14
- fetchMe,
15
- setUser
16
- } = useAuth()
17
-
18
- const [name, setName] = useState('')
19
- const [email, setEmail] = useState('')
20
- const [password, setPassword] = useState('')
21
- const [loading, setLoading] = useState(false)
22
- const [error, setError] = useState(null)
23
- const [message, setMessage] = useState(null)
24
-
25
- if (loadingUser) return null
26
-
27
- if (user && redirect) {
28
- if (typeof redirectTo === 'function') redirectTo(redirect)
29
- else if (typeof window !== 'undefined') window.location.assign(redirect)
30
- return null
268
+ const {
269
+ publishableKey,
270
+ baseUrl,
271
+ redirect,
272
+ redirectTo,
273
+ user,
274
+ loadingUser,
275
+ fetchMe,
276
+ setUser
277
+ } = useAuth()
278
+
279
+ const [name, setName] = useState('')
280
+ const [email, setEmail] = useState('')
281
+ const [password, setPassword] = useState('')
282
+ const [loading, setLoading] = useState(false)
283
+ const [message, setMessage] = useState(null)
284
+ const redirectTimer = useRef(null)
285
+
286
+ useEffect(() => {
287
+ return () => {
288
+ if (redirectTimer.current) clearTimeout(redirectTimer.current)
289
+ }
290
+ }, [])
291
+
292
+ if (loadingUser) return null
293
+
294
+ if (user && redirect) {
295
+ if (typeof redirectTo === 'function') redirectTo(redirect)
296
+ else if (typeof window !== 'undefined') window.location.assign(redirect)
297
+ return null
298
+ }
299
+
300
+ async function submit(e) {
301
+ e.preventDefault()
302
+ if (loading) return
303
+ setMessage(null)
304
+ setLoading(true)
305
+
306
+ const url = `${(baseUrl || '').replace(/\/+$/, '')}/api/sdk/signup`
307
+
308
+ try {
309
+ const res = await fetch(url, {
310
+ method: 'POST',
311
+ credentials: 'include',
312
+ headers: {
313
+ 'Content-Type': 'application/json',
314
+ 'x-publishable-key': publishableKey || ''
315
+ },
316
+ body: JSON.stringify({ name, email, password })
317
+ })
318
+
319
+ const data = await res.json().catch(async () => {
320
+ const text = await res.text().catch(() => '')
321
+ return { _raw: text }
322
+ })
323
+ if (!res.ok) {
324
+ const errMsg = data?.error || data?._raw || `Signup failed (${res.status})`
325
+ throw new Error(errMsg)
31
326
  }
32
327
 
33
- async function submit(e) {
34
- e.preventDefault()
35
- setError(null)
36
- setMessage(null)
37
- setLoading(true)
38
-
39
- const url = `${(baseUrl || '').replace(/\/+$/, '')}/api/sdk/signup`
40
-
41
- try {
42
- const res = await fetch(url, {
43
- method: 'POST',
44
- credentials: 'include',
45
- headers: {
46
- 'Content-Type': 'application/json',
47
- 'x-publishable-key': publishableKey || ''
48
- },
49
- body: JSON.stringify({ name, email, password })
50
- })
51
-
52
- const data = await res.json().catch(() => ({}))
53
- if (!res.ok) throw new Error(data.error || 'Signup failed')
54
-
55
- if (data.user && typeof setUser === 'function') setUser(data.user)
56
- if (typeof fetchMe === 'function') await fetchMe()
57
-
58
- setMessage('Account created. Redirecting…')
59
-
60
- if (redirect) {
61
- setTimeout(() => {
62
- if (typeof redirectTo === 'function') redirectTo(redirect)
63
- else if (typeof window !== 'undefined') window.location.assign(redirect)
64
- }, 300)
65
- }
66
- } catch (err) {
67
- setError(err?.message ?? 'Network error')
68
- console.error('Signup error:', err)
69
- } finally {
70
- setLoading(false)
71
- }
328
+ if (data.user && typeof setUser === 'function') setUser(data.user)
329
+ if (typeof fetchMe === 'function') await fetchMe()
330
+
331
+ setMessage('Account created. Redirecting…')
332
+ toast.success('Account created. Redirecting…')
333
+
334
+ if (redirect) {
335
+ redirectTimer.current = setTimeout(() => {
336
+ if (typeof redirectTo === 'function') redirectTo(redirect)
337
+ else if (typeof window !== 'undefined') window.location.assign(redirect)
338
+ }, 300)
72
339
  }
340
+ } catch (err) {
341
+ const text = err?.message ?? 'Network error'
342
+ toast.error(text)
343
+ console.error('Signup error:', err)
344
+ } finally {
345
+ setLoading(false)
346
+ }
73
347
 
74
- async function startOAuthFlow(provider) {
75
- // prevent double clicks
76
- setError(null)
77
- setLoading(true)
78
348
 
79
- try {
80
- const rid =
81
- (typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function')
82
- ? crypto.randomUUID()
83
- : `${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}`
349
+ }
84
350
 
85
- // callback must match what your SDK start expects and what Google console allows
86
- const callbackUrl = encodeURIComponent(`${window.location.origin}/signup`)
351
+ async function startOAuthFlow(provider) {
352
+ if (loading) return
353
+ // prevent double clicks
354
+ setLoading(true)
87
355
 
88
- // build start URL (server returns { oauthUrl })
89
- const sdkBase = (typeof process !== 'undefined' && process.env && process.env.NEXT_PUBLIC_FLOWLINK_BASE_URL)
90
- || baseUrl || window.location.origin.replace(/\/+$/, '')
91
- const startUrl = `${sdkBase}/sdk/auth/start?rid=${rid}&source=${encodeURIComponent(provider)}&callbackUrl=${callbackUrl}`
356
+ try {
357
+ const rid =
358
+ (typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function')
359
+ ? crypto.randomUUID()
360
+ : `${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}`
92
361
 
93
- // ensure publishableKey exists
94
- if (!publishableKey) {
95
- throw new Error('Missing publishable key (client side). Set NEXT_PUBLIC_FLOWLINK_PUBLISHABLE_KEY or provide publishableKey in provider.')
96
- }
362
+ // callback must match what your SDK start expects and what Google console allows
363
+ const callbackUrl = encodeURIComponent(`${window.location.origin}/signup`)
97
364
 
98
- const res = await fetch(startUrl, {
99
- method: 'GET',
100
- headers: {
101
- 'x-publishable-key': publishableKey
102
- }
103
- })
365
+ // build start URL (server returns { oauthUrl })
366
+ const sdkBase = baseUrl || window.location.origin.replace(/\/+$/, '')
367
+ const startUrl = `${sdkBase}/sdk/auth/start?rid=${rid}&source=${encodeURIComponent(provider)}&callbackUrl=${callbackUrl}`
104
368
 
105
- const data = await res.json().catch(() => null)
106
- if (!res.ok) {
107
- throw new Error(data?.error || `OAuth start failed (${res.status})`)
108
- }
109
- if (!data?.oauthUrl) {
110
- throw new Error('SDK start did not return oauthUrl')
369
+ // ensure publishableKey exists
370
+ if (!publishableKey) {
371
+ throw new Error('Missing publishable key (client side). Set NEXT_PUBLIC_FLOWLINK_PUBLISHABLE_KEY or provide publishableKey in provider.')
372
+ }
373
+
374
+ const res = await fetch(startUrl, {
375
+ method: 'GET',
376
+ headers: {
377
+ 'x-publishable-key': publishableKey
111
378
  }
379
+ })
112
380
 
113
- // navigate to provider (Google/GitHub)
114
- window.location.href = data.oauthUrl
115
- } catch (err) {
116
- console.error('OAuth start error:', err)
117
- setError(err?.message || 'OAuth start failed')
118
- setLoading(false)
381
+ const data = await res.json().catch(() => null)
382
+ if (!res.ok) {
383
+ throw new Error(data?.error || `OAuth start failed (${res.status})`)
384
+ }
385
+ if (!data?.oauthUrl) {
386
+ throw new Error('SDK start did not return oauthUrl')
119
387
  }
388
+
389
+ // navigate to provider (Google/GitHub)
390
+ window.location.href = data.oauthUrl
391
+ } catch (err) {
392
+ console.error('OAuth start error:', err)
393
+ toast.error(err?.message || 'OAuth start failed')
394
+ setLoading(false)
395
+ }
396
+
397
+
120
398
  }
121
399
 
122
400
  const handleGoogle = (e) => {
123
- if (e && typeof e.preventDefault === 'function') e.preventDefault()
124
- startOAuthFlow('google')
401
+ if (e && typeof e.preventDefault === 'function') e.preventDefault()
402
+ startOAuthFlow('google')
125
403
  }
126
404
 
127
405
  const handleGithub = (e) => {
128
- if (e && typeof e.preventDefault === 'function') e.preventDefault()
129
- startOAuthFlow('github')
406
+ if (e && typeof e.preventDefault === 'function') e.preventDefault()
407
+ startOAuthFlow('github')
130
408
  }
131
- return (
132
- <div style={overlay}>
133
- <div style={modal}>
134
- <h2 style={title}>Create account</h2>
135
409
 
136
- <form onSubmit={submit}>
137
- <label style={label}>Full name</label>
410
+ return (
411
+ <div style={page}>
412
+ <ToastContainer position="top-right" autoClose={5000} />
413
+ <div style={card}>
414
+ <div style={cardInner}>
415
+ <div style={brand}>
416
+ <div style={brandRow}>
417
+ <div style={logoPlaceholder} aria-hidden />
418
+ <h1 style={brandTitle}>Create account</h1>
419
+ </div>
420
+ <div style={brandSub}>
421
+ <div style={brandLead}>Sign up to continue</div>
422
+ <div style={brandMuted}>Welcome! Create your account.</div>
423
+ </div>
424
+ </div>
425
+
426
+ <div style={oauthRow}>
427
+ <button
428
+ onClick={handleGoogle}
429
+ type="button"
430
+ style={{ ...oauthButton, ...oauthGoogle }}
431
+ disabled={loading}
432
+ aria-disabled={loading}
433
+ >
434
+ <svg width={18} style={{ marginRight: 10 }} viewBox="-3 0 262 262" xmlns="http://www.w3.org/2000/svg" fill="#000000" aria-hidden>
435
+ <path d="M255.878 133.451c0-10.734-.871-18.567-2.756-26.69H130.55v48.448h71.947c-1.45 12.04-9.283 30.172-26.69 42.356l-.244 1.622 38.755 30.023 2.685.268c24.659-22.774 38.875-56.282 38.875-96.027" fill="#4285F4"></path>
436
+ <path d="M130.55 261.1c35.248 0 64.839-11.605 86.453-31.622l-41.196-31.913c-11.024 7.688-25.82 13.055-45.257 13.055-34.523 0-63.824-22.773-74.269-54.25l-1.531.13-40.298 31.187-.527 1.465C35.393 231.798 79.49 261.1 130.55 261.1" fill="#34A853"></path>
437
+ <path d="M56.281 156.37c-2.756-8.123-4.351-16.827-4.351-25.82 0-8.994 1.595-17.697 4.206-25.82l-.073-1.73L15.26 71.312l-1.335.635C5.077 89.644 0 109.517 0 130.55s5.077 40.905 13.925 58.602l42.356-32.782" fill="#FBBC05"></path>
438
+ <path d="M130.55 50.479c24.514 0 41.05 10.589 50.479 19.438l36.844-35.974C195.245 12.91 165.798 0 130.55 0 79.49 0 35.393 29.301 13.925 71.947l42.211 32.783c10.59-31.477 39.891-54.251 74.414-54.251" fill="#EB4335"></path>
439
+ </svg>
440
+ <span>{loading ? 'Loading...' : 'Continue with Google'}</span>
441
+ </button>
442
+
443
+ <button
444
+ onClick={handleGithub}
445
+ type="button"
446
+ style={{ ...oauthButton, ...oauthGithub }}
447
+ disabled={loading}
448
+ aria-disabled={loading}
449
+ >
450
+ <svg width={18} style={{ marginRight: 10 }} xmlns="http://www.w3.org/2000/svg" fill="white" viewBox="0 0 20 20" aria-hidden>
451
+ <path fillRule="evenodd" d="M10 .333A9.911 9.911 0 0 0 6.866 19.65c.5.092.678-.215.678-.477 0-.237-.01-1.017-.014-1.845-2.757.6-3.338-1.169-3.338-1.169a2.627 2.627 0 0 0-1.1-1.451c-.9-.615.07-.6.07-.6a2.084 2.084 0 0 1 1.518 1.021 2.11 2.11 0 0 0 2.884.823c.044-.503.268-.973.63-1.325-2.2-.25-4.516-1.1-4.516-4.9A3.832 3.832 0 0 1 4.7 7.068a3.56 3.56 0 0 1 .095-2.623s.832-.266 2.726 1.016a9.409 9.409 0 0 1 4.962 0c1.89-1.282 2.717-1.016 2.717-1.016.366.83.402 1.768.1 2.623a3.827 3.827 0 0 1 1.02 2.659c0 3.807-2.319 4.644-4.525 4.889a2.366 2.366 0 0 1 .673 1.834c0 1.326-.012 2.394-.012 2.72 0 .263.18.572.681.475A9.911 9.911 0 0 0 10 .333Z" clipRule="evenodd" />
452
+ </svg>
453
+ <span>{loading ? 'Loading...' : 'Continue with GitHub'}</span>
454
+ </button>
455
+ </div>
456
+
457
+ <div style={dividerRow}>
458
+ <div style={line} />
459
+ <div style={orText}>or</div>
460
+ <div style={line} />
461
+ </div>
462
+
463
+ <form onSubmit={submit} style={form}>
464
+ <label style={label} htmlFor="name">
465
+ <span style={labelText}>Name</span>
138
466
  <input
139
- name="name"
140
- style={input}
467
+ id="name"
468
+ type="text"
141
469
  value={name}
142
470
  onChange={e => setName(e.target.value)}
471
+ placeholder="Your name"
472
+ style={input}
143
473
  autoComplete="name"
144
474
  />
475
+ </label>
145
476
 
146
- <label style={label}>Email</label>
477
+ <label style={label} htmlFor="email">
478
+ <span style={labelText}>Email address</span>
147
479
  <input
148
- name="email"
480
+ id="email"
149
481
  type="email"
150
- style={input}
151
482
  value={email}
152
483
  onChange={e => setEmail(e.target.value)}
153
484
  required
485
+ placeholder="you@example.com"
486
+ style={input}
154
487
  autoComplete="email"
155
488
  />
489
+ </label>
156
490
 
157
- <label style={label}>Password</label>
491
+ <label style={label} htmlFor="password">
492
+ <span style={labelText}>Password</span>
158
493
  <input
159
- name="password"
494
+ id="password"
160
495
  type="password"
161
- style={input}
162
496
  value={password}
163
497
  onChange={e => setPassword(e.target.value)}
164
498
  required
499
+ placeholder="••••••••"
500
+ style={input}
165
501
  autoComplete="new-password"
166
502
  />
503
+ </label>
504
+
505
+ <button
506
+ type="submit"
507
+ style={submitButton}
508
+ disabled={loading}
509
+ aria-disabled={loading}
510
+ >
511
+ {loading ? 'Signing up...' : 'Sign Up'}
512
+ </button>
513
+ </form>
514
+ </div>
167
515
 
168
- <div style={{ marginTop: 12 }}>
169
- <button style={button} type="submit" disabled={loading}>
170
- {loading ? 'Creating...' : 'Create account'}
171
- </button>
172
- </div>
173
-
174
- <div style={{ display: 'flex', gap: 8, marginTop: 16 }}>
175
- <button
176
- type="button"
177
- onClick={handleGoogle}
178
- style={oauthButtonGoogle}
179
- >
180
- Continue with Google
181
- </button>
182
-
183
- <button
184
- type="button"
185
- onClick={handleGithub}
186
- style={oauthButtonGithub}
187
- >
188
- Continue with GitHub
189
- </button>
190
- </div>
191
-
192
- {error && <div style={errorBox}>{error}</div>}
193
- {message && <div style={successBox}>{message}</div>}
194
- </form>
516
+ <div style={below}>
517
+ <div style={belowRow}>
518
+ <span style={muted}>Already have an account? </span>
519
+ <a href="/sign-in" style={link}>Sign in</a>
520
+ </div>
521
+ <div style={dividerThin} />
522
+ <div style={secured}>
523
+ <div style={securedText}>Secured by auth</div>
195
524
  </div>
196
525
  </div>
197
- )
198
- }
199
-
200
- const overlay = {
201
- position: 'fixed',
202
- inset: 0,
203
- background: 'rgba(0,0,0,0.5)',
204
- display: 'flex',
205
- justifyContent: 'center',
206
- alignItems: 'center',
207
- padding: 20,
208
- zIndex: 9999
209
- }
210
- const modal = {
211
- width: '100%',
212
- maxWidth: 420,
213
- background: '#0f1724',
214
- borderRadius: 12,
215
- padding: 24,
216
- color: '#fff'
217
- }
218
- const title = { margin: 0, fontSize: 20, fontWeight: 600 }
219
- const label = { marginTop: 10, display: 'block', fontSize: 13 }
526
+ </div>
527
+ </div>
528
+
529
+
530
+ )
531
+ }
532
+
533
+ /* styles (JS objects) */
534
+ const page = {
535
+ position: 'fixed',
536
+ inset: 0,
537
+ display: 'flex',
538
+ alignItems: 'center',
539
+ justifyContent: 'center',
540
+ padding: 24,
541
+ background: 'linear-gradient(to bottom, #0b1220 0%, #071023 40%, #02040a 100%)',
542
+ color: '#f8e9d3',
543
+ minHeight: '100vh',
544
+ zIndex: 9999
545
+ }
546
+
547
+ const card = {
548
+ width: '100%',
549
+ maxWidth: 520,
550
+ borderRadius: 18,
551
+ background: 'linear-gradient(180deg, rgba(15,19,36,0.9) 0%, rgba(6,10,18,0.95) 100%)',
552
+ border: '1px solid rgba(148,163,184,0.06)',
553
+ boxShadow: '0 10px 30px rgba(2,6,23,0.6)',
554
+ overflow: 'hidden',
555
+ display: 'flex',
556
+ flexDirection: 'column',
557
+ gap: 12
558
+ }
559
+
560
+ const cardInner = {
561
+ padding: 26,
562
+ display: 'flex',
563
+ flexDirection: 'column',
564
+ gap: 18
565
+ }
566
+
567
+ const brand = {
568
+ display: 'flex',
569
+ flexDirection: 'column',
570
+ gap: 8
571
+ }
572
+
573
+ const brandRow = {
574
+ display: 'flex',
575
+ alignItems: 'center',
576
+ gap: 12
577
+ }
578
+
579
+ const logoPlaceholder = {
580
+ width: 36,
581
+ height: 36,
582
+ borderRadius: 999,
583
+ background: 'linear-gradient(135deg,#2b313a,#0f1724)'
584
+ }
585
+
586
+ const brandTitle = {
587
+ margin: 0,
588
+ fontSize: 20,
589
+ fontWeight: 600,
590
+ color: '#fff'
591
+ }
592
+
593
+ const brandSub = {
594
+ display: 'flex',
595
+ flexDirection: 'column',
596
+ gap: 4
597
+ }
598
+
599
+ const brandLead = {
600
+ fontSize: 15,
601
+ color: 'rgba(255,255,255,0.95)'
602
+ }
603
+
604
+ const brandMuted = {
605
+ fontSize: 13,
606
+ color: 'rgba(255,255,255,0.65)',
607
+ marginTop: 2
608
+ }
609
+
610
+ const oauthRow = {
611
+ display: 'flex',
612
+ gap: 10,
613
+ marginTop: 8
614
+ }
615
+
616
+ const oauthButton = {
617
+ flex: 1,
618
+ display: 'inline-flex',
619
+ alignItems: 'center',
620
+ justifyContent: 'center',
621
+ padding: '10px 12px',
622
+ borderRadius: 10,
623
+ border: '1px solid rgba(148,163,184,0.08)',
624
+ fontSize: 14,
625
+ cursor: 'pointer',
626
+ userSelect: 'none',
627
+ gap: 8
628
+ }
629
+
630
+ const oauthGoogle = {
631
+ background: 'rgba(255,255,255,0.06)',
632
+ color: '#fff'
633
+ }
634
+
635
+ const oauthGithub = {
636
+ background: 'rgba(255,255,255,0.03)',
637
+ color: '#fff'
638
+ }
639
+
640
+ const dividerRow = {
641
+ display: 'flex',
642
+ alignItems: 'center',
643
+ gap: 12,
644
+ marginTop: 14
645
+ }
646
+
647
+ const line = {
648
+ flex: 1,
649
+ height: 1,
650
+ background: 'rgba(148,163,184,0.06)'
651
+ }
652
+
653
+ const orText = {
654
+ fontSize: 13,
655
+ color: 'rgba(255,255,255,0.6)',
656
+ padding: '0 8px'
657
+ }
658
+
659
+ const form = {
660
+ display: 'flex',
661
+ flexDirection: 'column',
662
+ gap: 12,
663
+ marginTop: 6
664
+ }
665
+
666
+ const label = {
667
+ display: 'flex',
668
+ flexDirection: 'column',
669
+ gap: 6
670
+ }
671
+
672
+ const labelText = {
673
+ fontSize: 13,
674
+ color: 'rgba(255,255,255,0.66)'
675
+ }
676
+
220
677
  const input = {
221
- width: '100%',
222
- padding: '10px 12px',
223
- borderRadius: 8,
224
- background: '#0b1220',
225
- color: '#fff',
226
- border: '1px solid #1e293b',
227
- marginTop: 6
228
- }
229
- const button = {
230
- marginTop: 15,
231
- width: '100%',
232
- padding: '10px 12px',
233
- borderRadius: 8,
234
- background: '#2563eb',
235
- border: 'none',
236
- color: '#fff',
237
- fontWeight: 600,
238
- cursor: 'pointer'
239
- }
240
- const oauthButtonGoogle = {
241
- flex: 1,
242
- padding: '10px 12px',
243
- borderRadius: 8,
244
- background: '#db4437',
245
- color: '#fff',
246
- border: 'none',
247
- cursor: 'pointer'
248
- }
249
- const oauthButtonGithub = {
250
- flex: 1,
251
- padding: '10px 12px',
252
- borderRadius: 8,
253
- background: '#24292f',
254
- color: '#fff',
255
- border: 'none',
256
- cursor: 'pointer'
257
- }
258
- const errorBox = { marginTop: 10, color: '#ffb4b4', fontSize: 13 }
259
- const successBox = { marginTop: 10, color: '#bef264', fontSize: 13 }
678
+ width: '100%',
679
+ padding: '10px 12px',
680
+ borderRadius: 10,
681
+ background: 'rgba(11,18,32,0.5)',
682
+ color: '#f8e9d3',
683
+ border: '1px solid rgba(148,163,184,0.06)',
684
+ fontSize: 14,
685
+ outline: 'none',
686
+ boxSizing: 'border-box'
687
+ }
688
+
689
+ const submitButton = {
690
+ marginTop: 6,
691
+ width: '100%',
692
+ padding: '10px 12px',
693
+ borderRadius: 10,
694
+ background: 'linear-gradient(180deg,#2563eb,#60a5fa)',
695
+ border: 'none',
696
+ color: '#fff',
697
+ fontWeight: 600,
698
+ cursor: 'pointer',
699
+ fontSize: 15,
700
+ display: 'inline-flex',
701
+ alignItems: 'center',
702
+ justifyContent: 'center'
703
+ }
704
+
705
+ const below = {
706
+ padding: '12px 20px 20px 20px',
707
+ borderTop: '1px solid rgba(148,163,184,0.03)',
708
+ display: 'flex',
709
+ flexDirection: 'column',
710
+ gap: 10
711
+ }
712
+
713
+ const belowRow = {
714
+ textAlign: 'center',
715
+ display: 'flex',
716
+ justifyContent: 'center',
717
+ gap: 8,
718
+ alignItems: 'center'
719
+ }
720
+
721
+ const muted = {
722
+ color: 'rgba(255,255,255,0.65)',
723
+ fontSize: 13
724
+ }
725
+
726
+ const link = {
727
+ color: '#3b82f6',
728
+ textDecoration: 'none',
729
+ fontWeight: 600
730
+ }
731
+
732
+ const dividerThin = {
733
+ height: 1,
734
+ background: 'rgba(148,163,184,0.05)',
735
+ marginTop: 6,
736
+ marginBottom: 6
737
+ }
738
+
739
+ const secured = {
740
+ textAlign: 'center'
741
+ }
260
742
 
743
+ const securedText = {
744
+ color: 'rgba(255,255,255,0.9)',
745
+ fontSize: 13,
746
+ fontWeight: 600
747
+ }