add a date picker for games per hour page

This commit is contained in:
2025-06-04 11:30:21 +02:00
parent 4c0818f95f
commit 4d6cf88160
2 changed files with 115 additions and 40 deletions

View File

@@ -1,5 +1,7 @@
'use client' 'use client'
import { Button } from '@/components/ui/button'
import { Calendar } from '@/components/ui/calendar'
import { import {
Card, Card,
CardContent, CardContent,
@@ -13,6 +15,11 @@ import {
ChartTooltip, ChartTooltip,
ChartTooltipContent, ChartTooltipContent,
} from '@/components/ui/chart' } from '@/components/ui/chart'
import {
Popover,
PopoverContent,
PopoverTrigger,
} from '@/components/ui/popover'
import { import {
Select, Select,
SelectContent, SelectContent,
@@ -20,9 +27,12 @@ import {
SelectTrigger, SelectTrigger,
SelectValue, SelectValue,
} from '@/components/ui/select' } from '@/components/ui/select'
import { cn } from '@/lib/utils'
import { api } from '@/trpc/react' import { api } from '@/trpc/react'
import { format } from 'date-fns'
import { CalendarIcon } from 'lucide-react'
import { useState } from 'react' import { useState } from 'react'
import { BarChart, CartesianGrid, XAxis, YAxis, Bar } from 'recharts' import { Bar, BarChart, CartesianGrid, XAxis, YAxis } from 'recharts'
const chartConfig = { const chartConfig = {
count: { count: {
@@ -35,10 +45,22 @@ type GroupByOption = 'hour' | 'day' | 'week' | 'month'
export function GamesPerHourChart() { export function GamesPerHourChart() {
const [groupBy, setGroupBy] = useState<GroupByOption>('hour') const [groupBy, setGroupBy] = useState<GroupByOption>('hour')
const [dateRange, setDateRange] = useState<
| {
from?: Date | undefined
to?: Date | undefined
}
| undefined
>({
from: undefined,
to: undefined,
})
// Fetch games data with the selected grouping // Fetch games data with the selected grouping and date range
const [gamesData] = api.history.games_per_hour.useSuspenseQuery({ const [gamesData] = api.history.games_per_hour.useSuspenseQuery({
groupBy, groupBy,
startDate: dateRange?.from?.toISOString(),
endDate: dateRange?.to?.toISOString(),
}) })
// Format the title and description based on the grouping // Format the title and description based on the grouping
@@ -76,26 +98,71 @@ export function GamesPerHourChart() {
} }
return ( return (
<Card className="w-full"> <Card className='w-full'>
<CardHeader className="flex flex-row items-center justify-between"> <CardHeader className='flex flex-row items-center justify-between'>
<div> <div>
<CardTitle>{getTitleText()}</CardTitle> <CardTitle>{getTitleText()}</CardTitle>
<CardDescription>Number of games played over time</CardDescription> <CardDescription>Number of games played over time</CardDescription>
</div> </div>
<Select value={groupBy} onValueChange={(value) => setGroupBy(value as GroupByOption)}> <div className='flex gap-2'>
<SelectTrigger className="w-[180px]"> <Popover>
<SelectValue placeholder="Select grouping" /> <PopoverTrigger asChild>
</SelectTrigger> <Button
<SelectContent> id='date'
<SelectItem value="hour">Group by Hour</SelectItem> variant={'outline'}
<SelectItem value="day">Group by Day</SelectItem> className={cn(
<SelectItem value="week">Group by Week</SelectItem> 'w-[280px] justify-start text-left font-normal',
<SelectItem value="month">Group by Month</SelectItem> !dateRange?.from && 'text-muted-foreground'
</SelectContent> )}
</Select> >
<CalendarIcon className='mr-2 h-4 w-4' />
{dateRange?.from ? (
dateRange.to ? (
<>
{format(dateRange.from, 'LLL dd, y')} -{' '}
{format(dateRange.to, 'LLL dd, y')}
</>
) : (
format(dateRange.from, 'LLL dd, y')
)
) : (
<span>Pick a date range</span>
)}
</Button>
</PopoverTrigger>
<PopoverContent className='w-auto p-0' align='end'>
<Calendar
initialFocus
mode='range'
defaultMonth={dateRange?.from}
selected={{
from: dateRange?.from,
to: dateRange?.to,
}}
onSelect={setDateRange}
numberOfMonths={2}
/>
</PopoverContent>
</Popover>
<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>
</div>
</CardHeader> </CardHeader>
<CardContent className="w-full h-[500px] p-2"> <CardContent className='h-[500px] w-full p-2'>
<ChartContainer config={chartConfig} className="w-full h-full"> <ChartContainer config={chartConfig} className='h-full w-full'>
<BarChart <BarChart
data={gamesData} data={gamesData}
margin={{ margin={{
@@ -105,23 +172,21 @@ export function GamesPerHourChart() {
bottom: 60, bottom: 60,
}} }}
> >
<CartesianGrid strokeDasharray="3 3" /> <CartesianGrid strokeDasharray='3 3' />
<XAxis <XAxis
dataKey="timeUnit" dataKey='timeUnit'
angle={-45} angle={-45}
textAnchor="end" textAnchor='end'
height={60} height={60}
tickFormatter={formatXAxisTick} tickFormatter={formatXAxisTick}
/> />
<YAxis /> <YAxis />
<ChartTooltip <ChartTooltip
content={ content={
<ChartTooltipContent <ChartTooltipContent formatter={(value) => `${value} games`} />
formatter={(value) => `${value} games`}
/>
} }
/> />
<Bar dataKey="count" fill="var(--color-count)" /> <Bar dataKey='count' fill='var(--color-count)' />
</BarChart> </BarChart>
</ChartContainer> </ChartContainer>
</CardContent> </CardContent>

View File

@@ -1,43 +1,40 @@
import { createTRPCRouter, publicProcedure } from '@/server/api/trpc' import { createTRPCRouter, publicProcedure } from '@/server/api/trpc'
import { db } from '@/server/db' import { db } from '@/server/db'
import { metadata, player_games, raw_history } from '@/server/db/schema' import { metadata, player_games, raw_history } from '@/server/db/schema'
import { desc, eq } from 'drizzle-orm' import { and, desc, eq, gt, lt, sql } from 'drizzle-orm'
import ky from 'ky' import ky from 'ky'
import { chunk } from 'remeda' import { chunk } from 'remeda'
import { z } from 'zod' import { z } from 'zod'
export const history_router = createTRPCRouter({ export const history_router = createTRPCRouter({
user_games: publicProcedure
.input(
z.object({
user_id: z.string(),
})
)
.query(async ({ ctx, input }) => {
return await ctx.db
.select()
.from(player_games)
.where(eq(player_games.playerId, input.user_id))
.orderBy(desc(player_games.gameNum))
}),
games_per_hour: publicProcedure games_per_hour: publicProcedure
.input( .input(
z z
.object({ .object({
groupBy: z.enum(['hour', 'day', 'week', 'month']).default('hour'), groupBy: z.enum(['hour', 'day', 'week', 'month']).default('hour'),
startDate: z.string().optional(),
endDate: z.string().optional(),
}) })
.optional() .optional()
) )
.query(async ({ ctx, input }) => { .query(async ({ ctx, input }) => {
const groupBy = input?.groupBy || 'hour' const groupBy = input?.groupBy || 'hour'
const startDate = input?.startDate ? new Date(input.startDate) : undefined
// Fetch all games with their gameNum to identify unique games const endDate = input?.endDate ? new Date(input.endDate) : undefined
const nextDay = endDate ? new Date(endDate) : undefined
if (nextDay) nextDay.setDate(nextDay.getDate() + 1)
const games = await ctx.db const games = await ctx.db
.select({ .select({
gameTime: player_games.gameTime, gameTime: player_games.gameTime,
gameNum: player_games.gameNum, gameNum: player_games.gameNum,
}) })
.from(player_games) .from(player_games)
.where(
and(
startDate ? gt(player_games.gameTime, startDate) : undefined,
nextDay ? lt(player_games.gameTime, nextDay) : undefined
)
)
.orderBy(player_games.gameTime) .orderBy(player_games.gameTime)
// Track unique game numbers to avoid counting the same game twice // Track unique game numbers to avoid counting the same game twice
@@ -97,6 +94,19 @@ export const history_router = createTRPCRouter({
sync: publicProcedure.mutation(async () => { sync: publicProcedure.mutation(async () => {
return syncHistory() return syncHistory()
}), }),
user_games: publicProcedure
.input(
z.object({
user_id: z.string(),
})
)
.query(async ({ ctx, input }) => {
return await ctx.db
.select()
.from(player_games)
.where(eq(player_games.playerId, input.user_id))
.orderBy(desc(player_games.gameNum))
}),
}) })
export async function syncHistory() { export async function syncHistory() {