Update /my_account/index.html.jst to latest way, minor consistency changes in /my_acc...
[ndcode_site.git] / my_account / index.html.jst
index 724ed5c..d7deb7e 100644 (file)
@@ -1,7 +1,11 @@
-let XDate = require('xdate')
-
 return async env => {
   let breadcrumbs = await _require('/_lib/breadcrumbs.jst')
+  let fa_arrow_circle_left = await env.site.get_min_svg('/_svg/fa_arrow-circle-left.svg')
+  let fa_cloud_upload_alt = await env.site.get_min_svg('/_svg/fa_cloud-upload-alt.svg')
+  let fa_envelope = await env.site.get_min_svg('/_svg/fa_envelope.svg')
+  let fa_redo = await env.site.get_min_svg('/_svg/fa_redo.svg')
+  let fa_trash = await env.site.get_min_svg('/_svg/fa_trash.svg')
+  let get_placeholder = await _require('/_lib/get_placeholder.jst')
   let get_account = await _require('/_lib/get_account.jst')
   let get_session = await _require('/_lib/get_session.jst')
   let icon_cross = await env.site.get_min_svg('/_svg/icon_cross.svg')
@@ -9,45 +13,48 @@ return async env => {
   let menu = await env.site.get_menu('/my_account/_menu.json')
   let navbar = await _require('/_lib/navbar.jst')
 
-  // see whether signed in, if so preload details, and draft details if any
+  // preload draft details if any
   let transaction = await env.site.database.Transaction()
-  let signed_in_as
-  let details, draft_details
+  let placeholder
+  let signed_in_as, details, change_details_draft
   try {
     let root = await transaction.get({})
-
     let session = await get_session(env, root)
+
+    placeholder = await get_placeholder(env, session)
+
     signed_in_as = await session.get_json('signed_in_as')
 
-    let account = await get_account(root, session)
-    if (account !== undefined) {
-      details = {
+    account = await get_account(root, session)
+    details =
+      account === undefined ?
+      null :
+      {
         given_names: await account.get_json('given_names'),
         family_name: await account.get_json('family_name'),
         contact_me: await account.get_json('contact_me')
       }
 
-      let change_details_draft = await session.get('change_details_draft')
-      draft_details =
-        change_details_draft !== undefined &&
-          XDate.now() < await change_details_draft.get_json('expires') ?
-          {
-            given_names: await change_details_draft.get_json('given_names'),
-            family_name: await change_details_draft.get_json('family_name'),
-            contact_me: await change_details_draft.get_json('contact_me')
-          } :
-          null
-    }
+    change_details_draft = await session.get_json('change_details_draft')
+    if (change_details_draft === undefined || env.now >= change_details_draft.expires)
+      change_details_draft = null
+
+    transaction.commit()
   }
-  finally {
+  catch (error) {
     transaction.rollback()
+    throw error
   }
-  console.log(
-    'details',
-    JSON.stringify(details),
-    'draft_details',
-    JSON.stringify(draft_details)
+
+  // cheat a little by ignoring the draft if it matches the original details,
+  // because user might have saved the details and left page before the timeout
+  if (
+    change_details_draft &&
+      change_details_draft.given_names === details.given_names &&
+      change_details_draft.family_name === details.family_name &&
+      change_details_draft.contact_me === details.contact_me
   )
+    change_details_draft = null
 
   await navbar(
     env,
@@ -61,114 +68,160 @@ return async env => {
         // signed in
         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.'}
 
-        div.accordion#accordion.mb-5(role="tablist" aria-multiselectable="true") {
-          div.card#step-1 {
-            div.card-header#step-1-heading(role="tab") {
-              span#step-1-tick(style="display: none;") {
-                span.icon-color.pr-3 {_out.push(icon_tick)}
-              }
-              span#step-1-cross(style="display: none;") {
-                span.icon-color.pr-3 {_out.push(icon_cross)}
-              }
-              //span#step-1-spinner(style="display: none;") {
-              //  span.icon-color.pr-3 {
-              //    div.spinner-border(role="status") {
-              //      span.sr-only {'Loading...'}
-              //    }
-              //  }
-              //}
-              a.h5(data-toggle="collapse" data-parent="#accordion" href="#step-1-collapse" aria-expanded="true" aria-controls="step-1-collapse") {
+        div.accordion#accordion(role="tablist" aria-multiselectable="true") {
+          div.card#card-1 {
+            div.card-header#card-1-heading(role="tab") {
+              a.h5(data-toggle="collapse" data-parent="#accordion" href="#card-1-collapse" aria-expanded="true" aria-controls="card-1-collapse") {
                 'Change details'
               }
             }
-            div#step-1-collapse.collapse.show(role="tabpanel" aria-labelledby="step-1-heading" data-parent="#accordion") {
+            div#card-1-collapse.collapse.show(role="tabpanel" aria-labelledby="card-1-heading" data-parent="#accordion") {
               div.card-body {
-                div.row {
-                  div.col-md-6 {
-                    div.form-group {
-                      label.form-label(for="given-names") {'Given names *'}
-                      input.form-control#given-names(type="text" value=draft_details ? draft_details.given_names : details.given_names placeholder="Your given names" required="required" maxlength=256) {}
+                form#card-1-form {
+                  div.row {
+                    div.col-md-6 {
+                      div.form-group {
+                        label.form-label(for="given-names") {'Given names *'}
+                        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) {}
+                        div.invalid-feedback {'Please enter a name we can address you by.'}
+                      }
                     }
-                  }
-                  div.col-md-6 {
-                    div.form-group {
-                      label.form-label(for="family-name") {'Family name'}
-                      input.form-control#family-name(type="text" value=draft_details ? draft_details.family_name : details.family_name placeholder="Your family name" maxlength=256) {}
+                    div.col-md-6 {
+                      div.form-group {
+                        label.form-label(for="family-name") {'Family name'}
+                        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) {}
+                      }
                     }
                   }
-                }
-                div.row.mb-3 {
-                  div.col-md-12 {
-                    div.custom-control.custom-checkbox {
-                      if (draft_details ? draft_details.contact_me : details.contact_me)
-                        input.custom-control-input#contact-me(type="checkbox" checked="checked") {}
-                      else
-                        input.custom-control-input#contact-me(type="checkbox") {}
-                      ' '
-                      label.custom-control-label(for="contact-me") {
-                        'Contact me by email with updates and special offers'
+                  div.row.mb-3 {
+                    div.col-md-12 {
+                      div.custom-control.custom-checkbox {
+                        if (change_details_draft ? change_details_draft.contact_me : details.contact_me)
+                          input.custom-control-input#contact-me(type="checkbox" checked) {}
+                        else
+                          input.custom-control-input#contact-me(type="checkbox") {}
+                        ' '
+                        label.custom-control-label(for="contact-me") {
+                          'Contact me by email with updates and special offers'
+                        }
                       }
                     }
                   }
                 }
 
-                if (draft_details)
-                  button.btn.btn-outline-secondary#step-1-revert(type="button") {'Revert'}
+                if (change_details_draft)
+                  button.btn.btn-outline-secondary#card-1-revert(type="button") {
+                    div.icon24-outer.mr-2 {
+                      div.icon24-inner {_out.push(fa_trash)}
+                    }
+                    'Revert'
+                  }
                 else
-                  button.btn.btn-outline-secondary#step-1-revert(type="button" disabled="disabled") {'Revert'}
-                if (draft_details)
-                  button.btn.btn-success.ml-3#step-1-save(type="button") {'Save'}
+                  button.btn.btn-outline-secondary#card-1-revert(type="button" disabled) {
+                    div.icon24-outer.mr-2 {
+                      div.icon24-inner {_out.push(fa_trash)}
+                    }
+                    'Revert'
+                  }
+                if (change_details_draft)
+                  button.btn.btn-success.ml-3#card-1-save(type="button") {
+                    div.icon24-outer.mr-2#card-1-icon {
+                      div.icon24-inner {_out.push(fa_cloud_upload_alt)}
+                    }
+                    div.icon24-outer.mr-2#card-1-tick(hidden) {
+                      div.icon24-inner {_out.push(icon_tick)}
+                    }
+                    div.icon24-outer.mr-2#card-1-cross(hidden) {
+                      div.icon24-inner {_out.push(icon_cross)}
+                    }
+                    div.icon24-outer.mr-2#card-1-spinner(hidden) {
+                      div.icon24-inner {
+                        div.spinner-border.spinner-border-sm(role="status") {}
+                      }
+                    }
+                    'Save'
+                  }
                 else
-                  button.btn.btn-success.ml-3#step-1-save(type="button" disabled="disabled") {'Save'}
+                  button.btn.btn-success.ml-3#card-1-save(type="button" disabled) {
+                    div.icon24-outer.mr-2#card-1-icon {
+                      div.icon24-inner {_out.push(fa_cloud_upload_alt)}
+                    }
+                    div.icon24-outer.mr-2#card-1-tick(hidden) {
+                      div.icon24-inner {_out.push(icon_tick)}
+                    }
+                    div.icon24-outer.mr-2#card-1-cross(hidden) {
+                      div.icon24-inner {_out.push(icon_cross)}
+                    }
+                    div.icon24-outer.mr-2#card-1-spinner(hidden) {
+                      div.icon24-inner {
+                        div.spinner-border.spinner-border-sm(role="status") {}
+                      }
+                    }
+                    'Save'
+                  }
 
