game&api: Added API queries for geolocalized species and audio retrieval

This commit is contained in:
Samuel Ortion 2022-08-28 09:14:02 +02:00
parent f1f87afbae
commit 585e578e48
28 changed files with 913 additions and 133 deletions

View File

@ -1,15 +0,0 @@
# EditorConfig is awesome: https://EditorConfig.org
# top-most EditorConfig file
root = true
[*]
indent_style = space
indent_size = 4
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = false
insert_final_newline = false
[pug]
indent_size = 2

7
.gitignore vendored
View File

@ -54,7 +54,7 @@ typings/
# Yarn Integrity file
.yarn-integrity
# dotenv environment variables file
# dotenv environment constiables file
.env
# next.js build output
@ -62,4 +62,7 @@ typings/
# IDE junks
.vscode
.ideas
.ideas
data/*
!/data/.gitkeep

View File

@ -1,3 +1,31 @@
# soundbirder
A web application to play with bird sound identification with data from eBird and Xeno-Canto.
A web application to play with bird sound identification with data from eBird and Xeno-Canto.
## Requirements
- NodeJS, NPM
- MariaDB (MySQL)
- eBird API key
## Installation
```bash
git clone https://forge.chapril.org/UncleSamulus/soundbirder
cd soundbirder
npm install
```
Setup the EBIRD_API_KEY in `.env`:
```text
EBIRD_API_KEY="secret"
```
```bash
npm start
```
Then go to [http://localhost:3000](http://localhost:3000), and have fun !
TODO: add database configuration.

49
app.js
View File

@ -1,13 +1,17 @@
var createError = require('http-errors');
var express = require('express');
var path = require('path');
var cookieParser = require('cookie-parser');
var logger = require('morgan');
var indexRouter = require('./routes/index');
const createError = require('http-errors');
const express = require('express');
const session = require('express-session');
const csrf = require('csurf');
const path = require('path');
const cookieParser = require('cookie-parser');
const logger = require('morgan');
const i18n = require('i18n-2');
var app = express();
const indexRouter = require('./routes/index');
const apiRouter = require('./routes/api');
const authRouter = require('./routes/auth');
const app = express();
// view engine setup
app.set('views', path.join(__dirname, 'views'));
@ -19,6 +23,19 @@ app.use(express.urlencoded({ extended: false }));
app.use(cookieParser());
app.use(express.static(path.join(__dirname, 'public')));
const sess = {
secret: 'keyboard cat',
resave: true,
saveUninitialized: true
}
if (app.get('env') === 'production') {
app.set('trust proxy', 1); // trust first proxy
sess.cookie.secure = true; // serve secure cookies
}
app.use(session(sess));
i18n.expressBind(app, {
locales: ['en', 'es', 'fr', 'de'],
defaultLocale: 'en',
@ -27,7 +44,7 @@ i18n.expressBind(app, {
app.use('/dist/leaflet', express.static('node_modules/leaflet/dist'));
app.use('/dist/feather', express.static('node_modules/feather-icons/dist'));
app.use('/dist/axios', express.static('node_modules/axios/dist'));
app.use(function(req, res, next) {
req.i18n.setLocaleFromQuery();
@ -35,7 +52,21 @@ app.use(function(req, res, next) {
next();
});
app.use(function(req, res, next) {
res.header('Access-Control-Allow-Origin', 'http://localhost:3000');
res.header(
'Access-Control-Allow-Headers',
'Origin, X-Requested-With, Content-Type, Accept'
);
next();
});
app.use('/api/0', apiRouter);
const csrfProtection = csrf({ cookie: true });
app.use(csrfProtection);
app.use('/', indexRouter);
app.use('/auth', authRouter);
// catch 404 and forward to error handler
app.use(function(req, res, next) {

18
bin/www
View File

@ -4,22 +4,22 @@
* Module dependencies.
*/
var app = require('../app');
var debug = require('debug')('soundbirder:server');
var http = require('http');
const app = require('../app');
const debug = require('debug')('soundbirder:server');
const http = require('http');
/**
* Get port from environment and store in Express.
*/
var port = normalizePort(process.env.PORT || '3000');
const port = normalizePort(process.env.PORT || '3000');
app.set('port', port);
/**
* Create HTTP server.
*/
var server = http.createServer(app);
const server = http.createServer(app);
/**
* Listen on provided port, on all network interfaces.
@ -34,7 +34,7 @@ server.on('listening', onListening);
*/
function normalizePort(val) {
var port = parseInt(val, 10);
const port = parseInt(val, 10);
if (isNaN(port)) {
// named pipe
@ -58,7 +58,7 @@ function onError(error) {
throw error;
}
var bind = typeof port === 'string'
const bind = typeof port === 'string'
? 'Pipe ' + port
: 'Port ' + port;
@ -82,8 +82,8 @@ function onError(error) {
*/
function onListening() {
var addr = server.address();
var bind = typeof addr === 'string'
const addr = server.address();
const bind = typeof addr === 'string'
? 'pipe ' + addr
: 'port ' + addr.port;
debug('Listening on ' + bind);

View File

@ -1,3 +1,156 @@
const debug = require('debug')('soundbirder:api');
const debugResponses = require('debug')('soundbirder:api:responses');
require('dotenv').config();
const axios = require('axios');
const eBird = require('@unclesamulus/ebird-api')(process.env.EBIRD_API_KEY);
const XenoCanto = require('@unclesamulus/xeno-canto-api');
const { choices, choice } = require('../utils/choices');
function getRecord(req, res) {
const OPENCAGE_API_KEY = process.env.OPENCAGE_API_KEY;
const OPENCAGE_API_URL = 'https://api.opencagedata.com/geocode/v1/json?q=<lat>+<lon>&key=<key>';
const QUIZZ_SIZE = process.env.QUIZZ_SIZE ? process.env.QUIZZ_SIZE : 5;
function check(req, res) {
}
function quizz(req, res) {
debug('Generating quizz');
const { lat, lng } = req.body;
generateQuizz({ lat, lng }, req.locale)
.then(({ species, correct, audio }) => {
req.session.correct = correct;
res.json({ species, audio });
debug("Quizz sent");
})
.catch(error => {
debug("Faced error while generating quizz");
res.json({ error });
throw error;
});
}
async function generateQuizz(coordinates, locale) {
const quizz = {}
try {
const speciesSelection = await getSpeciesSelection(coordinates);
const speciesSelectionLocalized = await getLocalizedNames(speciesSelection, locale);
quizz.species = await speciesSelectionLocalized;
debug("Got species selection", quizz.species);
const answer = await choice(speciesSelectionLocalized);
debug("Got answer", answer);
quizz.correct = answer.code;
quizz.audio = await getAudio(answer.sciName);
} catch (error) {
debug("Error raised while generating quizz");
console.error(error);
}
return quizz;
}
async function getSpeciesSelection(coordinates) {
const { lat, lng } = coordinates;
const region = await getRegion(lat, lng);
region.country_code = region.country_code.toUpperCase();
// const regionCode = await getRegionCode(region);
const regionCode = region.country_code;
const speciesList = await getSpeciesList(regionCode);
const speciesSelection = choices(speciesList, QUIZZ_SIZE);
debug("Species proposals:", speciesSelection)
return speciesSelection;
}
function getHome(req, res) {
res.render('api', {
title: "SoundBirder api",
version: 0
}
);
}
function getRegion(lat, lon) {
const url = OPENCAGE_API_URL
.replace('<lat>', lat)
.replace('<lon>', lon)
.replace('<key>', OPENCAGE_API_KEY);
return axios.get(url).then(response => {
const { results } = response.data;
const { country_code, country, state, county } = results[0].components;
return { country_code, country, state, county };
}).catch(error => {
throw error;
});
}
function getRegionCode({ country_code, country, state, county }) {
return eBird.ref.region.list('subnational1', country_code)()
.then(states => {
console.log(states);
const regionCode = states.find(region => region.name === state).code;
return regionCode;
}).catch(error => {
throw error;
});
}
async function getLocalizedNames(speciesCodes, locale) {
let names = [];
for (let i = 0; i < speciesCodes.length; i++) {
const code = speciesCodes[i];
const { sciName, comName } = await getLocalizedName(code, locale);
names.push({ code, sciName, comName });
}
return names;
}
function getLocalizedName(speciesCode, locale) {
return eBird.ref.taxonomy.ebird({
fmt: 'json',
locale: locale,
species: speciesCode
}).then(
response => {
const names = response[0];
debug("Got localized species names");
debugResponses(names);
return names;
}
).catch(error => {
throw error;
});
}
function getSpeciesList(regionCode) {
return eBird.product.spplist.in(regionCode)()
.then(species => {
return species;
})
.catch(error => {
throw error;
});
}
function getAudio(speciesScientificName) {
return XenoCanto.search({
name: speciesScientificName,
quality: 'A'
}).then(response => {
debugResponses(response);
const { recordings } = response;
const randomRecord = choice(recordings);
const audio = randomRecord.file;
return audio;
}).catch(error => {
throw error;
});
}
const game = {
check,
quizz
}
module.exports = {
getHome,
game
}

48
controllers/auth.js Normal file
View File

@ -0,0 +1,48 @@
/**
* Auth controller
*/
function indexPage(req, res) {
res.redirect('/auth/login');
}
function loginPage(req, res) {
res.render('auth/login');
}
function login(req, res) {
}
function logout(req, res) {
}
function registerPage(req, res) {
res.render('auth/register');
}
function register(req, res) {
}
function forgotPassword(req, res) {
}
function resetPassword(req, res) {
}
const auth = {
indexPage,
login,
loginPage,
logout,
register,
registerPage,
forgotPassword,
resetPassword
}
module.exports = auth;

View File

@ -1,6 +1,9 @@
function getIndex(req, res) {
res.render('index', { title: 'SoundBirder' });
res.render('index', {
title: 'SoundBirder',
csrf_token: req.csrfToken()
});
}
function getAbout(req, res) {

0
data/.gitkeep Normal file
View File

View File

@ -7,5 +7,6 @@
"Author": "Author",
"The project is made with &hearts; by Samuel ORTION": "The project is made with &hearts; by Samuel ORTION",
"SoundBirder is an open-source web application to learn bird song identification. It is based on bird records from Xeno-Canto and data from eBird.": "SoundBirder is an open-source web application to learn bird song identification. It is based on bird records from Xeno-Canto and data from eBird.",
"The project is made with ♥ by Samuel ORTION.": "The project is made with ♥ by Samuel ORTION."
"The project is made with ♥ by Samuel ORTION.": "The project is made with ♥ by Samuel ORTION.",
"Welcome to SoundBirder' API": "Welcome to SoundBirder' API"
}

373
package-lock.json generated
View File

@ -8,14 +8,20 @@
"name": "soundbirder",
"version": "0.0.0",
"dependencies": {
"@unclesamulus/ebird-api": "^0.0.0",
"@unclesamulus/xeno-canto-api": "^0.0.0",
"axios": "^0.27.2",
"cookie-parser": "~1.4.4",
"csurf": "^1.11.0",
"debug": "~2.6.9",
"dotenv": "^16.0.1",
"express": "~4.16.1",
"express-session": "^1.17.3",
"feather-icons": "^4.29.0",
"http-errors": "~1.6.3",
"i18n-2": "^0.7.3",
"leaflet": "^1.8.0",
"lodash": "^4.17.21",
"morgan": "~1.9.1",
"pug": "^3.0.2"
}
@ -37,9 +43,9 @@
}
},
"node_modules/@babel/parser": {
"version": "7.18.11",
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.18.11.tgz",
"integrity": "sha512-9JKn5vN+hDt0Hdqn1PiJ2guflwP+B6Ga8qbDuoF0PzzVhrzsKIJo8yGqVk6CmMHiMei9w1C1Bp9IMJSIK+HPIQ==",
"version": "7.18.13",
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.18.13.tgz",
"integrity": "sha512-dgXcIfMuQ0kgzLB2b9tRZs7TTFFaGM2AbtA4fJgUUYukzGH4jwsS7hzQHEGs67jdehpm22vkgKwvbU+aEflgwg==",
"bin": {
"parser": "bin/babel-parser.js"
},
@ -48,9 +54,9 @@
}
},
"node_modules/@babel/types": {
"version": "7.18.10",
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.18.10.tgz",
"integrity": "sha512-MJvnbEiiNkpjo+LknnmRrqbY1GPUUggjv+wQVjetM/AONoupqRALB7I6jGqNUAZsKcRIEu2J6FRFvsczljjsaQ==",
"version": "7.18.13",
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.18.13.tgz",
"integrity": "sha512-ePqfTihzW0W6XAU+aMw2ykilisStJfDnsejDCXRchCcMJ4O0+8DhPXf2YUbZ6wjBlsEmZwLK/sPweWtu8hcJYQ==",
"dependencies": {
"@babel/helper-string-parser": "^7.18.10",
"@babel/helper-validator-identifier": "^7.18.6",
@ -60,6 +66,22 @@
"node": ">=6.9.0"
}
},
"node_modules/@unclesamulus/ebird-api": {
"version": "0.0.0",
"resolved": "https://registry.npmjs.org/@unclesamulus/ebird-api/-/ebird-api-0.0.0.tgz",
"integrity": "sha512-cQca/wS+35LuuLoVVB3jDeD3czVBy1FNQBO7uubsnwH9gYZzQEr+TpCeNcMO0S9/ZIjl36fJ3qyFH6R/AsHMFA==",
"dependencies": {
"axios": "^0.27.2"
}
},
"node_modules/@unclesamulus/xeno-canto-api": {
"version": "0.0.0",
"resolved": "https://registry.npmjs.org/@unclesamulus/xeno-canto-api/-/xeno-canto-api-0.0.0.tgz",
"integrity": "sha512-dVMpX6pbhQvxyjR4eyIMAvR9VNJ/qXnoPc+UQRnA5ESz2JcqVGxcFCByhu88Ym41/QzlfpFwO+/glsGHtzfZWw==",
"dependencies": {
"axios": "^0.27.2"
}
},
"node_modules/accepts": {
"version": "1.3.8",
"resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz",
@ -249,15 +271,83 @@
"integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ=="
},
"node_modules/core-js": {
"version": "3.24.1",
"resolved": "https://registry.npmjs.org/core-js/-/core-js-3.24.1.tgz",
"integrity": "sha512-0QTBSYSUZ6Gq21utGzkfITDylE8jWC9Ne1D2MrhvlsZBI1x39OdDIVbzSqtgMndIy6BlHxBXpMGqzZmnztg2rg==",
"version": "3.25.0",
"resolved": "https://registry.npmjs.org/core-js/-/core-js-3.25.0.tgz",
"integrity": "sha512-CVU1xvJEfJGhyCpBrzzzU1kjCfgsGUxhEvwUV2e/cOedYWHdmluamx+knDnmhqALddMG16fZvIqvs9aijsHHaA==",
"hasInstallScript": true,
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/core-js"
}
},
"node_modules/csrf": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/csrf/-/csrf-3.1.0.tgz",
"integrity": "sha512-uTqEnCvWRk042asU6JtapDTcJeeailFy4ydOQS28bj1hcLnYRiqi8SsD2jS412AY1I/4qdOwWZun774iqywf9w==",
"dependencies": {
"rndm": "1.2.0",
"tsscmp": "1.0.6",
"uid-safe": "2.1.5"
},
"engines": {
"node": ">= 0.8"
}
},
"node_modules/csurf": {
"version": "1.11.0",
"resolved": "https://registry.npmjs.org/csurf/-/csurf-1.11.0.tgz",
"integrity": "sha512-UCtehyEExKTxgiu8UHdGvHj4tnpE/Qctue03Giq5gPgMQ9cg/ciod5blZQ5a4uCEenNQjxyGuzygLdKUmee/bQ==",
"dependencies": {
"cookie": "0.4.0",
"cookie-signature": "1.0.6",
"csrf": "3.1.0",
"http-errors": "~1.7.3"
},
"engines": {
"node": ">= 0.8.0"
}
},
"node_modules/csurf/node_modules/cookie": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.0.tgz",
"integrity": "sha512-+Hp8fLp57wnUSt0tY0tHEXh4voZRDnoIrZPqlo3DPiI4y9lwg/jqx+1Om94/W6ZaPDOUbnjOt/99w66zk+l1Xg==",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/csurf/node_modules/http-errors": {
"version": "1.7.3",
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.7.3.tgz",
"integrity": "sha512-ZTTX0MWrsQ2ZAhA1cejAwDLycFsd7I7nVtnkT3Ol0aqodaKW+0CTZDQ1uBv5whptCnc8e8HeRRJxRs0kmm/Qfw==",
"dependencies": {
"depd": "~1.1.2",
"inherits": "2.0.4",
"setprototypeof": "1.1.1",
"statuses": ">= 1.5.0 < 2",
"toidentifier": "1.0.0"
},
"engines": {
"node": ">= 0.6"
}
},
"node_modules/csurf/node_modules/inherits": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
},
"node_modules/csurf/node_modules/setprototypeof": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.1.tgz",
"integrity": "sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw=="
},
"node_modules/csurf/node_modules/statuses": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz",
"integrity": "sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/debug": {
"version": "2.6.9",
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
@ -292,6 +382,14 @@
"resolved": "https://registry.npmjs.org/doctypes/-/doctypes-1.1.0.tgz",
"integrity": "sha512-LLBi6pEqS6Do3EKQ3J0NqHWV5hhb78Pi8vvESYwyOy2c31ZEZVdtitdzsQsKb7878PEERhzUk0ftqGhG6Mz+pQ=="
},
"node_modules/dotenv": {
"version": "16.0.1",
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.0.1.tgz",
"integrity": "sha512-1K6hR6wtk2FviQ4kEiSjFiH5rpzEVi8WW0x96aztHVMhEspNpc4DVOUTEHtEva5VThQ8IaBX1Pe4gSzpVVUsKQ==",
"engines": {
"node": ">=12"
}
},
"node_modules/ee-first": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
@ -358,6 +456,59 @@
"node": ">= 0.10.0"
}
},
"node_modules/express-session": {
"version": "1.17.3",
"resolved": "https://registry.npmjs.org/express-session/-/express-session-1.17.3.tgz",
"integrity": "sha512-4+otWXlShYlG1Ma+2Jnn+xgKUZTMJ5QD3YvfilX3AcocOAbIkVylSWEklzALe/+Pu4qV6TYBj5GwOBFfdKqLBw==",
"dependencies": {
"cookie": "0.4.2",
"cookie-signature": "1.0.6",
"debug": "2.6.9",
"depd": "~2.0.0",
"on-headers": "~1.0.2",
"parseurl": "~1.3.3",
"safe-buffer": "5.2.1",
"uid-safe": "~2.1.5"
},
"engines": {
"node": ">= 0.8.0"
}
},
"node_modules/express-session/node_modules/cookie": {
"version": "0.4.2",
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.2.tgz",
"integrity": "sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA==",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/express-session/node_modules/depd": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
"integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==",
"engines": {
"node": ">= 0.8"
}
},
"node_modules/express-session/node_modules/safe-buffer": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
"integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
]
},
"node_modules/express/node_modules/cookie": {
"version": "0.3.1",
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.3.1.tgz",
@ -616,6 +767,11 @@
"resolved": "https://registry.npmjs.org/leaflet/-/leaflet-1.8.0.tgz",
"integrity": "sha512-gwhMjFCQiYs3x/Sf+d49f10ERXaEFCPr+nVTryhAW8DWbMGqJqt9G4XuIaHmFW08zYvhgdzqXGr8AlW8v8dQkA=="
},
"node_modules/lodash": {
"version": "4.17.21",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="
},
"node_modules/media-typer": {
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
@ -877,6 +1033,14 @@
"node": ">=0.6"
}
},
"node_modules/random-bytes": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/random-bytes/-/random-bytes-1.0.0.tgz",
"integrity": "sha512-iv7LhNVO047HzYR3InF6pUcUsPQiHTM1Qal51DcGSuZFBil1aBBWG5eHPNek7bvILMaYJ/8RU1e8w1AMdHmLQQ==",
"engines": {
"node": ">= 0.8"
}
},
"node_modules/range-parser": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
@ -915,6 +1079,11 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/rndm": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/rndm/-/rndm-1.2.0.tgz",
"integrity": "sha512-fJhQQI5tLrQvYIYFpOnFinzv9dwmR7hRnUz1XqP3OJ1jIweTNOd6aTO4jwQSgcBSFUB+/KHJxuGneime+FdzOw=="
},
"node_modules/safe-buffer": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
@ -999,11 +1168,27 @@
"node": ">=4"
}
},
"node_modules/toidentifier": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.0.tgz",
"integrity": "sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw==",
"engines": {
"node": ">=0.6"
}
},
"node_modules/token-stream": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/token-stream/-/token-stream-1.0.0.tgz",
"integrity": "sha512-VSsyNPPW74RpHwR8Fc21uubwHY7wMDeJLys2IX5zJNih+OnAnaifKHo+1LHT7DAdloQ7apeaaWg8l7qnf/TnEg=="
},
"node_modules/tsscmp": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/tsscmp/-/tsscmp-1.0.6.tgz",
"integrity": "sha512-LxhtAkPDTkVCMQjt2h6eBVY28KCjikZqZfMcC15YBeNjkgUpdCfBu5HoiOTDu86v6smE8yOjyEktJ8hlbANHQA==",
"engines": {
"node": ">=0.6.x"
}
},
"node_modules/type-is": {
"version": "1.6.18",
"resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz",
@ -1016,6 +1201,17 @@
"node": ">= 0.6"
}
},
"node_modules/uid-safe": {
"version": "2.1.5",
"resolved": "https://registry.npmjs.org/uid-safe/-/uid-safe-2.1.5.tgz",
"integrity": "sha512-KPHm4VL5dDXKz01UuEd88Df+KzynaohSL9fBh096KWAxSKZQDI2uBrVqtvRM4rwrIrRRKsdLNML/lnaaVSRioA==",
"dependencies": {
"random-bytes": "~1.0.0"
},
"engines": {
"node": ">= 0.8"
}
},
"node_modules/unpipe": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
@ -1075,20 +1271,36 @@
"integrity": "sha512-MmetCkz9ej86nJQV+sFCxoGGrUbU3q02kgLciwkrt9QqEB7cP39oKEY0PakknEO0Gu20SskMRi+AYZ3b1TpN9g=="
},
"@babel/parser": {
"version": "7.18.11",
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.18.11.tgz",
"integrity": "sha512-9JKn5vN+hDt0Hdqn1PiJ2guflwP+B6Ga8qbDuoF0PzzVhrzsKIJo8yGqVk6CmMHiMei9w1C1Bp9IMJSIK+HPIQ=="
"version": "7.18.13",
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.18.13.tgz",
"integrity": "sha512-dgXcIfMuQ0kgzLB2b9tRZs7TTFFaGM2AbtA4fJgUUYukzGH4jwsS7hzQHEGs67jdehpm22vkgKwvbU+aEflgwg=="
},
"@babel/types": {
"version": "7.18.10",
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.18.10.tgz",
"integrity": "sha512-MJvnbEiiNkpjo+LknnmRrqbY1GPUUggjv+wQVjetM/AONoupqRALB7I6jGqNUAZsKcRIEu2J6FRFvsczljjsaQ==",
"version": "7.18.13",
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.18.13.tgz",
"integrity": "sha512-ePqfTihzW0W6XAU+aMw2ykilisStJfDnsejDCXRchCcMJ4O0+8DhPXf2YUbZ6wjBlsEmZwLK/sPweWtu8hcJYQ==",
"requires": {
"@babel/helper-string-parser": "^7.18.10",
"@babel/helper-validator-identifier": "^7.18.6",
"to-fast-properties": "^2.0.0"
}
},
"@unclesamulus/ebird-api": {
"version": "0.0.0",
"resolved": "https://registry.npmjs.org/@unclesamulus/ebird-api/-/ebird-api-0.0.0.tgz",
"integrity": "sha512-cQca/wS+35LuuLoVVB3jDeD3czVBy1FNQBO7uubsnwH9gYZzQEr+TpCeNcMO0S9/ZIjl36fJ3qyFH6R/AsHMFA==",
"requires": {
"axios": "^0.27.2"
}
},
"@unclesamulus/xeno-canto-api": {
"version": "0.0.0",
"resolved": "https://registry.npmjs.org/@unclesamulus/xeno-canto-api/-/xeno-canto-api-0.0.0.tgz",
"integrity": "sha512-dVMpX6pbhQvxyjR4eyIMAvR9VNJ/qXnoPc+UQRnA5ESz2JcqVGxcFCByhu88Ym41/QzlfpFwO+/glsGHtzfZWw==",
"requires": {
"axios": "^0.27.2"
}
},
"accepts": {
"version": "1.3.8",
"resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz",
@ -1239,9 +1451,64 @@
"integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ=="
},
"core-js": {
"version": "3.24.1",
"resolved": "https://registry.npmjs.org/core-js/-/core-js-3.24.1.tgz",
"integrity": "sha512-0QTBSYSUZ6Gq21utGzkfITDylE8jWC9Ne1D2MrhvlsZBI1x39OdDIVbzSqtgMndIy6BlHxBXpMGqzZmnztg2rg=="
"version": "3.25.0",
"resolved": "https://registry.npmjs.org/core-js/-/core-js-3.25.0.tgz",
"integrity": "sha512-CVU1xvJEfJGhyCpBrzzzU1kjCfgsGUxhEvwUV2e/cOedYWHdmluamx+knDnmhqALddMG16fZvIqvs9aijsHHaA=="
},
"csrf": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/csrf/-/csrf-3.1.0.tgz",
"integrity": "sha512-uTqEnCvWRk042asU6JtapDTcJeeailFy4ydOQS28bj1hcLnYRiqi8SsD2jS412AY1I/4qdOwWZun774iqywf9w==",
"requires": {
"rndm": "1.2.0",
"tsscmp": "1.0.6",
"uid-safe": "2.1.5"
}
},
"csurf": {
"version": "1.11.0",
"resolved": "https://registry.npmjs.org/csurf/-/csurf-1.11.0.tgz",
"integrity": "sha512-UCtehyEExKTxgiu8UHdGvHj4tnpE/Qctue03Giq5gPgMQ9cg/ciod5blZQ5a4uCEenNQjxyGuzygLdKUmee/bQ==",
"requires": {
"cookie": "0.4.0",
"cookie-signature": "1.0.6",
"csrf": "3.1.0",
"http-errors": "~1.7.3"
},
"dependencies": {
"cookie": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.0.tgz",
"integrity": "sha512-+Hp8fLp57wnUSt0tY0tHEXh4voZRDnoIrZPqlo3DPiI4y9lwg/jqx+1Om94/W6ZaPDOUbnjOt/99w66zk+l1Xg=="
},
"http-errors": {
"version": "1.7.3",
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.7.3.tgz",
"integrity": "sha512-ZTTX0MWrsQ2ZAhA1cejAwDLycFsd7I7nVtnkT3Ol0aqodaKW+0CTZDQ1uBv5whptCnc8e8HeRRJxRs0kmm/Qfw==",
"requires": {
"depd": "~1.1.2",
"inherits": "2.0.4",
"setprototypeof": "1.1.1",
"statuses": ">= 1.5.0 < 2",
"toidentifier": "1.0.0"
}
},
"inherits": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
},
"setprototypeof": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.1.tgz",
"integrity": "sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw=="
},
"statuses": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz",
"integrity": "sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA=="
}
}
},
"debug": {
"version": "2.6.9",
@ -1271,6 +1538,11 @@
"resolved": "https://registry.npmjs.org/doctypes/-/doctypes-1.1.0.tgz",
"integrity": "sha512-LLBi6pEqS6Do3EKQ3J0NqHWV5hhb78Pi8vvESYwyOy2c31ZEZVdtitdzsQsKb7878PEERhzUk0ftqGhG6Mz+pQ=="
},
"dotenv": {
"version": "16.0.1",
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.0.1.tgz",
"integrity": "sha512-1K6hR6wtk2FviQ4kEiSjFiH5rpzEVi8WW0x96aztHVMhEspNpc4DVOUTEHtEva5VThQ8IaBX1Pe4gSzpVVUsKQ=="
},
"ee-first": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
@ -1335,6 +1607,38 @@
}
}
},
"express-session": {
"version": "1.17.3",
"resolved": "https://registry.npmjs.org/express-session/-/express-session-1.17.3.tgz",
"integrity": "sha512-4+otWXlShYlG1Ma+2Jnn+xgKUZTMJ5QD3YvfilX3AcocOAbIkVylSWEklzALe/+Pu4qV6TYBj5GwOBFfdKqLBw==",
"requires": {
"cookie": "0.4.2",
"cookie-signature": "1.0.6",
"debug": "2.6.9",
"depd": "~2.0.0",
"on-headers": "~1.0.2",
"parseurl": "~1.3.3",
"safe-buffer": "5.2.1",
"uid-safe": "~2.1.5"
},
"dependencies": {
"cookie": {
"version": "0.4.2",
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.2.tgz",
"integrity": "sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA=="
},
"depd": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
"integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw=="
},
"safe-buffer": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
"integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="
}
}
},
"feather-icons": {
"version": "4.29.0",
"resolved": "https://registry.npmjs.org/feather-icons/-/feather-icons-4.29.0.tgz",
@ -1522,6 +1826,11 @@
"resolved": "https://registry.npmjs.org/leaflet/-/leaflet-1.8.0.tgz",
"integrity": "sha512-gwhMjFCQiYs3x/Sf+d49f10ERXaEFCPr+nVTryhAW8DWbMGqJqt9G4XuIaHmFW08zYvhgdzqXGr8AlW8v8dQkA=="
},
"lodash": {
"version": "4.17.21",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="
},
"media-typer": {
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
@ -1744,6 +2053,11 @@
"resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz",
"integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA=="
},
"random-bytes": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/random-bytes/-/random-bytes-1.0.0.tgz",
"integrity": "sha512-iv7LhNVO047HzYR3InF6pUcUsPQiHTM1Qal51DcGSuZFBil1aBBWG5eHPNek7bvILMaYJ/8RU1e8w1AMdHmLQQ=="
},
"range-parser": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
@ -1770,6 +2084,11 @@
"supports-preserve-symlinks-flag": "^1.0.0"
}
},
"rndm": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/rndm/-/rndm-1.2.0.tgz",
"integrity": "sha512-fJhQQI5tLrQvYIYFpOnFinzv9dwmR7hRnUz1XqP3OJ1jIweTNOd6aTO4jwQSgcBSFUB+/KHJxuGneime+FdzOw=="
},
"safe-buffer": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
@ -1836,11 +2155,21 @@
"resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz",
"integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog=="
},
"toidentifier": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.0.tgz",
"integrity": "sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw=="
},
"token-stream": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/token-stream/-/token-stream-1.0.0.tgz",
"integrity": "sha512-VSsyNPPW74RpHwR8Fc21uubwHY7wMDeJLys2IX5zJNih+OnAnaifKHo+1LHT7DAdloQ7apeaaWg8l7qnf/TnEg=="
},
"tsscmp": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/tsscmp/-/tsscmp-1.0.6.tgz",
"integrity": "sha512-LxhtAkPDTkVCMQjt2h6eBVY28KCjikZqZfMcC15YBeNjkgUpdCfBu5HoiOTDu86v6smE8yOjyEktJ8hlbANHQA=="
},
"type-is": {
"version": "1.6.18",
"resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz",
@ -1850,6 +2179,14 @@
"mime-types": "~2.1.24"
}
},
"uid-safe": {
"version": "2.1.5",
"resolved": "https://registry.npmjs.org/uid-safe/-/uid-safe-2.1.5.tgz",
"integrity": "sha512-KPHm4VL5dDXKz01UuEd88Df+KzynaohSL9fBh096KWAxSKZQDI2uBrVqtvRM4rwrIrRRKsdLNML/lnaaVSRioA==",
"requires": {
"random-bytes": "~1.0.0"
}
},
"unpipe": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",

