fix
This commit is contained in:
@@ -3,6 +3,7 @@ import { cookies } from 'next/headers';
|
||||
import { redirect } from 'next/navigation';
|
||||
import Dashboard from "@/components/dashboard/dashboard";
|
||||
import AnalyticsDashboard from '@/components/analytics/AnalyticsDashboard';
|
||||
import AnalyticsDashboardSkeleton from '@/components/analytics/AnalyticsDashboardSkeleton';
|
||||
import StoreSelector from '@/components/analytics/StoreSelector';
|
||||
import { getAnalyticsOverviewServer } from '@/lib/server-api';
|
||||
import { fetchServer } from '@/lib/api';
|
||||
@@ -65,14 +66,7 @@ export default async function AnalyticsPage({
|
||||
<Dashboard>
|
||||
<div className="space-y-6">
|
||||
{/* Analytics Content */}
|
||||
<Suspense fallback={
|
||||
<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>
|
||||
}>
|
||||
<Suspense fallback={<AnalyticsDashboardSkeleton />}>
|
||||
<AnalyticsDashboard initialData={initialData} />
|
||||
</Suspense>
|
||||
</div>
|
||||
|
||||
@@ -25,6 +25,7 @@ import OrderAnalyticsChart from "./OrderAnalyticsChart";
|
||||
import MetricsCard from "./MetricsCard";
|
||||
import { getAnalyticsOverviewWithStore, type AnalyticsOverview } from "@/lib/services/analytics-service";
|
||||
import { formatGBP } from "@/utils/format";
|
||||
import { MetricsCardSkeleton } from './SkeletonLoaders';
|
||||
|
||||
interface AnalyticsDashboardProps {
|
||||
initialData: AnalyticsOverview;
|
||||
@@ -95,9 +96,15 @@ export default function AnalyticsDashboard({ initialData }: AnalyticsDashboardPr
|
||||
<div className="space-y-6">
|
||||
{/* Key Metrics Cards */}
|
||||
<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} />
|
||||
))}
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Completion Rate Card */}
|
||||
@@ -112,6 +119,15 @@ export default function AnalyticsDashboard({ initialData }: AnalyticsDashboardPr
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<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="text-3xl font-bold">
|
||||
{data.orders.completionRate}%
|
||||
@@ -128,9 +144,30 @@ export default function AnalyticsDashboard({ initialData }: AnalyticsDashboardPr
|
||||
{data.orders.completed} / {data.orders.total}
|
||||
</Badge>
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</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 */}
|
||||
<div 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 { getCustomerInsightsWithStore, type CustomerInsights } from "@/lib/services/analytics-service";
|
||||
import { formatGBP } from "@/utils/format";
|
||||
import { CustomerInsightsSkeleton } from './SkeletonLoaders';
|
||||
|
||||
export default function CustomerInsightsChart() {
|
||||
const [data, setData] = useState<CustomerInsights | null>(null);
|
||||
@@ -85,26 +86,11 @@ export default function CustomerInsightsChart() {
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<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">
|
||||
<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>
|
||||
<CustomerInsightsSkeleton
|
||||
title="Customer Insights"
|
||||
description="Customer segmentation and behavior analysis"
|
||||
icon={Users}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ import { Skeleton } from "@/components/ui/skeleton";
|
||||
import { BarChart3, Clock, CheckCircle, XCircle, AlertCircle } from "lucide-react";
|
||||
import { getOrderAnalyticsWithStore, type OrderAnalytics } from "@/lib/services/analytics-service";
|
||||
import { formatGBP } from "@/utils/format";
|
||||
import { ChartSkeleton } from './SkeletonLoaders';
|
||||
|
||||
interface OrderAnalyticsChartProps {
|
||||
timeRange: string;
|
||||
@@ -105,23 +106,12 @@ export default function OrderAnalyticsChart({ timeRange }: OrderAnalyticsChartPr
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<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">
|
||||
<Skeleton className="h-32 w-full" />
|
||||
<Skeleton className="h-64 w-full" />
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<ChartSkeleton
|
||||
title="Order Analytics"
|
||||
description="Order status distribution and trends"
|
||||
icon={BarChart3}
|
||||
showStats={false}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ import { Skeleton } from "@/components/ui/skeleton";
|
||||
import { Package } from "lucide-react";
|
||||
import { getProductPerformanceWithStore, type ProductPerformance } from "@/lib/services/analytics-service";
|
||||
import { formatGBP } from "@/utils/format";
|
||||
import { TableSkeleton } from './SkeletonLoaders';
|
||||
|
||||
export default function ProductPerformanceChart() {
|
||||
const [data, setData] = useState<ProductPerformance[]>([]);
|
||||
@@ -41,33 +42,13 @@ export default function ProductPerformanceChart() {
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<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">
|
||||
<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>
|
||||
<TableSkeleton
|
||||
title="Product Performance"
|
||||
description="Top performing products by revenue and sales"
|
||||
icon={Package}
|
||||
rows={8}
|
||||
columns={5}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ import { TrendingUp, DollarSign } from "lucide-react";
|
||||
import { getRevenueTrendsWithStore, type RevenueData } from "@/lib/services/analytics-service";
|
||||
import { formatGBP } from "@/utils/format";
|
||||
import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, BarChart, Bar } from 'recharts';
|
||||
import { ChartSkeleton } from './SkeletonLoaders';
|
||||
|
||||
interface RevenueChartProps {
|
||||
timeRange: string;
|
||||
@@ -95,27 +96,12 @@ export default function RevenueChart({ timeRange }: RevenueChartProps) {
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<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-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>
|
||||
<ChartSkeleton
|
||||
title="Revenue Trends"
|
||||
description="Revenue performance over the selected time period"
|
||||
icon={TrendingUp}
|
||||
showStats={true}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
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