forked from adobe/aem-boilerplate
-
Notifications
You must be signed in to change notification settings - Fork 36
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
Add recommendations block with ACDL #11
Merged
Merged
Changes from 20 commits
Commits
Show all changes
25 commits
Select commit
Hold shift + click to select a range
bc902f8
Add recommendations block with ACDL2
herzog31 9f0553b
Fix array issue for product views
herzog31 c0223e8
Fix enrichment position
herzog31 1cf384c
Update recommendations context
herzog31 abd9fdc
Add schema validation for events
herzog31 b5f507a
Merge branch 'main' of github.com:hlxsites/aem-boilerplate-commerce i…
herzog31 769aa8c
Merge branch 'main' of github.com:hlxsites/aem-boilerplate-commerce i…
herzog31 c101adc
Merge branch 'main' of github.com:hlxsites/aem-boilerplate-commerce i…
herzog31 ae07731
Fix ACDL2 and product view history bugs
herzog31 774720f
Merge branch 'main' of github.com:hlxsites/aem-boilerplate-commerce i…
herzog31 f059e56
Fix CLS
herzog31 a2ca757
Merge branch 'main' of github.com:hlxsites/aem-boilerplate-commerce i…
herzog31 c6f2fb1
Use base design system
herzog31 671c4b4
Merge branch 'main' of github.com:hlxsites/aem-boilerplate-commerce i…
herzog31 a52c814
Update styles
herzog31 ec1f23b
Replace ACDL2 with mainline ACDL
herzog31 ac5c938
Add viewHistory and cartSkus
herzog31 d0554f4
Add storefront events to recommendations block
herzog31 9b97591
Optimize recommendations context
herzog31 5b50032
Merge branch 'main' of github.com:hlxsites/aem-boilerplate-commerce i…
herzog31 b47e843
Add PLP widget version that uses ACDL
herzog31 826d2ca
Add categoryContext to PLP
herzog31 3c33ebe
Remove optional category url path from PLP
herzog31 793494f
Add categoryIds filter
herzog31 3686f01
Update autocomplete widget
herzog31 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,5 +1,6 @@ | ||
helix-importer-ui | ||
scripts/preact.js | ||
scripts/htm.js | ||
scripts/acdl | ||
tools/picker | ||
scripts/widgets | ||
scripts/widgets |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
80 changes: 80 additions & 0 deletions
80
blocks/product-recommendations/product-recommendations.css
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,80 @@ | ||
main .section>div.product-recommendations-wrapper { | ||
max-width: 100%; | ||
padding: 0; | ||
text-align: left; | ||
margin: 0 0 5rem; | ||
} | ||
|
||
.product-recommendations { | ||
overflow: hidden; | ||
min-height: 512px; | ||
} | ||
|
||
.product-recommendations .scrollable { | ||
overflow-x: scroll; | ||
scroll-snap-type: x mandatory; | ||
padding-bottom: 1rem; | ||
} | ||
|
||
.product-recommendations .product-grid { | ||
display: inline-flex; | ||
flex-wrap: nowrap; | ||
gap: 2rem; | ||
margin: 0; | ||
} | ||
|
||
.product-recommendations .product-grid-item img { | ||
width: 100%; | ||
} | ||
|
||
.product-recommendations .product-grid .product-grid-item a { | ||
text-decoration: none; | ||
} | ||
|
||
.product-recommendations .product-grid .product-grid-item a:hover, | ||
.product-recommendations .product-grid .product-grid-item a:focus { | ||
text-decoration: underline; | ||
} | ||
|
||
.product-recommendations .product-grid .product-grid-item span { | ||
overflow: hidden; | ||
box-sizing: border-box; | ||
margin: 0; | ||
padding: .5rem 1rem 0 0; | ||
display: inline-block; | ||
} | ||
|
||
.product-recommendations .product-grid picture { | ||
background: none; | ||
display: block; | ||
width: 300px; | ||
aspect-ratio: 1 / 1.25; | ||
} | ||
|
||
.product-recommendations .product-grid img { | ||
display: inline-block; | ||
vertical-align: middle; | ||
width: 100%; | ||
object-fit: contain; | ||
background: none; | ||
} | ||
|
||
.product-recommendations .product-grid .placeholder { | ||
background-color: var(--color-neutral-500); | ||
scroll-snap-align: start; | ||
} | ||
|
||
.product-recommendations .product-grid .placeholder img { | ||
display: none; | ||
} | ||
|
||
.product-recommendations .product-grid-item { | ||
margin: 0; | ||
scroll-snap-align: start; | ||
} | ||
|
||
@media (width >= 900px) { | ||
.product-recommendations { | ||
min-height: 534px; | ||
} | ||
} |
214 changes: 214 additions & 0 deletions
214
blocks/product-recommendations/product-recommendations.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,214 @@ | ||
/* eslint-disable no-underscore-dangle */ | ||
import { performCatalogServiceQuery } from '../../scripts/commerce.js'; | ||
import { getConfigValue } from '../../scripts/configs.js'; | ||
|
||
const recommendationsQuery = `query GetRecommendations( | ||
$pageType: PageType! | ||
$category: String | ||
$currentSku: String | ||
$cartSkus: [String] | ||
$userPurchaseHistory: [PurchaseHistory] | ||
$userViewHistory: [ViewHistory] | ||
) { | ||
recommendations( | ||
cartSkus: $cartSkus | ||
category: $category | ||
currentSku: $currentSku | ||
pageType: $pageType | ||
userPurchaseHistory: $userPurchaseHistory | ||
userViewHistory: $userViewHistory | ||
) { | ||
results { | ||
displayOrder | ||
pageType | ||
productsView { | ||
name | ||
sku | ||
url | ||
images { | ||
url | ||
} | ||
externalId | ||
__typename | ||
} | ||
storefrontLabel | ||
totalProducts | ||
typeId | ||
unitId | ||
unitName | ||
} | ||
totalResults | ||
} | ||
}`; | ||
|
||
let recommendationsPromise; | ||
|
||
function renderPlaceholder(block) { | ||
block.innerHTML = `<h2></h2> | ||
<div class="scrollable"> | ||
<div class="product-grid"> | ||
${[...Array(5)].map(() => ` | ||
<div class="placeholder"> | ||
<picture><img width="300" height="375" src="" /></picture> | ||
</div> | ||
`).join('')} | ||
</div> | ||
</div>`; | ||
} | ||
|
||
function renderItem(unitId, product) { | ||
const urlKey = product.url.split('/').pop().replace('.html', ''); | ||
const image = product.images[0]?.url; | ||
|
||
const clickHandler = () => { | ||
window.adobeDataLayer.push((dl) => { | ||
dl.push({ event: 'recs-item-click', eventInfo: { ...dl.getState(), unitId, productId: parseInt(product.externalId, 10) || 0 } }); | ||
}); | ||
}; | ||
|
||
const item = document.createRange().createContextualFragment(`<div class="product-grid-item"> | ||
<a href="/products/${urlKey}/${product.sku.toLowerCase()}"> | ||
<picture> | ||
<source type="image/webp" srcset="${image}?width=300&format=webply&optimize=medium" /> | ||
<img loading="lazy" alt="${product.name}" width="300" height="375" src="${image}?width=300&format=jpg&optimize=medium" /> | ||
</picture> | ||
<span>${product.name}</span> | ||
</a> | ||
</div>`); | ||
item.querySelector('a').addEventListener('click', clickHandler); | ||
|
||
return item; | ||
} | ||
|
||
function renderItems(block, recommendations) { | ||
// Render only first recommendation | ||
const [recommendation] = recommendations.results; | ||
if (!recommendation) { | ||
// Hide block content if no recommendations are available | ||
block.textContent = ''; | ||
return; | ||
} | ||
|
||
window.adobeDataLayer.push((dl) => { | ||
dl.push({ event: 'recs-unit-impression-render', eventInfo: { ...dl.getState(), unitId: recommendation.unitId } }); | ||
}); | ||
|
||
// Title | ||
block.querySelector('h2').textContent = recommendation.storefrontLabel; | ||
|
||
// Grid | ||
const grid = block.querySelector('.product-grid'); | ||
grid.innerHTML = ''; | ||
const { productsView } = recommendation; | ||
productsView.forEach((product) => { | ||
grid.appendChild(renderItem(recommendation.unitId, product)); | ||
}); | ||
|
||
const inViewObserver = new IntersectionObserver((entries) => { | ||
entries.forEach((entry) => { | ||
if (entry.isIntersecting) { | ||
window.adobeDataLayer.push((dl) => { | ||
dl.push({ event: 'recs-unit-view', eventInfo: { ...dl.getState(), unitId: recommendation.unitId } }); | ||
}); | ||
inViewObserver.disconnect(); | ||
} | ||
}); | ||
}); | ||
inViewObserver.observe(block); | ||
} | ||
|
||
const mapUnit = (unit) => ({ | ||
...unit, | ||
unitType: 'primary', | ||
searchTime: 0, | ||
primaryProducts: unit.totalProducts, | ||
backupProducts: 0, | ||
products: unit.productsView.map((product, index) => ({ | ||
...product, | ||
rank: index, | ||
score: 0, | ||
productId: parseInt(product.externalId, 10) || 0, | ||
type: '?', | ||
queryType: product.__typename, | ||
})), | ||
}); | ||
|
||
async function loadRecommendation(block, context) { | ||
// Only proceed if all required data is available | ||
if (!context.pageType | ||
|| (context.pageType === 'Product' && !context.currentSku) | ||
|| (context.pageType === 'Category' && !context.category) | ||
|| (context.pageType === 'Cart' && !context.cartSkus)) { | ||
return; | ||
} | ||
|
||
if (recommendationsPromise) { | ||
return; | ||
} | ||
|
||
const storeViewCode = await getConfigValue('commerce-store-view-code'); | ||
// Get product view history | ||
try { | ||
const viewHistory = window.localStorage.getItem(`${storeViewCode}:productViewHistory`) || '[]'; | ||
context.userViewHistory = JSON.parse(viewHistory); | ||
} catch (e) { | ||
window.localStorage.removeItem('productViewHistory'); | ||
console.error('Error parsing product view history', e); | ||
} | ||
|
||
// Get purchase history | ||
try { | ||
const purchaseHistory = window.localStorage.getItem(`${storeViewCode}:purchaseHistory`) || '[]'; | ||
context.userPurchaseHistory = JSON.parse(purchaseHistory); | ||
} catch (e) { | ||
window.localStorage.removeItem('purchaseHistory'); | ||
console.error('Error parsing purchase history', e); | ||
} | ||
|
||
window.adobeDataLayer.push((dl) => { | ||
dl.push({ event: 'recs-api-request-sent', eventInfo: { ...dl.getState() } }); | ||
}); | ||
|
||
recommendationsPromise = performCatalogServiceQuery(recommendationsQuery, context); | ||
const { recommendations } = await recommendationsPromise; | ||
|
||
window.adobeDataLayer.push((dl) => { | ||
dl.push({ recommendationsContext: { units: recommendations.results.map(mapUnit) } }); | ||
dl.push({ event: 'recs-api-response-received', eventInfo: { ...dl.getState() } }); | ||
}); | ||
|
||
renderItems(block, recommendations); | ||
} | ||
|
||
export default async function decorate(block) { | ||
renderPlaceholder(block); | ||
|
||
const context = {}; | ||
|
||
function handleProductChanges({ productContext }) { | ||
context.currentSku = productContext.sku; | ||
loadRecommendation(block, context); | ||
} | ||
|
||
function handleCategoryChanges({ categoryContext }) { | ||
context.category = categoryContext.name; | ||
loadRecommendation(block, context); | ||
} | ||
|
||
function handlePageTypeChanges({ pageContext }) { | ||
context.pageType = pageContext.pageType; | ||
loadRecommendation(block, context); | ||
} | ||
|
||
function handleCartChanges({ shoppingCartContext }) { | ||
context.cartSkus = shoppingCartContext.items.map(({ product }) => product.sku); | ||
loadRecommendation(block, context); | ||
} | ||
|
||
window.adobeDataLayer.push((dl) => { | ||
dl.addEventListener('adobeDataLayer:change', handlePageTypeChanges, { path: 'pageContext' }); | ||
dl.addEventListener('adobeDataLayer:change', handleProductChanges, { path: 'productContext' }); | ||
dl.addEventListener('adobeDataLayer:change', handleCategoryChanges, { path: 'categoryContext' }); | ||
dl.addEventListener('adobeDataLayer:change', handleCartChanges, { path: 'shoppingCartContext' }); | ||
}); | ||
} |
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
Large diffs are not rendered by default.
Oops, something went wrong.
Large diffs are not rendered by default.
Oops, something went wrong.
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If you already know what "updated" means, cool, but maybe you could add a specific version or ticket here.