d7deb7ed940ba401b082a6c9ddafc2a21edcc005
[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_session = await _require('/_lib/get_session.jst')
11   let icon_cross = await env.site.get_min_svg('/_svg/icon_cross.svg')
12   let icon_tick = await env.site.get_min_svg('/_svg/icon_tick.svg')
13   let menu = await env.site.get_menu('/my_account/_menu.json')
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         ul.nav.flex-column {
231           let entries = menu.entries
232           for (let i = 0; i < entries.length; ++i) {
233             let entry = entries[i]
234             if (Object.prototype.hasOwnProperty.call(entry, 'icon'))
235               li.nav-item {
236                 a.nav-link(href=`${entry.dir}/index.html`) {
237                   table.icon-and-text {
238                     tr {
239                       td {
240                         _out.push(await env.site.get_min_svg(entry.icon))
241                       }
242                       td {
243                         span.h2{`${entry.name}`}
244                       }
245                     }
246                   }
247                 }
248               }
249           }
250         }
251       }
252     },
253     // scripts
254     async _out => {
255       console.log('details', details)
256       script {
257         // this will be called by navbar logic after sign in/out
258         function sign_in_out(status) {
259           window.location.reload()
260           return true // suppresses status/dialog
261         }
262       }
263
264       if (signed_in_as !== undefined) {
265         //script(src="/js/utils.js") {}
266
267         script {
268           document.addEventListener(
269             'DOMContentLoaded',
270             () => {
271               let id_accordion = document.getElementById('accordion')
272               let id_card_1 = document.getElementById('card-1')
273               let id_card_1_collapse = document.getElementById('card-1-collapse')
274               let id_card_1_cross = document.getElementById('card-1-cross')
275               let id_card_1_form = document.getElementById('card-1-form')
276               let id_card_1_heading = document.getElementById('card-1-heading')
277               let id_card_1_icon = document.getElementById('card-1-icon')
278               let id_card_1_message = document.getElementById('card-1-message')
279               let id_card_1_revert = document.getElementById('card-1-revert')
280               let id_card_1_save = document.getElementById('card-1-save')
281               let id_card_1_spinner = document.getElementById('card-1-spinner')
282               let id_card_1_tick = document.getElementById('card-1-tick')
283               let id_card_2 = document.getElementById('card-2')
284               let id_card_2_clear = document.getElementById('card-2-clear')
285               let id_card_2_collapse = document.getElementById('card-2-collapse')
286               let id_card_2_cross = document.getElementById('card-2-cross')
287               let id_card_2_form = document.getElementById('card-2-form')
288               let id_card_2_heading = document.getElementById('card-2-heading')
289               let id_card_2_icon = document.getElementById('card-2-icon')
290               let id_card_2_message = document.getElementById('card-2-message')
291               let id_card_2_save = document.getElementById('card-2-save')
292               let id_card_2_spinner = document.getElementById('card-2-spinner')
293               let id_card_2_tick = document.getElementById('card-2-tick')
294               let id_contact_me = document.getElementById('contact-me')
295               let id_family_name = document.getElementById('family-name')
296               let id_given_names = document.getElementById('given-names')
297               let id_new_password = document.getElementById('new-password')
298               let id_old_password = document.getElementById('old-password')
299
300               // pass original values in from server side
301               let orig_details = ${JSON.stringify(details)}
302
303               // change details card
304               let input_semaphore = new BinarySemaphore(false)
305               ;(
306                 async () => {
307                   while (true) {
308                     await input_semaphore.acquire()
309                     await new Promise(resolve => setTimeout(resolve, 3000))
310                     input_semaphore.try_acquire()
311                     await api_call(
312                       '/api/account/change_details/set_draft.json',
313                       id_given_names.value === orig_details.given_names &&
314                         id_family_name.value === orig_details.family_name &&
315                         id_contact_me.checked === orig_details.contact_me ?
316                         null :
317                         {
318                           given_names: id_given_names.value.slice(0, 256),
319                           family_name: id_family_name.value.slice(0, 256),
320                           contact_me: id_contact_me.checked
321                         }
322                     )
323                   }
324                 }
325               )() // ignore returned promise (start thread)
326
327               let card_1_edited = () => {
328                 input_semaphore.release()
329
330                 let disabled =
331                   id_given_names.value === orig_details.given_names &&
332                     id_family_name.value === orig_details.family_name &&
333                     id_contact_me.checked === orig_details.contact_me
334                 id_card_1_revert.disabled = disabled
335                 id_card_1_save.disabled = disabled
336                 id_card_1_icon.hidden = false
337                 id_card_1_tick.hidden = true
338                 id_card_1_cross.hidden = true
339                 id_card_1_spinner.hidden = true
340                 id_card_1_message.hidden = true
341               }
342
343               id_given_names.addEventListener('input', card_1_edited)
344               id_family_name.addEventListener('input', card_1_edited)
345               id_contact_me.addEventListener('input', card_1_edited)
346
347               id_card_1_revert.addEventListener(
348                 'click',
349                 async () => {
350                   id_given_names.value = orig_details.given_names
351                   id_family_name.value = orig_details.family_name
352                   id_contact_me.checked = orig_details.contact_me
353
354                   // cut down form of card_1_edited() logic:
355                   input_semaphore.release()
356
357                   id_card_1_revert.disabled = true
358                   id_card_1_save.disabled = true
359                   id_card_1_icon.hidden = false
360                   id_card_1_tick.hidden = true
361                   id_card_1_cross.hidden = true
362                   id_card_1_spinner.hidden = true
363                   id_card_1_message.hidden = true
364                 }
365               )
366
367               id_card_1_save.addEventListener(
368                 'click',
369                 async () => {
370                   id_card_1_icon.hidden = false
371                   id_card_1_tick.hidden = true
372                   id_card_1_cross.hidden = true
373                   id_card_1_spinner.hidden = true
374                   // the below causes an ugly flicker, so just keep the message
375                   //id_card_1_message.hidden = true
376
377                   if (!id_card_1_form.checkValidity()) {
378                     id_card_1_form.classList.add('was-validated');
379
380                     id_card_1_icon.hidden = true
381                     id_card_1_cross.hidden = false
382                     return
383                   }
384                   id_card_1_form.classList.remove('was-validated');
385
386                   id_card_1_icon.hidden = true
387                   id_card_1_spinner.hidden = false
388                   try {
389                     await api_call(
390                       '/api/account/change_details/set.json',
391                       {
392                         given_names: id_given_names.value.slice(0, 256),
393                         family_name: id_family_name.value.slice(0, 256),
394                         contact_me: id_contact_me.checked
395                       }
396                     )
397                   }
398                   catch (error) {
399                     let problem = Problem.from(error)
400
401                     id_card_1_cross.hidden = false
402                     id_card_1_spinner.hidden = true
403
404                     id_card_1_message.textContent = problem.detail
405                     //id_card_1_message.classList.remove('text-success')
406                     id_card_1_message.classList.add('text-danger')
407                     id_card_1_message.hidden = false
408                     return
409                   }
410                   id_card_1_tick.hidden = false
411                   id_card_1_spinner.hidden = true
412
413                   orig_details.given_names = id_given_names.value
414                   orig_details.family_name = id_family_name.value
415                   orig_details.contact_me = id_contact_me.checked
416
417                   // cut down form of card_1_edited() logic:
418                   input_semaphore.release()
419
420                   id_card_1_revert.disabled = true
421                   id_card_1_save.disabled = true
422                   //id_card_1_icon.hidden = false
423                   //id_card_1_tick.hidden = true
424                   //id_card_1_cross.hidden = true
425                   //id_card_1_spinner.hidden = true
426                   id_card_1_message.hidden = true
427                 }
428               )
429
430               // change password card
431               let card_2_edited = () => {
432                 let disabled =
433                   id_old_password.value.length === 0 &&
434                     id_new_password.value.length === 0
435                 id_card_2_clear.disabled = disabled
436                 id_card_2_save.disabled = disabled
437                 id_card_2_icon.hidden = false
438                 id_card_2_tick.hidden = true
439                 id_card_2_cross.hidden = true
440                 id_card_2_spinner.hidden = true
441                 id_card_2_message.hidden = true
442               }
443
444               id_old_password.addEventListener('input', card_2_edited)
445               id_new_password.addEventListener('input', card_2_edited)
446
447               id_card_2_clear.addEventListener(
448                 'click',
449                 async () => {
450                   id_old_password.value = ''
451                   id_new_password.value = ''
452
453                   // cut down form of card_2_edited() logic:
454                   id_card_2_clear.disabled = true
455                   id_card_2_save.disabled = true
456                   id_card_2_icon.hidden = false
457                   id_card_2_tick.hidden = true
458                   id_card_2_cross.hidden = true
459                   id_card_2_spinner.hidden = true
460                   id_card_2_message.hidden = true
461                 }
462               )
463
464               id_card_2_save.addEventListener(
465                 'click',
466                 async () => {
467                   id_card_2_icon.hidden = false
468                   id_card_2_tick.hidden = true
469                   id_card_2_cross.hidden = true
470                   id_card_2_spinner.hidden = true
471                   // the below causes an ugly flicker, so just keep the message
472                   //id_card_2_message.hidden = true
473
474                   if (!id_card_2_form.checkValidity()) {
475                     id_card_2_form.classList.add('was-validated');
476
477                     id_card_2_icon.hidden = true
478                     id_card_2_cross.hidden = false
479                     return
480                   }
481                   id_card_2_form.classList.remove('was-validated');
482
483                   id_card_2_icon.hidden = true
484                   id_card_2_spinner.hidden = false
485                   try {
486                     await api_call(
487                       '/api/account/change_password.json',
488                       id_old_password.value.slice(0, 256),
489                       id_new_password.value.slice(0, 256)
490                     )
491                   }
492                   catch (error) {
493                     let problem = Problem.from(error)
494
495                     id_card_2_cross.hidden = false
496                     id_card_2_spinner.hidden = true
497
498                     id_card_2_message.textContent = problem.detail
499                     //id_card_2_message.classList.remove('text-success')
500                     id_card_2_message.classList.add('text-danger')
501                     id_card_2_message.hidden = false
502                     return
503                   }
504                   id_card_2_tick.hidden = false
505                   id_card_2_spinner.hidden = true
506
507                   id_old_password.value = ''
508                   id_new_password.value = ''
509
510                   // cut down form of card_2_edited() logic:
511                   id_card_2_clear.disabled = true
512                   id_card_2_save.disabled = true
513                   //id_card_2_icon.hidden = false
514                   //id_card_2_tick.hidden = true
515                   //id_card_2_cross.hidden = true
516                   //id_card_2_spinner.hidden = true
517                   id_card_2_message.hidden = true
518                 }
519               )
520             }
521           )
522         }
523       }
524     }
525   )
526 }