265 lines
7 KiB
JavaScript
265 lines
7 KiB
JavaScript
|
import { CartBaseController } from './cart_base_controller'
|
||
|
|
||
|
/*
|
||
|
* Manages the cart and its contents.
|
||
|
*
|
||
|
* We need to create an order in Spree API to get a token that allows to
|
||
|
* make changes to it.
|
||
|
*
|
||
|
* The order contains attributes for the order and other data can be
|
||
|
* included, like line items, variants, payments, promotions, shipments,
|
||
|
* billing and shipping addresses, and the user.
|
||
|
*
|
||
|
* Variants are products added to the cart. To remove an item or change
|
||
|
* its quantity, a line item for the variant must be found. We store
|
||
|
* this information into localStorage so we don't have to make annoying
|
||
|
* queries to JSON:API everytime.
|
||
|
*/
|
||
|
|
||
|
export default class extends CartBaseController {
|
||
|
static targets = [ 'quantity', 'subtotal', 'addedQuantity' ]
|
||
|
|
||
|
connect () {
|
||
|
if (!this.hasQuantityTarget) return
|
||
|
|
||
|
/*
|
||
|
* When the quantity selector changes, we update the order to have
|
||
|
* that amount of items.
|
||
|
*
|
||
|
* TODO: Go back to previous amount if there's not enough.
|
||
|
*/
|
||
|
this.quantityTarget.addEventListener('change', async (event) => {
|
||
|
const quantity = event.target.value
|
||
|
|
||
|
if (quantity < 1) return;
|
||
|
|
||
|
const orderToken = await this.tokenGetOrCreate()
|
||
|
|
||
|
event.target.disabled = true
|
||
|
|
||
|
const response = await this.spree.cart.setQuantity({ orderToken }, {
|
||
|
line_item_id: this.product.line_item.id,
|
||
|
quantity,
|
||
|
include: 'line_items'
|
||
|
})
|
||
|
|
||
|
event.target.disabled = false
|
||
|
event.target.focus()
|
||
|
|
||
|
// If we're failing here it could be due to a missing order, so we
|
||
|
// ask the user to decide what they want to do about it
|
||
|
if (response.isFail()) {
|
||
|
this.handleFailure(response)
|
||
|
return
|
||
|
}
|
||
|
|
||
|
this.cart = response
|
||
|
this.subtotalUpdate()
|
||
|
this.counterUpdate()
|
||
|
await this.itemStore()
|
||
|
|
||
|
if (!this.hasSubtotalTarget) return
|
||
|
|
||
|
this.subtotalTarget.innerText = this.product.line_item.attributes.discounted_amount
|
||
|
})
|
||
|
}
|
||
|
|
||
|
subtotalUpdate () {
|
||
|
window.dispatchEvent(new Event('cart:subtotal:update'))
|
||
|
}
|
||
|
|
||
|
/*
|
||
|
* Creates an order and stores the data into localStorage.
|
||
|
*
|
||
|
* @return [String]
|
||
|
*/
|
||
|
async cartCreate () {
|
||
|
const response = await this.spree.cart.create()
|
||
|
|
||
|
// If we fail here it's probably a server error, so we inform the
|
||
|
// user.
|
||
|
if (response.isFail()) {
|
||
|
this.handleFailure(response)
|
||
|
return
|
||
|
}
|
||
|
|
||
|
this.cart = response
|
||
|
this.storage.setItem('token', response.success().data.attributes.token)
|
||
|
|
||
|
return this.token
|
||
|
}
|
||
|
|
||
|
/*
|
||
|
* Gets the order token and creates a cart if it doesn't exist.
|
||
|
*
|
||
|
* @return [String]
|
||
|
*/
|
||
|
async tokenGetOrCreate () {
|
||
|
let token = this.storage.getItem('token')
|
||
|
|
||
|
return token || await this.cartCreate()
|
||
|
}
|
||
|
|
||
|
/*
|
||
|
* The variant ID is used to identify products
|
||
|
*
|
||
|
* @return [String]
|
||
|
*/
|
||
|
get variantId () {
|
||
|
return this.data.get('variantId')
|
||
|
}
|
||
|
|
||
|
get product () {
|
||
|
return JSON.parse(this.storage.getItem(this.storageId))
|
||
|
}
|
||
|
|
||
|
/*
|
||
|
* Obtains the line_item_id by a variant_id by inspecting the cart and
|
||
|
* its included items
|
||
|
*
|
||
|
* @return [Object]
|
||
|
*/
|
||
|
findLineItem () {
|
||
|
const line_item = this.cart.included.find(x => (x.type === 'line_item' && x.relationships.variant.data.id == this.variantId))
|
||
|
|
||
|
return (line_item || {})
|
||
|
}
|
||
|
|
||
|
get storageId () {
|
||
|
return `cart:item:${this.variantId}`
|
||
|
}
|
||
|
|
||
|
/*
|
||
|
* Stores an item for later usage.
|
||
|
*
|
||
|
* @see {./order_controller.js}
|
||
|
*/
|
||
|
itemStore () {
|
||
|
this.storage.setItem(this.storageId, JSON.stringify({
|
||
|
variant_id: this.variantId,
|
||
|
line_item: this.findLineItem(),
|
||
|
image: this.data.get('image'),
|
||
|
title: this.data.get('title'),
|
||
|
url: this.data.get('url'),
|
||
|
stock: this.data.get('stock'),
|
||
|
in_stock: this.data.get('inStock'),
|
||
|
extra: this.data.get('extra') ? this.data.get('extra').split('|') : []
|
||
|
}))
|
||
|
}
|
||
|
|
||
|
/*
|
||
|
* Adds item to cart. This is meant to be used by an "Add to cart"
|
||
|
* button. If the item already exists in the cart it updates the
|
||
|
* quantity by +1.
|
||
|
*
|
||
|
* The item needs a variant ID to be added.
|
||
|
*/
|
||
|
async add(event, quantity = 1, floating_alert = true) {
|
||
|
const addedQuantity = this.addedQuantity()
|
||
|
if (addedQuantity > 1) quantity = addedQuantity
|
||
|
|
||
|
const orderToken = await this.tokenGetOrCreate()
|
||
|
const response = await this.spree.cart.addItem({ orderToken }, { variant_id: this.variantId, quantity, include: 'line_items' })
|
||
|
|
||
|
if (response.isFail()) {
|
||
|
this.handleFailure(response)
|
||
|
return
|
||
|
}
|
||
|
|
||
|
this.cart = response
|
||
|
this.itemStore()
|
||
|
this.counterUpdate()
|
||
|
this.fireCajon()
|
||
|
|
||
|
if (floating_alert) {
|
||
|
const site = await this.site()
|
||
|
const content = site.cart.added
|
||
|
window.dispatchEvent(new CustomEvent('floating:alert', { detail: { content }}))
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/*
|
||
|
* Remove the element from the cart. It contacts the API and if the
|
||
|
* item is removed, it removes itself from the page and the storage.
|
||
|
*/
|
||
|
async remove () {
|
||
|
if (!this.product.line_item) return
|
||
|
|
||
|
const orderToken = this.token
|
||
|
const response = await this.spree.cart.removeItem({ orderToken }, this.product.line_item.id, { include: 'line_items' })
|
||
|
|
||
|
if (response.isFail()) {
|
||
|
this.handleFailure(response)
|
||
|
return
|
||
|
}
|
||
|
|
||
|
this.cart = response
|
||
|
this.storage.removeItem(this.storageId)
|
||
|
this.element.remove()
|
||
|
this.subtotalUpdate()
|
||
|
this.counterUpdate()
|
||
|
}
|
||
|
|
||
|
/*
|
||
|
* Shows variants
|
||
|
*/
|
||
|
async variants () {
|
||
|
const template = 'variants'
|
||
|
const data = {
|
||
|
product: {
|
||
|
variant_id: this.data.get('variantId'),
|
||
|
digital_variant_id: this.data.get('digitalVariantId'),
|
||
|
image: this.data.get('image'),
|
||
|
title: this.data.get('title'),
|
||
|
price: this.data.get('price'),
|
||
|
digital_price: this.data.get('digitalPrice'),
|
||
|
in_stock: this.data.get('inStock'),
|
||
|
extra: this.data.get('extra').split('|')
|
||
|
}
|
||
|
}
|
||
|
|
||
|
window.dispatchEvent(new CustomEvent('notification', { detail: { template, data } }))
|
||
|
}
|
||
|
|
||
|
/*
|
||
|
* Recovers the order if something failed
|
||
|
*/
|
||
|
async recover () {
|
||
|
// Removes the failing token
|
||
|
this.storage.removeItem('token')
|
||
|
|
||
|
// Stores the previous cart
|
||
|
const cart = this.cart
|
||
|
|
||
|
// Get a new token and cart
|
||
|
await this.tokenGetOrCreate()
|
||
|
|
||
|
// Add previous items and their quantities to the new cart by
|
||
|
// mimicking user's actions
|
||
|
//
|
||
|
// XXX: We don't use forEach because it's not async
|
||
|
for (const variant of cart.data.relationships.variants.data) {
|
||
|
this.data.set('variantId', variant.id)
|
||
|
|
||
|
const product = this.product
|
||
|
|
||
|
this.data.set('image', product.image)
|
||
|
this.data.set('title', product.title)
|
||
|
this.data.set('extra', product.extra.join('|'))
|
||
|
|
||
|
await this.add(null, product.line_item.attributes.quantity, false)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/*
|
||
|
* Si le compradore aumenta la cantidad antes de agregar
|
||
|
*/
|
||
|
addedQuantity () {
|
||
|
if (!this.hasAddedQuantityTarget) return 0
|
||
|
|
||
|
const addedQuantity = parseInt(this.addedQuantityTarget.value)
|
||
|
|
||
|
return (isNaN(addedQuantity) ? 0 : addedQuantity)
|
||
|
}
|
||
|
}
|