From 604b951e4dc4647da9b251c5fff4ecb4c7b1b298 Mon Sep 17 00:00:00 2001 From: Benjamin Auder <benjamin.auder@somewhere> Date: Fri, 14 Feb 2020 17:18:52 +0100 Subject: [PATCH] Draft of a problems section + news system --- client/src/App.vue | 24 ++- client/src/components/ComputerGame.vue | 3 +- client/src/router.js | 5 + client/src/translations/en.js | 8 +- client/src/translations/es.js | 8 +- client/src/translations/fr.js | 8 +- client/src/views/Analyse.vue | 4 +- client/src/views/Game.vue | 3 +- client/src/views/News.vue | 166 +++++++++++++++ client/src/views/Problems.vue | 282 +++++++++++++++++++------ server/db/create.sql | 9 + server/models/News.js | 63 ++++++ server/models/Problem.js | 20 +- server/routes/all.js | 1 + server/routes/news.js | 50 +++++ server/routes/problems.js | 14 +- 16 files changed, 564 insertions(+), 104 deletions(-) create mode 100644 client/src/views/News.vue create mode 100644 server/models/News.js create mode 100644 server/routes/news.js diff --git a/client/src/App.vue b/client/src/App.vue index d720b6eb..6183d66f 100644 --- a/client/src/App.vue +++ b/client/src/App.vue @@ -32,12 +32,13 @@ .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 diff --git a/client/src/components/ComputerGame.vue b/client/src/components/ComputerGame.vue index b20b8fe1..3bbdc046 100644 --- a/client/src/components/ComputerGame.vue +++ b/client/src/components/ComputerGame.vue @@ -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> diff --git a/client/src/router.js b/client/src/router.js index 071af8c5..daaefce8 100644 --- a/client/src/router.js +++ b/client/src/router.js @@ -63,6 +63,11 @@ const router = new Router({ name: "about", component: loadView("About"), }, + { + path: "/news", + name: "news", + component: loadView("News"), + }, ] }); diff --git a/client/src/translations/en.js b/client/src/translations/en.js index 05a62bc9..b912ef39 100644 --- a/client/src/translations/en.js +++ b/client/src/translations/en.js @@ -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: diff --git a/client/src/translations/es.js b/client/src/translations/es.js index 38c96b04..cbc5c640 100644 --- a/client/src/translations/es.js +++ b/client/src/translations/es.js @@ -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: diff --git a/client/src/translations/fr.js b/client/src/translations/fr.js index dada230d..1510f44c 100644 --- a/client/src/translations/fr.js +++ b/client/src/translations/fr.js @@ -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: diff --git a/client/src/views/Analyse.vue b/client/src/views/Analyse.vue index c667290f..035cd089 100644 --- a/client/src/views/Analyse.vue +++ b/client/src/views/Analyse.vue @@ -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: "", diff --git a/client/src/views/Game.vue b/client/src/views/Game.vue index 88f2b5e2..1b82d81f 100644 --- a/client/src/views/Game.vue +++ b/client/src/views/Game.vue @@ -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 index 00000000..e23a6e24 --- /dev/null +++ b/client/src/views/News.vue @@ -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> diff --git a/client/src/views/Problems.vue b/client/src/views/Problems.vue index 4f832fad..9eca44b6 100644 --- a/client/src/views/Problems.vue +++ b/client/src/views/Problems.vue @@ -2,119 +2,267 @@ 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}); }, }, }; diff --git a/server/db/create.sql b/server/db/create.sql index 5bf3a3b0..c05c87ac 100644 --- a/server/db/create.sql +++ b/server/db/create.sql @@ -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 index 00000000..ebe65d04 --- /dev/null +++ b/server/models/News.js @@ -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; diff --git a/server/models/Problem.js b/server/models/Problem.js index 75c2e146..0e900256 100644 --- a/server/models/Problem.js +++ b/server/models/Problem.js @@ -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); }); }); diff --git a/server/routes/all.js b/server/routes/all.js index cca315f1..b58638e4 100644 --- a/server/routes/all.js +++ b/server/routes/all.js @@ -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 index 00000000..ed784564 --- /dev/null +++ b/server/routes/news.js @@ -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; diff --git a/server/routes/problems.js b/server/routes/problems.js index c45a1bac..a0886db5 100644 --- a/server/routes/problems.js +++ b/server/routes/problems.js @@ -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 || {}); }); }); -- 2.48.1