Last active
November 12, 2020 15:43
-
-
Save JWizerd/c44065281d88b9270e95ca1913988667 to your computer and use it in GitHub Desktop.
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
/** | |
* @source https://help.shopify.com/en/api/storefront-api/reference | |
*/ | |
/** | |
* the model is responsible for the data. This means API interactions, localStorage interaction etc. | |
* there are some improvements that could be made to the mode below | |
* 1. we could inject a Driver object to set the data layer that a model will interact with | |
* 2. we could abstract the graphql interaction into it's own query builder class | |
*/ | |
class Catalog { | |
constructor() { | |
this.querySettings = {}; | |
this.permanentDomain = 'https://example.myshopify.com'; | |
this.collectionCursor = ''; | |
} | |
getTags(tags) { | |
return tags.map(tag => `tag:'${tag}'`) | |
} | |
getCursor(cursor) { | |
return `${cursor}:"${this.collectionCursor}"` | |
} | |
buildQueryRoot() { | |
let queryRoot = ''; | |
queryRoot += `query:"`; | |
if (this.querySettings.tags) { | |
queryRoot += `${this.getTags(this.querySettings.tags).join(' ')}"`; | |
} | |
if (this.querySettings.cursor !== 'before') { | |
queryRoot += ` first:${this.querySettings.pageSize}, `; | |
} else { | |
queryRoot += ` last:${this.querySettings.pageSize}, `; | |
} | |
if (this.querySettings.cursor) { | |
queryRoot += this.getCursor(this.querySettings.cursor); | |
} | |
return queryRoot; | |
} | |
getQuery() { | |
return `query { | |
products(${this.buildQueryRoot()}) { | |
pageInfo { | |
hasNextPage | |
hasPreviousPage | |
} | |
edges { | |
cursor | |
node { | |
title | |
handle | |
productType | |
onlineStoreUrl | |
images(first:1){ | |
edges{ | |
node{ | |
originalSrc | |
} | |
} | |
} | |
} | |
} | |
} | |
}`; | |
} | |
getAjaxSettings(query) { | |
return { | |
'async': true, | |
'crossDomain': true, | |
'url': `${this.permanentDomain}/api/graphql`, | |
'method': 'POST', | |
'headers': { | |
'X-Shopify-Storefront-Access-Token': '{{settings.storefront_access_token}}', | |
'Content-Type': 'application/graphql', | |
}, | |
'data': query | |
} | |
} | |
handleResponse(response) { | |
try { | |
if (response.data.products.edges.length > 0) { | |
this.collectionCursor = response.data.products.edges[response.data.products.edges.length - 1].cursor; | |
} | |
return response.data.products; | |
} catch (error) { | |
console.error(`Failed to retrieve data from catalog query for reason: ${error}`) | |
return [] | |
} | |
} | |
async query(settings) { | |
this.querySettings = settings; | |
const query = this.getQuery(); | |
return $.ajax(this.getAjaxSettings(query)) | |
.done((response) => { | |
return this.handleResponse(response); | |
}) | |
} | |
} | |
class Filterer { | |
constructor() { | |
this.filterSelector = null; | |
} | |
get tags() { | |
return Array.from(this.filterSelector) | |
.filter(filter => filter.checked) | |
.map(filter => filter.value); | |
} | |
get filters() { | |
return { | |
pageSize: this.pageSize, | |
tags: [...this.tags, 'Catalog'], | |
cursor: this.cursor | |
} | |
} | |
check(tag) { | |
Array.from(this.filterSelector).find(filter => filter.value === tag).checked = true | |
} | |
autoFilter() { | |
const params = getUrlVars(); | |
const plush = params.hasOwnProperty('plush') ? params.plush : ''; | |
const colorgroup = params.hasOwnProperty('colorgroup') ? params.colorgroup : ''; | |
if (plush.length > 0) { | |
this.check(plush); | |
} | |
if (colorgroup.length > 0) { | |
this.check(colorgroup); | |
} | |
} | |
} | |
/** | |
* The controller is the proxy between the view and the model | |
* it takes a container to render the catalog items as a dependency | |
* allowing you to render items any where you want | |
*/ | |
class CatalogController { | |
constructor(container, model, paginator, filterer) { | |
this.container = container; | |
this.paginator = paginator; | |
this.model = model; | |
this.filterer = filterer | |
} | |
/** | |
* register all events and map them to controller actions for a feature | |
*/ | |
registerEvents() { | |
$(document).ready(() => { | |
this.beforeRender().then(() => { | |
this.container.on('click', 'a.init-roomvo', (e) => runRoomvo(e)); | |
this.paginator.nextButton.on('click', () => this.nextPage()); | |
this.paginator.prevButton.on('click', () => this.prevPage()); | |
this.filterer.filterSelector.on('change', () => this.show()); | |
this.show(); | |
}); | |
}); | |
} | |
async beforeRender() { | |
await new Promise(resolve => { | |
this.filterer.autoFilter(); | |
resolve(); | |
}) | |
} | |
nextPage() { | |
this.filterer.cursor = 'after'; | |
this.renderProducts(this.filterer.filters); | |
} | |
show() { | |
this.filterer.cursor = undefined; | |
this.renderProducts(this.filterer.filters); | |
} | |
prevPage() { | |
this.filterer.cursor = 'before'; | |
this.renderProducts(this.filterer.filters); | |
} | |
buildProducts(products) { | |
let queue = []; | |
let count = 0; | |
const collectionLength = products.length; | |
for (const product of products) { | |
if (product.node.images.edges.length > 0) { | |
const originalImgURL = product.node.images.edges[0].node.originalSrc; | |
const dicedUrl = originalImgURL.split('.jpg'); | |
const imgUrl = dicedUrl[0] + '_300x.jpg' + dicedUrl[1]; | |
const zoomImgUrl = dicedUrl[0] + '_1200x.jpg' + dicedUrl[1]; | |
queue.push(CatalogItem.buildTemplate(product, imgUrl, zoomImgUrl)); | |
} | |
count++; | |
if (count === collectionLength) { | |
return queue.join(''); | |
} | |
} | |
} | |
renderProducts() { | |
this.container.empty(); | |
this.model.query(this.filterer.filters).then((response) => { | |
scrollTo(`#${this.container.attr('id')}`) | |
if (response.data.products.edges.length > 0) { | |
this.container.html(this.buildProducts(response.data.products.edges)) | |
this.paginator.setButtonsState(response.data.products.pageInfo); | |
} else { | |
this.container.html(`<div class="no-products">No results. Please try again.</div>`) | |
} | |
}); | |
} | |
} | |
class CatalogItem { | |
static buildTemplate(item, imgUrl, zoomImgUrl) { | |
return `<div class="catalog-item text-center relative"> | |
<div class="catalog-image block image relative"> | |
<a class="btn-roomvo init-roomvo" href="#">See this in my room</a> | |
<a href="/products/${item.node.handle}"> | |
<div class="catalog-image block image"> | |
<img class="image-zoom full-width align-self" src="${imgUrl}" data-zoom="${zoomImgUrl}" /> | |
</div> | |
</a> | |
</div> | |
<a href="/products/${item.node.handle}"> | |
<span class="catalog-title block">${item.node.title}</a> | |
</a> | |
</div>`; | |
} | |
} | |
/** | |
* handle pagination buttons display | |
*/ | |
class CatalogPaginator { | |
constructor(buttonWrapper, prevButton, nextButton) { | |
this.buttonWrapper = buttonWrapper; | |
this.prevButton = prevButton; | |
this.nextButton = nextButton; | |
} | |
setButtonsState(pageInfo) { | |
if (pageInfo.hasPreviousPage === false && pageInfo.hasNextPage === false) { | |
this.buttonWrapper.hide(); | |
} else { | |
this.buttonWrapper.show(); | |
this.displayNextButtons(pageInfo); | |
this.displayPreviousButtons(pageInfo); | |
} | |
} | |
displayNextButtons(pageInfo) { | |
if (pageInfo.hasNextPage === false) { | |
this.nextButton.addClass('disabled').attr('disabled', true); | |
} else { | |
this.nextButton.removeClass('disabled').attr('disabled', false); | |
} | |
} | |
displayPreviousButtons(pageInfo) { | |
if (pageInfo.hasPreviousPage === false) { | |
this.prevButton.addClass('disabled').attr('disabled', true); | |
} else { | |
this.prevButton.removeClass('disabled').attr('disabled', false); | |
} | |
} | |
} | |
const container = $('#catalog'); | |
const paginator = new CatalogPaginator($('.catalog-buttons'), $('.catalog-prev-page'), $('.catalog-next-page')); | |
const model = new Catalog(); | |
const filterer = new Filterer(); | |
filterer.filterSelector = $('.catalog-filter'); | |
filterer.pageSize = parseInt($('#pageSize').text()); | |
const controller = new CatalogController(container, model, paginator, filterer); | |
controller.registerEvents(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment