fix
This commit is contained in:
@@ -3,6 +3,7 @@ import { cookies } from 'next/headers';
|
|||||||
import { redirect } from 'next/navigation';
|
import { redirect } from 'next/navigation';
|
||||||
import Dashboard from "@/components/dashboard/dashboard";
|
import Dashboard from "@/components/dashboard/dashboard";
|
||||||
import AnalyticsDashboard from '@/components/analytics/AnalyticsDashboard';
|
import AnalyticsDashboard from '@/components/analytics/AnalyticsDashboard';
|
||||||
|
import AnalyticsDashboardSkeleton from '@/components/analytics/AnalyticsDashboardSkeleton';
|
||||||
import StoreSelector from '@/components/analytics/StoreSelector';
|
import StoreSelector from '@/components/analytics/StoreSelector';
|
||||||
import { getAnalyticsOverviewServer } from '@/lib/server-api';
|
import { getAnalyticsOverviewServer } from '@/lib/server-api';
|
||||||
import { fetchServer } from '@/lib/api';
|
import { fetchServer } from '@/lib/api';
|
||||||
@@ -65,14 +66,7 @@ export default async function AnalyticsPage({
|
|||||||
<Dashboard>
|
<Dashboard>
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
{/* Analytics Content */}
|
{/* Analytics Content */}
|
||||||
<Suspense fallback={
|
<Suspense fallback={<AnalyticsDashboardSkeleton />}>
|
||||||
<div className="flex items-center justify-center h-64">
|
|
||||||
<div className="text-center">
|
|
||||||
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary mx-auto mb-4"></div>
|
|
||||||
<p className="text-muted-foreground">Loading analytics...</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
}>
|
|
||||||
<AnalyticsDashboard initialData={initialData} />
|
<AnalyticsDashboard initialData={initialData} />
|
||||||
</Suspense>
|
</Suspense>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ import OrderAnalyticsChart from "./OrderAnalyticsChart";
|
|||||||
import MetricsCard from "./MetricsCard";
|
import MetricsCard from "./MetricsCard";
|
||||||
import { getAnalyticsOverviewWithStore, type AnalyticsOverview } from "@/lib/services/analytics-service";
|
import { getAnalyticsOverviewWithStore, type AnalyticsOverview } from "@/lib/services/analytics-service";
|
||||||
import { formatGBP } from "@/utils/format";
|
import { formatGBP } from "@/utils/format";
|
||||||
|
import { MetricsCardSkeleton } from './SkeletonLoaders';
|
||||||
|
|
||||||
interface AnalyticsDashboardProps {
|
interface AnalyticsDashboardProps {
|
||||||
initialData: AnalyticsOverview;
|
initialData: AnalyticsOverview;
|
||||||
@@ -95,9 +96,15 @@ export default function AnalyticsDashboard({ initialData }: AnalyticsDashboardPr
|
|||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
{/* Key Metrics Cards */}
|
{/* Key Metrics Cards */}
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
|
||||||
{metrics.map((metric) => (
|
{isLoading ? (
|
||||||
|
[...Array(4)].map((_, i) => (
|
||||||
|
<MetricsCardSkeleton key={i} />
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
metrics.map((metric) => (
|
||||||
<MetricsCard key={metric.title} {...metric} />
|
<MetricsCard key={metric.title} {...metric} />
|
||||||
))}
|
))
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Completion Rate Card */}
|
{/* Completion Rate Card */}
|
||||||
@@ -112,6 +119,15 @@ export default function AnalyticsDashboard({ initialData }: AnalyticsDashboardPr
|
|||||||
</CardDescription>
|
</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
|
{isLoading ? (
|
||||||
|
<div className="flex items-center gap-4">
|
||||||
|
<div className="h-12 w-16 bg-muted/20 rounded animate-pulse" />
|
||||||
|
<div className="flex-1">
|
||||||
|
<div className="w-full bg-muted/20 rounded-full h-2 animate-pulse" />
|
||||||
|
</div>
|
||||||
|
<div className="h-6 w-16 bg-muted/20 rounded animate-pulse" />
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
<div className="flex items-center gap-4">
|
<div className="flex items-center gap-4">
|
||||||
<div className="text-3xl font-bold">
|
<div className="text-3xl font-bold">
|
||||||
{data.orders.completionRate}%
|
{data.orders.completionRate}%
|
||||||
@@ -128,9 +144,30 @@ export default function AnalyticsDashboard({ initialData }: AnalyticsDashboardPr
|
|||||||
{data.orders.completed} / {data.orders.total}
|
{data.orders.completed} / {data.orders.total}
|
||||||
</Badge>
|
</Badge>
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
|
{/* Time Period Selector */}
|
||||||
|
<div className="flex flex-col sm:flex-row gap-4 sm:items-center sm:justify-between">
|
||||||
|
<div>
|
||||||
|
<h3 className="text-lg font-semibold">Time period:</h3>
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
Revenue and Orders tabs use time filtering. Products and Customers show all-time data.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<Select value={timeRange} onValueChange={setTimeRange}>
|
||||||
|
<SelectTrigger className="w-32">
|
||||||
|
<SelectValue />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="7">Last 7 days</SelectItem>
|
||||||
|
<SelectItem value="30">Last 30 days</SelectItem>
|
||||||
|
<SelectItem value="90">Last 90 days</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Analytics Tabs */}
|
{/* Analytics Tabs */}
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
<Tabs defaultValue="revenue" className="space-y-6">
|
<Tabs defaultValue="revenue" className="space-y-6">
|
||||||
|
|||||||
204
components/analytics/AnalyticsDashboardSkeleton.tsx
Normal file
204
components/analytics/AnalyticsDashboardSkeleton.tsx
Normal file
@@ -0,0 +1,204 @@
|
|||||||
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
|
import { Skeleton } from "@/components/ui/skeleton";
|
||||||
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||||
|
import { MetricsCardSkeleton } from './SkeletonLoaders';
|
||||||
|
import {
|
||||||
|
TrendingUp,
|
||||||
|
Package,
|
||||||
|
Users,
|
||||||
|
BarChart3,
|
||||||
|
Activity
|
||||||
|
} from "lucide-react";
|
||||||
|
|
||||||
|
export default function AnalyticsDashboardSkeleton() {
|
||||||
|
return (
|
||||||
|
<div className="space-y-6">
|
||||||
|
{/* Key Metrics Cards */}
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
|
||||||
|
{[...Array(4)].map((_, i) => (
|
||||||
|
<MetricsCardSkeleton key={i} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Completion Rate Card */}
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="flex items-center gap-2">
|
||||||
|
<Activity className="h-5 w-5" />
|
||||||
|
Order Completion Rate
|
||||||
|
</CardTitle>
|
||||||
|
<CardDescription>
|
||||||
|
Percentage of orders that have been successfully completed
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="flex items-center gap-4">
|
||||||
|
<Skeleton className="h-12 w-16" />
|
||||||
|
<div className="flex-1">
|
||||||
|
<Skeleton className="w-full h-2 rounded-full" />
|
||||||
|
</div>
|
||||||
|
<Skeleton className="h-6 w-16" />
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
{/* Analytics Tabs */}
|
||||||
|
<div className="space-y-6">
|
||||||
|
<Tabs defaultValue="revenue" className="space-y-6">
|
||||||
|
<TabsList className="grid w-full grid-cols-4">
|
||||||
|
<TabsTrigger value="revenue" className="flex items-center gap-2">
|
||||||
|
<TrendingUp className="h-4 w-4" />
|
||||||
|
Revenue
|
||||||
|
</TabsTrigger>
|
||||||
|
<TabsTrigger value="products" className="flex items-center gap-2">
|
||||||
|
<Package className="h-4 w-4" />
|
||||||
|
Products
|
||||||
|
</TabsTrigger>
|
||||||
|
<TabsTrigger value="customers" className="flex items-center gap-2">
|
||||||
|
<Users className="h-4 w-4" />
|
||||||
|
Customers
|
||||||
|
</TabsTrigger>
|
||||||
|
<TabsTrigger value="orders" className="flex items-center gap-2">
|
||||||
|
<BarChart3 className="h-4 w-4" />
|
||||||
|
Orders
|
||||||
|
</TabsTrigger>
|
||||||
|
</TabsList>
|
||||||
|
|
||||||
|
<TabsContent value="revenue" className="space-y-6">
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="flex items-center gap-2">
|
||||||
|
<TrendingUp className="h-5 w-5" />
|
||||||
|
Revenue Trends
|
||||||
|
</CardTitle>
|
||||||
|
<CardDescription>
|
||||||
|
Revenue performance over the selected time period
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="space-y-6">
|
||||||
|
{/* Chart area */}
|
||||||
|
<div className="h-64 bg-muted/20 rounded-md animate-pulse" />
|
||||||
|
|
||||||
|
{/* Summary stats */}
|
||||||
|
<div className="grid grid-cols-3 gap-4 pt-4 border-t">
|
||||||
|
{[...Array(3)].map((_, i) => (
|
||||||
|
<div key={i} className="text-center space-y-2">
|
||||||
|
<Skeleton className="h-8 w-20 mx-auto" />
|
||||||
|
<Skeleton className="h-4 w-24 mx-auto" />
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</TabsContent>
|
||||||
|
|
||||||
|
<TabsContent value="products" className="space-y-6">
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="flex items-center gap-2">
|
||||||
|
<Package className="h-5 w-5" />
|
||||||
|
Product Performance
|
||||||
|
</CardTitle>
|
||||||
|
<CardDescription>
|
||||||
|
Top performing products by revenue and sales
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="space-y-4">
|
||||||
|
{/* Table header */}
|
||||||
|
<div className="grid grid-cols-5 gap-4">
|
||||||
|
{[...Array(5)].map((_, i) => (
|
||||||
|
<Skeleton key={i} className="h-4 w-full" />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Table rows */}
|
||||||
|
{[...Array(8)].map((_, rowIndex) => (
|
||||||
|
<div key={rowIndex} className="grid grid-cols-5 gap-4">
|
||||||
|
{[...Array(5)].map((_, colIndex) => (
|
||||||
|
<div key={colIndex} className="flex items-center gap-3">
|
||||||
|
{colIndex === 0 && (
|
||||||
|
<Skeleton className="h-10 w-10 rounded" />
|
||||||
|
)}
|
||||||
|
<Skeleton className="h-4 flex-1" />
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</TabsContent>
|
||||||
|
|
||||||
|
<TabsContent value="customers" className="space-y-6">
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="flex items-center gap-2">
|
||||||
|
<Users className="h-5 w-5" />
|
||||||
|
Customer Insights
|
||||||
|
</CardTitle>
|
||||||
|
<CardDescription>
|
||||||
|
Customer segmentation and behavior analysis
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="space-y-6">
|
||||||
|
{/* Customer segments */}
|
||||||
|
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
|
||||||
|
{[...Array(4)].map((_, i) => (
|
||||||
|
<div key={i} className="text-center space-y-2">
|
||||||
|
<Skeleton className="h-8 w-16 mx-auto" />
|
||||||
|
<Skeleton className="h-4 w-20 mx-auto" />
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Top customers table */}
|
||||||
|
<div className="space-y-4">
|
||||||
|
<Skeleton className="h-6 w-32" />
|
||||||
|
{[...Array(5)].map((_, i) => (
|
||||||
|
<div key={i} className="flex items-center justify-between p-4 border rounded-lg">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Skeleton className="h-4 w-24" />
|
||||||
|
<Skeleton className="h-3 w-16" />
|
||||||
|
</div>
|
||||||
|
<div className="text-right space-y-2">
|
||||||
|
<Skeleton className="h-4 w-16" />
|
||||||
|
<Skeleton className="h-3 w-12" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</TabsContent>
|
||||||
|
|
||||||
|
<TabsContent value="orders" className="space-y-6">
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="flex items-center gap-2">
|
||||||
|
<BarChart3 className="h-5 w-5" />
|
||||||
|
Order Analytics
|
||||||
|
</CardTitle>
|
||||||
|
<CardDescription>
|
||||||
|
Order status distribution and trends
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="space-y-6">
|
||||||
|
{/* Chart area */}
|
||||||
|
<div className="h-64 bg-muted/20 rounded-md animate-pulse" />
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</TabsContent>
|
||||||
|
</Tabs>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -8,6 +8,7 @@ import { Skeleton } from "@/components/ui/skeleton";
|
|||||||
import { Users, Crown, UserPlus, UserCheck, Star } from "lucide-react";
|
import { Users, Crown, UserPlus, UserCheck, Star } from "lucide-react";
|
||||||
import { getCustomerInsightsWithStore, type CustomerInsights } from "@/lib/services/analytics-service";
|
import { getCustomerInsightsWithStore, type CustomerInsights } from "@/lib/services/analytics-service";
|
||||||
import { formatGBP } from "@/utils/format";
|
import { formatGBP } from "@/utils/format";
|
||||||
|
import { CustomerInsightsSkeleton } from './SkeletonLoaders';
|
||||||
|
|
||||||
export default function CustomerInsightsChart() {
|
export default function CustomerInsightsChart() {
|
||||||
const [data, setData] = useState<CustomerInsights | null>(null);
|
const [data, setData] = useState<CustomerInsights | null>(null);
|
||||||
@@ -85,26 +86,11 @@ export default function CustomerInsightsChart() {
|
|||||||
|
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
return (
|
return (
|
||||||
<Card>
|
<CustomerInsightsSkeleton
|
||||||
<CardHeader>
|
title="Customer Insights"
|
||||||
<CardTitle className="flex items-center gap-2">
|
description="Customer segmentation and behavior analysis"
|
||||||
<Users className="h-5 w-5" />
|
icon={Users}
|
||||||
Customer Insights
|
/>
|
||||||
</CardTitle>
|
|
||||||
<CardDescription>
|
|
||||||
Customer segmentation and behavior analysis
|
|
||||||
</CardDescription>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent>
|
|
||||||
<div className="space-y-6">
|
|
||||||
<Skeleton className="h-32 w-full" />
|
|
||||||
<div className="grid grid-cols-2 gap-4">
|
|
||||||
<Skeleton className="h-20" />
|
|
||||||
<Skeleton className="h-20" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import { Skeleton } from "@/components/ui/skeleton";
|
|||||||
import { BarChart3, Clock, CheckCircle, XCircle, AlertCircle } from "lucide-react";
|
import { BarChart3, Clock, CheckCircle, XCircle, AlertCircle } from "lucide-react";
|
||||||
import { getOrderAnalyticsWithStore, type OrderAnalytics } from "@/lib/services/analytics-service";
|
import { getOrderAnalyticsWithStore, type OrderAnalytics } from "@/lib/services/analytics-service";
|
||||||
import { formatGBP } from "@/utils/format";
|
import { formatGBP } from "@/utils/format";
|
||||||
|
import { ChartSkeleton } from './SkeletonLoaders';
|
||||||
|
|
||||||
interface OrderAnalyticsChartProps {
|
interface OrderAnalyticsChartProps {
|
||||||
timeRange: string;
|
timeRange: string;
|
||||||
@@ -105,23 +106,12 @@ export default function OrderAnalyticsChart({ timeRange }: OrderAnalyticsChartPr
|
|||||||
|
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
return (
|
return (
|
||||||
<Card>
|
<ChartSkeleton
|
||||||
<CardHeader>
|
title="Order Analytics"
|
||||||
<CardTitle className="flex items-center gap-2">
|
description="Order status distribution and trends"
|
||||||
<BarChart3 className="h-5 w-5" />
|
icon={BarChart3}
|
||||||
Order Analytics
|
showStats={false}
|
||||||
</CardTitle>
|
/>
|
||||||
<CardDescription>
|
|
||||||
Order status distribution and trends
|
|
||||||
</CardDescription>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent>
|
|
||||||
<div className="space-y-6">
|
|
||||||
<Skeleton className="h-32 w-full" />
|
|
||||||
<Skeleton className="h-64 w-full" />
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import { Skeleton } from "@/components/ui/skeleton";
|
|||||||
import { Package } from "lucide-react";
|
import { Package } from "lucide-react";
|
||||||
import { getProductPerformanceWithStore, type ProductPerformance } from "@/lib/services/analytics-service";
|
import { getProductPerformanceWithStore, type ProductPerformance } from "@/lib/services/analytics-service";
|
||||||
import { formatGBP } from "@/utils/format";
|
import { formatGBP } from "@/utils/format";
|
||||||
|
import { TableSkeleton } from './SkeletonLoaders';
|
||||||
|
|
||||||
export default function ProductPerformanceChart() {
|
export default function ProductPerformanceChart() {
|
||||||
const [data, setData] = useState<ProductPerformance[]>([]);
|
const [data, setData] = useState<ProductPerformance[]>([]);
|
||||||
@@ -41,33 +42,13 @@ export default function ProductPerformanceChart() {
|
|||||||
|
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
return (
|
return (
|
||||||
<Card>
|
<TableSkeleton
|
||||||
<CardHeader>
|
title="Product Performance"
|
||||||
<CardTitle className="flex items-center gap-2">
|
description="Top performing products by revenue and sales"
|
||||||
<Package className="h-5 w-5" />
|
icon={Package}
|
||||||
Product Performance
|
rows={8}
|
||||||
</CardTitle>
|
columns={5}
|
||||||
<CardDescription>
|
/>
|
||||||
Top performing products by revenue and sales
|
|
||||||
</CardDescription>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent>
|
|
||||||
<div className="space-y-4">
|
|
||||||
<Skeleton className="h-8 w-full" />
|
|
||||||
{[...Array(5)].map((_, i) => (
|
|
||||||
<div key={i} className="flex items-center gap-4">
|
|
||||||
<Skeleton className="h-12 w-12 rounded" />
|
|
||||||
<div className="space-y-2 flex-1">
|
|
||||||
<Skeleton className="h-4 w-32" />
|
|
||||||
<Skeleton className="h-3 w-24" />
|
|
||||||
</div>
|
|
||||||
<Skeleton className="h-4 w-16" />
|
|
||||||
<Skeleton className="h-4 w-20" />
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import { TrendingUp, DollarSign } from "lucide-react";
|
|||||||
import { getRevenueTrendsWithStore, type RevenueData } from "@/lib/services/analytics-service";
|
import { getRevenueTrendsWithStore, type RevenueData } from "@/lib/services/analytics-service";
|
||||||
import { formatGBP } from "@/utils/format";
|
import { formatGBP } from "@/utils/format";
|
||||||
import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, BarChart, Bar } from 'recharts';
|
import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, BarChart, Bar } from 'recharts';
|
||||||
|
import { ChartSkeleton } from './SkeletonLoaders';
|
||||||
|
|
||||||
interface RevenueChartProps {
|
interface RevenueChartProps {
|
||||||
timeRange: string;
|
timeRange: string;
|
||||||
@@ -95,27 +96,12 @@ export default function RevenueChart({ timeRange }: RevenueChartProps) {
|
|||||||
|
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
return (
|
return (
|
||||||
<Card>
|
<ChartSkeleton
|
||||||
<CardHeader>
|
title="Revenue Trends"
|
||||||
<CardTitle className="flex items-center gap-2">
|
description="Revenue performance over the selected time period"
|
||||||
<TrendingUp className="h-5 w-5" />
|
icon={TrendingUp}
|
||||||
Revenue Trends
|
showStats={true}
|
||||||
</CardTitle>
|
/>
|
||||||
<CardDescription>
|
|
||||||
Revenue performance over the selected time period
|
|
||||||
</CardDescription>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent>
|
|
||||||
<div className="space-y-4">
|
|
||||||
<Skeleton className="h-64 w-full" />
|
|
||||||
<div className="grid grid-cols-3 gap-4">
|
|
||||||
<Skeleton className="h-16" />
|
|
||||||
<Skeleton className="h-16" />
|
|
||||||
<Skeleton className="h-16" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
170
components/analytics/SkeletonLoaders.tsx
Normal file
170
components/analytics/SkeletonLoaders.tsx
Normal file
@@ -0,0 +1,170 @@
|
|||||||
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
|
import { Skeleton } from "@/components/ui/skeleton";
|
||||||
|
|
||||||
|
// Chart skeleton for revenue trends and order analytics
|
||||||
|
export function ChartSkeleton({
|
||||||
|
title,
|
||||||
|
description,
|
||||||
|
icon: Icon,
|
||||||
|
showStats = false
|
||||||
|
}: {
|
||||||
|
title: string;
|
||||||
|
description: string;
|
||||||
|
icon: any;
|
||||||
|
showStats?: boolean;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="flex items-center gap-2">
|
||||||
|
<Icon className="h-5 w-5" />
|
||||||
|
{title}
|
||||||
|
</CardTitle>
|
||||||
|
<CardDescription>{description}</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="space-y-6">
|
||||||
|
{/* Chart area */}
|
||||||
|
<div className="h-64 bg-muted/20 rounded-md animate-pulse" />
|
||||||
|
|
||||||
|
{/* Summary stats if applicable */}
|
||||||
|
{showStats && (
|
||||||
|
<div className="grid grid-cols-3 gap-4 pt-4 border-t">
|
||||||
|
{[...Array(3)].map((_, i) => (
|
||||||
|
<div key={i} className="text-center space-y-2">
|
||||||
|
<Skeleton className="h-8 w-20 mx-auto" />
|
||||||
|
<Skeleton className="h-4 w-24 mx-auto" />
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Table skeleton for product performance
|
||||||
|
export function TableSkeleton({
|
||||||
|
title,
|
||||||
|
description,
|
||||||
|
icon: Icon,
|
||||||
|
rows = 5,
|
||||||
|
columns = 5
|
||||||
|
}: {
|
||||||
|
title: string;
|
||||||
|
description: string;
|
||||||
|
icon: any;
|
||||||
|
rows?: number;
|
||||||
|
columns?: number;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="flex items-center gap-2">
|
||||||
|
<Icon className="h-5 w-5" />
|
||||||
|
{title}
|
||||||
|
</CardTitle>
|
||||||
|
<CardDescription>{description}</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="space-y-4">
|
||||||
|
{/* Table header */}
|
||||||
|
<div className="grid gap-4" style={{ gridTemplateColumns: `repeat(${columns}, 1fr)` }}>
|
||||||
|
{[...Array(columns)].map((_, i) => (
|
||||||
|
<Skeleton key={i} className="h-4 w-full" />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Table rows */}
|
||||||
|
{[...Array(rows)].map((_, rowIndex) => (
|
||||||
|
<div key={rowIndex} className="grid gap-4" style={{ gridTemplateColumns: `repeat(${columns}, 1fr)` }}>
|
||||||
|
{[...Array(columns)].map((_, colIndex) => (
|
||||||
|
<div key={colIndex} className="flex items-center gap-3">
|
||||||
|
{colIndex === 0 && (
|
||||||
|
<Skeleton className="h-10 w-10 rounded" />
|
||||||
|
)}
|
||||||
|
<Skeleton className="h-4 flex-1" />
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Customer insights skeleton with segments
|
||||||
|
export function CustomerInsightsSkeleton({
|
||||||
|
title,
|
||||||
|
description,
|
||||||
|
icon: Icon
|
||||||
|
}: {
|
||||||
|
title: string;
|
||||||
|
description: string;
|
||||||
|
icon: any;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="flex items-center gap-2">
|
||||||
|
<Icon className="h-5 w-5" />
|
||||||
|
{title}
|
||||||
|
</CardTitle>
|
||||||
|
<CardDescription>{description}</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="space-y-6">
|
||||||
|
{/* Customer segments */}
|
||||||
|
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
|
||||||
|
{[...Array(4)].map((_, i) => (
|
||||||
|
<div key={i} className="text-center space-y-2">
|
||||||
|
<Skeleton className="h-8 w-16 mx-auto" />
|
||||||
|
<Skeleton className="h-4 w-20 mx-auto" />
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Top customers table */}
|
||||||
|
<div className="space-y-4">
|
||||||
|
<Skeleton className="h-6 w-32" />
|
||||||
|
{[...Array(5)].map((_, i) => (
|
||||||
|
<div key={i} className="flex items-center justify-between p-4 border rounded-lg">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Skeleton className="h-4 w-24" />
|
||||||
|
<Skeleton className="h-3 w-16" />
|
||||||
|
</div>
|
||||||
|
<div className="text-right space-y-2">
|
||||||
|
<Skeleton className="h-4 w-16" />
|
||||||
|
<Skeleton className="h-3 w-12" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Metrics card skeleton
|
||||||
|
export function MetricsCardSkeleton() {
|
||||||
|
return (
|
||||||
|
<Card>
|
||||||
|
<CardContent className="p-6">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Skeleton className="h-4 w-24" />
|
||||||
|
<Skeleton className="h-8 w-20" />
|
||||||
|
</div>
|
||||||
|
<Skeleton className="h-8 w-8 rounded" />
|
||||||
|
</div>
|
||||||
|
<div className="mt-4 flex items-center gap-2">
|
||||||
|
<Skeleton className="h-3 w-16" />
|
||||||
|
<Skeleton className="h-3 w-20" />
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user