Files
igny8/frontend/src/pages/Settings/Plans.tsx
2025-11-09 10:27:02 +00:00

214 lines
6.7 KiB
TypeScript

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';
interface Plan {
id: number;
name: string;
slug: string;
price: string | number;
billing_cycle: string;
is_active: boolean;
max_users: number;
max_sites: number;
max_keywords: number;
max_clusters: number;
max_content_ideas: number;
monthly_word_count_limit: number;
monthly_ai_credit_limit: number;
monthly_image_count: number;
daily_content_tasks: number;
daily_ai_request_limit: number;
daily_image_generation_limit: number;
included_credits: number;
image_model_choices: string[];
features: string[];
}
interface PlanResponse {
count: number;
next: string | null;
previous: string | null;
results: Plan[];
}
// Helper function to format numbers with commas
const formatNumber = (num: number): string => {
return num.toLocaleString();
};
// Helper function to format word count
const formatWordCount = (num: number): string => {
if (num >= 1000000) {
return `${(num / 1000000).toFixed(1)}M`;
}
if (num >= 1000) {
return `${(num / 1000).toFixed(0)}K`;
}
return num.toString();
};
// Extract major features from plan data
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');
}
}
return features;
};
// Transform Plan to PricingPlan
const transformPlanToPricingPlan = (plan: Plan, index: number, totalPlans: number): PricingPlan => {
const monthlyPrice = typeof plan.price === 'number' ? plan.price : parseFloat(String(plan.price || 0));
// Only highlight Growth plan (by slug)
const highlighted = plan.slug.toLowerCase() === 'growth';
return {
id: plan.id,
name: plan.name,
monthlyPrice: monthlyPrice,
price: monthlyPrice, // Will be calculated by component based on period
period: '/month',
description: getPlanDescription(plan),
features: extractFeatures(plan),
buttonText: 'Choose Plan',
highlighted: highlighted,
};
};
// 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';
}
return 'Choose the perfect plan for your needs';
};
export default function Plans() {
const toast = useToast();
const [plans, setPlans] = useState<Plan[]>([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
loadPlans();
}, []);
const loadPlans = async () => {
try {
setLoading(true);
const response: PlanResponse = await fetchAPI('/v1/auth/plans/');
// Filter only active plans and sort by price
const activePlans = (response.results || [])
.filter((plan) => plan.is_active)
.sort((a, b) => {
const priceA = typeof a.price === 'number' ? a.price : parseFloat(String(a.price || 0));
const priceB = typeof b.price === 'number' ? b.price : parseFloat(String(b.price || 0));
return priceA - priceB;
});
setPlans(activePlans);
} catch (error: any) {
toast.error(`Failed to load plans: ${error.message}`);
} finally {
setLoading(false);
}
};
const handlePlanSelect = (plan: PricingPlan) => {
console.log('Selected plan:', plan);
// TODO: Implement plan selection/subscription logic
toast.success(`Selected plan: ${plan.name}`);
};
const pricingPlans: PricingPlan[] = plans.map((plan, index) =>
transformPlanToPricingPlan(plan, index, plans.length)
);
return (
<div className="p-6">
<PageMeta title="Plans" />
<div className="mb-6">
<h1 className="text-2xl font-bold text-gray-900 dark:text-white">Plans</h1>
<p className="text-gray-600 dark:text-gray-400 mt-1">
Choose the perfect plan for your needs. All plans include our core features.
</p>
</div>
{loading ? (
<div className="flex items-center justify-center h-64">
<div className="text-gray-500">Loading plans...</div>
</div>
) : pricingPlans.length === 0 ? (
<div className="flex items-center justify-center h-64">
<div className="text-gray-500">No active plans available</div>
</div>
) : (
<>
<PricingTable
variant="1"
title="Flexible Plans Tailored to Fit Your Unique Needs!"
plans={pricingPlans}
showToggle={true}
onPlanSelect={handlePlanSelect}
/>
{/* Future: Add "View All Features" section here */}
<div className="mt-8 text-center">
<p className="text-sm text-gray-500 dark:text-gray-400">
Need more details? View all features and limits for each plan.
</p>
{/* TODO: Add expandable feature list component */}
</div>
</>
)}
</div>
);
}