Draft of a problems section + news system
authorBenjamin Auder <benjamin.auder@somewhere>
Fri, 14 Feb 2020 16:18:52 +0000 (17:18 +0100)
committerBenjamin Auder <benjamin.auder@somewhere>
Fri, 14 Feb 2020 16:18:52 +0000 (17:18 +0100)
16 files changed:
client/src/App.vue
client/src/components/ComputerGame.vue
client/src/router.js
client/src/translations/en.js
client/src/translations/es.js
client/src/translations/fr.js
client/src/views/Analyse.vue
client/src/views/Game.vue
client/src/views/News.vue [new file with mode: 0644]
client/src/views/Problems.vue
server/db/create.sql
server/models/News.js [new file with mode: 0644]
server/models/Problem.js
server/routes/all.js
server/routes/news.js [new file with mode: 0644]
server/routes/problems.js

index d720b6e..6183d66 100644 (file)
               .clickable#flagContainer(onClick="doClick('modalLang')")
                 img(v-if="!!st.lang" :src="flagImage")
     router-view
-    .row
-      .col-sm-12.col-md-10.col-md-offset-1.col-lg-8.col-lg-offset-2
-        footer
-          router-link.menuitem(to="/about") {{ st.tr["About"] }}
-          p.clickable(onClick="doClick('modalContact')")
-            | {{ st.tr["Contact"] }}
+  .row
+    .col-sm-12.col-md-10.col-md-offset-1.col-lg-8.col-lg-offset-2
+      footer
+        router-link.menuitem(to="/about") {{ st.tr["About"] }}
+        router-link.menuitem(to="/news") {{ st.tr["News"] }}
+        p.clickable(onClick="doClick('modalContact')")
+          | {{ st.tr["Contact"] }}
 </template>
 
 <script>
@@ -99,6 +100,8 @@ body
   -moz-osx-font-smoothing: grayscale
 
 .container
+  // 45px is footer height
+  min-height: calc(100vh - 45px)
   overflow: hidden
   @media screen and (max-width: 767px)
     padding: 0
@@ -197,12 +200,13 @@ nav
       border-top: 0
 
 footer
+  height: 45px
   border: 1px solid #ddd
+  box-sizing: border-box
   //background-color: #000033
   font-size: 1rem
   width: 100%
-  padding-left: 0
-  padding-right: 0
+  padding: 0
   display: inline-flex
   align-items: center
   justify-content: center
@@ -211,7 +215,7 @@ footer
     text-decoration: none
   & > .menuitem
     display: inline-block
-    margin: 0 10px
+    margin: 0 12px
     &:link
       color: #2c3e50
     &:visited, &:hover
@@ -219,7 +223,7 @@ footer
       text-decoration: none
   & > p
     display: inline-block
-    margin: 0 10px
+    margin: 0 12px
 
 @media screen and (max-width: 767px)
   footer
index b20b8fe..3bbdc04 100644 (file)
@@ -1,6 +1,5 @@
 <template lang="pug">
-BaseGame(:game="game" :vr="vr" ref="basegame"
-  @newmove="processMove" @gameover="gameOver")
+BaseGame(:game="game" :vr="vr" @newmove="processMove" @gameover="gameOver")
 </template>
 
 <script>
index 071af8c..daaefce 100644 (file)
@@ -63,6 +63,11 @@ const router = new Router({
       name: "about",
       component: loadView("About"),
     },
+    {
+      path: "/news",
+      name: "news",
+      component: loadView("News"),
+    },
   ]
 });
 
index 05a62bc..b912ef3 100644 (file)
@@ -9,6 +9,7 @@ export const translations =
   "Are you sure?": "Are you sure?",
   "Authentication successful!": "Authentication successful!",
   "Apply": "Apply",
+  "Back to list": "Back to list",
   "Black": "Black",
   "Black to move": "Black to move",
   "Black win": "Black win",
@@ -25,9 +26,11 @@ export const translations =
   "Correspondance challenges": "Correspondance challenges",
   "Correspondance games": "Correspondance games",
   "Database error:": "Database error:",
+  "Delete": "Delete",
   "Download": "Download",
   "Draw": "Draw",
   "Draw offer only in your turn": "Draw offer only in your turn",
+  "Edit": "Edit",
   "Email": "Email",
   "Email sent!": "Email sent!",
   "Empty message": "Empty message",
@@ -44,6 +47,7 @@ export const translations =
   "Language": "Language",
   "Live challenges": "Live challenges",
   "Live games": "Live games",
+  "Load more": "Load more",
   "Login": "Login",
   "Logout": "Logout",
   "Logout successful!": "Logout successful!",
@@ -57,6 +61,7 @@ export const translations =
   "New correspondance game:": "New correspondance game:",
   "New game": "New game",
   "New problem": "New problem",
+  "News": "News",
   "No subject. Send anyway?": "No subject. Send anyway?",
   "None": "None",
   "Notifications by email": "Notifications by email",
@@ -83,9 +88,9 @@ export const translations =
   "Send": "Send",
   "Self-challenge is forbidden": "Self-challenge is forbidden",
   "Send challenge": "Send challenge",
-  "Send problem": "Send problem",
   "Settings": "Settings",
   "Show possible moves?": "Show possible moves?",
+  "Show solution": "Show solution",
   "Solution": "Solution",
   "Stop game": "Stop game",
   "Subject": "Subject",
@@ -101,6 +106,7 @@ export const translations =
   "White to move": "White to move",
   "White win": "White win",
   "Who's there?": "Who's there?",
+  "Write news": "Write news",
   "Your message": "Your message",
 
   // Variants boxes:
index 38c96b0..cbc5c64 100644 (file)
@@ -9,6 +9,7 @@ export const translations =
   "Apply": "Aplicar",
   "Are you sure?": "¿Está usted seguro?",
   "Authentication successful!": "¡Autenticación exitosa!",
+  "Back to list": "Volver a la lista",
   "Black": "Negras",
   "Black to move": "Juegan las negras",
   "Black win": "Las negras gagnan",
@@ -25,9 +26,11 @@ export const translations =
   "Correspondance challenges": "Desafíos por correspondencia",
   "Correspondance games": "Partidas por correspondencia",
   "Database error:": "Error de la base de datos:",
+  "Delete": "Borrar",
   "Download": "Descargar",
   "Draw": "Tablas",
   "Draw offer only in your turn": "Oferta de tablas solo en tu turno",
+  "Edit": "Editar",
   "Email": "Email",
   "Email sent!": "¡Email enviado!",
   "Empty message": "Mensaje vacio",
@@ -44,6 +47,7 @@ export const translations =
   "Language": "Idioma",
   "Live challenges": "Desafíos en vivo",
   "Live games": "Partidas en vivo",
+  "Load more": "Cargar más",
   "Login": "Login",
   "Logout": "Logout",
   "Logout successful!": "¡Desconexión exitosa!",
@@ -57,6 +61,7 @@ export const translations =
   "New correspondance game:": "Nueva partida por correspondencia:",
   "New game": "Nueva partida",
   "New problem": "Nuevo problema",
+  "News": "Noticias",
   "No subject. Send anyway?": "Sin asunto. ¿Enviar sin embargo?",
   "None": "Ninguno",
   "Notifications by email": "Notificaciones por email",
@@ -83,9 +88,9 @@ export const translations =
   "Send": "Enviar",
   "Self-challenge is forbidden": "Auto desafío está prohibido",
   "Send challenge": "Enviar desafío",
-  "Send problem": "Enviar problema",
   "Settings": "Configuraciones",
   "Show possible moves?": "¿Mostrar posibles movimientos?",
+  "Show solution": "Mostrar la solución",
   "Solution": "Solución",
   "Stop game": "Terminar la partida",
   "Subject": "Asunto",
@@ -101,6 +106,7 @@ export const translations =
   "White to move": "Juegan las blancas",
   "White win": "Las blancas gagnan",
   "Who's there?": "¿Quién está ahí?",
+  "Write news": "Escribir una news",
   "Your message": "Tu mensaje",
 
   // Variants boxes:
index dada230..1510f44 100644 (file)
@@ -9,6 +9,7 @@ export const translations =
   "Apply": "Appliquer",
   "Authentication successful!": "Authentification réussie !",
   "Are you sure?": "Étes vous sûr?",
+  "Back to list": "Retour à la liste",
   "Black": "Noirs",
   "Black to move": "Trait aux noirs",
   "Black win": "Les noirs gagnent",
@@ -25,9 +26,11 @@ export const translations =
   "Correspondance challenges": "Défis par correspondance",
   "Correspondance games": "Parties par correspondance",
   "Database error:": "Erreur de base de données :",
+  "Delete": "Supprimer",
   "Download": "Télécharger",
   "Draw": "Nulle",
   "Draw offer only in your turn": "Proposition de nulle seulement sur votre temps",
+  "Edit": "Éditer",
   "Email": "Email",
   "Email sent!": "Email envoyé !",
   "Empty message": "Message vide",
@@ -44,6 +47,7 @@ export const translations =
   "Language": "Langue",
   "Live challenges": "Défis en direct",
   "Live games": "Parties en direct",
+  "Load more": "Charger plus",
   "Login": "Login",
   "Logout": "Logout",
   "Logout successful!": "Déconnection réussie !",
@@ -57,6 +61,7 @@ export const translations =
   "New correspondance game:": "Nouvelle partie par corespondance :",
   "New game": "Nouvelle partie",
   "New problem": "Nouveau problème",
+  "News": "Nouvelles",
   "No subject. Send anyway?": "Pas de sujet. Envoyer quand-même ??",
   "None": "Aucun",
   "Notifications by email": "Notifications par email",
@@ -83,9 +88,9 @@ export const translations =
   "Send": "Envoyer",
   "Self-challenge is forbidden": "Interdit de s'auto-défier",
   "Send challenge": "Envoyer défi",
-  "Send problem": "Envoyer problème",
   "Settings": "Réglages",
   "Show possible moves?": "Montrer les coups possibles ?",
+  "Show solution": "Montrer la solution",
   "Solution": "Solution",
   "Stop game": "Arrêter la partie",
   "Subject": "Sujet",
@@ -101,6 +106,7 @@ export const translations =
   "White to move": "Trait aux blancs",
   "White win": "Les blancs gagnent",
   "Who's there?": "Qui est là ?",
+  "Write news": "Écrire une news",
   "Your message": "Votre message",
 
   // Variants boxes:
index c667290..035cd08 100644 (file)
@@ -5,7 +5,7 @@ main
       .text-center
         input#fen(v-model="curFen" @input="adjustFenSize()")
         button(@click="gotoFen()") {{ st.tr["Go"] }}
-  BaseGame(:game="game" :vr="vr" ref="basegame")
+  BaseGame(:game="game" :vr="vr")
 </template>
 
 <script>
@@ -27,7 +27,7 @@ export default {
       },
       game: {
         players:[{name:"Analyse"},{name:"Analyse"}],
-        mode: "analyze"
+        mode: "analyze",
       },
       vr: null, //"variant rules" object initialized from FEN
       curFen: "",
index 88f2b5e..1b82d81 100644 (file)
@@ -30,8 +30,7 @@ main
           span.name(:class="{connected: isConnected(1)}")
             | {{ game.players[1].name || "@nonymous" }}
           span.time(v-if="game.score=='*'") {{ virtualClocks[1] }}
-  BaseGame(:game="game" :vr="vr" ref="basegame"
-    @newmove="processMove" @gameover="gameOver")
+  BaseGame(:game="game" :vr="vr" @newmove="processMove" @gameover="gameOver")
 </template>
 
 <script>
