diff --git a/docs/images/QualityTree.png b/docs/images/QualityTree.png index 6266b73f..e562dda5 100644 Binary files a/docs/images/QualityTree.png and b/docs/images/QualityTree.png differ diff --git a/docs/src/10_quality_requirements.adoc b/docs/src/10_quality_requirements.adoc index 13134a57..f48e10c7 100644 --- a/docs/src/10_quality_requirements.adoc +++ b/docs/src/10_quality_requirements.adoc @@ -96,7 +96,7 @@ occurs. |The user must correctly answer questions on different topics. This will improve the user experience and maintain the interest of the participants. |High -|Integrity +|Fiability |The game must be played without errors. |The answer determined as correct for each question by the system shall be the one that is actually correct. |Medium diff --git a/questionsservice/questiongeneratorservice/package.json b/questionsservice/questiongeneratorservice/package.json index 026b5970..4611c730 100644 --- a/questionsservice/questiongeneratorservice/package.json +++ b/questionsservice/questiongeneratorservice/package.json @@ -19,6 +19,7 @@ }, "homepage": "https://github.com/arquisoft/wiq_es6c#readme", "dependencies": { + "cors": "^2.8.5", "express": "^4.18.2", "axios": "^1.6.5", "cors": "^2.8.5", diff --git a/webapp/package-lock.json b/webapp/package-lock.json index bcc358d5..26ab6de7 100644 --- a/webapp/package-lock.json +++ b/webapp/package-lock.json @@ -15,8 +15,11 @@ "@testing-library/react": "^14.1.2", "@testing-library/user-event": "^14.5.2", "axios": "^1.6.5", + "fetch": "^1.1.0", "react": "^18.2.0", + "react-circular-progressbar": "^2.1.0", "react-dom": "^18.2.0", + "react-router-dom": "^6.22.1", "react-scripts": "5.0.1", "web-vitals": "^3.5.1" }, @@ -5026,6 +5029,14 @@ "node": ">=12" } }, + "node_modules/@remix-run/router": { + "version": "1.15.1", + "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.15.1.tgz", + "integrity": "sha512-zcU0gM3z+3iqj8UX45AmWY810l3oUmXM7uH4dt5xtzvMhRtYVhKGOmgOd1877dOPPepfCjUv57w+syamWIYe7w==", + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/@rollup/plugin-babel": { "version": "5.3.1", "resolved": "https://registry.npmjs.org/@rollup/plugin-babel/-/plugin-babel-5.3.1.tgz", @@ -7423,6 +7434,17 @@ "node": ">=8" } }, + "node_modules/biskviit": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/biskviit/-/biskviit-1.0.1.tgz", + "integrity": "sha512-VGCXdHbdbpEkFgtjkeoBN8vRlbj1ZRX2/mxhE8asCCRalUx2nBzOomLJv8Aw/nRt5+ccDb+tPKidg4XxcfGW4w==", + "dependencies": { + "psl": "^1.1.7" + }, + "engines": { + "node": ">=1.0.0" + } + }, "node_modules/bluebird": { "version": "3.7.2", "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz", @@ -9602,6 +9624,25 @@ "node": ">= 0.8" } }, + "node_modules/encoding": { + "version": "0.1.12", + "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.12.tgz", + "integrity": "sha512-bl1LAgiQc4ZWr++pNYUdRe/alecaHFeHxIJ/pNciqGdKXghaTCOwKkbKp6ye7pKZGu/GcaSXFk8PBVhgs+dJdA==", + "dependencies": { + "iconv-lite": "~0.4.13" + } + }, + "node_modules/encoding/node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/end-of-stream": { "version": "1.4.4", "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", @@ -11034,6 +11075,15 @@ "pend": "~1.2.0" } }, + "node_modules/fetch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fetch/-/fetch-1.1.0.tgz", + "integrity": "sha512-5O8TwrGzoNblBG/jtK4NFuZwNCkZX6s5GfRNOaGtm+QGJEuNakSC/i2RW0R93KX6E0jVjNXm6O3CRN4Ql3K+yA==", + "dependencies": { + "biskviit": "1.0.1", + "encoding": "0.1.12" + } + }, "node_modules/file-entry-cache": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", @@ -21883,6 +21933,14 @@ "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz", "integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==" }, + "node_modules/react-circular-progressbar": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/react-circular-progressbar/-/react-circular-progressbar-2.1.0.tgz", + "integrity": "sha512-xp4THTrod4aLpGy68FX/k1Q3nzrfHUjUe5v6FsdwXBl3YVMwgeXYQKDrku7n/D6qsJA9CuunarAboC2xCiKs1g==", + "peerDependencies": { + "react": "^0.14.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0" + } + }, "node_modules/react-dev-utils": { "version": "12.0.1", "resolved": "https://registry.npmjs.org/react-dev-utils/-/react-dev-utils-12.0.1.tgz", @@ -22030,6 +22088,36 @@ "node": ">=0.10.0" } }, + "node_modules/react-router": { + "version": "6.22.1", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.22.1.tgz", + "integrity": "sha512-0pdoRGwLtemnJqn1K0XHUbnKiX0S4X8CgvVVmHGOWmofESj31msHo/1YiqcJWK7Wxfq2a4uvvtS01KAQyWK/CQ==", + "dependencies": { + "@remix-run/router": "1.15.1" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "react": ">=16.8" + } + }, + "node_modules/react-router-dom": { + "version": "6.22.1", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.22.1.tgz", + "integrity": "sha512-iwMyyyrbL7zkKY7MRjOVRy+TMnS/OPusaFVxM2P11x9dzSzGmLsebkCvYirGq0DWB9K9hOspHYYtDz33gE5Duw==", + "dependencies": { + "@remix-run/router": "1.15.1", + "react-router": "6.22.1" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "react": ">=16.8", + "react-dom": ">=16.8" + } + }, "node_modules/react-scripts": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/react-scripts/-/react-scripts-5.0.1.tgz", diff --git a/webapp/package.json b/webapp/package.json index 6e59b09b..bf7583ba 100644 --- a/webapp/package.json +++ b/webapp/package.json @@ -10,8 +10,11 @@ "@testing-library/react": "^14.1.2", "@testing-library/user-event": "^14.5.2", "axios": "^1.6.5", + "fetch": "^1.1.0", "react": "^18.2.0", "react-dom": "^18.2.0", + "react-router-dom": "^6.22.1", + "react-circular-progressbar": "^2.1.0", "react-scripts": "5.0.1", "web-vitals": "^3.5.1" }, diff --git a/webapp/src/App.js b/webapp/src/App.js index 910935ab..06c2736a 100644 --- a/webapp/src/App.js +++ b/webapp/src/App.js @@ -17,7 +17,7 @@ function App() { - Welcome to wiq_0 + Welcome to wiq_06c {showLogin ? : } diff --git a/webapp/src/App.test.js b/webapp/src/App.test.js index 9aa27757..7235d3a7 100644 --- a/webapp/src/App.test.js +++ b/webapp/src/App.test.js @@ -1,8 +1,17 @@ import { render, screen } from '@testing-library/react'; import App from './App'; +import { ContextFun } from './components/Context'; +import { BrowserRouter as Router } from 'react-router-dom'; + test('renders learn react link', () => { - render(); + render( + + + + + + ); const linkElement = screen.getByText(/Welcome to wiq_0/i); expect(linkElement).toBeInTheDocument(); }); diff --git a/webapp/src/components/Context.js b/webapp/src/components/Context.js new file mode 100644 index 00000000..8a23041c --- /dev/null +++ b/webapp/src/components/Context.js @@ -0,0 +1,17 @@ +import React, { createContext, useContext, useState } from 'react'; + +const Context = createContext(); + +export function ContextFun({ children }) { + const [usernameGlobal, setUsernameGlobal] = useState(''); + + return ( + + {children} + + ); +} + +export function useUser() { + return useContext(Context); +} \ No newline at end of file diff --git a/webapp/src/components/FirstGame.css b/webapp/src/components/FirstGame.css new file mode 100644 index 00000000..0f620ae0 --- /dev/null +++ b/webapp/src/components/FirstGame.css @@ -0,0 +1,41 @@ +button { + font-size: 20px; + width: 200px; + height: 50px; + justify-content: center; + margin-bottom: 15px; +} + +.questionStructure .answers { + display: flex; + justify-content: center; /* Alinea los elementos en el centro horizontal / + align-items: center; / Alinea los elementos en el centro vertical / + height: 100vh; / Ajusta la altura al 100% del viewport */ +} + +.allAnswers { + width:100%; + display: block; +} + +.asnwers { + width:100%; +} +.progressBar { + height: 100%; + margin-top: 10; + width:10%; + display: inline-block; +} + +.question { + height: 100%; + width: 90%; + display: inline-block; +} + +.questionText { + display: flex; + margin-bottom: 25px; + +} \ No newline at end of file diff --git a/webapp/src/components/FirstGame.js b/webapp/src/components/FirstGame.js new file mode 100644 index 00000000..e11e9589 --- /dev/null +++ b/webapp/src/components/FirstGame.js @@ -0,0 +1,130 @@ +import React, { useState, useEffect } from 'react'; +import { Container, Typography } from '@mui/material'; +import './FirstGame.css'; +import { CircularProgressbar } from 'react-circular-progressbar'; +import 'react-circular-progressbar/dist/styles.css'; +import axios from 'axios'; +import { json } from 'react-router-dom'; +import { useLocation } from 'react-router-dom'; + +const apiEndpoint = 'http://localhost:8007'; +const Quiz = () => { + var questions = useLocation().state.questions; + + const [currentQuestionIndex, setCurrentQuestionIndex] = React.useState(0); + const [selectedOption, setSelectedOption] = React.useState(null); + const [isCorrect, setIsCorrect] = React.useState(null); + + const esperar = (ms) => { + return new Promise(resolve => setTimeout(resolve, ms)); + }; + + function secureRandomNumber(max) { + const randomBytes = new Uint32Array(1); + window.crypto.getRandomValues(randomBytes); + return randomBytes[0] % max; + } + + function shuffleArray(array) { + // Crea una copia del array original + const shuffledArray = [...array]; + + // Recorre el array desde el último elemento hasta el primero + for (let i = shuffledArray.length - 1; i > 0; i--) { + // Genera un índice aleatorio entre 0 y el índice actual + //const randomIndex = Math.floor(Math.random() * (i + 1)); + const randomIndex = secureRandomNumber(i + 1); + + // Intercambia el elemento actual con el elemento del índice aleatorio + const temp = shuffledArray[i]; + shuffledArray[i] = shuffledArray[randomIndex]; + shuffledArray[randomIndex] = temp; + } + + // Devuelve el array barajado + return shuffledArray; + } + + + const getQuestions = async () => { + try { + const response = await axios.get(`${apiEndpoint}/questions?n_preguntas=${1}`); + console.log(response.data.length) + for (var i = 0; i < response.data.length; i++) { + var possibleAnswers = [response.data[i].respuesta_correcta, response.data[i].respuestas_incorrectas[0], response.data[i].respuestas_incorrectas[1], response.data[i].respuestas_incorrectas[2]] + possibleAnswers = shuffleArray(possibleAnswers) + questions.push({ + question: response.data[i].pregunta, + options: possibleAnswers, + correctAnswer: response.data[i].respuesta_correcta + }) + } + } catch (error) { + console.error(error); + } + console.log(questions) +}; + + const checkAnswer = async (option) => { + getQuestions() + setIsCorrect(option === questions[currentQuestionIndex].correctAnswer); + setSelectedOption(option); + + const botonIncorrecta = document.getElementById('option-' + questions[currentQuestionIndex].options.indexOf(option)) + if (!isCorrect) { + botonIncorrecta.style.backgroundColor = 'red' + } + + const numberAnswer = questions[currentQuestionIndex].options.indexOf(questions[currentQuestionIndex].correctAnswer) + const botonCorrecta = document.getElementById('option-' + numberAnswer) + botonCorrecta.style.backgroundColor = 'green' + // Pasar a la siguiente pregunta después de responder + + await esperar(2000); // Espera 2000 milisegundos (2 segundos) + botonIncorrecta.style.backgroundColor = 'lightgrey' + botonCorrecta.style.backgroundColor = 'lightgrey' + if (questions.length-1 !== currentQuestionIndex) { + setCurrentQuestionIndex((prevIndex) => prevIndex + 1); + } + setIsCorrect(false) + + + }; + + return ( + +
+
+ + {questions[currentQuestionIndex].question} + +
+ +
+ {/* {MiCircularProgressbar} */} +
+
+ {questions[currentQuestionIndex].options.map((option, index) => ( +
+ +
+ ) + )} +
+
+ {/* {isCorrect !== null && ( +

{isCorrect ? '¡Respuesta correcta!' : 'Respuesta incorrecta.'}

+ )} */} +
+ ); +}; + +export default Quiz; diff --git a/webapp/src/components/Login.js b/webapp/src/components/Login.js index 0ad6268e..8937360d 100644 --- a/webapp/src/components/Login.js +++ b/webapp/src/components/Login.js @@ -2,6 +2,7 @@ import React, { useState } from 'react'; import axios from 'axios'; import { Container, Typography, TextField, Button, Snackbar } from '@mui/material'; +import { useNavigate } from 'react-router-dom'; // Importa useHistory const Login = () => { const [username, setUsername] = useState(''); @@ -13,6 +14,8 @@ const Login = () => { const apiEndpoint = process.env.REACT_APP_API_ENDPOINT || 'http://localhost:8000'; + const navigation = useNavigate(); // Añade esto + const loginUser = async () => { try { const response = await axios.post(`${apiEndpoint}/login`, { username, password }); @@ -24,6 +27,8 @@ const Login = () => { setLoginSuccess(true); setOpenSnackbar(true); + navigation("/menu") + } catch (error) { setError(error.response.data.error); } @@ -33,6 +38,7 @@ const Login = () => { setOpenSnackbar(false); }; + return ( {loginSuccess ? ( diff --git a/webapp/src/components/Login.test.js b/webapp/src/components/Login.test.js index af102dcf..3149be03 100644 --- a/webapp/src/components/Login.test.js +++ b/webapp/src/components/Login.test.js @@ -3,6 +3,10 @@ import { render, fireEvent, screen, waitFor, act } from '@testing-library/react' import axios from 'axios'; import MockAdapter from 'axios-mock-adapter'; import Login from './Login'; +import { ContextFun } from './Context'; +import { BrowserRouter as Router } from 'react-router-dom'; + + const mockAxios = new MockAdapter(axios); @@ -12,7 +16,12 @@ describe('Login component', () => { }); it('should log in successfully', async () => { - render(); + render( + + + + + ); const usernameInput = screen.getByLabelText(/Username/i); const passwordInput = screen.getByLabelText(/Password/i); @@ -23,10 +32,10 @@ describe('Login component', () => { // Simulate user input await act(async () => { - fireEvent.change(usernameInput, { target: { value: 'testUser' } }); - fireEvent.change(passwordInput, { target: { value: 'testPassword' } }); - fireEvent.click(loginButton); - }); + fireEvent.change(usernameInput, { target: { value: 'testUser' } }); + fireEvent.change(passwordInput, { target: { value: 'testPassword' } }); + fireEvent.click(loginButton); + }); // Verify that the user information is displayed expect(screen.getByText(/Hello testUser!/i)).toBeInTheDocument(); @@ -34,7 +43,14 @@ describe('Login component', () => { }); it('should handle error when logging in', async () => { - render(); + + render( + + + + + + ); const usernameInput = screen.getByLabelText(/Username/i); const passwordInput = screen.getByLabelText(/Password/i); diff --git a/webapp/src/components/Menu.js b/webapp/src/components/Menu.js new file mode 100644 index 00000000..990e84de --- /dev/null +++ b/webapp/src/components/Menu.js @@ -0,0 +1,106 @@ +import React, { useState, useEffect } from 'react'; +import { Container, Typography } from '@mui/material'; +import './FirstGame.css'; +import 'react-circular-progressbar/dist/styles.css'; +import axios from 'axios'; +import { json } from 'react-router-dom'; +import { useNavigate } from 'react-router-dom'; // Importa useHistory + +const apiEndpoint = 'http://localhost:8007'; + +var jsonApi = '' + +var isApiCalledRef = false; + + +const DatosContext = React.createContext(); + +var questions = [] + +function secureRandomNumber(max) { + const randomBytes = new Uint32Array(1); + window.crypto.getRandomValues(randomBytes); + return randomBytes[0] % max; +} + +function shuffleArray(array) { + // Crea una copia del array original + const shuffledArray = [...array]; + + // Recorre el array desde el último elemento hasta el primero + for (let i = shuffledArray.length - 1; i > 0; i--) { + // Genera un índice aleatorio entre 0 y el índice actual + //const randomIndex = Math.floor(Math.random() * (i + 1)); + const randomIndex = secureRandomNumber(i + 1); + + // Intercambia el elemento actual con el elemento del índice aleatorio + const temp = shuffledArray[i]; + shuffledArray[i] = shuffledArray[randomIndex]; + shuffledArray[randomIndex] = temp; + } + + // Devuelve el array barajado + return shuffledArray; +} + +// useEffect (() => { +// if (!isApiCalledRef) { +// getQuestions(); +// isApiCalledRef = true; +// } +// }, []); + + + + +const Menu = () => { + + const [n_preguntas, setn_preguntas] = useState(5); + + const navigation = useNavigate(); // Añade esto + + const initiateGame = async () => { + if (!isApiCalledRef) { + await getQuestions() + } + isApiCalledRef = true + navigation("/firstGame", {state: {questions}}) + } + + const getQuestions = async () => { + try { + setn_preguntas(5) + const response = await axios.get(`${apiEndpoint}/questions?n_preguntas=${n_preguntas}`); + console.log(response.data.length) + for (var i = 0; i < response.data.length; i++) { + var possibleAnswers = [response.data[i].respuesta_correcta, response.data[i].respuestas_incorrectas[0], response.data[i].respuestas_incorrectas[1], response.data[i].respuestas_incorrectas[2]] + possibleAnswers = shuffleArray(possibleAnswers) + questions.push({ + question: response.data[i].pregunta, + options: possibleAnswers, + correctAnswer: response.data[i].respuesta_correcta + }) + } + } catch (error) { + console.error(error); + } + console.log(questions) + }; + + return ( + +

Bienvenido a wiq_06c por favor seleccione un modo de juego para comenzar partida:

+ +
+ ); + +} + + +export default Menu; diff --git a/webapp/src/index.js b/webapp/src/index.js index d563c0fb..bf6814c8 100644 --- a/webapp/src/index.js +++ b/webapp/src/index.js @@ -4,10 +4,32 @@ import './index.css'; import App from './App'; import reportWebVitals from './reportWebVitals'; +import { + createBrowserRouter, + RouterProvider, + Route, + Routes, + useNavigate, + MemoryRouter + as Router +} from "react-router-dom"; + +import FirstGame from './components/FirstGame'; +import Menu from './components/Menu'; +import Login from './components/Login' + + const root = ReactDOM.createRoot(document.getElementById('root')); root.render( - + + + }> + }> + }> + }> + + ); @@ -15,3 +37,4 @@ root.render( // to log results (for example: reportWebVitals(console.log)) // or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals reportWebVitals(); +