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