187 lines
5.9 KiB
TypeScript
187 lines
5.9 KiB
TypeScript
|
|
"use client";
|
||
|
|
|
||
|
|
import { Fragment } from "react";
|
||
|
|
import Link from "next/link";
|
||
|
|
|
||
|
|
import { PricingAnimatedPrice } from "@/components/sections/PricingAnimatedPrice";
|
||
|
|
import { PricingBillingToggle } from "@/components/sections/PricingBillingToggle";
|
||
|
|
import { PricingCheckoutButton } from "@/components/sections/PricingCheckoutButton";
|
||
|
|
import {
|
||
|
|
PricingCompareFeatureLabel,
|
||
|
|
PricingCompareValueCell,
|
||
|
|
} from "@/components/sections/PricingCompareValue";
|
||
|
|
import type { BillingPeriod, PricingTier } from "@/components/sections/pricing-data";
|
||
|
|
import {
|
||
|
|
COMPARE_ANNUAL_SAVINGS_BADGE,
|
||
|
|
COMPARE_SECTIONS,
|
||
|
|
getCompareAtPrice,
|
||
|
|
getDisplayPrice,
|
||
|
|
PRICING_TIERS,
|
||
|
|
} from "@/components/sections/pricing-data";
|
||
|
|
import { Button } from "@/components/ui/button";
|
||
|
|
import type { PaidPlanId } from "@/lib/plans";
|
||
|
|
import { cn } from "@/lib/utils";
|
||
|
|
|
||
|
|
interface PricingCompareTableProps {
|
||
|
|
billing: BillingPeriod;
|
||
|
|
onBillingChange: (billing: BillingPeriod) => void;
|
||
|
|
}
|
||
|
|
|
||
|
|
function SavingsArrowIcon() {
|
||
|
|
return (
|
||
|
|
<svg
|
||
|
|
width="28"
|
||
|
|
height="20"
|
||
|
|
viewBox="0 0 28 20"
|
||
|
|
fill="none"
|
||
|
|
xmlns="http://www.w3.org/2000/svg"
|
||
|
|
className="text-blue-600"
|
||
|
|
aria-hidden
|
||
|
|
>
|
||
|
|
<path
|
||
|
|
d="M2 14C8 6 14 4 22 6"
|
||
|
|
stroke="currentColor"
|
||
|
|
strokeWidth="2"
|
||
|
|
strokeLinecap="round"
|
||
|
|
/>
|
||
|
|
<path
|
||
|
|
d="M18 4L23 6L21 11"
|
||
|
|
stroke="currentColor"
|
||
|
|
strokeWidth="2"
|
||
|
|
strokeLinecap="round"
|
||
|
|
strokeLinejoin="round"
|
||
|
|
/>
|
||
|
|
</svg>
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
function PlanHeaderCell({
|
||
|
|
tier,
|
||
|
|
billing,
|
||
|
|
}: {
|
||
|
|
tier: PricingTier;
|
||
|
|
billing: BillingPeriod;
|
||
|
|
}) {
|
||
|
|
const highlighted = tier.highlighted ?? false;
|
||
|
|
const isStripePlan = tier.id === "pro" || tier.id === "business";
|
||
|
|
|
||
|
|
return (
|
||
|
|
<th
|
||
|
|
className={cn(
|
||
|
|
"px-4 pb-4 pt-6 align-top",
|
||
|
|
highlighted && "bg-blue-50/30"
|
||
|
|
)}
|
||
|
|
>
|
||
|
|
{highlighted ? (
|
||
|
|
<span className="mb-2 inline-block rounded-full bg-gradient-to-r from-violet-500 to-blue-600 px-2.5 py-0.5 text-[10px] font-bold uppercase tracking-wide text-white">
|
||
|
|
Most Popular
|
||
|
|
</span>
|
||
|
|
) : (
|
||
|
|
<span className="mb-2 block h-5" aria-hidden />
|
||
|
|
)}
|
||
|
|
<p className="font-heading text-base font-bold text-neutral-900">
|
||
|
|
{tier.name}
|
||
|
|
</p>
|
||
|
|
<PricingAnimatedPrice
|
||
|
|
price={getDisplayPrice(tier, billing)}
|
||
|
|
compareAt={getCompareAtPrice(tier, billing)}
|
||
|
|
billing={billing}
|
||
|
|
size="compact"
|
||
|
|
/>
|
||
|
|
{isStripePlan ? (
|
||
|
|
<PricingCheckoutButton
|
||
|
|
plan={tier.id as PaidPlanId}
|
||
|
|
billing={billing}
|
||
|
|
label={tier.cta}
|
||
|
|
className={cn(
|
||
|
|
"mt-3 h-9 w-full rounded-lg text-sm font-semibold",
|
||
|
|
highlighted
|
||
|
|
? "bg-rf-blue hover:bg-rf-blue/90"
|
||
|
|
: "border border-gray-300 bg-white text-neutral-800 hover:bg-gray-50"
|
||
|
|
)}
|
||
|
|
variant={highlighted ? "default" : "secondary"}
|
||
|
|
/>
|
||
|
|
) : (
|
||
|
|
<Button
|
||
|
|
variant="outline"
|
||
|
|
className="mt-3 h-9 w-full rounded-lg border-gray-300 text-sm font-semibold"
|
||
|
|
asChild
|
||
|
|
>
|
||
|
|
<Link href="/auth?tab=sign-up">{tier.cta}</Link>
|
||
|
|
</Button>
|
||
|
|
)}
|
||
|
|
</th>
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
export function PricingCompareTable({
|
||
|
|
billing,
|
||
|
|
onBillingChange,
|
||
|
|
}: PricingCompareTableProps) {
|
||
|
|
const lite = PRICING_TIERS.find((t) => t.id === "lite");
|
||
|
|
const pro = PRICING_TIERS.find((t) => t.id === "pro");
|
||
|
|
const business = PRICING_TIERS.find((t) => t.id === "business");
|
||
|
|
|
||
|
|
if (!lite || !pro || !business) return null;
|
||
|
|
|
||
|
|
return (
|
||
|
|
<div className="mx-auto w-full max-w-5xl overflow-x-auto rounded-2xl border border-gray-100 bg-white shadow-sm">
|
||
|
|
<table className="w-full min-w-[760px] border-collapse">
|
||
|
|
<thead className="sticky top-0 z-10 bg-white">
|
||
|
|
<tr className="border-b border-gray-100">
|
||
|
|
<th className="w-[38%] px-6 pb-4 pt-6 text-left align-top">
|
||
|
|
<h3 className="bg-gradient-to-r from-blue-600 to-violet-600 bg-clip-text font-heading text-lg font-bold text-transparent sm:text-xl">
|
||
|
|
Compare Plans & Features
|
||
|
|
</h3>
|
||
|
|
<div className="mt-4 items-start">
|
||
|
|
<PricingBillingToggle
|
||
|
|
billing={billing}
|
||
|
|
onChange={onBillingChange}
|
||
|
|
layoutId="pricing-compare-billing-pill"
|
||
|
|
/>
|
||
|
|
</div>
|
||
|
|
<p className="mt-3 flex items-center gap-1 text-xs font-bold uppercase tracking-wide text-blue-600">
|
||
|
|
Save up to {COMPARE_ANNUAL_SAVINGS_BADGE}%
|
||
|
|
<SavingsArrowIcon />
|
||
|
|
</p>
|
||
|
|
</th>
|
||
|
|
<PlanHeaderCell tier={lite} billing={billing} />
|
||
|
|
<PlanHeaderCell tier={pro} billing={billing} />
|
||
|
|
<PlanHeaderCell tier={business} billing={billing} />
|
||
|
|
</tr>
|
||
|
|
</thead>
|
||
|
|
<tbody>
|
||
|
|
{COMPARE_SECTIONS.map((section) => (
|
||
|
|
<Fragment key={section.title}>
|
||
|
|
<tr className="bg-gray-50">
|
||
|
|
<td
|
||
|
|
colSpan={4}
|
||
|
|
className="px-6 py-3 text-xs font-bold uppercase tracking-widest text-gray-500"
|
||
|
|
>
|
||
|
|
{section.title}
|
||
|
|
</td>
|
||
|
|
</tr>
|
||
|
|
{section.rows.map((row) => (
|
||
|
|
<tr
|
||
|
|
key={`${section.title}-${row.feature}`}
|
||
|
|
className="border-b border-gray-100 transition-colors hover:bg-gray-50/60"
|
||
|
|
>
|
||
|
|
<td className="px-6 py-3">
|
||
|
|
<PricingCompareFeatureLabel
|
||
|
|
feature={row.feature}
|
||
|
|
tooltip={row.tooltip}
|
||
|
|
/>
|
||
|
|
</td>
|
||
|
|
<PricingCompareValueCell value={row.lite} />
|
||
|
|
<PricingCompareValueCell value={row.pro} highlighted />
|
||
|
|
<PricingCompareValueCell value={row.business} />
|
||
|
|
</tr>
|
||
|
|
))}
|
||
|
|
</Fragment>
|
||
|
|
))}
|
||
|
|
</tbody>
|
||
|
|
</table>
|
||
|
|
</div>
|
||
|
|
);
|
||
|
|
}
|