Compare commits

...

1 Commits

Author SHA1 Message Date
Henry
ac9bdaf316 feat: improve pricing tiers and add comparison table 2026-02-18 21:58:58 +00:00
3 changed files with 743 additions and 403 deletions

View File

@@ -1,293 +1,587 @@
import { t } from "@lingui/core/macro"; import { t } from "@lingui/core/macro";
import React, { useEffect, useRef, useState } from "react";
import { HiCheckCircle, HiXCircle } from "react-icons/hi2"; import { HiCheckCircle, HiXCircle } from "react-icons/hi2";
import { twMerge } from "tailwind-merge";
interface Feature {
key: string;
label: string;
kan: boolean | string;
trello: boolean | string;
}
interface Section {
name: string;
features: Feature[];
}
type FrequencyValue = "monthly" | "annually"; type FrequencyValue = "monthly" | "annually";
const CellValue = ({ value }: { value: boolean | string }) => { interface PlanFeature {
if (typeof value === "string") { key: string;
return ( label: string;
<span className="text-sm text-dark-900 dark:text-dark-900">{value}</span> free: string | boolean | { text: string; highlight?: string };
); teams: string | boolean | { text: string; highlight?: string };
} pro: string | boolean | { text: string; highlight?: string };
return value ? ( enterprise: string | boolean | { text: string; highlight?: string };
<HiCheckCircle className="h-5 w-5 text-light-950 dark:text-dark-1000" /> }
) : (
<HiXCircle className="h-5 w-5 text-light-700 dark:text-dark-600" /> interface FeatureSection {
); name: string;
}; features: PlanFeature[];
}
const FeatureComparisonTable = ({ const FeatureComparisonTable = ({
frequencyValue, frequencyValue: _frequencyValue,
}: { }: {
frequencyValue: FrequencyValue; frequencyValue: FrequencyValue;
}) => { }) => {
const sections: Section[] = [ // Keep pricing column headers visible as you scroll
const containerRef = useRef<HTMLDivElement | null>(null);
const [isHeaderFixed, setIsHeaderFixed] = useState(false);
const [headerRect, setHeaderRect] = useState<{ left: number; width: number }>({
left: 0,
width: 0,
});
useEffect(() => {
const HEADER_OFFSET = 64; // px; matches fixed top nav height
const handleScroll = () => {
if (!containerRef.current) return;
// Hide fixed header on mobile (screens smaller than 620px)
if (window.innerWidth < 620) {
setIsHeaderFixed(false);
return;
}
const rect = containerRef.current.getBoundingClientRect();
const pastHeader = rect.top <= HEADER_OFFSET;
const aboveBottom = rect.bottom > HEADER_OFFSET + 40;
const shouldFix = pastHeader && aboveBottom;
setIsHeaderFixed(shouldFix);
if (shouldFix) {
setHeaderRect({
left: rect.left,
width: rect.width,
});
}
};
handleScroll();
window.addEventListener("scroll", handleScroll, { passive: true });
window.addEventListener("resize", handleScroll);
return () => {
window.removeEventListener("scroll", handleScroll);
window.removeEventListener("resize", handleScroll);
};
}, []);
const planSpecificLimits: PlanFeature[] = [
{ {
name: t`Core features`, key: "boards",
features: [ label: t`Boards`,
{ free: { text: t`Unlimited boards`, highlight: "Unlimited" },
key: "ulimited-workspaces", teams: { text: t`Unlimited boards`, highlight: "Unlimited" },
label: t`Unlimited workspaces`, pro: { text: t`Unlimited boards`, highlight: "Unlimited" },
kan: true, enterprise: { text: t`Unlimited boards`, highlight: "Unlimited" },
trello: false,
},
{
key: "unlimited-boards",
label: t`Unlimited boards`,
kan: true,
trello: false,
},
{
key: "unlimited-lists",
label: t`Unlimited lists`,
kan: true,
trello: true,
},
{
key: "unlimited-cards",
label: t`Unlimited cards`,
kan: true,
trello: true,
},
{
key: "activity-log",
label: t`Activity log`,
kan: true,
trello: true,
},
{
key: "templates",
label: t`Custom templates`,
kan: true,
trello: false,
},
{ key: "labels", label: t`Labels & filters`, kan: true, trello: true },
{ key: "checklists", label: t`Checklists`, kan: true, trello: true },
{
key: "import",
label: t`Import from Trello`,
kan: true,
trello: false,
},
{
key: "visibility",
label: t`Board visibility`,
kan: true,
trello: true,
},
{
key: "search",
label: t`Intelligent search`,
kan: true,
trello: true,
},
{
key: "workspace-url",
label: t`Custom workspace link`,
kan: true,
trello: false,
},
],
}, },
{ {
name: t`Teams`, key: "members",
features: [ label: t`Members`,
{ free: { text: t`1 user`, highlight: "1" },
key: "pricing", teams: { text: t`Per-seat pricing`, highlight: "Per-seat" },
label: t`Price per user/month`, pro: { text: t`Unlimited members`, highlight: "Unlimited" },
kan: frequencyValue === "monthly" ? `$10.00` : `$8.00`, enterprise: { text: t`Unlimited members`, highlight: "Unlimited" },
trello: frequencyValue === "monthly" ? `$12.00` : `$10.00`, },
}, {
{ key: "file-uploads",
key: "comments", label: t`File uploads`,
label: t`Comments & mentions`, free: { text: t`10mb file uploads`, highlight: "10mb" },
kan: true, teams: { text: t`Unlimited file uploads`, highlight: "Unlimited" },
trello: true, pro: { text: t`Unlimited file uploads`, highlight: "Unlimited" },
}, enterprise: { text: t`Unlimited file uploads`, highlight: "Unlimited" },
{ },
key: "assignments", {
label: t`Assignees`, key: "workspace-username",
kan: true, label: t`Workspace username`,
trello: true, free: { text: t`Default username`, highlight: "Default" },
}, teams: { text: t`Default username`, highlight: "Default" },
{ pro: { text: t`Custom username`, highlight: "Custom" },
key: "sharing", enterprise: { text: t`Custom username`, highlight: "Custom" },
label: t`Board sharing & invites`, },
kan: true, ];
trello: true,
}, const coreFeatures: PlanFeature[] = [
{ {
key: "invite-links", key: "checklists",
label: t`Invite links`, label: t`Checklists`,
kan: true, free: true,
trello: true, teams: true,
}, pro: true,
], enterprise: true,
},
{
key: "custom-templates",
label: t`Board templates`,
free: true,
teams: true,
pro: true,
enterprise: true,
},
{
key: "labels-filters",
label: t`Labels & filters`,
free: true,
teams: true,
pro: true,
enterprise: true,
},
{
key: "board-visibility",
label: t`Board visibility`,
free: true,
teams: true,
pro: true,
enterprise: true,
},
{
key: "search",
label: t`Intelligent search`,
free: true,
teams: true,
pro: true,
enterprise: true,
},
{
key: "activity-log",
label: t`Activity log`,
free: true,
teams: true,
pro: true,
enterprise: true,
},
{
key: "comments",
label: t`Comments`,
free: true,
teams: true,
pro: true,
enterprise: true,
},
{
key: "roles-permissions",
label: t`Roles & permissions`,
free: false,
teams: false,
pro: true,
enterprise: true,
},
{
key: "custom-branding",
label: t`Custom branding`,
free: false,
teams: false,
pro: true,
enterprise: true,
},
];
const collaborationFeatures: PlanFeature[] = [
{
key: "mentions",
label: t`Mentions`,
free: false,
teams: true,
pro: true,
enterprise: true,
},
{
key: "email-notifications",
label: t`Email notifications`,
free: false,
teams: true,
pro: true,
enterprise: true,
},
{
key: "assignees",
label: t`Assignees`,
free: false,
teams: true,
pro: true,
enterprise: true,
},
{
key: "invite-links",
label: t`Invite links`,
free: false,
teams: true,
pro: true,
enterprise: true,
},
];
const importsFeatures: PlanFeature[] = [
{
key: "import-export",
label: t`Trello`,
free: true,
teams: true,
pro: true,
enterprise: true,
},
];
const platformFeatures: PlanFeature[] = [
{
key: "open-source",
label: t`Open source`,
free: true,
teams: true,
pro: true,
enterprise: true,
},
{
key: "rest-api",
label: t`API`,
free: { text: t`Limited API access` },
teams: { text: t`Full API access` },
pro: { text: t`Full API access` },
enterprise: { text: t`Full API access` },
},
{
key: "self-hostable",
label: t`On-premise`,
free: false,
teams: false,
pro: false,
enterprise: true,
},
];
const securityFeatures: PlanFeature[] = [
{
key: "sso",
label: t`SSO`,
free: { text: t`Google SSO` },
teams: { text: t`Google SSO` },
pro: { text: t`Google SSO` },
enterprise: { text: t`Google SSO + SAML` },
},
{
key: "admin-roles",
label: t`Admin roles`,
free: { text: t`Admin roles` },
teams: { text: t`Admin roles` },
pro: { text: t`Admin roles` },
enterprise: { text: t`Advanced admin roles` },
},
];
const supportFeatures: PlanFeature[] = [
{
key: "priority-email-support",
label: t`Priority email support`,
free: false,
teams: true,
pro: true,
enterprise: true,
},
{
key: "account-manager",
label: t`Account manager`,
free: false,
teams: false,
pro: false,
enterprise: true,
},
{
key: "sla",
label: t`SLA`,
free: false,
teams: false,
pro: false,
enterprise: true,
},
];
const sections: FeatureSection[] = [
{
name: "",
features: planSpecificLimits,
},
{
name: t`Core features`,
features: coreFeatures,
},
{
name: t`Collaboration`,
features: collaborationFeatures,
},
{
name: t`Imports`,
features: importsFeatures,
}, },
{ {
name: t`Platform`, name: t`Platform`,
features: [ features: platformFeatures,
{ },
key: "api", {
label: t`REST API`, name: t`Security`,
kan: true, features: securityFeatures,
trello: true, },
}, {
{ name: t`Support`,
key: "open-source", features: supportFeatures,
label: t`Open source`,
kan: true,
trello: false,
},
{
key: "self-hostable",
label: t`Self-hostable`,
kan: true,
trello: false,
},
{
key: "white-label",
label: t`White label`,
kan: true,
trello: false,
},
{
key: "performance",
label: t`Fast & lightweight`,
kan: true,
trello: false,
},
{
key: "this-is-a-joke",
label: t`Owned by Atlassian`,
kan: false,
trello: true,
},
],
}, },
]; ];
const products = [ const plans = [
{ { id: "free", name: t`Free`, tierId: "tier-free" },
id: "kan", { id: "teams", name: t`Teams`, tierId: "tier-teams" },
name: t`Kan`, { id: "pro", name: t`Pro`, tierId: "tier-pro" },
featured: true, { id: "enterprise", name: t`Enterprise`, tierId: "tier-enterprise" },
},
{
id: "trello",
name: t`Trello`,
featured: false,
},
]; ];
const isHighlightValue = (
value: string | boolean | { text: string; highlight?: string },
): value is { text: string; highlight?: string } => {
return typeof value === "object" && value !== null && "text" in value;
};
const shouldHighlight = (text: string): boolean => {
const lowerText = text.toLowerCase();
return ["unlimited", "multiple", "per-seat", "custom", "mentions", "full", "sso", "admin"].some(
(keyword) => lowerText.includes(keyword),
);
};
const CellValue = ({
value,
label,
}: {
value: string | boolean | { text: string; highlight?: string };
label: string;
}) => {
// Handle object with text and highlight
if (isHighlightValue(value)) {
const { text, highlight } = value;
const isUnlimited = shouldHighlight(text);
let displayText;
if (highlight) {
const parts = text.split(new RegExp(`(${highlight})`, "gi"));
displayText = (
<>
{parts.map((part, index) =>
part.toLowerCase() === highlight.toLowerCase() ? (
<span key={index} className="text-light-1000 dark:text-dark-1000">
{part}
</span>
) : (
<span key={index} className="text-light-950 dark:text-dark-800">
{part}
</span>
),
)}
</>
);
} else {
displayText = text;
}
return (
<div className="flex items-center gap-2.5">
<HiCheckCircle
className={twMerge(
"mt-0.5 h-4 w-4 shrink-0",
isUnlimited
? "text-light-1000 dark:text-dark-1000"
: "text-light-400 dark:text-dark-600",
)}
/>
<span
className={twMerge(
"text-sm font-medium",
isUnlimited
? "text-light-1000 dark:text-dark-950"
: "text-light-950 dark:text-dark-800",
)}
>
{displayText}
</span>
</div>
);
}
if (typeof value === "string") {
const isUnlimited = shouldHighlight(value);
return (
<div className="flex items-center gap-2.5">
<HiCheckCircle
className={twMerge(
"mt-0.5 h-4 w-4 shrink-0",
isUnlimited
? "text-light-1000 dark:text-dark-1000"
: "text-light-400 dark:text-dark-600",
)}
/>
<span
className={twMerge(
"text-sm font-medium",
isUnlimited
? "text-light-1000 dark:text-dark-950"
: "text-light-950 dark:text-dark-800",
)}
>
{value}
</span>
</div>
);
}
if (value) {
// Boolean true - show checkmark icon
return (
<div className="flex items-center gap-2.5">
<HiCheckCircle className="mt-0.5 h-4 w-4 shrink-0 text-light-1000 dark:text-dark-1000" />
<span className="text-sm font-medium text-light-1000 dark:text-dark-950">
{label}
</span>
</div>
);
}
// Boolean false - show cross icon
return (
<div className="flex items-center gap-2.5">
<HiXCircle className="mt-0.5 h-4 w-4 shrink-0 text-light-400 dark:text-dark-600" />
<span className="text-sm font-medium text-light-800 dark:text-dark-800">
{label}
</span>
</div>
);
};
const getFeatureValue = (
feature: PlanFeature,
planId: string,
): string | boolean | { text: string; highlight?: string } => {
switch (planId) {
case "free":
return feature.free;
case "teams":
return feature.teams;
case "pro":
return feature.pro;
case "enterprise":
return feature.enterprise;
default:
return false;
}
};
return ( return (
<section aria-labelledby="comparison-heading" className="w-full px-4"> <section aria-labelledby="comparison-heading" className="w-full">
<div className="mx-auto max-w-6xl py-8 lg:py-10"> <div className="mx-auto max-w-7xl">
<div className="grid grid-cols-3 gap-x-8 border-t border-light-500 before:block dark:border-dark-300"> <div
{products.map((product) => ( ref={containerRef}
<div key={product.id} aria-hidden="true" className="-mt-px"> className="relative overflow-x-auto rounded-lg border border-light-300 bg-light-50 dark:border-dark-300 dark:bg-dark-50"
<div >
className={`${product.featured ? "border-dark-200 dark:border-dark-800" : "border-transparent"} border-t-2 pt-8`} {isHeaderFixed && (
> <div
<p className="hidden sm:block fixed z-40 border-b border-light-300 bg-light-50/95 dark:border-dark-400 dark:bg-dark-50/95"
className={`${product.featured ? "text-dark-50 dark:text-dark-1000" : "text-light-1000 dark:text-dark-1000"} text-center text-sm font-semibold`} style={{
> top: 64,
{product.name} left: headerRect.left,
</p> width: headerRect.width,
}}
>
<div className="grid grid-cols-4 border-x border-light-300 dark:border-dark-300">
{plans.map((plan) => {
const isMostPopular = plan.id === "pro";
return (
<div
key={`fixed-${plan.id}`}
className={twMerge(
"flex items-center px-6 py-3 text-left text-base font-semibold",
isMostPopular
? "bg-light-200 text-light-1000 dark:bg-dark-100 dark:text-dark-1000"
: "bg-light-50 text-dark-50 dark:bg-dark-50 dark:text-dark-1000",
)}
>
{plan.name}
{plan.id === "pro" && <span className="ml-1 text-xl"></span>}
</div>
);
})}
</div> </div>
</div> </div>
))} )}
</div> <table className="w-full border-separate border-spacing-0">
<thead>
<div className="-mt-6 space-y-12"> <tr>
{sections.map((section) => ( {plans.map((plan) => {
<div key={section.name}> const isMostPopular = plan.id === "pro";
<h4 className="text-sm font-semibold text-light-1000 dark:text-dark-1000"> return (
{section.name} <th
</h4> key={plan.id}
<div className="relative -mx-8 mt-8"> scope="col"
<div className={twMerge(
aria-hidden="true" "w-1/4 px-6 py-4 text-left text-base font-semibold",
className="absolute inset-x-8 inset-y-0 grid grid-cols-3 gap-x-8 before:block" isMostPopular
> ? "bg-light-200 text-light-1000 dark:bg-dark-100 dark:text-dark-1000"
{products.map((product) => ( : "bg-light-50 text-dark-50 dark:bg-dark-50 dark:text-dark-1000",
<div )}
key={product.id} >
className={`size-full rounded-lg ${product.featured ? "bg-white shadow-sm dark:bg-dark-50 dark:shadow-none" : ""}`} <span className="flex items-center">
/> {plan.name}
))} {plan.id === "pro" && <span className="ml-1 text-xl"></span>}
</div> </span>
</th>
<table className="relative w-full border-separate border-spacing-x-8"> );
<thead> })}
<tr className="text-left"> </tr>
<th scope="col"> </thead>
<span className="sr-only">{t`Feature`}</span> <tbody>
</th> {sections.map((section, sectionIdx) => (
{products.map((p) => ( <React.Fragment key={section.name || `section-${sectionIdx}`}>
<th key={p.id} scope="col"> {section.name && (
<span className="sr-only">{p.name}</span> <tr>
</th> {plans.map((plan) => {
))} const isPro = plan.id === "pro";
</tr> return (
</thead>
<tbody>
{section.features.map((feature, featureIdx) => (
<tr key={feature.key}>
<th
scope="row"
className="w-1/3 py-3 pr-4 text-left text-sm font-normal text-light-1000 dark:text-dark-1000"
>
{feature.label}
{featureIdx !== section.features.length - 1 ? (
<div className="absolute inset-x-8 mt-3 h-px bg-light-500 dark:bg-dark-300" />
) : null}
</th>
{products.map((p) => (
<td <td
key={p.id} key={plan.id}
className="relative w-1/3 px-4 py-0 text-center" className={twMerge(
"border-t px-6 py-3 text-left text-sm font-semibold text-dark-900 dark:text-dark-900",
isPro
? "border-light-400 bg-light-200 dark:border-dark-400 dark:bg-dark-100"
: "border-light-300 bg-light-50 dark:border-dark-400 dark:bg-dark-50",
)}
> >
<span className="relative inline-flex w-full items-center justify-center py-3"> {plan.id === "free" ? section.name : ""}
<CellValue
value={
p.id === "kan" ? feature.kan : feature.trello
}
/>
</span>
</td> </td>
))} );
</tr> })}
))} </tr>
</tbody> )}
</table> {section.features.map((feature) => (
<tr key={feature.key}>
<div {plans.map((plan) => {
aria-hidden="true" const isPro = plan.id === "pro";
className="pointer-events-none absolute inset-x-8 inset-y-0 grid grid-cols-3 gap-x-8 before:block" return (
> <td
{products.map((product) => ( key={plan.id}
<div className={twMerge(
key={product.id} "w-1/4 border-t px-6 py-3 text-left",
className={`${product.featured ? "ring-1 ring-light-500 dark:ring-dark-800" : "ring-0"} rounded-2xl`} isPro
/> ? "border-light-400 bg-light-200 dark:border-dark-400 dark:bg-dark-100"
: "border-light-300 dark:border-dark-400",
)}
>
<CellValue
value={getFeatureValue(feature, plan.id)}
label={feature.label}
/>
</td>
);
})}
</tr>
))} ))}
</div> </React.Fragment>
</div> ))}
</div> </tbody>
))} </table>
</div> </div>
</div> </div>
</section> </section>

View File

@@ -23,81 +23,115 @@ const Pricing = ({
}) => { }) => {
const tiers = [ const tiers = [
{ {
name: t`Individuals`, name: t`Free`,
id: "tier-individuals", id: "tier-free",
href: "signup", href: "signup",
buttonText: t`Get Started`, buttonText: t`Get started`,
price: { monthly: t`Free`, annually: t`Free` }, price: { monthly: t`$0`, annually: t`$0` },
description: t`Everything you need, free forever. Unlimited boards, unlimited lists, unlimited cards. Upgrade any time.`, description: t`Free for everyone`,
featureHeader: t`Free, forever`, bestFor: t`Best for individuals and solo creators getting started`,
featureHeader: undefined,
features: [ features: [
t`1 user`, { text: t`1 user` },
t`Unlimited boards`, { text: t`Unlimited boards` },
t`Unlimited cards`, { text: t`Unlimited cards` },
t`Custom board templates`, { text: t`Custom board templates` },
t`Checklists`, { text: t`Checklists` },
], ],
showPrice: true, showPrice: true,
ctaSubtext: undefined,
mostPopular: false,
highlighted: false,
}, },
{ {
name: t`Teams`, name: t`Teams`,
id: "tier-teams", id: "tier-teams",
href: "signup", href: "signup",
buttonText: t`Get Started`, buttonText: t`Get started`,
price: { monthly: "$10.00", annually: "$8.00" }, price: { monthly: "$10", annually: "$8" },
description: t`Kan is better with a team. Perfect for small and growing teams looking to collaborate.`, description: t`Perfect for small and growing teams looking to collaborate.`,
featureHeader: t`Everything in the free plan, plus:`, bestFor: t`Best for small and growing teams that need collaboration`,
featureHeader: undefined,
features: [ features: [
t`Workspace members`, { text: t`All Free features +` },
t`Admin roles`, { text: t`Workspace members` },
t`Priority email support`, { text: t`Invite links` },
t`Support the development of the project`, { text: t`Unlimited file uploads` },
{ text: t`Email notifications for mentions` },
{ text: t`Priority email support` },
],
highlighted: false,
showPrice: true,
showPriceSuffix: true,
ctaSubtext: t`14 day free trial · Switch plans or cancel anytime`,
mostPopular: false,
},
{
name: t`Pro`,
id: "tier-pro",
href: "signup",
buttonText: t`Get started`,
price: { monthly: "$29", annually: "$24" },
description: t`Unlimited seats and advanced features for growing teams.`,
bestFor: t`Best for teams that want to scale without limits`,
featureHeader: undefined,
features: [
{ text: t`All Teams features +` },
{ text: t`Unlimited members` },
{ text: t`Custom workspace username` },
{ text: t`Configurable user roles and permissions` },
{ text: t`Custom branding` },
], ],
highlighted: true, highlighted: true,
showPrice: true, showPrice: true,
showPriceSuffix: true, showPriceSuffix: false,
ctaSubtext: t`14 day free trial · Switch plans or cancel anytime`,
mostPopular: true,
}, },
{ {
name: t`Self Host`, name: t`Enterprise`,
id: "tier-self-host", id: "tier-enterprise",
href: "https://github.com/kanbn/kan", href: "mailto:support@kan.bn?subject=Enterprise Inquiry",
buttonText: t`View docs`, buttonText: t`Contact Sales`,
price: { monthly: "-", annually: "-" }, price: { monthly: t`Contact us`, annually: t`Contact us` },
description: t`Host Kan on your own infrastructure. Ideal for organisations that need complete control over their data.`, description: t`Advanced security, compliance, and dedicated support for large organizations.`,
featureHeader: t`Complete control and ownership:`, bestFor: t`Best for large organizations with advanced needs`,
featureHeader: undefined,
features: [ features: [
t`Run on your own infrastructure`, { text: t`All Pro features +` },
t`Own your data`, { text: t`SAML SSO` },
t`Custom domain`, { text: t`Advanced admin controls` },
{ text: t`Dedicated account manager` },
{ text: t`Custom SLA` },
{ text: t`On-premise deployment option` },
], ],
highlighted: false,
showPrice: true,
showPriceSuffix: false,
ctaSubtext: undefined,
mostPopular: false, mostPopular: false,
showPrice: false,
}, },
]; ];
return ( return (
<> <>
<div className="flex flex-col items-center justify-center px-4 pb-10"> <div className="mx-auto max-w-7xl px-4">
<div className="mt-14 flex flex-col items-center justify-center"> <div className="mb-8 flex items-center justify-center">
<div className="mb-8 flex items-center gap-2 rounded-full border bg-white px-4 py-1.5 text-center text-xs font-bold text-gray-800 dark:border-dark-300 dark:bg-dark-1000 dark:text-gray-800 lg:text-sm">
<HiBolt />
<p>{t`Launch offer: unlimited seats for just $29/month with Pro`}</p>
</div>
<fieldset aria-label={t`Payment frequency`}> <fieldset aria-label={t`Payment frequency`}>
<RadioGroup <RadioGroup
value={frequency} value={frequency}
onChange={(value) => setFrequency(value)} onChange={(value) => setFrequency(value)}
className="grid grid-cols-2 gap-x-1 rounded-full p-1 text-center text-xs/5 font-semibold ring-1 ring-inset ring-light-600 dark:ring-dark-600" className="flex gap-1 rounded-lg border border-light-300 bg-light-50 p-1 dark:border-dark-300 dark:bg-dark-50"
> >
{frequencies.map((option) => ( {frequencies.map((option) => (
<Radio <Radio
key={option.value} key={option.value}
value={option} value={option}
className={twMerge( className={twMerge(
"cursor-pointer rounded-full px-2.5 py-1 text-xs transition-colors lg:text-sm", "cursor-pointer rounded-md px-4 py-1.5 text-sm font-medium transition-colors",
frequency?.value === option.value frequency?.value === option.value
? "bg-dark-50 text-white dark:bg-light-50 dark:text-dark-50" ? "bg-white text-light-1000 shadow-sm dark:bg-dark-100 dark:text-dark-1000"
: "text-light-900 hover:bg-light-100 dark:hover:bg-dark-100", : "text-light-600 dark:text-dark-700",
)} )}
> >
{option.label} {option.label}
@@ -106,103 +140,129 @@ const Pricing = ({
</RadioGroup> </RadioGroup>
</fieldset> </fieldset>
</div> </div>
</div>
<div className="isolate mx-auto mb-10 grid max-w-md grid-cols-1 gap-8 px-4 lg:mx-0 lg:max-w-none lg:grid-cols-3"> <div className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4">
{tiers.map((tier) => ( {tiers.map((tier) => (
<div <div
key={tier.id} key={tier.id}
className={twMerge( className={twMerge(
tier.highlighted "relative flex h-full flex-col rounded-lg border p-6",
? "bg-dark-50 ring-1 ring-dark-50 dark:ring-dark-800" tier.mostPopular
: "bg-light-50 ring-1 ring-light-300 dark:bg-dark-50 dark:ring-dark-300", ? "border-light-400 bg-light-200 dark:border-dark-400 dark:bg-dark-100"
"rounded-3xl p-8 xl:p-10", : "border-light-300 bg-light-50 dark:border-dark-300 dark:bg-dark-50",
)} )}
> >
<div className="flex items-center justify-between gap-x-4"> {tier.id === "tier-pro" && (
<h3 <div className="absolute right-2 top-2 z-10">
id={tier.id} <span className="inline-flex items-center gap-1 rounded-full border border-light-400 bg-light-100 px-3 py-1 text-center text-xs text-dark-600 dark:border-dark-400 dark:bg-dark-100 dark:text-dark-900">
className={twMerge( <HiBolt className="h-3 w-3 -ml-0.5" />
tier.highlighted {t`Launch offer`}
? "text-dark-1000 dark:text-dark-1000" </span>
: "text-dark-50 dark:text-dark-1000", </div>
"text-lg/8 font-semibold", )}
<h3
id={tier.id}
className={twMerge(
"mb-4 flex items-center text-base font-semibold",
tier.mostPopular
? "text-light-1000 dark:text-dark-1000"
: "text-light-1000 dark:text-dark-1000",
)}
>
{tier.name}
{tier.id === "tier-pro" && <span className="ml-1 text-xl leading-none"></span>}
</h3>
<div className="mb-4">
<p className="flex items-baseline gap-x-1">
<span
className={twMerge(
"text-2xl font-semibold",
tier.mostPopular
? "text-light-1000 dark:text-dark-1000"
: "text-light-1000 dark:text-dark-1000",
!tier.showPrice && "opacity-0",
)}
>
{tier.price[frequency?.value ?? "monthly"]}
</span>
{tier.id === "tier-pro" && (
<span
className={twMerge(
"text-2xl font-semibold line-through",
tier.mostPopular
? "text-light-600 dark:text-dark-600"
: "text-light-600 dark:text-dark-600",
)}
>
{frequency?.value === "annually" ? "$79" : "$100"}
</span>
)} )}
> {tier.showPriceSuffix && (
{tier.name} <span className="text-sm font-normal text-dark-400 dark:text-dark-700">
</h3> {frequency?.priceSuffix}
{tier.highlighted && ( </span>
<p className="rounded-full bg-light-50 px-2.5 py-1 text-[12px] font-semibold text-dark-500 dark:bg-dark-1000 dark:text-dark-50"> )}
{t`14 day free trial`} {tier.id === "tier-pro" && (
</p> <span className="text-sm font-normal text-dark-400 dark:text-dark-700">
{t`/month`}
</span>
)}
</p>
</div>
{tier.bestFor && (
<p className="mb-6 text-sm text-dark-400 dark:text-dark-700">
{tier.bestFor}
</p>
)}
<div className="mb-6">
{tier.id === "tier-enterprise" ? (
<a
href={tier.href}
aria-describedby={tier.id}
className="block w-full rounded-md border border-dark-200 bg-transparent px-4 py-2 text-center text-sm font-medium text-dark-50 transition-colors hover:bg-dark-50 hover:text-light-50 dark:border-dark-300 dark:text-dark-1000 dark:hover:bg-dark-200"
>
{tier.buttonText}
</a>
) : (
<>
<Link
href={tier.href}
aria-describedby={tier.id}
className={twMerge(
"block w-full rounded-md px-4 py-2 text-center text-sm font-medium transition-colors",
tier.mostPopular
? "bg-dark-50 text-light-50 shadow-sm hover:bg-dark-100 dark:bg-dark-1000 dark:text-dark-50 dark:hover:bg-dark-900"
: "border border-dark-200 bg-transparent text-dark-50 transition-colors hover:bg-dark-50 hover:text-light-50 dark:border-dark-300 dark:text-dark-1000 dark:hover:bg-dark-200",
)}
>
{tier.buttonText}
</Link>
</>
)} )}
</div> </div>
<p <ul role="list" className="space-y-2.5">
className={twMerge( {tier.features.map((feature, index) => (
"mt-4 text-sm/6 text-dark-950 dark:text-dark-900", <li
tier.highlighted ? "text-light-100" : "text-dark-50", key={`${tier.id}-feature-${index}`}
)} className="flex items-start gap-x-2.5"
> >
{tier.description} <HiCheckCircle
</p> className="mt-0.5 h-4 w-4 shrink-0 text-light-1000 dark:text-dark-1000"
<p className="mt-6 flex items-baseline gap-x-1"> />
<span <span className="text-sm text-dark-600 dark:text-dark-800">
className={twMerge( {feature.text}
"text-3xl font-semibold tracking-tight text-light-100", </span>
tier.highlighted
? "text-light-50 dark:text-dark-1000"
: "text-gray-900 dark:text-dark-1000",
!tier.showPrice && "opacity-0",
)}
>
{tier.price[frequency?.value ?? "monthly"]}
</span>
{tier.showPriceSuffix && (
<span className="text-sm/6 font-semibold text-light-50 dark:text-dark-900">
{frequency?.priceSuffix}
</span>
)}
</p>
<Link
href={tier.href}
aria-describedby={tier.id}
className={twMerge(
tier.highlighted
? "bg-light-50 text-dark-50 shadow-sm dark:bg-dark-1000 dark:text-dark-50"
: "bg-dark-50 text-light-50 dark:bg-dark-200 dark:text-dark-1000",
"mt-6 block rounded-md px-3 py-2 text-center text-sm/6 font-semibold focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600",
)}
>
{tier.buttonText}
</Link>
<p
className={twMerge(
"mt-8 text-sm/6 font-bold",
tier.highlighted
? "text-light-100 dark:text-dark-1000"
: "text-dark-50 dark:text-dark-1000",
)}
>
{tier.featureHeader}
</p>
<ul
role="list"
className={twMerge(
"mt-2 space-y-3 text-sm/6 text-light-600",
tier.highlighted
? "text-light-100 dark:text-dark-1000"
: "text-dark-50 dark:text-dark-1000",
)}
>
{tier.features.map((feature) => (
<li key={feature} className="flex items-center gap-x-3">
<HiCheckCircle className="h-5 w-5" />
{feature}
</li> </li>
))} ))}
</ul> </ul>
</div> </div>
))} ))}
</div>
<div className="mx-auto mt-8 max-w-3xl px-4 text-center">
<div className="inline-flex items-center gap-2 rounded-full border bg-light-50 px-4 py-1 text-center text-xs text-light-1000 dark:border-dark-300 dark:bg-dark-50 dark:text-dark-900">
<p>{t`14 day free trial · Switch plans or cancel anytime`}</p>
</div>
</div>
</div> </div>
</> </>
); );

View File

@@ -43,8 +43,8 @@ export default function PricingView() {
<p className="mt-4 text-center text-3xl font-bold text-light-1000 dark:text-dark-1000 lg:text-5xl"> <p className="mt-4 text-center text-3xl font-bold text-light-1000 dark:text-dark-1000 lg:text-5xl">
{t`Simple pricing`} {t`Simple pricing`}
</p> </p>
<p className="text:md lg:text-md mt-6 max-w-[500px] text-center text-light-950 dark:text-dark-900"> <p className="text:md lg:text-md mt-6 max-w-[300px] text-center text-light-950 dark:text-dark-900">
{t`Get started for free, with no usage limits. For collaboration, upgrade to a plan that fits the size of your team.`} {t`Start free. Upgrade as your team grows. No hidden fees, no contracts.`}
</p> </p>
</div> </div>
<PricingTiers <PricingTiers
@@ -59,23 +59,9 @@ export default function PricingView() {
</div> </div>
<div className="pb-22 flex flex-col items-center justify-center px-4"> <div className="pb-22 flex flex-col items-center justify-center px-4">
<div className="flex items-center gap-2 rounded-full border bg-light-50 px-4 py-1 text-center text-xs text-light-1000 dark:border-dark-300 dark:bg-dark-50 dark:text-dark-900 lg:text-sm">
<p>{t`Features`}</p>
</div>
<p className="mt-4 text-center text-3xl font-bold text-light-1000 dark:text-dark-1000 lg:text-4xl"> <div className="mt-10 w-full">
{t`Feature breakdown`}
</p>
<p className="text-md lg:text-md my-4 max-w-[500px] text-center text-light-950 dark:text-dark-900">
{t`Compare our features to see why Kan is the best choice.`}
</p>
<div className="mt-2">
<Button variant="primary" href="/signup">
{t`Get started`}
</Button>
</div>
<div className="mt-10 w-full max-w-[900px]">
<FeatureComparisonTable <FeatureComparisonTable
frequencyValue={frequency?.value ?? "annually"} frequencyValue={frequency?.value ?? "annually"}
/> />