Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Accessibility #125

Merged
merged 15 commits into from
Apr 27, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions webapp/src/components/GameConfigurator/GameConfigurator.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ function GameConfigurator(){
<h1>{t("gameConfigurator.game_config")}</h1>
<h2>{t("gameConfigurator.custo_game")}</h2>
<ButtonRandomizeCustom t={t} handleClick={handleClickRandomize} />
<label for="select">{t("gameConfigurator.type_quest")}</label>
<label htmlFor="select">{t("gameConfigurator.type_quest")}</label>
<select id="select" className="select-style" value={tipoPregunta} onChange={(e) => setTipoPregunta(e.target.value)}>
<option value="ALL">{t("gameConfigurator.option_all")}</option>
<option value="POPULATION">{t("gameConfigurator.option_population")}</option>
Expand All @@ -51,7 +51,7 @@ function GameConfigurator(){
<br></br>
<ButtonCustomized t={t} handleClick={handleClick}/>
<br></br>
<hr class="hr-style"></hr>
<hr className="hr-style"></hr>
<br></br>
<h2>{t("gameConfigurator.competi_game")}</h2>
<p>{t("gameConfigurator.rules_competi")}</p>
Expand Down
18 changes: 17 additions & 1 deletion webapp/src/components/Instructions.js
Original file line number Diff line number Diff line change
Expand Up @@ -50,14 +50,30 @@ function Instructions() {
{t("instructions.time_limit_p1")}
</li></ul>
</article>
<article>
<ul className='ins_ul'><p>{t("instructions.voice")}</p>
<li>
{t("instructions.voice_p1")}
</li>
<li>
{t("instructions.voice_p2")}
</li>
<li>
{t("instructions.voice_p3")}
</li>
<li>
{t("instructions.voice_p4")}
</li>
</ul>
</article>
<article>
<ul className='ins_ul'><p>{t("instructions.have_fun")}</p>
<li>
{t("instructions.have_fun_p1")}
</li>
</ul>
</article>

</section>

</div>
Expand Down
37 changes: 21 additions & 16 deletions webapp/src/components/fragments/NavBar.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import React, { useState } from 'react';
import { Link } from 'react-router-dom';
import { Link, useLocation } from 'react-router-dom';
import MenuItem from '@mui/material/MenuItem';
import Menu from '@mui/material/Menu';
import "../../custom.css";
Expand All @@ -10,6 +10,7 @@ import Cookies from 'js-cookie';

function Navbar() {
const navigate = useNavigate();
const location = useLocation();
const [t, i18n] = useTranslation("global");
const [anchorLanguage, setAnchorLanguage] = useState(null);
const [anchorUser, setAnchorUser] = useState(null);
Expand Down Expand Up @@ -41,16 +42,21 @@ function Navbar() {
handleLanguageMenuClose();
};

const isQuestionsPage = location.pathname === '/questions';

return (
<div className="navbar-container">
<div className='left-nav'>
<Profile />
<Link to="/home" className="home-button">
<h1 className='navbar-text'>{t("navBar.title")}</h1>
</Link>
<Profile />
<Link to="/home" className="home-button">
<h1 className='navbar-text'>{t("navBar.title")}</h1>
</Link>
</div>
<div className='right-nav'>
<button className="language-button" onClick={handleLanguageMenuOpen}>{t("navBar.language")}</button>
{/* If /questions, disable button*/}
{!isQuestionsPage && (
<button className="language-button" onClick={handleLanguageMenuOpen}>{t("navBar.language")}</button>
)}
<Menu
anchorEl={anchorLanguage}
open={Boolean(anchorLanguage)}
Expand All @@ -65,18 +71,17 @@ function Navbar() {

{Cookies.get('user') ? (
<>
<button className="user-button" onClick={handleUserMenuOpen}>{ JSON.parse(Cookies.get('user')).username}</button>
<Menu
anchorEl={anchorUser}
open={Boolean(anchorUser)}
onClose={handleUserMenuClose}
disableAutoFocusItem
>
<MenuItem id="logout" onClick={() => removeCookie()}> {t("navBar.logout")}</MenuItem>
</Menu>
<button className="user-button" onClick={handleUserMenuOpen}>{ JSON.parse(Cookies.get('user')).username}</button>
<Menu
anchorEl={anchorUser}
open={Boolean(anchorUser)}
onClose={handleUserMenuClose}
disableAutoFocusItem
>
<MenuItem id="logout" onClick={() => removeCookie()}> {t("navBar.logout")}</MenuItem>
</Menu>
</>
) : null}

</div>
</div>
);
Expand Down
48 changes: 24 additions & 24 deletions webapp/src/components/questionView/QuestionGenerator.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,33 +9,33 @@ class QuestionGenerator{
}

async generateQuestions(lang, type, amount, token) {
/*
try {
//const response = await fetch(this.apiUrl);
//const receivedQuestions = await response.json();

// 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":"¿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"]}}')
// //Mockup
// console.log("type: "+type+" amount: "+amount)
// const receivedQuestions = JSON.parse('{"0":{"question":"What is the population of 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 {
let response;
Expand Down
107 changes: 71 additions & 36 deletions webapp/src/components/questionView/QuestionView.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import QuestionGenerator from './QuestionGenerator';
import CreationHistoricalRecord from './CreationHistoricalRecord';
import { useState } from 'react';
import "../../custom.css";
import React from "react";
import React, { useState, useEffect, useCallback } from "react";
import Countdown from 'react-countdown';
import {useTranslation} from "react-i18next";
import $ from 'jquery';
Expand All @@ -21,8 +20,7 @@ function QuestionView({type= "COMPETITIVE", amount=5}){
const [questions, setQuestions] = useState(null);
const[t, i18n] = useTranslation("global");
const cookie = JSON.parse(Cookies.get('user')??JSON.stringify({username : playAsGuestUsername, token : playAsGuestToken}))
const [audio] = useState(new Audio('/tictac.mp3'));



const generateQuestions = async (numQuestion) => {
if (numQuestion < 0) {
Expand Down Expand Up @@ -56,11 +54,9 @@ function QuestionView({type= "COMPETITIVE", amount=5}){
});
}
if(answerGiven===correctAnswer){
audio.pause();
audioCorrect.play(); // Reproduce el sonido de respuesta incorrecta
}
else{
audio.pause();
audioIncorrect.play(); // Reproduce el sonido de respuesta correcta
}
$(this).css('pointer-events', 'none');
Expand All @@ -80,15 +76,17 @@ function QuestionView({type= "COMPETITIVE", amount=5}){
function computePointsForQuestion(correctAnswer, answerGiven){
if(answerGiven===correctAnswer){
points+=100;
audio.pause();
}else if(points-50>=0){
points-=50;
audio.pause();
}else{
points = 0;
}
}
function handleClick(text){
// Detener el síntesis de voz
if(window.speechSynthesis.speaking)
window.speechSynthesis.cancel();

//create the record to record the response
creationHistoricalRecord.addQuestion(questions[numQuestion].getQuestion(),
questions[numQuestion].getAnswers(),
Expand All @@ -108,7 +106,6 @@ function QuestionView({type= "COMPETITIVE", amount=5}){

//Last question sends the record
if(!(numQuestion < questions.length - 1)){
audio.pause();
creationHistoricalRecord.setCompetitive(type === 'COMPETITIVE');
creationHistoricalRecord.setDate(Date.now());
creationHistoricalRecord.setPoints(points);
Expand All @@ -118,41 +115,28 @@ function QuestionView({type= "COMPETITIVE", amount=5}){
}, 1000);

}

if(questions === null)
generateQuestions(numQuestion)


return (
<div className="question-view-container">
{numQuestion >= 0 ?
<QuestionComponent t={t} questions={questions} numQuestion={numQuestion} handleClick={handleClick} points={points} audio = {audio} language={i18n.language}/> :
<QuestionComponent t={t} questions={questions} numQuestion={numQuestion} handleClick={handleClick} points={points} language={i18n.language}/> :
<h1>{t("questionView.no_questions_message")}</h1> }
</div>);
}

function QuestionComponent({questions, numQuestion, handleClick, t, points, audio, language}){
function QuestionComponent({questions, numQuestion, handleClick, t, points, language}){


const speakQuestion = () => {
const speech = new SpeechSynthesisUtterance();
speech.lang = language;
console.log(language);
getVoicesForLanguage(language)
.then(voices => {
// const voice = voices.find(voice => voice.lang === language);
// speech.voice = voice || voices[0]; // If there is no voice for the lang, choose the first one
window.speechSynthesis.speak(speech);
})
.catch(error => {
console.error("Error al obtener las voces para el idioma:", error);
});
};

// Función para obtener las voces disponibles para un idioma
const getVoicesForLanguage = (language) => {
// To obtain available voices for language
const getVoicesForLanguage = useCallback((language) => {
return new Promise((resolve, reject) => {
const speech = new SpeechSynthesisUtterance();

//speaks the question
speech.text = questions[numQuestion].getQuestion();
speech.lang = language;

Expand All @@ -166,19 +150,70 @@ function QuestionComponent({questions, numQuestion, handleClick, t, points, audi

window.speechSynthesis.speak(speech);
});
};
},[questions, numQuestion]);

const speakAnswers = useCallback((answers) => {
const speech = new SpeechSynthesisUtterance();
speech.lang = language;
let concatenatedAnswers = Array.isArray(answers) ? answers.map((answer, index) => `${index + 1}. ${answer}`).join(". ") : '';

getVoicesForLanguage(language)
.then(voices => {
// const voice = voices.find(voice => voice.lang === language);
// speech.voice = voice || voices[0]; // If there is no voice for the lang, choose the first one
speech.text = concatenatedAnswers;
window.speechSynthesis.speak(speech);
})
.catch(error => {
console.error("Error al obtener las voces para el idioma:", error);
});
}, [getVoicesForLanguage, language]);

const speak = useCallback(() => {
speakAnswers(questions[numQuestion].getAnswers());
}, [numQuestion, questions, speakAnswers]);

useEffect(() => {
const handleKeyPress = (event) => {
if (event.key === 's') {
speak();
} else {
const answerIndex = parseInt(event.key) - 1;
if (!isNaN(answerIndex) && answerIndex >= 0 && answerIndex < questions[numQuestion].getAnswers().length) {

handleClick(questions[numQuestion].getAnswers()[answerIndex]);
}
}
};

window.addEventListener("keypress", handleKeyPress);

return () => {
window.removeEventListener("keypress", handleKeyPress);
};
}, [speak, numQuestion, questions, handleClick]);

//To stop the voice when changing of page
useEffect(() => {
const handleBeforeUnload = () => {
if(window.speechSynthesis.speaking)
window.speechSynthesis.cancel();
};

window.addEventListener("beforeunload", handleBeforeUnload);

return () => {
window.removeEventListener("beforeunload", handleBeforeUnload);
};
}, []);



const renderer = ({seconds, completed }) => {
if (completed) {
audio.pause();
return <span>{t("questionView.end_countdown")}</span>; // Rendered when countdown completes
} else {
if (audio.paused) {
audio.loop = true; // Loop of tiktak
audio.play();
}

return <span>{seconds} {t("questionView.seconds")}</span>; // Render countdown
}
};
Expand All @@ -189,7 +224,7 @@ function QuestionComponent({questions, numQuestion, handleClick, t, points, audi
<div className='questionContainer'>

<div className='topPanel'>
<h2>{questions[numQuestion].getQuestion()} <button className="altavoz" onClick={speakQuestion}>🔊</button></h2>
<h2>{questions[numQuestion].getQuestion()} <button className="altavoz" onClick={speak}>🔊</button></h2>
<div className="countdown">
<Countdown key={numQuestion} date={Date.now()+10000} renderer={renderer} onComplete={handleClick.bind(this,"no-answer")} />
</div>
Expand Down
5 changes: 3 additions & 2 deletions webapp/src/components/questionView/QuestionView.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,7 @@ describe('Question View component', () => {
expect(correctAnswerButton).toHaveStyle('background-color: #6EF26E');
}, { timeout: 1000 }); // Esperar 1 segundo
});

it('shows colors to reveal false answer and it sounds', async () => {
setupAudioMock()
await act(async () =>{
Expand All @@ -130,14 +131,14 @@ describe('Question View component', () => {
}, { timeout: 1000 }); // Esperar 1 segundo
});

it('shows timer and tiktak sound', async () => {
it('shows timer', async () => {
setupAudioMock()
await act(async () =>{
await render(<MemoryRouter><QuestionView /></MemoryRouter>);

})
await waitFor(() => expect(screen.getByText('What is the population of Oviedo?')).toBeInTheDocument());
expect(global.Audio).toHaveBeenCalledWith('/tictac.mp3');
// expect(global.Audio).toHaveBeenCalledWith('/tictac.mp3');

const timerElement = screen.getByText(new RegExp(`(\\d+) ${i18en.t('questionView.seconds')}`));
expect(timerElement).toBeInTheDocument(); // Verificar que el temporizador esté presente en el DOM
Expand Down
Loading