diff --git a/blocks/hero-slides/hero-slides.css b/blocks/hero-slides/hero-slides.css new file mode 100644 index 00000000..37887160 --- /dev/null +++ b/blocks/hero-slides/hero-slides.css @@ -0,0 +1,133 @@ +.section.hero-slides-container { + padding: 0; +} + +.section > .hero-slides-wrapper { + max-width: unset; + margin: unset; +} + +.hero-slides { + width: 100%; + position: relative; + aspect-ratio: 600/686; +} + +@media (min-width: 600px) { + .hero-slides { + height: 580px; + } +} + +.hero-slides .slide { + display: flex; + position: absolute; + height: 100%; + width: 100%; + top: 0; + left: 0; + flex-direction: column; + background-color: #394c5a; + opacity: 0; + transition: opacity 0.9s ease; +} + +.hero-slides .slide.active { + opacity: 1; + z-index: 2; +} + +@media (min-width: 600px) { + .hero-slides .slide { + flex-direction: row; + } +} + +.hero-slides .slide .image { + flex: 0 0 66%; + height: 100%; +} + +.hero-slides .slide .image img { + object-fit: cover; + object-position: top; + width: 100%; + height: 100%; +} + + +.hero-slides .slide .text { + color: var(--text-color-on-black-background); + align-self: center; + padding: 10px; + display: flex; + flex-direction: column; + justify-content: center; + height: 100%; + + /* font size is relative to screen width, but limited in min and max size. + the actual texts then use `em` to scale with this font size. + */ + font-size: clamp(11px, 1.5vw, 16px); +} + +@media (min-width: 600px) { + .hero-slides .slide .text { + height: 100%; + padding: 0 40px; + } +} + +.hero-slides .slide .text .price { + text-transform: uppercase; + color: var(--text-color-secondary-on-black-background); +} + +.hero-slides .slide .text .city { + margin: 0; + font-size: 1.5em; + line-height: 1.2; + font-weight: bold; +} + +@media screen and (min-width: 600px) { + .hero-slides .slide .text .city { + font-size: 2.5em; + line-height: 1; + } +} + +.hero-slides .slide .text .link { + text-transform: uppercase; + color: var(--text-color-secondary-on-black-background); +} + + +@media (min-width: 600px) { + .hero-slides .slideshow-buttons { + position: absolute; + bottom: 4rem; + right: 7%; + height: 20px; + display: flex; + justify-content: center; + gap: 0.7vw; + z-index: 3; + } + + .hero-slides .slideshow-buttons button { + height: 1.1vw; + aspect-ratio: 1/1; + padding: 0; + margin: 0; + border-radius: 50%; + border: none; + background-color: #bbb; + transition: background-color 0.3s ease; + } + + .hero-slides .slideshow-buttons button.active { + background-color: white; + } +} + diff --git a/blocks/hero-slides/hero-slides.js b/blocks/hero-slides/hero-slides.js new file mode 100644 index 00000000..d4ba1902 --- /dev/null +++ b/blocks/hero-slides/hero-slides.js @@ -0,0 +1,125 @@ +import { createOptimizedPicture, readBlockConfig } from '../../scripts/lib-franklin.js'; + +/** + * Slideshow with luxury listings. Supports swiping on touch screens. + * Also supports manually adding content into the block. + * @param block + */ +export default async function decorate(block) { + const config = readBlockConfig(block); + const listings = await fetchListings(config); + block.textContent = ''; + const { goToSlide } = setupSlideControls(block); + + const slideshowButtons = document.createElement('div'); + slideshowButtons.classList.add('slideshow-buttons'); + + listings.forEach((listing, index) => { + const slide = document.createElement('a'); + slide.classList.add('slide'); + slide.href = listing.path; + + const imageSizes = [ + // desktop + { media: '(min-width: 600px)', height: '600' }, + // tablet and mobile sizes: + { media: '(min-width: 400px)', height: '600' }, + { width: '400' }, + ]; + const picture = listing.picture || createOptimizedPicture( + listing.image, + listing.city, + index === 0, + imageSizes, + ); + slide.innerHTML = ` +
${picture.outerHTML}
+
+

${plainText(listing.city)}

+

${plainText(listing.price)}

+ LEARN MORE +
`; + block.append(slide); + + const button = document.createElement('button'); + button.ariaLabel = `go to listing in ${listing.city}`; + button.addEventListener('click', () => goToSlide(index)); + slideshowButtons.append(button); + + if (index === 0) { + slide.classList.add('active'); + button.classList.add('active'); + } + }); + + block.append(slideshowButtons); +} + +async function fetchListings(config) { + const resp = await fetch(`${window.hlx.codeBasePath}/listings.json}`); + // eslint-disable-next-line no-return-await + return (await resp.json()).data; +} + +function setupSlideControls(block) { + function goToSlide(index) { + block.querySelector('.slide.active').classList.remove('active'); + [...block.querySelectorAll('.slide')].at(index).classList.add('active'); + + block.querySelector('.slideshow-buttons .active')?.classList.remove('active'); + [...block.querySelectorAll('.slideshow-buttons button')].at(index).classList.add('active'); + + // automatically advance slides. Reset timer when user interacts with the slideshow + autoplaySlides(); + } + + let autoSlideInterval = null; + function autoplaySlides() { + clearInterval(autoSlideInterval); + autoSlideInterval = setInterval(() => advanceSlides(+1), 3000); + } + + function advanceSlides(diff) { + const allSlides = [...block.querySelectorAll('.slide')]; + const activeSlide = block.querySelector('.slide.active'); + const currentIndex = allSlides.indexOf(activeSlide); + + const newSlideIndex = (allSlides.length + currentIndex + diff) % allSlides.length; + goToSlide(newSlideIndex); + } + + /** detect swipe gestures on touch screens to advance slides */ + function gestureStart(event) { + const touchStartX = event.changedTouches[0].screenX; + + function gestureEnd(endEvent) { + const touchEndX = endEvent.changedTouches[0].screenX; + const delta = touchEndX - touchStartX; + if (delta < -5) { + advanceSlides(+1); + } else if (delta > 5) { + advanceSlides(-1); + } else { + // finger not moved enough, do nothing + } + } + + block.addEventListener('touchend', gestureEnd, { once: true }); + } + + block.addEventListener('touchstart', gestureStart, { passive: true }); + + autoplaySlides(); + return { goToSlide }; +} + +/** + * make text safe to use in innerHTML + * @param text any string + * @return {string} sanitized html string + */ +function plainText(text) { + const fragment = document.createElement('div'); + fragment.append(text); + return fragment.innerHTML; +}