Files
g 244014f33a
All checks were successful
Build Frontend / build (push) Successful in 1m7s
Improve admin UI and vendor invite experience
Enhanced the admin dashboard tab styling for better clarity. Refactored InviteVendorCard with improved UI, feedback, and clipboard copy functionality. Fixed vendor store ID update to send raw object instead of JSON string. Ensured product price formatting is robust against non-numeric values.
2026-01-12 07:33:16 +00:00

469 lines
16 KiB
TypeScript

"use client";
export const dynamic = "force-dynamic";
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";
// 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);
// 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-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 border-border/40 bg-background/50 backdrop-blur-sm shadow-sm"
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>
);
}
// 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 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}
className="overflow-hidden animate-in fade-in slide-in-from-bottom-4 border-border/40 bg-background/50 backdrop-blur-sm shadow-sm"
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 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>
))}
</div>
);
}
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">
<div className="flex items-center justify-between animate-in fade-in slide-in-from-top-2 duration-300">
<div>
<h1 className="text-2xl font-semibold tracking-tight text-foreground">Admin Dashboard</h1>
<p className="text-sm text-muted-foreground mt-1">Platform analytics and vendor management</p>
</div>
<Button asChild variant="outline" size="sm" className="border-border/50 bg-background/50 backdrop-blur-sm hover:bg-background/80 transition-all">
<Link href="/dashboard">Back to Dashboard</Link>
</Button>
</div>
<Tabs value={activeTab} onValueChange={setActiveTab} className="space-y-6">
<TabsList className="bg-muted/20 p-1 border border-border/40 backdrop-blur-sm h-auto">
<TabsTrigger
value="analytics"
onMouseEnter={() => handleTabHover("analytics")}
onFocus={() => handleTabFocus("analytics")}
className="data-[state=active]:bg-background/80 data-[state=active]:backdrop-blur-sm data-[state=active]:text-foreground data-[state=active]:shadow-sm px-6 py-2 transition-all"
>
Analytics
</TabsTrigger>
<TabsTrigger
value="management"
onMouseEnter={() => handleTabHover("management")}
onFocus={() => handleTabFocus("management")}
className="data-[state=active]:bg-background/80 data-[state=active]:backdrop-blur-sm data-[state=active]:text-foreground data-[state=active]:shadow-sm px-6 py-2 transition-all"
>
Management
</TabsTrigger>
</TabsList>
<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 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>
);
}