View File

@ -6,14 +6,20 @@
"start": "node ./bin/www"
},
"dependencies": {
"@unclesamulus/ebird-api": "^0.0.0",
"@unclesamulus/xeno-canto-api": "^0.0.0",
"axios": "^0.27.2",
"cookie-parser": "~1.4.4",
"csurf": "^1.11.0",
"debug": "~2.6.9",
"dotenv": "^16.0.1",
"express": "~4.16.1",
"express-session": "^1.17.3",
"feather-icons": "^4.29.0",
"http-errors": "~1.6.3",
"i18n-2": "^0.7.3",
"leaflet": "^1.8.0",
"lodash": "^4.17.21",
"morgan": "~1.9.1",
"pug": "^3.0.2"
}

View File

@ -0,0 +1,49 @@
const API_VERSION = "0";
const API_URL = `/api/${API_VERSION}`;
const TOKEN = document.querySelector('meta[name="csrf-token"]').getAttribute('content');
async function get(url, params) {
return await axios.get(url, {
params: params,
headers: {
'X-CSRF-Token': TOKEN
}
}).then(response => {
return response.data;
}).catch(error => {
throw error;
});
}
async function query(endpoint, params) {
return await get(`${API_URL}/${endpoint}`, params);
}
function getQuizz(lat, lng) {
return query('game/quizz', { lat, lng })
.then(data => {
console.log(data);
return data;
}).catch(error => {
throw error;
});
}
function checkResponse(speciesId) {
return query('game/check', {
species: speciesId
}).then(response => {
return response.data.correct
}).catch(error => {
throw error;
});
}
const client = {
getQuizz,
checkResponse
}
export default client;

