spine-framework 0.3.40 → 0.3.41
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/.framework/src/App.tsx
CHANGED
|
@@ -3,6 +3,7 @@ import { Routes, Route, Navigate } from 'react-router-dom'
|
|
|
3
3
|
import { useAuth } from './contexts/AuthContext'
|
|
4
4
|
import { LoadingSpinner } from './components/ui/LoadingSpinner'
|
|
5
5
|
import { LoginPage } from './pages/auth/LoginPage'
|
|
6
|
+
import { RegisterPage } from './pages/auth/RegisterPage'
|
|
6
7
|
import { NotFoundPage } from './pages/NotFoundPage'
|
|
7
8
|
import { DashboardPage } from './pages/DashboardPage'
|
|
8
9
|
import { AppWrapper } from './components/AppWrapper'
|
|
@@ -33,6 +34,7 @@ function App() {
|
|
|
33
34
|
{!user ? (
|
|
34
35
|
<Routes>
|
|
35
36
|
<Route path="/login" element={<LoginPage />} />
|
|
37
|
+
<Route path="/register" element={<RegisterPage />} />
|
|
36
38
|
<Route path="*" element={<Navigate to="/login" replace />} />
|
|
37
39
|
</Routes>
|
|
38
40
|
) : (
|
|
@@ -76,8 +78,9 @@ function AuthenticatedRouter() {
|
|
|
76
78
|
<Route path="/" element={<Navigate to={defaultRedirect} replace />} />
|
|
77
79
|
<Route path="/dashboard" element={<DashboardPage />} />
|
|
78
80
|
|
|
79
|
-
{/* Login
|
|
81
|
+
{/* Login/register routes for authenticated users (redirects to qualified app) */}
|
|
80
82
|
<Route path="/login" element={<LoginPage />} />
|
|
83
|
+
<Route path="/register" element={<RegisterPage />} />
|
|
81
84
|
|
|
82
85
|
{/* ── Spine Framework namespace (reserved — no custom app may use this path) ── */}
|
|
83
86
|
<Route path="/spine-framework/admin/*" element={<AdminApp />} />
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module src/pages/auth/RegisterPage
|
|
3
|
+
* @audience installer
|
|
4
|
+
* @layer frontend-page
|
|
5
|
+
* @stability stable
|
|
6
|
+
*
|
|
7
|
+
* Self-registration page. Renders a centred card with email/password/name
|
|
8
|
+
* inputs. On submit, calls Supabase signUp and navigates to /login with
|
|
9
|
+
* a confirmation message.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import React, { useState } from 'react'
|
|
13
|
+
import { Link, useNavigate } from 'react-router-dom'
|
|
14
|
+
import { useAuth } from '../../contexts/AuthContext'
|
|
15
|
+
import { LoadingSpinner } from '../../components/ui/LoadingSpinner'
|
|
16
|
+
import { supabase } from '../../lib/supabase'
|
|
17
|
+
|
|
18
|
+
export function RegisterPage() {
|
|
19
|
+
const [email, setEmail] = useState('')
|
|
20
|
+
const [password, setPassword] = useState('')
|
|
21
|
+
const [fullName, setFullName] = useState('')
|
|
22
|
+
const [error, setError] = useState('')
|
|
23
|
+
const [success, setSuccess] = useState('')
|
|
24
|
+
const [isLoading, setIsLoading] = useState(false)
|
|
25
|
+
|
|
26
|
+
const navigate = useNavigate()
|
|
27
|
+
|
|
28
|
+
const handleSubmit = async (e: React.FormEvent) => {
|
|
29
|
+
e.preventDefault()
|
|
30
|
+
setError('')
|
|
31
|
+
setSuccess('')
|
|
32
|
+
setIsLoading(true)
|
|
33
|
+
|
|
34
|
+
try {
|
|
35
|
+
const { error: signUpError } = await supabase.auth.signUp({
|
|
36
|
+
email,
|
|
37
|
+
password,
|
|
38
|
+
options: {
|
|
39
|
+
data: {
|
|
40
|
+
full_name: fullName,
|
|
41
|
+
},
|
|
42
|
+
},
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
if (signUpError) {
|
|
46
|
+
setError(signUpError.message)
|
|
47
|
+
return
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
setSuccess('Account created! Check your email for a confirmation link.')
|
|
51
|
+
setTimeout(() => navigate('/login'), 3000)
|
|
52
|
+
} catch (err: any) {
|
|
53
|
+
setError(err.message || 'Registration failed')
|
|
54
|
+
} finally {
|
|
55
|
+
setIsLoading(false)
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return (
|
|
60
|
+
<div className="min-h-screen flex items-center justify-center bg-slate-50 py-12 px-4 sm:px-6 lg:px-8">
|
|
61
|
+
<div className="max-w-md w-full space-y-8">
|
|
62
|
+
<div>
|
|
63
|
+
<div className="mx-auto h-12 w-12 flex items-center justify-center rounded-full bg-slate-900">
|
|
64
|
+
<span className="text-white font-bold text-xl">S</span>
|
|
65
|
+
</div>
|
|
66
|
+
<h2 className="mt-6 text-center text-3xl font-extrabold text-slate-900">
|
|
67
|
+
Create your account
|
|
68
|
+
</h2>
|
|
69
|
+
<p className="mt-2 text-center text-sm text-slate-600">
|
|
70
|
+
Already have an account?{' '}
|
|
71
|
+
<Link to="/login" className="font-medium text-accent-blue hover:text-accent-blue-light">
|
|
72
|
+
Sign in
|
|
73
|
+
</Link>
|
|
74
|
+
</p>
|
|
75
|
+
</div>
|
|
76
|
+
|
|
77
|
+
<form className="mt-8 space-y-6" onSubmit={handleSubmit}>
|
|
78
|
+
{error && (
|
|
79
|
+
<div className="rounded-md bg-red-50 p-4">
|
|
80
|
+
<div className="text-sm text-red-800">{error}</div>
|
|
81
|
+
</div>
|
|
82
|
+
)}
|
|
83
|
+
|
|
84
|
+
{success && (
|
|
85
|
+
<div className="rounded-md bg-green-50 p-4">
|
|
86
|
+
<div className="text-sm text-green-800">{success}</div>
|
|
87
|
+
</div>
|
|
88
|
+
)}
|
|
89
|
+
|
|
90
|
+
<div className="space-y-4">
|
|
91
|
+
<div>
|
|
92
|
+
<label htmlFor="fullName" className="block text-sm font-medium text-slate-700">
|
|
93
|
+
Full name
|
|
94
|
+
</label>
|
|
95
|
+
<input
|
|
96
|
+
id="fullName"
|
|
97
|
+
name="fullName"
|
|
98
|
+
type="text"
|
|
99
|
+
required
|
|
100
|
+
value={fullName}
|
|
101
|
+
onChange={(e) => setFullName(e.target.value)}
|
|
102
|
+
className="mt-1 block w-full px-3 py-2 border border-slate-300 rounded-[5px] shadow-sm placeholder-slate-400 focus:outline-none focus:ring-accent-blue focus:border-accent-blue sm:text-sm"
|
|
103
|
+
placeholder="Enter your full name"
|
|
104
|
+
/>
|
|
105
|
+
</div>
|
|
106
|
+
|
|
107
|
+
<div>
|
|
108
|
+
<label htmlFor="email" className="block text-sm font-medium text-slate-700">
|
|
109
|
+
Email address
|
|
110
|
+
</label>
|
|
111
|
+
<input
|
|
112
|
+
id="email"
|
|
113
|
+
name="email"
|
|
114
|
+
type="email"
|
|
115
|
+
autoComplete="email"
|
|
116
|
+
required
|
|
117
|
+
value={email}
|
|
118
|
+
onChange={(e) => setEmail(e.target.value)}
|
|
119
|
+
className="mt-1 block w-full px-3 py-2 border border-slate-300 rounded-[5px] shadow-sm placeholder-slate-400 focus:outline-none focus:ring-accent-blue focus:border-accent-blue sm:text-sm"
|
|
120
|
+
placeholder="Enter your email"
|
|
121
|
+
/>
|
|
122
|
+
</div>
|
|
123
|
+
|
|
124
|
+
<div>
|
|
125
|
+
<label htmlFor="password" className="block text-sm font-medium text-slate-700">
|
|
126
|
+
Password
|
|
127
|
+
</label>
|
|
128
|
+
<input
|
|
129
|
+
id="password"
|
|
130
|
+
name="password"
|
|
131
|
+
type="password"
|
|
132
|
+
autoComplete="new-password"
|
|
133
|
+
required
|
|
134
|
+
minLength={6}
|
|
135
|
+
value={password}
|
|
136
|
+
onChange={(e) => setPassword(e.target.value)}
|
|
137
|
+
className="mt-1 block w-full px-3 py-2 border border-slate-300 rounded-[5px] shadow-sm placeholder-slate-400 focus:outline-none focus:ring-accent-blue focus:border-accent-blue sm:text-sm"
|
|
138
|
+
placeholder="Choose a password (min 6 characters)"
|
|
139
|
+
/>
|
|
140
|
+
</div>
|
|
141
|
+
</div>
|
|
142
|
+
|
|
143
|
+
<div>
|
|
144
|
+
<button
|
|
145
|
+
type="submit"
|
|
146
|
+
disabled={isLoading}
|
|
147
|
+
className="group relative w-full flex justify-center py-2 px-4 border border-transparent rounded-[5px] shadow-sm text-sm font-medium text-white bg-slate-900 hover:bg-slate-800 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-accent-blue disabled:opacity-50 disabled:cursor-not-allowed"
|
|
148
|
+
>
|
|
149
|
+
{isLoading ? (
|
|
150
|
+
<>
|
|
151
|
+
<LoadingSpinner size="sm" />
|
|
152
|
+
<span className="ml-2">Creating account...</span>
|
|
153
|
+
</>
|
|
154
|
+
) : (
|
|
155
|
+
'Create account'
|
|
156
|
+
)}
|
|
157
|
+
</button>
|
|
158
|
+
</div>
|
|
159
|
+
</form>
|
|
160
|
+
</div>
|
|
161
|
+
</div>
|
|
162
|
+
)
|
|
163
|
+
}
|