mirror of
https://github.com/chillpadclub/bedolaga-cabinet.git
synced 2026-07-30 02:23:47 +00:00
feat(sales-stats): душевные карточки с иконками + фикс скачка при смене периода
- StatCard: иконка в мягком цветном чипе (tone success/accent/warning/…) в духе статистики Remnawave; дельта под значением. Обратно совместимо (valueClassName). - Карточки сводки получили осмысленные иконки (доход/подписки/триалы/конверсия/ продления/допы/пополнения) и цветовые тоны. - Скачок при переключении периодов убран: на все запросы статистики добавлен placeholderData: keepPreviousData — старые данные остаются на экране, пока грузятся новые, без вспышки скелетона и прыжка вёрстки.
This commit is contained in:
@@ -2,20 +2,29 @@ import { type ReactNode } from 'react';
|
||||
|
||||
import { TREND_STYLES } from './constants';
|
||||
|
||||
const DEFAULT_VALUE_CLASS = 'text-dark-100';
|
||||
|
||||
export interface StatCardDelta {
|
||||
/** Signed percent change vs the comparison period. */
|
||||
percent: number;
|
||||
trend: 'up' | 'down' | 'stable';
|
||||
}
|
||||
|
||||
/** Soft tinted chip + matching value colour, in the spirit of the Remnawave stats. */
|
||||
const TONE = {
|
||||
neutral: { chip: 'bg-dark-700/60 text-dark-300', value: 'text-dark-100' },
|
||||
success: { chip: 'bg-success-500/15 text-success-400', value: 'text-success-400' },
|
||||
accent: { chip: 'bg-accent-500/15 text-accent-400', value: 'text-accent-400' },
|
||||
warning: { chip: 'bg-warning-500/15 text-warning-400', value: 'text-warning-400' },
|
||||
error: { chip: 'bg-error-500/15 text-error-400', value: 'text-error-400' },
|
||||
} as const;
|
||||
|
||||
interface StatCardProps {
|
||||
label: string;
|
||||
value: string | number;
|
||||
icon?: ReactNode;
|
||||
/** Tints the icon chip and (unless valueClassName is set) the value colour. */
|
||||
tone?: keyof typeof TONE;
|
||||
valueClassName?: string;
|
||||
/** Optional period-over-period change shown next to the value. */
|
||||
/** Optional period-over-period change shown under the value. */
|
||||
delta?: StatCardDelta | null;
|
||||
}
|
||||
|
||||
@@ -23,26 +32,33 @@ export function StatCard({
|
||||
label,
|
||||
value,
|
||||
icon,
|
||||
valueClassName = DEFAULT_VALUE_CLASS,
|
||||
tone = 'neutral',
|
||||
valueClassName,
|
||||
delta,
|
||||
}: StatCardProps) {
|
||||
const toneStyle = TONE[tone];
|
||||
const valueClass = valueClassName ?? toneStyle.value;
|
||||
const trendStyle = delta ? (TREND_STYLES[delta.trend] ?? TREND_STYLES.stable) : null;
|
||||
|
||||
return (
|
||||
<div className="rounded-xl bg-dark-800/30 p-3">
|
||||
<div className="flex items-center gap-1.5 text-sm text-dark-500">
|
||||
{icon}
|
||||
<span>{label}</span>
|
||||
</div>
|
||||
<div className="mt-1">
|
||||
<div className={`truncate text-base font-semibold sm:text-lg ${valueClassName}`}>
|
||||
{value}
|
||||
</div>
|
||||
{trendStyle && (
|
||||
<div className={`mt-0.5 text-xs font-medium ${trendStyle.className}`}>
|
||||
{trendStyle.arrow} {Math.abs(delta!.percent)}%
|
||||
</div>
|
||||
<div className="rounded-xl bg-dark-800/30 p-3 transition-colors hover:bg-dark-800/50">
|
||||
<div className="flex items-center gap-2.5">
|
||||
{icon && (
|
||||
<span
|
||||
className={`flex h-9 w-9 shrink-0 items-center justify-center rounded-lg ${toneStyle.chip}`}
|
||||
>
|
||||
{icon}
|
||||
</span>
|
||||
)}
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="truncate text-xs text-dark-500 sm:text-sm">{label}</div>
|
||||
<div className={`truncate text-base font-semibold sm:text-lg ${valueClass}`}>{value}</div>
|
||||
{trendStyle && (
|
||||
<div className={`mt-0.5 text-xs font-medium ${trendStyle.className}`}>
|
||||
{trendStyle.arrow} {Math.abs(delta!.percent)}%
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user