fix: UI nits in QA verison of onboarding v3 (#25532)

* fix create button

* fix subdomain prefix in orgs details view

* remove redundant text

* fix prefix on link fields

* brand mode colours

* Fix banner positioning

* remove app descrtipion plus add mr to account for button

* fix gaps

* improve free plan welcome modal
This commit is contained in:
sean-brydon
2025-12-03 09:57:01 +00:00
committed by GitHub
parent fd6c6e6419
commit f87d4de546
14 changed files with 117 additions and 38 deletions
@@ -34,7 +34,7 @@ const ServerPage = async () => {
return <TeamInviteEmailView userEmail={userEmail} />;
}
return <TeamInviteView userEmail={userEmail} />;
return <TeamInviteEmailView userEmail={userEmail} />;
};
export default ServerPage;
@@ -50,7 +50,7 @@ export function EmailInviteForm({
<div className="flex h-full w-full flex-col gap-4">
<div className="flex flex-col gap-2">
{showTeamSelect ? (
<div className="grid grid-cols-2">
<div className="mr-7 grid grid-cols-2">
<Label className="text-emphasis mb-0 text-sm font-medium" htmlFor="invites.0.email">
{t("email")}
</Label>
@@ -32,7 +32,7 @@ export const OnboardingLayout = ({ userEmail, currentStep, totalSteps, children
{/* Logo and container - centered */}
<div className="flex w-full flex-1 flex-col items-center justify-center gap-6">
<Logo className="mt-4 h-5 w-auto shrink-0" />
<div className="border-subtle bg-default grid max-h-[690px] min-h-0 w-full max-w-[532px] flex-1 grid-cols-1 gap-6 overflow-hidden rounded-2xl border px-4 py-2 sm:px-12 sm:py-10 xl:h-[690px] xl:max-w-[1130px] xl:grid-cols-[40%_1fr] xl:pl-10 xl:pr-0">
<div className="border-subtle bg-default grid max-h-[690px] min-h-0 w-full max-w-[532px] flex-1 grid-cols-1 gap-12 overflow-hidden rounded-2xl border px-4 py-2 sm:px-12 sm:py-10 xl:h-[690px] xl:max-w-[1130px] xl:grid-cols-[40%_1fr] xl:pl-10 xl:pr-0">
{/* Column 1 - Always visible, 40% on xl+ */}
<div className="flex h-full min-h-0 flex-col">{column1}</div>
{/* Column 2 - Hidden on mobile, visible on xl+, 60% on xl+ */}
@@ -4,6 +4,8 @@ import classNames from "classnames";
import { AnimatePresence, motion } from "framer-motion";
import { usePathname } from "next/navigation";
import { useOrgBranding } from "@calcom/features/ee/organizations/context/provider";
import { subdomainSuffix } from "@calcom/features/ee/organizations/lib/orgDomains";
import { WEBAPP_URL } from "@calcom/lib/constants";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { Avatar } from "@calcom/ui/components/avatar";
@@ -18,6 +20,21 @@ type OnboardingBrowserViewProps = {
teamSlug?: string;
};
const getDisplayUrl = (
orgSlug: string | null | undefined,
username: string | null | undefined,
teamSlug: string | undefined
): string => {
if (orgSlug) {
return teamSlug !== undefined
? `${orgSlug}.${subdomainSuffix()}/team/${teamSlug || ""}`
: `${orgSlug}.${subdomainSuffix()}/${username || ""}`;
}
const webappUrl = WEBAPP_URL.replace(/^https?:\/\//, "");
return teamSlug !== undefined ? `${webappUrl}/team/${teamSlug || ""}` : `${webappUrl}/${username || ""}`;
};
export const OnboardingBrowserView = ({
avatar,
name,
@@ -27,9 +44,9 @@ export const OnboardingBrowserView = ({
}: OnboardingBrowserViewProps) => {
const { t } = useLocale();
const pathname = usePathname();
const webappUrl = WEBAPP_URL.replace(/^https?:\/\//, "");
const displayUrl =
teamSlug !== undefined ? `${webappUrl}/team/${teamSlug || ""}` : `${webappUrl}/${username || ""}`;
const orgBranding = useOrgBranding();
const displayUrl = getDisplayUrl(orgBranding?.slug, username, teamSlug);
// Animation variants for entry and exit
const containerVariants = {
@@ -133,22 +133,44 @@ export const OnboardingInviteBrowserView = ({
duration: 0.5,
ease: "backOut",
}}>
<div className="bg-default border-subtle flex flex-col gap-4 rounded-2xl border p-8">
<div className="flex flex-col items-start gap-4">
<Avatar
size="lg"
imageSrc={avatar || undefined}
alt={displayName}
className="border-default h-12 w-12 border-2"
/>
<div className="flex w-full flex-col items-start gap-1">
<h2 className="text-emphasis font-cal w-full text-left text-xl font-semibold leading-tight">
{displayName}
</h2>
<p className="text-subtle text-left text-sm font-normal leading-tight">
{displayBio || "We're emailing you all the details"}
</p>
</div>
<div className="bg-default border-subtle flex flex-col rounded-2xl border">
<div className="relative p-1">
{/* Banner Image */}
{organizationBrand.banner && (
<div className="border-subtle relative h-36 w-full overflow-hidden rounded-xl border">
{/* eslint-disable-next-line @next/next/no-img-element */}
<img
src={organizationBrand.banner}
alt={displayName}
className="h-full w-full object-cover"
/>
</div>
)}
{/* Organization Avatar - Overlaying the banner */}
{organizationBrand.banner && avatar && (
<div className="absolute -bottom-6 left-4">
<Avatar size="lg" imageSrc={avatar} alt={displayName} className="h-12 w-12 border" />
</div>
)}
</div>
{/* Organization Info */}
<div className={`flex flex-col items-start gap-1 px-4 pb-4 pt-8`}>
{!organizationBrand.banner && avatar && (
<Avatar
size="lg"
imageSrc={avatar}
alt={displayName}
className="border-default mb-4 h-12 w-12 border-2"
/>
)}
<h2 className="text-emphasis font-cal w-full text-left text-xl font-semibold leading-tight">
{displayName}
</h2>
<p className="text-subtle text-left text-sm font-normal leading-tight">
{displayBio || "We're emailing you all the details"}
</p>
</div>
</div>
@@ -3,19 +3,30 @@
import classNames from "classnames";
import { AnimatePresence, motion } from "framer-motion";
import { usePathname } from "next/navigation";
import { useEffect, useRef } from "react";
import { WEBAPP_URL } from "@calcom/lib/constants";
import { subdomainSuffix } from "@calcom/features/ee/organizations/lib/orgDomains";
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";
// Helper function to darken a hex color
const darkenColor = (hex: string, amount: number): string => {
const num = parseInt(hex.replace("#", ""), 16);
const r = Math.max(0, Math.min(255, ((num >> 16) & 0xff) * (1 - amount)));
const g = Math.max(0, Math.min(255, ((num >> 8) & 0xff) * (1 - amount)));
const b = Math.max(0, Math.min(255, (num & 0xff) * (1 - amount)));
return `#${((r << 16) | (g << 8) | b).toString(16).padStart(6, "0")}`;
};
type OnboardingOrganizationBrowserViewProps = {
avatar?: string | null;
name?: string;
bio?: string;
slug?: string;
bannerUrl?: string | null;
brandColor?: string;
};
export const OnboardingOrganizationBrowserView = ({
@@ -24,11 +35,22 @@ export const OnboardingOrganizationBrowserView = ({
bio,
slug,
bannerUrl,
brandColor,
}: OnboardingOrganizationBrowserViewProps) => {
const { t } = useLocale();
const pathname = usePathname();
const webappUrl = WEBAPP_URL.replace(/^https?:\/\//, "");
const displayUrl = `${webappUrl}/${slug || ""}`;
const containerRef = useRef<HTMLDivElement>(null);
const displayUrl = slug ? `${slug}.${subdomainSuffix()}` : subdomainSuffix();
// Update CSS variables when brandColor changes
useEffect(() => {
if (brandColor && containerRef.current) {
containerRef.current.style.setProperty("--cal-brand", brandColor);
// Set brand-emphasis as a slightly darker version for hover states
const darkerColor = darkenColor(brandColor, 0.1);
containerRef.current.style.setProperty("--cal-brand-emphasis", darkerColor);
}
}, [brandColor]);
// Animation variants for entry and exit
const containerVariants = {
@@ -79,7 +101,9 @@ export const OnboardingOrganizationBrowserView = ({
];
return (
<div className="bg-default border-subtle hidden h-full w-full flex-col overflow-hidden rounded-l-2xl border-y border-s xl:flex">
<div
ref={containerRef}
className="bg-default border-subtle hidden h-full w-full flex-col overflow-hidden rounded-l-2xl border-y border-s xl:flex">
{/* Browser header */}
<div className="border-subtle bg-default flex min-w-0 shrink-0 items-center gap-3 rounded-t-2xl border-b p-3">
{/* Navigation buttons */}
@@ -164,7 +188,7 @@ export const OnboardingOrganizationBrowserView = ({
</div>
<p className="text-subtle text-sm font-medium leading-tight">{event.description}</p>
</div>
<Button color="secondary" size="sm" EndIcon="arrow-right" tabIndex={-1}>
<Button color="primary" size="sm" EndIcon="arrow-right" tabIndex={-1}>
{t("book_now")}
</Button>
</div>
@@ -108,11 +108,6 @@ export const OnboardingTeamsBrowserView = ({
{/* Teams Info */}
<div className="flex flex-col gap-2 px-4 pb-4 pt-12">
<h2 className="text-emphasis text-xl font-semibold leading-tight">{t("teams")}</h2>
<p className="text-subtle text-sm leading-normal">
{hasValidTeams
? t("onboarding_teams_browser_view_subtitle")
: t("onboarding_teams_browser_view_placeholder_subtitle")}
</p>
</div>
</div>
@@ -18,11 +18,13 @@ export function useCreateTeam() {
const createTeamMutation = trpc.viewer.teams.create.useMutation();
const inviteMemberMutation = trpc.viewer.teams.inviteMember.useMutation();
const createTeam = async (store: OnboardingState) => {
const createTeam = async (store?: OnboardingState) => {
setIsSubmitting(true);
try {
const { teamDetails, teamBrand } = store;
// Get the latest state from the store to ensure we have the most up-to-date values
const currentStore = store || useOnboardingStore.getState();
const { teamDetails, teamBrand } = currentStore;
// Validate team details - if empty, redirect back to team details step
if (!teamDetails.name || !teamDetails.name.trim() || !teamDetails.slug || !teamDetails.slug.trim()) {
@@ -1,3 +1,4 @@
import { useSession } from "next-auth/react";
import { useRouter } from "next/navigation";
import { useState } from "react";
@@ -11,6 +12,7 @@ import type { OnboardingState } from "../store/onboarding-store";
export const useSubmitOnboarding = () => {
const router = useRouter();
const { update: updateSession } = useSession();
const [isSubmitting, setIsSubmitting] = useState(false);
const [error, setError] = useState<string | null>(null);
const flags = useFlagMap();
@@ -78,6 +80,8 @@ export const useSubmitOnboarding = () => {
// No checkout URL means billing is disabled (self-hosted flow)
// Organization has already been created by the backend
showToast("Organization created successfully!", "success");
// Refresh session to get updated organization data
await updateSession();
// Set flag to show welcome modal after personal onboarding redirect
setShowNewOrgModalFlag();
skipToPersonal(resetOnboarding);
@@ -189,6 +189,7 @@ export const OrganizationBrandView = ({ userEmail }: OrganizationBrandViewProps)
bio={organizationDetails.bio}
slug={organizationDetails.link}
bannerUrl={bannerPreview}
brandColor={brandColor}
/>
</OnboardingLayout>
);
@@ -42,7 +42,6 @@ export const InstallableAppCard = ({
title={app.name}>
{app.name}
</p>
<p className="text-subtle hidden text-left text-xs leading-tight xl:line-clamp-2">{app.description}</p>
<InstallAppButtonWithoutPlanCheck
type={app.type}
options={installOptions}
@@ -54,7 +54,7 @@ export const TeamInviteEmailView = ({ userEmail }: TeamInviteEmailViewProps) =>
const formSchema = z.object({
invites: z.array(
z.object({
email: z.string().email(t("invalid_email_address")),
email: z.union([z.literal(""), z.string().email(t("invalid_email_address"))]),
role: z.enum(["MEMBER", "ADMIN"]),
})
),
@@ -151,7 +151,7 @@ export const TeamInviteEmailView = ({ userEmail }: TeamInviteEmailViewProps) =>
{t("onboarding_skip_for_now")}
</Button>
<Button
type="submit"
type="button"
color="primary"
className="rounded-[10px]"
disabled={!hasValidInvites || isSubmitting}
@@ -401,8 +401,14 @@
"welcome_back": "Welcome back",
"welcome_to_calcom": "Welcome to {{appName}}",
"personal_welcome_description": "You're all set! Start scheduling meetings and managing your calendar.",
"unlimited_calendars": "Unlimited calendars",
"unlimited_event_types": "Unlimited event types",
"workflows_feature": "Workflows",
"integrate_with_favorite_apps": "Integrate with your favorite apps",
"accept_payments_via_stripe": "Accept payments via Stripe",
"html_react_embed": "HTML & React Embed",
"cal_ai_phone_agent": "Cal.ai phone agent",
"cal_video": "Cal Video",
"and_more": "And more…",
"welcome_instructions": "Tell us what to call you and let us know what timezone you're in. You'll be able to edit this later.",
"connect_caldav": "Connect to CalDav (Beta)",
@@ -9,7 +9,16 @@ import { Logo } from "@calcom/ui/components/logo";
import { useWelcomeToCalcomModal } from "../hooks/useWelcomeToCalcomModal";
const features = ["unlimited_event_types", "workflows_feature", "and_more"];
const features = [
"unlimited_calendars",
"unlimited_event_types",
"workflows_feature",
"integrate_with_favorite_apps",
"accept_payments_via_stripe",
"html_react_embed",
"cal_ai_phone_agent",
"cal_video",
];
export function WelcomeToCalcomModal() {
const { t } = useLocale();