diff --git a/backend/igny8_core/auth/admin.py b/backend/igny8_core/auth/admin.py index 3d80f73c..f8082a97 100644 --- a/backend/igny8_core/auth/admin.py +++ b/backend/igny8_core/auth/admin.py @@ -111,14 +111,15 @@ class AccountAdminForm(forms.ModelForm): @admin.register(Plan) class PlanAdmin(admin.ModelAdmin): """Plan admin - Global, no account filtering needed""" - list_display = ['name', 'slug', 'price', 'billing_cycle', 'max_sites', 'max_users', 'max_keywords', 'max_content_words', 'included_credits', 'is_active'] - list_filter = ['is_active', 'billing_cycle', 'is_internal'] + list_display = ['name', 'slug', 'price', 'billing_cycle', 'max_sites', 'max_users', 'max_keywords', 'max_content_words', 'included_credits', 'is_active', 'is_featured'] + list_filter = ['is_active', 'billing_cycle', 'is_internal', 'is_featured'] search_fields = ['name', 'slug'] readonly_fields = ['created_at'] fieldsets = ( ('Plan Info', { - 'fields': ('name', 'slug', 'price', 'billing_cycle', 'features', 'is_active', 'is_internal') + 'fields': ('name', 'slug', 'price', 'original_price', 'annual_discount_percent', 'billing_cycle', 'features', 'is_active', 'is_featured', 'is_internal'), + 'description': 'Price: Current price | Original Price: Crossed-out price (optional) | Annual Discount %: For annual billing | Is Featured: Show as popular/recommended plan' }), ('Account Management Limits', { 'fields': ('max_users', 'max_sites', 'max_industries', 'max_author_profiles'), diff --git a/backend/igny8_core/auth/migrations/0015_add_plan_original_price.py b/backend/igny8_core/auth/migrations/0015_add_plan_original_price.py new file mode 100644 index 00000000..7f303812 --- /dev/null +++ b/backend/igny8_core/auth/migrations/0015_add_plan_original_price.py @@ -0,0 +1,24 @@ +# Generated manually + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('igny8_core_auth', '0014_add_usage_tracking_to_account'), + ] + + operations = [ + migrations.AddField( + model_name='plan', + name='original_price', + field=models.DecimalField( + blank=True, + decimal_places=2, + help_text='Original price (before discount) - shows as crossed out price. Leave empty if no discount.', + max_digits=10, + null=True + ), + ), + ] diff --git a/backend/igny8_core/auth/models.py b/backend/igny8_core/auth/models.py index c9bae6dc..e70016b7 100644 --- a/backend/igny8_core/auth/models.py +++ b/backend/igny8_core/auth/models.py @@ -177,6 +177,13 @@ class Plan(models.Model): name = models.CharField(max_length=255) slug = models.SlugField(unique=True, max_length=255) price = models.DecimalField(max_digits=10, decimal_places=2) + original_price = models.DecimalField( + max_digits=10, + decimal_places=2, + null=True, + blank=True, + help_text="Original price (before discount) - shows as crossed out price. Leave empty if no discount." + ) billing_cycle = models.CharField(max_length=20, choices=BILLING_CYCLE_CHOICES, default='monthly') annual_discount_percent = models.DecimalField( max_digits=5, diff --git a/backend/igny8_core/auth/serializers.py b/backend/igny8_core/auth/serializers.py index 31ba7b17..7dde2a6b 100644 --- a/backend/igny8_core/auth/serializers.py +++ b/backend/igny8_core/auth/serializers.py @@ -10,7 +10,7 @@ class PlanSerializer(serializers.ModelSerializer): class Meta: model = Plan fields = [ - 'id', 'name', 'slug', 'price', 'billing_cycle', 'annual_discount_percent', + 'id', 'name', 'slug', 'price', 'original_price', 'billing_cycle', 'annual_discount_percent', 'is_featured', 'features', 'is_active', 'max_users', 'max_sites', 'max_industries', 'max_author_profiles', 'max_keywords', 'max_clusters', diff --git a/frontend/src/components/ui/pricing-table/PricingTable.tsx b/frontend/src/components/ui/pricing-table/PricingTable.tsx index 6c4b3dc6..ebeef4d0 100644 --- a/frontend/src/components/ui/pricing-table/PricingTable.tsx +++ b/frontend/src/components/ui/pricing-table/PricingTable.tsx @@ -142,7 +142,7 @@ export default function PricingTable({ 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" /> - Save 15% with annual billing + Save {plans[0]?.annualDiscountPercent || 15}% with annual billing )} diff --git a/frontend/src/components/ui/pricing-table/index.tsx b/frontend/src/components/ui/pricing-table/index.tsx index be846d3f..bd7258d3 100644 --- a/frontend/src/components/ui/pricing-table/index.tsx +++ b/frontend/src/components/ui/pricing-table/index.tsx @@ -5,20 +5,20 @@ import { useState } from 'react'; import { Check } from 'lucide-react'; -import Button from '../button/Button'; -import Badge from '../badge/Badge'; export interface PricingPlan { id: number; name: string; monthlyPrice: number; price: number; + originalPrice?: number; period: string; description: string; features: string[]; buttonText: string; highlighted?: boolean; disabled?: boolean; + annualDiscountPercent?: number; // Plan limits max_sites?: number; max_users?: number; @@ -44,7 +44,8 @@ export function PricingTable({ variant = '1', title, plans, showToggle = false, const getPrice = (plan: PricingPlan) => { if (billingPeriod === 'annual') { - return (plan.monthlyPrice * 12 * 0.8).toFixed(0); // 20% discount for annual + const discount = plan.annualDiscountPercent || 20; + return (plan.monthlyPrice * 12 * (100 - discount) / 100).toFixed(0); } return plan.monthlyPrice.toFixed(0); }; @@ -63,30 +64,34 @@ export function PricingTable({ variant = '1', title, plans, showToggle = false, {showToggle && (
-
- - +
+
+ + +
+ {billingPeriod === 'annual' && ( + + Save {Math.round(plans[0]?.annualDiscountPercent || 20)}% + + )}
)} @@ -97,15 +102,15 @@ export function PricingTable({ variant = '1', title, plans, showToggle = false, key={plan.id} className={`relative rounded-lg border ${ plan.highlighted - ? 'border-primary shadow-lg ring-2 ring-primary ring-opacity-50' + ? 'pricing-card-featured border-gray-700 shadow-lg ring-2 ring-gray-700' : 'border-gray-200 dark:border-gray-700' - } bg-white dark:bg-gray-800 p-6 flex flex-col`} + } ${plan.highlighted ? '' : 'bg-white dark:bg-gray-800'} p-6 flex flex-col`} > {plan.highlighted && (
- + Popular - +
)} @@ -115,15 +120,20 @@ export function PricingTable({ variant = '1', title, plans, showToggle = false,
-
- +
+ ${getPrice(plan)} - {getPeriod()} + {getPeriod()} + {plan.originalPrice && billingPeriod === 'monthly' && ( + + ${plan.originalPrice.toFixed(2)} + + )}
{billingPeriod === 'annual' && plan.monthlyPrice > 0 && (

- Billed ${(plan.monthlyPrice * 12 * 0.8).toFixed(0)}/year + Billed ${(plan.monthlyPrice * 12 * (100 - (plan.annualDiscountPercent || 20)) / 100).toFixed(0)}/year

)}
@@ -192,14 +202,13 @@ export function PricingTable({ variant = '1', title, plans, showToggle = false, )} - +
))} diff --git a/frontend/src/components/ui/pricing-table/pricing-table-1.tsx b/frontend/src/components/ui/pricing-table/pricing-table-1.tsx new file mode 100644 index 00000000..c22cea13 --- /dev/null +++ b/frontend/src/components/ui/pricing-table/pricing-table-1.tsx @@ -0,0 +1,25 @@ +import { PricingTable, PricingPlan } from "./index"; + +interface PricingTable1Props { + plans: PricingPlan[]; + title?: string; + showToggle?: boolean; + onPlanSelect?: (plan: PricingPlan) => void; +} + +export default function PricingTable1({ + plans, + title = "Flexible Plans Tailored to Fit Your Unique Needs!", + showToggle = true, + onPlanSelect +}: PricingTable1Props) { + return ( + + ); +} diff --git a/frontend/src/marketing/pages/Pricing.tsx b/frontend/src/marketing/pages/Pricing.tsx index 835a803b..dbb99e50 100644 --- a/frontend/src/marketing/pages/Pricing.tsx +++ b/frontend/src/marketing/pages/Pricing.tsx @@ -10,7 +10,8 @@ import { 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 { 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 { @@ -47,13 +48,25 @@ const convertToPricingPlan = (plan: Plan): PricingPlan => { 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_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, name: plan.name, @@ -61,9 +74,9 @@ const convertToPricingPlan = (plan: Plan): PricingPlan => { price: monthlyPrice, annualDiscountPercent: plan.annual_discount_percent || 15, period: '/month', - description: `Perfect for ${plan.name.toLowerCase()} needs`, + description: description, features, - buttonText: monthlyPrice === 0 ? 'Start Free' : 'Choose Plan', + buttonText: monthlyPrice === 0 ? 'Free Trial' : 'Choose Plan', highlighted: plan.is_featured || false, }; }; @@ -190,21 +203,148 @@ const Pricing: React.FC = () => { return ( <> +
{/* PRICING HERO SECTION */}
-
+
Pricing -

+

Simple plans that scale with your automation goals.

-

- Flexible pricing for teams of all sizes. No seat limits. No hidden charges. Built for growth. -

@@ -232,27 +372,25 @@ const Pricing: React.FC = () => { )} - {/* PRICING TIERS SECTION */} - {!loading && !error && ( -
- { - const plan = plans.find(p => p.id === pricingPlan.id); - if (plan) { - window.location.href = `https://app.igny8.com/signup?plan=${plan.slug}`; - } - }} - /> -
+ {/* PRICING TABLES SECTION - Dynamic Backend Plans */} + {!loading && !error && plans.length > 0 && ( +
+
+ { + window.location.href = `/signup?plan=${plan.id}`; + }} + /> +
+
)} {/* COMPARISON TABLE SECTION - Keep hardcoded for now */} {!loading && !error && ( -
+

Compare plan capabilities

diff --git a/frontend/src/pages/Settings/Plans.tsx b/frontend/src/pages/Settings/Plans.tsx index 74398316..7e26395e 100644 --- a/frontend/src/pages/Settings/Plans.tsx +++ b/frontend/src/pages/Settings/Plans.tsx @@ -2,7 +2,8 @@ import { useState, useEffect } from 'react'; import PageMeta from '../../components/common/PageMeta'; import { useToast } from '../../components/ui/toast/ToastContainer'; import { fetchAPI } from '../../services/api'; -import { PricingTable, PricingPlan } from '../../components/ui/pricing-table'; +import { PricingPlan } from '../../components/ui/pricing-table'; +import PricingTable1 from '../../components/ui/pricing-table/pricing-table-1'; interface Plan { id: number; @@ -86,7 +87,7 @@ const transformPlanToPricingPlan = (plan: Plan, index: number, totalPlans: numbe period: '/month', description: getPlanDescription(plan), features: extractFeatures(plan), - buttonText: monthlyPrice === 0 ? 'Start Free' : 'Select Plan', + buttonText: monthlyPrice === 0 ? 'Free Trial' : 'Choose Plan', highlighted: highlighted, }; }; @@ -94,14 +95,16 @@ const transformPlanToPricingPlan = (plan: Plan, index: number, totalPlans: numbe // Get plan description based on plan name or features const getPlanDescription = (plan: Plan): string => { const slug = plan.slug.toLowerCase(); - if (slug.includes('free')) { - return 'Perfect for getting started'; - } else if (slug.includes('starter')) { - return 'For solo designers & freelancers'; - } else if (slug.includes('growth')) { - return 'For growing businesses'; - } else if (slug.includes('scale') || slug.includes('enterprise')) { - return 'For teams and large organizations'; + const name = plan.name.toLowerCase(); + + if (slug.includes('free') || name.includes('free')) { + return 'Explore core features risk free'; + } else if (slug.includes('starter') || name.includes('starter')) { + return 'Launch SEO workflows for small teams'; + } else if (slug.includes('growth') || name.includes('growth')) { + return 'Scale content production with confidence'; + } else if (slug.includes('scale') || slug.includes('enterprise') || name.includes('scale') || name.includes('enterprise')) { + return 'Enterprise power for high volume growth'; } return 'Choose the perfect plan for your needs'; }; @@ -165,8 +168,7 @@ export default function Plans() {
) : ( <> - ( @@ -21,9 +44,95 @@ const StarIcon = () => ( ); +const formatNumber = (num: number | undefined | null): string => { + if (!num || num === 0) return '0'; + if (num >= 1000000) return `${(num / 1000000).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`); + + // 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, + 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, + period: '/month', + description: description, + features, + buttonText: monthlyPrice === 0 ? 'Free Trial' : 'Choose Plan', + highlighted: plan.is_featured || false, + annualDiscountPercent: plan.annual_discount_percent || 15, + }; +}; + export default function PricingTablePage() { + const [backendPlans, setBackendPlans] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + useEffect(() => { + const fetchPlans = async () => { + try { + const data = await getPublicPlans(); + setBackendPlans(data); + setLoading(false); + } catch (err) { + console.error('Error fetching plans:', err); + setError('Failed to load plans'); + setLoading(false); + } + }; + fetchPlans(); + }, []); + // Sample plans for variant 1 const plans1: PricingPlan[] = [ + { + id: 0, + name: 'Free Plan', + price: 0.00, + period: '/month', + description: 'Perfect for free plan needs', + features: [ + '1 Site', + '1 Team User', + '1K Monthly Credits', + '100K Words/Month', + '100 AI Keyword Clusters', + '300 Content Ideas', + '300 Basic / 60 Premium Images', + '300 Image Prompts', + ], + buttonText: 'Start Free', + }, { id: 1, name: 'Starter', @@ -205,10 +314,29 @@ export default function PricingTablePage() { description="This is React.js Pricing Tables page for TailAdmin - React.js Tailwind CSS Admin Dashboard Template" />
+ + {loading && ( +
+
+

Loading backend plans...

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

{error}

+
+ )} + {!loading && !error && backendPlans.length > 0 && ( + console.log('Selected backend plan:', plan)} + /> + )} +
+ - console.log('Selected plan:', plan)} diff --git a/frontend/src/pages/account/PlansAndBillingPage.tsx b/frontend/src/pages/account/PlansAndBillingPage.tsx index e0ead5c5..39dd9493 100644 --- a/frontend/src/pages/account/PlansAndBillingPage.tsx +++ b/frontend/src/pages/account/PlansAndBillingPage.tsx @@ -12,7 +12,8 @@ import { Card } from '../../components/ui/card'; import Badge from '../../components/ui/badge/Badge'; import Button from '../../components/ui/button/Button'; import { useToast } from '../../components/ui/toast/ToastContainer'; -import { PricingTable } from '../../components/ui/pricing-table'; +import { PricingPlan } from '../../components/ui/pricing-table'; +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'; @@ -716,9 +717,9 @@ export default function PlansAndBillingPage() { Select the plan that best fits your needs

-
- + { // Only show paid plans (exclude Free Plan) @@ -728,29 +729,42 @@ export default function PlansAndBillingPage() { }) .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: `/${plan.interval || 'month'}`, - description: plan.description || 'Standard plan', - features: plan.features && plan.features.length > 0 - ? plan.features - : ['Monthly credits included', 'Module access', 'Email support'], - buttonText: plan.id === currentPlanId ? 'Current Plan' : 'Select Plan', + 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, - max_sites: plan.max_sites, - max_users: plan.max_users, - max_keywords: plan.max_keywords, - max_clusters: plan.max_clusters, - max_content_ideas: plan.max_content_ideas, - max_content_words: plan.max_content_words, - max_images_basic: plan.max_images_basic, - max_images_premium: plan.max_images_premium, - included_credits: plan.included_credits, }; })} showToggle={true} diff --git a/frontend/src/services/billing.api.ts b/frontend/src/services/billing.api.ts index 21dc8754..72995da8 100644 --- a/frontend/src/services/billing.api.ts +++ b/frontend/src/services/billing.api.ts @@ -856,6 +856,7 @@ export interface Plan { name: string; slug?: string; price?: number | string; + original_price?: number; currency?: string; interval?: 'month' | 'year'; description?: string;