@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.
- package/package.json +3 -10
- package/src/{domain/config → config}/DashboardConfig.ts +1 -1
- package/src/config/index.ts +1 -0
- package/src/{domain/config/CalendarConfig.ts → domains/calendar/config.ts} +1 -1
- package/src/domains/calendar/hooks/useCalendar.ts +1 -1
- package/src/domains/calendar/index.ts +1 -0
- package/src/domains/calendar/utils/index.ts +0 -6
- package/src/domains/layouts/components/DashboardHeader.tsx +21 -13
- package/src/domains/layouts/types/index.ts +1 -1
- package/src/domains/layouts/types/layout.ts +18 -0
- package/src/index.ts +2 -14
- package/src/styles/dashboard.css +1 -652
- package/src/domain/config/index.ts +0 -6
- package/src/domains/onboarding/components/AppFocusStep.tsx +0 -144
- package/src/domains/onboarding/components/OnboardingWizard.tsx +0 -188
- package/src/domains/onboarding/components/PlanStep.tsx +0 -241
- package/src/domains/onboarding/components/PlatformsStep.tsx +0 -129
- package/src/domains/onboarding/components/StepNavigation.tsx +0 -108
- package/src/domains/onboarding/components/StepProgress.tsx +0 -62
- package/src/domains/onboarding/components/UserTypeStep.tsx +0 -114
- package/src/domains/onboarding/components/index.ts +0 -13
- package/src/domains/onboarding/hooks/index.ts +0 -5
- package/src/domains/onboarding/hooks/useOnboarding.ts +0 -156
- package/src/domains/onboarding/index.ts +0 -36
- package/src/domains/onboarding/types/index.ts +0 -16
- package/src/domains/onboarding/types/onboarding.ts +0 -254
- package/src/domains/onboarding/utils/i18nKeys.ts +0 -72
- package/src/domains/onboarding/utils/index.ts +0 -17
- 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;
|