@umituz/web-dashboard 2.0.6 → 2.0.8
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 +35 -77
- package/src/domains/layouts/components/BrandLogo.tsx +83 -0
- package/src/domains/layouts/components/DashboardHeader.tsx +240 -0
- package/src/domains/layouts/components/DashboardLayout.tsx +155 -0
- package/src/domains/layouts/components/DashboardSidebar.tsx +152 -0
- package/src/domains/layouts/components/index.ts +8 -0
- package/src/domains/layouts/hooks/dashboard.ts +81 -0
- package/src/domains/layouts/hooks/index.ts +8 -0
- package/src/domains/layouts/index.ts +11 -0
- package/{dist/layouts/theme/default.js → src/domains/layouts/theme/default.ts} +18 -11
- package/src/domains/layouts/theme/index.ts +18 -0
- package/src/domains/layouts/theme/presets.ts +96 -0
- package/src/domains/layouts/theme/utils.ts +67 -0
- package/src/domains/layouts/types/index.ts +9 -0
- package/src/domains/layouts/types/layout.ts +43 -0
- package/src/domains/layouts/types/notification.ts +19 -0
- package/src/domains/layouts/types/sidebar.ts +35 -0
- package/src/domains/layouts/types/theme.ts +64 -0
- package/src/domains/layouts/types/user.ts +35 -0
- package/src/domains/layouts/utils/dashboard.ts +96 -0
- package/src/domains/layouts/utils/index.ts +11 -0
- package/src/domains/onboarding/components/AppFocusStep.tsx +113 -0
- package/src/domains/onboarding/components/OnboardingWizard.tsx +262 -0
- package/src/domains/onboarding/components/PlanStep.tsx +208 -0
- package/src/domains/onboarding/components/PlatformsStep.tsx +109 -0
- package/src/domains/onboarding/components/UserTypeStep.tsx +135 -0
- package/src/domains/onboarding/components/index.ts +9 -0
- package/src/domains/onboarding/hooks/index.ts +5 -0
- package/{dist/onboarding/hooks/index.js → src/domains/onboarding/hooks/useOnboarding.ts} +65 -19
- package/src/domains/onboarding/index.ts +35 -0
- package/src/domains/onboarding/types/index.ts +16 -0
- package/src/domains/onboarding/types/onboarding.ts +214 -0
- package/src/domains/onboarding/utils/index.ts +15 -0
- package/src/domains/onboarding/utils/onboarding.ts +166 -0
- package/src/domains/settings/components/SettingsLayout.tsx +144 -0
- package/src/domains/settings/components/SettingsSection.tsx +106 -0
- package/src/domains/settings/components/index.ts +6 -0
- package/src/domains/settings/hooks/index.ts +7 -0
- package/src/domains/settings/hooks/useSettings.ts +80 -0
- package/src/domains/settings/index.ts +22 -0
- package/src/domains/settings/types/index.ts +11 -0
- package/src/domains/settings/types/settings.ts +81 -0
- package/src/domains/settings/utils/index.ts +11 -0
- package/src/domains/settings/utils/settings.ts +80 -0
- package/dist/layouts/components/BrandLogo.d.ts +0 -18
- package/dist/layouts/components/BrandLogo.js +0 -88
- package/dist/layouts/components/BrandLogo.js.map +0 -1
- package/dist/layouts/components/DashboardHeader.d.ts +0 -36
- package/dist/layouts/components/DashboardHeader.js +0 -225
- package/dist/layouts/components/DashboardHeader.js.map +0 -1
- package/dist/layouts/components/DashboardLayout.d.ts +0 -45
- package/dist/layouts/components/DashboardLayout.js +0 -501
- package/dist/layouts/components/DashboardLayout.js.map +0 -1
- package/dist/layouts/components/DashboardSidebar.d.ts +0 -29
- package/dist/layouts/components/DashboardSidebar.js +0 -189
- package/dist/layouts/components/DashboardSidebar.js.map +0 -1
- package/dist/layouts/components/index.d.ts +0 -10
- package/dist/layouts/components/index.js +0 -502
- package/dist/layouts/components/index.js.map +0 -1
- package/dist/layouts/hooks/dashboard.d.ts +0 -35
- package/dist/layouts/hooks/dashboard.js +0 -57
- package/dist/layouts/hooks/dashboard.js.map +0 -1
- package/dist/layouts/hooks/index.d.ts +0 -3
- package/dist/layouts/hooks/index.js +0 -57
- package/dist/layouts/hooks/index.js.map +0 -1
- package/dist/layouts/index.d.ts +0 -17
- package/dist/layouts/index.js +0 -756
- package/dist/layouts/index.js.map +0 -1
- package/dist/layouts/theme/default.d.ts +0 -18
- package/dist/layouts/theme/default.js.map +0 -1
- package/dist/layouts/theme/index.d.ts +0 -4
- package/dist/layouts/theme/index.js +0 -184
- package/dist/layouts/theme/index.js.map +0 -1
- package/dist/layouts/theme/presets.d.ts +0 -14
- package/dist/layouts/theme/presets.js +0 -137
- package/dist/layouts/theme/presets.js.map +0 -1
- package/dist/layouts/theme/utils.d.ts +0 -22
- package/dist/layouts/theme/utils.js +0 -181
- package/dist/layouts/theme/utils.js.map +0 -1
- package/dist/layouts/types/index.d.ts +0 -6
- package/dist/layouts/types/index.js +0 -2
- package/dist/layouts/types/index.js.map +0 -1
- package/dist/layouts/types/layout.d.ts +0 -45
- package/dist/layouts/types/layout.js +0 -2
- package/dist/layouts/types/layout.js.map +0 -1
- package/dist/layouts/types/notification.d.ts +0 -20
- package/dist/layouts/types/notification.js +0 -2
- package/dist/layouts/types/notification.js.map +0 -1
- package/dist/layouts/types/sidebar.d.ts +0 -36
- package/dist/layouts/types/sidebar.js +0 -2
- package/dist/layouts/types/sidebar.js.map +0 -1
- package/dist/layouts/types/theme.d.ts +0 -64
- package/dist/layouts/types/theme.js +0 -2
- package/dist/layouts/types/theme.js.map +0 -1
- package/dist/layouts/types/user.d.ts +0 -37
- package/dist/layouts/types/user.js +0 -2
- package/dist/layouts/types/user.js.map +0 -1
- package/dist/layouts/utils/dashboard.d.ts +0 -57
- package/dist/layouts/utils/dashboard.js +0 -44
- package/dist/layouts/utils/dashboard.js.map +0 -1
- package/dist/layouts/utils/index.d.ts +0 -1
- package/dist/layouts/utils/index.js +0 -44
- package/dist/layouts/utils/index.js.map +0 -1
- package/dist/onboarding/components/AppFocusStep.d.ts +0 -26
- package/dist/onboarding/components/AppFocusStep.js +0 -86
- package/dist/onboarding/components/AppFocusStep.js.map +0 -1
- package/dist/onboarding/components/OnboardingWizard.d.ts +0 -13
- package/dist/onboarding/components/OnboardingWizard.js +0 -332
- package/dist/onboarding/components/OnboardingWizard.js.map +0 -1
- package/dist/onboarding/components/PlanStep.d.ts +0 -21
- package/dist/onboarding/components/PlanStep.js +0 -167
- package/dist/onboarding/components/PlanStep.js.map +0 -1
- package/dist/onboarding/components/PlatformsStep.d.ts +0 -26
- package/dist/onboarding/components/PlatformsStep.js +0 -86
- package/dist/onboarding/components/PlatformsStep.js.map +0 -1
- package/dist/onboarding/components/UserTypeStep.d.ts +0 -30
- package/dist/onboarding/components/UserTypeStep.js +0 -93
- package/dist/onboarding/components/UserTypeStep.js.map +0 -1
- package/dist/onboarding/components/index.d.ts +0 -9
- package/dist/onboarding/components/index.js +0 -738
- package/dist/onboarding/components/index.js.map +0 -1
- package/dist/onboarding/hooks/index.d.ts +0 -4
- package/dist/onboarding/hooks/index.js.map +0 -1
- package/dist/onboarding/hooks/useOnboarding.d.ts +0 -50
- package/dist/onboarding/hooks/useOnboarding.js +0 -100
- package/dist/onboarding/hooks/useOnboarding.js.map +0 -1
- package/dist/onboarding/index.d.ts +0 -11
- package/dist/onboarding/index.js +0 -913
- package/dist/onboarding/index.js.map +0 -1
- package/dist/onboarding/types/index.d.ts +0 -3
- package/dist/onboarding/types/index.js +0 -2
- package/dist/onboarding/types/index.js.map +0 -1
- package/dist/onboarding/types/onboarding.d.ts +0 -209
- package/dist/onboarding/types/onboarding.js +0 -2
- package/dist/onboarding/types/onboarding.js.map +0 -1
- package/dist/onboarding/utils/index.d.ts +0 -4
- package/dist/onboarding/utils/index.js +0 -83
- package/dist/onboarding/utils/index.js.map +0 -1
- package/dist/onboarding/utils/onboarding.d.ts +0 -106
- package/dist/onboarding/utils/onboarding.js +0 -83
- package/dist/onboarding/utils/onboarding.js.map +0 -1
- package/dist/settings/components/SettingsLayout.d.ts +0 -19
- package/dist/settings/components/SettingsLayout.js +0 -170
- package/dist/settings/components/SettingsLayout.js.map +0 -1
- package/dist/settings/components/SettingsSection.d.ts +0 -24
- package/dist/settings/components/SettingsSection.js +0 -73
- package/dist/settings/components/SettingsSection.js.map +0 -1
- package/dist/settings/components/index.d.ts +0 -5
- package/dist/settings/components/index.js +0 -169
- package/dist/settings/components/index.js.map +0 -1
- package/dist/settings/hooks/index.d.ts +0 -3
- package/dist/settings/hooks/index.js +0 -59
- package/dist/settings/hooks/index.js.map +0 -1
- package/dist/settings/hooks/useSettings.d.ts +0 -25
- package/dist/settings/hooks/useSettings.js +0 -59
- package/dist/settings/hooks/useSettings.js.map +0 -1
- package/dist/settings/index.d.ts +0 -7
- package/dist/settings/index.js +0 -259
- package/dist/settings/index.js.map +0 -1
- package/dist/settings/types/index.d.ts +0 -2
- package/dist/settings/types/index.js +0 -2
- package/dist/settings/types/index.js.map +0 -1
- package/dist/settings/types/settings.d.ts +0 -79
- package/dist/settings/types/settings.js +0 -2
- package/dist/settings/types/settings.js.map +0 -1
- package/dist/settings/utils/index.d.ts +0 -3
- package/dist/settings/utils/index.js +0 -39
- package/dist/settings/utils/index.js.map +0 -1
- package/dist/settings/utils/settings.d.ts +0 -50
- package/dist/settings/utils/settings.js +0 -39
- package/dist/settings/utils/settings.js.map +0 -1
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dashboard Types - User
|
|
3
|
+
*
|
|
4
|
+
* Type definitions for user profile
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* User profile info for header
|
|
9
|
+
*/
|
|
10
|
+
export interface DashboardUser {
|
|
11
|
+
/** User ID */
|
|
12
|
+
id: string;
|
|
13
|
+
/** Display name */
|
|
14
|
+
name?: string;
|
|
15
|
+
/** Email address */
|
|
16
|
+
email?: string;
|
|
17
|
+
/** Avatar URL */
|
|
18
|
+
avatar?: string;
|
|
19
|
+
/** Whether user has mobile app access */
|
|
20
|
+
hasMobileApp?: boolean;
|
|
21
|
+
/** Whether user has web app access */
|
|
22
|
+
hasWebApp?: boolean;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Navigation item for user menu
|
|
27
|
+
*/
|
|
28
|
+
export interface UserNavMenuItem {
|
|
29
|
+
/** Display label */
|
|
30
|
+
label: string;
|
|
31
|
+
/** Icon component */
|
|
32
|
+
icon: React.ComponentType<{ className?: string }>;
|
|
33
|
+
/** Route path */
|
|
34
|
+
path: string;
|
|
35
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dashboard Utilities
|
|
3
|
+
*
|
|
4
|
+
* Utility functions for dashboard operations
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Format notification timestamp to relative time
|
|
9
|
+
*
|
|
10
|
+
* @param createdAt - Notification creation timestamp
|
|
11
|
+
* @param t - i18n translation function
|
|
12
|
+
* @returns Formatted time string
|
|
13
|
+
*/
|
|
14
|
+
export function formatNotificationTime(
|
|
15
|
+
createdAt: Date | string | number,
|
|
16
|
+
t: (key: string, params?: Record<string, unknown>) => string
|
|
17
|
+
): string {
|
|
18
|
+
const date = new Date(createdAt);
|
|
19
|
+
const secs = Math.floor((Date.now() - date.getTime()) / 1000);
|
|
20
|
+
|
|
21
|
+
if (secs < 60) return t("dashboard.activityFeed.times.justNow");
|
|
22
|
+
if (secs < 3600) return t("dashboard.activityFeed.times.minutesAgo", { val: Math.floor(secs / 60) });
|
|
23
|
+
if (secs < 86400) return t("dashboard.activityFeed.times.hoursAgo", { val: Math.floor(secs / 3600) });
|
|
24
|
+
return t("dashboard.activityFeed.times.daysAgo", { val: Math.floor(secs / 86400) });
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Check if a path is active
|
|
29
|
+
*
|
|
30
|
+
* @param currentPath - Current location pathname
|
|
31
|
+
* @param targetPath - Target path to check
|
|
32
|
+
* @returns True if paths match
|
|
33
|
+
*/
|
|
34
|
+
export function isPathActive(currentPath: string, targetPath: string): boolean {
|
|
35
|
+
return currentPath === targetPath;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Get page title from sidebar configuration
|
|
40
|
+
*
|
|
41
|
+
* @param pathname - Current pathname
|
|
42
|
+
* @param sidebarGroups - Sidebar groups configuration
|
|
43
|
+
* @param extraTitleMap - Extra title mappings
|
|
44
|
+
* @returns Page title or null
|
|
45
|
+
*/
|
|
46
|
+
export function getPageTitle(
|
|
47
|
+
pathname: string,
|
|
48
|
+
sidebarGroups: Array<{ items: Array<{ path: string; label: string }> }>,
|
|
49
|
+
extraTitleMap?: Record<string, string>
|
|
50
|
+
): string | null {
|
|
51
|
+
// Search in sidebar groups
|
|
52
|
+
for (const group of sidebarGroups) {
|
|
53
|
+
const item = group.items.find((i) => i.path === pathname);
|
|
54
|
+
if (item) return item.label;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Search in extra title map
|
|
58
|
+
if (extraTitleMap?.[pathname]) {
|
|
59
|
+
return extraTitleMap[pathname];
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Filter sidebar items by app type and enabled status
|
|
67
|
+
*
|
|
68
|
+
* @param items - Sidebar items to filter
|
|
69
|
+
* @param user - Current user object
|
|
70
|
+
* @returns Filtered items
|
|
71
|
+
*/
|
|
72
|
+
export function filterSidebarItems<T extends { enabled?: boolean; requiredApp?: "mobile" | "web" }>(
|
|
73
|
+
items: T[],
|
|
74
|
+
user?: { hasMobileApp?: boolean; hasWebApp?: boolean }
|
|
75
|
+
): T[] {
|
|
76
|
+
return items.filter((item) => {
|
|
77
|
+
// Skip disabled items
|
|
78
|
+
if (item.enabled === false) return false;
|
|
79
|
+
|
|
80
|
+
// Skip items that require specific app types
|
|
81
|
+
if (!item.requiredApp) return true;
|
|
82
|
+
if (item.requiredApp === "mobile") return user?.hasMobileApp ?? false;
|
|
83
|
+
if (item.requiredApp === "web") return user?.hasWebApp ?? false;
|
|
84
|
+
|
|
85
|
+
return true;
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Generate notification ID
|
|
91
|
+
*
|
|
92
|
+
* @returns Unique notification ID
|
|
93
|
+
*/
|
|
94
|
+
export function generateNotificationId(): string {
|
|
95
|
+
return crypto.randomUUID();
|
|
96
|
+
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { Check } from "lucide-react";
|
|
2
|
+
import { cn } from "@umituz/web-design-system/utils";
|
|
3
|
+
import type { OnboardingState } from "../types/onboarding";
|
|
4
|
+
|
|
5
|
+
interface AppFocusStepProps {
|
|
6
|
+
/** Current onboarding state */
|
|
7
|
+
state: OnboardingState;
|
|
8
|
+
/** Update state function */
|
|
9
|
+
updateState: (updates: Partial<OnboardingState>) => void;
|
|
10
|
+
/** Custom app type options */
|
|
11
|
+
appTypes?: Array<{
|
|
12
|
+
id: string;
|
|
13
|
+
label: string;
|
|
14
|
+
description: string;
|
|
15
|
+
icon: string;
|
|
16
|
+
}>;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* App Focus Selection Step
|
|
21
|
+
*
|
|
22
|
+
* Second step - select which app types to focus on (mobile/web)
|
|
23
|
+
*/
|
|
24
|
+
export const AppFocusStep = ({
|
|
25
|
+
state,
|
|
26
|
+
updateState,
|
|
27
|
+
appTypes,
|
|
28
|
+
}: AppFocusStepProps) => {
|
|
29
|
+
// Default app types
|
|
30
|
+
const defaultAppTypes = [
|
|
31
|
+
{
|
|
32
|
+
id: "mobile",
|
|
33
|
+
label: "Mobile App",
|
|
34
|
+
description: "iOS or Android application",
|
|
35
|
+
icon: "📱",
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
id: "web",
|
|
39
|
+
label: "Web App",
|
|
40
|
+
description: "Web application or website",
|
|
41
|
+
icon: "💻",
|
|
42
|
+
},
|
|
43
|
+
];
|
|
44
|
+
|
|
45
|
+
const types = appTypes || defaultAppTypes;
|
|
46
|
+
|
|
47
|
+
return (
|
|
48
|
+
<div className="w-full max-w-xl">
|
|
49
|
+
<div className="text-center mb-10">
|
|
50
|
+
<h1 className="text-3xl md:text-4xl font-extrabold text-foreground mb-3">
|
|
51
|
+
What type of app are you building?
|
|
52
|
+
</h1>
|
|
53
|
+
<p className="text-muted-foreground">
|
|
54
|
+
Select all that apply - you can change this later
|
|
55
|
+
</p>
|
|
56
|
+
</div>
|
|
57
|
+
|
|
58
|
+
<div className="grid grid-cols-1 gap-4">
|
|
59
|
+
{types.map((type) => {
|
|
60
|
+
const isSelected = type.id === "mobile" ? state.hasMobileApp : state.hasWebApp;
|
|
61
|
+
|
|
62
|
+
return (
|
|
63
|
+
<button
|
|
64
|
+
key={type.id}
|
|
65
|
+
onClick={() => {
|
|
66
|
+
if (type.id === "mobile") {
|
|
67
|
+
updateState({ hasMobileApp: !state.hasMobileApp });
|
|
68
|
+
} else {
|
|
69
|
+
updateState({ hasWebApp: !state.hasWebApp });
|
|
70
|
+
}
|
|
71
|
+
}}
|
|
72
|
+
className={cn(
|
|
73
|
+
"w-full flex items-center gap-4 p-6 rounded-2xl border bg-background text-left transition-all",
|
|
74
|
+
isSelected ? "border-primary ring-2 ring-primary/20 bg-primary/5" : "border-border hover:border-primary/40"
|
|
75
|
+
)}
|
|
76
|
+
>
|
|
77
|
+
<div
|
|
78
|
+
className={cn(
|
|
79
|
+
"w-12 h-12 rounded-2xl bg-muted flex items-center justify-center text-2xl shrink-0 transition-transform",
|
|
80
|
+
isSelected && "scale-110"
|
|
81
|
+
)}
|
|
82
|
+
>
|
|
83
|
+
{type.icon}
|
|
84
|
+
</div>
|
|
85
|
+
|
|
86
|
+
<div className="flex-1">
|
|
87
|
+
<p className="font-bold text-foreground text-lg">{type.label}</p>
|
|
88
|
+
<p className="text-sm text-muted-foreground">{type.description}</p>
|
|
89
|
+
</div>
|
|
90
|
+
|
|
91
|
+
<div
|
|
92
|
+
className={cn(
|
|
93
|
+
"w-6 h-6 rounded-full border-2 flex items-center justify-center shrink-0 transition-colors",
|
|
94
|
+
isSelected ? "border-primary bg-primary text-white" : "border-muted-foreground/30"
|
|
95
|
+
)}
|
|
96
|
+
>
|
|
97
|
+
{isSelected && <Check className="h-4 w-4" />}
|
|
98
|
+
</div>
|
|
99
|
+
</button>
|
|
100
|
+
);
|
|
101
|
+
})}
|
|
102
|
+
</div>
|
|
103
|
+
|
|
104
|
+
{(state.hasMobileApp || state.hasWebApp) && (
|
|
105
|
+
<p className="mt-8 text-center text-sm text-muted-foreground animate-in fade-in slide-in-from-bottom-2 duration-500">
|
|
106
|
+
✨ Great choice! We'll tailor your experience accordingly.
|
|
107
|
+
</p>
|
|
108
|
+
)}
|
|
109
|
+
</div>
|
|
110
|
+
);
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
export default AppFocusStep;
|
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
import { useState, useCallback } from "react";
|
|
2
|
+
import { useNavigate } from "react-router-dom";
|
|
3
|
+
import { Check, Loader2 } from "lucide-react";
|
|
4
|
+
import { cn } from "@umituz/web-design-system/utils";
|
|
5
|
+
import { Button } from "@umituz/web-design-system/atoms";
|
|
6
|
+
import { BrandLogo } from "../../layouts/components";
|
|
7
|
+
import type {
|
|
8
|
+
OnboardingConfig,
|
|
9
|
+
OnboardingState,
|
|
10
|
+
OnboardingWizardProps,
|
|
11
|
+
StepProgressProps,
|
|
12
|
+
StepNavigationProps,
|
|
13
|
+
} from "../types/onboarding";
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Step Progress Component
|
|
17
|
+
*/
|
|
18
|
+
const StepProgress = ({ currentStep, totalSteps, completedSteps = [] }: StepProgressProps) => {
|
|
19
|
+
return (
|
|
20
|
+
<div className="flex items-center gap-0 flex-1 justify-center">
|
|
21
|
+
{Array.from({ length: totalSteps }, (_, i) => i + 1).map((step) => {
|
|
22
|
+
const isCompleted = completedSteps.includes(step) || step < currentStep;
|
|
23
|
+
const isCurrent = step === currentStep;
|
|
24
|
+
|
|
25
|
+
return (
|
|
26
|
+
<div key={step} className="flex items-center">
|
|
27
|
+
<div
|
|
28
|
+
className={cn(
|
|
29
|
+
"w-8 h-8 rounded-full flex items-center justify-center text-sm font-semibold transition-colors",
|
|
30
|
+
isCompleted || isCurrent
|
|
31
|
+
? "bg-primary text-primary-foreground"
|
|
32
|
+
: "bg-muted text-muted-foreground"
|
|
33
|
+
)}
|
|
34
|
+
>
|
|
35
|
+
{isCompleted ? <Check className="h-4 w-4" /> : step}
|
|
36
|
+
</div>
|
|
37
|
+
{step < totalSteps && (
|
|
38
|
+
<div
|
|
39
|
+
className={cn(
|
|
40
|
+
"w-12 h-0.5 mx-1",
|
|
41
|
+
step < currentStep ? "bg-primary" : "bg-border"
|
|
42
|
+
)}
|
|
43
|
+
/>
|
|
44
|
+
)}
|
|
45
|
+
</div>
|
|
46
|
+
);
|
|
47
|
+
})}
|
|
48
|
+
</div>
|
|
49
|
+
);
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Step Navigation Component
|
|
54
|
+
*/
|
|
55
|
+
const StepNavigation = ({
|
|
56
|
+
currentStep,
|
|
57
|
+
totalSteps,
|
|
58
|
+
canGoNext,
|
|
59
|
+
isSaving = false,
|
|
60
|
+
nextLabel,
|
|
61
|
+
prevLabel,
|
|
62
|
+
onNext,
|
|
63
|
+
onPrev,
|
|
64
|
+
allowSkip = false,
|
|
65
|
+
onSkip,
|
|
66
|
+
}: StepNavigationProps) => {
|
|
67
|
+
const t = (key: string) => key; // Simple i18n fallback
|
|
68
|
+
|
|
69
|
+
return (
|
|
70
|
+
<footer className="bg-background border-t border-border px-8 py-6 flex items-center justify-between">
|
|
71
|
+
{currentStep > 1 ? (
|
|
72
|
+
<Button
|
|
73
|
+
variant="ghost"
|
|
74
|
+
onClick={onPrev}
|
|
75
|
+
className="rounded-full px-6"
|
|
76
|
+
disabled={isSaving}
|
|
77
|
+
>
|
|
78
|
+
← {prevLabel || t("onboarding.buttons.back")}
|
|
79
|
+
</Button>
|
|
80
|
+
) : (
|
|
81
|
+
<div />
|
|
82
|
+
)}
|
|
83
|
+
|
|
84
|
+
<div className="flex items-center gap-3">
|
|
85
|
+
{allowSkip && onSkip && currentStep < totalSteps && (
|
|
86
|
+
<Button
|
|
87
|
+
variant="ghost"
|
|
88
|
+
onClick={onSkip}
|
|
89
|
+
className="rounded-full px-6"
|
|
90
|
+
disabled={isSaving}
|
|
91
|
+
>
|
|
92
|
+
{t("onboarding.buttons.skip")}
|
|
93
|
+
</Button>
|
|
94
|
+
)}
|
|
95
|
+
|
|
96
|
+
<Button
|
|
97
|
+
onClick={onNext}
|
|
98
|
+
className="rounded-full px-12 h-12 text-base font-bold"
|
|
99
|
+
disabled={!canGoNext || isSaving}
|
|
100
|
+
>
|
|
101
|
+
{isSaving ? (
|
|
102
|
+
<>
|
|
103
|
+
<Loader2 className="h-4 w-4 mr-2 animate-spin" />
|
|
104
|
+
{t("onboarding.buttons.finalizing")}
|
|
105
|
+
</>
|
|
106
|
+
) : currentStep === totalSteps ? (
|
|
107
|
+
nextLabel || t("onboarding.buttons.getStarted")
|
|
108
|
+
) : (
|
|
109
|
+
t("onboarding.buttons.next")
|
|
110
|
+
)}
|
|
111
|
+
</Button>
|
|
112
|
+
</div>
|
|
113
|
+
</footer>
|
|
114
|
+
);
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Onboarding Wizard Component
|
|
119
|
+
*
|
|
120
|
+
* Main onboarding container with step management
|
|
121
|
+
*/
|
|
122
|
+
export const OnboardingWizard = ({
|
|
123
|
+
config,
|
|
124
|
+
initialState,
|
|
125
|
+
onComplete,
|
|
126
|
+
onCancel,
|
|
127
|
+
}: OnboardingWizardProps) => {
|
|
128
|
+
const navigate = useNavigate();
|
|
129
|
+
const [currentStep, setCurrentStep] = useState(1);
|
|
130
|
+
const [saving, setSaving] = useState(false);
|
|
131
|
+
const [completedSteps, setCompletedSteps] = useState<number[]>([]);
|
|
132
|
+
|
|
133
|
+
// State management
|
|
134
|
+
const [state, setState] = useState<OnboardingState>({
|
|
135
|
+
currentStep: 1,
|
|
136
|
+
connectedPlatforms: [],
|
|
137
|
+
billingCycle: "monthly",
|
|
138
|
+
stepData: {},
|
|
139
|
+
...initialState,
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
const totalSteps = config.steps.length;
|
|
143
|
+
|
|
144
|
+
// Validate current step
|
|
145
|
+
const canGoNext = useCallback(() => {
|
|
146
|
+
const currentStepConfig = config.steps[currentStep - 1];
|
|
147
|
+
if (currentStepConfig?.validate) {
|
|
148
|
+
return currentStepConfig.validate(state);
|
|
149
|
+
}
|
|
150
|
+
return true;
|
|
151
|
+
}, [config.steps, currentStep, state]);
|
|
152
|
+
|
|
153
|
+
// Navigation handlers
|
|
154
|
+
const goToNext = useCallback(async () => {
|
|
155
|
+
if (currentStep < totalSteps) {
|
|
156
|
+
setCompletedSteps((prev) => [...prev, currentStep]);
|
|
157
|
+
setCurrentStep((prev) => prev + 1);
|
|
158
|
+
setState((prev) => ({ ...prev, currentStep: prev.currentStep + 1 }));
|
|
159
|
+
} else {
|
|
160
|
+
// Complete onboarding
|
|
161
|
+
setSaving(true);
|
|
162
|
+
try {
|
|
163
|
+
await onComplete?.(state);
|
|
164
|
+
navigate(config.completeRoute);
|
|
165
|
+
} catch (error) {
|
|
166
|
+
console.error("Onboarding completion error:", error);
|
|
167
|
+
setSaving(false);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}, [currentStep, totalSteps, state, onComplete, navigate, config.completeRoute]);
|
|
171
|
+
|
|
172
|
+
const goToPrev = useCallback(() => {
|
|
173
|
+
if (currentStep > 1) {
|
|
174
|
+
setCurrentStep((prev) => prev - 1);
|
|
175
|
+
setState((prev) => ({ ...prev, currentStep: prev.currentStep - 1 }));
|
|
176
|
+
}
|
|
177
|
+
}, [currentStep]);
|
|
178
|
+
|
|
179
|
+
const goToStep = useCallback((step: number) => {
|
|
180
|
+
if (step >= 1 && step <= totalSteps) {
|
|
181
|
+
setCurrentStep(step);
|
|
182
|
+
setState((prev) => ({ ...prev, currentStep: step }));
|
|
183
|
+
}
|
|
184
|
+
}, [totalSteps]);
|
|
185
|
+
|
|
186
|
+
const updateState = useCallback((updates: Partial<OnboardingState>) => {
|
|
187
|
+
setState((prev) => ({ ...prev, ...updates }));
|
|
188
|
+
}, []);
|
|
189
|
+
|
|
190
|
+
const handleCancel = useCallback(() => {
|
|
191
|
+
onCancel?.();
|
|
192
|
+
navigate(config.cancelRoute || "/");
|
|
193
|
+
}, [onCancel, navigate, config.cancelRoute]);
|
|
194
|
+
|
|
195
|
+
const handleSkip = useCallback(() => {
|
|
196
|
+
if (currentStep < totalSteps) {
|
|
197
|
+
goToNext();
|
|
198
|
+
}
|
|
199
|
+
}, [currentStep, totalSteps, goToNext]);
|
|
200
|
+
|
|
201
|
+
// Render current step content
|
|
202
|
+
const currentStepConfig = config.steps[currentStep - 1];
|
|
203
|
+
const StepContent = currentStepConfig?.component;
|
|
204
|
+
|
|
205
|
+
return (
|
|
206
|
+
<div className="min-h-screen bg-secondary flex flex-col">
|
|
207
|
+
{/* Header */}
|
|
208
|
+
<header className="bg-background border-b border-border px-6 py-3 flex items-center">
|
|
209
|
+
<div className="flex items-center gap-2 mr-8">
|
|
210
|
+
<BrandLogo size={28} />
|
|
211
|
+
<span className="font-bold text-foreground">{config.brandName}</span>
|
|
212
|
+
</div>
|
|
213
|
+
|
|
214
|
+
{config.showProgress !== false && (
|
|
215
|
+
<StepProgress
|
|
216
|
+
currentStep={currentStep}
|
|
217
|
+
totalSteps={totalSteps}
|
|
218
|
+
completedSteps={completedSteps}
|
|
219
|
+
/>
|
|
220
|
+
)}
|
|
221
|
+
|
|
222
|
+
<div className="w-24" />
|
|
223
|
+
</header>
|
|
224
|
+
|
|
225
|
+
{/* Main Content */}
|
|
226
|
+
<main className="flex-1 flex flex-col items-center justify-center px-4 py-12">
|
|
227
|
+
<div className="w-full max-w-4xl">
|
|
228
|
+
{StepContent ? (
|
|
229
|
+
// Check if it's a function component (has call signature)
|
|
230
|
+
typeof StepContent === 'function' ? (
|
|
231
|
+
<StepContent
|
|
232
|
+
state={state}
|
|
233
|
+
updateState={updateState}
|
|
234
|
+
goToNext={goToNext}
|
|
235
|
+
goToPrev={goToPrev}
|
|
236
|
+
goToStep={goToStep}
|
|
237
|
+
config={config}
|
|
238
|
+
/>
|
|
239
|
+
) : (
|
|
240
|
+
// Otherwise it's a ReactElement
|
|
241
|
+
StepContent
|
|
242
|
+
)
|
|
243
|
+
) : null}
|
|
244
|
+
</div>
|
|
245
|
+
</main>
|
|
246
|
+
|
|
247
|
+
{/* Footer Navigation */}
|
|
248
|
+
<StepNavigation
|
|
249
|
+
currentStep={currentStep}
|
|
250
|
+
totalSteps={totalSteps}
|
|
251
|
+
canGoNext={canGoNext()}
|
|
252
|
+
isSaving={saving}
|
|
253
|
+
onNext={goToNext}
|
|
254
|
+
onPrev={goToPrev}
|
|
255
|
+
allowSkip={config.allowSkip}
|
|
256
|
+
onSkip={handleSkip}
|
|
257
|
+
/>
|
|
258
|
+
</div>
|
|
259
|
+
);
|
|
260
|
+
};
|
|
261
|
+
|
|
262
|
+
export default OnboardingWizard;
|