diff --git a/package-lock.json b/package-lock.json
index 0a9421f..f613223 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -11,10 +11,12 @@
"clsx": "^2.1.0",
"next": "14.1.0",
"react": "^18",
+ "react-confetti": "^6.1.0",
"react-dom": "^18",
"react-hot-toast": "^2.4.1",
"socket.io": "^4.7.4",
- "socket.io-client": "^4.7.4"
+ "socket.io-client": "^4.7.4",
+ "use-sound": "^4.0.1"
},
"devDependencies": {
"autoprefixer": "^10.0.1",
@@ -2422,6 +2424,11 @@
"node": ">= 0.4"
}
},
+ "node_modules/howler": {
+ "version": "2.2.4",
+ "resolved": "https://registry.npmjs.org/howler/-/howler-2.2.4.tgz",
+ "integrity": "sha512-iARIBPgcQrwtEr+tALF+rapJ8qSc+Set2GJQl7xT1MQzWaVkFebdJhR3alVlSiUf5U7nAANKuj3aWpwerocD5w=="
+ },
"node_modules/ignore": {
"version": "5.3.0",
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.0.tgz",
@@ -3823,6 +3830,20 @@
"node": ">=0.10.0"
}
},
+ "node_modules/react-confetti": {
+ "version": "6.1.0",
+ "resolved": "https://registry.npmjs.org/react-confetti/-/react-confetti-6.1.0.tgz",
+ "integrity": "sha512-7Ypx4vz0+g8ECVxr88W9zhcQpbeujJAVqL14ZnXJ3I23mOI9/oBVTQ3dkJhUmB0D6XOtCZEM6N0Gm9PMngkORw==",
+ "dependencies": {
+ "tween-functions": "^1.2.0"
+ },
+ "engines": {
+ "node": ">=10.18"
+ },
+ "peerDependencies": {
+ "react": "^16.3.0 || ^17.0.1 || ^18.0.0"
+ }
+ },
"node_modules/react-dom": {
"version": "18.2.0",
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.2.0.tgz",
@@ -4610,6 +4631,11 @@
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz",
"integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q=="
},
+ "node_modules/tween-functions": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/tween-functions/-/tween-functions-1.2.0.tgz",
+ "integrity": "sha512-PZBtLYcCLtEcjL14Fzb1gSxPBeL7nWvGhO5ZFPGqziCcr8uvHp0NDmdjBchp6KHL+tExcg0m3NISmKxhU394dA=="
+ },
"node_modules/type-check": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
@@ -4772,6 +4798,17 @@
"punycode": "^2.1.0"
}
},
+ "node_modules/use-sound": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/use-sound/-/use-sound-4.0.1.tgz",
+ "integrity": "sha512-hykJ86kNcu6y/FzlSHcQxhjSGMslZx2WlfLpZNoPbvueakv4OF3xPxEtGV2YmculrIaH0tPp9LtG4jgy17xMWg==",
+ "dependencies": {
+ "howler": "^2.1.3"
+ },
+ "peerDependencies": {
+ "react": ">=16.8"
+ }
+ },
"node_modules/util-deprecate": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
diff --git a/package.json b/package.json
index 646951a..81be07c 100644
--- a/package.json
+++ b/package.json
@@ -12,10 +12,12 @@
"clsx": "^2.1.0",
"next": "14.1.0",
"react": "^18",
+ "react-confetti": "^6.1.0",
"react-dom": "^18",
"react-hot-toast": "^2.4.1",
"socket.io": "^4.7.4",
- "socket.io-client": "^4.7.4"
+ "socket.io-client": "^4.7.4",
+ "use-sound": "^4.0.1"
},
"devDependencies": {
"autoprefixer": "^10.0.1",
diff --git a/public/sounds/answersMusic.mp3 b/public/sounds/answersMusic.mp3
new file mode 100644
index 0000000..c182028
Binary files /dev/null and b/public/sounds/answersMusic.mp3 differ
diff --git a/public/sounds/answersSound.mp3 b/public/sounds/answersSound.mp3
new file mode 100644
index 0000000..aa01044
Binary files /dev/null and b/public/sounds/answersSound.mp3 differ
diff --git a/public/sounds/boump.mp3 b/public/sounds/boump.mp3
new file mode 100644
index 0000000..42862b8
Binary files /dev/null and b/public/sounds/boump.mp3 differ
diff --git a/public/sounds/first.mp3 b/public/sounds/first.mp3
new file mode 100644
index 0000000..0fcf5e2
Binary files /dev/null and b/public/sounds/first.mp3 differ
diff --git a/public/sounds/results.mp3 b/public/sounds/results.mp3
new file mode 100644
index 0000000..172af89
Binary files /dev/null and b/public/sounds/results.mp3 differ
diff --git a/public/sounds/second.mp3 b/public/sounds/second.mp3
new file mode 100644
index 0000000..59ef123
Binary files /dev/null and b/public/sounds/second.mp3 differ
diff --git a/public/sounds/show.mp3 b/public/sounds/show.mp3
new file mode 100644
index 0000000..d11b30d
Binary files /dev/null and b/public/sounds/show.mp3 differ
diff --git a/public/sounds/snearRoll.mp3 b/public/sounds/snearRoll.mp3
new file mode 100644
index 0000000..0293b2d
Binary files /dev/null and b/public/sounds/snearRoll.mp3 differ
diff --git a/public/sounds/three.mp3 b/public/sounds/three.mp3
new file mode 100644
index 0000000..23c47a3
Binary files /dev/null and b/public/sounds/three.mp3 differ
diff --git a/socket/src/quizz.config.js b/socket/src/quizz.config.js
index 46b7528..5ef76a3 100644
--- a/socket/src/quizz.config.js
+++ b/socket/src/quizz.config.js
@@ -18,7 +18,7 @@ export const GAME_STATE_INIT = {
"Bill Gate",
],
solution: 1,
- cooldow: 5,
+ cooldown: 5,
time: 15,
},
{
diff --git a/socket/src/roles/player.js b/socket/src/roles/player.js
index aed2b31..ae61221 100644
--- a/socket/src/roles/player.js
+++ b/socket/src/roles/player.js
@@ -32,6 +32,11 @@ const Player = {
return
}
+ if (game.players.find((p) => p.username === player.username)) {
+ socket.emit("game:errorMessage", "Username already exists")
+ return
+ }
+
if (game.started) {
socket.emit("game:errorMessage", "Game already started")
return
diff --git a/src/components/game/states/Answers.jsx b/src/components/game/states/Answers.jsx
index faa3149..6fcf1cc 100644
--- a/src/components/game/states/Answers.jsx
+++ b/src/components/game/states/Answers.jsx
@@ -2,7 +2,14 @@ import AnswerButton from "../../AnswerButton"
import { useSocketContext } from "@/context/socket"
import { useEffect, useRef, useState } from "react"
import clsx from "clsx"
-import { ANSWERS_COLORS, ANSWERS_ICONS } from "@/constants"
+import {
+ ANSWERS_COLORS,
+ ANSWERS_ICONS,
+ SFX_ANSWERS_MUSIC,
+ SFX_ANSWERS_SOUND,
+ SFX_RESULTS_SOUND,
+} from "@/constants"
+import useSound from "use-sound"
const calculatePercentages = (objectResponses) => {
const keys = Object.keys(objectResponses)
@@ -35,13 +42,49 @@ export default function Answers({
const [cooldown, setCooldown] = useState(time)
const [totalAnswer, setTotalAnswer] = useState(0)
+ const [sfxPop] = useSound(SFX_ANSWERS_SOUND, {
+ volume: 0.1,
+ })
+
+ const [sfxResults] = useSound(SFX_RESULTS_SOUND, {
+ volume: 0.2,
+ })
+
+ const [playMusic, { stop: stopMusic, isPlaying }] = useSound(
+ SFX_ANSWERS_MUSIC,
+ {
+ volume: 0.2,
+ },
+ )
+
+ const handleAnswer = (answer) => {
+ socket.emit("player:selectedAnswer", answer)
+ sfxPop()
+ }
+
useEffect(() => {
if (!responses) {
+ playMusic()
return
}
+ stopMusic()
+ sfxResults()
+
setPercentages(calculatePercentages(responses))
- }, [responses])
+ }, [responses, playMusic, stopMusic])
+
+ useEffect(() => {
+ if (!isPlaying) {
+ playMusic()
+ }
+ }, [isPlaying])
+
+ useEffect(() => {
+ return () => {
+ stopMusic()
+ }
+ }, [playMusic, stopMusic])
useEffect(() => {
socket.on("game:cooldown", (sec) => {
@@ -50,13 +93,14 @@ export default function Answers({
socket.on("game:playerAnswer", (count) => {
setTotalAnswer(count)
+ sfxPop()
})
return () => {
socket.off("game:cooldown")
socket.off("game:playerAnswer")
}
- }, [])
+ }, [sfxPop])
return (
@@ -113,7 +157,7 @@ export default function Answers({
"opacity-65": responses && correct !== key,
})}
icon={ANSWERS_ICONS[key]}
- onClick={() => socket.emit("player:selectedAnswer", key)}
+ onClick={() => handleAnswer(key)}
>
{answer}
@@ -123,9 +167,3 @@ export default function Answers({
)
}
-
-/* OLD Timer
-
- className="drop-shadow-md">20
-
-*/
diff --git a/src/components/game/states/Leaderboard.jsx b/src/components/game/states/Leaderboard.jsx
index dbfb926..b87d959 100644
--- a/src/components/game/states/Leaderboard.jsx
+++ b/src/components/game/states/Leaderboard.jsx
@@ -5,8 +5,11 @@ export default function Leaderboard({ data: { leaderboard } }) {
Leaderboard
- {leaderboard.map(({ username, points }) => (
-
+ {leaderboard.map(({ username, points }, key) => (
+
{username}
{points}
diff --git a/src/components/game/states/Podium.jsx b/src/components/game/states/Podium.jsx
index b959fb7..34763db 100644
--- a/src/components/game/states/Podium.jsx
+++ b/src/components/game/states/Podium.jsx
@@ -1,13 +1,60 @@
import Loader from "@/components/Loader"
+import {
+ SFX_PODIUM_FIRST,
+ SFX_PODIUM_SECOND,
+ SFX_PODIUM_THREE,
+ SFX_SNEAR_ROOL,
+} from "@/constants"
+import useScreenSize from "@/hook/useScreenSize"
import clsx from "clsx"
import { useEffect, useState } from "react"
+import ReactConfetti from "react-confetti"
+import useSound from "use-sound"
export default function Podium({ data: { subject, top } }) {
const [apparition, setApparition] = useState(0)
+ const { width, height } = useScreenSize()
+
+ const [sfxtThree] = useSound(SFX_PODIUM_THREE, {
+ volume: 0.2,
+ })
+
+ const [sfxSecond] = useSound(SFX_PODIUM_SECOND, {
+ volume: 0.2,
+ })
+
+ const [sfxRool, { stop: sfxRoolStop }] = useSound(SFX_SNEAR_ROOL, {
+ volume: 0.2,
+ })
+
+ const [sfxFirst] = useSound(SFX_PODIUM_FIRST, {
+ volume: 0.2,
+ })
+
+ useEffect(() => {
+ console.log(apparition)
+ switch (apparition) {
+ case 4:
+ sfxRoolStop()
+ sfxFirst()
+ break
+ case 3:
+ sfxRool()
+ break
+ case 2:
+ sfxSecond()
+ break
+ case 1:
+ sfxtThree()
+ break
+ }
+ }, [apparition, sfxFirst, sfxSecond, sfxtThree, sfxRool])
+
useEffect(() => {
if (top.length < 3) {
setApparition(4)
+ return
}
const interval = setInterval(() => {
@@ -19,91 +66,120 @@ export default function Podium({ data: { subject, top } }) {
}, 2000)
return () => clearInterval(interval)
- }, [])
+ }, [apparition])
return (
-
-
- {subject}
-
+ <>
+ {apparition >= 4 && (
+
+ )}
-
- {top[1] && (
-
= 2 },
- )}
- >
-
- {top[1].username}
-
-
-
- 2
-
-
- {top[1].points}
-
-
-
- )}
+ {apparition >= 3 && top.length >= 3 && (
+
+ )}
+
+
+ {subject}
+
= 3,
- },
- {
- "md:min-w-64": top.length < 2,
- },
- )}
+ className={`grid w-full max-w-[800px] flex-1 grid-cols-${top.length} items-end justify-center justify-self-end overflow-y-hidden overflow-x-visible`}
>
-
= 4 },
- )}
- >
- {top[0].username}
-
-
-
- 1
-
-
- {top[0].points}
-
-
-
+ {top[1] && (
+ = 2 },
+ )}
+ >
+
= 4,
+ },
+ )}
+ >
+ {top[1].username}
+
+
+
+ 2
+
+
+ {top[1].points}
+
+
+
+ )}
- {top[2] && (
= 1,
+ "!translate-y-0 opacity-100": apparition >= 3,
+ },
+ {
+ "md:min-w-64": top.length < 2,
},
)}
>
-
- {top[2].username}
+
= 4 },
+ )}
+ >
+ {top[0].username}
-
- 3
+
+ 1
-
- {top[2].points}
+ {top[0].points}
- )}
-
-
+
+ {top[2] && (
+
= 1,
+ },
+ )}
+ >
+
= 4,
+ },
+ )}
+ >
+ {top[2].username}
+
+
+
+ 3
+
+
+
+ {top[2].points}
+
+
+
+ )}
+
+
+ >
)
}
diff --git a/src/components/game/states/Question.jsx b/src/components/game/states/Question.jsx
index 35feff3..b175361 100644
--- a/src/components/game/states/Question.jsx
+++ b/src/components/game/states/Question.jsx
@@ -1,7 +1,13 @@
-import { useRef } from "react"
+import { SFX_SHOW_SOUND } from "@/constants"
+import { useEffect, useRef } from "react"
+import useSound from "use-sound"
export default function Question({ data: { question, image, cooldown } }) {
- const barRef = useRef(null)
+ const [sfxShow] = useSound(SFX_SHOW_SOUND, { volume: 0.5 })
+
+ useEffect(() => {
+ sfxShow()
+ }, [sfxShow])
return (
@@ -15,7 +21,6 @@ export default function Question({ data: { question, image, cooldown } }) {
)}
diff --git a/src/components/game/states/Result.jsx b/src/components/game/states/Result.jsx
index d3b91fb..1d99bef 100644
--- a/src/components/game/states/Result.jsx
+++ b/src/components/game/states/Result.jsx
@@ -1,19 +1,27 @@
import CricleCheck from "@/components/icons/CricleCheck"
import CricleXmark from "@/components/icons/CricleXmark"
+import { SFX_RESULTS_SOUND } from "@/constants"
import { usePlayerContext } from "@/context/player"
import { useEffect } from "react"
+import useSound from "use-sound"
export default function Result({
data: { correct, message, points, myPoints, totalPlayer, rank, aheadOfMe },
}) {
const { dispatch } = usePlayerContext()
+ const [sfxResults] = useSound(SFX_RESULTS_SOUND, {
+ volume: 0.2,
+ })
+
useEffect(() => {
dispatch({
type: "UPDATE",
payload: { points: myPoints },
})
- }, [])
+
+ sfxResults()
+ }, [sfxResults])
return (
diff --git a/src/components/game/states/Start.jsx b/src/components/game/states/Start.jsx
index 8ee7b81..19fe4df 100644
--- a/src/components/game/states/Start.jsx
+++ b/src/components/game/states/Start.jsx
@@ -1,18 +1,26 @@
+import { SFX_BOUMP_SOUND } from "@/constants"
import { useSocketContext } from "@/context/socket"
import clsx from "clsx"
import { useEffect, useState } from "react"
+import useSound from "use-sound"
export default function Start({ data: { time, subject } }) {
const { socket } = useSocketContext()
const [showTitle, setShowTitle] = useState(true)
const [cooldown, setCooldown] = useState(time)
+ const [sfxBoump] = useSound(SFX_BOUMP_SOUND, {
+ volume: 0.2,
+ })
+
useEffect(() => {
socket.on("game:startCooldown", () => {
+ sfxBoump()
setShowTitle(false)
})
socket.on("game:cooldown", (sec) => {
+ sfxBoump()
setCooldown(sec)
})
@@ -20,7 +28,7 @@ export default function Start({ data: { time, subject } }) {
socket.off("game:startCooldown")
socket.off("game:cooldown")
}
- }, [])
+ }, [sfxBoump])
return (
diff --git a/src/constants.js b/src/constants.js
index 073b5fa..aa9e9ce 100644
--- a/src/constants.js
+++ b/src/constants.js
@@ -51,3 +51,13 @@ export const GAME_STATE_COMPONENTS_MANAGER = {
SHOW_LEADERBOARD: Leaderboard,
FINISH: Podium,
}
+
+export const SFX_ANSWERS_MUSIC = "/sounds/answersMusic.mp3"
+export const SFX_ANSWERS_SOUND = "/sounds/answersSound.mp3"
+export const SFX_RESULTS_SOUND = "/sounds/results.mp3"
+export const SFX_SHOW_SOUND = "/sounds/show.mp3"
+export const SFX_BOUMP_SOUND = "/sounds/boump.mp3"
+export const SFX_PODIUM_THREE = "/sounds/three.mp3"
+export const SFX_PODIUM_SECOND = "/sounds/second.mp3"
+export const SFX_PODIUM_FIRST = "/sounds/first.mp3"
+export const SFX_SNEAR_ROOL = "/sounds/snearRoll.mp3"
diff --git a/src/hook/useScreenSize.js b/src/hook/useScreenSize.js
new file mode 100644
index 0000000..5a49744
--- /dev/null
+++ b/src/hook/useScreenSize.js
@@ -0,0 +1,26 @@
+import { useState, useEffect } from "react"
+
+const useScreenSize = () => {
+ const [screenSize, setScreenSize] = useState({
+ width: window.innerWidth,
+ height: window.innerHeight,
+ })
+
+ useEffect(() => {
+ const handleResize = () => {
+ setScreenSize({
+ width: window.innerWidth,
+ height: window.innerHeight,
+ })
+ }
+
+ window.addEventListener("resize", handleResize)
+ return () => {
+ window.removeEventListener("resize", handleResize)
+ }
+ }, [])
+
+ return screenSize
+}
+
+export default useScreenSize
diff --git a/src/styles/globals.css b/src/styles/globals.css
index d014b46..ee0bcf6 100644
--- a/src/styles/globals.css
+++ b/src/styles/globals.css
@@ -32,6 +32,49 @@
box-shadow: rgba(0, 0, 0, 0.25) 0px -4px inset;
}
+.spotlight {
+ position: absolute;
+ height: 200%;
+ width: 200%;
+ z-index: 100;
+ background-image: radial-gradient(
+ circle,
+ transparent 180px,
+ rgba(0, 0, 0, 0.6) 200px
+ );
+ opacity: 0;
+ left: -50%;
+ top: -50%;
+ transition: all 0.5s;
+ animation: spotlightAnim 2.5s ease-in;
+}
+
+@keyframes spotlightAnim {
+ 0% {
+ left: -20%;
+ top: -20%;
+ }
+ 30% {
+ opacity: 100;
+ top: -80%;
+ left: -80%;
+ }
+ 60% {
+ top: -50%;
+ left: -20%;
+ }
+ 80% {
+ top: -50%;
+ left: -50%;
+ }
+ 98% {
+ opacity: 1;
+ }
+ 100% {
+ opacity: 0;
+ }
+}
+
.anim-show {
animation: show 0.5s ease-out;
}
@@ -52,6 +95,28 @@
animation: quizzButton 0.8s ease-out;
}
+.anim-balanced {
+ animation: balanced 0.8s linear infinite;
+}
+
+@keyframes balanced {
+ 0% {
+ transform: rotate(0deg);
+ }
+ 25% {
+ transform: rotate(-10deg) translateY(-10px);
+ }
+ 50% {
+ transform: rotate(0deg) translateY(0px);
+ }
+ 75% {
+ transform: rotate(10deg) translateY(-10px);
+ }
+ 100% {
+ transform: rotate(0deg);
+ }
+}
+
@keyframes show {
0% {
transform: scale(0);