diff --git a/blocks/article-grid/article-grid.css b/blocks/article-grid/article-grid.css new file mode 100644 index 0000000..99dd241 --- /dev/null +++ b/blocks/article-grid/article-grid.css @@ -0,0 +1,160 @@ +@import "../banner/banner.css"; + +.article-grid { + position: relative; + opacity: 0; + padding-bottom: var(--spacing-l); +} + +.article-grid .article-grid-result { + display: grid; + grid-template-columns: 1fr; + gap: var(--spacing-m); + padding: var(--spacing-l) 0; + opacity: 0; + transition: opacity 0.3s ease-in-out; + width: var(--grid-container-width); + margin: 0 auto; +} + +/* spinner styles in style */ +.article-grid .loading-container { + width: 100%; + height: 70vh; + position: relative; +} + +.article-grid.loading { + opacity: 1; +} + +.article-grid.ready .loading-container { + display: none; +} + +.article-grid.ready .article-grid-result { + opacity: 1; +} + +/* media */ +.article-grid .media { + color: var(--text-color); + padding: 0; +} + +.article-grid a.media .image { + border-radius: 8px; + background: var(--color-black); +} + +.article-grid .media .image img { + width: 100%; + aspect-ratio: 16 / 9; + object-fit: cover; + border-radius: 8px; + overflow: hidden; + transition: var(--transition-all-ease-in-out-1); +} + +.article-grid .media > .foreground { + width: 100%; +} + +.article-grid .media > .foreground .media-row { + display: flex; + flex-direction: column; + align-items: start; + width: 100%; + gap: var(--spacing-s); +} + +.article-grid .media .text .description { + display: -webkit-box; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; + overflow: hidden; + text-overflow: ellipsis; + min-height: 3.2rem; +} + +.article-grid .con-button::first-letter { + text-transform: capitalize; +} + +.article-grid a.media:hover .con-button { + background-color: var(--color-black); + border-color: var(--color-black); + color: var(--color-white); + text-decoration: none; +} + +div.article-grid .media .foreground > div.media-row { + padding: 0; +} + +.article-grid a.media:hover { + text-decoration: none; +} + +.article-grid a.media:hover .image img { + opacity: 0.6; +} + +/* banner */ +.article-grid .banner { + max-width: none; + aspect-ratio: 1 / 1; +} + +.article-grid .banner, +.article-grid .banner .banner-contents { + width: 100%; + height: 100%; + margin: 0; +} + +.article-grid .banner .content-wrapper { + box-sizing: border-box; + width: 100%; + height: 100%; +} + +.article-grid .load-more-container { + grid-column: 1 / -1; + display: flex; + justify-content: center; + align-items: center; +} + +@media (min-width: 600px) { + .article-grid .article-grid-result { + grid-template-columns: repeat(2, 1fr); + gap: var(--spacing-l) var(--spacing-m); + width: var(--grid-container-width); + margin-left: auto; + margin-right: auto; + } + + .article-grid.hide-odd-item-on-tablet .article-grid-result > a:last-of-type:nth-child(odd) { + display: none; + } + + .article-grid .media .text h2 { + display: -webkit-box; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; + overflow: hidden; + text-overflow: ellipsis; + min-height: 2.8rem; + } +} + +@media (min-width: 900px) { + .article-grid .article-grid-result { + grid-template-columns: repeat(3, 1fr); + } + + .article-grid.hide-odd-item-on-tablet .article-grid-result > a:last-of-type:nth-child(odd) { + display: flex; + } +} diff --git a/blocks/article-grid/article-grid.js b/blocks/article-grid/article-grid.js new file mode 100644 index 0000000..3cd986c --- /dev/null +++ b/blocks/article-grid/article-grid.js @@ -0,0 +1,359 @@ +import { getLibs, buildBlock, replacePlaceholderForLocalizedText } from '../../scripts/utils.js'; +import initBanner from '../banner/banner.js'; + +const miloLibs = getLibs(); +const DEFAULT_GRID_LIMIT = 9; +const DEFAULT_FEED_DATA_LIMIT = 500; +const ROOT_MARGIN = 20; + +// get data +function getMetadataTags(doc) { + const metaTagElements = doc.head.querySelectorAll('meta[property="article:tag"]'); + const tagContents = [...metaTagElements].map(metaTag => + metaTag.getAttribute('content')).filter(Boolean); + + return JSON.stringify(tagContents); +} + +async function getArticleDetails(articleLink) { + const { getMetadata } = await import(`${miloLibs}/utils/utils.js`); + + const path = new URL(articleLink).pathname; + const resp = await fetch(path); + if (!resp?.ok) { + // eslint-disable-next-line no-console + console.log(`Could not retrieve metadata for ${path}`); + return null; + } + const html = await resp.text(); + const parser = new DOMParser(); + const doc = parser.parseFromString(html, 'text/html'); + + const trimEndings = ['|Adobe', '| Adobe', '| Adobe Blog', '|Adobe Blog']; + let title = getMetadata('og:title', doc).trim(); + const ending = trimEndings.find((el) => title.endsWith(el)); + [title] = title.split(ending); + + const image = doc.querySelector('img'); + + // same data format for decorating media block + return { + title, + description: getMetadata('description', doc), + image: image.src, + imageAlt: image.alt || title, + tags: getMetadataTags(doc), + path, + }; +} + +async function fetchArticleFeedData(blogIndex) { + const { getConfig } = await import(`${miloLibs}/utils/utils.js`); + const { updateLinkWithLangRoot } = await import(`${miloLibs}/utils/helpers.js`); + const { feed, articles, limit } = blogIndex.config; + + // fetch article data based on links + if (articles) { + const fetchArticleDataPromise = articles.map((article) => getArticleDetails(article)); + const articleData = await Promise.all(fetchArticleDataPromise); + + articleData.forEach((data) => { + if (data) blogIndex.data.push(data); + }); + blogIndex.complete = true; + blogIndex.limit = DEFAULT_GRID_LIMIT; + return false; + } + + // fetch feed data + const finalLimit = limit || DEFAULT_FEED_DATA_LIMIT; + blogIndex.limit = finalLimit; + const queryParams = `?limit=${finalLimit}&offset=${blogIndex.offset}`; + const defaultPath = updateLinkWithLangRoot(`${getConfig().locale.contentRoot}/query-index.json`); + + const indexPath = feed + ? `${feed}${queryParams}` + : `${defaultPath}${queryParams}`; + + return fetch(indexPath) + .then((response) => response.json()) + .then((json) => { + const complete = (json.limit + json.offset) >= json.total; + json.data.forEach((post) => { + blogIndex.data.push(post); + blogIndex.byPath[post.path.split('.')[0]] = post; + }); + blogIndex.offsetData = json.data; + blogIndex.complete = complete; + + return blogIndex; + }); +} + +// filter data +const isInList = (list, val) => list?.map((t) => t.toLowerCase()).includes(val); + +async function filterArticleDataBasedOnConfig(blogIndex) { + const { getArticleTaxonomy } = await import(`${miloLibs}/blocks/article-feed/article-helpers.js`); + + /* filter posts by category, tag and author */ + const FILTER_NAMES = ['tags', 'topics', 'selectedProducts', 'selectedIndustries', 'author', 'category', 'exclude']; + + const filters = Object.keys(blogIndex.config).reduce((prev, key) => { + if (FILTER_NAMES.includes(key)) { + prev[key] = blogIndex.config[key].split(',').map((e) => e.toLowerCase().trim()); + } + return prev; + }, {}); + + const articleData = blogIndex.data; + const KEYWORDS = ['exclude', 'tags', 'topics']; + const SELECTED = ['selectedProducts', 'selectedIndustries']; + + const filteredArticleData = articleData.filter((article) => { + const articleTaxonomy = getArticleTaxonomy(article); + + const matchedAll = Object.keys(filters).every((key) => { + if (KEYWORDS.includes(key)) { + const matchedFilter = filters[key] + .some((val) => (isInList(articleTaxonomy?.allTopics, val))); + return key === 'exclude' ? !matchedFilter : matchedFilter; + } + + if (SELECTED.includes(key)) { + if (filters.selectedProducts && filters.selectedIndustries) { + // match product && industry + const matchProduct = filters.selectedProducts + .some((val) => (isInList(articleTaxonomy?.allTopics, val))); + const matchIndustry = filters.selectedIndustries + .some((val) => (isInList(articleTaxonomy?.allTopics, val))); + return matchProduct && matchIndustry; + } + const matchedFilter = filters[key] + .some((val) => isInList(articleTaxonomy.allTopics, val)); + return matchedFilter; + } + + const matchedFilter = filters[key].some((val) => isInList([article[key]], val)); + return matchedFilter; + }); + + return (matchedAll); + }); + + blogIndex.data = filteredArticleData; +} + +function decorateLoadingContainer() { + const container = document.createElement('div'); + container.classList.add('loading-container'); + const spinner = document.createElement('div'); + spinner.classList.add('spinner'); + + container.append(spinner); + return container; +} + +async function decorateMediaBlock(articleItem) { + const { createOptimizedPicture, getArticleTaxonomy } = await import(`${miloLibs}/blocks/article-feed/article-helpers.js`); + + const { title, description, image, imageAlt, path } = articleItem; + const picture = createOptimizedPicture(image, imageAlt, [{ width: '750' }]); + const pictureTag = picture.outerHTML; + const articleTax = getArticleTaxonomy(articleItem); + + const readMore = await replacePlaceholderForLocalizedText('read-more'); + + const mediaBlock = buildBlock('media', [ + [ + pictureTag, + { + elems: [ + `
${articleTax.category}
`, + `${description}
`, + ``, + ], + }, + ], + ]); + + mediaBlock.classList.add('small'); + window.initMediaBlock(mediaBlock); + + return mediaBlock; +} + +function getOrCreateResultContainer(block) { + let resultContainer = block.querySelector('.article-grid-result'); + if (!resultContainer) { + resultContainer = document.createElement('div'); + resultContainer.classList.add('article-grid-result'); + } + return resultContainer; +} + +function initLoadingState(block, resultContainer) { + block.innerHTML = ''; + const loadingContainer = decorateLoadingContainer(); + block.prepend(loadingContainer); + block.append(resultContainer); + block.classList.add('loading'); +} + +function calculateDisplayLimit(offset, totalArticles, totalBanners) { + const limit = offset + DEFAULT_GRID_LIMIT; + return limit > totalArticles ? (totalArticles + totalBanners) : limit; +} + +async function loadBannerIfAvailable(bannerPos, bannerData, index, resultContainer) { + const existingBannerIndex = bannerPos.indexOf(`${index + 1}`); + + if (existingBannerIndex !== -1 && bannerData[existingBannerIndex]) { + const bannerBlock = buildBlock('banner', [ + [``], + ]); + await initBanner(bannerBlock); + bannerBlock.classList.add('article-grid-item'); + resultContainer.append(bannerBlock); + + bannerData[existingBannerIndex] = ''; + return true; + } + return false; +} + +async function loadArticleItem(articleItem, resultContainer, createTag) { + let mediaBlock = await decorateMediaBlock(articleItem); + + const linkEl = mediaBlock.querySelector('a'); + if (linkEl.href) { + const mediaBlockContent = mediaBlock.innerHTML; + mediaBlock = createTag( + 'a', + { + class: mediaBlock.classList, + href: linkEl.href, + target: linkEl.target, + }, + mediaBlockContent, + ); + } + + mediaBlock.classList.add('article-grid-item'); + resultContainer.append(mediaBlock); +} + +async function loadAndExposeMediaBlock() { + try { + const module = await import(`${miloLibs}/blocks/media/media.js`); + window.initMediaBlock = module.default; + + const { loadStyle } = await import(`${miloLibs}/utils/utils.js`); + loadStyle(`${miloLibs}/blocks/media/media.css`); + } catch (error) { + console.error('Error loading the module:', error); + } +} + +async function decorateArticleGrid(block, blogIndex) { + const { createTag } = await import(`${miloLibs}/utils/utils.js`); + const articleData = blogIndex.data; + const { offset, config } = blogIndex; + const { banner, 'banner-position': bannerPosConfig } = config; + + const bannerData = Array.isArray(banner) ? banner : [banner]; + const bannerPos = bannerPosConfig ? bannerPosConfig.split(',').map((pos) => pos.trim()) : []; + const resultContainer = getOrCreateResultContainer(block); + + if (offset === 0) { + initLoadingState(block, resultContainer); + } + + let articleIndex = offset; + const displayLimit = calculateDisplayLimit(offset, articleData.length, bannerPos.length); + + // Load articles and banners + for (let i = offset; i < displayLimit; i += 1) { + const bannerLoaded = await loadBannerIfAvailable(bannerPos, bannerData, i, resultContainer); + + if (!bannerLoaded) { + await loadArticleItem(articleData[articleIndex], resultContainer, createTag); + articleIndex += 1; + } + } + + blogIndex.offset = articleIndex; + + // If more articles are available, show load more button + if (articleData.length > blogIndex.offset) { + await setupLoadMoreButton(block, blogIndex, resultContainer); + } +} + +async function decorateLoadMoreButton() { + const loadMore = document.createElement('a'); + loadMore.className = 'load-more con-button outline'; + loadMore.href = '#'; + loadMore.textContent = await replacePlaceholderForLocalizedText('load-more'); + + return loadMore; +} + +async function setupLoadMoreButton(block, blogIndex, resultContainer) { + const loadMoreButton = await decorateLoadMoreButton(); + const loadMoreContainer = document.createElement('p'); + loadMoreContainer.classList.add('load-more-container'); + loadMoreContainer.append(loadMoreButton); + resultContainer.append(loadMoreContainer); + + loadMoreButton.addEventListener('click', async (e) => { + e.preventDefault(); + loadMoreContainer.remove(); + await decorateArticleGrid(block, blogIndex); + }); +} + +// block for loading article + banner +// column key: articles -> fetch data based on article links +// column key: feed -> fetch data based on feed single link (has load more button) +export default async function init(block) { + const { readBlockConfig } = await import(`${miloLibs}/blocks/article-feed/article-feed.js`); + + const blogIndex = { + data: [], + byPath: {}, + offset: 0, // number of total items (article + banner) + complete: false, + config: {}, + offsetData: [], + }; + blogIndex.config = readBlockConfig(block); + + const initArticleGrid = async () => { + await loadAndExposeMediaBlock(); // for reusing media block init + + await fetchArticleFeedData(blogIndex); + await filterArticleDataBasedOnConfig(blogIndex); + + await decorateArticleGrid(block, blogIndex); + + block.classList.add('ready'); + }; + + async function handleIntersection(entries, observer) { + entries.forEach(async (entry) => { + if (entry.isIntersecting) { + await initArticleGrid(); + observer.unobserve(entry.target); + } + }); + } + + // reduce inital load time + const observer = new IntersectionObserver(handleIntersection, { + root: null, // Use the viewport as the root + rootMargin: `${ROOT_MARGIN}px`, + }); + observer.observe(block); +} diff --git a/blocks/article-meta/adobe-logo.svg b/blocks/article-meta/adobe-logo.svg new file mode 100644 index 0000000..18eac9f --- /dev/null +++ b/blocks/article-meta/adobe-logo.svg @@ -0,0 +1,2 @@ + diff --git a/blocks/article-meta/article-meta.css b/blocks/article-meta/article-meta.css new file mode 100644 index 0000000..eef370f --- /dev/null +++ b/blocks/article-meta/article-meta.css @@ -0,0 +1,172 @@ +.article-byline { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: var(--spacing-xs); + margin: var(--spacing-s) auto; + padding: 0; +} + +.article-byline .article-author-image, +.article-byline .article-author-image img { + flex: 0 0 64px; + margin: 0; + height: 64px; + width: 64px; + border-radius: 50%; + object-fit: cover; +} + +.article-byline .article-byline-info { + flex: 1 0 calc(100% - 80px); + margin: 0; + display: flex; + flex-direction: column; + justify-content: center; +} + +.article-author { + margin: 0; + padding: 0; + font-size: var(--type-detail-l-size); + line-height: var(--type-detail-l-lh); +} + +.article-author, +.article-author a { + font-weight: 600; + text-decoration: none; +} + +.article-author a:any-link { + color: var(--color-black); +} + +.article-author p { + margin: 0; +} + +.article-date { + margin: 0; + padding: 0; + font-weight: 300; + font-size: var(--type-detail-l-size); + line-height: var(--type-detail-l-lh); +} + +.article-date.article-date-invalid { + position: relative; + padding: 0 6px; + border: 10px solid red; + background: yellow; +} + +/* sharing */ +.article-meta .share { + padding: 0; + margin: 0; + width: 100%; + display: flex; +} + +.article-meta .share .icon-container { + display: flex; + justify-content: flex-start; + gap: var(--spacing-m); + width: 100%; + padding: var(--spacing-xs) 0; + margin-bottom: 0; + border-top: 1px solid var(--color-gray-200); + border-bottom: 1px solid var(--color-gray-200); +} + +.article-meta .share .icon-container a { + cursor: pointer; +} + +.article-meta .share .icon-container .icon { + color: var(--color-black); +} + +.article-meta .share .icon-container svg.icon { + width: 24px; + height: 24px; +} + +.article-meta .share p.icon-container > a, +.article-meta .share p.icon-container > a:not(:first-child), +.article-meta .share p.icon-container > button { + margin: 0; +} + +.article-meta .share .icon-container a:hover .icon.icon-twitter, +.article-meta .share .icon-container a:focus .icon.icon-twitter { + color: #1DA1F2; +} + +.article-meta .share .icon-container a:hover .icon.icon-linkedin, +.article-meta .share .icon-container a:focus .icon.icon-linkedin { + color: #0077b5; +} + +.article-meta .share .icon-container a:hover .icon.icon-facebook, +.article-meta .share .icon-container a:focus .icon.icon-facebook { + color: #3b5998; +} + +.article-meta .share .icon-container .copy-to-clipboard:hover svg, +.article-meta .share .icon-container .copy-to-clipboard:focus svg { + color: var(--color-info-accent-hover); +} + +@media (min-width: 600px) { + .article-byline { + margin: var(--spacing-m) auto; + padding-bottom: var(--spacing-xs); + border-bottom: 1px solid var(--color-gray-200); + } + + .article-byline .article-byline-info { + flex: 1 1 auto; + } + + .article-meta .share { + width: fit-content; + } + + .article-meta .share .icon-container { + flex: 0 1 auto; + margin-top: 0; + gap: var(--spacing-s); + border: 0; + } +} + +@media (min-width: 700px) { + .article-header .article-byline { + max-width: var(--grid-container-width); + margin-left: auto; + margin-right: auto; + padding-left: 0; + padding-right: 0; + } +} + +@media (min-width: 900px) { + .article-byline { + margin-top: 0; + padding-bottom: 0; + border: 0; + } + + .article-meta .share { + width: 100%; + } + + .article-meta .share .icon-container { + width: 100%; + justify-content: flex-start; + gap: var(--spacing-xs); + padding: var(--spacing-xxs) 0 0; + } +} diff --git a/blocks/article-meta/article-meta.js b/blocks/article-meta/article-meta.js new file mode 100644 index 0000000..d39f38c --- /dev/null +++ b/blocks/article-meta/article-meta.js @@ -0,0 +1,141 @@ +import { getLibs, buildBlock, replacePlaceholderForLocalizedText } from '../../scripts/utils.js'; + +async function validateAuthorUrl(url) { + if (!url) return null; + + const resp = await fetch(`${url.toLowerCase()}.plain.html`); + if (!resp?.ok) { + console.log(`Could not retrieve metadata for ${url}`); + return null; + } + + const html = await resp.text(); + const parser = new DOMParser(); + const doc = parser.parseFromString(html, 'text/html'); + + return doc; +} + +async function validateDate(date) { + const miloLibs = getLibs(); + const { getConfig } = await import(`${miloLibs}/utils/utils.js`); + const { env } = getConfig(); + + if (env?.name === 'prod') return; + if (date && !/^[0-1]\d{1}-[0-3]\d{1}-[2]\d{3}$/.test(date.textContent.trim())) { + // match publication date to MM-DD-YYYY format + date.classList.add('article-date-invalid'); + date.setAttribute('title', await replacePlaceholderForLocalizedText('invalid-date')); + } +} + +async function buildAuthorInfo(authorEl, bylineContainer) { + const miloLibs = getLibs(); + const { createTag, getConfig } = await import(`${miloLibs}/utils/utils.js`); + + const { href, textContent } = authorEl; + + const config = getConfig(); + const base = config.miloLibs || config.codeRoot; + const authorImg = createTag('div', { class: 'article-author-image' }); + authorImg.style.backgroundImage = `url(${base}/blocks/article-header/adobe-logo.svg)`; + bylineContainer.prepend(authorImg); + + const doc = await validateAuthorUrl(href); + if (!doc) { + const p = createTag('p', null, textContent); + authorEl.replaceWith(p); + return; + } + + const img = doc.querySelector('img'); + if (img) { + img.setAttribute('alt', authorEl.textContent); + authorImg.append(img); + if (!img.complete) { + img.addEventListener('load', () => { + authorImg.style.backgroundImage = 'none'; + }); + img.addEventListener('error', () => { + img.remove(); + }); + } else { + authorImg.style.backgroundImage = 'none'; + } + } +} + +export function initCopyLinkButtonFunction(block) { + const copyButton = block.querySelector('.copy-to-clipboard') + if (!copyButton) return false; + + copyButton.addEventListener('click', (e) => { + e.preventDefault(); + + navigator.clipboard.writeText(window.location.href).then(() => { + copyButton.classList.add('copy-to-clipboard-copied'); + setTimeout(() => document.activeElement.blur(), 500); + setTimeout( + () => copyButton.classList.remove('copy-to-clipboard-copied'), + 2000, + ); + }); + }); +} + +export default async function init(blockEl) { + const childrenEls = Array.from(blockEl.children); + if (childrenEls.length < 1) { + console.warn('Block does not have enough children'); + } + + // build author info + const bylineContainer = childrenEls[0]; + bylineContainer.classList.add('article-byline'); + bylineContainer.firstElementChild.classList.add('article-byline-info'); + + const authorContainer = bylineContainer.firstElementChild.firstElementChild; + const authorEl = authorContainer.querySelector('a'); + authorContainer.classList.add('article-author'); + + await buildAuthorInfo(authorEl, bylineContainer); + + const date = bylineContainer.querySelector('.article-byline-info > p:last-child'); + date.classList.add('article-date'); + await validateDate(date); + + // build link sharing + const miloLibs = getLibs(); + const { getMetadata, loadStyle } = await import(`${miloLibs}/utils/utils.js`); + const miloShareModule = await import(`${miloLibs}/blocks/share/share.js`); + const initMiloShareBlock = miloShareModule.default; + loadStyle(`${miloLibs}/blocks/share/share.css`); + + const url = encodeURIComponent(window.location.href); + const title = encodeURIComponent(document.querySelector('h1').textContent); + const description = encodeURIComponent(getMetadata('description')); + + const platformLinks = { + twitter: `https://www.twitter.com/share?&url=${url}&text=${title}`, + linkedin: `https://www.linkedin.com/shareArticle?mini=true&url=${url}&title=${title}&summary=${description || ''}`, + facebook: `https://www.facebook.com/sharer/sharer.php?u=${url}` + } + + const miloShareBlock = buildBlock('share', [ + [ + { + elems: [ + ` Twitter `, + ` Linkedin `, + ` Facebook `, + ] + } + ], + ]); + await initMiloShareBlock(miloShareBlock); + + const trackingHeader = miloShareBlock.querySelector('.tracking-header'); + if (trackingHeader) trackingHeader.innerHTML = ""; + + bylineContainer.append(miloShareBlock); +} diff --git a/blocks/banner/banner.css b/blocks/banner/banner.css index 23995c6..55510ad 100644 --- a/blocks/banner/banner.css +++ b/blocks/banner/banner.css @@ -1,8 +1,16 @@ + +/* v3 code */ main .banner { opacity: 0; min-height: 152px; - max-width: var(--grid-container-width); - margin: 0 auto; + width: 100%; + max-width: none; + margin: 32px auto; + border-radius: 8px; + overflow: hidden; + + /* fallback color without image */ + background: var(--color-red-gradient); } main .banner.is-loaded { @@ -10,23 +18,51 @@ main .banner.is-loaded { min-height: 0; } +main .banner-contents { + display: flex; + flex-direction: column; + width: 100%; + height: 100%; + min-height: 300px; + overflow: hidden; +} + main .banner .content-wrapper { - max-width: 600px; + box-sizing: border-box; + position: relative; display: flex; - padding: 16px 0; - border-top: 2px solid var(--color-gray-300); - border-bottom: 2px solid var(--color-gray-300); - align-items: center; - margin: 64px auto; + flex-direction: column; + justify-content: center; + height: 100%; + padding: 24px 20px; + flex-grow: 1; } -main .banner-text { - margin-left: 40px; +main .banner .content-wrapper::before { + content: ''; + position: absolute; + inset: 0; + width: 100%; + height: 100%; + background: var(--color-black); + opacity: 0.5; + z-index: 1; } main .banner img { - width: 100px; - max-width: unset; + position: absolute; + inset: 0; + width: 100%; + height: 100%; + object-fit: cover; + transition: var(--transition-all-ease-in-out-1); +} + + +main .banner-text { + color: var(--color-white); + width: 100%; + z-index: 2; } main .banner .banner-text h3, @@ -36,14 +72,45 @@ main .banner .banner-text p { } main .banner .banner-text h3 { - font-size: var(--body-font-size-xs); - line-height: var(--body-line-height); - letter-spacing: .1em; text-transform: uppercase; - margin: 16px 0; + margin-bottom: 24px; } -main .banner .banner-text p { - font-size: var(--body-font-size-s); - margin: 16px 0; +main .banner .banner-text .banner-description { + font-weight: 700; + margin-bottom: 24px; +} + +main .banner.bg-gradient-red { + background: var(--color-red-gradient); +} + +main .banner.bg-gradient-red img, +main .banner.bg-gradient-red .content-wrapper::before { + display: none; +} + +main a.banner:hover { + text-decoration: none; +} + +main a.banner:hover img { + transform: scale(1.07); +} + +main a.banner:hover .con-button { + background-color: var(--color-white); + color: var(--color-black); + text-decoration: none; +} + +@media (min-width: 900px) { + main .banner { + max-width: none; + margin: var(--spacing-s) 0; + } + + main .banner .content-wrapper { + padding: 24px 20px 48px; + } } diff --git a/blocks/banner/banner.js b/blocks/banner/banner.js index f0af796..097a4aa 100644 --- a/blocks/banner/banner.js +++ b/blocks/banner/banner.js @@ -1,11 +1,9 @@ -import { - createOptimizedPicture, - normalizeHeadings, -} from '../../scripts/utils.js'; +import { createOptimizedPicture, normalizeHeadings, changeHTMLTag } from '../../scripts/utils.js'; export default async function init(block) { const bannerContents = document.createElement('div'); bannerContents.classList.add('banner-contents'); + block.querySelectorAll('a').forEach(async (a) => { if (!a?.href) return; // content wrapper @@ -27,7 +25,7 @@ export default async function init(block) { const bannerImage = document.createElement('div'); const bannerText = document.createElement('div'); bannerImage.classList.add('banner-image'); - bannerText.classList.add('banner-text'); + bannerText.classList.add('banner-text', 'dark'); // banner image content const img = responseEl.querySelector('img'); @@ -40,8 +38,18 @@ export default async function init(block) { // banner text content normalizeHeadings(responseEl, ['h3']); + const heading = responseEl.querySelector('h3'); + heading.classList.add('detail-m'); + const descriptions = responseEl.querySelectorAll('p'); + descriptions.forEach((desc) => { + if (!desc.querySelector('a') && desc.textContent.trim().length > 0) { + desc.classList.add('heading-m', 'banner-description'); + } + }); + const link = responseEl.querySelector('a'); - link.classList.add('cta-link'); + if (link) link.classList.add('con-button'); + bannerText.append(responseEl); // appending DOM objects @@ -49,6 +57,20 @@ export default async function init(block) { bannerContents.append(bannerContent); block.innerHTML = ''; block.append(bannerContents); + + if (link?.href) { + // switch whole banner to + const linkedBannerProperties = { + class: block.classList, + href: link.href, + target: link.target, + }; + const linkedBanner = changeHTMLTag(block, 'a', linkedBannerProperties); + + // switch inner link back to + const linkedBannerLink = linkedBanner.querySelector('a'); + changeHTMLTag(linkedBannerLink, 'span', { class: linkedBannerLink.classList }); + } } else { block.remove(); } diff --git a/img/background/circle-gradient-bottom-left.jpg b/img/background/circle-gradient-bottom-left.jpg new file mode 100644 index 0000000..97759d4 Binary files /dev/null and b/img/background/circle-gradient-bottom-left.jpg differ diff --git a/img/background/circle-gradient-bottom-left.png b/img/background/circle-gradient-bottom-left.png new file mode 100644 index 0000000..598c01c Binary files /dev/null and b/img/background/circle-gradient-bottom-left.png differ diff --git a/img/background/circle-gradient-top-right.jpg b/img/background/circle-gradient-top-right.jpg new file mode 100644 index 0000000..9dee3a2 Binary files /dev/null and b/img/background/circle-gradient-top-right.jpg differ diff --git a/img/background/circle-gradient-top-right.png b/img/background/circle-gradient-top-right.png new file mode 100644 index 0000000..1ef69a4 Binary files /dev/null and b/img/background/circle-gradient-top-right.png differ diff --git a/img/background/marquee-circle-gradient-pink.jpg b/img/background/marquee-circle-gradient-pink.jpg new file mode 100644 index 0000000..095b2e2 Binary files /dev/null and b/img/background/marquee-circle-gradient-pink.jpg differ diff --git a/package.json b/package.json index ecf5f96..cb94f29 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,8 @@ "test:file:watch": "wtr --node-resolve --port=2000 --coverage --watch", "lint": "npm run lint:js && npm run lint:css", "lint:js": "eslint .", - "lint:css": "stylelint 'blocks/**/*.css' 'styles/*.css'" + "lint:css": "stylelint 'blocks/**/*.css' 'styles/*.css'", + "lint:fix": "eslint --fix --ext .js,.jsx . && npm run lint -- --fix" }, "repository": { "type": "git", diff --git a/scripts/articles.js b/scripts/articles.js new file mode 100644 index 0000000..e31f240 --- /dev/null +++ b/scripts/articles.js @@ -0,0 +1,115 @@ +import { getLibs } from '../../scripts/utils.js'; +import { initCopyLinkButtonFunction } from '../blocks/article-meta/article-meta.js'; + +function cloneBlockToSidebar(sidebar, blockClass) { + const targetBlock = document.querySelector(`.${blockClass}`); + if (!targetBlock) return false; + + const clonedBlock = targetBlock.cloneNode(true); + targetBlock.classList.add('hide-if-sidebar-visible'); + + sidebar.append(clonedBlock); + + return clonedBlock; +} + +async function initSidebar() { + const sidebar = document.createElement('div'); + sidebar.classList.add('blog-wrapper-sidebar'); + + const sidebarArticleMeta = cloneBlockToSidebar(sidebar, 'article-meta'); + initCopyLinkButtonFunction(sidebarArticleMeta); + cloneBlockToSidebar(sidebar, 'banner'); + cloneBlockToSidebar(sidebar, 'tags'); + + return sidebar; +} + +function moveRecommendedArticleAsFinalSection(main) { + const recBlock = main.querySelector('.recommended-articles'); + if (!recBlock) return; + + const section = document.createElement('div'); + section.classList.add('section'); + section.append(recBlock); + main.append(section); +} + +async function setUpSidebarLayoutForArticlePage() { + const miloLibs = getLibs(); + const { getMetadata } = await import(`${miloLibs}/utils/utils.js`); + + const main = document.querySelector('main'); + const childContent = document.querySelector('.content'); + const isArticlePage = getMetadata('content-type') === 'article'; + + if (!isArticlePage || !childContent) return; + + const sections = document.querySelectorAll('.section'); + if (!sections) return; + + // select first section to hold all the content + const mainContent = childContent.closest('.section'); + mainContent.classList.add('blog-wrapper-content'); + + sections.forEach((section, index) => { + const isArticleBanner = section.querySelector('.marquee') && index === 0; + const isMainContent = mainContent === section; + if (!isArticleBanner && !isMainContent) { + mainContent.appendChild(section); + } + }); + + // Put blog wrapper (wraps sidebar + content) first + const blogWrapper = document.createElement('div'); + blogWrapper.classList.add('blog-wrapper'); + main.insertBefore(blogWrapper, mainContent); + + // Put sidebar + content into blog wrapper + const sidebar = await initSidebar(); + blogWrapper.append(sidebar, mainContent); + + // Move recommended-articles as last separate section if any + moveRecommendedArticleAsFinalSection(main); +} + +async function buildProgressBar() { + const miloLibs = getLibs(); + const { getMetadata } = await import(`${miloLibs}/utils/utils.js`); + + if (!getMetadata('content-type') === 'article' || document.querySelector('.article-header')) return; + + const { createTag } = await import(`${miloLibs}/utils/utils.js`); + const progressBarContainer = createTag('div', { class: 'progress-bar-container' }); + const progressBar = createTag('div', { class: 'progress-bar' }); + progressBarContainer.appendChild(progressBar); + + const body = document.querySelector('body'); + body.append(progressBarContainer); + + function updateProgressBar() { + const content = document.querySelector('main'); + const contentHeight = content.scrollHeight - window.innerHeight; + const scrollPosition = window.scrollY; + const initialWidth = 5; + + let progress = initialWidth; + + if (scrollPosition < contentHeight) { + progress += (scrollPosition / contentHeight) * (100 - initialWidth); + } else { + progress = 100; + } + + progressBar.style.width = `${progress}%`; + } + + window.onscroll = function () { + updateProgressBar(); + }; +} + +export async function decorateArticlePageUI() { + await setUpSidebarLayoutForArticlePage(); + await buildProgressBar(); +} diff --git a/scripts/scripts.js b/scripts/scripts.js index 8d8c3e9..a18c969 100644 --- a/scripts/scripts.js +++ b/scripts/scripts.js @@ -11,6 +11,7 @@ */ import { decorateContent, setLibs, buildAutoBlocks } from './utils.js'; +import { decorateArticlePageUI } from './articles.js' // Add project-wide styles here. const STYLES = ['/styles/styles.css', '/styles/articles.css']; @@ -186,7 +187,7 @@ function overrideMiloBlocks() { function decorateTopicPage() { if (window.location.href.includes('/topics/')) { const sections = [...document.querySelectorAll('main > div')]; - if (sections.length = 1) { + if (sections.length === 1) { const articleFeed = document.querySelector('main div .article-feed'); if (!articleFeed) return; const newSection = document.createElement('div'); @@ -226,5 +227,6 @@ const { loadArea, setConfig, getMetadata } = await import(`${miloLibs}/utils/uti await buildAutoBlocks(); overrideMiloBlocks(); await loadArea(); + await decorateArticlePageUI(); initSidekick(); }()); diff --git a/scripts/utils.js b/scripts/utils.js index 734c845..b1c1f7a 100644 --- a/scripts/utils.js +++ b/scripts/utils.js @@ -151,6 +151,7 @@ export async function decorateContent() { const { createTag, loadStyle } = await import(`${miloLibs}/utils/utils.js`); loadStyle(`${miloLibs}/blocks/figure/figure.css`); + // eslint-disable-next-line consistent-return if (window.location.pathname.includes('/topics/')) return topicHeader(createTag); if (window.location.pathname.includes('/authors/')) return; @@ -182,7 +183,7 @@ export async function decorateContent() { * @param {string} blockName name of the block * @param {any} content two dimensional array or string or object of content */ -function buildBlock(blockName, content) { +export function buildBlock(blockName, content) { const table = Array.isArray(content) ? content : [[content]]; const blockEl = document.createElement('div'); // build image block nested div structure @@ -224,7 +225,7 @@ function buildAuthorHeader(mainEl) { heading.replaceWith(title); } - const authorHeading = title ? title : heading; + const authorHeading = title || heading; const authorHeader = buildBlock('author-header', [ [{ elems: [ @@ -240,46 +241,92 @@ function buildAuthorHeader(mainEl) { div.prepend(authorHeader); } -async function buildArticleHeader(el) { +/** + * * @param {HTMLElement} element + * * @param {string} targetTag, like 'ul' or 'div' + * result: return the new element with inner content of the element, desired tag and css class + */ +export function changeHTMLTag(el, targetTag, properties = {}) { + const newEl = document.createElement(targetTag); + Object.keys(properties).forEach((key) => { + newEl.setAttribute(key, properties[key]); + }); + + newEl.innerHTML = el.innerHTML; + el.replaceWith(newEl); + + return newEl; +} + +/** + * * @param {string} key key of the localized text + * return localized text based on inputted key + */ +export async function replacePlaceholderForLocalizedText(key) { const miloLibs = getLibs(); - const { getMetadata, getConfig } = await import(`${miloLibs}/utils/utils.js`); + const [{ replaceKey }, { getConfig }] = await Promise.all([ + await import(`${miloLibs}/features/placeholders.js`), + await import(`${miloLibs}/utils/utils.js`) + ]) + + const result = await replaceKey(key, getConfig()); + + return result; +} + + +async function buildArticleHeroBanner(el) { + const miloLibs = getLibs(); + const { getMetadata } = await import(`${miloLibs}/utils/utils.js`); const { loadTaxonomy, getLinkForTopic, getTaxonomyModule } = await import(`${miloLibs}/blocks/article-feed/article-helpers.js`); if (!getTaxonomyModule()) { await loadTaxonomy(); } + const div = document.createElement('div'); - // div.setAttribute('class', 'section'); + + const tag = getMetadata('article:tag'); + const category = tag || 'News'; + const categoryTag = getLinkForTopic(category); + const h1 = el.querySelector('h1'); + const description = getMetadata('description'); const picture = el.querySelector('a[href*=".mp4"], picture'); const caption = getImageCaption(picture); const figure = document.createElement('div'); figure.append(picture, caption); - const tag = getMetadata('article:tag'); - const category = tag || 'News'; - const author = getMetadata('author') || 'Adobe Communications Team'; - const { codeRoot } = getConfig(); - const authorURL = getMetadata('author-url') || (author ? `${codeRoot}/authors/${author.replace(/[^0-9a-z]/gi, '-').toLowerCase()}` : null); - const publicationDate = getMetadata('publication-date'); - const categoryTag = getLinkForTopic(category); + const tagEl = document.createElement('p'); + tagEl.textContent = category; - const articleHeaderBlockEl = buildBlock('article-header', [ - [`${categoryTag}
`], - [h1], - [`${authorURL ? `${author}` : author}
-${publicationDate}
`], - [figure], + const marqueeEl = buildBlock('marquee', [ + ['transparent
'], + [ + { + elems: [ + categoryTag, + h1, + `${description}
`, + ], + }, + picture, + ], ]); - div.append(articleHeaderBlockEl); + + marqueeEl.classList.add('split', 'medium', 'light', 'circle-gradient-pink', 'article-hero-banner'); + + const categoryLink = marqueeEl.querySelector('a'); + categoryLink.classList.add('article-hero-category-link'); + + div.append(marqueeEl); el.prepend(div); } -function buildTagsBlock() { +function buildTagsBlock(mainEl) { const tagsArray = [...document.head.querySelectorAll('meta[property="article:tag"]')].map((el) => el.content) || []; const tagsBlock = buildBlock('tags', tagsArray.join(', ')); - const main = document.querySelector('main'); - const recBlock = main.querySelector('.recommended-articles'); + const recBlock = mainEl.querySelector('.recommended-articles'); if (recBlock) { // Put tags block before recommended articles block if (recBlock.parentElement.childElementCount === 1) { @@ -288,21 +335,49 @@ function buildTagsBlock() { recBlock.before(tagsBlock); } } else { - main.lastElementChild.append(tagsBlock); + mainEl.lastElementChild.append(tagsBlock); } } +async function buildArticleMeta(mainEl) { + const miloLibs = getLibs(); + const { getMetadata, getConfig } = await import(`${miloLibs}/utils/utils.js`); + + const author = getMetadata('author') || 'Adobe Communications Team'; + const { codeRoot } = getConfig(); + const authorURL = getMetadata('author-url') || (author ? `${codeRoot}/authors/${author.replace(/[^0-9a-z]/gi, '-').toLowerCase()}` : null); + + const publicationDate = getMetadata('publication-date'); + + const articleMeta = buildBlock('article-meta', [ + [`${authorURL ? `${author}` : author}
+${publicationDate}
`], + ]); + + // put article meta (author + link sharings) in front of first content div + const allContentDivs = mainEl.querySelectorAll(':scope > div'); + if (!allContentDivs) return; + + const contentBlock = [...allContentDivs].find((div) => !div.querySelector('.article-hero-banner') && div.querySelector('p')); + if (!contentBlock) return; + + contentBlock.insertBefore(articleMeta, contentBlock.firstChild); +} + export async function buildAutoBlocks() { const miloLibs = getLibs(); const { getMetadata } = await import(`${miloLibs}/utils/utils.js`); const mainEl = document.querySelector('main'); + try { if (getMetadata('content-type') === 'article' && !mainEl.querySelector('.article-header')) { - await buildArticleHeader(mainEl); - buildTagsBlock(); + await buildArticleHeroBanner(mainEl); + buildTagsBlock(mainEl); + await buildArticleMeta(mainEl); } else if (getMetadata('content-type') === 'authors') { buildAuthorHeader(mainEl); } + } catch (error) { // eslint-disable-next-line no-console console.error('Auto Blocking failed', error); diff --git a/styles/articles.css b/styles/articles.css index bf96570..f927a67 100644 --- a/styles/articles.css +++ b/styles/articles.css @@ -1,7 +1,7 @@ /* * Put project specific base styles here. * - * Note: The proect does not load this file. + * Note: The project does not load this file. * You will need to load these using scripts.js. * * @@ -12,8 +12,8 @@ main h1, main h2, main h3, main h4, main h5, main h6 { scroll-margin-top: 90px; } -main .section{ - margin-top: 64px +main .section:first-of-type { + margin-top: 57px; } main .figure .figure img { @@ -28,7 +28,7 @@ main .article-header .article-byline { max-width: 800px; } -main video { +main .blog-wrapper video { max-height: 600px; margin: 40px auto; display: block; @@ -37,9 +37,163 @@ main video { @media (min-width: 700px) { main .section > div > :not(div):not(h1), main .section > .tags, - main .columns, main .columns.contained { + main .columns, main .columns.contained, + main .blog-wrapper-content > div { max-width: 800px; margin-left: auto; margin-right: auto; } } + +@media (min-width: 900px) { + main .section:first-of-type { + margin-top: var(--global-height-nav); + } +} + +/* Article v3 related styles */ +main .article-hero-banner .text p { + display: -webkit-box; + -webkit-line-clamp: 3; + -webkit-box-orient: vertical; + overflow: hidden; + text-overflow: ellipsis; +} + +@media (max-width: 600px) { + main .article-hero-banner .text p { + display: none; + } +} + +/* tags */ +main .tags { + max-width: none; + margin-top: var(--spacing-m); +} + +main .tags a { + font-weight: 700; + letter-spacing: normal; + padding: 12px; + margin: 0 12px 12px 0; +} + +@media (min-width: 900px) { + main .tags { + margin-top: var(--spacing-s); + } +} + +/* Blog Styles (sidebar + content) */ +main .article-hero-category-link { + display: inline-block; + color: var(--color-white); +} + +main .light .article-hero-category-link { + color: var(--text-color); +} + +.blog-wrapper { + display: flex; + flex-direction: column-reverse; + gap: var(--spacing-xs); + margin: 0 auto; + max-width: var(--grid-container-width); + padding-bottom: var(--spacing-m); +} + +.blog-wrapper-sidebar { + display: none; +} + +.blog-wrapper-sidebar .tags { + max-width: none; +} + +.blog-wrapper-sidebar .tags a { + padding: 12px; + margin: 0 8px 8px 0; +} + +@media (min-width: 900px) { + .blog-wrapper { + flex-direction: row; + gap: var(--spacing-s); + } + + .blog-wrapper-sidebar { + display: block; + position: sticky; + top: var(--spacing-m); + min-width: 240px; + width: 30%; + height: fit-content; + padding: calc(24px + var(--spacing-m)) 0 24px; + max-height: calc(100vh - var(--global-height-nav)); + overflow-y: auto; + padding-right: var(--spacing-xxs); + } + + .blog-wrapper-content { + max-width: 70%; + padding: 24px 0; + } + + .blog-wrapper-content p:first-of-type { + margin-top: var(--spacing-m); + } + + main .embed-instagram, main .embed-instagram iframe.instagram-media { + max-width: max-content; + } + + .hide-if-sidebar-visible { + display: none; + } +} + +@media (min-width: 1200px) { + .blog-wrapper { + gap: var(--spacing-m); + } + + .blog-wrapper-sidebar { + width: 28%; + min-width: 280px; + } +} + +/* progress bar */ +.progress-bar-container { + z-index: 10000; + position: fixed; + top: 57px; + left: 0; + right: 0; + transform-origin: 0%; + width: 100%; + height: 4px; +} + +.progress-bar { + background-color: var(--feds-color-adobeBrand); + height: 100%; + width: 5%; +} + +@media (min-width: 900px) { + .progress-bar-container { + top: var(--global-height-nav); + } +} + +/* quote */ +.blog-wrapper-content .text-block .foreground { + max-width: none; +} + +.blog-wrapper-content .inset.text-block .foreground > div { + padding-left: var(--spacing-s); +} diff --git a/styles/styles.css b/styles/styles.css index db37ce2..a33862e 100644 --- a/styles/styles.css +++ b/styles/styles.css @@ -9,6 +9,10 @@ :root { --color-quote: #367ea8; --page-top-spacing: 128px; + --color-red-gradient: radial-gradient(110.12% 95.52% at 25.93% 25.98%, #EB1000 -5%, #E9740A 60.34%, #FF61F6 110%); + + /* animation */ + --transition-all-ease-in-out-1: all 0.3s ease-in-out; } body header .gnav-wrapper a.gnav-brand { @@ -89,6 +93,10 @@ main .inset.text-block .foreground::before { margin-bottom: 40px; } +.con-button { + transition: var(--transition-all-ease-in-out-1); +} + /* * Modal */ @@ -253,3 +261,399 @@ main + footer .feds-regionPicker-wrapper > .fragment p strong > a::after { padding-right: inherit; } } + +/* + * Recommended Articles +*/ +main .recommended-articles-content-wrapper { + background: var(--color-white); + padding-bottom: var(--spacing-m); +} + +main .recommended-articles > h3 { + padding-top: 0; + margin-top: var(--spacing-m); + margin-bottom: var(--spacing-s); +} + +main .recommended-articles .article-card-body h3 { + margin-bottom: var(--spacing-xxs); +} + +.article-card .article-card-image { + height: 215px; +} + +main .article-card .article-card-category { + margin-top: 0; +} + +main .article-card .article-card-body { + padding: 20px 20px 24px; +} + +main .article-card .article-card-body h3 { + font-size: var(--type-heading-xs-size); + line-height: var(--type-heading-xs-lh); + margin: 0 0 8px; +} + +main .article-card .article-card-body .article-card-description { + font-size: var(--type-body-xs-size); + line-height: var(--type-body-xs-lh); + margin: 8px 0; +} + +main .article-card-body p.article-card-category, +main .article-card-body p.article-card-category a, +main .article-card-body p.article-card-date { + font-size: var(--type-body-xs-size); + line-height: var(--type-body-xs-lh); + letter-spacing: normal; + font-weight: 400; + color: var(--text-color); +} + +@media (min-width: 600px) { + main .recommended-articles h3 { + margin-bottom: var(--spacing-m); + } + + main .article-card-body h3 { + margin-bottom: var(--spacing-xxs); + -webkit-line-clamp: 2; + min-height: 2.2rem; + } +} + +/* aside */ +main .aside .foreground.container .text .heading-xl { + margin: 0; +} + +main .aside .foreground.container .text .heading-xl:last-of-type { + margin-bottom: var(--spacing-xs); +} + +@media (min-width: 1200px) { + main .aside .foreground.container .text { + padding-right: var(--spacing-l); + } +} + + + + +/* + * Editorial Card +*/ +main .editorial-card.click a { + color: var(--text-color); +} + +main .editorial-card .foreground { + padding-bottom: 20px; +} + +main .section:has(.editorial-card) { + padding-bottom: var(--spacing-xxl); +} + +/* + * Standardize card hovering effect based on design feedback +*/ +main .editorial-card .media-area > div { + background: var(--color-black); +} + +main .editorial-card .media-area img { + transition: var(--transition-all-ease-in-out-1); +} + +main .editorial-card:hover .media-area img { + opacity: 0.6; +} + +/* + * spinner for loading animation +*/ + +.spinner { + content: ''; + position: absolute; + top: 40%; + left: calc(50% - 1.6rem); + border: 2px dotted var(--color-gray-100); + border-bottom: 2px dotted var(--color-gray-200); + border-left: 2px dotted var(--color-gray-400); + border-top: 2px dotted var(--color-gray-800); + border-radius: 50%; + width: 1.6rem; + height: 1.6rem; + -webkit-animation: spin 2s linear infinite; + animation: spin 2s linear infinite; +} + +@-webkit-keyframes spin { + 0% { + -webkit-transform: rotate(0deg); + } + + 100% { + -webkit-transform: rotate(360deg); + } +} + +@keyframes spin { + 0% { + transform: rotate(0deg); + } + + 100% { + transform: rotate(360deg); + } +} + +/* + * Masonry Layout +*/ +main .masonry-layout .brick { + overflow: hidden; +} + +main .masonry-layout .brick .brick-media { + background: var(--color-black); + overflow: hidden; +} + +main .masonry-layout .brick .brick-media img { + transition: var(--transition-all-ease-in-out-1); +} + +main .masonry-layout .brick a:hover { + cursor: pointer; +} + +main .masonry-layout .brick a:hover .brick-media img { + opacity: 0.7; +} + +main .masonry-layout.hero-masonry { + padding-bottom: var(--spacing-xl); +} + +main .masonry-layout.hero-masonry .brick .foreground { + flex-direction: column-reverse; + justify-content: flex-end; + gap: var(--spacing-xs); +} + +main .masonry-layout.hero-masonry .foreground .brick-media { + display: block; +} + +main .masonry-layout.hero-masonry .brick .brick-media { + aspect-ratio: 16 / 9; + border-radius: 8px; + overflow: hidden; +} + +main .masonry-layout.hero-masonry .brick .brick-media img { + width: 100%; + height: 100%; + object-fit: cover; +} + +main .masonry-layout.hero-masonry .brick .brick-media video { + margin-top: 0; +} + +main .masonry-layout.hero-masonry .brick .foreground p.body-m { + display: -webkit-box; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; + overflow: hidden; + text-overflow: ellipsis; +} + +main .masonry-layout.hero-masonry .brick .foreground p.action-area { + padding-top: var(--spacing-s); + gap: var(--spacing-xs); + margin-top: auto; +} + +@media (max-width: 600px) { + main .masonry-layout.hero-masonry .brick .foreground { + padding: var(--spacing-s); + } + + main .masonry-layout.hero-masonry > .text { + text-align: left; + } +} + +@media (min-width: 600px) { + main .masonry-layout.hero-masonry .brick-media { + border-radius: 16px; + } +} + +@media screen and (min-width: 600px) and (max-width: 1200px) { + main .masonry-layout.hero-masonry .grid-full-width { + grid-column: 1 / -1; + } +} + +@media (min-width: 800px) { + main .masonry-layout.hero-masonry .brick.stack .foreground .brick-text { + display: flex; + flex-direction: column; + flex-grow: 1; + } +} + +@media (min-width: 900px) { + main .masonry-layout.hero-masonry .text-block .foreground { + max-width: 700px; + margin: 0 auto; + } +} + +@media (min-width: 1200px) { + main .masonry-layout.hero-masonry.background-circle-gradient { + grid-template-rows: auto auto min(540px, auto); + } + + main .masonry-layout.hero-masonry .text-block .foreground { + max-width: 800px; + margin: 0 auto; + } + + main .masonry-layout.hero-masonry .brick.stack .foreground { + flex-direction: column; + justify-content: space-between; + gap: var(--spacing-m); + padding: var(--spacing-l) var(--spacing-s); + } + + main .masonry-layout.hero-masonry .brick.grid-full-width .brick-media { + aspect-ratio: auto; + border-top-left-radius: 0; + border-bottom-left-radius: 0; + } + + main .masonry-layout.hero-masonry .brick.grid-full-width .brick-media video { + object-fit: cover; + } +} + +/* + * Background Circle Gradient +*/ +.background-circle-gradient { + position: relative; +} + +.background-circle-gradient > div { + z-index: 2; +} + +.background-circle-gradient::before, +.background-circle-gradient::after { + content: ''; + position: absolute; + width: 100%; + background-size: contain; + background-repeat: no-repeat; +} + +.background-circle-gradient::before { + background-image: linear-gradient(160deg, rgba(247 207 187 / 100%) 0%, rgba(252 235 223 / 100%) 51%, rgba(255 255 255 / 0%) 70%, rgba(255 255 255 / 0%) 100%); + top: 0; + right: 0; + height: 20%; +} + +.background-circle-gradient::after { + background-image: linear-gradient(30deg, rgba(248 198 248 / 100%) 0%, rgb(247 194 220 / 100%) 51%, rgba(255 255 255 / 0%) 80%, rgba(255 255 255 / 0%) 100%); + bottom: 0; + left: 0; + height: 40%; + z-index: 1; +} + +@media (min-width: 600px) { + .background-circle-gradient::before { + background-image: url('/img/background/circle-gradient-top-right.jpg'); + background-position: top right; + } + + .background-circle-gradient::after { + background-image: url('/img/background/circle-gradient-bottom-left.png'); + background-position: bottom left; + } + + .background-circle-gradient::before, + .background-circle-gradient::after { + background-size: contain; + background-repeat: no-repeat; + } +} + +@media (min-width: 768px) { + .background-circle-gradient::before { + width: 80%; + height: 80%; + } + + .background-circle-gradient::after { + width: 90%; + height: 90%; + } +} + +/* + * Marquee +*/ +.marquee { + position: relative; +} + +.marquee.split .asset { + position: relative; + z-index: 1; + aspect-ratio: 16 / 9; +} + +.marquee.split.circle-gradient-pink { + flex-direction: column-reverse; +} + +.marquee.circle-gradient-pink::before { + content: ''; + position: absolute; + left: 0; + bottom: 0; + width: 100%; + height: 100%; + background: url('/img/background/marquee-circle-gradient-pink.jpg'); + background-size: 110%; + background-repeat: no-repeat; + background-position: top left; +} + +@media (min-width: 600px) { + .marquee.split .asset { + aspect-ratio: auto; + } + + .marquee.circle-gradient-pink::before { + background-position: bottom left; + } +} + +@media (min-width: 700px) { + .marquee.circle-gradient-pink::before { + background-size: contain; + } +}