diff --git a/MobileApp/README.md b/MobileApp/README.md deleted file mode 100644 index e0e3f8e0..00000000 --- a/MobileApp/README.md +++ /dev/null @@ -1,6 +0,0 @@ -# QuizMaster Mobile Application - -List of directories - -- `backend` - contains the backend source codes -- `frontend` - contains the frontend source codes diff --git a/MobileApp/backend/remove_me_if_this_folder_is_filled b/MobileApp/backend/remove_me_if_this_folder_is_filled deleted file mode 100644 index e69de29b..00000000 diff --git a/MobileApp/frontend/remove_me_if_this_folder_is_filled b/MobileApp/frontend/remove_me_if_this_folder_is_filled deleted file mode 100644 index e69de29b..00000000 diff --git a/WebApp/.env b/WebApp/.env index 694bb0dd..85696433 100644 --- a/WebApp/.env +++ b/WebApp/.env @@ -17,6 +17,11 @@ OVERRIDE_QUESTION_TIMER_TYPEANSWER=25 OVERRIDE_QUESTION_TIMER_MULTIPLECHOICE=20 OVERRIDE_QUESTION_TIMER_TRUEORFALSE=15 BUFFER_TIME=3 +OVERRIDE_POINT_EASY_ROUND=1 +OVERRIDE_POINT_AVERAGE_ROUND=3 +OVERRIDE_POINT_DIFFICULT_ROUND=5 +OVERRIDE_POINT_CLINCHER_ROUND=5 +OVERRIDE_POINT_GENERAL=1 # QUIZMASTER-ADMIN diff --git a/WebApp/backend/QuizMaster.API.Gatewway/Configuration/QuizSettings.cs b/WebApp/backend/QuizMaster.API.Gatewway/Configuration/QuizSettings.cs index 7e534f38..fc739f1e 100644 --- a/WebApp/backend/QuizMaster.API.Gatewway/Configuration/QuizSettings.cs +++ b/WebApp/backend/QuizMaster.API.Gatewway/Configuration/QuizSettings.cs @@ -7,6 +7,7 @@ public class QuizSettings public int BufferTime { get; set; } = 0; public OverrideQuestionTimer OverrideQuestionTimer { get; set;} = new(); + public OverridePointSystem OverridePointSystem { get; set; } = new(); } public class OverrideQuestionTimer @@ -15,4 +16,14 @@ public class OverrideQuestionTimer public int MultipleChoice { get; set; } = 0; public int TrueOrFalse { get; set; } = 0; } + + + public class OverridePointSystem + { + public int Easy { get; set; } = 1; + public int Average { get; set; } = 3; + public int Difficult { get; set; } = 5; + public int Clincher { get; set; } = 5; + public int GeneralPoints { get; set; } = 1; + } } diff --git a/WebApp/backend/QuizMaster.API.Gatewway/Models/Report/ParticipantAnswerReport.cs b/WebApp/backend/QuizMaster.API.Gatewway/Models/Report/ParticipantAnswerReport.cs index b4f3a7e2..553d6c6e 100644 --- a/WebApp/backend/QuizMaster.API.Gatewway/Models/Report/ParticipantAnswerReport.cs +++ b/WebApp/backend/QuizMaster.API.Gatewway/Models/Report/ParticipantAnswerReport.cs @@ -11,6 +11,8 @@ public class ParticipantAnswerReport public string ParticipantName { get; set; } = string.Empty; // can be username public string Answer { get; set; } = string.Empty; public int QuestionId { get; set; } + public int Points { get; set; } = 0; + public int Score { get; set; } = 0; public string ScreenshotLink { get; set; } = string.Empty; } } diff --git a/WebApp/backend/QuizMaster.API.Gatewway/Services/QuizHandler.cs b/WebApp/backend/QuizMaster.API.Gatewway/Services/QuizHandler.cs index 79a08960..4553d2cd 100644 --- a/WebApp/backend/QuizMaster.API.Gatewway/Services/QuizHandler.cs +++ b/WebApp/backend/QuizMaster.API.Gatewway/Services/QuizHandler.cs @@ -72,6 +72,7 @@ public async Task StartQuiz(SessionHub hub, SessionHandler handler, QuizRoomServ bool ForcedToExit = false; foreach (var Qset in quizSets) { + ForcedToExit = handler.IsRoomForcedToExit(roomId); if (ForcedToExit) break; // Get the Sets var setRequest = new SetRequest() { Id = Qset.QSetId }; @@ -132,6 +133,7 @@ public async Task StartQuiz(SessionHub hub, SessionHandler handler, QuizRoomServ * - CurrentQuestionName * - TotalNumberOfQuestions * - bufferTime + * - points * - ParticipantsInRoom */ await hub.Clients.Group(roomPin).SendAsync("metadata", new { @@ -142,6 +144,8 @@ public async Task StartQuiz(SessionHub hub, SessionHandler handler, QuizRoomServ currentQuestionName = details.question.QStatement, totalNumberOfQuestions = Setquestions.Count, bufferTime = quizSettings.BufferTime, + points = quizSettings.OverridePointSystem, + currentDifficulty = details.question.QDifficulty.QDifficultyDesc.ToLower(), participantsInRoom = handler.GetParticipantLinkedConnectionsInAGroup(roomPin).Count(), }); @@ -178,6 +182,9 @@ public async Task StartQuiz(SessionHub hub, SessionHandler handler, QuizRoomServ int limit = quizSettings.ForceNextRoundTimeout; // DEFAULT (300s) of 5mins, if not clicked `proceed` then continue while (handler.GetPausedRoom(roomId) && limit > 0) { + ForcedToExit = handler.IsRoomForcedToExit(roomId); + // force break loop if triggered 'end game' + if (ForcedToExit) break; // Adding delay so it doesn't kill the backend when looping infinitely await Task.Delay(1000); // Notify the admin while the game is paused diff --git a/WebApp/backend/QuizMaster.API.Gatewway/Services/SessionHandler.cs b/WebApp/backend/QuizMaster.API.Gatewway/Services/SessionHandler.cs index 8f23b0cc..dcdcc32b 100644 --- a/WebApp/backend/QuizMaster.API.Gatewway/Services/SessionHandler.cs +++ b/WebApp/backend/QuizMaster.API.Gatewway/Services/SessionHandler.cs @@ -1,18 +1,22 @@ using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.SignalR; +using Microsoft.Extensions.Options; using Microsoft.Extensions.Primitives; using Newtonsoft.Json; using QuizMaster.API.Authentication.Models; using QuizMaster.API.Authentication.Proto; +using QuizMaster.API.Gateway.Configuration; using QuizMaster.API.Gateway.Hubs; using QuizMaster.API.Gateway.Models.Report; using QuizMaster.API.Gateway.Services.ReportService; using QuizMaster.API.QuizSession.Models; using QuizMaster.API.QuizSession.Protos; +using QuizMaster.Library.Common.Entities.Questionnaire; using QuizMaster.Library.Common.Entities.Rooms; using QuizMaster.Library.Common.Models.QuizSession; using System.IdentityModel.Tokens.Jwt; using System.Net.Http; +using System.Runtime.Intrinsics.X86; namespace QuizMaster.API.Gateway.Services { @@ -32,8 +36,9 @@ public class SessionHandler private readonly ReportServiceHandler ReportHandler; private Dictionary RoomNextSetPaused; private List RoomForceExitIds; + private QuizSettings QuizSettings; - public SessionHandler(ReportServiceHandler reportServiceHandler) + public SessionHandler(ReportServiceHandler reportServiceHandler, IOptions options) { connectionGroupPair = new(); participantLinkedConnectionId = new(); @@ -48,6 +53,7 @@ public SessionHandler(ReportServiceHandler reportServiceHandler) RoomNextSetPaused = new(); ReportHandler = reportServiceHandler; RoomForceExitIds = new(); + QuizSettings = options.Value; } public string GenerateSessionId(string roomPin) @@ -374,6 +380,21 @@ public async Task SubmitAnswer(QuizRoomService.QuizRoomServiceClient grp } } + // Get the question difficulty description + int Point = QuizSettings.OverridePointSystem.GeneralPoints; + QuestionDifficulty diff = questionData.question.QDifficulty; + if(diff != null || diff != default) + { + if (diff.QDifficultyDesc.ToLower().Contains("easy")) + Point = QuizSettings.OverridePointSystem.Easy; + else if (diff.QDifficultyDesc.ToLower().Contains("average")) + Point = QuizSettings.OverridePointSystem.Average; + else if (diff.QDifficultyDesc.ToLower().Contains("difficult")) + Point = QuizSettings.OverridePointSystem.Difficult; + else if (diff.QDifficultyDesc.ToLower().Contains("clincher")) + Point = QuizSettings.OverridePointSystem.Clincher; + } + QuizParticipant? participantData; // get the participant data participantData = GetLinkedParticipantInConnectionId(connectionId); @@ -382,7 +403,7 @@ public async Task SubmitAnswer(QuizRoomService.QuizRoomServiceClient grp if (correct) { // increment score by 1 - participantData.Score += 1; + participantData.Score += Point; } // Hold Submission of Answer HoldClientAnswerSubmission(connectionId); @@ -393,7 +414,9 @@ public async Task SubmitAnswer(QuizRoomService.QuizRoomServiceClient grp ParticipantName = participantData.QParticipantDesc, Answer = answer, QuestionId = questionData.question.Id, - ScreenshotLink = "" + ScreenshotLink = "", + Points = Point, + Score = correct ? Point:0 }); #endregion return "Answer submitted"; diff --git a/WebApp/backend/QuizMaster.API.Gatewway/appsettings.json b/WebApp/backend/QuizMaster.API.Gatewway/appsettings.json index 98fe99f5..4f3546b3 100644 --- a/WebApp/backend/QuizMaster.API.Gatewway/appsettings.json +++ b/WebApp/backend/QuizMaster.API.Gatewway/appsettings.json @@ -31,11 +31,18 @@ "QuizSettings": { "ShowAnswerAfterQuestionDelay": 10, "ForceNextRoundTimeout": 120, - "BufferTime": 3, + "BufferTime": 3, "OverrideQuestionTimer": { "TypeAnswer": 0, "MultipleChoice": 0, "TrueOrFalse": 0 + }, + "OverridePointSystem": { + "Easy": 1, + "Average": 3, + "Difficult": 5, + "Clincher": 5, + "GeneralPoints": 1 } } diff --git a/WebApp/backend/QuizMaster.API.Quiz/Models/QuestionDto.cs b/WebApp/backend/QuizMaster.API.Quiz/Models/QuestionDto.cs index 15b06e8a..d29b505f 100644 --- a/WebApp/backend/QuizMaster.API.Quiz/Models/QuestionDto.cs +++ b/WebApp/backend/QuizMaster.API.Quiz/Models/QuestionDto.cs @@ -16,8 +16,9 @@ public class QuestionDto public int QTime { get; set; } public int QDifficultyId { get; set; } + public QuestionDifficulty QDifficulty { get; set; } = default!; - public int QCategoryId { get; set; } + public int QCategoryId { get; set; } public int QTypeId { get; set; } public IEnumerable Details { get; set; } diff --git a/WebApp/backend/QuizMaster.API.Quiz/Services/Repositories/QuizRepository.cs b/WebApp/backend/QuizMaster.API.Quiz/Services/Repositories/QuizRepository.cs index d55568a4..39afef5b 100644 --- a/WebApp/backend/QuizMaster.API.Quiz/Services/Repositories/QuizRepository.cs +++ b/WebApp/backend/QuizMaster.API.Quiz/Services/Repositories/QuizRepository.cs @@ -114,7 +114,10 @@ public async Task> GetAllQuestionsAsync(QuestionResourcePara qDetail.DetailTypes = _context.QuestionDetailTypes.Where(qDetailType => qDetailType.QuestionDetailId == qDetail.Id).Select((qDetailType) => qDetailType.DetailType).ToList(); }); - } + + var diff = _context.Difficulties.Where(d => d.Id == question.QDifficultyId).FirstOrDefault(); + if(null != diff) question.QDifficulty = diff; + } return question; diff --git a/WebApp/backend/QuizMaster.API.QuizSession/Services/Grpc/QuizRoomServices.cs b/WebApp/backend/QuizMaster.API.QuizSession/Services/Grpc/QuizRoomServices.cs index 0a1dfcc8..f4946870 100644 --- a/WebApp/backend/QuizMaster.API.QuizSession/Services/Grpc/QuizRoomServices.cs +++ b/WebApp/backend/QuizMaster.API.QuizSession/Services/Grpc/QuizRoomServices.cs @@ -402,7 +402,7 @@ public override async Task GetQuestion(SetRequest request, ServerC var reply = new RoomResponse(); var id = request.Id; - var question = _context.Questions.FirstOrDefault(x => x.Id == id); + var question = _context.Questions.Include(q => q.QDifficulty).FirstOrDefault(x => x.Id == id); //_ = _context.DetailTypes.ToList(); //var details = _context.QuestionDetails.Where(x => x.QuestionId == question.Id).Include(qD => qD.DetailTypes).ToList(); var details = _context.QuestionDetails.Where(x => x.QuestionId == question.Id).Include(qD => qD.DetailTypes).ToList(); @@ -418,6 +418,9 @@ public override async Task GetQuestion(SetRequest request, ServerC return await Task.FromResult(reply); } + // include the qDifficulty + question.QDifficulty = _context.Difficulties.Where(d => d.Id == question.QDifficultyId).First(); + reply.Code = 200; reply.Data = JsonConvert.SerializeObject(new QuestionsDTO { question=question, details=details}); diff --git a/WebApp/backend/QuizMaster.API.QuizSession/Services/Grpc/QuizSetServices.cs b/WebApp/backend/QuizMaster.API.QuizSession/Services/Grpc/QuizSetServices.cs index 8d8736ad..aa702084 100644 --- a/WebApp/backend/QuizMaster.API.QuizSession/Services/Grpc/QuizSetServices.cs +++ b/WebApp/backend/QuizMaster.API.QuizSession/Services/Grpc/QuizSetServices.cs @@ -101,9 +101,9 @@ public override async Task AddQuizSet(QuizSetRequest request, Se } catch (Exception ex) { - reply.Code = 500; - reply.Message = ex.Message; - return await Task.FromResult(reply); + //reply.Code = 500; + //reply.Message = ex.Message; + //return await Task.FromResult(reply); } reply.Code = 200; @@ -249,9 +249,9 @@ public override async Task DeleteQuizSet(GetQuizSetRequest reque } catch (Exception ex) { - reply.Code = 500; - reply.Message = ex.Message; - return await Task.FromResult(reply); + //reply.Code = 500; + //reply.Message = ex.Message; + //return await Task.FromResult(reply); } reply.Code = 200; @@ -367,9 +367,9 @@ public override async Task UpdateQuizSet(QuizSetRequest request, } catch (Exception ex) { - reply.Code = 500; - reply.Message = ex.Message; - return await Task.FromResult(reply); + //reply.Code = 500; + //reply.Message = ex.Message; + //return await Task.FromResult(reply); } reply.Code = 200; diff --git a/WebApp/docker-compose-no-frontend.yml b/WebApp/docker-compose-no-frontend.yml index f431fcb5..51479b6f 100644 --- a/WebApp/docker-compose-no-frontend.yml +++ b/WebApp/docker-compose-no-frontend.yml @@ -89,14 +89,17 @@ services: - GrpcServerConfiguration:Session_Service=https://backend_api_quizsession:6006 - Kestrel:EndpointDefaults:Protocols=Http1AndHttp2 - Kestrel:Endpoints:Https:Url=https://*:443 - # - Kestrel:Certificates:Default:Path=/app/localhost_cert.pfx - # - Kestrel:Certificates:Default:Password=123456 - AppSettings:CORS_ORIGINS=${GATEWAY_CORS} - QuizSettings:ShowAnswerAfterQuestionDelay=${SHOW_ANSWER_AFTER_QUESTION_DELAY} - QuizSettings:ForceNextRoundTimeout=${FORCE_NEXT_ROUND_TIMEOUT} - QuizSettings:OverrideQuestionTimer:TypeAnswer=${OVERRIDE_QUESTION_TIMER_TYPEANSWER} - QuizSettings:OverrideQuestionTimer:MultipleChoice=${OVERRIDE_QUESTION_TIMER_MULTIPLECHOICE} - QuizSettings:OverrideQuestionTimer:TrueOrFalse=${OVERRIDE_QUESTION_TIMER_TRUEORFALSE} + - QuizSettings:OverridePointSystem:Easy=${OVERRIDE_POINT_EASY_ROUND} + - QuizSettings:OverridePointSystem:Average=${OVERRIDE_POINT_AVERAGE_ROUND} + - QuizSettings:OverridePointSystem:Difficult=${OVERRIDE_POINT_DIFFICULT_ROUND} + - QuizSettings:OverridePointSystem:Clincher=${OVERRIDE_POINT_CLINCHER_ROUND} + - QuizSettings:OverridePointSystem:GeneralPoints=${OVERRIDE_POINT_GENERAL} networks: - gateway diff --git a/WebApp/docker-compose.yml b/WebApp/docker-compose.yml index 26950f07..71679279 100644 --- a/WebApp/docker-compose.yml +++ b/WebApp/docker-compose.yml @@ -89,8 +89,6 @@ services: - GrpcServerConfiguration:Session_Service=https://backend_api_quizsession:6006 - Kestrel:EndpointDefaults:Protocols=Http1AndHttp2 - Kestrel:Endpoints:Https:Url=https://*:443 - # - Kestrel:Certificates:Default:Path=/app/localhost_cert.pfx - # - Kestrel:Certificates:Default:Password=123456 - AppSettings:CORS_ORIGINS=${GATEWAY_CORS} - QuizSettings:ShowAnswerAfterQuestionDelay=${SHOW_ANSWER_AFTER_QUESTION_DELAY} - QuizSettings:ForceNextRoundTimeout=${FORCE_NEXT_ROUND_TIMEOUT} @@ -98,6 +96,11 @@ services: - QuizSettings:OverrideQuestionTimer:TypeAnswer=${OVERRIDE_QUESTION_TIMER_TYPEANSWER} - QuizSettings:OverrideQuestionTimer:MultipleChoice=${OVERRIDE_QUESTION_TIMER_MULTIPLECHOICE} - QuizSettings:OverrideQuestionTimer:TrueOrFalse=${OVERRIDE_QUESTION_TIMER_TRUEORFALSE} + - QuizSettings:OverridePointSystem:Easy=${OVERRIDE_POINT_EASY_ROUND} + - QuizSettings:OverridePointSystem:Average=${OVERRIDE_POINT_AVERAGE_ROUND} + - QuizSettings:OverridePointSystem:Difficult=${OVERRIDE_POINT_DIFFICULT_ROUND} + - QuizSettings:OverridePointSystem:Clincher=${OVERRIDE_POINT_CLINCHER_ROUND} + - QuizSettings:OverridePointSystem:GeneralPoints=${OVERRIDE_POINT_GENERAL} networks: - gateway diff --git a/WebApp/frontend/quiz-master/app/dashboard/game.tsx b/WebApp/frontend/quiz-master/app/dashboard/game.tsx index 432260c2..a0848185 100644 --- a/WebApp/frontend/quiz-master/app/dashboard/game.tsx +++ b/WebApp/frontend/quiz-master/app/dashboard/game.tsx @@ -2,10 +2,12 @@ import { useSession } from "next-auth/react"; import CryptoJS from "crypto-js"; import { QUIZMASTER_SESSION_WEBSITE } from "@/api/api-routes"; +import jwtDecode from "jwt-decode"; export default function Game() { const { data: session } = useSession(); - const user = session?.user; + let user = session?.user; + let username; function encodeUTF8(input: string) { return encodeURIComponent(input); @@ -14,6 +16,14 @@ export default function Game() { // Get the data to be encrypted (e.g., from localStorage) const token = localStorage.getItem("token") || ""; + // Get username from token + if (token) { + const data: any = jwtDecode(token); + const userData = JSON.parse(data.token); + console.log(userData); + username = userData["UserData"]["UserName"]; + } + // Encode the data as UTF-8 const utf8EncodedToken = encodeUTF8(token); @@ -25,6 +35,6 @@ export default function Game() { ).toString(); const encodedEncryptedToken = encodeURIComponent(encryptedToken); - const linkVar = `${QUIZMASTER_SESSION_WEBSITE}?name=${user?.username}&token=${encodedEncryptedToken}`; + const linkVar = `${QUIZMASTER_SESSION_WEBSITE}?name=${username}&token=${encodedEncryptedToken}`; return linkVar; } diff --git a/WebApp/frontend/quiz-master/lib/correctAnswerUtils.tsx b/WebApp/frontend/quiz-master/lib/correctAnswerUtils.tsx index 618703bb..58d53b3c 100644 --- a/WebApp/frontend/quiz-master/lib/correctAnswerUtils.tsx +++ b/WebApp/frontend/quiz-master/lib/correctAnswerUtils.tsx @@ -24,4 +24,4 @@ export function isCorrectAnswer( return false; } } -} \ No newline at end of file +} diff --git a/WebApp/frontend/quiz_session/.env b/WebApp/frontend/quiz_session/.env index 1132ff2e..2d20ad92 100644 --- a/WebApp/frontend/quiz_session/.env +++ b/WebApp/frontend/quiz_session/.env @@ -1,2 +1,3 @@ NEXT_PUBLIC_QUIZMASTER_ADMIN=http://localhost:3000 NEXT_PUBLIC_QUIZMASTER_GATEWAY=https://localhost:7081 +NEXT_PUBLIC_QUIZMASTER_TRIGGER_SFX_SECONDS=5 diff --git a/WebApp/frontend/quiz_session/app/components/welcome.js b/WebApp/frontend/quiz_session/app/components/welcome.js index fd37b15e..cfb33bbd 100644 --- a/WebApp/frontend/quiz_session/app/components/welcome.js +++ b/WebApp/frontend/quiz_session/app/components/welcome.js @@ -12,7 +12,7 @@ import { useLeaderboard, useMetaData, useAnswer, - useAnsweredParticipants + useAnsweredParticipants, } from "../util/store"; import { notifications } from "@mantine/notifications"; import { useSearchParams } from "next/navigation"; @@ -93,13 +93,13 @@ export default function Welcome() { // answer connection.on("answer", (answer) => { - setAnswer(answer) - }) + setAnswer(answer); + }); // answered Participants connection.on("participant_answered", (participant_answered) => { setAnsweredParticipants(participant_answered ?? []); - }) + }); const token = params.get("token"); const username = params.get("name"); @@ -114,6 +114,8 @@ export default function Welcome() { const decryptedToken = decrypToken.toString(CryptoJS.enc.Utf8); connection.invoke("Login", decryptedToken); + + localStorage.clear(); localStorage.setItem("username", username.toLowerCase()); localStorage.setItem("token", decryptedToken); @@ -129,16 +131,20 @@ export default function Welcome() { if (loggedIn) { // Verify Auth Token const token = localStorage.getItem("token"); - fetch(BASE_URL+"/gateway/api/auth/info", {method:'GET', headers: {"Authorization": `Bearer ${token}`}}) - .then(r => r.json().then(d => ({status: r.status, body: d}))) - .then(d =>{ - if(d.status === 200){ - push("/auth/code"); - }else{ - push("/auth"); - } + fetch(BASE_URL + "/gateway/api/auth/info", { + method: "GET", + headers: { Authorization: `Bearer ${token}` }, }) - + .then((r) => + r.json().then((d) => ({ status: r.status, body: d })) + ) + .then((d) => { + if (d.status === 200) { + push("/auth/code"); + } else { + push("/auth"); + } + }); } else { push("/auth"); } diff --git a/WebApp/frontend/quiz_session/app/room/components/chat.js b/WebApp/frontend/quiz_session/app/room/components/chat.js index 54ae2335..dd04fae1 100644 --- a/WebApp/frontend/quiz_session/app/room/components/chat.js +++ b/WebApp/frontend/quiz_session/app/room/components/chat.js @@ -13,7 +13,7 @@ export default function Chat({ onToggleCollapseChat }) { return (
-
+
{ setConverstaion((prev) => [...prev, chat]); }, [chat]); + useEffect(() => { + if (lastMessageRef.current) { + lastMessageRef.current.scrollIntoView({ behavior: "smooth" }); + } + console.log("Conversations", conversation); + }, [conversation]); + return ( <> {conversation?.map((message, index) => { @@ -21,8 +30,11 @@ export default function Chats() {
-
{message?.name}
+
+ {`${message?.name} ${message?.isAdmin ? "(Host)" : ""}`} +
-
{message?.message}
+
+
{message?.message}
); } else { return ( -
-
{message?.name}
+
+
+
+ {`${message?.name} ${message?.isAdmin ? "(Host)" : ""}`} +
+
{message?.message} diff --git a/WebApp/frontend/quiz_session/app/room/components/roomPin.js b/WebApp/frontend/quiz_session/app/room/components/roomPin.js index 913b3231..e5ad51ea 100644 --- a/WebApp/frontend/quiz_session/app/room/components/roomPin.js +++ b/WebApp/frontend/quiz_session/app/room/components/roomPin.js @@ -14,19 +14,28 @@ export default function RoomPin() { // Save Room Information const SaveRoomInfo = () => { const _Data = localStorage.getItem("_rI"); - if(!_Data){ + if (!_Data) { const token = localStorage.getItem("token"); - fetch(BASE_URL+`/gateway/api/room/getRoomByPin/${params.get("roomPin")}`, { - headers:{"Content-Type":"application/json", "Authorization": `Bearer ${token}`}, - credentials: "include" - }).then(r => r.json()) - .then(d => { - const room = JSON.stringify(d.data); - localStorage.setItem("_rI", room); - - }).catch(e => {console.error("Failed to save room information: ", e)}) + fetch( + BASE_URL + `/gateway/api/room/getRoomByPin/${params.get("roomPin")}`, + { + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${token}`, + }, + credentials: "include", + } + ) + .then((r) => r.json()) + .then((d) => { + const room = JSON.stringify(d.data); + localStorage.setItem("_rI", room); + }) + .catch((e) => { + console.error("Failed to save room information: ", e); + }); } - } + }; SaveRoomInfo(); return (
diff --git a/WebApp/frontend/quiz_session/app/room/components/start.js b/WebApp/frontend/quiz_session/app/room/components/start.js index b1366ced..71af395e 100644 --- a/WebApp/frontend/quiz_session/app/room/components/start.js +++ b/WebApp/frontend/quiz_session/app/room/components/start.js @@ -37,7 +37,7 @@ export default function Start() {
10 ? " bg-green-700/50" : "bg-red-500" + className={`flex flex-col rounded-md px-4 py-2 w-28 justify-center items-center ${ + time > SFX_TRIGGER_SECONDS ? " bg-green-700/50" : "bg-red-500" }`} >
Time left
0 ? "animate-ping" : "" + time <= SFX_TRIGGER_SECONDS && time > 0 ? "animate-ping" : "" }`} > {timeFormater(time)} @@ -83,6 +86,7 @@ export default function Header() {
+
diff --git a/WebApp/frontend/quiz_session/app/room/quiz/components/interval.js b/WebApp/frontend/quiz_session/app/room/quiz/components/interval.js index 2fcbc07d..a8f34d3b 100644 --- a/WebApp/frontend/quiz_session/app/room/quiz/components/interval.js +++ b/WebApp/frontend/quiz_session/app/room/quiz/components/interval.js @@ -6,45 +6,60 @@ import { useEffect, useState } from "react"; import { BASE_URL } from "@/app/util/api"; import useUserTokenData from "@/app/util/useUserTokenData"; import { useMetaData, useQuestion } from "@/app/util/store"; +import { AnimateSlideInFromBottom } from "./AnimateSlideInFromBottom"; +import ConfirmEndGameModal from "../modals/ConfirmEndGameModal"; -export default function Interval({ leaderBoard, handleCloseLeaderboard}) { +export default function Interval({ leaderBoard, handleCloseLeaderboard }) { const { isAdmin } = useUserTokenData(); const { question } = useQuestion(); const { metadata } = useMetaData(); - + const [endGameModal, setEndGameModal] = useState(false); const handleNextRound = () => { - if(handleCloseLeaderboard){ + if (handleCloseLeaderboard) { const roomInformationJson = localStorage.getItem("_rI"); const roomInformation = JSON.parse(roomInformationJson); - if(roomInformation && roomInformationJson){ - const token = localStorage.getItem("token") - fetch(BASE_URL+`/gateway/api/room/proceed/${roomInformation.id}`, {headers:{"Authorization": `Bearer ${token}`}}, {credentials: "include"}).catch(e => {alert("Error: ",e)}) - handleCloseLeaderboard() + if (roomInformation && roomInformationJson) { + const token = localStorage.getItem("token"); + fetch( + BASE_URL + `/gateway/api/room/proceed/${roomInformation.id}`, + { headers: { Authorization: `Bearer ${token}` } }, + { credentials: "include" } + ).catch((e) => { + alert("Error: ", e); + }); + handleCloseLeaderboard(); } - } - } + }; const handleForceExit = () => { + const input = prompt( + "Are you sure you want to end game? Yes or No", + "No" + ).toLocaleLowerCase(); - const input = prompt("Are you sure you want to end game? Yes or No", "No").toLocaleLowerCase(); - - if(input === "yes" || input === "y"){ + if (input === "yes" || input === "y") { const roomInformationJson = localStorage.getItem("_rI"); const roomInformation = JSON.parse(roomInformationJson); - - if(roomInformation && roomInformationJson){ - const token = localStorage.getItem("token") - fetch(BASE_URL+`/gateway/api/room/forceExit/${roomInformation.id}`, {headers:{"Authorization": `Bearer ${token}`}}, {credentials: "include"}).catch(e => {alert("Error: ",e)}) - handleCloseLeaderboard() + + if (roomInformation && roomInformationJson) { + const token = localStorage.getItem("token"); + fetch( + BASE_URL + `/gateway/api/room/forceExit/${roomInformation.id}`, + { headers: { Authorization: `Bearer ${token}` } }, + { credentials: "include" } + ).catch((e) => { + alert("Error: ", e); + }); + handleCloseLeaderboard(); } - + // Always call handleNextRound(); } - } + }; useEffect(() => { // const timer = setInterval(() => { // setSeconds((prevSeconds) => prevSeconds - 1); @@ -56,14 +71,14 @@ export default function Interval({ leaderBoard, handleCloseLeaderboard}) { // }, 1000); // return () => clearInterval(timer); - if(question || metadata){ - if(question.question){ - if(question.remainingTime !== 0){ - handleCloseLeaderboard() + if (question || metadata) { + if (question.question) { + if (question.remainingTime !== 0) { + handleCloseLeaderboard(); } } } - }, [/*seconds,*/ question ,metadata, handleCloseLeaderboard]); + }, [/*seconds,*/ question, metadata, handleCloseLeaderboard]); return ( <>
@@ -75,13 +90,27 @@ export default function Interval({ leaderBoard, handleCloseLeaderboard}) { > {seconds}
*/} + setEndGameModal(false)} + onConfirm={handleForceExit} + />
- {isAdmin && - ( -
- - + {isAdmin && ( +
+ +
)} diff --git a/WebApp/frontend/quiz_session/app/room/quiz/components/progress.js b/WebApp/frontend/quiz_session/app/room/quiz/components/progress.js index 697856dd..6bb5f9b0 100644 --- a/WebApp/frontend/quiz_session/app/room/quiz/components/progress.js +++ b/WebApp/frontend/quiz_session/app/room/quiz/components/progress.js @@ -5,6 +5,10 @@ import { Progress } from "@mantine/core"; import { useState, useEffect } from "react"; import { useConnection, useQuestion } from "@/app/util/store"; +const SFX_TRIGGER_SECONDS = + process.env.QUIZMASTER_TRIGGER_SFX_SECONDS ?? + process.env.NEXT_PUBLIC_QUIZMASTER_TRIGGER_SFX_SECONDS; + export default function TimeProgress() { const { connection } = useConnection(); const { question } = useQuestion(); @@ -20,7 +24,7 @@ export default function TimeProgress() { return ( 10 ? "white" : "red"} + color={question?.remainingTime > SFX_TRIGGER_SECONDS ? "white" : "red"} bg={"green"} transitionDuration={1000} /> diff --git a/WebApp/frontend/quiz_session/app/room/quiz/modals/ConfirmEndGameModal.jsx b/WebApp/frontend/quiz_session/app/room/quiz/modals/ConfirmEndGameModal.jsx new file mode 100644 index 00000000..f784efec --- /dev/null +++ b/WebApp/frontend/quiz_session/app/room/quiz/modals/ConfirmEndGameModal.jsx @@ -0,0 +1,77 @@ +import { Button, Modal, RingProgress } from "@mantine/core"; +import React, { useState } from "react"; +import { useEffect } from "react"; +import { useRef } from "react"; +import { useLongPress } from "use-long-press"; + +export default function ConfirmEndGameModal({ + opened = false, + onClose = () => {}, + setsRemaining = 0, + onConfirm = () => {}, +}) { + const [ringValue, setRingValue] = useState(0); + const intervalRef = useRef(null); + + const bind = useLongPress( + () => { + onConfirm(); + clearInterval(intervalRef.current); + }, + { + onStart: (event) => { + intervalRef.current = setInterval(() => { + console.log("interval"); + setRingValue((prev) => prev + 7.5); + }, 200); + }, + onFinish: (event) => { + clearInterval(intervalRef.current); + }, + onCancel: (event) => { + clearInterval(intervalRef.current); + setRingValue(0); + }, + threshold: 3000, + } + ); + + useEffect(() => { + if (!opened) { + setRingValue(0); + } + }, [opened]); + + return ( + End this Quiz

} + > +
+

{`Are you sure want to end this quiz?`}

+
+ + +
+
+
+ ); +} diff --git a/WebApp/frontend/quiz_session/app/util/api.js b/WebApp/frontend/quiz_session/app/util/api.js index f75f2c86..2f6505ca 100644 --- a/WebApp/frontend/quiz_session/app/util/api.js +++ b/WebApp/frontend/quiz_session/app/util/api.js @@ -144,6 +144,8 @@ export const uploadScreenshot = ( // a.href = image; // a.download = createFileName(extension, name); // a.click(); + + const token = localStorage.getItem("token"); const blob = dataURLtoBlob(image); const formData = new FormData(); formData.append("File", blob, createFileName(extension, name)); @@ -153,11 +155,12 @@ export const uploadScreenshot = ( body: formData, redirect: "follow", credentials: "include", + headers: { + Authorization: `Bearer ${token}`, + }, }; console.info("uploading screenshot"); - const token = localStorage.getItem('token'); - fetch(`${BASE_URL}/gateway/api/Media/upload`, requestOptions) .then((response) => response.json()) .then((result) => { @@ -166,18 +169,21 @@ export const uploadScreenshot = ( // Submit Screenshot Link const payload = { method: "POST", - headers: { "Content-Type": "application/json", "Authorization": `Bearer ${token}` }, + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${token}`, + }, body: JSON.stringify({ questionId: id, connectionId, screenshotLink: `${BASE_URL}/gateway/api/media/download_media/${imageId}`, - }) - } + }), + }; - if(!token){ - payload.credentials="include" + if (!token) { + payload.credentials = "include"; } - + fetch(`${BASE_URL}/gateway/api/room/submitScreenshot`, payload) .then((response) => response.json()) .then((r) => { diff --git a/WebApp/frontend/quiz_session/next.config.js b/WebApp/frontend/quiz_session/next.config.js index c193a311..b02e3270 100644 --- a/WebApp/frontend/quiz_session/next.config.js +++ b/WebApp/frontend/quiz_session/next.config.js @@ -9,6 +9,9 @@ const nextConfig = { process.env.NEXT_PUBLIC_QUIZMASTER_GATEWAY, QUIZMASTER_ADMIN: process.env.QUIZMASTER_ADMIN ?? process.env.NEXT_PUBLIC_QUIZMASTER_ADMIN, + QUIZMASTER_TRIGGER_SFX_SECONDS: + process.env.QUIZMASTER_TRIGGER_SFX_SECONDS ?? + process.env.NEXT_PUBLIC_QUIZMASTER_TRIGGER_SFX_SECONDS, }, eslint: { ignoreDuringBuilds: ["/app", "/components", "/.next"], @@ -19,7 +22,7 @@ const nextConfig = { images: { domains: ["localhost"], }, - output: "standalone" + output: "standalone", }; module.exports = nextConfig; diff --git a/WebApp/frontend/quiz_session/package-lock.json b/WebApp/frontend/quiz_session/package-lock.json index 55382ba7..50be54a8 100644 --- a/WebApp/frontend/quiz_session/package-lock.json +++ b/WebApp/frontend/quiz_session/package-lock.json @@ -28,6 +28,7 @@ "react-dom": "^18", "react-use": "^17.4.2", "tailwind-scrollbar-hide": "^1.1.7", + "use-long-press": "^3.2.0", "use-sound": "^4.0.1", "zustand": "^4.4.7" }, @@ -5824,6 +5825,14 @@ } } }, + "node_modules/use-long-press": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/use-long-press/-/use-long-press-3.2.0.tgz", + "integrity": "sha512-uq5o2qFR1VRjHn8Of7Fl344/AGvgk7C5Mcb4aSb1ZRVp6PkgdXJJLdRrlSTJQVkkQcDuqFbFc3mDX4COg7mRTA==", + "peerDependencies": { + "react": ">=16.8.0" + } + }, "node_modules/use-memo-one": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/use-memo-one/-/use-memo-one-1.1.3.tgz", diff --git a/WebApp/frontend/quiz_session/package.json b/WebApp/frontend/quiz_session/package.json index eb086f2a..15c7d2e2 100644 --- a/WebApp/frontend/quiz_session/package.json +++ b/WebApp/frontend/quiz_session/package.json @@ -29,6 +29,7 @@ "react-dom": "^18", "react-use": "^17.4.2", "tailwind-scrollbar-hide": "^1.1.7", + "use-long-press": "^3.2.0", "use-sound": "^4.0.1", "zustand": "^4.4.7" },