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[]
|
answers: string[]
|
||||||
image?: string
|
image?: string
|
||||||
}
|
}
|
||||||
SHOW_LEADERBOARD: { leaderboard: Player[] }
|
SHOW_LEADERBOARD: { oldLeaderboard: Player[]; leaderboard: Player[] }
|
||||||
}
|
}
|
||||||
|
|
||||||
export type PlayerStatusDataMap = CommonStatusDataMap
|
export type PlayerStatusDataMap = CommonStatusDataMap
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ class Game {
|
|||||||
managerStatus: { name: Status; data: StatusDataMap[Status] } | null = null
|
managerStatus: { name: Status; data: StatusDataMap[Status] } | null = null
|
||||||
playerStatus: Map<string, { name: Status; data: StatusDataMap[Status] }> =
|
playerStatus: Map<string, { name: Status; data: StatusDataMap[Status] }> =
|
||||||
new Map()
|
new Map()
|
||||||
|
leaderboard: Player[]
|
||||||
|
|
||||||
quizz: Quizz
|
quizz: Quizz
|
||||||
players: Player[]
|
players: Player[]
|
||||||
@@ -58,6 +59,7 @@ class Game {
|
|||||||
this.lastBroadcastStatus = null
|
this.lastBroadcastStatus = null
|
||||||
this.managerStatus = null
|
this.managerStatus = null
|
||||||
this.playerStatus = new Map()
|
this.playerStatus = new Map()
|
||||||
|
this.leaderboard = []
|
||||||
|
|
||||||
this.players = []
|
this.players = []
|
||||||
|
|
||||||
@@ -490,20 +492,24 @@ class Game {
|
|||||||
this.round.currentQuestion + 1 === this.quizz.questions.length
|
this.round.currentQuestion + 1 === this.quizz.questions.length
|
||||||
|
|
||||||
const sortedPlayers = this.players.sort((a, b) => b.points - a.points)
|
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) {
|
if (isLastRound) {
|
||||||
this.started = false
|
this.started = false
|
||||||
|
|
||||||
this.broadcastStatus(STATUS.FINISHED, {
|
this.broadcastStatus(STATUS.FINISHED, {
|
||||||
subject: this.quizz.subject,
|
subject: this.quizz.subject,
|
||||||
top: sortedPlayers.slice(0, 3),
|
top: this.leaderboard.slice(0, 3),
|
||||||
})
|
})
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
this.sendStatus(this.manager.id, STATUS.SHOW_LEADERBOARD, {
|
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",
|
"@t3-oss/env-nextjs": "^0.13.8",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"ky": "^1.13.0",
|
"ky": "^1.13.0",
|
||||||
|
"motion": "^12.23.24",
|
||||||
"next": "15.5.4",
|
"next": "15.5.4",
|
||||||
"react": "19.1.0",
|
"react": "19.1.0",
|
||||||
"react-confetti": "^6.4.0",
|
"react-confetti": "^6.4.0",
|
||||||
|
|||||||
@@ -1,26 +1,63 @@
|
|||||||
import { ManagerStatusDataMap } from "@rahoot/common/types/game/status"
|
import { ManagerStatusDataMap } from "@rahoot/common/types/game/status"
|
||||||
|
import { AnimatePresence, motion } from "motion/react"
|
||||||
|
import { useEffect, useState } from "react"
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
data: ManagerStatusDataMap["SHOW_LEADERBOARD"]
|
data: ManagerStatusDataMap["SHOW_LEADERBOARD"]
|
||||||
}
|
}
|
||||||
|
|
||||||
const Leaderboard = ({ data: { leaderboard } }: Props) => (
|
const Leaderboard = ({ data: { oldLeaderboard, leaderboard } }: Props) => {
|
||||||
<section className="relative mx-auto flex w-full max-w-7xl flex-1 flex-col items-center justify-center px-2">
|
const [displayedLeaderboard, setDisplayedLeaderboard] =
|
||||||
<h2 className="mb-6 text-5xl font-bold text-white drop-shadow-md">
|
useState(oldLeaderboard)
|
||||||
Leaderboard
|
|
||||||
</h2>
|
useEffect(() => {
|
||||||
<div className="flex w-full flex-col gap-2">
|
setDisplayedLeaderboard(oldLeaderboard)
|
||||||
{leaderboard.map(({ username, points }, key) => (
|
|
||||||
<div
|
const timer = setTimeout(() => {
|
||||||
key={key}
|
setDisplayedLeaderboard(leaderboard)
|
||||||
className="bg-primary flex w-full justify-between rounded-md p-3 text-2xl font-bold text-white"
|
}, 2000)
|
||||||
>
|
|
||||||
<span className="drop-shadow-md">{username}</span>
|
return () => clearTimeout(timer)
|
||||||
<span className="drop-shadow-md">{points}</span>
|
}, [oldLeaderboard, leaderboard])
|
||||||
</div>
|
|
||||||
))}
|
return (
|
||||||
</div>
|
<section className="relative mx-auto flex w-full max-w-3xl flex-1 flex-col items-center justify-center px-2">
|
||||||
</section>
|
<h2 className="mb-6 text-5xl font-bold text-white drop-shadow-md">
|
||||||
)
|
Leaderboard
|
||||||
|
</h2>
|
||||||
|
<div className="flex w-full flex-col gap-2">
|
||||||
|
<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>
|
||||||
|
</motion.div>
|
||||||
|
))}
|
||||||
|
</AnimatePresence>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
export default Leaderboard
|
export default Leaderboard
|
||||||
|
|||||||
60
pnpm-lock.yaml
generated
60
pnpm-lock.yaml
generated
@@ -103,6 +103,9 @@ importers:
|
|||||||
ky:
|
ky:
|
||||||
specifier: ^1.13.0
|
specifier: ^1.13.0
|
||||||
version: 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:
|
next:
|
||||||
specifier: 15.5.4
|
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)
|
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==}
|
resolution: {integrity: sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==}
|
||||||
engines: {node: '>= 0.4'}
|
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:
|
fsevents@2.3.3:
|
||||||
resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==}
|
resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==}
|
||||||
engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
|
engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
|
||||||
@@ -1902,6 +1919,26 @@ packages:
|
|||||||
minimist@1.2.8:
|
minimist@1.2.8:
|
||||||
resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==}
|
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:
|
ms@2.1.3:
|
||||||
resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
|
resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
|
||||||
|
|
||||||
@@ -3889,6 +3926,15 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
is-callable: 1.2.7
|
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:
|
fsevents@2.3.3:
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
@@ -4268,6 +4314,20 @@ snapshots:
|
|||||||
|
|
||||||
minimist@1.2.8: {}
|
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: {}
|
ms@2.1.3: {}
|
||||||
|
|
||||||
nanoid@3.3.11: {}
|
nanoid@3.3.11: {}
|
||||||
|
|||||||
Reference in New Issue
Block a user