migrate to nextjs

This commit is contained in:
Fran Jurmanović
2025-08-27 20:38:19 +02:00
parent 965e13a0bf
commit b269144ee7
120 changed files with 10393 additions and 8526 deletions

View File

@@ -0,0 +1,77 @@
'use client';
import { Bar } from 'react-chartjs-2';
import {
Chart as ChartJS,
CategoryScale,
LinearScale,
BarElement,
Title,
Tooltip,
Legend
} from 'chart.js';
ChartJS.register(CategoryScale, LinearScale, BarElement, Title, Tooltip, Legend);
interface DailyActivity {
date: string;
sessionsCount: number;
}
interface DailyActivityChartProps {
data: DailyActivity[];
}
export function DailyActivityChart({ data }: DailyActivityChartProps) {
const chartData = {
labels: data.map((item) => new Date(item.date).toLocaleDateString()),
datasets: [
{
label: 'Sessions',
data: data.map((item) => item.sessionsCount),
backgroundColor: 'rgba(59, 130, 246, 0.8)',
borderColor: 'rgb(59, 130, 246)',
borderWidth: 1
}
]
};
const options = {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
position: 'top' as const,
labels: {
color: '#e5e7eb'
}
}
},
scales: {
x: {
ticks: {
color: '#9ca3af'
},
grid: {
color: '#374151'
}
},
y: {
beginAtZero: true,
ticks: {
color: '#9ca3af',
stepSize: 1
},
grid: {
color: '#374151'
}
}
}
};
return (
<div className="h-64">
<Bar data={chartData} options={options} />
</div>
);
}

View File

@@ -0,0 +1,95 @@
'use client';
import { Line } from 'react-chartjs-2';
import {
Chart as ChartJS,
CategoryScale,
LinearScale,
PointElement,
LineElement,
Title,
Tooltip,
Legend,
TimeScale
} from 'chart.js';
import 'chartjs-adapter-date-fns';
ChartJS.register(
CategoryScale,
LinearScale,
PointElement,
LineElement,
Title,
Tooltip,
Legend,
TimeScale
);
interface PlayerCountPoint {
timestamp: string;
count: number;
}
interface PlayerCountChartProps {
data: PlayerCountPoint[];
}
export function PlayerCountChart({ data }: PlayerCountChartProps) {
const chartData = {
datasets: [
{
label: 'Player Count',
data: data.map((point) => ({
x: new Date(point.timestamp),
y: point.count
})),
borderColor: 'rgb(59, 130, 246)',
backgroundColor: 'rgba(59, 130, 246, 0.1)',
tension: 0.4
}
]
};
const options = {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
position: 'top' as const,
labels: {
color: '#e5e7eb'
}
}
},
scales: {
x: {
type: 'time' as const,
time: {
unit: 'hour' as const
},
ticks: {
color: '#9ca3af'
},
grid: {
color: '#374151'
}
},
y: {
beginAtZero: true,
ticks: {
color: '#9ca3af',
stepSize: 1
},
grid: {
color: '#374151'
}
}
}
};
return (
<div className="h-64">
<Line data={chartData} options={options} />
</div>
);
}

View File

@@ -0,0 +1,85 @@
interface RecentSession {
id: number;
date: string;
type: string;
track: string;
duration: number;
players: number;
}
interface RecentSessionsProps {
sessions: RecentSession[];
}
export function RecentSessions({ sessions }: RecentSessionsProps) {
const formatDuration = (minutes: number) => {
const hours = Math.floor(minutes / 60);
const mins = minutes % 60;
return hours > 0 ? `${hours}h ${mins}m` : `${mins}m`;
};
const formatDate = (dateString: string) => {
return new Date(dateString).toLocaleDateString('en-US', {
month: 'short',
day: 'numeric',
hour: '2-digit',
minute: '2-digit'
});
};
if (sessions.length === 0) {
return (
<div className="py-8 text-center">
<div className="mb-2 text-4xl">📊</div>
<p className="text-gray-400">No recent sessions found</p>
</div>
);
}
return (
<div className="overflow-x-auto">
<table className="min-w-full divide-y divide-gray-700">
<thead>
<tr>
<th className="px-6 py-3 text-left text-xs font-medium tracking-wider text-gray-400 uppercase">
Date
</th>
<th className="px-6 py-3 text-left text-xs font-medium tracking-wider text-gray-400 uppercase">
Type
</th>
<th className="px-6 py-3 text-left text-xs font-medium tracking-wider text-gray-400 uppercase">
Track
</th>
<th className="px-6 py-3 text-left text-xs font-medium tracking-wider text-gray-400 uppercase">
Duration
</th>
<th className="px-6 py-3 text-left text-xs font-medium tracking-wider text-gray-400 uppercase">
Players
</th>
</tr>
</thead>
<tbody className="divide-y divide-gray-700">
{sessions.map((session) => (
<tr key={session.id} className="transition-colors hover:bg-gray-700">
<td className="px-6 py-4 text-sm whitespace-nowrap text-gray-300">
{formatDate(session.date)}
</td>
<td className="px-6 py-4 text-sm whitespace-nowrap text-gray-300">
<span className="inline-flex items-center rounded-full bg-blue-900 px-2.5 py-0.5 text-xs font-medium text-blue-300">
{session.type}
</span>
</td>
<td className="px-6 py-4 text-sm whitespace-nowrap text-gray-300">{session.track}</td>
<td className="px-6 py-4 text-sm whitespace-nowrap text-gray-300">
{formatDuration(session.duration)}
</td>
<td className="px-6 py-4 text-sm whitespace-nowrap text-gray-300">
{session.players}
</td>
</tr>
))}
</tbody>
</table>
</div>
);
}

