add games per hour/day chart

This commit is contained in:
2025-05-20 07:16:54 +02:00
parent 518b1fe178
commit ff241fefd2
4 changed files with 229 additions and 1 deletions

View File

@@ -0,0 +1,130 @@
'use client'
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from '@/components/ui/card'
import {
type ChartConfig,
ChartContainer,
ChartTooltip,
ChartTooltipContent,
} from '@/components/ui/chart'
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select'
import { api } from '@/trpc/react'
import { useState } from 'react'
import { BarChart, CartesianGrid, XAxis, YAxis, Bar } from 'recharts'
const chartConfig = {
count: {
label: 'Games',
color: 'var(--color-violet-500)',
},
} satisfies ChartConfig
type GroupByOption = 'hour' | 'day' | 'week' | 'month'
export function GamesPerHourChart() {
const [groupBy, setGroupBy] = useState<GroupByOption>('hour')
// Fetch games data with the selected grouping
const [gamesData] = api.history.games_per_hour.useSuspenseQuery({
groupBy,
})
// Format the title and description based on the grouping
const getTitleText = () => {
switch (groupBy) {
case 'hour':
return 'Games Played Per Hour'
case 'day':
return 'Games Played Per Day'
case 'week':
return 'Games Played Per Week'
case 'month':
return 'Games Played Per Month'
default:
return 'Games Played'
}
}
// Format the X-axis labels based on the grouping
const formatXAxisTick = (value: string) => {
const date = new Date(value)
switch (groupBy) {
case 'hour':
return `${date.toLocaleDateString()} ${date.getHours()}:00`
case 'day':
return date.toLocaleDateString()
case 'week':
return value // Already formatted as "Week of YYYY-MM-DD"
case 'month':
return `${date.toLocaleString('default', { month: 'long' })} ${date.getFullYear()}`
default:
return value
}
}
return (
<Card className="w-full">
<CardHeader className="flex flex-row items-center justify-between">
<div>
<CardTitle>{getTitleText()}</CardTitle>
<CardDescription>Number of games played over time</CardDescription>
</div>
<Select value={groupBy} onValueChange={(value) => setGroupBy(value as GroupByOption)}>
<SelectTrigger className="w-[180px]">
<SelectValue placeholder="Select grouping" />
</SelectTrigger>
<SelectContent>
<SelectItem value="hour">Group by Hour</SelectItem>
<SelectItem value="day">Group by Day</SelectItem>
<SelectItem value="week">Group by Week</SelectItem>
<SelectItem value="month">Group by Month</SelectItem>
</SelectContent>
</Select>
</CardHeader>
<CardContent className="w-full h-[500px] p-2">
<ChartContainer config={chartConfig} className="w-full h-full">
<BarChart
data={gamesData}
margin={{
top: 20,
right: 30,
left: 20,
bottom: 60,
}}
>
<CartesianGrid strokeDasharray="3 3" />
<XAxis
dataKey="timeUnit"
angle={-45}
textAnchor="end"
height={60}
tickFormatter={formatXAxisTick}
/>
<YAxis />
<ChartTooltip
content={
<ChartTooltipContent
formatter={(value) => `${value} games`}
/>
}
/>
<Bar dataKey="count" fill="var(--color-count)" />
</BarChart>
</ChartContainer>
</CardContent>
</Card>
)
}

View File

@@ -0,0 +1,24 @@
import { GamesPerHourChart } from './_components/games-per-hour-chart'
import { auth } from '@/server/auth'
import { HydrateClient, api } from '@/trpc/server'
import { Suspense } from 'react'
export default async function GamesPerHourPage() {
const session = await auth()
// Prefetch the games per hour data with default grouping (hour)
await api.history.games_per_hour.prefetch({
groupBy: 'hour',
})
return (
<div className="container mx-auto py-8">
<h1 className="text-3xl font-bold mb-6">Games Played Over Time</h1>
<Suspense>
<HydrateClient>
<GamesPerHourChart />
</HydrateClient>
</Suspense>
</div>
)
}

View File

@@ -1,6 +1,6 @@
import type { LinkItemType } from 'fumadocs-ui/layouts/links'
import type { BaseLayoutProps } from 'fumadocs-ui/layouts/shared'
import { BookOpen, CircleDollarSign, Trophy, Upload } from 'lucide-react'
import { BarChart3, BookOpen, CircleDollarSign, Trophy, Upload } from 'lucide-react'
import { Header } from './_components/header'
const links = [
@@ -37,6 +37,11 @@ const links = [
url: '/log-parser',
icon: <Upload />,
},
{
text: 'Games Per Hour',
url: '/games-per-hour',
icon: <BarChart3 />,
},
],
},

View File

@@ -20,6 +20,75 @@ export const history_router = createTRPCRouter({
.where(eq(player_games.playerId, input.user_id))
.orderBy(desc(player_games.gameNum))
}),
games_per_hour: publicProcedure
.input(
z.object({
groupBy: z.enum(['hour', 'day', 'week', 'month']).default('hour'),
}).optional()
)
.query(async ({ ctx, input }) => {
const groupBy = input?.groupBy || 'hour'
// Fetch all games with their gameNum to identify unique games
const games = await ctx.db
.select({
gameTime: player_games.gameTime,
gameNum: player_games.gameNum,
})
.from(player_games)
.orderBy(player_games.gameTime)
// Track unique game numbers to avoid counting the same game twice
const processedGameNums = new Set<number>()
// Group games by the selected time unit
const gamesByTimeUnit = games.reduce<Record<string, number>>((acc, game) => {
if (!game.gameTime || !game.gameNum) return acc
// Skip if we've already processed this game number
if (processedGameNums.has(game.gameNum)) return acc
// Mark this game as processed
processedGameNums.add(game.gameNum)
const date = new Date(game.gameTime)
let timeKey: string
switch (groupBy) {
case 'hour':
// Format: YYYY-MM-DD HH:00
timeKey = `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')} ${String(date.getHours()).padStart(2, '0')}:00`
break
case 'day':
// Format: YYYY-MM-DD
timeKey = `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}`
break
case 'week':
// Get the first day of the week (Sunday)
const firstDayOfWeek = new Date(date)
const day = date.getDay() // 0 = Sunday, 1 = Monday, etc.
firstDayOfWeek.setDate(date.getDate() - day)
timeKey = `Week of ${firstDayOfWeek.getFullYear()}-${String(firstDayOfWeek.getMonth() + 1).padStart(2, '0')}-${String(firstDayOfWeek.getDate()).padStart(2, '0')}`
break
case 'month':
// Format: YYYY-MM
timeKey = `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}`
break
default:
timeKey = `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')} ${String(date.getHours()).padStart(2, '0')}:00`
}
acc[timeKey] = (acc[timeKey] || 0) + 1
return acc
}, {})
// Convert to array format for chart
return Object.entries(gamesByTimeUnit).map(([timeUnit, count]) => ({
timeUnit,
count,
groupBy,
}))
}),
sync: publicProcedure.mutation(async () => {
return syncHistory()
}),