@umituz/web-dashboard 2.0.9 → 2.1.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.
- 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
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@umituz/web-dashboard",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.1.1",
|
|
4
4
|
"description": "Dashboard Layout System - Customizable, themeable dashboard layouts and settings",
|
|
5
5
|
"main": "./src/index.ts",
|
|
6
6
|
"types": "./src/index.ts",
|
|
@@ -23,6 +23,11 @@
|
|
|
23
23
|
"./onboarding/hooks": "./src/domains/onboarding/hooks/index.ts",
|
|
24
24
|
"./onboarding/utils": "./src/domains/onboarding/utils/index.ts",
|
|
25
25
|
"./onboarding/types": "./src/domains/onboarding/types/index.ts",
|
|
26
|
+
"./auth": "./src/domains/auth/index.ts",
|
|
27
|
+
"./auth/components": "./src/domains/auth/components/index.ts",
|
|
28
|
+
"./auth/hooks": "./src/domains/auth/hooks/index.ts",
|
|
29
|
+
"./auth/utils": "./src/domains/auth/utils/index.ts",
|
|
30
|
+
"./auth/types": "./src/domains/auth/types/index.ts",
|
|
26
31
|
"./package.json": "./package.json"
|
|
27
32
|
},
|
|
28
33
|
"files": [
|
|
@@ -64,6 +69,10 @@
|
|
|
64
69
|
"settings",
|
|
65
70
|
"onboarding",
|
|
66
71
|
"wizard",
|
|
72
|
+
"auth",
|
|
73
|
+
"authentication",
|
|
74
|
+
"login",
|
|
75
|
+
"register",
|
|
67
76
|
"react",
|
|
68
77
|
"typescript",
|
|
69
78
|
"components",
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Auth Layout Component
|
|
3
|
+
*
|
|
4
|
+
* Configurable layout wrapper for authentication pages
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { BrandLogo } from "../../layouts/components";
|
|
8
|
+
import type { AuthLayoutProps } from "../types/auth";
|
|
9
|
+
|
|
10
|
+
export const AuthLayout = ({
|
|
11
|
+
config,
|
|
12
|
+
authState,
|
|
13
|
+
authActions,
|
|
14
|
+
children,
|
|
15
|
+
}: AuthLayoutProps) => {
|
|
16
|
+
return (
|
|
17
|
+
<div className="min-h-screen bg-background flex flex-col">
|
|
18
|
+
{/* Header */}
|
|
19
|
+
<header className="border-b border-border px-6 py-4 flex items-center justify-between">
|
|
20
|
+
<div className="flex items-center gap-2">
|
|
21
|
+
<BrandLogo size={32} />
|
|
22
|
+
<span className="font-bold text-xl text-foreground">{config.brandName}</span>
|
|
23
|
+
</div>
|
|
24
|
+
|
|
25
|
+
{/* Additional Header Content */}
|
|
26
|
+
{config.afterLoginRoute && (
|
|
27
|
+
<a
|
|
28
|
+
href={config.afterLoginRoute}
|
|
29
|
+
className="text-sm text-muted-foreground hover:text-foreground transition-colors"
|
|
30
|
+
>
|
|
31
|
+
Back to home
|
|
32
|
+
</a>
|
|
33
|
+
)}
|
|
34
|
+
</header>
|
|
35
|
+
|
|
36
|
+
{/* Main Content */}
|
|
37
|
+
<main className="flex-1 flex items-center justify-center px-4 py-12 bg-secondary/30">
|
|
38
|
+
<div className="w-full max-w-md">
|
|
39
|
+
{/* Card Container */}
|
|
40
|
+
<div className="bg-background border border-border rounded-2xl shadow-sm p-8">
|
|
41
|
+
{/* Tagline */}
|
|
42
|
+
{config.brandTagline && (
|
|
43
|
+
<p className="text-center text-sm text-muted-foreground mb-6">
|
|
44
|
+
{config.brandTagline}
|
|
45
|
+
</p>
|
|
46
|
+
)}
|
|
47
|
+
|
|
48
|
+
{/* Children Content (Forms) */}
|
|
49
|
+
{children}
|
|
50
|
+
</div>
|
|
51
|
+
|
|
52
|
+
{/* Social Login */}
|
|
53
|
+
{config.showSocialLogin && config.socialProviders && config.socialProviders.length > 0 && (
|
|
54
|
+
<div className="mt-6 space-y-4">
|
|
55
|
+
{/* Divider */}
|
|
56
|
+
<div className="relative">
|
|
57
|
+
<div className="absolute inset-0 flex items-center">
|
|
58
|
+
<div className="w-full border-t border-border" />
|
|
59
|
+
</div>
|
|
60
|
+
<div className="relative flex justify-center text-sm">
|
|
61
|
+
<span className="px-2 bg-secondary/30 text-muted-foreground">
|
|
62
|
+
Or continue with
|
|
63
|
+
</span>
|
|
64
|
+
</div>
|
|
65
|
+
</div>
|
|
66
|
+
|
|
67
|
+
{/* Social Buttons */}
|
|
68
|
+
<div className="grid grid-cols-2 gap-3">
|
|
69
|
+
{config.socialProviders.map((provider) => (
|
|
70
|
+
<button
|
|
71
|
+
key={provider.id}
|
|
72
|
+
type="button"
|
|
73
|
+
className={cn(
|
|
74
|
+
"flex items-center justify-center gap-2 px-4 py-3 rounded-lg border",
|
|
75
|
+
"bg-background hover:bg-muted transition-colors",
|
|
76
|
+
"text-sm font-medium text-foreground"
|
|
77
|
+
)}
|
|
78
|
+
onClick={() => {
|
|
79
|
+
// Handle social login
|
|
80
|
+
console.log(`Social login with ${provider.id}`);
|
|
81
|
+
}}
|
|
82
|
+
>
|
|
83
|
+
<span className="text-lg">{provider.icon}</span>
|
|
84
|
+
<span>{provider.name}</span>
|
|
85
|
+
</button>
|
|
86
|
+
))}
|
|
87
|
+
</div>
|
|
88
|
+
</div>
|
|
89
|
+
)}
|
|
90
|
+
</div>
|
|
91
|
+
</main>
|
|
92
|
+
|
|
93
|
+
{/* Footer */}
|
|
94
|
+
<footer className="border-t border-border px-6 py-4">
|
|
95
|
+
<div className="max-w-4xl mx-auto flex items-center justify-between text-xs text-muted-foreground">
|
|
96
|
+
<p>© {new Date().getFullYear()} {config.brandName}. All rights reserved.</p>
|
|
97
|
+
<div className="flex items-center gap-4">
|
|
98
|
+
<a href="/terms" className="hover:text-foreground transition-colors">
|
|
99
|
+
Terms
|
|
100
|
+
</a>
|
|
101
|
+
<a href="/privacy" className="hover:text-foreground transition-colors">
|
|
102
|
+
Privacy
|
|
103
|
+
</a>
|
|
104
|
+
<a href="/contact" className="hover:text-foreground transition-colors">
|
|
105
|
+
Contact
|
|
106
|
+
</a>
|
|
107
|
+
</div>
|
|
108
|
+
</div>
|
|
109
|
+
</footer>
|
|
110
|
+
</div>
|
|
111
|
+
);
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
export default AuthLayout;
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Forgot Password Form Component
|
|
3
|
+
*
|
|
4
|
+
* Password reset request form
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { useState } from "react";
|
|
8
|
+
import { useNavigate } from "react-router-dom";
|
|
9
|
+
import { ArrowLeft, 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 { ForgotPasswordFormProps } from "../types/auth";
|
|
13
|
+
import { validateForgotPassword } from "../utils/auth";
|
|
14
|
+
|
|
15
|
+
export const ForgotPasswordForm = ({
|
|
16
|
+
config,
|
|
17
|
+
onSuccess,
|
|
18
|
+
onError,
|
|
19
|
+
showBackLink = true,
|
|
20
|
+
}: ForgotPasswordFormProps) => {
|
|
21
|
+
const navigate = useNavigate();
|
|
22
|
+
const [email, setEmail] = useState("");
|
|
23
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
24
|
+
const [error, setError] = useState<string | null>(null);
|
|
25
|
+
const [success, setSuccess] = useState(false);
|
|
26
|
+
|
|
27
|
+
const handleSubmit = async (e: React.FormEvent) => {
|
|
28
|
+
e.preventDefault();
|
|
29
|
+
setError(null);
|
|
30
|
+
|
|
31
|
+
// Validate
|
|
32
|
+
const validation = validateForgotPassword({ email });
|
|
33
|
+
if (!validation.valid) {
|
|
34
|
+
setError(validation.error || "Invalid email");
|
|
35
|
+
onError?.(validation.error || "Invalid email");
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
setIsLoading(true);
|
|
40
|
+
|
|
41
|
+
try {
|
|
42
|
+
// In production, call your auth API
|
|
43
|
+
// await forgotPassword({ email });
|
|
44
|
+
|
|
45
|
+
// Simulate API call
|
|
46
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
47
|
+
|
|
48
|
+
setSuccess(true);
|
|
49
|
+
await onSuccess?.();
|
|
50
|
+
} catch (err) {
|
|
51
|
+
const errorMessage = err instanceof Error ? err.message : "Failed to send reset email";
|
|
52
|
+
setError(errorMessage);
|
|
53
|
+
onError?.(errorMessage);
|
|
54
|
+
} finally {
|
|
55
|
+
setIsLoading(false);
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
if (success) {
|
|
60
|
+
return (
|
|
61
|
+
<div className="w-full max-w-md space-y-6">
|
|
62
|
+
{/* Success Message */}
|
|
63
|
+
<div className="text-center space-y-4">
|
|
64
|
+
<div className="flex justify-center">
|
|
65
|
+
<div className="w-16 h-16 bg-green-100 dark:bg-green-900/20 rounded-full flex items-center justify-center">
|
|
66
|
+
<CheckCircle className="h-8 w-8 text-green-600 dark:text-green-500" />
|
|
67
|
+
</div>
|
|
68
|
+
</div>
|
|
69
|
+
<div>
|
|
70
|
+
<h1 className="text-2xl font-extrabold text-foreground mb-2">
|
|
71
|
+
Check your email
|
|
72
|
+
</h1>
|
|
73
|
+
<p className="text-muted-foreground">
|
|
74
|
+
We sent a password reset link to{" "}
|
|
75
|
+
<span className="font-medium text-foreground">{email}</span>
|
|
76
|
+
</p>
|
|
77
|
+
</div>
|
|
78
|
+
</div>
|
|
79
|
+
|
|
80
|
+
{/* Instructions */}
|
|
81
|
+
<div className="bg-muted/50 border border-border rounded-lg p-4 space-y-2">
|
|
82
|
+
<p className="text-sm text-muted-foreground">
|
|
83
|
+
✓ Click the link in your email
|
|
84
|
+
</p>
|
|
85
|
+
<p className="text-sm text-muted-foreground">
|
|
86
|
+
✓ Create a new password
|
|
87
|
+
</p>
|
|
88
|
+
<p className="text-sm text-muted-foreground">
|
|
89
|
+
✓ Sign in with your new password
|
|
90
|
+
</p>
|
|
91
|
+
</div>
|
|
92
|
+
|
|
93
|
+
{/* Back to Login */}
|
|
94
|
+
{showBackLink && config.loginRoute && (
|
|
95
|
+
<Button
|
|
96
|
+
type="button"
|
|
97
|
+
variant="ghost"
|
|
98
|
+
onClick={() => navigate(config.loginRoute!)}
|
|
99
|
+
className="w-full"
|
|
100
|
+
>
|
|
101
|
+
<ArrowLeft className="h-4 w-4 mr-2" />
|
|
102
|
+
Back to sign in
|
|
103
|
+
</Button>
|
|
104
|
+
)}
|
|
105
|
+
</div>
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return (
|
|
110
|
+
<form onSubmit={handleSubmit} className="w-full max-w-md space-y-6">
|
|
111
|
+
{/* Header */}
|
|
112
|
+
<div className="text-center">
|
|
113
|
+
<h1 className="text-3xl font-extrabold text-foreground mb-2">
|
|
114
|
+
Forgot password?
|
|
115
|
+
</h1>
|
|
116
|
+
<p className="text-muted-foreground">
|
|
117
|
+
No worries, we'll send you reset instructions
|
|
118
|
+
</p>
|
|
119
|
+
</div>
|
|
120
|
+
|
|
121
|
+
{/* Error Message */}
|
|
122
|
+
{error && (
|
|
123
|
+
<div className="bg-destructive/10 border border-destructive/20 text-destructive px-4 py-3 rounded-lg text-sm">
|
|
124
|
+
{error}
|
|
125
|
+
</div>
|
|
126
|
+
)}
|
|
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
|
+
{/* Submit Button */}
|
|
151
|
+
<Button
|
|
152
|
+
type="submit"
|
|
153
|
+
className="w-full h-12 text-base font-bold rounded-full"
|
|
154
|
+
disabled={isLoading}
|
|
155
|
+
>
|
|
156
|
+
{isLoading ? (
|
|
157
|
+
<>
|
|
158
|
+
<Loader2 className="h-4 w-4 mr-2 animate-spin" />
|
|
159
|
+
Sending...
|
|
160
|
+
</>
|
|
161
|
+
) : (
|
|
162
|
+
"Send reset link"
|
|
163
|
+
)}
|
|
164
|
+
</Button>
|
|
165
|
+
|
|
166
|
+
{/* Back to Login */}
|
|
167
|
+
{showBackLink && config.loginRoute && (
|
|
168
|
+
<button
|
|
169
|
+
type="button"
|
|
170
|
+
onClick={() => navigate(config.loginRoute!)}
|
|
171
|
+
className="w-full flex items-center justify-center gap-2 text-sm text-muted-foreground hover:text-foreground transition-colors"
|
|
172
|
+
>
|
|
173
|
+
<ArrowLeft className="h-4 w-4" />
|
|
174
|
+
Back to sign in
|
|
175
|
+
</button>
|
|
176
|
+
)}
|
|
177
|
+
</form>
|
|
178
|
+
);
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
export default ForgotPasswordForm;
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Login Form Component
|
|
3
|
+
*
|
|
4
|
+
* Configurable login 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 { LoginFormProps } from "../types/auth";
|
|
13
|
+
import { validateLogin, calculatePasswordStrength } from "../utils/auth";
|
|
14
|
+
|
|
15
|
+
export const LoginForm = ({
|
|
16
|
+
config,
|
|
17
|
+
defaultCredentials = {},
|
|
18
|
+
showRememberMe = true,
|
|
19
|
+
showForgotPassword = true,
|
|
20
|
+
showRegisterLink = true,
|
|
21
|
+
onLoginSuccess,
|
|
22
|
+
onLoginError,
|
|
23
|
+
}: LoginFormProps) => {
|
|
24
|
+
const navigate = useNavigate();
|
|
25
|
+
const [email, setEmail] = useState(defaultCredentials.email || "");
|
|
26
|
+
const [password, setPassword] = useState(defaultCredentials.password || "");
|
|
27
|
+
const [rememberMe, setRememberMe] = useState(false);
|
|
28
|
+
const [showPassword, setShowPassword] = useState(false);
|
|
29
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
30
|
+
const [error, setError] = useState<string | null>(null);
|
|
31
|
+
|
|
32
|
+
const handleSubmit = async (e: React.FormEvent) => {
|
|
33
|
+
e.preventDefault();
|
|
34
|
+
setError(null);
|
|
35
|
+
|
|
36
|
+
// Validate
|
|
37
|
+
const validation = validateLogin({ email, password });
|
|
38
|
+
if (!validation.valid) {
|
|
39
|
+
setError(validation.error || "Invalid credentials");
|
|
40
|
+
onLoginError?.(validation.error || "Invalid credentials");
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
setIsLoading(true);
|
|
45
|
+
|
|
46
|
+
try {
|
|
47
|
+
// In production, call your auth API
|
|
48
|
+
// const user = await login({ email, password });
|
|
49
|
+
|
|
50
|
+
// Simulate API call
|
|
51
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
52
|
+
|
|
53
|
+
// Success
|
|
54
|
+
const mockUser = {
|
|
55
|
+
id: "1",
|
|
56
|
+
email,
|
|
57
|
+
name: email.split("@")[0],
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
await onLoginSuccess?.(mockUser);
|
|
61
|
+
navigate(config.afterLoginRoute);
|
|
62
|
+
} catch (err) {
|
|
63
|
+
const errorMessage = err instanceof Error ? err.message : "Login failed";
|
|
64
|
+
setError(errorMessage);
|
|
65
|
+
onLoginError?.(errorMessage);
|
|
66
|
+
} finally {
|
|
67
|
+
setIsLoading(false);
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
const passwordStrength = calculatePasswordStrength(password);
|
|
72
|
+
|
|
73
|
+
return (
|
|
74
|
+
<form onSubmit={handleSubmit} className="w-full max-w-md space-y-6">
|
|
75
|
+
{/* Header */}
|
|
76
|
+
<div className="text-center">
|
|
77
|
+
<h1 className="text-3xl font-extrabold text-foreground mb-2">
|
|
78
|
+
Welcome back
|
|
79
|
+
</h1>
|
|
80
|
+
<p className="text-muted-foreground">
|
|
81
|
+
Sign in to your {config.brandName} account
|
|
82
|
+
</p>
|
|
83
|
+
</div>
|
|
84
|
+
|
|
85
|
+
{/* Error Message */}
|
|
86
|
+
{error && (
|
|
87
|
+
<div className="bg-destructive/10 border border-destructive/20 text-destructive px-4 py-3 rounded-lg text-sm">
|
|
88
|
+
{error}
|
|
89
|
+
</div>
|
|
90
|
+
)}
|
|
91
|
+
|
|
92
|
+
{/* Email Field */}
|
|
93
|
+
<div className="space-y-2">
|
|
94
|
+
<label htmlFor="email" className="text-sm font-medium text-foreground">
|
|
95
|
+
Email
|
|
96
|
+
</label>
|
|
97
|
+
<input
|
|
98
|
+
id="email"
|
|
99
|
+
type="email"
|
|
100
|
+
value={email}
|
|
101
|
+
onChange={(e) => setEmail(e.target.value)}
|
|
102
|
+
placeholder="you@example.com"
|
|
103
|
+
className={cn(
|
|
104
|
+
"w-full px-4 py-3 rounded-lg border bg-background",
|
|
105
|
+
"focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent",
|
|
106
|
+
"placeholder:text-muted-foreground",
|
|
107
|
+
error && "border-destructive"
|
|
108
|
+
)}
|
|
109
|
+
disabled={isLoading}
|
|
110
|
+
autoComplete="email"
|
|
111
|
+
/>
|
|
112
|
+
</div>
|
|
113
|
+
|
|
114
|
+
{/* Password Field */}
|
|
115
|
+
<div className="space-y-2">
|
|
116
|
+
<label htmlFor="password" className="text-sm font-medium text-foreground">
|
|
117
|
+
Password
|
|
118
|
+
</label>
|
|
119
|
+
<div className="relative">
|
|
120
|
+
<input
|
|
121
|
+
id="password"
|
|
122
|
+
type={showPassword ? "text" : "password"}
|
|
123
|
+
value={password}
|
|
124
|
+
onChange={(e) => setPassword(e.target.value)}
|
|
125
|
+
placeholder="••••••••"
|
|
126
|
+
className={cn(
|
|
127
|
+
"w-full px-4 py-3 rounded-lg border bg-background pr-12",
|
|
128
|
+
"focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent",
|
|
129
|
+
"placeholder:text-muted-foreground",
|
|
130
|
+
error && "border-destructive"
|
|
131
|
+
)}
|
|
132
|
+
disabled={isLoading}
|
|
133
|
+
autoComplete="current-password"
|
|
134
|
+
/>
|
|
135
|
+
<button
|
|
136
|
+
type="button"
|
|
137
|
+
onClick={() => setShowPassword(!showPassword)}
|
|
138
|
+
className="absolute right-3 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground transition-colors"
|
|
139
|
+
tabIndex={-1}
|
|
140
|
+
>
|
|
141
|
+
{showPassword ? <EyeOff className="h-5 w-5" /> : <Eye className="h-5 w-5" />}
|
|
142
|
+
</button>
|
|
143
|
+
</div>
|
|
144
|
+
{/* Password Strength Indicator */}
|
|
145
|
+
{password && (
|
|
146
|
+
<div className="flex items-center gap-2">
|
|
147
|
+
<div className="flex-1 h-1 bg-muted rounded-full overflow-hidden">
|
|
148
|
+
<div
|
|
149
|
+
className={cn(
|
|
150
|
+
"h-full transition-all duration-300",
|
|
151
|
+
passwordStrength < 25 && "bg-destructive",
|
|
152
|
+
passwordStrength >= 25 && passwordStrength < 50 && "bg-orange-500",
|
|
153
|
+
passwordStrength >= 50 && passwordStrength < 75 && "bg-yellow-500",
|
|
154
|
+
passwordStrength >= 75 && "bg-green-500"
|
|
155
|
+
)}
|
|
156
|
+
style={{ width: `${passwordStrength}%` }}
|
|
157
|
+
/>
|
|
158
|
+
</div>
|
|
159
|
+
<span className="text-xs text-muted-foreground">
|
|
160
|
+
{passwordStrength < 25 && "Weak"}
|
|
161
|
+
{passwordStrength >= 25 && passwordStrength < 50 && "Fair"}
|
|
162
|
+
{passwordStrength >= 50 && passwordStrength < 75 && "Good"}
|
|
163
|
+
{passwordStrength >= 75 && "Strong"}
|
|
164
|
+
</span>
|
|
165
|
+
</div>
|
|
166
|
+
)}
|
|
167
|
+
</div>
|
|
168
|
+
|
|
169
|
+
{/* Remember Me & Forgot Password */}
|
|
170
|
+
{(showRememberMe || showForgotPassword) && (
|
|
171
|
+
<div className="flex items-center justify-between">
|
|
172
|
+
{showRememberMe && (
|
|
173
|
+
<label className="flex items-center gap-2 cursor-pointer">
|
|
174
|
+
<input
|
|
175
|
+
type="checkbox"
|
|
176
|
+
checked={rememberMe}
|
|
177
|
+
onChange={(e) => setRememberMe(e.target.checked)}
|
|
178
|
+
className="w-4 h-4 rounded border-border text-primary focus:ring-2 focus:ring-primary"
|
|
179
|
+
/>
|
|
180
|
+
<span className="text-sm text-muted-foreground">Remember me</span>
|
|
181
|
+
</label>
|
|
182
|
+
)}
|
|
183
|
+
{showForgotPassword && config.forgotPasswordRoute && (
|
|
184
|
+
<button
|
|
185
|
+
type="button"
|
|
186
|
+
onClick={() => navigate(config.forgotPasswordRoute!)}
|
|
187
|
+
className="text-sm text-primary hover:underline font-medium"
|
|
188
|
+
>
|
|
189
|
+
Forgot password?
|
|
190
|
+
</button>
|
|
191
|
+
)}
|
|
192
|
+
</div>
|
|
193
|
+
)}
|
|
194
|
+
|
|
195
|
+
{/* Submit Button */}
|
|
196
|
+
<Button
|
|
197
|
+
type="submit"
|
|
198
|
+
className="w-full h-12 text-base font-bold rounded-full"
|
|
199
|
+
disabled={isLoading}
|
|
200
|
+
>
|
|
201
|
+
{isLoading ? (
|
|
202
|
+
<>
|
|
203
|
+
<Loader2 className="h-4 w-4 mr-2 animate-spin" />
|
|
204
|
+
Signing in...
|
|
205
|
+
</>
|
|
206
|
+
) : (
|
|
207
|
+
"Sign in"
|
|
208
|
+
)}
|
|
209
|
+
</Button>
|
|
210
|
+
|
|
211
|
+
{/* Register Link */}
|
|
212
|
+
{showRegisterLink && config.registerRoute && (
|
|
213
|
+
<p className="text-center text-sm text-muted-foreground">
|
|
214
|
+
Don't have an account?{" "}
|
|
215
|
+
<button
|
|
216
|
+
type="button"
|
|
217
|
+
onClick={() => navigate(config.registerRoute)}
|
|
218
|
+
className="text-primary hover:underline font-medium"
|
|
219
|
+
>
|
|
220
|
+
Sign up
|
|
221
|
+
</button>
|
|
222
|
+
</p>
|
|
223
|
+
)}
|
|
224
|
+
</form>
|
|
225
|
+
);
|
|
226
|
+
};
|
|
227
|
+
|
|
228
|
+
export default LoginForm;
|