Draft of problems section
authorBenjamin Auder <benjamin.auder@somewhere>
Fri, 14 Feb 2020 00:14:52 +0000 (01:14 +0100)
committerBenjamin Auder <benjamin.auder@somewhere>
Fri, 14 Feb 2020 00:14:52 +0000 (01:14 +0100)
21 files changed:
client/src/App.vue
client/src/base_rules.js
client/src/components/BaseGame.vue
client/src/components/Board.vue
client/src/components/ContactForm.vue
client/src/data/problemCheck.js [new file with mode: 0644]
client/src/router.js
client/src/translations/en.js
client/src/translations/es.js
client/src/translations/fr.js
client/src/views/Game.vue
client/src/views/Hall.vue
client/src/views/MyGames.vue
client/src/views/Problems.vue [new file with mode: 0644]
client/src/views/Rules.vue
server/db/create.sql
server/models/Game.js
server/models/Problem.js [new file with mode: 0644]
server/package-lock.json
server/routes/all.js
server/routes/problems.js [new file with mode: 0644]

index 4cd1124..d720b6e 100644 (file)
@@ -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
+        // Left: hall, variants, problems, mygames
         // Right: usermenu, settings, flag
         nav
           label.drawer-toggle(for="drawerControl")
@@ -20,6 +20,8 @@
                 | {{ st.tr["Hall"] }}
               router-link(to="/variants")
                 | {{ st.tr["Variants"] }}
+              router-link(to="/problems")
+                | {{ st.tr["Problems"] }}
               router-link(to="/mygames")
                 | {{ st.tr["My games"] }}
             #rightMenu
@@ -36,8 +38,6 @@
           router-link.menuitem(to="/about") {{ st.tr["About"] }}
           p.clickable(onClick="doClick('modalContact')")
             | {{ st.tr["Contact"] }}
-          a.menuitem(href="https://forum.vchess.club")
-            | {{ st.tr["Forum"] }}
 </template>
 
 <script>
@@ -182,7 +182,9 @@ nav
     border: none
     & > label.drawer-toggle
       font-size: 1.2rem
-      //padding: 0 0 0 10px
+      position: absolute
+      top: -12px
+      //padding: -5px 0 0 10px
 
 [type="checkbox"].drawer+*
   right: -767px
@@ -222,4 +224,50 @@ footer
 @media screen and (max-width: 767px)
   footer
     border: none
+
+// Styles for diagrams and board (partial).
+// TODO: where to put that ?
+
+.light-square-diag
+  background-color: #e5e5ca
+
+.dark-square-diag
+  background-color: #6f8f57
+
+div.board
+  float: left
+  height: 0
+  display: inline-block
+  position: relative
+
+div.board8
+  width: 12.5%
+  padding-bottom: 12.5%
+
+div.board10
+  width: 10%
+  padding-bottom: 10%
+
+div.board11
+  width: 9.09%
+  padding-bottom: 9.1%
+
+img.piece
+  width: 100%
+
+img.piece, img.mark-square
+  max-width: 100%
+  height: auto
+  display: block
+
+img.mark-square
+  opacity: 0.6
+  width: 76%
+  position: absolute
+  top: 12%
+  left: 12%
+  opacity: .7
+
+.in-shadow
+  filter: brightness(50%)
 </style>
index 20e123b..924f737 100644 (file)
@@ -59,7 +59,7 @@ export const ChessRules = class ChessRules
     return (f.charCodeAt()<=90 ? "w"+f.toLowerCase() : "b"+f);
   }
 
