@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.
- package/package.json +10 -1
- package/src/domains/auth/components/AuthLayout.tsx +114 -0
- package/src/domains/auth/components/ForgotPasswordForm.tsx +181 -0
- package/src/domains/auth/components/LoginForm.tsx +228 -0
- package/src/domains/auth/components/RegisterForm.tsx +296 -0
- package/src/domains/auth/components/ResetPasswordForm.tsx +230 -0
- package/src/domains/auth/components/index.ts +11 -0
- package/src/domains/auth/hooks/index.ts +7 -0
- package/src/domains/auth/hooks/useAuth.ts +301 -0
- package/src/domains/auth/index.ts +58 -0
- package/src/domains/auth/types/auth.ts +265 -0
- package/src/domains/auth/types/index.ts +24 -0
- package/src/domains/auth/utils/auth.ts +280 -0
- package/src/domains/auth/utils/index.ts +23 -0
|
@@ -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";
|