diff --git a/blocks/carousel-slider/carousel-slider.css b/blocks/carousel-slider/carousel-slider.css
new file mode 100644
index 00000000..c89267f1
--- /dev/null
+++ b/blocks/carousel-slider/carousel-slider.css
@@ -0,0 +1,113 @@
+.carousel-slider {
+ position: relative;
+ width: 100%;
+ margin: 15px 0;
+ overflow: hidden;
+}
+
+.carousel-slider .slide-container {
+ display: flex;
+ gap: 5px;
+ transition: transform 0.5s ease;
+}
+
+.carousel-slider .slide {
+ flex: 0 0 auto;
+ position: relative;
+ max-height: 440px;
+}
+
+.carousel-slider .slide img {
+ width: 100%;
+ height: auto;
+ object-fit: cover;
+}
+
+.carousel-slider .slide .new-listing {
+ position: absolute;
+ top: 16px;
+ left: 16px;
+ z-index: 2;
+ height: 24px;
+ font-size: var(--heading-font-size-xs);
+ padding: 3px 7px;
+ line-height: var(--line-height-m);
+ font-weight: var(--font-weight-semibold);
+ display: none;
+ background: var(--primary-color);
+ color: var(--white);
+ letter-spacing: var(--letter-spacing-m);
+ text-transform: uppercase;
+}
+
+.carousel-slider .btns {
+ position: absolute;
+ top: 50%;
+ display: flex;
+ justify-content: space-between;
+}
+
+.carousel-slider .btn-prev::before {
+ content: "\f053";
+ font-family: var(--font-family-fontawesome);
+}
+
+.carousel-slider .btn-next::before {
+ content: "\f054";
+ font-family: var(--font-family-fontawesome);
+}
+
+.carousel-slider .btn-prev,
+.carousel-slider .btn-next {
+ background: #a9a9a990;
+ height: 40px;
+ width: 40px;
+ margin-top: -25px;
+ cursor: pointer;
+ font-size: 1.2rem;
+ color: black;
+ text-align: center;
+ line-height: 2.5rem;
+ transition: all 0.2s ease-in;
+}
+
+.carousel-slider div.thumbs {
+ padding: 10px 0;
+ width: 100%;
+ overflow-x: auto;
+ white-space: nowrap;
+}
+
+.carousel-slider div.thumbs div {
+ display: inline-block;
+ opacity: 0.5;
+}
+
+.carousel-slider div.thumbs div button {
+ padding: 0;
+ height: 40px;
+ width: 40px;
+ margin-right: 2px;
+ border: 0;
+ cursor: pointer;
+ background-position: center;
+ background-size: cover;
+}
+
+.carousel-slider div.thumbs div.active {
+ opacity: 1;
+}
+
+.carousel-slider-container {
+ margin: 1rem 1rem 2rem;
+}
+
+@media (min-width: 900px) {
+ .carousel-slider .btns,
+ .carousel-slider div.thumbs {
+ display: none;
+ }
+ .carousel-slider .arrows {
+ display: inline;
+ }
+}
\ No newline at end of file
diff --git a/blocks/carousel-slider/carousel-slider.js b/blocks/carousel-slider/carousel-slider.js
new file mode 100644
index 00000000..029eb1e7
--- /dev/null
+++ b/blocks/carousel-slider/carousel-slider.js
@@ -0,0 +1,308 @@
+import { getEnvelope } from '../../scripts/apis/creg/creg.js';
+import { button, div, img } from '../../scripts/dom-helpers.js';
+
+async function getPropertyByPropId(propId) {
+ const resp = await getEnvelope(propId);
+ return resp;
+}
+
+const SLIDE_ID_PREFIX = 'slide';
+const SLIDE_CONTROL_ID_PREFIX = 'carousel-slide-control';
+
+let curSlide = 0;
+let maxSlide = 0;
+let autoScroll;
+let scrollInterval;
+let scrollDuration = '1000';
+
+/**
+ * Synchronizes the active thumbnail with the active slide in the carousel.
+ * @param {HTMLElement} carousel - The carousel element.
+ * @param {number} activeSlide - The index of the active slide.
+ */
+function syncActiveThumb(carousel, activeSlide) {
+ carousel.querySelectorAll('div.thumbs div').forEach((item, index) => {
+ const btn = item.querySelector('button');
+ if (index === activeSlide) {
+ item.classList.add('active');
+ btn.setAttribute('aria-selected', 'true');
+ btn.setAttribute('tabindex', '0');
+ } else {
+ item.classList.remove('active');
+ btn.removeAttribute('aria-selected');
+ btn.setAttribute('tabindex', '-1');
+ }
+ });
+}
+
+/**
+ * Scrolls the carousel to the specified slide index and updates the active thumbnail.
+ * @param {HTMLElement} carousel - The carousel element.
+ * @param {number} [slideIndex=0] - The index of the slide to scroll to.
+ */
+function scrollToSlide(carousel, slideIndex = 0) {
+ const slideWidth = carousel.querySelector('.slide').offsetWidth + 5;
+ const carouselSlider = carousel.querySelector('.slide-container');
+ carouselSlider.style.transform = `translateX(${-curSlide * slideWidth}px)`;
+ const thumbSlider = carousel.querySelector('.thumbs');
+ if (curSlide > 1) thumbSlider.scrollTo({ left: thumbSlider.querySelector('div').offsetWidth * (curSlide - 1), behavior: 'smooth' });
+
+ syncActiveThumb(carousel, curSlide);
+ // sync slide
+ [...carouselSlider.children].forEach((slide, index) => {
+ if (index === slideIndex) {
+ slide.removeAttribute('tabindex');
+ slide.setAttribute('aria-hidden', 'false');
+ } else {
+ slide.setAttribute('tabindex', '-1');
+ slide.setAttribute('aria-hidden', 'true');
+ }
+ });
+ curSlide = slideIndex;
+}
+
+/**
+ * start auto scroll
+ */
+function startAutoScroll(block) {
+ if (!scrollInterval) {
+ scrollInterval = setInterval(() => {
+ scrollToSlide(block, curSlide < maxSlide ? curSlide + 1 : 0);
+ }, scrollDuration);
+ }
+}
+
+/**
+ * stop auto scroll
+ */
+function stopAutoScroll() {
+ clearInterval(scrollInterval);
+ scrollInterval = undefined;
+}
+
+/**
+ * Scrolls the element to the nearest block based on the scroll direction.
+ *
+ * @param {HTMLElement} el - The element to be scrolled.
+ * @param {number} [dir=1] - The scroll direction. Positive value for right, negative value for left.
+ */
+function snapScroll(el, dir = 1) {
+ if (!el) {
+ return;
+ }
+ let threshold = el.offsetWidth * 0.5;
+ if (dir >= 0) {
+ threshold -= (threshold * 0.5);
+ } else {
+ threshold += (threshold * 0.5);
+ }
+ const block = Math.floor(el.scrollLeft / el.offsetWidth);
+ const pos = el.scrollLeft - (el.offsetWidth * block);
+ const snapToBlock = pos <= threshold ? block : block + 1;
+ const carousel = el.closest('.carousel-slider');
+ scrollToSlide(carousel, snapToBlock);
+}
+
+/**
+ * Builds a navigation arrow element for the carousel slider.
+ *
+ * @param {string} dir - The direction of the arrow ('prev' or 'next').
+ * @returns {HTMLElement} - The navigation arrow element.
+ */
+function buildNav(dir) {
+ const arrow = div({
+ class: `btn-${dir}`,
+ 'aria-label': dir === 'prev' ? 'Previous Image' : 'Next Image',
+ role: 'button',
+ tabindex: dir === 'prev' ? '0' : '-1',
+ });
+ arrow.addEventListener('click', (e) => {
+ let nextSlide = 0;
+ if (dir === 'prev') {
+ nextSlide = curSlide === 0 ? maxSlide : curSlide - 1;
+ } else {
+ nextSlide = curSlide === maxSlide ? 0 : curSlide + 1;
+ }
+ const carousel = e.target.closest('.carousel-slider');
+ scrollToSlide(carousel, nextSlide);
+ });
+ return arrow;
+}
+
+/**
+ * Builds the thumbnail elements for the carousel slider.
+ *
+ * @param {Array} slides - An array of slide objects.
+ * @returns {HTMLElement} - The thumbnails container element.
+ */
+function buildThumbnails(slides = []) {
+ const thumbnails = div({ class: 'thumbs', role: 'tablist', style: `width: ${Math.round(window.innerWidth * 0.9)}px` });
+ slides.forEach((slide, index) => {
+ const thumb = div({
+ role: 'presentation',
+ class: index === 0 ? 'active' : '',
+ });
+ const btn = button({
+ type: 'button',
+ role: 'tab',
+ 'aria-controls': `${SLIDE_ID_PREFIX}${index}`,
+ 'aria-selected': index === 0 ? 'true' : 'false',
+ tabindex: index === 0 ? '0' : '-1',
+ 'aria-label': 'View Enlarged Image',
+ style: `background-image: url(${slide.mediaUrl})`,
+ });
+ thumb.append(btn);
+ btn.addEventListener('click', (e) => {
+ curSlide = index;
+ const carousel = e.target.closest('.carousel-slider');
+ scrollToSlide(carousel, curSlide);
+ });
+ thumbnails.append(thumb);
+ });
+ return thumbnails;
+}
+
+/**
+ * Decorate a base slide element.
+ *
+ * @param item A base block slide element
+ * @param index The slide's position
+ * @return {HTMLUListElement} A decorated carousel slide element
+ */
+function buildSlide(item, index) {
+ const slide = div({
+ class: 'slide',
+ id: `${SLIDE_ID_PREFIX}${index}`,
+ 'data-slide-index': index,
+ role: 'tabpanel',
+ 'aria-hidden': index === 0 ? 'false' : 'true',
+ 'aria-describedby': `${SLIDE_CONTROL_ID_PREFIX}${index}`,
+ tabindex: index === 0 ? '0' : '-1',
+ style: `width: ${Math.round(window.innerWidth * 0.9)}px`,
+ },
+ img({ src: item.mediaUrl }),
+ div({ class: 'new-listing' }, 'New Listing'),
+ );
+ return slide;
+}
+
+/**
+ * Decorate and transform a carousel block.
+ *
+ * @param block HTML block from Franklin
+ */
+export default async function decorate(block) {
+ // TODO: remove this test propId
+ if (!window.envelope) {
+ const propId = '358207023'; // commercial '368554873'; // '375215759'; // luxury '358207023';
+ window.envelope = await getPropertyByPropId(propId);
+ }
+ block.innerHTML = '';
+
+ const carousel = document.createElement('div');
+
+ carousel.classList.add('slide-container');
+
+ // if block contains class auto-scroll add scroll functionality and get interval
+ const blockClasses = block.className.split(' ');
+ const autoScrollClass = blockClasses.find((className) => className.startsWith('auto-scroll-'));
+
+ if (autoScrollClass) {
+ autoScroll = true;
+ // get scroll duration
+ // eslint-disable-next-line prefer-destructuring
+ scrollDuration = autoScrollClass.match(/\d+/)[0];
+ }
+
+ // make carousel draggable
+ let isDown = false;
+ let startX = 0;
+ let startScroll = 0;
+ let prevScroll = 0;
+
+ carousel.addEventListener('mouseenter', () => {
+ if (autoScroll) stopAutoScroll();
+ });
+
+ carousel.addEventListener('mouseleave', () => {
+ if (isDown) {
+ snapScroll(carousel, carousel.scrollLeft > startScroll ? 1 : -1);
+ }
+ if (autoScroll) startAutoScroll(block);
+ isDown = false;
+ });
+
+ carousel.addEventListener('mousedown', (e) => {
+ isDown = true;
+ startX = e.pageX - carousel.offsetLeft;
+ startScroll = carousel.scrollLeft;
+ prevScroll = startScroll;
+ });
+
+ carousel.addEventListener('mouseup', () => {
+ if (isDown) {
+ snapScroll(carousel, carousel.scrollLeft > startScroll ? 1 : -1);
+ }
+ isDown = false;
+ });
+
+ carousel.addEventListener('mousemove', (e) => {
+ if (!isDown) {
+ return;
+ }
+ e.preventDefault();
+ const x = e.pageX - carousel.offsetLeft;
+ const walk = (x - startX);
+ carousel.scrollLeft = prevScroll - walk;
+ });
+
+ // process each slide
+ const slides = [...window.envelope.propertyDetails.smallPhotos];
+ maxSlide = slides.length - 1;
+ slides.forEach((slide, index) => {
+ carousel.appendChild(buildSlide(slide, index));
+ });
+
+ block.append(carousel);
+
+ // add nav buttons and thumbs to block
+ if (slides.length > 1) {
+ const prevBtn = buildNav('prev');
+ const nextBtn = buildNav('next');
+ const btns = div({ class: 'btns', style: `width: ${Math.round(window.innerWidth * 0.9)}px` }, prevBtn, nextBtn);
+ const thumbs = buildThumbnails(slides);
+ block.append(btns, thumbs);
+ syncActiveThumb(block, 0);
+ }
+
+ // autoscroll functionality
+ if (autoScroll) {
+ // auto scroll when visible
+ const intersectionOptions = {
+ root: null,
+ rootMargin: '0px',
+ threshold: 1.0,
+ };
+
+ const handleAutoScroll = (entries) => {
+ entries.forEach((entry) => {
+ if (entry.isIntersecting) {
+ startAutoScroll(block);
+ } else {
+ stopAutoScroll();
+ }
+ });
+ };
+
+ const carouselObserver = new IntersectionObserver(handleAutoScroll, intersectionOptions);
+ carouselObserver.observe(block);
+
+ document.addEventListener('visibilitychange', () => {
+ if (document.hidden) {
+ stopAutoScroll();
+ } else {
+ startAutoScroll(block);
+ }
+ });
+ }
+}
diff --git a/blocks/economic-data/economic-data.js b/blocks/economic-data/economic-data.js
index 7b528b41..505c8cd0 100755
--- a/blocks/economic-data/economic-data.js
+++ b/blocks/economic-data/economic-data.js
@@ -1,6 +1,7 @@
import { getDetails, getEconomicDetails } from '../../scripts/apis/creg/creg.js';
import { div, span } from '../../scripts/dom-helpers.js';
import { decorateIcons } from '../../scripts/aem.js';
+import { toggleAccordion } from '../../scripts/util.js';
const keys = [
'ListPriceUS',
@@ -24,11 +25,6 @@ function pick(obj, ...args) {
return args.reduce((res, key) => ({ ...res, [key]: obj[key] }), { });
}
-function toggleAccordion(event) {
- const content = event.target;
- content.classList.toggle('active');
-}
-
/**
* Retrieves the property ID from the current URL path.
* @returns {string|null} The property ID if found in the URL path, or null if not found.
@@ -42,11 +38,6 @@ function getPropIdFromPath() {
return null;
}
-async function getPropertyByPropId(propId) {
- const resp = await getDetails(propId);
- return resp[0];
-}
-
async function getSocioEconomicData(latitude, longitude) {
const resp = await getEconomicDetails(latitude, longitude);
return resp[0];
@@ -157,7 +148,7 @@ export default async function decorate(block) {
// TODO: remove this test propId
if (!propId) propId = '370882966';
- const propertyData = await getPropertyByPropId(propId);
+ const propertyData = await getDetails(propId);
if (propertyData) {
property = pick(propertyData, ...keys);
if (property.Latitude && property.Longitude) {
diff --git a/blocks/property-attributes/property-attributes.css b/blocks/property-attributes/property-attributes.css
new file mode 100755
index 00000000..314810db
--- /dev/null
+++ b/blocks/property-attributes/property-attributes.css
@@ -0,0 +1,145 @@
+.section.property-attributes-container {
+ width: var(--full-page-width);
+ background-color: var(--tertiary-color);
+}
+
+.property-attributes.block .title {
+ font-size: var(--heading-font-size-l);
+ font-weight: var(--font-weight-semibold);
+ line-height: var(--line-height-s);
+ color: var(--primary-color);
+ width: 100%;
+ margin-left: 16px;
+ padding-top: 30px;
+ margin-bottom: 40px;
+}
+
+.property-attributes.block .attributes .table,
+.property-attributes.block .accordion .table {
+ display: grid;
+ grid-template-columns: 1fr 1fr;
+ gap: 20px;
+ padding: 0 16px 20px 16px;
+}
+
+.property-attributes.block .attributes .label,
+.property-attributes.block .accordion .label {
+ font-size: var(--body-font-size-xs);
+ line-height: var(--line-height-m);
+ color: var(--secondary-color);
+}
+
+.property-attributes.block .attributes .td,
+.property-attributes.block .accordion .td {
+ font-size: var(--body-font-size-s);
+ line-height: var(--line-height-m);
+ color: var(--body-color);
+}
+
+.property-attributes.block .tooltip {
+ position: relative;
+ display: inline-block;
+ height: 19px;
+ width: 19px;
+ margin-left: 5px;
+}
+
+.property-attributes.block .tooltip .tooltiptext {
+ visibility: hidden;
+ width: 290px;
+ background-color: var(--black);
+ color: var(--white);
+ text-align: left;
+ padding: 14px 18px;
+ position: absolute;
+ z-index: 1;
+ top: 100%;
+ left: 0;
+ margin: 12px 0 0 -10px;
+ font-size: var(--body-font-size-xxs);
+ letter-spacing: var(--letter-spacing-s);
+ line-height: var(--line-height-s);
+}
+
+.property-attributes.block .tooltip:hover .tooltiptext {
+ visibility: visible;
+}
+
+.property-attributes.block .tooltip .tooltiptext::before {
+ content: '';
+ position: absolute;
+ bottom: 100%;
+ left: 8px;
+ border-width: 10px;
+ border-style: solid;
+ border-color: transparent transparent var(--black) transparent;
+}
+
+.property-attributes.block .accordion-header {
+ font-size: var(--heading-font-size-xxs);
+ line-height: var(--line-height-s);
+ letter-spacing: var(--letter-spacing-s);
+ text-transform: uppercase;
+ cursor: pointer;
+ padding: 10px 16px;
+ margin: 10px 0;
+ border-top: 1px solid var(--secondary-medium-grey);
+ font-weight: var(--font-weight-bold);
+}
+
+.property-attributes.block .accordion-content {
+ display: none;
+ overflow: hidden;
+}
+
+.property-attributes.block .accordion-header.active + .accordion-content {
+ display: block;
+}
+
+.property-attributes.block .accordions .accordion-header::after {
+ border-color: var(--body-color) transparent transparent transparent;
+ border-style: solid;
+ border-width: 6px 5px 0;
+ content: '';
+ position: absolute;
+ right: 15px;
+ transition: transform .2s linear;
+ transform: rotate(0);
+}
+
+.property-attributes.block .accordions .accordion-header:not(.active)::after {
+ transform: rotate(90deg);
+}
+
+div.idxDisclaimer {
+ max-width: var(--normal-page-width);
+ margin: 0 auto 3.5em;
+ font-size: var(--body-font-size-xxs);
+ line-height: var(--line-height-s);
+ color: var(--dark-grey);
+ padding: 0 15px 20px 15px;
+ background-color: var(--white);
+}
+
+@media (min-width: 900px) {
+ .property-attributes.block .attributes {
+ display: grid;
+ grid-template-columns: 1fr 1fr;
+ gap: 20px;
+ }
+
+ .property-attributes.block .accordion-content {
+ display: block;
+ }
+
+ .property-attributes.block .accordion-header,
+ .property-attributes.block .accordions .accordion-header::after {
+ border: none;
+ }
+
+ .property-attributes.block .accordion-content .table {
+ display: grid;
+ grid-template-columns: repeat(4, 1fr);
+ gap: 10px;
+ }
+}
\ No newline at end of file
diff --git a/blocks/property-attributes/property-attributes.js b/blocks/property-attributes/property-attributes.js
new file mode 100755
index 00000000..8d606053
--- /dev/null
+++ b/blocks/property-attributes/property-attributes.js
@@ -0,0 +1,149 @@
+import {
+ div, domEl, span,
+} from '../../scripts/dom-helpers.js';
+import {
+ formatNumber, phoneFormat, formatCurrency, toggleAccordion,
+} from '../../scripts/util.js';
+import {
+ decorateIcons,
+} from '../../scripts/aem.js';
+
+export function formatListToHTML(str) {
+ if (!str) {
+ return '';
+ }
+ const strParts = str.split(',').map((part) => part.trim());
+
+ const strElements = [];
+ strParts.forEach((part, index) => {
+ if (index > 0) {
+ strElements.push(document.createElement('br'));
+ }
+ strElements.push(part); // Push the trimmed string directly
+ });
+
+ return div({ class: 'td' }, ...strElements);
+}
+
+export default async function decorate(block) {
+ block.innerHTML = '';
+
+ if (!window.envelope) {
+ block.innerHTML = 'Property not found';
+ } else {
+ const property = window.envelope.propertyDetails;
+ const lotSF = property.lotSizeSquareFeet ? `${formatNumber(property.lotSizeSquareFeet)} ${property.interiorFeatures.livingAreaUnits}` : '';
+
+ const title = div({ class: 'title' }, 'Property Details');
+ const details = div({ class: 'attributes' },
+ div({ class: 'table' },
+ div({ class: 'label' }, 'Type'),
+ div({ class: 'td' }, property.propertySubType),
+ div({ class: 'label' }, 'Status'),
+ div({ class: 'td' }, property.mlsStatus),
+ div({ class: 'label' }, 'County'),
+ div({ class: 'td' }, property.countyOrParish),
+ div({ class: 'label' }, 'Year built'),
+ div({ class: 'td' }, property.yearBuilt),
+ div({ class: 'label' }, 'Beds'),
+ div({ class: 'td' }, property.bedroomsTotal),
+ div({ class: 'label' }, 'Full Baths'),
+ div({ class: 'td' }, property.bathroomsFull),
+ div({ class: 'label' }, 'Half Baths'),
+ div({ class: 'td' }, property.bathroomsHalf),
+ ),
+ div({ class: 'table' },
+ div({ class: 'label' }, 'Sq. Ft.'),
+ div({ class: 'td' }, formatNumber(property.livingArea)),
+ div({ class: 'label' }, 'Lot Size'),
+ div({ class: 'td' }, `${lotSF}, ${formatNumber(property.lotSizeAcres, 2)} acres`),
+ div({ class: 'label' }, 'Listing Id'),
+ div({ class: 'td' }, property.listingId),
+ div({ class: 'label' }, 'Courtesy Of'),
+ div({ class: 'td' }, `${property.courtesyOf} ${phoneFormat(property.listAgentPreferredPhone || property.listOfficePhone)}`),
+ div({ class: 'label' }, 'List Office Phone'),
+ div({ class: 'td' }, phoneFormat(property.listOfficePhone)),
+ div({ class: 'label' }, 'Buyer Agency Commission', div({ class: 'tooltip' },
+ span({ class: 'icon icon-info-circle-dark' }),
+ span({ class: 'tooltiptext' },
+ 'If the Buyer Agency Compensation provided for this listing is unclear, please contact the brokerage for more info.'),
+ ),
+ ),
+ div({ class: 'td' }, property.buyerAgentCommission),
+ ),
+ );
+ const features = div({ class: 'accordions' },
+ div({ class: 'accordion' }, // we might have to generate this dynamically
+ div({ class: 'accordion-header', onclick: (e) => toggleAccordion(e) }, 'Interior Features'),
+ div({ class: 'accordion-content' },
+ div({ class: 'table' },
+ div({ class: 'label' }, 'Fireplaces Total'),
+ div({ class: 'td' }, property.interiorFeatures.fireplaceFeatures || 0),
+ div({ class: 'label' }, 'Flooring'),
+ div({ class: 'td' }, formatListToHTML(property.interiorFeatures.flooring)),
+ div({ class: 'label' }, 'Living Area Units'),
+ div({ class: 'td' }, property.interiorFeatures.livingAreaUnits),
+ div({ class: 'label' }, 'Rooms Total'),
+ div({ class: 'td' }, property.bedroomsTotal + property.interiorFeatures.bathroomsTotal),
+ div({ class: 'label' }, 'Full Baths'),
+ div({ class: 'td' }, property.bathroomsFull),
+ div({ class: 'label' }, 'Half Baths'),
+ div({ class: 'td' }, property.bathroomsHalf),
+ div({ class: 'label' }, 'Bathrooms Total'),
+ div({ class: 'td' }, property.interiorFeatures.bathroomsTotal),
+ ),
+ ),
+ ),
+ div({ class: 'accordion' }, // we might have to generate this dynamically
+ div({ class: 'accordion-header', onclick: (e) => toggleAccordion(e) }, 'Exterior Features'),
+ div({ class: 'accordion-content' },
+ div({ class: 'table' },
+ div({ class: 'label' }, 'Lot/Land Description'),
+ div({ class: 'td' }, formatListToHTML(property.interiorFeatures.description)),
+ div({ class: 'label' }, 'Foundation'),
+ div({ class: 'td' }, property.interiorFeatures.foundation),
+ div({ class: 'label' }, 'Parking Spaces'),
+ div({ class: 'td' }, property.interiorFeatures.parkingFeatures),
+ ),
+ ),
+ ),
+ div({ class: 'accordion' }, // we might have to generate this dynamically
+ div({ class: 'accordion-header', onclick: (e) => toggleAccordion(e) }, 'Utility & Building Info'),
+ div({ class: 'accordion-content' },
+ div({ class: 'table' },
+ div({ class: 'label' }, 'Sewer'),
+ div({ class: 'td' }, property.utilityAndBuilding.sewer),
+ div({ class: 'label' }, 'Parcel Number'),
+ div({ class: 'td' }, property.utilityAndBuilding.parcelNumber),
+ div({ class: 'label' }, 'Cooling'),
+ div({ class: 'td' }, property.utilityAndBuilding.cooling),
+ div({ class: 'label' }, 'Water Source'),
+ div({ class: 'td' }, property.utilityAndBuilding.waterSource),
+ div({ class: 'label' }, 'Heating'),
+ div({ class: 'td' }, formatListToHTML(property.utilityAndBuilding.heating)),
+ div({ class: 'label' }, 'Tax Amount'),
+ div({ class: 'td' }, property.utilityAndBuilding.YrPropTax),
+ div({ class: 'label' }, 'Building Area Total'),
+ div({ class: 'td' }, formatNumber(property.utilityAndBuilding.buildingAreaTotal)),
+ div({ class: 'label' }, 'Price Per Sq Ft'),
+ div({ class: 'td' }, formatCurrency(property.utilityAndBuilding.pricePerSqFt)),
+ div({ class: 'label' }, 'Architectural Style'),
+ div({ class: 'td' }, formatListToHTML(property.utilityAndBuilding.architecturalStyle)),
+ ),
+ ),
+ ),
+ );
+
+ // disclaimer
+ const disclaimer = div({ class: 'idxDisclaimer' },
+ domEl('hr'),
+ property.idxDisclaimer,
+ );
+
+ block.append(title, details, features);
+ const section = document.querySelector('.property-attributes-container');
+ section.insertAdjacentElement('afterend', disclaimer);
+ }
+
+ decorateIcons(block);
+}
diff --git a/blocks/property-data/property-data.css b/blocks/property-data/property-data.css
new file mode 100644
index 00000000..627cf1a2
--- /dev/null
+++ b/blocks/property-data/property-data.css
@@ -0,0 +1,54 @@
+.section.property-data-container {
+ width: var(--full-page-width);
+ background-color: var(--tertiary-color);
+}
+
+.property-data.block .accordion-header {
+ cursor: pointer;
+ padding: 16px 30px 16px 0;
+ position: relative;
+ display: inline-block;
+ font-family: var(--font-family-primary);
+ font-weight: var(--font-weight-semibold);
+ line-height: 26px;
+ margin: 0 5px 0 0;
+ font-size: 22px;
+ width: 100%;
+ color: var(--primary-color);
+}
+
+.property-data.block .accordion .accordion-header::after {
+ border-color: var(--body-color) transparent transparent transparent;
+ border-style: solid;
+ border-width: 6px 5px 0;
+ content: '';
+ margin-top: -5px;
+ position: absolute;
+ right: 8px;
+ top: 50%;
+ transition: transform .3s linear;
+ transform: rotate(0);
+}
+
+.property-data.block .accordion .accordion-header:not(.active)::after {
+ transform: rotate(90deg);
+ transition: transform .3s linear;
+}
+
+.property-data.block .accordion-content {
+ display: none;
+ padding-bottom: 60px;
+}
+
+.property-data.block .accordion-header.active + .accordion-content {
+ display: block;
+}
+
+.property-data.block .accordion-content .row .head {
+ font-size: var(--heading-font-size-m);
+ font-weight: var(--font-weight-bold);
+ line-height: var(--line-height-s);
+ color: var(--body-color);
+ letter-spacing: var(--letter-spacing-xs);
+ margin-bottom: 10px;
+}
\ No newline at end of file
diff --git a/blocks/property-data/property-data.js b/blocks/property-data/property-data.js
new file mode 100644
index 00000000..194e8c1c
--- /dev/null
+++ b/blocks/property-data/property-data.js
@@ -0,0 +1,103 @@
+import { getMarketTrends } from '../../scripts/apis/creg/creg.js';
+import { div } from '../../scripts/dom-helpers.js';
+import {
+ formatCurrency, toggleAccordion,
+} from '../../scripts/util.js';
+
+function daysOnMarket(listingContractDate) {
+ const listingDate = new Date(listingContractDate);
+ const currentDate = new Date();
+ const timeDifference = currentDate - listingDate;
+ const daysDifference = Math.floor(timeDifference / (1000 * 60 * 60 * 24));
+ return daysDifference;
+}
+
+export default async function decorate(block) {
+ block.innerHTML = '';
+ let property;
+ let marketdata;
+
+ if (window.envelope.propertyDetails) {
+ property = window.envelope.propertyDetails;
+ const data = await getMarketTrends(
+ property.PropId,
+ property.Latitude,
+ property.Longitude,
+ property.PostalCode,
+ );
+ if (data) {
+ [marketdata] = data;
+ }
+ }
+
+ const trends = div({ class: 'accordions' },
+ div({ class: 'accordion' },
+ div({ class: 'accordion-header', onclick: (e) => toggleAccordion(e) }, 'Market Trends'),
+ div({ class: 'accordion-content' },
+ div({ class: 'row' },
+ div({ class: 'head' }, property.unstructuredAddress),
+ div({ class: 'head' }, `ZIP Code: ${property.postalCode}`),
+ ),
+ div({ class: 'row' },
+ div({ class: 'td' },
+ div({ class: 'label' }, 'List Price'),
+ div({ class: 'value' }, formatCurrency(property.listPrice)),
+ ),
+ div({ class: 'td' },
+ div({ class: 'label' }, 'Median List Price'),
+ div({ class: 'value' }, marketdata.total.medianListPrice.split(' ')[0]),
+ div({ class: 'chart' }),
+ ),
+ ),
+ div({ class: 'row' },
+ div({ class: 'td' },
+ div({ class: 'label' }, 'Sale Price'),
+ div({ class: 'value' }, property.closePrice || '-'),
+ ),
+ div({ class: 'td' },
+ div({ class: 'label' }, 'Median Sold Price'),
+ div({ class: 'value' }, marketdata.total.medianSalesPrice.split(' ')[0]),
+ div({ class: 'chart' }),
+ ),
+ ),
+ div({ class: 'row' },
+ div({ class: 'td' },
+ div({ class: 'label' }, 'Price/SQFT'),
+ div({ class: 'value' }, formatCurrency(property.utilityAndBuilding.pricePerSqFt)),
+ ),
+ div({ class: 'td' },
+ div({ class: 'label' }, 'AVG Price/SQFT'),
+ div({ class: 'value' }, marketdata.total.avgPriceArea.split(' ')[0]),
+ div({ class: 'chart' }),
+ ),
+ ),
+ div({ class: 'row' },
+ div({ class: 'td' },
+ div({ class: 'label' }, 'Days on Market'),
+ div({ class: 'value' }, property.closePrice ? 0 : daysOnMarket(property.listingContractDate)),
+ ),
+ div({ class: 'td' },
+ div({ class: 'label' }, 'AVG Days on Market'),
+ div({ class: 'value' }, marketdata.total.avgDaysOnMarket),
+ div({ class: 'chart' }),
+ ),
+ ),
+ div({ class: 'row' },
+ div({ class: 'label' }, 'Homes for Sale'),
+ div({ class: 'label' }, 'Homes Sold'),
+ ),
+ div({ class: 'row' },
+ div({ class: 'td' },
+ div({ class: 'value' }, marketdata.total.homesForSale),
+ div({ class: 'chart' }),
+ ),
+ div({ class: 'td' },
+ div({ class: 'value' }, marketdata.total.homesSold),
+ div({ class: 'chart' }),
+ ),
+ ),
+ ),
+ ),
+ );
+ block.append(trends);
+}
diff --git a/blocks/property/property.css b/blocks/property/property.css
new file mode 100644
index 00000000..0e3e3e06
--- /dev/null
+++ b/blocks/property/property.css
@@ -0,0 +1,372 @@
+.property.block .details hr {
+ background-color: var(--secondary-accent);
+ height: 1px;
+ border: 0;
+ box-shadow: none;
+ margin: 2rem 0;
+}
+
+.property.block .property-details {
+ display: flex;
+ flex-wrap: wrap;
+ padding: 15px 0 20px;
+}
+
+.property.block .property-details .backnav {
+ flex: 0 0 100%;
+ max-width: 100%;
+}
+
+.property.block .property-details .back a {
+ font-family: var(--font-family-primary);
+ font-size: 12px;
+ font-style: normal;
+ font-weight: 400;
+ letter-spacing: normal;
+ line-height: 130%;
+ color: #2a2223;
+ margin-bottom: 25px;
+ text-transform: uppercase;
+}
+
+.property.block .property-details > .save-share {
+ order: 0;
+ display: flex;
+ justify-content: flex-end;
+ gap: 10px;
+ width: 100%;
+}
+
+.property.block .property-details > .property-info {
+ order: 2;
+ flex: 0 0 100%;
+ max-width: 100%;
+}
+
+.property.block .property-info .property-address {
+ font-size: var(--heading-font-size-l);
+ font-style: normal;
+ font-weight: 400;
+ letter-spacing: normal;
+ line-height: var(--line-height-m);
+ color: #2a2223;
+ font-family: var(--font-family-secondary);
+}
+
+.property.block .property-info .property-specs {
+ font-family: var(--font-family-primary);
+ font-size: var(--body-font-size-xs);
+ font-style: normal;
+ font-weight: 400;
+ letter-spacing: normal;
+ line-height: var(--line-height-m);
+ color: #2a2223;
+}
+
+.property.block .property-info .courtesy {
+ font-family: var(--font-family-primary);
+ font-size: var(--body-font-size-xxs);
+ font-style: normal;
+ font-weight: 400;
+ letter-spacing: normal;
+ line-height: var(--line-height-s);
+ color: var(--dark-grey);
+}
+
+.property.block .property-details .open-house {
+ display: flex;
+ flex-wrap: wrap;
+ margin: 1rem -15px 0;
+ padding: 0 15px;
+ font-family: var(--font-family-primary);
+}
+
+.property.block .property-details .row {
+ flex: 100%;
+ max-width: 100%;
+ display: flex;
+ flex-direction: column;
+ font-size: var(--body-font-size-xxs);
+ color: #2a2223;
+ font-weight: 700;
+ line-height: 30px;
+}
+
+.property.block .property-details .icon-wrapper,
+.property.block .property-details .meta-wrapper {
+ display: flex;
+ flex-direction: column;
+ margin: 0;
+}
+
+.property.block .property-details .icon-wrapper {
+ position: relative;
+ padding: 0 8px 0 32px;
+ border: 1px solid #2a2223;
+ line-height: 30px;
+ width: 115px;
+ height: 32px;
+}
+
+.property.block .property-details .icon-wrapper span {
+ position: absolute;
+ top: 2px;
+ left: 4px;
+ width: 24px;
+ height: 24px;
+}
+
+.property.block .property-details .icon-wrapper .label {
+ text-transform: capitalize;
+ font-weight: 400;
+}
+
+.property.block .property-details .datetime {
+ display: flex;
+}
+
+.property.block .property-details .datetime .date,
+.property.block .property-details .datetime .time {
+ font-weight: 700;
+}
+
+.property.block .property-details .datetime .time {
+ padding-left: 8px;
+}
+
+.property.block .property-details > .property-price {
+ order: 3;
+ font-family: var(--font-family-primary);
+ font-size: var(--heading-font-size-l);
+ font-style: normal;
+ font-weight: var(--font-weight-semibold);
+ letter-spacing: normal;
+ line-height: var(--line-height-xs);
+ color: var(--black);
+ margin-top: 1rem;
+}
+
+.property.block .button-container {
+ margin-bottom: 0;
+}
+
+.property.block .property-details .button-container a,
+.property.block .property-details .button-container a:hover {
+ color: #2a2223;
+ border: 1px solid var(--grey);
+ padding: 5px 15px;
+ background-color: var(--white);
+}
+
+.property.block .property-details .button-container .icon-heartempty,
+.property.block .property-details .button-container .icon-share-empty {
+ margin-right: 8px;
+}
+
+.property.block .property-details .button-container .icon-heartempty img,
+.property.block .property-details .button-container .icon-share-empty img{
+ width: 23px;
+ height: 23px;
+ vertical-align: middle;
+}
+
+.property.block .details-description .notes-header {
+ font-size: var(--heading-font-size-m);
+ line-height: var(--line-height-m);
+ font-weight: var(--font-weight-bold);
+ color: var(--body-color);
+ letter-spacing: var(--letter-spacing-xs);
+}
+
+.property.block .details-description .notes-body > div {
+ font-size: var(--body-font-size-xs);
+ line-height: var(--line-height-m);
+ background-color: #f3f2f2;
+ color: #463f40;
+ padding: 15px 20px;
+ margin: 20px 0;
+}
+
+.property.block .details-description .remarks {
+ margin-bottom: 40px;
+}
+.property.block .details-description .remarks > div {
+ height: 149px;
+ font-size: var(--body-font-size-xs);
+ line-height: var(--line-height-m);
+ color: var(--body-color);
+ overflow: hidden;
+}
+
+.property.block .details-description .remarks .view {
+ font-weight: var(--font-weight-bold);
+ color: var(--body-color);
+ font-size: var(--body-font-size-xs);
+ text-decoration: underline;
+}
+
+.property.block .details-description .remarks .view.more::after {
+ content: ">";
+ display: inline-block;
+ margin-left: 5px;
+}
+
+.property.block .details-description .remarks .view.less::before {
+ content: "<";
+ display: inline-block;
+ margin-right: 5px;
+}
+
+.property.block .contact-details {
+ display: flex;
+ flex-wrap: wrap;
+ color: var(--body-color);
+}
+
+.property.block .contact-details .profile {
+ margin-right: 16px;
+}
+
+.property.block .contact-details hr {
+ background-color: var(--secondary-accent);
+ height: 1px;
+ border: 0;
+ box-shadow: none;
+ margin: 10px 0;
+ width: 130px;
+}
+
+.property.block .company-name a {
+ font-size: var(--heading-font-size-s);
+ line-height: var(--line-height-xs);
+ font-weight: var(--font-weight-bold);
+ color: var(--body-color);
+ text-decoration: none;
+}
+
+.property.block .company-name .title,
+.property.block .company-email a,
+.property.block .company-phone,
+.property.block .company-phone a {
+ font-size: var(--body-font-size-xs);
+ color: var(--body-color);
+ margin-bottom: 5px;
+}
+
+.property.block .company-name .title {
+ text-transform: uppercase;
+ margin-top: 8px;
+ font-size: var(--body-font-size-xxs);
+}
+
+.property.block .company-email {
+ padding-bottom: 5px;
+}
+
+.property.block .cta {
+ display: flex;
+ flex-direction: row;
+ gap: .5em;
+ width: 100%;
+}
+
+.property.block .cta .button-container:not(:first-child) {
+ width: 100%;
+}
+
+.property.block .cta button {
+ font-weight: var(--font-weight-bold);
+ font-family: var(--font-family-primary);
+ border: 1px solid #000;
+ line-height: var(--line-height-m);
+ letter-spacing: var(--letter-spacing-s);
+ text-transform: uppercase;
+ background-color: var(--primary-color);
+ color: var(--tertiary-color);
+ font-size: var(--body-font-size-xs);
+ width: 100%;
+ font-weight: var(--font-weight-bold);
+ padding: 6px 8px;
+ height: auto;
+}
+.property.block .cta .contact {
+ display: none;
+}
+
+@media (min-width: 600px) {
+ .property.block .property-details > .property-price {
+ flex: 0 0 33.333%;
+ max-width: 33.333%;
+ margin-top: 0;
+ line-height: var(--line-height-m);
+ padding-left: 15px;
+ }
+
+ .property.block .property-details > .property-info {
+ flex: 0 0 66.667%;
+ max-width: 66.667%;
+ padding-right: 15px;
+ }
+}
+
+@media (min-width: 900px) {
+ .property.block .property-details > .save-share {
+ flex: 0 0 28%;
+ max-width: 28%;
+ order: 4;
+ }
+
+ .property.block .property-details > .property-price {
+ flex: 0 0 25%;
+ max-width: 25%;
+ }
+
+ .property.block .property-details > .property-info {
+ flex: 0 0 47%;
+ max-width: 47%;
+ }
+
+ .property.block .property-details .row {
+ flex-direction: row;
+ }
+
+ .property.block .property-details .meta-wrapper {
+ flex-direction: row;
+ }
+
+ .property.block .property-details .datetime {
+ margin-top: 0;
+ padding: 0 4px;
+ }
+
+ .property.block .property-details .datetime .date,
+ .property.block .property-details .datetime .time {
+ display: inline;
+ }
+
+ .property.block .row {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 15px;
+ width: 100%;
+ }
+
+ .property.block .row .details {
+ flex: 0 0 66.667%;
+ max-width: 66.667%;
+ margin-right: 10px;
+ }
+
+ .property.block .row .contact-details {
+ flex: 0 0 33.333%;
+ max-width: 33.333%;
+ }
+
+ .property.block #remark-content {
+ height: auto;
+ }
+
+ .property.block .details-description .remarks > a {
+ display: none;
+ }
+}
\ No newline at end of file
diff --git a/blocks/property/property.js b/blocks/property/property.js
new file mode 100644
index 00000000..02f70e0f
--- /dev/null
+++ b/blocks/property/property.js
@@ -0,0 +1,191 @@
+import { buildBlock, decorateBlock, decorateIcons, loadBlock } from '../../scripts/aem.js';
+import { getDetails, getEnvelope } from '../../scripts/apis/creg/creg.js';
+import {
+ a, button, div, domEl, img, p, span,
+} from '../../scripts/dom-helpers.js';
+import {
+ formatCurrency, formatNumber, formatDate, getImageURL, to12HourTime,
+} from '../../scripts/util.js';
+
+function toggleHeight() {
+ const content = document.getElementById('remark-content');
+ const link = document.querySelector('.remarks .view');
+ if (content.style.height === '149px') {
+ content.style.height = 'auto';
+ link.textContent = 'View Less';
+ link.classList.remove('more');
+ link.classList.add('less');
+ } else {
+ content.style.height = '149px';
+ link.textContent = 'View More';
+ link.classList.remove('less');
+ link.classList.add('more');
+ }
+}
+
+/**
+ * Retrieves the property ID from the current URL path.
+ * @returns {string|null} The property ID if found in the URL path, or null if not found.
+ */
+function getPropIdFromPath() {
+ const url = window.location.pathname;
+ const match = url.match(/pid-(\d+)/);
+ if (match && match[1]) {
+ return match[1];
+ }
+ return null;
+}
+
+export default async function decorate(block) {
+ let propId = getPropIdFromPath(); // assumes the listing page pathname ends with the propId
+ // TODO: remove this test propId
+ if (!propId) propId = '376611673'; // commercial '368554873'; // '375215759'; // luxury '358207023';
+
+ window.envelope = await getEnvelope(propId);
+ window.listing = await getDetails(propId);
+ block.innerHTML = '';
+
+ if (!window.envelope) {
+ block.innerHTML = 'Property not found';
+ } else {
+ const property = window.envelope.propertyDetails;
+ const [details] = window.listing;
+ const propertyPrice = formatCurrency(property.listPrice);
+ const propertyAddress = window.envelope.addressLine1;
+ const propertyAddress2 = window.envelope.addressLine2;
+ const rooms = property.bedroomsTotal + property.interiorFeatures.bathroomsTotal;
+ const bedBath = property.bedroomsTotal ? `${property.bedroomsTotal} bed / ${property.interiorFeatures.bathroomsTotal} bath` : '';
+ const livingSpace = property.livingArea ? `${formatNumber(property.livingArea)} ${property.interiorFeatures.livingAreaUnits}` : '';
+ const lotSF = property.lotSizeSquareFeet ? `${formatNumber(property.lotSizeSquareFeet)} ${property.interiorFeatures.livingAreaUnits}` : '';
+ const lotAcre = property.lotSizeAcres ? `${formatNumber(property.lotSizeAcres, 2)} acres lot size` : '';
+ let propertySpecs = bedBath;
+ propertySpecs += rooms ? ` / ${livingSpace}` : livingSpace;
+ propertySpecs += rooms ? ` / ${lotSF}` : '';
+ propertySpecs += property.lotSizeArea ? `, ${lotAcre}` : '';
+ propertySpecs += propertySpecs.length ? ` / ${property.propertySubType}` : '';
+ const propertyCourtesyOf = property.courtesyOf;
+
+ const propertyDetails = div({ class: 'property-details' },
+ div({ class: 'property-info' },
+ div({ class: 'property-address' }, propertyAddress, document.createElement('br'), propertyAddress2),
+ div({ class: 'property-specs' }, propertySpecs),
+ div({ class: 'courtesy' }, propertyCourtesyOf),
+ details.OpenHouses.length ? div({ class: 'open-house' },
+ div({ class: 'row' },
+ div({ class: 'icon-wrapper' },
+ span({ class: 'icon icon-openhouse' }),
+ div({ class: 'label' }, 'Open House'),
+ ),
+ div({ class: 'meta-wrapper' }),
+ ),
+ ) : '',
+ ),
+ div({ class: 'property-price' }, propertyPrice),
+ div({ class: 'save-share' },
+ p({ class: 'button-container' },
+ a({ href: '', 'aria-label': 'Save property listing', class: 'save button secondary' },
+ span({ class: 'icon icon-heartempty' }),
+ 'Save',
+ ),
+ ),
+ p({ class: 'button-container' },
+ a({ href: '', 'aria-label': 'Share property listing', class: 'share button secondary' },
+ span({ class: 'icon icon-share-empty' }),
+ 'Share',
+ ),
+ ),
+ ),
+ );
+ block.append(propertyDetails);
+
+ if (details.OpenHouses.length) {
+ details.OpenHouses.forEach((openHouse) => {
+ const temp = div({ class: 'datetime' },
+ div({ class: 'date' }, formatDate(openHouse.OpenHouseDate)),
+ div({ class: 'time' }, to12HourTime(openHouse.OpenHouseStartTime), ' to ', to12HourTime(openHouse.OpenHouseEndTime)),
+ );
+ block.querySelector('.open-house .meta-wrapper').append(temp);
+ });
+ }
+
+ if (property.isLuxury) {
+ const luxury = div({ class: 'luxury' },
+ img({ src: '/icons/lux_mark_classic_blk.svg', alt: 'Luxury Property' }),
+ );
+ propertyDetails.prepend(luxury);
+ }
+ const nav = div({ class: 'backnav' },
+ div({ class: 'back' },
+ a({ href: '#' }, 'Back'),
+ ),
+ );
+ propertyDetails.prepend(nav);
+ decorateIcons(block);
+
+ // Load the carousel slider block
+ const carousel = buildBlock('carousel-slider', '');
+ block.append(carousel);
+ decorateBlock(carousel);
+ loadBlock(carousel);
+
+ // create contact info
+ const description = div({ class: 'details-description' },
+ div({ class: 'row' },
+ div({ class: 'details' },
+ div({ class: 'notes' },
+ div({ class: 'notes-header' }, 'Your Notes'),
+ div({ class: 'notes-body' },
+ div({ class: 'placeholder' }, 'To add notes, please save this property.'),
+ ),
+ ),
+ domEl('hr'),
+ div({ class: 'remarks' },
+ div({ id: 'remark-content'}, property.publicRemarks),
+ a({
+ href: '#', rel: 'noopener', class: 'view more', onclick: toggleHeight,
+ }, 'View More'),
+ ),
+ ),
+ div({ class: 'contact-details' },
+ div({ class: 'contact-info' },
+ div({ class: 'company-name' }, 'Berkshire Hathaway HomeServices', domEl('br'), 'Commonwealth Real Estate'),
+ domEl('hr', { width: '130px', height: '1px', textAlign: 'left' }),
+ div({ class: 'company-email' }, a({ href: 'mailto:realestateinquiry@commonmoves.com' }, 'realestateinquiry@commonmoves.com')),
+ div({ class: 'company-phone' }, a({ href: 'tel:(508) 810-0700' }, '(508) 810-0700')),
+ ),
+ div({ class: 'cta' },
+ p({ class: 'button-container' }, button({ class: 'contact', href: '/fragments/contact-property-form' }, 'Contact')),
+ p({ class: 'button-container' }, button({ href: '/fragments/contact-property-form' }, 'See the property')),
+ p({ class: 'button-container' }, button({ href: '/fragments/contact-property-form' }, 'Make an offer')),
+ ),
+ ),
+ ),
+ );
+ block.append(description);
+
+ if (property.listAgentCd) {
+ const agent = window.propertyData.listAgent.reAgentDetail;
+ const info = block.querySelector('.contact-info');
+ const pic = getImageURL(agent.image);
+ const profile = div({ class: 'profile' }, img({ src: pic, alt: agent.name, width: '82px' }));
+ info.insertAdjacentElement('beforebegin', profile);
+ const name = block.querySelector('.company-name');
+ const link = a({ href: '#' }, agent.name); // TODO: add link to agent profile
+ name.replaceChildren(link);
+ if (agent.jobTitle) {
+ name.append(div({ class: 'title' }, JSON.parse(agent.jobTitle)));
+ }
+ if (agent.team) {
+ // name.append(div({ class: 'team' }, agent.team));
+ }
+ const email = block.querySelector('.company-email a');
+ email.textContent = agent.email;
+ email.href = `mailto:${agent.email}`;
+ const label = block.querySelector('.company-phone');
+ label.prepend('Direct: ');
+ const phone = block.querySelector('.company-phone a');
+ phone.textContent = agent.telephone;
+ phone.href = `tel:${agent.telephone}`;
+ }
+ }
+}
diff --git a/icons/share-empty.svg b/icons/share-empty.svg
new file mode 100644
index 00000000..be35c1b7
--- /dev/null
+++ b/icons/share-empty.svg
@@ -0,0 +1,9 @@
+
\ No newline at end of file
diff --git a/scripts/apis/creg/creg.js b/scripts/apis/creg/creg.js
index 4e42cceb..c2ba8314 100644
--- a/scripts/apis/creg/creg.js
+++ b/scripts/apis/creg/creg.js
@@ -94,3 +94,59 @@ export async function getEnvelope(listingId) {
});
});
}
+
+/**
+ * Gets the school details for the specified listing.
+ *
+ * @param {string} lat latitude
+ * @param {string} long longitude
+ * @return {Promise