Merge branch 'prettier' into 'master'

Prettier

See merge request sutty/jekyll/sutty-base-jekyll-theme!26
This commit is contained in:
Nulo 2021-11-22 17:55:40 +00:00
commit cb2516e5bb
28 changed files with 1302 additions and 911 deletions

View file

@ -49,6 +49,12 @@ npx: ## Correr comandos con npx (args="run")
npm: ## Correr comandos con npm (args="install -g paquete") npm: ## Correr comandos con npm (args="install -g paquete")
$(MAKE) hain args="npm $(args)" $(MAKE) hain args="npm $(args)"
prettier: ## Arreglar JS (por ahora)
$(MAKE) yarn args="prettier --write _packs/"
format-check: ## Verificar JS
$(MAKE) yarn args="prettier --check _packs/"
serve: /etc/hosts $(hain)/run/nginx/nginx.pid ## Servidor de desarrollo serve: /etc/hosts $(hain)/run/nginx/nginx.pid ## Servidor de desarrollo
@echo "Iniciado servidor web en https://$(domain):4000/" @echo "Iniciado servidor web en https://$(domain):4000/"

View file

@ -1,13 +1,13 @@
import { Controller } from 'stimulus' import { Controller } from "stimulus";
import { Liquid } from 'liquidjs' import { Liquid } from "liquidjs";
/* /*
* Base controller, shared methods go here and other classes extend from * Base controller, shared methods go here and other classes extend from
* this. * this.
*/ */
export class CartBaseController extends Controller { export class CartBaseController extends Controller {
get spree () { get spree() {
return window.spree return window.spree;
} }
/* /*
@ -15,40 +15,42 @@ export class CartBaseController extends Controller {
* *
* @return [Array] * @return [Array]
*/ */
get products () { get products() {
if (!this.cart) return [] if (!this.cart) return [];
return this.cart.data.relationships.variants.data.map(x => JSON.parse(this.storage.getItem(`cart:item:${x.id}`))).filter(x => x !== null) return this.cart.data.relationships.variants.data
.map((x) => JSON.parse(this.storage.getItem(`cart:item:${x.id}`)))
.filter((x) => x !== null);
} }
/* /*
* The storage * The storage
*/ */
get storage () { get storage() {
return window.localStorage return window.localStorage;
} }
/* /*
* Temporary storage * Temporary storage
*/ */
get storageTemp () { get storageTemp() {
return window.sessionStorage return window.sessionStorage;
} }
get cart () { get cart() {
return JSON.parse(this.storage.getItem('cart')) return JSON.parse(this.storage.getItem("cart"));
} }
set cart (response) { set cart(response) {
this.storage.setItem('cart', JSON.stringify(response.success())) this.storage.setItem("cart", JSON.stringify(response.success()));
} }
get email () { get email() {
return this.storageTemp.getItem('email') return this.storageTemp.getItem("email");
} }
set email (email) { set email(email) {
this.storageTemp.setItem('email', email) this.storageTemp.setItem("email", email);
} }
/* /*
@ -56,8 +58,8 @@ export class CartBaseController extends Controller {
* *
* @return [String] * @return [String]
*/ */
get token () { get token() {
return this.storage.getItem('token') return this.storage.getItem("token");
} }
/* /*
@ -65,12 +67,12 @@ export class CartBaseController extends Controller {
* *
* @return [String] * @return [String]
*/ */
get bearerToken () { get bearerToken() {
return this.storageTemp.getItem('bearerToken') return this.storageTemp.getItem("bearerToken");
} }
set bearerToken (token) { set bearerToken(token) {
this.storageTemp.setItem('bearerToken', token) this.storageTemp.setItem("bearerToken", token);
} }
/* /*
@ -78,20 +80,22 @@ export class CartBaseController extends Controller {
* *
* @return Liquid * @return Liquid
*/ */
get engine () { get engine() {
if (!window.liquid) window.liquid = new Liquid() if (!window.liquid) window.liquid = new Liquid();
return window.liquid return window.liquid;
} }
/* /*
* Updates the item counter * Updates the item counter
*/ */
counterUpdate () { counterUpdate() {
const item_count = this.cart.data.attributes.item_count const item_count = this.cart.data.attributes.item_count;
window.dispatchEvent(new CustomEvent('cart:counter', { detail: { item_count }})) window.dispatchEvent(
this.storage.setItem('cart:counter', item_count) new CustomEvent("cart:counter", { detail: { item_count } })
);
this.storage.setItem("cart:counter", item_count);
} }
/* /*
@ -99,81 +103,92 @@ export class CartBaseController extends Controller {
* *
* @return [String] * @return [String]
*/ */
idFromInputName (input) { idFromInputName(input) {
const matches = input.name.match(/\[([^\]]+)\]$/) const matches = input.name.match(/\[([^\]]+)\]$/);
return (matches === null) ? input.name : matches[1] return matches === null ? input.name : matches[1];
} }
async handleFailure (response) { async handleFailure(response) {
const data = { type: 'primary' } const data = { type: "primary" };
let template = 'alert' let template = "alert";
let notify = true let notify = true;
const site = window.site const site = window.site;
const fail = response.fail() const fail = response.fail();
switch (fail.name) { switch (fail.name) {
case 'MisconfigurationError': case "MisconfigurationError":
data.content = fail.message data.content = fail.message;
break break;
case 'NoResponseError': case "NoResponseError":
data.content = site.i18n.alerts.no_response_error data.content = site.i18n.alerts.no_response_error;
break break;
case 'SpreeError': case "SpreeError":
data.content = site.i18n.alerts.spree_error data.content = site.i18n.alerts.spree_error;
break break;
case 'BasicSpreeError': case "BasicSpreeError":
// XXX: The order is missing, we need to start a new one // XXX: The order is missing, we need to start a new one
if (fail.serverResponse.status === 404) { if (fail.serverResponse.status === 404) {
template = 'recover_order' template = "recover_order";
data.content = site.i18n.alerts.recover_order data.content = site.i18n.alerts.recover_order;
} else { } else {
data.content = response.fail().summary data.content = response.fail().summary;
} }
break break;
case 'ExpandedSpreeError': case "ExpandedSpreeError":
const content = [] const content = [];
// XXX: La API devuelve los mensajes de error tal cual en la // XXX: La API devuelve los mensajes de error tal cual en la
// llave `error` pero el cliente solo nos da acceso a `errors`. // llave `error` pero el cliente solo nos da acceso a `errors`.
for (const field of Object.keys(fail.errors)) { for (const field of Object.keys(fail.errors)) {
if (!site.i18n.errors?.fields[field]) { if (!site.i18n.errors?.fields[field]) {
console.error('El campo no tiene traducción', field) console.error("El campo no tiene traducción", field);
} }
content.push(`${site.i18n.errors?.fields[field]}: ${fail.errors[field].join(', ')}`) content.push(
`${site.i18n.errors?.fields[field]}: ${fail.errors[field].join(
", "
)}`
);
} }
data.content = content.join('. ') data.content = content.join(". ");
notify = false notify = false;
break break;
default: default:
data.content = fail.message data.content = fail.message;
} }
if (notify) console.error(fail.name, data.content) if (notify) console.error(fail.name, data.content);
window.dispatchEvent(new CustomEvent('notification', { detail: { template, data } })) window.dispatchEvent(
new CustomEvent("notification", { detail: { template, data } })
);
} }
set formDisabled (state) { set formDisabled(state) {
if (!this.hasFormTarget) return if (!this.hasFormTarget) return;
this.formTarget.elements.forEach(x => x.disabled = !!state) this.formTarget.elements.forEach((x) => (x.disabled = !!state));
} }
assignShippingAddress () { assignShippingAddress() {
if (!this.bearerToken) return if (!this.bearerToken) return;
const bearerToken = this.bearerToken const bearerToken = this.bearerToken;
const orderToken = this.token const orderToken = this.token;
this.spree.sutty.assignOrderShippingAddress({ bearerToken }, { orderToken }) this.spree.sutty.assignOrderShippingAddress(
{ bearerToken },
{ orderToken }
);
} }
notify (data = {}) { notify(data = {}) {
window.dispatchEvent(new CustomEvent('notification', { detail: { template: 'alert', data } })) window.dispatchEvent(
new CustomEvent("notification", { detail: { template: "alert", data } })
);
} }
/* /*
@ -181,66 +196,75 @@ export class CartBaseController extends Controller {
* *
* @param [String] URL * @param [String] URL
*/ */
visit (url) { visit(url) {
try { try {
Turbolinks.visit(url) Turbolinks.visit(url);
} catch { } catch {
window.location = url window.location = url;
} }
} }
async firstAddress (bearerToken) { async firstAddress(bearerToken) {
if (!this._firstAddress) { if (!this._firstAddress) {
const addresses = await this.spree.account.addressesList({ bearerToken }) const addresses = await this.spree.account.addressesList({ bearerToken });
if (addresses.isFail()) { if (addresses.isFail()) {
this.handleFailure(addresses) this.handleFailure(addresses);
return return;
} }
// XXX: Asumimos que si se registró tiene una dirección que vamos // XXX: Asumimos que si se registró tiene una dirección que vamos
// a actualizar. // a actualizar.
this._firstAddress = addresses.success().data[0] this._firstAddress = addresses.success().data[0];
} }
return this._firstAddress return this._firstAddress;
} }
async updateAddress(bearerToken, id, address) { async updateAddress(bearerToken, id, address) {
const updateAddress = await this.spree.account.updateAddress({ bearerToken }, id, { address }) const updateAddress = await this.spree.account.updateAddress(
{ bearerToken },
id,
{ address }
);
if (updateAddress.isFail()) { if (updateAddress.isFail()) {
this.handleFailure(updateAddress) this.handleFailure(updateAddress);
return return;
} }
return updateAddress.success() return updateAddress.success();
} }
async shippingMethods(orderToken) { async shippingMethods(orderToken) {
const shippingMethods = await this.spree.checkout.shippingMethods({ orderToken }, { include: 'shipping_rates' }) const shippingMethods = await this.spree.checkout.shippingMethods(
{ orderToken },
{ include: "shipping_rates" }
);
if (shippingMethods.isFail()) { if (shippingMethods.isFail()) {
this.handleFailure(shippingMethods) this.handleFailure(shippingMethods);
return return;
} }
return shippingMethods.success() return shippingMethods.success();
} }
fireCajon (state = 'open', cajon = 'cajon') { fireCajon(state = "open", cajon = "cajon") {
window.dispatchEvent(new CustomEvent('cajon', { detail: { cajon, state }})) window.dispatchEvent(
new CustomEvent("cajon", { detail: { cajon, state } })
);
} }
formDataToObject (formData) { formDataToObject(formData) {
const object = {} const object = {};
for (const field of formData) { for (const field of formData) {
if (field[0].startsWith('_ignore_')) continue if (field[0].startsWith("_ignore_")) continue;
object[field[0]] = field[1] object[field[0]] = field[1];
} }
return object return object;
} }
} }

View file

@ -1,41 +1,47 @@
import { CartBaseController } from './cart_base_controller' import { CartBaseController } from "./cart_base_controller";
export default class extends CartBaseController { export default class extends CartBaseController {
static targets = [ 'order' ] static targets = ["order"];
async connect () { async connect() {
if (this.clear) { if (this.clear) {
this.storage.clear() this.storage.clear();
window.dispatchEvent(new CustomEvent('cart:counter', { detail: { item_count: 0 }})) window.dispatchEvent(
new CustomEvent("cart:counter", { detail: { item_count: 0 } })
);
} }
if (!this.template) return if (!this.template) return;
if (this.storage.cart) { if (this.storage.cart) {
const order = this.cart.data.attributes const order = this.cart.data.attributes;
const products = this.products const products = this.products;
const site = window.site const site = window.site;
const shipping_address = JSON.parse(this.storage.getItem('shipping_address')) const shipping_address = JSON.parse(
this.storage.getItem("shipping_address")
);
const data = { order, products, site, shipping_address } const data = { order, products, site, shipping_address };
this.storage.setItem('confirmation', JSON.stringify(data)) this.storage.setItem("confirmation", JSON.stringify(data));
} else { } else {
data = JSON.parse(this.storage.getItem('confirmation')) data = JSON.parse(this.storage.getItem("confirmation"));
} }
this.render(data) this.render(data);
} }
render (data = {}) { render(data = {}) {
this.engine.parseAndRender(this.template, data).then(html => this.orderTarget.innerHTML = html) this.engine
.parseAndRender(this.template, data)
.then((html) => (this.orderTarget.innerHTML = html));
} }
get clear () { get clear() {
return this.element.dataset.clear return this.element.dataset.clear;
} }
get template () { get template() {
return window.templates[this.element.dataset.template] return window.templates[this.element.dataset.template];
} }
} }

View file

@ -1,24 +1,24 @@
import { CartBaseController } from './cart_base_controller' import { CartBaseController } from "./cart_base_controller";
export default class extends CartBaseController { export default class extends CartBaseController {
static targets = [ 'form', 'username' ] static targets = ["form", "username"];
connect () { connect() {
if (!this.hasUsernameTarget) return if (!this.hasUsernameTarget) return;
if (!this.hasFormTarget) return if (!this.hasFormTarget) return;
this.formTarget.addEventListener('focusout', event => { this.formTarget.addEventListener("focusout", (event) => {
if (!this.formTarget.checkValidity()) { if (!this.formTarget.checkValidity()) {
this.formTarget.classList.add('was-validated') this.formTarget.classList.add("was-validated");
return return;
} }
this.formTarget.classList.remove('was-validated') this.formTarget.classList.remove("was-validated");
const username = this.usernameTarget.value.trim() const username = this.usernameTarget.value.trim();
if (username.length === 0) return if (username.length === 0) return;
this.email = username this.email = username;
}) });
} }
} }

View file

@ -1,4 +1,4 @@
import { CartBaseController } from './cart_base_controller' import { CartBaseController } from "./cart_base_controller";
/* /*
* Manages the cart and its contents. * Manages the cart and its contents.
@ -17,10 +17,10 @@ import { CartBaseController } from './cart_base_controller'
*/ */
export default class extends CartBaseController { export default class extends CartBaseController {
static targets = [ 'quantity', 'subtotal', 'addedQuantity' ] static targets = ["quantity", "subtotal", "addedQuantity"];
connect () { connect() {
if (!this.hasQuantityTarget) return if (!this.hasQuantityTarget) return;
/* /*
* When the quantity selector changes, we update the order to have * When the quantity selector changes, we update the order to have
@ -28,47 +28,51 @@ export default class extends CartBaseController {
* *
* TODO: Go back to previous amount if there's not enough. * TODO: Go back to previous amount if there's not enough.
*/ */
this.quantityTarget.addEventListener('change', async (event) => { this.quantityTarget.addEventListener("change", async (event) => {
const quantity = event.target.value const quantity = event.target.value;
if (quantity < 1) return; if (quantity < 1) return;
const orderToken = await this.tokenGetOrCreate() const orderToken = await this.tokenGetOrCreate();
const product = this.product const product = this.product;
if (!product) return if (!product) return;
event.target.disabled = true event.target.disabled = true;
const response = await this.spree.cart.setQuantity({ orderToken }, { const response = await this.spree.cart.setQuantity(
{ orderToken },
{
line_item_id: product.line_item.id, line_item_id: product.line_item.id,
quantity, quantity,
include: 'line_items' include: "line_items",
}) }
);
event.target.disabled = false event.target.disabled = false;
event.target.focus() event.target.focus();
// If we're failing here it could be due to a missing order, so we // 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 // ask the user to decide what they want to do about it
if (response.isFail()) { if (response.isFail()) {
this.handleFailure(response) this.handleFailure(response);
return return;
} }
this.cart = response this.cart = response;
this.subtotalUpdate() this.subtotalUpdate();
this.counterUpdate() this.counterUpdate();
await this.itemStore() await this.itemStore();
if (!this.hasSubtotalTarget) return if (!this.hasSubtotalTarget) return;
this.subtotalTarget.innerText = product.line_item.attributes.discounted_amount this.subtotalTarget.innerText =
}) product.line_item.attributes.discounted_amount;
});
} }
subtotalUpdate () { subtotalUpdate() {
window.dispatchEvent(new Event('cart:subtotal:update')) window.dispatchEvent(new Event("cart:subtotal:update"));
} }
/* /*
@ -76,20 +80,20 @@ export default class extends CartBaseController {
* *
* @return [String] * @return [String]
*/ */
async cartCreate () { async cartCreate() {
const response = await this.spree.cart.create() const response = await this.spree.cart.create();
// If we fail here it's probably a server error, so we inform the // If we fail here it's probably a server error, so we inform the
// user. // user.
if (response.isFail()) { if (response.isFail()) {
this.handleFailure(response) this.handleFailure(response);
return return;
} }
this.cart = response this.cart = response;
this.storage.setItem('token', response.success().data.attributes.token) this.storage.setItem("token", response.success().data.attributes.token);
return this.token return this.token;
} }
/* /*
@ -97,10 +101,10 @@ export default class extends CartBaseController {
* *
* @return [String] * @return [String]
*/ */
async tokenGetOrCreate () { async tokenGetOrCreate() {
let token = this.storage.getItem('token') let token = this.storage.getItem("token");
return token || await this.cartCreate() return token || (await this.cartCreate());
} }
/* /*
@ -108,18 +112,23 @@ export default class extends CartBaseController {
* *
* @return [String] * @return [String]
*/ */
get variantId () { get variantId() {
return this.data.get('variantId') return this.data.get("variantId");
} }
get product () { get product() {
const product = JSON.parse(this.storage.getItem(this.storageId)) const product = JSON.parse(this.storage.getItem(this.storageId));
if (!product) { if (!product) {
console.error("El producto es nulo!", this.storageId, this.storage.length, this.cart) console.error(
"El producto es nulo!",
this.storageId,
this.storage.length,
this.cart
);
} }
return product return product;
} }
/* /*
@ -128,14 +137,18 @@ export default class extends CartBaseController {
* *
* @return [Object] * @return [Object]
*/ */
findLineItem () { findLineItem() {
const line_item = this.cart.included.find(x => (x.type === 'line_item' && x.relationships.variant.data.id == this.variantId)) const line_item = this.cart.included.find(
(x) =>
x.type === "line_item" &&
x.relationships.variant.data.id == this.variantId
);
return (line_item || {}) return line_item || {};
} }
get storageId () { get storageId() {
return `cart:item:${this.variantId}` return `cart:item:${this.variantId}`;
} }
/* /*
@ -143,17 +156,20 @@ export default class extends CartBaseController {
* *
* @see {./order_controller.js} * @see {./order_controller.js}
*/ */
itemStore () { itemStore() {
this.storage.setItem(this.storageId, JSON.stringify({ this.storage.setItem(
this.storageId,
JSON.stringify({
variant_id: this.variantId, variant_id: this.variantId,
line_item: this.findLineItem(), line_item: this.findLineItem(),
image: this.data.get('image'), image: this.data.get("image"),
title: this.data.get('title'), title: this.data.get("title"),
url: this.data.get('url'), url: this.data.get("url"),
stock: this.data.get('stock'), stock: this.data.get("stock"),
in_stock: this.data.get('inStock'), in_stock: this.data.get("inStock"),
extra: this.data.get('extra') ? this.data.get('extra').split('|') : [] extra: this.data.get("extra") ? this.data.get("extra").split("|") : [],
})) })
);
} }
/* /*
@ -164,26 +180,31 @@ export default class extends CartBaseController {
* The item needs a variant ID to be added. * The item needs a variant ID to be added.
*/ */
async add(event, quantity = 1, floating_alert = true) { async add(event, quantity = 1, floating_alert = true) {
const addedQuantity = this.addedQuantity() const addedQuantity = this.addedQuantity();
if (addedQuantity > 1) quantity = addedQuantity if (addedQuantity > 1) quantity = addedQuantity;
const orderToken = await this.tokenGetOrCreate() const orderToken = await this.tokenGetOrCreate();
const response = await this.spree.cart.addItem({ orderToken }, { variant_id: this.variantId, quantity, include: 'line_items' }) const response = await this.spree.cart.addItem(
{ orderToken },
{ variant_id: this.variantId, quantity, include: "line_items" }
);
if (response.isFail()) { if (response.isFail()) {
this.handleFailure(response) this.handleFailure(response);
return return;
} }
this.cart = response this.cart = response;
this.itemStore() this.itemStore();
this.counterUpdate() this.counterUpdate();
this.fireCajon() this.fireCajon();
if (floating_alert) { if (floating_alert) {
const site = window.site const site = window.site;
const content = site.cart.added const content = site.cart.added;
window.dispatchEvent(new CustomEvent('floating:alert', { detail: { content }})) window.dispatchEvent(
new CustomEvent("floating:alert", { detail: { content } })
);
} }
} }
@ -191,92 +212,98 @@ export default class extends CartBaseController {
* Remove the element from the cart. It contacts the API and if the * 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. * item is removed, it removes itself from the page and the storage.
*/ */
async remove () { async remove() {
const product = this.product const product = this.product;
if (!product) return if (!product) return;
if (!product.line_item) return if (!product.line_item) return;
const orderToken = this.token const orderToken = this.token;
const response = await this.spree.cart.removeItem({ orderToken }, product.line_item.id, { include: 'line_items' }) const response = await this.spree.cart.removeItem(
{ orderToken },
product.line_item.id,
{ include: "line_items" }
);
if (response.isFail()) { if (response.isFail()) {
this.handleFailure(response) this.handleFailure(response);
return return;
} }
this.cart = response this.cart = response;
this.storage.removeItem(this.storageId) this.storage.removeItem(this.storageId);
this.element.remove() this.element.remove();
this.subtotalUpdate() this.subtotalUpdate();
this.counterUpdate() this.counterUpdate();
} }
/* /*
* Shows variants * Shows variants
*/ */
async variants () { async variants() {
const template = 'variants' const template = "variants";
const data = { const data = {
product: { product: {
variant_id: this.data.get('variantId'), variant_id: this.data.get("variantId"),
digital_variant_id: this.data.get('digitalVariantId'), digital_variant_id: this.data.get("digitalVariantId"),
image: this.data.get('image'), image: this.data.get("image"),
title: this.data.get('title'), title: this.data.get("title"),
price: this.data.get('price'), price: this.data.get("price"),
digital_price: this.data.get('digitalPrice'), digital_price: this.data.get("digitalPrice"),
in_stock: this.data.get('inStock'), in_stock: this.data.get("inStock"),
extra: this.data.get('extra').split('|') extra: this.data.get("extra").split("|"),
} },
} };
window.dispatchEvent(new CustomEvent('notification', { detail: { template, data } })) window.dispatchEvent(
new CustomEvent("notification", { detail: { template, data } })
);
} }
/* /*
* Recovers the order if something failed * Recovers the order if something failed
*/ */
async recover () { async recover() {
console.error('Recuperando pedido', this.token) console.error("Recuperando pedido", this.token);
// Removes the failing token // Removes the failing token
this.storage.removeItem('token') this.storage.removeItem("token");
// Get a new token and cart // Get a new token and cart
await this.tokenGetOrCreate() await this.tokenGetOrCreate();
// Stores the previous cart // Stores the previous cart
const cart = this.cart const cart = this.cart;
if (!cart) return if (!cart) return;
// Add previous items and their quantities to the new cart by // Add previous items and their quantities to the new cart by
// mimicking user's actions // mimicking user's actions
// //
// XXX: We don't use forEach because it's not async // XXX: We don't use forEach because it's not async
for (const variant of cart.data.relationships.variants.data) { for (const variant of cart.data.relationships.variants.data) {
this.data.set('variantId', variant.id) this.data.set("variantId", variant.id);
const product = this.product const product = this.product;
if (!product) continue if (!product) continue;
this.data.set('image', product.image) this.data.set("image", product.image);
this.data.set('title', product.title) this.data.set("title", product.title);
this.data.set('extra', product.extra.join('|')) this.data.set("extra", product.extra.join("|"));
await this.add(null, product.line_item.attributes.quantity, false) await this.add(null, product.line_item.attributes.quantity, false);
} }
} }
/* /*
* Si le compradore aumenta la cantidad antes de agregar * Si le compradore aumenta la cantidad antes de agregar
*/ */
addedQuantity () { addedQuantity() {
if (!this.hasAddedQuantityTarget) return 0 if (!this.hasAddedQuantityTarget) return 0;
const addedQuantity = parseInt(this.addedQuantityTarget.value) const addedQuantity = parseInt(this.addedQuantityTarget.value);
return (isNaN(addedQuantity) ? 0 : addedQuantity) return isNaN(addedQuantity) ? 0 : addedQuantity;
} }
} }

View file

@ -1,25 +1,28 @@
import { CartBaseController } from './cart_base_controller' import { CartBaseController } from "./cart_base_controller";
export default class extends CartBaseController { export default class extends CartBaseController {
static targets = [ 'counter' ] static targets = ["counter"];
connect () { connect() {
if (!this.hasCounterTarget) { if (!this.hasCounterTarget) {
console.error("Missing counter target") console.error("Missing counter target");
return return;
} }
window.addEventListener('cart:counter', event => this.counter = event.detail.item_count) window.addEventListener(
window.addEventListener('storage', event => { "cart:counter",
if (event.key == 'cart:counter') this.counter = event.newValue (event) => (this.counter = event.detail.item_count)
}) );
window.addEventListener("storage", (event) => {
if (event.key == "cart:counter") this.counter = event.newValue;
});
if (!this.cart) return if (!this.cart) return;
this.counter = this.cart.data.attributes.item_count this.counter = this.cart.data.attributes.item_count;
} }
set counter (quantity) { set counter(quantity) {
this.counterTarget.innerText = quantity this.counterTarget.innerText = quantity;
} }
} }

View file

@ -1,64 +1,73 @@
import { CartBaseController } from './cart_base_controller' import { CartBaseController } from "./cart_base_controller";
/* /*
* Retrieves shipping methods * Retrieves shipping methods
*/ */
export default class extends CartBaseController { export default class extends CartBaseController {
static targets = [ 'couponCodeInvalid', 'preDiscount', 'total' ] static targets = ["couponCodeInvalid", "preDiscount", "total"];
connect () { connect() {
this.couponCode.addEventListener('input', event => { this.couponCode.addEventListener("input", (event) => {
this.couponCode.parentElement.classList.remove('was-validated') this.couponCode.parentElement.classList.remove("was-validated");
this.couponCode.setCustomValidity('') this.couponCode.setCustomValidity("");
}) });
} }
get couponCode () { get couponCode() {
if (!this._couponCode) this._couponCode = this.element.elements.coupon_code if (!this._couponCode) this._couponCode = this.element.elements.coupon_code;
return this._couponCode return this._couponCode;
} }
get couponCodeInvalid () { get couponCodeInvalid() {
return this.hasCouponCodeInvalidTarget ? this.couponCodeInvalidTarget : document.querySelector('#coupon-code-invalid') return this.hasCouponCodeInvalidTarget
? this.couponCodeInvalidTarget
: document.querySelector("#coupon-code-invalid");
} }
get preDiscount () { get preDiscount() {
return this.hasPreDiscountTarget ? this.preDiscountTarget : document.querySelector('#pre-discount') return this.hasPreDiscountTarget
? this.preDiscountTarget
: document.querySelector("#pre-discount");
} }
get total () { get total() {
return this.hasTotalTarget ? this.totalTarget : document.querySelector('#total') return this.hasTotalTarget
? this.totalTarget
: document.querySelector("#total");
} }
set total (total) { set total(total) {
this.total.innerHTML = total this.total.innerHTML = total;
} }
async apply (event = undefined) { async apply(event = undefined) {
event?.preventDefault() event?.preventDefault();
event?.stopPropagation() event?.stopPropagation();
const orderToken = this.token const orderToken = this.token;
const coupon_code = this.couponCode.value const coupon_code = this.couponCode.value;
const include = 'line_items' const include = "line_items";
const response = await window.spree.cart.applyCouponCode({ orderToken }, { coupon_code, include }) const response = await window.spree.cart.applyCouponCode(
{ orderToken },
{ coupon_code, include }
);
this.element.elements.forEach(x => x.disabled = true) this.element.elements.forEach((x) => (x.disabled = true));
if (response.isFail()) { if (response.isFail()) {
this.couponCodeInvalid.innerHTML = response.fail().summary this.couponCodeInvalid.innerHTML = response.fail().summary;
this.couponCode.setCustomValidity(response.fail().summary) this.couponCode.setCustomValidity(response.fail().summary);
this.couponCode.parentElement.classList.add('was-validated') this.couponCode.parentElement.classList.add("was-validated");
this.element.elements.forEach(x => x.disabled = false) this.element.elements.forEach((x) => (x.disabled = false));
return return;
} }
this.cart = response this.cart = response;
this.total = response.success().data.attributes.total this.total = response.success().data.attributes.total;
this.preDiscount.classList.remove('d-none') this.preDiscount.classList.remove("d-none");
} }
} }

View file

@ -1,88 +1,92 @@
import { CartBaseController } from './cart_base_controller' import { CartBaseController } from "./cart_base_controller";
/* /*
* Retrieves payment methods and redirect to external checkouts * Retrieves payment methods and redirect to external checkouts
*/ */
export default class extends CartBaseController { export default class extends CartBaseController {
static targets = [ 'form', 'submit', 'specialInstructions' ] static targets = ["form", "submit", "specialInstructions"];
async connect () { async connect() {
const orderToken = this.token const orderToken = this.token;
const response = await this.spree.checkout.paymentMethods({ orderToken }) const response = await this.spree.checkout.paymentMethods({ orderToken });
if (response.isFail()) { if (response.isFail()) {
this.handleFailure(response) this.handleFailure(response);
return return;
} }
const payment_methods = response.success().data const payment_methods = response.success().data;
const site = window.site const site = window.site;
const cart = this.cart const cart = this.cart;
const next = { url: this.data.get('nextUrl') } const next = { url: this.data.get("nextUrl") };
const back = { url: this.data.get('backUrl') } const back = { url: this.data.get("backUrl") };
this.render({ payment_methods, site, cart, next, back }) this.render({ payment_methods, site, cart, next, back });
} }
async render (data = {}) { async render(data = {}) {
const template = window.templates[this.data.get('template')] const template = window.templates[this.data.get("template")];
this.element.innerHTML = await this.engine.parseAndRender(template, data) this.element.innerHTML = await this.engine.parseAndRender(template, data);
if (!this.hasSubmitTarget) return if (!this.hasSubmitTarget) return;
this.formTarget.elements.forEach(p => p.addEventListener('change', e => this.submitTarget.disabled = false)) this.formTarget.elements.forEach((p) =>
p.addEventListener("change", (e) => (this.submitTarget.disabled = false))
);
} }
async pay (event) { async pay(event) {
event.preventDefault() event.preventDefault();
event.stopPropagation() event.stopPropagation();
this.formDisabled = true this.formDisabled = true;
const payment_method_id = this.formTarget.elements.payment_method_id.value const payment_method_id = this.formTarget.elements.payment_method_id.value;
const orderToken = this.token const orderToken = this.token;
const special_instructions = this.specialInstructionsTarget.value.trim() const special_instructions = this.specialInstructionsTarget.value.trim();
// XXX: Currently SpreeClient expects us to send payment source // XXX: Currently SpreeClient expects us to send payment source
// attributes as if it were a credit card. // attributes as if it were a credit card.
let response = await this.spree.checkout.orderUpdate({ orderToken }, let response = await this.spree.checkout.orderUpdate(
{ orderToken },
{ {
order: { order: {
special_instructions, special_instructions,
payments_attributes: [{ payment_method_id }] }, payments_attributes: [{ payment_method_id }],
payment_source: { payment_source: {
[payment_method_id]: { [payment_method_id]: {
name: 'Pepitx', name: "Pepitx",
month: 12, month: 12,
year: 2020 year: 2020,
},
},
},
} }
} );
}
})
if (response.isFail()) { if (response.isFail()) {
this.handleFailure(response) this.handleFailure(response);
this.formDisabled = false this.formDisabled = false;
return return;
} }
this.cart = response this.cart = response;
response = await this.spree.checkout.complete({ orderToken }) response = await this.spree.checkout.complete({ orderToken });
if (response.isFail()) { if (response.isFail()) {
this.handleFailure(response) this.handleFailure(response);
this.formDisabled = false this.formDisabled = false;
return return;
} }
this.cart = response this.cart = response;
const checkoutUrls = await this.spree.sutty.getCheckoutURL({ orderToken }) const checkoutUrls = await this.spree.sutty.getCheckoutURL({ orderToken });
let redirectUrl = this.data.get('nextUrl') let redirectUrl = this.data.get("nextUrl");
if (checkoutUrls.data.length > 0) redirectUrl = checkoutUrls.data[0] if (checkoutUrls.data.length > 0) redirectUrl = checkoutUrls.data[0];
window.location = redirectUrl window.location = redirectUrl;
} }
} }

