diff --git a/blocks/gmo-program-details/gmo-program-details.css b/blocks/gmo-program-details/gmo-program-details.css index 9f43a01..5c69faa 100644 --- a/blocks/gmo-program-details/gmo-program-details.css +++ b/blocks/gmo-program-details/gmo-program-details.css @@ -31,10 +31,12 @@ body { .gmo-program-details-wrapper { margin-top: 50px; + min-height: 400px; } .gmo-program-details.block { position: relative; + min-height: 400px; & .h1 { font: normal normal bold 18px/27px Adobe Clean; } @@ -48,6 +50,8 @@ body { font: normal normal normal 14px/21px Adobe Clean; } & > .main-body-wrapper { + min-height: 300px; + position: relative; display: flex; flex-direction: column; background: #FFFFFF; diff --git a/blocks/gmo-program-details/gmo-program-details.js b/blocks/gmo-program-details/gmo-program-details.js index 23ef08a..42efbb9 100644 --- a/blocks/gmo-program-details/gmo-program-details.js +++ b/blocks/gmo-program-details/gmo-program-details.js @@ -1,16 +1,20 @@ import { decorateIcons, readBlockConfig } from '../../scripts/lib-franklin.js'; import { executeQuery } from '../../scripts/graphql.js'; -import { filterArray, getProductMapping, checkBlankString, dateFormat, statusMapping, getMappingArray, showLoadingOverlay, hideLoadingOverlay } from '../../scripts/shared-program.js'; import { getBaseConfigPath } from '../../scripts/site-config.js'; import { searchAsset } from '../../scripts/assets.js'; +import { + filterArray, getProductMapping, checkBlankString, + dateFormat, statusMapping, getMappingArray, + showLoadingOverlay, hideLoadingOverlay, div, + span, img +} from '../../scripts/shared-program.js'; + let blockConfig; +let deliverableMappings, platformMappings, taskStatusMappings; const queryVars = extractQueryVars(); const programName = queryVars.programName; const programID = queryVars.programID; -const deliverableMappings = getMappingArray('deliverableType'); -const platformMappings = getMappingArray('platforms'); -const taskStatusMappings = getMappingArray('taskStatus'); const startDateProp = 'taskPlannedStartDate'; const endDateProp = 'taskPlannedEndDate'; let viewStart, viewEnd, calendarDeliverables; @@ -19,37 +23,228 @@ let viewStart, viewEnd, calendarDeliverables; const thumbnailCache = {}; export default async function decorate(block) { + blockConfig = readBlockConfig(block); + block.innerHTML = ` `; + + const currentYear = new Date().getFullYear(); + + const backButton = div({ class: 'back-button'}, span({ class: 'icon icon-back'}), span({ class: 'back-label'}, 'Back')); + const bodyWrapper = div({ class: 'main-body-wrapper'}); + const headerWrapper = div( + { class: 'details-header-wrapper'}, + div({ class: 'campaign-img'}), + div( + { class: 'header-title'}, + div({ class: 'header-row1'}, span({ class: 'h1'}, 'Loading details..')), + div( + { class: 'header-row2 header-row3'}, + div({ class: 'header-row3 data-element'}, + span({ class: 'icon icon-calendar'}), + span({ class: 'date-tooltip'}, 'Proposed Launch Date'), + span({ class: 'campaign-date'}, 'Loading details..'), + ), + span({ class: 'driver-text'}, 'Project Owner: Loading details..'), + div( + { class: 'header-row3 data-element'}, + span({ class: 'icon icon-release-tier'}), + span({ class: 'release-tier'}, 'Release Tier: Loading details..'), + ), + div( + { class: 'header-row3 data-element'}, + span({ class: 'icon icon-productGroup'}), + span({ class: 'productGroup'}, 'Loading details..'), + ), + ), + ), + ); + + // tab wrapper + const tabWrapper = div( + { class: 'tab-wrapper'}, + div({ id: 'tab1toggle', class: 'tabBtn active', 'data-target': 'tab1'}, 'Overview'), + div({ id: 'tab2toggle', class: 'tabBtn', 'data-target': 'tab2'}, 'Deliverables'), + div({ id: 'tab3toggle', class: 'tabBtn', 'data-target': 'tab3'}, 'Calendar'), + ); + + // overview tab + const overviewTab = div( + { id: 'tab1', class: 'two-column overview tab'}, + div( + { class: 'overview-wrapper'}, + span({ class: 'h1 overview-heading'}, 'At a Glance'), + div( + { class: 'product-overview-wrapper'}, + span({ class: 'h3'}, 'Marketing Goal'), + div({ class: 'overview paragraph hide-overflow'}, ' '), + div({ class: 'button no-bg read-more'}, 'Read more'), + ), + div( + { class: 'product-value-wrapper'}, + span({ class: 'h3'}, 'Product Value'), + div({ class: 'description paragraph hide-overflow'}, ' '), + div({ class: 'button no-bg read-more'}, 'Read more'), + ), + div( + { class: 'kpis-wrapper'}, + span({ class: 'h3'}, 'KPIs to Measure Success'), + ), + div( + { class: 'kpis-wrapper market-wrapper'}, + span({ class: 'h3'}, 'Target Market Area'), + ), + div( + { class: 'use-cases-wrapper inactive'}, + span({ class: 'h3'}, 'Hero Use Cases'), + div( + { class: 'tags-wrapper'}, + div({ class: 'use-case-tag'}, 'Text to Image'), + div({ class: 'use-case-tag'}, 'Use Case 2'), + ) + + ), + div( + { id: 'deliverable-type', class: 'channel-scope-wrapper'}, + span({ class: 'h3'}, 'Deliverable Type'), + div({ class: 'tags-wrapper'}), + ), + div( + { id: 'platforms', class: 'channel-scope-wrapper'}, + span({ class: 'h3'}, 'Platforms'), + div({ class: 'tags-wrapper'}), + ), + ), + div( + { class: 'infocards-wrapper'}, + div( + { class: 'card products'}, + div({ class: 'card-heading h3'}, 'Products'), + ), + div( + { class: 'card audiences'}, + div({ class: 'card-heading h3'}, 'Audiences'), + ) + ), + ); + + // deliverables tab + const deliverablesTab = div( + { id: 'tab2', class: 'deliverables tab inactive'}, + div( + { class: 'page-heading'}, + div( + { class: 'total-assets total-assets-tooltip' }, + div({ class: 'h3'}, 'Total Approved Assets'), + span({ id: 'totalassets', class: 'description'}, 'Not Available'), + span( + { class: 'tooltiptext'}, + 'To view the assets, go to the "All Asset" search page and use Program and Campaign name facet to filter the assets.', + ), + ), + ), + div( + { class: 'table-wrapper'}, + div( + { class: 'table-header' }, + div({ class: 'header table-column column1' }, 'Deliverable Task Name'), + div({ class: 'header table-column column2' }, 'Deliverable Type'), + div({ class: 'header table-column column3' }, 'Platforms'), + div({ class: 'header table-column column4' }, 'QA Files'), + div({ class: 'header table-column column5' }, 'Final Asset'), + div({ class: 'header table-column column7' }, 'Status Update'), + div({ class: 'header table-column column8' }, 'Completion Date'), + div({ class: 'header table-column column9' }, 'Task Owner'), + ), + div({ class: 'table-content' }), + ) + ); + + // calendar tab + const calendarTab = div( + { id: 'tab3', class: 'calendar tab inactive'}, + div( + { class: 'control-wrapper' }, + div( + { class: 'inc-dec-wrapper' }, + div( + { class: 'year-switch' }, + div( + { id: 'dec-year', class: 'year-toggle' }, + img( + { class: 'left', 'data-direction': 'left', src: '/icons/chevron-right.svg'}, + ), + ), + div( + { id: 'inc-year', class: 'year-toggle' }, + img( + { class: 'right', 'data-direction': 'right', src: '/icons/chevron-right.svg'}, + ), + ) + ), + div( + { class: 'current-year', 'data-quarter': '1', 'data-year': `${currentYear}` }, + `${currentYear}`, + ), + ), + div( + { class: 'right-controls' }, + div({ class: 'today-button' }, 'Today'), + div( + { class: 'filter-dropdown-wrapper' }, + div( + { class: 'filter-dropdown-button' }, + div({ class: 'label' }, 'Selected View: Year'), + span({ class: 'icon icon-chevronDown' }), + span({ class: 'icon icon-chevronUp inactive' }), + ), + ), + ), + ), + ); + + bodyWrapper.appendChild(headerWrapper); + bodyWrapper.appendChild(tabWrapper); + bodyWrapper.appendChild(overviewTab); + bodyWrapper.appendChild(deliverablesTab); + bodyWrapper.appendChild(calendarTab); + showLoadingOverlay(bodyWrapper); + block.appendChild(backButton); + block.appendChild(bodyWrapper); + decorateIcons(block); + + // add dynamic data + addProgramStats(block); + + // enable back button + enableBackBtn(block, blockConfig); +} + +async function addProgramStats(block) { + // mappings + deliverableMappings = getMappingArray('deliverableType'); + platformMappings = getMappingArray('platforms'); + taskStatusMappings = getMappingArray('taskStatus'); + + // main program data const encodedSemi = encodeURIComponent(';'); const encodedProgram = encodeURIComponent(programName); const encodedPath = queryVars.path ? `${encodeURIComponent(queryVars.path)}` : ''; - - blockConfig = readBlockConfig(block); - // Including path in the query if present const programQueryString = `getProgramDetails${encodedSemi}programName=${encodedProgram}${encodedSemi}programID=${encodeURIComponent(programID)}` + (encodedPath ? `${encodedSemi}path=${encodedPath}` : ''); - - const deliverableQueryString = `getProgramDeliverables${encodedSemi}programName=${encodedProgram}${encodedSemi}programID=${encodeURIComponent(programID)}`; - - // Immediately render a placeholder header - block.innerHTML = ` -
- - Back -
-
-
Loading program details...
-
- `; - showLoadingOverlay(block); - - // Wait for program data to render the actual header const programData = await executeQuery(programQueryString); const program = programData.data.programList.items[0]; + const uniqueDeliverableTypes = getUniqueItems(programData.data.deliverableList.items, 'deliverableType'); + const uniquePlatforms = getUniqueItems(programData.data.deliverableList.items, 'platforms'); + const bodyWrapper = document.querySelector('.main-body-wrapper'); + + // for deliverable list + const deliverableQueryString = `getProgramDeliverables${encodedSemi}programName=${encodedProgram}${encodedSemi}programID=${encodeURIComponent(programID)}`; + // for thumbnails let imageObject = {imageUrl : '', imageAltText: '', assetCount: 0}; let totalassets = 0; - let header = block.querySelector('.placeholder-header'); + // build header + let header = block.querySelector('.details-header-wrapper'); if (!(program === undefined)) { const programHeader = buildHeader(program, queryVars).outerHTML; // Update the header with the actual data @@ -63,7 +258,6 @@ export default async function decorate(block) { } catch (error) { console.error("Failed to load campaign image:", error); } - decorateIcons(block); } else { //programName and campaignName is null header.textContent = 'Unable to retrieve program information.'; @@ -87,175 +281,55 @@ export default async function decorate(block) { } catch (error) { console.error("Failed to load campaign image:", error); } - - decorateIcons(block); + hideLoadingOverlay(bodyWrapper); enableBackBtn(block, blockConfig); + decorateIcons(block); return; } - const p0TargetMarketArea = program.p0TargetMarketArea; - const p1TargetMarketArea = program.p1TargetMarketArea; - const kpis = buildKPIList(program).outerHTML; - const targetMarketAreas = buildTargetMarketAreaList(p0TargetMarketArea,p1TargetMarketArea).outerHTML; - const audiences = buildAudienceList(program).outerHTML; - const artifactLinks = buildArtifactLinks(program).outerHTML; - - const currentYear = new Date().getFullYear(); - - // Inject the additional HTML content - block.querySelector('.main-body-wrapper').innerHTML += ` -
-
Overview
-
Deliverables
-
Calendar
-
-
-
- At a Glance -
- Marketing Goal -
${checkBlankString(program.marketingGoal.plaintext)}
-
Read more
-
-
- Product Value -
${checkBlankString(program.productValue.plaintext)}
-
Read more
-
-
- KPIs to Measure Success - ${kpis} -
-
- Target Market Area - ${targetMarketAreas} -
-
- Hero Use Cases -
-
Text to Image
-
Use Case 2
-
-
-
- Main Message - - A major genAI release of the Photoshop beta app that delivers new and enhanced generative AI capabilities. - -
- -
- Deliverable Type -
-
-
- -
- Platforms -
-
-
- ${artifactLinks} -
-
-
-
Products
-
-
-
Audiences
- ${audiences} -
-
-
-
-
- ${artifactLinks} -
-
Total Approved Assets
- ${totalassets} - To view the assets, go to the "All Asset" search page and use Program and Campaign name facet to filter the assets -
-
-
-
-
Deliverable Task Name
-
Deliverable Type
-
Platforms
-
QA Files
-
Final Asset
-
Status Update
-
Completion Date
-
Task Owner
-
-
-
-
-
-
-
-
-
-
- -
-
- -
-
-
${currentYear}
-
-
-
Today
-
-
-
Selected View: Year
- - -
-
-
-
-
- - `; - - // Wait for deliverables data - const deliverables = executeQuery(deliverableQueryString); - - const uniqueDeliverableTypes = getUniqueItems(programData.data.deliverableList.items, 'deliverableType'); - const uniquePlatforms = getUniqueItems(programData.data.deliverableList.items, 'platforms'); - + // start populating needed statistics and areas + // add major UI elements + const p0TargetMarketArea = program.p0TargetMarketArea ? program.p0TargetMarketArea : null; + const p1TargetMarketArea = program.p1TargetMarketArea ? program.p1TargetMarketArea : null; + const kpis = buildKPIList(program); + const targetMarketAreas = buildTargetMarketAreaList(p0TargetMarketArea,p1TargetMarketArea); + const audiences = buildAudienceList(program); + const artifactLinks = buildArtifactLinks(program); + + document.querySelector('.kpis-wrapper').appendChild(kpis); + document.querySelector('.market-wrapper').appendChild(targetMarketAreas); + document.querySelector('.card.audiences').appendChild(audiences); + document.querySelector('.overview-wrapper').appendChild(artifactLinks); + + // add text + const marketingGoal = checkBlankString(program.marketingGoal.plaintext); + document.querySelector('.product-overview-wrapper > .paragraph').textContent = marketingGoal; + const productValue = checkBlankString(program.productValue.plaintext); + document.querySelector('.product-value-wrapper > .paragraph').textContent = productValue; + + // additional dom updates buildProductCard(program); buildFieldScopes('deliverable-type', uniqueDeliverableTypes, block); buildFieldScopes('platforms', uniquePlatforms, block); + // deliverables tab + const deliverables = executeQuery(deliverableQueryString); + document.querySelector('.page-heading').appendChild(artifactLinks); + document.querySelector('.total-assets > .description').textContent = totalassets; + const table = buildTable(await deliverables).then(async (rows) => { return rows; }); - - // Batch Dom Updates const tableRoot = block.querySelector('.table-content'); const fragment = document.createDocumentFragment(); fragment.appendChild(await table); tableRoot.appendChild(fragment); - buildStatus(program.status); - // Optimize Event Listeners: Added debouncing to event listeners to prevent performance issues. - const debounce = (func, delay) => { - let timeout; - return (...args) => { - clearTimeout(timeout); - timeout = setTimeout(() => func(...args), delay); - }; - }; - - block.querySelector('.tab-wrapper').addEventListener('click', debounce((event) => { - switchTab(event.target); - }, 300)); + // calendar tab + buildCalendar(await deliverables, block, "year", await deliverableMappings); - enableBackBtn(block, blockConfig); - + // enable 'read more' buttons block.querySelectorAll('.read-more').forEach((button) => { button.addEventListener('click', (event) => { const readMore = event.target; @@ -266,9 +340,16 @@ export default async function decorate(block) { }); }); + // enable tab switching + block.querySelector('.tab-wrapper').addEventListener('click', (event) => { + switchTab(event.target); + }); + + // decorate any new icons decorateIcons(block); - hideLoadingOverlay(block); - buildCalendar(await deliverables, block, "year", await deliverableMappings); + + // remove loading spinner + hideLoadingOverlay(bodyWrapper); } function enableBackBtn(block, blockConfig) { @@ -1346,7 +1427,7 @@ function scrollOnInit(element, scrollPct) { } function getHorizontalOverflow(element) { - return element.scrollWidth - element.clientWidth; + return ((element.scrollWidth - element.clientWidth) + 4); } function resizeGroups() { diff --git a/scripts/shared-program.js b/scripts/shared-program.js index 0f57f35..f52b2e3 100644 --- a/scripts/shared-program.js +++ b/scripts/shared-program.js @@ -93,4 +93,67 @@ export function hideLoadingOverlay(targetDiv) { if (overlay) { targetDiv.removeChild(overlay); } -} \ No newline at end of file +} + +/** + * Example Usage: + * + * domEl('main', + * div({ class: 'card' }, + * a({ href: item.path }, + * div({ class: 'card-thumb' }, + * createOptimizedPicture(item.image, item.title, 'lazy', [{ width: '800' }]), + * ), + * div({ class: 'card-caption' }, + * h3(item.title), + * p({ class: 'card-description' }, item.description), + * p({ class: 'button-container' }, + * a({ href: item.path, 'aria-label': 'Read More', class: 'button primary' }, 'Read More'), + * ), + * ), + * ), + * ) + */ + +/** + * Helper for more concisely generating DOM Elements with attributes and children + * @param {string} tag HTML tag of the desired element + * @param {[Object?, ...Element]} items: First item can optionally be an object of attributes, + * everything else is a child element + * @returns {Element} The constructred DOM Element + */ +export function domEl(tag, ...items) { + const element = document.createElement(tag); + + if (!items || items.length === 0) return element; + + if (!(items[0] instanceof Element || items[0] instanceof HTMLElement) && typeof items[0] === 'object') { + const [attributes, ...rest] = items; + items = rest; + + Object.entries(attributes).forEach(([key, value]) => { + if (!key.startsWith('on')) { + element.setAttribute(key, Array.isArray(value) ? value.join(' ') : value); + } else { + element.addEventListener(key.substring(2).toLowerCase(), value); + } + }); + } + + items.forEach((item) => { + item = item instanceof Element || item instanceof HTMLElement + ? item + : document.createTextNode(item); + element.appendChild(item); + }); + + return element; + } + +/* + More short hand functions can be added for very common DOM elements below. + domEl function from above can be used for one off DOM element occurrences. +*/ +export function div(...items) { return domEl('div', ...items); } +export function span(...items) { return domEl('span', ...items); } +export function img(...items) { return domEl('img', ...items); } \ No newline at end of file