diff --git a/client/src/views/News.vue b/client/src/views/News.vue
new file mode 100644 (file)
index 0000000..e23a6e2
--- /dev/null
@@ -0,0 +1,166 @@
+<template lang="pug">
+main
+  input#modalNews.modal(type="checkbox")
+  div#newnewsDiv(role="dialog" data-checkbox="modalNews")
+    .card
+      label.modal-close(for="modalNews")
+      textarea#newsContent(
+        v-model="curnews.content"
+        :placeholder="st.tr['News go here']"
+        @input="adjustHeight"
+      )
+      button(@click="sendNews()") {{ st.tr["Send"] }}
+      #dialog.text-center {{ st.tr[infoMsg] }}
+  .row
+    .col-sm-12.col-md-10.col-md-offset-1.col-lg-8.col-lg-offset-2
+      button(
+        v-if="devs.includes(st.user.id)"
+        @click="showModalNews"
+      )
+        | {{ st.tr["Write news"] }}
+      .news(v-for="n in sortedNewsList")
+        h4 {{ formatDatetime(n.added) }}
+        p(v-html="parseHtml(n.content)")
+        div(v-if="devs.includes(st.user.id)")
+          button(@click="editNews(n)") {{ st.tr["Edit"] }}
+          button(@click="deleteNews(n)") {{ st.tr["Delete"] }}
+      button(v-if="hasMore" @click="loadMore()")
+        | {{ st.tr["Load more"] }}
+</template>
+
+<script>
+import { store } from "@/store";
+import { ajax } from "@/utils/ajax";
+import { getDate, getTime } from "@/utils/datetime";
+import { processModalClick } from "@/utils/modalClick";
+export default {
+  name: "my-news",
+  data: function() {
+    return {
+      devs: [1], //for now the only dev is me
+      st: store.state,
+      cursor: 0, //ID of last showed news
+      hasMore: true, //a priori there could be more news to load
+      curnews: {id:0, content:""},
+      newsList: [],
+      infoMsg: "",
+    };
+  },
+  created: function() {
+    ajax("/news", "GET", {cursor:this.cursor}, (res) => {
+      this.newsList = res.newsList;
+      const L = res.newsList.length;
+      if (L > 0)
+        this.cursor = res.newsList[L-1].id;
+    });
+  },
+  mounted: function() {
+    document.getElementById("newnewsDiv").addEventListener("click", processModalClick);
+  },
+  computed: {
+    sortedNewsList: function() {
+      return this.newsList.sort( (n1,n2) => n1.added - n2.added );
+    },
+  },
+  methods: {
+    formatDatetime: function(dt) {
+      const dtObj = new Date(dt);
+      return getDate(dtObj) + " " + getTime(dtObj);
+    },
+    parseHtml: function(txt) {
+      return !txt.match(/<[/a-zA-Z]+>/)
+        ? txt.replace(/\n/g, "<br/>") //no HTML tag
+        : txt;
+    },
+    adjustHeight: function() {
+      const newsContent = document.getElementById("newsContent");
+      // https://stackoverflow.com/questions/995168/textarea-to-resize-based-on-content-length
+      newsContent.style.height = "1px";
+      newsContent.style.height = (10+newsContent.scrollHeight)+"px";
+    },
+    resetCurnews: function() {
+      this.curnews.id = 0;
+      this.curnews.content = "";
+      // No need for added and uid fields: never updated
+    },
+    showModalNews: function() {
+      this.resetCurnews();
+      doClick('modalNews');
+    },
+    sendNews: function() {
+      const edit = this.curnews.id > 0;
+      this.infoMsg = "Processing... Please wait";
+      ajax(
+        "/news",
+        edit ? "PUT" : "POST",
+        {news: this.curnews},
+        (res) => {
+          if (edit)
+          {
+            let n = this.newsList.find(n => n.id == this.curnews.id);
+            if (!!n)
+              n.content = this.curnews.content;
+          }
+          else
+          {
+            const newNews = {
+              content:this.curnews.content,
+              added:Date.now(),
+              uid: this.st.user.id,
+              id: res.id
+            };
+            this.newsList = this.newsList.concat([newNews]);
+          }
+          document.getElementById("modalNews").checked = false;
+          this.infoMsg = "";
+          this.resetCurnews();
+        }
+      );
+    },
+    editNews: function(n) {
+      this.curnews.content = n.content;
+      this.curnews.id = n.id;
+      // No need for added and uid fields: never updated
+      doClick('modalNews');
+    },
+    deleteNews: function(n) {
+      if (confirm(this.st.tr["Are you sure?"]))
+      {
+        this.infoMsg = "Processing... Please wait";
+        ajax("/news", "DELETE", {id:n.id}, () => {
+          const nIdx = this.newsList.findIndex(nw => nw.id == n.id);
+          this.newsList.splice(nIdx, 1);
+          this.infoMsg = "";
+          document.getElementById("modalNews").checked = false;
+        });
+      }
+    },
+    loadMore: function() {
+      ajax("/news", "GET", {cursor:this.cursor}, (res) => {
+        if (res.newsList.length > 0)
+        {
+          this.newsList = this.newsList.concat(res.newsList);
+          const L = res.newsList.length;
+          if (L > 0)
+            this.cursor = res.newsList[L-1].id;
+        }
+        else
+          this.hasMore = false;
+      });
+    },
+  },
+};
+</script>
+
+<style lang="sass" scoped>
+[type="checkbox"].modal+div .card
+  max-width: 767px
+  max-height: 100%
+textarea#newsContent
+  width: 100%
+  min-height: 200px
+  max-height: 100%
+#dialog
+  padding: 5px
+  color: blue
+</style>
index 4f832fa..9eca44b 100644 (file)
 main
   input#modalNewprob.modal(type="checkbox" @change="infoMsg=''")
   div#newprobDiv(role="dialog" data-checkbox="modalNewprob")
-    .card(@keyup.enter="newProblem()")
+    .card(@keyup.enter="sendProblem()")
       label#closeNewprob.modal-close(for="modalNewprob")
