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

@@ -24,6 +24,12 @@ interface StatCardProps {
/** Tints the icon chip and (unless valueClassName is set) the value colour. */
tone?: keyof typeof TONE;
valueClassName?: string;
/** Optional secondary line shown under the value (e.g. a subtitle or context). */
subValue?: string;
/** When true, shows a skeleton placeholder instead of the value. */
loading?: boolean;
/** Optional node rendered at the right edge of the label row (e.g. a chevron for nav cards). */
trailing?: ReactNode;
/** Optional period-over-period change shown under the value. */
delta?: StatCardDelta | null;
}
@@ -34,6 +40,9 @@ export function StatCard({
icon,
tone = 'neutral',
valueClassName,
subValue,
loading,
trailing,
delta,
}: StatCardProps) {
const toneStyle = TONE[tone];
@@ -42,19 +51,32 @@ export function StatCard({
return (
<div className="rounded-xl bg-dark-800/30 p-3 transition-colors hover:bg-dark-800/50">
<div className="truncate text-xs text-dark-500 sm:text-sm">{label}</div>
<div className="flex items-center justify-between gap-2">
<span className="truncate text-xs text-dark-500 sm:text-sm">{label}</span>
{trailing}
</div>
{/* Chip is centred against the value line only (delta sits below the whole
row), so the icon lands in the same spot on every card. */}
row), so the icon lands in the same spot on every card. The forced svg
size normalises every icon regardless of what the call site passes. */}
<div className="mt-1.5 flex items-center gap-2.5">
{icon && (
<span
className={`flex h-9 w-9 shrink-0 items-center justify-center rounded-lg ${toneStyle.chip}`}
className={`flex h-9 w-9 shrink-0 items-center justify-center rounded-lg [&>svg]:h-5 [&>svg]:w-5 ${toneStyle.chip}`}
>
{icon}
</span>
)}
<div className={`min-w-0 flex-1 truncate text-lg font-semibold sm:text-xl ${valueClass}`}>
{value}
<div className="min-w-0 flex-1">
{loading ? (
<div className="skeleton h-7 w-20 rounded" />
) : (
<>
<div className={`truncate text-lg font-semibold sm:text-xl ${valueClass}`}>
{value}
</div>
{subValue && <div className="truncate text-xs text-dark-500">{subValue}</div>}
</>
)}
</div>
</div>
{trendStyle && (