Refactor admin analytics stat cards to reusable component
Extracted repeated stat card logic in AdminAnalytics to a new AdminStatCard component and moved the trend indicator to its own file. Updated AdminAnalytics to use AdminStatCard for orders, revenue, vendors, and products, improving code maintainability and consistency. Also updated chart and loading skeleton handling for better UX.
This commit is contained in:
166
components/admin/AdminStatCard.tsx
Normal file
166
components/admin/AdminStatCard.tsx
Normal file
@@ -0,0 +1,166 @@
|
||||
"use client";
|
||||
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/common/card";
|
||||
import { Skeleton } from "@/components/common/skeleton";
|
||||
import { Area, AreaChart, ResponsiveContainer, Tooltip, TooltipProps } from "recharts";
|
||||
import { TrendIndicator } from "./TrendIndicator";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { LucideIcon } from "lucide-react";
|
||||
import { formatGBP } from "@/lib/utils/format";
|
||||
|
||||
interface ChartDataPoint {
|
||||
formattedDate: string;
|
||||
value: number;
|
||||
orders?: number;
|
||||
revenue?: number;
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
interface AdminStatCardProps {
|
||||
title: string;
|
||||
icon: LucideIcon;
|
||||
iconColorClass: string;
|
||||
iconBgClass: string;
|
||||
value: string | number;
|
||||
subtext?: React.ReactNode;
|
||||
trend?: {
|
||||
current: number;
|
||||
previous: number;
|
||||
};
|
||||
loading?: boolean;
|
||||
chartData?: ChartDataPoint[];
|
||||
chartColor: string;
|
||||
chartGradientId: string;
|
||||
tooltipPrefix?: string; // "£" or ""
|
||||
hideChart?: boolean;
|
||||
children?: React.ReactNode;
|
||||
}
|
||||
|
||||
const CustomTooltip = ({ active, payload, label, prefix = "" }: TooltipProps<any, any> & { prefix?: string }) => {
|
||||
if (active && payload && payload.length) {
|
||||
const data = payload[0].payload;
|
||||
return (
|
||||
<div className="bg-[#050505]/90 p-3 rounded-lg shadow-xl border border-white/10 backdrop-blur-md ring-1 ring-white/5">
|
||||
<p className="font-bold text-[10px] uppercase tracking-wide text-muted-foreground mb-2 border-b border-white/5 pb-1">
|
||||
{data.formattedDate || label}
|
||||
</p>
|
||||
<div className="flex items-center justify-between gap-4">
|
||||
<span className="text-[11px] font-semibold text-primary">
|
||||
{prefix === "£" ? "Revenue" : "Count"}
|
||||
</span>
|
||||
<span className="text-[11px] font-bold text-foreground tabular-nums">
|
||||
{prefix}{prefix === "£" ? (data.value || 0).toFixed(2) : data.value}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
export function AdminStatCard({
|
||||
title,
|
||||
icon: Icon,
|
||||
iconColorClass,
|
||||
iconBgClass,
|
||||
value,
|
||||
subtext,
|
||||
trend,
|
||||
loading,
|
||||
chartData,
|
||||
chartColor,
|
||||
chartGradientId,
|
||||
tooltipPrefix = "",
|
||||
hideChart = false,
|
||||
children,
|
||||
}: AdminStatCardProps) {
|
||||
if (loading) {
|
||||
return (
|
||||
<Card className="border-border/40 bg-background/50 backdrop-blur-sm shadow-sm overflow-hidden h-full">
|
||||
<CardHeader className="pb-2">
|
||||
<div className="flex justify-between items-start">
|
||||
<Skeleton className="h-4 w-24" />
|
||||
<Skeleton className="h-8 w-8 rounded-md" />
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Skeleton className="h-8 w-32 mb-2" />
|
||||
<div className="flex items-center gap-2 mb-4">
|
||||
<Skeleton className="h-4 w-20" />
|
||||
<Skeleton className="h-4 w-12 ml-auto" />
|
||||
</div>
|
||||
{!hideChart && <Skeleton className="h-14 w-full rounded-md" />}
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Card className="border-border/40 bg-background/50 backdrop-blur-sm shadow-sm overflow-hidden hover:shadow-md transition-shadow duration-300 h-full flex flex-col">
|
||||
<CardHeader className="pb-2">
|
||||
<div className="flex justify-between items-start">
|
||||
<CardTitle className="text-sm font-medium text-muted-foreground">
|
||||
{title}
|
||||
</CardTitle>
|
||||
<div className={cn("p-2 rounded-md", iconBgClass)}>
|
||||
<Icon className={cn("h-4 w-4", iconColorClass)} />
|
||||
</div>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent className="flex-1 flex flex-col">
|
||||
<div className="text-2xl font-bold">{value}</div>
|
||||
|
||||
<div className="flex items-center text-xs text-muted-foreground mt-1">
|
||||
{subtext}
|
||||
{trend && (
|
||||
<div className="ml-auto">
|
||||
<TrendIndicator current={trend.current} previous={trend.previous} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{children && <div className="mt-2">{children}</div>}
|
||||
|
||||
{!hideChart && (
|
||||
chartData && chartData.length > 0 ? (
|
||||
<div className="mt-auto pt-4 h-[72px] -mx-2">
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<AreaChart
|
||||
data={chartData}
|
||||
margin={{ top: 5, right: 0, left: 0, bottom: 0 }}
|
||||
>
|
||||
<defs>
|
||||
<linearGradient id={chartGradientId} x1="0" y1="0" x2="0" y2="1">
|
||||
<stop offset="5%" stopColor={chartColor} stopOpacity={0.3} />
|
||||
<stop offset="95%" stopColor={chartColor} stopOpacity={0} />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<Area
|
||||
type="monotone"
|
||||
dataKey="value"
|
||||
stroke={chartColor}
|
||||
fillOpacity={1}
|
||||
fill={`url(#${chartGradientId})`}
|
||||
strokeWidth={2}
|
||||
activeDot={{ r: 4, strokeWidth: 0, fill: chartColor }}
|
||||
/>
|
||||
<Tooltip
|
||||
content={<CustomTooltip prefix={tooltipPrefix} />}
|
||||
cursor={{ stroke: 'rgba(255,255,255,0.1)', strokeWidth: 1 }}
|
||||
/>
|
||||
</AreaChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
) : (
|
||||
<div className="mt-4 h-14 flex items-center justify-center text-xs text-muted-foreground bg-muted/20 rounded">
|
||||
No chart data
|
||||
</div>
|
||||
)
|
||||
)}
|
||||
|
||||
{/* Fill space if chart is hidden but we want structure consistency */}
|
||||
{hideChart && <div className="mt-auto pt-4" />}
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user