Files
ember-market-frontend/app/dashboard/shipping/page.tsx
g 73adbe5d07
All checks were successful
Build Frontend / build (push) Successful in 1m4s
Enhance admin dashboard UI and tables with new styles
Refactors admin dashboard, users, vendors, shipping, and stock pages to improve UI consistency and visual clarity. Adds new icons, animated transitions, and card styles for stats and tables. Updates table row rendering with framer-motion for smooth animations, improves badge and button styling, and enhances search/filter inputs. Refines loading skeletons and overall layout for a more modern, accessible admin experience.
2026-01-12 07:16:33 +00:00

320 lines
9.8 KiB
TypeScript

"use client";
import { useState, useEffect, ChangeEvent, Suspense } from "react";
import { useRouter } from "next/navigation";
import Layout from "@/components/layout/layout";
import { Edit, Plus, Trash, Truck } from "lucide-react";
import { Button } from "@/components/ui/button";
import { Skeleton } from "@/components/ui/skeleton";
import {
fetchShippingMethods,
addShippingMethod,
deleteShippingMethod,
updateShippingMethod,
ShippingMethod,
ShippingData
} from "@/lib/services/shipping-service";
import dynamic from "next/dynamic";
import { Card, CardContent, CardHeader } from "@/components/ui/card";
// 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: () => (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-background/80 backdrop-blur-sm">
<Card className="w-full max-w-md m-4 animate-in fade-in zoom-in-95 duration-300">
<CardHeader>
<Skeleton className="h-6 w-48" />
<Skeleton className="h-4 w-64 mt-2" />
</CardHeader>
<CardContent>
<div className="space-y-4">
{[...Array(3)].map((_, i) => (
<div key={i} className="space-y-2">
<Skeleton className="h-4 w-24" />
<Skeleton className="h-10 w-full rounded-md" />
</div>
))}
</div>
</CardContent>
</Card>
</div>
)
});
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: () => <ShippingTableSkeleton />
});
// Loading skeleton for shipping table
function ShippingTableSkeleton() {
return (
<Card className="animate-in fade-in duration-500 relative border-border/40 bg-background/50 backdrop-blur-sm shadow-sm">
{/* Subtle loading indicator */}
<div className="absolute top-0 left-0 right-0 h-1 bg-muted overflow-hidden rounded-t-lg">
<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>
<CardHeader>
<div className="flex items-center justify-between">
<Skeleton className="h-6 w-32" />
<Skeleton className="h-9 w-24 rounded-md" />
</div>
</CardHeader>
<CardContent>
<div className="border rounded-lg">
<div className="border-b p-4">
<div className="flex items-center gap-4">
{['Method Name', 'Price', 'Actions'].map((header, i) => (
<Skeleton
key={i}
className="h-4 w-20 flex-1 animate-in fade-in"
style={{
animationDelay: `${i * 50}ms`,
animationDuration: '300ms',
animationFillMode: 'both',
}}
/>
))}
</div>
</div>
{[...Array(4)].map((_, i) => (
<div
key={i}
className="border-b last:border-b-0 p-4 animate-in fade-in"
style={{
animationDelay: `${200 + i * 50}ms`,
animationDuration: '300ms',
animationFillMode: 'both',
}}
>
<div className="flex items-center gap-4">
<Skeleton className="h-4 w-32 flex-1" />
<Skeleton className="h-4 w-16 flex-1" />
<div className="flex gap-2 flex-1">
<Skeleton className="h-8 w-16 rounded-md" />
<Skeleton className="h-8 w-16 rounded-md" />
</div>
</div>
</div>
))}
</div>
</CardContent>
</Card>
);
}
export default function ShippingPage() {
const [shippingMethods, setShippingMethods] = useState<ShippingMethod[]>([]);
const [newShipping, setNewShipping] = useState<ShippingData>({
name: "",
price: 0,
});
const [loading, setLoading] = useState<boolean>(true);
const [modalOpen, setModalOpen] = useState<boolean>(false);
const [editing, setEditing] = useState<boolean>(false);
const [refreshTrigger, setRefreshTrigger] = useState<number>(0);
const router = useRouter();
const refreshShippingMethods = () => {
setRefreshTrigger(prev => prev + 1);
};
useEffect(() => {
const fetchShippingMethodsData = async () => {
try {
setLoading(true);
const authToken = document.cookie
.split("; ")
.find((row) => row.startsWith("Authorization="))
?.split("=")[1];
if (!authToken) {
router.push("/login");
return;
}
const fetchedMethods: ShippingMethod[] = await fetchShippingMethods(
authToken
);
const sanitizedMethods: ShippingMethod[] = fetchedMethods.map(
(method) => ({
...method,
_id: method._id ?? "",
})
);
console.log("Fetched Shipping Methods:", sanitizedMethods);
setShippingMethods(sanitizedMethods);
} catch (error) {
console.error("Error loading shipping options:", error);
} finally {
setLoading(false);
}
};
fetchShippingMethodsData();
}, [refreshTrigger]); // Add refreshTrigger as a dependency
const handleAddShipping = async () => {
if (!newShipping.name || !newShipping.price) return;
try {
setLoading(true);
const authToken = document.cookie
.split("; ")
.find((row) => row.startsWith("Authorization="))
?.split("=")[1];
if (!authToken) {
console.error("No auth token found");
return;
}
console.log("Sending request to add shipping method:", newShipping);
const response = await addShippingMethod(
newShipping,
authToken
);
console.log("Add shipping method response:", response);
// Close modal and reset form before refreshing to avoid UI delays
setModalOpen(false);
setNewShipping({ name: "", price: 0 });
// Refresh the list after adding
refreshShippingMethods();
console.log("Shipping method added successfully");
} catch (error) {
console.error("Error adding shipping method:", error);
alert("Failed to add shipping method. Please try again.");
} finally {
setLoading(false);
}
};
const handleUpdateShipping = async () => {
if (!newShipping.name || !newShipping.price || !newShipping._id) return; // Ensure `_id` exists
try {
setLoading(true);
const authToken = document.cookie
.split("; ")
.find((row) => row.startsWith("Authorization="))
?.split("=")[1];
if (!authToken) {
console.error("No auth token found");
return;
}
await updateShippingMethod(
newShipping._id,
newShipping,
authToken
);
// Close modal and reset form before refreshing to avoid UI delays
setModalOpen(false);
setNewShipping({ name: "", price: 0 });
setEditing(false);
// Refresh the list after updating
refreshShippingMethods();
console.log("Shipping method updated successfully");
} catch (error) {
console.error("Error updating shipping method:", error);
alert("Failed to update shipping method. Please try again.");
} finally {
setLoading(false);
}
};
const handleDeleteShipping = async (_id: string) => {
try {
const authToken = document.cookie.split("Authorization=")[1];
const response = await deleteShippingMethod(_id, authToken);
if (response.success) {
refreshShippingMethods(); // Refresh the list after deleting
} else {
console.error("Deletion was not successful.");
}
} catch (error) {
console.error("Error deleting shipping method:", error);
}
};
const handleEditShipping = (shipping: ShippingMethod) => {
setNewShipping({
...shipping,
_id: shipping._id ?? "", // ✅ Ensure _id is always a string
});
setEditing(true);
setModalOpen(true);
};
return (
<Layout>
<div className="space-y-6">
<div className="flex items-center justify-between">
<h1 className="text-2xl font-semibold text-gray-900 dark:text-white flex items-center">
<Truck className="mr-2 h-6 w-6" />
Shipping Methods
</h1>
<Button onClick={() => {
setNewShipping({ name: "", price: 0 });
setEditing(false);
setModalOpen(true);
}}>
<Plus className="mr-2 h-5 w-5" />
Add Shipping Method
</Button>
</div>
{/* Shipping Methods Table */}
<Suspense fallback={<ShippingTableSkeleton />}>
<ShippingTable
shippingMethods={shippingMethods}
loading={loading}
onEditShipping={handleEditShipping}
onDeleteShipping={handleDeleteShipping}
/>
</Suspense>
</div>
{/* Shipping Modal */}
<ShippingModal
open={modalOpen}
onClose={() => {
setNewShipping({ name: "", price: 0 });
setEditing(false);
setModalOpen(false);
}}
onSave={editing ? handleUpdateShipping : handleAddShipping}
shippingData={newShipping}
editing={editing}
handleChange={(e) =>
setNewShipping({ ...newShipping, [e.target.name]: e.target.value })
}
setShippingData={setNewShipping}
/>
</Layout>
);
}