first commit
This commit is contained in:
253
app/admin/page.tsx
Normal file
253
app/admin/page.tsx
Normal file
@@ -0,0 +1,253 @@
|
||||
'use client'
|
||||
|
||||
import { useEffect, useState } from 'react'
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
import { Users, Package, ShoppingCart, DollarSign, TrendingUp, TrendingDown } from 'lucide-react'
|
||||
import { motion } from 'framer-motion'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
|
||||
interface DashboardStats {
|
||||
totalUsers: number
|
||||
totalProducts: number
|
||||
totalOrders: number
|
||||
totalRevenue: number
|
||||
userGrowth: number
|
||||
productGrowth: number
|
||||
orderGrowth: number
|
||||
revenueGrowth: number
|
||||
}
|
||||
|
||||
interface RecentOrder {
|
||||
id: string
|
||||
total: number
|
||||
status: string
|
||||
createdAt: string
|
||||
user: {
|
||||
name: string
|
||||
email: string
|
||||
}
|
||||
}
|
||||
|
||||
interface TopProduct {
|
||||
id: string
|
||||
name: string
|
||||
price: number
|
||||
orderCount: number
|
||||
totalRevenue: number
|
||||
}
|
||||
|
||||
export default function AdminDashboard() {
|
||||
const [stats, setStats] = useState<DashboardStats | null>(null)
|
||||
const [recentOrders, setRecentOrders] = useState<RecentOrder[]>([])
|
||||
const [topProducts, setTopProducts] = useState<TopProduct[]>([])
|
||||
const [loading, setLoading] = useState(true)
|
||||
|
||||
useEffect(() => {
|
||||
fetchDashboardData()
|
||||
}, [])
|
||||
|
||||
const fetchDashboardData = async () => {
|
||||
try {
|
||||
const [statsRes, ordersRes, productsRes] = await Promise.all([
|
||||
fetch('/api/admin/dashboard/stats'),
|
||||
fetch('/api/admin/dashboard/recent-orders'),
|
||||
fetch('/api/admin/dashboard/top-products')
|
||||
])
|
||||
|
||||
const [statsData, ordersData, productsData] = await Promise.all([
|
||||
statsRes.json(),
|
||||
ordersRes.json(),
|
||||
productsRes.json()
|
||||
])
|
||||
|
||||
setStats(statsData)
|
||||
setRecentOrders(ordersData.orders || [])
|
||||
setTopProducts(productsData.products || [])
|
||||
} catch (error) {
|
||||
console.error('Error fetching dashboard data:', error)
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const getStatusColor = (status: string) => {
|
||||
switch (status) {
|
||||
case 'PENDING': return 'bg-yellow-100 text-yellow-800'
|
||||
case 'PAID': return 'bg-blue-100 text-blue-800'
|
||||
case 'SHIPPED': return 'bg-purple-100 text-purple-800'
|
||||
case 'DELIVERED': return 'bg-green-100 text-green-800'
|
||||
case 'CANCELLED': return 'bg-red-100 text-red-800'
|
||||
default: return 'bg-gray-100 text-gray-800'
|
||||
}
|
||||
}
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-br from-gray-50 to-gray-100 flex items-center justify-center">
|
||||
<div className="w-16 h-16 border-4 border-gray-200 border-t-blue-500 rounded-full animate-spin"></div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const dashboardStats = stats ? [
|
||||
{
|
||||
title: 'Total Users',
|
||||
value: stats.totalUsers.toLocaleString(),
|
||||
change: `${stats.userGrowth >= 0 ? '+' : ''}${stats.userGrowth.toFixed(1)}%`,
|
||||
trend: stats.userGrowth >= 0 ? 'up' : 'down',
|
||||
icon: Users,
|
||||
},
|
||||
{
|
||||
title: 'Products',
|
||||
value: stats.totalProducts.toLocaleString(),
|
||||
change: `${stats.productGrowth >= 0 ? '+' : ''}${stats.productGrowth.toFixed(1)}%`,
|
||||
trend: stats.productGrowth >= 0 ? 'up' : 'down',
|
||||
icon: Package,
|
||||
},
|
||||
{
|
||||
title: 'Orders',
|
||||
value: stats.totalOrders.toLocaleString(),
|
||||
change: `${stats.orderGrowth >= 0 ? '+' : ''}${stats.orderGrowth.toFixed(1)}%`,
|
||||
trend: stats.orderGrowth >= 0 ? 'up' : 'down',
|
||||
icon: ShoppingCart,
|
||||
},
|
||||
{
|
||||
title: 'Revenue',
|
||||
value: `₹${stats.totalRevenue.toLocaleString()}`,
|
||||
change: `${stats.revenueGrowth >= 0 ? '+' : ''}${stats.revenueGrowth.toFixed(1)}%`,
|
||||
trend: stats.revenueGrowth >= 0 ? 'up' : 'down',
|
||||
icon: DollarSign,
|
||||
},
|
||||
] : []
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-br from-gray-50 via-white to-gray-100">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8 space-y-8">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold text-gray-900">Admin Dashboard</h1>
|
||||
<p className="text-gray-600 mt-2">Welcome to your admin dashboard</p>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-4">
|
||||
{dashboardStats.map((stat, index) => (
|
||||
<motion.div
|
||||
key={stat.title}
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ delay: index * 0.1 }}
|
||||
>
|
||||
<Card className="bg-white/80 backdrop-blur-sm shadow-lg border-0">
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<CardTitle className="text-sm font-medium text-gray-600">
|
||||
{stat.title}
|
||||
</CardTitle>
|
||||
<stat.icon className="h-5 w-5 text-gray-400" />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold text-gray-900">{stat.value}</div>
|
||||
<p className="text-xs text-muted-foreground flex items-center mt-2">
|
||||
{stat.trend === 'up' ? (
|
||||
<TrendingUp className="h-3 w-3 text-green-500 mr-1" />
|
||||
) : (
|
||||
<TrendingDown className="h-3 w-3 text-red-500 mr-1" />
|
||||
)}
|
||||
<span className={stat.trend === 'up' ? 'text-green-600 font-medium' : 'text-red-600 font-medium'}>
|
||||
{stat.change}
|
||||
</span>
|
||||
<span className="ml-1 text-gray-500">from last month</span>
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</motion.div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 gap-8 lg:grid-cols-2">
|
||||
{/* Recent Orders */}
|
||||
<motion.div
|
||||
initial={{ opacity: 0, x: -20 }}
|
||||
animate={{ opacity: 1, x: 0 }}
|
||||
transition={{ delay: 0.4 }}
|
||||
>
|
||||
<Card className="bg-white/80 backdrop-blur-sm shadow-lg border-0">
|
||||
<CardHeader>
|
||||
<CardTitle>Recent Orders</CardTitle>
|
||||
<CardDescription>Latest orders from your store</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{recentOrders.length === 0 ? (
|
||||
<div className="text-center py-8">
|
||||
<ShoppingCart className="h-12 w-12 text-gray-400 mx-auto mb-4" />
|
||||
<p className="text-gray-500">No recent orders</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-4">
|
||||
{recentOrders.map((order) => (
|
||||
<div key={order.id} className="flex items-center justify-between p-3 bg-gray-50 rounded-lg">
|
||||
<div className="flex-1">
|
||||
<div className="flex items-center space-x-2 mb-1">
|
||||
<p className="font-medium">Order #{order.id.slice(-8)}</p>
|
||||
<Badge className={getStatusColor(order.status)}>
|
||||
{order.status}
|
||||
</Badge>
|
||||
</div>
|
||||
<p className="text-sm text-gray-500">{order.user.name}</p>
|
||||
<p className="text-xs text-gray-400">
|
||||
{new Date(order.createdAt).toLocaleDateString()}
|
||||
</p>
|
||||
</div>
|
||||
<span className="font-bold text-green-600">₹{order.total.toFixed(2)}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</motion.div>
|
||||
|
||||
{/* Top Products */}
|
||||
<motion.div
|
||||
initial={{ opacity: 0, x: 20 }}
|
||||
animate={{ opacity: 1, x: 0 }}
|
||||
transition={{ delay: 0.5 }}
|
||||
>
|
||||
<Card className="bg-white/80 backdrop-blur-sm shadow-lg border-0">
|
||||
<CardHeader>
|
||||
<CardTitle>Top Products</CardTitle>
|
||||
<CardDescription>Best selling products this month</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{topProducts.length === 0 ? (
|
||||
<div className="text-center py-8">
|
||||
<Package className="h-12 w-12 text-gray-400 mx-auto mb-4" />
|
||||
<p className="text-gray-500">No product data available</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-4">
|
||||
{topProducts.map((product, index) => (
|
||||
<div key={product.id} className="flex items-center justify-between p-3 bg-gray-50 rounded-lg">
|
||||
<div className="flex items-center space-x-3">
|
||||
<div className="w-8 h-8 bg-blue-100 rounded-full flex items-center justify-center">
|
||||
<span className="text-sm font-bold text-blue-600">#{index + 1}</span>
|
||||
</div>
|
||||
<div>
|
||||
<p className="font-medium text-gray-900">{product.name}</p>
|
||||
<p className="text-sm text-gray-500">{product.orderCount} sold</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<p className="font-bold text-gray-900">₹{product.price.toFixed(2)}</p>
|
||||
<p className="text-xs text-green-600">₹{product.totalRevenue.toFixed(0)} revenue</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</motion.div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user