59 lines
1.8 KiB
TypeScript
59 lines
1.8 KiB
TypeScript
import { motion } from 'framer-motion'
|
|
import { TrendingUp, TrendingDown, Minus } from 'lucide-react'
|
|
import { cn } from '@/lib/utils'
|
|
|
|
interface KPICardProps {
|
|
label: string
|
|
value: string
|
|
change: number
|
|
changeLabel: string
|
|
icon: React.ReactNode
|
|
index?: number
|
|
}
|
|
|
|
export function KPICard({ label, value, change, changeLabel, icon, index = 0 }: KPICardProps) {
|
|
const isPositive = change > 0
|
|
const isNeutral = change === 0
|
|
|
|
return (
|
|
<motion.div
|
|
initial={{ opacity: 0, y: 8 }}
|
|
animate={{ opacity: 1, y: 0 }}
|
|
transition={{ duration: 0.18, delay: index * 0.04, ease: 'easeOut' }}
|
|
className="bg-surface rounded-[20px] card-shadow p-6 md:p-7"
|
|
>
|
|
<div className="flex items-start justify-between">
|
|
<div className="space-y-4">
|
|
<p className="text-sm text-text-secondary">{label}</p>
|
|
<p className="text-2xl md:text-3xl font-semibold text-text-primary tracking-tight">
|
|
{value}
|
|
</p>
|
|
</div>
|
|
<div className="w-10 h-10 rounded-xl bg-bg flex items-center justify-center text-text-secondary">
|
|
{icon}
|
|
</div>
|
|
</div>
|
|
<div className="mt-5 flex items-center gap-2">
|
|
<span
|
|
className={cn(
|
|
'inline-flex items-center gap-1 text-xs font-medium px-2 py-0.5 rounded-full',
|
|
isPositive && 'bg-success-light text-success',
|
|
isNeutral && 'bg-bg text-text-secondary',
|
|
!isPositive && !isNeutral && 'bg-danger-light text-danger'
|
|
)}
|
|
>
|
|
{isPositive ? (
|
|
<TrendingUp size={12} />
|
|
) : isNeutral ? (
|
|
<Minus size={12} />
|
|
) : (
|
|
<TrendingDown size={12} />
|
|
)}
|
|
{Math.abs(change)}%
|
|
</span>
|
|
<span className="text-xs text-text-secondary">{changeLabel}</span>
|
|
</div>
|
|
</motion.div>
|
|
)
|
|
}
|