Add and standardize skeletons

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

View File

@@ -165,6 +165,81 @@ const AgentPerformanceTable = ({ agents, onSort }) => {
); );
}; };
const SkeletonMetricCard = () => (
<Card className="bg-white dark:bg-gray-900/60 backdrop-blur-sm">
<CardHeader className="flex flex-col items-start p-4">
<Skeleton className="h-4 w-24 mb-2" />
<Skeleton className="h-8 w-32 mb-2" />
<div className="flex gap-4">
<Skeleton className="h-4 w-20" />
<Skeleton className="h-4 w-20" />
</div>
</CardHeader>
</Card>
);
const SkeletonChart = ({ type = "line" }) => (
<div className="h-[300px] w-full bg-gray-50 dark:bg-gray-800 rounded-lg p-4">
<div className="h-full flex flex-col">
<div className="flex-1 relative">
{type === "bar" ? (
<div className="h-full flex items-end justify-between gap-1">
{[...Array(24)].map((_, i) => (
<div
key={i}
className="w-full bg-gray-200 dark:bg-gray-700 rounded-t animate-pulse"
style={{ height: `${15 + Math.random() * 70}%` }}
/>
))}
</div>
) : (
<div className="h-full w-full relative">
{[...Array(5)].map((_, i) => (
<div
key={i}
className="absolute w-full h-px bg-gray-200 dark:bg-gray-700"
style={{ top: `${20 + i * 20}%` }}
/>
))}
<div
className="absolute inset-0 bg-gray-300 dark:bg-gray-600 animate-pulse"
style={{
opacity: 0.2,
clipPath: "polygon(0 50%, 100% 20%, 100% 100%, 0 100%)",
}}
/>
</div>
)}
</div>
</div>
</div>
);
const SkeletonTable = ({ rows = 5 }) => (
<Table>
<TableHeader>
<TableRow className="hover:bg-transparent">
<TableHead><Skeleton className="h-4 w-24" /></TableHead>
<TableHead><Skeleton className="h-4 w-24" /></TableHead>
<TableHead><Skeleton className="h-4 w-24" /></TableHead>
<TableHead><Skeleton className="h-4 w-24" /></TableHead>
<TableHead><Skeleton className="h-4 w-24" /></TableHead>
</TableRow>
</TableHeader>
<TableBody>
{[...Array(rows)].map((_, i) => (
<TableRow key={i} className="hover:bg-gray-50 dark:hover:bg-gray-800/50">
<TableCell><Skeleton className="h-4 w-32" /></TableCell>
<TableCell><Skeleton className="h-4 w-16" /></TableCell>
<TableCell><Skeleton className="h-4 w-16" /></TableCell>
<TableCell><Skeleton className="h-4 w-16" /></TableCell>
<TableCell><Skeleton className="h-4 w-24" /></TableCell>
</TableRow>
))}
</TableBody>
</Table>
);
const AircallDashboard = () => { const 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>

View File

@@ -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>

View File

@@ -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

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>
); );

View File

@@ -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")}

View File

@@ -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>
); );

View File

@@ -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>

View File

@@ -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} />
))} ))}

View File

@@ -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>
); );