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

@@ -92,41 +92,60 @@ export default function PricingTable({
return (
<div className={`space-y-6 ${className}`}>
{title && (
<div className="mx-auto w-full max-w-[385px]">
<h2 className="font-bold text-center text-gray-800 mb-7 text-title-sm dark:text-white/90">
<div className="mx-auto w-full">
<h2 className="font-bold text-center text-gray-800 text-2xl mb-5 dark:text-white/90">
{title}
</h2>
</div>
)}
{showToggle && (
<div className="mb-10 text-center">
<div className="relative inline-flex p-1 mx-auto bg-gray-200 rounded-full z-1 dark:bg-gray-800">
<div className="mb-8 text-center">
<div className="relative inline-flex p-1 bg-gray-200 rounded-full dark:bg-gray-800 shadow-sm">
<span
className={`absolute top-1/2 -z-1 flex h-11 w-[120px] -translate-y-1/2 rounded-full bg-white shadow-theme-xs duration-200 ease-linear dark:bg-white/10 ${
billingPeriod === 'monthly' ? 'translate-x-0' : 'translate-x-[120px]'
}`}
className="absolute top-1 left-1 flex h-11 w-[130px] rounded-full shadow-theme-xs duration-200 ease-linear"
style={{
background: 'linear-gradient(to bottom right, #0693e3, #0472b8)',
transform: billingPeriod === 'monthly' ? 'translateX(0)' : 'translateX(130px)',
}}
></span>
<button
type="button"
onClick={() => setBillingPeriod('monthly')}
className={`flex h-11 w-[120px] items-center justify-center text-base font-medium transition-colors ${
className={`relative z-10 flex h-11 w-[130px] items-center justify-center font-medium transition-all duration-200 rounded-full ${
billingPeriod === 'monthly'
? 'text-gray-800 dark:text-white/90'
? 'text-white'
: 'text-gray-500 hover:text-gray-700 dark:hover:text-white/80 dark:text-gray-400'
}`}
>
Monthly
</button>
<button
type="button"
onClick={() => setBillingPeriod('annually')}
className={`flex h-11 w-[120px] items-center justify-center text-base font-medium transition-colors ${
className={`relative z-10 flex h-11 w-[130px] items-center justify-center font-medium transition-all duration-200 rounded-full ${
billingPeriod === 'annually'
? 'text-gray-800 dark:text-white/90'
? 'text-white'
: 'text-gray-500 hover:text-gray-700 dark:hover:text-white/80 dark:text-gray-400'
}`}
>
Annually
</button>
</div>
{billingPeriod === 'annually' && (
<div className="flex items-center justify-center mt-3">
<span className="inline-flex items-center gap-1.5 text-green-600 dark:text-green-400 font-semibold bg-green-50 dark:bg-green-900/20 px-3 py-1.5 rounded-full text-sm">
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M12 8c-1.657 0-3 .895-3 2s1.343 2 3 2 3 .895 3 2-1.343 2-3 2m0-8c1.11 0 2.08.402 2.599 1M12 8V7m0 1v8m0 0v1m0-1c-1.11 0-2.08-.402-2.599-1M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
/>
</svg>
Save 15% with annual billing
</span>
</div>
)}
</div>
)}
<div className="grid gap-5 grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 xl:gap-6">
@@ -218,7 +237,7 @@ export default function PricingTable({
: 'bg-gray-800 hover:bg-brand-500 dark:bg-white/10 dark:hover:bg-brand-600'
} ${plan.disabled ? 'opacity-50 cursor-not-allowed' : ''}`}
>
{plan.buttonText || 'Choose Plan'}
{plan.buttonText || (plan.price === 0 || plan.monthlyPrice === 0 ? 'Start Free' : 'Choose Plan')}
</button>
</div>
);

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

View File

@@ -50,49 +50,23 @@ const formatWordCount = (num: number): string => {
return num.toString();
};
// Extract major features from plan data
// Extract major features from plan data - SAME AS MARKETING PAGE
const extractFeatures = (plan: Plan): string[] => {
const features: string[] = [];
// Sites and Users
features.push(`${plan.max_sites} ${plan.max_sites === 1 ? 'Site' : 'Sites'}`);
features.push(`${plan.max_users} ${plan.max_users === 1 ? 'User' : 'Users'}`);
// Planner features
features.push(`${formatNumber(plan.max_keywords)} Keywords`);
features.push(`${formatNumber(plan.max_clusters)} Clusters`);
features.push(`${formatNumber(plan.max_content_ideas)} Content Ideas`);
// Writer features
features.push(`${formatWordCount(plan.monthly_word_count_limit)} Words/Month`);
features.push(`${plan.daily_content_tasks} Daily Content Tasks`);
// Image features
features.push(`${plan.monthly_image_count} Images/Month`);
if (plan.image_model_choices && plan.image_model_choices.length > 0) {
const models = plan.image_model_choices.map((m: string) => m.toUpperCase()).join(', ');
features.push(`${models} Image Models`);
}
// AI Credits
features.push(`${formatNumber(plan.included_credits)} AI Credits Included`);
features.push(`${formatNumber(plan.monthly_ai_credit_limit)} Monthly AI Credit Limit`);
// Feature flags
if (plan.features && Array.isArray(plan.features)) {
if (plan.features.includes('ai_writer')) {
features.push('AI Writer');
}
if (plan.features.includes('image_gen')) {
features.push('Image Generation');
}
if (plan.features.includes('auto_publish')) {
features.push('Auto Publish');
}
if (plan.features.includes('custom_prompts')) {
features.push('Custom Prompts');
}
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.monthly_word_count_limit) features.push(`${formatWordCount(plan.monthly_word_count_limit)} 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.monthly_image_count) {
// Try to split into basic/premium if data available, otherwise show total
const basicImages = Math.floor(plan.monthly_image_count * 0.8);
const premiumImages = Math.floor(plan.monthly_image_count * 0.2);
features.push(`${formatNumber(basicImages)} Basic / ${formatNumber(premiumImages)} Premium Images`);
}
if (plan.daily_image_generation_limit) features.push(`${formatNumber(plan.daily_image_generation_limit)} Image Prompts`);
return features;
};
@@ -108,11 +82,11 @@ const transformPlanToPricingPlan = (plan: Plan, index: number, totalPlans: numbe
id: plan.id,
name: plan.name,
monthlyPrice: monthlyPrice,
price: monthlyPrice, // Will be calculated by component based on period
price: monthlyPrice,
period: '/month',
description: getPlanDescription(plan),
features: extractFeatures(plan),
buttonText: 'Choose Plan',
buttonText: monthlyPrice === 0 ? 'Start Free' : 'Select Plan',
highlighted: highlighted,
};
};

View File

@@ -985,6 +985,16 @@ export async function cancelSubscription(subscriptionId: number): Promise<{ mess
});
}
// ============================================================================
// PUBLIC PLANS (for marketing/pricing page)
// ============================================================================
export async function getPublicPlans(): Promise<Plan[]> {
// Use the existing auth/plans endpoint which already filters for public plans
const response = await fetchAPI('/v1/auth/plans/');
return response.results || response;
}
// ============================================================================
// USAGE SUMMARY (PLAN LIMITS)
// ============================================================================