Reworked the pricing tiers to include a segmented controller and tooltips

This commit is contained in:
James Ritchie
2023-11-22 16:53:49 +00:00
parent 8da009d58d
commit c4d54d2e08
6 changed files with 196 additions and 107 deletions
@@ -3,6 +3,9 @@ import { Paragraph } from "../primitives/Paragraph";
import * as Slider from "@radix-ui/react-slider";
import { Button } from "../primitives/Buttons";
import { CheckIcon, XMarkIcon } from "@heroicons/react/24/solid";
import SegmentedControl from "../primitives/SegmentedControl";
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "../primitives/Tooltip";
import { Header3 } from "../primitives/Headers";
export function PricingTiers({
children,
@@ -27,15 +30,21 @@ export function TierFree() {
return (
<TierContainer>
<Header title="Free" flatCost={0} />
<TierLimit description="1 concurrent Run / month" />
<TierLimit>
Up to 5{" "}
<DefinitionTip title="Concurrent Runs" content="Description">
Concurrent Runs
</DefinitionTip>
</TierLimit>
<Button variant="secondary/large" fullWidth className="text-md my-6 font-medium">
Current Plan
</Button>
<ul className="flex flex-col gap-2.5">
<FeatureItem checked title="Up to 2 team members" />
<FeatureItem checked title="Up to 10 Jobs" />
<FeatureItem checked title="Unlimited Job Runs" />
<FeatureItem checked title="Unlimited Run duration" />
<FeatureItem checked title="Up to 10k Job Runs" />
<FeatureItem checked title="Unlimited Jobs" />
<FeatureItem checked title="Unlimited Tasks" />
<FeatureItem checked title="Unlimited Events" />
<FeatureItem checked title="Unlimited team members" />
<FeatureItem checked title="24 hour log retention" />
<FeatureItem checked title="Community support" />
<FeatureItem title="Custom integrations" />
@@ -51,15 +60,20 @@ export function TierPro() {
return (
<TierContainer isHighlighted>
<Header title="Pro" isHighlighted flatCost={25} />
<TierLimit pricedMetric description="Up to 5 concurrent Runs / month" />
<TierLimit pricedMetric>
<DefinitionTip title="Concurrent Runs" content="Description">
Concurrent Runs
</DefinitionTip>
</TierLimit>
<Button variant="primary/large" fullWidth className="text-md my-6 font-medium">
Upgrade
</Button>
<ul className="flex flex-col gap-2.5">
<FeatureItem checked title="Unlimited team members" />
<FeatureItem checked title="Includes 10k Job Runs, then < $1.30/1,000 Runs" />
<FeatureItem checked title="Unlimited Jobs" />
<FeatureItem checked title="Unlimited Job Runs" />
<FeatureItem checked title="Unlimited Run duration" />
<FeatureItem checked title="Unlimited Tasks" />
<FeatureItem checked title="Unlimited Events" />
<FeatureItem checked title="Unlimited team members" />
<FeatureItem checked title="7 day log retention" />
<FeatureItem checked title="Dedicated Slack support" />
<FeatureItem title="Custom integrations" />
@@ -75,15 +89,21 @@ export function TierEnterprise() {
return (
<TierContainer>
<Header title="Enterprise" />
<TierLimit description="Flexible concurrent Runs / month" />
<TierLimit>
Flexible{" "}
<DefinitionTip title="Concurrent Runs" content="Description">
concurrent Runs
</DefinitionTip>
</TierLimit>
<Button variant="secondary/large" fullWidth className="text-md my-6 font-medium">
Contact us
</Button>
<ul className="flex flex-col gap-2.5">
<FeatureItem checked title="Unlimited team members" />
<FeatureItem checked title="Flexible Job Runs" />
<FeatureItem checked title="Unlimited Jobs" />
<FeatureItem checked title="Unlimited Job Runs" />
<FeatureItem checked title="Unlimited Run duration" />
<FeatureItem checked title="Unlimited Tasks" />
<FeatureItem checked title="Unlimited Events" />
<FeatureItem checked title="Unlimited team members" />
<FeatureItem checked title="30 day log retention" />
<FeatureItem checked title="Priority support" />
<FeatureItem checked title="Custom integrations" />
@@ -114,6 +134,36 @@ function TierContainer({
);
}
function DefinitionTip({
content,
children,
title,
}: {
content: React.ReactNode;
children: React.ReactNode;
title: React.ReactNode;
}) {
return (
<TooltipProvider>
<Tooltip disableHoverableContent>
<TooltipTrigger>
<span className="underline decoration-slate-600 decoration-dashed underline-offset-4 transition hover:decoration-slate-500">
{children}
</span>
</TooltipTrigger>
<TooltipContent align="end" side="right" variant="dark" className="">
<Header3 className="mb-1">{title}</Header3>
{typeof content === "string" ? (
<Paragraph variant="small">{content}</Paragraph>
) : (
<div>{content}</div>
)}
</TooltipContent>
</Tooltip>
</TooltipProvider>
);
}
function Header({
title,
flatCost,
@@ -140,18 +190,41 @@ function Header({
);
}
function TierLimit({ description, pricedMetric }: { description: string; pricedMetric?: boolean }) {
function TierLimit({
children,
pricedMetric,
}: {
children: React.ReactNode;
pricedMetric?: boolean;
}) {
return (
<div>
{pricedMetric ? <PricingSlider /> : <hr className="my-[1.6rem]" />}
<Paragraph variant="small/bright" className="">
{description}
</Paragraph>
{pricedMetric ? (
<>
<Paragraph variant="small/bright" className="mb-2 mt-6">
{children}
</Paragraph>
<SegmentedControl name={"Concurrent Runs"} options={options} fullWidth />
</>
) : (
<>
<hr className="my-[1.9rem]" />
<Paragraph variant="small/bright" className="mb-[0.6rem]">
{children}
</Paragraph>
</>
)}
</div>
);
}
function PricingSlider() {
const options = [
{ label: "Up to 20", value: "20" },
{ label: "Up to 50", value: "50" },
{ label: "Up to 100", value: "100" },
];
function UsageSlider() {
return (
<form>
<Slider.Root
@@ -1,72 +0,0 @@
import { RadioGroup } from "@headlessui/react";
import { cn } from "~/utils/cn";
type Options = {
label: string;
value: string;
};
type FormSegmentedControlProps = {
name: string;
defaultValue?: string;
options: Options[];
onChange?: (value: string) => void;
};
export default function FormSegmentedControl({
name,
defaultValue,
options,
onChange,
}: FormSegmentedControlProps) {
return (
<div className="flex h-8 w-fit rounded bg-slate-800 p-1">
<RadioGroup
defaultValue={defaultValue ?? options[0].value}
name={name}
onChange={(c: string) => {
if (onChange) {
onChange(c);
}
}}
>
<div className="flex gap-x-1.5">
{options.map((option) => (
<RadioGroup.Option
key={option.value}
value={option.value}
className={({ active, checked }) =>
cn(
"relative flex cursor-pointer rounded-[2px] px-3 py-[0.13rem] shadow-md focus:outline-none",
active
? "focus-visible:ring focus-visible:ring-indigo-500 focus-visible:ring-opacity-60"
: "",
checked
? "bg-slate-700 text-bright"
: "bg-transparent transition hover:bg-slate-750"
)
}
>
{({ checked }) => (
<>
<div className="flex w-full items-center justify-between">
<div className="flex items-center">
<div className="text-sm">
<RadioGroup.Label
as="p"
className={cn("font-normal", checked ? "text-bright" : "text-dimmed")}
>
{option.label}
</RadioGroup.Label>
</div>
</div>
</div>
</>
)}
</RadioGroup.Option>
))}
</div>
</RadioGroup>
</div>
);
}
@@ -0,0 +1,74 @@
import { RadioGroup } from "@headlessui/react";
import { motion } from "framer-motion";
import { cn } from "~/utils/cn";
type Options = {
label: string;
value: string;
};
type SegmentedControlProps = {
name: string;
defaultValue?: string;
options: Options[];
fullWidth?: boolean;
onChange?: (value: string) => void;
};
export default function SegmentedControl({
name,
defaultValue,
options,
fullWidth,
onChange,
}: SegmentedControlProps) {
return (
<div className={cn("flex h-10 rounded bg-slate-850", fullWidth ? "w-full" : "w-fit")}>
<RadioGroup
defaultValue={defaultValue ?? options[0].value}
name={name}
onChange={(c: string) => {
if (onChange) {
onChange(c);
}
}}
className="w-full"
>
<div className="flex h-full w-full items-center justify-between">
{options.map((option) => (
<RadioGroup.Option
key={option.value}
value={option.value}
className={({ active, checked }) =>
cn(
"relative flex h-full grow cursor-pointer rounded-[2px] font-normal focus:outline-none",
active
? "ring-offset-2 focus-visible:ring focus-visible:ring-indigo-500 focus-visible:ring-opacity-60"
: "",
checked ? "text-bright" : "text-dimmed transition hover:text-bright"
)
}
>
{({ checked }) => (
<>
<div className="relative flex h-full w-full items-center justify-between px-3 py-[0.13rem]">
<div className="flex h-full w-full items-center justify-center text-sm">
<RadioGroup.Label as="p">{option.label}</RadioGroup.Label>
</div>
{checked && (
<motion.div
layoutId={`segmented-control-${name}`}
transition={{ duration: 0.4, type: "spring" }}
className="absolute left-0 top-0 h-full w-full rounded-md border-4 border-indigo-600 shadow"
></motion.div>
)}
</div>
</>
)}
</RadioGroup.Option>
))}
</div>
</RadioGroup>
</div>
);
}
@@ -1,9 +1,15 @@
"use client";
import * as React from "react";
import * as TooltipPrimitive from "@radix-ui/react-tooltip";
import { cn } from "~/utils/cn";
const variantClasses = {
basic:
"bg-popover border border-slate-800 rounded-md px-3 py-1.5 text-sm text-bright shadow-md fade-in-50",
dark: "bg-background border border-border rounded px-3 py-2 text-sm text-bright shadow-md fade-in-50",
};
type Variant = keyof typeof variantClasses;
const TooltipProvider = TooltipPrimitive.Provider;
const TooltipArrow = React.forwardRef<
@@ -22,15 +28,20 @@ Tooltip.displayName = TooltipPrimitive.Root.displayName;
const TooltipTrigger = TooltipPrimitive.Trigger;
type TooltipContentProps = {
variant?: Variant;
} & React.ComponentPropsWithoutRef<typeof TooltipPrimitive.Content>;
const TooltipContent = React.forwardRef<
React.ElementRef<typeof TooltipPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof TooltipPrimitive.Content>
>(({ className, sideOffset = 4, ...props }, ref) => (
TooltipContentProps
>(({ className, sideOffset = 4, variant = "basic", ...props }, ref) => (
<TooltipPrimitive.Content
ref={ref}
sideOffset={sideOffset}
className={cn(
"z-50 overflow-hidden rounded-md border border-slate-800 bg-popover px-3 py-1.5 text-sm text-bright shadow-md animate-in fade-in-50 data-[side=bottom]:slide-in-from-top-1 data-[side=left]:slide-in-from-right-1 data-[side=right]:slide-in-from-left-1 data-[side=top]:slide-in-from-bottom-1",
"z-50 overflow-hidden animate-in data-[side=bottom]:slide-in-from-top-1 data-[side=left]:slide-in-from-right-1 data-[side=right]:slide-in-from-left-1 data-[side=top]:slide-in-from-bottom-1",
variantClasses[variant],
className
)}
{...props}
@@ -43,17 +54,19 @@ function SimpleTooltip({
content,
side,
hidden,
variant,
}: {
button: React.ReactNode;
content: React.ReactNode;
side?: React.ComponentProps<typeof TooltipContent>["side"];
hidden?: boolean;
variant?: Variant;
}) {
return (
<TooltipProvider>
<Tooltip>
<TooltipTrigger>{button}</TooltipTrigger>
<TooltipContent side={side} hidden={hidden} className="text-xs">
<TooltipContent side={side} hidden={hidden} className="text-xs" variant={variant}>
{content}
</TooltipContent>
</Tooltip>
@@ -2,7 +2,7 @@ import type { Meta, StoryObj } from "@storybook/react";
import { PricingTiers, TierEnterprise, TierFree, TierPro } from "../billing/PricingTiers";
const meta: Meta<typeof AllPricingTiers> = {
title: "Components/PricingTiers",
title: "Billing/PricingTiers",
component: AllPricingTiers,
};
@@ -16,8 +16,8 @@ export const AllTiers: Story = {
function AllPricingTiers() {
return (
<div className="mx-4 flex h-screen items-center justify-center">
<PricingTiers className="">
<div className="mx-4 flex h-screen flex-col items-center justify-center gap-4">
<PricingTiers className="w-[80rem]">
<TierFree />
<TierPro />
<TierEnterprise />
@@ -1,19 +1,20 @@
import type { Meta, StoryObj } from "@storybook/react";
import { withDesign } from "storybook-addon-designs";
import FormSegmentedControl from "../primitives/FormSegmentedControl";
import { MainCenteredContainer } from "../layout/AppLayout";
import SegmentedControl from "../primitives/SegmentedControl";
const meta: Meta = {
title: "Primitives/FormSegmentedControl",
const meta: Meta<typeof StyledSegmentedControl> = {
title: "Primitives/SegmentedControl",
decorators: [withDesign],
component: StyledSegmentedControl,
};
export default meta;
type Story = StoryObj<typeof FormSegmentedControl>;
type Story = StoryObj<typeof StyledSegmentedControl>;
export const Basic: Story = {
render: () => <SegmentedControl />,
render: () => <StyledSegmentedControl />,
};
Basic.parameters = {
@@ -28,10 +29,10 @@ const options = [
{ label: "Label 2", value: "Users" },
];
function SegmentedControl() {
function StyledSegmentedControl() {
return (
<MainCenteredContainer>
<FormSegmentedControl name="name" options={options} />
<SegmentedControl name="name" options={options} />
</MainCenteredContainer>
);
}