From 0746b05bc98ad6f11af5ac52baa08dde7e69251a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gonzalo=20Su=C3=A1rez=20Losada?= Date: Sun, 7 Apr 2024 15:12:55 +0200 Subject: [PATCH 01/63] chore: added SpringBoot version --- docs/src/04_solution_strategy.adoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/src/04_solution_strategy.adoc b/docs/src/04_solution_strategy.adoc index cb800080..d7411166 100644 --- a/docs/src/04_solution_strategy.adoc +++ b/docs/src/04_solution_strategy.adoc @@ -15,7 +15,7 @@ Regarding the technologies, we decided to use the following ones: * **PostgreSQL** as DBMS to store the information. We nearly immediately discarded using MongoDB due to many of us not having experience with it, and those that did preferring SQL. Many modern DBMS also include either JSON or JSONB data types, so using a DBMS whose main appeal is JSON and not many of us have experience with did not sit well with us. - * **Java SpringBoot** for the backend/API, it being a language we are all comfortable with. The server will easily support multithreading if needed due to SpringBoot being an abstraction over servlets, something we would be able only to simulate if we used Node.js as it uses a single-threaded event loop. + * **Java SpringBoot 3** for the backend/API, it being a language we are all comfortable with. The server will easily support multithreading if needed due to SpringBoot being an abstraction over servlets, something we would be able only to simulate if we used Node.js as it uses a single-threaded event loop. === Organizational breakdown From 6e165a6208189898f8c6ae22936d13e302ce843d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gonzalo=20Su=C3=A1rez=20Losada?= Date: Sun, 7 Apr 2024 15:15:22 +0200 Subject: [PATCH 02/63] fix: solved grammar mistakes --- docs/src/04_solution_strategy.adoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/src/04_solution_strategy.adoc b/docs/src/04_solution_strategy.adoc index d7411166..bdb848a4 100644 --- a/docs/src/04_solution_strategy.adoc +++ b/docs/src/04_solution_strategy.adoc @@ -13,7 +13,7 @@ Regarding the technologies, we decided to use the following ones: ** As a consecuence of this, pure JavaScript is being used due to React 18 not supporting Typescript 5. - * **PostgreSQL** as DBMS to store the information. We nearly immediately discarded using MongoDB due to many of us not having experience with it, and those that did preferring SQL. Many modern DBMS also include either JSON or JSONB data types, so using a DBMS whose main appeal is JSON and not many of us have experience with did not sit well with us. + * **PostgreSQL** as DBMS to store the information. We nearly immediately discarded using MongoDB due to many of us not having experience with it, and those who did, preferred SQL. Many modern DBMS also include either JSON or JSONB data types, so using a DBMS whose main appeal is JSON and not many of us have experience with did not sit well with us. * **Java SpringBoot 3** for the backend/API, it being a language we are all comfortable with. The server will easily support multithreading if needed due to SpringBoot being an abstraction over servlets, something we would be able only to simulate if we used Node.js as it uses a single-threaded event loop. From 24a46f4fda2534fa8be0ae8a5482a1055bd16518 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gonzalo=20Su=C3=A1rez=20Losada?= Date: Sun, 7 Apr 2024 15:28:03 +0200 Subject: [PATCH 03/63] fix: solved typo --- docs/src/05_building_block_view.adoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/src/05_building_block_view.adoc b/docs/src/05_building_block_view.adoc index cf27c401..532a1217 100644 --- a/docs/src/05_building_block_view.adoc +++ b/docs/src/05_building_block_view.adoc @@ -38,7 +38,7 @@ An inner view on the WIQ Application and its components inside. How the WIQ appl Contained Building Blocks:: **** * **WIQ Client:** This is the connection between the user and the application. It will allow the users to play the WIQ game. This part will be developed in React with Typescript for its clear component structure, simplified code quality and separation of concerns. -* **WIQ REST API:** This is the part responsible for managing the users that log into the application, managing also the logic of the game and sending the request to the Wikidata API for the question generation. This part is going to be developed in Springboot due to its foundations on the Java programming language, which is the language that the developers find the easiest to develop in. +* **WIQ REST API:** This is the part responsible for managing the users that log into the application, managing also the logic of the game and sending the request to the Wikidata API for the question generation. This part is going to be developed in SpringBoot due to its foundations on the Java programming language, which is the language that the developers find the easiest to develop in. * **WIQ Database:** This is where the most important data is going to be stored. Such as, users questions and other game info that will be specified in the future. The database we chose to use is PostgreSQL, since it is compatible with Docker and it's an object-relational kind of database, which is easier for the developers to use. Another alternative would've been to use MySQL. **** From d96d60803486f4170520c9f90e7266bd4558b985 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gonzalo=20Su=C3=A1rez=20Losada?= Date: Sun, 7 Apr 2024 16:03:02 +0200 Subject: [PATCH 04/63] chore: added Question Generator Module --- docs/diagrams/BusinessContextDiagram.puml | 5 ++++- docs/src/05_building_block_view.adoc | 1 + 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/docs/diagrams/BusinessContextDiagram.puml b/docs/diagrams/BusinessContextDiagram.puml index 5387b081..afc0010c 100644 --- a/docs/diagrams/BusinessContextDiagram.puml +++ b/docs/diagrams/BusinessContextDiagram.puml @@ -15,10 +15,13 @@ Person(player, Player,"An authenticated player that wants to play WIQ games", $t Container(wiq, "WIQ Application","", "Application that allows the users to play WIQ games", $tags="Internal system") +Container(question_generator, "Question Generator Module","", "Organizes everything related with questions", $tags="Internal system") + System_Ext(wikidata,"WikiData API","Contains the information used for the question generation", $tags="External system") 'RELATIONS Rel(player,wiq,"Plays games") -Rel(wiq,wikidata,"Asks for data for question generation") +Rel(wiq, question_generator, "Asks for questions and answers") +Rel(question_generator,wikidata,"Asks for data for question generation") SHOW_LEGEND() @enduml \ No newline at end of file diff --git a/docs/src/05_building_block_view.adoc b/docs/src/05_building_block_view.adoc index 532a1217..36c2c330 100644 --- a/docs/src/05_building_block_view.adoc +++ b/docs/src/05_building_block_view.adoc @@ -39,6 +39,7 @@ Contained Building Blocks:: **** * **WIQ Client:** This is the connection between the user and the application. It will allow the users to play the WIQ game. This part will be developed in React with Typescript for its clear component structure, simplified code quality and separation of concerns. * **WIQ REST API:** This is the part responsible for managing the users that log into the application, managing also the logic of the game and sending the request to the Wikidata API for the question generation. This part is going to be developed in SpringBoot due to its foundations on the Java programming language, which is the language that the developers find the easiest to develop in. +* **Question Generator Module:** This component is in charge of everything related with the questions. It retrieves the information from WikiData QS and stores it in the WIQ Database. It is also asked for questions and answers by the WIQ REST API.x * **WIQ Database:** This is where the most important data is going to be stored. Such as, users questions and other game info that will be specified in the future. The database we chose to use is PostgreSQL, since it is compatible with Docker and it's an object-relational kind of database, which is easier for the developers to use. Another alternative would've been to use MySQL. **** From f4904d62510f463fa5c73be588e6d5f8dc71d3db Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gonzalo=20Su=C3=A1rez=20Losada?= Date: Sun, 7 Apr 2024 16:09:07 +0200 Subject: [PATCH 05/63] chore: added interfaces documentation --- docs/src/05_building_block_view.adoc | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/docs/src/05_building_block_view.adoc b/docs/src/05_building_block_view.adoc index 36c2c330..b274f1ef 100644 --- a/docs/src/05_building_block_view.adoc +++ b/docs/src/05_building_block_view.adoc @@ -39,10 +39,13 @@ Contained Building Blocks:: **** * **WIQ Client:** This is the connection between the user and the application. It will allow the users to play the WIQ game. This part will be developed in React with Typescript for its clear component structure, simplified code quality and separation of concerns. * **WIQ REST API:** This is the part responsible for managing the users that log into the application, managing also the logic of the game and sending the request to the Wikidata API for the question generation. This part is going to be developed in SpringBoot due to its foundations on the Java programming language, which is the language that the developers find the easiest to develop in. -* **Question Generator Module:** This component is in charge of everything related with the questions. It retrieves the information from WikiData QS and stores it in the WIQ Database. It is also asked for questions and answers by the WIQ REST API.x +* **Question Generator Module:** This component is in charge of everything related with the questions. It retrieves the information from WikiData QS and stores it in the WIQ Database. It is also asked for questions and answers by the WIQ REST API. * **WIQ Database:** This is where the most important data is going to be stored. Such as, users questions and other game info that will be specified in the future. The database we chose to use is PostgreSQL, since it is compatible with Docker and it's an object-relational kind of database, which is easier for the developers to use. Another alternative would've been to use MySQL. **** Important Interfaces:: -This part will be more detailed later, since the structure of the different interfaces/classes has not been discussed by the team yet. +We are connecting the front end with the back end via REST API. Also, the Question Generator Module communicates with WikiData QS this way. +The Question Generator Module connects to the WIQ DataBase through JPA. +In the near future, we are planning on allowing HTTPS connections, so there would be a proxy server in between the user's agent and the back end. + From fbc477fd88445583d27857e89686e2b3ac01cb60 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gonzalo=20Su=C3=A1rez=20Losada?= Date: Sun, 7 Apr 2024 16:30:13 +0200 Subject: [PATCH 06/63] fix: solved typo --- docs/diagrams/sequence/SequenceDiagramQuestionGeneration.puml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/diagrams/sequence/SequenceDiagramQuestionGeneration.puml b/docs/diagrams/sequence/SequenceDiagramQuestionGeneration.puml index b8b4a167..fd7e49a5 100644 --- a/docs/diagrams/sequence/SequenceDiagramQuestionGeneration.puml +++ b/docs/diagrams/sequence/SequenceDiagramQuestionGeneration.puml @@ -10,7 +10,7 @@ loop #PeachPuff Generate question templates activate QuestionGenerator #darksalmon QuestionGenerator -> WikiDataQS : request query template activate WikiDataQS #darksalmon -QuestionGenerator <-- WikiDataQS : returns query answer +QuestionGenerator <-- WikiDataQS : returns query answers deactivate WikiDataQS QuestionGenerator -> QuestionGenerator : process query answer QuestionGenerator -> DB : store answers From 61d46fabd5b0388c6d69a9dc7511c382298ad1a4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gonzalo=20Su=C3=A1rez=20Losada?= Date: Sun, 7 Apr 2024 16:57:44 +0200 Subject: [PATCH 07/63] fix: solved wrong diagram --- docs/diagrams/deployment/DeploymentDiagram.puml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/diagrams/deployment/DeploymentDiagram.puml b/docs/diagrams/deployment/DeploymentDiagram.puml index 8faacfe1..19174a75 100644 --- a/docs/diagrams/deployment/DeploymentDiagram.puml +++ b/docs/diagrams/deployment/DeploymentDiagram.puml @@ -31,6 +31,6 @@ node "WikiData Server" #DarkSalmon { "Web Client" ..> "WIQ React Application" : "HTTPS" "WIQ React Application" ..> "WIQ_API.jar" : "HTTPS" "WIQ_API.jar" ..> "WIQ Database" : "JPA" -"WIQ API" ..> "WikiData REST API" : "HTTPS, SPARQL" -"Question_Generator.jar" ..> "WIQ Database" : "JPA +"Question_Generator.jar" ..> "WikiData REST API" : "HTTPS, SPARQL" +"Question_Generator.jar" ..> "WIQ Database" : "JPA" @enduml \ No newline at end of file From d9f8e73811e17ac8837db8e32973011dfe77a352 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gonzalo=20Su=C3=A1rez=20Losada?= Date: Sun, 7 Apr 2024 17:27:41 +0200 Subject: [PATCH 08/63] fix: removed unused command --- docs/diagrams/BusinessContextDiagram.puml | 3 --- 1 file changed, 3 deletions(-) diff --git a/docs/diagrams/BusinessContextDiagram.puml b/docs/diagrams/BusinessContextDiagram.puml index afc0010c..2811c4cf 100644 --- a/docs/diagrams/BusinessContextDiagram.puml +++ b/docs/diagrams/BusinessContextDiagram.puml @@ -7,9 +7,6 @@ AddElementTag("Person", $bgColor="#salmon", $fontColor="#white") AddElementTag("Internal system", $bgColor="#darksalmon", $fontColor="#white") AddElementTag("External system", $bgColor="#peachpuff", $fontColor="#963b17") -AddRelTag("backup", $textColor="orange", $lineColor="orange", $lineStyle = DashedLine()) - - 'Containers Person(player, Player,"An authenticated player that wants to play WIQ games", $tags="Person") From c99c6f1a04986c6a7a9c1c95b5e96970c63ada29 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gonzalo=20Su=C3=A1rez=20Losada?= Date: Sun, 7 Apr 2024 17:33:39 +0200 Subject: [PATCH 09/63] fix: color legend updated for container diagram --- docs/diagrams/ContainerDiagram.puml | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/docs/diagrams/ContainerDiagram.puml b/docs/diagrams/ContainerDiagram.puml index e0bd97a1..8ef5f6d0 100644 --- a/docs/diagrams/ContainerDiagram.puml +++ b/docs/diagrams/ContainerDiagram.puml @@ -3,22 +3,26 @@ !include title Context Diagram for the WIQ System -LAYOUT_WITH_LEGEND() + +AddElementTag("Person", $bgColor="#salmon", $fontColor="#white") +AddElementTag("Internal system", $bgColor="#darksalmon", $fontColor="#white") +AddElementTag("External system", $bgColor="#peachpuff", $fontColor="#963b17") 'Containers Person(player, Player,"An authenticated player that wants to play WIQ games") System_Boundary(wiq,"WIQ"){ - Container(web_app, "WIQ Client", "React, Typescript", "Allows the user to play WIQ games") - Container(backend_api, "WIQ REST API","Java SpringBoot 3","Handles the users, game logic and question generation") - ContainerDb(database,"WIQ Database","PostgreSQL","Stores users, questions and other game info") + Container(web_app, "WIQ Client", "React, Typescript", "Allows the user to play WIQ games", $tags="Internal system") + Container(backend_api, "WIQ REST API","Java SpringBoot 3","Handles the users, game logic and question generation", $tags="Internal system") + ContainerDb(database,"WIQ Database","PostgreSQL","Stores users, questions and other game info", $tags="Internal system") } -System_Ext(wikidata,"WikiData API","Contains the information used for the question generation") +System_Ext(wikidata,"WikiData API","Contains the information used for the question generation", $tags="External system") 'RELATIONS Rel(player,web_app,"Uses","HTTPS") Rel(backend_api,wikidata,"Asks for data","SPARQL,HTTPS") Rel(web_app,backend_api,"Asks for user/game information","JSON,HTTPS") Rel(backend_api,database,"Stores game/user information","JPA") + @enduml \ No newline at end of file From d21bb36aa121c589cc8f25565c05855c9e845e4f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gonzalo=20Su=C3=A1rez=20Losada?= Date: Sun, 7 Apr 2024 18:10:43 +0200 Subject: [PATCH 10/63] fix: color legend updated for container diagram --- docs/diagrams/BusinessContextDiagram.puml | 5 ++--- docs/diagrams/ContainerDiagram.puml | 12 ++++++------ docs/diagrams/TechnicalContextDiagram.puml | 14 +++++++++----- 3 files changed, 17 insertions(+), 14 deletions(-) diff --git a/docs/diagrams/BusinessContextDiagram.puml b/docs/diagrams/BusinessContextDiagram.puml index 2811c4cf..0fdbcad7 100644 --- a/docs/diagrams/BusinessContextDiagram.puml +++ b/docs/diagrams/BusinessContextDiagram.puml @@ -4,8 +4,8 @@ title Context Diagram for the WIQ System AddElementTag("Person", $bgColor="#salmon", $fontColor="#white") -AddElementTag("Internal system", $bgColor="#darksalmon", $fontColor="#white") -AddElementTag("External system", $bgColor="#peachpuff", $fontColor="#963b17") +AddElementTag("Internal system", $bgColor="#peachpuff", $fontColor="#963b17") +AddElementTag("External system", $bgColor="#darksalmon", $fontColor="#white") 'Containers Person(player, Player,"An authenticated player that wants to play WIQ games", $tags="Person") @@ -20,5 +20,4 @@ System_Ext(wikidata,"WikiData API","Contains the information used for the questi Rel(player,wiq,"Plays games") Rel(wiq, question_generator, "Asks for questions and answers") Rel(question_generator,wikidata,"Asks for data for question generation") -SHOW_LEGEND() @enduml \ No newline at end of file diff --git a/docs/diagrams/ContainerDiagram.puml b/docs/diagrams/ContainerDiagram.puml index 8ef5f6d0..32805b8e 100644 --- a/docs/diagrams/ContainerDiagram.puml +++ b/docs/diagrams/ContainerDiagram.puml @@ -1,18 +1,18 @@ @startuml +!define CONTAINER_CONTAINER !includeurl https://raw.githubusercontent.com/RicardoNiepel/C4-PlantUML/master/C4_Container.puml !include title Context Diagram for the WIQ System - - AddElementTag("Person", $bgColor="#salmon", $fontColor="#white") -AddElementTag("Internal system", $bgColor="#darksalmon", $fontColor="#white") -AddElementTag("External system", $bgColor="#peachpuff", $fontColor="#963b17") +AddElementTag("Internal system", $bgColor="#peachpuff", $fontColor="#963b17") +AddElementTag("External system", $bgColor="#darksalmon", $fontColor="#white") + 'Containers Person(player, Player,"An authenticated player that wants to play WIQ games") System_Boundary(wiq,"WIQ"){ - Container(web_app, "WIQ Client", "React, Typescript", "Allows the user to play WIQ games", $tags="Internal system") + Container(web_app, "WIQ Client", "React, JavaScript", "Allows the user to play WIQ games", $tags="Internal system") Container(backend_api, "WIQ REST API","Java SpringBoot 3","Handles the users, game logic and question generation", $tags="Internal system") ContainerDb(database,"WIQ Database","PostgreSQL","Stores users, questions and other game info", $tags="Internal system") } @@ -25,4 +25,4 @@ Rel(backend_api,wikidata,"Asks for data","SPARQL,HTTPS") Rel(web_app,backend_api,"Asks for user/game information","JSON,HTTPS") Rel(backend_api,database,"Stores game/user information","JPA") -@enduml \ No newline at end of file +@enduml diff --git a/docs/diagrams/TechnicalContextDiagram.puml b/docs/diagrams/TechnicalContextDiagram.puml index 0c035bb9..2d2e0f1a 100644 --- a/docs/diagrams/TechnicalContextDiagram.puml +++ b/docs/diagrams/TechnicalContextDiagram.puml @@ -3,21 +3,25 @@ !include title Context Diagram for the WIQ System -LAYOUT_WITH_LEGEND() +AddElementTag("Person", $bgColor="#salmon", $fontColor="#white") +AddElementTag("Internal system", $bgColor="#peachpuff", $fontColor="#963b17") +AddElementTag("External system", $bgColor="#darksalmon", $fontColor="#white") 'Containers Person(player, Player's Browser,"Preferred browser (Firefox, Chrome, Opera...)") System_Boundary(wiq,"WIQ Server"){ - Container(web_app, "WIQ Client", "React, Typescript", "nginx web server") - Container(backend_api, "WIQ REST API","Java SpringBoot 3",".jar file") - ContainerDb(database,"WIQ Database","PostgreSQL","PostgreSQL docker container") + Container(web_app, "WIQ Client", "React, Typescript", "nginx web server", $tags="Internal system") + Container(backend_api, "WIQ REST API","Java SpringBoot 3",".jar file", $tags="Internal system") + ContainerDb(database,"WIQ Database","PostgreSQL","PostgreSQL docker container", $tags="Internal system") } -System_Ext(wikidata,"WikiData API","REST API") +System_Ext(wikidata,"WikiData API","REST API", $tags="External system") 'RELATIONS Rel(player,web_app,"Uses","HTTPS") Rel(backend_api,wikidata,"Asks for data","SPARQL,HTTPS") Rel(web_app,backend_api,"Asks for user/game information","JSON,HTTPS") Rel(backend_api,database,"Stores game/user information","JPA") + + @enduml \ No newline at end of file From f538dc26cf312ea7be3b7975cd6876127eeff0b1 Mon Sep 17 00:00:00 2001 From: Diego Villanueva <98838739+UO283615@users.noreply.github.com> Date: Sun, 7 Apr 2024 18:25:14 +0200 Subject: [PATCH 11/63] chore: Removed prints --- api/src/main/java/lab/en2b/quizapi/game/GameService.java | 3 --- 1 file changed, 3 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 39704e44..e3e47e66 100644 --- a/api/src/main/java/lab/en2b/quizapi/game/GameService.java +++ b/api/src/main/java/lab/en2b/quizapi/game/GameService.java @@ -60,9 +60,6 @@ public GameResponseDto answerQuestion(Long id, GameAnswerDto dto, Authentication Game game = gameRepository.findByIdForUser(id, userService.getUserByAuthentication(authentication).getId()).orElseThrow(); game.answerQuestion(dto.getAnswerId(), questionRepository); - System.out.println("Current round: " + game.getActualRound()); - System.out.println("Total round: " + game.getRounds()); - if (game.isLastRound()){ game.setGameOver(true); gameRepository.save(game); From d51afdf8ba28bffd475955e0178074f076e5e0b2 Mon Sep 17 00:00:00 2001 From: Diego Villanueva <98838739+UO283615@users.noreply.github.com> Date: Sun, 7 Apr 2024 18:26:11 +0200 Subject: [PATCH 12/63] Chore: Updated the model --- docs/src/08_concepts.adoc | 56 +++++++++++++++++++-------------------- 1 file changed, 28 insertions(+), 28 deletions(-) diff --git a/docs/src/08_concepts.adoc b/docs/src/08_concepts.adoc index bb47d5c3..22230c6a 100644 --- a/docs/src/08_concepts.adoc +++ b/docs/src/08_concepts.adoc @@ -9,23 +9,18 @@ This is the first version of the diagram, it will be updated if needed. [plantuml,"ConceptsDomainModel1",png] ---- @startuml - enum QuestionCategory { - HISTORY GEOGRAPHY - SCIENCE - MATH - LITERATURE - ART SPORTS + MUSIC } -enum AnsswerCategory { - CITY +enum AnswerCategory { + CAPITAL_CITY COUNTRY - PERSON - DATE - OTHER + SONG + STADIUM + BALLON_DOR } enum QuestionType{ @@ -39,44 +34,49 @@ class Question{ answers: List correctAnswer: Answer questionCategory: QuestionCategory - answerCategory: AnswerCategory - language: String - QuestionType: Type + type: QuestionType + games: List } class User{ username: String email: String password: String - answeredQuestions: int + role: String + games: List } -class UserStat{ +class Statistics{ + correct: Long + wrong: Long + total: Long + user: User } class Answer { text: String category: AnswerCategory - questionsWithThisAnswer: List - + language: String } class Game { user: User questions: List + rounds: int + actualRound: int + correctlyAnsweredQuestions: int + language: String + roundStartTime: LocalDateTime + roundDuration: Integer + currentQuestionAnswered: boolean + isGameOver: boolean } -class Ranking << Singleton >> { - -} - -User o--> Question -User "1" --> "1" UserStat -Game o--> Question -Game "n" --> "n" User -Question "n" --> "n" Answer -Ranking "1" --> "n" User +User "1"--"1" Statistics +Game "n"--"n" Question +Game "1" -- "n" User +Question "n" -- "n" Answer @enduml ---- From 8732d465f1b4dd76a65b597449e136d7d87be220 Mon Sep 17 00:00:00 2001 From: Diego Villanueva <98838739+UO283615@users.noreply.github.com> Date: Sun, 7 Apr 2024 18:33:55 +0200 Subject: [PATCH 13/63] Docs: Changed the DB to validate instead of create --- api/src/main/resources/application.properties | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/api/src/main/resources/application.properties b/api/src/main/resources/application.properties index 8f7af9da..1d2d87c7 100644 --- a/api/src/main/resources/application.properties +++ b/api/src/main/resources/application.properties @@ -1,6 +1,6 @@ JWT_EXPIRATION_MS=86400000 REFRESH_TOKEN_DURATION_MS=86400000 -spring.jpa.hibernate.ddl-auto=create +spring.jpa.hibernate.ddl-auto=validate spring.datasource.url=${DATABASE_URL} spring.datasource.username=${DATABASE_USER} spring.datasource.password=${DATABASE_PASSWORD} @@ -9,4 +9,4 @@ springdoc.swagger-ui.path=/swagger/swagger-ui.html springdoc.api-docs.path=/swagger/api-docs management.endpoints.web.exposure.include=prometheus -management.endpoint.prometheus.enabled=true \ No newline at end of file +management.endpoint.prometheus.enabled=true From b889c6d95891d782f7521dc6f5735df45491ea57 Mon Sep 17 00:00:00 2001 From: Diego Villanueva <98838739+UO283615@users.noreply.github.com> Date: Sun, 7 Apr 2024 18:34:53 +0200 Subject: [PATCH 14/63] Docs: Updated the game definition --- docs/src/08_concepts.adoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/src/08_concepts.adoc b/docs/src/08_concepts.adoc index 22230c6a..2fe77cb0 100644 --- a/docs/src/08_concepts.adoc +++ b/docs/src/08_concepts.adoc @@ -86,7 +86,7 @@ Question "n" -- "n" Answer | Question | The model of the questions, has a type to specify if it is text, image or audio. Stores both right and wrong answers | User | The people using the application, they have statistics and take part in a ranking to compete | Answer | Models each possible answer, created to reuse answers that are common to different questions, as well as distractors -| Game | It is created when the user starts a game and destroyed just when it ends. +| Game | It is created when the user starts a game and includes the rounds that the user has to answer |=== .Architecture and design patterns From 01f194235a400c0903495ad7c293b50e9bd6dbb1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gonzalo=20Su=C3=A1rez=20Losada?= Date: Sun, 7 Apr 2024 18:55:19 +0200 Subject: [PATCH 15/63] fix: updated architectural decisions --- docs/src/09_architecture_decisions.adoc | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/docs/src/09_architecture_decisions.adoc b/docs/src/09_architecture_decisions.adoc index 0454c52f..28136dad 100644 --- a/docs/src/09_architecture_decisions.adoc +++ b/docs/src/09_architecture_decisions.adoc @@ -20,4 +20,10 @@ During the application development process, decisions had to be made as issues e |React Libraries |To enhance the efficiency and effectiveness of our development process, we've taken proactive steps to incorporate specific libraries into our project. These carefully chosen libraries, meticulously outlined in detail within issue #16. + +|HTTPS +|To improve security we have decided to make HTTPS as one main requirements in out project as can be seen in issue #51. |=== + +If needed, a more descriptive record can be seen link:https://github.com/Arquisoft/wiq_en2b/wiki[here]. + From c80e53785ea4c14f9f53a20510198dcd2bf24722 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gonzalo=20Su=C3=A1rez=20Losada?= Date: Sun, 7 Apr 2024 18:57:11 +0200 Subject: [PATCH 16/63] fix: updated availability --- docs/src/10_quality_requirements.adoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/src/10_quality_requirements.adoc b/docs/src/10_quality_requirements.adoc index b47f7d8c..cdd563ce 100644 --- a/docs/src/10_quality_requirements.adoc +++ b/docs/src/10_quality_requirements.adoc @@ -41,7 +41,7 @@ To obtain a measurable system response to stimulus corresponding to the various |Quality attribute|Scenario|Priority | Functional suitability | Users shall be able to register, log in, play the quiz, and access historical data without encountering errors or glitches. | High, Medium | Reliability | The system shall reliably generate accurate and diverse questions from Wikidata. User registrations, logins, and game data storage shall be handled without errors. | High, Medium -| Availability | The system shall be available 99.99% of the time when a user attempts to access it. | High, High +| Availability | The system shall be available 99% of the time when a user attempts to access it. | High, High | Performance efficiency | The system shall deliver optimal performance, ensuring responsive interactions for users. It shall efficiently generate questions from Wikidata and handle real-time gameplay with up to N concurrent users. | High, High | Usability | The system shall provide a user-friendly interface, allowing users to register, log in, and play the game with a learning time of less than 4 hours. | High, Medium | Security | User data shall be securely handled. Robust authentication mechanisms shall be in place for user registration and login. API access points for user information and generated questions shall be secured with proper authorization. | Medium, High From 66db64316ecba5fcbeeaa907ea945a45f71bc685 Mon Sep 17 00:00:00 2001 From: Diego Villanueva <98838739+UO283615@users.noreply.github.com> Date: Sun, 7 Apr 2024 18:57:11 +0200 Subject: [PATCH 17/63] Docs: Cross cutting concepts updated --- docs/src/08_concepts.adoc | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/docs/src/08_concepts.adoc b/docs/src/08_concepts.adoc index 2fe77cb0..743e5634 100644 --- a/docs/src/08_concepts.adoc +++ b/docs/src/08_concepts.adoc @@ -89,6 +89,13 @@ Question "n" -- "n" Answer | Game | It is created when the user starts a game and includes the rounds that the user has to answer |=== +.Question Generator +The Question Generator is an important part of our application, it is already briefly described in Section 6, but mopre insight is given here. + +The Question Generator module is written in Java and connects via HTTP with Wikidata query service. +It follows a template design pattern where each Java class is responsible for generating the questions and answers. +The query is ran against Wikidata and it returns a text in JSON format that is processed into the question and answers, which are later stored in the DB. + .Architecture and design patterns We decided to use a React based frontend with BootSpring for the backend, which will follow the model view controller pattern to make the code clear. From 2d49c9328fa1d406e8d03cd9cb432322b29caf91 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gonzalo=20Su=C3=A1rez=20Losada?= Date: Sun, 7 Apr 2024 18:57:55 +0200 Subject: [PATCH 18/63] fix: increased testability to meet sonar cloud requirements --- docs/src/10_quality_requirements.adoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/src/10_quality_requirements.adoc b/docs/src/10_quality_requirements.adoc index cdd563ce..f5f10a41 100644 --- a/docs/src/10_quality_requirements.adoc +++ b/docs/src/10_quality_requirements.adoc @@ -47,7 +47,7 @@ To obtain a measurable system response to stimulus corresponding to the various | Security | User data shall be securely handled. Robust authentication mechanisms shall be in place for user registration and login. API access points for user information and generated questions shall be secured with proper authorization. | Medium, High | Compatibility | The system shall be compatible with various web browsers and devices, providing a seamless experience for users regardless of their choice of platform. It shall be well-optimized for different screen sizes and functionalities. | High, Medium | Transferability | The system shall allow for easy transfer of user data and game-related information through its APIs. | Medium, High -| Testability | The unit tests shall have at least 75% coverage. | High, Medium +| Testability | The unit tests shall have at least 80% coverage. | High, Medium |=== ==== Change Scenarios [options="header",cols="1,3,1"] From 7394fa81b3b09551fc45cb9b4ec01b47d177b8ea Mon Sep 17 00:00:00 2001 From: Diego Villanueva <98838739+UO283615@users.noreply.github.com> Date: Mon, 8 Apr 2024 09:49:11 +0200 Subject: [PATCH 19/63] Fix: changed dB to update --- api/src/main/resources/application.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/src/main/resources/application.properties b/api/src/main/resources/application.properties index 1d2d87c7..a2042204 100644 --- a/api/src/main/resources/application.properties +++ b/api/src/main/resources/application.properties @@ -1,6 +1,6 @@ JWT_EXPIRATION_MS=86400000 REFRESH_TOKEN_DURATION_MS=86400000 -spring.jpa.hibernate.ddl-auto=validate +spring.jpa.hibernate.ddl-auto=update spring.datasource.url=${DATABASE_URL} spring.datasource.username=${DATABASE_USER} spring.datasource.password=${DATABASE_PASSWORD} From 8499be665acab3608224b0745685801c92673e8d Mon Sep 17 00:00:00 2001 From: Dario Date: Mon, 8 Apr 2024 15:57:35 +0200 Subject: [PATCH 20/63] 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 21/63] 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 22/63] 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 23/63] 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 24/63] 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 25/63] 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 26/63] 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 27/63] 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 28/63] 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 29/63] 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 6ba4ae2db3337ac458b4a968f32822e56b1b97f7 Mon Sep 17 00:00:00 2001 From: Diego Villanueva <98838739+UO283615@users.noreply.github.com> Date: Mon, 8 Apr 2024 19:44:13 +0200 Subject: [PATCH 30/63] Chore: Small change in the introduction --- docs/src/01_introduction_and_goals.adoc | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/src/01_introduction_and_goals.adoc b/docs/src/01_introduction_and_goals.adoc index 61e69a83..d3a42dfb 100644 --- a/docs/src/01_introduction_and_goals.adoc +++ b/docs/src/01_introduction_and_goals.adoc @@ -2,7 +2,7 @@ ifndef::imagesdir[:imagesdir: ../images] [[section-introduction-and-goals]] == Introduction and Goals -RTVE has hired the company HappySw, composed of students from the Oviedo School of Software Engineering, to develop a new experimental version of the quiz show Saber y Ganar. This application will be called WIQ, where users will be able to register and log in to play. The application will consist of answering questions of different types generated with Wikidata. For each question answered correctly, points will be obtained. +RTVE has hired the company HappySw, composed of students from the Oviedo School of Software Engineering, to develop a new experimental version of the quiz show Saber y Ganar. This application will be called KiWiq, where users will be able to register and log in to play. The application will consist of answering questions of different types generated with Wikidata. For each question answered correctly, points will be obtained. === Requirements Overview * The system shall provide non-registered users with the option to sign up. @@ -40,7 +40,7 @@ See the complete functional requirements in the xref:#section-annex[Annex] of th |=== |Role/Name|Expectations | RTVE | To have a new experimental version of the Saber y Ganar quiz show. -| HappySw | Develop a good application that fullfills the requirements expected by the client. +| HappySw | Develop a good application that fulfills the requirements expected by the client. | Registered user | To play with an entertaining and easy-to-use application. An application with which the user learn about different topics. | Wikidata | Being able to offer service allowing people to use the data through the API. |=== From 47d111028e28897124fe43af193e5b292422c55e Mon Sep 17 00:00:00 2001 From: Diego Villanueva <98838739+UO283615@users.noreply.github.com> Date: Mon, 8 Apr 2024 19:50:36 +0200 Subject: [PATCH 31/63] Chore: Added information about the QG --- docs/src/04_solution_strategy.adoc | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/src/04_solution_strategy.adoc b/docs/src/04_solution_strategy.adoc index bdb848a4..7d189696 100644 --- a/docs/src/04_solution_strategy.adoc +++ b/docs/src/04_solution_strategy.adoc @@ -17,6 +17,8 @@ Regarding the technologies, we decided to use the following ones: * **Java SpringBoot 3** for the backend/API, it being a language we are all comfortable with. The server will easily support multithreading if needed due to SpringBoot being an abstraction over servlets, something we would be able only to simulate if we used Node.js as it uses a single-threaded event loop. + * **Java ** for the Question Generator process, due to the ease for connecting with the DB via JPA and our familiarity. + === Organizational breakdown Currently, we have been meeting twice a week, but that may change in the future. Most, if not all of us, were present for these meetings, and relevant decisions were also discussed on the side and between members, as well discussions related to opinions on some matter. Some of these meetings took place in person while others took place on a Discord Server. @@ -46,4 +48,4 @@ Regarding the code style, we must make two important distinctions: the frontend * In the backend, the structure will be that of a typical Maven project. * In the frontend, the structure will be quite different: ** The `src/components` will contain single components which we may reuse. - ** The `src/pages` will contain the endpoints and will follow a simple structure. For instance, given a `/statistics/personal` and a `/statistics/general` endpoints, both will be independent React components that will return the page and be placed under the `src/pages/statistics` folder. \ No newline at end of file + ** The `src/pages` will contain the endpoints and will follow a simple structure. For instance, given a `/statistics/personal` and a `/statistics/general` endpoints, both will be independent React components that will return the page and be placed under the `src/pages/statistics` folder. From 99e5bcf7e2e858e5f47d1bbee9eea8f07aed6ede Mon Sep 17 00:00:00 2001 From: Diego Villanueva <98838739+UO283615@users.noreply.github.com> Date: Mon, 8 Apr 2024 19:52:15 +0200 Subject: [PATCH 32/63] Chore: Reviewed some info about the QG --- docs/src/05_building_block_view.adoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/src/05_building_block_view.adoc b/docs/src/05_building_block_view.adoc index b274f1ef..147b29e1 100644 --- a/docs/src/05_building_block_view.adoc +++ b/docs/src/05_building_block_view.adoc @@ -39,7 +39,7 @@ Contained Building Blocks:: **** * **WIQ Client:** This is the connection between the user and the application. It will allow the users to play the WIQ game. This part will be developed in React with Typescript for its clear component structure, simplified code quality and separation of concerns. * **WIQ REST API:** This is the part responsible for managing the users that log into the application, managing also the logic of the game and sending the request to the Wikidata API for the question generation. This part is going to be developed in SpringBoot due to its foundations on the Java programming language, which is the language that the developers find the easiest to develop in. -* **Question Generator Module:** This component is in charge of everything related with the questions. It retrieves the information from WikiData QS and stores it in the WIQ Database. It is also asked for questions and answers by the WIQ REST API. +* **Question Generator Module:** This component is in charge of everything related with the questions. It retrieves the information from WikiData QS and stores it in the WIQ Database, which later is queried for questions and answers from the REST API. * **WIQ Database:** This is where the most important data is going to be stored. Such as, users questions and other game info that will be specified in the future. The database we chose to use is PostgreSQL, since it is compatible with Docker and it's an object-relational kind of database, which is easier for the developers to use. Another alternative would've been to use MySQL. **** From 13b9d497008903e6bbb8ec902521b6fab1bb15d7 Mon Sep 17 00:00:00 2001 From: Diego Villanueva <98838739+UO283615@users.noreply.github.com> Date: Mon, 8 Apr 2024 19:55:48 +0200 Subject: [PATCH 33/63] Chore: More info in the docu about the QG --- docs/src/07_deployment_view.adoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/src/07_deployment_view.adoc b/docs/src/07_deployment_view.adoc index f9a446c1..703b45f0 100644 --- a/docs/src/07_deployment_view.adoc +++ b/docs/src/07_deployment_view.adoc @@ -43,4 +43,4 @@ Our main idea is that the server will be a self-contained .jar file with all the The database will contain the data used by the system. Therefore, it will contain user data, as well as the data related to the questions and their answers. The databases to store the questions (and therefore the answers) and the user data might be different, though. ===== Question Generator -The question generator will be run only at the beginning of the application. It will connect with Wikidata using SPARQL to generate questions and answers and store them in the database. This question generation will generate all the questions used by the application at once. +The question generator will be run only at the beginning of the application. It will connect with Wikidata using SPARQL to generate questions and answers and store them in the database. This question generation will generate all the questions used by the application at once. It could be run again to generate new questions if they are added. From 01288b8eadbdc3bf93b2e01891e09ebdbb68ba0a Mon Sep 17 00:00:00 2001 From: Diego Villanueva <98838739+UO283615@users.noreply.github.com> Date: Mon, 8 Apr 2024 20:00:43 +0200 Subject: [PATCH 34/63] Chore: Updated section 8 --- docs/src/08_concepts.adoc | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/src/08_concepts.adoc b/docs/src/08_concepts.adoc index 743e5634..27ab1de8 100644 --- a/docs/src/08_concepts.adoc +++ b/docs/src/08_concepts.adoc @@ -87,10 +87,11 @@ Question "n" -- "n" Answer | User | The people using the application, they have statistics and take part in a ranking to compete | Answer | Models each possible answer, created to reuse answers that are common to different questions, as well as distractors | Game | It is created when the user starts a game and includes the rounds that the user has to answer +| Statistics | Stores information about the amount of correct and wrong answers that each user has answered |=== .Question Generator -The Question Generator is an important part of our application, it is already briefly described in Section 6, but mopre insight is given here. +The Question Generator is an important part of our application, it is already briefly described in Section 6, but more insight is given here. The Question Generator module is written in Java and connects via HTTP with Wikidata query service. It follows a template design pattern where each Java class is responsible for generating the questions and answers. @@ -113,4 +114,4 @@ In order to archieve this, we will implement two modules regarding questions, on Our code will be deployed within an Azure's Virtual Machine using continuous integration. .Under-the-hood: -Regarding Data persistence, our project has two DB, one for storing questions as stated before while the other one will be in charge of storing any other meaningful data for the game such us users or game's histories. +Regarding Data persistence, our project has a PostgreSQL DB, which stores information about users, statistics, games, answers and questions. From 03b410c24f792087601a1d8e0b148b62832cfc87 Mon Sep 17 00:00:00 2001 From: Diego Villanueva <98838739+UO283615@users.noreply.github.com> Date: Mon, 8 Apr 2024 20:03:32 +0200 Subject: [PATCH 35/63] Chore: Added an architectural decission to the docu --- docs/src/09_architecture_decisions.adoc | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/src/09_architecture_decisions.adoc b/docs/src/09_architecture_decisions.adoc index 28136dad..421aff80 100644 --- a/docs/src/09_architecture_decisions.adoc +++ b/docs/src/09_architecture_decisions.adoc @@ -23,6 +23,9 @@ During the application development process, decisions had to be made as issues e |HTTPS |To improve security we have decided to make HTTPS as one main requirements in out project as can be seen in issue #51. + +|Architecture of the Question Generator +|It has been decided to implement the QG in an independent module so it is only run once and it is not generating questions at real time, this way, we are not dependent on wikidata for having our application available. |=== If needed, a more descriptive record can be seen link:https://github.com/Arquisoft/wiq_en2b/wiki[here]. From 063185165f6a6e93b0fcf02294ecacf90c2695b0 Mon Sep 17 00:00:00 2001 From: Dario Date: Mon, 8 Apr 2024 20:29:02 +0200 Subject: [PATCH 36/63] 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 37/63] 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 38/63] 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 39/63] 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 40/63] 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 41/63] 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 41dc7f616c5110ac8bdc22971fe3f523e6d56c97 Mon Sep 17 00:00:00 2001 From: jjgancfer Date: Mon, 8 Apr 2024 22:36:22 +0200 Subject: [PATCH 42/63] feat: add a config to tests that work --- ...ve_logged_user_seeing_about_screen.feature | 4 +- webapp/e2e/jest.config.js | 4 +- webapp/e2e/steps/about.steps.js | 60 +++++++++++++++++++ webapp/e2e/steps/register-form.steps.js | 52 ---------------- webapp/e2e/test-environment-setup.js | 19 ------ webapp/package.json | 2 +- webapp/src/components/LateralMenu.jsx | 2 +- webapp/src/components/MenuButton.jsx | 2 +- webapp/src/pages/About.jsx | 3 +- 9 files changed, 68 insertions(+), 80 deletions(-) create mode 100644 webapp/e2e/steps/about.steps.js delete mode 100644 webapp/e2e/steps/register-form.steps.js delete mode 100644 webapp/e2e/test-environment-setup.js diff --git a/webapp/e2e/features/about_features/positive_logged_user_seeing_about_screen.feature b/webapp/e2e/features/about_features/positive_logged_user_seeing_about_screen.feature index b75ae049..63d9fa6c 100644 --- a/webapp/e2e/features/about_features/positive_logged_user_seeing_about_screen.feature +++ b/webapp/e2e/features/about_features/positive_logged_user_seeing_about_screen.feature @@ -3,5 +3,5 @@ Feature: Seeing the about screen of the webpage Scenario: A logged user wants to see the about screen of the webpage Given A logged user in the main menu When The user presses the button for deploying the lateral menu - And the user presses the button for seeing the about secction (i) - Then The screen shows redirects the user to the about screen \ No newline at end of file + And the user presses the button for seeing the about section (i) + Then The user is presented to the about screen \ No newline at end of file diff --git a/webapp/e2e/jest.config.js b/webapp/e2e/jest.config.js index db3be3d9..147c0817 100644 --- a/webapp/e2e/jest.config.js +++ b/webapp/e2e/jest.config.js @@ -1,5 +1,5 @@ module.exports = { testMatch: ["**/steps/*.js"], - testTimeout: 30000, - setupFilesAfterEnv: ["expect-puppeteer"] + moduleFileExtensions: ["ts", "tsx", "js", "jsx", "json", "node"], + testTimeout: 30000 } \ No newline at end of file diff --git a/webapp/e2e/steps/about.steps.js b/webapp/e2e/steps/about.steps.js new file mode 100644 index 00000000..4730db3a --- /dev/null +++ b/webapp/e2e/steps/about.steps.js @@ -0,0 +1,60 @@ +const { defineFeature, loadFeature }=require('jest-cucumber'); +const puppeteer = require('puppeteer'); +const setDefaultOptions = require("expect-puppeteer").setDefaultOptions; +const feature = loadFeature('./features/about_features/positive_logged_user_seeing_about_screen.feature'); +let page; +let browser; + +defineFeature(feature, test => { + + beforeAll(async () => { + browser = process.env.GITHUB_ACTIONS + ? await puppeteer.launch() + : await puppeteer.launch({ headless: false, slowMo: 100 }); + page = await browser.newPage(); + //Way of setting up the timeout + setDefaultOptions({ timeout: 10000 }) + + await page + .goto("http://localhost:3000", { + waitUntil: "networkidle0", + }) + .catch(() => {}); + }); + + test("A logged user wants to see the about screen of the webpage", ({given,when,and,then}) => { + + let username; + let password; + + given("A logged user in the main menu", async () => { + username = "test@email.com" + password = "password" + + await expect(page).toClick("button[data-testid='Login'"); + await expect(page).toFill("#user", username); + await expect(page).toFill("#password", password); + await expect(page).toClick("button[data-testid='Login'"); + }); + + when("The user presses the button for deploying the lateral menu", async () => { + await expect(page).toClick("#lateralMenuButton"); + }); + + and("the user presses the button for seeing the about section (i)", async () => { + await expect(page).toClick("#aboutButton"); + }); + + then("The user is presented to the about screen", async () => { + let header = await page.$eval("h2", (element) => { + return element.innerHTML + }) + let value = header === "About" || header === "Sobre nosotros"; + expect(value).toBeTruthy(); + }); + }); + + afterAll((done) => { + done(); + }); +}); \ No newline at end of file diff --git a/webapp/e2e/steps/register-form.steps.js b/webapp/e2e/steps/register-form.steps.js deleted file mode 100644 index 172e1969..00000000 --- a/webapp/e2e/steps/register-form.steps.js +++ /dev/null @@ -1,52 +0,0 @@ -const puppeteer = require('puppeteer'); -const { defineFeature, loadFeature }=require('jest-cucumber'); -const setDefaultOptions = require('expect-puppeteer').setDefaultOptions -const feature = loadFeature('./features/register-form.feature'); - -let page; -let browser; - -defineFeature(feature, test => { - - beforeAll(async () => { - browser = process.env.GITHUB_ACTIONS - ? await puppeteer.launch() - : await puppeteer.launch({ headless: false, slowMo: 100 }); - page = await browser.newPage(); - //Way of setting up the timeout - setDefaultOptions({ timeout: 10000 }) - - await page - .goto("http://localhost:3000", { - waitUntil: "networkidle0", - }) - .catch(() => {}); - }); - - test('The user is not registered in the site', ({given,when,then}) => { - - let username; - let password; - - given('An unregistered user', async () => { - username = "pablo" - password = "pabloasw" - await expect(page).toClick("button", { text: "Don't have an account? Register here." }); - }); - - when('I fill the data in the form and press submit', async () => { - await expect(page).toFill('input[name="username"]', username); - await expect(page).toFill('input[name="password"]', password); - await expect(page).toClick('button', { text: 'Add User' }) - }); - - then('A confirmation message should be shown in the screen', async () => { - await expect(page).toMatchElement("div", { text: "User added successfully" }); - }); - }) - - afterAll(async ()=>{ - browser.close() - }) - -}); \ No newline at end of file diff --git a/webapp/e2e/test-environment-setup.js b/webapp/e2e/test-environment-setup.js deleted file mode 100644 index 7b7ed511..00000000 --- a/webapp/e2e/test-environment-setup.js +++ /dev/null @@ -1,19 +0,0 @@ -const { MongoMemoryServer } = require('mongodb-memory-server'); - - -let mongoserver; -let userservice; -let authservice; -let gatewayservice; - -async function startServer() { - console.log('Starting MongoDB memory server...'); - mongoserver = await MongoMemoryServer.create(); - const mongoUri = mongoserver.getUri(); - process.env.MONGODB_URI = mongoUri; - userservice = await require("../../users/userservice/user-service"); - authservice = await require("../../users/authservice/auth-service"); - gatewayservice = await require("../../gatewayservice/gateway-service"); - } - - startServer(); diff --git a/webapp/package.json b/webapp/package.json index 00f78c59..7b1b35ac 100644 --- a/webapp/package.json +++ b/webapp/package.json @@ -35,7 +35,7 @@ "build": "react-scripts build", "prod": "serve -s build", "test": "react-scripts test --transformIgnorePatterns 'node_modules/(?!axios)/'", - "test:e2e": "start-server-and-test 'node e2e/test-environment-setup.js' http://localhost:8000/health prod 3000 \"cd e2e && jest\"", + "test:e2e": "start-server-and-test start 3000 \"cd e2e && jest --detectOpenHandles\"", "eject": "react-scripts eject" }, "eslintConfig": { diff --git a/webapp/src/components/LateralMenu.jsx b/webapp/src/components/LateralMenu.jsx index c9066075..b6a63379 100644 --- a/webapp/src/components/LateralMenu.jsx +++ b/webapp/src/components/LateralMenu.jsx @@ -103,7 +103,7 @@ const LateralMenu = ({ isOpen, onClose, changeLanguage, isDashboard }) => { {isLoggedIn && ( )} - } className={"custom-button effect1"} onClick={() => {navigate("/about");}} margin={"10px"}> + } className={"custom-button effect1"} onClick={() => {navigate("/about");}} margin={"10px"} id={"aboutButton"}> diff --git a/webapp/src/components/MenuButton.jsx b/webapp/src/components/MenuButton.jsx index 73e5a3e7..8c4da367 100644 --- a/webapp/src/components/MenuButton.jsx +++ b/webapp/src/components/MenuButton.jsx @@ -22,7 +22,7 @@ const MenuButton = ({ onClick }) => { }, []); return ( - + {t('about.title')} - + {t("about.description1")} -

From 60b2faf044035d042b2b61fbcb8196720b15f7e0 Mon Sep 17 00:00:00 2001 From: Dario Date: Mon, 8 Apr 2024 22:47:41 +0200 Subject: [PATCH 43/63] 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 44/63] 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 } }); From 81f5b0335c022d98fe5b77dbcc30b0fa86d12953 Mon Sep 17 00:00:00 2001 From: jjgancfer Date: Mon, 8 Apr 2024 23:06:12 +0200 Subject: [PATCH 45/63] chore: restore e2e --- .github/workflows/release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index b082ddb8..dc6639b7 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -41,7 +41,7 @@ jobs: node-version: 20 - run: npm --prefix webapp install - run: npm --prefix webapp run build - #- run: npm --prefix webapp run test:e2e TODO: re-enable + - run: npm --prefix webapp run test:e2e docker-push-api: runs-on: ubuntu-latest needs: [ e2e-tests ] From 61d4ffb673f85e437883403df2f1c0193df98faa Mon Sep 17 00:00:00 2001 From: jjgancfer Date: Mon, 8 Apr 2024 23:08:23 +0200 Subject: [PATCH 46/63] chore: restore e2e --- .github/workflows/release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index b082ddb8..dc6639b7 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -41,7 +41,7 @@ jobs: node-version: 20 - run: npm --prefix webapp install - run: npm --prefix webapp run build - #- run: npm --prefix webapp run test:e2e TODO: re-enable + - run: npm --prefix webapp run test:e2e docker-push-api: runs-on: ubuntu-latest needs: [ e2e-tests ] From aa0cb9213c285ac7b301b835c6fd8b9a323ed77c Mon Sep 17 00:00:00 2001 From: jjgancfer Date: Mon, 8 Apr 2024 23:26:52 +0200 Subject: [PATCH 47/63] fix: dependencies in Game.jsx --- ...ve_logged_user_seeing_about_screen.feature | 12 +- webapp/e2e/jest.config.js | 8 +- webapp/e2e/steps/register-form.steps.js | 102 ++-- webapp/e2e/test-environment-setup.js | 38 +- webapp/package.json | 146 +++--- webapp/src/components/LateralMenu.jsx | 244 ++++----- webapp/src/components/MenuButton.jsx | 86 +-- webapp/src/pages/About.jsx | 146 +++--- webapp/src/pages/Game.jsx | 495 +++++++++--------- 9 files changed, 639 insertions(+), 638 deletions(-) diff --git a/webapp/e2e/features/about_features/positive_logged_user_seeing_about_screen.feature b/webapp/e2e/features/about_features/positive_logged_user_seeing_about_screen.feature index b75ae049..ac028cd1 100644 --- a/webapp/e2e/features/about_features/positive_logged_user_seeing_about_screen.feature +++ b/webapp/e2e/features/about_features/positive_logged_user_seeing_about_screen.feature @@ -1,7 +1,7 @@ -Feature: Seeing the about screen of the webpage - - Scenario: A logged user wants to see the about screen of the webpage - Given A logged user in the main menu - When The user presses the button for deploying the lateral menu - And the user presses the button for seeing the about secction (i) +Feature: Seeing the about screen of the webpage + + Scenario: A logged user wants to see the about screen of the webpage + Given A logged user in the main menu + When The user presses the button for deploying the lateral menu + And the user presses the button for seeing the about secction (i) Then The screen shows redirects the user to the about screen \ No newline at end of file diff --git a/webapp/e2e/jest.config.js b/webapp/e2e/jest.config.js index db3be3d9..75fb744e 100644 --- a/webapp/e2e/jest.config.js +++ b/webapp/e2e/jest.config.js @@ -1,5 +1,5 @@ -module.exports = { - testMatch: ["**/steps/*.js"], - testTimeout: 30000, - setupFilesAfterEnv: ["expect-puppeteer"] +module.exports = { + testMatch: ["**/steps/*.js"], + testTimeout: 30000, + setupFilesAfterEnv: ["expect-puppeteer"] } \ No newline at end of file diff --git a/webapp/e2e/steps/register-form.steps.js b/webapp/e2e/steps/register-form.steps.js index 172e1969..fdfff05a 100644 --- a/webapp/e2e/steps/register-form.steps.js +++ b/webapp/e2e/steps/register-form.steps.js @@ -1,52 +1,52 @@ -const puppeteer = require('puppeteer'); -const { defineFeature, loadFeature }=require('jest-cucumber'); -const setDefaultOptions = require('expect-puppeteer').setDefaultOptions -const feature = loadFeature('./features/register-form.feature'); - -let page; -let browser; - -defineFeature(feature, test => { - - beforeAll(async () => { - browser = process.env.GITHUB_ACTIONS - ? await puppeteer.launch() - : await puppeteer.launch({ headless: false, slowMo: 100 }); - page = await browser.newPage(); - //Way of setting up the timeout - setDefaultOptions({ timeout: 10000 }) - - await page - .goto("http://localhost:3000", { - waitUntil: "networkidle0", - }) - .catch(() => {}); - }); - - test('The user is not registered in the site', ({given,when,then}) => { - - let username; - let password; - - given('An unregistered user', async () => { - username = "pablo" - password = "pabloasw" - await expect(page).toClick("button", { text: "Don't have an account? Register here." }); - }); - - when('I fill the data in the form and press submit', async () => { - await expect(page).toFill('input[name="username"]', username); - await expect(page).toFill('input[name="password"]', password); - await expect(page).toClick('button', { text: 'Add User' }) - }); - - then('A confirmation message should be shown in the screen', async () => { - await expect(page).toMatchElement("div", { text: "User added successfully" }); - }); - }) - - afterAll(async ()=>{ - browser.close() - }) - +const puppeteer = require('puppeteer'); +const { defineFeature, loadFeature }=require('jest-cucumber'); +const setDefaultOptions = require('expect-puppeteer').setDefaultOptions +const feature = loadFeature('./features/register-form.feature'); + +let page; +let browser; + +defineFeature(feature, test => { + + beforeAll(async () => { + browser = process.env.GITHUB_ACTIONS + ? await puppeteer.launch() + : await puppeteer.launch({ headless: false, slowMo: 100 }); + page = await browser.newPage(); + //Way of setting up the timeout + setDefaultOptions({ timeout: 10000 }) + + await page + .goto("http://localhost:3000", { + waitUntil: "networkidle0", + }) + .catch(() => {}); + }); + + test('The user is not registered in the site', ({given,when,then}) => { + + let username; + let password; + + given('An unregistered user', async () => { + username = "pablo" + password = "pabloasw" + await expect(page).toClick("button", { text: "Don't have an account? Register here." }); + }); + + when('I fill the data in the form and press submit', async () => { + await expect(page).toFill('input[name="username"]', username); + await expect(page).toFill('input[name="password"]', password); + await expect(page).toClick('button', { text: 'Add User' }) + }); + + then('A confirmation message should be shown in the screen', async () => { + await expect(page).toMatchElement("div", { text: "User added successfully" }); + }); + }) + + afterAll(async ()=>{ + browser.close() + }) + }); \ No newline at end of file diff --git a/webapp/e2e/test-environment-setup.js b/webapp/e2e/test-environment-setup.js index 7b7ed511..d6a4f5cc 100644 --- a/webapp/e2e/test-environment-setup.js +++ b/webapp/e2e/test-environment-setup.js @@ -1,19 +1,19 @@ -const { MongoMemoryServer } = require('mongodb-memory-server'); - - -let mongoserver; -let userservice; -let authservice; -let gatewayservice; - -async function startServer() { - console.log('Starting MongoDB memory server...'); - mongoserver = await MongoMemoryServer.create(); - const mongoUri = mongoserver.getUri(); - process.env.MONGODB_URI = mongoUri; - userservice = await require("../../users/userservice/user-service"); - authservice = await require("../../users/authservice/auth-service"); - gatewayservice = await require("../../gatewayservice/gateway-service"); - } - - startServer(); +const { MongoMemoryServer } = require('mongodb-memory-server'); + + +let mongoserver; +let userservice; +let authservice; +let gatewayservice; + +async function startServer() { + console.log('Starting MongoDB memory server...'); + mongoserver = await MongoMemoryServer.create(); + const mongoUri = mongoserver.getUri(); + process.env.MONGODB_URI = mongoUri; + userservice = await require("../../users/userservice/user-service"); + authservice = await require("../../users/authservice/auth-service"); + gatewayservice = await require("../../gatewayservice/gateway-service"); + } + + startServer(); diff --git a/webapp/package.json b/webapp/package.json index 00f78c59..af58c4dd 100644 --- a/webapp/package.json +++ b/webapp/package.json @@ -1,73 +1,73 @@ -{ - "name": "webapp", - "version": "0.1.0", - "private": true, - "dependencies": { - "@chakra-ui/icons": "^2.1.1", - "@chakra-ui/react": "^2.8.2", - "@emotion/react": "^11.11.4", - "@emotion/styled": "^11.11.0", - "@fontsource-variable/outfit": "^5.0.12", - "@testing-library/jest-dom": "^5.17.0", - "@testing-library/react": "^14.1.2", - "@testing-library/user-event": "^14.5.2", - "axios": "^1.6.5", - "dotenv": "^16.4.1", - "framer-motion": "^11.0.6", - "i18next": "^23.8.2", - "i18next-browser-languagedetector": "^7.2.0", - "i18next-http-backend": "^2.4.3", - "prop-types": "^15.8.1", - "react": "^18.2.0", - "react-confetti": "^6.1.0", - "react-dom": "^18.2.0", - "react-i18next": "^14.0.5", - "react-icons": "^5.0.1", - "react-router": "^6.21.3", - "react-router-dom": "^6.21.3", - "react-scripts": "5.0.1", - "react-use": "^17.5.0", - "recharts": "^2.12.3", - "web-vitals": "^3.5.1" - }, - "scripts": { - "start": "react-scripts start", - "build": "react-scripts build", - "prod": "serve -s build", - "test": "react-scripts test --transformIgnorePatterns 'node_modules/(?!axios)/'", - "test:e2e": "start-server-and-test 'node e2e/test-environment-setup.js' http://localhost:8000/health prod 3000 \"cd e2e && jest\"", - "eject": "react-scripts eject" - }, - "eslintConfig": { - "extends": [ - "react-app", - "react-app/jest" - ] - }, - "browserslist": { - "production": [ - ">0.2%", - "not dead", - "not op_mini all" - ], - "development": [ - "last 1 chrome version", - "last 1 firefox version", - "last 1 safari version" - ] - }, - "devDependencies": { - "@babel/plugin-proposal-private-property-in-object": "^7.21.11", - "@babel/plugin-transform-private-property-in-object": "^7.23.4", - "axios-mock-adapter": "^1.22.0", - "expect-puppeteer": "^9.0.2", - "jest": "^29.3.1", - "jest-cucumber": "^3.0.1", - "jest-each": "^29.7.0", - "jest-environment-node": "^29.7.0", - "mongodb-memory-server": "^9.1.4", - "puppeteer": "^21.7.0", - "serve": "^14.2.1", - "start-server-and-test": "^2.0.3" - } -} +{ + "name": "webapp", + "version": "0.1.0", + "private": true, + "dependencies": { + "@chakra-ui/icons": "^2.1.1", + "@chakra-ui/react": "^2.8.2", + "@emotion/react": "^11.11.4", + "@emotion/styled": "^11.11.0", + "@fontsource-variable/outfit": "^5.0.12", + "@testing-library/jest-dom": "^5.17.0", + "@testing-library/react": "^14.1.2", + "@testing-library/user-event": "^14.5.2", + "axios": "^1.6.5", + "dotenv": "^16.4.1", + "framer-motion": "^11.0.6", + "i18next": "^23.8.2", + "i18next-browser-languagedetector": "^7.2.0", + "i18next-http-backend": "^2.4.3", + "prop-types": "^15.8.1", + "react": "^18.2.0", + "react-confetti": "^6.1.0", + "react-dom": "^18.2.0", + "react-i18next": "^14.0.5", + "react-icons": "^5.0.1", + "react-router": "^6.21.3", + "react-router-dom": "^6.21.3", + "react-scripts": "5.0.1", + "react-use": "^17.5.0", + "recharts": "^2.12.3", + "web-vitals": "^3.5.1" + }, + "scripts": { + "start": "react-scripts start", + "build": "react-scripts build", + "prod": "serve -s build", + "test": "react-scripts test --transformIgnorePatterns 'node_modules/(?!axios)/'", + "test:e2e": "start-server-and-test 'node e2e/test-environment-setup.js' http://localhost:8000/health prod 3000 \"cd e2e && jest\"", + "eject": "react-scripts eject" + }, + "eslintConfig": { + "extends": [ + "react-app", + "react-app/jest" + ] + }, + "browserslist": { + "production": [ + ">0.2%", + "not dead", + "not op_mini all" + ], + "development": [ + "last 1 chrome version", + "last 1 firefox version", + "last 1 safari version" + ] + }, + "devDependencies": { + "@babel/plugin-proposal-private-property-in-object": "^7.21.11", + "@babel/plugin-transform-private-property-in-object": "^7.23.4", + "axios-mock-adapter": "^1.22.0", + "expect-puppeteer": "^9.0.2", + "jest": "^29.3.1", + "jest-cucumber": "^3.0.1", + "jest-each": "^29.7.0", + "jest-environment-node": "^29.7.0", + "mongodb-memory-server": "^9.1.4", + "puppeteer": "^21.7.0", + "serve": "^14.2.1", + "start-server-and-test": "^2.0.3" + } +} diff --git a/webapp/src/components/LateralMenu.jsx b/webapp/src/components/LateralMenu.jsx index c9066075..6fc781c8 100644 --- a/webapp/src/components/LateralMenu.jsx +++ b/webapp/src/components/LateralMenu.jsx @@ -1,122 +1,122 @@ -import React, { useState, useEffect } from 'react'; -import PropTypes from 'prop-types'; -import { useNavigate } from "react-router-dom"; -import { useTranslation } from "react-i18next"; -import { Box, Drawer, DrawerOverlay, DrawerContent, DrawerCloseButton, DrawerHeader, DrawerBody, DrawerFooter, Select, Button, Text, IconButton, Flex, Image } from '@chakra-ui/react'; -import { FaChartBar, FaBook, FaTachometerAlt } from 'react-icons/fa'; -import { InfoIcon, SettingsIcon } from '@chakra-ui/icons'; - -import AuthManager from "components/auth/AuthManager"; - -const LateralMenu = ({ isOpen, onClose, changeLanguage, isDashboard }) => { - const navigate = useNavigate(); - const [selectedLanguage, setSelectedLanguage] = useState(''); - const [isLoggedIn, setIsLoggedIn] = useState(false); - const { t } = useTranslation(); - - useEffect(() => { - checkIsLoggedIn(); - }, []); - - const handleChangeLanguage = (e) => { - const selectedValue = e.target.value; - setSelectedLanguage(selectedValue); - changeLanguage(selectedValue); - }; - - const handleApiClick = () => { - window.open(`http://${process.env.REACT_APP_API_ENDPOINT}/swagger/swagger-ui/index.html#/auth-controller/registerUser`, "_blank", "noopener"); - }; - - const handleLogout = async () => { - try { - await new AuthManager().logout(); - navigate("/"); - } catch (error) { - console.error("Error al cerrar sesión:", error); - } - }; - - const checkIsLoggedIn = async () => { - try { - const loggedIn = await new AuthManager().isLoggedIn(); - setIsLoggedIn(loggedIn); - } catch (error) { - console.error("Error al verificar el estado de inicio de sesión:", error); - } - }; - - return ( - - - - - - - KIWIQ - - - - - {t("common.language")} - - - - {isLoggedIn && ( - <> - {!isDashboard && ( - - - - )} - - - - - - - - - - - )} - - - - - - {isLoggedIn && ( - - )} - } className={"custom-button effect1"} onClick={() => {navigate("/about");}} margin={"10px"}> - - - - - - ); -}; - -LateralMenu.propTypes = { - isOpen: PropTypes.bool.isRequired, - onClose: PropTypes.func.isRequired, - changeLanguage: PropTypes.func.isRequired, - isDashboard: PropTypes.bool.isRequired -}; - -export default LateralMenu; +import React, { useState, useEffect } from 'react'; +import PropTypes from 'prop-types'; +import { useNavigate } from "react-router-dom"; +import { useTranslation } from "react-i18next"; +import { Box, Drawer, DrawerOverlay, DrawerContent, DrawerCloseButton, DrawerHeader, DrawerBody, DrawerFooter, Select, Button, Text, IconButton, Flex, Image } from '@chakra-ui/react'; +import { FaChartBar, FaBook, FaTachometerAlt } from 'react-icons/fa'; +import { InfoIcon, SettingsIcon } from '@chakra-ui/icons'; + +import AuthManager from "components/auth/AuthManager"; + +const LateralMenu = ({ isOpen, onClose, changeLanguage, isDashboard }) => { + const navigate = useNavigate(); + const [selectedLanguage, setSelectedLanguage] = useState(''); + const [isLoggedIn, setIsLoggedIn] = useState(false); + const { t } = useTranslation(); + + useEffect(() => { + checkIsLoggedIn(); + }, []); + + const handleChangeLanguage = (e) => { + const selectedValue = e.target.value; + setSelectedLanguage(selectedValue); + changeLanguage(selectedValue); + }; + + const handleApiClick = () => { + window.open(`http://${process.env.REACT_APP_API_ENDPOINT}/swagger/swagger-ui/index.html#/auth-controller/registerUser`, "_blank", "noopener"); + }; + + const handleLogout = async () => { + try { + await new AuthManager().logout(); + navigate("/"); + } catch (error) { + console.error("Error al cerrar sesión:", error); + } + }; + + const checkIsLoggedIn = async () => { + try { + const loggedIn = await new AuthManager().isLoggedIn(); + setIsLoggedIn(loggedIn); + } catch (error) { + console.error("Error al verificar el estado de inicio de sesión:", error); + } + }; + + return ( + + + + + + + KIWIQ + + + + + {t("common.language")} + + + + {isLoggedIn && ( + <> + {!isDashboard && ( + + + + )} + + + + + + + + + + + )} + + + + + + {isLoggedIn && ( + + )} + } className={"custom-button effect1"} onClick={() => {navigate("/about");}} margin={"10px"}> + + + + + + ); +}; + +LateralMenu.propTypes = { + isOpen: PropTypes.bool.isRequired, + onClose: PropTypes.func.isRequired, + changeLanguage: PropTypes.func.isRequired, + isDashboard: PropTypes.bool.isRequired +}; + +export default LateralMenu; diff --git a/webapp/src/components/MenuButton.jsx b/webapp/src/components/MenuButton.jsx index 73e5a3e7..b321675a 100644 --- a/webapp/src/components/MenuButton.jsx +++ b/webapp/src/components/MenuButton.jsx @@ -1,44 +1,44 @@ -import React, { useState, useEffect } from "react"; -import PropTypes from 'prop-types'; -import { FaBars } from "react-icons/fa"; -import { chakra, Box, IconButton } from "@chakra-ui/react"; - -const MenuButton = ({ onClick }) => { - const [isFixed, setIsFixed] = useState(false); - - useEffect(() => { - const handleScroll = () => { - if (window.scrollY > 0) { - setIsFixed(true); - } else { - setIsFixed(false); - } - }; - - window.addEventListener("scroll", handleScroll); - return () => { - window.removeEventListener("scroll", handleScroll); - }; - }, []); - - return ( - - } - onClick={onClick} - /> - - ); -} - -MenuButton.propTypes = { - onClick: PropTypes.func.isRequired -}; - +import React, { useState, useEffect } from "react"; +import PropTypes from 'prop-types'; +import { FaBars } from "react-icons/fa"; +import { chakra, Box, IconButton } from "@chakra-ui/react"; + +const MenuButton = ({ onClick }) => { + const [isFixed, setIsFixed] = useState(false); + + useEffect(() => { + const handleScroll = () => { + if (window.scrollY > 0) { + setIsFixed(true); + } else { + setIsFixed(false); + } + }; + + window.addEventListener("scroll", handleScroll); + return () => { + window.removeEventListener("scroll", handleScroll); + }; + }, []); + + return ( + + } + onClick={onClick} + /> + + ); +} + +MenuButton.propTypes = { + onClick: PropTypes.func.isRequired +}; + export default MenuButton; \ No newline at end of file diff --git a/webapp/src/pages/About.jsx b/webapp/src/pages/About.jsx index 58a79186..cf1fd6f0 100644 --- a/webapp/src/pages/About.jsx +++ b/webapp/src/pages/About.jsx @@ -1,74 +1,74 @@ -import React, { useState } from "react"; -import { useTranslation } from 'react-i18next'; -import { Center, Heading, Stack, Box, Text, Table, Thead, Tr, Td, Th, Tbody, Container } from '@chakra-ui/react'; -import { InfoIcon } from '@chakra-ui/icons'; - -import LateralMenu from '../components/LateralMenu'; -import MenuButton from '../components/MenuButton'; -import GoBack from "components/GoBack"; - -export default function About() { - const { t, i18n } = useTranslation(); - const [isMenuOpen, setIsMenuOpen] = useState(false); - - const changeLanguage = (selectedLanguage) => { - i18n.changeLanguage(selectedLanguage); - }; - - return ( -
- setIsMenuOpen(true)} /> - setIsMenuOpen(false)} changeLanguage={changeLanguage} isDashboard={false}/> - - - - {t('about.title')} - - - {t("about.description1")} -

