mirror of
https://github.com/ershisan99/www.git
synced 2025-12-18 05:19:23 +00:00
add games per hour/day chart
This commit is contained in:
@@ -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>
|
||||||
|
)
|
||||||
|
}
|
||||||
24
src/app/(home)/games-per-hour/page.tsx
Normal file
24
src/app/(home)/games-per-hour/page.tsx
Normal 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>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import type { LinkItemType } from 'fumadocs-ui/layouts/links'
|
import type { LinkItemType } from 'fumadocs-ui/layouts/links'
|
||||||
import type { BaseLayoutProps } from 'fumadocs-ui/layouts/shared'
|
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'
|
import { Header } from './_components/header'
|
||||||
|
|
||||||
const links = [
|
const links = [
|
||||||
@@ -37,6 +37,11 @@ const links = [
|
|||||||
url: '/log-parser',
|
url: '/log-parser',
|
||||||
icon: <Upload />,
|
icon: <Upload />,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
text: 'Games Per Hour',
|
||||||
|
url: '/games-per-hour',
|
||||||
|
icon: <BarChart3 />,
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
@@ -20,6 +20,75 @@ export const history_router = createTRPCRouter({
|
|||||||
.where(eq(player_games.playerId, input.user_id))
|
.where(eq(player_games.playerId, input.user_id))
|
||||||
.orderBy(desc(player_games.gameNum))
|
.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 () => {
|
sync: publicProcedure.mutation(async () => {
|
||||||
return syncHistory()
|
return syncHistory()
|
||||||
}),
|
}),
|
||||||
|
|||||||
Reference in New Issue
Block a user