pricign plans updates
This commit is contained in:
@@ -212,6 +212,8 @@ export default function PricingTable({
|
||||
{plan.features.map((feature, idx) => {
|
||||
const isExcluded = feature.startsWith('!');
|
||||
const featureText = isExcluded ? feature.substring(1) : feature;
|
||||
const isBold = featureText.startsWith('**') && featureText.endsWith('**');
|
||||
const displayText = isBold ? featureText.slice(2, -2) : featureText;
|
||||
return (
|
||||
<li
|
||||
key={idx}
|
||||
@@ -224,7 +226,7 @@ export default function PricingTable({
|
||||
}`}
|
||||
>
|
||||
{isExcluded ? <XIcon /> : <CheckIcon />}
|
||||
{featureText}
|
||||
<span className={isBold ? 'font-semibold' : ''}>{displayText}</span>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
|
||||
@@ -13,76 +13,7 @@ import { getPublicPlans } from "../../services/billing.api";
|
||||
import { PricingTable, PricingPlan } from "../../components/ui/pricing-table";
|
||||
import PricingTable1 from "../../components/ui/pricing-table/pricing-table-1";
|
||||
import { Link } from "react-router-dom";
|
||||
|
||||
interface Plan {
|
||||
id: number;
|
||||
name: string;
|
||||
slug?: string;
|
||||
price: number | string;
|
||||
original_price?: number | string;
|
||||
annual_discount_percent?: number;
|
||||
is_featured?: boolean;
|
||||
max_sites?: number;
|
||||
max_users?: number;
|
||||
max_keywords?: number;
|
||||
max_clusters?: number;
|
||||
max_content_ideas?: number;
|
||||
max_content_words?: number;
|
||||
max_images_basic?: number;
|
||||
max_images_premium?: number;
|
||||
max_image_prompts?: number;
|
||||
included_credits?: number;
|
||||
}
|
||||
|
||||
const formatNumber = (num: number | undefined | null): string => {
|
||||
if (!num || num === 0) return '0';
|
||||
if (num >= 1000000) return `${(num / 1000).toFixed(0)}M`;
|
||||
if (num >= 1000) return `${(num / 1000).toFixed(0)}K`;
|
||||
return num.toString();
|
||||
};
|
||||
|
||||
const convertToPricingPlan = (plan: Plan): PricingPlan => {
|
||||
const monthlyPrice = typeof plan.price === 'number' ? plan.price : parseFloat(String(plan.price || 0));
|
||||
const features: string[] = [];
|
||||
|
||||
if (plan.max_sites) features.push(`${plan.max_sites === 999999 ? 'Unlimited' : plan.max_sites} Site${plan.max_sites > 1 ? 's' : ''}`);
|
||||
if (plan.max_users) features.push(`${plan.max_users} Team User${plan.max_users > 1 ? 's' : ''}`);
|
||||
if (plan.included_credits) features.push(`${formatNumber(plan.included_credits)} Monthly Credits`);
|
||||
if (plan.max_content_words) features.push(`${formatNumber(plan.max_content_words)} Words/Month`);
|
||||
if (plan.max_clusters) features.push(`${plan.max_clusters === 999 ? '500' : plan.max_clusters} AI Keyword Clusters`);
|
||||
if (plan.max_content_ideas) features.push(`${formatNumber(plan.max_content_ideas)} Content Ideas`);
|
||||
if (plan.max_images_basic && plan.max_images_premium) {
|
||||
features.push(`${formatNumber(plan.max_images_basic)} Basic / ${formatNumber(plan.max_images_premium)} Premium Images`);
|
||||
}
|
||||
if (plan.max_image_prompts) features.push(`${formatNumber(plan.max_image_prompts)} Image Prompts`);
|
||||
|
||||
// Custom descriptions based on plan name
|
||||
let description = `Perfect for ${plan.name.toLowerCase()} needs`;
|
||||
if (plan.name.toLowerCase().includes('free')) {
|
||||
description = 'Explore core features risk free';
|
||||
} else if (plan.name.toLowerCase().includes('starter')) {
|
||||
description = 'Launch SEO workflows for small teams';
|
||||
} else if (plan.name.toLowerCase().includes('growth')) {
|
||||
description = 'Scale content production with confidence';
|
||||
} else if (plan.name.toLowerCase().includes('scale')) {
|
||||
description = 'Enterprise power for high volume growth';
|
||||
}
|
||||
|
||||
return {
|
||||
id: plan.id,
|
||||
slug: plan.slug,
|
||||
name: plan.name,
|
||||
monthlyPrice: monthlyPrice,
|
||||
price: monthlyPrice,
|
||||
originalPrice: plan.original_price ? (typeof plan.original_price === 'number' ? plan.original_price : parseFloat(String(plan.original_price))) : undefined,
|
||||
annualDiscountPercent: plan.annual_discount_percent || 15,
|
||||
period: '/month',
|
||||
description: description,
|
||||
features,
|
||||
buttonText: monthlyPrice === 0 ? 'Free Trial' : 'Choose Plan',
|
||||
highlighted: plan.is_featured || false,
|
||||
};
|
||||
};
|
||||
import { Plan, convertToPricingPlan } from "../../utils/pricingHelpers";
|
||||
|
||||
const Pricing: React.FC = () => {
|
||||
const [plans, setPlans] = useState<Plan[]>([]);
|
||||
@@ -341,14 +272,44 @@ const Pricing: React.FC = () => {
|
||||
<section className="relative overflow-hidden bg-gradient-to-b from-white via-slate-50/30 to-white">
|
||||
<div className="absolute inset-0 bg-[radial-gradient(circle_at_50%_50%,rgba(6,147,227,0.02),transparent_60%)]" />
|
||||
|
||||
<div className="relative max-w-4xl mx-auto px-6 py-16 md:py-20 text-center z-10">
|
||||
<div className="relative max-w-4xl mx-auto px-6 pt-12 md:pt-16 pb-6 text-center z-10">
|
||||
<span className="inline-flex items-center gap-2 text-xs font-semibold uppercase tracking-[0.28em] text-slate-500 bg-slate-100 px-4 py-2 rounded-full mb-6">
|
||||
Pricing
|
||||
</span>
|
||||
<h1 className="text-4xl md:text-5xl font-bold leading-tight text-slate-900 mb-4">
|
||||
<h1 className="text-4xl md:text-5xl font-bold leading-tight text-slate-900 mb-0">
|
||||
Simple plans that scale with your automation goals.
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
{/* SPECIAL PROMO BANNER */}
|
||||
<div className="relative max-w-5xl mx-auto px-6 z-10">
|
||||
<div className="relative overflow-hidden rounded-2xl bg-gradient-to-r from-amber-500 via-orange-500 to-red-500 p-[2px] shadow-xl">
|
||||
<div className="relative bg-gradient-to-r from-amber-50 via-orange-50 to-red-50 rounded-2xl px-6 py-4 flex items-center justify-center gap-3">
|
||||
{/* Fire Icon */}
|
||||
<svg className="w-6 h-6 text-orange-600 animate-pulse" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fillRule="evenodd" d="M12.395 2.553a1 1 0 00-1.45-.385c-.345.23-.614.558-.822.88-.214.33-.403.713-.57 1.116-.334.804-.614 1.768-.84 2.734a31.365 31.365 0 00-.613 3.58 2.64 2.64 0 01-.945-1.067c-.328-.68-.398-1.534-.398-2.654A1 1 0 005.05 6.05 6.981 6.981 0 003 11a7 7 0 1011.95-4.95c-.592-.591-.98-.985-1.348-1.467-.363-.476-.724-1.063-1.207-2.03zM12.12 15.12A3 3 0 017 13s.879.5 2.5.5c0-1 .5-4 1.25-4.5.5 1 .786 1.293 1.371 1.879A2.99 2.99 0 0113 13a2.99 2.99 0 01-.879 2.121z" clipRule="evenodd"></path>
|
||||
</svg>
|
||||
|
||||
{/* Message Text */}
|
||||
<div className="flex flex-col sm:flex-row items-center gap-2 sm:gap-3">
|
||||
<span className="inline-flex items-center gap-2 bg-gradient-to-r from-amber-600 to-orange-600 text-white px-3 py-1 rounded-full text-xs font-bold uppercase tracking-wider shadow-lg">
|
||||
<svg className="w-3.5 h-3.5" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path d="M9.049 2.927c.3-.921 1.603-.921 1.902 0l1.07 3.292a1 1 0 00.95.69h3.462c.969 0 1.371 1.24.588 1.81l-2.8 2.034a1 1 0 00-.364 1.118l1.07 3.292c.3.921-.755 1.688-1.54 1.118l-2.8-2.034a1 1 0 00-1.175 0l-2.8 2.034c-.784.57-1.838-.197-1.539-1.118l1.07-3.292a1 1 0 00-.364-1.118L2.98 8.72c-.783-.57-.38-1.81.588-1.81h3.461a1 1 0 00.951-.69l1.07-3.292z"></path>
|
||||
</svg>
|
||||
Limited Time
|
||||
</span>
|
||||
<p className="text-base sm:text-lg font-bold text-transparent bg-clip-text bg-gradient-to-r from-amber-700 via-orange-700 to-red-700">
|
||||
Lifetime Discount on all Plans for First 100 Users!
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Fire Icon */}
|
||||
<svg className="w-6 h-6 text-orange-600 animate-pulse" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fillRule="evenodd" d="M12.395 2.553a1 1 0 00-1.45-.385c-.345.23-.614.558-.822.88-.214.33-.403.713-.57 1.116-.334.804-.614 1.768-.84 2.734a31.365 31.365 0 00-.613 3.58 2.64 2.64 0 01-.945-1.067c-.328-.68-.398-1.534-.398-2.654A1 1 0 005.05 6.05 6.981 6.981 0 003 11a7 7 0 1011.95-4.95c-.592-.591-.98-.985-1.348-1.467-.363-.476-.724-1.063-1.207-2.03zM12.12 15.12A3 3 0 017 13s.879.5 2.5.5c0-1 .5-4 1.25-4.5.5 1 .786 1.293 1.371 1.879A2.99 2.99 0 0113 13a2.99 2.99 0 01-.879 2.121z" clipRule="evenodd"></path>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* LOADING/ERROR STATES */}
|
||||
@@ -377,7 +338,7 @@ const Pricing: React.FC = () => {
|
||||
|
||||
{/* PRICING TABLES SECTION - Dynamic Backend Plans */}
|
||||
{!loading && !error && plans.length > 0 && (
|
||||
<section className="px-6 py-16">
|
||||
<section className="px-6 py-8">
|
||||
<div className="pricing-table-wrapper">
|
||||
<PricingTable1
|
||||
plans={plans.map(convertToPricingPlan)}
|
||||
@@ -387,11 +348,18 @@ const Pricing: React.FC = () => {
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* What's Included Explanation */}
|
||||
<div className="max-w-5xl mx-auto mt-8 text-center">
|
||||
<p className="text-slate-600 dark:text-slate-400 text-sm leading-relaxed">
|
||||
Every content piece includes High-Opportunity Keywords, AI clustering, idea generation, standard/premium images, Automation, WP publishing, SEO optimization, and internal linking.
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
|
||||
{/* COMPARISON TABLE SECTION - Keep hardcoded for now */}
|
||||
{!loading && !error && (
|
||||
{/* COMPARISON TABLE SECTION REMOVED */}
|
||||
{!loading && !error && false && (
|
||||
<section className="max-w-7xl mx-auto px-6 pb-24 pt-16">
|
||||
<h3 className="text-3xl font-bold text-slate-900 mb-8 text-center">
|
||||
Compare plan capabilities
|
||||
|
||||
@@ -17,6 +17,7 @@ import PricingTable1 from '../../components/ui/pricing-table/pricing-table-1';
|
||||
import CreditCostBreakdownPanel from '../../components/billing/CreditCostBreakdownPanel';
|
||||
import CreditCostsPanel from '../../components/billing/CreditCostsPanel';
|
||||
import UsageLimitsPanel from '../../components/billing/UsageLimitsPanel';
|
||||
import { convertToPricingPlan } from '../../utils/pricingHelpers';
|
||||
import {
|
||||
getCreditBalance,
|
||||
getCreditPackages,
|
||||
@@ -727,46 +728,11 @@ export default function PlansAndBillingPage() {
|
||||
const planPrice = plan.price || 0;
|
||||
return planPrice > 0 && !planName.includes('free');
|
||||
})
|
||||
.map(plan => {
|
||||
const discount = plan.annual_discount_percent || 15;
|
||||
|
||||
// Get custom description based on plan name
|
||||
let description = 'Standard plan';
|
||||
const planName = plan.name.toLowerCase();
|
||||
if (planName.includes('starter')) {
|
||||
description = 'Launch SEO workflows for small teams';
|
||||
} else if (planName.includes('growth')) {
|
||||
description = 'Scale content production with confidence';
|
||||
} else if (planName.includes('scale')) {
|
||||
description = 'Enterprise power for high volume growth';
|
||||
}
|
||||
|
||||
// Build features array
|
||||
const features: string[] = [];
|
||||
if (plan.max_sites) features.push(`${plan.max_sites === 999999 ? 'Unlimited' : plan.max_sites} Site${plan.max_sites > 1 ? 's' : ''}`);
|
||||
if (plan.max_users) features.push(`${plan.max_users} Team User${plan.max_users > 1 ? 's' : ''}`);
|
||||
if (plan.included_credits) features.push(`${(plan.included_credits / 1000).toFixed(0)}K Monthly Credits`);
|
||||
if (plan.max_content_words) features.push(`${(plan.max_content_words / 1000).toFixed(0)}K Words/Month`);
|
||||
if (plan.max_clusters) features.push(`${plan.max_clusters} AI Keyword Clusters`);
|
||||
if (plan.max_content_ideas) features.push(`${plan.max_content_ideas} Content Ideas`);
|
||||
if (plan.max_images_basic && plan.max_images_premium) {
|
||||
features.push(`${plan.max_images_basic} Basic / ${plan.max_images_premium} Premium Images`);
|
||||
}
|
||||
|
||||
return {
|
||||
id: plan.id,
|
||||
name: plan.name,
|
||||
monthlyPrice: plan.price || 0,
|
||||
price: plan.price || 0,
|
||||
annualDiscountPercent: discount,
|
||||
period: '/month',
|
||||
description: description,
|
||||
features: features.length > 0 ? features : ['Monthly credits included', 'Module access', 'Email support'],
|
||||
buttonText: plan.id === currentPlanId ? 'Current Plan' : 'Choose Plan',
|
||||
highlighted: plan.is_featured || false,
|
||||
disabled: plan.id === currentPlanId || planLoadingId === plan.id,
|
||||
};
|
||||
})}
|
||||
.map(plan => ({
|
||||
...convertToPricingPlan(plan),
|
||||
buttonText: plan.id === currentPlanId ? 'Current Plan' : 'Choose Plan',
|
||||
disabled: plan.id === currentPlanId || planLoadingId === plan.id,
|
||||
}))}
|
||||
showToggle={true}
|
||||
onPlanSelect={(plan) => plan.id && handleSelectPlan(plan.id)}
|
||||
/>
|
||||
|
||||
93
frontend/src/utils/pricingHelpers.ts
Normal file
93
frontend/src/utils/pricingHelpers.ts
Normal file
@@ -0,0 +1,93 @@
|
||||
/**
|
||||
* Shared pricing utilities for converting backend Plan data to PricingPlan format
|
||||
* Used by both marketing pricing page and app's plans page
|
||||
*/
|
||||
|
||||
import { PricingPlan } from '../components/ui/pricing-table';
|
||||
|
||||
export interface Plan {
|
||||
id: number;
|
||||
name: string;
|
||||
slug?: string;
|
||||
price: number | string;
|
||||
original_price?: number | string;
|
||||
annual_discount_percent?: number;
|
||||
is_featured?: boolean;
|
||||
max_sites?: number;
|
||||
max_users?: number;
|
||||
max_keywords?: number;
|
||||
max_clusters?: number;
|
||||
max_content_ideas?: number;
|
||||
max_content_words?: number;
|
||||
max_images_basic?: number;
|
||||
max_images_premium?: number;
|
||||
max_image_prompts?: number;
|
||||
included_credits?: number;
|
||||
}
|
||||
|
||||
export const formatNumber = (num: number | undefined | null): string => {
|
||||
if (!num || num === 0) return '0';
|
||||
if (num >= 1000000) return `${(num / 1000).toFixed(0)}M`;
|
||||
if (num >= 1000) return `${(num / 1000).toFixed(0)}K`;
|
||||
return num.toString();
|
||||
};
|
||||
|
||||
export const convertToPricingPlan = (plan: Plan): PricingPlan => {
|
||||
const monthlyPrice = typeof plan.price === 'number' ? plan.price : parseFloat(String(plan.price || 0));
|
||||
const features: string[] = [];
|
||||
|
||||
// Dynamic counts - shown with numbers from backend
|
||||
if (plan.max_content_ideas) {
|
||||
features.push(`**${formatNumber(plan.max_content_ideas)} Pages/Articles per month**`);
|
||||
}
|
||||
if (plan.max_sites) {
|
||||
features.push(`${plan.max_sites === 999999 ? 'Unlimited' : formatNumber(plan.max_sites)} Site${plan.max_sites > 1 && plan.max_sites !== 999999 ? 's' : ''}`);
|
||||
}
|
||||
if (plan.max_users) {
|
||||
features.push(`${formatNumber(plan.max_users)} Team User${plan.max_users > 1 ? 's' : ''}`);
|
||||
}
|
||||
|
||||
// Features - shown with checkmarks only (no counts)
|
||||
features.push('AI Keyword Clustering');
|
||||
features.push('AI Content Ideas');
|
||||
features.push('AI Image Generation');
|
||||
features.push('Internal Linking');
|
||||
features.push('SEO Optimization');
|
||||
features.push('WordPress Publishing');
|
||||
features.push('Pre-Researched High-Opportunity Keywords');
|
||||
|
||||
// Priority Support - only for Scale plan (excluded for others)
|
||||
const planName = plan.name.toLowerCase();
|
||||
if (planName.includes('scale')) {
|
||||
features.push('Priority Support');
|
||||
} else {
|
||||
features.push('!Priority Support');
|
||||
}
|
||||
|
||||
// Custom descriptions based on plan name
|
||||
let description = `Perfect for ${plan.name.toLowerCase()} needs`;
|
||||
if (plan.name.toLowerCase().includes('free')) {
|
||||
description = 'Explore core features risk free';
|
||||
} else if (plan.name.toLowerCase().includes('starter')) {
|
||||
description = 'Launch SEO workflows for small teams';
|
||||
} else if (plan.name.toLowerCase().includes('growth')) {
|
||||
description = 'Scale content production with confidence';
|
||||
} else if (plan.name.toLowerCase().includes('scale')) {
|
||||
description = 'Enterprise power for high volume growth';
|
||||
}
|
||||
|
||||
return {
|
||||
id: plan.id,
|
||||
slug: plan.slug,
|
||||
name: plan.name,
|
||||
monthlyPrice: monthlyPrice,
|
||||
price: monthlyPrice,
|
||||
originalPrice: plan.original_price ? (typeof plan.original_price === 'number' ? plan.original_price : parseFloat(String(plan.original_price))) : undefined,
|
||||
annualDiscountPercent: plan.annual_discount_percent || 15,
|
||||
period: '/month',
|
||||
description: description,
|
||||
features,
|
||||
buttonText: monthlyPrice === 0 ? 'Free Trial' : 'Choose Plan',
|
||||
highlighted: plan.is_featured || false,
|
||||
};
|
||||
};
|
||||
Reference in New Issue
Block a user