@umituz/web-dashboard 3.2.2 → 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,52 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* RegisterForm
|
|
3
3
|
*
|
|
4
|
-
*
|
|
4
|
+
* Composed from shared auth primitives (EmailInput, PasswordInput,
|
|
5
|
+
* PasswordStrengthIndicator, AuthErrorBanner, SocialLoginButtons).
|
|
6
|
+
* Hardcoded mock user data + 1s setTimeout are gone — the form is
|
|
7
|
+
* pure orchestration over the consumer's `onRegisterAttempt` provider.
|
|
5
8
|
*/
|
|
6
9
|
|
|
7
|
-
import { useState } from "react";
|
|
10
|
+
import { useState, useCallback } from "react";
|
|
8
11
|
import { useNavigate } from "react-router-dom";
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
11
|
-
import { Button } from "
|
|
12
|
-
import
|
|
13
|
-
import {
|
|
12
|
+
import { useT } from "../../../i18n";
|
|
13
|
+
import { Loader2 } from "lucide-react";
|
|
14
|
+
import { Button, Checkbox, Input } from "../../../primitives";
|
|
15
|
+
import { cn } from "../../../primitives";
|
|
16
|
+
import type { RegisterFormProps, RegisterData, Translate } from "../types/auth";
|
|
17
|
+
import { validateRegister } from "../utils/auth";
|
|
18
|
+
import { AUTH_KEYS } from "../utils/i18nKeys";
|
|
19
|
+
import { AUTH_VALIDATION_KEYS } from "../utils/validationKeys";
|
|
20
|
+
import {
|
|
21
|
+
AuthErrorBanner,
|
|
22
|
+
EmailInput,
|
|
23
|
+
PasswordInput,
|
|
24
|
+
PasswordStrengthIndicator,
|
|
25
|
+
SocialLoginButtons,
|
|
26
|
+
} from "./shared";
|
|
27
|
+
|
|
28
|
+
const KEYS = {
|
|
29
|
+
title: 'auth.register.createAccount',
|
|
30
|
+
join: 'auth.register.join',
|
|
31
|
+
nameLabel: 'auth.fields.name',
|
|
32
|
+
namePlaceholder: 'auth.placeholders.fullName',
|
|
33
|
+
emailLabel: AUTH_KEYS.fields.email,
|
|
34
|
+
emailPlaceholder: AUTH_KEYS.placeholders.emailExample,
|
|
35
|
+
passwordLabel: AUTH_KEYS.fields.password,
|
|
36
|
+
passwordPlaceholder: AUTH_KEYS.placeholders.password,
|
|
37
|
+
confirmPassword: 'auth.register.confirmPassword',
|
|
38
|
+
submit: 'auth.register.submit',
|
|
39
|
+
submitting: 'auth.register.submitting',
|
|
40
|
+
acceptTerms: 'auth.register.acceptTerms',
|
|
41
|
+
termsOfService: 'auth.register.termsOfService',
|
|
42
|
+
privacyPolicy: 'auth.register.privacyPolicy',
|
|
43
|
+
haveAccount: 'auth.register.haveAccount',
|
|
44
|
+
signIn: 'auth.register.signIn',
|
|
45
|
+
orContinueWith: 'auth.login.orContinueWith',
|
|
46
|
+
invalidData: 'auth.errors.registrationFailed',
|
|
47
|
+
mustAcceptTerms: 'auth.register.mustAcceptTerms',
|
|
48
|
+
passwordsDoNotMatch: AUTH_VALIDATION_KEYS.passwordsDoNotMatch,
|
|
49
|
+
} as const;
|
|
14
50
|
|
|
15
51
|
export const RegisterForm = ({
|
|
16
52
|
config,
|
|
@@ -18,7 +54,7 @@ export const RegisterForm = ({
|
|
|
18
54
|
showTerms = true,
|
|
19
55
|
showLoginLink = true,
|
|
20
56
|
requirePasswordConfirm = true,
|
|
21
|
-
showSocialLogin,
|
|
57
|
+
showSocialLogin = true,
|
|
22
58
|
onRegisterAttempt,
|
|
23
59
|
onRegisterSuccess,
|
|
24
60
|
onRegisterError,
|
|
@@ -26,333 +62,227 @@ export const RegisterForm = ({
|
|
|
26
62
|
onAppleLogin,
|
|
27
63
|
}: RegisterFormProps) => {
|
|
28
64
|
const navigate = useNavigate();
|
|
29
|
-
const
|
|
30
|
-
const
|
|
31
|
-
const [password, setPassword] = useState(defaultData.password || "");
|
|
32
|
-
const [confirmPassword, setConfirmPassword] = useState("");
|
|
33
|
-
const [agreeToTerms, setAgreeToTerms] = useState(false);
|
|
34
|
-
const [showPassword, setShowPassword] = useState(false);
|
|
35
|
-
const [showConfirmPassword, setShowConfirmPassword] = useState(false);
|
|
36
|
-
const [isLoading, setIsLoading] = useState(false);
|
|
37
|
-
const [error, setError] = useState<string | null>(null);
|
|
38
|
-
|
|
39
|
-
const handleSubmit = async (e: React.FormEvent) => {
|
|
40
|
-
e.preventDefault();
|
|
41
|
-
setError(null);
|
|
65
|
+
const t = useT();
|
|
66
|
+
const translate: Translate = t;
|
|
42
67
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
setError(validation.error || "Invalid registration data");
|
|
51
|
-
onRegisterError?.(validation.error || "Invalid registration data");
|
|
52
|
-
return;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
// Check terms if required
|
|
56
|
-
if (showTerms && !agreeToTerms) {
|
|
57
|
-
setError("You must agree to the terms and conditions");
|
|
58
|
-
onRegisterError?.("You must agree to the terms and conditions");
|
|
59
|
-
return;
|
|
60
|
-
}
|
|
68
|
+
const [name, setName] = useState<string>(defaultData.name ?? '');
|
|
69
|
+
const [email, setEmail] = useState<string>(defaultData.email ?? '');
|
|
70
|
+
const [password, setPassword] = useState<string>(defaultData.password ?? '');
|
|
71
|
+
const [confirmPassword, setConfirmPassword] = useState<string>('');
|
|
72
|
+
const [agreeToTerms, setAgreeToTerms] = useState<boolean>(false);
|
|
73
|
+
const [isLoading, setIsLoading] = useState<boolean>(false);
|
|
74
|
+
const [error, setError] = useState<string | null>(null);
|
|
61
75
|
|
|
62
|
-
|
|
76
|
+
const passwordsMatch = !requirePasswordConfirm || password === confirmPassword;
|
|
63
77
|
|
|
64
|
-
|
|
65
|
-
|
|
78
|
+
const handleSubmit = useCallback(
|
|
79
|
+
async (e: React.FormEvent) => {
|
|
80
|
+
e.preventDefault();
|
|
81
|
+
setError(null);
|
|
66
82
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
name: name || email.split("@")[0],
|
|
78
|
-
};
|
|
83
|
+
const validation = validateRegister(
|
|
84
|
+
{ email, password, name },
|
|
85
|
+
false,
|
|
86
|
+
requirePasswordConfirm,
|
|
87
|
+
);
|
|
88
|
+
if (!validation.valid) {
|
|
89
|
+
const message = validation.error ?? KEYS.invalidData;
|
|
90
|
+
setError(message);
|
|
91
|
+
onRegisterError?.(message);
|
|
92
|
+
return;
|
|
79
93
|
}
|
|
80
94
|
|
|
81
|
-
|
|
82
|
-
|
|
95
|
+
if (showTerms && !agreeToTerms) {
|
|
96
|
+
setError(KEYS.mustAcceptTerms);
|
|
97
|
+
onRegisterError?.(KEYS.mustAcceptTerms);
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
83
100
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
101
|
+
setIsLoading(true);
|
|
102
|
+
try {
|
|
103
|
+
if (!onRegisterAttempt) {
|
|
104
|
+
// The form has no auth provider — fail loud rather than
|
|
105
|
+
// silently inserting a mock user.
|
|
106
|
+
throw new Error('No register handler configured');
|
|
107
|
+
}
|
|
108
|
+
const registerData: RegisterData = { email, password, name };
|
|
109
|
+
const user = await onRegisterAttempt(registerData);
|
|
110
|
+
await onRegisterSuccess?.(user);
|
|
111
|
+
navigate(config.afterLoginRoute);
|
|
112
|
+
} catch (err) {
|
|
113
|
+
const message = err instanceof Error ? err.message : KEYS.invalidData;
|
|
114
|
+
setError(message);
|
|
115
|
+
onRegisterError?.(message);
|
|
116
|
+
} finally {
|
|
117
|
+
setIsLoading(false);
|
|
118
|
+
}
|
|
119
|
+
},
|
|
120
|
+
[
|
|
121
|
+
agreeToTerms,
|
|
122
|
+
config.afterLoginRoute,
|
|
123
|
+
confirmPassword,
|
|
124
|
+
email,
|
|
125
|
+
name,
|
|
126
|
+
navigate,
|
|
127
|
+
onRegisterAttempt,
|
|
128
|
+
onRegisterError,
|
|
129
|
+
onRegisterSuccess,
|
|
130
|
+
password,
|
|
131
|
+
requirePasswordConfirm,
|
|
132
|
+
showTerms,
|
|
133
|
+
],
|
|
134
|
+
);
|
|
94
135
|
|
|
95
|
-
const
|
|
96
|
-
|
|
136
|
+
const handleSocial = useCallback(
|
|
137
|
+
(provider: 'google' | 'apple') => {
|
|
138
|
+
if (provider === 'google') onGoogleLogin?.();
|
|
139
|
+
if (provider === 'apple') onAppleLogin?.();
|
|
140
|
+
},
|
|
141
|
+
[onGoogleLogin, onAppleLogin],
|
|
142
|
+
);
|
|
97
143
|
|
|
98
144
|
return (
|
|
99
|
-
<form onSubmit={handleSubmit} className="
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
Create an account
|
|
145
|
+
<form onSubmit={handleSubmit} className="udb-form" noValidate>
|
|
146
|
+
<div className="udb-form__header">
|
|
147
|
+
<h1 className="udb-form__title udb-form__title--lg">
|
|
148
|
+
{translate(KEYS.title)}
|
|
104
149
|
</h1>
|
|
105
|
-
<p className="
|
|
106
|
-
|
|
150
|
+
<p className="udb-form__subtitle">
|
|
151
|
+
{translate(KEYS.join, { brand: config.brandName })}
|
|
107
152
|
</p>
|
|
108
153
|
</div>
|
|
109
154
|
|
|
110
|
-
{
|
|
111
|
-
{error && (
|
|
112
|
-
<div className="bg-destructive/10 border border-destructive/20 text-destructive px-4 py-3 rounded-lg text-sm">
|
|
113
|
-
{error}
|
|
114
|
-
</div>
|
|
115
|
-
)}
|
|
155
|
+
<AuthErrorBanner message={error} translate={translate} />
|
|
116
156
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
Full Name
|
|
157
|
+
<div className="udb-field">
|
|
158
|
+
<label htmlFor="register-name" className="udb-field__label">
|
|
159
|
+
{translate(KEYS.nameLabel)}
|
|
121
160
|
</label>
|
|
122
|
-
<
|
|
123
|
-
id="name"
|
|
161
|
+
<Input
|
|
162
|
+
id="register-name"
|
|
124
163
|
type="text"
|
|
164
|
+
autoComplete="name"
|
|
165
|
+
disabled={isLoading}
|
|
125
166
|
value={name}
|
|
167
|
+
placeholder={translate(KEYS.namePlaceholder)}
|
|
126
168
|
onChange={(e) => setName(e.target.value)}
|
|
127
|
-
placeholder="John Doe"
|
|
128
|
-
className={cn(
|
|
129
|
-
"w-full px-4 py-3 rounded-lg border bg-background",
|
|
130
|
-
"focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent",
|
|
131
|
-
"placeholder:text-muted-foreground"
|
|
132
|
-
)}
|
|
133
|
-
disabled={isLoading}
|
|
134
|
-
autoComplete="name"
|
|
135
169
|
/>
|
|
136
170
|
</div>
|
|
137
171
|
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
172
|
+
<EmailInput
|
|
173
|
+
id="register-email"
|
|
174
|
+
labelKey={KEYS.emailLabel}
|
|
175
|
+
placeholderKey={KEYS.emailPlaceholder}
|
|
176
|
+
value={email}
|
|
177
|
+
onChange={setEmail}
|
|
178
|
+
translate={translate}
|
|
179
|
+
disabled={isLoading}
|
|
180
|
+
required
|
|
181
|
+
/>
|
|
182
|
+
|
|
183
|
+
<div className="udb-form__group">
|
|
184
|
+
<PasswordInput
|
|
185
|
+
id="register-password"
|
|
186
|
+
labelKey={KEYS.passwordLabel}
|
|
187
|
+
placeholderKey={KEYS.passwordPlaceholder}
|
|
188
|
+
value={password}
|
|
189
|
+
onChange={setPassword}
|
|
190
|
+
translate={translate}
|
|
191
|
+
autoComplete="new-password"
|
|
155
192
|
disabled={isLoading}
|
|
156
|
-
|
|
193
|
+
required
|
|
157
194
|
/>
|
|
195
|
+
<PasswordStrengthIndicator password={password} translate={translate} />
|
|
158
196
|
</div>
|
|
159
197
|
|
|
160
|
-
{/* Password Field */}
|
|
161
|
-
<div className="space-y-2">
|
|
162
|
-
<label htmlFor="password" className="text-sm font-medium text-foreground">
|
|
163
|
-
Password
|
|
164
|
-
</label>
|
|
165
|
-
<div className="relative">
|
|
166
|
-
<input
|
|
167
|
-
id="password"
|
|
168
|
-
type={showPassword ? "text" : "password"}
|
|
169
|
-
value={password}
|
|
170
|
-
onChange={(e) => setPassword(e.target.value)}
|
|
171
|
-
placeholder="••••••••"
|
|
172
|
-
className={cn(
|
|
173
|
-
"w-full px-4 py-3 rounded-lg border bg-background pr-12",
|
|
174
|
-
"focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent",
|
|
175
|
-
"placeholder:text-muted-foreground"
|
|
176
|
-
)}
|
|
177
|
-
disabled={isLoading}
|
|
178
|
-
autoComplete="new-password"
|
|
179
|
-
/>
|
|
180
|
-
<button
|
|
181
|
-
type="button"
|
|
182
|
-
onClick={() => setShowPassword(!showPassword)}
|
|
183
|
-
className="absolute right-3 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground transition-colors"
|
|
184
|
-
tabIndex={-1}
|
|
185
|
-
>
|
|
186
|
-
{showPassword ? <EyeOff className="h-5 w-5" /> : <Eye className="h-5 w-5" />}
|
|
187
|
-
</button>
|
|
188
|
-
</div>
|
|
189
|
-
{/* Password Strength Indicator */}
|
|
190
|
-
{password && (
|
|
191
|
-
<div className="flex items-center gap-2">
|
|
192
|
-
<div className="flex-1 h-1 bg-muted rounded-full overflow-hidden">
|
|
193
|
-
<div
|
|
194
|
-
className={cn(
|
|
195
|
-
"h-full transition-all duration-300",
|
|
196
|
-
passwordStrength < 25 && "bg-destructive",
|
|
197
|
-
passwordStrength >= 25 && passwordStrength < 50 && "bg-orange-500",
|
|
198
|
-
passwordStrength >= 50 && passwordStrength < 75 && "bg-yellow-500",
|
|
199
|
-
passwordStrength >= 75 && "bg-green-500"
|
|
200
|
-
)}
|
|
201
|
-
style={{ width: `${passwordStrength}%` }}
|
|
202
|
-
/>
|
|
203
|
-
</div>
|
|
204
|
-
<span className="text-xs text-muted-foreground">
|
|
205
|
-
{passwordStrength < 25 && "Weak"}
|
|
206
|
-
{passwordStrength >= 25 && passwordStrength < 50 && "Fair"}
|
|
207
|
-
{passwordStrength >= 50 && passwordStrength < 75 && "Good"}
|
|
208
|
-
{passwordStrength >= 75 && "Strong"}
|
|
209
|
-
</span>
|
|
210
|
-
</div>
|
|
211
|
-
)}
|
|
212
|
-
</div>
|
|
213
|
-
|
|
214
|
-
{/* Confirm Password Field */}
|
|
215
198
|
{requirePasswordConfirm && (
|
|
216
|
-
<div className="
|
|
217
|
-
<label htmlFor="
|
|
218
|
-
|
|
199
|
+
<div className="udb-field">
|
|
200
|
+
<label htmlFor="register-confirm" className="udb-field__label">
|
|
201
|
+
{translate(KEYS.confirmPassword)}
|
|
202
|
+
<span className="udb-field__required" aria-hidden="true">*</span>
|
|
219
203
|
</label>
|
|
220
|
-
<
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
)}
|
|
233
|
-
disabled={isLoading}
|
|
234
|
-
autoComplete="new-password"
|
|
235
|
-
/>
|
|
236
|
-
<button
|
|
237
|
-
type="button"
|
|
238
|
-
onClick={() => setShowConfirmPassword(!showConfirmPassword)}
|
|
239
|
-
className="absolute right-3 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground transition-colors"
|
|
240
|
-
tabIndex={-1}
|
|
241
|
-
>
|
|
242
|
-
{showConfirmPassword ? <EyeOff className="h-5 w-5" /> : <Eye className="h-5 w-5" />}
|
|
243
|
-
</button>
|
|
244
|
-
</div>
|
|
204
|
+
<Input
|
|
205
|
+
id="register-confirm"
|
|
206
|
+
type="password"
|
|
207
|
+
autoComplete="new-password"
|
|
208
|
+
required
|
|
209
|
+
disabled={isLoading}
|
|
210
|
+
value={confirmPassword}
|
|
211
|
+
placeholder={translate(KEYS.passwordPlaceholder)}
|
|
212
|
+
onChange={(e) => setConfirmPassword(e.target.value)}
|
|
213
|
+
aria-invalid={Boolean(confirmPassword) && !passwordsMatch}
|
|
214
|
+
className={cn(confirmPassword && !passwordsMatch && 'udb-input--error')}
|
|
215
|
+
/>
|
|
245
216
|
{confirmPassword && !passwordsMatch && (
|
|
246
|
-
<p className="
|
|
217
|
+
<p className="udb-field__error" role="alert">
|
|
218
|
+
{translate(KEYS.passwordsDoNotMatch)}
|
|
219
|
+
</p>
|
|
247
220
|
)}
|
|
248
221
|
</div>
|
|
249
222
|
)}
|
|
250
223
|
|
|
251
|
-
{/* Terms Checkbox */}
|
|
252
224
|
{showTerms && (
|
|
253
|
-
<label className="
|
|
254
|
-
<
|
|
255
|
-
type="checkbox"
|
|
225
|
+
<label className="udb-check-label udb-check-label--multiline">
|
|
226
|
+
<Checkbox
|
|
256
227
|
checked={agreeToTerms}
|
|
257
|
-
|
|
258
|
-
|
|
228
|
+
onCheckedChange={(checked) => setAgreeToTerms(Boolean(checked))}
|
|
229
|
+
disabled={isLoading}
|
|
230
|
+
aria-label={translate(KEYS.acceptTerms)}
|
|
259
231
|
/>
|
|
260
|
-
<span className="
|
|
261
|
-
|
|
262
|
-
<a href="/terms" className="text-
|
|
263
|
-
|
|
264
|
-
</a>{
|
|
265
|
-
and{
|
|
266
|
-
<a href="/privacy" className="text-
|
|
267
|
-
|
|
232
|
+
<span className="udb-check-label__text">
|
|
233
|
+
{translate(KEYS.acceptTerms)}{' '}
|
|
234
|
+
<a href="/terms" className="udb-text-link">
|
|
235
|
+
{translate(KEYS.termsOfService)}
|
|
236
|
+
</a>{' '}
|
|
237
|
+
and{' '}
|
|
238
|
+
<a href="/privacy" className="udb-text-link">
|
|
239
|
+
{translate(KEYS.privacyPolicy)}
|
|
268
240
|
</a>
|
|
269
241
|
</span>
|
|
270
242
|
</label>
|
|
271
243
|
)}
|
|
272
244
|
|
|
273
|
-
{/* Submit Button */}
|
|
274
245
|
<Button
|
|
275
246
|
type="submit"
|
|
276
|
-
className="
|
|
247
|
+
className="udb-submit-btn udb-submit-btn--pill"
|
|
277
248
|
disabled={isLoading || (requirePasswordConfirm && !passwordsMatch)}
|
|
278
249
|
>
|
|
279
250
|
{isLoading ? (
|
|
280
251
|
<>
|
|
281
|
-
<Loader2 className="
|
|
282
|
-
|
|
252
|
+
<Loader2 className="udb-btn__spinner" aria-hidden="true" />
|
|
253
|
+
{translate(KEYS.submitting)}
|
|
283
254
|
</>
|
|
284
255
|
) : (
|
|
285
|
-
|
|
256
|
+
translate(KEYS.submit)
|
|
286
257
|
)}
|
|
287
258
|
</Button>
|
|
288
259
|
|
|
289
|
-
{/* Login Link */}
|
|
290
260
|
{showLoginLink && config.loginRoute && (
|
|
291
|
-
<p className="
|
|
292
|
-
|
|
261
|
+
<p className="udb-form__footer-text">
|
|
262
|
+
{translate(KEYS.haveAccount)}{' '}
|
|
293
263
|
<button
|
|
294
264
|
type="button"
|
|
295
265
|
onClick={() => navigate(config.loginRoute)}
|
|
296
|
-
className="
|
|
266
|
+
className="udb-link-btn udb-link-btn--strong"
|
|
267
|
+
disabled={isLoading}
|
|
297
268
|
>
|
|
298
|
-
|
|
269
|
+
{translate(KEYS.signIn)}
|
|
299
270
|
</button>
|
|
300
271
|
</p>
|
|
301
272
|
)}
|
|
302
273
|
|
|
303
|
-
{/* Social Login */}
|
|
304
274
|
{showSocialLogin && config.showSocialLogin && config.socialProviders && config.socialProviders.length > 0 && (
|
|
305
275
|
<>
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
</div>
|
|
311
|
-
<div className="relative flex justify-center text-sm">
|
|
312
|
-
<span className="px-2 bg-background text-muted-foreground">
|
|
313
|
-
Or continue with
|
|
314
|
-
</span>
|
|
315
|
-
</div>
|
|
316
|
-
</div>
|
|
317
|
-
|
|
318
|
-
{/* Social Buttons */}
|
|
319
|
-
<div className="grid grid-cols-2 gap-3">
|
|
320
|
-
{config.socialProviders.map((provider) => (
|
|
321
|
-
<button
|
|
322
|
-
key={provider.id}
|
|
323
|
-
type="button"
|
|
324
|
-
className={cn(
|
|
325
|
-
"flex items-center justify-center gap-2 px-4 py-3 rounded-lg border",
|
|
326
|
-
"bg-background hover:bg-muted transition-colors",
|
|
327
|
-
"text-sm font-medium text-foreground",
|
|
328
|
-
isLoading && "opacity-50 cursor-not-allowed"
|
|
329
|
-
)}
|
|
330
|
-
onClick={async () => {
|
|
331
|
-
if (provider.id === "google" && onGoogleLogin) {
|
|
332
|
-
await onGoogleLogin();
|
|
333
|
-
} else if (provider.id === "apple" && onAppleLogin) {
|
|
334
|
-
await onAppleLogin();
|
|
335
|
-
}
|
|
336
|
-
}}
|
|
337
|
-
disabled={isLoading}
|
|
338
|
-
>
|
|
339
|
-
{provider.icon === "google" && (
|
|
340
|
-
<svg width="18" height="18" viewBox="0 0 24 24">
|
|
341
|
-
<path d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92a5.06 5.06 0 01-2.2 3.32v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.1z" fill="#4285F4"/>
|
|
342
|
-
<path d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z" fill="#34A853"/>
|
|
343
|
-
<path d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z" fill="#FBBC05"/>
|
|
344
|
-
<path d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z" fill="#EA4335"/>
|
|
345
|
-
</svg>
|
|
346
|
-
)}
|
|
347
|
-
{provider.icon === "apple" && (
|
|
348
|
-
<svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor" className="text-foreground">
|
|
349
|
-
<path d="M17.05 20.28c-.98.95-2.05.8-3.08.35-1.09-.46-2.09-.48-3.24 0-1.44.62-2.2.44-3.06-.35C2.79 15.25 3.51 7.59 9.05 7.31c1.35.07 2.29.74 3.08.8 1.18-.24 2.31-.93 3.57-.84 1.51.12 2.65.72 3.4 1.8.3-3.12 1.38-5.98 0-8.1-3.12-1.87-3.12-3.28-8.1.3-8.1.8-1.06 0-2.29.44-3.06.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z"/>
|
|
350
|
-
</svg>
|
|
351
|
-
)}
|
|
352
|
-
<span>{provider.name}</span>
|
|
353
|
-
</button>
|
|
354
|
-
))}
|
|
276
|
+
<div className="udb-divider-row">
|
|
277
|
+
<span className="udb-divider-row__label">
|
|
278
|
+
{translate(KEYS.orContinueWith)}
|
|
279
|
+
</span>
|
|
355
280
|
</div>
|
|
281
|
+
<SocialLoginButtons
|
|
282
|
+
onProvider={handleSocial}
|
|
283
|
+
translate={translate}
|
|
284
|
+
disabled={isLoading}
|
|
285
|
+
/>
|
|
356
286
|
</>
|
|
357
287
|
)}
|
|
358
288
|
</form>
|