-      form(@submit.prevent="newProblem()" @keyup.enter="newProblem()")
-        fieldset
-          label(for="selectVariant") {{ st.tr["Variant"] }}
-          select#selectVariant(v-model="newproblem.vid" @change="loadVariant()")
-            option(v-for="v in [emptyVar].concat(st.variants)" :value="v.id"
-                :selected="newproblem.vid==v.id")
-              | {{ v.name }}
-        fieldset
-          label(for="inputFen") FEN
-          input#inputFen(type="text" v-model="newproblem.fen" @input="tryGetDiagram()")
-        fieldset
-          textarea#instructions(:placeholder="st.tr['Instructions']")
-          textarea#solution(:placeholder="st.tr['Solution']")
-      #preview
-        div(v-html="curDiag")
-        p instru: v-html=... .replace("\n", "<br/>") --> si pas de tags détectés !
-        p solution: v-html=...
-      button(@click="newProblem()") {{ st.tr["Send problem"] }}
+      fieldset
+        label(for="selectVariant") {{ st.tr["Variant"] }}
+        select#selectVariant(
+          v-model="curproblem.vid"
+          @change="changeVariant(curproblem)"
+        )
+          option(
+            v-for="v in [emptyVar].concat(st.variants)"
+            :value="v.id"
+            :selected="curproblem.vid==v.id"
+          )
+            | {{ v.name }}
+      fieldset
+        label(for="inputFen") FEN
+        input#inputFen(
+          type="text"
+          v-model="curproblem.fen"
+          @input="trySetDiagram(curproblem)"
+        )
+        div(v-html="curproblem.diag")
+      fieldset
+        textarea#instructions(
+          :placeholder="st.tr['Instructions']"
+          v-model="curproblem.instruction"
+        )
+        p(v-html="parseHtml(curproblem.instruction)")
+      fieldset
+        textarea#solution(
+          :placeholder="st.tr['Solution']"
+          v-model="curproblem.solution"
+        )
+        p(v-html="parseHtml(curproblem.solution)")
+      button(@click="sendProblem()") {{ st.tr["Send"] }}
       #dialog.text-center {{ st.tr[infoMsg] }}
   .row
     .col-sm-12
-      button#newProblem(onClick="doClick('modalNewprob')") {{ st.tr["New problem"] }}
-  .row
+      button#newProblem(onClick="doClick('modalNewprob')")
+        | {{ st.tr["New problem"] }}
+  .row(v-if="showOne")
+    .col-sm-12.col-md-10.col-md-offset-1.col-lg-8.col-lg-offset-2
+      #actions
+        button(@click="showOne=false") {{ st.tr["Back to list"] }}
+        button(
+          v-if="st.user.id == curproblem.uid"
+          @click="editProblem(curproblem)"
+        )
+          | {{ st.tr["Edit"] }}
+        button(
+          v-if="st.user.id == curproblem.uid"
+          @click="deleteProblem(curproblem)"
+        )
+          | {{ st.tr["Delete"] }}
+      h4 {{ curproblem.vname }}
+      p(v-html="parseHtml(curproblem.instruction)")
+      h4(@click="curproblem.showSolution=!curproblem.showSolution")
+        | {{ st.tr["Show solution"] }}
+      p(
+        v-show="curproblem.showSolution"
+        v-html="parseHtml(curproblem.solution)"
+      )
+  .row(v-else)
     .col-sm-12.col-md-10.col-md-offset-1.col-lg-8.col-lg-offset-2
       label(for="checkboxMine") {{ st.tr["My problems"] }}
-      input#checkboxMine(type="checkbox" v-model="onlyMines")
+      input#checkboxMine(
+        type="checkbox"
+        v-model="onlyMines"
+      )
       label(for="selectVariant") {{ st.tr["Variant"] }}
-      select#selectVariant(v-model="newproblem.vid")
-        option(v-for="v in [emptyVar].concat(st.variants)" :value="v.id")
+      select#selectVariant(v-model="selectedVar")
+        option(
+          v-for="v in [emptyVar].concat(st.variants)"
+          :value="v.id"
+        )
           | {{ v.name }}
-      // TODO: nice problems printing :: same as in preview ==> subComponent (inlined?)
-      div(v-for="p in problems" v-show="showProblem(p)")
-        p {{ p.vid }}
+      div(
+        v-for="p in problems"
+        v-show="displayProblem(p)"
+        @click="showProblem(p)"
+      )
+        h4 {{ p.vname }}
         p {{ p.fen }}
-        p {{ p.instruction }}
-        p {{ p.solution }}
+        p(v-html="p.instruction")
+  BaseGame(v-if="showOne" :game="game" :vr="vr")
 </template>
 
 <script>
+// TODO: si showProblem(p), changer URL (ajouter problem ID)
+// Et si au lancement l'URL comprend un pid, alors showOne=true et curproblem=...
+
 import { store } from "@/store";
 import { ajax } from "@/utils/ajax";
 import { checkProblem } from "@/data/problemCheck";
 import { getDiagram } from "@/utils/printDiagram";
+import BaseGame from "@/components/BaseGame.vue";
 export default {
   name: "my-problems",
+  components: {
+    BaseGame,
+  },
   data: function() {
     return {
+      st: store.state,
       emptyVar: {
         vid: 0,
         vname: "",
       },
-      newproblem: {
+      // Problem currently showed, or edited:
+      curproblem: {
+        id: 0, //used in case of edit
         vid: 0,
         fen: "",
+        diag: "",
         instruction: "",
         solution: "",
+        showSolution: false,
       },
-      onlyMines: false,
-      st: store.state,
+      loadedVar: 0, //corresponding to loaded V
+      selectedVar: 0, //to filter problems based on variant
       problems: [],
+      onlyMines: false,
+      showOne: false,
       infoMsg: "",
-      curVar: 0,
-      curDiag: "",
+      vr: null, //"variant rules" object initialized from FEN
+      game: {
+        players:[{name:"Problem"},{name:"Problem"}],
+        mode: "analyze",
+      },
     };
   },
   created: function() {
     ajax("/problems", "GET", (res) => {
       this.problems = res.problems;
+      if (this.st.variants.length > 0)
+        this.problems.forEach(p => this.setVname(p))
     });
   },
+  watch: {
+    // st.variants changes only once, at loading from [] to [...]
+    "st.variants": function(variantArray) {
+      // 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));
+    },
+  },
   methods: {
-    showProblem: function(p) {
-      return (this.vid == 0 || p.vid == this.vid) &&
-        (!this.onlyMines || p.uid != this.st.user.id);
+    setVname: function(prob) {
+      prob.vname = this.st.variants.find(v => v.id == prob.vid).name;
+    },
+    copyProblem: function(p1, p2) {
+      for (let key in p1)
+        p2[key] = p1[key];
+    },
+    resetCurProb: function() {
+      this.curproblem.id = 0;
+      this.curproblem.uid = 0;
+      this.curproblem.vid = "";
+      this.curproblem.vname = "";
+      this.curproblem.fen = "";
+      this.curproblem.diag = "";
+      this.curproblem.instruction = "";
+      this.curproblem.solution = "";
+      this.curproblem.showSolution = false;
     },
-    loadVariant: async function() {
-      if (this.newproblem.vid == 0)
-        return;
-      this.curVar = 0;
-      const variant = this.st.variants.find(v => v.id == this.newproblem.vid);
+    parseHtml: function(txt) {
+      return !txt.match(/<[/a-zA-Z]+>/)
+        ? txt.replace(/\n/g, "<br/>") //no HTML tag
+        : txt;
+    },
+    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);
+        }
+      );
+    },
+    loadVariant: async function(vid, cb) {
+      // Condition: vid is a valid variant ID
+      this.loadedVar = 0;
+      const variant = this.st.variants.find(v => v.id == vid);
       const vModule = await import("@/variants/" + variant.name + ".js");
       window.V = vModule.VariantRules;
-      this.curVar = this.newproblem.vid;
-      this.tryGetDiagram(); //the FEN might be already filled
+      this.loadedVar = vid;
+      cb();
+    },
+    trySetDiagram: function(prob) {
+      // Problem edit: FEN could be wrong or incomplete,
+      // variant could not be ready, or not defined
+      if (prob.vid > 0 && this.loadedVar == prob.vid && V.IsGoodFen(prob.fen))
+        this.setDiagram(prob);
     },
-    tryGetDiagram: async function() {
-      if (this.newproblem.vid == 0)
-        return;
-      // Check through curVar if V is ready:
-      if (this.curVar == this.newproblem.vid && V.IsGoodFen(this.newproblem.fen))
-      {
-        const parsedFen = V.ParseFen(this.newproblem.fen);
-        const args = {
-          position: parsedFen.position,
-          orientation: parsedFen.turn,
-        };
-        this.curDiag = getDiagram(args);
-      }
-      else
-        this.curDiag = "<p>FEN not yet correct</p>";
+    setDiagram: function(prob) {
+      // Condition: prob.fen is correct and global V is ready
+      const parsedFen = V.ParseFen(prob.fen);
+      const args = {
+        position: parsedFen.position,
+        orientation: parsedFen.turn,
+      };
+      prob.diag = getDiagram(args);
     },
-    newProblem: function() {
-      const error = checkProblem(this.newproblem);
+    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)));
+    },
+    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;
+        }
+      );
+    },
+    sendProblem: function() {
+      const error = checkProblem(this.curproblem);
       if (!!error)
         return alert(error);
-      ajax("/problems", "POST", {prob:this.newproblem}, (ret) => {
-        this.infoMsg = this.st.tr["Problem sent!"];
-        let newProblem = Object.Assign({}, this.newproblem);
-        newProblem.id = ret.id;
-        this.problems = this.problems.concat(newProblem);
-      });
+      const edit = this.curproblem.id > 0;
+      this.infoMsg = "Processing... Please wait";
+      ajax(
+        "/problems",
+        edit ? "PUT" : "POST",
+        {prob: this.curproblem},
+        (ret) => {
+          if (edit)
+          {
+            let editedP = this.problems.find(p => p.id == this.curproblem.id);
+            this.copyProblem(this.curproblem, editedP);
+          }
+          else //new problem
+          {
+            let newProblem = Object.assign({}, this.curproblem);
+            newProblem.id = ret.id;
+            this.problems = this.problems.concat(newProblem);
+          }
+          this.resetCurProb();
+          this.infoMsg = "";
+        }
+      );
+    },
+    editProblem: function(prob) {
+      if (!prob.diag)
+        this.setDiagram(prob); //possible because V is loaded at this stage
+      this.copyProblem(prob, this.curproblem);
+      doClick('modalNewprob');
+    },
+    deleteProblem: function(prob) {
+      if (confirm(this.st.tr["Are you sure?"]))
+        ajax("/problems", "DELETE", {pid:prob.id});
     },
   },
 };
