292 lines
7.6 KiB
TypeScript
292 lines
7.6 KiB
TypeScript
'use client';
|
|
|
|
import { useState, useEffect } from 'react';
|
|
import { Game, User, fetchGames, fetchUsers, getDisplayGameName } from '../utils/api';
|
|
import Link from 'next/link';
|
|
import { Line, Bar } from 'react-chartjs-2';
|
|
import {
|
|
Chart as ChartJS,
|
|
CategoryScale,
|
|
LinearScale,
|
|
PointElement,
|
|
LineElement,
|
|
BarElement,
|
|
Title,
|
|
Tooltip,
|
|
Legend
|
|
} from 'chart.js';
|
|
|
|
ChartJS.register(
|
|
CategoryScale,
|
|
LinearScale,
|
|
PointElement,
|
|
LineElement,
|
|
BarElement,
|
|
Title,
|
|
Tooltip,
|
|
Legend
|
|
);
|
|
|
|
type TimeRange = '7d' | '30d' | '90d';
|
|
|
|
export default function Dashboard() {
|
|
const [users, setUsers] = useState<User[]>([]);
|
|
const [games, setGames] = useState<Game[]>([]);
|
|
const [loading, setLoading] = useState(true);
|
|
const [error, setError] = useState<string | null>(null);
|
|
const [gameTimeRange, setGameTimeRange] = useState<TimeRange>('30d');
|
|
|
|
useEffect(() => {
|
|
const loadData = async () => {
|
|
try {
|
|
const [gamesData, usersData] = await Promise.all([
|
|
fetchGames(),
|
|
fetchUsers()
|
|
]);
|
|
setGames(gamesData);
|
|
setUsers(usersData);
|
|
} catch (err) {
|
|
setError('Failed to load data. Please try again later.');
|
|
console.error('Error loading data:', err);
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
loadData();
|
|
}, []);
|
|
|
|
const getDaysFromTimeRange = (range: TimeRange): number => {
|
|
switch (range) {
|
|
case '7d': return 7;
|
|
case '30d': return 30;
|
|
case '90d': return 90;
|
|
}
|
|
};
|
|
|
|
const getGameCountData = () => {
|
|
const days = getDaysFromTimeRange(gameTimeRange);
|
|
const lastDays = Array.from({ length: days }, (_, i) => {
|
|
const date = new Date();
|
|
date.setDate(date.getDate() - i);
|
|
return date.toISOString().split('T')[0];
|
|
}).reverse();
|
|
|
|
const gameCounts = lastDays.map(date => {
|
|
return games.filter(game => {
|
|
try {
|
|
const gameDate = new Date(game.ended_time).toISOString().split('T')[0];
|
|
return gameDate === date;
|
|
} catch (err) {
|
|
console.error('Error parsing date:', err);
|
|
return false;
|
|
}
|
|
}).length;
|
|
});
|
|
|
|
return {
|
|
labels: lastDays.map(date => new Date(date).toLocaleDateString('en-US', { month: 'short', day: 'numeric' })),
|
|
datasets: [
|
|
{
|
|
label: 'Games per Day',
|
|
data: gameCounts,
|
|
borderColor: '#FFA500',
|
|
backgroundColor: 'rgba(255, 165, 0, 0.2)',
|
|
tension: 0.4,
|
|
fill: true
|
|
}
|
|
]
|
|
};
|
|
};
|
|
|
|
const getGamePopularityData = () => {
|
|
const gameTypes = Array.from(new Set(games.map(game => game.game)));
|
|
const gameCounts = gameTypes.map(type => ({
|
|
type,
|
|
displayName: getDisplayGameName(type),
|
|
count: games.filter(game => game.game === type).length
|
|
})).sort((a, b) => b.count - a.count);
|
|
|
|
return {
|
|
labels: gameCounts.map(game => game.displayName),
|
|
datasets: [
|
|
{
|
|
label: 'Games Played',
|
|
data: gameCounts.map(game => game.count),
|
|
backgroundColor: 'rgba(255, 165, 0, 0.7)',
|
|
borderColor: '#FFA500',
|
|
borderWidth: 1
|
|
}
|
|
]
|
|
};
|
|
};
|
|
|
|
const chartOptions = {
|
|
responsive: true,
|
|
plugins: {
|
|
legend: {
|
|
display: false,
|
|
labels: {
|
|
color: '#ffffff'
|
|
}
|
|
},
|
|
tooltip: {
|
|
titleColor: '#ffffff',
|
|
bodyColor: '#ffffff',
|
|
backgroundColor: 'rgba(0, 0, 0, 0.8)',
|
|
borderColor: 'rgba(255, 255, 255, 0.1)',
|
|
borderWidth: 1
|
|
}
|
|
},
|
|
scales: {
|
|
y: {
|
|
beginAtZero: true,
|
|
grid: {
|
|
color: 'rgba(255, 255, 255, 0.1)'
|
|
},
|
|
ticks: {
|
|
color: '#ffffff'
|
|
}
|
|
},
|
|
x: {
|
|
grid: {
|
|
color: 'rgba(255, 255, 255, 0.1)'
|
|
},
|
|
ticks: {
|
|
color: '#ffffff'
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
const barChartOptions = {
|
|
responsive: true,
|
|
plugins: {
|
|
legend: {
|
|
display: false,
|
|
labels: {
|
|
color: '#ffffff'
|
|
}
|
|
},
|
|
tooltip: {
|
|
titleColor: '#ffffff',
|
|
bodyColor: '#ffffff',
|
|
backgroundColor: 'rgba(0, 0, 0, 0.8)',
|
|
borderColor: 'rgba(255, 255, 255, 0.1)',
|
|
borderWidth: 1
|
|
}
|
|
},
|
|
scales: {
|
|
y: {
|
|
beginAtZero: true,
|
|
grid: {
|
|
color: 'rgba(255, 255, 255, 0.1)'
|
|
},
|
|
ticks: {
|
|
color: '#ffffff'
|
|
}
|
|
},
|
|
x: {
|
|
grid: {
|
|
color: 'rgba(255, 255, 255, 0.1)'
|
|
},
|
|
ticks: {
|
|
color: '#ffffff'
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
const TimeRangeButton = ({
|
|
range,
|
|
currentRange,
|
|
onChange
|
|
}: {
|
|
range: TimeRange;
|
|
currentRange: TimeRange;
|
|
onChange: (range: TimeRange) => void;
|
|
}) => (
|
|
<button
|
|
onClick={() => onChange(range)}
|
|
className={`px-3 py-1 text-sm rounded-md transition-colors ${
|
|
currentRange === range
|
|
? 'bg-[var(--accent)] text-white'
|
|
: 'bg-[var(--card-bg)] text-[var(--text-secondary)] hover:bg-[var(--card-border)]'
|
|
}`}
|
|
>
|
|
{range}
|
|
</button>
|
|
);
|
|
|
|
if (loading) {
|
|
return (
|
|
<div className="flex items-center justify-center min-h-screen">
|
|
<div className="text-lg text-[var(--text-primary)]">Loading...</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
if (error) {
|
|
return (
|
|
<div className="flex items-center justify-center min-h-screen">
|
|
<div className="text-[var(--accent)]">{error}</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div className="space-y-6">
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
|
{/* Users Card */}
|
|
<div className="bg-[var(--card-bg)] border border-[var(--card-border)] rounded-lg p-6">
|
|
<div className="flex justify-between items-center">
|
|
<h2 className="text-xl font-semibold text-[var(--text-primary)]">
|
|
Users - {users.length}
|
|
</h2>
|
|
<Link
|
|
href="/users"
|
|
className="text-[var(--accent)] hover:text-[var(--accent-hover)] transition-colors"
|
|
>
|
|
View All →
|
|
</Link>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Game Count Card */}
|
|
<div className="bg-[var(--card-bg)] border border-[var(--card-border)] rounded-lg p-6">
|
|
<div className="flex justify-between items-center mb-4">
|
|
<h2 className="text-xl font-semibold text-[var(--text-primary)]">
|
|
Games - {games.length}
|
|
</h2>
|
|
<Link
|
|
href="/games"
|
|
className="text-[var(--accent)] hover:text-[var(--accent-hover)] transition-colors"
|
|
>
|
|
View All →
|
|
</Link>
|
|
</div>
|
|
<div className="flex justify-end space-x-2 mb-4">
|
|
<TimeRangeButton range="7d" currentRange={gameTimeRange} onChange={setGameTimeRange} />
|
|
<TimeRangeButton range="30d" currentRange={gameTimeRange} onChange={setGameTimeRange} />
|
|
<TimeRangeButton range="90d" currentRange={gameTimeRange} onChange={setGameTimeRange} />
|
|
</div>
|
|
<div className="h-[300px]">
|
|
<Line data={getGameCountData()} options={chartOptions} />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Game Popularity Card */}
|
|
<div className="bg-[var(--card-bg)] border border-[var(--card-border)] rounded-lg p-6">
|
|
<div className="flex justify-between items-center mb-4">
|
|
<h2 className="text-xl font-semibold text-[var(--text-primary)]">
|
|
Game Popularity
|
|
</h2>
|
|
</div>
|
|
<div className="h-[300px]">
|
|
<Bar data={getGamePopularityData()} options={barChartOptions} />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|