/ frontend / src / components / analytics / MetricsCard.tsx
MetricsCard.tsx
  1  import type { ReactNode } from 'react'
  2  
  3  interface MetricsCardProps {
  4    title: string
  5    value: string | number
  6    subtitle?: string
  7    trend?: number  // Percentage change
  8    icon?: ReactNode
  9    color?: 'default' | 'green' | 'red' | 'blue' | 'purple' | 'orange'
 10  }
 11  
 12  export default function MetricsCard({
 13    title,
 14    value,
 15    subtitle,
 16    trend,
 17    icon,
 18    color = 'default',
 19  }: MetricsCardProps) {
 20    const colorClasses = {
 21      default: 'bg-white',
 22      green: 'bg-green-50',
 23      red: 'bg-red-50',
 24      blue: 'bg-blue-50',
 25      purple: 'bg-purple-50',
 26      orange: 'bg-orange-50',
 27    }
 28  
 29    const iconColors = {
 30      default: 'text-gray-500',
 31      green: 'text-green-600',
 32      red: 'text-red-600',
 33      blue: 'text-blue-600',
 34      purple: 'text-purple-600',
 35      orange: 'text-orange-600',
 36    }
 37  
 38    return (
 39      <div className={`${colorClasses[color]} border rounded-lg p-4`}>
 40        <div className="flex items-start justify-between">
 41          <div>
 42            <p className="text-sm text-gray-600">{title}</p>
 43            <p className="text-2xl font-bold mt-1">{value}</p>
 44            {subtitle && (
 45              <p className="text-xs text-gray-500 mt-1">{subtitle}</p>
 46            )}
 47          </div>
 48          {icon && (
 49            <div className={`p-2 rounded-lg bg-white/50 ${iconColors[color]}`}>
 50              {icon}
 51            </div>
 52          )}
 53        </div>
 54  
 55        {trend !== undefined && (
 56          <div className="mt-3 flex items-center gap-1">
 57            {trend > 0 ? (
 58              <TrendUpIcon className="h-4 w-4 text-green-600" />
 59            ) : trend < 0 ? (
 60              <TrendDownIcon className="h-4 w-4 text-red-600" />
 61            ) : (
 62              <TrendNeutralIcon className="h-4 w-4 text-gray-400" />
 63            )}
 64            <span
 65              className={`text-sm font-medium ${
 66                trend > 0
 67                  ? 'text-green-600'
 68                  : trend < 0
 69                  ? 'text-red-600'
 70                  : 'text-gray-500'
 71              }`}
 72            >
 73              {trend > 0 ? '+' : ''}{trend.toFixed(1)}%
 74            </span>
 75            <span className="text-xs text-gray-400">vs last period</span>
 76          </div>
 77        )}
 78      </div>
 79    )
 80  }
 81  
 82  /**
 83   * Compact metrics row for dashboard
 84   */
 85  export function MetricsRow({
 86    items,
 87  }: {
 88    items: { label: string; value: string | number; trend?: number }[]
 89  }) {
 90    return (
 91      <div className="grid grid-cols-2 md:grid-cols-4 gap-4">
 92        {items.map((item, idx) => (
 93          <div key={idx} className="text-center p-3 bg-gray-50 rounded-lg">
 94            <p className="text-xs text-gray-500">{item.label}</p>
 95            <p className="text-lg font-semibold">{item.value}</p>
 96            {item.trend !== undefined && (
 97              <span
 98                className={`text-xs ${
 99                  item.trend > 0
100                    ? 'text-green-600'
101                    : item.trend < 0
102                    ? 'text-red-600'
103                    : 'text-gray-500'
104                }`}
105              >
106                {item.trend > 0 ? '+' : ''}{item.trend.toFixed(1)}%
107              </span>
108            )}
109          </div>
110        ))}
111      </div>
112    )
113  }
114  
115  function TrendUpIcon({ className }: { className?: string }) {
116    return (
117      <svg className={className} fill="none" viewBox="0 0 24 24" stroke="currentColor">
118        <path
119          strokeLinecap="round"
120          strokeLinejoin="round"
121          strokeWidth={2}
122          d="M13 7h8m0 0v8m0-8l-8 8-4-4-6 6"
123        />
124      </svg>
125    )
126  }
127  
128  function TrendDownIcon({ className }: { className?: string }) {
129    return (
130      <svg className={className} fill="none" viewBox="0 0 24 24" stroke="currentColor">
131        <path
132          strokeLinecap="round"
133          strokeLinejoin="round"
134          strokeWidth={2}
135          d="M13 17h8m0 0v-8m0 8l-8-8-4 4-6-6"
136        />
137      </svg>
138    )
139  }
140  
141  function TrendNeutralIcon({ className }: { className?: string }) {
142    return (
143      <svg className={className} fill="none" viewBox="0 0 24 24" stroke="currentColor">
144        <path
145          strokeLinecap="round"
146          strokeLinejoin="round"
147          strokeWidth={2}
148          d="M5 12h14"
149        />
150      </svg>
151    )
152  }