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);