chore: add client/src/pages/admin/dashboard.tsx

This commit is contained in:
notshop 2026-04-26 16:36:05 +00:00
parent 3f899d558d
commit 771d2001db

View 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 };