From 0062aa2dfe8415018c44cd2c43dbb8476b845931 Mon Sep 17 00:00:00 2001 From: g Date: Wed, 31 Dec 2025 05:20:44 +0000 Subject: [PATCH] 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. --- app/dashboard/admin/page.tsx | 458 ++++++++++++++++++-- app/dashboard/chats/page.tsx | 133 +++++- app/dashboard/dashboard-content-wrapper.tsx | 262 +++++++++++ app/dashboard/orders/page.tsx | 135 +++++- app/dashboard/page.tsx | 72 ++- app/dashboard/products/page.tsx | 83 +++- app/dashboard/shipping/page.tsx | 72 ++- app/dashboard/storefront/customers/page.tsx | 55 ++- app/globals.css | 10 + public/git-info.json | 4 +- 10 files changed, 1166 insertions(+), 118 deletions(-) create mode 100644 app/dashboard/dashboard-content-wrapper.tsx diff --git a/app/dashboard/admin/page.tsx b/app/dashboard/admin/page.tsx index 9a7cc8c..d4548cf 100644 --- a/app/dashboard/admin/page.tsx +++ b/app/dashboard/admin/page.tsx @@ -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 { + 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 ( + + + Failed to load {this.props.componentName || 'component'} + +

+ {this.state.error?.message || 'An unexpected error occurred while loading this component.'} +

+ {this.retryCount < this.maxRetries && ( +

+ Retry attempt {this.retryCount + 1} of {this.maxRetries + 1} +

+ )} +
+ + +
+
+
+ ); + } + + 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 ( -
- - - - - -
- - - -
-
-
+
+ {showSlowWarning && ( + + + Taking longer than expected + + The component is still loading. This may be due to a slow connection. Please wait... + + + )} + {/* Subtle loading indicator */} +
+
+
+ + {/* Header skeleton */} +
+
+ + +
+
+ + + +
+
+ + {/* Metric cards grid skeleton */} +
+ {[1, 2, 3, 4, 5, 6, 7, 8].map((i) => ( + + +
+ + +
+
+ + +
+ + +
+ {/* Chart area skeleton */} +
+ {[...Array(7)].map((_, idx) => { + // Deterministic heights to avoid hydration mismatches + const heights = [45, 65, 55, 80, 50, 70, 60]; + return ( + + ); + })} +
+
+
+ ))} +
); } -// 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 ( -
+ + {children} + + ); +} + +// Loading skeleton for management cards +function ManagementCardsSkeleton({ showSlowWarning = false }: { showSlowWarning?: boolean }) { + return ( +
+ {showSlowWarning && ( +
+ + + Taking longer than expected + + The components are still loading. This may be due to a slow connection. Please wait... + + +
+ )} + {/* Subtle loading indicator */} +
+
+
{[1, 2, 3, 4].map((i) => ( - - - + + + + - - + + {/* Form elements skeleton for interactive cards */} + {i <= 2 ? ( + <> + + + + + ) : ( + /* List items skeleton for list cards */ + <> + {[1, 2, 3].map((j) => ( +
+ + +
+ + +
+
+ ))} + + )}
))} @@ -55,6 +339,56 @@ function ManagementCardsSkeleton() { export default function AdminPage() { const [activeTab, setActiveTab] = useState("analytics"); + const [prefetchedTabs, setPrefetchedTabs] = useState>(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 (
@@ -70,25 +404,59 @@ export default function AdminPage() { - Analytics - Management + handleTabHover("analytics")} + onFocus={() => handleTabFocus("analytics")} + > + Analytics + + handleTabHover("management")} + onFocus={() => handleTabFocus("management")} + > + Management + - - }> - - + + + } + timeout={5000} + timeoutFallback={} + > +
+ +
+
+
- - }> -
- - - - -
-
+ + + } + timeout={5000} + timeoutFallback={} + > +
+ + + + + + + + + + + + +
+
+
diff --git a/app/dashboard/chats/page.tsx b/app/dashboard/chats/page.tsx index e4b743b..f338905 100644 --- a/app/dashboard/chats/page.tsx +++ b/app/dashboard/chats/page.tsx @@ -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 { + 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 ( + + + Failed to load {this.props.componentName || 'component'} + +

+ {this.state.error?.message || 'An unexpected error occurred while loading this component.'} +

+ {this.retryCount < this.maxRetries && ( +

+ Retry attempt {this.retryCount + 1} of {this.maxRetries + 1} +

+ )} +
+ +
+
+
+ ); + } + + 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: () => }); // Loading skeleton for the chat table function ChatTableSkeleton() { return ( - + + {/* Subtle loading indicator */} +
+
+
+
- +
@@ -28,13 +115,29 @@ function ChatTableSkeleton() {
{['Customer', 'Last Message', 'Date', 'Status', 'Actions'].map((header, i) => ( - + ))}
{[...Array(6)].map((_, i) => ( -
+
@@ -45,10 +148,10 @@ function ChatTableSkeleton() {
- +
- - + +
@@ -83,9 +186,11 @@ export default function ChatsPage() {
- }> - - + + }> + + +
); diff --git a/app/dashboard/dashboard-content-wrapper.tsx b/app/dashboard/dashboard-content-wrapper.tsx new file mode 100644 index 0000000..bf0d4df --- /dev/null +++ b/app/dashboard/dashboard-content-wrapper.tsx @@ -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 { + 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 ( + + + Failed to load {this.props.componentName || 'component'} + +

+ {this.state.error?.message || 'An unexpected error occurred while loading this component.'} +

+ {this.retryCount < this.maxRetries && ( +

+ Retry attempt {this.retryCount + 1} of {this.maxRetries + 1} +

+ )} +
+ + +
+
+
+ ); + } + + 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 ( + + {children} + + ); +} + +// Loading skeleton with timeout warning +function DashboardContentSkeletonWithWarning() { + return ( +
+ + + Taking longer than expected + + The dashboard is still loading. This may be due to a slow connection. Please wait... + + +
+
+ + +
+
+ {[...Array(4)].map((_, i) => ( + + + + + + + + + + + ))} +
+
+
+ ); +} + +// Import the skeleton from the page +function DashboardContentSkeleton() { + return ( +
+ {/* Subtle loading indicator */} +
+
+
+ + {/* Header skeleton */} +
+ + +
+ + {/* Stats cards skeleton */} +
+ {[...Array(4)].map((_, i) => ( + + + + + + + + + + + ))} +
+ + {/* Best selling products skeleton */} + + + + + + +
+ {[...Array(5)].map((_, i) => ( +
+ +
+ + +
+
+ + +
+
+ ))} +
+
+
+
+ ); +} + +export default function DashboardContentWrapper({ children }: { children: ReactNode }) { + return ( + + } + timeout={5000} + timeoutFallback={} + > + {children} + + + ); +} + diff --git a/app/dashboard/orders/page.tsx b/app/dashboard/orders/page.tsx index c004ab2..e6428a3 100644 --- a/app/dashboard/orders/page.tsx +++ b/app/dashboard/orders/page.tsx @@ -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 { + 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 ( + + + Failed to load {this.props.componentName || 'component'} + +

+ {this.state.error?.message || 'An unexpected error occurred while loading this component.'} +

+ {this.retryCount < this.maxRetries && ( +

+ Retry attempt {this.retryCount + 1} of {this.maxRetries + 1} +

+ )} +
+ +
+
+
+ ); + } + + 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: () => }); // Loading skeleton for the order table function OrderTableSkeleton() { return ( - + + {/* Subtle loading indicator */} +
+
+
+
- - + +
@@ -32,23 +119,39 @@ function OrderTableSkeleton() {
{['Order ID', 'Customer', 'Status', 'Total', 'Date', 'Actions'].map((header, i) => ( - + ))}
{/* Table rows skeleton */} {[...Array(8)].map((_, i) => ( -
+
- +
- - + +
@@ -83,9 +186,11 @@ export default function OrdersPage() {
- }> - - + + }> + + +
); diff --git a/app/dashboard/page.tsx b/app/dashboard/page.tsx index 35d44fa..c71afc7 100644 --- a/app/dashboard/page.tsx +++ b/app/dashboard/page.tsx @@ -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: () => -}); +import DashboardContentWrapper from './dashboard-content-wrapper'; // Loading skeleton for the dashboard content function DashboardContentSkeleton() { return ( -
+
+ {/* Subtle loading indicator */} +
+
+
+ {/* Header skeleton */}
- +
{/* Stats cards skeleton */}
{[...Array(4)].map((_, i) => ( - + @@ -41,15 +61,29 @@ function DashboardContentSkeleton() {
{/* Best selling products skeleton */} - + - +
{[...Array(5)].map((_, i) => ( -
+
@@ -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: () => +}); + // ✅ Corrected Vendor Type interface Vendor { _id: string; @@ -108,9 +150,11 @@ export default async function DashboardPage() { return ( - }> - - + + }> + + +
diff --git a/app/dashboard/products/page.tsx b/app/dashboard/products/page.tsx index 1b7c9b5..e2de997 100644 --- a/app/dashboard/products/page.tsx +++ b/app/dashboard/products/page.tsx @@ -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: () => }); -const ProductModal = dynamic(() => import("@/components/modals/product-modal").then(mod => ({ default: mod.ProductModal })), { - loading: () =>
Loading...
+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: () => }); -const ImportProductsModal = dynamic(() => import("@/components/modals/import-products-modal"), { - loading: () =>
Loading...
+const ImportProductsModal = dynamic(() => import("@/components/modals/import-products-modal").catch((err) => { + console.error("Failed to load ImportProductsModal:", err); + throw err; +}), { + loading: () => }); -const ProfitAnalysisModal = dynamic(() => import("@/components/modals/profit-analysis-modal").then(mod => ({ default: mod.ProfitAnalysisModal })), { - loading: () =>
Loading...
+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: () => }); function ProductTableSkeleton() { return ( - +
- - + +
@@ -48,13 +60,29 @@ function ProductTableSkeleton() {
{['Product', 'Category', 'Price', 'Stock', 'Status', 'Actions'].map((header, i) => ( - + ))}
{[...Array(8)].map((_, i) => ( -
+
@@ -66,10 +94,10 @@ function ProductTableSkeleton() { - +
- - + +
@@ -80,6 +108,29 @@ function ProductTableSkeleton() { ); } +function ModalSkeleton() { + return ( +
+ + + + + + +
+ {[...Array(5)].map((_, i) => ( +
+ + +
+ ))} +
+
+
+
+ ); +} + export default function ProductsPage() { const router = useRouter(); const [products, setProducts] = useState([]); diff --git a/app/dashboard/shipping/page.tsx b/app/dashboard/shipping/page.tsx index e2541e7..82917ba 100644 --- a/app/dashboard/shipping/page.tsx +++ b/app/dashboard/shipping/page.tsx @@ -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: () =>
Loading...
+// 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: () => ( +
+ + + + + + +
+ {[...Array(3)].map((_, i) => ( +
+ + +
+ ))} +
+
+
+
+ ) }); -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: () => }); // Loading skeleton for shipping table function ShippingTableSkeleton() { return ( - + + {/* Subtle loading indicator */} +
+
+
+
- +
@@ -41,19 +77,35 @@ function ShippingTableSkeleton() {
{['Method Name', 'Price', 'Actions'].map((header, i) => ( - + ))}
{[...Array(4)].map((_, i) => ( -
+
- - + +
diff --git a/app/dashboard/storefront/customers/page.tsx b/app/dashboard/storefront/customers/page.tsx index 7083fd0..9841853 100644 --- a/app/dashboard/storefront/customers/page.tsx +++ b/app/dashboard/storefront/customers/page.tsx @@ -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() {
{loading ? ( -
- +
+ {/* Loading indicator */} +
+
+
+ + {/* Table skeleton */} +
+
+ {['Customer', 'Orders', 'Total Spent', 'Last Order', 'Status'].map((header, i) => ( + + ))} +
+ + {[...Array(5)].map((_, i) => ( +
+
+ +
+ + +
+
+ + + + +
+ ))} +
) : filteredCustomers.length === 0 ? (
diff --git a/app/globals.css b/app/globals.css index b245c5f..784197a 100644 --- a/app/globals.css +++ b/app/globals.css @@ -11,6 +11,16 @@ body { text-wrap: balance; } + /* Shimmer animation for loading indicators */ + @keyframes shimmer { + 0% { + background-position: -200% 0; + } + 100% { + background-position: 200% 0; + } + } + /* Accessibility improvements */ .sr-only { position: absolute; diff --git a/public/git-info.json b/public/git-info.json index b85f8f5..44bb8a8 100644 --- a/public/git-info.json +++ b/public/git-info.json @@ -1,4 +1,4 @@ { - "commitHash": "2db13cc", - "buildTime": "2025-12-27T20:56:32.712Z" + "commitHash": "96638f9", + "buildTime": "2025-12-31T05:14:19.565Z" } \ No newline at end of file