6f03fd973bae4330c0eba1dceada95e00dee5669
[vchess.git] / public / javascripts / components / problems.js
1 Vue.component('my-problems', {
2 props: ["queryHash","settings"],
3 data: function () {
4 return {
5 userId: user.id,
6 problems: [], //oldest first
7 myProblems: [], //same, but only mine
8 singletons: [], //requested problems (using #num)
9 display: "others", //or "mine"
10 curProb: null, //(reference to) current displayed problem (if any)
11 showSolution: false,
12 nomoreMessage: "",
13 pbNum: 0, //to navigate directly to some problem
14 // New problem (to upload), or existing problem to edit:
15 modalProb: {
16 id: 0, //defined if it's an edit
17 fen: "",
18 instructions: "",
19 solution: "",
20 preview: false,
21 },
22 };
23 },
24 template: `
25 <div class="col-sm-12 col-md-10 col-md-offset-1 col-lg-8 col-lg-offset-2">
26 <div id="problemControls" class="button-group">
27 <button :aria-label='translate("Previous problem(s)")' class="tooltip" @click="showNext('backward')">
28 <i class="material-icons">skip_previous</i>
29 </button>
30 <button :aria-label='translate("Add a problem")' class="tooltip" onClick="doClick('modal-newproblem')">
31 {{ translate("New") }}
32 </button>
33 <button :aria-label='translate("Next problem(s)")' class="tooltip" @click="showNext('forward')">
34 <i class="material-icons">skip_next</i>
35 </button>
36 </div>
37 <div id="mainBoard" v-if="!!curProb">
38 <div id="instructions-div" class="section-content">
39 <p id="problem-instructions">
40 {{ curProb.instructions }}
41 </p>
42 </div>
43 <my-game :fen="curProb.fen" :mode="analyze" :allowMovelist="true" :settings="settings">
44 </my-game>
45 <div id="solution-div" class="section-content">
46 <h3 class="clickable" @click="showSolution = !showSolution">
47 {{ translations["Show solution"] }}
48 </h3>
49 <p id="problem-solution" v-show="showSolution">
50 {{ curProb.solution }}
51 </p>
52 </div>
53 <button @click="displayList()">
54 <span>Back to list display</span>
55 </button>
56 </div>
57 <div>
58 <input type="text" placeholder="Type problem number" v-model="pbNum"/>
59 <button @click="showProblem()">
60 <span>Show problem</span>
61 </button>
62 </div>
63 <button v-if="!!userId" @click="toggleListDisplay()">
64 <span>My problems (only)</span>
65 </button>
66 <my-problem-summary v-show="!curProb"
67 v-on:edit-problem="editProblem(p)" v-on:delete-problem="deleteProblem(p.id)"
68 v-for="p in curProblems" @click="curProb=p"
69 v-bind:prob="p" v-bind:userid="userId" v-bind:key="p.id">
70 </my-problem-summary>
71 <input type="checkbox" id="modal-newproblem" class="modal"/>
72 <div role="dialog" aria-labelledby="modalProblemTxt">
73 <div v-show="!modalProb.preview" class="card newproblem-form">
74 <label for="modal-newproblem" class="modal-close">
75 </label>
76 <h3 id="modalProblemTxt">
77 {{ translate("Add a problem") }}
78 </h3>
79 <form @submit.prevent="previewProblem()">
80 <fieldset>
81 <label for="newpbFen">FEN</label>
82 <input id="newpbFen" type="text" v-model="modalProb.fen"
83 :placeholder='translate("Full FEN description")'/>
84 </fieldset>
85 <fieldset>
86 <p class="emphasis">
87 {{ translate("Safe HTML tags allowed") }}
88 </p>
89 <label for="newpbInstructions">
90 {{ translate("Instructions") }}
91 </label>
92 <textarea id="newpbInstructions" v-model="modalProb.instructions"
93 :placeholder='translate("Describe the problem goal")'>
94 </textarea>
95 <label for="newpbSolution">
96 {{ translate("Solution") }}
97 </label>
98 <textarea id="newpbSolution" v-model="modalProb.solution"
99 :placeholder='translate("How to solve the problem?")'>
100 </textarea>
101 <button class="center-btn">
102 {{ translate("Preview") }}
103 </button>
104 </fieldset>
105 </form>
106 </div>
107 <div v-show="modalProb.preview" class="card newproblem-preview">
108 <label for="modal-newproblem" class="modal-close">
109 </label>
110 <my-problem-summary v-bind:prob="modalProb" v-bind:userid="userId">
111 </my-problem-summary>
112 <div class="button-group">
113 <button @click="modalProb.preview=false">
114 {{ translate("Cancel") }}
115 </button>
116 <button @click="sendProblem()">
117 {{ translate("Send") }}
118 </button>
119 </div>
120 </div>
121 </div>
122 <input id="modalNomore" type="checkbox" class="modal"/>
123 <div role="dialog" aria-labelledby="nomoreMessage">
124 <div class="card smallpad small-modal text-center">
125 <label for="modalNomore" class="modal-close"></label>
126 <h3 id="nomoreMessage" class="section">
127 {{ nomoreMessage }}
128 </h3>
129 </div>
130 </div>
131 </div>
132 `,
133 watch: {
134 queryHash: function(newQhash) {
135 if (!!newQhash)
136 {
137 // New query hash = "id=42"; get 42 as problem ID
138 const pid = parseInt(newQhash.substr(2));
139 this.showProblem(pid);
140 }
141 else
142 this.curProb = null; //(back to) list display
143 },
144 },
145 created: function() {
146 if (!!this.queryHash)
147 {
148 const pid = parseInt(this.queryHash.substr(2));
149 this.showProblem(pid);
150 }
151 else
152 this.firstFetch();
153 },
154 methods: {
155 firstFetch: function() {
156 // Fetch most recent problems from server, for both lists
157 this.fetchProblems("others", "bacwkard");
158 this.fetchProblems("mine", "bacwkard");
159 this.listsInitialized = true;
160 },
161 showProblem: function(num) {
162 const pid = num || this.pbNum;
163 location.hash = "#" + pid;
164 const pIdx = this.singletons.findIndex(p => p.id == pid);
165 if (pIdx >= 0)
166 curProb = this.singletons[pIdx];
167 else
168 {
169 // Cannot find problem in current set; get from server, and add to singletons.
170 ajax(
171 "/problems/" + variant.name + "/" + pid, //TODO: use variant._id ?
172 "GET",
173 response => {
174 if (!!response.problem)
175 {
176 this.singletons.push(response.problem);
177 this.curProb = response.problem;
178 }
179 else
180 this.noMoreProblems("Sorry, problem " + pid + " does not exist");
181 }
182 );
183 }
184 },
185 translate: function(text) {
186 return translations[text];
187 },
188 curProblems: function() {
189 switch (this.display)
190 {
191 case "others":
192 return this.problems;
193 case "mine":
194 return this.myProblems;
195 }
196 },
197 // TODO?: get 50 from server but only show 10 at a time (for example)
198 showNext: function(direction) {
199 if (!this.curProb)
200 return this.fetchProblems(this.display, direction);
201 // Show next problem (older or newer):
202 let curProbs = this.curProblems();
203 // Try to find a neighbour problem in the direction, among current set
204 const neighbor = this.findClosestNeighbor(this.curProb, curProbs, direction);
205 if (!!neighbor)
206 {
207 this.curProb = neighbor;
208 return;
209 }
210 // Boundary case: nothing in current set, need to fetch from server
211 const curSize = curProbs.length;
212 this.fetchProblems(this.display, direction);
213 const newSize = curProbs.length;
214 if (curSize == newSize) //no problems found
215 return this.noMoreProblems("No more problems in this direction");
216 // Ok, found something:
217 this.curProb = this.findClosestNeighbor(this.curProb, curProbs, direction);
218 },
219 findClosestNeighbor: function(problem, probList, direction) {
220 let neighbor = undefined;
221 let smallestDistance = Number.MAX_SAFE_INTEGER;
222 for (let prob of probList)
223 {
224 const delta = Math.abs(prob.id - problem.id);
225 if (delta < smallestDistance &&
226 ((direction == "backward" && prob.id < problem.id)
227 || (direction == "forward" && prob.id > problem.id)))
228 {
229 neighbor = prob;
230 smallestDistance = delta;
231 }
232 }
233 return neighbor;
234 },
235 noMoreProblems: function(message) {
236 this.nomoreMessage = message;
237 let modalNomore = document.getElementById("modalNomore");
238 modalNomore.checked = true;
239 setTimeout(() => modalNomore.checked = false, 2000);
240 },
241 displayList: function() {
242 this.curProb = null;
243 location.hash = "";
244 // Fetch problems if first call (if #num, and then lists)
245 if (!this.listsInitialized)
246 this.firstFetch();
247 },
248 toggleListDisplay: function() {
249 this.display = (this.display == "others" ? "mine" : "others");
250 },
251 fetchProblems: function(type, direction) {
252 let problems = (type == "others" ? this.problems : this.myProblems);
253 let last_dt = (direction=="forward" ? 0 : Number.MAX_SAFE_INTEGER);
254 if (this.problems.length > 0)
255 {
256 // Search for newest date (or oldest)
257 last_dt = problems[0].added;
258 for (let i=1; i<problems.length; i++)
259 {
260 if ((direction == "forward" && this.problems[i].added > last_dt) ||
261 (direction == "backward" && this.problems[i].added < last_dt))
262 {
263 last_dt = this.problems[i].added;
264 }
265 }
266 }
267 ajax(
268 "/problems/" + variant.id,
269 "GET",
270 {
271 type: type,
272 direction: direction,
273 last_dt: last_dt,
274 },
275 response => {
276 if (response.problems.length > 0)
277 {
278 Array.prototype.push.apply(problems,
279 response.problems.sort((p1,p2) => { return p1.added - p2.added; }));
280 // If one list is empty but not the other, show the non-empty
281 const otherArray = (type == "mine" ? this.problems : this.myProblems);
282 if (problems.length > 0 && otherArray.length == 0)
283 this.display = type;
284 }
285 }
286 );
287 },
288 previewProblem: function() {
289 if (!V.IsGoodFen(this.newProblem.fen))
290 return alert(translations["Bad FEN description"]);
291 if (this.newProblem.instructions.trim().length == 0)
292 return alert(translations["Empty instructions"]);
293 if (this.newProblem.solution.trim().length == 0)
294 return alert(translations["Empty solution"]);
295 this.modalProb.preview = true;
296 },
297 editProblem: function(prob) {
298 this.modalProb = prob;
299 document.getElementById("modal-newproblem").checked = true;
300 },
301 deleteProblem: function(pid) {
302 ajax(
303 "/problems/" + variant.id + "/" + pid,
304 "DELETE",
305 response => {
306 // Delete problem from the list on client side
307 let problems = this.curProblems();
308 const pIdx = problems.findIndex(p => p.id == pid);
309 problems.splice(pIdx, 1);
310 }
311 );
312 },
313 sendProblem: function() {
314 // Send it to the server and close modal
315 ajax(
316 "/problems/" + variant.id,
317 (this.modalProb.id > 0 ? "PUT" : "POST"),
318 this.modalProb,
319 response => {
320 document.getElementById("modal-newproblem").checked = false;
321 if (this.modalProb.id == 0)
322 {
323 this.modalProb.added = Date.now();
324 this.modalProb.preview = false;
325 this.myProblems.push(JSON.parse(JSON.stringify(this.modalProb)));
326 }
327 else
328 this.modalProb.id = 0;
329 }
330 );
331 },
332 },
333 })