From 8499be665acab3608224b0745685801c92673e8d Mon Sep 17 00:00:00 2001 From: Dario Date: Mon, 8 Apr 2024 15:57:35 +0200 Subject: [PATCH 01/18] refactor: find random question and added error handling --- .../exceptions/CustomControllerAdvice.java | 7 ++++- .../exceptions/InternalApiErrorException.java | 7 +++++ .../questions/question/QuestionService.java | 28 +++++++++++++------ 3 files changed, 32 insertions(+), 10 deletions(-) create mode 100644 api/src/main/java/lab/en2b/quizapi/commons/exceptions/InternalApiErrorException.java diff --git a/api/src/main/java/lab/en2b/quizapi/commons/exceptions/CustomControllerAdvice.java b/api/src/main/java/lab/en2b/quizapi/commons/exceptions/CustomControllerAdvice.java index 8caa8b61..07fed185 100644 --- a/api/src/main/java/lab/en2b/quizapi/commons/exceptions/CustomControllerAdvice.java +++ b/api/src/main/java/lab/en2b/quizapi/commons/exceptions/CustomControllerAdvice.java @@ -18,6 +18,11 @@ @Log4j2 @Order(Ordered.HIGHEST_PRECEDENCE) public class CustomControllerAdvice extends ResponseEntityExceptionHandler { + @ExceptionHandler(InternalApiErrorException.class) + public ResponseEntity handleInternalApiErrorException(InternalApiErrorException exception){ + log.error(exception.getMessage(),exception); + return new ResponseEntity<>(exception.getMessage(),HttpStatus.SERVICE_UNAVAILABLE); + } @ExceptionHandler(InvalidAuthenticationException.class) public ResponseEntity handleInvalidAuthenticationException(InvalidAuthenticationException exception){ log.error(exception.getMessage(),exception); @@ -60,7 +65,7 @@ public ResponseEntity handleInternalAuthenticationServiceException(Inter @ExceptionHandler(Exception.class) public ResponseEntity handleException(Exception exception){ log.error(exception.getMessage(),exception); - return new ResponseEntity<>(exception.getMessage(),HttpStatus.INTERNAL_SERVER_ERROR); + return new ResponseEntity<>("Internal Server Error",HttpStatus.INTERNAL_SERVER_ERROR); } } diff --git a/api/src/main/java/lab/en2b/quizapi/commons/exceptions/InternalApiErrorException.java b/api/src/main/java/lab/en2b/quizapi/commons/exceptions/InternalApiErrorException.java new file mode 100644 index 00000000..595be51b --- /dev/null +++ b/api/src/main/java/lab/en2b/quizapi/commons/exceptions/InternalApiErrorException.java @@ -0,0 +1,7 @@ +package lab.en2b.quizapi.commons.exceptions; + +public class InternalApiErrorException extends RuntimeException{ + public InternalApiErrorException(String message) { + super(message); + } +} diff --git a/api/src/main/java/lab/en2b/quizapi/questions/question/QuestionService.java b/api/src/main/java/lab/en2b/quizapi/questions/question/QuestionService.java index 498e5c8e..264e3605 100644 --- a/api/src/main/java/lab/en2b/quizapi/questions/question/QuestionService.java +++ b/api/src/main/java/lab/en2b/quizapi/questions/question/QuestionService.java @@ -1,5 +1,6 @@ package lab.en2b.quizapi.questions.question; +import lab.en2b.quizapi.commons.exceptions.InternalApiErrorException; import lab.en2b.quizapi.questions.answer.Answer; import lab.en2b.quizapi.questions.answer.AnswerRepository; import lab.en2b.quizapi.questions.answer.dtos.AnswerDto; @@ -21,6 +22,12 @@ public class QuestionService { private final QuestionRepository questionRepository; private final QuestionResponseDtoMapper questionResponseDtoMapper; + /** + * Answer a question + * @param id The id of the question + * @param answerDto The answer dto + * @return The response dto + */ public AnswerCheckResponseDto answerQuestion(Long id, AnswerDto answerDto) { Question question = questionRepository.findById(id).orElseThrow(); if(question.getCorrectAnswer().getId().equals(answerDto.getAnswerId())){ @@ -35,20 +42,22 @@ else if(question.getAnswers().stream().noneMatch(i -> i.getId().equals(answerDto } public QuestionResponseDto getRandomQuestion(String lang) { - if (lang==null || lang.isBlank()) { - lang = "en"; - } - Question q = questionRepository.findRandomQuestion(lang); - loadAnswers(q); - - return questionResponseDtoMapper.apply(q); + return questionResponseDtoMapper.apply(findRandomQuestion(lang)); } - + + /** + * Find a random question for the specified language + * @param lang The language to find the question for + * @return The random question + */ public Question findRandomQuestion(String lang){ if (lang==null || lang.isBlank()) { lang = "en"; } Question q = questionRepository.findRandomQuestion(lang); + if(q==null) { + throw new InternalApiErrorException("No questions found for the specified language!"); + } loadAnswers(q); return q; } @@ -62,7 +71,8 @@ public QuestionResponseDto getQuestionById(Long id) { * Load the answers for a question (The distractors and the correct one) * @param question The question to load the answers for */ - public void loadAnswers(Question question) { + //TODO: CHAPUZAS, FIXEAR ESTO + private void loadAnswers(Question question) { // Create the new answers list with the distractors List answers = new ArrayList<>(QuestionHelper.getDistractors(answerRepository, question)); // Add the correct From 8e0fde2ebed9ec045db4f1d8a11f409d7d1858a0 Mon Sep 17 00:00:00 2001 From: Dario Date: Mon, 8 Apr 2024 16:04:11 +0200 Subject: [PATCH 02/18] fix: descriptive error code for illegal state --- .../quizapi/commons/exceptions/CustomControllerAdvice.java | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/api/src/main/java/lab/en2b/quizapi/commons/exceptions/CustomControllerAdvice.java b/api/src/main/java/lab/en2b/quizapi/commons/exceptions/CustomControllerAdvice.java index 07fed185..b932d268 100644 --- a/api/src/main/java/lab/en2b/quizapi/commons/exceptions/CustomControllerAdvice.java +++ b/api/src/main/java/lab/en2b/quizapi/commons/exceptions/CustomControllerAdvice.java @@ -33,7 +33,11 @@ public ResponseEntity handleNoSuchElementException(NoSuchElementExceptio log.error(exception.getMessage(),exception); return new ResponseEntity<>(exception.getMessage(),HttpStatus.NOT_FOUND); } - + @ExceptionHandler(IllegalStateException.class) + public ResponseEntity handleIllegalStateException(IllegalStateException exception){ + log.error(exception.getMessage(),exception); + return new ResponseEntity<>(exception.getMessage(),HttpStatus.CONFLICT); + } @ExceptionHandler(IllegalArgumentException.class) public ResponseEntity handleIllegalArgumentException(IllegalArgumentException exception){ log.error(exception.getMessage(),exception); From ea1b18e458d1794b1596e637baf8484a8b437555 Mon Sep 17 00:00:00 2001 From: Dario Date: Mon, 8 Apr 2024 16:11:02 +0200 Subject: [PATCH 03/18] fix: error when asking for question with no round started --- api/src/main/java/lab/en2b/quizapi/game/Game.java | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/api/src/main/java/lab/en2b/quizapi/game/Game.java b/api/src/main/java/lab/en2b/quizapi/game/Game.java index 686c0a68..d509fc07 100644 --- a/api/src/main/java/lab/en2b/quizapi/game/Game.java +++ b/api/src/main/java/lab/en2b/quizapi/game/Game.java @@ -79,6 +79,9 @@ public boolean isLastRound(){ } public Question getCurrentQuestion() { + if(getRoundStartTime() == null){ + throw new IllegalStateException("The round is not active!"); + } if(currentRoundIsOver()) throw new IllegalStateException("The current round is over!"); if(isGameOver()) @@ -91,7 +94,7 @@ private boolean currentRoundIsOver(){ } private boolean roundTimeHasExpired(){ - return LocalDateTime.now().isAfter(getRoundStartTime().plusSeconds(getRoundDuration())); + return getRoundStartTime()!= null && LocalDateTime.now().isAfter(getRoundStartTime().plusSeconds(getRoundDuration())); } public void answerQuestion(Long answerId, QuestionRepository questionRepository){ From d1a1931371b59288ed370872b30155a966b0e7b2 Mon Sep 17 00:00:00 2001 From: Dario Date: Mon, 8 Apr 2024 16:26:36 +0200 Subject: [PATCH 04/18] refactor: game vars now are long --- .../main/java/lab/en2b/quizapi/game/Game.java | 6 +-- .../lab/en2b/quizapi/game/GameService.java | 48 ++++++++++--------- .../quizapi/game/dtos/GameResponseDto.java | 8 ++-- .../en2b/quizapi/game/GameServiceTest.java | 18 +++---- 4 files changed, 42 insertions(+), 38 deletions(-) diff --git a/api/src/main/java/lab/en2b/quizapi/game/Game.java b/api/src/main/java/lab/en2b/quizapi/game/Game.java index d509fc07..dc53836e 100644 --- a/api/src/main/java/lab/en2b/quizapi/game/Game.java +++ b/api/src/main/java/lab/en2b/quizapi/game/Game.java @@ -25,10 +25,10 @@ public class Game { @Setter(AccessLevel.NONE) private Long id; - private int rounds = 9; - private int actualRound = 0; + private Long rounds = 9L; + private Long actualRound = 0L; - private int correctlyAnsweredQuestions = 0; + private Long correctlyAnsweredQuestions = 0L; private String language; private LocalDateTime roundStartTime; @NonNull diff --git a/api/src/main/java/lab/en2b/quizapi/game/GameService.java b/api/src/main/java/lab/en2b/quizapi/game/GameService.java index e3e47e66..f95a2fb0 100644 --- a/api/src/main/java/lab/en2b/quizapi/game/GameService.java +++ b/api/src/main/java/lab/en2b/quizapi/game/GameService.java @@ -14,6 +14,7 @@ import lombok.RequiredArgsConstructor; import org.springframework.security.core.Authentication; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; import java.util.ArrayList; import java.util.Arrays; @@ -29,19 +30,20 @@ public class GameService { private final QuestionRepository questionRepository; private final QuestionResponseDtoMapper questionResponseDtoMapper; private final StatisticsRepository statisticsRepository; + public GameResponseDto newGame(Authentication authentication) { if (gameRepository.findActiveGameForUser(userService.getUserByAuthentication(authentication).getId()).isPresent()){ return gameResponseDtoMapper.apply(gameRepository.findActiveGameForUser(userService.getUserByAuthentication(authentication).getId()).get()); } - Game g = gameRepository.save(Game.builder() + return gameResponseDtoMapper.apply(gameRepository.save(Game.builder() .user(userService.getUserByAuthentication(authentication)) .questions(new ArrayList<>()) - .rounds(9) - .correctlyAnsweredQuestions(0) + .rounds(9L) + .actualRound(0L) + .correctlyAnsweredQuestions(0L) .roundDuration(30) .language("en") - .build()); - return gameResponseDtoMapper.apply(g); + .build())); } public GameResponseDto startRound(Long id, Authentication authentication) { @@ -63,28 +65,28 @@ public GameResponseDto answerQuestion(Long id, GameAnswerDto dto, Authentication if (game.isLastRound()){ game.setGameOver(true); gameRepository.save(game); - } - if (game.isGameOver()){ - if (statisticsRepository.findByUserId(game.getUser().getId()).isPresent()){ - Statistics statistics = statisticsRepository.findByUserId(game.getUser().getId()).get(); - statistics.updateStatistics(Long.valueOf(game.getCorrectlyAnsweredQuestions()), - Long.valueOf(game.getQuestions().size()-game.getCorrectlyAnsweredQuestions()), - Long.valueOf(game.getRounds())); - statisticsRepository.save(statistics); - } else { - Statistics statistics = Statistics.builder() - .user(game.getUser()) - .correct(Long.valueOf(game.getCorrectlyAnsweredQuestions())) - .wrong(Long.valueOf(game.getQuestions().size()-game.getCorrectlyAnsweredQuestions())) - .total(Long.valueOf(game.getRounds())) - .build(); - statisticsRepository.save(statistics); - } + saveStatistics(game); } return gameResponseDtoMapper.apply(game); } - + private void saveStatistics(Game game){ + if (statisticsRepository.findByUserId(game.getUser().getId()).isPresent()){ + Statistics statistics = statisticsRepository.findByUserId(game.getUser().getId()).get(); + statistics.updateStatistics(game.getCorrectlyAnsweredQuestions(), + game.getQuestions().size()-game.getCorrectlyAnsweredQuestions(), + game.getRounds()); + statisticsRepository.save(statistics); + } else { + Statistics statistics = Statistics.builder() + .user(game.getUser()) + .correct(game.getCorrectlyAnsweredQuestions()) + .wrong(game.getQuestions().size()-game.getCorrectlyAnsweredQuestions()) + .total(game.getRounds()) + .build(); + statisticsRepository.save(statistics); + } + } public GameResponseDto changeLanguage(Long id, String language, Authentication authentication) { Game game = gameRepository.findByIdForUser(id, userService.getUserByAuthentication(authentication).getId()).orElseThrow(); game.setLanguage(language); diff --git a/api/src/main/java/lab/en2b/quizapi/game/dtos/GameResponseDto.java b/api/src/main/java/lab/en2b/quizapi/game/dtos/GameResponseDto.java index e7b680bf..c2f979c4 100644 --- a/api/src/main/java/lab/en2b/quizapi/game/dtos/GameResponseDto.java +++ b/api/src/main/java/lab/en2b/quizapi/game/dtos/GameResponseDto.java @@ -22,14 +22,14 @@ public class GameResponseDto { private UserResponseDto user; @Schema(description = "Total rounds for the game", example = "9") - private int rounds; + private Long rounds; @Schema(description = "Actual round for the game", example = "3") - private int actualRound; + private Long actualRound; @Schema(description = "Number of correct answered questions", example = "2") @JsonProperty("correctly_answered_questions") - private int correctlyAnsweredQuestions; + private Long correctlyAnsweredQuestions; @Schema(description = "Moment when the timer has started", example = "LocalDateTime.now()") @JsonProperty("round_start_time") @@ -37,7 +37,7 @@ public class GameResponseDto { @Schema(description = "Number of seconds for the player to answer the question", example = "20") @JsonProperty("round_duration") - private int roundDuration; + private Integer roundDuration; @Schema(description = "Whether the game has finished or not", example = "true") private boolean isGameOver; diff --git a/api/src/test/java/lab/en2b/quizapi/game/GameServiceTest.java b/api/src/test/java/lab/en2b/quizapi/game/GameServiceTest.java index 6913c942..839bf246 100644 --- a/api/src/test/java/lab/en2b/quizapi/game/GameServiceTest.java +++ b/api/src/test/java/lab/en2b/quizapi/game/GameServiceTest.java @@ -133,16 +133,18 @@ void setUp() { LocalDateTime now = LocalDateTime.now(); this.defaultGameResponseDto = GameResponseDto.builder() .user(defaultUserResponseDto) - .rounds(9) - .correctlyAnsweredQuestions(0) + .rounds(9L) + .correctlyAnsweredQuestions(0L) + .actualRound(0L) .roundDuration(30) .build(); this.defaultGame = Game.builder() .id(1L) .user(defaultUser) .questions(new ArrayList<>()) - .rounds(9) - .correctlyAnsweredQuestions(0) + .rounds(9L) + .actualRound(0L) + .correctlyAnsweredQuestions(0L) .language("en") .roundDuration(30) .build(); @@ -166,7 +168,7 @@ public void startRound(){ when(userService.getUserByAuthentication(authentication)).thenReturn(defaultUser); GameResponseDto gameDto = gameService.startRound(1L, authentication); GameResponseDto result = defaultGameResponseDto; - result.setActualRound(1); + result.setActualRound(1L); result.setId(1L); result.setRoundStartTime(defaultGame.getRoundStartTime()); assertEquals(result, gameDto); @@ -177,7 +179,7 @@ public void startRoundGameOver(){ when(gameRepository.findByIdForUser(any(), any())).thenReturn(Optional.of(defaultGame)); when(questionService.findRandomQuestion(any())).thenReturn(defaultQuestion); when(userService.getUserByAuthentication(authentication)).thenReturn(defaultUser); - defaultGame.setActualRound(10); + defaultGame.setActualRound(10L); assertThrows(IllegalStateException.class, () -> gameService.startRound(1L,authentication)); } @@ -229,7 +231,7 @@ public void getCurrentQuestionGameFinished() { when(gameRepository.save(any())).thenAnswer(invocation -> invocation.getArgument(0)); when(questionService.findRandomQuestion(any())).thenReturn(defaultQuestion); gameService.startRound(1L,authentication); - defaultGame.setActualRound(10); + defaultGame.setActualRound(10L); assertThrows(IllegalStateException.class, () -> gameService.getCurrentQuestion(1L,authentication)); } @@ -269,7 +271,7 @@ public void answerQuestionWhenGameHasFinished(){ when(questionService.findRandomQuestion(any())).thenReturn(defaultQuestion); gameService.newGame(authentication); gameService.startRound(1L, authentication); - defaultGame.setActualRound(30); + defaultGame.setActualRound(30L); assertThrows(IllegalStateException.class, () -> gameService.answerQuestion(1L, new GameAnswerDto(1L), authentication)); } From 9a3ee1664e0b6ea1b22b0dfbd07d710a4088b483 Mon Sep 17 00:00:00 2001 From: Dario Date: Mon, 8 Apr 2024 16:31:07 +0200 Subject: [PATCH 05/18] fix: transactional new game, new round and answer --- api/src/main/java/lab/en2b/quizapi/game/GameService.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/api/src/main/java/lab/en2b/quizapi/game/GameService.java b/api/src/main/java/lab/en2b/quizapi/game/GameService.java index f95a2fb0..a64b7d76 100644 --- a/api/src/main/java/lab/en2b/quizapi/game/GameService.java +++ b/api/src/main/java/lab/en2b/quizapi/game/GameService.java @@ -31,6 +31,7 @@ public class GameService { private final QuestionResponseDtoMapper questionResponseDtoMapper; private final StatisticsRepository statisticsRepository; + @Transactional public GameResponseDto newGame(Authentication authentication) { if (gameRepository.findActiveGameForUser(userService.getUserByAuthentication(authentication).getId()).isPresent()){ return gameResponseDtoMapper.apply(gameRepository.findActiveGameForUser(userService.getUserByAuthentication(authentication).getId()).get()); @@ -46,6 +47,7 @@ public GameResponseDto newGame(Authentication authentication) { .build())); } + @Transactional public GameResponseDto startRound(Long id, Authentication authentication) { Game game = gameRepository.findByIdForUser(id, userService.getUserByAuthentication(authentication).getId()).orElseThrow(); game.newRound(questionService.findRandomQuestion(game.getLanguage())); @@ -58,6 +60,7 @@ public QuestionResponseDto getCurrentQuestion(Long id, Authentication authentica return questionResponseDtoMapper.apply(game.getCurrentQuestion()); } + @Transactional public GameResponseDto answerQuestion(Long id, GameAnswerDto dto, Authentication authentication){ Game game = gameRepository.findByIdForUser(id, userService.getUserByAuthentication(authentication).getId()).orElseThrow(); game.answerQuestion(dto.getAnswerId(), questionRepository); From c8303bccfaa30597f3013552e712106abe7c1ced Mon Sep 17 00:00:00 2001 From: Dario Date: Mon, 8 Apr 2024 16:31:32 +0200 Subject: [PATCH 06/18] fix: save statistics after game over now if game is checked --- .../main/java/lab/en2b/quizapi/game/GameService.java | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/api/src/main/java/lab/en2b/quizapi/game/GameService.java b/api/src/main/java/lab/en2b/quizapi/game/GameService.java index a64b7d76..6d0f1ea7 100644 --- a/api/src/main/java/lab/en2b/quizapi/game/GameService.java +++ b/api/src/main/java/lab/en2b/quizapi/game/GameService.java @@ -65,7 +65,7 @@ public GameResponseDto answerQuestion(Long id, GameAnswerDto dto, Authentication Game game = gameRepository.findByIdForUser(id, userService.getUserByAuthentication(authentication).getId()).orElseThrow(); game.answerQuestion(dto.getAnswerId(), questionRepository); - if (game.isLastRound()){ + if (game.isLastRound() && !game.isGameOver()){ game.setGameOver(true); gameRepository.save(game); saveStatistics(game); @@ -97,7 +97,13 @@ public GameResponseDto changeLanguage(Long id, String language, Authentication a } public GameResponseDto getGameDetails(Long id, Authentication authentication) { - return gameResponseDtoMapper.apply(gameRepository.findByIdForUser(id, userService.getUserByAuthentication(authentication).getId()).orElseThrow()); + Game game = gameRepository.findByIdForUser(id, userService.getUserByAuthentication(authentication).getId()).orElseThrow(); + if (game.isLastRound() && !game.isGameOver()){ + game.setGameOver(true); + gameRepository.save(game); + saveStatistics(game); + } + return gameResponseDtoMapper.apply(game); } public List getQuestionCategories() { From 6f9ca42d21074f1d14fa889f38ccb1f897d3826d Mon Sep 17 00:00:00 2001 From: Dario Date: Mon, 8 Apr 2024 16:31:49 +0200 Subject: [PATCH 07/18] fix: cannot change language after game over --- api/src/main/java/lab/en2b/quizapi/game/GameService.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/api/src/main/java/lab/en2b/quizapi/game/GameService.java b/api/src/main/java/lab/en2b/quizapi/game/GameService.java index 6d0f1ea7..c296ed5e 100644 --- a/api/src/main/java/lab/en2b/quizapi/game/GameService.java +++ b/api/src/main/java/lab/en2b/quizapi/game/GameService.java @@ -92,6 +92,9 @@ private void saveStatistics(Game game){ } public GameResponseDto changeLanguage(Long id, String language, Authentication authentication) { Game game = gameRepository.findByIdForUser(id, userService.getUserByAuthentication(authentication).getId()).orElseThrow(); + if(game.isGameOver()){ + throw new IllegalStateException("Cannot change language after the game is over!"); + } game.setLanguage(language); return gameResponseDtoMapper.apply(gameRepository.save(game)); } From fe37fc20a389de0b3a0ca9bcbb53b3ff315be6eb Mon Sep 17 00:00:00 2001 From: Dario Date: Mon, 8 Apr 2024 16:40:01 +0200 Subject: [PATCH 08/18] test: language change when game over --- .../main/java/lab/en2b/quizapi/game/Game.java | 2 +- .../lab/en2b/quizapi/game/GameServiceTest.java | 18 ++++++++++++++++-- 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/api/src/main/java/lab/en2b/quizapi/game/Game.java b/api/src/main/java/lab/en2b/quizapi/game/Game.java index dc53836e..a1f3ecdb 100644 --- a/api/src/main/java/lab/en2b/quizapi/game/Game.java +++ b/api/src/main/java/lab/en2b/quizapi/game/Game.java @@ -71,7 +71,7 @@ private void increaseRound(){ } public boolean isGameOver(){ - return getActualRound() > getRounds(); + return isGameOver || getActualRound() > getRounds(); } public boolean isLastRound(){ diff --git a/api/src/test/java/lab/en2b/quizapi/game/GameServiceTest.java b/api/src/test/java/lab/en2b/quizapi/game/GameServiceTest.java index 839bf246..e5a4cd5b 100644 --- a/api/src/test/java/lab/en2b/quizapi/game/GameServiceTest.java +++ b/api/src/test/java/lab/en2b/quizapi/game/GameServiceTest.java @@ -303,10 +303,24 @@ public void changeLanguage(){ when(gameRepository.findByIdForUser(any(), any())).thenReturn(Optional.of(defaultGame)); when(gameRepository.save(any())).thenAnswer(invocation -> invocation.getArgument(0)); when(userService.getUserByAuthentication(authentication)).thenReturn(defaultUser); - GameResponseDto gameDto = gameService.newGame(authentication); + gameService.newGame(authentication); gameService.startRound(1L, authentication); gameService.changeLanguage(1L, "es", authentication); - assertEquals(defaultGameResponseDto, gameDto); + gameService.getGameDetails(1L, authentication); + assertEquals("es",defaultGame.getLanguage()); + } + + @Test + public void changeLanguageGameOver(){ + when(gameRepository.findByIdForUser(any(), any())).thenReturn(Optional.of(defaultGame)); + when(gameRepository.save(any())).thenAnswer(invocation -> invocation.getArgument(0)); + when(userService.getUserByAuthentication(authentication)).thenReturn(defaultUser); + + gameService.newGame(authentication); + gameService.startRound(1L, authentication); + defaultGame.setGameOver(true); + assertThrows(IllegalStateException.class,() -> gameService.changeLanguage(1L, "es", authentication)); + } @Test From fe12dca2ce45de74c16df9a932c43d0b59013f9f Mon Sep 17 00:00:00 2001 From: Dario Date: Mon, 8 Apr 2024 17:15:11 +0200 Subject: [PATCH 09/18] fix: statistics are created when not there for user --- .../en2b/quizapi/statistics/Statistics.java | 3 +++ .../quizapi/statistics/StatisticsService.java | 24 ++++++++++++++++--- 2 files changed, 24 insertions(+), 3 deletions(-) diff --git a/api/src/main/java/lab/en2b/quizapi/statistics/Statistics.java b/api/src/main/java/lab/en2b/quizapi/statistics/Statistics.java index 015ea2b7..5edba8f7 100644 --- a/api/src/main/java/lab/en2b/quizapi/statistics/Statistics.java +++ b/api/src/main/java/lab/en2b/quizapi/statistics/Statistics.java @@ -32,6 +32,9 @@ public class Statistics { private User user; public Long getCorrectRate() { + if(total == 0){ + return 0L; + } return (correct * 100) / total; } diff --git a/api/src/main/java/lab/en2b/quizapi/statistics/StatisticsService.java b/api/src/main/java/lab/en2b/quizapi/statistics/StatisticsService.java index 49110478..e067afba 100644 --- a/api/src/main/java/lab/en2b/quizapi/statistics/StatisticsService.java +++ b/api/src/main/java/lab/en2b/quizapi/statistics/StatisticsService.java @@ -1,5 +1,6 @@ package lab.en2b.quizapi.statistics; +import lab.en2b.quizapi.commons.user.User; import lab.en2b.quizapi.commons.user.UserService; import lab.en2b.quizapi.statistics.dtos.StatisticsResponseDto; import lab.en2b.quizapi.statistics.mappers.StatisticsResponseDtoMapper; @@ -10,6 +11,7 @@ import java.util.ArrayList; import java.util.Comparator; import java.util.List; +import java.util.Optional; import java.util.stream.Collectors; @Service @@ -20,15 +22,31 @@ public class StatisticsService { private final UserService userService; private final StatisticsResponseDtoMapper statisticsResponseDtoMapper; + /** + * Updates the statistics for a user. If no statistics are found for the user, they are created. + * @param authentication the user to get the statistics for + * @return the retrieved or created statistics + */ public StatisticsResponseDto getStatisticsForUser(Authentication authentication){ - return statisticsResponseDtoMapper.apply(statisticsRepository.findByUserId(userService. - getUserByAuthentication(authentication).getId()).orElseThrow()); + User user = userService.getUserByAuthentication(authentication); + Optional statistics = statisticsRepository.findByUserId(user.getId()); + + if (statistics.isEmpty()){ + return statisticsResponseDtoMapper.apply(statisticsRepository.save(Statistics.builder() + .user(user) + .correct(0L) + .wrong(0L) + .total(0L) + .build())); + } + + return statisticsResponseDtoMapper.apply(statistics.get()); } public List getTopTenStatistics(){ List all = new ArrayList<>(statisticsRepository.findAll()); all.sort(Comparator.comparing(Statistics::getCorrectRate).reversed()); - List topTen = all.stream().limit(10).collect(Collectors.toList()); + List topTen = all.stream().limit(10).toList(); return topTen.stream().map(statisticsResponseDtoMapper).collect(Collectors.toList()); } From b7f0e3f0f5508bf5490cc957b06efbdc59f5a012 Mon Sep 17 00:00:00 2001 From: Dario Date: Mon, 8 Apr 2024 17:41:22 +0200 Subject: [PATCH 10/18] fix: statistics --- .../components/statistics/UserStatistics.jsx | 14 +++++++++----- webapp/src/pages/Statistics.jsx | 19 ++++++++++++------- 2 files changed, 21 insertions(+), 12 deletions(-) diff --git a/webapp/src/components/statistics/UserStatistics.jsx b/webapp/src/components/statistics/UserStatistics.jsx index 172bdcfd..f1b36b86 100644 --- a/webapp/src/components/statistics/UserStatistics.jsx +++ b/webapp/src/components/statistics/UserStatistics.jsx @@ -2,7 +2,7 @@ import { Box, Flex, Heading, Stack, Text, CircularProgress } from "@chakra-ui/re import { HttpStatusCode } from "axios"; import ErrorMessageAlert from "components/ErrorMessageAlert"; import AuthManager from "components/auth/AuthManager"; -import React, { useState } from "react"; +import React, {useEffect, useState} from "react"; import { useTranslation } from "react-i18next"; import { Cell, Pie, PieChart } from "recharts"; @@ -21,14 +21,14 @@ export default function UserStatistics() { "raw": [ { "name": t("statistics.texts.personalRight"), - "value": request.data.correct + "value": request.data.right }, { "name": t("statistics.texts.personalWrong"), "value": request.data.wrong } ], - "rate": request.data.correctRate + "rate": request.data.correct_rate }); setRetrievedData(true); } else { @@ -50,8 +50,12 @@ export default function UserStatistics() { setErrorMessage(errorType); } } - - return { + if(!retrievedData){ + getData(); + } + }); + return { retrievedData ? diff --git a/webapp/src/pages/Statistics.jsx b/webapp/src/pages/Statistics.jsx index 7d58ddb6..c59f32bc 100644 --- a/webapp/src/pages/Statistics.jsx +++ b/webapp/src/pages/Statistics.jsx @@ -1,6 +1,6 @@ import { Box, Center, Heading, Stack, StackDivider, Table, Tbody, Text, Td, Th, Thead, Tr, CircularProgress} from "@chakra-ui/react"; -import React, { useState } from "react"; +import React, {useEffect, useState} from "react"; import { useTranslation } from "react-i18next"; import GoBack from "components/GoBack"; import AuthManager from "components/auth/AuthManager"; @@ -18,6 +18,7 @@ export default function Statistics() { const [errorMessage, setErrorMessage] = useState(null); const getData = async () => { + console.log('lmao') try { const request = await new AuthManager().getAxiosInstance() .get(process.env.REACT_APP_API_ENDPOINT + "/statistics/top"); @@ -48,15 +49,19 @@ export default function Statistics() { return topTen.map((element, counter) => { return {counter + 1} - {element.username} - {element.correct} + {element.user.username} + {element.right} {element.wrong} {element.total} - {element.rate}% + {element.correct_rate}% }); - } - + } + useEffect(() => { + if(!retrievedData){ + getData(); + } + }); const [isMenuOpen, setIsMenuOpen] = useState(false); @@ -65,7 +70,7 @@ export default function Statistics() { }; return ( -
setIsMenuOpen(true)}/> From 063185165f6a6e93b0fcf02294ecacf90c2695b0 Mon Sep 17 00:00:00 2001 From: Dario Date: Mon, 8 Apr 2024 20:29:02 +0200 Subject: [PATCH 11/18] fix: game over --- .../main/java/lab/en2b/quizapi/game/Game.java | 12 ++++++---- .../lab/en2b/quizapi/game/GameService.java | 23 +++++++++++++++---- .../quizapi/game/dtos/GameResponseDto.java | 1 + .../en2b/quizapi/game/GameServiceTest.java | 3 +++ 4 files changed, 30 insertions(+), 9 deletions(-) diff --git a/api/src/main/java/lab/en2b/quizapi/game/Game.java b/api/src/main/java/lab/en2b/quizapi/game/Game.java index a1f3ecdb..fa75cf5c 100644 --- a/api/src/main/java/lab/en2b/quizapi/game/Game.java +++ b/api/src/main/java/lab/en2b/quizapi/game/Game.java @@ -56,8 +56,8 @@ public void newRound(Question question){ if(getActualRound() != 0){ if (isGameOver()) throw new IllegalStateException("You can't start a round for a finished game!"); - //if(!currentQuestionAnswered) - // throw new IllegalStateException("You can't start a new round when the current round is not over yet!"); + if(!currentRoundIsOver()) + throw new IllegalStateException("You can't start a new round when the current round is not over yet!"); } setCurrentQuestionAnswered(false); @@ -71,11 +71,11 @@ private void increaseRound(){ } public boolean isGameOver(){ - return isGameOver || getActualRound() > getRounds(); + return isGameOver && getActualRound() > getRounds(); } public boolean isLastRound(){ - return getActualRound() >= getRounds(); + return getActualRound() > getRounds(); } public Question getCurrentQuestion() { @@ -119,4 +119,8 @@ public void setLanguage(String language){ private boolean isLanguageSupported(String language) { return language.equals("en") || language.equals("es"); } + + public boolean shouldBeGameOver() { + return getActualRound() > getRounds() && !isGameOver; + } } diff --git a/api/src/main/java/lab/en2b/quizapi/game/GameService.java b/api/src/main/java/lab/en2b/quizapi/game/GameService.java index c296ed5e..11ea96c3 100644 --- a/api/src/main/java/lab/en2b/quizapi/game/GameService.java +++ b/api/src/main/java/lab/en2b/quizapi/game/GameService.java @@ -19,6 +19,7 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.List; +import java.util.Optional; @Service @RequiredArgsConstructor @@ -33,8 +34,16 @@ public class GameService { @Transactional public GameResponseDto newGame(Authentication authentication) { - if (gameRepository.findActiveGameForUser(userService.getUserByAuthentication(authentication).getId()).isPresent()){ - return gameResponseDtoMapper.apply(gameRepository.findActiveGameForUser(userService.getUserByAuthentication(authentication).getId()).get()); + Optional game = gameRepository.findActiveGameForUser(userService.getUserByAuthentication(authentication).getId()); + + if (game.isPresent()){ + if (game.get().shouldBeGameOver()){ + game.get().setGameOver(true); + gameRepository.save(game.get()); + saveStatistics(game.get()); + }else{ + return gameResponseDtoMapper.apply(game.get()); + } } return gameResponseDtoMapper.apply(gameRepository.save(Game.builder() .user(userService.getUserByAuthentication(authentication)) @@ -47,9 +56,13 @@ public GameResponseDto newGame(Authentication authentication) { .build())); } - @Transactional public GameResponseDto startRound(Long id, Authentication authentication) { Game game = gameRepository.findByIdForUser(id, userService.getUserByAuthentication(authentication).getId()).orElseThrow(); + if (game.shouldBeGameOver()){ + game.setGameOver(true); + gameRepository.save(game); + saveStatistics(game); + } game.newRound(questionService.findRandomQuestion(game.getLanguage())); return gameResponseDtoMapper.apply(gameRepository.save(game)); @@ -65,7 +78,7 @@ public GameResponseDto answerQuestion(Long id, GameAnswerDto dto, Authentication Game game = gameRepository.findByIdForUser(id, userService.getUserByAuthentication(authentication).getId()).orElseThrow(); game.answerQuestion(dto.getAnswerId(), questionRepository); - if (game.isLastRound() && !game.isGameOver()){ + if (game.shouldBeGameOver()){ game.setGameOver(true); gameRepository.save(game); saveStatistics(game); @@ -101,7 +114,7 @@ public GameResponseDto changeLanguage(Long id, String language, Authentication a public GameResponseDto getGameDetails(Long id, Authentication authentication) { Game game = gameRepository.findByIdForUser(id, userService.getUserByAuthentication(authentication).getId()).orElseThrow(); - if (game.isLastRound() && !game.isGameOver()){ + if (game.shouldBeGameOver()){ game.setGameOver(true); gameRepository.save(game); saveStatistics(game); diff --git a/api/src/main/java/lab/en2b/quizapi/game/dtos/GameResponseDto.java b/api/src/main/java/lab/en2b/quizapi/game/dtos/GameResponseDto.java index c2f979c4..991ec4a3 100644 --- a/api/src/main/java/lab/en2b/quizapi/game/dtos/GameResponseDto.java +++ b/api/src/main/java/lab/en2b/quizapi/game/dtos/GameResponseDto.java @@ -25,6 +25,7 @@ public class GameResponseDto { private Long rounds; @Schema(description = "Actual round for the game", example = "3") + @JsonProperty("actual_round") private Long actualRound; @Schema(description = "Number of correct answered questions", example = "2") diff --git a/api/src/test/java/lab/en2b/quizapi/game/GameServiceTest.java b/api/src/test/java/lab/en2b/quizapi/game/GameServiceTest.java index e5a4cd5b..16ca722d 100644 --- a/api/src/test/java/lab/en2b/quizapi/game/GameServiceTest.java +++ b/api/src/test/java/lab/en2b/quizapi/game/GameServiceTest.java @@ -231,6 +231,7 @@ public void getCurrentQuestionGameFinished() { when(gameRepository.save(any())).thenAnswer(invocation -> invocation.getArgument(0)); when(questionService.findRandomQuestion(any())).thenReturn(defaultQuestion); gameService.startRound(1L,authentication); + defaultGame.setGameOver(true); defaultGame.setActualRound(10L); assertThrows(IllegalStateException.class, () -> gameService.getCurrentQuestion(1L,authentication)); } @@ -271,6 +272,7 @@ public void answerQuestionWhenGameHasFinished(){ when(questionService.findRandomQuestion(any())).thenReturn(defaultQuestion); gameService.newGame(authentication); gameService.startRound(1L, authentication); + defaultGame.setGameOver(true); defaultGame.setActualRound(30L); assertThrows(IllegalStateException.class, () -> gameService.answerQuestion(1L, new GameAnswerDto(1L), authentication)); } @@ -319,6 +321,7 @@ public void changeLanguageGameOver(){ gameService.newGame(authentication); gameService.startRound(1L, authentication); defaultGame.setGameOver(true); + defaultGame.setActualRound(10L); assertThrows(IllegalStateException.class,() -> gameService.changeLanguage(1L, "es", authentication)); } From 84c0abcfbfd970f40cc5dee0bf03884dc1e9b77c Mon Sep 17 00:00:00 2001 From: Dario Date: Mon, 8 Apr 2024 21:42:05 +0200 Subject: [PATCH 12/18] fix: game can be played --- webapp/src/components/game/Game.js | 21 +---- webapp/src/pages/Game.jsx | 128 ++++++++++++++++++++--------- 2 files changed, 92 insertions(+), 57 deletions(-) diff --git a/webapp/src/components/game/Game.js b/webapp/src/components/game/Game.js index edcf768b..2c84c647 100644 --- a/webapp/src/components/game/Game.js +++ b/webapp/src/components/game/Game.js @@ -1,4 +1,4 @@ -import { HttpStatusCode } from "axios"; +import {HttpStatusCode} from "axios"; import AuthManager from "components/auth/AuthManager"; const authManager = new AuthManager(); @@ -15,25 +15,11 @@ export async function newGame() { } export async function startRound(gameId) { - try { - let requestAnswer = await authManager.getAxiosInstance().post(process.env.REACT_APP_API_ENDPOINT + "/games/" + gameId + "/startRound"); - if (HttpStatusCode.Ok === requestAnswer.status) { - return requestAnswer.data; - } - } catch { - - } + return await authManager.getAxiosInstance().post(process.env.REACT_APP_API_ENDPOINT + "/games/" + gameId + "/startRound"); } export async function getCurrentQuestion(gameId) { - try { - let requestAnswer = await authManager.getAxiosInstance().get(process.env.REACT_APP_API_ENDPOINT + "/games/" + gameId + "/question"); - if (HttpStatusCode.Ok === requestAnswer.status) { - return requestAnswer.data; - } - } catch { - - } + return await authManager.getAxiosInstance().get(process.env.REACT_APP_API_ENDPOINT + "/games/" + gameId + "/question"); } export async function changeLanguage(gameId, language) { @@ -68,3 +54,4 @@ export async function getGameDetails(gameId) { } } + diff --git a/webapp/src/pages/Game.jsx b/webapp/src/pages/Game.jsx index 512b4c86..4d9013ed 100644 --- a/webapp/src/pages/Game.jsx +++ b/webapp/src/pages/Game.jsx @@ -21,6 +21,10 @@ export default function Game() { const [correctAnswers, setCorrectAnswers] = useState(0); const [showConfetti, setShowConfetti] = useState(false); const [timeElapsed, setTimeElapsed] = useState(0); + const [timeStartRound, setTimeStartRound] = useState(-1); + const [roundDuration, setRoundDuration] = useState(0); + const [maxRoundNumber, setMaxRoundNumber] = useState(9); + const [questionLoading, setQuestionLoading] = useState(false); const { t, i18n } = useTranslation(); const [isMenuOpen, setIsMenuOpen] = useState(false); @@ -31,19 +35,34 @@ export default function Game() { const calculateProgress = (timeElapsed) => { const totalTime = 30; - const percentage = (timeElapsed / totalTime) * 100; + const percentage = (((Date.now()-timeStartRound)/1000) / totalTime) * 100; return Math.min(Math.max(percentage, 0), 100); }; - + /* + Initialize game when loading the page + */ useEffect(() => { const initializeGame = async () => { try { const newGameResponse = await newGame(); if (newGameResponse) { setLoading(false); - await startRound(newGameResponse.id); - setGameId(newGameResponse.id); - startTimer(); + setRoundNumber(newGameResponse.actual_round) + await setGameId(newGameResponse.id); + setTimeStartRound(new Date(newGameResponse.round_start_time).getTime()); + setRoundDuration(newGameResponse.round_duration) + setMaxRoundNumber(newGameResponse.rounds); + try{ + await getCurrentQuestion(newGameResponse.id).then((result) => { + if (result.status === 200) { + setQuestion(result.data); + setQuestionLoading(false); + } + }); + }catch (error) { + await startNewRound(newGameResponse.id); + } + } else { navigate("/dashboard"); } @@ -56,28 +75,32 @@ export default function Game() { initializeGame(); }, [navigate]); - const generateQuestion = useCallback(async () => { + + /* + Generate new question when the round changes + */ + const assignQuestion = useCallback(async () => { try { const result = await getCurrentQuestion(gameId); - if (result !== undefined) { - setQuestion(result); - setTimeElapsed(0); + if (result.status === 200) { + await setQuestion(result.data); + await setQuestionLoading(false); + setTimeElapsed(0); } else { - navigate("/dashboard"); + console.log(result) + //navigate("/dashboard"); } } catch (error) { console.error("Error fetching question:", error); - navigate("/dashboard"); + //navigate("/dashboard"); } }, [gameId, navigate]); - useEffect(() => { if (gameId !== null) { - setSelectedOption(null); - generateQuestion(); + //setSelectedOption(null); + //generateQuestion(); } - }, [gameId, generateQuestion]); - + }, [gameId, assignQuestion]); const answerButtonClick = (optionIndex, answer) => { const selectedOptionIndex = selectedOption === optionIndex ? null : optionIndex; @@ -90,29 +113,58 @@ export default function Game() { const nextButtonClick = useCallback(async () => { try { const isCorrect = (await answerQuestion(gameId, answer.id)).correctly_answered_questions; - + if (isCorrect) { setCorrectAnswers(correctAnswers + (isCorrect ? 1 : 0)); setShowConfetti(true); } - + setSelectedOption(null); - - const nextRoundNumber = roundNumber + 1; - if (nextRoundNumber > 9) - navigate("/dashboard/game/results", { state: { correctAnswers: correctAnswers + (isCorrect ? 1 : 0) } }); - else { - setAnswer({}); - setRoundNumber(nextRoundNumber); - setNextDisabled(true); - await startRound(gameId); - await generateQuestion(); - } + await nextRound() + } catch (error) { console.error("Error processing next question:", error); navigate("/dashboard"); } - }, [gameId, answer.id, roundNumber, correctAnswers, generateQuestion, navigate]); + }, [gameId, answer.id, roundNumber, correctAnswers, assignQuestion, navigate]); + + const nextRound = useCallback(async () => { + const nextRoundNumber = roundNumber + 1; + if (nextRoundNumber > maxRoundNumber) + navigate("/dashboard/game/results", { state: { correctAnswers: correctAnswers } }); + else { + setAnswer({}); + setRoundNumber(nextRoundNumber); + setNextDisabled(true); + setQuestionLoading(true); + await startNewRound(gameId); + + await assignQuestion(); + } + + }, [gameId, answer.id, roundNumber, correctAnswers, assignQuestion, navigate]); + + const startNewRound = useCallback(async (gameId) => { + try{ + const result = await startRound(gameId); + setTimeStartRound(new Date(result.data.round_start_time).getTime()); + setRoundNumber(result.data.actual_round ) + setRoundDuration(result.data.round_duration); + await assignQuestion(); + } + catch(error){ + if(error.status === 409){ + if(roundNumber >= 9){ + navigate("/dashboard/game/results", { state: { correctAnswers: correctAnswers } }); + } else { + await assignQuestion() + } + } + + } + + }, [gameId, answer.id, roundNumber, correctAnswers, assignQuestion, navigate]); + useEffect(() => { let timeout; @@ -123,8 +175,11 @@ export default function Game() { useEffect(() => { let timeout; - if (timeElapsed >= 30) { - timeout = setTimeout(() => nextButtonClick(), 1000); + + //console.log(timeElapsed) + if ((Date.now()-timeStartRound)/1000 >= roundDuration && timeStartRound !== -1) { + timeout = setTimeout(() => nextRound(), 1000); + } else { timeout = setTimeout(() => { setTimeElapsed((prevTime) => prevTime + 1); @@ -132,14 +187,7 @@ export default function Game() { } return () => clearTimeout(timeout); }, [timeElapsed, nextButtonClick]); - - const startTimer = () => { - const timer = setTimeout(() => { - setTimeElapsed((prevTime) => prevTime + 1); - }, 1000); - return () => clearTimeout(timer); - }; return (
@@ -182,7 +230,7 @@ export default function Game() { - From 334e6bc6cec953695209c12cbce006403f6f7efa Mon Sep 17 00:00:00 2001 From: Dario Date: Mon, 8 Apr 2024 21:48:52 +0200 Subject: [PATCH 13/18] fix: game over again --- api/src/main/java/lab/en2b/quizapi/game/Game.java | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/api/src/main/java/lab/en2b/quizapi/game/Game.java b/api/src/main/java/lab/en2b/quizapi/game/Game.java index fa75cf5c..e8ae10c9 100644 --- a/api/src/main/java/lab/en2b/quizapi/game/Game.java +++ b/api/src/main/java/lab/en2b/quizapi/game/Game.java @@ -5,7 +5,6 @@ import lab.en2b.quizapi.commons.user.User; import lab.en2b.quizapi.questions.answer.Answer; import lab.en2b.quizapi.questions.question.Question; -import lab.en2b.quizapi.questions.question.QuestionRepository; import lombok.*; import java.time.LocalDateTime; @@ -71,12 +70,9 @@ private void increaseRound(){ } public boolean isGameOver(){ - return isGameOver && getActualRound() > getRounds(); + return isGameOver && getActualRound() >= getRounds(); } - public boolean isLastRound(){ - return getActualRound() > getRounds(); - } public Question getCurrentQuestion() { if(getRoundStartTime() == null){ @@ -97,7 +93,7 @@ private boolean roundTimeHasExpired(){ return getRoundStartTime()!= null && LocalDateTime.now().isAfter(getRoundStartTime().plusSeconds(getRoundDuration())); } - public void answerQuestion(Long answerId, QuestionRepository questionRepository){ + public void answerQuestion(Long answerId){ if(currentRoundIsOver()) throw new IllegalStateException("You can't answer a question when the current round is over!"); if (isGameOver()) @@ -121,6 +117,6 @@ private boolean isLanguageSupported(String language) { } public boolean shouldBeGameOver() { - return getActualRound() > getRounds() && !isGameOver; + return getActualRound() >= getRounds() && !isGameOver; } } From aa0b499ff63f1e3208f9e28be3b0c7444352f543 Mon Sep 17 00:00:00 2001 From: Dario Date: Mon, 8 Apr 2024 21:57:07 +0200 Subject: [PATCH 14/18] fix: answer now returns boolean --- api/src/main/java/lab/en2b/quizapi/game/Game.java | 3 ++- .../java/lab/en2b/quizapi/game/GameController.java | 3 ++- .../java/lab/en2b/quizapi/game/GameService.java | 7 ++++--- .../quizapi/game/dtos/AnswerGameResponseDto.java | 14 ++++++++++++++ webapp/src/pages/Game.jsx | 4 ++-- 5 files changed, 24 insertions(+), 7 deletions(-) create mode 100644 api/src/main/java/lab/en2b/quizapi/game/dtos/AnswerGameResponseDto.java diff --git a/api/src/main/java/lab/en2b/quizapi/game/Game.java b/api/src/main/java/lab/en2b/quizapi/game/Game.java index e8ae10c9..579f0346 100644 --- a/api/src/main/java/lab/en2b/quizapi/game/Game.java +++ b/api/src/main/java/lab/en2b/quizapi/game/Game.java @@ -93,7 +93,7 @@ private boolean roundTimeHasExpired(){ return getRoundStartTime()!= null && LocalDateTime.now().isAfter(getRoundStartTime().plusSeconds(getRoundDuration())); } - public void answerQuestion(Long answerId){ + public boolean answerQuestion(Long answerId){ if(currentRoundIsOver()) throw new IllegalStateException("You can't answer a question when the current round is over!"); if (isGameOver()) @@ -105,6 +105,7 @@ public void answerQuestion(Long answerId){ setCorrectlyAnsweredQuestions(getCorrectlyAnsweredQuestions() + 1); } setCurrentQuestionAnswered(true); + return q.isCorrectAnswer(answerId); } public void setLanguage(String language){ if(!isLanguageSupported(language)) diff --git a/api/src/main/java/lab/en2b/quizapi/game/GameController.java b/api/src/main/java/lab/en2b/quizapi/game/GameController.java index 2f5e2d7e..c39409ae 100644 --- a/api/src/main/java/lab/en2b/quizapi/game/GameController.java +++ b/api/src/main/java/lab/en2b/quizapi/game/GameController.java @@ -3,6 +3,7 @@ import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.responses.ApiResponses; +import lab.en2b.quizapi.game.dtos.AnswerGameResponseDto; import lab.en2b.quizapi.game.dtos.GameAnswerDto; import lab.en2b.quizapi.game.dtos.GameResponseDto; import lab.en2b.quizapi.questions.question.QuestionCategory; @@ -57,7 +58,7 @@ public ResponseEntity getCurrentQuestion(@PathVariable Long @ApiResponse(responseCode = "403", description = "You are not logged in", content = @io.swagger.v3.oas.annotations.media.Content), }) @PostMapping("/{id}/answer") - public ResponseEntity answerQuestion(@PathVariable Long id, @RequestBody GameAnswerDto dto, Authentication authentication){ + public ResponseEntity answerQuestion(@PathVariable Long id, @RequestBody GameAnswerDto dto, Authentication authentication){ return ResponseEntity.ok(gameService.answerQuestion(id, dto, authentication)); } diff --git a/api/src/main/java/lab/en2b/quizapi/game/GameService.java b/api/src/main/java/lab/en2b/quizapi/game/GameService.java index 11ea96c3..ab75b519 100644 --- a/api/src/main/java/lab/en2b/quizapi/game/GameService.java +++ b/api/src/main/java/lab/en2b/quizapi/game/GameService.java @@ -1,6 +1,7 @@ package lab.en2b.quizapi.game; import lab.en2b.quizapi.commons.user.UserService; +import lab.en2b.quizapi.game.dtos.AnswerGameResponseDto; import lab.en2b.quizapi.game.dtos.GameAnswerDto; import lab.en2b.quizapi.game.dtos.GameResponseDto; import lab.en2b.quizapi.game.mappers.GameResponseDtoMapper; @@ -74,9 +75,9 @@ public QuestionResponseDto getCurrentQuestion(Long id, Authentication authentica } @Transactional - public GameResponseDto answerQuestion(Long id, GameAnswerDto dto, Authentication authentication){ + public AnswerGameResponseDto answerQuestion(Long id, GameAnswerDto dto, Authentication authentication){ Game game = gameRepository.findByIdForUser(id, userService.getUserByAuthentication(authentication).getId()).orElseThrow(); - game.answerQuestion(dto.getAnswerId(), questionRepository); + boolean wasCorrect = game.answerQuestion(dto.getAnswerId()); if (game.shouldBeGameOver()){ game.setGameOver(true); @@ -84,7 +85,7 @@ public GameResponseDto answerQuestion(Long id, GameAnswerDto dto, Authentication saveStatistics(game); } - return gameResponseDtoMapper.apply(game); + return new AnswerGameResponseDto(wasCorrect); } private void saveStatistics(Game game){ if (statisticsRepository.findByUserId(game.getUser().getId()).isPresent()){ diff --git a/api/src/main/java/lab/en2b/quizapi/game/dtos/AnswerGameResponseDto.java b/api/src/main/java/lab/en2b/quizapi/game/dtos/AnswerGameResponseDto.java new file mode 100644 index 00000000..9d9d3955 --- /dev/null +++ b/api/src/main/java/lab/en2b/quizapi/game/dtos/AnswerGameResponseDto.java @@ -0,0 +1,14 @@ +package lab.en2b.quizapi.game.dtos; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@AllArgsConstructor +@Data +@NoArgsConstructor +public class AnswerGameResponseDto { + @JsonProperty("was_correct") + private boolean wasCorrect; +} diff --git a/webapp/src/pages/Game.jsx b/webapp/src/pages/Game.jsx index 4d9013ed..5fcc6b28 100644 --- a/webapp/src/pages/Game.jsx +++ b/webapp/src/pages/Game.jsx @@ -112,7 +112,7 @@ export default function Game() { const nextButtonClick = useCallback(async () => { try { - const isCorrect = (await answerQuestion(gameId, answer.id)).correctly_answered_questions; + const isCorrect = (await answerQuestion(gameId, answer.id)).was_correct; if (isCorrect) { setCorrectAnswers(correctAnswers + (isCorrect ? 1 : 0)); @@ -231,7 +231,7 @@ export default function Game() { From 3d9bbb24fe559bcceffdac9518541f427efadb6f Mon Sep 17 00:00:00 2001 From: Dario Date: Mon, 8 Apr 2024 22:00:04 +0200 Subject: [PATCH 15/18] fix: answer now returns boolean --- webapp/public/locales/en/translation.json | 2 +- webapp/public/locales/es/translation.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/webapp/public/locales/en/translation.json b/webapp/public/locales/en/translation.json index 07f9238f..b61195dc 100644 --- a/webapp/public/locales/en/translation.json +++ b/webapp/public/locales/en/translation.json @@ -75,7 +75,7 @@ }, "game": { "round": "Round ", - "next": "Next" + "answer": "Answer" }, "about": { "title": "About", diff --git a/webapp/public/locales/es/translation.json b/webapp/public/locales/es/translation.json index f854287b..a67bc786 100644 --- a/webapp/public/locales/es/translation.json +++ b/webapp/public/locales/es/translation.json @@ -74,7 +74,7 @@ }, "game": { "round": "Ronda ", - "next": "Siguiente" + "answer": "Responder" }, "about": { "title": "Sobre nosotros", From 2d660297bf315788dca5ccc89babb04d4cdf8df0 Mon Sep 17 00:00:00 2001 From: Dario Date: Mon, 8 Apr 2024 22:18:37 +0200 Subject: [PATCH 16/18] fix: user stats in game --- webapp/src/pages/Results.jsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/webapp/src/pages/Results.jsx b/webapp/src/pages/Results.jsx index 3abbd0cb..3d934288 100644 --- a/webapp/src/pages/Results.jsx +++ b/webapp/src/pages/Results.jsx @@ -2,6 +2,7 @@ import React from "react"; import { useTranslation } from "react-i18next"; import { Button, Flex, Box, Heading, Center } from "@chakra-ui/react"; import { useNavigate, useLocation } from "react-router-dom"; +import UserStatistics from "../components/statistics/UserStatistics"; export default function Results() { const { t } = useTranslation(); @@ -19,6 +20,7 @@ export default function Results() { {t("common.finish")} +
); From 60b2faf044035d042b2b61fbcb8196720b15f7e0 Mon Sep 17 00:00:00 2001 From: Dario Date: Mon, 8 Apr 2024 22:47:41 +0200 Subject: [PATCH 17/18] fix: question not loading --- webapp/src/components/game/Game.js | 9 +-------- webapp/src/pages/Game.jsx | 31 +++++++++++++++--------------- 2 files changed, 17 insertions(+), 23 deletions(-) diff --git a/webapp/src/components/game/Game.js b/webapp/src/components/game/Game.js index 2c84c647..ffe3ac04 100644 --- a/webapp/src/components/game/Game.js +++ b/webapp/src/components/game/Game.js @@ -34,14 +34,7 @@ export async function changeLanguage(gameId, language) { } export async function answerQuestion(gameId, aId) { - try { - let requestAnswer = await authManager.getAxiosInstance().post(process.env.REACT_APP_API_ENDPOINT + "/games/" + gameId + "/answer", {answer_id:aId}); - if (HttpStatusCode.Ok === requestAnswer.status) { - return requestAnswer.data; - } - } catch { - - } + return await authManager.getAxiosInstance().post(process.env.REACT_APP_API_ENDPOINT + "/games/" + gameId + "/answer", {answer_id:aId}); } export async function getGameDetails(gameId) { diff --git a/webapp/src/pages/Game.jsx b/webapp/src/pages/Game.jsx index 5fcc6b28..6e719e7c 100644 --- a/webapp/src/pages/Game.jsx +++ b/webapp/src/pages/Game.jsx @@ -79,7 +79,7 @@ export default function Game() { /* Generate new question when the round changes */ - const assignQuestion = useCallback(async () => { + const assignQuestion = useCallback(async (gameId) => { try { const result = await getCurrentQuestion(gameId); if (result.status === 200) { @@ -87,12 +87,11 @@ export default function Game() { await setQuestionLoading(false); setTimeElapsed(0); } else { - console.log(result) - //navigate("/dashboard"); + navigate("/dashboard"); } } catch (error) { console.error("Error fetching question:", error); - //navigate("/dashboard"); + navigate("/dashboard"); } }, [gameId, navigate]); useEffect(() => { @@ -102,29 +101,31 @@ export default function Game() { } }, [gameId, assignQuestion]); - const answerButtonClick = (optionIndex, answer) => { + const answerButtonClick = async (optionIndex, answer) => { const selectedOptionIndex = selectedOption === optionIndex ? null : optionIndex; setSelectedOption(selectedOptionIndex); - setAnswer(answer); + await setAnswer(answer); const anyOptionSelected = selectedOptionIndex !== null; setNextDisabled(!anyOptionSelected); }; const nextButtonClick = useCallback(async () => { try { - const isCorrect = (await answerQuestion(gameId, answer.id)).was_correct; - + const result = await answerQuestion(gameId, answer.id); + let isCorrect = result.data.was_correct; if (isCorrect) { setCorrectAnswers(correctAnswers + (isCorrect ? 1 : 0)); setShowConfetti(true); } - setSelectedOption(null); await nextRound() } catch (error) { - console.error("Error processing next question:", error); - navigate("/dashboard"); + if(error.response.status === 400){ + setTimeout(nextButtonClick, 2000) + }else{ + console.log('xd'+error.status) + } } }, [gameId, answer.id, roundNumber, correctAnswers, assignQuestion, navigate]); @@ -139,7 +140,7 @@ export default function Game() { setQuestionLoading(true); await startNewRound(gameId); - await assignQuestion(); + await assignQuestion(gameId); } }, [gameId, answer.id, roundNumber, correctAnswers, assignQuestion, navigate]); @@ -150,14 +151,14 @@ export default function Game() { setTimeStartRound(new Date(result.data.round_start_time).getTime()); setRoundNumber(result.data.actual_round ) setRoundDuration(result.data.round_duration); - await assignQuestion(); + await assignQuestion(gameId); } catch(error){ if(error.status === 409){ if(roundNumber >= 9){ navigate("/dashboard/game/results", { state: { correctAnswers: correctAnswers } }); } else { - await assignQuestion() + await assignQuestion(gameId) } } @@ -186,7 +187,7 @@ export default function Game() { }, 1000); } return () => clearTimeout(timeout); - }, [timeElapsed, nextButtonClick]); + }, [timeElapsed]); return ( From 1aa45df2285876d7e5a4e13e9c59e8dc4bcdc0db Mon Sep 17 00:00:00 2001 From: Dario Date: Mon, 8 Apr 2024 22:53:21 +0200 Subject: [PATCH 18/18] fix: loading after loading questions --- webapp/src/pages/Game.jsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/webapp/src/pages/Game.jsx b/webapp/src/pages/Game.jsx index 6e719e7c..918c4a61 100644 --- a/webapp/src/pages/Game.jsx +++ b/webapp/src/pages/Game.jsx @@ -46,7 +46,6 @@ export default function Game() { try { const newGameResponse = await newGame(); if (newGameResponse) { - setLoading(false); setRoundNumber(newGameResponse.actual_round) await setGameId(newGameResponse.id); setTimeStartRound(new Date(newGameResponse.round_start_time).getTime()); @@ -62,7 +61,7 @@ export default function Game() { }catch (error) { await startNewRound(newGameResponse.id); } - + setLoading(false); } else { navigate("/dashboard"); } @@ -154,6 +153,7 @@ export default function Game() { await assignQuestion(gameId); } catch(error){ + console.log(error) if(error.status === 409){ if(roundNumber >= 9){ navigate("/dashboard/game/results", { state: { correctAnswers: correctAnswers } });