@stevederico/skateboard-ui 2.9.3 → 2.9.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/.claude/settings.local.json +3 -1
- package/CHANGELOG.md +11 -0
- package/components/AuthOverlay.jsx +18 -217
- package/package.json +1 -1
- package/views/SignInView.jsx +87 -64
- package/views/SignUpView.jsx +113 -90
package/CHANGELOG.md
CHANGED
|
@@ -1,12 +1,9 @@
|
|
|
1
|
-
import React, { useState, useEffect
|
|
2
|
-
import { Dialog, DialogContent, DialogHeader, DialogTitle
|
|
3
|
-
import { Input } from '../shadcn/ui/input.jsx';
|
|
4
|
-
import { Label } from '../shadcn/ui/label.jsx';
|
|
5
|
-
import { Button } from '../shadcn/ui/button.jsx';
|
|
6
|
-
import { Alert, AlertDescription } from '../shadcn/ui/alert.jsx';
|
|
1
|
+
import React, { useState, useEffect } from 'react';
|
|
2
|
+
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '../shadcn/ui/dialog.jsx';
|
|
7
3
|
import DynamicIcon from '../core/DynamicIcon.jsx';
|
|
8
4
|
import { getState } from '../core/Context.jsx';
|
|
9
|
-
import
|
|
5
|
+
import SignInView from '../views/SignInView.jsx';
|
|
6
|
+
import SignUpView from '../views/SignUpView.jsx';
|
|
10
7
|
|
|
11
8
|
/**
|
|
12
9
|
* Modal authentication overlay with sign-in and sign-up forms.
|
|
@@ -29,109 +26,25 @@ export default function AuthOverlay() {
|
|
|
29
26
|
const { visible } = state.authOverlay;
|
|
30
27
|
|
|
31
28
|
const [mode, setMode] = useState('signin');
|
|
32
|
-
const [email, setEmail] = useState('');
|
|
33
|
-
const [password, setPassword] = useState('');
|
|
34
|
-
const [name, setName] = useState('');
|
|
35
|
-
const [errorMessage, setErrorMessage] = useState('');
|
|
36
|
-
const [isSubmitting, setIsSubmitting] = useState(false);
|
|
37
|
-
const firstInputRef = useRef(null);
|
|
38
29
|
|
|
39
|
-
// Reset
|
|
30
|
+
// Reset mode when dialog opens
|
|
40
31
|
useEffect(() => {
|
|
41
32
|
if (visible) {
|
|
42
33
|
setMode('signin');
|
|
43
|
-
setEmail('');
|
|
44
|
-
setPassword('');
|
|
45
|
-
setName('');
|
|
46
|
-
setErrorMessage('');
|
|
47
|
-
setIsSubmitting(false);
|
|
48
34
|
}
|
|
49
35
|
}, [visible]);
|
|
50
36
|
|
|
51
|
-
// Focus first input when dialog opens or mode changes
|
|
52
|
-
useEffect(() => {
|
|
53
|
-
if (visible && firstInputRef.current) {
|
|
54
|
-
const t = setTimeout(() => firstInputRef.current?.focus(), 100);
|
|
55
|
-
return () => clearTimeout(t);
|
|
56
|
-
}
|
|
57
|
-
}, [visible, mode]);
|
|
58
|
-
|
|
59
37
|
function handleClose() {
|
|
60
38
|
dispatch({ type: 'HIDE_AUTH_OVERLAY' });
|
|
61
39
|
}
|
|
62
40
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
if (isSubmitting) return;
|
|
66
|
-
setIsSubmitting(true);
|
|
67
|
-
try {
|
|
68
|
-
const response = await fetch(`${getBackendURL()}/signin`, {
|
|
69
|
-
method: 'POST',
|
|
70
|
-
credentials: 'include',
|
|
71
|
-
headers: { 'Content-Type': 'application/json' },
|
|
72
|
-
body: JSON.stringify({ email, password })
|
|
73
|
-
});
|
|
74
|
-
|
|
75
|
-
if (response.ok) {
|
|
76
|
-
const data = await response.json();
|
|
77
|
-
dispatch({ type: 'SET_USER', payload: data });
|
|
78
|
-
dispatch({ type: 'AUTH_OVERLAY_SUCCESS' });
|
|
79
|
-
} else {
|
|
80
|
-
setErrorMessage('Invalid Credentials');
|
|
81
|
-
}
|
|
82
|
-
} catch (error) {
|
|
83
|
-
setErrorMessage('Server Error');
|
|
84
|
-
} finally {
|
|
85
|
-
setIsSubmitting(false);
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
async function handleSignUp(e) {
|
|
90
|
-
e.preventDefault();
|
|
91
|
-
if (isSubmitting) return;
|
|
92
|
-
if (password.length < 6) {
|
|
93
|
-
setErrorMessage('Password must be at least 6 characters');
|
|
94
|
-
return;
|
|
95
|
-
}
|
|
96
|
-
if (password.length > 72) {
|
|
97
|
-
setErrorMessage('Password must be 72 characters or less');
|
|
98
|
-
return;
|
|
99
|
-
}
|
|
100
|
-
setIsSubmitting(true);
|
|
101
|
-
try {
|
|
102
|
-
const response = await fetch(`${getBackendURL()}/signup`, {
|
|
103
|
-
method: 'POST',
|
|
104
|
-
credentials: 'include',
|
|
105
|
-
headers: { 'Content-Type': 'application/json' },
|
|
106
|
-
body: JSON.stringify({ email, password, name })
|
|
107
|
-
});
|
|
108
|
-
|
|
109
|
-
if (response.ok) {
|
|
110
|
-
const data = await response.json();
|
|
111
|
-
// Save CSRF token
|
|
112
|
-
const csrfCookie = document.cookie.split('; ').find(row => row.startsWith('csrf_token='));
|
|
113
|
-
const csrfToken = csrfCookie ? csrfCookie.split('=')[1] : data.csrfToken;
|
|
114
|
-
if (csrfToken) {
|
|
115
|
-
const appName = constants.appName || 'skateboard';
|
|
116
|
-
const csrfKey = `${appName.toLowerCase().replace(/\s+/g, '-')}_csrf`;
|
|
117
|
-
localStorage.setItem(csrfKey, csrfToken);
|
|
118
|
-
}
|
|
119
|
-
dispatch({ type: 'SET_USER', payload: data });
|
|
120
|
-
dispatch({ type: 'AUTH_OVERLAY_SUCCESS' });
|
|
121
|
-
} else {
|
|
122
|
-
setErrorMessage('Invalid Credentials');
|
|
123
|
-
}
|
|
124
|
-
} catch (error) {
|
|
125
|
-
setErrorMessage('Server Error');
|
|
126
|
-
} finally {
|
|
127
|
-
setIsSubmitting(false);
|
|
128
|
-
}
|
|
41
|
+
function handleSuccess() {
|
|
42
|
+
dispatch({ type: 'AUTH_OVERLAY_SUCCESS' });
|
|
129
43
|
}
|
|
130
44
|
|
|
131
45
|
return (
|
|
132
46
|
<Dialog open={visible} onOpenChange={(open) => { if (!open) handleClose(); }}>
|
|
133
47
|
<DialogContent>
|
|
134
|
-
{/* Branding */}
|
|
135
48
|
<DialogHeader className="items-center text-center">
|
|
136
49
|
<div className="flex items-center justify-center gap-3">
|
|
137
50
|
<div className="bg-app rounded-2xl flex aspect-square size-10 items-center justify-center">
|
|
@@ -139,133 +52,21 @@ export default function AuthOverlay() {
|
|
|
139
52
|
</div>
|
|
140
53
|
<span className="text-2xl font-bold">{constants.appName}</span>
|
|
141
54
|
</div>
|
|
142
|
-
<DialogTitle>{mode === 'signin' ? '
|
|
143
|
-
<DialogDescription>
|
|
144
|
-
{mode === 'signin' ? 'Sign in to your account' : 'Enter your details to get started'}
|
|
145
|
-
</DialogDescription>
|
|
55
|
+
<DialogTitle className="sr-only">{mode === 'signin' ? 'Sign In' : 'Sign Up'}</DialogTitle>
|
|
146
56
|
</DialogHeader>
|
|
147
57
|
|
|
148
|
-
{errorMessage && (
|
|
149
|
-
<Alert variant="destructive">
|
|
150
|
-
<AlertDescription className="text-center">{errorMessage}</AlertDescription>
|
|
151
|
-
</Alert>
|
|
152
|
-
)}
|
|
153
|
-
|
|
154
58
|
{mode === 'signin' ? (
|
|
155
|
-
<
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
id="overlay-email"
|
|
161
|
-
type="email"
|
|
162
|
-
placeholder="john@example.com"
|
|
163
|
-
required
|
|
164
|
-
value={email}
|
|
165
|
-
onChange={(e) => { setEmail(e.target.value); setErrorMessage(''); }}
|
|
166
|
-
/>
|
|
167
|
-
</div>
|
|
168
|
-
<div className="flex flex-col gap-2">
|
|
169
|
-
<Label htmlFor="overlay-password">Password</Label>
|
|
170
|
-
<Input
|
|
171
|
-
id="overlay-password"
|
|
172
|
-
type="password"
|
|
173
|
-
placeholder="••••••••"
|
|
174
|
-
required
|
|
175
|
-
value={password}
|
|
176
|
-
onChange={(e) => setPassword(e.target.value)}
|
|
177
|
-
/>
|
|
178
|
-
</div>
|
|
179
|
-
<Button
|
|
180
|
-
type="submit"
|
|
181
|
-
variant="gradient"
|
|
182
|
-
size="cta"
|
|
183
|
-
className="w-full"
|
|
184
|
-
disabled={isSubmitting}
|
|
185
|
-
>
|
|
186
|
-
<span className="relative z-20 flex items-center justify-center gap-2 drop-shadow-sm">
|
|
187
|
-
<DynamicIcon name="sparkles" size={16} color="currentColor" strokeWidth={2} className="animate-pulse" />
|
|
188
|
-
{isSubmitting ? 'Signing in...' : 'Sign In'}
|
|
189
|
-
</span>
|
|
190
|
-
</Button>
|
|
191
|
-
<div className="text-center text-sm">
|
|
192
|
-
<span className="text-muted-foreground">Don't have an account?</span>{' '}
|
|
193
|
-
<Button
|
|
194
|
-
variant="link"
|
|
195
|
-
className="p-0 h-auto"
|
|
196
|
-
onClick={() => { setMode('signup'); setErrorMessage(''); }}
|
|
197
|
-
>
|
|
198
|
-
Sign Up
|
|
199
|
-
</Button>
|
|
200
|
-
</div>
|
|
201
|
-
</form>
|
|
59
|
+
<SignInView
|
|
60
|
+
embedded
|
|
61
|
+
onSuccess={handleSuccess}
|
|
62
|
+
onSwitchMode={() => setMode('signup')}
|
|
63
|
+
/>
|
|
202
64
|
) : (
|
|
203
|
-
<
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
id="overlay-name"
|
|
209
|
-
placeholder="John Doe"
|
|
210
|
-
required
|
|
211
|
-
value={name}
|
|
212
|
-
onChange={(e) => { setName(e.target.value); setErrorMessage(''); }}
|
|
213
|
-
/>
|
|
214
|
-
</div>
|
|
215
|
-
<div className="flex flex-col gap-2">
|
|
216
|
-
<Label htmlFor="overlay-signup-email">Email</Label>
|
|
217
|
-
<Input
|
|
218
|
-
id="overlay-signup-email"
|
|
219
|
-
type="email"
|
|
220
|
-
placeholder="john@example.com"
|
|
221
|
-
required
|
|
222
|
-
value={email}
|
|
223
|
-
onChange={(e) => { setEmail(e.target.value); setErrorMessage(''); }}
|
|
224
|
-
/>
|
|
225
|
-
</div>
|
|
226
|
-
<div className="flex flex-col gap-2">
|
|
227
|
-
<Label htmlFor="overlay-signup-password">Password</Label>
|
|
228
|
-
<Input
|
|
229
|
-
id="overlay-signup-password"
|
|
230
|
-
type="password"
|
|
231
|
-
placeholder="••••••••"
|
|
232
|
-
required
|
|
233
|
-
minLength={6}
|
|
234
|
-
maxLength={72}
|
|
235
|
-
value={password}
|
|
236
|
-
onChange={(e) => { setPassword(e.target.value); setErrorMessage(''); }}
|
|
237
|
-
/>
|
|
238
|
-
<p className="text-xs text-muted-foreground">Minimum 6 characters</p>
|
|
239
|
-
</div>
|
|
240
|
-
<Button
|
|
241
|
-
type="submit"
|
|
242
|
-
variant="gradient"
|
|
243
|
-
size="cta"
|
|
244
|
-
className="w-full"
|
|
245
|
-
disabled={isSubmitting}
|
|
246
|
-
>
|
|
247
|
-
<span className="relative z-20 flex items-center justify-center gap-2 drop-shadow-sm">
|
|
248
|
-
<DynamicIcon name="sparkles" size={16} color="currentColor" strokeWidth={2} className="animate-pulse" />
|
|
249
|
-
{isSubmitting ? 'Signing up...' : 'Sign Up'}
|
|
250
|
-
</span>
|
|
251
|
-
</Button>
|
|
252
|
-
<div className="text-center text-sm">
|
|
253
|
-
<span className="text-muted-foreground">Already have an account?</span>{' '}
|
|
254
|
-
<Button
|
|
255
|
-
variant="link"
|
|
256
|
-
className="p-0 h-auto"
|
|
257
|
-
onClick={() => { setMode('signin'); setErrorMessage(''); }}
|
|
258
|
-
>
|
|
259
|
-
Sign In
|
|
260
|
-
</Button>
|
|
261
|
-
</div>
|
|
262
|
-
<div className="text-center text-xs text-muted-foreground">
|
|
263
|
-
By registering you agree to our{" "}
|
|
264
|
-
<a href="/terms" target="_blank" rel="noopener noreferrer" className="underline underline-offset-4 hover:text-foreground">Terms of Service</a>,{" "}
|
|
265
|
-
<a href="/eula" target="_blank" rel="noopener noreferrer" className="underline underline-offset-4 hover:text-foreground">EULA</a>,{" "}
|
|
266
|
-
<a href="/privacy" target="_blank" rel="noopener noreferrer" className="underline underline-offset-4 hover:text-foreground">Privacy Policy</a>
|
|
267
|
-
</div>
|
|
268
|
-
</form>
|
|
65
|
+
<SignUpView
|
|
66
|
+
embedded
|
|
67
|
+
onSuccess={handleSuccess}
|
|
68
|
+
onSwitchMode={() => setMode('signin')}
|
|
69
|
+
/>
|
|
269
70
|
)}
|
|
270
71
|
</DialogContent>
|
|
271
72
|
</Dialog>
|
package/package.json
CHANGED
package/views/SignInView.jsx
CHANGED
|
@@ -3,7 +3,7 @@ import { cn } from "../shadcn/lib/utils"
|
|
|
3
3
|
import { Button } from "../shadcn/ui/button"
|
|
4
4
|
import { Input } from "../shadcn/ui/input"
|
|
5
5
|
import { Label } from "../shadcn/ui/label"
|
|
6
|
-
import { Card, CardContent, CardHeader
|
|
6
|
+
import { Card, CardContent, CardHeader } from "../shadcn/ui/card"
|
|
7
7
|
import { Alert, AlertDescription } from "../shadcn/ui/alert"
|
|
8
8
|
import DynamicIcon from '../core/DynamicIcon';
|
|
9
9
|
import { useNavigate } from 'react-router-dom';
|
|
@@ -11,22 +11,31 @@ import { getState } from "../core/Context.jsx";
|
|
|
11
11
|
import { getBackendURL } from '../core/Utilities'
|
|
12
12
|
|
|
13
13
|
/**
|
|
14
|
-
*
|
|
14
|
+
* Sign-in form component.
|
|
15
15
|
*
|
|
16
|
-
* Authenticates via POST to /signin, dispatches SET_USER on success
|
|
17
|
-
*
|
|
16
|
+
* Authenticates via POST to /signin, dispatches SET_USER on success.
|
|
17
|
+
* In full-page mode, navigates to /app. In embedded mode, calls onSuccess.
|
|
18
18
|
*
|
|
19
19
|
* @param {Object} props
|
|
20
20
|
* @param {string} [props.className] - Additional CSS classes
|
|
21
|
-
* @
|
|
21
|
+
* @param {boolean} [props.embedded=false] - Render without page wrapper (for dialogs)
|
|
22
|
+
* @param {function} [props.onSuccess] - Called after successful sign-in (embedded mode)
|
|
23
|
+
* @param {function} [props.onSwitchMode] - Called when user clicks "Sign Up" (embedded mode)
|
|
24
|
+
* @returns {JSX.Element} Sign-in form
|
|
22
25
|
*
|
|
23
26
|
* @example
|
|
24
|
-
*
|
|
25
|
-
*
|
|
27
|
+
* // Full page
|
|
26
28
|
* <Route path="/signin" element={<SignInView />} />
|
|
29
|
+
*
|
|
30
|
+
* @example
|
|
31
|
+
* // Embedded in dialog
|
|
32
|
+
* <SignInView embedded onSuccess={handleSuccess} onSwitchMode={() => setMode('signup')} />
|
|
27
33
|
*/
|
|
28
34
|
export default function LoginForm({
|
|
29
35
|
className,
|
|
36
|
+
embedded = false,
|
|
37
|
+
onSuccess,
|
|
38
|
+
onSwitchMode,
|
|
30
39
|
...props
|
|
31
40
|
}) {
|
|
32
41
|
const { state, dispatch } = getState();
|
|
@@ -62,7 +71,11 @@ export default function LoginForm({
|
|
|
62
71
|
if (response.ok) {
|
|
63
72
|
const data = await response.json();
|
|
64
73
|
dispatch({ type: 'SET_USER', payload: data });
|
|
65
|
-
|
|
74
|
+
if (embedded && onSuccess) {
|
|
75
|
+
onSuccess();
|
|
76
|
+
} else {
|
|
77
|
+
navigate('/app');
|
|
78
|
+
}
|
|
66
79
|
} else {
|
|
67
80
|
setErrorMessage('Invalid Credentials');
|
|
68
81
|
}
|
|
@@ -73,6 +86,70 @@ export default function LoginForm({
|
|
|
73
86
|
}
|
|
74
87
|
}
|
|
75
88
|
|
|
89
|
+
const formContent = (
|
|
90
|
+
<>
|
|
91
|
+
{errorMessage && (
|
|
92
|
+
<Alert variant="destructive" className="mb-4">
|
|
93
|
+
<AlertDescription className="text-center">{errorMessage}</AlertDescription>
|
|
94
|
+
</Alert>
|
|
95
|
+
)}
|
|
96
|
+
|
|
97
|
+
<form onSubmit={signInClicked} className="flex flex-col gap-4">
|
|
98
|
+
<div className="flex flex-col gap-2">
|
|
99
|
+
<Label htmlFor="email">Email</Label>
|
|
100
|
+
<Input
|
|
101
|
+
ref={emailInputRef}
|
|
102
|
+
id="email"
|
|
103
|
+
type="email"
|
|
104
|
+
placeholder="john@example.com"
|
|
105
|
+
required
|
|
106
|
+
value={email}
|
|
107
|
+
onChange={(e) => {
|
|
108
|
+
setEmail(e.target.value);
|
|
109
|
+
setErrorMessage('');
|
|
110
|
+
}}
|
|
111
|
+
/>
|
|
112
|
+
</div>
|
|
113
|
+
|
|
114
|
+
<div className="flex flex-col gap-2">
|
|
115
|
+
<Label htmlFor="password">Password</Label>
|
|
116
|
+
<Input
|
|
117
|
+
id="password"
|
|
118
|
+
type="password"
|
|
119
|
+
placeholder="••••••••"
|
|
120
|
+
required
|
|
121
|
+
value={password}
|
|
122
|
+
onChange={(e) => setPassword(e.target.value)}
|
|
123
|
+
/>
|
|
124
|
+
</div>
|
|
125
|
+
|
|
126
|
+
<Button
|
|
127
|
+
type="submit"
|
|
128
|
+
variant="gradient"
|
|
129
|
+
size="cta"
|
|
130
|
+
className="w-full"
|
|
131
|
+
disabled={isSubmitting}
|
|
132
|
+
>
|
|
133
|
+
<span className="relative z-20 flex items-center justify-center gap-2 drop-shadow-sm">
|
|
134
|
+
<DynamicIcon name="sparkles" size={16} color="currentColor" strokeWidth={2} className="animate-pulse" />
|
|
135
|
+
{isSubmitting ? "Signing in..." : "Sign In"}
|
|
136
|
+
</span>
|
|
137
|
+
</Button>
|
|
138
|
+
|
|
139
|
+
<div className="text-center text-sm">
|
|
140
|
+
<span className="text-muted-foreground">Don't have an account?</span>{" "}
|
|
141
|
+
<Button variant="link" className="p-0 h-auto" onClick={() => embedded && onSwitchMode ? onSwitchMode() : navigate('/signup')}>
|
|
142
|
+
Sign Up
|
|
143
|
+
</Button>
|
|
144
|
+
</div>
|
|
145
|
+
</form>
|
|
146
|
+
</>
|
|
147
|
+
);
|
|
148
|
+
|
|
149
|
+
if (embedded) {
|
|
150
|
+
return formContent;
|
|
151
|
+
}
|
|
152
|
+
|
|
76
153
|
return (
|
|
77
154
|
<div className="fixed inset-0 bg-background overflow-auto">
|
|
78
155
|
<div className={cn("flex flex-col items-center justify-center min-h-screen p-4", className)} {...props}>
|
|
@@ -84,63 +161,9 @@ export default function LoginForm({
|
|
|
84
161
|
</div>
|
|
85
162
|
<span className="text-3xl font-bold">{constants.appName}</span>
|
|
86
163
|
</div>
|
|
87
|
-
|
|
164
|
+
</CardHeader>
|
|
88
165
|
<CardContent>
|
|
89
|
-
{
|
|
90
|
-
<Alert variant="destructive" className="mb-4">
|
|
91
|
-
<AlertDescription className="text-center">{errorMessage}</AlertDescription>
|
|
92
|
-
</Alert>
|
|
93
|
-
)}
|
|
94
|
-
|
|
95
|
-
<form onSubmit={signInClicked} className="flex flex-col gap-4">
|
|
96
|
-
<div className="flex flex-col gap-2">
|
|
97
|
-
<Label htmlFor="email">Email</Label>
|
|
98
|
-
<Input
|
|
99
|
-
ref={emailInputRef}
|
|
100
|
-
id="email"
|
|
101
|
-
type="email"
|
|
102
|
-
placeholder="john@example.com"
|
|
103
|
-
required
|
|
104
|
-
value={email}
|
|
105
|
-
onChange={(e) => {
|
|
106
|
-
setEmail(e.target.value);
|
|
107
|
-
setErrorMessage('');
|
|
108
|
-
}}
|
|
109
|
-
/>
|
|
110
|
-
</div>
|
|
111
|
-
|
|
112
|
-
<div className="flex flex-col gap-2">
|
|
113
|
-
<Label htmlFor="password">Password</Label>
|
|
114
|
-
<Input
|
|
115
|
-
id="password"
|
|
116
|
-
type="password"
|
|
117
|
-
placeholder="••••••••"
|
|
118
|
-
required
|
|
119
|
-
value={password}
|
|
120
|
-
onChange={(e) => setPassword(e.target.value)}
|
|
121
|
-
/>
|
|
122
|
-
</div>
|
|
123
|
-
|
|
124
|
-
<Button
|
|
125
|
-
type="submit"
|
|
126
|
-
variant="gradient"
|
|
127
|
-
size="cta"
|
|
128
|
-
className="w-full"
|
|
129
|
-
disabled={isSubmitting}
|
|
130
|
-
>
|
|
131
|
-
<span className="relative z-20 flex items-center justify-center gap-2 drop-shadow-sm">
|
|
132
|
-
<DynamicIcon name="sparkles" size={16} color="currentColor" strokeWidth={2} className="animate-pulse" />
|
|
133
|
-
{isSubmitting ? "Signing in..." : "Sign In"}
|
|
134
|
-
</span>
|
|
135
|
-
</Button>
|
|
136
|
-
|
|
137
|
-
<div className="text-center text-sm">
|
|
138
|
-
<span className="text-muted-foreground">Don't have an account?</span>{" "}
|
|
139
|
-
<Button variant="link" className="p-0 h-auto" onClick={() => navigate('/signup')}>
|
|
140
|
-
Sign Up
|
|
141
|
-
</Button>
|
|
142
|
-
</div>
|
|
143
|
-
</form>
|
|
166
|
+
{formContent}
|
|
144
167
|
</CardContent>
|
|
145
168
|
</Card>
|
|
146
169
|
</div>
|
package/views/SignUpView.jsx
CHANGED
|
@@ -3,7 +3,7 @@ import { cn } from "../shadcn/lib/utils"
|
|
|
3
3
|
import { Button } from "../shadcn/ui/button"
|
|
4
4
|
import { Input } from "../shadcn/ui/input"
|
|
5
5
|
import { Label } from "../shadcn/ui/label"
|
|
6
|
-
import { Card, CardContent, CardHeader
|
|
6
|
+
import { Card, CardContent, CardHeader } from "../shadcn/ui/card"
|
|
7
7
|
import { Alert, AlertDescription } from "../shadcn/ui/alert"
|
|
8
8
|
import DynamicIcon from '../core/DynamicIcon';
|
|
9
9
|
import { useNavigate } from 'react-router-dom';
|
|
@@ -11,23 +11,32 @@ import { getState } from "../core/Context.jsx";
|
|
|
11
11
|
import { getBackendURL } from '../core/Utilities'
|
|
12
12
|
|
|
13
13
|
/**
|
|
14
|
-
*
|
|
14
|
+
* Sign-up form component.
|
|
15
15
|
*
|
|
16
16
|
* Creates account via POST to /signup with name, email, and password.
|
|
17
|
-
* Validates password length (6-72 chars), dispatches SET_USER on success
|
|
18
|
-
*
|
|
17
|
+
* Validates password length (6-72 chars), dispatches SET_USER on success.
|
|
18
|
+
* In full-page mode, navigates to /app. In embedded mode, calls onSuccess.
|
|
19
19
|
*
|
|
20
20
|
* @param {Object} props
|
|
21
21
|
* @param {string} [props.className] - Additional CSS classes
|
|
22
|
-
* @
|
|
22
|
+
* @param {boolean} [props.embedded=false] - Render without page wrapper (for dialogs)
|
|
23
|
+
* @param {function} [props.onSuccess] - Called after successful sign-up (embedded mode)
|
|
24
|
+
* @param {function} [props.onSwitchMode] - Called when user clicks "Sign In" (embedded mode)
|
|
25
|
+
* @returns {JSX.Element} Sign-up form
|
|
23
26
|
*
|
|
24
27
|
* @example
|
|
25
|
-
*
|
|
26
|
-
*
|
|
28
|
+
* // Full page
|
|
27
29
|
* <Route path="/signup" element={<SignUpView />} />
|
|
30
|
+
*
|
|
31
|
+
* @example
|
|
32
|
+
* // Embedded in dialog
|
|
33
|
+
* <SignUpView embedded onSuccess={handleSuccess} onSwitchMode={() => setMode('signin')} />
|
|
28
34
|
*/
|
|
29
35
|
export default function LoginForm({
|
|
30
36
|
className,
|
|
37
|
+
embedded = false,
|
|
38
|
+
onSuccess,
|
|
39
|
+
onSwitchMode,
|
|
31
40
|
...props
|
|
32
41
|
}) {
|
|
33
42
|
const { state, dispatch } = getState();
|
|
@@ -76,7 +85,11 @@ export default function LoginForm({
|
|
|
76
85
|
localStorage.setItem(csrfKey, csrfToken);
|
|
77
86
|
}
|
|
78
87
|
dispatch({ type: 'SET_USER', payload: data });
|
|
79
|
-
|
|
88
|
+
if (embedded && onSuccess) {
|
|
89
|
+
onSuccess();
|
|
90
|
+
} else {
|
|
91
|
+
navigate('/app');
|
|
92
|
+
}
|
|
80
93
|
} else {
|
|
81
94
|
setErrorMessage('Invalid Credentials')
|
|
82
95
|
}
|
|
@@ -86,6 +99,96 @@ export default function LoginForm({
|
|
|
86
99
|
}
|
|
87
100
|
}
|
|
88
101
|
|
|
102
|
+
const formContent = (
|
|
103
|
+
<>
|
|
104
|
+
{errorMessage !== '' && (
|
|
105
|
+
<Alert variant="destructive" className="mb-4">
|
|
106
|
+
<AlertDescription className="text-center">{errorMessage}</AlertDescription>
|
|
107
|
+
</Alert>
|
|
108
|
+
)}
|
|
109
|
+
|
|
110
|
+
<form onSubmit={signUpClicked} className="flex flex-col gap-4">
|
|
111
|
+
<div className="flex flex-col gap-2">
|
|
112
|
+
<Label htmlFor="name">Name</Label>
|
|
113
|
+
<Input
|
|
114
|
+
ref={nameInputRef}
|
|
115
|
+
id="name"
|
|
116
|
+
placeholder="John Doe"
|
|
117
|
+
required
|
|
118
|
+
value={name}
|
|
119
|
+
onChange={(e) => {
|
|
120
|
+
setName(e.target.value);
|
|
121
|
+
setErrorMessage('');
|
|
122
|
+
}}
|
|
123
|
+
/>
|
|
124
|
+
</div>
|
|
125
|
+
|
|
126
|
+
<div className="flex flex-col gap-2">
|
|
127
|
+
<Label htmlFor="email">Email</Label>
|
|
128
|
+
<Input
|
|
129
|
+
id="email"
|
|
130
|
+
type="email"
|
|
131
|
+
placeholder="john@example.com"
|
|
132
|
+
required
|
|
133
|
+
value={email}
|
|
134
|
+
onChange={(e) => {
|
|
135
|
+
setEmail(e.target.value);
|
|
136
|
+
setErrorMessage('');
|
|
137
|
+
}}
|
|
138
|
+
/>
|
|
139
|
+
</div>
|
|
140
|
+
|
|
141
|
+
<div className="flex flex-col gap-2">
|
|
142
|
+
<Label htmlFor="password">Password</Label>
|
|
143
|
+
<Input
|
|
144
|
+
id="password"
|
|
145
|
+
type="password"
|
|
146
|
+
placeholder="••••••••"
|
|
147
|
+
required
|
|
148
|
+
minLength={6}
|
|
149
|
+
maxLength={72}
|
|
150
|
+
value={password}
|
|
151
|
+
onChange={(e) => {
|
|
152
|
+
setPassword(e.target.value);
|
|
153
|
+
setErrorMessage('');
|
|
154
|
+
}}
|
|
155
|
+
/>
|
|
156
|
+
<p className="text-xs text-muted-foreground">Minimum 6 characters</p>
|
|
157
|
+
</div>
|
|
158
|
+
|
|
159
|
+
<Button
|
|
160
|
+
type="submit"
|
|
161
|
+
variant="gradient"
|
|
162
|
+
size="cta"
|
|
163
|
+
className="w-full"
|
|
164
|
+
>
|
|
165
|
+
<span className="relative z-20 flex items-center justify-center gap-2 drop-shadow-sm">
|
|
166
|
+
<DynamicIcon name="sparkles" size={16} color="currentColor" strokeWidth={2} className="animate-pulse" />
|
|
167
|
+
Sign Up
|
|
168
|
+
</span>
|
|
169
|
+
</Button>
|
|
170
|
+
|
|
171
|
+
<div className="text-center text-sm">
|
|
172
|
+
<span className="text-muted-foreground">Already have an account?</span>{" "}
|
|
173
|
+
<Button variant="link" className="p-0 h-auto" onClick={(e) => { e.preventDefault(); embedded && onSwitchMode ? onSwitchMode() : navigate('/signin'); }}>
|
|
174
|
+
Sign In
|
|
175
|
+
</Button>
|
|
176
|
+
</div>
|
|
177
|
+
</form>
|
|
178
|
+
|
|
179
|
+
<div className="mt-4 text-center text-xs text-muted-foreground">
|
|
180
|
+
By registering you agree to our{" "}
|
|
181
|
+
<a href="/terms" className="underline underline-offset-4 hover:text-foreground">Terms of Service</a>,{" "}
|
|
182
|
+
<a href="/eula" className="underline underline-offset-4 hover:text-foreground">EULA</a>,{" "}
|
|
183
|
+
<a href="/privacy" className="underline underline-offset-4 hover:text-foreground">Privacy Policy</a>
|
|
184
|
+
</div>
|
|
185
|
+
</>
|
|
186
|
+
);
|
|
187
|
+
|
|
188
|
+
if (embedded) {
|
|
189
|
+
return formContent;
|
|
190
|
+
}
|
|
191
|
+
|
|
89
192
|
return (
|
|
90
193
|
<div className="fixed inset-0 bg-background overflow-auto">
|
|
91
194
|
<div className={cn("flex flex-col items-center justify-center min-h-screen p-4", className)} {...props}>
|
|
@@ -97,89 +200,9 @@ export default function LoginForm({
|
|
|
97
200
|
</div>
|
|
98
201
|
<span className="text-3xl font-bold">{constants.appName}</span>
|
|
99
202
|
</div>
|
|
100
|
-
|
|
203
|
+
</CardHeader>
|
|
101
204
|
<CardContent>
|
|
102
|
-
{
|
|
103
|
-
<Alert variant="destructive" className="mb-4">
|
|
104
|
-
<AlertDescription className="text-center">{errorMessage}</AlertDescription>
|
|
105
|
-
</Alert>
|
|
106
|
-
)}
|
|
107
|
-
|
|
108
|
-
<form onSubmit={signUpClicked} className="flex flex-col gap-4">
|
|
109
|
-
<div className="flex flex-col gap-2">
|
|
110
|
-
<Label htmlFor="name">Name</Label>
|
|
111
|
-
<Input
|
|
112
|
-
ref={nameInputRef}
|
|
113
|
-
id="name"
|
|
114
|
-
placeholder="John Doe"
|
|
115
|
-
required
|
|
116
|
-
value={name}
|
|
117
|
-
onChange={(e) => {
|
|
118
|
-
setName(e.target.value);
|
|
119
|
-
setErrorMessage('');
|
|
120
|
-
}}
|
|
121
|
-
/>
|
|
122
|
-
</div>
|
|
123
|
-
|
|
124
|
-
<div className="flex flex-col gap-2">
|
|
125
|
-
<Label htmlFor="email">Email</Label>
|
|
126
|
-
<Input
|
|
127
|
-
id="email"
|
|
128
|
-
type="email"
|
|
129
|
-
placeholder="john@example.com"
|
|
130
|
-
required
|
|
131
|
-
value={email}
|
|
132
|
-
onChange={(e) => {
|
|
133
|
-
setEmail(e.target.value);
|
|
134
|
-
setErrorMessage('');
|
|
135
|
-
}}
|
|
136
|
-
/>
|
|
137
|
-
</div>
|
|
138
|
-
|
|
139
|
-
<div className="flex flex-col gap-2">
|
|
140
|
-
<Label htmlFor="password">Password</Label>
|
|
141
|
-
<Input
|
|
142
|
-
id="password"
|
|
143
|
-
type="password"
|
|
144
|
-
placeholder="••••••••"
|
|
145
|
-
required
|
|
146
|
-
minLength={6}
|
|
147
|
-
maxLength={72}
|
|
148
|
-
value={password}
|
|
149
|
-
onChange={(e) => {
|
|
150
|
-
setPassword(e.target.value);
|
|
151
|
-
setErrorMessage('');
|
|
152
|
-
}}
|
|
153
|
-
/>
|
|
154
|
-
<p className="text-xs text-muted-foreground">Minimum 6 characters</p>
|
|
155
|
-
</div>
|
|
156
|
-
|
|
157
|
-
<Button
|
|
158
|
-
type="submit"
|
|
159
|
-
variant="gradient"
|
|
160
|
-
size="cta"
|
|
161
|
-
className="w-full"
|
|
162
|
-
>
|
|
163
|
-
<span className="relative z-20 flex items-center justify-center gap-2 drop-shadow-sm">
|
|
164
|
-
<DynamicIcon name="sparkles" size={16} color="currentColor" strokeWidth={2} className="animate-pulse" />
|
|
165
|
-
Sign Up
|
|
166
|
-
</span>
|
|
167
|
-
</Button>
|
|
168
|
-
|
|
169
|
-
<div className="text-center text-sm">
|
|
170
|
-
<span className="text-muted-foreground">Already have an account?</span>{" "}
|
|
171
|
-
<Button variant="link" className="p-0 h-auto" onClick={(e) => { e.preventDefault(); navigate('/signin'); }}>
|
|
172
|
-
Sign In
|
|
173
|
-
</Button>
|
|
174
|
-
</div>
|
|
175
|
-
</form>
|
|
176
|
-
|
|
177
|
-
<div className="mt-4 text-center text-xs text-muted-foreground">
|
|
178
|
-
By registering you agree to our{" "}
|
|
179
|
-
<a href="/terms" className="underline underline-offset-4 hover:text-foreground">Terms of Service</a>,{" "}
|
|
180
|
-
<a href="/eula" className="underline underline-offset-4 hover:text-foreground">EULA</a>,{" "}
|
|
181
|
-
<a href="/privacy" className="underline underline-offset-4 hover:text-foreground">Privacy Policy</a>
|
|
182
|
-
</div>
|
|
205
|
+
{formContent}
|
|
183
206
|
</CardContent>
|
|
184
207
|
</Card>
|
|
185
208
|
</div>
|