214 lines
6.7 KiB
TypeScript
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>
|
|
);
|
|
}
|