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(); const product = this.product; if (!product) return; event.target.disabled = true; const response = await this.spree.cart.setQuantity( { orderToken }, { line_item_id: 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 = 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() { const product = JSON.parse(this.storage.getItem(this.storageId)); if (!product) { console.error( "El producto es nulo!", this.storageId, this.storage.length, this.cart ); } return product; } /* * 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 = window.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() { const product = this.product; if (!product) return; if (!product.line_item) return; const orderToken = this.token; const response = await this.spree.cart.removeItem( { orderToken }, 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() { console.error("Recuperando pedido", this.token); // Removes the failing token this.storage.removeItem("token"); // Get a new token and cart await this.tokenGetOrCreate(); // Stores the previous cart const cart = this.cart; if (!cart) return; // 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; if (!product) continue; 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; } }