From 34c79b3498962da316d378efe4e72199f9a02a37 Mon Sep 17 00:00:00 2001 From: farazcsk Date: Fri, 5 Sep 2025 14:24:45 +0100 Subject: [PATCH] feat: polish pricing page --- .../src/components/product-tier-badge.tsx | 89 +++++++++--- client/dashboard/src/components/ui/button.tsx | 8 +- .../dashboard/src/pages/billing/Billing.tsx | 128 ++++++++++++++---- 3 files changed, 173 insertions(+), 52 deletions(-) diff --git a/client/dashboard/src/components/product-tier-badge.tsx b/client/dashboard/src/components/product-tier-badge.tsx index 300222b60..726b84e05 100644 --- a/client/dashboard/src/components/product-tier-badge.tsx +++ b/client/dashboard/src/components/product-tier-badge.tsx @@ -1,8 +1,44 @@ +import { cva, type VariantProps } from "class-variance-authority"; import { useSession } from "@/contexts/Auth"; +import { cn } from "@/lib/utils"; export type ProductTier = "free" | "pro" | "enterprise"; -export const ProductTierBadge = ({ tier }: { tier?: ProductTier }) => { +const productTierBadgeVariants = cva( + "inline-flex items-center text-xs uppercase font-mono px-1 py-0.5 rounded-xs w-fit", + { + variants: { + tier: { + free: "border border-neutral-600 text-neutral-600 dark:border-neutral-400 dark:text-neutral-400", + pro: "border border-success-foreground text-success-foreground dark:border-success dark:text-success", + enterprise: "text-foreground", // Enterprise uses gradient wrapper, so different styling + }, + }, + defaultVariants: { + tier: "free", + }, + } +); + +const productTierRingVariants = cva("", { + variants: { + tier: { + free: "ring-neutral-600/50", + pro: "ring-success-foreground/50 dark:ring-success/50", + enterprise: "ring-brand-gradient-end/50", + }, + }, + defaultVariants: { + tier: "free", + }, +}); + +type ProductTierBadgeProps = VariantProps & { + tier?: ProductTier; + className?: string; +}; + +export const ProductTierBadge = ({ tier, className }: ProductTierBadgeProps) => { const session = useSession(); const finalTier = tier ?? (session.gramAccountType as ProductTier); @@ -13,11 +49,24 @@ export const ProductTierBadge = ({ tier }: { tier?: ProductTier }) => { enterprise: "Enterprise", }[finalTier]; - const classes = productTierColors(finalTier); + // Enterprise tier uses gradient border technique + if (finalTier === "enterprise") { + return ( +
+
+ {name} +
+
+ ); + } + // Free and Pro use regular borders return (
{name}
@@ -25,21 +74,21 @@ export const ProductTierBadge = ({ tier }: { tier?: ProductTier }) => { }; export const productTierColors = (tier: ProductTier) => { + // Return classes that can be used for other components + if (tier === "enterprise") { + return { + bg: '', // No background color for enterprise (uses gradient wrapper) + border: '', // No simple border for enterprise + text: 'text-foreground', + ring: productTierRingVariants({ tier }), + }; + } + + const variantClasses = productTierBadgeVariants({ tier }).split(' '); return { - free: { - bg: "bg-neutral-600", - text: "text-white", - ring: "ring-neutral-600/50", - }, - pro: { - bg: "bg-violet-500", - text: "text-white", - ring: "ring-violet-500/50", - }, - enterprise: { - bg: "bg-success-foreground dark:bg-success", - text: "text-success dark:text-white", - ring: "ring-success-foreground/50 dark:ring-success/50", - }, - }[tier]; -}; + bg: '', // No background color anymore + border: variantClasses.filter(c => c.startsWith('border-')).join(' '), + text: variantClasses.find(c => c.startsWith('text-')) || '', + ring: productTierRingVariants({ tier }), + }; +}; \ No newline at end of file diff --git a/client/dashboard/src/components/ui/button.tsx b/client/dashboard/src/components/ui/button.tsx index e65c076fe..20fc6872b 100644 --- a/client/dashboard/src/components/ui/button.tsx +++ b/client/dashboard/src/components/ui/button.tsx @@ -11,7 +11,7 @@ import { cva, type VariantProps } from "class-variance-authority"; import * as React from "react"; const buttonVariants = cva( - "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive", + "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-sm text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive", { variants: { variant: { @@ -32,8 +32,8 @@ const buttonVariants = cva( size: { default: "h-9 px-4 py-2 has-[>svg]:px-3", inline: "h-7 rounded-sm px-1.5 py-1.5 gap-0.5", - sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5", - lg: "h-10 rounded-md px-6 has-[>svg]:px-4", + sm: "h-8 rounded-sm gap-1.5 px-3 has-[>svg]:px-2.5", + lg: "h-10 rounded-sm px-6 has-[>svg]:px-4", icon: "size-9", "icon-sm": "size-8", }, @@ -64,7 +64,7 @@ export function Button({ caps?: boolean; href?: string; }) { - const Comp: React.ElementType = asChild ? Slot : (props.href ? "a" : "button"); + const Comp: React.ElementType = asChild ? Slot : props.href ? "a" : "button"; const iconColors = { default: "text-primary-foreground/60 group-hover:text-primary-foreground", diff --git a/client/dashboard/src/pages/billing/Billing.tsx b/client/dashboard/src/pages/billing/Billing.tsx index 06771baa5..fb5b8eea5 100644 --- a/client/dashboard/src/pages/billing/Billing.tsx +++ b/client/dashboard/src/pages/billing/Billing.tsx @@ -1,10 +1,6 @@ import { FeatureRequestModal } from "@/components/FeatureRequestModal"; import { Page } from "@/components/page-layout"; -import { - ProductTier, - ProductTierBadge, - productTierColors, -} from "@/components/product-tier-badge"; +import { ProductTier } from "@/components/product-tier-badge"; import { Button } from "@/components/ui/button"; import { Card, Cards, CardSkeleton } from "@/components/ui/card"; import { Heading } from "@/components/ui/heading"; @@ -57,11 +53,11 @@ const UsageSection = () => { billing portal to see complete details or manage your account. -
+
{periodUsage ? ( <> -
- +
+ Tool Calls @@ -76,8 +72,8 @@ const UsageSection = () => { noMax={session.gramAccountType === "enterprise"} />
-
- +
+ Servers @@ -102,8 +98,8 @@ const UsageSection = () => { )} {creditUsage ? ( -
- +
+ Playground Credits @@ -163,15 +159,88 @@ const UsageTiers = () => { ? "Tailored pricing" : `$${tier.basePrice.toLocaleString()}`; - const ringColor = productTierColors(name.toLowerCase() as ProductTier).ring; + if (active) { + const gradientStyle = + name === "Pro" ? { background: "var(--gradient-brand-green)" } : {}; + + return ( +
+ + + + + + {name} + + + {price} + + + + + + + + + {previousTier + ? `Everything from ${previousTier}, plus` + : "Features"} + +
    + {(tier.featureBullets || []).map((bullet) => ( +
  • + {" "} + {bullet} +
  • + ))} +
+
+ + + Included + +
    + {(tier.includedBullets || []).map((bullet) => ( +
  • + {" "} + {bullet} +
  • + ))} +
+
+
+
+
+
+ ); + } return ( - + - - - {price} + + {name} + + {price} + @@ -190,8 +259,8 @@ const UsageTiers = () => { : "Features"}
    - {tier.featureBullets.map((bullet) => ( -
  • + {(tier.featureBullets || []).map((bullet) => ( +
  • {bullet}
  • ))} @@ -208,8 +277,8 @@ const UsageTiers = () => { Included
      - {tier.includedBullets.map((bullet) => ( -
    • + {(tier.includedBullets || []).map((bullet) => ( +
    • {bullet}
    • ))} @@ -235,7 +304,7 @@ const UsageTiers = () => { )} - + {isLoading ? ( <> @@ -297,15 +366,16 @@ const UsageProgress = ({ const includedProgress = (
      @@ -313,13 +383,14 @@ const UsageProgress = ({ const overageProgress = anyOverage ? (
      @@ -334,7 +405,7 @@ const UsageProgress = ({
      {/* Included label underneath, always show */}
      {anyOverage @@ -353,7 +424,7 @@ const UsageProgress = ({ /> {/* Overage label underneath */}
      Extra: {(value - included).toLocaleString()} @@ -398,6 +469,7 @@ const PolarPortalLink = ({ children }: { children: React.ReactNode }) => { ? "Enterprise: Contact support to manage billing" : undefined } + caps > {children}