Skip to content

Commit aff494f

Browse files
authored
chore: refactor onboarding v3 to use reusable components (calcom#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
1 parent 18ad7e3 commit aff494f

6 files changed

Lines changed: 395 additions & 569 deletions

File tree

apps/web/modules/onboarding/organization/brand/organization-brand-view.tsx

Lines changed: 147 additions & 182 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,9 @@ import { HexColorPicker } from "react-colorful";
77

88
import { useLocale } from "@calcom/lib/hooks/useLocale";
99
import { Button } from "@calcom/ui/components/button";
10-
import { Logo } from "@calcom/ui/components/logo";
1110

11+
import { OnboardingCard } from "../../personal/_components/OnboardingCard";
12+
import { OnboardingLayout } from "../../personal/_components/OnboardingLayout";
1213
import { useOnboardingStore } from "../../store/onboarding-store";
1314

1415
type OrganizationBrandViewProps = {
@@ -116,70 +117,137 @@ export const OrganizationBrandView = ({ userEmail }: OrganizationBrandViewProps)
116117
};
117118

118119
return (
119-
<div className="bg-default flex min-h-screen w-full flex-col items-start overflow-clip rounded-xl">
120-
{/* Header */}
121-
<div className="flex w-full items-center justify-between px-6 py-4">
122-
<Logo className="h-5 w-auto" />
123-
124-
{/* Progress dots - centered */}
125-
<div className="absolute left-1/2 flex -translate-x-1/2 items-center justify-center gap-1">
126-
<div className="bg-emphasis h-1 w-1 rounded-full" />
127-
<div className="bg-emphasis h-1.5 w-1.5 rounded-full" />
128-
<div className="bg-subtle h-1 w-1 rounded-full" />
129-
<div className="bg-subtle h-1 w-1 rounded-full" />
130-
</div>
120+
<OnboardingLayout userEmail={userEmail} currentStep={2}>
121+
<OnboardingCard
122+
title={t("onboarding_org_brand_title")}
123+
subtitle={t("onboarding_org_brand_subtitle")}
124+
footer={
125+
<>
126+
<Button color="minimal" className="rounded-[10px]" onClick={handleSkip}>
127+
{t("ill_do_this_later")}
128+
</Button>
129+
<Button color="primary" className="rounded-[10px]" onClick={handleContinue}>
130+
{t("continue")}
131+
</Button>
132+
</>
133+
}>
134+
{/* Form */}
135+
<div className="bg-default border-muted w-full rounded-[10px] border">
136+
<div className="rounded-inherit flex w-full flex-col items-start overflow-clip">
137+
<div className="flex w-full flex-col items-start">
138+
<div className="flex w-full gap-6 px-5 py-4">
139+
{/* Left side - Form */}
140+
<div className="flex w-full flex-col gap-6">
141+
{/* Brand Color */}
142+
<div className="flex w-full flex-col gap-6">
143+
<p className="text-emphasis text-sm font-medium leading-4">{t("brand_color")}</p>
144+
<div className="flex w-full items-center gap-2">
145+
<p className="text-subtle w-[98px] overflow-hidden text-ellipsis whitespace-nowrap text-sm font-medium leading-4">
146+
{t("onboarding_primary_color_label")}
147+
</p>
148+
<BrandColorPicker
149+
value={brandColor}
150+
onChange={(value) => {
151+
setBrandColor(value);
152+
setOrganizationBrand({ color: value });
153+
}}
154+
t={t}
155+
/>
156+
</div>
157+
</div>
131158

132-
<div className="bg-muted flex items-center gap-2 rounded-full px-3 py-2">
133-
<p className="text-emphasis text-sm font-medium leading-none">{userEmail}</p>
134-
</div>
135-
</div>
159+
{/* Logo Upload */}
160+
<div className="flex w-full flex-col gap-2">
161+
<p className="text-emphasis text-sm font-medium leading-4">{t("logo")}</p>
162+
<div className="flex items-center gap-2">
163+
<div className="bg-muted border-muted relative h-16 w-16 shrink-0 overflow-hidden rounded-md border">
164+
{logoPreview && (
165+
<img
166+
src={logoPreview}
167+
alt={t("onboarding_logo_preview_alt")}
168+
className="h-full w-full object-cover"
169+
/>
170+
)}
171+
</div>
172+
<div className="flex flex-col gap-2">
173+
<Button
174+
color="secondary"
175+
size="sm"
176+
onClick={() => document.getElementById("logo-upload")?.click()}>
177+
{t("upload")}
178+
</Button>
179+
<input
180+
id="logo-upload"
181+
type="file"
182+
accept="image/*"
183+
className="hidden"
184+
onChange={(e) => handleLogoChange(e.target.files?.[0] || null)}
185+
/>
186+
</div>
187+
</div>
188+
<p className="text-subtle text-xs font-normal leading-3">{t("onboarding_logo_size_hint")}</p>
189+
</div>
136190

137-
{/* Main content */}
138-
<div className="flex h-full w-full items-start justify-center px-6 py-8">
139-
<div className="relative flex w-full max-w-[600px] flex-col gap-6">
140-
{/* Card */}
141-
<div className="bg-muted border-muted relative rounded-xl border p-1">
142-
<div className="rounded-inherit flex w-full flex-col items-start overflow-clip">
143-
{/* Card Header */}
144-
<div className="flex w-full gap-1.5 px-5 py-4">
145-
<div className="flex w-full flex-col gap-1">
146-
<h1 className="font-cal text-xl font-semibold leading-6">{t("onboarding_org_brand_title")}</h1>
147-
<p className="text-subtle text-sm font-medium leading-tight">
148-
{t("onboarding_org_brand_subtitle")}
149-
</p>
191+
{/* Banner Upload */}
192+
<div className="flex w-full flex-col gap-2">
193+
<p className="text-emphasis text-sm font-medium leading-4">
194+
{t("onboarding_banner_label")}
195+
</p>
196+
<div className="flex w-full flex-col gap-2">
197+
<div className="bg-muted border-muted relative h-[92px] w-full overflow-hidden rounded-md border">
198+
{bannerPreview && (
199+
<img
200+
src={bannerPreview}
201+
alt={t("onboarding_banner_preview_alt")}
202+
className="h-full w-full object-cover"
203+
/>
204+
)}
205+
</div>
206+
<div className="flex flex-col gap-2">
207+
<Button
208+
color="secondary"
209+
size="sm"
210+
className="w-fit"
211+
onClick={() => document.getElementById("banner-upload")?.click()}>
212+
{t("upload")}
213+
</Button>
214+
<input
215+
id="banner-upload"
216+
type="file"
217+
accept="image/*"
218+
className="hidden"
219+
onChange={(e) => handleBannerChange(e.target.files?.[0] || null)}
220+
/>
221+
</div>
222+
</div>
223+
<p className="text-subtle text-xs font-normal leading-3">
224+
{t("onboarding_banner_size_hint")}
225+
</p>
226+
</div>
150227
</div>
151-
</div>
152228

153-
{/* Form */}
154-
<div className="bg-default border-muted w-full rounded-[10px] border">
155-
<div className="rounded-inherit flex w-full flex-col items-start overflow-clip">
156-
<div className="flex w-full flex-col items-start">
157-
<div className="flex w-full gap-6 px-5 py-4">
158-
{/* Left side - Form */}
159-
<div className="flex w-full flex-col gap-6">
160-
{/* Brand Color */}
161-
<div className="flex w-full flex-col gap-6">
162-
<p className="text-emphasis text-sm font-medium leading-4">{t("brand_color")}</p>
163-
<div className="flex w-full items-center gap-2">
164-
<p className="text-subtle w-[98px] overflow-hidden text-ellipsis whitespace-nowrap text-sm font-medium leading-4">
165-
{t("onboarding_primary_color_label")}
166-
</p>
167-
<BrandColorPicker
168-
value={brandColor}
169-
onChange={(value) => {
170-
setBrandColor(value);
171-
setOrganizationBrand({ color: value });
172-
}}
173-
t={t}
174-
/>
175-
</div>
176-
</div>
229+
{/* Right side - Preview */}
230+
<div className="bg-muted border-muted flex hidden h-[328px] w-full grow overflow-hidden rounded-[10px] border p-5 md:block">
231+
<div className="flex flex-col gap-2.5">
232+
<p className="text-subtle text-sm font-medium leading-4">{t("preview")}</p>
233+
<div className="border-subtle bg-default relative flex w-[110%] flex-col gap-2.5 rounded-md border px-5 pb-5 pt-[74px]">
234+
{/* Banner preview */}
235+
<div className="bg-muted border-muted absolute left-1 top-1 h-[92px] w-[272px] overflow-hidden rounded-[4px] border">
236+
{bannerPreview && (
237+
<img
238+
src={bannerPreview}
239+
alt={t("onboarding_banner_preview_alt")}
240+
className="h-full w-full object-cover"
241+
/>
242+
)}
243+
</div>
177244

178-
{/* Logo Upload */}
179-
<div className="flex w-full flex-col gap-2">
180-
<p className="text-emphasis text-sm font-medium leading-4">{t("logo")}</p>
181-
<div className="flex items-center gap-2">
182-
<div className="bg-muted border-muted relative h-16 w-16 shrink-0 overflow-hidden rounded-md border">
245+
{/* Content */}
246+
<div className="flex flex-col gap-4">
247+
<div className="flex flex-col gap-3 p-1">
248+
<div className="flex flex-col gap-3">
249+
{/* Logo preview */}
250+
<div className="bg-muted z-20 h-9 w-9 shrink-0 overflow-hidden rounded-md border-2 border-[var(--cal-bg)]">
183251
{logoPreview && (
184252
<img
185253
src={logoPreview}
@@ -188,141 +256,38 @@ export const OrganizationBrandView = ({ userEmail }: OrganizationBrandViewProps)
188256
/>
189257
)}
190258
</div>
191-
<div className="flex flex-col gap-2">
192-
<Button
193-
color="secondary"
194-
size="sm"
195-
onClick={() => document.getElementById("logo-upload")?.click()}>
196-
{t("upload")}
197-
</Button>
198-
<input
199-
id="logo-upload"
200-
type="file"
201-
accept="image/*"
202-
className="hidden"
203-
onChange={(e) => handleLogoChange(e.target.files?.[0] || null)}
204-
/>
205-
</div>
259+
<p className="text-subtle text-sm font-medium capitalize leading-4 ">
260+
{organizationDetails.name || t("onboarding_preview_nameless")}
261+
</p>
206262
</div>
207-
<p className="text-subtle text-xs font-normal leading-3">
208-
{t("onboarding_logo_size_hint")}
209-
</p>
210-
</div>
211-
212-
{/* Banner Upload */}
213-
<div className="flex w-full flex-col gap-2">
214-
<p className="text-emphasis text-sm font-medium leading-4">{t("onboarding_banner_label")}</p>
215-
<div className="flex w-full flex-col gap-2">
216-
<div className="bg-muted border-muted relative h-[92px] w-full overflow-hidden rounded-md border">
217-
{bannerPreview && (
218-
<img
219-
src={bannerPreview}
220-
alt={t("onboarding_banner_preview_alt")}
221-
className="h-full w-full object-cover"
222-
/>
223-
)}
224-
</div>
225-
<div className="flex flex-col gap-2">
226-
<Button
227-
color="secondary"
228-
size="sm"
229-
className="w-fit"
230-
onClick={() => document.getElementById("banner-upload")?.click()}>
231-
{t("upload")}
232-
</Button>
233-
<input
234-
id="banner-upload"
235-
type="file"
236-
accept="image/*"
237-
className="hidden"
238-
onChange={(e) => handleBannerChange(e.target.files?.[0] || null)}
239-
/>
263+
<div className="flex flex-col gap-3">
264+
<div className="flex flex-col gap-3">
265+
<p className="font-cal text-xl leading-5 tracking-[0.2px]">
266+
{t("onboarding_preview_example_title")}
267+
</p>
268+
<p className="text-subtle text-sm font-medium leading-5">
269+
{t("onboarding_preview_example_description")}
270+
</p>
240271
</div>
241272
</div>
242-
<p className="text-subtle text-xs font-normal leading-3">
243-
{t("onboarding_banner_size_hint")}
244-
</p>
245273
</div>
246-
</div>
247-
248-
{/* Right side - Preview */}
249-
<div className="bg-muted border-muted flex hidden h-[328px] w-full grow overflow-hidden rounded-[10px] border p-5 md:block">
250-
<div className="flex flex-col gap-2.5">
251-
<p className="text-subtle text-sm font-medium leading-4">{t("preview")}</p>
252-
<div className="border-subtle bg-default relative flex w-[110%] flex-col gap-2.5 rounded-md border px-5 pb-5 pt-[74px]">
253-
{/* Banner preview */}
254-
<div className="bg-muted border-muted absolute left-1 top-1 h-[92px] w-[272px] overflow-hidden rounded-[4px] border">
255-
{bannerPreview && (
256-
<img
257-
src={bannerPreview}
258-
alt={t("onboarding_banner_preview_alt")}
259-
className="h-full w-full object-cover"
260-
/>
261-
)}
262-
</div>
263-
264-
{/* Content */}
265-
<div className="flex flex-col gap-4">
266-
<div className="flex flex-col gap-3 p-1">
267-
<div className="flex flex-col gap-3">
268-
{/* Logo preview */}
269-
<div className="bg-muted z-20 h-9 w-9 shrink-0 overflow-hidden rounded-md border-2 border-[var(--cal-bg)]">
270-
{logoPreview && (
271-
<img
272-
src={logoPreview}
273-
alt={t("onboarding_logo_preview_alt")}
274-
className="h-full w-full object-cover"
275-
/>
276-
)}
277-
</div>
278-
<p className="text-subtle text-sm font-medium capitalize leading-4 ">
279-
{organizationDetails.name || t("onboarding_preview_nameless")}
280-
</p>
281-
</div>
282-
<div className="flex flex-col gap-3">
283-
<div className="flex flex-col gap-3">
284-
<p className="font-cal text-xl leading-5 tracking-[0.2px]">
285-
{t("onboarding_preview_example_title")}
286-
</p>
287-
<p className="text-subtle text-sm font-medium leading-5">
288-
{t("onboarding_preview_example_description")}
289-
</p>
290-
</div>
291-
</div>
292-
</div>
293-
<div className="flex flex-col gap-1">
294-
{[134, 104, 84, 104].map((width, i) => (
295-
<div key={i} className="flex items-center gap-2 p-1">
296-
<div className="bg-subtle h-5 w-5 shrink-0 rounded-full" />
297-
<div
298-
className="bg-subtle h-2.5 rounded-full"
299-
style={{ width: `${width}px` }}
300-
/>
301-
</div>
302-
))}
303-
</div>
274+
<div className="flex flex-col gap-1">
275+
{[134, 104, 84, 104].map((width, i) => (
276+
<div key={i} className="flex items-center gap-2 p-1">
277+
<div className="bg-subtle h-5 w-5 shrink-0 rounded-full" />
278+
<div className="bg-subtle h-2.5 rounded-full" style={{ width: `${width}px` }} />
304279
</div>
305-
</div>
280+
))}
306281
</div>
307282
</div>
308283
</div>
309284
</div>
310285
</div>
311286
</div>
312-
313-
{/* Footer */}
314-
<div className="flex w-full items-center justify-end gap-1 px-5 py-4">
315-
<Button color="minimal" className="rounded-[10px]" onClick={handleSkip}>
316-
{t("ill_do_this_later")}
317-
</Button>
318-
<Button color="primary" className="rounded-[10px]" onClick={handleContinue}>
319-
{t("continue")}
320-
</Button>
321-
</div>
322287
</div>
323288
</div>
324289
</div>
325-
</div>
326-
</div>
290+
</OnboardingCard>
291+
</OnboardingLayout>
327292
);
328293
};

0 commit comments

Comments
 (0)