@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.
Files changed (125) hide show
  1. package/README.md +202 -14
  2. package/package.json +21 -29
  3. package/src/domains/analytics/components/AnalyticsCard.tsx +23 -21
  4. package/src/domains/analytics/components/AnalyticsChart.tsx +345 -149
  5. package/src/domains/analytics/components/AnalyticsLayout.tsx +93 -112
  6. package/src/domains/analytics/components/MetricCard.tsx +42 -64
  7. package/src/domains/analytics/hooks/index.ts +7 -1
  8. package/src/domains/analytics/hooks/useAnalytics.ts +144 -139
  9. package/src/domains/analytics/index.ts +11 -0
  10. package/src/domains/analytics/services/AnalyticsEngineService.ts +103 -55
  11. package/src/domains/analytics/services/PerformanceService.ts +117 -69
  12. package/src/domains/analytics/types/analytics.ts +20 -6
  13. package/src/domains/analytics/types/index.ts +3 -0
  14. package/src/domains/analytics/utils/analytics.test.ts +183 -0
  15. package/src/domains/analytics/utils/analytics.ts +26 -9
  16. package/src/domains/analytics/utils/i18nKeys.ts +40 -0
  17. package/src/domains/analytics/utils/index.ts +2 -0
  18. package/src/domains/auth/components/AuthLayout.tsx +63 -54
  19. package/src/domains/auth/components/ForgotPasswordForm.tsx +96 -98
  20. package/src/domains/auth/components/LoginForm.tsx +167 -226
  21. package/src/domains/auth/components/RegisterForm.tsx +202 -272
  22. package/src/domains/auth/components/ResetPasswordForm.tsx +105 -151
  23. package/src/domains/auth/components/shared/AuthErrorBanner.tsx +35 -0
  24. package/src/domains/auth/components/shared/EmailInput.tsx +91 -0
  25. package/src/domains/auth/components/shared/PasswordInput.tsx +119 -0
  26. package/src/domains/auth/components/shared/PasswordStrengthIndicator.tsx +75 -0
  27. package/src/domains/auth/components/shared/SocialLoginButtons.tsx +58 -0
  28. package/src/domains/auth/components/shared/SocialProviderIcons.tsx +48 -0
  29. package/src/domains/auth/components/shared/index.ts +20 -0
  30. package/src/domains/auth/hooks/index.ts +1 -0
  31. package/src/domains/auth/hooks/useAuth.ts +151 -232
  32. package/src/domains/auth/index.ts +8 -4
  33. package/src/domains/auth/types/auth.ts +26 -5
  34. package/src/domains/auth/types/index.ts +1 -0
  35. package/src/domains/auth/utils/auth.test.ts +182 -0
  36. package/src/domains/auth/utils/auth.ts +71 -130
  37. package/src/domains/auth/utils/i18nKeys.ts +127 -0
  38. package/src/domains/auth/utils/index.ts +7 -1
  39. package/src/domains/auth/utils/secureToken.ts +33 -0
  40. package/src/domains/auth/utils/validationKeys.ts +23 -0
  41. package/src/domains/calendar/hooks/useCalendar.ts +145 -151
  42. package/src/domains/calendar/index.ts +0 -1
  43. package/src/domains/calendar/services/CalendarService.ts +122 -179
  44. package/src/domains/calendar/utils/contentItemMapper.test.ts +108 -0
  45. package/src/domains/calendar/utils/contentItemMapper.ts +99 -0
  46. package/src/domains/calendar/utils/index.ts +33 -30
  47. package/src/domains/layouts/components/BrandLogo.tsx +12 -9
  48. package/src/domains/layouts/components/DashboardHeader.tsx +78 -80
  49. package/src/domains/layouts/components/DashboardLayout.tsx +45 -21
  50. package/src/domains/layouts/components/DashboardSidebar.tsx +48 -51
  51. package/src/domains/layouts/types/layout.ts +7 -1
  52. package/src/domains/layouts/types/user.ts +3 -1
  53. package/src/domains/modules/build.test.tsx +97 -0
  54. package/src/domains/modules/build.tsx +71 -0
  55. package/src/domains/modules/index.ts +2 -0
  56. package/src/domains/modules/types.ts +29 -0
  57. package/src/domains/onboarding/components/AppFocusStep.tsx +99 -68
  58. package/src/domains/onboarding/components/OnboardingWizard.tsx +109 -183
  59. package/src/domains/onboarding/components/PlanStep.tsx +156 -123
  60. package/src/domains/onboarding/components/PlatformsStep.tsx +74 -54
  61. package/src/domains/onboarding/components/StepNavigation.tsx +108 -0
  62. package/src/domains/onboarding/components/StepProgress.tsx +62 -0
  63. package/src/domains/onboarding/components/UserTypeStep.tsx +59 -80
  64. package/src/domains/onboarding/components/index.ts +4 -0
  65. package/src/domains/onboarding/hooks/useOnboarding.ts +76 -66
  66. package/src/domains/onboarding/index.ts +1 -0
  67. package/src/domains/onboarding/types/onboarding.ts +57 -17
  68. package/src/domains/onboarding/utils/i18nKeys.ts +72 -0
  69. package/src/domains/onboarding/utils/index.ts +2 -0
  70. package/src/domains/permissions/PermissionsContext.tsx +103 -0
  71. package/src/domains/permissions/hasPermission.test.ts +50 -0
  72. package/src/domains/permissions/hasPermission.ts +38 -0
  73. package/src/domains/permissions/index.ts +19 -0
  74. package/src/domains/permissions/types.ts +35 -0
  75. package/src/domains/permissions/utils/i18nKeys.ts +11 -0
  76. package/src/domains/session/DemoLogin.tsx +50 -0
  77. package/src/domains/session/OrgSwitcher.tsx +110 -0
  78. package/src/domains/session/SessionContext.tsx +150 -0
  79. package/src/domains/session/inMemoryAuthAdapter.ts +53 -0
  80. package/src/domains/session/index.ts +18 -0
  81. package/src/domains/session/types.ts +47 -0
  82. package/src/domains/session/utils/i18nKeys.ts +12 -0
  83. package/src/domains/settings/components/AuditLogList.tsx +59 -0
  84. package/src/domains/settings/components/PermissionMatrixScreen.tsx +80 -0
  85. package/src/domains/settings/components/SettingsLayout.tsx +75 -60
  86. package/src/domains/settings/components/SettingsSection.tsx +35 -22
  87. package/src/domains/settings/components/UserManagementTable.tsx +87 -0
  88. package/src/domains/settings/components/index.ts +10 -0
  89. package/src/domains/settings/hooks/useSettings.ts +77 -52
  90. package/src/domains/settings/types/settings.ts +2 -0
  91. package/src/domains/settings/utils/i18nKeys.ts +18 -0
  92. package/src/domains/settings/utils/settings.ts +11 -4
  93. package/src/i18n/I18nContext.tsx +33 -0
  94. package/src/i18n/defaultT.test.ts +43 -0
  95. package/src/i18n/defaultT.ts +34 -0
  96. package/src/i18n/index.ts +7 -0
  97. package/src/i18n/types.ts +13 -0
  98. package/src/index.ts +1 -7
  99. package/src/primitives/Button.tsx +30 -0
  100. package/src/primitives/Checkbox.tsx +40 -0
  101. package/src/primitives/Input.tsx +19 -0
  102. package/src/primitives/Skeleton.tsx +26 -0
  103. package/src/primitives/cn.ts +15 -0
  104. package/src/primitives/index.ts +23 -0
  105. package/src/styles/dashboard.css +3045 -0
  106. package/src/styles/tokens.contract.test.ts +66 -0
  107. package/src/styles/tokens.css +124 -0
  108. package/src/domains/billing/components/BillingLayout.tsx +0 -63
  109. package/src/domains/billing/components/BillingPage.tsx +0 -49
  110. package/src/domains/billing/components/BillingPortal.tsx +0 -198
  111. package/src/domains/billing/components/InvoiceCard.tsx +0 -143
  112. package/src/domains/billing/components/PaymentMethodsList.tsx +0 -116
  113. package/src/domains/billing/components/PlanComparison.tsx +0 -199
  114. package/src/domains/billing/components/UsageCard.tsx +0 -103
  115. package/src/domains/billing/components/index.ts +0 -13
  116. package/src/domains/billing/constants/billing.ts +0 -69
  117. package/src/domains/billing/hooks/index.ts +0 -7
  118. package/src/domains/billing/hooks/useBilling.ts +0 -385
  119. package/src/domains/billing/index.ts +0 -73
  120. package/src/domains/billing/types/billing.ts +0 -342
  121. package/src/domains/billing/types/index.ts +0 -30
  122. package/src/domains/billing/utils/billing.ts +0 -360
  123. package/src/domains/billing/utils/index.ts +0 -31
  124. package/src/domains/billing/utils/polar.ts +0 -63
  125. package/src/domains/calendar/components/index.ts +0 -7
