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 | /** * RevenueCard Component * * Displays revenue metrics with trend indicators and sparkline. */ "use client"; import { Icon } from "@/components/ui/icons"; import { cn } from "@/lib/core"; export interface RevenueCardProps { title: string; value: number; previousValue?: number; trend?: number; icon?: "dollar-sign" | "trending-up" | "shopping-cart" | "users"; format?: "currency" | "number" | "percent"; subtitle?: string; className?: string; } function formatValue(value: number, format: "currency" | "number" | "percent"): string { switch (format) { case "currency": return new Intl.NumberFormat("en-US", { style: "currency", currency: "USD", minimumFractionDigits: 0, maximumFractionDigits: 0, }).format(value); case "percent": return `${value.toFixed(1)}%`; default: return value.toLocaleString(); } } export function RevenueCard({ title, value, previousValue, trend, icon = "dollar-sign", format = "currency", subtitle, className, }: RevenueCardProps) { const calculatedTrend = trend !== undefined ? trend : previousValue && previousValue > 0 ? ((value - previousValue) / previousValue) * 100 : 0; const isPositive = calculatedTrend >= 0; return ( <div className={cn( "bg-white dark:bg-gray-900 rounded-lg border border-gray-200 dark:border-gray-800 p-6", className )} > <div className="flex items-center justify-between"> <span className="text-sm font-medium text-gray-500 dark:text-gray-400">{title}</span> <div className="p-2 bg-primary-50 dark:bg-primary-900/20 rounded-lg"> <Icon name={icon} className="h-5 w-5 text-primary-600 dark:text-primary-400" /> </div> </div> <div className="mt-3"> <div className="flex items-baseline gap-2"> <span className="text-2xl font-bold text-gray-900 dark:text-white"> {formatValue(value, format)} </span> {calculatedTrend !== 0 && ( <span className={cn( "inline-flex items-center gap-0.5 text-sm font-medium", isPositive ? "text-green-600 dark:text-green-400" : "text-red-600 dark:text-red-400" )} > <Icon name={isPositive ? "trending-up" : "trending-up"} className={cn("h-4 w-4", !isPositive && "rotate-180")} /> {Math.abs(calculatedTrend).toFixed(1)}% </span> )} </div> {subtitle && ( <p className="mt-1 text-sm text-gray-500 dark:text-gray-400">{subtitle}</p> )} {previousValue !== undefined && ( <p className="mt-1 text-xs text-gray-400 dark:text-gray-500"> vs {formatValue(previousValue, format)} previous </p> )} </div> </div> ); } export default RevenueCard; |