diff --git a/README.md b/README.md index 90c552b7..453a7b48 100644 --- a/README.md +++ b/README.md @@ -76,7 +76,7 @@ In this setup, the proxy is configured to route non-API traffic on the Staging d This setup uses a locally resolved domain, which will Proxyman will use to route the traffic. All non-API traffic will route to localhost, all API traffic will go to the Stage domain. -1. Add an entry to `/etc/maps`: +1. Add an entry to `/etc/hosts`: Proxyman won't proxy localhost, so a custom domain is required. Add the following (if you already have a host entry for 127.0.0.1, simply add the new domain) > 127.0.0.1 proxyman.debug diff --git a/blocks/agent-about/agent-about.css b/blocks/agent-about/agent-about.css index 49b456d0..29fbbb77 100644 --- a/blocks/agent-about/agent-about.css +++ b/blocks/agent-about/agent-about.css @@ -7,10 +7,6 @@ letter-spacing: normal; } -.agent-about.block a { - cursor: pointer; -} - .agent-about.block .hide { display: none; } @@ -20,6 +16,8 @@ display: inline-block; margin-top: 1rem; text-decoration: underline; + font-size: var(--body-font-size-xs); + color: var(--black); } .agent-about.block a.view-more::after { @@ -33,7 +31,7 @@ .agent-about.block>div.cols-1, .agent-about.block>div.cols-2, .agent-about.block>div.cols-3 { - padding-bottom: 1rem; + padding-bottom: 2rem; } .agent-about.block>div>div:first-of-type { diff --git a/blocks/agent-about/agent-about.js b/blocks/agent-about/agent-about.js index 80949538..fabeecae 100644 --- a/blocks/agent-about/agent-about.js +++ b/blocks/agent-about/agent-about.js @@ -1,3 +1,4 @@ +import { getMetadata } from '../../scripts/aem.js'; import { a, div, ul, li, } from '../../scripts/dom-helpers.js'; @@ -18,7 +19,23 @@ const viewMoreOnClick = (name, anchor, block) => { }); }; +const getCol = (list, colText) => { + const colsUl = ul(); + list.split(',').forEach((x) => { + colsUl.append(li(x.trim())); + }); + return div(div(colText), div(colsUl)); +}; + export default function decorate(block) { + const aboutText = getMetadata('about'); + const accreditations = getMetadata('professional-accreditations'); + const languages = getMetadata('languages'); + + block.replaceChildren(div(div('About'), div(aboutText)), + getCol(accreditations, 'Professional Accreditations'), + getCol(languages, 'Languages')); + const children = [...block.children]; if (children?.length) { children.forEach((child, index) => { @@ -31,7 +48,7 @@ export default function decorate(block) { child.children[1].classList.add('hide'); child.append(div({ class: `${name}-truncate` }, `${child.children[1].textContent.substring(0, threshold)}...`)); - const anchor = a({ class: 'view-more' }); + const anchor = a({ class: 'view-more', href: '#' }); child.append(anchor); viewMoreOnClick(name, anchor, block); } @@ -43,15 +60,15 @@ export default function decorate(block) { if (liItems.length > threshold) { child.children[1].classList.add('hide'); - const tempUl = ul({ }); + const tempUl = ul(); Array.from(child.children[1].querySelectorAll('li')) .slice(0, threshold).forEach((liItem) => { - const tempLi = li({}, liItem.textContent); + const tempLi = li(liItem.textContent); tempUl.append(tempLi); }); child.append(div({ class: `${name}-truncate` }, tempUl)); - const anchor = a({ class: 'view-more' }); + const anchor = a({ class: 'view-more', href: '#' }); child.append(anchor); viewMoreOnClick(name, anchor, block); } diff --git a/blocks/agent-address/agent-address.css b/blocks/agent-address/agent-address.css new file mode 100644 index 00000000..bc074896 --- /dev/null +++ b/blocks/agent-address/agent-address.css @@ -0,0 +1,35 @@ +.agent-address.block { + padding: 2rem; + background-color: var(--tertiary-color); +} + +.agent-address.block .address { + margin-bottom: 2rem; +} + +.agent-address.block .address>p { + margin-bottom: 0; + font-size: var(--body-font-size-xs); +} + +.agent-address.block a { + border: 1px solid var(--primary-color); + color: var(--primary-color); + font-weight: var(--font-weight-bold); + letter-spacing: var(--letter-spacing-m); + text-transform: uppercase; + padding: 0.5rem 1rem; + text-decoration: none; + font-size: var(--body-font-size-s); +} + +.agent-address.block a:hover { + color: var(--primary-light); + background-color: var(--primary-color); +} + +@media (min-width: 600px) { + .agent-address.block { + display: none; + } +} diff --git a/blocks/agent-address/agent-address.js b/blocks/agent-address/agent-address.js new file mode 100644 index 00000000..187419f0 --- /dev/null +++ b/blocks/agent-address/agent-address.js @@ -0,0 +1,22 @@ +import { getMetadata } from '../../scripts/aem.js'; +import { + a, div, p, +} from '../../scripts/dom-helpers.js'; + +export default function decorate(block) { + const streetAddress = getMetadata('streetaddress'); + const addressLocality = getMetadata('addresslocality'); + const addressRegion = getMetadata('addressregion'); + const postalCode = getMetadata('postalcode'); + + const textDiv = div({ class: 'address' }, + p('Berkshire Hathaway HomeServices'), + p('Commonwealth Real Estate'), + p(streetAddress), + p(`${addressLocality}, ${addressRegion} ${postalCode}`), + ); + const text = `${streetAddress}, ${addressLocality}, ${addressRegion} ${postalCode}`; + + const anchor = a({ href: `https://maps.google.com/maps?q=${text}`, target: '_blank' }, 'Directions'); + block.replaceChildren(textDiv, anchor); +} diff --git a/blocks/agent-profile/agent-profile.js b/blocks/agent-profile/agent-profile.js index 61f963b7..191b685f 100644 --- a/blocks/agent-profile/agent-profile.js +++ b/blocks/agent-profile/agent-profile.js @@ -8,13 +8,13 @@ const getPhoneDiv = () => { let phoneUl; if (getMetadata('direct-phone')) { - phoneUl = ul({}); - phoneUl.append(li({}, 'Direct: ', getMetadata('direct-phone'))); + phoneUl = ul(); + phoneUl.append(li('Direct: ', getMetadata('direct-phone'))); } if (getMetadata('office-phone')) { - phoneUl = phoneUl || ul({}); - phoneUl.append(li({}, 'Office: ', getMetadata('office-phone'))); + phoneUl = phoneUl || ul(); + phoneUl.append(li('Office: ', getMetadata('office-phone'))); } if (phoneUl) { @@ -62,9 +62,9 @@ const getSocialDiv = () => { ['facebook', 'instagram', 'linkedin'].forEach((x) => { const url = getMetadata(x); - socialUl = socialUl || ul({}); + socialUl = socialUl || ul(); if (url) { - const socialLi = li({}, a({ + const socialLi = li(a({ href: url, class: x, title: x, 'aria-label': x, }, span({ class: `icon icon-${x}` }))); socialUl.append(socialLi); @@ -82,7 +82,7 @@ const getSocialDiv = () => { export default async function decorate(block) { const profileImage = getImageDiv(); const profileContent = div({ class: 'profile-content' }, - div({ class: 'name' }, h1({}, getMetadata('name'))), + div({ class: 'name' }, h1(getMetadata('name'))), div({ class: 'designation' }, getMetadata('designation')), ); diff --git a/blocks/agent-transactions/agent-transactions.css b/blocks/agent-transactions/agent-transactions.css new file mode 100644 index 00000000..061fc67c --- /dev/null +++ b/blocks/agent-transactions/agent-transactions.css @@ -0,0 +1,157 @@ +.agent-transactions.block table { + width: 100%; + font-size: var(--body-font-size-s); + line-height: var(--line-height-m); + margin-bottom: 0.25rem; +} + +.agent-transactions.block h1 { + font-size: var(--heading-font-size-l); + line-height: var(--line-height-s); + color: var(--primary-color); + margin: 0 1.875rem 1.5rem 0; + font-weight: 600; +} + +.agent-transactions.block .hide { + display: none; +} + +.agent-transactions.block .show { + display: table-row; +} + +.agent-transactions.block thead { + background: var(--platinum); + font-size: var(--body-font-size-xs); + line-height: var(--line-height-m); + font-weight: var(--font-weight-bold); + text-transform: capitalize; +} + +.agent-transactions.block tbody td { + padding-bottom: 0.5rem; + padding-top: 0.25rem; + font-size: var(--body-font-size-xs); +} + +.agent-transactions.block a { + line-height: var(--line-height-m); + letter-spacing: var(--letter-spacing-m); + text-transform: uppercase; + padding-left: 1rem; + cursor: pointer; +} + +.agent-transactions.block a.show-more::after { + content: "View More"; + display: inline-block; + background-image: url("../../icons/arrow-down.svg"); + background-repeat: no-repeat; + background-position: right; + padding-right: 1rem; + font-size: var(--body-font-size-xs); + font-weight: var(--font-weight-bold); +} + +.agent-transactions.block a.show-less::after { + content: "View Less"; + display: inline-block; + background-image: url("../../icons/arrow-up.svg"); + background-repeat: no-repeat; + background-position: right; + padding-right: 1rem; + font-size: var(--body-font-size-xs); + font-weight: var(--font-weight-bold); +} + +.agent-transactions.block thead th { + padding-top: 0.125rem; + padding-bottom: 0.125rem; +} + +.agent-transactions.block thead th:first-of-type { + padding-left: 1rem; +} + +.agent-transactions.block tbody td:first-of-type { + padding-left: 1rem; +} + +.agent-transactions.block .default { + display: table-cell; +} + +.agent-transactions.block .medium, +.agent-transactions.block .large, +.agent-transactions.block .xl { + display: none; +} + +.agent-transactions.block .sold-price { + width: 40%; +} + +@media (min-width: 600px) { + .agent-transactions.block .medium { + display: table-cell; + } + + .agent-transactions.block .address { + width: 48.7%; + } + + .agent-transactions.block .city { + width: 24.5%; + } + + .agent-transactions.block .state { + width: 7.4%; + } + + .agent-transactions.block .sold-price { + width: 40%; + } +} + +@media (min-width: 992px) { + .agent-transactions.block h1 { + font-size: 1.875rem; + } + + .agent-transactions.block thead { + font-size: var(--body-font-size-s); + } + + .agent-transactions.block tbody td { + font-size: var(--body-font-size-s); + padding-top: 0.5rem; + } + + .agent-transactions.block .large { + display: table-cell; + } + + .agent-transactions.block .address { + width: auto; + } + + .agent-transactions.block .city { + width: 15%; + } + + .agent-transactions.block .sold-price { + width: 13%; + } +} + +@media (min-width: 1200px) { + .agent-transactions.block .xl { + display: table-cell; + } + + .agent-transactions.block .beds, + .agent-transactions.block .baths { + width: 5%; + } +} diff --git a/blocks/agent-transactions/agent-transactions.js b/blocks/agent-transactions/agent-transactions.js new file mode 100644 index 00000000..d7e13c00 --- /dev/null +++ b/blocks/agent-transactions/agent-transactions.js @@ -0,0 +1,105 @@ +import { + table, tbody, th, thead, tr, td, h1, a, +} from '../../scripts/dom-helpers.js'; +import { getMetadata } from '../../scripts/aem.js'; + +const getClosedTransactions = async () => { + const agentId = getMetadata('agent-id'); + const formattedData = []; + + try { + const response = await fetch(`/bin/bhhs/agentPropertyListingsServlet.${agentId}.json`); + const data = await response.json(); + + if (data && data?.closedTransactions?.properties?.length) { + data.closedTransactions.properties.forEach((property) => { + formattedData.push({ + address: property.StreetName, + city: property.City, + state: property.StateOrProvince, + 'sold-price': property.closePrice, + beds: property.BedroomsTotal, + baths: property.BathroomsTotal, + 'approx-sq-ft': property.LivingArea, + type: property.PropertyType, + 'closed-date': property.ClosedDate, + }); + }); + } + } catch (error) { + // eslint-disable-next-line no-console + console.error('Error fetching closed transactions', error); + } + + return formattedData; +}; + +export default async function decorate(block) { + const transactionsData = await getClosedTransactions(); + if (transactionsData.length === 0) { + return; + } + + const thList = ['address', 'city', 'state', 'sold price', 'beds', 'baths', 'approx sq. ft.', 'type', 'closed date']; + const thDefault = { class: 'default', list: [0, 3] }; + const thMedium = { class: 'medium', list: [1, 2] }; + const thLarge = { class: 'large', list: [4, 5, 8] }; + const thXL = { class: 'xl', list: [6, 7] }; + + const theadTr = tr(); + const getClass = (index) => { + if (thDefault.list.includes(index)) { + return `${thDefault.class}`; + } + if (thMedium.list.includes(index)) { + return `${thMedium.class}`; + } + if (thLarge.list.includes(index)) { + return `${thLarge.class}`; + } + return `${thXL.class}`; + }; + + thList.forEach((x, index) => { + theadTr.appendChild(th({ class: `${x.split(' ').join('-').replace(/\./g, '')} ${getClass(index)}` }, x)); + }); + + const trBody = tbody(); + const intialTransactionsCount = 6; // show 6 transactions initially + + transactionsData.forEach((data, topIndex) => { + const trElement = tr({ class: `${topIndex < intialTransactionsCount ? 'show' : 'hide'}` }); + + thList.forEach((x, index) => { + const key = x.split(' ').join('-').replace(/\./g, ''); + trElement.appendChild(td({ class: `${x.split(' ').join('-').replace(/\./g, '')} ${getClass(index)}` }, (data[key]) || '')); + }); + + trBody.appendChild(trElement); + }); + + const tableElement = table(thead(theadTr), trBody); + const heading1 = h1('Closed Transactions'); + const anchor = a({ class: 'show-more' }); + anchor.addEventListener('click', () => { + if (anchor.classList.contains('show-more')) { + anchor.classList.remove('show-more'); + anchor.classList.add('show-less'); + const tBodyTr = block.querySelectorAll('tbody tr.hide'); + tBodyTr.forEach((trElement) => { + trElement.classList.remove('hide'); + }); + } else { + anchor.classList.remove('show-less'); + anchor.classList.add('show-more'); + const tBodyTr = block.querySelectorAll('tbody tr'); + tBodyTr.forEach((trElement, index) => { + if (index >= intialTransactionsCount) { + trElement.classList.add('hide'); + } + }); + } + }); + + block.replaceChildren(heading1, tableElement, anchor); +} 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) { diff --git a/blocks/contact-form/contact-form.css b/blocks/contact-form/contact-form.css new file mode 100644 index 00000000..03a1cd47 --- /dev/null +++ b/blocks/contact-form/contact-form.css @@ -0,0 +1,234 @@ +.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 .contact-details { + display: flex; + flex-direction: row; + gap: 1em; + margin-bottom: 2em; +} + +.contact-form.block .profile { + display: flex; + flex-direction: column; + gap: 1em; +} + +.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 { + 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 { + display: none; + padding: 10px 4px; + margin-bottom: 1em; + flex-direction: row; + align-items: center; + border: 1px solid; + column-gap: 4px; +} + +.contact-form.block form.contact-form .message.error { + display: flex; + color: var(--error); + border-color: var(--error); +} + +.contact-form.block form.contact-form .message.success { + display: flex; + color: var(--success); + border-color: var(--success); +} + +.contact-form.block form.contact-form .message .icon { + display: none; + align-self: flex-start; + width: 20px; + height: 20px; +} + +.contact-form.block form.contact-form .message.error .icon.error { + display: block; +} + +.contact-form.block form.contact-form .message.success .icon.success { + display: block; +} + +.contact-form.block form.contact-form .message .details { + display: flex; + flex-direction: column; +} + +.contact-form.block form.contact-form .message span { + font-size: var(--body-font-size-xs); + line-height: var(--line-height-s); +} + +.contact-form.block form.contact-form .inputs .name, +.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 select, +.contact-form.block form.contact-form .inputs select option { + height: 50px; + width: 100%; + padding-left: 15px; + margin-bottom: 1em; + font-size: var(--body-font-size-s); + line-height: var(--line-height-xs); + color: var(--body-color); + border: 1px solid var(--dark-grey); +} + +.contact-form.block form.contact-form .inputs textarea { + width: 100%; + height: 110px; + padding: 15px; +} + +.contact-form.block form.contact-form .agent div { + font-weight: 400; + font-size: 14px; + color: var(--body-color); + letter-spacing: .5px; + line-height: 1; + display: flex; + justify-content: flex-start; + margin-bottom: 10px; +} + + +.contact-form.block form.contact-form .agent div.disclaimer { + font-size: var(--body-font-size-xxs); + line-height: var(--line-height-s); + color: var(--dark-grey); +} + +.contact-form.block form.contact-form .agent > div:first-child { + margin-bottom: .5rem; +} + +.contact-form.block form.contact-form .agent .agent-check { + display: inline-flex; + gap: 10px; +} + +.contact-form.block form.contact-form .agent input[type="radio"] { + 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 form.contact-form .agent .checkbox { + cursor: pointer; + height: 24px; + width: 24px; + border: 1px solid #aaa; + margin-right: 8px; + position: relative; +} + +.contact-form.block form.contact-form .agent .checkbox svg { + display: none; + height: 10px; + width: 12px; + top: calc(50% - 5px); + position: absolute; + left: 5px; +} + +.contact-form.block form.contact-form .agent input[type="radio"]:checked+.checkbox svg { + display: block; +} + +.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); +} + +.contact-form.block form.contact-form .cta { + padding-bottom: 2rem; +} + +.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 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 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 new file mode 100644 index 00000000..2c33df20 --- /dev/null +++ b/blocks/contact-form/contact-form.js @@ -0,0 +1,433 @@ +import { loadScript } from '../../scripts/aem.js'; +// import { getEnvelope } from '../../scripts/apis/creg/creg.js'; +import { removeSideModal, i18nLookup, getCookieValue } from '../../scripts/util.js'; +import { a, div, img } from '../../scripts/dom-helpers.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}$/; + +// Load reCaptcha script used on all forms. +loadScript('/blocks/contact-form/forms/callback.js'); + +function getImageURL(jsonString) { + try { + const data = JSON.parse(jsonString); + if (Array.isArray(data) && data.length > 0) { + const imageUrl = new URL(data[0].url); + // Replace the hostname and pathname with the new ones + imageUrl.hostname = 'hsfazpw2storagesf1.blob.core.windows.net'; + imageUrl.pathname = `/hsflibrary${imageUrl.pathname}`; + return imageUrl.toString(); + } + } catch (error) { + return '/media/images/no-profile-image.png'; + } + return null; // Add a return statement at the end of the function +} + +/** + * 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'; + 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.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'; + } + // Data format to JSON + return JSON.stringify(jsonObj); + } + + // Remaining Team Inquiry form elements + const title = 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); + 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'); + // Data format to URL Params + return new URLSearchParams(formData).toString(); +} + +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'); +} + +async function validateFormInputs(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.value)) { + 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('Phone number is required.')); + phone.classList.add('error'); + } + if (!phoneRegex.test(phone.value)) { + errors.push(i18n('Please enter a 10 digit phone number.')); + phone.classList.add('error'); + } + + 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'); + } + } + + /* eslint-disable no-undef */ + if (!errors.length) { + 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.')); + } + } + + if (errors.length > 0) { + displayError(errors); + return false; + } + return true; +} + +// eslint-disable no-console +const addForm = async (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(); + + const form = block.querySelector('form.contact-form'); + + // if there is a thank you, highjack the submission + // otherwise submit form normally. + if (thankYou) { + const oldSubmit = validateFormInputs; + thankYou.classList.add('form-thank-you'); + form.onsubmit = function handleSubmit(e) { + e.preventDefault(); + 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 { + 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(); + removeSideModal(); + }); + sideModal?.replaceChildren(thankYou); + } else { + block.replaceChildren(thankYou); + block.parentNode.nextSibling.remove(); + } + if (window.grecaptcha) { + recaptchaToken = null; + } + } + }); + } + }); + return false; + }; + } + + // 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); + formEl.ariaLabel = i18n(formEl.ariaLabel); + }); + + const taEl = block.querySelector('textarea'); + if (taEl?.placeholder) taEl.placeholder = i18n(taEl.placeholder); + if (window.selectedListing) { + // const prop = await findListing(); + const prop = window.selectedListing; + // if the listing agent is supposed to be displayed vs the office + if (prop.propertyDetails.listAgentCd) { + const info = block.querySelector('.contact-info'); + const pic = getImageURL(prop.listAgent.reAgentDetail.image); + const profile = div({ class: 'profile' }, img({ src: pic, alt: prop.listAgent.recipientName, width: '82px' })); + info.insertAdjacentElement('beforebegin', profile); + const name = block.querySelector('.company-name'); + const link = a({ href: '#' }, prop.listAgent.recipientName); // TODO: add link to agent profile + name.replaceChildren(link); + const email = block.querySelector('.company-email a'); + email.textContent = prop.listAgent.reAgentDetail.email; + email.href = `mailto:${prop.listAgent.reAgentDetail.email}`; + const phone = block.querySelector('.company-phone a'); + phone.textContent = prop.listAgent.reAgentDetail.officeTelephone; + phone.href = `tel:${prop.listAgent.reAgentDetail.officeTelephone}`; + } + + taEl.value = `Hi, I would like more information about ${prop.propertyDetails.unparsedAddress}`; + + if (window.location.pathname.length === 1) { + block.querySelector('.disclaimer').remove(); + } + } + + block.style.display = displayValue; + + const cancelBtn = block.querySelector('.contact-form.block .cta button.cancel'); + if (cancelBtn) { + cancelBtn.addEventListener('click', (e) => { + e.preventDefault(); + e.stopPropagation(); + removeSideModal(); + }); + } + + [...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'); + } + }); + // 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"]')] + .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 (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) { + const observer = new IntersectionObserver((entries) => { + if (entries.some((e) => e.isIntersecting)) { + observer.disconnect(); + addForm(block); + } + }, { + rootMargin: '300px', + }); + await observer.observe(block); +} diff --git a/blocks/contact-form/forms/callback.js b/blocks/contact-form/forms/callback.js new file mode 100644 index 00000000..87e1b35e --- /dev/null +++ b/blocks/contact-form/forms/callback.js @@ -0,0 +1,23 @@ +/* eslint-disable no-unused-vars */ +/* eslint-disable no-undef */ +let recaptchaToken = null; + +function verifyCallback(resp) { + recaptchaToken = resp; +} + +function onloadCallback() { + grecaptcha.render('captcha-20285', { + sitekey: window.placeholders.default.recaptchaSitekey, + 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 new file mode 100644 index 00000000..452c2be9 --- /dev/null +++ b/blocks/contact-form/forms/contact-property.html @@ -0,0 +1,71 @@ +