nbcvhc
This commit is contained in:
@@ -1,108 +1,92 @@
|
||||
import React from "react";
|
||||
import { Link } from "react-router-dom";
|
||||
import React, { useState, useEffect } from "react";
|
||||
import {
|
||||
RocketLaunchIcon,
|
||||
ChatBubbleLeftRightIcon,
|
||||
CheckIcon,
|
||||
XMarkIcon,
|
||||
SparklesIcon,
|
||||
PhotoIcon,
|
||||
BoltIcon,
|
||||
ChartBarIcon,
|
||||
UserGroupIcon,
|
||||
CreditCardIcon,
|
||||
ShieldCheckIcon,
|
||||
} from "@heroicons/react/24/outline";
|
||||
import SEO from "../components/SEO";
|
||||
import { getMetaTags } from "../config/metaTags";
|
||||
import { getPublicPlans } from "../../services/billing.api";
|
||||
import PricingTable, { PricingPlan } from "../../components/ui/pricing-table/PricingTable";
|
||||
import { Link } from "react-router-dom";
|
||||
|
||||
interface Plan {
|
||||
id: number;
|
||||
name: string;
|
||||
slug?: string;
|
||||
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} 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`);
|
||||
|
||||
return {
|
||||
id: plan.id,
|
||||
name: plan.name,
|
||||
monthlyPrice: monthlyPrice,
|
||||
price: monthlyPrice,
|
||||
annualDiscountPercent: plan.annual_discount_percent || 15,
|
||||
period: '/month',
|
||||
description: `Perfect for ${plan.name.toLowerCase()} needs`,
|
||||
features,
|
||||
buttonText: monthlyPrice === 0 ? 'Start Free' : 'Choose Plan',
|
||||
highlighted: plan.is_featured || false,
|
||||
};
|
||||
};
|
||||
|
||||
const Pricing: React.FC = () => {
|
||||
const renderCta = (cta: { label: string; href: string }, className: string) => {
|
||||
const isExternal = cta.href.startsWith("http");
|
||||
|
||||
if (isExternal) {
|
||||
return (
|
||||
<a
|
||||
href={cta.href}
|
||||
className={className}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
{cta.label}
|
||||
</a>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Link to={cta.href} className={className}>
|
||||
{cta.label}
|
||||
</Link>
|
||||
);
|
||||
};
|
||||
const [plans, setPlans] = useState<Plan[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const tiers = [
|
||||
{
|
||||
name: "Starter",
|
||||
slug: "starter",
|
||||
price: "$99",
|
||||
cadence: "per month",
|
||||
description: "For small teams starting workflows.",
|
||||
icon: SparklesIcon,
|
||||
iconColor: "from-[var(--color-primary)] to-[var(--color-primary-dark)]",
|
||||
features: [
|
||||
"2 sites",
|
||||
"1 team user",
|
||||
"1,000 monthly credits",
|
||||
"100K words content generation",
|
||||
"100 AI keyword clusters",
|
||||
"300 content ideas",
|
||||
"300 basic / 60 premium images",
|
||||
"300 image prompts",
|
||||
],
|
||||
badge: "For startups",
|
||||
},
|
||||
{
|
||||
name: "Growth",
|
||||
slug: "growth",
|
||||
price: "$199",
|
||||
cadence: "per month",
|
||||
description: "For teams automating multiple workflows.",
|
||||
icon: BoltIcon,
|
||||
iconColor: "from-[var(--color-success)] to-[var(--color-success-dark)]",
|
||||
features: [
|
||||
"5 sites",
|
||||
"3 team users",
|
||||
"3,000 monthly credits",
|
||||
"300K words content generation",
|
||||
"300 AI keyword clusters",
|
||||
"900 content ideas",
|
||||
"900 basic / 180 premium images",
|
||||
"900 image prompts",
|
||||
],
|
||||
featured: true,
|
||||
badge: "Best value",
|
||||
},
|
||||
{
|
||||
name: "Scale",
|
||||
slug: "scale",
|
||||
price: "$299",
|
||||
cadence: "per month",
|
||||
description: "For publishers and large orgs needing deeper control.",
|
||||
icon: ChartBarIcon,
|
||||
iconColor: "from-[var(--color-purple)] to-[var(--color-purple-dark)]",
|
||||
features: [
|
||||
"Unlimited sites",
|
||||
"5 team users",
|
||||
"5,000 monthly credits",
|
||||
"500K words content generation",
|
||||
"500 AI keyword clusters",
|
||||
"1,500 content ideas",
|
||||
"1,500 basic / 300 premium images",
|
||||
"1,500 image prompts",
|
||||
],
|
||||
badge: "For scaling teams",
|
||||
},
|
||||
];
|
||||
useEffect(() => {
|
||||
const fetchPlans = async () => {
|
||||
try {
|
||||
const data = await getPublicPlans();
|
||||
setPlans(data);
|
||||
setLoading(false);
|
||||
} catch (err) {
|
||||
console.error('Error fetching public plans:', err);
|
||||
setError('Failed to load pricing plans');
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
fetchPlans();
|
||||
}, []);
|
||||
|
||||
const featureMatrix = [
|
||||
{ feature: "ACCOUNT & ACCESS", starter: null, growth: null, scale: null, isCategory: true },
|
||||
@@ -224,95 +208,50 @@ const Pricing: React.FC = () => {
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* LOADING/ERROR STATES */}
|
||||
{loading && (
|
||||
<section className="max-w-7xl mx-auto px-6 pb-24">
|
||||
<div className="text-center py-12">
|
||||
<div className="inline-block animate-spin rounded-full h-12 w-12 border-b-2 border-[var(--color-primary)]"></div>
|
||||
<p className="mt-4 text-slate-600">Loading pricing plans...</p>
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
|
||||
{error && (
|
||||
<section className="max-w-7xl mx-auto px-6 pb-24">
|
||||
<div className="text-center py-12">
|
||||
<p className="text-red-600">{error}</p>
|
||||
<button
|
||||
onClick={() => window.location.reload()}
|
||||
className="mt-4 px-6 py-2 bg-[var(--color-primary)] text-white rounded-lg hover:bg-[var(--color-primary-dark)]"
|
||||
>
|
||||
Retry
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
|
||||
{/* PRICING TIERS SECTION */}
|
||||
<section className="max-w-7xl mx-auto px-6 pb-24">
|
||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-8">
|
||||
{tiers.map((tier) => {
|
||||
const Icon = tier.icon;
|
||||
return (
|
||||
<div
|
||||
key={tier.name}
|
||||
className={`relative rounded-3xl border-2 ${
|
||||
tier.featured
|
||||
? "border-[var(--color-primary)]/60 bg-gradient-to-br from-[#0693e3]/10 via-[#5d4ae3]/5 to-[#0bbf87]/5 shadow-[0_0_70px_rgba(6,147,227,0.25)]"
|
||||
: "border-slate-200 bg-gradient-to-br from-white to-slate-50/50"
|
||||
} p-10 flex flex-col gap-6 hover:shadow-xl transition-all group ${
|
||||
tier.featured ? "lg:scale-105" : "hover:-translate-y-1"
|
||||
}`}
|
||||
>
|
||||
{/* Badge and Icon - Aligned */}
|
||||
<div className="flex items-center gap-4">
|
||||
{/* Icon */}
|
||||
<div className={`inline-flex size-14 rounded-2xl bg-gradient-to-br ${tier.iconColor} items-center justify-center text-white shadow-lg flex-shrink-0`}>
|
||||
<Icon className="h-7 w-7" />
|
||||
</div>
|
||||
|
||||
{/* Badge and Plan Name */}
|
||||
<div className="flex-1">
|
||||
{tier.badge && (
|
||||
<span
|
||||
className={`inline-flex items-center rounded-full border px-3 py-1 text-[10px] uppercase tracking-[0.25em] mb-2 ${
|
||||
tier.featured
|
||||
? "border-[var(--color-primary)]/30 bg-gradient-to-r from-[#0693e3]/20 to-[#0472b8]/20 text-[#0472b8]"
|
||||
: "border-slate-200 bg-gradient-to-r from-[#0693e3]/10 to-[#0bbf87]/10 text-[var(--color-primary)]"
|
||||
}`}
|
||||
>
|
||||
{tier.badge}
|
||||
</span>
|
||||
)}
|
||||
<h3 className="text-2xl font-semibold text-slate-900">{tier.name}</h3>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Plan Description */}
|
||||
<p className="text-sm text-slate-600">{tier.description}</p>
|
||||
|
||||
{/* Price */}
|
||||
<div className="text-4xl font-semibold text-slate-900">
|
||||
{tier.price}
|
||||
{tier.cadence && (
|
||||
<span className="text-sm font-normal text-slate-500 ml-2">
|
||||
{tier.cadence}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Features List */}
|
||||
<ul className="space-y-4 text-sm text-slate-600 flex-1">
|
||||
{tier.features.map((feature, idx) => {
|
||||
// Subtle check icons: light bg with dark check for starter/scale, colored for growth
|
||||
const checkStyle = tier.featured
|
||||
? "bg-[var(--color-success)]/10 text-[#0bbf87]"
|
||||
: "bg-slate-100 text-slate-600";
|
||||
return (
|
||||
<li key={feature} className="flex gap-3 items-start">
|
||||
<CheckIcon className={`h-5 w-5 ${checkStyle} rounded-full p-0.5 flex-shrink-0 mt-0.5`} />
|
||||
<span>{feature}</span>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
|
||||
{/* CTA Button */}
|
||||
<div className="pt-4">
|
||||
<a
|
||||
href={`https://app.igny8.com/signup?plan=${tier.slug}`}
|
||||
className={`inline-flex w-full items-center justify-center rounded-full px-6 py-3 text-sm font-semibold transition ${
|
||||
tier.featured
|
||||
? "bg-gradient-to-r from-[var(--color-primary)] to-[var(--color-primary-dark)] text-white hover:from-[#0472b8] hover:to-[#0693e3] shadow-lg shadow-[#0693e3]/30"
|
||||
: "border-2 border-slate-300 bg-white/50 backdrop-blur-sm text-slate-900 hover:border-[var(--color-primary)] hover:bg-white"
|
||||
}`}
|
||||
>
|
||||
{tier.price === "Free" ? "Start free trial" : `Continue to billing`}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
{!loading && !error && (
|
||||
<section className="mx-auto px-6 py-16" style={{ maxWidth: '1560px' }}>
|
||||
<PricingTable
|
||||
variant="1"
|
||||
title="Flexible Plans Tailored to Fit Your Unique Needs!"
|
||||
plans={plans.map(convertToPricingPlan)}
|
||||
showToggle={true}
|
||||
onPlanSelect={(pricingPlan) => {
|
||||
const plan = plans.find(p => p.id === pricingPlan.id);
|
||||
if (plan) {
|
||||
window.location.href = `https://app.igny8.com/signup?plan=${plan.slug}`;
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</section>
|
||||
)}
|
||||
|
||||
{/* COMPARISON TABLE SECTION */}
|
||||
{/* COMPARISON TABLE SECTION - Keep hardcoded for now */}
|
||||
{!loading && !error && (
|
||||
<section className="max-w-7xl mx-auto px-6 pb-24">
|
||||
<h3 className="text-3xl font-bold text-slate-900 mb-8 text-center">
|
||||
Compare plan capabilities
|
||||
@@ -399,6 +338,7 @@ const Pricing: React.FC = () => {
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
|
||||
{/* INFO BLOCKS SECTION */}
|
||||
<section className="bg-gradient-to-b from-white via-slate-50/30 to-white">
|
||||
|
||||
Reference in New Issue
Block a user