import { t } from "@lingui/core/macro"; import React, { useEffect, useRef, useState } from "react"; import { HiCheckCircle, HiXCircle } from "react-icons/hi2"; import { twMerge } from "tailwind-merge"; type FrequencyValue = "monthly" | "annually"; interface PlanFeature { key: string; label: string; free: string | boolean | { text: string; highlight?: boolean; highlightFirstWord?: boolean }; teams: string | boolean | { text: string; highlight?: boolean; highlightFirstWord?: boolean }; pro: string | boolean | { text: string; highlight?: boolean; highlightFirstWord?: boolean }; enterprise: string | boolean | { text: string; highlight?: boolean; highlightFirstWord?: boolean }; } interface FeatureSection { name: string; features: PlanFeature[]; } const FeatureComparisonTable = ({ frequencyValue: _frequencyValue, }: { frequencyValue: FrequencyValue; }) => { // Keep pricing column headers visible as you scroll const containerRef = useRef(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[] = [ { key: "boards", label: t`Boards`, free: { text: t`Unlimited boards`, highlightFirstWord: true }, teams: { text: t`Unlimited boards`, highlightFirstWord: true }, pro: { text: t`Unlimited boards`, highlightFirstWord: true }, enterprise: { text: t`Unlimited boards`, highlightFirstWord: true }, }, { key: "members", label: t`Members`, free: { text: t`1 user`, highlightFirstWord: true }, teams: { text: t`Per-seat pricing`, highlightFirstWord: true }, pro: { text: t`Unlimited members`, highlightFirstWord: true }, enterprise: { text: t`Unlimited members`, highlightFirstWord: true }, }, { key: "file-uploads", label: t`File uploads`, free: { text: t`10mb file uploads`, highlightFirstWord: true }, teams: { text: t`Unlimited file uploads`, highlightFirstWord: true }, pro: { text: t`Unlimited file uploads`, highlightFirstWord: true }, enterprise: { text: t`Unlimited file uploads`, highlightFirstWord: true }, }, { key: "workspace-username", label: t`Workspace username`, free: { text: t`Default username`, highlightFirstWord: true }, teams: { text: t`Default username`, highlightFirstWord: true }, pro: { text: t`Custom username`, highlightFirstWord: true }, enterprise: { text: t`Custom username`, highlightFirstWord: true }, }, ]; const coreFeatures: PlanFeature[] = [ { key: "checklists", label: t`Checklists`, free: 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`, highlight: true }, pro: { text: t`Full API access`, highlight: true }, enterprise: { text: t`Full API access`, highlight: true }, }, { 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`, highlight: true }, teams: { text: t`Google SSO`, highlight: true }, pro: { text: t`Google SSO`, highlight: true }, enterprise: { text: t`Google SSO + SAML`, highlight: true }, }, { key: "admin-roles", label: t`Admin roles`, free: { text: t`Admin roles`, highlight: true }, teams: { text: t`Admin roles`, highlight: true }, pro: { text: t`Admin roles`, highlight: true }, enterprise: { text: t`Advanced admin roles`, highlight: true }, }, ]; 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`, features: platformFeatures, }, { name: t`Security`, features: securityFeatures, }, { name: t`Support`, features: supportFeatures, }, ]; const plans = [ { id: "free", name: t`Free`, tierId: "tier-free" }, { id: "teams", name: t`Teams`, tierId: "tier-teams" }, { id: "pro", name: t`Pro`, tierId: "tier-pro" }, { id: "enterprise", name: t`Enterprise`, tierId: "tier-enterprise" }, ]; const CellValue = ({ value, label, }: { value: string | boolean | { text: string; highlight?: boolean; highlightFirstWord?: boolean }; label: string; }) => { if (typeof value === "object" && value !== null) { const { text, highlight, highlightFirstWord } = value; if (highlightFirstWord) { const spaceIndex = text.indexOf(" "); const firstWord = spaceIndex === -1 ? text : text.slice(0, spaceIndex); const rest = spaceIndex === -1 ? "" : text.slice(spaceIndex); return (
{firstWord} {rest}
); } return (
{text}
); } if (value) { return (
{label}
); } return (
{label}
); }; const getFeatureValue = ( feature: PlanFeature, planId: string, ): string | boolean | { text: string; highlight?: boolean; highlightFirstWord?: boolean } => { 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 (
{isHeaderFixed && (
{plans.map((plan) => { const isMostPopular = plan.id === "pro"; return (
{plan.name} {plan.id === "pro" && }
); })}
)} {plans.map((plan) => { const isMostPopular = plan.id === "pro"; return ( ); })} {sections.map((section, sectionIdx) => ( {section.name && ( {plans.map((plan) => { const isPro = plan.id === "pro"; return ( ); })} )} {section.features.map((feature) => ( {plans.map((plan) => { const isPro = plan.id === "pro"; return ( ); })} ))} ))}
{plan.name} {plan.id === "pro" && }
{plan.id === "free" ? section.name : ""}
); }; export default FeatureComparisonTable;