mirror of
https://github.com/ershisan99/www.git
synced 2025-12-17 21:09:22 +00:00
add a date picker for games per hour page
This commit is contained in:
@@ -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>
|
||||||
|
|||||||
@@ -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() {
|
||||||
|
|||||||
Reference in New Issue
Block a user