194 lines
7.3 KiB
TypeScript
194 lines
7.3 KiB
TypeScript
/**
|
|
* Pricing Table Component
|
|
* Display subscription plans in a table format
|
|
*/
|
|
|
|
import { useState } from 'react';
|
|
import { CheckIcon } from '../../../icons';
|
|
import Button from '../button/Button';
|
|
|
|
export interface PricingPlan {
|
|
id: number;
|
|
name: string;
|
|
monthlyPrice: number;
|
|
price: number;
|
|
originalPrice?: number;
|
|
period: string;
|
|
description: string;
|
|
features: string[];
|
|
buttonText: string;
|
|
highlighted?: boolean;
|
|
disabled?: boolean;
|
|
annualDiscountPercent?: number;
|
|
// Plan limits
|
|
max_sites?: number;
|
|
max_users?: number;
|
|
max_keywords?: number;
|
|
max_ahrefs_queries?: number;
|
|
included_credits?: number;
|
|
}
|
|
|
|
interface PricingTableProps {
|
|
variant?: '1' | '2';
|
|
title?: string;
|
|
plans: PricingPlan[];
|
|
showToggle?: boolean;
|
|
onPlanSelect?: (plan: PricingPlan) => void;
|
|
}
|
|
|
|
export function PricingTable({ variant = '1', title, plans, showToggle = false, onPlanSelect }: PricingTableProps) {
|
|
const [billingPeriod, setBillingPeriod] = useState<'monthly' | 'annual'>('monthly');
|
|
|
|
const getPrice = (plan: PricingPlan) => {
|
|
if (billingPeriod === 'annual') {
|
|
const discount = plan.annualDiscountPercent || 20;
|
|
return (plan.monthlyPrice * 12 * (100 - discount) / 100).toFixed(0);
|
|
}
|
|
return plan.monthlyPrice.toFixed(0);
|
|
};
|
|
|
|
const getPeriod = () => {
|
|
return billingPeriod === 'annual' ? '/year' : '/month';
|
|
};
|
|
|
|
return (
|
|
<div className="w-full">
|
|
{title && (
|
|
<div className="text-center mb-8">
|
|
<h2 className="text-3xl font-bold text-gray-900 dark:text-white">{title}</h2>
|
|
</div>
|
|
)}
|
|
|
|
{showToggle && (
|
|
<div className="flex justify-center mb-8">
|
|
<div className="inline-flex items-center gap-3">
|
|
<div className="inline-flex items-center gap-3 p-1 bg-gray-100 dark:bg-gray-800 rounded-lg">
|
|
<Button
|
|
variant={billingPeriod === 'monthly' ? 'secondary' : 'ghost'}
|
|
size="sm"
|
|
onClick={() => setBillingPeriod('monthly')}
|
|
>
|
|
Monthly
|
|
</Button>
|
|
<Button
|
|
variant={billingPeriod === 'annual' ? 'secondary' : 'ghost'}
|
|
size="sm"
|
|
onClick={() => setBillingPeriod('annual')}
|
|
>
|
|
Annually
|
|
</Button>
|
|
</div>
|
|
{billingPeriod === 'annual' && (
|
|
<span className="badge-success">
|
|
Save {Math.round(plans[0]?.annualDiscountPercent || 20)}%
|
|
</span>
|
|
)}
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
|
|
{plans.map((plan) => (
|
|
<div
|
|
key={plan.id}
|
|
className={`relative rounded-lg border ${
|
|
plan.highlighted
|
|
? 'pricing-card-featured border-gray-700 shadow-lg ring-2 ring-gray-700'
|
|
: 'border-gray-200 dark:border-gray-700'
|
|
} ${plan.highlighted ? '' : 'bg-white dark:bg-gray-800'} p-6 flex flex-col`}
|
|
>
|
|
{plan.highlighted && (
|
|
<div className="absolute -top-4 left-1/2 transform -translate-x-1/2">
|
|
<span className="badge-primary px-3 py-1 inline-block">
|
|
Popular
|
|
</span>
|
|
</div>
|
|
)}
|
|
|
|
<div className="mb-4">
|
|
<h3 className="text-xl font-bold text-gray-900 dark:text-white">{plan.name}</h3>
|
|
<p className="text-sm text-gray-600 dark:text-gray-400 mt-1">{plan.description}</p>
|
|
</div>
|
|
|
|
<div className="mb-6">
|
|
<div className="flex items-baseline gap-2">
|
|
<span className={`text-4xl font-bold ${plan.highlighted ? 'text-white' : 'text-gray-900 dark:text-white'} price-text`}>
|
|
${getPrice(plan)}
|
|
</span>
|
|
<span className={plan.highlighted ? 'text-gray-300' : 'text-gray-600 dark:text-gray-400'}>{getPeriod()}</span>
|
|
{plan.originalPrice && billingPeriod === 'monthly' && (
|
|
<span className="text-lg line-through text-gray-400">
|
|
${plan.originalPrice.toFixed(2)}
|
|
</span>
|
|
)}
|
|
</div>
|
|
{billingPeriod === 'annual' && plan.monthlyPrice > 0 && (
|
|
<p className="text-sm text-gray-500 mt-1">
|
|
Billed ${(plan.monthlyPrice * 12 * (100 - (plan.annualDiscountPercent || 20)) / 100).toFixed(0)}/year
|
|
</p>
|
|
)}
|
|
</div>
|
|
|
|
<ul className="space-y-3 mb-6 flex-grow">
|
|
{plan.features.map((feature, index) => (
|
|
<li key={index} className="flex items-start gap-2">
|
|
<CheckIcon className="w-5 h-5 text-success-500 flex-shrink-0 mt-0.5" />
|
|
<span className="text-sm text-gray-700 dark:text-gray-300">{feature}</span>
|
|
</li>
|
|
))}
|
|
|
|
{/* Plan Limits Section */}
|
|
{(plan.max_sites || plan.max_keywords || plan.included_credits) && (
|
|
<div className="pt-3 mt-3 border-t border-gray-200 dark:border-gray-700">
|
|
<div className="text-xs font-semibold text-gray-500 dark:text-gray-400 mb-2">LIMITS</div>
|
|
{plan.max_sites && (
|
|
<li className="flex items-start gap-2">
|
|
<CheckIcon className="w-4 h-4 text-brand-500 flex-shrink-0 mt-0.5" />
|
|
<span className="text-xs text-gray-600 dark:text-gray-400">
|
|
{plan.max_sites === 99999 ? 'Unlimited' : plan.max_sites} Sites
|
|
</span>
|
|
</li>
|
|
)}
|
|
{plan.max_users && (
|
|
<li className="flex items-start gap-2">
|
|
<CheckIcon className="w-4 h-4 text-brand-500 flex-shrink-0 mt-0.5" />
|
|
<span className="text-xs text-gray-600 dark:text-gray-400">
|
|
{plan.max_users === 99999 ? 'Unlimited' : plan.max_users} Team Members
|
|
</span>
|
|
</li>
|
|
)}
|
|
{plan.max_keywords && (
|
|
<li className="flex items-start gap-2">
|
|
<CheckIcon className="w-4 h-4 text-brand-500 flex-shrink-0 mt-0.5" />
|
|
<span className="text-xs text-gray-600 dark:text-gray-400">
|
|
{plan.max_keywords.toLocaleString()} Keywords
|
|
</span>
|
|
</li>
|
|
)}
|
|
{plan.included_credits && (
|
|
<li className="flex items-start gap-2">
|
|
<CheckIcon className="w-4 h-4 text-brand-500 flex-shrink-0 mt-0.5" />
|
|
<span className="text-xs text-gray-600 dark:text-gray-400">
|
|
{plan.included_credits.toLocaleString()} Credits/month
|
|
</span>
|
|
</li>
|
|
)}
|
|
</div>
|
|
)}
|
|
</ul>
|
|
|
|
<Button
|
|
variant={plan.highlighted ? 'primary' : 'outline'}
|
|
fullWidth
|
|
onClick={() => onPlanSelect?.(plan)}
|
|
disabled={plan.disabled}
|
|
>
|
|
{plan.buttonText}
|
|
</Button>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|