Add and standardize skeletons
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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")}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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} />
|
||||
))}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user