From c66a829b3770122fe0ff2fb9db8def9635bbc334 Mon Sep 17 00:00:00 2001
From: Benjamin Auder <benjamin.auder@somewhere>
Date: Wed, 30 Jan 2019 17:33:22 +0100
Subject: [PATCH] Apply store pattern to track global app state

---
 client/src/App.vue                            | 29 +++---
 client/src/components/ContactForm.vue         | 18 ++--
 client/src/components/Language.vue            | 13 ++-
 client/src/components/Settings.vue            | 53 +++++-----
 .../components/UpsertUser.vue}                | 97 +++++++++----------
 client/src/main.js                            | 81 +++++-----------
 client/src/store.js                           | 56 +++++++++++
 client/src/utils/alea.js                      | 36 +++++++
 client/src/utils/array.js                     |  5 +
 client/src/utils/cookie.js                    | 22 +++++
 client/src/utils/misc.js                      | 74 --------------
 client/src/welcome/en.pug                     | 12 +--
 12 files changed, 262 insertions(+), 234 deletions(-)
 rename client/{next_src/components/upsertUser.js => src/components/UpsertUser.vue} (61%)
 create mode 100644 client/src/store.js
 create mode 100644 client/src/utils/alea.js
 create mode 100644 client/src/utils/cookie.js
 delete mode 100644 client/src/utils/misc.js

diff --git a/client/src/App.vue b/client/src/App.vue
index 7b3fb8e5..2b131276 100644
--- a/client/src/App.vue
+++ b/client/src/App.vue
@@ -3,11 +3,12 @@
   // modal "welcome" will be filled in the selected language
   #modalWelcome
   Language
-  Settings(:settings="settings")
+  Settings
   ContactForm
+  UpsertUser
   .container
     .row(v-show="$route.path == '/'")
-      // Header (on index only)
+      // Header (on index only ?!)
       header
         .col-sm-12.col-md-10.col-md-offset-1.col-lg-8.col-lg-offset-2
           img(src="./assets/images/index/unicorn.svg")
@@ -27,20 +28,21 @@
             // select options all variants + filter possible (as in problems)
             | Home
           router-link(to="/myGames")
-            | {{ $tr["My games"] }}
+            | {{ st.tr["My games"] }}
           router-link(to="/rules")
             // Boxes OK for rules/Atomic/ ...etc
-            | {{ $tr["Rules"] }}
+            | {{ st.tr["Rules"] }}
           router-link(to="/problems")
-            | {{ $tr["Problems"] }}
+            | {{ st.tr["Problems"] }}
           #userMenu.clickable.right-menu(onClick="doClick('modalUser')")
             .info-container
               p
-                span {{ !$user.email ? "Login" : "Update" }}
+                span {{ !st.user.id ? "Login" : "Update" }}
                 span.icon-user
           #flagMenu.clickable.right-menu(onClick="doClick('modalLang')")
           img(src="/images/flags/" + lang + ".svg")
         #settings.clickable(onClick="doClick('modalSettings')")
+          | Settings
           i(data-feather="settings")
     .row
       router-view
@@ -49,7 +51,7 @@
         .col-sm-12.col-md-10.col-md-offset-1.col-lg-8.col-lg-offset-2.text-center
           a(href="https://github.com/yagu0/vchess") Source code
           p.clickable(onClick="doClick('modalContact')")
-            | {{ $tr["Contact form"] }}
+            | {{ st.tr["Contact form"] }}
   //my-game(:game-ref="gameRef" :mode="mode" :settings="settings" @game-over="archiveGame")
   //// TODO: add only the necessary icons to mini-css custom build
   //script(src="//unpkg.com/feather-icons")
@@ -60,16 +62,19 @@
 import ContactForm from "@/components/ContactForm.vue";
 import Language from "@/components/Language.vue";
 import Settings from "@/components/Settings.vue";
+import UpsertUser from "@/components/UpsertUser.vue";
+import { store } from "./store.js";
 export default {
-  data: function() {
-    return {
-      settings: {}, //TODO
-    };
-  },
   components: {
     ContactForm,
     Language,
     Settings,
+    UpsertUser,
+  },
+  data: function() {
+    return {
+      st: store.state,
+    };
   },
 };
 </script>
