/*
Copyright (C) 2023-2026 QuantumNous
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
published by the Free Software Foundation, either version 3 of the
License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see .
For commercial licensing, please contact support@quantumnous.com
*/
import type { ReactNode } from 'react'
import { type LucideIcon } from 'lucide-react'
import { cn } from '@/lib/utils'
import { Skeleton } from '@/components/ui/skeleton'
type StatCardTone = 'rose' | 'teal' | 'gray'
interface StatCardProps {
title: string
value: string | number
description: string
icon: LucideIcon
sparkline?: number[]
tone?: StatCardTone
loading?: boolean
error?: boolean
action?: ReactNode
}
const TONE_CLASSES: Record = {
rose: 'from-rose-500/80 via-rose-300/70 to-rose-200/20 dark:from-rose-400/70 dark:via-rose-500/30 dark:to-rose-500/5',
teal: 'from-teal-500/80 via-teal-300/70 to-teal-200/20 dark:from-teal-400/70 dark:via-teal-500/30 dark:to-teal-500/5',
gray: 'from-muted-foreground/50 via-muted-foreground/20 to-transparent dark:from-muted-foreground/40 dark:via-muted-foreground/20',
}
function normalizeSparkline(values?: number[]): number[] {
if (!values?.length) return []
const sanitized = values.map((value) => Math.max(0, Number(value) || 0))
const max = Math.max(...sanitized)
if (max <= 0) return sanitized.map(() => 0)
return sanitized.map((value) => Math.max(8, (value / max) * 100))
}
export function StatCard(props: StatCardProps) {
const Icon = props.icon
const tone = props.tone ?? 'gray'
const sparkline = normalizeSparkline(props.sparkline)
return (