@@ -1,16 +1,52 @@
1
1
  /**
2
- * Register Form Component
2
+ * RegisterForm
3
3
  *
4
- * Configurable registration form with validation
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 { 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";
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 [name, setName] = useState(defaultData.name || "");
30
- const [email, setEmail] = useState(defaultData.email || "");
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
- // Validate
44
- const validation = validateRegister(
45
- { email, password, name },
46
- false,
47
- requirePasswordConfirm
48
- );
49
- if (!validation.valid) {
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
- setIsLoading(true);
76
+ const passwordsMatch = !requirePasswordConfirm || password === confirmPassword;
63
77
 
64
- try {
65
- let user: { id: string; email: string; name?: string };
78
+ const handleSubmit = useCallback(
79
+ async (e: React.FormEvent) => {
80
+ e.preventDefault();
81
+ setError(null);
66
82
 
67
- // Use custom register handler if provided (real auth), otherwise use mock auth
68
- if (onRegisterAttempt) {
69
- // Real authentication flow
70
- user = await onRegisterAttempt({ email, password, name });
71
- } else {
72
- // Mock authentication flow (for development/demo)
73
- await new Promise((resolve) => setTimeout(resolve, 1000));
74
- user = {
75
- id: "1",
76
- email,
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
- // Call success callback
82
- await onRegisterSuccess?.(user);
95
+ if (showTerms && !agreeToTerms) {
96
+ setError(KEYS.mustAcceptTerms);
97
+ onRegisterError?.(KEYS.mustAcceptTerms);
98
+ return;
99
+ }
83
100
 
84
- // Navigate to after-login route
85
- navigate(config.afterLoginRoute);
86
- } catch (err) {
87
- const errorMessage = err instanceof Error ? err.message : "Registration failed";
88
- setError(errorMessage);
89
- onRegisterError?.(errorMessage);
90
- } finally {
91
- setIsLoading(false);
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 passwordStrength = calculatePasswordStrength(password);
96
- const passwordsMatch = !requirePasswordConfirm || password === confirmPassword;
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="w-full max-w-md space-y-5">
100
- {/* Header */}
101
- <div className="text-center">
102
- <h1 className="text-3xl font-extrabold text-foreground mb-2">
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="text-muted-foreground">
106
- Join {config.brandName} today
150
+ <p className="udb-form__subtitle">
151
+ {translate(KEYS.join, { brand: config.brandName })}
107
152
  </p>
