Upgrade to nick_site commit f981fa57, adds alerts and inactive sidebar option
[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     if (signed_in_as === undefined)
28       details = null
29     else {
30       account = await get_account(root, session)
31       details = {
32         given_names: await account.get_json('given_names'),
33         family_name: await account.get_json('family_name'),
34         contact_me: await account.get_json('contact_me')
35       }
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     await 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/*.mt-3*/ {'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                 div.alert.alert-danger.'mt-3'.mb-0#card-1-alert(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                 div.alert.alert-danger.'mt-3'.mb-0#card-2-alert(hidden) {}
219               }
220             }
221           }
222         }
223
224         p.text-muted.mt-3 {'* These fields are required.'}
225       }
226       else {
227         // signed out
228         p/*.mt-3*/ {'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           nav {
238             for (let i = 0; i < menu.length; ++i) {
239               let name = menu[i]
240               let q = await children.get(name)
241
242               // note: make the row a link to show hover background at edges
243               // (similar to the grid-gutter-background class I use elsewhere)
244               // note: position: relative sets the target for stretched link
245               div.row.flex-nowrap.align-items-center.nav-link-outer.position-relative {
246                 div.col.col-icon128 {
247                   div.img-fluid.py-3 {
248                     _out.push(
249                       await env.site.get_min_svg(await q.get_json('icon'))
250                     )
251                   }
252                 }
253                 div.col.col-text {
254                   a.nav-link-inner.stretched-link(href=`${name}/index.html`) {
255                     span.h2{
256                       `${await q.get_json('title')}`
257                     }
258                   }
259                 }
260               }
261             }
262           }
263         }
264         finally {
265           transaction.rollback()
266         }
267       }
268     },
269     // scripts
270     async _out => {
271       console.log('details', details)
272       script {
273         // this will be called by navbar logic after sign in/out
274         function sign_in_out(status) {
275           window.location.reload()
276           return true // suppresses status/dialog
277         }
278       }
279
280       if (signed_in_as !== undefined) {
281         //script(src="/js/utils.js") {}
282
283         script {
284           document.addEventListener(
285             'DOMContentLoaded',
286             () => {
287               let id_accordion = document.getElementById('accordion')
288               let id_card_1 = document.getElementById('card-1')
289               let id_card_1_alert = document.getElementById('card-1-alert')
290               let id_card_1_collapse = document.getElementById('card-1-collapse')
291               let id_card_1_cross = document.getElementById('card-1-cross')
292               let id_card_1_form = document.getElementById('card-1-form')
293               let id_card_1_heading = document.getElementById('card-1-heading')
294               let id_card_1_icon = document.getElementById('card-1-icon')
295               let id_card_1_revert = document.getElementById('card-1-revert')
296               let id_card_1_save = document.getElementById('card-1-save')
297               let id_card_1_spinner = document.getElementById('card-1-spinner')
298               let id_card_1_tick = document.getElementById('card-1-tick')
299               let id_card_2 = document.getElementById('card-2')
300               let id_card_2_alert = document.getElementById('card-2-alert')
301               let id_card_2_clear = document.getElementById('card-2-clear')
302               let id_card_2_collapse = document.getElementById('card-2-collapse')
303               let id_card_2_cross = document.getElementById('card-2-cross')
304               let id_card_2_form = document.getElementById('card-2-form')
305               let id_card_2_heading = document.getElementById('card-2-heading')
306               let id_card_2_icon = document.getElementById('card-2-icon')
307               let id_card_2_save = document.getElementById('card-2-save')
308               let id_card_2_spinner = document.getElementById('card-2-spinner')
309               let id_card_2_tick = document.getElementById('card-2-tick')
310               let id_contact_me = document.getElementById('contact-me')
311               let id_family_name = document.getElementById('family-name')
312               let id_given_names = document.getElementById('given-names')
313               let id_new_password = document.getElementById('new-password')
314               let id_old_password = document.getElementById('old-password')
315
316               // pass original values in from server side
317               let orig_details = ${JSON.stringify(details)}
318
319               // change details card
320               let input_semaphore = new BinarySemaphore(false)
321               ;(
322                 async () => {
323                   while (true) {
324                     await input_semaphore.acquire()
325                     await new Promise(resolve => setTimeout(resolve, 3000))
326                     input_semaphore.try_acquire()
327                     await api_call(
328                       '/api/account/change_details/set_draft.json',
329                       id_given_names.value === orig_details.given_names &&
330                         id_family_name.value === orig_details.family_name &&
331                         id_contact_me.checked === orig_details.contact_me ?
332                         null :
333                         {
334                           given_names: id_given_names.value.slice(0, 256),
335                           family_name: id_family_name.value.slice(0, 256),
336                           contact_me: id_contact_me.checked
337                         }
338                     )
339                   }
340                 }
341               )() // ignore returned promise (start thread)
342
343               let card_1_edited = () => {
344                 input_semaphore.release()
345
346                 let disabled =
347                   id_given_names.value === orig_details.given_names &&
348                     id_family_name.value === orig_details.family_name &&
349                     id_contact_me.checked === orig_details.contact_me
350                 id_card_1_revert.disabled = disabled
351                 id_card_1_save.disabled = disabled
352                 id_card_1_icon.hidden = false
353                 id_card_1_tick.hidden = true
354                 id_card_1_cross.hidden = true
355                 id_card_1_spinner.hidden = true
356                 id_card_1_alert.hidden = true
357               }
358
359               id_given_names.addEventListener('input', card_1_edited)
360               id_family_name.addEventListener('input', card_1_edited)
361               id_contact_me.addEventListener('input', card_1_edited)
362
363               id_card_1_revert.addEventListener(
364                 'click',
365                 async () => {
366                   id_given_names.value = orig_details.given_names
367                   id_family_name.value = orig_details.family_name
368                   id_contact_me.checked = orig_details.contact_me
369
370                   // cut down form of card_1_edited() logic:
371                   input_semaphore.release()
372
373                   id_card_1_revert.disabled = true
374                   id_card_1_save.disabled = true
375                   id_card_1_icon.hidden = false
376                   id_card_1_tick.hidden = true
377                   id_card_1_cross.hidden = true
378                   id_card_1_spinner.hidden = true
379                   id_card_1_alert.hidden = true
380                 }
381               )
382
383               id_card_1_save.addEventListener(
384                 'click',
385                 async () => {
386                   id_card_1_icon.hidden = false
387                   id_card_1_tick.hidden = true
388                   id_card_1_cross.hidden = true
389                   id_card_1_spinner.hidden = true
390                   // the below causes an ugly flicker, so just keep the alert
391                   //id_card_1_alert.hidden = true
392
393                   if (!id_card_1_form.checkValidity()) {
394                     id_card_1_form.classList.add('was-validated');
395
396                     id_card_1_icon.hidden = true
397                     id_card_1_cross.hidden = false
398                     return
399                   }
400                   id_card_1_form.classList.remove('was-validated');
401
402                   id_card_1_icon.hidden = true
403                   id_card_1_spinner.hidden = false
404                   try {
405                     await api_call(
406                       '/api/account/change_details/set.json',
407                       {
408                         given_names: id_given_names.value.slice(0, 256),
409                         family_name: id_family_name.value.slice(0, 256),
410                         contact_me: id_contact_me.checked
411                       }
412                     )
413                   }
414                   catch (error) {
415                     let problem = Problem.from(error)
416
417                     id_card_1_cross.hidden = false
418                     id_card_1_spinner.hidden = true
419
420                     id_card_1_alert.textContent = problem.detail
421                     //id_card_1_alert.classList.remove('alert-success')
422                     //id_card_1_alert.classList.add('alert-danger')
423                     id_card_1_alert.hidden = false
424                     return
425                   }
426                   id_card_1_tick.hidden = false
427                   id_card_1_spinner.hidden = true
428
429                   orig_details.given_names = id_given_names.value
430                   orig_details.family_name = id_family_name.value
431                   orig_details.contact_me = id_contact_me.checked
432
433                   // cut down form of card_1_edited() logic:
434                   input_semaphore.release()
435
436                   id_card_1_revert.disabled = true
437                   id_card_1_save.disabled = true
438                   //id_card_1_icon.hidden = false
439                   //id_card_1_tick.hidden = true
440                   //id_card_1_cross.hidden = true
441                   //id_card_1_spinner.hidden = true
442                   id_card_1_alert.hidden = true
443                 }
444               )
445
446               // change password card
447               let card_2_edited = () => {
448                 let disabled =
449                   id_old_password.value.length === 0 &&
450                     id_new_password.value.length === 0
451                 id_card_2_clear.disabled = disabled
452                 id_card_2_save.disabled = disabled
453                 id_card_2_icon.hidden = false
454                 id_card_2_tick.hidden = true
455                 id_card_2_cross.hidden = true
456                 id_card_2_spinner.hidden = true
457                 id_card_2_alert.hidden = true
458               }
459
460               id_old_password.addEventListener('input', card_2_edited)
461               id_new_password.addEventListener('input', card_2_edited)
462
463               id_card_2_clear.addEventListener(
464                 'click',
465                 async () => {
466                   id_old_password.value = ''
467                   id_new_password.value = ''
468
469                   // cut down form of card_2_edited() logic:
470                   id_card_2_clear.disabled = true
471                   id_card_2_save.disabled = true
472                   id_card_2_icon.hidden = false
473                   id_card_2_tick.hidden = true
474                   id_card_2_cross.hidden = true
475                   id_card_2_spinner.hidden = true
476                   id_card_2_alert.hidden = true
477                 }
478               )
479
480               id_card_2_save.addEventListener(
481                 'click',
482                 async () => {
483                   id_card_2_icon.hidden = false
484                   id_card_2_tick.hidden = true
485                   id_card_2_cross.hidden = true
486                   id_card_2_spinner.hidden = true
487                   // the below causes an ugly flicker, so just keep the alert
488                   //id_card_2_alert.hidden = true
489
490                   if (!id_card_2_form.checkValidity()) {
491                     id_card_2_form.classList.add('was-validated');
492
493                     id_card_2_icon.hidden = true
494                     id_card_2_cross.hidden = false
495                     return
496                   }
497                   id_card_2_form.classList.remove('was-validated');
498
499                   id_card_2_icon.hidden = true
500                   id_card_2_spinner.hidden = false
501                   try {
502                     await api_call(
503                       '/api/account/change_password.json',
504                       id_old_password.value.slice(0, 256),
505                       id_new_password.value.slice(0, 256)
506                     )
507                   }
508                   catch (error) {
509                     let problem = Problem.from(error)
510
511                     id_card_2_cross.hidden = false
512                     id_card_2_spinner.hidden = true
513
514                     id_card_2_alert.textContent = problem.detail
515                     //id_card_2_alert.classList.remove('alert-success')
516                     //id_card_2_alert.classList.add('alert-danger')
517                     id_card_2_alert.hidden = false
518                     return
519                   }
520                   id_card_2_tick.hidden = false
521                   id_card_2_spinner.hidden = true
522
523                   id_old_password.value = ''
524                   id_new_password.value = ''
525
526                   // cut down form of card_2_edited() logic:
527                   id_card_2_clear.disabled = true
528                   id_card_2_save.disabled = true
529                   //id_card_2_icon.hidden = false
530                   //id_card_2_tick.hidden = true
531                   //id_card_2_cross.hidden = true
532                   //id_card_2_spinner.hidden = true
533                   id_card_2_alert.hidden = true
534                 }
535               )
536             }
537           )
538         }
539       }
540     }
541   )
542 }