diff --git a/package-lock.json b/package-lock.json index 116a297..d6da7f3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,6 +10,7 @@ "dependencies": { "@ffmpeg/ffmpeg": "^0.12.10", "axios": "^1.6.7", + "byte-guide": "^1.0.8", "eslint-config-react-app": "^7.0.1", "lodash": "^4.17.21", "net": "^1.0.2", @@ -23,6 +24,7 @@ "serve": "^14.2.1", "sockjs-client": "^1.6.1", "stompjs": "^2.3.3", + "toastify": "^2.0.1", "uuid": "^9.0.1", "wavesurfer.js": "^7.7.5" }, @@ -5651,6 +5653,15 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/byte-guide": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/byte-guide/-/byte-guide-1.0.8.tgz", + "integrity": "sha512-imyryhwTZ6JSDlocaaql70dsB54HGqAH1Ady9TbbJU74aCVeLwWyUe17vga9PzakU2Ssaql578HlR62BTiAQ0w==", + "peerDependencies": { + "react": ">=16.12.0", + "react-dom": ">=16.12.0" + } + }, "node_modules/bytes": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz", @@ -17456,6 +17467,11 @@ "node": ">=8.0" } }, + "node_modules/toastify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/toastify/-/toastify-2.0.1.tgz", + "integrity": "sha512-Me/O93yAlwiHYwqX0su60Wv+X9ngzIT6nBLbNAFED095gMSjCBYKdu+faBxV6SE8CiQjUTBLrz8LwoXMtGz+yw==" + }, "node_modules/toidentifier": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", diff --git a/package.json b/package.json index 4932c9c..30211eb 100644 --- a/package.json +++ b/package.json @@ -5,6 +5,7 @@ "dependencies": { "@ffmpeg/ffmpeg": "^0.12.10", "axios": "^1.6.7", + "byte-guide": "^1.0.8", "eslint-config-react-app": "^7.0.1", "lodash": "^4.17.21", "net": "^1.0.2", @@ -18,6 +19,7 @@ "serve": "^14.2.1", "sockjs-client": "^1.6.1", "stompjs": "^2.3.3", + "toastify": "^2.0.1", "uuid": "^9.0.1", "wavesurfer.js": "^7.7.5" }, diff --git a/src/components/routing/routers/AppRouter.js b/src/components/routing/routers/AppRouter.js index 5f3e7b1..b125d38 100644 --- a/src/components/routing/routers/AppRouter.js +++ b/src/components/routing/routers/AppRouter.js @@ -6,6 +6,7 @@ import {LoginGuard} from "../routeProtectors/LoginGuard"; import Register from "../../views/Register"; import Login from "../../views/Login"; import Gameroom from "../../views/Gameroom"; +import RuleGuide from "../../views/RuleGuide"; /** * Main router of your application. * In the following class, different routes are rendered. In our case, there is a Login Route with matches the path "/login" @@ -42,6 +43,8 @@ const AppRouter = () => { } /> + }/> + }/> diff --git a/src/components/ui/ButtonPlayer.tsx b/src/components/ui/ButtonPlayer.tsx index 64e643a..96bcf49 100644 --- a/src/components/ui/ButtonPlayer.tsx +++ b/src/components/ui/ButtonPlayer.tsx @@ -18,13 +18,17 @@ export const ButtonPlayer = (props: ButtonPlayerProps) => { const playAudio = () => { if (isPlaying) { - audioRef.current.pause(); + audioRef.current.pause().catch((e) => console.error(e)); setIsPlaying((prev) => !prev); } else { // play form start audioRef.current.currentTime = 0; - audioRef.current.play(); - setIsPlaying((prev) => !prev); + audioRef.current.play() + .then(() => { + setIsPlaying((prev) => !prev); + } + ) + .catch((e) => console.error(e)); } }; @@ -48,7 +52,8 @@ export const ButtonPlayer = (props: ButtonPlayerProps) => { return (
- diff --git a/src/components/ui/Popup.tsx b/src/components/ui/Popup.tsx index 0c941eb..556579f 100644 --- a/src/components/ui/Popup.tsx +++ b/src/components/ui/Popup.tsx @@ -4,6 +4,7 @@ import "../../styles/ui/Popup.scss"; type PopupProps = { className?: string; children: React.ReactNode; + buttonJSX?: React.ReactNode; toggleDialog: () => void; }; @@ -27,6 +28,12 @@ const Popup = forwardRef((props, ref) => {
{props.children}
+ { + props.buttonJSX + &&
+ {props.buttonJSX} +
+ } ); }); diff --git a/src/components/views/Gameroom.tsx b/src/components/views/Gameroom.tsx index a11cc84..ff9b945 100644 --- a/src/components/views/Gameroom.tsx +++ b/src/components/views/Gameroom.tsx @@ -1,5 +1,4 @@ import React, { useCallback,useEffect, useState, useRef, useMemo } from "react"; -import { api, handleError } from "helpers/api"; import { useNavigate, useParams } from "react-router-dom"; import BaseContainer from "components/ui/BaseContainer"; import PropTypes from "prop-types"; @@ -36,7 +35,6 @@ type SharedAudioURL = { [userId: string]: string }; const Gameroom = () => { const navigate = useNavigate(); const { currentRoomID,currentRoomName } = useParams(); - const currentRoomNameValid = useRef(null); const stompClientRef = useRef(null); const user = { token: sessionStorage.getItem("token"), @@ -53,6 +51,7 @@ const Gameroom = () => { const [playerLists, setPlayerLists] = useState([]); const roundFinished = useRef(false); const [endTime, setEndTime] = useState(null); + const currentRoomNameValid = useRef(""); const gameTheme = useRef("Loading...."); const leaderboardInfoRecieved = useRef(false); const [leaderboardInfo, setLeaderboardInfo] = useState([]); @@ -119,7 +118,6 @@ const Gameroom = () => { let sharedAudioSuber; let responseSuber; - //const roomId = 5; const connectWebSocket = () => { const baseurl = getDomain(); let Sock = new SockJS(`${baseurl}/ws`); @@ -150,7 +148,6 @@ const Gameroom = () => { `/user/${user.id}/response/${currentRoomID}`, onResponseReceived ); - // enterRoom(); throttledEnterRoom(); //connect or reconnect }; @@ -201,13 +198,6 @@ const Gameroom = () => { } else if (messageType === "upload") { toastMessage = success ? "You have uploaded the audio successfully!" : msg.message; } - // else if (messageType === "leave") { - // toastMessage = success ? "You have left the room successfully!" : msg.message; - // if (success){ - // navigate("/lobby"); - // return; - // } - // } if (success) { showToast(toastMessage, "success"); @@ -215,11 +205,6 @@ const Gameroom = () => { showToast(toastMessage, "error"); } } - // const payloadData = JSON.parse(payload.body); - // console.error("Response received", payloadData.message); - // alert("Response server side receive!"+payloadData.message) - // navigate("/lobby"); - // TODO: handle response /// 1. filter the response by the receiptId /// 2. if the response is success, do nothing /// 3. if the response is failure, show the error message @@ -238,12 +223,8 @@ const Gameroom = () => { } readyStatus.current = myInfo.ready; if (!showReadyPopup && !gameOver){ - //console.log("set info for myself") - //console.log(myInfo); if (myInfo && myInfo.roundFinished !== null){ roundFinished.current = myInfo.roundFinished; - //console.log("roundFinished?") - //console.log(roundFinished.current); } } if (gameOverRef.current === true && leaderboardInfoRecieved.current === false){ @@ -254,10 +235,9 @@ const Gameroom = () => { const onGameInfoReceived = (payload) => { const payloadData = JSON.parse(payload.body); - // console.error("GameInfo received", JSON.stringify(payloadData.message)); if (JSON.stringify(gameInfoRef.current) === JSON.stringify(payloadData.message)) { console.log("Same game info received, ignore"); - + return; } if (currentRoomNameValid.current !== payloadData.message.roomName){ @@ -339,26 +319,6 @@ const Gameroom = () => { } }; - // const onMessageReceived = (payload) => { - // var payloadData = JSON.parse(payload.body); - // switch (payloadData.messageType) { - // case "PLAYERS": - // //jiaoyan - // setPlayerLists(payloadData.message); - // break; - // case "GAME": - // setGameInfo(payloadData.message); - // break; - // case "ROOM": - // setRoomInfo(payloadData.message); - // break; - // case "AUIDOSHARE": - // //function to deal with audio - // break; - // } - // } - - connectWebSocket(); // Cleanup on component unmount @@ -429,7 +389,7 @@ const Gameroom = () => { const getReady = useCallback(() => { console.log("ready once - throttle") const payload: Timestamped = { - // TODO: need to make sure the timestamp is UTC format + // need to make sure the timestamp is UTC format // and invariant to the time zone settings timestamp: new Date().getTime(), message: { @@ -463,7 +423,7 @@ const Gameroom = () => { const cancelReady = useCallback(() => { console.log("unready once - throttle") const payload: Timestamped = { - // TODO: need to make sure the timestamp is UTC format + // need to make sure the timestamp is UTC format // and invariant to the time zone settings timestamp: new Date().getTime(), message: { @@ -546,17 +506,6 @@ const Gameroom = () => { token: sessionStorage.getItem("token") }, JSON.stringify(payload) ); - // requestLists.current.push({ type: "leave",receiptId: receiptId }); - // console.log(requestLists.current) - // const timeoutId = setTimeout(() => { - // const index = requestLists.current.findIndex(request => request.receiptId === receiptId); - // if (index !== INDEX_NOT_FOUND) { - // requestLists.current.splice(index, 1); - // } - // console.log(requestLists.current) - // }, RESPONSE_TIME); - - // return () => clearTimeout(timeoutId); navigate("/lobby") },[user.id,currentRoomID]); const throttledExitRoom = useCallback(throttle(exitRoom, THROTTLE_TIME),[exitRoom, THROTTLE_TIME]); @@ -743,9 +692,9 @@ const Gameroom = () => { }; - if (playerLists === null || playerLists.length === 0) { - return
Loading...
; - } + // if (playerLists === null || playerLists.length === 0) { + // return
Loading...
; + // } return ( diff --git a/src/components/views/Lobby.tsx b/src/components/views/Lobby.tsx index a843e8b..4c2609a 100644 --- a/src/components/views/Lobby.tsx +++ b/src/components/views/Lobby.tsx @@ -1,8 +1,8 @@ import React, { useCallback, useRef, useEffect, useState } from "react"; -import { api, handleError } from "helpers/api"; +import { api } from "helpers/api"; import { Button } from "components/ui/Button"; import { throttle } from "lodash"; -import { useLocation, useNavigate } from "react-router-dom"; +import { useNavigate } from "react-router-dom"; import BaseContainer from "components/ui/BaseContainer"; import PropTypes from "prop-types"; import { User, Room } from "types"; @@ -11,10 +11,10 @@ import { Dropdown } from "components/ui/Dropdown"; import "styles/views/Lobby.scss"; import { getDomain } from "helpers/getDomain"; import "styles/ui/Popup.scss"; -import { MAX_USERNAME_LENGTH, MAX_ROOM_NAME_LENGTH, HTTP_STATUS } from "../../constants/constants"; +import { MAX_USERNAME_LENGTH, MAX_ROOM_NAME_LENGTH, HTTP_STATUS,AVATAR_LIST } from "../../constants/constants"; import SockJS from "sockjs-client"; import { over } from "stompjs"; -import { showToast} from "../../helpers/toastService"; +import { showToast } from "../../helpers/toastService"; import { Timestamped, RoomInfo, RoomPlayer, PlayerAndRoomID } from "stomp_types"; const DEFAULT_MAX_PLAYERS = 5; const DEFAULT_MIN_PLAYERS = 2; @@ -35,72 +35,10 @@ const Player: React.FC = ({ user }) => (
); -interface FormFieldProps { - label: string; - placeholder?: string; - value: string; - type?: string; - onChange: (value: string) => void; - disabled?: boolean; -} - Player.propTypes = { user: PropTypes.object, }; -const avatarList: string[] = [ - "angry-face", - "angry-face-with-horns", - "anguished-face", - "anxious-face-with-sweat", - "astonished-face", - "beaming-face-with-smiling-eyes", - "cat-face", - "clown-face", - "cold-face", - "confounded-face", - "confused-face", - "cow-face", - "cowboy-hat-face", - "crying-face", - "disappointed-face", - "disguised-face", - "dog-face", - "dotted-line-face", - "downcast-face-with-sweat", - "dragon-face", - "drooling-face", - "expressionless-face", - "face-blowing-a-kiss", - "face-exhaling", - "face-holding-back-tears", - "face-in-clouds", - "face-savoring-food", - "face-screaming-in-fear", - "face-vomiting", - "face-with-crossed-out-eyes", - "face-with-diagonal-mouth", - "face-with-hand-over-mouth", - "face-with-head-bandage", - "face-with-medical-mask", - "face-with-monocle", - "face-with-open-eyes-and-hand-over-mouth", - "face-with-open-mouth", - "face-with-peeking-eye", - "face-with-raised-eyebrow", - "face-with-rolling-eyes", - "face-with-spiral-eyes", - "face-with-steam-from-nose", - "face-with-symbols-on-mouth", - "face-with-tears-of-joy", - "face-with-thermometer", - "face-with-tongue", - "face-without-mouth", - "fearful-face", - "first-quarter-moon-face", - "flushed-face" -] - const Lobby = () => { const navigate = useNavigate(); const roomCreationPopRef = useRef(null); @@ -110,7 +48,7 @@ const Lobby = () => { const [rooms, setRooms] = useState([]); const [user, setUser] = useState(null); const [username, setUsername] = useState(""); - const [avatar, setAvatar] = useState(null); + const [currentAvatar, setCurrentAvatar] = useState(null); const [roomName, setRoomName] = useState(""); const [maxRoomPlayers, SetMaxRoomPlayers] = useState(DEFAULT_MIN_PLAYERS); const [roomTheme, setRoomTheme] = useState(""); @@ -125,7 +63,7 @@ const Lobby = () => { console.log(response); sessionStorage.clear(); } catch (error) { - showToast("Something went wrong during the logout: \n${handleError(error)}", "error"); + handleError(error); } navigate("/login"); }; @@ -138,6 +76,7 @@ const Lobby = () => { // try { const userResponse = await api.get(`/users/${userId}`); setUser(userResponse.data); // Set user data from API + setCurrentAvatar(userResponse.data.avatar); console.log("User data:", userResponse.data); // } catch (error) { // handleError(error); @@ -146,6 +85,8 @@ const Lobby = () => { // } } else { console.error("User ID not found in sessionStorage!"); + showToast("User ID not found in sessionStorage!", "error"); + navigate("/login"); } } @@ -165,7 +106,11 @@ const Lobby = () => { onLobbyInfoReceived ); stompClientRef.current?.send( - "/app/message/lobby/info", { receiptId: "" } + "/app/message/lobby/info", + { + receiptId: "", + token: sessionStorage.getItem("token") + } ); @@ -185,7 +130,7 @@ const Lobby = () => { // showToast("Reconnect to your previous room!", "success"); // } - setRooms(payload); + setRooms(payload); console.log("Rooms updated:", message_lobby.message); } else { console.error("Received data is not in expected format:", message_lobby); @@ -201,12 +146,16 @@ const Lobby = () => { navigate("/login"); }; - // make sure user was fetched before set timeoutId - fetchData().catch(error => { - handleError(error); - }); - - connectWebSocket(); + // make sure the ws connection was opened after fetching data + fetchData() + .then( + () => { + connectWebSocket(); + } + ) + .catch(error => { + handleError(error); + }); return () => { @@ -250,7 +199,7 @@ const Lobby = () => { const doEdit = async () => { try { - const requestBody = JSON.stringify({ username: username, avatar: avatar }); + const requestBody = JSON.stringify({ username: username, avatar: currentAvatar }); const id = sessionStorage.getItem("id"); console.log("Request body:", requestBody); await api.put(`/users/${id}`, requestBody); @@ -266,8 +215,8 @@ const Lobby = () => { const createRoom = async () => { // if not chrome, alert the user if (!navigator.userAgent.includes("Chrome")) { - showToast("Your browser is currently not supported, please use Chrome to play this game!","error"); - + showToast("Your browser is currently not supported, please use Chrome to play this game!", "error"); + return; } try { @@ -352,7 +301,7 @@ const Lobby = () => { const changeAvatar = async (newAvatar) => { try { // 更新本地状态 - setAvatar(newAvatar); + setCurrentAvatar(newAvatar); // 构造请求体,只包含 avatar 更改 const requestBody = JSON.stringify({ avatar: newAvatar }); @@ -404,7 +353,7 @@ const Lobby = () => { } else { // Handle other types of errors generically console.error(`Error: ${data.message}\n${error}`); - showToast("An error occurred: ${data.message}", "error"); + showToast(`An error occurred: ${data.message}`, "error"); } } else if (error.message && error.message.match(/Network Error/)) { // Handle network errors @@ -415,7 +364,7 @@ const Lobby = () => { showToast("The server cannot be reached.\nDid you start it?", "error"); } else { console.error(`Something went wrong: \n${error}`); - showToast("Something went wrong: \n${error}", "error"); + showToast(`Something went wrong: \n${error}`, "error"); } } @@ -427,7 +376,7 @@ const Lobby = () => { showToast("Session expired or invalid, please log in again.", "error"); sessionStorage.clear(); // Clear session storage navigate("/login"); - + return; // Exit the function to avoid further processing } @@ -450,49 +399,68 @@ const Lobby = () => { }, [navigate, showToast]); const renderRoomLists = () => { - return rooms.map((Room) => ( -
-
- {Room.roomPlayersList?.map((user, index) => ( -
- + return rooms.map((Room) => { + const playerSlots = []; + + // 生成玩家头像或空白框,总数等于房间最大玩家数 + for (let i = 0; i < Room.roomMaxNum; i++) { + if (i < Room.roomPlayersList.length) { + const user = Room.roomPlayersList[i]; + playerSlots.push( +
+
{user.userName}
- ))} -
-
-
{Room.roomName}
-
{Room.theme}
- - {Room.status} - + ); + } else { + // 空白框 + playerSlots.push( +
+ +
+ ); + } + } + + return ( +
+
+ {playerSlots} +
+
+
{Room.roomName}
+
{Room.theme}
+ + {Room.status} + +
-
- )); + ) + }); }; - if (user === null) { - return Loading...; - } + // if (user === null) { + // return Loading...; + // } return ( -
- -
{user.username}
-
- + {user && ( +
+ +
{user.username}
+
+ +
-
+ )}
Kaeps
i
@@ -511,62 +479,69 @@ const Lobby = () => {
- - - -
{ - toggleAvatarPop(); - toggleProfilePop(); - }}> - -
-
-
+
User ID: {user.id}
+
Register Date: {user.registerDate}
- {/*
RegisterDate: {user && new Date(user.registerDate).toLocaleDateString()}
*/} + {/*
RegisterDate: {user && new Date(user.registerDate).toLocaleDateString()}
*/} -
- - -
-
-
+
+ + )} -
- {avatarList?.map((avatar, index) => ( -
+ {AVATAR_LIST?.map((avatar, index) => ( +
{ changeAvatar(avatar).then(r => toggleAvatarPop); toggleAvatarPop(); @@ -576,10 +551,16 @@ const Lobby = () => {
- + + + + } >
Create Room
@@ -598,12 +579,12 @@ const Lobby = () => {
Number of Maximum Players:
{ const value = parseInt(e.target.value); // console.error("Value:", value); - SetMaxRoomPlayers(value >= DEFAULT_MIN_PLAYERS && value <= DEFAULT_MAX_PLAYERS ? value : DEFAULT_MIN_PLAYERS); + SetMaxRoomPlayers(value); }} min={DEFAULT_MIN_PLAYERS} max={DEFAULT_MAX_PLAYERS} @@ -620,33 +601,41 @@ const Lobby = () => { ]} onChange={(value) => setRoomTheme(value)} /> -
- - -
- + + + + + }>

Welcome to KAEPS!

-

Here are some guides for playing this game:

+

Here are some guides to help you get started with the game:

    -
  • Speaker: Receives a word, records it, inverts the audio, and sends it to other players.
  • -
  • Challenger: Receives the reversed audio recording from the speaker. - The challenger should then mimic this reversed recording. - After recording their own version of the reversed audio, they should play it backwards to guess the original word.
  • -
  • Scoring: Correctly deciphering the word scores you points.
  • -
  • Turns: Each round has one Speaker and multiple Challengers. Players take turns to be the Speaker.
  • +
  • Speaker: The speaker receives a word, records it, inverts the recording, and then sends this inverted audio to other players.
  • +
  • Challenger: Challengers listen to the inverted audio sent by the speaker. + You must mimic this recording and then play their recording backwards to guess the original word. + You can guess multiple times before time is up. +
  • +
  • Scoring: Points are awarded for correctly guessing the word. The faster you guess, the more points you earn.
  • +
  • Turns: The game is played in rounds. Each round has one speaker and several challengers. Players alternate roles as the Speaker to ensure fairness.
+

Click GUIDE for more detailed instructions.

Join a room or create one to play with friends!

-
-
- +
diff --git a/src/components/views/RuleGuide.tsx b/src/components/views/RuleGuide.tsx new file mode 100644 index 0000000..e233f24 --- /dev/null +++ b/src/components/views/RuleGuide.tsx @@ -0,0 +1,325 @@ +import React, { useState,useEffect, useRef, useMemo, useLayoutEffect } from "react"; +import { useNavigate } from "react-router-dom"; +import Guide from "byte-guide"; +import { PlayerList } from "./GameroomPlayerList"; +import { Roundstatus } from "./GameroomRoundStatus"; +import { ValidateAnswerForm } from "./GameroomAnswerForm"; +import BaseContainer from "components/ui/BaseContainer"; +import Header from "./Header"; +import { FFmpeg } from "@ffmpeg/ffmpeg"; +import "../../styles/views/RuleGuide.scss"; +import { showToast} from "../../helpers/toastService"; + +const mockGameTheme = "FOOD"; +const mockRoomName = "GuideRoom"; +const mockGlobalVolume = 0.5; +const MS_PER_SEC = 1000; +const DEFAULT_ROUND_DURATION_S = 20; + +const mockGameInfo = { + roomID: "1", + currentSpeaker: { + userID: "id2", + username: "myself", + avatar: "dog-face" + }, + currentAnswer: "Banana", + roundStatus: "speak", + currentRoundNum: 1 +}; +const mockSharedAudioList = { + "id1": "fakeURL" +}; + +const mockPlayerLists = [ + { + user: { + id: "id1", + name: "user1", + avatar: "clown-face" + }, + score: { + total: 0 + }, + ifGuess: true, + roundFinished: false, + ready: true + }, + { + user: { + id: "id2", + name: "myself", + avatar: "dog-face" + }, + score: { + total: 0 + }, + ifGuess: false, + roundFinished: false, + ready: true + }, + { + user: { + id: "id3", + name: "user3", + avatar: "cat-face" + }, + score: { + total: 0 + }, + ifGuess: true, + roundFinished: false, + ready: true + }, + { + user: { + id: "id4", + name: "user4", + avatar: "angry-face" + }, + score: { + total: 0 + }, + ifGuess: true, + roundFinished: false, + ready: true + + } +]; +const gameOver = false; +const user = { + token: "mockToken", + id: "id2", + username: "myself", +}; +const currentSpeakerAudioURL = "fakeURL"; + +const RuleGuide = () => { + useLayoutEffect(() => { + // remove the guide key from localStorage + if (localStorage.getItem("RuleGuide")) { + localStorage.removeItem("RuleGuide"); + } + }, []); + const navigate = useNavigate(); + const roundStatusComponentRef = useRef(null); + const [gameInfo, setGameInfo] = useState(mockGameInfo); + const [playerInfo, setPlayerInfo] = useState(mockPlayerLists); + const endTime = useMemo(() => new Date(Date.now() + DEFAULT_ROUND_DURATION_S * MS_PER_SEC).toISOString(), [gameInfo.roundStatus]); + const ffmpegObj = useMemo(() => { + const ffmpeg = new FFmpeg(); + try { + ffmpeg.load(); + } + catch (e) { + console.error(e); + alert("Failed to load ffmpeg"); + } + + return ffmpeg; + }); + + useEffect(() => { + const handleEscKey = (event) => { + if (event.key === "Escape") { + event.preventDefault(); // Cancel the default action + navigate("/lobby"); + } + }; + + window.addEventListener("keydown", handleEscKey); + + return () => { + window.removeEventListener("keydown", handleEscKey); + }; + }, [navigate]); + + + return ( + + {/*
*/} + +
+
{ + // setGlobalVolume(e.target.value); + // console.log("[volume] set to", e.target.value); + } + } + onClickMute={ + () => { + // if (globalVolume === 0) { + // setGlobalVolume(globalVolumeBeforeMute.current); + // } else { + // globalVolumeBeforeMute.current = globalVolume; + // setGlobalVolume(0); + // } + } + } + volume={mockGlobalVolume} + /> + {!gameOver && ( + { }} + ref={roundStatusComponentRef} + /> + )} +
+ {gameInfo.roundStatus === "guess" && ( +
+ { }} + roundFinished={false} + /> +
+ )} + {gameInfo.roundStatus === "speak" && ( + + )} + {gameInfo.roundStatus === "guess" && ( +
{ + //console.log("upload audio"); + // throttledUploadAudio(); + } + }>share your audio
+ )} +
+
+ { + setGameInfo( + { + ...gameInfo, + roundStatus: "guess", + currentSpeaker: { + userID: "id3", + username: "user3", + avatar: "cat-face" + } + } + ); + setPlayerInfo( + playerInfo.map( + (player) => { + if (player.user.id === "id3") { + return { + ...player, + ifGuess: false + }; + } + if (player.user.name === "myself") { + return { + ...player, + ifGuess: true + }; + } + + return { + ...player, + }; + } + ) + ); + } + }, + { + selector: ".roundstatus", + title: "Guess-Phase", + content: "Now, user3 is the Speaker, and you need to guess the word by simulating the reversed audio", + placement: "left", + }, + { + selector: ".speakPlayerContainer", + title: "Guess-Phase", + content: "Click here to listen to the speaker's audio", + }, + { + selector: ".remindermssg", + title: "Guess-Phase", + content: "You shuold simulate the reversed audio and reverse it again to figure out the word", + placement: "top", + }, + { + selector: ".inputarea", + title: "Guess-Phase", + content: "After you figure out the word, you can submit your answer here, also you can share your audio with others (Optional)", + placement: "top", + }, + { + selector: ".btn-player", + title: "Guess-Phase", + content: "When someone shares their audio, you can click here to listen to their audio", + beforeStepChange: () => { + showToast("Congratulations! You have successfully completed the Rule Guide!\nEnjoy the game!", "success"); + navigate("/lobby"); + } + }, + ]} + localKey="RuleGuide" + arrow={true} + visible={true} + lang="en" + mask={true} + step={0} + /> + + ); +} + +export default RuleGuide; \ No newline at end of file diff --git a/src/constants/constants.ts b/src/constants/constants.ts index 123b55a..d0c3ab0 100644 --- a/src/constants/constants.ts +++ b/src/constants/constants.ts @@ -16,3 +16,56 @@ export const HTTP_STATUS = { INTERNAL_SERVER_ERROR: 500 }; +export const AVATAR_LIST : string[] = [ + "angry-face", + "angry-face-with-horns", + "anguished-face", + "anxious-face-with-sweat", + "astonished-face", + "beaming-face-with-smiling-eyes", + "cat-face", + "clown-face", + "cold-face", + "confounded-face", + "confused-face", + "cow-face", + "cowboy-hat-face", + "crying-face", + "disappointed-face", + "disguised-face", + "dog-face", + "dotted-line-face", + "downcast-face-with-sweat", + "dragon-face", + "drooling-face", + "expressionless-face", + "face-blowing-a-kiss", + "face-exhaling", + "face-holding-back-tears", + "face-in-clouds", + "face-savoring-food", + "face-screaming-in-fear", + "face-vomiting", + "face-with-crossed-out-eyes", + "face-with-diagonal-mouth", + "face-with-hand-over-mouth", + "face-with-head-bandage", + "face-with-medical-mask", + "face-with-monocle", + "face-with-open-eyes-and-hand-over-mouth", + "face-with-open-mouth", + "face-with-peeking-eye", + "face-with-raised-eyebrow", + "face-with-rolling-eyes", + "face-with-spiral-eyes", + "face-with-steam-from-nose", + "face-with-symbols-on-mouth", + "face-with-tears-of-joy", + "face-with-thermometer", + "face-with-tongue", + "face-without-mouth", + "fearful-face", + "first-quarter-moon-face", + "flushed-face" +] + diff --git a/src/styles/ui/Popup.scss b/src/styles/ui/Popup.scss index 0732e27..de35d3d 100644 --- a/src/styles/ui/Popup.scss +++ b/src/styles/ui/Popup.scss @@ -6,9 +6,9 @@ border: none; border-radius: 10px; box-shadow: 0 0 0 10px $classicYellow; - min-height: auto; + min-height: fit-content; } -.popup::-webkit-scrollbar { +.popup-content::-webkit-scrollbar { width: 10px; height: 10px; &-track { @@ -23,10 +23,28 @@ } } .popup-content { - height: 100%; - padding: 20px; + max-height: 60vh; + max-width: 100vw; + padding-left: 20px; + padding-right: 20px; display: flex; flex-direction: column; justify-content: space-between; align-items: center; + overflow: scroll; +} +.popup-button-container { + display: flex; + flex-direction: row; + justify-content: center; + align-items: flex-end; + Button { + height: auto; + font-size: 1em; + padding: 0.6em; + margin: 0 10px; + color: white; + background-color: $classicYellow; + text-transform: none; + } } diff --git a/src/styles/views/Lobby.scss b/src/styles/views/Lobby.scss index c2dee16..70a4d5f 100644 --- a/src/styles/views/Lobby.scss +++ b/src/styles/views/Lobby.scss @@ -6,18 +6,8 @@ for more information visit https://sass-lang.com/guide */ - -.lobby { - &.container { - background: rgba(255, 255, 255); - padding: 1.5em; - border-radius: $borderRadius; - display: flex; - flex-direction: column; - align-items: center; - box-shadow: $dropShadow; - } - &.room-list::-webkit-scrollbar { +// scrollbar styling for the whole page +::-webkit-scrollbar { width: 10px; height: 10px; @@ -31,6 +21,20 @@ -webkit-border-radius: 10px; border-radius: 10px; } +} +Button { + font-family: sans-serif; +} + +.lobby { + &.container { + background: rgba(255, 255, 255); + padding: 1.5em; + border-radius: $borderRadius; + display: flex; + flex-direction: column; + align-items: center; + box-shadow: $dropShadow; } &.room-list-wrapper { position: absolute; // Using absolute positioning @@ -51,6 +55,14 @@ flex-direction: column; flex-grow: 1; overflow-y: auto; + h1 { + color: $whiteYellow; + text-shadow: none; + text-transform: none; + margin-inline: 5%; + font-weight: 800; + font-size: 2em; + } &.btn-container { all: initial; // avoid the inherited styles position: relative; // absolute position relative to the parent @@ -65,6 +77,7 @@ left: 0%; margin-top: 30px; flex-grow: 1; + font-family: sans-serif; .create-room-btn { bottom: 0%; min-width: fit-content; @@ -128,26 +141,26 @@ margin-inline: 5%; justify-content: space-between; flex-direction: row; - text-align: center; + text-align: end; } .room { background-color: #fff; margin-bottom: 10px; border-radius: 8px; text-align: center; + cursor: pointer; + //text-align: center; //overflow: hidden; - .room-header { margin-left: 10px; flex-shrink: 0; // Allow both children to grow and take up equal space display: flex; flex-direction: column; // Stack the children of these containers vertically align-items: center; // Center the items horizontally - justify-content: center; // Center the items vertically + justify-content: flex-end; // Center the items vertically padding: 10px; - text-align: center; + text-align: right; } - .room-header { flex-shrink: 0; flex-grow: 0; @@ -155,11 +168,10 @@ margin-left: 10px; flex-direction: column; /* Stack children vertically */ align-items: center; /* Horizontally center */ - justify-content: center; /* Vertically center */ - text-align: center; + justify-content: flex-end; /* Vertically center */ + text-align: right; padding: 10px; // Align room details to the end of the flex container } - .room-footer { display: flex; justify-content: space-between; @@ -171,50 +183,68 @@ } } } -.avatar-list { - display: flex; - flex-wrap: wrap; // Allow avatar list to wrap - justify-content: flex-start; // Align avatars to the left - margin: 0 -10px; // Offset for each avatar's margin, adjustable as needed - .player { - width: 20%; // Four avatars per row, each taking 25% of total width - padding-left: 10%; // Padding around each avatar, adjustable as needed - box-sizing: border-box; // Include padding in width calculation +.avatar-popup{ + max-width: 80vw; + justify-content: center; + .popup-content{ + display: flex; + } + .avatar-list { + display: flex; + flex-wrap: wrap; + justify-content: center; + padding: 0; + } - i { - display: block; // Ensure icons are block-level - font-size: 3.8rem; // Icon size - cursor: pointer; // Show pointer on hover - margin: 10px 0; // Vertical spacing, adjustable as needed - } + .avatar-container { + border-radius: 10%; + padding: 10px; + margin: 10px; + box-sizing: border-box; + background-color: #D9D9D9; /* Default background color */ + transition: background-color 0.5s ease; } + + .avatar-container.selected { + background-color: $lightYellow; /* Background color for selected avatar */ + } + + .avatar-container:hover { + background-color: darken(#D9D9D9, 20%); + } + .avatar-container.selected:hover { + background-color: $lightYellow; + } + + i { + display: block; + font-size: 3.8rem; + cursor: pointer; + } + } .player { + height: 100%; + aspect-ratio: 1; margin-right: 10px; margin-left: 10px; text-align: center; - .player-avatar { - width: 40px; - height: 40px; + width: 60.8px; + height: 60.8px; border-radius: 50%; background-color: #D9D9D9; // Default background color } - .player-username { font-size: 0.8em; } } .room-players { - //width: 70%; - align-items: flex-start; // Align the players to the start of the flex container flex: 1 1 auto; // Allow both children to grow and take up equal space display: flex; flex-direction: row; // Stack children vertically - //align-items: center; // Horizontally center items - //justify-content: center; // Vertically center items padding: 10px; overflow-x: auto; white-space: nowrap; @@ -225,20 +255,26 @@ } .room-status { padding: 4px; - width: 70px; + //width: 70px; display: inline-block; text-align: center; + border-radius: $borderRadius; // 确保你已定义了 $borderRadius 变量 + &.in-game { - border-radius: $borderRadius; background-color: orange; } - &.free { - - border-radius: $borderRadius; - background-color: lightgreen; + &.waiting { + background-color: lightgreen; // 等待状态的颜色 + } + &.game-over { + background-color: lightcoral; // 游戏结束状态的颜色 } } .player { + background: rgba(217, 217, 217, 1); + border-radius: 20px; + width: 85px; + height: 85px; &.container { margin: 0.5em 0; width: 20em; @@ -246,7 +282,7 @@ border-radius: $borderRadius; display: flex; align-items: center; - background: lighten($background, 5); + background: gray; } &.status { font-weight: 600; @@ -296,61 +332,62 @@ .information { position: absolute; + display: flex; top: 0; - right: 0; - width: 1.5rem; - height: 2rem; - font-size: 1.5rem; + right: -10%; + width: 10%; + aspect-ratio: 1; + font-size: 0.15em; font-weight: bold; - //display: flex; text-align: center; - //line-height: 2rem; + align-items: center; + justify-content: center; color: white; background: $classicYellow; - border-radius: $borderRadius; - //padding: 0.25rem; + border-radius: 50%; cursor: pointer; } -.intro-popup { - height:60%; - //width: 500px; - &.btn-container { - width: 200px; - display: flex; - flex-direction: row; - justify-content: center; - align-items: flex-end; - //margin-top: 10%; - Button { - height: auto; - font-size: 1em; - padding: 0.6em; - margin: 0 10px; - color: white; - background-color: $classicYellow; - } - } -} .intro-cnt { width: 450px; + max-width: 40vw; + ul { + padding-left: 1rem; + } } .profile-popup { - height:60%; &.content { display: flex; flex-direction: column; flex-grow: 1; - font-size: 1.5em; - .title { - font-size: 1.5em; - margin-bottom: 20px; - display: inline; - text-align: center; + justify-content: center; + // font-size: 1.5em; + .field{ + display: flex; + flex-direction: row; + align-content: center; + .label { + display: flex; + align-items: center; + } + } + input { + width: 300px; + max-width: 60vw; + padding-left: 10px; + border: 1px solid black; + border-radius: 0.75em; + background: transparentize(white, 0.4); + color: $textColor; } .avatar-container { display: flex; justify-content: center; /* Horizontally center */ margin-bottom: 3rem; + i { + font-size: 10rem; + color: $classicYellow; + cursor: pointer; + } } } @@ -381,34 +418,34 @@ background-color: $classicYellow; } } - &.label { - display: flex; - font-size: 32px; - } - &.field { - display: flex; - flex-direction: row; - justify-content: center; - } - &.input { - height: 35px; - padding-left: 15px; - margin-left: 4px; - border: none; - border-radius: 0.75em; - margin-top: 5px; - background: #FFF3CF; - color: $textColor; - } + // &.label { + // display: flex; + // font-size: 32px; + // } + // &.field { + // display: flex; + // flex-direction: row; + // justify-content: center; + // } + // &.input { + // height: 35px; + // padding-left: 15px; + // margin-left: 4px; + // border: none; + // border-radius: 0.75em; + // margin-top: 5px; + // background: #FFF3CF; + // color: $textColor; + // } } .room-creation-popup { - height: 60%; + //height: 60%; &.content { display: flex; flex-direction: column; flex-grow: 1; - font-size: 2em; + font-size: 1.5em; .title { font-size: 1.5em; margin-bottom: 20px; @@ -416,8 +453,9 @@ text-align: center; } input { - height: 100px; + height: 20%; width: 600px; + max-width: 60vw; padding-left: 1px; margin-left: 4px; border: 1px solid black; @@ -433,9 +471,10 @@ align-items: center; justify-items: center; width: 600px; - height: 100px; - margin-top: 20px; - margin-bottom: 40px; + max-width: 60vw; + height: 60px; + //margin-top: 20px; + // margin-bottom: 40px; select { appearance: none; -webkit-appearance: none; @@ -478,6 +517,7 @@ left: 0%; margin-top: 10px; flex-grow: 1; + font-family: sans-serif; .logout-btn { bottom: 1%; min-width: fit-content; @@ -493,6 +533,5 @@ color: white; text-transform: none; font-weight: bolder; - } } diff --git a/src/styles/views/Login.scss b/src/styles/views/Login.scss index 4dca06f..286afe4 100644 --- a/src/styles/views/Login.scss +++ b/src/styles/views/Login.scss @@ -48,11 +48,13 @@ font-weight: 300; } &.button-container { + font-family: sans-serif; display: flex; justify-content: center; margin-top: 2em; } &.button-container1 { + font-family: sans-serif; display: flex; justify-content: center; margin-top: -1em; diff --git a/src/styles/views/Register.scss b/src/styles/views/Register.scss index e24ac68..3356612 100644 --- a/src/styles/views/Register.scss +++ b/src/styles/views/Register.scss @@ -48,11 +48,13 @@ font-weight: 300; } &.button-container { + font-family: sans-serif; display: flex; justify-content: center; margin-top: 1em; } &.button-container1 { + font-family: sans-serif; display: flex; justify-content: center; margin-top: 0em; diff --git a/src/styles/views/RuleGuide.scss b/src/styles/views/RuleGuide.scss new file mode 100644 index 0000000..fef0fc9 --- /dev/null +++ b/src/styles/views/RuleGuide.scss @@ -0,0 +1,22 @@ +@import "../theme"; +$modal-background: $lightYellow; + +.guide-modal { + background-color: $modal-background; + .guide-modal-arrow { + background-color: $modal-background; + } + button{ + background-color: $darkBlue; + color: $whiteYellow; + border: 1px solid $darkBlue; + &:hover { + background-color: $middleBlue; + color: $whiteYellow; + border: 1px solid $middleBlue; + } + } + .guide-modal-close-icon { + display: none; + } +}