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 AircallDashboard = () => {
|
||||||
const [timeRange, setTimeRange] = useState("last7days");
|
const [timeRange, setTimeRange] = useState("last7days");
|
||||||
const [metrics, setMetrics] = useState(null);
|
const [metrics, setMetrics] = useState(null);
|
||||||
@@ -316,7 +391,7 @@ const AircallDashboard = () => {
|
|||||||
<div className="grid grid-cols-2 lg:grid-cols-4 gap-4">
|
<div className="grid grid-cols-2 lg:grid-cols-4 gap-4">
|
||||||
{isLoading ? (
|
{isLoading ? (
|
||||||
[...Array(4)].map((_, i) => (
|
[...Array(4)].map((_, i) => (
|
||||||
<Skeleton key={i} className="h-32 rounded-lg" />
|
<SkeletonMetricCard key={i} />
|
||||||
))
|
))
|
||||||
) : metrics ? (
|
) : metrics ? (
|
||||||
<>
|
<>
|
||||||
@@ -407,6 +482,9 @@ const AircallDashboard = () => {
|
|||||||
<CardTitle className="text-lg font-semibold text-gray-900 dark:text-gray-100">Daily Call Volume</CardTitle>
|
<CardTitle className="text-lg font-semibold text-gray-900 dark:text-gray-100">Daily Call Volume</CardTitle>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="h-[300px]">
|
<CardContent className="h-[300px]">
|
||||||
|
{isLoading ? (
|
||||||
|
<SkeletonChart type="bar" />
|
||||||
|
) : (
|
||||||
<ResponsiveContainer width="100%" height="100%">
|
<ResponsiveContainer width="100%" height="100%">
|
||||||
<BarChart data={chartData.daily} margin={{ top: 0, right: 5, left: -35, bottom: 0 }}>
|
<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" />
|
<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" />
|
<Bar dataKey="outbound" fill={COLORS.outbound} name="Outbound" />
|
||||||
</BarChart>
|
</BarChart>
|
||||||
</ResponsiveContainer>
|
</ResponsiveContainer>
|
||||||
|
)}
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
@@ -434,6 +513,9 @@ const AircallDashboard = () => {
|
|||||||
<CardTitle className="text-lg font-semibold text-gray-900 dark:text-gray-100">Hourly Distribution</CardTitle>
|
<CardTitle className="text-lg font-semibold text-gray-900 dark:text-gray-100">Hourly Distribution</CardTitle>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="h-[300px]">
|
<CardContent className="h-[300px]">
|
||||||
|
{isLoading ? (
|
||||||
|
<SkeletonChart type="bar" />
|
||||||
|
) : (
|
||||||
<ResponsiveContainer width="100%" height="100%">
|
<ResponsiveContainer width="100%" height="100%">
|
||||||
<BarChart data={chartData.hourly} margin={{ top: 0, right: 5, left: -35, bottom: 0 }}>
|
<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" />
|
<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" />
|
<Bar dataKey="calls" fill={COLORS.hourly} name="Calls" />
|
||||||
</BarChart>
|
</BarChart>
|
||||||
</ResponsiveContainer>
|
</ResponsiveContainer>
|
||||||
|
)}
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
@@ -463,10 +546,14 @@ const AircallDashboard = () => {
|
|||||||
<CardTitle className="text-lg font-semibold text-gray-900 dark:text-gray-100">Agent Performance</CardTitle>
|
<CardTitle className="text-lg font-semibold text-gray-900 dark:text-gray-100">Agent Performance</CardTitle>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
|
{isLoading ? (
|
||||||
|
<SkeletonTable rows={5} />
|
||||||
|
) : (
|
||||||
<AgentPerformanceTable
|
<AgentPerformanceTable
|
||||||
agents={sortedAgents}
|
agents={sortedAgents}
|
||||||
onSort={(key, direction) => setAgentSort({ key, direction })}
|
onSort={(key, direction) => setAgentSort({ key, direction })}
|
||||||
/>
|
/>
|
||||||
|
)}
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
@@ -476,6 +563,9 @@ const AircallDashboard = () => {
|
|||||||
<CardTitle className="text-lg font-semibold text-gray-900 dark:text-gray-100">Missed Call Reasons</CardTitle>
|
<CardTitle className="text-lg font-semibold text-gray-900 dark:text-gray-100">Missed Call Reasons</CardTitle>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
|
{isLoading ? (
|
||||||
|
<SkeletonTable rows={5} />
|
||||||
|
) : (
|
||||||
<Table>
|
<Table>
|
||||||
<TableHeader>
|
<TableHeader>
|
||||||
<TableRow className="hover:bg-transparent">
|
<TableRow className="hover:bg-transparent">
|
||||||
@@ -496,6 +586,7 @@ const AircallDashboard = () => {
|
|||||||
))}
|
))}
|
||||||
</TableBody>
|
</TableBody>
|
||||||
</Table>
|
</Table>
|
||||||
|
)}
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -56,29 +56,50 @@ const SkeletonChart = () => (
|
|||||||
clipPath: "polygon(0 50%, 100% 20%, 100% 100%, 0 100%)",
|
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>
|
||||||
</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>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
const SkeletonStats = () => (
|
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) => (
|
{[...Array(4)].map((_, i) => (
|
||||||
<Card key={i}>
|
<Card key={i}>
|
||||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
<CardHeader className="flex flex-row items-center justify-between space-y-0 p-4 pb-2">
|
||||||
<Skeleton className="h-4 w-24" />
|
<Skeleton className="h-4 w-24 dark:bg-gray-700" />
|
||||||
<Skeleton className="h-4 w-4 rounded-full" />
|
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent className="p-4 pt-0">
|
||||||
<Skeleton className="h-8 w-32 mb-2" />
|
<Skeleton className="h-8 w-32 mb-2 dark:bg-gray-700" />
|
||||||
<Skeleton className="h-4 w-24" />
|
<Skeleton className="h-4 w-24 dark:bg-gray-700" />
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
))}
|
))}
|
||||||
</div>
|
</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
|
// Add StatCard component
|
||||||
const StatCard = ({
|
const StatCard = ({
|
||||||
title,
|
title,
|
||||||
@@ -264,6 +285,9 @@ export const AnalyticsDashboard = () => {
|
|||||||
<CardTitle className="text-xl font-semibold text-gray-900 dark:text-gray-100">
|
<CardTitle className="text-xl font-semibold text-gray-900 dark:text-gray-100">
|
||||||
Analytics Overview
|
Analytics Overview
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
|
{loading ? (
|
||||||
|
<Skeleton className="h-9 w-[130px] dark:bg-gray-700" />
|
||||||
|
) : (
|
||||||
<Select value={timeRange} onValueChange={setTimeRange}>
|
<Select value={timeRange} onValueChange={setTimeRange}>
|
||||||
<SelectTrigger className="w-[130px] h-9">
|
<SelectTrigger className="w-[130px] h-9">
|
||||||
<SelectValue placeholder="Select range" />
|
<SelectValue placeholder="Select range" />
|
||||||
@@ -275,6 +299,7 @@ export const AnalyticsDashboard = () => {
|
|||||||
<SelectItem value="90">Last 90 days</SelectItem>
|
<SelectItem value="90">Last 90 days</SelectItem>
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{loading ? (
|
{loading ? (
|
||||||
@@ -317,6 +342,9 @@ export const AnalyticsDashboard = () => {
|
|||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
<div className="flex flex-col sm:flex-row gap-0 sm:gap-4 pt-2">
|
<div className="flex flex-col sm:flex-row gap-0 sm:gap-4 pt-2">
|
||||||
|
{loading ? (
|
||||||
|
<SkeletonButtons />
|
||||||
|
) : (
|
||||||
<div className="flex flex-wrap gap-1">
|
<div className="flex flex-wrap gap-1">
|
||||||
<Button
|
<Button
|
||||||
variant={metrics.activeUsers ? "default" : "outline"}
|
variant={metrics.activeUsers ? "default" : "outline"}
|
||||||
@@ -371,6 +399,7 @@ export const AnalyticsDashboard = () => {
|
|||||||
<span className="sm:hidden">Conversions</span>
|
<span className="sm:hidden">Conversions</span>
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
|
|||||||
@@ -142,19 +142,34 @@ const formatShipMethodSimple = (method) => {
|
|||||||
|
|
||||||
// Loading State Component
|
// Loading State Component
|
||||||
const LoadingState = () => (
|
const LoadingState = () => (
|
||||||
<div className="flex items-center justify-center p-8">
|
<div className="divide-y divide-gray-100 dark:divide-gray-800">
|
||||||
<div className="space-y-4 w-full">
|
{[...Array(8)].map((_, i) => (
|
||||||
{[...Array(5)].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 key={i} className="flex items-center space-x-4">
|
<div className="shrink-0">
|
||||||
<div className="h-12 w-12 rounded-full bg-gray-200 dark:bg-gray-800 animate-pulse" />
|
<Skeleton className="h-10 w-10 rounded-full bg-muted" />
|
||||||
<div className="space-y-2 flex-1">
|
</div>
|
||||||
<div className="h-4 w-[250px] bg-gray-200 dark:bg-gray-800 animate-pulse rounded" />
|
<div className="flex-1 min-w-0">
|
||||||
<div className="h-4 w-[200px] bg-gray-200 dark:bg-gray-800 animate-pulse rounded" />
|
<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>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
|
|
||||||
// Empty State Component
|
// Empty State Component
|
||||||
|
|||||||
@@ -111,10 +111,17 @@ const MetricCard = ({
|
|||||||
<CardContent className="pt-6 h-full">
|
<CardContent className="pt-6 h-full">
|
||||||
<div className="flex justify-between items-start">
|
<div className="flex justify-between items-start">
|
||||||
<div className="flex-1 min-w-0">
|
<div className="flex-1 min-w-0">
|
||||||
<p className="text-sm font-medium text-muted-foreground">{title}</p>
|
|
||||||
{loading ? (
|
{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">
|
<div className="flex items-baseline gap-2">
|
||||||
<p className="text-2xl font-bold">
|
<p className="text-2xl font-bold">
|
||||||
{typeof value === "number"
|
{typeof value === "number"
|
||||||
@@ -134,9 +141,10 @@ const MetricCard = ({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
{Icon && (
|
{!loading && Icon && (
|
||||||
<Icon className={`h-5 w-5 flex-shrink-0 ml-2 ${colorClass === "blue" ? "text-blue-500" :
|
<Icon className={`h-5 w-5 flex-shrink-0 ml-2 ${colorClass === "blue" ? "text-blue-500" :
|
||||||
colorClass === "green" ? "text-green-500" :
|
colorClass === "green" ? "text-green-500" :
|
||||||
colorClass === "purple" ? "text-purple-500" :
|
colorClass === "purple" ? "text-purple-500" :
|
||||||
@@ -146,19 +154,53 @@ const MetricCard = ({
|
|||||||
colorClass === "cyan" ? "text-cyan-500" :
|
colorClass === "cyan" ? "text-cyan-500" :
|
||||||
"text-blue-500"}`} />
|
"text-blue-500"}`} />
|
||||||
)}
|
)}
|
||||||
|
{loading && (
|
||||||
|
<Skeleton className="h-5 w-5 rounded-full dark:bg-gray-700" />
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const TableSkeleton = () => (
|
const SkeletonMetricCard = () => (
|
||||||
<div className="space-y-2">
|
<Card className="h-full">
|
||||||
<Skeleton className="h-8 w-full dark:bg-gray-700" />
|
<CardContent className="pt-6 h-full">
|
||||||
<Skeleton className="h-8 w-full dark:bg-gray-700" />
|
<div className="flex justify-between items-start">
|
||||||
<Skeleton className="h-8 w-full dark:bg-gray-700" />
|
<div className="flex-1 min-w-0">
|
||||||
<Skeleton className="h-8 w-full 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>
|
</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 = () => {
|
const GorgiasOverview = () => {
|
||||||
@@ -324,6 +366,12 @@ const GorgiasOverview = () => {
|
|||||||
<CardContent className="space-y-4">
|
<CardContent className="space-y-4">
|
||||||
<div className="grid grid-cols-2 lg:grid-cols-4 gap-4">
|
<div className="grid grid-cols-2 lg:grid-cols-4 gap-4">
|
||||||
{/* Message & Response Metrics */}
|
{/* Message & Response Metrics */}
|
||||||
|
{loading ? (
|
||||||
|
[...Array(8)].map((_, i) => (
|
||||||
|
<SkeletonMetricCard key={i} />
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
<div className="h-full">
|
<div className="h-full">
|
||||||
<MetricCard
|
<MetricCard
|
||||||
title="Messages Received"
|
title="Messages Received"
|
||||||
@@ -366,6 +414,8 @@ const GorgiasOverview = () => {
|
|||||||
loading={loading}
|
loading={loading}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Satisfaction & Efficiency */}
|
{/* Satisfaction & Efficiency */}
|
||||||
<div className="h-full">
|
<div className="h-full">
|
||||||
@@ -423,9 +473,7 @@ const GorgiasOverview = () => {
|
|||||||
</h3>
|
</h3>
|
||||||
</div>
|
</div>
|
||||||
{loading ? (
|
{loading ? (
|
||||||
<div className="p-4">
|
|
||||||
<TableSkeleton />
|
<TableSkeleton />
|
||||||
</div>
|
|
||||||
) : (
|
) : (
|
||||||
<Table>
|
<Table>
|
||||||
<TableHeader>
|
<TableHeader>
|
||||||
@@ -487,9 +535,7 @@ const GorgiasOverview = () => {
|
|||||||
</h3>
|
</h3>
|
||||||
</div>
|
</div>
|
||||||
{loading ? (
|
{loading ? (
|
||||||
<div className="p-4">
|
|
||||||
<TableSkeleton />
|
<TableSkeleton />
|
||||||
</div>
|
|
||||||
) : (
|
) : (
|
||||||
<Table>
|
<Table>
|
||||||
<TableHeader>
|
<TableHeader>
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import {
|
|||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { TIME_RANGES } from "@/lib/constants";
|
import { TIME_RANGES } from "@/lib/constants";
|
||||||
import { Mail, MessageSquare, ArrowUpDown } from "lucide-react";
|
import { Mail, MessageSquare, ArrowUpDown } from "lucide-react";
|
||||||
|
import { Skeleton } from "@/components/ui/skeleton";
|
||||||
|
|
||||||
// Helper functions for formatting
|
// Helper functions for formatting
|
||||||
const formatRate = (value, isSMS = false, hideForSMS = false) => {
|
const formatRate = (value, isSMS = false, hideForSMS = false) => {
|
||||||
@@ -37,14 +38,76 @@ const formatCurrency = (value) => {
|
|||||||
|
|
||||||
// Loading skeleton component
|
// Loading skeleton component
|
||||||
const TableSkeleton = () => (
|
const TableSkeleton = () => (
|
||||||
<div className="space-y-4">
|
<table className="w-full">
|
||||||
{[...Array(5)].map((_, i) => (
|
<thead>
|
||||||
<div
|
<tr>
|
||||||
key={i}
|
<th className="p-2 text-left font-medium sticky top-0 bg-white dark:bg-gray-900 z-10">
|
||||||
className="h-16 bg-gray-100 dark:bg-gray-800 animate-pulse rounded"
|
<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>
|
||||||
|
</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
|
// Error alert component
|
||||||
@@ -167,10 +230,21 @@ const KlaviyoCampaigns = ({ className }) => {
|
|||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
return (
|
return (
|
||||||
<Card className="h-full bg-white dark:bg-gray-900">
|
<Card className="h-full bg-white dark:bg-gray-900">
|
||||||
<CardHeader>
|
<CardHeader className="pb-2">
|
||||||
<div className="h-6 w-48 bg-gray-200 dark:bg-gray-700 animate-pulse rounded" />
|
<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>
|
</CardHeader>
|
||||||
<CardContent className="overflow-y-auto pl-4 max-h-[350px]">
|
<CardContent className="overflow-y-auto pl-4 max-h-[400px] mb-4">
|
||||||
<TableSkeleton />
|
<TableSkeleton />
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ import {
|
|||||||
Hash,
|
Hash,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Skeleton } from "@/components/ui/skeleton";
|
||||||
|
|
||||||
// Helper functions for formatting
|
// Helper functions for formatting
|
||||||
const formatCurrency = (value, decimalPlaces = 2) =>
|
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 MetaCampaigns = () => {
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [error, setError] = useState(null);
|
const [error, setError] = useState(null);
|
||||||
@@ -392,8 +450,28 @@ const MetaCampaigns = () => {
|
|||||||
if (loading) {
|
if (loading) {
|
||||||
return (
|
return (
|
||||||
<Card className="bg-white dark:bg-gray-900">
|
<Card className="bg-white dark:bg-gray-900">
|
||||||
<CardContent className="h-[400px] flex items-center justify-center">
|
<CardHeader className="pb-2">
|
||||||
<Loader2 className="h-8 w-8 animate-spin text-gray-400 dark:text-gray-500" />
|
<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>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -100,59 +100,75 @@ const ProductGrid = ({
|
|||||||
);
|
);
|
||||||
|
|
||||||
const SkeletonProduct = () => (
|
const SkeletonProduct = () => (
|
||||||
<tr>
|
<tr className="hover:bg-muted/50 transition-colors">
|
||||||
<td className="p-1 align-middle w-[50px]">
|
<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>
|
||||||
<td className="p-1 align-middle min-w-[200px]">
|
<td className="p-1 align-middle min-w-[200px]">
|
||||||
<div className="flex flex-col gap-1">
|
<div className="flex flex-col gap-1.5">
|
||||||
<Skeleton className="h-4 w-[130px]" />
|
<Skeleton className="h-4 w-[180px] bg-muted" />
|
||||||
<Skeleton className="h-3 w-[120px]" />
|
<Skeleton className="h-3 w-[140px] bg-muted" />
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td className="p-1 align-middle text-center">
|
<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>
|
||||||
<td className="p-1 align-middle text-center">
|
<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>
|
||||||
<td className="p-1 align-middle text-center">
|
<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>
|
||||||
</tr>
|
</tr>
|
||||||
);
|
);
|
||||||
|
|
||||||
const LoadingState = () => (
|
const LoadingState = () => (
|
||||||
<div className="rounded-md border h-full">
|
<div className="h-full">
|
||||||
<div className="overflow-y-auto h-full">
|
<div className="overflow-y-auto h-full">
|
||||||
<table className="w-full">
|
<table className="w-full">
|
||||||
<thead>
|
<thead>
|
||||||
<tr className="hover:bg-transparent">
|
<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 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">
|
<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">
|
||||||
<div className="inline-flex items-center justify-start w-full px-2 py-1 text-sm font-medium">
|
<Button
|
||||||
<Skeleton className="h-4 w-16" />
|
variant="ghost"
|
||||||
</div>
|
className="w-full p-2 justify-start h-8 pointer-events-none"
|
||||||
|
disabled
|
||||||
|
>
|
||||||
|
<Skeleton className="h-4 w-16 bg-muted" />
|
||||||
|
</Button>
|
||||||
</th>
|
</th>
|
||||||
<th className="p-1.5 font-medium text-center sticky top-0 bg-white dark:bg-background z-10 border-b">
|
<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">
|
||||||
<div className="inline-flex items-center justify-center w-full px-2 py-1 text-sm font-medium">
|
<Button
|
||||||
<Skeleton className="h-4 w-8" />
|
variant="ghost"
|
||||||
</div>
|
className="w-full p-2 justify-center h-8 pointer-events-none"
|
||||||
|
disabled
|
||||||
|
>
|
||||||
|
<Skeleton className="h-4 w-12 bg-muted" />
|
||||||
|
</Button>
|
||||||
</th>
|
</th>
|
||||||
<th className="p-1.5 font-medium text-center sticky top-0 bg-white dark:bg-background z-10 border-b">
|
<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">
|
||||||
<div className="inline-flex items-center justify-center w-full px-2 py-1 text-sm font-medium">
|
<Button
|
||||||
<Skeleton className="h-4 w-8" />
|
variant="ghost"
|
||||||
</div>
|
className="w-full p-2 justify-center h-8 pointer-events-none"
|
||||||
|
disabled
|
||||||
|
>
|
||||||
|
<Skeleton className="h-4 w-12 bg-muted" />
|
||||||
|
</Button>
|
||||||
</th>
|
</th>
|
||||||
<th className="p-1.5 font-medium text-center sticky top-0 bg-white dark:bg-background z-10 border-b">
|
<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">
|
||||||
<div className="inline-flex items-center justify-center w-full px-2 py-1 text-sm font-medium">
|
<Button
|
||||||
<Skeleton className="h-4 w-12" />
|
variant="ghost"
|
||||||
</div>
|
className="w-full p-2 justify-center h-8 pointer-events-none"
|
||||||
|
disabled
|
||||||
|
>
|
||||||
|
<Skeleton className="h-4 w-16 bg-muted" />
|
||||||
|
</Button>
|
||||||
</th>
|
</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody className="divide-y divide-gray-200 dark:divide-gray-800">
|
<tbody className="divide-y divide-gray-200 dark:divide-gray-800">
|
||||||
{[...Array(10)].map((_, i) => (
|
{[...Array(20)].map((_, i) => (
|
||||||
<SkeletonProduct key={i} />
|
<SkeletonProduct key={i} />
|
||||||
))}
|
))}
|
||||||
</tbody>
|
</tbody>
|
||||||
@@ -161,6 +177,39 @@ const ProductGrid = ({
|
|||||||
</div>
|
</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 (
|
return (
|
||||||
<Card className="flex flex-col h-full bg-white dark:bg-gray-900/60 backdrop-blur-sm">
|
<Card className="flex flex-col h-full bg-white dark:bg-gray-900/60 backdrop-blur-sm">
|
||||||
<CardHeader className="p-6 pb-4">
|
<CardHeader className="p-6 pb-4">
|
||||||
@@ -230,9 +279,7 @@ const ProductGrid = ({
|
|||||||
|
|
||||||
<CardContent className="p-6 pt-0 flex-1 overflow-hidden -mt-1">
|
<CardContent className="p-6 pt-0 flex-1 overflow-hidden -mt-1">
|
||||||
<div className="h-full">
|
<div className="h-full">
|
||||||
{loading ? (
|
{error ? (
|
||||||
<LoadingState />
|
|
||||||
) : error ? (
|
|
||||||
<Alert variant="destructive" >
|
<Alert variant="destructive" >
|
||||||
<AlertCircle className="h-4 w-4" />
|
<AlertCircle className="h-4 w-4" />
|
||||||
<AlertTitle>Error</AlertTitle>
|
<AlertTitle>Error</AlertTitle>
|
||||||
@@ -247,13 +294,13 @@ const ProductGrid = ({
|
|||||||
<p className="text-sm text-muted-foreground">Try selecting a different time range</p>
|
<p className="text-sm text-muted-foreground">Try selecting a different time range</p>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="rounded-md border h-full">
|
<div className="h-full">
|
||||||
<div className="overflow-y-auto h-full">
|
<div className="overflow-y-auto h-full">
|
||||||
<table className="w-full">
|
<table className="w-full">
|
||||||
<thead>
|
<thead>
|
||||||
<tr className="hover:bg-transparent">
|
<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 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">
|
<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
|
<Button
|
||||||
variant={sorting.column === "name" ? "default" : "ghost"}
|
variant={sorting.column === "name" ? "default" : "ghost"}
|
||||||
onClick={() => handleSort("name")}
|
onClick={() => handleSort("name")}
|
||||||
@@ -262,7 +309,7 @@ const ProductGrid = ({
|
|||||||
Product
|
Product
|
||||||
</Button>
|
</Button>
|
||||||
</th>
|
</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
|
<Button
|
||||||
variant={sorting.column === "totalQuantity" ? "default" : "ghost"}
|
variant={sorting.column === "totalQuantity" ? "default" : "ghost"}
|
||||||
onClick={() => handleSort("totalQuantity")}
|
onClick={() => handleSort("totalQuantity")}
|
||||||
@@ -271,7 +318,7 @@ const ProductGrid = ({
|
|||||||
Sold
|
Sold
|
||||||
</Button>
|
</Button>
|
||||||
</th>
|
</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
|
<Button
|
||||||
variant={sorting.column === "totalRevenue" ? "default" : "ghost"}
|
variant={sorting.column === "totalRevenue" ? "default" : "ghost"}
|
||||||
onClick={() => handleSort("totalRevenue")}
|
onClick={() => handleSort("totalRevenue")}
|
||||||
@@ -280,7 +327,7 @@ const ProductGrid = ({
|
|||||||
Rev
|
Rev
|
||||||
</Button>
|
</Button>
|
||||||
</th>
|
</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
|
<Button
|
||||||
variant={sorting.column === "orderCount" ? "default" : "ghost"}
|
variant={sorting.column === "orderCount" ? "default" : "ghost"}
|
||||||
onClick={() => handleSort("orderCount")}
|
onClick={() => handleSort("orderCount")}
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ import {
|
|||||||
TableCell,
|
TableCell,
|
||||||
} from "@/components/ui/table";
|
} from "@/components/ui/table";
|
||||||
import { format } from "date-fns";
|
import { format } from "date-fns";
|
||||||
|
import { Skeleton } from "@/components/ui/skeleton";
|
||||||
|
|
||||||
const METRIC_COLORS = {
|
const METRIC_COLORS = {
|
||||||
activeUsers: {
|
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 = () => {
|
export const RealtimeAnalytics = () => {
|
||||||
const [basicData, setBasicData] = useState({
|
const [basicData, setBasicData] = useState({
|
||||||
last30MinUsers: 0,
|
last30MinUsers: 0,
|
||||||
@@ -353,9 +434,30 @@ export const RealtimeAnalytics = () => {
|
|||||||
|
|
||||||
if (loading && !basicData && !detailedData) {
|
if (loading && !basicData && !detailedData) {
|
||||||
return (
|
return (
|
||||||
<Card className="bg-white dark:bg-gray-900">
|
<Card className="w-full bg-white dark:bg-gray-900/60 backdrop-blur-sm h-full">
|
||||||
<CardContent className="h-96 flex items-center justify-center">
|
<CardHeader className="p-6 pb-2">
|
||||||
<Loader2 className="h-8 w-8 animate-spin text-gray-400 dark:text-gray-500" />
|
<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>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -502,22 +502,35 @@ SummaryStats.displayName = "SummaryStats";
|
|||||||
|
|
||||||
// Add these skeleton components near the top of the file
|
// Add these skeleton components near the top of the file
|
||||||
const SkeletonChart = () => (
|
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="h-full flex flex-col">
|
||||||
<div className="flex-1 relative">
|
<div className="flex-1 relative">
|
||||||
<div className="h-full w-full relative">
|
{/* Grid lines */}
|
||||||
{[...Array(5)].map((_, i) => (
|
{[...Array(6)].map((_, i) => (
|
||||||
<div
|
<div
|
||||||
key={i}
|
key={i}
|
||||||
className="absolute w-full h-px bg-gray-200 dark:bg-gray-700"
|
className="absolute w-full h-px bg-muted"
|
||||||
style={{ top: `${20 + i * 20}%` }}
|
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
|
<div
|
||||||
className="absolute inset-0 bg-gray-300 dark:bg-gray-600 animate-pulse"
|
className="absolute inset-0 bg-muted/50"
|
||||||
style={{
|
style={{
|
||||||
opacity: 0.2,
|
clipPath: "polygon(0 50%, 20% 20%, 40% 40%, 60% 30%, 80% 60%, 100% 40%, 100% 100%, 0 100%)",
|
||||||
clipPath: "polygon(0 50%, 100% 20%, 100% 100%, 0 100%)",
|
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -527,16 +540,16 @@ const SkeletonChart = () => (
|
|||||||
);
|
);
|
||||||
|
|
||||||
const SkeletonStats = () => (
|
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) => (
|
{[...Array(4)].map((_, i) => (
|
||||||
<Card key={i}>
|
<Card key={i}>
|
||||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
<CardHeader className="flex flex-row items-center justify-between space-y-0 p-4 pb-2">
|
||||||
<Skeleton className="h-4 w-24" />
|
<Skeleton className="h-4 w-24 bg-muted" />
|
||||||
<Skeleton className="h-4 w-4 rounded-full" />
|
<Skeleton className="h-4 w-16 bg-muted" />
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent className="p-4 pt-0">
|
||||||
<Skeleton className="h-8 w-32 mb-2" />
|
<Skeleton className="h-7 w-28 mb-1 bg-muted" />
|
||||||
<Skeleton className="h-4 w-24" />
|
<Skeleton className="h-4 w-20 bg-muted" />
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
))}
|
))}
|
||||||
@@ -544,25 +557,45 @@ const SkeletonStats = () => (
|
|||||||
);
|
);
|
||||||
|
|
||||||
const SkeletonTable = () => (
|
const SkeletonTable = () => (
|
||||||
<div className="mt-4 overflow-x-auto rounded-lg border bg-card">
|
<div className="rounded-lg border bg-card">
|
||||||
<Table>
|
<Table>
|
||||||
<TableHeader>
|
<TableHeader>
|
||||||
<TableRow>
|
<TableRow className="hover:bg-transparent">
|
||||||
{[...Array(8)].map((_, i) => (
|
<TableHead className="w-[120px]">
|
||||||
<TableHead key={i}>
|
<Skeleton className="h-4 w-16 bg-muted" />
|
||||||
<Skeleton className="h-4 w-20" />
|
</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>
|
</TableHead>
|
||||||
))}
|
|
||||||
</TableRow>
|
</TableRow>
|
||||||
</TableHeader>
|
</TableHeader>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
{[...Array(5)].map((_, i) => (
|
{[...Array(10)].map((_, i) => (
|
||||||
<TableRow key={i}>
|
<TableRow key={i} className="hover:bg-muted/50">
|
||||||
{[...Array(8)].map((_, j) => (
|
<TableCell>
|
||||||
<TableCell key={j}>
|
<Skeleton className="h-4 w-20 bg-muted" />
|
||||||
<Skeleton className="h-4 w-16" />
|
</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>
|
</TableCell>
|
||||||
))}
|
|
||||||
</TableRow>
|
</TableRow>
|
||||||
))}
|
))}
|
||||||
</TableBody>
|
</TableBody>
|
||||||
|
|||||||
@@ -1094,69 +1094,108 @@ const useDebouncedEffect = (effect, deps, delay) => {
|
|||||||
|
|
||||||
// Add these skeleton components near the top of the file
|
// Add these skeleton components near the top of the file
|
||||||
const SkeletonCard = () => (
|
const SkeletonCard = () => (
|
||||||
<Card>
|
<Card className="relative">
|
||||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
<CardHeader className="flex flex-row items-center justify-between space-y-0 p-4 pb-2">
|
||||||
<Skeleton className="h-4 w-24" />
|
<div className="flex items-center gap-2">
|
||||||
<Skeleton className="h-4 w-4 rounded-full" />
|
<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>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent className="p-4 pt-0">
|
||||||
<Skeleton className="h-8 w-32 mb-2" />
|
<div className="space-y-2">
|
||||||
<Skeleton className="h-4 w-24" />
|
<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>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
);
|
);
|
||||||
|
|
||||||
const SkeletonChart = ({ type = "line" }) => (
|
const SkeletonChart = ({ type = "line" }) => (
|
||||||
<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="h-full relative">
|
||||||
<div className="flex-1 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" ? (
|
{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) => (
|
{[...Array(24)].map((_, i) => (
|
||||||
<div
|
<div
|
||||||
key={i}
|
key={i}
|
||||||
className="w-full bg-gray-200 dark:bg-gray-700 rounded-t animate-pulse"
|
className="w-2 bg-muted"
|
||||||
style={{ height: `${15 + Math.random() * 70}%` }}
|
style={{ height: `${Math.random() * 80 + 10}%` }}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
|
<div className="absolute inset-x-8 bottom-6 top-4">
|
||||||
<div className="h-full w-full relative">
|
<div className="h-full w-full relative">
|
||||||
{[...Array(5)].map((_, i) => (
|
|
||||||
<div
|
<div
|
||||||
key={i}
|
className="absolute inset-0 bg-muted"
|
||||||
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={{
|
style={{
|
||||||
opacity: 0.2,
|
opacity: 0.5,
|
||||||
clipPath: "polygon(0 50%, 100% 20%, 100% 100%, 0 100%)",
|
clipPath: "polygon(0 50%, 100% 20%, 100% 100%, 0 100%)",
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
const SkeletonTable = ({ rows = 5 }) => (
|
const SkeletonTable = ({ rows = 8 }) => (
|
||||||
<div className="space-y-2">
|
<div className="rounded-lg border bg-card">
|
||||||
<div className="grid grid-cols-3 gap-4 pb-2">
|
<Table>
|
||||||
{[...Array(3)].map((_, i) => (
|
<TableHeader>
|
||||||
<Skeleton key={i} className="h-5 w-full" />
|
<TableRow className="dark:border-gray-800">
|
||||||
))}
|
<TableHead>
|
||||||
</div>
|
<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) => (
|
{[...Array(rows)].map((_, i) => (
|
||||||
<div key={i} className="grid grid-cols-3 gap-4 py-2">
|
<TableRow key={i} className="dark:border-gray-800">
|
||||||
{[...Array(3)].map((_, j) => (
|
<TableCell>
|
||||||
<Skeleton key={j} className="h-4 w-full" />
|
<Skeleton className="h-4 w-48 bg-muted" />
|
||||||
))}
|
</TableCell>
|
||||||
</div>
|
<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>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -1696,18 +1735,24 @@ const StatCards = ({
|
|||||||
<div className="flex flex-col space-y-2">
|
<div className="flex flex-col space-y-2">
|
||||||
<div className="flex justify-between items-start">
|
<div className="flex justify-between items-start">
|
||||||
<div>
|
<div>
|
||||||
<Skeleton className="h-6 w-48 mb-2" />
|
<CardTitle className="text-xl font-semibold text-gray-900 dark:text-gray-100">
|
||||||
<Skeleton className="h-4 w-64" />
|
{title}
|
||||||
|
</CardTitle>
|
||||||
|
{description && (
|
||||||
|
<CardDescription className="mt-1">
|
||||||
|
{description}
|
||||||
|
</CardDescription>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-4">
|
<div className="flex items-center gap-4">
|
||||||
<Skeleton className="h-4 w-32" />
|
<Skeleton className="h-4 w-32 bg-muted" />
|
||||||
<Skeleton className="h-9 w-[130px]" />
|
<Skeleton className="h-9 w-[130px] bg-muted rounded-md" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="p-6 pt-0">
|
<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) => (
|
{[...Array(12)].map((_, i) => (
|
||||||
<SkeletonCard key={i} />
|
<SkeletonCard key={i} />
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -25,6 +25,59 @@ import {
|
|||||||
Legend,
|
Legend,
|
||||||
} from "recharts";
|
} from "recharts";
|
||||||
import { Loader2 } from "lucide-react";
|
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 = () => {
|
export const UserBehaviorDashboard = () => {
|
||||||
const [data, setData] = useState(null);
|
const [data, setData] = useState(null);
|
||||||
@@ -141,9 +194,35 @@ export const UserBehaviorDashboard = () => {
|
|||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return (
|
return (
|
||||||
<Card className="bg-white dark:bg-gray-900">
|
<Card className="bg-white dark:bg-gray-900 h-full">
|
||||||
<CardContent className="h-96 flex items-center justify-center">
|
<CardHeader>
|
||||||
<Loader2 className="h-8 w-8 animate-spin text-gray-400 dark:text-gray-500" />
|
<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>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user