feat: extend feature comparison table

This commit is contained in:
Henry
2025-10-15 21:52:56 +01:00
parent ad2f7197bb
commit 6c90de2b00
4 changed files with 370 additions and 25 deletions

View File

@@ -7,7 +7,6 @@ import { IoLogoGithub } from "react-icons/io";
import Button from "~/components/Button";
import { PageHead } from "~/components/PageHead";
import Pricing from "../pricing/components/Pricing";
import Cta from "./components/Cta";
import FAQs from "./components/Faqs";
import Features from "./components/Features";
@@ -16,8 +15,6 @@ import Layout from "./components/Layout";
export default function HomeView() {
const { resolvedTheme } = useTheme();
const isDarkMode = resolvedTheme === "dark";
return (
<Layout>
<PageHead title="Kan.bn | The open source alternative to Trello" />

View File

@@ -0,0 +1,297 @@
import { t } from "@lingui/core/macro";
import { HiCheckCircle, HiXCircle } from "react-icons/hi2";
interface Feature {
key: string;
label: string;
kan: boolean | string;
trello: boolean | string;
}
interface Section {
name: string;
features: Feature[];
}
type FrequencyValue = "monthly" | "annually";
const CellValue = ({ value }: { value: boolean | string }) => {
if (typeof value === "string") {
return (
<span className="text-sm text-dark-900 dark:text-dark-900">{value}</span>
);
}
return value ? (
<HiCheckCircle className="h-5 w-5 text-dark-900 dark:text-dark-1000" />
) : (
<HiXCircle className="h-5 w-5 text-light-600 dark:text-dark-600" />
);
};
const FeatureComparisonTable = ({
frequencyValue,
}: {
frequencyValue: FrequencyValue;
}) => {
const sections: Section[] = [
{
name: t`Core features`,
features: [
{
key: "ulimited-workspaces",
label: t`Unlimited workspaces`,
kan: true,
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: false },
{ key: "checklists", label: t`Checklists`, kan: true, trello: true },
{
key: "import",
label: t`Import from Trello`,
kan: true,
trello: true,
},
{
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,
},
{
key: "this-is-a-joke",
label: t`Owned by Atlassian`,
kan: false,
trello: true,
},
],
},
{
name: t`Teams`,
features: [
{
key: "pricing",
label: t`Price per user/month`,
kan: frequencyValue === "monthly" ? `$10.00` : `$8.00`,
trello: frequencyValue === "monthly" ? `$12.00` : `$10.00`,
},
{
key: "comments",
label: t`Comments & mentions`,
kan: true,
trello: true,
},
{
key: "assignments",
label: t`Assignees`,
kan: true,
trello: true,
},
{
key: "sharing",
label: t`Board sharing & invites`,
kan: true,
trello: true,
},
{
key: "invite-links",
label: t`Invite links`,
kan: true,
trello: true,
},
],
},
{
name: t`Platform`,
features: [
{
key: "api",
label: t`REST API`,
kan: true,
trello: true,
},
{
key: "open-source",
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,
},
],
},
];
const products = [
{
id: "kan",
name: t`Kan`,
featured: true,
},
{
id: "trello",
name: t`Trello`,
featured: false,
},
];
return (
<section aria-labelledby="comparison-heading" className="w-full px-4">
<div className="mx-auto max-w-6xl py-8 lg:py-10">
<div className="grid grid-cols-3 gap-x-8 border-t border-light-300 before:block dark:border-dark-300">
{products.map((product) => (
<div key={product.id} aria-hidden="true" className="-mt-px">
<div
className={`${product.featured ? "border-dark-200 dark:border-dark-800" : "border-transparent"} border-t-2 pt-8`}
>
<p
className={`${product.featured ? "text-dark-50 dark:text-dark-1000" : "text-light-1000 dark:text-dark-1000"} text-center text-sm font-semibold`}
>
{product.name}
</p>
</div>
</div>
))}
</div>
<div className="-mt-6 space-y-12">
{sections.map((section) => (
<div key={section.name}>
<h4 className="text-sm font-semibold text-light-1000 dark:text-dark-1000">
{section.name}
</h4>
<div className="relative -mx-8 mt-8">
<div
aria-hidden="true"
className="absolute inset-x-8 inset-y-0 grid grid-cols-3 gap-x-8 before:block"
>
{products.map((product) => (
<div
key={product.id}
className={`size-full rounded-lg ${product.featured ? "bg-white shadow-sm dark:bg-dark-50 dark:shadow-none" : ""}`}
/>
))}
</div>
<table className="relative w-full border-separate border-spacing-x-8">
<thead>
<tr className="text-left">
<th scope="col">
<span className="sr-only">{t`Feature`}</span>
</th>
{products.map((p) => (
<th key={p.id} scope="col">
<span className="sr-only">{p.name}</span>
</th>
))}
</tr>
</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-300 dark:bg-dark-300" />
) : null}
</th>
{products.map((p) => (
<td
key={p.id}
className="relative w-1/3 px-4 py-0 text-center"
>
<span className="relative inline-flex w-full items-center justify-center py-3">
<CellValue
value={
p.id === "kan" ? feature.kan : feature.trello
}
/>
</span>
</td>
))}
</tr>
))}
</tbody>
</table>
<div
aria-hidden="true"
className="pointer-events-none absolute inset-x-8 inset-y-0 grid grid-cols-3 gap-x-8 before:block"
>
{products.map((product) => (
<div
key={product.id}
className={`${product.featured ? "ring-1 ring-dark-200 dark:ring-dark-800" : "ring-0"} rounded-lg`}
/>
))}
</div>
</div>
</div>
))}
</div>
</div>
</section>
);
};
export default FeatureComparisonTable;

