Last active
August 25, 2020 02:47
-
-
Save panoply/b2aeb205ddee3a275111941cc1a03c79 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
import m from 'mithril' | |
export default ({ | |
lowStock, | |
remains, | |
name, | |
colour, | |
price, | |
pricing, | |
percent, | |
compare_at_price | |
}) => [ | |
((compare_at_price === price) | |
? null | |
: m('.sale h5', `${Math.abs(percent)} ${percentOff}`) | |
), | |
((remains < 4) | |
? m('.low-stock h6', [ | |
m('span.mr-1', `${remains} ${lowStock}`) | |
]) | |
: null | |
), | |
m('ul', [ | |
m('li.h5 mb-1', name), | |
m('li.h6 mt-0', colour), | |
(compare_at_price !== price ? [ | |
m('li.price.h5.d-inline.mr-1', pricing), | |
m('li.price-old.h5.d-inline.ml-1', money(compare_at_price)) | |
] : [ | |
m('li.price.h5.d-inline.text-center', pricing) | |
]) | |
]) | |
] |
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
export class Products extends Controller { | |
/** | |
* State - Keeps cache of fetched products | |
* | |
* @static | |
* @memberof Controller | |
*/ | |
static state = new Map() | |
/** | |
* State - Keeps cache of fetched products | |
* | |
* @static | |
* @memberof Controller | |
*/ | |
static cache = new Map() | |
/** | |
* Stimulus Values | |
* | |
* @static | |
* @memberof Controller | |
* @version 2.0 | |
*/ | |
static values = { | |
mount: String, | |
fetch: Boolean, | |
loadingIcon: Boolean, | |
active: Number, | |
breakpoint: String, | |
collectionPath: String, | |
productPath: String, | |
currentPage: Number, | |
totalPages: Number, | |
perPage: Number, | |
totalItems: Number, | |
fetchedItems: Number, | |
loadedItems: Number, | |
isVirtualQuickview: Boolean | |
} | |
/** | |
* Stimulus Targets | |
*/ | |
static targets = [ | |
'mount', | |
'product', | |
'quickview', | |
'pagination' | |
] | |
/** | |
* Stimulus Classes | |
* | |
* @static | |
* @memberof Dropdown | |
* @version 2.0 | |
*/ | |
static classes = [ | |
'product' | |
] | |
initialize () { | |
if (isPreview() || this.totalItemsValue === 0) return | |
} | |
connect () { | |
if (isPreview() || this.totalItemsValue === 0) return | |
// if (App.screen.isActive('md')) smoothscroll.polyfill() | |
this.node = document.createElement('div') | |
this.scroll = new IntersectionObserver(this.paginate.bind(this)) | |
if (!this.hasLoadedItemsValue) { | |
this.loadedItemsValue = this.perPageValue | |
} | |
m.mount(this.node, this.collectionVnode()) | |
if (!this.hasMountValue) { | |
this.mountValue = 'initial' | |
} | |
} | |
disconnect () { | |
if (this.scroll) { | |
this.scroll.disconnect() | |
} | |
m.mount(this.node, null) | |
if (this.mountValue !== 'stored' && this.products?.length) { | |
this.mountValue = 'cached' | |
this.fetchedItemsValue = this.products.length | |
} | |
} | |
/** | |
* Paginate Observer Intersection Callback | |
* | |
* @returns {Promise<void>} | |
* @memberof Products | |
*/ | |
paginate ([ entry ]) { | |
// console.log('in paginate', this) | |
if (!entry.isIntersecting) return | |
this.collectionFetch() | |
this.collectionFetch() // run 2 times | |
} | |
/** | |
* Fetch Collection - Leverages the Shopify public endpoint API | |
* | |
* @returns {Promise<void>} | |
* @memberof Products | |
*/ | |
async collectionFetch () { | |
if (this.complete) { | |
this.mountValue = 'stored' | |
return null | |
} | |
this.fetchValue = false | |
this.currentPageValue = this.currentPageValue + 1 | |
this.collectionPathValue = window.location.pathname | |
try { | |
const { products } = await m.request({ | |
method: 'GET', | |
url: `${this.collectionPathValue}/products.json`, | |
params: { | |
page: this.currentPageValue, | |
limit: this.perPageValue * 2 | |
} | |
}) | |
this.products = products | |
this.loadedItemsValue = (this.loadedItemsValue + products.length) | |
this.fetchValue = true | |
} catch (e) { | |
console.log(e) | |
// return Turbolinks.visit(`${this.collectionPathValue}?page=${this.currentPageValue}`) | |
} | |
} | |
/** | |
* Render Collection Product Items | |
* | |
* @memberof Products | |
*/ | |
collectionVnode () { | |
console.log(this.products) | |
return ({ | |
oninit: () => this.scroll.observe(this.paginationTarget), | |
oncreate: () => this.collectionFetch(), | |
onupdate: ({ state }) => { | |
this.mountTarget.append(...this.node.children) | |
}, | |
view: () => [ | |
this.fetchValue ? m(Product, { | |
perPage: this.perPageValue, | |
className: this.productClass, | |
products: this.mountValue === 'cached' | |
? this.products.slice(this.fetchedItemsValue) | |
: this.products | |
}) : null | |
] | |
}) | |
} | |
productPathValueChanged () { | |
// m.redraw() | |
} | |
/** | |
* Toggle Quickview open/close (used in touch devices) | |
* | |
* @param {Event} event | |
* @returns | |
* @memberof Quickview | |
*/ | |
quickview (event) { | |
// skip when viewport is in desktop | |
if (!App.screen.isActive('sm')) return | |
event.preventDefault() | |
const index = parseInt(event.currentTarget.id) | |
const path = new URL(event.currentTarget.href).pathname | |
// index = isEven(index) ? index - 1 : index | |
// set state in dom | |
this.productPathValue = path | |
this.isVirtualQuickviewValue = false | |
// unmount any active quickviews | |
let node | |
if (this.activeValue > -1 && index !== this.activeValue) { | |
node = this.element.querySelector(`[data-${this.activeValue}]`) | |
node.classList.remove('opened') | |
node.classList.add('closed') | |
m.mount(node, null) | |
} | |
this.activeValue = index | |
// select view in grid | |
node = this.element.querySelector(`[data-${this.activeValue}]`) | |
node.classList.remove('closed') | |
node.classList.add('opened') | |
// mount virtual nodes | |
m.mount(node, this.quickviewVnode(node)) | |
} | |
/** | |
* Fetch Product - Leverages the Shopify public endpoint API | |
* | |
* @returns {Promise<void>} | |
* @memberof Products | |
*/ | |
async productFetch () { | |
if (this.exists) return null | |
this.product = null | |
try { | |
const { product } = await m.request({ | |
method: 'GET', | |
url: this.productPathValue + '.json' | |
}) | |
this.product = product | |
} catch (e) { | |
console.log(e) | |
// return Turbolinks.visit(this.productPathValue) | |
} | |
} | |
/** | |
* Render Collection Product Items | |
* | |
* @memberof Products | |
*/ | |
quickviewVnode () { | |
return ({ | |
oninit: () => this.productFetch(), | |
oncreate: ({ dom }) => dom.scrollIntoView({ behavior: 'smooth' }), | |
view: () => m('.product-viewer', this.product ? m(Quickview, this.product) : [ | |
m('.aspect-ratio.d-block', this.loadingIconValue | |
? m('svg.loading', m('use[xlink:href="#loading"]')) | |
: null) | |
]) | |
}) | |
} | |
/** | |
* Check for completed pagination | |
* | |
* @memberof Products | |
*/ | |
get complete () { | |
return (( | |
(this.loadedItemsValue + this.perPageValue) === this.totalItemsValue | |
) || this.currentPageValue === this.totalPagesValue) | |
} | |
/** | |
* Get Product - returns JSON response of single products | |
* | |
* @memberof Products | |
*/ | |
get exists () { | |
return Products.state.has(this.productPathValue) | |
} | |
/** | |
* Get Product - returns JSON response of single products | |
* | |
* @memberof Products | |
*/ | |
get product () { | |
return Products.state.get(this.productPathValue) | |
} | |
/** | |
* Set Product - Sets JSON response of a single product | |
* | |
* @memberof Products | |
*/ | |
set product (data) { | |
Products.state.set(this.productPathValue, data) | |
} | |
/** | |
* Get Products | |
* | |
* @memberof Products | |
*/ | |
get products () { | |
return Products.state.get(this.collectionPathValue) | |
} | |
/** | |
* Set Products | |
* | |
* @memberof Products | |
*/ | |
set products (data) { | |
if (Products.state.has(this.collectionPathValue)) { | |
Products.state.get(this.collectionPathValue).push(...data) | |
} else { | |
Products.state.set(this.collectionPathValue, data) | |
} | |
} | |
} |
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
import m from 'mithril' | |
import { Dropdown } from '../../controllers/Dropdown' | |
import { handleize, parse } from '../../application/Utilities' | |
export default { | |
view: ({ | |
attrs: { | |
variants | |
} | |
}) => m('.dropdown', Dropdown.public.contoller, [ | |
m('button.dropdown-button', Dropdown.public.button, [ | |
m('span', Locale.product.selectSize), | |
m('svg.icon', m("use[xlink:href='#chevron-bottom']")) | |
]), | |
m('.dropdown-list[data-dropdown-target="list"]', [ | |
m('ul[data-action="click->dropdown#options"]', [ | |
variants.map(({ | |
id, | |
title, | |
inventory_quantity | |
}) => inventory_quantity > 0 ? m('li.d-flex.ai-center', [ | |
m(`label[id="${id}"][for=${handleize(title)}]`, { | |
'data-action': 'click->purchase#select' | |
}, title), | |
inventory_quantity <= 3 | |
? m('span.ml-auto.pr-2', parse(Locale.product.stockRemain, inventory_quantity)) | |
: null, | |
m(`input[type="hidden"][id=${id}][value="${id}"]`) | |
]) : m('li.d-flex.ai-center', [ | |
m('label.disabled[data-disabled=][for=][id=]', title), | |
m('span.disabled.ml-auto.pr-2[data-disabled=]', Locale.product.outOfStock) | |
])) | |
]) | |
]) | |
]) | |
} |
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
import m from 'mithril' | |
import { splitTitle, money, parse } from '../../application/Utilities' | |
/** | |
* Pricing Virtual Node | |
* | |
* Generates product pricing vnode. Text locales are binded | |
* to `this` scope. | |
* | |
* @export | |
* @param {string} title | |
* @param {string} variants | |
* @param {{percentOff: string, lowStock: string }} param | |
* @returns | |
*/ | |
export function Pricing (title, variants) { | |
let stock = variants.filter(({ available }) => available === true) | |
if (stock.length < 1) stock = variants | |
const [ { compare_at_price, price } ] = stock | |
const { name, colour } = splitTitle(title) | |
const remains = stock.length | |
const pricing = money(price) | |
const percent = Math.floor((compare_at_price - price) / compare_at_price * 100) | |
return { | |
header: () => [ | |
m('.row.jc-between.ai-center.text-center.text-md-left', [ | |
m('.col-12', [ | |
m('h4.mt-3.mb-0', name), | |
m('span.font-weight-light.text-uppercase.text-muted', colour) | |
]), | |
m('.col-12.col-md-auto.py-3.py-md-0', [ | |
m('h3.mb-0', pricing), | |
((compare_at_price > price) | |
? m('s', money(compare_at_price)) | |
: m('span.percent', `${Math.abs(percent)} ${Locale.product.percentOff}`)) | |
]) | |
]) | |
], | |
hover: () => [ | |
((compare_at_price === price) | |
? null | |
: m('.sale h5', `${Math.abs(percent)} ${Locale.product.percentOff}`) | |
), | |
((remains < 4) | |
? m('.low-stock h6', [ | |
m('span.mr-1', parse(Locale.product.stockRemain, remains)) | |
]) | |
: null | |
), | |
m('ul', [ | |
m('li.d-block h5 mb-1', name), | |
m('li.d-block h6 mt-0', colour), | |
(compare_at_price !== price ? [ | |
m('li.price h5 d-inline mr-1', pricing), | |
m('li.price-old h5 d-inline ml-1', money(compare_at_price)) | |
] : [ | |
m('li.price h5 d-inline text-center', pricing) | |
]) | |
]) | |
] | |
} | |
} |
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
// mithril product collection item | |
import m from 'mithril' | |
import { Pricing } from './Pricing' | |
import { imgSrc, isEven } from './../../application/Utilities' | |
export default ({ | |
oninit: ({ state }) => { | |
console.log(state) | |
state.index = -1 | |
}, | |
onupdate: () => App.images.observe(), | |
oncreate: () => App.images.observe(), | |
view: ({ | |
state, | |
attrs: { | |
className, | |
products, | |
perPage | |
} | |
}) => products.map(({ | |
id | |
, handle | |
, title | |
, variants | |
, images: [ | |
{ | |
alt, | |
src | |
} | |
] | |
}, index) => [ | |
m('.' + className, [ | |
m('a.d-block', { | |
id, | |
title, | |
href: `/products/${handle}`, | |
'data-index': (perPage / 2) + index, | |
'data-action': 'click->products#quickview' | |
}, [ | |
m('.details.d-flex.ai-center.jc-center.text-center', ( | |
Pricing(title, variants).hover() | |
)), | |
m('.aspect-ratio d-block', [ | |
m('picture.image[data-src]', { alt, 'data-iesrc': imgSrc(src, 720) }, [ | |
m('source', { | |
srcset: imgSrc(src, 1024), | |
media: '(min-width: 1640px)' | |
}), | |
m('source', { | |
srcset: imgSrc(src, 720), | |
media: '(min-width: 540px)' | |
}), | |
m('source', { | |
srcset: imgSrc(src, 420), | |
media: '(min-width: 320px)' | |
}) | |
]) | |
]) | |
]) | |
]), | |
!isEven(index) | |
? m(`.product-quickview.col-12.px-2.py-4.closed[data-${state.index}][data-${id}]`, { | |
'data-products-target': 'quickview' | |
}) : (state.index = id) | |
]) | |
}) |
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
{%- unless grid -%} | |
{%- capture grid -%} | |
col-6 col-sm-4 col-xl-3 p-{{ settings.product_item_gutter }} | |
{%- endcapture -%} | |
{%- endunless -%} | |
<div class="product-item {{ grid }}" {{ target }} {%- if style -%} {{ style }} {%- endif -%}> | |
<a | |
class="d-block" | |
data-action="click->products#quickview" | |
data-index=" {{- index -}}" | |
href=" {{- product.url -}}" | |
id=" {{- product.id }}" | |
title=" {{- product.title }}"> | |
<div class="details d-flex ai-center jc-center text-center" data-id=" {{- product.id }}"> | |
{%- if quantity <= 4 -%} | |
<div class="low-stock h6"> | |
Only | |
<span class="mr-1">{{ quantity }}</span> | |
{{- 'product_item.low_stock' | t -}} | |
</div> | |
{%- endif -%} | |
<ul> | |
<li class="d-block h5 mb-1"> | |
{{ product.title | split: '–' | first }} | |
</li> | |
<li class="d-block h6 mt-0"> | |
{{ product.title | split: '–' | last | strip }} | |
</li> | |
<!-- Product Discounted --> | |
{%- if product.compare_at_price > product.price -%} | |
<li class="price h5 d-inline mr-1"> | |
{{ product.price | money }} | |
</li> | |
<li class="price-old h5 d-inline ml-1"> | |
{{ product.compare_at_price | money }} | |
</li> | |
{%- else -%} | |
<li class="price h5 d-inline text-center"> | |
{{ product.price | money }} | |
</li> | |
{%- endif -%} | |
</ul> | |
<!-- Product Highlight --> | |
{%- if product.metafields.product.highlight != null -%} | |
<div class="highlight h6"> | |
<!-- Reverse the R for aesthetic --> | |
{%- if product.metafields.product.highlight contains 'Reversible'-%} | |
{{- product.metafields.product.highlight | replace: 'Reversible' | |
, 'Reve<span class="letter-flip">r</span>sible' -}} | |
{%- else -%} | |
{{- product.metafields.product.highlight -}} | |
{%- endif -%} | |
</div> | |
{%- endif -%} | |
</div> | |
<!-- Product Images --> | |
<div class="aspect-ratio"> | |
<picture | |
class="image" | |
data-id=" {{- product.id -}}" | |
data-iesrc=" {{- product.featured_image.src | img_url: '640x' -}}" | |
data-src> | |
<source | |
media="(min-width: 1640px)" | |
srcset="{{- product.featured_image.src | img_url: '720x' -}}" /> | |
<source | |
media="(min-width: 540px)" | |
srcset="{{- product.featured_image.src | img_url: '640x' -}}" /> | |
<source | |
media="(min-width: 320px)" | |
srcset="{{- product.featured_image.src | img_url: '320x' -}}" /> | |
<source | |
media="(max-width: 320px)" | |
srcset="{{- product.featured_image.src | img_url: '320x' -}}" /> | |
<noscript> | |
<img | |
alt=" {{- product.title -}}" | |
class="img-fluid" | |
src=" {{- product.featured_image.src | img_url: '640x' -}}"> | |
</noscript> | |
</picture> | |
</div> | |
</a> | |
</div> |
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
import m from 'mithril' | |
import { Pricing } from './Pricing' | |
import Options from './Options' | |
import Images from './Images' | |
export default { | |
view: ({ | |
attrs: { | |
title, | |
variants, | |
images, | |
handle | |
} | |
}) => [ | |
m(Images, { images }), | |
Pricing(title, variants).header(), | |
m('.row.jc-center.no-gutter.mt-3.px-0.product-purchase', { | |
'data-controller': 'purchase', | |
'data-purchase-selected-value': 'selected' | |
}, [ | |
m('.col-12.mb-2', m(Options, { variants })), | |
m('div.col-12', [ | |
m('button.cart-button', { | |
disabled: 'true', | |
'data-action': 'click->purchase#addToCart', | |
'data-drawer': 'ajax-cart', | |
'data-purchase-target': 'addToCart' | |
}, [ | |
m('span.add-to-cart', Locale.product.addToCart), | |
m('span.select-size', Locale.product.sizeRequired) | |
]) | |
]) | |
]), | |
m(`a.d-block.h5.my-4.text-center[href="${handle}"]`, Locale.collection.viewMore) | |
] | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment