Introduces reusable error boundary and suspense timeout components across dashboard pages for better error handling and user feedback. Enhances loading skeletons with subtle progress indicators, animation, and slow-loading warnings. All dynamic imports now include error handling and improved fallback skeletons, and a shared DashboardContentWrapper is added for consistent dashboard content loading experience.
320 lines
9.8 KiB
TypeScript
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">
|
|
{/* 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>
|
|
);
|
|
} |