feat(game): enhance leaderboard structure and animation in Leaderboard component

This commit is contained in:
Ralex
2025-10-26 17:41:04 +01:00
parent 2b499190a2
commit 349c9337e6
5 changed files with 125 additions and 21 deletions

View File

@@ -47,7 +47,7 @@ type ManagerExtraStatus = {
answers: string[]
image?: string
}
SHOW_LEADERBOARD: { leaderboard: Player[] }
SHOW_LEADERBOARD: { oldLeaderboard: Player[]; leaderboard: Player[] }
}
export type PlayerStatusDataMap = CommonStatusDataMap

View File

@@ -25,6 +25,7 @@ class Game {
managerStatus: { name: Status; data: StatusDataMap[Status] } | null = null
playerStatus: Map<string, { name: Status; data: StatusDataMap[Status] }> =
new Map()
leaderboard: Player[]
quizz: Quizz
players: Player[]
@@ -58,6 +59,7 @@ class Game {
this.lastBroadcastStatus = null
this.managerStatus = null
this.playerStatus = new Map()
this.leaderboard = []
this.players = []
@@ -490,20 +492,24 @@ class Game {
this.round.currentQuestion + 1 === this.quizz.questions.length
const sortedPlayers = this.players.sort((a, b) => b.points - a.points)
const oldLeaderboard =
this.leaderboard.length === 0 ? sortedPlayers : this.leaderboard
this.leaderboard = this.players.sort((a, b) => b.points - a.points)
if (isLastRound) {
this.started = false
this.broadcastStatus(STATUS.FINISHED, {
subject: this.quizz.subject,
top: sortedPlayers.slice(0, 3),
top: this.leaderboard.slice(0, 3),
})
return
}
this.sendStatus(this.manager.id, STATUS.SHOW_LEADERBOARD, {
leaderboard: sortedPlayers.slice(0, 5),
oldLeaderboard: oldLeaderboard.slice(0, 5),
leaderboard: this.leaderboard.slice(0, 5),
})
}
}

View File

@@ -13,6 +13,7 @@
"@t3-oss/env-nextjs": "^0.13.8",
"clsx": "^2.1.1",
"ky": "^1.13.0",
"motion": "^12.23.24",
"next": "15.5.4",
"react": "19.1.0",
"react-confetti": "^6.4.0",

View File

@@ -1,26 +1,63 @@
import { ManagerStatusDataMap } from "@rahoot/common/types/game/status"
import { AnimatePresence, motion } from "motion/react"
import { useEffect, useState } from "react"
type Props = {
data: ManagerStatusDataMap["SHOW_LEADERBOARD"]
}
const Leaderboard = ({ data: { leaderboard } }: Props) => (
<section className="relative mx-auto flex w-full max-w-7xl flex-1 flex-col items-center justify-center px-2">
const Leaderboard = ({ data: { oldLeaderboard, leaderboard } }: Props) => {
const [displayedLeaderboard, setDisplayedLeaderboard] =
useState(oldLeaderboard)
useEffect(() => {
setDisplayedLeaderboard(oldLeaderboard)
const timer = setTimeout(() => {
setDisplayedLeaderboard(leaderboard)
}, 2000)
return () => clearTimeout(timer)
}, [oldLeaderboard, leaderboard])
return (
<section className="relative mx-auto flex w-full max-w-3xl flex-1 flex-col items-center justify-center px-2">
<h2 className="mb-6 text-5xl font-bold text-white drop-shadow-md">
Leaderboard
</h2>
<div className="flex w-full flex-col gap-2">
{leaderboard.map(({ username, points }, key) => (
<div
key={key}
<AnimatePresence mode="popLayout">
{displayedLeaderboard.map(({ username, points }) => (
<motion.div
key={username}
layout
initial={{ opacity: 0, y: 50 }}
animate={{
opacity: 1,
y: 0,
}}
exit={{
opacity: 0,
y: 50,
transition: { duration: 0.2 },
}}
transition={{
layout: {
type: "spring",
stiffness: 350,
damping: 25,
},
}}
className="bg-primary flex w-full justify-between rounded-md p-3 text-2xl font-bold text-white"
>
<span className="drop-shadow-md">{username}</span>
<span className="drop-shadow-md">{points}</span>
</div>
</motion.div>
))}
</AnimatePresence>
</div>
</section>
)
)
}
export default Leaderboard

60
pnpm-lock.yaml generated
View File

@@ -103,6 +103,9 @@ importers:
ky:
specifier: ^1.13.0
version: 1.13.0
motion:
specifier: ^12.23.24
version: 12.23.24(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
next:
specifier: 15.5.4
version: 15.5.4(@babel/core@7.28.5)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
@@ -1493,6 +1496,20 @@ packages:
resolution: {integrity: sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==}
engines: {node: '>= 0.4'}
framer-motion@12.23.24:
resolution: {integrity: sha512-HMi5HRoRCTou+3fb3h9oTLyJGBxHfW+HnNE25tAXOvVx/IvwMHK0cx7IR4a2ZU6sh3IX1Z+4ts32PcYBOqka8w==}
peerDependencies:
'@emotion/is-prop-valid': '*'
react: ^18.0.0 || ^19.0.0
react-dom: ^18.0.0 || ^19.0.0
peerDependenciesMeta:
'@emotion/is-prop-valid':
optional: true
react:
optional: true
react-dom:
optional: true
fsevents@2.3.3:
resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==}
engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
@@ -1902,6 +1919,26 @@ packages:
minimist@1.2.8:
resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==}
motion-dom@12.23.23:
resolution: {integrity: sha512-n5yolOs0TQQBRUFImrRfs/+6X4p3Q4n1dUEqt/H58Vx7OW6RF+foWEgmTVDhIWJIMXOuNNL0apKH2S16en9eiA==}
motion-utils@12.23.6:
resolution: {integrity: sha512-eAWoPgr4eFEOFfg2WjIsMoqJTW6Z8MTUCgn/GZ3VRpClWBdnbjryiA3ZSNLyxCTmCQx4RmYX6jX1iWHbenUPNQ==}
motion@12.23.24:
resolution: {integrity: sha512-Rc5E7oe2YZ72N//S3QXGzbnXgqNrTESv8KKxABR20q2FLch9gHLo0JLyYo2hZ238bZ9Gx6cWhj9VO0IgwbMjCw==}
peerDependencies:
'@emotion/is-prop-valid': '*'
react: ^18.0.0 || ^19.0.0
react-dom: ^18.0.0 || ^19.0.0
peerDependenciesMeta:
'@emotion/is-prop-valid':
optional: true
react:
optional: true
react-dom:
optional: true
ms@2.1.3:
resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
@@ -3889,6 +3926,15 @@ snapshots:
dependencies:
is-callable: 1.2.7
framer-motion@12.23.24(react-dom@19.1.0(react@19.1.0))(react@19.1.0):
dependencies:
motion-dom: 12.23.23
motion-utils: 12.23.6
tslib: 2.8.1
optionalDependencies:
react: 19.1.0
react-dom: 19.1.0(react@19.1.0)
fsevents@2.3.3:
optional: true
@@ -4268,6 +4314,20 @@ snapshots:
minimist@1.2.8: {}
motion-dom@12.23.23:
dependencies:
motion-utils: 12.23.6
motion-utils@12.23.6: {}
motion@12.23.24(react-dom@19.1.0(react@19.1.0))(react@19.1.0):
dependencies:
framer-motion: 12.23.24(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
tslib: 2.8.1
optionalDependencies:
react: 19.1.0
react-dom: 19.1.0(react@19.1.0)
ms@2.1.3: {}
nanoid@3.3.11: {}