Add and standardize skeletons

This commit is contained in:
2024-12-29 01:37:45 -05:00
parent 9702045d15
commit 90b0dfa700
11 changed files with 995 additions and 356 deletions

View File

@@ -165,6 +165,81 @@ const AgentPerformanceTable = ({ agents, onSort }) => {
);
};
const SkeletonMetricCard = () => (
<Card className="bg-white dark:bg-gray-900/60 backdrop-blur-sm">
<CardHeader className="flex flex-col items-start p-4">
<Skeleton className="h-4 w-24 mb-2" />
<Skeleton className="h-8 w-32 mb-2" />
<div className="flex gap-4">
<Skeleton className="h-4 w-20" />
<Skeleton className="h-4 w-20" />
</div>
</CardHeader>
</Card>
);
const SkeletonChart = ({ type = "line" }) => (
<div className="h-[300px] w-full bg-gray-50 dark:bg-gray-800 rounded-lg p-4">
<div className="h-full flex flex-col">
<div className="flex-1 relative">
{type === "bar" ? (
<div className="h-full flex items-end justify-between gap-1">
{[...Array(24)].map((_, i) => (
<div
key={i}
className="w-full bg-gray-200 dark:bg-gray-700 rounded-t animate-pulse"
style={{ height: `${15 + Math.random() * 70}%` }}
/>
))}
</div>
) : (
<div className="h-full w-full relative">
{[...Array(5)].map((_, i) => (
<div
key={i}
className="absolute w-full h-px bg-gray-200 dark:bg-gray-700"
style={{ top: `${20 + i * 20}%` }}
/>
))}
<div
className="absolute inset-0 bg-gray-300 dark:bg-gray-600 animate-pulse"
style={{
opacity: 0.2,
clipPath: "polygon(0 50%, 100% 20%, 100% 100%, 0 100%)",
}}
/>
</div>
)}
</div>
</div>
</div>
);
const SkeletonTable = ({ rows = 5 }) => (
<Table>
<TableHeader>
<TableRow className="hover:bg-transparent">
<TableHead><Skeleton className="h-4 w-24" /></TableHead>
<TableHead><Skeleton className="h-4 w-24" /></TableHead>
<TableHead><Skeleton className="h-4 w-24" /></TableHead>
<TableHead><Skeleton className="h-4 w-24" /></TableHead>
<TableHead><Skeleton className="h-4 w-24" /></TableHead>
</TableRow>
</TableHeader>
<TableBody>
{[...Array(rows)].map((_, i) => (
<TableRow key={i} className="hover:bg-gray-50 dark:hover:bg-gray-800/50">
<TableCell><Skeleton className="h-4 w-32" /></TableCell>
<TableCell><Skeleton className="h-4 w-16" /></TableCell>
<TableCell><Skeleton className="h-4 w-16" /></TableCell>
<TableCell><Skeleton className="h-4 w-16" /></TableCell>
<TableCell><Skeleton className="h-4 w-24" /></TableCell>
</TableRow>
))}
</TableBody>
</Table>
);
const AircallDashboard = () => {
const [timeRange, setTimeRange] = useState("last7days");
const [metrics, setMetrics] = useState(null);
@@ -316,7 +391,7 @@ const AircallDashboard = () => {
<div className="grid grid-cols-2 lg:grid-cols-4 gap-4">
{isLoading ? (
[...Array(4)].map((_, i) => (
<Skeleton key={i} className="h-32 rounded-lg" />
<SkeletonMetricCard key={i} />
))
) : metrics ? (
<>
@@ -407,6 +482,9 @@ const AircallDashboard = () => {
<CardTitle className="text-lg font-semibold text-gray-900 dark:text-gray-100">Daily Call Volume</CardTitle>
</CardHeader>
<CardContent className="h-[300px]">
{isLoading ? (
<SkeletonChart type="bar" />
) : (
<ResponsiveContainer width="100%" height="100%">
<BarChart data={chartData.daily} margin={{ top: 0, right: 5, left: -35, bottom: 0 }}>
<CartesianGrid strokeDasharray="3 3" className="stroke-gray-200 dark:stroke-gray-700" />
@@ -425,6 +503,7 @@ const AircallDashboard = () => {
<Bar dataKey="outbound" fill={COLORS.outbound} name="Outbound" />
</BarChart>
</ResponsiveContainer>
)}
</CardContent>
</Card>
@@ -434,6 +513,9 @@ const AircallDashboard = () => {
<CardTitle className="text-lg font-semibold text-gray-900 dark:text-gray-100">Hourly Distribution</CardTitle>
</CardHeader>
<CardContent className="h-[300px]">
{isLoading ? (
<SkeletonChart type="bar" />
) : (
<ResponsiveContainer width="100%" height="100%">
<BarChart data={chartData.hourly} margin={{ top: 0, right: 5, left: -35, bottom: 0 }}>
<CartesianGrid strokeDasharray="3 3" className="stroke-gray-200 dark:stroke-gray-700" />
@@ -451,6 +533,7 @@ const AircallDashboard = () => {
<Bar dataKey="calls" fill={COLORS.hourly} name="Calls" />
</BarChart>
</ResponsiveContainer>
)}
</CardContent>
</Card>
</div>
@@ -463,10 +546,14 @@ const AircallDashboard = () => {
<CardTitle className="text-lg font-semibold text-gray-900 dark:text-gray-100">Agent Performance</CardTitle>
</CardHeader>
<CardContent>
{isLoading ? (
<SkeletonTable rows={5} />
) : (
<AgentPerformanceTable
agents={sortedAgents}
onSort={(key, direction) => setAgentSort({ key, direction })}
/>
)}
</CardContent>
</Card>
@@ -476,6 +563,9 @@ const AircallDashboard = () => {
<CardTitle className="text-lg font-semibold text-gray-900 dark:text-gray-100">Missed Call Reasons</CardTitle>
</CardHeader>
<CardContent>
{isLoading ? (
<SkeletonTable rows={5} />
) : (
<Table>
<TableHeader>
<TableRow className="hover:bg-transparent">
@@ -496,6 +586,7 @@ const AircallDashboard = () => {
))}
</TableBody>
</Table>
)}
</CardContent>
</Card>
</div>

View File

@@ -56,29 +56,50 @@ const SkeletonChart = () => (
clipPath: "polygon(0 50%, 100% 20%, 100% 100%, 0 100%)",
}}
/>
<div className="absolute inset-0 flex justify-between">
{[...Array(8)].map((_, i) => (
<div
key={i}
className="w-px h-full bg-gray-200 dark:bg-gray-700"
style={{ opacity: 0.5 }}
/>
))}
</div>
</div>
</div>
<div className="flex justify-between pt-4">
{[...Array(6)].map((_, i) => (
<Skeleton key={i} className="h-4 w-12 dark:bg-gray-700" />
))}
</div>
</div>
</div>
);
const SkeletonStats = () => (
<div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4">
<div className="grid grid-cols-2 lg:grid-cols-4 gap-4 py-4">
{[...Array(4)].map((_, i) => (
<Card key={i}>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<Skeleton className="h-4 w-24" />
<Skeleton className="h-4 w-4 rounded-full" />
<CardHeader className="flex flex-row items-center justify-between space-y-0 p-4 pb-2">
<Skeleton className="h-4 w-24 dark:bg-gray-700" />
</CardHeader>
<CardContent>
<Skeleton className="h-8 w-32 mb-2" />
<Skeleton className="h-4 w-24" />
<CardContent className="p-4 pt-0">
<Skeleton className="h-8 w-32 mb-2 dark:bg-gray-700" />
<Skeleton className="h-4 w-24 dark:bg-gray-700" />
</CardContent>
</Card>
))}
</div>
);
const SkeletonButtons = () => (
<div className="flex flex-wrap gap-1">
{[...Array(4)].map((_, i) => (
<Skeleton key={i} className="h-8 w-20 dark:bg-gray-700" />
))}
</div>
);
// Add StatCard component
const StatCard = ({
title,
@@ -264,6 +285,9 @@ export const AnalyticsDashboard = () => {
<CardTitle className="text-xl font-semibold text-gray-900 dark:text-gray-100">
Analytics Overview
</CardTitle>
{loading ? (
<Skeleton className="h-9 w-[130px] dark:bg-gray-700" />
) : (
<Select value={timeRange} onValueChange={setTimeRange}>
<SelectTrigger className="w-[130px] h-9">
<SelectValue placeholder="Select range" />
@@ -275,6 +299,7 @@ export const AnalyticsDashboard = () => {
<SelectItem value="90">Last 90 days</SelectItem>
</SelectContent>
</Select>
)}
</div>
{loading ? (
@@ -317,6 +342,9 @@ export const AnalyticsDashboard = () => {
) : null}
<div className="flex flex-col sm:flex-row gap-0 sm:gap-4 pt-2">
{loading ? (
<SkeletonButtons />
) : (
<div className="flex flex-wrap gap-1">
<Button
variant={metrics.activeUsers ? "default" : "outline"}
@@ -371,6 +399,7 @@ export const AnalyticsDashboard = () => {
<span className="sm:hidden">Conversions</span>
</Button>
</div>
)}
</div>
</div>
</CardHeader>

View File

@@ -142,19 +142,34 @@ const formatShipMethodSimple = (method) => {
// Loading State Component
const LoadingState = () => (
<div className="flex items-center justify-center p-8">
<div className="space-y-4 w-full">
{[...Array(5)].map((_, i) => (
<div key={i} className="flex items-center space-x-4">
<div className="h-12 w-12 rounded-full bg-gray-200 dark:bg-gray-800 animate-pulse" />
<div className="space-y-2 flex-1">
<div className="h-4 w-[250px] bg-gray-200 dark:bg-gray-800 animate-pulse rounded" />
<div className="h-4 w-[200px] bg-gray-200 dark:bg-gray-800 animate-pulse rounded" />
<div className="divide-y divide-gray-100 dark:divide-gray-800">
{[...Array(8)].map((_, i) => (
<div key={i} className="flex items-center gap-3 p-4 hover:bg-gray-50 dark:hover:bg-gray-800/50 transition-colors">
<div className="shrink-0">
<Skeleton className="h-10 w-10 rounded-full bg-muted" />
</div>
<div className="flex-1 min-w-0">
<div className="space-y-2">
<div className="flex items-center gap-2">
<Skeleton className="h-4 w-24 bg-muted" />
</div>
<div className="flex items-center gap-2">
<Skeleton className="h-4 w-48 bg-muted" />
</div>
<div className="flex gap-1.5 items-center flex-wrap">
<Skeleton className="h-5 w-16 rounded-full bg-muted" />
<Skeleton className="h-5 w-20 rounded-full bg-muted" />
<Skeleton className="h-5 w-14 rounded-full bg-muted" />
</div>
</div>
</div>
<div className="flex items-center gap-2 shrink-0">
<Skeleton className="h-4 w-16 bg-muted" />
<Skeleton className="h-4 w-4 bg-muted" />
</div>
</div>
))}
</div>
</div>
);
// Empty State Component

View File

@@ -111,10 +111,17 @@ const MetricCard = ({
<CardContent className="pt-6 h-full">
<div className="flex justify-between items-start">
<div className="flex-1 min-w-0">
<p className="text-sm font-medium text-muted-foreground">{title}</p>
{loading ? (
<Skeleton className="h-8 w-24 dark:bg-gray-700" />
<>
<Skeleton className="h-4 w-24 mb-4 dark:bg-gray-700" />
<div className="flex items-baseline gap-2">
<Skeleton className="h-8 w-20 dark:bg-gray-700" />
<Skeleton className="h-4 w-12 dark:bg-gray-700" />
</div>
</>
) : (
<>
<p className="text-sm font-medium text-muted-foreground">{title}</p>
<div className="flex items-baseline gap-2">
<p className="text-2xl font-bold">
{typeof value === "number"
@@ -134,9 +141,10 @@ const MetricCard = ({
</div>
)}
</div>
</>
)}
</div>
{Icon && (
{!loading && Icon && (
<Icon className={`h-5 w-5 flex-shrink-0 ml-2 ${colorClass === "blue" ? "text-blue-500" :
colorClass === "green" ? "text-green-500" :
colorClass === "purple" ? "text-purple-500" :
@@ -146,19 +154,53 @@ const MetricCard = ({
colorClass === "cyan" ? "text-cyan-500" :
"text-blue-500"}`} />
)}
{loading && (
<Skeleton className="h-5 w-5 rounded-full dark:bg-gray-700" />
)}
</div>
</CardContent>
</Card>
);
};
const TableSkeleton = () => (
<div className="space-y-2">
<Skeleton className="h-8 w-full dark:bg-gray-700" />
<Skeleton className="h-8 w-full dark:bg-gray-700" />
<Skeleton className="h-8 w-full dark:bg-gray-700" />
<Skeleton className="h-8 w-full dark:bg-gray-700" />
const SkeletonMetricCard = () => (
<Card className="h-full">
<CardContent className="pt-6 h-full">
<div className="flex justify-between items-start">
<div className="flex-1 min-w-0">
<Skeleton className="h-4 w-24 mb-4 dark:bg-gray-700" />
<div className="flex items-baseline gap-2">
<Skeleton className="h-8 w-20 dark:bg-gray-700" />
<Skeleton className="h-4 w-12 dark:bg-gray-700" />
</div>
</div>
<Skeleton className="h-5 w-5 rounded-full dark:bg-gray-700" />
</div>
</CardContent>
</Card>
);
const TableSkeleton = () => (
<Table>
<TableHeader>
<TableRow className="dark:border-gray-800">
<TableHead><Skeleton className="h-4 w-24 dark:bg-gray-700" /></TableHead>
<TableHead className="text-right"><Skeleton className="h-4 w-16 ml-auto dark:bg-gray-700" /></TableHead>
<TableHead className="text-right"><Skeleton className="h-4 w-16 ml-auto dark:bg-gray-700" /></TableHead>
<TableHead className="text-right"><Skeleton className="h-4 w-16 ml-auto dark:bg-gray-700" /></TableHead>
</TableRow>
</TableHeader>
<TableBody>
{[...Array(5)].map((_, i) => (
<TableRow key={i} className="dark:border-gray-800">
<TableCell><Skeleton className="h-4 w-32 dark:bg-gray-700" /></TableCell>
<TableCell className="text-right"><Skeleton className="h-4 w-12 ml-auto dark:bg-gray-700" /></TableCell>
<TableCell className="text-right"><Skeleton className="h-4 w-12 ml-auto dark:bg-gray-700" /></TableCell>
<TableCell className="text-right"><Skeleton className="h-4 w-16 ml-auto dark:bg-gray-700" /></TableCell>
</TableRow>
))}
</TableBody>
</Table>
);
const GorgiasOverview = () => {
@@ -324,6 +366,12 @@ const GorgiasOverview = () => {
<CardContent className="space-y-4">
<div className="grid grid-cols-2 lg:grid-cols-4 gap-4">
{/* Message & Response Metrics */}
{loading ? (
[...Array(8)].map((_, i) => (
<SkeletonMetricCard key={i} />
))
) : (
<>
<div className="h-full">
<MetricCard
title="Messages Received"
@@ -366,6 +414,8 @@ const GorgiasOverview = () => {
loading={loading}
/>
</div>
</>
)}
{/* Satisfaction & Efficiency */}
<div className="h-full">
@@ -423,9 +473,7 @@ const GorgiasOverview = () => {
</h3>
</div>
{loading ? (
<div className="p-4">
<TableSkeleton />
</div>
) : (
<Table>
<TableHeader>
@@ -487,9 +535,7 @@ const GorgiasOverview = () => {
</h3>
</div>
{loading ? (
<div className="p-4">
<TableSkeleton />
</div>
) : (
<Table>
<TableHeader>

View File

@@ -17,6 +17,7 @@ import {
import { Button } from "@/components/ui/button";
import { TIME_RANGES } from "@/lib/constants";
import { Mail, MessageSquare, ArrowUpDown } from "lucide-react";
import { Skeleton } from "@/components/ui/skeleton";
// Helper functions for formatting
const formatRate = (value, isSMS = false, hideForSMS = false) => {
@@ -37,14 +38,76 @@ const formatCurrency = (value) => {
// Loading skeleton component
const TableSkeleton = () => (
<div className="space-y-4">
{[...Array(5)].map((_, i) => (
<div
key={i}
className="h-16 bg-gray-100 dark:bg-gray-800 animate-pulse rounded"
/>
))}
<table className="w-full">
<thead>
<tr>
<th className="p-2 text-left font-medium sticky top-0 bg-white dark:bg-gray-900 z-10">
<Skeleton className="h-8 w-24 dark:bg-gray-700" />
</th>
<th className="p-2 text-center font-medium sticky top-0 bg-white dark:bg-gray-900 z-10">
<Skeleton className="h-8 w-20 mx-auto dark:bg-gray-700" />
</th>
<th className="p-2 text-center font-medium sticky top-0 bg-white dark:bg-gray-900 z-10">
<Skeleton className="h-8 w-20 mx-auto dark:bg-gray-700" />
</th>
<th className="p-2 text-center font-medium sticky top-0 bg-white dark:bg-gray-900 z-10">
<Skeleton className="h-8 w-20 mx-auto dark:bg-gray-700" />
</th>
<th className="p-2 text-center font-medium sticky top-0 bg-white dark:bg-gray-900 z-10">
<Skeleton className="h-8 w-20 mx-auto dark:bg-gray-700" />
</th>
<th className="p-2 text-center font-medium sticky top-0 bg-white dark:bg-gray-900 z-10">
<Skeleton className="h-8 w-20 mx-auto dark:bg-gray-700" />
</th>
</tr>
</thead>
<tbody className="divide-y divide-gray-200 dark:divide-gray-800">
{[...Array(15)].map((_, i) => (
<tr key={i} className="hover:bg-gray-50 dark:hover:bg-gray-800">
<td className="p-2">
<div className="flex items-center gap-2">
<Skeleton className="h-4 w-4 dark:bg-gray-700" />
<div className="space-y-2">
<Skeleton className="h-4 w-48 dark:bg-gray-700" />
<Skeleton className="h-3 w-64 dark:bg-gray-700" />
<Skeleton className="h-3 w-32 dark:bg-gray-700" />
</div>
</div>
</td>
<td className="p-2 text-center">
<div className="flex flex-col items-center gap-1">
<Skeleton className="h-4 w-16 dark:bg-gray-700" />
<Skeleton className="h-3 w-24 dark:bg-gray-700" />
</div>
</td>
<td className="p-2 text-center">
<div className="flex flex-col items-center gap-1">
<Skeleton className="h-4 w-16 dark:bg-gray-700" />
<Skeleton className="h-3 w-24 dark:bg-gray-700" />
</div>
</td>
<td className="p-2 text-center">
<div className="flex flex-col items-center gap-1">
<Skeleton className="h-4 w-16 dark:bg-gray-700" />
<Skeleton className="h-3 w-24 dark:bg-gray-700" />
</div>
</td>
<td className="p-2 text-center">
<div className="flex flex-col items-center gap-1">
<Skeleton className="h-4 w-16 dark:bg-gray-700" />
<Skeleton className="h-3 w-24 dark:bg-gray-700" />
</div>
</td>
<td className="p-2 text-center">
<div className="flex flex-col items-center gap-1">
<Skeleton className="h-4 w-16 dark:bg-gray-700" />
<Skeleton className="h-3 w-24 dark:bg-gray-700" />
</div>
</td>
</tr>
))}
</tbody>
</table>
);
// Error alert component
@@ -167,10 +230,21 @@ const KlaviyoCampaigns = ({ className }) => {
if (isLoading) {
return (
<Card className="h-full bg-white dark:bg-gray-900">
<CardHeader>
<div className="h-6 w-48 bg-gray-200 dark:bg-gray-700 animate-pulse rounded" />
<CardHeader className="pb-2">
<div className="flex justify-between items-center">
<CardTitle className="text-xl font-semibold text-gray-900 dark:text-gray-100">
<Skeleton className="h-6 w-48 dark:bg-gray-700" />
</CardTitle>
<div className="flex gap-2">
<div className="flex ml-1 gap-1 items-center">
<Skeleton className="h-8 w-20 dark:bg-gray-700" />
<Skeleton className="h-8 w-20 dark:bg-gray-700" />
</div>
<Skeleton className="h-8 w-[130px] dark:bg-gray-700" />
</div>
</div>
</CardHeader>
<CardContent className="overflow-y-auto pl-4 max-h-[350px]">
<CardContent className="overflow-y-auto pl-4 max-h-[400px] mb-4">
<TableSkeleton />
</CardContent>
</Card>

View File

@@ -28,6 +28,7 @@ import {
Hash,
} from "lucide-react";
import { Button } from "@/components/ui/button";
import { Skeleton } from "@/components/ui/skeleton";
// Helper functions for formatting
const formatCurrency = (value, decimalPlaces = 2) =>
@@ -250,6 +251,63 @@ const processCampaignData = (campaign) => {
};
};
const SkeletonMetricCard = () => (
<Card className="h-full">
<CardContent className="pt-6 h-full">
<div className="flex justify-between items-start">
<div className="flex-1 min-w-0">
<Skeleton className="h-4 w-24 mb-4 dark:bg-gray-700" />
<div className="flex items-baseline gap-2">
<Skeleton className="h-8 w-20 dark:bg-gray-700" />
</div>
</div>
<Skeleton className="h-5 w-5 rounded-full dark:bg-gray-700" />
</div>
</CardContent>
</Card>
);
const SkeletonTable = () => (
<div className="grid overflow-x-auto">
<div className="overflow-y-auto max-h-[400px]">
<table className="min-w-full">
<thead>
<tr className="border-b border-gray-200 dark:border-gray-800">
<th className="p-2 sticky top-0 bg-white dark:bg-gray-900 z-10">
<Skeleton className="h-4 w-32 dark:bg-gray-700" />
</th>
{[...Array(8)].map((_, i) => (
<th key={i} className="p-2 text-center sticky top-0 bg-white dark:bg-gray-900 z-10">
<Skeleton className="h-4 w-20 mx-auto dark:bg-gray-700" />
</th>
))}
</tr>
</thead>
<tbody className="divide-y divide-gray-200 dark:divide-gray-800">
{[...Array(5)].map((_, rowIndex) => (
<tr key={rowIndex}>
<td className="p-2">
<div className="space-y-2">
<Skeleton className="h-4 w-48 dark:bg-gray-700" />
<Skeleton className="h-3 w-24 dark:bg-gray-700" />
</div>
</td>
{[...Array(8)].map((_, colIndex) => (
<td key={colIndex} className="p-2 text-center">
<div className="space-y-1">
<Skeleton className="h-4 w-16 mx-auto dark:bg-gray-700" />
<Skeleton className="h-3 w-12 mx-auto dark:bg-gray-700" />
</div>
</td>
))}
</tr>
))}
</tbody>
</table>
</div>
</div>
);
const MetaCampaigns = () => {
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
@@ -392,8 +450,28 @@ const MetaCampaigns = () => {
if (loading) {
return (
<Card className="bg-white dark:bg-gray-900">
<CardContent className="h-[400px] flex items-center justify-center">
<Loader2 className="h-8 w-8 animate-spin text-gray-400 dark:text-gray-500" />
<CardHeader className="pb-2">
<div className="flex justify-between items-start mb-6">
<CardTitle className="text-xl font-semibold text-gray-900 dark:text-gray-100">
Meta Ads Performance
</CardTitle>
<Select disabled value="7">
<SelectTrigger className="w-[130px] bg-white dark:bg-gray-800">
<SelectValue placeholder="Select range" />
</SelectTrigger>
<SelectContent>
<SelectItem value="7">Last 7 days</SelectItem>
</SelectContent>
</Select>
</div>
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 xl:grid-cols-6 gap-4">
{[...Array(12)].map((_, i) => (
<SkeletonMetricCard key={i} />
))}
</div>
</CardHeader>
<CardContent className="p-4">
<SkeletonTable />
</CardContent>
</Card>
);

View File

@@ -100,59 +100,75 @@ const ProductGrid = ({
);
const SkeletonProduct = () => (
<tr>
<tr className="hover:bg-muted/50 transition-colors">
<td className="p-1 align-middle w-[50px]">
<Skeleton className="h-[50px] w-[50px] rounded" />
<Skeleton className="h-[50px] w-[50px] rounded bg-muted" />
</td>
<td className="p-1 align-middle min-w-[200px]">
<div className="flex flex-col gap-1">
<Skeleton className="h-4 w-[130px]" />
<Skeleton className="h-3 w-[120px]" />
<div className="flex flex-col gap-1.5">
<Skeleton className="h-4 w-[180px] bg-muted" />
<Skeleton className="h-3 w-[140px] bg-muted" />
</div>
</td>
<td className="p-1 align-middle text-center">
<Skeleton className="h-4 w-8 mx-auto" />
<Skeleton className="h-4 w-8 mx-auto bg-muted" />
</td>
<td className="p-1 align-middle text-center">
<Skeleton className="h-4 w-16 mx-auto" />
<Skeleton className="h-4 w-16 mx-auto bg-muted" />
</td>
<td className="p-1 align-middle text-center">
<Skeleton className="h-4 w-8 mx-auto" />
<Skeleton className="h-4 w-8 mx-auto bg-muted" />
</td>
</tr>
);
const LoadingState = () => (
<div className="rounded-md border h-full">
<div className="h-full">
<div className="overflow-y-auto h-full">
<table className="w-full">
<thead>
<tr className="hover:bg-transparent">
<th className="p-1.5 text-left font-medium sticky top-0 bg-white dark:bg-background z-10 w-[50px] min-w-[50px] border-b" />
<th className="p-1.5 text-left font-medium sticky top-0 bg-white dark:bg-background z-10 min-w-[200px] border-b">
<div className="inline-flex items-center justify-start w-full px-2 py-1 text-sm font-medium">
<Skeleton className="h-4 w-16" />
</div>
<th className="p-1.5 text-left font-medium sticky top-0 bg-white dark:bg-background z-10 w-[50px] min-w-[50px] border-b dark:border-gray-800" />
<th className="p-1.5 text-left font-medium sticky top-0 bg-white dark:bg-background z-10 min-w-[200px] border-b dark:border-gray-800">
<Button
variant="ghost"
className="w-full p-2 justify-start h-8 pointer-events-none"
disabled
>
<Skeleton className="h-4 w-16 bg-muted" />
</Button>
</th>
<th className="p-1.5 font-medium text-center sticky top-0 bg-white dark:bg-background z-10 border-b">
<div className="inline-flex items-center justify-center w-full px-2 py-1 text-sm font-medium">
<Skeleton className="h-4 w-8" />
</div>
<th className="p-1.5 font-medium text-center sticky top-0 bg-white dark:bg-background z-10 border-b dark:border-gray-800">
<Button
variant="ghost"
className="w-full p-2 justify-center h-8 pointer-events-none"
disabled
>
<Skeleton className="h-4 w-12 bg-muted" />
</Button>
</th>
<th className="p-1.5 font-medium text-center sticky top-0 bg-white dark:bg-background z-10 border-b">
<div className="inline-flex items-center justify-center w-full px-2 py-1 text-sm font-medium">
<Skeleton className="h-4 w-8" />
</div>
<th className="p-1.5 font-medium text-center sticky top-0 bg-white dark:bg-background z-10 border-b dark:border-gray-800">
<Button
variant="ghost"
className="w-full p-2 justify-center h-8 pointer-events-none"
disabled
>
<Skeleton className="h-4 w-12 bg-muted" />
</Button>
</th>
<th className="p-1.5 font-medium text-center sticky top-0 bg-white dark:bg-background z-10 border-b">
<div className="inline-flex items-center justify-center w-full px-2 py-1 text-sm font-medium">
<Skeleton className="h-4 w-12" />
</div>
<th className="p-1.5 font-medium text-center sticky top-0 bg-white dark:bg-background z-10 border-b dark:border-gray-800">
<Button
variant="ghost"
className="w-full p-2 justify-center h-8 pointer-events-none"
disabled
>
<Skeleton className="h-4 w-16 bg-muted" />
</Button>
</th>
</tr>
</thead>
<tbody className="divide-y divide-gray-200 dark:divide-gray-800">
{[...Array(10)].map((_, i) => (
{[...Array(20)].map((_, i) => (
<SkeletonProduct key={i} />
))}
</tbody>
@@ -161,6 +177,39 @@ const ProductGrid = ({
</div>
);
if (loading) {
return (
<Card className="flex flex-col h-full bg-white dark:bg-gray-900/60 backdrop-blur-sm">
<CardHeader className="p-6 pb-4">
<div className="flex flex-col gap-4">
<div className="flex justify-between items-start">
<div>
<CardTitle className="text-xl font-semibold text-gray-900 dark:text-gray-100">
<Skeleton className="h-6 w-32 bg-muted" />
</CardTitle>
{description && (
<CardDescription className="mt-1">
<Skeleton className="h-4 w-48 bg-muted" />
</CardDescription>
)}
</div>
<div className="flex items-center gap-2">
<Skeleton className="h-9 w-9 bg-muted" />
<Skeleton className="h-9 w-[130px] bg-muted" />
</div>
</div>
</div>
</CardHeader>
<CardContent className="p-6 pt-0 flex-1 overflow-hidden -mt-1">
<div className="h-full">
<LoadingState />
</div>
</CardContent>
</Card>
);
}
return (
<Card className="flex flex-col h-full bg-white dark:bg-gray-900/60 backdrop-blur-sm">
<CardHeader className="p-6 pb-4">
@@ -230,9 +279,7 @@ const ProductGrid = ({
<CardContent className="p-6 pt-0 flex-1 overflow-hidden -mt-1">
<div className="h-full">
{loading ? (
<LoadingState />
) : error ? (
{error ? (
<Alert variant="destructive" >
<AlertCircle className="h-4 w-4" />
<AlertTitle>Error</AlertTitle>
@@ -247,13 +294,13 @@ const ProductGrid = ({
<p className="text-sm text-muted-foreground">Try selecting a different time range</p>
</div>
) : (
<div className="rounded-md border h-full">
<div className="h-full">
<div className="overflow-y-auto h-full">
<table className="w-full">
<thead>
<tr className="hover:bg-transparent">
<th className="p-1 text-left font-medium sticky top-0 bg-white dark:bg-background z-10 h-[50px] min-h-[50px] w-[50px] min-w-[35px] border-b" />
<th className="p-1 text-left font-medium sticky top-0 bg-white dark:bg-background z-10 border-b">
<th className="p-1 text-left font-medium sticky top-0 bg-white dark:bg-background z-10 h-[50px] min-h-[50px] w-[50px] min-w-[35px] border-b dark:border-gray-800" />
<th className="p-1 text-left font-medium sticky top-0 bg-white dark:bg-background z-10 border-b dark:border-gray-800">
<Button
variant={sorting.column === "name" ? "default" : "ghost"}
onClick={() => handleSort("name")}
@@ -262,7 +309,7 @@ const ProductGrid = ({
Product
</Button>
</th>
<th className="p-1 font-medium text-center sticky top-0 bg-white dark:bg-background z-10 border-b">
<th className="p-1 font-medium text-center sticky top-0 bg-white dark:bg-background z-10 border-b dark:border-gray-800">
<Button
variant={sorting.column === "totalQuantity" ? "default" : "ghost"}
onClick={() => handleSort("totalQuantity")}
@@ -271,7 +318,7 @@ const ProductGrid = ({
Sold
</Button>
</th>
<th className="p-1 font-medium text-center sticky top-0 bg-white dark:bg-background z-10 border-b">
<th className="p-1 font-medium text-center sticky top-0 bg-white dark:bg-background z-10 border-b dark:border-gray-800">
<Button
variant={sorting.column === "totalRevenue" ? "default" : "ghost"}
onClick={() => handleSort("totalRevenue")}
@@ -280,7 +327,7 @@ const ProductGrid = ({
Rev
</Button>
</th>
<th className="p-1 font-medium text-center sticky top-0 bg-white dark:bg-background z-10 border-b">
<th className="p-1 font-medium text-center sticky top-0 bg-white dark:bg-background z-10 border-b dark:border-gray-800">
<Button
variant={sorting.column === "orderCount" ? "default" : "ghost"}
onClick={() => handleSort("orderCount")}

View File

@@ -30,6 +30,7 @@ import {
TableCell,
} from "@/components/ui/table";
import { format } from "date-fns";
import { Skeleton } from "@/components/ui/skeleton";
const METRIC_COLORS = {
activeUsers: {
@@ -179,6 +180,86 @@ const QuotaInfo = ({ tokenQuota }) => {
);
};
const SkeletonSummaryCard = () => (
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 px-4 py-2">
<Skeleton className="h-4 w-24 bg-muted" />
</CardHeader>
<CardContent className="px-4 pt-0 pb-2">
<Skeleton className="h-8 w-20 mb-1 bg-muted" />
<Skeleton className="h-4 w-32 bg-muted" />
</CardContent>
</Card>
);
const SkeletonBarChart = () => (
<div className="h-[235px] bg-card rounded-lg p-4">
<div className="h-full relative">
{/* Grid lines */}
{[...Array(5)].map((_, i) => (
<div
key={i}
className="absolute w-full h-px bg-muted"
style={{ top: `${(i + 1) * 20}%` }}
/>
))}
{/* Y-axis labels */}
<div className="absolute left-0 top-0 bottom-0 w-8 flex flex-col justify-between py-4">
{[...Array(5)].map((_, i) => (
<Skeleton key={i} className="h-3 w-6 bg-muted" />
))}
</div>
{/* X-axis labels */}
<div className="absolute left-8 right-4 bottom-0 flex justify-between">
{[...Array(6)].map((_, i) => (
<Skeleton key={i} className="h-3 w-8 bg-muted" />
))}
</div>
{/* Bars */}
<div className="absolute inset-x-8 bottom-6 top-4 flex items-end justify-between">
{[...Array(30)].map((_, i) => (
<div
key={i}
className="w-1.5 bg-muted"
style={{
height: `${Math.random() * 80 + 10}%`,
}}
/>
))}
</div>
</div>
</div>
);
const SkeletonTable = () => (
<div className="space-y-2 h-[230px] overflow-y-auto">
<Table>
<TableHeader>
<TableRow className="dark:border-gray-800">
<TableHead>
<Skeleton className="h-4 w-32 bg-muted" />
</TableHead>
<TableHead className="text-right">
<Skeleton className="h-4 w-24 ml-auto bg-muted" />
</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{[...Array(8)].map((_, i) => (
<TableRow key={i} className="dark:border-gray-800">
<TableCell>
<Skeleton className="h-4 w-48 bg-muted" />
</TableCell>
<TableCell className="text-right">
<Skeleton className="h-4 w-12 ml-auto bg-muted" />
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div>
);
export const RealtimeAnalytics = () => {
const [basicData, setBasicData] = useState({
last30MinUsers: 0,
@@ -353,9 +434,30 @@ export const RealtimeAnalytics = () => {
if (loading && !basicData && !detailedData) {
return (
<Card className="bg-white dark:bg-gray-900">
<CardContent className="h-96 flex items-center justify-center">
<Loader2 className="h-8 w-8 animate-spin text-gray-400 dark:text-gray-500" />
<Card className="w-full bg-white dark:bg-gray-900/60 backdrop-blur-sm h-full">
<CardHeader className="p-6 pb-2">
<div className="flex justify-between items-center">
<CardTitle className="text-xl font-semibold text-gray-900 dark:text-gray-100">
<Skeleton className="h-6 w-48 bg-muted" />
</CardTitle>
<Skeleton className="h-4 w-32 bg-muted" />
</div>
</CardHeader>
<CardContent className="p-6 pt-0">
<div className="grid grid-cols-2 gap-4 mt-1 mb-3">
<SkeletonSummaryCard />
<SkeletonSummaryCard />
</div>
<div className="space-y-4">
<div className="flex gap-2">
{[...Array(3)].map((_, i) => (
<Skeleton key={i} className="h-8 w-20 bg-muted" />
))}
</div>
<SkeletonBarChart />
</div>
</CardContent>
</Card>
);

View File

@@ -502,22 +502,35 @@ SummaryStats.displayName = "SummaryStats";
// Add these skeleton components near the top of the file
const SkeletonChart = () => (
<div className="h-[400px] w-full bg-gray-50 dark:bg-gray-800 rounded-lg p-4">
<div className="h-[400px] w-full bg-card rounded-lg p-4">
<div className="h-full flex flex-col">
<div className="flex-1 relative">
<div className="h-full w-full relative">
{[...Array(5)].map((_, i) => (
{/* Grid lines */}
{[...Array(6)].map((_, i) => (
<div
key={i}
className="absolute w-full h-px bg-gray-200 dark:bg-gray-700"
style={{ top: `${20 + i * 20}%` }}
className="absolute w-full h-px bg-muted"
style={{ top: `${(i + 1) * 16}%` }}
/>
))}
{/* Y-axis labels */}
<div className="absolute left-0 top-0 bottom-0 w-16 flex flex-col justify-between py-4">
{[...Array(6)].map((_, i) => (
<Skeleton key={i} className="h-4 w-12 bg-muted" />
))}
</div>
{/* X-axis labels */}
<div className="absolute left-16 right-0 bottom-0 flex justify-between px-4">
{[...Array(7)].map((_, i) => (
<Skeleton key={i} className="h-4 w-12 bg-muted" />
))}
</div>
{/* Chart line */}
<div className="absolute inset-0 mt-8 mb-8 ml-20 mr-4">
<div
className="absolute inset-0 bg-gray-300 dark:bg-gray-600 animate-pulse"
className="absolute inset-0 bg-muted/50"
style={{
opacity: 0.2,
clipPath: "polygon(0 50%, 100% 20%, 100% 100%, 0 100%)",
clipPath: "polygon(0 50%, 20% 20%, 40% 40%, 60% 30%, 80% 60%, 100% 40%, 100% 100%, 0 100%)",
}}
/>
</div>
@@ -527,16 +540,16 @@ const SkeletonChart = () => (
);
const SkeletonStats = () => (
<div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4">
<div className="grid grid-cols-2 lg:grid-cols-4 gap-4 py-4">
{[...Array(4)].map((_, i) => (
<Card key={i}>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<Skeleton className="h-4 w-24" />
<Skeleton className="h-4 w-4 rounded-full" />
<CardHeader className="flex flex-row items-center justify-between space-y-0 p-4 pb-2">
<Skeleton className="h-4 w-24 bg-muted" />
<Skeleton className="h-4 w-16 bg-muted" />
</CardHeader>
<CardContent>
<Skeleton className="h-8 w-32 mb-2" />
<Skeleton className="h-4 w-24" />
<CardContent className="p-4 pt-0">
<Skeleton className="h-7 w-28 mb-1 bg-muted" />
<Skeleton className="h-4 w-20 bg-muted" />
</CardContent>
</Card>
))}
@@ -544,25 +557,45 @@ const SkeletonStats = () => (
);
const SkeletonTable = () => (
<div className="mt-4 overflow-x-auto rounded-lg border bg-card">
<div className="rounded-lg border bg-card">
<Table>
<TableHeader>
<TableRow>
{[...Array(8)].map((_, i) => (
<TableHead key={i}>
<Skeleton className="h-4 w-20" />
<TableRow className="hover:bg-transparent">
<TableHead className="w-[120px]">
<Skeleton className="h-4 w-16 bg-muted" />
</TableHead>
<TableHead className="text-center">
<Skeleton className="h-4 w-20 mx-auto bg-muted" />
</TableHead>
<TableHead className="text-center">
<Skeleton className="h-4 w-24 mx-auto bg-muted" />
</TableHead>
<TableHead className="text-center">
<Skeleton className="h-4 w-16 mx-auto bg-muted" />
</TableHead>
<TableHead className="text-center">
<Skeleton className="h-4 w-24 mx-auto bg-muted" />
</TableHead>
))}
</TableRow>
</TableHeader>
<TableBody>
{[...Array(5)].map((_, i) => (
<TableRow key={i}>
{[...Array(8)].map((_, j) => (
<TableCell key={j}>
<Skeleton className="h-4 w-16" />
{[...Array(10)].map((_, i) => (
<TableRow key={i} className="hover:bg-muted/50">
<TableCell>
<Skeleton className="h-4 w-20 bg-muted" />
</TableCell>
<TableCell className="text-center">
<Skeleton className="h-4 w-12 mx-auto bg-muted" />
</TableCell>
<TableCell className="text-center">
<Skeleton className="h-4 w-20 mx-auto bg-muted" />
</TableCell>
<TableCell className="text-center">
<Skeleton className="h-4 w-16 mx-auto bg-muted" />
</TableCell>
<TableCell className="text-center">
<Skeleton className="h-4 w-20 mx-auto bg-muted" />
</TableCell>
))}
</TableRow>
))}
</TableBody>

View File

@@ -1094,69 +1094,108 @@ const useDebouncedEffect = (effect, deps, delay) => {
// Add these skeleton components near the top of the file
const SkeletonCard = () => (
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<Skeleton className="h-4 w-24" />
<Skeleton className="h-4 w-4 rounded-full" />
<Card className="relative">
<CardHeader className="flex flex-row items-center justify-between space-y-0 p-4 pb-2">
<div className="flex items-center gap-2">
<Skeleton className="h-4 w-24 bg-muted" />
<Skeleton className="h-4 w-4 rounded-full bg-muted" />
</div>
<Skeleton className="h-4 w-4 rounded-full bg-muted" />
</CardHeader>
<CardContent>
<Skeleton className="h-8 w-32 mb-2" />
<Skeleton className="h-4 w-24" />
<CardContent className="p-4 pt-0">
<div className="space-y-2">
<Skeleton className="h-8 w-24 bg-muted" />
<div className="flex items-center gap-2">
<Skeleton className="h-4 w-32 bg-muted" />
<Skeleton className="h-4 w-16 bg-muted" />
</div>
</div>
</CardContent>
</Card>
);
const SkeletonChart = ({ type = "line" }) => (
<div className="h-[400px] w-full bg-gray-50 dark:bg-gray-800 rounded-lg p-4">
<div className="h-full flex flex-col">
<div className="flex-1 relative">
<div className="h-[400px] w-full bg-card rounded-lg p-4">
<div className="h-full relative">
{/* Grid lines */}
{[...Array(5)].map((_, i) => (
<div
key={i}
className="absolute w-full h-px bg-muted"
style={{ top: `${(i + 1) * 20}%` }}
/>
))}
{/* Y-axis labels */}
<div className="absolute left-0 top-0 bottom-0 w-8 flex flex-col justify-between py-4">
{[...Array(5)].map((_, i) => (
<Skeleton key={i} className="h-3 w-6 bg-muted" />
))}
</div>
{/* X-axis labels */}
<div className="absolute left-8 right-4 bottom-0 flex justify-between">
{[...Array(6)].map((_, i) => (
<Skeleton key={i} className="h-3 w-8 bg-muted" />
))}
</div>
{type === "bar" ? (
<div className="h-full flex items-end justify-between gap-1">
<div className="absolute inset-x-8 bottom-6 top-4 flex items-end justify-between">
{[...Array(24)].map((_, i) => (
<div
key={i}
className="w-full bg-gray-200 dark:bg-gray-700 rounded-t animate-pulse"
style={{ height: `${15 + Math.random() * 70}%` }}
className="w-2 bg-muted"
style={{ height: `${Math.random() * 80 + 10}%` }}
/>
))}
</div>
) : (
<div className="absolute inset-x-8 bottom-6 top-4">
<div className="h-full w-full relative">
{[...Array(5)].map((_, i) => (
<div
key={i}
className="absolute w-full h-px bg-gray-200 dark:bg-gray-700"
style={{ top: `${20 + i * 20}%` }}
/>
))}
<div
className="absolute inset-0 bg-gray-300 dark:bg-gray-600 animate-pulse"
className="absolute inset-0 bg-muted"
style={{
opacity: 0.2,
opacity: 0.5,
clipPath: "polygon(0 50%, 100% 20%, 100% 100%, 0 100%)",
}}
/>
</div>
)}
</div>
)}
</div>
</div>
);
const SkeletonTable = ({ rows = 5 }) => (
<div className="space-y-2">
<div className="grid grid-cols-3 gap-4 pb-2">
{[...Array(3)].map((_, i) => (
<Skeleton key={i} className="h-5 w-full" />
))}
</div>
const SkeletonTable = ({ rows = 8 }) => (
<div className="rounded-lg border bg-card">
<Table>
<TableHeader>
<TableRow className="dark:border-gray-800">
<TableHead>
<Skeleton className="h-4 w-32 bg-muted" />
</TableHead>
<TableHead className="text-right">
<Skeleton className="h-4 w-24 ml-auto bg-muted" />
</TableHead>
<TableHead className="text-right">
<Skeleton className="h-4 w-24 ml-auto bg-muted" />
</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{[...Array(rows)].map((_, i) => (
<div key={i} className="grid grid-cols-3 gap-4 py-2">
{[...Array(3)].map((_, j) => (
<Skeleton key={j} className="h-4 w-full" />
))}
</div>
<TableRow key={i} className="dark:border-gray-800">
<TableCell>
<Skeleton className="h-4 w-48 bg-muted" />
</TableCell>
<TableCell className="text-right">
<Skeleton className="h-4 w-16 ml-auto bg-muted" />
</TableCell>
<TableCell className="text-right">
<Skeleton className="h-4 w-16 ml-auto bg-muted" />
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div>
);
@@ -1696,18 +1735,24 @@ const StatCards = ({
<div className="flex flex-col space-y-2">
<div className="flex justify-between items-start">
<div>
<Skeleton className="h-6 w-48 mb-2" />
<Skeleton className="h-4 w-64" />
<CardTitle className="text-xl font-semibold text-gray-900 dark:text-gray-100">
{title}
</CardTitle>
{description && (
<CardDescription className="mt-1">
{description}
</CardDescription>
)}
</div>
<div className="flex items-center gap-4">
<Skeleton className="h-4 w-32" />
<Skeleton className="h-9 w-[130px]" />
<Skeleton className="h-4 w-32 bg-muted" />
<Skeleton className="h-9 w-[130px] bg-muted rounded-md" />
</div>
</div>
</div>
</CardHeader>
<CardContent className="p-6 pt-0">
<div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4">
<div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-3 lg:grid-cols-4 xl:grid-cols-3 2xl:grid-cols-4 gap-2 md:gap-3">
{[...Array(12)].map((_, i) => (
<SkeletonCard key={i} />
))}

View File

@@ -25,6 +25,59 @@ import {
Legend,
} from "recharts";
import { Loader2 } from "lucide-react";
import { Skeleton } from "@/components/ui/skeleton";
// Add skeleton components
const SkeletonTable = ({ rows = 12 }) => (
<div className="h-full max-h-[540px] overflow-y-auto scrollbar-thin scrollbar-thumb-gray-200 dark:scrollbar-thumb-gray-700 scrollbar-track-transparent hover:scrollbar-thumb-gray-300 dark:hover:scrollbar-thumb-gray-600 pr-2">
<Table>
<TableHeader>
<TableRow className="dark:border-gray-800">
<TableHead><Skeleton className="h-4 w-48 dark:bg-gray-700" /></TableHead>
<TableHead className="text-right"><Skeleton className="h-4 w-16 ml-auto dark:bg-gray-700" /></TableHead>
<TableHead className="text-right"><Skeleton className="h-4 w-16 ml-auto dark:bg-gray-700" /></TableHead>
<TableHead className="text-right"><Skeleton className="h-4 w-16 ml-auto dark:bg-gray-700" /></TableHead>
</TableRow>
</TableHeader>
<TableBody>
{[...Array(rows)].map((_, i) => (
<TableRow key={i} className="dark:border-gray-800">
<TableCell className="py-3"><Skeleton className="h-4 w-64 dark:bg-gray-700" /></TableCell>
<TableCell className="text-right py-3"><Skeleton className="h-4 w-12 ml-auto dark:bg-gray-700" /></TableCell>
<TableCell className="text-right py-3"><Skeleton className="h-4 w-12 ml-auto dark:bg-gray-700" /></TableCell>
<TableCell className="text-right py-3"><Skeleton className="h-4 w-16 ml-auto dark:bg-gray-700" /></TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div>
);
const SkeletonPieChart = () => (
<div className="h-60 relative">
<div className="absolute inset-0 flex items-center justify-center">
<div className="w-40 h-40 rounded-full bg-gray-100 dark:bg-gray-800 animate-pulse" />
</div>
<div className="absolute bottom-0 left-1/2 transform -translate-x-1/2 flex gap-4">
{[...Array(3)].map((_, i) => (
<div key={i} className="flex items-center gap-2">
<Skeleton className="h-3 w-3 rounded-full dark:bg-gray-700" />
<Skeleton className="h-4 w-16 dark:bg-gray-700" />
</div>
))}
</div>
</div>
);
const SkeletonTabs = () => (
<div className="space-y-2">
<div className="flex gap-2 mb-4">
{[...Array(3)].map((_, i) => (
<Skeleton key={i} className="h-8 w-24 dark:bg-gray-700" />
))}
</div>
</div>
);
export const UserBehaviorDashboard = () => {
const [data, setData] = useState(null);
@@ -141,9 +194,35 @@ export const UserBehaviorDashboard = () => {
if (loading) {
return (
<Card className="bg-white dark:bg-gray-900">
<CardContent className="h-96 flex items-center justify-center">
<Loader2 className="h-8 w-8 animate-spin text-gray-400 dark:text-gray-500" />
<Card className="bg-white dark:bg-gray-900 h-full">
<CardHeader>
<div className="flex justify-between items-start">
<CardTitle className="text-xl font-semibold text-gray-900 dark:text-gray-100">
User Behavior Analysis
</CardTitle>
<Skeleton className="h-9 w-36 dark:bg-gray-700" />
</div>
</CardHeader>
<CardContent>
<Tabs defaultValue="pages" className="w-full">
<TabsList className="mb-4">
<TabsTrigger value="pages" disabled>Top Pages</TabsTrigger>
<TabsTrigger value="sources" disabled>Traffic Sources</TabsTrigger>
<TabsTrigger value="devices" disabled>Device Usage</TabsTrigger>
</TabsList>
<TabsContent value="pages" className="mt-4 space-y-2">
<SkeletonTable rows={15} />
</TabsContent>
<TabsContent value="sources" className="mt-4 space-y-2">
<SkeletonTable rows={12} />
</TabsContent>
<TabsContent value="devices" className="mt-4 space-y-2">
<SkeletonPieChart />
</TabsContent>
</Tabs>
</CardContent>
</Card>
);