Skip to content

Commit

Permalink
Container PR for initial TWP features (#2407)
Browse files Browse the repository at this point in the history
* Mwpw-136871: TwP merch card (#2088)

* MWPW-136871: TwP Merch Card

* unit tests

* TWP block draft (#2105)

* add draft for a merch-twp block

---------

Co-authored-by: Axel Cureno Basurto <[email protected]>

* Mwpw 136871: Price display inside TwP merch card (#2110)

* MWPW-136871: TwP Merch Card

* unit tests

* twp card

* price display in TwP

* deps update

* Mwpw 136871 (#2154)

* MWPW-136871: TwP Merch Card

* unit tests

* twp card

* price display in TwP

* deps update

* MWPW-138927: merch-twp WIP (#2160)

Merging proactively for a GWP demo.

* MWPW-138927: desktop step 2 layout WIP

* remove comment

* pr feedback

* update deps

* update deps

* update deps

* update deps

* fixed tests

* fix lagging subscription panel

* WIP

* fix tests

* fix tests

* wip

* fix failing test

* fix failing tests

* remove files

* improve code coverage

* address first batch of comments

* fix dependencies

* fix unit tests

* add missing test cases

---------

Co-authored-by: Axel Cureno Basurto <[email protected]>
Co-authored-by: Mariia Lukianets <[email protected]>
Co-authored-by: Axel Cureno Basurto <[email protected]>
  • Loading branch information
4 people authored Jun 17, 2024
1 parent 82b5a83 commit 16c64e4
Show file tree
Hide file tree
Showing 53 changed files with 3,964 additions and 540 deletions.
5 changes: 5 additions & 0 deletions libs/blocks/merch-card/merch-card.css
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,11 @@ div[class*="-merch-card"] > div,
color: var(--text-color);
}

.twp.merch-card .merch-card-price {
font-weight: 700;
margin: 0;
}

merch-card.special-offers del span[is="inline-price"] {
text-decoration: line-through;
}
Expand Down
187 changes: 119 additions & 68 deletions libs/blocks/merch-card/merch-card.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,24 @@ import '../../deps/merch-card.js';

const TAG_PATTERN = /^[a-zA-Z0-9_-]+:[a-zA-Z0-9_-]+\/[a-zA-Z0-9_-].*$/;

const CARD_TYPES = ['segment', 'special-offers', 'plans', 'catalog', 'product', 'inline-heading', 'image', 'mini-compare-chart'];
const SEGMENT = 'segment';
const SPECIAL_OFFERS = 'special-offers';
const PLANS = 'plans';
const CATALOG = 'catalog';
const PRODUCT = 'product';
const MINI_COMPARE_CHART = 'mini-compare-chart';
const TWP = 'twp';
const CARD_TYPES = [
SEGMENT,
SPECIAL_OFFERS,
PLANS,
CATALOG,
PRODUCT,
'inline-heading',
'image',
MINI_COMPARE_CHART,
TWP,
];

const CARD_SIZES = ['wide', 'super-wide'];

Expand All @@ -25,13 +42,9 @@ const HEADING_MAP = {
},
};

const MINI_COMPARE_CHART = 'mini-compare-chart';
const PLANS = 'plans';
const SEGMENT = 'segment';

const INNER_ELEMENTS_SELECTOR = 'h2, h3, h4, h5, p, ul, em';

const MULTI_OFFER_CARDS = ['plans', 'product', MINI_COMPARE_CHART];
const MULTI_OFFER_CARDS = [PLANS, PRODUCT, MINI_COMPARE_CHART, TWP];
// Force cards to refresh once they become visible so that the footer rows are properly aligned.
const intersectionObserver = new IntersectionObserver((entries) => {
entries.forEach((entry) => {
Expand All @@ -46,10 +59,10 @@ const getPodType = (styles) => styles?.find((style) => CARD_TYPES.includes(style
const isHeadingTag = (tagName) => /^H[2-5]$/.test(tagName);
const isParagraphTag = (tagName) => tagName === 'P';

const appendSlot = (slotEls, slotName, merchCard) => {
const appendSlot = (slotEls, slotName, merchCard, nodeName = 'p') => {
if (slotEls.length === 0 || merchCard.variant !== MINI_COMPARE_CHART) return;
const newEl = createTag(
'p',
nodeName,
{ slot: slotName, class: slotName },
);
slotEls.forEach((e) => {
Expand All @@ -72,6 +85,68 @@ export async function loadMnemonicList(foreground) {
}
}

function extractQuantitySelect(el) {
const quantitySelectConfig = [...el.querySelectorAll('ul')]
.find((ul) => ul.querySelector('li')?.innerText?.includes('Quantity'));
const configMarkup = quantitySelectConfig?.querySelector('ul');
if (!configMarkup) return null;
const config = configMarkup.children;
if (config.length !== 2) return null;
const attributes = {};
attributes.title = config[0].textContent.trim();
const values = config[1].textContent.split(',')
.map((value) => value.trim())
.filter((value) => /^\d*$/.test(value))
.map((value) => (value === '' ? undefined : Number(value)));
quantitySelectConfig.remove();
if (![3, 4, 5].includes(values.length)) return null;
import('../../deps/merch-quantity-select.js');
[attributes.min, attributes.max, attributes.step, attributes['default-value'], attributes['max-input']] = values;
const quantitySelect = createTag('merch-quantity-select', attributes);
return quantitySelect;
}

const parseTwpContent = async (el, merchCard) => {
const quantitySelect = extractQuantitySelect(el);
if (quantitySelect) {
merchCard.append(quantitySelect);
}
let allElements = el?.children[0]?.children[0]?.children;
if (!allElements?.length) return;
allElements = [...allElements];
const contentGroups = allElements.reduce((acc, curr) => {
if (curr.tagName.toLowerCase() === 'p' && curr.textContent.trim() === '--') {
acc.push([]);
} else {
acc[acc.length - 1].push(curr);
}
return acc;
}, [[]]);

contentGroups.forEach((group, index) => {
if (index === 0) { // Top section
const headings = group.filter((e) => e.tagName.toLowerCase() === 'h3');
const topBody = group.filter((e) => e.tagName.toLowerCase() === 'p');
appendSlot(headings, 'heading-xs', merchCard);
appendSlot(topBody, 'body-xs-top', merchCard);
} else if (index === 1) { // Body section
const content = group.filter((e) => e.tagName.toLowerCase() === 'p' || e.tagName.toLowerCase() === 'ul');
const bodySlot = createTag('div', { slot: 'body-xs' }, content);
merchCard.append(bodySlot);
} else if (index === 2) { // Footer section
const footerContent = group.filter((e) => ['h5', 'p'].includes(e.tagName.toLowerCase()));
const footer = createTag('div', { slot: 'footer' }, footerContent);
merchCard.append(footer);
}
});

const offerSelection = el.querySelector('ul');
if (offerSelection) {
const { initOfferSelection } = await import('./merch-offer-select.js');
await initOfferSelection(merchCard, offerSelection);
}
};

const parseContent = async (el, merchCard) => {
let bodySlotName = `body-${merchCard.variant !== MINI_COMPARE_CHART ? 'xs' : 'm'}`;
let headingMCount = 0;
Expand Down Expand Up @@ -201,7 +276,7 @@ const decorateMerchCardLinkAnalytics = (el) => {
};

const addStock = (merchCard, styles) => {
if (styles.includes('add-stock')) {
if (styles.includes('add-stock') && merchCard.variant !== TWP) {
let stock;
const selector = styles.includes('edu') ? '.merch-offers.stock.edu > *' : '.merch-offers.stock > *';
const [label, ...rest] = [...document.querySelectorAll(selector)];
Expand All @@ -225,27 +300,6 @@ const simplifyHrs = (el) => {
});
};

async function extractQuantitySelect(el) {
const quantitySelectConfig = el.querySelector('ul');
if (!quantitySelectConfig) return null;
const configMarkup = quantitySelectConfig.querySelector('li');
if (!configMarkup || !configMarkup.textContent.includes('Quantity')) return null;
const config = configMarkup.querySelector('ul').querySelectorAll('li');
if (config.length !== 2) return null;
const attributes = {};
attributes.title = config[0].textContent.trim();
const values = config[1].textContent.split(',')
.map((value) => value.trim())
.filter((value) => /^\d*$/.test(value))
.map((value) => (value === '' ? undefined : Number(value)));
if (![3, 4, 5].includes(values.length)) return null;
await import('../../deps/merch-quantity-select.js');
[attributes.min, attributes.max, attributes.step, attributes['default-value'], attributes['max-input']] = values;
const quantitySelect = createTag('merch-quantity-select', attributes);
quantitySelectConfig.remove();
return quantitySelect;
}

const getMiniCompareChartFooterRows = (el) => {
let footerRows = Array.from(el.children).slice(1);
footerRows = footerRows.filter((row) => !row.querySelector('.footer-row-cell'));
Expand Down Expand Up @@ -289,7 +343,7 @@ const setMiniCompareOfferSlot = (merchCard, offers) => {
export default async function init(el) {
if (!el.querySelector(INNER_ELEMENTS_SELECTOR)) return el;
const styles = [...el.classList];
const cardType = getPodType(styles) || 'product';
const cardType = getPodType(styles) || PRODUCT;
if (!styles.includes(cardType)) {
styles.push(cardType);
}
Expand Down Expand Up @@ -344,7 +398,6 @@ export default async function init(el) {
);
merchCard.setAttribute('badge-color', badge.badgeColor);
merchCard.setAttribute('badge-text', badge.badgeText);
if (document.querySelector('html').dir === 'rtl') merchCard.setAttribute('is-rtl', 'true');
merchCard.classList.add('badge-card');
}
}
Expand Down Expand Up @@ -372,7 +425,7 @@ export default async function init(el) {
}
}
});
const actionMenuContent = cardType === 'catalog'
const actionMenuContent = cardType === CATALOG
? getActionMenuContent(el)
: null;
if (actionMenuContent) {
Expand All @@ -397,7 +450,6 @@ export default async function init(el) {
imageSlot.appendChild(image);
merchCard.appendChild(imageSlot);
}
parseContent(el, merchCard);
if (!icons || icons.length > 0) {
const iconImgs = Array.from(icons).map((icon) => {
const img = {
Expand All @@ -421,46 +473,45 @@ export default async function init(el) {
merchCard.setAttribute('filters', categories.join(','));
merchCard.setAttribute('types', types.join(','));

const footer = createTag('div', { slot: 'footer' });
if (ctas) {
if (merchCard.variant === 'mini-compare-chart') {
decorateButtons(ctas, 'button-l');
} else {
decorateButtons(ctas);
}
const links = ctas.querySelectorAll('a');
ctas.remove();
footer.append(...links);
}
merchCard.appendChild(footer);
if (merchCard.variant !== TWP) {
parseContent(el, merchCard);

if (MULTI_OFFER_CARDS.includes(cardType)) {
if (merchCard.variant === MINI_COMPARE_CHART) {
const miniCompareOffers = createTag('div', { slot: 'offers' });
merchCard.append(miniCompareOffers);
}
const quantitySelect = await extractQuantitySelect(el, cardType);
const offerSelection = el.querySelector('ul');
if (offerSelection) {
const { initOfferSelection } = await import('./merch-offer-select.js');
setMiniCompareOfferSlot(merchCard, undefined);
initOfferSelection(merchCard, offerSelection, quantitySelect);
const footer = createTag('div', { slot: 'footer' });
if (ctas) {
decorateButtons(ctas, (merchCard.variant === MINI_COMPARE_CHART) ? 'button-l' : undefined);
footer.append(ctas);
}
if (quantitySelect) {
merchCard.appendChild(footer);

if (MULTI_OFFER_CARDS.includes(cardType)) {
const quantitySelect = extractQuantitySelect(el);
const offerSelection = el.querySelector('ul');
if (merchCard.variant === MINI_COMPARE_CHART) {
setMiniCompareOfferSlot(merchCard, quantitySelect);
} else {
const bodySlot = merchCard.querySelector('div[slot="body-xs"]');
bodySlot.append(quantitySelect);
const miniCompareOffers = createTag('div', { slot: 'offers' });
merchCard.append(miniCompareOffers);
}
if (offerSelection) {
const { initOfferSelection } = await import('./merch-offer-select.js');
setMiniCompareOfferSlot(merchCard, undefined);
initOfferSelection(merchCard, offerSelection, quantitySelect);
}
if (quantitySelect) {
if (merchCard.variant === MINI_COMPARE_CHART) {
setMiniCompareOfferSlot(merchCard, quantitySelect);
} else {
const bodySlot = merchCard.querySelector('div[slot="body-xs"]');
bodySlot.append(quantitySelect);
}
}
}

decorateBlockHrs(merchCard);
simplifyHrs(merchCard);
if (merchCard.classList.contains('has-divider')) merchCard.setAttribute('custom-hr', true);
decorateFooterRows(merchCard, footerRows);
} else {
parseTwpContent(el, merchCard);
}
decorateBlockHrs(merchCard);
simplifyHrs(merchCard);
if (merchCard.classList.contains('has-divider')) {
merchCard.setAttribute('custom-hr', true);
}
decorateFooterRows(merchCard, footerRows);
el.replaceWith(merchCard);
decorateMerchCardLinkAnalytics(merchCard);
return merchCard;
Expand Down
57 changes: 34 additions & 23 deletions libs/blocks/merch-card/merch-offer-select.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,32 +2,29 @@ import { createTag } from '../../utils/utils.js';
import { decorateButtons } from '../../utils/decorate.js';
import '../../deps/merch-offer-select.js';

const TWP = 'twp';
const MINI_COMPARE_CHART = 'mini-compare-chart';

function createDynamicSlots(el, bodySlot) {
const isTWP = el.variant === TWP;
const pricePlaceholder = el.querySelector("span[is='inline-price']");
if (pricePlaceholder) {
pricePlaceholder.setAttribute('slot', 'price');
} else {
const priceSlot = createTag('h5', { class: 'merch-card-price' });
const tagName = isTWP ? 'p' : 'h5';
const priceSlot = createTag(tagName, { class: 'merch-card-price' });
createTag('span', { slot: 'price', is: 'inline-price' }, null, { parent: priceSlot });
bodySlot.append(priceSlot);
}
if (isTWP) return; // twp card do not display cta's
const p = createTag('p', { class: 'action-area' });
createTag('a', { slot: 'secondary-cta', is: 'checkout-link' }, null, { parent: p });
createTag('a', { slot: 'cta', is: 'checkout-link' }, null, { parent: p });
const footer = el.querySelector('div[slot="footer"]');
footer.append(p);
bodySlot.querySelector('p')?.setAttribute('slot', 'description');
if (el.variant === MINI_COMPARE_CHART) {
const description = el.querySelector('div[slot="body-m"] p:last-child');
if (description) {
const descriptionSlot = el.querySelector('p[slot="description"]');
if (descriptionSlot) {
descriptionSlot.innerHTML += description.innerHTML;
}
}
}
const descriptionSlot = bodySlot.querySelector('p');
if (!descriptionSlot) return;
descriptionSlot.setAttribute('slot', 'description');
}

function createMerchOffer(option, quantitySelector, variant) {
Expand Down Expand Up @@ -57,18 +54,32 @@ function createMerchOffer(option, quantitySelector, variant) {
const isHorizontal = (offerSelection) => [...offerSelection.querySelectorAll('merch-offer')].map((o) => o.text).every((t) => /^\d+.B$/.test(t));

export const initOfferSelection = (merchCard, offerSelection, quantitySelector) => {
const bodySlot = merchCard.querySelector(`div[slot="${merchCard.variant === 'mini-compare-chart' ? 'offers' : 'body-xs'}"]`);
if (!bodySlot) return;
createDynamicSlots(merchCard, bodySlot);
const merchOffers = createTag('merch-offer-select', { container: 'merch-card' });
let merchOfferSlot;
switch (merchCard.variant) {
case 'mini-compare-chart':
merchOfferSlot = merchCard.querySelector('div[slot="body-m"]');
break;
case 'twp':
merchOfferSlot = merchCard.querySelector('[slot="footer"]');
break;
default:
merchOfferSlot = merchCard.querySelector('div[slot="body-xs"]');
break;
}
if (!merchOfferSlot) return;
createDynamicSlots(merchCard, merchOfferSlot);
const merchOfferSelect = createTag('merch-offer-select', { container: 'merch-card' });
if (merchCard.classList.contains('add-stock')) {
merchOfferSelect.setAttribute('stock', '');
}
[...offerSelection.children].forEach((option) => {
merchOffers.append(createMerchOffer(option, quantitySelector, merchCard.variant));
merchOfferSelect.append(createMerchOffer(option, quantitySelector, merchCard.variant));
});
merchOffers.querySelectorAll('a[is="checkout-link"]').forEach((link) => { link.setAttribute('slot', 'cta'); });
if (isHorizontal(merchOffers)) {
merchOffers.setAttribute('variant', 'horizontal');
merchOfferSelect.querySelectorAll('a[is="checkout-link"]').forEach((link) => { link.setAttribute('slot', 'cta'); });
if (isHorizontal(merchOfferSelect)) {
merchOfferSelect.setAttribute('variant', 'horizontal');
}
merchOffers.querySelectorAll('merch-offer').forEach((offer) => {
merchOfferSelect.querySelectorAll('merch-offer').forEach((offer) => {
const links = offer.querySelectorAll('a[is="checkout-link"]');
if (links.length > 1) {
links[0].setAttribute('slot', 'secondary-cta');
Expand All @@ -77,11 +88,11 @@ export const initOfferSelection = (merchCard, offerSelection, quantitySelector)
links[0].setAttribute('slot', 'cta');
}
});
merchOffers.querySelectorAll('span[is="inline-price"]').forEach((price) => { price.setAttribute('slot', 'price'); });
merchOfferSelect.querySelectorAll('span[is="inline-price"]').forEach((price) => { price.setAttribute('slot', 'price'); });
if (quantitySelector) {
quantitySelector.append(merchOffers);
quantitySelector.append(merchOfferSelect);
} else {
bodySlot.append(merchOffers);
merchOfferSlot.append(merchOfferSelect);
}
};

Expand Down
Loading

0 comments on commit 16c64e4

Please sign in to comment.