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.
167 lines
6.7 KiB
TypeScript
167 lines
6.7 KiB
TypeScript
"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>
|
|
);
|
|
}
|