diff --git a/.editorconfig b/.editorconfig deleted file mode 100644 index a850bf9..0000000 --- a/.editorconfig +++ /dev/null @@ -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 diff --git a/.gitignore b/.gitignore index 8ee743b..939490e 100644 --- a/.gitignore +++ b/.gitignore @@ -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 \ No newline at end of file +.ideas + +data/* +!/data/.gitkeep \ No newline at end of file diff --git a/README.md b/README.md index a5a5541..0f27e3f 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,31 @@ # soundbirder -A web application to play with bird sound identification with data from eBird and Xeno-Canto. \ No newline at end of file +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. \ No newline at end of file diff --git a/app.js b/app.js index ef6a911..d0336fd 100644 --- a/app.js +++ b/app.js @@ -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) { diff --git a/bin/www b/bin/www index 66b60eb..5a5c134 100755 --- a/bin/www +++ b/bin/www @@ -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); diff --git a/controllers/api.js b/controllers/api.js index 8b35959..24a94c7 100644 --- a/controllers/api.js +++ b/controllers/api.js @@ -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=+&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) + .replace('', lon) + .replace('', 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 } \ No newline at end of file diff --git a/controllers/auth.js b/controllers/auth.js new file mode 100644 index 0000000..6a51805 --- /dev/null +++ b/controllers/auth.js @@ -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; \ No newline at end of file diff --git a/controllers/home.js b/controllers/index.js similarity index 62% rename from controllers/home.js rename to controllers/index.js index 53a7ca8..b085744 100644 --- a/controllers/home.js +++ b/controllers/index.js @@ -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) { diff --git a/data/.gitkeep b/data/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/locales/en.js b/locales/en.js index 32ff642..3cbabc1 100644 --- a/locales/en.js +++ b/locales/en.js @@ -7,5 +7,6 @@ "Author": "Author", "The project is made with ♥ by Samuel ORTION": "The project is made with ♥ 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" } \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 55c4921..e7a0fa6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", diff --git a/package.json b/package.json index 049db1c..2a91ef1 100644 --- a/package.json +++ b/package.json @@ -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" } diff --git a/public/javascripts/api-client.js b/public/javascripts/api-client.js new file mode 100644 index 0000000..84e4300 --- /dev/null +++ b/public/javascripts/api-client.js @@ -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; \ No newline at end of file diff --git a/public/javascripts/app.js b/public/javascripts/app.js index 8368e80..a85f5e6 100644 --- a/public/javascripts/app.js +++ b/public/javascripts/app.js @@ -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(); \ No newline at end of file diff --git a/public/javascripts/game.js b/public/javascripts/game.js index 2510ac9..c889f26 100644 --- a/public/javascripts/game.js +++ b/public/javascripts/game.js @@ -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; \ No newline at end of file +function game() { + geolocationStep(); +} + +game(); \ No newline at end of file diff --git a/public/javascripts/map.js b/public/javascripts/map.js index 44888a9..1dc791d 100644 --- a/public/javascripts/map.js +++ b/public/javascripts/map.js @@ -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: '© OpenStreetMap' }).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 } \ No newline at end of file diff --git a/public/javascripts/utils.js b/public/javascripts/utils.js index faf1f8a..82da365 100644 --- a/public/javascripts/utils.js +++ b/public/javascripts/utils.js @@ -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) { diff --git a/public/stylesheets/style.css b/public/stylesheets/style.css index ef198aa..101ad2b 100644 --- a/public/stylesheets/style.css +++ b/public/stylesheets/style.css @@ -69,3 +69,7 @@ nav ul { flex-direction: row; justify-content: space-evenly; } + +.none { + display: none; +} diff --git a/routes/api.js b/routes/api.js new file mode 100644 index 0000000..25e6591 --- /dev/null +++ b/routes/api.js @@ -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; \ No newline at end of file diff --git a/routes/auth.js b/routes/auth.js new file mode 100644 index 0000000..54e1594 --- /dev/null +++ b/routes/auth.js @@ -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; \ No newline at end of file diff --git a/routes/index.js b/routes/index.js index 1d79505..57f443c 100644 --- a/routes/index.js +++ b/routes/index.js @@ -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; diff --git a/utils/choices.js b/utils/choices.js new file mode 100644 index 0000000..6fc2afc --- /dev/null +++ b/utils/choices.js @@ -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 +} \ No newline at end of file diff --git a/views/about.pug b/views/about.pug index b15f494..a8b9b83 100644 --- a/views/about.pug +++ b/views/about.pug @@ -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.") } - diff --git a/views/api.pug b/views/api.pug new file mode 100644 index 0000000..e92e4a2 --- /dev/null +++ b/views/api.pug @@ -0,0 +1,5 @@ +extends layout + +block content + h2= __('Welcome to SoundBirder\'s API') + .version api v#{version} \ No newline at end of file diff --git a/views/error.pug b/views/error.pug index 51ec12c..3b25cfa 100644 --- a/views/error.pug +++ b/views/error.pug @@ -1,6 +1,6 @@ extends layout block content - h1= message - h2= error.status - pre #{error.stack} + h1= message + h2= error.status + pre #{error.stack} diff --git a/views/game.pug b/views/game.pug index 39053f1..ea63aa8 100644 --- a/views/game.pug +++ b/views/game.pug @@ -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") \ No newline at end of file + .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") \ No newline at end of file diff --git a/views/index.pug b/views/index.pug index 7cfe919..185c3f0 100644 --- a/views/index.pug +++ b/views/index.pug @@ -1,4 +1,4 @@ extends layout block content - include game + include game diff --git a/views/layout.pug b/views/layout.pug index 3a821ef..70b1065 100644 --- a/views/layout.pug +++ b/views/layout.pug @@ -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 © 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") \ No newline at end of file + 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 © 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") \ No newline at end of file