Improve browser detection and table UX for Firefox
All checks were successful
Build Frontend / build (push) Successful in 1m10s
All checks were successful
Build Frontend / build (push) Successful in 1m10s
Standardizes browser detection logic across admin and storefront pages to more accurately identify Firefox. Updates table rendering logic to provide better compatibility and fallback for Firefox, including conditional use of AnimatePresence and improved loading/empty states. Refines table UI styles for consistency and accessibility.
This commit is contained in:
217
app/dashboard/admin/vendors/page.tsx
vendored
217
app/dashboard/admin/vendors/page.tsx
vendored
@@ -12,6 +12,14 @@ import { Label } from "@/components/ui/label";
|
||||
import { fetchClient } from "@/lib/api-client";
|
||||
import { useToast } from "@/hooks/use-toast";
|
||||
import { motion, AnimatePresence } from "framer-motion";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
|
||||
interface Vendor {
|
||||
_id: string;
|
||||
@@ -40,6 +48,13 @@ export default function AdminVendorsPage() {
|
||||
const { toast } = useToast();
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [vendors, setVendors] = useState<Vendor[]>([]);
|
||||
// State for browser detection
|
||||
const [isFirefox, setIsFirefox] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const ua = navigator.userAgent.toLowerCase();
|
||||
setIsFirefox(ua.includes("firefox") && !ua.includes("chrome"));
|
||||
}, []);
|
||||
const [page, setPage] = useState(1);
|
||||
const [pagination, setPagination] = useState<PaginationResponse['pagination'] | null>(null);
|
||||
const [searchQuery, setSearchQuery] = useState("");
|
||||
@@ -48,6 +63,26 @@ export default function AdminVendorsPage() {
|
||||
const [newStoreId, setNewStoreId] = useState("");
|
||||
const [updating, setUpdating] = useState(false);
|
||||
|
||||
const handleToggleStatus = async (vendor: Vendor) => {
|
||||
try {
|
||||
await fetchClient(`/admin/vendors/${vendor._id}/status`, {
|
||||
method: 'PATCH',
|
||||
body: { isActive: !vendor.isActive }
|
||||
});
|
||||
toast({
|
||||
title: "Success",
|
||||
description: `Vendor ${vendor.isActive ? 'suspended' : 'activated'} successfully`,
|
||||
});
|
||||
fetchVendors();
|
||||
} catch (error: any) {
|
||||
toast({
|
||||
title: "Error",
|
||||
description: error.message || "Failed to update vendor status",
|
||||
variant: "destructive",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleEditStore = (vendor: Vendor) => {
|
||||
setEditingVendor(vendor);
|
||||
setNewStoreId(vendor.storeId || "");
|
||||
@@ -212,8 +247,8 @@ export default function AdminVendorsPage() {
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
<AnimatePresence mode="popLayout">
|
||||
{loading ? (
|
||||
{isFirefox ? (
|
||||
loading ? (
|
||||
<TableRow>
|
||||
<TableCell colSpan={6} className="h-32 text-center">
|
||||
<div className="flex flex-col items-center justify-center gap-2 text-muted-foreground">
|
||||
@@ -234,7 +269,6 @@ export default function AdminVendorsPage() {
|
||||
key={vendor._id}
|
||||
initial={{ opacity: 0, y: 10 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
exit={{ opacity: 0 }}
|
||||
transition={{ duration: 0.2, delay: index * 0.03 }}
|
||||
className="group hover:bg-muted/40 border-b border-border/50 transition-colors"
|
||||
>
|
||||
@@ -302,22 +336,173 @@ export default function AdminVendorsPage() {
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell className="text-right">
|
||||
<div className="flex items-center justify-end space-x-1">
|
||||
<Button variant="ghost" size="icon" className="h-8 w-8 text-muted-foreground hover:text-primary">
|
||||
<UserCheck className="h-4 w-4" />
|
||||
</Button>
|
||||
<Button variant="ghost" size="icon" className="h-8 w-8 text-muted-foreground hover:text-destructive hover:bg-destructive/10">
|
||||
<UserX className="h-4 w-4" />
|
||||
</Button>
|
||||
<Button variant="ghost" size="icon" className="h-8 w-8 text-muted-foreground">
|
||||
<MoreHorizontal className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="ghost" className="h-8 w-8 p-0">
|
||||
<span className="sr-only">Open menu</span>
|
||||
<MoreHorizontal className="h-4 w-4" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuLabel>Actions</DropdownMenuLabel>
|
||||
<DropdownMenuItem
|
||||
onClick={() => navigator.clipboard.writeText(vendor._id)}
|
||||
>
|
||||
Copy Vendor ID
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem
|
||||
className={vendor.isActive ? "text-red-600" : "text-green-600"}
|
||||
onClick={() => handleToggleStatus(vendor)}
|
||||
>
|
||||
{vendor.isActive ? (
|
||||
<>
|
||||
<UserX className="mr-2 h-4 w-4" />
|
||||
Suspend Vendor
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<UserCheck className="mr-2 h-4 w-4" />
|
||||
Activate Vendor
|
||||
</>
|
||||
)}
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</TableCell>
|
||||
</motion.tr>
|
||||
))
|
||||
)}
|
||||
</AnimatePresence>
|
||||
)
|
||||
) : (
|
||||
<AnimatePresence mode="popLayout">
|
||||
{loading ? (
|
||||
<TableRow>
|
||||
<TableCell colSpan={6} className="h-32 text-center">
|
||||
<div className="flex flex-col items-center justify-center gap-2 text-muted-foreground">
|
||||
<Loader2 className="h-8 w-8 animate-spin opacity-25" />
|
||||
<p>Loading vendors...</p>
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
) : filteredVendors.length === 0 ? (
|
||||
<TableRow>
|
||||
<TableCell colSpan={6} className="h-32 text-center text-muted-foreground">
|
||||
{searchQuery.trim() ? "No vendors found matching your search" : "No vendors found"}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
) : (
|
||||
filteredVendors.map((vendor, index) => (
|
||||
<motion.tr
|
||||
key={vendor._id}
|
||||
initial={{ opacity: 0, y: 10 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
exit={{ opacity: 0 }}
|
||||
transition={{ duration: 0.2, delay: index * 0.03 }}
|
||||
className="group hover:bg-muted/40 border-b border-border/50 transition-colors"
|
||||
>
|
||||
<TableCell>
|
||||
<div className="font-medium flex items-center gap-2">
|
||||
<div className="h-8 w-8 rounded-full bg-primary/10 flex items-center justify-center text-primary font-bold text-xs">
|
||||
{vendor.username.substring(0, 2).toUpperCase()}
|
||||
</div>
|
||||
{vendor.username}
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{vendor.storeId ? (
|
||||
<div className="flex items-center gap-2 group/store">
|
||||
<span className="font-mono text-xs">{vendor.storeId}</span>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-6 w-6 opacity-0 group-hover/store:opacity-100 transition-opacity"
|
||||
onClick={() => handleEditStore(vendor)}
|
||||
>
|
||||
<Pencil className="h-3 w-3" />
|
||||
</Button>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-muted-foreground italic text-xs">No store</span>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-6 w-6 text-muted-foreground hover:text-primary"
|
||||
onClick={() => handleEditStore(vendor)}
|
||||
>
|
||||
<Plus className="h-3 w-3" />
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<div className="flex items-center gap-2">
|
||||
<Badge
|
||||
variant={vendor.isActive ? "default" : "destructive"}
|
||||
className={vendor.isActive ? "bg-green-600 hover:bg-green-700" : ""}
|
||||
>
|
||||
{vendor.isActive ? "Active" : "Suspended"}
|
||||
</Badge>
|
||||
{vendor.isAdmin && (
|
||||
<Badge variant="outline" className="border-blue-200 text-blue-700 bg-blue-50 dark:bg-blue-900/20 dark:text-blue-400 dark:border-blue-900">
|
||||
<Shield className="h-3 w-3 mr-1" />
|
||||
Admin
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell className="text-sm text-muted-foreground">
|
||||
<div className="flex items-center gap-1.5">
|
||||
<Calendar className="h-3.5 w-3.5 opacity-70" />
|
||||
{vendor.createdAt ? new Date(vendor.createdAt).toLocaleDateString() : 'N/A'}
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell className="text-sm text-muted-foreground">
|
||||
<div className="flex items-center gap-1.5">
|
||||
<Clock className="h-3.5 w-3.5 opacity-70" />
|
||||
{vendor.lastLogin ? new Date(vendor.lastLogin).toLocaleDateString() : 'Never'}
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell className="text-right">
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="ghost" className="h-8 w-8 p-0">
|
||||
<span className="sr-only">Open menu</span>
|
||||
<MoreHorizontal className="h-4 w-4" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuLabel>Actions</DropdownMenuLabel>
|
||||
<DropdownMenuItem
|
||||
onClick={() => navigator.clipboard.writeText(vendor._id)}
|
||||
>
|
||||
Copy Vendor ID
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem
|
||||
className={vendor.isActive ? "text-red-600" : "text-green-600"}
|
||||
onClick={() => handleToggleStatus(vendor)}
|
||||
>
|
||||
{vendor.isActive ? (
|
||||
<>
|
||||
<UserX className="mr-2 h-4 w-4" />
|
||||
Suspend Vendor
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<UserCheck className="mr-2 h-4 w-4" />
|
||||
Activate Vendor
|
||||
</>
|
||||
)}
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</TableCell>
|
||||
</motion.tr>
|
||||
))
|
||||
)}
|
||||
</AnimatePresence>
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user