-  // Check if FEN describe a position
+  // Check if FEN describe a board situation correctly
   static IsGoodFen(fen)
   {
     const fenParsed = V.ParseFen(fen);
index a1539ba..b34eae9 100644 (file)
@@ -84,6 +84,11 @@ export default {
       if (!!newMove) //if stop + launch new game, get undefined move
         this.play(newMove, "receive");
     },
+    // ...Or to undo (corr game, move not validated)
+    "game.moveToUndo": function(move) {
+      if (!!move)
+        this.undo(move);
+    },
   },
   computed: {
     showMoves: function() {
index 8d429a3..1c259f9 100644 (file)
@@ -394,24 +394,6 @@ export default {
 
 // NOTE: no variants with reserve of size != 8
 
-div.board
-  float: left
-  height: 0
-  display: inline-block
-  position: relative
-
-div.board8
-  width: 12.5%
-  padding-bottom: 12.5%
-
-div.board10
-  width: 10%
-  padding-bottom: 10%
-
-div.board11
-  width: 9.09%
-  padding-bottom: 9.1%
-
 .game
   width: 100%
   margin: 0
@@ -434,22 +416,6 @@ div.board11
       height: auto
       display: block
 
-img.piece
-  width: 100%
-
-img.piece, img.mark-square
-  max-width: 100%
-  height: auto
-  display: block
-
-img.mark-square
-  opacity: 0.6
-  width: 76%
-  position: absolute
-  top: 12%
-  left: 12%
-  opacity: .7
-
 img.ghost
   position: absolute
   opacity: 0.4
@@ -458,9 +424,6 @@ img.ghost
 .highlight
   background-color: #00cc66 !important
 
-.in-shadow
-  filter: brightness(50%)
-
 .incheck
   background-color: #cc3300 !important
 
index 14273b0..61796bf 100644 (file)
@@ -12,9 +12,7 @@ div
           label(for="mailSubject") {{ st.tr["Subject"] }}
           input#mailSubject(type="text")
         fieldset
-          label(for="mailContent") {{ st.tr["Content"] }} *
-          br
-          textarea#mailContent
+          textarea#mailContent(:placeholder="st.tr['Your message']")
       button(@click="trySendMessage()") {{ st.tr["Send"] }}
       #dialog.text-center {{ st.tr[infoMsg] }}
 </template>
diff --git a/client/src/data/problemCheck.js b/client/src/data/problemCheck.js
new file mode 100644 (file)
index 0000000..3b9ccb4
--- /dev/null
@@ -0,0 +1,9 @@
+export function checkProblem(p)
+{
+  const vid = parseInt(p.vid);
+  if (isNaN(vid) || vid <= 0)
+    return "Please select a variant";
+
+  if (!V.IsGoodFen(p.fen))
+    return "Bad FEN string";
+}
index ede2186..071af8c 100644 (file)
@@ -38,6 +38,11 @@ const router = new Router({
       name: "logout",
       component: loadView("Logout"),
     },
+    {
+      path: "/problems",
+      name: "myproblems",
+      component: loadView("Problems"),
+    },
     {
       path: "/mygames",
       name: "mygames",
index 4d6a972..05a62bc 100644 (file)
@@ -6,6 +6,7 @@ export const translations =
   "All": "All",
   "Analyse": "Analyse",
   "Analyse in Dark mode makes no sense!": "Analyse in Dark mode makes no sense!",
+  "Are you sure?": "Are you sure?",
   "Authentication successful!": "Authentication successful!",
   "Apply": "Apply",
   "Black": "Black",
@@ -21,7 +22,6 @@ export const translations =
   "Chat here": "Chat here",
   "Connection token sent. Check your emails!": "Connection token sent. Check your emails!",
   "Contact": "Contact",
-  "Content": "Content",
   "Correspondance challenges": "Correspondance challenges",
   "Correspondance games": "Correspondance games",
   "Database error:": "Database error:",
@@ -33,7 +33,6 @@ export const translations =
   "Empty message": "Empty message",
   "Error while loading database:": "Error while loading database:",
   "Example game": "Example game",
-  "Forum": "Forum",
   "From": "From",
   "Game retrieval failed:": "Game retrieval failed:",
   "Game removal failed:": "Game removal failed:",
@@ -41,6 +40,7 @@ export const translations =
   "green": "green",
   "Hall": "Hall",
   "Highlight last move and checks?": "Highlight last move and checks?",
+  "Instructions": "Instructions",
   "Language": "Language",
   "Live challenges": "Live challenges",
   "Live games": "Live games",
@@ -50,11 +50,13 @@ export const translations =
   "Modifications applied!": "Modifications applied!",
   "Mutual agreement": "Mutual agreement",
   "My games": "My games",
+  "My problems": "My problems",
   "Name": "Name",
   "Name or Email": "Name or Email",
   "New connexion detected: tab now offline": "New connexion detected: tab now offline",
   "New correspondance game:": "New correspondance game:",
   "New game": "New game",
+  "New problem": "New problem",
   "No subject. Send anyway?": "No subject. Send anyway?",
   "None": "None",
   "Notifications by email": "Notifications by email",
@@ -69,6 +71,7 @@ export const translations =
   "Practice": "Practice",
   "Prefix?": "Prefix?",
   "Processing... Please wait": "Processing... Please wait",
+  "Problems": "Problems",
   "participant(s):": "participant(s):",
   "Register": "Register",
   "Registration complete! Please check your emails": "Registration complete! Please check your emails",
@@ -78,10 +81,12 @@ export const translations =
   "Result": "Result",
   "Rules": "Rules",
   "Send": "Send",
-  "Show possible moves?": "Show possible moves?",
   "Self-challenge is forbidden": "Self-challenge is forbidden",
   "Send challenge": "Send challenge",
+  "Send problem": "Send problem",
   "Settings": "Settings",
+  "Show possible moves?": "Show possible moves?",
+  "Solution": "Solution",
   "Stop game": "Stop game",
   "Subject": "Subject",
   "Terminate game?": "Terminate game?",
@@ -96,6 +101,7 @@ export const translations =
   "White to move": "White to move",
   "White win": "White win",
   "Who's there?": "Who's there?",
+  "Your message": "Your message",
 
   // Variants boxes:
   "Balanced sliders & leapers": "Balanced sliders & leapers",
index f4918f8..38c96b0 100644 (file)
@@ -7,6 +7,7 @@ export const translations =
   "Analyse": "Analizar",
   "Analyse in Dark mode makes no sense!": "¡Analizar en modo Dark no tiene sentido!",
   "Apply": "Aplicar",
+  "Are you sure?": "¿Está usted seguro?",
   "Authentication successful!": "¡Autenticación exitosa!",
   "Black": "Negras",
   "Black to move": "Juegan las negras",
@@ -21,7 +22,6 @@ export const translations =
   "Chat here": "Chat aquí",
   "Connection token sent. Check your emails!": "Token de conexión enviado. ¡Revisa tus correos!",
   "Contact": "Contacto",
-  "Content": "Contenido",
   "Correspondance challenges": "Desafíos por correspondencia",
   "Correspondance games": "Partidas por correspondencia",
   "Database error:": "Error de la base de datos:",
@@ -33,7 +33,6 @@ export const translations =
   "Empty message": "Mensaje vacio",
   "Error while loading database:": "Error al cargar la base de datos:",
   "Example game": "Ejemplo de partida",
-  "Forum": "Foro",
   "From": "De",
   "Game retrieval failed:": "La recuperación de la partida falló:",
   "Game removal failed:": "La eliminación de la partida falló:",
@@ -41,6 +40,7 @@ export const translations =
   "green": "verde",
   "Hall": "Salón",
   "Highlight last move and checks?": "¿Resaltar el último movimiento y jaques?",
+  "Instructions": "Instrucciones",
   "Language": "Idioma",
   "Live challenges": "Desafíos en vivo",
   "Live games": "Partidas en vivo",
@@ -50,11 +50,13 @@ export const translations =
   "Modifications applied!": "¡Modificaciones aplicadas!",
   "Mutual agreement": "Acuerdo mutuo",
   "My games": "Mis partidas",
+  "My problems": "Mis problemas",
   "Name": "Nombre",
   "Name or Email": "Nombre o Email",
   "New connexion detected: tab now offline": "Nueva conexión detectada: pestaña ahora desconectada",
   "New correspondance game:": "Nueva partida por correspondencia:",
   "New game": "Nueva partida",
+  "New problem": "Nuevo problema",
   "No subject. Send anyway?": "Sin asunto. ¿Enviar sin embargo?",
   "None": "Ninguno",
   "Notifications by email": "Notificaciones por email",
@@ -69,6 +71,7 @@ export const translations =
   "Practice": "Práctica",
   "Prefix?": "¿Prefijo?",
   "Processing... Please wait": "Procesando... por favor espere",
+  "Problems": "Problemas",
   "participant(s):": "participante(s):",
   "Register": "Registrarse",
   "Registration complete! Please check your emails": "¡Registro completo! Por favor revise sus correos electrónicos",
@@ -78,10 +81,12 @@ export const translations =
   "Result": "Resultado",
   "Rules": "Reglas",
   "Send": "Enviar",
-  "Show possible moves?": "¿Mostrar posibles movimientos?",
   "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?",
+  "Solution": "Solución",
   "Stop game": "Terminar la partida",
   "Subject": "Asunto",
   "Terminate game?": "¿Terminar la partida?",
@@ -96,6 +101,7 @@ export const translations =
   "White to move": "Juegan las blancas",
   "White win": "Las blancas gagnan",
   "Who's there?": "¿Quién está ahí?",
+  "Your message": "Tu mensaje",
 
   // Variants boxes:
   "Balanced sliders & leapers": "Modos de desplazamiento equilibrados",
index 6d10338..dada230 100644 (file)
@@ -8,6 +8,7 @@ export const translations =
   "Analyse in Dark mode makes no sense!": "Analyser en mode Dark n'a pas de sens !",
   "Apply": "Appliquer",
   "Authentication successful!": "Authentification réussie !",
+  "Are you sure?": "Étes vous sûr?",
   "Black": "Noirs",
   "Black to move": "Trait aux noirs",
   "Black win": "Les noirs gagnent",
@@ -21,7 +22,6 @@ export const translations =
   "Chat here": "Chattez ici",
   "Connection token sent. Check your emails!": "Token de connection envoyé. Allez voir vos emails !",
   "Contact": "Contact",
-  "Content": "Contenu",
   "Correspondance challenges": "Défis par correspondance",
   "Correspondance games": "Parties par correspondance",
   "Database error:": "Erreur de base de données :",
@@ -33,7 +33,6 @@ export const translations =
   "Empty message": "Message vide",
   "Error while loading database:": "Erreur lors du chargement de la base de données :",
   "Example game": "Partie exemple",
-  "Forum": "Forum",
   "From": "De",
   "Game retrieval failed:": "Échec de la récupération de la partie :",
   "Game removal failed:": "Échec de la suppresion de la partie :",
@@ -41,6 +40,7 @@ export const translations =
   "green": "vert",
   "Hall": "Salon",
   "Highlight last move and checks?": "Mettre en valeur le dernier coup et les échecs ?",
+  "Instructions": "Instructions",
   "Language": "Langue",
   "Live challenges": "Défis en direct",
   "Live games": "Parties en direct",
@@ -50,11 +50,13 @@ export const translations =
   "Modifications applied!": "Modifications effectuées !",
   "Mutual agreement": "Accord mutuel",
   "My games": "Mes parties",
+  "My problems": "Mes problèmes",
   "Name": "Nom",
   "Name or Email": "Nom ou Email",
   "New connexion detected: tab now offline": "Nouvelle connexion détectée : onglet désormais hors ligne",
   "New correspondance game:": "Nouvelle partie par corespondance :",
   "New game": "Nouvelle partie",
+  "New problem": "Nouveau problème",
   "No subject. Send anyway?": "Pas de sujet. Envoyer quand-même ??",
   "None": "Aucun",
   "Notifications by email": "Notifications par email",
@@ -69,6 +71,7 @@ export const translations =
   "Practice": "Pratiquer",
   "Prefix?": "Préfixe ?",
   "Processing... Please wait": "Traitement en cours... Attendez SVP",
+  "Problems": "Problèmes",
   "participant(s):": "participant(s) :",
   "Register": "S'enregistrer",
   "Registration complete! Please check your emails": "Enregistrement terminé ! Allez voir vos emails",
@@ -78,10 +81,12 @@ export const translations =
   "Result": "Résultat",
   "Rules": "Règles",
   "Send": "Envoyer",
-  "Show possible moves?": "Montrer les coups possibles ?",
   "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 ?",
+  "Solution": "Solution",
   "Stop game": "Arrêter la partie",
   "Subject": "Sujet",
   "Terminate game?": "Stopper la partie ?",
@@ -96,6 +101,7 @@ export const translations =
   "White to move": "Trait aux blancs",
   "White win": "Les blancs gagnent",
   "Who's there?": "Qui est là ?",
+  "Your message": "Votre message",
 
   // Variants boxes:
   "Balanced sliders & leapers": "Modes de déplacement équilibrés",
index 062990d..88f2b5e 100644 (file)
@@ -560,8 +560,13 @@ export default {
         GameStorage.get(this.gameRef.id, afterRetrieval);
       }
     },
-    // Post-process a move (which was just played)
+    // Post-process a move (which was just played in BaseGame)
     processMove: function(move) {
+      if (this.game.type == "corr" && move.color == this.game.mycolor)
+      {
+        if (!confirm(this.st.tr["Are you sure?"]))
+          return this.$set(this.game, "moveToUndo", move);
+      }
       // Update storage (corr or live) if I play in the game
       const colorIdx = ["w","b"].indexOf(move.color);
       const nextIdx = ["w","b"].indexOf(this.vr.turn);
index 9b58409..079584c 100644 (file)
@@ -7,28 +7,29 @@ main
       p(v-html="infoMessage")
   input#modalNewgame.modal(type="checkbox")
   div#newgameDiv(role="dialog" data-checkbox="modalNewgame")
-    .card(@keyup.enter="newChallenge()")
+    .card
       label#closeNewgame.modal-close(for="modalNewgame")
-      fieldset
-        label(for="selectVariant") {{ st.tr["Variant"] }} *
-        select#selectVariant(v-model="newchallenge.vid")
-          option(v-for="v in st.variants" :value="v.id"
-              :selected="newchallenge.vid==v.id")
-            | {{ v.name }}
-      fieldset
-        label(for="cadence") {{ st.tr["Cadence"] }} *
-        div#predefinedCadences
-          button 3+2
-          button 5+3
-          button 15+5
-        input#cadence(type="text" v-model="newchallenge.cadence"
-          placeholder="5+0, 1h+30s, 7d+1d ...")
-      fieldset(v-if="st.user.id > 0")
-        label(for="selectPlayers") {{ st.tr["Play with?"] }}
-        input#selectPlayers(type="text" v-model="newchallenge.to")
-      fieldset(v-if="st.user.id > 0 && newchallenge.to.length > 0")
-        label(for="inputFen") FEN
-        input#inputFen(type="text" v-model="newchallenge.fen")
+      form(@submit.prevent="newChallenge()" @keyup.enter="newChallenge()")
+        fieldset
+          label(for="selectVariant") {{ st.tr["Variant"] }} *
+          select#selectVariant(v-model="newchallenge.vid")
+            option(v-for="v in st.variants" :value="v.id"
+                :selected="newchallenge.vid==v.id")
+              | {{ v.name }}
+        fieldset
+          label(for="cadence") {{ st.tr["Cadence"] }} *
+          div#predefinedCadences
+            button 3+2
+            button 5+3
+            button 15+5
+          input#cadence(type="text" v-model="newchallenge.cadence"
+            placeholder="5+0, 1h+30s, 7d+1d ...")
+        fieldset(v-if="st.user.id > 0")
+          label(for="selectPlayers") {{ st.tr["Play with?"] }}
+          input#selectPlayers(type="text" v-model="newchallenge.to")
+        fieldset(v-if="st.user.id > 0 && newchallenge.to.length > 0")
+          label(for="inputFen") FEN
+          input#inputFen(type="text" v-model="newchallenge.fen")
       button(@click="newChallenge()") {{ st.tr["Send challenge"] }}
   .row
     .col-sm-12
index 9e58cfd..c89b2d6 100644 (file)
@@ -17,7 +17,7 @@ import { GameStorage } from "@/utils/gameStorage";
 import { ajax } from "@/utils/ajax";
 import GameList from "@/components/GameList.vue";
 export default {
-  name: "my-games",
+  name: "my-my-games",
   components: {
     GameList,
   },
diff --git a/client/src/views/Problems.vue b/client/src/views/Problems.vue
new file mode 100644 (file)
index 0000000..4f832fa
--- /dev/null
@@ -0,0 +1,127 @@
+<template lang="pug">
+main
+  input#modalNewprob.modal(type="checkbox" @change="infoMsg=''")
+  div#newprobDiv(role="dialog" data-checkbox="modalNewprob")
+    .card(@keyup.enter="newProblem()")
+      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"] }}
+      #dialog.text-center {{ st.tr[infoMsg] }}
+  .row
+    .col-sm-12
+      button#newProblem(onClick="doClick('modalNewprob')") {{ st.tr["New problem"] }}
+  .row
+    .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")
+      label(for="selectVariant") {{ st.tr["Variant"] }}
+      select#selectVariant(v-model="newproblem.vid")
+        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 }}
+        p {{ p.fen }}
+        p {{ p.instruction }}
+        p {{ p.solution }}
+</template>
+
+<script>
+import { store } from "@/store";
+import { ajax } from "@/utils/ajax";
+import { checkProblem } from "@/data/problemCheck";
+import { getDiagram } from "@/utils/printDiagram";
+export default {
+  name: "my-problems",
+  data: function() {
+    return {
+      emptyVar: {
+        vid: 0,
+        vname: "",
+      },
+      newproblem: {
+        vid: 0,
+        fen: "",
+        instruction: "",
+        solution: "",
+      },
+      onlyMines: false,
+      st: store.state,
+      problems: [],
+      infoMsg: "",
+      curVar: 0,
+      curDiag: "",
+    };
+  },
+  created: function() {
+    ajax("/problems", "GET", (res) => {
+      this.problems = res.problems;
+    });
+  },
+  methods: {
+    showProblem: function(p) {
+      return (this.vid == 0 || p.vid == this.vid) &&
+        (!this.onlyMines || p.uid != this.st.user.id);
+    },
+    loadVariant: async function() {
+      if (this.newproblem.vid == 0)
+        return;
+      this.curVar = 0;
+      const variant = this.st.variants.find(v => v.id == this.newproblem.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
+    },
+    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>";
+    },
+    newProblem: function() {
+      const error = checkProblem(this.newproblem);
+      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);
+      });
+    },
+  },
+};
+</script>
+
+<style lang="sass" scoped>
+#newProblem
+  display: block
+  margin: 10px auto 5px auto
+</style>
index 5f9d6dd..d06ae26 100644 (file)
@@ -186,48 +186,4 @@ ul:not(.browser-default)
 
 ul:not(.browser-default) > li
   list-style-type: disc
