Phase 3 - credts, usage, plans app pages #Migrations
This commit is contained in:
@@ -22,7 +22,6 @@ export default function ProtectedRoute({ children }: ProtectedRouteProps) {
|
||||
|
||||
const PLAN_ALLOWED_PATHS = [
|
||||
'/account/plans',
|
||||
'/account/purchase-credits',
|
||||
'/account/settings',
|
||||
'/account/team',
|
||||
'/account/usage',
|
||||
|
||||
@@ -24,7 +24,7 @@ interface Plan {
|
||||
max_users: number;
|
||||
max_sites: number;
|
||||
max_keywords: number;
|
||||
monthly_word_count_limit: number;
|
||||
max_ahrefs_queries: number;
|
||||
included_credits: number;
|
||||
features: string[];
|
||||
}
|
||||
@@ -260,8 +260,7 @@ export default function SignUpFormUnified({
|
||||
features.push(`${plan.max_sites} ${plan.max_sites === 1 ? 'Site' : 'Sites'}`);
|
||||
features.push(`${plan.max_users} ${plan.max_users === 1 ? 'User' : 'Users'}`);
|
||||
features.push(`${formatNumber(plan.max_keywords || 0)} Keywords`);
|
||||
features.push(`${formatNumber(plan.monthly_word_count_limit || 0)} Words/Month`);
|
||||
features.push(`${formatNumber(plan.included_credits || 0)} AI Credits`);
|
||||
features.push(`${formatNumber(plan.included_credits || 0)} Credits/Month`);
|
||||
return features;
|
||||
};
|
||||
|
||||
|
||||
450
frontend/src/components/billing/CreditInsightsCharts.tsx
Normal file
450
frontend/src/components/billing/CreditInsightsCharts.tsx
Normal file
@@ -0,0 +1,450 @@
|
||||
/**
|
||||
* Credit Insights Charts Component
|
||||
* Displays credit usage analytics with visual charts
|
||||
* - Donut chart: Credits by operation type
|
||||
* - Line chart: Daily credit usage timeline
|
||||
* - Bar chart: Top credit-consuming operations
|
||||
*/
|
||||
|
||||
import Chart from 'react-apexcharts';
|
||||
import { ApexOptions } from 'apexcharts';
|
||||
import { Card } from '../ui/card';
|
||||
import { ActivityIcon, TrendingUpIcon, PieChartIcon, BarChart3Icon } from '../../icons';
|
||||
import type { UsageAnalytics } from '../../services/billing.api';
|
||||
|
||||
interface CreditInsightsChartsProps {
|
||||
analytics: UsageAnalytics | null;
|
||||
loading?: boolean;
|
||||
period: number;
|
||||
}
|
||||
|
||||
// Friendly names for operation types
|
||||
const OPERATION_LABELS: Record<string, string> = {
|
||||
content_generation: 'Content Generation',
|
||||
image_generation: 'Image Generation',
|
||||
keyword_clustering: 'Keyword Clustering',
|
||||
content_analysis: 'Content Analysis',
|
||||
subscription: 'Subscription',
|
||||
purchase: 'Credit Purchase',
|
||||
refund: 'Refund',
|
||||
adjustment: 'Adjustment',
|
||||
grant: 'Credit Grant',
|
||||
deduction: 'Deduction',
|
||||
};
|
||||
|
||||
const CHART_COLORS = [
|
||||
'var(--color-brand-500)',
|
||||
'var(--color-purple-500)',
|
||||
'var(--color-success-500)',
|
||||
'var(--color-warning-500)',
|
||||
'var(--color-error-500)',
|
||||
'var(--color-info-500)',
|
||||
'#6366f1', // indigo
|
||||
'#ec4899', // pink
|
||||
'#14b8a6', // teal
|
||||
'#f97316', // orange
|
||||
];
|
||||
|
||||
export default function CreditInsightsCharts({ analytics, loading, period }: CreditInsightsChartsProps) {
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||
{[1, 2, 3, 4].map((i) => (
|
||||
<Card key={i} className="p-6 animate-pulse">
|
||||
<div className="h-8 bg-gray-200 dark:bg-gray-700 rounded w-1/3 mb-4"></div>
|
||||
<div className="h-64 bg-gray-200 dark:bg-gray-700 rounded"></div>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!analytics) {
|
||||
return (
|
||||
<Card className="p-6">
|
||||
<div className="text-center py-12 text-gray-500 dark:text-gray-400">
|
||||
<PieChartIcon className="w-12 h-12 mx-auto mb-3 opacity-30" />
|
||||
<p>No analytics data available</p>
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
// Prepare data for donut chart (credits by operation type)
|
||||
const usageByType = analytics.usage_by_type.filter(item => Math.abs(item.total) > 0);
|
||||
const donutLabels = usageByType.map(item => OPERATION_LABELS[item.transaction_type] || item.transaction_type.replace(/_/g, ' '));
|
||||
const donutSeries = usageByType.map(item => Math.abs(item.total));
|
||||
|
||||
const donutOptions: ApexOptions = {
|
||||
chart: {
|
||||
type: 'donut',
|
||||
fontFamily: 'Outfit, sans-serif',
|
||||
},
|
||||
labels: donutLabels,
|
||||
colors: CHART_COLORS.slice(0, donutLabels.length),
|
||||
legend: {
|
||||
position: 'bottom',
|
||||
fontFamily: 'Outfit',
|
||||
labels: {
|
||||
colors: 'var(--color-gray-600)',
|
||||
},
|
||||
},
|
||||
plotOptions: {
|
||||
pie: {
|
||||
donut: {
|
||||
size: '65%',
|
||||
labels: {
|
||||
show: true,
|
||||
name: {
|
||||
show: true,
|
||||
fontSize: '14px',
|
||||
fontFamily: 'Outfit',
|
||||
color: 'var(--color-gray-600)',
|
||||
},
|
||||
value: {
|
||||
show: true,
|
||||
fontSize: '24px',
|
||||
fontFamily: 'Outfit',
|
||||
fontWeight: 600,
|
||||
color: 'var(--color-gray-900)',
|
||||
formatter: (val: string) => parseInt(val).toLocaleString(),
|
||||
},
|
||||
total: {
|
||||
show: true,
|
||||
label: 'Total Credits',
|
||||
fontSize: '14px',
|
||||
fontFamily: 'Outfit',
|
||||
color: 'var(--color-gray-600)',
|
||||
formatter: () => analytics.total_usage.toLocaleString(),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
dataLabels: {
|
||||
enabled: false,
|
||||
},
|
||||
tooltip: {
|
||||
y: {
|
||||
formatter: (val: number) => `${val.toLocaleString()} credits`,
|
||||
},
|
||||
},
|
||||
responsive: [
|
||||
{
|
||||
breakpoint: 480,
|
||||
options: {
|
||||
chart: {
|
||||
width: 300,
|
||||
},
|
||||
legend: {
|
||||
position: 'bottom',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
// Prepare data for timeline chart (daily usage)
|
||||
const dailyData = analytics.daily_usage || [];
|
||||
const timelineCategories = dailyData.map(d => {
|
||||
const date = new Date(d.date);
|
||||
return date.toLocaleDateString('en-US', { month: 'short', day: 'numeric' });
|
||||
});
|
||||
const usageSeries = dailyData.map(d => Math.abs(d.usage));
|
||||
const purchasesSeries = dailyData.map(d => d.purchases);
|
||||
|
||||
const timelineOptions: ApexOptions = {
|
||||
chart: {
|
||||
type: 'area',
|
||||
fontFamily: 'Outfit, sans-serif',
|
||||
height: 300,
|
||||
toolbar: {
|
||||
show: false,
|
||||
},
|
||||
zoom: {
|
||||
enabled: false,
|
||||
},
|
||||
},
|
||||
colors: ['var(--color-brand-500)', 'var(--color-success-500)'],
|
||||
dataLabels: {
|
||||
enabled: false,
|
||||
},
|
||||
stroke: {
|
||||
curve: 'smooth',
|
||||
width: 2,
|
||||
},
|
||||
fill: {
|
||||
type: 'gradient',
|
||||
gradient: {
|
||||
shadeIntensity: 1,
|
||||
opacityFrom: 0.4,
|
||||
opacityTo: 0.1,
|
||||
stops: [0, 90, 100],
|
||||
},
|
||||
},
|
||||
xaxis: {
|
||||
categories: timelineCategories,
|
||||
axisBorder: {
|
||||
show: false,
|
||||
},
|
||||
axisTicks: {
|
||||
show: false,
|
||||
},
|
||||
labels: {
|
||||
style: {
|
||||
colors: 'var(--color-gray-500)',
|
||||
fontFamily: 'Outfit',
|
||||
},
|
||||
rotate: -45,
|
||||
rotateAlways: dailyData.length > 14,
|
||||
},
|
||||
},
|
||||
yaxis: {
|
||||
labels: {
|
||||
style: {
|
||||
colors: 'var(--color-gray-500)',
|
||||
fontFamily: 'Outfit',
|
||||
},
|
||||
formatter: (val: number) => val.toLocaleString(),
|
||||
},
|
||||
},
|
||||
grid: {
|
||||
borderColor: 'var(--color-gray-200)',
|
||||
strokeDashArray: 4,
|
||||
},
|
||||
legend: {
|
||||
position: 'top',
|
||||
horizontalAlign: 'right',
|
||||
fontFamily: 'Outfit',
|
||||
labels: {
|
||||
colors: 'var(--color-gray-600)',
|
||||
},
|
||||
},
|
||||
tooltip: {
|
||||
y: {
|
||||
formatter: (val: number) => `${val.toLocaleString()} credits`,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const timelineSeries = [
|
||||
{ name: 'Credits Used', data: usageSeries },
|
||||
{ name: 'Credits Added', data: purchasesSeries },
|
||||
];
|
||||
|
||||
// Prepare data for bar chart (top operations by count)
|
||||
const operationsByCount = [...analytics.usage_by_type]
|
||||
.filter(item => item.count > 0)
|
||||
.sort((a, b) => b.count - a.count)
|
||||
.slice(0, 8);
|
||||
|
||||
const barCategories = operationsByCount.map(item =>
|
||||
OPERATION_LABELS[item.transaction_type] || item.transaction_type.replace(/_/g, ' ')
|
||||
);
|
||||
const barSeries = operationsByCount.map(item => item.count);
|
||||
|
||||
const barOptions: ApexOptions = {
|
||||
chart: {
|
||||
type: 'bar',
|
||||
fontFamily: 'Outfit, sans-serif',
|
||||
height: 300,
|
||||
toolbar: {
|
||||
show: false,
|
||||
},
|
||||
},
|
||||
colors: ['var(--color-purple-500)'],
|
||||
plotOptions: {
|
||||
bar: {
|
||||
horizontal: true,
|
||||
borderRadius: 4,
|
||||
barHeight: '60%',
|
||||
},
|
||||
},
|
||||
dataLabels: {
|
||||
enabled: false,
|
||||
},
|
||||
xaxis: {
|
||||
categories: barCategories,
|
||||
labels: {
|
||||
style: {
|
||||
colors: 'var(--color-gray-500)',
|
||||
fontFamily: 'Outfit',
|
||||
},
|
||||
},
|
||||
},
|
||||
yaxis: {
|
||||
labels: {
|
||||
style: {
|
||||
colors: 'var(--color-gray-600)',
|
||||
fontFamily: 'Outfit',
|
||||
},
|
||||
},
|
||||
},
|
||||
grid: {
|
||||
borderColor: 'var(--color-gray-200)',
|
||||
strokeDashArray: 4,
|
||||
},
|
||||
tooltip: {
|
||||
y: {
|
||||
formatter: (val: number) => `${val.toLocaleString()} operations`,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
// Summary stats
|
||||
const avgDailyUsage = dailyData.length > 0
|
||||
? Math.round(dailyData.reduce((sum, d) => sum + Math.abs(d.usage), 0) / dailyData.length)
|
||||
: 0;
|
||||
|
||||
const peakUsage = dailyData.length > 0
|
||||
? Math.max(...dailyData.map(d => Math.abs(d.usage)))
|
||||
: 0;
|
||||
|
||||
const topOperation = usageByType.length > 0
|
||||
? usageByType.reduce((max, item) => Math.abs(item.total) > Math.abs(max.total) ? item : max, usageByType[0])
|
||||
: null;
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* Summary Stats */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
<Card className="p-4 bg-white dark:bg-gray-900 border border-gray-200 dark:border-gray-800">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="p-2 bg-brand-100 dark:bg-brand-900/30 rounded-lg">
|
||||
<TrendingUpIcon className="w-5 h-5 text-brand-600 dark:text-brand-400" />
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-xs text-gray-600 dark:text-gray-400">Avg Daily Usage</div>
|
||||
<div className="text-xl font-bold text-brand-600 dark:text-brand-400">
|
||||
{avgDailyUsage.toLocaleString()}
|
||||
</div>
|
||||
<div className="text-xs text-gray-500 dark:text-gray-400">credits/day</div>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Card className="p-4 bg-white dark:bg-gray-900 border border-gray-200 dark:border-gray-800">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="p-2 bg-purple-100 dark:bg-purple-900/30 rounded-lg">
|
||||
<BarChart3Icon className="w-5 h-5 text-purple-600 dark:text-purple-400" />
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-xs text-gray-600 dark:text-gray-400">Peak Usage</div>
|
||||
<div className="text-xl font-bold text-purple-600 dark:text-purple-400">
|
||||
{peakUsage.toLocaleString()}
|
||||
</div>
|
||||
<div className="text-xs text-gray-500 dark:text-gray-400">credits in one day</div>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Card className="p-4 bg-white dark:bg-gray-900 border border-gray-200 dark:border-gray-800">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="p-2 bg-success-100 dark:bg-success-900/30 rounded-lg">
|
||||
<ActivityIcon className="w-5 h-5 text-success-600 dark:text-success-400" />
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-xs text-gray-600 dark:text-gray-400">Top Operation</div>
|
||||
<div className="text-base font-bold text-success-600 dark:text-success-400 truncate max-w-[150px]">
|
||||
{topOperation
|
||||
? (OPERATION_LABELS[topOperation.transaction_type] || topOperation.transaction_type.replace(/_/g, ' '))
|
||||
: 'N/A'
|
||||
}
|
||||
</div>
|
||||
<div className="text-xs text-gray-500 dark:text-gray-400">
|
||||
{topOperation ? `${Math.abs(topOperation.total).toLocaleString()} credits` : ''}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Charts Grid */}
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||
{/* Usage by Type - Donut Chart */}
|
||||
<Card className="p-6">
|
||||
<div className="flex items-center gap-3 mb-4">
|
||||
<div className="p-2 bg-brand-100 dark:bg-brand-900/30 rounded-lg">
|
||||
<PieChartIcon className="w-5 h-5 text-brand-600 dark:text-brand-400" />
|
||||
</div>
|
||||
<h3 className="text-lg font-semibold text-gray-900 dark:text-white">
|
||||
Credits by Type
|
||||
</h3>
|
||||
</div>
|
||||
{donutSeries.length > 0 ? (
|
||||
<Chart
|
||||
options={donutOptions}
|
||||
series={donutSeries}
|
||||
type="donut"
|
||||
height={320}
|
||||
/>
|
||||
) : (
|
||||
<div className="flex items-center justify-center h-64 text-gray-500 dark:text-gray-400">
|
||||
<div className="text-center">
|
||||
<PieChartIcon className="w-12 h-12 mx-auto mb-2 opacity-30" />
|
||||
<p>No usage data for this period</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Card>
|
||||
|
||||
{/* Operations by Count - Bar Chart */}
|
||||
<Card className="p-6">
|
||||
<div className="flex items-center gap-3 mb-4">
|
||||
<div className="p-2 bg-purple-100 dark:bg-purple-900/30 rounded-lg">
|
||||
<BarChart3Icon className="w-5 h-5 text-purple-600 dark:text-purple-400" />
|
||||
</div>
|
||||
<h3 className="text-lg font-semibold text-gray-900 dark:text-white">
|
||||
Operations Count
|
||||
</h3>
|
||||
</div>
|
||||
{barSeries.length > 0 ? (
|
||||
<Chart
|
||||
options={barOptions}
|
||||
series={[{ name: 'Operations', data: barSeries }]}
|
||||
type="bar"
|
||||
height={320}
|
||||
/>
|
||||
) : (
|
||||
<div className="flex items-center justify-center h-64 text-gray-500 dark:text-gray-400">
|
||||
<div className="text-center">
|
||||
<BarChart3Icon className="w-12 h-12 mx-auto mb-2 opacity-30" />
|
||||
<p>No operations in this period</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Daily Timeline - Full Width */}
|
||||
<Card className="p-6">
|
||||
<div className="flex items-center gap-3 mb-4">
|
||||
<div className="p-2 bg-success-100 dark:bg-success-900/30 rounded-lg">
|
||||
<TrendingUpIcon className="w-5 h-5 text-success-600 dark:text-success-400" />
|
||||
</div>
|
||||
<h3 className="text-lg font-semibold text-gray-900 dark:text-white">
|
||||
Credit Activity Timeline
|
||||
</h3>
|
||||
<span className="text-sm text-gray-500 dark:text-gray-400">
|
||||
Last {period} days
|
||||
</span>
|
||||
</div>
|
||||
{dailyData.length > 0 ? (
|
||||
<Chart
|
||||
options={timelineOptions}
|
||||
series={timelineSeries}
|
||||
type="area"
|
||||
height={300}
|
||||
/>
|
||||
) : (
|
||||
<div className="flex items-center justify-center h-64 text-gray-500 dark:text-gray-400">
|
||||
<div className="text-center">
|
||||
<TrendingUpIcon className="w-12 h-12 mx-auto mb-2 opacity-30" />
|
||||
<p>No daily activity data available</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
168
frontend/src/components/billing/InsufficientCreditsModal.tsx
Normal file
168
frontend/src/components/billing/InsufficientCreditsModal.tsx
Normal file
@@ -0,0 +1,168 @@
|
||||
/**
|
||||
* Insufficient Credits Modal
|
||||
* Shows when user doesn't have enough credits for an operation
|
||||
* Provides options to upgrade plan or buy credits
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { Modal } from '../ui/modal';
|
||||
import Button from '../ui/button/Button';
|
||||
import { ZapIcon, TrendingUpIcon, CreditCardIcon } from '../../icons';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
|
||||
interface InsufficientCreditsModalProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
requiredCredits: number;
|
||||
availableCredits: number;
|
||||
operationType?: string;
|
||||
}
|
||||
|
||||
export default function InsufficientCreditsModal({
|
||||
isOpen,
|
||||
onClose,
|
||||
requiredCredits,
|
||||
availableCredits,
|
||||
operationType = 'this operation',
|
||||
}: InsufficientCreditsModalProps) {
|
||||
const navigate = useNavigate();
|
||||
const shortfall = requiredCredits - availableCredits;
|
||||
|
||||
const handleUpgradePlan = () => {
|
||||
onClose();
|
||||
navigate('/account/billing/upgrade');
|
||||
};
|
||||
|
||||
const handleBuyCredits = () => {
|
||||
onClose();
|
||||
navigate('/account/billing/credits');
|
||||
};
|
||||
|
||||
const handleViewUsage = () => {
|
||||
onClose();
|
||||
navigate('/account/usage');
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal isOpen={isOpen} onClose={onClose} showCloseButton={true}>
|
||||
<div className="p-6 text-center">
|
||||
{/* Warning Icon */}
|
||||
<div className="relative flex items-center justify-center w-20 h-20 mx-auto mb-6">
|
||||
<div className="absolute inset-0 bg-warning-100 dark:bg-warning-900/30 rounded-full"></div>
|
||||
<div className="relative bg-warning-500 rounded-full w-14 h-14 flex items-center justify-center">
|
||||
<ZapIcon className="w-7 h-7 text-white" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Title */}
|
||||
<h2 className="text-xl font-bold text-gray-900 dark:text-white mb-2">
|
||||
Insufficient Credits
|
||||
</h2>
|
||||
|
||||
{/* Message */}
|
||||
<p className="text-gray-600 dark:text-gray-400 mb-6">
|
||||
You don't have enough credits for {operationType}.
|
||||
</p>
|
||||
|
||||
{/* Credit Stats */}
|
||||
<div className="bg-gray-50 dark:bg-gray-800 rounded-lg p-4 mb-6">
|
||||
<div className="grid grid-cols-3 gap-4 text-center">
|
||||
<div>
|
||||
<div className="text-xs text-gray-500 dark:text-gray-400 mb-1">Required</div>
|
||||
<div className="text-lg font-bold text-warning-600 dark:text-warning-400">
|
||||
{requiredCredits.toLocaleString()}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-xs text-gray-500 dark:text-gray-400 mb-1">Available</div>
|
||||
<div className="text-lg font-bold text-gray-900 dark:text-white">
|
||||
{availableCredits.toLocaleString()}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-xs text-gray-500 dark:text-gray-400 mb-1">Shortfall</div>
|
||||
<div className="text-lg font-bold text-error-600 dark:text-error-400">
|
||||
{shortfall.toLocaleString()}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Action Buttons */}
|
||||
<div className="space-y-3">
|
||||
<Button
|
||||
variant="primary"
|
||||
fullWidth
|
||||
onClick={handleUpgradePlan}
|
||||
className="flex items-center justify-center gap-2"
|
||||
>
|
||||
<TrendingUpIcon className="w-4 h-4" />
|
||||
Upgrade Plan
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
variant="outline"
|
||||
fullWidth
|
||||
onClick={handleBuyCredits}
|
||||
className="flex items-center justify-center gap-2"
|
||||
>
|
||||
<CreditCardIcon className="w-4 h-4" />
|
||||
Buy Credits
|
||||
</Button>
|
||||
|
||||
<button
|
||||
onClick={handleViewUsage}
|
||||
className="text-sm text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-300"
|
||||
>
|
||||
View Usage Details →
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Cancel Button */}
|
||||
<div className="mt-4 pt-4 border-t border-gray-200 dark:border-gray-700">
|
||||
<Button
|
||||
variant="ghost"
|
||||
tone="neutral"
|
||||
onClick={onClose}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook to manage insufficient credits modal state
|
||||
*/
|
||||
export function useInsufficientCreditsModal() {
|
||||
const [isOpen, setIsOpen] = React.useState(false);
|
||||
const [modalProps, setModalProps] = React.useState({
|
||||
requiredCredits: 0,
|
||||
availableCredits: 0,
|
||||
operationType: 'this operation',
|
||||
});
|
||||
|
||||
const showInsufficientCreditsModal = (props: {
|
||||
requiredCredits: number;
|
||||
availableCredits: number;
|
||||
operationType?: string;
|
||||
}) => {
|
||||
setModalProps({
|
||||
requiredCredits: props.requiredCredits,
|
||||
availableCredits: props.availableCredits,
|
||||
operationType: props.operationType || 'this operation',
|
||||
});
|
||||
setIsOpen(true);
|
||||
};
|
||||
|
||||
const closeModal = () => setIsOpen(false);
|
||||
|
||||
return {
|
||||
isOpen,
|
||||
modalProps,
|
||||
showInsufficientCreditsModal,
|
||||
closeModal,
|
||||
};
|
||||
}
|
||||
@@ -177,15 +177,11 @@ export default function UsageLimitsPanel() {
|
||||
sites: { icon: <GlobeIcon className="w-5 h-5" />, color: 'success' as const },
|
||||
users: { icon: <UsersIcon className="w-5 h-5" />, color: 'info' as const },
|
||||
keywords: { icon: <TagIcon className="w-5 h-5" />, color: 'purple' as const },
|
||||
clusters: { icon: <TrendingUpIcon className="w-5 h-5" />, color: 'warning' as const },
|
||||
};
|
||||
|
||||
// Simplified to only 1 monthly limit: Ahrefs keyword research queries
|
||||
const monthlyLimitConfig = {
|
||||
content_ideas: { icon: <FileTextIcon className="w-5 h-5" />, color: 'brand' as const },
|
||||
content_words: { icon: <FileTextIcon className="w-5 h-5" />, color: 'indigo' as const },
|
||||
images_basic: { icon: <ImageIcon className="w-5 h-5" />, color: 'teal' as const },
|
||||
images_premium: { icon: <ZapIcon className="w-5 h-5" />, color: 'cyan' as const },
|
||||
image_prompts: { icon: <ImageIcon className="w-5 h-5" />, color: 'pink' as const },
|
||||
ahrefs_queries: { icon: <TrendingUpIcon className="w-5 h-5" />, color: 'brand' as const },
|
||||
};
|
||||
|
||||
return (
|
||||
|
||||
@@ -24,11 +24,7 @@ export interface PricingPlan {
|
||||
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_ahrefs_queries?: number;
|
||||
included_credits?: number;
|
||||
}
|
||||
|
||||
@@ -142,7 +138,7 @@ export function PricingTable({ variant = '1', title, plans, showToggle = false,
|
||||
))}
|
||||
|
||||
{/* Plan Limits Section */}
|
||||
{(plan.max_sites || plan.max_content_words || plan.included_credits) && (
|
||||
{(plan.max_sites || plan.max_keywords || plan.included_credits) && (
|
||||
<div className="pt-3 mt-3 border-t border-gray-200 dark:border-gray-700">
|
||||
<div className="text-xs font-semibold text-gray-500 dark:text-gray-400 mb-2">LIMITS</div>
|
||||
{plan.max_sites && (
|
||||
@@ -161,27 +157,11 @@ export function PricingTable({ variant = '1', title, plans, showToggle = false,
|
||||
</span>
|
||||
</li>
|
||||
)}
|
||||
{plan.max_content_words && (
|
||||
{plan.max_keywords && (
|
||||
<li className="flex items-start gap-2">
|
||||
<CheckIcon className="w-4 h-4 text-brand-500 flex-shrink-0 mt-0.5" />
|
||||
<span className="text-xs text-gray-600 dark:text-gray-400">
|
||||
{(plan.max_content_words / 1000).toLocaleString()}K Words/month
|
||||
</span>
|
||||
</li>
|
||||
)}
|
||||
{plan.max_content_ideas && (
|
||||
<li className="flex items-start gap-2">
|
||||
<CheckIcon className="w-4 h-4 text-brand-500 flex-shrink-0 mt-0.5" />
|
||||
<span className="text-xs text-gray-600 dark:text-gray-400">
|
||||
{plan.max_content_ideas} Ideas/month
|
||||
</span>
|
||||
</li>
|
||||
)}
|
||||
{plan.max_images_basic && (
|
||||
<li className="flex items-start gap-2">
|
||||
<CheckIcon className="w-4 h-4 text-brand-500 flex-shrink-0 mt-0.5" />
|
||||
<span className="text-xs text-gray-600 dark:text-gray-400">
|
||||
{plan.max_images_basic} Images/month
|
||||
{plan.max_keywords.toLocaleString()} Keywords
|
||||
</span>
|
||||
</li>
|
||||
)}
|
||||
@@ -189,7 +169,7 @@ export function PricingTable({ variant = '1', title, plans, showToggle = false,
|
||||
<li className="flex items-start gap-2">
|
||||
<CheckIcon className="w-4 h-4 text-brand-500 flex-shrink-0 mt-0.5" />
|
||||
<span className="text-xs text-gray-600 dark:text-gray-400">
|
||||
{plan.included_credits.toLocaleString()} Content pieces/month
|
||||
{plan.included_credits.toLocaleString()} Credits/month
|
||||
</span>
|
||||
</li>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user