From db1fd2fff86ccbf3e46ab0d51b794229e19213c1 Mon Sep 17 00:00:00 2001 From: "IGNY8 VPS (Salman)" Date: Sat, 13 Dec 2025 13:43:55 +0000 Subject: [PATCH] nbcvhc --- .../ui/pricing-table/PricingTable.tsx | 43 ++- frontend/src/marketing/pages/Pricing.tsx | 296 +++++++----------- frontend/src/pages/Settings/Plans.tsx | 58 +--- frontend/src/services/billing.api.ts | 10 + 4 files changed, 175 insertions(+), 232 deletions(-) diff --git a/frontend/src/components/ui/pricing-table/PricingTable.tsx b/frontend/src/components/ui/pricing-table/PricingTable.tsx index 69b08fdd..6c4b3dc6 100644 --- a/frontend/src/components/ui/pricing-table/PricingTable.tsx +++ b/frontend/src/components/ui/pricing-table/PricingTable.tsx @@ -92,41 +92,60 @@ export default function PricingTable({ return (
{title && ( -
-

+
+

{title}

)} {showToggle && ( -
-
+
+
+ {billingPeriod === 'annually' && ( +
+ + + + + Save 15% with annual billing + +
+ )}
)}
@@ -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')}
); diff --git a/frontend/src/marketing/pages/Pricing.tsx b/frontend/src/marketing/pages/Pricing.tsx index 272dfdd4..835a803b 100644 --- a/frontend/src/marketing/pages/Pricing.tsx +++ b/frontend/src/marketing/pages/Pricing.tsx @@ -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 ( - - {cta.label} - - ); - } - - return ( - - {cta.label} - - ); - }; + const [plans, setPlans] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(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 = () => {
+ {/* LOADING/ERROR STATES */} + {loading && ( +
+
+
+

Loading pricing plans...

+
+
+ )} + + {error && ( +
+
+

{error}

+ +
+
+ )} + {/* PRICING TIERS SECTION */} -
-
- {tiers.map((tier) => { - const Icon = tier.icon; - return ( -
- {/* Badge and Icon - Aligned */} -
- {/* Icon */} -
- -
- - {/* Badge and Plan Name */} -
- {tier.badge && ( - - {tier.badge} - - )} -

{tier.name}

-
-
- - {/* Plan Description */} -

{tier.description}

- - {/* Price */} -
- {tier.price} - {tier.cadence && ( - - {tier.cadence} - - )} -
- - {/* Features List */} -
    - {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 ( -
  • - - {feature} -
  • - ); - })} -
- - {/* CTA Button */} - -
- ); - })} -
+ {!loading && !error && ( +
+ { + const plan = plans.find(p => p.id === pricingPlan.id); + if (plan) { + window.location.href = `https://app.igny8.com/signup?plan=${plan.slug}`; + } + }} + />
+ )} - {/* COMPARISON TABLE SECTION */} + {/* COMPARISON TABLE SECTION - Keep hardcoded for now */} + {!loading && !error && (

Compare plan capabilities @@ -399,6 +338,7 @@ const Pricing: React.FC = () => {

+ )} {/* INFO BLOCKS SECTION */}
diff --git a/frontend/src/pages/Settings/Plans.tsx b/frontend/src/pages/Settings/Plans.tsx index 246bfb4f..74398316 100644 --- a/frontend/src/pages/Settings/Plans.tsx +++ b/frontend/src/pages/Settings/Plans.tsx @@ -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, }; }; diff --git a/frontend/src/services/billing.api.ts b/frontend/src/services/billing.api.ts index 17a76d33..21dc8754 100644 --- a/frontend/src/services/billing.api.ts +++ b/frontend/src/services/billing.api.ts @@ -985,6 +985,16 @@ export async function cancelSubscription(subscriptionId: number): Promise<{ mess }); } +// ============================================================================ +// PUBLIC PLANS (for marketing/pricing page) +// ============================================================================ + +export async function getPublicPlans(): Promise { + // 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) // ============================================================================