This commit is contained in:
IGNY8 VPS (Salman)
2025-12-13 13:43:55 +00:00
parent ad895fcb3a
commit db1fd2fff8
4 changed files with 175 additions and 232 deletions

View File

@@ -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">