refactor(cabinet): unify all statistics onto the shared StatCard

Bring every stat strip across the admin panel and user cabinet onto the
canonical StatCard (icon chip + label-on-top + tone-coloured value),
matching the sales/Remnawave style the rest of the app already used.

- Convert ~20 hand-rolled stat strips across 21 pages/components
- Remove 4 divergent local StatCard components (Dashboard, Users,
  BanSystem, Payments) and migrate Remnawave's local StatCard (33 cards)
- Extend StatCard with optional subValue, loading skeleton, trailing slot,
  and icon-size normalisation (all backward-compatible)
- StatsGrid nav tiles now use StatCard while keeping link/chevron/loading
- Preserve all data, i18n keys, conditionals and interactive filters;
  fix the dead 'info' colour by mapping it to accent
This commit is contained in:
c0mrade
2026-06-06 21:27:57 +03:00
parent 9fc681b56d
commit 3b48abbb7b
23 changed files with 863 additions and 973 deletions

View File

@@ -7,12 +7,16 @@ import { DateField } from '../components/DateField';
import { useCurrency } from '../hooks/useCurrency';
import type { PendingPayment, PaginatedResponse } from '../types';
import { usePlatform } from '../platform/hooks/usePlatform';
import { StatCard } from '@/components/stats';
import {
BackIcon,
SearchIcon,
CalendarIcon,
RefreshIcon,
CheckCircleIcon,
ChartBarIcon,
ClockIcon,
XCircleIcon,
} from '@/components/icons';
interface StatusBadgeProps {
@@ -38,38 +42,6 @@ function StatusBadge({ status }: StatusBadgeProps) {
);
}
interface StatCardProps {
label: string;
value: number;
color: 'blue' | 'amber' | 'green' | 'red';
isActive: boolean;
onClick: () => void;
}
function StatCard({ label, value, color, isActive, onClick }: StatCardProps) {
const colors: Record<string, string> = {
blue: 'border-accent-500/30 bg-accent-500/20 text-accent-400',
amber: 'border-warning-500/30 bg-warning-500/20 text-warning-400',
green: 'border-success-500/30 bg-success-500/20 text-success-400',
red: 'border-error-500/30 bg-error-500/20 text-error-400',
};
return (
<button
type="button"
onClick={onClick}
className={`rounded-xl border p-4 text-left transition-all ${
isActive
? colors[color]
: 'border-dark-700/50 bg-dark-800/50 text-dark-300 hover:border-dark-600'
}`}
>
<div className={`text-2xl font-bold ${isActive ? '' : 'text-dark-50'}`}>{value}</div>
<div className="text-sm opacity-80">{label}</div>
</button>
);
}
export default function AdminPayments() {
const { t } = useTranslation();
const navigate = useNavigate();
@@ -353,34 +325,62 @@ export default function AdminPayments() {
{/* Stats cards */}
{stats && (
<div className="grid grid-cols-2 gap-3 sm:grid-cols-4">
<StatCard
label={t('admin.payments.totalCount')}
value={stats.total}
color="blue"
isActive={statusFilter === 'all'}
<button
type="button"
onClick={() => handleStatusCardClick('all')}
/>
<StatCard
label={t('admin.payments.pendingCount')}
value={stats.pending}
color="amber"
isActive={statusFilter === 'pending'}
className={`rounded-xl text-left transition-all ${
statusFilter === 'all' ? 'ring-2 ring-accent-500' : ''
}`}
>
<StatCard
label={t('admin.payments.totalCount')}
value={stats.total}
icon={<ChartBarIcon className="h-5 w-5" />}
tone="accent"
/>
</button>
<button
type="button"
onClick={() => handleStatusCardClick('pending')}
/>
<StatCard
label={t('admin.payments.paidCount')}
value={stats.paid}
color="green"
isActive={statusFilter === 'paid'}
className={`rounded-xl text-left transition-all ${
statusFilter === 'pending' ? 'ring-2 ring-warning-500' : ''
}`}
>
<StatCard
label={t('admin.payments.pendingCount')}
value={stats.pending}
icon={<ClockIcon className="h-5 w-5" />}
tone="warning"
/>
</button>
<button
type="button"
onClick={() => handleStatusCardClick('paid')}
/>
<StatCard
label={t('admin.payments.cancelledCount')}
value={stats.cancelled}
color="red"
isActive={statusFilter === 'cancelled'}
className={`rounded-xl text-left transition-all ${
statusFilter === 'paid' ? 'ring-2 ring-success-500' : ''
}`}
>
<StatCard
label={t('admin.payments.paidCount')}
value={stats.paid}
icon={<CheckCircleIcon className="h-5 w-5" />}
tone="success"
/>
</button>
<button
type="button"
onClick={() => handleStatusCardClick('cancelled')}
/>
className={`rounded-xl text-left transition-all ${
statusFilter === 'cancelled' ? 'ring-2 ring-error-500' : ''
}`}
>
<StatCard
label={t('admin.payments.cancelledCount')}
value={stats.cancelled}
icon={<XCircleIcon className="h-5 w-5" />}
tone="error"
/>
</button>
</div>
)}