From ff241fefd2b1cf2b989e4f0cb35a24104057a06a Mon Sep 17 00:00:00 2001 From: Andres Date: Tue, 20 May 2025 07:16:54 +0200 Subject: [PATCH] add games per hour/day chart --- .../_components/games-per-hour-chart.tsx | 130 ++++++++++++++++++ src/app/(home)/games-per-hour/page.tsx | 24 ++++ src/app/layout.config.tsx | 7 +- src/server/api/routers/history.ts | 69 ++++++++++ 4 files changed, 229 insertions(+), 1 deletion(-) create mode 100644 src/app/(home)/games-per-hour/_components/games-per-hour-chart.tsx create mode 100644 src/app/(home)/games-per-hour/page.tsx diff --git a/src/app/(home)/games-per-hour/_components/games-per-hour-chart.tsx b/src/app/(home)/games-per-hour/_components/games-per-hour-chart.tsx new file mode 100644 index 0000000..f077de0 --- /dev/null +++ b/src/app/(home)/games-per-hour/_components/games-per-hour-chart.tsx @@ -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('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 ( + + +
+ {getTitleText()} + Number of games played over time +
+ +
+ + + + + + + `${value} games`} + /> + } + /> + + + + +
+ ) +} diff --git a/src/app/(home)/games-per-hour/page.tsx b/src/app/(home)/games-per-hour/page.tsx new file mode 100644 index 0000000..cdc6ed3 --- /dev/null +++ b/src/app/(home)/games-per-hour/page.tsx @@ -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 ( +
+

Games Played Over Time

+ + + + + +
+ ) +} diff --git a/src/app/layout.config.tsx b/src/app/layout.config.tsx index 8c6a69d..645ea00 100644 --- a/src/app/layout.config.tsx +++ b/src/app/layout.config.tsx @@ -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: , }, + { + text: 'Games Per Hour', + url: '/games-per-hour', + icon: , + }, ], }, diff --git a/src/server/api/routers/history.ts b/src/server/api/routers/history.ts index 881543a..10c6dca 100644 --- a/src/server/api/routers/history.ts +++ b/src/server/api/routers/history.ts @@ -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() + + // Group games by the selected time unit + const gamesByTimeUnit = games.reduce>((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() }),