Move navigation from _menu.json files in each navigation-parent directory to a naviga...
[ndcode_site.git] / my_account / index.html.jst
1 return async env => {
2   let breadcrumbs = await _require('/_lib/breadcrumbs.jst')
3   let fa_arrow_circle_left = await env.site.get_min_svg('/_svg/fa_arrow-circle-left.svg')
4   let fa_cloud_upload_alt = await env.site.get_min_svg('/_svg/fa_cloud-upload-alt.svg')
5   let fa_envelope = await env.site.get_min_svg('/_svg/fa_envelope.svg')
6   let fa_redo = await env.site.get_min_svg('/_svg/fa_redo.svg')
7   let fa_trash = await env.site.get_min_svg('/_svg/fa_trash.svg')
8   let get_placeholder = await _require('/_lib/get_placeholder.jst')
9   let get_account = await _require('/_lib/get_account.jst')
10   let get_navigation = await _require('/_lib/get_navigation.jst')
11   let get_session = await _require('/_lib/get_session.jst')
12   let icon_cross = await env.site.get_min_svg('/_svg/icon_cross.svg')
13   let icon_tick = await env.site.get_min_svg('/_svg/icon_tick.svg')
14   let navbar = await _require('/_lib/navbar.jst')
15
16   // preload draft details if any
17   let transaction = await env.site.database.Transaction()
18   let placeholder
19   let signed_in_as, details, change_details_draft
20   try {
21     let root = await transaction.get({})
22     let session = await get_session(env, root)
23
24     placeholder = await get_placeholder(env, session)
25
26     signed_in_as = await session.get_json('signed_in_as')
27
28     account = await get_account(root, session)
29     details =
30       account === undefined ?
31       null :
32       {
33         given_names: await account.get_json('given_names'),
34         family_name: await account.get_json('family_name'),
35         contact_me: await account.get_json('contact_me')
36       }
37
38     change_details_draft = await session.get_json('change_details_draft')
39     if (change_details_draft === undefined || env.now >= change_details_draft.expires)
40       change_details_draft = null
41
42     transaction.commit()
43   }
44   catch (error) {
45     transaction.rollback()
46     throw error
47   }
48
49   // cheat a little by ignoring the draft if it matches the original details,
50   // because user might have saved the details and left page before the timeout
51   if (
52     change_details_draft &&
53       change_details_draft.given_names === details.given_names &&
54       change_details_draft.family_name === details.family_name &&
55       change_details_draft.contact_me === details.contact_me
56   )
57     change_details_draft = null
58
59   await navbar(
60     env,
61     // head
62     async _out => {},
63     // body
64     async _out => {
65       await breadcrumbs(env, _out)
66
67       if (signed_in_as !== undefined) {
68         // signed in
69         p {'Your given names are visible to other users if you comment on our blog. Your email and family name remain private. If your name is one word or does not fit given names/family name pattern, then please enter given names only.'}
70
71         div.accordion#accordion(role="tablist" aria-multiselectable="true") {
72           div.card#card-1 {
73             div.card-header#card-1-heading(role="tab") {
74               a.h5(data-toggle="collapse" data-parent="#accordion" href="#card-1-collapse" aria-expanded="true" aria-controls="card-1-collapse") {
75                 'Change details'
76               }
77             }
78             div#card-1-collapse.collapse.show(role="tabpanel" aria-labelledby="card-1-heading" data-parent="#accordion") {
79               div.card-body {
80                 form#card-1-form {
81                   div.row {
82                     div.col-md-6 {
83                       div.form-group {
84                         label.form-label(for="given-names") {'Given names *'}
85                         input.form-control#given-names(type="text" value=change_details_draft ? change_details_draft.given_names : details.given_names placeholder=placeholder.given_names required maxlength=256) {}
86                         div.invalid-feedback {'Please enter a name we can address you by.'}
87                       }
88                     }
89                     div.col-md-6 {
90                       div.form-group {
91                         label.form-label(for="family-name") {'Family name'}
92                         input.form-control#family-name(type="text" value=change_details_draft ? change_details_draft.family_name : details.family_name placeholder=placeholder.family_name maxlength=256) {}
93                       }
94                     }
95                   }
96                   div.row.mb-3 {
97                     div.col-md-12 {
98                       div.custom-control.custom-checkbox {
99                         if (change_details_draft ? change_details_draft.contact_me : details.contact_me)
100                           input.custom-control-input#contact-me(type="checkbox" checked) {}
101                         else
102                           input.custom-control-input#contact-me(type="checkbox") {}
103                         ' '
104                         label.custom-control-label(for="contact-me") {
105                           'Contact me by email with updates and special offers'
106                         }
107                       }
108                     }
109                   }
110                 }
111
112                 if (change_details_draft)
113                   button.btn.btn-outline-secondary#card-1-revert(type="button") {
114                     div.icon24-outer.mr-2 {
115                       div.icon24-inner {_out.push(fa_trash)}
116                     }
117                     'Revert'
118                   }
119                 else
120                   button.btn.btn-outline-secondary#card-1-revert(type="button" disabled) {
121                     div.icon24-outer.mr-2 {
122                       div.icon24-inner {_out.push(fa_trash)}
123                     }
124                     'Revert'
125                   }
126                 if (change_details_draft)
127                   button.btn.btn-success.ml-3#card-1-save(type="button") {
128                     div.icon24-outer.mr-2#card-1-icon {
129                       div.icon24-inner {_out.push(fa_cloud_upload_alt)}
130                     }
131                     div.icon24-outer.mr-2#card-1-tick(hidden) {
132                       div.icon24-inner {_out.push(icon_tick)}
133                     }
134                     div.icon24-outer.mr-2#card-1-cross(hidden) {
135                       div.icon24-inner {_out.push(icon_cross)}
136                     }
137                     div.icon24-outer.mr-2#card-1-spinner(hidden) {
138                       div.icon24-inner {
139                         div.spinner-border.spinner-border-sm(role="status") {}
140                       }
141                     }
142                     'Save'
143                   }
144                 else
145                   button.btn.btn-success.ml-3#card-1-save(type="button" disabled) {
146                     div.icon24-outer.mr-2#card-1-icon {
147                       div.icon24-inner {_out.push(fa_cloud_upload_alt)}
148                     }
149                     div.icon24-outer.mr-2#card-1-tick(hidden) {
150                       div.icon24-inner {_out.push(icon_tick)}
151                     }
152                     div.icon24-outer.mr-2#card-1-cross(hidden) {
153                       div.icon24-inner {_out.push(icon_cross)}
154                     }
155                     div.icon24-outer.mr-2#card-1-spinner(hidden) {
156                       div.icon24-inner {
157                         div.spinner-border.spinner-border-sm(role="status") {}
158                       }
159                     }
160                     'Save'
161                   }
162
163                 p.'mt-3'.mb-0#card-1-message(hidden) {}
164               }
165             }
166           }
167           div.card#card-2 {
168             div.card-header#card-2-heading(role="tab") {
169               a.h5.collapsed(data-toggle="collapse" data-parent="#accordion" href="#card-2-collapse" aria-expanded="false" aria-controls="card-2-collapse") {
170                 'Change password'
171               }
172             }
173             div#card-2-collapse.collapse(role="tabpanel" aria-labelledby="step-2-heading" data-parent="#accordion") {
174               div.card-body {
175                 form#card-2-form {
176                   div.row {
177                     div.col-md-6 {
178                       div.form-group {
179                         label.form-label(for="old-password") {'Old password *'}
180                         input.form-control#old-password(type="password" placeholder="Verify" required minlength=8 maxlength=256) {}
181                         div.invalid-feedback {'Please enter your account\'s password of at least 8 characters.'}
182                       }
183                     }
184                     div.col-md-6 {
185                       div.form-group {
186                         label.form-label(for="new-password") {'New password *'}
187                         input.form-control#'new-password'(type="password" placeholder="Choose" required minlength=8 maxlength=256) {}
188                         div.invalid-feedback {'Please choose a secure password of at least 8 characters.'}
189                       }
190                     }
191                   }
192                 }
193
194                 button.btn.btn-outline-secondary#card-2-clear(type="button" disabled) {
195                   div.icon24-outer.mr-2 {
196                     div.icon24-inner {_out.push(fa_trash)}
197                   }
198                   'Clear'
199                 }
200                 button.btn.btn-success.ml-3#card-2-save(type="button" disabled) {
201                   div.icon24-outer.mr-2#card-2-icon {
202                     div.icon24-inner {_out.push(fa_cloud_upload_alt)}
203                   }
204                   div.icon24-outer.mr-2#card-2-tick(hidden) {
205                     div.icon24-inner {_out.push(icon_tick)}
206                   }
207                   div.icon24-outer.mr-2#card-2-cross(hidden) {
208                     div.icon24-inner {_out.push(icon_cross)}
209                   }
210                   div.icon24-outer.mr-2#card-2-spinner(hidden) {
211                     div.icon24-inner {
212                       div.spinner-border.spinner-border-sm(role="status") {}
213                     }
214                   }
215                   'Save'
216                 }
217
218                 p.'mt-3'.mb-0#card-2-message(hidden) {}
219               }
220             }
221           }
222         }
223
224         p.text-muted.mt-3 {'* These fields are required.'}
225       }
226       else {
227         // signed out
228         p {'For account maintenance, please click on one of the options below.'}
229
230         let transaction = await env.site.database.Transaction()
231         try {
232           let root = await transaction.get()
233           let p = await get_navigation(root, env.component_names)
234           let children = await p.get('children')
235           let menu = await p.get_json('menu')
236   
237           ul.nav.flex-column {
238             for (let i = 0; i < menu.length; ++i) {
239               let name = menu[i]
240               let q = await children.get(name)
241   
242               li.nav-item {
243                 a.nav-link(href=`${name}/index.html`) {
244                   table.icon-and-text {
245                     tr {
246                       td {
247                         _out.push(
248                           await env.site.get_min_svg(await q.get_json('icon'))
249                         )
250                       }
251                       td {
252                         span.h2{
253                           `${await q.get_json('title')}`
254                         }
255                       }
256                     }
257                   }
258                 }
259               }
260             }
261           }
262         }
263         finally {
264           transaction.rollback()
265         }
266       }
267     },
268     // scripts
269     async _out => {
270       console.log('details', details)
271       script {
272         // this will be called by navbar logic after sign in/out
273         function sign_in_out(status) {
274           window.location.reload()
275           return true // suppresses status/dialog
276         }
277       }
278
279       if (signed_in_as !== undefined) {
280         //script(src="/js/utils.js") {}
281
282         script {
283           document.addEventListener(
284             'DOMContentLoaded',
285             () => {
286               let id_accordion = document.getElementById('accordion')
287               let id_card_1 = document.getElementById('card-1')
288               let id_card_1_collapse = document.getElementById('card-1-collapse')
289               let id_card_1_cross = document.getElementById('card-1-cross')
290               let id_card_1_form = document.getElementById('card-1-form')
291               let id_card_1_heading = document.getElementById('card-1-heading')
292               let id_card_1_icon = document.getElementById('card-1-icon')
293               let id_card_1_message = document.getElementById('card-1-message')
294               let id_card_1_revert = document.getElementById('card-1-revert')
295               let id_card_1_save = document.getElementById('card-1-save')
296               let id_card_1_spinner = document.getElementById('card-1-spinner')
297               let id_card_1_tick = document.getElementById('card-1-tick')
298               let id_card_2 = document.getElementById('card-2')
299               let id_card_2_clear = document.getElementById('card-2-clear')
300               let id_card_2_collapse = document.getElementById('card-2-collapse')
301               let id_card_2_cross = document.getElementById('card-2-cross')
302               let id_card_2_form = document.getElementById('card-2-form')
303               let id_card_2_heading = document.getElementById('card-2-heading')
304               let id_card_2_icon = document.getElementById('card-2-icon')
305               let id_card_2_message = document.getElementById('card-2-message')
306               let id_card_2_save = document.getElementById('card-2-save')
307               let id_card_2_spinner = document.getElementById('card-2-spinner')
308               let id_card_2_tick = document.getElementById('card-2-tick')
309               let id_contact_me = document.getElementById('contact-me')
310               let id_family_name = document.getElementById('family-name')
311               let id_given_names = document.getElementById('given-names')
312               let id_new_password = document.getElementById('new-password')
313               let id_old_password = document.getElementById('old-password')
314
315               // pass original values in from server side
316               let orig_details = ${JSON.stringify(details)}
317
318               // change details card
319               let input_semaphore = new BinarySemaphore(false)
320               ;(
321                 async () => {
322                   while (true) {
323                     await input_semaphore.acquire()
324                     await new Promise(resolve => setTimeout(resolve, 3000))
325                     input_semaphore.try_acquire()
326                     await api_call(
327                       '/api/account/change_details/set_draft.json',
328                       id_given_names.value === orig_details.given_names &&
329                         id_family_name.value === orig_details.family_name &&
330                         id_contact_me.checked === orig_details.contact_me ?
331                         null :
332                         {
333                           given_names: id_given_names.value.slice(0, 256),
334                           family_name: id_family_name.value.slice(0, 256),
335                           contact_me: id_contact_me.checked
336                         }
337                     )
338                   }
339                 }
340               )() // ignore returned promise (start thread)
341
342               let card_1_edited = () => {
343                 input_semaphore.release()
344
345                 let disabled =
346                   id_given_names.value === orig_details.given_names &&
347                     id_family_name.value === orig_details.family_name &&
348                     id_contact_me.checked === orig_details.contact_me
349                 id_card_1_revert.disabled = disabled
350                 id_card_1_save.disabled = disabled
351                 id_card_1_icon.hidden = false
352                 id_card_1_tick.hidden = true
353                 id_card_1_cross.hidden = true
354                 id_card_1_spinner.hidden = true
355                 id_card_1_message.hidden = true
356               }
357
358               id_given_names.addEventListener('input', card_1_edited)
359               id_family_name.addEventListener('input', card_1_edited)
360               id_contact_me.addEventListener('input', card_1_edited)
361
362               id_card_1_revert.addEventListener(
363                 'click',
364                 async () => {
365                   id_given_names.value = orig_details.given_names
366                   id_family_name.value = orig_details.family_name
367                   id_contact_me.checked = orig_details.contact_me
368
369                   // cut down form of card_1_edited() logic:
370                   input_semaphore.release()
371
372                   id_card_1_revert.disabled = true
373                   id_card_1_save.disabled = true
374                   id_card_1_icon.hidden = false
375                   id_card_1_tick.hidden = true
376                   id_card_1_cross.hidden = true
377                   id_card_1_spinner.hidden = true
378                   id_card_1_message.hidden = true
379                 }
380               )
381
382               id_card_1_save.addEventListener(
383                 'click',
384                 async () => {
385                   id_card_1_icon.hidden = false
386                   id_card_1_tick.hidden = true
387                   id_card_1_cross.hidden = true
388                   id_card_1_spinner.hidden = true
389                   // the below causes an ugly flicker, so just keep the message
390                   //id_card_1_message.hidden = true
391
392                   if (!id_card_1_form.checkValidity()) {
393                     id_card_1_form.classList.add('was-validated');
394
395                     id_card_1_icon.hidden = true
396                     id_card_1_cross.hidden = false
397                     return
398                   }
399                   id_card_1_form.classList.remove('was-validated');
400
401                   id_card_1_icon.hidden = true
402                   id_card_1_spinner.hidden = false
403                   try {
404                     await api_call(
405                       '/api/account/change_details/set.json',
406                       {
407                         given_names: id_given_names.value.slice(0, 256),
408                         family_name: id_family_name.value.slice(0, 256),
409                         contact_me: id_contact_me.checked
410                       }
411                     )
412                   }
413                   catch (error) {
414                     let problem = Problem.from(error)
415
416                     id_card_1_cross.hidden = false
417                     id_card_1_spinner.hidden = true
418
419                     id_card_1_message.textContent = problem.detail
420                     //id_card_1_message.classList.remove('text-success')
421                     id_card_1_message.classList.add('text-danger')
422                     id_card_1_message.hidden = false
423                     return
424                   }
425                   id_card_1_tick.hidden = false
426                   id_card_1_spinner.hidden = true
427
428                   orig_details.given_names = id_given_names.value
429                   orig_details.family_name = id_family_name.value
430                   orig_details.contact_me = id_contact_me.checked
431
432                   // cut down form of card_1_edited() logic:
433                   input_semaphore.release()
434
435                   id_card_1_revert.disabled = true
436                   id_card_1_save.disabled = true
437                   //id_card_1_icon.hidden = false
438                   //id_card_1_tick.hidden = true
439                   //id_card_1_cross.hidden = true
440                   //id_card_1_spinner.hidden = true
441                   id_card_1_message.hidden = true
442                 }
443               )
444
445               // change password card
446               let card_2_edited = () => {
447                 let disabled =
448                   id_old_password.value.length === 0 &&
449                     id_new_password.value.length === 0
450                 id_card_2_clear.disabled = disabled
451                 id_card_2_save.disabled = disabled
452                 id_card_2_icon.hidden = false
453                 id_card_2_tick.hidden = true
454                 id_card_2_cross.hidden = true
455                 id_card_2_spinner.hidden = true
456                 id_card_2_message.hidden = true
457               }
458
459               id_old_password.addEventListener('input', card_2_edited)
460               id_new_password.addEventListener('input', card_2_edited)
461
462               id_card_2_clear.addEventListener(
463                 'click',
464                 async () => {
465                   id_old_password.value = ''
466                   id_new_password.value = ''
467
468                   // cut down form of card_2_edited() logic:
469                   id_card_2_clear.disabled = true
470                   id_card_2_save.disabled = true
471                   id_card_2_icon.hidden = false
472                   id_card_2_tick.hidden = true
473                   id_card_2_cross.hidden = true
474                   id_card_2_spinner.hidden = true
475                   id_card_2_message.hidden = true
476                 }
477               )
478
479               id_card_2_save.addEventListener(
480                 'click',
481                 async () => {
482                   id_card_2_icon.hidden = false
483                   id_card_2_tick.hidden = true
484                   id_card_2_cross.hidden = true
485                   id_card_2_spinner.hidden = true
486                   // the below causes an ugly flicker, so just keep the message
487                   //id_card_2_message.hidden = true
488
489                   if (!id_card_2_form.checkValidity()) {
490                     id_card_2_form.classList.add('was-validated');
491
492                     id_card_2_icon.hidden = true
493                     id_card_2_cross.hidden = false
494                     return
495                   }
496                   id_card_2_form.classList.remove('was-validated');
497
498                   id_card_2_icon.hidden = true
499                   id_card_2_spinner.hidden = false
500                   try {
501                     await api_call(
502                       '/api/account/change_password.json',
503                       id_old_password.value.slice(0, 256),
504                       id_new_password.value.slice(0, 256)
505                     )
506                   }
507                   catch (error) {
508                     let problem = Problem.from(error)
509
510                     id_card_2_cross.hidden = false
511                     id_card_2_spinner.hidden = true
512
513                     id_card_2_message.textContent = problem.detail
514                     //id_card_2_message.classList.remove('text-success')
515                     id_card_2_message.classList.add('text-danger')
516                     id_card_2_message.hidden = false
517                     return
518                   }
519                   id_card_2_tick.hidden = false
520                   id_card_2_spinner.hidden = true
521
522                   id_old_password.value = ''
523                   id_new_password.value = ''
524
525                   // cut down form of card_2_edited() logic:
526                   id_card_2_clear.disabled = true
527                   id_card_2_save.disabled = true
528                   //id_card_2_icon.hidden = false
529                   //id_card_2_tick.hidden = true
530                   //id_card_2_cross.hidden = true
531                   //id_card_2_spinner.hidden = true
532                   id_card_2_message.hidden = true
533                 }
534               )
535             }
536           )
537         }
538       }
539     }
540   )
541 }