From 3d70776191ec731092be659699cb8b68a02a87d0 Mon Sep 17 00:00:00 2001 From: Nick Downing Date: Wed, 26 Jan 2022 22:11:14 +1100 Subject: [PATCH] In /lib/navbar.jst rework feedback to be similar to /contact/index.html.jst way --- _lib/navbar.jst | 186 +++++++++++++++--- _svg/fa_times-circle.svg | 1 + _svg/fa_unlock-alt.svg | 1 + api/feedback/get_draft.json.jst | 25 +++ .../send_message.json.jst} | 0 api/feedback/set_draft.json.jst | 46 +++++ contact/index.html.jst | 16 +- 7 files changed, 244 insertions(+), 31 deletions(-) create mode 100644 _svg/fa_times-circle.svg create mode 100644 _svg/fa_unlock-alt.svg create mode 100644 api/feedback/get_draft.json.jst rename api/{feedback.json.jst => feedback/send_message.json.jst} (100%) create mode 100644 api/feedback/set_draft.json.jst diff --git a/_lib/navbar.jst b/_lib/navbar.jst index f9a7682..da759f9 100644 --- a/_lib/navbar.jst +++ b/_lib/navbar.jst @@ -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 index 0000000..3ef8c3f --- /dev/null +++ b/_svg/fa_times-circle.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/_svg/fa_unlock-alt.svg b/_svg/fa_unlock-alt.svg new file mode 100644 index 0000000..e58c569 --- /dev/null +++ b/_svg/fa_unlock-alt.svg @@ -0,0 +1 @@ + \ 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 index 0000000..210982a --- /dev/null +++ b/api/feedback/get_draft.json.jst @@ -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.json.jst b/api/feedback/send_message.json.jst similarity index 100% rename from api/feedback.json.jst rename to api/feedback/send_message.json.jst diff --git a/api/feedback/set_draft.json.jst b/api/feedback/set_draft.json.jst new file mode 100644 index 0000000..546a473 --- /dev/null +++ b/api/feedback/set_draft.json.jst @@ -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 + } + } + ) +} diff --git a/contact/index.html.jst b/contact/index.html.jst index 5691020..d52613c 100644 --- a/contact/index.html.jst +++ b/contact/index.html.jst @@ -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) { -- 2.34.1