View file

@ -1,4 +1,4 @@
import { CartBaseController } from './cart_base_controller' import { CartBaseController } from "./cart_base_controller";
/* /*
* Replaces checkout.js. * Replaces checkout.js.
@ -9,15 +9,22 @@ import { CartBaseController } from './cart_base_controller'
* discarded. * discarded.
*/ */
export default class extends CartBaseController { export default class extends CartBaseController {
async connect () { async connect() {
if (this.params.PayerID === undefined) return if (this.params.PayerID === undefined) return;
this.site = window.site this.site = window.site;
this.element.innerHTML = this.site.i18n.cart.layouts.paypal.confirming this.element.innerHTML = this.site.i18n.cart.layouts.paypal.confirming;
fetch(this.executeURL) fetch(this.executeURL)
.then(r => this.element.innerHTML = this.site.i18n.cart.layouts.paypal[(r.ok ? 'confirmed' : 'failure')]) .then(
.catch(e => this.element.innerHTML = this.site.i18n.cart.layouts.paypal.failure) (r) =>
(this.element.innerHTML =
this.site.i18n.cart.layouts.paypal[r.ok ? "confirmed" : "failure"])
)
.catch(
(e) =>
(this.element.innerHTML = this.site.i18n.cart.layouts.paypal.failure)
);
} }
/* /*
@ -25,18 +32,29 @@ export default class extends CartBaseController {
* *
* @return [Object] * @return [Object]
*/ */
get params () { get params() {
if (this._params) return this._params if (this._params) return this._params;
this._params = Object.fromEntries(decodeURIComponent(window.location.search.replace('?', '')).split('&').map(x => x.split('='))) this._params = Object.fromEntries(
decodeURIComponent(window.location.search.replace("?", ""))
.split("&")
.map((x) => x.split("="))
);
return this._params return this._params;
} }
/* /*
* URL to contact the store and execute the payment. * URL to contact the store and execute the payment.
*/ */
get executeURL () { get executeURL() {
return [ window.spree.host, 'paypal', 'execute', this.params.orderId, this.params.paymentId, this.params.PayerID ].join('/') return [
window.spree.host,
"paypal",
"execute",
this.params.orderId,
this.params.paymentId,
this.params.PayerID,
].join("/");
} }
} }

View file

@ -1,110 +1,127 @@
import { CartBaseController } from './cart_base_controller' import { CartBaseController } from "./cart_base_controller";
export default class extends CartBaseController { export default class extends CartBaseController {
static targets = [ 'methods', 'rates', 'form' ] static targets = ["methods", "rates", "form"];
connect () { connect() {
this.formTarget.addEventListener('formdata', event => this.processShippingAddress(event.formData)) this.formTarget.addEventListener("formdata", (event) =>
this.processShippingAddress(event.formData)
);
} }
async rates (event) { async rates(event) {
event.preventDefault() event.preventDefault();
event.stopPropagation() event.stopPropagation();
if (!this.formTarget.checkValidity()) { if (!this.formTarget.checkValidity()) {
this.adressTarget.classList.add('was-validated') this.adressTarget.classList.add("was-validated");
return return;
} }
this.formTarget.classList.remove('was-validated') this.formTarget.classList.remove("was-validated");
// FormDataEvent es muy reciente // FormDataEvent es muy reciente
if (window.FormDataEvent) { if (window.FormDataEvent) {
// Esto lanza el evento formdata en el formulario // Esto lanza el evento formdata en el formulario
new FormData(event.target) new FormData(event.target);
} else { } else {
// Fallback // Fallback
this.processShippingAddress(new FormData(event.target)) this.processShippingAddress(new FormData(event.target));
} }
} }
payment (event) { payment(event) {
event.preventDefault() event.preventDefault();
event.stopPropagation() event.stopPropagation();
// FormDataEvent es muy reciente // FormDataEvent es muy reciente
if (window.FormDataEvent) { if (window.FormDataEvent) {
// Esto lanza el evento formdata en el formulario // Esto lanza el evento formdata en el formulario
new FormData(event.target) new FormData(event.target);
} else { } else {
this.processShippingRate(new FormData(event.target)) this.processShippingRate(new FormData(event.target));
} }
} }
async processShippingAddress (formData) { async processShippingAddress(formData) {
this.formDisabled = true this.formDisabled = true;
const email = this.email const email = this.email;
const orderToken = this.token const orderToken = this.token;
const ship_address_attributes = this.formDataToObject(formData) const ship_address_attributes = this.formDataToObject(formData);
const bill_address_attributes = ship_address_attributes const bill_address_attributes = ship_address_attributes;
const response = await this.spree.checkout.orderUpdate({ orderToken }, { order: { email, ship_address_attributes, bill_address_attributes }}) const response = await this.spree.checkout.orderUpdate(
{ orderToken },
{ order: { email, ship_address_attributes, bill_address_attributes } }
);
if (response.isFail()) { if (response.isFail()) {
this.handleFailure(response) this.handleFailure(response);
this.formDisabled = false this.formDisabled = false;
return return;
} }
const shippingMethods = await this.shippingMethods(orderToken) const shippingMethods = await this.shippingMethods(orderToken);
if (!shippingMethods) { if (!shippingMethods) {
this.formDisabled = false this.formDisabled = false;
return return;
} }
const shipping_rates = shippingMethods.included.filter(x => x.type == 'shipping_rate') const shipping_rates = shippingMethods.included.filter(
(x) => x.type == "shipping_rate"
);
// XXX: No hay varios paquetes // XXX: No hay varios paquetes
const shipping_method = shippingMethods.data[0] const shipping_method = shippingMethods.data[0];
const site = window.site const site = window.site;
await this.render({ shipping_method, shipping_rates, site }) await this.render({ shipping_method, shipping_rates, site });
const nextStep = document.querySelector(`#${this.element.dataset.scrollTo}`) const nextStep = document.querySelector(
if (nextStep) nextStep.scrollIntoView() `#${this.element.dataset.scrollTo}`
);
if (nextStep) nextStep.scrollIntoView();
} }
async processShippingRate (formData) { async processShippingRate(formData) {
const rate = this.formDataToObject(formData) const rate = this.formDataToObject(formData);
const orderToken = this.token const orderToken = this.token;
// XXX: Deshabilitar el formulario después del evento FormData, de // XXX: Deshabilitar el formulario después del evento FormData, de
// lo contrario el objeto queda vacío. // lo contrario el objeto queda vacío.
this.ratesTarget.elements.forEach(x => x.disabled = true) this.ratesTarget.elements.forEach((x) => (x.disabled = true));
const response = await window.spree.checkout.orderUpdate({ orderToken }, { order: { shipments_attributes: [{ ...rate }] } }) const response = await window.spree.checkout.orderUpdate(
{ orderToken },
{ order: { shipments_attributes: [{ ...rate }] } }
);
if (response.isFail()) { if (response.isFail()) {
this.handleFailure(response) this.handleFailure(response);
return return;
} }
this.cart = response this.cart = response;
// Continue to next step // Continue to next step
try { try {
Turbolinks.visit(this.data.get('next')) Turbolinks.visit(this.data.get("next"));
} catch { } catch {
window.location = this.data.get('next') window.location = this.data.get("next");
} }
} }
async render (data = {}) { async render(data = {}) {
const template = window.templates[this.data.get('template')] const template = window.templates[this.data.get("template")];
this.methodsTarget.innerHTML = await this.engine.parseAndRender(template, data) this.methodsTarget.innerHTML = await this.engine.parseAndRender(
this.ratesTarget.addEventListener('formdata', event => this.processShippingRate(event.formData)) template,
data
);
this.ratesTarget.addEventListener("formdata", (event) =>
this.processShippingRate(event.formData)
);
} }
} }

View file

