mirror of
https://github.com/ershisan99/www.git
synced 2025-12-17 12:34:17 +00:00
add dark mode
This commit is contained in:
@@ -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
3
src/app/about/page.tsx
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
export default async function About() {
|
||||||
|
return <div>About</div>
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
|
|||||||
28
src/app/leaderboards/page.tsx
Normal file
28
src/app/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>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -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>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
206
src/components/header.tsx
Normal 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>
|
||||||
|
)
|
||||||
|
}
|
||||||
40
src/components/mode-toggle.tsx
Normal file
40
src/components/mode-toggle.tsx
Normal 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>
|
||||||
|
)
|
||||||
|
}
|
||||||
11
src/components/theme-provider.tsx
Normal file
11
src/components/theme-provider.tsx
Normal 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>
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user