Improve style, implement 'next' for corr games. TODO: rematch logic
[vchess.git] / client / src / views / Problems.vue
index 97e8c8e..a1c3e83 100644 (file)
@@ -1,8 +1,14 @@
 <template lang="pug">
 main
-  input#modalNewprob.modal(type="checkbox" @change="infoMsg=''")
-  div#newprobDiv(role="dialog" data-checkbox="modalNewprob")
-    .card(@keyup.enter="sendProblem()")
+  input#modalNewprob.modal(
+    type="checkbox"
+    @change="fenFocusIfOpened($event)"
+  )
+  div#newprobDiv(
+    role="dialog"
+    data-checkbox="modalNewprob"
+  )
+    .card
       label#closeNewprob.modal-close(for="modalNewprob")
       fieldset
         label(for="selectVariant") {{ st.tr["Variant"] }}
@@ -39,22 +45,20 @@ main
       button(@click="sendProblem()") {{ st.tr["Send"] }}
       #dialog.text-center {{ st.tr[infoMsg] }}
   .row(v-if="showOne")
-    .col-sm-12.col-md-9.col-md-offset-3.col-lg-10.col-lg-offset-2
+    .col-sm-12.col-md-10.col-md-offset-1.col-lg-8.col-lg-offset-2
       #topPage
-        span {{ curproblem.vname }}
+        .button-group(v-if="st.user.id == curproblem.uid")
+          button(@click="editProblem(curproblem)") {{ st.tr["Edit"] }}
+          button(@click="deleteProblem(curproblem)") {{ st.tr["Delete"] }}
+        span.vname {{ curproblem.vname }}
+        span.uname ({{ curproblem.uname }})
         button.marginleft(@click="backToList()") {{ st.tr["Back to list"] }}
-        button.nomargin(
-          v-if="st.user.id == curproblem.uid"
-          @click="editProblem(curproblem)"
-        )
-          | {{ st.tr["Edit"] }}
-        button.nomargin(
-          v-if="st.user.id == curproblem.uid"
-          @click="deleteProblem(curproblem)"
-        )
-          | {{ st.tr["Delete"] }}
-      p.clickable(
-        v-html="curproblem.uname + ' : ' + parseHtml(curproblem.instruction)"
+        button.nomargin(@click="gotoPrevNext($event,curproblem,1)")
+          | {{ st.tr["Previous"] }}
+        button.nomargin(@click="gotoPrevNext($event,curproblem,-1)")
+          | {{ st.tr["Next_p"] }}
+      p.oneInstructions.clickable(
+        v-html="parseHtml(curproblem.instruction)"
         @click="curproblem.showSolution=!curproblem.showSolution"
       )
         | {{ st.tr["Show solution"] }}
@@ -65,7 +69,7 @@ main
   .row(v-else)
     .col-sm-12.col-md-10.col-md-offset-1.col-lg-8.col-lg-offset-2
       #controls
-        button#newProblem(onClick="doClick('modalNewprob')")
+        button#newProblem(@click="prepareNewProblem()")
           | {{ st.tr["New problem"] }}
         label(for="checkboxMine") {{ st.tr["My problems"] }}
         input#checkboxMine(
@@ -79,18 +83,23 @@ main
             :value="v.id"
           )
             | {{ v.name }}
-      table
+      table#tProblems
         tr
           th {{ st.tr["Variant"] }}
           th {{ st.tr["Instructions"] }}
+          th {{ st.tr["Number"] }}
         tr(
           v-for="p in problems"
           v-show="displayProblem(p)"
           @click="setHrefPid(p)"
         )
           td {{ p.vname }}
-          td(v-html="p.instruction")
-  BaseGame(v-if="showOne" :game="game" :vr="vr")
+          td {{ firstChars(p.instruction) }}
+          td {{ p.id }}
+  BaseGame(
+    v-if="showOne"
+    :game="game"
+  )
 </template>
 
 <script>
@@ -104,14 +113,14 @@ import BaseGame from "@/components/BaseGame.vue";
 export default {
   name: "my-problems",
   components: {
-    BaseGame,
+    BaseGame
   },
   data: function() {
     return {
       st: store.state,
       emptyVar: {
         vid: 0,
-        vname: "",
+        vname: ""
       },
       // Problem currently showed, or edited:
       curproblem: {
@@ -121,7 +130,7 @@ export default {
         diag: "",
         instruction: "",
         solution: "",
-        showSolution: false,
+        showSolution: false
       },
       loadedVar: 0, //corresponding to loaded V
       selectedVar: 0, //to filter problems based on variant
@@ -129,67 +138,85 @@ export default {
       onlyMines: false,
       showOne: false,
       infoMsg: "",
-      vr: null, //"variant rules" object initialized from FEN
       game: {
-        players:[{name:"Problem"},{name:"Problem"}],
-        mode: "analyze",
-      },
+        players: [{ name: "Problem" }, { name: "Problem" }],
+        mode: "analyze"
+      }
     };
   },
   created: function() {
-    ajax("/problems", "GET", (res) => {
-      this.problems = res.problems;
+    ajax("/problems", "GET", res => {
+      // Show newest problem first:
+      this.problems = res.problems.sort((p1, p2) => p2.added - p1.added);
       if (this.st.variants.length > 0)
         this.problems.forEach(p => this.setVname(p));
       // Retrieve all problems' authors' names
       let names = {};
       this.problems.forEach(p => {
-        if (p.uid != this.st.user.id)
-          names[p.uid] = ""; //unknwon for now
-        else { console.log("assign " + this.st.user.name);
-          p.uname = this.st.user.name; console.log(p); console.log(this.problems); }
+        if (p.uid != this.st.user.id) names[p.uid] = "";
+        else p.uname = this.st.user.name;
       });
-      if (Object.keys(name).length > 0)
-      {
-        ajax("/users",
-          "GET",
-          { ids: Object.keys(names).join(",") },
-          res2 => {
-            res2.users.forEach(u => {names[u.id] = u.name});
-            this.problems.forEach(p => p.uname = names[p.uid]);
-          }
-        );
-      }
-      const pid = this.$route.query["id"];
-      if (!!pid)
-        this.showProblem(this.problems.find(p => p.id == pid));
+      const showOneIfPid = () => {
+        const pid = this.$route.query["id"];
+        if (pid) this.showProblem(this.problems.find(p => p.id == pid));
+      };
+      if (Object.keys(names).length > 0) {
+        ajax("/users", "GET", { ids: Object.keys(names).join(",") }, res2 => {
+          res2.users.forEach(u => {
+            names[u.id] = u.name;
+          });
+          this.problems.forEach(p => {
+            if (!p.uname)
+              p.uname = names[p.uid];
+          });
+          showOneIfPid();
+        });
+      } else showOneIfPid();
     });
   },
   mounted: function() {
-    document.getElementById("newprobDiv").addEventListener("click", processModalClick);
+    document
+      .getElementById("newprobDiv")
+      .addEventListener("click", processModalClick);
   },
   watch: {
     // st.variants changes only once, at loading from [] to [...]
-    "st.variants": function(variantArray) {
+    "st.variants": function() {
       // Set problems vname (either all are set or none)
       if (this.problems.length > 0 && this.problems[0].vname == "")
         this.problems.forEach(p => this.setVname(p));
     },
-    "$route": function(to, from) { console.log("ddddd");
+    $route: function(to) {
       const pid = to.query["id"];
-      if (!!pid)
-        this.showProblem(this.problems.find(p => p.id == pid));
-      else
-        this.showOne = false
-    },
+      if (pid) this.showProblem(this.problems.find(p => p.id == pid));
+      else this.showOne = false;
+    }
   },
   methods: {
+    fenFocusIfOpened: function(event) {
+      if (event.target.checked) {
+        this.infoMsg = "";
+        document.getElementById("inputFen").focus();
+      }
+    },
     setVname: function(prob) {
       prob.vname = this.st.variants.find(v => v.id == prob.vid).name;
     },
+    firstChars: function(text) {
+      let preparedText = text
+        // Replace line jumps and <br> by spaces
+        .replace(/\n/g, " ")
+        .replace(/<br\/?>/g, " ")
+        .replace(/<[^>]+>/g, "") //remove remaining HTML tags
+        .replace(/[ ]+/g, " ") //remove series of spaces by only one
+        .trim();
+      const maxLength = 32; //arbitrary...
+      if (preparedText.length > maxLength)
+        return preparedText.substr(0, 32) + "...";
+      return preparedText;
+    },
     copyProblem: function(p1, p2) {
-      for (let key in p1)
-        p2[key] = p1[key];
+      for (let key in p1) p2[key] = p1[key];
     },
     setHrefPid: function(p) {
       // Change href => $route changes, watcher notices, call showProblem
@@ -218,14 +245,10 @@ export default {
     },
     changeVariant: function(prob) {
       this.setVname(prob);
-      this.loadVariant(
-        prob.vid,
-        () => {
-          // Set FEN if possible (might not be correct yet)
-          if (V.IsGoodFen(prob.fen))
-            this.setDiagram(prob);
-        }
-      );
+      this.loadVariant(prob.vid, () => {
+        // Set FEN if possible (might not be correct yet)
+        if (V.IsGoodFen(prob.fen)) this.setDiagram(prob);
+      });
     },
     loadVariant: async function(vid, cb) {
       // Condition: vid is a valid variant ID
@@ -247,75 +270,92 @@ export default {
       const parsedFen = V.ParseFen(prob.fen);
       const args = {
         position: parsedFen.position,
-        orientation: parsedFen.turn,
+        orientation: parsedFen.turn
       };
       prob.diag = getDiagram(args);
     },
     displayProblem: function(p) {
-      return ((this.selectedVar == 0 || p.vid == this.selectedVar) &&
-        ((this.onlyMines && p.uid == this.st.user.id)
-          || (!this.onlyMines && p.uid != this.st.user.id)));
+      return (
+        (!this.selectedVar || p.vid == this.selectedVar) &&
+        ((this.onlyMines && p.uid == this.st.user.id) ||
+          (!this.onlyMines && p.uid != this.st.user.id))
+      );
     },
     showProblem: function(p) {
-      this.loadVariant(
-        p.vid,
-        () => {
-          // The FEN is already checked at this stage:
-          this.vr = new V(p.fen);
-          this.game.vname = p.vname;
-          this.game.mycolor = this.vr.turn; //diagram orientation
-          this.game.fen = p.fen;
-          this.$set(this.game, "fenStart", p.fen);
-          this.copyProblem(p, this.curproblem);
-          this.showOne = true;
-        }
-      );
+      this.loadVariant(p.vid, () => {
+        // The FEN is already checked at this stage:
+        this.game.vname = p.vname;
+        this.game.mycolor = V.ParseFen(p.fen).turn; //diagram orientation
+        this.game.fen = p.fen;
+        this.$set(this.game, "fenStart", p.fen);
+        this.copyProblem(p, this.curproblem);
+        this.showOne = true;
+      });
+    },
+    gotoPrevNext: function(e, prob, dir) {
+      const startIdx = this.problems.findIndex(p => p.id == prob.id);
+      let nextIdx = startIdx + dir;
+      while (
+        nextIdx >= 0 &&
+        nextIdx < this.problems.length &&
+        ((this.onlyMines && this.problems[nextIdx].uid != this.st.user.id) ||
+          (!this.onlyMines && this.problems[nextIdx].uid == this.st.user.id))
+      )
+        nextIdx += dir;
+      if (nextIdx >= 0 && nextIdx < this.problems.length)
+        this.setHrefPid(this.problems[nextIdx]);
+      else
+        alert(this.st.tr["No more problems"]);
+    },
+    prepareNewProblem: function() {
+      this.resetCurProb();
+      window.doClick("modalNewprob");
     },
     sendProblem: function() {
       const error = checkProblem(this.curproblem);
-      if (!!error)
-        return alert(error);
+      if (error) {
+        alert(this.st.tr[error]);
+        return;
+      }
       const edit = this.curproblem.id > 0;
       this.infoMsg = "Processing... Please wait";
       ajax(
         "/problems",
         edit ? "PUT" : "POST",
-        {prob: this.curproblem},
-        (ret) => {
-          if (edit)
-          {
+        { prob: this.curproblem },
+        ret => {
+          if (edit) {
             let editedP = this.problems.find(p => p.id == this.curproblem.id);
             this.copyProblem(this.curproblem, editedP);
+            this.showProblem(editedP);
           }
-          else //new problem
-          {
+          else {
             let newProblem = Object.assign({}, this.curproblem);
             newProblem.id = ret.id;
             newProblem.uid = this.st.user.id;
             newProblem.uname = this.st.user.name;
-            this.problems = this.problems.concat(newProblem);
+            this.problems = [newProblem].concat(this.problems);
           }
-          this.resetCurProb();
+          document.getElementById("modalNewprob").checked = false;
           this.infoMsg = "";
         }
       );
     },
     editProblem: function(prob) {
-      if (!prob.diag)
-        this.setDiagram(prob); //possible because V is loaded at this stage
+      // prob.diag might correspond to some other problem or be empty:
+      this.setDiagram(prob); //V is loaded at this stage
       this.copyProblem(prob, this.curproblem);
-      doClick('modalNewprob');
+      window.doClick("modalNewprob");
     },
     deleteProblem: function(prob) {
-      if (confirm(this.st.tr["Are you sure?"]))
-      {
-        ajax("/problems", "DELETE", {id:prob.id}, () => {
+      if (confirm(this.st.tr["Are you sure?"])) {
+        ajax("/problems", "DELETE", { id: prob.id }, () => {
           ArrayFun.remove(this.problems, p => p.id == prob.id);
           this.backToList();
         });
       }
-    },
-  },
+    }
+  }
 };
 </script>
 
@@ -323,27 +363,45 @@ export default {
 [type="checkbox"].modal+div .card
   max-width: 767px
   max-height: 100%
+
 #inputFen
   width: 100%
+
 textarea
   width: 100%
+
 #diagram
   margin: 0 auto
   max-width: 400px
+
+table#tProblems
+  max-height: 100%
+
 #controls
   margin: 0
   width: 100%
   text-align: center
   & > *
     margin: 0
+
+p.oneInstructions
+  margin: 0
+  padding: 2px 5px
+  background-color: lightgreen
+
 #topPage
-  span
+  span.vname
     font-weight: bold
     padding-left: var(--universal-margin)
+  span.uname
+    padding-left: var(--universal-margin)
   margin: 0 auto
   & > .nomargin
     margin: 0
   & > .marginleft
     margin: 0 0 0 15px
 
+@media screen and (max-width: 767px)
+  #topPage
+    text-align: center
 </style>