Add robust error boundaries and improved skeletons to dashboard

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.
This commit is contained in:
g
2025-12-31 05:20:44 +00:00
parent 96638f968f
commit 0062aa2dfe
10 changed files with 1166 additions and 118 deletions

View File

@@ -1,51 +1,335 @@
"use client";
export const dynamic = "force-dynamic";
import React, { Suspense, lazy, useState } from "react";
import React, { Suspense, lazy, useState, useEffect, Component, ReactNode } from "react";
import { Button } from "@/components/ui/button";
import Link from "next/link";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { Skeleton } from "@/components/ui/skeleton";
import { Card, CardContent, CardHeader } from "@/components/ui/card";
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
import { AlertCircle, RefreshCw } from "lucide-react";
// Lazy load admin components
const AdminAnalytics = lazy(() => import("@/components/admin/AdminAnalytics"));
const InviteVendorCard = lazy(() => import("@/components/admin/InviteVendorCard"));
const BanUserCard = lazy(() => import("@/components/admin/BanUserCard"));
const InvitationsListCard = lazy(() => import("@/components/admin/InvitationsListCard"));
const VendorsCard = lazy(() => import("@/components/admin/VendorsCard"));
// Error Boundary Component
interface ErrorBoundaryState {
hasError: boolean;
error: Error | null;
}
// Loading skeleton for admin components
function AdminComponentSkeleton() {
interface ErrorBoundaryProps {
children: ReactNode;
fallback?: 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);
// Log to error tracking service if available
if (typeof window !== 'undefined' && (window as any).gtag) {
(window as any).gtag('event', 'exception', {
description: `Failed to load ${this.props.componentName || 'component'}: ${error.message}`,
fatal: false,
});
}
}
handleRetry = () => {
if (this.retryCount < this.maxRetries) {
this.retryCount++;
this.setState({ hasError: false, error: null });
// Force a re-render by updating the key or reloading the component
// The lazy import will be retried when the component re-mounts
} else {
// After max retries, suggest a full page reload
window.location.reload();
}
};
render() {
if (this.state.hasError) {
if (this.props.fallback) {
return this.props.fallback;
}
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>
<Button
variant="ghost"
size="sm"
onClick={() => window.location.reload()}
>
Full Reload
</Button>
</div>
</AlertDescription>
</Alert>
);
}
return this.props.children;
}
}
// Lazy load admin components with error handling
const AdminAnalytics = lazy(() =>
import("@/components/admin/AdminAnalytics").catch((err) => {
console.error("Failed to load AdminAnalytics:", err);
throw err;
})
);
const InviteVendorCard = lazy(() =>
import("@/components/admin/InviteVendorCard").catch((err) => {
console.error("Failed to load InviteVendorCard:", err);
throw err;
})
);
const BanUserCard = lazy(() =>
import("@/components/admin/BanUserCard").catch((err) => {
console.error("Failed to load BanUserCard:", err);
throw err;
})
);
const InvitationsListCard = lazy(() =>
import("@/components/admin/InvitationsListCard").catch((err) => {
console.error("Failed to load InvitationsListCard:", err);
throw err;
})
);
const VendorsCard = lazy(() =>
import("@/components/admin/VendorsCard").catch((err) => {
console.error("Failed to load VendorsCard:", err);
throw err;
})
);
// Loading skeleton with timeout warning
function AdminComponentSkeleton({ showSlowWarning = false }: { showSlowWarning?: boolean }) {
return (
<div className="space-y-4">
<Card>
<CardHeader>
<Skeleton className="h-6 w-48" />
</CardHeader>
<CardContent>
<div className="space-y-2">
<Skeleton className="h-4 w-full" />
<Skeleton className="h-4 w-3/4" />
<Skeleton className="h-4 w-1/2" />
</div>
</CardContent>
</Card>
<div
className="space-y-6 animate-in fade-in duration-500 relative"
role="status"
aria-label="Loading analytics dashboard"
aria-live="polite"
>
{showSlowWarning && (
<Alert className="mb-4 animate-in fade-in duration-300">
<AlertCircle className="h-4 w-4" />
<AlertTitle>Taking longer than expected</AlertTitle>
<AlertDescription>
The component is still loading. This may be due to a slow connection. Please wait...
</AlertDescription>
</Alert>
)}
{/* 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 animate-[shimmer_2s_ease-in-out_infinite]"
style={{
background: 'linear-gradient(90deg, transparent, hsl(var(--primary)), transparent)',
backgroundSize: '200% 100%',
animation: 'shimmer 2s ease-in-out infinite',
}}
/>
</div>
{/* Header skeleton */}
<div className="flex justify-between items-center">
<div className="space-y-2">
<Skeleton className="h-8 w-64" />
<Skeleton className="h-4 w-96 max-w-full" />
</div>
<div className="flex items-center gap-2">
<Skeleton className="h-10 w-[140px]" />
<Skeleton className="h-10 w-10 rounded-md" />
<Skeleton className="h-9 w-24 rounded-md" />
</div>
</div>
{/* Metric cards grid skeleton */}
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4">
{[1, 2, 3, 4, 5, 6, 7, 8].map((i) => (
<Card
key={i}
className="overflow-hidden animate-in fade-in slide-in-from-bottom-4"
style={{
animationDelay: `${i * 50}ms`,
animationDuration: '400ms',
animationFillMode: 'both',
}}
>
<CardHeader className="pb-2">
<div className="flex justify-between items-start">
<Skeleton className="h-4 w-24" />
<Skeleton className="h-4 w-4 rounded" />
</div>
</CardHeader>
<CardContent className="space-y-3">
<Skeleton className="h-8 w-20" />
<div className="space-y-2">
<Skeleton className="h-3 w-full" />
<Skeleton className="h-3 w-3/4" />
</div>
{/* Chart area skeleton */}
<div className="mt-4 h-12 flex items-end gap-1">
{[...Array(7)].map((_, idx) => {
// Deterministic heights to avoid hydration mismatches
const heights = [45, 65, 55, 80, 50, 70, 60];
return (
<Skeleton
key={idx}
className="flex-1 rounded-t"
style={{
height: `${heights[idx % heights.length]}%`,
}}
/>
);
})}
</div>
</CardContent>
</Card>
))}
</div>
</div>
);
}
// Loading skeleton for management cards
function ManagementCardsSkeleton() {
// Suspense wrapper with timeout
function SuspenseWithTimeout({
children,
fallback,
timeout = 5000,
timeoutFallback
}: {
children: ReactNode;
fallback: ReactNode;
timeout?: number;
timeoutFallback?: ReactNode;
}) {
const [showTimeoutWarning, setShowTimeoutWarning] = useState(false);
useEffect(() => {
const timer = setTimeout(() => {
setShowTimeoutWarning(true);
}, timeout);
return () => clearTimeout(timer);
}, [timeout]);
return (
<div className="grid gap-4 lg:gap-6 sm:grid-cols-2 lg:grid-cols-3 items-stretch">
<Suspense fallback={showTimeoutWarning && timeoutFallback ? timeoutFallback : fallback}>
{children}
</Suspense>
);
}
// Loading skeleton for management cards
function ManagementCardsSkeleton({ showSlowWarning = false }: { showSlowWarning?: boolean }) {
return (
<div
className="grid gap-4 lg:gap-6 sm:grid-cols-2 lg:grid-cols-3 items-stretch relative"
role="status"
aria-label="Loading management tools"
aria-live="polite"
>
{showSlowWarning && (
<div className="col-span-full mb-4">
<Alert className="animate-in fade-in duration-300">
<AlertCircle className="h-4 w-4" />
<AlertTitle>Taking longer than expected</AlertTitle>
<AlertDescription>
The components are still loading. This may be due to a slow connection. Please wait...
</AlertDescription>
</Alert>
</div>
)}
{/* Subtle loading indicator */}
<div className="absolute -top-6 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>
{[1, 2, 3, 4].map((i) => (
<Card key={i}>
<CardHeader>
<Skeleton className="h-6 w-32" />
<Card
key={i}
className="overflow-hidden animate-in fade-in slide-in-from-bottom-4"
style={{
animationDelay: `${i * 75}ms`,
animationDuration: '400ms',
animationFillMode: 'both',
}}
>
<CardHeader className="space-y-2">
<Skeleton className="h-5 w-32" />
<Skeleton className="h-3 w-48 max-w-full" />
</CardHeader>
<CardContent>
<Skeleton className="h-20 w-full" />
<CardContent className="space-y-3">
{/* Form elements skeleton for interactive cards */}
{i <= 2 ? (
<>
<Skeleton className="h-10 w-full rounded-md" />
<Skeleton className="h-10 w-full rounded-md" />
<Skeleton className="h-9 w-24 rounded-md" />
</>
) : (
/* List items skeleton for list cards */
<>
{[1, 2, 3].map((j) => (
<div
key={j}
className="space-y-2 p-3 rounded border border-border/50 animate-in fade-in"
style={{
animationDelay: `${(i - 2) * 75 + j * 50}ms`,
animationDuration: '300ms',
animationFillMode: 'both',
}}
>
<Skeleton className="h-4 w-full" />
<Skeleton className="h-3 w-3/4" />
<div className="flex justify-end gap-2 mt-2">
<Skeleton className="h-6 w-16 rounded" />
<Skeleton className="h-6 w-12 rounded" />
</div>
</div>
))}
</>
)}
</CardContent>
</Card>
))}
@@ -55,6 +339,56 @@ function ManagementCardsSkeleton() {
export default function AdminPage() {
const [activeTab, setActiveTab] = useState("analytics");
const [prefetchedTabs, setPrefetchedTabs] = useState<Set<string>>(new Set());
// Prefetch components on tab hover or focus for better performance
const prefetchTabComponents = (tab: string) => {
// Avoid prefetching if already done
if (prefetchedTabs.has(tab)) return;
const startTime = performance.now();
if (tab === "analytics") {
// Prefetch analytics component
import("@/components/admin/AdminAnalytics")
.then(() => {
const loadTime = performance.now() - startTime;
console.log(`[Performance] AdminAnalytics prefetched in ${loadTime.toFixed(2)}ms`);
})
.catch(() => {});
} else if (tab === "management") {
// Prefetch management components
Promise.all([
import("@/components/admin/VendorsCard"),
import("@/components/admin/InviteVendorCard"),
import("@/components/admin/BanUserCard"),
import("@/components/admin/InvitationsListCard"),
])
.then(() => {
const loadTime = performance.now() - startTime;
console.log(`[Performance] Management components prefetched in ${loadTime.toFixed(2)}ms`);
})
.catch(() => {});
}
setPrefetchedTabs(prev => new Set(prev).add(tab));
};
// Prefetch on hover
const handleTabHover = (tab: string) => {
prefetchTabComponents(tab);
};
// Prefetch on focus (keyboard navigation)
const handleTabFocus = (tab: string) => {
prefetchTabComponents(tab);
};
// Prefetch the active tab immediately if not already prefetched
useEffect(() => {
prefetchTabComponents(activeTab);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [activeTab]);
return (
<div className="space-y-6">
@@ -70,25 +404,59 @@ export default function AdminPage() {
<Tabs value={activeTab} onValueChange={setActiveTab} className="space-y-6">
<TabsList>
<TabsTrigger value="analytics">Analytics</TabsTrigger>
<TabsTrigger value="management">Management</TabsTrigger>
<TabsTrigger
value="analytics"
onMouseEnter={() => handleTabHover("analytics")}
onFocus={() => handleTabFocus("analytics")}
>
Analytics
</TabsTrigger>
<TabsTrigger
value="management"
onMouseEnter={() => handleTabHover("management")}
onFocus={() => handleTabFocus("management")}
>
Management
</TabsTrigger>
</TabsList>
<TabsContent value="analytics" className="space-y-6">
<Suspense fallback={<AdminComponentSkeleton />}>
<AdminAnalytics />
</Suspense>
<TabsContent value="analytics" className="space-y-6 relative">
<ErrorBoundary componentName="Analytics Dashboard">
<SuspenseWithTimeout
fallback={<AdminComponentSkeleton />}
timeout={5000}
timeoutFallback={<AdminComponentSkeleton showSlowWarning={true} />}
>
<div className="animate-in fade-in slide-in-from-bottom-2 duration-300">
<AdminAnalytics />
</div>
</SuspenseWithTimeout>
</ErrorBoundary>
</TabsContent>
<TabsContent value="management" className="space-y-6">
<Suspense fallback={<ManagementCardsSkeleton />}>
<div className="grid gap-4 lg:gap-6 sm:grid-cols-2 lg:grid-cols-3 items-stretch">
<VendorsCard />
<InviteVendorCard />
<BanUserCard />
<InvitationsListCard />
</div>
</Suspense>
<TabsContent value="management" className="space-y-6 relative">
<ErrorBoundary componentName="Management Tools">
<SuspenseWithTimeout
fallback={<ManagementCardsSkeleton />}
timeout={5000}
timeoutFallback={<ManagementCardsSkeleton showSlowWarning={true} />}
>
<div className="grid gap-4 lg:gap-6 sm:grid-cols-2 lg:grid-cols-3 items-stretch animate-in fade-in slide-in-from-bottom-2 duration-300">
<ErrorBoundary componentName="Vendors Card">
<VendorsCard />
</ErrorBoundary>
<ErrorBoundary componentName="Invite Vendor Card">
<InviteVendorCard />
</ErrorBoundary>
<ErrorBoundary componentName="Ban User Card">
<BanUserCard />
</ErrorBoundary>
<ErrorBoundary componentName="Invitations List Card">
<InvitationsListCard />
</ErrorBoundary>
</div>
</SuspenseWithTimeout>
</ErrorBoundary>
</TabsContent>
</Tabs>
</div>

View File

@@ -1,26 +1,113 @@
"use client";
import { useEffect, Suspense } from "react";
import { useEffect, Suspense, Component, ReactNode } from "react";
import { useRouter } from "next/navigation";
import Dashboard from "@/components/dashboard/dashboard";
import { MessageCircle } from "lucide-react";
import { MessageCircle, 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";
// Lazy load the ChatTable component
const ChatTable = dynamic(() => import("@/components/dashboard/ChatTable"), {
// 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 ChatTable component with error handling
const ChatTable = dynamic(() => import("@/components/dashboard/ChatTable").catch((err) => {
console.error("Failed to load ChatTable:", err);
throw err;
}), {
loading: () => <ChatTableSkeleton />
});
// Loading skeleton for the chat table
function ChatTableSkeleton() {
return (
<Card>
<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" />
<Skeleton className="h-9 w-24" />
<Skeleton className="h-9 w-24 rounded-md" />
</div>
</CardHeader>
<CardContent>
@@ -28,13 +115,29 @@ function ChatTableSkeleton() {
<div className="border-b p-4">
<div className="flex items-center gap-4">
{['Customer', 'Last Message', 'Date', 'Status', 'Actions'].map((header, i) => (
<Skeleton key={i} className="h-4 w-20 flex-1" />
<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>
{[...Array(6)].map((_, i) => (
<div key={i} className="border-b last:border-b-0 p-4">
<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">
<div className="flex items-center gap-3 flex-1">
<Skeleton className="h-10 w-10 rounded-full" />
@@ -45,10 +148,10 @@ function ChatTableSkeleton() {
</div>
<Skeleton className="h-4 w-40 flex-1" />
<Skeleton className="h-4 w-20 flex-1" />
<Skeleton className="h-6 w-16 flex-1" />
<Skeleton className="h-6 w-16 flex-1 rounded-full" />
<div className="flex gap-2 flex-1">
<Skeleton className="h-8 w-16" />
<Skeleton className="h-8 w-16" />
<Skeleton className="h-8 w-16 rounded-md" />
<Skeleton className="h-8 w-16 rounded-md" />
</div>
</div>
</div>
@@ -83,9 +186,11 @@ export default function ChatsPage() {
</h1>
</div>
<Suspense fallback={<ChatTableSkeleton />}>
<ChatTable />
</Suspense>
<ErrorBoundary componentName="Chat Table">
<Suspense fallback={<ChatTableSkeleton />}>
<ChatTable />
</Suspense>
</ErrorBoundary>
</div>
</Dashboard>
);

View File

@@ -0,0 +1,262 @@
"use client";
import { Component, ReactNode, useState, useEffect, Suspense } from "react";
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
import { Button } from "@/components/ui/button";
import { AlertCircle, RefreshCw } from "lucide-react";
import { Skeleton } from "@/components/ui/skeleton";
import { Card, CardContent, CardHeader } from "@/components/ui/card";
// Error Boundary Component
interface ErrorBoundaryState {
hasError: boolean;
error: Error | null;
}
interface ErrorBoundaryProps {
children: ReactNode;
fallback?: 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) {
if (this.props.fallback) {
return this.props.fallback;
}
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>
<Button
variant="ghost"
size="sm"
onClick={() => window.location.reload()}
>
Full Reload
</Button>
</div>
</AlertDescription>
</Alert>
);
}
return this.props.children;
}
}
// Suspense wrapper with timeout
function SuspenseWithTimeout({
children,
fallback,
timeout = 5000,
timeoutFallback
}: {
children: ReactNode;
fallback: ReactNode;
timeout?: number;
timeoutFallback?: ReactNode;
}) {
const [showTimeoutWarning, setShowTimeoutWarning] = useState(false);
useEffect(() => {
const timer = setTimeout(() => {
setShowTimeoutWarning(true);
}, timeout);
return () => clearTimeout(timer);
}, [timeout]);
return (
<Suspense fallback={showTimeoutWarning && timeoutFallback ? timeoutFallback : fallback}>
{children}
</Suspense>
);
}
// Loading skeleton with timeout warning
function DashboardContentSkeletonWithWarning() {
return (
<div className="space-y-6 animate-in fade-in duration-500 relative">
<Alert className="mb-4 animate-in fade-in duration-300">
<AlertCircle className="h-4 w-4" />
<AlertTitle>Taking longer than expected</AlertTitle>
<AlertDescription>
The dashboard is still loading. This may be due to a slow connection. Please wait...
</AlertDescription>
</Alert>
<div className="space-y-6">
<div>
<Skeleton className="h-8 w-64 mb-2" />
<Skeleton className="h-4 w-96 max-w-full" />
</div>
<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}>
<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>
</div>
</div>
);
}
// Import the skeleton from the page
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>
);
}
export default function DashboardContentWrapper({ children }: { children: ReactNode }) {
return (
<ErrorBoundary componentName="Dashboard Content">
<SuspenseWithTimeout
fallback={<DashboardContentSkeleton />}
timeout={5000}
timeoutFallback={<DashboardContentSkeletonWithWarning />}
>
{children}
</SuspenseWithTimeout>
</ErrorBoundary>
);
}

View File

@@ -1,28 +1,115 @@
"use client";
import { useEffect, Suspense } from "react";
import { useEffect, Suspense, Component, ReactNode, useState } from "react";
import { useRouter } from "next/navigation";
import Dashboard from "@/components/dashboard/dashboard";
import { Package } from "lucide-react";
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";
// Lazy load the OrderTable component
const OrderTable = dynamic(() => import("@/components/tables/order-table"), {
// 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>
<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" />
<Skeleton className="h-9 w-32" />
<Skeleton className="h-9 w-24 rounded-md" />
<Skeleton className="h-9 w-32 rounded-md" />
</div>
</div>
</CardHeader>
@@ -32,23 +119,39 @@ function OrderTableSkeleton() {
<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" />
<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">
<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" />
<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" />
<Skeleton className="h-8 w-16" />
<Skeleton className="h-8 w-16 rounded-md" />
<Skeleton className="h-8 w-16 rounded-md" />
</div>
</div>
</div>
@@ -83,9 +186,11 @@ export default function OrdersPage() {
</h1>
</div>
<Suspense fallback={<OrderTableSkeleton />}>
<OrderTable />
</Suspense>
<ErrorBoundary componentName="Order Table">
<Suspense fallback={<OrderTableSkeleton />}>
<OrderTable />
</Suspense>
</ErrorBoundary>
</div>
</Dashboard>
);

View File

@@ -8,26 +8,46 @@ 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';
// Lazy load the Content component
const Content = dynamic(() => import("@/components/dashboard/content"), {
loading: () => <DashboardContentSkeleton />
});
import DashboardContentWrapper from './dashboard-content-wrapper';
// Loading skeleton for the dashboard content
function DashboardContentSkeleton() {
return (
<div className="space-y-6">
<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" />
<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}>
<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" />
@@ -41,15 +61,29 @@ function DashboardContentSkeleton() {
</div>
{/* Best selling products skeleton */}
<Card>
<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" />
<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">
<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" />
@@ -68,6 +102,14 @@ function DashboardContentSkeleton() {
);
}
// 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;
@@ -108,9 +150,11 @@ export default async function DashboardPage() {
return (
<Dashboard>
<Suspense fallback={<DashboardContentSkeleton />}>
<Content username={vendor.username} orderStats={orderStats} />
</Suspense>
<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" />

View File

@@ -14,32 +14,44 @@ import dynamic from "next/dynamic";
import { Skeleton } from "@/components/ui/skeleton";
import { Card, CardContent, CardHeader } from "@/components/ui/card";
// Lazy load heavy components
const ProductTable = dynamic(() => import("@/components/tables/product-table"), {
// Lazy load heavy components with error handling
const ProductTable = dynamic(() => import("@/components/tables/product-table").catch((err) => {
console.error("Failed to load ProductTable:", err);
throw err;
}), {
loading: () => <ProductTableSkeleton />
});
const ProductModal = dynamic(() => import("@/components/modals/product-modal").then(mod => ({ default: mod.ProductModal })), {
loading: () => <div>Loading...</div>
const ProductModal = dynamic(() => import("@/components/modals/product-modal").then(mod => ({ default: mod.ProductModal })).catch((err) => {
console.error("Failed to load ProductModal:", err);
throw err;
}), {
loading: () => <ModalSkeleton />
});
const ImportProductsModal = dynamic(() => import("@/components/modals/import-products-modal"), {
loading: () => <div>Loading...</div>
const ImportProductsModal = dynamic(() => import("@/components/modals/import-products-modal").catch((err) => {
console.error("Failed to load ImportProductsModal:", err);
throw err;
}), {
loading: () => <ModalSkeleton />
});
const ProfitAnalysisModal = dynamic(() => import("@/components/modals/profit-analysis-modal").then(mod => ({ default: mod.ProfitAnalysisModal })), {
loading: () => <div>Loading...</div>
const ProfitAnalysisModal = dynamic(() => import("@/components/modals/profit-analysis-modal").then(mod => ({ default: mod.ProfitAnalysisModal })).catch((err) => {
console.error("Failed to load ProfitAnalysisModal:", err);
throw err;
}), {
loading: () => <ModalSkeleton />
});
function ProductTableSkeleton() {
return (
<Card>
<Card className="animate-in fade-in duration-500">
<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" />
<Skeleton className="h-9 w-32" />
<Skeleton className="h-9 w-24 rounded-md" />
<Skeleton className="h-9 w-32 rounded-md" />
</div>
</div>
</CardHeader>
@@ -48,13 +60,29 @@ function ProductTableSkeleton() {
<div className="border-b p-4">
<div className="flex items-center gap-4">
{['Product', 'Category', 'Price', 'Stock', 'Status', 'Actions'].map((header, i) => (
<Skeleton key={i} className="h-4 w-20 flex-1" />
<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>
{[...Array(8)].map((_, i) => (
<div key={i} className="border-b last:border-b-0 p-4">
<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">
<div className="flex items-center gap-3 flex-1">
<Skeleton className="h-12 w-12 rounded-md" />
@@ -66,10 +94,10 @@ function ProductTableSkeleton() {
<Skeleton className="h-4 w-24 flex-1" />
<Skeleton className="h-4 w-16 flex-1" />
<Skeleton className="h-4 w-16 flex-1" />
<Skeleton className="h-6 w-20 flex-1" />
<Skeleton className="h-6 w-20 flex-1 rounded-full" />
<div className="flex gap-2 flex-1">
<Skeleton className="h-8 w-16" />
<Skeleton className="h-8 w-16" />
<Skeleton className="h-8 w-16 rounded-md" />
<Skeleton className="h-8 w-16 rounded-md" />
</div>
</div>
</div>
@@ -80,6 +108,29 @@ function ProductTableSkeleton() {
);
}
function ModalSkeleton() {
return (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-background/80 backdrop-blur-sm">
<Card className="w-full max-w-2xl m-4 animate-in fade-in zoom-in-95 duration-300">
<CardHeader>
<Skeleton className="h-6 w-48" />
<Skeleton className="h-4 w-64 mt-2" />
</CardHeader>
<CardContent>
<div className="space-y-4">
{[...Array(5)].map((_, i) => (
<div key={i} className="space-y-2">
<Skeleton className="h-4 w-24" />
<Skeleton className="h-10 w-full rounded-md" />
</div>
))}
</div>
</CardContent>
</Card>
</div>
);
}
export default function ProductsPage() {
const router = useRouter();
const [products, setProducts] = useState<Product[]>([]);

View File

@@ -17,23 +17,59 @@ import {
import dynamic from "next/dynamic";
import { Card, CardContent, CardHeader } from "@/components/ui/card";
// Lazy load components
const ShippingModal = dynamic(() => import("@/components/modals/shipping-modal").then(mod => ({ default: mod.ShippingModal })), {
loading: () => <div>Loading...</div>
// Lazy load components with error handling
const ShippingModal = dynamic(() => import("@/components/modals/shipping-modal").then(mod => ({ default: mod.ShippingModal })).catch((err) => {
console.error("Failed to load ShippingModal:", err);
throw err;
}), {
loading: () => (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-background/80 backdrop-blur-sm">
<Card className="w-full max-w-md m-4 animate-in fade-in zoom-in-95 duration-300">
<CardHeader>
<Skeleton className="h-6 w-48" />
<Skeleton className="h-4 w-64 mt-2" />
</CardHeader>
<CardContent>
<div className="space-y-4">
{[...Array(3)].map((_, i) => (
<div key={i} className="space-y-2">
<Skeleton className="h-4 w-24" />
<Skeleton className="h-10 w-full rounded-md" />
</div>
))}
</div>
</CardContent>
</Card>
</div>
)
});
const ShippingTable = dynamic(() => import("@/components/tables/shipping-table").then(mod => ({ default: mod.ShippingTable })), {
const ShippingTable = dynamic(() => import("@/components/tables/shipping-table").then(mod => ({ default: mod.ShippingTable })).catch((err) => {
console.error("Failed to load ShippingTable:", err);
throw err;
}), {
loading: () => <ShippingTableSkeleton />
});
// Loading skeleton for shipping table
function ShippingTableSkeleton() {
return (
<Card>
<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" />
<Skeleton className="h-9 w-24" />
<Skeleton className="h-9 w-24 rounded-md" />
</div>
</CardHeader>
<CardContent>
@@ -41,19 +77,35 @@ function ShippingTableSkeleton() {
<div className="border-b p-4">
<div className="flex items-center gap-4">
{['Method Name', 'Price', 'Actions'].map((header, i) => (
<Skeleton key={i} className="h-4 w-20 flex-1" />
<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>
{[...Array(4)].map((_, i) => (
<div key={i} className="border-b last:border-b-0 p-4">
<div
key={i}
className="border-b last:border-b-0 p-4 animate-in fade-in"
style={{
animationDelay: `${200 + i * 50}ms`,
animationDuration: '300ms',
animationFillMode: 'both',
}}
>
<div className="flex items-center gap-4">
<Skeleton className="h-4 w-32 flex-1" />
<Skeleton className="h-4 w-16 flex-1" />
<div className="flex gap-2 flex-1">
<Skeleton className="h-8 w-16" />
<Skeleton className="h-8 w-16" />
<Skeleton className="h-8 w-16 rounded-md" />
<Skeleton className="h-8 w-16 rounded-md" />
</div>
</div>
</div>

View File

@@ -44,6 +44,7 @@ import {
} from "lucide-react";
import { Badge } from "@/components/ui/badge";
import { Input } from "@/components/ui/input";
import { Skeleton } from "@/components/ui/skeleton";
import {
DropdownMenu,
DropdownMenuTrigger,
@@ -232,8 +233,58 @@ export default function CustomerManagementPage() {
</div>
{loading ? (
<div className="p-8 flex justify-center bg-black/60">
<Loader2 className="h-8 w-8 animate-spin text-primary" />
<div className="p-8 bg-black/60">
{/* Loading indicator */}
<div className="absolute top-0 left-0 right-0 h-1 bg-muted overflow-hidden">
<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>
{/* Table skeleton */}
<div className="space-y-4">
<div className="flex items-center gap-4 pb-4 border-b border-zinc-800">
{['Customer', 'Orders', 'Total Spent', 'Last Order', 'Status'].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>
{[...Array(5)].map((_, i) => (
<div
key={i}
className="flex items-center gap-4 pb-4 border-b border-zinc-800 last:border-b-0 animate-in fade-in"
style={{
animationDelay: `${250 + i * 50}ms`,
animationDuration: '300ms',
animationFillMode: 'both',
}}
>
<div className="flex items-center gap-3 flex-1">
<Skeleton className="h-10 w-10 rounded-full" />
<div className="space-y-1">
<Skeleton className="h-4 w-32" />
<Skeleton className="h-3 w-24" />
</div>
</div>
<Skeleton className="h-6 w-12 flex-1 rounded-full" />
<Skeleton className="h-4 w-20 flex-1" />
<Skeleton className="h-4 w-24 flex-1" />
<Skeleton className="h-6 w-24 flex-1 rounded-full" />
</div>
))}
</div>
</div>
) : filteredCustomers.length === 0 ? (
<div className="p-8 text-center bg-black/60">