@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,323 @@
1
+ /**
2
+ * @pixygon/auth - Login Form Component
3
+ * Branded login form for Pixygon Account
4
+ */
5
+
6
+ import { useState, useCallback, type FormEvent } from 'react';
7
+ import { useAuth } from '../hooks';
8
+ import type { LoginFormProps, AuthError } from '../types';
9
+
10
+ export function LoginForm({
11
+ onSuccess,
12
+ onError,
13
+ onNavigateRegister,
14
+ onNavigateForgotPassword,
15
+ showBranding = true,
16
+ className = '',
17
+ }: LoginFormProps) {
18
+ const { login, isLoading, error } = useAuth();
19
+
20
+ const [userName, setUserName] = useState('');
21
+ const [password, setPassword] = useState('');
22
+ const [localError, setLocalError] = useState<string | null>(null);
23
+
24
+ const handleSubmit = useCallback(
25
+ async (e: FormEvent) => {
26
+ e.preventDefault();
27
+ setLocalError(null);
28
+
29
+ if (!userName.trim()) {
30
+ setLocalError('Please enter your username or email');
31
+ return;
32
+ }
33
+
34
+ if (!password) {
35
+ setLocalError('Please enter your password');
36
+ return;
37
+ }
38
+
39
+ try {
40
+ const response = await login({ userName: userName.trim(), password });
41
+ onSuccess?.(response.user);
42
+ } catch (err) {
43
+ const authError = err as AuthError;
44
+ onError?.(authError);
45
+ }
46
+ },
47
+ [userName, password, login, onSuccess, onError]
48
+ );
49
+
50
+ const displayError = localError || error?.message;
51
+
52
+ return (
53
+ <div className={`pixygon-auth-container ${className}`}>
54
+ <style>{`
55
+ .pixygon-auth-container {
56
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
57
+ background: #0f0f0f;
58
+ color: #ffffff;
59
+ padding: 2rem;
60
+ border-radius: 1rem;
61
+ max-width: 400px;
62
+ width: 100%;
63
+ margin: 0 auto;
64
+ }
65
+
66
+ .pixygon-auth-header {
67
+ text-align: center;
68
+ margin-bottom: 2rem;
69
+ }
70
+
71
+ .pixygon-auth-logo {
72
+ width: 64px;
73
+ height: 64px;
74
+ margin: 0 auto 1rem;
75
+ display: block;
76
+ }
77
+
78
+ .pixygon-auth-title {
79
+ font-size: 1.5rem;
80
+ font-weight: 600;
81
+ margin: 0 0 0.5rem;
82
+ }
83
+
84
+ .pixygon-auth-subtitle {
85
+ font-size: 0.875rem;
86
+ color: #a3a3a3;
87
+ margin: 0;
88
+ }
89
+
90
+ .pixygon-auth-form {
91
+ display: flex;
92
+ flex-direction: column;
93
+ gap: 1rem;
94
+ }
95
+
96
+ .pixygon-auth-input-group {
97
+ display: flex;
98
+ flex-direction: column;
99
+ gap: 0.375rem;
100
+ }
101
+
102
+ .pixygon-auth-label {
103
+ font-size: 0.875rem;
104
+ font-weight: 500;
105
+ color: #a3a3a3;
106
+ }
107
+
108
+ .pixygon-auth-input {
109
+ background: #262626;
110
+ border: 1px solid #404040;
111
+ border-radius: 0.5rem;
112
+ padding: 0.75rem 1rem;
113
+ font-size: 1rem;
114
+ color: #ffffff;
115
+ outline: none;
116
+ transition: border-color 0.2s, box-shadow 0.2s;
117
+ }
118
+
119
+ .pixygon-auth-input:focus {
120
+ border-color: #6366f1;
121
+ box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.1);
122
+ }
123
+
124
+ .pixygon-auth-input.error {
125
+ border-color: #ef4444;
126
+ }
127
+
128
+ .pixygon-auth-button {
129
+ background: #6366f1;
130
+ color: white;
131
+ border: none;
132
+ border-radius: 0.5rem;
133
+ padding: 0.875rem 1.5rem;
134
+ font-size: 1rem;
135
+ font-weight: 600;
136
+ cursor: pointer;
137
+ transition: background 0.2s;
138
+ display: flex;
139
+ align-items: center;
140
+ justify-content: center;
141
+ gap: 0.5rem;
142
+ margin-top: 0.5rem;
143
+ }
144
+
145
+ .pixygon-auth-button:hover:not(:disabled) {
146
+ background: #4f46e5;
147
+ }
148
+
149
+ .pixygon-auth-button:disabled {
150
+ opacity: 0.6;
151
+ cursor: not-allowed;
152
+ }
153
+
154
+ .pixygon-auth-error {
155
+ background: rgba(239, 68, 68, 0.1);
156
+ border: 1px solid #ef4444;
157
+ border-radius: 0.5rem;
158
+ padding: 0.75rem 1rem;
159
+ color: #fca5a5;
160
+ font-size: 0.875rem;
161
+ }
162
+
163
+ .pixygon-auth-link {
164
+ color: #6366f1;
165
+ text-decoration: none;
166
+ font-size: 0.875rem;
167
+ cursor: pointer;
168
+ background: none;
169
+ border: none;
170
+ padding: 0;
171
+ font-family: inherit;
172
+ }
173
+
174
+ .pixygon-auth-link:hover {
175
+ color: #818cf8;
176
+ text-decoration: underline;
177
+ }
178
+
179
+ .pixygon-auth-footer {
180
+ text-align: center;
181
+ margin-top: 1.5rem;
182
+ font-size: 0.875rem;
183
+ color: #a3a3a3;
184
+ }
185
+
186
+ .pixygon-auth-branding {
187
+ display: flex;
188
+ align-items: center;
189
+ justify-content: center;
190
+ gap: 0.5rem;
191
+ margin-top: 1.5rem;
192
+ font-size: 0.75rem;
193
+ color: #737373;
194
+ }
195
+
196
+ .pixygon-auth-spinner {
197
+ width: 20px;
198
+ height: 20px;
199
+ border: 2px solid transparent;
200
+ border-top-color: currentColor;
201
+ border-radius: 50%;
202
+ animation: pixygon-spin 0.8s linear infinite;
203
+ }
204
+
205
+ @keyframes pixygon-spin {
206
+ to { transform: rotate(360deg); }
207
+ }
208
+
209
+ .pixygon-auth-forgot-password {
210
+ text-align: right;
211
+ margin-top: -0.5rem;
212
+ }
213
+ `}</style>
214
+
215
+ <div className="pixygon-auth-header">
216
+ <svg
217
+ className="pixygon-auth-logo"
218
+ viewBox="0 0 100 100"
219
+ fill="none"
220
+ xmlns="http://www.w3.org/2000/svg"
221
+ >
222
+ <circle cx="50" cy="50" r="45" fill="#6366f1" />
223
+ <path
224
+ d="M30 45L45 30L70 55L55 70L30 45Z"
225
+ fill="white"
226
+ fillOpacity="0.9"
227
+ />
228
+ <path
229
+ d="M35 55L50 70L45 75L30 60L35 55Z"
230
+ fill="white"
231
+ fillOpacity="0.7"
232
+ />
233
+ </svg>
234
+ <h1 className="pixygon-auth-title">Welcome back</h1>
235
+ <p className="pixygon-auth-subtitle">Sign in to your Pixygon Account</p>
236
+ </div>
237
+
238
+ <form className="pixygon-auth-form" onSubmit={handleSubmit}>
239
+ {displayError && (
240
+ <div className="pixygon-auth-error">{displayError}</div>
241
+ )}
242
+
243
+ <div className="pixygon-auth-input-group">
244
+ <label className="pixygon-auth-label" htmlFor="pixygon-login-username">
245
+ Username or Email
246
+ </label>
247
+ <input
248
+ id="pixygon-login-username"
249
+ className={`pixygon-auth-input ${displayError ? 'error' : ''}`}
250
+ type="text"
251
+ value={userName}
252
+ onChange={(e) => setUserName(e.target.value)}
253
+ placeholder="Enter your username or email"
254
+ autoComplete="username"
255
+ disabled={isLoading}
256
+ />
257
+ </div>
258
+
259
+ <div className="pixygon-auth-input-group">
260
+ <label className="pixygon-auth-label" htmlFor="pixygon-login-password">
261
+ Password
262
+ </label>
263
+ <input
264
+ id="pixygon-login-password"
265
+ className={`pixygon-auth-input ${displayError ? 'error' : ''}`}
266
+ type="password"
267
+ value={password}
268
+ onChange={(e) => setPassword(e.target.value)}
269
+ placeholder="Enter your password"
270
+ autoComplete="current-password"
271
+ disabled={isLoading}
272
+ />
273
+ </div>
274
+
275
+ {onNavigateForgotPassword && (
276
+ <div className="pixygon-auth-forgot-password">
277
+ <button
278
+ type="button"
279
+ className="pixygon-auth-link"
280
+ onClick={onNavigateForgotPassword}
281
+ >
282
+ Forgot password?
283
+ </button>
284
+ </div>
285
+ )}
286
+
287
+ <button
288
+ type="submit"
289
+ className="pixygon-auth-button"
290
+ disabled={isLoading}
291
+ >
292
+ {isLoading ? (
293
+ <>
294
+ <div className="pixygon-auth-spinner" />
295
+ Signing in...
296
+ </>
297
+ ) : (
298
+ 'Sign in'
299
+ )}
300
+ </button>
301
+ </form>
302
+
303
+ {onNavigateRegister && (
304
+ <div className="pixygon-auth-footer">
305
+ Don't have an account?{' '}
306
+ <button
307
+ type="button"
308
+ className="pixygon-auth-link"
309
+ onClick={onNavigateRegister}
310
+ >
311
+ Create one
312
+ </button>
313
+ </div>
314
+ )}
315
+
316
+ {showBranding && (
317
+ <div className="pixygon-auth-branding">
318
+ Secured by Pixygon Account
319
+ </div>
320
+ )}
321
+ </div>
322
+ );
323
+ }
@@ -0,0 +1,170 @@
1
+ /**
2
+ * @pixygon/auth - Pixygon Auth Component
3
+ * Unified auth component with all forms and mode switching
4
+ */
5
+
6
+ import { useState, useCallback, useEffect } from 'react';
7
+ import type { PixygonAuthProps, User, AuthError } from '../types';
8
+ import { LoginForm } from './LoginForm';
9
+ import { RegisterForm } from './RegisterForm';
10
+ import { VerifyForm } from './VerifyForm';
11
+ import { ForgotPasswordForm } from './ForgotPasswordForm';
12
+
13
+ type AuthMode = PixygonAuthProps['mode'];
14
+
15
+ export function PixygonAuth({
16
+ mode: initialMode,
17
+ onSuccess,
18
+ onError,
19
+ onModeChange,
20
+ userName: initialUserName,
21
+ showBranding = true,
22
+ theme = 'dark',
23
+ className = '',
24
+ }: PixygonAuthProps) {
25
+ const [mode, setMode] = useState<AuthMode>(initialMode);
26
+ const [userName, setUserName] = useState(initialUserName || '');
27
+
28
+ // Sync mode with prop changes
29
+ useEffect(() => {
30
+ setMode(initialMode);
31
+ }, [initialMode]);
32
+
33
+ const handleModeChange = useCallback(
34
+ (newMode: AuthMode) => {
35
+ setMode(newMode);
36
+ onModeChange?.(newMode);
37
+ },
38
+ [onModeChange]
39
+ );
40
+
41
+ const handleLoginSuccess = useCallback(
42
+ (user: User) => {
43
+ if (!user.isVerified) {
44
+ setUserName(user.userName);
45
+ handleModeChange('verify');
46
+ } else {
47
+ onSuccess?.(user);
48
+ }
49
+ },
50
+ [handleModeChange, onSuccess]
51
+ );
52
+
53
+ const handleRegisterSuccess = useCallback(
54
+ (user: User) => {
55
+ setUserName(user.userName);
56
+ handleModeChange('verify');
57
+ },
58
+ [handleModeChange]
59
+ );
60
+
61
+ const handleVerifySuccess = useCallback(
62
+ (user: User) => {
63
+ onSuccess?.(user);
64
+ },
65
+ [onSuccess]
66
+ );
67
+
68
+ const handleForgotPasswordSuccess = useCallback(() => {
69
+ // Stay on forgot password page with success message
70
+ // or navigate to login
71
+ }, []);
72
+
73
+ const handleError = useCallback(
74
+ (error: AuthError) => {
75
+ onError?.(error);
76
+ },
77
+ [onError]
78
+ );
79
+
80
+ const containerClassName = `pixygon-auth-wrapper ${theme === 'light' ? 'light' : ''} ${className}`;
81
+
82
+ return (
83
+ <div className={containerClassName}>
84
+ <style>{`
85
+ .pixygon-auth-wrapper {
86
+ width: 100%;
87
+ }
88
+
89
+ .pixygon-auth-wrapper.light .pixygon-auth-container {
90
+ background: #ffffff;
91
+ color: #0f0f0f;
92
+ }
93
+
94
+ .pixygon-auth-wrapper.light .pixygon-auth-input {
95
+ background: #f5f5f5;
96
+ border-color: #e5e5e5;
97
+ color: #0f0f0f;
98
+ }
99
+
100
+ .pixygon-auth-wrapper.light .pixygon-auth-input:focus {
101
+ border-color: #6366f1;
102
+ }
103
+
104
+ .pixygon-auth-wrapper.light .pixygon-auth-label {
105
+ color: #525252;
106
+ }
107
+
108
+ .pixygon-auth-wrapper.light .pixygon-auth-subtitle {
109
+ color: #525252;
110
+ }
111
+
112
+ .pixygon-auth-wrapper.light .pixygon-auth-footer {
113
+ color: #525252;
114
+ }
115
+
116
+ .pixygon-auth-wrapper.light .pixygon-auth-branding {
117
+ color: #a3a3a3;
118
+ }
119
+ `}</style>
120
+
121
+ {mode === 'login' && (
122
+ <LoginForm
123
+ onSuccess={handleLoginSuccess}
124
+ onError={handleError}
125
+ onNavigateRegister={() => handleModeChange('register')}
126
+ onNavigateForgotPassword={() => handleModeChange('forgot-password')}
127
+ showBranding={showBranding}
128
+ />
129
+ )}
130
+
131
+ {mode === 'register' && (
132
+ <RegisterForm
133
+ onSuccess={handleRegisterSuccess}
134
+ onError={handleError}
135
+ onNavigateLogin={() => handleModeChange('login')}
136
+ showBranding={showBranding}
137
+ />
138
+ )}
139
+
140
+ {mode === 'verify' && (
141
+ <VerifyForm
142
+ userName={userName}
143
+ onSuccess={handleVerifySuccess}
144
+ onError={handleError}
145
+ onNavigateLogin={() => handleModeChange('login')}
146
+ showBranding={showBranding}
147
+ />
148
+ )}
149
+
150
+ {mode === 'forgot-password' && (
151
+ <ForgotPasswordForm
152
+ onSuccess={handleForgotPasswordSuccess}
153
+ onError={handleError}
154
+ onNavigateLogin={() => handleModeChange('login')}
155
+ showBranding={showBranding}
156
+ />
157
+ )}
158
+
159
+ {mode === 'recover-password' && (
160
+ // Recovery form would go here - handled via email link typically
161
+ <ForgotPasswordForm
162
+ onSuccess={handleForgotPasswordSuccess}
163
+ onError={handleError}
164
+ onNavigateLogin={() => handleModeChange('login')}
165
+ showBranding={showBranding}
166
+ />
167
+ )}
168
+ </div>
169
+ );
170
+ }