-
-.light-square-diag
-  background-color: #e5e5ca
-
-.dark-square-diag
-  background-color: #6f8f57
-
-// TODO: following is duplicated (Board.vue)
-div.board
-  float: left
-  height: 0
-  display: inline-block
-  position: relative
-
-div.board8
-  width: 12.5%
-  padding-bottom: 12.5%
-
-div.board10
-  width: 10%
-  padding-bottom: 10%
-
-div.board11
-  width: 9.09%
-  padding-bottom: 9.1%
-
-img.piece
-  width: 100%
-
-img.piece, img.mark-square
-  max-width: 100%
-  height: auto
-  display: block
-
-img.mark-square
-  opacity: 0.6
-  width: 76%
-  position: absolute
-  top: 12%
-  left: 12%
-  opacity: .7
-
-.in-shadow
-  filter: brightness(50%)
 </style>
index b7b76c9..5bf3a3b 100644 (file)
@@ -17,8 +17,16 @@ create table Users (
   notify boolean
 );
 
--- All the following tables are for correspondance play only
--- (Live games are stored in browser)
+create table Problems (
+  id integer primary key,
+  added datetime,
+  uid integer,
+  vid integer,
+  instruction text,
+  solution text,
+  foreign key (uid) references Users(id),
+  foreign key (vid) references Variants(id)
+);
 
 create table Challenges (
   id integer primary key,
index 9d70082..27f5d1e 100644 (file)
@@ -289,7 +289,7 @@ const GameModel =
             if ((mstats.nbMoves == 0 && tsNow - g.created > 91*day) ||
               (mstats.nbMoves == 1 && tsNow - mstats.lastMaj > 91*day))
             {
-              return GameModel.remove(g.id);
+              GameModel.remove(g.id);
             }
           });
         });
diff --git a/server/models/Problem.js b/server/models/Problem.js
new file mode 100644 (file)
index 0000000..75c2e14
--- /dev/null
@@ -0,0 +1,106 @@
+var db = require("../utils/database");
+
+/*
+ * Structure:
+ *   id: integer
+ *   added: datetime
+ *   uid: user id (int)
+ *   vid: variant id (int)
+ *   fen: varchar (optional)
+ *   instruction: text
+ *   solution: text
+ */
+
+const ProblemModel =
+{
+  checkProblem: function(p)
+  {
+    if (!p.vid.toString().match(/^[0-9]+$/))
+      return "Wrong variant ID";
+    if (!p.fen.match(/^[a-zA-Z0-9, /-]*$/))
+      return "Bad FEN string";
+    return "";
+  },
+
+  create: function(p, cb)
+  {
+    db.serialize(function() {
+      const query =
+        "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) {
+        return cb(err, {pid: this.lastID});
+      });
+    });
+  },
+
+  getAll: function(cb)
+  {
+    db.serialize(function() {
+      const query =
+        "SELECT * " +
+        "FROM Problems";
+      db.all(query, (err,problems) => {
+        return cb(err, problems);
+      });
+    });
+  },
+
+  getOne: function(id, cb)
+  {
+    db.serialize(function() {
+      const query =
+        "SELECT * " +
+        "FROM Problems " +
+        "WHERE id = " + id;
+      db.get(query, (err,problem) => {
+        return cb(err, problem);
+      });
+    });
+  },
+
+  update: function(id, prob)
+  {
+    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);
+    });
+  },
+
+  remove: function(id)
+  {
+    db.serialize(function() {
+      const query =
+        "DELETE FROM Problems " +
+        "WHERE id = " + id;
+      db.run(query);
+    });
+  },
+
+  safeRemove: function(id, uid, cb)
+  {
+    db.serialize(function() {
+      const query =
+        "SELECT 1 " +
+        "FROM Problems " +
+        "WHERE id = " + id + " AND uid = " + uid;
+      db.get(query, (err,prob) => {
+        if (!prob)
+          return cb({errmsg: "Not your problem"});
+        ProvlemModel.remove(id);
+        cb(null);
+      });
+    });
+  },
+}
+
+module.exports = ProblemModel;
index 0daef0a..f84f617 100644 (file)
       },
       "dependencies": {
         "readable-stream": {
-          "version": "3.5.0",
-          "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.5.0.tgz",
-          "integrity": "sha512-gSz026xs2LfxBPudDuI41V1lka8cxg64E66SGe78zJlsUofOg/yqwezdIcdfwik6B4h8LFmWPA9ef9X3FiNFLA==",
+          "version": "3.6.0",
+          "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz",
+          "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==",
           "requires": {
             "inherits": "^2.0.3",
             "string_decoder": "^1.1.1",
index a3eb858..cca315f 100644 (file)
@@ -11,5 +11,6 @@ router.use("/", require("./games"));
 router.use("/", require("./messages"));
 router.use("/", require("./users"));
 router.use("/", require("./variants"));
+router.use("/", require("./problems"));
 
 module.exports = router;
diff --git a/server/routes/problems.js b/server/routes/problems.js
new file mode 100644 (file)
index 0000000..c45a1ba
--- /dev/null
@@ -0,0 +1,70 @@
+// AJAX methods to get, create, update or delete a problem
+
+let router = require("express").Router();
+const access = require("../utils/access");
+const ProblemModel = require("../models/Problem");
+const sanitizeHtml = require('sanitize-html');
+
+router.get("/problems", (req,res) => {
+  const probId = req.query["pid"];
+  if (!!probId)
+  {
+    if (!probId.match(/^[0-9]+$/))
+      return res.json({errmsg: "Wrong problem ID"});
+    ProblemModel.getOne(req.query["pid"], (err,problem) => {
+      access.checkRequest(res, err, problem, "Problem not found", () => {
+        res.json({problem: problem});
+      });
+    });
+  }
+  else
+  {
+    ProblemModel.getAll((err,problems) => {
+      res.json(err || {problems:problems});
+    });
+  }
+});
+
+router.post("/problems", access.logged, access.ajax, (req,res) => {
+  const error = ProblemModel.checkProblem(req.body.prob);
+  if (!!error)
+    return res.json({errmsg:error});
+  const problem =
+  {
+    vid: req.body.prob.vid,
+    fen: req.body.prob.fen,
+    uid: req.userId,
+    instruction: sanitizeHtml(req.body.prob.instruction),
+    solution: sanitizeHtml(req.body.prob.solution),
+  };
+  ProblemModel.create(problem, (err,ret) => {
+    return res.json(err || {pid:ret.pid});
+  });
+});
+
+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);
+  if (!!error)
+    return res.json({errmsg: error});
+  ProblemModel.update(pid, obj, (err) => {
+    res.json(err || {});
+  });
+});
+
+router.delete("/problems", access.logged, access.ajax, (req,res) => {
+  const pid = req.query.id;
+  if (!pid.match(/^[0-9]+$/))
+    res.json({errmsg: "Bad problem ID"});
+  ProblemModel.safeRemove(pid, req.userId, err => {
+    res.json(err || {});
+  });
+});
+
+module.exports = router;