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) } }