@umituz/web-dashboard 4.6.5 → 4.7.1

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 (29) hide show
  1. package/package.json +3 -10
  2. package/src/{domain/config → config}/DashboardConfig.ts +1 -1
  3. package/src/config/index.ts +1 -0
  4. package/src/{domain/config/CalendarConfig.ts → domains/calendar/config.ts} +1 -1
  5. package/src/domains/calendar/hooks/useCalendar.ts +1 -1
  6. package/src/domains/calendar/index.ts +1 -0
  7. package/src/domains/calendar/utils/index.ts +0 -6
  8. package/src/domains/layouts/components/DashboardHeader.tsx +21 -13
  9. package/src/domains/layouts/types/index.ts +1 -1
  10. package/src/domains/layouts/types/layout.ts +18 -0
  11. package/src/index.ts +2 -14
  12. package/src/styles/dashboard.css +1 -652
  13. package/src/domain/config/index.ts +0 -6
  14. package/src/domains/onboarding/components/AppFocusStep.tsx +0 -144
  15. package/src/domains/onboarding/components/OnboardingWizard.tsx +0 -188
  16. package/src/domains/onboarding/components/PlanStep.tsx +0 -241
  17. package/src/domains/onboarding/components/PlatformsStep.tsx +0 -129
  18. package/src/domains/onboarding/components/StepNavigation.tsx +0 -108
  19. package/src/domains/onboarding/components/StepProgress.tsx +0 -62
  20. package/src/domains/onboarding/components/UserTypeStep.tsx +0 -114
  21. package/src/domains/onboarding/components/index.ts +0 -13
  22. package/src/domains/onboarding/hooks/index.ts +0 -5
  23. package/src/domains/onboarding/hooks/useOnboarding.ts +0 -156
  24. package/src/domains/onboarding/index.ts +0 -36
  25. package/src/domains/onboarding/types/index.ts +0 -16
  26. package/src/domains/onboarding/types/onboarding.ts +0 -254
  27. package/src/domains/onboarding/utils/i18nKeys.ts +0 -72
  28. package/src/domains/onboarding/utils/index.ts +0 -17
  29. package/src/domains/onboarding/utils/onboarding.ts +0 -166
