diff --git a/.github/workflows/build_test_before_pr.yml b/.github/workflows/build_test_before_pr.yml index 2bd6fed..3aae2d8 100644 --- a/.github/workflows/build_test_before_pr.yml +++ b/.github/workflows/build_test_before_pr.yml @@ -34,6 +34,3 @@ jobs: node-version: "20.x" - run: npm install - run: npm run build - - - name: Test - run: npm test diff --git a/Contribution.md b/Contribution.md deleted file mode 100644 index f3eed42..0000000 --- a/Contribution.md +++ /dev/null @@ -1,19 +0,0 @@ -# Contribution - -## WEEK1 2024.3.27-2024.4.10 - -| Task Link | Description | Assigned to | Finished? | -| --------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------- | ------------------ | -| https://github.com/sopra-fs24-group-09/sopra-fs24-group-09-client/issues/15 | Design UI components for deleting the local audio file or initiating a re-recording process. | [@Shaochang Tan](https://github.com/petertheprocess) | :white_check_mark: | -| https://github.com/sopra-fs24-group-09/sopra-fs24-group-09-client/issues/14 | Design and implement a user interface with a button to initiate the recording process and another button to stop recording. | [@Shaochang Tan](https://github.com/petertheprocess) | :white_check_mark: | -| https://github.com/sopra-fs24-group-09/sopra-fs24-group-09-client/issues/12 | Ensure that the UI elements for controlling the audio playback are accessible and intuitive for the player to use. | [@Shaochang Tan](https://github.com/petertheprocess) | :white_check_mark: | -| https://github.com/sopra-fs24-group-09/sopra-fs24-group-09-client/issues/11 | Develop functionality to adjust the playback speed of the audio, enabling the player to listen to it at a slower pace. | [@Shaochang Tan](https://github.com/petertheprocess) | :white_check_mark: | -| https://github.com/sopra-fs24-group-09/sopra-fs24-group-09-client/issues/10 | Create play, pause, and speed control buttons to allow the player to control the playback of the reverse audio. | [@Shaochang Tan](https://github.com/petertheprocess) | :white_check_mark: | - -## WEEK2 2024.4.10-2024.4.17 - -| Task Link | Description | Assigned to | Finished? | -| --------- | ----------- | ----------- | --------- | -|https://github.com/sopra-fs24-group-09/sopra-fs24-group-09-client/issues/42|Play the submitted audio from other players| [@Shaochang Tan](https://github.com/petertheprocess) | | -|https://github.com/sopra-fs24-group-09/sopra-fs24-group-09-client/issues/59|Add the recorder widget to the game page| [@Shaochang Tan](https://github.com/petertheprocess) | | -|https://github.com/sopra-fs24-group-09/sopra-fs24-group-09-client/issues/59|Show widget for playing given reversed audio in different playback rate| [@Shaochang Tan](https://github.com/petertheprocess) | | diff --git a/package.json b/package.json index 7eed190..0093c0e 100644 --- a/package.json +++ b/package.json @@ -25,17 +25,11 @@ "build": "react-scripts build", "gcp-build": "", "eject": "react-scripts eject", - "test": "react-scripts test --env=jsdom", "lint": "eslint --fix src/**/*.ts src/**/*.tsx src/**/*.js" }, "eslintConfig": { "extends": "react-app" }, - "jest": { - "moduleNameMapper": { - "^axios$": "axios/dist/node/axios.cjs" - } - }, "engines": { "node": "20.11.0" }, diff --git a/src/App.test.js b/src/App.test.js deleted file mode 100644 index 226fb4e..0000000 --- a/src/App.test.js +++ /dev/null @@ -1,17 +0,0 @@ -import React from "react"; -import {createRoot} from "react-dom/client"; -// import App from "./App"; - -// fake test -it("renders without crashing", () => { - const div = document.createElement("div"); - const root = createRoot(div); // createRoot(div!) if you use TypeScript - // root.render(); - root.unmount(); -}); -// it("renders without crashing", () => { -// const div = document.createElement("div"); -// const root = createRoot(div); // createRoot(div!) if you use TypeScript -// root.render(); -// root.unmount(); -// }); diff --git a/src/components/routing/routeProtectors/LobbyGuard.js b/src/components/routing/routeProtectors/LobbyGuard.js index c241335..ee82652 100644 --- a/src/components/routing/routeProtectors/LobbyGuard.js +++ b/src/components/routing/routeProtectors/LobbyGuard.js @@ -12,14 +12,14 @@ import PropTypes from "prop-types"; * @param props */ export const LobbyGuard = () => { - if (localStorage.getItem("token")) { + if (sessionStorage.getItem("token")) { return ; } // Debugging - return ; - //return ; + // return ; + return ; }; LobbyGuard.propTypes = { diff --git a/src/components/routing/routeProtectors/LoginGuard.js b/src/components/routing/routeProtectors/LoginGuard.js index cc25f38..61e78ca 100644 --- a/src/components/routing/routeProtectors/LoginGuard.js +++ b/src/components/routing/routeProtectors/LoginGuard.js @@ -8,7 +8,7 @@ import PropTypes from "prop-types"; * instead of 'export default' at the end of the file. */ export const LoginGuard = () => { - if (!localStorage.getItem("token")) { + if (!sessionStorage.getItem("token")) { return ; } diff --git a/src/components/routing/routers/AppRouter.js b/src/components/routing/routers/AppRouter.js index 3575eaf..3f1f6dd 100644 --- a/src/components/routing/routers/AppRouter.js +++ b/src/components/routing/routers/AppRouter.js @@ -41,7 +41,7 @@ const AppRouter = () => { {/* no guard for gameroom page now*/} {/* the parameter name should match with the useParam in the Gameroom */} - } /> + } /> }> } /> diff --git a/src/components/ui/AudioRecorder.tsx b/src/components/ui/AudioRecorder.tsx index 367b94d..804fcde 100644 --- a/src/components/ui/AudioRecorder.tsx +++ b/src/components/ui/AudioRecorder.tsx @@ -214,15 +214,18 @@ export const AudioRecorder = React.forwardRef((props,ref) => { console.warn("record-end", blob); // save audio to local storage as encoded base64 string const reader = new FileReader(); - reader.onloadend = () => { + reader.onloadend = async () =>{ const base64data = reader.result as Base64audio; sessionStorage.setItem(cachedName, base64data); - reverseAudioByFFmpegAndCacheIt(base64data); + await reverseAudioByFFmpegAndCacheIt(base64data); compressAudioByFFmpegAndCacheIt(base64data); console.log(`[${props.audioName}]`,"save audio to local storage"); // set isReverse to false setIsReversed(false); sessionStorage.setItem(cachedIsReversedName, "false"); + // pass audio to parent component + props.handleReversedAudioChange && props.handleReversedAudioChange(audioReversedBlobRef.current); + console.log(`[${props.audioName}]`,"reversed audio passing to parent", audioReversedBlobRef.current); }; reader.readAsDataURL(blob); }); @@ -291,7 +294,7 @@ export const AudioRecorder = React.forwardRef((props,ref) => { // load audio, layoutEffect is used to make sure the wavesurfer is initialized before loading audio useLayoutEffect(() => { initializeWaveSurferWithRecorder(); - loadCachedAudio(); + // loadCachedAudio(); return () => { // // clean up wavesurfer @@ -323,15 +326,13 @@ export const AudioRecorder = React.forwardRef((props,ref) => { }; useImperativeHandle(ref, () => ({ - clearAudio: clearAudio + clearAudio: clearAudio, + setVolume: (volume: number) => { + wavesurfer.current?.setVolume(volume); + console.log(`[${props.audioName}]`,"set volume to", volume); + } }),[]); - // pass reversed audio to parent component - useEffect(() => { - props.handleReversedAudioChange && props.handleReversedAudioChange(audioReversedBlobRef.current); - console.log(`[${props.audioName}]`,"reversed audio changed", audioReversedBlobRef.current); - }, [audioReversedBlobRef.current]); - return (
diff --git a/src/components/ui/ButtonPlayer.tsx b/src/components/ui/ButtonPlayer.tsx index dc41d45..64e643a 100644 --- a/src/components/ui/ButtonPlayer.tsx +++ b/src/components/ui/ButtonPlayer.tsx @@ -9,6 +9,7 @@ import "../../styles/ui/ButtonPlayer.scss"; type ButtonPlayerProps = { audioURL: string; className?: string; + volume: number; }; export const ButtonPlayer = (props: ButtonPlayerProps) => { @@ -35,13 +36,15 @@ export const ButtonPlayer = (props: ButtonPlayerProps) => { audio.addEventListener("ended", () => { setIsPlaying(false); }); + audio.volume = props.volume; + console.log(`[ButtonPlayer-${props.className}]`, "audio volume set to", props.volume); return () => { audio.removeEventListener("ended", () => { setIsPlaying(false); }); }; - }, []); + }, [props.volume]); return (
diff --git a/src/components/ui/CounterDown.tsx b/src/components/ui/CounterDown.tsx new file mode 100644 index 0000000..65c5907 --- /dev/null +++ b/src/components/ui/CounterDown.tsx @@ -0,0 +1,42 @@ +import React, { useState, useLayoutEffect } from "react"; + +type CounterDownProps = { + endTimeString: string; +}; + +export const CounterDown: React.FC = ({ endTimeString }) => { + const [timeLeft, setTimeLeft] = useState(); + + useLayoutEffect(() => { + // const now = Date.now(); + // console.log(`[debug][${now}-countdown-useLayoutEffect]endTimeString`, endTimeString); + if (endTimeString === null || endTimeString === "") { + return; + } + const updateCountdown= () => { + const cleanedEndTimeString = endTimeString.replace("[UTC]", "").trim(); + const endTime = new Date(cleanedEndTimeString).getTime(); + const now = Date.now(); + // console.log(`[debug][${now}-countdown-interval]`, endTimeString); + const leftTimeSeconds = Math.max(0, Math.floor((endTime - now) / 1000)); + setTimeLeft(leftTimeSeconds); + } + updateCountdown(); + const intervalId = setInterval(updateCountdown, 1000); + + return () => { + clearInterval(intervalId); + }; + }, [endTimeString]); + + return ( + <> +
+ + {timeLeft} +
+ + ); +}; + +export default CounterDown; \ No newline at end of file diff --git a/src/components/ui/Dropdown.tsx b/src/components/ui/Dropdown.tsx index cb417a7..46358ab 100644 --- a/src/components/ui/Dropdown.tsx +++ b/src/components/ui/Dropdown.tsx @@ -3,15 +3,21 @@ import PropTypes from "prop-types"; import "../../styles/ui/Dropdown.scss"; export const Dropdown = (props) => { + + const handleChange = (event) => { + // Call the passed onChange function with the selected option's value + props.onChange(event.target.value); + }; + return (
{ min="0" max="1" step="0.01" - value={volume} - onChange={e => { - setVolume(e.target.value); - }} + value={props.volume} + // onChange={e => { + // setVolume(e.target.value); + // }} + onChange={props.onChange} />
@@ -42,6 +34,8 @@ export const VolumeBar = props => { VolumeBar.propTypes = { volume: Proptypes.number, + onChange: Proptypes.func, + onClickMute: Proptypes.func, }; diff --git a/src/components/ui/WavePlayer.tsx b/src/components/ui/WavePlayer.tsx index ae530ba..3948225 100644 --- a/src/components/ui/WavePlayer.tsx +++ b/src/components/ui/WavePlayer.tsx @@ -1,10 +1,10 @@ -import React, {useEffect, useState, useRef} from "react"; +import React, {useEffect, useState, useRef, useImperativeHandle } from "react"; import WaveSurfer from "wavesurfer.js"; import propTypes from "prop-types"; import { Button } from "./Button"; import "styles/ui/WavePlayer.scss"; -const WavePlayer = props => { +export const WavePlayer = React.forwardRef((props,ref) => { const waveformRef = useRef(null); const wavesurfer = useRef(null); const [playbackRate, setPlaybackRate] = useState(1); @@ -42,7 +42,6 @@ const WavePlayer = props => { // setIsPlaying(prev => !prev); } ); - }; useEffect(() => { @@ -62,14 +61,21 @@ const WavePlayer = props => { } , [props.audioURL]); + useImperativeHandle(ref, () => ({ + setVolume: (volume:number) => { + wavesurfer.current?.setVolume(volume); + console.log(`[${props.className}]`,"WaveSurfer set volume to", volume); + } + }), []); + return (
-
- ........is recording...... +
+ ........No Audio Uploaded......
+ style={{display: props.audioURL ? "flex":"none"}}>
); -} +}); + +WavePlayer.displayName = "WavePlayer"; WavePlayer.propTypes = { className: propTypes.string, diff --git a/src/components/views/Gameroom.tsx b/src/components/views/Gameroom.tsx index 876b78e..6a916dd 100644 --- a/src/components/views/Gameroom.tsx +++ b/src/components/views/Gameroom.tsx @@ -1,17 +1,16 @@ -import React, { useEffect, useState, useRef, useMemo, useImperativeHandle } from "react"; +import React, { 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"; -import { User } from "types"; import "styles/views/Gameroom.scss"; import "styles/views/Header.scss"; import "styles/twemoji-amazing.css"; import Header from "./Header"; import { FFmpeg } from "@ffmpeg/ffmpeg"; -import { AudioRecorder } from "components/ui/AudioRecorder"; -import WavePlayer from "components/ui/WavePlayer"; -import { ButtonPlayer } from "components/ui/ButtonPlayer"; +import { Roundstatus, RoundstatusProps} from "components/views/GameroomRoundStatus"; +import { PlayerList } from "components/views/GameroomPlayerList"; +import { ValidateAnswerForm } from "components/views/GameroomAnswerForm"; // Stomp related imports import SockJS from "sockjs-client"; import { over } from "stompjs"; @@ -24,39 +23,42 @@ import type { Base64audio, } from "stomp_types"; import { v4 as uuidv4 } from "uuid"; +import { getDomain } from "helpers/getDomain"; // type AudioBlobDict = { [userId: number]: Base64audio }; -type SharedAudioURL = { [userId: number]: string }; +type SharedAudioURL = { [userId: string]: string }; const Gameroom = () => { const navigate = useNavigate(); - const { currentRoomID } = useParams(); // get the room ID from the URL + const { currentRoomID,currentRoomName } = useParams(); // get the room ID from the URL const stompClientRef = useRef(null); /** * Question: why we need this user state here? * if just for saving my id and name, we can make it a const prop */ - const [user, setUser] = useState(); + const user = { + token: sessionStorage.getItem("token"), + id: sessionStorage.getItem("id"), + username: sessionStorage.getItem("username") + }; + console.log(user) const [showReadyPopup, setShowReadyPopup] = useState(false); const [gameOver, setGameOver] = useState(false); + const gameOverRef = useRef(false); const [currentSpeakerID, setCurrentSpeakerID] = useState(null); - const [validateAnswer, setValidateAnswer] = useState(null); const [playerLists, setPlayerLists] = useState([]); - const [gameInfo, setGameInfo] = useState({ - roomID: 5, - currentSpeaker: { - id: 2, - name: "Hanky", - avatar: "grinning-face-with-sweat", - }, - currentAnswer: "Success", - roundStatus: "speak", - currentRoundNum: 2, - }); - const [roomInfo, setRoomInfo] = useState({ - roomID: currentRoomID, - theme: "Advanced", - }); + const roundFinished = useRef(false); + const [endTime, setEndTime] = useState(null); + const gameTheme = useRef("Loading...."); + const leaderboardInfoRecieved = useRef(false); + const [leaderboardInfo, setLeaderboardInfo] = useState([]); + + const [gameInfo, setGameInfo] = useState(null); + const gameInfoRef = useRef(null); + // const [roomInfo, setRoomInfo] = useState({ + // roomID: currentRoomID, + // theme: "Advanced", + // }); const prevStatus = useRef("start"); const [currentStatus, setCurrentStatus] = useState< "speak" | "guess" | "reveal" @@ -67,19 +69,16 @@ const Gameroom = () => { >(null); const myRecordingReversedRef = useRef(null); const roundStatusComponentRef = useRef(null); - // const sharedAudioListRef = useRef({}); // store all shared audio blobs - /** - * Attention!!: Just for testing purposes - * need to pass an audio blob to the WavePlayer like this: - */ - // const [testAudioBlob, setTestAudioBlob] = useState(null); - // const [testAudioURL, setTestAudioURL] = useState(null); // this ref is used to track the current speaker id in callback functions const currentSpeakerIdRef = useRef(); if (gameInfo && gameInfo.currentSpeaker) { - currentSpeakerIdRef.current = gameInfo.currentSpeaker.id; + currentSpeakerIdRef.current = gameInfo.currentSpeaker.userID; } + const [globalVolume, setGlobalVolume] = useState(0.5); + const globalVolumeBeforeMute = useRef(0); + + gameInfoRef.current = gameInfo; // useMemo to initialize and load FFmpeg wasm module // load FFmpeg wasm module @@ -98,7 +97,17 @@ const Gameroom = () => { console.log("GameInfo", gameInfo); + useEffect(() => { + const isChrome = (window as any).chrome; + // console.error("ISCHROME",isChrome); + if (!isChrome) { + alert("Please use Chrome browser to play the game."); + navigate("/lobby"); + + return; + } + // refuse non-chrome browser // define subscription instances let playerInfoSuber; let gameInfoSuber; @@ -107,35 +116,49 @@ const Gameroom = () => { //const roomId = 5; const connectWebSocket = () => { - let Sock = new SockJS(`http://localhost:8080/ws/${currentRoomID}`); + const baseurl = getDomain(); + let Sock = new SockJS(`${baseurl}/ws/${currentRoomID}`); //let Sock = new SockJS('https://sopra-fs23-group-01-server.oa.r.appspot.com/ws'); stompClientRef.current = over(Sock); stompClientRef.current.connect({}, onConnected, onError); }; + console.log(sessionStorage.getItem("id")); const timestamp = new Date().getTime(); // Get current timestamp const onConnected = () => { // subscribe to the topic playerInfoSuber = stompClientRef.current.subscribe( - "/plays/info", + `/plays/info/${currentRoomID}`, onPlayerInfoReceived ); gameInfoSuber = stompClientRef.current.subscribe( - "/games/info", + `/games/info/${currentRoomID}`, onGameInfoReceived ); sharedAudioSuber = stompClientRef.current.subscribe( - "/plays/audio", + `/plays/audio/${currentRoomID}`, onShareAudioReceived ); responseSuber = stompClientRef.current.subscribe( - "/response", + // `/response/${currentRoomID}`, + `/user/${user.id}/response/${currentRoomID}`, onResponseReceived ); + enterRoom(); //connect or reconnect }; + + const onError = (err) => { + console.error("WebSocket Error: ", err); + alert("WebSocket connection error. Check console for details."); + navigate("/lobby"); + }; + const onResponseReceived = (payload) => { + const payloadData = JSON.parse(payload.body); + 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 @@ -146,27 +169,62 @@ const Gameroom = () => { const onPlayerInfoReceived = (payload) => { const payloadData = JSON.parse(payload.body); setPlayerLists(payloadData.message); - //resp success + if (!showReadyPopup && !gameOver){ + const myInfo = payloadData.message.find(item => item.user.id === user.id); + console.log("set info for myself") + console.log(myInfo); + if (myInfo.roundFinished !== null){ + roundFinished.current = myInfo.roundFinished; + console.log("roundFinished?") + console.log(roundFinished.current); + } + } + if (gameOverRef.current === true && leaderboardInfoRecieved.current === false){ + setLeaderboardInfo(payloadData.message); + leaderboardInfoRecieved.current = true; + } }; const onGameInfoReceived = (payload) => { + // const now = new Date().getTime(); + // console.log(`[onGameInfoReceived-${now}] payload: ${payload.body}`); 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 (gameTheme.current !== payloadData.message.theme){ + gameTheme.current = payloadData.message.theme + } + // const diff = now - payloadData.timestamp; + // console.log(`[onGameInfoReceived-${now}] diff: ${diff}`); if (payloadData.message.gameStatus === "ready") { setShowReadyPopup(true); } else if (payloadData.message.gameStatus === "over") { setShowReadyPopup(false); + gameOverRef.current = true; setGameOver(true); } else { setShowReadyPopup(false); } - setCurrentSpeakerID(payloadData.message.currentSpeaker.id); + // if currentSpeaker is not null + if (payloadData.message.currentSpeaker) { + setCurrentSpeakerID(payloadData.message.currentSpeaker.userID); + } + + // console.log("============================="); + // console.log("prevStatus.current", prevStatus.current); + // console.log("payloadData.message.roundStatus", payloadData.message.roundStatus); if ( prevStatus.current === "reveal" && payloadData.message.roundStatus === "speak" ) { //if(payloadData.message.roundStatus === "speak"){ //empty all the audio + console.log("=====clear audio===="); setCurrentSpeakerAudioURL(null); setSharedAudioList([]); roundStatusComponentRef.current?.clearAudio(); @@ -174,6 +232,7 @@ const Gameroom = () => { } prevStatus.current = payloadData.message.roundStatus; //"speak" | "guess" | "reveal" only allowed + setEndTime(payloadData.message.roundDue); setCurrentStatus(payloadData.message.roundStatus); setGameInfo(payloadData.message); }; @@ -233,15 +292,12 @@ const Gameroom = () => { // } // } - const onError = (err) => { - console.error("WebSocket Error: ", err); - alert("WebSocket connection error. Check console for details."); - }; connectWebSocket(); // Cleanup on component unmount return () => { + if (playerInfoSuber) { playerInfoSuber.unsubscribe(); } @@ -259,12 +315,36 @@ const Gameroom = () => { console.log("Disconnected"); }); } + }; }, []); //#region -----------------WebSocket Send Functions----------------- + + // when volume changes, apply the change to all audio players + useEffect(() => { + if(roundStatusComponentRef.current){ + roundStatusComponentRef.current.setVolumeTo(globalVolume); + } + }, [globalVolume]); //debounce-throttle + const enterRoom = () => { + const payload: Timestamped = { + timestamp: new Date().getTime(), + message: { + userID: user.id, + roomID: currentRoomID, + }, + }; + const receiptId = uuidv4(); + stompClientRef.current?.send( + "/app/message/users/enterroom", + { receiptId: receiptId }, + JSON.stringify(payload) + ); + } + //ready const getReady = () => { const payload: Timestamped = { @@ -273,13 +353,13 @@ const Gameroom = () => { timestamp: new Date().getTime(), message: { userID: user.id, - roomID: roomInfo.roomID, + roomID: currentRoomID, }, }; // get a random receipt uuid const receiptId = uuidv4(); stompClientRef.current?.send( - "/users/ready", + "/app/message/users/ready", { receiptId: receiptId }, JSON.stringify(payload) ); @@ -292,12 +372,12 @@ const Gameroom = () => { timestamp: new Date().getTime(), message: { userID: user.id, - roomID: roomInfo.roomID, + roomID: currentRoomID, }, }; const receiptId = uuidv4(); stompClientRef.current?.send( - "/users/unready", + "/app/message/users/unready", { receiptId: receiptId }, JSON.stringify(payload) ); @@ -311,12 +391,12 @@ const Gameroom = () => { timestamp: new Date().getTime(), message: { userID: user.id, - roomID: roomInfo.roomID, + roomID: currentRoomID, }, }; const receiptId = uuidv4(); stompClientRef.current?.send( - "/games/start", + "/app/message/games/start", { receiptId: receiptId }, JSON.stringify(payload) ); @@ -330,15 +410,16 @@ const Gameroom = () => { timestamp: new Date().getTime(), message: { userID: user.id, - roomID: roomInfo.roomID, + roomID: currentRoomID, }, }; const receiptId = uuidv4(); stompClientRef.current?.send( - "/games/exitRoom", + "/app/message/users/exitroom", { receiptId: receiptId }, JSON.stringify(payload) ); + navigate("/lobby") }; //start game @@ -348,15 +429,15 @@ const Gameroom = () => { timestamp: new Date().getTime(), message: { userID: user.id, - roomID: roomInfo.roomID, + roomID: currentRoomID, guess: answer, roundNum: gameInfo.currentRoundNum, - currentSpeakerID: gameInfo.currentSpeaker.id, + currentSpeakerID: gameInfo.currentSpeaker.userID, }, }; const receiptId = uuidv4(); stompClientRef.current?.send( - "/games/validate", + "/app/message/games/validate", { receiptId: receiptId }, JSON.stringify(payload) ); @@ -364,518 +445,108 @@ const Gameroom = () => { //upload audio const uploadAudio = () => { + console.log("[uploadAudio], myRecordingReversedRef.current", myRecordingReversedRef.current); if (!myRecordingReversedRef.current) { console.error("No audio to upload"); - + return; } - const payload: Timestamped = { - timestamp: new Date().getTime(), - message: { - userID: user.id, - audioData: myRecordingReversedRef.current, - }, + // covert the audio blob to base64 string + const reader = new FileReader(); + reader.onload = () => { + const base64data = reader.result as Base64audio; + const payload: Timestamped = { + timestamp: new Date().getTime(), + message: { + userID: user.id, + audioData:base64data, + }, + }; + const receiptId = uuidv4(); + stompClientRef.current.send( + "/app/message/games/audio/upload" /*URL*/, + { receiptId: receiptId }, + JSON.stringify(payload) + ); }; - const receiptId = uuidv4(); - stompClientRef.current?.send( - "/games/audio/upload" /*URL*/, - { receiptId: receiptId }, - JSON.stringify(payload) - ); + reader.readAsDataURL(myRecordingReversedRef.current); }; - //#endregion -----------------WebSocket Send Functions----------------- - const handleAudioReversed = (audioReversed: Base64audio) => { - if (audioReversed) { - myRecordingReversedRef.current = audioReversed; + + //#dendregion -----------------WebSocket Send Functions----------------- + + const handleAudioReversed = useMemo(() => (audio: Blob) => { + if (audio) { + myRecordingReversedRef.current = audio; console.log("[GameRoom]Get reversed audio from AudioRecorder Success"); console.log("Reversed Audio: ", myRecordingReversedRef.current); } - }; - - const togglePopup = () => { - setShowReadyPopup((prevState) => !prevState); - }; - - const toggleStatus = () => { - setCurrentStatus((prevState) => { - switch (prevState) { - case "speak": - return "guess"; - case "guess": - return "reveal"; - case "reveal": - return "speak"; - default: - return "speak"; - } - }); - }; - - // const userRecordings = [ - // { userId: 1, audioURL: null }, - // { userId: 3, audioURL: null }, - // ]; - - const playerReadyStatus = [ - { - user: { - id: 1, - name: "Maxwell", - avatar: "smiling-face-with-smiling-eyes", - }, - score: { - total: 70, - guess: 50, - read: 20, - details: [ - { word: "Lemon", role: 1, score: 20 }, - { word: "Apple", role: 0, score: 30 }, - { word: "Orange", role: 0, score: 20 }, - ], - }, - ready: true, - ifGuess: true, - roundFinished: true, - }, - { - user: { - id: 2, - name: "Hanky", - avatar: "grinning-face-with-sweat", - }, - score: { - total: 30, - guess: 30, - read: 0, - details: [ - { word: "Lemon", role: 0, score: 10 }, - { word: "Apple", role: 1, score: 0 }, - { word: "Orange", role: 0, score: 20 }, - ], - }, - ready: true, - ifGuess: false, - roundFinished: true, - }, - { - user: { - id: 3, - name: "Yang", - avatar: "face-with-monocle", - }, - score: { - total: 50, - guess: 30, - read: 20, - details: [ - { word: "Lemon", role: 0, score: 30 }, - { word: "Apple", role: 0, score: 0 }, - { word: "Orange", role: 1, score: 20 }, - ], - }, - ready: false, - ifGuess: true, - roundFinished: false, - }, - { - user: { - id: 4, - name: "Sophia", - avatar: "grinning-squinting-face", - }, - score: { - total: 60, - guess: 0, - read: 60, - details: [ - { word: "Lemon", role: 0, score: 30 }, - { word: "Apple", role: 0, score: 10 }, - { word: "Orange", role: 0, score: 20 }, - ], - }, - ready: true, - ifGuess: true, - roundFinished: false, - }, - ]; - - let mePlayer = { - id: 3, - name: "Hanky", - avatar: "grinning-face-with-sweat", - }; - - let mockgameInfo = { - roomID: 5, - currentSpeaker: { - id: 2, - name: "Hanky", - avatar: "grinning-face-with-sweat", - }, - currentAnswer: "Success", - roundStatus: "speak", - currentRoundNum: 2, - }; - - const changeSpeaker = () => { - setShowReadyPopup((prevState) => !prevState); - }; + }, []); - const Roundstatus = React.forwardRef((props,ref) => { - const { gameInfo, currentSpeakerAudioURL } = props; - console.log("gameInfo", gameInfo); - const _audioRecorderRef = useRef(null); - useImperativeHandle(ref, () => ({ - clearAudio: () => { - console.log("----clear audio"); - _audioRecorderRef.current?.clearAudio(); - } - }), []); + console.log("[Gameroom]the player list is") + console.log(playerLists); + const LeaderBoard = ({ playerStatus }) => { + // console.log("123456") + console.log("[LeaderBoard]",playerStatus) + return ( <> -
-
- - 50 -
-
-
- {/*{playerInfo.user.name}*/} - - - - - {currentSpeakerID === mePlayer.id && - currentStatus === "speak" && ( - <> -
-
- - {"Round " + gameInfo.currentRoundNum + " "} - - - {gameInfo.currentSpeaker.name + ", please"} - - - {" record:"} - -
- - {gameInfo.currentAnswer} - -
- - )} - {currentSpeakerID !== mePlayer.id && - currentStatus === "speak" && ( - <> -
-
- - {"Round " + gameInfo.currentRoundNum + " "} - - - {gameInfo.currentSpeaker.name + "'s'"} - - - {"turn to record"} - -
- {/*{gameInfo.currentAnswer}*/} -
- - )} - {currentSpeakerID !== mePlayer.id && - currentStatus === "guess" && ( - <> -
-
- - {gameInfo.currentSpeaker.name + "'s revesed audio:"} - -
- -
- - )} - {currentSpeakerID === mePlayer.id && - currentStatus === "guess" && ( - <> -
-
- - {"Your revesed audio:"} - -
- +
+ {playerStatus.map((playerInfo, index) => ( +
+ {index + 1} + + -
- - )} - {currentStatus === "reveal" && ( - <> -
-
- - {"The word " + - gameInfo.currentSpeaker.name + - " recorded is "} - - - {" "} - {gameInfo.currentAnswer} - -
- -
- - )} -
-
-
- {gameInfo.currentSpeaker.id === mePlayer.id && - currentStatus === "speak" && ( - - {"Try to read and record the word steadily and loudly!"} - - )} - {gameInfo.currentSpeaker.id !== mePlayer.id && - currentStatus === "speak" && ( - - { - "Please wait until the speak player finishes recording and uploading!" - } - - )} - {gameInfo.currentSpeaker.id !== mePlayer.id && - currentStatus === "guess" && ( - - { - "Try to simulate the reversed audio and reverse again to figure out the word!" - } - - )} - {gameInfo.currentSpeaker.id === mePlayer.id && - currentStatus === "guess" && ( - - { - "You can try to simulate the reversed audio or listen to others' audio!" - } - - )} - {currentStatus === "reveal" && ( - - {"Time is up and now reveals the answer!"} - - )} - -
-
- - ); - }); - Roundstatus.displayName = "Roundstatus"; - - Roundstatus.propTypes = { - gameInfo: PropTypes.shape({ - roomID: PropTypes.number.isRequired, - currentSpeaker: PropTypes.shape({ - id: PropTypes.number.isRequired, - name: PropTypes.string.isRequired, - avatar: PropTypes.string.isRequired, - }).isRequired, - currentAnswer: PropTypes.string.isRequired, - roundStatus: PropTypes.string.isRequired, - currentRoundNum: PropTypes.number.isRequired, - }).isRequired, - currentSpeakerAudioURL: PropTypes.string, - }; - - const LeaderBoard = ({ playerStatus }) => { - return ( -
-
- {playerStatus.map((playerInfo, index) => ( -
- {index + 1} - - - - - {playerInfo.user.name} - - - {playerInfo.score.total} - - + + + {playerInfo.user.name} + + + {playerInfo.score.total} + + Total - - - {playerInfo.score.guess} - - + + + {playerInfo.score.guess} + + Guess - - - {playerInfo.score.read} - - + + + {playerInfo.score.read} + + Read - - {playerInfo.score.details.map((detail, detailIndex) => ( - - - {detail.score} + {playerInfo.score.details.map((detail, detailIndex) => ( + + + {detail.score} + - - {detail.word} - - + + {detail.word} + + + ))} +
))}
- ))} -
-
- ); - }; - - const PlayerList = ({ playerStatus, sharedAudioList }) => { - return ( - <> -
-
ROOM
-
- {"#" + roomInfo.roomID + "-" + roomInfo.theme}
-
-
- {/*map begin*/} - {playerStatus.map((playerInfo, index) => { - // const hasRecording = sharedAudioList.some( - // (recording) => recording.userId === playerInfo.user.id - // ); - const hasRecording = playerInfo.user.id in sharedAudioList; - let _audioURL = null; - if (hasRecording) { - _audioURL = sharedAudioList[playerInfo.user.id]; - } - - return ( -
- - - - {!showReadyPopup && ( - <> -
- - {playerInfo.user.name} - - - Score: - - {playerInfo.score.total} - - {playerInfo.ifGuess ? ( - - ) : ( - - )} - -
-
- {playerInfo.roundFinished ? ( - - ) : ( - - )} - {hasRecording && } -
- - )} - {showReadyPopup && ( - <> -
- - {playerInfo.user.name} - - -
-
- {playerInfo.ready ? ( - - ) : ( - - )} -
- - )} -
- ); - })} -
+ )} ); }; @@ -906,119 +577,161 @@ const Gameroom = () => { ).isRequired, }; - PlayerList.propTypes = { - playerStatus: PropTypes.arrayOf( - PropTypes.shape({ - user: PropTypes.shape({ - id: PropTypes.number.isRequired, - name: PropTypes.string.isRequired, - avatar: PropTypes.string.isRequired, - }).isRequired, - score: PropTypes.shape({ - total: PropTypes.number.isRequired, - guess: PropTypes.number.isRequired, - read: PropTypes.number.isRequired, - details: PropTypes.arrayOf( - PropTypes.shape({ - word: PropTypes.string.isRequired, - role: PropTypes.number.isRequired, - score: PropTypes.number.isRequired, - }) - ).isRequired, - }).isRequired, - ready: PropTypes.bool.isRequired, - ifGuess: PropTypes.bool.isRequired, - }) - ).isRequired, - sharedAudioList: PropTypes.arrayOf( - PropTypes.shape({ - userId: PropTypes.number.isRequired, - audioURL: PropTypes.string, - }) - ).isRequired, - }; + + if (playerLists === null) { + return
Loading...
; + } 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={globalVolume} + /> {!gameOver && showReadyPopup && (
- Room#05 - Advanced + {"Room#" + currentRoomName} + {gameTheme.current} {" "} Ready to start the game?
-
getReady()} - onKeyDown={() => getReady()} - > - Confirm -
-
cancelReady()} - onKeyDown={() => cancelReady()} - > - Cancel -
+ {gameInfo.roomOwner.id === user.id &&( + <> +
startGame()} + //onKeyDown={() => getReady()} + > + Start +
+
exitRoom()} + > + Quit +
+ + )} + {gameInfo.roomOwner.id !== user.id &&( + <> +
getReady()} + onKeyDown={() => getReady()} + > + Confirm +
+
cancelReady()} + onKeyDown={() => cancelReady()} + > + Cancel +
+ + )}
)} {gameOver && ( - + )} {!gameOver && !showReadyPopup && ( )}
- {!gameOver && + { gameInfo !== null && + !gameOver && !showReadyPopup && - gameInfo.currentSpeaker.id !== mePlayer.id && + gameInfo.currentSpeaker.userID !== user.id && currentStatus === "guess" && (
- setValidateAnswer(e.target.value)} - className="gameroom validateForm" - type="text" - placeholder="Validate your answer..." + -
)} - - - - +
+ {showReadyPopup === true &&( + // {showReadyPopup === true && user.id !== gameInfo.roomOwner.id &&( +
{ + console.log("leave room"); + exitRoom(); + } + }>leave
+ )} + {gameOver === true &&( +
{ + console.log("leave room after over"); + exitRoom(); + // navigate("/lobby"); + } + }>leave
+ )} + {currentSpeakerID === user.id && + currentStatus === "speak" && ( + + )} + {currentSpeakerID !== user.id && + currentStatus === "guess" && ( +
{ + console.log("upload audio"); + uploadAudio(); + } + }>share your audio
+ )} +
+
diff --git a/src/components/views/GameroomAnswerForm.tsx b/src/components/views/GameroomAnswerForm.tsx new file mode 100644 index 0000000..150b82a --- /dev/null +++ b/src/components/views/GameroomAnswerForm.tsx @@ -0,0 +1,37 @@ + +import React, { useState } from "react"; + +type ValidateAnswerFormProps = { + submitAnswer: (answer: string) => void; + roundFinished: boolean; +}; + +export const ValidateAnswerForm = ({ + submitAnswer, + roundFinished, +}: ValidateAnswerFormProps) => { + const [validateAnswer, setValidateAnswer] = useState(""); + + const handleInputChange = (e: React.ChangeEvent) => { + setValidateAnswer(e.target.value); + }; + + return ( +
+ + +
+ ); +}; diff --git a/src/components/views/GameroomPlayerList.tsx b/src/components/views/GameroomPlayerList.tsx new file mode 100644 index 0000000..68ec1a3 --- /dev/null +++ b/src/components/views/GameroomPlayerList.tsx @@ -0,0 +1,137 @@ +import React from "react"; +import { ButtonPlayer } from "components/ui/ButtonPlayer"; + +type PlayerListProps = { + playerStatus: { + user: { + id: string; + name: string; + avatar: string; + }; + score: { + total: number; + }; + ifGuess: boolean; + roundFinished: boolean; + ready: boolean; + }[]; + sharedAudioList: { + [userId: string]: string; + }; + gameTheme: string; + currentRoomName: string; + showReadyPopup: boolean; + gameOver: boolean; + globalVolume: number; +} + +export const PlayerList = (props:PlayerListProps) => { + const { playerStatus, sharedAudioList, gameTheme, currentRoomName, showReadyPopup, gameOver, globalVolume } = props; + + return ( +
+
+
ROOM
+
+ {"#" + currentRoomName + "-" + gameTheme} +
+
+
+ {/*map begin*/} + {playerStatus.map((playerInfo, index) => { + // const hasRecording = sharedAudioList.some( + // (recording) => recording.userId === playerInfo.user.id + // ); + const hasRecording = playerInfo.user.id in sharedAudioList; + let _audioURL = null; + if (hasRecording) { + _audioURL = sharedAudioList[playerInfo.user.id]; + } + + return ( +
+ + + + {!showReadyPopup && !gameOver && ( + <> +
+ + {playerInfo.user.name} + + + Score: + + {playerInfo.score.total} + + {playerInfo.ifGuess ? ( + + ) : ( + + )} + +
+
+ {playerInfo.roundFinished ? ( + + ) : ( + + )} + {hasRecording && } +
+ + )} + {gameOver && ( + <> +
+ + {playerInfo.user.name} + + + Score: + + {playerInfo.score.total} + + +
+ + )} + {showReadyPopup && ( + <> +
+ + {playerInfo.user.name} + + +
+
+ {playerInfo.ready ? ( + + ) : ( + + )} +
+ + )} +
+ ); + })} +
+
+ ); +}; diff --git a/src/components/views/GameroomRoundStatus.tsx b/src/components/views/GameroomRoundStatus.tsx new file mode 100644 index 0000000..b87d944 --- /dev/null +++ b/src/components/views/GameroomRoundStatus.tsx @@ -0,0 +1,213 @@ +import React, { useRef, useImperativeHandle } from "react"; +import { CounterDown } from "components/ui/CounterDown"; +import { WavePlayer } from "components/ui/WavePlayer"; +import { AudioRecorder } from "components/ui/AudioRecorder"; + +export type RoundstatusProps = { + gameInfo: { + roomID: string; + currentSpeaker: { + userID: string; + username: string; + avatar: string; + }; + currentAnswer: string; + roundStatus: string; + currentRoundNum: number; + }; + currentSpeakerAudioURL: string; + endTime: string; + ffmpegObj: object; + meId: string; + globalVolume: number; + handleAudioReversed: (audio:Blob) => void; +}; + +export const Roundstatus : React.ForwardRefRenderFunction = React.forwardRef((props:RoundstatusProps,ref) => { + const { gameInfo, currentSpeakerAudioURL,endTime,meId,ffmpegObj,globalVolume,handleAudioReversed } = props; + console.log("gameInfo", gameInfo); + const _audioRecorderRef = useRef(null); + const _wavePlayerRef = useRef(null); + useImperativeHandle(ref, () => ({ + clearAudio: () => { + console.log("----clear audio"); + _audioRecorderRef.current?.clearAudio(); + }, + setVolumeTo: (volume) => { + console.log("----set volume to", volume); + _audioRecorderRef.current?.setVolume(volume); + _wavePlayerRef.current?.setVolume(volume); + } + }), []); + + if (!gameInfo) { + return
loading...
; + } + + return ( + <> +
+ +
+
+ {/*{playerInfo.user.name}*/} + + + + + {gameInfo.currentSpeaker.userID === meId && + gameInfo.roundStatus === "speak" && ( + <> +
+
+ + {"Round " + gameInfo.currentRoundNum + " "} + + + {gameInfo.currentSpeaker.username + ", please"} + + + {" record:"} + +
+ + {gameInfo.currentAnswer} + +
+ + )} + {gameInfo.currentSpeaker.userID !== meId && + gameInfo.roundStatus === "speak" && ( + <> +
+
+ + {"Round " + gameInfo.currentRoundNum + " "} + + + {gameInfo.currentSpeaker.username + "'s'"} + + + {"turn to record"} + +
+ {/*{gameInfo.currentAnswer}*/} +
+ + )} + {gameInfo.roundStatus === "guess" && ( + <> +
+
+ + {gameInfo.currentSpeaker.userID === meId ? + "Your revesed audio:" : + gameInfo.currentSpeaker.username + "'s revesed audio:"} + +
+ +
+ + )} + {gameInfo.roundStatus === "reveal" && ( + <> +
+
+ + {"The word " + + gameInfo.currentSpeaker.username + + " recorded is "} + + + {" "} + {gameInfo.currentAnswer} + +
+ {/**/} +
+ + )} +
+
+
+ {gameInfo.currentSpeaker.userID === meId && + gameInfo.roundStatus === "speak" && ( + + {"Try to read and record the word steadily and loudly!"} + + )} + {gameInfo.currentSpeaker.userID !== meId && + gameInfo.roundStatus === "speak" && ( + + { + "Please wait until the speak player finishes recording and uploading!" + } + + )} + {gameInfo.currentSpeaker.userID !== meId && + gameInfo.roundStatus === "guess" && ( + + { + "Try to simulate the reversed audio and reverse again to figure out the word!" + } + + )} + {gameInfo.currentSpeaker.userID === meId && + gameInfo.roundStatus === "guess" && ( + + { + "You can try to simulate the reversed audio or listen to others' audio!" + } + + )} + {gameInfo.roundStatus === "reveal" && ( + + {"Time is up and now reveals the answer!"} + + )} + +
+
+ + ); +}); +Roundstatus.displayName = "Roundstatus"; diff --git a/src/components/views/Header.tsx b/src/components/views/Header.tsx index 938b580..0bd1573 100644 --- a/src/components/views/Header.tsx +++ b/src/components/views/Header.tsx @@ -1,6 +1,6 @@ import React from "react"; -import {ReactLogo} from "../ui/ReactLogo"; import PropTypes from "prop-types"; +import { VolumeBar } from "../ui/VolumeBar"; import "../../styles/views/Header.scss"; /** @@ -13,12 +13,18 @@ import "../../styles/views/Header.scss"; */ const Header = props => (
-

state bar to be implemented

+
); Header.propTypes = { left: PropTypes.string, + onChange: PropTypes.func, + onClickMute: PropTypes.func, + volume: PropTypes.number, }; /** diff --git a/src/components/views/Lobby.tsx b/src/components/views/Lobby.tsx index a8bb941..b192d00 100644 --- a/src/components/views/Lobby.tsx +++ b/src/components/views/Lobby.tsx @@ -1,8 +1,7 @@ import React, { useRef, useEffect, useState } from "react"; import { api, handleError } from "helpers/api"; -import { Spinner } from "components/ui/Spinner"; import { Button } from "components/ui/Button"; -import { useNavigate } from "react-router-dom"; +import { useLocation, useNavigate } from "react-router-dom"; import BaseContainer from "components/ui/BaseContainer"; import PropTypes from "prop-types"; import { User, Room } from "types"; @@ -87,11 +86,11 @@ Player.propTypes = { const mockRoomPlayers: User[] = [ - { id: 1, username: "Alice", avatar: "grinning-face-with-sweat", name: "Alice Wonderland", status: "ONLINE", registerDate: new Date("2021-08-01"), birthday: new Date("1990-01-01") }, - { id: 2, username: "Bob", avatar: "grinning-face-with-sweat", name: "Bob Builder", status: "OFFLINE", registerDate: new Date("2021-09-01"), birthday: new Date("1985-02-02") }, - { id: 3, username: "Han", avatar: "grinning-face-with-sweat", name: "Alice Wonderland", status: "ONLINE", registerDate: new Date("2021-08-01"), birthday: new Date("1990-01-01") }, - { id: 4, username: "Li", avatar: "grinning-face-with-sweat", name: "Bob Builder", status: "OFFLINE", registerDate: new Date("2021-09-01"), birthday: new Date("1985-02-02") }, - { id: 5, username: "Liuz", avatar: "grinning-face-with-sweat", name: "Bob Builder", status: "OFFLINE", registerDate: new Date("2021-09-01"), birthday: new Date("1985-02-02") }, + { id: "1", username: "Alice", avatar: "grinning-face-with-sweat", name: "Alice Wonderland", status: "ONLINE", registerDate: new Date("2021-08-01"), birthday: new Date("1990-01-01") }, + { id: "2", username: "Bob", avatar: "grinning-face-with-sweat", name: "Bob Builder", status: "OFFLINE", registerDate: new Date("2021-09-01"), birthday: new Date("1985-02-02") }, + { id: "3", username: "Han", avatar: "grinning-face-with-sweat", name: "Alice Wonderland", status: "ONLINE", registerDate: new Date("2021-08-01"), birthday: new Date("1990-01-01") }, + { id: "4", username: "Li", avatar: "grinning-face-with-sweat", name: "Bob Builder", status: "OFFLINE", registerDate: new Date("2021-09-01"), birthday: new Date("1985-02-02") }, + { id: "5", username: "Liuz", avatar: "grinning-face-with-sweat", name: "Bob Builder", status: "OFFLINE", registerDate: new Date("2021-09-01"), birthday: new Date("1985-02-02") }, ]; @@ -131,14 +130,16 @@ const Lobby = () => { const profilePopRef = useRef(null); const changeAvatarPopRef = useRef(null); const infoPopRef = useRef(null); - const [rooms, setRooms] = useState(mockRooms); - const [user, setUser] = useState(mockRoomPlayers[0]); + const [rooms, setRooms] = useState([]); + const [user, setUser] = useState([]); const [username, setUsername] = useState(null); const [avatar, setAvatar] = useState(null); const [roomName, setRoomName] = useState(""); - const [numRounds, setNumRounds] = useState(0); - const [roomTheme, setRoomTheme] = useState("food"); - + const [numRounds, setNumRounds] = useState(2); + const [roomTheme, setRoomTheme] = useState(""); + // const needReloadRooms = useRef(false); + // const RELOAD_TIME = 3000; + const logout = async () => { const id = sessionStorage.getItem("id"); sessionStorage.removeItem("token"); @@ -147,92 +148,144 @@ const Lobby = () => { const requestBody = JSON.stringify({ id: id }); const response = await api.post("/users/logout", requestBody); console.log(response); + sessionStorage.clear(); } catch (error) { alert(`Something went wrong during the logout: \n${handleError(error)}`); } navigate("/login"); }; + async function fetchData() { + // try { + // 获取所有房间信息 + const roomsResponse = await api.get("/games/lobby"); + console.log("Rooms data:", roomsResponse.data); + + // 使用 Promise.all 来并发获取每个房间的用户详细信息 + const roomsWithPlayerDetails = await Promise.all(roomsResponse.data.map(async (room) => { + // 对每个房间的用户 ID 列表并发请求用户信息 + const playerDetails = await Promise.all(room.roomPlayersList.map(async (userId) => { + const userResponse = await api.get(`/users/${userId}`); + + return userResponse.data; // 返回用户的详细信息 + })); + + // needReloadRooms.current = false; + + // setTimeout(() => { + // needReloadRooms.current = true; + // }, RELOAD_TIME); + + return { + ...room, + roomPlayersList: playerDetails // 替换房间中的用户 ID 列表为用户详细信息 + }; + })); - useEffect(() => { - async function fetchData() { + // 更新房间状态,包含了用户的详细信息 + setRooms(roomsWithPlayerDetails); + + console.log("request to:", roomsResponse.request.responseURL); + console.log("status code:", roomsResponse.status); + console.log("status text:", roomsResponse.statusText); + console.log("requested data:", roomsResponse.data); + + // See here to get more data. + console.log(roomsResponse); + + // Get user ID from sessionStorage + const userId = sessionStorage.getItem("id"); + if (userId) { + // Get current user's information try { - //get all rooms - const response = await api.get("/games/lobby"); - await new Promise((resolve) => setTimeout(resolve, 1000)); - setRooms(response.data); - - console.log("request to:", response.request.responseURL); - console.log("status code:", response.status); - console.log("status text:", response.statusText); - console.log("requested data:", response.data); - - // See here to get more data. - console.log(response); - - // Get user ID from sessionStorage - const userId = sessionStorage.getItem("id"); - if (userId) { - // Get current user's information - const userResponse = await api.get(`/users/${userId}`); - setUser(userResponse.data); // Set user data from API - console.log("User data:", userResponse.data); - } else { - console.log("No user ID found in sessionStorage."); - } + const userResponse = await api.get(`/users/${userId}`); + setUser(userResponse.data); // Set user data from API + console.log("User data:", userResponse.data); } catch (error) { - console.error( - `Something went wrong while fetching the users: \n${handleError( - error - )}` - ); - console.error("Details:", error); - alert( - "Something went wrong while fetching the users! See the console for details." - ); + handleError(error); + + return; } + } else { + console.error("User ID not found in sessionStorage!"); } + // } catch (error) { + // console.error( + // `Something went wrong while fetching the users: \n${handleError( + // error + // )}` + // ); + // console.error("Details:", error); + // alert( + // "Something went wrong while fetching the users! See the console for details." + // ); + // } + } + useEffect(() => { fetchData().catch(error => { - console.error("Unhandled error in fetchData:", error); + handleError(error); }); }, []); + // when user get navigated back to this page, fetch data again + const location = useLocation(); + // console.warn("Location:", location); + useEffect(() => { + // wait for 1 second before fetching data + const timeoutId = setTimeout(() => { + console.log("========fetchData========"); + fetchData().catch(error => { + handleError(error); + }); + }, 500); + + return () => { + clearTimeout(timeoutId); + } + }, [location]); + const doEdit = async () => { try { - const requestBody = JSON.stringify({ username, avatar: avatar }); + const requestBody = JSON.stringify({ username: username, avatar: avatar }); const id = sessionStorage.getItem("id"); console.log("Request body:", requestBody); await api.put(`/users/${id}`, requestBody); + updateUsername(username); toggleProfilePop(); } catch (error) { - if (error.response && error.response.data) { - alert(error.response.data.message); - } else { - console.error("Error:", error.message); - alert("An unexpected error occurred."); - } + handleError(error); + + return; } }; const createRoom = async () => { - try { + // if not chrome, alert the user + if (!navigator.userAgent.includes("Chrome")) { + alert("Your browser is currently not supported, please use Chrome to play this game!"); + return; + } + try { + console.log("Current theme:", roomTheme); const ownerId = sessionStorage.getItem("id"); // 假设ownerId存储在sessionStorage中 const requestBody = JSON.stringify({ - name: roomName, - num: numRounds, + roomName: roomName, + maxPlayersNum: numRounds, roomOwnerId: ownerId, - theme: "FOOD" + theme: roomTheme }); console.log(requestBody) const response = await api.post("/games", requestBody); console.log("Room created successfully:", response); - const roomId = response.roomId; - navigate(`/room=${roomId}`); + console.log("Room ID:", response.data.roomId); + const roomId = response.data.roomId; + navigate(`/rooms/${roomId}/${roomName}`); //toggleRoomCreationPop(); // 关闭创建房间的弹窗 } catch (error) { - console.error("Error creating room:", handleError(error)); - alert(`Error creating room: ${handleError(error)}`); + handleError(error); + + return; } }; @@ -260,10 +313,12 @@ const Lobby = () => { async function enterRoom(roomId, userId) { try { - const requestBody = JSON.stringify({ userId, roomId }); - await api.put("/games", requestBody); + const requestBody = JSON.stringify({ id: userId }); + await api.put(`/games/${roomId}`, requestBody); } catch (error) { - console.error(`Something went wrong during the enterRoom: \n${handleError(error)}`); + handleError(error); + + return; } } @@ -296,22 +351,16 @@ const Lobby = () => { setAvatar(newAvatar); // 构造请求体,只包含 avatar 更改 - const requestBody = JSON.stringify({ username, avatar: newAvatar }); + const requestBody = JSON.stringify({ avatar: newAvatar }); const id = sessionStorage.getItem("id"); console.log("Request body:", requestBody); - // 执行更新请求 await api.put(`/users/${id}`, requestBody); - - // 可能需要关闭弹窗或执行其他 UI 反馈 - console.log("Avatar changed successfully"); + updateAvatar(newAvatar); } catch (error) { - if (error.response && error.response.data) { - alert(error.response.data.message); - } else { - console.error("Error:", error.message); - alert("An unexpected error occurred."); - } + handleError(error); + + return; } } @@ -322,25 +371,68 @@ const Lobby = () => { })); }; - const userinfo = () => { - return; + const updateUsername = (newUsername) => { + setUser(prevUser => ({ + ...prevUser, // 复制 prevUser 对象的所有现有属性 + username: newUsername // 更新 avatar 属性 + })); }; + /// + /// if error is network error, clear the session and navigate to login page + /// + const handleError = (error) => { + if(error.message.match(/Network Error/)) { + console.error(`The server cannot be reached.\nDid you start it?\n${error}`); + alert(`The server cannot be reached.\nDid you start it?\n${error}`); + sessionStorage.clear(); + navigate("/login"); + } else { + console.error(`Something went wrong: \n${error}`); + alert(`Something went wrong: \n${error}`); + } + } + const renderRoomLists = () => { return rooms.map((Room) => ( -
{ +
{ e.preventDefault(); + + /// when user click the room, fetch the data again + /// and check if the room is still in the list + try { + await fetchData(); + } catch (error) { + handleError(error); + + return; + } + // check if roomId is still in the list + const room = rooms.find(r => r.roomId === Room.roomId); + if (!room) { + alert("The room's info is outdated, please try again!"); + + return; + } + const currentId = sessionStorage.getItem("id"); - // const isPlayerInRoom = Room.roomPlayersList.join().includes(currentId); + const isPlayerInRoom = Room.roomPlayersList.join().includes(currentId); enterRoom(Room.roomId, currentId) .then(() => { - navigate(`/rooms/${Room.roomId}`); + //alert(currentId); + if(Room.roomPlayersList.length===Room.maxPlayersNum) + alert("Room is Full, please enter another room!"); + else if(Room.status==="In Game") + alert("Game is already started, please enter another room!"); + else + navigate(`/rooms/${Room.roomId}/${Room.roomName}`); }) .catch(error => { - console.error(`Something went wrong during the enterRoom: \n${handleError(error)}`); - alert(`Something went wrong during the enterRoom: \n${handleError(error)}`); + console.error(`Something went wrong during the enterRoom: \n${error}`); + alert(`Something went wrong during the enterRoom: \n${error}`); }); + }}>
{Room.roomPlayersList?.map((user, index) => ( @@ -351,7 +443,7 @@ const Lobby = () => { ))}
- ROOM #{Room.roomId} +
{Room.roomName}
{Room.theme}
{

Rooms

{renderRoomLists()} -
- -
+
+
+ +
- - -
Create Room
- - - -
- - -
-
-
-
{ toggleAvatarPop(); - // toggleProfilePop(); + toggleProfilePop(); }}>
@@ -430,16 +505,16 @@ const Lobby = () => { setUsername(e)} + onChange={e => setUsername(e.target.value)} />
-
Name: {user.name}
+
Id: {user.id}
Status: {user.status}
-
RegisterDate: {user && new Date(user.registerDate).toLocaleDateString()}
-
Birthday: {user && new Date(user.birthday).toLocaleDateString()}
+ {/*
RegisterDate: {user && new Date(user.registerDate).toLocaleDateString()}
*/}
+
- -
Here is some Guidelines....
-
+ +
+