index 5bf3a3b..c05c87a 100644 (file)
@@ -20,6 +20,7 @@ create table Users (
 create table Problems (
   id integer primary key,
   added datetime,
+  fen varchar,
   uid integer,
   vid integer,
   instruction text,
@@ -28,6 +29,14 @@ create table Problems (
   foreign key (vid) references Variants(id)
 );
 
+create table News (
+  id integer primary key,
+  uid integer,
+  added datetime,
+  content text,
+  foreign key (uid) references Users(id)
+);
+
 create table Challenges (
   id integer primary key,
   added datetime,
diff --git a/server/models/News.js b/server/models/News.js
new file mode 100644 (file)
index 0000000..ebe65d0
--- /dev/null
@@ -0,0 +1,63 @@
+var db = require("../utils/database");
+
+/*
+ * Structure:
+ *   id: integer
+ *   added: datetime
+ *   uid: user id (int)
+ *   content: text
+ */
+
+const NewsModel =
+{
+  create: function(content, uid, cb)
+  {
+    db.serialize(function() {
+      const query =
+        "INSERT INTO News " +
+        "(added, uid, content) " +
+          "VALUES " +
+        "(" + Date.now() + "," + uid + ",?)";
+      db.run(query, content, function(err) {
+        return cb(err, {nid: this.lastID});
+      });
+    });
+  },
+
+  getNext: function(cursor, cb)
+  {
+    db.serialize(function() {
+      const query =
+        "SELECT * " +
+        "FROM News " +
+        "WHERE id > " + cursor + " " +
+        "LIMIT 10"; //TODO: 10 currently hard-coded
+      db.all(query, (err,newsList) => {
+        return cb(err, newsList);
+      });
+    });
+  },
+
+  update: function(news, cb)
+  {
+    db.serialize(function() {
+      let query =
+        "UPDATE News " +
+        "SET content = ? " +
+        "WHERE id = " + news.id;
+      db.run(query, news.content, cb);
+    });
+  },
+
+  remove: function(id, cb)
+  {
+    db.serialize(function() {
+      const query =
+        "DELETE FROM News " +
+        "WHERE id = " + id;
+      db.run(query, cb);
+    });
+  },
+}
+
+module.exports = NewsModel;
index 75c2e14..0e90025 100644 (file)
@@ -15,6 +15,8 @@ const ProblemModel =
 {
   checkProblem: function(p)
   {
+    if (!p.id.toString().match(/^[0-9]+$/))
+      return "Wrong problem ID";
     if (!p.vid.toString().match(/^[0-9]+$/))
       return "Wrong variant ID";
     if (!p.fen.match(/^[a-zA-Z0-9, /-]*$/))
@@ -29,8 +31,8 @@ const ProblemModel =
         "INSERT INTO Problems " +
         "(added, uid, vid, fen, instruction, solution) " +
           "VALUES " +
-        "(" + Date.now() + "," + p.uid + ",'" + p.fen  + "',?,?)";
-      db.run(query, p.instruction, p.solution, function(err) {
+        "(" + Date.now() + "," + p.uid + "," + p.vid + ",'" + p.fen  + "',?,?)";
+      db.run(query, [p.instruction,p.solution], function(err) {
         return cb(err, {pid: this.lastID});
       });
     });
@@ -61,18 +63,18 @@ const ProblemModel =
     });
   },
 
-  update: function(id, prob)
+  update: function(prob, cb)
   {
     db.serialize(function() {
       let query =
         "UPDATE Problems " +
         "SET " +
           "vid = " + prob.vid + "," +
-          "fen = " + prob.fen + "," +
-          "instruction = " + prob.instruction + "," +
-          "solution = " + prob.solution + " " +
-        "WHERE id = " + id;
-      db.run(query);
+          "fen = '" + prob.fen + "'," +
+          "instruction = ?," +
+          "solution = ? " +
+        "WHERE id = " + prob.id;
+      db.run(query, [prob.instruction,prob.solution], cb);
     });
   },
 
@@ -96,7 +98,7 @@ const ProblemModel =
       db.get(query, (err,prob) => {
         if (!prob)
           return cb({errmsg: "Not your problem"});
-        ProvlemModel.remove(id);
+        ProblemModel.remove(id);
         cb(null);
       });
     });