@ -1,42 +1,42 @@
import { Controller } from 'stimulus' import { Controller } from "stimulus";
/* /*
* Sólo permite enviar el formulario de contacto después de unos * Sólo permite enviar el formulario de contacto después de unos
* segundos, para evitar el spam. * segundos, para evitar el spam.
*/ */
export default class extends Controller { export default class extends Controller {
static targets = [ 'submit' ] static targets = ["submit"];
connect () { connect() {
if (!this.hasSubmitTarget) return if (!this.hasSubmitTarget) return;
this.submitTarget.disabled = true this.submitTarget.disabled = true;
this._value = this.submitTarget.value this._value = this.submitTarget.value;
// Esperar un minuto desde que se carga la página hasta que se // Esperar un minuto desde que se carga la página hasta que se
// permite enviar. // permite enviar.
this._interval = setInterval(() => { this._interval = setInterval(() => {
const delay = this.delay const delay = this.delay;
if (this.delay == 0) { if (this.delay == 0) {
clearInterval(this._interval) clearInterval(this._interval);
this.submitTarget.disabled = false this.submitTarget.disabled = false;
this.submitTarget.value = this._value this.submitTarget.value = this._value;
} else { } else {
this.delay = delay - 1 this.delay = delay - 1;
} }
}, 1000) }, 1000);
} }
get delay () { get delay() {
const delay = parseInt(this.element.dataset.delay) const delay = parseInt(this.element.dataset.delay);
return isNaN(delay) ? 0 : delay return isNaN(delay) ? 0 : delay;
} }
set delay (value) { set delay(value) {
this.element.dataset.delay = value this.element.dataset.delay = value;
this.submitTarget.value = `${this._value} (${value})` this.submitTarget.value = `${this._value} (${value})`;
} }
} }

View file

@ -1,4 +1,4 @@
import { CartBaseController } from './cart_base_controller' import { CartBaseController } from "./cart_base_controller";
/* /*
* Populates a country field where users can type to filter and select * Populates a country field where users can type to filter and select
@ -6,96 +6,104 @@ import { CartBaseController } from './cart_base_controller'
*/ */
export default class extends CartBaseController { export default class extends CartBaseController {
// All are required! // All are required!
static targets = [ 'id', 'iso', 'list', 'name' ] static targets = ["id", "iso", "list", "name"];
async connect () { async connect() {
const countries = await this.countries() const countries = await this.countries();
countries.forEach(country => { countries.forEach((country) => {
const option = document.createElement('option') const option = document.createElement("option");
option.value = country.attributes.name option.value = country.attributes.name;
option.dataset.id = country.id option.dataset.id = country.id;
option.dataset.iso = country.attributes.iso option.dataset.iso = country.attributes.iso;
option.dataset.statesRequired = country.attributes.states_required option.dataset.statesRequired = country.attributes.states_required;
option.dataset.zipcodeRequired = country.attributes.zipcode_required option.dataset.zipcodeRequired = country.attributes.zipcode_required;
this.listTarget.appendChild(option) this.listTarget.appendChild(option);
}) });
const site = window.site const site = window.site;
// Only allow names on this list // Only allow names on this list
this.nameTarget.pattern = countries.map(x => x.attributes.name).join('|') this.nameTarget.pattern = countries.map((x) => x.attributes.name).join("|");
this.nameTarget.addEventListener('input', event => this.nameTarget.setCustomValidity('')) this.nameTarget.addEventListener("input", (event) =>
this.nameTarget.addEventListener('invalid', event => this.nameTarget.setCustomValidity(site.i18n.countries.validation)) this.nameTarget.setCustomValidity("")
);
this.nameTarget.addEventListener("invalid", (event) =>
this.nameTarget.setCustomValidity(site.i18n.countries.validation)
);
// When the input changes we update the actual value and also the // When the input changes we update the actual value and also the
// state list via an Event // state list via an Event
this.nameTarget.addEventListener('change', event => { this.nameTarget.addEventListener("change", (event) => {
const value = this.nameTarget.value.trim() const value = this.nameTarget.value.trim();
if (value === '') return if (value === "") return;
const options = Array.from(this.nameTarget.list.options) const options = Array.from(this.nameTarget.list.options);
const option = options.find(x => x.value == value) const option = options.find((x) => x.value == value);
// TODO: If no option is found, mark the field as invalid // TODO: If no option is found, mark the field as invalid
if (!option) return if (!option) return;
this.idTarget.value = option.dataset.id this.idTarget.value = option.dataset.id;
this.isoTarget.value = option.dataset.iso this.isoTarget.value = option.dataset.iso;
this.idTarget.dispatchEvent(new Event('change')) this.idTarget.dispatchEvent(new Event("change"));
this.isoTarget.dispatchEvent(new Event('change')) this.isoTarget.dispatchEvent(new Event("change"));
this.dispatchChangedEvent(option.dataset) this.dispatchChangedEvent(option.dataset);
// XXX: Prevent mixing data // XXX: Prevent mixing data
delete this.nameTarget.dataset.selectedState delete this.nameTarget.dataset.selectedState;
delete this.nameTarget.dataset.selectedZipcode delete this.nameTarget.dataset.selectedZipcode;
}) });
// The input is disabled at this point // The input is disabled at this point
this.nameTarget.disabled = false this.nameTarget.disabled = false;
// Load data if the input is autocompleted // Load data if the input is autocompleted
if (this.nameTarget.value.trim() !== '') this.nameTarget.dispatchEvent(new CustomEvent('change')) if (this.nameTarget.value.trim() !== "")
this.nameTarget.dispatchEvent(new CustomEvent("change"));
} }
/* /*
* Sends a `cart:country:update` event so other controllers can * Sends a `cart:country:update` event so other controllers can
* subscribe to changes. * subscribe to changes.
*/ */
dispatchChangedEvent (data = {}) { dispatchChangedEvent(data = {}) {
const event = new CustomEvent('cart:country:update', { const event = new CustomEvent("cart:country:update", {
detail: { detail: {
id: this.idTarget.value, id: this.idTarget.value,
iso: this.isoTarget.value, iso: this.isoTarget.value,
group: this.data.get('group'), group: this.data.get("group"),
selectedState: this.nameTarget.dataset.selectedState, selectedState: this.nameTarget.dataset.selectedState,
selectedZipcode: this.nameTarget.dataset.selectedZipcode, selectedZipcode: this.nameTarget.dataset.selectedZipcode,
data data,
} },
}) });
window.dispatchEvent(event) window.dispatchEvent(event);
} }
/* /*
* Fetch the country list from storage or from API * Fetch the country list from storage or from API
*/ */
async countries () { async countries() {
const countries = JSON.parse(this.storageTemp.getItem('countries')) const countries = JSON.parse(this.storageTemp.getItem("countries"));
if (countries) return countries if (countries) return countries;
const response = await this.spree.countries.list() const response = await this.spree.countries.list();
// TODO: Show error message // TODO: Show error message
if (!response.success()) return if (!response.success()) return;
this.storageTemp.setItem('countries', JSON.stringify(response.success().data)) this.storageTemp.setItem(
"countries",
JSON.stringify(response.success().data)
);
return response.success().data return response.success().data;
} }
} }

View file

@ -1,18 +1,18 @@
import { Controller } from 'stimulus' import { Controller } from "stimulus";
export default class extends Controller { export default class extends Controller {
static targets = [ 'content' ] static targets = ["content"];
connect () { connect() {
window.addEventListener('toast', event => { window.addEventListener("toast", (event) => {
this.contentTarget.innerText = event.detail.content this.contentTarget.innerText = event.detail.content;
this.element.classList.toggle('hide') this.element.classList.toggle("hide");
this.element.classList.toggle('show') this.element.classList.toggle("show");
setTimeout(() => { setTimeout(() => {
this.element.classList.toggle('hide') this.element.classList.toggle("hide");
this.element.classList.toggle('show') this.element.classList.toggle("show");
}, 3000) }, 3000);
}) });
} }
} }

View file

@ -1,29 +1,31 @@
import { Controller } from 'stimulus' import { Controller } from "stimulus";
export default class extends Controller { export default class extends Controller {
static targets = [ 'item' ] static targets = ["item"];
connect () { connect() {
window.addEventListener('scroll:section', event => this.update(event.detail.id)) window.addEventListener("scroll:section", (event) =>
this.update(event.detail.id)
);
} }
get items () { get items() {
if (!this._items) { if (!this._items) {
this._items = {} this._items = {};
for (const item of this.itemTargets) { for (const item of this.itemTargets) {
this._items[item.href.split('#')[1]] = item this._items[item.href.split("#")[1]] = item;
} }
} }
return this._items return this._items;
} }
update (id) { update(id) {
for (const item of Object.values(this.items)) { for (const item of Object.values(this.items)) {
item.classList.remove('active') item.classList.remove("active");
} }
if (this.items[id]) this.items[id].classList.add('active') if (this.items[id]) this.items[id].classList.add("active");
} }
} }

View file

@ -1,43 +1,47 @@
import { Controller } from 'stimulus' import { Controller } from "stimulus";
import { Liquid } from 'liquidjs' import { Liquid } from "liquidjs";
/* /*
* Waits for notifications and shows them by fetching and rendering * Waits for notifications and shows them by fetching and rendering
* a template. * a template.
*/ */
export default class extends Controller { export default class extends Controller {
connect () { connect() {
window.addEventListener('notification', async event => await this.render(event.detail.template, event.detail.data)) window.addEventListener(
"notification",
async (event) =>
await this.render(event.detail.template, event.detail.data)
);
} }
/* /*
* Renders and replaces notification contents and then shows it. Does * Renders and replaces notification contents and then shows it. Does
* nothing if the template isn't found. * nothing if the template isn't found.
*/ */
async render (name, data = {}) { async render(name, data = {}) {
data.site = window.site data.site = window.site;
const template = window.templates.alert const template = window.templates.alert;
const html = await this.engine.parseAndRender(template, data) const html = await this.engine.parseAndRender(template, data);
this.element.innerHTML = html this.element.innerHTML = html;
this.show() this.show();
} }
/* /*
* Shows the notification * Shows the notification
*/ */
show () { show() {
this.element.classList.add('show') this.element.classList.add("show");
this.element.classList.remove('hide') this.element.classList.remove("hide");
} }
/* /*
* Hides the notification * Hides the notification
*/ */
hide () { hide() {
this.element.classList.add('hide') this.element.classList.add("hide");
this.element.classList.remove('show') this.element.classList.remove("show");
} }
/* /*
@ -45,9 +49,9 @@ export default class extends Controller {
* *
* @return Liquid * @return Liquid
*/ */
get engine () { get engine() {
if (!window.liquid) window.liquid = new Liquid() if (!window.liquid) window.liquid = new Liquid();
return window.liquid return window.liquid;
} }
} }

View file

@ -1,4 +1,4 @@
import { CartBaseController } from './cart_base_controller' import { CartBaseController } from "./cart_base_controller";
/* /*
* Renders the order table. All products are stored on localStorage, so * Renders the order table. All products are stored on localStorage, so
@ -6,46 +6,48 @@ import { CartBaseController } from './cart_base_controller'
* Liquid. * Liquid.
*/ */
export default class extends CartBaseController { export default class extends CartBaseController {
static targets = [ 'cart', 'subtotal', 'itemCount' ] static targets = ["cart", "subtotal", "itemCount"];
async connect () { async connect() {
const products = this.products const products = this.products;
const site = window.site const site = window.site;
this.render({ products, site }) this.render({ products, site });
this.subtotalUpdate() this.subtotalUpdate();
this.itemCountUpdate() this.itemCountUpdate();
this.subscribe() this.subscribe();
} }
/* /*
* Subscribe to change on the storage to update the cart. * Subscribe to change on the storage to update the cart.
*/ */
subscribe () { subscribe() {
window.addEventListener('storage', async event => { window.addEventListener("storage", async (event) => {
if (!event.key?.startsWith('cart:item:')) return if (!event.key?.startsWith("cart:item:")) return;
const products = this.products const products = this.products;
const site = window.site const site = window.site;
this.render({ products, site }) this.render({ products, site });
this.subtotalUpdate() this.subtotalUpdate();
this.itemCountUpdate() this.itemCountUpdate();
}) });
window.addEventListener('cart:subtotal:update', event => { window.addEventListener("cart:subtotal:update", (event) => {
this.itemCountUpdate() this.itemCountUpdate();
this.subtotalUpdate() this.subtotalUpdate();
}) });
} }
/* /*
* Download the item template and render the order * Download the item template and render the order
*/ */
render (data = {}) { render(data = {}) {
const template = window.templates[this.data.get('itemTemplate')] const template = window.templates[this.data.get("itemTemplate")];
this.engine.parseAndRender(template, data).then(html => this.cartTarget.innerHTML = html) this.engine
.parseAndRender(template, data)
.then((html) => (this.cartTarget.innerHTML = html));
} }
/* /*
@ -53,16 +55,16 @@ export default class extends CartBaseController {
* *
* XXX: This also updates the currency * XXX: This also updates the currency
*/ */
subtotalUpdate () { subtotalUpdate() {
if (!this.cart) return if (!this.cart) return;
this.subtotalTarget.innerText = this.cart.data.attributes.display_total this.subtotalTarget.innerText = this.cart.data.attributes.display_total;
} }
itemCountUpdate () { itemCountUpdate() {
if (!this.hasItemCountTarget) return if (!this.hasItemCountTarget) return;
if (!this.cart) return if (!this.cart) return;
this.itemCountTarget.innerText = this.cart.data.attributes.item_count this.itemCountTarget.innerText = this.cart.data.attributes.item_count;
} }
} }

View file

