# All files / src/components/stats StatsOverview.tsx

0% Statements 0/92
100% Branches 1/1
100% Functions 1/1
0% Lines 0/92

Press n or j to go to the next uncovered block, b, p or k for the previous block.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    127
    128
    129
    130
    131
    132
    133
    134
    135
    136
    137
    138
    139
    140
    141

     import React from 'react';
    import { formatCurrency, formatPercentage } from '../../utils/format';

    interface StatCardProps {
      label: string;
      value: string | number;
      format?: 'currency' | 'percentage' | 'number';
      change?: number;
      icon?: React.ReactNode;
      // outcome: only set when the value has genuine win/loss meaning (P&L, ROI).
      // Omit for neutral stats (count, rate). This is the only color signal in the card.
      outcome?: 'win' | 'loss';
    }

    const outcomeValueClass: Record<NonNullable<StatCardProps['outcome']>, string> = {
      win:  'text-win-600 dark:text-win-400',
      loss: 'text-loss-600 dark:text-loss-400',
    };

    function StatCard({ label, value, format = 'number', change, icon, outcome }: StatCardProps) {
      const formattedValue = () => {
        if (value === null || value === undefined) return '0';
        if (format === 'currency') return formatCurrency(Number(value));
        if (format === 'percentage') return formatPercentage(Number(value));
        return value.toString();
      };

      const isPositiveChange = change !== undefined && change > 0;
      const isNegativeChange = change !== undefined && change < 0;

      return (
        <div className="card p-6">
          <div className="flex items-start justify-between">
            <div className="flex-1">
              <p className="text-xs font-display font-bold tracking-widest uppercase text-gray-500 dark:text-gray-400 mb-1">
                {label}
              </p>
              {/* Value — semantic color only when outcome is explicitly set */}
              <p className={`text-3xl font-bold ${outcome ? outcomeValueClass[outcome] : 'text-gray-900 dark:text-white'}`}>
                {formattedValue()}
              </p>

              {change !== undefined && (
                <div className="mt-2 flex items-center gap-1">
                  {isPositiveChange && (
                    <svg className="w-4 h-4 text-win-600 dark:text-win-400" fill="currentColor" viewBox="0 0 20 20">
                      <path fillRule="evenodd" d="M5.293 9.707a1 1 0 010-1.414l4-4a1 1 0 011.414 0l4 4a1 1 0 01-1.414 1.414L11 7.414V15a1 1 0 11-2 0V7.414L6.707 9.707a1 1 0 01-1.414 0z" clipRule="evenodd" />
                    </svg>
                  )}
                  {isNegativeChange && (
                    <svg className="w-4 h-4 text-loss-600 dark:text-loss-400" fill="currentColor" viewBox="0 0 20 20">
                      <path fillRule="evenodd" d="M14.707 10.293a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 111.414-1.414L9 12.586V5a1 1 0 012 0v7.586l2.293-2.293a1 1 0 011.414 0z" clipRule="evenodd" />
                    </svg>
                  )}
                  <span className={`text-sm font-medium ${
                    isPositiveChange ? 'text-win-600 dark:text-win-400'
                    : isNegativeChange ? 'text-loss-600 dark:text-loss-400'
                    : 'text-gray-500'
                  }`}>
                    {Math.abs(change).toFixed(1)}%
                  </span>
                </div>
              )}
            </div>

            {/* Icon badge — always neutral. Color is carried by the value, not the decoration. */}
            {icon && (
              <div className="bg-surface-100 dark:bg-surface-700 text-gray-500 dark:text-gray-400 p-3 rounded-lg">
                {icon}
              </div>
            )}
          </div>
        </div>
      );
    }

    interface StatsOverviewProps {
      stats: {
        totalBets: number;
        wonBets: number;
        lostBets: number;
        pushBets: number;
        winRate: number;
        totalStaked: number;
        totalReturn: number;
        netProfit: number;
        roi: number;
      };
    }

    export default function StatsOverview({ stats }: StatsOverviewProps) {
      return (
        <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
          <StatCard
            label="Total Bets"
            value={stats.totalBets}
            icon={
              <svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
                <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.75} d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2" />
              </svg>
            }
          />

          <StatCard
            label="Win Rate"
            value={stats.winRate}
            format="percentage"
            icon={
              <svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
                <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.75} d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z" />
              </svg>
            }
          />

          <StatCard
            label="Total P&L"
            value={stats.netProfit}
            format="currency"
            outcome={stats.netProfit >= 0 ? 'win' : 'loss'}
            icon={
              <svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
                <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.75} d="M12 8c-1.657 0-3 .895-3 2s1.343 2 3 2 3 .895 3 2-1.343 2-3 2m0-8c1.11 0 2.08.402 2.599 1M12 8V7m0 1v8m0 0v1m0-1c-1.11 0-2.08-.402-2.599-1M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
              </svg>
            }
          />

          <StatCard
            label="ROI"
            value={stats.roi}
            format="percentage"
            outcome={stats.roi >= 0 ? 'win' : 'loss'}
            icon={
              <svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
                <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.75} d="M13 7h8m0 0v8m0-8l-8 8-4-4-6 6" />
              </svg>
            }
          />
        </div>
      );
    }