Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[MWPW-153363] Countdown Timer implementation based on page metadata #2928

Open
wants to merge 11 commits into
base: stage
Choose a base branch
from
9 changes: 9 additions & 0 deletions libs/blocks/hero-marquee/hero-marquee.js
Original file line number Diff line number Diff line change
Expand Up @@ -259,5 +259,14 @@ export default async function init(el) {
}
});
decorateTextOverrides(el, ['-heading', '-body', '-detail'], mainCopy);

if (el.classList.contains('countdown-timer')) {
const { default: initCDT } = await import('../../features/cdt/cdt.js');
const classesToAdd = [];
if (el.classList.contains('center')) classesToAdd.push('center');
classesToAdd.push(el.classList.contains('dark') ? 'dark' : 'light');
await initCDT(copy, classesToAdd);
rahulgupta999 marked this conversation as resolved.
Show resolved Hide resolved
}

await Promise.all(promiseArr);
}
8 changes: 8 additions & 0 deletions libs/blocks/marquee/marquee.js
Original file line number Diff line number Diff line change
Expand Up @@ -136,4 +136,12 @@ export default async function init(el) {
if (el.classList.contains('mnemonic-list') && foreground) {
await loadMnemonicList(foreground);
}

if (el.classList.contains('countdown-timer')) {
const { default: initCDT } = await import('../../features/cdt/cdt.js');
const classesToAdd = [];
if (el.classList.contains('center')) classesToAdd.push('center');
classesToAdd.push(el.classList.contains('dark') ? 'dark' : 'light');
rahulgupta999 marked this conversation as resolved.
Show resolved Hide resolved
await initCDT(text, classesToAdd);
}
}
9 changes: 8 additions & 1 deletion libs/blocks/media/media.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,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;
Expand Down Expand Up @@ -105,4 +105,11 @@ 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')) {
const { default: initCDT } = await import('../../features/cdt/cdt.js');
const classesToAdd = [];
classesToAdd.push(el.classList.contains('dark') ? 'dark' : 'light');
rahulgupta999 marked this conversation as resolved.
Show resolved Hide resolved
await initCDT(container, classesToAdd);
}
}
86 changes: 86 additions & 0 deletions libs/features/cdt/cdt.css
Original file line number Diff line number Diff line change
@@ -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 45px;
}

.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;
}
124 changes: 124 additions & 0 deletions libs/features/cdt/cdt.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
import { getMetadata, getConfig, loadStyle } from '../../utils/utils.js';
import { replaceKey } from '../placeholders.js';

const replacePlaceholder = async (key) => replaceKey(key, getConfig());

