From 52e27bab536501b3842f1b1826c44f0e41bebab3 Mon Sep 17 00:00:00 2001 From: sean-brydon <55134778+sean-brydon@users.noreply.github.com> Date: Fri, 7 Nov 2025 07:58:34 +0000 Subject: [PATCH 1/3] chore: Add shared onboarding components (OnboardingCard, OnboardingLayout, browser views) (#24946) ## What does this PR do? Creates new onboarding UI components for the user onboarding flow, including: - `OnboardingCard` - A reusable card component with title, subtitle, content, and footer sections - `OnboardingLayout` - A layout component with progress indicators and sign-out functionality - `OnboardingBrowserView` - A browser preview showing how the user's booking page will look - `OnboardingCalendarBrowserView` - A calendar preview showing sample events - `OnboardingInviteBrowserView` - A preview of the team invitation email - Enhanced `PlanIcon` component with new variants and animations for organization and team plans ## Visual Demo (For contributors especially) #### Image Demo: The PR adds several visual components for the onboarding flow, including browser previews, calendar views, and animated plan icons with concentric rings and user avatars. ## 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 to the onboarding flow to see the new components in action - Test the layout with different screen sizes to ensure responsive behavior - Verify that the browser previews render correctly with sample data - Check that the plan icon animations work properly for different variants (single, team, organization) - Ensure the calendar view displays sample events correctly ## 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 --- .../onboarding/components/OnboardingCard.tsx | 41 ++++ .../components/OnboardingLayout.tsx | 64 ++++++ .../components/onboarding-browser-view.tsx | 134 ++++++++++++ .../onboarding-invite-browser-view.tsx | 67 ++++++ .../onboarding/components/plan-icon.tsx | 205 +++++++++++------- .../getting-started/onboarding-view.tsx | 4 +- 6 files changed, 437 insertions(+), 78 deletions(-) create mode 100644 apps/web/modules/onboarding/components/OnboardingCard.tsx create mode 100644 apps/web/modules/onboarding/components/OnboardingLayout.tsx create mode 100644 apps/web/modules/onboarding/components/onboarding-browser-view.tsx create mode 100644 apps/web/modules/onboarding/components/onboarding-invite-browser-view.tsx diff --git a/apps/web/modules/onboarding/components/OnboardingCard.tsx b/apps/web/modules/onboarding/components/OnboardingCard.tsx new file mode 100644 index 00000000000000..72ca7500db1bb6 --- /dev/null +++ b/apps/web/modules/onboarding/components/OnboardingCard.tsx @@ -0,0 +1,41 @@ +import type { ReactNode } from "react"; + +import { SkeletonText } from "@calcom/ui/components/skeleton"; + +type OnboardingCardProps = { + title: string; + subtitle: string; + children: ReactNode; + footer: ReactNode; + isLoading?: boolean; +}; + +export const OnboardingCard = ({ title, subtitle, children, footer, isLoading }: OnboardingCardProps) => { + return ( +
+ {/* Card Header */} +
+
+

{title}

+

{subtitle}

+
+
+ + {/* Content */} +
+ {isLoading ? ( +
+ + +
+ ) : ( + children + )} +
+ + {/* Footer */} +
{footer}
+
+ ); +}; + diff --git a/apps/web/modules/onboarding/components/OnboardingLayout.tsx b/apps/web/modules/onboarding/components/OnboardingLayout.tsx new file mode 100644 index 00000000000000..1a538362f0c1a6 --- /dev/null +++ b/apps/web/modules/onboarding/components/OnboardingLayout.tsx @@ -0,0 +1,64 @@ +"use client"; + +import { signOut } from "next-auth/react"; +import { Children, type ReactNode } from "react"; + +import { useLocale } from "@calcom/lib/hooks/useLocale"; +import { Button } from "@calcom/ui/components/button"; +import { Logo } from "@calcom/ui/components/logo"; + +type OnboardingLayoutProps = { + userEmail: string; + currentStep: 1 | 2 | 3; + children: ReactNode; +}; + +export const OnboardingLayout = ({ userEmail, currentStep, children }: OnboardingLayoutProps) => { + const { t } = useLocale(); + + const handleSignOut = () => { + signOut({ callbackUrl: "/auth/logout" }); + }; + + // Extract children as array + const childrenArray = Children.toArray(children); + const column1 = childrenArray[0]; + const column2 = childrenArray[1]; + + return ( +
+ {/* Logo and container - centered */} +
+ +
+ {/* Column 1 - Always visible, 40% on xl+ */} +
{column1}
+ {/* Column 2 - Hidden on mobile, visible on xl+, 60% on xl+ */} + {column2 && ( +
{column2}
+ )} +
+
+ + {/* Footer with progress dots and sign out */} +
+
+ {[1, 2, 3].map((step) => ( +
+
+ {step <= currentStep &&
} +
+ ))} +
+ +
+
+ ); +}; + diff --git a/apps/web/modules/onboarding/components/onboarding-browser-view.tsx b/apps/web/modules/onboarding/components/onboarding-browser-view.tsx new file mode 100644 index 00000000000000..37362668712702 --- /dev/null +++ b/apps/web/modules/onboarding/components/onboarding-browser-view.tsx @@ -0,0 +1,134 @@ +"use client"; + +import { WEBAPP_URL } from "@calcom/lib/constants"; +import { useLocale } from "@calcom/lib/hooks/useLocale"; +import { Avatar } from "@calcom/ui/components/avatar"; +import { Button } from "@calcom/ui/components/button"; +import { Icon, type IconName } from "@calcom/ui/components/icon"; + +type OnboardingBrowserViewProps = { + avatar?: string | null; + name?: string; + bio?: string; + username?: string | null; + teamSlug?: string; +}; + +export const OnboardingBrowserView = ({ + avatar, + name, + bio, + username, + teamSlug, +}: OnboardingBrowserViewProps) => { + const { t } = useLocale(); + const webappUrl = WEBAPP_URL.replace(/^https?:\/\//, ""); + const displayUrl = + teamSlug !== undefined ? `${webappUrl}/team/${teamSlug || ""}` : `${webappUrl}/${username || ""}`; + + const events: Array<{ + title: string; + description: string; + duration: number; + icon: IconName; + }> = [ + { + title: t("onboarding_browser_view_demo"), + description: t("onboarding_browser_view_demo_description"), + duration: 15, + icon: "bell", + }, + { + title: t("onboarding_browser_view_quick_meeting"), + description: t("onboarding_browser_view_quick_meeting_description"), + duration: 15, + icon: "bell", + }, + { + title: t("onboarding_browser_view_longer_meeting"), + description: t("onboarding_browser_view_longer_meeting_description"), + duration: 30, + icon: "clock", + }, + { + title: t("in_person_meeting"), + description: t("onboarding_browser_view_in_person_description"), + duration: 120, + icon: "map-pin", + }, + { + title: t("onboarding_browser_view_ask_question"), + description: t("onboarding_browser_view_ask_question_description"), + duration: 15, + icon: "message-circle", + }, + ]; + return ( +
+ {/* Browser header */} +
+ {/* Navigation buttons */} +
+ + + +
+
+ +

{displayUrl}

+
+ +
+ {/* Content */} +
+
+ {/* Profile Header */} +
+
+ +
+

+ {name || t("your_name")} +

+

+ {bio || t("onboarding_browser_view_default_bio")} +

+
+
+
+ + {/* Events List */} +
+ {events.map((event, index) => ( +
+ {index > 0 &&
} +
+
+
+

{event.title}

+
+ + + {event.duration} {t("minute_timeUnit")} + +
+
+

{event.description}

+
+ +
+
+ ))} +
+
+
+
+ ); +}; diff --git a/apps/web/modules/onboarding/components/onboarding-invite-browser-view.tsx b/apps/web/modules/onboarding/components/onboarding-invite-browser-view.tsx new file mode 100644 index 00000000000000..e9ce7747bcf046 --- /dev/null +++ b/apps/web/modules/onboarding/components/onboarding-invite-browser-view.tsx @@ -0,0 +1,67 @@ +"use client"; + +import { trpc } from "@calcom/trpc/react"; +import { Avatar } from "@calcom/ui/components/avatar"; + +import { useOnboardingStore } from "../store/onboarding-store"; + +type OnboardingInviteBrowserViewProps = { + teamName?: string; +}; + +export const OnboardingInviteBrowserView = ({ teamName }: OnboardingInviteBrowserViewProps) => { + const { data: user } = trpc.viewer.me.get.useQuery(); + const { teamBrand } = useOnboardingStore(); + + // Use default values if not provided + const rawInviterName = user?.name || user?.username || "Alex"; + const displayInviterName = rawInviterName.charAt(0).toUpperCase() + rawInviterName.slice(1); + const displayTeamName = teamName || "Deel"; + const teamAvatar = teamBrand.logo || null; + + return ( +
+ {/* Browser header */} +
+ {/* Navigation buttons */} +
+
+
+
+
+
+
+

mail.example.com

+
+
+ + {/* Content */} +
+
+ {/* Email Header */} +
+
+ +
+

+ {displayInviterName} invited you to join {displayTeamName} +

+

+ We're emailing you all the details +

+
+
+
+ + {/* Email Body */} +
+
+
+
+ ); +}; diff --git a/apps/web/modules/onboarding/components/plan-icon.tsx b/apps/web/modules/onboarding/components/plan-icon.tsx index bd047409794dfc..52e9cf8b99c912 100644 --- a/apps/web/modules/onboarding/components/plan-icon.tsx +++ b/apps/web/modules/onboarding/components/plan-icon.tsx @@ -1,24 +1,68 @@ -import classNames from "@calcom/ui/classNames"; +import { motion } from "framer-motion"; + import { Icon, type IconName } from "@calcom/ui/components/icon"; -export function PlanIcon({ icon, variant = "single" }: { icon: IconName; variant?: "single" | "double" }) { - const renderIconContainer = (index: number) => { - // For double variant: 55px icon width + 24px gap = 79px between centers, so ±39.5px from center - const leftPosition = variant === "double" ? `calc(50% + ${index === 0 ? -39.5 : 39.5}px)` : "50%"; +// Ring sizes - just the diameters, all centered on the icon +const RING_SIZES = [166, 233, 345, 465]; + +// Positions for small user icons on rings (for team variant) +// Format: [ringIndex, angleInDegrees] +// Angles: 0 = top, 90 = right, 180 = bottom, 270 = left +const TEAM_ICON_POSITIONS = [ + { ringIndex: 2, angle: 30 }, // Top-right of third ring + { ringIndex: 2, angle: 150 }, // Bottom-left of third ring + { ringIndex: 2, angle: 210 }, // Bottom-left of third ring + { ringIndex: 2, angle: 330 }, // Top-left of third ring + { ringIndex: 3, angle: 0 }, // Top of largest ring + { ringIndex: 3, angle: 180 }, // Bottom of largest ring +]; +export function PlanIcon({ + icon, + variant = "single", + animationDirection = "down", +}: { + icon: IconName; + variant?: "single" | "organization" | "team"; + animationDirection?: "up" | "down"; +}) { + const renderIconContainer = (index: number) => { return ( -
{/* Icon */} -
- +
+
{/* Inner highlight/shine effect */} @@ -29,79 +73,88 @@ export function PlanIcon({ icon, variant = "single" }: { icon: IconName; variant opacity: 0.15, }} /> -
+ ); }; - return ( -
- {/* Outer ring - SVG with linear gradient */} - - - - - - - - - + const renderSmallUserIcon = (ringIndex: number, angle: number, index: number) => { + const ringSize = RING_SIZES[ringIndex]; + const radius = ringSize / 2; + const angleRad = (angle * Math.PI) / 180; + + // Calculate position on the ring + const x = Math.cos(angleRad) * radius; + const y = Math.sin(angleRad) * radius; + const iconHalfSize = 20.25; // Half of 40.5px - {/* Middle ring - SVG with linear gradient */} - - + +
+
- - - - - - - + + ); + }; + + return ( +
+ {/* Generate concentric rings centered on icon */} + {RING_SIZES.map((size, index) => { + const opacity = [0.6, 0.5, 0.4, 0.35][index] || 0.3; + return ( +
+ ); + })} + + {/* Small user icons on rings (for team variant) */} + {(variant === "team" || variant === "organization") && + TEAM_ICON_POSITIONS.map(({ ringIndex, angle }, index) => + renderSmallUserIcon(ringIndex, angle, index) + )} - {/* Main icon container(s) with gradient background */} - {variant === "single" ? renderIconContainer(0) : [renderIconContainer(0), renderIconContainer(1)]} + {renderIconContainer(0)}
); } diff --git a/apps/web/modules/onboarding/getting-started/onboarding-view.tsx b/apps/web/modules/onboarding/getting-started/onboarding-view.tsx index 4a908efa8e243d..0fd6c51ebf886a 100644 --- a/apps/web/modules/onboarding/getting-started/onboarding-view.tsx +++ b/apps/web/modules/onboarding/getting-started/onboarding-view.tsx @@ -10,9 +10,9 @@ import { Button } from "@calcom/ui/components/button"; import { type IconName } from "@calcom/ui/components/icon"; import { RadioAreaGroup } from "@calcom/ui/components/radio"; +import { OnboardingLayout } from "../components/OnboardingLayout"; import { OnboardingContinuationPrompt } from "../components/onboarding-continuation-prompt"; import { PlanIcon } from "../components/plan-icon"; -import { OnboardingLayout } from "../personal/_components/OnboardingLayout"; import { useOnboardingStore, type PlanType } from "../store/onboarding-store"; type OnboardingViewProps = { @@ -64,7 +64,7 @@ export const OnboardingView = ({ userName, userEmail }: OnboardingViewProps) => badge: t("onboarding_plan_organization_badge"), description: t("onboarding_plan_organization_description"), icon: planIconByType.organization, - variant: "double" as const, + variant: "organization" as const, }, ]; From 02b1393ff428cfbc7b8a9f7ebac3a74ab5f9e70c Mon Sep 17 00:00:00 2001 From: sean-brydon <55134778+sean-brydon@users.noreply.github.com> Date: Fri, 7 Nov 2025 08:54:55 +0000 Subject: [PATCH 2/3] chore: Update personal and team onboarding flows (remove profile/video steps, add settings, improve team invites) (#24947) ## What does this PR do? - Redesigns and streamlines the onboarding flow for personal, team, and organization accounts - Consolidates shared components and improves code organization - Adds browser preview for better user experience during onboarding - Simplifies the personal onboarding flow by removing the video integration step - Enhances team onboarding with CSV upload functionality ## Visual Demo (For contributors especially) #### Image Demo: - The PR adds a new browser preview component that shows users how their profile/team will look during onboarding - Redesigned UI with a more consistent layout across all onboarding steps - Improved mobile responsiveness with better component organization ## 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? 1. Test the complete onboarding flow for personal accounts: - Start at `/onboarding/getting-started` - Proceed through personal details and calendar setup - Verify the flow completes successfully 2. Test the team onboarding flow: - Start at `/onboarding/getting-started` and select team - Complete team details - Test the invite options including CSV upload - Verify team creation works correctly 3. Test the organization onboarding flow: - Start at `/onboarding/getting-started` and select organization - Complete organization details and branding - Test member invitations - Verify organization creation works correctly 4. Verify browser preview functionality: - Check that the preview updates in real-time as you enter information - Confirm it displays correctly on different screen sizes ## 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 --- .../onboarding/getting-started/page.tsx | 4 +- .../onboarding/personal/profile/page.tsx | 4 +- .../onboarding/personal/video/page.tsx | 34 -- .../onboarding/teams/brand/page.tsx | 34 -- .../onboarding-calendar-browser-view.tsx | 92 ++++++ .../getting-started/onboarding-view.tsx | 112 ++++--- .../hooks/useSubmitPersonalOnboarding.ts | 80 +++++ .../brand/organization-brand-view.tsx | 71 +--- .../details/organization-details-view.tsx | 9 +- .../invite/organization-invite-view.tsx | 295 ++++++++--------- .../calendar/personal-calendar-view.tsx | 41 ++- .../profile/personal-profile-view.tsx | 182 ----------- .../settings/personal-settings-view.tsx | 206 ++++++++---- .../personal/video/personal-video-view.tsx | 162 ---------- .../onboarding/store/onboarding-store.ts | 2 + .../teams/brand/team-brand-view.tsx | 258 --------------- .../teams/details/team-details-view.tsx | 184 ++++++----- .../teams/details/validated-team-slug.tsx | 11 + .../teams/invite/csv-upload-modal.tsx | 245 ++++++++++++++ .../invite/email/team-invite-email-view.tsx | 188 +++++++++++ .../teams/invite/team-invite-view.tsx | 303 ++++++++---------- .../shell/hooks/useWelcomeToCalcomModal.ts | 51 +++ .../routers/viewer/teams/create.handler.ts | 3 +- .../routers/viewer/teams/create.schema.ts | 1 + 24 files changed, 1310 insertions(+), 1262 deletions(-) delete mode 100644 apps/web/app/(use-page-wrapper)/onboarding/personal/video/page.tsx delete mode 100644 apps/web/app/(use-page-wrapper)/onboarding/teams/brand/page.tsx create mode 100644 apps/web/modules/onboarding/components/onboarding-calendar-browser-view.tsx create mode 100644 apps/web/modules/onboarding/hooks/useSubmitPersonalOnboarding.ts delete mode 100644 apps/web/modules/onboarding/personal/profile/personal-profile-view.tsx delete mode 100644 apps/web/modules/onboarding/personal/video/personal-video-view.tsx delete mode 100644 apps/web/modules/onboarding/teams/brand/team-brand-view.tsx create mode 100644 apps/web/modules/onboarding/teams/invite/csv-upload-modal.tsx create mode 100644 apps/web/modules/onboarding/teams/invite/email/team-invite-email-view.tsx create mode 100644 packages/features/shell/hooks/useWelcomeToCalcomModal.ts diff --git a/apps/web/app/(use-page-wrapper)/onboarding/getting-started/page.tsx b/apps/web/app/(use-page-wrapper)/onboarding/getting-started/page.tsx index 29ec2170dc6082..e96495d6a80ef3 100644 --- a/apps/web/app/(use-page-wrapper)/onboarding/getting-started/page.tsx +++ b/apps/web/app/(use-page-wrapper)/onboarding/getting-started/page.tsx @@ -26,11 +26,9 @@ const ServerPage = async () => { return redirect("/auth/login"); } - // Hello {username} || there. Not sure how to do this nicely with i18n just yet - const userName = session.user.name || "there"; const userEmail = session.user.email || ""; - return ; + 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 index dc709084234e9a..da56b018205998 100644 --- a/apps/web/app/(use-page-wrapper)/onboarding/personal/profile/page.tsx +++ b/apps/web/app/(use-page-wrapper)/onboarding/personal/profile/page.tsx @@ -7,7 +7,7 @@ import { APP_NAME } from "@calcom/lib/constants"; import { buildLegacyRequest } from "@lib/buildLegacyCtx"; -import { PersonalProfileView } from "~/onboarding/personal/profile/personal-profile-view"; +import { PersonalSettingsView } from "~/onboarding/personal/settings/personal-settings-view"; export const generateMetadata = async () => { return await _generateMetadata( @@ -28,7 +28,7 @@ const ServerPage = async () => { const userEmail = session.user.email || ""; - return ; + 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 deleted file mode 100644 index 1ec119859f5589..00000000000000 --- a/apps/web/app/(use-page-wrapper)/onboarding/personal/video/page.tsx +++ /dev/null @@ -1,34 +0,0 @@ -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/app/(use-page-wrapper)/onboarding/teams/brand/page.tsx b/apps/web/app/(use-page-wrapper)/onboarding/teams/brand/page.tsx deleted file mode 100644 index 72f40aaa215189..00000000000000 --- a/apps/web/app/(use-page-wrapper)/onboarding/teams/brand/page.tsx +++ /dev/null @@ -1,34 +0,0 @@ -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 { TeamBrandView } from "~/onboarding/teams/brand/team-brand-view"; - -export const generateMetadata = async () => { - return await _generateMetadata( - (t) => `${APP_NAME} - ${t("team_brand")}`, - () => "", - true, - undefined, - "/onboarding/teams/brand" - ); -}; - -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/components/onboarding-calendar-browser-view.tsx b/apps/web/modules/onboarding/components/onboarding-calendar-browser-view.tsx new file mode 100644 index 00000000000000..f45e562c50f5a5 --- /dev/null +++ b/apps/web/modules/onboarding/components/onboarding-calendar-browser-view.tsx @@ -0,0 +1,92 @@ +"use client"; + +import { WEBAPP_URL } from "@calcom/lib/constants"; +import { useLocale } from "@calcom/lib/hooks/useLocale"; +import { Icon, type IconName } from "@calcom/ui/components/icon"; + +export const OnboardingCalendarBrowserView = () => { + const { t } = useLocale(); + const webappUrl = WEBAPP_URL.replace(/^https?:\/\//, ""); + + const calendarIntegrations: Array<{ + name: string; + description: string; + icon: IconName; + }> = [ + { + name: t("google_calendar"), + description: t("onboarding_calendar_browser_view_google_description"), + icon: "calendar", + }, + { + name: t("outlook_calendar"), + description: t("onboarding_calendar_browser_view_outlook_description"), + icon: "mail", + }, + { + name: t("apple_calendar"), + description: t("onboarding_calendar_browser_view_apple_description"), + icon: "calendar-days", + }, + ]; + + return ( +
+ {/* Browser header */} +
+ {/* Navigation buttons */} +
+ + + +
+
+ +

{webappUrl}/settings/calendars

+
+ +
+ {/* Content */} +
+
+ {/* Header */} +
+
+

+ {t("connect_your_calendar")} +

+

+ {t("onboarding_calendar_browser_view_subtitle")} +

+
+
+ + {/* Calendar Integrations List */} +
+ {calendarIntegrations.map((integration, index) => ( +
+ {index > 0 &&
} +
+
+
+ +
+
+

{integration.name}

+

+ {integration.description} +

+
+
+
+ {t("connected")} +
+
+
+ ))} +
+
+
+
+ ); +}; diff --git a/apps/web/modules/onboarding/getting-started/onboarding-view.tsx b/apps/web/modules/onboarding/getting-started/onboarding-view.tsx index 0fd6c51ebf886a..b41cbf0168ef90 100644 --- a/apps/web/modules/onboarding/getting-started/onboarding-view.tsx +++ b/apps/web/modules/onboarding/getting-started/onboarding-view.tsx @@ -1,6 +1,8 @@ "use client"; +import { AnimatePresence, motion } from "framer-motion"; import { useRouter } from "next/navigation"; +import { useEffect, useRef } from "react"; import { isCompanyEmail } from "@calcom/features/ee/organizations/lib/utils"; import { useLocale } from "@calcom/lib/hooks/useLocale"; @@ -10,20 +12,43 @@ import { Button } from "@calcom/ui/components/button"; import { type IconName } from "@calcom/ui/components/icon"; import { RadioAreaGroup } from "@calcom/ui/components/radio"; +import { OnboardingCard } from "../components/OnboardingCard"; import { OnboardingLayout } from "../components/OnboardingLayout"; import { OnboardingContinuationPrompt } from "../components/onboarding-continuation-prompt"; import { PlanIcon } from "../components/plan-icon"; import { useOnboardingStore, type PlanType } from "../store/onboarding-store"; type OnboardingViewProps = { - userName: string; userEmail: string; }; -export const OnboardingView = ({ userName, userEmail }: OnboardingViewProps) => { +export const OnboardingView = ({ userEmail }: OnboardingViewProps) => { const router = useRouter(); const { t } = useLocale(); const { selectedPlan, setSelectedPlan } = useOnboardingStore(); + const previousPlanRef = useRef(null); + + // Plan order mapping for determining direction + const planOrder: Record = { + personal: 0, + team: 1, + organization: 2, + }; + + // Calculate animation direction synchronously + const getDirection = (): "up" | "down" => { + if (!selectedPlan || !previousPlanRef.current) return "down"; + const previousOrder = planOrder[previousPlanRef.current]; + const currentOrder = planOrder[selectedPlan]; + return currentOrder > previousOrder ? "down" : "up"; + }; + + const direction = getDirection(); + + // Update previous plan ref after render + useEffect(() => { + previousPlanRef.current = selectedPlan; + }, [selectedPlan]); const handleContinue = () => { if (selectedPlan === "organization") { @@ -37,7 +62,7 @@ export const OnboardingView = ({ userName, userEmail }: OnboardingViewProps) => const planIconByType: Record = { personal: "user", - team: "users", + team: "user", organization: "users", }; @@ -56,7 +81,7 @@ export const OnboardingView = ({ userName, userEmail }: OnboardingViewProps) => badge: t("onboarding_plan_team_badge"), description: t("onboarding_plan_team_description"), icon: planIconByType.team, - variant: "single" as const, + variant: "team" as const, }, { id: "organization" as PlanType, @@ -76,26 +101,26 @@ export const OnboardingView = ({ userName, userEmail }: OnboardingViewProps) => return true; }); + const selectedPlanData = plans.find((plan) => plan.id === selectedPlan); + return ( <> -
+ {/* Left column - Main content */} + + +
+ }> {/* Card */} -
+
- {/* Card Header */} -
-
-

- {t("onboarding_welcome_message", { userName })} -

-

- {t("onboarding_welcome_question")} -

-
-
- {/* Plan options */} "pr-12 [&>button]:left-auto [&>button]:right-6 [&>button]:mt-0 [&>button]:transform" )} classNames={{ - container: "flex w-full items-center gap-4 p-4 pl-5 pr-12 md:p-5 md:pr-14", + container: "flex w-full items-center gap-3 p-5 pr-12", }}> -
- -
-
-

