chore: add client/src/pages/admin/dashboard.tsx
This commit is contained in:
parent
3f899d558d
commit
771d2001db
1 changed files with 125 additions and 0 deletions
125
client/src/pages/admin/dashboard.tsx
Normal file
125
client/src/pages/admin/dashboard.tsx
Normal file
|
|
@ -0,0 +1,125 @@
|
||||||
|
import { useQuery } from "@tanstack/react-query";
|
||||||
|
import { Link, useLocation } from "wouter";
|
||||||
|
import { ShoppingBag, Users, Package, DollarSign, LayoutDashboard, Tag, LogOut } from "lucide-react";
|
||||||
|
import { apiRequest, queryClient } from "../../lib/queryClient";
|
||||||
|
|
||||||
|
function AdminNav({ active }: { active: string }) {
|
||||||
|
const [, setLocation] = useLocation();
|
||||||
|
const navItems = [
|
||||||
|
{ href: "/admin", label: "Dashboard", icon: LayoutDashboard },
|
||||||
|
{ href: "/admin/orders", label: "Orders", icon: ShoppingBag },
|
||||||
|
{ href: "/admin/products", label: "Products", icon: Package },
|
||||||
|
{ href: "/admin/customers", label: "Customers", icon: Users },
|
||||||
|
{ href: "/admin/discounts", label: "Discounts", icon: Tag },
|
||||||
|
];
|
||||||
|
|
||||||
|
const handleLogout = async () => {
|
||||||
|
await apiRequest("POST", "/api/admin/logout");
|
||||||
|
await queryClient.invalidateQueries();
|
||||||
|
setLocation("/admin/login");
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<aside className="w-56 flex-shrink-0 bg-card border-r border-border flex flex-col">
|
||||||
|
<div className="p-5 border-b border-border">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<ShoppingBag size={20} className="text-primary" />
|
||||||
|
<span className="font-bold text-sm">Store Admin</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<nav className="flex-1 p-3 space-y-1">
|
||||||
|
{navItems.map((item) => {
|
||||||
|
const Icon = item.icon;
|
||||||
|
const isActive = active === item.href;
|
||||||
|
return (
|
||||||
|
<Link key={item.href} href={item.href}
|
||||||
|
className={`flex items-center gap-2.5 px-3 py-2 rounded-lg text-sm font-medium transition-colors ${isActive ? "bg-primary/10 text-primary" : "text-muted-foreground hover:bg-muted hover:text-foreground"}`}
|
||||||
|
data-testid={`nav-${item.label.toLowerCase()}`}>
|
||||||
|
<Icon size={15} />
|
||||||
|
{item.label}
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</nav>
|
||||||
|
<div className="p-3 border-t border-border">
|
||||||
|
<button onClick={handleLogout} className="flex items-center gap-2 px-3 py-2 rounded-lg text-sm text-muted-foreground hover:text-foreground hover:bg-muted w-full transition-colors">
|
||||||
|
<LogOut size={15} /> Sign out
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</aside>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Stats {
|
||||||
|
totalOrders: number;
|
||||||
|
totalRevenue: number;
|
||||||
|
totalCustomers: number;
|
||||||
|
activeProducts: number;
|
||||||
|
recentOrders: Array<{ id: number; orderNumber: string; email: string; total: string; status: string; createdAt: string }>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function AdminDashboard() {
|
||||||
|
const { data: stats, isLoading } = useQuery<Stats>({
|
||||||
|
queryKey: ["/api/admin/stats"],
|
||||||
|
queryFn: async () => { const r = await fetch("/api/admin/stats", { credentials: "include" }); if (r.status === 401) window.location.href = "/admin/login"; return r.json(); },
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex h-screen overflow-hidden" data-testid="admin-dashboard">
|
||||||
|
<AdminNav active="/admin" />
|
||||||
|
<main className="flex-1 overflow-y-auto p-8">
|
||||||
|
<h1 className="text-2xl font-bold mb-6">Dashboard</h1>
|
||||||
|
{isLoading ? (
|
||||||
|
<div className="grid grid-cols-2 lg:grid-cols-4 gap-4">
|
||||||
|
{[1, 2, 3, 4].map((i) => <div key={i} className="h-28 bg-muted rounded-xl animate-pulse" />)}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<div className="grid grid-cols-2 lg:grid-cols-4 gap-4 mb-8">
|
||||||
|
{[
|
||||||
|
{ label: "Total Revenue", value: `$${(stats?.totalRevenue || 0).toLocaleString("en-US", { minimumFractionDigits: 2 })}`, icon: DollarSign, color: "text-green-600 bg-green-100" },
|
||||||
|
{ label: "Total Orders", value: stats?.totalOrders || 0, icon: ShoppingBag, color: "text-blue-600 bg-blue-100" },
|
||||||
|
{ label: "Customers", value: stats?.totalCustomers || 0, icon: Users, color: "text-purple-600 bg-purple-100" },
|
||||||
|
{ label: "Active Products", value: stats?.activeProducts || 0, icon: Package, color: "text-orange-600 bg-orange-100" },
|
||||||
|
].map((stat) => {
|
||||||
|
const Icon = stat.icon;
|
||||||
|
return (
|
||||||
|
<div key={stat.label} className="bg-card border border-border rounded-xl p-5">
|
||||||
|
<div className={`w-10 h-10 rounded-lg flex items-center justify-center mb-3 ${stat.color}`}>
|
||||||
|
<Icon size={18} />
|
||||||
|
</div>
|
||||||
|
<p className="text-2xl font-bold">{stat.value}</p>
|
||||||
|
<p className="text-sm text-muted-foreground">{stat.label}</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="bg-card border border-border rounded-xl">
|
||||||
|
<div className="p-5 border-b border-border flex items-center justify-between">
|
||||||
|
<h2 className="font-semibold">Recent Orders</h2>
|
||||||
|
<Link href="/admin/orders" className="text-sm text-primary hover:underline">View all</Link>
|
||||||
|
</div>
|
||||||
|
<div className="divide-y divide-border">
|
||||||
|
{stats?.recentOrders.map((order) => (
|
||||||
|
<div key={order.id} className="px-5 py-3 flex items-center justify-between gap-4">
|
||||||
|
<div>
|
||||||
|
<p className="font-mono text-sm font-medium">#{order.orderNumber}</p>
|
||||||
|
<p className="text-xs text-muted-foreground">{order.email}</p>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<span className={`text-xs px-2 py-0.5 rounded-full font-medium ${order.status === "confirmed" ? "bg-blue-100 text-blue-700" : "bg-muted text-muted-foreground"}`}>{order.status}</span>
|
||||||
|
<span className="font-semibold text-sm">${parseFloat(order.total).toFixed(2)}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export { AdminNav };
|
||||||
Loading…
Add table
Reference in a new issue