diff --git a/edsdme/scripts/personalization.js b/edsdme/scripts/personalization.js new file mode 100644 index 0000000..3f6669d --- /dev/null +++ b/edsdme/scripts/personalization.js @@ -0,0 +1,89 @@ +import { + getCurrentProgramType, + getPartnerDataCookieValue, + isMember, + partnerIsSignedIn, + getPartnerDataCookieObject, + signedInNonMember, + isReseller, + getNodesByXPath +} + from './utils.js'; + +const PAGE_PERSONALIZATION_PLACEHOLDERS = { firstName: '//*[contains(text(), "$firstName")]' }; + +const LEVEL_CONDITION = 'partner-level'; +const PERSONALIZATION_MARKER = 'partner-personalization'; +const PROGRAM = getCurrentProgramType(); +const PARTNER_LEVEL = getPartnerDataCookieValue(PROGRAM, 'level'); +const COOKIE_OBJECT = getPartnerDataCookieObject(PROGRAM); + +const PERSONALIZATION_CONDITIONS = { + 'partner-not-member': signedInNonMember(), + 'partner-not-signed-in': !partnerIsSignedIn(), + 'partner-all-levels': isMember(), + 'partner-reseller': isReseller (PARTNER_LEVEL), + 'partner-level': (level) => PARTNER_LEVEL === level, +}; + + +function personalizePlaceholders(placeholders, context = document) { + Object.entries(placeholders).forEach(([key, value]) => { + const placeholderValue = COOKIE_OBJECT[key]; + getNodesByXPath(value, context).forEach((el) => { + if (!placeholderValue) { + el.remove(); + return; + } + el.textContent = el.textContent.replace(`$${key}`, placeholderValue); + }); + }); +} + +function shouldHide(conditions) { + return conditions.every((condition) => { + const conditionLevel = condition.startsWith(LEVEL_CONDITION) ? condition.split('-').pop() : ''; + return conditionLevel + ? !PERSONALIZATION_CONDITIONS[LEVEL_CONDITION](conditionLevel) : !PERSONALIZATION_CONDITIONS[condition]; + }); +} + +function hideElement(element, conditions) { + if (!element || !conditions?.length) return; + shouldHide(conditions) && element.classList.add('personalization-hide'); +} + +function hideSections(page) { + const sections = Array.from(page.getElementsByClassName('section-metadata')); + sections.forEach((section) => { + let hide = false; + Array.from(section.children).forEach((child) => { + const col1 = child.firstElementChild; + let col2 = child.lastElementChild; + if (col1?.textContent !== 'style' || !col2?.textContent.includes(PERSONALIZATION_MARKER)) return; + const conditions = col2?.textContent?.split(',').map((text) => text.trim()); + hide = shouldHide(conditions); + }); + if (!hide) return; + const parent = section.parentElement; + Array.from(parent.children).forEach((el) => { + el.classList.add('personalization-hide'); + }); + }); +} + + +function personalizePage(page) { + const blocks = Array.from(page.getElementsByClassName(PERSONALIZATION_MARKER)); + blocks.forEach((el) => { + const conditions = Object.values(el.classList); + hideElement(el, conditions); + }); + hideSections(page); +} + +export function applyPagePersonalization() { + const main = document.querySelector('main') ?? document; + personalizePlaceholders(PAGE_PERSONALIZATION_PLACEHOLDERS, main); + personalizePage(main); +} diff --git a/edsdme/scripts/scripts.js b/edsdme/scripts/scripts.js index 48014f8..68a99e9 100644 --- a/edsdme/scripts/scripts.js +++ b/edsdme/scripts/scripts.js @@ -1,3 +1,4 @@ +import { applyPagePersonalization } from './personalization.js'; import { setLibs, redirectLoggedinPartner, updateIMSConfig, preloadResources, getRenewBanner, updateNavigation, updateFooter } from './utils.js'; // Add project-wide style path here. @@ -77,6 +78,7 @@ function setUpPage() { } (async function loadPage() { + applyPagePersonalization(); setUpPage(); redirectLoggedinPartner(); updateIMSConfig(); diff --git a/edsdme/scripts/utils.js b/edsdme/scripts/utils.js index 36a4cbf..b49a142 100644 --- a/edsdme/scripts/utils.js +++ b/edsdme/scripts/utils.js @@ -13,6 +13,17 @@ /** * The decision engine for where to get Milo's libs from. */ + +export const LEVELS = { + REGISTERED: 'registered', + CERTIFIED: 'certified', + GOLD: 'gold', + PLATINUM: 'platinum', + DISTRIBBUTOR: 'distributor', +}; + +export const RESSELER_LEVELS = [LEVELS.REGISTERED, LEVELS.CERTIFIED, LEVELS.GOLD, LEVELS.PLATINUM]; + export const [setLibs, getLibs] = (() => { let libs; return [ @@ -115,6 +126,18 @@ export function isMember() { return status === 'MEMBER'; } +export function partnerIsSignedIn() { + return getCookieValue('partner_data'); +} + +export function signedInNonMember() { + return partnerIsSignedIn() && !isMember(); +} + +export function isReseller (level) { + return RESSELER_LEVELS.includes(level?.toLowerCase()); +} + export function getMetadataContent(name) { return document.querySelector(`meta[name="${name}"]`)?.content; } @@ -373,3 +396,14 @@ export function updateFooter(locales) { const footerLoggedIn = getMetadataContent('footer-loggedin-source'); footerMeta.content = footerLoggedIn ?? `${prefix}/edsdme/partners-shared/loggedin-footer`; } + +export function getNodesByXPath(query, context = document) { + const nodes = []; + const xpathResult = document.evaluate(query, context); + let current = xpathResult?.iterateNext(); + while (current) { + nodes.push(current); + current = xpathResult.iterateNext(); + } + return nodes; +} diff --git a/edsdme/styles/styles.css b/edsdme/styles/styles.css index e80440f..3d0ff2f 100644 --- a/edsdme/styles/styles.css +++ b/edsdme/styles/styles.css @@ -36,3 +36,7 @@ font-size: 16px; font-weight: bold; } + +.personalization-hide { + display: none; +} diff --git a/test/scripts/mocks/personalization.html b/test/scripts/mocks/personalization.html new file mode 100644 index 0000000..a1be0d6 --- /dev/null +++ b/test/scripts/mocks/personalization.html @@ -0,0 +1,114 @@ +
+
+
+
+
+ + + + + + +
+
+
+
+