{plan.title}

- - - {plan.badge} - - -
+
+
+

{plan.title}

+ className="hidden h-4 rounded-md px-1 py-1 md:flex md:items-center"> {plan.badge} -

- {plan.description} -

+ + {plan.badge} + +

+ {plan.description} +

); })} - - {/* Footer */} -
- -
+ + + {/* Right column - Icon display */} +
+ + {selectedPlanData && ( + + )} +
diff --git a/apps/web/modules/onboarding/hooks/useSubmitPersonalOnboarding.ts b/apps/web/modules/onboarding/hooks/useSubmitPersonalOnboarding.ts new file mode 100644 index 00000000000000..15c3b10c73eb42 --- /dev/null +++ b/apps/web/modules/onboarding/hooks/useSubmitPersonalOnboarding.ts @@ -0,0 +1,80 @@ +import { useRouter } from "next/navigation"; + +import { setShowWelcomeToCalcomModalFlag } from "@calcom/features/shell/hooks/useWelcomeToCalcomModal"; +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 { showToast } from "@calcom/ui/components/toast"; + +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 useSubmitPersonalOnboarding = () => { + const router = useRouter(); + const { t } = useLocale(); + const telemetry = useTelemetry(); + const utils = trpc.useUtils(); + + const { data: eventTypes } = trpc.viewer.eventTypes.list.useQuery(); + const createEventType = trpc.viewer.eventTypesHeavy.create.useMutation(); + + 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(); + // Set flag to show welcome modal after redirect + setShowWelcomeToCalcomModalFlag(); + router.push("/event-types?welcomeToCalcomModal=true"); + }, + onError: (error) => { + showToast(t("something_went_wrong"), "error"); + console.error(error); + }, + }); + + const submitPersonalOnboarding = () => { + telemetry.event(telemetryEventTypes.onboardingFinished); + mutation.mutate({ + completedOnboarding: true, + }); + }; + + return { + submitPersonalOnboarding, + isSubmitting: mutation.isPending, + }; +}; 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 6868e24e86d293..66df2dbc2ea71e 100644 --- a/apps/web/modules/onboarding/organization/brand/organization-brand-view.tsx +++ b/apps/web/modules/onboarding/organization/brand/organization-brand-view.tsx @@ -8,8 +8,9 @@ import { HexColorPicker } from "react-colorful"; import { useLocale } from "@calcom/lib/hooks/useLocale"; import { Button } from "@calcom/ui/components/button"; -import { OnboardingCard } from "../../personal/_components/OnboardingCard"; -import { OnboardingLayout } from "../../personal/_components/OnboardingLayout"; +import { OnboardingCard } from "../../components/OnboardingCard"; +import { OnboardingLayout } from "../../components/OnboardingLayout"; +import { OnboardingBrowserView } from "../../components/onboarding-browser-view"; import { useOnboardingStore } from "../../store/onboarding-store"; type OrganizationBrandViewProps = { @@ -118,6 +119,7 @@ export const OrganizationBrandView = ({ userEmail }: OrganizationBrandViewProps) return ( + {/* Left column - Main content */}
-

{t("onboarding_logo_size_hint")}

+

+ {t("onboarding_logo_size_hint")} +

{/* Banner Upload */} @@ -225,69 +229,14 @@ export const OrganizationBrandView = ({ userEmail }: OrganizationBrandViewProps)

- - {/* Right side - Preview */} -
-
-

{t("preview")}

-
- {/* Banner preview */} -
- {bannerPreview && ( - {t("onboarding_banner_preview_alt")} - )} -
- - {/* Content */} -
-
-
- {/* Logo preview */} -
- {logoPreview && ( - {t("onboarding_logo_preview_alt")} - )} -
-

- {organizationDetails.name || t("onboarding_preview_nameless")} -

-
-
-
-

- {t("onboarding_preview_example_title")} -

-

- {t("onboarding_preview_example_description")} -

-
-
-
-
- {[134, 104, 84, 104].map((width, i) => ( -
-
-
-
- ))} -
-
-
-
-
+ + {/* Right column - Browser view */} + ); }; 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 be492a3a4058f0..10542bfc8dbfe0 100644 --- a/apps/web/modules/onboarding/organization/details/organization-details-view.tsx +++ b/apps/web/modules/onboarding/organization/details/organization-details-view.tsx @@ -7,8 +7,9 @@ import { useLocale } from "@calcom/lib/hooks/useLocale"; import { Button } from "@calcom/ui/components/button"; import { Label, TextField, TextArea } from "@calcom/ui/components/form"; -import { OnboardingCard } from "../../personal/_components/OnboardingCard"; -import { OnboardingLayout } from "../../personal/_components/OnboardingLayout"; +import { OnboardingBrowserView } from "../../components/onboarding-browser-view"; +import { OnboardingCard } from "../../components/OnboardingCard"; +import { OnboardingLayout } from "../../components/OnboardingLayout"; import { useOnboardingStore } from "../../store/onboarding-store"; import { ValidatedOrganizationSlug } from "./validated-organization-slug"; @@ -77,6 +78,7 @@ export const OrganizationDetailsView = ({ userEmail }: OrganizationDetailsViewPr return ( + {/* Left column - Main content */}
+ + {/* Right column - Browser view */} + ); }; diff --git a/apps/web/modules/onboarding/organization/invite/organization-invite-view.tsx b/apps/web/modules/onboarding/organization/invite/organization-invite-view.tsx index 11f7a590089f12..c0cd7966295a6e 100644 --- a/apps/web/modules/onboarding/organization/invite/organization-invite-view.tsx +++ b/apps/web/modules/onboarding/organization/invite/organization-invite-view.tsx @@ -12,6 +12,7 @@ import { Form, Label, TextField, Select, ToggleGroup } from "@calcom/ui/componen import { Icon } from "@calcom/ui/components/icon"; import { Logo } from "@calcom/ui/components/logo"; +import { OnboardingBrowserView } from "../../components/onboarding-browser-view"; import { useSubmitOnboarding } from "../../hooks/useSubmitOnboarding"; import { useOnboardingStore } from "../../store/onboarding-store"; @@ -117,135 +118,55 @@ export const OrganizationInviteView = ({ userEmail }: OrganizationInviteViewProp {/* Main content */}
-
- {/* Card */} -
-
- {/* Card Header */} -
-
-

