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

Merged
merged 19 commits into from
Sep 23, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions libs/blocks/hero-marquee/hero-marquee.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
decorateTextOverrides,
decorateButtons,
handleObjectFit,
loadCDT,
} from '../../utils/decorate.js';
import { createTag, loadStyle, getConfig } from '../../utils/utils.js';

Expand Down Expand Up @@ -259,5 +260,10 @@ export default async function init(el) {
}
});
decorateTextOverrides(el, ['-heading', '-body', '-detail'], mainCopy);

if (el.classList.contains('countdown-timer')) {
rahulgupta999 marked this conversation as resolved.
Show resolved Hide resolved
promiseArr.push(loadCDT(copy, el.classList));
}

await Promise.all(promiseArr);
}
12 changes: 10 additions & 2 deletions libs/blocks/marquee/marquee.js
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down Expand Up @@ -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));
rahulgupta999 marked this conversation as resolved.
Show resolved Hide resolved
}

await Promise.all(promiseArr);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

amazing, can you add a test example of a marquee with both mnemonic-list & countdown-timer to ensure this faster implementation also doesn't break anything?

}
15 changes: 13 additions & 2 deletions libs/blocks/media/media.js
Original file line number Diff line number Diff line change
@@ -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 = {
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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);
}
}
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 {
rahulgupta999 marked this conversation as resolved.
Show resolved Hide resolved
color: #000;
}

.dark .timer-label {
color: #FFF;
}

.horizontal .timer-label {
margin: 0 2px 27px;
}

.timer-block {
display: flex;
}

.horizontal .timer-block {
margin-left: 10px;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what about RTL support ? In RTL there's currently no space between text and timer
Screenshot 2024-09-23 at 15 20 04

}

.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;
}
121 changes: 121 additions & 0 deletions libs/features/cdt/cdt.js
Original file line number Diff line number Diff line change
@@ -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();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why always remove the countdown ? Can you not just update the timer label?


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;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The value gets cached and will not work when rotating screens. You need to refactor this to always see if the viewport matches the width.
Example:

const isMobile = window.matchMedia('(max-width: 767px)');
if (isMobile.matches) doSomething(); // this will work after you resize or rotate screens.

}

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)) {
rahulgupta999 marked this conversation as resolved.
Show resolved Hide resolved
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');
rahulgupta999 marked this conversation as resolved.
Show resolved Hide resolved
if (classList.contains('center')) cdtDiv.classList.add('center');

loadCountdownTimer(cdtDiv, cdtLabel, cdtDays, cdtHours, cdtMins, timeRangesEpoch);
}
16 changes: 15 additions & 1 deletion libs/utils/decorate.js
Original file line number Diff line number Diff line change
@@ -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');
Expand Down Expand Up @@ -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' });
}
rahulgupta999 marked this conversation as resolved.
Show resolved Hide resolved
}
2 changes: 1 addition & 1 deletion test/blocks/hero-marquee/mocks/body.html
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ <h1 id="row-cell---text-right-2">2-Row Cell - text right</h1>
</div>
</div>

<div id="hero-w-adobetv" class="hero-marquee">
<div id="hero-w-adobetv" class="hero-marquee countdown-timer">
<div>
<div>#ffffff</div>
</div>
Expand Down
2 changes: 1 addition & 1 deletion test/blocks/marquee/mocks/body.html
Original file line number Diff line number Diff line change
Expand Up @@ -184,7 +184,7 @@ <h2 id="marquee-inline">Marquee inline</h2>

<h2>Marquee (split)</h2>
<p>small</p>
<div class="marquee split small" id="media-credit-text">
<div class="marquee split small countdown-timer" id="media-credit-text">
<div>
<div>#000000</div>
</div>
Expand Down
11 changes: 10 additions & 1 deletion test/blocks/media/media.test.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
import { readFile, setViewport } from '@web/test-runner-commands';
import { expect } from '@esm-bundle/chai';
import { setConfig, getConfig } from '../../../libs/utils/utils.js';

document.head.innerHTML = "<link rel='stylesheet' href='../../../libs/blocks/media/media.css'>";
const locales = { '': { ietf: 'en-US', tk: 'hah7vzn.css' } };
const conf = { locales, miloLibs: 'http://localhost:2000/libs' };
setConfig(conf);
getConfig().locale.contentRoot = '/test/blocks/media/mocks';

document.head.innerHTML = '<link rel="stylesheet" href="../../../libs/blocks/media/media.css"><meta name="countdown-timer" content="2024-08-26 12:00:00 PST,2026-08-30 00:00:00 PST">';
document.body.innerHTML = await readFile({ path: './mocks/body.html' });
const { default: init } = await import('../../../libs/blocks/media/media.js');
describe('media', () => {
Expand Down Expand Up @@ -132,5 +138,8 @@ describe('media', () => {
const detail = medias[8].querySelector('.detail-l');
expect(detail).to.exist;
});
it('has a cdt', () => {
expect(medias[8].querySelectorAll('.timer-label')).to.have.lengthOf(1);
});
});
});
2 changes: 1 addition & 1 deletion test/blocks/media/mocks/body.html
Original file line number Diff line number Diff line change
Expand Up @@ -170,7 +170,7 @@ <h2 id="heading-xs-1822-media-merch-small"><strong>Heading XS 18/22 Media (merch
</div>
</div>
</div>
<div class="media medium-compact">
<div class="media medium-compact countdown-timer">
<div>
<div data-valign="middle">
<picture>
Expand Down
24 changes: 24 additions & 0 deletions test/blocks/media/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"
}
Loading
Loading