Welcome to KAEPS!

+

Here are some guides for playing this game:

+
    +
  • Speaker: Receives a word, records it, inverts the audio, and sends it to other players.
  • +
  • Challenger: Listens to the inverted audio and tries 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.
  • +
+

Join a room or create one to play with friends!

+
+
diff --git a/src/components/views/Profile.tsx b/src/components/views/Profile.tsx index bbd2161..141464d 100644 --- a/src/components/views/Profile.tsx +++ b/src/components/views/Profile.tsx @@ -10,7 +10,7 @@ import { User } from "types"; const Profile = () => { const navigate = useNavigate(); const { id } = useParams<{ id: string }>(); - const localId = parseInt(sessionStorage.getItem("id") ?? "0", 10); + const localId = sessionStorage.getItem("id"); const [user, setUser] = useState(""); useEffect(() => { diff --git a/src/stomp_types.ts b/src/stomp_types.ts index bd87a24..dab19ab 100644 --- a/src/stomp_types.ts +++ b/src/stomp_types.ts @@ -8,21 +8,21 @@ export type Timestamped = { } export type PlayerAudio = { - userID: number; + userID: string; audioData: Base64audio; } export type PlayerAndRoomID = { - userID: number; - roomID: number; + userID: string; + roomID: string; } export type AnswerGuess = { - userID: number; - roomID: number; + userID: string; + roomID: string; guess: string; roundNum: number; - currentSpeakerID: number; + currentSpeakerID: string; } export type StompResponse = { diff --git a/src/styles/ui/VolumeBar.scss b/src/styles/ui/VolumeBar.scss index 7e07a88..2e5ea9f 100644 --- a/src/styles/ui/VolumeBar.scss +++ b/src/styles/ui/VolumeBar.scss @@ -13,8 +13,11 @@ $backgroundColor: $darkBlue; width: fit-content; // only apply padding in horizontal direction padding: 2px 10px; - cursor: pointer; + // cursor: pointer; transition: background-color 0.3s ease; + input { + cursor: pointer; + } &.toggle-mute{ height: 20px; align-items: center; diff --git a/src/styles/ui/WavePlayer.scss b/src/styles/ui/WavePlayer.scss index b54e676..bee5170 100644 --- a/src/styles/ui/WavePlayer.scss +++ b/src/styles/ui/WavePlayer.scss @@ -23,7 +23,7 @@ } .no-audio-placeholder { position: absolute; - left: 1.5rem; + left: 2.5rem; font-size: 1.5rem; animation: pulse 10s infinite; color: $classicYellow; diff --git a/src/styles/views/Gameroom.scss b/src/styles/views/Gameroom.scss index 5fb0178..48d681e 100644 --- a/src/styles/views/Gameroom.scss +++ b/src/styles/views/Gameroom.scss @@ -21,13 +21,21 @@ flex-direction: row; justify-content: space-between; overflow: auto; + .header.container{ + padding-left: 10%; + } + } + &.left-area{ + display: flex; + height: 100%; + flex-direction: column; } &.right-area{ display: flex; flex-direction: column; justify-content: space-between; align-items: center; - margin-top: 5vh; + // margin-top: 5vh; flex-grow: 1; // height: 100%; .inputarea { @@ -79,9 +87,10 @@ &.playercontainer { // position: relative; // top:5vh; - margin-top: 5vh; + // margin-top: 5vh; left: 0; - width: 28vw; + // width: 28vw; + width: 100%; min-width: min-content; // height: 100%; background: $classicYellow; @@ -91,15 +100,17 @@ align-items: flex-start; /* Align items to the start (left) */ justify-content: flex-start; /* Align content to the start (left) */ box-shadow: $dropShadow; + flex-grow: 1; gap: 1rem; } &.roominfocontainer { z-index:999; - position: fixed; + // position: fixed; + position: sticky; left:0; top:0; height: 5vh; - width: 28vw; + width: 100%; display: flex; flex-wrap: nowrap; align-items: center; @@ -116,8 +127,7 @@ font-family: 'Inter'; font-style: normal; font-weight: bold; - font-size: 1.6rem - ; + font-size: 1.6rem; color: $darkBlue; } &.singlePlayerContainer { @@ -288,10 +298,12 @@ .readybutton { background-color: $classicYellow; + border:0; } .cancelbutton { background-color: rgba(129, 129, 129, 1); + border:0; } .readybutton:hover { @@ -411,7 +423,7 @@ border-radius: 22px; background-color: $whiteYellow; display: grid; - grid-template-columns: repeat(auto-fill, minmax(4vw, 1fr)); + grid-template-columns: minmax(5vw, 5vw) minmax(5vw, 5vw) minmax(5vw, 5vw) minmax(5vw, 5vw) repeat(auto-fit, minmax(6vw, 6vw)); gap: 0.5rem; } &.ldPlayerAvatar { @@ -543,6 +555,8 @@ font-weight: 700; font-size: 0.8rem; color:lighten($darkBlue,20%); + max-width: 100%; + word-wrap: break-word; } &.currentAnswer { position: relative; @@ -555,7 +569,7 @@ .validateForm { background-color: $whiteYellow; border-radius: 5%; - width: 200%; + width: 80%; padding:0 0.6rem; padding-right: 2rem; border: 1px solid $classicYellow; @@ -566,7 +580,7 @@ margin-left: 5%; border-radius: 15%; border: 0px; - width: 40%; + width: 20%; display: flex; align-items: center; justify-content: center; diff --git a/src/styles/views/Header.scss b/src/styles/views/Header.scss index 1cc845a..79dfb1d 100644 --- a/src/styles/views/Header.scss +++ b/src/styles/views/Header.scss @@ -2,11 +2,11 @@ .header { &.container { - position: fixed; + position: sticky; top:0; left: 0; background-color: $greyBlue; - width: 100vw; + width: 100%; height: 5vh; display: flex; align-content: center; diff --git a/src/styles/views/Lobby.scss b/src/styles/views/Lobby.scss index d2fffbb..252a785 100644 --- a/src/styles/views/Lobby.scss +++ b/src/styles/views/Lobby.scss @@ -61,9 +61,9 @@ justify-content: center; height: auto; width: 100%; - bottom: 3%; + bottom: 1.5%; left: 0%; - margin-top: 2%; + margin-top: 30px; flex-grow: 1; .create-room-btn{ bottom: 0%; @@ -83,6 +83,25 @@ // make the text bold font-weight: bolder; } + .reload-room-btn{ + margin-left: 20px; + bottom: 0%; + min-width: fit-content; + width: 20%; + min-height: 50px; + display: flex; + justify-items: center; + align-items: center; + justify-content: center; + align-content: center; + font-size: 1.3em; + background-color: $darkBlue; + color: white; + // dont capitalize the text + text-transform: none; + // make the text bold + font-weight: bolder; + } } } &.user-list { @@ -109,11 +128,13 @@ margin-inline: 5%; justify-content: space-between; flex-direction: row; + text-align: center; } .room { background-color: #fff; margin-bottom: 10px; border-radius: 8px; + text-align: center; //overflow: hidden; .room-header{ @@ -124,6 +145,7 @@ align-items: center; // Center the items horizontally justify-content: center; // Center the items vertically padding: 10px; + text-align: center; } @@ -148,6 +170,7 @@ flex-direction: column; /* 堆叠子元素 */ align-items: center; /* 水平居中 */ justify-content: center; /* 垂直居中 */ + text-align: center; padding: 10px; // Align the room details to the end of the flex container } @@ -279,7 +302,7 @@ position: absolute; // 使用绝对定位 left: 15%; // 定位到zuo边 top: 25%; // 从顶部留下10%的空间以居中显示 - font-size: 20rem; /* 设定容器的字体大小 */ + font-size: 15vw; /* 设定容器的字体大小 */ } .big-title { @@ -305,13 +328,36 @@ //padding: 0.25rem; /* 小间距增加点击区域 */ cursor: pointer; } +.intro-popup{ + height:55%; + //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; +} .profile-popup{ - height:50%; + height:70%; &.content{ display: flex; flex-direction: column; flex-grow: 1; - font-size: 2em; + font-size: 1.5em; .title{ font-size: 1.5em; margin-bottom: 20px; @@ -374,7 +420,7 @@ } .room-creation-popup{ - height: 50%; + height: 80%; &.content{ display: flex; flex-direction: column; @@ -388,7 +434,7 @@ } input { height: 100px; - width: 800px; + width: 600px; padding-left: 1px; margin-left: 4px; border: 1px solid black; @@ -403,9 +449,10 @@ display: flex; align-items: center; justify-items: center; - width: 800px; + width: 600px; height: 100px; - margin-bottom: 20px; + margin-top: 20px; + margin-bottom: 40px; select{ appearance: none; -webkit-appearance: none; diff --git a/src/types.ts b/src/types.ts index 4ccbe4e..1c8d1df 100644 --- a/src/types.ts +++ b/src/types.ts @@ -3,7 +3,7 @@ export type User = { username: string; avatar: string; name: string; - id: number; + id: string; registerDate: Date; birthday: Date; }; @@ -20,4 +20,5 @@ export type Room = { playToOuted: boolean | null; }; -export type Base64audio = `data:audio/${string};base64,${string}`; +// export type Base64audio = `data:audio/${string};base64,${string}`; +export type Base64audio = string;