From 89021f181ac0689bbc785ce0ebd9a910e66352b0 Mon Sep 17 00:00:00 2001 From: Benjamin Auder <benjamin.auder@somewhere> Date: Fri, 14 Feb 2020 01:14:52 +0100 Subject: [PATCH] Draft of problems section --- client/src/App.vue | 56 +++++++++++- client/src/base_rules.js | 2 +- client/src/components/BaseGame.vue | 5 + client/src/components/Board.vue | 37 -------- client/src/components/ContactForm.vue | 4 +- client/src/data/problemCheck.js | 9 ++ client/src/router.js | 5 + client/src/translations/en.js | 12 ++- client/src/translations/es.js | 12 ++- client/src/translations/fr.js | 12 ++- client/src/views/Game.vue | 7 +- client/src/views/Hall.vue | 43 ++++----- client/src/views/MyGames.vue | 2 +- client/src/views/Problems.vue | 127 ++++++++++++++++++++++++++ client/src/views/Rules.vue | 44 --------- server/db/create.sql | 12 ++- server/models/Game.js | 2 +- server/models/Problem.js | 106 +++++++++++++++++++++ server/package-lock.json | 6 +- server/routes/all.js | 1 + server/routes/problems.js | 70 ++++++++++++++ 21 files changed, 447 insertions(+), 127 deletions(-) create mode 100644 client/src/data/problemCheck.js create mode 100644 client/src/views/Problems.vue create mode 100644 server/models/Problem.js create mode 100644 server/routes/problems.js diff --git a/client/src/App.vue b/client/src/App.vue index 4cd1124f..d720b6eb 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 + // 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> diff --git a/client/src/base_rules.js b/client/src/base_rules.js index 20e123b3..924f7374 100644 --- a/client/src/base_rules.js +++ b/client/src/base_rules.js @@ -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); diff --git a/client/src/components/BaseGame.vue b/client/src/components/BaseGame.vue index a1539bab..b34eae9f 100644 --- a/client/src/components/BaseGame.vue +++ b/client/src/components/BaseGame.vue @@ -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() { diff --git a/client/src/components/Board.vue b/client/src/components/Board.vue index 8d429a34..1c259f9a 100644 --- a/client/src/components/Board.vue +++ b/client/src/components/Board.vue @@ -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 diff --git a/client/src/components/ContactForm.vue b/client/src/components/ContactForm.vue index 14273b08..61796bfa 100644 --- a/client/src/components/ContactForm.vue +++ b/client/src/components/ContactForm.vue @@ -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 index 00000000..3b9ccb46 --- /dev/null +++ b/client/src/data/problemCheck.js @@ -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"; +} diff --git a/client/src/router.js b/client/src/router.js index ede2186b..071af8c5 100644 --- a/client/src/router.js +++ b/client/src/router.js @@ -38,6 +38,11 @@ const router = new Router({ name: "logout", component: loadView("Logout"), }, + { + path: "/problems", + name: "myproblems", + component: loadView("Problems"), + }, { path: "/mygames", name: "mygames", diff --git a/client/src/translations/en.js b/client/src/translations/en.js index 4d6a9726..05a62bc9 100644 --- a/client/src/translations/en.js +++ b/client/src/translations/en.js @@ -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", diff --git a/client/src/translations/es.js b/client/src/translations/es.js index f4918f8a..38c96b04 100644 --- a/client/src/translations/es.js +++ b/client/src/translations/es.js @@ -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", diff --git a/client/src/translations/fr.js b/client/src/translations/fr.js index 6d10338b..dada230d 100644 --- a/client/src/translations/fr.js +++ b/client/src/translations/fr.js @@ -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", diff --git a/client/src/views/Game.vue b/client/src/views/Game.vue index 062990d2..88f2b5e2 100644 --- a/client/src/views/Game.vue +++ b/client/src/views/Game.vue @@ -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); diff --git a/client/src/views/Hall.vue b/client/src/views/Hall.vue index 9b584095..079584c0 100644 --- a/client/src/views/Hall.vue +++ b/client/src/views/Hall.vue @@ -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 diff --git a/client/src/views/MyGames.vue b/client/src/views/MyGames.vue index 9e58cfd5..c89b2d6f 100644 --- a/client/src/views/MyGames.vue +++ b/client/src/views/MyGames.vue @@ -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 index 00000000..4f832fad --- /dev/null +++ b/client/src/views/Problems.vue @@ -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> diff --git a/client/src/views/Rules.vue b/client/src/views/Rules.vue index 5f9d6dd7..d06ae269 100644 --- a/client/src/views/Rules.vue +++ b/client/src/views/Rules.vue @@ -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> diff --git a/server/db/create.sql b/server/db/create.sql index b7b76c9b..5bf3a3b0 100644 --- a/server/db/create.sql +++ b/server/db/create.sql @@ -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, diff --git a/server/models/Game.js b/server/models/Game.js index 9d700826..27f5d1e3 100644 --- a/server/models/Game.js +++ b/server/models/Game.js @@ -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 index 00000000..75c2e146 --- /dev/null +++ b/server/models/Problem.js @@ -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; diff --git a/server/package-lock.json b/server/package-lock.json index 0daef0ab..f84f6174 100644 --- a/server/package-lock.json +++ b/server/package-lock.json @@ -2750,9 +2750,9 @@ }, "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", diff --git a/server/routes/all.js b/server/routes/all.js index a3eb8587..cca315f1 100644 --- a/server/routes/all.js +++ b/server/routes/all.js @@ -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 index 00000000..c45a1bac --- /dev/null +++ b/server/routes/problems.js @@ -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; -- 2.44.0