Skip to content

Commit 01346ab

Browse files
feat: Add personal v3 onboarding flow (calcom#24681)
* WIP personal onbaording flow * Add i18n * Fix overflow clip blockin timezone select * Remove duplicate label * Move to two column approach * Restore lock fle * Remove duplicate key in common json * Apply suggestion from @cubic-dev-ai[bot] Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com> * Apply suggestion from @cubic-dev-ai[bot] Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com> * Update personal-settings-view.tsx * Fix endpoint --------- Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com>
1 parent 3d9650c commit 01346ab

11 files changed

Lines changed: 903 additions & 1 deletion

File tree

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import { _generateMetadata } from "app/_utils";
2+
import { cookies, headers } from "next/headers";
3+
import { redirect } from "next/navigation";
4+
5+
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
6+
import { APP_NAME } from "@calcom/lib/constants";
7+
8+
import { buildLegacyRequest } from "@lib/buildLegacyCtx";
9+
10+
import { PersonalCalendarView } from "~/onboarding/personal/calendar/personal-calendar-view";
11+
12+
export const generateMetadata = async () => {
13+
return await _generateMetadata(
14+
(t) => `${APP_NAME} - ${t("connect_calendar")}`,
15+
() => "",
16+
true,
17+
undefined,
18+
"/onboarding/personal/calendar"
19+
);
20+
};
21+
22+
const ServerPage = async () => {
23+
const session = await getServerSession({ req: buildLegacyRequest(await headers(), await cookies()) });
24+
25+
if (!session?.user?.id) {
26+
return redirect("/auth/login");
27+
}
28+
29+
const userEmail = session.user.email || "";
30+
31+
return <PersonalCalendarView userEmail={userEmail} />;
32+
};
33+
34+
export default ServerPage;
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import { _generateMetadata } from "app/_utils";
2+
import { cookies, headers } from "next/headers";
3+
import { redirect } from "next/navigation";
4+
5+
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
6+
import { APP_NAME } from "@calcom/lib/constants";
7+
8+
import { buildLegacyRequest } from "@lib/buildLegacyCtx";
9+
10+
import { PersonalProfileView } from "~/onboarding/personal/profile/personal-profile-view";
11+
12+
export const generateMetadata = async () => {
13+
return await _generateMetadata(
14+
(t) => `${APP_NAME} - ${t("personal_profile")}`,
15+
() => "",
16+
true,
17+
undefined,
18+
"/onboarding/personal/profile"
19+
);
20+
};
21+
22+
const ServerPage = async () => {
23+
const session = await getServerSession({ req: buildLegacyRequest(await headers(), await cookies()) });
24+
25+
if (!session?.user?.id) {
26+
return redirect("/auth/login");
27+
}
28+
29+
const userEmail = session.user.email || "";
30+
31+
return <PersonalProfileView userEmail={userEmail} />;
32+
};
33+
34+
export default ServerPage;
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import { _generateMetadata } from "app/_utils";
2+
import { cookies, headers } from "next/headers";
3+
import { redirect } from "next/navigation";
4+
5+
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
6+
import { APP_NAME } from "@calcom/lib/constants";
7+
8+
import { buildLegacyRequest } from "@lib/buildLegacyCtx";
9+
10+
import { PersonalSettingsView } from "~/onboarding/personal/settings/personal-settings-view";
11+
12+
export const generateMetadata = async () => {
13+
return await _generateMetadata(
14+
(t) => `${APP_NAME} - ${t("personal_settings")}`,
15+
() => "",
16+
true,
17+
undefined,
18+
"/onboarding/personal/settings"
19+
);
20+
};
21+
22+
const ServerPage = async () => {
23+
const session = await getServerSession({ req: buildLegacyRequest(await headers(), await cookies()) });
24+
25+
if (!session?.user?.id) {
26+
return redirect("/auth/login");
27+
}
28+
29+
const userEmail = session.user.email || "";
30+
const userName = session.user.name || "";
31+
32+
return <PersonalSettingsView userEmail={userEmail} userName={userName} />;
33+
};
34+
35+
export default ServerPage;
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import { _generateMetadata } from "app/_utils";
2+
import { cookies, headers } from "next/headers";
3+
import { redirect } from "next/navigation";
4+
5+
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
6+
import { APP_NAME } from "@calcom/lib/constants";
7+
8+
import { buildLegacyRequest } from "@lib/buildLegacyCtx";
9+
10+
import { PersonalVideoView } from "~/onboarding/personal/video/personal-video-view";
11+
12+
export const generateMetadata = async () => {
13+
return await _generateMetadata(
14+
(t) => `${APP_NAME} - ${t("connect_video")}`,
15+
() => "",
16+
true,
17+
undefined,
18+
"/onboarding/personal/video"
19+
);
20+
};
21+
22+
const ServerPage = async () => {
23+
const session = await getServerSession({ req: buildLegacyRequest(await headers(), await cookies()) });
24+
25+
if (!session?.user?.id) {
26+
return redirect("/auth/login");
27+
}
28+
29+
const userEmail = session.user.email || "";
30+
31+
return <PersonalVideoView userEmail={userEmail} />;
32+
};
33+
34+
export default ServerPage;

apps/web/modules/onboarding/getting-started/onboarding-view.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,9 @@ export const OnboardingView = ({ userName, userEmail }: OnboardingViewProps) =>
2525
router.push("/onboarding/organization/details");
2626
} else if (selectedPlan === "team") {
2727
router.push("/onboarding/teams/details");
28-
} // TODO: Handle other plan types
28+
} else if (selectedPlan === "personal") {
29+
router.push("/onboarding/personal/settings");
30+
}
2931
};
3032

