In /lib/navbar.jst rework feedback to be similar to /contact/index.html.jst way
authorNick Downing <nick@ndcode.org>
Wed, 26 Jan 2022 11:11:14 +0000 (22:11 +1100)
committerNick Downing <nick@ndcode.org>
Wed, 26 Jan 2022 12:09:18 +0000 (23:09 +1100)
_lib/navbar.jst
_svg/fa_times-circle.svg [new file with mode: 0644]
_svg/fa_unlock-alt.svg [new file with mode: 0644]
api/feedback/get_draft.json.jst [new file with mode: 0644]
api/feedback/send_message.json.jst [moved from api/feedback.json.jst with 100% similarity]
api/feedback/set_draft.json.jst [new file with mode: 0644]
contact/index.html.jst

index f9a7682..da759f9 100644 (file)
@@ -3,9 +3,15 @@ let XDate = require('xdate')
 
 return async (env, head, body, scripts) => {
   //let cart = await _require('/online_store/cart.jst')
+  let fa_arrow_circle_left = await env.site.get_min_svg('/_svg/fa_arrow-circle-left.svg')
+  let fa_times_circle = await env.site.get_min_svg('/_svg/fa_times-circle.svg')
+  let fa_envelope = await env.site.get_min_svg('/_svg/fa_envelope.svg')
+  let fa_unlock_alt = await env.site.get_min_svg('/_svg/fa_unlock-alt.svg')
   let fa_search = await env.site.get_min_svg('/_svg/fa_search.svg')
   let get_session = await _require('/_lib/get_session.jst')
   //let icon_cart_small = await env.site.get_min_svg('/_svg/icon_cart_small.svg')
+  let icon_cross = await env.site.get_min_svg('/_svg/icon_cross.svg')
+  //let icon_tick = await env.site.get_min_svg('/_svg/icon_tick.svg')
   let logo_large = await env.site.get_min_svg('/_svg/logo_large.svg')
   let menu = await env.site.get_menu('/_menu.json')
   let page = await _require('/_lib/page.jst')
@@ -16,6 +22,7 @@ return async (env, head, body, scripts) => {
   let transaction = await env.site.database.Transaction()
   let signed_in_as
   let site_title, copyright
+  let feedback_draft
   try {
     let root = await transaction.get({})
 
@@ -25,6 +32,10 @@ return async (env, head, body, scripts) => {
     let globals = await root.get('globals', {})
     site_title = await globals.get_json('site_title')
     copyright = await globals.get_json('copyright')
+
+    feedback_draft = await session.get_json('feedback_draft')
+    if (feedback_draft === undefined || env.now >= feedback_draft.expires)
+      feedback_draft = null
   }
   finally {
     transaction.rollback()
@@ -227,7 +238,7 @@ return async (env, head, body, scripts) => {
                 div.col-md-12 {
                   div.form-group {
                     label.form-label(for="navbar-sign-in-email") {'Email'}
-                    input.form-control#navbar-sign-in-email(type="text" placeholder="Account email address" required="required" maxlength=256) {}
+                    input.form-control#navbar-sign-in-email(type="text" placeholder="Account email address" required maxlength=256) {}
                   }
                 }
               }
@@ -235,7 +246,7 @@ return async (env, head, body, scripts) => {
                 div.col-md-12 {
                   div.form-group {
                     label.form-label(for="navbar-sign-in-password") {'Password'}
-                    input.form-control#navbar-sign-in-password(type="password" placeholder="Account password" required="required" minlength=8 maxlength=256) {}
+                    input.form-control#navbar-sign-in-password(type="password" placeholder="Account password" required minlength=8 maxlength=256) {}
                   }
                 }
               }
@@ -252,9 +263,15 @@ return async (env, head, body, scripts) => {
             }
             div.modal-footer {
               button.btn.btn-outline-secondary(type="button" data-dismiss="modal") {
-                'Cancel'
+                div.icon24-outer.mr-2 {
+                  div.icon24-inner {_out.push(fa_arrow_circle_left)}
+                }
+                'Back'
               }
               button.btn.btn-primary#navbar-sign-in-submit(type="button") {
+                div.icon24-outer.mr-2 {
+                  div.icon24-inner {_out.push(fa_unlock_alt)}
+                }
                 'Sign in'
               }
             }
@@ -272,22 +289,67 @@ return async (env, head, body, scripts) => {
               p {
                 'Did you notice something not quite right, or just want to share your impression of this page?'
               }
-              div.row {
-                div.col-md-12 {
-                  div.form-group {
-                    label.form-label(for="navbar-feedback-message") {'Message'}
-                    textarea.form-control#navbar-feedback-message(placeholder="Please tell us your thoughts" required="required" rows=4 maxlength=65536) {}
+
+              form#navbar-feedback-form {
+                div.row {
+                  div.col-md-12 {
+                    div.form-group {
+                      label.form-label(for="navbar-feedback-message1") {'Message'}
+                      textarea.form-control#navbar-feedback-message1(placeholder="I noticed that..." required rows=4 maxlength=65536) {
+                        if (feedback_draft)
+                          `${feedback_draft.message}`
+                      }
+                      div.invalid-feedback {'Please let us have your thoughts.'}
+                    }
                   }
                 }
               }
+
+              p.'mt-3'.mb-0#navbar-feedback-message(hidden) {}
             }
             div.modal-footer {
               button.btn.btn-outline-secondary(type="button" data-dismiss="modal") {
-                'Cancel'
-              }
-              button.btn.btn-primary#navbar-feedback-submit(type="button") {
-                'Submit'
+                div.icon24-outer.mr-2 {
+                  div.icon24-inner {_out.push(fa_arrow_circle_left)}
+                }
+                'Back'
               }
+              if (feedback_draft)
+                button.btn.btn-primary#navbar-feedback-send-message(type="button") {
+                  div.icon24-outer.mr-2#navbar-feedback-icon {
+                    div.icon24-inner {_out.push(fa_envelope)}
+                  }
+                  //div.icon24-outer.mr-2#navbar-feedback-tick(hidden) {
+                  //  div.icon24-inner {_out.push(icon_tick)}
+                  //}
+                  div.icon24-outer.mr-2#navbar-feedback-cross(hidden) {
+                    div.icon24-inner {_out.push(icon_cross)}
+                  }
+                  div.icon24-outer.mr-2#navbar-feedback-spinner(hidden) {
+                    div.icon24-inner {
+                      div.spinner-border.spinner-border-sm(role="status") {}
+                    }
+                  }
+                  'Send message'
+                }
+              else
+                button.btn.btn-primary#navbar-feedback-send-message(type="button" disabled) {
+                  div.icon24-outer.mr-2#navbar-feedback-icon {
+                    div.icon24-inner {_out.push(fa_envelope)}
+                  }
+                  //div.icon24-outer.mr-2#navbar-feedback-tick(hidden) {
+                  //  div.icon24-inner {_out.push(icon_tick)}
+                  //}
+                  div.icon24-outer.mr-2#navbar-feedback-cross(hidden) {
+                    div.icon24-inner {_out.push(icon_cross)}
+                  }
+                  div.icon24-outer.mr-2#navbar-feedback-spinner(hidden) {
+                    div.icon24-inner {
+                      div.spinner-border.spinner-border-sm(role="status") {}
+                    }
+                  }
+                  'Send message'
+                }
             }
           }
         }