diff --git a/client/src/components/ContactForm.vue b/client/src/components/ContactForm.vue
index a521e6aa..20d35b0e 100644
--- a/client/src/components/ContactForm.vue
+++ b/client/src/components/ContactForm.vue
@@ -4,26 +4,32 @@ div
   div(role="dialog" aria-labelledby="contactTitle")
     form.card.smallpad
       label.modal-close(for="modalContact")
-      h3#contactTitle.section {{ $tr["Contact form"] }}
+      h3#contactTitle.section {{ st.tr["Contact form"] }}
       fieldset
-        label(for="userEmail") {{ $tr["Email"] }}
+        label(for="userEmail") {{ st.tr["Email"] }}
         input#userEmail(type="email")
       fieldset
-        label(for="mailSubject") {{ $tr["Subject"] }}
+        label(for="mailSubject") {{ st.tr["Subject"] }}
         input#mailSubject(type="text")
       fieldset
-        label(for="mailContent") {{ $tr["Content"] }}
+        label(for="mailContent") {{ st.tr["Content"] }}
         br
         textarea#mailContent
       fieldset
         button(type="button" onClick="trySendMessage()") Send
-        p#emailSent {{ $tr["Email sent!"] }}
+        p#emailSent {{ st.tr["Email sent!"] }}
 </template>
 
 <script>
 import { ajax } from "../utils/ajax";
