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

@@ -14,7 +14,12 @@ import {
CheckIcon,
CopyIcon,
ChartIcon,
ChartBarIcon,
CheckCircleIcon,
TagIcon,
TicketIcon,
} from '@/components/icons';
import { StatCard } from '../components/stats';
// Helper functions
const getTypeLabel = (type: PromoCodeType): string => {
@@ -113,30 +118,30 @@ export default function AdminPromocodes() {
{/* Stats Overview */}
{promocodes.length > 0 && (
<div className="mb-6 grid grid-cols-2 gap-3 sm:grid-cols-4">
<div className="rounded-xl border border-dark-700 bg-dark-800 p-4">
<div className="text-2xl font-bold text-dark-100">{promocodes.length}</div>
<div className="text-xs text-dark-400">
{t('admin.promocodes.stats.totalPromocodes')}
</div>
</div>
<div className="rounded-xl border border-dark-700 bg-dark-800 p-4">
<div className="text-2xl font-bold text-success-400">
{promocodes.filter((p) => p.is_active && p.is_valid).length}
</div>
<div className="text-xs text-dark-400">{t('admin.promocodes.stats.activeCount')}</div>
</div>
<div className="rounded-xl border border-dark-700 bg-dark-800 p-4">
<div className="text-2xl font-bold text-accent-400">
{promocodes.reduce((sum, p) => sum + p.current_uses, 0)}
</div>
<div className="text-xs text-dark-400">{t('admin.promocodes.stats.usagesCount')}</div>
</div>
<div className="rounded-xl border border-dark-700 bg-dark-800 p-4">
<div className="text-2xl font-bold text-warning-400">
{promocodes.filter((p) => p.uses_left === 0 && p.max_uses > 0).length}
</div>
<div className="text-xs text-dark-400">{t('admin.promocodes.stats.exhausted')}</div>
</div>
<StatCard
label={t('admin.promocodes.stats.totalPromocodes')}
value={promocodes.length}
icon={<TagIcon className="h-5 w-5" />}
tone="neutral"
/>
<StatCard
label={t('admin.promocodes.stats.activeCount')}
value={promocodes.filter((p) => p.is_active && p.is_valid).length}
icon={<CheckCircleIcon className="h-5 w-5" />}
tone="success"
/>
<StatCard
label={t('admin.promocodes.stats.usagesCount')}
value={promocodes.reduce((sum, p) => sum + p.current_uses, 0)}
icon={<ChartBarIcon className="h-5 w-5" />}
tone="accent"
/>
<StatCard
label={t('admin.promocodes.stats.exhausted')}
value={promocodes.filter((p) => p.uses_left === 0 && p.max_uses > 0).length}
icon={<TicketIcon className="h-5 w-5" />}
tone="warning"
/>
</div>
)}