@umituz/web-dashboard 2.0.8 → 2.1.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,296 @@
1
+ /**
2
+ * Register Form Component
3
+ *
4
+ * Configurable registration form with validation
5
+ */
6
+
7
+ import { useState } from "react";
8
+ import { useNavigate } from "react-router-dom";
9
+ import { Eye, EyeOff, Loader2 } from "lucide-react";
10
+ import { cn } from "@umituz/web-design-system/utils";
11
+ import { Button } from "@umituz/web-design-system/atoms";
12
+ import type { RegisterFormProps } from "../types/auth";
13
+ import { validateRegister, calculatePasswordStrength } from "../utils/auth";
14
+
15
+ export const RegisterForm = ({
16
+ config,
17
+ defaultData = {},
18
+ showTerms = true,
19
+ showLoginLink = true,
20
+ requirePasswordConfirm = true,
21
+ onRegisterSuccess,
22
+ onRegisterError,
23
+ }: RegisterFormProps) => {
24
+ const navigate = useNavigate();
25
+ const [name, setName] = useState(defaultData.name || "");
26
+ const [email, setEmail] = useState(defaultData.email || "");
27
+ const [password, setPassword] = useState(defaultData.password || "");
28
+ const [confirmPassword, setConfirmPassword] = useState("");
29
+ const [agreeToTerms, setAgreeToTerms] = useState(false);
30
+ const [showPassword, setShowPassword] = useState(false);
31
+ const [showConfirmPassword, setShowConfirmPassword] = useState(false);
32
+ const [isLoading, setIsLoading] = useState(false);
33
+ const [error, setError] = useState<string | null>(null);
34
+
35
+ const handleSubmit = async (e: React.FormEvent) => {
36
+ e.preventDefault();
37
+ setError(null);
38
+
39
+ // Validate
40
+ const validation = validateRegister(
41
+ { email, password, name },
42
+ false,
43
+ requirePasswordConfirm
44
+ );
45
+ if (!validation.valid) {
46
+ setError(validation.error || "Invalid registration data");
47
+ onRegisterError?.(validation.error || "Invalid registration data");
48
+ return;
49
+ }
50
+
51
+ // Check terms if required
52
+ if (showTerms && !agreeToTerms) {
53
+ setError("You must agree to the terms and conditions");
54
+ onRegisterError?.("You must agree to the terms and conditions");
55
+ return;
56
+ }
57
+
58
+ setIsLoading(true);
59
+
60
+ try {
61
+ // In production, call your auth API
62
+ // const user = await register({ email, password, name, metadata });
63
+
64
+ // Simulate API call
65
+ await new Promise((resolve) => setTimeout(resolve, 1000));
66
+
67
+ // Success
68
+ const mockUser = {
69
+ id: "1",
70
+ email,
71
+ name: name || email.split("@")[0],
72
+ };
73
+
74
+ await onRegisterSuccess?.(mockUser);
75
+ navigate(config.afterLoginRoute);
76
+ } catch (err) {
77
+ const errorMessage = err instanceof Error ? err.message : "Registration failed";
78
+ setError(errorMessage);
79
+ onRegisterError?.(errorMessage);
80
+ } finally {
81
+ setIsLoading(false);
82
+ }
83
+ };
84
+
85
+ const passwordStrength = calculatePasswordStrength(password);
86
+ const passwordsMatch = !requirePasswordConfirm || password === confirmPassword;
87
+
88
+ return (
89
+ <form onSubmit={handleSubmit} className="w-full max-w-md space-y-5">
90
+ {/* Header */}
91
+ <div className="text-center">
92
+ <h1 className="text-3xl font-extrabold text-foreground mb-2">
93
+ Create an account
94
+ </h1>
95
+ <p className="text-muted-foreground">
96
+ Join {config.brandName} today
97
+ </p>
98
+ </div>
99
+
100
+ {/* Error Message */}
101
+ {error && (
102
+ <div className="bg-destructive/10 border border-destructive/20 text-destructive px-4 py-3 rounded-lg text-sm">
103
+ {error}
104
+ </div>
105
+ )}
106
+
107
+ {/* Name Field */}
108
+ <div className="space-y-2">
109
+ <label htmlFor="name" className="text-sm font-medium text-foreground">
110
+ Full Name
111
+ </label>
112
+ <input
113
+ id="name"
114
+ type="text"
115
+ value={name}
116
+ onChange={(e) => setName(e.target.value)}
117
+ placeholder="John Doe"
118
+ className={cn(
119
+ "w-full px-4 py-3 rounded-lg border bg-background",
120
+ "focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent",
121
+ "placeholder:text-muted-foreground"
122
+ )}
123
+ disabled={isLoading}
124
+ autoComplete="name"
125
+ />
126
+ </div>
127
+
128
+ {/* Email Field */}
129
+ <div className="space-y-2">
130
+ <label htmlFor="email" className="text-sm font-medium text-foreground">
131
+ Email
132
+ </label>
133
+ <input
134
+ id="email"
135
+ type="email"
136
+ value={email}
137
+ onChange={(e) => setEmail(e.target.value)}
138
+ placeholder="you@example.com"
139
+ className={cn(
140
+ "w-full px-4 py-3 rounded-lg border bg-background",
141
+ "focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent",
142
+ "placeholder:text-muted-foreground",
143
+ error && "border-destructive"
144
+ )}
145
+ disabled={isLoading}
146
+ autoComplete="email"
147
+ />
148
+ </div>
149
+
150
+ {/* Password Field */}
151
+ <div className="space-y-2">
152
+ <label htmlFor="password" className="text-sm font-medium text-foreground">
153
+ Password
154
+ </label>
155
+ <div className="relative">
156
+ <input
157
+ id="password"
158
+ type={showPassword ? "text" : "password"}
159
+ value={password}
160
+ onChange={(e) => setPassword(e.target.value)}
161
+ placeholder="••••••••"
162
+ className={cn(
163
+ "w-full px-4 py-3 rounded-lg border bg-background pr-12",
164
+ "focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent",
165
+ "placeholder:text-muted-foreground"
166
+ )}
167
+ disabled={isLoading}
168
+ autoComplete="new-password"
169
+ />
170
+ <button
171
+ type="button"
172
+ onClick={() => setShowPassword(!showPassword)}
173
+ className="absolute right-3 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground transition-colors"
174
+ tabIndex={-1}
175
+ >
176
+ {showPassword ? <EyeOff className="h-5 w-5" /> : <Eye className="h-5 w-5" />}
177
+ </button>
178
+ </div>
179
+ {/* Password Strength Indicator */}
180
+ {password && (
181
+ <div className="flex items-center gap-2">
182
+ <div className="flex-1 h-1 bg-muted rounded-full overflow-hidden">
183
+ <div
184
+ className={cn(
185
+ "h-full transition-all duration-300",
186
+ passwordStrength < 25 && "bg-destructive",
187
+ passwordStrength >= 25 && passwordStrength < 50 && "bg-orange-500",
188
+ passwordStrength >= 50 && passwordStrength < 75 && "bg-yellow-500",
189
+ passwordStrength >= 75 && "bg-green-500"
190
+ )}
191
+ style={{ width: `${passwordStrength}%` }}
192
+ />
193
+ </div>
194
+ <span className="text-xs text-muted-foreground">
195
+ {passwordStrength < 25 && "Weak"}
196
+ {passwordStrength >= 25 && passwordStrength < 50 && "Fair"}
197
+ {passwordStrength >= 50 && passwordStrength < 75 && "Good"}
198
+ {passwordStrength >= 75 && "Strong"}
199
+ </span>
200
+ </div>
201
+ )}
202
+ </div>
203
+
204
+ {/* Confirm Password Field */}
205
+ {requirePasswordConfirm && (
206
+ <div className="space-y-2">
207
+ <label htmlFor="confirmPassword" className="text-sm font-medium text-foreground">
208
+ Confirm Password
209
+ </label>
210
+ <div className="relative">
211
+ <input
212
+ id="confirmPassword"
213
+ type={showConfirmPassword ? "text" : "password"}
214
+ value={confirmPassword}
215
+ onChange={(e) => setConfirmPassword(e.target.value)}
216
+ placeholder="••••••••"
217
+ className={cn(
218
+ "w-full px-4 py-3 rounded-lg border bg-background pr-12",
219
+ "focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent",
220
+ "placeholder:text-muted-foreground",
221
+ confirmPassword && !passwordsMatch && "border-destructive"
222
+ )}
223
+ disabled={isLoading}
224
+ autoComplete="new-password"
225
+ />
226
+ <button
227
+ type="button"
228
+ onClick={() => setShowConfirmPassword(!showConfirmPassword)}
229
+ className="absolute right-3 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground transition-colors"
230
+ tabIndex={-1}
231
+ >
232
+ {showConfirmPassword ? <EyeOff className="h-5 w-5" /> : <Eye className="h-5 w-5" />}
233
+ </button>
234
+ </div>
235
+ {confirmPassword && !passwordsMatch && (
236
+ <p className="text-xs text-destructive">Passwords do not match</p>
237
+ )}
238
+ </div>
239
+ )}
240
+
241
+ {/* Terms Checkbox */}
242
+ {showTerms && (
243
+ <label className="flex items-start gap-2 cursor-pointer">
244
+ <input
245
+ type="checkbox"
246
+ checked={agreeToTerms}
247
+ onChange={(e) => setAgreeToTerms(e.target.checked)}
248
+ className="w-4 h-4 mt-1 rounded border-border text-primary focus:ring-2 focus:ring-primary"
249
+ />
250
+ <span className="text-sm text-muted-foreground">
251
+ I agree to the{" "}
252
+ <a href="/terms" className="text-primary hover:underline">
253
+ Terms of Service
254
+ </a>{" "}
255
+ and{" "}
256
+ <a href="/privacy" className="text-primary hover:underline">
257
+ Privacy Policy
258
+ </a>
259
+ </span>
260
+ </label>
261
+ )}
262
+
263
+ {/* Submit Button */}
264
+ <Button
265
+ type="submit"
266
+ className="w-full h-12 text-base font-bold rounded-full"
267
+ disabled={isLoading || (requirePasswordConfirm && !passwordsMatch)}
268
+ >
269
+ {isLoading ? (
270
+ <>
271
+ <Loader2 className="h-4 w-4 mr-2 animate-spin" />
272
+ Creating account...
273
+ </>
274
+ ) : (
275
+ "Create account"
276
+ )}
277
+ </Button>
278
+
279
+ {/* Login Link */}
280
+ {showLoginLink && config.loginRoute && (
281
+ <p className="text-center text-sm text-muted-foreground">
282
+ Already have an account?{" "}
283
+ <button
284
+ type="button"
285
+ onClick={() => navigate(config.loginRoute)}
286
+ className="text-primary hover:underline font-medium"
287
+ >
288
+ Sign in
289
+ </button>
290
+ </p>
291
+ )}
292
+ </form>
293
+ );
294
+ };
295
+
296
+ export default RegisterForm;
@@ -0,0 +1,230 @@
1
+ /**
2
+ * Reset Password Form Component
3
+ *
4
+ * Password reset confirmation form
5
+ */
6
+
7
+ import { useState } from "react";
8
+ import { useNavigate } from "react-router-dom";
9
+ import { Eye, EyeOff, Loader2, CheckCircle } from "lucide-react";
10
+ import { cn } from "@umituz/web-design-system/utils";
11
+ import { Button } from "@umituz/web-design-system/atoms";
12
+ import type { ResetPasswordFormProps } from "../types/auth";
13
+ import { validateResetPassword, calculatePasswordStrength } from "../utils/auth";
14
+
15
+ export const ResetPasswordForm = ({
16
+ config,
17
+ token,
18
+ onSuccess,
19
+ onError,
20
+ }: ResetPasswordFormProps) => {
21
+ const navigate = useNavigate();
22
+ const [password, setPassword] = useState("");
23
+ const [confirmPassword, setConfirmPassword] = useState("");
24
+ const [showPassword, setShowPassword] = useState(false);
25
+ const [showConfirmPassword, setShowConfirmPassword] = useState(false);
26
+ const [isLoading, setIsLoading] = useState(false);
27
+ const [error, setError] = useState<string | null>(null);
28
+ const [success, setSuccess] = useState(false);
29
+
30
+ const handleSubmit = async (e: React.FormEvent) => {
31
+ e.preventDefault();
32
+ setError(null);
33
+
34
+ // Validate
35
+ const validation = validateResetPassword({
36
+ token,
37
+ password,
38
+ confirmPassword,
39
+ });
40
+ if (!validation.valid) {
41
+ setError(validation.error || "Invalid reset data");
42
+ onError?.(validation.error || "Invalid reset data");
43
+ return;
44
+ }
45
+
46
+ setIsLoading(true);
47
+
48
+ try {
49
+ // In production, call your auth API
50
+ // await resetPassword({ token, password });
51
+
52
+ // Simulate API call
53
+ await new Promise((resolve) => setTimeout(resolve, 1000));
54
+
55
+ setSuccess(true);
56
+ await onSuccess?.();
57
+ } catch (err) {
58
+ const errorMessage = err instanceof Error ? err.message : "Failed to reset password";
59
+ setError(errorMessage);
60
+ onError?.(errorMessage);
61
+ } finally {
62
+ setIsLoading(false);
63
+ }
64
+ };
65
+
66
+ const passwordStrength = calculatePasswordStrength(password);
67
+ const passwordsMatch = password === confirmPassword;
68
+
69
+ if (success) {
70
+ return (
71
+ <div className="w-full max-w-md space-y-6">
72
+ {/* Success Message */}
73
+ <div className="text-center space-y-4">
74
+ <div className="flex justify-center">
75
+ <div className="w-16 h-16 bg-green-100 dark:bg-green-900/20 rounded-full flex items-center justify-center">
76
+ <CheckCircle className="h-8 w-8 text-green-600 dark:text-green-500" />
77
+ </div>
78
+ </div>
79
+ <div>
80
+ <h1 className="text-2xl font-extrabold text-foreground mb-2">
81
+ Password reset successful
82
+ </h1>
83
+ <p className="text-muted-foreground">
84
+ Your password has been updated. You can now sign in with your new password.
85
+ </p>
86
+ </div>
87
+ </div>
88
+
89
+ {/* Sign In Button */}
90
+ {config.loginRoute && (
91
+ <Button
92
+ type="button"
93
+ onClick={() => navigate(config.loginRoute)}
94
+ className="w-full h-12 text-base font-bold rounded-full"
95
+ >
96
+ Go to sign in
97
+ </Button>
98
+ )}
99
+ </div>
100
+ );
101
+ }
102
+
103
+ return (
104
+ <form onSubmit={handleSubmit} className="w-full max-w-md space-y-5">
105
+ {/* Header */}
106
+ <div className="text-center">
107
+ <h1 className="text-3xl font-extrabold text-foreground mb-2">
108
+ Create new password
109
+ </h1>
110
+ <p className="text-muted-foreground">
111
+ Enter your new password below
112
+ </p>
113
+ </div>
114
+
115
+ {/* Error Message */}
116
+ {error && (
117
+ <div className="bg-destructive/10 border border-destructive/20 text-destructive px-4 py-3 rounded-lg text-sm">
118
+ {error}
119
+ </div>
120
+ )}
121
+
122
+ {/* Password Field */}
123
+ <div className="space-y-2">
124
+ <label htmlFor="password" className="text-sm font-medium text-foreground">
125
+ New Password
126
+ </label>
127
+ <div className="relative">
128
+ <input
129
+ id="password"
130
+ type={showPassword ? "text" : "password"}
131
+ value={password}
132
+ onChange={(e) => setPassword(e.target.value)}
133
+ placeholder="••••••••"
134
+ className={cn(
135
+ "w-full px-4 py-3 rounded-lg border bg-background pr-12",
136
+ "focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent",
137
+ "placeholder:text-muted-foreground"
138
+ )}
139
+ disabled={isLoading}
140
+ autoComplete="new-password"
141
+ />
142
+ <button
143
+ type="button"
144
+ onClick={() => setShowPassword(!showPassword)}
145
+ className="absolute right-3 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground transition-colors"
146
+ tabIndex={-1}
147
+ >
148
+ {showPassword ? <EyeOff className="h-5 w-5" /> : <Eye className="h-5 w-5" />}
149
+ </button>
150
+ </div>
151
+ {/* Password Strength Indicator */}
152
+ {password && (
153
+ <div className="flex items-center gap-2">
154
+ <div className="flex-1 h-1 bg-muted rounded-full overflow-hidden">
155
+ <div
156
+ className={cn(
157
+ "h-full transition-all duration-300",
158
+ passwordStrength < 25 && "bg-destructive",
159
+ passwordStrength >= 25 && passwordStrength < 50 && "bg-orange-500",
160
+ passwordStrength >= 50 && passwordStrength < 75 && "bg-yellow-500",
161
+ passwordStrength >= 75 && "bg-green-500"
162
+ )}
163
+ style={{ width: `${passwordStrength}%` }}
164
+ />
165
+ </div>
166
+ <span className="text-xs text-muted-foreground">
167
+ {passwordStrength < 25 && "Weak"}
168
+ {passwordStrength >= 25 && passwordStrength < 50 && "Fair"}
169
+ {passwordStrength >= 50 && passwordStrength < 75 && "Good"}
170
+ {passwordStrength >= 75 && "Strong"}
171
+ </span>
172
+ </div>
173
+ )}
174
+ </div>
175
+
176
+ {/* Confirm Password Field */}
177
+ <div className="space-y-2">
178
+ <label htmlFor="confirmPassword" className="text-sm font-medium text-foreground">
179
+ Confirm New Password
180
+ </label>
181
+ <div className="relative">
182
+ <input
183
+ id="confirmPassword"
184
+ type={showConfirmPassword ? "text" : "password"}
185
+ value={confirmPassword}
186
+ onChange={(e) => setConfirmPassword(e.target.value)}
187
+ placeholder="••••••••"
188
+ className={cn(
189
+ "w-full px-4 py-3 rounded-lg border bg-background pr-12",
190
+ "focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent",
191
+ "placeholder:text-muted-foreground",
192
+ confirmPassword && !passwordsMatch && "border-destructive"
193
+ )}
194
+ disabled={isLoading}
195
+ autoComplete="new-password"
196
+ />
197
+ <button
198
+ type="button"
199
+ onClick={() => setShowConfirmPassword(!showConfirmPassword)}
200
+ className="absolute right-3 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground transition-colors"
201
+ tabIndex={-1}
202
+ >
203
+ {showConfirmPassword ? <EyeOff className="h-5 w-5" /> : <Eye className="h-5 w-5" />}
204
+ </button>
205
+ </div>
206
+ {confirmPassword && !passwordsMatch && (
207
+ <p className="text-xs text-destructive">Passwords do not match</p>
208
+ )}
209
+ </div>
210
+
211
+ {/* Submit Button */}
212
+ <Button
213
+ type="submit"
214
+ className="w-full h-12 text-base font-bold rounded-full"
215
+ disabled={isLoading || !passwordsMatch}
216
+ >
217
+ {isLoading ? (
218
+ <>
219
+ <Loader2 className="h-4 w-4 mr-2 animate-spin" />
220
+ Resetting...
221
+ </>
222
+ ) : (
223
+ "Reset password"
224
+ )}
225
+ </Button>
226
+ </form>
227
+ );
228
+ };
229
+
230
+ export default ResetPasswordForm;
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Auth Components
3
+ *
4
+ * Export all auth components
5
+ */
6
+
7
+ export { AuthLayout } from "./AuthLayout";
8
+ export { LoginForm } from "./LoginForm";
9
+ export { RegisterForm } from "./RegisterForm";
10
+ export { ForgotPasswordForm } from "./ForgotPasswordForm";
11
+ export { ResetPasswordForm } from "./ResetPasswordForm";
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Auth Hooks
3
+ *
4
+ * Export all auth hooks
5
+ */
6
+
7
+ export { useAuth } from "./useAuth";