@ -1,4 +1,4 @@
import { CartBaseController } from './cart_base_controller' import { CartBaseController } from "./cart_base_controller";
/* /*
* Al pagar lo que podamos, primero hay que crear una orden y luego * Al pagar lo que podamos, primero hay que crear una orden y luego
@ -7,29 +7,29 @@ import { CartBaseController } from './cart_base_controller'
* proceso de pago. * proceso de pago.
*/ */
export default class extends CartBaseController { export default class extends CartBaseController {
static targets = [ 'form' ] static targets = ["form"];
static values = { static values = {
variantId: Number, variantId: Number,
currency: String, currency: String,
price: Number, price: Number,
firstname: String firstname: String,
} };
connect () { connect() {
this.paymentMethodByCurrency = { this.paymentMethodByCurrency = {
ARS: 'Spree::PaymentMethod::MercadoPago', ARS: "Spree::PaymentMethod::MercadoPago",
USD: 'Spree::PaymentMethod::Paypal' USD: "Spree::PaymentMethod::Paypal",
} };
} }
store (event) { store(event) {
const target = event.currentTarget || event.target const target = event.currentTarget || event.target;
this[`${target.dataset.name}Value`] = target.value this[`${target.dataset.name}Value`] = target.value;
} }
set formDisable (disable) { set formDisable(disable) {
this.formTarget.elements.forEach(x => x.disabled = disable) this.formTarget.elements.forEach((x) => (x.disabled = disable));
} }
/* /*
@ -45,162 +45,204 @@ export default class extends CartBaseController {
* * Reenviar a confirmación * * Reenviar a confirmación
* * Ejecutar el pago (si aplica) * * Ejecutar el pago (si aplica)
*/ */
async pay (event = undefined) { async pay(event = undefined) {
if (event) { if (event) {
event.preventDefault() event.preventDefault();
event.stopPropagation() event.stopPropagation();
} }
if (!this.formTarget.checkValidity()) { if (!this.formTarget.checkValidity()) {
this.formTarget.classList.add('was-validated') this.formTarget.classList.add("was-validated");
return return;
} }
this.formDisable = true this.formDisable = true;
// Crear pedido. Todos los pedidos van a ser hechos desde // Crear pedido. Todos los pedidos van a ser hechos desde
// Argentina, no hay forma de cambiar esto. // Argentina, no hay forma de cambiar esto.
const orderToken = await this.tempCartCreate() const orderToken = await this.tempCartCreate();
const quantity = 1 const quantity = 1;
const include = 'line_items' const include = "line_items";
const currency = this.currencyValue const currency = this.currencyValue;
const price = this.priceValue const price = this.priceValue;
const email = 'noreply@sutty.nl' const email = "noreply@sutty.nl";
const firstname = this.firstnameValue const firstname = this.firstnameValue;
const lastname = '-' const lastname = "-";
const address1 = '-' const address1 = "-";
const country_id = 250 // XXX: Internet const country_id = 250; // XXX: Internet
const city = '-' const city = "-";
const phone = '11111111' const phone = "11111111";
const zipcode = '1111' const zipcode = "1111";
const ship_address_attributes = { firstname, lastname, address1, city, country_id, zipcode, phone } const ship_address_attributes = {
const bill_address_attributes = ship_address_attributes firstname,
const confirmation_delivered = true lastname,
const custom_return_url = this.customReturnUrl() address1,
city,
country_id,
zipcode,
phone,
};
const bill_address_attributes = ship_address_attributes;
const confirmation_delivered = true;
const custom_return_url = this.customReturnUrl();
let variant_id = this.variantIdValue let variant_id = this.variantIdValue;
// Crear la variante // Crear la variante
const payWhatYouCanResponse = await this.spree.sutty.payWhatYouCan({ orderToken }, { variant_id, price, currency, quantity }) const payWhatYouCanResponse = await this.spree.sutty.payWhatYouCan(
{ orderToken },
{ variant_id, price, currency, quantity }
);
variant_id = payWhatYouCanResponse.data.id variant_id = payWhatYouCanResponse.data.id;
if (!variant_id) { if (!variant_id) {
this.formDisable = false this.formDisable = false;
console.error('No se pudo generar la variante', { variant_id, price, currency, quantity }) console.error("No se pudo generar la variante", {
return variant_id,
price,
currency,
quantity,
});
return;
} }
// Configurar la moneda del pedido // Configurar la moneda del pedido
let response = await this.spree.sutty.updateOrder({ orderToken }, { currency, confirmation_delivered, custom_return_url }) let response = await this.spree.sutty.updateOrder(
{ orderToken },
{ currency, confirmation_delivered, custom_return_url }
);
if (response.status > 299) { if (response.status > 299) {
console.error(response) console.error(response);
this.formDisable = false this.formDisable = false;
return return;
} }
// Agregar al carrito // Agregar al carrito
response = await this.spree.cart.addItem({ orderToken }, { variant_id, quantity, include }) response = await this.spree.cart.addItem(
{ orderToken },
{ variant_id, quantity, include }
);
if (response.isFail()) { if (response.isFail()) {
this.handleFailure(response) this.handleFailure(response);
this.formDisable = false this.formDisable = false;
return return;
} }
// Actualizar la dirección // Actualizar la dirección
response = await this.spree.checkout.orderUpdate({ orderToken }, { order: { email, ship_address_attributes, bill_address_attributes }}) response = await this.spree.checkout.orderUpdate(
{ orderToken },
{ order: { email, ship_address_attributes, bill_address_attributes } }
);
if (response.isFail()) { if (response.isFail()) {
this.handleFailure(response) this.handleFailure(response);
this.formDisable = false this.formDisable = false;
return return;
} }
// Obtener los medios de envío // Obtener los medios de envío
response = await this.spree.checkout.shippingMethods({ orderToken }, { include: 'shipping_rates' }) response = await this.spree.checkout.shippingMethods(
{ orderToken },
{ include: "shipping_rates" }
);
if (response.isFail()) { if (response.isFail()) {
this.handleFailure(response) this.handleFailure(response);
this.formDisable = false this.formDisable = false;
return return;
} }
// Elegir medio de envío // Elegir medio de envío
response = await this.spree.checkout.orderUpdate({ orderToken }, { response = await this.spree.checkout.orderUpdate(
{ orderToken },
{
order: { order: {
shipments_attributes: [{ shipments_attributes: [
{
id: response.success().data[0].id, id: response.success().data[0].id,
selected_shipping_rate_id: response.success().included.filter(x => x.type == 'shipping_rate')[0].id selected_shipping_rate_id: response
}] .success()
.included.filter((x) => x.type == "shipping_rate")[0].id,
},
],
},
} }
}) );
// Elegir medio de pago // Elegir medio de pago
response = await this.spree.checkout.paymentMethods({ orderToken }) response = await this.spree.checkout.paymentMethods({ orderToken });
if (response.isFail()) { if (response.isFail()) {
this.handleFailure(response) this.handleFailure(response);
this.formDisable = false this.formDisable = false;
return return;
} }
const payment_method_id = response.success().data.find(x => this.paymentMethodByCurrency[this.currencyValue] == x.attributes.type).id const payment_method_id = response
.success()
.data.find(
(x) =>
this.paymentMethodByCurrency[this.currencyValue] == x.attributes.type
).id;
response = await this.spree.checkout.orderUpdate({ orderToken }, response = await this.spree.checkout.orderUpdate(
{ orderToken },
{ {
order: { payments_attributes: [{ payment_method_id }] }, order: { payments_attributes: [{ payment_method_id }] },
payment_source: { payment_source: {
[payment_method_id]: { [payment_method_id]: {
name: 'Pepitx', name: "Pepitx",
month: 12, month: 12,
year: 2021 year: 2021,
},
},
} }
} );
})
if (response.isFail()) { if (response.isFail()) {
this.handleFailure(response) this.handleFailure(response);
this.formDisable = false this.formDisable = false;
return return;
} }
response = await this.spree.checkout.complete({ orderToken }) response = await this.spree.checkout.complete({ orderToken });
if (response.isFail()) { if (response.isFail()) {
this.handleFailure(response) this.handleFailure(response);
this.formDisable = false this.formDisable = false;
return return;
} }
// Reenviar al medio de pago // Reenviar al medio de pago
const checkoutUrls = await this.spree.sutty.getCheckoutURL({ orderToken }) const checkoutUrls = await this.spree.sutty.getCheckoutURL({ orderToken });
const redirectUrl = checkoutUrls.data[0] const redirectUrl = checkoutUrls.data[0];
Turbolinks.visit(redirectUrl) Turbolinks.visit(redirectUrl);
// Volver // Volver
} }
async tempCartCreate () { async tempCartCreate() {
const response = await this.spree.cart.create() const response = await this.spree.cart.create();
// If we fail here it's probably a server error, so we inform the // If we fail here it's probably a server error, so we inform the
// user. // user.
if (response.isFail()) { if (response.isFail()) {
this.handleFailure(response) this.handleFailure(response);
return return;
} }
return response.success().data.attributes.token return response.success().data.attributes.token;
} }
// @return [String] // @return [String]
customReturnUrl () { customReturnUrl() {
const url = new URL(window.location.href) const url = new URL(window.location.href);
url.searchParams.set('open', '') url.searchParams.set("open", "");
return url.toString() return url.toString();
} }
} }

View file

