diff --git a/src/app/stream-card/[id]/_components/stream-card-client.tsx b/src/app/stream-card/[id]/_components/stream-card-client.tsx
new file mode 100644
index 0000000..43d0ddf
--- /dev/null
+++ b/src/app/stream-card/[id]/_components/stream-card-client.tsx
@@ -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 (
+
+
+ {playerData.rank}
+
+
+ {/* Player Name */}
+
+
{playerData.username}
+
+
+ {/* MMR */}
+
+
MMR:
+
{playerData.mmr}
+
{playerData.mmrChange}
+
+
+ {/* Win Rate */}
+
+
Win Rate:
+
+ {playerData.winRate}%
+
+
+
+ {/* Win/Loss */}
+
+
+
+
+ {playerData.wins}
+
+
+
+
+
+ {playerData.losses}
+
+
+
+
+ {/* Streak */}
+
+
Streak:
+
{playerData.streak}
+
+
+ {/* Live Indicator */}
+
+
+ )
+}
diff --git a/src/app/stream-card/[id]/page.tsx b/src/app/stream-card/[id]/page.tsx
index 5571747..876ea75 100644
--- a/src/app/stream-card/[id]/page.tsx
+++ b/src/app/stream-card/[id]/page.tsx
@@ -1,123 +1,32 @@
-'use client'
-
+import { StreamCardClient } from '@/app/stream-card/[id]/_components/stream-card-client'
import { RANKED_CHANNEL } from '@/shared/constants'
-import { api } from '@/trpc/react'
-import { ArrowDown, ArrowUp } from 'lucide-react'
-import { useParams } from 'next/navigation'
+import { HydrateClient, api } from '@/trpc/server'
+import { Suspense } from 'react'
-export default function LeaderboardCard() {
- const { id } = useParams()
- if (!id || typeof id !== 'string') {
- return null
+export default async function StreamCardPage({
+ params,
+}: {
+ 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 (
-
-
- {playerData.rank}
-
-
- {/* Player Name */}
-
-
{playerData.username}
-
-
- {/* MMR */}
-
-
MMR:
-
{playerData.mmr}
-
{playerData.mmrChange}
-
-
- {/* Win Rate */}
-
-
Win Rate:
-
- {playerData.winRate}%
-
-
-
- {/* Win/Loss */}
-
-
-
-
- {playerData.wins}
-
-
-
-
-
- {playerData.losses}
-
-
-
-
- {/* Streak */}
-
-
Streak:
-
{playerData.streak}
-
-
- {/* Live Indicator */}
-
-
+
+
+
+
+
)
}