Improve browser detection and table UX for Firefox
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:
g
2026-01-12 08:59:04 +00:00
parent 064cd7a486
commit 3f9d28bf1b
7 changed files with 400 additions and 114 deletions

View File

@@ -50,6 +50,14 @@ export default function AdminUsersPage() {
const { toast } = useToast();
const [loading, setLoading] = useState(true);
const [users, setUsers] = useState<TelegramUser[]>([]);
// State for browser detection
// Browser detection
const [isFirefox, setIsFirefox] = useState(false);
useEffect(() => {
const ua = navigator.userAgent.toLowerCase();
setIsFirefox(ua.includes("firefox") && !ua.includes("chrome"));
}, []);
const [searchQuery, setSearchQuery] = useState("");
const [page, setPage] = useState(1);
const [pagination, setPagination] = useState<PaginationResponse['pagination'] | null>(null);
@@ -198,20 +206,14 @@ export default function AdminUsersPage() {
<AnimatePresence mode="popLayout">
{loading ? (
<TableRow>
<TableCell colSpan={8} 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 users...</p>
<TableCell colSpan={8} className="h-24 text-center">
<div className="flex items-center justify-center gap-2 text-muted-foreground">
<Loader2 className="h-4 w-4 animate-spin" />
Loading users...
</div>
</TableCell>
</TableRow>
) : users.length === 0 ? (
<TableRow>
<TableCell colSpan={8} className="h-32 text-center text-muted-foreground">
{searchQuery ? "No users found matching your search" : "No users found"}
</TableCell>
</TableRow>
) : (
) : users.length > 0 ? (
users.map((user, index) => (
<motion.tr
key={user.telegramUserId}
@@ -219,84 +221,75 @@ export default function AdminUsersPage() {
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"
className={`group border-b border-border/50 transition-colors ${user.isBlocked ? "bg-destructive/5 hover:bg-destructive/10" : "hover:bg-muted/40"}`}
>
<TableCell>
<div className="font-mono text-xs text-muted-foreground/80">{user.telegramUserId}</div>
</TableCell>
<TableCell>
<div className="font-medium flex items-center gap-2">
{user.telegramUsername !== "Unknown" ? (
<>
<span className="text-blue-500/80">@</span>
{user.telegramUsername}
</>
) : (
<span className="text-muted-foreground italic">Unknown</span>
)}
</div>
</TableCell>
<TableCell className="font-mono text-xs">{user.telegramUserId}</TableCell>
<TableCell>
<div className="flex items-center gap-2">
<span className="font-medium">{user.totalOrders}</span>
{user.completedOrders > 0 && (
<Badge variant="outline" className="text-[10px] h-5 px-1.5 bg-green-500/10 text-green-600 border-green-200 dark:border-green-900">
{user.completedOrders} done
</Badge>
<span className="font-medium">@{user.telegramUsername || "Unknown"}</span>
{user.isBlocked && (
<Badge variant="destructive" className="h-5 px-1.5 text-[10px]">Blocked</Badge>
)}
</div>
</TableCell>
<TableCell>{user.totalOrders}</TableCell>
<TableCell>{formatCurrency(user.totalSpent)}</TableCell>
<TableCell>
<span className="font-medium tabular-nums">{formatCurrency(user.totalSpent)}</span>
<div className="flex flex-col gap-1 text-xs">
<span className="text-emerald-500">{user.completedOrders} Completed</span>
<span className="text-muted-foreground">{user.paidOrders - user.completedOrders} Pending</span>
</div>
</TableCell>
<TableCell>
{user.isBlocked ? (
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<Badge variant="destructive" className="items-center gap-1">
<Ban className="h-3 w-3" />
Blocked
</Badge>
</TooltipTrigger>
{user.blockedReason && (
<TooltipContent>
<p className="max-w-xs">{user.blockedReason}</p>
</TooltipContent>
)}
</Tooltip>
</TooltipProvider>
) : user.totalOrders > 0 ? (
<Badge variant="default" className="bg-green-600 hover:bg-green-700">Active</Badge>
) : (
<Badge variant="secondary">No Orders</Badge>
)}
<TableCell className="text-xs text-muted-foreground">
{user.firstOrderDate ? new Date(user.firstOrderDate).toLocaleDateString() : "-"}
</TableCell>
<TableCell className="text-sm text-muted-foreground">
{user.firstOrderDate
? new Date(user.firstOrderDate).toLocaleDateString()
: '-'}
</TableCell>
<TableCell className="text-sm text-muted-foreground">
{user.lastOrderDate
? new Date(user.lastOrderDate).toLocaleDateString()
: '-'}
<TableCell className="text-xs text-muted-foreground">
{user.lastOrderDate ? new Date(user.lastOrderDate).toLocaleDateString() : "-"}
</TableCell>
<TableCell className="text-right">
<div className="flex items-center justify-end gap-1">
{!user.isBlocked ? (
<Button variant="ghost" size="icon" className="h-8 w-8 text-muted-foreground hover:text-destructive hover:bg-destructive/10">
<Ban className="h-4 w-4" />
</Button>
<div className="flex justify-end gap-1">
{user.isBlocked ? (
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<Button size="sm" variant="outline" className="h-8 border-emerald-500/20 text-emerald-500 hover:bg-emerald-500/10 hover:text-emerald-400">
<UserCheck className="h-4 w-4 mr-1" />
Unblock
</Button>
</TooltipTrigger>
<TooltipContent>
<p>Unblock this user</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
) : (
<Button variant="ghost" size="icon" className="h-8 w-8 text-muted-foreground hover:text-green-600 hover:bg-green-500/10">
<UserCheck className="h-4 w-4" />
</Button>
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<Button size="sm" variant="outline" className="h-8 border-destructive/20 text-destructive hover:bg-destructive/10 hover:text-destructive">
<Ban className="h-4 w-4 mr-1" />
Block
</Button>
</TooltipTrigger>
<TooltipContent>
<p>Block access to the store</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
)}
</div>
</TableCell>
</motion.tr>
))
) : (
<TableRow>
<TableCell colSpan={8} className="h-32 text-center text-muted-foreground">
<div className="flex flex-col items-center justify-center gap-2">
<Users className="h-8 w-8 opacity-20" />
<p>No users found</p>
</div>
</TableCell>
</TableRow>
)}
</AnimatePresence>
</TableBody>

View File

@@ -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>

View File

@@ -66,7 +66,8 @@ export default function CustomerManagementPage() {
const [isFirefox, setIsFirefox] = useState(false);
useEffect(() => {
setIsFirefox(navigator.userAgent.toLowerCase().indexOf("firefox") > -1);
const ua = navigator.userAgent.toLowerCase();
setIsFirefox(ua.includes("firefox") && !ua.includes("chrome"));
}, []);
const [filteredCustomers, setFilteredCustomers] = useState<CustomerStats[]>([]);
const [searchQuery, setSearchQuery] = useState("");

View File

@@ -158,10 +158,12 @@ export default function OrderTable() {
// Fetch orders with server-side pagination
// State for browser detection
// Browser detection
const [isFirefox, setIsFirefox] = useState(false);
useEffect(() => {
setIsFirefox(navigator.userAgent.toLowerCase().indexOf("firefox") > -1);
const ua = navigator.userAgent.toLowerCase();
setIsFirefox(ua.includes("firefox") && !ua.includes("chrome"));
}, []);
const fetchOrders = useCallback(async () => {
@@ -401,9 +403,9 @@ export default function OrderTable() {
return (
<div className="space-y-4">
<Card className="border-white/10 bg-black/40 backdrop-blur-xl shadow-2xl overflow-hidden rounded-xl">
<Card className="border-border/40 bg-background/50 backdrop-blur-sm shadow-sm overflow-hidden">
{/* Filters header */}
<div className="p-4 border-b border-white/5 bg-white/[0.02]">
<div className="p-4 border-b border-border/50 bg-muted/30">
<div className="flex flex-col lg:flex-row justify-between items-start lg:items-center gap-4">
<div className="flex flex-col sm:flex-row gap-2 sm:items-center w-full lg:w-auto">
<StatusFilter
@@ -423,7 +425,7 @@ export default function OrderTable() {
disabled={exporting}
variant="outline"
size="sm"
className="bg-black/20 border-white/10 hover:bg-white/5 hover:text-white transition-colors"
className="bg-background/50 border-border/50 hover:bg-muted/50 transition-colors"
>
{exporting ? (
<>
@@ -443,7 +445,7 @@ export default function OrderTable() {
<div className="flex items-center gap-2 self-end lg:self-auto">
<AlertDialog>
<AlertDialogTrigger asChild>
<Button disabled={selectedOrders.size === 0 || isShipping} className="shadow-lg bg-indigo-600 hover:bg-indigo-700 text-white border-0 transition-all hover:scale-105 active:scale-95">
<Button disabled={selectedOrders.size === 0 || isShipping} className="shadow-md">
<Truck className="mr-2 h-4 w-4" />
{isShipping ? (
<Loader2 className="h-4 w-4 animate-spin" />
@@ -484,8 +486,8 @@ export default function OrderTable() {
)}
<div className="max-h-[calc(100vh-350px)] overflow-auto">
<Table>
<TableHeader className="bg-white/[0.02] sticky top-0 z-20 backdrop-blur-md">
<TableRow className="hover:bg-transparent border-white/5">
<TableHeader className="bg-muted/30 sticky top-0 z-20">
<TableRow className="hover:bg-transparent border-border/50">
<TableHead className="w-12">
<Checkbox
checked={selectedOrders.size === paginatedOrders.length && paginatedOrders.length > 0}
@@ -525,7 +527,7 @@ export default function OrderTable() {
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ duration: 0.2 }}
className="group hover:bg-white/[0.03] border-b border-white/5 transition-colors"
className="group hover:bg-muted/50 border-b border-border/50 transition-colors"
>
<TableCell>
<Checkbox
@@ -641,11 +643,11 @@ export default function OrderTable() {
<motion.tr
key={order._id}
layout
initial={{ opacity: 0, scale: 0.98 }}
animate={{ opacity: 1, scale: 1 }}
exit={{ opacity: 0, scale: 0.98 }}
transition={{ duration: 0.2 }}
className="group hover:bg-white/[0.03] border-b border-white/5 transition-colors"
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/50 border-b border-border/50 transition-colors"
>
<TableCell>
<Checkbox

View File

@@ -18,6 +18,7 @@ import {
Archive
} from "lucide-react";
import { Button } from "@/components/ui/button";
import React, { useState, useEffect } from "react";
import { Product } from "@/models/products";
import { Badge } from "@/components/ui/badge";
import { Switch } from "@/components/ui/switch";
@@ -191,6 +192,15 @@ const ProductTable = ({
</motion.tr>
);
const [searchQuery, setSearchQuery] = useState("");
// Browser detection
const [isFirefox, setIsFirefox] = useState(false);
useEffect(() => {
const ua = navigator.userAgent.toLowerCase();
setIsFirefox(ua.includes("firefox") && !ua.includes("chrome"));
}, []);
const renderTableHeader = () => (
<TableHeader className="bg-muted/50 sticky top-0 z-10">
<TableRow className="hover:bg-transparent border-border/50">
@@ -228,8 +238,8 @@ const ProductTable = ({
<Table>
{renderTableHeader()}
<TableBody>
<AnimatePresence>
{loading ? (
{isFirefox ? (
loading ? (
<TableRow>
<TableCell colSpan={6} className="h-32 text-center text-muted-foreground">
<div className="flex flex-col items-center gap-2">
@@ -249,8 +259,32 @@ const ProductTable = ({
</div>
</TableCell>
</TableRow>
)}
</AnimatePresence>
)
) : (
<AnimatePresence>
{loading ? (
<TableRow>
<TableCell colSpan={6} className="h-32 text-center text-muted-foreground">
<div className="flex flex-col items-center gap-2">
<div className="h-6 w-6 animate-spin rounded-full border-2 border-indigo-500 border-t-transparent" />
<span>Loading products...</span>
</div>
</TableCell>
</TableRow>
) : sortedEnabledProducts.length > 0 ? (
sortedEnabledProducts.map((product, index) => renderProductRow(product, index))
) : (
<TableRow>
<TableCell colSpan={6} className="h-32 text-center text-muted-foreground">
<div className="flex flex-col items-center justify-center gap-2">
<PackageX className="h-8 w-8 opacity-20" />
<p>No active products found</p>
</div>
</TableCell>
</TableRow>
)}
</AnimatePresence>
)}
</TableBody>
</Table>
</div>

View File

@@ -1,3 +1,4 @@
import React, { useState, useEffect } from "react";
import { Skeleton } from "@/components/ui/skeleton";
import {
Table,
@@ -26,6 +27,14 @@ export const ShippingTable: React.FC<ShippingTableProps> = ({
onEditShipping,
onDeleteShipping,
}) => {
// Browser detection
const [isFirefox, setIsFirefox] = useState(false);
useEffect(() => {
const ua = navigator.userAgent.toLowerCase();
setIsFirefox(ua.includes("firefox") && !ua.includes("chrome"));
}, []);
return (
<Card className="border-border/40 bg-background/50 backdrop-blur-sm shadow-sm overflow-hidden">
<CardHeader className="py-4 px-6 border-b border-border/50 bg-muted/30">
@@ -45,8 +54,8 @@ export const ShippingTable: React.FC<ShippingTableProps> = ({
</TableRow>
</TableHeader>
<TableBody>
<AnimatePresence mode="popLayout">
{loading ? (
{isFirefox ? (
loading ? (
<TableRow>
<TableCell colSpan={3} className="h-24 text-center">
<div className="flex items-center justify-center gap-2 text-muted-foreground">
@@ -61,7 +70,6 @@ export const ShippingTable: React.FC<ShippingTableProps> = ({
key={method._id}
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0 }}
transition={{ duration: 0.2, delay: index * 0.05 }}
className="group hover:bg-muted/40 border-b border-border/50 transition-colors"
>
@@ -100,13 +108,76 @@ export const ShippingTable: React.FC<ShippingTableProps> = ({
<TableRow>
<TableCell colSpan={3} className="h-32 text-center text-muted-foreground">
<div className="flex flex-col items-center justify-center gap-2">
<PackageX className="h-8 w-8 opacity-50" />
<PackageX className="h-8 w-8 opacity-20" />
<p>No shipping methods found</p>
</div>
</TableCell>
</TableRow>
)}
</AnimatePresence>
)
) : (
<AnimatePresence mode="popLayout">
{loading ? (
<TableRow>
<TableCell colSpan={3} className="h-24 text-center">
<div className="flex items-center justify-center gap-2 text-muted-foreground">
<Skeleton className="h-4 w-4 rounded-full" />
Loading methods...
</div>
</TableCell>
</TableRow>
) : shippingMethods.length > 0 ? (
shippingMethods.map((method, index) => (
<motion.tr
key={method._id}
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0 }}
transition={{ duration: 0.2, delay: index * 0.05 }}
className="group hover:bg-muted/40 border-b border-border/50 transition-colors"
>
<TableCell className="font-medium pl-6">
<div className="flex items-center gap-3">
<div className="h-8 w-8 rounded bg-primary/10 flex items-center justify-center">
<Truck className="h-4 w-4 text-primary" />
</div>
{method.name}
</div>
</TableCell>
<TableCell className="text-center font-mono">£{method.price}</TableCell>
<TableCell className="text-right pr-6">
<div className="flex justify-end gap-1">
<Button
size="icon"
variant="ghost"
className="h-8 w-8 text-muted-foreground hover:text-primary hover:bg-primary/10 transition-colors"
onClick={() => onEditShipping(method)}
>
<Edit className="h-4 w-4" />
</Button>
<Button
size="icon"
variant="ghost"
className="h-8 w-8 text-muted-foreground hover:text-destructive hover:bg-destructive/10 transition-colors"
onClick={() => onDeleteShipping(method._id ?? "")}
>
<Trash className="h-4 w-4" />
</Button>
</div>
</TableCell>
</motion.tr>
))
) : (
<TableRow>
<TableCell colSpan={3} className="h-32 text-center text-muted-foreground">
<div className="flex flex-col items-center justify-center gap-2">
<PackageX className="h-8 w-8 opacity-20" />
<p>No shipping methods found</p>
</div>
</TableCell>
</TableRow>
)}
</AnimatePresence>
)}
</TableBody>
</Table>
</div>

View File

@@ -1,4 +1,4 @@
{
"commitHash": "7b95589",
"buildTime": "2026-01-12T06:32:31.897Z"
"commitHash": "064cd7a",
"buildTime": "2026-01-12T08:43:31.133Z"
}