-
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
{t("about.table1")}{t("about.table2")}
Gonzalo Alonso FernándezUO282104
Sergio Rodríguez GarcíaUO282598
Jorge Joaquín Gancedo FernándezUO282161
Darío Gutiérrez MoriUO282435
Sergio Quintana FernándezUO288090
Diego Villanueva BerrosUO283615
Gonzalo Suárez LosadaUO283928
- -
- -
- ); +import React, { useState } from "react"; +import { useTranslation } from 'react-i18next'; +import { Center, Heading, Stack, Box, Text, Table, Thead, Tr, Td, Th, Tbody, Container } from '@chakra-ui/react'; +import { InfoIcon } from '@chakra-ui/icons'; + +import LateralMenu from '../components/LateralMenu'; +import MenuButton from '../components/MenuButton'; +import GoBack from "components/GoBack"; + +export default function About() { + const { t, i18n } = useTranslation(); + const [isMenuOpen, setIsMenuOpen] = useState(false); + + const changeLanguage = (selectedLanguage) => { + i18n.changeLanguage(selectedLanguage); + }; + + return ( +
+ setIsMenuOpen(true)} /> + setIsMenuOpen(false)} changeLanguage={changeLanguage} isDashboard={false}/> + + + + {t('about.title')} + + + {t("about.description1")} +

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
{t("about.table1")}{t("about.table2")}
Gonzalo Alonso FernándezUO282104
Sergio Rodríguez GarcíaUO282598
Jorge Joaquín Gancedo FernándezUO282161
Darío Gutiérrez MoriUO282435
Sergio Quintana FernándezUO288090
Diego Villanueva BerrosUO283615
Gonzalo Suárez LosadaUO283928
+ +
+
+
+ ); } \ No newline at end of file diff --git a/webapp/src/pages/Game.jsx b/webapp/src/pages/Game.jsx index 918c4a61..a5ec1f09 100644 --- a/webapp/src/pages/Game.jsx +++ b/webapp/src/pages/Game.jsx @@ -1,248 +1,249 @@ -import React, { useState, useEffect, useCallback } from "react"; -import { Grid, Flex, Heading, Button, Box, Text, Spinner, CircularProgress } from "@chakra-ui/react"; -import { Center } from "@chakra-ui/layout"; -import { useNavigate } from "react-router-dom"; -import { useTranslation } from "react-i18next"; -import Confetti from "react-confetti"; -import { newGame, startRound, getCurrentQuestion, answerQuestion } from '../components/game/Game'; -import LateralMenu from '../components/LateralMenu'; -import MenuButton from '../components/MenuButton'; - -export default function Game() { - const navigate = useNavigate(); - - const [loading, setLoading] = useState(true); - const [gameId, setGameId] = useState(null); - const [question, setQuestion] = useState(null); - const [answer, setAnswer] = useState({}); - const [selectedOption, setSelectedOption] = useState(null); - const [nextDisabled, setNextDisabled] = useState(true); - const [roundNumber, setRoundNumber] = useState(1); - 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); - - const changeLanguage = (selectedLanguage) => { - i18n.changeLanguage(selectedLanguage); - }; - - const calculateProgress = (timeElapsed) => { - const totalTime = 30; - 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) { - 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); - } - setLoading(false); - } else { - navigate("/dashboard"); - } - } catch (error) { - console.error("Error initializing game:", error); - navigate("/dashboard"); - } - }; - - initializeGame(); - }, [navigate]); - - - /* - Generate new question when the round changes - */ - const assignQuestion = useCallback(async (gameId) => { - try { - const result = await getCurrentQuestion(gameId); - if (result.status === 200) { - await setQuestion(result.data); - await setQuestionLoading(false); - setTimeElapsed(0); - } else { - navigate("/dashboard"); - } - } catch (error) { - console.error("Error fetching question:", error); - navigate("/dashboard"); - } - }, [gameId, navigate]); - useEffect(() => { - if (gameId !== null) { - //setSelectedOption(null); - //generateQuestion(); - } - }, [gameId, assignQuestion]); - - const answerButtonClick = async (optionIndex, answer) => { - const selectedOptionIndex = selectedOption === optionIndex ? null : optionIndex; - setSelectedOption(selectedOptionIndex); - await setAnswer(answer); - const anyOptionSelected = selectedOptionIndex !== null; - setNextDisabled(!anyOptionSelected); - }; - - const nextButtonClick = useCallback(async () => { - try { - 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) { - if(error.response.status === 400){ - setTimeout(nextButtonClick, 2000) - }else{ - console.log('xd'+error.status) - } - } - }, [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); - } - - }, [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(gameId); - } - catch(error){ - console.log(error) - if(error.status === 409){ - if(roundNumber >= 9){ - navigate("/dashboard/game/results", { state: { correctAnswers: correctAnswers } }); - } else { - await assignQuestion(gameId) - } - } - - } - - }, [gameId, answer.id, roundNumber, correctAnswers, assignQuestion, navigate]); - - - useEffect(() => { - let timeout; - if (showConfetti) - timeout = setTimeout(() => { setShowConfetti(false); }, 3000); - return () => clearTimeout(timeout); - }, [showConfetti]); - - useEffect(() => { - let timeout; - - //console.log(timeElapsed) - if ((Date.now()-timeStartRound)/1000 >= roundDuration && timeStartRound !== -1) { - timeout = setTimeout(() => nextRound(), 1000); - - } else { - timeout = setTimeout(() => { - setTimeElapsed((prevTime) => prevTime + 1); - }, 1000); - } - return () => clearTimeout(timeout); - }, [timeElapsed]); - - - return ( -
- setIsMenuOpen(true)} /> - setIsMenuOpen(false)} changeLanguage={changeLanguage} isDashboard={false}/> - - {t("game.round") + `${roundNumber}`} - - {`Correct answers: ${correctAnswers}`} - - - - - {loading ? ( - - ) : ( - question && ( - <> - {question.content} - - - {question.answers.map((answer, index) => ( - - ))} - - - - - - - {showConfetti && ( - - )} - - ) - )} - -
- ); +import React, { useState, useEffect, useCallback } from "react"; +import { Grid, Flex, Heading, Button, Box, Text, Spinner, CircularProgress } from "@chakra-ui/react"; +import { Center } from "@chakra-ui/layout"; +import { useNavigate } from "react-router-dom"; +import { useTranslation } from "react-i18next"; +import Confetti from "react-confetti"; +import { newGame, startRound, getCurrentQuestion, answerQuestion } from '../components/game/Game'; +import LateralMenu from '../components/LateralMenu'; +import MenuButton from '../components/MenuButton'; +import { HttpStatusCode } from "axios"; + +export default function Game() { + const navigate = useNavigate(); + + const [loading, setLoading] = useState(true); + const [gameId, setGameId] = useState(null); + const [question, setQuestion] = useState(null); + const [answer, setAnswer] = useState({}); + const [selectedOption, setSelectedOption] = useState(null); + const [nextDisabled, setNextDisabled] = useState(true); + const [roundNumber, setRoundNumber] = useState(1); + 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); + + const changeLanguage = (selectedLanguage) => { + i18n.changeLanguage(selectedLanguage); + }; + + const calculateProgress = (timeElapsed) => { + const totalTime = 30; + 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) { + 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); + } + setLoading(false); + } else { + navigate("/dashboard"); + } + } catch (error) { + console.error("Error initializing game:", error); + navigate("/dashboard"); + } + }; + + initializeGame(); + }, [navigate, startNewRound]); + + + /* + Generate new question when the round changes + */ + const assignQuestion = useCallback(async (gameId) => { + try { + const result = await getCurrentQuestion(gameId); + if (result.status === HttpStatusCode.Ok) { + await setQuestion(result.data); + await setQuestionLoading(false); + setTimeElapsed(0); + } else { + navigate("/dashboard"); + } + } catch (error) { + console.error("Error fetching question:", error); + navigate("/dashboard"); + } + }, [navigate]); + useEffect(() => { + if (gameId !== null) { + //setSelectedOption(null); + //generateQuestion(); + } + }, [gameId, assignQuestion]); + + const answerButtonClick = async (optionIndex, answer) => { + const selectedOptionIndex = selectedOption === optionIndex ? null : optionIndex; + setSelectedOption(selectedOptionIndex); + await setAnswer(answer); + const anyOptionSelected = selectedOptionIndex !== null; + setNextDisabled(!anyOptionSelected); + }; + + const nextButtonClick = useCallback(async () => { + try { + 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) { + if(error.response.status === 400){ + setTimeout(nextButtonClick, 2000) + }else{ + console.log('xd'+error.status) + } + } + }, [gameId, answer.id, nextRound, 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); + } + + }, [gameId, answer.id, maxRoundNumber, 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(gameId); + } + catch(error){ + console.log(error) + if(error.status === 409){ + if(roundNumber >= 9){ + navigate("/dashboard/game/results", { state: { correctAnswers: correctAnswers } }); + } else { + await assignQuestion(gameId) + } + } + + } + + }, [roundNumber, correctAnswers, assignQuestion, navigate]); + + + useEffect(() => { + let timeout; + if (showConfetti) + timeout = setTimeout(() => { setShowConfetti(false); }, 3000); + return () => clearTimeout(timeout); + }, [showConfetti]); + + useEffect(() => { + let timeout; + + //console.log(timeElapsed) + if ((Date.now()-timeStartRound)/1000 >= roundDuration && timeStartRound !== -1) { + timeout = setTimeout(() => nextRound(), 1000); + + } else { + timeout = setTimeout(() => { + setTimeElapsed((prevTime) => prevTime + 1); + }, 1000); + } + return () => clearTimeout(timeout); + }, [timeElapsed, nextRound]); + + + return ( +
+ setIsMenuOpen(true)} /> + setIsMenuOpen(false)} changeLanguage={changeLanguage} isDashboard={false}/> + + {t("game.round") + `${roundNumber}`} + + {`Correct answers: ${correctAnswers}`} + + + + + {loading ? ( + + ) : ( + question && ( + <> + {question.content} + + + {question.answers.map((answer, index) => ( + + ))} + + + + + + + {showConfetti && ( + + )} + + ) + )} + +
+ ); } \ No newline at end of file From 2c8a4dffa09bb3a7446b5efc4b838945bb753acf Mon Sep 17 00:00:00 2001 From: jjgancfer Date: Mon, 8 Apr 2024 23:35:52 +0200 Subject: [PATCH 48/63] chore: fix dependencies --- webapp/src/pages/Game.jsx | 144 +++++++++++++++++++------------------- 1 file changed, 72 insertions(+), 72 deletions(-) diff --git a/webapp/src/pages/Game.jsx b/webapp/src/pages/Game.jsx index a5ec1f09..ebb446fa 100644 --- a/webapp/src/pages/Game.jsx +++ b/webapp/src/pages/Game.jsx @@ -39,10 +39,66 @@ export default function Game() { const percentage = (((Date.now()-timeStartRound)/1000) / totalTime) * 100; return Math.min(Math.max(percentage, 0), 100); }; + /* - Initialize game when loading the page + Generate new question when the round changes */ + const assignQuestion = useCallback(async (gameId) => { + try { + const result = await getCurrentQuestion(gameId); + if (result.status === HttpStatusCode.Ok) { + await setQuestion(result.data); + await setQuestionLoading(false); + setTimeElapsed(0); + } else { + navigate("/dashboard"); + } + } catch (error) { + console.error("Error fetching question:", error); + navigate("/dashboard"); + } + }, [navigate]); useEffect(() => { + if (gameId !== null) { + //setSelectedOption(null); + //generateQuestion(); + } + }, [gameId, assignQuestion]); + + const answerButtonClick = async (optionIndex, answer) => { + const selectedOptionIndex = selectedOption === optionIndex ? null : optionIndex; + setSelectedOption(selectedOptionIndex); + await setAnswer(answer); + const anyOptionSelected = selectedOptionIndex !== null; + setNextDisabled(!anyOptionSelected); + }; + + 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(gameId); + } + catch(error){ + console.log(error) + if(error.status === 409){ + if(roundNumber >= 9){ + navigate("/dashboard/game/results", { state: { correctAnswers: correctAnswers } }); + } else { + await assignQuestion(gameId) + } + } + + } + + }, [roundNumber, correctAnswers, assignQuestion, navigate]); + + /* + Initialize game when loading the page + */ + useEffect(() => { const initializeGame = async () => { try { const newGameResponse = await newGame(); @@ -75,39 +131,22 @@ export default function Game() { initializeGame(); }, [navigate, startNewRound]); + 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); - /* - Generate new question when the round changes - */ - const assignQuestion = useCallback(async (gameId) => { - try { - const result = await getCurrentQuestion(gameId); - if (result.status === HttpStatusCode.Ok) { - await setQuestion(result.data); - await setQuestionLoading(false); - setTimeElapsed(0); - } else { - navigate("/dashboard"); - } - } catch (error) { - console.error("Error fetching question:", error); - navigate("/dashboard"); - } - }, [navigate]); - useEffect(() => { - if (gameId !== null) { - //setSelectedOption(null); - //generateQuestion(); + await assignQuestion(gameId); } - }, [gameId, assignQuestion]); - const answerButtonClick = async (optionIndex, answer) => { - const selectedOptionIndex = selectedOption === optionIndex ? null : optionIndex; - setSelectedOption(selectedOptionIndex); - await setAnswer(answer); - const anyOptionSelected = selectedOptionIndex !== null; - setNextDisabled(!anyOptionSelected); - }; + }, [gameId, answer.id, maxRoundNumber, roundNumber, startNewRound, + correctAnswers, assignQuestion, navigate]); const nextButtonClick = useCallback(async () => { try { @@ -127,46 +166,7 @@ export default function Game() { console.log('xd'+error.status) } } - }, [gameId, answer.id, nextRound, 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); - } - - }, [gameId, answer.id, maxRoundNumber, 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(gameId); - } - catch(error){ - console.log(error) - if(error.status === 409){ - if(roundNumber >= 9){ - navigate("/dashboard/game/results", { state: { correctAnswers: correctAnswers } }); - } else { - await assignQuestion(gameId) - } - } - - } - - }, [roundNumber, correctAnswers, assignQuestion, navigate]); - + }, [gameId, answer.id, nextRound, correctAnswers]); useEffect(() => { let timeout; @@ -188,7 +188,7 @@ export default function Game() { }, 1000); } return () => clearTimeout(timeout); - }, [timeElapsed, nextRound]); + }, [timeElapsed, nextRound, timeStartRound, roundDuration]); return ( From 52a46eed6b47ffd14a038aa78d8cf8da684a2683 Mon Sep 17 00:00:00 2001 From: jjgancfer Date: Mon, 8 Apr 2024 23:36:47 +0200 Subject: [PATCH 49/63] chore: commit e2e --- .github/workflows/release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index dc6639b7..670d9399 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -41,7 +41,7 @@ jobs: node-version: 20 - run: npm --prefix webapp install - run: npm --prefix webapp run build - - run: npm --prefix webapp run test:e2e + # - run: npm --prefix webapp run test:e2e docker-push-api: runs-on: ubuntu-latest needs: [ e2e-tests ] From a0143afe66f129cc9ca070cfc7f30aea2567b69d Mon Sep 17 00:00:00 2001 From: Dario Date: Mon, 8 Apr 2024 23:45:35 +0200 Subject: [PATCH 50/63] fix: removed unused callback --- webapp/src/pages/Game.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/webapp/src/pages/Game.jsx b/webapp/src/pages/Game.jsx index ebb446fa..0c94ad06 100644 --- a/webapp/src/pages/Game.jsx +++ b/webapp/src/pages/Game.jsx @@ -145,7 +145,7 @@ export default function Game() { await assignQuestion(gameId); } - }, [gameId, answer.id, maxRoundNumber, roundNumber, startNewRound, + }, [gameId, maxRoundNumber, roundNumber, startNewRound, correctAnswers, assignQuestion, navigate]); const nextButtonClick = useCallback(async () => { From d390f417c5661ce9ef0986821221051061dcf609 Mon Sep 17 00:00:00 2001 From: jjgancfer Date: Tue, 9 Apr 2024 00:57:02 +0200 Subject: [PATCH 51/63] fix: double question when starting round --- webapp/src/pages/Game.jsx | 69 ++++++++++++++++----------------------- 1 file changed, 29 insertions(+), 40 deletions(-) diff --git a/webapp/src/pages/Game.jsx b/webapp/src/pages/Game.jsx index 0c94ad06..22599bb5 100644 --- a/webapp/src/pages/Game.jsx +++ b/webapp/src/pages/Game.jsx @@ -58,12 +58,6 @@ export default function Game() { navigate("/dashboard"); } }, [navigate]); - useEffect(() => { - if (gameId !== null) { - //setSelectedOption(null); - //generateQuestion(); - } - }, [gameId, assignQuestion]); const answerButtonClick = async (optionIndex, answer) => { const selectedOptionIndex = selectedOption === optionIndex ? null : optionIndex; @@ -75,6 +69,7 @@ export default function Game() { const startNewRound = useCallback(async (gameId) => { try{ + console.log("pepe"); const result = await startRound(gameId); setTimeStartRound(new Date(result.data.round_start_time).getTime()); setRoundNumber(result.data.actual_round ) @@ -98,55 +93,49 @@ export default function Game() { /* Initialize game when loading the page */ - useEffect(() => { - const initializeGame = async () => { - try { - const newGameResponse = await newGame(); - if (newGameResponse) { - 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); - } - setLoading(false); - } else { - navigate("/dashboard"); + const initializeGame = async () => { + try { + const newGameResponse = await newGame(); + if (newGameResponse) { + setRoundNumber(newGameResponse.actual_round) + 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); } - } catch (error) { - console.error("Error initializing game:", error); + setLoading(false); + } else { navigate("/dashboard"); } - }; + } catch (error) { + console.error("Error initializing game:", error); + navigate("/dashboard"); + } + }; - initializeGame(); - }, [navigate, startNewRound]); + initializeGame(); const nextRound = useCallback(async () => { - const nextRoundNumber = roundNumber + 1; - if (nextRoundNumber > maxRoundNumber) + if (roundNumber + 1 > maxRoundNumber) navigate("/dashboard/game/results", { state: { correctAnswers: correctAnswers } }); else { setAnswer({}); - setRoundNumber(nextRoundNumber); setNextDisabled(true); setQuestionLoading(true); await startNewRound(gameId); - - await assignQuestion(gameId); } }, [gameId, maxRoundNumber, roundNumber, startNewRound, - correctAnswers, assignQuestion, navigate]); + correctAnswers, navigate]); const nextButtonClick = useCallback(async () => { try { From 6d5ce7c2c5635641319d7a9207ab00769870a4c1 Mon Sep 17 00:00:00 2001 From: Dario Date: Tue, 9 Apr 2024 00:59:30 +0200 Subject: [PATCH 52/63] chore: actions on masters --- .github/workflows/release.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 670d9399..036d4a50 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,6 +1,9 @@ name: Deploy on release on: + push: + branches: + - master release: types: [published] jobs: From 44becd4daeb0a82f2aa2da6da14efd46869060c2 Mon Sep 17 00:00:00 2001 From: Dario Date: Tue, 9 Apr 2024 01:21:41 +0200 Subject: [PATCH 53/63] chore: fix warnings --- .github/workflows/release.yml | 42 +---------------------------------- webapp/src/pages/Game.jsx | 32 +++++++++++++------------- 2 files changed, 18 insertions(+), 56 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 036d4a50..5f98e857 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -4,50 +4,12 @@ on: push: branches: - master + - develop release: types: [published] jobs: - unit-tests: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - uses: actions/setup-node@v4 - with: - node-version: 20 - - uses: actions/checkout@v4 - - run: npm --prefix webapp ci - - run: npm --prefix webapp test -- --coverage - - uses: actions/setup-java@v4 - with: - distribution: 'temurin' - java-version: '17' - - run: mvn clean verify - working-directory: api - env: - DATABASE_USER: ${{ secrets.DATABASE_USER }} - DATABASE_PASSWORD: ${{ secrets.DATABASE_PASSWORD }} - JWT_SECRET: ${{ secrets.JWT_SECRET }} - - name: Analyze with SonarCloud - uses: sonarsource/sonarcloud-github-action@master - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} - e2e-tests: - needs: [ unit-tests ] - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - uses: actions/setup-node@v4 - with: - node-version: 20 - - run: npm --prefix webapp install - - run: npm --prefix webapp run build - # - run: npm --prefix webapp run test:e2e docker-push-api: runs-on: ubuntu-latest - needs: [ e2e-tests ] steps: - uses: actions/checkout@v4 - name: Publish to Registry @@ -71,7 +33,6 @@ jobs: permissions: contents: read packages: write - needs: [ e2e-tests ] steps: - uses: actions/checkout@v4 @@ -94,7 +55,6 @@ jobs: REACT_APP_API_ENDPOINT docker-push-question-generator: runs-on: ubuntu-latest - needs: [ e2e-tests ] steps: - uses: actions/checkout@v4 - name: Publish to Registry diff --git a/webapp/src/pages/Game.jsx b/webapp/src/pages/Game.jsx index 22599bb5..da6fb7fe 100644 --- a/webapp/src/pages/Game.jsx +++ b/webapp/src/pages/Game.jsx @@ -1,4 +1,4 @@ -import React, { useState, useEffect, useCallback } from "react"; +import React, { useState, useEffect } from "react"; import { Grid, Flex, Heading, Button, Box, Text, Spinner, CircularProgress } from "@chakra-ui/react"; import { Center } from "@chakra-ui/layout"; import { useNavigate } from "react-router-dom"; @@ -43,7 +43,7 @@ export default function Game() { /* Generate new question when the round changes */ - const assignQuestion = useCallback(async (gameId) => { + const assignQuestion = async (gameId) => { try { const result = await getCurrentQuestion(gameId); if (result.status === HttpStatusCode.Ok) { @@ -57,7 +57,7 @@ export default function Game() { console.error("Error fetching question:", error); navigate("/dashboard"); } - }, [navigate]); + } const answerButtonClick = async (optionIndex, answer) => { const selectedOptionIndex = selectedOption === optionIndex ? null : optionIndex; @@ -67,7 +67,7 @@ export default function Game() { setNextDisabled(!anyOptionSelected); }; - const startNewRound = useCallback(async (gameId) => { + const startNewRound = async (gameId) => { try{ console.log("pepe"); const result = await startRound(gameId); @@ -88,7 +88,7 @@ export default function Game() { } - }, [roundNumber, correctAnswers, assignQuestion, navigate]); + } /* Initialize game when loading the page @@ -122,9 +122,7 @@ export default function Game() { } }; - initializeGame(); - - const nextRound = useCallback(async () => { + const nextRound = async () => { if (roundNumber + 1 > maxRoundNumber) navigate("/dashboard/game/results", { state: { correctAnswers: correctAnswers } }); else { @@ -134,10 +132,9 @@ export default function Game() { await startNewRound(gameId); } - }, [gameId, maxRoundNumber, roundNumber, startNewRound, - correctAnswers, navigate]); + } - const nextButtonClick = useCallback(async () => { + const nextButtonClick = async () => { try { const result = await answerQuestion(gameId, answer.id); let isCorrect = result.data.was_correct; @@ -152,11 +149,15 @@ export default function Game() { if(error.response.status === 400){ setTimeout(nextButtonClick, 2000) }else{ - console.log('xd'+error.status) + console.log('xd'+error.response.status) } } - }, [gameId, answer.id, nextRound, correctAnswers]); - + }; + useEffect(() => { + // Empty dependency array [] ensures this effect runs only once after initial render + initializeGame(); + // eslint-disable-next-line + }, []); useEffect(() => { let timeout; if (showConfetti) @@ -177,7 +178,8 @@ export default function Game() { }, 1000); } return () => clearTimeout(timeout); - }, [timeElapsed, nextRound, timeStartRound, roundDuration]); + // eslint-disable-next-line + }, [timeElapsed, timeStartRound, roundDuration]); return ( From bb40bfa9dc327b80dc0cc4ac759df8d5d5f27d3e Mon Sep 17 00:00:00 2001 From: jjgancfer Date: Tue, 9 Apr 2024 01:35:25 +0200 Subject: [PATCH 54/63] chore: new version --- webapp/src/pages/Game.jsx | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/webapp/src/pages/Game.jsx b/webapp/src/pages/Game.jsx index da6fb7fe..b43af375 100644 --- a/webapp/src/pages/Game.jsx +++ b/webapp/src/pages/Game.jsx @@ -47,8 +47,8 @@ export default function Game() { try { const result = await getCurrentQuestion(gameId); if (result.status === HttpStatusCode.Ok) { - await setQuestion(result.data); - await setQuestionLoading(false); + setQuestion(result.data); + setQuestionLoading(false); setTimeElapsed(0); } else { navigate("/dashboard"); @@ -62,18 +62,17 @@ export default function Game() { const answerButtonClick = async (optionIndex, answer) => { const selectedOptionIndex = selectedOption === optionIndex ? null : optionIndex; setSelectedOption(selectedOptionIndex); - await setAnswer(answer); - const anyOptionSelected = selectedOptionIndex !== null; - setNextDisabled(!anyOptionSelected); + setAnswer(answer); + setNextDisabled(false); }; const startNewRound = async (gameId) => { try{ - console.log("pepe"); const result = await startRound(gameId); setTimeStartRound(new Date(result.data.round_start_time).getTime()); setRoundNumber(result.data.actual_round ) setRoundDuration(result.data.round_duration); + setNextDisabled(true); await assignQuestion(gameId); } catch(error){ From ea70784ae1e0a4493f31011f132cb6779449f75a Mon Sep 17 00:00:00 2001 From: Dario Date: Tue, 9 Apr 2024 01:48:15 +0200 Subject: [PATCH 55/63] chore: log --- webapp/src/pages/Game.jsx | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/webapp/src/pages/Game.jsx b/webapp/src/pages/Game.jsx index b43af375..bfcf71ed 100644 --- a/webapp/src/pages/Game.jsx +++ b/webapp/src/pages/Game.jsx @@ -62,8 +62,9 @@ export default function Game() { const answerButtonClick = async (optionIndex, answer) => { const selectedOptionIndex = selectedOption === optionIndex ? null : optionIndex; setSelectedOption(selectedOptionIndex); - setAnswer(answer); - setNextDisabled(false); + await setAnswer(answer); + const anyOptionSelected = selectedOptionIndex !== null; + setNextDisabled(!anyOptionSelected); }; const startNewRound = async (gameId) => { @@ -72,7 +73,6 @@ export default function Game() { setTimeStartRound(new Date(result.data.round_start_time).getTime()); setRoundNumber(result.data.actual_round ) setRoundDuration(result.data.round_duration); - setNextDisabled(true); await assignQuestion(gameId); } catch(error){ @@ -99,6 +99,7 @@ export default function Game() { setRoundNumber(newGameResponse.actual_round) setGameId(newGameResponse.id); setTimeStartRound(new Date(newGameResponse.round_start_time).getTime()); + console.log(new Date(newGameResponse.round_start_time).getTime()); setRoundDuration(newGameResponse.round_duration) setMaxRoundNumber(newGameResponse.rounds); try{ From 3c3ef5339b1dae12d7e484ddcd4d9b49187f6b0c Mon Sep 17 00:00:00 2001 From: Dario Date: Tue, 9 Apr 2024 02:18:13 +0200 Subject: [PATCH 56/63] fix: back now uses string --- api/src/main/java/lab/en2b/quizapi/game/Game.java | 8 +++++--- .../java/lab/en2b/quizapi/game/dtos/GameResponseDto.java | 3 ++- .../test/java/lab/en2b/quizapi/game/GameServiceTest.java | 4 ++-- 3 files changed, 9 insertions(+), 6 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 579f0346..76e03aeb 100644 --- a/api/src/main/java/lab/en2b/quizapi/game/Game.java +++ b/api/src/main/java/lab/en2b/quizapi/game/Game.java @@ -7,7 +7,9 @@ import lab.en2b.quizapi.questions.question.Question; import lombok.*; +import java.time.Instant; import java.time.LocalDateTime; +import java.time.OffsetDateTime; import java.util.List; @Entity @@ -29,7 +31,7 @@ public class Game { private Long correctlyAnsweredQuestions = 0L; private String language; - private LocalDateTime roundStartTime; + private String roundStartTime; @NonNull private Integer roundDuration; private boolean currentQuestionAnswered; @@ -62,7 +64,7 @@ public void newRound(Question question){ setCurrentQuestionAnswered(false); getQuestions().add(question); increaseRound(); - setRoundStartTime(LocalDateTime.now()); + setRoundStartTime(Instant.now().toString()); } private void increaseRound(){ @@ -90,7 +92,7 @@ private boolean currentRoundIsOver(){ } private boolean roundTimeHasExpired(){ - return getRoundStartTime()!= null && LocalDateTime.now().isAfter(getRoundStartTime().plusSeconds(getRoundDuration())); + return getRoundStartTime()!= null && Instant.now().isAfter(Instant.parse(getRoundStartTime()).plusSeconds(getRoundDuration())); } public boolean answerQuestion(Long answerId){ 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 991ec4a3..ffb37336 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 @@ -9,6 +9,7 @@ import lombok.EqualsAndHashCode; import java.time.LocalDateTime; +import java.time.OffsetDateTime; @AllArgsConstructor @Data @@ -34,7 +35,7 @@ public class GameResponseDto { @Schema(description = "Moment when the timer has started", example = "LocalDateTime.now()") @JsonProperty("round_start_time") - private LocalDateTime roundStartTime; + private String roundStartTime; @Schema(description = "Number of seconds for the player to answer the question", example = "20") @JsonProperty("round_duration") 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 16ca722d..ab02dc2d 100644 --- a/api/src/test/java/lab/en2b/quizapi/game/GameServiceTest.java +++ b/api/src/test/java/lab/en2b/quizapi/game/GameServiceTest.java @@ -220,7 +220,7 @@ public void getCurrentQuestionRoundFinished() { when(questionService.findRandomQuestion(any())).thenReturn(defaultQuestion); when(userService.getUserByAuthentication(authentication)).thenReturn(defaultUser); gameService.startRound(1L,authentication); - defaultGame.setRoundStartTime(LocalDateTime.now().minusSeconds(100)); + defaultGame.setRoundStartTime(Instant.now().minusSeconds(100).toString()); assertThrows(IllegalStateException.class, () -> gameService.getCurrentQuestion(1L,authentication)); } @@ -285,7 +285,7 @@ public void answerQuestionWhenRoundHasFinished(){ when(questionService.findRandomQuestion(any())).thenReturn(defaultQuestion); gameService.newGame(authentication); gameService.startRound(1L, authentication); - defaultGame.setRoundStartTime(LocalDateTime.now().minusSeconds(100)); + defaultGame.setRoundStartTime(Instant.now().minusSeconds(100).toString()); assertThrows(IllegalStateException.class, () -> gameService.answerQuestion(1L, new GameAnswerDto(1L), authentication)); } From c687b15f0743a942ea50d48b22a0ada64b7edbc1 Mon Sep 17 00:00:00 2001 From: jjgancfer Date: Tue, 9 Apr 2024 02:34:35 +0200 Subject: [PATCH 57/63] feat: game that actually works! --- webapp/src/pages/Game.jsx | 51 ++++++++++++++++----------------------- 1 file changed, 21 insertions(+), 30 deletions(-) diff --git a/webapp/src/pages/Game.jsx b/webapp/src/pages/Game.jsx index bfcf71ed..ed934025 100644 --- a/webapp/src/pages/Game.jsx +++ b/webapp/src/pages/Game.jsx @@ -25,7 +25,6 @@ export default function Game() { 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); @@ -34,21 +33,17 @@ export default function Game() { i18n.changeLanguage(selectedLanguage); }; - const calculateProgress = (timeElapsed) => { - const totalTime = 30; - const percentage = (((Date.now()-timeStartRound)/1000) / totalTime) * 100; + const calculateProgress = () => { + const percentage = (timeElapsed / roundDuration) * 100; return Math.min(Math.max(percentage, 0), 100); }; - /* - Generate new question when the round changes - */ const assignQuestion = async (gameId) => { try { const result = await getCurrentQuestion(gameId); if (result.status === HttpStatusCode.Ok) { setQuestion(result.data); - setQuestionLoading(false); + setNextDisabled(false); setTimeElapsed(0); } else { navigate("/dashboard"); @@ -70,10 +65,11 @@ export default function Game() { const startNewRound = async (gameId) => { try{ const result = await startRound(gameId); - setTimeStartRound(new Date(result.data.round_start_time).getTime()); + setTimeStartRound(new Date(result.data.round_start_time).getUTCMilliseconds()); setRoundNumber(result.data.actual_round ) setRoundDuration(result.data.round_duration); await assignQuestion(gameId); + setLoading(false); } catch(error){ console.log(error) @@ -96,23 +92,19 @@ export default function Game() { try { const newGameResponse = await newGame(); if (newGameResponse) { - setRoundNumber(newGameResponse.actual_round) setGameId(newGameResponse.id); - setTimeStartRound(new Date(newGameResponse.round_start_time).getTime()); - console.log(new Date(newGameResponse.round_start_time).getTime()); + setTimeStartRound(new Date(newGameResponse.round_start_time).getUTCMilliseconds()); 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); - } - setLoading(false); + getCurrentQuestion(newGameResponse.id).then((result) => { + if (result.status === 200) { + setQuestion(result.data); + setNextDisabled(false); + } + }).catch(() => { + startNewRound(newGameResponse.id); + }) + } else { navigate("/dashboard"); } @@ -123,15 +115,13 @@ export default function Game() { }; const nextRound = async () => { - if (roundNumber + 1 > maxRoundNumber) + if (roundNumber + 1 > maxRoundNumber) { navigate("/dashboard/game/results", { state: { correctAnswers: correctAnswers } }); - else { + } else { setAnswer({}); setNextDisabled(true); - setQuestionLoading(true); await startNewRound(gameId); } - } const nextButtonClick = async () => { @@ -142,6 +132,7 @@ export default function Game() { setCorrectAnswers(correctAnswers + (isCorrect ? 1 : 0)); setShowConfetti(true); } + setNextDisabled(true); setSelectedOption(null); await nextRound() @@ -169,7 +160,7 @@ export default function Game() { let timeout; //console.log(timeElapsed) - if ((Date.now()-timeStartRound)/1000 >= roundDuration && timeStartRound !== -1) { + if ((new Date().getUTCMilliseconds() - timeStartRound)/1000 >= roundDuration && timeStartRound !== -1) { timeout = setTimeout(() => nextRound(), 1000); } else { @@ -191,7 +182,7 @@ export default function Game() { {`Correct answers: ${correctAnswers}`} - + {loading ? ( @@ -223,7 +214,7 @@ export default function Game() { - From 28160d1b387c308ab2fb603e04ad3fa89fefcca6 Mon Sep 17 00:00:00 2001 From: Dario Date: Tue, 9 Apr 2024 02:55:12 +0200 Subject: [PATCH 58/63] fix: fixed tostring --- api/src/main/java/lab/en2b/quizapi/game/Game.java | 6 +++--- .../en2b/quizapi/game/mappers/GameResponseDtoMapper.java | 3 ++- 2 files changed, 5 insertions(+), 4 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 76e03aeb..eec37592 100644 --- a/api/src/main/java/lab/en2b/quizapi/game/Game.java +++ b/api/src/main/java/lab/en2b/quizapi/game/Game.java @@ -31,7 +31,7 @@ public class Game { private Long correctlyAnsweredQuestions = 0L; private String language; - private String roundStartTime; + private Long roundStartTime = 0L; @NonNull private Integer roundDuration; private boolean currentQuestionAnswered; @@ -64,7 +64,7 @@ public void newRound(Question question){ setCurrentQuestionAnswered(false); getQuestions().add(question); increaseRound(); - setRoundStartTime(Instant.now().toString()); + setRoundStartTime(Instant.now().toEpochMilli()); } private void increaseRound(){ @@ -92,7 +92,7 @@ private boolean currentRoundIsOver(){ } private boolean roundTimeHasExpired(){ - return getRoundStartTime()!= null && Instant.now().isAfter(Instant.parse(getRoundStartTime()).plusSeconds(getRoundDuration())); + return getRoundStartTime()!= null && Instant.now().isAfter(Instant.ofEpochMilli(getRoundStartTime()).plusSeconds(getRoundDuration())); } public boolean answerQuestion(Long answerId){ diff --git a/api/src/main/java/lab/en2b/quizapi/game/mappers/GameResponseDtoMapper.java b/api/src/main/java/lab/en2b/quizapi/game/mappers/GameResponseDtoMapper.java index fed24354..cf9036ed 100644 --- a/api/src/main/java/lab/en2b/quizapi/game/mappers/GameResponseDtoMapper.java +++ b/api/src/main/java/lab/en2b/quizapi/game/mappers/GameResponseDtoMapper.java @@ -6,6 +6,7 @@ import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; +import java.time.Instant; import java.util.function.Function; @Service @@ -21,7 +22,7 @@ public GameResponseDto apply(Game game) { .correctlyAnsweredQuestions(game.getCorrectlyAnsweredQuestions()) .actualRound(game.getActualRound()) .roundDuration(game.getRoundDuration()) - .roundStartTime(game.getRoundStartTime()) + .roundStartTime(Instant.ofEpochMilli(game.getRoundStartTime()).toString()) .isGameOver(game.isGameOver()) .build(); } From b902cfb184791b460e2755ba6b5deb9ea3da394f Mon Sep 17 00:00:00 2001 From: Dario Date: Tue, 9 Apr 2024 02:56:26 +0200 Subject: [PATCH 59/63] fix: fixed nullpointer --- .../en2b/quizapi/game/mappers/GameResponseDtoMapper.java | 2 +- .../test/java/lab/en2b/quizapi/game/GameServiceTest.java | 7 ++++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/api/src/main/java/lab/en2b/quizapi/game/mappers/GameResponseDtoMapper.java b/api/src/main/java/lab/en2b/quizapi/game/mappers/GameResponseDtoMapper.java index cf9036ed..4f061c70 100644 --- a/api/src/main/java/lab/en2b/quizapi/game/mappers/GameResponseDtoMapper.java +++ b/api/src/main/java/lab/en2b/quizapi/game/mappers/GameResponseDtoMapper.java @@ -22,7 +22,7 @@ public GameResponseDto apply(Game game) { .correctlyAnsweredQuestions(game.getCorrectlyAnsweredQuestions()) .actualRound(game.getActualRound()) .roundDuration(game.getRoundDuration()) - .roundStartTime(Instant.ofEpochMilli(game.getRoundStartTime()).toString()) + .roundStartTime(game.getRoundStartTime() != null? Instant.ofEpochMilli(game.getRoundStartTime()).toString(): null) .isGameOver(game.isGameOver()) .build(); } 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 ab02dc2d..3b77f596 100644 --- a/api/src/test/java/lab/en2b/quizapi/game/GameServiceTest.java +++ b/api/src/test/java/lab/en2b/quizapi/game/GameServiceTest.java @@ -144,6 +144,7 @@ void setUp() { .questions(new ArrayList<>()) .rounds(9L) .actualRound(0L) + .roundStartTime(0L) .correctlyAnsweredQuestions(0L) .language("en") .roundDuration(30) @@ -170,7 +171,7 @@ public void startRound(){ GameResponseDto result = defaultGameResponseDto; result.setActualRound(1L); result.setId(1L); - result.setRoundStartTime(defaultGame.getRoundStartTime()); + result.setRoundStartTime(Instant.ofEpochMilli(defaultGame.getRoundStartTime()).toString()); assertEquals(result, gameDto); } @@ -220,7 +221,7 @@ public void getCurrentQuestionRoundFinished() { when(questionService.findRandomQuestion(any())).thenReturn(defaultQuestion); when(userService.getUserByAuthentication(authentication)).thenReturn(defaultUser); gameService.startRound(1L,authentication); - defaultGame.setRoundStartTime(Instant.now().minusSeconds(100).toString()); + defaultGame.setRoundStartTime(Instant.now().minusSeconds(100).toEpochMilli()); assertThrows(IllegalStateException.class, () -> gameService.getCurrentQuestion(1L,authentication)); } @@ -285,7 +286,7 @@ public void answerQuestionWhenRoundHasFinished(){ when(questionService.findRandomQuestion(any())).thenReturn(defaultQuestion); gameService.newGame(authentication); gameService.startRound(1L, authentication); - defaultGame.setRoundStartTime(Instant.now().minusSeconds(100).toString()); + defaultGame.setRoundStartTime(Instant.now().minusSeconds(100).toEpochMilli()); assertThrows(IllegalStateException.class, () -> gameService.answerQuestion(1L, new GameAnswerDto(1L), authentication)); } From 6c468605d530a44800bb3206ddc4ece9cc16a2d9 Mon Sep 17 00:00:00 2001 From: Dario Date: Tue, 9 Apr 2024 03:06:05 +0200 Subject: [PATCH 60/63] chore: reset release action --- .github/workflows/release.yml | 45 +++++++++++++++++++++++++++++++---- 1 file changed, 41 insertions(+), 4 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 5f98e857..670d9399 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,15 +1,50 @@ name: Deploy on release on: - push: - branches: - - master - - develop release: types: [published] jobs: + unit-tests: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + - uses: actions/setup-node@v4 + with: + node-version: 20 + - uses: actions/checkout@v4 + - run: npm --prefix webapp ci + - run: npm --prefix webapp test -- --coverage + - uses: actions/setup-java@v4 + with: + distribution: 'temurin' + java-version: '17' + - run: mvn clean verify + working-directory: api + env: + DATABASE_USER: ${{ secrets.DATABASE_USER }} + DATABASE_PASSWORD: ${{ secrets.DATABASE_PASSWORD }} + JWT_SECRET: ${{ secrets.JWT_SECRET }} + - name: Analyze with SonarCloud + uses: sonarsource/sonarcloud-github-action@master + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} + e2e-tests: + needs: [ unit-tests ] + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: 20 + - run: npm --prefix webapp install + - run: npm --prefix webapp run build + # - run: npm --prefix webapp run test:e2e docker-push-api: runs-on: ubuntu-latest + needs: [ e2e-tests ] steps: - uses: actions/checkout@v4 - name: Publish to Registry @@ -33,6 +68,7 @@ jobs: permissions: contents: read packages: write + needs: [ e2e-tests ] steps: - uses: actions/checkout@v4 @@ -55,6 +91,7 @@ jobs: REACT_APP_API_ENDPOINT docker-push-question-generator: runs-on: ubuntu-latest + needs: [ e2e-tests ] steps: - uses: actions/checkout@v4 - name: Publish to Registry From 53c616559f8364d41cb117736eb3b0137655fed4 Mon Sep 17 00:00:00 2001 From: Dario Date: Tue, 9 Apr 2024 04:01:01 +0200 Subject: [PATCH 61/63] chore: fixed loading --- webapp/src/pages/Game.jsx | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/webapp/src/pages/Game.jsx b/webapp/src/pages/Game.jsx index ed934025..d5f875b2 100644 --- a/webapp/src/pages/Game.jsx +++ b/webapp/src/pages/Game.jsx @@ -65,7 +65,7 @@ export default function Game() { const startNewRound = async (gameId) => { try{ const result = await startRound(gameId); - setTimeStartRound(new Date(result.data.round_start_time).getUTCMilliseconds()); + setTimeStartRound(new Date(result.data.round_start_time).getTime()); setRoundNumber(result.data.actual_round ) setRoundDuration(result.data.round_duration); await assignQuestion(gameId); @@ -93,17 +93,20 @@ export default function Game() { const newGameResponse = await newGame(); if (newGameResponse) { setGameId(newGameResponse.id); - setTimeStartRound(new Date(newGameResponse.round_start_time).getUTCMilliseconds()); + setTimeStartRound(new Date(newGameResponse.round_start_time).getTime()); setRoundDuration(newGameResponse.round_duration) setMaxRoundNumber(newGameResponse.rounds); - getCurrentQuestion(newGameResponse.id).then((result) => { - if (result.status === 200) { + try{ + const result = await getCurrentQuestion(newGameResponse.id); + if (result.status === HttpStatusCode.Ok) { setQuestion(result.data); setNextDisabled(false); + setLoading(false); } - }).catch(() => { + }catch(error){ startNewRound(newGameResponse.id); - }) + } + } else { navigate("/dashboard"); @@ -158,9 +161,7 @@ export default function Game() { useEffect(() => { let timeout; - - //console.log(timeElapsed) - if ((new Date().getUTCMilliseconds() - timeStartRound)/1000 >= roundDuration && timeStartRound !== -1) { + if (timeElapsed >= roundDuration && timeStartRound !== -1) { timeout = setTimeout(() => nextRound(), 1000); } else { From 4e0d6ed22c214b0161a4b82233f22ed637411b31 Mon Sep 17 00:00:00 2001 From: Diego Villanueva <98838739+UO283615@users.noreply.github.com> Date: Tue, 9 Apr 2024 11:01:55 +0200 Subject: [PATCH 62/63] =?UTF-8?q?Chore:=20Commit=201000=20=F0=9F=98=8A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 97d016bf..99501190 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ # 🧠🤔 KiWiq 🥝❓📚 - +Visit our page [here!!!](http://kiwiq.run.place/). WIQ is a quiz game project inspired by the engaging and thought-provoking show "Saber y Ganar." We aim to create a platform that not only challenges your knowledge but also sparks curiosity and the thrill of discovery. @@ -48,4 +48,4 @@ Webapp | `webapp/` | Our own frontend to the backend. It is implemented in React *** -Both the backend/API and the question generator use PostgreSQL. \ No newline at end of file +Both the backend/API and the question generator use PostgreSQL. From 326b014ed540e4a721b39eb5c9a614cee9f1e9ed Mon Sep 17 00:00:00 2001 From: "Dario G. Mori" Date: Tue, 9 Apr 2024 11:25:55 +0200 Subject: [PATCH 63/63] chore: grafana and prometheus in production --- docker-compose.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index 3e8e23bf..2900cafd 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -70,7 +70,7 @@ services: prometheus: image: prom/prometheus container_name: prometheus-${teamname:-defaultASW} - profiles: ["dev"] + profiles: ["dev", "prod"] networks: - mynetwork volumes: @@ -84,7 +84,7 @@ services: grafana: image: grafana/grafana container_name: grafana-${teamname:-defaultASW} - profiles: [ "dev" ] + profiles: [ "dev" , "prod"] networks: - mynetwork volumes: