From 298c42e63ae321526693e9ce418c4113af36e025 Mon Sep 17 00:00:00 2001
From: Benjamin Auder <benjamin.auder@somewhere>
Date: Mon, 7 Jan 2019 17:36:27 +0100
Subject: [PATCH] Index page almost OK. Now work on variant page (main hall...)

---
 .gitignore                                |   6 +-
 app.js                                    |   5 +-
 package-lock.json                         |  39 +--
 package.json                              |   3 +-
 public/javascripts/components/game.js     |  20 +-
 public/javascripts/components/problems.js |  14 +-
 public/javascripts/contactForm.js         |  31 ++
 public/javascripts/index.js               |  54 +--
 public/javascripts/utils/md5.js           | 379 ----------------------
 public/javascripts/variant.js             |   1 +
 public/stylesheets/index.sass             |  27 +-
 public/stylesheets/layout.sass            |  30 +-
 routes/all.js                             | 140 +-------
 routes/index.js                           |  21 ++
 routes/messages.js                        |  20 ++
 routes/problems.js                        |  58 ++++
 routes/variant.js                         |  33 ++
 utils/language.js                         |  27 ++
 utils/sendEmail.js.dist                   |  32 ++
 views/contactForm.pug                     |  18 +
 views/index.pug                           |  14 +-
 views/layout.pug                          |  27 +-
 views/settings.pug                        |   8 +-
 views/translations/en.pug                 |   7 +
 views/variant.pug                         |   7 +-
 25 files changed, 384 insertions(+), 637 deletions(-)
 create mode 100644 public/javascripts/contactForm.js
 delete mode 100644 public/javascripts/utils/md5.js
 create mode 100644 routes/index.js
 create mode 100644 routes/messages.js
 create mode 100644 routes/problems.js
 create mode 100644 routes/variant.js
 create mode 100644 utils/language.js
 create mode 100644 utils/sendEmail.js.dist
 create mode 100644 views/contactForm.pug

diff --git a/.gitignore b/.gitignore
index 30651eea..c273cf0c 100644
--- a/.gitignore
+++ b/.gitignore
@@ -15,11 +15,9 @@ pids
 *.swp
 *~
 
-# Demo video, database
-*.webm
+# Various files
 /db/vchess.sqlite
-
-# socket URL file
+/utils/sendEmail.js
 /public/javascripts/socket_url.js
 
 # CSS generated files
diff --git a/app.js b/app.js
index b44be791..914e29b1 100644
--- a/app.js
+++ b/app.js
@@ -6,8 +6,6 @@ var logger = require('morgan');
 var sassMiddleware = require('node-sass-middleware');
 var favicon = require('serve-favicon');
 
-var router = require('./routes/all');
-
 var app = express();
 
 app.use(favicon(path.join(__dirname, "public", "images", "favicon", "favicon.ico")));
@@ -45,7 +43,8 @@ app.use(sassMiddleware({
 }));
 app.use(express.static(path.join(__dirname, 'public')));
 
