mirror of
https://github.com/randyjc/Rahoot.git
synced 2026-03-13 20:15:35 +01:00
feat(game): enhance leaderboard structure and animation in Leaderboard component
This commit is contained in:
@@ -47,7 +47,7 @@ type ManagerExtraStatus = {
|
||||
answers: string[]
|
||||
image?: string
|
||||
}
|
||||
SHOW_LEADERBOARD: { leaderboard: Player[] }
|
||||
SHOW_LEADERBOARD: { oldLeaderboard: Player[]; leaderboard: Player[] }
|
||||
}
|
||||
|
||||
export type PlayerStatusDataMap = CommonStatusDataMap
|
||||
|
||||
@@ -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),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
60
pnpm-lock.yaml
generated
@@ -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: {}
|
||||
|
||||
Reference in New Issue
Block a user