- {t("onboarding_org_invite_title")} -

-

- {isEmailMode - ? t("onboarding_org_invite_subtitle_email") - : t("onboarding_org_invite_subtitle_full")} -

+
+ {/* Left column - Main content */} +
+ {/* Card */} +
+
+ {/* Card Header */} +
+
+

+ {t("onboarding_org_invite_title")} +

+

+ {isEmailMode + ? t("onboarding_org_invite_subtitle_email") + : t("onboarding_org_invite_subtitle_full")} +

+
-
- - {/* Content */} -
-
- {!isEmailMode ? ( - // Coming soon - // Initial invite options view -
- -
-
- {t("onboarding_or_divider")} -
-
- -
- - - -
- {/* Role selector */} -
-
- {t("onboarding_invite_all_as")} - value && setInviteRole(value as "MEMBER" | "ADMIN")} - options={[ - { value: "ADMIN", label: t("onboarding_admins") }, - { value: "MEMBER", label: t("members") }, - ]} - /> +
+
+ {t("onboarding_or_divider")} +
- {t("onboarding_modify_roles_later")} -
-
- ) : ( - // Email invite form -
-
- {/* Email and Team inputs */} -
-
- - -
- {fields.map((field, index) => ( -
-
- - t.value === form.watch(`invites.${index}.team`))} + onChange={(option) => { + if (option) { + form.setValue(`invites.${index}.team`, option.value); + } + }} + placeholder={t("select_team")} + /> +
+ +
+ ))} + + {/* Add button */} + +
+ + {/* Role selector */} +
+
+ {t("onboarding_invite_all_as")} + value && setInviteRole(value as "MEMBER" | "ADMIN")} + options={[ + { value: "MEMBER", label: t("members") }, + { value: "ADMIN", label: t("onboarding_admins") }, + ]} + /> +
+ {t("onboarding_modify_roles_later")} +
+
+
+ )} +
-
- {/* Footer */} -
- + {/* Footer */} +
+ +
-
- {/* Skip button */} -
- + {/* Skip button */} +
+ +
+ + {/* Right column - Browser view */} +
diff --git a/apps/web/modules/onboarding/personal/calendar/personal-calendar-view.tsx b/apps/web/modules/onboarding/personal/calendar/personal-calendar-view.tsx index c7b76a37bd1feb..f30674773e75ed 100644 --- a/apps/web/modules/onboarding/personal/calendar/personal-calendar-view.tsx +++ b/apps/web/modules/onboarding/personal/calendar/personal-calendar-view.tsx @@ -1,15 +1,14 @@ "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 { OnboardingCalendarBrowserView } from "../../components/onboarding-calendar-browser-view"; +import { useSubmitPersonalOnboarding } from "../../hooks/useSubmitPersonalOnboarding"; import { InstallableAppCard } from "../_components/InstallableAppCard"; -import { OnboardingCard } from "../_components/OnboardingCard"; -import { OnboardingLayout } from "../_components/OnboardingLayout"; -import { SkipButton } from "../_components/SkipButton"; +import { OnboardingCard } from "../../components/OnboardingCard"; +import { OnboardingLayout } from "../../components/OnboardingLayout"; import { useAppInstallation } from "../_components/useAppInstallation"; type PersonalCalendarViewProps = { @@ -17,9 +16,9 @@ type PersonalCalendarViewProps = { }; export const PersonalCalendarView = ({ userEmail }: PersonalCalendarViewProps) => { - const router = useRouter(); const { t } = useLocale(); const { installingAppSlug, setInstallingAppSlug, createInstallHandlers } = useAppInstallation(); + const { submitPersonalOnboarding, isSubmitting } = useSubmitPersonalOnboarding(); const queryIntegrations = trpc.viewer.apps.integrations.useQuery({ variant: "calendar", @@ -29,23 +28,38 @@ export const PersonalCalendarView = ({ userEmail }: PersonalCalendarViewProps) = }); const handleContinue = () => { - router.push("/onboarding/personal/video"); + submitPersonalOnboarding(); }; const handleSkip = () => { - router.push("/onboarding/personal/video"); + submitPersonalOnboarding(); }; return ( - + + {/* Left column - Main content */} - {t("continue")} - +
+ + +
}>
{queryIntegrations.data?.items?.map((app) => ( @@ -60,7 +74,8 @@ export const PersonalCalendarView = ({ userEmail }: PersonalCalendarViewProps) =
- + {/* Right column - Browser view */} +
); }; diff --git a/apps/web/modules/onboarding/personal/profile/personal-profile-view.tsx b/apps/web/modules/onboarding/personal/profile/personal-profile-view.tsx deleted file mode 100644 index 67efa29b4f623a..00000000000000 --- a/apps/web/modules/onboarding/personal/profile/personal-profile-view.tsx +++ /dev/null @@ -1,182 +0,0 @@ -"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 { 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 = { - 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; - } - - return ( - - - {t("continue")} - - }> - {/* Form */} -
-
-
-
-
- {/* Avatar */} -
- -
- {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 */} -
- - md.render(getValues("bio") || user?.bio || "")} - setText={(value: string) => setValue("bio", turndown(value))} - excludedToolbarItems={["blockType"]} - firstRender={firstRender} - setFirstRender={setFirstRender} - /> -

