add dark mode

This commit is contained in:
2025-04-04 11:38:44 +02:00
parent 31cb676567
commit 9cb72fa134
9 changed files with 407 additions and 118 deletions

View File

@@ -1,11 +1,16 @@
'use client' 'use client'
import type React from 'react' import type React from 'react'
import { type ComponentPropsWithoutRef, Fragment, useRef } from 'react' import {
type ComponentPropsWithoutRef,
Fragment,
useRef,
useState,
} from 'react'
import { Badge } from '@/components/ui/badge' import { Badge } from '@/components/ui/badge'
import { Button } from '@/components/ui/button' import { Button } from '@/components/ui/button'
import { Card, CardContent, CardHeader } from '@/components/ui/card' import { CardContent } from '@/components/ui/card'
import { Input } from '@/components/ui/input' import { Input } from '@/components/ui/input'
import { import {
Table, Table,
@@ -34,7 +39,6 @@ import {
} from 'lucide-react' } from 'lucide-react'
import Link from 'next/link' import Link from 'next/link'
import { useRouter, useSearchParams } from 'next/navigation' import { useRouter, useSearchParams } from 'next/navigation'
import { useState } from 'react'
export function LeaderboardPage() { export function LeaderboardPage() {
const router = useRouter() const router = useRouter()
@@ -118,41 +122,44 @@ export function LeaderboardPage() {
} }
return ( return (
<div className='flex min-h-screen flex-col bg-gradient-to-b from-slate-50 to-slate-100 dark:from-slate-950 dark:to-slate-900'> <div className='flex h-screen flex-col overflow-hidden bg-gradient-to-b from-gray-50 to-gray-100 dark:from-zinc-900 dark:to-zinc-950'>
<div className='container mx-auto flex flex-1 flex-col px-4 py-4'> <div className='container mx-auto flex flex-1 flex-col px-4 py-4'>
<Card className='flex flex-1 flex-col overflow-hidden border-none bg-white p-0 shadow-lg dark:bg-slate-900'> <div className='flex flex-1 flex-col overflow-hidden border-none bg-white p-0 shadow-lg dark:bg-zinc-900'>
<CardHeader className='bg-gradient-to-r from-violet-500 to-purple-600 p-6'> <div 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 justify-between gap-4 md:flex-row'> <div className='flex flex-col items-center justify-between gap-4 md:flex-row'>
<div> <div>
<h1 className='flex items-center gap-2 font-bold text-3xl text-white'> <h1 className='flex items-center gap-2 font-bold text-3xl text-gray-900 dark:text-white'>
<Trophy className='h-7 w-7' /> <Trophy className='h-7 w-7 text-violet-500 dark:text-violet-400' />
Leaderboards Leaderboards
</h1> </h1>
<p className='mt-1 text-violet-200'> <p className='mt-1 text-gray-500 dark:text-zinc-400'>
View player rankings and statistics View player rankings and statistics
</p> </p>
</div> </div>
<div className='flex items-center gap-3'> <div className='flex items-center gap-3'>
<Badge <Badge
variant='secondary' variant='outline'
className='bg-white/20 text-white hover:bg-white/30' className='border-gray-200 bg-gray-50 dark:border-zinc-700 dark:bg-zinc-800'
> >
<Users className='mr-1 h-3 w-3' /> <Users className='mr-1 h-3 w-3 text-gray-500 dark:text-zinc-400' />
<span className='text-gray-700 dark:text-zinc-300'>
{currentLeaderboard.length} Players {currentLeaderboard.length} Players
</span>
</Badge> </Badge>
<Button <Button
variant='secondary' variant='outline'
size='sm' size='sm'
className='bg-white/20 text-white hover:bg-white/30' className='border-gray-200 dark:border-zinc-700'
> >
<Info className='mr-1 h-4 w-4' /> <Info className='mr-1 h-4 w-4 text-gray-500 dark:text-zinc-400' />
<span className='text-gray-700 dark:text-zinc-300'>
How Rankings Work How Rankings Work
</span>
</Button> </Button>
</div> </div>
</div> </div>
</CardHeader> </div>
<CardContent className='flex flex-1 flex-col p-0'> <CardContent className='flex flex-1 flex-col p-0'>
<Tabs <Tabs
@@ -162,16 +169,16 @@ export function LeaderboardPage() {
className='flex flex-1 flex-col p-4 md:p-6' className='flex flex-1 flex-col p-4 md:p-6'
> >
<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-slate-100 dark:bg-slate-800'> <TabsList className='border border-gray-200 border-b bg-gray-50 dark:border-zinc-800 dark:bg-zinc-800/50'>
<TabsTrigger value='ranked'>Ranked Leaderboard</TabsTrigger> <TabsTrigger value='ranked'>Ranked Leaderboard</TabsTrigger>
<TabsTrigger value='vanilla'>Vanilla Leaderboard</TabsTrigger> <TabsTrigger value='vanilla'>Vanilla Leaderboard</TabsTrigger>
</TabsList> </TabsList>
<div className='relative w-full sm:w-auto'> <div className='relative w-full sm:w-auto'>
<Search className='absolute top-2.5 left-2.5 h-4 w-4 text-slate-400' /> <Search className='absolute top-2.5 left-2.5 h-4 w-4 text-gray-400 dark:text-zinc-400' />
<Input <Input
placeholder='Search players...' placeholder='Search players...'
className='w-full pl-9 sm:w-[250px]' className='w-full border-gray-200 bg-white pl-9 sm:w-[250px] dark:border-zinc-700 dark:bg-zinc-900'
value={searchQuery} value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)} onChange={(e) => setSearchQuery(e.target.value)}
/> />
@@ -201,7 +208,7 @@ export function LeaderboardPage() {
</TabsContent> </TabsContent>
</Tabs> </Tabs>
</CardContent> </CardContent>
</Card> </div>
</div> </div>
</div> </div>
) )
@@ -228,7 +235,6 @@ function LeaderboardTable({
// Set a fixed row height for virtualization // Set a fixed row height for virtualization
const ROW_HEIGHT = 39 // Adjust based on your actual row height const ROW_HEIGHT = 39 // Adjust based on your actual row height
console.log(leaderboard.length)
// Create virtualizer instance // Create virtualizer instance
const rowVirtualizer = useVirtualizer({ const rowVirtualizer = useVirtualizer({
count: leaderboard.length, count: leaderboard.length,
@@ -239,8 +245,6 @@ function LeaderboardTable({
// Get the virtualized rows // Get the virtualized rows
const virtualRows = rowVirtualizer.getVirtualItems() const virtualRows = rowVirtualizer.getVirtualItems()
console.log({ virtualRows })
console.log(rowVirtualizer.getTotalSize())
const paddingTop = virtualRows.length > 0 ? (virtualRows?.[0]?.start ?? 0) : 0 const paddingTop = virtualRows.length > 0 ? (virtualRows?.[0]?.start ?? 0) : 0
const paddingBottom = const paddingBottom =
virtualRows.length > 0 virtualRows.length > 0
@@ -255,8 +259,8 @@ function LeaderboardTable({
style={{ maxHeight: 'calc(100vh - 300px)' }} style={{ maxHeight: 'calc(100vh - 300px)' }}
> >
<Table> <Table>
<TableHeader className='sticky top-0 z-10 bg-white dark:bg-slate-900'> <TableHeader className='sticky top-0 z-10 bg-white dark:bg-zinc-900'>
<TableRow className=' bg-slate-50 dark:bg-slate-800/50'> <TableRow className='bg-gray-50 dark:bg-zinc-800/50'>
<TableHead className='w-[80px]'> <TableHead className='w-[80px]'>
<SortableHeader <SortableHeader
className='w-full justify-end' className='w-full justify-end'
@@ -364,7 +368,7 @@ function LeaderboardTable({
<TableRow <TableRow
className={cn( className={cn(
'transition-colors hover:bg-slate-50 dark:hover:bg-slate-800/70' 'transition-colors hover:bg-gray-50 dark:hover:bg-zinc-800/70'
)} )}
> >
<TableCell className='w-24 font-medium'> <TableCell className='w-24 font-medium'>
@@ -393,7 +397,7 @@ function LeaderboardTable({
<TableCell className='text-right font-mono'> <TableCell className='text-right font-mono'>
<div className='flex items-center justify-end gap-1'> <div className='flex items-center justify-end gap-1'>
{Math.round(entry.peak_mmr)} {Math.round(entry.peak_mmr)}
<TrendingUp className='h-3.5 w-3.5 text-violet-500' /> <TrendingUp className='h-3.5 w-3.5 text-violet-400' />
</div> </div>
</TableCell> </TableCell>
<TableCell className='text-right'> <TableCell className='text-right'>
@@ -405,7 +409,7 @@ function LeaderboardTable({
? 'border-emerald-200 bg-emerald-50 text-emerald-700 dark:border-emerald-800 dark:bg-emerald-950 dark:text-emerald-300' ? 'border-emerald-200 bg-emerald-50 text-emerald-700 dark:border-emerald-800 dark:bg-emerald-950 dark:text-emerald-300'
: winrate < 40 : winrate < 40
? 'border-rose-200 bg-rose-50 text-rose-700 dark:border-rose-800 dark:bg-rose-950 dark:text-rose-300' ? 'border-rose-200 bg-rose-50 text-rose-700 dark:border-rose-800 dark:bg-rose-950 dark:text-rose-300'
: 'border-slate-200 bg-slate-50 text-slate-700 dark:border-slate-800 dark:bg-slate-900 dark:text-slate-300' : 'border-gray-200 bg-gray-50 text-gray-700 dark:border-zinc-700 dark:bg-zinc-800 dark:text-zinc-300'
)} )}
> >
{Math.round(winrate)}% {Math.round(winrate)}%
@@ -444,7 +448,9 @@ function LeaderboardTable({
) : ( ) : (
<TableRow> <TableRow>
<TableCell colSpan={9} className='h-24 text-center'> <TableCell colSpan={9} className='h-24 text-center'>
<p className='text-gray-500 dark:text-zinc-400'>
No players found No players found
</p>
</TableCell> </TableCell>
</TableRow> </TableRow>
)} )}
@@ -483,7 +489,7 @@ function SortableHeader({
<button <button
type={'button'} type={'button'}
className={cn( className={cn(
'flex items-center gap-1 transition-colors hover:text-violet-600 dark:hover:text-violet-400', 'flex items-center gap-1 transition-colors hover:text-violet-500 dark:hover:text-violet-400',
className className
)} )}
{...rest} {...rest}

3
src/app/about/page.tsx Normal file
View File

@@ -0,0 +1,3 @@
export default async function About() {
return <div>About</div>
}

View File

@@ -3,6 +3,8 @@ import '@/styles/globals.css'
import type { Metadata } from 'next' import type { Metadata } from 'next'
import { Geist } from 'next/font/google' import { Geist } from 'next/font/google'
import { MainHeader } from '@/components/header'
import { ThemeProvider } from '@/components/theme-provider'
import { TRPCReactProvider } from '@/trpc/react' import { TRPCReactProvider } from '@/trpc/react'
import { NextIntlClientProvider } from 'next-intl' import { NextIntlClientProvider } from 'next-intl'
import { getLocale } from 'next-intl/server' import { getLocale } from 'next-intl/server'
@@ -23,10 +25,24 @@ export default async function RootLayout({
}: Readonly<{ children: React.ReactNode }>) { }: Readonly<{ children: React.ReactNode }>) {
const locale = await getLocale() const locale = await getLocale()
return ( return (
<html lang={locale} className={`${geist.variable}`}> <html
lang={locale}
className={`${geist.variable}`}
suppressHydrationWarning
>
<body> <body>
<TRPCReactProvider> <TRPCReactProvider>
<NextIntlClientProvider>{children}</NextIntlClientProvider> <NextIntlClientProvider>
<ThemeProvider
attribute='class'
defaultTheme='system'
enableSystem
disableTransitionOnChange
>
<MainHeader />
{children}
</ThemeProvider>
</NextIntlClientProvider>
</TRPCReactProvider> </TRPCReactProvider>
</body> </body>
</html> </html>

View 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>
)
}

View File

@@ -1,28 +1,3 @@
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() { export default async function Home() {
const session = await auth() return <div>Hello</div>
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>
)
} }

View File

@@ -135,28 +135,28 @@ export function UserInfo() {
.at(0) .at(0)
return ( return (
<div className='min-h-screen bg-gradient-to-b from-slate-50 to-slate-100 dark:from-slate-950 dark:to-slate-900'> <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 px-4 py-8'> <div className='container mx-auto px-4 py-8'>
<Card className='overflow-hidden border-none bg-white py-0 shadow-lg dark:bg-slate-900'> <Card className='overflow-hidden border-none bg-white py-0 shadow-lg dark:bg-zinc-900'>
<CardHeader className='bg-gradient-to-r from-violet-500 to-purple-600 p-6'> <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='flex flex-col items-center gap-6 md:flex-row'>
<div className='relative'> <div className='relative'>
<Avatar className='h-24 w-24 border-4 border-white shadow-md'> <Avatar className='h-24 w-24 border-4 border-gray-100 shadow-md dark:border-zinc-800'>
<AvatarImage <AvatarImage
src={profileData.avatar} src={profileData.avatar}
alt={profileData.username} alt={profileData.username}
/> />
<AvatarFallback className='bg-violet-200 font-bold text-2xl text-violet-700'> <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()} {profileData.username.slice(0, 2).toUpperCase()}
</AvatarFallback> </AvatarFallback>
</Avatar> </Avatar>
</div> </div>
<div className='text-center md:text-left'> <div className='text-center md:text-left'>
<h1 className='font-bold text-3xl text-white'> <h1 className='font-bold text-3xl text-gray-900 dark:text-white'>
{profileData.username} {profileData.username}
</h1> </h1>
<p className='text-sm text-violet-200'> <p className='text-gray-500 text-sm dark:text-zinc-400'>
{firstGame ? ( {firstGame ? (
<>First game: {dateFormatter.format(firstGame.gameTime)}</> <>First game: {dateFormatter.format(firstGame.gameTime)}</>
) : ( ) : (
@@ -166,26 +166,30 @@ export function UserInfo() {
<div className='mt-2 flex flex-wrap items-center justify-center gap-2 md:justify-start'> <div className='mt-2 flex flex-wrap items-center justify-center gap-2 md:justify-start'>
{!!rankedLeaderboard && ( {!!rankedLeaderboard && (
<Badge <Badge
variant='secondary' variant='outline'
className='bg-white/20 text-white hover:bg-white/30' className='border-gray-200 bg-gray-50 dark:border-zinc-700 dark:bg-zinc-800'
> >
<Trophy className='mr-1 h-3 w-3' /> <Trophy className='mr-1 h-3 w-3 text-violet-500' />
<span className='text-gray-700 dark:text-zinc-300'>
Ranked Queue:{' '} Ranked Queue:{' '}
{isNonNullish(rankedUserRank?.rank) {isNonNullish(rankedUserRank?.rank)
? `#${rankedUserRank.rank}` ? `#${rankedUserRank.rank}`
: 'N/A'} : 'N/A'}
</span>
</Badge> </Badge>
)} )}
{!!vanillaLeaderboard && ( {!!vanillaLeaderboard && (
<Badge <Badge
variant='secondary' variant='outline'
className='bg-white/20 text-white hover:bg-white/30' className='border-gray-200 bg-gray-50 dark:border-zinc-700 dark:bg-zinc-800'
> >
<Star className='mr-1 h-3 w-3' /> <Trophy className='mr-1 h-3 w-3 text-violet-500' />
<span className='text-gray-700 dark:text-zinc-300'>
Vanilla Queue:{' '} Vanilla Queue:{' '}
{isNonNullish(vanillaUserRank?.rank) {isNonNullish(vanillaUserRank?.rank)
? `#${vanillaUserRank.rank}` ? `#${vanillaUserRank.rank}`
: 'N/A'} : 'N/A'}
</span>
</Badge> </Badge>
)} )}
</div> </div>
@@ -194,19 +198,19 @@ export function UserInfo() {
<div className='flex flex-1 justify-end'> <div className='flex flex-1 justify-end'>
<div className='flex gap-3'> <div className='flex gap-3'>
{lastGameLeaderboard1 && ( {lastGameLeaderboard1 && (
<div className='hidden rounded-lg bg-white/10 p-3 text-white backdrop-blur-sm md:block'> <div className='hidden rounded-lg border border-gray-200 bg-gray-50 p-3 md:block dark:border-zinc-700 dark:bg-zinc-800'>
<div className='font-medium text-sm'> <div className='font-medium text-gray-500 text-sm dark:text-zinc-400'>
Ranked Queue MMR Ranked Queue MMR
</div> </div>
<div className='font-bold text-2xl'> <div className='font-bold text-2xl text-gray-900 dark:text-white'>
{Math.trunc( {Math.trunc(
lastGameLeaderboard1.playerMmr + lastGameLeaderboard1.playerMmr +
lastGameLeaderboard1.mmrChange lastGameLeaderboard1.mmrChange
)} )}
</div> </div>
<div className='text-violet-200 text-xs'> <div className='text-gray-500 text-xs dark:text-zinc-400'>
{lastGameLeaderboard1.mmrChange > 0 ? ( {lastGameLeaderboard1.mmrChange > 0 ? (
<span className='flex items-center text-green-300'> <span className='flex items-center text-emerald-500'>
<ChevronUp className='h-3 w-3' /> <ChevronUp className='h-3 w-3' />
{numberFormatter.format( {numberFormatter.format(
Math.trunc(lastGameLeaderboard1.mmrChange) Math.trunc(lastGameLeaderboard1.mmrChange)
@@ -214,7 +218,7 @@ export function UserInfo() {
last match last match
</span> </span>
) : ( ) : (
<span className='flex items-center text-red-300'> <span className='flex items-center text-rose-500'>
<ChevronDown className='h-3 w-3' /> <ChevronDown className='h-3 w-3' />
{numberFormatter.format( {numberFormatter.format(
Math.trunc(lastGameLeaderboard1.mmrChange) Math.trunc(lastGameLeaderboard1.mmrChange)
@@ -227,19 +231,19 @@ export function UserInfo() {
)} )}
{lastGameLeaderboard2 && ( {lastGameLeaderboard2 && (
<div className='hidden rounded-lg bg-white/10 p-3 text-white backdrop-blur-sm md:block'> <div className='hidden rounded-lg border border-gray-200 bg-gray-50 p-3 md:block dark:border-zinc-700 dark:bg-zinc-800'>
<div className='font-medium text-sm'> <div className='font-medium text-gray-500 text-sm dark:text-zinc-400'>
Leaderboard 2 MMR Vanilla Queue MMR
</div> </div>
<div className='font-bold text-2xl'> <div className='font-bold text-2xl text-gray-900 dark:text-white'>
{Math.trunc( {Math.trunc(
lastGameLeaderboard2.playerMmr + lastGameLeaderboard2.playerMmr +
lastGameLeaderboard2.mmrChange lastGameLeaderboard2.mmrChange
)} )}
</div> </div>
<div className='text-violet-200 text-xs'> <div className='text-gray-500 text-xs dark:text-zinc-400'>
{lastGameLeaderboard2.mmrChange > 0 ? ( {lastGameLeaderboard2.mmrChange > 0 ? (
<span className='flex items-center text-green-300'> <span className='flex items-center text-emerald-500'>
<ChevronUp className='h-3 w-3' /> <ChevronUp className='h-3 w-3' />
{numberFormatter.format( {numberFormatter.format(
Math.trunc(lastGameLeaderboard2.mmrChange) Math.trunc(lastGameLeaderboard2.mmrChange)
@@ -247,7 +251,7 @@ export function UserInfo() {
last match last match
</span> </span>
) : ( ) : (
<span className='flex items-center text-red-300'> <span className='flex items-center text-rose-500'>
<ChevronDown className='h-3 w-3' /> <ChevronDown className='h-3 w-3' />
{numberFormatter.format( {numberFormatter.format(
Math.trunc(lastGameLeaderboard2.mmrChange) Math.trunc(lastGameLeaderboard2.mmrChange)
@@ -264,7 +268,7 @@ export function UserInfo() {
</CardHeader> </CardHeader>
<CardContent className='p-0'> <CardContent className='p-0'>
<div className='grid grid-cols-2 divide-x divide-y divide-gray-100 md:grid-cols-4 md:divide-y-0 dark:divide-gray-800'> <div className='grid grid-cols-2 divide-x divide-y divide-gray-100 md:grid-cols-4 md:divide-y-0 dark:divide-zinc-800'>
<StatsCard <StatsCard
title='Games' title='Games'
value={profileData.games} value={profileData.games}
@@ -296,7 +300,7 @@ export function UserInfo() {
<Tabs defaultValue='matches' className='p-6'> <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'> <div className='mb-6 flex flex-col items-start justify-between gap-4 sm:flex-row sm:items-center'>
<TabsList className='bg-slate-100 dark:bg-slate-800'> <TabsList className='bg-gray-100 dark:bg-zinc-800'>
<TabsTrigger value='matches'>Match History</TabsTrigger> <TabsTrigger value='matches'>Match History</TabsTrigger>
<TabsTrigger value='stats'>Statistics</TabsTrigger> <TabsTrigger value='stats'>Statistics</TabsTrigger>
<TabsTrigger value='achievements'>Achievements</TabsTrigger> <TabsTrigger value='achievements'>Achievements</TabsTrigger>
@@ -304,7 +308,7 @@ export function UserInfo() {
<div className='flex items-center gap-2'> <div className='flex items-center gap-2'>
<div className='mr-2 flex items-center gap-2'> <div className='mr-2 flex items-center gap-2'>
<Trophy className='h-4 w-4 text-slate-400' /> <Trophy className='h-4 w-4 text-gray-400 dark:text-zinc-400' />
<Select <Select
value={leaderboardFilter} value={leaderboardFilter}
onValueChange={setLeaderboardFilter} onValueChange={setLeaderboardFilter}
@@ -320,7 +324,7 @@ export function UserInfo() {
</Select> </Select>
</div> </div>
<Filter className='h-4 w-4 text-slate-400' /> <Filter className='h-4 w-4 text-gray-400 dark:text-zinc-400' />
<Select value={filter} onValueChange={setFilter}> <Select value={filter} onValueChange={setFilter}>
<SelectTrigger className='h-9 w-[120px]'> <SelectTrigger className='h-9 w-[120px]'>
<SelectValue placeholder='Filter' /> <SelectValue placeholder='Filter' />
@@ -340,7 +344,7 @@ export function UserInfo() {
<div className='overflow-x-auto'> <div className='overflow-x-auto'>
<Table> <Table>
<TableHeader> <TableHeader>
<TableRow className='bg-slate-50 dark:bg-slate-800/50'> <TableRow className='bg-gray-50 dark:bg-zinc-800/50'>
<TableHead className='w-[100px]'>Game Type</TableHead> <TableHead className='w-[100px]'>Game Type</TableHead>
<TableHead>Opponent</TableHead> <TableHead>Opponent</TableHead>
<TableHead className='text-right'> <TableHead className='text-right'>
@@ -367,7 +371,7 @@ export function UserInfo() {
{filteredGames.map((game) => ( {filteredGames.map((game) => (
<TableRow <TableRow
key={game.gameId} key={game.gameId}
className='transition-colors hover:bg-slate-50 dark:hover:bg-slate-800/70' className='transition-colors hover:bg-gray-50 dark:hover:bg-zinc-800/70'
> >
<TableCell> <TableCell>
<Badge <Badge
@@ -389,17 +393,17 @@ export function UserInfo() {
<TableCell className='text-right font-mono'> <TableCell className='text-right font-mono'>
{game.mmrChange > 0 ? ( {game.mmrChange > 0 ? (
<span className='flex items-center justify-end font-medium text-emerald-500'> <span className='flex items-center justify-end font-medium text-emerald-500'>
<ArrowUpCircle className='mr-1 inline h-4 w-4' />
{numberFormatter.format( {numberFormatter.format(
Math.trunc(game.mmrChange) Math.trunc(game.mmrChange)
)} )}
<ArrowUpCircle className='ml-1 inline h-4 w-4' />
</span> </span>
) : ( ) : (
<span className='flex items-center justify-end font-medium text-rose-500'> <span className='flex items-center justify-end font-medium text-rose-500'>
<ArrowDownCircle className='mr-1 inline h-4 w-4' />
{numberFormatter.format( {numberFormatter.format(
Math.trunc(game.mmrChange) Math.trunc(game.mmrChange)
)} )}
<ArrowDownCircle className='ml-1 inline h-4 w-4' />
</span> </span>
)} )}
</TableCell> </TableCell>
@@ -412,7 +416,7 @@ export function UserInfo() {
? 'border-violet-200 bg-violet-50 text-violet-700 dark:border-violet-800 dark:bg-violet-950 dark:text-violet-300' ? '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' : 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-amber-200 bg-amber-50 text-amber-700 dark:border-amber-800 dark:bg-amber-950 dark:text-amber-300'
: 'border-slate-200 bg-slate-50 text-slate-700 dark:border-slate-800 dark:bg-slate-950 dark:text-slate-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' {game.gameType === 'ranked'
@@ -483,8 +487,8 @@ export function UserInfo() {
!vanillaLeaderboard && !vanillaLeaderboard &&
!lastGameLeaderboard1 && !lastGameLeaderboard1 &&
!lastGameLeaderboard2 && ( !lastGameLeaderboard2 && (
<div className='col-span-2 flex h-40 items-center justify-center rounded-lg border bg-slate-50 dark:bg-slate-800/50'> <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-slate-500 dark:text-slate-400'> <p className='text-gray-500 dark:text-zinc-400'>
No leaderboard data available No leaderboard data available
</p> </p>
</div> </div>
@@ -493,8 +497,8 @@ export function UserInfo() {
</TabsContent> </TabsContent>
<TabsContent value='achievements' className='m-0'> <TabsContent value='achievements' className='m-0'>
<div className='flex h-40 items-center justify-center rounded-lg border bg-slate-50 dark:bg-slate-800/50'> <div className='flex h-40 items-center justify-center rounded-lg border bg-gray-50 dark:bg-zinc-800/50'>
<p className='text-slate-500 dark:text-slate-400'> <p className='text-gray-500 dark:text-zinc-400'>
Achievements coming soon Achievements coming soon
</p> </p>
</div> </div>
@@ -525,11 +529,11 @@ function StatsCard({
return ( return (
<div className='flex flex-col items-center p-6 text-center'> <div className='flex flex-col items-center p-6 text-center'>
<div className='mb-2 flex items-center justify-center'>{icon}</div> <div className='mb-2 flex items-center justify-center'>{icon}</div>
<h3 className='mb-1 font-medium text-slate-500 text-sm dark:text-slate-400'> <h3 className='mb-1 font-medium text-gray-500 text-sm dark:text-zinc-400'>
{title} {title}
</h3> </h3>
<p className={cn('font-bold text-3xl', accentColor)}>{value}</p> <p className={cn('font-bold text-3xl', accentColor)}>{value}</p>
<p className='mt-1 text-slate-500 text-xs dark:text-slate-400'> <p className='mt-1 text-gray-500 text-xs dark:text-zinc-400'>
{description} {description}
</p> </p>
</div> </div>
@@ -552,11 +556,11 @@ function LeaderboardStatsCard({
accentColor = 'text-violet-500', accentColor = 'text-violet-500',
}: LeaderboardStatsCardProps) { }: LeaderboardStatsCardProps) {
return ( return (
<div className='rounded-lg border bg-white p-6 dark:bg-slate-800/20'> <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='mb-4 flex items-center gap-3'>
<div <div
className={cn( className={cn(
'rounded-full bg-slate-100 p-2 dark:bg-slate-800', 'rounded-full bg-gray-100 p-2 dark:bg-zinc-800',
accentColor accentColor
)} )}
> >
@@ -567,8 +571,8 @@ function LeaderboardStatsCard({
<div className='grid grid-cols-2 gap-4'> <div className='grid grid-cols-2 gap-4'>
{rank !== undefined && ( {rank !== undefined && (
<div className='rounded-lg bg-slate-50 p-4 dark:bg-slate-800/40'> <div className='rounded-lg bg-gray-50 p-4 dark:bg-zinc-800/40'>
<p className='text-slate-500 text-sm dark:text-slate-400'>Rank</p> <p className='text-gray-500 text-sm dark:text-zinc-400'>Rank</p>
<p className={cn('mt-1 font-bold text-2xl', accentColor)}> <p className={cn('mt-1 font-bold text-2xl', accentColor)}>
#{rank} #{rank}
</p> </p>
@@ -576,15 +580,15 @@ function LeaderboardStatsCard({
)} )}
{mmr !== undefined && ( {mmr !== undefined && (
<div className='rounded-lg bg-slate-50 p-4 dark:bg-slate-800/40'> <div className='rounded-lg bg-gray-50 p-4 dark:bg-zinc-800/40'>
<p className='text-slate-500 text-sm dark:text-slate-400'>MMR</p> <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> <p className={cn('mt-1 font-bold text-2xl', accentColor)}>{mmr}</p>
</div> </div>
)} )}
{rank === undefined && mmr === undefined && ( {rank === undefined && mmr === undefined && (
<div className='col-span-2 flex h-20 items-center justify-center rounded-lg bg-slate-50 p-4 dark:bg-slate-800/40'> <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-slate-500 dark:text-slate-400'> <p className='text-gray-500 dark:text-zinc-400'>
No data available No data available
</p> </p>
</div> </div>

206
src/components/header.tsx Normal file
View File

@@ -0,0 +1,206 @@
'use client'
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'
import { Button } from '@/components/ui/button'
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu'
import { LogIn, LogOut, Menu, Moon, Settings, Sun, User, X } from 'lucide-react'
import { useTheme } from 'next-themes'
import Link from 'next/link'
import { useState } from 'react'
export function MainHeader() {
const { setTheme, theme } = useTheme()
const [mobileMenuOpen, setMobileMenuOpen] = useState(false)
// Mock authentication state - replace with your actual auth logic
const [isAuthenticated, setIsAuthenticated] = useState(false)
// Mock user data - replace with your actual user data
const userData = {
name: 'Player123',
avatar: '/placeholder.svg?height=40&width=40',
}
// Toggle authentication for demo purposes
const toggleAuth = () => {
setIsAuthenticated(!isAuthenticated)
}
return (
<header className='sticky top-0 z-40 border-gray-200 border-b bg-white dark:border-zinc-800 dark:bg-zinc-900'>
<div className='container mx-auto px-4'>
<div className='flex h-16 items-center justify-between'>
{/* Logo and Brand */}
<div className='flex items-center'>
<Link href='/' className='flex items-center gap-2'>
<span className='hidden font-bold text-xl sm:inline-block'>
Balatro Multiplayer
</span>
</Link>
</div>
{/* Desktop Navigation */}
<nav className='hidden items-center space-x-6 md:flex'>
<Link
href='/'
className='font-medium text-gray-700 text-sm transition-colors hover:text-violet-500 dark:text-zinc-300 dark:hover:text-violet-400'
>
Home
</Link>
<Link
href='/leaderboards'
className='font-medium text-gray-700 text-sm transition-colors hover:text-violet-500 dark:text-zinc-300 dark:hover:text-violet-400'
>
Leaderboards
</Link>
{/*<Link*/}
{/* href='/matches'*/}
{/* className='font-medium text-gray-700 text-sm transition-colors hover:text-violet-500 dark:text-zinc-300 dark:hover:text-violet-400'*/}
{/*>*/}
{/* Matches*/}
{/*</Link>*/}
<Link
href='/about'
className='font-medium text-gray-700 text-sm transition-colors hover:text-violet-500 dark:text-zinc-300 dark:hover:text-violet-400'
>
About
</Link>
</nav>
{/* Actions: Theme Toggle, Sign In/User Menu */}
<div className='flex items-center gap-2'>
{/* Theme Toggle */}
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant='ghost' size='icon' className='h-9 w-9'>
<Sun className='dark:-rotate-90 h-[1.2rem] w-[1.2rem] rotate-0 scale-100 transition-all dark:scale-0' />
<Moon className='absolute h-[1.2rem] w-[1.2rem] rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100' />
<span className='sr-only'>Toggle theme</span>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align='end'>
<DropdownMenuItem onClick={() => setTheme('light')}>
Light
</DropdownMenuItem>
<DropdownMenuItem onClick={() => setTheme('dark')}>
Dark
</DropdownMenuItem>
<DropdownMenuItem onClick={() => setTheme('system')}>
System
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
{/* Sign In Button or User Menu */}
{isAuthenticated ? (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
variant='ghost'
className='relative h-9 w-9 rounded-full'
>
<Avatar className='h-9 w-9'>
<AvatarImage src={userData.avatar} alt={userData.name} />
<AvatarFallback className='bg-violet-50 text-violet-600 dark:bg-violet-900/50 dark:text-violet-300'>
{userData.name.slice(0, 2).toUpperCase()}
</AvatarFallback>
</Avatar>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent className='w-56' align='end' forceMount>
<div className='flex items-center justify-start gap-2 p-2'>
<div className='flex flex-col space-y-1 leading-none'>
<p className='font-medium'>{userData.name}</p>
</div>
</div>
<DropdownMenuItem asChild>
<Link href='/profile' className='flex w-full items-center'>
<User className='mr-2 h-4 w-4' />
<span>Profile</span>
</Link>
</DropdownMenuItem>
<DropdownMenuItem asChild>
<Link href='/settings' className='flex w-full items-center'>
<Settings className='mr-2 h-4 w-4' />
<span>Settings</span>
</Link>
</DropdownMenuItem>
<DropdownMenuItem onClick={toggleAuth}>
<LogOut className='mr-2 h-4 w-4' />
<span>Log out</span>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
) : (
<Button
variant='default'
size='sm'
className='bg-violet-600 hover:bg-violet-700 dark:text-zinc-100'
onClick={toggleAuth}
>
<LogIn className='mr-2 h-4 w-4' />
Sign In
</Button>
)}
{/* Mobile Menu Toggle */}
<Button
variant='ghost'
size='icon'
className='h-9 w-9 md:hidden'
onClick={() => setMobileMenuOpen(!mobileMenuOpen)}
>
{mobileMenuOpen ? (
<X className='h-5 w-5' />
) : (
<Menu className='h-5 w-5' />
)}
<span className='sr-only'>Toggle menu</span>
</Button>
</div>
</div>
</div>
{/* Mobile Navigation */}
{mobileMenuOpen && (
<div className='border-gray-200 border-t px-4 py-4 md:hidden dark:border-zinc-800'>
<nav className='flex flex-col space-y-4'>
<Link
href='/'
className='font-medium text-gray-700 text-sm transition-colors hover:text-violet-500 dark:text-zinc-300 dark:hover:text-violet-400'
onClick={() => setMobileMenuOpen(false)}
>
Home
</Link>
<Link
href='/leaderboards'
className='font-medium text-gray-700 text-sm transition-colors hover:text-violet-500 dark:text-zinc-300 dark:hover:text-violet-400'
onClick={() => setMobileMenuOpen(false)}
>
Leaderboards
</Link>
{/*<Link*/}
{/* href='/matches'*/}
{/* className='font-medium text-gray-700 text-sm transition-colors hover:text-violet-500 dark:text-zinc-300 dark:hover:text-violet-400'*/}
{/* onClick={() => setMobileMenuOpen(false)}*/}
{/*>*/}
{/* Matches*/}
{/*</Link>*/}
<Link
href='/about'
className='font-medium text-gray-700 text-sm transition-colors hover:text-violet-500 dark:text-zinc-300 dark:hover:text-violet-400'
onClick={() => setMobileMenuOpen(false)}
>
About
</Link>
</nav>
</div>
)}
</header>
)
}

View File

@@ -0,0 +1,40 @@
'use client'
import { Moon, Sun } from 'lucide-react'
import { useTheme } from 'next-themes'
import * as React from 'react'
import { Button } from '@/components/ui/button'
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu'
export function ModeToggle() {
const { setTheme } = useTheme()
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant='outline' size='icon'>
<Sun className='dark:-rotate-90 h-[1.2rem] w-[1.2rem] rotate-0 scale-100 transition-all dark:scale-0' />
<Moon className='absolute h-[1.2rem] w-[1.2rem] rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100' />
<span className='sr-only'>Toggle theme</span>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align='end'>
<DropdownMenuItem onClick={() => setTheme('light')}>
Light
</DropdownMenuItem>
<DropdownMenuItem onClick={() => setTheme('dark')}>
Dark
</DropdownMenuItem>
<DropdownMenuItem onClick={() => setTheme('system')}>
System
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
)
}

View File

@@ -0,0 +1,11 @@
'use client'
import { ThemeProvider as NextThemesProvider } from 'next-themes'
import type * as React from 'react'
export function ThemeProvider({
children,
...props
}: React.ComponentProps<typeof NextThemesProvider>) {
return <NextThemesProvider {...props}>{children}</NextThemesProvider>
}