diff --git a/apps/web/app/(use-page-wrapper)/onboarding/personal/calendar/page.tsx b/apps/web/app/(use-page-wrapper)/onboarding/personal/calendar/page.tsx
new file mode 100644
index 0000000000..68d29f225b
--- /dev/null
+++ b/apps/web/app/(use-page-wrapper)/onboarding/personal/calendar/page.tsx
@@ -0,0 +1,34 @@
+import { _generateMetadata } from "app/_utils";
+import { cookies, headers } from "next/headers";
+import { redirect } from "next/navigation";
+
+import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
+import { APP_NAME } from "@calcom/lib/constants";
+
+import { buildLegacyRequest } from "@lib/buildLegacyCtx";
+
+import { PersonalCalendarView } from "~/onboarding/personal/calendar/personal-calendar-view";
+
+export const generateMetadata = async () => {
+ return await _generateMetadata(
+ (t) => `${APP_NAME} - ${t("connect_calendar")}`,
+ () => "",
+ true,
+ undefined,
+ "/onboarding/personal/calendar"
+ );
+};
+
+const ServerPage = async () => {
+ const session = await getServerSession({ req: buildLegacyRequest(await headers(), await cookies()) });
+
+ if (!session?.user?.id) {
+ return redirect("/auth/login");
+ }
+
+ const userEmail = session.user.email || "";
+
+ return ;
+};
+
+export default ServerPage;
diff --git a/apps/web/app/(use-page-wrapper)/onboarding/personal/profile/page.tsx b/apps/web/app/(use-page-wrapper)/onboarding/personal/profile/page.tsx
new file mode 100644
index 0000000000..dc70908423
--- /dev/null
+++ b/apps/web/app/(use-page-wrapper)/onboarding/personal/profile/page.tsx
@@ -0,0 +1,34 @@
+import { _generateMetadata } from "app/_utils";
+import { cookies, headers } from "next/headers";
+import { redirect } from "next/navigation";
+
+import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
+import { APP_NAME } from "@calcom/lib/constants";
+
+import { buildLegacyRequest } from "@lib/buildLegacyCtx";
+
+import { PersonalProfileView } from "~/onboarding/personal/profile/personal-profile-view";
+
+export const generateMetadata = async () => {
+ return await _generateMetadata(
+ (t) => `${APP_NAME} - ${t("personal_profile")}`,
+ () => "",
+ true,
+ undefined,
+ "/onboarding/personal/profile"
+ );
+};
+
+const ServerPage = async () => {
+ const session = await getServerSession({ req: buildLegacyRequest(await headers(), await cookies()) });
+
+ if (!session?.user?.id) {
+ return redirect("/auth/login");
+ }
+
+ const userEmail = session.user.email || "";
+
+ return ;
+};
+
+export default ServerPage;
diff --git a/apps/web/app/(use-page-wrapper)/onboarding/personal/settings/page.tsx b/apps/web/app/(use-page-wrapper)/onboarding/personal/settings/page.tsx
new file mode 100644
index 0000000000..114be2a853
--- /dev/null
+++ b/apps/web/app/(use-page-wrapper)/onboarding/personal/settings/page.tsx
@@ -0,0 +1,35 @@
+import { _generateMetadata } from "app/_utils";
+import { cookies, headers } from "next/headers";
+import { redirect } from "next/navigation";
+
+import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
+import { APP_NAME } from "@calcom/lib/constants";
+
+import { buildLegacyRequest } from "@lib/buildLegacyCtx";
+
+import { PersonalSettingsView } from "~/onboarding/personal/settings/personal-settings-view";
+
+export const generateMetadata = async () => {
+ return await _generateMetadata(
+ (t) => `${APP_NAME} - ${t("personal_settings")}`,
+ () => "",
+ true,
+ undefined,
+ "/onboarding/personal/settings"
+ );
+};
+
+const ServerPage = async () => {
+ const session = await getServerSession({ req: buildLegacyRequest(await headers(), await cookies()) });
+
+ if (!session?.user?.id) {
+ return redirect("/auth/login");
+ }
+
+ const userEmail = session.user.email || "";
+ const userName = session.user.name || "";
+
+ return ;
+};
+
+export default ServerPage;
diff --git a/apps/web/app/(use-page-wrapper)/onboarding/personal/video/page.tsx b/apps/web/app/(use-page-wrapper)/onboarding/personal/video/page.tsx
new file mode 100644
index 0000000000..1ec119859f
--- /dev/null
+++ b/apps/web/app/(use-page-wrapper)/onboarding/personal/video/page.tsx
@@ -0,0 +1,34 @@
+import { _generateMetadata } from "app/_utils";
+import { cookies, headers } from "next/headers";
+import { redirect } from "next/navigation";
+
+import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
+import { APP_NAME } from "@calcom/lib/constants";
+
+import { buildLegacyRequest } from "@lib/buildLegacyCtx";
+
+import { PersonalVideoView } from "~/onboarding/personal/video/personal-video-view";
+
+export const generateMetadata = async () => {
+ return await _generateMetadata(
+ (t) => `${APP_NAME} - ${t("connect_video")}`,
+ () => "",
+ true,
+ undefined,
+ "/onboarding/personal/video"
+ );
+};
+
+const ServerPage = async () => {
+ const session = await getServerSession({ req: buildLegacyRequest(await headers(), await cookies()) });
+
+ if (!session?.user?.id) {
+ return redirect("/auth/login");
+ }
+
+ const userEmail = session.user.email || "";
+
+ return ;
+};
+
+export default ServerPage;
diff --git a/apps/web/modules/onboarding/getting-started/onboarding-view.tsx b/apps/web/modules/onboarding/getting-started/onboarding-view.tsx
index e5f263eb05..e2d0f8252c 100644
--- a/apps/web/modules/onboarding/getting-started/onboarding-view.tsx
+++ b/apps/web/modules/onboarding/getting-started/onboarding-view.tsx
@@ -25,7 +25,9 @@ export const OnboardingView = ({ userName, userEmail }: OnboardingViewProps) =>
router.push("/onboarding/organization/details");
} else if (selectedPlan === "team") {
router.push("/onboarding/teams/details");
- } // TODO: Handle other plan types
+ } else if (selectedPlan === "personal") {
+ router.push("/onboarding/personal/settings");
+ }
};
const allPlans = [
diff --git a/apps/web/modules/onboarding/personal/calendar/personal-calendar-view.tsx b/apps/web/modules/onboarding/personal/calendar/personal-calendar-view.tsx
new file mode 100644
index 0000000000..094dfbb9e9
--- /dev/null
+++ b/apps/web/modules/onboarding/personal/calendar/personal-calendar-view.tsx
@@ -0,0 +1,124 @@
+"use client";
+
+import { useRouter } from "next/navigation";
+
+import { useLocale } from "@calcom/lib/hooks/useLocale";
+import { trpc } from "@calcom/trpc/react";
+import { Button } from "@calcom/ui/components/button";
+import { Logo } from "@calcom/ui/components/logo";
+import { SkeletonText } from "@calcom/ui/components/skeleton";
+
+type PersonalCalendarViewProps = {
+ userEmail: string;
+};
+
+export const PersonalCalendarView = ({ userEmail }: PersonalCalendarViewProps) => {
+ const router = useRouter();
+ const { t } = useLocale();
+
+ const queryIntegrations = trpc.viewer.apps.integrations.useQuery({
+ variant: "calendar",
+ onlyInstalled: false,
+ sortByMostPopular: true,
+ sortByInstalledFirst: true,
+ });
+
+ const handleContinue = () => {
+ router.push("/onboarding/personal/video");
+ };
+
+ const handleSkip = () => {
+ router.push("/onboarding/personal/video");
+ };
+
+ return (
+
+ {/* Header */}
+
+
+
+ {/* Progress dots - centered */}
+
+
+
+
+
+ {/* Main content */}
+
+
+ {/* Card */}
+
+
+ {/* Card Header */}
+
+
+
{t("connect_your_calendar")}
+
+ {t("connect_calendar_to_prevent_conflicts")}
+
+
+
+
+ {/* Content */}
+
+ {queryIntegrations.isPending ? (
+
+
+
+
+ ) : (
+
+ {queryIntegrations.data?.items.map((app) => (
+
+ {app.logo &&

}
+
+ {app.name}
+
+
+ {app.description}
+
+
+
+ ))}
+
+ )}
+
+
+ {/* Footer */}
+
+
+
+
+
+
+ {/* Skip button */}
+
+
+
+
+
+
+ );
+};
diff --git a/apps/web/modules/onboarding/personal/profile/personal-profile-view.tsx b/apps/web/modules/onboarding/personal/profile/personal-profile-view.tsx
new file mode 100644
index 0000000000..8fbdb6beda
--- /dev/null
+++ b/apps/web/modules/onboarding/personal/profile/personal-profile-view.tsx
@@ -0,0 +1,220 @@
+"use client";
+
+import { useRouter } from "next/navigation";
+import { useEffect, useRef, useState } from "react";
+import { useForm } from "react-hook-form";
+
+import { useLocale } from "@calcom/lib/hooks/useLocale";
+import { md } from "@calcom/lib/markdownIt";
+import turndown from "@calcom/lib/turndownService";
+import { trpc } from "@calcom/trpc/react";
+import { UserAvatar } from "@calcom/ui/components/avatar";
+import { Button } from "@calcom/ui/components/button";
+import { Editor } from "@calcom/ui/components/editor";
+import { Label } from "@calcom/ui/components/form";
+import { ImageUploader } from "@calcom/ui/components/image-uploader";
+import { Logo } from "@calcom/ui/components/logo";
+import { showToast } from "@calcom/ui/components/toast";
+
+import { UsernameAvailabilityField } from "@components/ui/UsernameAvailability";
+
+import { useOnboardingStore } from "../../store/onboarding-store";
+
+type PersonalProfileViewProps = {
+ userEmail: string;
+};
+
+type FormData = {
+ bio: string;
+};
+
+export const PersonalProfileView = ({ userEmail }: PersonalProfileViewProps) => {
+ const { data: user } = trpc.viewer.me.get.useQuery();
+ const router = useRouter();
+ const { t } = useLocale();
+ const { personalDetails, setPersonalDetails } = useOnboardingStore();
+
+ const avatarRef = useRef(null);
+ const [imageSrc, setImageSrc] = useState("");
+ const [firstRender, setFirstRender] = useState(true);
+
+ // Update imageSrc when user loads
+ useEffect(() => {
+ if (user) {
+ setImageSrc(personalDetails.avatar || user.avatar || "");
+ }
+ }, [user, personalDetails.avatar]);
+
+ const { setValue, handleSubmit, getValues } = useForm({
+ defaultValues: { bio: personalDetails.bio || user?.bio || "" },
+ });
+
+ const utils = trpc.useUtils();
+
+ // Avatar mutation
+ const avatarMutation = trpc.viewer.me.updateProfile.useMutation({
+ onSuccess: async (data) => {
+ showToast(t("your_user_profile_updated_successfully"), "success");
+ setImageSrc(data.avatarUrl ?? "");
+ setPersonalDetails({ avatar: data.avatarUrl ?? null });
+ },
+ onError: () => {
+ showToast(t("problem_saving_user_profile"), "error");
+ },
+ });
+
+ // Profile mutation
+ const mutation = trpc.viewer.me.updateProfile.useMutation({
+ onSuccess: async () => {
+ await utils.viewer.me.invalidate();
+ },
+ });
+
+ const onSubmit = handleSubmit(async (data: { bio: string }) => {
+ const { bio } = data;
+
+ // Save to store
+ setPersonalDetails({
+ bio,
+ });
+
+ // Save to backend
+ await mutation.mutateAsync({
+ bio,
+ });
+
+ router.push("/onboarding/personal/calendar");
+ });
+
+ async function updateProfileHandler(newAvatar: string) {
+ avatarMutation.mutate({
+ avatarUrl: newAvatar,
+ });
+ }
+
+ if (!user) {
+ return null; // or a loading spinner
+ }
+
+ return (
+
+ {/* Header */}
+
+
+
+ {/* Progress dots - centered */}
+
+
+
+
+
+ {/* Main content */}
+
+
+ {/* Card */}
+
+
+ {/* Card Header */}
+
+
+
{t("complete_your_profile")}
+
+ {t("personal_profile_subtitle")}
+
+
+
+
+ {/* Form */}
+
+
+ {/* Footer */}
+
+
+
+
+
+
+
+
+ );
+};
diff --git a/apps/web/modules/onboarding/personal/settings/personal-settings-view.tsx b/apps/web/modules/onboarding/personal/settings/personal-settings-view.tsx
new file mode 100644
index 0000000000..04238f116c
--- /dev/null
+++ b/apps/web/modules/onboarding/personal/settings/personal-settings-view.tsx
@@ -0,0 +1,179 @@
+"use client";
+
+import { zodResolver } from "@hookform/resolvers/zod";
+import { useRouter } from "next/navigation";
+import { useEffect, useState } from "react";
+import { useForm } from "react-hook-form";
+import { z } from "zod";
+
+import dayjs from "@calcom/dayjs";
+import { useTimePreferences } from "@calcom/features/bookings/lib";
+import { TimezoneSelect } from "@calcom/features/components/timezone-select";
+import { FULL_NAME_LENGTH_MAX_LIMIT } from "@calcom/lib/constants";
+import { useLocale } from "@calcom/lib/hooks/useLocale";
+import { trpc } from "@calcom/trpc/react";
+import { Button } from "@calcom/ui/components/button";
+import { Label, TextField } from "@calcom/ui/components/form";
+import { Logo } from "@calcom/ui/components/logo";
+
+import { OnboardingContinuationPrompt } from "../../components/onboarding-continuation-prompt";
+import { useOnboardingStore } from "../../store/onboarding-store";
+
+type PersonalSettingsViewProps = {
+ userEmail: string;
+ userName?: string;
+};
+
+export const PersonalSettingsView = ({ userEmail, userName }: PersonalSettingsViewProps) => {
+ const router = useRouter();
+ const { t } = useLocale();
+ const { personalDetails, setPersonalDetails } = useOnboardingStore();
+ const { setTimezone: setSelectedTimeZone, timezone: selectedTimeZone } = useTimePreferences();
+
+ const [name, setName] = useState("");
+
+ useEffect(() => {
+ setName(personalDetails.name || userName || "");
+ if (personalDetails.timezone) {
+ setSelectedTimeZone(personalDetails.timezone);
+ }
+ }, [personalDetails, userName, setSelectedTimeZone]);
+
+ const formSchema = z.object({
+ name: z
+ .string()
+ .min(1, t("name_required"))
+ .max(FULL_NAME_LENGTH_MAX_LIMIT, {
+ message: t("max_limit_allowed_hint", { limit: FULL_NAME_LENGTH_MAX_LIMIT }),
+ }),
+ });
+
+ const form = useForm>({
+ resolver: zodResolver(formSchema),
+ defaultValues: {
+ name: personalDetails.name || userName || "",
+ },
+ });
+
+ const utils = trpc.useUtils();
+ const mutation = trpc.viewer.me.updateProfile.useMutation({
+ onSuccess: async () => {
+ await utils.viewer.me.invalidate();
+ },
+ });
+
+ const handleContinue = form.handleSubmit(async (data) => {
+ // Save to store
+ setPersonalDetails({
+ name: data.name,
+ timezone: selectedTimeZone,
+ });
+
+ // Save to backend
+ await mutation.mutateAsync({
+ name: data.name,
+ timeZone: selectedTimeZone,
+ });
+
+ router.push("/onboarding/personal/profile");
+ });
+
+ return (
+
+
+ {/* Header */}
+
+
+
+ {/* Progress dots - centered */}
+
+
+
+
+
+ {/* Main content */}
+
+
+ {/* Card */}
+
+
+ {/* Card Header */}
+
+
+
{t("welcome_to_cal_com")}
+
+ {t("personal_settings_subtitle")}
+
+
+
+
+ {/* Form */}
+
+
+
+
+
+ {/* Name */}
+
+
{
+ setName(e.target.value);
+ form.setValue("name", e.target.value);
+ }}
+ placeholder="John Doe"
+ className="border-default h-7 rounded-[10px] border px-2 py-1.5 text-sm"
+ />
+ {form.formState.errors.name && (
+ {form.formState.errors.name.message}
+ )}
+
+
+ {/* Timezone */}
+
+
+
setSelectedTimeZone(value)}
+ className="rounded-[10px] text-sm"
+ />
+
+ {t("current_time")}{" "}
+ {dayjs().tz(selectedTimeZone).format("LT").toString().toLowerCase()}
+
+
+
+
+
+
+
+
+ {/* Footer */}
+
+
+
+
+
+
+
+
+ );
+};
diff --git a/apps/web/modules/onboarding/personal/video/personal-video-view.tsx b/apps/web/modules/onboarding/personal/video/personal-video-view.tsx
new file mode 100644
index 0000000000..8a1809ac70
--- /dev/null
+++ b/apps/web/modules/onboarding/personal/video/personal-video-view.tsx
@@ -0,0 +1,203 @@
+"use client";
+
+import { useRouter } from "next/navigation";
+
+import { useLocale } from "@calcom/lib/hooks/useLocale";
+import { useTelemetry } from "@calcom/lib/hooks/useTelemetry";
+import { telemetryEventTypes } from "@calcom/lib/telemetry";
+import { trpc } from "@calcom/trpc/react";
+import { Button } from "@calcom/ui/components/button";
+import { Logo } from "@calcom/ui/components/logo";
+import { SkeletonText } from "@calcom/ui/components/skeleton";
+
+type PersonalVideoViewProps = {
+ userEmail: string;
+};
+
+const DEFAULT_EVENT_TYPES = [
+ {
+ title: "15min_meeting",
+ slug: "15min",
+ length: 15,
+ },
+ {
+ title: "30min_meeting",
+ slug: "30min",
+ length: 30,
+ },
+ {
+ title: "secret_meeting",
+ slug: "secret",
+ length: 15,
+ hidden: true,
+ },
+];
+
+export const PersonalVideoView = ({ userEmail }: PersonalVideoViewProps) => {
+ const { data: user } = trpc.viewer.me.get.useQuery();
+ const router = useRouter();
+ const { t } = useLocale();
+ const telemetry = useTelemetry();
+
+ const { data: queryConnectedVideoApps, isPending } = trpc.viewer.apps.integrations.useQuery({
+ variant: "conferencing",
+ onlyInstalled: false,
+ sortByMostPopular: true,
+ sortByInstalledFirst: true,
+ });
+
+ const { data: eventTypes } = trpc.viewer.eventTypes.list.useQuery();
+ const createEventType = trpc.viewer.eventTypesHeavy.create.useMutation();
+
+ const utils = trpc.useUtils();
+ const mutation = trpc.viewer.me.updateProfile.useMutation({
+ onSuccess: async () => {
+ try {
+ // Create default event types if user has none
+ if (eventTypes?.length === 0) {
+ await Promise.all(
+ DEFAULT_EVENT_TYPES.map(async (event) => {
+ return createEventType.mutateAsync({
+ title: t(event.title),
+ slug: event.slug,
+ length: event.length,
+ hidden: event.hidden,
+ });
+ })
+ );
+ }
+ } catch (error) {
+ console.error(error);
+ }
+
+ await utils.viewer.me.get.refetch();
+ router.push("/event-types");
+ },
+ });
+
+ const handleContinue = async () => {
+ telemetry.event(telemetryEventTypes.onboardingFinished);
+
+ // Complete onboarding
+ mutation.mutate({
+ completedOnboarding: true,
+ });
+ };
+
+ const handleSkip = async () => {
+ telemetry.event(telemetryEventTypes.onboardingFinished);
+
+ // Complete onboarding
+ mutation.mutate({
+ completedOnboarding: true,
+ });
+ };
+
+ if (!user) {
+ return null; // or a loading spinner
+ }
+
+ return (
+
+ {/* Header */}
+
+
+
+ {/* Progress dots - centered */}
+
+
+
+
+
+ {/* Main content */}
+
+
+ {/* Card */}
+
+
+ {/* Card Header */}
+
+
+
{t("connect_video_app")}
+
+ {t("video_app_connection_subtitle")}
+
+
+
+
+ {/* Content */}
+
+ {isPending ? (
+
+
+
+
+ ) : (
+
+ {queryConnectedVideoApps?.items
+ .filter((app) => app.slug !== "daily-video")
+ .map((app) => (
+
+ {app.userCredentialIds.length > 0 && (
+
+ {t("connected")}
+
+ )}
+ {app.logo &&

}
+
+ {app.name}
+
+
+ {app.description}
+
+
+
+ ))}
+
+ )}
+
+
+ {/* Footer */}
+
+
+
+
+
+
+ {/* Skip button */}
+
+
+
+
+
+
+ );
+};
diff --git a/apps/web/modules/onboarding/store/onboarding-store.ts b/apps/web/modules/onboarding/store/onboarding-store.ts
index b352b1232d..46a2d38d90 100644
--- a/apps/web/modules/onboarding/store/onboarding-store.ts
+++ b/apps/web/modules/onboarding/store/onboarding-store.ts
@@ -36,6 +36,14 @@ export interface TeamBrand {
logo: string | null; // base64 or URL
}
+export interface PersonalDetails {
+ name: string;
+ username: string;
+ timezone: string;
+ bio: string;
+ avatar: string | null;
+}
+
export interface OnboardingState {
selectedPlan: PlanType | null;
@@ -53,6 +61,9 @@ export interface OnboardingState {
teamBrand: TeamBrand;
teamInvites: Invite[];
+ // Personal user state
+ personalDetails: PersonalDetails;
+
// Actions
setSelectedPlan: (plan: PlanType) => void;
setOrganizationDetails: (details: Partial) => void;
@@ -66,6 +77,9 @@ export interface OnboardingState {
setTeamBrand: (brand: Partial) => void;
setTeamInvites: (invites: Invite[]) => void;
+ // Personal actions
+ setPersonalDetails: (details: Partial) => void;
+
// Reset
resetOnboarding: () => void;
}
@@ -94,6 +108,13 @@ const initialState = {
logo: null,
},
teamInvites: [],
+ personalDetails: {
+ name: "",
+ username: "",
+ timezone: "",
+ bio: "",
+ avatar: null,
+ },
};
export const useOnboardingStore = create()(
@@ -131,6 +152,11 @@ export const useOnboardingStore = create()(
setTeamInvites: (invites) => set({ teamInvites: invites }),
+ setPersonalDetails: (details) =>
+ set((state) => ({
+ personalDetails: { ...state.personalDetails, ...details },
+ })),
+
resetOnboarding: () => set(initialState),
}),
{
@@ -146,6 +172,7 @@ export const useOnboardingStore = create()(
teamDetails: state.teamDetails,
teamBrand: state.teamBrand,
teamInvites: state.teamInvites,
+ personalDetails: state.personalDetails,
}),
}
)
diff --git a/apps/web/public/static/locales/en/common.json b/apps/web/public/static/locales/en/common.json
index d16543f383..b178ff4523 100644
--- a/apps/web/public/static/locales/en/common.json
+++ b/apps/web/public/static/locales/en/common.json
@@ -1729,6 +1729,16 @@
"set_availability_getting_started_subtitle_2": "Customize it later in the availability page.",
"connect_calendar_first": "Connect a calendar first",
"complete_profile": "Complete your profile",
+ "complete_your_profile": "Complete your profile",
+ "personal_profile_subtitle": "Add details to personalize your scheduling experience",
+ "personal_settings_subtitle": "Let's set up your personal account",
+ "profile_photo": "Profile photo",
+ "connect_calendar_to_prevent_conflicts": "Connect your calendar to prevent double bookings and conflicts",
+ "connect_video_app": "Connect your video app",
+ "video_app_connection_subtitle": "Connect your preferred video conferencing app",
+ "welcome_to_cal_com": "Welcome to Cal.com",
+ "name_required": "Name is required",
+ "connected": "Connected",
"connect_calendars_from_app_store": "You can add more calendars from the app store",
"connect_conference_apps": "Connect conference apps",
"connect_calendar_apps": "Connect calendar apps",