108
153
  </div>
109
154
 
110
- {/* Error Message */}
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
- {/* Name Field */}
118
- <div className="space-y-2">
119
- <label htmlFor="name" className="text-sm font-medium text-foreground">
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
- <input
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
- {/* Email Field */}
139
- <div className="space-y-2">
140
- <label htmlFor="email" className="text-sm font-medium text-foreground">
141
- Email
142
- </label>
143
- <input
144
- id="email"
145
- type="email"
146
- value={email}
147
- onChange={(e) => setEmail(e.target.value)}
148
- placeholder="you@example.com"
149
- className={cn(
150
- "w-full px-4 py-3 rounded-lg border bg-background",
151
- "focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent",
152
- "placeholder:text-muted-foreground",
153
- error && "border-destructive"
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
- autoComplete="email"
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="space-y-2">
217
- <label htmlFor="confirmPassword" className="text-sm font-medium text-foreground">
218
- Confirm Password
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
- <div className="relative">
221
- <input
222
- id="confirmPassword"
223
- type={showConfirmPassword ? "text" : "password"}
224
- value={confirmPassword}
225
- onChange={(e) => setConfirmPassword(e.target.value)}
226
- placeholder="••••••••"
227
- className={cn(
228
- "w-full px-4 py-3 rounded-lg border bg-background pr-12",
229
- "focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent",
230
- "placeholder:text-muted-foreground",
231
- confirmPassword && !passwordsMatch && "border-destructive"
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="text-xs text-destructive">Passwords do not match</p>
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="flex items-start gap-2 cursor-pointer">
254
- <input
255
- type="checkbox"
225
+ <label className="udb-check-label udb-check-label--multiline">
226
+ <Checkbox
256
227
  checked={agreeToTerms}
257
- onChange={(e) => setAgreeToTerms(e.target.checked)}
258
- className="w-4 h-4 mt-1 rounded border-border text-primary focus:ring-2 focus:ring-primary"
228
+ onCheckedChange={(checked) => setAgreeToTerms(Boolean(checked))}
229
+ disabled={isLoading}
230
+ aria-label={translate(KEYS.acceptTerms)}
259
231
  />
260
- <span className="text-sm text-muted-foreground">
261
- I agree to the{" "}
262
- <a href="/terms" className="text-primary hover:underline">
263
- Terms of Service
264
- </a>{" "}
265
- and{" "}
266
- <a href="/privacy" className="text-primary hover:underline">
267
- Privacy Policy
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="w-full h-12 text-base font-bold rounded-full"
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="h-4 w-4 mr-2 animate-spin" />
282
- Creating account...
252
+ <Loader2 className="udb-btn__spinner" aria-hidden="true" />
253
+ {translate(KEYS.submitting)}
283
254
  </>
284
255
  ) : (
285
- "Create account"
256
+ translate(KEYS.submit)
286
257
  )}
287
258
  </Button>
288
259
 
289
- {/* Login Link */}
290
260
  {showLoginLink && config.loginRoute && (
291
- <p className="text-center text-sm text-muted-foreground">
292
- Already have an account?{" "}
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="text-primary hover:underline font-medium"
266
+ className="udb-link-btn udb-link-btn--strong"
267
+ disabled={isLoading}
297
268
  >
298
- Sign in
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
- {/* Divider */}
307
- <div className="relative">
308
- <div className="absolute inset-0 flex items-center">
309
- <div className="w-full border-t border-border" />
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>