170 lines
4.8 KiB
TypeScript
170 lines
4.8 KiB
TypeScript
import React, { ReactNode, useState } from "react";
|
|
import { Link } from "react-router";
|
|
import { ArrowUpIcon, ArrowDownIcon } from "../../icons";
|
|
import { EnhancedTooltip } from "../ui/tooltip/EnhancedTooltip";
|
|
|
|
export interface MetricCardProps {
|
|
title: string;
|
|
value: string | number;
|
|
subtitle?: string;
|
|
trend?: number; // Positive or negative trend value
|
|
icon: ReactNode;
|
|
accentColor: "blue" | "green" | "orange" | "purple" | "red";
|
|
href?: string;
|
|
onClick?: () => void;
|
|
tooltip?: string | ReactNode; // Enhanced tooltip content
|
|
details?: Array<{ label: string; value: string | number }>; // Breakdown for tooltip
|
|
className?: string;
|
|
}
|
|
|
|
const accentColors = {
|
|
blue: {
|
|
bg: "bg-blue-50 dark:bg-blue-500/10",
|
|
hover: "hover:bg-blue-100 dark:hover:bg-blue-500/20",
|
|
border: "bg-brand-500",
|
|
icon: "text-brand-500",
|
|
},
|
|
green: {
|
|
bg: "bg-green-50 dark:bg-green-500/10",
|
|
hover: "hover:bg-green-100 dark:hover:bg-green-500/20",
|
|
border: "bg-success-500",
|
|
icon: "text-success-500",
|
|
},
|
|
orange: {
|
|
bg: "bg-amber-50 dark:bg-amber-500/10",
|
|
hover: "hover:bg-amber-100 dark:hover:bg-amber-500/20",
|
|
border: "bg-warning-500",
|
|
icon: "text-warning-500",
|
|
},
|
|
purple: {
|
|
bg: "bg-purple-50 dark:bg-purple-500/10",
|
|
hover: "hover:bg-purple-100 dark:hover:bg-purple-500/20",
|
|
border: "bg-purple-500",
|
|
icon: "text-purple-500",
|
|
},
|
|
red: {
|
|
bg: "bg-red-50 dark:bg-red-500/10",
|
|
hover: "hover:bg-red-100 dark:hover:bg-red-500/20",
|
|
border: "bg-error-500",
|
|
icon: "text-error-500",
|
|
},
|
|
};
|
|
|
|
export default function EnhancedMetricCard({
|
|
title,
|
|
value,
|
|
subtitle,
|
|
trend,
|
|
icon,
|
|
accentColor,
|
|
href,
|
|
onClick,
|
|
tooltip,
|
|
details,
|
|
className = "",
|
|
}: MetricCardProps) {
|
|
const [isHovered, setIsHovered] = useState(false);
|
|
const colors = accentColors[accentColor];
|
|
|
|
const formatValue = (val: string | number): string => {
|
|
if (typeof val === "number") {
|
|
return val.toLocaleString();
|
|
}
|
|
return val;
|
|
};
|
|
|
|
const tooltipContent = tooltip || (
|
|
details ? (
|
|
<div className="space-y-1">
|
|
{details.map((detail, idx) => (
|
|
<div key={idx} className="flex justify-between gap-4 text-xs">
|
|
<span className="text-gray-300">{detail.label}:</span>
|
|
<span className="font-medium text-white">{detail.value}</span>
|
|
</div>
|
|
))}
|
|
</div>
|
|
) : null
|
|
);
|
|
|
|
const cardContent = (
|
|
<div
|
|
className={`
|
|
rounded-2xl border border-gray-200 bg-white p-5
|
|
dark:border-gray-800 dark:bg-white/[0.03] md:p-6
|
|
hover:shadow-lg transition-all cursor-pointer group
|
|
relative overflow-hidden
|
|
${className}
|
|
`}
|
|
onMouseEnter={() => setIsHovered(true)}
|
|
onMouseLeave={() => setIsHovered(false)}
|
|
onClick={onClick}
|
|
>
|
|
{/* Accent Border */}
|
|
<div className={`absolute left-0 top-0 bottom-0 w-1 ${colors.border}`} />
|
|
|
|
<div className="flex items-start justify-between">
|
|
<div className="flex-1">
|
|
<p className="text-sm text-gray-500 dark:text-gray-400">{title}</p>
|
|
<div className="flex items-center gap-2 mt-2">
|
|
<h4 className="font-bold text-gray-800 text-title-sm dark:text-white/90">
|
|
{formatValue(value)}
|
|
</h4>
|
|
{trend !== undefined && trend !== 0 && (
|
|
<div
|
|
className={`flex items-center gap-1 text-xs ${
|
|
trend > 0 ? "text-success-500" : "text-error-500"
|
|
}`}
|
|
>
|
|
{trend > 0 ? (
|
|
<ArrowUpIcon className="size-3" />
|
|
) : (
|
|
<ArrowDownIcon className="size-3" />
|
|
)}
|
|
<span>{Math.abs(trend)}</span>
|
|
</div>
|
|
)}
|
|
</div>
|
|
{subtitle && (
|
|
<p className="text-xs text-gray-500 dark:text-gray-400 mt-1">
|
|
{subtitle}
|
|
</p>
|
|
)}
|
|
</div>
|
|
<div
|
|
className={`flex items-center justify-center w-12 h-12 rounded-xl ${colors.bg} ${colors.hover} transition-colors flex-shrink-0`}
|
|
>
|
|
{React.cloneElement(icon as React.ReactElement, { className: `${colors.icon} size-6` })}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Hover Effect */}
|
|
{isHovered && tooltipContent && (
|
|
<div className="absolute inset-0 bg-black/5 dark:bg-white/5 rounded-2xl" />
|
|
)}
|
|
</div>
|
|
);
|
|
|
|
// Wrap with tooltip if provided
|
|
if (tooltipContent) {
|
|
const wrappedContent = href ? (
|
|
<Link to={href} className="block">
|
|
{cardContent}
|
|
</Link>
|
|
) : (
|
|
cardContent
|
|
);
|
|
|
|
return (
|
|
<EnhancedTooltip
|
|
content={tooltipContent}
|
|
placement="top"
|
|
>
|
|
{wrappedContent}
|
|
</EnhancedTooltip>
|
|
);
|
|
}
|
|
|
|
return href ? <Link to={href}>{cardContent}</Link> : cardContent;
|
|
}
|
|
|