@ -1,11 +1,11 @@
import { Controller } from 'stimulus' import { Controller } from "stimulus";
/* /*
* Subscribes to the country change event and changes the validation * Subscribes to the country change event and changes the validation
* pattern of its input. * pattern of its input.
*/ */
export default class extends Controller { export default class extends Controller {
static targets = [ 'code' ] static targets = ["code"];
/* /*
* Twitter CLDR is pretty big and we only need the postal codes * Twitter CLDR is pretty big and we only need the postal codes
@ -13,26 +13,186 @@ export default class extends Controller {
* *
* @see {https://github.com/twitter/twitter-cldr-npm/blob/4388dfc55900b0feb80eafcac030f9f26981a41d/full/core.js#L1999} * @see {https://github.com/twitter/twitter-cldr-npm/blob/4388dfc55900b0feb80eafcac030f9f26981a41d/full/core.js#L1999}
*/ */
postal_codes = {"ad":"^AD\\d{3}$","am":"^(37)?\\d{4}$","ar":"^([A-HJ-NP-Z])?\\d{4}([A-Z]{3})?$","as":"^96799$","at":"^\\d{4}$","au":"^\\d{4}$","ax":"^22\\d{3}$","az":"^\\d{4}$","ba":"^\\d{5}$","bb":"^(BB\\d{5})?$","bd":"^\\d{4}$","be":"^\\d{4}$","bg":"^\\d{4}$","bh":"^((1[0-2]|[2-9])\\d{2})?$","bm":"^[A-Z]{2}[ ]?[A-Z0-9]{2}$","bn":"^[A-Z]{2}[ ]?\\d{4}$","br":"^\\d{5}[\\-]?\\d{3}$","by":"^\\d{6}$","ca":"^[ABCEGHJKLMNPRSTVXY]\\d[ABCEGHJ-NPRSTV-Z][ ]?\\d[ABCEGHJ-NPRSTV-Z]\\d$","cc":"^6799$","ch":"^\\d{4}$","ck":"^\\d{4}$","cl":"^\\d{7}$","cn":"^\\d{6}$","cr":"^\\d{4,5}|\\d{3}-\\d{4}$","cs":"^\\d{5}$","cv":"^\\d{4}$","cx":"^6798$","cy":"^\\d{4}$","cz":"^\\d{3}[ ]?\\d{2}$","de":"^\\d{5}$","dk":"^\\d{4}$","do":"^\\d{5}$","dz":"^\\d{5}$","ec":"^([A-Z]\\d{4}[A-Z]|(?:[A-Z]{2})?\\d{6})?$","ee":"^\\d{5}$","eg":"^\\d{5}$","es":"^\\d{5}$","et":"^\\d{4}$","fi":"^\\d{5}$","fk":"^FIQQ 1ZZ$","fm":"^(9694[1-4])([ \\-]\\d{4})?$","fo":"^\\d{3}$","fr":"^\\d{2}[ ]?\\d{3}$","gb":"^GIR[ ]?0AA|((AB|AL|B|BA|BB|BD|BH|BL|BN|BR|BS|BT|CA|CB|CF|CH|CM|CO|CR|CT|CV|CW|DA|DD|DE|DG|DH|DL|DN|DT|DY|E|EC|EH|EN|EX|FK|FY|G|GL|GY|GU|HA|HD|HG|HP|HR|HS|HU|HX|IG|IM|IP|IV|JE|KA|KT|KW|KY|L|LA|LD|LE|LL|LN|LS|LU|M|ME|MK|ML|N|NE|NG|NN|NP|NR|NW|OL|OX|PA|PE|PH|PL|PO|PR|RG|RH|RM|S|SA|SE|SG|SK|SL|SM|SN|SO|SP|SR|SS|ST|SW|SY|TA|TD|TF|TN|TQ|TR|TS|TW|UB|W|WA|WC|WD|WF|WN|WR|WS|WV|YO|ZE)(\\d[\\dA-Z]?[ ]?\\d[ABD-HJLN-UW-Z]{2}))|BFPO[ ]?\\d{1,4}$","ge":"^\\d{4}$","gf":"^9[78]3\\d{2}$","gg":"^GY\\d[\\dA-Z]?[ ]?\\d[ABD-HJLN-UW-Z]{2}$","gl":"^39\\d{2}$","gn":"^\\d{3}$","gp":"^9[78][01]\\d{2}$","gr":"^\\d{3}[ ]?\\d{2}$","gs":"^SIQQ 1ZZ$","gt":"^\\d{5}$","gu":"^969[123]\\d([ \\-]\\d{4})?$","gw":"^\\d{4}$","hm":"^\\d{4}$","hn":"^(?:\\d{5})?$","hr":"^\\d{5}$","ht":"^\\d{4}$","hu":"^\\d{4}$","id":"^\\d{5}$","il":"^\\d{5}$","im":"^IM\\d[\\dA-Z]?[ ]?\\d[ABD-HJLN-UW-Z]{2}$","in":"^\\d{6}$","io":"^BBND 1ZZ$","iq":"^\\d{5}$","is":"^\\d{3}$","it":"^\\d{5}$","je":"^JE\\d[\\dA-Z]?[ ]?\\d[ABD-HJLN-UW-Z]{2}$","jo":"^\\d{5}$","jp":"^\\d{3}-\\d{4}$","ke":"^\\d{5}$","kg":"^\\d{6}$","kh":"^\\d{5}$","kr":"^\\d{3}[\\-]\\d{3}$","kw":"^\\d{5}$","kz":"^\\d{6}$","la":"^\\d{5}$","lb":"^(\\d{4}([ ]?\\d{4})?)?$","li":"^(948[5-9])|(949[0-7])$","lk":"^\\d{5}$","lr":"^\\d{4}$","ls":"^\\d{3}$","lt":"^\\d{5}$","lu":"^\\d{4}$","lv":"^\\d{4}$","ma":"^\\d{5}$","mc":"^980\\d{2}$","md":"^\\d{4}$","me":"^8\\d{4}$","mg":"^\\d{3}$","mh":"^969[67]\\d([ \\-]\\d{4})?$","mk":"^\\d{4}$","mn":"^\\d{6}$","mp":"^9695[012]([ \\-]\\d{4})?$","mq":"^9[78]2\\d{2}$","mt":"^[A-Z]{3}[ ]?\\d{2,4}$","mu":"^(\\d{3}[A-Z]{2}\\d{3})?$","mv":"^\\d{5}$","mx":"^\\d{5}$","my":"^\\d{5}$","nc":"^988\\d{2}$","ne":"^\\d{4}$","nf":"^2899$","ng":"^(\\d{6})?$","ni":"^((\\d{4}-)?\\d{3}-\\d{3}(-\\d{1})?)?$","nl":"^\\d{4}[ ]?[A-Z]{2}$","no":"^\\d{4}$","np":"^\\d{5}$","nz":"^\\d{4}$","om":"^(PC )?\\d{3}$","pf":"^987\\d{2}$","pg":"^\\d{3}$","ph":"^\\d{4}$","pk":"^\\d{5}$","pl":"^\\d{2}-\\d{3}$","pm":"^9[78]5\\d{2}$","pn":"^PCRN 1ZZ$","pr":"^00[679]\\d{2}([ \\-]\\d{4})?$","pt":"^\\d{4}([\\-]\\d{3})?$","pw":"^96940$","py":"^\\d{4}$","re":"^9[78]4\\d{2}$","ro":"^\\d{6}$","rs":"^\\d{6}$","ru":"^\\d{6}$","sa":"^\\d{5}$","se":"^\\d{3}[ ]?\\d{2}$","sg":"^\\d{6}$","sh":"^(ASCN|STHL) 1ZZ$","si":"^\\d{4}$","sj":"^\\d{4}$","sk":"^\\d{3}[ ]?\\d{2}$","sm":"^4789\\d$","sn":"^\\d{5}$","so":"^\\d{5}$","sz":"^[HLMS]\\d{3}$","tc":"^TKCA 1ZZ$","th":"^\\d{5}$","tj":"^\\d{6}$","tm":"^\\d{6}$","tn":"^\\d{4}$","tr":"^\\d{5}$","tw":"^\\d{3}(\\d{2})?$","ua":"^\\d{5}$","us":"^\\d{5}([ \\-]\\d{4})?$","uy":"^\\d{5}$","uz":"^\\d{6}$","va":"^00120$","ve":"^\\d{4}$","vi":"^008(([0-4]\\d)|(5[01]))([ \\-]\\d{4})?$","wf":"^986\\d{2}$","xk":"^\\d{5}$","yt":"^976\\d{2}$","yu":"^\\d{5}$","za":"^\\d{4}$","zm":"^\\d{5}$"} postal_codes = {
ad: "^AD\\d{3}$",
am: "^(37)?\\d{4}$",
ar: "^([A-HJ-NP-Z])?\\d{4}([A-Z]{3})?$",
as: "^96799$",
at: "^\\d{4}$",
au: "^\\d{4}$",
ax: "^22\\d{3}$",
az: "^\\d{4}$",
ba: "^\\d{5}$",
bb: "^(BB\\d{5})?$",
bd: "^\\d{4}$",
be: "^\\d{4}$",
bg: "^\\d{4}$",
bh: "^((1[0-2]|[2-9])\\d{2})?$",
bm: "^[A-Z]{2}[ ]?[A-Z0-9]{2}$",
bn: "^[A-Z]{2}[ ]?\\d{4}$",
br: "^\\d{5}[\\-]?\\d{3}$",
by: "^\\d{6}$",
ca: "^[ABCEGHJKLMNPRSTVXY]\\d[ABCEGHJ-NPRSTV-Z][ ]?\\d[ABCEGHJ-NPRSTV-Z]\\d$",
cc: "^6799$",
ch: "^\\d{4}$",
ck: "^\\d{4}$",
cl: "^\\d{7}$",
cn: "^\\d{6}$",
cr: "^\\d{4,5}|\\d{3}-\\d{4}$",
cs: "^\\d{5}$",
cv: "^\\d{4}$",
cx: "^6798$",
cy: "^\\d{4}$",
cz: "^\\d{3}[ ]?\\d{2}$",
de: "^\\d{5}$",
dk: "^\\d{4}$",
do: "^\\d{5}$",
dz: "^\\d{5}$",
ec: "^([A-Z]\\d{4}[A-Z]|(?:[A-Z]{2})?\\d{6})?$",
ee: "^\\d{5}$",
eg: "^\\d{5}$",
es: "^\\d{5}$",
et: "^\\d{4}$",
fi: "^\\d{5}$",
fk: "^FIQQ 1ZZ$",
fm: "^(9694[1-4])([ \\-]\\d{4})?$",
fo: "^\\d{3}$",
fr: "^\\d{2}[ ]?\\d{3}$",
gb: "^GIR[ ]?0AA|((AB|AL|B|BA|BB|BD|BH|BL|BN|BR|BS|BT|CA|CB|CF|CH|CM|CO|CR|CT|CV|CW|DA|DD|DE|DG|DH|DL|DN|DT|DY|E|EC|EH|EN|EX|FK|FY|G|GL|GY|GU|HA|HD|HG|HP|HR|HS|HU|HX|IG|IM|IP|IV|JE|KA|KT|KW|KY|L|LA|LD|LE|LL|LN|LS|LU|M|ME|MK|ML|N|NE|NG|NN|NP|NR|NW|OL|OX|PA|PE|PH|PL|PO|PR|RG|RH|RM|S|SA|SE|SG|SK|SL|SM|SN|SO|SP|SR|SS|ST|SW|SY|TA|TD|TF|TN|TQ|TR|TS|TW|UB|W|WA|WC|WD|WF|WN|WR|WS|WV|YO|ZE)(\\d[\\dA-Z]?[ ]?\\d[ABD-HJLN-UW-Z]{2}))|BFPO[ ]?\\d{1,4}$",
ge: "^\\d{4}$",
gf: "^9[78]3\\d{2}$",
gg: "^GY\\d[\\dA-Z]?[ ]?\\d[ABD-HJLN-UW-Z]{2}$",
gl: "^39\\d{2}$",
gn: "^\\d{3}$",
gp: "^9[78][01]\\d{2}$",
gr: "^\\d{3}[ ]?\\d{2}$",
gs: "^SIQQ 1ZZ$",
gt: "^\\d{5}$",
gu: "^969[123]\\d([ \\-]\\d{4})?$",
gw: "^\\d{4}$",
hm: "^\\d{4}$",
hn: "^(?:\\d{5})?$",
hr: "^\\d{5}$",
ht: "^\\d{4}$",
hu: "^\\d{4}$",
id: "^\\d{5}$",
il: "^\\d{5}$",
im: "^IM\\d[\\dA-Z]?[ ]?\\d[ABD-HJLN-UW-Z]{2}$",
in: "^\\d{6}$",
io: "^BBND 1ZZ$",
iq: "^\\d{5}$",
is: "^\\d{3}$",
it: "^\\d{5}$",
je: "^JE\\d[\\dA-Z]?[ ]?\\d[ABD-HJLN-UW-Z]{2}$",
jo: "^\\d{5}$",
jp: "^\\d{3}-\\d{4}$",
ke: "^\\d{5}$",
kg: "^\\d{6}$",
kh: "^\\d{5}$",
kr: "^\\d{3}[\\-]\\d{3}$",
kw: "^\\d{5}$",
kz: "^\\d{6}$",
la: "^\\d{5}$",
lb: "^(\\d{4}([ ]?\\d{4})?)?$",
li: "^(948[5-9])|(949[0-7])$",
lk: "^\\d{5}$",
lr: "^\\d{4}$",
ls: "^\\d{3}$",
lt: "^\\d{5}$",
lu: "^\\d{4}$",
lv: "^\\d{4}$",
ma: "^\\d{5}$",
mc: "^980\\d{2}$",
md: "^\\d{4}$",
me: "^8\\d{4}$",
mg: "^\\d{3}$",
mh: "^969[67]\\d([ \\-]\\d{4})?$",
mk: "^\\d{4}$",
mn: "^\\d{6}$",
mp: "^9695[012]([ \\-]\\d{4})?$",
mq: "^9[78]2\\d{2}$",
mt: "^[A-Z]{3}[ ]?\\d{2,4}$",
mu: "^(\\d{3}[A-Z]{2}\\d{3})?$",
mv: "^\\d{5}$",
mx: "^\\d{5}$",
my: "^\\d{5}$",
nc: "^988\\d{2}$",
ne: "^\\d{4}$",
nf: "^2899$",
ng: "^(\\d{6})?$",
ni: "^((\\d{4}-)?\\d{3}-\\d{3}(-\\d{1})?)?$",
nl: "^\\d{4}[ ]?[A-Z]{2}$",
no: "^\\d{4}$",
np: "^\\d{5}$",
nz: "^\\d{4}$",
om: "^(PC )?\\d{3}$",
pf: "^987\\d{2}$",
pg: "^\\d{3}$",
ph: "^\\d{4}$",
pk: "^\\d{5}$",
pl: "^\\d{2}-\\d{3}$",
pm: "^9[78]5\\d{2}$",
pn: "^PCRN 1ZZ$",
pr: "^00[679]\\d{2}([ \\-]\\d{4})?$",
pt: "^\\d{4}([\\-]\\d{3})?$",
pw: "^96940$",
py: "^\\d{4}$",
re: "^9[78]4\\d{2}$",
ro: "^\\d{6}$",
rs: "^\\d{6}$",
ru: "^\\d{6}$",
sa: "^\\d{5}$",
se: "^\\d{3}[ ]?\\d{2}$",
sg: "^\\d{6}$",
sh: "^(ASCN|STHL) 1ZZ$",
si: "^\\d{4}$",
sj: "^\\d{4}$",
sk: "^\\d{3}[ ]?\\d{2}$",
sm: "^4789\\d$",
sn: "^\\d{5}$",
so: "^\\d{5}$",
sz: "^[HLMS]\\d{3}$",
tc: "^TKCA 1ZZ$",
th: "^\\d{5}$",
tj: "^\\d{6}$",
tm: "^\\d{6}$",
tn: "^\\d{4}$",
tr: "^\\d{5}$",
tw: "^\\d{3}(\\d{2})?$",
ua: "^\\d{5}$",
us: "^\\d{5}([ \\-]\\d{4})?$",
uy: "^\\d{5}$",
uz: "^\\d{6}$",
va: "^00120$",
ve: "^\\d{4}$",
vi: "^008(([0-4]\\d)|(5[01]))([ \\-]\\d{4})?$",
wf: "^986\\d{2}$",
xk: "^\\d{5}$",
yt: "^976\\d{2}$",
yu: "^\\d{5}$",
za: "^\\d{4}$",
zm: "^\\d{5}$",
};
connect () { connect() {
window.addEventListener('cart:country:update', event => { window.addEventListener("cart:country:update", (event) => {
if (this.data.get('group') !== event.detail.group) return if (this.data.get("group") !== event.detail.group) return;
const zipcodeRequired = event.detail.data.zipcodeRequired == 'true' const zipcodeRequired = event.detail.data.zipcodeRequired == "true";
this.codeTarget.value = '' this.codeTarget.value = "";
this.codeTarget.disabled = !zipcodeRequired this.codeTarget.disabled = !zipcodeRequired;
this.codeTarget.required = zipcodeRequired this.codeTarget.required = zipcodeRequired;
if (!zipcodeRequired) return if (!zipcodeRequired) return;
this.codeTarget.pattern = this.postal_codes[event.detail.iso.toLowerCase()] || '.*' this.codeTarget.pattern =
this.postal_codes[event.detail.iso.toLowerCase()] || ".*";
if (event.detail.selectedZipcode) { if (event.detail.selectedZipcode) {
this.codeTarget.value = event.detail.selectedZipcode this.codeTarget.value = event.detail.selectedZipcode;
this.codeTarget.dispatchEvent(new Event('change')) this.codeTarget.dispatchEvent(new Event("change"));
} }
}) });
} }
} }

View file

@ -1,4 +1,4 @@
import { Controller } from 'stimulus' import { Controller } from "stimulus";
/* /*
* Al navegar por el sitio y llegar a ciertas secciones, se van * Al navegar por el sitio y llegar a ciertas secciones, se van
@ -8,34 +8,40 @@ import { Controller } from 'stimulus'
* a medida que van apareciendo secciones actualizamos el menú. * a medida que van apareciendo secciones actualizamos el menú.
*/ */
export default class extends Controller { export default class extends Controller {
static targets = [ 'section' ] static targets = ["section"];
connect () { connect() {
for (const section of this.sectionTargets) { for (const section of this.sectionTargets) {
this.observer.observe(section) this.observer.observe(section);
} }
} }
/* /*
* Solo nos interesa la primera * Solo nos interesa la primera
*/ */
get observer () { get observer() {
if (!this._observer) this._observer = new IntersectionObserver((entries, observer) => this.update(entries), this.options) if (!this._observer)
this._observer = new IntersectionObserver(
(entries, observer) => this.update(entries),
this.options
);
return this._observer return this._observer;
} }
get options () { get options() {
if (!this._options) this._options = { threshold: 0, rootMargin: '0px' } if (!this._options) this._options = { threshold: 0, rootMargin: "0px" };
return this._options return this._options;
} }
update (entries) { update(entries) {
const section = entries.find(x => x.isIntersecting) const section = entries.find((x) => x.isIntersecting);
if (!section) return if (!section) return;
window.dispatchEvent(new CustomEvent('scroll:section', { detail: { id: section.target.id }})) window.dispatchEvent(
new CustomEvent("scroll:section", { detail: { id: section.target.id } })
);
} }
} }

