@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.
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,190 +1,222 @@
1
- import { Check } from "lucide-react";
2
- import { cn } from "@umituz/web-design-system/utils";
1
+ /**
2
+ * PlanStep
3
+ *
4
+ * Subscription plan selection step. All copy is i18n-keyed;
5
+ * the 20% yearly discount is a named constant, not a magic 0.8.
6
+ * Badge tones are semantic (`muted` | `primary` | `accent`) —
7
+ * no data-driven utility classes.
8
+ */
9
+
10
+ import { useMemo } from "react";
11
+ import { Check, Sparkles } from "lucide-react";
12
+ import { useT } from "../../../i18n";
13
+ import { cn } from "../../../primitives";
3
14
  import type { OnboardingState, PlanOption } from "../types/onboarding";
15
+ import { ONBOARDING_KEYS } from "../utils/i18nKeys";
4
16
 
5
- interface PlanStepProps {
6
- /** Current onboarding state */
17
+ export interface PlanStepProps {
7
18
  state: OnboardingState;
8
- /** Update state function */
9
19
  updateState: (updates: Partial<OnboardingState>) => void;
10
- /** Plan options */
11
20
  plans?: PlanOption[];
21
+ /** Yearly discount percentage (0-100). Default 20. */
22
+ yearlyDiscountPercent?: number;
23
+ /** Free trial duration in days, shown in the subtitle. Default 14. */
24
+ trialDays?: number;
12
25
  }
13
26
 
14
- /**
15
- * Plan Selection Step
16
- *
17
- * Fourth step - select subscription plan
18
- */
27
+ const MONTHS_IN_YEAR = 12;
28
+ const DEFAULT_YEARLY_DISCOUNT = 20;
29
+ const DEFAULT_TRIAL_DAYS = 14;
30
+
31
+ const DEFAULT_PLANS: PlanOption[] = [
32
+ {
33
+ id: "standard",
34
+ name: "Standard",
35
+ badgeKey: ONBOARDING_KEYS.plan.mostPopular,
36
+ badgeTone: "muted",
37
+ description: "Perfect for individuals and small businesses",
38
+ price: 12,
39
+ features: [
40
+ { text: "3 Social Accounts", bold: true },
41
+ "100 posts per month",
42
+ "Basic Analytics",
43
+ "Email Support",
44
+ ],
45
+ },
46
+ {
47
+ id: "pro",
48
+ name: "Pro",
49
+ badgeKey: ONBOARDING_KEYS.plan.bestValue,
50
+ badgeTone: "primary",
51
+ description: "For growing businesses and teams",
52
+ price: 29,
53
+ features: [
54
+ { text: "15 Social Accounts", bold: true },
55
+ "Unlimited posts",
56
+ "Advanced Analytics",
57
+ "AI Caption Suggestions",
58
+ { text: "5 Team Members", bold: true },
59
+ "Priority Support",
60
+ ],
61
+ highlight: true,
62
+ },
63
+ {
64
+ id: "enterprise",
65
+ name: "Enterprise",
66
+ badgeKey: ONBOARDING_KEYS.plan.custom,
67
+ badgeTone: "accent",
68
+ description: "For large organizations with custom needs",
69
+ price: 99,
70
+ features: [
71
+ { text: "Unlimited Accounts", bold: true },
72
+ "Unlimited everything",
73
+ "Custom Integrations",
74
+ "Dedicated Account Manager",
75
+ "24/7 Phone Support",
76
+ "SLA Guarantee",
77
+ ],
78
+ },
79
+ ];
80
+
19
81
  export const PlanStep = ({
20
82
  state,
21
83
  updateState,
22
84
  plans,
85
+ yearlyDiscountPercent = DEFAULT_YEARLY_DISCOUNT,
86
+ trialDays = DEFAULT_TRIAL_DAYS,
23
87
  }: PlanStepProps) => {
24
- // Default plans
25
- const defaultPlans: PlanOption[] = [
26
- {
27
- id: "standard",
28
- name: "Standard",
29
- badge: "Most Popular",
30
- badgeColor: "bg-muted text-muted-foreground",
31
- description: "Perfect for individuals and small businesses",
32
- price: 12,
33
- features: [
34
- { text: "3 Social Accounts", bold: true },
35
- "100 posts per month",
36
- "Basic Analytics",
37
- "Email Support",
38
- ],
39
- },
40
- {
41
- id: "pro",
42
- name: "Pro",
43
- badge: "Best Value",
44
- badgeColor: "bg-primary text-primary-foreground",
45
- description: "For growing businesses and teams",
46
- price: 29,
47
- features: [
48
- { text: "15 Social Accounts", bold: true },
49
- "Unlimited posts",
50
- "Advanced Analytics",
51
- "AI Caption Suggestions",
52
- { text: "5 Team Members", bold: true },
53
- "Priority Support",
54
- ],
55
- highlight: true,
56
- },
57
- {
58
- id: "enterprise",
59
- name: "Enterprise",
60
- badge: "Custom",
61
- badgeColor: "bg-purple-600 text-white",
62
- description: "For large organizations with custom needs",
63
- price: 99,
64
- features: [
65
- { text: "Unlimited Accounts", bold: true },
66
- "Unlimited everything",
67
- "Custom Integrations",
68
- "Dedicated Account Manager",
69
- "24/7 Phone Support",
70
- "SLA Guarantee",
71
- ],
72
- },
73
- ];
74
-
75
- const planOptions = plans || defaultPlans;
76
-
77
- // Calculate price with billing cycle discount
78
- const getPlanPrice = (plan: PlanOption) => {
79
- const basePrice = plan.price;
80
- return state.billingCycle === "yearly" ? basePrice * 12 * 0.8 : basePrice; // 20% discount
88
+ const t = useT();
89
+ const planOptions = plans ?? DEFAULT_PLANS;
90
+
91
+ /**
92
+ * Pure function for plan pricing — given a base monthly price and
93
+ * a cycle, return the effective price. The discount multiplier
94
+ * is derived from the named constant; no more magic 0.8.
95
+ */
96
+ const getPlanPrice = (plan: PlanOption): number => {
97
+ const base = plan.price;
98
+ if (state.billingCycle === "yearly") {
99
+ return (base * MONTHS_IN_YEAR * (100 - yearlyDiscountPercent)) / 100;
100
+ }
101
+ return base;
81
102
  };
82
103
 
104
+ const selectedPlan = useMemo(
105
+ () => planOptions.find((p) => p.id === state.selectedPlan) ?? null,
106
+ [planOptions, state.selectedPlan],
107
+ );
108
+
83
109
  return (
84
- <div className="w-full max-w-5xl">
85
- <div className="text-center mb-8">
86
- <h1 className="text-3xl md:text-4xl font-extrabold text-foreground mb-3">
87
- Choose your plan
110
+ <div className="udb-step udb-step--widest">
111
+ <div className="udb-step__header">
112
+ <h1 className="udb-step__title">
113
+ {t(ONBOARDING_KEYS.plan.title)}
88
114
  </h1>
89
- <p className="text-muted-foreground text-lg">
90
- Start with a 14-day free trial - no credit card required
115
+ <p className="udb-step__description udb-step__description--lg">
116
+ {t(ONBOARDING_KEYS.plan.description, { days: trialDays })}
91
117
  </p>
92
118
  </div>
93
119
 
94
- {/* Billing Cycle Toggle */}
95
- <div className="flex items-center justify-center gap-2 mb-12">
96
- <div className="flex bg-muted rounded-full p-1.5 border border-border">
120
+ <div className="udb-cycle-toggle-row">
121
+ <div
122
+ className="udb-cycle-toggle"
123
+ role="group"
124
+ aria-label={t(ONBOARDING_KEYS.plan.title)}
125
+ >
97
126
  <button
127
+ type="button"
128
+ aria-pressed={state.billingCycle === "monthly"}
98
129
  onClick={() => updateState({ billingCycle: "monthly" })}
99
130
  className={cn(
100
- "px-6 py-2 rounded-full text-sm font-bold transition-all",
101
- state.billingCycle === "monthly"
102
- ? "bg-primary text-primary-foreground shadow-lg shadow-primary/20"
103
- : "text-muted-foreground hover:text-foreground"
131
+ "udb-cycle-toggle__pill",
132
+ state.billingCycle === "monthly" && "udb-cycle-toggle__pill--on",
104
133
  )}
105
134
  >
106
- Monthly
135
+ {t(ONBOARDING_KEYS.plan.monthly)}
107
136
  </button>
108
137
  <button
138
+ type="button"
139
+ aria-pressed={state.billingCycle === "yearly"}
109
140
  onClick={() => updateState({ billingCycle: "yearly" })}
110
141
  className={cn(
111
- "px-6 py-2 rounded-full text-sm font-bold transition-all flex items-center gap-2",
112
- state.billingCycle === "yearly"
113
- ? "bg-primary text-primary-foreground shadow-lg shadow-primary/20"
114
- : "text-muted-foreground hover:text-foreground"
142
+ "udb-cycle-toggle__pill udb-cycle-toggle__pill--with-badge",
143
+ state.billingCycle === "yearly" && "udb-cycle-toggle__pill--on",
115
144
  )}
116
145
  >
117
- Yearly
146
+ {t(ONBOARDING_KEYS.plan.yearly)}
118
147
  <span
119
148
  className={cn(
120
- "text-[10px] px-1.5 py-0.5 rounded-full transition-colors",
121
- state.billingCycle === "yearly" ? "bg-white/20 text-white" : "bg-green-500/10 text-green-600"
149
+ "udb-cycle-toggle__save",
150
+ state.billingCycle === "yearly" && "udb-cycle-toggle__save--on",
122
151
  )}
123
152
  >
124
- Save 20%
153
+ {t(ONBOARDING_KEYS.plan.save, { percent: yearlyDiscountPercent })}
125
154
  </span>
126
155
  </button>
127
156
  </div>
128
157
  </div>
129
158
 
130
- {/* Plans Grid */}
131
- <div className="grid md:grid-cols-3 gap-8">
159
+ <div
160
+ className="udb-plans"
161
+ role="radiogroup"
162
+ aria-label={t(ONBOARDING_KEYS.plan.title)}
163
+ >
132
164
  {planOptions.map((plan) => {
133
165
  const isSelected = state.selectedPlan === plan.id;
134
166
  const planPrice = getPlanPrice(plan);
167
+ const badge = plan.badge ?? (plan.badgeKey ? t(plan.badgeKey) : undefined);
135
168
 
136
169
  return (
137
170
  <button
138
171
  key={plan.id}
172
+ type="button"
173
+ role="radio"
174
+ aria-checked={isSelected}
139
175
  onClick={() => updateState({ selectedPlan: plan.id })}
140
176
  className={cn(
141
- "bg-background border-2 rounded-[32px] p-8 text-left transition-all hover:translate-y-[-4px] relative",
142
- isSelected
143
- ? "border-primary ring-4 ring-primary/10"
144
- : "border-border hover:border-primary/40",
145
- plan.highlight && "shadow-xl"
177
+ "udb-plan",
178
+ isSelected && "udb-plan--selected",
179
+ plan.highlight && "udb-plan--highlight",
146
180
  )}
147
181
  >
148
182
  {plan.highlight && (
149
- <div className="absolute -top-3 left-1/2 -translate-x-1/2 bg-gradient-to-r from-primary to-purple-600 text-white text-[10px] font-black uppercase tracking-widest px-4 py-1 rounded-full">
150
- Recommended
183
+ <div className="udb-plan__recommended">
184
+ {t(ONBOARDING_KEYS.plan.recommended)}
151
185
  </div>
152
186
  )}
153
187
 
154
- <div className="flex items-center justify-between mb-4">
155
- <h3 className="text-2xl font-black text-foreground">{plan.name}</h3>
156
- {plan.badge && (
157
- <div
158
- className={cn(
159
- "text-[10px] font-black uppercase tracking-widest px-3 py-1 rounded-full",
160
- plan.badgeColor
161
- )}
162
- >
163
- {plan.badge}
188
+ <div className="udb-plan__heading">
189
+ <h3 className="udb-plan__name">{plan.name}</h3>
190
+ {badge && (
191
+ <div className={cn("udb-plan__badge", plan.badgeTone && `udb-plan__badge--${plan.badgeTone}`)}>
192
+ {badge}
164
193
  </div>
165
194
  )}
166
195
  </div>
167
196
 
168
- <p className="text-muted-foreground mb-8 font-medium">{plan.description}</p>
197
+ <p className="udb-plan__description">{plan.description}</p>
169
198
 
170
- <div className="flex items-baseline gap-1 mb-8">
171
- <span className="text-5xl font-black text-foreground">
199
+ <div className="udb-plan__price-row">
200
+ <span className="udb-plan__price">
172
201
  ${Math.round(planPrice)}
173
202
  </span>
174
- <span className="text-lg font-bold text-muted-foreground">/mo</span>
203
+ <span className="udb-plan__price-unit">
204
+ /{state.billingCycle === "yearly"
205
+ ? t(ONBOARDING_KEYS.plan.yearlyShort)
206
+ : t(ONBOARDING_KEYS.plan.monthlyShort)}
207
+ </span>
175
208
  </div>
176
209
 
177
- <ul className="space-y-4">
210
+ <ul className="udb-plan__features">
178
211
  {plan.features.map((feature, i) => {
179
212
  const text = typeof feature === "string" ? feature : feature.text;
180
213
  const bold = typeof feature !== "string" && feature.bold;
181
-
182
214
  return (
183
- <li key={i} className="flex items-start gap-3 text-sm font-medium">
184
- <div className="w-5 h-5 rounded-full bg-primary/10 flex items-center justify-center shrink-0 mt-0.5">
185
- <Check className="h-3 w-3 text-primary" />
186
- </div>
187
- <span className={cn("text-foreground/90", bold && "font-black")}>
215
+ <li key={`${plan.id}-feature-${i}`} className="udb-plan__feature">
216
+ <span className="udb-plan__feature-check">
217
+ <Check className="udb-plan__feature-check-icon" aria-hidden="true" />
218
+ </span>
219
+ <span className={cn("udb-plan__feature-text", bold && "udb-plan__feature-text--bold")}>
188
220
  {text}
189
221
  </span>
190
222
  </li>
@@ -196,9 +228,10 @@ export const PlanStep = ({
196
228
  })}
197
229
  </div>
198
230
 
199
- {state.selectedPlan && (
200
- <p className="mt-8 text-center text-sm text-muted-foreground animate-in fade-in">
201
- ✨ Great choice! You selected the {planOptions.find((p) => p.id === state.selectedPlan)?.name} plan
231
+ {selectedPlan && (
232
+ <p className="udb-step__hint" role="status" aria-live="polite">
233
+ <Sparkles className="udb-step__hint-icon" aria-hidden="true" />
234
+ {t(ONBOARDING_KEYS.plan.selectedPlan, { name: selectedPlan.name })}
202
235
  </p>
203
236
  )}
204
237
  </div>
@@ -1,43 +1,52 @@
1
- import { cn } from "@umituz/web-design-system/utils";
2
- import type { OnboardingState } from "../types/onboarding";
1
+ /**
2
+ * PlatformsStep
3
+ *
4
+ * Connect-platform onboarding step. Icons are Lucide components —
5
+ * emoji is forbidden by project rules. Platform `color` is a plain
6
+ * CSS color (brand hex), applied inline — no utility classes.
7
+ */
3
8
 
4
- interface PlatformsStepProps {
5
- /** Current onboarding state */
9
+ import {
10
+ Instagram,
11
+ Twitter,
12
+ Facebook,
13
+ Linkedin,
14
+ Youtube,
15
+ Pin,
16
+ Hash,
17
+ } from "lucide-react";
18
+ import { useT } from "../../../i18n";
19
+ import { cn } from "../../../primitives";
20
+ import type { OnboardingState, PlatformOption } from "../types/onboarding";
21
+ import { ONBOARDING_KEYS } from "../utils/i18nKeys";
22
+
23
+ export interface PlatformsStepProps {
6
24
  state: OnboardingState;
7
- /** Update state function */
8
25
  updateState: (updates: Partial<OnboardingState>) => void;
9
- /** Platform options */
10
- platforms?: Array<{
11
- id: string;
12
- name: string;
13
- icon: string;
14
- color?: string;
15
- }>;
26
+ platforms?: PlatformOption[];
16
27
  }
17
28
 
18
29
  /**
19
- * Platform Selection Step
20
- *
21
- * Third step - select social media platforms to connect
30
+ * Default platform set with brand-accurate Lucide icons and
31
+ * brand colors (CSS color strings, no emoji).
22
32
  */
33
+ const DEFAULT_PLATFORMS: PlatformOption[] = [
34
+ { id: "instagram", name: "Instagram", icon: Instagram, color: "#c026d3" },
35
+ { id: "twitter", name: "Twitter / X", icon: Twitter, color: "#0f172a" },
36
+ { id: "facebook", name: "Facebook", icon: Facebook, color: "#1d4ed8" },
37
+ { id: "linkedin", name: "LinkedIn", icon: Linkedin, color: "#1e3a8a" },
38
+ { id: "tiktok", name: "TikTok", icon: Hash, color: "#111111" },
39
+ { id: "youtube", name: "YouTube", icon: Youtube, color: "#dc2626" },
40
+ { id: "pinterest", name: "Pinterest", icon: Pin, color: "#b91c1c" },
41
+ ];
42
+
23
43
  export const PlatformsStep = ({
24
44
  state,
25
45
  updateState,
26
46
  platforms,
27
47
  }: PlatformsStepProps) => {
28
- // Default platforms
29
- const defaultPlatforms = [
30
- { id: "instagram", name: "Instagram", icon: "📸", color: "bg-gradient-to-br from-purple-500 to-pink-500" },
31
- { id: "twitter", name: "Twitter / X", icon: "🐦", color: "bg-black" },
32
- { id: "facebook", name: "Facebook", icon: "👍", color: "bg-blue-600" },
33
- { id: "linkedin", name: "LinkedIn", icon: "💼", color: "bg-blue-700" },
34
- { id: "tiktok", name: "TikTok", icon: "🎵", color: "bg-black" },
35
- { id: "youtube", name: "YouTube", icon: "📺", color: "bg-red-600" },
36
- { id: "pinterest", name: "Pinterest", icon: "📌", color: "bg-red-700" },
37
- { id: "threads", name: "Threads", icon: "🧵", color: "bg-black" },
38
- ];
39
-
40
- const platformOptions = platforms || defaultPlatforms;
48
+ const t = useT();
49
+ const platformOptions = platforms ?? DEFAULT_PLATFORMS;
41
50
 
42
51
  const togglePlatform = (id: string) => {
43
52
  const isConnected = state.connectedPlatforms.includes(id);
@@ -48,58 +57,69 @@ export const PlatformsStep = ({
48
57
  });
49
58
  };
50
59
 
60
+ const selectedCount = state.connectedPlatforms.length;
61
+
51
62
  return (
52
- <div className="w-full max-w-4xl">
53
- <div className="text-center mb-10">
54
- <h1 className="text-3xl md:text-4xl font-extrabold text-foreground mb-3">
55
- Connect your platforms
63
+ <div className="udb-step udb-step--wide">
64
+ <div className="udb-step__header">
65
+ <h1 className="udb-step__title">
66
+ {t(ONBOARDING_KEYS.platforms.title)}
56
67
  </h1>
57
- <p className="text-muted-foreground">
58
- Select the platforms you want to manage - you can add more later
68
+ <p className="udb-step__description">
69
+ {t(ONBOARDING_KEYS.platforms.description)}
59
70
  </p>
60
71
  </div>
61
72
 
62
- <div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 gap-4">
73
+ <div
74
+ className="udb-platforms"
75
+ role="group"
76
+ aria-label={t(ONBOARDING_KEYS.platforms.title)}
77
+ >
63
78
  {platformOptions.map((platform) => {
64
79
  const isConnected = state.connectedPlatforms.includes(platform.id);
65
-
80
+ const Icon = platform.icon;
66
81
  return (
67
82
  <button
68
83
  key={platform.id}
84
+ type="button"
85
+ aria-pressed={isConnected}
69
86
  onClick={() => togglePlatform(platform.id)}
70
87
  className={cn(
71
- "group bg-background border rounded-2xl p-6 flex flex-col items-center gap-4 transition-all hover:scale-[1.02]",
72
- isConnected ? "border-primary ring-2 ring-primary/20 bg-primary/5" : "border-border hover:border-primary/40"
88
+ "udb-platform",
89
+ isConnected && "udb-platform--connected",
73
90
  )}
74
91
  >
75
- <div
76
- className={cn(
77
- "w-12 h-12 rounded-2xl bg-muted flex items-center justify-center text-2xl group-hover:scale-110 transition-transform",
78
- platform.color
79
- )}
92
+ <span
93
+ className="udb-platform__icon-box"
94
+ style={{ background: platform.color }}
80
95
  >
81
- {platform.icon}
82
- </div>
96
+ <Icon className="udb-platform__icon" aria-hidden="true" />
97
+ </span>
83
98
 
84
- <div className="flex flex-col items-center">
85
- <span className="font-bold text-foreground text-sm">{platform.name}</span>
99
+ <span className="udb-platform__meta">
100
+ <span className="udb-platform__name">{platform.name}</span>
86
101
  <span
87
102
  className={cn(
88
- "text-[10px] font-bold uppercase tracking-wider mt-1 transition-colors",
89
- isConnected ? "text-primary" : "text-muted-foreground"
103
+ "udb-platform__state",
104
+ isConnected && "udb-platform__state--on",
90
105
  )}
91
106
  >
92
- {isConnected ? "Connected" : "Connect"}
107
+ {isConnected
108
+ ? t(ONBOARDING_KEYS.platforms.connected)
109
+ : t(ONBOARDING_KEYS.platforms.connect)}
93
110
  </span>
94
- </div>
111
+ </span>
95
112
  </button>
96
113
  );
97
114
  })}
98
115
  </div>
99
116
 
100
- {state.connectedPlatforms.length > 0 && (
101
- <p className="mt-8 text-center text-sm text-muted-foreground animate-in fade-in">
102
- ✨ {state.connectedPlatforms.length} platform{state.connectedPlatforms.length > 1 ? "s" : ""} selected
117
+ {selectedCount > 0 && (
118
+ <p className="udb-step__hint" role="status" aria-live="polite">
119
+ {t(ONBOARDING_KEYS.platforms.selected, {
120
+ count: selectedCount,
121
+ plural: selectedCount === 1 ? '' : 's',
122
+ })}
103
123
  </p>
104
124
  )}
105
125
  </div>
@@ -0,0 +1,108 @@
1
+ /**
2
+ * Onboarding - Step Navigation Footer
3
+ *
4
+ * Renders the wizard's bottom action bar: back, skip, next/getStarted.
5
+ * All labels are i18n keys; the consumer maps them to localized strings.
6
+ */
7
+
8
+ import { ArrowLeft, Loader2 } from "lucide-react";
9
+ import { Button } from "../../../primitives";
10
+
11
+ /**
12
+ * Centralized i18n keys for navigation labels.
13
+ * Components stay free of hardcoded UI strings.
14
+ */
15
+ export const STEP_NAVIGATION_KEYS = {
16
+ back: 'onboarding.buttons.back',
17
+ skip: 'onboarding.buttons.skip',
18
+ next: 'onboarding.buttons.next',
19
+ getStarted: 'onboarding.buttons.getStarted',
20
+ finalizing: 'onboarding.buttons.finalizing',
21
+ } as const;
22
+
23
+ export type StepNavigationKey =
24
+ (typeof STEP_NAVIGATION_KEYS)[keyof typeof STEP_NAVIGATION_KEYS];
25
+
26
+ /**
27
+ * Translation function shape consumers must inject.
28
+ * The wizard never depends on a specific i18n library.
29
+ */
30
+ export type Translate = (key: string) => string;
31
+
32
+ export interface StepNavigationProps {
33
+ currentStep: number;
34
+ totalSteps: number;
35
+ canGoNext: boolean;
36
+ isSaving?: boolean;
37
+ nextLabel?: string;
38
+ prevLabel?: string;
39
+ onNext: () => void;
40
+ onPrev: () => void;
41
+ allowSkip?: boolean;
42
+ onSkip?: () => void;
43
+ translate: Translate;
44
+ }
45
+
46
+ export const StepNavigation = ({
47
+ currentStep,
48
+ totalSteps,
49
+ canGoNext,
50
+ isSaving = false,
51
+ nextLabel,
52
+ prevLabel,
53
+ onNext,
54
+ onPrev,
55
+ allowSkip = false,
56
+ onSkip,
57
+ translate: t,
58
+ }: StepNavigationProps) => {
59
+ const isLastStep = currentStep === totalSteps;
60
+
61
+ return (
62
+ <footer className="udb-onboarding__footer">
63
+ {currentStep > 1 ? (
64
+ <Button
65
+ variant="ghost"
66
+ onClick={onPrev}
67
+ className="udb-onboarding__footer-btn"
68
+ disabled={isSaving}
69
+ >
70
+ <ArrowLeft className="udb-onboarding__footer-icon" aria-hidden="true" />
71
+ {prevLabel ?? t(STEP_NAVIGATION_KEYS.back)}
72
+ </Button>
73
+ ) : (
74
+ <div />
75
+ )}
76
+
77
+ <div className="udb-onboarding__footer-actions">
78
+ {allowSkip && onSkip && !isLastStep && (
79
+ <Button
80
+ variant="ghost"
81
+ onClick={onSkip}
82
+ className="udb-onboarding__footer-btn"
83
+ disabled={isSaving}
84
+ >
85
+ {t(STEP_NAVIGATION_KEYS.skip)}
86
+ </Button>
87
+ )}
88
+
89
+ <Button
90
+ onClick={onNext}
91
+ className="udb-onboarding__footer-next"
92
+ disabled={!canGoNext || isSaving}
93
+ >
94
+ {isSaving ? (
95
+ <>
96
+ <Loader2 className="udb-onboarding__footer-icon udb-spin" aria-hidden="true" />
97
+ {t(STEP_NAVIGATION_KEYS.finalizing)}
98
+ </>
99
+ ) : isLastStep ? (
100
+ nextLabel ?? t(STEP_NAVIGATION_KEYS.getStarted)
101
+ ) : (
102
+ t(STEP_NAVIGATION_KEYS.next)
103
+ )}
104
+ </Button>
105
+ </div>
106
+ </footer>
107
+ );
108
+ };