+ + + + + + +

+

Heading XL Marquee standard medium left

+

Welcome $firstName

+

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.

+

Lorem ipsum Learn more Text link

+
+
+ + + + + + +
+
+
+
+
+
+
+
+

Partner NOT SIGNED IN

+

Featuring over 600,000 hand-picked stock photos and graphics, curated from the world’s leading photographers, illustrators, and agencies. Our Premium collection is perfect for organizations looking for authentic, high-quality commercial content, and easy licensing plans.

+

Explore the premium collection

+

Join Now

+
+
+
+
+
+
+

Partner NON MEMBER

+

Featuring over 600,000 hand-picked stock photos and graphics, curated from the world’s leading photographers, illustrators, and agencies. Our Premium collection is perfect for organizations looking for authentic, high-quality commercial content, and easy licensing plans.

+

Explore the premium collection

+

Join Now

+
+
+
+
+
+
+

MEMBER

+

Featuring over 600,000 hand-picked stock photos and graphics, curated from the world’s leading photographers, illustrators, and agencies. Our Premium collection is perfect for organizations looking for authentic, high-quality commercial content, and easy licensing plans.

+
+
+
+
+
+
+
+
+

Partner GOLD

+

Featuring over 600,000 hand-picked stock photos and graphics, curated from the world’s leading photographers, illustrators, and agencies. Our Premium collection is perfect for organizations looking for authentic, high-quality commercial content, and easy licensing plans.

+
+
+
+
+
+
+
+
+

Partner Platinum

+

Featuring over 600,000 hand-picked stock photos and graphics, curated from the world’s leading photographers, illustrators, and agencies. Our Premium collection is perfect for organizations looking for authentic, high-quality commercial content, and easy licensing plans.

+

Explore the premium collection

+
+
+
+ +
+
+
+
+
+

Partner Platinum section

+

Featuring over 600,000 hand-picked stock photos and graphics, curated from the world’s leading photographers, illustrators, and agencies. Our Premium collection is perfect for organizations looking for authentic, high-quality commercial content, and easy licensing plans.

+

Explore the premium collection