@@ -1,144 +0,0 @@
1
- /**
2
- * AppFocusStep
3
- *
4
- * Second step of onboarding: which platforms is the user building for.
5
- * Icons are Lucide components (no emoji). State mapping is
6
- * config-driven: each option declares which state field it toggles.
7
- */
8
-
9
- import { Check, Smartphone, Monitor, Sparkles } from "lucide-react";
10
- import { useT } from "../../../i18n";
11
- import { cn } from "../../../primitives";
12
- import type {
13
- OnboardingState,
14
- AppTypeOption,
15
- } from "../types/onboarding";
16
- import { ONBOARDING_KEYS } from "../utils/i18nKeys";
17
-
18
- export interface AppFocusStepProps {
19
- state: OnboardingState;
20
- updateState: (updates: Partial<OnboardingState>) => void;
21
- appTypes?: AppTypeOption[];
22
- }
23
-
24
- /**
25
- * Each option toggles a `stateField` of OnboardingState. This keeps the
26
- * wizard data-driven — adding a new platform doesn't require a code
27
- * change in this file. Consumer-supplied options may omit `stateField`;
28
- * it is then derived from the well-known ids below.
29
- */
30
- type AppFocusStateField = NonNullable<AppTypeOption["stateField"]>;
31
-
32
- const DEFAULT_STATE_FIELD_BY_ID: Record<string, AppFocusStateField> = {
33
- mobile: "hasMobileApp",
34
- web: "hasWebApp",
35
- };
36
-
37
- const DEFAULT_OPTIONS: AppTypeOption[] = [
38
- {
39
- id: "mobile",
40
- name: "Mobile App",
41
- description: "iOS or Android application",
42
- icon: Smartphone,
43
- stateField: "hasMobileApp",
44
- },
45
- {
46
- id: "web",
47
- name: "Web App",
48
- description: "Web application or website",
49
- icon: Monitor,
50
- stateField: "hasWebApp",
51
- },
52
- ];
53
-
54
- export const AppFocusStep = ({
55
- state,
56
- updateState,
57
- appTypes,
58
- }: AppFocusStepProps) => {
59
- const t = useT();
60
-
61
- const resolveStateField = (option: AppTypeOption): AppFocusStateField | undefined =>
62
- option.stateField ?? DEFAULT_STATE_FIELD_BY_ID[option.id];
63
-
64
- const types = (appTypes ?? DEFAULT_OPTIONS).map((option) => ({
65
- ...option,
66
- resolvedField: resolveStateField(option),
67
- }));
68
-
69
- const toggle = (option: AppTypeOption, field: AppFocusStateField) => {
70
- const current = Boolean(state[field]);
71
- updateState({ [field]: !current } as Partial<OnboardingState>);
72
- };
73
-
74
- const anySelected = types.some((type) =>
75
- type.resolvedField ? Boolean(state[type.resolvedField]) : false,
76
- );
77
-
78
- return (
79
- <div className="udb-step">
80
- <div className="udb-step__header">
81
- <h1 className="udb-step__title">
82
- {t(ONBOARDING_KEYS.appFocus.title)}
83
- </h1>
84
- <p className="udb-step__description">
85
- {t(ONBOARDING_KEYS.appFocus.description)}
86
- </p>
87
- </div>
88
-
89
- <div className="udb-step__options udb-step__options--list" role="group" aria-label={t(ONBOARDING_KEYS.appFocus.title)}>
90
- {types.map((type) => {
91
- const isSelected = type.resolvedField
92
- ? Boolean(state[type.resolvedField])
93
- : false;
94
- const Icon = type.icon;
95
- return (
96
- <button
97
- key={type.id}
98
- type="button"
99
- aria-pressed={isSelected}
100
- onClick={() => type.resolvedField && toggle(type, type.resolvedField)}
101
- className={cn(
102
- "udb-choice",
103
- isSelected && "udb-choice--selected",
104
- )}
105
- >
106
- <span
107
- className={cn(
108
- "udb-choice__icon-box udb-choice__icon-box--lg",
109
- isSelected && "udb-choice__icon-box--on",
110
- )}
111
- >
112
- <Icon className="udb-choice__icon udb-choice__icon--lg" aria-hidden="true" />
113
- </span>
114
-
115
- <span className="udb-choice__body">
116
- <span className="udb-choice__label udb-choice__label--lg">{type.name}</span>
117
- <span className="udb-choice__description">{type.description}</span>
118
- </span>
119
-
120
- <span
121
- className={cn(
122
- "udb-choice__check",
123
- isSelected && "udb-choice__check--on",
124
- )}
125
- aria-hidden="true"
126
- >
127
- {isSelected && <Check className="udb-choice__check-icon" />}
128
- </span>
129
- </button>
130
- );
131
- })}
132
- </div>
133
-
134
- {anySelected && (
135
- <p className="udb-step__hint" role="status" aria-live="polite">
136
- <Sparkles className="udb-step__hint-icon" aria-hidden="true" />
137
- {t(ONBOARDING_KEYS.appFocus.success)}
138
- </p>
139
- )}
140
- </div>
141
- );
142
- };
143
-
144
- export default AppFocusStep;
@@ -1,188 +0,0 @@
1
- /**
2
- * Onboarding Wizard
3
- *
4
- * Multi-step onboarding container. Delegates state, navigation, and
5
- * progress to `useOnboarding` so there's a single source of truth
6
- * for the wizard's lifecycle. Styling is entirely `udb-onboarding*`
7
- * classes from the package stylesheet.
8
- */
9
-
10
- import { useCallback, useMemo, useState } from "react";
11
- import { useNavigate } from "react-router-dom";
12
- import { useT } from "../../../i18n";
13
- import { Button } from "../../../primitives";
14
- import { useOnboarding } from "../hooks/useOnboarding";
15
- import { StepProgress } from "./StepProgress";
16
- import { StepNavigation } from "./StepNavigation";
17
- import type {
18
- OnboardingWizardProps,
19
- OnboardingError,
20
- } from "../types/onboarding";
21
-
22
- /**
23
- * Onboarding Wizard Component
24
- */
25
- export const OnboardingWizard = ({
26
- config,
27
- initialState,
28
- onComplete,
29
- onCancel,
30
- }: OnboardingWizardProps) => {
31
- const navigate = useNavigate();
32
- const t = useT();
33
- const [saving, setSaving] = useState(false);
34
- const [error, setError] = useState<OnboardingError | null>(null);
35
-
36
- const {
37
- state,
38
- currentStep,
39
- totalSteps,
40
- isLastStep,
41
- goToNext,
42
- goToPrev,
43
- goToStep,
44
- updateState,
45
- } = useOnboarding(config, initialState);
46
-
47
- const completedSteps = useMemo(() => {
48
- if (currentStep <= 1) return [] as number[];
49
- return Array.from({ length: currentStep - 1 }, (_, i) => i + 1);
50
- }, [currentStep]);
51
-
52
- const validateCurrentStep = useCallback((): boolean => {
53
- const currentStepConfig = config.steps[currentStep - 1];
54
- return currentStepConfig?.validate ? currentStepConfig.validate(state) : true;
55
- }, [config.steps, currentStep, state]);
56
-
57
- const handleNext = useCallback(async () => {
58
- if (!validateCurrentStep()) {
59
- return;
60
- }
61
-
62
- if (isLastStep) {
63
- setSaving(true);
64
- setError(null);
65
- try {
66
- await onComplete?.(state);
67
- navigate(config.completeRoute);
68
- } catch (err) {
69
- // Surface the failure to the user — silently logging is
70
- // forbidden on critical paths.
71
- setError({
72
- step: currentStep,
73
- message: err instanceof Error ? err.message : 'onboarding.errors.completionFailed',
74
- });
75
- } finally {
76
- setSaving(false);
77
- }
78
- return;
79
- }
80
-
81
- goToNext();
82
- }, [
83
- config.completeRoute,
84
- currentStep,
85
- goToNext,
86
- isLastStep,
87
- navigate,
88
- onComplete,
89
- state,
90
- validateCurrentStep,
91
- ]);
92
-
93
- const handlePrev = useCallback(() => {
94
- goToPrev();
95
- }, [goToPrev]);
96
-
97
- const handleCancel = useCallback(() => {
98
- onCancel?.();
99
- navigate(config.cancelRoute ?? "/");
100
- }, [config.cancelRoute, navigate, onCancel]);
101
-
102
- const handleSkip = useCallback(() => {
103
- if (!isLastStep) {
104
- handleNext();
105
- }
106
- }, [handleNext, isLastStep]);
107
-
108
- const currentStepConfig = config.steps[currentStep - 1];
109
- const StepContent = currentStepConfig?.component;
110
-
111
- return (
112
- <div className="udb-root udb-onboarding">
113
- <header className="udb-onboarding__header">
114
- <div className="udb-onboarding__brand">
115
- <span className="udb-onboarding__brand-name">{config.brandName}</span>
116
- </div>
117
-
118
- {config.showProgress !== false && (
119
- <StepProgress
120
- currentStep={currentStep}
121
- totalSteps={totalSteps}
122
- completedSteps={completedSteps}
123
- />
124
- )}
125
-
126
- <div className="udb-onboarding__header-actions">
127
- {config.allowCancel !== false && (
128
- <Button
129
- variant="ghost"
130
- size="sm"
131
- onClick={handleCancel}
132
- disabled={saving}
133
- >
134
- {t('onboarding.buttons.cancel')}
135
- </Button>
136
- )}
137
- </div>
138
- </header>
139
-
140
- <main className="udb-onboarding__main">
141
- <div className="udb-onboarding__content">
142
- {error && (
143
- <div
144
- role="alert"
145
- aria-live="polite"
146
- className="udb-onboarding__error"
147
- >
148
- {t(error.message)}
149
- </div>
150
- )}
151
-
152
- {StepContent ? (
153
- typeof StepContent === 'function' ? (
154
- <StepContent
155
- state={state}
156
- updateState={updateState}
157
- goToNext={goToNext}
158
- goToPrev={goToPrev}
159
- goToStep={goToStep}
160
- config={config}
161
- />
162
- ) : (
163
- StepContent
164
- )
165
- ) : (
166
- <div className="udb-onboarding__empty">
167
- {t('onboarding.errors.stepNotConfigured')}
168
- </div>
169
- )}
170
- </div>
171
- </main>
172
-
173
- <StepNavigation
174
- currentStep={currentStep}
175
- totalSteps={totalSteps}
176
- canGoNext={validateCurrentStep()}
177
- isSaving={saving}
178
- onNext={handleNext}
179
- onPrev={handlePrev}
180
- allowSkip={config.allowSkip}
181
- onSkip={handleSkip}
182
- translate={t}
183
- />
184
- </div>
185
- );
186
- };
187
-
188
- export default OnboardingWizard;
@@ -1,241 +0,0 @@
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";
14
- import type { OnboardingState, PlanOption } from "../types/onboarding";
15
- import { ONBOARDING_KEYS } from "../utils/i18nKeys";
16
-
17
- export interface PlanStepProps {
18
- state: OnboardingState;
19
- updateState: (updates: Partial<OnboardingState>) => void;
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;
25
- }
26
-
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
-
81
- export const PlanStep = ({
82
- state,
83
- updateState,
84
- plans,
85
- yearlyDiscountPercent = DEFAULT_YEARLY_DISCOUNT,
86
- trialDays = DEFAULT_TRIAL_DAYS,
87
- }: PlanStepProps) => {
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;
102
- };
103
-
104
- const selectedPlan = useMemo(
105
- () => planOptions.find((p) => p.id === state.selectedPlan) ?? null,
106
- [planOptions, state.selectedPlan],
107
- );
108
-
109
- return (
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)}
114
- </h1>
115
- <p className="udb-step__description udb-step__description--lg">
116
- {t(ONBOARDING_KEYS.plan.description, { days: trialDays })}
117
- </p>
118
- </div>
119
-
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
- >
126
- <button
127
- type="button"
128
- aria-pressed={state.billingCycle === "monthly"}
129
- onClick={() => updateState({ billingCycle: "monthly" })}
130
- className={cn(
131
- "udb-cycle-toggle__pill",
132
- state.billingCycle === "monthly" && "udb-cycle-toggle__pill--on",
133
- )}
134
- >
135
- {t(ONBOARDING_KEYS.plan.monthly)}
136
- </button>
137
- <button
138
- type="button"
139
- aria-pressed={state.billingCycle === "yearly"}
140
- onClick={() => updateState({ billingCycle: "yearly" })}
141
- className={cn(
142
- "udb-cycle-toggle__pill udb-cycle-toggle__pill--with-badge",
143
- state.billingCycle === "yearly" && "udb-cycle-toggle__pill--on",
144
- )}
145
- >
146
- {t(ONBOARDING_KEYS.plan.yearly)}
147
- <span
148
- className={cn(
149
- "udb-cycle-toggle__save",
150
- state.billingCycle === "yearly" && "udb-cycle-toggle__save--on",
151
- )}
152
- >
153
- {t(ONBOARDING_KEYS.plan.save, { percent: yearlyDiscountPercent })}
154
- </span>
155
- </button>
156
- </div>
157
- </div>
158
-
159
- <div
160
- className="udb-plans"
161
- role="radiogroup"
162
- aria-label={t(ONBOARDING_KEYS.plan.title)}
163
- >
164
- {planOptions.map((plan) => {
165
- const isSelected = state.selectedPlan === plan.id;
166
- const planPrice = getPlanPrice(plan);
167
- const badge = plan.badge ?? (plan.badgeKey ? t(plan.badgeKey) : undefined);
168
-
169
- return (
170
- <button
171
- key={plan.id}
172
- type="button"
173
- role="radio"
174
- aria-checked={isSelected}
175
- onClick={() => updateState({ selectedPlan: plan.id })}
176
- className={cn(
177
- "udb-plan",
178
- isSelected && "udb-plan--selected",
179
- plan.highlight && "udb-plan--highlight",
180
- )}
181
- >
182
- {plan.highlight && (
183
- <div className="udb-plan__recommended">
184
- {t(ONBOARDING_KEYS.plan.recommended)}
185
- </div>
186
- )}
187
-
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}
193
- </div>
194
- )}
195
- </div>
196
-
197
- <p className="udb-plan__description">{plan.description}</p>
198
-
199
- <div className="udb-plan__price-row">
200
- <span className="udb-plan__price">
201
- ${Math.round(planPrice)}
202
- </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>
208
- </div>
209
-
210
- <ul className="udb-plan__features">
211
- {plan.features.map((feature, i) => {
212
- const text = typeof feature === "string" ? feature : feature.text;
213
- const bold = typeof feature !== "string" && feature.bold;
214
- return (
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")}>
220
- {text}
221
- </span>
222
- </li>
223
- );
224
- })}
225
- </ul>
226
- </button>
227
- );
228
- })}
229
- </div>
230
-
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 })}
235
- </p>
236
- )}
237
- </div>
238
- );
239
- };
240
-
241
- export default PlanStep;
@@ -1,129 +0,0 @@
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
- */
8
-
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 {
24
- state: OnboardingState;
25
- updateState: (updates: Partial<OnboardingState>) => void;
26
- platforms?: PlatformOption[];
27
- }
28
-
29
- /**
30
- * Default platform set with brand-accurate Lucide icons and
31
- * brand colors (CSS color strings, no emoji).
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
-
43
- export const PlatformsStep = ({
44
- state,
45
- updateState,
46
- platforms,
47
- }: PlatformsStepProps) => {
48
- const t = useT();
49
- const platformOptions = platforms ?? DEFAULT_PLATFORMS;
50
-
51
- const togglePlatform = (id: string) => {
52
- const isConnected = state.connectedPlatforms.includes(id);
53
- updateState({
54
- connectedPlatforms: isConnected
55
- ? state.connectedPlatforms.filter((p) => p !== id)
56
- : [...state.connectedPlatforms, id],
57
- });
58
- };
59
-
60
- const selectedCount = state.connectedPlatforms.length;
61
-
62
- return (
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)}
67
- </h1>
68
- <p className="udb-step__description">
69
- {t(ONBOARDING_KEYS.platforms.description)}
70
- </p>
71
- </div>
72
-
73
- <div
74
- className="udb-platforms"
75
- role="group"
76
- aria-label={t(ONBOARDING_KEYS.platforms.title)}
77
- >
78
- {platformOptions.map((platform) => {
79
- const isConnected = state.connectedPlatforms.includes(platform.id);
80
- const Icon = platform.icon;
81
- return (
82
- <button
83
- key={platform.id}
84
- type="button"
85
- aria-pressed={isConnected}
86
- onClick={() => togglePlatform(platform.id)}
87
- className={cn(
88
- "udb-platform",
89
- isConnected && "udb-platform--connected",
90
- )}
91
- >
92
- <span
93
- className="udb-platform__icon-box"
94
- style={{ background: platform.color }}
95
- >
96
- <Icon className="udb-platform__icon" aria-hidden="true" />
97
- </span>
98
-
99
- <span className="udb-platform__meta">
100
- <span className="udb-platform__name">{platform.name}</span>
101
- <span
102
- className={cn(
103
- "udb-platform__state",
104
- isConnected && "udb-platform__state--on",
105
- )}
106
- >
107
- {isConnected
108
- ? t(ONBOARDING_KEYS.platforms.connected)
109
- : t(ONBOARDING_KEYS.platforms.connect)}
110
- </span>
111
- </span>
112
- </button>
113
- );
114
- })}
115
- </div>
116
-
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
- })}
123
- </p>
124
- )}
125
- </div>
126
- );
127
- };
128
-
129
- export default PlatformsStep;