From aff494f21cd703a5edbe97bf6fca1b958ef695f9 Mon Sep 17 00:00:00 2001
From: sean-brydon <55134778+sean-brydon@users.noreply.github.com>
Date: Wed, 29 Oct 2025 12:51:21 +0000
Subject: [PATCH] chore: refactor onboarding v3 to use reusable components
(#24711)
\## What does this PR do?
Refactors the onboarding UI components to use shared layout components across personal and organization onboarding flows. This change:
- Creates reusable `OnboardingCard` and `OnboardingLayout` components
- Implements these components across all onboarding views
- Maintains consistent UI and reduces code duplication
- Improves maintainability by centralizing layout logic
## Visual Demo (For contributors especially)
#### Image Demo:
Before this change, each onboarding view had its own layout implementation with duplicated header, footer, and card structure. After this change, the shared components provide consistent styling and structure.
## 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 through the onboarding flow for both personal and organization accounts
- Verify that all screens maintain consistent layout and styling
- Check that progress indicators correctly show the current step
- Ensure all functionality (form submissions, navigation between steps) works as expected
## 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
---
.../brand/organization-brand-view.tsx | 331 ++++++++----------
.../details/organization-details-view.tsx | 143 +++-----
.../teams/organization-teams-view.tsx | 150 ++++----
.../personal/_components/OnboardingCard.tsx | 2 +-
.../profile/personal-profile-view.tsx | 190 ++++------
.../settings/personal-settings-view.tsx | 146 +++-----
6 files changed, 394 insertions(+), 568 deletions(-)
diff --git a/apps/web/modules/onboarding/organization/brand/organization-brand-view.tsx b/apps/web/modules/onboarding/organization/brand/organization-brand-view.tsx
index 708f6b36bf..6868e24e86 100644
--- a/apps/web/modules/onboarding/organization/brand/organization-brand-view.tsx
+++ b/apps/web/modules/onboarding/organization/brand/organization-brand-view.tsx
@@ -7,8 +7,9 @@ import { HexColorPicker } from "react-colorful";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { Button } from "@calcom/ui/components/button";
-import { Logo } from "@calcom/ui/components/logo";
+import { OnboardingCard } from "../../personal/_components/OnboardingCard";
+import { OnboardingLayout } from "../../personal/_components/OnboardingLayout";
import { useOnboardingStore } from "../../store/onboarding-store";
type OrganizationBrandViewProps = {
@@ -116,70 +117,137 @@ export const OrganizationBrandView = ({ userEmail }: OrganizationBrandViewProps)
};
return (
-
- {/* Header */}
-
-
+
+
+
+ {t("ill_do_this_later")}
+
+
+ {t("continue")}
+
+ >
+ }>
+ {/* Form */}
+
+
+
+
+ {/* Left side - Form */}
+
+ {/* Brand Color */}
+
+
{t("brand_color")}
+
+
+ {t("onboarding_primary_color_label")}
+
+
{
+ setBrandColor(value);
+ setOrganizationBrand({ color: value });
+ }}
+ t={t}
+ />
+
+
- {/* Progress dots - centered */}
-
+ {/* Logo Upload */}
+
+
{t("logo")}
+
+
+ {logoPreview && (
+
+ )}
+
+
+ document.getElementById("logo-upload")?.click()}>
+ {t("upload")}
+
+ handleLogoChange(e.target.files?.[0] || null)}
+ />
+
+
+
{t("onboarding_logo_size_hint")}
+
-
-
-
- {/* Main content */}
-
-
- {/* Card */}
-
-
- {/* Card Header */}
-
-
-
{t("onboarding_org_brand_title")}
-
- {t("onboarding_org_brand_subtitle")}
-
+ {/* Banner Upload */}
+
+
+ {t("onboarding_banner_label")}
+
+
+
+ {bannerPreview && (
+
+ )}
+
+
+ document.getElementById("banner-upload")?.click()}>
+ {t("upload")}
+
+ handleBannerChange(e.target.files?.[0] || null)}
+ />
+
+
+
+ {t("onboarding_banner_size_hint")}
+
+
-
- {/* Form */}
-
-
-
-
- {/* Left side - Form */}
-
- {/* Brand Color */}
-
-
{t("brand_color")}
-
-
- {t("onboarding_primary_color_label")}
-
-
{
- setBrandColor(value);
- setOrganizationBrand({ color: value });
- }}
- t={t}
- />
-
-
+ {/* Right side - Preview */}
+
+
+
{t("preview")}
+
+ {/* Banner preview */}
+
+ {bannerPreview && (
+
+ )}
+
- {/* Logo Upload */}
-
-
{t("logo")}
-
-
+ {/* Content */}
+
+
+
+ {/* Logo preview */}
+
{logoPreview && (
)}
-
-
document.getElementById("logo-upload")?.click()}>
- {t("upload")}
-
-
handleLogoChange(e.target.files?.[0] || null)}
- />
+
+ {organizationDetails.name || t("onboarding_preview_nameless")}
+
+
+
+
+
+ {t("onboarding_preview_example_title")}
+
+
+ {t("onboarding_preview_example_description")}
+
-
- {t("onboarding_logo_size_hint")}
-
-
- {/* Banner Upload */}
-
-
{t("onboarding_banner_label")}
-
-
- {bannerPreview && (
-
- )}
+
+ {[134, 104, 84, 104].map((width, i) => (
+
-
- document.getElementById("banner-upload")?.click()}>
- {t("upload")}
-
- handleBannerChange(e.target.files?.[0] || null)}
- />
-
-
-
- {t("onboarding_banner_size_hint")}
-
-
-
-
- {/* Right side - Preview */}
-
-
-
{t("preview")}
-
- {/* Banner preview */}
-
- {bannerPreview && (
-
- )}
-
-
- {/* Content */}
-
-
-
- {/* Logo preview */}
-
- {logoPreview && (
-
- )}
-
-
- {organizationDetails.name || t("onboarding_preview_nameless")}
-
-
-
-
-
- {t("onboarding_preview_example_title")}
-
-
- {t("onboarding_preview_example_description")}
-
-
-
-
-
- {[134, 104, 84, 104].map((width, i) => (
-
- ))}
-
-
-
+ ))}
-
- {/* Footer */}
-
-
- {t("ill_do_this_later")}
-
-
- {t("continue")}
-
-
-
-
+
+
);
};
diff --git a/apps/web/modules/onboarding/organization/details/organization-details-view.tsx b/apps/web/modules/onboarding/organization/details/organization-details-view.tsx
index be4e22b223..be492a3a40 100644
--- a/apps/web/modules/onboarding/organization/details/organization-details-view.tsx
+++ b/apps/web/modules/onboarding/organization/details/organization-details-view.tsx
@@ -6,9 +6,9 @@ import { useEffect, useState } from "react";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { Button } from "@calcom/ui/components/button";
import { Label, TextField, TextArea } from "@calcom/ui/components/form";
-import { Logo } from "@calcom/ui/components/logo";
-import { OnboardingContinuationPrompt } from "../../components/onboarding-continuation-prompt";
+import { OnboardingCard } from "../../personal/_components/OnboardingCard";
+import { OnboardingLayout } from "../../personal/_components/OnboardingLayout";
import { useOnboardingStore } from "../../store/onboarding-store";
import { ValidatedOrganizationSlug } from "./validated-organization-slug";
@@ -76,101 +76,64 @@ export const OrganizationDetailsView = ({ userEmail }: OrganizationDetailsViewPr
};
return (
-
- {/* Header */}
-
-
+
+
+ {t("continue")}
+
+ }>
+ {/* Form */}
+
+
+
+
+
+ {/* Organization Name */}
+
+
+ {t("organization_name")}
+
+ setOrganizationName(e.target.value)}
+ placeholder={t("organization_name")}
+ className="border-default h-7 rounded-[10px] border px-2 py-1.5 text-sm"
+ />
+
- {/* Progress dots - centered */}
-
+ {/* Organization Link */}
+
-
-
-
- {/* Main content */}
-
-
- {/* Card */}
-
-
- {/* Card Header */}
-
-
-
- {t("onboarding_org_details_title")}
-
-
- {t("onboarding_org_details_subtitle")}
-
-
-
-
- {/* Form */}
-
-
-
-
-
- {/* Organization Name */}
-
-
- {t("organization_name")}
-
- setOrganizationName(e.target.value)}
- placeholder={t("organization_name")}
- className="border-default h-7 rounded-[10px] border px-2 py-1.5 text-sm"
- />
-
-
- {/* Organization Link */}
-
-
- {/* Organization Bio */}
-
-
- {t("onboarding_org_bio_label")}
-
-
-
-
+ {/* Organization Bio */}
+
+
+ {t("onboarding_org_bio_label")}
+
+
-
- {/* Footer */}
-
-
- {t("continue")}
-
-
-
-
+
+
);
};
diff --git a/apps/web/modules/onboarding/organization/teams/organization-teams-view.tsx b/apps/web/modules/onboarding/organization/teams/organization-teams-view.tsx
index b4641060d9..645c618b74 100644
--- a/apps/web/modules/onboarding/organization/teams/organization-teams-view.tsx
+++ b/apps/web/modules/onboarding/organization/teams/organization-teams-view.tsx
@@ -9,8 +9,9 @@ import { useLocale } from "@calcom/lib/hooks/useLocale";
import { Button } from "@calcom/ui/components/button";
import { Form, TextField } from "@calcom/ui/components/form";
import { Icon } from "@calcom/ui/components/icon";
-import { Logo } from "@calcom/ui/components/logo";
+import { OnboardingCard } from "../../personal/_components/OnboardingCard";
+import { OnboardingLayout } from "../../personal/_components/OnboardingLayout";
import { useOnboardingStore } from "../../store/onboarding-store";
type OrganizationTeamsViewProps = {
@@ -63,107 +64,74 @@ export const OrganizationTeamsView = ({ userEmail }: OrganizationTeamsViewProps)
});
return (
-
- {/* Header */}
-
-
-
- {/* Progress dots - centered */}
-
-
-
-
-
- {/* Main content */}
-
-
- {/* Card */}
-
-
- {/* Card Header */}
-
-
-
{t("onboarding_org_teams_title")}
-
- {t("onboarding_org_teams_subtitle")}
-
+
+
+
+ {t("onboarding_skip_for_now")}
+
+
+ {t("continue")}
+
+ >
+ }>
+
+
+
+
);
};
diff --git a/apps/web/modules/onboarding/personal/_components/OnboardingCard.tsx b/apps/web/modules/onboarding/personal/_components/OnboardingCard.tsx
index ea97f9e8b2..03e2805120 100644
--- a/apps/web/modules/onboarding/personal/_components/OnboardingCard.tsx
+++ b/apps/web/modules/onboarding/personal/_components/OnboardingCard.tsx
@@ -23,7 +23,7 @@ export const OnboardingCard = ({ title, subtitle, children, footer, isLoading }:
{/* Content */}
-
+
{isLoading ? (
diff --git a/apps/web/modules/onboarding/personal/profile/personal-profile-view.tsx b/apps/web/modules/onboarding/personal/profile/personal-profile-view.tsx
index 8fbdb6beda..67efa29b4f 100644
--- a/apps/web/modules/onboarding/personal/profile/personal-profile-view.tsx
+++ b/apps/web/modules/onboarding/personal/profile/personal-profile-view.tsx
@@ -13,11 +13,12 @@ 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 { OnboardingCard } from "../_components/OnboardingCard";
+import { OnboardingLayout } from "../_components/OnboardingLayout";
import { useOnboardingStore } from "../../store/onboarding-store";
type PersonalProfileViewProps = {
@@ -93,128 +94,89 @@ export const PersonalProfileView = ({ userEmail }: PersonalProfileViewProps) =>
}
if (!user) {
- return null; // or a loading spinner
+ return null;
}
return (
-
- {/* Header */}
-
-
+
+
+ {t("continue")}
+
+ }>
+ {/* Form */}
+
+
+
+
+
+ {/* Avatar */}
+
+
{t("profile_photo")}
+
+ {user && }
+
+ {
+ if (avatarRef.current) {
+ avatarRef.current.value = newAvatar;
+ }
+ const nativeInputValueSetter = Object.getOwnPropertyDescriptor(
+ window.HTMLInputElement.prototype,
+ "value"
+ )?.set;
+ nativeInputValueSetter?.call(avatarRef.current, newAvatar);
+ const ev2 = new Event("input", { bubbles: true });
+ avatarRef.current?.dispatchEvent(ev2);
+ updateProfileHandler(newAvatar);
+ }}
+ imageSrc={imageSrc}
+ />
+
+
- {/* Progress dots - centered */}
-
+ {/* Username */}
+
+
+
-
-
-
- {/* Main content */}
-
-
- {/* Card */}
-
-
- {/* Card Header */}
-
-
-
{t("complete_your_profile")}
-
- {t("personal_profile_subtitle")}
-
-
-
-
- {/* Form */}
-
-
-
-
-
- {/* Avatar */}
-
-
- {t("profile_photo")}
-
-
- {user && }
-
- {
- if (avatarRef.current) {
- avatarRef.current.value = newAvatar;
- }
- const nativeInputValueSetter = Object.getOwnPropertyDescriptor(
- window.HTMLInputElement.prototype,
- "value"
- )?.set;
- nativeInputValueSetter?.call(avatarRef.current, newAvatar);
- const ev2 = new Event("input", { bubbles: true });
- avatarRef.current?.dispatchEvent(ev2);
- updateProfileHandler(newAvatar);
- }}
- imageSrc={imageSrc}
- />
-
-
-
- {/* Username */}
-
-
-
-
- {/* Bio */}
-
-
- {t("about")}
-
-
md.render(getValues("bio") || user?.bio || "")}
- setText={(value: string) => setValue("bio", turndown(value))}
- excludedToolbarItems={["blockType"]}
- firstRender={firstRender}
- setFirstRender={setFirstRender}
- />
-
- {t("few_sentences_about_yourself")}
-
-
-
-
+ {/* Bio */}
+
+
{t("about")}
+
md.render(getValues("bio") || user?.bio || "")}
+ setText={(value: string) => setValue("bio", turndown(value))}
+ excludedToolbarItems={["blockType"]}
+ firstRender={firstRender}
+ setFirstRender={setFirstRender}
+ />
+
+ {t("few_sentences_about_yourself")}
+
-
-
- {/* Footer */}
-
-
- {t("continue")}
-
-
+
-
-
+
+
);
};
diff --git a/apps/web/modules/onboarding/personal/settings/personal-settings-view.tsx b/apps/web/modules/onboarding/personal/settings/personal-settings-view.tsx
index 04238f116c..ae7689211c 100644
--- a/apps/web/modules/onboarding/personal/settings/personal-settings-view.tsx
+++ b/apps/web/modules/onboarding/personal/settings/personal-settings-view.tsx
@@ -14,10 +14,11 @@ 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";
+import { OnboardingCard } from "../_components/OnboardingCard";
+import { OnboardingLayout } from "../_components/OnboardingLayout";
type PersonalSettingsViewProps = {
userEmail: string;
@@ -79,101 +80,68 @@ export const PersonalSettingsView = ({ userEmail, userName }: PersonalSettingsVi
});
return (
-
+ <>
- {/* Header */}
-
-
+
+
+ {t("continue")}
+
+ }>
+ {/* 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}
+ )}
+
- {/* 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 */}
-
-
- {t("timezone")}
-
-
setSelectedTimeZone(value)}
- className="rounded-[10px] text-sm"
- />
-
- {t("current_time")}{" "}
- {dayjs().tz(selectedTimeZone).format("LT").toString().toLowerCase()}
-
-
-
+ {/* Timezone */}
+
+
+ {t("timezone")}
+
+
setSelectedTimeZone(value)}
+ size="sm"
+ />
+
+ {t("current_time")}{" "}
+ {dayjs().tz(selectedTimeZone).format("LT").toString().toLowerCase()}
+
-
- {/* Footer */}
-
-
- {t("continue")}
-
-
-
-
-
+
+
+ >
);
};