3133
const allPlans = [
Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
"use client";
2+
3+
import { useRouter } from "next/navigation";
4+
5+
import { useLocale } from "@calcom/lib/hooks/useLocale";
6+
import { trpc } from "@calcom/trpc/react";
7+
import { Button } from "@calcom/ui/components/button";
8+
import { Logo } from "@calcom/ui/components/logo";
9+
import { SkeletonText } from "@calcom/ui/components/skeleton";
10+
11+
type PersonalCalendarViewProps = {
12+
userEmail: string;
13+
};
14+
15+
export const PersonalCalendarView = ({ userEmail }: PersonalCalendarViewProps) => {
16+
const router = useRouter();
17+
const { t } = useLocale();
18+
19+
const queryIntegrations = trpc.viewer.apps.integrations.useQuery({
20+
variant: "calendar",
21+
onlyInstalled: false,
22+
sortByMostPopular: true,
23+
sortByInstalledFirst: true,
24+
});
25+
26+
const handleContinue = () => {
27+
router.push("/onboarding/personal/video");
28+
};
29+
30+
const handleSkip = () => {
31+
router.push("/onboarding/personal/video");
32+
};
33+
34+
return (
35+
<div className="bg-default flex min-h-screen w-full flex-col items-start overflow-clip rounded-xl">
36+
{/* Header */}
37+
<div className="flex w-full items-center justify-between px-6 py-4">
38+
<Logo className="h-5 w-auto" />
39+
40+
{/* Progress dots - centered */}
41+
<div className="absolute left-1/2 flex -translate-x-1/2 items-center justify-center gap-1">
42+
<div className="bg-emphasis h-1 w-1 rounded-full" />
43+
<div className="bg-emphasis h-1 w-1 rounded-full" />
44+
<div className="bg-emphasis h-1.5 w-1.5 rounded-full" />
45+
<div className="bg-subtle h-1 w-1 rounded-full" />
46+
</div>
47+
48+
<div className="bg-muted flex items-center gap-2 rounded-full px-3 py-2">
49+
<p className="text-emphasis text-sm font-medium leading-none">{userEmail}</p>
50+
</div>
51+
</div>
52+
53+
{/* Main content */}
54+
<div className="flex h-full w-full items-start justify-center px-6 py-8">
55+
<div className="flex w-full max-w-[600px] flex-col gap-4">
56+
{/* Card */}
57+
<div className="bg-muted border-muted relative rounded-xl border p-1">
58+
<div className="rounded-inherit flex w-full flex-col items-start overflow-clip">
59+
{/* Card Header */}
60+
<div className="flex w-full gap-1.5 px-5 py-4">
61+
<div className="flex w-full flex-col gap-1">
62+
<h1 className="font-cal text-xl font-semibold leading-6">{t("connect_your_calendar")}</h1>
63+
<p className="text-subtle text-sm font-medium leading-tight">
64+
{t("connect_calendar_to_prevent_conflicts")}
65+
</p>
66+
</div>
67+
</div>
68+
69+
{/* Content */}
70+
<div className="flex w-full flex-col gap-4 px-5 py-5">
71+
{queryIntegrations.isPending ? (
72+
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2">
73+
<SkeletonText className="h-40 w-full" />
74+
<SkeletonText className="h-40 w-full" />
75+
</div>
76+
) : (
77+
<div className="scroll-bar grid max-h-[45vh] grid-cols-1 gap-3 overflow-y-scroll sm:grid-cols-2">
78+
{queryIntegrations.data?.items.map((app) => (
79+
<div
80+
key={app.slug}
81+
className="border-subtle bg-default flex flex-col items-start gap-4 rounded-xl border p-5">
82+
{app.logo && <img src={app.logo} alt={app.name} className="h-9 w-9 rounded-md" />}
83+
<p
84+
className="text-default line-clamp-1 break-words text-left text-sm font-medium leading-none"
85+
title={app.name}>
86+
{app.name}
87+
</p>
88+
<p className="text-subtle line-clamp-2 text-left text-xs leading-tight">
89+
{app.description}
90+
</p>
91+
<Button
92+
color="secondary"
93+
href={`/apps/${app.slug}`}
94+
className="mt-auto w-full items-center justify-center rounded-[10px]">
95+
{t("connect")}
96+
</Button>
97+
</div>
98+
))}
99+
</div>
100+
)}
101+
</div>
102+
103+
{/* Footer */}
104+
<div className="flex w-full items-center justify-end gap-1 px-5 py-4">
105+
<Button color="primary" className="rounded-[10px]" onClick={handleContinue}>
106+
{t("continue")}
107+
</Button>
108+
</div>
109+
</div>
110+
</div>
111+
112+
{/* Skip button */}
113+
<div className="flex w-full justify-center">
114+
<button
115+
onClick={handleSkip}
116+
className="text-subtle hover:bg-subtle rounded-[10px] px-2 py-1.5 text-sm font-medium leading-4">
117+
{t("ill_do_this_later")}
118+
</button>
119+
</div>
120+
</div>
121+
</div>
122+
</div>
123+
);
124+
};

0 commit comments

Comments
 (0)