From afd3240d89a2f6191fe9426960dc0c1667b40c77 Mon Sep 17 00:00:00 2001
From: Benjamin Auder <benjamin.auder@somewhere>
Date: Sun, 29 Dec 2019 23:21:43 +0100
Subject: [PATCH] A few fixes, drop planned problems support (replaced by forum
 + mode analyze)

---
 client/next_src/views/MyGames.vue   | 45 -------------
 client/next_src/views/Problem.vue   | 94 +++++++++++++++++++++++++++
 client/next_src/views/Problems.vue  | 99 -----------------------------
 client/src/App.vue                  |  7 +-
 client/src/components/GameList.vue  |  6 +-
 client/src/router.js                | 11 +++-
 client/src/stylesheets/variant.sass | 48 +-------------
 client/src/translations/en.js       | 18 +-----
 client/src/translations/fr.js       | 18 +-----
 client/src/views/Game.vue           |  5 +-
 client/src/views/MyGames.vue        | 73 +++++++++++++++++++--
 server/models/Game.js               | 13 ++--
 server/routes/games.js              |  2 +-
 13 files changed, 194 insertions(+), 245 deletions(-)
 delete mode 100644 client/next_src/views/MyGames.vue
 create mode 100644 client/next_src/views/Problem.vue

diff --git a/client/next_src/views/MyGames.vue b/client/next_src/views/MyGames.vue
deleted file mode 100644
index 5f183da1..00000000
--- a/client/next_src/views/MyGames.vue
+++ /dev/null
@@ -1,45 +0,0 @@
-<template>
-  <div class="about">
-    <h1>This is an about page</h1>
-  </div>
-</template>
-// "My" games: tabs my archived local games, my correspondance games
-// + my imported games (of any type).
-// TODO: later, also add possibility to upload a game (parse PGN).
-Vue.component("my-tab-games", {
-	props: ["settings"],
-	data: function() {
-		return {
-			display: "",
-			imported: [],
-			local: [],
-			corr: []
-		};
-	},
-	template: `
-		<div>
-			<div class="button-group">
-				<button @click="display='local'">Local games</button>
-				<button @click="display='corr'">Correspondance games</button>
-				<button @click="display='imported'">Imported games</button>
-			</div>
-			<my-game-list v-show="display=='local'" :games="local">
-			</my-game-list>
-			<my-game-list v-show="display=='corr'" :games="corr">
-			</my-game-list>
-			<my-game-list v-show="display=='imported'" :games="imported">
-			</my-game-list>
-			<button @click="update">Refresh</button>
-		</div>
-	`,
-	created: function() {
-		// TODO: fetch corr games, local and corr
-		// if any corr game where it's my turn, set display = "corr",
-		// else set display = "local" (if any) or imported (if any and no local)
-	},
-	methods: {
-		update: function() {
-			// TODO: scan local + imported games, if any new then add it
-		},
-	},
-});
diff --git a/client/next_src/views/Problem.vue b/client/next_src/views/Problem.vue
new file mode 100644
index 00000000..b30a22e9
--- /dev/null
+++ b/client/next_src/views/Problem.vue
@@ -0,0 +1,94 @@
+//TODO: new problem form + problem visualisation, like Game.vue (but simpler)
+// --> mode analyze, moves = [], "load problem"
+		<div class="col-sm-12 col-md-10 col-md-offset-1 col-lg-8 col-lg-offset-2">
+			<input type="checkbox" id="modal-newproblem" class="modal"/>
+			<div role="dialog" aria-labelledby="modalProblemTxt">
+				<div v-show="!modalProb.preview" class="card newproblem-form">
+					<label for="modal-newproblem" class="modal-close">
+					</label>
+					<h3 id="modalProblemTxt">{{ translate("Add a problem") }}</h3>
+					<form @submit.prevent="previewProblem()">
+						<fieldset>
+							<label for="newpbFen">FEN</label>
+							<input id="newpbFen" type="text" v-model="modalProb.fen"
+								:placeholder='translate("Full FEN description")'/>
+						</fieldset>
+						<fieldset>
+							<p class="emphasis">{{ translate("Safe HTML tags allowed") }}</p>
+							<label for="newpbInstructions">{{ translate("Instructions") }}</label>
+							<textarea id="newpbInstructions" v-model="modalProb.instructions"
+								:placeholder='translate("Describe the problem goal")'>
+							</textarea>
+							<label for="newpbSolution">{{ translate("Solution") }}</label>
+							<textarea id="newpbSolution" v-model="modalProb.solution"
+								:placeholder='translate("How to solve the problem?")'>
+							</textarea>
+							<button class="center-btn">{{ translate("Preview") }}</button>
+						</fieldset>
+					</form>
+				</div>
+				<div v-show="modalProb.preview" class="card newproblem-preview">
+					<label for="modal-newproblem" class="modal-close"
+						@click="modalProb.preview=false">
+					</label>
+					<my-problem-summary :prob="modalProb" :userid="userId" :preview="true">
+					</my-problem-summary>
+					<div class="button-group">
+						<button @click="modalProb.preview=false">{{ translate("Cancel") }}</button>
+						<button @click="sendProblem()">{{ translate("Send") }}</button>
+					</div>
+				</div>
+			</div>
+		previewProblem: function() {
+			if (!V.IsGoodFen(this.modalProb.fen))
+				return alert(translations["Bad FEN description"]);
+			if (this.modalProb.instructions.trim().length == 0)
+				return alert(translations["Empty instructions"]);
+			if (this.modalProb.solution.trim().length == 0)
+				return alert(translations["Empty solution"]);
+			Vue.set(this.modalProb, "preview", true);
+		},
+		editProblem: function(prob) {
+			this.modalProb = prob;
+			Vue.set(this.modalProb, "preview", false);
+			document.getElementById("modal-newproblem").checked = true;
+		},
+		deleteProblem: function(pid) {
+			ajax(
+				"/problems/" + pid,
+				"DELETE",
+				response => {
+					// Delete problem from the list on client side
+					let problems = this.curProblems();
+					const pIdx = problems.findIndex(p => p.id == pid);
+					problems.splice(pIdx, 1);
+				}
+			);
+		},
+		sendProblem: function() {
+			// Send it to the server and close modal
+			ajax(
+				"/problems/" + variant.id,
+				(this.modalProb.id > 0 ? "PUT" : "POST"),
+				this.modalProb,
+				response => {
+					document.getElementById("modal-newproblem").checked = false;
+					Vue.set(this.modalProb, "preview", false);
+					if (this.modalProb.id == 0)
+					{
+						this.myProblems.unshift({
+							added: Date.now(),
+							id: response.id,
+							uid: user.id,
+							fen: this.modalProb.fen,
+							instructions: this.modalProb.instructions,
+							solution: this.modalProb.solution,
+						});
+						if (!this.curProb && this.display != "mine")
+							this.display = "mine";
+					}
+					else
+						this.modalProb.id = 0;
+				}
+			);
+		},
diff --git a/client/next_src/views/Problems.vue b/client/next_src/views/Problems.vue
index b7f217d0..368e09a7 100644
--- a/client/next_src/views/Problems.vue
+++ b/client/next_src/views/Problems.vue
@@ -47,52 +47,6 @@ Vue.component('my-problems', {
 	},
 	// NOTE: always modals first, because otherwise "scroll to the end" undesirable effect
 	template: `
-		<div class="col-sm-12 col-md-10 col-md-offset-1 col-lg-8 col-lg-offset-2">
-			<input type="checkbox" id="modal-newproblem" class="modal"/>
-			<div role="dialog" aria-labelledby="modalProblemTxt">
-				<div v-show="!modalProb.preview" class="card newproblem-form">
-					<label for="modal-newproblem" class="modal-close">
-					</label>
-					<h3 id="modalProblemTxt">{{ translate("Add a problem") }}</h3>
-					<form @submit.prevent="previewProblem()">
-						<fieldset>
-							<label for="newpbFen">FEN</label>
-							<input id="newpbFen" type="text" v-model="modalProb.fen"
-								:placeholder='translate("Full FEN description")'/>
-						</fieldset>
-						<fieldset>
-							<p class="emphasis">{{ translate("Safe HTML tags allowed") }}</p>
-							<label for="newpbInstructions">{{ translate("Instructions") }}</label>
-							<textarea id="newpbInstructions" v-model="modalProb.instructions"
-								:placeholder='translate("Describe the problem goal")'>
-							</textarea>
-							<label for="newpbSolution">{{ translate("Solution") }}</label>
-							<textarea id="newpbSolution" v-model="modalProb.solution"
-								:placeholder='translate("How to solve the problem?")'>
-							</textarea>
-							<button class="center-btn">{{ translate("Preview") }}</button>
-						</fieldset>
-					</form>
-				</div>
-				<div v-show="modalProb.preview" class="card newproblem-preview">
-					<label for="modal-newproblem" class="modal-close"
-						@click="modalProb.preview=false">
-					</label>
-					<my-problem-summary :prob="modalProb" :userid="userId" :preview="true">
-					</my-problem-summary>
-					<div class="button-group">
-						<button @click="modalProb.preview=false">{{ translate("Cancel") }}</button>
-						<button @click="sendProblem()">{{ translate("Send") }}</button>
-					</div>
-				</div>
-			</div>
-			<input id="modalNomore" type="checkbox" class="modal"/>
-			<div role="dialog" aria-labelledby="nomoreMessage">
-				<div class="card smallpad small-modal text-center">
-					<label for="modalNomore" class="modal-close"></label>
-					<h3 id="nomoreMessage" class="section">{{ nomoreMessage }}</h3>
-				</div>
-			</div>
 			<div id="problemControls" class="button-group">
 				<button :aria-label='translate("Previous problem(s)")' class="tooltip"
 					@click="showNext('backward')"
@@ -309,58 +263,5 @@ Vue.component('my-problems', {
 				}
 			);
 		},
-		previewProblem: function() {
-			if (!V.IsGoodFen(this.modalProb.fen))
-				return alert(translations["Bad FEN description"]);
-			if (this.modalProb.instructions.trim().length == 0)
-				return alert(translations["Empty instructions"]);
-			if (this.modalProb.solution.trim().length == 0)
-				return alert(translations["Empty solution"]);
-			Vue.set(this.modalProb, "preview", true);
-		},
-		editProblem: function(prob) {
-			this.modalProb = prob;
-			Vue.set(this.modalProb, "preview", false);
-			document.getElementById("modal-newproblem").checked = true;
-		},
-		deleteProblem: function(pid) {
-			ajax(
-				"/problems/" + pid,
-				"DELETE",
-				response => {
-					// Delete problem from the list on client side
-					let problems = this.curProblems();
-					const pIdx = problems.findIndex(p => p.id == pid);
-					problems.splice(pIdx, 1);
-				}
-			);
-		},
-		sendProblem: function() {
-			// Send it to the server and close modal
-			ajax(
-				"/problems/" + variant.id,
-				(this.modalProb.id > 0 ? "PUT" : "POST"),
-				this.modalProb,
-				response => {
-					document.getElementById("modal-newproblem").checked = false;
-					Vue.set(this.modalProb, "preview", false);
-					if (this.modalProb.id == 0)
-					{
-						this.myProblems.unshift({
-							added: Date.now(),
-							id: response.id,
-							uid: user.id,
-							fen: this.modalProb.fen,
-							instructions: this.modalProb.instructions,
-							solution: this.modalProb.solution,
-						});
-						if (!this.curProb && this.display != "mine")
-							this.display = "mine";
-					}
-					else
-						this.modalProb.id = 0;
-				}
-			);
-		},
 	},
 })
diff --git a/client/src/App.vue b/client/src/App.vue
index a9c1d4cc..e06a2fef 100644
--- a/client/src/App.vue
+++ b/client/src/App.vue
@@ -8,7 +8,7 @@
     .row
       .col-sm-12.col-md-10.col-md-offset-1.col-lg-8.col-lg-offset-2
         // Menu (top of page):
-        // Left: hall, variants, mygames, problems
+        // Left: hall, variants, mygames, forum (ext. link)
         // Right: usermenu, settings, flag
         nav
           label.drawer-toggle(for="drawerControl")
@@ -22,8 +22,9 @@
                 | {{ st.tr["Variants"] }}
               router-link(to="/mygames")
                 | {{ st.tr["My games"] }}
-              router-link(to="/problems")
-                | {{ st.tr["Problems"] }}
+              // TODO: parametric URL, "forumURL"
+              a(href="https://forum.vchess.club")
+                | {{ st.tr["Forum"] }}
             #rightMenu
               .clickable(onClick="doClick('modalUser')")
                 | {{ st.user.id > 0 ? "Update" : "Login" }}
diff --git a/client/src/components/GameList.vue b/client/src/components/GameList.vue
index 8481048d..2b7f6573 100644
--- a/client/src/components/GameList.vue
+++ b/client/src/components/GameList.vue
@@ -8,8 +8,8 @@ table
     th(v-if="showResult") Result
   tr(v-for="g in games" @click="$emit('show-game',g)")
     td {{ g.vname }}
-    td {{ g.players[0] }}
-    td {{ g.players[1] }}
+    td {{ g.players[0].name || "@nonymous" }}
+    td {{ g.players[1].name || "@nonymous" }}
     td {{ g.timeControl }}
     td(v-if="showResult") {{ g.score }}
 </template>
@@ -20,7 +20,7 @@ export default {
 	props: ["games"],
 	computed: {
 		showResult: function() {
-			return this.games.length > 0 && this.games[0].score != "*";
+			return this.games.some(g => g.score != "*");
 		},
 	},
 };
diff --git a/client/src/router.js b/client/src/router.js
index 49f777d0..26bdee94 100644
--- a/client/src/router.js
+++ b/client/src/router.js
@@ -55,17 +55,26 @@ const router = new Router({
       component: Hall,
       //redirect: "/", //problem: redirection before end of AJAX request
     },
+    {
+      path: "/mygames",
+      name: "mygames",
+      component: loadView("MyGames"),
+    },
     {
       path: "/game/:id",
       name: "game",
       component: loadView("Game"),
     },
+    {
+      path: "/analyze/:vname([a-zA-Z0-9]+)",
+      name: "analyze",
+      component: loadView("Game"),
+    },
     {
       path: "/about",
       name: "about",
       component: loadView("About"),
     },
-    // TODO: myGames, problemId: https://router.vuejs.org/guide/essentials/dynamic-matching.html
   ]
 });
 
diff --git a/client/src/stylesheets/variant.sass b/client/src/stylesheets/variant.sass
index 4c00a626..a1181597 100644
--- a/client/src/stylesheets/variant.sass
+++ b/client/src/stylesheets/variant.sass
@@ -172,50 +172,4 @@ button.seek
   margin: 10px 0
 
 // Rules section:
-
-
-// Problems section:
-
-.newproblem-form input, .newproblem-form textarea
-  width: 100%
-
-.emphasis
-  font-style: italic
-
-#newpbInstructions
-  margin-bottom: var(--universal-margin);
-
-.center-btn
-  margin-left: 40%
-
-//TODO?
-.center-inline
-  text-align: center
-.center-block
-  margin-left: auto
-  margin-right: auto
-
-.mistake-newproblem
-  color: #663300
-
-#solution-div h3
-  background-color: lightgrey
-  padding: 3px 5px
-
-.newproblem-form, .newproblem-preview
-  max-width: 90%
-
-#problemControls
-  width: 75%
-  margin: 0 auto
-  @media screen and (max-width: 767px)
-    width: 100%
-    margin: 0
-
-.problem
-  margin: 10px 0
-
-.only-mine
-  background-color: yellow
-  &:hover
-    background-color: yellow
+// TODO
diff --git a/client/src/translations/en.js b/client/src/translations/en.js
index 0e3b668c..b4af338e 100644
--- a/client/src/translations/en.js
+++ b/client/src/translations/en.js
@@ -3,7 +3,7 @@ export const translations =
   "Hall": "Hall",
   "Variants": "Variants",
   "My games": "My games",
-  "Problems": "Problems",
+  "Forum": "Forum",
   "Contact form": "Contact form",
   "Source code": "Source code",
 
@@ -73,23 +73,7 @@ export const translations =
   "Type here": "Type here",
   "Send": "Send",
   "Download PGN": "Download PGN",
-  "Show solution": "Show solution",
-  "Load previous problems": "Load previous problems",
-  "Load next problems": "Load next problems",
-  "New": "New",
-  "Add a problem": "Add a problem",
-  "Full FEN description": "Full FEN description",
-  "Safe HTML tags allowed": "Safe HTML tags allowed",
-  "Instructions": "Instructions",
-  "Describe the problem goal": "Describe the problem goal",
-  "Solution": "Solution",
-  "How to solve the problem?": "How to solve the problem?",
-  "Preview": "Preview",
   "Cancel": "Cancel",
-  "Solve": "Solve",
-  "Bad FEN description": "Bad FEN description",
-  "Empty instructions": "Empty instructions",
-  "Empty solution": "Empty solution",
   "Already playing a game in this variant on another tab!":
     "Already playing a game in this variant on another tab!",
   "Finish your ": "Finish your ",
diff --git a/client/src/translations/fr.js b/client/src/translations/fr.js
index 4fa79575..815c2aa2 100644
--- a/client/src/translations/fr.js
+++ b/client/src/translations/fr.js
@@ -3,7 +3,7 @@ export const translations =
   "Hall": "Hall",
   "Variants": "Variantes",
   "My games": "Mes parties",
-  "Problems": "Problèmes",
+  "Forum": "Forum",
   "Contact form": "Formulaire de contact",
   "Source code": "Code source",
 
@@ -67,23 +67,7 @@ export const translations =
   "Type here": "Écrivez ici",
   "Send": "Envoyer",
   "Download PGN": "Télécharger le PGN",
-  "Show solution": "Montrer la solution",
-  "Load previous problems": "Charger les problèmes précédents",
-  "Load next problems": "Charger les problèmes suivants",
-  "New": "Nouveau",
-  "Add a problem": "Ajouter un problème",
-  "Full FEN description": "Description FEN complète",
-  "Safe HTML tags allowed": "HTML 'sûr' autorisé",
-  "Instructions": "Instructions",
-  "Describe the problem goal": "Décrire le but du problème",
-  "Solution": "Solution",
-  "How to solve the problem?": "Comment résoudre le problème ?",
-  "Preview": "Prévisualiser",
   "Cancel": "Annuler",
-  "Solve": "Résoudre",
-  "Bad FEN string": "Mauvaise description FEN",
-  "Empty instructions": "Instructions vides",
-  "Empty solution": "Solution vide",
   "Already playing a game in this variant on another tab!":
     "Une partie est en cours sur cette variante dans un autre onglet !",
   "Finish your ": "Terminez votre ",
diff --git a/client/src/views/Game.vue b/client/src/views/Game.vue
index b10be754..1cb9af58 100644
--- a/client/src/views/Game.vue
+++ b/client/src/views/Game.vue
@@ -12,7 +12,7 @@
     BaseGame(:game="game" :vr="vr" ref="basegame"
       @newmove="processMove" @gameover="gameOver")
     div Names: {{ game.players[0].name }} - {{ game.players[1].name }}
-    div Time: {{ virtualClocks[0] }} - {{ virtualClocks[1] }}
+    div(v-if="game.score=='*'") Time: {{ virtualClocks[0] }} - {{ virtualClocks[1] }}
     .button-group(v-if="game.mode!='analyze' && game.score=='*'")
       button(@click="offerDraw") Draw
       button(@click="() => abortGame()") Abort
@@ -24,7 +24,6 @@
 // TODO: movelist dans basegame et chat ici
 // ==> après, implémenter/vérifier les passages de challenges + parties en cours
 // observer,
-// + problèmes, habiller et publier. (+ corr...)
 // when send to chat (or a move), reach only this group (send gid along)
 -->
 
@@ -111,6 +110,8 @@ export default {
       this.gameRef.rid = this.$route.query["rid"];
       this.loadGame();
     }
+    // TODO: mode analyse (/analyze/Atomic/rn
+    // ... fen = query[], vname=params[] ...
     // 0.1] Ask server for room composition:
     const funcPollClients = () => {
       this.st.conn.send(JSON.stringify({code:"pollclients"}));
diff --git a/client/src/views/MyGames.vue b/client/src/views/MyGames.vue
index c630668f..d601bf04 100644
--- a/client/src/views/MyGames.vue
+++ b/client/src/views/MyGames.vue
@@ -1,5 +1,68 @@
-    // TODO: AJAX call get corr games (all variants)
-		// si dernier lastMove sur serveur n'est pas le mien et nextColor == moi, alors background orange
-		// ==> background orange si à moi de jouer par corr (sur main index)
-		// (helper: static fonction "GetNextCol()" dans base_rules.js)
-//use GameStorage.getAll()
+<template lang="pug">
+main
+  .row
+    .col-sm-12.col-md-10.col-md-offset-1.col-lg-8.col-lg-offset-2
+      .button-group
+        button(@click="display='live'") Live games
+        button(@click="display='corr'") Correspondance games
+      GameList(v-show="display=='live'" :games="filterGames('live')"
+        @show-game="showGame")
+      GameList(v-show="display=='corr'" :games="filterGames('corr')"
+        @show-game="showGame")
+</template>
+
+<script>
+// TODO: background orange si à moi de jouer
+// (helper: static fonction "GetNextCol()" dans base_rules.js)
+// use GameStorage.getAll()
+
+import { store } from "@/store";
+import { GameStorage } from "@/utils/gameStorage";
+import { ajax } from "@/utils/ajax";
+import GameList from "@/components/GameList.vue";
+export default {
+  name: "my-games",
+  components: {
+    GameList,
+  },
+  data: function() {
+    return {
+      st: store.state,
+			display: "live",
+      games: [],
+    };
+  },
+  created: function() {
+    GameStorage.getAll((localGames) => {
+      localGames.forEach((g) => g.type = this.classifyObject(g));
+      Array.prototype.push.apply(this.games, localGames);
+    });
+    if (this.st.user.id > 0)
+    {
+      ajax("/games", "GET", {uid: this.st.user.id}, (res) => {
+        res.games.forEach((g) => g.type = this.classifyObject(g));
+        //Array.prototype.push.apply(this.games, res.games); //TODO: Vue 3
+        this.games = this.games.concat(res.games);
+      });
+    }
+  },
+  methods: {
+    // TODO: classifyObject and filterGames are redundant (see Hall.vue)
+    classifyObject: function(o) {
+      return (o.timeControl.indexOf('d') === -1 ? "live" : "corr");
+    },
+    filterGames: function(type) {
+      return this.games.filter(g => g.type == type);
+    },
+    showGame: function(g) {
+      // NOTE: we play in this game, since this is "MyGames" page
+      this.$router.push("/game/" + g.id);
+    },
+  },
+};
+</script>
+
+<!-- Add "scoped" attribute to limit CSS to this component only -->
+<style scoped lang="sass">
+/* TODO */
+</style>
diff --git a/server/models/Game.js b/server/models/Game.js
index 786f1e78..0949fc98 100644
--- a/server/models/Game.js
+++ b/server/models/Game.js
@@ -98,19 +98,22 @@ const GameModel =
 				"SELECT gid " +
 				"FROM Players " +
 				"WHERE uid " + (excluded ? "<>" : "=") + " " + uid;
-			db.run(query, (err,gameIds) => {
+			db.all(query, (err,gameIds) => {
 				if (!!err)
 					return cb(err);
         gameIds = gameIds || []; //might be empty
 				let gameArray = [];
-				gameIds.forEach(gidRow => {
-					GameModel.getOne(gidRow["gid"], (err2,game) => {
+				for (let i=0; i<gameIds.length; i++)
+				{
+					GameModel.getOne(gameIds[i]["gid"], (err2,game) => {
 						if (!!err2)
 							return cb(err2);
 						gameArray.push(game);
+						// Call callback function only when gameArray is complete:
+						if (i == gameIds.length - 1)
+							return cb(null, gameArray);
 					});
-				});
-				return cb(null, gameArray);
+				}
 			});
 		});
 	},
diff --git a/server/routes/games.js b/server/routes/games.js
index 9bd9a30b..a444acdf 100644
--- a/server/routes/games.js
+++ b/server/routes/games.js
@@ -44,7 +44,7 @@ router.get("/games", access.ajax, (req,res) => {
     const userId = req.query["uid"];
     const excluded = !!req.query["excluded"];
     GameModel.getByUser(userId, excluded, (err,games) => {
-		  if (!!err)
+			if (!!err)
         return res.json({errmsg: err.errmsg || err.toString()});
 			res.json({games: games});
 	  });
-- 
2.48.1