diff --git a/bun.lock b/bun.lock index 899ebe0..33e2e2e 100644 --- a/bun.lock +++ b/bun.lock @@ -34,6 +34,7 @@ "@radix-ui/react-tooltip": "^1.1.8", "@t3-oss/env-nextjs": "^0.12.0", "@tanstack/react-query": "^5.69.0", + "@tanstack/react-table": "^8.21.2", "@tanstack/react-virtual": "^3.13.6", "@trpc/client": "^11.0.0", "@trpc/react-query": "^11.0.0", @@ -392,8 +393,12 @@ "@tanstack/react-query": ["@tanstack/react-query@5.71.5", "", { "dependencies": { "@tanstack/query-core": "5.71.5" }, "peerDependencies": { "react": "^18 || ^19" } }, "sha512-WpxZWy4fDASjY+iAaXB+aY+LC95PQ34W6EWVkjJ0hdzWWbczFnr9nHvHkVDpwdR18I1NO8igNGQJFrLrgyzI8Q=="], + "@tanstack/react-table": ["@tanstack/react-table@8.21.2", "", { "dependencies": { "@tanstack/table-core": "8.21.2" }, "peerDependencies": { "react": ">=16.8", "react-dom": ">=16.8" } }, "sha512-11tNlEDTdIhMJba2RBH+ecJ9l1zgS2kjmexDPAraulc8jeNA4xocSNeyzextT0XJyASil4XsCYlJmf5jEWAtYg=="], + "@tanstack/react-virtual": ["@tanstack/react-virtual@3.13.6", "", { "dependencies": { "@tanstack/virtual-core": "3.13.6" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-WT7nWs8ximoQ0CDx/ngoFP7HbQF9Q2wQe4nh2NB+u2486eX3nZRE40P9g6ccCVq7ZfTSH5gFOuCoVH5DLNS/aA=="], + "@tanstack/table-core": ["@tanstack/table-core@8.21.2", "", {}, "sha512-uvXk/U4cBiFMxt+p9/G7yUWI/UbHYbyghLCjlpWZ3mLeIZiUBSKcUnw9UnKkdRz7Z/N4UBuFLWQdJCjUe7HjvA=="], + "@tanstack/virtual-core": ["@tanstack/virtual-core@3.13.6", "", {}, "sha512-cnQUeWnhNP8tJ4WsGcYiX24Gjkc9ALstLbHcBj1t3E7EimN6n6kHH+DPV4PpDnuw00NApQp+ViojMj1GRdwYQg=="], "@trpc/client": ["@trpc/client@11.0.1", "", { "peerDependencies": { "@trpc/server": "11.0.1", "typescript": ">=5.7.2" } }, "sha512-HvOrvWAXbGBwt4om+NfuxrK4f+ik2aaNIXq7WLrJbEp7U+YXfh5++1a5p4JDaikrvSaObJ389DhYAGWz90xSGw=="], diff --git a/package.json b/package.json index 86aac5a..7ab3496 100644 --- a/package.json +++ b/package.json @@ -49,6 +49,7 @@ "@radix-ui/react-tooltip": "^1.1.8", "@t3-oss/env-nextjs": "^0.12.0", "@tanstack/react-query": "^5.69.0", + "@tanstack/react-table": "^8.21.2", "@tanstack/react-virtual": "^3.13.6", "@trpc/client": "^11.0.0", "@trpc/react-query": "^11.0.0", diff --git a/src/app/players/[id]/_components/games-table.tsx b/src/app/players/[id]/_components/games-table.tsx new file mode 100644 index 0000000..9180afd --- /dev/null +++ b/src/app/players/[id]/_components/games-table.tsx @@ -0,0 +1,228 @@ +'use client' + +import { Badge } from '@/components/ui/badge' +import { Button } from '@/components/ui/button' +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from '@/components/ui/table' +import { cn } from '@/lib/utils' +import type { SelectGames } from '@/server/db/types' +import { + type SortingState, + createColumnHelper, + flexRender, + getCoreRowModel, + getSortedRowModel, + useReactTable, +} from '@tanstack/react-table' +import { ArrowDownCircle, ArrowUp, ArrowUpCircle } from 'lucide-react' +import { useFormatter } from 'next-intl' +import Link from 'next/link' +import { useMemo, useState } from 'react' + +const numberFormatter = new Intl.NumberFormat('en-US', { + signDisplay: 'exceptZero', +}) + +const columnHelper = createColumnHelper() + +const useColumns = () => { + const format = useFormatter() + return useMemo( + () => [ + columnHelper.accessor('opponentName', { + meta: { className: 'pl-4' }, + header: 'Opponent', + cell: (info) => ( + + {info.getValue()} + + ), + }), + columnHelper.accessor('gameType', { + header: 'Game Type', + cell: (info) => { + const gameType = info.getValue() + return ( + + {info.getValue()} + + ) + }, + }), + columnHelper.accessor('opponentMmr', { + header: 'Opponent MMR', + meta: { className: 'justify-end' }, + cell: (info) => ( + + {Math.trunc(info.getValue())} + + ), + }), + columnHelper.accessor('playerMmr', { + header: 'MMR', + meta: { className: 'justify-end' }, + cell: (info) => ( + + {Math.trunc(info.getValue())} + + ), + }), + columnHelper.accessor('mmrChange', { + header: 'Result', + meta: { className: 'justify-end' }, + cell: (info) => { + const mmrChange = info.getValue() + return ( + 0 ? 'text-emerald-500' : 'text-rose-500' + )} + > + {numberFormatter.format(Math.trunc(mmrChange))} + {mmrChange > 0 ? ( + + ) : ( + + )} + + ) + }, + }), + columnHelper.accessor('gameTime', { + header: 'Date', + meta: { className: 'justify-end' }, + cell: (info) => ( + + {format.dateTime(info.getValue(), { + year: 'numeric', + month: '2-digit', + day: '2-digit', + })} + + ), + }), + columnHelper.accessor('gameTime', { + header: 'Time', + meta: { className: 'justify-end pr-4' }, + cell: (info) => ( + + {format.dateTime(info.getValue(), { + hour: '2-digit', + minute: '2-digit', + })} + + ), + id: 'time', + }), + ], + [] + ) +} + +export function GamesTable({ games }: { games: SelectGames[] }) { + const [sorting, setSorting] = useState([]) + const columns = useColumns() + const table = useReactTable({ + data: games, + columns, + state: { + sorting, + }, + onSortingChange: setSorting, + getCoreRowModel: getCoreRowModel(), + getSortedRowModel: getSortedRowModel(), + getRowId: (originalRow) => originalRow.gameNum.toString(), + }) + + return ( +
+ + + {table.getHeaderGroups().map((headerGroup) => ( + + {headerGroup.headers.map((header) => { + const sortDirection = header.column.getIsSorted() + return ( + + + + + + ) + })} + + ))} + + + {table.getRowModel().rows.map((row) => ( + + {row.getVisibleCells().map((cell) => ( + + {flexRender(cell.column.columnDef.cell, cell.getContext())} + + ))} + + ))} + +
+
+ ) +} diff --git a/src/app/players/[id]/user.tsx b/src/app/players/[id]/user.tsx index d78b6e0..be91e17 100644 --- a/src/app/players/[id]/user.tsx +++ b/src/app/players/[id]/user.tsx @@ -3,6 +3,7 @@ import type React from 'react' import { useState } from 'react' +import { GamesTable } from '@/app/players/[id]/_components/games-table' import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar' import { Badge } from '@/components/ui/badge' import { Card, CardContent, CardHeader } from '@/components/ui/card' @@ -13,14 +14,6 @@ import { SelectTrigger, SelectValue, } from '@/components/ui/select' -import { - Table, - TableBody, - TableCell, - TableHead, - TableHeader, - TableRow, -} from '@/components/ui/table' import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs' import { cn } from '@/lib/utils' import { RANKED_CHANNEL, VANILLA_CHANNEL } from '@/shared/constants' @@ -29,10 +22,8 @@ import { ArrowDownCircle, ArrowUpCircle, BarChart3, - Calendar, ChevronDown, ChevronUp, - Clock, Filter, MinusCircle, Star, @@ -342,107 +333,7 @@ export function UserInfo() {
- - - - Game Type - Opponent - - Opponent MMR - - MMR - Result - - Leaderboard - - - - Date - - - - - Time - - - - - - {filteredGames.map((game) => ( - - - - {game.gameType} - - - - {game.opponentName} - - - {Math.trunc(game.opponentMmr)} - - - {Math.trunc(game.playerMmr)} - - - {game.mmrChange > 0 ? ( - - {numberFormatter.format( - Math.trunc(game.mmrChange) - )} - - - ) : ( - - {numberFormatter.format( - Math.trunc(game.mmrChange) - )} - - - )} - - - - {game.gameType === 'ranked' - ? 'Ranked Queue' - : game.gameType.toLowerCase() === 'vanilla' - ? 'Vanilla Queue' - : 'N/A'} - - - - {format.dateTime(game.gameTime, { - year: 'numeric', - month: '2-digit', - day: '2-digit', - })} - - - {format.dateTime(game.gameTime, { - hour: '2-digit', - minute: '2-digit', - })} - - - ))} - -
+
diff --git a/src/components/ui/button.tsx b/src/components/ui/button.tsx index a2df8dc..ec037f8 100644 --- a/src/components/ui/button.tsx +++ b/src/components/ui/button.tsx @@ -1,36 +1,37 @@ -import * as React from "react" -import { Slot } from "@radix-ui/react-slot" -import { cva, type VariantProps } from "class-variance-authority" +import { Slot } from '@radix-ui/react-slot' +import { type VariantProps, cva } from 'class-variance-authority' +import type * as React from 'react' -import { cn } from "@/lib/utils" +import { cn } from '@/lib/utils' const buttonVariants = cva( - "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive", + `inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive`, { variants: { variant: { default: - "bg-primary text-primary-foreground shadow-xs hover:bg-primary/90", + 'bg-primary text-primary-foreground shadow-xs hover:bg-primary/90', destructive: - "bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60", + 'bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:bg-destructive/60 dark:focus-visible:ring-destructive/40', outline: - "border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50", + 'border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:border-input dark:bg-input/30 dark:hover:bg-input/50', secondary: - "bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80", + 'bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80', ghost: - "hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50", - link: "text-primary underline-offset-4 hover:underline", + 'hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50', + link: 'text-primary underline-offset-4 hover:underline', }, size: { - default: "h-9 px-4 py-2 has-[>svg]:px-3", - sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5", - lg: "h-10 rounded-md px-6 has-[>svg]:px-4", - icon: "size-9", + table: 'h-9 px-2 py-2', + default: 'h-9 px-4 py-2 has-[>svg]:px-3', + sm: 'h-8 gap-1.5 rounded-md px-3 has-[>svg]:px-2.5', + lg: 'h-10 rounded-md px-6 has-[>svg]:px-4', + icon: 'size-9', }, }, defaultVariants: { - variant: "default", - size: "default", + variant: 'default', + size: 'default', }, } ) @@ -41,15 +42,15 @@ function Button({ size, asChild = false, ...props -}: React.ComponentProps<"button"> & +}: React.ComponentProps<'button'> & VariantProps & { asChild?: boolean }) { - const Comp = asChild ? Slot : "button" + const Comp = asChild ? Slot : 'button' return ( diff --git a/src/server/db/types.ts b/src/server/db/types.ts new file mode 100644 index 0000000..ddb09b5 --- /dev/null +++ b/src/server/db/types.ts @@ -0,0 +1,3 @@ +import type { player_games } from '@/server/db/schema' + +export type SelectGames = typeof player_games.$inferSelect