diff --git a/blocks/contact-form/contact-form.css b/blocks/contact-form/contact-form.css
index 11d8e01c..03a1cd47 100644
--- a/blocks/contact-form/contact-form.css
+++ b/blocks/contact-form/contact-form.css
@@ -12,6 +12,19 @@
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);
@@ -115,7 +128,13 @@
display: flex;
justify-content: flex-start;
margin-bottom: 10px;
- cursor: pointer;
+}
+
+
+.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 {
diff --git a/blocks/contact-form/contact-form.js b/blocks/contact-form/contact-form.js
index d38dfff2..2c33df20 100644
--- a/blocks/contact-form/contact-form.js
+++ b/blocks/contact-form/contact-form.js
@@ -1,5 +1,7 @@
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();
@@ -9,6 +11,22 @@ 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.
*
@@ -314,7 +332,33 @@ const addForm = async (block) => {
});
const taEl = block.querySelector('textarea');
- if (taEl && taEl.placeholder) taEl.placeholder = i18n(taEl.placeholder);
+ 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;
diff --git a/blocks/contact-form/forms/contact-property.html b/blocks/contact-form/forms/contact-property.html
index 63039424..452c2be9 100644
--- a/blocks/contact-form/forms/contact-property.html
+++ b/blocks/contact-form/forms/contact-property.html
@@ -1,8 +1,12 @@
-
+ By providing this information, you are giving permission to Berkshire Hathaway HomeServices, Constellation1, a division of Constellation Web Solutions, Inc., and the members of the Berkshire Hathaway HomeServices real estate network (1) to contact you in response to your specific question or message, and (2) to register you in our system in order to communicate with you about properties for sale or rent in locations of interest to you. For more about how we will use your contact information, review our Privacy Policy.
diff --git a/blocks/property-listing/property-listing.js b/blocks/property-listing/property-listing.js
index 7c714bb4..11549b93 100644
--- a/blocks/property-listing/property-listing.js
+++ b/blocks/property-listing/property-listing.js
@@ -2,36 +2,40 @@ import { getMetadata, readBlockConfig } from '../../scripts/aem.js';
import { render as renderCards } from '../shared/property/cards.js';
import Search from '../../scripts/apis/creg/search/Search.js';
import { propertySearch } from '../../scripts/apis/creg/creg.js';
+import {
+ a, div, p, span,
+} from '../../scripts/dom-helpers.js';
export default async function decorate(block) {
// Find and process list type configurations.
const config = readBlockConfig(block);
+ const search = await Search.fromBlockConfig(config);
+ search.franchiseeCode = getMetadata('office-id');
+ const searchUrl = `search?${search.asCregURLSearchParameters()}`;
if (config.title) {
- block.innerHTML = `
-
- `;
- if (config['link-text']) {
- const div = document.createElement('div');
- const url = config['link-url'] || '';
- div.innerHTML = `
-
- ${config['link-text'] || 'See More'}
-
`;
- block.querySelector('.header').append(div);
+ const blockTitle = div({ class: 'header' },
+ div(
+ span(config.title),
+ ),
+ );
+ block.replaceChildren(blockTitle);
+
+ if (config.link) {
+ const moreBtn = div(
+ p({ class: 'button-container' },
+ a({ href: config['link-url'] || searchUrl, 'aria-label': config.link || 'See More', class: 'button secondary' },
+ config.link || 'See More',
+ ),
+ ),
+ );
+ block.querySelector('.header').append(moreBtn);
}
} else {
block.innerHTML = '';
}
- const search = await Search.fromBlockConfig(config);
- search.franchiseeCode = getMetadata('office-id');
- const list = document.createElement('div');
- list.classList.add('property-list-cards', `rows-${Math.floor(search.pageSize / 8)}`);
+ const list = div({ class: `property-list-cards rows-${Math.floor(search.pageSize / 8)}` });
block.append(list);
propertySearch(search).then((results) => {
renderCards(list, results.properties);
diff --git a/blocks/shared/property/cards.js b/blocks/shared/property/cards.js
index 52afd8fc..1b4076c9 100644
--- a/blocks/shared/property/cards.js
+++ b/blocks/shared/property/cards.js
@@ -1,10 +1,16 @@
import { decorateFormLinks } from '../../../scripts/scripts.js';
+import {
+ a, div, domEl, img, p, span,
+} from '../../../scripts/dom-helpers.js';
function createImage(listing) {
if (listing.SmallMedia?.length > 0) {
- return ` `;
+ const tempImg = img({
+ src: listing.SmallMedia[0].mediaUrl, alt: listing.StreetName, loading: 'lazy', class: 'property-thumbnail',
+ });
+ return tempImg;
}
- return 'no images available
';
+ return div({ class: 'property-no-available-image' }, span('no images available'));
}
export function createCard(listing) {
@@ -27,98 +33,106 @@ export function createCard(listing) {
specs.push(`${listing.LivingArea} ${listing.LivingAreaUnits}`);
}
- const item = document.createElement('div');
- item.classList.add('listing-tile');
+ let classes = 'listing-tile';
if (listing.OpenHouses?.length > 0) {
- item.classList.add('has-open-houses');
+ classes += ' has-open-houses';
}
if (listing.FeaturedListing) {
- item.classList.add('is-featured');
+ classes += ' is-featured';
}
if (listing.PdpPath.includes('LuxuryTheme=true')) {
- item.classList.add('is-luxury');
+ classes += ' is-luxury';
}
const applicationType = listing.ListingType && listing.ListingType === 'For Rent' ? `${listing.ListingType} ` : '';
if (listing.ClosedDate !== '01/01/0001') {
- item.classList.add('is-sold');
+ classes += 'is-sold';
listing.mlsStatus = 'Closed';
}
- item.innerHTML = `
-
-
-
- ${createImage(listing)}
-
-
-
-
Luxury Collection
-
-
- Open House
-
-
-
-
-
- Featured Listing
- ${applicationType}
- ${listing.mlsStatus}
-
-
-
${listing.ListPriceUS}
-
-
-
-
-
-
-
-
Closed: ${listing.ClosedDate}
-
- ${listing.StreetName}
-
- ${listing.City}, ${listing.StateOrProvince} ${listing.PostalCode}
-
-
${specs.join(' / ')}
-
-
-
-
-
-
- `;
- return item;
+ const newEl = div({ class: classes },
+ a({ href: detailsPath, rel: 'noopener', 'aria-labelledby': `listing-${listing.ListingId}-address` },
+ div({ class: 'listing-image-container' },
+ div({ class: 'property-image' }, createImage(listing)),
+ div({ class: 'image-position-top' },
+ div({ class: 'property-labels' },
+ div({ class: 'property-label luxury' }, 'Luxury Collection'),
+ div({ class: 'property-label open-house' },
+ span({ class: 'icon icon-openhouse' }, 'Open House'),
+ ),
+ ),
+ ),
+ div({ class: 'image-position-bottom' },
+ div({ class: 'property-labels' },
+ span({ class: 'property-label featured' }, 'Featured Listing'),
+ applicationType,
+ span({ class: 'property-label' }, listing.mlsStatus),
+ ),
+ div({ class: 'property-price' },
+ p(listing.ListPriceUS),
+ ),
+ ),
+ ),
+ ),
+ div({ class: 'property-details' },
+ div({ class: 'property-info-wrapper' },
+ div({ class: 'property-info' },
+ div({ class: 'sold-date' }, `Closed: ${listing.ClosedDate}`),
+ div({ id: `listing-${listing.PropId}-address`, class: 'address' },
+ listing.StreetName,
+ domEl('br'),
+ `${listing.City}, `,
+ `${listing.StateOrProvince} `,
+ listing.PostalCode,
+ ),
+ div({ class: 'specs' }, specs.join(' / ')),
+ ),
+ ),
+ div({ class: 'property-buttons' },
+ div({ class: 'buttons-row-flex' },
+ a({ 'aria-label': `Contact us about ${listing.StreetName}`, href: '/fragments/contact-property-form', class: 'button-property' },
+ span({ class: 'icon icon-envelope' },
+ img({
+ 'data-icon-name': 'envelope', src: '/icons/envelope.svg', loading: 'lazy', alt: 'envelope',
+ }),
+ ),
+ span({ class: 'icon icon-envelopedark' },
+ img({
+ 'data-icon-name': 'envelopedark', src: '/icons/envelopedark.svg', loading: 'lazy', alt: 'envelope',
+ }),
+ ),
+ ),
+ a({ 'aria-label': `Save ${listing.StreetName} to saved properties.`, href: '#', class: 'button-property' },
+ span({ class: 'icon icon-heartempty' },
+ img({
+ 'data-icon-name': 'heartempty', src: '/icons/heartempty.svg', loading: 'lazy', alt: 'heart',
+ }),
+ ),
+ span({ class: 'icon icon-heartemptydark' },
+ img({
+ 'data-icon-name': 'heartempty', src: '/icons/heartemptydark.svg', loading: 'lazy', alt: 'heart',
+ }),
+ ),
+ ),
+ ),
+ ),
+ ),
+ domEl('hr'),
+ div({ class: 'extra-info' },
+ div(
+ div({ class: 'courtesy-info' }, `Listing courtesy of: ${listing.CourtesyOf}`),
+ div({ class: 'courtesy-provided' }, `Listing provided by: ${listing.listAor}`),
+ ),
+ div({ class: `listing-aor ${listing.listAor.toLowerCase()}` },
+ img({
+ class: 'rimls-image', src: '/styles/images/rimls_logo.jpg', alt: 'Disclaimer Logo Image', loading: 'lazy', height: '20', width: '33',
+ }),
+ ),
+ ),
+ );
+ return newEl;
}
/**
diff --git a/blocks/side-modal/side-modal.css b/blocks/side-modal/side-modal.css
index 9e2e282d..9fb8c48b 100644
--- a/blocks/side-modal/side-modal.css
+++ b/blocks/side-modal/side-modal.css
@@ -1,10 +1,10 @@
aside.side-modal {
overflow: hidden scroll;
position: fixed;
- top: 200px;
+ top: 65px;
bottom: 0;
- width: 100vw;
- right: -100vw;
+ width: 100%;
+ right: 0;
transition: right .2s cubic-bezier(.4,0,.2,1) .1s;
background-color: white;
z-index: 1090;
@@ -19,15 +19,25 @@ aside.side-modal > div {
padding: 53px 15px 15px;
}
-@media (min-width: 768px) {
+@media (min-width: 600px) {
aside.side-modal {
- width: 50vw;
- right: -50vw;
+ top: 0;
+ width: 600px;
+ right: -600px;
}
-}
-@media (min-width: 992px) {
aside.side-modal > div {
- padding: 53px 16% 16%;
+ padding: 53px 45px 15px;
+ }
+
+ body > div > div.side-modal-overlay {
+ background-color: #212529;
+ width: 100%;
+ height: 100%;
+ position: fixed;
+ opacity: .5;
+ z-index: 1040;
+ top: 0;
+ left: 0;
}
}
diff --git a/blocks/side-modal/side-modal.js b/blocks/side-modal/side-modal.js
index 850807a5..1835ba28 100644
--- a/blocks/side-modal/side-modal.js
+++ b/blocks/side-modal/side-modal.js
@@ -2,9 +2,12 @@
import {
decorateSections, decorateBlocks, loadBlocks, decorateButtons, decorateIcons, loadCSS,
} from '../../scripts/aem.js';
+import { getEnvelope } from '../../scripts/apis/creg/creg.js';
export async function showSideModal(a) {
const { href } = a;
+ const listing = a.parentNode.parentNode.previousElementSibling.querySelector('div.address').id.split('-')[1];
+ window.selectedListing = await getEnvelope(listing);
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`);
diff --git a/scripts/apis/creg/creg.js b/scripts/apis/creg/creg.js
index e7c2d9bd..4e42cceb 100644
--- a/scripts/apis/creg/creg.js
+++ b/scripts/apis/creg/creg.js
@@ -78,3 +78,19 @@ export async function getEconomicDetails(lat, long) {
});
});
}
+
+/**
+ * Retrieves the envelope for a given listing ID.
+ *
+ * @param {string} listingId - The ID of the listing.
+ * @returns {Promise} A promise that resolves to the envelope data.
+ */
+export async function getEnvelope(listingId) {
+ return new Promise((resolve) => {
+ const worker = new Worker(`${window.hlx.codeBasePath}/scripts/apis/creg/workers/envelope.js`, { type: 'module' });
+ worker.onmessage = (e) => resolve(e.data);
+ worker.postMessage({
+ listingId,
+ });
+ });
+}
diff --git a/scripts/apis/creg/workers/envelope.js b/scripts/apis/creg/workers/envelope.js
new file mode 100644
index 00000000..768ed801
--- /dev/null
+++ b/scripts/apis/creg/workers/envelope.js
@@ -0,0 +1,19 @@
+/**
+ * Handle the Worker event. Fetches details for each provided listing id.
+ *
+ * @param {Object} event the worker event.
+ * @param {string} event.data.api the URL to fetch.
+ * @param {string[]} event.data.ids list of listing ids
+ */
+onmessage = async (event) => {
+ const { listingId } = event.data;
+
+ try {
+ const response = await fetch(`/bin/bhhs/CregPropertySearchServlet?SearchType=Envelope&ListingId=${listingId}`);
+ const data = response.ok ? await response.json() : undefined;
+
+ postMessage(data);
+ } catch (error) {
+ postMessage({});
+ }
+};
diff --git a/scripts/util.js b/scripts/util.js
index 6a78bdd8..db887d1c 100644
--- a/scripts/util.js
+++ b/scripts/util.js
@@ -1,4 +1,5 @@
import { fetchPlaceholders } from './aem.js';
+import { div, domEl } from './dom-helpers.js';
/**
* Creates the standard Spinner Div.
@@ -6,10 +7,10 @@ import { fetchPlaceholders } from './aem.js';
* @returns {HTMLDivElement} the spinner div.
*/
export function getSpinner() {
- const div = document.createElement('div');
- div.classList.add('loading-spinner');
- div.innerHTML = ' ';
- return div;
+ const spinner = document.createElement('div');
+ spinner.classList.add('loading-spinner');
+ spinner.innerHTML = ' ';
+ return spinner;
}
/**
@@ -54,6 +55,7 @@ let focusElement;
export function removeSideModal() {
if (!sideModal) return;
+ sideModal.parentNode.nextSibling.remove();
sideModal.parentNode.remove();
sideModal = null;
document.body.classList.remove('disable-scroll');
@@ -62,15 +64,12 @@ export function removeSideModal() {
export async function showSideModal(content, decorateContent) {
if (!sideModal) {
- const fragment = document.createRange().createContextualFragment(`
-
- `);
- sideModal = fragment.querySelector('.side-modal');
- document.body.append(...fragment.children);
+ const temp = div(
+ domEl('aside', { class: 'side-modal' }, div()),
+ div({ class: 'side-modal-overlay' }),
+ );
+ sideModal = temp.querySelector('.side-modal');
+ document.body.append(temp);
}
const container = sideModal.querySelector('div');
container.replaceChildren(...content);