diff --git a/libs/blocks/hero-marquee/hero-marquee.js b/libs/blocks/hero-marquee/hero-marquee.js index 91a0499e68..e464c7d3c2 100644 --- a/libs/blocks/hero-marquee/hero-marquee.js +++ b/libs/blocks/hero-marquee/hero-marquee.js @@ -5,6 +5,7 @@ import { decorateTextOverrides, decorateButtons, handleObjectFit, + loadCDT, } from '../../utils/decorate.js'; import { createTag, loadStyle, getConfig } from '../../utils/utils.js'; @@ -259,5 +260,10 @@ export default async function init(el) { } }); decorateTextOverrides(el, ['-heading', '-body', '-detail'], mainCopy); + + if (el.classList.contains('countdown-timer')) { + promiseArr.push(loadCDT(copy, el.classList)); + } + await Promise.all(promiseArr); } diff --git a/libs/blocks/marquee/marquee.js b/libs/blocks/marquee/marquee.js index 39f52449c6..d3e06edf4e 100644 --- a/libs/blocks/marquee/marquee.js +++ b/libs/blocks/marquee/marquee.js @@ -2,7 +2,7 @@ * Marquee - v6.0 */ -import { decorateButtons, getBlockSize, decorateBlockBg } from '../../utils/decorate.js'; +import { decorateButtons, getBlockSize, decorateBlockBg, loadCDT } from '../../utils/decorate.js'; import { createTag, getConfig, loadStyle } from '../../utils/utils.js'; // [headingSize, bodySize, detailSize] @@ -133,7 +133,15 @@ export default async function init(el) { if (iconArea?.childElementCount > 1) decorateMultipleIconArea(iconArea); extendButtonsClass(text); if (el.classList.contains('split')) decorateSplit(el, foreground, media); + + const promiseArr = []; if (el.classList.contains('mnemonic-list') && foreground) { - await loadMnemonicList(foreground); + promiseArr.push(loadMnemonicList(foreground)); + } + + if (el.classList.contains('countdown-timer')) { + promiseArr.push(loadCDT(text, el.classList)); } + + await Promise.all(promiseArr); } diff --git a/libs/blocks/media/media.js b/libs/blocks/media/media.js index 2fcc018576..1dbcb9e0b1 100644 --- a/libs/blocks/media/media.js +++ b/libs/blocks/media/media.js @@ -1,6 +1,13 @@ /* media - consonant v6 */ -import { decorateBlockBg, decorateBlockText, getBlockSize, decorateTextOverrides, applyHoverPlay } from '../../utils/decorate.js'; +import { + decorateBlockBg, + decorateBlockText, + getBlockSize, + decorateTextOverrides, + applyHoverPlay, + loadCDT, +} from '../../utils/decorate.js'; import { createTag, loadStyle, getConfig } from '../../utils/utils.js'; const blockTypeSizes = { @@ -33,7 +40,7 @@ function decorateQr(el) { qrImage.classList.add('qr-code-img'); } -export default function init(el) { +export default async function init(el) { if (el.className.includes('rounded-corners')) { const { miloLibs, codeRoot } = getConfig(); const base = miloLibs || codeRoot; @@ -105,4 +112,8 @@ export default function init(el) { const mediaRowReversed = el.querySelector(':scope > .foreground > .media-row > div').classList.contains('text'); if (mediaRowReversed) el.classList.add('media-reverse-mobile'); decorateTextOverrides(el); + + if (el.classList.contains('countdown-timer')) { + await loadCDT(container, el.classList); + } } diff --git a/libs/features/cdt/cdt.css b/libs/features/cdt/cdt.css new file mode 100644 index 0000000000..c67183c43a --- /dev/null +++ b/libs/features/cdt/cdt.css @@ -0,0 +1,86 @@ +.horizontal, +.vertical { + display: flex; + padding: 20px 0; +} + +.vertical { + flex-direction: column; +} + +.center { + align-items: center; + justify-content: center; +} + +.timer-label { + font-size: var(--type-body-s-size); + font-weight: 700; + height: 27px; +} + +.light .timer-label { + color: #000; +} + +.dark .timer-label { + color: #FFF; +} + +.horizontal .timer-label { + margin: 0 2px 27px; +} + +.timer-block { + display: flex; +} + +.horizontal .timer-block { + margin-left: 10px; +} + +.timer-fragment { + display: flex; + flex-direction: column; + align-items: center; +} + +.timer-box { + padding: 0 9px; + width: 10px; + border-radius: 5px; + font-size: var(--type-body-m-size); + font-weight: 700; + text-align: center; +} + +.light .timer-box { + background-color: #222; + color: #FFF; +} + +.dark .timer-box { + background-color: #EBEBEB; + color: #1D1D1D; +} + +.timer-unit-container { + display: flex; + column-gap: 2px; + align-items: center; +} + +.timer-unit-label { + width: 100%; + font-size: var(--type-body-xs-size); + font-weight: 400; + text-align: start; +} + +.light .timer-unit-label { + color: #464646; +} + +.dark .timer-unit-label { + color: #D1D1D1; +} diff --git a/libs/features/cdt/cdt.js b/libs/features/cdt/cdt.js new file mode 100644 index 0000000000..8afb6b5116 --- /dev/null +++ b/libs/features/cdt/cdt.js @@ -0,0 +1,121 @@ +import { getMetadata, getConfig, createTag } from '../../utils/utils.js'; +import { replaceKey } from '../placeholders.js'; + +function loadCountdownTimer( + container, + cdtLabel, + cdtDays, + cdtHours, + cdtMins, + timeRangesEpoch, +) { + let isVisible = false; + let interval; + + function appendTimerBox(parent, value, label) { + const fragment = createTag('div', { class: 'timer-fragment' }, null, { parent }); + const unitContainer = createTag('div', { class: 'timer-unit-container' }, null, { parent: fragment }); + createTag('div', { class: 'timer-unit-label' }, label, { parent: fragment }); + + createTag('div', { class: 'timer-box' }, Math.floor(value / 10).toString(), { parent: unitContainer }); + createTag('div', { class: 'timer-box' }, (value % 10).toString(), { parent: unitContainer }); + } + + function appendSeparator(parent) { + createTag('div', { class: 'timer-separator' }, ':', { parent }); + } + + function appendTimerBlock(parent, daysLeft, hoursLeft, minutesLeft) { + const timerBlock = createTag('div', { class: 'timer-block' }, null, { parent }); + appendTimerBox(timerBlock, daysLeft, cdtDays); + appendSeparator(timerBlock); + appendTimerBox(timerBlock, hoursLeft, cdtHours); + appendSeparator(timerBlock); + appendTimerBox(timerBlock, minutesLeft, cdtMins); + } + + function appendTimerLabel(parent, label) { + createTag('div', { class: 'timer-label' }, label, { parent }); + } + + function removeCountdown() { + container.replaceChildren(); + } + + function render(daysLeft, hoursLeft, minutesLeft) { + if (!isVisible) return; + + removeCountdown(); + + appendTimerLabel(container, cdtLabel); + appendTimerBlock(container, daysLeft, hoursLeft, minutesLeft); + } + + function updateCountdown() { + const instant = new URL(window.location.href)?.searchParams?.get('instant'); + const currentTime = instant ? new Date(instant) : Date.now(); + + for (let i = 0; i < timeRangesEpoch.length; i += 2) { + const startTime = timeRangesEpoch[i]; + const endTime = timeRangesEpoch[i + 1]; + + if (currentTime >= startTime && currentTime <= endTime) { + isVisible = true; + const diffTime = endTime - currentTime; + const daysLeft = Math.floor(diffTime / (1000 * 60 * 60 * 24)); + const hoursLeft = Math.floor((diffTime % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60)); + const minutesLeft = Math.floor((diffTime % (1000 * 60 * 60)) / (1000 * 60)); + render(daysLeft, hoursLeft, minutesLeft); + return; + } + } + + isVisible = false; + clearInterval(interval); + removeCountdown(); + } + + function startCountdown() { + const oneMinuteinMs = 60000; + updateCountdown(); + interval = setInterval(updateCountdown, oneMinuteinMs); + } + + startCountdown(); +} + +function isMobile() { + return window.matchMedia('(max-width: 767px)').matches; +} + +export default async function initCDT(el, classList) { + const placeholders = ['cdt-ends-in', 'cdt-days', 'cdt-hours', 'cdt-mins']; + const [cdtLabel, cdtDays, cdtHours, cdtMins] = await Promise.all( + placeholders.map((placeholder) => replaceKey(placeholder, getConfig())), + ); + + const cdtMetadata = getMetadata('countdown-timer'); + if (cdtMetadata === null) { + throw new Error('Metadata for countdown-timer is not available'); + } + + const cdtRange = cdtMetadata.split(','); + if (cdtRange.length % 2 !== 0) { + throw new Error('Invalid countdown timer range'); + } + + const timeRangesEpoch = cdtRange.map((time) => { + const parsedTime = Date.parse(time?.trim()); + return Number.isNaN(parsedTime) ? null : parsedTime; + }); + if (timeRangesEpoch.includes(null)) { + throw new Error('Invalid format for countdown timer range'); + } + + const cdtDiv = createTag('div', { class: 'countdown-timer' }, null, { parent: el }); + cdtDiv.classList.add(isMobile() ? 'vertical' : 'horizontal'); + cdtDiv.classList.add(classList.contains('dark') ? 'dark' : 'light'); + if (classList.contains('center')) cdtDiv.classList.add('center'); + + loadCountdownTimer(cdtDiv, cdtLabel, cdtDays, cdtHours, cdtMins, timeRangesEpoch); +} diff --git a/libs/utils/decorate.js b/libs/utils/decorate.js index 556670c79f..c577622b95 100644 --- a/libs/utils/decorate.js +++ b/libs/utils/decorate.js @@ -1,4 +1,6 @@ -import { createTag } from './utils.js'; +import { createTag, loadStyle, getConfig } from './utils.js'; + +const { miloLibs, codeRoot } = getConfig(); export function decorateButtons(el, size) { const buttons = el.querySelectorAll('em a, strong a, p > a strong'); @@ -309,3 +311,15 @@ export function decorateMultiViewport(el) { } return foreground; } + +export async function loadCDT(el, classList) { + try { + await Promise.all([ + loadStyle(`${miloLibs || codeRoot}/features/cdt/cdt.css`), + import('../features/cdt/cdt.js') + .then(({ default: initCDT }) => initCDT(el, classList)), + ]); + } catch (error) { + window.lana?.log(`Failed to load countdown timer module: ${error}`, { tags: 'countdown-timer' }); + } +} diff --git a/test/blocks/hero-marquee/mocks/body.html b/test/blocks/hero-marquee/mocks/body.html index 72166c8f81..2031f3cd68 100644 --- a/test/blocks/hero-marquee/mocks/body.html +++ b/test/blocks/hero-marquee/mocks/body.html @@ -106,7 +106,7 @@
small
-