View file

@ -1,90 +1,96 @@
import { Controller } from 'stimulus' import { Controller } from "stimulus";
import { Liquid } from 'liquidjs' import { Liquid } from "liquidjs";
const lunr = require("lunr") const lunr = require("lunr");
require("lunr-languages/lunr.stemmer.support")(lunr) require("lunr-languages/lunr.stemmer.support")(lunr);
require("lunr-languages/lunr.es")(lunr) require("lunr-languages/lunr.es")(lunr);
export default class extends Controller { export default class extends Controller {
static targets = [ 'q' ] static targets = ["q"];
get q () { get q() {
if (!this.hasQTarget) return if (!this.hasQTarget) return;
if (!this.qTarget.value.trim().length === 0) return if (!this.qTarget.value.trim().length === 0) return;
return this.qTarget.value.trim().replaceAll(/[:~\*\^\+\-]/gi, '') return this.qTarget.value.trim().replaceAll(/[:~\*\^\+\-]/gi, "");
} }
connect () { connect() {
const q = new URLSearchParams(window.location.search).get('q')?.trim() const q = new URLSearchParams(window.location.search).get("q")?.trim();
if (q) { if (q) {
this.qTarget.value = q this.qTarget.value = q;
this.search() this.search();
} }
} }
async search (event) { async search(event) {
// Detiene el envío del formulario // Detiene el envío del formulario
if (event) { if (event) {
event.preventDefault() event.preventDefault();
event.stopPropagation() event.stopPropagation();
} }
this.formDisable = true this.formDisable = true;
// Obtiene el término de búsqueda // Obtiene el término de búsqueda
const q = this.q const q = this.q;
// Si no hay término de búsqueda, no hay búsqueda // Si no hay término de búsqueda, no hay búsqueda
if (q) { if (q) {
// Trae el índice de búsqueda // Trae el índice de búsqueda
await this.fetch() await this.fetch();
// Hasta que no haya índice no buscamos nada, esto evita que se // Hasta que no haya índice no buscamos nada, esto evita que se
// aprete enter dos veces y se fallen cosas. // aprete enter dos veces y se fallen cosas.
if (!window.index) return if (!window.index) return;
} }
const main = document.querySelector('main') const main = document.querySelector("main");
const results = window.index.search(q).map(r => window.data.find(a => a.id == r.ref)) const results = window.index
const site = window.site .search(q)
const template = window.templates.results .map((r) => window.data.find((a) => a.id == r.ref));
const html = await this.engine.parseAndRender(template, { q, site, results }) const site = window.site;
const title = `${site.i18n.search.title} - ${q}` const template = window.templates.results;
const query = new URLSearchParams({ q }) const html = await this.engine.parseAndRender(template, {
q,
site,
results,
});
const title = `${site.i18n.search.title} - ${q}`;
const query = new URLSearchParams({ q });
window.history.pushState({ q }, title, `?${query.toString()}`) window.history.pushState({ q }, title, `?${query.toString()}`);
document.title = title document.title = title;
main.innerHTML = html main.innerHTML = html;
this.formDisable = false this.formDisable = false;
} }
async fetch () { async fetch() {
// Solo permite descargar uno a la vez // Solo permite descargar uno a la vez
if (this.fetching) return if (this.fetching) return;
this.fetching = true this.fetching = true;
let response let response;
// Si no existe el índice, lo descarga y procesa Lunr // Si no existe el índice, lo descarga y procesa Lunr
if (!window.data) { if (!window.data) {
response = await fetch('data.json') response = await fetch("data.json");
window.data = await response.json() window.data = await response.json();
} }
if (!window.index) { if (!window.index) {
response = await fetch('idx.json') response = await fetch("idx.json");
const idx = await response.json() const idx = await response.json();
window.index = lunr.Index.load(idx) window.index = lunr.Index.load(idx);
} }
// Permitir volver a ejecutar // Permitir volver a ejecutar
this.fetching = false this.fetching = false;
} }
set formDisable (disable) { set formDisable(disable) {
this.element.elements.forEach(x => x.disabled = disable) this.element.elements.forEach((x) => (x.disabled = disable));
} }
/* /*
@ -92,9 +98,9 @@ export default class extends Controller {
* *
* @return Liquid * @return Liquid
*/ */
get engine () { get engine() {
if (!window.liquid) window.liquid = new Liquid() if (!window.liquid) window.liquid = new Liquid();
return window.liquid return window.liquid;
} }
} }

View file

