mirror of
https://github.com/ershisan99/www.git
synced 2025-12-17 12:34:17 +00:00
refactor user table, make opponent name a link
This commit is contained in:
5
bun.lock
5
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=="],
|
||||
|
||||
@@ -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",
|
||||
|
||||
228
src/app/players/[id]/_components/games-table.tsx
Normal file
228
src/app/players/[id]/_components/games-table.tsx
Normal file
@@ -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<SelectGames>()
|
||||
|
||||
const useColumns = () => {
|
||||
const format = useFormatter()
|
||||
return useMemo(
|
||||
() => [
|
||||
columnHelper.accessor('opponentName', {
|
||||
meta: { className: 'pl-4' },
|
||||
header: 'Opponent',
|
||||
cell: (info) => (
|
||||
<Link
|
||||
href={`/players/${info.row.original.opponentId}`}
|
||||
className='pl-4 font-medium hover:underline'
|
||||
>
|
||||
{info.getValue()}
|
||||
</Link>
|
||||
),
|
||||
}),
|
||||
columnHelper.accessor('gameType', {
|
||||
header: 'Game Type',
|
||||
cell: (info) => {
|
||||
const gameType = info.getValue()
|
||||
return (
|
||||
<Badge
|
||||
variant='outline'
|
||||
className={cn(
|
||||
'font-normal capitalize',
|
||||
gameType === 'ranked'
|
||||
? 'border-violet-200 bg-violet-50 text-violet-700 dark:border-violet-800 dark:bg-violet-950 dark:text-violet-300'
|
||||
: gameType.toLowerCase() === 'vanilla'
|
||||
? 'border-amber-200 bg-amber-50 text-amber-700 dark:border-amber-800 dark:bg-amber-950 dark:text-amber-300'
|
||||
: 'border-zinc-200 bg-zinc-50 text-zinc-700 dark:border-zinc-800 dark:bg-zinc-700 dark:text-zinc-300'
|
||||
)}
|
||||
>
|
||||
{info.getValue()}
|
||||
</Badge>
|
||||
)
|
||||
},
|
||||
}),
|
||||
columnHelper.accessor('opponentMmr', {
|
||||
header: 'Opponent MMR',
|
||||
meta: { className: 'justify-end' },
|
||||
cell: (info) => (
|
||||
<span className='flex w-full justify-end font-mono'>
|
||||
{Math.trunc(info.getValue())}
|
||||
</span>
|
||||
),
|
||||
}),
|
||||
columnHelper.accessor('playerMmr', {
|
||||
header: 'MMR',
|
||||
meta: { className: 'justify-end' },
|
||||
cell: (info) => (
|
||||
<span className='flex w-full justify-end font-mono'>
|
||||
{Math.trunc(info.getValue())}
|
||||
</span>
|
||||
),
|
||||
}),
|
||||
columnHelper.accessor('mmrChange', {
|
||||
header: 'Result',
|
||||
meta: { className: 'justify-end' },
|
||||
cell: (info) => {
|
||||
const mmrChange = info.getValue()
|
||||
return (
|
||||
<span
|
||||
className={cn(
|
||||
'flex items-center justify-end font-medium font-mono',
|
||||
mmrChange > 0 ? 'text-emerald-500' : 'text-rose-500'
|
||||
)}
|
||||
>
|
||||
{numberFormatter.format(Math.trunc(mmrChange))}
|
||||
{mmrChange > 0 ? (
|
||||
<ArrowUpCircle className='ml-1 h-4 w-4' />
|
||||
) : (
|
||||
<ArrowDownCircle className='ml-1 h-4 w-4' />
|
||||
)}
|
||||
</span>
|
||||
)
|
||||
},
|
||||
}),
|
||||
columnHelper.accessor('gameTime', {
|
||||
header: 'Date',
|
||||
meta: { className: 'justify-end' },
|
||||
cell: (info) => (
|
||||
<span
|
||||
className={'flex items-center justify-end font-medium font-mono'}
|
||||
>
|
||||
{format.dateTime(info.getValue(), {
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
})}
|
||||
</span>
|
||||
),
|
||||
}),
|
||||
columnHelper.accessor('gameTime', {
|
||||
header: 'Time',
|
||||
meta: { className: 'justify-end pr-4' },
|
||||
cell: (info) => (
|
||||
<span
|
||||
className={
|
||||
'flex items-center justify-end pr-4 font-medium font-mono'
|
||||
}
|
||||
>
|
||||
{format.dateTime(info.getValue(), {
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
})}
|
||||
</span>
|
||||
),
|
||||
id: 'time',
|
||||
}),
|
||||
],
|
||||
[]
|
||||
)
|
||||
}
|
||||
|
||||
export function GamesTable({ games }: { games: SelectGames[] }) {
|
||||
const [sorting, setSorting] = useState<SortingState>([])
|
||||
const columns = useColumns()
|
||||
const table = useReactTable({
|
||||
data: games,
|
||||
columns,
|
||||
state: {
|
||||
sorting,
|
||||
},
|
||||
onSortingChange: setSorting,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
getSortedRowModel: getSortedRowModel(),
|
||||
getRowId: (originalRow) => originalRow.gameNum.toString(),
|
||||
})
|
||||
|
||||
return (
|
||||
<div className='rounded-md border'>
|
||||
<Table>
|
||||
<TableHeader>
|
||||
{table.getHeaderGroups().map((headerGroup) => (
|
||||
<TableRow key={headerGroup.id}>
|
||||
{headerGroup.headers.map((header) => {
|
||||
const sortDirection = header.column.getIsSorted()
|
||||
return (
|
||||
<TableHead key={header.id} className={'px-0'}>
|
||||
<span
|
||||
className={cn(
|
||||
'flex w-full items-center',
|
||||
(header.column.columnDef.meta as any)?.className
|
||||
)}
|
||||
>
|
||||
<Button
|
||||
className={cn(
|
||||
header.column.getCanSort() &&
|
||||
'cursor-pointer select-none',
|
||||
(
|
||||
header.column.columnDef.meta as any
|
||||
)?.className?.includes('justify-end') &&
|
||||
'flex-row-reverse'
|
||||
)}
|
||||
size={'table'}
|
||||
variant='ghost'
|
||||
onClick={header.column.getToggleSortingHandler()}
|
||||
>
|
||||
{flexRender(
|
||||
header.column.columnDef.header,
|
||||
header.getContext()
|
||||
)}
|
||||
{sortDirection ? (
|
||||
<ArrowUp
|
||||
className={cn(
|
||||
'transition-transform',
|
||||
sortDirection === 'asc' ? 'rotate-180' : ''
|
||||
)}
|
||||
/>
|
||||
) : (
|
||||
<div className={'h-4 w-4'} />
|
||||
)}
|
||||
</Button>
|
||||
</span>
|
||||
</TableHead>
|
||||
)
|
||||
})}
|
||||
</TableRow>
|
||||
))}
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{table.getRowModel().rows.map((row) => (
|
||||
<TableRow key={row.id}>
|
||||
{row.getVisibleCells().map((cell) => (
|
||||
<TableCell key={cell.id}>
|
||||
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
||||
</TableCell>
|
||||
))}
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -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() {
|
||||
<TabsContent value='matches' className='m-0'>
|
||||
<div className='overflow-hidden rounded-lg border'>
|
||||
<div className='overflow-x-auto'>
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow className='bg-gray-50 dark:bg-zinc-800/50'>
|
||||
<TableHead className='w-[100px]'>Game Type</TableHead>
|
||||
<TableHead>Opponent</TableHead>
|
||||
<TableHead className='text-right'>
|
||||
Opponent MMR
|
||||
</TableHead>
|
||||
<TableHead className='text-right'>MMR</TableHead>
|
||||
<TableHead className='text-right'>Result</TableHead>
|
||||
<TableHead className='text-center'>
|
||||
Leaderboard
|
||||
</TableHead>
|
||||
<TableHead className='text-right'>
|
||||
<span className='flex items-center justify-end gap-1'>
|
||||
<Calendar className='h-4 w-4' /> Date
|
||||
</span>
|
||||
</TableHead>
|
||||
<TableHead className='text-right'>
|
||||
<span className='flex items-center justify-end gap-1'>
|
||||
<Clock className='h-4 w-4' /> Time
|
||||
</span>
|
||||
</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{filteredGames.map((game) => (
|
||||
<TableRow
|
||||
key={game.gameId}
|
||||
className='transition-colors hover:bg-gray-50 dark:hover:bg-zinc-800/70'
|
||||
>
|
||||
<TableCell>
|
||||
<Badge
|
||||
variant='outline'
|
||||
className='font-normal capitalize'
|
||||
>
|
||||
{game.gameType}
|
||||
</Badge>
|
||||
</TableCell>
|
||||
<TableCell className='font-medium'>
|
||||
{game.opponentName}
|
||||
</TableCell>
|
||||
<TableCell className='text-right font-mono'>
|
||||
{Math.trunc(game.opponentMmr)}
|
||||
</TableCell>
|
||||
<TableCell className='text-right font-mono'>
|
||||
{Math.trunc(game.playerMmr)}
|
||||
</TableCell>
|
||||
<TableCell className='text-right font-mono'>
|
||||
{game.mmrChange > 0 ? (
|
||||
<span className='flex items-center justify-end font-medium text-emerald-500'>
|
||||
{numberFormatter.format(
|
||||
Math.trunc(game.mmrChange)
|
||||
)}
|
||||
<ArrowUpCircle className='ml-1 inline h-4 w-4' />
|
||||
</span>
|
||||
) : (
|
||||
<span className='flex items-center justify-end font-medium text-rose-500'>
|
||||
{numberFormatter.format(
|
||||
Math.trunc(game.mmrChange)
|
||||
)}
|
||||
<ArrowDownCircle className='ml-1 inline h-4 w-4' />
|
||||
</span>
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell className='text-center'>
|
||||
<Badge
|
||||
variant='outline'
|
||||
className={cn(
|
||||
'w-full font-normal',
|
||||
game.gameType === 'ranked'
|
||||
? 'border-violet-200 bg-violet-50 text-violet-700 dark:border-violet-800 dark:bg-violet-950 dark:text-violet-300'
|
||||
: game.gameType.toLowerCase() === 'vanilla'
|
||||
? 'border-amber-200 bg-amber-50 text-amber-700 dark:border-amber-800 dark:bg-amber-950 dark:text-amber-300'
|
||||
: 'border-zinc-200 bg-zinc-50 text-zinc-700 dark:border-zinc-800 dark:bg-zinc-700 dark:text-zinc-300'
|
||||
)}
|
||||
>
|
||||
{game.gameType === 'ranked'
|
||||
? 'Ranked Queue'
|
||||
: game.gameType.toLowerCase() === 'vanilla'
|
||||
? 'Vanilla Queue'
|
||||
: 'N/A'}
|
||||
</Badge>
|
||||
</TableCell>
|
||||
<TableCell className='text-right font-mono text-slate-500 dark:text-slate-400'>
|
||||
{format.dateTime(game.gameTime, {
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
})}
|
||||
</TableCell>
|
||||
<TableCell className='text-right font-mono text-slate-500 dark:text-slate-400'>
|
||||
{format.dateTime(game.gameTime, {
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
})}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
<GamesTable games={games} />
|
||||
</div>
|
||||
</div>
|
||||
</TabsContent>
|
||||
|
||||
@@ -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<typeof buttonVariants> & {
|
||||
asChild?: boolean
|
||||
}) {
|
||||
const Comp = asChild ? Slot : "button"
|
||||
const Comp = asChild ? Slot : 'button'
|
||||
|
||||
return (
|
||||
<Comp
|
||||
data-slot="button"
|
||||
data-slot='button'
|
||||
className={cn(buttonVariants({ variant, size, className }))}
|
||||
{...props}
|
||||
/>
|
||||
|
||||
3
src/server/db/types.ts
Normal file
3
src/server/db/types.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import type { player_games } from '@/server/db/schema'
|
||||
|
||||
export type SelectGames = typeof player_games.$inferSelect
|
||||
Reference in New Issue
Block a user