refactor: refactor the color schema
This commit is contained in:
@@ -7,7 +7,6 @@ import {
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select"
|
||||
import { FolderOpen, MapPin, Building2 } from "lucide-react"
|
||||
import { type Project } from "@/lib/api"
|
||||
|
||||
interface CompactProjectSelectorProps {
|
||||
@@ -26,7 +25,7 @@ export function CompactProjectSelector({
|
||||
isLoading = false
|
||||
}: CompactProjectSelectorProps) {
|
||||
return (
|
||||
<div className="flex flex-wrap items-center gap-3 p-3 rounded-xl bg-white/60 dark:bg-gray-800/60 backdrop-blur-sm border border-gray-200/50 dark:border-gray-700/50">
|
||||
<div className="flex flex-wrap items-center gap-3 p-3 rounded-md border">
|
||||
{/* Project Dropdown */}
|
||||
<div className="flex flex-col min-w-[120px]">
|
||||
<span className="text-[10px] font-bold uppercase tracking-wider text-muted-foreground leading-none mb-1">Project</span>
|
||||
@@ -35,7 +34,7 @@ export function CompactProjectSelector({
|
||||
onValueChange={onProjectChange}
|
||||
disabled={isLoading || projects.length === 0}
|
||||
>
|
||||
<SelectTrigger className="h-auto p-0 border-0 shadow-none focus:ring-0 text-sm font-bold text-gray-900 dark:text-white bg-transparent text-left">
|
||||
<SelectTrigger className="h-auto p-0 border-0 shadow-none focus:ring-0 text-sm font-bold bg-transparent text-left">
|
||||
<SelectValue placeholder="Select project" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
@@ -51,10 +50,10 @@ export function CompactProjectSelector({
|
||||
{/* Corridor Badge */}
|
||||
{selectedProject?.corridor_name && (
|
||||
<>
|
||||
<div className="h-8 w-px bg-gray-200 dark:bg-gray-700 mx-2" />
|
||||
<div className="h-8 w-px bg-border mx-2" />
|
||||
<div className="flex flex-col">
|
||||
<span className="text-[10px] font-bold uppercase tracking-wider text-muted-foreground leading-none mb-1">Corridor</span>
|
||||
<span className="text-xs font-bold text-gray-600 dark:text-gray-400">
|
||||
<span className="text-xs font-bold text-muted-foreground">
|
||||
{selectedProject.corridor_name}
|
||||
</span>
|
||||
</div>
|
||||
@@ -64,10 +63,10 @@ export function CompactProjectSelector({
|
||||
{/* State Badge */}
|
||||
{selectedProject?.state && (
|
||||
<>
|
||||
<div className="h-8 w-px bg-gray-200 dark:bg-gray-700 mx-2" />
|
||||
<div className="h-8 w-px bg-border mx-2" />
|
||||
<div className="flex flex-col">
|
||||
<span className="text-[10px] font-bold uppercase tracking-wider text-muted-foreground leading-none mb-1">State</span>
|
||||
<span className="text-xs font-bold text-gray-600 dark:text-gray-400">
|
||||
<span className="text-xs font-bold text-muted-foreground">
|
||||
{selectedProject.state}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
import { useEffect, useState } from "react"
|
||||
import dynamic from "next/dynamic"
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
|
||||
import { Loader2, MapPin, AlertTriangle, RectangleHorizontal } from "lucide-react"
|
||||
import { fetchAllDetections, type Detection } from "@/lib/api"
|
||||
import { Loader2, MapPin } from "lucide-react"
|
||||
import { type Detection } from "@/lib/api"
|
||||
|
||||
// Dynamically import the map to avoid SSR issues with Leaflet
|
||||
const DashboardMapContent = dynamic(
|
||||
@@ -89,18 +89,18 @@ export function DashboardMap({
|
||||
}
|
||||
|
||||
return (
|
||||
<Card className={`overflow-hidden pb-0 bg-white/60 rounded-b-none shadow-none border-none ${className}`}>
|
||||
<Card className={`overflow-hidden ${className}`}>
|
||||
<CardHeader className="pb-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-10 h-10 rounded-md bg-linear-to-b from-[#225999] to-[#56A5FF] flex items-center justify-center ">
|
||||
<MapPin className="h-5 w-5 text-white" />
|
||||
<div className="p-2 rounded-md bg-secondary text-secondary-foreground flex items-center justify-center">
|
||||
<MapPin className="h-5 w-5" />
|
||||
</div>
|
||||
<div>
|
||||
<CardTitle className="text-base font-bold text-[#2563eb]">
|
||||
<CardTitle className="text-base font-bold">
|
||||
Detection Map
|
||||
</CardTitle>
|
||||
<p className="text-xs text-gray-500 dark:text-gray-400">
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{isLoading ? "Loading..." : `${validDetections.length} detections with GPS coordinates`}
|
||||
</p>
|
||||
</div>
|
||||
@@ -109,43 +109,43 @@ export function DashboardMap({
|
||||
{/* Compact Color Legend */}
|
||||
<div className="flex flex-wrap items-center justify-end gap-x-4 gap-y-1 text-[10px] max-w-[60%]">
|
||||
<div className="flex items-center gap-1">
|
||||
<div className="w-2.5 h-2.5 rounded-full bg-[#ef4444] shadow-sm shadow-[#ef4444]/30" />
|
||||
<span className="text-gray-600 dark:text-gray-400 font-medium">Potholes ({counts.pothole})</span>
|
||||
<div className="w-2.5 h-2.5 rounded-full bg-red-500" />
|
||||
<span className="text-muted-foreground font-medium">Potholes ({counts.pothole})</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-1">
|
||||
<div className="w-2.5 h-2.5 rounded-full bg-[#3b82f6] shadow-sm shadow-[#3b82f6]/30" />
|
||||
<span className="text-gray-600 dark:text-gray-400 font-medium">Defected Signs ({counts.defected_sign_board})</span>
|
||||
<div className="w-2.5 h-2.5 rounded-full bg-blue-500" />
|
||||
<span className="text-muted-foreground font-medium">Defected Signs ({counts.defected_sign_board})</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-1">
|
||||
<div className="w-2.5 h-2.5 rounded-full bg-[#f59e0b] shadow-sm shadow-[#f59e0b]/30" />
|
||||
<span className="text-gray-600 dark:text-gray-400 font-medium">Cracks ({counts.road_crack})</span>
|
||||
<div className="w-2.5 h-2.5 rounded-full bg-amber-500" />
|
||||
<span className="text-muted-foreground font-medium">Cracks ({counts.road_crack})</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-1">
|
||||
<div className="w-2.5 h-2.5 rounded-full bg-[#6366f1] shadow-sm shadow-[#6366f1]/30" />
|
||||
<span className="text-gray-600 dark:text-gray-400 font-medium">Markings ({counts.damaged_road_marking})</span>
|
||||
<div className="w-2.5 h-2.5 rounded-full bg-indigo-500" />
|
||||
<span className="text-muted-foreground font-medium">Markings ({counts.damaged_road_marking})</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-1">
|
||||
<div className="w-2.5 h-2.5 rounded-full bg-[#10b981] shadow-sm shadow-[#10b981]/30" />
|
||||
<span className="text-gray-600 dark:text-gray-400 font-medium">Good Signs ({counts.good_sign_board})</span>
|
||||
<div className="w-2.5 h-2.5 rounded-full bg-emerald-500" />
|
||||
<span className="text-muted-foreground font-medium">Good Signs ({counts.good_sign_board})</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent className="p-0">
|
||||
<div className="h-[500px] p-2 w-full relative">
|
||||
<div className="h-[500px] p-2 w-full relative">
|
||||
{isLoading ? (
|
||||
<div className="h-full w-full flex items-center justify-center bg-gray-50 dark:bg-gray-900/50">
|
||||
<Loader2 className="h-8 w-8 text-indigo-500" />
|
||||
<div className="h-full w-full flex items-center justify-center bg-muted/50">
|
||||
<Loader2 className="h-8 w-8 text-primary animate-spin" />
|
||||
</div>
|
||||
) : error ? (
|
||||
<div className="h-full w-full flex items-center justify-center bg-gray-50 dark:bg-gray-900/50">
|
||||
<p className="text-gray-500">{error}</p>
|
||||
<div className="h-full w-full flex items-center justify-center bg-muted/50">
|
||||
<p className="text-muted-foreground">{error}</p>
|
||||
</div>
|
||||
) : validDetections.length === 0 ? (
|
||||
<div className="h-full w-full flex items-center justify-center bg-gray-50 dark:bg-gray-900/50">
|
||||
<div className="h-full w-full flex items-center justify-center bg-muted/50">
|
||||
<div className="text-center">
|
||||
<p className="text-gray-500">No detections with GPS coordinates found</p>
|
||||
<p className="text-sm text-gray-400 mt-1">Process some videos to see detections on the map</p>
|
||||
<p className="text-muted-foreground">No detections with GPS coordinates found</p>
|
||||
<p className="text-sm text-muted-foreground mt-1 opacity-70">Process some videos to see detections on the map</p>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
|
||||
@@ -9,7 +9,7 @@ export function DashboardSkeleton() {
|
||||
{/* Stats Cards Skeleton */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 mb-6">
|
||||
{[...Array(6)].map((_, i) => (
|
||||
<Card key={i} className="bg-white/60 backdrop-blur-lg border-none shadow-none overflow-hidden py-8 px-6">
|
||||
<Card key={i} className="py-8 px-6">
|
||||
<CardContent className="p-0 flex items-center justify-between gap-6">
|
||||
<div className="flex flex-col gap-2 flex-1">
|
||||
<Skeleton className="h-4 w-32" />
|
||||
@@ -25,8 +25,8 @@ export function DashboardSkeleton() {
|
||||
{/* Charts Row Skeleton */}
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4 mb-4">
|
||||
{[...Array(2)].map((_, i) => (
|
||||
<Card key={i} className="rounded-md bg-white/60 backdrop-blur-lg overflow-hidden border-none shadow-none">
|
||||
<CardHeader className="pb-2 border-none">
|
||||
<Card key={i}>
|
||||
<CardHeader className="pb-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<Skeleton className="w-10 h-10 rounded-md" />
|
||||
<Skeleton className="h-6 w-48" />
|
||||
@@ -40,7 +40,7 @@ export function DashboardSkeleton() {
|
||||
</div>
|
||||
|
||||
{/* Map Skeleton */}
|
||||
<Card className="rounded-md bg-white/60 backdrop-blur-lg overflow-hidden border-none shadow-none">
|
||||
<Card>
|
||||
<CardHeader className="pb-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
|
||||
@@ -112,7 +112,7 @@ export function DetectionDonutChart({
|
||||
if (active && payload && payload.length) {
|
||||
const data = payload[0]
|
||||
return (
|
||||
<div className="bg-background/95 backdrop-blur-sm border border-border rounded-lg px-3 py-2 shadow-lg">
|
||||
<div className="bg-popover border border-border rounded-lg px-3 py-2 shadow-lg">
|
||||
<p className="font-medium text-sm">{data.name}</p>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Count: <span className="font-semibold text-foreground">{data.value}</span>
|
||||
|
||||
@@ -36,26 +36,26 @@ export function FilterSelector({
|
||||
isLoading = false
|
||||
}: FilterSelectorProps) {
|
||||
return (
|
||||
<div className="flex flex-col gap-4 p-4">
|
||||
<div className="flex flex-col gap-5 p-5">
|
||||
{/* Project Dropdown */}
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<div className="p-1 rounded-md bg-blue-100 dark:bg-blue-900/50">
|
||||
<FolderOpen className="h-3.5 w-3.5 text-blue-600" />
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center gap-2.5">
|
||||
<div className="p-2 rounded-lg bg-gray-900 dark:bg-gray-100 text-gray-100 dark:text-gray-900 shadow-sm">
|
||||
<FolderOpen className="h-4 w-4" />
|
||||
</div>
|
||||
<span className="text-[11px] font-bold text-gray-400 uppercase tracking-wider">Project</span>
|
||||
<span className="text-[11px] font-black text-muted-foreground uppercase tracking-[0.15em]">Project</span>
|
||||
</div>
|
||||
<Select
|
||||
value={selectedProjectId || ""}
|
||||
onValueChange={onProjectChange}
|
||||
disabled={isLoading || projects.length === 0}
|
||||
>
|
||||
<SelectTrigger className="h-9 text-sm bg-slate-50 border border-slate-200 shadow-none ring-0! focus:ring-0 px-3 w-full rounded-lg">
|
||||
<SelectTrigger className="h-10 text-sm w-full bg-background border-border/60 shadow-xs">
|
||||
<SelectValue placeholder="Select project" />
|
||||
</SelectTrigger>
|
||||
<SelectContent className="rounded-xl border-slate-200">
|
||||
<SelectContent>
|
||||
{projects.map((project) => (
|
||||
<SelectItem key={project.id} value={project.id} className="rounded-lg">
|
||||
<SelectItem key={project.id} value={project.id}>
|
||||
{project.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
@@ -65,25 +65,25 @@ export function FilterSelector({
|
||||
|
||||
{/* Package Dropdown */}
|
||||
{selectedProjectId && (
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<div className="p-1 rounded-md bg-emerald-100 dark:bg-emerald-900/50">
|
||||
<Package className="h-3.5 w-3.5 text-emerald-600" />
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center gap-2.5">
|
||||
<div className="p-2 rounded-lg bg-gray-900 dark:bg-gray-100 text-gray-100 dark:text-gray-900 shadow-sm">
|
||||
<Package className="h-4 w-4" />
|
||||
</div>
|
||||
<span className="text-[11px] font-bold text-gray-400 uppercase tracking-wider">Package</span>
|
||||
<span className="text-[11px] font-black text-muted-foreground uppercase tracking-[0.15em]">Package</span>
|
||||
</div>
|
||||
<Select
|
||||
value={selectedPackageId || "all"}
|
||||
onValueChange={onPackageChange}
|
||||
disabled={isLoading}
|
||||
>
|
||||
<SelectTrigger className="h-9 text-sm bg-slate-50 border border-slate-200 shadow-none ring-0! focus:ring-0 px-3 w-full rounded-lg">
|
||||
<SelectTrigger className="h-10 text-sm w-full bg-background border-border/60 shadow-xs">
|
||||
<SelectValue placeholder="All packages" />
|
||||
</SelectTrigger>
|
||||
<SelectContent className="rounded-xl border-slate-200">
|
||||
<SelectItem value="all" className="rounded-lg">All Packages</SelectItem>
|
||||
<SelectContent>
|
||||
<SelectItem value="all">All Packages</SelectItem>
|
||||
{packages.map((pkg) => (
|
||||
<SelectItem key={pkg.id} value={pkg.id} className="rounded-lg">
|
||||
<SelectItem key={pkg.id} value={pkg.id}>
|
||||
{pkg.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
@@ -94,25 +94,25 @@ export function FilterSelector({
|
||||
|
||||
{/* Location Dropdown */}
|
||||
{selectedPackageId && selectedPackageId !== "all" && (
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<div className="p-1 rounded-md bg-purple-100 dark:bg-purple-900/50">
|
||||
<MapPin className="h-3.5 w-3.5 text-purple-600" />
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center gap-2.5">
|
||||
<div className="p-2 rounded-lg bg-gray-900 dark:bg-gray-100 text-gray-100 dark:text-gray-900 shadow-sm">
|
||||
<MapPin className="h-4 w-4" />
|
||||
</div>
|
||||
<span className="text-[11px] font-bold text-gray-400 uppercase tracking-wider">Location</span>
|
||||
<span className="text-[11px] font-black text-muted-foreground uppercase tracking-[0.15em]">Location</span>
|
||||
</div>
|
||||
<Select
|
||||
value={selectedLocationId || "all"}
|
||||
onValueChange={onLocationChange}
|
||||
disabled={isLoading}
|
||||
>
|
||||
<SelectTrigger className="h-9 text-sm bg-slate-50 border border-slate-200 shadow-none ring-0! focus:ring-0 px-3 w-full rounded-lg">
|
||||
<SelectTrigger className="h-10 text-sm w-full bg-background border-border/60 shadow-xs">
|
||||
<SelectValue placeholder="All locations" />
|
||||
</SelectTrigger>
|
||||
<SelectContent className="rounded-xl border-slate-200">
|
||||
<SelectItem value="all" className="rounded-lg">All Locations</SelectItem>
|
||||
<SelectContent>
|
||||
<SelectItem value="all">All Locations</SelectItem>
|
||||
{locations.map((loc) => (
|
||||
<SelectItem key={loc.id} value={loc.id} className="rounded-lg">
|
||||
<SelectItem key={loc.id} value={loc.id}>
|
||||
{loc.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
|
||||
@@ -2,88 +2,41 @@
|
||||
|
||||
import { Card, CardContent } from "@/components/ui/card"
|
||||
import { LucideIcon } from "lucide-react"
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
interface GradientStatsCardProps {
|
||||
title: string
|
||||
subtitle?: string
|
||||
value: number | string
|
||||
icon: LucideIcon
|
||||
gradient: "green" | "coral" | "blue" | "purple" | "orange" | "indigo" | "emerald"
|
||||
gradient?: "green" | "coral" | "blue" | "purple" | "orange" | "indigo" | "emerald"
|
||||
isLoading?: boolean
|
||||
}
|
||||
|
||||
const gradientStyles = {
|
||||
green: {
|
||||
background: "bg-card",
|
||||
border: "border-l-4 border-l-[var(--border)]",
|
||||
iconBg: "bg-gradient-to-br from-blue-400 to-blue-600",
|
||||
|
||||
},
|
||||
coral: {
|
||||
background: "bg-card",
|
||||
border: "border-l-4 border-l-[var(--border)]",
|
||||
iconBg: "bg-gradient-to-br from-blue-400 to-blue-600",
|
||||
|
||||
},
|
||||
blue: {
|
||||
background: "bg-card",
|
||||
border: "border-l-4 border-l-[var(--border)]",
|
||||
iconBg: "bg-gradient-to-br from-blue-400 to-blue-600",
|
||||
|
||||
},
|
||||
purple: {
|
||||
background: "bg-card",
|
||||
border: "border-l-4 border-l-[var(--border)]",
|
||||
iconBg: "bg-gradient-to-br from-blue-400 to-blue-600",
|
||||
|
||||
},
|
||||
orange: {
|
||||
background: "bg-card",
|
||||
border: "border-l-4 border-l-[var(--border)]",
|
||||
iconBg: "bg-gradient-to-br from-blue-400 to-blue-600",
|
||||
|
||||
},
|
||||
indigo: {
|
||||
background: "bg-card",
|
||||
border: "border-l-4 border-l-[var(--border)]",
|
||||
iconBg: "bg-gradient-to-br from-blue-400 to-blue-600",
|
||||
|
||||
},
|
||||
emerald: {
|
||||
background: "bg-card",
|
||||
border: "border-l-4 border-l-[var(--border)]",
|
||||
iconBg: "bg-gradient-to-br from-blue-400 to-blue-600",
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
export function GradientStatsCard({
|
||||
title,
|
||||
subtitle,
|
||||
value,
|
||||
icon: Icon,
|
||||
gradient,
|
||||
isLoading = false
|
||||
}: GradientStatsCardProps) {
|
||||
const styles = gradientStyles[gradient]
|
||||
|
||||
return (
|
||||
<Card className="bg-white/60 backdrop-blur-lg border-none shadow-none hover:shadow-none overflow-hidden py-8 px-6">
|
||||
<CardContent className="p-0 flex items-center justify-between gap-6">
|
||||
<Card className="overflow-hidden border shadow-sm">
|
||||
<CardContent className="p-6 flex items-center justify-between gap-4">
|
||||
<div className="flex flex-col gap-1 min-w-0">
|
||||
<h3 className="text-sm font-bold text-blue-600/70 dark:text-blue-400/70 uppercase tracking-widest">
|
||||
<h3 className="text-[10px] font-bold text-muted-foreground uppercase tracking-widest">
|
||||
{title}
|
||||
</h3>
|
||||
<div className="flex flex-col">
|
||||
{isLoading ? (
|
||||
<div className="h-14 w-24 bg-gray-100 dark:bg-gray-800 rounded-xl mt-2" />
|
||||
<div className="h-10 w-24 bg-muted animate-pulse rounded-md mt-1" />
|
||||
) : (
|
||||
<>
|
||||
<p className="text-4xl font-bold heading-blue-gradient">
|
||||
<p className="text-3xl font-bold tracking-tight">
|
||||
{value}
|
||||
</p>
|
||||
{subtitle && (
|
||||
<p className="text-xs font-medium text-gray-500 mt-1">
|
||||
<p className="text-[10px] font-semibold text-muted-foreground mt-0.5">
|
||||
{subtitle}
|
||||
</p>
|
||||
)}
|
||||
@@ -92,14 +45,8 @@ export function GradientStatsCard({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
style={{
|
||||
background: "linear-gradient(180deg, #225999 0%, #56A5FF 100%)",
|
||||
}}
|
||||
className={`
|
||||
w-16 h-16 self-start rounded-lg flex items-center justify-center
|
||||
`}>
|
||||
<Icon className="h-10 w-10 text-white" />
|
||||
<div className="p-3 rounded-lg bg-secondary shrink-0">
|
||||
<Icon className="h-6 w-6 text-primary" />
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
@@ -100,7 +100,7 @@ export function LocationBarChart({ data, isLoading = false }: LocationBarChartPr
|
||||
content={({ active, payload, label }) => {
|
||||
if (active && payload && payload.length) {
|
||||
return (
|
||||
<div className="bg-background/95 backdrop-blur-sm border border-border rounded-lg px-3 py-2 shadow-lg">
|
||||
<div className="bg-popover border border-border rounded-lg px-3 py-2 shadow-lg">
|
||||
<p className="font-medium text-xs mb-1">{label}</p>
|
||||
{payload.map((entry, index) => (
|
||||
<div key={index} className="flex items-center gap-2 text-xs">
|
||||
|
||||
52
src/components/dashboard/stats-card.tsx
Normal file
52
src/components/dashboard/stats-card.tsx
Normal file
@@ -0,0 +1,52 @@
|
||||
"use client"
|
||||
|
||||
import { Card, CardContent } from "@/components/ui/card"
|
||||
import { LucideIcon } from "lucide-react"
|
||||
|
||||
interface StatsCardProps {
|
||||
title: string
|
||||
subtitle?: string
|
||||
value: number | string
|
||||
icon: LucideIcon
|
||||
isLoading?: boolean
|
||||
}
|
||||
|
||||
export function StatsCard({
|
||||
title,
|
||||
subtitle,
|
||||
value,
|
||||
icon: Icon,
|
||||
isLoading = false
|
||||
}: StatsCardProps) {
|
||||
return (
|
||||
<Card className="py-6 px-6">
|
||||
<CardContent className="p-0 flex items-center justify-between gap-6">
|
||||
<div className="flex flex-col gap-1 min-w-0">
|
||||
<h3 className="text-sm font-semibold text-muted-foreground uppercase tracking-wider">
|
||||
{title}
|
||||
</h3>
|
||||
<div className="flex flex-col">
|
||||
{isLoading ? (
|
||||
<div className="h-10 w-24 bg-muted rounded-md mt-2 animate-pulse" />
|
||||
) : (
|
||||
<>
|
||||
<p className="text-3xl font-bold">
|
||||
{value}
|
||||
</p>
|
||||
{subtitle && (
|
||||
<p className="text-xs text-muted-foreground mt-1">
|
||||
{subtitle}
|
||||
</p>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="p-3 rounded-md bg-secondary text-secondary-foreground">
|
||||
<Icon className="h-6 w-6" />
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user