index cca315f..b58638e 100644 (file)
@@ -12,5 +12,6 @@ router.use("/", require("./messages"));
 router.use("/", require("./users"));
 router.use("/", require("./variants"));
 router.use("/", require("./problems"));
+router.use("/", require("./news"));
 
 module.exports = router;
diff --git a/server/routes/news.js b/server/routes/news.js
new file mode 100644 (file)
index 0000000..ed78456
--- /dev/null
@@ -0,0 +1,50 @@
+// AJAX methods to get, create, update or delete a problem
+
+let router = require("express").Router();
+const access = require("../utils/access");
+const NewsModel = require("../models/News");
+const sanitizeHtml = require('sanitize-html');
+const devs = [1]; //hard-coded list of developers, allowed to post news
+
+router.get("/news", (req,res) => {
+  const cursor = req.query["cursor"];
+  if (!cursor.match(/^[0-9]+$/))
+    return res.json({errmsg: "Bad cursor value"});
+  NewsModel.getNext(cursor, (err,newsList) => {
+    res.json(err || {newsList:newsList});
+  });
+});
+
+router.post("/news", access.logged, access.ajax, (req,res) => {
+  if (!devs.includes(req.userId))
+    return res.json({errmsg: "Not allowed to post"});
+  const content = sanitizeHtml(req.body.news.content);
+  NewsModel.create(content, req.userId, (err,ret) => {
+    return res.json(err || {nid:ret.nid});
+  });
+});
+
+router.put("/news", access.logged, access.ajax, (req,res) => {
+  if (!devs.includes(req.userId))
+    return res.json({errmsg: "Not allowed to edit"});
+  let news = req.body.news;
+  if (!news.id.toString().match(/^[0-9]+$/))
+    res.json({errmsg: "Bad news ID"});
+  news.content = sanitizeHtml(news.content);
+  NewsModel.update(news, (err) => {
+    res.json(err || {});
+  });
+});
+
+router.delete("/news", access.logged, access.ajax, (req,res) => {
+  if (!devs.includes(req.userId))
+    return res.json({errmsg: "Not allowed to delete"});
+  const nid = req.query.id;
+  if (!nid.toString().match(/^[0-9]+$/))
+    res.json({errmsg: "Bad news ID"});
+  NewsModel.remove(nid, err => {
+    res.json(err || {});
+  });
+});
+
+module.exports = router;
index c45a1ba..a0886db 100644 (file)
@@ -43,17 +43,13 @@ router.post("/problems", access.logged, access.ajax, (req,res) => {
 });
 
 router.put("/problems", access.logged, access.ajax, (req,res) => {
-  const pid = req.body.pid;
-  let error = "";
-  if (!pid.toString().match(/^[0-9]+$/))
-    error = "Wrong problem ID";
-  let obj = req.body.newProb;
-  error = ProblemModel.checkProblem(obj);
-  obj.instruction = sanitizeHtml(obj.instruction);
-  obj.solution = sanitizeHtml(obj.solution);
+  let obj = req.body.prob;
+  const error = ProblemModel.checkProblem(obj);
   if (!!error)
     return res.json({errmsg: error});
-  ProblemModel.update(pid, obj, (err) => {
+  obj.instruction = sanitizeHtml(obj.instruction);
+  obj.solution = sanitizeHtml(obj.solution);
+  ProblemModel.update(obj, (err) => {
     res.json(err || {});
   });
 });