View File

@@ -0,0 +1,67 @@
'use client';
import { Doughnut } from 'react-chartjs-2';
import { Chart as ChartJS, ArcElement, Tooltip, Legend } from 'chart.js';
ChartJS.register(ArcElement, Tooltip, Legend);
interface SessionCount {
name: string;
count: number;
}
interface SessionTypesChartProps {
data: SessionCount[];
}
export function SessionTypesChart({ data }: SessionTypesChartProps) {
const colors = [
'#3b82f6', // blue
'#10b981', // emerald
'#f59e0b', // amber
'#ef4444', // red
'#8b5cf6', // violet
'#06b6d4' // cyan
];
const chartData = {
labels: data.map((item) => item.name),
datasets: [
{
data: data.map((item) => item.count),
backgroundColor: colors.slice(0, data.length),
borderColor: colors.slice(0, data.length).map((color) => color + '20'),
borderWidth: 2
}
]
};
const options = {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
position: 'bottom' as const,
labels: {
color: '#e5e7eb',
padding: 20
}
},
tooltip: {
callbacks: {
label: function (context: { label: string; parsed: number }) {
const total = data.reduce((sum, item) => sum + item.count, 0);
const percentage = ((context.parsed / total) * 100).toFixed(1);
return `${context.label}: ${context.parsed} (${percentage}%)`;
}
}
}
}
};
return (
<div className="h-64">
<Doughnut data={chartData} options={options} />
</div>
);
}

View File

@@ -0,0 +1,21 @@
interface StatCardProps {
title: string;
value: string | number;
icon: string;
}
export function StatCard({ title, value, icon }: StatCardProps) {
return (
<div className="rounded-lg bg-gray-800 p-6">
<div className="flex items-center">
<div className="flex-shrink-0">
<span className="text-3xl">{icon}</span>
</div>
<div className="ml-4">
<div className="text-2xl font-semibold text-white">{value}</div>
<div className="text-sm text-gray-400">{title}</div>
</div>
</div>
</div>
);
}

View File

@@ -0,0 +1,67 @@
'use client';
import type { StateHistoryStats } from '@/lib/types/statistics';
import { PlayerCountChart } from './PlayerCountChart';
import { SessionTypesChart } from './SessionTypesChart';
import { DailyActivityChart } from './DailyActivityChart';
import { StatCard } from './StatCard';
import { RecentSessions } from './RecentSessions';
interface StatisticsDashboardProps {
stats: StateHistoryStats;
}
export function StatisticsDashboard({ stats }: StatisticsDashboardProps) {
if (!stats) {
return (
<div className="py-12 text-center">
<div className="mb-4 text-6xl">📊</div>
<h3 className="mb-2 text-xl font-semibold text-white">No Statistics Available</h3>
<p className="text-gray-400">No data found for the selected date range.</p>
</div>
);
}
return (
<div className="space-y-6">
<div className="grid grid-cols-1 gap-6 md:grid-cols-2 lg:grid-cols-4">
<StatCard title="Total Sessions" value={stats.totalSessions ?? 0} icon="🏁" />
<StatCard
title="Total Playtime"
value={`${Math.round(stats.totalPlaytime ?? 0 / 60)}h`}
icon="⏱️"
/>
<StatCard
title="Average Players"
value={(stats.averagePlayers ?? 0).toFixed(1)}
icon="👥"
/>
<StatCard title="Peak Players" value={stats.peakPlayers ?? 0} icon="🔥" />
</div>
{/* Charts */}
<div className="grid grid-cols-1 gap-6 lg:grid-cols-2">
<div className="rounded-lg bg-gray-800 p-6">
<h3 className="mb-4 text-lg font-medium text-white">Player Count Over Time</h3>
<PlayerCountChart data={stats.playerCountOverTime ?? []} />
</div>
<div className="rounded-lg bg-gray-800 p-6">
<h3 className="mb-4 text-lg font-medium text-white">Session Types</h3>
<SessionTypesChart data={stats.sessionTypes ?? []} />
</div>
</div>
<div className="rounded-lg bg-gray-800 p-6">
<h3 className="mb-4 text-lg font-medium text-white">Daily Activity</h3>
<DailyActivityChart data={stats.dailyActivity ?? []} />
</div>
{/* Recent Sessions */}
<div className="rounded-lg bg-gray-800 p-6">
<h3 className="mb-4 text-lg font-medium text-white">Recent Sessions</h3>
<RecentSessions sessions={stats.recentSessions ?? []} />
</div>
</div>
);
}