Spaces:
Running
Running
Download apps/web/src/components/ui/StatCard.tsx from Benaah/MicProtocol: direct link, hf CLI and curl.
- Browser
- Download file 4.52 kB
-
https://huggingface.co/spaces/Benaah/MicProtocol/resolve/main/apps/web/src/components/ui/StatCard.tsx
- Command line
-
hf download hf://spaces/Benaah/MicProtocol/apps/web/src/components/ui/StatCard.tsx
-
curl -L -o StatCard.tsx https://huggingface.co/spaces/Benaah/MicProtocol/resolve/main/apps/web/src/components/ui/StatCard.tsx
4.52 kB
| "use client"; | |
| import React, { useEffect, useState } from "react"; | |
| import { ArrowUp, ArrowDown, Activity } from "lucide-react"; | |
| interface StatCardProps { | |
| label: string; | |
| value: number | string; | |
| trend?: "up" | "down" | "neutral"; | |
| trendLabels?: { | |
| label: string; | |
| value: string; | |
| }; | |
| previous?: number; | |
| delta?: number; | |
| goodWhen?: "up" | "down"; | |
| period?: string; | |
| format?: "number" | "currency" | "percent"; | |
| color?: string; | |
| animated?: boolean; | |
| className?: string; | |
| } | |
| export function StatCard({ | |
| label, | |
| value, | |
| trend = "neutral", | |
| trendLabels, | |
| previous, | |
| delta, | |
| goodWhen = "up", | |
| period, | |
| format = "number", | |
| color, | |
| animated = true, | |
| className, | |
| }: StatCardProps) { | |
| const [displayValue, setDisplayValue] = useState( | |
| typeof value === "number" ? 0 : value | |
| ); | |
| // Count-up animation | |
| useEffect(() => { | |
| if (!animated || typeof value !== "number") return; | |
| let start = 0; | |
| const end = value; | |
| const duration = 1000; | |
| const startTime = Date.now(); | |
| const animate = () => { | |
| const elapsed = Date.now() - startTime; | |
| const progress = Math.min(elapsed / duration, 1); | |
| const current = start + (end - start) * progress; | |
| setDisplayValue(current); | |
| if (progress < 1) { | |
| requestAnimationFrame(animate); | |
| } | |
| }; | |
| animate(); | |
| }, [value, animated]); | |
| const formatValue = (val: number | string) => { | |
| if (typeof val === "string") return val; | |
| if (format === "currency") | |
| return new Intl.NumberFormat("en-KE", { | |
| style: "currency", | |
| currency: "KES", | |
| }).format(val); | |
| if (format === "percent") return `${val}%`; | |
| return val.toLocaleString(); | |
| }; | |
| const isPositive = delta != null ? delta >= 0 : trend === "up"; | |
| const isGood = | |
| goodWhen === "up" ? isPositive : !isPositive; | |
| const trendColor = isGood | |
| ? "var(--color-accent)" | |
| : "var(--color-error)"; | |
| const trendBg = isGood | |
| ? "rgba(16, 185, 129, 0.12)" | |
| : "rgba(239, 68, 68, 0.12)"; | |
| const showDelta = delta != null || trendLabels?.value; | |
| const deltaText = | |
| delta != null | |
| ? `${isPositive ? "+" : ""}${delta.toLocaleString()}` | |
| : trendLabels?.value ?? ""; | |
| const sparklineColor = color || "var(--color-primary)"; | |
| // Simple sparkline data (mock) | |
| const sparklinePoints = [30, 50, 40, 70, 60, 90, 75, 110]; | |
| return ( | |
| <div | |
| className={[ | |
| "rounded-xl border border-border-color bg-bg-elevated p-5 shadow-sm transition-all", | |
| className, | |
| ].filter(Boolean).join(" ")} | |
| style={{ borderColor: "var(--border-color)" }} | |
| > | |
| <div className="flex items-start justify-between mb-3"> | |
| <div> | |
| <p className="text-text-tertiary text-xs uppercase tracking-wider font-medium"> | |
| {label} | |
| </p> | |
| <p className="text-2xl font-display font-bold text-text-primary"> | |
| {formatValue(displayValue)} | |
| </p> | |
| </div> | |
| <div className="w-10 h-10 rounded-md bg-bg-tertiary flex items-center justify-center border border-border-color-subtle"> | |
| <Activity size={16} className="text-text-tertiary" style={{ color: sparklineColor }} /> | |
| </div> | |
| </div> | |
| {showDelta && ( | |
| <div className="flex items-center gap-2 mb-2"> | |
| {trend === "up" ? ( | |
| <ArrowUp size={12} className={trendColor} /> | |
| ) : trend === "down" ? ( | |
| <ArrowDown size={12} className={trendColor} /> | |
| ) : ( | |
| <span className="w-3 h-3 rounded-full bg-text-tertiary" /> | |
| )} | |
| <span | |
| className="text-xs font-semibold" | |
| style={{ color: trendColor }} | |
| > | |
| {deltaText} | |
| </span> | |
| <span className="text-text-tertiary text-xs"> | |
| {trendLabels?.label ?? ""} | |
| </span> | |
| </div> | |
| )} | |
| {period && ( | |
| <p className="text-text-tertiary text-xs"> | |
| {period} | |
| </p> | |
| )} | |
| {/* Sparkline */} | |
| <div className="mt-3 h-10 w-full"> | |
| <svg | |
| className="w-full h-full" | |
| viewBox="0 0 100 40" | |
| fill="none" | |
| > | |
| <polyline | |
| points={sparklinePoints | |
| .map((p, i) => `${i * 14.28},${40 - p}`) | |
| .join(" ")} | |
| stroke={sparklineColor} | |
| strokeWidth="2" | |
| strokeLinecap="round" | |
| strokeLinejoin="round" | |
| fill="none" | |
| className="opacity-60" | |
| /> | |
| </svg> | |
| </div> | |
| </div> | |
| ); | |
| } | |