Introduces reusable error boundary and suspense timeout components across dashboard pages for better error handling and user feedback. Enhances loading skeletons with subtle progress indicators, animation, and slow-loading warnings. All dynamic imports now include error handling and improved fallback skeletons, and a shared DashboardContentWrapper is added for consistent dashboard content loading experience.
185 lines
6.0 KiB
TypeScript
185 lines
6.0 KiB
TypeScript
import Dashboard from "@/components/dashboard/dashboard";
|
|
import { fetchServer } from '@/lib/api';
|
|
import { performance } from 'perf_hooks';
|
|
import { Info, GitCommit, User, Zap } from 'lucide-react';
|
|
import packageJson from '../../package.json';
|
|
import { getGitInfo, getShortGitHash } from '@/lib/utils/git';
|
|
import { Suspense } from 'react';
|
|
import dynamic from 'next/dynamic';
|
|
import { Skeleton } from '@/components/ui/skeleton';
|
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
|
|
import DashboardContentWrapper from './dashboard-content-wrapper';
|
|
|
|
// Loading skeleton for the dashboard content
|
|
function DashboardContentSkeleton() {
|
|
return (
|
|
<div
|
|
className="space-y-6 animate-in fade-in duration-500 relative"
|
|
role="status"
|
|
aria-label="Loading dashboard content"
|
|
aria-live="polite"
|
|
>
|
|
{/* Subtle loading indicator */}
|
|
<div className="absolute top-0 left-0 right-0 h-1 bg-muted overflow-hidden rounded-full">
|
|
<div className="h-full bg-primary w-1/3"
|
|
style={{
|
|
background: 'linear-gradient(90deg, transparent, hsl(var(--primary)), transparent)',
|
|
backgroundSize: '200% 100%',
|
|
animation: 'shimmer 2s ease-in-out infinite',
|
|
}}
|
|
/>
|
|
</div>
|
|
|
|
{/* Header skeleton */}
|
|
<div>
|
|
<Skeleton className="h-8 w-64 mb-2" />
|
|
<Skeleton className="h-4 w-96 max-w-full" />
|
|
</div>
|
|
|
|
{/* Stats cards skeleton */}
|
|
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4 lg:gap-6">
|
|
{[...Array(4)].map((_, i) => (
|
|
<Card
|
|
key={i}
|
|
className="animate-in fade-in slide-in-from-bottom-4"
|
|
style={{
|
|
animationDelay: `${i * 75}ms`,
|
|
animationDuration: '400ms',
|
|
animationFillMode: 'both',
|
|
}}
|
|
>
|
|
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
|
<Skeleton className="h-4 w-20" />
|
|
<Skeleton className="h-5 w-5 rounded" />
|
|
</CardHeader>
|
|
<CardContent>
|
|
<Skeleton className="h-7 w-16 mb-1" />
|
|
<Skeleton className="h-3 w-24" />
|
|
</CardContent>
|
|
</Card>
|
|
))}
|
|
</div>
|
|
|
|
{/* Best selling products skeleton */}
|
|
<Card className="animate-in fade-in slide-in-from-bottom-4"
|
|
style={{
|
|
animationDelay: '300ms',
|
|
animationDuration: '400ms',
|
|
animationFillMode: 'both',
|
|
}}
|
|
>
|
|
<CardHeader>
|
|
<Skeleton className="h-6 w-48" />
|
|
<Skeleton className="h-4 w-72 max-w-full" />
|
|
</CardHeader>
|
|
<CardContent>
|
|
<div className="space-y-4">
|
|
{[...Array(5)].map((_, i) => (
|
|
<div
|
|
key={i}
|
|
className="flex items-center gap-4 animate-in fade-in"
|
|
style={{
|
|
animationDelay: `${400 + i * 50}ms`,
|
|
animationDuration: '300ms',
|
|
animationFillMode: 'both',
|
|
}}
|
|
>
|
|
<Skeleton className="h-12 w-12 rounded-md" />
|
|
<div className="space-y-2">
|
|
<Skeleton className="h-4 w-40" />
|
|
<Skeleton className="h-4 w-20" />
|
|
</div>
|
|
<div className="ml-auto text-right">
|
|
<Skeleton className="h-4 w-16 ml-auto" />
|
|
<Skeleton className="h-4 w-16 ml-auto mt-2" />
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
// Lazy load the Content component with error handling
|
|
const Content = dynamic(() => import("@/components/dashboard/content").catch((err) => {
|
|
console.error("Failed to load dashboard content:", err);
|
|
throw err;
|
|
}), {
|
|
loading: () => <DashboardContentSkeleton />
|
|
});
|
|
|
|
// ✅ Corrected Vendor Type
|
|
interface Vendor {
|
|
_id: string;
|
|
username: string;
|
|
storeId: string;
|
|
pgpKey: string;
|
|
__v: number;
|
|
}
|
|
|
|
interface User {
|
|
vendor: Vendor;
|
|
}
|
|
|
|
interface OrderStats {
|
|
totalOrders: number;
|
|
pendingOrders: number;
|
|
completedOrders: number;
|
|
cancelledOrders: number;
|
|
}
|
|
|
|
export default async function DashboardPage() {
|
|
const startTime = performance.now();
|
|
|
|
const [userResponse, orderStats] = await Promise.all([
|
|
fetchServer<User>("/auth/me"),
|
|
fetchServer<OrderStats>("/orders/stats")
|
|
]);
|
|
|
|
// Get git info using the new utility
|
|
const gitInfo = getGitInfo();
|
|
|
|
const endTime = performance.now();
|
|
const generationTime = (endTime - startTime).toFixed(2);
|
|
const panelVersion = packageJson.version;
|
|
const commitHash = gitInfo.hash;
|
|
|
|
const vendor = userResponse.vendor;
|
|
|
|
return (
|
|
<Dashboard>
|
|
<DashboardContentWrapper>
|
|
<Suspense fallback={<DashboardContentSkeleton />}>
|
|
<Content username={vendor.username} orderStats={orderStats} />
|
|
</Suspense>
|
|
</DashboardContentWrapper>
|
|
<div className="fixed bottom-2 right-2 text-xs text-muted-foreground bg-background/80 backdrop-blur-sm px-2 py-1 rounded border border-border/50 z-50 flex items-center space-x-2">
|
|
<div className="flex items-center gap-1">
|
|
<Info size={12} className="text-muted-foreground/80" />
|
|
<span>v{panelVersion}</span>
|
|
</div>
|
|
|
|
<div className="flex items-center gap-1">
|
|
<User size={12} className="text-muted-foreground/80" />
|
|
<span>{vendor.username}</span>
|
|
</div>
|
|
|
|
<div className="flex items-center gap-1">
|
|
<GitCommit size={12} className="text-muted-foreground/80" />
|
|
<span>{commitHash}</span>
|
|
</div>
|
|
|
|
<div className="flex items-center gap-1">
|
|
<Zap size={12} className="text-amber-500" />
|
|
<span>Generated in {generationTime}ms</span>
|
|
</div>
|
|
|
|
<span className="px-1.5 py-0.5 text-xs rounded-sm bg-emerald-500/20 text-emerald-300">
|
|
{process.env.NODE_ENV || 'development'}
|
|
</span>
|
|
</div>
|
|
</Dashboard>
|
|
);
|
|
} |