async function loadCountdownTimer(container) {
const cdtLabel = await replacePlaceholder('cdt-ends-in');
const cdtDays = await replacePlaceholder('cdt-days');
const cdtHours = await replacePlaceholder('cdt-hours');
const cdtMins = await replacePlaceholder('cdt-mins');
rahulgupta999 marked this conversation as resolved.
Show resolved Hide resolved
const cdtRange = (getMetadata('countdown-timer')).split(',');
rahulgupta999 marked this conversation as resolved.
Show resolved Hide resolved
const timeRangesEpoch = cdtRange.map((time) => Date.parse(time.trim()));
rahulgupta999 marked this conversation as resolved.
Show resolved Hide resolved
let isVisible = false;
let interval;

const { miloLibs, codeRoot } = getConfig();
await Promise.resolve(loadStyle(`${miloLibs || codeRoot}/features/cdt/cdt.css`));
rahulgupta999 marked this conversation as resolved.
Show resolved Hide resolved

function createTimerFragment(value, label) {
const fragment = document.createElement('div');
fragment.classList.add('timer-fragment');

const unitContainer = document.createElement('div');
unitContainer.classList.add('timer-unit-container');
fragment.appendChild(unitContainer);

const tensBox = document.createElement('div');
tensBox.classList.add('timer-box');
tensBox.textContent = Math.floor(value / 10);
unitContainer.appendChild(tensBox);
rahulgupta999 marked this conversation as resolved.
Show resolved Hide resolved

const onesBox = document.createElement('div');
onesBox.classList.add('timer-box');
onesBox.textContent = value % 10;
unitContainer.appendChild(onesBox);

const unitLabel = document.createElement('div');
unitLabel.classList.add('timer-unit-label');
unitLabel.textContent = label;
fragment.appendChild(unitLabel);

return fragment;
}

function removeCountdown() {
container.innerHTML = '';
}

function createSeparator() {
const separator = document.createElement('div');
separator.classList.add('timer-label');
separator.textContent = ':';
return separator;
}

function render(daysLeft, hoursLeft, minutesLeft) {
if (!isVisible) return;

removeCountdown();

const labelElement = document.createElement('div');
labelElement.classList.add('timer-label');
labelElement.textContent = cdtLabel;
container.appendChild(labelElement);

const timerBlock = document.createElement('div');
timerBlock.classList.add('timer-block');
container.appendChild(timerBlock);

timerBlock.appendChild(createTimerFragment(daysLeft, cdtDays));
timerBlock.appendChild(createSeparator());
rahulgupta999 marked this conversation as resolved.
Show resolved Hide resolved
timerBlock.appendChild(createTimerFragment(hoursLeft, cdtHours));
timerBlock.appendChild(createSeparator());
timerBlock.appendChild(createTimerFragment(minutesLeft, cdtMins));
}

function updateCountdown() {
const currentTime = 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();
}

const isMobileDevice = () => /Android|webOS|iPhone|iPad|iPod/i.test(navigator.userAgent);

export default async function initCDT(el, classesToAdd) {
try {
const cdtDiv = document.createElement('div');
cdtDiv.classList.add('countdown-timer');
classesToAdd.forEach((className) => {
cdtDiv.classList.add(className);
});
cdtDiv.classList.add(isMobileDevice() ? 'vertical' : 'horizontal');
el.appendChild(cdtDiv);
await loadCountdownTimer(cdtDiv);
} catch (error) {
window.lana?.log(`Failed to load countdown timer module: ${error}`, { tags: 'countdown-timer' });
}
}
7 changes: 7 additions & 0 deletions test/blocks/hero-marquee/hero-marquee.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ setConfig(conf);
describe('Hero Marquee', () => {
before(async () => {
document.body.innerHTML = await readFile({ path: './mocks/body.html' });
const meta = Object.assign(document.createElement('meta'), { name: 'countdown-timer', content: '2024-08-26 12:00:00 PST,2026-08-30 00:00:00 PST' });
document.head.appendChild(meta);
const { default: init } = await import('../../../libs/blocks/hero-marquee/hero-marquee.js');
const marquees = document.querySelectorAll('.hero-marquee');
marquees.forEach(async (marquee) => {
Expand All @@ -35,4 +37,9 @@ describe('Hero Marquee', () => {
const hr = await waitForElement('.has-divider');
expect(hr).to.exist;
});

it('Embedding countdown-timer inside hero-marquee', async () => {
const marquee = document.getElementById('hero-cdt');
expect(marquee.getElementsByClassName('timer-label')).to.exist;
});
});
41 changes: 41 additions & 0 deletions test/blocks/hero-marquee/mocks/body.html
Original file line number Diff line number Diff line change
Expand Up @@ -117,3 +117,44 @@ <h1 id="row-cell---text-right-2">Hero w/ Adobe.tv link</h1>
<div><a href="https://video.tv.adobe.com/v/3427744">https://video.tv.adobe.com/v/3427744</a></div>
</div>
</div>

<div id="hero-cdt" class="hero-marquee xxxl-heading countdown-timer">
<div>
<div>
<picture>
<img loading="lazy" alt="" src="./" width="100" height="100">
</picture>
</div>
</div>
<div>
<div>
<p><picture><img loading="lazy" src="./"></picture> After Effects</p>
<p>DETAIL TEXT</p>
<h2 id="this-hero-has-all-row-types">This Hero has all row types</h2>
<p>lockup, list, qrcode, text, background</p>
<p><em><a href="#the-bg-image-is-on-top-for-mobiletablet"><span class="icon icon-checkmark-circle"></span>See more</a></em> <strong><a href="#bg-mobile">Other options you say?</a></strong></p>
</div>
</div>
<div>
<div data-valign="middle">con-block-row-list (max-width-6-tablet)</div>
<div>
<ul>
<li><span class="icon icon-checkmark"></span>Small</li>
<li><span class="icon icon-checkmark"></span>Medium length text</li>
<li><span class="icon icon-checkmark"></span>Long length text that may break onto a new line, what will happen, keep it going so this is even longer and really wraps?</li>
<li><span class="icon icon-checkmark"></span>Another list</li>
</ul>
</div>
</div>
<div>
<div>--- white</div>
</div>
<div>
<div>con-block-row-text (xs-body, m-button)</div>
<div>See plans for <a href="#teach">students and teachers</a> or <a href="#biz">small and medium business.</a></div>
</div>
<div>
<div>con-block-row-text (body-m)</div>
<div>Text with no button class</div>
</div>
</div>
24 changes: 24 additions & 0 deletions test/blocks/hero-marquee/mocks/placeholders.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
{
"total": 21,
"offset": 0,
"limit": 21,
"data": [
{
"key": "cdt-ends-in",
"value": "ENDS IN"
},
{
"key": "cdt-days",
"value": "days"
},
{
"key": "cdt-hours",
"value": "hours"
},
{
"key": "cdt-mins",
"value": "mins"
}
],
":type": "sheet"
}
7 changes: 7 additions & 0 deletions test/blocks/marquee/marquee.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ const video = await readFile({ path: './mocks/video.html' });
const multipleIcons = await readFile({ path: './mocks/multiple-icons.html' });

describe('marquee', () => {
const meta = Object.assign(document.createElement('meta'), { name: 'countdown-timer', content: '2024-08-26 12:00:00 PST,2026-08-30 00:00:00 PST' });
document.head.appendChild(meta);
const marquees = document.querySelectorAll('.marquee');
marquees.forEach((marquee) => {
init(marquee);
Expand Down Expand Up @@ -163,4 +165,9 @@ describe('marquee', () => {
expect(log.calledOnceWith(`Failed to load mnemonic marquee module: ${error}`)).to.be.false;
});
});

describe('Embedding countdown-timer inside marquee', () => {
const marquee = document.getElementById('countdown-timer');
expect(marquee.getElementsByClassName('timer-label')).to.exist;
});
});
14 changes: 14 additions & 0 deletions test/blocks/marquee/mocks/body.html
Original file line number Diff line number Diff line change
Expand Up @@ -381,3 +381,17 @@ <h2>Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod temp
</div>
</div>
</div>
<h2>Catalog Marquee with Countdown timer</h2>
<div class="marquee quiet light center countdown-timer" id="countdown-timer">
<div>
<div data-valign="middle">linear-gradient(90deg, rgba(226,106,96,1) 0%, rgba(228,170,166,1) 51%, rgba(163,243,120,1) 100%)</div>
</div>
<div>
<div data-valign="middle">
<h2>Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation.</h2>
<p>Body M Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation.</p>
<p><br><strong>Includes:</strong></p>
<p><picture><img loading="lazy" src="/assets/img/product-icons/svg/acrobat-pro-40.svg" alt="Acrobat Pro"></picture> <strong>Acrobat</strong></p>
</div>
</div>
</div>
Loading
Loading