From dcd68c4108412f45b8ce119ae80ce8f6e296800b Mon Sep 17 00:00:00 2001 From: Benjamin Auder <benjamin.auder@somewhere> Date: Sat, 1 Feb 2020 18:59:32 +0100 Subject: [PATCH] Finished. Now some last styling --- client/src/App.vue | 62 +++++++++++++- client/src/components/BaseGame.vue | 13 ++- client/src/components/Chat.vue | 10 +-- client/src/components/ContactForm.vue | 4 +- client/src/components/Language.vue | 2 +- client/src/components/Settings.vue | 9 +- client/src/components/UpsertUser.vue | 2 +- client/src/stylesheets/layout.sass | 95 --------------------- client/src/translations/about/en.pug | 4 +- client/src/utils/gameStorage.js | 4 +- client/src/utils/modalClick.js | 7 ++ client/src/views/Game.vue | 106 +++++++++++++++-------- client/src/views/Hall.vue | 117 ++++++++++++++++---------- client/src/views/Rules.vue | 13 ++- server/db/create.sql | 2 +- server/models/Game.js | 21 ++--- server/sockets.js | 2 +- 17 files changed, 262 insertions(+), 211 deletions(-) delete mode 100644 client/src/stylesheets/layout.sass create mode 100644 client/src/utils/modalClick.js diff --git a/client/src/App.vue b/client/src/App.vue index 27ef66c2..671e7469 100644 --- a/client/src/App.vue +++ b/client/src/App.vue @@ -41,12 +41,12 @@ </template> <script> -// See https://stackoverflow.com/a/35417159 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"; +import { processModalClick } from "./utils/modalClick.js"; export default { components: { ContactForm, @@ -64,9 +64,12 @@ export default { return `/images/flags/${this.st.lang}.svg`; }, }, -// mounted: function() { -// feather.replace(); -// }, + mounted: function() { + let dialogs = document.querySelectorAll("div[role='dialog']"); + dialogs.forEach(d => { + d.addEventListener("click", processModalClick); + }); + }, methods: { hideDrawer: function(e) { if (e.target.innerText == "Forum") @@ -79,12 +82,23 @@ export default { </script> <style lang="sass"> +//html, * +// font-family: "Open Sans", Arial, sans-serif +// --back-color: #f2f2f2 +// --a-link-color: black +// --a-visited-color: black + +body + padding: 0 + min-width: 320px + #app font-family: "Avenir", Helvetica, Arial, sans-serif -webkit-font-smoothing: antialiased -moz-osx-font-smoothing: grayscale .container + overflow: hidden @media screen and (max-width: 767px) padding: 0 @@ -107,6 +121,27 @@ header .clickable cursor: pointer +.text-center + text-align: center + +.smallpad + padding: 5px + +.emphasis + font-style: italic + +.clearer + clear: both + +.smallfont + font-size: 0.8em + +.bigfont + font-size: 1.2em + +.bold + font-weight: bold + nav width: 100% margin: 0 @@ -151,6 +186,13 @@ nav label.drawer-close top: 50px +@media screen and (max-width: 767px) + .button-group + flex-direction: row + button:not(:first-child) + border-left: 1px solid var(--button-group-border-color) + border-top: 0 + footer border: 1px solid #ddd //background-color: #000033 @@ -179,4 +221,16 @@ footer @media screen and (max-width: 767px) footer border: none + +//#settings, #contactForm +// max-width: 767px +// @media screen and (max-width: 767px) +// max-width: 100vw +//[type="checkbox"].modal+div .card +// max-width: 767px +// max-height: 100vh +//[type="checkbox"].modal+div .card.small-modal +// max-width: 320px +//[type="checkbox"].modal+div .card.big-modal +// max-width: 90vw </style> diff --git a/client/src/components/BaseGame.vue b/client/src/components/BaseGame.vue index f4900435..2bf1fbdb 100644 --- a/client/src/components/BaseGame.vue +++ b/client/src/components/BaseGame.vue @@ -1,7 +1,8 @@ <template lang="pug"> -div#baseGame(tabindex=-1 @click="() => focusBg()" @keydown="handleKeys") +div#baseGame(tabindex=-1 @click="() => focusBg()" + @keydown="handleKeys" @wheel="handleScroll") input#modalEog.modal(type="checkbox") - div(role="dialog" aria-labelledby="eogMessage") + div(role="dialog" data-checkbox="modalEog" aria-labelledby="eogMessage") .card.smallpad.small-modal.text-center label.modal-close(for="modalEog") h3#eogMessage.section {{ endgameMessage }} @@ -118,6 +119,12 @@ export default { break; } }, + handleScroll: function(e) { + if (e.deltaY < 0) + this.undo(); + else if (e.deltaY > 0) + this.play(); + }, re_setVariables: function() { this.endgameMessage = ""; this.orientation = this.game.mycolor || "w"; //default orientation for observed games @@ -387,6 +394,4 @@ export default { padding: 0 td text-align: left -.clearer - clear: both </style> diff --git a/client/src/components/Chat.vue b/client/src/components/Chat.vue index 5834cbe1..cbfb8059 100644 --- a/client/src/components/Chat.vue +++ b/client/src/components/Chat.vue @@ -29,8 +29,7 @@ export default { const data = JSON.parse(msg.data); if (data.code == "newchat") //only event at this level { - this.chats.unshift({msg:data.msg, - name:data.name || "@nonymous", sid:data.from}); + this.chats.unshift({msg:data.msg, name:data.name || "@nonymous"}); this.$emit("newchat-received"); //data not required here } }; @@ -45,17 +44,16 @@ export default { methods: { classObject: function(chat) { return { - "my-chatmsg": chat.sid == this.st.user.sid, + "my-chatmsg": chat.name == this.st.user.name, "opp-chatmsg": this.players.some( - p => p.sid == chat.sid && p.sid != this.st.user.sid) + p => p.name == chat.name && p.name != this.st.user.name) }; }, sendChat: function() { let chatInput = document.getElementById("inputChat"); const chatTxt = chatInput.value; chatInput.value = ""; - const chat = {msg:chatTxt, name: this.st.user.name || "@nonymous", - sid:this.st.user.sid}; + const chat = {msg:chatTxt, name: this.st.user.name || "@nonymous"}; this.$emit("newchat-sent", chat); //useful for corr games this.chats.unshift(chat); this.st.conn.send(JSON.stringify({ diff --git a/client/src/components/ContactForm.vue b/client/src/components/ContactForm.vue index 2f970ae7..734d7f19 100644 --- a/client/src/components/ContactForm.vue +++ b/client/src/components/ContactForm.vue @@ -1,7 +1,8 @@ <template lang="pug"> div input#modalContact.modal(type="checkbox") - div(role="dialog" aria-labelledby="contactTitle") + div(role="dialog" data-checkbox="modalContact" + aria-labelledby="contactTitle") form.card.smallpad label.modal-close(for="modalContact") h3#contactTitle.section {{ st.tr["Contact form"] }} @@ -70,5 +71,6 @@ export default { <style lang="sass" scoped> #emailSent + color: blue display: none </style> diff --git a/client/src/components/Language.vue b/client/src/components/Language.vue index 5c7e1e52..ac4faa79 100644 --- a/client/src/components/Language.vue +++ b/client/src/components/Language.vue @@ -7,7 +7,7 @@ div "fr": "Français", }; input#modalLang.modal(type="checkbox") - div(role="dialog") + div(role="dialog" data-checkbox="modalLang") #language.card label.modal-close(for="modalLang") form(@change="setLanguage") diff --git a/client/src/components/Settings.vue b/client/src/components/Settings.vue index d08b943f..6034ab45 100644 --- a/client/src/components/Settings.vue +++ b/client/src/components/Settings.vue @@ -1,12 +1,14 @@ <template lang="pug"> div input#modalSettings.modal(type="checkbox") - div(role="dialog" aria-labelledby="settingsTitle") + div(role="dialog" data-checkbox="modalSettings" + aria-labelledby="settingsTitle") .card.smallpad(@change="updateSettings") label.modal-close(for="modalSettings") h3#settingsTitle.section {{ st.tr["Preferences"] }} fieldset - label(for="setSqSize") {{ st.tr["Square size (in pixels). 0 for 'adaptative'"] }} + 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") {{ st.tr["Show move hints?"] }} @@ -15,7 +17,8 @@ div option(value="1") {{ st.tr["Moves from a square"] }} option(value="2") {{ st.tr["Pieces which can move"] }} fieldset - label(for="setHighlight") {{ st.tr["Highlight squares? (Last move & checks)"] }} + label(for="setHighlight") + | {{ st.tr["Highlight squares? (Last move & checks)"] }} input#setHighlight(type="checkbox" v-model="st.settings.highlight") fieldset label(for="setCoords") {{ st.tr["Show board coordinates?"] }} diff --git a/client/src/components/UpsertUser.vue b/client/src/components/UpsertUser.vue index 93c1fd4b..64616574 100644 --- a/client/src/components/UpsertUser.vue +++ b/client/src/components/UpsertUser.vue @@ -1,7 +1,7 @@ <template lang="pug"> div input#modalUser.modal(type="checkbox" @change="trySetEnterTime") - div(role="dialog") + div(role="dialog" data-checkbox="modalUser") .card label.modal-close(for="modalUser") h3 {{ stage }} diff --git a/client/src/stylesheets/layout.sass b/client/src/stylesheets/layout.sass deleted file mode 100644 index 0e08e272..00000000 --- a/client/src/stylesheets/layout.sass +++ /dev/null @@ -1,95 +0,0 @@ -html, * - font-family: "Open Sans", Arial, sans-serif - --back-color: #f2f2f2 - --a-link-color: blue - --a-visited-color: blue - -body - padding: 0 - min-width: 320px - -.container - padding: 0 - overflow: hidden - -div - padding: 0 - -.section-content - * - margin-left: auto - margin-right: auto - max-width: 767px - figure.diagram-container - max-width: 1000px - @media screen and (max-width: 767px) - max-width: 100% - padding: 0 5px - -@media screen and (max-width: 767px) - .button-group - flex-direction: row - button:not(:first-child) - border-left: 1px solid var(--button-group-border-color) - border-top: 0 - -.right-menu - float: right - @media screen and (max-width: 767px) - .info-container - p - margin-right: 5px - -a.right-menu - &:link, &:visited, &:hover - color: black - -#settings, #contactForm - max-width: 767px - @media screen and (max-width: 767px) - max-width: 100vw - -#emailSent - color: blue - display: none - -a - text-decoration: underline - -.text-center - text-align: center - -.smallpad - padding: 5px - -.emphasis - font-style: italic - -.clickable - cursor: pointer - -.clearer - clear: both - -.red - color: #cc3300 - -.purple - color: purple - -.smallfont - font-size: 0.8em - -.bigfont - font-size: 1.2em - -.bold - font-weight: bold - -[type="checkbox"].modal+div .card - max-width: 767px - max-height: 100vh -[type="checkbox"].modal+div .card.small-modal - max-width: 320px -[type="checkbox"].modal+div .card.big-modal - max-width: 90vw diff --git a/client/src/translations/about/en.pug b/client/src/translations/about/en.pug index 0e434213..695c27ee 100644 --- a/client/src/translations/about/en.pug +++ b/client/src/translations/about/en.pug @@ -33,8 +33,8 @@ p ul li Translations: see client/src/translations/ folder li. - Styling: client/src/stylesheets/ and <style> part of .vue - files in client/src/{components,views} + Styling: see <style> parts of .vue files + in client/src/{components,views} li. Back-end and front-end code: a lot can be improved! Feel free to send pull requests :) diff --git a/client/src/utils/gameStorage.js b/client/src/utils/gameStorage.js index a9ebf33a..630d1c75 100644 --- a/client/src/utils/gameStorage.js +++ b/client/src/utils/gameStorage.js @@ -67,7 +67,8 @@ export const GameStorage = }, // TODO: also option to takeback a move ? - update: function(gameId, obj) //chat, move, fen, clocks, score, initime, ... + // obj: chat, move, fen, clocks, score[Msg], initime, ... + update: function(gameId, obj) { if (Number.isInteger(gameId) || !isNaN(parseInt(gameId))) { @@ -83,6 +84,7 @@ export const GameStorage = move: obj.move, //may be undefined... fen: obj.fen, score: obj.score, + scoreMsg: obj.scoreMsg, drawOffer: obj.drawOffer, } } diff --git a/client/src/utils/modalClick.js b/client/src/utils/modalClick.js new file mode 100644 index 00000000..8e0dca7f --- /dev/null +++ b/client/src/utils/modalClick.js @@ -0,0 +1,7 @@ +export function processModalClick(e) +{ + // Close a modal when click on it but outside focused element + const data = e.target.dataset; + if (!!data.checkbox) + document.getElementById(data.checkbox).checked = false; +} diff --git a/client/src/views/Game.vue b/client/src/views/Game.vue index e16fa2f8..30fbc39b 100644 --- a/client/src/views/Game.vue +++ b/client/src/views/Game.vue @@ -1,7 +1,8 @@ <template lang="pug"> main input#modalChat.modal(type="checkbox" @change="toggleChat") - div(role="dialog" aria-labelledby="inputChat") + div#chatWrap(role="dialog" data-checkbox="modalChat" + aria-labelledby="inputChat") #chat.card label.modal-close(for="modalChat") Chat(:players="game.players" :pastChats="game.chats" @@ -10,7 +11,7 @@ main #aboveBoard.col-sm-12.col-md-9.col-md-offset-3.col-lg-10.col-lg-offset-2 button#chatBtn(onClick="doClick('modalChat')") Chat #actions(v-if="game.mode!='analyze' && game.score=='*'") - button(@click="offerDraw") Draw + button(@click="clickDraw" :class="{['draw-' + drawOffer]: true}") Draw button(@click="abortGame") Abort button(@click="resign") Resign #playersInfo @@ -32,6 +33,7 @@ import { GameStorage } from "@/utils/gameStorage"; import { ppt } from "@/utils/datetime"; import { extractTime } from "@/utils/timeControl"; import { ArrayFun } from "@/utils/array"; +import { processModalClick } from "@/utils/modalClick"; export default { name: 'my-game', @@ -50,8 +52,8 @@ export default { game: {players:[{name:""},{name:""}]}, //passed to BaseGame virtualClocks: [0, 0], //initialized with true game.clocks vr: null, //"variant rules" object initialized from FEN - drawOffer: "", //TODO: use for button style - people: [], //players + observers + drawOffer: "", + people: {}, //players + observers lastate: undefined, //used if opponent send lastate before game is ready repeat: {}, //detect position repetition }; @@ -94,11 +96,11 @@ export default { }, 1000); }, }, - // TODO: redundant code with Hall.vue (related to people array) + // NOTE: some redundant code with Hall.vue (related to people array) created: function() { // Always add myself to players' list const my = this.st.user; - this.people.push({sid:my.sid, id:my.id, name:my.name}); + this.$set(this.people, my.sid, {id:my.id, name:my.name}); this.gameRef.id = this.$route.params["id"]; this.gameRef.rid = this.$route.query["rid"]; //may be undefined // Define socket .onmessage() and .onclose() events: @@ -126,6 +128,10 @@ export default { socketInit(this.loadGame); } }, + mounted: function() { + document.getElementById("chatWrap").addEventListener( + "click", processModalClick); + }, methods: { // O.1] Ask server for room composition: roomInit: function() { @@ -135,7 +141,7 @@ export default { const name = this.game.players[index].name; if (this.st.user.name == name) return true; - return this.people.some(p => p.name == name); + return Object.values(this.people).some(p => p.name == name); }, socketMessageListener: function(msg) { const data = JSON.parse(msg.data); @@ -148,7 +154,11 @@ export default { case "pollclients": { data.sockIds.forEach(sid => { - this.people.push({sid:sid, id:0, name:""}); + // TODO: understand clearly what happens here, problems when a + // game is quit, and then launch a new game from hall. + if (!!this.people[sid]) + return; + this.$set(this.people, sid, {id:0, name:""}); // Ask only identity this.st.conn.send(JSON.stringify({code:"askidentity", target:sid})); }); @@ -159,15 +169,20 @@ export default { // Request for identification: reply if I'm not anonymous if (this.st.user.id > 0) { - this.st.conn.send(JSON.stringify( - // people[0] instead of st.user to avoid sending email - {code:"identity", user:this.people[0], target:data.from})); + this.st.conn.send(JSON.stringify({code:"identity", + user: { + // NOTE: decompose to avoid revealing email + name: this.st.user.name, + sid: this.st.user.sid, + id: this.st.user.id, + }, + target:data.from})); } break; } case "identity": { - let player = this.people.find(p => p.sid == data.user.sid); + let player = this.people[data.user.sid]; // NOTE: sometimes player.id fails because player is undefined... // Probably because the event was meant for Hall? if (!player) @@ -175,7 +190,7 @@ export default { player.id = data.user.id; player.name = data.user.name; // Sending last state only for live games: corr games are complete - if (this.game.type == "live" && this.game.oppsid == player.sid) + if (this.game.type == "live" && this.game.oppsid == data.user.sid) { // Send our "last state" informations to opponent const L = this.game.moves.length; @@ -184,7 +199,7 @@ export default { lastMove.draw = true; this.st.conn.send(JSON.stringify({ code: "lastate", - target: player.sid, + target: data.user.sid, state: { lastMove: lastMove, @@ -227,7 +242,7 @@ export default { this.gameOver("?", "Abort"); break; case "draw": - this.gameOver("1/2", "Mutual agreement"); + this.gameOver("1/2", data.message); break; case "drawoffer": this.drawOffer = "received"; //TODO: observers don't know who offered draw @@ -241,12 +256,16 @@ export default { break; case "connect": { - this.people.push({name:"", id:0, sid:data.from}); - this.st.conn.send(JSON.stringify({code:"askidentity", target:data.from})); + // TODO: next condition is probably not required. See note line 150 + if (!this.people[data.from]) + { + this.$set(this.people, data.from, {name:"", id:0}); + this.st.conn.send(JSON.stringify({code:"askidentity", target:data.from})); + } break; } case "disconnect": - ArrayFun.remove(this.people, p => p.sid == data.from); + this.$delete(this.people, data.from); break; } }, @@ -270,18 +289,21 @@ export default { this.drawOffer = "received"; } }, - offerDraw: function() { + clickDraw: function() { if (["received","threerep"].includes(this.drawOffer)) { if (!confirm("Accept draw?")) return; - this.people.forEach(p => { - if (p.sid != this.st.user.sid) - this.st.conn.send(JSON.stringify({code:"draw", target:p.sid})); - }); const message = (this.drawOffer == "received" ? "Mutual agreement" : "Three repetitions"); + Object.keys(this.people).forEach(sid => { + if (sid != this.st.user.sid) + { + this.st.conn.send(JSON.stringify({code:"draw", + message:message, target:sid})); + } + }); this.gameOver("1/2", message); } else if (this.drawOffer == "sent") @@ -295,9 +317,9 @@ export default { if (!confirm("Offer draw?")) return; this.drawOffer = "sent"; - this.people.forEach(p => { - if (p.sid != this.st.user.sid) - this.st.conn.send(JSON.stringify({code:"drawoffer", target:p.sid})); + Object.keys(this.people).forEach(sid => { + if (sid != this.st.user.sid) + this.st.conn.send(JSON.stringify({code:"drawoffer", target:sid})); }); if (this.game.type == "corr") GameStorage.update(this.gameRef.id, {drawOffer: true}); @@ -307,12 +329,12 @@ export default { if (!confirm(this.st.tr["Terminate game?"])) return; this.gameOver("?", "Abort"); - this.people.forEach(p => { - if (p.sid != this.st.user.sid) + Object.keys(this.people).forEach(sid => { + if (sid != this.st.user.sid) { this.st.conn.send(JSON.stringify({ code: "abort", - target: p.sid, + target: sid, })); } }); @@ -320,11 +342,11 @@ export default { resign: function(e) { if (!confirm("Resign the game?")) return; - this.people.forEach(p => { - if (p.sid != this.st.user.sid) + Object.keys(this.people).forEach(sid => { + if (sid != this.st.user.sid) { this.st.conn.send(JSON.stringify({code:"resign", - side:this.game.mycolor, target:p.sid})); + side:this.game.mycolor, target:sid})); } }); this.gameOver(this.game.mycolor=="w" ? "0-1" : "1-0", "Resign"); @@ -464,12 +486,12 @@ export default { addTime = this.game.increment - elapsed/1000; } let sendMove = Object.assign({}, filtered_move, {addTime: addTime}); - this.people.forEach(p => { - if (p.sid != this.st.user.sid) + Object.keys(this.people).forEach(sid => { + if (sid != this.st.user.sid) { this.st.conn.send(JSON.stringify({ code: "newmove", - target: p.sid, + target: sid, move: sendMove, })); } @@ -547,7 +569,10 @@ export default { return p.sid == this.st.user.sid || p.uid == this.st.user.id; }); if (myIdx >= 0) //OK, I play in this game - GameStorage.update(this.gameRef.id, { score: score }); + { + GameStorage.update(this.gameRef.id, + {score: score, scoreMsg: scoreMsg}); + } }, }, }; @@ -596,4 +621,13 @@ export default { #chatBtn margin: 0 10px 0 0 + +.draw-sent, .draw-sent:hover + background-color: lightyellow + +.draw-received, .draw-received:hover + background-color: lightgreen + +.draw-threerep, .draw-threerep:hover + background-color: #e4d1fc </style> diff --git a/client/src/views/Hall.vue b/client/src/views/Hall.vue index 24ec364f..7b42854d 100644 --- a/client/src/views/Hall.vue +++ b/client/src/views/Hall.vue @@ -7,7 +7,8 @@ main h3#infoMessage.section p(v-html="infoMessage") input#modalNewgame.modal(type="checkbox") - div(role="dialog" aria-labelledby="titleFenedit") + div(role="dialog" data-checkbox="modalNewgame" + aria-labelledby="titleFenedit") .card.smallpad(@keyup.enter="newChallenge") label#closeNewgame.modal-close(for="modalNewgame") fieldset @@ -48,11 +49,11 @@ main button(@click="pdisplay='players'") Players button(@click="pdisplay='chat'") Chat #players(v-show="pdisplay=='players'") - h3 Online players - .player(v-for="p in uniquePlayers" @click="tryChallenge(p)" - :class="{anonymous: !!p.count}" - ) - | {{ p.name + (!!p.count ? " ("+p.count+")" : "") }} + p.text-center(v-for="p in uniquePlayers") + span(:class="{anonymous: !!p.count}") + | {{ (p.name || '@nonymous') + (!!p.count ? " ("+p.count+")" : "") }} + button.player-action(v-if="!p.count" @click="challOrWatch(p,$event)") + | {{ whatPlayerDoes(p) }} #chat(v-show="pdisplay=='chat'") Chat(:players="[]") input#gameSection(type="radio" aria-hidden="true" name="accordion") @@ -92,7 +93,7 @@ export default { gdisplay: "live", games: [], challenges: [], - people: [], //people in main hall + people: {}, //people in main hall infoMessage: "", newchallenge: { fen: "", @@ -119,14 +120,14 @@ export default { computed: { uniquePlayers: function() { // Show e.g. "@nonymous (5)", and do nothing on click on anonymous - let anonymous = {name:"@nonymous", count:0}; + let anonymous = {name:"", count:0}; let playerList = {}; - this.people.forEach(p => { + Object.values(this.people).forEach(p => { if (p.id > 0) { // We don't count registered users connections: either they are here or not. if (!playerList[p.id]) - playerList[p.id] = {name: p.name, count: 0}; + playerList[p.id] = {name: p.name}; } else anonymous.count++; @@ -139,7 +140,7 @@ export default { created: function() { // Always add myself to players' list const my = this.st.user; - this.people.push({sid:my.sid, id:my.id, name:my.name}); + this.$set(this.people, my.sid, {id:my.id, name:my.name}); // Retrieve live challenge (not older than 30 minute) if any: const chall = JSON.parse(localStorage.getItem("challenge") || "false"); if (!!chall) @@ -231,13 +232,10 @@ export default { // this.st.variants might be uninitialized (variant == null) return (!!variant ? variant.name : ""); }, - getSid: function(pname) { - const pIdx = this.people.findIndex(pl => pl.name == pname); - return (pIdx === -1 ? null : this.people[pIdx].sid); - }, - getPname: function(sid) { - const pIdx = this.people.findIndex(pl => pl.sid == sid); - return (pIdx === -1 ? null : this.people[pIdx].name); + whatPlayerDoes: function(p) { + if (this.games.some(g => g.players.some(pl => pl.sid == p.sid))) + return "Playing"; + return "Challenge"; //player is available }, sendSomethingTo: function(to, code, obj, warnDisconnected) { const doSend = (code, obj, sid) => { @@ -251,7 +249,8 @@ export default { if (!!to) { // Challenge with targeted players - const targetSid = this.getSid(to); + const targetSid = + Object.keys(this.people).find(sid => this.people[sid].name == to); if (!targetSid) { if (!!warnDisconnected) @@ -263,9 +262,9 @@ export default { else { // Open challenge: send to all connected players (except us) - this.people.forEach(p => { - if (p.sid != this.st.user.sid) //only sid is always set - doSend(code, obj, p.sid); + Object.keys(this.people).forEach(sid => { + if (sid != this.st.user.sid) + doSend(code, obj, sid); }); } }, @@ -281,7 +280,7 @@ export default { case "pollclients": { data.sockIds.forEach(sid => { - this.people.push({sid:sid, id:0, name:""}); + this.$set(this.people, sid, {id:0, name:""}); // Ask identity, challenges and game(s) this.st.conn.send(JSON.stringify({code:"askidentity", target:sid})); this.st.conn.send(JSON.stringify({code:"askchallenge", target:sid})); @@ -295,12 +294,23 @@ export default { // Request for identification: reply if I'm not anonymous if (this.st.user.id > 0) { - this.st.conn.send(JSON.stringify( - // people[0] instead of st.user to avoid sending email - {code:"identity", user:this.people[0], target:data.from})); + this.st.conn.send(JSON.stringify({code:"identity", + user: { + // NOTE: decompose to avoid revealing email + name: this.st.user.name, + sid: this.st.user.sid, + id: this.st.user.id, + }, + target:data.from})); } break; } + case "identity": + { + this.$set(this.people, data.user.sid, + {id: data.user.id, name: data.user.name}); + break; + } case "askchallenge": { // Send my current live challenge (if any) @@ -323,20 +333,13 @@ export default { } break; } - case "identity": - { - const pIdx = this.people.findIndex(p => p.sid == data.user.sid); - this.people[pIdx].id = data.user.id; - this.people[pIdx].name = data.user.name; - break; - } case "challenge": { // Receive challenge from some player (+sid) let newChall = data.chall; newChall.type = this.classifyObject(data.chall); - const pIdx = this.people.findIndex(p => p.sid == data.from); - newChall.from = this.people[pIdx]; //may be anonymous + newChall.from = + Object.assign({sid:data.from}, this.people[data.from]); newChall.added = Date.now(); //TODO: this is reception timestamp, not creation newChall.vname = this.getVname(newChall.vid); this.challenges.push(newChall); @@ -377,7 +380,7 @@ export default { } case "refusechallenge": { - alert(this.getPname(data.from) + " declined your challenge"); + alert(this.people[data.from].name + " declined your challenge"); ArrayFun.remove(this.challenges, c => c.id == data.cid); break; } @@ -390,7 +393,7 @@ export default { } case "connect": { - this.people.push({name:"", id:0, sid:data.from}); + this.$set(this.people, data.from, {name:"", id:0}); this.st.conn.send(JSON.stringify({code:"askidentity", target:data.from})); this.st.conn.send(JSON.stringify({code:"askchallenge", target:data.from})); this.st.conn.send(JSON.stringify({code:"askgame", target:data.from})); @@ -398,13 +401,13 @@ export default { } case "disconnect": { - ArrayFun.remove(this.people, p => p.sid == data.from); + this.$delete(this.people, data.from); // Also remove all challenges sent by this player: ArrayFun.remove(this.challenges, c => c.from.sid == data.from); // And all live games where he plays and no other opponent is online ArrayFun.remove(this.games, g => g.type == "live" && (g.players.every(p => p.sid == data.from - || !this.people.some(pl => pl.sid == p.sid))), "all"); + || !this.people[p.sid])), "all"); break; } } @@ -416,10 +419,25 @@ export default { this.newchallenge.to = player.name; doClick("modalNewgame"); }, + challOrWatch: function(p, e) { + switch (e.target.innerHTML) + { + case "Challenge": + this.tryChallenge(p); + break; + case "Playing": + // NOTE: this search for game was already done for rendering + this.showGame(this.games.find( + g => g.players.some(pl => pl.sid == p.sid))); + break; + }; + }, newChallenge: async function() { const vname = this.getVname(this.newchallenge.vid); const vModule = await import("@/variants/" + vname + ".js"); window.V = vModule.VariantRules; + if (!!this.newchallenge.timeControl.match(/^[0-9]+$/)) + this.newchallenge.timeControl += "+0"; //assume minutes, no increment const error = checkChallenge(this.newchallenge); if (!!error) return alert(error); @@ -436,7 +454,11 @@ export default { // NOTE: vname and type are redundant (can be deduced from timeControl + vid) chall.type = ctype; chall.vname = vname; - chall.from = this.people[0]; //avoid sending email + chall.from = { //decompose to avoid revealing email + sid: this.st.user.sid, + id: this.st.user.id, + name: this.st.user.name, + }; this.challenges.push(chall); if (ctype == "live") localStorage.setItem("challenge", JSON.stringify(chall)); @@ -490,7 +512,11 @@ export default { } if (c.accepted) { - c.seat = this.people[0]; //== this.st.user, avoid revealing email + c.seat = { //again, avoid c.seat = st.user to not reveal email + sid: this.st.user.sid, + id: this.st.user.id, + name: this.st.user.name, + }; this.launchGame(c); } else @@ -535,9 +561,8 @@ export default { let target = c.from.sid; //may not be defined if corr + offline opp if (!target) { - const opponent = this.people.find(p => p.id == c.from.id); - if (!!opponent) - target = opponent.sid + target = Object.keys(this.people).find(sid => + this.people[sid].id == c.from.id); } const tryNotifyOpponent = () => { if (!!target) //opponent is online @@ -603,4 +628,8 @@ export default { max-width: 100% margin: 0; border: none; +.anonymous + font-style: italic +button.player-action + margin-left: 20px </style> diff --git a/client/src/views/Rules.vue b/client/src/views/Rules.vue index 8056ba32..e1ef8fef 100644 --- a/client/src/views/Rules.vue +++ b/client/src/views/Rules.vue @@ -7,7 +7,7 @@ main button(v-show="!gameInProgress" @click="() => startGame('auto')") | Sample game button(v-show="!gameInProgress" @click="() => startGame('versus')") - | Practice! + | Practice button(v-show="gameInProgress" @click="() => stopGame()") | Stop game button(@click="gotoAnalyze") Analyze @@ -112,6 +112,17 @@ export default { </script> <style lang="sass"> +//.section-content +// * +// margin-left: auto +// margin-right: auto +// max-width: 767px +// figure.diagram-container +// max-width: 1000px +// @media screen and (max-width: 767px) +// max-width: 100% +// padding: 0 5px + .warn padding: 3px color: red diff --git a/server/db/create.sql b/server/db/create.sql index a3930725..d18a3105 100644 --- a/server/db/create.sql +++ b/server/db/create.sql @@ -38,6 +38,7 @@ create table Games ( fenStart varchar, --initial state fen varchar, --current state score varchar, + scoreMsg varchar, timeControl varchar, created datetime, --used only for DB cleaning drawOffer boolean, @@ -47,7 +48,6 @@ create table Games ( create table Chats ( gid integer, name varchar, - sid varchar, msg varchar, added datetime ); diff --git a/server/models/Game.js b/server/models/Game.js index 5b225ce6..02e3194b 100644 --- a/server/models/Game.js +++ b/server/models/Game.js @@ -9,6 +9,7 @@ const UserModel = require("./User"); * fen: varchar (current position) * timeControl: string * score: varchar (result) + * scoreMsg: varchar ("Time", "Mutual agreement"...) * created: datetime * drawOffer: boolean * @@ -27,7 +28,6 @@ const UserModel = require("./User"); * gid: game id (int) * msg: varchar * name: varchar - * sid: varchar (socket ID when sending message) * added: datetime */ @@ -80,8 +80,10 @@ const GameModel = db.serialize(function() { // TODO: optimize queries? let query = + // NOTE: g.scoreMsg can be NULL + // (in this case score = "*" and no reason to look at it) "SELECT g.id, g.vid, g.fen, g.fenStart, g.timeControl, g.score, " + - "v.name AS vname " + + "g.scoreMsg, v.name AS vname " + "FROM Games g " + "JOIN Variants v " + " ON g.vid = v.id " + @@ -106,7 +108,7 @@ const GameModel = if (!!err3) return cb(err3); query = - "SELECT msg, name, sid, added " + + "SELECT msg, name, added " + "FROM Chats " + "WHERE gid = " + id; db.all(query, (err4,chats) => { @@ -183,12 +185,10 @@ const GameModel = return "Wrong FEN string"; if (!!obj.score && !obj.score.match(/^[012?*\/-]+$/)) return "Wrong characters in score"; + if (!!obj.scoreMsg && !obj.scoreMsg.match(/^[a-zA-Z ]+$/)) + return "Wrong characters in score message"; if (!!obj.chat) - { - if (!obj.chat.sid.match(/^[a-zA-Z0-9]+$/)) - return "Wrong user SID"; return UserModel.checkNameEmail({name: obj.chat.name}); - } return ""; }, @@ -208,6 +208,8 @@ const GameModel = modifs += "fen = '" + obj.fen + "',"; if (!!obj.score) modifs += "score = '" + obj.score + "',"; + if (!!obj.scoreMsg) + modifs += "scoreMsg = '" + obj.scoreMsg + "',"; modifs = modifs.slice(0,-1); //remove last comma if (modifs.length > 0) { @@ -225,9 +227,8 @@ const GameModel = if (!!obj.chat) { query = - "INSERT INTO Chats (gid, msg, name, sid, added) VALUES " + - "(" + id + ",?,'" + obj.chat.name + "','" - + obj.chat.sid + "'," + Date.now() + ")"; + "INSERT INTO Chats (gid, msg, name, added) VALUES (" + + id + ",?,'" + obj.chat.name + "'," + "," + Date.now() + ")"; db.run(query, obj.chat.msg); } }); diff --git a/server/sockets.js b/server/sockets.js index fae71106..b31334c1 100644 --- a/server/sockets.js +++ b/server/sockets.js @@ -162,7 +162,7 @@ module.exports = function(wss) { break; case "draw": clients[obj.target].sock.send(JSON.stringify( - {code:"draw"})); + {code:"draw", message:obj.message})); break; } }); -- 2.44.0