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
This commit is contained in:
sean-brydon
2025-10-29 12:51:21 +00:00
committed by GitHub
parent 18ad7e348e
commit aff494f21c
6 changed files with 394 additions and 568 deletions
@@ -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 (
<div className="bg-default flex min-h-screen w-full flex-col items-start overflow-clip rounded-xl">
{/* Header */}
<div className="flex w-full items-center justify-between px-6 py-4">
<Logo className="h-5 w-auto" />
<OnboardingLayout userEmail={userEmail} currentStep={2}>
<OnboardingCard
title={t("onboarding_org_brand_title")}
subtitle={t("onboarding_org_brand_subtitle")}
footer={
<>
<Button color="minimal" className="rounded-[10px]" onClick={handleSkip}>
{t("ill_do_this_later")}
</Button>
<Button color="primary" className="rounded-[10px]" onClick={handleContinue}>
{t("continue")}
</Button>
</>
}>
{/* Form */}
<div className="bg-default border-muted w-full rounded-[10px] border">
<div className="rounded-inherit flex w-full flex-col items-start overflow-clip">
<div className="flex w-full flex-col items-start">
<div className="flex w-full gap-6 px-5 py-4">
{/* Left side - Form */}
<div className="flex w-full flex-col gap-6">
{/* Brand Color */}
<div className="flex w-full flex-col gap-6">
<p className="text-emphasis text-sm font-medium leading-4">{t("brand_color")}</p>
<div className="flex w-full items-center gap-2">
<p className="text-subtle w-[98px] overflow-hidden text-ellipsis whitespace-nowrap text-sm font-medium leading-4">
{t("onboarding_primary_color_label")}
</p>
<BrandColorPicker
value={brandColor}
onChange={(value) => {
setBrandColor(value);
setOrganizationBrand({ color: value });
}}
t={t}
/>
</div>
</div>
{/* Progress dots - centered */}
<div className="absolute left-1/2 flex -translate-x-1/2 items-center justify-center gap-1">
<div className="bg-emphasis h-1 w-1 rounded-full" />
<div className="bg-emphasis h-1.5 w-1.5 rounded-full" />
<div className="bg-subtle h-1 w-1 rounded-full" />
<div className="bg-subtle h-1 w-1 rounded-full" />
</div>
{/* Logo Upload */}
<div className="flex w-full flex-col gap-2">
<p className="text-emphasis text-sm font-medium leading-4">{t("logo")}</p>
<div className="flex items-center gap-2">
<div className="bg-muted border-muted relative h-16 w-16 shrink-0 overflow-hidden rounded-md border">
{logoPreview && (
<img
src={logoPreview}
alt={t("onboarding_logo_preview_alt")}
className="h-full w-full object-cover"
/>
)}
</div>
<div className="flex flex-col gap-2">
<Button
color="secondary"
size="sm"
onClick={() => document.getElementById("logo-upload")?.click()}>
{t("upload")}
</Button>
<input
id="logo-upload"
type="file"
accept="image/*"
className="hidden"
onChange={(e) => handleLogoChange(e.target.files?.[0] || null)}
/>
</div>
</div>
<p className="text-subtle text-xs font-normal leading-3">{t("onboarding_logo_size_hint")}</p>
</div>
<div className="bg-muted flex items-center gap-2 rounded-full px-3 py-2">
<p className="text-emphasis text-sm font-medium leading-none">{userEmail}</p>
</div>
</div>
{/* Main content */}
<div className="flex h-full w-full items-start justify-center px-6 py-8">
<div className="relative flex w-full max-w-[600px] flex-col gap-6">
{/* Card */}
<div className="bg-muted border-muted relative rounded-xl border p-1">
<div className="rounded-inherit flex w-full flex-col items-start overflow-clip">
{/* Card Header */}
<div className="flex w-full gap-1.5 px-5 py-4">
<div className="flex w-full flex-col gap-1">
<h1 className="font-cal text-xl font-semibold leading-6">{t("onboarding_org_brand_title")}</h1>
<p className="text-subtle text-sm font-medium leading-tight">
{t("onboarding_org_brand_subtitle")}
</p>
{/* Banner Upload */}
<div className="flex w-full flex-col gap-2">
<p className="text-emphasis text-sm font-medium leading-4">
{t("onboarding_banner_label")}
</p>
<div className="flex w-full flex-col gap-2">
<div className="bg-muted border-muted relative h-[92px] w-full overflow-hidden rounded-md border">
{bannerPreview && (
<img
src={bannerPreview}
alt={t("onboarding_banner_preview_alt")}
className="h-full w-full object-cover"
/>
)}
</div>
<div className="flex flex-col gap-2">
<Button
color="secondary"
size="sm"
className="w-fit"
onClick={() => document.getElementById("banner-upload")?.click()}>
{t("upload")}
</Button>
<input
id="banner-upload"
type="file"
accept="image/*"
className="hidden"
onChange={(e) => handleBannerChange(e.target.files?.[0] || null)}
/>
</div>
</div>
<p className="text-subtle text-xs font-normal leading-3">
{t("onboarding_banner_size_hint")}
</p>
</div>
</div>
</div>
{/* Form */}
<div className="bg-default border-muted w-full rounded-[10px] border">
<div className="rounded-inherit flex w-full flex-col items-start overflow-clip">
<div className="flex w-full flex-col items-start">
<div className="flex w-full gap-6 px-5 py-4">
{/* Left side - Form */}
<div className="flex w-full flex-col gap-6">
{/* Brand Color */}
<div className="flex w-full flex-col gap-6">
<p className="text-emphasis text-sm font-medium leading-4">{t("brand_color")}</p>
<div className="flex w-full items-center gap-2">
<p className="text-subtle w-[98px] overflow-hidden text-ellipsis whitespace-nowrap text-sm font-medium leading-4">
{t("onboarding_primary_color_label")}
</p>
<BrandColorPicker
value={brandColor}
onChange={(value) => {
setBrandColor(value);
setOrganizationBrand({ color: value });
}}
t={t}
/>
</div>
</div>
{/* Right side - Preview */}
<div className="bg-muted border-muted flex hidden h-[328px] w-full grow overflow-hidden rounded-[10px] border p-5 md:block">
<div className="flex flex-col gap-2.5">
<p className="text-subtle text-sm font-medium leading-4">{t("preview")}</p>
<div className="border-subtle bg-default relative flex w-[110%] flex-col gap-2.5 rounded-md border px-5 pb-5 pt-[74px]">
{/* Banner preview */}
<div className="bg-muted border-muted absolute left-1 top-1 h-[92px] w-[272px] overflow-hidden rounded-[4px] border">
{bannerPreview && (
<img
src={bannerPreview}
alt={t("onboarding_banner_preview_alt")}
className="h-full w-full object-cover"
/>
)}
</div>
{/* Logo Upload */}
<div className="flex w-full flex-col gap-2">
<p className="text-emphasis text-sm font-medium leading-4">{t("logo")}</p>
<div className="flex items-center gap-2">
<div className="bg-muted border-muted relative h-16 w-16 shrink-0 overflow-hidden rounded-md border">
{/* Content */}
<div className="flex flex-col gap-4">
<div className="flex flex-col gap-3 p-1">
<div className="flex flex-col gap-3">
{/* Logo preview */}
<div className="bg-muted z-20 h-9 w-9 shrink-0 overflow-hidden rounded-md border-2 border-[var(--cal-bg)]">
{logoPreview && (
<img
src={logoPreview}
@@ -188,141 +256,38 @@ export const OrganizationBrandView = ({ userEmail }: OrganizationBrandViewProps)
/>
)}
</div>
<div className="flex flex-col gap-2">
<Button
color="secondary"
size="sm"
onClick={() => document.getElementById("logo-upload")?.click()}>
{t("upload")}
</Button>
<input
id="logo-upload"
type="file"
accept="image/*"
className="hidden"
onChange={(e) => handleLogoChange(e.target.files?.[0] || null)}
/>
<p className="text-subtle text-sm font-medium capitalize leading-4 ">
{organizationDetails.name || t("onboarding_preview_nameless")}
</p>
</div>
<div className="flex flex-col gap-3">
<div className="flex flex-col gap-3">
<p className="font-cal text-xl leading-5 tracking-[0.2px]">
{t("onboarding_preview_example_title")}
</p>
<p className="text-subtle text-sm font-medium leading-5">
{t("onboarding_preview_example_description")}
</p>
</div>
</div>
<p className="text-subtle text-xs font-normal leading-3">
{t("onboarding_logo_size_hint")}
</p>
</div>
{/* Banner Upload */}
<div className="flex w-full flex-col gap-2">
<p className="text-emphasis text-sm font-medium leading-4">{t("onboarding_banner_label")}</p>
<div className="flex w-full flex-col gap-2">
<div className="bg-muted border-muted relative h-[92px] w-full overflow-hidden rounded-md border">
{bannerPreview && (
<img
src={bannerPreview}
alt={t("onboarding_banner_preview_alt")}
className="h-full w-full object-cover"
/>
)}
<div className="flex flex-col gap-1">
{[134, 104, 84, 104].map((width, i) => (
<div key={i} className="flex items-center gap-2 p-1">
<div className="bg-subtle h-5 w-5 shrink-0 rounded-full" />
<div className="bg-subtle h-2.5 rounded-full" style={{ width: `${width}px` }} />
</div>
<div className="flex flex-col gap-2">
<Button
color="secondary"
size="sm"
className="w-fit"
onClick={() => document.getElementById("banner-upload")?.click()}>
{t("upload")}
</Button>
<input
id="banner-upload"
type="file"
accept="image/*"
className="hidden"
onChange={(e) => handleBannerChange(e.target.files?.[0] || null)}
/>
</div>
</div>
<p className="text-subtle text-xs font-normal leading-3">
{t("onboarding_banner_size_hint")}
</p>
</div>
</div>
{/* Right side - Preview */}
<div className="bg-muted border-muted flex hidden h-[328px] w-full grow overflow-hidden rounded-[10px] border p-5 md:block">
<div className="flex flex-col gap-2.5">
<p className="text-subtle text-sm font-medium leading-4">{t("preview")}</p>
<div className="border-subtle bg-default relative flex w-[110%] flex-col gap-2.5 rounded-md border px-5 pb-5 pt-[74px]">
{/* Banner preview */}
<div className="bg-muted border-muted absolute left-1 top-1 h-[92px] w-[272px] overflow-hidden rounded-[4px] border">
{bannerPreview && (
<img
src={bannerPreview}
alt={t("onboarding_banner_preview_alt")}
className="h-full w-full object-cover"
/>
)}
</div>
{/* Content */}
<div className="flex flex-col gap-4">
<div className="flex flex-col gap-3 p-1">
<div className="flex flex-col gap-3">
{/* Logo preview */}
<div className="bg-muted z-20 h-9 w-9 shrink-0 overflow-hidden rounded-md border-2 border-[var(--cal-bg)]">
{logoPreview && (
<img
src={logoPreview}
alt={t("onboarding_logo_preview_alt")}
className="h-full w-full object-cover"
/>
)}
</div>
<p className="text-subtle text-sm font-medium capitalize leading-4 ">
{organizationDetails.name || t("onboarding_preview_nameless")}
</p>
</div>
<div className="flex flex-col gap-3">
<div className="flex flex-col gap-3">
<p className="font-cal text-xl leading-5 tracking-[0.2px]">
{t("onboarding_preview_example_title")}
</p>
<p className="text-subtle text-sm font-medium leading-5">
{t("onboarding_preview_example_description")}
</p>
</div>
</div>
</div>
<div className="flex flex-col gap-1">
{[134, 104, 84, 104].map((width, i) => (
<div key={i} className="flex items-center gap-2 p-1">
<div className="bg-subtle h-5 w-5 shrink-0 rounded-full" />
<div
className="bg-subtle h-2.5 rounded-full"
style={{ width: `${width}px` }}
/>
</div>
))}
</div>
</div>
</div>
))}
</div>
</div>
</div>
</div>
</div>
</div>
{/* Footer */}
<div className="flex w-full items-center justify-end gap-1 px-5 py-4">
<Button color="minimal" className="rounded-[10px]" onClick={handleSkip}>
{t("ill_do_this_later")}
</Button>
<Button color="primary" className="rounded-[10px]" onClick={handleContinue}>
{t("continue")}
</Button>
</div>
</div>
</div>
</div>
</div>
</div>
</OnboardingCard>
</OnboardingLayout>
);
};
@@ -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 (
<div className="bg-default flex min-h-screen w-full flex-col items-start overflow-clip rounded-xl">
{/* Header */}
<div className="flex w-full items-center justify-between px-6 py-4">
<Logo className="h-5 w-auto" />
<OnboardingLayout userEmail={userEmail} currentStep={2}>
<OnboardingCard
title={t("onboarding_org_details_title")}
subtitle={t("onboarding_org_details_subtitle")}
footer={
<Button
color="primary"
className="rounded-[10px]"
onClick={handleContinue}
disabled={!isSlugValid || !organizationName || !organizationLink}>
{t("continue")}
</Button>
}>
{/* Form */}
<div className="bg-default border-muted w-full rounded-[10px] border">
<div className="rounded-inherit flex w-full flex-col items-start overflow-clip">
<div className="flex w-full flex-col items-start">
<div className="flex w-full gap-6 px-5 py-5">
<div className="flex w-full flex-col gap-4 rounded-xl">
{/* Organization Name */}
<div className="flex w-full flex-col gap-1.5">
<Label className="text-emphasis text-sm font-medium leading-4">
{t("organization_name")}
</Label>
<TextField
value={organizationName}
onChange={(e) => setOrganizationName(e.target.value)}
placeholder={t("organization_name")}
className="border-default h-7 rounded-[10px] border px-2 py-1.5 text-sm"
/>
</div>
{/* Progress dots - centered */}
<div className="absolute left-1/2 flex -translate-x-1/2 items-center justify-center gap-1">
<div className="bg-emphasis h-1 w-1 rounded-full" />
<div className="bg-emphasis h-1.5 w-1.5 rounded-full" />
<div className="bg-subtle h-1 w-1 rounded-full" />
<div className="bg-subtle h-1 w-1 rounded-full" />
</div>
{/* Organization Link */}
<ValidatedOrganizationSlug
value={organizationLink}
onChange={handleSlugChange}
onValidationChange={setIsSlugValid}
/>
<div className="bg-muted flex items-center gap-2 rounded-full px-3 py-2">
<p className="text-emphasis text-sm font-medium leading-none">{userEmail}</p>
</div>
</div>
{/* Main content */}
<div className="flex h-full w-full items-start justify-center px-6 py-8">
<div className="flex w-full max-w-[600px] flex-col gap-6">
{/* Card */}
<div className="bg-muted border-muted relative rounded-xl border p-1">
<div className="rounded-inherit flex w-full flex-col items-start overflow-clip">
{/* Card Header */}
<div className="flex w-full gap-1.5 px-5 py-4">
<div className="flex w-full flex-col gap-1">
<h1 className="font-cal text-xl font-semibold leading-6">
{t("onboarding_org_details_title")}
</h1>
<p className="text-subtle text-sm font-medium leading-tight">
{t("onboarding_org_details_subtitle")}
</p>
</div>
</div>
{/* Form */}
<div className="bg-default border-muted w-full rounded-[10px] border">
<div className="rounded-inherit flex w-full flex-col items-start overflow-clip">
<div className="flex w-full flex-col items-start">
<div className="flex w-full gap-6 px-5 py-5">
<div className="flex w-full flex-col gap-4 rounded-xl">
{/* Organization Name */}
<div className="flex w-full flex-col gap-1.5">
<Label className="text-emphasis text-sm font-medium leading-4">
{t("organization_name")}
</Label>
<TextField
value={organizationName}
onChange={(e) => setOrganizationName(e.target.value)}
placeholder={t("organization_name")}
className="border-default h-7 rounded-[10px] border px-2 py-1.5 text-sm"
/>
</div>
{/* Organization Link */}
<ValidatedOrganizationSlug
value={organizationLink}
onChange={handleSlugChange}
onValidationChange={setIsSlugValid}
/>
{/* Organization Bio */}
<div className="flex w-full flex-col gap-1.5">
<Label className="text-emphasis text-sm font-medium leading-4">
{t("onboarding_org_bio_label")}
</Label>
<TextArea
value={organizationBio}
onChange={(e) => setOrganizationBio(e.target.value)}
placeholder={t("onboarding_org_bio_placeholder")}
rows={4}
className="border-default rounded-lg border px-2 py-2 text-sm leading-tight"
/>
</div>
</div>
</div>
{/* Organization Bio */}
<div className="flex w-full flex-col gap-1.5">
<Label className="text-emphasis text-sm font-medium leading-4">
{t("onboarding_org_bio_label")}
</Label>
<TextArea
value={organizationBio}
onChange={(e) => setOrganizationBio(e.target.value)}
placeholder={t("onboarding_org_bio_placeholder")}
rows={4}
className="border-default rounded-lg border px-2 py-2 text-sm leading-tight"
/>
</div>
</div>
</div>
{/* Footer */}
<div className="flex w-full items-center justify-end gap-1 px-5 py-4">
<Button
color="primary"
className="rounded-[10px]"
onClick={handleContinue}
disabled={!isSlugValid || !organizationName || !organizationLink}>
{t("continue")}
</Button>
</div>
</div>
</div>
</div>
</div>
</div>
</OnboardingCard>
</OnboardingLayout>
);
};
@@ -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 (
<div className="bg-default flex min-h-screen w-full flex-col items-start overflow-clip rounded-xl">
{/* Header */}
<div className="flex w-full items-center justify-between px-6 py-4">
<Logo className="h-5 w-auto" />
{/* Progress dots - centered */}
<div className="absolute left-1/2 flex -translate-x-1/2 items-center justify-center gap-1">
<div className="bg-emphasis h-1 w-1 rounded-full" />
<div className="bg-emphasis h-1 w-1 rounded-full" />
<div className="bg-emphasis h-1.5 w-1.5 rounded-full" />
<div className="bg-subtle h-1 w-1 rounded-full" />
</div>
<div className="bg-muted flex items-center gap-2 rounded-full px-3 py-2">
<p className="text-emphasis text-sm font-medium leading-none">{userEmail}</p>
</div>
</div>
{/* Main content */}
<div className="flex h-full w-full items-start justify-center px-6 py-8">
<div className="flex w-full max-w-[600px] flex-col gap-4">
{/* Card */}
<div className="bg-muted border-muted relative rounded-xl border p-1">
<div className="rounded-inherit flex w-full flex-col items-start overflow-clip">
{/* Card Header */}
<div className="flex w-full gap-1.5 px-5 py-4">
<div className="flex w-full flex-col gap-1">
<h1 className="font-cal text-xl font-semibold leading-6">{t("onboarding_org_teams_title")}</h1>
<p className="text-subtle text-sm font-medium leading-tight">
{t("onboarding_org_teams_subtitle")}
</p>
<OnboardingLayout userEmail={userEmail} currentStep={3}>
<OnboardingCard
title={t("onboarding_org_teams_title")}
subtitle={t("onboarding_org_teams_subtitle")}
footer={
<>
<Button type="button" color="minimal" className="rounded-[10px]" onClick={handleSkip}>
{t("onboarding_skip_for_now")}
</Button>
<Button
type="submit"
form="teams-form"
color="primary"
className="rounded-[10px]"
disabled={!hasValidTeams}>
{t("continue")}
</Button>
</>
}>
<Form id="teams-form" form={form} handleSubmit={handleContinue} className="w-full">
<div className="bg-default border-subtle w-full rounded-md border">
<div className="flex w-full flex-col gap-8 px-5 py-5">
<div className="flex w-full flex-col gap-2">
<div className="flex flex-col gap-1">
<p className="text-emphasis text-sm font-medium leading-4">{t("team")}</p>
</div>
</div>
{/* Form */}
<Form form={form} handleSubmit={handleContinue} className="w-full">
<div className="bg-default border-subtle w-full rounded-md border">
<div className="flex w-full flex-col gap-8 px-5 py-5">
<div className="flex w-full flex-col gap-2">
<div className="flex flex-col gap-1">
<p className="text-emphasis text-sm font-medium leading-4">{t("team")}</p>
{/* Team fields */}
<div className="flex flex-col gap-2">
{fields.map((field, index) => (
<div key={field.id} className="flex w-full items-end gap-0.5">
<div className="flex-1">
<TextField
labelSrOnly
{...form.register(`teams.${index}.name`)}
placeholder={t("team")}
className="h-7 w-full rounded-[10px] text-sm"
/>
</div>
{/* Team fields */}
<div className="flex flex-col gap-2">
{fields.map((field, index) => (
<div key={field.id} className="flex w-full items-end gap-0.5">
<div className="flex-1">
<TextField
labelSrOnly
{...form.register(`teams.${index}.name`)}
placeholder={t("team")}
className="h-7 w-full rounded-[10px] text-sm"
/>
</div>
<Button
type="button"
color="minimal"
variant="icon"
size="sm"
className="h-7 w-7"
disabled={fields.length === 1}
onClick={() => remove(index)}>
<Icon name="x" className="h-4 w-4" />
</Button>
</div>
))}
</div>
{/* Add button */}
<Button
type="button"
color="secondary"
color="minimal"
variant="icon"
size="sm"
StartIcon="plus"
className="w-fit"
onClick={() => append({ name: "" })}>
{t("add")}
className="h-7 w-7"
disabled={fields.length === 1}
onClick={() => remove(index)}>
<Icon name="x" className="h-4 w-4" />
</Button>
</div>
</div>
))}
</div>
{/* Footer */}
<div className="relative flex w-full items-center justify-end gap-1 px-5 py-4">
<Button
type="button"
color="minimal"
className="absolute left-[368px] rounded-[10px]"
onClick={handleSkip}>
{t("onboarding_skip_for_now")}
</Button>
<Button type="submit" color="primary" className="rounded-[10px]" disabled={!hasValidTeams}>
{t("continue")}
</Button>
</div>
</Form>
{/* Add button */}
<Button
type="button"
color="secondary"
size="sm"
StartIcon="plus"
className="w-fit"
onClick={() => append({ name: "" })}>
{t("add")}
</Button>
</div>
</div>
</div>
</div>
</div>
</div>
</Form>
</OnboardingCard>
</OnboardingLayout>
);
};
@@ -23,7 +23,7 @@ export const OnboardingCard = ({ title, subtitle, children, footer, isLoading }:
</div>
{/* Content */}
<div className="flex w-full flex-col gap-4 px-5 py-5">
<div className="flex w-full flex-col gap-4">
{isLoading ? (
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2">
<SkeletonText className="h-40 w-full" />
@@ -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 (
<div className="bg-default flex min-h-screen w-full flex-col items-start overflow-clip rounded-xl">
{/* Header */}
<div className="flex w-full items-center justify-between px-6 py-4">
<Logo className="h-5 w-auto" />
<OnboardingLayout userEmail={userEmail} currentStep={2}>
<OnboardingCard
title={t("complete_your_profile")}
subtitle={t("personal_profile_subtitle")}
footer={
<Button
color="primary"
className="rounded-[10px]"
onClick={onSubmit}
loading={mutation.isPending}
disabled={mutation.isPending}>
{t("continue")}
</Button>
}>
{/* Form */}
<div className="bg-default border-muted w-full rounded-[10px] border">
<div className="rounded-inherit flex w-full flex-col items-start overflow-clip">
<div className="flex w-full flex-col items-start">
<form onSubmit={onSubmit} className="flex w-full gap-6 px-5 py-5">
<div className="flex w-full flex-col gap-6 rounded-xl">
{/* Avatar */}
<div className="flex w-full flex-col gap-2">
<Label className="text-emphasis text-sm font-medium leading-4">{t("profile_photo")}</Label>
<div className="flex flex-row items-center justify-start gap-4 rtl:justify-end">
{user && <UserAvatar size="lg" user={user} previewSrc={imageSrc} />}
<input
ref={avatarRef}
type="hidden"
name="avatar"
id="avatar"
defaultValue={imageSrc}
/>
<ImageUploader
target="avatar"
id="avatar-upload"
buttonMsg={t("add_profile_photo")}
handleAvatarChange={(newAvatar) => {
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}
/>
</div>
</div>
{/* Progress dots - centered */}
<div className="absolute left-1/2 flex -translate-x-1/2 items-center justify-center gap-1">
<div className="bg-emphasis h-1 w-1 rounded-full" />
<div className="bg-emphasis h-1.5 w-1.5 rounded-full" />
<div className="bg-subtle h-1 w-1 rounded-full" />
<div className="bg-subtle h-1 w-1 rounded-full" />
</div>
{/* Username */}
<div className="flex w-full flex-col gap-1.5">
<UsernameAvailabilityField />
</div>
<div className="bg-muted flex items-center gap-2 rounded-full px-3 py-2">
<p className="text-emphasis text-sm font-medium leading-none">{userEmail}</p>
</div>
</div>
{/* Main content */}
<div className="flex h-full w-full items-start justify-center px-6 py-8">
<div className="flex w-full max-w-[600px] flex-col gap-6">
{/* Card */}
<div className="bg-muted border-muted relative rounded-xl border p-1">
<div className="rounded-inherit flex w-full flex-col items-start overflow-clip">
{/* Card Header */}
<div className="flex w-full gap-1.5 px-5 py-4">
<div className="flex w-full flex-col gap-1">
<h1 className="font-cal text-xl font-semibold leading-6">{t("complete_your_profile")}</h1>
<p className="text-subtle text-sm font-medium leading-tight">
{t("personal_profile_subtitle")}
</p>
</div>
</div>
{/* Form */}
<div className="bg-default border-muted w-full rounded-[10px] border">
<div className="rounded-inherit flex w-full flex-col items-start overflow-clip">
<div className="flex w-full flex-col items-start">
<form onSubmit={onSubmit} className="flex w-full gap-6 px-5 py-5">
<div className="flex w-full flex-col gap-6 rounded-xl">
{/* Avatar */}
<div className="flex w-full flex-col gap-2">
<Label className="text-emphasis text-sm font-medium leading-4">
{t("profile_photo")}
</Label>
<div className="flex flex-row items-center justify-start gap-4 rtl:justify-end">
{user && <UserAvatar size="lg" user={user} previewSrc={imageSrc} />}
<input
ref={avatarRef}
type="hidden"
name="avatar"
id="avatar"
defaultValue={imageSrc}
/>
<ImageUploader
target="avatar"
id="avatar-upload"
buttonMsg={t("add_profile_photo")}
handleAvatarChange={(newAvatar) => {
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}
/>
</div>
</div>
{/* Username */}
<div className="flex w-full flex-col gap-1.5">
<UsernameAvailabilityField />
</div>
{/* Bio */}
<div className="flex w-full flex-col gap-1.5">
<Label className="text-emphasis mb-1 text-sm font-medium leading-4">
{t("about")}
</Label>
<Editor
getText={() => md.render(getValues("bio") || user?.bio || "")}
setText={(value: string) => setValue("bio", turndown(value))}
excludedToolbarItems={["blockType"]}
firstRender={firstRender}
setFirstRender={setFirstRender}
/>
<p className="text-subtle text-xs font-normal leading-3">
{t("few_sentences_about_yourself")}
</p>
</div>
</div>
</form>
{/* Bio */}
<div className="flex w-full flex-col gap-1.5">
<Label className="text-emphasis mb-1 text-sm font-medium leading-4">{t("about")}</Label>
<Editor
getText={() => md.render(getValues("bio") || user?.bio || "")}
setText={(value: string) => setValue("bio", turndown(value))}
excludedToolbarItems={["blockType"]}
firstRender={firstRender}
setFirstRender={setFirstRender}
/>
<p className="text-subtle text-xs font-normal leading-3">
{t("few_sentences_about_yourself")}
</p>
</div>
</div>
</div>
{/* Footer */}
<div className="flex w-full items-center justify-end gap-1 px-5 py-4">
<Button
color="primary"
className="rounded-[10px]"
onClick={onSubmit}
loading={mutation.isPending}
disabled={mutation.isPending}>
{t("continue")}
</Button>
</div>
</form>
</div>
</div>
</div>
</div>
</div>
</OnboardingCard>
</OnboardingLayout>
);
};
@@ -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 (
<div className="bg-default flex min-h-screen w-full flex-col items-start rounded-xl">
<>
<OnboardingContinuationPrompt />
{/* Header */}
<div className="flex w-full items-center justify-between px-6 py-4">
<Logo className="h-5 w-auto" />
<OnboardingLayout userEmail={userEmail} currentStep={1}>
<OnboardingCard
title={t("welcome_to_cal_com")}
subtitle={t("personal_settings_subtitle")}
footer={
<Button
color="primary"
className="rounded-[10px]"
onClick={handleContinue}
loading={mutation.isPending}
disabled={mutation.isPending || !form.formState.isValid}>
{t("continue")}
</Button>
}>
{/* Form */}
<div className="bg-default border-muted w-full rounded-[10px] border">
<div className="rounded-inherit flex w-full flex-col items-start">
<div className="flex w-full flex-col items-start">
<div className="flex w-full gap-6 px-5 py-5">
<div className="flex w-full flex-col gap-4 rounded-xl">
{/* Name */}
<div className="flex w-full flex-col gap-1.5">
<TextField
label={t("full_name")}
{...form.register("name")}
value={name}
onChange={(e) => {
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 && (
<p className="text-error text-sm">{form.formState.errors.name.message}</p>
)}
</div>
{/* Progress dots - centered */}
<div className="absolute left-1/2 flex -translate-x-1/2 items-center justify-center gap-1">
<div className="bg-emphasis h-1.5 w-1.5 rounded-full" />
<div className="bg-subtle h-1 w-1 rounded-full" />
<div className="bg-subtle h-1 w-1 rounded-full" />
<div className="bg-subtle h-1 w-1 rounded-full" />
</div>
<div className="bg-muted flex items-center gap-2 rounded-full px-3 py-2">
<p className="text-emphasis text-sm font-medium leading-none">{userEmail}</p>
</div>
</div>
{/* Main content */}
<div className="flex h-full w-full items-start justify-center px-6 py-8">
<div className="flex w-full max-w-[600px] flex-col gap-6">
{/* Card */}
<div className="bg-muted border-muted relative rounded-xl border p-1">
<div className="rounded-inherit flex w-full flex-col items-start ">
{/* Card Header */}
<div className="flex w-full gap-1.5 px-5 py-4">
<div className="flex w-full flex-col gap-1">
<h1 className="font-cal text-xl font-semibold leading-6">{t("welcome_to_cal_com")}</h1>
<p className="text-subtle text-sm font-medium leading-tight">
{t("personal_settings_subtitle")}
</p>
</div>
</div>
{/* Form */}
<div className="bg-default border-muted w-full rounded-[10px] border">
<div className="rounded-inherit flex w-full flex-col items-start">
<div className="flex w-full flex-col items-start">
<div className="flex w-full gap-6 px-5 py-5">
<div className="flex w-full flex-col gap-4 rounded-xl">
{/* Name */}
<div className="flex w-full flex-col gap-1.5">
<TextField
label={t("full_name")}
{...form.register("name")}
value={name}
onChange={(e) => {
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 && (
<p className="text-error text-sm">{form.formState.errors.name.message}</p>
)}
</div>
{/* Timezone */}
<div className="flex w-full flex-col gap-1.5">
<Label className="text-emphasis mb-0 text-sm font-medium leading-4">
{t("timezone")}
</Label>
<TimezoneSelect
value={selectedTimeZone}
onChange={({ value }) => setSelectedTimeZone(value)}
className="rounded-[10px] text-sm"
/>
<p className="text-subtle text-xs font-normal leading-3">
{t("current_time")}{" "}
{dayjs().tz(selectedTimeZone).format("LT").toString().toLowerCase()}
</p>
</div>
</div>
{/* Timezone */}
<div className="flex w-full flex-col gap-1.5">
<Label className="text-emphasis mb-0 text-sm font-medium leading-4">
{t("timezone")}
</Label>
<TimezoneSelect
value={selectedTimeZone}
onChange={({ value }) => setSelectedTimeZone(value)}
size="sm"
/>
<p className="text-subtle text-xs font-normal leading-3">
{t("current_time")}{" "}
{dayjs().tz(selectedTimeZone).format("LT").toString().toLowerCase()}
</p>
</div>
</div>
</div>
</div>
{/* Footer */}
<div className="flex w-full items-center justify-end gap-1 px-5 py-4">
<Button
color="primary"
className="rounded-[10px]"
onClick={handleContinue}
loading={mutation.isPending}
disabled={mutation.isPending || !form.formState.isValid}>
{t("continue")}
</Button>
</div>
</div>
</div>
</div>
</div>
</div>
</OnboardingCard>
</OnboardingLayout>
</>
);
};