diff --git a/blocks/economic-data/economic-data.css b/blocks/economic-data/economic-data.css
new file mode 100644
index 00000000..79ad51ac
--- /dev/null
+++ b/blocks/economic-data/economic-data.css
@@ -0,0 +1,176 @@
+.economic-data.block {
+ margin: 0;
+ padding: 20px;
+ box-sizing: border-box;
+}
+
+.economic-data .accordion-header {
+ border-top: 1px solid var(--grey);
+ cursor: pointer;
+ padding: 16px 30px 16px 0;
+ position: relative;
+ display: inline-block;
+ font-family: var(--font-family-georgia);
+ font-weight: var(--font-weight-semibold);
+ line-height: 26px;
+ margin: 0 5px 0 0;
+ font-size: 22px;
+ width: 100%;
+}
+
+.economic-data .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);
+}
+
+.economic-data .accordion .accordion-header:not(.active)::after {
+ transform: rotate(90deg);
+ transition: transform .3s linear;
+}
+
+.economic-data .accordion-content {
+ display: none;
+ padding-bottom: 60px;
+}
+
+.economic-data .accordion-header.active + .accordion-content {
+ display: block;
+}
+
+.economic-data .container {
+ display: flex;
+ flex-direction: column;
+}
+
+.economic-data .row {
+ display: flex;
+ flex-wrap: wrap;
+ border-bottom: 1px solid #ccc;
+ padding: 10px 0;
+}
+
+.economic-data.block .accordion-content .row:last-child {
+ border-bottom: none;
+}
+
+.economic-data .cell {
+ padding: 10px 10px 10px 0;
+ box-sizing: border-box;
+}
+
+.economic-data .cell-header {
+ font-weight: bold;
+ text-transform: uppercase;
+ font-size: var(--body-font-size-xs);
+ line-height: var(--line-height-xs);
+ letter-spacing: var(--letter-spacing-xs);
+}
+
+.economic-data .cell-1 {
+ width: 100%;
+ text-transform: uppercase;
+ font-size: 14px;
+}
+
+.economic-data .cell-1.cell-header {
+ padding: 0;
+}
+
+.economic-data .cell-2, .economic-data .cell-3, .economic-data .cell-4 {
+ width: 33.33%;
+}
+
+.economic-data .progress-bar {
+ width: calc(100% - 60px);
+ background-color: #f3f3f3;
+ height: 5px;
+ margin-left: 50px;
+ margin-top: -12px;
+ position: relative;
+}
+
+.economic-data .progress-owner, .progress-renter {
+ height: 100%;
+ position: absolute;
+ top: 0;
+}
+
+.economic-data .progress-owner {
+ background-color: var(--primary-color);
+ left: 0;
+}
+
+.economic-data .progress-renter {
+ background-color: var(--light-grey);
+ right: 0;
+}
+
+.economic-data .tooltip {
+ position: relative;
+ display: inline-block;
+ height: 19px;
+ width: 19px;
+ margin-left: 5px;
+}
+
+.economic-data .tooltip .icon-info-circle-dark {
+ display: none;
+}
+
+.economic-data .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-family: var(--font-family-proxima);
+ font-size: var(--body-font-size-s);
+ letter-spacing: var(--letter-spacing-s);
+ line-height: var(--line-height-s);
+}
+
+.economic-data .tooltip:hover .icon-info-circle {
+ display: none;
+}
+
+.economic-data .tooltip:hover .icon-info-circle-dark {
+ display: block;
+}
+
+.economic-data .tooltip:hover .tooltiptext {
+ visibility: visible;
+}
+
+.economic-data .tooltip .tooltiptext::before {
+ content: '';
+ position: absolute;
+ bottom: 100%;
+ left: 8px;
+ border-width: 10px;
+ border-style: solid;
+ border-color: transparent transparent var(--black) transparent;
+}
+
+@media (min-width: 900px) {
+ .economic-data .cell-1 {
+ width: 25%;
+ }
+
+ .economic-data .cell-2, .economic-data .cell-3, .economic-data .cell-4 {
+ width: 25%;
+ }
+}
diff --git a/blocks/economic-data/economic-data.js b/blocks/economic-data/economic-data.js
new file mode 100755
index 00000000..7b528b41
--- /dev/null
+++ b/blocks/economic-data/economic-data.js
@@ -0,0 +1,174 @@
+import { getDetails, getEconomicDetails } from '../../scripts/apis/creg/creg.js';
+import { div, span } from '../../scripts/dom-helpers.js';
+import { decorateIcons } from '../../scripts/aem.js';
+
+const keys = [
+ 'ListPriceUS',
+ 'StreetName',
+ 'City',
+ 'StateOrProvince',
+ 'PostalCode',
+ 'Latitude',
+ 'Longitude',
+ 'LotSizeAcres',
+ 'LotSizeSquareFeet',
+ 'LivingAreaUnits',
+ 'Media',
+ 'SmallMedia',
+ 'PropId',
+ 'OpenHouses',
+ 'CourtesyOf',
+];
+
+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.
+ */
+function getPropIdFromPath() {
+ const url = window.location.pathname;
+ const match = url.match(/pid-(\d+)/);
+ if (match && match[1]) {
+ return match[1];
+ }
+ 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];
+}
+
+function getHeaderLabels(title) {
+ switch (title.toLowerCase()) {
+ case 'occupancy':
+ return 'Occupancy';
+ case 'housing trends':
+ return 'Housing Trends';
+ case 'economic data':
+ return 'Economic Data';
+ default:
+ return 'Untitled';
+ }
+}
+
+function getColumnHeader(title, index) {
+ switch (title.toLowerCase()) {
+ case 'occupancy':
+ return ['Owned', 'Rented', 'Vacant'][index - 1];
+ case 'housing trends':
+ return ['Home Appreciation', 'Median Age'][index - 1];
+ case 'economic data':
+ return ['Median House. Income', 'Unemployment', 'Cost of Living Index'][index - 1];
+ default:
+ return '';
+ }
+}
+
+function getDataValue(item, title, index) {
+ switch (title.toLowerCase()) {
+ case 'occupancy':
+ if (index === 1) {
+ return `${item.ownerOccupiedPercent}%`;
+ }
+ if (index === 2) {
+ return `${item.renterOccupiedPercent}%`;
+ }
+ return `${item.vacancyPercent}%`;
+ case 'housing trends':
+ if (index === 1) {
+ return `${item.homeValueAppreciationPercent}%`;
+ }
+ return `${item.medianHomeAge}`;
+ case 'economic data':
+ if (index === 1) {
+ return `${item.medianIncome}`;
+ }
+ return `${item.unemploymentPercent}%`;
+ default:
+ return '';
+ }
+}
+
+function generateDataTable(block, title, socioEconData) {
+ // Create the accordion structure
+ const accordion = div({ class: 'accordion' },
+ div({ class: 'accordion-header', onclick: (e) => toggleAccordion(e) }, getHeaderLabels(title), div({ class: 'tooltip' },
+ span({ class: 'icon icon-info-circle' }),
+ span({ class: 'icon icon-info-circle-dark' }),
+ span({ class: 'tooltiptext' }, `${socioEconData.citation}`),
+ ),
+ ),
+ div({ class: 'accordion-content' },
+ div({ id: `${title.toLowerCase().replace(' ', '-')}-data-container`, class: 'container', role: 'grid' }),
+ ),
+ );
+ block.appendChild(accordion);
+
+ const container = document.getElementById(`${title.toLowerCase().replace(' ', '-')}-data-container`);
+
+ // Create header row
+ const headerRow = div({ class: 'row', role: 'row' },
+ div({ class: 'cell cell-1 cell-header', role: 'columnheader' }),
+ div({ class: 'cell cell-2 cell-header', role: 'columnheader' }, getColumnHeader(title, 1)),
+ div({ class: 'cell cell-3 cell-header', role: 'columnheader' }, getColumnHeader(title, 2)),
+ div({ class: 'cell cell-4 cell-header', role: 'columnheader' }, getColumnHeader(title, 3) ? getColumnHeader(title, 3) : ''),
+ );
+ container.appendChild(headerRow);
+
+ // Create data rows
+ socioEconData.data.forEach((item) => {
+ const dataRow = div({ class: 'row', role: 'row' },
+ div({ class: 'cell cell-1', role: 'cell' },
+ div({ role: 'presentation' }, `${item.level.charAt(0).toUpperCase() + item.level.slice(1)}: ${item.label}`),
+ ),
+ div({ class: 'cell cell-2', role: 'cell' },
+ getDataValue(item, title, 1),
+ title.toLowerCase() === 'occupancy'
+ ? div({ class: 'progress-bar' },
+ span({ class: 'progress-owner', style: `width: ${item.ownerOccupiedPercent}%` }),
+ span({ class: 'progress-renter', style: `width: ${100 - item.ownerOccupiedPercent}%` }),
+ ) : '',
+ ),
+ div({ class: 'cell cell-3', role: 'cell' }, getDataValue(item, title, 2)),
+ div({ class: 'cell cell-4', role: 'cell' }, title.toLowerCase() === 'housing trends' ? '' : getDataValue(item, title, 3)),
+ );
+
+ container.appendChild(dataRow);
+ });
+}
+
+export default async function decorate(block) {
+ let property = {};
+ let propId = getPropIdFromPath(); // assumes the listing page pathname ends with the propId
+ // TODO: remove this test propId
+ if (!propId) propId = '370882966';
+
+ const propertyData = await getPropertyByPropId(propId);
+ if (propertyData) {
+ property = pick(propertyData, ...keys);
+ if (property.Latitude && property.Longitude) {
+ const socioEconData = await getSocioEconomicData(property.Latitude, property.Longitude);
+ if (socioEconData) {
+ generateDataTable(block, 'Occupancy', socioEconData);
+ generateDataTable(block, 'Housing Trends', socioEconData);
+ generateDataTable(block, 'Economic Data', socioEconData);
+ }
+ }
+ }
+ decorateIcons(block);
+ window.property = property;
+}
diff --git a/blocks/fragment/fragment.css b/blocks/fragment/fragment.css
new file mode 100644
index 00000000..b3c58707
--- /dev/null
+++ b/blocks/fragment/fragment.css
@@ -0,0 +1 @@
+/* stylelint-disable-next-line no-empty-source */
diff --git a/blocks/fragment/fragment.js b/blocks/fragment/fragment.js
new file mode 100644
index 00000000..71e40355
--- /dev/null
+++ b/blocks/fragment/fragment.js
@@ -0,0 +1,57 @@
+/*
+* Fragment Block
+* Include content on a page as a fragment.
+* https://www.aem.live/developer/block-collection/fragment
+*/
+
+import {
+ decorateMain,
+} from '../../scripts/scripts.js';
+
+import {
+ loadBlocks,
+} from '../../scripts/aem.js';
+
+/**
+ * Loads a fragment.
+ * @param {string} path The path to the fragment
+ * @returns {HTMLElement} The root element of the fragment
+ */
+export async function loadFragment(path) {
+ if (path?.startsWith('/')) {
+ const resp = await fetch(`${path}.plain.html`);
+ if (resp.ok) {
+ const main = document.createElement('main');
+ main.innerHTML = await resp.text();
+
+ // reset base path for media to fragment base
+ const resetAttributeBase = (tag, attr) => {
+ main.querySelectorAll(`${tag}[${attr}^="./media_"]`).forEach((elem) => {
+ elem[attr] = new URL(elem.getAttribute(attr), new URL(path, window.location)).href;
+ });
+ };
+ resetAttributeBase('img', 'src');
+ resetAttributeBase('source', 'srcset');
+
+ decorateMain(main);
+ await loadBlocks(main);
+ return main;
+ }
+ }
+ return null;
+}
+
+export default async function decorate(block) {
+ const link = block.querySelector('a');
+ const path = link ? link.getAttribute('href') : block.textContent.trim();
+ const fragment = await loadFragment(path);
+
+ if (fragment) {
+ const fragmentSection = fragment.querySelector(':scope .section');
+
+ if (fragmentSection) {
+ block.closest('.section').classList.add(...fragmentSection.classList);
+ block.closest('.fragment').replaceWith(...fragment.childNodes);
+ }
+ }
+}
diff --git a/blocks/info-mouseover/info-mouseover.js b/blocks/info-mouseover/info-mouseover.js
index 25e679f5..10c170fa 100644
--- a/blocks/info-mouseover/info-mouseover.js
+++ b/blocks/info-mouseover/info-mouseover.js
@@ -16,7 +16,7 @@ export default async function decorate(block) {
const heading = block.closest('.section').querySelector('h1,h2,h3,h4,h5,h6');
const icon = document.createElement('span');
- icon.classList.add('icon', 'icon-info_circle');
+ icon.classList.add('icon', 'icon-info-circle');
positionIcon(heading, icon);
block.append(icon);
diff --git a/blocks/profile/profile.js b/blocks/profile/profile.js
index 087c423f..d2437a7b 100644
--- a/blocks/profile/profile.js
+++ b/blocks/profile/profile.js
@@ -119,7 +119,7 @@ function showNotification(type, iconHtml, message, message2) {
}
function showError(err) {
- showNotification('error', '', i18n('There was a problem processing your request.'), i18n(err));
+ showNotification('error', '', i18n('There was a problem processing your request.'), i18n(err));
}
function showSuccess(message) {
diff --git a/icons/info_circle_dark.svg b/icons/info-circle-dark.svg
similarity index 80%
rename from icons/info_circle_dark.svg
rename to icons/info-circle-dark.svg
index 91c154dc..eefe9959 100644
--- a/icons/info_circle_dark.svg
+++ b/icons/info-circle-dark.svg
@@ -4,6 +4,7 @@
aria-hidden="true"
tabindex="-1"
viewBox="0 0 18 18">
+
diff --git a/icons/info_circle.svg b/icons/info-circle.svg
similarity index 100%
rename from icons/info_circle.svg
rename to icons/info-circle.svg
diff --git a/scripts/apis/creg/creg.js b/scripts/apis/creg/creg.js
index 50a93e5b..a7b940e9 100644
--- a/scripts/apis/creg/creg.js
+++ b/scripts/apis/creg/creg.js
@@ -67,3 +67,22 @@ export async function getDetails(...listingIds) {
});
});
}
+
+/**
+ * Gets the economic details for the specified listing.
+ *
+ * @param {string} lat latitude
+ * @param {string} long longitude
+ * @return {Promise