Started code review + some fixes (unfinished)
[vchess.git] / client / src / views / Problems.vue
1 <template lang="pug">
2 main
3 input#modalNewprob.modal(type="checkbox" @change="infoMsg=''")
4 div#newprobDiv(role="dialog" data-checkbox="modalNewprob")
5 .card(@keyup.enter="sendProblem()")
6 label#closeNewprob.modal-close(for="modalNewprob")
7 fieldset
8 label(for="selectVariant") {{ st.tr["Variant"] }}
9 select#selectVariant(
10 v-model="curproblem.vid"
11 @change="changeVariant(curproblem)"
12 )
13 option(
14 v-for="v in [emptyVar].concat(st.variants)"
15 :value="v.id"
16 :selected="curproblem.vid==v.id"
17 )
18 | {{ v.name }}
19 fieldset
20 input#inputFen(
21 type="text"
22 placeholder="FEN"
23 v-model="curproblem.fen"
24 @input="trySetDiagram(curproblem)"
25 )
26 #diagram(v-html="curproblem.diag")
27 fieldset
28 textarea(
29 :placeholder="st.tr['Instructions']"
30 v-model="curproblem.instruction"
31 )
32 p(v-html="parseHtml(curproblem.instruction)")
33 fieldset
34 textarea(
35 :placeholder="st.tr['Solution']"
36 v-model="curproblem.solution"
37 )
38 p(v-html="parseHtml(curproblem.solution)")
39 button(@click="sendProblem()") {{ st.tr["Send"] }}
40 #dialog.text-center {{ st.tr[infoMsg] }}
41 .row(v-if="showOne")
42 .col-sm-12.col-md-10.col-md-offset-2
43 #topPage
44 span.vname {{ curproblem.vname }}
45 span.uname ({{ curproblem.uname }})
46 button.marginleft(@click="backToList()") {{ st.tr["Back to list"] }}
47 button.nomargin(
48 v-if="st.user.id == curproblem.uid"
49 @click="editProblem(curproblem)"
50 )
51 | {{ st.tr["Edit"] }}
52 button.nomargin(
53 v-if="st.user.id == curproblem.uid"
54 @click="deleteProblem(curproblem)"
55 )
56 | {{ st.tr["Delete"] }}
57 p.clickable(
58 v-html="parseHtml(curproblem.instruction)"
59 @click="curproblem.showSolution=!curproblem.showSolution"
60 )
61 | {{ st.tr["Show solution"] }}
62 p(
63 v-show="curproblem.showSolution"
64 v-html="parseHtml(curproblem.solution)"
65 )
66 .row(v-else)
67 .col-sm-12.col-md-10.col-md-offset-1.col-lg-8.col-lg-offset-2
68 #controls
69 button#newProblem(onClick="window.doClick('modalNewprob')")
70 | {{ st.tr["New problem"] }}
71 label(for="checkboxMine") {{ st.tr["My problems"] }}
72 input#checkboxMine(
73 type="checkbox"
74 v-model="onlyMines"
75 )
76 label(for="selectVariant") {{ st.tr["Variant"] }}
77 select#selectVariant(v-model="selectedVar")
78 option(
79 v-for="v in [emptyVar].concat(st.variants)"
80 :value="v.id"
81 )
82 | {{ v.name }}
83 table
84 tr
85 th {{ st.tr["Variant"] }}
86 th {{ st.tr["Instructions"] }}
87 th {{ st.tr["Number"] }}
88 tr(
89 v-for="p in problems"
90 v-show="displayProblem(p)"
91 @click="setHrefPid(p)"
92 )
93 td {{ p.vname }}
94 td {{ firstChars(p.instruction) }}
95 td {{ p.id }}
96 BaseGame(v-if="showOne" :game="game" :vr="vr")
97 </template>
98
99 <script>
100 import { store } from "@/store";
101 import { ajax } from "@/utils/ajax";
102 import { checkProblem } from "@/data/problemCheck";
103 import { getDiagram } from "@/utils/printDiagram";
104 import { processModalClick } from "@/utils/modalClick";
105 import { ArrayFun } from "@/utils/array";
106 import BaseGame from "@/components/BaseGame.vue";
107 export default {
108 name: "my-problems",
109 components: {
110 BaseGame
111 },
112 data: function() {
113 return {
114 st: store.state,
115 emptyVar: {
116 vid: 0,
117 vname: ""
118 },
119 // Problem currently showed, or edited:
120 curproblem: {
121 id: 0, //used in case of edit
122 vid: 0,
123 fen: "",
124 diag: "",
125 instruction: "",
126 solution: "",
127 showSolution: false
128 },
129 loadedVar: 0, //corresponding to loaded V
130 selectedVar: 0, //to filter problems based on variant
131 problems: [],
132 onlyMines: false,
133 showOne: false,
134 infoMsg: "",
135 vr: null, //"variant rules" object initialized from FEN
136 game: {
137 players: [{ name: "Problem" }, { name: "Problem" }],
138 mode: "analyze"
139 }
140 };
141 },
142 created: function() {
143 ajax("/problems", "GET", res => {
144 // Show newest problem first:
145 this.problems = res.problems.sort((p1, p2) => p2.added - p1.added);
146 if (this.st.variants.length > 0)
147 this.problems.forEach(p => this.setVname(p));
148 // Retrieve all problems' authors' names
149 let names = {};
150 this.problems.forEach(p => {
151 if (p.uid != this.st.user.id) names[p.uid] = "";
152 //unknwon for now
153 else p.uname = this.st.user.name;
154 });
155 const showOneIfPid = () => {
156 const pid = this.$route.query["id"];
157 if (pid) this.showProblem(this.problems.find(p => p.id == pid));
158 };
159 if (Object.keys(names).length > 0) {
160 ajax("/users", "GET", { ids: Object.keys(names).join(",") }, res2 => {
161 res2.users.forEach(u => {
162 names[u.id] = u.name;
163 });
164 this.problems.forEach(p => (p.uname = names[p.uid]));
165 showOneIfPid();
166 });
167 } else showOneIfPid();
168 });
169 },
170 mounted: function() {
171 document
172 .getElementById("newprobDiv")
173 .addEventListener("click", processModalClick);
174 },
175 watch: {
176 // st.variants changes only once, at loading from [] to [...]
177 "st.variants": function() {
178 // Set problems vname (either all are set or none)
179 if (this.problems.length > 0 && this.problems[0].vname == "")
180 this.problems.forEach(p => this.setVname(p));
181 },
182 $route: function(to) {
183 const pid = to.query["id"];
184 if (pid) this.showProblem(this.problems.find(p => p.id == pid));
185 else this.showOne = false;
186 }
187 },
188 methods: {
189 setVname: function(prob) {
190 prob.vname = this.st.variants.find(v => v.id == prob.vid).name;
191 },
192 firstChars: function(text) {
193 let preparedText = text
194 // Replace line jumps and <br> by spaces
195 .replace(/\n/g, " ")
196 .replace(/<br\/?>/g, " ")
197 .replace(/<[^>]+>/g, "") //remove remaining HTML tags
198 .replace(/[ ]+/g, " ") //remove series of spaces by only one
199 .trim();
200 const maxLength = 32; //arbitrary...
201 if (preparedText.length > maxLength)
202 return preparedText.substr(0, 32) + "...";
203 return preparedText;
204 },
205 copyProblem: function(p1, p2) {
206 for (let key in p1) p2[key] = p1[key];
207 },
208 setHrefPid: function(p) {
209 // Change href => $route changes, watcher notices, call showProblem
210 const curHref = document.location.href;
211 document.location.href = curHref.split("?")[0] + "?id=" + p.id;
212 },
213 backToList: function() {
214 // Change href => $route change, watcher notices, reset showOne to false
215 document.location.href = document.location.href.split("?")[0];
216 },
217 resetCurProb: function() {
218 this.curproblem.id = 0;
219 this.curproblem.uid = 0;
220 this.curproblem.vid = "";
221 this.curproblem.vname = "";
222 this.curproblem.fen = "";
223 this.curproblem.diag = "";
224 this.curproblem.instruction = "";
225 this.curproblem.solution = "";
226 this.curproblem.showSolution = false;
227 },
228 parseHtml: function(txt) {
229 return !txt.match(/<[/a-zA-Z]+>/)
230 ? txt.replace(/\n/g, "<br/>") //no HTML tag
231 : txt;
232 },
233 changeVariant: function(prob) {
234 this.setVname(prob);
235 this.loadVariant(prob.vid, () => {
236 // Set FEN if possible (might not be correct yet)
237 if (V.IsGoodFen(prob.fen)) this.setDiagram(prob);
238 });
239 },
240 loadVariant: async function(vid, cb) {
241 // Condition: vid is a valid variant ID
242 this.loadedVar = 0;
243 const variant = this.st.variants.find(v => v.id == vid);
244 const vModule = await import("@/variants/" + variant.name + ".js");
245 window.V = vModule.VariantRules;
246 this.loadedVar = vid;
247 cb();
248 },
249 trySetDiagram: function(prob) {
250 // Problem edit: FEN could be wrong or incomplete,
251 // variant could not be ready, or not defined
252 if (prob.vid > 0 && this.loadedVar == prob.vid && V.IsGoodFen(prob.fen))
253 this.setDiagram(prob);
254 },
255 setDiagram: function(prob) {
256 // Condition: prob.fen is correct and global V is ready
257 const parsedFen = V.ParseFen(prob.fen);
258 const args = {
259 position: parsedFen.position,
260 orientation: parsedFen.turn
261 };
262 prob.diag = getDiagram(args);
263 },
264 displayProblem: function(p) {
265 return (
266 (this.selectedVar == 0 || p.vid == this.selectedVar) &&
267 ((this.onlyMines && p.uid == this.st.user.id) ||
268 (!this.onlyMines && p.uid != this.st.user.id))
269 );
270 },
271 showProblem: function(p) {
272 this.loadVariant(p.vid, () => {
273 // The FEN is already checked at this stage:
274 this.vr = new V(p.fen);
275 this.game.vname = p.vname;
276 this.game.mycolor = this.vr.turn; //diagram orientation
277 this.game.fen = p.fen;
278 this.$set(this.game, "fenStart", p.fen);
279 this.copyProblem(p, this.curproblem);
280 this.showOne = true;
281 });
282 },
283 sendProblem: function() {
284 const error = checkProblem(this.curproblem);
285 if (error) {
286 alert(error);
287 return;
288 }
289 const edit = this.curproblem.id > 0;
290 this.infoMsg = "Processing... Please wait";
291 ajax(
292 "/problems",
293 edit ? "PUT" : "POST",
294 { prob: this.curproblem },
295 ret => {
296 if (edit) {
297 let editedP = this.problems.find(p => p.id == this.curproblem.id);
298 this.copyProblem(this.curproblem, editedP);
299 } //new problem
300 else {
301 let newProblem = Object.assign({}, this.curproblem);
302 newProblem.id = ret.id;
303 newProblem.uid = this.st.user.id;
304 newProblem.uname = this.st.user.name;
305 this.problems = this.problems.concat(newProblem);
306 }
307 this.resetCurProb();
308 this.infoMsg = "";
309 }
310 );
311 },
312 editProblem: function(prob) {
313 if (!prob.diag) this.setDiagram(prob); //possible because V is loaded at this stage
314 this.copyProblem(prob, this.curproblem);
315 window.doClick("modalNewprob");
316 },
317 deleteProblem: function(prob) {
318 if (confirm(this.st.tr["Are you sure?"])) {
319 ajax("/problems", "DELETE", { id: prob.id }, () => {
320 ArrayFun.remove(this.problems, p => p.id == prob.id);
321 this.backToList();
322 });
323 }
324 }
325 }
326 };
327 </script>
328
329 <style lang="sass" scoped>
330 [type="checkbox"].modal+div .card
331 max-width: 767px
332 max-height: 100%
333 #inputFen
334 width: 100%
335 textarea
336 width: 100%
337 #diagram
338 margin: 0 auto
339 max-width: 400px
340 #controls
341 margin: 0
342 width: 100%
343 text-align: center
344 & > *
345 margin: 0
346
347 #topPage
348 span.vname
349 font-weight: bold
350 padding-left: var(--universal-margin)
351 span.uname
352 padding-left: var(--universal-margin)
353 margin: 0 auto
354 & > .nomargin
355 margin: 0
356 & > .marginleft
357 margin: 0 0 0 15px
358
359 @media screen and (max-width: 767px)
360 #topPage
361 text-align: center
362 </style>