mirror of
https://github.com/ershisan99/www.git
synced 2025-12-18 12:34:17 +00:00
add stream card
This commit is contained in:
137
src/app/stream-card/[id]/_components/stream-card-client.tsx
Normal file
137
src/app/stream-card/[id]/_components/stream-card-client.tsx
Normal file
@@ -0,0 +1,137 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
import { RANKED_CHANNEL } from '@/shared/constants'
|
||||||
|
import { api } from '@/trpc/react'
|
||||||
|
import { ArrowDown, ArrowUp } from 'lucide-react'
|
||||||
|
import { useParams } from 'next/navigation'
|
||||||
|
import { useEffect } from 'react'
|
||||||
|
|
||||||
|
export function StreamCardClient() {
|
||||||
|
const { id } = useParams()
|
||||||
|
if (!id || typeof id !== 'string') {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
const [gamesQueryResult, gamesQuery] =
|
||||||
|
api.history.user_games.useSuspenseQuery({ user_id: id })
|
||||||
|
const games = gamesQueryResult || [] // Ensure games is always an array
|
||||||
|
|
||||||
|
const [rankedUserRank, rankedUserQuery] =
|
||||||
|
api.leaderboard.get_user_rank.useSuspenseQuery({
|
||||||
|
channel_id: RANKED_CHANNEL,
|
||||||
|
user_id: id,
|
||||||
|
})
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setInterval(
|
||||||
|
() => {
|
||||||
|
gamesQuery.refetch()
|
||||||
|
rankedUserQuery.refetch()
|
||||||
|
},
|
||||||
|
// 3 minutes
|
||||||
|
1000 * 60 * 3
|
||||||
|
)
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
if (!rankedUserRank || !games?.length) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
const filteredGamesByLeaderboard = games.filter(
|
||||||
|
(game) => game.gameType === 'ranked'
|
||||||
|
)
|
||||||
|
|
||||||
|
const games_played = filteredGamesByLeaderboard.length
|
||||||
|
let wins = 0
|
||||||
|
let losses = 0
|
||||||
|
let ties = 0
|
||||||
|
for (const game of filteredGamesByLeaderboard) {
|
||||||
|
if (game.result === 'win') {
|
||||||
|
wins++
|
||||||
|
} else if (game.result === 'loss') {
|
||||||
|
losses++
|
||||||
|
} else if (game.result === 'tie') {
|
||||||
|
ties++
|
||||||
|
} else {
|
||||||
|
ties++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const lastGame = filteredGamesByLeaderboard.at(0)
|
||||||
|
|
||||||
|
const currentName = lastGame?.playerName
|
||||||
|
const meaningful_games = games_played - ties
|
||||||
|
const playerData = {
|
||||||
|
username: currentName,
|
||||||
|
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,
|
||||||
|
rank: rankedUserRank.rank,
|
||||||
|
mmr: Math.round(rankedUserRank.mmr),
|
||||||
|
mmrChange: `${(lastGame?.mmrChange ?? 0) >= 0 ? '+' : '-'}${Math.round(lastGame?.mmrChange ?? 0)}`,
|
||||||
|
streak: rankedUserRank?.streak,
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
style={{ zoom: '200%' }}
|
||||||
|
className='flex h-10 w-full max-w-[700px] items-center overflow-hidden rounded-md border-2 border-slate-800 bg-slate-900/90 text-white shadow-lg backdrop-blur-sm'
|
||||||
|
>
|
||||||
|
<div className='flex h-full items-center gap-1 border-slate-700 border-r bg-gradient-to-r from-indigo-600 to-purple-600 px-2'>
|
||||||
|
<span className='font-bold text-sm'>{playerData.rank}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Player Name */}
|
||||||
|
<div className='max-w-[180px] flex-shrink-0 border-slate-700 border-r px-2'>
|
||||||
|
<div className='truncate font-medium'>{playerData.username}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* MMR */}
|
||||||
|
<div className='flex items-center gap-1.5 border-slate-700 border-r px-2'>
|
||||||
|
<div>MMR:</div>
|
||||||
|
<div className='font-bold'>{playerData.mmr}</div>
|
||||||
|
<div className='text-emerald-400 text-sm'>{playerData.mmrChange}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Win Rate */}
|
||||||
|
<div className='flex items-center gap-1.5 border-slate-700 border-r px-2'>
|
||||||
|
<div>Win Rate:</div>
|
||||||
|
<div className='text font-bold text-emerald-400'>
|
||||||
|
{playerData.winRate}%
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Win/Loss */}
|
||||||
|
<div className='flex items-center gap-2 border-slate-700 border-r px-2'>
|
||||||
|
<div className='flex items-center'>
|
||||||
|
<ArrowUp className='h-3 w-3 text-emerald-400' />
|
||||||
|
<div className='ml-0.5 font-bold text-emerald-400'>
|
||||||
|
{playerData.wins}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className='flex items-center'>
|
||||||
|
<ArrowDown className='h-3 w-3 text-rose-400' />
|
||||||
|
<div className='ml-0.5 font-bold text-rose-400'>
|
||||||
|
{playerData.losses}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Streak */}
|
||||||
|
<div className='flex items-center gap-1.5 border-slate-700 border-r px-2'>
|
||||||
|
<div>Streak:</div>
|
||||||
|
<div className='font-bold text-emerald-400'>{playerData.streak}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Live Indicator */}
|
||||||
|
<div className='ml-auto flex items-center gap-1 px-2'>
|
||||||
|
<div className='h-1.5 w-1.5 animate-pulse rounded-full bg-emerald-500' />
|
||||||
|
<span className='text-slate-400 text-sm'>Live</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,123 +1,32 @@
|
|||||||
'use client'
|
import { StreamCardClient } from '@/app/stream-card/[id]/_components/stream-card-client'
|
||||||
|
|
||||||
import { RANKED_CHANNEL } from '@/shared/constants'
|
import { RANKED_CHANNEL } from '@/shared/constants'
|
||||||
import { api } from '@/trpc/react'
|
import { HydrateClient, api } from '@/trpc/server'
|
||||||
import { ArrowDown, ArrowUp } from 'lucide-react'
|
import { Suspense } from 'react'
|
||||||
import { useParams } from 'next/navigation'
|
|
||||||
|
|
||||||
export default function LeaderboardCard() {
|
export default async function StreamCardPage({
|
||||||
const { id } = useParams()
|
params,
|
||||||
if (!id || typeof id !== 'string') {
|
}: {
|
||||||
return null
|
params: Promise<{ id: string }>
|
||||||
|
}) {
|
||||||
|
const { id } = await params
|
||||||
|
if (id) {
|
||||||
|
await Promise.all([
|
||||||
|
api.history.user_games.prefetch({
|
||||||
|
user_id: id,
|
||||||
|
}),
|
||||||
|
|
||||||
|
api.leaderboard.get_user_rank.prefetch({
|
||||||
|
channel_id: RANKED_CHANNEL,
|
||||||
|
user_id: id,
|
||||||
|
}),
|
||||||
|
])
|
||||||
}
|
}
|
||||||
|
|
||||||
const gamesQuery = api.history.user_games.useQuery({ user_id: id })
|
|
||||||
const games = gamesQuery?.data || [] // Ensure games is always an array
|
|
||||||
|
|
||||||
const { data: rankedUserRank } = api.leaderboard.get_user_rank.useQuery({
|
|
||||||
channel_id: RANKED_CHANNEL,
|
|
||||||
user_id: id,
|
|
||||||
})
|
|
||||||
|
|
||||||
if (!rankedUserRank || !games?.length) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
const filteredGamesByLeaderboard = games.filter(
|
|
||||||
(game) => game.gameType === 'ranked'
|
|
||||||
)
|
|
||||||
|
|
||||||
const games_played = filteredGamesByLeaderboard.length
|
|
||||||
let wins = 0
|
|
||||||
let losses = 0
|
|
||||||
let ties = 0
|
|
||||||
for (const game of filteredGamesByLeaderboard) {
|
|
||||||
if (game.result === 'win') {
|
|
||||||
wins++
|
|
||||||
} else if (game.result === 'loss') {
|
|
||||||
losses++
|
|
||||||
} else if (game.result === 'tie') {
|
|
||||||
ties++
|
|
||||||
} else {
|
|
||||||
ties++
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const lastGame = filteredGamesByLeaderboard.at(0)
|
|
||||||
|
|
||||||
const currentName = lastGame?.playerName
|
|
||||||
const meaningful_games = games_played - ties
|
|
||||||
const playerData = {
|
|
||||||
username: currentName,
|
|
||||||
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,
|
|
||||||
rank: rankedUserRank.rank,
|
|
||||||
mmr: Math.round(rankedUserRank.mmr),
|
|
||||||
mmrChange: `${(lastGame?.mmrChange ?? 0) >= 0 ? '+' : '-'}${Math.round(lastGame?.mmrChange ?? 0)}`,
|
|
||||||
streak: rankedUserRank?.streak,
|
|
||||||
}
|
|
||||||
return (
|
return (
|
||||||
<div
|
<Suspense>
|
||||||
style={{ zoom: '200%' }}
|
<HydrateClient>
|
||||||
className='flex h-10 w-full max-w-[700px] items-center overflow-hidden rounded-md border-2 border-slate-800 bg-slate-900/90 text-white shadow-lg backdrop-blur-sm'
|
<StreamCardClient />
|
||||||
>
|
</HydrateClient>
|
||||||
<div className='flex h-full items-center gap-1 border-slate-700 border-r bg-gradient-to-r from-indigo-600 to-purple-600 px-2'>
|
</Suspense>
|
||||||
<span className='font-bold text-sm'>{playerData.rank}</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Player Name */}
|
|
||||||
<div className='max-w-[180px] flex-shrink-0 border-slate-700 border-r px-2'>
|
|
||||||
<div className='truncate font-medium'>{playerData.username}</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* MMR */}
|
|
||||||
<div className='flex items-center gap-1.5 border-slate-700 border-r px-2'>
|
|
||||||
<div>MMR:</div>
|
|
||||||
<div className='font-bold'>{playerData.mmr}</div>
|
|
||||||
<div className='text-emerald-400 text-sm'>{playerData.mmrChange}</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Win Rate */}
|
|
||||||
<div className='flex items-center gap-1.5 border-slate-700 border-r px-2'>
|
|
||||||
<div>Win Rate:</div>
|
|
||||||
<div className='text font-bold text-emerald-400'>
|
|
||||||
{playerData.winRate}%
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Win/Loss */}
|
|
||||||
<div className='flex items-center gap-2 border-slate-700 border-r px-2'>
|
|
||||||
<div className='flex items-center'>
|
|
||||||
<ArrowUp className='h-3 w-3 text-emerald-400' />
|
|
||||||
<div className='ml-0.5 font-bold text-emerald-400'>
|
|
||||||
{playerData.wins}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className='flex items-center'>
|
|
||||||
<ArrowDown className='h-3 w-3 text-rose-400' />
|
|
||||||
<div className='ml-0.5 font-bold text-rose-400'>
|
|
||||||
{playerData.losses}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Streak */}
|
|
||||||
<div className='flex items-center gap-1.5 border-slate-700 border-r px-2'>
|
|
||||||
<div>Streak:</div>
|
|
||||||
<div className='font-bold text-emerald-400'>{playerData.streak}</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Live Indicator */}
|
|
||||||
<div className='ml-auto flex items-center gap-1 px-2'>
|
|
||||||
<div className='h-1.5 w-1.5 animate-pulse rounded-full bg-emerald-500' />
|
|
||||||
<span className='text-slate-400 text-sm'>Live</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user