'use client' 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 { OpponentsTable } from '@/app/(home)/players/[id]/_components/opponents-table' import { WinrateTrendChart } from '@/app/(home)/players/[id]/_components/winrate-trend-chart' import { TimeZoneProvider } from '@/components/timezone-provider' import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar' import { Badge } from '@/components/ui/badge' 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 { type Season, filterGamesBySeason, getSeasonDisplayName, } from '@/shared/seasons' import { api } from '@/trpc/react' import { ArrowDownCircle, ArrowUpCircle, BarChart3, Calendar, ChevronDown, ChevronUp, Filter, IceCreamCone, ShieldHalf, Star, Trophy, Twitch, UserIcon, Youtube, } from 'lucide-react' import { useFormatter, useTimeZone } from 'next-intl' import { useParams } from 'next/navigation' import { isNonNullish } from 'remeda' const numberFormatter = new Intl.NumberFormat('en-US', { signDisplay: 'exceptZero', }) export function UserInfo() { return ( ) } function UserInfoComponent() { const [filter, setFilter] = useState('all') const format = useFormatter() const timeZone = useTimeZone() const [season, setSeason] = useState('season3') 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, }) // Fetch current season data const [rankedLeaderboard] = api.leaderboard.get_leaderboard.useSuspenseQuery({ channel_id: RANKED_CHANNEL, season, }) const [vanillaLeaderboard] = api.leaderboard.get_leaderboard.useSuspenseQuery( { channel_id: VANILLA_CHANNEL, season, } ) // Fetch current season user rank const [vanillaUserRankQ] = api.leaderboard.get_user_rank.useSuspenseQuery({ channel_id: VANILLA_CHANNEL, user_id: id, season, }) const [rankedUserRankQ] = api.leaderboard.get_user_rank.useSuspenseQuery({ channel_id: RANKED_CHANNEL, user_id: id, season, }) // Fetch Season 2 data for historic comparison const [rankedUserRankS2Q] = api.leaderboard.get_user_rank.useSuspenseQuery({ channel_id: RANKED_CHANNEL, user_id: id, season: 'season2', }) // Fetch Season 3 data for historic comparison const [rankedUserRankS3Q] = api.leaderboard.get_user_rank.useSuspenseQuery({ channel_id: RANKED_CHANNEL, user_id: id, season: 'season3', }) const rankedUserRank = rankedUserRankQ?.data const vanillaUserRank = vanillaUserRankQ?.data // Extract historic data const rankedUserRankS2 = rankedUserRankS2Q?.data const rankedUserRankS3 = rankedUserRankS3Q?.data // Determine which historic data to show (opposite of current season) const historicRankedData = season === 'season2' ? rankedUserRankS3 : rankedUserRankS2 // Filter games by season const seasonFilteredGames = filterGamesBySeason(games, season) const filteredGamesByLeaderboard = leaderboardFilter === 'all' ? seasonFilteredGames : seasonFilteredGames.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 = seasonFilteredGames.length let wins = 0 let losses = 0 let ties = 0 for (const game of seasonFilteredGames) { 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(seasonFilteredGames.map((g) => g.playerName))] const lastGame = seasonFilteredGames.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 = seasonFilteredGames.at(-1) // Get last games for each leaderboard const lastRankedGame = seasonFilteredGames .filter((game) => game.gameType === 'ranked') .at(0) const lastVanillaGame = seasonFilteredGames .filter((game) => game.gameType.toLowerCase() === 'vanilla') .at(0) // Calculate average opponent MMR for meaningful games const rankedMeaningfulGames = seasonFilteredGames.filter( (g) => g.result !== 'tie' && g.result !== 'unknown' && g.gameType === 'ranked' ) const avgOpponentMmr = rankedMeaningfulGames.length > 0 ? rankedMeaningfulGames.reduce((acc, g) => acc + g.opponentMmr, 0) / rankedMeaningfulGames.length : 0 return (
{profileData.username.slice(0, 2).toUpperCase()}

{profileData.username}

Also known as:

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

{firstGame ? ( <> First game:{' '} {format.dateTime(firstGame.gameTime, { dateStyle: 'long', timeZone, })} ) : ( <>No games played yet )}

{!!rankedLeaderboard && ( Ranked Queue:{' '} {isNonNullish(rankedUserRank?.rank) ? `#${rankedUserRank.rank}` : 'N/A'} )} {/* Show historic rank data if available */} {historicRankedData && season !== 'all' && ( {season === 'season3' ? 'Season 2' : 'Season 3'} Rank:{' '} {isNonNullish(historicRankedData.rank) ? `#${historicRankedData.rank}` : 'N/A'} {isNonNullish(historicRankedData.mmr) ? ` (${Math.round(historicRankedData.mmr)} MMR)` : ''} )} {!!vanillaLeaderboard && ( Vanilla Queue:{' '} {isNonNullish(vanillaUserRank?.rank) ? `#${vanillaUserRank.rank}` : 'N/A'} )} {discord_user.twitch_url && ( Twitch )} {discord_user.youtube_url && ( YouTube )}
} 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

)}
) }