@umituz/web-dashboard 3.2.4 → 4.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.
- package/README.md +202 -14
- package/package.json +21 -29
- package/src/domains/analytics/components/AnalyticsCard.tsx +23 -21
- package/src/domains/analytics/components/AnalyticsChart.tsx +345 -149
- package/src/domains/analytics/components/AnalyticsLayout.tsx +93 -112
- package/src/domains/analytics/components/MetricCard.tsx +42 -64
- package/src/domains/analytics/hooks/index.ts +7 -1
- package/src/domains/analytics/hooks/useAnalytics.ts +144 -139
- package/src/domains/analytics/index.ts +11 -0
- package/src/domains/analytics/services/AnalyticsEngineService.ts +103 -55
- package/src/domains/analytics/services/PerformanceService.ts +117 -69
- package/src/domains/analytics/types/analytics.ts +20 -6
- package/src/domains/analytics/types/index.ts +3 -0
- package/src/domains/analytics/utils/analytics.test.ts +183 -0
- package/src/domains/analytics/utils/analytics.ts +26 -9
- package/src/domains/analytics/utils/i18nKeys.ts +40 -0
- package/src/domains/analytics/utils/index.ts +2 -0
- package/src/domains/auth/components/AuthLayout.tsx +63 -54
- package/src/domains/auth/components/ForgotPasswordForm.tsx +96 -98
- package/src/domains/auth/components/LoginForm.tsx +167 -226
- package/src/domains/auth/components/RegisterForm.tsx +202 -272
- package/src/domains/auth/components/ResetPasswordForm.tsx +105 -151
- package/src/domains/auth/components/shared/AuthErrorBanner.tsx +35 -0
- package/src/domains/auth/components/shared/EmailInput.tsx +91 -0
- package/src/domains/auth/components/shared/PasswordInput.tsx +119 -0
- package/src/domains/auth/components/shared/PasswordStrengthIndicator.tsx +75 -0
- package/src/domains/auth/components/shared/SocialLoginButtons.tsx +58 -0
- package/src/domains/auth/components/shared/SocialProviderIcons.tsx +48 -0
- package/src/domains/auth/components/shared/index.ts +20 -0
- package/src/domains/auth/hooks/index.ts +1 -0
- package/src/domains/auth/hooks/useAuth.ts +151 -232
- package/src/domains/auth/index.ts +8 -4
- package/src/domains/auth/types/auth.ts +26 -5
- package/src/domains/auth/types/index.ts +1 -0
- package/src/domains/auth/utils/auth.test.ts +182 -0
- package/src/domains/auth/utils/auth.ts +71 -130
- package/src/domains/auth/utils/i18nKeys.ts +127 -0
- package/src/domains/auth/utils/index.ts +7 -1
- package/src/domains/auth/utils/secureToken.ts +33 -0
- package/src/domains/auth/utils/validationKeys.ts +23 -0
- package/src/domains/calendar/hooks/useCalendar.ts +145 -151
- package/src/domains/calendar/index.ts +0 -1
- package/src/domains/calendar/services/CalendarService.ts +122 -179
- package/src/domains/calendar/utils/contentItemMapper.test.ts +108 -0
- package/src/domains/calendar/utils/contentItemMapper.ts +99 -0
- package/src/domains/calendar/utils/index.ts +33 -30
- package/src/domains/layouts/components/BrandLogo.tsx +12 -9
- package/src/domains/layouts/components/DashboardHeader.tsx +78 -80
- package/src/domains/layouts/components/DashboardLayout.tsx +45 -21
- package/src/domains/layouts/components/DashboardSidebar.tsx +48 -51
- package/src/domains/layouts/types/layout.ts +7 -1
- package/src/domains/layouts/types/user.ts +3 -1
- package/src/domains/modules/build.test.tsx +97 -0
- package/src/domains/modules/build.tsx +71 -0
- package/src/domains/modules/index.ts +2 -0
- package/src/domains/modules/types.ts +29 -0
- package/src/domains/onboarding/components/AppFocusStep.tsx +99 -68
- package/src/domains/onboarding/components/OnboardingWizard.tsx +109 -183
- package/src/domains/onboarding/components/PlanStep.tsx +156 -123
- package/src/domains/onboarding/components/PlatformsStep.tsx +74 -54
- package/src/domains/onboarding/components/StepNavigation.tsx +108 -0
- package/src/domains/onboarding/components/StepProgress.tsx +62 -0
- package/src/domains/onboarding/components/UserTypeStep.tsx +59 -80
- package/src/domains/onboarding/components/index.ts +4 -0
- package/src/domains/onboarding/hooks/useOnboarding.ts +76 -66
- package/src/domains/onboarding/index.ts +1 -0
- package/src/domains/onboarding/types/onboarding.ts +57 -17
- package/src/domains/onboarding/utils/i18nKeys.ts +72 -0
- package/src/domains/onboarding/utils/index.ts +2 -0
- package/src/domains/permissions/PermissionsContext.tsx +103 -0
- package/src/domains/permissions/hasPermission.test.ts +50 -0
- package/src/domains/permissions/hasPermission.ts +38 -0
- package/src/domains/permissions/index.ts +19 -0
- package/src/domains/permissions/types.ts +35 -0
- package/src/domains/permissions/utils/i18nKeys.ts +11 -0
- package/src/domains/session/DemoLogin.tsx +50 -0
- package/src/domains/session/OrgSwitcher.tsx +110 -0
- package/src/domains/session/SessionContext.tsx +150 -0
- package/src/domains/session/inMemoryAuthAdapter.ts +53 -0
- package/src/domains/session/index.ts +18 -0
- package/src/domains/session/types.ts +47 -0
- package/src/domains/session/utils/i18nKeys.ts +12 -0
- package/src/domains/settings/components/AuditLogList.tsx +59 -0
- package/src/domains/settings/components/PermissionMatrixScreen.tsx +80 -0
- package/src/domains/settings/components/SettingsLayout.tsx +75 -60
- package/src/domains/settings/components/SettingsSection.tsx +35 -22
- package/src/domains/settings/components/UserManagementTable.tsx +87 -0
- package/src/domains/settings/components/index.ts +10 -0
- package/src/domains/settings/hooks/useSettings.ts +77 -52
- package/src/domains/settings/types/settings.ts +2 -0
- package/src/domains/settings/utils/i18nKeys.ts +18 -0
- package/src/domains/settings/utils/settings.ts +11 -4
- package/src/i18n/I18nContext.tsx +33 -0
- package/src/i18n/defaultT.test.ts +43 -0
- package/src/i18n/defaultT.ts +34 -0
- package/src/i18n/index.ts +7 -0
- package/src/i18n/types.ts +13 -0
- package/src/index.ts +1 -7
- package/src/primitives/Button.tsx +30 -0
- package/src/primitives/Checkbox.tsx +40 -0
- package/src/primitives/Input.tsx +19 -0
- package/src/primitives/Skeleton.tsx +26 -0
- package/src/primitives/cn.ts +15 -0
- package/src/primitives/index.ts +23 -0
- package/src/styles/dashboard.css +3045 -0
- package/src/styles/tokens.contract.test.ts +66 -0
- package/src/styles/tokens.css +124 -0
- package/src/domains/billing/components/BillingLayout.tsx +0 -63
- package/src/domains/billing/components/BillingPage.tsx +0 -49
- package/src/domains/billing/components/BillingPortal.tsx +0 -198
- package/src/domains/billing/components/InvoiceCard.tsx +0 -143
- package/src/domains/billing/components/PaymentMethodsList.tsx +0 -116
- package/src/domains/billing/components/PlanComparison.tsx +0 -199
- package/src/domains/billing/components/UsageCard.tsx +0 -103
- package/src/domains/billing/components/index.ts +0 -13
- package/src/domains/billing/constants/billing.ts +0 -69
- package/src/domains/billing/hooks/index.ts +0 -7
- package/src/domains/billing/hooks/useBilling.ts +0 -385
- package/src/domains/billing/index.ts +0 -73
- package/src/domains/billing/types/billing.ts +0 -342
- package/src/domains/billing/types/index.ts +0 -30
- package/src/domains/billing/utils/billing.ts +0 -360
- package/src/domains/billing/utils/index.ts +0 -31
- package/src/domains/billing/utils/polar.ts +0 -63
- package/src/domains/calendar/components/index.ts +0 -7
|
@@ -1,16 +1,40 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* ResetPasswordForm
|
|
3
3
|
*
|
|
4
|
-
*
|
|
4
|
+
* Composed from shared auth primitives (PasswordInput, PasswordStrengthIndicator,
|
|
5
|
+
* AuthErrorBanner). All copy is i18n-keyed.
|
|
5
6
|
*/
|
|
6
7
|
|
|
7
8
|
import { useState } from "react";
|
|
8
9
|
import { useNavigate } from "react-router-dom";
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
11
|
-
import { Button } from "
|
|
12
|
-
import
|
|
13
|
-
import {
|
|
10
|
+
import { useT } from "../../../i18n";
|
|
11
|
+
import { Loader2, CheckCircle2 } from "lucide-react";
|
|
12
|
+
import { Button, Input } from "../../../primitives";
|
|
13
|
+
import { cn } from "../../../primitives";
|
|
14
|
+
import type { ResetPasswordFormProps, Translate } from "../types/auth";
|
|
15
|
+
import { validateResetPassword } from "../utils/auth";
|
|
16
|
+
import { AUTH_KEYS } from "../utils/i18nKeys";
|
|
17
|
+
import { AUTH_VALIDATION_KEYS } from "../utils/validationKeys";
|
|
18
|
+
import {
|
|
19
|
+
AuthErrorBanner,
|
|
20
|
+
PasswordInput,
|
|
21
|
+
PasswordStrengthIndicator,
|
|
22
|
+
} from "./shared";
|
|
23
|
+
|
|
24
|
+
const KEYS = {
|
|
25
|
+
title: 'auth.resetPassword.title',
|
|
26
|
+
description: 'auth.resetPassword.description',
|
|
27
|
+
newPassword: 'auth.resetPassword.newPassword',
|
|
28
|
+
confirmNewPassword: 'auth.resetPassword.confirmNewPassword',
|
|
29
|
+
passwordsDoNotMatch: AUTH_VALIDATION_KEYS.passwordsDoNotMatch,
|
|
30
|
+
submit: 'auth.resetPassword.submit',
|
|
31
|
+
submitting: 'auth.resetPassword.submitting',
|
|
32
|
+
successTitle: 'auth.resetPassword.successTitle',
|
|
33
|
+
successMessage: 'auth.resetPassword.successMessage',
|
|
34
|
+
goToSignIn: 'auth.resetPassword.goToSignIn',
|
|
35
|
+
invalidResetData: AUTH_VALIDATION_KEYS.passwordTooShort,
|
|
36
|
+
resetFailed: 'auth.errors.resetFailed',
|
|
37
|
+
} as const;
|
|
14
38
|
|
|
15
39
|
export const ResetPasswordForm = ({
|
|
16
40
|
config,
|
|
@@ -19,81 +43,70 @@ export const ResetPasswordForm = ({
|
|
|
19
43
|
onError,
|
|
20
44
|
}: ResetPasswordFormProps) => {
|
|
21
45
|
const navigate = useNavigate();
|
|
46
|
+
const t = useT();
|
|
47
|
+
const translate: Translate = t;
|
|
48
|
+
|
|
22
49
|
const [password, setPassword] = useState("");
|
|
23
50
|
const [confirmPassword, setConfirmPassword] = useState("");
|
|
24
|
-
const [showPassword, setShowPassword] = useState(false);
|
|
25
|
-
const [showConfirmPassword, setShowConfirmPassword] = useState(false);
|
|
26
51
|
const [isLoading, setIsLoading] = useState(false);
|
|
27
52
|
const [error, setError] = useState<string | null>(null);
|
|
28
53
|
const [success, setSuccess] = useState(false);
|
|
29
54
|
|
|
55
|
+
const passwordsMatch = password === confirmPassword;
|
|
56
|
+
|
|
30
57
|
const handleSubmit = async (e: React.FormEvent) => {
|
|
31
58
|
e.preventDefault();
|
|
32
59
|
setError(null);
|
|
33
60
|
|
|
34
|
-
|
|
35
|
-
const validation = validateResetPassword({
|
|
36
|
-
token,
|
|
37
|
-
password,
|
|
38
|
-
confirmPassword,
|
|
39
|
-
});
|
|
61
|
+
const validation = validateResetPassword({ token, password, confirmPassword });
|
|
40
62
|
if (!validation.valid) {
|
|
41
|
-
|
|
42
|
-
|
|
63
|
+
const message = validation.error ?? KEYS.invalidResetData;
|
|
64
|
+
setError(message);
|
|
65
|
+
onError?.(message);
|
|
43
66
|
return;
|
|
44
67
|
}
|
|
45
68
|
|
|
46
69
|
setIsLoading(true);
|
|
47
|
-
|
|
48
70
|
try {
|
|
49
|
-
//
|
|
50
|
-
//
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
71
|
+
// Hand off the actual reset to the consumer. The form is
|
|
72
|
+
// pure orchestration; no network knowledge here. Token and the
|
|
73
|
+
// new password are passed through so the consumer can persist it.
|
|
74
|
+
await onSuccess?.({ token, password });
|
|
75
|
+
// Don't keep the new password in component state after completion.
|
|
76
|
+
setPassword("");
|
|
77
|
+
setConfirmPassword("");
|
|
55
78
|
setSuccess(true);
|
|
56
|
-
await onSuccess?.();
|
|
57
79
|
} catch (err) {
|
|
58
|
-
const
|
|
59
|
-
setError(
|
|
60
|
-
onError?.(
|
|
80
|
+
const message = err instanceof Error ? err.message : KEYS.resetFailed;
|
|
81
|
+
setError(message);
|
|
82
|
+
onError?.(message);
|
|
61
83
|
} finally {
|
|
62
84
|
setIsLoading(false);
|
|
63
85
|
}
|
|
64
86
|
};
|
|
65
87
|
|
|
66
|
-
const passwordStrength = calculatePasswordStrength(password);
|
|
67
|
-
const passwordsMatch = password === confirmPassword;
|
|
68
|
-
|
|
69
88
|
if (success) {
|
|
70
89
|
return (
|
|
71
|
-
<div className="
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
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>
|
|
90
|
+
<div className="udb-form" role="status" aria-live="polite">
|
|
91
|
+
<div className="udb-form__header">
|
|
92
|
+
<div className="udb-success-badge">
|
|
93
|
+
<CheckCircle2 className="udb-success-badge__icon" aria-hidden="true" />
|
|
86
94
|
</div>
|
|
95
|
+
<h1 className="udb-form__title">
|
|
96
|
+
{translate(KEYS.successTitle)}
|
|
97
|
+
</h1>
|
|
98
|
+
<p className="udb-form__subtitle">
|
|
99
|
+
{translate(KEYS.successMessage)}
|
|
100
|
+
</p>
|
|
87
101
|
</div>
|
|
88
102
|
|
|
89
|
-
{/* Sign In Button */}
|
|
90
103
|
{config.loginRoute && (
|
|
91
104
|
<Button
|
|
92
105
|
type="button"
|
|
93
106
|
onClick={() => navigate(config.loginRoute)}
|
|
94
|
-
className="
|
|
107
|
+
className="udb-submit-btn udb-submit-btn--pill"
|
|
95
108
|
>
|
|
96
|
-
|
|
109
|
+
{translate(KEYS.goToSignIn)}
|
|
97
110
|
</Button>
|
|
98
111
|
)}
|
|
99
112
|
</div>
|
|
@@ -101,126 +114,67 @@ export const ResetPasswordForm = ({
|
|
|
101
114
|
}
|
|
102
115
|
|
|
103
116
|
return (
|
|
104
|
-
<form onSubmit={handleSubmit} className="
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
Create new password
|
|
117
|
+
<form onSubmit={handleSubmit} className="udb-form" noValidate>
|
|
118
|
+
<div className="udb-form__header">
|
|
119
|
+
<h1 className="udb-form__title udb-form__title--lg">
|
|
120
|
+
{translate(KEYS.title)}
|
|
109
121
|
</h1>
|
|
110
|
-
<p className="
|
|
111
|
-
Enter your new password below
|
|
112
|
-
</p>
|
|
122
|
+
<p className="udb-form__subtitle">{translate(KEYS.description)}</p>
|
|
113
123
|
</div>
|
|
114
124
|
|
|
115
|
-
{
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
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
|
-
)}
|
|
125
|
+
<AuthErrorBanner message={error} translate={translate} />
|
|
126
|
+
|
|
127
|
+
<div className="udb-form__group">
|
|
128
|
+
<PasswordInput
|
|
129
|
+
id="reset-password"
|
|
130
|
+
labelKey={KEYS.newPassword}
|
|
131
|
+
placeholderKey={AUTH_KEYS.placeholders.password}
|
|
132
|
+
value={password}
|
|
133
|
+
onChange={setPassword}
|
|
134
|
+
translate={translate}
|
|
135
|
+
autoComplete="new-password"
|
|
136
|
+
disabled={isLoading}
|
|
137
|
+
required
|
|
138
|
+
/>
|
|
139
|
+
<PasswordStrengthIndicator password={password} translate={translate} />
|
|
174
140
|
</div>
|
|
175
141
|
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
142
|
+
<div className="udb-field">
|
|
143
|
+
<label htmlFor="reset-confirm" className="udb-field__label">
|
|
144
|
+
{translate(KEYS.confirmNewPassword)}
|
|
145
|
+
<span className="udb-field__required" aria-hidden="true">*</span>
|
|
180
146
|
</label>
|
|
181
|
-
<
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
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>
|
|
147
|
+
<Input
|
|
148
|
+
id="reset-confirm"
|
|
149
|
+
type="password"
|
|
150
|
+
autoComplete="new-password"
|
|
151
|
+
required
|
|
152
|
+
disabled={isLoading}
|
|
153
|
+
value={confirmPassword}
|
|
154
|
+
placeholder={translate(AUTH_KEYS.placeholders.password)}
|
|
155
|
+
onChange={(e) => setConfirmPassword(e.target.value)}
|
|
156
|
+
aria-invalid={Boolean(confirmPassword) && !passwordsMatch}
|
|
157
|
+
className={cn(confirmPassword && !passwordsMatch && 'udb-input--error')}
|
|
158
|
+
/>
|
|
206
159
|
{confirmPassword && !passwordsMatch && (
|
|
207
|
-
<p className="
|
|
160
|
+
<p className="udb-field__error" role="alert">
|
|
161
|
+
{translate(KEYS.passwordsDoNotMatch)}
|
|
162
|
+
</p>
|
|
208
163
|
)}
|
|
209
164
|
</div>
|
|
210
165
|
|
|
211
|
-
{/* Submit Button */}
|
|
212
166
|
<Button
|
|
213
167
|
type="submit"
|
|
214
|
-
className="
|
|
168
|
+
className="udb-submit-btn udb-submit-btn--pill"
|
|
215
169
|
disabled={isLoading || !passwordsMatch}
|
|
216
170
|
>
|
|
217
171
|
{isLoading ? (
|
|
218
172
|
<>
|
|
219
|
-
<Loader2 className="
|
|
220
|
-
|
|
173
|
+
<Loader2 className="udb-btn__spinner" aria-hidden="true" />
|
|
174
|
+
{translate(KEYS.submitting)}
|
|
221
175
|
</>
|
|
222
176
|
) : (
|
|
223
|
-
|
|
177
|
+
translate(KEYS.submit)
|
|
224
178
|
)}
|
|
225
179
|
</Button>
|
|
226
180
|
</form>
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AuthErrorBanner
|
|
3
|
+
*
|
|
4
|
+
* Inline error display for auth forms. Renders an alert with the
|
|
5
|
+
* provided i18n key resolved by the consumer's translation function.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { AlertCircle } from "lucide-react";
|
|
9
|
+
import { cn } from "../../../../primitives";
|
|
10
|
+
|
|
11
|
+
export interface AuthErrorBannerProps {
|
|
12
|
+
/** i18n key for the message (already resolved by the consumer) */
|
|
13
|
+
message?: string | null;
|
|
14
|
+
/** Translation function used to resolve the key */
|
|
15
|
+
translate: (key: string) => string;
|
|
16
|
+
/** Optional className for layout integration */
|
|
17
|
+
className?: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export const AuthErrorBanner = ({ message, translate, className }: AuthErrorBannerProps) => {
|
|
21
|
+
if (!message) return null;
|
|
22
|
+
|
|
23
|
+
const resolved = message.includes('.') ? translate(message) : message;
|
|
24
|
+
|
|
25
|
+
return (
|
|
26
|
+
<div
|
|
27
|
+
role="alert"
|
|
28
|
+
aria-live="polite"
|
|
29
|
+
className={cn("udb-form-alert", className)}
|
|
30
|
+
>
|
|
31
|
+
<AlertCircle className="udb-form-alert__icon" aria-hidden="true" />
|
|
32
|
+
<span>{resolved}</span>
|
|
33
|
+
</div>
|
|
34
|
+
);
|
|
35
|
+
};
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* EmailInput
|
|
3
|
+
*
|
|
4
|
+
* Standardized email input used across all auth forms.
|
|
5
|
+
* Renders label, input, error state, and disabled state consistently.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { forwardRef } from "react";
|
|
9
|
+
import { Input } from "../../../../primitives";
|
|
10
|
+
import { cn } from "../../../../primitives";
|
|
11
|
+
|
|
12
|
+
export interface EmailInputProps {
|
|
13
|
+
/** Input id (used to wire up the label) */
|
|
14
|
+
id: string;
|
|
15
|
+
/** Label i18n key — resolved via `translate` */
|
|
16
|
+
labelKey: string;
|
|
17
|
+
/** Placeholder i18n key */
|
|
18
|
+
placeholderKey: string;
|
|
19
|
+
/** Current value */
|
|
20
|
+
value: string;
|
|
21
|
+
/** Change handler */
|
|
22
|
+
onChange: (value: string) => void;
|
|
23
|
+
/** Translation function */
|
|
24
|
+
translate: (key: string) => string;
|
|
25
|
+
/** Auto-complete hint (default: "email") */
|
|
26
|
+
autoComplete?: string;
|
|
27
|
+
/** Disable the input */
|
|
28
|
+
disabled?: boolean;
|
|
29
|
+
/** Whether to mark the field as required (renders an asterisk) */
|
|
30
|
+
required?: boolean;
|
|
31
|
+
/** Optional error message to display below the field */
|
|
32
|
+
error?: string | null;
|
|
33
|
+
/** Custom className */
|
|
34
|
+
className?: string;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export const EmailInput = forwardRef<HTMLInputElement, EmailInputProps>(
|
|
38
|
+
(
|
|
39
|
+
{
|
|
40
|
+
id,
|
|
41
|
+
labelKey,
|
|
42
|
+
placeholderKey,
|
|
43
|
+
value,
|
|
44
|
+
onChange,
|
|
45
|
+
translate,
|
|
46
|
+
autoComplete = "email",
|
|
47
|
+
disabled = false,
|
|
48
|
+
required = false,
|
|
49
|
+
error = null,
|
|
50
|
+
className,
|
|
51
|
+
},
|
|
52
|
+
ref,
|
|
53
|
+
) => {
|
|
54
|
+
return (
|
|
55
|
+
<div className={cn("udb-field", className)}>
|
|
56
|
+
<label htmlFor={id} className="udb-field__label">
|
|
57
|
+
{translate(labelKey)}
|
|
58
|
+
{required && (
|
|
59
|
+
<span className="udb-field__required" aria-hidden="true">
|
|
60
|
+
*
|
|
61
|
+
</span>
|
|
62
|
+
)}
|
|
63
|
+
</label>
|
|
64
|
+
<Input
|
|
65
|
+
ref={ref}
|
|
66
|
+
id={id}
|
|
67
|
+
type="email"
|
|
68
|
+
autoComplete={autoComplete}
|
|
69
|
+
required={required}
|
|
70
|
+
disabled={disabled}
|
|
71
|
+
value={value}
|
|
72
|
+
placeholder={translate(placeholderKey)}
|
|
73
|
+
onChange={(e) => onChange(e.target.value)}
|
|
74
|
+
aria-invalid={Boolean(error)}
|
|
75
|
+
aria-describedby={error ? `${id}-error` : undefined}
|
|
76
|
+
/>
|
|
77
|
+
{error && (
|
|
78
|
+
<p
|
|
79
|
+
id={`${id}-error`}
|
|
80
|
+
className="udb-field__error"
|
|
81
|
+
role="alert"
|
|
82
|
+
>
|
|
83
|
+
{error}
|
|
84
|
+
</p>
|
|
85
|
+
)}
|
|
86
|
+
</div>
|
|
87
|
+
);
|
|
88
|
+
},
|
|
89
|
+
);
|
|
90
|
+
|
|
91
|
+
EmailInput.displayName = "EmailInput";
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PasswordInput
|
|
3
|
+
*
|
|
4
|
+
* Password field with show/hide toggle, used across all auth forms.
|
|
5
|
+
* Centralizes the visibility toggle, autoComplete, and label structure
|
|
6
|
+
* so individual forms can stay focused on their unique concerns.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { forwardRef, useState } from "react";
|
|
10
|
+
import { Eye, EyeOff } from "lucide-react";
|
|
11
|
+
import { Input } from "../../../../primitives";
|
|
12
|
+
import { Button } from "../../../../primitives";
|
|
13
|
+
import { cn } from "../../../../primitives";
|
|
14
|
+
|
|
15
|
+
export interface PasswordInputProps {
|
|
16
|
+
/** Input id (used to wire up the label) */
|
|
17
|
+
id: string;
|
|
18
|
+
/** Label i18n key */
|
|
19
|
+
labelKey: string;
|
|
20
|
+
/** Placeholder i18n key */
|
|
21
|
+
placeholderKey: string;
|
|
22
|
+
/** Current value */
|
|
23
|
+
value: string;
|
|
24
|
+
/** Change handler */
|
|
25
|
+
onChange: (value: string) => void;
|
|
26
|
+
/** Translation function */
|
|
27
|
+
translate: (key: string) => string;
|
|
28
|
+
/** Auto-complete hint (e.g., "current-password", "new-password") */
|
|
29
|
+
autoComplete?: string;
|
|
30
|
+
/** Disable the input */
|
|
31
|
+
disabled?: boolean;
|
|
32
|
+
/** Whether to mark the field as required (renders an asterisk) */
|
|
33
|
+
required?: boolean;
|
|
34
|
+
/** Optional error message to display below the field */
|
|
35
|
+
error?: string | null;
|
|
36
|
+
/** Custom className */
|
|
37
|
+
className?: string;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export const PasswordInput = forwardRef<HTMLInputElement, PasswordInputProps>(
|
|
41
|
+
(
|
|
42
|
+
{
|
|
43
|
+
id,
|
|
44
|
+
labelKey,
|
|
45
|
+
placeholderKey,
|
|
46
|
+
value,
|
|
47
|
+
onChange,
|
|
48
|
+
translate,
|
|
49
|
+
autoComplete = "current-password",
|
|
50
|
+
disabled = false,
|
|
51
|
+
required = false,
|
|
52
|
+
error = null,
|
|
53
|
+
className,
|
|
54
|
+
},
|
|
55
|
+
ref,
|
|
56
|
+
) => {
|
|
57
|
+
const [visible, setVisible] = useState(false);
|
|
58
|
+
|
|
59
|
+
return (
|
|
60
|
+
<div className={cn("udb-field", className)}>
|
|
61
|
+
<label htmlFor={id} className="udb-field__label">
|
|
62
|
+
{translate(labelKey)}
|
|
63
|
+
{required && (
|
|
64
|
+
<span className="udb-field__required" aria-hidden="true">
|
|
65
|
+
*
|
|
66
|
+
</span>
|
|
67
|
+
)}
|
|
68
|
+
</label>
|
|
69
|
+
<div className="udb-password-wrap">
|
|
70
|
+
<Input
|
|
71
|
+
ref={ref}
|
|
72
|
+
id={id}
|
|
73
|
+
type={visible ? "text" : "password"}
|
|
74
|
+
autoComplete={autoComplete}
|
|
75
|
+
required={required}
|
|
76
|
+
disabled={disabled}
|
|
77
|
+
value={value}
|
|
78
|
+
placeholder={translate(placeholderKey)}
|
|
79
|
+
onChange={(e) => onChange(e.target.value)}
|
|
80
|
+
aria-invalid={Boolean(error)}
|
|
81
|
+
aria-describedby={error ? `${id}-error` : undefined}
|
|
82
|
+
className="udb-input--with-toggle"
|
|
83
|
+
/>
|
|
84
|
+
<Button
|
|
85
|
+
type="button"
|
|
86
|
+
variant="ghost"
|
|
87
|
+
size="icon"
|
|
88
|
+
tabIndex={-1}
|
|
89
|
+
onClick={() => setVisible((prev) => !prev)}
|
|
90
|
+
disabled={disabled}
|
|
91
|
+
aria-label={
|
|
92
|
+
visible
|
|
93
|
+
? translate('auth.a11y.hidePassword')
|
|
94
|
+
: translate('auth.a11y.showPassword')
|
|
95
|
+
}
|
|
96
|
+
className="udb-password-toggle"
|
|
97
|
+
>
|
|
98
|
+
{visible ? (
|
|
99
|
+
<EyeOff className="udb-password-toggle__icon" aria-hidden="true" />
|
|
100
|
+
) : (
|
|
101
|
+
<Eye className="udb-password-toggle__icon" aria-hidden="true" />
|
|
102
|
+
)}
|
|
103
|
+
</Button>
|
|
104
|
+
</div>
|
|
105
|
+
{error && (
|
|
106
|
+
<p
|
|
107
|
+
id={`${id}-error`}
|
|
108
|
+
className="udb-field__error"
|
|
109
|
+
role="alert"
|
|
110
|
+
>
|
|
111
|
+
{error}
|
|
112
|
+
</p>
|
|
113
|
+
)}
|
|
114
|
+
</div>
|
|
115
|
+
);
|
|
116
|
+
},
|
|
117
|
+
);
|
|
118
|
+
|
|
119
|
+
PasswordInput.displayName = "PasswordInput";
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PasswordStrengthIndicator
|
|
3
|
+
*
|
|
4
|
+
* Renders a 4-segment strength bar plus a localized label band.
|
|
5
|
+
* The label i18n key is computed from the strength band so the
|
|
6
|
+
* component stays free of hardcoded strings. Bar/label colors come
|
|
7
|
+
* from `--u-*` tones (weak/danger, fair/warning-2, good/warning,
|
|
8
|
+
* strong/success).
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { useMemo } from "react";
|
|
12
|
+
import { calculatePasswordStrength } from "../../utils/auth";
|
|
13
|
+
import { cn } from "../../../../primitives";
|
|
14
|
+
|
|
15
|
+
const STRENGTH_KEYS = {
|
|
16
|
+
weak: 'auth.passwordStrength.weak',
|
|
17
|
+
fair: 'auth.passwordStrength.fair',
|
|
18
|
+
good: 'auth.passwordStrength.good',
|
|
19
|
+
strong: 'auth.passwordStrength.strong',
|
|
20
|
+
} as const;
|
|
21
|
+
|
|
22
|
+
type StrengthBand = keyof typeof STRENGTH_KEYS;
|
|
23
|
+
|
|
24
|
+
const scoreToBand = (score: number): StrengthBand => {
|
|
25
|
+
if (score < 25) return 'weak';
|
|
26
|
+
if (score < 50) return 'fair';
|
|
27
|
+
if (score < 75) return 'good';
|
|
28
|
+
return 'strong';
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
export interface PasswordStrengthIndicatorProps {
|
|
32
|
+
/** Password to evaluate */
|
|
33
|
+
password: string;
|
|
34
|
+
/** Translation function */
|
|
35
|
+
translate: (key: string) => string;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export const PasswordStrengthIndicator = ({
|
|
39
|
+
password,
|
|
40
|
+
translate,
|
|
41
|
+
}: PasswordStrengthIndicatorProps) => {
|
|
42
|
+
const { band, filledSegments } = useMemo(() => {
|
|
43
|
+
const value = calculatePasswordStrength(password);
|
|
44
|
+
const resolvedBand = scoreToBand(value);
|
|
45
|
+
const segments = Math.round(value / 25);
|
|
46
|
+
return {
|
|
47
|
+
band: resolvedBand,
|
|
48
|
+
filledSegments: segments,
|
|
49
|
+
};
|
|
50
|
+
}, [password]);
|
|
51
|
+
|
|
52
|
+
if (!password) return null;
|
|
53
|
+
|
|
54
|
+
return (
|
|
55
|
+
<div className="udb-strength" aria-live="polite">
|
|
56
|
+
<div className="udb-strength__bars">
|
|
57
|
+
{[0, 1, 2, 3].map((segment) => {
|
|
58
|
+
const isActive = segment < filledSegments;
|
|
59
|
+
return (
|
|
60
|
+
<div
|
|
61
|
+
key={segment}
|
|
62
|
+
className={cn(
|
|
63
|
+
"udb-strength__bar",
|
|
64
|
+
isActive && `udb-strength__bar--${band}`,
|
|
65
|
+
)}
|
|
66
|
+
/>
|
|
67
|
+
);
|
|
68
|
+
})}
|
|
69
|
+
</div>
|
|
70
|
+
<p className={cn("udb-strength__label", `udb-strength__label--${band}`)}>
|
|
71
|
+
{translate(STRENGTH_KEYS[band])}
|
|
72
|
+
</p>
|
|
73
|
+
</div>
|
|
74
|
+
);
|
|
75
|
+
};
|