diff --git a/api/src/main/java/cz/cvut/fel/sem/controller/SessionWSController.java b/api/src/main/java/cz/cvut/fel/sem/controller/SessionWSController.java index ba07339ea81f0e191ec8f5261918647898a4fd8e..1a9427d392aa4e30d6f60526235352e41f21ce54 100644 --- a/api/src/main/java/cz/cvut/fel/sem/controller/SessionWSController.java +++ b/api/src/main/java/cz/cvut/fel/sem/controller/SessionWSController.java @@ -5,13 +5,11 @@ import cz.cvut.fel.sem.dto.session.forStudent.JoinSessionResponseEnum; import cz.cvut.fel.sem.dto.session.forStudent.StudentResponseType; import cz.cvut.fel.sem.dto.session.forTeacher.CreateSessionResponseDto; import cz.cvut.fel.sem.dto.session.forTeacher.MessageType; +import cz.cvut.fel.sem.dto.session.forTeacher.QuestionEvaluationDto; import cz.cvut.fel.sem.dto.session.forTeacher.StudentResultsResponseDto; import cz.cvut.fel.sem.dto.session.fromStudent.AnswerToQuestionDto; import cz.cvut.fel.sem.dto.session.fromStudent.JoinSessionRequestDto; -import cz.cvut.fel.sem.dto.session.fromTeacher.CreateSessionDto; -import cz.cvut.fel.sem.dto.session.fromTeacher.EndSessionRequestDto; -import cz.cvut.fel.sem.dto.session.fromTeacher.NextQuestionRequestDto; -import cz.cvut.fel.sem.dto.session.fromTeacher.StudentResultsRequestDto; +import cz.cvut.fel.sem.dto.session.fromTeacher.*; import cz.cvut.fel.sem.model.session.Session; import cz.cvut.fel.sem.service.session.SessionService; import org.springframework.beans.factory.annotation.Autowired; @@ -20,6 +18,7 @@ import org.springframework.messaging.simp.annotation.SendToUser; import org.springframework.stereotype.Controller; import java.security.Principal; +import java.util.Objects; @Controller public class SessionWSController { @@ -68,8 +67,15 @@ public class SessionWSController { } - @MessageMapping("/getEvaluation") - private void getEvaluation(){ + @MessageMapping("/sendResults") + private void sendResultHandler(StudentResultsRequestDto studentResultsRequestDto){ + sessionService.sendResultsToStudents(studentResultsRequestDto.getSessionId()); + } + @MessageMapping("/getEvaluation") + @SendToUser("/topic/session") + private QuestionEvaluationDto getEvaluation(EvaluationRequestDto evaluationRequestDto){ + Objects.requireNonNull(evaluationRequestDto); + return sessionService.sendEvaluationToTeacher(evaluationRequestDto); } } diff --git a/api/src/main/java/cz/cvut/fel/sem/dto/session/forStudent/QuestionEndDto.java b/api/src/main/java/cz/cvut/fel/sem/dto/session/forStudent/QuestionEndDto.java new file mode 100644 index 0000000000000000000000000000000000000000..a0141daad937b9b46ecbe0412a0ce9dfc94fb749 --- /dev/null +++ b/api/src/main/java/cz/cvut/fel/sem/dto/session/forStudent/QuestionEndDto.java @@ -0,0 +1,12 @@ +package cz.cvut.fel.sem.dto.session.forStudent; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor +@AllArgsConstructor +public class QuestionEndDto { + private StudentResponseType responseType; +} diff --git a/api/src/main/java/cz/cvut/fel/sem/dto/session/forStudent/StudentResponseType.java b/api/src/main/java/cz/cvut/fel/sem/dto/session/forStudent/StudentResponseType.java index 3a3a6619564b1bc17c77fc47e0a16ec712db7d4d..9f03c7d28707036de6d567f5b7ffc81723dc23c3 100644 --- a/api/src/main/java/cz/cvut/fel/sem/dto/session/forStudent/StudentResponseType.java +++ b/api/src/main/java/cz/cvut/fel/sem/dto/session/forStudent/StudentResponseType.java @@ -3,5 +3,7 @@ package cz.cvut.fel.sem.dto.session.forStudent; public enum StudentResponseType { JOINSESSION, NEXTQUESTION, - ENDSESSION + ENDSESSION, + SENDRESULT, + QUESTIONEND } diff --git a/api/src/main/java/cz/cvut/fel/sem/dto/session/forStudent/UserResultDto.java b/api/src/main/java/cz/cvut/fel/sem/dto/session/forStudent/UserResultDto.java new file mode 100644 index 0000000000000000000000000000000000000000..f807daf18f1294eeda3ade31a3bf55e5c87bc0e9 --- /dev/null +++ b/api/src/main/java/cz/cvut/fel/sem/dto/session/forStudent/UserResultDto.java @@ -0,0 +1,13 @@ +package cz.cvut.fel.sem.dto.session.forStudent; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@AllArgsConstructor +@NoArgsConstructor +public class UserResultDto { + private int correctAnswers; + private StudentResponseType responseType; +} diff --git a/api/src/main/java/cz/cvut/fel/sem/dto/session/fromTeacher/EvaluationRequestDto.java b/api/src/main/java/cz/cvut/fel/sem/dto/session/fromTeacher/EvaluationRequestDto.java new file mode 100644 index 0000000000000000000000000000000000000000..109f06e1eb661fdbf9405588dcba0ccc36adf2f8 --- /dev/null +++ b/api/src/main/java/cz/cvut/fel/sem/dto/session/fromTeacher/EvaluationRequestDto.java @@ -0,0 +1,13 @@ +package cz.cvut.fel.sem.dto.session.fromTeacher; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor +@AllArgsConstructor +public class EvaluationRequestDto { + private Long sessionId; + private int questionKey; +} diff --git a/api/src/main/java/cz/cvut/fel/sem/service/session/SessionService.java b/api/src/main/java/cz/cvut/fel/sem/service/session/SessionService.java index 1225a2bb1072e52ec07fdde1d9bbeda5b7a97769..b574464368e01806f7e951c30b8963830182b80a 100644 --- a/api/src/main/java/cz/cvut/fel/sem/service/session/SessionService.java +++ b/api/src/main/java/cz/cvut/fel/sem/service/session/SessionService.java @@ -1,20 +1,13 @@ package cz.cvut.fel.sem.service.session; -import cz.cvut.fel.sem.dto.session.forStudent.JoinSessionResponseDto; -import cz.cvut.fel.sem.dto.session.forStudent.JoinSessionResponseEnum; -import cz.cvut.fel.sem.dto.session.forStudent.NextQuestionResponseDto; -import cz.cvut.fel.sem.dto.session.forStudent.StudentResponseType; -import cz.cvut.fel.sem.dto.session.forStudent.EndSessionResponseDto; +import cz.cvut.fel.sem.dto.session.forStudent.*; import cz.cvut.fel.sem.dto.session.forTeacher.MessageType; import cz.cvut.fel.sem.dto.session.forTeacher.NewStudentInSessionDto; import cz.cvut.fel.sem.dto.session.forTeacher.QuestionEvaluationDto; import cz.cvut.fel.sem.dto.session.forTeacher.StudentResultsResponseDto; import cz.cvut.fel.sem.dto.session.fromStudent.AnswerToQuestionDto; import cz.cvut.fel.sem.dto.session.fromStudent.JoinSessionRequestDto; -import cz.cvut.fel.sem.dto.session.fromTeacher.EndOfSessionReason; -import cz.cvut.fel.sem.dto.session.fromTeacher.EndSessionRequestDto; -import cz.cvut.fel.sem.dto.session.fromTeacher.NextQuestionRequestDto; -import cz.cvut.fel.sem.dto.session.fromTeacher.StudentResultsRequestDto; +import cz.cvut.fel.sem.dto.session.fromTeacher.*; import cz.cvut.fel.sem.exception.InvalidKeyException; import cz.cvut.fel.sem.exception.NotFoundException; import cz.cvut.fel.sem.mapper.QuizMapper; @@ -362,4 +355,40 @@ public class SessionService { } return new StudentResultsResponseDto(studentResults, MessageType.STUDENTRESULTS); } + + public void sendResultsToStudents(Long sessionId){ + if(!sessionRepository.existsById(sessionId)) { + throw new NotFoundException("The session with id " + sessionId + " was not found"); + } + Session currentSession = sessionRepository.findById(sessionId).get(); + currentSession.getStudentsInQuiz().parallelStream().forEach(student -> simpMessagingTemplate.convertAndSendToUser( + student.getSessionUserIdentifier(), + "/topic/session", + new UserResultDto(student.getAmountOfCorrectAnswers(), StudentResponseType.SENDRESULT) + )); + } + + public QuestionEvaluationDto sendEvaluationToTeacher(EvaluationRequestDto evaluationRequestDto){ + if(!sessionRepository.existsById(evaluationRequestDto.getSessionId())) { + throw new NotFoundException("The session with id " + evaluationRequestDto.getSessionId() + " was not found"); + } + Session currentSession = sessionRepository.findById(evaluationRequestDto.getSessionId()).get(); + if(evaluationRequestDto.getQuestionKey() != currentSession.getCurrentQuestionKey()){ + throw new InvalidKeyException("The keys don't match!"); + } + List<QuestionInSession> questionsInSession = currentSession.getQuestionsInSession(); + QuestionInSession currentTestedQuestion = findQuestionInSessionByKey(questionsInSession, evaluationRequestDto.getQuestionKey()); + currentSession.getStudentsInQuiz().parallelStream().forEach(student -> simpMessagingTemplate.convertAndSendToUser( + student.getSessionUserIdentifier(), + "/topic/session", + new QuestionEndDto(StudentResponseType.QUESTIONEND) + )); + return new QuestionEvaluationDto( + currentTestedQuestion.getAmountOfAnswersTotal(), + currentTestedQuestion.getAmountOfCorrectAnswers(), + currentTestedQuestion.getQuestionKey(), + currentTestedQuestion.getAmountsOfPositiveAnswersToEachAnswer(), + MessageType.QUESTIONEVALUATION + ); + } } diff --git a/ui/package-lock.json b/ui/package-lock.json index 821f0a164a38779398cb5253c269ca37a7094df9..211390f34f23f75970550bb92bd1711c64ba55b6 100644 --- a/ui/package-lock.json +++ b/ui/package-lock.json @@ -1203,6 +1203,43 @@ "resolved": "https://registry.npmjs.org/@csstools/normalize.css/-/normalize.css-10.1.0.tgz", "integrity": "sha512-ij4wRiunFfaJxjB0BdrYHIH8FxBJpOwNPhhAcunlmPdXudL1WQV1qoP9un6JsEBAgQH+7UXyyjh0g7jTxXK6tg==" }, + "@date-io/core": { + "version": "2.13.1", + "resolved": "https://registry.npmjs.org/@date-io/core/-/core-2.13.1.tgz", + "integrity": "sha512-pVI9nfkf2qClb2Cxdq0Q4zJhdawMG4ybWZUVGifT78FDwzRMX2SwXBb55s5NRJk0HcIicDuxktmCtemZqMH1Zg==" + }, + "@date-io/date-fns": { + "version": "2.13.1", + "resolved": "https://registry.npmjs.org/@date-io/date-fns/-/date-fns-2.13.1.tgz", + "integrity": "sha512-8fmfwjiLMpFLD+t4NBwDx0eblWnNcgt4NgfT/uiiQTGI81fnPu9tpBMYdAcuWxaV7LLpXgzLBx1SYWAMDVUDQQ==", + "requires": { + "@date-io/core": "^2.13.1" + } + }, + "@date-io/dayjs": { + "version": "2.13.1", + "resolved": "https://registry.npmjs.org/@date-io/dayjs/-/dayjs-2.13.1.tgz", + "integrity": "sha512-5bL4WWWmlI4uGZVScANhHJV7Mjp93ec2gNeUHDqqLaMZhp51S0NgD25oqj/k0LqBn1cdU2MvzNpk/ObMmVv5cQ==", + "requires": { + "@date-io/core": "^2.13.1" + } + }, + "@date-io/luxon": { + "version": "2.13.1", + "resolved": "https://registry.npmjs.org/@date-io/luxon/-/luxon-2.13.1.tgz", + "integrity": "sha512-yG+uM7lXfwLyKKEwjvP8oZ7qblpmfl9gxQYae55ifbwiTs0CoCTkYkxEaQHGkYtTqGTzLqcb0O9Pzx6vgWg+yg==", + "requires": { + "@date-io/core": "^2.13.1" + } + }, + "@date-io/moment": { + "version": "2.13.1", + "resolved": "https://registry.npmjs.org/@date-io/moment/-/moment-2.13.1.tgz", + "integrity": "sha512-XX1X/Tlvl3TdqQy2j0ZUtEJV6Rl8tOyc5WOS3ki52He28Uzme4Ro/JuPWTMBDH63weSWIZDlbR7zBgp3ZA2y1A==", + "requires": { + "@date-io/core": "^2.13.1" + } + }, "@emotion/babel-plugin": { "version": "11.3.0", "resolved": "https://registry.npmjs.org/@emotion/babel-plugin/-/babel-plugin-11.3.0.tgz", @@ -1969,6 +2006,14 @@ "react-transition-group": "^4.4.0" } }, + "@material-ui/icons": { + "version": "4.11.2", + "resolved": "https://registry.npmjs.org/@material-ui/icons/-/icons-4.11.2.tgz", + "integrity": "sha512-fQNsKX2TxBmqIGJCSi3tGTO/gZ+eJgWmMJkgDiOfyNaunNaxcklJQFaFogYcFl0qFuaEz1qaXYXboa/bUXVSOQ==", + "requires": { + "@babel/runtime": "^7.4.4" + } + }, "@material-ui/styles": { "version": "4.11.4", "resolved": "https://registry.npmjs.org/@material-ui/styles/-/styles-4.11.4.tgz", @@ -2032,6 +2077,60 @@ "react-is": "^16.8.0 || ^17.0.0" } }, + "@mui/base": { + "version": "5.0.0-alpha.73", + "resolved": "https://registry.npmjs.org/@mui/base/-/base-5.0.0-alpha.73.tgz", + "integrity": "sha512-TEUCIIEAWrngAqpIa+dY3nofGSNj70LC3KC9WcCzyXPK3M4AG2GNi7ndd/g/0DtC55kbxrudzlV8TG3vrB2Vjw==", + "requires": { + "@babel/runtime": "^7.17.2", + "@emotion/is-prop-valid": "^1.1.2", + "@mui/utils": "^5.4.4", + "@popperjs/core": "^2.11.4", + "clsx": "^1.1.1", + "prop-types": "^15.7.2", + "react-is": "^17.0.2" + }, + "dependencies": { + "@babel/runtime": { + "version": "7.17.8", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.17.8.tgz", + "integrity": "sha512-dQpEpK0O9o6lj6oPu0gRDbbnk+4LeHlNcBpspf6Olzt3GIX4P1lWF1gS+pHLDFlaJvbR6q7jCfQ08zA4QJBnmA==", + "requires": { + "regenerator-runtime": "^0.13.4" + } + }, + "@emotion/is-prop-valid": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-1.1.2.tgz", + "integrity": "sha512-3QnhqeL+WW88YjYbQL5gUIkthuMw7a0NGbZ7wfFVk2kg/CK5w8w5FFa0RzWjyY1+sujN0NWbtSHH6OJmWHtJpQ==", + "requires": { + "@emotion/memoize": "^0.7.4" + } + }, + "@mui/utils": { + "version": "5.4.4", + "resolved": "https://registry.npmjs.org/@mui/utils/-/utils-5.4.4.tgz", + "integrity": "sha512-hfYIXEuhc2mXMGN5nUPis8beH6uE/zl3uMWJcyHX0/LN/+QxO9zhYuV6l8AsAaphHFyS/fBv0SW3Nid7jw5hKQ==", + "requires": { + "@babel/runtime": "^7.17.2", + "@types/prop-types": "^15.7.4", + "@types/react-is": "^16.7.1 || ^17.0.0", + "prop-types": "^15.7.2", + "react-is": "^17.0.2" + } + }, + "@popperjs/core": { + "version": "2.11.4", + "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.4.tgz", + "integrity": "sha512-q/ytXxO5NKvyT37pmisQAItCFqA7FD/vNb8dgaJy3/630Fsc+Mz9/9f2SziBoIZ30TJooXyTwZmhi1zjXmObYg==" + }, + "react-is": { + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", + "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==" + } + } + }, "@mui/core": { "version": "5.0.0-alpha.50", "resolved": "https://registry.npmjs.org/@mui/core/-/core-5.0.0-alpha.50.tgz", @@ -2060,6 +2159,120 @@ "@babel/runtime": "^7.15.4" } }, + "@mui/lab": { + "version": "5.0.0-alpha.74", + "resolved": "https://registry.npmjs.org/@mui/lab/-/lab-5.0.0-alpha.74.tgz", + "integrity": "sha512-a3spQ3uWzlHVVBLcHqqMXPOwto39EVojaV3+3x102kTwPU6ZT1CsU+nI6+EiGCjZ7hW09gxjhUWxiU3fow9UWA==", + "requires": { + "@babel/runtime": "^7.17.2", + "@date-io/date-fns": "^2.13.1", + "@date-io/dayjs": "^2.13.1", + "@date-io/luxon": "^2.13.1", + "@date-io/moment": "^2.13.1", + "@mui/base": "5.0.0-alpha.73", + "@mui/system": "^5.5.2", + "@mui/utils": "^5.4.4", + "clsx": "^1.1.1", + "prop-types": "^15.7.2", + "react-is": "^17.0.2", + "react-transition-group": "^4.4.2", + "rifm": "^0.12.1" + }, + "dependencies": { + "@babel/runtime": { + "version": "7.17.8", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.17.8.tgz", + "integrity": "sha512-dQpEpK0O9o6lj6oPu0gRDbbnk+4LeHlNcBpspf6Olzt3GIX4P1lWF1gS+pHLDFlaJvbR6q7jCfQ08zA4QJBnmA==", + "requires": { + "regenerator-runtime": "^0.13.4" + } + }, + "@emotion/cache": { + "version": "11.7.1", + "resolved": "https://registry.npmjs.org/@emotion/cache/-/cache-11.7.1.tgz", + "integrity": "sha512-r65Zy4Iljb8oyjtLeCuBH8Qjiy107dOYC6SJq7g7GV5UCQWMObY4SJDPGFjiiVpPrOJ2hmJOoBiYTC7hwx9E2A==", + "requires": { + "@emotion/memoize": "^0.7.4", + "@emotion/sheet": "^1.1.0", + "@emotion/utils": "^1.0.0", + "@emotion/weak-memoize": "^0.2.5", + "stylis": "4.0.13" + } + }, + "@emotion/sheet": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@emotion/sheet/-/sheet-1.1.0.tgz", + "integrity": "sha512-u0AX4aSo25sMAygCuQTzS+HsImZFuS8llY8O7b9MDRzbJM0kVJlAz6KNDqcG7pOuQZJmj/8X/rAW+66kMnMW+g==" + }, + "@mui/private-theming": { + "version": "5.4.4", + "resolved": "https://registry.npmjs.org/@mui/private-theming/-/private-theming-5.4.4.tgz", + "integrity": "sha512-V/gxttr6736yJoU9q+4xxXsa0K/w9Hn9pg99zsOHt7i/O904w2CX5NHh5WqDXtoUzVcayLF0RB17yr6l79CE+A==", + "requires": { + "@babel/runtime": "^7.17.2", + "@mui/utils": "^5.4.4", + "prop-types": "^15.7.2" + } + }, + "@mui/styled-engine": { + "version": "5.5.2", + "resolved": "https://registry.npmjs.org/@mui/styled-engine/-/styled-engine-5.5.2.tgz", + "integrity": "sha512-jkz5AHHbA43akBo5L3y1X1/X0f+RvXvCp3eXKt+iOf3qnKSAausbtlVz7gBbC4xIWDnP1Jb/6T+t/0/7gObRYA==", + "requires": { + "@babel/runtime": "^7.17.2", + "@emotion/cache": "^11.7.1", + "prop-types": "^15.7.2" + } + }, + "@mui/system": { + "version": "5.5.2", + "resolved": "https://registry.npmjs.org/@mui/system/-/system-5.5.2.tgz", + "integrity": "sha512-OATYFI36nliud8xh0u+ZNqDo0jWjxpO0vZLlzqNB+ZtkR5Q/+1X3GgboA9ruiB8Rq+udnJlMBQNGW0qqjvAOHQ==", + "requires": { + "@babel/runtime": "^7.17.2", + "@mui/private-theming": "^5.4.4", + "@mui/styled-engine": "^5.5.2", + "@mui/types": "^7.1.3", + "@mui/utils": "^5.4.4", + "clsx": "^1.1.1", + "csstype": "^3.0.11", + "prop-types": "^15.7.2" + } + }, + "@mui/types": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/@mui/types/-/types-7.1.3.tgz", + "integrity": "sha512-DDF0UhMBo4Uezlk+6QxrlDbchF79XG6Zs0zIewlR4c0Dt6GKVFfUtzPtHCH1tTbcSlq/L2bGEdiaoHBJ9Y1gSA==" + }, + "@mui/utils": { + "version": "5.4.4", + "resolved": "https://registry.npmjs.org/@mui/utils/-/utils-5.4.4.tgz", + "integrity": "sha512-hfYIXEuhc2mXMGN5nUPis8beH6uE/zl3uMWJcyHX0/LN/+QxO9zhYuV6l8AsAaphHFyS/fBv0SW3Nid7jw5hKQ==", + "requires": { + "@babel/runtime": "^7.17.2", + "@types/prop-types": "^15.7.4", + "@types/react-is": "^16.7.1 || ^17.0.0", + "prop-types": "^15.7.2", + "react-is": "^17.0.2" + } + }, + "csstype": { + "version": "3.0.11", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.0.11.tgz", + "integrity": "sha512-sa6P2wJ+CAbgyy4KFssIb/JNMLxFvKF1pCYCSXS8ZMuqZnMsrxqI2E5sPyoTpxoPU/gVZMzr2zjOfg8GIZOMsw==" + }, + "react-is": { + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", + "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==" + }, + "stylis": { + "version": "4.0.13", + "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.0.13.tgz", + "integrity": "sha512-xGPXiFVl4YED9Jh7Euv2V220mriG9u4B2TA6Ybjc1catrstKD2PpIdU3U0RKpkVBC2EhmL/F0sPCr9vrFTNRag==" + } + } + }, "@mui/material": { "version": "5.0.3", "resolved": "https://registry.npmjs.org/@mui/material/-/material-5.0.3.tgz", @@ -2704,6 +2917,15 @@ "@types/node": "*" } }, + "@types/hoist-non-react-statics": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.1.tgz", + "integrity": "sha512-iMIqiko6ooLrTh1joXodJK5X9xeEALT1kM5G3ZLhD3hszxBdIEd5C75U834D9mLcINgD4OyZf5uQXjkuYydWvA==", + "requires": { + "@types/react": "*", + "hoist-non-react-statics": "^3.3.0" + } + }, "@types/html-minifier-terser": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/@types/html-minifier-terser/-/html-minifier-terser-5.1.2.tgz", @@ -2893,6 +3115,12 @@ "resolved": "https://registry.npmjs.org/@types/q/-/q-1.5.5.tgz", "integrity": "sha512-L28j2FcJfSZOnL1WBjDYp2vUHCeIFlyYI/53EwD/rKUBQ7MtUUfbQWiyKJGpcnv4/WgrhWsFKrcPstcAt/J0tQ==" }, + "@types/raf": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/@types/raf/-/raf-3.4.0.tgz", + "integrity": "sha512-taW5/WYqo36N7V39oYyHP9Ipfd5pNFvGTIQsNGj86xV88YQ7GnI30/yMfKDF7Zgin0m3e+ikX88FvImnK4RjGw==", + "optional": true + }, "@types/react": { "version": "17.0.27", "resolved": "https://registry.npmjs.org/@types/react/-/react-17.0.27.tgz", @@ -2911,6 +3139,17 @@ "@types/react": "*" } }, + "@types/react-redux": { + "version": "7.1.23", + "resolved": "https://registry.npmjs.org/@types/react-redux/-/react-redux-7.1.23.tgz", + "integrity": "sha512-D02o3FPfqQlfu2WeEYwh3x2otYd2Dk1o8wAfsA0B1C2AJEFxE663Ozu7JzuWbznGgW248NaOF6wsqCGNq9d3qw==", + "requires": { + "@types/hoist-non-react-statics": "^3.3.0", + "@types/react": "*", + "hoist-non-react-statics": "^3.3.0", + "redux": "^4.0.0" + } + }, "@types/react-transition-group": { "version": "4.4.3", "resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.3.tgz", @@ -4159,6 +4398,12 @@ } } }, + "base64-arraybuffer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-1.0.2.tgz", + "integrity": "sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ==", + "optional": true + }, "base64-js": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", @@ -4387,6 +4632,11 @@ "node-int64": "^0.4.0" } }, + "btoa": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/btoa/-/btoa-1.2.1.tgz", + "integrity": "sha512-SB4/MIGlsiVkMcHmT+pSmIPoNDoHg+7cMzmt3Uxt628MTz2487DKSqK/fuhFBrkuqrYv5UCEnACpF4dTFNKc/g==" + }, "buffer": { "version": "4.9.2", "resolved": "https://registry.npmjs.org/buffer/-/buffer-4.9.2.tgz", @@ -4558,6 +4808,22 @@ "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001265.tgz", "integrity": "sha512-YzBnspggWV5hep1m9Z6sZVLOt7vrju8xWooFAgN6BA5qvy98qPAPb7vNUzypFaoh2pb3vlfzbDO8tB57UPGbtw==" }, + "canvg": { + "version": "3.0.10", + "resolved": "https://registry.npmjs.org/canvg/-/canvg-3.0.10.tgz", + "integrity": "sha512-qwR2FRNO9NlzTeKIPIKpnTY6fqwuYSequ8Ru8c0YkYU7U0oW+hLUvWadLvAu1Rl72OMNiFhoLu4f8eUjQ7l/+Q==", + "optional": true, + "requires": { + "@babel/runtime": "^7.12.5", + "@types/raf": "^3.4.0", + "core-js": "^3.8.3", + "raf": "^3.4.1", + "regenerator-runtime": "^0.13.7", + "rgbcolor": "^1.0.1", + "stackblur-canvas": "^2.0.0", + "svg-pathdata": "^6.0.3" + } + }, "capture-exit": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/capture-exit/-/capture-exit-2.0.0.tgz", @@ -5082,6 +5348,14 @@ "postcss": "^7.0.5" } }, + "css-box-model": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/css-box-model/-/css-box-model-1.2.1.tgz", + "integrity": "sha512-a7Vr4Q/kd/aw96bnJG332W9V9LkJO69JRcaCYDUqjp6/z0w6VcZjgAcTbgFxEPfBgdnAwlh3iwu+hLopa+flJw==", + "requires": { + "tiny-invariant": "^1.0.6" + } + }, "css-color-names": { "version": "0.0.4", "resolved": "https://registry.npmjs.org/css-color-names/-/css-color-names-0.0.4.tgz", @@ -5122,6 +5396,15 @@ } } }, + "css-line-break": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/css-line-break/-/css-line-break-2.1.0.tgz", + "integrity": "sha512-FHcKFCZcAha3LwfVBhCQbW2nCNbkZXn7KVUJcsT5/P8YmfsVja0FMPJr0B903j/E69HUphKiV9iQArX8SDYA4w==", + "optional": true, + "requires": { + "utrie": "^1.0.2" + } + }, "css-loader": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/css-loader/-/css-loader-4.3.0.tgz", @@ -5472,6 +5755,16 @@ "whatwg-url": "^8.0.0" } }, + "date-fns": { + "version": "2.28.0", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.28.0.tgz", + "integrity": "sha512-8d35hViGYx/QH0icHYCeLmsLmMUheMmTyV9Fcm6gvNwdw31yXXH+O85sOBJ+OLnLQMKZowvpKb6FgMIQjcpvQw==" + }, + "debounce": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/debounce/-/debounce-1.2.0.tgz", + "integrity": "sha512-mYtLl1xfZLi1m4RtQYlZgJUNQjl4ZxVnHzIR8nLLgi4q1YT8o/WM+MK/f8yfcc9s5Ir5zRaPZyZU6xs1Syoocg==" + }, "debug": { "version": "4.3.2", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.2.tgz", @@ -5834,6 +6127,12 @@ } } }, + "dompurify": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-2.3.6.tgz", + "integrity": "sha512-OFP2u/3T1R5CEgWCEONuJ1a5+MFKnOYpkywpUSxv/dj1LeBT1erK+JwM7zK0ROy2BRhqVCf0LRw/kHqKuMkVGg==", + "optional": true + }, "domutils": { "version": "1.7.0", "resolved": "https://registry.npmjs.org/domutils/-/domutils-1.7.0.tgz", @@ -7179,6 +7478,11 @@ } } }, + "filefy": { + "version": "0.1.10", + "resolved": "https://registry.npmjs.org/filefy/-/filefy-0.1.10.tgz", + "integrity": "sha512-VgoRVOOY1WkTpWH+KBy8zcU1G7uQTVsXqhWEgzryB9A5hg2aqCyZ6aQ/5PSzlqM5+6cnVrX6oYV0XqD3HZSnmQ==" + }, "filesize": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/filesize/-/filesize-6.1.0.tgz", @@ -7889,6 +8193,16 @@ } } }, + "html2canvas": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/html2canvas/-/html2canvas-1.4.1.tgz", + "integrity": "sha512-fPU6BHNpsyIhr8yyMpTLLxAbkaK8ArIBcmZIRiBLiDhjeqvXolaEmDGmELFuX9I4xDcaKKcJl+TKZLqruBbmWA==", + "optional": true, + "requires": { + "css-line-break": "^2.1.0", + "text-segmentation": "^1.0.3" + } + }, "htmlparser2": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-6.1.0.tgz", @@ -10290,6 +10604,24 @@ "universalify": "^2.0.0" } }, + "jspdf": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/jspdf/-/jspdf-2.1.0.tgz", + "integrity": "sha512-NQygqZEKhSw+nExySJxB72Ge/027YEyIM450Vh/hgay/H9cgZNnkXXOQPRspe9EuCW4sq92zg8hpAXyyBdnaIQ==", + "requires": { + "atob": "^2.1.2", + "btoa": "^1.2.1", + "canvg": "^3.0.6", + "core-js": "^3.6.0", + "dompurify": "^2.0.12", + "html2canvas": "^1.0.0-rc.5" + } + }, + "jspdf-autotable": { + "version": "3.5.9", + "resolved": "https://registry.npmjs.org/jspdf-autotable/-/jspdf-autotable-3.5.9.tgz", + "integrity": "sha512-ZRfiI5P7leJuWmvC0jGVXu227m68C2Jfz1dkDckshmDYDeVFCGxwIBYdCUXJ8Eb2CyFQC2ok82fEWO+xRDovDQ==" + }, "jss": { "version": "10.8.0", "resolved": "https://registry.npmjs.org/jss/-/jss-10.8.0.tgz", @@ -10631,6 +10963,100 @@ "object-visit": "^1.0.0" } }, + "material-table": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/material-table/-/material-table-2.0.3.tgz", + "integrity": "sha512-jr+BZIITa52TruOn4uuFwgCjoPSlso5CFygFLVvHIWLU/GRfb64VgvJ5W8SaMfsni+b5l3KpWSberRsjWGStTw==", + "requires": { + "@date-io/date-fns": "2.13.1", + "@emotion/styled": "11.8.1", + "classnames": "2.2.6", + "date-fns": "2.28.0", + "debounce": "1.2.0", + "fast-deep-equal": "2.0.1", + "filefy": "0.1.10", + "jspdf": "2.1.0", + "jspdf-autotable": "3.5.9", + "prop-types": "15.6.2", + "react-beautiful-dnd": "13.1.0", + "react-double-scrollbar": "0.0.15" + }, + "dependencies": { + "@emotion/babel-plugin": { + "version": "11.7.2", + "resolved": "https://registry.npmjs.org/@emotion/babel-plugin/-/babel-plugin-11.7.2.tgz", + "integrity": "sha512-6mGSCWi9UzXut/ZAN6lGFu33wGR3SJisNl3c0tvlmb8XChH1b2SUvxvnOh7hvLpqyRdHHU9AiazV3Cwbk5SXKQ==", + "requires": { + "@babel/helper-module-imports": "^7.12.13", + "@babel/plugin-syntax-jsx": "^7.12.13", + "@babel/runtime": "^7.13.10", + "@emotion/hash": "^0.8.0", + "@emotion/memoize": "^0.7.5", + "@emotion/serialize": "^1.0.2", + "babel-plugin-macros": "^2.6.1", + "convert-source-map": "^1.5.0", + "escape-string-regexp": "^4.0.0", + "find-root": "^1.1.0", + "source-map": "^0.5.7", + "stylis": "4.0.13" + } + }, + "@emotion/is-prop-valid": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-1.1.2.tgz", + "integrity": "sha512-3QnhqeL+WW88YjYbQL5gUIkthuMw7a0NGbZ7wfFVk2kg/CK5w8w5FFa0RzWjyY1+sujN0NWbtSHH6OJmWHtJpQ==", + "requires": { + "@emotion/memoize": "^0.7.4" + } + }, + "@emotion/styled": { + "version": "11.8.1", + "resolved": "https://registry.npmjs.org/@emotion/styled/-/styled-11.8.1.tgz", + "integrity": "sha512-OghEVAYBZMpEquHZwuelXcRjRJQOVayvbmNR0zr174NHdmMgrNkLC6TljKC5h9lZLkN5WGrdUcrKlOJ4phhoTQ==", + "requires": { + "@babel/runtime": "^7.13.10", + "@emotion/babel-plugin": "^11.7.1", + "@emotion/is-prop-valid": "^1.1.2", + "@emotion/serialize": "^1.0.2", + "@emotion/utils": "^1.1.0" + } + }, + "@emotion/utils": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@emotion/utils/-/utils-1.1.0.tgz", + "integrity": "sha512-iRLa/Y4Rs5H/f2nimczYmS5kFJEbpiVvgN3XVfZ022IYhuNA1IRSHEizcof88LtCTXtl9S2Cxt32KgaXEu72JQ==" + }, + "classnames": { + "version": "2.2.6", + "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.2.6.tgz", + "integrity": "sha512-JR/iSQOSt+LQIWwrwEzJ9uk0xfN3mTVYMwt1Ir5mUcSN6pU+V4zQFFaJsclJbPuAUQH+yfWef6tm7l1quW3C8Q==" + }, + "escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==" + }, + "fast-deep-equal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz", + "integrity": "sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk=" + }, + "prop-types": { + "version": "15.6.2", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.6.2.tgz", + "integrity": "sha512-3pboPvLiWD7dkI3qf3KbUe6hKFKa52w+AE0VCqECtf+QHAKgOL37tTaNCnuX1nAAQ4ZhyP+kYVKf8rLmJ/feDQ==", + "requires": { + "loose-envify": "^1.3.1", + "object-assign": "^4.1.1" + } + }, + "stylis": { + "version": "4.0.13", + "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.0.13.tgz", + "integrity": "sha512-xGPXiFVl4YED9Jh7Euv2V220mriG9u4B2TA6Ybjc1catrstKD2PpIdU3U0RKpkVBC2EhmL/F0sPCr9vrFTNRag==" + } + } + }, "md5.js": { "version": "1.3.5", "resolved": "https://registry.npmjs.org/md5.js/-/md5.js-1.3.5.tgz", @@ -10651,6 +11077,11 @@ "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=" }, + "memoize-one": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-5.2.1.tgz", + "integrity": "sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q==" + }, "memory-fs": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/memory-fs/-/memory-fs-0.4.1.tgz", @@ -12969,6 +13400,11 @@ "performance-now": "^2.1.0" } }, + "raf-schd": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/raf-schd/-/raf-schd-4.0.3.tgz", + "integrity": "sha512-tQkJl2GRWh83ui2DiPTJz9wEiMN20syf+5oKfB03yYP7ioZcJwsIK8FjrtLwH1m7C7e+Tt2yYBlrOpdT+dyeIQ==" + }, "randombytes": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", @@ -13031,6 +13467,20 @@ "whatwg-fetch": "^3.4.1" } }, + "react-beautiful-dnd": { + "version": "13.1.0", + "resolved": "https://registry.npmjs.org/react-beautiful-dnd/-/react-beautiful-dnd-13.1.0.tgz", + "integrity": "sha512-aGvblPZTJowOWUNiwd6tNfEpgkX5OxmpqxHKNW/4VmvZTNTbeiq7bA3bn5T+QSF2uibXB0D1DmJsb1aC/+3cUA==", + "requires": { + "@babel/runtime": "^7.9.2", + "css-box-model": "^1.2.0", + "memoize-one": "^5.1.1", + "raf-schd": "^4.0.2", + "react-redux": "^7.2.0", + "redux": "^4.0.4", + "use-memo-one": "^1.1.1" + } + }, "react-codemirror2": { "version": "7.2.1", "resolved": "https://registry.npmjs.org/react-codemirror2/-/react-codemirror2-7.2.1.tgz", @@ -13200,6 +13650,11 @@ "scheduler": "^0.20.2" } }, + "react-double-scrollbar": { + "version": "0.0.15", + "resolved": "https://registry.npmjs.org/react-double-scrollbar/-/react-double-scrollbar-0.0.15.tgz", + "integrity": "sha1-6RWrjLO5WYdwdfSUNt6/2wQoj+Q=" + }, "react-error-overlay": { "version": "6.0.9", "resolved": "https://registry.npmjs.org/react-error-overlay/-/react-error-overlay-6.0.9.tgz", @@ -13220,6 +13675,26 @@ "resolved": "https://registry.npmjs.org/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz", "integrity": "sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==" }, + "react-redux": { + "version": "7.2.6", + "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-7.2.6.tgz", + "integrity": "sha512-10RPdsz0UUrRL1NZE0ejTkucnclYSgXp5q+tB5SWx2qeG2ZJQJyymgAhwKy73yiL/13btfB6fPr+rgbMAaZIAQ==", + "requires": { + "@babel/runtime": "^7.15.4", + "@types/react-redux": "^7.1.20", + "hoist-non-react-statics": "^3.3.2", + "loose-envify": "^1.4.0", + "prop-types": "^15.7.2", + "react-is": "^17.0.2" + }, + "dependencies": { + "react-is": { + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", + "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==" + } + } + }, "react-refresh": { "version": "0.8.3", "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.8.3.tgz", @@ -13561,6 +14036,14 @@ } } }, + "redux": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/redux/-/redux-4.1.2.tgz", + "integrity": "sha512-SH8PglcebESbd/shgf6mii6EIoRM0zrQyjcuQ+ojmfxjTtE0z9Y8pa62iA/OJ58qjP6j27uyW4kUF4jl/jd6sw==", + "requires": { + "@babel/runtime": "^7.9.2" + } + }, "regenerate": { "version": "1.4.2", "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz", @@ -13933,6 +14416,17 @@ "resolved": "https://registry.npmjs.org/rgba-regex/-/rgba-regex-1.0.0.tgz", "integrity": "sha1-QzdOLiyglosO8VI0YLfXMP8i7rM=" }, + "rgbcolor": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/rgbcolor/-/rgbcolor-1.0.1.tgz", + "integrity": "sha1-1lBezbMEplldom+ktDMHMGd1lF0=", + "optional": true + }, + "rifm": { + "version": "0.12.1", + "resolved": "https://registry.npmjs.org/rifm/-/rifm-0.12.1.tgz", + "integrity": "sha512-OGA1Bitg/dSJtI/c4dh90svzaUPt228kzFsUkJbtA2c964IqEAwWXeL9ZJi86xWv3j5SMqRvGULl7bA6cK0Bvg==" + }, "rimraf": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", @@ -14840,6 +15334,12 @@ } } }, + "stackblur-canvas": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/stackblur-canvas/-/stackblur-canvas-2.5.0.tgz", + "integrity": "sha512-EeNzTVfj+1In7aSLPKDD03F/ly4RxEuF/EX0YcOG0cKoPXs+SLZxDawQbexQDBzwROs4VKLWTOaZQlZkGBFEIQ==", + "optional": true + }, "stackframe": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/stackframe/-/stackframe-1.2.0.tgz", @@ -15117,6 +15617,12 @@ "resolved": "https://registry.npmjs.org/svg-parser/-/svg-parser-2.0.4.tgz", "integrity": "sha512-e4hG1hRwoOdRb37cIMSgzNsxyzKfayW6VOflrwvR+/bzrkyxY/31WkbgnQpgtrNp1SdpJvpUAGTa/ZoiPNDuRQ==" }, + "svg-pathdata": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/svg-pathdata/-/svg-pathdata-6.0.3.tgz", + "integrity": "sha512-qsjeeq5YjBZ5eMdFuUa4ZosMLxgr5RZ+F+Y1OrDhuOCEInRMA3x74XdBtggJcj9kOeInz0WE+LgCPDkZFlBYJw==", + "optional": true + }, "svgo": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/svgo/-/svgo-1.3.2.tgz", @@ -15355,6 +15861,15 @@ "minimatch": "^3.0.4" } }, + "text-segmentation": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/text-segmentation/-/text-segmentation-1.0.3.tgz", + "integrity": "sha512-iOiPUo/BGnZ6+54OsWxZidGCsdU8YbE4PSpdPinp7DeMtUJNJBoJ/ouUSTJjHkh1KntHaltHl/gDs2FC4i5+Nw==", + "optional": true, + "requires": { + "utrie": "^1.0.2" + } + }, "text-table": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", @@ -15789,6 +16304,11 @@ "resolved": "https://registry.npmjs.org/use/-/use-3.1.1.tgz", "integrity": "sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ==" }, + "use-memo-one": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/use-memo-one/-/use-memo-one-1.1.2.tgz", + "integrity": "sha512-u2qFKtxLsia/r8qG0ZKkbytbztzRb317XCkT7yP8wxL0tZ/CzK2G+WWie5vWvpyeP7+YoPIwbJoIHJ4Ba4k0oQ==" + }, "utf-8-validate": { "version": "5.0.8", "resolved": "https://registry.npmjs.org/utf-8-validate/-/utf-8-validate-5.0.8.tgz", @@ -15839,6 +16359,15 @@ "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", "integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=" }, + "utrie": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/utrie/-/utrie-1.0.2.tgz", + "integrity": "sha512-1MLa5ouZiOmQzUbjbu9VmjLzn1QLXBhwpUa7kdLUQK+KQ5KA9I1vk5U4YHe/X2Ch7PYnJfWuWT+VbuxbGwljhw==", + "optional": true, + "requires": { + "base64-arraybuffer": "^1.0.2" + } + }, "uuid": { "version": "8.3.2", "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", diff --git a/ui/package.json b/ui/package.json index 76a8b4fea60a831a3913ee34c423728ea58465b3..3dbb293937e04605ffe9a9ce8163d363461c1a33 100644 --- a/ui/package.json +++ b/ui/package.json @@ -6,13 +6,16 @@ "@emotion/react": "^11.4.1", "@emotion/styled": "^11.3.0", "@material-ui/core": "^4.12.3", + "@material-ui/icons": "^4.11.2", "@mui/icons-material": "^5.0.3", + "@mui/lab": "^5.0.0-alpha.74", "@mui/material": "^5.0.3", "@mui/styles": "^5.0.1", "@testing-library/jest-dom": "^5.14.1", "@testing-library/react": "^11.2.7", "@testing-library/user-event": "^12.8.3", "codemirror": "^5.65.1", + "material-table": "^2.0.3", "react": "^17.0.2", "react-codemirror2": "^7.2.1", "react-dom": "^17.0.2", diff --git a/ui/src/pages/quiz/CreateQuiz.js b/ui/src/pages/quiz/CreateQuiz.js index f91f2bab2bd4bd6813051988a3a1fbefbb4053a2..7d30b8f814ca612738ec9eaec43ac965c1dd2f65 100644 --- a/ui/src/pages/quiz/CreateQuiz.js +++ b/ui/src/pages/quiz/CreateQuiz.js @@ -88,11 +88,10 @@ const CreateQuiz = (props) => { if(document.getElementById('name').value === ""){ return "Name of the question is required" } - for (const [key, value] of Object.entries(answersValues)) { - if(value === ""){ - emptyAnswerValues += 1 - } - } + Object.entries(answersValues).forEach(([, value]) => { if(value === ""){ + emptyAnswerValues += 1 + } }) + if(emptyAnswerValues > 2){ return "At least 2 answers should be filled" } diff --git a/ui/src/pages/quizSession/student/JoinQuiz.tsx b/ui/src/pages/quizSession/student/JoinQuiz.tsx index 852d0ddfa26cd5b9306221dbbe08a5c5960e7c4c..bbc5fe068a7547c363a497aaf67c71d60f9e616a 100644 --- a/ui/src/pages/quizSession/student/JoinQuiz.tsx +++ b/ui/src/pages/quizSession/student/JoinQuiz.tsx @@ -26,6 +26,10 @@ interface nextQuestionMessage extends ResponseMessage { questionKey: number; } +interface resultMessage extends ResponseMessage { + correctAnswers: number; +} + interface QuestionAnswer { sessionId: number; questionKey: number; @@ -44,7 +48,9 @@ const responseStatus = { const responseType = { JOINSESSION: "JOINSESSION", NEXTQUESTION: "NEXTQUESTION", - ENDSESSION: "ENDSESSION" + ENDSESSION: "ENDSESSION", + SENDRESULT: "SENDRESULT", + QUESTIONEND: "QUESTIONEND" } const LayoutType = { @@ -62,11 +68,14 @@ const JoinQuiz = () => { //Holds the info about current question in the session const [currentQuestionKey, setCurrentQuestionKey] = useState<number>(0) //Contains info about which answers are correct and which are false. This info is saved to each question. + //Student makes the decision which answers he thinks are correct when he is answering the question const [answersCorrect, setAnswersCorrect] = useState(answersCorrectInitialState) //Contains current type of layout the page should have const [currentLayout, setCurrentLayout] = useState<string>(LayoutType.JoiningQuiz) + const [result, setResult] = useState<number>() //Holds current session id the user is connected to const sessionId = useRef(0) + let textWhenWaiting = useRef("undefined") //derived answers state -> is computed when quiz or currentQuestionKey change //it is set to null if user is not yet connected to session or the teacher @@ -167,6 +176,7 @@ const JoinQuiz = () => { setAnswersCorrect(answersCorrectInitialState) setCurrentQuestionKey(nextQuestionMessage.questionKey) setCurrentLayout(LayoutType.AnsweringQuestion) + textWhenWaiting.current = "undefined" break //the session just ended case responseType.ENDSESSION: @@ -174,9 +184,19 @@ const JoinQuiz = () => { setCurrentLayout(LayoutType.JoiningQuiz) setCurrentQuestionKey(0) setQuiz(null) + setResult(undefined) //reset event handler of the window window.removeEventListener("beforeunload", alertUser) break + case responseType.SENDRESULT: + const result: resultMessage = payloadData + setResult(result.correctAnswers) + break + case responseType.QUESTIONEND: + if(textWhenWaiting.current === "undefined") textWhenWaiting.current = "was not" + setAnswersCorrect(answersCorrectInitialState) + setCurrentLayout(LayoutType.WaitingForTeacher) + break default: break } @@ -269,6 +289,7 @@ const JoinQuiz = () => { bottomRightAnswer: answersCorrect.BottomRight } stompClient.send("/ws/submitAnswer", {}, JSON.stringify(newQuestionAnswer)) + textWhenWaiting.current = "was" setAnswersCorrect(answersCorrectInitialState) setCurrentLayout(LayoutType.WaitingForTeacher) } @@ -300,13 +321,17 @@ const JoinQuiz = () => { <Typography variant="h5"> {currentQuestionKey === 0 ? "You should see yourself on the screen!" : - "Your answer was recorded" + result ? + "You got " + result + "/" + quiz?.questions.length + " answers correct. Thanks for playing!" : + "Your answer " + textWhenWaiting.current + " recorded" } </Typography> </Grid> <Typography variant="h5"> {currentQuestionKey === 0 ? "Wait for your teacher to start the quiz." : + result ? + "You can now leave this page." : "Wait for your teacher to activate next question." } </Typography> diff --git a/ui/src/pages/quizSession/teacher/StartQuiz.tsx b/ui/src/pages/quizSession/teacher/StartQuiz.tsx index eb1050225608c6fbbe1277c31609d2009d49005f..02625b14a4c4305f8a9713ca1980d03187672254 100644 --- a/ui/src/pages/quizSession/teacher/StartQuiz.tsx +++ b/ui/src/pages/quizSession/teacher/StartQuiz.tsx @@ -14,6 +14,7 @@ import { useHistory } from "react-router-dom"; import { Quiz, Question } from '../../../common/types' import QuestionCreator from '../../quiz/questionParameters/QuestionCreator' import QuestionEvaluation from './questionEvaluation/QuestionEvaluation' +import StudentResults from './studentResults/StudentResults' interface NewStudentMessageResponse { name: string; @@ -55,10 +56,12 @@ interface EndSessionRequest { reason: string; } +export interface StudentScores { + [key: string]: number +} + interface StudentResults { - studentScores: Array<{ - [key: string]: number - }> + studentScores: StudentScores } const MessageType = { @@ -71,7 +74,8 @@ const MessageType = { const LayoutType = { UsersJoining: "UsersJoining", DisplayQuestion: "DisplayQuestion", - ShowEvaluation: "ShowEvaluation" + ShowEvaluation: "ShowEvaluation", + StudentScores: "StudentScores" } //The reasons why the end session request is sent to the server. @@ -104,6 +108,7 @@ const StartQuiz = (props) => { const [currentQuizKey, setCurrentQuizKey] = useState<number>(0) const [currentLayout, setCurrentLayout] = useState<string>(LayoutType.UsersJoining) const [currentQuestionEvaluation, setCurrentQuestionEvaluation] = useState<QuestionEvaluationType | null>(null); + const [studentScores, setStudentScores] = useState<StudentScores | null>(null) //In current implementation when user closes the page, the function which sends the request to end the session is called. //This is unnecessary at the end of the quiz, this state helps to determine whether the request should be called or not const quizAlreadyEnded = useRef(false); @@ -142,7 +147,7 @@ const StartQuiz = (props) => { break case MessageType.StudentResults: const studentResults: StudentResults = payloadData - console.log(studentResults) + setStudentScores(studentResults.studentScores) } } @@ -191,7 +196,7 @@ const StartQuiz = (props) => { } stompClient.send("/ws/getStudentResults", {}, JSON.stringify(getStudentResultsRequest)) } - }, [lastQuestion, currentLayout]) + }, [lastQuestion, currentLayout, sessionId]) //Moves the quiz session to the next question. //Handles the button displayed on the question evaluation page and on the start page before the quiz starts @@ -214,19 +219,28 @@ const StartQuiz = (props) => { //send request to get the evaluation of the current question with the current submitted answers const handleEndQuestionButton = () => { - const getEvaluationRequest: SessionEvaluationRequest = { + const getEvaluationRequest = { sessionId: sessionId, + questionKey: currentQuestion?.key } stompClient.send("/ws/getEvaluation", {}, JSON.stringify(getEvaluationRequest)) } + const handleShowStudentResults = () => { + const getEvaluationRequest: SessionEvaluationRequest = { + sessionId: sessionId, + } + stompClient.send("/ws/sendResults", {}, JSON.stringify(getEvaluationRequest)) + setCurrentLayout(LayoutType.StudentScores) + } + return ( <> <Prompt - when={!(lastQuestion && (LayoutType.ShowEvaluation === currentLayout))} + when={!(lastQuestion && (LayoutType.ShowEvaluation === currentLayout || LayoutType.StudentScores === currentLayout))} message='If you leave now, the whole session will end. Are you sure you want to do this?' /> - {currentLayout === LayoutType.UsersJoining ? + {currentLayout === LayoutType.UsersJoining && <Grid container direction={"column"} @@ -287,7 +301,8 @@ const StartQuiz = (props) => { } </Grid> </Grid> - : currentLayout === LayoutType.DisplayQuestion ? + } + { currentLayout === LayoutType.DisplayQuestion && <Grid container alignItems={"center"} @@ -319,12 +334,20 @@ const StartQuiz = (props) => { </Button> </Grid> </Grid> - : + } + { currentLayout === LayoutType.ShowEvaluation && <QuestionEvaluation handleNextQuestionButton={handleNextQuestionButton} questionEvaluation={currentQuestionEvaluation} currentQuestion={currentQuestion} lastQuestion={lastQuestion} + handleShowStudentResults={handleShowStudentResults} + /> + } + { currentLayout === LayoutType.StudentScores && + <StudentResults + studentScores={studentScores} + handleNextQuestionButton={handleNextQuestionButton} /> } </> diff --git a/ui/src/pages/quizSession/teacher/questionEvaluation/CustomTooltip.tsx b/ui/src/pages/quizSession/teacher/questionEvaluation/CustomTooltip.tsx new file mode 100644 index 0000000000000000000000000000000000000000..0a45ba7c69ec7700201500d65a99bae0cf23f605 --- /dev/null +++ b/ui/src/pages/quizSession/teacher/questionEvaluation/CustomTooltip.tsx @@ -0,0 +1,25 @@ +// recharts doesn't export the default tooltip, +// but it's located in the package lib so you can get to it anyways +import { useState } from 'react'; +import { DefaultTooltipContent } from 'recharts/lib/component/DefaultTooltipContent'; + +const CustomTooltip = (props) => { + const [alreadyChanged, setAlreadyChanged] = useState<boolean>(false) + // payload[0] doesn't exist when tooltip isn't visible + if (props.payload[0] != null) { + // mutating props directly is against react's conventions + // so we create a new payload with the name and value fields set to what we want + if(!alreadyChanged){ + props.payload[0].payload.Total = props.payload[0].payload.Total + props.payload[0].payload.Correct + setAlreadyChanged(true) + } + + // we render the default, but with our overridden payload + return <DefaultTooltipContent {...props} />; + } + + // we just render the default + return <DefaultTooltipContent {...props} />; +}; + +export default CustomTooltip \ No newline at end of file diff --git a/ui/src/pages/quizSession/teacher/questionEvaluation/QuestionEvaluation.tsx b/ui/src/pages/quizSession/teacher/questionEvaluation/QuestionEvaluation.tsx index 49d800e7c25e2fdcc414620970597f715d372db8..568cb8aee98f2616ab888c73d645eed76346cb2f 100644 --- a/ui/src/pages/quizSession/teacher/questionEvaluation/QuestionEvaluation.tsx +++ b/ui/src/pages/quizSession/teacher/questionEvaluation/QuestionEvaluation.tsx @@ -1,16 +1,20 @@ import React from 'react' import { makeStyles } from '@mui/styles'; import { Button, TextField } from '@mui/material'; +import { BarChart, Bar, XAxis, YAxis, Tooltip, ResponsiveContainer } from 'recharts'; import './index.css' import { QuestionEvaluationType } from '../StartQuiz' import { Question, languageTypes } from '../../../../common/types' import CustomCodeEditor from '../../../quiz/questionParameters/codeEditor/CustomCodeEditor' +import CustomTooltip from './CustomTooltip' +import AnswersEvaluation from './answersEvaluation/AnswersEvaluation' interface QuestionEvaluationProps { questionEvaluation: QuestionEvaluationType | null; currentQuestion: Question | null; handleNextQuestionButton(): void; + handleShowStudentResults(): void; lastQuestion: boolean; } @@ -29,11 +33,24 @@ const QuestionEvaluation = ({ questionEvaluation, currentQuestion, handleNextQuestionButton, - lastQuestion + lastQuestion, + handleShowStudentResults }: QuestionEvaluationProps) => { const classes = useStyles() + const totalAnswers = (questionEvaluation?.amountOfAnswersTotal ? questionEvaluation?.amountOfAnswersTotal : 0) - + (questionEvaluation?.amountOfCorrectAnswers ? questionEvaluation?.amountOfCorrectAnswers : 0) + + console.log(questionEvaluation?.amountOfAnswersTotal) + console.log(questionEvaluation?.amountOfCorrectAnswers) + console.log(totalAnswers) + + //the data passed into the chart of correct answers out of total answers + const answersChartData = [ + { name: "Answers", Correct: questionEvaluation?.amountOfCorrectAnswers, Total: totalAnswers } + ] + return ( <> <div className='wrapperDiv'> @@ -55,35 +72,45 @@ const QuestionEvaluation = ({ languageTypes={languageTypes} /> </div> - <div> - Amount of positive choices on each answer: - </div> - <div> - {currentQuestion?.topLeftAnswer.value} : {questionEvaluation?.amountsOfPositiveAnswersToEachAnswer.TOPLEFT} - </div> - <div> - {currentQuestion?.topRightAnswer.value} : {questionEvaluation?.amountsOfPositiveAnswersToEachAnswer.TOPRIGHT} - </div> - <div> - {currentQuestion?.bottomLeftAnswer.value} : {questionEvaluation?.amountsOfPositiveAnswersToEachAnswer.BOTTOMLEFT} - </div> - <div> - {currentQuestion?.bottomRightAnswer.value} : {questionEvaluation?.amountsOfPositiveAnswersToEachAnswer.BOTTOMRIGHT} - </div> - <div> - Amount of students who answered: {questionEvaluation?.amountOfAnswersTotal} + <div className='answersWrapper'> + <AnswersEvaluation + currentQuestion={currentQuestion} + questionEvaluation={questionEvaluation} + /> </div> - <div> - Amount of students who answered correctly: {questionEvaluation?.amountOfCorrectAnswers} + <div className='barWrapper'> + <ResponsiveContainer width="100%" height="100%" > + <BarChart stackOffset="expand" data={answersChartData} layout="vertical" > + <Tooltip cursor={{fill: 'white'}} wrapperStyle={{ top: 40, }} content={<CustomTooltip labelFormatter={() => ""}/>}/> + <YAxis dataKey={"name"} type="category" + axisLine={false} + tickLine={false} + width={150} /> + <XAxis hide type="number"/> + <Bar dataKey="Correct" stackId="a" fill="#288f36" /> + <Bar dataKey="Total" stackId="a" fill="#grey" fillOpacity={0.2}/> + </BarChart> + </ ResponsiveContainer> </div> <div className="buttonWrapper"> <Button variant='contained' color='primary' onClick={handleNextQuestionButton} + sx = {{width: "190px"}} > {lastQuestion ? "End the quiz" : "Next question"} </Button> + {lastQuestion && + <Button + variant='contained' + color='primary' + onClick={handleShowStudentResults} + sx={{marginTop: "20px", width: "190px"}} + > + Student's results + </Button> + } </div> </div> </> diff --git a/ui/src/pages/quizSession/teacher/questionEvaluation/answersEvaluation/AnswersEvaluation.tsx b/ui/src/pages/quizSession/teacher/questionEvaluation/answersEvaluation/AnswersEvaluation.tsx new file mode 100644 index 0000000000000000000000000000000000000000..10868e109a9aa814b25e908c7a6d894a2f7be1b5 --- /dev/null +++ b/ui/src/pages/quizSession/teacher/questionEvaluation/answersEvaluation/AnswersEvaluation.tsx @@ -0,0 +1,179 @@ +import React from "react" + +import { QuestionEvaluationType } from '../../StartQuiz' +import { Question } from '../../../../../common/types' + +import ThumbDownIcon from '@mui/icons-material/ThumbDown'; +import ThumbUpIcon from '@mui/icons-material/ThumbUp'; +import { TextField, Grid, InputAdornment, Tooltip } from "@mui/material"; +import { tooltipClasses } from "@mui/material"; +import { makeStyles, styled } from '@mui/styles'; + +interface AnswersEvaluationProps { + currentQuestion: Question | null; + questionEvaluation: QuestionEvaluationType | null; +} + +interface QuestionAnswer { + value: string; + isCorrect: boolean; +} + +const useStyles = makeStyles(() => ({ + answer: { + borderRadius: "4px", + //assuring that even when the text fields are disabled, the font color stays black + "& .MuiInputBase-root": { + "& .Mui-disabled": { + "-webkit-text-fill-color": "rgba(0, 0, 0, 1)", + cursor: "pointer" + } + } + } +})); + +//Custom styling of the tooltip, which gets displayed, when user hoovers over thumbs up and down icons in the answers field +const CustomTooltip = styled(({ className, ...props }) => ( + <Tooltip title={"Submitted / Total"} placement="top" arrow {...props} classes={{ popper: className }} /> +))({ + [`& .${tooltipClasses.tooltip}`]: { + maxWidth: 160, + padding: 10, + textAlign:"center", + backgroundColor: "#373737" + }, + [`& .${tooltipClasses.arrow}`]: { + color: "#373737" + } +}); + +interface props { + multiline: boolean; + maxRows: number; + size: "medium" | "small"; + disabled: boolean; +} + +const textFieldStaticProps: props = { + multiline: true, + maxRows: 3, + size: "small", + disabled: true +} + +interface AdornmentProps { + id: string; +} + +const AnswersEvaluation = (props: AnswersEvaluationProps) => { + const {questionEvaluation, currentQuestion} = props + const classes = useStyles() + + //Custom adornment of the tooltip, it is used 4 times for the answers, that's why it is extracted into the variable + const AdornmentCustom = (props: AdornmentProps) =>{ + const {id} = props + if(currentQuestion){ + return ( + <> + <InputAdornment position="end"> + {currentQuestion[id as keyof QuestionAnswer].isCorrect ? + <ThumbUpIcon + color="secondary" + /> : + <ThumbDownIcon + color="error" + /> + } + </InputAdornment> + </> + ) + } + return <></> + } + + return ( + <> + <Grid + container + direction={"row"} + spacing={1} + justifyContent="center" + alignItems="center" + //margin evens the spacing issues with grid + sx={{width: "100%", margin: "0 0 0 -4px"}} + > + <Grid item xs={6}> + <Grid container direction={"row"}> + <TextField + {...textFieldStaticProps} + className={classes.answer} + value={currentQuestion?.topLeftAnswer.value} + inputProps={{style: { textAlign: 'center' }}} + sx={{ backgroundColor: '#66A4FF', flexGrow:1 }} + InputProps = {{ + endAdornment: <AdornmentCustom id="topLeftAnswer"/> + }} + /> + <CustomTooltip> + <div className={"answerScore"}>{questionEvaluation?.amountsOfPositiveAnswersToEachAnswer.TOPLEFT}/{questionEvaluation?.amountOfAnswersTotal}</div> + </CustomTooltip> + </Grid> + </Grid> + <Grid item xs={6}> + <Grid container direction={"row"}> + <TextField + {...textFieldStaticProps} + className={classes.answer} + //if the user is answering to the question in the session, he should be able to toggle answerCorrect when he clicks on the whole text field + value={currentQuestion?.topRightAnswer.value} + inputProps={{style: { textAlign: 'center' }}} + sx={{ backgroundColor: "#B456EB", flexGrow: 1 }} + InputProps = {{ + endAdornment: <AdornmentCustom id="topRightAnswer"/> + }} + /> + <CustomTooltip> + <div className={"answerScore"}>{questionEvaluation?.amountsOfPositiveAnswersToEachAnswer.TOPRIGHT}/{questionEvaluation?.amountOfAnswersTotal}</div> + </CustomTooltip> + </Grid> + </Grid> + <Grid item xs={6}> + <Grid container direction={"row"}> + <TextField + {...textFieldStaticProps} + className={classes.answer} + value={currentQuestion?.bottomLeftAnswer.value} + inputProps={{style: { textAlign: 'center' }}} + sx={{ backgroundColor: "#EB9B56", flexGrow:1 }} + InputProps = {{ + endAdornment: <AdornmentCustom id="bottomLeftAnswer"/> + }} + /> + <CustomTooltip> + <div className={"answerScore"}>{questionEvaluation?.amountsOfPositiveAnswersToEachAnswer.BOTTOMLEFT}/{questionEvaluation?.amountOfAnswersTotal}</div> + </CustomTooltip> + </Grid> + </Grid> + <Grid item xs={6}> + <Grid container direction={"row"}> + <TextField + {...textFieldStaticProps} + className={classes.answer} + value={currentQuestion?.bottomRightAnswer.value} + inputProps={{style: { textAlign: 'center' }}} + sx={{ backgroundColor: "#FFFF99", flexGrow: 1 }} + InputProps = {{ + endAdornment: <AdornmentCustom id="bottomRightAnswer"/> + }} + /> + <CustomTooltip> + <div className={"answerScore"}>{questionEvaluation?.amountsOfPositiveAnswersToEachAnswer.BOTTOMRIGHT}/{questionEvaluation?.amountOfAnswersTotal}</div> + </CustomTooltip> + </Grid> + </Grid> + </Grid> + </> + ) +} + +export default AnswersEvaluation \ No newline at end of file diff --git a/ui/src/pages/quizSession/teacher/questionEvaluation/index.css b/ui/src/pages/quizSession/teacher/questionEvaluation/index.css index 700061426aea026ac983edb1ec5e5ecde1af3532..4d7ee81ddce88b08b0ceaf449f428f9943957fba 100644 --- a/ui/src/pages/quizSession/teacher/questionEvaluation/index.css +++ b/ui/src/pages/quizSession/teacher/questionEvaluation/index.css @@ -24,36 +24,60 @@ min-height: 230px; } +.answersWrapper { + width: 80%; + height: 25%; + min-height: 225px; + display: flex; + align-items: center; +} + .buttonWrapper { display: flex; flex-grow: 1; justify-content: center; align-items: center; + flex-direction: column; } -@media (orientation: portrait){ - .questionNameWrapper { - width: 95%; +.barWrapper { + width: 40%; + height: 50px; + padding-right: 10%; +} + +.answerScore { + border: 2px solid black; + font-weight: bold; + border-left: none; + display: flex; + align-items: center; + padding: 0 3px 0 3px; + border-top-right-radius: 4px; + border-bottom-right-radius: 4px; + margin-left: -1.5px; +} + +@media (max-width: 640px){ + .barWrapper { + display: none; } - .editorWrapper { +} + +@media (orientation: portrait){ + .editorWrapper.answersWrapper.questionNameWrapper { width: 95% } } @media (min-width: 1100px){ - .questionNameWrapper { - width: 70%; - } - .editorWrapper { + .editorWrapper.answersWrapper.questionNameWrapper { width: 70% } } @media (min-width: 1300px){ - .questionNameWrapper { - width: 60%; - } - .editorWrapper { + .editorWrapper.answersWrapper.questionNameWrapper { width: 60% } } \ No newline at end of file diff --git a/ui/src/pages/quizSession/teacher/studentResults/StudentResults.tsx b/ui/src/pages/quizSession/teacher/studentResults/StudentResults.tsx new file mode 100644 index 0000000000000000000000000000000000000000..f5aae0fb99b3afbe6facf2cb747c935929e0480c --- /dev/null +++ b/ui/src/pages/quizSession/teacher/studentResults/StudentResults.tsx @@ -0,0 +1,81 @@ +import React from "react"; +import MaterialTable from 'material-table' +import './index.css' +import { Button } from "@mui/material"; + +import { StudentScores as StudentScoresType } from '../StartQuiz' + +interface StudentResultsProps { + studentScores: StudentScoresType | null; + handleNextQuestionButton: () => void; +} + +interface StudentInTable { + position: number; + name: string; + score: number +} + +//creates an array of top 3 students +const getTopThree = (studentScores: StudentScoresType): Array<StudentInTable> => { + if(studentScores === null){ + return []; + } + let studentArray: Array<StudentInTable> = [] + let position = 1 + + const studentScoresAmount = Object.keys(studentScores).length + + for(let i = 0; i <= (studentScoresAmount > 3 ? 2 : studentScoresAmount); i++){ + let mostSuccessfulStudent: StudentInTable | null = null + for (const [key, value] of Object.entries(studentScores)){ + if(mostSuccessfulStudent === null){ + mostSuccessfulStudent = {position: position, name: key, score: value} + } + else if(mostSuccessfulStudent.score <= value){ + mostSuccessfulStudent = {position: position, name: key, score: value} + } + } + if(mostSuccessfulStudent !== null) { + studentArray.push(mostSuccessfulStudent) + delete studentScores[mostSuccessfulStudent.name] + } + position++ + } + return studentArray +} + +const StudentResults = (props: StudentResultsProps) => { + const tableScores = getTopThree(props.studentScores ? props.studentScores : {}) + + return ( + <> + <div className="studentResultsWrapper"> + <div className="tableWrapper"> + <MaterialTable + options={{ + search: false, + paging: false + }} + columns={[ + { title: 'Position', field: 'position'}, + { title: 'Student', field: 'name' }, + { title: 'Score', field: 'score' }, + ]} + data={tableScores} + title="Student's scores" + /> + </div> + <Button + variant='contained' + color='primary' + onClick = {props.handleNextQuestionButton} + > + End the quiz + </Button> + </div> + </> + ) +} + +export default StudentResults \ No newline at end of file diff --git a/ui/src/pages/quizSession/teacher/studentResults/index.css b/ui/src/pages/quizSession/teacher/studentResults/index.css new file mode 100644 index 0000000000000000000000000000000000000000..50b56a494799c937e680b21e3c993402941ddf18 --- /dev/null +++ b/ui/src/pages/quizSession/teacher/studentResults/index.css @@ -0,0 +1,37 @@ +.studentResultsWrapper { + height: calc(100vh - 45px); + width: 100%; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; +} + +.tableWrapper { + height: 350px; + width: 35%; +} + +@media (max-width: 1300px){ + .tableWrapper { + width: 45%; + } +} + +@media (max-width: 1100px){ + .tableWrapper { + width: 50%; + } +} + +@media (max-width: 800px){ + .tableWrapper { + width: 65%; + } +} + +@media (max-width: 500px){ + .tableWrapper { + width: 80%; + } +} \ No newline at end of file