Files
igny8/frontend/src/components/dashboard/CreditAvailabilityWidget.tsx
IGNY8 VPS (Salman) 5f9a4b8dca final polish phase 1
2025-12-27 21:27:37 +00:00

148 lines
5.6 KiB
TypeScript

/**
* CreditAvailabilityWidget - Shows available operations based on credit balance
* Calculates how many operations can be performed with remaining credits
*/
import { Link } from 'react-router-dom';
import {
GroupIcon,
BoltIcon,
FileTextIcon,
FileIcon,
DollarLineIcon,
} from '../../icons';
interface CreditAvailabilityWidgetProps {
availableCredits: number;
totalCredits: number;
loading?: boolean;
}
// Average credit costs per operation
const OPERATION_COSTS = {
clustering: { label: 'Clustering Runs', cost: 10, icon: GroupIcon, color: 'text-purple-600 dark:text-purple-400' },
ideas: { label: 'Content Ideas', cost: 2, icon: BoltIcon, color: 'text-orange-600 dark:text-orange-400' },
content: { label: 'Articles', cost: 50, icon: FileTextIcon, color: 'text-green-600 dark:text-green-400' },
images: { label: 'Images', cost: 5, icon: FileIcon, color: 'text-pink-600 dark:text-pink-400' },
};
export default function CreditAvailabilityWidget({
availableCredits,
totalCredits,
loading = false
}: CreditAvailabilityWidgetProps) {
const usedCredits = totalCredits - availableCredits;
const usagePercent = totalCredits > 0 ? Math.round((usedCredits / totalCredits) * 100) : 0;
// Calculate available operations
const availableOps = Object.entries(OPERATION_COSTS).map(([key, config]) => ({
type: key,
label: config.label,
icon: config.icon,
color: config.color,
cost: config.cost,
available: Math.floor(availableCredits / config.cost),
}));
return (
<div className="bg-white dark:bg-gray-900 rounded-xl border border-gray-200 dark:border-gray-800 p-5">
{/* Header */}
<div className="flex items-center justify-between mb-4">
<h3 className="text-base font-semibold text-gray-800 dark:text-gray-200 uppercase tracking-wide">
Credit Availability
</h3>
<Link
to="/billing/credits"
className="text-sm font-medium text-brand-600 hover:text-brand-700 dark:text-brand-400"
>
Add Credits
</Link>
</div>
{/* Credits Balance */}
<div className="bg-gradient-to-r from-brand-50 to-purple-50 dark:from-brand-900/20 dark:to-purple-900/20 rounded-lg p-4 mb-4">
<div className="flex items-center justify-between mb-2">
<span className="text-sm text-gray-600 dark:text-gray-400">Available Credits</span>
<span className="text-2xl font-bold text-brand-600 dark:text-brand-400">
{loading ? '—' : availableCredits.toLocaleString()}
</span>
</div>
<div className="w-full bg-white dark:bg-gray-800 rounded-full h-2 mb-1">
<div
className={`h-2 rounded-full transition-all ${
usagePercent > 90 ? 'bg-red-500' : usagePercent > 75 ? 'bg-amber-500' : 'bg-green-500'
}`}
style={{ width: `${Math.max(100 - usagePercent, 0)}%` }}
></div>
</div>
<p className="text-xs text-gray-600 dark:text-gray-400">
{totalCredits > 0 ? `${usedCredits.toLocaleString()} of ${totalCredits.toLocaleString()} used (${usagePercent}%)` : 'No credits allocated'}
</p>
</div>
{/* Available Operations */}
<div className="space-y-2.5">
<p className="text-xs font-semibold text-gray-600 dark:text-gray-400 uppercase tracking-wide mb-2">
You can run:
</p>
{loading ? (
<div className="py-4 text-center">
<p className="text-sm text-gray-500">Loading...</p>
</div>
) : availableCredits === 0 ? (
<div className="py-4 text-center">
<p className="text-sm text-gray-600 dark:text-gray-400">No credits available</p>
<Link
to="/billing/credits"
className="text-sm text-brand-600 hover:text-brand-700 dark:text-brand-400"
>
Purchase credits to continue
</Link>
</div>
) : (
availableOps.map((op) => {
const Icon = op.icon;
return (
<div
key={op.type}
className="flex items-center gap-3 p-2 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-800/50 transition-colors"
>
<div className={`flex-shrink-0 ${op.color}`}>
<Icon className="w-5 h-5" />
</div>
<div className="flex-1 min-w-0">
<p className="text-sm font-medium text-gray-800 dark:text-gray-200">
{op.label}
</p>
<p className="text-xs text-gray-600 dark:text-gray-400">
{op.cost} credits each
</p>
</div>
<span className={`text-lg font-bold ${
op.available > 10 ? 'text-green-600 dark:text-green-400' :
op.available > 0 ? 'text-amber-600 dark:text-amber-400' :
'text-gray-400 dark:text-gray-600'
}`}>
{op.available === 0 ? '—' : op.available > 999 ? '999+' : op.available}
</span>
</div>
);
})
)}
</div>
{/* Warning if low */}
{!loading && availableCredits > 0 && availableCredits < 100 && (
<div className="mt-4 pt-3 border-t border-gray-200 dark:border-gray-700">
<div className="flex items-start gap-2 text-amber-600 dark:text-amber-400">
<DollarLineIcon className="w-4 h-4 mt-0.5" />
<p className="text-xs">
You're running low on credits. Consider purchasing more to avoid interruptions.
</p>
</div>
</div>
)}
</div>
);
}