tercul-frontend/client/src/components/ui/stat-card.tsx
mukimovd 6b00938084 Build out initial UI components and component implementation tracker
Implement ActivityFeed, DashboardHeader, EmptyState, StatCard, TagInput, DataTable, WorkPreview components and add component implementation tracker.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: bddfbb2b-6d6b-457b-b18c-05792cdaa035
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/39b5c689-6e8a-4d5a-9792-69cc81a56534/90ee7bd9-7d3b-4bfb-93e2-2aa13b83f414.jpg
2025-05-10 21:07:28 +00:00

141 lines
3.9 KiB
TypeScript

import { cn } from "@/lib/utils";
import { cva, type VariantProps } from "class-variance-authority";
import { LucideIcon } from "lucide-react";
/**
* Stat card component for displaying statistical metrics
*
* @example
* ```tsx
* <StatCard
* title="Total Works"
* value="124"
* description="+12% from last month"
* trend="up"
* icon={Book}
* />
* ```
*/
const statCardVariants = cva(
"rounded-lg border p-4 flex flex-col space-y-2",
{
variants: {
variant: {
default: "bg-background border-border",
primary: "bg-primary/10 border-primary/20",
secondary: "bg-secondary/10 border-secondary/20",
accent: "bg-russet/10 border-russet/20 dark:bg-russet/5",
success: "bg-emerald-50 border-emerald-200 dark:bg-emerald-950/20 dark:border-emerald-900/30",
warning: "bg-amber-50 border-amber-200 dark:bg-amber-950/20 dark:border-amber-900/30",
danger: "bg-rose-50 border-rose-200 dark:bg-rose-950/20 dark:border-rose-900/30",
},
size: {
default: "p-4",
sm: "p-3",
lg: "p-6",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
}
);
export interface StatCardProps extends VariantProps<typeof statCardVariants> {
/**
* The title of the stat
*/
title: string;
/**
* The value to display
*/
value: string | number;
/**
* Optional description text (like change from previous period)
*/
description?: string;
/**
* Optional trend direction
*/
trend?: "up" | "down" | "neutral";
/**
* Optional icon to display
*/
icon?: LucideIcon;
/**
* Additional CSS classes
*/
className?: string;
/**
* Whether the stat is loading
*/
isLoading?: boolean;
}
export function StatCard({
title,
value,
description,
trend,
icon: Icon,
variant,
size,
className,
isLoading = false,
}: StatCardProps) {
// Determine the color for trend
const trendColor = trend === "up"
? "text-emerald-600 dark:text-emerald-400"
: trend === "down"
? "text-rose-600 dark:text-rose-400"
: "text-gray-600 dark:text-gray-400";
// Determine the arrow for trend
const trendSymbol = trend === "up"
? "↑"
: trend === "down"
? "↓"
: "→";
return (
<div className={cn(statCardVariants({ variant, size }), className)}>
{/* Header with icon and title */}
<div className="flex items-center justify-between">
<h3 className="text-sm font-medium text-muted-foreground">{title}</h3>
{Icon && (
<div className={cn(
"rounded-full p-1.5",
variant === "primary" && "bg-primary/10 text-primary",
variant === "secondary" && "bg-secondary/10 text-secondary",
variant === "accent" && "bg-russet/10 text-russet dark:bg-russet/5",
variant === "success" && "bg-emerald-100 text-emerald-600 dark:bg-emerald-900/30 dark:text-emerald-400",
variant === "warning" && "bg-amber-100 text-amber-600 dark:bg-amber-900/30 dark:text-amber-400",
variant === "danger" && "bg-rose-100 text-rose-600 dark:bg-rose-900/30 dark:text-rose-400",
!variant && "bg-gray-100 text-gray-600 dark:bg-gray-800 dark:text-gray-400",
)}>
<Icon className="h-4 w-4" />
</div>
)}
</div>
{/* Value */}
{isLoading ? (
<div className="h-8 w-24 bg-gray-200 dark:bg-gray-800 animate-pulse rounded" />
) : (
<div className="text-2xl font-bold">{value}</div>
)}
{/* Description with trend */}
{description && (
<div className="text-xs flex items-center gap-1">
{trend && <span className={cn("font-medium", trendColor)}>{trendSymbol}</span>}
<span className={cn(trend ? trendColor : "text-muted-foreground")}>
{description}
</span>
</div>
)}
</div>
);
}