'use client' import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, } from '@/components/ui/dropdown-menu' import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger, } from '@/components/ui/mobile-tooltip' import type React from 'react' import { useState } from 'react' import { GamesTable } from '@/app/(home)/players/[id]/_components/games-table' import { MmrTrendChart } from '@/app/(home)/players/[id]/_components/mmr-trend-chart' import { WinrateTrendChart } from '@/app/(home)/players/[id]/_components/winrate-trend-chart' import { OpponentsTable } from '@/app/(home)/players/[id]/_components/opponents-table' import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar' import { Badge } from '@/components/ui/badge' import { Button } from '@/components/ui/button' import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from '@/components/ui/select' import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs' import { cn } from '@/lib/utils' import { RANKED_CHANNEL, VANILLA_CHANNEL } from '@/shared/constants' import { api } from '@/trpc/react' import { ArrowDownCircle, ArrowUpCircle, BarChart3, ChevronDown, ChevronUp, EllipsisVertical, Filter, IceCreamCone, ShieldHalf, Star, Trophy, UserIcon, } from 'lucide-react' import { ExternalIcon } from 'next/dist/client/components/react-dev-overlay/ui/icons/external' import Link from 'next/link' import { useParams } from 'next/navigation' import { isNonNullish } from 'remeda' const numberFormatter = new Intl.NumberFormat('en-US', { signDisplay: 'exceptZero', }) const dateFormatter = new Intl.DateTimeFormat('en-US', { dateStyle: 'long', }) export function UserInfo() { const [filter, setFilter] = useState('all') const [leaderboardFilter, setLeaderboardFilter] = useState('all') const { id } = useParams() if (!id || typeof id !== 'string') return null // Fetch games data unconditionally const gamesQuery = api.history.user_games.useSuspenseQuery({ user_id: id }) const games = gamesQuery[0] || [] // Ensure games is always an array const [discord_user] = api.discord.get_user_by_id.useSuspenseQuery({ user_id: id, }) const [rankedLeaderboard] = api.leaderboard.get_leaderboard.useSuspenseQuery({ channel_id: RANKED_CHANNEL, }) const [vanillaLeaderboard] = api.leaderboard.get_leaderboard.useSuspenseQuery( { channel_id: VANILLA_CHANNEL, } ) const [vanillaUserRank] = api.leaderboard.get_user_rank.useSuspenseQuery({ channel_id: VANILLA_CHANNEL, user_id: id, }) const [rankedUserRank] = api.leaderboard.get_user_rank.useSuspenseQuery({ channel_id: RANKED_CHANNEL, user_id: id, }) const filteredGamesByLeaderboard = leaderboardFilter === 'all' ? games : games.filter( (game) => game.gameType.toLowerCase() === leaderboardFilter?.toLowerCase() ) // Filter by result const filteredGames = filter === 'all' ? filteredGamesByLeaderboard : filter === 'wins' ? filteredGamesByLeaderboard.filter((game) => game.result === 'win') : filter === 'losses' ? filteredGamesByLeaderboard.filter((game) => game.result === 'loss') : filter === 'wins-and-losses' ? filteredGamesByLeaderboard.filter( (game) => game.result === 'win' || game.result === 'loss' ) : filteredGamesByLeaderboard.filter((game) => game.result === 'tie') const games_played = games.length let wins = 0 let losses = 0 let ties = 0 for (const game of games) { if (game.result === 'win') { wins++ } else if (game.result === 'loss') { losses++ } else if (game.result === 'tie' || game.result === 'unknown') { ties++ } else { ties++ } } const aliases = [...new Set(games.map((g) => g.playerName))] const lastGame = games.at(0) const currentName = lastGame?.playerName ?? discord_user.username const meaningful_games = games_played - ties const profileData = { username: currentName, avatar: discord_user.avatar_url, games: games_played, meaningful_games, wins, losses, ties, winRate: meaningful_games > 0 ? Math.ceil((wins / meaningful_games) * 100) : 0, lossRate: meaningful_games > 0 ? Math.floor((losses / meaningful_games) * 100) : 0, } const firstGame = games.at(-1) // Get last games for each leaderboard const lastRankedGame = games .filter((game) => game.gameType === 'ranked') .at(0) const lastVanillaGame = games .filter((game) => game.gameType.toLowerCase() === 'vanilla') .at(0) console.log(games) const avgOpponentMmr = games .filter( (g) => g.result !== 'tie' && g.result !== 'unknown' && g.gameType === 'ranked' ) .reduce((acc, g) => acc + g.opponentMmr, 0) / meaningful_games return (
{profileData.username.slice(0, 2).toUpperCase()}

{profileData.username}

Also known as:

    {aliases.map((alias) => (
  • {alias}
  • ))}
Stream widget

{firstGame ? ( <>First game: {dateFormatter.format(firstGame.gameTime)} ) : ( <>No games played yet )}

{!!rankedLeaderboard && ( Ranked Queue:{' '} {isNonNullish(rankedUserRank?.rank) ? `#${rankedUserRank.rank}` : 'N/A'} )} {!!vanillaLeaderboard && ( Vanilla Queue:{' '} {isNonNullish(vanillaUserRank?.rank) ? `#${vanillaUserRank.rank}` : 'N/A'} )}
} description='Total matches' /> } description={`${profileData.winRate}% win rate`} accentColor='text-emerald-500' /> } description={`${profileData.lossRate}% loss rate`} accentColor='text-rose-500' /> {isNonNullish(rankedUserRank?.mmr) && ( 0 ? 'text-emerald-500' : 'text-rose-500' )} > {lastRankedGame.mmrChange === 0 ? ( 'Tied' ) : lastRankedGame.mmrChange > 0 ? ( ) : ( )} {lastRankedGame.mmrChange !== 0 ? numberFormatter.format( Math.trunc(lastRankedGame.mmrChange) ) : null}{' '} last match ) : null } icon={ } accentColor='text-zink-800 dark:text-zink-200' /> )} {isNonNullish(vanillaUserRank?.mmr) && ( } accentColor='text-zink-800 dark:text-zink-200' description={ lastVanillaGame ? ( 0 ? 'text-emerald-500' : 'text-rose-500' )} > {lastVanillaGame.mmrChange === 0 ? ( 'Tied' ) : lastVanillaGame.mmrChange > 0 ? ( ) : ( )} {lastVanillaGame.mmrChange !== 0 ? numberFormatter.format( Math.trunc(lastVanillaGame.mmrChange) ) : null}{' '} last match ) : null } /> )} } description={''} accentColor='text-zink-800 dark:text-zink-200' />
Match History Opponents MMR Trends Winrate Trends Statistics Achievements
{(rankedLeaderboard || lastRankedGame) && ( } accentColor='text-violet-500' /> )} {(vanillaLeaderboard || lastVanillaGame) && ( } accentColor='text-amber-500' /> )} {!rankedLeaderboard && !vanillaLeaderboard && !lastRankedGame && !lastVanillaGame && (

No leaderboard data available

)}

Achievements coming soon

) } interface StatsCardProps { title: string value: number icon: React.ReactNode description: React.ReactNode accentColor?: string } function StatsCard({ title, value, icon, description, accentColor = 'text-violet-500', }: StatsCardProps) { return (

{title}

{icon}

{value}

{description}

) } interface LeaderboardStatsCardProps { title: string rank?: number mmr?: number icon: React.ReactNode accentColor?: string } function LeaderboardStatsCard({ title, rank, mmr, icon, accentColor = 'text-violet-500', }: LeaderboardStatsCardProps) { return (
{icon}

{title}

{rank !== undefined && (

Rank

#{rank}

)} {mmr !== undefined && (

MMR

{mmr}

)} {rank === undefined && mmr === undefined && (

No data available

)}
) }