fix: Language selector keep on the page

This commit is contained in:
Samuel Ortion 2024-02-12 09:29:34 +01:00
parent e4a7479f19
commit 150da0f844
15 changed files with 209 additions and 32 deletions

View File

@ -17,19 +17,18 @@ async function region(req, res) {
}) })
} }
async function quizz(req, res) { async function question(req, res) {
debug('Generating quizz'); debug('Generating question');
const { region } = req.query; const { region } = req.query;
req.session.region = region; req.session.region = region;
// debug(`Coordinates: ${lat}, ${lng}`);
const locale = req.i18n.locale; const locale = req.i18n.locale;
debugLocale("Locale:", locale); debugLocale("Locale:", locale);
quizzController.getQuizzCached(region, locale, QUIZZ_SIZE) quizzController.getQuestionCached(region, locale, QUIZZ_SIZE)
.then(({ species, answer, audio }) => { .then(({ species, answer, audio }) => {
req.session.answer = answer; req.session.answer = answer;
res.json({ species, audio }).send(); res.json({ species, audio }).send();
debug("Quizz sent"); debug("Quizz sent");
quizzController.cacheQuizz(); // Prepare the next question in advance. quizzController.cacheQuestion(); // Prepare the next question in advance.
debug("New quizz cached"); debug("New quizz cached");
}) })
.catch(error => { .catch(error => {
@ -38,6 +37,17 @@ async function quizz(req, res) {
}); });
} }
async function quizz(req, res) {
const { region } = req.query;
req.session.region = region;
const { locale } = req.i18n.locale;
quizzController.generateQuizz(region, locale, 10)
.then(({ questions, answers, propositions }) => {
res.json({questions, answers, propositions }).send();
});
}
function check(req, res) { function check(req, res) {
let answer, correct; let answer, correct;
try { try {
@ -87,8 +97,10 @@ async function birdSpeciesCompletion(req, res) {
res.json(speciesCompletion); res.json(speciesCompletion);
} }
const game = { const game = {
check, check,
question,
quizz, quizz,
birdSpeciesCompletion birdSpeciesCompletion
} }

View File

@ -7,6 +7,14 @@ function getIndex(req, res) {
}); });
} }
function getQuizz(req, res) {
res.render('quizz', {
title: 'SoundBirder Quizz',
csrf_token: req.csrfToken(),
locale: req.i18n.locale
})
}
function getAbout(req, res) { function getAbout(req, res) {
res.render('about', { res.render('about', {
title: req.i18n.__('About SoundBirder'), title: req.i18n.__('About SoundBirder'),
@ -16,5 +24,6 @@ function getAbout(req, res) {
module.exports = { module.exports = {
getIndex, getIndex,
getQuizz,
getAbout getAbout
} }

View File

@ -5,35 +5,34 @@ const cache = require('./cache');
const eBird = require('@unclesamulus/ebird-api')(process.env.EBIRD_API_KEY); const eBird = require('@unclesamulus/ebird-api')(process.env.EBIRD_API_KEY);
const XenoCanto = require('@unclesamulus/xeno-canto-api'); const XenoCanto = require('@unclesamulus/xeno-canto-api');
const { choices, choice } = require('../utils/choices'); const { choices, choice } = require('../utils/choices');
const { getRegion } = require('./region');
function quizzKey(region, locale, size) { function questionKey(region, locale, size) {
return `${region}:${locale}#${size}`; return `${region}:${locale}#${size}`;
} }
async function getQuizzCached(region, locale, size) { async function getQuestionCached(region, locale, size) {
let key = quizzKey(region, locale, size); let key = questionKey(region, locale, size);
let quizz = await cache.pop(key); let quizz = await cache.pop(key);
if (!quizz) { if (!quizz) {
quizz = generateQuizz(region, locale, size); quizz = generateQuestion(region, locale, size);
cache.push(key, quizz); cache.push(key, quizz);
} }
return quizz; return quizz;
} }
async function cacheQuizz(region, locale, size) { async function cacheQuestion(region, locale, size) {
let quizz = await generateQuizz(region, locale, size); let quizz = await generateQuestion(region, locale, size);
let key = quizzKey(region, locale, size); let key = questionKey(region, locale, size);
cache.push(key, quizz); cache.push(key, quizz);
return quizz; return quizz;
} }
async function generateQuizz(region, locale, size) { async function generateQuestion(region, locale, nb_propositions) {
const quizz = {} const quizz = {}
try { try {
let speciesSelection = []; let speciesSelection = [];
do { do {
speciesSelection = await getSpeciesSelection(region, size); speciesSelection = await getSpeciesSelection(region, nb_propositions);
} while (speciesSelection == []); } while (speciesSelection == []);
debugResponses(`Species selection: ${speciesSelection}`); debugResponses(`Species selection: ${speciesSelection}`);
const speciesSelectionLocalized = await getLocalizedNames(speciesSelection, locale); const speciesSelectionLocalized = await getLocalizedNames(speciesSelection, locale);
@ -62,6 +61,25 @@ async function generateQuizz(region, locale, size) {
return quizz; return quizz;
} }
async function generateQuizz(region, locale, nb_questions) {
let questions = [];
let answers = [];
const species_in_region = await getSpeciesList(region);
const species_in_region_localized = await getLocalizedNames(species_in_region, locale);
console.error(species_in_region_localized);
for (let i=0; i < nb_questions; i++) {
try {
let question_species = choice(species_in_region_localize);
let question_audio = await getAudio(question_species.sciName);
questions.push(question_audio);
answers.push(question_species);
} catch (error) {
console.error(error);
}
}
return {questions: questions, answers: answers, propositions: species_in_region_localized};
}
async function getSpeciesSelection(region, number) { async function getSpeciesSelection(region, number) {
const regionCode = region; const regionCode = region;
const speciesList = await getSpeciesList(regionCode); const speciesList = await getSpeciesList(regionCode);
@ -74,7 +92,8 @@ async function getLocalizedNames(speciesCodes, locale) {
const localizedNames = []; const localizedNames = [];
for (const speciesCode of speciesCodes) { for (const speciesCode of speciesCodes) {
const localized = await getLocalizedName(speciesCode, locale); const localized = await getLocalizedName(speciesCode, locale);
localizedNames.push(localized); if (!localized.sciName.includes(" x "))
localizedNames.push(localized);
} }
return localizedNames; return localizedNames;
} }
@ -145,8 +164,9 @@ async function getSpeciesCompletion(region, locale, term) {
} }
module.exports = { module.exports = {
generateQuizz, generateQuestion,
getQuizzCached, getQuestionCached,
getSpeciesCompletion, getSpeciesCompletion,
cacheQuizz cacheQuestion,
generateQuizz
} }

View File

@ -17,5 +17,6 @@
"Set": "Set", "Set": "Set",
"Play the quizz": "Play the quizz", "Play the quizz": "Play the quizz",
"About this game": "About this game", "About this game": "About this game",
"Launch a quizz": "Launch a quizz" "Launch a quizz": "Launch a quizz",
"The project is made with ♥ by Samuel Ortion.": "The project is made with ♥ by Samuel Ortion."
} }

View File

@ -19,5 +19,6 @@
"Play the quizz": "Jouer une partie", "Play the quizz": "Jouer une partie",
"About this game": "À propos de ce jeu", "About this game": "À propos de ce jeu",
"Set language": "Modifier la langue", "Set language": "Modifier la langue",
"Launch a quizz": "Launch a quizz" "Launch a quizz": "Launch a quizz",
"Enter species name": "Enter species name"
} }

View File

@ -1,6 +1,6 @@
{ {
"name": "soundbirder", "name": "soundbirder",
"version": "0.0.0", "version": "0.1.0",
"private": true, "private": true,
"scripts": { "scripts": {
"start": "node ./bin/www", "start": "node ./bin/www",

View File

@ -19,10 +19,15 @@ async function query(endpoint, params) {
return await get(`${API_URL}/${endpoint}`, params); return await get(`${API_URL}/${endpoint}`, params);
} }
function getQuestion(region) {
return query('game/question', { region })
}
function getQuizz(region) { function getQuizz(region) {
return query('game/quizz', { region }) return query('game/quizz', { region })
} }
function getRegion(lat, lon) { function getRegion(lat, lon) {
return query('region', { lat, lon }) return query('region', { lat, lon })
} }
@ -33,13 +38,14 @@ function checkAnswer(species) {
}) })
} }
function getSpeciesCompletion(term) { // function getSpeciesCompletion(term) {
return query('game/species/completion', { // return query('game/species/completion', {
q: term // q: term
}) // })
} // }
const client = { const client = {
getQuestion,
getQuizz, getQuizz,
getRegion, getRegion,
checkAnswer checkAnswer

View File

@ -47,7 +47,7 @@ function quizzStep() {
gameQuizz.classList.remove("none"); gameQuizz.classList.remove("none");
gameLoading.classList.remove("none"); gameLoading.classList.remove("none");
audio.classList.add("none"); audio.classList.add("none");
client.getQuizz(region) client.getQuestion(region)
.then(quizz => { .then(quizz => {
gameLoading.classList.add("none"); gameLoading.classList.add("none");
displayQuizz(quizz); displayQuizz(quizz);

View File

@ -1,9 +1,27 @@
let languageSelectorButton = document.getElementById('language-selector-button'); let languageSelectorButton = document.getElementById('language-selector-button');
let languageSelector = document.getElementById('language-selector'); let languageSelector = document.getElementById('language-selector');
let selecting = false; let selecting = false;
function addReplaceLangCode(url, langCode) {
let a = window.location;
let path = a.pathname.split('/');
path.shift();
if(path[0].length == 2) {
path[0] = langCode;
}else{
path.unshift(langCode);
}
return a.protocol + '//' +
a.host + '/' + path.join('/') +
(a.search != '' ? a.search : '') +
(a.hash != '' ? a.hash : '');
}
function update() { function update() {
let code = languageSelector.value; let langCode = languageSelector.value;
window.location = '/' + code; let url = addReplaceLangCode(window.location.href, langCode);
window.location = url;
} }
languageSelector.addEventListener('click', function() { languageSelector.addEventListener('click', function() {
if (selecting) { if (selecting) {

View File

@ -0,0 +1,32 @@
import client from './api-client.js';
let Quizz = {
questions: [],
answers: [],
propositions: [],
region: 'FR', // TODO: update
questionPointer: 0,
init: function() {
let { questions, answers, propositions } = client.getQuizz(region);
this.questions = questions;
this.answers = answers;
this.propositions = propositions;
this.questionPointer = 0;
},
nextQuestion: function() {
this.questionPointer += 1;
this.render();
},
previousQuestion: function() {
if (this.questionPointer >= 1) {
this.questionPointer -= 1;
}
this.render()
},
render: function() {
}
}

View File

@ -579,6 +579,9 @@ h1 {
.flex-col { .flex-col {
flex-direction: column; flex-direction: column;
} }
.justify-between {
justify-content: space-between;
}
.rounded-\[7px\] { .rounded-\[7px\] {
border-radius: 7px; border-radius: 7px;
} }
@ -647,217 +650,277 @@ h1 {
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
transition-duration: 150ms; transition-duration: 150ms;
} }
nav {
background-color: white;
opacity: 50%;
}
.before\:pointer-events-none::before { .before\:pointer-events-none::before {
content: var(--tw-content); content: var(--tw-content);
pointer-events: none; pointer-events: none;
} }
.before\:mr-1::before { .before\:mr-1::before {
content: var(--tw-content); content: var(--tw-content);
margin-right: 0.25rem; margin-right: 0.25rem;
} }
.before\:mt-\[6\.5px\]::before { .before\:mt-\[6\.5px\]::before {
content: var(--tw-content); content: var(--tw-content);
margin-top: 6.5px; margin-top: 6.5px;
} }
.before\:box-border::before { .before\:box-border::before {
content: var(--tw-content); content: var(--tw-content);
box-sizing: border-box; box-sizing: border-box;
} }
.before\:block::before { .before\:block::before {
content: var(--tw-content); content: var(--tw-content);
display: block; display: block;
} }
.before\:h-1::before { .before\:h-1::before {
content: var(--tw-content); content: var(--tw-content);
height: 0.25rem; height: 0.25rem;
} }
.before\:h-1\.5::before { .before\:h-1\.5::before {
content: var(--tw-content); content: var(--tw-content);
height: 0.375rem; height: 0.375rem;
} }
.before\:w-2::before { .before\:w-2::before {
content: var(--tw-content); content: var(--tw-content);
width: 0.5rem; width: 0.5rem;
} }
.before\:w-2\.5::before { .before\:w-2\.5::before {
content: var(--tw-content); content: var(--tw-content);
width: 0.625rem; width: 0.625rem;
} }
.before\:rounded-tl-md::before { .before\:rounded-tl-md::before {
content: var(--tw-content); content: var(--tw-content);
border-top-left-radius: 0.375rem; border-top-left-radius: 0.375rem;
} }
.before\:border-l::before { .before\:border-l::before {
content: var(--tw-content); content: var(--tw-content);
border-left-width: 1px; border-left-width: 1px;
} }
.before\:border-t::before { .before\:border-t::before {
content: var(--tw-content); content: var(--tw-content);
border-top-width: 1px; border-top-width: 1px;
} }
.before\:transition-all::before { .before\:transition-all::before {
content: var(--tw-content); content: var(--tw-content);
transition-property: all; transition-property: all;
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
transition-duration: 150ms; transition-duration: 150ms;
} }
.after\:pointer-events-none::after { .after\:pointer-events-none::after {
content: var(--tw-content); content: var(--tw-content);
pointer-events: none; pointer-events: none;
} }
.after\:ml-1::after { .after\:ml-1::after {
content: var(--tw-content); content: var(--tw-content);
margin-left: 0.25rem; margin-left: 0.25rem;
} }
.after\:mt-\[6\.5px\]::after { .after\:mt-\[6\.5px\]::after {
content: var(--tw-content); content: var(--tw-content);
margin-top: 6.5px; margin-top: 6.5px;
} }
.after\:box-border::after { .after\:box-border::after {
content: var(--tw-content); content: var(--tw-content);
box-sizing: border-box; box-sizing: border-box;
} }
.after\:block::after { .after\:block::after {
content: var(--tw-content); content: var(--tw-content);
display: block; display: block;
} }
.after\:h-1::after { .after\:h-1::after {
content: var(--tw-content); content: var(--tw-content);
height: 0.25rem; height: 0.25rem;
} }
.after\:h-1\.5::after { .after\:h-1\.5::after {
content: var(--tw-content); content: var(--tw-content);
height: 0.375rem; height: 0.375rem;
} }
.after\:w-2::after { .after\:w-2::after {
content: var(--tw-content); content: var(--tw-content);
width: 0.5rem; width: 0.5rem;
} }
.after\:w-2\.5::after { .after\:w-2\.5::after {
content: var(--tw-content); content: var(--tw-content);
width: 0.625rem; width: 0.625rem;
} }
.after\:flex-grow::after { .after\:flex-grow::after {
content: var(--tw-content); content: var(--tw-content);
flex-grow: 1; flex-grow: 1;
} }
.after\:rounded-tr-md::after { .after\:rounded-tr-md::after {
content: var(--tw-content); content: var(--tw-content);
border-top-right-radius: 0.375rem; border-top-right-radius: 0.375rem;
} }
.after\:border-r::after { .after\:border-r::after {
content: var(--tw-content); content: var(--tw-content);
border-right-width: 1px; border-right-width: 1px;
} }
.after\:border-t::after { .after\:border-t::after {
content: var(--tw-content); content: var(--tw-content);
border-top-width: 1px; border-top-width: 1px;
} }
.after\:transition-all::after { .after\:transition-all::after {
content: var(--tw-content); content: var(--tw-content);
transition-property: all; transition-property: all;
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
transition-duration: 150ms; transition-duration: 150ms;
} }
.placeholder-shown\:border:-moz-placeholder-shown { .placeholder-shown\:border:-moz-placeholder-shown {
border-width: 1px; border-width: 1px;
} }
.placeholder-shown\:border:placeholder-shown { .placeholder-shown\:border:placeholder-shown {
border-width: 1px; border-width: 1px;
} }
.empty\:\!bg-gray-900:empty { .empty\:\!bg-gray-900:empty {
--tw-bg-opacity: 1 !important; --tw-bg-opacity: 1 !important;
background-color: rgb(17 24 39 / var(--tw-bg-opacity)) !important; background-color: rgb(17 24 39 / var(--tw-bg-opacity)) !important;
} }
.focus\:border-2:focus { .focus\:border-2:focus {
border-width: 2px; border-width: 2px;
} }
.focus\:border-gray-900:focus { .focus\:border-gray-900:focus {
--tw-border-opacity: 1; --tw-border-opacity: 1;
border-color: rgb(17 24 39 / var(--tw-border-opacity)); border-color: rgb(17 24 39 / var(--tw-border-opacity));
} }
.focus\:border-t-transparent:focus { .focus\:border-t-transparent:focus {
border-top-color: transparent; border-top-color: transparent;
} }
.focus\:outline-0:focus { .focus\:outline-0:focus {
outline-width: 0px; outline-width: 0px;
} }
.disabled\:border-0:disabled { .disabled\:border-0:disabled {
border-width: 0px; border-width: 0px;
} }
.peer:-moz-placeholder-shown ~ .peer-placeholder-shown\:text-sm { .peer:-moz-placeholder-shown ~ .peer-placeholder-shown\:text-sm {
font-size: 0.875rem; font-size: 0.875rem;
line-height: 1.25rem; line-height: 1.25rem;
} }
.peer:placeholder-shown ~ .peer-placeholder-shown\:text-sm { .peer:placeholder-shown ~ .peer-placeholder-shown\:text-sm {
font-size: 0.875rem; font-size: 0.875rem;
line-height: 1.25rem; line-height: 1.25rem;
} }
.peer:-moz-placeholder-shown ~ .peer-placeholder-shown\:leading-\[3\.75\] { .peer:-moz-placeholder-shown ~ .peer-placeholder-shown\:leading-\[3\.75\] {
line-height: 3.75; line-height: 3.75;
} }
.peer:placeholder-shown ~ .peer-placeholder-shown\:leading-\[3\.75\] { .peer:placeholder-shown ~ .peer-placeholder-shown\:leading-\[3\.75\] {
line-height: 3.75; line-height: 3.75;
} }
.peer:-moz-placeholder-shown ~ .peer-placeholder-shown\:before\:border-transparent::before { .peer:-moz-placeholder-shown ~ .peer-placeholder-shown\:before\:border-transparent::before {
content: var(--tw-content); content: var(--tw-content);
border-color: transparent; border-color: transparent;
} }
.peer:placeholder-shown ~ .peer-placeholder-shown\:before\:border-transparent::before { .peer:placeholder-shown ~ .peer-placeholder-shown\:before\:border-transparent::before {
content: var(--tw-content); content: var(--tw-content);
border-color: transparent; border-color: transparent;
} }
.peer:-moz-placeholder-shown ~ .peer-placeholder-shown\:after\:border-transparent::after { .peer:-moz-placeholder-shown ~ .peer-placeholder-shown\:after\:border-transparent::after {
content: var(--tw-content); content: var(--tw-content);
border-color: transparent; border-color: transparent;
} }
.peer:placeholder-shown ~ .peer-placeholder-shown\:after\:border-transparent::after { .peer:placeholder-shown ~ .peer-placeholder-shown\:after\:border-transparent::after {
content: var(--tw-content); content: var(--tw-content);
border-color: transparent; border-color: transparent;
} }
.peer:focus ~ .peer-focus\:text-\[11px\] { .peer:focus ~ .peer-focus\:text-\[11px\] {
font-size: 11px; font-size: 11px;
} }
.peer:focus ~ .peer-focus\:leading-tight { .peer:focus ~ .peer-focus\:leading-tight {
line-height: 1.25; line-height: 1.25;
} }
.peer:focus ~ .peer-focus\:text-gray-900 { .peer:focus ~ .peer-focus\:text-gray-900 {
--tw-text-opacity: 1; --tw-text-opacity: 1;
color: rgb(17 24 39 / var(--tw-text-opacity)); color: rgb(17 24 39 / var(--tw-text-opacity));
} }
.peer:focus ~ .peer-focus\:before\:border-l-2::before { .peer:focus ~ .peer-focus\:before\:border-l-2::before {
content: var(--tw-content); content: var(--tw-content);
border-left-width: 2px; border-left-width: 2px;
} }
.peer:focus ~ .peer-focus\:before\:border-t-2::before { .peer:focus ~ .peer-focus\:before\:border-t-2::before {
content: var(--tw-content); content: var(--tw-content);
border-top-width: 2px; border-top-width: 2px;
} }
.peer:focus ~ .peer-focus\:before\:border-gray-900::before { .peer:focus ~ .peer-focus\:before\:border-gray-900::before {
content: var(--tw-content); content: var(--tw-content);
--tw-border-opacity: 1; --tw-border-opacity: 1;
border-color: rgb(17 24 39 / var(--tw-border-opacity)); border-color: rgb(17 24 39 / var(--tw-border-opacity));
} }
.peer:focus ~ .peer-focus\:after\:border-r-2::after { .peer:focus ~ .peer-focus\:after\:border-r-2::after {
content: var(--tw-content); content: var(--tw-content);
border-right-width: 2px; border-right-width: 2px;
} }
.peer:focus ~ .peer-focus\:after\:border-t-2::after { .peer:focus ~ .peer-focus\:after\:border-t-2::after {
content: var(--tw-content); content: var(--tw-content);
border-top-width: 2px; border-top-width: 2px;
} }
.peer:focus ~ .peer-focus\:after\:border-gray-900::after { .peer:focus ~ .peer-focus\:after\:border-gray-900::after {
content: var(--tw-content); content: var(--tw-content);
--tw-border-opacity: 1; --tw-border-opacity: 1;
border-color: rgb(17 24 39 / var(--tw-border-opacity)); border-color: rgb(17 24 39 / var(--tw-border-opacity));
} }
.peer:disabled ~ .peer-disabled\:text-transparent { .peer:disabled ~ .peer-disabled\:text-transparent {
color: transparent; color: transparent;
} }
.peer:disabled ~ .peer-disabled\:before\:border-transparent::before { .peer:disabled ~ .peer-disabled\:before\:border-transparent::before {
content: var(--tw-content); content: var(--tw-content);
border-color: transparent; border-color: transparent;
} }
.peer:disabled ~ .peer-disabled\:after\:border-transparent::after { .peer:disabled ~ .peer-disabled\:after\:border-transparent::after {
content: var(--tw-content); content: var(--tw-content);
border-color: transparent; border-color: transparent;

View File

@ -3,6 +3,9 @@ const router = express.Router();
const apiController = require('../controllers/api.js'); const apiController = require('../controllers/api.js');
router.route('/game/question')
.get(apiController.game.question);
router.route('/game/quizz') router.route('/game/quizz')
.get(apiController.game.quizz); .get(apiController.game.quizz);

View File

@ -5,6 +5,9 @@ const indexController = require('../controllers/index');
router.route('/') router.route('/')
.get(indexController.getIndex); .get(indexController.getIndex);
router.route('/quizz')
.get(indexController.getQuizz);
router.route('/about') router.route('/about')
.get(indexController.getAbout); .get(indexController.getAbout);

View File

@ -1,5 +1,5 @@
doctype html doctype html
html html(lang=locale)
head head
title= title title= title
meta(name="viewport" content="width=device-width, initial-scale=1.0") meta(name="viewport" content="width=device-width, initial-scale=1.0")
@ -10,7 +10,7 @@ html
body body
.flex.flex-col.min-h-screen .flex.flex-col.min-h-screen
.flex-1.p-5 .flex-1.p-5
.menu .menu.justify-between
nav(role="navigation").flex.flex-row nav(role="navigation").flex.flex-row
- var i18n_prefix = locale ? '/' + locale : '' - var i18n_prefix = locale ? '/' + locale : ''
span.nav-item span.nav-item

9
views/quizz.pug Normal file
View File

@ -0,0 +1,9 @@
extends layout
block content
.game
.quizz-question
audio(controls)
input(type="text", text="", placeholder=__('Enter species name')).p-1
script(src="/dist/axios/axios.min.js")
script(src="/javascripts/game.js" type="module")