@@ -303,6 +365,9 @@ return async (env, head, body, scripts) => {
             }
             div.modal-footer {
               button.btn.btn-outline-secondary(type="button" data-dismiss="modal") {
+                div.icon24-outer.mr-2 {
+                  div.icon24-inner {_out.push(fa_times_circle)}
+                }
                 'Close'
               }
             }
@@ -322,9 +387,15 @@ return async (env, head, body, scripts) => {
         document.addEventListener(
           'DOMContentLoaded',
           () => {
+            let id_navbar_feedback_cross = document.getElementById('navbar-feedback-cross')
+            let id_navbar_feedback_form = document.getElementById('navbar-feedback-form')
+            let id_navbar_feedback_icon = document.getElementById('navbar-feedback-icon')
             let id_navbar_feedback_message = document.getElementById('navbar-feedback-message')
+            let id_navbar_feedback_message1 = document.getElementById('navbar-feedback-message1')
             let id_navbar_feedback_modal = document.getElementById('navbar-feedback-modal')
-            let id_navbar_feedback_submit = document.getElementById('navbar-feedback-submit')
+            let id_navbar_feedback_send_message = document.getElementById('navbar-feedback-send-message')
+            let id_navbar_feedback_spinner = document.getElementById('navbar-feedback-spinner')
+            //let id_navbar_feedback_tick = document.getElementById('navbar-feedback-tick')
             let id_navbar_give_feedback = document.getElementById('navbar-give-feedback')
             let id_navbar_message_modal = document.getElementById('navbar-message-modal')
             let id_navbar_message_modal_message = document.getElementById('navbar-message-modal-message')
@@ -427,7 +498,11 @@ return async (env, head, body, scripts) => {
             id_navbar_give_feedback.addEventListener(
               'click',
               () => {
-                id_navbar_feedback_message.value = ''
+                // hack to move cursor to end of textarea
+                let temp = id_navbar_feedback_message1.value
+                id_navbar_feedback_message1.value = ''
+                id_navbar_feedback_message1.value = temp
+
                 $('#navbar-feedback-modal').modal('show')
                 return false
               }
@@ -435,28 +510,95 @@ return async (env, head, body, scripts) => {
 
             $('#navbar-feedback-modal').on(
               'shown.bs.modal',
-              () => {id_navbar_feedback_message.focus()}
+              () => {id_navbar_feedback_message1.focus()}
             )
 
-            id_navbar_feedback_submit.addEventListener(
+            let feedback_input_semaphore = new BinarySemaphore(false)
+            ;(
+              async () => {
+                while (true) {
+                  await feedback_input_semaphore.acquire()
+                  await new Promise(resolve => setTimeout(resolve, 3000))
+                  feedback_input_semaphore.try_acquire()
+                  await api_call(
+                    '/api/feedback/set_draft.json',
+                    id_navbar_feedback_message1.value.length === 0 ?
+                      null :
+                      {
+                        message: id_navbar_feedback_message1.value.slice(0, 65536)
+                      }
+                  )
+                }
+              }
+            )() // ignore returned promise (start thread)
+
+            let feedback_edited = () => {
+              feedback_input_semaphore.release()
+
+              id_navbar_feedback_send_message.disabled =
+                id_navbar_feedback_message1.value.length === 0
+              id_navbar_feedback_icon.hidden = false
+              //id_navbar_feedback_tick.hidden = true
+              id_navbar_feedback_cross.hidden = true
+              id_navbar_feedback_spinner.hidden = true
+              id_navbar_feedback_message.hidden = true
+            }
+
+            id_navbar_feedback_message1.addEventListener(
+              'input',
+              feedback_edited
+            )
+
+            id_navbar_feedback_send_message.addEventListener(
               'click',
               async () => {
+                id_navbar_feedback_icon.hidden = false
+                //id_navbar_feedback_tick.hidden = true
+                id_navbar_feedback_cross.hidden = true
+                id_navbar_feedback_spinner.hidden = true
+                // the below causes an ugly flicker, so just keep the message
+                //id_navbar_feedback_message.hidden = true
+
+                if (!id_navbar_feedback_form.checkValidity()) {
+                  id_navbar_feedback_form.classList.add('was-validated');
+
+                  id_navbar_feedback_icon.hidden = true
+                  id_navbar_feedback_cross.hidden = false
+                  return
+                }
+                id_navbar_feedback_form.classList.remove('was-validated');
+
+                id_navbar_feedback_icon.hidden = true
+                id_navbar_feedback_spinner.hidden = false
                 try {
                   await api_call(
-                    '/api/feedback.json',
+                    '/api/feedback/send_message.json',
                     location.href,
-                    id_navbar_feedback_message.value.slice(0, 65536)
+                    id_navbar_feedback_message1.value.slice(0, 65536)
                   )
                 }
                 catch (error) {
                   let problem = Problem.from(error)
 
-                  id_navbar_message_modal_message.textContent = problem.detail
-                  $('#navbar-feedback-modal').modal('hide')
-                  $('#navbar-message-modal').modal('show')
+                  id_navbar_feedback_cross.hidden = false
+                  id_navbar_feedback_spinner.hidden = true
+
+                  id_navbar_feedback_message.textContent = problem.detail
+                  //id_navbar_feedback_message.classList.remove('text-success')
+                  id_navbar_feedback_message.classList.add('text-danger')
+                  id_navbar_feedback_message.hidden = false
                   return
                 }
-
+                //id_navbar_feedback_tick.hidden = false
+                //id_navbar_feedback_spinner.hidden = true
+                //id_navbar_feedback_message.textContent = 'We have received your message. We will be in touch as soon as possible.'
+                //id_navbar_feedback_message.classList.add('text-success')
+                //id_navbar_feedback_message.classList.remove('text-danger')
+                //id_navbar_feedback_message.hidden = false
+
+                id_navbar_feedback_icon.hidden = false
+                id_navbar_feedback_spinner.hidden = true
+                id_navbar_feedback_message.hidden = true
                 id_navbar_message_modal_message.textContent = 'Thanks! We have received your feedback.'
                 $('#navbar-feedback-modal').modal('hide')
                 $('#navbar-message-modal').modal('show')
diff --git a/_svg/fa_times-circle.svg b/_svg/fa_times-circle.svg
new file mode 100644 (file)
index 0000000..3ef8c3f
--- /dev/null
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><!-- Font Awesome Free 5.15.4 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) --><path d="M256 8C119 8 8 119 8 256s111 248 248 248 248-111 248-248S393 8 256 8zm121.6 313.1c4.7 4.7 4.7 12.3 0 17L338 377.6c-4.7 4.7-12.3 4.7-17 0L256 312l-65.1 65.6c-4.7 4.7-12.3 4.7-17 0L134.4 338c-4.7-4.7-4.7-12.3 0-17l65.6-65-65.6-65.1c-4.7-4.7-4.7-12.3 0-17l39.6-39.6c4.7-4.7 12.3-4.7 17 0l65 65.7 65.1-65.6c4.7-4.7 12.3-4.7 17 0l39.6 39.6c4.7 4.7 4.7 12.3 0 17L312 256l65.6 65.1z"/></svg>
\ No newline at end of file
diff --git a/_svg/fa_unlock-alt.svg b/_svg/fa_unlock-alt.svg
new file mode 100644 (file)
index 0000000..e58c569
--- /dev/null
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><!-- Font Awesome Free 5.15.4 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) --><path d="M400 256H152V152.9c0-39.6 31.7-72.5 71.3-72.9 40-.4 72.7 32.1 72.7 72v16c0 13.3 10.7 24 24 24h32c13.3 0 24-10.7 24-24v-16C376 68 307.5-.3 223.5 0 139.5.3 72 69.5 72 153.5V256H48c-26.5 0-48 21.5-48 48v160c0 26.5 21.5 48 48 48h352c26.5 0 48-21.5 48-48V304c0-26.5-21.5-48-48-48zM264 408c0 22.1-17.9 40-40 40s-40-17.9-40-40v-48c0-22.1 17.9-40 40-40s40 17.9 40 40v48z"/></svg>
\ No newline at end of file
diff --git a/api/feedback/get_draft.json.jst b/api/feedback/get_draft.json.jst
new file mode 100644 (file)
index 0000000..210982a
--- /dev/null
@@ -0,0 +1,25 @@
+return async env => {
+  let post_request = await _require('/_lib/post_request.jst')
+  let get_session = await _require('/_lib/get_session.jst')
+
+  await post_request(
+    // env
+    env,
+    // handler
+    async () => {
+      let transaction = await env.site.database.Transaction()
+      try {
+        let root = await transaction.get({})
+        let session = await get_session(env, root)
+
+        let feedback_draft = await session.get_json('feedback_draft')
+        if (feedback_draft === undefined || env.now >= feedback_draft.expires)
+          feedback_draft = null
+        return feedback_draft
+      }
+      finally {
+        transaction.rollback()
+      }
+    }
+  )
+}
diff --git a/api/feedback/set_draft.json.jst b/api/feedback/set_draft.json.jst
new file mode 100644 (file)
index 0000000..546a473
--- /dev/null
@@ -0,0 +1,46 @@
+let jst_server = (await import('@ndcode/jst_server')).default
+let XDate = require('xdate')
+
+return async env => {
+  let post_request = await _require('/_lib/post_request.jst')
+  let get_session = await _require('/_lib/get_session.jst')
+
+  await post_request(
+    // env
+    env,
+    // handler
+    async details => {
+      // coerce and/or validate
+      if (details !== null)
+        details = {
+          message: details.message.slice(0, 65536)
+        }
+
+      let transaction = await env.site.database.Transaction()
+      try {
+        let root = await transaction.get({})
+        let session = await get_session(env, root)
+
+        if (details) {
+          let expires = new XDate()
+          expires.addDays(1)
+          session.set_json(
+            'feedback_draft',
+            {
+              message: details.message,
+              expires: expires.getTime()
+            }
+          )
+        }
+        else
+          session.delete('feedback_draft')
+
+        await transaction.commit()
+      }
+      catch (error) {
+        transaction.rollback()
+        throw error
+      }
+    }
+  )
+}
index 5691020..d52613c 100644 (file)
@@ -212,20 +212,18 @@ return async env => {
                 }
                 id_form.classList.remove('was-validated');
 
-                let details = {
-                  given_names: id_given_names.value.slice(0, 256),
-                  family_name: id_family_name.value.slice(0, 256),
-                  company: id_company.value.slice(0, 256),
-                  email: id_email.value.slice(0, 256).toLowerCase(),
-                  message: id_message1.value.slice(0, 65536)
-                }
-
                 id_icon.hidden = true
                 id_spinner.hidden = false
                 try {
                   await api_call(
                     '/api/contact/send_enquiry.json',
-                    details
+                    {
+                      given_names: id_given_names.value.slice(0, 256),
+                      family_name: id_family_name.value.slice(0, 256),
+                      company: id_company.value.slice(0, 256),
+                      email: id_email.value.slice(0, 256).toLowerCase(),
+                      message: id_message1.value.slice(0, 65536)
+                    }
                   )
                 }
                 catch (error) {