From: Benjamin Auder Date: Wed, 30 Jan 2019 16:33:22 +0000 (+0100) Subject: Apply store pattern to track global app state X-Git-Url: https://git.auder.net/variants/doc/html/img/scripts/current/git-favicon.png?a=commitdiff_plain;h=c66a829b3770122fe0ff2fb9db8def9635bbc334;p=vchess.git Apply store pattern to track global app state --- 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, + }; }, }; 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!"] }} 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 - |  website. - p.smallfont - | Image credit: - a(href=wikipediaUrl _target="blank" rel="noopener") Wikipedia + a(href="https://www.chessvariants.com/" _target="blank" rel="noopener") + | chessvariants + |  website. + p.smallfont + | Image credit: + a(href=wikipediaUrl _target="blank" rel="noopener") Wikipedia