+
+
+
+ +
+
diff --git a/test/scripts/personalization.jest.js b/test/scripts/personalization.jest.js new file mode 100644 index 0000000..2319a18 --- /dev/null +++ b/test/scripts/personalization.jest.js @@ -0,0 +1,158 @@ +/** + * @jest-environment jsdom + */ +import path from 'path'; +import fs from 'fs'; + +const PERSONALIZATION_HIDE_CLASS = 'personalization-hide'; + +function importModules() { + const utils = require('../../edsdme/scripts/utils.js'); + const placeholderElement = document.querySelector('#welcome-firstname'); + jest.spyOn(utils, 'getNodesByXPath').mockImplementation(() => [placeholderElement]); + const { applyPagePersonalization } = require('../../edsdme/scripts/personalization.js'); + + return applyPagePersonalization; +} + +describe('Test utils.js', () => { + beforeEach(() => { + jest.clearAllMocks(); + window = Object.create(window); + Object.defineProperty(window, 'location', { + value: { + pathname:'/channelpartners', + }, + writable: true + }); + document.body.innerHTML = fs.readFileSync( + path.resolve(__dirname, './mocks/personalization.html'), + 'utf8' + ); + document.cookie = 'partner_data='; + }); + afterEach(() => { + document.body.innerHTML = ''; + }); + it('Populate placeholder if user is a member', () => { + jest.isolateModules(() => { + const cookieObject = { + CPP: { + status: 'MEMBER', + firstName: 'Test user' + } + }; + document.cookie = `partner_data=${JSON.stringify(cookieObject)}`; + const applyPagePersonalization = importModules(); + applyPagePersonalization(); + const placeholderElementAfter = document.querySelector('#welcome-firstname'); + expect(placeholderElementAfter.textContent.includes(cookieObject.CPP.firstName)).toBe(true); + }); + }); + it('Remove placeholder if user is not a member', () => { + jest.isolateModules(() => { + const cookieObject = { + SPP: { + status: 'MEMBER', + firstName: 'Test use' + } + }; + document.cookie = `partner_data=${JSON.stringify(cookieObject)}`; + const applyPagePersonalization = importModules(); + applyPagePersonalization(); + const placeholderElementAfter = document.querySelector('#welcome-firstname'); + expect(placeholderElementAfter).toBe(null); + }); + }); + it('Show partner-not-signed-in block', () => { + jest.isolateModules(() => { + const applyPagePersonalization = importModules(); + applyPagePersonalization(); + const notSignedInBlock = document.querySelector('.partner-not-signed-in'); + expect(notSignedInBlock.classList.contains(PERSONALIZATION_HIDE_CLASS)).toBe(false); + }); + }); + + it('Show partner-not-member block', () => { + jest.isolateModules(() => { + const cookieObject = { + SPP: { + status: 'MEMBER', + firstName: 'Test use' + } + }; + document.cookie = `partner_data=${JSON.stringify(cookieObject)}`; + const applyPagePersonalization = importModules(); + applyPagePersonalization(); + const notMemberBlock = document.querySelector('.partner-not-member'); + expect(notMemberBlock.classList.contains(PERSONALIZATION_HIDE_CLASS)).toBe(false); + }); + }); + it('Show partner-all-levels block', () => { + jest.isolateModules(() => { + const cookieObject = { + CPP: { + status: 'MEMBER', + firstName: 'Test use', + level: 'Gold' + } + }; + document.cookie = `partner_data=${JSON.stringify(cookieObject)}`; + const applyPagePersonalization = importModules(); + applyPagePersonalization(); + const allLevelsBlock = document.querySelector('.partner-all-levels'); + expect(allLevelsBlock.classList.contains(PERSONALIZATION_HIDE_CLASS)).toBe(false); + }); + }); + it('Show partner-level-gold block', () => { + jest.isolateModules(() => { + const cookieObject = { + CPP: { + status: 'MEMBER', + firstName: 'Test use', + level: 'Gold' + } + }; + document.cookie = `partner_data=${JSON.stringify(cookieObject)}`; + const applyPagePersonalization = importModules(); + applyPagePersonalization(); + const goldBlock = document.querySelector('.partner-level-gold'); + expect(goldBlock.classList.contains(PERSONALIZATION_HIDE_CLASS)).toBe(false); + }); + }); + it('Show partner-level-platinum but don\'t show partner-level-gold block', () => { + jest.isolateModules(() => { + const cookieObject = { + CPP: { + status: 'MEMBER', + firstName: 'Test use', + level: 'Platinum' + } + }; + document.cookie = `partner_data=${JSON.stringify(cookieObject)}`; + const applyPagePersonalization = importModules(); + applyPagePersonalization(); + const goldBlock = document.querySelector('.partner-level-gold'); + const platinumBlock = document.querySelector('.partner-level-platinum'); + expect(platinumBlock.classList.contains(PERSONALIZATION_HIDE_CLASS)).toBe(false); + expect(goldBlock.classList.contains(PERSONALIZATION_HIDE_CLASS)).toBe(true); + }); + }); + it('Show partner-level-platinum section', () => { + jest.isolateModules(() => { + const cookieObject = { + CPP: { + status: 'MEMBER', + firstName: 'Test use', + level: 'Platinum' + } + }; + document.cookie = `partner_data=${JSON.stringify(cookieObject)}`; + const applyPagePersonalization = importModules(); + applyPagePersonalization(); + const platinumBlock = document.querySelector('#platinum-section'); + expect(platinumBlock.classList.contains(PERSONALIZATION_HIDE_CLASS)).toBe(false); + }); + }); +}); +