Next.js website for Rocky Mountain Vending company featuring: - Product catalog with Stripe integration - Service areas and parts pages - Admin dashboard with Clerk authentication - SEO optimized pages with JSON-LD structured data Co-authored-by: Cursor <cursoragent@cursor.com>
412 lines
14 KiB
TypeScript
412 lines
14 KiB
TypeScript
import Link from 'next/link'
|
|
import { Button } from '@/components/ui/button'
|
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
|
|
import { Badge } from '@/components/ui/badge'
|
|
import {
|
|
ShoppingCart,
|
|
Package,
|
|
Users,
|
|
TrendingUp,
|
|
DollarSign,
|
|
Clock,
|
|
CheckCircle,
|
|
Truck,
|
|
AlertTriangle,
|
|
Settings,
|
|
BarChart3
|
|
} from 'lucide-react'
|
|
import { fetchAllProducts } from '@/lib/stripe/products'
|
|
|
|
// Mock analytics data for demo
|
|
const mockAnalytics = {
|
|
totalOrders: 156,
|
|
totalRevenue: 48567.89,
|
|
pendingOrders: 12,
|
|
completedOrders: 144,
|
|
lowStockProducts: 3,
|
|
avgOrderValue: 311.46,
|
|
conversionRate: 2.8,
|
|
monthlyGrowth: 15.2
|
|
}
|
|
|
|
async function getProductsCount() {
|
|
try {
|
|
const products = await fetchAllProducts()
|
|
return products.length
|
|
} catch {
|
|
return 0
|
|
}
|
|
}
|
|
|
|
async function getOrdersCount() {
|
|
try {
|
|
const response = await fetch('/api/orders')
|
|
if (response.ok) {
|
|
const data = await response.json()
|
|
return data.pagination.total || 0
|
|
}
|
|
} catch {}
|
|
return mockAnalytics.totalOrders
|
|
}
|
|
|
|
export default async function AdminDashboard() {
|
|
const [productsCount, ordersCount] = await Promise.all([
|
|
getProductsCount(),
|
|
getOrdersCount()
|
|
])
|
|
|
|
const dashboardCards = [
|
|
{
|
|
title: 'Total Revenue',
|
|
value: `$${mockAnalytics.totalRevenue.toLocaleString()}`,
|
|
description: 'Total revenue from all orders',
|
|
icon: DollarSign,
|
|
trend: '+15.2%',
|
|
trendPositive: true,
|
|
color: 'text-green-600'
|
|
},
|
|
{
|
|
title: 'Total Orders',
|
|
value: mockAnalytics.totalOrders.toString(),
|
|
description: 'Total number of orders',
|
|
icon: ShoppingCart,
|
|
trend: '+12.8%',
|
|
trendPositive: true,
|
|
color: 'text-blue-600'
|
|
},
|
|
{
|
|
title: 'Products',
|
|
value: productsCount.toString(),
|
|
description: 'Active products in inventory',
|
|
icon: Package,
|
|
trend: '+5',
|
|
trendPositive: true,
|
|
color: 'text-purple-600'
|
|
},
|
|
{
|
|
title: 'Pending Orders',
|
|
value: mockAnalytics.pendingOrders.toString(),
|
|
description: 'Orders awaiting processing',
|
|
icon: Clock,
|
|
trend: '-3',
|
|
trendPositive: false,
|
|
color: 'text-orange-600'
|
|
}
|
|
]
|
|
|
|
const quickStats = [
|
|
{
|
|
title: 'Average Order Value',
|
|
value: `$${mockAnalytics.avgOrderValue.toFixed(2)}`,
|
|
description: 'Average value per order',
|
|
icon: TrendingUp
|
|
},
|
|
{
|
|
title: 'Conversion Rate',
|
|
value: `${mockAnalytics.conversionRate}%`,
|
|
description: 'Visitors to orders ratio',
|
|
icon: Users
|
|
},
|
|
{
|
|
title: 'Monthly Growth',
|
|
value: `${mockAnalytics.monthlyGrowth}%`,
|
|
description: 'Revenue growth this month',
|
|
icon: BarChart3
|
|
},
|
|
{
|
|
title: 'Low Stock Alert',
|
|
value: mockAnalytics.lowStockProducts.toString(),
|
|
description: 'Products need restocking',
|
|
icon: AlertTriangle
|
|
}
|
|
]
|
|
|
|
const recentOrders = [
|
|
{
|
|
id: 'ORD-001234',
|
|
customer: 'john.doe@email.com',
|
|
amount: 2799.98,
|
|
status: 'paid',
|
|
date: '2024-01-15 10:30'
|
|
},
|
|
{
|
|
id: 'ORD-001233',
|
|
customer: 'jane.smith@email.com',
|
|
amount: 1499.99,
|
|
status: 'fulfilled',
|
|
date: '2024-01-15 09:45'
|
|
},
|
|
{
|
|
id: 'ORD-001232',
|
|
customer: 'bob.johnson@email.com',
|
|
amount: 899.97,
|
|
status: 'pending',
|
|
date: '2024-01-15 08:20'
|
|
},
|
|
{
|
|
id: 'ORD-001231',
|
|
customer: 'alice.wilson@email.com',
|
|
amount: 3499.99,
|
|
status: 'cancelled',
|
|
date: '2024-01-14 16:15'
|
|
}
|
|
]
|
|
|
|
const popularProducts = [
|
|
{
|
|
name: 'SEAGA HY900 Vending Machine',
|
|
orders: 45,
|
|
revenue: 112499.55
|
|
},
|
|
{
|
|
name: 'Vending Machine Stand',
|
|
orders: 38,
|
|
revenue: 11399.62
|
|
},
|
|
{
|
|
name: 'Snack Vending Machine Combo',
|
|
orders: 23,
|
|
revenue: 45999.77
|
|
},
|
|
{
|
|
name: 'Drink Vending Machine',
|
|
orders: 19,
|
|
revenue: 37999.81
|
|
}
|
|
]
|
|
|
|
return (
|
|
<div className="container mx-auto px-4 py-8">
|
|
<div className="space-y-8">
|
|
{/* Header */}
|
|
<div className="flex justify-between items-start">
|
|
<div>
|
|
<h1 className="text-4xl md:text-5xl font-bold tracking-tight text-balance">Admin Dashboard</h1>
|
|
<p className="text-muted-foreground mt-2">
|
|
Overview of your store performance and management tools
|
|
</p>
|
|
</div>
|
|
<div className="flex gap-2">
|
|
<Link href="/admin/products">
|
|
<Button variant="outline">
|
|
<Settings className="h-4 w-4 mr-2" />
|
|
Settings
|
|
</Button>
|
|
</Link>
|
|
<Button>Export Report</Button>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Main Stats */}
|
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
|
|
{dashboardCards.map((card, index) => {
|
|
const Icon = card.icon
|
|
return (
|
|
<Card key={index}>
|
|
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
|
<CardTitle className="text-sm font-medium">
|
|
{card.title}
|
|
</CardTitle>
|
|
<Icon className="h-4 w-4 text-muted-foreground" />
|
|
</CardHeader>
|
|
<CardContent>
|
|
<div className="text-2xl font-bold">{card.value}</div>
|
|
<div className="flex items-center gap-1 mt-2">
|
|
<span className={`text-sm ${card.color}`}>
|
|
{card.trend} {card.trendPositive ? '↑' : '↓'}
|
|
</span>
|
|
<span className="text-sm text-muted-foreground">
|
|
from last month
|
|
</span>
|
|
</div>
|
|
<p className="text-xs text-muted-foreground mt-2">
|
|
{card.description}
|
|
</p>
|
|
</CardContent>
|
|
</Card>
|
|
)
|
|
})}
|
|
</div>
|
|
|
|
{/* Quick Stats */}
|
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
|
|
{quickStats.map((stat, index) => {
|
|
const Icon = stat.icon
|
|
return (
|
|
<Card key={index}>
|
|
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
|
<CardTitle className="text-sm font-medium">
|
|
{stat.title}
|
|
</CardTitle>
|
|
<Icon className="h-4 w-4 text-muted-foreground" />
|
|
</CardHeader>
|
|
<CardContent>
|
|
<div className="text-2xl font-bold">{stat.value}</div>
|
|
<p className="text-xs text-muted-foreground mt-2">
|
|
{stat.description}
|
|
</p>
|
|
</CardContent>
|
|
</Card>
|
|
)
|
|
})}
|
|
</div>
|
|
|
|
{/* Main Content Grid */}
|
|
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8">
|
|
{/* Recent Orders */}
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle className="flex items-center justify-between">
|
|
Recent Orders
|
|
<Link href="/admin/orders">
|
|
<Button variant="outline" size="sm">View All</Button>
|
|
</Link>
|
|
</CardTitle>
|
|
<CardDescription>
|
|
Latest customer orders and their status
|
|
</CardDescription>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<div className="space-y-4">
|
|
{recentOrders.map((order) => (
|
|
<div key={order.id} className="flex items-center justify-between py-3 border-b last:border-b-0">
|
|
<div className="flex items-center gap-3">
|
|
<div className="min-w-0 flex-1">
|
|
<div className="font-medium">{order.id}</div>
|
|
<div className="text-sm text-muted-foreground truncate max-w-[150px]">
|
|
{order.customer}
|
|
</div>
|
|
<div className="text-xs text-muted-foreground">
|
|
{order.date}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div className="text-right">
|
|
<div className="font-medium">${order.amount.toFixed(2)}</div>
|
|
<Badge
|
|
variant={
|
|
order.status === 'paid' ? 'default' :
|
|
order.status === 'fulfilled' ? 'default' :
|
|
order.status === 'pending' ? 'secondary' : 'destructive'
|
|
}
|
|
className="mt-1"
|
|
>
|
|
{order.status.charAt(0).toUpperCase() + order.status.slice(1)}
|
|
</Badge>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
{/* Popular Products */}
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle className="flex items-center justify-between">
|
|
Popular Products
|
|
<Link href="/admin/products">
|
|
<Button variant="outline" size="sm">View All</Button>
|
|
</Link>
|
|
</CardTitle>
|
|
<CardDescription>
|
|
Top-selling products this month
|
|
</CardDescription>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<div className="space-y-4">
|
|
{popularProducts.map((product, index) => (
|
|
<div key={index} className="flex items-center justify-between py-3 border-b last:border-b-0">
|
|
<div className="flex items-center gap-3">
|
|
<div className="w-8 h-8 rounded-md bg-muted flex items-center justify-center text-xs font-bold text-muted-foreground">
|
|
{index + 1}
|
|
</div>
|
|
<div className="min-w-0">
|
|
<div className="font-medium truncate max-w-[200px]">
|
|
{product.name}
|
|
</div>
|
|
<div className="text-sm text-muted-foreground">
|
|
{product.orders} orders
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div className="text-right">
|
|
<div className="font-medium">${product.revenue.toLocaleString()}</div>
|
|
<div className="text-xs text-muted-foreground">
|
|
${(product.revenue / product.orders).toFixed(2)} avg
|
|
</div>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
</div>
|
|
|
|
{/* Quick Actions */}
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle>Quick Actions</CardTitle>
|
|
<CardDescription>
|
|
Common administrative tasks and operations
|
|
</CardDescription>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
|
|
<Link href="/admin/orders">
|
|
<Card className="h-full cursor-pointer hover:shadow-md transition-shadow">
|
|
<CardContent className="p-6 flex flex-col items-center text-center">
|
|
<ShoppingCart className="h-8 w-8 text-blue-600 mb-3" />
|
|
<h3 className="font-medium mb-1">Manage Orders</h3>
|
|
<p className="text-sm text-muted-foreground">
|
|
View and process customer orders
|
|
</p>
|
|
</CardContent>
|
|
</Card>
|
|
</Link>
|
|
|
|
<Link href="/admin/products">
|
|
<Card className="h-full cursor-pointer hover:shadow-md transition-shadow">
|
|
<CardContent className="p-6 flex flex-col items-center text-center">
|
|
<Package className="h-8 w-8 text-purple-600 mb-3" />
|
|
<h3 className="font-medium mb-1">Manage Products</h3>
|
|
<p className="text-sm text-muted-foreground">
|
|
Add and update product inventory
|
|
</p>
|
|
</CardContent>
|
|
</Card>
|
|
</Link>
|
|
|
|
<Link href="/orders">
|
|
<Card className="h-full cursor-pointer hover:shadow-md transition-shadow">
|
|
<CardContent className="p-6 flex flex-col items-center text-center">
|
|
<Truck className="h-8 w-8 text-green-600 mb-3" />
|
|
<h3 className="font-medium mb-1">Track Shipments</h3>
|
|
<p className="text-sm text-muted-foreground">
|
|
Monitor order shipments and deliveries
|
|
</p>
|
|
</CardContent>
|
|
</Card>
|
|
</Link>
|
|
|
|
<Card className="h-full hover:shadow-md transition-shadow">
|
|
<CardContent className="p-6 flex flex-col items-center text-center">
|
|
<CheckCircle className="h-8 w-8 text-orange-600 mb-3" />
|
|
<h3 className="font-medium mb-1">Reports</h3>
|
|
<p className="text-sm text-muted-foreground">
|
|
Generate sales and analytics reports
|
|
</p>
|
|
</CardContent>
|
|
</Card>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
export const metadata = {
|
|
title: 'Admin Dashboard | Rocky Mountain Vending',
|
|
description: 'Administrative dashboard for managing your vending machine business',
|
|
}
|