diff --git a/README.md b/README.md index dca70aad..90c552b7 100644 --- a/README.md +++ b/README.md @@ -24,3 +24,96 @@ npm run lint 1. Install the [AEM CLI](https://github.com/adobe/aem-cli): `npm install -g @adobe/aem-cli` 1. Start AEM Proxy: `aem up` (opens your browser at `http://localhost:3000`) 1. Open the `{repo}` directory in your favorite IDE and start coding :) + + +### Local Proxy for APIs +There are numerous APIs that this site relies on, including but not limited to: + +* Account (login, profile, etc) +* Blog +* Properties (search, listing, etc) +* Agent (details, search) +* Suggestions + +All of these APIs are hosted on an AMS system. To make use of them locally, a proxy is needed. + +There are two ways to set up the local proxy: + +* [Real Domain Proxy](#real-domain-proxy) + * Less configuration, but must be aware when proxy is/isn't running. +* [Local Proxy to Remote](#local-proxy-to-remote) + * More configuration, but explicit domain for proxy - clear delineation of live and proxied traffic. + +Either way, you need to download and install a local proxy. These instructions use [Proxyman](https://proxyman.io/download). Once you download and install Proxyman, you will need to also install the Proxyman CA certs. These are needed to be able to route the secure traffic. + + +#### Real Domain Proxy + +In this setup, the proxy is configured to route non-API traffic on the Staging domain to your local computer. So all requests _except_ the APIs will go the local host. The API calls will remain routed to the Stage domain. + +1. Create a new Allow List entry: + * _Tools_ => _Allow List_ + * Name: `BHHS Stage` + * Rule: `https://stage.commonmoves.com*` + * Any, Use Wildcard + * Include all subpaths of this URL: Checked + +1. Create a new Map Remote Entry: + * _Tools_ => _Map Remote_ + * Matching Rule: + * Name: `BHHS API` + * Rule: `https://stage.commonmoves.com(?!(/bin|/content)).*` + * Any, Use Regex + * Include all subpaths of this URL: Checked + * Map To: + * Protocol: `http` + * Host: localhost + * Port: 3000 + + + +#### Local Proxy to Remote + +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`: + 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 + +2. Create a self-signed cert. This is necessary for the AEM Cli to so that cookies can be persisted. It really doesn't matter what you answer for the questions. Just make sure you do not create the certs in the code repo, or add them to Git. + +(`/some/path` is used as a reference here, find an appropriate spot on your local computer) + +``` +$ cd /some/path +$ openssl req -x509 -sha256 -nodes -newkey rsa:2048 -days 365 -keyout localhost.key -out localhost.crt +``` + +3. Create a new Allow List entry: + * _Tools_ => _Allow List_ + * Name: *Localhost* + * Matching Rule `https:\/\/proxyman\.debug:\d+.*` + * Any, Use Regex + * Include all subpaths of this URL: checked + +4. Create a new Map Remote Entry: + * _Tools_ => _Map Remote_ + * Matching Rule + * Name: *BHHS API* + * Rule: `https://proxyman.debug:\d+(/bin|/content).*` + * Any, Use Regex + * Include all subpaths of this URL: checked + * Map To + * Protocol: `https` + * Host: `stage.commonmoves.com` + * Port: `443` + +5. Staring the AEM local proxy requires also passing in the Cert info, to run using https: + +(`/some/path` is the same place you previously created the cert) + +``` +% aem up --tls-cert /some/path/localhost.crt --tls-key /some/path/localhost.key +``` + +To view Proxied site use: https://proxyman.debug:3000/ diff --git a/blocks/agent-about/agent-about.css b/blocks/agent-about/agent-about.css new file mode 100644 index 00000000..49b456d0 --- /dev/null +++ b/blocks/agent-about/agent-about.css @@ -0,0 +1,101 @@ +.agent-about.block { + display: flex; + flex-direction: column; + margin-top: 3rem; + line-height: var(--line-height-m); + font-size: var(--body-font-size-xs); + letter-spacing: normal; +} + +.agent-about.block a { + cursor: pointer; +} + +.agent-about.block .hide { + display: none; +} + +.agent-about.block a.view-more::after, +.agent-about.block a.view-less::after { + display: inline-block; + margin-top: 1rem; + text-decoration: underline; +} + +.agent-about.block a.view-more::after { + content: "View More"; +} + +.agent-about.block a.view-less::after { + content: "View Less"; +} + +.agent-about.block>div.cols-1, +.agent-about.block>div.cols-2, +.agent-about.block>div.cols-3 { + padding-bottom: 1rem; +} + +.agent-about.block>div>div:first-of-type { + font-size: var(--body-font-size-s); + font-weight: var(--font-weight-bold); + margin-bottom: 0.5rem; + text-transform: capitalize; +} + +.agent-about.block ul { + list-style: unset; + margin: 0 0 0 1.25rem; +} + +@media (min-width: 600px) { + .agent-about.block { + flex-direction: row; + } + + .agent-about.block>div.cols-1 { + flex: 0 0 41.6667%; + max-width: 41.6667%; + padding-right: 1rem; + } + + .agent-about.block>div.cols-2 { + flex: 0 0 33.33%; + max-width: 33.33%; + padding-left: 1rem; + padding-right: 1rem; + } + + .agent-about.block>div.cols-3 { + flex: 0 0 25%; + max-width: 25%; + padding-left: 1rem; + padding-right: 1rem; + } +} + +@media (min-width: 900px) { + .agent-about.block>div.cols-1 { + flex: 0 0 50%; + max-width: 50%; + } + + .agent-about.block>div.cols-2, + .agent-about.block>div.cols-3 { + flex: 0 0 25%; + max-width: 25%; + } + + .agent-about.block { + font-size: var(--body-font-size-s); + } + + .agent-about.block>div>div:first-of-type { + font-size: var(--body-font-size-m); + } + + .agent-about.block a.view-more::after, + .agent-about.block a.view-less::after { + font-size: var(--body-font-size-s); + } +} diff --git a/blocks/agent-about/agent-about.js b/blocks/agent-about/agent-about.js new file mode 100644 index 00000000..80949538 --- /dev/null +++ b/blocks/agent-about/agent-about.js @@ -0,0 +1,61 @@ +import { + a, div, ul, li, +} from '../../scripts/dom-helpers.js'; + +const viewMoreOnClick = (name, anchor, block) => { + anchor.addEventListener('click', () => { + if (anchor.classList.contains('view-more')) { + anchor.classList.remove('view-more'); + anchor.classList.add('view-less'); + block.querySelector(`.${name}`).classList.remove('hide'); + block.querySelector(`.${name}-truncate`).classList.add('hide'); + } else { + anchor.classList.remove('view-less'); + anchor.classList.add('view-more'); + block.querySelector(`.${name}`).classList.add('hide'); + block.querySelector(`.${name}-truncate`).classList.remove('hide'); + } + }); +}; + +export default function decorate(block) { + const children = [...block.children]; + if (children?.length) { + children.forEach((child, index) => { + child.classList.add(`cols-${index + 1}`); + if (index === 0) { + const name = 'about-text'; + const threshold = 245; + child.children[1].classList.add(name); + if (child.children[1].textContent > threshold) { + 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' }); + child.append(anchor); + viewMoreOnClick(name, anchor, block); + } + } else { + const threshold = 3; + const name = child.children[0].textContent.toLowerCase().replace(/\s/g, '-'); + const liItems = child.children[1].querySelectorAll('li'); + child.children[1].classList.add(name); + + if (liItems.length > threshold) { + child.children[1].classList.add('hide'); + const tempUl = ul({ }); + Array.from(child.children[1].querySelectorAll('li')) + .slice(0, threshold).forEach((liItem) => { + const tempLi = li({}, liItem.textContent); + tempUl.append(tempLi); + }); + + child.append(div({ class: `${name}-truncate` }, tempUl)); + const anchor = a({ class: 'view-more' }); + child.append(anchor); + viewMoreOnClick(name, anchor, block); + } + } + }); + } +} diff --git a/blocks/agent-profile/agent-profile.css b/blocks/agent-profile/agent-profile.css new file mode 100644 index 00000000..fa575e7f --- /dev/null +++ b/blocks/agent-profile/agent-profile.css @@ -0,0 +1,140 @@ +.agent-profile.block { + display: flex; + padding: 0 1rem; +} + +main .section.agent-profile-container .agent-profile-wrapper { + word-break: break-word; +} + +.agent-profile.block .profile-image img { + width: 9.375rem; + height: 11.875rem; +} + +.agent-profile.block .profile-content .contact-me { + margin-top: 1.5rem; + display: none; +} + +.agent-profile.block .profile-content { + padding-left: 1.5rem; + font-size: var(--body-font-size-s); +} + +.agent-profile.block .profile-content .name { + font-size: var(--body-font-size-xl); + line-height: var(--line-height-m); + margin-bottom: 0.5rem; +} + +.agent-profile.block .profile-content .designation, +.agent-profile.block .profile-content .license-number { + font-size: var(--body-font-size-xs); + text-transform: uppercase; + margin-bottom: 0.25rem; +} + +.agent-profile.block .profile-content .social ul { + display: flex; + margin-top: 1rem; + opacity: .5; +} + +.agent-profile.block .profile-content .social li { + margin-right: 0.5rem; +} + +.agent-profile.block .profile-content .email a, +.agent-profile.block .profile-content .website a { + font-size: var(--body-font-size-xs); + color: var(--black); + text-transform: lowercase; + word-wrap: break-word; +} + +.agent-profile.block .profile-content .contact-me 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; +} + +.agent-profile.block .profile-content .contact-me a:hover { + color: var(--primary-light); + background-color: var(--primary-color); +} + +.agent-profile.block .profile-content .website, +.agent-profile.block .profile-content .email { + margin-bottom: 0.25rem; +} + +.agent-profile.block .profile-content .phone { + font-size: var(--body-font-size-xs); +} + +.agent-profile.block .profile-content .phone li { + margin-bottom: 0.25rem; +} + +@media (min-width: 600px) { + .agent-profile.block .profile-content .contact-me { + display: block; + } +} + +@media (min-width: 1200px) { + main .section.agent-profile-container { + position: relative; + } + + main .section.agent-profile-container .agent-profile-wrapper { + position: absolute; + display: flex; + left: auto; + width: 34rem; + right: 0; + bottom: 4.625rem; + padding: 1.875rem 1.875rem 0; + z-index: 1; + background-color: var(--white); + } + + .agent-profile.block { + position: relative; + line-height: var(--line-height-s); + } + + .agent-profile.block .profile-image img { + width: 13.125rem; + height: 15.625rem; + } + + .agent-profile.block .profile-content .phone { + font-size: var(--body-font-size-s); + margin-top: 2px; + line-height: var(--line-height-m); + } + + .agent-profile.block .profile-content .email a, + .agent-profile.block .profile-content .website a { + font-size: var(--body-font-size-xs); + } + + .agent-profile.block .profile-content { + padding-left: 1.875rem; + } + + .agent-profile.block .profile-content .designation, + .agent-profile.block .profile-content .license-number { + margin-bottom: unset; + } + + .agent-profile.block .profile-content .phone li { + margin-bottom: unset; + } +} diff --git a/blocks/agent-profile/agent-profile.js b/blocks/agent-profile/agent-profile.js new file mode 100644 index 00000000..61f963b7 --- /dev/null +++ b/blocks/agent-profile/agent-profile.js @@ -0,0 +1,119 @@ +import { decorateIcons, getMetadata } from '../../scripts/aem.js'; +import { + a, div, h1, ul, li, img, span, +} from '../../scripts/dom-helpers.js'; + +const getPhoneDiv = () => { + let phoneDiv; + let phoneUl; + + if (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'))); + } + + if (phoneUl) { + phoneDiv = div({ class: 'phone' }); + phoneDiv.append(phoneUl); + return phoneDiv; + } + + return phoneDiv; +}; + +const getWebsiteDiv = () => { + let websiteDiv; + const websiteUrl = getMetadata('website'); + + if (websiteUrl) { + const text = 'my website'; + const anchor = a({ href: websiteUrl, title: text, 'aria-label': text }, text); + websiteDiv = div({ class: 'website' }, anchor); + } + + return websiteDiv; +}; + +const getEmailDiv = () => { + let emailDiv; + const agentEmail = getMetadata('email'); + + if (agentEmail) { + const anchor = a({ href: `mailto:${agentEmail}`, title: agentEmail, 'aria-label': agentEmail }, agentEmail); + emailDiv = div({ class: 'email' }, anchor); + } + + return emailDiv; +}; + +const getImageDiv = () => { + const agentPhoto = getMetadata('photo'); + return div({ class: 'profile-image' }, img({ src: agentPhoto, alt: getMetadata('name'), loading: 'lazy' })); +}; + +const getSocialDiv = () => { + const socialDiv = div({ class: 'social' }); + let socialUl; + + ['facebook', 'instagram', 'linkedin'].forEach((x) => { + const url = getMetadata(x); + socialUl = socialUl || ul({}); + if (url) { + const socialLi = li({}, a({ + href: url, class: x, title: x, 'aria-label': x, + }, span({ class: `icon icon-${x}` }))); + socialUl.append(socialLi); + } + }); + + if (socialUl) { + socialDiv.append(socialUl); + return socialDiv; + } + + return null; +}; + +export default async function decorate(block) { + const profileImage = getImageDiv(); + const profileContent = div({ class: 'profile-content' }, + div({ class: 'name' }, h1({}, getMetadata('name'))), + div({ class: 'designation' }, getMetadata('designation')), + ); + + const licenseNumber = getMetadata('license-number'); + if (licenseNumber) { + profileContent.append(div({ class: 'license-number' }, `LIC# ${licenseNumber}`)); + } + + const emailDiv = getEmailDiv(); + if (emailDiv) { + profileContent.append(emailDiv); + } + + const websiteDiv = getWebsiteDiv(); + if (websiteDiv) { + profileContent.append(websiteDiv); + } + + const phoneDiv = getPhoneDiv(); + if (phoneDiv) { + profileContent.append(phoneDiv); + } + + const contactMeText = 'Contact Me'; + profileContent.append(div({ class: 'contact-me' }, + a({ href: '#', title: contactMeText, 'aria-label': contactMeText }, contactMeText))); + + const socialDiv = getSocialDiv(); + if (socialDiv) { + profileContent.append(socialDiv); + } + decorateIcons(profileContent); + block.replaceChildren(profileImage, profileContent); +} diff --git a/blocks/agent-testimonials/agent-testimonials.css b/blocks/agent-testimonials/agent-testimonials.css new file mode 100644 index 00000000..7da5fa4f --- /dev/null +++ b/blocks/agent-testimonials/agent-testimonials.css @@ -0,0 +1,94 @@ +.agent-testimonials.block { + position: relative; + width: 100%; + overflow: hidden; + border-radius: 10px; +} + +.agent-testimonials.block .testimonials { + display: flex; + transition: transform 0.5s ease; + width: 100%; +} + +.agent-testimonials.block .testimonials-inner { + display: flex; + width: 100%; +} + +.agent-testimonials.block .testimonials-item { + min-width: 100%; + box-sizing: border-box; + padding: 20px; + text-align: center; + display: flex; + flex-direction: column; + justify-content: space-between; + position: relative; +} + +.agent-testimonials.block .rating-stars { + color: var(--primary-color); + margin-bottom: 10px; + font-size: 25px; + margin-left: 50px; +} + +.agent-testimonials.block .review-text.full { + max-height: none; + font-size: 26px; +} + +.agent-testimonials.block .review-text { + font-size: 26px; + margin-bottom: 20px; + padding-left: 50px; + padding-right: 50px; +} + +.agent-testimonials.block .read-more { + font-size: 14px; + color: #607C8C; + cursor: pointer; + display: inline-block; + +} + +.agent-testimonials.block .reviewer-name { + font-weight: bold; + margin-bottom: 10px; + align-self: center; +} + +.agent-testimonials.block .testimonials-arrow { + position: absolute; + top: 50%; + transform: translateY(-50%); + background: none; + border: none; + color: var(--black); + font-size: 90px; + padding: 0; + cursor: pointer; +} + +.agent-testimonials.block .left-arrow { + left: 10px; +} + +.agent-testimonials.block .right-arrow { + right: 10px; +} + +.agent-testimonials.block .testimonials-counter { + position: absolute; + bottom: 10px; + right: 10px; + font-size: 16px; + color: #333; +} + +.agent-testimonials.block .remaining-text { + font: inherit; + font-size: 26px; +} \ No newline at end of file diff --git a/blocks/agent-testimonials/agent-testimonials.js b/blocks/agent-testimonials/agent-testimonials.js new file mode 100644 index 00000000..54419661 --- /dev/null +++ b/blocks/agent-testimonials/agent-testimonials.js @@ -0,0 +1,81 @@ +import { getMetadata } from '../../scripts/aem.js'; +import { button, div } from '../../scripts/dom-helpers.js'; + +export default function decorate(block) { + const leftArrow = button({ class: 'testimonials-arrow left-arrow' }, '<'); + const testimonialsInner = div({ class: 'testimonials-inner' }); + const testimonialsWrapper = div({ class: 'testimonials' }, testimonialsInner); + const rightArrow = button({ class: 'testimonials-arrow right-arrow' }, '>'); + const testimonialsCounter = div({ class: 'testimonials-counter' }); + block.append(leftArrow, testimonialsWrapper, rightArrow, testimonialsCounter); + + let currentIndex = 0; + let totalReviews = 0; + const updateCounter = () => { + testimonialsCounter.textContent = `${currentIndex + 1} of ${totalReviews}`; + }; + + const addReadMoreFunctionality = () => { + const reviewTexts = document.querySelectorAll('.review-text'); + reviewTexts.forEach((reviewText) => { + const words = reviewText.textContent.split(' '); + if (words.length > 75) { + const initialText = words.slice(0, 50).join(' '); + const remainingText = words.slice(50).join(' '); + const readMore = document.createElement('span'); + readMore.classList.add('read-more'); + readMore.textContent = '... Read more'; + + reviewText.innerHTML = `${initialText}${remainingText}`; + reviewText.appendChild(readMore); + reviewText.querySelector('.remaining-text').style.display = 'none'; + + readMore.addEventListener('click', () => { + const remainingTextSpan = reviewText.querySelector('.remaining-text'); + if (remainingTextSpan.style.display === 'none') { + remainingTextSpan.style.display = 'inline'; + readMore.textContent = ' Show less'; + } else { + remainingTextSpan.style.display = 'none'; + readMore.textContent = '... Read more'; + } + }); + } + }); + }; + + const externalID = getMetadata('externalid'); + fetch(`https://testimonialtree.com/Widgets/jsonFeed.aspx?widgetid=45133&externalID=${externalID}`) + .then((response) => response.json()) + .then((data) => { + const reviews = data.testimonialtreewidget.testimonials.testimonial.slice(0, 4); + totalReviews = reviews.length; + reviews.forEach((review) => { + const reviewElement = div({ class: 'testimonials-item' }, + div({ class: 'rating-stars' }, '★'.repeat(review.rating)), + div({ class: 'review-text-container' }, + div({ class: 'review-text' }, decodeURIComponent(review.testimonial.replace(/\+/g, ' '))), + ), + div({ class: 'reviewer-name' }, review.signature.replace(/\+/g, ' ') || 'Anonymous'), + ); + testimonialsInner.appendChild(reviewElement); + }); + addReadMoreFunctionality(); + updateCounter(); + }); + + const updatetestimonials = () => { + testimonialsInner.style.transform = `translateX(-${currentIndex * 100}%)`; + updateCounter(); + }; + + leftArrow.addEventListener('click', () => { + currentIndex = (currentIndex > 0) ? currentIndex - 1 : totalReviews - 1; + updatetestimonials(); + }); + + rightArrow.addEventListener('click', () => { + currentIndex = (currentIndex < totalReviews - 1) ? currentIndex + 1 : 0; + updatetestimonials(); + }); +} diff --git a/blocks/blog-details/blog-details.js b/blocks/blog-details/blog-details.js index 392cf2da..626f65e1 100644 --- a/blocks/blog-details/blog-details.js +++ b/blocks/blog-details/blog-details.js @@ -1,6 +1,3 @@ -const urlParams = new URLSearchParams(window.location.search); -export const API_HOST = urlParams.get('env') === 'stage' ? 'https://ignite-staging.bhhs.com' : 'https://www.bhhs.com'; - function getBlogDetailsPath() { const url = window.location.pathname; const startIndex = url.indexOf('/blog/blog-detail/') + '/blog/blog-detail/'.length; @@ -8,11 +5,11 @@ function getBlogDetailsPath() { } function buildApiPath() { - return `${API_HOST}/blog/blog-detail/jcr:content/${getBlogDetailsPath()}.json`; + return `/blog/blog-detail/jcr:content/${getBlogDetailsPath()}.json`; } function buildImageUrl(path) { - return `${API_HOST}${path}`; + return `${path}`; } /** diff --git a/blocks/blog-listing/blog-listing.js b/blocks/blog-listing/blog-listing.js index 7248bd4a..3bcacea9 100644 --- a/blocks/blog-listing/blog-listing.js +++ b/blocks/blog-listing/blog-listing.js @@ -2,9 +2,6 @@ import { readBlockConfig, } from '../../scripts/aem.js'; -const urlParams = new URLSearchParams(window.location.search); -export const API_HOST = urlParams.get('env') === 'stage' ? 'https://ignite-staging.bhhs.com' : 'https://www.bhhs.com'; - const DEFAULT_SCROLL_INTERVAL_MS = 6000; const DEFAULT_DESCRIPTION_LENGTH = 141; const category = window.location.pathname.split('/').filter(Boolean)[1] ?? ''; @@ -18,9 +15,9 @@ let scrollInterval; function buildApiPath(offset, count) { let url; if (category === '') { - url = `${API_HOST}/content/bhhs-franchisee/ma312/en/us/blog/jcr:content/root/blog_home.blogs.offset_${offset}.count_${count}.json`; + url = `/content/bhhs-franchisee/ma312/en/us/blog/_jcr_content/root/blog_home.blogs.offset_${offset}.count_${count}.json`; } else { - url = `${API_HOST}/content/bhhs-franchisee/ma312/en/us/blog/blog-category/jcr:content/root/blog_category.blogCategory.category_${category}.offset_${offset}.count_${count}.json`; + url = `/content/bhhs-franchisee/ma312/en/us/blog/blog-category/_jcr_content/root/blog_category.blogCategory.category_${category}.offset_${offset}.count_${count}.json`; } return url; } @@ -49,10 +46,6 @@ function trimDescription(description) { return `${trimmedDescription.substring(0, DEFAULT_DESCRIPTION_LENGTH)}...`; } -function buildImageUrl(path) { - return new URL(`${API_HOST}${path}`).href; -} - function prepareBlogArticleUrl(link) { return link.replace(/\.html$/, ''); } @@ -72,9 +65,9 @@ function buildBlogItem(block, data, addClass = false) { blogContainer.innerHTML = `
- - - ${title} + + + ${title}
diff --git a/blocks/cards/cards.css b/blocks/cards/cards.css index 07888440..85c99319 100644 --- a/blocks/cards/cards.css +++ b/blocks/cards/cards.css @@ -16,6 +16,17 @@ width: 100%; } +.cards.block.mobile-slide .cards-list { + flex-flow: row nowrap; + overflow-x: auto; + scroll-snap-type: x mandatory; + scrollbar-width: none; /* Firefox */ +} + +.cards-list::-webkit-scrollbar { + display: none; /* Chrome, Safari, and Opera */ +} + .cards.block .title { padding: 2em 0; } @@ -86,6 +97,16 @@ text-transform: uppercase; } +.cards.block.mobile-slide .cards-item .card-body { + padding: 0 30px; + height: 90px; +} + +.cards.block.mobile-slide .cards-item .card-body h4, +.cards.block.mobile-slide .cards-item .card-body p { + text-align: center; +} + .cards.block .cards-list .cards-item .card-body h3 { padding-top: 16px; font-size: var(--body-font-size-l); @@ -117,6 +138,34 @@ border-bottom: 1px solid var(--secondary-medium-grey); } +.cards.block.mobile-slide .cards-list .cards-item { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + min-width: 250px; + flex: 0 0 auto; + margin-right: 20px; + text-align: center; + border: 1px solid var(--secondary-accent); + height: 100%; + scroll-snap-align: start; +} + +.cards.block.shade-icon .cards-list .cards-item { + background-color: var(--light-grey); + padding: 0; + border-top: 1px solid #000; + min-height: 274px; + align-items: center; + justify-content: center; +} + +.cards.block.mobile-slide.icons .cards-list .cards-item { + flex-direction: column; + width: 90%; +} + .cards.block.icons .cards-list .cards-item .card-icon { margin-top: .5em; margin-right: 1em; @@ -147,15 +196,6 @@ margin-bottom: 0; } -.cards.block.shade-icon .cards-list .cards-item { - background-color: var(--light-grey); - padding: 0; - border-top: 1px solid #000; - min-height: 274px; - align-items: center; - justify-content: center; -} - .cards.block.tertiary-background.border-top .cards-list .cards-item { background-color: var(--tertiary-color); border-top: 1px solid var(--secondary-light); @@ -207,6 +247,27 @@ max-width: 750px; } + .cards.block.mobile-slide .cards-list { + flex-flow: column unset; + overflow-x: unset; + } + + .cards.block.mobile-slide .cards-list .cards-item { + flex-direction: column; + align-items: center; + justify-content: unset; + min-width: unset; + flex: unset; + margin: unset; + text-align: unset; + border: unset; + border-bottom: 1px solid var(--secondary-medium-grey); + } + + .cards.block.mobile-slide.icons .cards-list .cards-item { + flex-direction: row; + } + .cards.block.shade-icon .cards-list { flex-direction: row; column-gap: 20px; @@ -214,6 +275,15 @@ margin-bottom: 50px; } + .cards.block.mobile-slide .cards-item .card-body { + padding: unset; + } + + .cards.block.mobile-slide .cards-item .card-body h4, + .cards.block.mobile-slide .cards-item .card-body p { + text-align: left; + } + .cards.block .cards-list .cards-item .card-body h3 { font-size: var(--heading-font-size-m); } diff --git a/blocks/fragment/fragment.js b/blocks/fragment/fragment.js index 648a70e1..da92d8bb 100644 --- a/blocks/fragment/fragment.js +++ b/blocks/fragment/fragment.js @@ -18,7 +18,7 @@ import { * @returns {HTMLElement} The root element of the fragment */ export async function loadFragment(path) { - if (path && path.startsWith('/')) { + if (path?.startsWith('/')) { const resp = await fetch(`${path}.plain.html`); if (resp.ok) { const main = document.createElement('main'); diff --git a/blocks/quote-carousel/quote-carousel.css b/blocks/quote-carousel/quote-carousel.css index 62606fac..7fec8984 100644 --- a/blocks/quote-carousel/quote-carousel.css +++ b/blocks/quote-carousel/quote-carousel.css @@ -13,6 +13,11 @@ position: relative; } +.quote-carousel.block p, +.quote-carousel.block .pagination span { + color: var(--white); +} + .quote-carousel.block .title { text-align: center; text-transform: capitalize; @@ -98,13 +103,13 @@ transform: translateY(2px); } -.quote-carousel.block .controls-container svg { +.quote-carousel.block .controls-container img { color: var(--white); height: var(--body-font-size-m); width: var(--body-font-size-m); } -.quote-carousel.block .controls-container [name="prev"] svg { +.quote-carousel.block .controls-container [name="prev"] img { transform: rotate(-180deg); } diff --git a/blocks/quote-carousel/quote-carousel.js b/blocks/quote-carousel/quote-carousel.js index bc1dc97a..49f55a64 100644 --- a/blocks/quote-carousel/quote-carousel.js +++ b/blocks/quote-carousel/quote-carousel.js @@ -1,3 +1,8 @@ +import { + button, div, p, span, +} from '../../scripts/dom-helpers.js'; +import { decorateIcons } from '../../scripts/aem.js'; + /** * Returns block content from the spreadsheet * @@ -40,6 +45,7 @@ export default async function decorate(block) { const blockId = crypto && crypto.randomUUID ? crypto.randomUUID() : 'UUID-CRYPTO-NEEDS-HTTPS'; const dataUrl = block.querySelector('div > div > div:nth-child(2) > a').href; const title = getTitle(block); + const content = await getContent(dataUrl); // generate carousel content from loaded data block.setAttribute('id', blockId); block.innerHTML = ''; @@ -48,46 +54,44 @@ export default async function decorate(block) { titleElement.innerText = title.trim(); titleElement.classList.add('title'); - const controlsContainer = document.createElement('div'); - controlsContainer.classList.add('controls-container'); + const controlsContainer = div({ class: 'controls-container' }, + div({ class: 'pagination' }, + span({ class: 'index' }, '1'), + span({ class: 'of' }, 'of'), + span({ class: 'total' }, content.total), + ), + button({ + name: 'prev', class: 'control-button', 'aria-label': 'Previous', disabled: true, + }, + span({ class: 'icon icon-chevron-right-white' }), + ), + button({ name: 'next', class: 'control-button', 'aria-label': 'Next' }, + span({ class: 'icon icon-chevron-right-white' }), + ), + ); + decorateIcons(controlsContainer); const slidesContainer = document.createElement('div'); slidesContainer.classList.add('carousel-content'); block.replaceChildren(titleElement, slidesContainer, controlsContainer); - const content = await getContent(dataUrl); - if (content.data.length > 0) { [...content.data].forEach((row) => { - const rowContent = document.createElement('div'); if (!row.quote.startsWith('"')) { row.quote = `"${row.quote}`; } if (!row.quote.endsWith('"')) { row.quote = `${row.quote}"`; } - rowContent.classList.add('item'); - rowContent.innerHTML = ` -

${row.quote}

-

${row.author}

-

${row.position}

- `; - rowContent.classList.add('item'); + const rowContent = div({ class: 'item' }, + p({ class: 'quote' }, row.quote), + p({ class: 'author' }, row.author), + p({ class: 'position' }, row.position), + ); slidesContainer.appendChild(rowContent); }); slidesContainer.children[0].setAttribute('active', true); - - // generate container for carousel controls - controlsContainer.innerHTML = ` - - - - `; window.setTimeout(observeCarousel, 3000); } } diff --git a/scripts/apis/agent/agent.js b/scripts/apis/agent/agent.js index 538672cd..0209caf0 100644 --- a/scripts/apis/agent/agent.js +++ b/scripts/apis/agent/agent.js @@ -1,16 +1,17 @@ -const urlParams = new URLSearchParams(window.location.search); -export const DOMAIN = urlParams.get('env') === 'stage' ? 'ignite-staging.bhhs.com' : 'www.bhhs.com'; -const API_URL = `https://${DOMAIN}/bin/bhhs`; +/* + Agent API + */ /** * Search for Agents * @param {SearchParameters} params the parameters * @return {Promise} */ +// eslint-disable-next-line import/prefer-default-export export async function search(params) { return new Promise((resolve) => { const worker = new Worker(`${window.hlx.codeBasePath}/scripts/apis/agent/workers/search.js`); - const url = `${API_URL}/solrAgentSearchServlet?${params.asQueryString()}&_=${Date.now()}`; + const url = `/bin/bhhs/solrAgentSearchServlet?${params.asQueryString()}&_=${Date.now()}`; worker.onmessage = (e) => resolve(e.data); worker.postMessage({ url }); }); diff --git a/scripts/apis/agent/suggestion.js b/scripts/apis/agent/suggestion.js index 2ee25ad8..84d2887d 100644 --- a/scripts/apis/agent/suggestion.js +++ b/scripts/apis/agent/suggestion.js @@ -1,8 +1,5 @@ -import { DOMAIN } from './agent.js'; import Filter from './Filter.js'; -const API_URL = `https://${DOMAIN}/bin/bhhs`; - let suggestionFetchController; /** @@ -18,7 +15,7 @@ export async function getSuggestions(office, keyword) { suggestionFetchController = new AbortController(); const { signal } = suggestionFetchController; - const endpoint = `${API_URL}/suggesterServlet?search_type=agent&keyword=${keyword}&office_id=${office}&_=${Date.now()}`; + const endpoint = `/bin/bhhs/suggesterServlet?search_type=agent&keyword=${keyword}&office_id=${office}&_=${Date.now()}`; return fetch(endpoint, { signal }) .then((resp) => { if (resp.ok) { diff --git a/scripts/apis/creg/creg.js b/scripts/apis/creg/creg.js index a7b940e9..e7c2d9bd 100644 --- a/scripts/apis/creg/creg.js +++ b/scripts/apis/creg/creg.js @@ -3,10 +3,6 @@ // TODO: Use Sidekick Plugin for this import { getMetadata } from '../../aem.js'; -const urlParams = new URLSearchParams(window.location.search); -export const DOMAIN = urlParams.get('env') === 'stage' ? 'ignite-staging.bhhs.com' : 'www.bhhs.com'; -const CREG_API_URL = `https://${DOMAIN}/bin/bhhs`; - /** * @typedef {Object} SearchResults * @property {Array} properties @@ -27,7 +23,6 @@ export async function propertySearch(search) { const worker = new Worker(`${window.hlx.codeBasePath}/scripts/apis/creg/workers/properties.js`, { type: 'module' }); worker.onmessage = (e) => resolve(e.data); worker.postMessage({ - api: CREG_API_URL, search, }); }); @@ -44,7 +39,6 @@ export async function metadataSearch(search) { const worker = new Worker(`${window.hlx.codeBasePath}/scripts/apis/creg/workers/metadata.js`, { type: 'module' }); worker.onmessage = (e) => resolve(e.data); worker.postMessage({ - api: CREG_API_URL, search, }); }); @@ -61,7 +55,6 @@ export async function getDetails(...listingIds) { const worker = new Worker(`${window.hlx.codeBasePath}/scripts/apis/creg/workers/listing.js`, { type: 'module' }); worker.onmessage = (e) => resolve(e.data); worker.postMessage({ - api: CREG_API_URL, ids: listingIds, officeId, }); @@ -80,7 +73,6 @@ export async function getEconomicDetails(lat, long) { const worker = new Worker(`${window.hlx.codeBasePath}/scripts/apis/creg/workers/economic.js`, { type: 'module' }); worker.onmessage = (e) => resolve(e.data); worker.postMessage({ - api: CREG_API_URL, lat, long, }); diff --git a/scripts/apis/creg/suggestion.js b/scripts/apis/creg/suggestion.js index 50198598..d5771b66 100644 --- a/scripts/apis/creg/suggestion.js +++ b/scripts/apis/creg/suggestion.js @@ -1,6 +1,6 @@ -const urlParams = new URLSearchParams(window.location.search); -export const DOMAIN = urlParams.get('env') === 'stage' ? 'ignite-staging.bhhs.com' : 'www.bhhs.com'; -const CREG_API_URL = `https://${DOMAIN}/bin/bhhs`; +/* + Suggestion API + */ let suggestionFetchController; @@ -46,7 +46,7 @@ export async function get(keyword, country = undefined) { const { signal } = suggestionFetchController; - let endpoint = `${CREG_API_URL}/cregSearchSuggesterServlet?Keyword=${keyword}&_=${Date.now()}`; + let endpoint = `/bin/bhhs/cregSearchSuggesterServlet?Keyword=${keyword}&_=${Date.now()}`; if (country) { endpoint += `&Country=${country}`; } diff --git a/scripts/apis/creg/workers/economic.js b/scripts/apis/creg/workers/economic.js index 99fccffd..33bbb7ca 100644 --- a/scripts/apis/creg/workers/economic.js +++ b/scripts/apis/creg/workers/economic.js @@ -7,10 +7,10 @@ * @param {string} event.data.long longitude */ onmessage = async (event) => { - const { api, lat, long } = event.data; + const { lat, long } = event.data; const promises = []; promises.push( - fetch(`${api}/pdp/socioEconomicDataServlet?latitude=${lat}&longitude=${long}`) + fetch(`/bin/bhhs/pdp/socioEconomicDataServlet?latitude=${lat}&longitude=${long}`) .then((resp) => (resp.ok ? resp.json() : undefined)), ); diff --git a/scripts/apis/creg/workers/listing.js b/scripts/apis/creg/workers/listing.js index 4872eefa..25745d0b 100644 --- a/scripts/apis/creg/workers/listing.js +++ b/scripts/apis/creg/workers/listing.js @@ -6,11 +6,11 @@ * @param {string[]} event.data.ids list of listing ids */ onmessage = async (event) => { - const { api, ids, officeId } = event.data; + const { ids, officeId } = event.data; const promises = []; ids.forEach((id) => { promises.push( - fetch(`${api}/CregPropertySearchServlet?SearchType=ListingId&ListingId=${id}${officeId ? `&OfficeCode=${officeId}` : ''}`) + fetch(`/bin/bhhs/CregPropertySearchServlet?SearchType=ListingId&ListingId=${id}${officeId ? `&OfficeCode=${officeId}` : ''}`) .then((resp) => (resp.ok ? resp.json() : undefined)), ); }); diff --git a/scripts/apis/creg/workers/metadata.js b/scripts/apis/creg/workers/metadata.js index a1b3b156..fbc60875 100644 --- a/scripts/apis/creg/workers/metadata.js +++ b/scripts/apis/creg/workers/metadata.js @@ -9,11 +9,11 @@ import Search from '../search/Search.js'; * */ onmessage = async (event) => { - const { api, search } = event.data; + const { search } = event.data; const results = await Search.fromJSON(search) .then((s) => { try { - return fetch(`${api}/CregPropertySearchServlet?${s.asCregURLSearchParameters()}`); + return fetch(`/bin/bhhs/CregPropertySearchServlet?${s.asCregURLSearchParameters()}`); } catch (error) { // eslint-disable-next-line no-console console.log('Failed to fetch properties from API.', error); diff --git a/scripts/apis/creg/workers/properties.js b/scripts/apis/creg/workers/properties.js index 0f856ff1..e9981042 100644 --- a/scripts/apis/creg/workers/properties.js +++ b/scripts/apis/creg/workers/properties.js @@ -8,11 +8,11 @@ import Search from '../search/Search.js'; * @param {Search} event.data.searches search context */ onmessage = async (event) => { - const { api, search } = event.data; + const { search } = event.data; const results = await Search.fromJSON(search) .then((s) => { try { - return fetch(`${api}/CregPropertySearchServlet?${s.asCregURLSearchParameters()}`); + return fetch(`/bin/bhhs/CregPropertySearchServlet?${s.asCregURLSearchParameters()}`); } catch (error) { // eslint-disable-next-line no-console console.log('Failed to fetch properties from API.', error); diff --git a/scripts/apis/user.js b/scripts/apis/user.js index 2784c8bc..b926fcb4 100644 --- a/scripts/apis/user.js +++ b/scripts/apis/user.js @@ -2,10 +2,6 @@ * API for interacting with the User system. */ -const urlParams = new URLSearchParams(window.location.search); -export const DOMAIN = urlParams.get('env') === 'stage' ? 'ignite-staging.bhhs.com' : 'www.bhhs.com'; -const API_URL = '/bin/bhhs'; - /** * Confirms if user is logged in or not * @return {boolean} @@ -43,7 +39,7 @@ export function getUserDetails() { async function fetchUserProfile(username) { const time = new Date().getTime(); - const profileResponse = await fetch(`${API_URL}/cregUserProfile?Email=${encodeURIComponent(username)}&_=${time}`); + const profileResponse = await fetch(`/bin/bhhs/cregUserProfile?Email=${encodeURIComponent(username)}&_=${time}`); const json = profileResponse.json(); return json; } @@ -63,7 +59,7 @@ export function onProfileUpdate(listener) { /** Make changes to the user profile in session (does not save to the servlet) * This also triggers any listeners that are registered for profile updates * - * @param {Object} Updated user profile + * @param {Object} profile the updated user profile */ export function updateProfile(profile) { const userDetails = getUserDetails(); @@ -80,7 +76,7 @@ export function updateProfile(profile) { /** * Attempt to update the user profile. If successful, also update session copy. * Caller must look at response to see if it was successful, etc. - * @param {Object} Updated user profile + * @param {Object} profile the updated user profile * @returns response object with status, null if user not logged in */ export async function saveProfile(profile) { @@ -91,7 +87,7 @@ export async function saveProfile(profile) { const existingProfile = userDetails.profile; // Update profile in backend, post object as name/value pairs - const url = `${API_URL}/cregUserProfile`; + const url = '/bin/bhhs/cregUserProfile'; const postBody = { FirstName: profile.firstName, LastName: profile.lastName, @@ -139,7 +135,7 @@ export async function requestPasswordReset() { return null; } - const url = `${API_URL}/cregForgotPasswordtServlet`; + const url = '/bin/bhhs/cregForgotPasswordtServlet'; const postBody = { Email: userDetails.username, }; @@ -182,11 +178,11 @@ export function logout() { * @param {object} credentials * @param {string} credentials.username * @param {string} credentials.password - * @param {function} failureCallback Callback provided reponse object. + * @param {function} failureCallback Callback provided response object. * @return {Promise} User details if login is successful. */ export async function login(credentials, failureCallback = null) { - const url = `${API_URL}/cregLoginServlet`; + const url = '/bin/bhhs/cregLoginServlet'; let error; try { const resp = await fetch(url, { diff --git a/tools/sidekick/library.html b/tools/sidekick/library.html index 97a7232b..b1a1e719 100644 --- a/tools/sidekick/library.html +++ b/tools/sidekick/library.html @@ -28,7 +28,7 @@ const library = document.createElement('sidekick-library') library.config = { base: '/tools/sidekick/library.json', - } + } document.body.prepend(library)