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 }