mirror of
https://github.com/ershisan99/www.git
synced 2025-12-18 21:09:23 +00:00
opponents table
This commit is contained in:
@@ -417,52 +417,58 @@ export default function LogParser() {
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
{moneyReports.map((report, i) => (
|
{moneyReports.map((report, i) => {
|
||||||
// biome-ignore lint/suspicious/noArrayIndexKey: <explanation>
|
const mostShops =
|
||||||
<div key={i}>
|
report.spentPerShop.length > report.spentPerShopOpponent.length
|
||||||
<div className='font-bold text-lg'>Game {i + 1}</div>
|
? report.spentPerShop.length
|
||||||
<Table>
|
: report.spentPerShopOpponent.length
|
||||||
<TableHeader>
|
return (
|
||||||
<TableRow>
|
// biome-ignore lint/suspicious/noArrayIndexKey: <explanation>
|
||||||
<TableHead className={'text-right font-mono'}>
|
<div key={i}>
|
||||||
Shop #
|
<div className='font-bold text-lg'>Game {i + 1}</div>
|
||||||
</TableHead>
|
<Table>
|
||||||
<TableHead className={'text-right font-mono'}>
|
<TableHeader>
|
||||||
Logs owner
|
<TableRow>
|
||||||
</TableHead>
|
<TableHead className={'text-right font-mono'}>
|
||||||
<TableHead className={'text-right font-mono'}>
|
Shop #
|
||||||
Opponent
|
</TableHead>
|
||||||
</TableHead>
|
<TableHead className={'text-right font-mono'}>
|
||||||
</TableRow>
|
Logs owner
|
||||||
</TableHeader>
|
</TableHead>
|
||||||
<TableBody>
|
<TableHead className={'text-right font-mono'}>
|
||||||
{report.spentPerShop.map((spent, j) => (
|
Opponent
|
||||||
// biome-ignore lint/suspicious/noArrayIndexKey: <explanation>
|
</TableHead>
|
||||||
<TableRow key={j}>
|
</TableRow>
|
||||||
|
</TableHeader>
|
||||||
|
<TableBody>
|
||||||
|
{Array.from({ length: mostShops }).map((_, j) => (
|
||||||
|
// biome-ignore lint/suspicious/noArrayIndexKey: <explanation>
|
||||||
|
<TableRow key={j}>
|
||||||
|
<TableCell className={'text-right font-mono'}>
|
||||||
|
{j + 1}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className={'text-right font-mono'}>
|
||||||
|
{report.spentPerShop[j] ?? 'Skipped'}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className={'text-right font-mono'}>
|
||||||
|
{report.spentPerShopOpponent[j] ?? 'Skipped'}
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
))}
|
||||||
|
<TableRow>
|
||||||
|
<TableCell>Total</TableCell>
|
||||||
<TableCell className={'text-right font-mono'}>
|
<TableCell className={'text-right font-mono'}>
|
||||||
{j + 1}
|
{report.totalSpent}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell className={'text-right font-mono'}>
|
<TableCell className={'text-right font-mono'}>
|
||||||
{spent ?? 'Skipped'}
|
{report.totalSpentOpponent}
|
||||||
</TableCell>
|
|
||||||
<TableCell className={'text-right font-mono'}>
|
|
||||||
{report.spentPerShopOpponent[j] ?? 'Skipped'}
|
|
||||||
</TableCell>
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
))}
|
</TableBody>
|
||||||
<TableRow>
|
</Table>
|
||||||
<TableCell>Total</TableCell>
|
</div>
|
||||||
<TableCell className={'text-right font-mono'}>
|
)
|
||||||
{report.totalSpent}
|
})}
|
||||||
</TableCell>
|
|
||||||
<TableCell className={'text-right font-mono'}>
|
|
||||||
{report.totalSpentOpponent}
|
|
||||||
</TableCell>
|
|
||||||
</TableRow>
|
|
||||||
</TableBody>
|
|
||||||
</Table>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
266
src/app/(home)/players/[id]/_components/opponents-table.tsx
Normal file
266
src/app/(home)/players/[id]/_components/opponents-table.tsx
Normal file
@@ -0,0 +1,266 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
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,
|
||||||
|
MinusCircle,
|
||||||
|
} from 'lucide-react'
|
||||||
|
import Link from 'next/link'
|
||||||
|
import { memo, useMemo, useState } from 'react'
|
||||||
|
import { groupBy } from 'remeda'
|
||||||
|
|
||||||
|
const numberFormatter = new Intl.NumberFormat('en-US', {
|
||||||
|
signDisplay: 'exceptZero',
|
||||||
|
})
|
||||||
|
|
||||||
|
const columnHelper = createColumnHelper<Stats>()
|
||||||
|
|
||||||
|
const useColumns = () => {
|
||||||
|
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('totalGames', {
|
||||||
|
header: 'Games Played',
|
||||||
|
meta: { className: 'justify-end' },
|
||||||
|
cell: (info) => {
|
||||||
|
const totalGames = info.getValue()
|
||||||
|
return (
|
||||||
|
<span className='flex w-full justify-end font-mono'>
|
||||||
|
{totalGames}
|
||||||
|
</span>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
|
||||||
|
columnHelper.accessor('wins', {
|
||||||
|
header: 'Wins',
|
||||||
|
meta: { className: 'justify-end' },
|
||||||
|
cell: (info) => {
|
||||||
|
const wins = info.getValue()
|
||||||
|
return (
|
||||||
|
<span className='flex w-full justify-end font-mono'>{wins}</span>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
columnHelper.accessor('losses', {
|
||||||
|
header: 'Losses',
|
||||||
|
meta: { className: 'justify-end' },
|
||||||
|
cell: (info) => {
|
||||||
|
const losses = info.getValue()
|
||||||
|
return (
|
||||||
|
<span className='flex w-full justify-end font-mono'>{losses}</span>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
columnHelper.accessor('winRate', {
|
||||||
|
header: 'Win rate',
|
||||||
|
meta: { className: 'justify-end' },
|
||||||
|
cell: (info) => {
|
||||||
|
const winRate = info.getValue()
|
||||||
|
return (
|
||||||
|
<span
|
||||||
|
className={cn(
|
||||||
|
'flex items-center justify-end font-medium font-mono',
|
||||||
|
winRate === 50
|
||||||
|
? 'text-zink-800 dark:text-zink-200'
|
||||||
|
: winRate > 50
|
||||||
|
? 'text-emerald-500'
|
||||||
|
: 'text-rose-500'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{Math.round(winRate)}%
|
||||||
|
</span>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
columnHelper.accessor('totalMMRChange', {
|
||||||
|
header: 'Total MMR change',
|
||||||
|
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-zink-800 dark:text-zink-200'
|
||||||
|
: mmrChange > 0
|
||||||
|
? 'text-emerald-500'
|
||||||
|
: 'text-rose-500'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{numberFormatter.format(Math.trunc(mmrChange))}
|
||||||
|
{mmrChange === 0 ? (
|
||||||
|
<MinusCircle className='ml-1 h-4 w-4' />
|
||||||
|
) : mmrChange > 0 ? (
|
||||||
|
<ArrowUpCircle className='ml-1 h-4 w-4' />
|
||||||
|
) : (
|
||||||
|
<ArrowDownCircle className='ml-1 h-4 w-4' />
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
[]
|
||||||
|
)
|
||||||
|
}
|
||||||
|
type Stats = {
|
||||||
|
totalGames: number
|
||||||
|
wins: number
|
||||||
|
losses: number
|
||||||
|
opponentName: string
|
||||||
|
opponentId: string
|
||||||
|
totalMMRChange: number
|
||||||
|
winRate: number
|
||||||
|
}
|
||||||
|
|
||||||
|
function RawOpponentsTable({ games }: { games: SelectGames[] }) {
|
||||||
|
const [sorting, setSorting] = useState<SortingState>([
|
||||||
|
{ id: 'totalGames', desc: true },
|
||||||
|
])
|
||||||
|
const grouped = useMemo(
|
||||||
|
() => groupBy(games, (game) => game.opponentId),
|
||||||
|
[games]
|
||||||
|
)
|
||||||
|
const tableData: Stats[] = useMemo(
|
||||||
|
() =>
|
||||||
|
Object.values(grouped).map((gamesAgainstOpponent) => {
|
||||||
|
const totalGames = gamesAgainstOpponent.length
|
||||||
|
let wins = 0
|
||||||
|
let losses = 0
|
||||||
|
let totalMMRChange = 0
|
||||||
|
for (const game of gamesAgainstOpponent) {
|
||||||
|
totalMMRChange += game.mmrChange
|
||||||
|
|
||||||
|
if (game.mmrChange > 0) wins++
|
||||||
|
else if (game.mmrChange < 0) losses++
|
||||||
|
}
|
||||||
|
const stats: Stats = {
|
||||||
|
totalGames,
|
||||||
|
wins,
|
||||||
|
losses,
|
||||||
|
opponentName: gamesAgainstOpponent[0].opponentName,
|
||||||
|
opponentId: gamesAgainstOpponent[0].opponentId,
|
||||||
|
totalMMRChange,
|
||||||
|
winRate: (wins / totalGames) * 100,
|
||||||
|
}
|
||||||
|
return stats
|
||||||
|
}),
|
||||||
|
[grouped]
|
||||||
|
)
|
||||||
|
|
||||||
|
const columns = useColumns()
|
||||||
|
|
||||||
|
const table = useReactTable({
|
||||||
|
data: tableData,
|
||||||
|
columns,
|
||||||
|
state: {
|
||||||
|
sorting,
|
||||||
|
},
|
||||||
|
onSortingChange: setSorting,
|
||||||
|
getCoreRowModel: getCoreRowModel(),
|
||||||
|
getSortedRowModel: getSortedRowModel(),
|
||||||
|
getRowId: (originalRow) => originalRow.opponentId,
|
||||||
|
})
|
||||||
|
|
||||||
|
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 === 'desc' ? '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>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const OpponentsTable = memo(RawOpponentsTable)
|
||||||
|
OpponentsTable.displayName = 'OpponentsTable'
|
||||||
@@ -9,7 +9,8 @@ import {
|
|||||||
import type React from 'react'
|
import type React from 'react'
|
||||||
import { useState } from 'react'
|
import { useState } from 'react'
|
||||||
|
|
||||||
import { GamesTable } from '@/app/players/[id]/_components/games-table'
|
import { GamesTable } from '@/app/(home)/players/[id]/_components/games-table'
|
||||||
|
import { OpponentsTable } from '@/app/(home)/players/[id]/_components/opponents-table'
|
||||||
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'
|
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'
|
||||||
import { Badge } from '@/components/ui/badge'
|
import { Badge } from '@/components/ui/badge'
|
||||||
import {
|
import {
|
||||||
@@ -48,6 +49,7 @@ const dateFormatter = new Intl.DateTimeFormat('en-US', {
|
|||||||
|
|
||||||
export function UserInfo() {
|
export function UserInfo() {
|
||||||
const [filter, setFilter] = useState('all')
|
const [filter, setFilter] = useState('all')
|
||||||
|
|
||||||
const [leaderboardFilter, setLeaderboardFilter] = useState('all')
|
const [leaderboardFilter, setLeaderboardFilter] = useState('all')
|
||||||
const { id } = useParams()
|
const { id } = useParams()
|
||||||
if (!id || typeof id !== 'string') return null
|
if (!id || typeof id !== 'string') return null
|
||||||
@@ -55,15 +57,14 @@ export function UserInfo() {
|
|||||||
// Fetch games data unconditionally
|
// Fetch games data unconditionally
|
||||||
const gamesQuery = api.history.user_games.useSuspenseQuery({ user_id: id })
|
const gamesQuery = api.history.user_games.useSuspenseQuery({ user_id: id })
|
||||||
const games = gamesQuery[0] || [] // Ensure games is always an array
|
const games = gamesQuery[0] || [] // Ensure games is always an array
|
||||||
|
|
||||||
const [discord_user] = api.discord.get_user_by_id.useSuspenseQuery({
|
const [discord_user] = api.discord.get_user_by_id.useSuspenseQuery({
|
||||||
user_id: id,
|
user_id: id,
|
||||||
})
|
})
|
||||||
|
|
||||||
// Mock data for the two leaderboards - replace with actual API calls
|
|
||||||
const [rankedLeaderboard] = api.leaderboard.get_leaderboard.useSuspenseQuery({
|
const [rankedLeaderboard] = api.leaderboard.get_leaderboard.useSuspenseQuery({
|
||||||
channel_id: RANKED_CHANNEL,
|
channel_id: RANKED_CHANNEL,
|
||||||
})
|
})
|
||||||
|
|
||||||
const [vanillaLeaderboard] = api.leaderboard.get_leaderboard.useSuspenseQuery(
|
const [vanillaLeaderboard] = api.leaderboard.get_leaderboard.useSuspenseQuery(
|
||||||
{
|
{
|
||||||
channel_id: VANILLA_CHANNEL,
|
channel_id: VANILLA_CHANNEL,
|
||||||
@@ -78,11 +79,13 @@ export function UserInfo() {
|
|||||||
user_id: id,
|
user_id: id,
|
||||||
})
|
})
|
||||||
|
|
||||||
// Filter games by leaderboard if needed
|
|
||||||
const filteredGamesByLeaderboard =
|
const filteredGamesByLeaderboard =
|
||||||
leaderboardFilter === 'all'
|
leaderboardFilter === 'all'
|
||||||
? games
|
? games
|
||||||
: games.filter((game) => game.gameType === leaderboardFilter)
|
: games.filter(
|
||||||
|
(game) =>
|
||||||
|
game.gameType.toLowerCase() === leaderboardFilter?.toLowerCase()
|
||||||
|
)
|
||||||
|
|
||||||
// Filter by result
|
// Filter by result
|
||||||
const filteredGames =
|
const filteredGames =
|
||||||
@@ -332,6 +335,7 @@ export function UserInfo() {
|
|||||||
<div className='mb-6 flex flex-col items-start justify-between gap-4 sm:flex-row sm:items-center'>
|
<div className='mb-6 flex flex-col items-start justify-between gap-4 sm:flex-row sm:items-center'>
|
||||||
<TabsList className='bg-gray-100 dark:bg-zinc-800'>
|
<TabsList className='bg-gray-100 dark:bg-zinc-800'>
|
||||||
<TabsTrigger value='matches'>Match History</TabsTrigger>
|
<TabsTrigger value='matches'>Match History</TabsTrigger>
|
||||||
|
<TabsTrigger value='opponents'>Opponents</TabsTrigger>
|
||||||
<TabsTrigger value='stats'>Statistics</TabsTrigger>
|
<TabsTrigger value='stats'>Statistics</TabsTrigger>
|
||||||
<TabsTrigger value='achievements'>Achievements</TabsTrigger>
|
<TabsTrigger value='achievements'>Achievements</TabsTrigger>
|
||||||
</TabsList>
|
</TabsList>
|
||||||
@@ -376,6 +380,13 @@ export function UserInfo() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
<TabsContent value='opponents' className='m-0'>
|
||||||
|
<div className='overflow-hidden rounded-lg border'>
|
||||||
|
<div className='overflow-x-auto'>
|
||||||
|
<OpponentsTable games={filteredGames} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</TabsContent>
|
||||||
|
|
||||||
<TabsContent value='stats' className='m-0'>
|
<TabsContent value='stats' className='m-0'>
|
||||||
<div className='grid grid-cols-1 gap-6 md:grid-cols-2'>
|
<div className='grid grid-cols-1 gap-6 md:grid-cols-2'>
|
||||||
|
|||||||
Reference in New Issue
Block a user