@pixygon/auth 1.0.0

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.
@@ -0,0 +1,463 @@
1
+ /**
2
+ * @pixygon/auth - Register Form Component
3
+ * Branded registration form for Pixygon Account
4
+ */
5
+
6
+ import { useState, useCallback, type FormEvent } from 'react';
7
+ import { useAuth } from '../hooks';
8
+ import type { RegisterFormProps, AuthError } from '../types';
9
+
10
+ export function RegisterForm({
11
+ onSuccess,
12
+ onError,
13
+ onNavigateLogin,
14
+ showBranding = true,
15
+ className = '',
16
+ }: RegisterFormProps) {
17
+ const { register, isLoading, error } = useAuth();
18
+
19
+ const [userName, setUserName] = useState('');
20
+ const [email, setEmail] = useState('');
21
+ const [password, setPassword] = useState('');
22
+ const [confirmPassword, setConfirmPassword] = useState('');
23
+ const [localError, setLocalError] = useState<string | null>(null);
24
+
25
+ const validateForm = useCallback(() => {
26
+ if (!userName.trim()) {
27
+ setLocalError('Please enter a username');
28
+ return false;
29
+ }
30
+
31
+ if (userName.length < 3) {
32
+ setLocalError('Username must be at least 3 characters');
33
+ return false;
34
+ }
35
+
36
+ if (!/^[a-zA-Z0-9_-]+$/.test(userName)) {
37
+ setLocalError('Username can only contain letters, numbers, underscores, and hyphens');
38
+ return false;
39
+ }
40
+
41
+ if (!email.trim()) {
42
+ setLocalError('Please enter your email');
43
+ return false;
44
+ }
45
+
46
+ if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
47
+ setLocalError('Please enter a valid email address');
48
+ return false;
49
+ }
50
+
51
+ if (!password) {
52
+ setLocalError('Please enter a password');
53
+ return false;
54
+ }
55
+
56
+ if (password.length < 6) {
57
+ setLocalError('Password must be at least 6 characters');
58
+ return false;
59
+ }
60
+
61
+ if (password !== confirmPassword) {
62
+ setLocalError('Passwords do not match');
63
+ return false;
64
+ }
65
+
66
+ return true;
67
+ }, [userName, email, password, confirmPassword]);
68
+
69
+ const handleSubmit = useCallback(
70
+ async (e: FormEvent) => {
71
+ e.preventDefault();
72
+ setLocalError(null);
73
+
74
+ if (!validateForm()) return;
75
+
76
+ try {
77
+ const response = await register({
78
+ userName: userName.trim(),
79
+ email: email.trim().toLowerCase(),
80
+ password,
81
+ confirmPassword,
82
+ });
83
+ onSuccess?.(response.user);
84
+ } catch (err) {
85
+ const authError = err as AuthError;
86
+ onError?.(authError);
87
+ }
88
+ },
89
+ [userName, email, password, confirmPassword, register, validateForm, onSuccess, onError]
90
+ );
91
+
92
+ const displayError = localError || error?.message;
93
+
94
+ // Password strength indicator
95
+ const getPasswordStrength = (pwd: string): { level: number; label: string } => {
96
+ if (!pwd) return { level: 0, label: '' };
97
+ let strength = 0;
98
+ if (pwd.length >= 6) strength++;
99
+ if (pwd.length >= 10) strength++;
100
+ if (/[A-Z]/.test(pwd)) strength++;
101
+ if (/[0-9]/.test(pwd)) strength++;
102
+ if (/[^A-Za-z0-9]/.test(pwd)) strength++;
103
+
104
+ if (strength <= 2) return { level: 1, label: 'Weak' };
105
+ if (strength <= 3) return { level: 2, label: 'Fair' };
106
+ if (strength <= 4) return { level: 3, label: 'Good' };
107
+ return { level: 4, label: 'Strong' };
108
+ };
109
+
110
+ const passwordStrength = getPasswordStrength(password);
111
+
112
+ return (
113
+ <div className={`pixygon-auth-container ${className}`}>
114
+ <style>{`
115
+ .pixygon-auth-container {
116
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
117
+ background: #0f0f0f;
118
+ color: #ffffff;
119
+ padding: 2rem;
120
+ border-radius: 1rem;
121
+ max-width: 400px;
122
+ width: 100%;
123
+ margin: 0 auto;
124
+ }
125
+
126
+ .pixygon-auth-header {
127
+ text-align: center;
128
+ margin-bottom: 2rem;
129
+ }
130
+
131
+ .pixygon-auth-logo {
132
+ width: 64px;
133
+ height: 64px;
134
+ margin: 0 auto 1rem;
135
+ display: block;
136
+ }
137
+
138
+ .pixygon-auth-title {
139
+ font-size: 1.5rem;
140
+ font-weight: 600;
141
+ margin: 0 0 0.5rem;
142
+ }
143
+
144
+ .pixygon-auth-subtitle {
145
+ font-size: 0.875rem;
146
+ color: #a3a3a3;
147
+ margin: 0;
148
+ }
149
+
150
+ .pixygon-auth-form {
151
+ display: flex;
152
+ flex-direction: column;
153
+ gap: 1rem;
154
+ }
155
+
156
+ .pixygon-auth-input-group {
157
+ display: flex;
158
+ flex-direction: column;
159
+ gap: 0.375rem;
160
+ }
161
+
162
+ .pixygon-auth-label {
163
+ font-size: 0.875rem;
164
+ font-weight: 500;
165
+ color: #a3a3a3;
166
+ }
167
+
168
+ .pixygon-auth-input {
169
+ background: #262626;
170
+ border: 1px solid #404040;
171
+ border-radius: 0.5rem;
172
+ padding: 0.75rem 1rem;
173
+ font-size: 1rem;
174
+ color: #ffffff;
175
+ outline: none;
176
+ transition: border-color 0.2s, box-shadow 0.2s;
177
+ }
178
+
179
+ .pixygon-auth-input:focus {
180
+ border-color: #6366f1;
181
+ box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.1);
182
+ }
183
+
184
+ .pixygon-auth-input.error {
185
+ border-color: #ef4444;
186
+ }
187
+
188
+ .pixygon-auth-button {
189
+ background: #6366f1;
190
+ color: white;
191
+ border: none;
192
+ border-radius: 0.5rem;
193
+ padding: 0.875rem 1.5rem;
194
+ font-size: 1rem;
195
+ font-weight: 600;
196
+ cursor: pointer;
197
+ transition: background 0.2s;
198
+ display: flex;
199
+ align-items: center;
200
+ justify-content: center;
201
+ gap: 0.5rem;
202
+ margin-top: 0.5rem;
203
+ }
204
+
205
+ .pixygon-auth-button:hover:not(:disabled) {
206
+ background: #4f46e5;
207
+ }
208
+
209
+ .pixygon-auth-button:disabled {
210
+ opacity: 0.6;
211
+ cursor: not-allowed;
212
+ }
213
+
214
+ .pixygon-auth-error {
215
+ background: rgba(239, 68, 68, 0.1);
216
+ border: 1px solid #ef4444;
217
+ border-radius: 0.5rem;
218
+ padding: 0.75rem 1rem;
219
+ color: #fca5a5;
220
+ font-size: 0.875rem;
221
+ }
222
+
223
+ .pixygon-auth-link {
224
+ color: #6366f1;
225
+ text-decoration: none;
226
+ font-size: 0.875rem;
227
+ cursor: pointer;
228
+ background: none;
229
+ border: none;
230
+ padding: 0;
231
+ font-family: inherit;
232
+ }
233
+
234
+ .pixygon-auth-link:hover {
235
+ color: #818cf8;
236
+ text-decoration: underline;
237
+ }
238
+
239
+ .pixygon-auth-footer {
240
+ text-align: center;
241
+ margin-top: 1.5rem;
242
+ font-size: 0.875rem;
243
+ color: #a3a3a3;
244
+ }
245
+
246
+ .pixygon-auth-branding {
247
+ display: flex;
248
+ align-items: center;
249
+ justify-content: center;
250
+ gap: 0.5rem;
251
+ margin-top: 1.5rem;
252
+ font-size: 0.75rem;
253
+ color: #737373;
254
+ }
255
+
256
+ .pixygon-auth-spinner {
257
+ width: 20px;
258
+ height: 20px;
259
+ border: 2px solid transparent;
260
+ border-top-color: currentColor;
261
+ border-radius: 50%;
262
+ animation: pixygon-spin 0.8s linear infinite;
263
+ }
264
+
265
+ @keyframes pixygon-spin {
266
+ to { transform: rotate(360deg); }
267
+ }
268
+
269
+ .pixygon-auth-password-strength {
270
+ display: flex;
271
+ gap: 0.25rem;
272
+ margin-top: 0.5rem;
273
+ }
274
+
275
+ .pixygon-auth-password-strength-bar {
276
+ flex: 1;
277
+ height: 4px;
278
+ border-radius: 2px;
279
+ background: #404040;
280
+ transition: background 0.3s;
281
+ }
282
+
283
+ .pixygon-auth-password-strength-bar.active {
284
+ background: #6366f1;
285
+ }
286
+
287
+ .pixygon-auth-password-strength-bar.weak {
288
+ background: #ef4444;
289
+ }
290
+
291
+ .pixygon-auth-password-strength-bar.fair {
292
+ background: #f59e0b;
293
+ }
294
+
295
+ .pixygon-auth-password-strength-bar.good {
296
+ background: #22c55e;
297
+ }
298
+
299
+ .pixygon-auth-password-strength-bar.strong {
300
+ background: #10b981;
301
+ }
302
+
303
+ .pixygon-auth-password-label {
304
+ font-size: 0.75rem;
305
+ color: #737373;
306
+ margin-top: 0.25rem;
307
+ }
308
+ `}</style>
309
+
310
+ <div className="pixygon-auth-header">
311
+ <svg
312
+ className="pixygon-auth-logo"
313
+ viewBox="0 0 100 100"
314
+ fill="none"
315
+ xmlns="http://www.w3.org/2000/svg"
316
+ >
317
+ <circle cx="50" cy="50" r="45" fill="#6366f1" />
318
+ <path
319
+ d="M30 45L45 30L70 55L55 70L30 45Z"
320
+ fill="white"
321
+ fillOpacity="0.9"
322
+ />
323
+ <path
324
+ d="M35 55L50 70L45 75L30 60L35 55Z"
325
+ fill="white"
326
+ fillOpacity="0.7"
327
+ />
328
+ </svg>
329
+ <h1 className="pixygon-auth-title">Create Account</h1>
330
+ <p className="pixygon-auth-subtitle">Join Pixygon to access all apps</p>
331
+ </div>
332
+
333
+ <form className="pixygon-auth-form" onSubmit={handleSubmit}>
334
+ {displayError && (
335
+ <div className="pixygon-auth-error">{displayError}</div>
336
+ )}
337
+
338
+ <div className="pixygon-auth-input-group">
339
+ <label className="pixygon-auth-label" htmlFor="pixygon-register-username">
340
+ Username
341
+ </label>
342
+ <input
343
+ id="pixygon-register-username"
344
+ className={`pixygon-auth-input ${displayError ? 'error' : ''}`}
345
+ type="text"
346
+ value={userName}
347
+ onChange={(e) => setUserName(e.target.value)}
348
+ placeholder="Choose a username"
349
+ autoComplete="username"
350
+ disabled={isLoading}
351
+ />
352
+ </div>
353
+
354
+ <div className="pixygon-auth-input-group">
355
+ <label className="pixygon-auth-label" htmlFor="pixygon-register-email">
356
+ Email
357
+ </label>
358
+ <input
359
+ id="pixygon-register-email"
360
+ className={`pixygon-auth-input ${displayError ? 'error' : ''}`}
361
+ type="email"
362
+ value={email}
363
+ onChange={(e) => setEmail(e.target.value)}
364
+ placeholder="Enter your email"
365
+ autoComplete="email"
366
+ disabled={isLoading}
367
+ />
368
+ </div>
369
+
370
+ <div className="pixygon-auth-input-group">
371
+ <label className="pixygon-auth-label" htmlFor="pixygon-register-password">
372
+ Password
373
+ </label>
374
+ <input
375
+ id="pixygon-register-password"
376
+ className={`pixygon-auth-input ${displayError ? 'error' : ''}`}
377
+ type="password"
378
+ value={password}
379
+ onChange={(e) => setPassword(e.target.value)}
380
+ placeholder="Create a password"
381
+ autoComplete="new-password"
382
+ disabled={isLoading}
383
+ />
384
+ {password && (
385
+ <>
386
+ <div className="pixygon-auth-password-strength">
387
+ {[1, 2, 3, 4].map((level) => (
388
+ <div
389
+ key={level}
390
+ className={`pixygon-auth-password-strength-bar ${
391
+ level <= passwordStrength.level
392
+ ? passwordStrength.level === 1
393
+ ? 'weak'
394
+ : passwordStrength.level === 2
395
+ ? 'fair'
396
+ : passwordStrength.level === 3
397
+ ? 'good'
398
+ : 'strong'
399
+ : ''
400
+ }`}
401
+ />
402
+ ))}
403
+ </div>
404
+ <div className="pixygon-auth-password-label">
405
+ Password strength: {passwordStrength.label}
406
+ </div>
407
+ </>
408
+ )}
409
+ </div>
410
+
411
+ <div className="pixygon-auth-input-group">
412
+ <label className="pixygon-auth-label" htmlFor="pixygon-register-confirm">
413
+ Confirm Password
414
+ </label>
415
+ <input
416
+ id="pixygon-register-confirm"
417
+ className={`pixygon-auth-input ${displayError ? 'error' : ''}`}
418
+ type="password"
419
+ value={confirmPassword}
420
+ onChange={(e) => setConfirmPassword(e.target.value)}
421
+ placeholder="Confirm your password"
422
+ autoComplete="new-password"
423
+ disabled={isLoading}
424
+ />
425
+ </div>
426
+
427
+ <button
428
+ type="submit"
429
+ className="pixygon-auth-button"
430
+ disabled={isLoading}
431
+ >
432
+ {isLoading ? (
433
+ <>
434
+ <div className="pixygon-auth-spinner" />
435
+ Creating account...
436
+ </>
437
+ ) : (
438
+ 'Create Account'
439
+ )}
440
+ </button>
441
+ </form>
442
+
443
+ {onNavigateLogin && (
444
+ <div className="pixygon-auth-footer">
445
+ Already have an account?{' '}
446
+ <button
447
+ type="button"
448
+ className="pixygon-auth-link"
449
+ onClick={onNavigateLogin}
450
+ >
451
+ Sign in
452
+ </button>
453
+ </div>
454
+ )}
455
+
456
+ {showBranding && (
457
+ <div className="pixygon-auth-branding">
458
+ Secured by Pixygon Account
459
+ </div>
460
+ )}
461
+ </div>
462
+ );
463
+ }