View File

@ -1,13 +1 @@
import { geoLocationHandler } from './map.js';
import './game.js';
feather.replace();
(function () {
if (document.getElementById('map') != undefined)
geoLocationHandler();
let start_button = document.querySelector('.game .start-button');
if (start_button != undefined)
start_button.addEventListener('click', launchGame);
}());
feather.replace();

View File

@ -1,15 +1,52 @@
const API_VERSION = "1";
import { geolocationHandler, getCoordinates } from './map.js';
import client from './api-client.js';
function launch() {
const API_VERSION = "0";
function geolocationStep() {
if (document.getElementById('map') != undefined)
geolocationHandler();
let start_button = document.querySelector('.game .start-button');
if (start_button != undefined)
start_button.addEventListener('click', quizzStep);
}
function get_new_record() {
const endpoint = `/api/${API_VERSION}/record`;
function quizzStep() {
// Start by disallowing geolocation step
document.querySelector('.game-map-step').classList.toggle('none');
// Then allow the quizz step
document.querySelector('.game-quizz-step').classList.remove('none');
// Retrieve coordinates from former done geolocation (TODO: fix the need of cookie)
const coordinates = getCoordinates();
client.getQuizz(coordinates)
.then(quizz => {
// Display the quizz
displayQuizz(quizz);
}).catch(error => {
console.log(error);
});
}
const Game = {
launch
function displayQuizz(quizz) {
let audio = document.querySelector('.game-quizz-step audio');
audio.src = quizz.audio;
audio.play();
let proposals = document.querySelector('.game-quizz-step .proposals');
quizz.species.forEach(sp => {
let proposal = document.createElement('li');
proposal.classList.add('proposal');
let button = document.createElement('button');
button.classList.add('proposal-button');
button.value = sp.code;
button.innerText = sp.comName;
proposal.appendChild(button);
proposals.appendChild(proposal);
});
}
export default Game;
function game() {
geolocationStep();
}
game();

View File

@ -1,6 +1,6 @@
import { getCookie, setCookie } from './utils.js'
function geoLocationHandler() {
function getCoordinates() {
let location = [51.505, -0.09]; // London by default on leaflet
let lat = getCookie("lat");
let lng = getCookie("lng");
@ -8,15 +8,25 @@ function geoLocationHandler() {
location = [lat, lng];
console.log(`Got a previous geolocation cookie at ${location[0]}, ${location[1]}`)
}
return location;
}
function updateLocationCookies([lat, lng]) {
setCookie("lat", lat, 10);
setCookie("lng", lng, 10);
}
function geolocationHandler() {
let location = getCoordinates();
let message = document.querySelector('.message');
// Init map
let map = L.map('map').setView(location, 15);
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
attribution: '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a>'
}).addTo(map);
// Init marker
let marker = L.marker(location).addTo(map);
let marker = undefined;
// Get geolocation
function getLocation() {
if (navigator.geolocation) {
@ -28,17 +38,41 @@ function geoLocationHandler() {
function setLocation(position) {
location = [position.coords.latitude, position.coords.longitude];
marker.setLatLng(location);
if (marker === undefined) {
marker = new L.marker(location, { draggable: 'true' });
marker.on('dragend', updateMarker);
marker.addTo(map);
} else {
marker.setLatLng(location);
}
map.setView(location, 15);
setCookie("lat", position.coords.latitude, 10);
setCookie("lng", position.coords.longitude, 10);
console.log("Geolocation cookie saved for future games");
updateLocationCookies(location);
}
function updateMap(event) {
if (marker === undefined) {
marker = new L.marker(event.latlng, { draggable: 'true' });
marker.on('dragend', updateMarker);
marker.addTo(map);
} else {
marker.setLatLng(event.latlng);
}
updateLocationCookies([event.latlng.lat, event.latlng.lng]);
}
function updateMarker(event) {
let position = marker.getLatLng();
map.panTo([position.lat, position.lng]);
updateLocationCookies([position.lat, position.lng]);
}
map.on('click', updateMap);
document.querySelector('.geolocation-button')
.addEventListener('click', getLocation);
}
export {
geoLocationHandler
geolocationHandler,
getCoordinates
}

View File

@ -2,7 +2,7 @@ function setCookie(cname, cvalue, exdays) {
const d = new Date();
d.setTime(d.getTime() + (exdays * 24 * 60 * 60 * 1000));
let expires = "expires=" + d.toUTCString();
document.cookie = cname + "=" + cvalue + ";" + expires + ";path=/";
document.cookie = cname + "=" + cvalue + ";" + expires + ";path=/" + ";sameSite=strict;";
}
function getCookie(cname) {

View File

@ -69,3 +69,7 @@ nav ul {
flex-direction: row;
justify-content: space-evenly;
}
.none {
display: none;
}

14
routes/api.js Normal file
View File

@ -0,0 +1,14 @@
const express = require('express');
const router = express.Router();
const apiController = require('../controllers/api.js');
router.route('/')
.get(apiController.getHome);
router.route('/game/quizz')
.get(apiController.game.quizz);
router.route('/game/check')
.post(apiController.game.check);
module.exports = router;

11
routes/auth.js Normal file
View File

@ -0,0 +1,11 @@
const express = require('express');
const router = express.Router();
const authController = require('../controllers/auth');
router.route('/')
.get(authController.indexPage);
router.route('/login')
.get(authController.loginPage);
module.exports = router;

View File

@ -1,12 +1,11 @@
var express = require('express');
var router = express.Router();
var homeController = require('../controllers/home');
/* GET home page. */
const express = require('express');
const router = express.Router();
const indexController = require('../controllers/index');
router.route('/')
.get(homeController.getIndex);
.get(indexController.getIndex);
router.route('/about')
.get(homeController.getAbout);
.get(indexController.getAbout);
module.exports = router;

34
utils/choices.js Normal file
View File

@ -0,0 +1,34 @@
/**
* Get a random selection from an array.
*
* @param {Array} array
* @param {Number} number
* @returns `number` random elements from the array
*/
function choices(array, number) {
const result = [];
for (let i = 0; i < number; i++) {
let c = choice(array);
if (result.includes(c)) {
i--;
} else {
result.push(choice(array));
}
}
return result;
}
/**
* Get a random element from an array.
*
* @param {Array} array
* @returns a random element from the array
*/
function choice(array) {
return array[Math.floor(Math.random() * array.length)];
}
module.exports = {
choices,
choice
}

View File

@ -1,8 +1,8 @@
extends layout.pug
block content
p #{ __("SoundBirder is an open-source web application to learn bird song identification. It is based on bird records from Xeno-Canto and data from eBird.") }
p #{ __("SoundBirder is an open-source web application to learn bird song identification. It is based on bird records from Xeno-Canto and data from eBird.") }
h2 #{ __("Author") }
p #{ __("The project is made with ♥ by Samuel ORTION.") }
h2 #{ __("Author") }
p #{ __("The project is made with ♥ by Samuel ORTION.") }

5
views/api.pug Normal file
View File

@ -0,0 +1,5 @@
extends layout
block content
h2= __('Welcome to SoundBirder\'s API')
.version api v#{version}

View File

@ -1,6 +1,6 @@
extends layout
block content
h1= message
h2= error.status
pre #{error.stack}
h1= message
h2= error.status
pre #{error.stack}

View File

@ -1,8 +1,18 @@
.game
#map
button.button.geolocation-button
i(data-feather="map-pin")
button.button.start-button
i(data-feather="play")
link(rel="stylesheet", href="/dist/leaflet/leaflet.css")
script(src="/dist/leaflet/leaflet.js")
.game-map-step
#map
button.button.geolocation-button
i(data-feather="map-pin")
button.button.start-button
i(data-feather="play")
.game-quizz-step.none
ul.proposals
audio
.game-result-step.none
p.result
button.button.restart-button.disabled
i(data-feather="repeat")
link(rel="stylesheet" href="/dist/leaflet/leaflet.css")
script(src="/dist/leaflet/leaflet.js")
script(src="/dist/axios/axios.min.js")
script(src="/javascripts/game.js" type="module")

View File

@ -1,4 +1,4 @@
extends layout
block content
include game
include game

View File

@ -1,26 +1,26 @@
doctype html
html
head
title= title
meta(name="iewport", content="width=device-width, initial-scale=1.0")
link(rel="stylesheet", href="/stylesheets/style.css")
body
header
h1= title
nav
ul
li
a(href="/") #{ __("Game") }
li
a(href="/about") #{ __("About") }
li
a(href="/contact") #{ __("Contact") }
main
block content
footer
.description
.copyright Copyright &copy; 2022 -
span.author
a(href="https://samuel.ortion.fr", class="link") Samuel ORTION
script(src="/javascripts/app.js" type="module")
script(src="/dist/feather/feather.min.js")
head
title= title
meta(name="viewport" content="width=device-width, initial-scale=1.0")
link(rel="stylesheet" href="/stylesheets/style.css")
if csrf_token
meta(name="csrf-token" content=csrf_token)
body
header
h1= title
nav
ul
li
a(href="/") #{ __("Game") }
li
a(href="/about") #{ __("About") }
main
block content
footer
.description
.copyright Copyright &copy; 2022 -
span.author
a(href="https://samuel.ortion.fr" class="link") Samuel ORTION
script(src="/javascripts/app.js" type="module")
script(src="/dist/feather/feather.min.js")