- {t("few_sentences_about_yourself")} -

-
-
-
-
-
-
-
-
- ); -}; 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 84b4638351dd86..ba2ae424910145 100644 --- a/apps/web/modules/onboarding/personal/settings/personal-settings-view.tsx +++ b/apps/web/modules/onboarding/personal/settings/personal-settings-view.tsx @@ -2,23 +2,26 @@ import { zodResolver } from "@hookform/resolvers/zod"; import { useRouter } from "next/navigation"; -import { useEffect, useState } from "react"; -import { useForm } from "react-hook-form"; +import { useEffect, useRef, useState } from "react"; +import { FormProvider, 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 { UserAvatar } from "@calcom/ui/components/avatar"; import { Button } from "@calcom/ui/components/button"; -import { Label, TextField } from "@calcom/ui/components/form"; +import { Label, TextArea, TextField } from "@calcom/ui/components/form"; +import { ImageUploader } from "@calcom/ui/components/image-uploader"; +import { showToast } from "@calcom/ui/components/toast"; +import { UsernameAvailabilityField } from "@components/ui/UsernameAvailability"; + +import { OnboardingCard } from "../../components/OnboardingCard"; +import { OnboardingLayout } from "../../components/OnboardingLayout"; +import { OnboardingBrowserView } from "../../components/onboarding-browser-view"; 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; @@ -28,17 +31,17 @@ type PersonalSettingsViewProps = { export const PersonalSettingsView = ({ userEmail, userName }: PersonalSettingsViewProps) => { const router = useRouter(); const { t } = useLocale(); + const { data: user } = trpc.viewer.me.get.useQuery(); const { personalDetails, setPersonalDetails } = useOnboardingStore(); - const { setTimezone: setSelectedTimeZone, timezone: selectedTimeZone } = useTimePreferences(); - const [name, setName] = useState(""); + const avatarRef = useRef(null); + const [imageSrc, setImageSrc] = useState(""); useEffect(() => { - setName(personalDetails.name || userName || ""); - if (personalDetails.timezone) { - setSelectedTimeZone(personalDetails.timezone); + if (user) { + setImageSrc(personalDetails.avatar || user.avatar || ""); } - }, [personalDetails, userName, setSelectedTimeZone]); + }, [personalDetails.avatar, user]); const formSchema = z.object({ name: z @@ -47,98 +50,169 @@ export const PersonalSettingsView = ({ userEmail, userName }: PersonalSettingsVi .max(FULL_NAME_LENGTH_MAX_LIMIT, { message: t("max_limit_allowed_hint", { limit: FULL_NAME_LENGTH_MAX_LIMIT }), }), + bio: z.string().optional(), }); const form = useForm>({ resolver: zodResolver(formSchema), defaultValues: { name: personalDetails.name || userName || "", + bio: personalDetails.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(); }, }); + async function updateProfileHandler(newAvatar: string) { + avatarMutation.mutate({ + avatarUrl: newAvatar, + }); + } + const handleContinue = form.handleSubmit(async (data) => { // Save to store setPersonalDetails({ name: data.name, - timezone: selectedTimeZone, + bio: data.bio || "", }); // Save to backend await mutation.mutateAsync({ name: data.name, - timeZone: selectedTimeZone, + bio: data.bio || "", }); - router.push("/onboarding/personal/profile"); + router.push("/onboarding/personal/calendar"); }); + if (!user) { + return null; + } + return ( <> + {/* Left column - Main content */} - {t("continue")} - +
+ + +
}> - {/* Form */} -
-
-
-
-
- {/* Name */} -
- { - setName(e.target.value); - form.setValue("name", e.target.value); - }} - placeholder="John Doe" - /> - {form.formState.errors.name && ( -

{form.formState.errors.name.message}

- )} + +
+ {/* Profile Picture */} +
+ +
+ {user && ( +
+
- - {/* Timezone */} -
- - setSelectedTimeZone(value)} - /> -

- {t("current_time")}{" "} - {dayjs().tz(selectedTimeZone).format("LT").toString().toLowerCase()} -

-
-
+ )} + + { + 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} + />
+

{t("onboarding_logo_size_hint")}

-
-
+ + {/* Name */} +
+ + {form.formState.errors.name && ( +

{form.formState.errors.name.message}

+ )} +
+ + {/* Username */} +
+ { + // Refetch user to get updated username and save to store + const updatedUser = await utils.viewer.me.get.fetch(); + if (updatedUser?.username) { + setPersonalDetails({ username: updatedUser.username }); + } + }} + /> +
+ + {/* Bio */} +
+ +