+import { store } from "@/store";
 export default {
-  name: "ContactForm",
+  name: "my-contact-form",
+  data: function() {
+    return {
+      st: store.state,
+    };
+  },
   methods: {
 		// Note: not using Vue here, but would be possible
     trySendMessage: function() {
diff --git a/client/src/components/Language.vue b/client/src/components/Language.vue
index 35c717ff..fc305620 100644
--- a/client/src/components/Language.vue
+++ b/client/src/components/Language.vue
@@ -12,7 +12,7 @@ div
       label.modal-close(for="modalLang")
       form
         fieldset
-          label(for="langSelect") {{ $tr["Language"] }}
+          label(for="langSelect") {{ st.tr["Language"] }}
           select#langSelect
             each language,langCode in langName
               option(value=langCode selected=(lang==langCode))
@@ -20,13 +20,18 @@ div
 </template>
 
 <script>
+import { store } from "@/store";
 export default {
-  name: "Language",
+  name: "my-language",
+  data: function() {
+    return {
+      st: store.state,
+    };
+  },
 	methods: {
-    // Used both on index and variant page, to switch language
     setLanguage: function(e) {
       localStorage["lang"] = e.target.value;
-      this.$lang = e.target.value;
+      store.setLanguage(e.target.value);
     },
 	},
 };
diff --git a/client/src/components/Settings.vue b/client/src/components/Settings.vue
index 7aa0162e..318689c4 100644
--- a/client/src/components/Settings.vue
+++ b/client/src/components/Settings.vue
@@ -4,40 +4,45 @@ div
   div(role="dialog" aria-labelledby="settingsTitle")
     .card.smallpad(@change="updateSettings")
       label.modal-close(for="modalSettings")
-      h3#settingsTitle.section {{ $tr["Preferences"] }}
+      h3#settingsTitle.section {{ st.tr["Preferences"] }}
       fieldset
-        label(for="setSqSize") {{ $tr["Square size (in pixels). 0 for 'adaptative'"] }}
-        input#setSqSize(type="number" v-model="$settings.sqSize")
+        label(for="setSqSize") {{ st.tr["Square size (in pixels). 0 for 'adaptative'"] }}
+        input#setSqSize(type="number" v-model="st.settings.sqSize")
       fieldset
-        label(for="selectHints") {{ $tr["Show move hints?"] }}
-        select#setHints(v-model="$settings.hints")
-          option(value="0") {{ $tr["None"] }}
-          option(value="1") {{ $tr["Moves from a square"] }}
-          option(value="2") {{ $tr["Pieces which can move"] }}
+        label(for="selectHints") {{ st.tr["Show move hints?"] }}
+        select#setHints(v-model="st.settings.hints")
+          option(value="0") {{ st.tr["None"] }}
+          option(value="1") {{ st.tr["Moves from a square"] }}
+          option(value="2") {{ st.tr["Pieces which can move"] }}
       fieldset
-        label(for="setHighlight") {{ $tr["Highlight squares? (Last move & checks)"] }}
-        input#setHighlight(type="checkbox" v-model="$settings.highlight")
+        label(for="setHighlight") {{ st.tr["Highlight squares? (Last move & checks)"] }}
+        input#setHighlight(type="checkbox" v-model="st.settings.highlight")
       fieldset
-        label(for="setCoords") {{ $tr["Show board coordinates?"] }}
-        input#setCoords(type="checkbox" v-model="$settings.coords")
+        label(for="setCoords") {{ st.tr["Show board coordinates?"] }}
+        input#setCoords(type="checkbox" v-model="st.settings.coords")
       fieldset
-        label(for="selectColor") {{ $tr["Board colors"] }}
-        select#setBcolor(v-model="$settings.bcolor")
-          option(value="lichess") {{ $tr["brown"] }}
-          option(value="chesscom") {{ $tr["green"] }}
-          option(value="chesstempo") {{ $tr["blue"] }}
+        label(for="selectColor") {{ st.tr["Board colors"] }}
+        select#setBcolor(v-model="st.settings.bcolor")
+          option(value="lichess") {{ st.tr["brown"] }}
+          option(value="chesscom") {{ st.tr["green"] }}
+          option(value="chesstempo") {{ st.tr["blue"] }}
       fieldset
-        label(for="selectSound") {{ $tr["Play sounds?"] }}
-        select#setSound(v-model="$settings.sound")
-          option(value="0") {{ $tr["None"] }}
-          option(value="1") {{ $tr["New game"] }}
-          option(value="2") {{ $tr["All"] }}
+        label(for="selectSound") {{ st.tr["Play sounds?"] }}
+        select#setSound(v-model="st.settings.sound")
+          option(value="0") {{ st.tr["None"] }}
+          option(value="1") {{ st.tr["New game"] }}
+          option(value="2") {{ st.tr["All"] }}
 </template>
 
 <script>
+import { store } from "@/store.js";
 export default {
-  name: "Settings",
-  //props: ["settings"],
+  name: "my-settings",
+  data: function() {
+    return {
+      st: store.state,
+    };
+  },
 	methods: {
     updateSettings: function(event) {
       const propName =
diff --git a/client/next_src/components/upsertUser.js b/client/src/components/UpsertUser.vue
similarity index 61%
rename from client/next_src/components/upsertUser.js
rename to client/src/components/UpsertUser.vue
index cca959e5..5ea93a05 100644
--- a/client/next_src/components/upsertUser.js
+++ b/client/src/components/UpsertUser.vue
@@ -1,61 +1,50 @@
 // Logic to login, or create / update a user (and also logout)
-vv = Vue.component('my-upsert-user', {
+<template lang="pug">
+div
+  input#modalUser.modal(type="checkbox" @change="trySetEnterTime")
+  div(role="dialog")
+    .card
+      label.modal-close(for="modalUser")
+      h3 {{ stage }}
+      form#userForm(@submit.prevent="onSubmit()")
+        div(v-show="stage!='Login'")
+          fieldset
+            label(for="username") Name
+            input#username(type="text" v-model="user.name")
+          fieldset
+            <label for="useremail">Email</label>
+            <input id="useremail" type="email" v-model="user.email"/>
+          fieldset
+            <label for="notifyNew">Notify new moves &amp; games</label>
+            <input id="notifyNew" type="checkbox" v-model="user.notify"/>
+        div(v-show="stage=='Login'")
+          fieldset
+            <label for="nameOrEmail">Name or Email</label>
+            <input id="nameOrEmail" type="text" v-model="nameOrEmail"/>
+      .button-group
+        button#submit(@click="onSubmit()")
+          span {{ submitMessage }}
+          i.material-icons send
+        button(v-if="stage!='Update'" @click="toggleStage()")
+          span {{ stage=="Login" ? "Register" : "Login" }}
+        button(v-if="stage=='Update'" onClick="location.replace('/logout')")
+          span Logout
+      #dialog(:style="{display: displayInfo}") {{ infoMsg }}
+</template>
+
+<script>
+import { store } from "@/store";
+export default {
+  name: 'my-upsert-user',
 	data: function() {
 		return {
-			user: user, //initialized with global user object
+			user: store.state.user, //initialized with global user object
 			nameOrEmail: "", //for login
-			stage: (!user.email ? "Login" : "Update"),
+			stage: (!store.state.user.id ? "Login" : "Update"),
 			infoMsg: "",
 			enterTime: Number.MAX_SAFE_INTEGER, //for a basic anti-bot strategy
 		};
 	},
-	template: `
-		<div>
-			<input id="modalUser" class="modal" type="checkbox"
-					@change="trySetEnterTime"/>
-			<div role="dialog">
-				<div class="card">
-					<label class="modal-close" for="modalUser"></label>
-					<h3>{{ stage }}</h3>
-					<form id="userForm" @submit.prevent="onSubmit()">
-						<div v-show="stage!='Login'">
-							<fieldset>
-								<label for="username">Name</label>
-								<input id="username" type="text" v-model="user.name"/>
-							</fieldset>
-							<fieldset>
-								<label for="useremail">Email</label>
-								<input id="useremail" type="email" v-model="user.email"/>
-							</fieldset>
-							<fieldset>
-								<label for="notifyNew">Notify new moves &amp; games</label>
-								<input id="notifyNew" type="checkbox" v-model="user.notify"/>
-							</fieldset>
-						</div>
-						<div v-show="stage=='Login'">
-							<fieldset>
-								<label for="nameOrEmail">Name or Email</label>
-								<input id="nameOrEmail" type="text" v-model="nameOrEmail"/>
-							</fieldset>
-						</div>
-					</form>
-					<div class="button-group">
-						<button id="submit" @click="onSubmit()">
-							<span>{{ submitMessage }}</span>
-							<i class="material-icons">send</i>
-						</button>
-						<button v-if="stage!='Update'" @click="toggleStage()">
-							<span>{{ stage=="Login" ? "Register" : "Login" }}</span>
-						</button>
-						<button v-if="stage=='Update'" onClick="location.replace('/logout')">
-							<span>Logout</span>
-						</button>
-					</div>
-					<div id="dialog" :style="{display: displayInfo}">{{ infoMsg }}</div>
-				</div>
-			</div>
-		</div>
-	`,
 	computed: {
 		submitMessage: function() {
 			switch (this.stage)
@@ -142,6 +131,9 @@ vv = Vue.component('my-upsert-user', {
 						// Store our identifiers in local storage (by little anticipation...)
 						localStorage["myid"] = res.id;
 						localStorage["myname"] = res.name;
+            // Also in global object
+            this.$user.id = res.id;
+            this.$user.name = res.name;
 					}
 					setTimeout(() => {
 						this.infoMsg = "";
@@ -156,5 +148,6 @@ vv = Vue.component('my-upsert-user', {
 				}
 			);
 		},
-	}
-});
+	},
+};
+</script>
diff --git a/client/src/main.js b/client/src/main.js
index 66517004..46d99abe 100644
--- a/client/src/main.js
+++ b/client/src/main.js
@@ -1,9 +1,8 @@
 import Vue from "vue";
 import App from "./App.vue";
 import router from "./router";
-import params from "./parameters"; //for socket connection
-import { ajax } from "./utils/ajax";
-import { util } from "./utils/misc";
+// Global store: see https://medium.com/fullstackio/managing-state-in-vue-js-23a0352b1c87
+import { store } from "./store";
 
 Vue.config.productionTip = false;
 
@@ -13,67 +12,37 @@ new Vue({
     return h(App);
   },
 //  watch: {
-//    $lang: async function(newLang) {
-//      // Fill modalWelcome, and import translations from "./translations/$lang.js"
-//      document.getElementById("modalWelcome").innerHTML =
-//        require("raw-loader!pug-plain-loader!./modals/welcome/" + newLang + ".pug");
-//      const tModule = await import("./translations/" + newLang + ".js");
-//      Vue.prototype.$tr = tModule.translations;
-//      //console.log(tModule.translations);
-//    },
 //    $route: function(newRoute) {
 //      //console.log(this.$route.params);
 //      console.log("navig to " + newRoute);
 //      //TODO: conn.send("enter", newRoute)
 //    },
 //  },
-	created: function() {
-    const supportedLangs = ["en","es","fr"];
-    Vue.prototype.$lang = localStorage["lang"] ||
-      supportedLangs.includes(navigator.language)
-        ? navigator.language
-        : "en";
-		Vue.prototype.$variants = []; //avoid runtime error
-		ajax("/variants", "GET", res => { Vue.prototype.$variants = res.variantArray; });
-    Vue.prototype.$tr = {}; //to avoid a compiler error
-		Vue.prototype.$user = {}; //TODO: from storage
-		// TODO: if there is a socket ID in localStorage, it means a live game was interrupted (and should resume)
-		const myid = localStorage["myid"] || util.getRandString();
-		// NOTE: in this version, we don't say on which page we are, yet
-		// ==> we'll say "enter/leave" page XY (in fact juste "enter", seemingly)
-		Vue.prototype.$conn = new WebSocket(params.socketUrl + "/?sid=" + myid);
-		// Settings initialized with values from localStorage
-		Vue.prototype.$settings = {
-			bcolor: localStorage["bcolor"] || "lichess",
-			sound: parseInt(localStorage["sound"]) || 2,
-			hints: parseInt(localStorage["hints"]) || 1,
-			coords: !!eval(localStorage["coords"]),
-			highlight: !!eval(localStorage["highlight"]),
-			sqSize: parseInt(localStorage["sqSize"]),
-		};
-		const socketCloseListener = () => {
-			Vue.prototype.$conn = new WebSocket(params.socketUrl + "/?sid=" + myid);
-		}
-		Vue.prototype.$conn.onclose = socketCloseListener;
-		//TODO: si une partie en cours dans storage, rediriger vers cette partie
-		//(à condition que l'URL n'y corresponde pas déjà !)
-		// TODO: à l'arrivée sur le site : set peerID (un identifiant unique
-		// en tout cas...) si pas trouvé dans localStorage "myid"
-		// (l'identifiant de l'utilisateur si connecté)
-//		if (!!localStorage["variant"])
-//			location.hash = "#game?id=" + localStorage["gameId"];
-	},
-	// Later, for icons (if using feather):
-//	mounted: function() {
-//		feather.replace();
-//	},
+  created: function() {
+    window.doClick = (elemId) => { document.getElementById(elemId).click() };
+
+    //TODO: si une partie en cours dans storage, rediriger vers cette partie
+    //(à condition que l'URL n'y corresponde pas déjà !)
+    // TODO: à l'arrivée sur le site : set peerID (un identifiant unique
+    // en tout cas...) si pas trouvé dans localStorage "myid"
+    // (l'identifiant de l'utilisateur si connecté)
+//    if (!!localStorage["variant"])
+//      location.hash = "#game?id=" + localStorage["gameId"];
+  },
+  // Later, for icons (if using feather):
+//  mounted: function() {
+//    feather.replace();
+//  },
+  mounted: function() {
+    store.initialize();
+  },
 }).$mount("#app");
 
 // TODO: get rules, dynamic import
 // Load a rules page (AJAX)
 // router.get("/rules/:vname([a-zA-Z0-9]+)", access.ajax, (req,res) => {
-//	const lang = selectLanguage(req, res);
-//	res.render("rules/" + req.params["vname"] + "/" + lang);
+//  const lang = selectLanguage(req, res);
+//  res.render("rules/" + req.params["vname"] + "/" + lang);
 // });
 //
 // board2, 3, 4 automatiquement, mais rules separement (les 3 pour une)
@@ -81,7 +50,7 @@ new Vue({
 // problems: on-demand
 //
 // See https://router.vuejs.org/guide/essentials/dynamic-matching.html#reacting-to-params-changes
-//	created: function() {
-//		window.onhashchange = this.setDisplay;
-//	},
+//  created: function() {
+//    window.onhashchange = this.setDisplay;
+//  },
 //});
diff --git a/client/src/store.js b/client/src/store.js
new file mode 100644
index 00000000..e8f4284b
--- /dev/null
+++ b/client/src/store.js
@@ -0,0 +1,56 @@
+import { ajax } from "./utils/ajax";
+import { getRandString } from "./utils/alea";
+import params from "./parameters"; //for socket connection
+
+export const store =
+{
+  state: {
+    variants: [],
+    tr: {},
+    user: {},
+    conn: null,
+    settings: {},
+    lang: "",
+  },
+  initialize() {
+    ajax("/variants", "GET", res => { this.state.variants = res.variantArray; });
+    this.state.user = {
+      // id and name could be undefined
+      id: localStorage["myuid"],
+      name: localStorage["myname"],
+    };
+    // TODO: if there is a socket ID in localStorage, it means a live game was interrupted (and should resume)
+    const mysid = localStorage["mysid"] || getRandString();
+    this.state.conn = new WebSocket(params.socketUrl + "/?sid=" + mysid);
+    // Settings initialized with values from localStorage
+    this.state.settings = {
+      bcolor: localStorage["bcolor"] || "lichess",
+      sound: parseInt(localStorage["sound"]) || 2,
+      hints: parseInt(localStorage["hints"]) || 1,
+      coords: !!eval(localStorage["coords"]),
+      highlight: !!eval(localStorage["highlight"]),
+      sqSize: parseInt(localStorage["sqSize"]),
+    };
+    const socketCloseListener = () => {
+      this.state.conn = new WebSocket(params.socketUrl + "/?sid=" + mysid);
+    }
+    this.state.conn.onclose = socketCloseListener;
+    const supportedLangs = ["en","es","fr"];
+    this.state.lang = localStorage["lang"] ||
+      supportedLangs.includes(navigator.language)
+        ? navigator.language
+        : "en";
+    this.setTranslations();
+  },
+  setTranslations: async function() {
+    // Fill modalWelcome, and import translations from "./translations/$lang.js"
+    document.getElementById("modalWelcome").innerHTML =
+      require("raw-loader!pug-plain-loader!@/welcome/" + this.state.lang + ".pug");
+    const tModule = await import("@/translations/" + this.state.lang + ".js");
+    this.state.tr = tModule.translations;
+  },
+  setLanguage(lang) {
+    this.state.lang = lang;
+    this.setTranslations();
+  },
+};
diff --git a/client/src/utils/alea.js b/client/src/utils/alea.js
new file mode 100644
index 00000000..337b25fa
--- /dev/null
+++ b/client/src/utils/alea.js
@@ -0,0 +1,36 @@
+// Random (enough) string for socket and game IDs
+export function getRandString()
+{
+  return (Date.now().toString(36) + Math.random().toString(36).substr(2, 7))
+    .toUpperCase();
+}
+
+export function random (min, max)
+{
+  if (!max)
+  {
+    max = min;
+    min = 0;
+  }
+  return Math.floor(Math.random() * (max - min) ) + min;
+}
+
+// Inspired by https://github.com/jashkenas/underscore/blob/master/underscore.js
+export function sample (arr, n)
+{
+  n = n || 1;
+  let cpArr = arr.map(e => e);
+  for (let index = 0; index < n; index++)
+  {
+    const rand = getRandInt(index, n);
+    const temp = cpArr[index];
+    cpArr[index] = cpArr[rand];
+    cpArr[rand] = temp;
+  }
+  return cpArr.slice(0, n);
+}
+
+export function shuffle(arr)
+{
+  return sample(arr, arr.length);
+}
diff --git a/client/src/utils/array.js b/client/src/utils/array.js
index a8466c66..b438eaae 100644
--- a/client/src/utils/array.js
+++ b/client/src/utils/array.js
@@ -22,3 +22,8 @@ export function init(size1, size2, initElem)
 {
   return [...Array(size1)].map(e => Array(size2).fill(initElem));
 }
+
+export function range(max)
+{
+  return [...Array(max).keys()];
+}
diff --git a/client/src/utils/cookie.js b/client/src/utils/cookie.js
new file mode 100644
index 00000000..b0518442
--- /dev/null
+++ b/client/src/utils/cookie.js
@@ -0,0 +1,22 @@
+// Source: https://www.quirksmode.org/js/cookies.html
+export function setCookie(name, value)
+{
+  var date = new Date();
+  date.setTime(date.getTime()+(183*24*60*60*1000)); //6 months
+  var expires = "; expires="+date.toGMTString();
+  document.cookie = name+"="+value+expires+"; path=/";
+}
+
+export function getCookie(name, defaut) {
+  var nameEQ = name + "=";
+  var ca = document.cookie.split(';');
+  for (var i=0;i < ca.length;i++)
+  {
+    var c = ca[i];
+    while (c.charAt(0)==' ')
+      c = c.substring(1,c.length);
+    if (c.indexOf(nameEQ) == 0)
+      return c.substring(nameEQ.length,c.length);
+  }
+  return defaut; //cookie not found
+}
diff --git a/client/src/utils/misc.js b/client/src/utils/misc.js
deleted file mode 100644
index 5a32ff9e..00000000
--- a/client/src/utils/misc.js
+++ /dev/null
@@ -1,74 +0,0 @@
-export const util =
-{
-  // Source: https://www.quirksmode.org/js/cookies.html
-  setCookie: function(name, value)
-  {
-    var date = new Date();
-    date.setTime(date.getTime()+(183*24*60*60*1000)); //6 months
-    var expires = "; expires="+date.toGMTString();
-    document.cookie = name+"="+value+expires+"; path=/";
-  },
-
-  getCookie: function(name, defaut) {
-    var nameEQ = name + "=";
-    var ca = document.cookie.split(';');
-    for (var i=0;i < ca.length;i++)
-    {
-      var c = ca[i];
-      while (c.charAt(0)==' ')
-        c = c.substring(1,c.length);
-      if (c.indexOf(nameEQ) == 0)
-        return c.substring(nameEQ.length,c.length);
-    }
-    return defaut; //cookie not found
-  },
-
-  random: function(min, max)
-  {
-    if (!max)
-    {
-      max = min;
-      min = 0;
-    }
-    return Math.floor(Math.random() * (max - min) ) + min;
-  },
-
-  // Inspired by https://github.com/jashkenas/underscore/blob/master/underscore.js
-  sample: function(arr, n)
-  {
-    n = n || 1;
-    let cpArr = arr.map(e => e);
-    for (let index = 0; index < n; index++)
-    {
-      const rand = getRandInt(index, n);
-      const temp = cpArr[index];
-      cpArr[index] = cpArr[rand];
-      cpArr[rand] = temp;
-    }
-    return cpArr.slice(0, n);
-  },
-
-  shuffle: function(arr)
-  {
-    return sample(arr, arr.length);
-  },
-
-  range: function(max)
-  {
-    return [...Array(max).keys()];
-  },
-
-  // TODO: rename into "cookie" et supprimer les deux ci-dessous
-  // Random (enough) string for socket and game IDs
-  getRandString: function()
-  {
-    return (Date.now().toString(36) + Math.random().toString(36).substr(2, 7))
-      .toUpperCase();
-  },
-
-  // Shortcut for an often used click (on a modal)
-  doClick: function(elemId)
-  {
-    document.getElementById(elemId).click(); //or ".checked = true"
-  },
-};
diff --git a/client/src/welcome/en.pug b/client/src/welcome/en.pug
index 27823f36..6f9675fa 100644
--- a/client/src/welcome/en.pug
+++ b/client/src/welcome/en.pug
@@ -45,9 +45,9 @@ div(role="dialog")
       p
         | For informations about hundreds (if not thousands) of variants, you
         | can visit the excellent 
-				a(href="https://www.chessvariants.com/" _target="blank" rel="noopener")
-					| chessvariants
-				| &nbsp;website.
-		p.smallfont
-			| Image credit:
-			a(href=wikipediaUrl _target="blank" rel="noopener") Wikipedia
+        a(href="https://www.chessvariants.com/" _target="blank" rel="noopener")
+          | chessvariants
+        | &nbsp;website.
+    p.smallfont
+      | Image credit:
+      a(href=wikipediaUrl _target="blank" rel="noopener") Wikipedia
-- 
2.44.0