fix: show correct highlighting in pricing comparison table

This commit is contained in:
Henry
2026-03-03 22:31:05 +00:00
parent 2c5195a2b8
commit a6780e3e28

View File

@@ -8,10 +8,10 @@ type FrequencyValue = "monthly" | "annually";
interface PlanFeature { interface PlanFeature {
key: string; key: string;
label: string; label: string;
free: string | boolean | { text: string; highlight?: string }; free: string | boolean | { text: string; highlight?: boolean; highlightFirstWord?: boolean };
teams: string | boolean | { text: string; highlight?: string }; teams: string | boolean | { text: string; highlight?: boolean; highlightFirstWord?: boolean };
pro: string | boolean | { text: string; highlight?: string }; pro: string | boolean | { text: string; highlight?: boolean; highlightFirstWord?: boolean };
enterprise: string | boolean | { text: string; highlight?: string }; enterprise: string | boolean | { text: string; highlight?: boolean; highlightFirstWord?: boolean };
} }
interface FeatureSection { interface FeatureSection {
@@ -71,34 +71,34 @@ const FeatureComparisonTable = ({
{ {
key: "boards", key: "boards",
label: t`Boards`, label: t`Boards`,
free: { text: t`Unlimited boards`, highlight: "Unlimited" }, free: { text: t`Unlimited boards`, highlightFirstWord: true },
teams: { text: t`Unlimited boards`, highlight: "Unlimited" }, teams: { text: t`Unlimited boards`, highlightFirstWord: true },
pro: { text: t`Unlimited boards`, highlight: "Unlimited" }, pro: { text: t`Unlimited boards`, highlightFirstWord: true },
enterprise: { text: t`Unlimited boards`, highlight: "Unlimited" }, enterprise: { text: t`Unlimited boards`, highlightFirstWord: true },
}, },
{ {
key: "members", key: "members",
label: t`Members`, label: t`Members`,
free: { text: t`1 user`, highlight: "1" }, free: { text: t`1 user`, highlightFirstWord: true },
teams: { text: t`Per-seat pricing`, highlight: "Per-seat" }, teams: { text: t`Per-seat pricing`, highlightFirstWord: true },
pro: { text: t`Unlimited members`, highlight: "Unlimited" }, pro: { text: t`Unlimited members`, highlightFirstWord: true },
enterprise: { text: t`Unlimited members`, highlight: "Unlimited" }, enterprise: { text: t`Unlimited members`, highlightFirstWord: true },
}, },
{ {
key: "file-uploads", key: "file-uploads",
label: t`File uploads`, label: t`File uploads`,
free: { text: t`10mb file uploads`, highlight: "10mb" }, free: { text: t`10mb file uploads`, highlightFirstWord: true },
teams: { text: t`Unlimited file uploads`, highlight: "Unlimited" }, teams: { text: t`Unlimited file uploads`, highlightFirstWord: true },
pro: { text: t`Unlimited file uploads`, highlight: "Unlimited" }, pro: { text: t`Unlimited file uploads`, highlightFirstWord: true },
enterprise: { text: t`Unlimited file uploads`, highlight: "Unlimited" }, enterprise: { text: t`Unlimited file uploads`, highlightFirstWord: true },
}, },
{ {
key: "workspace-username", key: "workspace-username",
label: t`Workspace username`, label: t`Workspace username`,
free: { text: t`Default username`, highlight: "Default" }, free: { text: t`Default username`, highlightFirstWord: true },
teams: { text: t`Default username`, highlight: "Default" }, teams: { text: t`Default username`, highlightFirstWord: true },
pro: { text: t`Custom username`, highlight: "Custom" }, pro: { text: t`Custom username`, highlightFirstWord: true },
enterprise: { text: t`Custom username`, highlight: "Custom" }, enterprise: { text: t`Custom username`, highlightFirstWord: true },
}, },
]; ];
@@ -236,9 +236,9 @@ const FeatureComparisonTable = ({
key: "rest-api", key: "rest-api",
label: t`API`, label: t`API`,
free: { text: t`Limited API access` }, free: { text: t`Limited API access` },
teams: { text: t`Full API access` }, teams: { text: t`Full API access`, highlight: true },
pro: { text: t`Full API access` }, pro: { text: t`Full API access`, highlight: true },
enterprise: { text: t`Full API access` }, enterprise: { text: t`Full API access`, highlight: true },
}, },
{ {
key: "self-hostable", key: "self-hostable",
@@ -254,18 +254,18 @@ const FeatureComparisonTable = ({
{ {
key: "sso", key: "sso",
label: t`SSO`, label: t`SSO`,
free: { text: t`Google SSO` }, free: { text: t`Google SSO`, highlight: true },
teams: { text: t`Google SSO` }, teams: { text: t`Google SSO`, highlight: true },
pro: { text: t`Google SSO` }, pro: { text: t`Google SSO`, highlight: true },
enterprise: { text: t`Google SSO + SAML` }, enterprise: { text: t`Google SSO + SAML`, highlight: true },
}, },
{ {
key: "admin-roles", key: "admin-roles",
label: t`Admin roles`, label: t`Admin roles`,
free: { text: t`Admin roles` }, free: { text: t`Admin roles`, highlight: true },
teams: { text: t`Admin roles` }, teams: { text: t`Admin roles`, highlight: true },
pro: { text: t`Admin roles` }, pro: { text: t`Admin roles`, highlight: true },
enterprise: { text: t`Advanced admin roles` }, enterprise: { text: t`Advanced admin roles`, highlight: true },
}, },
]; ];
@@ -334,51 +334,29 @@ const FeatureComparisonTable = ({
{ id: "enterprise", name: t`Enterprise`, tierId: "tier-enterprise" }, { id: "enterprise", name: t`Enterprise`, tierId: "tier-enterprise" },
]; ];
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 = ({ const CellValue = ({
value, value,
label, label,
}: { }: {
value: string | boolean | { text: string; highlight?: string }; value: string | boolean | { text: string; highlight?: boolean; highlightFirstWord?: boolean };
label: string; label: string;
}) => { }) => {
// Handle object with text and highlight if (typeof value === "object" && value !== null) {
if (isHighlightValue(value)) { const { text, highlight, highlightFirstWord } = value;
const { text, highlight } = value;
const isUnlimited = shouldHighlight(text);
let displayText; if (highlightFirstWord) {
if (highlight) { const spaceIndex = text.indexOf(" ");
const parts = text.split(new RegExp(`(${highlight})`, "gi")); const firstWord = spaceIndex === -1 ? text : text.slice(0, spaceIndex);
displayText = ( const rest = spaceIndex === -1 ? "" : text.slice(spaceIndex);
<> return (
{parts.map((part, index) => <div className="flex items-center gap-2.5">
part.toLowerCase() === highlight.toLowerCase() ? ( <HiCheckCircle className="mt-0.5 h-4 w-4 shrink-0 text-light-1000 dark:text-dark-1000" />
<span key={index} className="text-light-1000 dark:text-dark-1000"> <span className="text-sm font-medium">
{part} <span className="text-light-1000 dark:text-dark-1000">{firstWord}</span>
</span> <span className="text-light-950 dark:text-dark-800">{rest}</span>
) : ( </span>
<span key={index} className="text-light-950 dark:text-dark-800"> </div>
{part}
</span>
),
)}
</>
); );
} else {
displayText = text;
} }
return ( return (
@@ -386,7 +364,7 @@ const FeatureComparisonTable = ({
<HiCheckCircle <HiCheckCircle
className={twMerge( className={twMerge(
"mt-0.5 h-4 w-4 shrink-0", "mt-0.5 h-4 w-4 shrink-0",
isUnlimited highlight
? "text-light-1000 dark:text-dark-1000" ? "text-light-1000 dark:text-dark-1000"
: "text-light-400 dark:text-dark-600", : "text-light-400 dark:text-dark-600",
)} )}
@@ -394,44 +372,18 @@ const FeatureComparisonTable = ({
<span <span
className={twMerge( className={twMerge(
"text-sm font-medium", "text-sm font-medium",
isUnlimited highlight
? "text-light-1000 dark:text-dark-950" ? "text-light-1000 dark:text-dark-950"
: "text-light-950 dark:text-dark-800", : "text-light-950 dark:text-dark-800",
)} )}
> >
{displayText} {text}
</span> </span>
</div> </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) { if (value) {
// Boolean true - show checkmark icon
return ( return (
<div className="flex items-center gap-2.5"> <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" /> <HiCheckCircle className="mt-0.5 h-4 w-4 shrink-0 text-light-1000 dark:text-dark-1000" />
@@ -441,7 +393,7 @@ const FeatureComparisonTable = ({
</div> </div>
); );
} }
// Boolean false - show cross icon
return ( return (
<div className="flex items-center gap-2.5"> <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" /> <HiXCircle className="mt-0.5 h-4 w-4 shrink-0 text-light-400 dark:text-dark-600" />
@@ -455,7 +407,7 @@ const FeatureComparisonTable = ({
const getFeatureValue = ( const getFeatureValue = (
feature: PlanFeature, feature: PlanFeature,
planId: string, planId: string,
): string | boolean | { text: string; highlight?: string } => { ): string | boolean | { text: string; highlight?: boolean; highlightFirstWord?: boolean } => {
switch (planId) { switch (planId) {
case "free": case "free":
return feature.free; return feature.free;