-app.use('/', router);
+const routes = require(path.join(__dirname, "routes", "all"));
+app.use('/', routes);
 
 // catch 404 and forward to error handler
 app.use(function(req, res, next) {
diff --git a/package-lock.json b/package-lock.json
index cc552a26..2acac2b0 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1141,9 +1141,9 @@
       }
     },
     "debug": {
-      "version": "4.1.0",
-      "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.0.tgz",
-      "integrity": "sha512-heNPJUJIqC+xB6ayLAMHaIrmN9HKa7aQO8MGqKpvCA+uJYVcvR6l5kgdrhRuwPFHU7P5/A1w0BjByPHwpfTDKg==",
+      "version": "4.1.1",
+      "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz",
+      "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==",
       "requires": {
         "ms": "^2.1.1"
       }
@@ -1948,14 +1948,12 @@
         "balanced-match": {
           "version": "1.0.0",
           "bundled": true,
-          "dev": true,
-          "optional": true
+          "dev": true
         },
         "brace-expansion": {
           "version": "1.1.11",
           "bundled": true,
           "dev": true,
-          "optional": true,
           "requires": {
             "balanced-match": "^1.0.0",
             "concat-map": "0.0.1"
@@ -1970,20 +1968,17 @@
         "code-point-at": {
           "version": "1.1.0",
           "bundled": true,
-          "dev": true,
-          "optional": true
+          "dev": true
         },
         "concat-map": {
           "version": "0.0.1",
           "bundled": true,
-          "dev": true,
-          "optional": true
+          "dev": true
         },
         "console-control-strings": {
           "version": "1.1.0",
           "bundled": true,
-          "dev": true,
-          "optional": true
+          "dev": true
         },
         "core-util-is": {
           "version": "1.0.2",
@@ -2100,8 +2095,7 @@
         "inherits": {
           "version": "2.0.3",
           "bundled": true,
-          "dev": true,
-          "optional": true
+          "dev": true
         },
         "ini": {
           "version": "1.3.5",
@@ -2113,7 +2107,6 @@
           "version": "1.0.0",
           "bundled": true,
           "dev": true,
-          "optional": true,
           "requires": {
             "number-is-nan": "^1.0.0"
           }
@@ -2128,7 +2121,6 @@
           "version": "3.0.4",
           "bundled": true,
           "dev": true,
-          "optional": true,
           "requires": {
             "brace-expansion": "^1.1.7"
           }
@@ -2136,14 +2128,12 @@
         "minimist": {
           "version": "0.0.8",
           "bundled": true,
-          "dev": true,
-          "optional": true
+          "dev": true
         },
         "minipass": {
           "version": "2.2.4",
           "bundled": true,
           "dev": true,
-          "optional": true,
           "requires": {
             "safe-buffer": "^5.1.1",
             "yallist": "^3.0.0"
@@ -2162,7 +2152,6 @@
           "version": "0.5.1",
           "bundled": true,
           "dev": true,
-          "optional": true,
           "requires": {
             "minimist": "0.0.8"
           }
@@ -2243,8 +2232,7 @@
         "number-is-nan": {
           "version": "1.0.1",
           "bundled": true,
-          "dev": true,
-          "optional": true
+          "dev": true
         },
         "object-assign": {
           "version": "4.1.1",
@@ -2256,7 +2244,6 @@
           "version": "1.4.0",
           "bundled": true,
           "dev": true,
-          "optional": true,
           "requires": {
             "wrappy": "1"
           }
@@ -2378,7 +2365,6 @@
           "version": "1.0.2",
           "bundled": true,
           "dev": true,
-          "optional": true,
           "requires": {
             "code-point-at": "^1.0.0",
             "is-fullwidth-code-point": "^1.0.0",
@@ -3891,6 +3877,11 @@
         "node-sass": "^4.3.0"
       }
     },
+    "nodemailer": {
+      "version": "5.0.0",
+      "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-5.0.0.tgz",
+      "integrity": "sha512-XI4PI5L7GYcJyHkPcHlvPyRrYohNYBNRNbt1tU8PXNU3E1ADJC84a13V0vbL9AM431OP+ETacaGXAF8fGn1JvA=="
+    },
     "nodemon": {
       "version": "1.18.9",
       "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-1.18.9.tgz",
diff --git a/package.json b/package.json
index 622ca8ff..4a67cf93 100644
--- a/package.json
+++ b/package.json
@@ -7,11 +7,12 @@
   },
   "dependencies": {
     "cookie-parser": "~1.4.3",
-    "debug": "~4.1.0",
+    "debug": "^4.1.1",
     "express": "~4.16.4",
     "http-errors": "~1.7.1",
     "morgan": "~1.9.1",
     "node-sass-middleware": "~0.11.0",
+    "nodemailer": "^5.0.0",
     "pug": "~2.0.3",
     "sanitize-html": "^1.20.0",
     "serve-favicon": "~2.5.0",
diff --git a/public/javascripts/components/game.js b/public/javascripts/components/game.js
index fec3df54..c2cce62c 100644
--- a/public/javascripts/components/game.js
+++ b/public/javascripts/components/game.js
@@ -1480,17 +1480,15 @@ Vue.component('my-game', {
 	},
 })
 
-// TODO: keep moves list here
-get lastMove()
-	{
-		const L = this.moves.length;
-		return (L>0 ? this.moves[L-1] : null);
-	}
-
-// here too:
-			move.notation = this.getNotation(move);
-			// Hash of current game state *after move*, to detect repetitions
-			move.hash = hex_md5(this.getBaseFen() + this.getTurnFen() + this.getFlagsFen());
+//// TODO: keep moves list here
+//get lastMove()
+//	{
+//		const L = this.moves.length;
+//		return (L>0 ? this.moves[L-1] : null);
+//	}
+//
+//// here too:
+//			move.notation = this.getNotation(move);
 //TODO: confirm dialog with "opponent offers draw", avec possible bouton "prevent future offers" + bouton "proposer nulle"
 //+ bouton "abort" avec score == "?" + demander confirmation pour toutes ces actions,
 //comme sur lichess
diff --git a/public/javascripts/components/problems.js b/public/javascripts/components/problems.js
index 4caabb55..bcd069cc 100644
--- a/public/javascripts/components/problems.js
+++ b/public/javascripts/components/problems.js
@@ -1,7 +1,7 @@
 Vue.component('my-problems', {
 	data: function () {
 		return {
-			problems: problemArray, //initial value
+			problems: [],
 			newProblem: {
 				fen: "",
 				instructions: "",
@@ -71,14 +71,18 @@ Vue.component('my-problems', {
 			return this.problems.sort((p1,p2) => { return p2.added - p1.added; });
 		},
 	},
+	created: function() {
+		// TODO: fetch most recent problems from server
+	},
 	methods: {
 		translate: function(text) {
 			return translations[text];
 		},
-		// Propagate "show problem" event to parent component (my-variant)
-		bubbleUp: function(problem) {
-			this.$emit('show-problem', JSON.stringify(problem));
-		},
+		// TODO: obsolete:
+//		// Propagate "show problem" event to parent component (my-variant)
+//		bubbleUp: function(problem) {
+//			this.$emit('show-problem', JSON.stringify(problem));
+//		},
 		fetchProblems: function(direction) {
 			if (this.problems.length == 0)
 				return; //what could we do?!
diff --git a/public/javascripts/contactForm.js b/public/javascripts/contactForm.js
new file mode 100644
index 00000000..0c4fea3a
--- /dev/null
+++ b/public/javascripts/contactForm.js
@@ -0,0 +1,31 @@
+// Note: not using Vue, but would be possible
+function trySendMessage()
+{
+	let email = document.getElementById("userEmail");
+	let subject = document.getElementById("mailSubject");
+	let content = document.getElementById("mailContent");
+	if (!email.value.match(/^[^@]+@[^@]+\.[^@]+$/))
+		return alert("Bad email");
+	if (content.value.trim().length == 0)
+		return alert("Empty message");
+	if (subject.value.trim().length == 0 && !confirm("No subject. Send anyway?"))
+		return;
+
+	// Message sending:
+	ajax(
+		"/messages",
+		"POST",
+		{
+			email: email.value,
+			subject: subject.value,
+			content: content.value,
+		},
+		() => {
+			subject.value = "";
+			content.value = "";
+			let emailSent = document.getElementById("emailSent");
+			emailSent.style.display = "inline-block";
+			setTimeout(() => { emailSent.style.display = "none"; }, 2000);
+		}
+	);
+}
diff --git a/public/javascripts/index.js b/public/javascripts/index.js
index 64489262..6859e8bf 100644
--- a/public/javascripts/index.js
+++ b/public/javascripts/index.js
@@ -49,33 +49,33 @@ new Vue({
 		this.conn.onmessage = socketMessageListener;
 		this.conn.onclose = socketCloseListener;
 	},
-	mounted: function() {
-		// Handle key stroke
-		document.onkeydown = event => {
-			// Is it Back or Esc? If yes, apply action on current word
-			if (event.keyCode == 8) //Back
-			{
-				event.preventDefault();
-				this.curPrefix = this.curPrefix.slice(0,-1);
-			}
-			else if (event.keyCode == 27) //Esc
-			{
-				event.preventDefault();
-				this.curPrefix = "";
-			}
-			// Is it alphanumeric? If yes, stack it
-			else if (_.range(48,58).includes(event.keyCode)
-				|| _.range(65,91).includes(event.keyCode)
-				|| _.range(97,123).includes(event.keyCode))
-			{
-				let newChar = String.fromCharCode(event.keyCode);
-				this.curPrefix += this.curPrefix.length==0
-					? newChar.toUpperCase()
-					: newChar.toLowerCase();
-			}
-			// ...ignore everything else
-		};
-	},
+//	mounted: function() {
+//		// Handle key stroke
+//		document.onkeydown = event => {
+//			// Is it Back or Esc? If yes, apply action on current word
+//			if (event.keyCode == 8) //Back
+//			{
+//				event.preventDefault();
+//				this.curPrefix = this.curPrefix.slice(0,-1);
+//			}
+//			else if (event.keyCode == 27) //Esc
+//			{
+//				event.preventDefault();
+//				this.curPrefix = "";
+//			}
+//			// Is it alphanumeric? If yes, stack it
+//			else if (_.range(48,58).includes(event.keyCode)
+//				|| _.range(65,91).includes(event.keyCode)
+//				|| _.range(97,123).includes(event.keyCode))
+//			{
+//				let newChar = String.fromCharCode(event.keyCode);
+//				this.curPrefix += this.curPrefix.length==0
+//					? newChar.toUpperCase()
+//					: newChar.toLowerCase();
+//			}
+//			// ...ignore everything else
+//		};
+//	},
 });
 
 // TODO:
diff --git a/public/javascripts/utils/md5.js b/public/javascripts/utils/md5.js
deleted file mode 100644
index 24d190e4..00000000
--- a/public/javascripts/utils/md5.js
+++ /dev/null
@@ -1,379 +0,0 @@
-/*
- * A JavaScript implementation of the RSA Data Security, Inc. MD5 Message
- * Digest Algorithm, as defined in RFC 1321.
- * Version 2.2 Copyright (C) Paul Johnston 1999 - 2009
- * Other contributors: Greg Holt, Andrew Kepert, Ydnar, Lostinet
- * Distributed under the BSD License
- * See http://pajhome.org.uk/crypt/md5 for more info.
- */
-
-/*
- * Configurable variables. You may need to tweak these to be compatible with
- * the server-side, but the defaults work in most cases.
- */
-var hexcase = 0;   /* hex output format. 0 - lowercase; 1 - uppercase        */
-var b64pad  = "";  /* base-64 pad character. "=" for strict RFC compliance   */
-
-/*
- * These are the functions you'll usually want to call
- * They take string arguments and return either hex or base-64 encoded strings
- */
-function hex_md5(s)    { return rstr2hex(rstr_md5(str2rstr_utf8(s))); }
-function b64_md5(s)    { return rstr2b64(rstr_md5(str2rstr_utf8(s))); }
-function any_md5(s, e) { return rstr2any(rstr_md5(str2rstr_utf8(s)), e); }
-function hex_hmac_md5(k, d)
-  { return rstr2hex(rstr_hmac_md5(str2rstr_utf8(k), str2rstr_utf8(d))); }
-function b64_hmac_md5(k, d)
-  { return rstr2b64(rstr_hmac_md5(str2rstr_utf8(k), str2rstr_utf8(d))); }
-function any_hmac_md5(k, d, e)
-  { return rstr2any(rstr_hmac_md5(str2rstr_utf8(k), str2rstr_utf8(d)), e); }
-
-/*
- * Perform a simple self-test to see if the VM is working
- */
-function md5_vm_test()
-{
-  return hex_md5("abc").toLowerCase() == "900150983cd24fb0d6963f7d28e17f72";
-}
-
-/*
- * Calculate the MD5 of a raw string
- */
-function rstr_md5(s)
-{
-  return binl2rstr(binl_md5(rstr2binl(s), s.length * 8));
-}
-
-/*
- * Calculate the HMAC-MD5, of a key and some data (raw strings)
- */
-function rstr_hmac_md5(key, data)
-{
-  var bkey = rstr2binl(key);
-  if(bkey.length > 16) bkey = binl_md5(bkey, key.length * 8);
-
-  var ipad = Array(16), opad = Array(16);
-  for(var i = 0; i < 16; i++)
-  {
-    ipad[i] = bkey[i] ^ 0x36363636;
-    opad[i] = bkey[i] ^ 0x5C5C5C5C;
-  }
-
-  var hash = binl_md5(ipad.concat(rstr2binl(data)), 512 + data.length * 8);
-  return binl2rstr(binl_md5(opad.concat(hash), 512 + 128));
-}
-
-/*
- * Convert a raw string to a hex string
- */
-function rstr2hex(input)
-{
-  try { hexcase } catch(e) { hexcase=0; }
-  var hex_tab = hexcase ? "0123456789ABCDEF" : "0123456789abcdef";
-  var output = "";
-  var x;
-  for(var i = 0; i < input.length; i++)
-  {
-    x = input.charCodeAt(i);
-    output += hex_tab.charAt((x >>> 4) & 0x0F)
-           +  hex_tab.charAt( x        & 0x0F);
-  }
-  return output;
-}
-
-/*
- * Convert a raw string to a base-64 string
- */
-function rstr2b64(input)
-{
-  try { b64pad } catch(e) { b64pad=''; }
-  var tab = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
-  var output = "";
-  var len = input.length;
-  for(var i = 0; i < len; i += 3)
-  {
-    var triplet = (input.charCodeAt(i) << 16)
-                | (i + 1 < len ? input.charCodeAt(i+1) << 8 : 0)
-                | (i + 2 < len ? input.charCodeAt(i+2)      : 0);
-    for(var j = 0; j < 4; j++)
-    {
-      if(i * 8 + j * 6 > input.length * 8) output += b64pad;
-      else output += tab.charAt((triplet >>> 6*(3-j)) & 0x3F);
-    }
-  }
-  return output;
-}
-
-/*
- * Convert a raw string to an arbitrary string encoding
- */
-function rstr2any(input, encoding)
-{
-  var divisor = encoding.length;
-  var i, j, q, x, quotient;
-
-  /* Convert to an array of 16-bit big-endian values, forming the dividend */
-  var dividend = Array(Math.ceil(input.length / 2));
-  for(i = 0; i < dividend.length; i++)
-  {
-    dividend[i] = (input.charCodeAt(i * 2) << 8) | input.charCodeAt(i * 2 + 1);
-  }
-
-  /*
-   * Repeatedly perform a long division. The binary array forms the dividend,
-   * the length of the encoding is the divisor. Once computed, the quotient
-   * forms the dividend for the next step. All remainders are stored for later
-   * use.
-   */
-  var full_length = Math.ceil(input.length * 8 /
-                                    (Math.log(encoding.length) / Math.log(2)));
-  var remainders = Array(full_length);
-  for(j = 0; j < full_length; j++)
-  {
-    quotient = Array();
-    x = 0;
-    for(i = 0; i < dividend.length; i++)
-    {
-      x = (x << 16) + dividend[i];
-      q = Math.floor(x / divisor);
-      x -= q * divisor;
-      if(quotient.length > 0 || q > 0)
-        quotient[quotient.length] = q;
-    }
-    remainders[j] = x;
-    dividend = quotient;
-  }
-
-  /* Convert the remainders to the output string */
-  var output = "";
-  for(i = remainders.length - 1; i >= 0; i--)
-    output += encoding.charAt(remainders[i]);
-
-  return output;
-}
-
-/*
- * Encode a string as utf-8.
- * For efficiency, this assumes the input is valid utf-16.
- */
-function str2rstr_utf8(input)
-{
-  var output = "";
-  var i = -1;
-  var x, y;
-
-  while(++i < input.length)
-  {
-    /* Decode utf-16 surrogate pairs */
-    x = input.charCodeAt(i);
-    y = i + 1 < input.length ? input.charCodeAt(i + 1) : 0;
-    if(0xD800 <= x && x <= 0xDBFF && 0xDC00 <= y && y <= 0xDFFF)
-    {
-      x = 0x10000 + ((x & 0x03FF) << 10) + (y & 0x03FF);
-      i++;
-    }
-
-    /* Encode output as utf-8 */
-    if(x <= 0x7F)
-      output += String.fromCharCode(x);
-    else if(x <= 0x7FF)
-      output += String.fromCharCode(0xC0 | ((x >>> 6 ) & 0x1F),
-                                    0x80 | ( x         & 0x3F));
-    else if(x <= 0xFFFF)
-      output += String.fromCharCode(0xE0 | ((x >>> 12) & 0x0F),
-                                    0x80 | ((x >>> 6 ) & 0x3F),
-                                    0x80 | ( x         & 0x3F));
-    else if(x <= 0x1FFFFF)
-      output += String.fromCharCode(0xF0 | ((x >>> 18) & 0x07),
-                                    0x80 | ((x >>> 12) & 0x3F),
-                                    0x80 | ((x >>> 6 ) & 0x3F),
-                                    0x80 | ( x         & 0x3F));
-  }
-  return output;
-}
-
-/*
- * Encode a string as utf-16
- */
-function str2rstr_utf16le(input)
-{
-  var output = "";
-  for(var i = 0; i < input.length; i++)
-    output += String.fromCharCode( input.charCodeAt(i)        & 0xFF,
-                                  (input.charCodeAt(i) >>> 8) & 0xFF);
-  return output;
-}
-
-function str2rstr_utf16be(input)
-{
-  var output = "";
-  for(var i = 0; i < input.length; i++)
-    output += String.fromCharCode((input.charCodeAt(i) >>> 8) & 0xFF,
-                                   input.charCodeAt(i)        & 0xFF);
-  return output;
-}
-
-/*
- * Convert a raw string to an array of little-endian words
- * Characters >255 have their high-byte silently ignored.
- */
-function rstr2binl(input)
-{
-  var output = Array(input.length >> 2);
-  for(var i = 0; i < output.length; i++)
-    output[i] = 0;
-  for(var i = 0; i < input.length * 8; i += 8)
-    output[i>>5] |= (input.charCodeAt(i / 8) & 0xFF) << (i%32);
-  return output;
-}
-
-/*
- * Convert an array of little-endian words to a string
- */
-function binl2rstr(input)
-{
-  var output = "";
-  for(var i = 0; i < input.length * 32; i += 8)
-    output += String.fromCharCode((input[i>>5] >>> (i % 32)) & 0xFF);
-  return output;
-}
-
-/*
- * Calculate the MD5 of an array of little-endian words, and a bit length.
- */
-function binl_md5(x, len)
-{
-  /* append padding */
-  x[len >> 5] |= 0x80 << ((len) % 32);
-  x[(((len + 64) >>> 9) << 4) + 14] = len;
-
-  var a =  1732584193;
-  var b = -271733879;
-  var c = -1732584194;
-  var d =  271733878;
-
-  for(var i = 0; i < x.length; i += 16)
-  {
-    var olda = a;
-    var oldb = b;
-    var oldc = c;
-    var oldd = d;
-
-    a = md5_ff(a, b, c, d, x[i+ 0], 7 , -680876936);
-    d = md5_ff(d, a, b, c, x[i+ 1], 12, -389564586);
-    c = md5_ff(c, d, a, b, x[i+ 2], 17,  606105819);
-    b = md5_ff(b, c, d, a, x[i+ 3], 22, -1044525330);
-    a = md5_ff(a, b, c, d, x[i+ 4], 7 , -176418897);
-    d = md5_ff(d, a, b, c, x[i+ 5], 12,  1200080426);
-    c = md5_ff(c, d, a, b, x[i+ 6], 17, -1473231341);
-    b = md5_ff(b, c, d, a, x[i+ 7], 22, -45705983);
-    a = md5_ff(a, b, c, d, x[i+ 8], 7 ,  1770035416);
-    d = md5_ff(d, a, b, c, x[i+ 9], 12, -1958414417);
-    c = md5_ff(c, d, a, b, x[i+10], 17, -42063);
-    b = md5_ff(b, c, d, a, x[i+11], 22, -1990404162);
-    a = md5_ff(a, b, c, d, x[i+12], 7 ,  1804603682);
-    d = md5_ff(d, a, b, c, x[i+13], 12, -40341101);
-    c = md5_ff(c, d, a, b, x[i+14], 17, -1502002290);
-    b = md5_ff(b, c, d, a, x[i+15], 22,  1236535329);
-
-    a = md5_gg(a, b, c, d, x[i+ 1], 5 , -165796510);
-    d = md5_gg(d, a, b, c, x[i+ 6], 9 , -1069501632);
-    c = md5_gg(c, d, a, b, x[i+11], 14,  643717713);
-    b = md5_gg(b, c, d, a, x[i+ 0], 20, -373897302);
-    a = md5_gg(a, b, c, d, x[i+ 5], 5 , -701558691);
-    d = md5_gg(d, a, b, c, x[i+10], 9 ,  38016083);
-    c = md5_gg(c, d, a, b, x[i+15], 14, -660478335);
-    b = md5_gg(b, c, d, a, x[i+ 4], 20, -405537848);
-    a = md5_gg(a, b, c, d, x[i+ 9], 5 ,  568446438);
-    d = md5_gg(d, a, b, c, x[i+14], 9 , -1019803690);
-    c = md5_gg(c, d, a, b, x[i+ 3], 14, -187363961);
-    b = md5_gg(b, c, d, a, x[i+ 8], 20,  1163531501);
-    a = md5_gg(a, b, c, d, x[i+13], 5 , -1444681467);
-    d = md5_gg(d, a, b, c, x[i+ 2], 9 , -51403784);
-    c = md5_gg(c, d, a, b, x[i+ 7], 14,  1735328473);
-    b = md5_gg(b, c, d, a, x[i+12], 20, -1926607734);
-
-    a = md5_hh(a, b, c, d, x[i+ 5], 4 , -378558);
-    d = md5_hh(d, a, b, c, x[i+ 8], 11, -2022574463);
-    c = md5_hh(c, d, a, b, x[i+11], 16,  1839030562);
-    b = md5_hh(b, c, d, a, x[i+14], 23, -35309556);
-    a = md5_hh(a, b, c, d, x[i+ 1], 4 , -1530992060);
-    d = md5_hh(d, a, b, c, x[i+ 4], 11,  1272893353);
-    c = md5_hh(c, d, a, b, x[i+ 7], 16, -155497632);
-    b = md5_hh(b, c, d, a, x[i+10], 23, -1094730640);
-    a = md5_hh(a, b, c, d, x[i+13], 4 ,  681279174);
-    d = md5_hh(d, a, b, c, x[i+ 0], 11, -358537222);
-    c = md5_hh(c, d, a, b, x[i+ 3], 16, -722521979);
-    b = md5_hh(b, c, d, a, x[i+ 6], 23,  76029189);
-    a = md5_hh(a, b, c, d, x[i+ 9], 4 , -640364487);
-    d = md5_hh(d, a, b, c, x[i+12], 11, -421815835);
-    c = md5_hh(c, d, a, b, x[i+15], 16,  530742520);
-    b = md5_hh(b, c, d, a, x[i+ 2], 23, -995338651);
-
-    a = md5_ii(a, b, c, d, x[i+ 0], 6 , -198630844);
-    d = md5_ii(d, a, b, c, x[i+ 7], 10,  1126891415);
-    c = md5_ii(c, d, a, b, x[i+14], 15, -1416354905);
-    b = md5_ii(b, c, d, a, x[i+ 5], 21, -57434055);
-    a = md5_ii(a, b, c, d, x[i+12], 6 ,  1700485571);
-    d = md5_ii(d, a, b, c, x[i+ 3], 10, -1894986606);
-    c = md5_ii(c, d, a, b, x[i+10], 15, -1051523);
-    b = md5_ii(b, c, d, a, x[i+ 1], 21, -2054922799);
-    a = md5_ii(a, b, c, d, x[i+ 8], 6 ,  1873313359);
-    d = md5_ii(d, a, b, c, x[i+15], 10, -30611744);
-    c = md5_ii(c, d, a, b, x[i+ 6], 15, -1560198380);
-    b = md5_ii(b, c, d, a, x[i+13], 21,  1309151649);
-    a = md5_ii(a, b, c, d, x[i+ 4], 6 , -145523070);
-    d = md5_ii(d, a, b, c, x[i+11], 10, -1120210379);
-    c = md5_ii(c, d, a, b, x[i+ 2], 15,  718787259);
-    b = md5_ii(b, c, d, a, x[i+ 9], 21, -343485551);
-
-    a = safe_add(a, olda);
-    b = safe_add(b, oldb);
-    c = safe_add(c, oldc);
-    d = safe_add(d, oldd);
-  }
-  return Array(a, b, c, d);
-}
-
-/*
- * These functions implement the four basic operations the algorithm uses.
- */
-function md5_cmn(q, a, b, x, s, t)
-{
-  return safe_add(bit_rol(safe_add(safe_add(a, q), safe_add(x, t)), s),b);
-}
-function md5_ff(a, b, c, d, x, s, t)
-{
-  return md5_cmn((b & c) | ((~b) & d), a, b, x, s, t);
-}
-function md5_gg(a, b, c, d, x, s, t)
-{
-  return md5_cmn((b & d) | (c & (~d)), a, b, x, s, t);
-}
-function md5_hh(a, b, c, d, x, s, t)
-{
-  return md5_cmn(b ^ c ^ d, a, b, x, s, t);
-}
-function md5_ii(a, b, c, d, x, s, t)
-{
-  return md5_cmn(c ^ (b | (~d)), a, b, x, s, t);
-}
-
-/*
- * Add integers, wrapping at 2^32. This uses 16-bit operations internally
- * to work around bugs in some JS interpreters.
- */
-function safe_add(x, y)
-{
-  var lsw = (x & 0xFFFF) + (y & 0xFFFF);
-  var msw = (x >> 16) + (y >> 16) + (lsw >> 16);
-  return (msw << 16) | (lsw & 0xFFFF);
-}
-
-/*
- * Bitwise rotate a 32-bit number to the left.
- */
-function bit_rol(num, cnt)
-{
-  return (num << cnt) | (num >>> (32 - cnt));
-}
diff --git a/public/javascripts/variant.js b/public/javascripts/variant.js
index ae576d89..736d8b3d 100644
--- a/public/javascripts/variant.js
+++ b/public/javascripts/variant.js
@@ -2,6 +2,7 @@ new Vue({
 	el: "#variantPage",
 	data: {
 		display: "room", //default: main hall
+		gameid: "undefined", //...yet
 	},
 	created: function() {
 		// TODO: navigation becomes a little more complex
diff --git a/public/stylesheets/index.sass b/public/stylesheets/index.sass
index 0d7803f4..e38f87dd 100644
--- a/public/stylesheets/index.sass
+++ b/public/stylesheets/index.sass
@@ -14,7 +14,7 @@
     display: inline-block
     padding: 3px
     border: 1px solid black;
-    margin: 27px 15px 5px 7px
+    margin: 25px 15px 5px 7px
     @media screen and (max-width: 767px)
       margin-top: 7px
 
@@ -34,31 +34,19 @@
     font-weight: bold
     padding: 0
     border: none
-    margin-top: 22px
+    margin-top: 21px
     font-size: 1.5em
     @media screen and (max-width: 767px)
       margin-top: 10px
       font-size: 1em
 
-#helpMenu
+#settingsMenu, #introductionMenu
   float: right
   @media screen and (max-width: 767px)
     .info-container
       p
         margin-right: 5px
 
-#flagMenu
-  float: right
-  margin-right: 10px
-  @media screen and (max-width: 767px)
-    margin-right: 5px
-  img
-    display: inline-block
-    height: 30px
-    padding-top: 27px
-    @media screen and (max-width: 767px)
-      padding-top: 8px
-
 // TODO: box-shadow or box-sizing ? https://stackoverflow.com/a/13517809
 .variant
   box-sizing: border-box
@@ -76,11 +64,6 @@
     @media screen and (max-width: 767px)
       margin-top: 0
 
-#readThis
-  margin-top: 0
-  color: var(--a-link-color)
-  text-decoration: underline
-
 #welcome
   max-width: 767px
   @media screen and (max-width: 767px)
@@ -92,7 +75,7 @@
       max-width: 552px
   ul
     list-style-type: none
-  // TODO: bad practice, use table to align things...
+  // TODO: bad practice, shouldn't use table to align things...
   table.list-table
     width: 300px
     margin: 0 auto
@@ -107,5 +90,3 @@
           padding: 0
           text-align: left
           border: 0
-  #disableMsg
-    color: darkred
diff --git a/public/stylesheets/layout.sass b/public/stylesheets/layout.sass
index d4f6d6d0..dedab08e 100644
--- a/public/stylesheets/layout.sass
+++ b/public/stylesheets/layout.sass
@@ -7,7 +7,6 @@ html, *
 body
   padding: 0
   min-width: 320px
-  margin-bottom: 10px
 
 .container
   padding: 0
@@ -34,6 +33,35 @@ body
       border-left: 1px solid var(--button-group-border-color)
       border-top: 0
 
+#settings, #contactForm
+  max-width: 767px
+  @media screen and (max-width: 767px)
+    max-width: 100vw
+
+#emailSent
+  color: blue
+  display: none
+
+footer
+  height: 77px
+  background-color: #000033
+  div
+    line-height: 77px
+    a
+      margin: 0 10px 0 0
+      display: inline-block
+      &:visited, &:link
+        color: white
+    p
+      margin: 0 0 0 10px
+      display: inline-block
+      color: white
+      text-decoration: underline
+  @media screen and (max-width: 767px)
+    height: 43px
+    div
+      line-height: 43px
+
 a
   text-decoration: underline
 
diff --git a/routes/all.js b/routes/all.js
index 79f7c3d8..7c2a6da9 100644
--- a/routes/all.js
+++ b/routes/all.js
@@ -1,138 +1,8 @@
-let express = require('express');
-let router = express.Router();
-const createError = require('http-errors');
-const sqlite3 = require('sqlite3');//.verbose();
-const DbPath = __dirname.replace("/routes", "/db/vchess.sqlite");
-const db = new sqlite3.Database(DbPath);
-const sanitizeHtml = require('sanitize-html');
-const MaxNbProblems = 20;
+var router = require("express").Router();
 
-const supportedLang = ["en","es","fr"];
-function selectLanguage(req, res)
-{
-	// If preferred language already set:
-	if (!!req.cookies["lang"])
-		return req.cookies["lang"];
-
-	// Else: search and set it
-	const langString = req.headers["accept-language"];
-	let langArray = langString
-		.replace(/;q=[0-9.]+/g, "") //priority
-		.replace(/-[A-Z]+/g, "") //region (skipped for now...)
-		.split(",") //may have some duplicates, but removal is too costly
-	let bestLang = "en"; //default: English
-	for (let lang of langArray)
-	{
-		if (supportedLang.includes(lang))
-		{
-			bestLang = lang;
-			break;
-		}
-	}
-	// Cookie expires in 183 days (expressed in milliseconds)
-	res.cookie('lang', bestLang, { maxAge: 183*24*3600*1000 });
-	return bestLang;
-}
-
-// Home
-router.get('/', function(req, res, next) {
-	db.serialize(function() {
-		db.all("SELECT * FROM Variants", (err,variants) => {
-			if (!!err)
-				return next(err);
-			res.render('index', {
-				title: 'club',
-				variantArray: variants,
-				lang: selectLanguage(req, res),
-				languages: supportedLang,
-			});
-		});
-	});
-});
-
-// Variant
-router.get("/:variant([a-zA-Z0-9]+)", (req,res,next) => {
-	const vname = req.params["variant"];
-	db.serialize(function() {
-		db.all("SELECT * FROM Variants WHERE name='" + vname + "'", (err,variant) => {
-			if (!!err)
-				return next(err);
-			if (!variant || variant.length==0)
-				return next(createError(404));
-			// Get only N most recent problems
-			const query2 = "SELECT * FROM Problems " +
-				"WHERE variant='" + vname + "' " +
-				"ORDER BY added DESC " +
-				"LIMIT " + MaxNbProblems;
-			db.all(query2, (err2,problems) => {
-				if (!!err2)
-					return next(err2);
-				res.render('variant', {
-					title: vname + ' Variant',
-					variant: vname,
-					problemArray: problems,
-					lang: selectLanguage(req, res),
-					languages: supportedLang,
-				});
-			});
-		});
-	});
-});
-
-// Load a rules page (AJAX)
-router.get("/rules/:variant([a-zA-Z0-9]+)", (req,res) => {
-	if (!req.xhr)
-		return res.json({errmsg: "Unauthorized access"});
-	const lang = selectLanguage(req, res);
-	res.render("rules/" + req.params["variant"] + "/" + lang);
-});
-
-// Fetch N previous or next problems (AJAX)
-router.get("/problems/:variant([a-zA-Z0-9]+)", (req,res) => {
-	if (!req.xhr)
-		return res.json({errmsg: "Unauthorized access"});
-	const vname = req.params["variant"];
-	const directionStr = (req.query.direction == "forward" ? ">" : "<");
-	const lastDt = req.query.last_dt;
-	if (!lastDt.match(/[0-9]+/))
-		return res.json({errmsg: "Bad timestamp"});
-	db.serialize(function() {
-		const query = "SELECT * FROM Problems " +
-			"WHERE variant='" + vname + "' " +
-			"  AND added " + directionStr + " " + lastDt + " " +
-			"ORDER BY added " + (directionStr=="<" ? "DESC " : "") +
-			"LIMIT " + MaxNbProblems;
-		db.all(query, (err,problems) => {
-			if (!!err)
-				return res.json(err);
-			return res.json({problems: problems});
-		});
-	});
-});
-
-// Upload a problem (AJAX)
-router.post("/problems/:variant([a-zA-Z0-9]+)", (req,res) => {
-	if (!req.xhr)
-		return res.json({errmsg: "Unauthorized access"});
-	const vname = req.params["variant"];
-	const timestamp = Date.now();
-	// Sanitize them
-	const fen = req.body["fen"];
-	if (!fen.match(/^[a-zA-Z0-9, /-]*$/))
-		return res.json({errmsg: "Bad characters in FEN string"});
-	const instructions = sanitizeHtml(req.body["instructions"]).trim();
-	const solution = sanitizeHtml(req.body["solution"]).trim();
-	if (instructions.length == 0)
-		return res.json({errmsg: "Empty instructions"});
-	if (solution.length == 0)
-		return res.json({errmsg: "Empty solution"});
-	db.serialize(function() {
-		let stmt = db.prepare("INSERT INTO Problems " +
-			"(added,variant,fen,instructions,solution) VALUES (?,?,?,?,?)");
-		stmt.run(timestamp, vname, fen, instructions, solution);
-		stmt.finalize();
-	});
-  res.json({});
-});
+router.use("/", require("./index"));
+router.use("/", require("./variant"));
+router.use("/", require("./problems"));
+router.use("/", require("./messages"));
 
 module.exports = router;
diff --git a/routes/index.js b/routes/index.js
new file mode 100644
index 00000000..ab44cf5e
--- /dev/null
+++ b/routes/index.js
@@ -0,0 +1,21 @@
+let router = require("express").Router();
+const sqlite3 = require('sqlite3');//.verbose();
+const DbPath = __dirname.replace("/routes", "/db/vchess.sqlite");
+const db = new sqlite3.Database(DbPath);
+const selectLanguage = require(__dirname.replace("/routes", "/utils/language.js"));
+
+router.get('/', function(req, res, next) {
+	db.serialize(function() {
+		db.all("SELECT * FROM Variants", (err,variants) => {
+			if (!!err)
+				return next(err);
+			res.render('index', {
+				title: 'club',
+				variantArray: variants,
+				lang: selectLanguage(req, res),
+			});
+		});
+	});
+});
+
+module.exports = router;
diff --git a/routes/messages.js b/routes/messages.js
new file mode 100644
index 00000000..7e60b7cd
--- /dev/null
+++ b/routes/messages.js
@@ -0,0 +1,20 @@
+let router = require("express").Router();
+const sendEmail = require(__dirname.replace("/routes", "/utils/sendEmail"));
+
+// Send a message through contact form
+router.post("/messages", (req,res,next) => {
+	if (!req.xhr)
+		return res.json({errmsg: "Unauthorized access"});
+	const email = req.body["email"];
+	const subject = req.body["subject"];
+	const content = req.body["content"];
+	// TODO: sanitize ?
+	sendEmail(email, subject, content, err => {
+		if (!!err)
+			return res.json({errmsg:err});
+		// OK, everything fine
+		res.json({}); //ignored
+	});
+});
+
+module.exports = router;
diff --git a/routes/problems.js b/routes/problems.js
new file mode 100644
index 00000000..62a2da15
--- /dev/null
+++ b/routes/problems.js
@@ -0,0 +1,58 @@
+let router = require("express").Router();
+const sqlite3 = require('sqlite3');
+const DbPath = __dirname.replace("/routes", "/db/vchess.sqlite");
+const db = new sqlite3.Database(DbPath);
+const sanitizeHtml = require('sanitize-html');
+const MaxNbProblems = 20;
+
+// Fetch N previous or next problems (AJAX)
+router.get("/problems/:variant([a-zA-Z0-9]+)", (req,res) => {
+	if (!req.xhr)
+		return res.json({errmsg: "Unauthorized access"});
+	const vname = req.params["variant"];
+	const directionStr = (req.query.direction == "forward" ? ">" : "<");
+	const lastDt = req.query.last_dt;
+	if (!lastDt.match(/[0-9]+/))
+		return res.json({errmsg: "Bad timestamp"});
+	db.serialize(function() {
+		const query = "SELECT * FROM Problems " +
+			"WHERE variant='" + vname + "' " +
+			"  AND added " + directionStr + " " + lastDt + " " +
+			"ORDER BY added " + (directionStr=="<" ? "DESC " : "") +
+			"LIMIT " + MaxNbProblems;
+		db.all(query, (err,problems) => {
+			if (!!err)
+				return res.json(err);
+			return res.json({problems: problems});
+		});
+	});
+});
+
+// Upload a problem (AJAX)
+router.post("/problems/:variant([a-zA-Z0-9]+)", (req,res) => {
+	if (!req.xhr)
+		return res.json({errmsg: "Unauthorized access"});
+	const vname = req.params["variant"];
+	const timestamp = Date.now();
+	// Sanitize them
+	const fen = req.body["fen"];
+	if (!fen.match(/^[a-zA-Z0-9, /-]*$/))
+		return res.json({errmsg: "Bad characters in FEN string"});
+	const instructions = sanitizeHtml(req.body["instructions"]).trim();
+	const solution = sanitizeHtml(req.body["solution"]).trim();
+	if (instructions.length == 0)
+		return res.json({errmsg: "Empty instructions"});
+	if (solution.length == 0)
+		return res.json({errmsg: "Empty solution"});
+	db.serialize(function() {
+		let stmt = db.prepare("INSERT INTO Problems " +
+			"(added,variant,fen,instructions,solution) VALUES (?,?,?,?,?)");
+		stmt.run(timestamp, vname, fen, instructions, solution);
+		stmt.finalize();
+	});
+  res.json({});
+});
+
+// TODO: edit, delete a problem
+
+module.exports = router;
diff --git a/routes/variant.js b/routes/variant.js
new file mode 100644
index 00000000..44b7d804
--- /dev/null
+++ b/routes/variant.js
@@ -0,0 +1,33 @@
+let router = require("express").Router();
+const createError = require('http-errors');
+const sqlite3 = require('sqlite3');
+const DbPath = __dirname.replace("/routes", "/db/vchess.sqlite");
+const db = new sqlite3.Database(DbPath);
+const selectLanguage = require(__dirname.replace("/routes", "/utils/language.js"));
+
+router.get("/:variant([a-zA-Z0-9]+)", (req,res,next) => {
+	const vname = req.params["variant"];
+	db.serialize(function() {
+		db.all("SELECT * FROM Variants WHERE name='" + vname + "'", (err,variant) => {
+			if (!!err)
+				return next(err);
+			if (!variant || variant.length==0)
+				return next(createError(404));
+			res.render('variant', {
+				title: vname + ' Variant',
+				variant: vname,
+				lang: selectLanguage(req, res),
+			});
+		});
+	});
+});
+
+// Load a rules page (AJAX)
+router.get("/rules/:variant([a-zA-Z0-9]+)", (req,res) => {
+	if (!req.xhr)
+		return res.json({errmsg: "Unauthorized access"});
+	const lang = selectLanguage(req, res);
+	res.render("rules/" + req.params["variant"] + "/" + lang);
+});
+
+module.exports = router;
diff --git a/utils/language.js b/utils/language.js
new file mode 100644
index 00000000..52c3842d
--- /dev/null
+++ b/utils/language.js
@@ -0,0 +1,27 @@
+// Select a language based on browser preferences, or cookie
+module.exports = function(req, res)
+{
+	// If preferred language already set:
+	if (!!req.cookies["lang"])
+		return req.cookies["lang"];
+
+	// Else: search and set it
+	const supportedLang = ["en","es","fr"];
+	const langString = req.headers["accept-language"];
+	let langArray = langString
+		.replace(/;q=[0-9.]+/g, "") //priority
+		.replace(/-[A-Z]+/g, "") //region (skipped for now...)
+		.split(",") //may have some duplicates, but removal is too costly
+	let bestLang = "en"; //default: English
+	for (let lang of langArray)
+	{
+		if (supportedLang.includes(lang))
+		{
+			bestLang = lang;
+			break;
+		}
+	}
+	// Cookie expires in 183 days (expressed in milliseconds)
+	res.cookie('lang', bestLang, { maxAge: 183*24*3600*1000 });
+	return bestLang;
+}
diff --git a/utils/sendEmail.js.dist b/utils/sendEmail.js.dist
new file mode 100644
index 00000000..cad123e8
--- /dev/null
+++ b/utils/sendEmail.js.dist
@@ -0,0 +1,32 @@
+const nodemailer = require('nodemailer');
+
+module.exports = function(email, subject, content, cb)
+{
+  // create reusable transporter object using the default SMTP transport
+	const transporter = nodemailer.createTransport({
+		host: "smtp_host_address",
+		port: 465, //if secure; otherwise use 587
+		secure: true,
+		auth: {
+			user: "user_name",
+			pass: "user_password"
+		}
+	});
+
+	// setup email data with unicode symbols
+	const mailOptions = {
+		from: email, //note: some SMTP serves might forbid this
+		to: "contact_email",
+		subject: subject,
+		text: content,
+  };
+
+	// send mail with defined transport object
+	transporter.sendMail(mailOptions, (error, info) => {
+		if (!!error)
+			return cb(error);
+    // Ignore info. Option:
+		//console.log('Message sent: %s', info.messageId);
+		return cb();
+  });
+};
diff --git a/views/contactForm.pug b/views/contactForm.pug
new file mode 100644
index 00000000..a07369e3
--- /dev/null
+++ b/views/contactForm.pug
@@ -0,0 +1,18 @@
+input#modalContact.modal(type="checkbox")
+div(role="dialog" aria-labelledby="contactTitle")
+	form.card.smallpad
+		label.modal-close(for="modalContact")
+		h3#contactTitle.section= translations["Contact form"]
+		fieldset
+			label(for="userEmail")= translations["Email"]
+			input#userEmail(type="email")
+		fieldset
+			label(for="mailSubject")= translations["Subject"]
+			input#mailSubject(type="text")
+		fieldset
+			label(for="mailContent")= translations["Content"]
+			br
+			textarea#mailContent
+		fieldset
+			button(type="button" onClick="trySendMessage()") Send
+			p#emailSent= translations["Email sent!"]
diff --git a/views/index.pug b/views/index.pug
index 5d27eb8a..dd095931 100644
--- a/views/index.pug
+++ b/views/index.pug
@@ -14,21 +14,23 @@ block content
 				include welcome/fr
 		.row
 			#header.col-sm-12.col-md-10.col-md-offset-1.col-lg-8.col-lg-offset-2
-				#mainTitle.clickable(
-						onClick="document.getElementById('modalWelcome').checked=true")
+				#mainTitle
 					img(src="/images/index/unicorn.svg")
 					.info-container
 						p vchess.club
 					img(src="/images/index/wildebeest.svg")
-				#settings.clickable(
+				#settingsMenu.clickable(
 						onClick="document.getElementById('modalSettings').checked=true")
-					i.material-icons settings
+					.info-container
+						p Settings
+				#introductionMenu.clickable(
+						onClick="document.getElementById('modalWelcome').checked=true")
+					.info-container
+						p Introduction
 		.row
 			my-variant-summary(v-for="(v,idx) in sortedCounts"
 				v-bind:vobj="v" v-bind:index="idx" v-bind:key="v.name")
 
-					redesign index page :: lien github, lien contact mail, settings
-
 block javascripts
 	script.
 		const variantArray = !{JSON.stringify(variantArray)};
diff --git a/views/layout.pug b/views/layout.pug
index d894ff0c..da99fc31 100644
--- a/views/layout.pug
+++ b/views/layout.pug
@@ -25,20 +25,29 @@ html
 
 	body
 
+		include langNames
+		case lang
+			when "en"
+				include translations/en
+			when "es"
+				include translations/es
+			when "fr"
+				include translations/fr
+		include contactForm
+		include settings
 		main
-			include langNames
-			case lang
-				when "en"
-					include translations/en
-				when "es"
-					include translations/es
-				when "fr"
-					include translations/fr
-			include settings
 			block content
+		footer.col-sm-12.col-md-10.col-md-offset-1.col-lg-8.col-lg-offset-2.text-center
+			div
+				a(href="https://github.com/yagu0/vchess") Source code
+				p.clickable(onClick="document.getElementById('modalContact').checked=true")
+					=translations["Contact"]
 
 		script(src="//cdnjs.cloudflare.com/ajax/libs/underscore.js/1.9.1/underscore-min.js")
+		script(src="/javascripts/utils/ajax.js")
 		script(src="/javascripts/layout.js")
+		script(src="/javascripts/settings.js")
+		script(src="/javascripts/contactForm.js")
 		if development
 			script(src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js")
 		else
diff --git a/views/settings.pug b/views/settings.pug
index c46c5f8f..063e61a1 100644
--- a/views/settings.pug
+++ b/views/settings.pug
@@ -1,15 +1,15 @@
-input#modal-settings.modal(type="checkbox")
+input#modalSettings.modal(type="checkbox")
 div(role="dialog" aria-labelledby="settingsTitle")
 	.card.smallpad(onChange="blabla(event)")
-		label#close-settings.modal-close(for="modal-settings")
+		label.modal-close(for="modalSettings")
 		h3#settingsTitle.section= translations["Preferences"]
 		fieldset
 			label(for="langSelect")= translations["Language"]
 			// image avec drapeau + select language ici
 			select#langSelect
-				each langCode in languages
+				each language,langCode in langName
 					option(value=langCode selected=(lang==langCode))
-						=langName[langCode]
+						=language
 		fieldset
 			label(for="nameSetter")
 				=translations["My name is..."]
diff --git a/views/translations/en.pug b/views/translations/en.pug
index fe67aba9..96633111 100644
--- a/views/translations/en.pug
+++ b/views/translations/en.pug
@@ -2,6 +2,13 @@
 	var translations =
 	{
 		"Language": "Language",
+		"Contact": "Contact",
+		"Email": "Email",
+		"Subject": "Subject",
+		"Content": "Content",
+		"Email sent!": "Email sent!",
+		"Hall": "Hall",
+		"My games": "My games",
 
 		// Index page:
 		"Help": "Help",
diff --git a/views/variant.pug b/views/variant.pug
index ca9091bf..a43ba601 100644
--- a/views/variant.pug
+++ b/views/variant.pug
@@ -16,7 +16,7 @@ block content
 					a(href="#room" @click="setDisplay('room')")
 						=translations["Hall"]
 					a(href="#gameList" @click="setDisplay('gameList')")
-						=translations["Play"]
+						=translations["My games"]
 					a(href="#rules" @click="setDisplay('rules')")
 						=translations["Rules"]
 					a(href="#problems" @click="setDisplay('problems')")
@@ -30,14 +30,12 @@ block content
 			my-rules(v-show="display=='rules'")
 			my-problems(v-show="display=='problems'")
 			// my-game: for room and games-list components
-			my-game(v-show="display=='game'" :gameId="")
+			my-game(v-show="display=='game'" :gameId="gameid")
 
 block javascripts
 	script(src="/javascripts/utils/misc.js")
 	script(src="/javascripts/utils/array.js")
-	script(src="/javascripts/utils/md5.js")
 	script(src="/javascripts/utils/printDiagram.js")
-	script(src="/javascripts/utils/ajax.js")
 	script(src="/javascripts/utils/datetime.js")
 	script(src="/javascripts/socket_url.js")
 	script(src="/javascripts/base_rules.js")
@@ -45,7 +43,6 @@ block javascripts
 	script.
 		const V = VariantRules; //because this variable is often used
 		const variant = "#{variant}";
-		const problemArray = !{JSON.stringify(problemArray)};
 	script(src="/javascripts/components/rules.js")
 	script(src="/javascripts/components/game.js")
 	script(src="/javascripts/components/problemSummary.js")
-- 
2.44.0