-                p.'mt-3'.mb-0 {'* These fields are required.'}
+                p.'mt-3'.mb-0#card-1-message(hidden) {}
               }
             }
           }
-          div.card#step-2 {
-            div.card-header#step-2-heading(role="tab") {
-              span#step-2-tick(style="display: none;") {
-                span.icon-color.pr-3 {_out.push(icon_tick)}
-              }
-              span#step-2-cross(style="display: none;") {
-                span.icon-color.pr-3 {_out.push(icon_cross)}
-              }
-              span#step-2-spinner(style="display: none;") {
-                span.icon-color.pr-3 {
-                  div.spinner-border(role="status") {
-                    span.sr-only {'Loading...'}
-                  }
-                }
-              }
-              a.h5.collapsed(data-toggle="collapse" data-parent="#accordion" href="#step-2-collapse" aria-expanded="false" aria-controls="step-2-collapse") {
+          div.card#card-2 {
+            div.card-header#card-2-heading(role="tab") {
+              a.h5.collapsed(data-toggle="collapse" data-parent="#accordion" href="#card-2-collapse" aria-expanded="false" aria-controls="card-2-collapse") {
                 'Change password'
               }
             }
-            div#step-2-collapse.collapse(role="tabpanel" aria-labelledby="step-2-heading" data-parent="#accordion") {
+            div#card-2-collapse.collapse(role="tabpanel" aria-labelledby="step-2-heading" data-parent="#accordion") {
               div.card-body {
-                div.row.mb-3 {
-                  div.col-md-6 {
-                    div.form-group {
-                      label.form-label(for="old-password") {'Old password *'}
-                      input.form-control#old-password(type="password" placeholder="Old password" required="required" minlength=8 maxlength=256) {}
+                form#card-2-form {
+                  div.row {
+                    div.col-md-6 {
+                      div.form-group {
+                        label.form-label(for="old-password") {'Old password *'}
+                        input.form-control#old-password(type="password" placeholder="Verify" required minlength=8 maxlength=256) {}
+                        div.invalid-feedback {'Please enter your account\'s password of at least 8 characters.'}
+                      }
                     }
-                  }
-                  div.col-md-6 {
-                    div.form-group {
-                      label.form-label(for="new-password") {'New password *'}
-                      input.form-control#'new-password'(type="password" placeholder="New password" required="required" minlength=8 maxlength=256) {}
+                    div.col-md-6 {
+                      div.form-group {
+                        label.form-label(for="new-password") {'New password *'}
+                        input.form-control#'new-password'(type="password" placeholder="Choose" required minlength=8 maxlength=256) {}
+                        div.invalid-feedback {'Please choose a secure password of at least 8 characters.'}
+                      }
                     }
                   }
                 }
 
-                button.btn.btn-outline-secondary#step-2-clear(type="button" disabled="disabled") {'Clear'}
-                button.btn.btn-success.ml-3#step-2-save(type="button" disabled="disabled") {'Save'}
+                button.btn.btn-outline-secondary#card-2-clear(type="button" disabled) {
+                  div.icon24-outer.mr-2 {
+                    div.icon24-inner {_out.push(fa_trash)}
+                  }
+                  'Clear'
+                }
+                button.btn.btn-success.ml-3#card-2-save(type="button" disabled) {
+                  div.icon24-outer.mr-2#card-2-icon {
+                    div.icon24-inner {_out.push(fa_cloud_upload_alt)}
+                  }
+                  div.icon24-outer.mr-2#card-2-tick(hidden) {
+                    div.icon24-inner {_out.push(icon_tick)}
+                  }
+                  div.icon24-outer.mr-2#card-2-cross(hidden) {
+                    div.icon24-inner {_out.push(icon_cross)}
+                  }
+                  div.icon24-outer.mr-2#card-2-spinner(hidden) {
+                    div.icon24-inner {
+                      div.spinner-border.spinner-border-sm(role="status") {}
+                    }
+                  }
+                  'Save'
+                }
 
-                p.'mt-3'.mb-0 {'* These fields are required.'}
+                p.'mt-3'.mb-0#card-2-message(hidden) {}
               }
             }
           }
         }
+
+        p.text-muted.mt-3 {'* These fields are required.'}
       }
       else {
         // signed out
@@ -199,10 +252,12 @@ return async env => {
     },
     // scripts
     async _out => {
+      console.log('details', details)
       script {
         // this will be called by navbar logic after sign in/out
         function sign_in_out(status) {
           window.location.reload()
+          return true // suppresses status/dialog
         }
       }
 
@@ -210,207 +265,256 @@ return async env => {
         //script(src="/js/utils.js") {}
 
         script {
-          let step_1_dirty = ${JSON.stringify(draft_details !== null)}
-          let draft_timeout_running = false
-          let draft_timeout_handler = async () => {
-            draft_timeout_running = false
-            await api_call(
-              '/api/account/change_details/set_draft.json',
-              step_1_dirty ?
-                {
-                  given_names: document.getElementById('given-names').value.slice(0, 256),
-                  family_name: document.getElementById('family-name').value.slice(0, 256),
-                  contact_me: document.getElementById('contact-me').checked ? true : false
-                } :
-                null
-            )
-            //console.log('draft', await api_call('/api/account/change_details/get_draft.json'))
-          }
-
           document.addEventListener(
             'DOMContentLoaded',
             () => {
-              let step_1_change_handler = () => {
-                step_1_dirty = true
-                document.getElementById('step-1-revert').disabled = false
-                document.getElementById('step-1-save').disabled = false
-
-                if (!draft_timeout_running) {
-                  draft_timeout_running = true
-                  setTimeout(draft_timeout_handler, 5000)
-                }
-              }
-              document.getElementById('given-names').addEventListener(
-                'change',
-                step_1_change_handler
-              )
-              document.getElementById('family-name').addEventListener(
-                'change',
-                step_1_change_handler
-              )
-              document.getElementById('contact-me').addEventListener(
-                'change',
-                step_1_change_handler
-              )
-
-              document.getElementById('step-1-revert').addEventListener(
-                'click',
+              let id_accordion = document.getElementById('accordion')
+              let id_card_1 = document.getElementById('card-1')
+              let id_card_1_collapse = document.getElementById('card-1-collapse')
+              let id_card_1_cross = document.getElementById('card-1-cross')
+              let id_card_1_form = document.getElementById('card-1-form')
+              let id_card_1_heading = document.getElementById('card-1-heading')
+              let id_card_1_icon = document.getElementById('card-1-icon')
+              let id_card_1_message = document.getElementById('card-1-message')
+              let id_card_1_revert = document.getElementById('card-1-revert')
+              let id_card_1_save = document.getElementById('card-1-save')
+              let id_card_1_spinner = document.getElementById('card-1-spinner')
+              let id_card_1_tick = document.getElementById('card-1-tick')
+              let id_card_2 = document.getElementById('card-2')
+              let id_card_2_clear = document.getElementById('card-2-clear')
+              let id_card_2_collapse = document.getElementById('card-2-collapse')
+              let id_card_2_cross = document.getElementById('card-2-cross')
+              let id_card_2_form = document.getElementById('card-2-form')
+              let id_card_2_heading = document.getElementById('card-2-heading')
+              let id_card_2_icon = document.getElementById('card-2-icon')
+              let id_card_2_message = document.getElementById('card-2-message')
+              let id_card_2_save = document.getElementById('card-2-save')
+              let id_card_2_spinner = document.getElementById('card-2-spinner')
+              let id_card_2_tick = document.getElementById('card-2-tick')
+              let id_contact_me = document.getElementById('contact-me')
+              let id_family_name = document.getElementById('family-name')
+              let id_given_names = document.getElementById('given-names')
+              let id_new_password = document.getElementById('new-password')
+              let id_old_password = document.getElementById('old-password')
+
+              // pass original values in from server side
+              let orig_details = ${JSON.stringify(details)}
+
+              // change details card
+              let input_semaphore = new BinarySemaphore(false)
+              ;(
                 async () => {
-                  $('#step-1-tick').hide()
-                  $('#step-1-cross').hide()
-                  $('#step-1-spinner').show()
-
-                  let details
-                  try {
-                    details = await api_call(
-                      '/api/account/change_details/get.json'
+                  while (true) {
+                    await input_semaphore.acquire()
+                    await new Promise(resolve => setTimeout(resolve, 3000))
+                    input_semaphore.try_acquire()
+                    await api_call(
+                      '/api/account/change_details/set_draft.json',
+                      id_given_names.value === orig_details.given_names &&
+                        id_family_name.value === orig_details.family_name &&
+                        id_contact_me.checked === orig_details.contact_me ?
+                        null :
+                        {
+                          given_names: id_given_names.value.slice(0, 256),
+                          family_name: id_family_name.value.slice(0, 256),
+                          contact_me: id_contact_me.checked
+                        }
                     )
                   }
-                  catch (error) {
-                    let problem = Problem.from(error)
-                    console.log(problem.detail)
-
-                    $('#step-1-tick').hide()
-                    $('#step-1-cross').show()
-                    $('#step-1-spinner').hide()
-                    return
-                  }
-                  $('#step-1-tick').hide()
-                  $('#step-1-cross').hide()
-                  $('#step-1-spinner').hide()
-
-                  step_1_dirty = false
-                  document.getElementById('step-1-revert').disabled = true
-                  document.getElementById('step-1-save').disabled = true
+                }
+              )() // ignore returned promise (start thread)
+
+              let card_1_edited = () => {
+                input_semaphore.release()
+
+                let disabled =
+                  id_given_names.value === orig_details.given_names &&
+                    id_family_name.value === orig_details.family_name &&
+                    id_contact_me.checked === orig_details.contact_me
+                id_card_1_revert.disabled = disabled
+                id_card_1_save.disabled = disabled
+                id_card_1_icon.hidden = false
+                id_card_1_tick.hidden = true
+                id_card_1_cross.hidden = true
+                id_card_1_spinner.hidden = true
+                id_card_1_message.hidden = true
+              }
 
-                  document.getElementById('given-names').value = details.given_names
-                  document.getElementById('family-name').value = details.family_name
-                  document.getElementById('contact-me').checked = details.contact_me
+              id_given_names.addEventListener('input', card_1_edited)
+              id_family_name.addEventListener('input', card_1_edited)
+              id_contact_me.addEventListener('input', card_1_edited)
 
-                  if (!draft_timeout_running) {
-                    draft_timeout_running = true
-                    setTimeout(draft_timeout_handler, 5000)
-                  }
+              id_card_1_revert.addEventListener(
+                'click',
+                async () => {
+                  id_given_names.value = orig_details.given_names
+                  id_family_name.value = orig_details.family_name
+                  id_contact_me.checked = orig_details.contact_me
+
+                  // cut down form of card_1_edited() logic:
+                  input_semaphore.release()
+
+                  id_card_1_revert.disabled = true
+                  id_card_1_save.disabled = true
+                  id_card_1_icon.hidden = false
+                  id_card_1_tick.hidden = true
+                  id_card_1_cross.hidden = true
+                  id_card_1_spinner.hidden = true
+                  id_card_1_message.hidden = true
                 }
               )
 
-              document.getElementById('step-1-save').addEventListener(
+              id_card_1_save.addEventListener(
                 'click',
                 async () => {
-                  if (
-                    !document.getElementById('given-names').reportValidity() ||
-                      !document.getElementById('family-name').reportValidity()
-                  ) {
-                    $('#step-1-tick').hide()
-                    $('#step-1-cross').show()
-                    $('#step-1-spinner').hide()
-                    return false
+                  id_card_1_icon.hidden = false
+                  id_card_1_tick.hidden = true
+                  id_card_1_cross.hidden = true
+                  id_card_1_spinner.hidden = true
+                  // the below causes an ugly flicker, so just keep the message
+                  //id_card_1_message.hidden = true
+
+                  if (!id_card_1_form.checkValidity()) {
+                    id_card_1_form.classList.add('was-validated');
+
+                    id_card_1_icon.hidden = true
+                    id_card_1_cross.hidden = false
+                    return
                   }
-                  $('#step-1-tick').hide()
-                  $('#step-1-cross').hide()
-                  $('#step-1-spinner').show()
+                  id_card_1_form.classList.remove('was-validated');
 
+                  id_card_1_icon.hidden = true
+                  id_card_1_spinner.hidden = false
                   try {
                     await api_call(
                       '/api/account/change_details/set.json',
                       {
-                        given_names: document.getElementById('given-names').value.slice(0, 256),
-                        family_name: document.getElementById('family-name').value.slice(0, 256),
-                        contact_me: document.getElementById('contact-me').checked ? true : false
+                        given_names: id_given_names.value.slice(0, 256),
+                        family_name: id_family_name.value.slice(0, 256),
+                        contact_me: id_contact_me.checked
                       }
                     )
                   }
                   catch (error) {
                     let problem = Problem.from(error)
-                    console.log(problem.detail)
 
-                    $('#step-1-tick').hide()
-                    $('#step-1-cross').show()
-                    $('#step-1-spinner').hide()
+                    id_card_1_cross.hidden = false
+                    id_card_1_spinner.hidden = true
+
+                    id_card_1_message.textContent = problem.detail
+                    //id_card_1_message.classList.remove('text-success')
+                    id_card_1_message.classList.add('text-danger')
+                    id_card_1_message.hidden = false
                     return
                   }
-                  $('#step-1-tick').show()
-                  $('#step-1-cross').hide()
-                  $('#step-1-spinner').hide()
-
-                  step_1_dirty = false
-                  document.getElementById('step-1-revert').disabled = true
-                  document.getElementById('step-1-save').disabled = true
-
-                  // SHOULD execute immediately here
-                  // (because user is likely to leave the page after save)
-                  if (!draft_timeout_running) {
-                    draft_timeout_running = true
-                    setTimeout(draft_timeout_handler, 5000)
-                  }
+                  id_card_1_tick.hidden = false
+                  id_card_1_spinner.hidden = true
+
+                  orig_details.given_names = id_given_names.value
+                  orig_details.family_name = id_family_name.value
+                  orig_details.contact_me = id_contact_me.checked
+
+                  // cut down form of card_1_edited() logic:
+                  input_semaphore.release()
+
+                  id_card_1_revert.disabled = true
+                  id_card_1_save.disabled = true
+                  //id_card_1_icon.hidden = false
+                  //id_card_1_tick.hidden = true
+                  //id_card_1_cross.hidden = true
+                  //id_card_1_spinner.hidden = true
+                  id_card_1_message.hidden = true
                 }
               )
 
-              let step_2_change_handler = () => {
-                document.getElementById('step-2-clear').disabled = false
-                document.getElementById('step-2-save').disabled = false
+              // change password card
+              let card_2_edited = () => {
+                let disabled =
+                  id_old_password.value.length === 0 &&
+                    id_new_password.value.length === 0
+                id_card_2_clear.disabled = disabled
+                id_card_2_save.disabled = disabled
+                id_card_2_icon.hidden = false
+                id_card_2_tick.hidden = true
+                id_card_2_cross.hidden = true
+                id_card_2_spinner.hidden = true
+                id_card_2_message.hidden = true
               }
-              document.getElementById('old-password').addEventListener(
-                'change',
-                step_2_change_handler
-              )
-              document.getElementById('new-password').addEventListener(
-                'change',
-                step_2_change_handler
-              )
 
-              document.getElementById('step-2-clear').addEventListener(
-                'click',
-                () => {
-                  document.getElementById('step-2-clear').disabled = true
-                  document.getElementById('step-2-save').disabled = true
+              id_old_password.addEventListener('input', card_2_edited)
+              id_new_password.addEventListener('input', card_2_edited)
 
-                  document.getElementById('old-password').value = ''
-                  document.getElementById('new-password').value = ''
+              id_card_2_clear.addEventListener(
+                'click',
+                async () => {
+                  id_old_password.value = ''
+                  id_new_password.value = ''
+
+                  // cut down form of card_2_edited() logic:
+                  id_card_2_clear.disabled = true
+                  id_card_2_save.disabled = true
+                  id_card_2_icon.hidden = false
+                  id_card_2_tick.hidden = true
+                  id_card_2_cross.hidden = true
+                  id_card_2_spinner.hidden = true
+                  id_card_2_message.hidden = true
                 }
               )
 
-              document.getElementById('step-2-save').addEventListener(
+              id_card_2_save.addEventListener(
                 'click',
                 async () => {
-                  if (
-                    !document.getElementById('old-password').reportValidity() ||
-                      !document.getElementById('new-password').reportValidity()
-                  ) {
-                    $('#step-2-tick').hide()
-                    $('#step-2-cross').show()
-                    $('#step-2-spinner').hide()
-                    return false
+                  id_card_2_icon.hidden = false
+                  id_card_2_tick.hidden = true
+                  id_card_2_cross.hidden = true
+                  id_card_2_spinner.hidden = true
+                  // the below causes an ugly flicker, so just keep the message
+                  //id_card_2_message.hidden = true
+
+                  if (!id_card_2_form.checkValidity()) {
+                    id_card_2_form.classList.add('was-validated');
+
+                    id_card_2_icon.hidden = true
+                    id_card_2_cross.hidden = false
+                    return
                   }
-                  $('#step-2-tick').hide()
-                  $('#step-2-cross').hide()
-                  $('#step-2-spinner').show()
+                  id_card_2_form.classList.remove('was-validated');
 
+                  id_card_2_icon.hidden = true
+                  id_card_2_spinner.hidden = false
                   try {
                     await api_call(
                       '/api/account/change_password.json',
-                      document.getElementById('old-password').value.slice(0, 256),
-                      document.getElementById('new-password').value.slice(0, 256)
+                      id_old_password.value.slice(0, 256),
+                      id_new_password.value.slice(0, 256)
                     )
                   }
                   catch (error) {
                     let problem = Problem.from(error)
-                    console.log(problem.detail)
 
-                    $('#step-2-tick').hide()
-                    $('#step-2-cross').show()
-                    $('#step-2-spinner').hide()
+                    id_card_2_cross.hidden = false
+                    id_card_2_spinner.hidden = true
+
+                    id_card_2_message.textContent = problem.detail
+                    //id_card_2_message.classList.remove('text-success')
+                    id_card_2_message.classList.add('text-danger')
+                    id_card_2_message.hidden = false
                     return
                   }
-                  $('#step-2-tick').show()
-                  $('#step-2-cross').hide()
-                  $('#step-2-spinner').hide()
-
-                  document.getElementById('step-2-clear').disabled = true
-                  document.getElementById('step-2-save').disabled = true
-
-                  document.getElementById('old-password').value = ''
-                  document.getElementById('new-password').value = ''
+                  id_card_2_tick.hidden = false
+                  id_card_2_spinner.hidden = true
+
+                  id_old_password.value = ''
+                  id_new_password.value = ''
+
+                  // cut down form of card_2_edited() logic:
+                  id_card_2_clear.disabled = true
+                  id_card_2_save.disabled = true
+                  //id_card_2_icon.hidden = false
+                  //id_card_2_tick.hidden = true
+                  //id_card_2_cross.hidden = true
+                  //id_card_2_spinner.hidden = true
+                  id_card_2_message.hidden = true
                 }
               )
             }