final polish phase 1
This commit is contained in:
147
frontend/src/components/dashboard/CreditAvailabilityWidget.tsx
Normal file
147
frontend/src/components/dashboard/CreditAvailabilityWidget.tsx
Normal file
@@ -0,0 +1,147 @@
|
||||
/**
|
||||
* 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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user