diff --git a/.eslintignore b/.eslintignore index 158f7d56..944d9b0a 100644 --- a/.eslintignore +++ b/.eslintignore @@ -1,4 +1,5 @@ helix-importer-ui /scripts/alloy*.js -/tools/actions/ \ No newline at end of file +/tools/actions/ +/tools/importer \ No newline at end of file diff --git a/blocks/cards/cards.css b/blocks/cards/cards.css index 07916ddb..9cdc7b83 100644 --- a/blocks/cards/cards.css +++ b/blocks/cards/cards.css @@ -1,3 +1,5 @@ +/* stylelint-disable no-descending-specificity */ + .cards { --heading-font-size-m: 1.125rem; } @@ -64,6 +66,7 @@ } .cards > ul > li img { + display: inline-block; width: 100%; aspect-ratio: 4 / 3; object-fit: cover; @@ -73,6 +76,91 @@ text-decoration: none; } +.cards-pagination ul { + list-style-type: none; + display: flex; + gap: 0.5rem; + padding: 0; + justify-content: center; +} + +.cards-pagination ul li > a { + font-family: var(--body-font-family); + font-size: .75rem; + line-height: 1.313rem; + color: var(--text-color); + text-decoration: none; + border: 2px solid var(--text-color); + border-radius: 10px; + padding: 5px 10px; +} + +.cards-pagination ul li.active > a { + background-color: var(--text-color); + color: var(--background-color); + pointer-events: none; +} + +.cards-searchbar { + padding: 0.9375rem; + box-shadow: 0 4px 3px #00000040; + border-radius: 10px; + margin-bottom: 40px; +} + +.cards-searchbar label { + display: none; +} + +.cards-searchbar input { + max-width: unset; + border: 0; + border-radius: 0; + border-bottom: 2px solid var(--text-color); +} + +.cards-filterselect { + display: flex; + gap: 0.5rem; + flex-direction: column; +} + +.cards-filterselect .total, .cards-filterselect label { + font-family: var(--heading-font-family); + font-weight: 700; + line-height: 1.625rem; +} + +.cards-filterselect select { + appearance: none; + background-color: transparent; + border: none; + padding: 1rem 1rem 1rem 0; + width: 100%; + font-size: 18px; +} + +.cards-filterselect .select-group { + display: flex; + align-items: center; + cursor: pointer; + border-bottom: 2px solid var(--text-color); + max-width: 300px; + margin: 0 0 2rem; +} + +.cards-filterselect .select-group::after { + content: ''; + justify-self: end; + width: 11px; + height: 11px; + margin-top: 13px; + background-image: url('/icons/arrow-down.png'); + background-repeat: no-repeat; + background-size: contain; + padding-bottom: 1rem; +} + @media (min-width: 768px) { .cards > ul { grid-template-columns: 1fr 1fr 1fr; diff --git a/blocks/cards/cards.js b/blocks/cards/cards.js index ec701b82..3855beb9 100644 --- a/blocks/cards/cards.js +++ b/blocks/cards/cards.js @@ -1,12 +1,86 @@ -import { createOptimizedPicture } from '../../scripts/lib-franklin.js'; +import { createOptimizedPicture, getMetadata } from '../../scripts/lib-franklin.js'; -const host = 'https://www.24petwatch.com'; -const sitesearchUrl = `https://${window.location.hostname.includes('aem-stage') ? 'aem-stage' : 'www'}.24petwatch.com/bin/24pethealth/sitesearch.json`; -const sitesearchPayload = JSON.stringify({ - context: '/content/24petwatch/us/en/blog/jcr:content/root/container/container_177885842/container_51910998/contentsearchresults', - resultsPerPage: 3, - requestedPage: 1, -}); +const isCanada = window.location.pathname.startsWith('/ca/'); + +async function getTagFilters() { + let index = new URL(`${isCanada ? '/ca' : ''}/blog/tag-filters.json`, window.location.origin); + if (!window.location.hostname.includes('24petwatch.com')) { + index = new URL(`https://main--24petwatch--hlxsites.hlx.live${isCanada ? '/ca' : ''}/blog/tag-filters.json`); + } + const response = await fetch(index); + const json = await response.json(); + return json.data; +} + +async function loadBlogPosts() { + let index = new URL(`${isCanada ? '/ca' : ''}/blog/query-index.json`, window.location.origin); + if (!window.location.hostname.includes('24petwatch.com')) { + index = new URL(`https://main--24petwatch--hlxsites.hlx.live${isCanada ? '/ca' : ''}/blog/query-index.json`); + } + const chunkSize = 100; + const loadChunk = async (offset) => { + index.searchParams.set('limit', chunkSize); + index.searchParams.set('offset', offset); + + const response = await fetch(index); + const json = await response.json(); + + // Check if more has to be loaded + if (json.total > offset + chunkSize) { + return { + data: [...json.data, ...(await loadChunk(offset + 100)).data], + total: json.total, + }; + } + return { + data: json.data, + total: json.total, + }; + }; + + if (!window.blogPosts) { + window.blogPosts = await loadChunk(0); + } + return window.blogPosts; +} + +// eslint-disable-next-line no-unused-vars +const fetchBlogPosts = async (page = 1, tags = [], searchTerm = '', pagesize = 9) => { + let { data, total } = await loadBlogPosts(); + + // Filter by tags + if (tags.length > 0) { + data = data.filter(({ tags: blogTag }) => tags.some((t) => blogTag.includes(t))); + total = data.length; + } + + // Filter by search term + if (searchTerm) { + data = data + .filter(({ title, description }) => title.toLowerCase().includes(searchTerm.toLowerCase()) + || description.toLowerCase().includes(searchTerm.toLowerCase())); + total = data.length; + } + + // Filter by page + const start = (page - 1) * pagesize; + const end = start + pagesize; + + let currentPage = page; + if (currentPage > Math.ceil(total / pagesize)) { + currentPage = Math.ceil(total / pagesize); + } + if (currentPage < 1) { + currentPage = 1; + } + + return { + items: data.slice(start, end), + pages: Math.ceil(total / pagesize), + currentPage, + total, + }; +}; function wrapInAnchor(element, href) { const anchor = document.createElement('a'); @@ -17,34 +91,165 @@ function wrapInAnchor(element, href) { } function createBlogCard(item = {}) { - const blogThumbnail = `${host}${item.url.replace('.html', '')}.thumb.319.319.png`; - const blogUrl = `${host}${item.url.substring(item.url.indexOf('/blog')).replace('.html', '')}`; - - return `
Please check your spelling or try again using different keywords
+ `)); + } else { + createFilterSelect(block, total, tags.length > 0 ? tags[0] : null); + } + + createSearchBox(block, searchTerm); + createPagination(block, pages, currentPage); } export default async function decorate(block) { @@ -53,6 +258,13 @@ export default async function decorate(block) { await populateBlogTeaser(block); } + const isBlogGrid = block.classList.contains('blog-grid'); + if (isBlogGrid) { + block.textContent = ''; + block.closest('.cards-wrapper').querySelectorAll(':scope > *:not(.block)').forEach((e) => e.remove()); + await populateBlogGrid(block); + } + /* change to ul, li */ const ul = document.createElement('ul'); [...block.children].forEach((row) => { @@ -60,7 +272,6 @@ export default async function decorate(block) { li.innerHTML = row.innerHTML; [...li.children].forEach((div) => { const href = li.querySelector('a')?.href; - if (div.children.length === 1 && div.querySelector('picture')) { div.className = 'cards-card-image'; wrapInAnchor(div, href); @@ -74,7 +285,6 @@ export default async function decorate(block) { }); [...ul.querySelectorAll('img')] - .filter((img) => !(img.src || '').startsWith('http')) // do not optimize absolute images for now .forEach((img) => img.closest('picture').replaceWith(createOptimizedPicture(img.src, img.alt, false, [{ width: '750' }]))); block.textContent = ''; block.append(ul); diff --git a/blocks/columns/columns.css b/blocks/columns/columns.css index e4bb6a05..c4ce5c46 100644 --- a/blocks/columns/columns.css +++ b/blocks/columns/columns.css @@ -32,6 +32,63 @@ text-align: center; } +.columns.teaser { + background-color: #fff1d6; + border-radius: 0.625rem; + overflow: hidden; +} + +.columns.teaser > div, .columns.gray > div { + padding: 0; +} + +.columns.teaser p a.button.primary { + width: 100%; +} + +.columns.teaser > div > .columns-img-col { + order: 1; +} + +.columns.teaser > div > div { + font-size: 1rem; + line-height: 1.5625rem; +} + +.columns.teaser > div > div:last-child, .columns.gray > div > div:first-child { + margin-bottom: 1.25rem; + padding-left: 0.9375rem; + padding-right: 0.9375rem; +} + +.columns.teaser p.button-container, .columns.gray p.button-container { + margin: 0; +} + +.columns.teaser p.button-container a.buttonm, .columns.gray p.button-container a.button { + margin: 0.3125rem auto; +} + +.columns.gray p.button-container a.button { + display: inline-block; + border: 0.1875rem solid var(--text-color); + background: var(--background-color); + color: var(--text-color); +} + +.columns.gray p.button-container a.button:hover { + background: #d0dfe8; +} + +.columns.teaser > div > div h3 { + font-size: 1.3125rem; + line-height: 1.938rem; +} + +.columns.gray { + background-color: #e7e9ea; +} + @media (min-width: 768px) { .columns > div { align-items: center; @@ -44,7 +101,6 @@ .columns > div > div { flex: 1; order: unset; - } .columns > div > .columns-img-col { @@ -54,4 +110,12 @@ .columns h2 { text-align: left; } + + .columns.teaser > div > div:last-child, .columns.gray > div > div:first-child { + padding: 3.125rem; + } + + .columns.teaser > div > .columns-img-col { + order: unset; + } } diff --git a/blocks/fragment/fragment.css b/blocks/fragment/fragment.css new file mode 100644 index 00000000..46bab39e --- /dev/null +++ b/blocks/fragment/fragment.css @@ -0,0 +1,14 @@ +/* suppress nested section padding */ + +.fragment-wrapper > .section { + padding-left: 0; + padding-right: 0; +} + +.fragment-wrapper > .section:first-of-type { + padding-top: 0; +} + +.fragment-wrapper > .section:last-of-type { + padding-bottom: 0; +} diff --git a/blocks/fragment/fragment.js b/blocks/fragment/fragment.js new file mode 100644 index 00000000..b03b8f0d --- /dev/null +++ b/blocks/fragment/fragment.js @@ -0,0 +1,55 @@ +/* + * 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/lib-franklin.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 && 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-wrapper').replaceWith(...fragmentSection.childNodes); + } + } +} diff --git a/blocks/sharing/sharing.css b/blocks/sharing/sharing.css new file mode 100644 index 00000000..4b0dd9cb --- /dev/null +++ b/blocks/sharing/sharing.css @@ -0,0 +1,3 @@ +.sharing { + min-height: 48px; +} \ No newline at end of file diff --git a/blocks/sharing/sharing.js b/blocks/sharing/sharing.js new file mode 100644 index 00000000..d6ced3b3 --- /dev/null +++ b/blocks/sharing/sharing.js @@ -0,0 +1,3 @@ +export default async function decorate(block) { + block.append(document.createRange().createContextualFragment('')); +} diff --git a/icons/arrow-down.png b/icons/arrow-down.png new file mode 100644 index 00000000..c99f1f37 Binary files /dev/null and b/icons/arrow-down.png differ diff --git a/scripts/scripts.js b/scripts/scripts.js index 6c2789c3..497abc34 100644 --- a/scripts/scripts.js +++ b/scripts/scripts.js @@ -44,13 +44,46 @@ function buildHeroBlock(main) { } } +function buildBlockPostPage(main) { + // Below h1 + const h1 = main.querySelector('h1'); + const socialMediaButtons = document.createRange().createContextualFragment(''); + + if (h1) { + const author = h1.parentElement.querySelector('h1 + p > em'); + if (author) { + const authorElem = document.createRange().createContextualFragment(` `); + author.parentElement.replaceWith(authorElem); + } + + h1.parentElement.insertBefore(socialMediaButtons.cloneNode(true), h1.nextSibling); + } + + // Below last content + const lastContentSection = main.querySelector('* > div:last-of-type'); + if (lastContentSection) { + lastContentSection.appendChild(socialMediaButtons.cloneNode(true)); + + const fragment = document.createRange().createContextualFragment('