250 lines
9.2 KiB
TypeScript
250 lines
9.2 KiB
TypeScript
|
|
import { useQuery } from '@tanstack/react-query'
|
||
|
|
import { Users, FileText, Package, DollarSign, TrendingUp, Clock } from 'lucide-react'
|
||
|
|
import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, BarChart, Bar } from 'recharts'
|
||
|
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
|
||
|
|
import { Badge } from '@/components/ui/badge'
|
||
|
|
import { analyticsApi, type DashboardData, type ChartDataPoint } from '@/api/analytics'
|
||
|
|
import { useAuthStore } from '@/stores/authStore'
|
||
|
|
|
||
|
|
export default function DashboardPage() {
|
||
|
|
const { isAuthenticated, token } = useAuthStore()
|
||
|
|
|
||
|
|
// Only make requests if authenticated and token exists
|
||
|
|
const isReady = isAuthenticated && !!token && !!localStorage.getItem('admin_token')
|
||
|
|
|
||
|
|
const { data: dashboardData, isLoading: dashboardLoading } = useQuery<DashboardData>({
|
||
|
|
queryKey: ['dashboard'],
|
||
|
|
queryFn: analyticsApi.getDashboard,
|
||
|
|
enabled: isReady,
|
||
|
|
})
|
||
|
|
|
||
|
|
const { data: usersChartData } = useQuery<{ data: ChartDataPoint[] }>({
|
||
|
|
queryKey: ['users-chart'],
|
||
|
|
queryFn: analyticsApi.getUsersChart,
|
||
|
|
enabled: isReady,
|
||
|
|
})
|
||
|
|
|
||
|
|
const { data: revenueChartData } = useQuery<{ data: ChartDataPoint[] }>({
|
||
|
|
queryKey: ['revenue-chart'],
|
||
|
|
queryFn: analyticsApi.getRevenueChart,
|
||
|
|
enabled: isReady,
|
||
|
|
})
|
||
|
|
|
||
|
|
if (dashboardLoading) {
|
||
|
|
return (
|
||
|
|
<div className="space-y-6">
|
||
|
|
<div>
|
||
|
|
<h1 className="text-3xl font-bold tracking-tight">Dashboard</h1>
|
||
|
|
<p className="text-muted-foreground">
|
||
|
|
Welcome to Mnemo Cards Admin Panel
|
||
|
|
</p>
|
||
|
|
</div>
|
||
|
|
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-4">
|
||
|
|
{[...Array(4)].map((_, i) => (
|
||
|
|
<Card key={i}>
|
||
|
|
<CardContent className="pt-6">
|
||
|
|
<div className="animate-pulse">
|
||
|
|
<div className="h-4 bg-gray-200 rounded w-3/4 mb-2"></div>
|
||
|
|
<div className="h-8 bg-gray-200 rounded w-1/2"></div>
|
||
|
|
</div>
|
||
|
|
</CardContent>
|
||
|
|
</Card>
|
||
|
|
))}
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
)
|
||
|
|
}
|
||
|
|
|
||
|
|
return (
|
||
|
|
<div className="space-y-6">
|
||
|
|
<div>
|
||
|
|
<h1 className="text-3xl font-bold tracking-tight">Dashboard</h1>
|
||
|
|
<p className="text-muted-foreground">
|
||
|
|
Welcome to Mnemo Cards Admin Panel
|
||
|
|
</p>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
{/* Stats Cards */}
|
||
|
|
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-4">
|
||
|
|
<Card>
|
||
|
|
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||
|
|
<CardTitle className="text-sm font-medium">Total Users</CardTitle>
|
||
|
|
<Users className="h-4 w-4 text-muted-foreground" />
|
||
|
|
</CardHeader>
|
||
|
|
<CardContent>
|
||
|
|
<div className="text-2xl font-bold">{dashboardData?.stats.users || 0}</div>
|
||
|
|
<p className="text-xs text-muted-foreground">
|
||
|
|
Registered users
|
||
|
|
</p>
|
||
|
|
</CardContent>
|
||
|
|
</Card>
|
||
|
|
|
||
|
|
<Card>
|
||
|
|
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||
|
|
<CardTitle className="text-sm font-medium">Total Cards</CardTitle>
|
||
|
|
<FileText className="h-4 w-4 text-muted-foreground" />
|
||
|
|
</CardHeader>
|
||
|
|
<CardContent>
|
||
|
|
<div className="text-2xl font-bold">{dashboardData?.stats.cards || 0}</div>
|
||
|
|
<p className="text-xs text-muted-foreground">
|
||
|
|
Game cards created
|
||
|
|
</p>
|
||
|
|
</CardContent>
|
||
|
|
</Card>
|
||
|
|
|
||
|
|
<Card>
|
||
|
|
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||
|
|
<CardTitle className="text-sm font-medium">Active Packs</CardTitle>
|
||
|
|
<Package className="h-4 w-4 text-muted-foreground" />
|
||
|
|
</CardHeader>
|
||
|
|
<CardContent>
|
||
|
|
<div className="text-2xl font-bold">{dashboardData?.stats.enabledPacks || 0}</div>
|
||
|
|
<p className="text-xs text-muted-foreground">
|
||
|
|
of {dashboardData?.stats.packs || 0} total packs
|
||
|
|
</p>
|
||
|
|
</CardContent>
|
||
|
|
</Card>
|
||
|
|
|
||
|
|
<Card>
|
||
|
|
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||
|
|
<CardTitle className="text-sm font-medium">Total Payments</CardTitle>
|
||
|
|
<DollarSign className="h-4 w-4 text-muted-foreground" />
|
||
|
|
</CardHeader>
|
||
|
|
<CardContent>
|
||
|
|
<div className="text-2xl font-bold">{dashboardData?.stats.payments || 0}</div>
|
||
|
|
<p className="text-xs text-muted-foreground">
|
||
|
|
Successful transactions
|
||
|
|
</p>
|
||
|
|
</CardContent>
|
||
|
|
</Card>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
{/* Charts */}
|
||
|
|
<div className="grid gap-4 md:grid-cols-2">
|
||
|
|
<Card>
|
||
|
|
<CardHeader>
|
||
|
|
<CardTitle>User Registrations</CardTitle>
|
||
|
|
<CardDescription>
|
||
|
|
Daily user registrations for the last 30 days
|
||
|
|
</CardDescription>
|
||
|
|
</CardHeader>
|
||
|
|
<CardContent>
|
||
|
|
{usersChartData?.data && (
|
||
|
|
<ResponsiveContainer width="100%" height={300}>
|
||
|
|
<LineChart data={usersChartData.data}>
|
||
|
|
<CartesianGrid strokeDasharray="3 3" />
|
||
|
|
<XAxis
|
||
|
|
dataKey="date"
|
||
|
|
tickFormatter={(value) => new Date(value).toLocaleDateString('en-US', { month: 'short', day: 'numeric' })}
|
||
|
|
/>
|
||
|
|
<YAxis />
|
||
|
|
<Tooltip
|
||
|
|
labelFormatter={(value) => new Date(value).toLocaleDateString()}
|
||
|
|
formatter={(value: number) => [value, 'Registrations']}
|
||
|
|
/>
|
||
|
|
<Line
|
||
|
|
type="monotone"
|
||
|
|
dataKey="registrations"
|
||
|
|
stroke="#8884d8"
|
||
|
|
strokeWidth={2}
|
||
|
|
dot={{ fill: '#8884d8' }}
|
||
|
|
/>
|
||
|
|
</LineChart>
|
||
|
|
</ResponsiveContainer>
|
||
|
|
)}
|
||
|
|
</CardContent>
|
||
|
|
</Card>
|
||
|
|
|
||
|
|
<Card>
|
||
|
|
<CardHeader>
|
||
|
|
<CardTitle>Revenue Trend</CardTitle>
|
||
|
|
<CardDescription>
|
||
|
|
Daily revenue for the last 30 days
|
||
|
|
</CardDescription>
|
||
|
|
</CardHeader>
|
||
|
|
<CardContent>
|
||
|
|
{revenueChartData?.data && (
|
||
|
|
<ResponsiveContainer width="100%" height={300}>
|
||
|
|
<BarChart data={revenueChartData.data}>
|
||
|
|
<CartesianGrid strokeDasharray="3 3" />
|
||
|
|
<XAxis
|
||
|
|
dataKey="date"
|
||
|
|
tickFormatter={(value) => new Date(value).toLocaleDateString('en-US', { month: 'short', day: 'numeric' })}
|
||
|
|
/>
|
||
|
|
<YAxis />
|
||
|
|
<Tooltip
|
||
|
|
labelFormatter={(value) => new Date(value).toLocaleDateString()}
|
||
|
|
formatter={(value: number) => [`$${value}`, 'Revenue']}
|
||
|
|
/>
|
||
|
|
<Bar dataKey="revenue" fill="#82ca9d" />
|
||
|
|
</BarChart>
|
||
|
|
</ResponsiveContainer>
|
||
|
|
)}
|
||
|
|
</CardContent>
|
||
|
|
</Card>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
{/* Recent Users and Top Packs */}
|
||
|
|
<div className="grid gap-4 md:grid-cols-2">
|
||
|
|
<Card>
|
||
|
|
<CardHeader>
|
||
|
|
<CardTitle className="flex items-center gap-2">
|
||
|
|
<Clock className="h-5 w-5" />
|
||
|
|
Recent Users
|
||
|
|
</CardTitle>
|
||
|
|
<CardDescription>
|
||
|
|
Latest user registrations
|
||
|
|
</CardDescription>
|
||
|
|
</CardHeader>
|
||
|
|
<CardContent>
|
||
|
|
<div className="space-y-4">
|
||
|
|
{dashboardData?.recentUsers.slice(0, 5).map((user) => (
|
||
|
|
<div key={user.id} className="flex items-center justify-between">
|
||
|
|
<div>
|
||
|
|
<p className="font-medium">{user.name || 'No name'}</p>
|
||
|
|
<p className="text-sm text-muted-foreground">{user.email || 'No email'}</p>
|
||
|
|
</div>
|
||
|
|
<div className="text-sm text-muted-foreground">
|
||
|
|
{user.createdAt ? new Date(user.createdAt).toLocaleDateString() : 'N/A'}
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
)) || (
|
||
|
|
<p className="text-muted-foreground">No recent users</p>
|
||
|
|
)}
|
||
|
|
</div>
|
||
|
|
</CardContent>
|
||
|
|
</Card>
|
||
|
|
|
||
|
|
<Card>
|
||
|
|
<CardHeader>
|
||
|
|
<CardTitle className="flex items-center gap-2">
|
||
|
|
<TrendingUp className="h-5 w-5" />
|
||
|
|
Popular Packs
|
||
|
|
</CardTitle>
|
||
|
|
<CardDescription>
|
||
|
|
Most popular card packs
|
||
|
|
</CardDescription>
|
||
|
|
</CardHeader>
|
||
|
|
<CardContent>
|
||
|
|
<div className="space-y-4">
|
||
|
|
{dashboardData?.topPacks.map((pack) => (
|
||
|
|
<div key={pack.id} className="flex items-center justify-between">
|
||
|
|
<div>
|
||
|
|
<p className="font-medium">{pack.title}</p>
|
||
|
|
<p className="text-sm text-muted-foreground">{pack.cards} cards</p>
|
||
|
|
</div>
|
||
|
|
<Badge variant={pack.enabled ? "default" : "secondary"}>
|
||
|
|
{pack.enabled ? 'Active' : 'Disabled'}
|
||
|
|
</Badge>
|
||
|
|
</div>
|
||
|
|
)) || (
|
||
|
|
<p className="text-muted-foreground">No packs available</p>
|
||
|
|
)}
|
||
|
|
</div>
|
||
|
|
</CardContent>
|
||
|
|
</Card>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
)
|
||
|
|
}
|