diff --git a/components/admin/AdminAnalytics.tsx b/components/admin/AdminAnalytics.tsx
index 89647b5..7965d14 100644
--- a/components/admin/AdminAnalytics.tsx
+++ b/components/admin/AdminAnalytics.tsx
@@ -47,6 +47,14 @@ import {
} from "recharts";
import { formatGBP, formatNumber } from "@/lib/utils/format";
import { PieChart, Pie, Cell, Legend } from "recharts";
+import { AdminStatCard } from "./AdminStatCard";
+import { TrendIndicator } from "./TrendIndicator";
+import {
+ ChartSkeleton,
+ CustomerInsightsSkeleton,
+ MetricsCardSkeleton,
+ TableSkeleton
+} from "../analytics/SkeletonLoaders";
interface GrowthData {
launchDate: string;
@@ -417,33 +425,7 @@ export default function AdminAnalytics() {
return null;
};
- // Trend indicator component for metric cards
- const TrendIndicator = ({
- current,
- previous,
- }: {
- current: number;
- previous: number;
- }) => {
- if (!current || !previous) return null;
- const percentChange = ((current - previous) / previous) * 100;
-
- if (Math.abs(percentChange) < 0.1) return null;
-
- return (
-
= 0 ? "text-green-500" : "text-red-500"}`}
- >
- {percentChange >= 0 ? (
-
- ) : (
-
- )}
- {Math.abs(percentChange).toFixed(1)}%
-
- );
- };
// Format currency
const formatCurrency = (value: number) => {
@@ -707,234 +689,98 @@ export default function AdminAnalytics() {
{/* Orders Card */}
-
-
-
-
- Total Orders
-
-
-
-
-
-
-
-
- {formatNumber(analyticsData?.orders?.total)}
-
-
-
Today: {analyticsData?.orders?.totalToday || 0}
-
-
-
-
-
- {loading || refreshing ? (
-
- ) : analyticsData?.orders?.dailyOrders &&
- analyticsData.orders.dailyOrders.length > 0 ? (
-
-
-
-
-
-
-
-
-
-
- } cursor={{ stroke: 'rgba(255,255,255,0.1)', strokeWidth: 1 }} />
-
-
-
- ) : (
-
- No chart data
-
- )}
-
-
+
+ Today: {analyticsData?.orders?.totalToday || 0}
+
+ }
+ trend={{
+ current: analyticsData?.orders?.totalToday || 0,
+ previous: (analyticsData?.orders?.total || 0) / 30, // Approx simple moving average
+ }}
+ loading={loading || refreshing}
+ chartData={transformChartData(
+ analyticsData?.orders?.dailyOrders || [],
+ "count"
+ )}
+ chartColor="#3b82f6"
+ chartGradientId="colorOrdersStat"
+ />
{/* Revenue Card */}
-
-
-
-
- Total Revenue
-
-
-
-
-
-
-
-
- {formatCurrency(analyticsData?.revenue?.total || 0)}
-
-
-
Today: {formatCurrency(analyticsData?.revenue?.today || 0)}
-
-
-
-
-
- {loading || refreshing ? (
-
- ) : analyticsData?.revenue?.dailyRevenue &&
- analyticsData.revenue.dailyRevenue.length > 0 ? (
-
-
-
-
-
-
-
-
-
-
- } cursor={{ stroke: 'rgba(255,255,255,0.1)', strokeWidth: 1 }} />
-
-
-
- ) : (
-
- No chart data
-
- )}
-
-
+
+ Today: {formatCurrency(analyticsData?.revenue?.today || 0)}
+
+ }
+ trend={{
+ current: analyticsData?.revenue?.today || 0,
+ previous: (analyticsData?.revenue?.total || 0) / 30,
+ }}
+ loading={loading || refreshing}
+ chartData={transformChartData(
+ analyticsData?.revenue?.dailyRevenue || [],
+ "amount"
+ )}
+ chartColor="#10b981"
+ chartGradientId="colorRevenueStat"
+ tooltipPrefix="£"
+ />
{/* Vendors Card */}
-
-
-
-
-
-
- {analyticsData?.vendors?.total?.toLocaleString() || "0"}
-
-
- Active: {analyticsData?.vendors?.active || 0}
- Stores: {analyticsData?.vendors?.activeStores || 0}
-
-
-
New: {analyticsData?.vendors?.newToday || 0}
-
-
-
-
-
- {loading || refreshing ? (
-
- ) : analyticsData?.vendors?.dailyGrowth &&
- analyticsData.vendors.dailyGrowth.length > 0 ? (
-
-
-
-
-
-
-
-
-
-
- } cursor={{ stroke: 'rgba(255,255,255,0.1)', strokeWidth: 1 }} />
-
-
-
- ) : (
-
- No chart data
-
- )}
-
-
+ New: {analyticsData?.vendors?.newToday || 0}}
+ trend={{
+ current: analyticsData?.vendors?.newToday || 0,
+ previous: (analyticsData?.vendors?.newThisWeek || 0) / 7,
+ }}
+ loading={loading || refreshing}
+ chartData={transformChartData(
+ analyticsData?.vendors?.dailyGrowth || [],
+ "count"
+ )}
+ chartColor="#8b5cf6"
+ chartGradientId="colorVendorsStat"
+ >
+
+
+ Active: {analyticsData?.vendors?.active || 0}
+
+
+ Stores: {analyticsData?.vendors?.activeStores || 0}
+
+
+
{/* Products Card */}
-
-
-
-
-
-
- {formatNumber(analyticsData?.products?.total)}
-
-
- New This Week: {analyticsData?.products?.recent || 0}
-
- {/* Visual spacer since no chart here */}
-
- Inventory Overview
-
-
-
+
@@ -960,251 +806,290 @@ export default function AdminAnalytics() {
-
-
- Order Trends
-
- Daily order volume and revenue processed over the selected time period
-
-
-
- {loading || refreshing ? (
-
-
-
-
- Loading chart data...
-
-
-
- ) : analyticsData?.orders?.dailyOrders &&
- analyticsData.orders.dailyOrders.length > 0 ? (
-
-
-
-
-
-
-
- `£${(value / 1000).toFixed(0)}k`
- }
- label={{
- value: "Revenue / AOV",
- angle: 90,
- position: "insideRight",
- }}
- />
- } />
-
-
-
-
-
-
- ) : (
-
- No order data available for the selected time period
-
- )}
-
- {/* Calculate totals for the selected period */}
- {analyticsData?.orders?.dailyOrders &&
- analyticsData?.revenue?.dailyRevenue && (
-
-
-
- Total Revenue
-
-
- {formatCurrency(
- analyticsData.revenue.dailyRevenue.reduce(
- (sum, day) => sum + (day.amount || 0),
- 0,
- ),
+ {loading || refreshing ? (
+
+ ) : (
+
+
+
+ Order Trends
+
+
+ Daily order volume and revenue processed over the selected time
+ period
+
+
+
+ {analyticsData?.orders?.dailyOrders &&
+ analyticsData.orders.dailyOrders.length > 0 ? (
+
+
+
-
-
-
- Total Orders
-
-
- {analyticsData.orders.dailyOrders
- .reduce((sum, day) => sum + (day.count || 0), 0)
- .toLocaleString()}
-
-
+ margin={{ top: 5, right: 30, left: 20, bottom: 5 }}
+ >
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ `£${(value / 1000).toFixed(0)}k`
+ }
+ label={{
+ value: "Revenue",
+ angle: 90,
+ position: "insideRight",
+ style: { fill: 'hsl(var(--muted-foreground))' }
+ }}
+ />
+ } cursor={{ stroke: 'rgba(255,255,255,0.1)', strokeWidth: 1 }} />
+
+
+
+
+
+ ) : (
+
+ No order data available for the selected time period
)}
-
-
+
+ {/* Calculate totals for the selected period */}
+ {analyticsData?.orders?.dailyOrders &&
+ analyticsData?.revenue?.dailyRevenue && (
+
+
+
+ Total Revenue
+
+
+ {formatCurrency(
+ analyticsData.revenue.dailyRevenue.reduce(
+ (sum, day) => sum + (day.amount || 0),
+ 0,
+ ),
+ )}
+
+
+
+
+ Total Orders
+
+
+ {analyticsData.orders.dailyOrders
+ .reduce((sum, day) => sum + (day.count || 0), 0)
+ .toLocaleString()}
+
+
+
+ )}
+
+
+ )}
-
-
- Vendor Growth
-
- New vendor registrations over time
-
-
-
- {loading || refreshing ? (
-
-
-
-
- Loading chart data...
-
+ {loading || refreshing ? (
+
+ ) : (
+
+
+
+ Vendor Growth
+
+ New vendor registrations over time
+
+
+ {analyticsData?.vendors?.dailyGrowth &&
+ analyticsData.vendors.dailyGrowth.length > 0 ? (
+
+
+
+
+
+
+
+
+
+
+
+
+
+ } />
+
+
-
- ) : analyticsData?.vendors?.dailyGrowth &&
- analyticsData.vendors.dailyGrowth.length > 0 ? (
-
-
-
-
-
-
- } />
-
-
-
-
- ) : (
-
- No vendor data available for the selected time period
-
- )}
-
-
-
-
Total Vendors
-
- {formatNumber(analyticsData?.vendors?.total)}
-
-
-
-
Active Vendors
-
- {formatNumber(analyticsData?.vendors?.active)}
-
-
-
-
Active Stores
-
- {formatNumber(analyticsData?.vendors?.activeStores)}
-
-
-
-
New This Week
-
- {formatNumber(analyticsData?.vendors?.newThisWeek)}
-
-
-
-
- {/* Top Vendors by Revenue */}
- {analyticsData?.vendors?.topVendors &&
- analyticsData.vendors.topVendors.length > 0 && (
-
-
- Top Vendors by Revenue
-
-
- {analyticsData.vendors.topVendors.map((vendor, index) => (
-
-
-
- {index + 1}
-
-
-
- {vendor.vendorName}
-
-
- {vendor.orderCount} orders
-
-
-
-
-
- {formatCurrency(vendor.totalRevenue)}
-
-
-
- ))}
-
+ ) : (
+
+ No vendor data available for the selected time period
)}
-
-
+
+
+
+
Total Vendors
+
+ {formatNumber(analyticsData?.vendors?.total)}
+
+
+
+
Active Vendors
+
+ {formatNumber(analyticsData?.vendors?.active)}
+
+
+
+
Active Stores
+
+ {formatNumber(analyticsData?.vendors?.activeStores)}
+
+
+
+
New This Week
+
+ {formatNumber(analyticsData?.vendors?.newThisWeek)}
+
+
+
+
+ {/* Top Vendors by Revenue */}
+ {analyticsData?.vendors?.topVendors &&
+ analyticsData.vendors.topVendors.length > 0 && (
+
+
+ Top Vendors by Revenue
+
+
+ {analyticsData.vendors.topVendors.map((vendor, index) => (
+
+
+
+ {index + 1}
+
+
+
+ {vendor.vendorName}
+
+
+ {vendor.orderCount} orders
+
+
+
+
+
+ {formatCurrency(vendor.totalRevenue)}
+
+
+
+ ))}
+
+
+ )}
+
+
+ )}
diff --git a/components/admin/AdminStatCard.tsx b/components/admin/AdminStatCard.tsx
new file mode 100644
index 0000000..0e0b555
--- /dev/null
+++ b/components/admin/AdminStatCard.tsx
@@ -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 & { prefix?: string }) => {
+ if (active && payload && payload.length) {
+ const data = payload[0].payload;
+ return (
+
+
+ {data.formattedDate || label}
+
+
+
+ {prefix === "£" ? "Revenue" : "Count"}
+
+
+ {prefix}{prefix === "£" ? (data.value || 0).toFixed(2) : data.value}
+
+
+
+ );
+ }
+ 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 (
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {!hideChart && }
+
+
+ );
+ }
+
+ return (
+
+
+
+
+
+ {value}
+
+
+ {subtext}
+ {trend && (
+
+
+
+ )}
+
+
+ {children && {children}
}
+
+ {!hideChart && (
+ chartData && chartData.length > 0 ? (
+
+
+
+
+
+
+
+
+
+
+ }
+ cursor={{ stroke: 'rgba(255,255,255,0.1)', strokeWidth: 1 }}
+ />
+
+
+
+ ) : (
+
+ No chart data
+
+ )
+ )}
+
+ {/* Fill space if chart is hidden but we want structure consistency */}
+ {hideChart && }
+
+
+ );
+}
diff --git a/components/admin/TrendIndicator.tsx b/components/admin/TrendIndicator.tsx
new file mode 100644
index 0000000..bf6faa6
--- /dev/null
+++ b/components/admin/TrendIndicator.tsx
@@ -0,0 +1,31 @@
+"use client";
+
+import { TrendingDown, TrendingUp } from "lucide-react";
+
+// Trend indicator component for metric cards
+export const TrendIndicator = ({
+ current,
+ previous,
+}: {
+ current: number;
+ previous: number;
+}) => {
+ if (!current || !previous) return null;
+
+ const percentChange = ((current - previous) / previous) * 100;
+
+ if (Math.abs(percentChange) < 0.1) return null;
+
+ return (
+ = 0 ? "text-green-500" : "text-red-500"}`}
+ >
+ {percentChange >= 0 ? (
+
+ ) : (
+
+ )}
+ {Math.abs(percentChange).toFixed(1)}%
+
+ );
+};
diff --git a/public/git-info.json b/public/git-info.json
index 408c62e..38e1d6a 100644
--- a/public/git-info.json
+++ b/public/git-info.json
@@ -1,4 +1,4 @@
{
- "commitHash": "4c15f43",
- "buildTime": "2026-01-13T05:27:43.269Z"
+ "commitHash": "a07ca55",
+ "buildTime": "2026-01-13T05:50:49.436Z"
}
\ No newline at end of file