diff --git a/gatewayservice/gateway-service.js b/gatewayservice/gateway-service.js
index 009db707..317c55ae 100644
--- a/gatewayservice/gateway-service.js
+++ b/gatewayservice/gateway-service.js
@@ -58,9 +58,39 @@ app.get('/questions', async (req, res) => {
}
});
+app.get('/questions/:lang/:amount/:type', async (req, res) => {
+ try {
+ const lang = req.params.lang.toString();
+ const amount = req.params.amount.toString();
+ const type = req.params.type.toString();
+ // Forward the question request to the quetion service
+ const questionResponse = await axios.get(questionServiceUrl+'/questions/' + lang + '/' + amount + '/' + type);
+
+ res.json(questionResponse.data);
+ } catch (error) {
+
+ res.status(error.response.status).json({ error: error.response.data.error });
+ }
+});
+
+
+app.get('/questions/:lang/:amount', async (req, res) => {
+ try {
+ const lang = req.params.lang.toString();
+ const amount = req.params.amount.toString();
+ // Forward the question request to the quetion service
+ const questionResponse = await axios.get(questionServiceUrl+'/questions/' + lang + '/' + amount);
+
+ res.json(questionResponse.data);
+ } catch (error) {
+
+ res.status(error.response.status).json({ error: error.response.data.error });
+ }
+});
+
app.get('/questions/:lang', async (req, res) => {
try {
- const lang = req.params.lang;
+ const lang = req.params.lang.toString();
// Forward the question request to the quetion service
const questionResponse = await axios.get(questionServiceUrl+'/questions/' + lang);
@@ -81,6 +111,27 @@ app.post('/record', async(req, res) => {
}
});
+app.get('/record/ranking/top10', async(req, res)=>{
+ try {
+ // Forward the record request to the record service
+ const recordResponse = await axios.get(recordServiceUrl + '/record/ranking/top10');
+ res.json(recordResponse.data);
+ } catch (error) {
+ res.send(error);
+ }
+});
+
+app.get('/record/ranking/:user', async(req, res)=>{
+ try {
+ const user = req.params.user;
+ // Forward the record request to the record service
+ const recordResponse = await axios.get(recordServiceUrl + '/record/ranking/' + user);
+ res.json(recordResponse.data);
+ } catch (error) {
+ res.send(error);
+ }
+});
+
app.get('/record/:user', async(req, res)=>{
try {
const user = req.params.user;
diff --git a/gatewayservice/gateway-service.test.js b/gatewayservice/gateway-service.test.js
index bfed664f..971fead9 100644
--- a/gatewayservice/gateway-service.test.js
+++ b/gatewayservice/gateway-service.test.js
@@ -20,16 +20,28 @@ describe('Gateway Service', () => {
}
});
+ const question = { data: [{question: "¿Cuál es la población de Oviedo?",
+ answers: ["225089","272357","267855","231841"]}] };
+
+ //Dont need to check a good record just that it redirects the call
+ const record = {data : {record:'undefined'}};
+
axios.get.mockImplementation((url, data) => {
if (url.endsWith('/questions')){
- return Promise.resolve({ data: [{question: "¿Cuál es la población de Oviedo?",
- answers: ["225089","272357","267855","231841"]}] });
+ return Promise.resolve(question);
+ } else if (url.endsWith('/questions/es/1/CAPITAL')){
+ return Promise.resolve(question);
+ } else if (url.endsWith('/questions/es/1')){
+ return Promise.resolve(question);
} else if (url.endsWith('/questions/es')){
- return Promise.resolve({ data: [{question: "¿Cuál es la población de Oviedo?",
- answers: ["225089","272357","267855","231841"]}] });
+ return Promise.resolve(question);
+
} else if(url.endsWith('/record/testuser')){
- //Dont need to check a good record just that it redirects the call
- return Promise.resolve({data : {record:'undefined'}})
+ return Promise.resolve(record)
+ } else if(url.endsWith('/record/ranking/top10')){
+ return Promise.resolve(record)
+ } else if(url.endsWith('/record/ranking/testuser')){
+ return Promise.resolve(record)
}
});
@@ -59,8 +71,7 @@ describe('Gateway Service', () => {
const response = await request(app)
.get('/questions');
- expect(response.statusCode).toBe(200);
- expect(response.body[0]).toHaveProperty('question', "¿Cuál es la población de Oviedo?");
+ checkQuestion(response);
});
// Test /questions/:lang endpoint
@@ -68,8 +79,23 @@ describe('Gateway Service', () => {
const response = await request(app)
.get('/questions/es');
- expect(response.statusCode).toBe(200);
- expect(response.body[0]).toHaveProperty('question', "¿Cuál es la población de Oviedo?");
+ checkQuestion(response);
+ });
+
+ // Test /questions/:lang/:amount endpoint
+ it('should forward questions request to question service', async () => {
+ const response = await request(app)
+ .get('/questions/es/1');
+
+ checkQuestion(response);
+ });
+
+ // Test /questions/:lang/:amount/:type endpoint
+ it('should forward questions request to question service', async () => {
+ const response = await request(app)
+ .get('/questions/es/1/CAPITAL');
+
+ checkQuestion(response);
});
// Test /record endpoint
@@ -86,7 +112,32 @@ describe('Gateway Service', () => {
const response = await request(app)
.get('/record/testuser');
- expect(response.statusCode).toBe(200);
- expect(response.body).toHaveProperty('record', "undefined");
+ checkRecord(response);
+ });
+
+ // Test /record/ranking/:user endpoint
+ it('should forward record request to record service', async () => {
+ const response = await request(app)
+ .get('/record/ranking/testuser');
+
+ checkRecord(response);
});
-});
\ No newline at end of file
+
+ // Test /record/ranking/top10 endpoint
+ it('should forward record request to record service', async () => {
+ const response = await request(app)
+ .get('/record/ranking/top10');
+ checkRecord(response);
+
+ });
+});
+
+function checkRecord(response){
+ expect(response.statusCode).toBe(200);
+ expect(response.body).toHaveProperty('record', "undefined");
+}
+
+function checkQuestion(response){
+ expect(response.statusCode).toBe(200);
+ expect(response.body[0]).toHaveProperty('question', "¿Cuál es la población de Oviedo?");
+}
\ No newline at end of file
diff --git a/questionservice/question-service.js b/questionservice/question-service.js
index 026feb5c..effc5ac7 100644
--- a/questionservice/question-service.js
+++ b/questionservice/question-service.js
@@ -30,6 +30,60 @@ app.get('/questions', async (req, res) => {
}
});
+app.get('/questions/:lang/:amount/:type', async (req, res) => {
+ try {
+ const lang = req.params.lang.toString();
+ let amount = checkAmount(parseInt(req.params.amount));
+ const type = req.params.type.toString();
+
+ if(amount > 20 || amount < 1)
+ amount = 5;
+
+ const questions = await Question.aggregate([
+ {$match: {language : lang, type: type}}, //Condition
+ {$sample: {size:amount}}
+ ]);
+
+ let jsonResult = {};
+ for (let i = 0; i < questions.length; i++) {
+ const question = questions[i];
+ jsonResult[i] = {
+ question : question.question,
+ answers : question.answers
+ }
+ }
+ res.json(jsonResult);
+ } catch (error) {
+ res.status(500).json({ error: 'Internal Server Error' });
+ }
+});
+
+app.get('/questions/:lang/:amount', async (req, res) => {
+ try {
+ const lang = req.params.lang;
+ let amount = checkAmount(parseInt(req.params.amount));
+
+
+
+ const questions = await Question.aggregate([
+ {$match: {language : lang}}, //Condition
+ {$sample: {size:amount}}
+ ]);
+
+ let jsonResult = {};
+ for (let i = 0; i < questions.length; i++) {
+ const question = questions[i];
+ jsonResult[i] = {
+ question : question.question,
+ answers : question.answers
+ }
+ }
+ res.json(jsonResult);
+ } catch (error) {
+ res.status(500).json({ error: 'Internal Server Error' });
+ }
+});
+
app.get('/questions/:lang', async (req, res) => {
try {
const lang = req.params.lang;
@@ -53,6 +107,11 @@ app.get('/questions/:lang', async (req, res) => {
}
});
+function checkAmount(amount){
+ if(amount > 20 || amount < 1)
+ return 5;
+ return amount;
+}
const server = app.listen(port, () => {
console.log(`Question Service listening at http://localhost:${port}`);
diff --git a/questionservice/question-service.test.js b/questionservice/question-service.test.js
index 67b5ca66..35b70eae 100644
--- a/questionservice/question-service.test.js
+++ b/questionservice/question-service.test.js
@@ -12,7 +12,7 @@ beforeAll(async () => {
app = require('./question-service');
//Populate db
- for(let i = 0; i < 6 ; i++){
+ for(let i = 0; i < 21 ; i++){
const question = new Question( {
question: "¿Cuál es la población de Oviedo?",
answers: [
@@ -60,4 +60,52 @@ describe('Question Service', () => {
expect(response.body[0]).toHaveProperty('question', "¿Cuál es la población de Oviedo?");
expect(Object.keys(response.body).length).toBe(5);
});
+
+
+ it('Should give 20 questions /questions/es/20', async () => {
+
+ let response = await request(app).get('/questions/es/20');
+ expect(response.status).toBe(200);
+ expect(response.body[0]).toHaveProperty('question', "¿Cuál es la población de Oviedo?");
+ expect(Object.keys(response.body).length).toBe(20);
+ });
+
+ it('Should give 1 questions /questions/es/1', async () => {
+
+ let response = await request(app).get('/questions/es/20');
+ expect(response.status).toBe(200);
+ expect(response.body[0]).toHaveProperty('question', "¿Cuál es la población de Oviedo?");
+ expect(Object.keys(response.body).length).toBe(20);
+ });
+
+ it('Should give 5 questions as the max is 20 /questions/es/21', async () => {
+
+ let response = await request(app).get('/questions/es/21');
+ expect(response.status).toBe(200);
+ expect(response.body[0]).toHaveProperty('question', "¿Cuál es la población de Oviedo?");
+ expect(Object.keys(response.body).length).toBe(5);
+ });
+
+ it('Should give 5 questions as the min is 1 /questions/es/0', async () => {
+
+ let response = await request(app).get('/questions/es/0');
+ expect(response.status).toBe(200);
+ expect(response.body[0]).toHaveProperty('question', "¿Cuál es la población de Oviedo?");
+ expect(Object.keys(response.body).length).toBe(5);
+ });
+
+ it('Should give 10 questions /questions/es/10/POPULATION', async () => {
+
+ let response = await request(app).get('/questions/es/10/POPULATION');
+ expect(response.status).toBe(200);
+ expect(response.body[0]).toHaveProperty('question', "¿Cuál es la población de Oviedo?");
+ expect(Object.keys(response.body).length).toBe(10);
+ });
+
+ it('Should give 0 questions /questions/es/10/CAPITAL', async () => {
+
+ let response = await request(app).get('/questions/es/10/CAPITAL');
+ expect(response.status).toBe(200);
+ expect(Object.keys(response.body).length).toBe(0);
+ });
});
\ No newline at end of file
diff --git a/sonar-project.properties b/sonar-project.properties
index 6db6137a..fad83281 100644
--- a/sonar-project.properties
+++ b/sonar-project.properties
@@ -14,4 +14,5 @@ sonar.coverage.exclusions=**/*.test.js
sonar.sources=webapp/src/components,users/authservice,users/userservice,gatewayservice
sonar.sourceEncoding=UTF-8
sonar.exclusions=node_modules/**
-sonar.javascript.lcov.reportPaths=**/coverage/lcov.info
\ No newline at end of file
+sonar.javascript.lcov.reportPaths=**/coverage/lcov.info
+sonar.cpd.exclusions=**/*.test.js,**/*steps.js,**/*Tests.java
diff --git a/users/recordservice/record-model.js b/users/recordservice/record-model.js
index a7a30d76..a302df52 100644
--- a/users/recordservice/record-model.js
+++ b/users/recordservice/record-model.js
@@ -1,16 +1,18 @@
const mongoose = require('mongoose');
+const { Schema } = mongoose;
-const recordSchema = new mongoose.Schema({
- user: String,
+const recordSchema = new Schema({
+ user: { type: String, required: true },
games: [{
questions: [{
- question: String,
- answers: [String],
- answerGiven: String,
- correctAnswer: String
+ question: { type: String, required: true },
+ answers: { type: [String], required: true },
+ answerGiven: { type: String, required: true },
+ correctAnswer: { type: String, required: true }
}],
- points: Number,
- date: String
+ points: { type: Number, required: true },
+ date: { type: String, required: true },
+ competitive: { type: Boolean, required: true }
}]
});
const Record = mongoose.model('Record', recordSchema);
diff --git a/users/recordservice/record-service.js b/users/recordservice/record-service.js
index dd383984..cf94544a 100644
--- a/users/recordservice/record-service.js
+++ b/users/recordservice/record-service.js
@@ -1,8 +1,7 @@
const express = require('express');
const mongoose = require('mongoose');
-const bcrypt = require('bcrypt');
const bodyParser = require('body-parser');
-const Record = require('./record-model')
+const Record = require('./record-model');
const app = express();
const port = 8004;
@@ -10,6 +9,10 @@ const port = 8004;
// Middleware to parse JSON in request body
app.use(bodyParser.json());
+var ranking = [];
+var lastTime = new Date();
+const minTimeDifferenceInMiliseconds = 120_000;
+
// Connect to MongoDB
const mongoUri = process.env.MONGODB_URI || 'mongodb://localhost:27017/userdb';
mongoose.connect(mongoUri);
@@ -18,14 +21,12 @@ mongoose.connect(mongoUri);
app.post('/record', async (req, res) => {
const user = req.body.user;
const game = req.body.game;
- console.log(user)
- console.log(game)
if(user && game){
let record = await Record.findOne({ user : user });
if(record){ //If it exits
record.games.push(game);
}
- else{ //Lo creamos
+ else{ //We make it
record = new Record({
user:user,
games:[game]
@@ -45,9 +46,34 @@ app.post('/record', async (req, res) => {
});
+app.get('/record/ranking/top10', async (req, res) => {
+ try {
+ let usersRanking = await getRanking();
+ let usersCompetitiveStats = usersRanking.slice(0, 10);
+
+ res.json({usersCompetitiveStats: usersCompetitiveStats });
+ } catch (err) {
+ res.status(500).send();
+ }
+});
+
+app.get('/record/ranking/:user', async (req, res) => {
+ try {
+ const user = req.params.user.toString();
+
+ let usersRanking = await getRanking();
+ let userCompetitiveStats = usersRanking.filter(userData => userData._id === user);
+
+ res.json({userCompetitiveStats: userCompetitiveStats[0] });
+ } catch (err) {
+ console.log(err)
+ res.status(500).send();
+ }
+});
+
app.get('/record/:user', async (req, res) => {
try {
- const recordFound = await Record.findOne({ user: req.params.user }, 'games');
+ const recordFound = await Record.findOne({ user: req.params.user.toString() }, 'games');
if (!recordFound) {
res.json({record: "undefined" });
} else {
@@ -58,6 +84,37 @@ app.get('/record/:user', async (req, res) => {
}
});
+
+async function getRanking(){
+ const nowTime = new Date();
+ let timeDifferenceInMiliseconds = nowTime - lastTime;
+ if(ranking.length == 0 || timeDifferenceInMiliseconds > minTimeDifferenceInMiliseconds){
+ ranking = await Record.aggregate([
+ // Unwind the games array to work with each game separately
+ { $unwind: "$games" },
+ // Match only competitive games
+ { $match: { "games.competitive": true } },
+ // Group by user and calculate total points and total competitive games per user
+ {
+ $group: {
+ _id: "$user",
+ totalPoints: { $sum: "$games.points" },
+ totalCompetitiveGames: { $sum: 1 } // Count the number of competitive games
+ }
+ },
+ // Sort by total points in descending order (top 1 will have the highest points)
+ { $sort: { totalPoints: -1 } }
+ ]);
+
+ //The operator ... dumps the user json making it {_id , totalPoints, totalCompetitiveGames, position}
+ ranking = ranking.map((user, index) => ({ ...user, position: index + 1 }));
+ lastTime = new Date();
+ }
+
+ return ranking;
+
+}
+
const server = app.listen(port, () => {
console.log(`Record Service listening at http://localhost:${port}`);
});
diff --git a/users/recordservice/record-service.test.js b/users/recordservice/record-service.test.js
index d2c2ca6e..5594f781 100644
--- a/users/recordservice/record-service.test.js
+++ b/users/recordservice/record-service.test.js
@@ -1,5 +1,6 @@
const request = require('supertest');
const { MongoMemoryServer } = require('mongodb-memory-server');
+const Record = require('./record-model')
let mongoServer;
let app;
@@ -9,6 +10,8 @@ beforeAll(async () => {
const mongoUri = mongoServer.getUri();
process.env.MONGODB_URI = mongoUri;
app = require('./record-service');
+
+ await populateDatabase();
});
afterAll(async () => {
@@ -99,6 +102,33 @@ describe('Record Service', () => {
response = await request(app).post('/record').send(newUser);
expect(response.status).toBe(400);
+ //Data lacks competitive field
+ newUser = {
+ user:"testuser",
+ game:
+ {
+ "questions": [
+ {
+ "question": "¿Cuál es el río más largo del mundo?",
+ "answers": ["Nilo", "Amazonas", "Yangtsé", "Misisipi"],
+ "answerGiven": "Amazonas",
+ "correctAnswer": "Amazonas"
+ },
+ {
+ "question": "¿Cuál es el elemento más abundante en la corteza terrestre?",
+ "answers": ["Hierro", "Oxígeno", "Silicio", "Aluminio"],
+ "answerGiven": "Oxígeno",
+ "correctAnswer": "Oxígeno"
+ }
+ ],
+ "points": 2500,
+ "date": "02/02/24"
+ }
+ };
+
+ response = await request(app).post('/record').send(newUser);
+ expect(response.status).toBe(500);
+
});
it('should add a new record on POST /record', async () => {
const newUser = {
@@ -120,7 +150,8 @@ describe('Record Service', () => {
}
],
"points": 2500,
- "date": "02/02/24"
+ "date": "02/02/24",
+ "competitive": false
}
};
@@ -161,7 +192,8 @@ describe('Record Service', () => {
}
],
"points": 3000,
- "date": "03/03/24"
+ "date": "03/03/24",
+ "competitive": false
}
};
@@ -174,4 +206,78 @@ describe('Record Service', () => {
expect(responseGet.body.record.games[1]).toHaveProperty('date', '03/03/24');
});
-});
\ No newline at end of file
+
+ it('should get back on GET /record/testuser', async () => {
+ const responseGet = await request(app).get('/record/testuser');
+ expect(responseGet.status).toBe(200);
+ expect(responseGet.body.record.games[0]).toHaveProperty('date', '02/02/24');
+ });
+
+ it('should get back on GET /record/testuser', async () => {
+ const responseGet = await request(app).get('/record/testuser');
+ expect(responseGet.status).toBe(200);
+ expect(responseGet.body.record.games[0]).toHaveProperty('date', '02/02/24');
+ });
+
+ it('should get back on GET /record/ranking/top10', async () => {
+ const responseGet = await request(app).get('/record/ranking/top10');
+ expect(responseGet.status).toBe(200);
+ const usersStats = responseGet.body.usersCompetitiveStats;
+ expect(usersStats.length).toBe(10); //Only top 10
+
+ //Ordered by points
+ expect(usersStats[0]).toHaveProperty('_id', 'user10');
+ expect(usersStats[9]).toHaveProperty('_id', 'user1');
+
+ expect(usersStats[0]).toHaveProperty('totalCompetitiveGames', 2);
+ expect(usersStats[0]).toHaveProperty('totalPoints', 200);
+ });
+
+ it('should get back on GET /record/ranking/user1', async () => {
+ const responseGet = await request(app).get('/record/ranking/user1');
+ expect(responseGet.status).toBe(200);
+ const userStats = responseGet.body.userCompetitiveStats;
+
+ expect(userStats).toHaveProperty('_id', 'user1');
+ expect(userStats).toHaveProperty('position', 10);
+ expect(userStats).toHaveProperty('totalCompetitiveGames', 2);
+ expect(userStats).toHaveProperty('totalPoints', 20); //i * 10 * totalCompetitiveGames , i = 2
+ });
+});
+
+
+
+async function populateDatabase() {
+ try {
+ // Generate 10 users
+ for (let i = 1; i <= 10; i++) {
+ const user = `user${i}`;
+ const games = [];
+
+ // Generate 3 games for each user
+ for (let j = 1; j <= 3; j++) {
+ const game = {
+ questions: [
+ {
+ question: `Question ${j} for ${user}`,
+ answers: ["Answer 1", "Answer 2", "Answer 3", "Answer 4"],
+ answerGiven: "Answer 1",
+ correctAnswer: "Answer 1"
+ }
+ ],
+ points: i * 10,
+ date: "04/01/2024",
+ competitive: j <= 2 ? true : false // Only 2 games are competitive
+ };
+ games.push(game);
+ }
+
+ // Guardar el usuario en la base de datos
+ await Record.create({ user, games });
+ }
+
+ console.log('Database populated successfully');
+ } catch (error) {
+ console.error('Error populating database:', error);
+ }
+}
\ No newline at end of file
diff --git a/webapp/e2e/features/gameMenu.feature b/webapp/e2e/features/gameMenu.feature
new file mode 100644
index 00000000..a156fbbc
--- /dev/null
+++ b/webapp/e2e/features/gameMenu.feature
@@ -0,0 +1,8 @@
+Feature: Game Menu page functionality
+ Scenario: There should be visible three links
+ Given I am on the game menu
+ Then three buttons should be visible
+ Scenario: New Game should go to game configurator
+ Given I am on the game menu
+ When I click on New Game
+ Then I should be in the game configurator
diff --git a/webapp/e2e/steps/gameMenu.steps.js b/webapp/e2e/steps/gameMenu.steps.js
new file mode 100644
index 00000000..215af2f7
--- /dev/null
+++ b/webapp/e2e/steps/gameMenu.steps.js
@@ -0,0 +1,48 @@
+const puppeteer = require('puppeteer');
+const { defineFeature, loadFeature } = require('jest-cucumber');
+const setDefaultOptions = require('expect-puppeteer').setDefaultOptions;
+
+const feature = loadFeature('./features/gameMenu.feature');
+
+let page;
+let browser;
+
+defineFeature(feature, test => {
+
+ beforeAll(async () => {
+ browser = await puppeteer.launch({
+ slowMo: 20,
+ defaultViewport: { width: 1920, height: 1080 },
+ args: ['--window-size=1920,1080']
+ });
+
+ page = await browser.newPage();
+ setDefaultOptions({ timeout: 10000 });
+ });
+
+ test('There should be visible three links', ({ given, then }) => {
+ given('I am on the game menu', async () => {
+ await page.goto('http://localhost:3000/menu');
+ await page.waitForSelector('.divMenu');
+ });
+
+ then('three buttons should be visible', async () => {
+ //await expect(page).toMatchElement('.linkButton');
+ const elements = await page.$$('.linkButton');
+ expect(elements.length).toBeGreaterThan(0); // At least one element with class 'linkButton'
+ });
+ });
+ test('New Game should go to game configurator', ({ given, when, then }) => {
+ given('I am on the game menu', async () => {
+ await page.goto('http://localhost:3000/menu');
+ await page.waitForSelector('.divMenu');
+ });
+ when('I click on New Game', async () => {
+ await page.click('.linkButton');
+ });
+ then('I should be in the game configurator', async () => {
+ await expect(page).toMatchElement('.GameConfiguratorDiv');
+ });
+ });
+
+});
diff --git a/webapp/public/favicon.ico b/webapp/public/favicon.ico
index a11777cc..5019a7d6 100644
Binary files a/webapp/public/favicon.ico and b/webapp/public/favicon.ico differ
diff --git a/webapp/public/index.html b/webapp/public/index.html
index aa069f27..a9a639d1 100644
--- a/webapp/public/index.html
+++ b/webapp/public/index.html
@@ -3,6 +3,7 @@
+
{
+ document.title = 'WIQ';
+ }, []);
return (
@@ -29,6 +35,8 @@ function App() {
} />
} />
} />
+ }/>
+ } />
} />
diff --git a/webapp/src/components/GameConfigurator/GameConfigurator.js b/webapp/src/components/GameConfigurator/GameConfigurator.js
new file mode 100644
index 00000000..4a536729
--- /dev/null
+++ b/webapp/src/components/GameConfigurator/GameConfigurator.js
@@ -0,0 +1,89 @@
+import React, { useState } from 'react';
+
+import {useTranslation} from "react-i18next";
+import { Link } from "react-router-dom";
+import QuestionView from '../questionView/QuestionView'
+import BackButtonToGameMenu from '../fragments/BackButtonToGameMenu';
+
+function GameConfigurator(){
+ const [tipoPregunta, setTipoPregunta] = useState('POPULATION');
+ const [numeroPreguntas, setNumeroPreguntas] = useState(5);
+ const [clickedForNewGame, setClickedForNewGame]= useState(false);
+ const[t] = useTranslation("global");
+
+ function handleClick() {
+ setClickedForNewGame(true);
+ }
+
+ function handleClickRandomize() {
+ const options = ['ALL', 'POPULATION', 'CAPITAL', 'LANGUAGE', 'SIZE'];
+ const randomOptionIndex = Math.floor(Math.random() * options.length);
+ setTipoPregunta(options[randomOptionIndex]);
+
+ const randomNumQuestions = Math.floor(Math.random() * 20) + 1; // Random number between 1 and 20
+ setNumeroPreguntas(randomNumQuestions);
+ }
+ return (
+ clickedForNewGame ? :
+
+
+
{t("gameConfigurator.game_config")}
+
{t("gameConfigurator.custo_game")}
+
+
+
+
+
+
+ {/* Spinner para seleccionar el número de preguntas */}
+
setNumeroPreguntas(e.target.value)}
+ min="1" max="20"
+ />
+
+
+
+
+
+ {t("gameConfigurator.competi_game")}
+ {t("gameConfigurator.rules_competi")}
+ {/* Botones para jugar un juego personalizado o competitivo */}
+
+
+
+ );
+}
+
+function ButtonRandomizeCustom({t,handleClick}){
+ return (
+
+ );
+
+}
+
+function ButtonCustomized({t,handleClick}) {
+ return (
+
+ );
+}
+
+
+function ButtonCompetitive({t}){
+
+ return (
+
+ {t("gameConfigurator.play_competi")}
+
+ );
+}
+
+
+export default GameConfigurator;
diff --git a/webapp/src/components/GameConfigurator/GameConfigurator.test.js b/webapp/src/components/GameConfigurator/GameConfigurator.test.js
new file mode 100644
index 00000000..273b93c0
--- /dev/null
+++ b/webapp/src/components/GameConfigurator/GameConfigurator.test.js
@@ -0,0 +1,51 @@
+import React from 'react';
+import { render, screen, fireEvent } from '@testing-library/react';
+import '@testing-library/jest-dom/extend-expect'; // For additional matchers like toBeInTheDocument
+import { BrowserRouter as Router } from 'react-router-dom';
+import GameConfigurator from './GameConfigurator';
+import { initReactI18next } from 'react-i18next';
+import i18en from 'i18next';
+
+
+i18en.use(initReactI18next).init({
+ resources: {},
+ lng: 'en',
+ interpolation:{
+ escapeValue: false,
+ }
+});
+global.i18en = i18en;
+
+describe('GameConfigurator', () => {
+ test('renders GameConfigurator component', () => {
+ render();
+ expect(screen.getByText(i18en.t("gameConfigurator.game_config"))).toBeInTheDocument();
+ });
+
+
+ test('updates tipoPregunta state when select value changes', () => {
+ render();
+ const selectElement = screen.getByLabelText(i18en.t("gameConfigurator.type_quest"));
+ fireEvent.change(selectElement, { target: { value: 'CAPITAL' } });
+ expect(selectElement.value).toBe('CAPITAL');
+ });
+
+ test('updates numeroPreguntas state when input value changes', () => {
+ render();
+ const inputElement = screen.getByLabelText(i18en.t("gameConfigurator.num_quest"));
+ fireEvent.change(inputElement, { target: { value: '10' } });
+ expect(inputElement.value).toBe('10');
+ });
+ it('renders option to play customized game', () => {
+ render();
+ const text = screen.getByText(i18en.t('gameConfigurator.custo_game'));
+ expect(text).toBeInTheDocument();
+});
+
+it('renders option to play Competitive game', () => {
+ render();
+ const text = screen.getByText(i18en.t('gameConfigurator.competi_game'));
+ expect(text).toBeInTheDocument();
+});
+
+});
diff --git a/webapp/src/components/GameMenu/GameMenu.js b/webapp/src/components/GameMenu/GameMenu.js
index 7f7bd957..7a1f94ed 100644
--- a/webapp/src/components/GameMenu/GameMenu.js
+++ b/webapp/src/components/GameMenu/GameMenu.js
@@ -5,12 +5,15 @@ import ButtonHistoricalData from "../HistoricalData/ButtonHistoricalData";
export default function GameMenu() {
const[t] = useTranslation("global");
+
+
return (
{t("gameMenu.title")}
-
+
+
);
}
@@ -18,8 +21,19 @@ export default function GameMenu() {
function ButtonNewGame({ t }) {
return (
-
+
{t("gameMenu.new_game_button")}
);
- }
\ No newline at end of file
+ }
+
+ function ButtonRanking({ t }) {
+ return (
+
+ {t("gameMenu.view_ranking")}
+
+
+ );
+ }
+
+
\ No newline at end of file
diff --git a/webapp/src/components/GameMenu/GameMenu.test.js b/webapp/src/components/GameMenu/GameMenu.test.js
index 7d9b7e4d..a52ed153 100644
--- a/webapp/src/components/GameMenu/GameMenu.test.js
+++ b/webapp/src/components/GameMenu/GameMenu.test.js
@@ -33,6 +33,12 @@ describe('GameMenu component', () => {
const text = screen.getByText(i18en.t('gameMenu.history_button'));
expect(text).toBeInTheDocument();
});
+
+ it('renders option to view ranking data', () => {
+ render();
+ const text = screen.getByText(i18en.t('gameMenu.view_ranking'));
+ expect(text).toBeInTheDocument();
+ });
});
diff --git a/webapp/src/components/HistoricalData/HistoricalView.js b/webapp/src/components/HistoricalData/HistoricalView.js
index f110430c..66dc8a2d 100644
--- a/webapp/src/components/HistoricalData/HistoricalView.js
+++ b/webapp/src/components/HistoricalData/HistoricalView.js
@@ -2,7 +2,7 @@ import React, { useState } from 'react';
import {useTranslation} from "react-i18next";
import HistoryRecordRetriever from './HistoryRecordRetriever';
import { useUserContext } from '../loginAndRegistration/UserContext';
-
+import BackButtonToGameMenu from '../fragments/BackButtonToGameMenu';
import RecordList from './RecordList';
@@ -29,6 +29,7 @@ export default function HistoricalView() {
return (
+
{(records && records.length !== 0) ? records.map((record, index) => (
)): {t("historicalView.no_games_played")}
}
@@ -53,4 +54,5 @@ function HistoricalGameElement({record,t}){
);
-}
\ No newline at end of file
+}
+
diff --git a/webapp/src/components/HistoricalData/HistoryRecordRetriever.js b/webapp/src/components/HistoricalData/HistoryRecordRetriever.js
index d586a867..0e65d6ff 100644
--- a/webapp/src/components/HistoricalData/HistoryRecordRetriever.js
+++ b/webapp/src/components/HistoricalData/HistoryRecordRetriever.js
@@ -11,15 +11,81 @@ class HistoryRecordRetriever{
try {
const response = await axios.get(this.apiUrl + '/' + user);
const receivedRecords = await response.data;
- console.log(receivedRecords)
- console.log(receivedRecords[0])
return receivedRecords.record;
} catch (error) {
console.log(error)
throw new Error(error);
}
+ /*
+ return {
+ userId: user,
+ games: [
+ {
+ questions: [
+ {
+ question: "¿Cuál es la capital de Francia?",
+ answers: ["Madrid", "París", "Londres", "Roma"],
+ answerGiven: "París",
+ correctAnswer: "París"
+ },
+ {
+ question: "¿En qué año comenzó la Segunda Guerra Mundial?",
+ answers: ["1939", "1945", "1914", "1941"],
+ answerGiven: "1939",
+ correctAnswer: "1939"
+ },
+ {
+ question: "¿Quién escribió 'Don Quijote de la Mancha'?",
+ answers: ["Miguel de Cervantes", "Gabriel García Márquez", "Federico García Lorca", "Jorge Luis Borges"],
+ answerGiven: "Miguel de Cervantes",
+ correctAnswer: "Miguel de Cervantes"
+ }
+ ],
+ points: 3000,
+ date: "01/02/24"
+ },
+ {
+ questions: [
+ {
+ question: "¿Cuál es el río más largo del mundo?",
+ answers: ["Nilo", "Amazonas", "Yangtsé", "Misisipi"],
+ answerGiven: "Amazonas",
+ correctAnswer: "Amazonas"
+ },
+ {
+ question: "¿Cuál es el elemento más abundante en la corteza terrestre?",
+ answers: ["Hierro", "Oxígeno", "Silicio", "Aluminio"],
+ answerGiven: "Oxígeno",
+ correctAnswer: "Oxígeno"
+ }
+ ],
+ points: 2500,
+ date: "02/02/24"
+ },
+ {
+ questions: [
+ {
+ question: "¿Quién pintó la Mona Lisa?",
+ answers: ["Leonardo da Vinci", "Pablo Picasso", "Vincent van Gogh", "Rembrandt"],
+ answerGiven: "Leonardo da Vinci",
+ correctAnswer: "Leonardo da Vinci"
+ },
+ {
+ question: "¿Cuál es el planeta más grande del sistema solar?",
+ answers: ["Júpiter", "Saturno", "Neptuno", "Urano"],
+ answerGiven: "Júpiter",
+ correctAnswer: "Júpiter"
+ }
+ ],
+ points: 3500,
+ date: "03/02/24"
+ }
+ ]
+ };*/
}
+
+
}
diff --git a/webapp/src/components/fragments/BackButtonToGameMenu.js b/webapp/src/components/fragments/BackButtonToGameMenu.js
new file mode 100644
index 00000000..e9bab12c
--- /dev/null
+++ b/webapp/src/components/fragments/BackButtonToGameMenu.js
@@ -0,0 +1,9 @@
+import { Link } from "react-router-dom";
+
+export default function BackButton({t}){
+ return(
+
+ ⬅ {t("gameMenu.back")}
+
+ );
+ }
\ No newline at end of file
diff --git a/webapp/src/components/fragments/BackButtonToGameMenu.test.js b/webapp/src/components/fragments/BackButtonToGameMenu.test.js
new file mode 100644
index 00000000..586cf695
--- /dev/null
+++ b/webapp/src/components/fragments/BackButtonToGameMenu.test.js
@@ -0,0 +1,31 @@
+import { render, screen } from '@testing-library/react';
+import BackButtonToGameMenu from './BackButtonToGameMenu';
+import { MemoryRouter } from 'react-router-dom';
+
+import { initReactI18next } from 'react-i18next';
+import i18en from 'i18next';
+
+i18en.use(initReactI18next).init({
+ resources: {},
+ lng: 'en',
+ interpolation:{
+ escapeValue: false,
+ }
+});
+global.i18en = i18en;
+
+
+describe('BackButtonToGameMenu component', () => {
+
+ it('renders option to go back to the game menu', () => {
+ render();
+ const text = screen.getByText((content, element) => {
+ const regex = new RegExp(i18en.t("gameMenu.back"));
+ return regex.test(content);
+ });
+
+ expect(text).toBeInTheDocument();
+ });
+});
+
+
diff --git a/webapp/src/components/fragments/Loader.js b/webapp/src/components/fragments/Loader.js
new file mode 100644
index 00000000..2cd8af7c
--- /dev/null
+++ b/webapp/src/components/fragments/Loader.js
@@ -0,0 +1,15 @@
+import React from 'react';
+
+const Loader = () => {
+ return (
+
+ );
+}
+
+export default Loader;
diff --git a/webapp/src/components/questionView/CreationHistoricalRecord.js b/webapp/src/components/questionView/CreationHistoricalRecord.js
index 9da8f624..3bdb6370 100644
--- a/webapp/src/components/questionView/CreationHistoricalRecord.js
+++ b/webapp/src/components/questionView/CreationHistoricalRecord.js
@@ -29,35 +29,36 @@ class CreationHistoricalRecord{
this.record.game.date = date;
}
+ setCompetitive(isCompetitive){
+ this.record.game.competitive = isCompetitive;
+ }
+
getRecord() {
return this.record;
}
async sendRecord(user) {
- const apiUrl = (process.env.REACT_APP_API_ENDPOINT || 'http://localhost:8000') + "/record";
-
- const body = {
- user:user,
- game:this.record.game
- }
- try {
+ const apiUrl = (process.env.REACT_APP_API_ENDPOINT || 'http://localhost:8000') + "/record";
+
+ const body = {
+ user: user,
+ game: this.record.game
+ };
+
+ try {
const response = await axios.post(apiUrl, body, {
- headers: {
- 'Content-Type': 'application/json'
- }
+ headers: {
+ 'Content-Type': 'application/json'
+ }
});
-
- if (!response.ok) {
- throw new Error('Error al enviar el registro');
- }
-
- const data = await response.json();
- console.log(data);
- } catch (error) {
- console.error('Error:', error);
- }
+
+
+ console.log('Registro enviado:', response.data);
+ } catch (error) {
+ console.error('Error al enviar el registro:', error.message);
}
+ }
}
export default CreationHistoricalRecord;
diff --git a/webapp/src/components/questionView/QuestionGenerator.js b/webapp/src/components/questionView/QuestionGenerator.js
index 32facf1a..9d204004 100644
--- a/webapp/src/components/questionView/QuestionGenerator.js
+++ b/webapp/src/components/questionView/QuestionGenerator.js
@@ -8,37 +8,42 @@ class QuestionGenerator{
}
- async generateQuestions(lang) {
-
- // try {
- // //const response = await fetch(this.apiUrl);
- // //const receivedQuestions = await response.json();
+ async generateQuestions(lang, type, amount) {
+ /*
+ try {
+ //const response = await fetch(this.apiUrl);
+ //const receivedQuestions = await response.json();
- // //Mockup
- // // console.log("type: "+type+" amount: "+amount)
- // const receivedQuestions = JSON.parse('{"0":{"question":"¿Cuál es la población de Oviedo?","answers":["225089","191325","220587","121548"]},'+
- // '"1":{"question":"¿Which is the population of Gijon?","answers":["275274","159658","233982","305554"]},'+
- // '"2":{"question":"¿Cuál es la población de Avilés?","answers":["82568","115595","41284","122200"]},'+
- // '"3":{"question":"¿Cuál es la capital de Asturias?","answers":["Ciudad de Oviedo","a","b","c"]},'+
- // '"4":{"question":"¿Cuál es la capital de España?","answers":["Madrid","a","b","c"]},'+
- // '"5":{"question":"¿Cuál es la capital de Turquía?","answers":["Ankara","a","b","c"]}}')
+ //Mockup
+ console.log("type: "+type+" amount: "+amount)
+ const receivedQuestions = JSON.parse('{"0":{"question":"¿Cuál es la población de Oviedo?","answers":["225089","191325","220587","121548"]},'+
+ '"1":{"question":"¿Cuál es la población de Gijón?","answers":["275274","159658","233982","305554"]},'+
+ '"2":{"question":"¿Cuál es la población de Avilés?","answers":["82568","115595","41284","122200"]},'+
+ '"3":{"question":"¿Cuál es la capital de Asturias?","answers":["Ciudad de Oviedo","a","b","c"]},'+
+ '"4":{"question":"¿Cuál es la capital de España?","answers":["Madrid","a","b","c"]},'+
+ '"5":{"question":"¿Cuál es la capital de Turquía?","answers":["Ankara","a","b","c"]}}')
- // let i = 0;
- // var questions = [];
- // for (const key in receivedQuestions) {
- // questions[i] = new Question(receivedQuestions[key]);
- // i += 1;
- // }
- // console.log(questions);
- // return questions;
- // } catch (error) {
- // throw new Error(error);
- // }
-
+ let i = 0;
+ var questions = [];
+ for (const key in receivedQuestions) {
+ questions[i] = new Question(receivedQuestions[key]);
+ i += 1;
+ }
+ console.log(questions);
+ return questions;
+ } catch (error) {
+ throw new Error(error);
+ }
+ */
try {
- const response = await axios.get(this.apiUrl + '/' + lang);
+ let response;
+ if(type==="COMPETITIVE"){
+ response = await axios.get(this.apiUrl + '/' + lang);
+ }else{
+ response = await axios.get(this.apiUrl + '/' + lang + '/' +amount + '/' + type);
+ }
const receivedQuestions = await response.data;
let i = 0;
var questions = [];
diff --git a/webapp/src/components/questionView/QuestionView.js b/webapp/src/components/questionView/QuestionView.js
index ca598e11..b84d4c4a 100644
--- a/webapp/src/components/questionView/QuestionView.js
+++ b/webapp/src/components/questionView/QuestionView.js
@@ -9,12 +9,12 @@ import $ from 'jquery';
import RecordList from '../HistoricalData/RecordList';
import ButtonHistoricalData from "../HistoricalData/ButtonHistoricalData";
import { useUserContext } from '../loginAndRegistration/UserContext';
+import BackButtonToGameMenu from '../fragments/BackButtonToGameMenu';
const creationHistoricalRecord = new CreationHistoricalRecord();
const questionGenerator = new QuestionGenerator();
var points = 0;
-function QuestionView(){
-
+function QuestionView({type= "COMPETITIVE", amount=5}){
const [numQuestion, setnumQuestion] = useState(-1);
const [questions, setQuestions] = useState(null);
const[t, i18n] = useTranslation("global");
@@ -25,7 +25,7 @@ function QuestionView(){
const generateQuestions = async (numQuestion) => {
if (numQuestion < 0) {
try {
- var generatedQuestions = await questionGenerator.generateQuestions(i18n.language);
+ var generatedQuestions = await questionGenerator.generateQuestions(i18n.language, type, amount);
setQuestions(generatedQuestions);
setnumQuestion(0);
} catch (error) {
@@ -106,6 +106,7 @@ function QuestionView(){
//Last question sends the record
if(!(numQuestion < questions.length - 1)){
audio.pause();
+ creationHistoricalRecord.setCompetitive(type === 'COMPETITIVE');
creationHistoricalRecord.setDate(Date.now());
creationHistoricalRecord.setPoints(points);
creationHistoricalRecord.sendRecord(user.username);
@@ -204,6 +205,7 @@ function QuestionComponent({questions, numQuestion, handleClick, t, points, audi
<>
{t("questionView.finished_game")}
+
{points} {t("questionView.point")}
< RecordList record={creationHistoricalRecord.getRecord().game}/>
diff --git a/webapp/src/components/questionView/QuestionView.test.js b/webapp/src/components/questionView/QuestionView.test.js
index a3a34531..90248453 100644
--- a/webapp/src/components/questionView/QuestionView.test.js
+++ b/webapp/src/components/questionView/QuestionView.test.js
@@ -51,9 +51,9 @@ global.i18en = i18en;
describe('Question View component', () => {
- beforeEach(() => {
- mockAxios.reset();
- });
+ mockAxios.onGet('http://localhost:8000/questions/en').reply(200,
+ [{question: "What is the population of Oviedo?",
+ answers: ["225089","272357","267855","231841"]}]);
it('shows the no_questions_message as the endpoint does not exist',async () => {
render();
@@ -65,10 +65,6 @@ describe('Question View component', () => {
// Test for sound functionality
it('speaks the question when the speaker button is clicked', async () => {
- const questionText = "What is the population of Oviedo?";
- mockAxios.onGet('http://localhost:8000/questions/en').reply(200,
- [{question: questionText,
- answers: ["225089","272357","267855","231841"]}]);
await act(async () => {
render();
@@ -83,10 +79,6 @@ describe('Question View component', () => {
});
it('shows a question and answers',async () => {
-
- mockAxios.onGet('http://localhost:8000/questions/en').reply(200,
- [{question: "What is the population of Oviedo?",
- answers: ["225089","272357","267855","231841"]}]);
//It gives an error as we are not wrapping it by act, however by doing this we simulate a no questions situation
await act(async () =>{
@@ -104,9 +96,6 @@ describe('Question View component', () => {
});
it('shows colors to reveal correct answer and it sounds', async () => {
setupAudioMock();
- mockAxios.onGet('http://localhost:8000/questions/en').reply(200,
- [{question: "What is the population of Oviedo?",
- answers: ["225089","272357","267855","231841"]}]);
await act(async () =>{
await render();
@@ -125,9 +114,6 @@ describe('Question View component', () => {
});
it('shows colors to reveal false answer and it sounds', async () => {
setupAudioMock()
- mockAxios.onGet('http://localhost:8000/questions/en').reply(200,
- [{question: "What is the population of Oviedo?",
- answers: ["225089","272357","267855","231841"]}]);
await act(async () =>{
await render();
@@ -146,9 +132,6 @@ describe('Question View component', () => {
it('shows timer and tiktak sound', async () => {
setupAudioMock()
- mockAxios.onGet('http://localhost:8000/questions/en').reply(200,
- [{question: "What is the population of Oviedo?",
- answers: ["225089","272357","267855","231841"]}]);
await act(async () =>{
await render();
@@ -158,7 +141,24 @@ describe('Question View component', () => {
const timerElement = screen.getByText(new RegExp(`(\\d+) ${i18en.t('questionView.seconds')}`));
expect(timerElement).toBeInTheDocument(); // Verificar que el temporizador esté presente en el DOM
- });
+ });
+
+ it('shows finish game review',async () => {
+ mockAxios.onGet('http://localhost:8000/questions/en').reply(200, []);
+ mockAxios.onPost('http://localhost:8000/record').reply(200, {user:'myUser'});
+
+ const user = { username: 'myUser' };
+
+ //It gives an error as we are not wrapping it by act, however by doing this we simulate a no questions situation
+ await act(async () =>{
+ await render();
+ })
+
+ await waitFor(() => expect(screen.getByText(i18en.t('questionView.finished_game'))).toBeInTheDocument());
+
+ expect(screen.getByText('What is the population of Oviedo?')).toBeInTheDocument()
+
+ });
// it('renders end message when countdown completes', async() => {
diff --git a/webapp/src/components/ranking/RankingRetriever.js b/webapp/src/components/ranking/RankingRetriever.js
new file mode 100644
index 00000000..a84de7e6
--- /dev/null
+++ b/webapp/src/components/ranking/RankingRetriever.js
@@ -0,0 +1,101 @@
+import axios from 'axios';
+
+class RankingRetriever{
+
+ constructor(){
+ this.apiUrl = (process.env.REACT_APP_API_ENDPOINT || 'http://localhost:8000')+ "/record/ranking";
+
+ }
+
+ async getTopTen() {
+
+ try {
+ const response = await axios.get(this.apiUrl + '/top10');//finding the top ten
+ const receivedTopTenRanking = await response.data;
+ return receivedTopTenRanking;
+ } catch (error) {
+ console.log(error)
+ throw new Error(error);
+
+ }
+ /*
+ return {
+ "usersCompetitiveStats": [
+ {
+ "_id": "user",
+ "totalPoints": 1000,
+ "totalCompetitiveGames": 4
+ },
+ {
+ "_id": "user2",
+ "totalPoints": 900,
+ "totalCompetitiveGames": 2
+ },
+ {
+ "_id": "user3",
+ "totalPoints": 800,
+ "totalCompetitiveGames": 3
+ },
+ {
+ "_id": "user4",
+ "totalPoints": 700,
+ "totalCompetitiveGames": 5
+ },
+ {
+ "_id": "user5",
+ "totalPoints": 600,
+ "totalCompetitiveGames": 6
+ },
+ {
+ "_id": "user6",
+ "totalPoints": 500,
+ "totalCompetitiveGames": 7
+ },
+ {
+ "_id": "user7",
+ "totalPoints": 400,
+ "totalCompetitiveGames": 8
+ },
+ {
+ "_id": "user8",
+ "totalPoints": 300,
+ "totalCompetitiveGames": 9
+ },
+ {
+ "_id": "user9",
+ "totalPoints": 200,
+ "totalCompetitiveGames": 10
+ },
+ {
+ "_id": "user10",
+ "totalPoints": 100,
+ "totalCompetitiveGames": 11
+ }
+ ]
+ }*/
+ }
+ async getUser(user){
+
+ try {
+ const response = await axios.get(this.apiUrl + '/'+user);//finding the top ten
+ const receivedMyRanking = await response.data;
+ return receivedMyRanking.userCompetitiveStats;
+ } catch (error) {
+ console.log(error)
+ throw new Error(error);
+
+ }
+ /*
+ return {
+ "_id": "myUser",
+ "totalPoints": 250,
+ "totalCompetitiveGames": 1
+ };*/
+ }
+
+
+
+}
+
+export default RankingRetriever;
+
diff --git a/webapp/src/components/ranking/RankingView.js b/webapp/src/components/ranking/RankingView.js
new file mode 100644
index 00000000..f2aad16e
--- /dev/null
+++ b/webapp/src/components/ranking/RankingView.js
@@ -0,0 +1,105 @@
+import React, { useState } from 'react';
+import RankingRetriever from './RankingRetriever';
+import {useTranslation} from "react-i18next";
+import Loader from "../fragments/Loader"
+import BackButton from '../fragments/BackButtonToGameMenu';
+import { useUserContext } from '../loginAndRegistration/UserContext';
+
+const retriever = new RankingRetriever();
+
+const RankingView = () => {
+ const[t] = useTranslation("global");
+ const {user} = useUserContext();
+
+ const [rankingData, setRankingData] = useState(null);
+ const [myRankingData, setMyRankingData] = useState(null);
+ const [searchTerm, setSearchTerm] = useState(user.username);
+
+
+
+ const getRanking = async () => {
+ try {
+ var ranking = await retriever.getTopTen();
+ setRankingData(ranking.usersCompetitiveStats);
+ var myrank = await retriever.getUser(user.username);
+ setMyRankingData(myrank);
+ } catch (error) {
+ console.log(error);
+ }
+ }
+ const handleSearch = async (e) => {
+ e.preventDefault();
+ if(searchTerm.length!==0){
+ try {
+ const rank = await retriever.getUser(searchTerm);
+ setMyRankingData(rank);
+ } catch (error) {
+ console.log(error);
+ }
+ }
+
+ }
+ if(rankingData==null || myRankingData == null){
+ getRanking();
+ }
+
+ return (
+
+
+
{t("ranking.ranking")}
+ {rankingData && rankingData.length > 0 && myRankingData ? (
+ <>
+
+ >
+ ) : (
+ < Loader />
+ )}
+
+ );
+};
+
+export default RankingView;
diff --git a/webapp/src/components/ranking/RankingView.test.js b/webapp/src/components/ranking/RankingView.test.js
new file mode 100644
index 00000000..76c0506a
--- /dev/null
+++ b/webapp/src/components/ranking/RankingView.test.js
@@ -0,0 +1,182 @@
+import { render , screen, waitFor, fireEvent } from '@testing-library/react';
+import { MemoryRouter } from 'react-router-dom';
+import axios from 'axios';
+import { initReactI18next } from 'react-i18next';
+import i18en from 'i18next';
+import RankingView from './RankingView';
+import MockAdapter from 'axios-mock-adapter';
+import { act } from 'react-dom/test-utils';
+import { UserContextProvider} from '../loginAndRegistration/UserContext';
+i18en.use(initReactI18next).init({
+ resources: {},
+ lng: 'en',
+ interpolation:{
+ escapeValue: false,
+ }
+});
+global.i18en = i18en;
+
+const mockAxios = new MockAdapter(axios);
+describe('RankingView component', () => {
+ const user = { username: 'myUser' };
+
+ it('renders title', () => {
+ act(()=>{
+ render();
+ })
+ const text = screen.getByText(i18en.t('ranking.ranking'));
+ expect(text).toBeInTheDocument();
+ });
+ it('renders Loading if the call to the gateway has not been done', () => {
+ act(()=>{
+ render();
+ })
+ const text = screen.getByText('Loading...');
+ expect(text).toBeInTheDocument();
+ });
+});
+ describe('RankingView component with endpoint', ()=>{
+ mockAxios.onGet('http://localhost:8000/record/ranking/top10').reply(200,
+ {
+ "usersCompetitiveStats": [
+ {
+ "_id": "user",
+ "totalPoints": 1000,
+ "totalCompetitiveGames": 4
+ },
+ {
+ "_id": "user2",
+ "totalPoints": 900,
+ "totalCompetitiveGames": 2
+ },
+ {
+ "_id": "user3",
+ "totalPoints": 800,
+ "totalCompetitiveGames": 3
+ },
+ {
+ "_id": "user4",
+ "totalPoints": 700,
+ "totalCompetitiveGames": 5
+ },
+ {
+ "_id": "user5",
+ "totalPoints": 600,
+ "totalCompetitiveGames": 6
+ },
+ {
+ "_id": "user6",
+ "totalPoints": 500,
+ "totalCompetitiveGames": 7
+ },
+ {
+ "_id": "user7",
+ "totalPoints": 400,
+ "totalCompetitiveGames": 8
+ },
+ {
+ "_id": "user8",
+ "totalPoints": 300,
+ "totalCompetitiveGames": 9
+ },
+ {
+ "_id": "user9",
+ "totalPoints": 200,
+ "totalCompetitiveGames": 10
+ },
+ {
+ "_id": "user10",
+ "totalPoints": 100,
+ "totalCompetitiveGames": 11
+ }
+ ]
+ });
+
+ mockAxios.onGet('http://localhost:8000/record/ranking/myUser').reply(200,
+ {userCompetitiveStats:
+ {
+ "_id": "myUser",
+ "totalPoints": 250,
+ "totalCompetitiveGames": 1,
+ "position":10
+ }
+ }
+ );
+ const user = { username: 'myUser' };
+
+ it('renders position all headers in the table',async ()=>{
+
+ await act(async () =>{
+ await render();
+
+ })
+ await waitFor(() => expect(screen.getByText(i18en.t('ranking.position'))).toBeInTheDocument());
+ expect(screen.getByText(i18en.t('ranking.username'))).toBeInTheDocument()
+ expect(screen.getByText(i18en.t('ranking.points'))).toBeInTheDocument()
+ expect(screen.getByText(i18en.t('ranking.num_games'))).toBeInTheDocument()
+ });
+
+ it('renders position all users usernames',async ()=>{
+ await act(async () =>{
+ await render();
+
+ })
+ await waitFor(() => expect(screen.getByText(i18en.t('ranking.position'))).toBeInTheDocument());
+ //expect(screen.getByText("user1")).toBeInTheDocument()
+ expect(screen.getByText("user2")).toBeInTheDocument()
+ expect(screen.getByText("user3")).toBeInTheDocument()
+ expect(screen.getByText("user4")).toBeInTheDocument()
+ expect(screen.getByText("user5")).toBeInTheDocument()
+ expect(screen.getByText("user6")).toBeInTheDocument()
+ expect(screen.getByText("user7")).toBeInTheDocument()
+ expect(screen.getByText("user8")).toBeInTheDocument()
+ expect(screen.getByText("user9")).toBeInTheDocument()
+ expect(screen.getByText("user10")).toBeInTheDocument()
+ });
+ it('renders position all users totalPoints',async ()=>{
+
+ await act(async () =>{
+ await render();
+
+ })
+ await waitFor(() => expect(screen.getByText(i18en.t('ranking.position'))).toBeInTheDocument());
+ expect(screen.getByText("1000")).toBeInTheDocument()
+ expect(screen.getByText("900")).toBeInTheDocument()
+ expect(screen.getByText("800")).toBeInTheDocument()
+ expect(screen.getByText("700")).toBeInTheDocument()
+ expect(screen.getByText("600")).toBeInTheDocument()
+ expect(screen.getByText("500")).toBeInTheDocument()
+ expect(screen.getByText("400")).toBeInTheDocument()
+ expect(screen.getByText("300")).toBeInTheDocument()
+ expect(screen.getByText("200")).toBeInTheDocument()
+ });
+ it('renders position all users competitive games',async ()=>{
+
+ await act(async () =>{
+ await render();
+
+ })
+ await waitFor(() => expect(screen.getByText(i18en.t('ranking.position'))).toBeInTheDocument());
+
+ expect(screen.getAllByText(/2/).length).toBeGreaterThanOrEqual(2);//hay dos pq hay una posicion
+ expect(screen.getAllByText(/5/).length).toBeGreaterThanOrEqual(2);//hay dos pq hay una posicion
+ expect(screen.getAllByText(/6/).length).toBeGreaterThanOrEqual(2);//hay dos pq hay una posicion
+ expect(screen.getAllByText(/7/).length).toBeGreaterThanOrEqual(2);//hay dos pq hay una posicion
+ });
+ it('renders position all users competitive games',async ()=>{
+
+ await act(async () =>{
+ await render();
+
+ })
+ await waitFor(() => expect(screen.getByText(i18en.t('ranking.position'))).toBeInTheDocument());
+
+ expect(screen.getByText("myUser")).toBeInTheDocument()
+ expect(screen.getByText("250")).toBeInTheDocument()
+ //should be one if only your rank is shown
+ expect(screen.getAllByText(/1/).length).toBeGreaterThanOrEqual(2);//hay dos pq hay una posicion
+
+ });
+
+})
+
diff --git a/webapp/src/custom.css b/webapp/src/custom.css
index 453ce6c4..f6660f91 100644
--- a/webapp/src/custom.css
+++ b/webapp/src/custom.css
@@ -1362,6 +1362,26 @@ svg {
/*------------------------------Historical--------------------------------------------*/
/* Estilos para los botones */
+.linkButtonHistorical{
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ width: 10em;
+ height: 45px;
+ background:#00b8ff;
+ border: none;
+ outline: none;
+ border-radius: 40px;
+ box-shadow: 0 0 10px black;
+ cursor: pointer;
+ font-size: 1em;
+ color: black;
+ font-weight: 700;
+ margin-top: 1.5em;
+ margin-bottom: 1em;
+ text-decoration: none;
+
+}
.historicalButton {
color: white;
width: 50em;
@@ -1426,3 +1446,235 @@ svg {
h2{
text-align: center;
}
+/*------------------------Loader------------------------------*/
+@keyframes blinkCursor {
+ 50% {
+ border-right-color: transparent;
+ }
+}
+
+@keyframes typeAndDelete {
+ 0%,
+ 10% {
+ width: 0;
+ }
+ 45%,
+ 55% {
+ width: 6.2em;
+ } /* adjust width based on content */
+ 90%,
+ 100% {
+ width: 0;
+ }
+}
+
+.terminal-loader {
+ border: 0.1em solid #333;
+ background-color: #1a1a1a;
+ color: #0f0;
+ font-family: "Courier New", Courier, monospace;
+ font-size: 1em;
+ padding: 1.5em 1em;
+ width: 12em;
+ margin: 100px auto;
+ box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
+ border-radius: 4px;
+ position: relative;
+ overflow: hidden;
+ box-sizing: border-box;
+}
+
+.terminal-header {
+ position: absolute;
+ top: 0;
+ left: 0;
+ right: 0;
+ height: 1.5em;
+ background-color: #333;
+ border-top-left-radius: 4px;
+ border-top-right-radius: 4px;
+ padding: 0 0.4em;
+ box-sizing: border-box;
+}
+
+
+
+.terminal-title {
+ float: left;
+ line-height: 1.5em;
+ color: #eee;
+}
+
+.text {
+ display: inline-block;
+ white-space: nowrap;
+ overflow: hidden;
+ border-right: 0.2em solid green; /* Cursor */
+ animation: typeAndDelete 4s steps(11) infinite,
+ blinkCursor 0.5s step-end infinite alternate;
+ margin-top: 1.5em;
+}
+
+/*--------------------------------------------------Configurator---------------------------------*/
+/* Estilo para el elemento select */
+.buttonRandomize{
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ width: 15em;
+ height: 45px;
+ background:#00b8ff;
+ border: none;
+ outline: none;
+ border-radius: 40px;
+ box-shadow: 0 0 10px black;
+ cursor: pointer;
+ font-size: 1em;
+ color: black;
+ font-weight: 700;
+ margin-top: 1.5em;
+ margin-bottom: 1em;
+ text-decoration: none;
+}
+.select-style {
+ width: 200px;
+ height: 35px;
+ border: 1px solid #ccc;
+ border-radius: 5px;
+ background-color: #f8f8f8;
+ padding: 5px;
+ font-size: 16px;
+ color: #333;
+}
+
+.select-style:focus {
+ border-color: #6e9ecf;
+ outline: none;
+}
+
+/* Estilo para el spinner */
+.spinner-style {
+ width: 40px;
+ height: 30px;
+ border: 1px solid #ccc;
+ background-color: #f8f8f8;
+ border-radius: 5px;
+ font-size: medium;
+}
+.GameConfiguratorDiv> * {
+ margin-top: 10px;
+ margin-bottom: 10px;
+}
+.GameConfiguratorDiv {
+ left: 50%;
+ top: 50%;
+}
+/*
+.GameConfiguratorDiv {
+ display: flex;
+ flex-direction: column;
+}
+
+.GameConfiguratorDiv > *:not(:first-child):not(:nth-child(4)) {
+ text-align: left;
+}*/
+
+.hr-style {
+ border: 0;
+ border-top: 1px solid #8e888885; /* Color gris */
+ margin: 20px 0; /* Espacio antes y después de la línea */
+}
+
+.GameConfiguratorDiv h2 {
+ text-align: left; /* Alinea el texto a la izquierda */
+ margin-top:20px;
+ margin-bottom:20px;
+}
+.table tbody tr.penultimate-row td {
+ background-color: black;
+ padding: 15px;
+ /*
+ border: 1px solid rgba(205, 187, 187, 0.455);
+ box-shadow: 0 0 10px rgba(197, 191, 191, 0.726);*/
+}
+
+.table tbody tr.penultimate-row td div{
+ background-color: #171717;
+ padding: 0.5em;
+}
+
+.table th,td{
+ padding-left: 30px;
+ padding-right: 30px;
+ padding-top: 10px;
+ padding-bottom: 10px;
+ text-align: center;
+
+}
+/* Estilo para todos los td excepto el penúltimo */
+.table tbody tr:not(:nth-last-child(2)) td:nth-child(1) {
+ background-color: #4d1e51a8;
+}
+
+
+.table tr:nth-child(even) {background-color: #f2f2f22a;}
+.table th {
+ background-color: #69276f;
+ color: white;
+}
+.table tr:hover {background-color: #a59c9ca2;}
+
+.table table{
+ border-collapse: collapse;
+}
+/* Estilo para la fila de headers */
+.table th {
+ border-top: 2px solid rgba(205, 187, 187, 0.455); /* Borde superior para todas las celdas de la fila de headers */
+ border-left: 2px solid rgba(205, 187, 187, 0.455); /* Borde izquierdo en la primera celda */
+ border-right: 2px solid rgba(205, 187, 187, 0.455); /* Borde derecho en la última celda */
+}
+
+/* Estilo para las filas de datos (excepto la penúltima) */
+.table tbody tr:not(:nth-last-child(2)) td {
+ border-left: 2px solid rgba(205, 187, 187, 0.455); /* Borde izquierdo en la primera celda */
+ border-right: 2px solid rgba(205, 187, 187, 0.455); /* Borde derecho en la última celda */
+}
+
+/* Borde superior e inferior para la penúltima fila */
+.table tbody tr:nth-last-child(2) td {
+ border-top: 2px solid rgba(205, 187, 187, 0.455); /* Borde superior */
+ border-bottom: 2px solid rgba(205, 187, 187, 0.455); /* Borde inferior */
+}
+
+/* Borde inferior para todas las celdas de la última fila */
+.table tbody tr:last-child td {
+ border-bottom: 2px solid rgba(205, 187, 187, 0.455); /* Borde inferior */
+}
+
+/* Borde izquierdo para la primera celda de la última fila */
+.table tbody tr:last-child td:first-child {
+ border-left: 2px solid rgba(205, 187, 187, 0.455); /* Borde izquierdo */
+}
+
+/* Borde derecho para la última celda de la última fila */
+.table tbody tr:last-child td:last-child {
+ border-right: 2px solid rgba(205, 187, 187, 0.455); /* Borde derecho */
+}
+
+/* Estilo para el cuadro de búsqueda */
+input[type="text"] {
+ border: 2px solid #636262; /* Borde sólido */
+ padding: 8px; /* Espaciado interno */
+ font-size: 16px; /* Tamaño de letra */
+ color: #e5e0e0; /* Color de letra */
+ background-color: #2a2929; /* Color de fondo */
+}
+
+/* Estilo para el botón */
+#search {
+ font-size: 18px; /* Tamaño de letra */
+ width: 7em;
+ height: 2em;
+}
+
+
diff --git a/webapp/src/index.js b/webapp/src/index.js
index 409a8681..103dc805 100644
--- a/webapp/src/index.js
+++ b/webapp/src/index.js
@@ -32,12 +32,14 @@ i18next.init({
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
+
{/* envolviendo la app con el sistema de traducciones */}
+
);
// If you want to start measuring performance in your app, pass a function
diff --git a/webapp/src/translations/en/global.json b/webapp/src/translations/en/global.json
index d352e7a8..c076edbd 100644
--- a/webapp/src/translations/en/global.json
+++ b/webapp/src/translations/en/global.json
@@ -65,7 +65,9 @@
"gameMenu":{
"history_button":"View Historical Data",
"new_game_button":"Create New Game",
- "title":"Game Menu"
+ "view_ranking":"Ranking",
+ "title":"Game Menu",
+ "back":"Back"
},
"questionView":{
"seconds":"seconds",
@@ -80,6 +82,31 @@
"points":"points",
"no_games_played":"No games played yet"
},
+ "gameConfigurator":{
+ "game_config":"Game configuration",
+ "type_quest":"Type of question: ",
+ "num_quest":"Number of questions: ",
+ "play_custom":"Play Customized Game",
+ "rules_competi":"Play with all kinds of questions and a quantity of 5",
+ "play_competi":"Play competitive Game",
+ "option_all":"All",
+ "option_population":"Population",
+ "option_capital":"Capital",
+ "option_language":"Language",
+ "option_size":"Size",
+ "custo_game":"Create custom game",
+ "competi_game":"Play Competitive",
+ "randomize":"Randomize Parameters"
+ },
+ "ranking":{
+ "ranking":"Ranking",
+ "position":"Position",
+ "username":"Username",
+ "points":"Points",
+ "num_games":"Competitive games",
+ "search":"Search",
+ "enter_username":"Enter Username..."
+ },
"error":{
"error":"Error",
"sorry":"We're sorry, this page does not exist. Don't be angry, I'm just a little cat."
diff --git a/webapp/src/translations/es/global.json b/webapp/src/translations/es/global.json
index 73a4820f..898263d1 100644
--- a/webapp/src/translations/es/global.json
+++ b/webapp/src/translations/es/global.json
@@ -69,7 +69,9 @@
"gameMenu":{
"history_button":"Ver Historial",
"new_game_button":"Crear nuevo juego",
- "title":"Menú del Juego"
+ "view_ranking":"Ranking",
+ "title":"Menú del Juego",
+ "back":"Atrás"
},"questionView":{
"seconds":"segundos",
"question_counter":"Pregunta nº ",
@@ -87,8 +89,32 @@
"error":{
"error":"Error",
"sorry":"Lo sentimos, esta página no existe. No te enfades, solo soy un gatito."
- }
-
+ },
+ "gameConfigurator":{
+ "game_config":"Configuración del Juego",
+ "type_quest":"Tipo de Pregunta : ",
+ "num_quest":"Número de Preguntas : ",
+ "play_custom":"Jugar personalizado",
+ "rules_competi":"Jugar con todo tipo de preguntas siendo estas 5",
+ "play_competi":"Jugar Competitivo",
+ "option_all":"Todas",
+ "option_population":"Población",
+ "option_capital":"Capital",
+ "option_language":"Lenguaje",
+ "option_size":"Extensión",
+ "custo_game":"Crea una partida personalizada",
+ "competi_game":"Juega en modo Competitivo",
+ "randomize":"Randomiza Parámetros"
+ },
+ "ranking":{
+ "ranking":"Ranking",
+ "position":"Posición",
+ "username":"Username",
+ "points":"Puntos",
+ "num_games":"Juegos Competitivos",
+ "search":"Buscar",
+ "enter_username":"Inserta usuario..."
+ }
}
\ No newline at end of file