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.
197 lines
6.3 KiB
TypeScript
197 lines
6.3 KiB
TypeScript
"use client";
|
|
|
|
import { useEffect, Suspense, Component, ReactNode, useState } from "react";
|
|
import { useRouter } from "next/navigation";
|
|
import Dashboard from "@/components/dashboard/dashboard";
|
|
import { Package, AlertCircle, RefreshCw } from "lucide-react";
|
|
import dynamic from "next/dynamic";
|
|
import { Skeleton } from "@/components/ui/skeleton";
|
|
import { Card, CardContent, CardHeader } from "@/components/ui/card";
|
|
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
|
|
import { Button } from "@/components/ui/button";
|
|
|
|
// Error Boundary Component
|
|
interface ErrorBoundaryState {
|
|
hasError: boolean;
|
|
error: Error | null;
|
|
}
|
|
|
|
interface ErrorBoundaryProps {
|
|
children: ReactNode;
|
|
componentName?: string;
|
|
}
|
|
|
|
class ErrorBoundary extends Component<ErrorBoundaryProps, ErrorBoundaryState> {
|
|
private retryCount = 0;
|
|
private maxRetries = 2;
|
|
|
|
constructor(props: ErrorBoundaryProps) {
|
|
super(props);
|
|
this.state = { hasError: false, error: null };
|
|
}
|
|
|
|
static getDerivedStateFromError(error: Error): ErrorBoundaryState {
|
|
return { hasError: true, error };
|
|
}
|
|
|
|
componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
|
|
console.error(`Error loading ${this.props.componentName || 'component'}:`, error, errorInfo);
|
|
}
|
|
|
|
handleRetry = () => {
|
|
if (this.retryCount < this.maxRetries) {
|
|
this.retryCount++;
|
|
this.setState({ hasError: false, error: null });
|
|
} else {
|
|
window.location.reload();
|
|
}
|
|
};
|
|
|
|
render() {
|
|
if (this.state.hasError) {
|
|
return (
|
|
<Alert variant="destructive" className="animate-in fade-in duration-300">
|
|
<AlertCircle className="h-4 w-4" />
|
|
<AlertTitle>Failed to load {this.props.componentName || 'component'}</AlertTitle>
|
|
<AlertDescription className="mt-2">
|
|
<p className="mb-3">
|
|
{this.state.error?.message || 'An unexpected error occurred while loading this component.'}
|
|
</p>
|
|
{this.retryCount < this.maxRetries && (
|
|
<p className="text-xs text-muted-foreground mb-3">
|
|
Retry attempt {this.retryCount + 1} of {this.maxRetries + 1}
|
|
</p>
|
|
)}
|
|
<div className="flex gap-2">
|
|
<Button
|
|
variant="outline"
|
|
size="sm"
|
|
onClick={this.handleRetry}
|
|
>
|
|
<RefreshCw className="h-3 w-3 mr-2" />
|
|
{this.retryCount < this.maxRetries ? 'Try Again' : 'Reload Page'}
|
|
</Button>
|
|
</div>
|
|
</AlertDescription>
|
|
</Alert>
|
|
);
|
|
}
|
|
|
|
return this.props.children;
|
|
}
|
|
}
|
|
|
|
// Lazy load the OrderTable component with error handling
|
|
const OrderTable = dynamic(() => import("@/components/tables/order-table").catch((err) => {
|
|
console.error("Failed to load OrderTable:", err);
|
|
throw err;
|
|
}), {
|
|
loading: () => <OrderTableSkeleton />
|
|
});
|
|
|
|
// Loading skeleton for the order table
|
|
function OrderTableSkeleton() {
|
|
return (
|
|
<Card className="animate-in fade-in duration-500 relative">
|
|
{/* Subtle loading indicator */}
|
|
<div className="absolute top-0 left-0 right-0 h-1 bg-muted overflow-hidden rounded-t-lg">
|
|
<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>
|
|
|
|
<CardHeader>
|
|
<div className="flex items-center justify-between">
|
|
<Skeleton className="h-6 w-32" />
|
|
<div className="flex gap-2">
|
|
<Skeleton className="h-9 w-24 rounded-md" />
|
|
<Skeleton className="h-9 w-32 rounded-md" />
|
|
</div>
|
|
</div>
|
|
</CardHeader>
|
|
<CardContent>
|
|
{/* Table header skeleton */}
|
|
<div className="border rounded-lg">
|
|
<div className="border-b p-4">
|
|
<div className="flex items-center gap-4">
|
|
{['Order ID', 'Customer', 'Status', 'Total', 'Date', 'Actions'].map((header, i) => (
|
|
<Skeleton
|
|
key={i}
|
|
className="h-4 w-20 flex-1 animate-in fade-in"
|
|
style={{
|
|
animationDelay: `${i * 50}ms`,
|
|
animationDuration: '300ms',
|
|
animationFillMode: 'both',
|
|
}}
|
|
/>
|
|
))}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Table rows skeleton */}
|
|
{[...Array(8)].map((_, i) => (
|
|
<div
|
|
key={i}
|
|
className="border-b last:border-b-0 p-4 animate-in fade-in"
|
|
style={{
|
|
animationDelay: `${300 + i * 50}ms`,
|
|
animationDuration: '300ms',
|
|
animationFillMode: 'both',
|
|
}}
|
|
>
|
|
<div className="flex items-center gap-4">
|
|
<Skeleton className="h-4 w-24 flex-1" />
|
|
<Skeleton className="h-4 w-32 flex-1" />
|
|
<Skeleton className="h-6 w-20 flex-1 rounded-full" />
|
|
<Skeleton className="h-4 w-16 flex-1" />
|
|
<Skeleton className="h-4 w-20 flex-1" />
|
|
<div className="flex gap-2 flex-1">
|
|
<Skeleton className="h-8 w-16 rounded-md" />
|
|
<Skeleton className="h-8 w-16 rounded-md" />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
);
|
|
}
|
|
|
|
export default function OrdersPage() {
|
|
const router = useRouter();
|
|
|
|
useEffect(() => {
|
|
const authToken = document.cookie
|
|
.split("; ")
|
|
.find((row) => row.startsWith("Authorization="))
|
|
?.split("=")[1];
|
|
|
|
if (!authToken) {
|
|
router.push("/login");
|
|
}
|
|
}, [router]);
|
|
|
|
return (
|
|
<Dashboard>
|
|
<div className="space-y-6">
|
|
<div className="flex items-center justify-between">
|
|
<h1 className="text-2xl font-semibold text-gray-900 dark:text-white flex items-center">
|
|
<Package className="mr-2 h-6 w-6" />
|
|
Orders
|
|
</h1>
|
|
</div>
|
|
|
|
<ErrorBoundary componentName="Order Table">
|
|
<Suspense fallback={<OrderTableSkeleton />}>
|
|
<OrderTable />
|
|
</Suspense>
|
|
</ErrorBoundary>
|
|
</div>
|
|
</Dashboard>
|
|
);
|
|
} |