View File

@@ -1,28 +1,26 @@
import Link from "next/link";
import { Radio, RadioGroup } from "@headlessui/react";
import { t } from "@lingui/core/macro";
import { useState } from "react";
import { HiBolt, HiCheckCircle } from "react-icons/hi2";
import { twMerge } from "tailwind-merge";
type Frequency = "monthly" | "annually";
type FrequencyValue = "monthly" | "annually";
const Pricing = () => {
const frequencies = [
{
value: "monthly" as Frequency,
label: t`Monthly`,
priceSuffix: t`per user/month`,
},
{
value: "annually" as Frequency,
label: t`Yearly`,
priceSuffix: t`per user/month`,
},
];
const [frequency, setFrequency] = useState(frequencies[1]);
interface Frequency {
value: FrequencyValue;
label: string;
priceSuffix: string;
}
const Pricing = ({
frequency,
frequencies,
setFrequency,
}: {
frequency: Frequency | undefined;
frequencies: Frequency[];
setFrequency: (frequency: Frequency) => void;
}) => {
const tiers = [
{
name: t`Individuals`,
@@ -35,10 +33,9 @@ const Pricing = () => {
features: [
t`1 user`,
t`Unlimited boards`,
t`Unlimited lists`,
t`Unlimited cards`,
t`Unlimited comments`,
t`Unlimited activity log`,
t`Custom board templates`,
t`Checklists`,
],
showPrice: true,
},
@@ -48,7 +45,7 @@ const Pricing = () => {
href: "signup",
buttonText: t`Get Started`,
price: { monthly: "$10.00", annually: "$8.00" },
description: t`Kanban is better with a team. Perfect for small and growing teams looking to collaborate.`,
description: t`Kan is better with a team. Perfect for small and growing teams looking to collaborate.`,
featureHeader: t`Everything in the free plan, plus:`,
features: [
t`Workspace members`,

View File

@@ -1,10 +1,33 @@
import { t } from "@lingui/core/macro";
import { useTheme } from "next-themes";
import { useState } from "react";
import Button from "~/components/Button";
import { PageHead } from "~/components/PageHead";
import Cta from "../home/components/Cta";
import Layout from "../home/components/Layout";
import FeatureComparisonTable from "./components/FeatureComparisonTable";
import PricingTiers from "./components/PricingTiers";
type FrequencyValue = "monthly" | "annually";
export default function PricingView() {
const { resolvedTheme } = useTheme();
const frequencies = [
{
value: "monthly" as FrequencyValue,
label: t`Monthly`,
priceSuffix: t`per user/month`,
},
{
value: "annually" as FrequencyValue,
label: t`Yearly`,
priceSuffix: t`per user/month`,
},
];
const [frequency, setFrequency] = useState(frequencies[1]);
return (
<Layout>
<PageHead title={`${t`Pricing`} | kan.bn`} />
@@ -23,7 +46,38 @@ export default function PricingView() {
{t`Get started for free, with no usage limits. For collaboration, upgrade to a plan that fits the size of your team.`}
</p>
</div>
<PricingTiers />
<PricingTiers
frequency={frequency}
frequencies={frequencies}
setFrequency={setFrequency}
/>
</div>
<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">
{t`Feature breakdown`}
</p>
<p className="text-md lg:text-md my-4 max-w-[500px] text-center 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
frequencyValue={frequency?.value ?? "annually"}
/>
</div>
</div>
<div className="relative">
<Cta theme={resolvedTheme ?? "light"} />
</div>
</div>
</Layout>