Stats
Stat cards with trends, badges, progress, and small charts.
Fromblocks.so
Stats with Trending
import { Card, CardContent } from "@/components/ui/card"
import { cn } from "@/lib/utils"
const data = [
{
name: "Profit",
value: "$287,654.00",
change: "+8.32%",
changeType: "positive",
},
{
name: "Late payments",
value: "$9,435.00",
change: "-12.64%",
changeType: "negative",
},
{
name: "Pending orders",
value: "$173,229.00",
change: "+2.87%",
changeType: "positive",
},
{
name: "Operating costs",
value: "$52,891.00",
change: "-5.73%",
changeType: "negative",
},
]
export default function Stats01() {
return (
<div className="flex items-center justify-center p-10">
<div className="mx-auto grid grid-cols-1 gap-px rounded-xl bg-border sm:grid-cols-2 lg:grid-cols-4">
{data.map((stat, index) => (
<Card
className={cn(
"rounded-none border-0 py-0 shadow-none",
index === 0 && "rounded-l-xl",
index === data.length - 1 && "rounded-r-xl"
)}
key={stat.name}
>
<CardContent className="flex flex-wrap items-baseline justify-between gap-x-4 gap-y-2 p-4 sm:p-6">
<div className="font-medium text-muted-foreground text-sm">
{stat.name}
</div>
<div
className={cn(
"font-medium text-xs tabular-nums",
stat.changeType === "positive"
? "text-green-800 dark:text-green-400"
: "text-red-800 dark:text-red-400"
)}
>
{stat.change}
</div>
<div className="w-full flex-none font-medium text-3xl text-foreground tabular-nums tracking-tight">
{stat.value}
</div>
</CardContent>
</Card>
))}
</div>
</div>
)
}npx shadcn@latest add https://blocks.so/r/stats-01.jsonStats with Borders
import { TrendingDown, TrendingUp } from "lucide-react"
import { Badge } from "@/components/ui/badge"
import { Card, CardContent, CardTitle } from "@/components/ui/card"
import { cn } from "@/lib/utils"
const stats = [
{
metric: "Active Users",
current: "128,456",
previous: "115,789",
difference: "10.9%",
trend: "up",
},
{
metric: "Conversion Rate",
current: "5.32%",
previous: "6.18%",
difference: "0.86%",
trend: "down",
},
{
metric: "Avg. Session Duration",
current: "3m 42s",
previous: "3m 15s",
difference: "13.8%",
trend: "up",
},
]
export default function Stats02() {
return (
<div className="flex items-center justify-center p-10">
<div className="grid grid-cols-1 divide-y divide-border overflow-hidden rounded-lg bg-border md:grid-cols-3 md:divide-x md:divide-y-0">
{stats.map((item) => (
<Card
className="rounded-none border-0 py-0 shadow-sm"
key={item.metric}
>
<CardContent className="p-4 sm:p-6">
<CardTitle className="font-normal text-base">
{item.metric}
</CardTitle>
<div className="mt-1 flex items-baseline gap-2 md:block lg:flex">
<div className="flex items-baseline font-semibold text-2xl text-primary tabular-nums">
{item.current}
<span className="ml-2 font-medium text-muted-foreground text-sm tabular-nums">
from {item.previous}
</span>
</div>
<Badge
className={cn(
"inline-flex h-auto items-center border-transparent px-1.5 py-0.5 ps-2.5 font-medium text-xs tabular-nums md:mt-2 lg:mt-0 [&>svg]:size-5!",
item.trend === "up"
? "bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-400"
: "bg-red-100 text-red-800 dark:bg-red-900/30 dark:text-red-400"
)}
variant="outline"
>
{item.trend === "up" ? (
<TrendingUp className="-ml-1 mr-0.5 h-5 w-5 shrink-0 self-center text-green-500" />
) : (
<TrendingDown className="-ml-1 mr-0.5 h-5 w-5 shrink-0 self-center text-red-500" />
)}
<span className="sr-only">
{" "}
{item.trend === "up" ? "Increased" : "Decreased"} by{" "}
</span>
{item.difference}
</Badge>
</div>
</CardContent>
</Card>
))}
</div>
</div>
)
}npx shadcn@latest add https://blocks.so/r/stats-02.jsonStats with Card Layout
"use client"
import { Card, CardContent } from "@/components/ui/card"
import { cn } from "@/lib/utils"
const data = [
{
name: "Unique visitors",
stat: "10,450",
change: "-12.5%",
changeType: "negative",
},
{
name: "Bounce rate",
stat: "56.1%",
change: "+1.8%",
changeType: "positive",
},
{
name: "Visit duration",
stat: "5.2min",
change: "+19.7%",
changeType: "positive",
},
{
name: "Conversion rate",
stat: "3.2%",
change: "-2.4%",
changeType: "negative",
},
]
export default function Stats03() {
return (
<div className="flex w-full items-center justify-center p-10">
<dl className="grid w-full grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-4">
{data.map((item) => (
<Card className="p-6 py-4 shadow-2xs" key={item.name}>
<CardContent className="p-0">
<dt className="font-medium text-muted-foreground text-sm">
{item.name}
</dt>
<dd className="mt-2 flex items-baseline space-x-2.5">
<span className="font-semibold text-3xl text-foreground tabular-nums">
{item.stat}
</span>
<span
className={cn(
item.changeType === "positive"
? "text-green-800 dark:text-green-400"
: "text-red-800 dark:text-red-400",
"font-medium text-sm"
)}
>
{item.change}
</span>
</dd>
</CardContent>
</Card>
))}
</dl>
</div>
)
}npx shadcn@latest add https://blocks.so/r/stats-03.jsonStats with Badges
"use client"
import { TrendingDown, TrendingUp } from "lucide-react"
import { Badge } from "@/components/ui/badge"
import { Card, CardContent } from "@/components/ui/card"
import { cn } from "@/lib/utils"
const data = [
{
name: "Daily active users",
stat: "3,450",
change: "+12.1%",
changeType: "positive",
},
{
name: "Weekly sessions",
stat: "1,342",
change: "-9.8%",
changeType: "negative",
},
{
name: "Duration",
stat: "5.2min",
change: "+7.7%",
changeType: "positive",
},
]
export default function Stats04() {
return (
<div className="flex w-full items-center justify-center p-10">
<dl className="grid w-full grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-3">
{data.map((item) => (
<Card className="w-full p-6 py-4 shadow-2xs" key={item.name}>
<CardContent className="p-0">
<div className="flex items-center justify-between">
<dt className="font-medium text-muted-foreground text-sm">
{item.name}
</dt>
<Badge
className={cn(
"tabular-nums",
item.changeType === "positive"
? "bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-400"
: "bg-red-100 text-red-800 dark:bg-red-900/30 dark:text-red-400"
)}
>
{item.changeType === "positive" ? (
<TrendingUp aria-hidden={true} data-icon="inline-start" />
) : (
<TrendingDown aria-hidden={true} data-icon="inline-start" />
)}
<span className="sr-only">
{item.changeType === "positive" ? "Increased" : "Decreased"}{" "}
by{" "}
</span>
{item.change}
</Badge>
</div>
<dd className="mt-2 font-semibold text-3xl text-foreground tabular-nums">
{item.stat}
</dd>
</CardContent>
</Card>
))}
</dl>
</div>
)
}npx shadcn@latest add https://blocks.so/r/stats-04.jsonStats with Links
"use client"
import { Card, CardContent, CardFooter } from "@/components/ui/card"
import { cn } from "@/lib/utils"
const data = [
{
name: "Monthly recurring revenue",
value: "$34.1K",
change: "+6.1%",
changeType: "positive",
href: "#",
},
{
name: "Users",
value: "500.1K",
change: "+19.2%",
changeType: "positive",
href: "#",
},
{
name: "User growth",
value: "11.3%",
change: "-1.2%",
changeType: "negative",
href: "#",
},
]
export default function Stats05() {
return (
<div className="flex w-full items-center justify-center p-10">
<dl className="grid w-full grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-3">
{data.map((item) => (
<Card className="gap-0 p-0 shadow-2xs" key={item.name}>
<CardContent className="p-6">
<dd className="flex items-start justify-between space-x-2">
<span className="truncate text-muted-foreground text-sm">
{item.name}
</span>
<span
className={cn(
"font-medium text-sm",
item.changeType === "positive"
? "text-emerald-700 dark:text-emerald-500"
: "text-red-700 dark:text-red-500"
)}
>
{item.change}
</span>
</dd>
<dd className="mt-1 font-semibold text-3xl text-foreground tabular-nums">
{item.value}
</dd>
</CardContent>
<CardFooter className="flex justify-end border-border border-t p-0!">
<a
className="px-6 py-3 font-medium text-primary text-sm hover:text-primary/90"
href={item.href}
>
View more →
</a>
</CardFooter>
</Card>
))}
</dl>
</div>
)
}npx shadcn@latest add https://blocks.so/r/stats-05.jsonStats with Status
"use client"
import { ArrowRight } from "lucide-react"
import Link from "next/link"
import { cn } from "@/lib/utils"
const statuses = {
within: { label: "On track", color: "bg-emerald-500" },
observe: { label: "Needs attention", color: "bg-amber-500" },
critical: { label: "At risk", color: "bg-red-500" },
}
const data = [
{
name: "Europe",
stat: "$10,023",
goalsAchieved: 3,
status: "observe",
href: "#",
},
{
name: "North America",
stat: "$14,092",
goalsAchieved: 5,
status: "within",
href: "#",
},
{
name: "Asia",
stat: "$113,232",
goalsAchieved: 1,
status: "critical",
href: "#",
},
] as {
name: string
stat: string
goalsAchieved: number
status: keyof typeof statuses
href: string
}[]
const goals = [1, 2, 3, 4, 5]
export default function Stats06() {
return (
<div className="@container flex w-full items-center justify-center p-10">
<dl className="grid w-full @2xl:grid-cols-3 grid-cols-1 @2xl:divide-x divide-y @2xl:divide-y-0 divide-foreground/10 overflow-hidden rounded-xl bg-background shadow-[0_0_0_1px_oklch(0_0_0/0.06),0_1px_2px_-1px_oklch(0_0_0/0.06),0_2px_4px_0_oklch(0_0_0/0.04)] dark:shadow-[0_0_0_1px_oklch(1_0_0/0.08)]">
{data.map((item) => {
const status = statuses[item.status]
return (
<div
className="group relative flex flex-col gap-6 p-6 hover:bg-muted/40"
key={item.name}
>
<div className="flex items-center justify-between gap-3">
<dt className="truncate font-medium text-foreground text-sm">
{item.name}
</dt>
<dd className="flex shrink-0 items-center gap-2 text-muted-foreground text-sm">
<span
aria-hidden={true}
className={cn("size-1.5 rounded-full", status.color)}
/>
{status.label}
</dd>
</div>
<dd className="font-semibold text-3xl text-foreground tabular-nums tracking-tight">
{item.stat}
</dd>
<dd className="flex flex-col gap-2">
<div className="flex items-center justify-between text-sm">
<span className="text-muted-foreground">Goals achieved</span>
<span className="font-medium text-foreground tabular-nums">
{item.goalsAchieved}/{goals.length}
</span>
</div>
<div aria-hidden={true} className="flex gap-1">
{goals.map((goal) => (
<span
className={cn(
"h-1.5 flex-1 rounded-full",
goal <= item.goalsAchieved
? status.color
: "bg-foreground/10"
)}
key={goal}
/>
))}
</div>
</dd>
<dd className="text-sm">
<Link
className="flex items-center gap-1 font-medium text-foreground focus:outline-none"
href={item.href}
>
<span aria-hidden={true} className="absolute inset-0" />
View region
<ArrowRight
aria-hidden={true}
className="size-4 shrink-0 transition-transform group-hover:translate-x-0.5"
/>
</Link>
</dd>
</div>
)
})}
</dl>
</div>
)
}npx shadcn@latest add https://blocks.so/r/stats-06.jsonStats with Circular Progress
"use client"
import { ExternalLink } from "lucide-react"
import Link from "next/link"
import { PolarAngleAxis, RadialBar, RadialBarChart } from "recharts"
import { Card, CardContent } from "@/components/ui/card"
import { type ChartConfig, ChartContainer } from "@/components/ui/chart"
const data = [
{
name: "Workspaces",
capacity: 20,
current: 1,
allowed: 5,
fill: "var(--chart-1)",
},
{
name: "Dashboards",
capacity: 10,
current: 2,
allowed: 20,
fill: "var(--chart-2)",
},
{
name: "Chart widgets",
capacity: 30,
current: 15,
allowed: 50,
fill: "var(--chart-3)",
},
{
name: "Storage",
capacity: 50,
current: 25,
allowed: 100,
fill: "var(--chart-4)",
},
]
const chartConfig = {
capacity: {
label: "Capacity",
color: "hsl(var(--primary))",
},
} satisfies ChartConfig
export default function Stats07() {
return (
<div className="flex w-full items-center justify-center p-10">
<div className="w-full">
<h2 className="text-balance font-medium text-foreground text-xl">
Plan overview
</h2>
<p className="mt-1 text-pretty text-muted-foreground text-sm leading-6">
You are currently on the{" "}
<span className="font-medium text-foreground">starter plan</span>.{" "}
<Link
className="inline-flex items-center gap-1 text-primary hover:underline hover:underline-offset-4"
href="#"
>
View other plans
<ExternalLink aria-hidden={true} className="size-4" />
</Link>
</p>
<dl className="mt-6 grid grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-4">
{data.map((item) => (
<Card className="p-4 shadow-2xs" key={item.name}>
<CardContent className="flex items-center space-x-4 p-0">
<div className="relative flex items-center justify-center">
<ChartContainer
className="h-[80px] w-[80px]"
config={chartConfig}
>
<RadialBarChart
barSize={6}
data={[item]}
endAngle={-270}
innerRadius={30}
outerRadius={40}
startAngle={90}
>
<PolarAngleAxis
angleAxisId={0}
axisLine={false}
domain={[0, 100]}
tick={false}
type="number"
/>
<RadialBar
angleAxisId={0}
background
cornerRadius={10}
dataKey="capacity"
fill="var(--primary)"
/>
</RadialBarChart>
</ChartContainer>
<div className="absolute inset-0 flex items-center justify-center">
<span className="font-medium text-base text-foreground">
{item.capacity}%
</span>
</div>
</div>
<div>
<dt className="font-medium text-foreground text-sm">
{item.name}
</dt>
<dd className="text-muted-foreground text-sm">
{item.current} of {item.allowed} used
</dd>
</div>
</CardContent>
</Card>
))}
</dl>
</div>
</div>
)
}npx shadcn@latest add https://blocks.so/r/stats-07.jsonStats with Circular Progress and Links
"use client"
import { PolarAngleAxis, RadialBar, RadialBarChart } from "recharts"
import { Card, CardContent, CardFooter } from "@/components/ui/card"
import { type ChartConfig, ChartContainer } from "@/components/ui/chart"
const data = [
{
name: "HR",
progress: 25,
budget: "$1,000",
current: "$250",
href: "#",
fill: "var(--chart-1)",
},
{
name: "Marketing",
progress: 55,
budget: "$1,000",
current: "$550",
href: "#",
fill: "var(--chart-2)",
},
{
name: "Finance",
progress: 85,
budget: "$1,000",
current: "$850",
href: "#",
fill: "var(--chart-3)",
},
{
name: "Engineering",
progress: 70,
budget: "$2,000",
current: "$1,400",
href: "#",
fill: "var(--chart-4)",
},
]
const chartConfig = {
progress: {
label: "Progress",
color: "var(--primary)",
},
} satisfies ChartConfig
export default function Stats08() {
return (
<div className="flex w-full items-center justify-center p-10">
<dl className="grid w-full grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-4">
{data.map((item) => (
<Card className="gap-0 p-0 shadow-2xs" key={item.name}>
<CardContent className="p-4">
<div className="flex items-center space-x-3">
<div className="relative flex items-center justify-center">
<ChartContainer
className="h-[80px] w-[80px]"
config={chartConfig}
>
<RadialBarChart
barSize={6}
data={[item]}
endAngle={-270}
innerRadius={30}
outerRadius={40}
startAngle={90}
>
<PolarAngleAxis
angleAxisId={0}
axisLine={false}
domain={[0, 100]}
tick={false}
type="number"
/>
<RadialBar
angleAxisId={0}
background
cornerRadius={10}
dataKey="progress"
fill={item.fill}
/>
</RadialBarChart>
</ChartContainer>
<div className="absolute inset-0 flex items-center justify-center">
<span className="font-medium text-base text-foreground">
{item.progress}%
</span>
</div>
</div>
<div>
<dd className="font-medium text-base text-foreground">
{item.current} / {item.budget}
</dd>
<dt className="text-muted-foreground text-sm">
Budget {item.name}
</dt>
</div>
</div>
</CardContent>
<CardFooter className="flex items-center justify-end border-border border-t p-0!">
<a
className="px-6 py-3 font-medium text-primary text-sm hover:text-primary/90"
href={item.href}
>
View more →
</a>
</CardFooter>
</Card>
))}
</dl>
</div>
)
}npx shadcn@latest add https://blocks.so/r/stats-08.jsonStats with Progress
"use client"
import { Card, CardContent } from "@/components/ui/card"
import { Progress } from "@/components/ui/progress"
const data = [
{
name: "Requests",
stat: "996",
limit: "10,000",
percentage: 9.96,
},
{
name: "Credits",
stat: "$672",
limit: "$1,000",
percentage: 67.2,
},
{
name: "Storage",
stat: "1.85",
limit: "10GB",
percentage: 18.5,
},
{
name: "API Calls",
stat: "4,328",
limit: "5,000",
percentage: 86.56,
},
]
export default function Stats09() {
return (
<div className="flex w-full items-center justify-center p-10">
<dl className="grid w-full grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-4">
{data.map((item) => (
<Card className="py-4 shadow-2xs" key={item.name}>
<CardContent className="">
<dt className="text-muted-foreground text-sm">{item.name}</dt>
<dd className="font-semibold text-2xl text-foreground tabular-nums">
{item.stat}
</dd>
<Progress className="mt-6 h-2" value={item.percentage} />
<dd className="mt-2 flex items-center justify-between text-sm">
<span className="text-primary">{item.percentage}%</span>
<span className="text-muted-foreground">
{item.stat} of {item.limit}
</span>
</dd>
</CardContent>
</Card>
))}
</dl>
</div>
)
}npx shadcn@latest add https://blocks.so/r/stats-09.jsonStats with Area Chart
"use client"
import { Area, AreaChart, XAxis } from "recharts"
import { Card, CardContent } from "@/components/ui/card"
import { ChartContainer } from "@/components/ui/chart"
import { cn } from "@/lib/utils"
const data = [
{
date: "Nov 24, 2023",
"Alpha Corp": 142.87,
"Beta Solutions": 65.32,
"Gamma Industries": 83.25,
},
{
date: "Nov 25, 2023",
"Alpha Corp": 151.43,
"Beta Solutions": 59.78,
"Gamma Industries": 79.64,
},
{
date: "Nov 26, 2023",
"Alpha Corp": 157.28,
"Beta Solutions": 64.21,
"Gamma Industries": 76.19,
},
{
date: "Nov 27, 2023",
"Alpha Corp": 162.94,
"Beta Solutions": 57.46,
"Gamma Industries": 72.84,
},
{
date: "Nov 28, 2023",
"Alpha Corp": 148.37,
"Beta Solutions": 49.82,
"Gamma Industries": 81.56,
},
{
date: "Nov 29, 2023",
"Alpha Corp": 139.56,
"Beta Solutions": 55.63,
"Gamma Industries": 92.38,
},
{
date: "Nov 30, 2023",
"Alpha Corp": 145.83,
"Beta Solutions": 61.27,
"Gamma Industries": 88.75,
},
{
date: "Dec 01, 2023",
"Alpha Corp": 138.29,
"Beta Solutions": 68.94,
"Gamma Industries": 93.42,
},
{
date: "Dec 02, 2023",
"Alpha Corp": 129.64,
"Beta Solutions": 74.56,
"Gamma Industries": 97.18,
},
{
date: "Dec 03, 2023",
"Alpha Corp": 119.82,
"Beta Solutions": 71.38,
"Gamma Industries": 89.43,
},
{
date: "Dec 04, 2023",
"Alpha Corp": 128.54,
"Beta Solutions": 63.95,
"Gamma Industries": 92.76,
},
{
date: "Dec 05, 2023",
"Alpha Corp": 137.21,
"Beta Solutions": 58.47,
"Gamma Industries": 84.29,
},
{
date: "Dec 06, 2023",
"Alpha Corp": 134.68,
"Beta Solutions": 69.12,
"Gamma Industries": 79.38,
},
{
date: "Dec 07, 2023",
"Alpha Corp": 152.73,
"Beta Solutions": 73.89,
"Gamma Industries": 81.42,
},
{
date: "Dec 08, 2023",
"Alpha Corp": 168.59,
"Beta Solutions": 78.54,
"Gamma Industries": 75.68,
},
]
const summary = [
{
name: "Alpha Corp",
tickerSymbol: "ACP",
value: "$168.59",
change: "+15.86",
percentageChange: "+10.4%",
changeType: "positive",
},
{
name: "Beta Solutions",
tickerSymbol: "BTS",
value: "$78.54",
change: "+4.65",
percentageChange: "+6.3%",
changeType: "positive",
},
{
name: "Gamma Industries",
tickerSymbol: "GMI",
value: "$75.68",
change: "-5.74",
percentageChange: "-7.1%",
changeType: "negative",
},
]
const sanitizeName = (name: string) => {
return name
.replace(/\\s+/g, "-")
.replace(/[^a-zA-Z0-9-]/g, "_")
.toLowerCase()
}
export default function Stats10() {
return (
<div className="flex w-full items-center justify-center p-10">
<dl className="grid w-full grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-3">
{summary.map((item) => {
const sanitizedName = sanitizeName(item.name)
const gradientId = `gradient-${sanitizedName}`
const color =
item.changeType === "positive"
? "hsl(142.1 76.2% 36.3%)"
: "hsl(0 72.2% 50.6%)"
return (
<Card className="overflow-hidden p-0 shadow-2xs" key={item.name}>
<CardContent className="p-0">
<div className="p-4 pb-0">
<dt className="font-medium text-foreground text-sm">
{item.name}{" "}
<span className="font-normal text-muted-foreground">
({item.tickerSymbol})
</span>
</dt>
<div className="flex items-baseline justify-between">
<dd
className={cn(
item.changeType === "positive"
? "text-green-600 dark:text-green-500"
: "text-red-600 dark:text-red-500",
"font-semibold text-lg"
)}
>
{item.value}
</dd>
<dd className="flex items-center space-x-1 text-sm">
<span className="font-medium text-foreground">
{item.change}
</span>
<span
className={cn(
item.changeType === "positive"
? "text-green-600 dark:text-green-500"
: "text-red-600 dark:text-red-500"
)}
>
({item.percentageChange})
</span>
</dd>
</div>
</div>
<div className="mt-2 h-16 overflow-hidden">
<ChartContainer
className="h-full w-full"
config={{
[item.name]: {
label: item.name,
color,
},
}}
>
<AreaChart
data={data}
margin={{ top: 0, right: 0, bottom: 0, left: 0 }}
>
<defs>
<linearGradient
id={gradientId}
x1="0"
x2="0"
y1="0"
y2="1"
>
<stop
offset="5%"
stopColor={color}
stopOpacity={0.3}
/>
<stop
offset="95%"
stopColor={color}
stopOpacity={0}
/>
</linearGradient>
</defs>
<XAxis dataKey="date" hide={true} />
<Area
dataKey={item.name}
fill={`url(#${gradientId})`}
fillOpacity={0.4}
stroke={color}
strokeWidth={1.5}
type="monotone"
/>
</AreaChart>
</ChartContainer>
</div>
</CardContent>
</Card>
)
})}
</dl>
</div>
)
}npx shadcn@latest add https://blocks.so/r/stats-10.jsonStats Dashboard with Progress Bars
"use client"
import { Box, Edit } from "lucide-react"
import type React from "react"
import { useState } from "react"
import { Button } from "@/components/ui/button"
import { Card, CardContent } from "@/components/ui/card"
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog"
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
interface MetricCardProps {
title: string
value: string
limit: string
percentage: number
status?: string
statusColor?: string
progressColor: string
details?: Array<{ label: string; value: string; color: string }>
actionLabel: string
actionIcon: React.ReactNode
warningMessage?: string
onActionClick?: () => void
}
function MetricCard({
title,
value,
limit,
percentage,
status,
statusColor = "text-emerald-600 dark:text-emerald-400",
progressColor,
details,
actionLabel,
actionIcon,
warningMessage,
onActionClick,
}: MetricCardProps) {
const renderProgressBar = () => {
if (details && title === "Commands") {
const writes = Number.parseInt(details[0].value.replace(/,/g, ""))
const reads = Number.parseInt(details[1].value.replace(/,/g, ""))
const total = writes + reads
const writesPercentage = (writes / total) * 100
const readsPercentage = (reads / total) * 100
return (
<div className="relative h-1 w-full overflow-hidden rounded-full bg-muted">
<div
className="absolute left-0 h-full w-full origin-left bg-emerald-500 transition-transform duration-200 ease-out"
style={{ transform: `scaleX(${writesPercentage / 100})` }}
/>
<div
className="absolute left-0 h-full w-full origin-left bg-blue-500 transition-transform duration-200 ease-out"
style={{
transform: `translateX(${writesPercentage}%) scaleX(${readsPercentage / 100})`,
}}
/>
</div>
)
}
return (
<div className="relative h-1 w-full overflow-hidden rounded-full bg-muted">
<div
className={`h-full w-full origin-left transition-transform duration-200 ease-out ${progressColor}`}
style={{ transform: `scaleX(${Math.min(percentage, 100) / 100})` }}
/>
</div>
)
}
return (
<Card className="relative max-w-[280px] overflow-hidden shadow-2xs">
<CardContent className="p-4 py-0">
<h5 className="font-normal text-muted-foreground text-xs uppercase leading-none tracking-wide dark:text-foreground/80">
{title}
</h5>
<div className="mt-2 flex items-baseline gap-1">
<div className="font-medium text-[1.2rem] text-foreground tabular-nums leading-none">
{value}
</div>
<div className="text-muted-foreground text-xs leading-none">
/ {limit}
</div>
</div>
<div className="mt-3">
{renderProgressBar()}
{details && (
<div className="my-6 mb-8">
<div className="flex flex-col gap-3">
{details.map((detail, index) => (
<div
className="flex w-full items-center text-muted-foreground text-xs leading-none dark:text-foreground/70"
key={index}
>
<div
className={`mr-[6px] h-2 w-2 rounded-full ${detail.color}`}
/>
<div className="mr-1">{detail.label}</div>
<div className="h-[9px] flex-1 border-border border-b-2 border-dotted" />
<div className="ml-1 tabular-nums">{detail.value}</div>
</div>
))}
</div>
</div>
)}
{status && (
<div className="pt-2">
<div className={statusColor}>{status}</div>
</div>
)}
{warningMessage && (
<div className="pt-2">
<div className="text-amber-700 text-sm dark:text-amber-400">
{warningMessage}
</div>
</div>
)}
</div>
<div className="absolute right-0 bottom-0 left-0">
<Button
className="h-8 w-full justify-start gap-0 rounded-none bg-muted/50 text-blue-500 hover:text-blue-600 dark:text-blue-400 dark:hover:text-blue-300"
onClick={onActionClick}
variant="ghost"
>
{actionIcon}
<span className="ml-1 text-xs">{actionLabel}</span>
</Button>
</div>
</CardContent>
</Card>
)
}
function BudgetDialog({
open,
onOpenChange,
}: {
open: boolean
onOpenChange: (open: boolean) => void
}) {
const [budget, setBudget] = useState("150")
const handleUpdate = () => {
onOpenChange(false)
}
return (
<Dialog onOpenChange={onOpenChange} open={open}>
<DialogContent className="sm:max-w-md">
<DialogHeader>
<DialogTitle>Update budget</DialogTitle>
<DialogDescription>
When your monthly cost reaches the max budget, we send an email and
throttle your database. You will not be charged beyond your set
budget for this database.
</DialogDescription>
</DialogHeader>
<div className="space-y-2">
<Label htmlFor="budget">Max budget per month</Label>
<Input
id="budget"
onChange={(e) => setBudget(e.target.value)}
placeholder="150"
type="number"
value={budget}
/>
</div>
<DialogFooter className="pt-2">
<Button onClick={() => onOpenChange(false)} variant="outline">
Cancel
</Button>
<Button onClick={handleUpdate}>Update</Button>
</DialogFooter>
</DialogContent>
</Dialog>
)
}
export default function Stats11() {
const [budgetDialogOpen, setBudgetDialogOpen] = useState(false)
return (
<>
<div className="grid grid-cols-1 gap-3 md:grid-cols-2 lg:grid-cols-4">
<MetricCard
actionIcon={<Box className="h-4 w-4" />}
actionLabel="Upgrade"
details={[
{ label: "Writes", value: "11,276,493", color: "bg-emerald-500" },
{ label: "Reads", value: "2,548,921", color: "bg-blue-500" },
]}
limit="Unlimited"
percentage={67}
progressColor="bg-blue-500"
title="Commands"
value="13.8M"
/>
<MetricCard
actionIcon={<Box className="h-4 w-4" />}
actionLabel="Upgrade"
limit="150 GB"
percentage={94}
progressColor="bg-orange-500"
title="Bandwidth"
value="141 GB"
warningMessage="There will be a charge for the excessive bandwidth over the limit."
/>
<MetricCard
actionIcon={<Box className="h-4 w-4" />}
actionLabel="Upgrade"
limit="500 GB"
percentage={7.4}
progressColor="bg-emerald-500"
status="It's all right."
title="Storage"
value="37 GB"
/>
<MetricCard
actionIcon={<Edit className="h-4 w-4" />}
actionLabel="Change Budget"
limit="$150 Budget"
onActionClick={() => setBudgetDialogOpen(true)}
percentage={48.95}
progressColor="bg-emerald-500"
status="It's all right."
title="Cost"
value="$73.42"
/>
</div>
<BudgetDialog
onOpenChange={setBudgetDialogOpen}
open={budgetDialogOpen}
/>
</>
)
}npx shadcn@latest add https://blocks.so/r/stats-11.jsonStats Usage Dashboard
"use client"
import { Cell, Pie, PieChart } from "recharts"
import { Button } from "@/components/ui/button"
import { Card, CardContent, CardHeader } from "@/components/ui/card"
import { type ChartConfig, ChartContainer } from "@/components/ui/chart"
interface UsageItem {
name: string
current: string
limit: string
percentage: number
href?: string
}
const usageData: UsageItem[] = [
{ name: "ISR Reads", current: "358K", limit: "1M", percentage: 35.8 },
{ name: "Edge Requests", current: "317K", limit: "1M", percentage: 31.7 },
{
name: "Fast Origin Transfer",
current: "3.07 GB",
limit: "10 GB",
percentage: 30.7,
},
{
name: "Speed Insights Data Points",
current: "791",
limit: "10K",
percentage: 7.9,
},
{
name: "Fast Data Transfer",
current: "4.98 GB",
limit: "100 GB",
percentage: 5.0,
},
{
name: "Function Duration",
current: "3.1 GB-Hrs",
limit: "100 GB-Hrs",
percentage: 3.1,
},
{
name: "Web Analytics Events",
current: "1.3K",
limit: "50K",
percentage: 2.6,
},
{ name: "ISR Writes", current: "4.8K", limit: "200K", percentage: 2.4 },
{
name: "Function Invocations",
current: "19K",
limit: "1M",
percentage: 1.9,
},
{
name: "Image Optimization - Cache Reads",
current: "4.3K",
limit: "300K",
percentage: 1.4,
},
]
const chartConfig = {
used: {
label: "Used",
color: "hsl(var(--primary))",
},
remaining: {
label: "Remaining",
color: "hsl(var(--muted))",
},
} satisfies ChartConfig
function DonutChart({ percentage }: { percentage: number }) {
const backgroundData = [{ name: "background", value: 100, fill: "#E5E7EB" }]
const foregroundData = [
{
name: "used",
value: Math.max(0, Math.min(100, Number(percentage))),
fill: "#3B82F6",
},
{
name: "empty",
value: 100 - Math.max(0, Math.min(100, Number(percentage))),
fill: "transparent",
},
]
return (
<ChartContainer
className="aspect-square h-6 w-6 shrink-0"
config={chartConfig}
>
<PieChart>
<Pie
cx="50%"
cy="50%"
data={backgroundData}
dataKey="value"
innerRadius={6}
isAnimationActive={false}
nameKey="name"
outerRadius={10}
>
{backgroundData.map((entry, index) => (
<Cell fill={entry.fill} key={`bg-cell-${index}`} />
))}
</Pie>
<Pie
cx="50%"
cy="50%"
data={foregroundData}
dataKey="value"
endAngle={-270}
innerRadius={6}
nameKey="name"
outerRadius={10}
startAngle={90}
>
{foregroundData.map((entry, index) => (
<Cell fill={entry.fill} key={`fg-cell-${index}`} />
))}
</Pie>
</PieChart>
</ChartContainer>
)
}
export default function Stats12() {
return (
<Card className="w-full max-w-md gap-3 py-5 shadow-2xs">
<CardHeader className="px-5">
<div className="flex items-center justify-between">
<div className="flex flex-col">
<h3 className="text-balance font-medium text-sm">Last 30 days</h3>
<p className="text-pretty font-medium text-muted-foreground text-xs">
Updated just now
</p>
</div>
<Button className="h-6 font-medium text-xs" size="sm">
Upgrade
</Button>
</div>
</CardHeader>
<CardContent className="px-3 pt-0">
<div className="space-y-0">
{usageData.map((item, index) => (
<div
className={`flex items-center gap-3 rounded-sm p-2 transition-colors hover:bg-muted/50 ${
index % 2 === 1 ? "bg-muted/20" : ""
}`}
key={item.name}
>
<DonutChart percentage={item.percentage} />
<span className="flex-1 truncate text-sm leading-4">
{item.name}
</span>
<span className="font-medium text-muted-foreground text-xs tabular-nums tracking-tighter">
{item.current} /{" "}
<span className="text-foreground">{item.limit}</span>
</span>
</div>
))}
</div>
</CardContent>
</Card>
)
}npx shadcn@latest add https://blocks.so/r/stats-12.jsonStats with Segmented Progress
"use client"
import { Card, CardContent } from "@/components/ui/card"
import { cn } from "@/lib/utils"
interface Props {
title?: string
used?: number
total?: number
usedLabel?: string
totalLabel?: string
segments?: {
label: string
value: number
color: string
}[]
className?: string
}
const defaultSegments: NonNullable<Props["segments"]> = [
{ label: "Documents", value: 2400, color: "bg-blue-500" },
{ label: "Photos", value: 1800, color: "bg-emerald-500" },
{ label: "Videos", value: 3200, color: "bg-amber-500" },
{ label: "Music", value: 900, color: "bg-purple-500" },
]
export default function Stats13({
title = "Using Storage",
used = 8300,
total = 15,
usedLabel = "MB",
totalLabel = "GB",
segments = defaultSegments,
className,
}: Props) {
const totalValue = total * 1000
const freeValue = totalValue - used
return (
<Card className={cn("w-full max-w-4xl shadow-sm", className)}>
<CardContent className="py-0">
<p className="mb-4 text-pretty text-base text-muted-foreground">
{title}{" "}
<span className="font-semibold text-foreground tabular-nums">
{used.toLocaleString(undefined, {
minimumFractionDigits: 0,
maximumFractionDigits: 2,
})}{" "}
{usedLabel}
</span>{" "}
of {total} {totalLabel}
</p>
<div className="mb-4 flex h-2.5 w-full overflow-hidden rounded-full bg-muted">
{segments.map((segment) => {
const percentage = (segment.value / totalValue) * 100
return (
<div
aria-label={segment.label}
aria-valuemax={totalValue}
aria-valuemin={0}
aria-valuenow={segment.value}
className={cn("h-full", segment.color)}
key={segment.label}
role="progressbar"
style={{ width: `${percentage}%` }}
/>
)
})}
</div>
<div className="flex flex-wrap items-center gap-x-8 gap-y-2">
{segments.map((segment) => (
<div className="flex items-center gap-2" key={segment.label}>
<span
aria-hidden="true"
className={cn("size-3 shrink-0 rounded", segment.color)}
/>
<span className="text-muted-foreground text-sm">
{segment.label}
</span>
<span className="text-muted-foreground text-sm tabular-nums">
{Math.round(segment.value)}
{usedLabel}
</span>
</div>
))}
<div className="flex items-center gap-2">
<span
aria-hidden="true"
className="size-3 shrink-0 rounded-sm bg-muted"
/>
<span className="text-muted-foreground text-sm">Free</span>
<span className="text-muted-foreground text-sm tabular-nums">
{Math.round(freeValue)}
{usedLabel}
</span>
</div>
</div>
</CardContent>
</Card>
)
}npx shadcn@latest add https://blocks.so/r/stats-13.jsonStats with Usage Breakdown
import { Badge } from "@/components/ui/badge"
import { Card, CardContent } from "@/components/ui/card"
interface UsageItem {
label: string
amount: number
percentage: number
color: "emerald" | "amber" | "rose"
}
const data: UsageItem[] = [
{ label: "Compute", amount: 450, percentage: 52.3, color: "emerald" },
{ label: "Storage", amount: 285, percentage: 33.1, color: "amber" },
{ label: "Bandwidth", amount: 125, percentage: 14.6, color: "rose" },
]
const colorClasses = {
emerald: "bg-emerald-500 dark:bg-emerald-400",
amber: "bg-amber-500 dark:bg-amber-400",
rose: "bg-rose-500 dark:bg-rose-400",
}
export default function Stats14() {
return (
<Card className="w-full max-w-sm shadow-none">
<CardContent className="flex flex-col justify-between pt-0">
<div>
<div className="flex items-center gap-2">
<h3 className="text-balance font-bold text-foreground text-sm">
Usage
</h3>
<Badge
className="bg-amber-50 text-amber-700 ring-1 ring-amber-500/30 dark:bg-amber-400/10 dark:text-amber-300 dark:ring-amber-400/20"
variant="secondary"
>
+12.5%
</Badge>
</div>
<p className="mt-2 flex items-baseline gap-2 text-pretty">
<span className="text-foreground text-xl">$860</span>
<span className="text-muted-foreground text-sm">this month</span>
</p>
<div className="mt-4">
<p className="text-pretty font-medium text-foreground text-sm">
Resource breakdown
</p>
<div className="mt-2 flex items-center gap-0.5">
{data.map((item, index) => (
<div
className={`${colorClasses[item.color]} h-1.5 rounded-xs`}
key={index}
style={{ width: `${item.percentage}%` }}
/>
))}
</div>
</div>
<ul className="mt-5 space-y-2" role="list">
{data.map((item, index) => (
<li className="flex items-center gap-2 text-xs" key={index}>
<span
aria-hidden="true"
className={`${colorClasses[item.color]} size-2.5 rounded-xs`}
/>
<span className="text-foreground">{item.label}</span>
<span className="text-muted-foreground">
(${item.amount} / {item.percentage}%)
</span>
</li>
))}
</ul>
</div>
<p className="mt-6 text-pretty text-muted-foreground text-xs">
Configure limits in{" "}
<a
className="text-emerald-600 hover:underline dark:text-emerald-400"
href="#"
>
resource settings.
</a>
</p>
</CardContent>
</Card>
)
}npx shadcn@latest add https://blocks.so/r/stats-14.jsonStats with Value Breakdown
import { cn } from "@/lib/utils"
const data = [
{
label: "After 1 year",
value: "$2,400",
percentage: "+8.2%",
},
{
label: "After 5 years",
value: "$14,800",
percentage: "+24.6%",
},
{
label: "After 10 years",
value: "$38,500",
percentage: "+52.1%",
},
]
export default function Stats15() {
return (
<div className="w-full max-w-2xs">
<h3 className="text-balance font-medium text-foreground text-sm">
Investment growth projection
</h3>
<ul className="mt-2 divide-y divide-border text-sm" role="list">
{data.map((item, index) => (
<li className="flex items-center justify-between py-3" key={index}>
<span className="text-muted-foreground">{item.label}</span>
<span className="flex items-center gap-3 tabular-nums">
<span className="text-right font-medium text-foreground">
{item.value}
</span>
<span aria-hidden="true" className="h-5 w-px bg-border" />
<span
className={cn(
"w-15 rounded px-1.5 py-1 text-center font-semibold text-xs",
"bg-emerald-50 text-emerald-600 dark:bg-emerald-400/10 dark:text-emerald-400"
)}
>
{item.percentage}
</span>
</span>
</li>
))}
</ul>
</div>
)
}npx shadcn@latest add https://blocks.so/r/stats-15.json