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')
16 // preload draft details if any
17 let transaction = await env.site.database.Transaction()
19 let signed_in_as, details, change_details_draft
21 let root = await transaction.get()
22 let session = await get_session(env, root)
24 placeholder = await get_placeholder(env, session)
26 signed_in_as = await session.get_json('signed_in_as')
27 if (signed_in_as === undefined)
30 account = await get_account(root, session)
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')
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
42 await transaction.commit()
45 transaction.rollback()
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
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
57 change_details_draft = null
65 await breadcrumbs(env, _out)
67 if (signed_in_as !== undefined) {
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.'}
71 div.accordion#accordion(role="tablist" aria-multiselectable="true") {
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") {
78 div#card-1-collapse.collapse.show(role="tabpanel" aria-labelledby="card-1-heading" data-parent="#accordion") {
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.'}
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) {}
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) {}
102 input.custom-control-input#contact-me(type="checkbox") {}
104 label.custom-control-label(for="contact-me") {
105 'Contact me by email with updates and special offers'
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)}
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)}
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)}
131 div.icon24-outer.mr-2#card-1-tick(hidden) {
132 div.icon24-inner {_out.push(icon_tick)}
134 div.icon24-outer.mr-2#card-1-cross(hidden) {
135 div.icon24-inner {_out.push(icon_cross)}
137 div.icon24-outer.mr-2#card-1-spinner(hidden) {
139 div.spinner-border.spinner-border-sm(role="status") {}
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)}
149 div.icon24-outer.mr-2#card-1-tick(hidden) {
150 div.icon24-inner {_out.push(icon_tick)}
152 div.icon24-outer.mr-2#card-1-cross(hidden) {
153 div.icon24-inner {_out.push(icon_cross)}
155 div.icon24-outer.mr-2#card-1-spinner(hidden) {
157 div.spinner-border.spinner-border-sm(role="status") {}
163 div.alert.alert-danger.'mt-3'.mb-0#card-1-alert(hidden) {}
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") {
173 div#card-2-collapse.collapse(role="tabpanel" aria-labelledby="step-2-heading" data-parent="#accordion") {
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.'}
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.'}
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)}
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)}
204 div.icon24-outer.mr-2#card-2-tick(hidden) {
205 div.icon24-inner {_out.push(icon_tick)}
207 div.icon24-outer.mr-2#card-2-cross(hidden) {
208 div.icon24-inner {_out.push(icon_cross)}
210 div.icon24-outer.mr-2#card-2-spinner(hidden) {
212 div.spinner-border.spinner-border-sm(role="status") {}
218 div.alert.alert-danger.'mt-3'.mb-0#card-2-alert(hidden) {}
224 p.text-muted.mt-3 {'* These fields are required.'}
228 p/*.mt-3*/ {'For account maintenance, please click on one of the options below.'}
230 let transaction = await env.site.database.Transaction()
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')
238 for (let i = 0; i < menu.length; ++i) {
240 let q = await children.get(name)
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 {
249 await env.site.get_min_svg(await q.get_json('icon'))
254 a.nav-link-inner.stretched-link(href=`${name}/index.html`) {
256 `${await q.get_json('title')}`
265 transaction.rollback()
271 console.log('details', details)
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
280 if (signed_in_as !== undefined) {
281 //script(src="/js/utils.js") {}
284 document.addEventListener(
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')
316 // pass original values in from server side
317 let orig_details = ${JSON.stringify(details)}
319 // change details card
320 let input_semaphore = new BinarySemaphore(false)
324 await input_semaphore.acquire()
325 await new Promise(resolve => setTimeout(resolve, 3000))
326 input_semaphore.try_acquire()
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 ?
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
341 )() // ignore returned promise (start thread)
343 let card_1_edited = () => {
344 input_semaphore.release()
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
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)
363 id_card_1_revert.addEventListener(
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
370 // cut down form of card_1_edited() logic:
371 input_semaphore.release()
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
383 id_card_1_save.addEventListener(
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
393 if (!id_card_1_form.checkValidity()) {
394 id_card_1_form.classList.add('was-validated');
396 id_card_1_icon.hidden = true
397 id_card_1_cross.hidden = false
400 id_card_1_form.classList.remove('was-validated');
402 id_card_1_icon.hidden = true
403 id_card_1_spinner.hidden = false
406 '/api/account/change_details/set.json',
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
415 let problem = Problem.from(error)
417 id_card_1_cross.hidden = false
418 id_card_1_spinner.hidden = true
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
426 id_card_1_tick.hidden = false
427 id_card_1_spinner.hidden = true
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
433 // cut down form of card_1_edited() logic:
434 input_semaphore.release()
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
446 // change password card
447 let card_2_edited = () => {
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
460 id_old_password.addEventListener('input', card_2_edited)
461 id_new_password.addEventListener('input', card_2_edited)
463 id_card_2_clear.addEventListener(
466 id_old_password.value = ''
467 id_new_password.value = ''
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
480 id_card_2_save.addEventListener(
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
490 if (!id_card_2_form.checkValidity()) {
491 id_card_2_form.classList.add('was-validated');
493 id_card_2_icon.hidden = true
494 id_card_2_cross.hidden = false
497 id_card_2_form.classList.remove('was-validated');
499 id_card_2_icon.hidden = true
500 id_card_2_spinner.hidden = false
503 '/api/account/change_password.json',
504 id_old_password.value.slice(0, 256),
505 id_new_password.value.slice(0, 256)
509 let problem = Problem.from(error)
511 id_card_2_cross.hidden = false
512 id_card_2_spinner.hidden = true
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
520 id_card_2_tick.hidden = false
521 id_card_2_spinner.hidden = true
523 id_old_password.value = ''
524 id_new_password.value = ''
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