chore: illustrations to getting-started page in onboarding v3 (#24673)

* i18n

* WIP icon stuff

* More icon work

* More gradient tuning

* Mvoe plan-icon to planicon.tsx

* Fix cubic suggestion

* Fix darkmode icon and gradients

* Fix type error
This commit is contained in:
sean-brydon authored and GitHub committed 2025-10-27 17:32:06 +00:00
1 parent 66c5f810ea
commit 66234ffba9
3 files changed
+191 -35

No files matched your search

@@ -0,0 +1,107 @@
import classNames from "@calcom/ui/classNames";
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%";
return (
<div
key={index}
className="bg-default absolute top-[10px] flex h-[55px] w-[55px] -translate-x-1/2 items-center justify-center overflow-clip rounded-full"
style={{
left: leftPosition,
background: "linear-gradient(to bottom, var(--cal-bg, #ffffff), var(--cal-bg-muted, #f7f7f7))",
boxShadow:
"0px 2.818px 5.635px 0px var(--cal-border-subtle), 0px 0px 0px 0.704px var(--cal-border), 0px 0.704px 3.522px -2.818px rgba(0, 0, 0, 0.3)",
}}>
{/* Icon */}
<div className="size-8 flex items-center justify-center">
<Icon name={icon} size={24} strokeWidth={1.75} className="text-emphasis opacity-80" />
</div>
{/* Inner highlight/shine effect */}
<div
className="pointer-events-none absolute inset-0 rounded-full"
style={{
boxShadow: "0px 0.704px 0.423px 0px inset var(--cal-bg-inverted)",
opacity: 0.15,
}}
/>
</div>
);
};
return (
<div className="relative h-[76px] w-[160px] shrink-0 overflow-visible">
{/* Outer ring - SVG with linear gradient */}
<svg
className="pointer-events-none absolute left-[calc(50%+0.627px)] top-[-40px] -translate-x-1/2"
width="156"
height="156"
viewBox="0 0 156 156"
fill="none"
xmlns="http://www.w3.org/2000/svg"
style={{
maskImage:
"linear-gradient(180deg, transparent 0%, black 2%, black 50%, black 75%, transparent 100%)",
WebkitMaskImage:
"linear-gradient(180deg, transparent 0%, black 2%, black 50%, black 75%, transparent 100%)",
}}>
<circle opacity="0.3" cx="78" cy="78" r="77.5" stroke="url(#paint0_linear_outer)" strokeWidth="0.5" />
<defs>
<linearGradient
id="paint0_linear_outer"
x1="78"
y1="0"
x2="78"
y2="156"
gradientUnits="userSpaceOnUse">
<stop stopColor="var(--cal-border-default, #D3D3D3)" />
<stop offset="1" stopColor="var(--cal-border-emphasis, #B2B2B2)" />
</linearGradient>
</defs>
</svg>
{/* Middle ring - SVG with linear gradient */}
<svg
className="pointer-events-none absolute left-[calc(50%+0.628px)] top-[-20.01px] -translate-x-1/2"
width="111"
height="111"
viewBox="0 0 111 111"
fill="none"
xmlns="http://www.w3.org/2000/svg"
style={{
maskImage:
"linear-gradient(180deg, transparent 0%, black 2%, black 50%, black 75%, transparent 100%)",
WebkitMaskImage:
"linear-gradient(180deg, transparent 0%, black 2%, black 50%, black 75%, transparent 100%)",
}}>
<circle
opacity="0.4"
cx="55.461"
cy="55.455"
r="55.211"
stroke="url(#paint0_linear_middle)"
strokeWidth="0.5"
/>
<defs>
<linearGradient
id="paint0_linear_middle"
x1="55.461"
y1="-0.006"
x2="55.461"
y2="110.916"
gradientUnits="userSpaceOnUse">
<stop stopColor="var(--cal-border-default, #D3D3D3)" />
<stop offset="1" stopColor="var(--cal-border-emphasis, #B2B2B2)" />
</linearGradient>
</defs>
</svg>
{/* Main icon container(s) with gradient background */}
{variant === "single" ? renderIconContainer(0) : [renderIconContainer(0), renderIconContainer(1)]}
</div>
);
}
@@ -3,12 +3,16 @@
import { useRouter } from "next/navigation";
import { isCompanyEmail } from "@calcom/features/ee/organizations/lib/utils";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import classNames from "@calcom/ui/classNames";
import { Badge } from "@calcom/ui/components/badge";
import { Button } from "@calcom/ui/components/button";
import { type IconName } from "@calcom/ui/components/icon";
import { Logo } from "@calcom/ui/components/logo";
import { RadioAreaGroup } from "@calcom/ui/components/radio";
import { OnboardingContinuationPrompt } from "../components/onboarding-continuation-prompt";
import { PlanIcon } from "../components/plan-icon";
import { useOnboardingStore, type PlanType } from "../store/onboarding-store";
type OnboardingViewProps = {
@@ -18,6 +22,7 @@ type OnboardingViewProps = {
export const OnboardingView = ({ userName, userEmail }: OnboardingViewProps) => {
const router = useRouter();
const { t } = useLocale();
const { selectedPlan, setSelectedPlan } = useOnboardingStore();
const handleContinue = () => {
@@ -30,25 +35,36 @@ export const OnboardingView = ({ userName, userEmail }: OnboardingViewProps) =>
}
};
const planIconByType: Record<PlanType, IconName> = {
personal: "user",
team: "users",
organization: "users",
};
const allPlans = [
{
id: "personal" as PlanType,
title: "For personal use",
badge: "Free",
description: "Good for individuals who are just starting out and simply want the essentials.",
title: t("onboarding_plan_personal_title"),
badge: t("onboarding_plan_personal_badge"),
description: t("onboarding_plan_personal_description"),
icon: planIconByType.personal,
variant: "single" as const,
},
{
id: "team" as PlanType,
title: "With my team",
badge: "$15 p/mo p/person",
description:
"Highly recommended for small teams who seek to upgrade their time and perform better as a unit.",
title: t("onboarding_plan_team_title"),
badge: t("onboarding_plan_team_badge"),
description: t("onboarding_plan_team_description"),
icon: planIconByType.team,
variant: "single" as const,
},
{
id: "organization" as PlanType,
title: "For my organization",
badge: "$37 p/mo p/person",
description: "Robust scheduling for larger teams looking to have more control, privacy, and security.",
title: t("onboarding_plan_organization_title"),
badge: t("onboarding_plan_organization_badge"),
description: t("onboarding_plan_organization_description"),
icon: planIconByType.organization,
variant: "double" as const,
},
];
@@ -89,9 +105,11 @@ export const OnboardingView = ({ userName, userEmail }: OnboardingViewProps) =>
{/* 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">Hey {userName}</h1>
<h1 className="font-cal text-xl font-semibold leading-6">
{t("onboarding_welcome_message", { userName })}
</h1>
<p className="text-subtle text-sm font-medium leading-tight">
To personalize your experience, what do you plan to use Cal.com for?
{t("onboarding_welcome_question")}
</p>
</div>
</div>
@@ -101,35 +119,55 @@ export const OnboardingView = ({ userName, userEmail }: OnboardingViewProps) =>
value={selectedPlan ?? undefined}
onValueChange={(value) => setSelectedPlan(value as PlanType)}
className="flex w-full flex-col gap-1 rounded-[10px]">
{plans.map((plan) => (
<RadioAreaGroup.Item
key={plan.id}
value={plan.id}
className={
selectedPlan === plan.id ? "border-emphasis bg-default" : "bg-default border-subtle"
}>
<div className="flex w-full flex-col gap-2">
<div className="flex items-start gap-2">
<p className="text-emphasis text-base font-semibold leading-4">{plan.title}</p>
<Badge variant="gray" size="md" className="hidden h-4 rounded-md px-1 py-1 md:block">
<span className="text-emphasis text-xs font-medium leading-3">{plan.badge}</span>
</Badge>
{plans.map((plan) => {
const isSelected = selectedPlan === plan.id;
return (
<RadioAreaGroup.Item
key={plan.id}
value={plan.id}
className={classNames(
"bg-default relative flex items-center overflow-hidden rounded-[10px] border transition",
isSelected ? "border-emphasis shadow-sm" : "border-subtle",
"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",
}}>
<div className="flex w-full items-center gap-4">
<PlanIcon icon={plan.icon} variant={plan.variant} />
<div className="flex flex-1 flex-col gap-2">
<div className="flex flex-wrap items-center gap-2">
<p className="text-emphasis text-base font-semibold leading-5">{plan.title}</p>
<Badge
variant="gray"
size="md"
className="hidden h-4 rounded-md px-1 py-1 md:flex md:items-center">
<span className="text-emphasis text-xs font-medium leading-3">
{plan.badge}
</span>
</Badge>
</div>
<Badge
variant="gray"
size="md"
className="h-4 w-fit rounded-md px-1 py-1 md:hidden">
<span className="text-emphasis text-xs font-medium leading-3">{plan.badge}</span>
</Badge>
<p className="text-subtle max-w-full text-sm font-normal leading-tight">
{plan.description}
</p>
</div>
</div>
<Badge variant="gray" size="md" className="h-4 w-fit rounded-md px-1 py-1 md:hidden">
<span className="text-emphasis text-xs font-medium leading-3">{plan.badge}</span>
</Badge>
<p className="text-subtle max-w-full text-sm font-normal leading-tight">
{plan.description}
</p>
</div>
</RadioAreaGroup.Item>
))}
</RadioAreaGroup.Item>
);
})}
</RadioAreaGroup.Group>
{/* Footer */}
<div className="flex w-full items-center justify-end gap-1 px-5 py-4">
<Button color="primary" className="rounded-[10px]" onClick={handleContinue}>
Continue
{t("continue")}
</Button>
</div>
</div>
@@ -3886,5 +3886,16 @@
"onboarding_skip_for_now": "Skip for now",
"onboarding_or_divider": "or",
"onboarding_team_required": "Team is required",
"onboarding_welcome_message": "Hey {{userName}}",
"onboarding_welcome_question": "To personalize your experience, what do you plan to use Cal.com for?",
"onboarding_plan_personal_title": "For personal use",
"onboarding_plan_personal_badge": "Free",
"onboarding_plan_personal_description": "Good for individuals who are just starting out and simply want the essentials.",
"onboarding_plan_team_title": "With my team",
"onboarding_plan_team_description": "Highly recommended for small teams who seek to upgrade their time and perform better as a unit.",
"onboarding_plan_organization_title": "For my organization",
"onboarding_plan_team_badge": "$15/user/mo",
"onboarding_plan_organization_badge": "$37/user/mo",
"onboarding_plan_organization_description": "Robust scheduling for larger teams looking to have more control, privacy, and security.",
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Add your new strings above here ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
}