Files
calendar/apps/web/modules/onboarding/components/plan-icon.tsx
T
sean-brydonandGitHub 52e27bab53 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
2025-11-07 07:58:34 +00:00

161 lines
5.4 KiB
TypeScript

import { motion } from "framer-motion";
import { Icon, type IconName } from "@calcom/ui/components/icon";
// 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 (
<motion.div
key={`main-icon-${variant}-${icon}`}
initial={{
opacity: 0,
y: (animationDirection === "down" ? -20 : 20) - 40, // -40 is half of 80px for centering
}}
animate={{
opacity: 1,
y: -40, // Centered position (half of 80px)
}}
exit={{
opacity: 0,
y: (animationDirection === "down" ? 20 : -20) - 40,
}}
transition={{
duration: 0.5,
ease: "easeInOut",
}}
className="bg-default absolute flex h-[80px] w-[80px] items-center justify-center overflow-clip rounded-full"
style={{
x: "-50%",
left: "50%",
top: "50%",
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-12 flex items-center justify-center">
<Icon
name={variant === "organization" ? "users" : icon}
size={32}
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,
}}
/>
</motion.div>
);
};
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
return (
<motion.div
key={`small-icon-${variant}-${ringIndex}-${angle}`}
initial={{
opacity: 0,
y: y - iconHalfSize + (animationDirection === "down" ? -20 : 20),
}}
animate={{
opacity: 1,
y: y - iconHalfSize,
}}
exit={{
opacity: 0,
y: y - iconHalfSize + (animationDirection === "down" ? 20 : -20),
}}
transition={{
duration: 0.5,
ease: "easeInOut",
delay: index * 0.05, // Stagger animation for small icons
}}
className="bg-default absolute flex h-[40.5px] w-[40.5px] items-center justify-center overflow-clip rounded-full shadow-[0px_2.075px_4.15px_0px_rgba(34,42,53,0.05),0px_0px_0px_0.519px_rgba(34,42,53,0.08),0px_0.519px_2.594px_-2.075px_rgba(19,19,22,0.7)]"
style={{
x: x - iconHalfSize,
left: "50%",
top: "50%",
background: "linear-gradient(to bottom, var(--cal-bg, #ffffff), var(--cal-bg-muted, #f7f7f7))",
}}>
<div className="flex items-center justify-center opacity-70">
<Icon name="user" size={24} strokeWidth={1.75} className="text-emphasis" />
</div>
<div
className="pointer-events-none absolute inset-0 rounded-full"
style={{
boxShadow: "0px 0.519px 0.311px 0px inset var(--cal-bg-inverted)",
opacity: 0.15,
}}
/>
</motion.div>
);
};
return (
<div className="relative mx-auto h-[465px] w-[465px] shrink-0 overflow-visible">
{/* 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 (
<div
key={index}
className="pointer-events-none absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 rounded-full"
style={{
width: `${size}px`,
height: `${size}px`,
border: "0.5px solid",
borderColor: "var(--cal-border-default, #D3D3D3)",
opacity: opacity,
}}
/>
);
})}
{/* Small user icons on rings (for team variant) */}
{(variant === "team" || variant === "organization") &&
TEAM_ICON_POSITIONS.map(({ ringIndex, angle }, index) =>
renderSmallUserIcon(ringIndex, angle, index)
)}
{renderIconContainer(0)}
</div>
);
}