@urbackend/react 0.1.1 → 0.2.1

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.
@@ -1,405 +0,0 @@
1
- import React, { useState, useEffect } from 'react';
2
- import { useAuth } from '../hooks';
3
- import { Toast } from './Toast';
4
-
5
- export interface UrAuthProps {
6
- providers?: ('google' | 'github')[];
7
- theme?: 'light' | 'dark'; // Dark mode not perfectly matched to image, but kept for API compat
8
- onSuccess?: () => void;
9
- }
10
-
11
- export const UrAuth: React.FC<UrAuthProps> = ({
12
- providers = ['google', 'github'],
13
- theme = 'light',
14
- onSuccess
15
- }) => {
16
- const { login, signUp, socialLogin, requestPasswordReset, resetPassword, isLoading, error, clearError } = useAuth();
17
- const [mode, setMode] = useState<'signin' | 'signup' | 'forgot' | 'reset'>('signin');
18
- const [email, setEmail] = useState('');
19
- const [password, setPassword] = useState('');
20
- const [otp, setOtp] = useState('');
21
- const [name, setName] = useState('');
22
- const [toast, setToast] = useState<{message: string, type: 'success' | 'error'} | null>(null);
23
-
24
- useEffect(() => {
25
- if (error) {
26
- setToast({ message: error, type: 'error' });
27
- }
28
- }, [error]);
29
-
30
- const handleSubmit = async (e: React.FormEvent) => {
31
- e.preventDefault();
32
- try {
33
- if (mode === 'signin') {
34
- await login({ email, password });
35
- setToast({ message: 'Welcome back!', type: 'success' });
36
- if (onSuccess) onSuccess();
37
- } else if (mode === 'signup') {
38
- await signUp({ email, password, name });
39
- // Auto-login after signup for convenience
40
- await login({ email, password });
41
- setToast({ message: 'Account created successfully!', type: 'success' });
42
- if (onSuccess) onSuccess();
43
- } else if (mode === 'forgot') {
44
- await requestPasswordReset({ email });
45
- setToast({ message: 'Reset code sent to your email', type: 'success' });
46
- setMode('reset');
47
- } else if (mode === 'reset') {
48
- await resetPassword({ email, otp, newPassword: password });
49
- setToast({ message: 'Password reset successfully', type: 'success' });
50
- setMode('signin');
51
- setPassword('');
52
- setOtp('');
53
- }
54
- } catch (err: any) {
55
- // Error is now handled and stored globally by useAuth hook, which triggers the useEffect toast
56
- }
57
- };
58
-
59
- const isDark = theme === 'dark';
60
- const bg = isDark ? '#1a1a1a' : '#ffffff';
61
- const text = isDark ? '#ffffff' : '#0f172a';
62
- const textMuted = isDark ? '#a1a1aa' : '#64748b';
63
- const border = isDark ? '#333' : '#e2e8f0';
64
- const inputBg = isDark ? '#2a2a2a' : '#ffffff';
65
-
66
- const styles = {
67
- wrapper: {
68
- width: '100%',
69
- maxWidth: '420px',
70
- margin: '0 auto',
71
- borderRadius: '0',
72
- background: bg,
73
- boxShadow: isDark ? '0 20px 40px rgba(0,0,0,0.5)' : '0 20px 40px rgba(0,0,0,0.06), 0 1px 3px rgba(0,0,0,0.05)',
74
- border: `1px solid ${border}`,
75
- overflow: 'hidden',
76
- fontFamily: 'system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif',
77
- color: text,
78
- },
79
- body: {
80
- padding: '32px 32px 24px 32px',
81
- },
82
- switcherContainer: {
83
- display: 'flex',
84
- alignItems: 'center',
85
- justifyContent: 'center',
86
- marginBottom: '32px'
87
- },
88
- switcher: {
89
- display: 'inline-flex',
90
- background: isDark ? '#2a2a2a' : '#f1f5f9',
91
- padding: '4px',
92
- borderRadius: '0',
93
- },
94
- switchBtn: (active: boolean) => ({
95
- display: 'flex',
96
- alignItems: 'center',
97
- gap: '6px',
98
- padding: '8px 20px',
99
- borderRadius: '0',
100
- fontSize: '13px',
101
- fontWeight: 600,
102
- cursor: 'pointer',
103
- color: active ? text : textMuted,
104
- background: active ? (isDark ? '#444' : '#ffffff') : 'transparent',
105
- boxShadow: active ? (isDark ? '0 2px 4px rgba(0,0,0,0.2)' : '0 2px 8px rgba(0,0,0,0.05)') : 'none',
106
- border: 'none',
107
- transition: 'all 0.2s ease',
108
- }),
109
- field: {
110
- marginBottom: '20px',
111
- },
112
- labelRow: {
113
- display: 'flex',
114
- justifyContent: 'space-between',
115
- alignItems: 'center',
116
- marginBottom: '8px',
117
- },
118
- label: {
119
- fontSize: '13px',
120
- fontWeight: 600,
121
- color: isDark ? '#ddd' : '#334155',
122
- },
123
- forgotLink: {
124
- fontSize: '12px',
125
- fontWeight: 600,
126
- color: text,
127
- cursor: 'pointer',
128
- textDecoration: 'none',
129
- background: 'none',
130
- border: 'none',
131
- padding: 0,
132
- },
133
- input: {
134
- width: '100%',
135
- padding: '12px 16px',
136
- borderRadius: '0',
137
- border: `1px solid ${border}`,
138
- background: inputBg,
139
- color: text,
140
- fontSize: '14px',
141
- boxSizing: 'border-box' as const,
142
- outline: 'none',
143
- transition: 'border-color 0.2s ease',
144
- },
145
- primaryBtn: {
146
- width: '100%',
147
- padding: '14px',
148
- borderRadius: '0',
149
- background: 'linear-gradient(180deg, #2a2a2a 0%, #111111 100%)',
150
- color: '#ffffff',
151
- fontSize: '15px',
152
- fontWeight: 600,
153
- border: 'none',
154
- boxShadow: '0 4px 12px rgba(0,0,0,0.15)',
155
- cursor: 'pointer',
156
- marginTop: '8px',
157
- transition: 'transform 0.1s ease',
158
- },
159
- divider: {
160
- display: 'flex',
161
- alignItems: 'center',
162
- margin: '24px 0',
163
- color: '#94a3b8',
164
- fontSize: '11px',
165
- fontWeight: 600,
166
- letterSpacing: '1px',
167
- },
168
- dividerLine: {
169
- flex: 1,
170
- height: '1px',
171
- background: border,
172
- },
173
- dividerText: {
174
- padding: '0 12px',
175
- },
176
- socialBtn: {
177
- width: '100%',
178
- padding: '12px',
179
- borderRadius: '0',
180
- border: `1px solid ${border}`,
181
- background: isDark ? '#2a2a2a' : '#ffffff',
182
- color: text,
183
- fontSize: '14px',
184
- fontWeight: 600,
185
- display: 'flex',
186
- alignItems: 'center',
187
- justifyContent: 'center',
188
- gap: '10px',
189
- marginBottom: '12px',
190
- cursor: 'pointer',
191
- boxShadow: isDark ? 'none' : '0 1px 2px rgba(0,0,0,0.02)',
192
- transition: 'background 0.2s ease',
193
- },
194
- footer: {
195
- background: isDark ? '#222' : '#f8fafc',
196
- padding: '24px',
197
- textAlign: 'center' as const,
198
- borderTop: `1px solid ${border}`,
199
- fontSize: '13px',
200
- color: textMuted,
201
- },
202
- footerLink: {
203
- color: text,
204
- fontWeight: 600,
205
- textDecoration: 'underline',
206
- cursor: 'pointer',
207
- marginLeft: '4px',
208
- background: 'none',
209
- border: 'none',
210
- padding: 0,
211
- }
212
- };
213
-
214
- const GoogleIcon = () => (
215
- <svg width="18" height="18" viewBox="0 0 24 24" fill="none">
216
- <path d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z" fill="#4285F4"/>
217
- <path d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z" fill="#34A853"/>
218
- <path d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z" fill="#FBBC05"/>
219
- <path d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z" fill="#EA4335"/>
220
- </svg>
221
- );
222
-
223
- const GithubIcon = () => (
224
- <svg width="18" height="18" viewBox="0 0 24 24" fill={isDark ? '#fff' : '#000'}>
225
- <path d="M12 2C6.477 2 2 6.484 2 12.017c0 4.425 2.865 8.18 6.839 9.504.5.092.682-.217.682-.483 0-.237-.008-.868-.013-1.703-2.782.605-3.369-1.343-3.369-1.343-.454-1.158-1.11-1.466-1.11-1.466-.908-.62.069-.608.069-.608 1.003.07 1.531 1.032 1.531 1.032.892 1.53 2.341 1.088 2.91.832.092-.647.35-1.088.636-1.338-2.22-.253-4.555-1.113-4.555-4.951 0-1.093.39-1.988 1.029-2.688-.103-.253-.446-1.272.098-2.65 0 0 .84-.27 2.75 1.026A9.564 9.564 0 0112 6.844c.85.004 1.705.115 2.504.337 1.909-1.296 2.747-1.027 2.747-1.027.546 1.379.202 2.398.1 2.651.64.7 1.028 1.595 1.028 2.688 0 3.848-2.339 4.695-4.566 4.943.359.309.678.92.678 1.855 0 1.338-.012 2.419-.012 2.747 0 .268.18.58.688.482A10.019 10.019 0 0022 12.017C22 6.484 17.522 2 12 2z"/>
226
- </svg>
227
- );
228
-
229
- return (
230
- <div style={styles.wrapper}>
231
- {toast && (
232
- <Toast
233
- message={toast.message}
234
- type={toast.type}
235
- isDark={isDark}
236
- onClose={() => {
237
- setToast(null);
238
- if (toast.type === 'error') clearError();
239
- }}
240
- />
241
- )}
242
-
243
- <div style={styles.body}>
244
- {(mode === 'signin' || mode === 'signup') && (
245
- <div style={styles.switcherContainer}>
246
- <div style={styles.switcher}>
247
- <button
248
- type="button"
249
- style={styles.switchBtn(mode === 'signin')}
250
- onClick={() => { setMode('signin'); clearError(); }}
251
- >
252
- <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round"><path d="M15 3h4a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2h-4"/><polyline points="10 17 15 12 10 7"/><line x1="15" y1="12" x2="3" y2="12"/></svg>
253
- Login
254
- </button>
255
- <button
256
- type="button"
257
- style={styles.switchBtn(mode === 'signup')}
258
- onClick={() => { setMode('signup'); clearError(); }}
259
- >
260
- <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round"><path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><line x1="19" y1="8" x2="19" y2="14"/><line x1="22" y1="11" x2="16" y2="11"/></svg>
261
- Sign Up
262
- </button>
263
- </div>
264
- </div>
265
- )}
266
-
267
- {(mode === 'forgot' || mode === 'reset') && (
268
- <div style={{ marginBottom: '24px', textAlign: 'center' }}>
269
- <h2 style={{ margin: '0 0 8px', fontSize: '20px', fontWeight: 700, color: text }}>
270
- {mode === 'forgot' ? 'Reset Password' : 'Enter Reset Code'}
271
- </h2>
272
- <p style={{ margin: 0, fontSize: '14px', color: textMuted }}>
273
- {mode === 'forgot' ? "Enter your email and we'll send a code" : `Enter the code sent to ${email}`}
274
- </p>
275
- </div>
276
- )}
277
-
278
- <form onSubmit={handleSubmit}>
279
- {mode === 'signup' && (
280
- <div style={styles.field}>
281
- <div style={styles.labelRow}>
282
- <label style={styles.label}>Full Name</label>
283
- </div>
284
- <input
285
- style={styles.input}
286
- type="text"
287
- placeholder="Enter your name"
288
- value={name}
289
- onChange={e => setName(e.target.value)}
290
- required
291
- />
292
- </div>
293
- )}
294
-
295
- <div style={styles.field}>
296
- <div style={styles.labelRow}>
297
- <label style={styles.label}>Email address</label>
298
- </div>
299
- <input
300
- style={styles.input}
301
- type="email"
302
- placeholder="Enter your email address"
303
- value={email}
304
- onChange={e => setEmail(e.target.value)}
305
- required
306
- readOnly={mode === 'reset'}
307
- />
308
- </div>
309
-
310
- {mode === 'reset' && (
311
- <div style={styles.field}>
312
- <div style={styles.labelRow}>
313
- <label style={styles.label}>6-digit OTP Code</label>
314
- </div>
315
- <input
316
- style={styles.input}
317
- type="text"
318
- placeholder="Enter reset code"
319
- value={otp}
320
- onChange={e => setOtp(e.target.value)}
321
- required
322
- />
323
- </div>
324
- )}
325
-
326
- {(mode === 'signin' || mode === 'signup' || mode === 'reset') && (
327
- <div style={styles.field}>
328
- <div style={styles.labelRow}>
329
- <label style={styles.label}>{mode === 'reset' ? 'New Password' : 'Password'}</label>
330
- {mode === 'signin' && (
331
- <button type="button" style={styles.forgotLink} onClick={() => { setMode('forgot'); clearError(); }}>
332
- Forgot password?
333
- </button>
334
- )}
335
- </div>
336
- <input
337
- style={styles.input}
338
- type="password"
339
- placeholder={mode === 'reset' ? "Enter new password" : "Enter your password"}
340
- value={password}
341
- onChange={e => setPassword(e.target.value)}
342
- required
343
- />
344
- </div>
345
- )}
346
-
347
- <button style={styles.primaryBtn} type="submit" disabled={isLoading}
348
- onMouseDown={e => e.currentTarget.style.transform = 'scale(0.98)'}
349
- onMouseUp={e => e.currentTarget.style.transform = 'scale(1)'}
350
- onMouseLeave={e => e.currentTarget.style.transform = 'scale(1)'}
351
- >
352
- {isLoading
353
- ? 'Processing...'
354
- : (mode === 'signin' ? 'Log In'
355
- : mode === 'signup' ? 'Create Account'
356
- : mode === 'forgot' ? 'Send Reset Code'
357
- : 'Reset Password')
358
- }
359
- </button>
360
- </form>
361
-
362
- {(mode === 'signin' || mode === 'signup') && providers && providers.length > 0 && (
363
- <>
364
- <div style={styles.divider}>
365
- <div style={styles.dividerLine} />
366
- <span style={styles.dividerText}>OR</span>
367
- <div style={styles.dividerLine} />
368
- </div>
369
-
370
- <div>
371
- {providers.includes('google') && (
372
- <button style={styles.socialBtn} onClick={() => socialLogin('google')} type="button">
373
- <GoogleIcon />
374
- Continue with Google
375
- </button>
376
- )}
377
- {providers.includes('github') && (
378
- <button style={styles.socialBtn} onClick={() => socialLogin('github')} type="button">
379
- <GithubIcon />
380
- Continue with GitHub
381
- </button>
382
- )}
383
- </div>
384
- </>
385
- )}
386
- </div>
387
-
388
- <div style={styles.footer}>
389
- {mode === 'signin' ? "Don't have an account yet?"
390
- : mode === 'signup' ? "Already have an account?"
391
- : "Remember your password?"}
392
- <button
393
- type="button"
394
- style={styles.footerLink}
395
- onClick={() => {
396
- setMode(mode === 'signin' ? 'signup' : 'signin');
397
- clearError();
398
- }}
399
- >
400
- {mode === 'signin' ? 'Sign up' : 'Log in'}
401
- </button>
402
- </div>
403
- </div>
404
- );
405
- };
@@ -1,207 +0,0 @@
1
- import React, { useState, useRef, useEffect } from 'react';
2
- import { useUser, useAuth } from '../hooks';
3
-
4
- export interface UrUserButtonProps {
5
- /**
6
- * Shape of the profile avatar. Defaults to 'square' as requested.
7
- */
8
- shape?: 'square' | 'circle';
9
- /**
10
- * Position of the button on the screen. Defaults to 'top-right'.
11
- * Use 'inline' if you want to place it within a normal flex/grid layout instead of absolute positioning.
12
- */
13
- position?: 'top-right' | 'top-left' | 'bottom-right' | 'bottom-left' | 'inline';
14
- /**
15
- * Called when "Profile" is clicked.
16
- */
17
- onProfileClick?: () => void;
18
- /**
19
- * Called when "Settings" is clicked.
20
- */
21
- onSettingsClick?: () => void;
22
- /**
23
- * Z-index for the fixed container. Defaults to 999.
24
- */
25
- zIndex?: number;
26
- }
27
-
28
- export const UrUserButton: React.FC<UrUserButtonProps> = ({
29
- shape = 'square',
30
- position = 'top-right',
31
- onProfileClick,
32
- onSettingsClick,
33
- zIndex = 999,
34
- }) => {
35
- const { user } = useUser();
36
- const { logout } = useAuth();
37
- const [isOpen, setIsOpen] = useState(false);
38
- const containerRef = useRef<HTMLDivElement>(null);
39
-
40
- useEffect(() => {
41
- const handleClickOutside = (event: MouseEvent) => {
42
- if (containerRef.current && !containerRef.current.contains(event.target as Node)) {
43
- setIsOpen(false);
44
- }
45
- };
46
- document.addEventListener('mousedown', handleClickOutside);
47
- return () => document.removeEventListener('mousedown', handleClickOutside);
48
- }, []);
49
-
50
- if (!user) return null; // Only render if logged in
51
-
52
- const borderRadius = shape === 'circle' ? '50%' : '0px';
53
- const isFixed = position !== 'inline';
54
-
55
- const positionStyles: React.CSSProperties = isFixed
56
- ? {
57
- position: 'fixed',
58
- zIndex,
59
- top: position.includes('top') ? '24px' : 'auto',
60
- bottom: position.includes('bottom') ? '24px' : 'auto',
61
- right: position.includes('right') ? '24px' : 'auto',
62
- left: position.includes('left') ? '24px' : 'auto',
63
- }
64
- : { position: 'relative' };
65
-
66
- const dropdownStyles: React.CSSProperties = {
67
- position: 'absolute',
68
- top: position.includes('top') || position === 'inline' ? 'calc(100% + 8px)' : 'auto',
69
- bottom: position.includes('bottom') ? 'calc(100% + 8px)' : 'auto',
70
- right: position.includes('right') || position === 'inline' ? '0' : 'auto',
71
- left: position.includes('left') ? '0' : 'auto',
72
- background: '#ffffff',
73
- border: '1px solid #e2e8f0',
74
- borderRadius: '0px',
75
- boxShadow: '0 10px 25px rgba(0,0,0,0.1)',
76
- width: '220px',
77
- display: isOpen ? 'block' : 'none',
78
- overflow: 'hidden',
79
- fontFamily: 'system-ui, -apple-system, sans-serif',
80
- };
81
-
82
- const getInitials = () => {
83
- return user.name?.[0]?.toUpperCase() || user.email?.[0]?.toUpperCase() || 'U';
84
- };
85
-
86
- return (
87
- <div ref={containerRef} style={positionStyles}>
88
- <button
89
- onClick={() => setIsOpen(!isOpen)}
90
- style={{
91
- width: '40px',
92
- height: '40px',
93
- padding: 0,
94
- border: '1px solid #e2e8f0',
95
- background: '#f8fafc',
96
- borderRadius,
97
- cursor: 'pointer',
98
- display: 'flex',
99
- alignItems: 'center',
100
- justifyContent: 'center',
101
- overflow: 'hidden',
102
- boxShadow: '0 2px 5px rgba(0,0,0,0.05)',
103
- transition: 'transform 0.1s ease',
104
- }}
105
- >
106
- {user.avatarUrl ? (
107
- <img src={user.avatarUrl as string} alt="User" style={{ width: '100%', height: '100%', objectFit: 'cover' }} />
108
- ) : (
109
- <span style={{ fontSize: '16px', fontWeight: 600, color: '#475569' }}>
110
- {getInitials()}
111
- </span>
112
- )}
113
- </button>
114
-
115
- <div style={dropdownStyles}>
116
- {/* User Info Header */}
117
- <div style={{ padding: '16px', borderBottom: '1px solid #e2e8f0', background: '#f8fafc' }}>
118
- <div style={{ fontSize: '14px', fontWeight: 600, color: '#0f172a', whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>
119
- {user.name || 'User'}
120
- </div>
121
- <div style={{ fontSize: '12px', color: '#64748b', whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis', marginTop: '2px' }}>
122
- {user.email}
123
- </div>
124
- </div>
125
-
126
- {/* Action List */}
127
- <div style={{ padding: '8px' }}>
128
- {onProfileClick && (
129
- <button
130
- onClick={() => {
131
- setIsOpen(false);
132
- onProfileClick();
133
- }}
134
- style={{
135
- width: '100%',
136
- textAlign: 'left',
137
- padding: '10px 12px',
138
- background: 'transparent',
139
- border: 'none',
140
- fontSize: '14px',
141
- color: '#334155',
142
- cursor: 'pointer',
143
- borderRadius: '0px',
144
- display: 'block',
145
- }}
146
- onMouseEnter={(e) => (e.currentTarget.style.background = '#f1f5f9')}
147
- onMouseLeave={(e) => (e.currentTarget.style.background = 'transparent')}
148
- >
149
- Profile
150
- </button>
151
- )}
152
-
153
- {onSettingsClick && (
154
- <button
155
- onClick={() => {
156
- setIsOpen(false);
157
- onSettingsClick();
158
- }}
159
- style={{
160
- width: '100%',
161
- textAlign: 'left',
162
- padding: '10px 12px',
163
- background: 'transparent',
164
- border: 'none',
165
- fontSize: '14px',
166
- color: '#334155',
167
- cursor: 'pointer',
168
- borderRadius: '0px',
169
- display: 'block',
170
- }}
171
- onMouseEnter={(e) => (e.currentTarget.style.background = '#f1f5f9')}
172
- onMouseLeave={(e) => (e.currentTarget.style.background = 'transparent')}
173
- >
174
- Settings
175
- </button>
176
- )}
177
-
178
- <div style={{ height: '1px', background: '#e2e8f0', margin: '4px 0' }} />
179
-
180
- <button
181
- onClick={() => {
182
- setIsOpen(false);
183
- logout();
184
- }}
185
- style={{
186
- width: '100%',
187
- textAlign: 'left',
188
- padding: '10px 12px',
189
- background: 'transparent',
190
- border: 'none',
191
- fontSize: '14px',
192
- color: '#ef4444',
193
- fontWeight: 500,
194
- cursor: 'pointer',
195
- borderRadius: '0px',
196
- display: 'block',
197
- }}
198
- onMouseEnter={(e) => (e.currentTarget.style.background = '#fef2f2')}
199
- onMouseLeave={(e) => (e.currentTarget.style.background = 'transparent')}
200
- >
201
- Logout
202
- </button>
203
- </div>
204
- </div>
205
- </div>
206
- );
207
- };
@@ -1,83 +0,0 @@
1
- import React, { useEffect } from 'react';
2
- import { useUser } from './hooks';
3
-
4
- export interface ProtectedRouteProps {
5
- children: React.ReactNode;
6
- redirectTo?: string;
7
- fallback?: React.ReactNode;
8
- onRedirect?: () => void;
9
- }
10
-
11
- /**
12
- * A wrapper component that requires the user to be authenticated.
13
- * If the user is not authenticated after initialization, they will be redirected,
14
- * or the fallback will be rendered (or nothing if fallback is not provided and no window redirect occurs).
15
- */
16
- export const ProtectedRoute: React.FC<ProtectedRouteProps> = ({
17
- children,
18
- redirectTo = '/login',
19
- fallback = null,
20
- onRedirect
21
- }) => {
22
- const { isAuthenticated, isInitializing } = useUser();
23
-
24
- useEffect(() => {
25
- if (!isInitializing && !isAuthenticated) {
26
- if (onRedirect) {
27
- onRedirect();
28
- } else if (typeof window !== 'undefined') {
29
- window.location.href = redirectTo;
30
- }
31
- }
32
- }, [isAuthenticated, isInitializing, redirectTo, onRedirect]);
33
-
34
- if (isInitializing) {
35
- return fallback;
36
- }
37
-
38
- if (!isAuthenticated) {
39
- return fallback;
40
- }
41
-
42
- return <>{children}</>;
43
- };
44
-
45
- export interface GuestRouteProps {
46
- children: React.ReactNode;
47
- redirectTo?: string;
48
- fallback?: React.ReactNode;
49
- onRedirect?: () => void;
50
- }
51
-
52
- /**
53
- * A wrapper component that requires the user to NOT be authenticated (e.g. for Login pages).
54
- * If the user IS authenticated, they will be redirected to the specified route.
55
- */
56
- export const GuestRoute: React.FC<GuestRouteProps> = ({
57
- children,
58
- redirectTo = '/dashboard',
59
- fallback = null,
60
- onRedirect
61
- }) => {
62
- const { isAuthenticated, isInitializing } = useUser();
63
-
64
- useEffect(() => {
65
- if (!isInitializing && isAuthenticated) {
66
- if (onRedirect) {
67
- onRedirect();
68
- } else if (typeof window !== 'undefined') {
69
- window.location.href = redirectTo;
70
- }
71
- }
72
- }, [isAuthenticated, isInitializing, redirectTo, onRedirect]);
73
-
74
- if (isInitializing) {
75
- return fallback;
76
- }
77
-
78
- if (isAuthenticated) {
79
- return fallback;
80
- }
81
-
82
- return <>{children}</>;
83
- };