135 lines
3.8 KiB
TypeScript
135 lines
3.8 KiB
TypeScript
"use client";
|
|
|
|
import { Card, CardContent } from "@/components/ui/card";
|
|
import { cn } from "@/lib/utils";
|
|
import { ReactNode } from "react";
|
|
|
|
interface StatCardProps {
|
|
title: string;
|
|
value: string | number;
|
|
icon: ReactNode;
|
|
trend?: {
|
|
value: number;
|
|
isPositive: boolean;
|
|
};
|
|
color?: "primary" | "accent" | "green" | "blue" | "purple" | "orange";
|
|
}
|
|
|
|
const colorVariants = {
|
|
primary: {
|
|
bg: "bg-primary-50",
|
|
icon: "bg-primary-100 text-primary-600",
|
|
text: "text-primary-700",
|
|
},
|
|
accent: {
|
|
bg: "bg-accent-50",
|
|
icon: "bg-accent-100 text-accent-600",
|
|
text: "text-accent-700",
|
|
},
|
|
green: {
|
|
bg: "bg-green-50",
|
|
icon: "bg-green-100 text-green-600",
|
|
text: "text-green-700",
|
|
},
|
|
blue: {
|
|
bg: "bg-blue-50",
|
|
icon: "bg-blue-100 text-blue-600",
|
|
text: "text-blue-700",
|
|
},
|
|
purple: {
|
|
bg: "bg-purple-50",
|
|
icon: "bg-purple-100 text-purple-600",
|
|
text: "text-purple-700",
|
|
},
|
|
orange: {
|
|
bg: "bg-orange-50",
|
|
icon: "bg-orange-100 text-orange-600",
|
|
text: "text-orange-700",
|
|
},
|
|
};
|
|
|
|
export function StatCard({ title, value, icon, trend, color = "primary" }: StatCardProps) {
|
|
const colors = colorVariants[color];
|
|
|
|
return (
|
|
<Card className="hover:shadow-md transition-shadow">
|
|
<CardContent className="p-6">
|
|
<div className="flex items-start justify-between">
|
|
<div className="flex-1">
|
|
<p className="text-sm font-medium text-primary-500 mb-1">{title}</p>
|
|
<p className="text-3xl font-bold text-primary-800">{value}</p>
|
|
{trend && (
|
|
<div className="flex items-center mt-2">
|
|
<span
|
|
className={cn(
|
|
"text-sm font-medium flex items-center gap-1",
|
|
trend.isPositive ? "text-green-600" : "text-red-600"
|
|
)}
|
|
>
|
|
{trend.isPositive ? (
|
|
<svg
|
|
className="w-4 h-4"
|
|
fill="none"
|
|
stroke="currentColor"
|
|
viewBox="0 0 24 24"
|
|
>
|
|
<path
|
|
strokeLinecap="round"
|
|
strokeLinejoin="round"
|
|
strokeWidth={2}
|
|
d="M5 10l7-7m0 0l7 7m-7-7v18"
|
|
/>
|
|
</svg>
|
|
) : (
|
|
<svg
|
|
className="w-4 h-4"
|
|
fill="none"
|
|
stroke="currentColor"
|
|
viewBox="0 0 24 24"
|
|
>
|
|
<path
|
|
strokeLinecap="round"
|
|
strokeLinejoin="round"
|
|
strokeWidth={2}
|
|
d="M19 14l-7 7m0 0l-7-7m7 7V3"
|
|
/>
|
|
</svg>
|
|
)}
|
|
{trend.isPositive ? "+" : ""}
|
|
{trend.value}%
|
|
</span>
|
|
<span className="text-xs text-primary-400 ml-1">vs yesterday</span>
|
|
</div>
|
|
)}
|
|
</div>
|
|
<div
|
|
className={cn(
|
|
"flex-shrink-0 w-12 h-12 rounded-lg flex items-center justify-center",
|
|
colors.icon
|
|
)}
|
|
>
|
|
{icon}
|
|
</div>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
);
|
|
}
|
|
|
|
// Loading skeleton for stat card
|
|
export function StatCardSkeleton() {
|
|
return (
|
|
<Card>
|
|
<CardContent className="p-6">
|
|
<div className="flex items-start justify-between animate-pulse">
|
|
<div className="flex-1">
|
|
<div className="h-4 bg-primary-100 rounded w-24 mb-2"></div>
|
|
<div className="h-8 bg-primary-100 rounded w-16"></div>
|
|
</div>
|
|
<div className="w-12 h-12 bg-primary-100 rounded-lg"></div>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
);
|
|
}
|