@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,190 +1,222 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
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
|
-
|
|
16
|
-
|
|
17
|
-
|
|
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
|
-
|
|
25
|
-
const
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
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="
|
|
85
|
-
<div className="
|
|
86
|
-
<h1 className="
|
|
87
|
-
|
|
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="
|
|
90
|
-
|
|
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
|
-
|
|
95
|
-
|
|
96
|
-
|
|
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
|
-
"
|
|
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
|
-
|
|
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
|
-
"
|
|
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
|
-
|
|
146
|
+
{t(ONBOARDING_KEYS.plan.yearly)}
|
|
118
147
|
<span
|
|
119
148
|
className={cn(
|
|
120
|
-
"
|
|
121
|
-
state.billingCycle === "yearly"
|
|
149
|
+
"udb-cycle-toggle__save",
|
|
150
|
+
state.billingCycle === "yearly" && "udb-cycle-toggle__save--on",
|
|
122
151
|
)}
|
|
123
152
|
>
|
|
124
|
-
|
|
153
|
+
{t(ONBOARDING_KEYS.plan.save, { percent: yearlyDiscountPercent })}
|
|
125
154
|
</span>
|
|
126
155
|
</button>
|
|
127
156
|
</div>
|
|
128
157
|
</div>
|
|
129
158
|
|
|
130
|
-
|
|
131
|
-
|
|
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
|
-
"
|
|
142
|
-
isSelected
|
|
143
|
-
|
|
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="
|
|
150
|
-
|
|
183
|
+
<div className="udb-plan__recommended">
|
|
184
|
+
{t(ONBOARDING_KEYS.plan.recommended)}
|
|
151
185
|
</div>
|
|
152
186
|
)}
|
|
153
187
|
|
|
154
|
-
<div className="
|
|
155
|
-
<h3 className="
|
|
156
|
-
{
|
|
157
|
-
<div
|
|
158
|
-
|
|
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="
|
|
197
|
+
<p className="udb-plan__description">{plan.description}</p>
|
|
169
198
|
|
|
170
|
-
<div className="
|
|
171
|
-
<span className="
|
|
199
|
+
<div className="udb-plan__price-row">
|
|
200
|
+
<span className="udb-plan__price">
|
|
172
201
|
${Math.round(planPrice)}
|
|
173
202
|
</span>
|
|
174
|
-
<span className="
|
|
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="
|
|
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="
|
|
184
|
-
<
|
|
185
|
-
<Check className="
|
|
186
|
-
</
|
|
187
|
-
<span className={cn("text
|
|
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
|
-
{
|
|
200
|
-
<p className="
|
|
201
|
-
|
|
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
|
-
|
|
2
|
-
|
|
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
|
-
|
|
5
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
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
|
-
|
|
29
|
-
const
|
|
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="
|
|
53
|
-
<div className="
|
|
54
|
-
<h1 className="
|
|
55
|
-
|
|
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="
|
|
58
|
-
|
|
68
|
+
<p className="udb-step__description">
|
|
69
|
+
{t(ONBOARDING_KEYS.platforms.description)}
|
|
59
70
|
</p>
|
|
60
71
|
</div>
|
|
61
72
|
|
|
62
|
-
<div
|
|
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
|
-
"
|
|
72
|
-
isConnected
|
|
88
|
+
"udb-platform",
|
|
89
|
+
isConnected && "udb-platform--connected",
|
|
73
90
|
)}
|
|
74
91
|
>
|
|
75
|
-
<
|
|
76
|
-
className=
|
|
77
|
-
|
|
78
|
-
platform.color
|
|
79
|
-
)}
|
|
92
|
+
<span
|
|
93
|
+
className="udb-platform__icon-box"
|
|
94
|
+
style={{ background: platform.color }}
|
|
80
95
|
>
|
|
81
|
-
|
|
82
|
-
</
|
|
96
|
+
<Icon className="udb-platform__icon" aria-hidden="true" />
|
|
97
|
+
</span>
|
|
83
98
|
|
|
84
|
-
<
|
|
85
|
-
<span className="
|
|
99
|
+
<span className="udb-platform__meta">
|
|
100
|
+
<span className="udb-platform__name">{platform.name}</span>
|
|
86
101
|
<span
|
|
87
102
|
className={cn(
|
|
88
|
-
"
|
|
89
|
-
isConnected
|
|
103
|
+
"udb-platform__state",
|
|
104
|
+
isConnected && "udb-platform__state--on",
|
|
90
105
|
)}
|
|
91
106
|
>
|
|
92
|
-
{isConnected
|
|
107
|
+
{isConnected
|
|
108
|
+
? t(ONBOARDING_KEYS.platforms.connected)
|
|
109
|
+
: t(ONBOARDING_KEYS.platforms.connect)}
|
|
93
110
|
</span>
|
|
94
|
-
</
|
|
111
|
+
</span>
|
|
95
112
|
</button>
|
|
96
113
|
);
|
|
97
114
|
})}
|
|
98
115
|
</div>
|
|
99
116
|
|
|
100
|
-
{
|
|
101
|
-
<p className="
|
|
102
|
-
|
|
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
|
+
};
|