From: Nick Downing Date: Tue, 18 Jan 2022 02:36:50 +0000 (+1100) Subject: Get contact form working again with new style form and API X-Git-Url: https://git.ndcode.org/public/gitweb.cgi?p=ndcode_site.git;a=commitdiff_plain;h=a0a746525f210f9f27684e76ee9824ca2fd89281 Get contact form working again with new style form and API --- diff --git a/_lib/navbar.jst b/_lib/navbar.jst index 7a96065..32cce07 100644 --- a/_lib/navbar.jst +++ b/_lib/navbar.jst @@ -254,7 +254,7 @@ return async (env, head, body, scripts) => { div.col-md-12 { div.form-group { label.form-label(for="feedback-message") {'Message'} - textarea.form-control#feedback-message(placeholder="Please tell us your thoughts" rows="4" required="required") {} + textarea.form-control#feedback-message(placeholder="Please tell us your thoughts" required="required" rows=4 maxlength=65536) {} } } } diff --git a/api/contact/get_draft.json.jst b/api/contact/get_draft.json.jst new file mode 100644 index 0000000..138c55a --- /dev/null +++ b/api/contact/get_draft.json.jst @@ -0,0 +1,51 @@ +let logjson = (await import('@ndcode/logjson')).default +let XDate = require('xdate') + +return async env => { + let post_request = await _require('/_lib/post_request.jst') + let session_cookie = await _require('/_lib/session_cookie.jst') + let Problem = await _require('/_lib/Problem.jst') + + await post_request( + // env + env, + // handler + async () => { + let transaction = await env.site.database.Transaction() + try { + // initialize env.session_key, set cookie in env.response + let session = await session_cookie(env, transaction) + + let contact_draft = await session.get('contact_draft') + let details = + contact_draft !== undefined && + XDate.now() < await logjson.logjson_to_json( + await contact_draft.get('expires') + ) ? { + given_names: await logjson.logjson_to_json( + await contact_draft.get('given_names') + ), + family_name: await logjson.logjson_to_json( + await contact_draft.get('family_name') + ), + company: await logjson.logjson_to_json( + await contact_draft.get('company') + ), + email: await logjson.logjson_to_json( + await contact_draft.get('email') + ), + message: await logjson.logjson_to_json( + await contact_draft.get('message') + ) + } : null + + await transaction.commit() + return details + } + catch (error) { + transaction.rollback() + throw error + } + } + ) +} diff --git a/api/contact/send_enquiry.json.jst b/api/contact/send_enquiry.json.jst new file mode 100644 index 0000000..b26531f --- /dev/null +++ b/api/contact/send_enquiry.json.jst @@ -0,0 +1,68 @@ +let crypto = require('crypto') +let logjson = (await import('@ndcode/logjson')).default +let XDate = require('xdate') + +return async env => { + let globals = await env.site.get_json('/_config/globals.json') + let nodemailer_contact = await env.site.get_nodemailer( + '/_config/nodemailer_contact.json' + ) + let post_request = await _require('/_lib/post_request.jst') + let session_cookie = await _require('/_lib/session_cookie.jst') + let Problem = await _require('/_lib/Problem.jst') + + await post_request( + // env + env, + // handler + async details => { + // coerce and/or validate + details = { + given_names: details.given_names.slice(0, 256), + family_name: details.family_name.slice(0, 256), + company: details.company.slice(0, 256), + email: details.email.slice(0, 256).toLowerCase(), + message: details.message.slice(0, 65536), + } + if ( + details.given_names.length === 0 || + details.email.length === 0 || + details.message.length === 0 + ) + throw new Problem( + 'Bad request', + 'Minimum length check failed', + 400 + ) + + let transaction = await env.site.database.Transaction() + try { + // initialize env.session_key, set cookie in env.response + session_cookie(env, transaction) + await transaction.commit() + } + catch (error) { + transaction.rollback() + throw error + } + + let name = + details.family_name.length ? + `${details.given_names} ${details.family_name}` : + details.given_names + let subject = + details.company.length ? + `Enquiry: ${details.company}` : + 'Enquiry' + await nodemailer_contact.sendMail( + { + from: globals.contact_from, + to: globals.contact_to, + replyTo: `${name} <${details.email}>`, + subject: subject, + text: details.message + } + ) + } + ) +} diff --git a/api/contact/set_draft.json.jst b/api/contact/set_draft.json.jst new file mode 100644 index 0000000..e6facb7 --- /dev/null +++ b/api/contact/set_draft.json.jst @@ -0,0 +1,56 @@ +let XDate = require('xdate') + +return async env => { + let post_request = await _require('/_lib/post_request.jst') + let session_cookie = await _require('/_lib/session_cookie.jst') + let Problem = await _require('/_lib/Problem.jst') + + await post_request( + // env + env, + // handler + async details => { + // coerce and/or validate + if (details !== null) + details = { + given_names: details.given_names.slice(0, 256), + family_name: details.family_name.slice(0, 256), + company: details.company.slice(0, 256), + email: details.email.slice(0, 256).toLowerCase(), + message: details.message.slice(0, 65536), + } + + let transaction = await env.site.database.Transaction() + try { + // initialize env.session_key, set cookie in env.response + let session = await session_cookie(env, transaction) + + if (details) { + let expires = new XDate() + expires.addDays(1) + session.set( + 'contact_draft', + transaction.json_to_logjson( + { + given_names: details.given_names, + family_name: details.family_name, + company: details.company, + email: details.email, + message: details.message, + expires: expires.getTime() + } + ) + ) + } + else + session.delete('contact_draft') + + await transaction.commit() + } + catch (error) { + transaction.rollback() + throw error + } + } + ) +} diff --git a/contact/index.html.jst b/contact/index.html.jst index d74ae78..bb28ef7 100644 --- a/contact/index.html.jst +++ b/contact/index.html.jst @@ -1,140 +1,312 @@ -let querystring = require('querystring') -let stream_buffers = require('stream-buffers') +let logjson = (await import('@ndcode/logjson')).default let XDate = require('xdate') return async env => { let breadcrumbs = await _require('/_lib/breadcrumbs.jst') - let globals = await env.site.get_json('/_config/globals.json') + 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 navbar = await _require('/_lib/navbar.jst') + let session_cookie = await _require('/_lib/session_cookie.jst') + + // preload draft details if any + let transaction = await env.site.database.Transaction(), draft_details + try { + // initialize env.session_key, set cookie in env.response + let session = await session_cookie(env, transaction) + + let contact_draft = await session.get('contact_draft') + draft_details = + contact_draft !== undefined && + XDate.now() < await logjson.logjson_to_json( + await contact_draft.get('expires') + ) ? { + email: await logjson.logjson_to_json( + await contact_draft.get('email') + ), + given_names: await logjson.logjson_to_json( + await contact_draft.get('given_names') + ), + family_name: await logjson.logjson_to_json( + await contact_draft.get('family_name') + ), + company: await logjson.logjson_to_json( + await contact_draft.get('company') + ), + email: await logjson.logjson_to_json( + await contact_draft.get('email') + ), + message: await logjson.logjson_to_json( + await contact_draft.get('message') + ) + } : null + + await transaction.commit() + } + catch (error) { + transaction.rollback() + throw error + } + console.log('draft_details', JSON.stringify(draft_details)) await navbar( env, + // head async _out => {}, + // body async _out => { await breadcrumbs(env, _out) - if (env.request.method == 'POST') { - let write_stream = new stream_buffers.WritableStreamBuffer() - let data = new Promise( - (resolve, reject) => { - write_stream. - on('finish', () => {resolve(write_stream.getContents())}). - on('error', () => {reject()}) - } - ) - env.request.pipe(write_stream) - let query = querystring.parse((await data).toString()) - console.log('received contact form:', query.email) - - // save the form contents in a dated logfile, so that we can - // recover manually if the email doesn't send for some reason - date = new XDate() - query.date = date.toUTCString() - - let transaction = await env.site.database.Transaction() - ;( - await ( - await ( - await ( - await transaction.get({}) - ).get('logs', {}) - ).get(date.toUTCString('yyyyMMdd'), {}) - ).get('contact', []) - ).push(transaction.json_to_logjson(query)) - transaction.commit() - - // send email (asynchronously) - let emailjs_contact = await env.site.get_emailjs( - '/_config/email_contact.json' - ) - emailjs_contact.send( - { - from: globals.contact_from, - 'reply-to': - `${query.first_name} ${query.last_name} <${query.email}>`, - to: globals.contact_to, - subject: - Object.prototype.hasOwnProperty.call(query, 'company') ? - 'Enquiry: ' + query.company : - 'Enquiry', - text: query.message - }, - (err, message) => { - if (err) - console.error(err.stack || err.message) - else - console.log('sent contact email:', query.email) - } - ) // ignore returned promise + p {'Do you require more information, or assistance with integrating the projects on this site? We’d love to hear from you.'} - p {'Thanks! We\'ll be in touch as soon as we can.'} - } - else { - p {'Do you require more information or consulting assistance with integrating the projects on this site? We\'d love to hear from you.'} - - form#contact-form(method="post" action="index.html" role="form") { - div.row { - div.col-md-6 { - div.form-group { - label(for="contact_form_first_name") {'First name *'} - input.form-control#contact_form_first_name(type="text" name="first_name" placeholder="Please enter your first name" required="required" data-error="First name is required.") {} - div.help-block.with-errors {} - } + 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)} } - div.col-md-6 { - div.form-group { - label(for="contact_form_last_name") {'Last name *'} - input.form-control#contact_form_last_name(type="text" name="last_name" placeholder="Please enter your last name" required="required" data-error="Last name is required.") {} - div.help-block.with-errors {} - } + 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") { + 'Enquiry details' } } - div.row { - div.col-md-6 { - div.form-group { - label(for="contact_form_company") {'Company'} - input.form-control#contact_form_company(type="text" name="company" placeholder="Please enter your company") {} - div.help-block.with-errors {} + div#step-1-collapse.collapse.show(role="tabpanel" aria-labelledby="step-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 : '' placeholder="Your given names" required="required" 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=draft_details ? draft_details.family_name : '' placeholder="Your family name" maxlength=256) {} + } + } } - } - div.col-md-6 { - div.form-group { - label(for="contact_form_email") {'Email *'} - input.form-control#contact_form_email(type="email" name="email" placeholder="Please enter your email" required="required" data-error="Valid email is required.") {} - div.help-block.with-errors {} + div.row { + div.col-md-6 { + div.form-group { + label.form-label(for="company") {'Company'} + input.form-control#company(type="company" value=draft_details ? draft_details.company : '' placeholder="Your company" maxlength=256) {} + } + } + div.col-md-6 { + div.form-group { + label.form-label(for="email") {'Email *'} + input.form-control#email(type="email" value=draft_details ? draft_details.email : '' placeholder="Your email address" required="required" maxlength=256) {} + } + } + } + div.row { + div.col-md-12 { + div.form-group { + label.form-label(for="message") {'Message *'} + textarea.form-control#message(placeholder="Your message" required="required" rows=6 maxlength=65536) { + if (draft_details) + `${draft_details.message}` + } + } + } } + + button.btn.btn-success#step-1-continue(type="button") {'Continue'} + p.'mt-3'.mb-0 {'* These fields are required.'} } } - div.row { - div.col-md-12 { - div.form-group { - label(for="contact_form_message") {'Message *'} - textarea.form-control#contact_form_message(name="message" placeholder="Please explain your application" rows="4" required="required" data-error="Please, leave us a message.") {} - div.help-block.with-errors {} + } + 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...'} + } } } - } - p {} // fix this later - div.row { - div.col-md-12 { - button.btn.btn-success.btn-send(type="submit") {'Send message'} + a.h5.collapsed(data-toggle="collapse" data-parent="#accordion" href="#step-2-collapse" aria-expanded="false" aria-controls="step-2-collapse") { + 'Send enquiry' } } - p {} // fix this later - div.row { - div.col-md-12 { - p.text-muted { - strong {'*'} - 'These fields are required.' - //'Contact form template by ' - //a(href="https://bootstrapious.com/p/how-to-build-a-working-bootstrap-contact-form" target="_blank") {'Bootstrapious'} - //'.' - } + div#step-2-collapse.collapse(role="tabpanel" aria-labelledby="step-2-heading" data-parent="#accordion") { + div.card-body { + p#step-2-message {'Please enter enquiry details first.'} + + button.btn.btn-outline-secondary#step-2-back(type="button") {'Back'} + button.btn.btn-outline-secondary.ml-3#step-2-resend-enquiry(type="button") {'Re-send enquiry'} } } } } }, - async _out => {} + // scripts + async _out => { + //script(src="/js/api_call.js") {} + + script { + //let api_contact_get_draft = async (...arguments) => api_call( + // '/api/contact/get_draft.json', + // ...arguments + //) + let api_contact_set_draft = async (...arguments) => api_call( + '/api/contact/set_draft.json', + ...arguments + ) + let api_contact_send_enquiry = async (...arguments) => api_call( + '/api/contact/send_enquiry.json', + ...arguments + ) + + let draft_timeout_running = false + let draft_timeout_handler = async () => { + draft_timeout_running = false + await api_contact_set_draft( + { + given_names: document.getElementById('given-names').value.slice(0, 256), + family_name: document.getElementById('family-name').value.slice(0, 256), + company: document.getElementById('company').value.slice(0, 256), + email: document.getElementById('email').value.slice(0, 256).toLowerCase(), + message: document.getElementById('message').value.slice(0, 65536) + } + ) + //console.log('draft', await api_contact_get_draft()) + } + let draft_change_handler = () => { + if (!draft_timeout_running) { + draft_timeout_running = true + setTimeout(draft_timeout_handler, 5000) + } + } + + let details + let step_1 = async () => { + if ( + !document.getElementById('given-names').reportValidity() || + !document.getElementById('family-name').reportValidity() || + !document.getElementById('company').reportValidity() || + !document.getElementById('email').reportValidity() || + !document.getElementById('message').reportValidity() + ) { + $('#step-1-tick').hide() + $('#step-1-cross').show() + //$('#step-1-spinner').hide() + return false + } + $('#step-1-tick').show() + $('#step-1-cross').hide() + //$('#step-1-spinner').hide() + + details = { + given_names: document.getElementById('given-names').value.slice(0, 256), + family_name: document.getElementById('family-name').value.slice(0, 256), + company: document.getElementById('company').value.slice(0, 256), + email: document.getElementById('email').value.slice(0, 256).toLowerCase(), + message: document.getElementById('message').value.slice(0, 65536) + } + return true + } + + let step_2 = async () => { + $('#step-2-tick').hide() + $('#step-2-cross').hide() + $('#step-2-spinner').show() + document.getElementById('step-2').scrollIntoView() + + try { + await api_contact_send_enquiry(details) + } + catch (error) { + let problem = + error instanceof Problem ? + error : + new Problem( + // title + 'Bad request', + // detail + (error.stack || error.message).toString() + // status + 400 + ) + + $('#step-2-tick').hide() + $('#step-2-cross').show() + $('#step-2-spinner').hide() + + document.getElementById('step-2-message').textContent = problem.detail + $('#step-2-collapse').collapse('show') + return false + } + $('#step-2-tick').show() + $('#step-2-cross').hide() + $('#step-2-spinner').hide() + + document.getElementById('step-2-message').textContent = 'We have received your enquiry. We will be in touch as soon as possible.' + return true + } + + document.addEventListener( + 'DOMContentLoaded', + () => { + document.getElementById('given-names').addEventListener( + 'change', + draft_change_handler + ) + document.getElementById('family-name').addEventListener( + 'change', + draft_change_handler + ) + document.getElementById('company').addEventListener( + 'change', + draft_change_handler + ) + document.getElementById('email').addEventListener( + 'change', + draft_change_handler + ) + document.getElementById('message').addEventListener( + 'change', + draft_change_handler + ) + + document.getElementById('step-1-continue').addEventListener( + 'click', + async () => { + if (await step_1() && await step_2()) + $('#step-2-collapse').collapse('show') + } + ) + + document.getElementById('step-2-back').addEventListener( + 'click', + () => {$('#step-1-collapse').collapse('show')} + ) + + document.getElementById('step-2-resend-enquiry').addEventListener( + 'click', + async () => { + if (await step_2()) + $('#step-2-collapse').collapse('show') + } + ) + } + ) + } + } ) }