.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>
-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
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
text-decoration: none
& > .menuitem
display: inline-block
- margin: 0 10px
+ margin: 0 12px
&:link
color: #2c3e50
&:visited, &:hover
text-decoration: none
& > p
display: inline-block
- margin: 0 10px
+ margin: 0 12px
@media screen and (max-width: 767px)
footer
<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>
name: "about",
component: loadView("About"),
},
+ {
+ path: "/news",
+ name: "news",
+ component: loadView("News"),
+ },
]
});
"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",
"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",
"Language": "Language",
"Live challenges": "Live challenges",
"Live games": "Live games",
+ "Load more": "Load more",
"Login": "Login",
"Logout": "Logout",
"Logout successful!": "Logout successful!",
"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",
"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",
"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:
"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",
"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",
"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!",
"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",
"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",
"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:
"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",
"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",
"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 !",
"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",
"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",
"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:
.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>
},
game: {
players:[{name:"Analyse"},{name:"Analyse"}],
- mode: "analyze"
+ mode: "analyze",
},
vr: null, //"variant rules" object initialized from FEN
curFen: "",
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>
--- /dev/null
+<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>
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});
},
},
};
create table Problems (
id integer primary key,
added datetime,
+ fen varchar,
uid integer,
vid integer,
instruction text,
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,
--- /dev/null
+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;
{
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, /-]*$/))
"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});
});
});
});
},
- 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);
});
},
db.get(query, (err,prob) => {
if (!prob)
return cb({errmsg: "Not your problem"});
- ProvlemModel.remove(id);
+ ProblemModel.remove(id);
cb(null);
});
});
router.use("/", require("./users"));
router.use("/", require("./variants"));
router.use("/", require("./problems"));
+router.use("/", require("./news"));
module.exports = router;
--- /dev/null
+// 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;
});
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 || {});
});
});