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:
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 ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
|
||||
}
|
||||
Reference in new issue
Block a user