Sanitized mirror from private repository - 2026-04-18 11:19:59 UTC
This commit is contained in:
134
dashboard/ui/components/stat-card.tsx
Normal file
134
dashboard/ui/components/stat-card.tsx
Normal file
@@ -0,0 +1,134 @@
|
||||
import { Card, CardContent } from "@/components/ui/card";
|
||||
|
||||
type StatColor = "blue" | "green" | "violet" | "amber" | "emerald";
|
||||
|
||||
const gradientMap: Record<StatColor, string> = {
|
||||
blue: "from-blue-300 to-blue-500",
|
||||
green: "from-green-300 to-green-500",
|
||||
violet: "from-violet-300 to-violet-500",
|
||||
amber: "from-amber-300 to-amber-500",
|
||||
emerald: "from-emerald-300 to-emerald-500",
|
||||
};
|
||||
|
||||
const accentLineMap: Record<StatColor, string> = {
|
||||
blue: "#3b82f6",
|
||||
green: "#22c55e",
|
||||
violet: "#8b5cf6",
|
||||
amber: "#f59e0b",
|
||||
emerald: "#10b981",
|
||||
};
|
||||
|
||||
const strokeColorMap: Record<StatColor, [string, string]> = {
|
||||
blue: ["#93c5fd", "#3b82f6"],
|
||||
green: ["#86efac", "#22c55e"],
|
||||
violet: ["#c4b5fd", "#8b5cf6"],
|
||||
amber: ["#fcd34d", "#f59e0b"],
|
||||
emerald: ["#6ee7b7", "#10b981"],
|
||||
};
|
||||
|
||||
interface StatCardProps {
|
||||
label: string;
|
||||
value: string | number;
|
||||
sub?: React.ReactNode;
|
||||
color?: StatColor;
|
||||
gauge?: number; // 0-100 percentage for ring gauge
|
||||
}
|
||||
|
||||
function colorizeSubText(sub: React.ReactNode): React.ReactNode {
|
||||
if (typeof sub !== "string") return sub;
|
||||
const keywords: [RegExp, string][] = [
|
||||
[/\b(running|online|healthy|clean|clear|all good|ok)\b/gi, "text-green-400"],
|
||||
[/\b(error|alert|fail|errors detected)\b/gi, "text-red-400"],
|
||||
];
|
||||
for (const [pattern, cls] of keywords) {
|
||||
if (pattern.test(sub)) {
|
||||
return (
|
||||
<>
|
||||
{sub.split(pattern).map((part, i) =>
|
||||
pattern.test(part) ? (
|
||||
<span key={i} className={cls}>{part}</span>
|
||||
) : (
|
||||
<span key={i}>{part}</span>
|
||||
)
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
return sub;
|
||||
}
|
||||
|
||||
function GaugeRing({ pct, color, size = 64, stroke = 4 }: { pct: number; color: StatColor; size?: number; stroke?: number }) {
|
||||
const radius = (size - stroke) / 2;
|
||||
const circumference = 2 * Math.PI * radius;
|
||||
const offset = circumference - (pct / 100) * circumference;
|
||||
const gradientId = `gauge-grad-${color}`;
|
||||
const [stopStart, stopEnd] = strokeColorMap[color] ?? ["#93c5fd", "#3b82f6"];
|
||||
|
||||
return (
|
||||
<svg width={size} height={size} viewBox={`0 0 ${size} ${size}`} className="-rotate-90">
|
||||
<defs>
|
||||
<linearGradient id={gradientId}>
|
||||
<stop offset="0%" stopColor={stopStart} />
|
||||
<stop offset="100%" stopColor={stopEnd} />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<circle
|
||||
className="gauge-track"
|
||||
cx={size / 2}
|
||||
cy={size / 2}
|
||||
r={radius}
|
||||
strokeWidth={stroke}
|
||||
/>
|
||||
<circle
|
||||
className="gauge-fill"
|
||||
cx={size / 2}
|
||||
cy={size / 2}
|
||||
r={radius}
|
||||
strokeWidth={stroke}
|
||||
stroke={`url(#${gradientId})`}
|
||||
strokeDasharray={circumference}
|
||||
strokeDashoffset={offset}
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
export function StatCard({ label, value, sub, color = "blue", gauge }: StatCardProps) {
|
||||
const hasGauge = gauge != null && gauge >= 0;
|
||||
const accent = accentLineMap[color];
|
||||
|
||||
return (
|
||||
<Card className="card-hover-lift overflow-hidden relative group">
|
||||
{/* Top accent line */}
|
||||
<span
|
||||
className="absolute top-0 left-[20%] right-[20%] h-px"
|
||||
style={{
|
||||
background: `linear-gradient(90deg, transparent, ${accent}, transparent)`,
|
||||
}}
|
||||
/>
|
||||
<CardContent className="pt-5 pb-4 px-4 relative flex flex-col items-center justify-center text-center min-h-[110px]">
|
||||
{hasGauge ? (
|
||||
<div className="relative flex items-center justify-center mb-1" style={{ width: 64, height: 64 }}>
|
||||
<GaugeRing pct={gauge} color={color} />
|
||||
<span
|
||||
className={`absolute inset-0 flex items-center justify-center text-xl font-bold bg-gradient-to-b ${gradientMap[color]} bg-clip-text text-transparent tabular-nums-transition`}
|
||||
>
|
||||
{value}
|
||||
</span>
|
||||
</div>
|
||||
) : (
|
||||
<p
|
||||
className={`text-3xl font-bold bg-gradient-to-b ${gradientMap[color]} bg-clip-text text-transparent tabular-nums-transition mb-1`}
|
||||
>
|
||||
{value}
|
||||
</p>
|
||||
)}
|
||||
<p className="text-xs uppercase tracking-wider text-muted-foreground font-medium">
|
||||
{label}
|
||||
</p>
|
||||
{sub && <div className="mt-0.5 text-sm text-muted-foreground/70">{colorizeSubText(sub)}</div>}
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user