@ -1,26 +1,26 @@
import { Controller } from 'stimulus' import { Controller } from "stimulus";
export default class extends Controller { export default class extends Controller {
static values = { static values = {
title: String, title: String,
text: String, text: String,
url: String url: String,
} };
async share (event = undefined) { async share(event = undefined) {
event?.preventDefault() event?.preventDefault();
event?.stopPropagation() event?.stopPropagation();
const title = this.titleValue const title = this.titleValue;
const text = this.textValue const text = this.textValue;
const url = this.urlValue const url = this.urlValue;
const data = { title, text, url } const data = { title, text, url };
if ('share' in navigator) { if ("share" in navigator) {
if (navigator.canShare(data)) { if (navigator.canShare(data)) {
navigator.share(data) navigator.share(data);
} else { } else {
console.error('No se puede compartir', data) console.error("No se puede compartir", data);
} }
} }
} }

View file

@ -1,64 +1,73 @@
import { Controller } from 'stimulus' import { Controller } from "stimulus";
/* /*
* Slider con scroll automático * Slider con scroll automático
*/ */
export default class extends Controller { export default class extends Controller {
static targets = [ 'control' ] static targets = ["control"];
connect () { connect() {
this.active(this.controlTargets.find(x => x.href.endsWith(window.location.hash))) this.active(
this.controlTargets.find((x) => x.href.endsWith(window.location.hash))
);
this.interval = setInterval(() => this.inViewport ? this.controlTargets[this.next].click() : null, this.duration * 1000) this.interval = setInterval(
() => (this.inViewport ? this.controlTargets[this.next].click() : null),
this.duration * 1000
);
} }
get duration () { get duration() {
const duration = parseInt(this.data.get('duration')) const duration = parseInt(this.data.get("duration"));
return isNaN(duration) ? 15 : duration return isNaN(duration) ? 15 : duration;
} }
disconnect () { disconnect() {
clearInterval(this.interval) clearInterval(this.interval);
} }
active (control) { active(control) {
if (!control) return if (!control) return;
this.controlTargets.forEach(other => other.classList.toggle('active', control.href === other.href)) this.controlTargets.forEach((other) =>
this.current = this.controlTargets.indexOf(control) other.classList.toggle("active", control.href === other.href)
);
this.current = this.controlTargets.indexOf(control);
} }
activate (event) { activate(event) {
// XXX: En Firefox, el target del evento también puede ser el // XXX: En Firefox, el target del evento también puede ser el
// contenido del link :/ // contenido del link :/
let t = (event.target.href) ? event.target : event.target.parentElement let t = event.target.href ? event.target : event.target.parentElement;
this.active(t) this.active(t);
} }
get current () { get current() {
return parseInt(this.data.get('current')) || 0 return parseInt(this.data.get("current")) || 0;
} }
set current (value) { set current(value) {
this.data.set('current', value) this.data.set("current", value);
} }
get next () { get next() {
const next = this.current + 1 const next = this.current + 1;
return (this.controlTargets[next]) ? next : 0 return this.controlTargets[next] ? next : 0;
} }
get inViewport () { get inViewport() {
const bounding = this.element.getBoundingClientRect(); const bounding = this.element.getBoundingClientRect();
return ( return (
bounding.top >= 0 && bounding.top >= 0 &&
bounding.left >= 0 && bounding.left >= 0 &&
bounding.bottom <= (window.innerHeight || document.documentElement.clientHeight) && bounding.bottom <=
bounding.right <= (window.innerWidth || document.documentElement.clientWidth) (window.innerHeight || document.documentElement.clientHeight) &&
bounding.right <=
(window.innerWidth || document.documentElement.clientWidth)
); );
}; }
} }

View file

@ -1,4 +1,4 @@
import { CartBaseController } from './cart_base_controller' import { CartBaseController } from "./cart_base_controller";
/* /*
* Populates a state field where users can type to filter and select * Populates a state field where users can type to filter and select
@ -7,85 +7,91 @@ import { CartBaseController } from './cart_base_controller'
*/ */
export default class extends CartBaseController { export default class extends CartBaseController {
// All are required! // All are required!
static targets = [ 'id', 'list', 'name' ] static targets = ["id", "list", "name"];
connect () { connect() {
window.addEventListener('cart:country:update', async event => { window.addEventListener("cart:country:update", async (event) => {
if (this.data.get('group') !== event.detail.group) return if (this.data.get("group") !== event.detail.group) return;
this.idTarget.value = '' this.idTarget.value = "";
this.nameTarget.value = '' this.nameTarget.value = "";
this.listTarget.innerHTML = '' this.listTarget.innerHTML = "";
const statesRequired = event.detail.data.statesRequired == 'true' const statesRequired = event.detail.data.statesRequired == "true";
this.nameTarget.disabled = !statesRequired this.nameTarget.disabled = !statesRequired;
this.nameTarget.required = statesRequired this.nameTarget.required = statesRequired;
if (!statesRequired) return if (!statesRequired) return;
const states = await this.states(event.detail.iso) const states = await this.states(event.detail.iso);
const site = window.site const site = window.site;
states.forEach(state => { states.forEach((state) => {
let option = document.createElement('option') let option = document.createElement("option");
option.value = state.attributes.name option.value = state.attributes.name;
option.dataset.id = state.id option.dataset.id = state.id;
this.listTarget.appendChild(option) this.listTarget.appendChild(option);
}) });
this.nameTarget.pattern = states.map(x => x.attributes.name).join('|') this.nameTarget.pattern = states.map((x) => x.attributes.name).join("|");
this.nameTarget.addEventListener('input', event => this.nameTarget.setCustomValidity('')) this.nameTarget.addEventListener("input", (event) =>
this.nameTarget.addEventListener('invalid', event => this.nameTarget.setCustomValidity(site.i18n.states.validation)) this.nameTarget.setCustomValidity("")
);
this.nameTarget.addEventListener("invalid", (event) =>
this.nameTarget.setCustomValidity(site.i18n.states.validation)
);
if (event.detail.selectedState) { if (event.detail.selectedState) {
this.nameTarget.value = event.detail.selectedState this.nameTarget.value = event.detail.selectedState;
this.nameTarget.dispatchEvent(new Event('change')) this.nameTarget.dispatchEvent(new Event("change"));
} }
}) });
// When the input changes we update the actual value and also the // When the input changes we update the actual value and also the
// state list via an Event // state list via an Event
this.nameTarget.addEventListener('change', event => { this.nameTarget.addEventListener("change", (event) => {
const options = Array.from(this.listTarget.options) const options = Array.from(this.listTarget.options);
const option = options.find(x => x.value == this.nameTarget.value) const option = options.find((x) => x.value == this.nameTarget.value);
// TODO: If no option is found, mark the field as invalid // TODO: If no option is found, mark the field as invalid
if (!option) return if (!option) return;
this.idTarget.value = option.dataset.id this.idTarget.value = option.dataset.id;
this.idTarget.dispatchEvent(new Event('change')) this.idTarget.dispatchEvent(new Event("change"));
}) });
} }
/* /*
* Fetch the state list from storage or from API using a country ISO * Fetch the state list from storage or from API using a country ISO
* code * code
*/ */
async states (countryIso) { async states(countryIso) {
const stateId = `states:${countryIso}` const stateId = `states:${countryIso}`;
let states = JSON.parse(this.storageTemp.getItem(stateId)) let states = JSON.parse(this.storageTemp.getItem(stateId));
if (states) return states if (states) return states;
// There's no state query, but we can fetch the country and include // There's no state query, but we can fetch the country and include
// its states. // its states.
const response = await this.spree.countries.show(countryIso, { include: 'states' }) const response = await this.spree.countries.show(countryIso, {
include: "states",
});
// TODO: Show error message // TODO: Show error message
if (response.isFail()) { if (response.isFail()) {
this.handleFailure(response) this.handleFailure(response);
return {} return {};
} }
states = response.success().included states = response.success().included;
// Order alphabetically by name // Order alphabetically by name
states.sort((x, y) => x.attributes.name > y.attributes.name) states.sort((x, y) => x.attributes.name > y.attributes.name);
this.storageTemp.setItem(stateId, JSON.stringify(states)) this.storageTemp.setItem(stateId, JSON.stringify(states));
return states return states;
} }
} }

View file

@ -1,4 +1,4 @@
import { Controller } from 'stimulus' import { Controller } from "stimulus";
/* /*
* Mantiene el stock actualizado, consultando a la API. * Mantiene el stock actualizado, consultando a la API.
@ -9,48 +9,48 @@ import { Controller } from 'stimulus'
* * Deshabilita botón si no está en stock * * Deshabilita botón si no está en stock
*/ */
export default class extends Controller { export default class extends Controller {
static targets = [ 'product' ] static targets = ["product"];
async connect () { async connect() {
const all_skus = this.skus const all_skus = this.skus;
if (all_skus.length === 0) return if (all_skus.length === 0) return;
// El paginado es para prevenir que la petición se haga muy grande y // El paginado es para prevenir que la petición se haga muy grande y
// falle entera. // falle entera.
const pages = Math.ceil(all_skus.length / this.per_page) const pages = Math.ceil(all_skus.length / this.per_page);
let start = 0 let start = 0;
let end = this.per_page let end = this.per_page;
for (let local_page = 1; local_page <= pages; local_page++) { for (let local_page = 1; local_page <= pages; local_page++) {
const skus = all_skus.slice(start, end).join(',') const skus = all_skus.slice(start, end).join(",");
start = this.per_page * local_page start = this.per_page * local_page;
end = start + this.per_page end = start + this.per_page;
const filter = { skus } const filter = { skus };
let response = await window.spree.products.list({ filter }) let response = await window.spree.products.list({ filter });
if (response.isFail()) { if (response.isFail()) {
console.error(response.fail()) console.error(response.fail());
return return;
} }
this.update_local_products(response.success().data) this.update_local_products(response.success().data);
// Recorrer todas las páginas // Recorrer todas las páginas
// XXX: Podríamos usar next pero la página 1 siempre se devuelve a // XXX: Podríamos usar next pero la página 1 siempre se devuelve a
// sí misma y entraríamos en un loop infinito. // sí misma y entraríamos en un loop infinito.
for (let page = 2; page <= response.success().meta.total_pages; page++) { for (let page = 2; page <= response.success().meta.total_pages; page++) {
response = await window.spree.products.list({ filter, page }) response = await window.spree.products.list({ filter, page });
if (response.isFail()) { if (response.isFail()) {
console.error(response.fail()) console.error(response.fail());
continue continue;
} }
this.update_local_products(response.success().data) this.update_local_products(response.success().data);
} }
} }
} }
@ -62,34 +62,56 @@ export default class extends Controller {
* *
* @return [Array] * @return [Array]
*/ */
get skus () { get skus() {
return [...new Set(this.productTargets.map(p=> p.dataset.sku).filter(x => x.length > 0))] return [
...new Set(
this.productTargets
.map((p) => p.dataset.sku)
.filter((x) => x.length > 0)
),
];
} }
/* /*
* La cantidad de productos por página que vamos a pedir * La cantidad de productos por página que vamos a pedir
*/ */
get per_page () { get per_page() {
if (!this._per_page) { if (!this._per_page) {
this._per_page = parseInt(this.element.dataset.perPage) this._per_page = parseInt(this.element.dataset.perPage);
if (isNaN(this._per_page)) this._per_page = 100 if (isNaN(this._per_page)) this._per_page = 100;
} }
return this._per_page return this._per_page;
} }
/* /*
* Los productos pueden estar duplicados así que buscamos todos. * Los productos pueden estar duplicados así que buscamos todos.
*/ */
update_local_products (products) { update_local_products(products) {
for (const local of this.productTargets) { for (const local of this.productTargets) {
for (const product of products.filter(p => local.dataset.cartVariantId === p.relationships.default_variant.data.id)) { for (const product of products.filter(
local.dataset.cartInStock = product.attributes.in_stock (p) =>
local.dataset.cartPrice = product.attributes.price local.dataset.cartVariantId ===
p.relationships.default_variant.data.id
)) {
local.dataset.cartInStock = product.attributes.in_stock;
local.dataset.cartPrice = product.attributes.price;
local.querySelectorAll('[data-stock-add]').forEach(button => button.disabled = !product.attributes.in_stock) local
local.querySelectorAll('[data-stock-price]').forEach(price => price.innerText = parseInt(product.attributes.price)) .querySelectorAll("[data-stock-add]")
local.querySelectorAll('[data-stock-currency]').forEach(currency => currency.innerText = product.attributes.currency) .forEach(
(button) => (button.disabled = !product.attributes.in_stock)
);
local
.querySelectorAll("[data-stock-price]")
.forEach(
(price) => (price.innerText = parseInt(product.attributes.price))
);
local
.querySelectorAll("[data-stock-currency]")
.forEach(
(currency) => (currency.innerText = product.attributes.currency)
);
} }
} }
} }

View file

@ -1,80 +1,81 @@
import Axios from 'axios' import Axios from "axios";
import * as qs from 'qs' import * as qs from "qs";
/* /*
* XXX: We're copying code from @spree/storefront-api-v2-sdk/src/Http.ts * XXX: We're copying code from @spree/storefront-api-v2-sdk/src/Http.ts
* because we don't know how to mix Typescript :D * because we don't know how to mix Typescript :D
*/ */
export class Sutty { export class Sutty {
constructor (host = 'http://localhost:3000') { constructor(host = "http://localhost:3000") {
this.host = host this.host = host;
this.axios = Axios.create({ this.axios = Axios.create({
baseURL: this.host, baseURL: this.host,
headers: { 'Content-Type': 'application/json' }, headers: { "Content-Type": "application/json" },
paramsSerializer: params => qs.stringify(params, { arrayFormat: 'brackets' }) paramsSerializer: (params) =>
}) qs.stringify(params, { arrayFormat: "brackets" }),
});
} }
async getCheckoutURL(tokens = {}) { async getCheckoutURL(tokens = {}) {
const headers = this.spreeOrderHeaders(tokens) const headers = this.spreeOrderHeaders(tokens);
const axiosConfig = { const axiosConfig = {
url: 'api/v2/storefront/checkout_redirect.json', url: "api/v2/storefront/checkout_redirect.json",
params: {}, params: {},
method: 'get', method: "get",
headers headers,
} };
return await this.axios(axiosConfig) return await this.axios(axiosConfig);
} }
async assignOrderOwnership(tokens = {}, params = {}) { async assignOrderOwnership(tokens = {}, params = {}) {
const headers = this.spreeOrderHeaders(tokens) const headers = this.spreeOrderHeaders(tokens);
const axiosConfig = { const axiosConfig = {
url: 'api/v2/storefront/assign_order_ownership.json', url: "api/v2/storefront/assign_order_ownership.json",
params, params,
method: 'post', method: "post",
headers headers,
} };
return await this.axios(axiosConfig) return await this.axios(axiosConfig);
} }
async assignOrderShippingAddress(tokens = {}, params = {}) { async assignOrderShippingAddress(tokens = {}, params = {}) {
const headers = this.spreeOrderHeaders(tokens) const headers = this.spreeOrderHeaders(tokens);
const axiosConfig = { const axiosConfig = {
url: 'api/v2/storefront/assign_order_shipping_address.json', url: "api/v2/storefront/assign_order_shipping_address.json",
params, params,
method: 'post', method: "post",
headers headers,
} };
return await this.axios(axiosConfig) return await this.axios(axiosConfig);
} }
async assignOrderBillingAddress(tokens = {}, params = {}) { async assignOrderBillingAddress(tokens = {}, params = {}) {
const headers = this.spreeOrderHeaders(tokens) const headers = this.spreeOrderHeaders(tokens);
const axiosConfig = { const axiosConfig = {
url: 'api/v2/storefront/assign_order_billing_address.json', url: "api/v2/storefront/assign_order_billing_address.json",
params, params,
method: 'post', method: "post",
headers headers,
} };
return await this.axios(axiosConfig) return await this.axios(axiosConfig);
} }
spreeOrderHeaders(tokens) { spreeOrderHeaders(tokens) {
const header = {} const header = {};
if (tokens.orderToken) { if (tokens.orderToken) {
header['X-Spree-Order-Token'] = tokens.orderToken header["X-Spree-Order-Token"] = tokens.orderToken;
} }
if (tokens.bearerToken) { if (tokens.bearerToken) {
header['Authorization'] = `Bearer ${tokens.bearerToken}` header["Authorization"] = `Bearer ${tokens.bearerToken}`;
} }
return header return header;
} }
} }

View file

@ -1,25 +1,25 @@
import BotDetector from 'device-detector-js/dist/parsers/bot' import BotDetector from "device-detector-js/dist/parsers/bot";
import { Notifier } from '@airbrake/browser' import { Notifier } from "@airbrake/browser";
window.bot_detector = new BotDetector window.bot_detector = new BotDetector();
const bot = window.bot_detector.parse(navigator.userAgent) const bot = window.bot_detector.parse(navigator.userAgent);
if (!bot) { if (!bot) {
window.airbrake = new Notifier({ window.airbrake = new Notifier({
projectId: window.env.AIRBRAKE_PROJECT_ID, projectId: window.env.AIRBRAKE_PROJECT_ID,
projectKey: window.env.AIRBRAKE_PROJECT_KEY, projectKey: window.env.AIRBRAKE_PROJECT_KEY,
host: 'https://panel.sutty.nl' host: "https://panel.sutty.nl",
}) });
console.originalError = console.error console.originalError = console.error;
console.error = (...e) => { console.error = (...e) => {
window.airbrake.notify(e.join(' ')) window.airbrake.notify(e.join(" "));
return console.originalError(...e) return console.originalError(...e);
} };
} }
import 'core-js/stable' import "core-js/stable";
import 'regenerator-runtime/runtime' import "regenerator-runtime/runtime";
// Turbo acelera la navegación al no tener que recargar todo el JS y CSS // Turbo acelera la navegación al no tener que recargar todo el JS y CSS
// de la página, con lo que se siente más rápida y "nativa". // de la página, con lo que se siente más rápida y "nativa".
@ -27,46 +27,49 @@ import 'regenerator-runtime/runtime'
// Cambiamos de turbolinks a turbo porque turbo soporta la función // Cambiamos de turbolinks a turbo porque turbo soporta la función
// fetch(), que luego es interceptada por el SW para obtener las // fetch(), que luego es interceptada por el SW para obtener las
// direcciones localmente. // direcciones localmente.
import * as Turbo from "@hotwired/turbo" import * as Turbo from "@hotwired/turbo";
Turbo.start() Turbo.start();
import { Application } from 'stimulus' import { Application } from "stimulus";
import { definitionsFromContext } from "stimulus/webpack-helpers" import { definitionsFromContext } from "stimulus/webpack-helpers";
const application = Application.start() const application = Application.start();
const context = require.context("./controllers", true, /\.js$/) const context = require.context("./controllers", true, /\.js$/);
application.load(definitionsFromContext(context)) application.load(definitionsFromContext(context));
import { makeClient } from '@spree/storefront-api-v2-sdk' import { makeClient } from "@spree/storefront-api-v2-sdk";
import { Sutty } from './endpoints/sutty' import { Sutty } from "./endpoints/sutty";
window.spree = makeClient({ host: window.env.SPREE_URL }) window.spree = makeClient({ host: window.env.SPREE_URL });
window.spree.sutty = new Sutty(window.spree.host) window.spree.sutty = new Sutty(window.spree.host);
try { try {
window.axe = require('axe-core/axe') window.axe = require("axe-core/axe");
} catch(e) {} } catch (e) {}
if (window.axe) window.axe.configure({ locale: require('axe-core/locales/es.json') }) if (window.axe)
window.axe.configure({ locale: require("axe-core/locales/es.json") });
document.addEventListener('turbo:load', event => { document.addEventListener("turbo:load", (event) => {
document.querySelectorAll("a[href^='http://'],a[href^='https://'],a[href^='//']").forEach(a => { document
a.rel = "noopener" .querySelectorAll("a[href^='http://'],a[href^='https://'],a[href^='//']")
a.target = "_blank" .forEach((a) => {
}) a.rel = "noopener";
a.target = "_blank";
});
if (!window.axe) return if (!window.axe) return;
window.axe.run().then(results => { window.axe.run().then((results) => {
results.violations.forEach(violation => { results.violations.forEach((violation) => {
violation.nodes.forEach(node => { violation.nodes.forEach((node) => {
node.target.forEach(target => { node.target.forEach((target) => {
document.querySelectorAll(target).forEach(element => { document.querySelectorAll(target).forEach((element) => {
element.classList.add('inaccesible') element.classList.add("inaccesible");
element.ariaLabel = node.failureSummary element.ariaLabel = node.failureSummary;
}) });
}) });
}) });
}) });
}) });
}) });

View file

@ -20,6 +20,7 @@
"device-detector-js": "^2.2.10", "device-detector-js": "^2.2.10",
"dotenv-webpack": "^6.0.0", "dotenv-webpack": "^6.0.0",
"liquidjs": "^9.14.0", "liquidjs": "^9.14.0",
"prettier": "^2.4.1",
"regenerator-runtime": "^0.13.5", "regenerator-runtime": "^0.13.5",
"sassdoc": "^2.7.3", "sassdoc": "^2.7.3",
"sassdoc-theme-herman": "^4.0.2", "sassdoc-theme-herman": "^4.0.2",

View file

@ -5161,6 +5161,11 @@ prepend-http@^2.0.0:
resolved "https://registry.yarnpkg.com/prepend-http/-/prepend-http-2.0.0.tgz#e92434bfa5ea8c19f41cdfd401d741a3c819d897" resolved "https://registry.yarnpkg.com/prepend-http/-/prepend-http-2.0.0.tgz#e92434bfa5ea8c19f41cdfd401d741a3c819d897"
integrity sha1-6SQ0v6XqjBn0HN/UAddBo8gZ2Jc= integrity sha1-6SQ0v6XqjBn0HN/UAddBo8gZ2Jc=
prettier@^2.4.1:
version "2.4.1"
resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.4.1.tgz#671e11c89c14a4cfc876ce564106c4a6726c9f5c"
integrity sha512-9fbDAXSBcc6Bs1mZrDYb3XKzDLm4EXXL9sC1LqKP5rZkT6KRr/rf9amVUcODVXgguK/isJz0d0hP72WeaKWsvA==
process-nextick-args@^2.0.0, process-nextick-args@~2.0.0: process-nextick-args@^2.0.0, process-nextick-args@~2.0.0:
version "2.0.1" version "2.0.1"
resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2" resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2"