From 79d5947494a247f5d5115d486c051cf956191997 Mon Sep 17 00:00:00 2001 From: Rob Rusher Date: Thu, 25 Jan 2024 09:57:49 -0700 Subject: [PATCH 01/33] base form load --- blocks/contact-form/contact-form.css | 0 blocks/contact-form/contact-form.js | 16 ++++++ blocks/contact-form/forms/contact-us.html | 65 +++++++++++++++++++++++ icons/error.svg | 1 + icons/success.svg | 1 + 5 files changed, 83 insertions(+) create mode 100644 blocks/contact-form/contact-form.css create mode 100644 blocks/contact-form/contact-form.js create mode 100644 blocks/contact-form/forms/contact-us.html create mode 100644 icons/error.svg create mode 100644 icons/success.svg diff --git a/blocks/contact-form/contact-form.css b/blocks/contact-form/contact-form.css new file mode 100644 index 00000000..e69de29b diff --git a/blocks/contact-form/contact-form.js b/blocks/contact-form/contact-form.js new file mode 100644 index 00000000..e140a6c0 --- /dev/null +++ b/blocks/contact-form/contact-form.js @@ -0,0 +1,16 @@ +export default async function decorate(block) { + const displayValue = block.style.display; + block.style.display = 'none'; + + const formName = block.firstElementChild.innerText.trim(); + const thankYou = block.firstElementChild.nextElementSibling; + const data = await fetch(`${window.hlx.codeBasePath}/blocks/contact-form/forms/${formName}.html`); + if (!data.ok) { + /* eslint-disable-next-line no-console */ + console.error(`failed to load form: ${formName}`); + block.innerHTML = ''; + return; + } + + block.innerHTML = await data.text(); +} \ No newline at end of file diff --git a/blocks/contact-form/forms/contact-us.html b/blocks/contact-form/forms/contact-us.html new file mode 100644 index 00000000..3857b836 --- /dev/null +++ b/blocks/contact-form/forms/contact-us.html @@ -0,0 +1,65 @@ +
+
+ + +
+
+ + +
+
+
+
+
+
+
+
+
are you currently working with an agent?
+
+
+
+
+
+
+
+
+
+
+
+ +
\ No newline at end of file diff --git a/icons/error.svg b/icons/error.svg new file mode 100644 index 00000000..1918d7eb --- /dev/null +++ b/icons/error.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/icons/success.svg b/icons/success.svg new file mode 100644 index 00000000..12c3cfe8 --- /dev/null +++ b/icons/success.svg @@ -0,0 +1 @@ + \ No newline at end of file From fd3035c66208f1f05db27901771b0f3041cc8a51 Mon Sep 17 00:00:00 2001 From: Rob Rusher Date: Fri, 26 Jan 2024 17:56:18 -0700 Subject: [PATCH 02/33] form styling --- blocks/contact-form/contact-form.css | 167 ++++++++++++++++++++++ blocks/contact-form/contact-form.js | 18 ++- blocks/contact-form/forms/contact-us.html | 131 +++++++++-------- icons/error.svg | 1 - icons/success.svg | 1 - 5 files changed, 252 insertions(+), 66 deletions(-) delete mode 100644 icons/error.svg delete mode 100644 icons/success.svg diff --git a/blocks/contact-form/contact-form.css b/blocks/contact-form/contact-form.css index e69de29b..bdd0d0e1 100644 --- a/blocks/contact-form/contact-form.css +++ b/blocks/contact-form/contact-form.css @@ -0,0 +1,167 @@ +.contact-form.block .contact-form form .message { + display: none; + padding: 10px 4px; + margin-bottom: 1em; + flex-direction: row; + align-items: center; + border: 1px solid; + column-gap: 4px; +} + +.contact-form.block .contact-form form .message.error { + display: flex; + color: var(--error); + border-color: var(--error); +} + +.contact-form.block .contact-form form .message.success { + display: flex; + color: var(--success); + border-color: var(--success); +} + +.contact-form.block .contact-form form .message .icon { + display: none; + align-self: flex-start; + width: 20px; + height: 20px; +} + +.contact-form.block .contact-form form .message.error .icon.error { + display: block; +} + +.contact-form.block .contact-form form .message.success .icon.success { + display: block; +} + +.contact-form.block .contact-form form .message .details { + display: flex; + flex-direction: column; +} + +.contact-form.block .contact-form form .message span { + font-family: var(--font-family-proxima); + font-size: var(--body-font-size-xs); + line-height: var(--line-height-s); +} + +.contact-form.block .contact-form form .inputs .name, +.contact-form.block .contact-form form .inputs .contact-info { + display: block; +} + +.contact-form.block .contact-form form .inputs input[type="text"], +.contact-form.block .contact-form form .inputs input[type="email"], +.contact-form.block .contact-form form .inputs textarea { + height: 50px; + width: 100%; + padding-left: 15px; + margin-bottom: 1em; + font-family: var(--font-family-proxima); + font-size: var(--body-font-size-s); + line-height: 50px; + color: var(--body-color); + border: 1px solid var(--dark-grey); +} + +.contact-form.block .contact-form form .inputs textarea { + width: 100%; + height: 110px; +} + +.contact-form.block .contact-form form .agent > div:first-child { + margin-bottom: .5rem; +} + +.contact-form.block .contact-form form .agent .label-check { + display: inline-flex; + gap: 10px; +} +.contact-form.block .contact-form form .agent label { + font-weight: 400; + font-size: 14px; + color: var(--body-color); + letter-spacing: .5px; + line-height: 1; + display: flex; + align-items: center; + justify-content: flex-start; + margin-bottom: 15px; + cursor: pointer; +} + +.contact-form.block .contact-form form .agent input[type="checkbox"] { + height: 24px; + min-width: 24px; + width: 24px; + border-radius: 50%; + border: 1px solid var(--body-color); + opacity: 0; + position: absolute; + z-index: 1; + cursor: pointer; +} + +.contact-form.block .contact-form form .agent .checkbox { + cursor: pointer; + height: 24px; + width: 24px; + border: 1px solid #aaa; + margin-right: 8px; + position: relative; +} + +.contact-form.block .contact-form form .agent .checkbox svg { + display: none; + height: 10px; + width: 12px; + top: -webkit-calc(50% - 5px); + top: calc(50% - 5px); + position: absolute; + left: 5px; +} + +.contact-form.block .contact-form form .agent input[type="checkbox"]:checked+.checkbox svg { + display: block; +} + +.contact-form.block .contact-form form .inputs input[type="text"].error, +.contact-form.block .contact-form form .inputs input[type="email"].error { + color: var(--error); + background-color: var(--error-highlight); + border: 2px solid var(--error); +} + +.contact-form.block .contact-form .g-recaptcha { + padding: 3rem 0; +} + +.contact-form.block .contact-form form .cta { + padding-bottom: 2rem; +} + +.contact-form.block .contact-form form .cta .button-container a.button.primary { + background-color: var(--primary-color); + color: var(--white); +} + +.contact-form.block .contact-form form .cta .button-container a.button.primary:hover { + background-color: var(--white); + color: var(--body-color); + border-color: var(--grey); +} + +@media (min-width: 600px) { + .contact-form.block .contact-form form .inputs .name, + .contact-form.block .contact-form form .inputs .contact-info { + display: flex; + gap: 10px; + + } + + .contact-form.block .contact-form form .inputs input[type="text"], + .contact-form.block .contact-form form .inputs input[type="email"] { + width: 50%; + } +} diff --git a/blocks/contact-form/contact-form.js b/blocks/contact-form/contact-form.js index e140a6c0..b394d83d 100644 --- a/blocks/contact-form/contact-form.js +++ b/blocks/contact-form/contact-form.js @@ -1,4 +1,5 @@ -export default async function decorate(block) { +// eslint-disable no-console +const addForm = async (block) => { const displayValue = block.style.display; block.style.display = 'none'; @@ -13,4 +14,17 @@ export default async function decorate(block) { } block.innerHTML = await data.text(); -} \ No newline at end of file + block.style.display = displayValue; +}; + +export default async function decorate(block) { + const observer = new IntersectionObserver((entries) => { + if (entries.some((e) => e.isIntersecting)) { + observer.disconnect(); + addForm(block); + } + }, { + rootMargin: '300px', + }); + observer.observe(block); +} diff --git a/blocks/contact-form/forms/contact-us.html b/blocks/contact-form/forms/contact-us.html index 3857b836..5418d5ff 100644 --- a/blocks/contact-form/forms/contact-us.html +++ b/blocks/contact-form/forms/contact-us.html @@ -1,65 +1,72 @@ -
-
- - -
-
- - -
-
-
-
-
-
-
-
-
are you currently working with an agent?
-
-
-
+
+ +
+ + + + + + +
+ +
-
-
-
-
-
-
-
- - \ No newline at end of file + +
+ +
+
+
+ Submit +
+
+ + \ No newline at end of file diff --git a/icons/error.svg b/icons/error.svg deleted file mode 100644 index 1918d7eb..00000000 --- a/icons/error.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/icons/success.svg b/icons/success.svg deleted file mode 100644 index 12c3cfe8..00000000 --- a/icons/success.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file From 096b81acdf6fbc79d58cc257a664693db6b5902e Mon Sep 17 00:00:00 2001 From: Rob Rusher Date: Tue, 30 Jan 2024 11:26:11 -0700 Subject: [PATCH 03/33] update form-header style --- styles/styles.css | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/styles/styles.css b/styles/styles.css index 55b923c1..d24033be 100644 --- a/styles/styles.css +++ b/styles/styles.css @@ -294,6 +294,17 @@ main .section.two-columns > div { flex: 0 1 40%; } +main .section.form-header h1 { + font-size: 60px; + font-family: var(--font-family-georgia); +} + +main .section.form-header p { + font-size: var(--heading-font-size-s); + margin: 0 auto 60px; + max-width: 420px; +} + /* Center rules for a section, but don't apply it to blocks! */ /* stylelint-disable-next-line no-descending-specificity */ main div[data-align="center"] h1, From 0f170402e33c304134e2ebe017b80941850ca7c5 Mon Sep 17 00:00:00 2001 From: Rob Rusher Date: Fri, 2 Feb 2024 18:03:04 -0700 Subject: [PATCH 04/33] add validation --- blocks/contact-form/contact-form.css | 18 +- blocks/contact-form/contact-form.js | 221 ++++++++++++++++++ .../contact-form/forms/contact-property.html | 78 +++++++ blocks/contact-form/forms/contact-us.html | 14 +- blocks/side-modal/side-modal.css | 33 +++ blocks/side-modal/side-modal.js | 31 +++ scripts/scripts.js | 16 ++ scripts/util.js | 38 ++- styles/lazy-styles.css | 3 + 9 files changed, 438 insertions(+), 14 deletions(-) create mode 100644 blocks/contact-form/forms/contact-property.html create mode 100644 blocks/side-modal/side-modal.css create mode 100644 blocks/side-modal/side-modal.js diff --git a/blocks/contact-form/contact-form.css b/blocks/contact-form/contact-form.css index bdd0d0e1..df32f787 100644 --- a/blocks/contact-form/contact-form.css +++ b/blocks/contact-form/contact-form.css @@ -1,3 +1,9 @@ +.contact-form.block form, +.contact-form.block .company-email, +.contact-form.block .company-phone { + font-family: var(--font-family-proxima); +} + .contact-form.block .contact-form form .message { display: none; padding: 10px 4px; @@ -41,7 +47,6 @@ } .contact-form.block .contact-form form .message span { - font-family: var(--font-family-proxima); font-size: var(--body-font-size-xs); line-height: var(--line-height-s); } @@ -58,9 +63,8 @@ width: 100%; padding-left: 15px; margin-bottom: 1em; - font-family: var(--font-family-proxima); font-size: var(--body-font-size-s); - line-height: 50px; + line-height: var(--line-height-xs); color: var(--body-color); border: 1px solid var(--dark-grey); } @@ -68,24 +72,24 @@ .contact-form.block .contact-form form .inputs textarea { width: 100%; height: 110px; + padding: 15px; } .contact-form.block .contact-form form .agent > div:first-child { margin-bottom: .5rem; } -.contact-form.block .contact-form form .agent .label-check { +.contact-form.block .contact-form form .agent .agent-check { display: inline-flex; gap: 10px; } -.contact-form.block .contact-form form .agent label { +.contact-form.block .contact-form form .agent div { font-weight: 400; font-size: 14px; color: var(--body-color); letter-spacing: .5px; line-height: 1; display: flex; - align-items: center; justify-content: flex-start; margin-bottom: 15px; cursor: pointer; @@ -144,6 +148,8 @@ .contact-form.block .contact-form form .cta .button-container a.button.primary { background-color: var(--primary-color); color: var(--white); + text-transform: uppercase; + margin-right: 12px; } .contact-form.block .contact-form form .cta .button-container a.button.primary:hover { diff --git a/blocks/contact-form/contact-form.js b/blocks/contact-form/contact-form.js index b394d83d..4857bb68 100644 --- a/blocks/contact-form/contact-form.js +++ b/blocks/contact-form/contact-form.js @@ -1,3 +1,69 @@ +import { hideSideModal, i18nLookup } from '../../scripts/util.js'; + +const LOGIN_ERROR = 'There was a problem processing your request.'; +const i18n = await i18nLookup(); +const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; +const phoneRegex = /^\d{10}$/; + +function displayError(errors) { + const message = document.body.querySelector('.contact-form.block').querySelector('.message'); + const details = message.querySelector('.details'); + const spans = []; + [LOGIN_ERROR, ...errors].forEach((m) => { + const span = document.createElement('span'); + span.textContent = i18n(m); + spans.push(span); + }); + details.replaceChildren(...spans); + message.classList.add('error'); +} + +function isValid(form) { + const errors = []; + const firstName = form.querySelector('input[name="first_name"]'); + if (!firstName.value || firstName.value.trim().length === 0) { + errors.push(i18n('First name is required.')); + firstName.classList.add('error'); + } + + const lastName = form.querySelector('input[name="last_name"]'); + if (!lastName.value || lastName.value.trim().length === 0) { + errors.push(i18n('Last name is required.')); + lastName.classList.add('error'); + } + + const email = form.querySelector('input[name="email"]'); + if (!email.value || email.value.trim().length === 0) { + errors.push(i18n('Email address is required.')); + email.classList.add('error'); + } + if (!emailRegex.test(email)) { + errors.push(i18n('Please enter an email address in the format: email@domain.com.')); + email.classList.add('error'); + } + + const phone = form.querySelector('input[name="phone"]'); + if (!phone.value || phone.value.trim().length === 0) { + errors.push(i18n('Email address is required.')); + phone.classList.add('error'); + } + if (!phoneRegex.test(phone)) { + errors.push(i18n('Please enter a 10 digit phone number.')); + phone.classList.add('error'); + } + + if (errors.length > 0) { + displayError(errors); + return false; + } + return true; +} + +function submitContactForm(form) { + console.log('submitted'); + return isValid(form); +} + // eslint-disable no-console const addForm = async (block) => { const displayValue = block.style.display; @@ -14,6 +80,161 @@ const addForm = async (block) => { } block.innerHTML = await data.text(); + + const submitBtn = block.querySelector('.cta a.submit'); + if (submitBtn) { + submitBtn.addEventListener('click', (e) => { + e.preventDefault(); + e.stopPropagation(); + submitContactForm(block.querySelector('form')); + }); + } + + const cancelBtn = block.querySelector('.cta a.cancel'); + if (cancelBtn) { + cancelBtn.addEventListener('click', (e) => { + e.preventDefault(); + e.stopPropagation(); + hideSideModal(); + }); + } + + [...block.querySelectorAll('input[name="first_name"], input[name="last_name"]')] + .forEach((el) => { + el.addEventListener('blur', (e) => { + const { value } = e.currentTarget; + if (!value || value.trim().length === 0) { + e.currentTarget.classList.add('error'); + } else { + e.currentTarget.classList.remove('error'); + } + }); + }); + + [...block.querySelectorAll('input[name="phone"]')] + .forEach((el) => { + el.addEventListener('blur', (e) => { + const { value } = e.currentTarget; + if (!value || value.trim().length === 0 || !phoneRegex.test(value)) { + e.currentTarget.classList.add('error'); + } else { + e.currentTarget.classList.remove('error'); + } + }); + }); + + [...block.querySelectorAll('input[name="email"]')] + .forEach((el) => { + el.addEventListener('blur', (e) => { + const { value } = e.currentTarget; + if (!value || value.trim().length === 0 || !emailRegex.test(value)) { + e.currentTarget.classList.add('error'); + } else { + e.currentTarget.classList.remove('error'); + } + }); + }); + + if (thankYou) { + const form = block.querySelector('#contactForm'); + const oldSubmit = form.onsubmit; + thankYou.classList.add('form-thank-you'); + form.onsubmit = function handleSubmit() { + if (oldSubmit.call(this)) { + const body = new FormData(this); + const { action, method } = this; + fetch(action, { method, body, redirect: 'manual' }).then((resp) => { + /* eslint-disable-next-line no-console */ + if (!resp.ok) console.error(`Form submission failed: ${resp.status} / ${resp.statusText}`); + const firstContent = thankYou.firstElementChild; + if (firstContent.tagName === 'A') { + // redirect to thank you page + window.location.href = firstContent.href; + } else { + // show thank you content + const btn = thankYou.querySelector('a'); + const sideModal = document.querySelector('.side-modal-form'); + if (btn && sideModal) { + btn.setAttribute('href', '#'); + btn.addEventListener('click', (e) => { + e.preventDefault(); + hideSideModal(); + }); + sideModal?.replaceChildren(thankYou); + } else { + block.replaceChildren(thankYou); + } + } + }); + } + return false; + }; + } + + // If the form has it's own styles, add them. + const styles = block.querySelectorAll('style'); + styles.forEach((styleSheet) => { + document.head.appendChild(styleSheet); + }); + + // If the form has it's own scripts, load them one by one to maintain execution order. + // eslint-disable-next-line no-restricted-syntax + for (const script of [...block.querySelectorAll('script')]) { + let waitForLoad = Promise.resolve(); + // The script element added by innerHTML is NOT executed. + // The workaround is to create the new script tag, copy attibutes and content. + const newScript = document.createElement('script'); + newScript.setAttribute('type', 'text/javascript'); + // Copy script attributes to the new element. + script.getAttributeNames().forEach((attrName) => { + const attrValue = script.getAttribute(attrName); + newScript.setAttribute(attrName, attrValue); + + if (attrName === 'src') { + waitForLoad = new Promise((resolve) => { + newScript.addEventListener('load', resolve); + }); + } + }); + newScript.innerHTML = script.innerHTML; + script.remove(); + document.body.append(newScript); + + // eslint-disable-next-line no-await-in-loop + await waitForLoad; + } + + const inputs = block.querySelectorAll('input'); + inputs.forEach((formEl) => { + formEl.placeholder = i18n(formEl.placeholder); + formEl.ariaLabel = i18n(formEl.ariaLabel); + }); + + const taEl = block.querySelector('textarea'); + if (taEl && taEl.placeholder) taEl.placeholder = i18n(taEl.placeholder); + + // Get all checkboxes with class 'checkbox' + const checkboxes = document.querySelectorAll('input[type="checkbox"]'); + + // Define a function declaration to handle the change event + function handleChange() { + // Store the clicked checkbox in a variable + const clickedCheckbox = this; + + // Uncheck all checkboxes that are not the clicked checkbox + checkboxes.forEach((cb) => { + if (cb !== clickedCheckbox) { + cb.checked = false; + } + }); + } + + // Add the change event listener to each checkbox using the function declaration + checkboxes.forEach((checkbox) => { + checkbox.addEventListener('change', handleChange); + checkbox.nextElementSibling.addEventListener('change', handleChange); + }); + block.style.display = displayValue; }; diff --git a/blocks/contact-form/forms/contact-property.html b/blocks/contact-form/forms/contact-property.html new file mode 100644 index 00000000..aed6c0e5 --- /dev/null +++ b/blocks/contact-form/forms/contact-property.html @@ -0,0 +1,78 @@ +
+

Contact Us

+
+
+
Direct:
+
+
+ + + + + + +
+ +
+
+
+
+ + +
+
+ + +
+ +
+
+
Are you currently working with an agent?
+
+
+ +
+ + + +
+ yes +
+
+ +
+ + + +
+ no +
+
+
+
+
+
+ + +
+ +
+
+
+ Send + Cancel +
+
+
+
\ No newline at end of file diff --git a/blocks/contact-form/forms/contact-us.html b/blocks/contact-form/forms/contact-us.html index 5418d5ff..941dbf99 100644 --- a/blocks/contact-form/forms/contact-us.html +++ b/blocks/contact-form/forms/contact-us.html @@ -1,5 +1,5 @@
-
+
@@ -30,8 +30,8 @@
Are you currently working with an agent?
-
-
@@ -65,7 +65,7 @@
diff --git a/blocks/side-modal/side-modal.css b/blocks/side-modal/side-modal.css new file mode 100644 index 00000000..557d3d8e --- /dev/null +++ b/blocks/side-modal/side-modal.css @@ -0,0 +1,33 @@ +aside.side-modal { + overflow: hidden scroll; + position: fixed; + top: 200px; + bottom: 0; + width: 100vw; + right: -100vw; + transition: right .2s cubic-bezier(.4,0,.2,1) .1s; + background-color: white; + z-index: 1090; +} + +aside.side-modal[aria-expanded=true] { + right: 0; +} + +aside.side-modal > div { + width: 100%; + padding: 53px 15px 15px; +} + +@media (min-width: 768px) { + aside.side-modal { + width: 50vw; + right: -50vw; + } +} + +@media (min-width: 992px) { + aside.side-modal > div { + padding: 53px 16% 16%; + } +} diff --git a/blocks/side-modal/side-modal.js b/blocks/side-modal/side-modal.js new file mode 100644 index 00000000..850807a5 --- /dev/null +++ b/blocks/side-modal/side-modal.js @@ -0,0 +1,31 @@ +/* eslint-disable import/prefer-default-export */ +import { + decorateSections, decorateBlocks, loadBlocks, decorateButtons, decorateIcons, loadCSS, +} from '../../scripts/aem.js'; + +export async function showSideModal(a) { + const { href } = a; + const module$ = import(`${window.hlx.codeBasePath}/scripts/util.js`); + await loadCSS(`${window.hlx.codeBasePath}/blocks/side-modal/side-modal.css`); + const content = await fetch(`${href}.plain.html`); + + async function decorateSideModal(container) { + decorateButtons(container); + decorateIcons(container); + decorateSections(container); + decorateBlocks(container); + + container.classList.add('side-modal-form'); + const [theForm] = container.children; + theForm.classList.add('form'); + + await loadBlocks(container); + } + + if (content.ok) { + const html = await content.text(); + const fragment = document.createRange().createContextualFragment(html); + const [module] = await Promise.all([module$]); + module.showSideModal(fragment.children, decorateSideModal); + } +} diff --git a/scripts/scripts.js b/scripts/scripts.js index 3f1027ca..d45af915 100644 --- a/scripts/scripts.js +++ b/scripts/scripts.js @@ -152,6 +152,21 @@ function decorateVideoLinks(main) { }); } +function decorateFormLinks(main) { + async function openSideModal(event) { + event.preventDefault(); + const module = await import(`${window.hlx.codeBasePath}/blocks/side-modal/side-modal.js`); + if (module.showSideModal) { + await module.showSideModal(event.target); + } + } + main.querySelectorAll('a[href*="form"]').forEach((a) => { + if (a.href.endsWith('-form')) { + a.addEventListener('click', openSideModal); + } + }); +} + function decorateImages(main) { main.querySelectorAll('.section .default-content-wrapper picture').forEach((picture) => { const img = picture.querySelector('img'); @@ -262,6 +277,7 @@ export function decorateMain(main) { decorateSections(main); decorateBlocks(main); decorateVideoLinks(main); + decorateFormLinks(main); decorateImages(main); } diff --git a/scripts/util.js b/scripts/util.js index f89c8c4b..a0196e1c 100644 --- a/scripts/util.js +++ b/scripts/util.js @@ -1,4 +1,4 @@ -import { fetchPlaceholders } from './aem.js'; +import { fetchPlaceholders, loadCSS } from './aem.js'; /** * Creates the standard Spinner Div. @@ -49,6 +49,42 @@ export function showModal(content) { document.body.append(modal); } +let sideModal; +let focusElement; + +export function hideSideModal() { + if (!sideModal) return; + sideModal.ariaExpanded = false; + document.body.classList.remove('disable-scroll'); + if (focusElement) focusElement.focus(); +} + +export async function showSideModal(content, decorateContent) { + if (!sideModal) { + const fragment = document.createRange().createContextualFragment(` +
+ +
+ `); + sideModal = fragment.querySelector('.side-modal'); + document.body.append(...fragment.children); + } + const container = sideModal.querySelector('div'); + container.replaceChildren(...content); + + if (decorateContent) await decorateContent(container); + + // required delay for animation to work + setTimeout(() => { + document.body.classList.add('disable-scroll'); + sideModal.ariaExpanded = true; + }); + + focusElement = document.activeElement; +} + function createTextKey(text) { // create a key that can be used to look up the text in the placeholders const words = text.toLowerCase().replace(/[^a-z0-9\s]/g, '').split(/\s+/); diff --git a/styles/lazy-styles.css b/styles/lazy-styles.css index 84e7d6c9..c8ced821 100644 --- a/styles/lazy-styles.css +++ b/styles/lazy-styles.css @@ -1 +1,4 @@ /* add global styles that can be loaded post LCP here */ +body.disable-scroll, body.disable-scroll main { + overflow: hidden; +} \ No newline at end of file From c63b0eb4e52b5db01166f72a1ec4e488723a9c1a Mon Sep 17 00:00:00 2001 From: Rob Rusher Date: Mon, 5 Feb 2024 13:31:07 -0700 Subject: [PATCH 05/33] test submission --- blocks/contact-form/contact-form.js | 10 +++++----- scripts/util.js | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/blocks/contact-form/contact-form.js b/blocks/contact-form/contact-form.js index 4857bb68..318bdc7b 100644 --- a/blocks/contact-form/contact-form.js +++ b/blocks/contact-form/contact-form.js @@ -37,7 +37,7 @@ function isValid(form) { errors.push(i18n('Email address is required.')); email.classList.add('error'); } - if (!emailRegex.test(email)) { + if (!emailRegex.test(email.value)) { errors.push(i18n('Please enter an email address in the format: email@domain.com.')); email.classList.add('error'); } @@ -47,7 +47,7 @@ function isValid(form) { errors.push(i18n('Email address is required.')); phone.classList.add('error'); } - if (!phoneRegex.test(phone)) { + if (!phoneRegex.test(phone.value)) { errors.push(i18n('Please enter a 10 digit phone number.')); phone.classList.add('error'); } @@ -140,9 +140,9 @@ const addForm = async (block) => { const oldSubmit = form.onsubmit; thankYou.classList.add('form-thank-you'); form.onsubmit = function handleSubmit() { - if (oldSubmit.call(this)) { - const body = new FormData(this); - const { action, method } = this; + if (oldSubmit.call(form)) { + const body = new FormData(form); + const { action, method } = form; fetch(action, { method, body, redirect: 'manual' }).then((resp) => { /* eslint-disable-next-line no-console */ if (!resp.ok) console.error(`Form submission failed: ${resp.status} / ${resp.statusText}`); diff --git a/scripts/util.js b/scripts/util.js index a0196e1c..b73cfc38 100644 --- a/scripts/util.js +++ b/scripts/util.js @@ -1,4 +1,4 @@ -import { fetchPlaceholders, loadCSS } from './aem.js'; +import { fetchPlaceholders } from './aem.js'; /** * Creates the standard Spinner Div. From 938993e38b0b3d8f3b79011bea15ea90c481e1c1 Mon Sep 17 00:00:00 2001 From: Rob Rusher Date: Mon, 5 Feb 2024 13:34:18 -0700 Subject: [PATCH 06/33] linting --- blocks/contact-form/contact-form.css | 18 +++++++++--------- blocks/side-modal/side-modal.css | 2 +- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/blocks/contact-form/contact-form.css b/blocks/contact-form/contact-form.css index df32f787..e965f626 100644 --- a/blocks/contact-form/contact-form.css +++ b/blocks/contact-form/contact-form.css @@ -75,14 +75,6 @@ padding: 15px; } -.contact-form.block .contact-form form .agent > div:first-child { - margin-bottom: .5rem; -} - -.contact-form.block .contact-form form .agent .agent-check { - display: inline-flex; - gap: 10px; -} .contact-form.block .contact-form form .agent div { font-weight: 400; font-size: 14px; @@ -95,6 +87,15 @@ cursor: pointer; } +.contact-form.block .contact-form form .agent > div:first-child { + margin-bottom: .5rem; +} + +.contact-form.block .contact-form form .agent .agent-check { + display: inline-flex; + gap: 10px; +} + .contact-form.block .contact-form form .agent input[type="checkbox"] { height: 24px; min-width: 24px; @@ -120,7 +121,6 @@ display: none; height: 10px; width: 12px; - top: -webkit-calc(50% - 5px); top: calc(50% - 5px); position: absolute; left: 5px; diff --git a/blocks/side-modal/side-modal.css b/blocks/side-modal/side-modal.css index 557d3d8e..9e2e282d 100644 --- a/blocks/side-modal/side-modal.css +++ b/blocks/side-modal/side-modal.css @@ -10,7 +10,7 @@ aside.side-modal { z-index: 1090; } -aside.side-modal[aria-expanded=true] { +aside.side-modal[aria-expanded="true"] { right: 0; } From eae9440b84558c1cd1c985fddaed0fdc50b23625 Mon Sep 17 00:00:00 2001 From: Rob Rusher Date: Wed, 14 Feb 2024 10:18:52 -0700 Subject: [PATCH 07/33] added validation, form data, and listeners --- blocks/contact-form/contact-form.css | 4 +- blocks/contact-form/contact-form.js | 206 ++++++++++------------ blocks/contact-form/forms/contact-us.html | 14 +- scripts/util.js | 28 ++- 4 files changed, 127 insertions(+), 125 deletions(-) diff --git a/blocks/contact-form/contact-form.css b/blocks/contact-form/contact-form.css index e965f626..75726806 100644 --- a/blocks/contact-form/contact-form.css +++ b/blocks/contact-form/contact-form.css @@ -96,7 +96,7 @@ gap: 10px; } -.contact-form.block .contact-form form .agent input[type="checkbox"] { +.contact-form.block .contact-form form .agent input[type="radio"] { height: 24px; min-width: 24px; width: 24px; @@ -126,7 +126,7 @@ left: 5px; } -.contact-form.block .contact-form form .agent input[type="checkbox"]:checked+.checkbox svg { +.contact-form.block .contact-form form .agent input[type="radio"]:checked+.checkbox svg { display: block; } diff --git a/blocks/contact-form/contact-form.js b/blocks/contact-form/contact-form.js index 318bdc7b..1afbdf9f 100644 --- a/blocks/contact-form/contact-form.js +++ b/blocks/contact-form/contact-form.js @@ -1,9 +1,34 @@ -import { hideSideModal, i18nLookup } from '../../scripts/util.js'; +import { hideSideModal, i18nLookup, getCookieValue } from '../../scripts/util.js'; const LOGIN_ERROR = 'There was a problem processing your request.'; const i18n = await i18nLookup(); const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; -const phoneRegex = /^\d{10}$/; +const phoneRegex = /^[+]?[ (]?\d{3}[)]?[-.\s]?\d{3}[-.\s]?\d{4}$/; + +/** + * Adds customID and recipientID cookie values to the request body based on the form name. + * + * @param {FormData} form - The FormData object representing the form data. + */ +function addFranchiseData(form) { + console.log('add data to form'); + const formName = form.body.id; + const customID = getCookieValue('customerID'); + + form.append('customID', customID); + if (formName === 'contactForm') { + form.append('recipientId', 'https://ma312.bhhs.hsfaffiliates.com/profile/card#me'); + form.append('recipientName', 'Commonwealth Real Estate'); + form.append('recipientType', 'organization'); + form.append('text', `Name: ${form.get('first_name')} ${form.get('last_name')}\n + Email: ${form.get('email')}\n + Phone: ${form.get('phone')}\n\n + ${form.get('comments')}`); + } + if (formName === 'makeOffer') { + form.append('recipientID', 'recipientID'); + } +} function displayError(errors) { const message = document.body.querySelector('.contact-form.block').querySelector('.message'); @@ -18,7 +43,7 @@ function displayError(errors) { message.classList.add('error'); } -function isValid(form) { +function validateFormInputs(form) { const errors = []; const firstName = form.querySelector('input[name="first_name"]'); if (!firstName.value || firstName.value.trim().length === 0) { @@ -44,7 +69,7 @@ function isValid(form) { const phone = form.querySelector('input[name="phone"]'); if (!phone.value || phone.value.trim().length === 0) { - errors.push(i18n('Email address is required.')); + errors.push(i18n('Phone number is required.')); phone.classList.add('error'); } if (!phoneRegex.test(phone.value)) { @@ -56,14 +81,10 @@ function isValid(form) { displayError(errors); return false; } + console.log('validation passed'); return true; } -function submitContactForm(form) { - console.log('submitted'); - return isValid(form); -} - // eslint-disable no-console const addForm = async (block) => { const displayValue = block.style.display; @@ -81,16 +102,70 @@ const addForm = async (block) => { block.innerHTML = await data.text(); - const submitBtn = block.querySelector('.cta a.submit'); + const form = block.querySelector('form#contactForm'); + + if (thankYou) { + const oldSubmit = form.onsubmit; + thankYou.classList.add('form-thank-you'); + form.onsubmit = function handleSubmit() { + console.log('Handle submit'); // Check if this log appears + if (oldSubmit.call(this)) { + console.log('Form submitted'); // Check if this log appears + const body = new FormData(this); + addFranchiseData(body); + const { action, method } = this; + fetch(action, { method, body, redirect: 'manual' }).then((resp) => { + /* eslint-disable-next-line no-console */ + if (!resp.ok) console.error(`Form submission failed: ${resp.status} / ${resp.statusText}`); + const firstContent = thankYou.firstElementChild; + if (firstContent.tagName === 'A') { + // redirect to thank you page + window.location.href = firstContent.href; + } else { + // show thank you content + const btn = thankYou.querySelector('a'); + const sideModal = document.querySelector('.side-modal-form'); + if (btn && sideModal) { + btn.setAttribute('href', '#'); + btn.addEventListener('click', (e) => { + e.preventDefault(); + hideSideModal(); + }); + sideModal?.replaceChildren(thankYou); + } else { + block.replaceChildren(thankYou); + } + } + }); + } + return false; + }; + } + + const inputs = block.querySelectorAll('input'); + inputs.forEach((formEl) => { + formEl.placeholder = i18n(formEl.placeholder); + formEl.ariaLabel = i18n(formEl.ariaLabel); + }); + + const taEl = block.querySelector('textarea'); + if (taEl && taEl.placeholder) taEl.placeholder = i18n(taEl.placeholder); + + block.style.display = displayValue; + + const submitBtn = block.querySelector('.contact-form.block .cta a.submit'); if (submitBtn) { submitBtn.addEventListener('click', (e) => { + console.log('button clicked'); e.preventDefault(); e.stopPropagation(); - submitContactForm(block.querySelector('form')); + if (validateFormInputs(form)) { + form.submit(); + } }); } - const cancelBtn = block.querySelector('.cta a.cancel'); + const cancelBtn = block.querySelector('.contact-form.block .cta a.cancel'); if (cancelBtn) { cancelBtn.addEventListener('click', (e) => { e.preventDefault(); @@ -121,6 +196,11 @@ const addForm = async (block) => { e.currentTarget.classList.remove('error'); } }); + // create input mask + el.addEventListener('input', (e) => { + const x = e.target.value.replace(/\D/g, '').match(/(\d{0,3})(\d{0,3})(\d{0,4})/); + e.target.value = !x[2] ? x[1] : `(${x[1]}) ${x[2]}${x[3] ? `-${x[3]}` : ''}`; + }); }); [...block.querySelectorAll('input[name="email"]')] @@ -134,108 +214,6 @@ const addForm = async (block) => { } }); }); - - if (thankYou) { - const form = block.querySelector('#contactForm'); - const oldSubmit = form.onsubmit; - thankYou.classList.add('form-thank-you'); - form.onsubmit = function handleSubmit() { - if (oldSubmit.call(form)) { - const body = new FormData(form); - const { action, method } = form; - fetch(action, { method, body, redirect: 'manual' }).then((resp) => { - /* eslint-disable-next-line no-console */ - if (!resp.ok) console.error(`Form submission failed: ${resp.status} / ${resp.statusText}`); - const firstContent = thankYou.firstElementChild; - if (firstContent.tagName === 'A') { - // redirect to thank you page - window.location.href = firstContent.href; - } else { - // show thank you content - const btn = thankYou.querySelector('a'); - const sideModal = document.querySelector('.side-modal-form'); - if (btn && sideModal) { - btn.setAttribute('href', '#'); - btn.addEventListener('click', (e) => { - e.preventDefault(); - hideSideModal(); - }); - sideModal?.replaceChildren(thankYou); - } else { - block.replaceChildren(thankYou); - } - } - }); - } - return false; - }; - } - - // If the form has it's own styles, add them. - const styles = block.querySelectorAll('style'); - styles.forEach((styleSheet) => { - document.head.appendChild(styleSheet); - }); - - // If the form has it's own scripts, load them one by one to maintain execution order. - // eslint-disable-next-line no-restricted-syntax - for (const script of [...block.querySelectorAll('script')]) { - let waitForLoad = Promise.resolve(); - // The script element added by innerHTML is NOT executed. - // The workaround is to create the new script tag, copy attibutes and content. - const newScript = document.createElement('script'); - newScript.setAttribute('type', 'text/javascript'); - // Copy script attributes to the new element. - script.getAttributeNames().forEach((attrName) => { - const attrValue = script.getAttribute(attrName); - newScript.setAttribute(attrName, attrValue); - - if (attrName === 'src') { - waitForLoad = new Promise((resolve) => { - newScript.addEventListener('load', resolve); - }); - } - }); - newScript.innerHTML = script.innerHTML; - script.remove(); - document.body.append(newScript); - - // eslint-disable-next-line no-await-in-loop - await waitForLoad; - } - - const inputs = block.querySelectorAll('input'); - inputs.forEach((formEl) => { - formEl.placeholder = i18n(formEl.placeholder); - formEl.ariaLabel = i18n(formEl.ariaLabel); - }); - - const taEl = block.querySelector('textarea'); - if (taEl && taEl.placeholder) taEl.placeholder = i18n(taEl.placeholder); - - // Get all checkboxes with class 'checkbox' - const checkboxes = document.querySelectorAll('input[type="checkbox"]'); - - // Define a function declaration to handle the change event - function handleChange() { - // Store the clicked checkbox in a variable - const clickedCheckbox = this; - - // Uncheck all checkboxes that are not the clicked checkbox - checkboxes.forEach((cb) => { - if (cb !== clickedCheckbox) { - cb.checked = false; - } - }); - } - - // Add the change event listener to each checkbox using the function declaration - checkboxes.forEach((checkbox) => { - checkbox.addEventListener('change', handleChange); - checkbox.nextElementSibling.addEventListener('change', handleChange); - }); - - block.style.display = displayValue; }; export default async function decorate(block) { diff --git a/blocks/contact-form/forms/contact-us.html b/blocks/contact-form/forms/contact-us.html index 941dbf99..6b8aa78b 100644 --- a/blocks/contact-form/forms/contact-us.html +++ b/blocks/contact-form/forms/contact-us.html @@ -1,5 +1,7 @@
-
+
@@ -21,7 +23,7 @@
-
-
+
-->
Submit diff --git a/scripts/util.js b/scripts/util.js index 9fea1784..9eac4d1b 100644 --- a/scripts/util.js +++ b/scripts/util.js @@ -116,9 +116,12 @@ export async function i18nLookup(prefix) { }; } -/* - * Returns the environment type based on the hostname. -*/ +/** + * Retrieves the environment type based on the provided hostname. + * + * @param {string} [hostname=window.location.hostname] - The hostname to determine the environment. + * @returns {string} The environment type ('live', 'preview', or 'dev'). + */ export function getEnvType(hostname = window.location.hostname) { const fqdnToEnvType = { 'commonmoves.com': 'live', @@ -131,11 +134,30 @@ export function getEnvType(hostname = window.location.hostname) { return fqdnToEnvType[hostname] || 'dev'; } +/** + * Retrieves the value of a cookie by its name. + * + * @param {string} cookieName - The name of the cookie to retrieve. + * @returns {string|null} The value of the cookie, or null if not found. + */ +export function getCookieValue(cookieName) { + const cookies = document.cookie.split(';'); + const foundCookie = cookies.find((cookie) => { + const trimmedCookie = cookie.trim(); + return trimmedCookie.includes(cookieName); + }); + if (foundCookie) { + return foundCookie.split('=', 2)[1]; + } + return null; +} + const Util = { getSpinner, showModal, i18nLookup, getEnvType, + getCookieValue, }; export default Util; From d5dcb8bafd852309c078033eff7586030fb46b82 Mon Sep 17 00:00:00 2001 From: Rob Rusher Date: Thu, 15 Feb 2024 14:26:01 -0700 Subject: [PATCH 08/33] adjust data payload --- blocks/contact-form/contact-form.css | 60 +++++++------- blocks/contact-form/contact-form.js | 96 ++++++++++++++--------- blocks/contact-form/forms/contact-us.html | 9 +-- styles/styles.css | 7 +- 4 files changed, 99 insertions(+), 73 deletions(-) diff --git a/blocks/contact-form/contact-form.css b/blocks/contact-form/contact-form.css index 75726806..cc53b69a 100644 --- a/blocks/contact-form/contact-form.css +++ b/blocks/contact-form/contact-form.css @@ -4,7 +4,7 @@ font-family: var(--font-family-proxima); } -.contact-form.block .contact-form form .message { +.contact-form.block form.contact-form .message { display: none; padding: 10px 4px; margin-bottom: 1em; @@ -14,51 +14,51 @@ column-gap: 4px; } -.contact-form.block .contact-form form .message.error { +.contact-form.block form.contact-form .message.error { display: flex; color: var(--error); border-color: var(--error); } -.contact-form.block .contact-form form .message.success { +.contact-form.block form.contact-form .message.success { display: flex; color: var(--success); border-color: var(--success); } -.contact-form.block .contact-form form .message .icon { +.contact-form.block form.contact-form .message .icon { display: none; align-self: flex-start; width: 20px; height: 20px; } -.contact-form.block .contact-form form .message.error .icon.error { +.contact-form.block form.contact-form .message.error .icon.error { display: block; } -.contact-form.block .contact-form form .message.success .icon.success { +.contact-form.block form.contact-form .message.success .icon.success { display: block; } -.contact-form.block .contact-form form .message .details { +.contact-form.block form.contact-form .message .details { display: flex; flex-direction: column; } -.contact-form.block .contact-form form .message span { +.contact-form.block form.contact-form .message span { font-size: var(--body-font-size-xs); line-height: var(--line-height-s); } -.contact-form.block .contact-form form .inputs .name, -.contact-form.block .contact-form form .inputs .contact-info { +.contact-form.block form.contact-form .inputs .name, +.contact-form.block form.contact-form .inputs .contact-info { display: block; } -.contact-form.block .contact-form form .inputs input[type="text"], -.contact-form.block .contact-form form .inputs input[type="email"], -.contact-form.block .contact-form form .inputs textarea { +.contact-form.block form.contact-form .inputs input[type="text"], +.contact-form.block form.contact-form .inputs input[type="email"], +.contact-form.block form.contact-form .inputs textarea { height: 50px; width: 100%; padding-left: 15px; @@ -69,13 +69,13 @@ border: 1px solid var(--dark-grey); } -.contact-form.block .contact-form form .inputs textarea { +.contact-form.block form.contact-form .inputs textarea { width: 100%; height: 110px; padding: 15px; } -.contact-form.block .contact-form form .agent div { +.contact-form.block form.contact-form .agent div { font-weight: 400; font-size: 14px; color: var(--body-color); @@ -87,16 +87,16 @@ cursor: pointer; } -.contact-form.block .contact-form form .agent > div:first-child { +.contact-form.block form.contact-form .agent > div:first-child { margin-bottom: .5rem; } -.contact-form.block .contact-form form .agent .agent-check { +.contact-form.block form.contact-form .agent .agent-check { display: inline-flex; gap: 10px; } -.contact-form.block .contact-form form .agent input[type="radio"] { +.contact-form.block form.contact-form .agent input[type="radio"] { height: 24px; min-width: 24px; width: 24px; @@ -108,7 +108,7 @@ cursor: pointer; } -.contact-form.block .contact-form form .agent .checkbox { +.contact-form.block form.contact-form .agent .checkbox { cursor: pointer; height: 24px; width: 24px; @@ -117,7 +117,7 @@ position: relative; } -.contact-form.block .contact-form form .agent .checkbox svg { +.contact-form.block form.contact-form .agent .checkbox svg { display: none; height: 10px; width: 12px; @@ -126,12 +126,12 @@ left: 5px; } -.contact-form.block .contact-form form .agent input[type="radio"]:checked+.checkbox svg { +.contact-form.block form.contact-form .agent input[type="radio"]:checked+.checkbox svg { display: block; } -.contact-form.block .contact-form form .inputs input[type="text"].error, -.contact-form.block .contact-form form .inputs input[type="email"].error { +.contact-form.block form.contact-form .inputs input[type="text"].error, +.contact-form.block form.contact-form .inputs input[type="email"].error { color: var(--error); background-color: var(--error-highlight); border: 2px solid var(--error); @@ -141,33 +141,33 @@ padding: 3rem 0; } -.contact-form.block .contact-form form .cta { +.contact-form.block form.contact-form .cta { padding-bottom: 2rem; } -.contact-form.block .contact-form form .cta .button-container a.button.primary { +.contact-form.block form.contact-form .cta .button-container button.primary { background-color: var(--primary-color); color: var(--white); text-transform: uppercase; margin-right: 12px; } -.contact-form.block .contact-form form .cta .button-container a.button.primary:hover { +.contact-form.block form.contact-form .cta .button-container button.primary:hover { background-color: var(--white); color: var(--body-color); border-color: var(--grey); } @media (min-width: 600px) { - .contact-form.block .contact-form form .inputs .name, - .contact-form.block .contact-form form .inputs .contact-info { + .contact-form.block form.contact-form .inputs .name, + .contact-form.block form.contact-form .inputs .contact-info { display: flex; gap: 10px; } - .contact-form.block .contact-form form .inputs input[type="text"], - .contact-form.block .contact-form form .inputs input[type="email"] { + .contact-form.block form.contact-form .inputs input[type="text"], + .contact-form.block form.contact-form .inputs input[type="email"] { width: 50%; } } diff --git a/blocks/contact-form/contact-form.js b/blocks/contact-form/contact-form.js index 1afbdf9f..41b0d9d4 100644 --- a/blocks/contact-form/contact-form.js +++ b/blocks/contact-form/contact-form.js @@ -11,23 +11,48 @@ const phoneRegex = /^[+]?[ (]?\d{3}[)]?[-.\s]?\d{3}[-.\s]?\d{4}$/; * @param {FormData} form - The FormData object representing the form data. */ function addFranchiseData(form) { - console.log('add data to form'); - const formName = form.body.id; - const customID = getCookieValue('customerID'); - - form.append('customID', customID); - if (formName === 'contactForm') { - form.append('recipientId', 'https://ma312.bhhs.hsfaffiliates.com/profile/card#me'); - form.append('recipientName', 'Commonwealth Real Estate'); - form.append('recipientType', 'organization'); - form.append('text', `Name: ${form.get('first_name')} ${form.get('last_name')}\n - Email: ${form.get('email')}\n - Phone: ${form.get('phone')}\n\n - ${form.get('comments')}`); + const jsonObj = {}; + jsonObj.form = form.id; + + const firstName = form.elements.first_name.value; + const lastName = form.elements.last_name.value; + const email = form.elements.email.value; + const phone = form.elements.phone.value; + const comments = form.elements.comments.value; + const hasAgentRadio = form.elements.hasAgent; + const hasAgentValue = Array.from(hasAgentRadio).find((radio) => radio.checked)?.value === 'yes'; + const officeIdMeta = document.querySelector('meta[name="office-id"]'); + + try { + const consumerID = getCookieValue('customerID'); + if (consumerID !== null) { + jsonObj.data.consumerID = consumerID; + } else { + /* eslint-disable-next-line no-console */ + console.warn('Cookie not found: customerID'); + } + } catch (error) { + /* eslint-disable-next-line no-console */ + console.error('Error getting cookie value:', error); } - if (formName === 'makeOffer') { - form.append('recipientID', 'recipientID'); + jsonObj.data = {}; + jsonObj.data.email = email; + jsonObj.data.name = `${firstName} ${lastName}`; + jsonObj.data.recipientId = `https://${officeIdMeta}.bhhs.hsfaffiliates.com/profile/card#me`; + jsonObj.data.recipientName = 'Commonwealth Real Estate'; + jsonObj.data.recipientType = 'organization'; + jsonObj.data.source = `https://${officeIdMeta}.bhhs.hsfaffiliates.com/profile/card#me`; + jsonObj.data.telephone = phone; + jsonObj.data.text = `Name: ${firstName} ${lastName}\n + Email: ${email}\n + Phone: ${phone}\n\n + ${comments}`; + jsonObj.data.url = `${window.location.href} | ${document.title}`; + jsonObj.data.workingWithAgent = hasAgentValue; + if (form.id === 'makeOffer') { + jsonObj.data.prixe = form.elements.price; } + return JSON.stringify(jsonObj); } function displayError(errors) { @@ -102,19 +127,29 @@ const addForm = async (block) => { block.innerHTML = await data.text(); - const form = block.querySelector('form#contactForm'); + const form = block.querySelector('form.contact-form'); + // if there is a thank you, highjack the submission + // otherwise submit form normally. if (thankYou) { - const oldSubmit = form.onsubmit; + const oldSubmit = validateFormInputs; thankYou.classList.add('form-thank-you'); - form.onsubmit = function handleSubmit() { + form.onsubmit = function handleSubmit(e) { console.log('Handle submit'); // Check if this log appears - if (oldSubmit.call(this)) { - console.log('Form submitted'); // Check if this log appears - const body = new FormData(this); - addFranchiseData(body); + e.preventDefault(); + if (oldSubmit(this)) { + console.log('OnSubmit called'); // Check if this log appears + const jsonData = addFranchiseData(this); const { action, method } = this; - fetch(action, { method, body, redirect: 'manual' }).then((resp) => { + console.log('Call fetch'); // Check if this log appears + fetch(action, { + method, + headers: { + 'Content-Type': 'application/json', + }, + body: jsonData, + mode: 'no-cors', + }).then((resp) => { /* eslint-disable-next-line no-console */ if (!resp.ok) console.error(`Form submission failed: ${resp.status} / ${resp.statusText}`); const firstContent = thankYou.firstElementChild; @@ -134,6 +169,7 @@ const addForm = async (block) => { sideModal?.replaceChildren(thankYou); } else { block.replaceChildren(thankYou); + const temp = block.parent.nextSibling; } } }); @@ -153,18 +189,6 @@ const addForm = async (block) => { block.style.display = displayValue; - const submitBtn = block.querySelector('.contact-form.block .cta a.submit'); - if (submitBtn) { - submitBtn.addEventListener('click', (e) => { - console.log('button clicked'); - e.preventDefault(); - e.stopPropagation(); - if (validateFormInputs(form)) { - form.submit(); - } - }); - } - const cancelBtn = block.querySelector('.contact-form.block .cta a.cancel'); if (cancelBtn) { cancelBtn.addEventListener('click', (e) => { @@ -199,7 +223,7 @@ const addForm = async (block) => { // create input mask el.addEventListener('input', (e) => { const x = e.target.value.replace(/\D/g, '').match(/(\d{0,3})(\d{0,3})(\d{0,4})/); - e.target.value = !x[2] ? x[1] : `(${x[1]}) ${x[2]}${x[3] ? `-${x[3]}` : ''}`; + e.target.value = !x[2] ? x[1] : `${x[1]}-${x[2]}${x[3] ? `-${x[3]}` : ''}`; }); }); diff --git a/blocks/contact-form/forms/contact-us.html b/blocks/contact-form/forms/contact-us.html index 6b8aa78b..604e2e9a 100644 --- a/blocks/contact-form/forms/contact-us.html +++ b/blocks/contact-form/forms/contact-us.html @@ -1,7 +1,6 @@ -
- +
+
@@ -67,7 +66,7 @@
-->
- Submit +
diff --git a/styles/styles.css b/styles/styles.css index d24033be..9d2aae37 100644 --- a/styles/styles.css +++ b/styles/styles.css @@ -295,8 +295,11 @@ main .section.two-columns > div { } main .section.form-header h1 { - font-size: 60px; - font-family: var(--font-family-georgia); + font-size: var(--heading-font-size-m); + font-family: var(--font-family-proxima); + font-weight: 700; + line-height: 130%; + margin-bottom: 15px; } main .section.form-header p { From 0b283a73ad4f841d173c1afdf469a9252f4a87e9 Mon Sep 17 00:00:00 2001 From: Rob Rusher Date: Thu, 15 Feb 2024 15:56:13 -0700 Subject: [PATCH 09/33] remove the disclaimer --- blocks/contact-form/contact-form.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/blocks/contact-form/contact-form.js b/blocks/contact-form/contact-form.js index 41b0d9d4..e97eb2c2 100644 --- a/blocks/contact-form/contact-form.js +++ b/blocks/contact-form/contact-form.js @@ -169,7 +169,7 @@ const addForm = async (block) => { sideModal?.replaceChildren(thankYou); } else { block.replaceChildren(thankYou); - const temp = block.parent.nextSibling; + block.parentNode.nextSibling.remove(); } } }); From 918947011d6791d462dcc3e257f1fb701ecf3f91 Mon Sep 17 00:00:00 2001 From: Rob Rusher Date: Thu, 15 Feb 2024 17:01:32 -0700 Subject: [PATCH 10/33] json update --- blocks/contact-form/contact-form.js | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/blocks/contact-form/contact-form.js b/blocks/contact-form/contact-form.js index e97eb2c2..5824475a 100644 --- a/blocks/contact-form/contact-form.js +++ b/blocks/contact-form/contact-form.js @@ -6,14 +6,11 @@ const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; const phoneRegex = /^[+]?[ (]?\d{3}[)]?[-.\s]?\d{3}[-.\s]?\d{4}$/; /** - * Adds customID and recipientID cookie values to the request body based on the form name. + * Adds form and cookie values to a JSON object. * * @param {FormData} form - The FormData object representing the form data. */ function addFranchiseData(form) { - const jsonObj = {}; - jsonObj.form = form.id; - const firstName = form.elements.first_name.value; const lastName = form.elements.last_name.value; const email = form.elements.email.value; @@ -21,21 +18,24 @@ function addFranchiseData(form) { const comments = form.elements.comments.value; const hasAgentRadio = form.elements.hasAgent; const hasAgentValue = Array.from(hasAgentRadio).find((radio) => radio.checked)?.value === 'yes'; - const officeIdMeta = document.querySelector('meta[name="office-id"]'); + const officeIdMeta = document.querySelector('meta[name="office-id"]').getAttribute('content'); + const jsonObj = {}; + jsonObj.data = {}; + jsonObj.form = form.id; try { - const consumerID = getCookieValue('customerID'); - if (consumerID !== null) { - jsonObj.data.consumerID = consumerID; + const consumerCookie = getCookieValue('consumerID'); + if (consumerCookie !== null) { + jsonObj.data.consumerID = consumerCookie; } else { /* eslint-disable-next-line no-console */ - console.warn('Cookie not found: customerID'); + console.warn('Cookie not found: consumerID'); } } catch (error) { /* eslint-disable-next-line no-console */ console.error('Error getting cookie value:', error); } - jsonObj.data = {}; + jsonObj.data.email = email; jsonObj.data.name = `${firstName} ${lastName}`; jsonObj.data.recipientId = `https://${officeIdMeta}.bhhs.hsfaffiliates.com/profile/card#me`; @@ -148,7 +148,7 @@ const addForm = async (block) => { 'Content-Type': 'application/json', }, body: jsonData, - mode: 'no-cors', + credentials: 'include', }).then((resp) => { /* eslint-disable-next-line no-console */ if (!resp.ok) console.error(`Form submission failed: ${resp.status} / ${resp.statusText}`); From c221ae2fc0c07cd30730699a2caf17b07cc0d10f Mon Sep 17 00:00:00 2001 From: Rob Rusher Date: Wed, 21 Feb 2024 14:57:47 -0700 Subject: [PATCH 11/33] update payload --- blocks/contact-form/contact-form.js | 24 +++++++++++++++---- .../contact-form/forms/contact-property.html | 13 +++++----- styles/lazy-styles.css | 2 +- styles/styles.css | 4 ++-- 4 files changed, 28 insertions(+), 15 deletions(-) diff --git a/blocks/contact-form/contact-form.js b/blocks/contact-form/contact-form.js index 5824475a..55c742b2 100644 --- a/blocks/contact-form/contact-form.js +++ b/blocks/contact-form/contact-form.js @@ -49,8 +49,22 @@ function addFranchiseData(form) { ${comments}`; jsonObj.data.url = `${window.location.href} | ${document.title}`; jsonObj.data.workingWithAgent = hasAgentValue; - if (form.id === 'makeOffer') { - jsonObj.data.prixe = form.elements.price; + if (form.id === 'property-contact') { + jsonObj.data.addressLocality = 'Boston'; + jsonObj.data.addressRegion = 'Boston'; + jsonObj.data.agentType = 'Boston'; + jsonObj.data.coListing = 'Boston'; + jsonObj.data.postalCode = 'Boston'; + jsonObj.data.price = 'Boston'; + jsonObj.data.priceCurrency = 'Boston'; + jsonObj.data.streetAddress = 'Boston'; + } + if (form.id === 'make-offer' || form.id === 'see-property') { + jsonObj.listAor = 'mamlspin'; + jsonObj.mlsId = '234234'; + jsonObj.mlsKey = '234234'; + jsonObj.mlsName = 'MLSPIN - MLS Property Information Network'; + jsonObj.pid = '234234'; } return JSON.stringify(jsonObj); } @@ -162,8 +176,8 @@ const addForm = async (block) => { const sideModal = document.querySelector('.side-modal-form'); if (btn && sideModal) { btn.setAttribute('href', '#'); - btn.addEventListener('click', (e) => { - e.preventDefault(); + btn.addEventListener('click', (event) => { + event.preventDefault(); hideSideModal(); }); sideModal?.replaceChildren(thankYou); @@ -189,7 +203,7 @@ const addForm = async (block) => { block.style.display = displayValue; - const cancelBtn = block.querySelector('.contact-form.block .cta a.cancel'); + const cancelBtn = block.querySelector('.contact-form.block .cta button.cancel'); if (cancelBtn) { cancelBtn.addEventListener('click', (e) => { e.preventDefault(); diff --git a/blocks/contact-form/forms/contact-property.html b/blocks/contact-form/forms/contact-property.html index aed6c0e5..cba6dcb6 100644 --- a/blocks/contact-form/forms/contact-property.html +++ b/blocks/contact-form/forms/contact-property.html @@ -3,7 +3,7 @@

Contact Us

Direct:
-
+
@@ -33,10 +33,10 @@

Contact Us

autocomplete="off">
-
Are you currently working with an agent?
+
Are you currently working with an agent?
- +
@@ -45,7 +45,7 @@

Contact Us

yes
- +
@@ -53,7 +53,6 @@

Contact Us

no
-
@@ -70,8 +69,8 @@

Contact Us

- Send - Cancel + +
diff --git a/styles/lazy-styles.css b/styles/lazy-styles.css index c8ced821..4293513f 100644 --- a/styles/lazy-styles.css +++ b/styles/lazy-styles.css @@ -1,4 +1,4 @@ /* add global styles that can be loaded post LCP here */ -body.disable-scroll, body.disable-scroll main { +body.disable-scroll { overflow: hidden; } \ No newline at end of file diff --git a/styles/styles.css b/styles/styles.css index 9d2aae37..43e4f211 100644 --- a/styles/styles.css +++ b/styles/styles.css @@ -381,7 +381,7 @@ main .section .default-content-wrapper picture img { animation: spinner-spin 2s linear infinite; } -.button-container a { +.button-container a, .button-container button.cancel { display: inline-block; padding: .75em 1.5em; font-family: var(--font-family-proxima); @@ -463,7 +463,7 @@ main .section.button-primary-color .button-container a:hover { background-color: var(--primary-color); } -form button[type="submit"] { +form button[type="submit"], .button-container button { display: inline-block; padding: 13px 16px; margin: 16px 0; From 1ebbc3a5ed29be11a3ff3d45448cb2f6a8c3c744 Mon Sep 17 00:00:00 2001 From: Rob Rusher Date: Mon, 26 Feb 2024 17:41:54 -0700 Subject: [PATCH 12/33] add team inquiry form --- blocks/contact-form/contact-form.css | 30 +- blocks/contact-form/contact-form.js | 134 ++-- blocks/contact-form/forms/contact-us.html | 4 +- blocks/contact-form/forms/join-our-team.html | 797 +++++++++++++++++++ 4 files changed, 908 insertions(+), 57 deletions(-) create mode 100644 blocks/contact-form/forms/join-our-team.html diff --git a/blocks/contact-form/contact-form.css b/blocks/contact-form/contact-form.css index cc53b69a..bf253409 100644 --- a/blocks/contact-form/contact-form.css +++ b/blocks/contact-form/contact-form.css @@ -52,13 +52,16 @@ } .contact-form.block form.contact-form .inputs .name, -.contact-form.block form.contact-form .inputs .contact-info { +.contact-form.block form.contact-form .inputs .contact-info, +.contact-form.block form.contact-form .inputs .size { display: block; } .contact-form.block form.contact-form .inputs input[type="text"], .contact-form.block form.contact-form .inputs input[type="email"], -.contact-form.block form.contact-form .inputs textarea { +.contact-form.block form.contact-form .inputs textarea, +.contact-form.block form.contact-form .inputs select, +.contact-form.block form.contact-form .inputs select option { height: 50px; width: 100%; padding-left: 15px; @@ -159,15 +162,30 @@ } @media (min-width: 600px) { - .contact-form.block form.contact-form .inputs .name, - .contact-form.block form.contact-form .inputs .contact-info { - display: flex; + .contact-form.block form.contact-form .inputs div { + display: flex !important; gap: 10px; - } .contact-form.block form.contact-form .inputs input[type="text"], .contact-form.block form.contact-form .inputs input[type="email"] { width: 50%; } + + .contact-form.block form.contact-form .inputs input[name="title"] { + width: calc(50% - 6px); + } + + .contact-form.block form.contact-form .inputs input[name="city"] { + flex: 0 0 41.667%; + } + + .contact-form.block form.contact-form .inputs input[name="state"] { + flex: 0 0 33.333%; + } + + .contact-form.block form.contact-form .inputs input[name="postalCode"] { + flex: 0 0 25%; + flex-shrink: 1; + } } diff --git a/blocks/contact-form/contact-form.js b/blocks/contact-form/contact-form.js index 55c742b2..86e91d75 100644 --- a/blocks/contact-form/contact-form.js +++ b/blocks/contact-form/contact-form.js @@ -16,57 +16,89 @@ function addFranchiseData(form) { const email = form.elements.email.value; const phone = form.elements.phone.value; const comments = form.elements.comments.value; - const hasAgentRadio = form.elements.hasAgent; - const hasAgentValue = Array.from(hasAgentRadio).find((radio) => radio.checked)?.value === 'yes'; - const officeIdMeta = document.querySelector('meta[name="office-id"]').getAttribute('content'); - const jsonObj = {}; - jsonObj.data = {}; - jsonObj.form = form.id; - try { - const consumerCookie = getCookieValue('consumerID'); - if (consumerCookie !== null) { - jsonObj.data.consumerID = consumerCookie; - } else { + if (form.id !== 'team-inquiry') { + const hasAgentRadio = form.elements.hasAgent; + const hasAgentValue = Array.from(hasAgentRadio).find((radio) => radio.checked)?.value === 'yes'; + const officeIdMeta = document.querySelector('meta[name="office-id"]').getAttribute('content'); + const jsonObj = {}; + jsonObj.data = {}; + jsonObj.form = form.id; + + try { + const consumerCookie = getCookieValue('consumerID'); + if (consumerCookie !== null) { + jsonObj.data.consumerID = consumerCookie; + } else { + /* eslint-disable-next-line no-console */ + console.warn('Cookie not found: consumerID'); + } + } catch (error) { /* eslint-disable-next-line no-console */ - console.warn('Cookie not found: consumerID'); + console.error('Error getting cookie value:', error); } - } catch (error) { - /* eslint-disable-next-line no-console */ - console.error('Error getting cookie value:', error); - } - jsonObj.data.email = email; - jsonObj.data.name = `${firstName} ${lastName}`; - jsonObj.data.recipientId = `https://${officeIdMeta}.bhhs.hsfaffiliates.com/profile/card#me`; - jsonObj.data.recipientName = 'Commonwealth Real Estate'; - jsonObj.data.recipientType = 'organization'; - jsonObj.data.source = `https://${officeIdMeta}.bhhs.hsfaffiliates.com/profile/card#me`; - jsonObj.data.telephone = phone; - jsonObj.data.text = `Name: ${firstName} ${lastName}\n - Email: ${email}\n - Phone: ${phone}\n\n - ${comments}`; - jsonObj.data.url = `${window.location.href} | ${document.title}`; - jsonObj.data.workingWithAgent = hasAgentValue; - if (form.id === 'property-contact') { - jsonObj.data.addressLocality = 'Boston'; - jsonObj.data.addressRegion = 'Boston'; - jsonObj.data.agentType = 'Boston'; - jsonObj.data.coListing = 'Boston'; - jsonObj.data.postalCode = 'Boston'; - jsonObj.data.price = 'Boston'; - jsonObj.data.priceCurrency = 'Boston'; - jsonObj.data.streetAddress = 'Boston'; - } - if (form.id === 'make-offer' || form.id === 'see-property') { - jsonObj.listAor = 'mamlspin'; - jsonObj.mlsId = '234234'; - jsonObj.mlsKey = '234234'; - jsonObj.mlsName = 'MLSPIN - MLS Property Information Network'; - jsonObj.pid = '234234'; + jsonObj.data.email = email; + jsonObj.data.name = `${firstName} ${lastName}`; + jsonObj.data.recipientId = `https://${officeIdMeta}.bhhs.hsfaffiliates.com/profile/card#me`; + jsonObj.data.recipientName = 'Commonwealth Real Estate'; + jsonObj.data.recipientType = 'organization'; + jsonObj.data.source = `https://${officeIdMeta}.bhhs.hsfaffiliates.com/profile/card#me`; + jsonObj.data.telephone = phone; + jsonObj.data.text = `Name: ${firstName} ${lastName}\n + Email: ${email}\n + Phone: ${phone}\n\n + ${comments}`; + jsonObj.data.url = `${window.location.href} | ${document.title}`; + jsonObj.data.workingWithAgent = hasAgentValue; + if (form.id === 'property-contact') { + jsonObj.data.addressLocality = 'Boston'; + jsonObj.data.addressRegion = 'Boston'; + jsonObj.data.agentType = 'Boston'; + jsonObj.data.coListing = 'Boston'; + jsonObj.data.postalCode = 'Boston'; + jsonObj.data.price = 'Boston'; + jsonObj.data.priceCurrency = 'Boston'; + jsonObj.data.streetAddress = 'Boston'; + } + if (form.id === 'make-offer' || form.id === 'see-property') { + jsonObj.listAor = 'mamlspin'; + jsonObj.mlsId = '234234'; + jsonObj.mlsKey = '234234'; + jsonObj.mlsName = 'MLSPIN - MLS Property Information Network'; + jsonObj.pid = '234234'; + } + return JSON.stringify(jsonObj); } - return JSON.stringify(jsonObj); + const title = form.elements.title.value; + const zip = form.elements.title.value; + const country = form.elements.title.value; + const state = form.elements.title.value; + const city = form.elements.title.value; + const address1 = form.elements.title.value; + const address2 = form.elements.title.value; + const numAgents = form.elements.title.value; + const gci = form.elements.title.value; + + const formData = new FormData(); + formData.append('FirstName', firstName); + formData.append('LastName', lastName); + formData.append('Phone', phone); + formData.append('Email', email); + formData.append('Title', title); + formData.append('Message', comments); + formData.append('ZipCode', zip); + formData.append('Country', country); + formData.append('State', state); + formData.append('City', city); + formData.append('AddressOne', address1); + formData.append('AddressTwo', address2); + formData.append('NumOfAgents', numAgents); + formData.append('GCI', gci); + formData.append('Subject', 'Join our Team Website Inquiry'); + formData.append('SendEmail', true); + formData.append('To', 'marketing@commonmoves.com'); + return formData; } function displayError(errors) { @@ -154,13 +186,17 @@ const addForm = async (block) => { if (oldSubmit(this)) { console.log('OnSubmit called'); // Check if this log appears const jsonData = addFranchiseData(this); + const headers = new Headers(); + if (this.id === 'team-inquiry') { + headers.append('Content-Type', 'application/x-www-form-urlencoded; charset=UTF-8'); + } else { + headers.append('Content-Type', 'application/json; charset=UTF-8'); + } const { action, method } = this; console.log('Call fetch'); // Check if this log appears fetch(action, { method, - headers: { - 'Content-Type': 'application/json', - }, + headers, body: jsonData, credentials: 'include', }).then((resp) => { diff --git a/blocks/contact-form/forms/contact-us.html b/blocks/contact-form/forms/contact-us.html index 604e2e9a..4fc8aab7 100644 --- a/blocks/contact-form/forms/contact-us.html +++ b/blocks/contact-form/forms/contact-us.html @@ -52,7 +52,7 @@
- +
diff --git a/blocks/contact-form/forms/join-our-team.html b/blocks/contact-form/forms/join-our-team.html new file mode 100644 index 00000000..806d70d3 --- /dev/null +++ b/blocks/contact-form/forms/join-our-team.html @@ -0,0 +1,797 @@ +
+
+
+ + + + + + +
+ +
+
+
+
+ + +
+
+ + +
+
+ +
+
+ + +
+
+ +
+
+ + +
+
+ + + +
+ +
+
+
+ + +
+ +
+
+
+ +
+
+
+
\ No newline at end of file From dfc38db206bbd0ff5522d4506f35ab4461cdcfdd Mon Sep 17 00:00:00 2001 From: Rob Rusher Date: Wed, 28 Feb 2024 11:46:38 -0700 Subject: [PATCH 13/33] update form payload --- blocks/contact-form/contact-form.js | 29 +++++++++++--------- blocks/contact-form/forms/join-our-team.html | 15 ++-------- 2 files changed, 18 insertions(+), 26 deletions(-) diff --git a/blocks/contact-form/contact-form.js b/blocks/contact-form/contact-form.js index 86e91d75..242ebddc 100644 --- a/blocks/contact-form/contact-form.js +++ b/blocks/contact-form/contact-form.js @@ -6,17 +6,19 @@ const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; const phoneRegex = /^[+]?[ (]?\d{3}[)]?[-.\s]?\d{3}[-.\s]?\d{4}$/; /** - * Adds form and cookie values to a JSON object. + * Adds form and cookie values to payload. * * @param {FormData} form - The FormData object representing the form data. */ function addFranchiseData(form) { + // Common form elements const firstName = form.elements.first_name.value; const lastName = form.elements.last_name.value; const email = form.elements.email.value; const phone = form.elements.phone.value; const comments = form.elements.comments.value; + // All forms except team inquiry if (form.id !== 'team-inquiry') { const hasAgentRadio = form.elements.hasAgent; const hasAgentValue = Array.from(hasAgentRadio).find((radio) => radio.checked)?.value === 'yes'; @@ -68,17 +70,20 @@ function addFranchiseData(form) { jsonObj.mlsName = 'MLSPIN - MLS Property Information Network'; jsonObj.pid = '234234'; } + // Data format to JSON return JSON.stringify(jsonObj); } + + // Remaining Team Inquiry form elements const title = form.elements.title.value; - const zip = form.elements.title.value; - const country = form.elements.title.value; - const state = form.elements.title.value; - const city = form.elements.title.value; - const address1 = form.elements.title.value; - const address2 = form.elements.title.value; - const numAgents = form.elements.title.value; - const gci = form.elements.title.value; + const zip = form.elements.postalCode.value; + const country = form.elements.country.value; + const state = form.elements.state.value; + const city = form.elements.city.value; + const address1 = form.elements.addressOne.value; + const address2 = form.elements.addressTwo.value; + const numAgents = form.elements.numOfAgents.value; + const gci = form.elements.gci.value; const formData = new FormData(); formData.append('FirstName', firstName); @@ -98,7 +103,8 @@ function addFranchiseData(form) { formData.append('Subject', 'Join our Team Website Inquiry'); formData.append('SendEmail', true); formData.append('To', 'marketing@commonmoves.com'); - return formData; + // Data format to URL Params + return new URLSearchParams(formData).toString(); } function displayError(errors) { @@ -181,10 +187,8 @@ const addForm = async (block) => { const oldSubmit = validateFormInputs; thankYou.classList.add('form-thank-you'); form.onsubmit = function handleSubmit(e) { - console.log('Handle submit'); // Check if this log appears e.preventDefault(); if (oldSubmit(this)) { - console.log('OnSubmit called'); // Check if this log appears const jsonData = addFranchiseData(this); const headers = new Headers(); if (this.id === 'team-inquiry') { @@ -193,7 +197,6 @@ const addForm = async (block) => { headers.append('Content-Type', 'application/json; charset=UTF-8'); } const { action, method } = this; - console.log('Call fetch'); // Check if this log appears fetch(action, { method, headers, diff --git a/blocks/contact-form/forms/join-our-team.html b/blocks/contact-form/forms/join-our-team.html index 806d70d3..392ac04b 100644 --- a/blocks/contact-form/forms/join-our-team.html +++ b/blocks/contact-form/forms/join-our-team.html @@ -33,7 +33,7 @@
-
-
-
- - -
- -
+
From 6428a4f28b244cb2d78d31eae994c285bfea3739 Mon Sep 17 00:00:00 2001 From: Rob Rusher Date: Mon, 1 Apr 2024 16:49:57 -0600 Subject: [PATCH 14/33] build recaptcha server calls --- blocks/contact-form/contact-form.js | 59 ++++++++++++++++++++++- blocks/contact-form/forms/contact-us.html | 24 ++++----- scripts/delayed.js | 2 + 3 files changed, 72 insertions(+), 13 deletions(-) diff --git a/blocks/contact-form/contact-form.js b/blocks/contact-form/contact-form.js index 242ebddc..05567cd6 100644 --- a/blocks/contact-form/contact-form.js +++ b/blocks/contact-form/contact-form.js @@ -4,6 +4,7 @@ const LOGIN_ERROR = 'There was a problem processing your request.'; const i18n = await i18nLookup(); const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; const phoneRegex = /^[+]?[ (]?\d{3}[)]?[-.\s]?\d{3}[-.\s]?\d{4}$/; +let recaptchaToken = null; /** * Adds form and cookie values to payload. @@ -120,7 +121,7 @@ function displayError(errors) { message.classList.add('error'); } -function validateFormInputs(form) { +async function validateFormInputs(form) { const errors = []; const firstName = form.querySelector('input[name="first_name"]'); if (!firstName.value || firstName.value.trim().length === 0) { @@ -154,6 +155,35 @@ function validateFormInputs(form) { phone.classList.add('error'); } + if (!errors.length) { + const payload = `user_response=${encodeURIComponent(recaptchaToken)}`; + const options = { + method: 'POST', + body: payload, + headers: { + 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8', + }, + }; + const url = 'https://www.commonmoves.com/bin/bhhs/googleRecaptchaServlet'; + + fetch(url, options) + .then((response) => { + if (!response.ok) { + throw new Error('Network response was not ok'); + } + return response.json(); + }) + .then((data) => { + // Handle the response based on the success property + if (!data.success) { + errors.push(i18n('Captcha verification is required.')); + } + }) + .catch(() => { + errors.push(i18n('Captcha verification failed.')); + }); + } + if (errors.length > 0) { displayError(errors); return false; @@ -231,6 +261,33 @@ const addForm = async (block) => { }; } + // eslint-disable-next-line no-restricted-syntax + for (const script of [...block.querySelectorAll('script')]) { + let waitForLoad = Promise.resolve(); + // the script element added by innerHTML is NOT executed + // the workaround is to create the new script tag, copy attibutes and content + const newScript = document.createElement('script'); + + newScript.setAttribute('type', 'text/javascript'); + // coping all script attribute to the new one + script.getAttributeNames().forEach((attrName) => { + const attrValue = script.getAttribute(attrName); + newScript.setAttribute(attrName, attrValue); + + if (attrName === 'src') { + waitForLoad = new Promise((resolve) => { + newScript.addEventListener('load', resolve); + }); + } + }); + newScript.innerHTML = script.innerHTML; + script.remove(); + document.body.append(newScript); + + // eslint-disable-next-line no-await-in-loop + await waitForLoad; + } + const inputs = block.querySelectorAll('input'); inputs.forEach((formEl) => { formEl.placeholder = i18n(formEl.placeholder); diff --git a/blocks/contact-form/forms/contact-us.html b/blocks/contact-form/forms/contact-us.html index 4fc8aab7..8a301a30 100644 --- a/blocks/contact-form/forms/contact-us.html +++ b/blocks/contact-form/forms/contact-us.html @@ -1,3 +1,14 @@ +
@@ -52,18 +63,7 @@
-
-
- - -
- -
+
diff --git a/scripts/delayed.js b/scripts/delayed.js index a1f80c5f..3c9d6e3d 100644 --- a/scripts/delayed.js +++ b/scripts/delayed.js @@ -18,3 +18,5 @@ async function loadAdobeLaunch() { } if (!window.location.host.includes('localhost')) await loadAdobeLaunch(); + +loadScript('https://www.google.com/recaptcha/api.js?onload=onloadCallback&render=explicit', { async: true, defer: true }); From ec90a7ec116fd8037c8d5428ac317cbc57237be2 Mon Sep 17 00:00:00 2001 From: Rob Rusher Date: Thu, 18 Apr 2024 15:43:02 -0600 Subject: [PATCH 15/33] contact complete --- blocks/contact-form/contact-form.js | 86 ++++++++++++----------- blocks/contact-form/forms/contact-us.html | 1 + 2 files changed, 45 insertions(+), 42 deletions(-) diff --git a/blocks/contact-form/contact-form.js b/blocks/contact-form/contact-form.js index 05567cd6..30878a6b 100644 --- a/blocks/contact-form/contact-form.js +++ b/blocks/contact-form/contact-form.js @@ -155,6 +155,11 @@ async function validateFormInputs(form) { phone.classList.add('error'); } + if (errors.length > 0) { + displayError(errors); + return false; + } + if (!errors.length) { const payload = `user_response=${encodeURIComponent(recaptchaToken)}`; const options = { @@ -183,12 +188,6 @@ async function validateFormInputs(form) { errors.push(i18n('Captcha verification failed.')); }); } - - if (errors.length > 0) { - displayError(errors); - return false; - } - console.log('validation passed'); return true; } @@ -218,45 +217,48 @@ const addForm = async (block) => { thankYou.classList.add('form-thank-you'); form.onsubmit = function handleSubmit(e) { e.preventDefault(); - if (oldSubmit(this)) { - const jsonData = addFranchiseData(this); - const headers = new Headers(); - if (this.id === 'team-inquiry') { - headers.append('Content-Type', 'application/x-www-form-urlencoded; charset=UTF-8'); - } else { - headers.append('Content-Type', 'application/json; charset=UTF-8'); - } - const { action, method } = this; - fetch(action, { - method, - headers, - body: jsonData, - credentials: 'include', - }).then((resp) => { - /* eslint-disable-next-line no-console */ - if (!resp.ok) console.error(`Form submission failed: ${resp.status} / ${resp.statusText}`); - const firstContent = thankYou.firstElementChild; - if (firstContent.tagName === 'A') { - // redirect to thank you page - window.location.href = firstContent.href; - } else { - // show thank you content - const btn = thankYou.querySelector('a'); - const sideModal = document.querySelector('.side-modal-form'); - if (btn && sideModal) { - btn.setAttribute('href', '#'); - btn.addEventListener('click', (event) => { - event.preventDefault(); - hideSideModal(); - }); - sideModal?.replaceChildren(thankYou); + oldSubmit(this) + .then((result) => { + if (result) { + const jsonData = addFranchiseData(this); + const headers = new Headers(); + if (this.id === 'team-inquiry') { + headers.append('Content-Type', 'application/x-www-form-urlencoded; charset=UTF-8'); } else { - block.replaceChildren(thankYou); - block.parentNode.nextSibling.remove(); + headers.append('Content-Type', 'application/json; charset=UTF-8'); } + const { action, method } = this; + fetch(action, { + method, + headers, + body: jsonData, + credentials: 'include', + }).then((resp) => { + /* eslint-disable-next-line no-console */ + if (!resp.ok) console.error(`Form submission failed: ${resp.status} / ${resp.statusText}`); + const firstContent = thankYou.firstElementChild; + if (firstContent.tagName === 'A') { + // redirect to thank you page + window.location.href = firstContent.href; + } else { + // show thank you content + const btn = thankYou.querySelector('a'); + const sideModal = document.querySelector('.side-modal-form'); + if (btn && sideModal) { + btn.setAttribute('href', '#'); + btn.addEventListener('click', (event) => { + event.preventDefault(); + hideSideModal(); + }); + sideModal?.replaceChildren(thankYou); + } else { + block.replaceChildren(thankYou); + block.parentNode.nextSibling.remove(); + } + } + }); } }); - } return false; }; } @@ -359,5 +361,5 @@ export default async function decorate(block) { }, { rootMargin: '300px', }); - observer.observe(block); + await observer.observe(block); } diff --git a/blocks/contact-form/forms/contact-us.html b/blocks/contact-form/forms/contact-us.html index 8a301a30..fb753d25 100644 --- a/blocks/contact-form/forms/contact-us.html +++ b/blocks/contact-form/forms/contact-us.html @@ -2,6 +2,7 @@ var verifyCallback = function(response) { recaptchaToken = response; }; + var onloadCallback = function() { grecaptcha.render('captcha-20285', { 'sitekey' : '6LebYaYUAAAAAC9SqASljwaF57MpKSvEkwDOzk6l', From 74b78e97cfe3ac1bc1e52f25886cf291f85637b4 Mon Sep 17 00:00:00 2001 From: Rob Rusher Date: Thu, 18 Apr 2024 18:59:15 -0600 Subject: [PATCH 16/33] added consumerID cookie and finished up reCaptcha verification --- blocks/contact-form/contact-form.js | 71 ++++++++++++-------- blocks/contact-form/forms/callback.js | 12 ++++ blocks/contact-form/forms/contact-us.html | 14 +--- blocks/contact-form/forms/join-our-team.html | 2 +- scripts/delayed.js | 23 ++++++- 5 files changed, 78 insertions(+), 44 deletions(-) create mode 100644 blocks/contact-form/forms/callback.js diff --git a/blocks/contact-form/contact-form.js b/blocks/contact-form/contact-form.js index 30878a6b..4edc4353 100644 --- a/blocks/contact-form/contact-form.js +++ b/blocks/contact-form/contact-form.js @@ -1,10 +1,13 @@ +import { loadScript } from '../../scripts/aem.js'; import { hideSideModal, i18nLookup, getCookieValue } from '../../scripts/util.js'; const LOGIN_ERROR = 'There was a problem processing your request.'; const i18n = await i18nLookup(); const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; const phoneRegex = /^[+]?[ (]?\d{3}[)]?[-.\s]?\d{3}[-.\s]?\d{4}$/; -let recaptchaToken = null; + +// Load reCaptcha script used on all forms. +loadScript('./blocks/contact-form/forms/callback.js'); /** * Adds form and cookie values to payload. @@ -155,38 +158,48 @@ async function validateFormInputs(form) { phone.classList.add('error'); } - if (errors.length > 0) { - displayError(errors); - return false; + if (form.id === 'team-inquiry') { + const numAgentsEl = form.querySelector('input[name="numOfAgents"]'); + if (!numAgentsEl.value || numAgentsEl.value.trim().length === 0) { + errors.push(i18n('Number of agents is required.')); + numAgentsEl.classList.add('error'); + } + const cgiEl = form.querySelector('input[name="CGI"]'); + if (!cgiEl.value || cgiEl.value.trim().length === 0) { + errors.push(i18n('CGI in USD is required.')); + cgiEl.classList.add('error'); + } } if (!errors.length) { - const payload = `user_response=${encodeURIComponent(recaptchaToken)}`; - const options = { - method: 'POST', - body: payload, - headers: { - 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8', - }, - }; - const url = 'https://www.commonmoves.com/bin/bhhs/googleRecaptchaServlet'; + if (recaptchaToken) { + const payload = `user_response=${encodeURIComponent(recaptchaToken)}`; + const options = { + method: 'POST', + body: payload, + headers: { + 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8', + }, + }; + const url = '/bin/bhhs/googleRecaptchaServlet'; + await fetch(url, options) + .then((data) => { + // Handle the response based on the success property + if (!data.ok) { + errors.push(i18n('Captcha verification is required.')); + } + }) + .catch(() => { + errors.push(i18n('Captcha verification failed.')); + }); + } else { + errors.push(i18n('Captcha verification is required.')); + } + } - fetch(url, options) - .then((response) => { - if (!response.ok) { - throw new Error('Network response was not ok'); - } - return response.json(); - }) - .then((data) => { - // Handle the response based on the success property - if (!data.success) { - errors.push(i18n('Captcha verification is required.')); - } - }) - .catch(() => { - errors.push(i18n('Captcha verification failed.')); - }); + if (errors.length > 0) { + displayError(errors); + return false; } return true; } diff --git a/blocks/contact-form/forms/callback.js b/blocks/contact-form/forms/callback.js new file mode 100644 index 00000000..5ebb5c1d --- /dev/null +++ b/blocks/contact-form/forms/callback.js @@ -0,0 +1,12 @@ +let recaptchaToken = null; + +function verifyCallback(resp) { + recaptchaToken = resp; +} + +function onloadCallback() { + grecaptcha.render('captcha-20285', { + sitekey: '6LebYaYUAAAAAC9SqASljwaF57MpKSvEkwDOzk6l', + callback: verifyCallback, + }); +} \ No newline at end of file diff --git a/blocks/contact-form/forms/contact-us.html b/blocks/contact-form/forms/contact-us.html index fb753d25..40c4fd3f 100644 --- a/blocks/contact-form/forms/contact-us.html +++ b/blocks/contact-form/forms/contact-us.html @@ -1,18 +1,6 @@ -
+ action="/bin/bhhs/websiteTopicServlet">
diff --git a/blocks/contact-form/forms/join-our-team.html b/blocks/contact-form/forms/join-our-team.html index 392ac04b..fa3de2a4 100644 --- a/blocks/contact-form/forms/join-our-team.html +++ b/blocks/contact-form/forms/join-our-team.html @@ -1,6 +1,6 @@
+ action="/bin/bhhs/InquiryFormServlet">
diff --git a/scripts/delayed.js b/scripts/delayed.js index 3c9d6e3d..56eeb655 100644 --- a/scripts/delayed.js +++ b/scripts/delayed.js @@ -1,6 +1,6 @@ // eslint-disable-next-line import/no-cycle import { sampleRUM, loadScript } from './aem.js'; -import { getEnvType } from './util.js'; +import { getCookieValue, getEnvType } from './util.js'; // Core Web Vitals RUM collection sampleRUM('cwv'); @@ -17,6 +17,27 @@ async function loadAdobeLaunch() { }); } +async function loadIDServlet() { + const sessionID = getCookieValue('XSESSIONID'); + const options = { + method: 'POST', + body: `sameAs=%7B%22cregcontactid%22%3A%22${sessionID}`, + headers: { + 'Content-Type': 'application/json;charset=utf-8', + }, + }; + const url = '/bin/bhhs/graphIdServlet'; + const resp = await fetch(url, options); + if (resp.ok) { + const id = await resp.json(); + const idString = JSON.stringify(id); + document.cookie = `consumerID=${idString}`; + } +} + if (!window.location.host.includes('localhost')) await loadAdobeLaunch(); +if (!getCookieValue('consumerID')) { + loadIDServlet(); +} loadScript('https://www.google.com/recaptcha/api.js?onload=onloadCallback&render=explicit', { async: true, defer: true }); From b0fad8c763a42e0d6ed2a6da709a05b106208356 Mon Sep 17 00:00:00 2001 From: Rob Rusher Date: Fri, 19 Apr 2024 11:28:15 -0600 Subject: [PATCH 17/33] reset recaptcha token --- blocks/contact-form/contact-form.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/blocks/contact-form/contact-form.js b/blocks/contact-form/contact-form.js index 4edc4353..eb309284 100644 --- a/blocks/contact-form/contact-form.js +++ b/blocks/contact-form/contact-form.js @@ -268,6 +268,9 @@ const addForm = async (block) => { block.replaceChildren(thankYou); block.parentNode.nextSibling.remove(); } + if (window.grecaptcha) { + recaptchaToken = null; + } } }); } From be268a527a8e52a5ab15230dd79e4bcedb64d4b5 Mon Sep 17 00:00:00 2001 From: Rob Rusher Date: Fri, 19 Apr 2024 11:38:25 -0600 Subject: [PATCH 18/33] disable some lint errors --- blocks/contact-form/contact-form.js | 1 + blocks/contact-form/forms/callback.js | 4 +++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/blocks/contact-form/contact-form.js b/blocks/contact-form/contact-form.js index eb309284..dc3baf52 100644 --- a/blocks/contact-form/contact-form.js +++ b/blocks/contact-form/contact-form.js @@ -171,6 +171,7 @@ async function validateFormInputs(form) { } } + /* eslint-disable no-undef */ if (!errors.length) { if (recaptchaToken) { const payload = `user_response=${encodeURIComponent(recaptchaToken)}`; diff --git a/blocks/contact-form/forms/callback.js b/blocks/contact-form/forms/callback.js index 5ebb5c1d..3e7b9e53 100644 --- a/blocks/contact-form/forms/callback.js +++ b/blocks/contact-form/forms/callback.js @@ -1,3 +1,5 @@ +/* eslint-disable no-unused-vars */ +/* eslint-disable no-undef */ let recaptchaToken = null; function verifyCallback(resp) { @@ -9,4 +11,4 @@ function onloadCallback() { sitekey: '6LebYaYUAAAAAC9SqASljwaF57MpKSvEkwDOzk6l', callback: verifyCallback, }); -} \ No newline at end of file +} From 94844097c696a6881becb8bdc1b92a997c70dddd Mon Sep 17 00:00:00 2001 From: Rob Rusher Date: Fri, 19 Apr 2024 11:48:52 -0600 Subject: [PATCH 19/33] adjust for CLS --- blocks/contact-form/contact-form.css | 1 + 1 file changed, 1 insertion(+) diff --git a/blocks/contact-form/contact-form.css b/blocks/contact-form/contact-form.css index bf253409..8450563c 100644 --- a/blocks/contact-form/contact-form.css +++ b/blocks/contact-form/contact-form.css @@ -142,6 +142,7 @@ .contact-form.block .contact-form .g-recaptcha { padding: 3rem 0; + height: 174px; } .contact-form.block form.contact-form .cta { From 3d4ac1716e6da640e672896b0ee3f50ee8a90d7b Mon Sep 17 00:00:00 2001 From: Rob Rusher Date: Fri, 19 Apr 2024 14:22:23 -0600 Subject: [PATCH 20/33] add fragment block --- blocks/fragment/fragment.css | 13 +++++++++ blocks/fragment/fragment.js | 55 ++++++++++++++++++++++++++++++++++++ 2 files changed, 68 insertions(+) create mode 100644 blocks/fragment/fragment.css create mode 100644 blocks/fragment/fragment.js diff --git a/blocks/fragment/fragment.css b/blocks/fragment/fragment.css new file mode 100644 index 00000000..b38cf896 --- /dev/null +++ b/blocks/fragment/fragment.css @@ -0,0 +1,13 @@ +/* suppress nested section padding */ +.fragment-wrapper > .section { + padding-left: 0; + padding-right: 0; +} + +.fragment-wrapper > .section:first-of-type { + padding-top: 0; +} + +.fragment-wrapper > .section:last-of-type { + padding-bottom: 0; +} diff --git a/blocks/fragment/fragment.js b/blocks/fragment/fragment.js new file mode 100644 index 00000000..648a70e1 --- /dev/null +++ b/blocks/fragment/fragment.js @@ -0,0 +1,55 @@ +/* + * Fragment Block + * Include content on a page as a fragment. + * https://www.aem.live/developer/block-collection/fragment + */ + +import { + decorateMain, +} from '../../scripts/scripts.js'; + +import { + loadBlocks, +} from '../../scripts/aem.js'; + +/** + * Loads a fragment. + * @param {string} path The path to the fragment + * @returns {HTMLElement} The root element of the fragment + */ +export async function loadFragment(path) { + if (path && path.startsWith('/')) { + const resp = await fetch(`${path}.plain.html`); + if (resp.ok) { + const main = document.createElement('main'); + main.innerHTML = await resp.text(); + + // reset base path for media to fragment base + const resetAttributeBase = (tag, attr) => { + main.querySelectorAll(`${tag}[${attr}^="./media_"]`).forEach((elem) => { + elem[attr] = new URL(elem.getAttribute(attr), new URL(path, window.location)).href; + }); + }; + resetAttributeBase('img', 'src'); + resetAttributeBase('source', 'srcset'); + + decorateMain(main); + await loadBlocks(main); + return main; + } + } + return null; +} + +export default async function decorate(block) { + const link = block.querySelector('a'); + const path = link ? link.getAttribute('href') : block.textContent.trim(); + const fragment = await loadFragment(path); + if (fragment) { + const fragmentSection = fragment.querySelector(':scope .section'); + if (fragmentSection) { + block.closest('.section').classList.add(...fragmentSection.classList); + block.closest('.fragment').replaceWith(...fragment.childNodes); + } + } +} From 0dd9b6d68ff0ec9db8a2775976e3d348c47d8072 Mon Sep 17 00:00:00 2001 From: Rob Rusher Date: Fri, 19 Apr 2024 14:56:07 -0600 Subject: [PATCH 21/33] adjust disclaimer css --- blocks/fragment/fragment.css | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/blocks/fragment/fragment.css b/blocks/fragment/fragment.css index b38cf896..efdf8a01 100644 --- a/blocks/fragment/fragment.css +++ b/blocks/fragment/fragment.css @@ -11,3 +11,10 @@ .fragment-wrapper > .section:last-of-type { padding-bottom: 0; } + +.fragment-wrapper .disclaimer div p { + font-family: var(--font-family-primary); + font-size: var(--body-font-size-xxs); + line-height: var(--line-height-s); + color: var(--dark-grey); +} From eb5c07da46b366398e52045a43ad04f1f468f39f Mon Sep 17 00:00:00 2001 From: Rob Rusher Date: Mon, 13 May 2024 17:25:54 -0600 Subject: [PATCH 22/33] use placeholder and limit script load --- blocks/contact-form/forms/callback.js | 2 +- scripts/delayed.js | 6 ++++-- scripts/util.js | 1 - 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/blocks/contact-form/forms/callback.js b/blocks/contact-form/forms/callback.js index 3e7b9e53..5712dce9 100644 --- a/blocks/contact-form/forms/callback.js +++ b/blocks/contact-form/forms/callback.js @@ -8,7 +8,7 @@ function verifyCallback(resp) { function onloadCallback() { grecaptcha.render('captcha-20285', { - sitekey: '6LebYaYUAAAAAC9SqASljwaF57MpKSvEkwDOzk6l', + sitekey: window.placeholders.default.recaptchaSitekey, callback: verifyCallback, }); } diff --git a/scripts/delayed.js b/scripts/delayed.js index 706fd9d6..ba3feb1c 100644 --- a/scripts/delayed.js +++ b/scripts/delayed.js @@ -35,9 +35,11 @@ async function loadIDServlet() { } } -if (!window.location.host.includes('localhost')) await loadAdobeLaunch(); +if (!window.location.host.includes('localhost')) loadAdobeLaunch(); if (!getCookieValue('consumerID')) { loadIDServlet(); } -loadScript('https://www.google.com/recaptcha/api.js?onload=onloadCallback&render=explicit', { async: true, defer: true }); +if (window.location.pathname.startsWith('/contact-us') || window.location.pathname.startsWith('/search')) { + loadScript('https://www.google.com/recaptcha/api.js?onload=onloadCallback&render=explicit', { async: true, defer: true }); +} diff --git a/scripts/util.js b/scripts/util.js index c79e7774..93130afe 100644 --- a/scripts/util.js +++ b/scripts/util.js @@ -134,7 +134,6 @@ export function getEnvType(hostname = window.location.hostname) { return fqdnToEnvType[hostname] || 'dev'; } - /** * Retrieves the value of a cookie by its name. * From f3e4c0aa341a67cec3c9b61b8fa8bedd5545e58b Mon Sep 17 00:00:00 2001 From: Rob Rusher Date: Wed, 15 May 2024 11:09:37 -0600 Subject: [PATCH 23/33] recatpcha update --- blocks/contact-form/forms/contact-property.html | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/blocks/contact-form/forms/contact-property.html b/blocks/contact-form/forms/contact-property.html index cba6dcb6..c05aa0cb 100644 --- a/blocks/contact-form/forms/contact-property.html +++ b/blocks/contact-form/forms/contact-property.html @@ -3,7 +3,7 @@

Contact Us

Direct:
- +
@@ -56,16 +56,6 @@

Contact Us

-
- - -
-
From 3548c33d71972d2956f156ac1ac874020fbf3dc8 Mon Sep 17 00:00:00 2001 From: Rob Rusher Date: Wed, 15 May 2024 14:17:47 -0600 Subject: [PATCH 24/33] clean up form html, correct url to luxury listing --- blocks/contact-form/contact-form.js | 2 +- blocks/contact-form/forms/contact-property.html | 2 +- blocks/contact-form/forms/join-our-team.html | 2 +- blocks/hero-slides/hero-slides.js | 2 +- scripts/delayed.js | 5 ++++- 5 files changed, 8 insertions(+), 5 deletions(-) diff --git a/blocks/contact-form/contact-form.js b/blocks/contact-form/contact-form.js index dc3baf52..02c82bfa 100644 --- a/blocks/contact-form/contact-form.js +++ b/blocks/contact-form/contact-form.js @@ -7,7 +7,7 @@ const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; const phoneRegex = /^[+]?[ (]?\d{3}[)]?[-.\s]?\d{3}[-.\s]?\d{4}$/; // Load reCaptcha script used on all forms. -loadScript('./blocks/contact-form/forms/callback.js'); +loadScript('/blocks/contact-form/forms/callback.js'); /** * Adds form and cookie values to payload. diff --git a/blocks/contact-form/forms/contact-property.html b/blocks/contact-form/forms/contact-property.html index c05aa0cb..45111aa2 100644 --- a/blocks/contact-form/forms/contact-property.html +++ b/blocks/contact-form/forms/contact-property.html @@ -55,7 +55,7 @@

Contact Us

-
+
diff --git a/blocks/contact-form/forms/join-our-team.html b/blocks/contact-form/forms/join-our-team.html index fa3de2a4..a490033f 100644 --- a/blocks/contact-form/forms/join-our-team.html +++ b/blocks/contact-form/forms/join-our-team.html @@ -776,7 +776,7 @@ placeholder="Leave a comment or question and we'll get back to you shortly." autocomplete="off">
-
+
diff --git a/blocks/hero-slides/hero-slides.js b/blocks/hero-slides/hero-slides.js index ff46460c..317923f4 100644 --- a/blocks/hero-slides/hero-slides.js +++ b/blocks/hero-slides/hero-slides.js @@ -2,7 +2,7 @@ import { createOptimizedPicture, readBlockConfig } from '../../scripts/aem.js'; // eslint-disable-next-line no-unused-vars async function fetchListings(config) { - const resp = await fetch(`${window.hlx.codeBasePath}/drafts/rrusher/listings.json`); + const resp = await fetch(`${window.hlx.codeBasePath}/luxury-listings.json`); // eslint-disable-next-line no-return-await return (await resp.json()).data; } diff --git a/scripts/delayed.js b/scripts/delayed.js index ba3feb1c..c12024d8 100644 --- a/scripts/delayed.js +++ b/scripts/delayed.js @@ -40,6 +40,9 @@ if (!getCookieValue('consumerID')) { loadIDServlet(); } -if (window.location.pathname.startsWith('/contact-us') || window.location.pathname.startsWith('/search')) { +// Load reCAPTCHA script on specific pages that have forms (including sidebar forms) +const paths = ['/contact-us', '/search', '/careers', '/luxury-collection']; + +if (paths.some((path) => window.location.pathname.startsWith(path))) { loadScript('https://www.google.com/recaptcha/api.js?onload=onloadCallback&render=explicit', { async: true, defer: true }); } From 4061119dd2a2141eaf0bc260741e6126a81bfa28 Mon Sep 17 00:00:00 2001 From: Rob Rusher Date: Fri, 17 May 2024 16:27:36 -0600 Subject: [PATCH 25/33] reduced script loading and added rerenderer for recaptcha --- blocks/contact-form/contact-form.css | 32 ++++++++++++- blocks/contact-form/contact-form.js | 13 ++++-- blocks/contact-form/forms/callback.js | 9 ++++ .../contact-form/forms/contact-property.html | 8 ++-- blocks/property-listing/cards/cards.js | 46 ++++++++++--------- scripts/delayed.js | 7 --- scripts/scripts.js | 21 ++++++++- scripts/util.js | 5 +- 8 files changed, 99 insertions(+), 42 deletions(-) diff --git a/blocks/contact-form/contact-form.css b/blocks/contact-form/contact-form.css index 8450563c..c9b0ba23 100644 --- a/blocks/contact-form/contact-form.css +++ b/blocks/contact-form/contact-form.css @@ -1,7 +1,35 @@ -.contact-form.block form, +.contact-form.block, +.contact-form.block form { + font-family: var(--font-family-primary); +} + +.contact-form.block .form-title { + font-size: var(--heading-font-size-l); + line-height: var(--line-height-xs); + font-weight: 700; + margin-top: 30px; + margin-left: 40px; + margin-bottom: 3em; +} + +.contact-form.block .company-name { + font-size: var(--heading-font-size-m); + line-height: var(--line-height-xs); + font-weight: 800; + margin-bottom: .5em; +} + .contact-form.block .company-email, .contact-form.block .company-phone { - font-family: var(--font-family-proxima); + padding-bottom: .5em; +} + +.contact-form.block a { + color: var(--body-color); +} + +.contact-form.block form#property-contact { + margin-top: 3rem; } .contact-form.block form.contact-form .message { diff --git a/blocks/contact-form/contact-form.js b/blocks/contact-form/contact-form.js index 02c82bfa..d38dfff2 100644 --- a/blocks/contact-form/contact-form.js +++ b/blocks/contact-form/contact-form.js @@ -1,5 +1,5 @@ import { loadScript } from '../../scripts/aem.js'; -import { hideSideModal, i18nLookup, getCookieValue } from '../../scripts/util.js'; +import { removeSideModal, i18nLookup, getCookieValue } from '../../scripts/util.js'; const LOGIN_ERROR = 'There was a problem processing your request.'; const i18n = await i18nLookup(); @@ -262,7 +262,7 @@ const addForm = async (block) => { btn.setAttribute('href', '#'); btn.addEventListener('click', (event) => { event.preventDefault(); - hideSideModal(); + removeSideModal(); }); sideModal?.replaceChildren(thankYou); } else { @@ -323,7 +323,7 @@ const addForm = async (block) => { cancelBtn.addEventListener('click', (e) => { e.preventDefault(); e.stopPropagation(); - hideSideModal(); + removeSideModal(); }); } @@ -367,6 +367,13 @@ const addForm = async (block) => { } }); }); + + if (window.grecaptcha) { + recaptchaToken = null; + renderRecaptcha(); + } else { + loadScript('https://www.google.com/recaptcha/api.js?onload=onloadCallback&render=explicit', { async: true, defer: true }); + } }; export default async function decorate(block) { diff --git a/blocks/contact-form/forms/callback.js b/blocks/contact-form/forms/callback.js index 5712dce9..87e1b35e 100644 --- a/blocks/contact-form/forms/callback.js +++ b/blocks/contact-form/forms/callback.js @@ -12,3 +12,12 @@ function onloadCallback() { callback: verifyCallback, }); } + +function renderRecaptcha() { + if (document.getElementById('captcha-20285')) { + grecaptcha.render('captcha-20285', { + sitekey: window.placeholders.default.recaptchaSitekey, + callback: verifyCallback, + }); + } +} diff --git a/blocks/contact-form/forms/contact-property.html b/blocks/contact-form/forms/contact-property.html index 45111aa2..63039424 100644 --- a/blocks/contact-form/forms/contact-property.html +++ b/blocks/contact-form/forms/contact-property.html @@ -1,8 +1,8 @@
-

Contact Us

-
-
-
Direct:
+
Contact Us
+
Berkshire Hathaway HomeServices
Commonwealth Real Estate
+ +
diff --git a/blocks/property-listing/cards/cards.js b/blocks/property-listing/cards/cards.js index d690fc3e..bd1d8109 100644 --- a/blocks/property-listing/cards/cards.js +++ b/blocks/property-listing/cards/cards.js @@ -1,5 +1,6 @@ import { propertySearch } from '../../../scripts/apis/creg/creg.js'; import { decorateIcons } from '../../../scripts/aem.js'; +import { decorateFormLinks } from '../../../scripts/scripts.js'; function createImage(listing) { if (listing.SmallMedia?.length > 0) { @@ -50,9 +51,9 @@ export function createCard(listing) { item.innerHTML = ` -
-
- ${createImage(listing)} +
+
+ ${createImage(listing)}
@@ -63,7 +64,7 @@ export function createCard(listing) {
-
+
Featured Listing ${applicationType} @@ -71,38 +72,38 @@ export function createCard(listing) {
${listing.ListPriceUS} -
-
+
+
-
-
+
+
Closed: ${listing.ClosedDate}
-
+
${listing.StreetName} -
- ${listing.City}, ${listing.StateOrProvince} ${listing.PostalCode} -
-
${specs.join(' / ')}
-
-
- +
+
-
+
-
+
Listing courtesy of: ${listing.CourtesyOf}
Listing provided by: ${listing.listAor}
@@ -132,6 +133,7 @@ export async function render(searchParams, parent) { list.append(createCard(listing)); }); decorateIcons(parent); + decorateFormLinks(parent); } }); } diff --git a/scripts/delayed.js b/scripts/delayed.js index c12024d8..83afb01c 100644 --- a/scripts/delayed.js +++ b/scripts/delayed.js @@ -39,10 +39,3 @@ if (!window.location.host.includes('localhost')) loadAdobeLaunch(); if (!getCookieValue('consumerID')) { loadIDServlet(); } - -// Load reCAPTCHA script on specific pages that have forms (including sidebar forms) -const paths = ['/contact-us', '/search', '/careers', '/luxury-collection']; - -if (paths.some((path) => window.location.pathname.startsWith(path))) { - loadScript('https://www.google.com/recaptcha/api.js?onload=onloadCallback&render=explicit', { async: true, defer: true }); -} diff --git a/scripts/scripts.js b/scripts/scripts.js index b0f15a05..9397d263 100644 --- a/scripts/scripts.js +++ b/scripts/scripts.js @@ -152,12 +152,29 @@ function decorateVideoLinks(main) { }); } -function decorateFormLinks(main) { +/** + * Finds the parent anchor tag of the given event target. + * @param {Event} event - The event object. + * @returns {HTMLAnchorElement|null} - The parent anchor tag, or null if not found. + */ +function findParentAnchorTag(event) { + let element = event.target; + while (element && element.nodeName !== 'A') { + element = element.parentNode; + } + return element; +} + +/** + * Decorates form links by attaching a click event listener to open a side modal. + * @param {HTMLElement} main - The main element containing the form links. + */ +export function decorateFormLinks(main) { async function openSideModal(event) { event.preventDefault(); const module = await import(`${window.hlx.codeBasePath}/blocks/side-modal/side-modal.js`); if (module.showSideModal) { - await module.showSideModal(event.target); + await module.showSideModal(findParentAnchorTag(event)); } } main.querySelectorAll('a[href*="form"]').forEach((a) => { diff --git a/scripts/util.js b/scripts/util.js index 93130afe..dc93dc14 100644 --- a/scripts/util.js +++ b/scripts/util.js @@ -52,9 +52,10 @@ export function showModal(content) { let sideModal; let focusElement; -export function hideSideModal() { +export function removeSideModal() { if (!sideModal) return; - sideModal.ariaExpanded = false; + sideModal.parentNode.remove(); + sideModal = null; document.body.classList.remove('disable-scroll'); if (focusElement) focusElement.focus(); } From 9903e6a11483291d6cd98ed336128a5c3e07bd14 Mon Sep 17 00:00:00 2001 From: Bryan Stopp Date: Mon, 20 May 2024 12:47:18 -0400 Subject: [PATCH 26/33] Fix bad merge. --- blocks/shared/property/cards.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/blocks/shared/property/cards.js b/blocks/shared/property/cards.js index e6dd4d95..f50bee1f 100644 --- a/blocks/shared/property/cards.js +++ b/blocks/shared/property/cards.js @@ -71,7 +71,7 @@ export function createCard(listing) { ${listing.mlsStatus}
- ${listing.ListPriceUS} +

${listing.ListPriceUS}

${listing.ListPriceUS}

From 686c115ae94f843d1d869b2eb24b1aac822aecf2 Mon Sep 17 00:00:00 2001 From: Bryan Stopp Date: Mon, 20 May 2024 12:48:24 -0400 Subject: [PATCH 27/33] Fix bad merge. --- blocks/shared/property/cards.js | 3 --- 1 file changed, 3 deletions(-) diff --git a/blocks/shared/property/cards.js b/blocks/shared/property/cards.js index f50bee1f..49096382 100644 --- a/blocks/shared/property/cards.js +++ b/blocks/shared/property/cards.js @@ -74,9 +74,6 @@ export function createCard(listing) {

${listing.ListPriceUS}

-

${listing.ListPriceUS}

-
-
From afa294e3a15beae23ff24a96cab26020522a7ffc Mon Sep 17 00:00:00 2001 From: Bryan Stopp Date: Mon, 20 May 2024 13:01:34 -0400 Subject: [PATCH 28/33] Fix lint. --- blocks/shared/property/cards.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/blocks/shared/property/cards.js b/blocks/shared/property/cards.js index 49096382..52afd8fc 100644 --- a/blocks/shared/property/cards.js +++ b/blocks/shared/property/cards.js @@ -1,5 +1,3 @@ -import { propertySearch } from '../../../scripts/apis/creg/creg.js'; -import { decorateIcons } from '../../../scripts/aem.js'; import { decorateFormLinks } from '../../../scripts/scripts.js'; function createImage(listing) { @@ -136,4 +134,5 @@ export function render(parent, properties = []) { cards.push(createCard(listing)); }); parent.replaceChildren(...cards); + decorateFormLinks(parent); } From 09a6237fbdc9b68509e1efb55fa589512d71074f Mon Sep 17 00:00:00 2001 From: Rob Rusher Date: Thu, 23 May 2024 12:24:11 -0600 Subject: [PATCH 29/33] update styles --- blocks/contact-form/contact-form.css | 7 +------ blocks/hero/hero.css | 11 +++++++++++ 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/blocks/contact-form/contact-form.css b/blocks/contact-form/contact-form.css index c9b0ba23..11d8e01c 100644 --- a/blocks/contact-form/contact-form.css +++ b/blocks/contact-form/contact-form.css @@ -114,7 +114,7 @@ line-height: 1; display: flex; justify-content: flex-start; - margin-bottom: 15px; + margin-bottom: 10px; cursor: pointer; } @@ -168,11 +168,6 @@ border: 2px solid var(--error); } -.contact-form.block .contact-form .g-recaptcha { - padding: 3rem 0; - height: 174px; -} - .contact-form.block form.contact-form .cta { padding-bottom: 2rem; } diff --git a/blocks/hero/hero.css b/blocks/hero/hero.css index 062f7e72..5f13b0cb 100644 --- a/blocks/hero/hero.css +++ b/blocks/hero/hero.css @@ -103,6 +103,17 @@ main .section.full-width > .hero-wrapper { color: var(--white); } +.hero.block > div .content .button-container a { + border: 1px solid; + color: var(--white); +} + +.hero.block > div .content .button-container a:hover { + background-color: var(--primary-light); + border: 1px solid var(--primary-light); + color: var(--primary-color); +} + .hero.block > .row > .headline { color: var(--white); font-size: var(--body-font-size-m); From cdda86c10d74835a46c911ba199a23f8b600a0b6 Mon Sep 17 00:00:00 2001 From: Rob Rusher Date: Thu, 23 May 2024 12:39:52 -0600 Subject: [PATCH 30/33] move selector for linting --- blocks/hero/hero.css | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/blocks/hero/hero.css b/blocks/hero/hero.css index a4ac050b..d52e7d8f 100644 --- a/blocks/hero/hero.css +++ b/blocks/hero/hero.css @@ -119,17 +119,6 @@ main .section.full-width > .hero-wrapper { z-index: 1; } -.hero.block > div .content .button-container a { - border: 1px solid; - color: var(--white); -} - -.hero.block > div .content .button-container a:hover { - background-color: var(--primary-light); - border: 1px solid var(--primary-light); - color: var(--primary-color); -} - .hero.block > .row > .headline { color: var(--white); font-size: var(--body-font-size-m); @@ -154,6 +143,17 @@ main .section.full-width > .hero-wrapper { color: var(--white); } +.hero.block > div .content .button-container a { + border: 1px solid; + color: var(--white); +} + +.hero.block > div .content .button-container a:hover { + background-color: var(--primary-light); + border: 1px solid var(--primary-light); + color: var(--primary-color); +} + @media screen and (min-width: 600px) { .hero.block { height: 620px; From 8d7d84edc7aa80a3eb005c84aac99f169bd519bd Mon Sep 17 00:00:00 2001 From: Rob Rusher Date: Wed, 29 May 2024 13:56:16 -0600 Subject: [PATCH 31/33] Update fragment.js removed padding lint --- blocks/fragment/fragment.js | 1 - 1 file changed, 1 deletion(-) diff --git a/blocks/fragment/fragment.js b/blocks/fragment/fragment.js index 3cb40df8..da92d8bb 100644 --- a/blocks/fragment/fragment.js +++ b/blocks/fragment/fragment.js @@ -18,7 +18,6 @@ import { * @returns {HTMLElement} The root element of the fragment */ export async function loadFragment(path) { - if (path?.startsWith('/')) { const resp = await fetch(`${path}.plain.html`); if (resp.ok) { From bdfabb71b7057473edb5444f0b6d071622e06bc7 Mon Sep 17 00:00:00 2001 From: Rob Rusher Date: Fri, 31 May 2024 10:30:15 -0600 Subject: [PATCH 32/33] remove fragment css --- blocks/fragment/fragment.css | 21 +-------------------- 1 file changed, 1 insertion(+), 20 deletions(-) diff --git a/blocks/fragment/fragment.css b/blocks/fragment/fragment.css index efdf8a01..b3c58707 100644 --- a/blocks/fragment/fragment.css +++ b/blocks/fragment/fragment.css @@ -1,20 +1 @@ -/* suppress nested section padding */ -.fragment-wrapper > .section { - padding-left: 0; - padding-right: 0; -} - -.fragment-wrapper > .section:first-of-type { - padding-top: 0; -} - -.fragment-wrapper > .section:last-of-type { - padding-bottom: 0; -} - -.fragment-wrapper .disclaimer div p { - font-family: var(--font-family-primary); - font-size: var(--body-font-size-xxs); - line-height: var(--line-height-s); - color: var(--dark-grey); -} +/* stylelint-disable-next-line no-empty-source */ From 0aa9ffa2480accaac93927695eb0b1787222ea47 Mon Sep 17 00:00:00 2001 From: Rob Rusher Date: Fri, 31 May 2024 13:30:57 -0600 Subject: [PATCH 33/33] move disclaimer css to column --- blocks/columns/columns.css | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/blocks/columns/columns.css b/blocks/columns/columns.css index 4a63594e..b8aa5596 100644 --- a/blocks/columns/columns.css +++ b/blocks/columns/columns.css @@ -48,6 +48,12 @@ order: 1; } +.columns.block.disclaimer div { + font-size: var(--body-font-size-xxs); + line-height: var(--line-height-s); + color: var(--dark-grey); +} + .columns.columns-2-cols > div > div:first-of-type { margin-bottom: 24px; } @@ -66,7 +72,6 @@ .columns.block h3 { margin-bottom: 24px; - } @media (min-width: 600px) {