mirror of
https://github.com/ershisan99/www.git
synced 2026-01-30 21:12:03 +00:00
add fumadocs
This commit is contained in:
3
src/app/(home)/about/page.tsx
Normal file
3
src/app/(home)/about/page.tsx
Normal file
@@ -0,0 +1,3 @@
|
||||
export default async function About() {
|
||||
return <div>About</div>
|
||||
}
|
||||
7
src/app/(home)/layout.tsx
Normal file
7
src/app/(home)/layout.tsx
Normal file
@@ -0,0 +1,7 @@
|
||||
import { baseOptions } from '@/app/layout.config'
|
||||
import { HomeLayout } from 'fumadocs-ui/layouts/home'
|
||||
import type { ReactNode } from 'react'
|
||||
|
||||
export default function Layout({ children }: { children: ReactNode }) {
|
||||
return <HomeLayout {...baseOptions}>{children}</HomeLayout>
|
||||
}
|
||||
13
src/app/(home)/leaderboards/layout.tsx
Normal file
13
src/app/(home)/leaderboards/layout.tsx
Normal file
@@ -0,0 +1,13 @@
|
||||
import { MainHeader } from '@/components/header'
|
||||
import type { ReactNode } from 'react'
|
||||
|
||||
export default function RootLayout({
|
||||
children,
|
||||
}: Readonly<{ children: ReactNode }>) {
|
||||
return (
|
||||
<>
|
||||
<MainHeader />
|
||||
{children}
|
||||
</>
|
||||
)
|
||||
}
|
||||
28
src/app/(home)/leaderboards/page.tsx
Normal file
28
src/app/(home)/leaderboards/page.tsx
Normal file
@@ -0,0 +1,28 @@
|
||||
import { LeaderboardPage } from '@/app/_components/leaderboard'
|
||||
import { auth } from '@/server/auth'
|
||||
import { RANKED_CHANNEL, VANILLA_CHANNEL } from '@/shared/constants'
|
||||
import { HydrateClient, api } from '@/trpc/server'
|
||||
import { Suspense } from 'react'
|
||||
|
||||
export default async function Home() {
|
||||
const session = await auth()
|
||||
await Promise.all([
|
||||
api.leaderboard.get_leaderboard.prefetch({
|
||||
channel_id: RANKED_CHANNEL,
|
||||
}),
|
||||
api.leaderboard.get_leaderboard.prefetch({
|
||||
channel_id: VANILLA_CHANNEL,
|
||||
}),
|
||||
])
|
||||
if (session?.user) {
|
||||
}
|
||||
|
||||
return (
|
||||
<Suspense>
|
||||
<HydrateClient>
|
||||
{/*<UserStats/>*/}
|
||||
<LeaderboardPage />
|
||||
</HydrateClient>
|
||||
</Suspense>
|
||||
)
|
||||
}
|
||||
427
src/app/(home)/page.tsx
Normal file
427
src/app/(home)/page.tsx
Normal file
@@ -0,0 +1,427 @@
|
||||
import { GithubLinks } from '@/app/_components/github-links'
|
||||
import { ModeToggle } from '@/components/mode-toggle'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
import { DropdownMenuTrigger } from '@/components/ui/dropdown-menu'
|
||||
import { Sheet, SheetContent, SheetTrigger } from '@/components/ui/sheet'
|
||||
import { SiGithub } from '@icons-pack/react-simple-icons'
|
||||
import {
|
||||
Award,
|
||||
BookOpen,
|
||||
ChevronRight,
|
||||
Info,
|
||||
Menu,
|
||||
MessageSquare,
|
||||
Trophy,
|
||||
Users,
|
||||
} from 'lucide-react'
|
||||
import Image from 'next/image'
|
||||
import Link from 'next/link'
|
||||
export default function Home() {
|
||||
return (
|
||||
<div className='flex min-h-screen flex-col'>
|
||||
<main className='flex-1'>
|
||||
<section className='space-y-6 pt-6 pb-8 md:pt-10 md:pb-12 lg:py-32'>
|
||||
<div className='container mx-auto flex flex-col items-center gap-4 text-center'>
|
||||
<Link
|
||||
href='https://github.com'
|
||||
className='rounded-2xl bg-muted px-4 py-1.5 font-medium text-sm'
|
||||
target='_blank'
|
||||
>
|
||||
Follow along on GitHub
|
||||
</Link>
|
||||
<h1 className='font-bold text-3xl leading-tight tracking-tighter md:text-5xl lg:text-6xl'>
|
||||
Play Balatro Against Your Friends
|
||||
</h1>
|
||||
<p className='max-w-[42rem] text-muted-foreground leading-normal sm:text-xl sm:leading-8'>
|
||||
The unofficial multiplayer mod for Balatro. Challenge your
|
||||
friends, compete in tournaments, and climb the global
|
||||
leaderboards.
|
||||
</p>
|
||||
<div className='flex flex-col gap-4 sm:flex-row sm:space-x-4'>
|
||||
<Button
|
||||
asChild
|
||||
size='lg'
|
||||
className='w-full bg-red-600 text-white hover:bg-red-700 sm:w-auto'
|
||||
>
|
||||
<Link href='/docs/installation'>
|
||||
Get Started
|
||||
<ChevronRight className='ml-2 h-4 w-4' />
|
||||
</Link>
|
||||
</Button>
|
||||
<Button variant='outline' size='lg' asChild>
|
||||
<Link href='/docs'>Learn More</Link>
|
||||
</Button>
|
||||
</div>
|
||||
<div className='mt-8 w-full max-w-5xl rounded-lg border bg-card p-4 shadow-xl'>
|
||||
<Image
|
||||
src='/multiplayer-screenshot.jpeg'
|
||||
width={1200}
|
||||
height={600}
|
||||
alt='Balatro Multiplayer Screenshot'
|
||||
className='rounded-md'
|
||||
priority
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<section className='container mx-auto space-y-6 py-8 md:py-12 lg:py-24'>
|
||||
<div className='mx-auto flex max-w-[58rem] flex-col items-center space-y-4 text-center'>
|
||||
<h2 className='font-bold text-3xl leading-tight tracking-tighter md:text-4xl'>
|
||||
Features
|
||||
</h2>
|
||||
<p className='max-w-[85%] text-muted-foreground leading-normal sm:text-lg sm:leading-7'>
|
||||
Everything you need to enjoy Balatro with friends and the wider
|
||||
community.
|
||||
</p>
|
||||
</div>
|
||||
<div className='mx-auto grid justify-center gap-4 sm:grid-cols-2 md:max-w-[64rem] md:grid-cols-3'>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Real-time Matches</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p>
|
||||
Challenge friends to head-to-head poker matches with
|
||||
synchronized gameplay.
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Tournaments</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p>
|
||||
Create and join tournaments with custom rules and brackets.
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Global Leaderboards</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p>
|
||||
Compete for the highest scores and track your progress against
|
||||
players worldwide.
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Spectator Mode</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p>
|
||||
Watch live matches between top players and learn new
|
||||
strategies.
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Custom Game Modes</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p>
|
||||
Play with modified rules and unique challenges created by the
|
||||
community.
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Cross-Platform</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p>
|
||||
Play with friends regardless of their platform - Windows, Mac,
|
||||
or Linux.
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className='container mx-auto border-t border-b py-12 md:py-16'>
|
||||
<div className='mx-auto mb-10 flex max-w-[58rem] flex-col items-center space-y-4 text-center'>
|
||||
<h2 className='font-bold text-3xl leading-tight tracking-tighter md:text-4xl'>
|
||||
Ways to Play
|
||||
</h2>
|
||||
<p className='max-w-[85%] text-muted-foreground leading-normal sm:text-lg sm:leading-7'>
|
||||
Choose how you want to experience Balatro Multiplayer
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className='mx-auto grid max-w-6xl gap-8 sm:grid-cols-2 lg:grid-cols-3'>
|
||||
<Card className='flex h-full flex-col'>
|
||||
<CardHeader className='pb-2'>
|
||||
<div className='mb-2 flex h-12 w-12 items-center justify-center rounded-full bg-red-100'>
|
||||
<Users className='h-6 w-6 text-red-600' />
|
||||
</div>
|
||||
<CardTitle>Direct Play</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className='flex-grow'>
|
||||
<p className='mb-4 text-muted-foreground'>
|
||||
Create a lobby and share the code with friends for casual
|
||||
matches.
|
||||
</p>
|
||||
<ul className='space-y-2 text-sm'>
|
||||
<li className='flex items-start gap-2'>
|
||||
<span className='font-medium'>
|
||||
• No matchmaking or ratings
|
||||
</span>
|
||||
</li>
|
||||
<li className='flex items-start gap-2'>
|
||||
<span className='font-medium'>
|
||||
• Customize all lobby options
|
||||
</span>
|
||||
</li>
|
||||
<li className='flex items-start gap-2'>
|
||||
<span className='font-medium'>
|
||||
• Play with friends directly
|
||||
</span>
|
||||
</li>
|
||||
<li className='flex items-start gap-2'>
|
||||
<span className='font-medium'>
|
||||
• Perfect for casual games
|
||||
</span>
|
||||
</li>
|
||||
</ul>
|
||||
</CardContent>
|
||||
<div className='mt-auto p-6 pt-0'>
|
||||
<Button variant='outline' className='w-full' asChild>
|
||||
<Link href='/docs/direct-play'>Learn More</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Card className='flex h-full flex-col'>
|
||||
<CardHeader className='pb-2'>
|
||||
<div className='mb-2 flex h-12 w-12 items-center justify-center rounded-full bg-red-100'>
|
||||
<Trophy className='h-6 w-6 text-red-600' />
|
||||
</div>
|
||||
<CardTitle>Matchmaking Queues</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className='flex-grow'>
|
||||
<p className='mb-4 text-muted-foreground'>
|
||||
Join ranked queues with MMR-based matchmaking and
|
||||
leaderboards.
|
||||
</p>
|
||||
<div className='space-y-4'>
|
||||
<div className='space-y-2 text-sm'>
|
||||
<h4 className='font-semibold'>Vanilla Queue:</h4>
|
||||
<ul className='space-y-1'>
|
||||
<li>• Original game balance</li>
|
||||
<li>• Has MMR but no MMR-based matchmaking</li>
|
||||
<li>• Same seed for fair competition</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div className='space-y-2 text-sm'>
|
||||
<h4 className='font-semibold'>Ranked Queue:</h4>
|
||||
<ul className='space-y-1'>
|
||||
<li>• Rebalanced cards and mechanics</li>
|
||||
<li>• MMR-based matchmaking</li>
|
||||
<li>• New jokers, reworked tarots, new planets</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
<div className='mt-auto p-6 pt-0'>
|
||||
<Button variant='outline' className='w-full' asChild>
|
||||
<Link href='/docs/matchmaking'>Learn More</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Card className='flex h-full flex-col'>
|
||||
<CardHeader className='pb-2'>
|
||||
<div className='mb-2 flex h-12 w-12 items-center justify-center rounded-full bg-red-100'>
|
||||
<MessageSquare className='h-6 w-6 text-red-600' />
|
||||
</div>
|
||||
<CardTitle>Custom LFG</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className='flex-grow'>
|
||||
<p className='mb-4 text-muted-foreground'>
|
||||
Find players for games with custom rulesets via our Discord
|
||||
community.
|
||||
</p>
|
||||
<ul className='space-y-2 text-sm'>
|
||||
<li className='flex items-start gap-2'>
|
||||
<span className='font-medium'>
|
||||
• Any ruleset or lobby options
|
||||
</span>
|
||||
</li>
|
||||
<li className='flex items-start gap-2'>
|
||||
<span className='font-medium'>
|
||||
• Compatible with mods (with agreement)
|
||||
</span>
|
||||
</li>
|
||||
<li className='flex items-start gap-2'>
|
||||
<span className='font-medium'>• No MMR or rankings</span>
|
||||
</li>
|
||||
<li className='flex items-start gap-2'>
|
||||
<span className='font-medium'>
|
||||
• Great for experimenting with new formats
|
||||
</span>
|
||||
</li>
|
||||
</ul>
|
||||
</CardContent>
|
||||
<div className='mt-auto p-6 pt-0'>
|
||||
<Button variant='outline' className='w-full' asChild>
|
||||
<Link href='https://discord.gg/balatro' target='_blank'>
|
||||
Join Discord
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<div className='mt-10 text-center'>
|
||||
<Button
|
||||
asChild
|
||||
size='lg'
|
||||
className='w-full bg-red-600 text-white hover:bg-red-700 sm:w-auto'
|
||||
>
|
||||
<Link href='/leaderboards'>
|
||||
View Leaderboards
|
||||
<Trophy className='ml-2 h-4 w-4' />
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className='container mx-auto py-8 md:py-12 lg:py-24'>
|
||||
<div className='mx-auto max-w-[85rem]'>
|
||||
<div className='mx-auto grid max-w-6xl gap-8 md:grid-cols-2'>
|
||||
<div className='flex flex-col justify-center space-y-4'>
|
||||
<div className='space-y-2'>
|
||||
<h2 className='font-bold text-3xl tracking-tighter sm:text-4xl'>
|
||||
How It Works
|
||||
</h2>
|
||||
<p className='max-w-[600px] text-muted-foreground md:text-xl/relaxed'>
|
||||
Getting started with Balatro Multiplayer is simple. Install
|
||||
the mod, connect with friends, and start playing.
|
||||
</p>
|
||||
</div>
|
||||
<ul className='grid gap-6'>
|
||||
<li className='flex items-start gap-4'>
|
||||
<div className='flex h-8 w-8 flex-shrink-0 items-center justify-center rounded-full bg-red-100 text-red-600'>
|
||||
1
|
||||
</div>
|
||||
<div className='space-y-1'>
|
||||
<h3 className='font-bold text-xl'>Install the Mod</h3>
|
||||
<p className='text-muted-foreground'>
|
||||
Download and install the multiplayer mod using our
|
||||
simple installer.
|
||||
</p>
|
||||
</div>
|
||||
</li>
|
||||
<li className='flex items-start gap-4'>
|
||||
<div className='flex h-8 w-8 flex-shrink-0 items-center justify-center rounded-full bg-red-100 text-red-600'>
|
||||
2
|
||||
</div>
|
||||
<div className='space-y-1'>
|
||||
<h3 className='font-bold text-xl'>
|
||||
Create or Join a Game
|
||||
</h3>
|
||||
<p className='text-muted-foreground'>
|
||||
Host your own game or join an existing one with a simple
|
||||
game code.
|
||||
</p>
|
||||
</div>
|
||||
</li>
|
||||
<li className='flex items-start gap-4'>
|
||||
<div className='flex h-8 w-8 flex-shrink-0 items-center justify-center rounded-full bg-red-100 text-red-600'>
|
||||
3
|
||||
</div>
|
||||
<div className='space-y-1'>
|
||||
<h3 className='font-bold text-xl'>Play Together</h3>
|
||||
<p className='text-muted-foreground'>
|
||||
Compete in real-time with synchronized gameplay and live
|
||||
scoring.
|
||||
</p>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
<div className='flex flex-col gap-2 min-[400px]:flex-row'>
|
||||
<Button
|
||||
size='lg'
|
||||
className='w-full bg-red-600 text-white hover:bg-red-700 sm:w-auto'
|
||||
asChild
|
||||
>
|
||||
<Link href='/docs/installation'>Install Now</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div className='flex items-center justify-center'>
|
||||
<Image
|
||||
src='/multiplayer-screenshot.jpeg'
|
||||
width={500}
|
||||
height={500}
|
||||
alt='Balatro Multiplayer Gameplay'
|
||||
className='rounded-lg shadow-xl'
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<section className='bg-muted py-12 md:py-16'>
|
||||
<div className='container mx-auto flex flex-col items-center justify-center gap-4 text-center'>
|
||||
<h2 className='font-bold text-3xl tracking-tighter sm:text-4xl'>
|
||||
Ready to Play?
|
||||
</h2>
|
||||
<p className='max-w-[600px] text-muted-foreground md:text-xl/relaxed'>
|
||||
Join thousands of Balatro players already enjoying multiplayer
|
||||
matches.
|
||||
</p>
|
||||
<Button
|
||||
size='lg'
|
||||
className='mt-4 w-full bg-red-600 text-white hover:bg-red-700 sm:w-auto'
|
||||
asChild
|
||||
>
|
||||
<Link href='/docs/installation'>Get Started Now</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
<footer className='border-t py-6 md:py-0'>
|
||||
<div className='container mx-auto flex flex-col items-center justify-between gap-4 md:h-24 md:flex-row'>
|
||||
<p className='text-center text-muted-foreground text-sm leading-loose md:text-left'>
|
||||
© {new Date().getFullYear()} Balatro Multiplayer Mod. All
|
||||
rights reserved. Not affiliated with LocalThunk or Playstack.
|
||||
</p>
|
||||
<nav className='flex items-center gap-4'>
|
||||
<Link
|
||||
href='/docs'
|
||||
className='text-muted-foreground text-sm underline-offset-4 hover:underline'
|
||||
>
|
||||
Documentation
|
||||
</Link>
|
||||
<Link
|
||||
href='/about'
|
||||
className='text-muted-foreground text-sm underline-offset-4 hover:underline'
|
||||
>
|
||||
About
|
||||
</Link>
|
||||
<Link
|
||||
href='/credits'
|
||||
className='text-muted-foreground text-sm underline-offset-4 hover:underline'
|
||||
>
|
||||
Credits
|
||||
</Link>
|
||||
<GithubLinks>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant='ghost' size='icon'>
|
||||
<SiGithub className='h-5 w-5' />
|
||||
<span className='sr-only'>GitHub</span>
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
</GithubLinks>
|
||||
</nav>
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
45
src/app/(home)/players/[id]/page.tsx
Normal file
45
src/app/(home)/players/[id]/page.tsx
Normal file
@@ -0,0 +1,45 @@
|
||||
import { auth } from '@/server/auth'
|
||||
import { RANKED_CHANNEL, VANILLA_CHANNEL } from '@/shared/constants'
|
||||
import { HydrateClient, api } from '@/trpc/server'
|
||||
import { Suspense } from 'react'
|
||||
import { UserInfo } from './user'
|
||||
|
||||
export default async function PlayerPage({
|
||||
params,
|
||||
}: {
|
||||
params: Promise<{ id: string }>
|
||||
}) {
|
||||
const session = await auth()
|
||||
const { id } = await params
|
||||
if (id) {
|
||||
await Promise.all([
|
||||
api.history.user_games.prefetch({
|
||||
user_id: id,
|
||||
}),
|
||||
api.discord.get_user_by_id.prefetch({
|
||||
user_id: id,
|
||||
}),
|
||||
api.leaderboard.get_leaderboard.prefetch({
|
||||
channel_id: RANKED_CHANNEL,
|
||||
}),
|
||||
api.leaderboard.get_leaderboard.prefetch({
|
||||
channel_id: VANILLA_CHANNEL,
|
||||
}),
|
||||
api.leaderboard.get_user_rank.prefetch({
|
||||
channel_id: RANKED_CHANNEL,
|
||||
user_id: id,
|
||||
}),
|
||||
api.leaderboard.get_user_rank.prefetch({
|
||||
channel_id: VANILLA_CHANNEL,
|
||||
user_id: id,
|
||||
}),
|
||||
])
|
||||
}
|
||||
return (
|
||||
<Suspense>
|
||||
<HydrateClient>
|
||||
<UserInfo />
|
||||
</HydrateClient>
|
||||
</Suspense>
|
||||
)
|
||||
}
|
||||
535
src/app/(home)/players/[id]/user.tsx
Normal file
535
src/app/(home)/players/[id]/user.tsx
Normal file
@@ -0,0 +1,535 @@
|
||||
'use client'
|
||||
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from '@/components/ui/tooltip'
|
||||
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'
|
||||
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,
|
||||
Filter,
|
||||
IceCreamCone,
|
||||
InfoIcon,
|
||||
ShieldHalf,
|
||||
Star,
|
||||
Trophy,
|
||||
} from 'lucide-react'
|
||||
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,
|
||||
})
|
||||
|
||||
// Mock data for the two leaderboards - replace with actual API calls
|
||||
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,
|
||||
})
|
||||
console.log({ vanillaUserRank, rankedUserRank })
|
||||
|
||||
// Filter games by leaderboard if needed
|
||||
const filteredGamesByLeaderboard =
|
||||
leaderboardFilter === 'all'
|
||||
? games
|
||||
: games.filter((game) => game.gameType === leaderboardFilter)
|
||||
|
||||
// Filter by result
|
||||
const filteredGames =
|
||||
filter === 'all'
|
||||
? filteredGamesByLeaderboard
|
||||
: filter === 'wins'
|
||||
? filteredGamesByLeaderboard.filter((game) => game.result === 'win')
|
||||
: filter === 'losses'
|
||||
? filteredGamesByLeaderboard.filter((game) => 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') {
|
||||
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)
|
||||
|
||||
return (
|
||||
<div className='min-h-screen bg-gradient-to-b from-gray-50 to-gray-100 dark:from-zinc-900 dark:to-zinc-950'>
|
||||
<div className='container mx-auto'>
|
||||
<Card className='overflow-hidden border-none bg-white py-0 shadow-lg dark:bg-zinc-900'>
|
||||
<CardHeader className='border-gray-200 border-b bg-white p-6 dark:border-zinc-800 dark:bg-zinc-900'>
|
||||
<div className='flex flex-col items-center gap-6 md:flex-row'>
|
||||
<div className='relative'>
|
||||
<Avatar className='h-24 w-24 border-4 border-gray-100 shadow-md dark:border-zinc-800'>
|
||||
<AvatarImage
|
||||
src={profileData.avatar}
|
||||
alt={profileData.username}
|
||||
/>
|
||||
<AvatarFallback className='bg-violet-50 font-bold text-2xl text-violet-600 dark:bg-violet-900/30 dark:text-violet-300'>
|
||||
{profileData.username.slice(0, 2).toUpperCase()}
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
</div>
|
||||
|
||||
<div className='text-center md:text-left'>
|
||||
<div className={'flex items-start gap-2'}>
|
||||
<h1 className='font-bold text-3xl text-gray-900 dark:text-white'>
|
||||
{profileData.username}
|
||||
</h1>
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<InfoIcon className={'size-4'} />
|
||||
</TooltipTrigger>
|
||||
<TooltipContent align={'center'} sideOffset={5}>
|
||||
<div>
|
||||
<p>Also known as:</p>
|
||||
<ul className={'list-disc pl-4'}>
|
||||
{aliases.map((alias) => (
|
||||
<li key={alias}>{alias}</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
</div>
|
||||
|
||||
<p className='text-gray-500 text-sm dark:text-zinc-400'>
|
||||
{firstGame ? (
|
||||
<>First game: {dateFormatter.format(firstGame.gameTime)}</>
|
||||
) : (
|
||||
<>No games played yet</>
|
||||
)}
|
||||
</p>
|
||||
<div className='mt-2 flex flex-wrap items-center justify-center gap-2 md:justify-start'>
|
||||
{!!rankedLeaderboard && (
|
||||
<Badge
|
||||
variant='outline'
|
||||
className='border-gray-200 bg-gray-50 dark:border-zinc-700 dark:bg-zinc-800'
|
||||
>
|
||||
<Trophy className='mr-1 h-3 w-3 text-violet-500' />
|
||||
<span className='text-gray-700 dark:text-zinc-300'>
|
||||
Ranked Queue:{' '}
|
||||
{isNonNullish(rankedUserRank?.rank)
|
||||
? `#${rankedUserRank.rank}`
|
||||
: 'N/A'}
|
||||
</span>
|
||||
</Badge>
|
||||
)}
|
||||
{!!vanillaLeaderboard && (
|
||||
<Badge
|
||||
variant='outline'
|
||||
className='border-gray-200 bg-gray-50 dark:border-zinc-700 dark:bg-zinc-800'
|
||||
>
|
||||
<Trophy className='mr-1 h-3 w-3 text-violet-500' />
|
||||
<span className='text-gray-700 dark:text-zinc-300'>
|
||||
Vanilla Queue:{' '}
|
||||
{isNonNullish(vanillaUserRank?.rank)
|
||||
? `#${vanillaUserRank.rank}`
|
||||
: 'N/A'}
|
||||
</span>
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className={cn(
|
||||
'grid w-full flex-grow grid-cols-2 divide-gray-100 md:w-auto md:grid-cols-3 md:divide-y-0 dark:divide-zinc-800',
|
||||
isNonNullish(rankedUserRank?.mmr) && 'lg:grid-cols-4',
|
||||
isNonNullish(vanillaUserRank?.mmr) && 'lg:grid-cols-4',
|
||||
isNonNullish(rankedUserRank?.mmr) &&
|
||||
isNonNullish(vanillaUserRank?.mmr) &&
|
||||
'lg:grid-cols-5'
|
||||
)}
|
||||
>
|
||||
<StatsCard
|
||||
title='Games'
|
||||
value={profileData.games}
|
||||
icon={<BarChart3 className='h-5 w-5 text-violet-500' />}
|
||||
description='Total matches'
|
||||
/>
|
||||
<StatsCard
|
||||
title='Wins'
|
||||
value={profileData.wins}
|
||||
icon={<ArrowUpCircle className='h-5 w-5 text-emerald-500' />}
|
||||
description={`${profileData.winRate}% win rate`}
|
||||
accentColor='text-emerald-500'
|
||||
/>
|
||||
<StatsCard
|
||||
title='Losses'
|
||||
value={profileData.losses}
|
||||
icon={<ArrowDownCircle className='h-5 w-5 text-rose-500' />}
|
||||
description={`${profileData.lossRate}% loss rate`}
|
||||
accentColor='text-rose-500'
|
||||
/>
|
||||
{isNonNullish(rankedUserRank?.mmr) && (
|
||||
<StatsCard
|
||||
title='Ranked MMR'
|
||||
value={Math.round(rankedUserRank.mmr)}
|
||||
description={
|
||||
lastRankedGame ? (
|
||||
<span
|
||||
className={cn(
|
||||
'flex items-center',
|
||||
lastRankedGame.mmrChange === 0
|
||||
? 'text-zink-800 dark:text-zink-200'
|
||||
: lastRankedGame.mmrChange > 0
|
||||
? 'text-emerald-500'
|
||||
: 'text-rose-500'
|
||||
)}
|
||||
>
|
||||
{lastRankedGame.mmrChange === 0 ? (
|
||||
'Tied'
|
||||
) : lastRankedGame.mmrChange > 0 ? (
|
||||
<ChevronUp className='h-3 w-3' />
|
||||
) : (
|
||||
<ChevronDown className='h-3 w-3' />
|
||||
)}
|
||||
{lastRankedGame.mmrChange !== 0
|
||||
? numberFormatter.format(
|
||||
Math.trunc(lastRankedGame.mmrChange)
|
||||
)
|
||||
: null}{' '}
|
||||
last match
|
||||
</span>
|
||||
) : null
|
||||
}
|
||||
icon={
|
||||
<ShieldHalf className='h-5 w-5 text-zink-800 dark:text-zink-200' />
|
||||
}
|
||||
accentColor='text-zink-800 dark:text-zink-200'
|
||||
/>
|
||||
)}
|
||||
{isNonNullish(vanillaUserRank?.mmr) && (
|
||||
<StatsCard
|
||||
title='Vanilla MMR'
|
||||
value={Math.round(vanillaUserRank.mmr)}
|
||||
icon={
|
||||
<IceCreamCone className='h-5 w-5 text-zink-800 dark:text-zink-200' />
|
||||
}
|
||||
accentColor='text-zink-800 dark:text-zink-200'
|
||||
description={
|
||||
lastVanillaGame ? (
|
||||
<span
|
||||
className={cn(
|
||||
'flex items-center',
|
||||
lastVanillaGame.mmrChange === 0
|
||||
? 'text-zink-800 dark:text-zink-200'
|
||||
: lastVanillaGame.mmrChange > 0
|
||||
? 'text-emerald-500'
|
||||
: 'text-rose-500'
|
||||
)}
|
||||
>
|
||||
{lastVanillaGame.mmrChange === 0 ? (
|
||||
'Tied'
|
||||
) : lastVanillaGame.mmrChange > 0 ? (
|
||||
<ChevronUp className='h-3 w-3' />
|
||||
) : (
|
||||
<ChevronDown className='h-3 w-3' />
|
||||
)}
|
||||
{lastVanillaGame.mmrChange !== 0
|
||||
? numberFormatter.format(
|
||||
Math.trunc(lastVanillaGame.mmrChange)
|
||||
)
|
||||
: null}{' '}
|
||||
last match
|
||||
</span>
|
||||
) : null
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</CardHeader>
|
||||
|
||||
<CardContent className='p-0'>
|
||||
<Tabs defaultValue='matches' className='p-6'>
|
||||
<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'>
|
||||
<TabsTrigger value='matches'>Match History</TabsTrigger>
|
||||
<TabsTrigger value='stats'>Statistics</TabsTrigger>
|
||||
<TabsTrigger value='achievements'>Achievements</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<div className='flex items-center gap-2'>
|
||||
<div className='mr-2 flex items-center gap-2'>
|
||||
<Trophy className='h-4 w-4 text-gray-400 dark:text-zinc-400' />
|
||||
<Select
|
||||
value={leaderboardFilter}
|
||||
onValueChange={setLeaderboardFilter}
|
||||
>
|
||||
<SelectTrigger className='h-9 w-[150px]'>
|
||||
<SelectValue placeholder='Leaderboard' />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value='all'>All Leaderboards</SelectItem>
|
||||
<SelectItem value='ranked'>Ranked</SelectItem>
|
||||
<SelectItem value='vanilla'>Vanilla</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<Filter className='h-4 w-4 text-gray-400 dark:text-zinc-400' />
|
||||
<Select value={filter} onValueChange={setFilter}>
|
||||
<SelectTrigger className='h-9 w-[120px]'>
|
||||
<SelectValue placeholder='Filter' />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value='all'>All Games</SelectItem>
|
||||
<SelectItem value='wins'>Wins</SelectItem>
|
||||
<SelectItem value='losses'>Losses</SelectItem>
|
||||
<SelectItem value='ties'>Ties</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<TabsContent value='matches' className='m-0'>
|
||||
<div className='overflow-hidden rounded-lg border'>
|
||||
<div className='overflow-x-auto'>
|
||||
<GamesTable games={filteredGames} />
|
||||
</div>
|
||||
</div>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value='stats' className='m-0'>
|
||||
<div className='grid grid-cols-1 gap-6 md:grid-cols-2'>
|
||||
{(rankedLeaderboard || lastRankedGame) && (
|
||||
<LeaderboardStatsCard
|
||||
title='Ranked Queue Stats'
|
||||
rank={rankedUserRank?.rank}
|
||||
mmr={
|
||||
lastRankedGame
|
||||
? Math.trunc(
|
||||
lastRankedGame.playerMmr +
|
||||
lastRankedGame.mmrChange
|
||||
)
|
||||
: undefined
|
||||
}
|
||||
icon={<Trophy className='h-5 w-5 text-violet-500' />}
|
||||
accentColor='text-violet-500'
|
||||
/>
|
||||
)}
|
||||
|
||||
{(vanillaLeaderboard || lastVanillaGame) && (
|
||||
<LeaderboardStatsCard
|
||||
title='Vanilla Queue Stats'
|
||||
rank={vanillaUserRank?.rank}
|
||||
mmr={
|
||||
lastVanillaGame
|
||||
? Math.trunc(
|
||||
lastVanillaGame.playerMmr +
|
||||
lastVanillaGame.mmrChange
|
||||
)
|
||||
: undefined
|
||||
}
|
||||
icon={<Star className='h-5 w-5 text-amber-500' />}
|
||||
accentColor='text-amber-500'
|
||||
/>
|
||||
)}
|
||||
|
||||
{!rankedLeaderboard &&
|
||||
!vanillaLeaderboard &&
|
||||
!lastRankedGame &&
|
||||
!lastVanillaGame && (
|
||||
<div className='col-span-2 flex h-40 items-center justify-center rounded-lg border bg-gray-50 dark:bg-zinc-800/50'>
|
||||
<p className='text-gray-500 dark:text-zinc-400'>
|
||||
No leaderboard data available
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value='achievements' className='m-0'>
|
||||
<div className='flex h-40 items-center justify-center rounded-lg border bg-gray-50 dark:bg-zinc-800/50'>
|
||||
<p className='text-gray-500 dark:text-zinc-400'>
|
||||
Achievements coming soon
|
||||
</p>
|
||||
</div>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
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 (
|
||||
<div className='flex w-fit flex-col items-start justify-self-center p-2 text-center md:justify-self-auto'>
|
||||
<h3 className='mb-1 text-nowrap font-medium text-gray-500 text-sm dark:text-zinc-400'>
|
||||
{title}
|
||||
</h3>
|
||||
<div className={'flex items-center gap-2'}>
|
||||
<div className='flex items-center justify-center'>{icon}</div>
|
||||
<p className={cn('font-bold text-3xl', accentColor)}>{value}</p>
|
||||
</div>
|
||||
<p className='mt-1 text-gray-500 text-xs dark:text-zinc-400'>
|
||||
{description}
|
||||
</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
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 (
|
||||
<div className='rounded-lg border bg-white p-6 dark:bg-zinc-800/20'>
|
||||
<div className='mb-4 flex items-center gap-3'>
|
||||
<div
|
||||
className={cn(
|
||||
'rounded-full bg-gray-100 p-2 dark:bg-zinc-800',
|
||||
accentColor
|
||||
)}
|
||||
>
|
||||
{icon}
|
||||
</div>
|
||||
<h3 className='font-semibold text-lg'>{title}</h3>
|
||||
</div>
|
||||
|
||||
<div className='grid grid-cols-2 gap-4'>
|
||||
{rank !== undefined && (
|
||||
<div className='rounded-lg bg-gray-50 p-4 dark:bg-zinc-800/40'>
|
||||
<p className='text-gray-500 text-sm dark:text-zinc-400'>Rank</p>
|
||||
<p className={cn('mt-1 font-bold text-2xl', accentColor)}>
|
||||
#{rank}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{mmr !== undefined && (
|
||||
<div className='rounded-lg bg-gray-50 p-4 dark:bg-zinc-800/40'>
|
||||
<p className='text-gray-500 text-sm dark:text-zinc-400'>MMR</p>
|
||||
<p className={cn('mt-1 font-bold text-2xl', accentColor)}>{mmr}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{rank === undefined && mmr === undefined && (
|
||||
<div className='col-span-2 flex h-20 items-center justify-center rounded-lg bg-gray-50 p-4 dark:bg-zinc-800/40'>
|
||||
<p className='text-gray-500 dark:text-zinc-400'>
|
||||
No data available
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user