Skip to content

Commit 52e27ba

Browse files
authored
chore: Add shared onboarding components (OnboardingCard, OnboardingLayout, browser views) (calcom#24946)
## What does this PR do? Creates new onboarding UI components for the user onboarding flow, including: - `OnboardingCard` - A reusable card component with title, subtitle, content, and footer sections - `OnboardingLayout` - A layout component with progress indicators and sign-out functionality - `OnboardingBrowserView` - A browser preview showing how the user's booking page will look - `OnboardingCalendarBrowserView` - A calendar preview showing sample events - `OnboardingInviteBrowserView` - A preview of the team invitation email - Enhanced `PlanIcon` component with new variants and animations for organization and team plans ## Visual Demo (For contributors especially) #### Image Demo: The PR adds several visual components for the onboarding flow, including browser previews, calendar views, and animated plan icons with concentric rings and user avatars. ## Mandatory Tasks (DO NOT REMOVE) - [x] I have self-reviewed the code. - [x] I have updated the developer docs in /docs if this PR makes changes that would require a documentation change. If N/A, write N/A here and check the checkbox. - [x] I confirm automated tests are in place that prove my fix is effective or that my feature works. ## How should this be tested? - Navigate to the onboarding flow to see the new components in action - Test the layout with different screen sizes to ensure responsive behavior - Verify that the browser previews render correctly with sample data - Check that the plan icon animations work properly for different variants (single, team, organization) - Ensure the calendar view displays sample events correctly ## Checklist - I have read the [contributing guide](https://github.com/calcom/cal.com/blob/main/CONTRIBUTING.md) - My code follows the style guidelines of this project - I have commented my code, particularly in hard-to-understand areas - I have checked if my changes generate no new warnings
1 parent 45278a2 commit 52e27ba

6 files changed

Lines changed: 437 additions & 78 deletions

File tree

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import type { ReactNode } from "react";
2+
3+
import { SkeletonText } from "@calcom/ui/components/skeleton";
4+
5+
type OnboardingCardProps = {
6+
title: string;
7+
subtitle: string;
8+
children: ReactNode;
9+
footer: ReactNode;
10+
isLoading?: boolean;
11+
};
12+
13+
export const OnboardingCard = ({ title, subtitle, children, footer, isLoading }: OnboardingCardProps) => {
14+
return (
15+
<div className="relative flex h-full min-h-0 w-full flex-col">
16+
{/* Card Header */}
17+
<div className="flex w-full gap-1.5 px-5 py-4">
18+
<div className="flex w-full flex-col gap-1">
19+
<h1 className="font-cal text-xl font-semibold leading-6">{title}</h1>
20+
<p className="text-subtle text-sm font-medium leading-tight">{subtitle}</p>
21+
</div>
22+
</div>
23+
24+
{/* Content */}
25+
<div className="flex min-h-0 w-full flex-1 flex-col gap-4">
26+
{isLoading ? (
27+
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2">
28+
<SkeletonText className="h-40 w-full" />
29+
<SkeletonText className="h-40 w-full" />
30+
</div>
31+
) : (
32+
children
33+
)}
34+
</div>
35+
36+
{/* Footer */}
37+
<div className="flex w-full items-center justify-start px-5 py-4">{footer}</div>
38+
</div>
39+
);
40+
};
41+
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
"use client";
2+
3+
import { signOut } from "next-auth/react";
4+
import { Children, type ReactNode } from "react";
5+
6+
import { useLocale } from "@calcom/lib/hooks/useLocale";
7+
import { Button } from "@calcom/ui/components/button";
8+
import { Logo } from "@calcom/ui/components/logo";
9+
10+
type OnboardingLayoutProps = {
11+
userEmail: string;
12+
currentStep: 1 | 2 | 3;
13+
children: ReactNode;
14+
};
15+
16+
export const OnboardingLayout = ({ userEmail, currentStep, children }: OnboardingLayoutProps) => {
17+
const { t } = useLocale();
18+
19+
const handleSignOut = () => {
20+
signOut({ callbackUrl: "/auth/logout" });
21+
};
22+
23+
// Extract children as array
24+
const childrenArray = Children.toArray(children);
25+
const column1 = childrenArray[0];
26+
const column2 = childrenArray[1];
27+
28+
return (
29+
<div className="bg-muted flex min-h-screen w-full flex-col items-center justify-between overflow-clip rounded-[12px] px-6 py-10">
30+
{/* Logo and container - centered */}
31+
<div className="flex w-full flex-1 flex-col items-center justify-center gap-8">
32+
<Logo className="h-5 w-auto shrink-0" />
33+
<div className="border-subtle bg-default grid w-full max-w-[1130px] grid-cols-1 gap-6 overflow-hidden rounded-2xl border px-12 py-10 xl:h-[690px] xl:grid-cols-[40%_1fr] xl:pl-10 xl:pr-0">
34+
{/* Column 1 - Always visible, 40% on xl+ */}
35+
<div className="flex min-h-0 flex-col">{column1}</div>
36+
{/* Column 2 - Hidden on mobile, visible on xl+, 60% on xl+ */}
37+
{column2 && (
38+
<div className="hidden h-full max-h-full min-h-0 flex-col overflow-hidden xl:flex">{column2}</div>
39+
)}
40+
</div>
41+
</div>
42+
43+
{/* Footer with progress dots and sign out */}
44+
<div className="flex w-full flex-col items-center justify-center gap-4 px-10 py-8">
45+
<div className="flex items-center gap-2">
46+
{[1, 2, 3].map((step) => (
47+
<div key={step} className="relative flex h-2 w-2 shrink-0 items-center justify-center">
48+
<div
49+
className={`absolute inset-0 rounded-full ${
50+
step <= currentStep ? "bg-emphasis" : "bg-muted"
51+
}`}
52+
/>
53+
{step <= currentStep && <div className="bg-emphasis absolute h-1 w-1 rounded-full" />}
54+
</div>
55+
))}
56+
</div>
57+
<Button onClick={handleSignOut} color="minimal" className="text-subtle h-7">
58+
{t("sign_out")}
59+
</Button>
60+
</div>
61+
</div>
62+
);
63+
};
64+
Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
"use client";
2+
3+
import { WEBAPP_URL } from "@calcom/lib/constants";
4+
import { useLocale } from "@calcom/lib/hooks/useLocale";
5+
import { Avatar } from "@calcom/ui/components/avatar";
6+
import { Button } from "@calcom/ui/components/button";
7+
import { Icon, type IconName } from "@calcom/ui/components/icon";
8+
9+
type OnboardingBrowserViewProps = {
10+
avatar?: string | null;
11+
name?: string;
12+
bio?: string;
13+
username?: string | null;
14+
teamSlug?: string;
15+
};
16+
17+
export const OnboardingBrowserView = ({
18+
avatar,
19+
name,
20+
bio,
21+
username,
22+
teamSlug,
23+
}: OnboardingBrowserViewProps) => {
24+
const { t } = useLocale();
25+
const webappUrl = WEBAPP_URL.replace(/^https?:\/\//, "");
26+
const displayUrl =
27+
teamSlug !== undefined ? `${webappUrl}/team/${teamSlug || ""}` : `${webappUrl}/${username || ""}`;
28+
29+
const events: Array<{
30+
title: string;
31+
description: string;
32+
duration: number;
33+
icon: IconName;
34+
}> = [
35+
{
36+
title: t("onboarding_browser_view_demo"),
37+
description: t("onboarding_browser_view_demo_description"),
38+
duration: 15,
39+
icon: "bell",
40+
},
41+
{
42+
title: t("onboarding_browser_view_quick_meeting"),
43+
description: t("onboarding_browser_view_quick_meeting_description"),
44+
duration: 15,
45+
icon: "bell",
46+
},
47+
{
48+
title: t("onboarding_browser_view_longer_meeting"),
49+
description: t("onboarding_browser_view_longer_meeting_description"),
50+
duration: 30,
51+
icon: "clock",
52+
},
53+
{
54+
title: t("in_person_meeting"),
55+
description: t("onboarding_browser_view_in_person_description"),
56+
duration: 120,
57+
icon: "map-pin",
58+
},
59+
{
60+
title: t("onboarding_browser_view_ask_question"),
61+
description: t("onboarding_browser_view_ask_question_description"),
62+
duration: 15,
63+
icon: "message-circle",
64+
},
65+
];
66+
return (
67+
<div className="bg-default border-subtle hidden h-full w-full flex-col rounded-l-2xl border xl:flex">
68+
{/* Browser header */}
69+
<div className="border-subtle flex min-w-0 shrink-0 items-center gap-3 rounded-t-2xl border-b bg-white p-3">
70+
{/* Navigation buttons */}
71+
<div className="flex shrink-0 items-center gap-4 opacity-50">
72+
<Icon name="arrow-left" className="text-subtle h-4 w-4" />
73+
<Icon name="arrow-right" className="text-subtle h-4 w-4" />
74+
<Icon name="rotate-cw" className="text-subtle h-4 w-4" />
75+
</div>
76+
<div className="bg-muted flex w-full items-center gap-2 rounded-[32px] px-3 py-2">
77+
<Icon name="lock" className="text-subtle h-4 w-4" />
78+
<p className="text-default text-sm font-medium leading-tight">{displayUrl}</p>
79+
</div>
80+
<Icon name="ellipsis-vertical" className="text-subtle h-4 w-4" />
81+
</div>
82+
{/* Content */}
83+
<div className="bg-muted h-full pl-11 pt-11">
84+
<div className="bg-default border-muted flex h-full w-full flex-col overflow-hidden rounded-xl border">
85+
{/* Profile Header */}
86+
<div className="border-subtle flex flex-col gap-4 border-b p-4">
87+
<div className="flex flex-col items-start gap-4">
88+
<Avatar
89+
size="lg"
90+
imageSrc={avatar || undefined}
91+
alt={name || ""}
92+
className="border-2 border-white"
93+
/>
94+
<div className="flex flex-col gap-2">
95+
<h2 className="text-emphasis text-xl font-semibold leading-tight">
96+
{name || t("your_name")}
97+
</h2>
98+
<p className="text-default text-sm leading-normal">
99+
{bio || t("onboarding_browser_view_default_bio")}
100+
</p>
101+
</div>
102+
</div>
103+
</div>
104+
105+
{/* Events List */}
106+
<div className="flex flex-col overflow-y-auto">
107+
{events.map((event, index) => (
108+
<div key={event.title} className="opacity-30">
109+
{index > 0 && <div className="border-subtle h-px border-t" />}
110+
<div className="flex items-center justify-between gap-3 px-5 py-4">
111+
<div className="flex min-w-0 flex-1 flex-col gap-1">
112+
<div className="flex items-center gap-1">
113+
<h3 className="text-default text-sm font-semibold leading-none">{event.title}</h3>
114+
<div className="bg-emphasis flex h-4 items-center justify-center gap-1 rounded-md px-1">
115+
<Icon name={event.icon} className="text-emphasis h-3 w-3" />
116+
<span className="text-emphasis text-xs font-medium leading-none">
117+
{event.duration} {t("minute_timeUnit")}
118+
</span>
119+
</div>
120+
</div>
121+
<p className="text-subtle text-sm font-medium leading-tight">{event.description}</p>
122+
</div>
123+
<Button color="secondary" size="sm" EndIcon="arrow-right">
124+
{t("book_now")}
125+
</Button>
126+
</div>
127+
</div>
128+
))}
129+
</div>
130+
</div>
131+
</div>
132+
</div>
133+
);
134+
};
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
"use client";
2+
3+
import { trpc } from "@calcom/trpc/react";
4+
import { Avatar } from "@calcom/ui/components/avatar";
5+
6+
import { useOnboardingStore } from "../store/onboarding-store";
7+
8+
type OnboardingInviteBrowserViewProps = {
9+
teamName?: string;
10+
};
11+
12+
export const OnboardingInviteBrowserView = ({ teamName }: OnboardingInviteBrowserViewProps) => {
13+
const { data: user } = trpc.viewer.me.get.useQuery();
14+
const { teamBrand } = useOnboardingStore();
15+
16+
// Use default values if not provided
17+
const rawInviterName = user?.name || user?.username || "Alex";
18+
const displayInviterName = rawInviterName.charAt(0).toUpperCase() + rawInviterName.slice(1);
19+
const displayTeamName = teamName || "Deel";
20+
const teamAvatar = teamBrand.logo || null;
21+
22+
return (
23+
<div className="bg-default border-subtle hidden h-full w-full flex-col rounded-l-2xl border xl:flex">
24+
{/* Browser header */}
25+
<div className="border-subtle flex min-w-0 shrink-0 items-center gap-3 rounded-t-2xl border-b bg-white p-3">
26+
{/* Navigation buttons */}
27+
<div className="flex shrink-0 items-center gap-4 opacity-50">
28+
<div className="h-3 w-3 rounded-full bg-gray-300" />
29+
<div className="h-3 w-3 rounded-full bg-gray-300" />
30+
<div className="h-3 w-3 rounded-full bg-gray-300" />
31+
</div>
32+
<div className="bg-muted flex w-full items-center gap-2 rounded-[32px] px-3 py-2">
33+
<div className="h-3 w-3 rounded-full bg-gray-400" />
34+
<p className="text-subtle text-xs font-medium leading-tight">mail.example.com</p>
35+
</div>
36+
</div>
37+
38+
{/* Content */}
39+
<div className="bg-muted h-full pl-8 pt-8">
40+
<div className="bg-default border-subtle flex h-full w-full flex-col overflow-hidden rounded-tl-2xl border-l border-t">
41+
{/* Email Header */}
42+
<div className="border-subtle bg-muted flex flex-col gap-4 border-b p-8">
43+
<div className="flex flex-col items-start gap-4">
44+
<Avatar
45+
size="lg"
46+
imageSrc={teamAvatar || undefined}
47+
alt={displayTeamName}
48+
className="border-default h-12 w-12 border-2"
49+
/>
50+
<div className="flex w-full flex-col items-start gap-1">
51+
<h2 className="text-emphasis font-cal w-full text-left text-xl font-semibold leading-tight">
52+
{displayInviterName} invited you to join {displayTeamName}
53+
</h2>
54+
<p className="text-subtle text-left text-sm font-normal leading-tight">
55+
We're emailing you all the details
56+
</p>
57+
</div>
58+
</div>
59+
</div>
60+
61+
{/* Email Body */}
62+
<div className="bg-default flex-1 rounded-bl-2xl" />
63+
</div>
64+
</div>
65+
</div>
66+
);
67+
};

0 commit comments

Comments
 (0)