Skip to content

Instantly share code, notes, and snippets.

@JWizerd
Last active November 12, 2020 15:43
Show Gist options
  • Save JWizerd/c44065281d88b9270e95ca1913988667 to your computer and use it in GitHub Desktop.
Save JWizerd/c44065281d88b9270e95ca1913988667 to your computer and use it in GitHub Desktop.
/**
* @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