From 990deb509d15915d9ef516d716c3e9d2d938ce48 Mon Sep 17 00:00:00 2001 From: f Date: Tue, 1 Jun 2021 18:33:49 -0300 Subject: [PATCH 01/60] tienda --- _config.yml | 1 + _data/en.yml | 19 ++ _data/es.yml | 19 ++ _data/forms/contacto.yml | 51 ++-- _data/forms/shipping_address.yml | 94 +++++++ _data/forms/user.yml | 13 + _data/layouts/cart.yml | 198 +++++++++++++ _data/layouts/confirmation.yml | 68 +++++ _data/layouts/email.yml | 56 ++++ _data/layouts/payment.yml | 114 ++++++++ _data/layouts/product.yml | 142 ++++++++++ _data/layouts/shipment.yml | 126 +++++++++ _includes/cart_add.html | 8 + _includes/cart_controller.html | 10 + _includes/country.html | 53 ++++ _includes/input.html | 16 +- _includes/postal_code.html | 45 +++ _includes/submit.html | 2 +- _layouts/cart.html | 72 +++++ _layouts/confirmation.html | 17 ++ _layouts/page.html | 5 + _layouts/payment.html | 21 ++ _layouts/shipment.html | 52 ++++ _packs/controllers/cart_base_controller.js | 249 +++++++++++++++++ .../cart_confirmation_controller.js | 39 +++ _packs/controllers/cart_contact_controller.js | 24 ++ _packs/controllers/cart_controller.js | 264 ++++++++++++++++++ _packs/controllers/cart_counter_controller.js | 25 ++ .../cart_payment_methods_controller.js | 89 ++++++ .../cart_paypal_confirmation_controller.js | 42 +++ .../controllers/cart_shipping_controller.js | 111 ++++++++ _packs/controllers/country_controller.js | 101 +++++++ _packs/controllers/order_controller.js | 68 +++++ _packs/controllers/postal_code_controller.js | 38 +++ _packs/controllers/state_controller.js | 91 ++++++ _packs/controllers/stock_controller.js | 71 +++++ _packs/endpoints/sutty.js | 80 ++++++ _packs/entry.js | 6 + env.js | 3 +- package.json | 1 + sutty-base-jekyll-theme.gemspec | 2 + yarn.lock | 41 ++- 42 files changed, 2516 insertions(+), 31 deletions(-) create mode 100644 _data/forms/shipping_address.yml create mode 100644 _data/forms/user.yml create mode 100644 _data/layouts/cart.yml create mode 100644 _data/layouts/confirmation.yml create mode 100644 _data/layouts/email.yml create mode 100644 _data/layouts/payment.yml create mode 100644 _data/layouts/product.yml create mode 100644 _data/layouts/shipment.yml create mode 100644 _includes/cart_add.html create mode 100644 _includes/cart_controller.html create mode 100644 _includes/country.html create mode 100644 _includes/postal_code.html create mode 100644 _layouts/cart.html create mode 100644 _layouts/confirmation.html create mode 100644 _layouts/page.html create mode 100644 _layouts/payment.html create mode 100644 _layouts/shipment.html create mode 100644 _packs/controllers/cart_base_controller.js create mode 100644 _packs/controllers/cart_confirmation_controller.js create mode 100644 _packs/controllers/cart_contact_controller.js create mode 100644 _packs/controllers/cart_controller.js create mode 100644 _packs/controllers/cart_counter_controller.js create mode 100644 _packs/controllers/cart_payment_methods_controller.js create mode 100644 _packs/controllers/cart_paypal_confirmation_controller.js create mode 100644 _packs/controllers/cart_shipping_controller.js create mode 100644 _packs/controllers/country_controller.js create mode 100644 _packs/controllers/order_controller.js create mode 100644 _packs/controllers/postal_code_controller.js create mode 100644 _packs/controllers/state_controller.js create mode 100644 _packs/controllers/stock_controller.js create mode 100644 _packs/endpoints/sutty.js diff --git a/_config.yml b/_config.yml index 651d41e..28289af 100644 --- a/_config.yml +++ b/_config.yml @@ -58,6 +58,7 @@ locales: - es ignored_layouts: - menu +- email linked_fields: - post - item diff --git a/_data/en.yml b/_data/en.yml index ddcde21..6bc7a3b 100644 --- a/_data/en.yml +++ b/_data/en.yml @@ -1,5 +1,19 @@ --- locale: English +countries: + validation: 'Choose a country from the list' +states: + validation: 'Choose a state from the list' +recover_order: 'Recover order' +alerts: + incorrect_password: "You're registered but your password is incorrect, you can try again or continue shopping as a guest." + successful_login: "You're logged in, your order will be added to your shopping history." + successful_signup: "Thanks for signing up!" + error: "There was an error contacting the store, please try again in a few minutes :(" + no_response_error: "There was an error contacting the store, please try again in a few minutes :(" + spree_error: "There was an error contacting the store, please try again in a few minutes :(" + recover_order: "We can't find your order" + select_format: "Choose a format:" date: format: '%m/%d/%Y' abbr_day_names: @@ -49,6 +63,11 @@ time: pm: pm layouts: post: Article + cart: Cart + confirmation: Order confirmation + payment: Payment + shipment: Shipment + email: Confirmation E-mail menu: Menu about: About this site menu: diff --git a/_data/es.yml b/_data/es.yml index 358e94b..8e14f53 100644 --- a/_data/es.yml +++ b/_data/es.yml @@ -1,5 +1,19 @@ --- locale: Castellano +countries: + validation: Elige un país de la lista +states: + validation: Elige una provincia/estado de la lista +recover_order: Rehacer el pedido +alerts: + incorrect_password: Estás registrade pero la contraseña es incorrecta, podés intentar de nuevo o continuar la compra como invitade. + successful_login: Iniciaste sesión, tu pedido se agregará a tu historia de compras. + successful_signup: ¡Gracias por registrarte! + error: 'Hubo un error al comunicarse con la tienda, te invitamos a intentar en unos minutos :(' + no_response_error: 'Hubo un error al comunicarse con la tienda, te invitamos a intentar en unos minutos :(' + spree_error: 'Hubo un error al comunicarse con la tienda, te invitamos a intentar en unos minutos :(' + recover_order: 'No encontramos el pedido en la tienda' + select_format: 'Seleccionar formato:' date: format: '%d/%m/%Y' abbr_day_names: @@ -49,6 +63,11 @@ time: pm: pm layouts: post: Artículo + cart: Carrito + confirmation: Confirmación de compra + payment: Pago + shipment: Envío + email: Correo de confirmación menu: Menú about: Información del sitio menu: diff --git a/_data/forms/contacto.yml b/_data/forms/contacto.yml index 0be3cab..5a246a5 100644 --- a/_data/forms/contacto.yml +++ b/_data/forms/contacto.yml @@ -1,40 +1,41 @@ --- pronouns: - type: "string" - autocomplete: "sex" + type: 'string' + autocomplete: 'sex' label: - es: "Pronombres" - en: "Pronouns" + es: 'Pronombres' + en: 'Pronouns' placeholder: - es: "¿Qué pronombres usás?" - en: "What are your pronouns?" + es: '¿Qué pronombres usás?' + en: 'What are your pronouns?' name: - type: "string" - autocomplete: "name" + type: 'string' + autocomplete: 'name' label: - es: "Nombre" - en: "Name" + es: 'Nombre' + en: 'Name' placeholder: - es: "Nombre, pseudónimo, alias" - en: "Name, pseudonym, alias" + es: 'Nombre, pseudónimo, alias' + en: 'Name, pseudonym, alias' from: - type: "email" - autocomplete: "email" + type: 'email' + autocomplete: 'email' label: - es: "Correo electrónico" - en: "E-mail address" + es: 'Correo electrónico' + en: 'E-mail address' body: - type: text + type: 'text' label: - es: Mensaje - en: Message + es: 'Mensaje' + en: 'Message' consent: - type: boolean + type: 'boolean' label: - es: Acepto las políticas de privacidad - en: I agree to the privacy policy + es: 'Acepto las políticas de privacidad' + en: 'I agree to the privacy policy' submit: - type: submit + type: 'submit' label: - es: Enviar - en: Send + es: 'Enviar' + en: 'Send' + extra: 'data-target="contact.submit"' diff --git a/_data/forms/shipping_address.yml b/_data/forms/shipping_address.yml new file mode 100644 index 0000000..57899ec --- /dev/null +++ b/_data/forms/shipping_address.yml @@ -0,0 +1,94 @@ +--- +firstname: + type: 'string' + required: true + label: + es: 'Nombre' + en: 'Name' + autocomplete: 'given-name' + error: + es: 'El nombre es obligatorio' + en: 'Name is required' +lastname: + type: 'string' + required: true + label: + es: 'Apellido' + en: 'Last name' + autocomplete: 'family-name' + error: + es: 'El apellido es obligatorio' + en: 'Last name is required' +address1: + type: 'string' + required: true + label: + es: 'Calle y número' + en: 'Street and number' + autocomplete: 'address-line1' + error: + es: 'La dirección es obligatoria' + en: 'Address is required' +city: + type: 'string' + required: true + label: + es: 'Ciudad' + en: 'City' + autocomplete: 'city' + error: + es: 'La ciudad es obligatoria' + en: 'City is required' +country_id: + type: 'country' + required: true + group: 'shipping_address' + autocomplete: 'country-name' + label: + es: 'País' + en: 'Country' + help: + es: 'Comenzar a escribir para filtrar' + en: 'Start writing to filter' + error: + es: 'El país debe estar en la lista' + en: 'Country must be on the list' +state_id: + type: 'state' + required: true + group: 'shipping_address' + label: + es: 'Provincia / Estado' + en: 'Province / State' + help: + es: 'Comenzar a escribir para filtrar' + en: 'Start writing to filter' + error: + es: 'La provincia debe estar en la lista' + en: 'Province must be on the list' +zipcode: + type: 'postal_code' + group: 'shipping_address' + required: true + label: + es: 'Código postal' + en: 'Postal/ZIP code' + autocomplete: 'postal-code' + error: + es: 'El código postal no tiene el formato correcto' + en: 'Postal/ZIP code is not on the correct format' +phone: + type: 'string' + required: true + label: + es: 'Teléfono' + en: 'Phone number' + autocomplete: 'tel' + error: + es: 'El teléfono es obligatorio' + en: 'Phone number is required' +submit: + type: 'submit' + label: + es: 'Enviar' + en: 'Send' diff --git a/_data/forms/user.yml b/_data/forms/user.yml new file mode 100644 index 0000000..5645e21 --- /dev/null +++ b/_data/forms/user.yml @@ -0,0 +1,13 @@ +--- +email: + required: true + autocomplete: 'email' + type: 'email' + label: + es: 'Correo electrónico' + en: 'E-mail' + extra: + input: 'data-target="cart-contact.username"' + error: + es: 'Debe ser una dirección válida' + en: 'E-mail address is not valid' diff --git a/_data/layouts/cart.yml b/_data/layouts/cart.yml new file mode 100644 index 0000000..022177c --- /dev/null +++ b/_data/layouts/cart.yml @@ -0,0 +1,198 @@ +--- +meta: + limit: 1 + help: + es: 'Personalización de varios elementos del carrito. Si hay varios artículos, se toma el primero de la lista.' + en: 'Personalization for several cart elements. If there are several articles, the first in the list is used.' +title: + type: string + required: true + label: + es: Título + en: Title + help: + es: 'El título de la página del carrito de compras' + en: 'Page title for cart page' + default: + es: 'Carrito de compras' + en: 'Cart' +content: + type: 'content' + label: + es: 'Contenido' + en: 'Content' + help: + es: 'Texto que quieras agregar al carrito, forma de funcionamiento, anuncios, tiempos de entrega, etc.' + en: 'Any text you want to add to the cart: how it works, announcements, etc.' +product: + type: 'string' + required: true + label: + es: 'Nombre del ítem en venta' + en: 'Name of the item for sale' + help: + es: 'El nombre que tienen los productos o servicios que vendés, por ejemplo Libros' + en: "The name of the product or service you're selling, for example Books" + default: + es: 'Producto' + en: 'Product' +currency: + type: 'string' + required: true + label: + es: 'Moneda' + en: 'Currency' + help: + es: 'Código de tres letras de la moneda en la que se expresan los precios' + en: 'Three letter code to denote currency used in pricing' + default: + es: 'ARS' + en: 'USD' +currency_alternate: + type: 'string' + required: true + label: + es: 'Moneda alternativa' + en: 'Alternative currency' + help: + es: 'Nombre alternativo de la moneda, para mostrar, por ejemplo "$", "pesos", "pe", etc' + en: 'Alternative name of currency to be shown, for example "$", "dollars", etc' + default: + es: 'ARS' + en: 'USD' +price: + type: 'string' + required: true + label: + es: 'Precio' + en: 'Price' + help: + es: '' + en: '' + default: + es: 'Precio' + en: 'Price' +quantity: + type: 'string' + required: true + label: + es: 'Cantidad' + en: 'Quantity' + help: + es: 'Nombre del selector de cantidades' + en: 'Name of quantity selector' + default: + es: 'Cantidad' + en: 'Quantity' +subtotal: + type: 'string' + required: true + label: + es: 'Subtotal' + en: 'Subtotal' + help: + es: 'Nombre de la suma de precios por ítem de venta' + en: 'Name for the price sum per sale item' + default: + es: 'Subtotal' + en: 'Subtotal' +total: + type: 'string' + required: true + label: + es: 'Total' + en: 'Total' + help: + es: 'Nombre de la suma total del pedido' + en: 'Name of total sum' + default: + es: 'Total' + en: 'Total' +add: + type: 'string' + required: true + label: + es: 'Botón de agregar libro' + en: 'Add to cart button' + help: + es: 'El texto del botón de agregar al carrito' + en: 'Text on Add to cart button' + default: + es: 'Agregar al carrito' + en: 'Add to cart' +added: + type: 'string' + required: true + label: + es: 'Producto agregado' + en: 'Product added to cart' + help: + es: 'Texto para avisar que el producto fue agregado al carrito' + en: 'Text to notify when a product has been added to the cart' + default: + es: 'Agregado al carrito' + en: 'Product added to cart' +out_of_stock: + type: 'string' + required: true + label: + es: 'Sin stock' + en: 'Sold out' + help: + es: 'Texto del botón de agregar al carrito cuando el libro está agotado' + en: 'Text for grayed-out Add to cart button when no stock is available' + default: + es: 'Agotado' + en: 'Sold out' +remove: + type: 'string' + required: true + label: + es: 'Botón de quitar producto' + en: 'Remove product button' + help: + es: 'El texto del botón de quitar del carrito' + en: 'Text for Remove product button' + default: + es: 'Quitar del carrito' + en: 'Remove product' +back: + type: 'string' + required: true + label: + es: 'Volver a la tienda' + en: 'Back' + help: + es: 'Nombre del botón para volver a la tienda' + en: 'Text for Back to store button' + default: + es: 'Volver a la tienda' + en: 'Back' +permalink: + type: 'permalink' + required: true + label: + es: 'Dirección de la página' + en: '' + help: + es: 'La dirección de la página del carrito dentro del sitio' + en: 'Address for cart page inside site' + default: + es: 'carrito/' + en: 'cart/' +draft: + type: 'boolean' + label: + es: 'Borrador' + en: 'Draft' + help: + es: 'Este artículo aun no está listo para publicar' + en: "This post isn't ready to be published yet" +order: + type: 'order' + label: + es: 'Orden' + en: 'Order' + help: + es: 'La posición del artículo en la lista de artículos' + en: 'The post position in the posts list' diff --git a/_data/layouts/confirmation.yml b/_data/layouts/confirmation.yml new file mode 100644 index 0000000..cc38cd6 --- /dev/null +++ b/_data/layouts/confirmation.yml @@ -0,0 +1,68 @@ +--- +meta: + limit: 1 + help: + es: Página de confirmación de compra. Si hay varias se toma la primera de la lista. + en: '' +title: + type: string + required: true + label: + es: Título + en: Title + help: + es: 'Título de la confirmación' + en: 'Confirmation page title' + default: + es: '¡Gracias por tu compra!' + en: 'Thank you for your purchase!' +content: + type: 'content' + label: + es: 'Contenido' + en: 'Content' + help: + es: 'Puedes agregar información de contacto, tiempos de entrega, etc. aquí.' + en: 'You may add contact information, timeframe for delivery, etc. here' + default: + es: 'En breve te llegará un correo con la información de tu pedido.' + en: 'You will receive an email detailing your purchase soon.' +permalink: + type: 'permalink' + required: true + label: + es: 'Dirección de la página' + en: 'Confirmation page address' + help: + es: 'La dirección de la página de confirmación dentro del sitio' + en: 'The address the confirmation page holds within your site' + default: + es: 'confirmacion/' + en: 'confirmation/' +back: + type: 'string' + label: + es: 'Volver al sitio' + en: 'Back to site' + help: + es: 'Texto del botón de volver al sitio' + en: 'Text for Back to site button' + default: + es: 'Volver al sitio' + en: 'Back to site' +draft: + type: 'boolean' + label: + es: 'Borrador' + en: 'Draft' + help: + es: 'Este artículo aun no está listo para publicar' + en: "This post isn't ready to be published yet" +order: + type: 'order' + label: + es: 'Orden' + en: 'Order' + help: + es: 'La posición del artículo en la lista de artículos' + en: 'The post position in the posts list' diff --git a/_data/layouts/email.yml b/_data/layouts/email.yml new file mode 100644 index 0000000..ed716ed --- /dev/null +++ b/_data/layouts/email.yml @@ -0,0 +1,56 @@ +--- +title: + type: string + required: true + label: + es: Título + en: Title + help: + es: '' + en: '' + default: + es: 'Correo de confirmación' + en: '' +intro: + type: 'html' + label: + es: 'Introducción' + en: '' + help: + es: 'Texto de apertura del correo de confirmación' + en: '' +thanks: + type: 'html' + label: + es: 'Pie' + en: '' + help: + es: 'Pie del correo' + en: '' +show_address: + type: 'boolean' + label: + es: 'Mostrar la dirección' + en: '' + help: + es: 'Agregar la dirección informada por le cliente en el correo' + en: '' + default: + es: true + en: true +order: + type: 'order' + label: + es: 'Orden' + en: '' + help: + es: 'La posicion del articulo en la lista de articulos' + en: '' +draft: + type: 'boolean' + label: + es: 'Borrador' + en: '' + help: + es: 'Tildar si no está listo para ser publicado' + en: '' diff --git a/_data/layouts/payment.yml b/_data/layouts/payment.yml new file mode 100644 index 0000000..2fd1705 --- /dev/null +++ b/_data/layouts/payment.yml @@ -0,0 +1,114 @@ +--- +meta: + limit: 1 + help: + en: 'Página de confirmación de pago. Si hay varias se usa la primera de la lista.' + es: 'Confirmation of payment page. If there are several, the first in list will be used.' +title: + type: string + required: true + label: + es: Título + en: Title + help: + es: 'El título de la página de medios de pago' + en: 'Title for payment options page' + default: + es: 'Medios de pago' + en: 'Payment options' +content: + type: 'content' + label: + es: 'Contenido' + en: 'Content' + help: + es: 'Puedes agregar texto opcional aquí' + en: 'You may add optional text here' +total: + type: 'string' + required: true + label: + es: 'Total' + en: 'Total' + help: + es: 'El precio total de la compra' + en: 'Total price' + default: + es: 'Total' + en: 'Total' +special_instructions: + type: 'string' + required: true + label: + es: 'Instrucciones especiales' + en: 'Special instructions' + help: + es: '' + en: 'Title text for Special instructions section' + default: + es: 'Instrucciones especiales' + en: 'Special instructions' +special_instructions_help: + type: 'string' + required: true + label: + es: 'Ayuda de las instrucciones especiales' + en: 'Help for special instructions section' + help: + es: '' + en: 'You might want to give examples of special instructions that can be added' + default: + es: 'Horas específicas de entrega, etc.' + en: 'Specific delivery hours, etc.' +permalink: + type: 'permalink' + required: true + label: + es: 'Dirección de la página' + en: 'Payment page address' + help: + es: 'La dirección de la página de medios de pago dentro del sitio' + en: 'The address the payment page hold within the site url' + default: + es: 'pago/' + en: 'payment/' +back: + type: 'string' + required: true + label: + es: 'Volver a métodos de envío' + en: 'Back to shipping options' + help: + es: 'Nombre del botón para volver a los métodos de envío' + en: 'Button text for Back to shipping options' + default: + es: 'Volver al métodos de envío' + en: 'Back to shipping options' +next_step: + type: 'string' + required: true + label: + es: 'Siguiente paso' + en: 'Next step' + help: + es: 'Texto del botón de pasar al siguiente paso de la compra (el pago)' + en: 'Text for next step in purchase button' + default: + es: 'Pagar' + en: 'Checkout' +draft: + type: 'boolean' + label: + es: 'Borrador' + en: 'Draft' + help: + es: 'Este artículo aun no está listo para publicar' + en: "This post isn't ready to be published yet" +order: + type: 'order' + label: + es: 'Orden' + en: 'Order' + help: + es: 'La posición del artículo en la lista de artículos' + en: 'The post position in the posts list' diff --git a/_data/layouts/product.yml b/_data/layouts/product.yml new file mode 100644 index 0000000..d14a7cb --- /dev/null +++ b/_data/layouts/product.yml @@ -0,0 +1,142 @@ +--- +title: + type: 'string' + required: true + label: + es: 'Producto' + en: 'Product' +description: + type: 'text' + required: true + label: + es: 'Alerta de contenido o descripción del libro' + en: 'Content warning or book description' + help: + es: | + Resumen del contenido del libro, que también usarán redes + sociales y buscadores. Si el libro trata de violencias y otros + temas sensibles, te invitamos a usar este campo como alerta de + contenido, para que las personas puedan determinar cuándo quieren + abrirlo. + en: | + Summary of book contents, also used by social media and search + engines. If the book is about violence or other sensitive + topics, we invite you to use it as a content warning, so others + can decide when they want to read it. +image: + type: 'image' + path: + label: + es: 'Imagen principal' + en: 'Main image' + help: + es: 'Resolución recomendada: 1280 píxeles de ancho' + en: 'Recommended resolution: 1280 pixels wide' + description: + label: + es: 'Descripción de la imagen' + en: 'Main image description' + help: + es: Describe la cubierta para usuaries no videntes y buscadores + en: | + Describe the book cover for blind or partially sighted users and + search engines +content: + type: 'content' + label: + es: 'Contenido' + en: 'Content' + help: + es: 'Escribe aquí el artículo' + en: 'Write the post here' +price: + type: 'number' + required: true + writable: 'once' + label: + es: 'Precio' + en: 'Price' + help: + es: 'El precio se gestiona a través de la tienda' + en: 'Price is managed through the store' +stock: + type: 'number' + required: true + writable: 'once' + label: + es: 'Stock inicial' + en: 'Starting stock' + help: + es: 'El stock se gestiona automáticamente a través de la tienda' + en: 'Stock is automatically managed by the store' +sku: + type: 'sku' + required: true + label: + es: 'SKU' + en: 'SKU' + help: + es: 'Código único del producto' + en: 'Product code' +cost_price: + type: 'number' + writable: 'once' + label: + es: 'Precio de costo' + en: 'Cost price' + help: + es: '' + en: '' +width: + type: 'number' + writable: 'once' + label: + es: 'Ancho' + en: 'Width' + help: + es: 'En milímetros' + en: 'In millimeters' +height: + type: 'number' + writable: 'once' + label: + es: 'Alto' + en: 'Height' + help: + es: 'En milímetros' + en: 'In millimeters' +depth: + type: 'number' + writable: 'once' + label: + es: 'Profundidad (Lomo)' + en: 'Depth' + help: + es: 'En milímetros' + en: 'In millimeters' +weight: + type: 'number' + required: true + writable: 'once' + label: + es: 'Peso' + en: 'Weight' + help: + es: 'En gramos, se utiliza para calcular el costo de envío' + en: 'In grams, used to calculate shipping rates' +order: + type: 'order' + label: + es: 'Orden' + en: 'Order' + help: + es: 'La posición del artículo en la lista de artículos' + en: 'The post position in the posts list' +draft: + type: 'boolean' + label: + es: 'Borrador' + en: 'Draft' + help: + es: 'Este artículo aun no está listo para publicar' + en: "This post isn't ready to be published yet" diff --git a/_data/layouts/shipment.yml b/_data/layouts/shipment.yml new file mode 100644 index 0000000..886c5ec --- /dev/null +++ b/_data/layouts/shipment.yml @@ -0,0 +1,126 @@ +--- +meta: + limit: 1 + help: + en: 'Página del proceso de envío. Si hay varias se toma la primera de la lista.' + es: 'Shipment page. If there are several, the first in the list will be used.' +title: + type: string + required: true + label: + es: Título + en: Title + help: + es: 'El título de la página de envío' + en: 'Title for Shipment page' + default: + es: 'Envío' + en: 'Shipment' +content: + type: 'content' + label: + es: 'Contenido' + en: 'Content' + help: + es: 'Podés agregar instrucciones, demora en las entregas, etc. aquí' + en: 'You can add instructions, shipping delay announcements, etc. here' +user: + type: 'string' + required: true + label: + es: 'Cuenta' + en: 'User' + help: + es: 'Subtítulo de contacto' + en: 'Contact subtitle' + default: + es: 'Contacto' + en: 'Contact' +shipping_address: + type: 'string' + required: true + label: + es: 'Subtítulo de la dirección de envío' + en: 'Shipping address subtitle' + help: + es: 'Texto para subtítulo de Dirección de envío' + en: 'Text for Shipping address subtitle' + default: + es: 'Dirección de envío' + en: 'Shipping address' +shipping_methods: + type: 'string' + required: true + label: + es: 'Subtítulo de métodos de envío' + en: 'Shipping options subtitle' + help: + es: 'Texto para subtítulo de métodos de envío' + en: 'Text for shipping options subtitle' + default: + es: 'Métodos de envío' + en: 'Shipping options' +shipping_methods_help: + type: 'text' + required: true + label: + es: 'Ayuda para los métodos de envío' + en: 'Help text for shipping options' + help: + es: 'El texto de la sección' + en: 'Help the client choose a shipping method' + default: + es: 'Completa la dirección de entrega para calcular los métodos de envío.' + en: 'Complete your shipping address to calculate available shipping methods.' +back: + type: 'string' + required: true + label: + es: 'Volver al carrito' + en: 'Back to cart' + help: + es: 'Nombre del botón para volver al carrito' + en: 'Text for Back to cart button' + default: + es: 'Volver al carrito' + en: 'Back to cart' +next_step: + type: 'string' + required: true + label: + es: 'Siguiente paso' + en: 'Next step' + help: + es: 'Texto del botón de pasar al siguiente paso de la compra (el pago)' + en: 'Text for next step button' + default: + es: 'Pagar' + en: 'Payment' +permalink: + type: 'permalink' + required: true + label: + es: 'Dirección de la página' + en: 'Shipment page address' + help: + es: 'La dirección de la página del carrito dentro del sitio' + en: 'The address this page has inside the site url' + default: + es: 'envio/' + en: 'shipment/' +draft: + type: 'boolean' + label: + es: 'Borrador' + en: 'Draft' + help: + es: 'Este artículo aun no está listo para publicar' + en: "This post isn't ready to be published yet" +order: + type: 'order' + label: + es: 'Orden' + en: 'Order' + help: + es: 'La posición del artículo en la lista de artículos' + en: 'The post position in the posts list' diff --git a/_includes/cart_add.html b/_includes/cart_add.html new file mode 100644 index 0000000..e3cce65 --- /dev/null +++ b/_includes/cart_add.html @@ -0,0 +1,8 @@ + diff --git a/_includes/cart_controller.html b/_includes/cart_controller.html new file mode 100644 index 0000000..115249b --- /dev/null +++ b/_includes/cart_controller.html @@ -0,0 +1,10 @@ +data-controller="cart" +data-target="stock.product" +data-cart-url="{{ include.product.url }}" +data-cart-variant-id="{{ include.product.variant_id }}" +data-cart-image="{{ include.product.image.path | thumbnail: 212, 300 }}" +data-cart-title="{{ include.product.title }}" +data-cart-price="{{ include.product.price }}" +data-cart-in-stock="{{ include.product.in_stock }}" +data-cart-stock="{{ include.product.stock }}" +data-cart-extra="{{ include.extra | join: '|' }}" diff --git a/_includes/country.html b/_includes/country.html new file mode 100644 index 0000000..92f1284 --- /dev/null +++ b/_includes/country.html @@ -0,0 +1,53 @@ +{% if include.form %} + {%- assign name = include.field[0] | append: ']' | prepend: '[' | prepend: include.form -%} + {%- assign id = include.field[1].id | default: name | replace: '[', '_' | remove: ']' -%} +{% else %} + {%- assign name = include.field[0] -%} + {%- assign id = include.field[1].id | default: name %} +{% endif %} + +{%- assign label = include.field[1].label[site.locale] -%} +{%- assign help = include.field[1].help[site.locale] -%} +{%- assign error = include.field[1].error[site.locale] -%} +{%- assign autocomplete = include.field[1].autocomplete -%} + +
+ + + + + + + + {%- if error -%} +
{{ error }}
+ {%- endif -%} + + {%- if help -%} + + {{ help }} + + {%- endif -%} + + + +
diff --git a/_includes/input.html b/_includes/input.html index cc674c2..2667aad 100644 --- a/_includes/input.html +++ b/_includes/input.html @@ -1,7 +1,14 @@ -{%- assign name = include.field[0] -%} -{%- assign id = include.field[1].id | default: name -%} +{% if include.form %} + {%- assign name = include.field[0] | append: ']' | prepend: '[' | prepend: include.form -%} + {%- assign id = include.field[1].id | default: name | replace: '[', '_' | remove: ']' -%} +{% else %} + {%- assign name = include.field[0] -%} + {%- assign id = include.field[1].id | default: name %} +{% endif %} + {%- assign label = include.field[1].label[site.locale] -%} {%- assign help = include.field[1].help[site.locale] -%} +{%- assign error = include.field[1].error[site.locale] -%} {%- assign autocomplete = include.field[1].autocomplete -%}
@@ -11,6 +18,7 @@ + {%- if error -%} +
{{ error }}
+ {%- endif -%} + {%- if help -%} {{ help }} diff --git a/_includes/postal_code.html b/_includes/postal_code.html new file mode 100644 index 0000000..151c6ac --- /dev/null +++ b/_includes/postal_code.html @@ -0,0 +1,45 @@ +{% if include.form %} + {%- assign name = include.field[0] | append: ']' | prepend: '[' | prepend: include.form -%} + {%- assign id = include.field[1].id | default: name | replace: '[', '_' | remove: ']' -%} +{% else %} + {%- assign name = include.field[0] -%} + {%- assign id = include.field[1].id | default: name %} +{% endif %} + +{%- assign label = include.field[1].label[site.locale] -%} +{%- assign help = include.field[1].help[site.locale] -%} +{%- assign error = include.field[1].error[site.locale] -%} +{%- assign autocomplete = include.field[1].autocomplete -%} + +
+ + + + + {%- if error -%} +
{{ error }}
+ {%- endif -%} + + {%- if help -%} + + {{ help }} + + {%- endif -%} +
diff --git a/_includes/submit.html b/_includes/submit.html index 6f82e96..863b3b3 100644 --- a/_includes/submit.html +++ b/_includes/submit.html @@ -1,5 +1,5 @@ diff --git a/_layouts/cart.html b/_layouts/cart.html new file mode 100644 index 0000000..8592a97 --- /dev/null +++ b/_layouts/cart.html @@ -0,0 +1,72 @@ +--- +layout: default +--- + +
+

{{ page.title }}

+
+ {{ content | replace: ' + +
+ +
+
+

{{ page.product }}

+
+ +
+
+
+

{{ page.price }}

+
+ +
+

{{ page.quantity }}

+
+ +
+

{{ page.subtotal }}

+
+ +
+

{{ page.remove }}

+
+
+
+ +
+
+
+ +
+ + +
+
+
+ +
+

{{ page.subtotal }}

+
+ +
+

0

+
+ +
+ + +
+
+
+
+
diff --git a/_layouts/confirmation.html b/_layouts/confirmation.html new file mode 100644 index 0000000..265be89 --- /dev/null +++ b/_layouts/confirmation.html @@ -0,0 +1,17 @@ +--- +layout: default +--- + +
+
+

{{ page.title }}

+
+ +
+ {{ content }} +
+ +

+ + {{ page.back }} +
diff --git a/_layouts/page.html b/_layouts/page.html new file mode 100644 index 0000000..5e71126 --- /dev/null +++ b/_layouts/page.html @@ -0,0 +1,5 @@ +--- +layout: default +--- + +{{ content }} diff --git a/_layouts/payment.html b/_layouts/payment.html new file mode 100644 index 0000000..af3ff98 --- /dev/null +++ b/_layouts/payment.html @@ -0,0 +1,21 @@ +--- +layout: default +--- + +
+
+

{{ page.title }}

+
+ +
+ {{ content }} +
+ +
+
+
diff --git a/_layouts/shipment.html b/_layouts/shipment.html new file mode 100644 index 0000000..9111a08 --- /dev/null +++ b/_layouts/shipment.html @@ -0,0 +1,52 @@ +--- +layout: default +--- + +
+
+

{{ page.title }}

+
+ +
+ {{ content }} +
+ +
+

{{ page.user }}

+ +
+ {% for field in site.data.forms.user %} + {% assign template = field[1].type | append: '.html' %} + {% include {{ template }} field=field form='user' %} + {% endfor %} +
+
+ +
+ +

{{ page.shipping_address }}

+ +
+ {% for field in site.data.forms.shipping_address %} + {% assign template = field[1].type | append: '.html' %} + {% include {{ template }} field=field %} + {% endfor %} +
+ +
+

{{ page.shipping_methods }}

+ +
+ {{ page.shipping_methods_help | markdownify }} +
+
+
+
diff --git a/_packs/controllers/cart_base_controller.js b/_packs/controllers/cart_base_controller.js new file mode 100644 index 0000000..5458396 --- /dev/null +++ b/_packs/controllers/cart_base_controller.js @@ -0,0 +1,249 @@ +import { Controller } from 'stimulus' +import { Liquid } from 'liquidjs' + +/* + * Base controller, shared methods go here and other classes extend from + * this. + */ +export class CartBaseController extends Controller { + get spree () { + return window.spree + } + + /* + * Gets all products from storage + * + * @return [Array] + */ + get products () { + 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) + } + + /* + * The storage + */ + get storage () { + return window.localStorage + } + + /* + * Temporary storage + */ + get storageTemp () { + return window.sessionStorage + } + + get cart () { + return JSON.parse(this.storage.getItem('cart')) + } + + set cart (response) { + this.storage.setItem('cart', JSON.stringify(response.success())) + } + + get email () { + return this.storageTemp.getItem('email') + } + + set email (email) { + this.storageTemp.setItem('email', email) + } + + /* + * The orderToken + * + * @return [String] + */ + get token () { + return this.storage.getItem('token') + } + + /* + * The bearerToken + * + * @return [String] + */ + get bearerToken () { + return this.storageTemp.getItem('bearerToken') + } + + set bearerToken (token) { + this.storageTemp.setItem('bearerToken', token) + } + + /* + * Liquid renderer + * + * @return Liquid + */ + get engine () { + if (!window.liquid) window.liquid = new Liquid() + + return window.liquid + } + + /* + * Site config (actually just translation strings) + * + * @return [Object] + */ + async site () { + if (!window.site) { + const data = await fetch('assets/data/site.json') + window.site = await data.json() + } + + return window.site + } + + /* + * Updates the item counter + */ + counterUpdate () { + const item_count = this.cart.data.attributes.item_count + + window.dispatchEvent(new CustomEvent('cart:counter', { detail: { item_count }})) + this.storage.setItem('cart:counter', item_count) + } + + /* + * Removes the brackets from the name or returns the name + * + * @return [String] + */ + idFromInputName (input) { + const matches = input.name.match(/\[([^\]]+)\]$/) + + return (matches === null) ? input.name : matches[1] + } + + async handleFailure (response) { + const data = { type: 'primary' } + let template = 'alert' + + if (!window.airbrake) window.airbrake.notify(response.fail()) + + const site = await this.site() + + switch (response.fail().name) { + case 'MisconfigurationError': + data.content = response.fail().message + break + case 'NoResponseError': + data.content = site.i18n.alerts.no_response_error + break + case 'SpreeError': + data.content = site.i18n.alerts.spree_error + break + case 'BasicSpreeError': + // XXX: The order is missing, we need to start a new one + if (response.fail().serverResponse.status === 404) { + template = 'recover_order' + data.content = site.i18n.alerts.recover_order + } else { + data.content = response.fail().summary + } + + break + case 'ExpandedSpreeError': + data.content = response.fail().summary + + break + default: + data.content = response.fail().message + } + + console.error(response.fail().name, data.content) + + window.dispatchEvent(new CustomEvent('notification', { detail: { template, data } })) + } + + set formDisabled (state) { + if (!this.hasFormTarget) return + + this.formTarget.elements.forEach(x => x.disabled = !!state) + } + + assignShippingAddress () { + if (!this.bearerToken) return + + const bearerToken = this.bearerToken + const orderToken = this.token + + this.spree.sutty.assignOrderShippingAddress({ bearerToken }, { orderToken }) + } + + notify (data = {}) { + window.dispatchEvent(new CustomEvent('notification', { detail: { template: 'alert', data } })) + } + + /* + * Visita una página con soporte para Turbolinks + * + * @param [String] URL + */ + visit (url) { + try { + Turbolinks.visit(url) + } catch { + window.location = url + } + } + + async firstAddress (bearerToken) { + if (!this._firstAddress) { + const addresses = await this.spree.account.addressesList({ bearerToken }) + + if (addresses.isFail()) { + this.handleFailure(addresses) + return + } + + // XXX: Asumimos que si se registró tiene una dirección que vamos + // a actualizar. + this._firstAddress = addresses.success().data[0] + } + + return this._firstAddress + } + + async updateAddress(bearerToken, id, address) { + const updateAddress = await this.spree.account.updateAddress({ bearerToken }, id, { address }) + + if (updateAddress.isFail()) { + this.handleFailure(updateAddress) + return + } + + return updateAddress.success() + } + + async shippingMethods(orderToken) { + const shippingMethods = await this.spree.checkout.shippingMethods({ orderToken }, { include: 'shipping_rates' }) + + if (shippingMethods.isFail()) { + this.handleFailure(shippingMethods) + return + } + + return shippingMethods.success() + } + + fireCajon (state = 'open', cajon = 'cajon') { + window.dispatchEvent(new CustomEvent('cajon', { detail: { cajon, state }})) + } + + formDataToObject (formData) { + const object = {} + + for (const field of formData) { + if (field[0].startsWith('_ignore_')) continue + + object[field[0]] = field[1] + } + + return object + } +} diff --git a/_packs/controllers/cart_confirmation_controller.js b/_packs/controllers/cart_confirmation_controller.js new file mode 100644 index 0000000..74e4d48 --- /dev/null +++ b/_packs/controllers/cart_confirmation_controller.js @@ -0,0 +1,39 @@ +import { CartBaseController } from './cart_base_controller' + +export default class extends CartBaseController { + static targets = [ 'order' ] + + async connect () { + if (this.clear) this.storage.clear() + + if (!this.template) return + + const order = this.cart.data.attributes + const products = this.products + const site = await this.site() + const shipping_address = JSON.parse(this.storage.getItem('shipping_address')) + + const data = { + order, + products, + site, + shipping_address + } + + this.render(data) + } + + render (data = {}) { + fetch(this.template).then(r => r.text()).then(template => { + this.engine.parseAndRender(template, data).then(html => this.orderTarget.innerHTML = html) + }) + } + + get template () { + return this.element.dataset.template + } + + get clear () { + return this.element.dataset.clear + } +} diff --git a/_packs/controllers/cart_contact_controller.js b/_packs/controllers/cart_contact_controller.js new file mode 100644 index 0000000..b663c3e --- /dev/null +++ b/_packs/controllers/cart_contact_controller.js @@ -0,0 +1,24 @@ +import { CartBaseController } from './cart_base_controller' + +export default class extends CartBaseController { + static targets = [ 'form', 'username' ] + + connect () { + if (!this.hasUsernameTarget) return + if (!this.hasFormTarget) return + + this.formTarget.addEventListener('focusout', event => { + if (!this.formTarget.checkValidity()) { + this.formTarget.classList.add('was-validated') + return + } + + this.formTarget.classList.remove('was-validated') + + const username = this.usernameTarget.value.trim() + if (username.length === 0) return + + this.email = username + }) + } +} diff --git a/_packs/controllers/cart_controller.js b/_packs/controllers/cart_controller.js new file mode 100644 index 0000000..e711bd8 --- /dev/null +++ b/_packs/controllers/cart_controller.js @@ -0,0 +1,264 @@ +import { CartBaseController } from './cart_base_controller' + +/* + * Manages the cart and its contents. + * + * We need to create an order in Spree API to get a token that allows to + * make changes to it. + * + * The order contains attributes for the order and other data can be + * included, like line items, variants, payments, promotions, shipments, + * billing and shipping addresses, and the user. + * + * Variants are products added to the cart. To remove an item or change + * its quantity, a line item for the variant must be found. We store + * this information into localStorage so we don't have to make annoying + * queries to JSON:API everytime. + */ + +export default class extends CartBaseController { + static targets = [ 'quantity', 'subtotal', 'addedQuantity' ] + + connect () { + if (!this.hasQuantityTarget) return + + /* + * When the quantity selector changes, we update the order to have + * that amount of items. + * + * TODO: Go back to previous amount if there's not enough. + */ + this.quantityTarget.addEventListener('change', async (event) => { + const quantity = event.target.value + + if (quantity < 1) return; + + const orderToken = await this.tokenGetOrCreate() + + event.target.disabled = true + + const response = await this.spree.cart.setQuantity({ orderToken }, { + line_item_id: this.product.line_item.id, + quantity, + include: 'line_items' + }) + + event.target.disabled = false + event.target.focus() + + // If we're failing here it could be due to a missing order, so we + // ask the user to decide what they want to do about it + if (response.isFail()) { + this.handleFailure(response) + return + } + + this.cart = response + this.subtotalUpdate() + this.counterUpdate() + await this.itemStore() + + if (!this.hasSubtotalTarget) return + + this.subtotalTarget.innerText = this.product.line_item.attributes.discounted_amount + }) + } + + subtotalUpdate () { + window.dispatchEvent(new Event('cart:subtotal:update')) + } + + /* + * Creates an order and stores the data into localStorage. + * + * @return [String] + */ + async cartCreate () { + const response = await this.spree.cart.create() + + // If we fail here it's probably a server error, so we inform the + // user. + if (response.isFail()) { + this.handleFailure(response) + return + } + + this.cart = response + this.storage.setItem('token', response.success().data.attributes.token) + + return this.token + } + + /* + * Gets the order token and creates a cart if it doesn't exist. + * + * @return [String] + */ + async tokenGetOrCreate () { + let token = this.storage.getItem('token') + + return token || await this.cartCreate() + } + + /* + * The variant ID is used to identify products + * + * @return [String] + */ + get variantId () { + return this.data.get('variantId') + } + + get product () { + return JSON.parse(this.storage.getItem(this.storageId)) + } + + /* + * Obtains the line_item_id by a variant_id by inspecting the cart and + * its included items + * + * @return [Object] + */ + findLineItem () { + const line_item = this.cart.included.find(x => (x.type === 'line_item' && x.relationships.variant.data.id == this.variantId)) + + return (line_item || {}) + } + + get storageId () { + return `cart:item:${this.variantId}` + } + + /* + * Stores an item for later usage. + * + * @see {./order_controller.js} + */ + itemStore () { + this.storage.setItem(this.storageId, JSON.stringify({ + variant_id: this.variantId, + line_item: this.findLineItem(), + image: this.data.get('image'), + title: this.data.get('title'), + url: this.data.get('url'), + stock: this.data.get('stock'), + in_stock: this.data.get('inStock'), + extra: this.data.get('extra') ? this.data.get('extra').split('|') : [] + })) + } + + /* + * Adds item to cart. This is meant to be used by an "Add to cart" + * button. If the item already exists in the cart it updates the + * quantity by +1. + * + * The item needs a variant ID to be added. + */ + async add(event, quantity = 1, floating_alert = true) { + const addedQuantity = this.addedQuantity() + if (addedQuantity > 1) quantity = addedQuantity + + const orderToken = await this.tokenGetOrCreate() + const response = await this.spree.cart.addItem({ orderToken }, { variant_id: this.variantId, quantity, include: 'line_items' }) + + if (response.isFail()) { + this.handleFailure(response) + return + } + + this.cart = response + this.itemStore() + this.counterUpdate() + this.fireCajon() + + if (floating_alert) { + const site = await this.site() + const content = site.cart.added + window.dispatchEvent(new CustomEvent('floating:alert', { detail: { content }})) + } + } + + /* + * Remove the element from the cart. It contacts the API and if the + * item is removed, it removes itself from the page and the storage. + */ + async remove () { + if (!this.product.line_item) return + + const orderToken = this.token + const response = await this.spree.cart.removeItem({ orderToken }, this.product.line_item.id, { include: 'line_items' }) + + if (response.isFail()) { + this.handleFailure(response) + return + } + + this.cart = response + this.storage.removeItem(this.storageId) + this.element.remove() + this.subtotalUpdate() + this.counterUpdate() + } + + /* + * Shows variants + */ + async variants () { + const template = 'variants' + const data = { + product: { + variant_id: this.data.get('variantId'), + digital_variant_id: this.data.get('digitalVariantId'), + image: this.data.get('image'), + title: this.data.get('title'), + price: this.data.get('price'), + digital_price: this.data.get('digitalPrice'), + in_stock: this.data.get('inStock'), + extra: this.data.get('extra').split('|') + } + } + + window.dispatchEvent(new CustomEvent('notification', { detail: { template, data } })) + } + + /* + * Recovers the order if something failed + */ + async recover () { + // Removes the failing token + this.storage.removeItem('token') + + // Stores the previous cart + const cart = this.cart + + // Get a new token and cart + await this.tokenGetOrCreate() + + // Add previous items and their quantities to the new cart by + // mimicking user's actions + // + // XXX: We don't use forEach because it's not async + for (const variant of cart.data.relationships.variants.data) { + this.data.set('variantId', variant.id) + + const product = this.product + + this.data.set('image', product.image) + this.data.set('title', product.title) + this.data.set('extra', product.extra.join('|')) + + await this.add(null, product.line_item.attributes.quantity, false) + } + } + + /* + * Si le compradore aumenta la cantidad antes de agregar + */ + addedQuantity () { + if (!this.hasAddedQuantityTarget) return 0 + + const addedQuantity = parseInt(this.addedQuantityTarget.value) + + return (isNaN(addedQuantity) ? 0 : addedQuantity) + } +} diff --git a/_packs/controllers/cart_counter_controller.js b/_packs/controllers/cart_counter_controller.js new file mode 100644 index 0000000..0db1abd --- /dev/null +++ b/_packs/controllers/cart_counter_controller.js @@ -0,0 +1,25 @@ +import { CartBaseController } from './cart_base_controller' + +export default class extends CartBaseController { + static targets = [ 'counter' ] + + connect () { + if (!this.hasCounterTarget) { + console.error("Missing counter target") + return + } + + window.addEventListener('cart:counter', event => this.counter = event.detail.item_count) + window.addEventListener('storage', event => { + if (event.key == 'cart:counter') this.counter = event.newValue + }) + + if (!this.cart) return + + this.counter = this.cart.data.attributes.item_count + } + + set counter (quantity) { + this.counterTarget.innerText = quantity + } +} diff --git a/_packs/controllers/cart_payment_methods_controller.js b/_packs/controllers/cart_payment_methods_controller.js new file mode 100644 index 0000000..e6b5217 --- /dev/null +++ b/_packs/controllers/cart_payment_methods_controller.js @@ -0,0 +1,89 @@ +import { CartBaseController } from './cart_base_controller' + +/* + * Retrieves payment methods and redirect to external checkouts + */ +export default class extends CartBaseController { + static targets = [ 'form', 'submit' ] + + async connect () { + const orderToken = this.token + const response = await this.spree.checkout.paymentMethods({ orderToken }) + + if (response.isFail()) { + this.handleFailure(response) + return + } + + const payment_methods = response.success().data + const site = await this.site() + const cart = this.cart + const next = { url: this.data.get('nextUrl') } + const back = { url: this.data.get('backUrl') } + + this.render({ payment_methods, site, cart, next, back }) + } + + async render (data = {}) { + const request = await fetch(this.data.get('template')) + const template = await request.text() + + this.element.innerHTML = await this.engine.parseAndRender(template, data) + + if (!this.hasSubmitTarget) return + this.formTarget.elements.forEach(p => p.addEventListener('change', e => this.submitTarget.disabled = false)) + } + + async pay (event) { + event.preventDefault() + event.stopPropagation() + + this.formDisabled = true + + const payment_method_id = this.formTarget.elements.payment_method_id.value + const orderToken = this.token + + // XXX: Currently SpreeClient expects us to send payment source + // attributes as if it were a credit card. + let response = await this.spree.checkout.orderUpdate({ orderToken }, + { + order: { payments_attributes: [{ payment_method_id }] }, + payment_source: { + [payment_method_id]: { + name: 'Pepitx', + month: 12, + year: 2020 + } + } + }) + + if (response.isFail()) { + this.handleFailure(response) + this.formDisabled = false + return + } + + this.cart = response + + response = await this.spree.checkout.complete({ orderToken }) + + if (response.isFail()) { + this.handleFailure(response) + this.formDisabled = false + return + } + + this.cart = response + + const checkoutUrls = await this.spree.sutty.getCheckoutURL({ orderToken }) + let redirectUrl = this.data.get('nextUrl') + + if (checkoutUrls.data.length > 0) redirectUrl = checkoutUrls.data[0] + + try { + Turbolinks.visit(redirectUrl) + } catch { + window.location = redirectUrl + } + } +} diff --git a/_packs/controllers/cart_paypal_confirmation_controller.js b/_packs/controllers/cart_paypal_confirmation_controller.js new file mode 100644 index 0000000..3410eb3 --- /dev/null +++ b/_packs/controllers/cart_paypal_confirmation_controller.js @@ -0,0 +1,42 @@ +import { CartBaseController } from './cart_base_controller' + +/* + * Replaces checkout.js. + * + * When the customer is redirected back from the approval URL, there's + * three params attached to the URL. We need paymentId and PayerID to + * execute the payment and later capture it via IPN. The token can be + * discarded. + */ +export default class extends CartBaseController { + async connect () { + if (this.params.PayerID === undefined) return + + this.site = await this.site() + this.element.innerHTML = this.site.i18n.cart.layouts.paypal.confirming + + fetch(this.executeURL) + .then(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) + } + + /* + * Convert URL params to Object + * + * @return [Object] + */ + get params () { + if (this._params) return this._params + + this._params = Object.fromEntries(decodeURIComponent(window.location.search.replace('?', '')).split('&').map(x => x.split('='))) + + return this._params + } + + /* + * URL to contact the store and execute the payment. + */ + get executeURL () { + return [ window.spree.host, 'paypal', 'execute', this.params.orderId, this.params.paymentId, this.params.PayerID ].join('/') + } +} diff --git a/_packs/controllers/cart_shipping_controller.js b/_packs/controllers/cart_shipping_controller.js new file mode 100644 index 0000000..d78d906 --- /dev/null +++ b/_packs/controllers/cart_shipping_controller.js @@ -0,0 +1,111 @@ +import { CartBaseController } from './cart_base_controller' + +export default class extends CartBaseController { + static targets = [ 'methods', 'rates', 'form' ] + + connect () { + this.formTarget.addEventListener('formdata', event => this.processShippingAddress(event.formData)) + } + + async rates (event) { + event.preventDefault() + event.stopPropagation() + + if (!this.formTarget.checkValidity()) { + this.adressTarget.classList.add('was-validated') + return + } + + this.formTarget.classList.remove('was-validated') + + // FormDataEvent es muy reciente + if (window.FormDataEvent) { + // Esto lanza el evento formdata en el formulario + new FormData(event.target) + } else { + // Fallback + this.processShippingAddress(new FormData(event.target)) + } + } + + payment (event) { + event.preventDefault() + event.stopPropagation() + + // FormDataEvent es muy reciente + if (window.FormDataEvent) { + // Esto lanza el evento formdata en el formulario + new FormData(event.target) + } else { + this.processShippingRate(new FormData(event.target)) + } + } + + async processShippingAddress (formData) { + this.formDisabled = true + + const email = this.email + const orderToken = this.token + + const ship_address_attributes = this.formDataToObject(formData) + const bill_address_attributes = ship_address_attributes + + const response = await this.spree.checkout.orderUpdate({ orderToken }, { order: { email, ship_address_attributes, bill_address_attributes }}) + + if (response.isFail()) { + this.handleFailure(response) + this.formDisabled = false + return + } + + const shippingMethods = await this.shippingMethods(orderToken) + + if (!shippingMethods) { + this.formDisabled = false + return + } + + const shipping_rates = shippingMethods.included.filter(x => x.type == 'shipping_rate') + // XXX: No hay varios paquetes + const shipping_method = shippingMethods.data[0] + const site = await this.site() + + await this.render({ shipping_method, shipping_rates, site }) + + const nextStep = document.querySelector(`#${this.element.dataset.scrollTo}`) + if (nextStep) nextStep.scrollIntoView() + } + + async processShippingRate (formData) { + const rate = this.formDataToObject(formData) + const orderToken = this.token + + // XXX: Deshabilitar el formulario después del evento FormData, de + // lo contrario el objeto queda vacío. + this.ratesTarget.elements.forEach(x => x.disabled = true) + + const response = await window.spree.checkout.orderUpdate({ orderToken }, { order: { shipments_attributes: [{ ...rate }] } }) + + if (response.isFail()) { + this.handleFailure(response) + return + } + + this.cart = response + + // Continue to next step + try { + Turbolinks.visit(this.data.get('next')) + } catch { + window.location = this.data.get('next') + } + } + + async render (data = {}) { + const request = await fetch(this.data.get('template')) + const template = await request.text() + + this.methodsTarget.innerHTML = await this.engine.parseAndRender(template, data) + this.ratesTarget.addEventListener('formdata', event => this.processShippingRate(event.formData)) + } +} diff --git a/_packs/controllers/country_controller.js b/_packs/controllers/country_controller.js new file mode 100644 index 0000000..3f8476f --- /dev/null +++ b/_packs/controllers/country_controller.js @@ -0,0 +1,101 @@ +import { CartBaseController } from './cart_base_controller' + +/* + * Populates a country field where users can type to filter and select + * from a predefined list. + */ +export default class extends CartBaseController { + // All are required! + static targets = [ 'id', 'iso', 'list', 'name' ] + + async connect () { + const countries = await this.countries() + + countries.forEach(country => { + const option = document.createElement('option') + + option.value = country.attributes.name + option.dataset.id = country.id + option.dataset.iso = country.attributes.iso + option.dataset.statesRequired = country.attributes.states_required + option.dataset.zipcodeRequired = country.attributes.zipcode_required + + this.listTarget.appendChild(option) + }) + + const site = await this.site() + + // Only allow names on this list + this.nameTarget.pattern = countries.map(x => x.attributes.name).join('|') + this.nameTarget.addEventListener('input', event => 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 + // state list via an Event + this.nameTarget.addEventListener('change', event => { + const value = this.nameTarget.value.trim() + + if (value === '') return + + const options = Array.from(this.nameTarget.list.options) + const option = options.find(x => x.value == value) + + // TODO: If no option is found, mark the field as invalid + if (!option) return + + this.idTarget.value = option.dataset.id + this.isoTarget.value = option.dataset.iso + + this.idTarget.dispatchEvent(new Event('change')) + this.isoTarget.dispatchEvent(new Event('change')) + + this.dispatchChangedEvent(option.dataset) + + // XXX: Prevent mixing data + delete this.nameTarget.dataset.selectedState + delete this.nameTarget.dataset.selectedZipcode + }) + + // The input is disabled at this point + this.nameTarget.disabled = false + // Load data if the input is autocompleted + if (this.nameTarget.value.trim() !== '') this.nameTarget.dispatchEvent(new CustomEvent('change')) + } + + /* + * Sends a `cart:country:update` event so other controllers can + * subscribe to changes. + */ + dispatchChangedEvent (data = {}) { + const event = new CustomEvent('cart:country:update', { + detail: { + id: this.idTarget.value, + iso: this.isoTarget.value, + group: this.data.get('group'), + selectedState: this.nameTarget.dataset.selectedState, + selectedZipcode: this.nameTarget.dataset.selectedZipcode, + data + } + }) + + window.dispatchEvent(event) + } + + /* + * Fetch the country list from storage or from API + */ + async countries () { + const countries = JSON.parse(this.storageTemp.getItem('countries')) + + if (countries) return countries + + const response = await this.spree.countries.list() + + // TODO: Show error message + if (!response.success()) return + + this.storageTemp.setItem('countries', JSON.stringify(response.success().data)) + + return response.success().data + } +} diff --git a/_packs/controllers/order_controller.js b/_packs/controllers/order_controller.js new file mode 100644 index 0000000..dea675f --- /dev/null +++ b/_packs/controllers/order_controller.js @@ -0,0 +1,68 @@ +import { CartBaseController } from './cart_base_controller' + +/* + * Renders the order table. All products are stored on localStorage, so + * we just fetch that information and render the cart contents using + * Liquid. + */ +export default class extends CartBaseController { + static targets = [ 'cart', 'subtotal', 'itemCount' ] + + async connect () { + const products = this.products + const site = await this.site() + + this.render({ products, site }) + this.subtotalUpdate() + this.itemCountUpdate() + this.subscribe() + } + + /* + * Subscribe to change on the storage to update the cart. + */ + subscribe () { + window.addEventListener('storage', async event => { + if (!event.key.startsWith('cart:item:')) return + + const products = this.products + const site = await this.site() + + this.render({ products, site }) + this.subtotalUpdate() + this.itemCountUpdate() + }) + + window.addEventListener('cart:subtotal:update', event => { + this.itemCountUpdate() + this.subtotalUpdate() + }) + } + + /* + * Download the item template and render the order + */ + render (data = {}) { + fetch(this.data.get('itemTemplate')).then(r => r.text()).then(template => { + this.engine.parseAndRender(template, data).then(html => this.cartTarget.innerHTML = html) + }) + } + + /* + * Updates the subtotal + * + * XXX: This also updates the currency + */ + subtotalUpdate () { + if (!this.cart) return + + this.subtotalTarget.innerText = this.cart.data.attributes.display_total + } + + itemCountUpdate () { + if (!this.hasItemCountTarget) return + if (!this.cart) return + + this.itemCountTarget.innerText = this.cart.data.attributes.item_count + } +} diff --git a/_packs/controllers/postal_code_controller.js b/_packs/controllers/postal_code_controller.js new file mode 100644 index 0000000..9a65db5 --- /dev/null +++ b/_packs/controllers/postal_code_controller.js @@ -0,0 +1,38 @@ +import { Controller } from 'stimulus' + +/* + * Subscribes to the country change event and changes the validation + * pattern of its input. + */ +export default class extends Controller { + static targets = [ 'code' ] + + /* + * Twitter CLDR is pretty big and we only need the postal codes + * patterns. + * + * @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}$"} + + connect () { + window.addEventListener('cart:country:update', event => { + if (this.data.get('group') !== event.detail.group) return + + const zipcodeRequired = event.detail.data.zipcodeRequired == 'true' + + this.codeTarget.value = '' + this.codeTarget.disabled = !zipcodeRequired + this.codeTarget.required = zipcodeRequired + + if (!zipcodeRequired) return + + this.codeTarget.pattern = this.postal_codes[event.detail.iso.toLowerCase()] || '.*' + + if (event.detail.selectedZipcode) { + this.codeTarget.value = event.detail.selectedZipcode + this.codeTarget.dispatchEvent(new Event('change')) + } + }) + } +} diff --git a/_packs/controllers/state_controller.js b/_packs/controllers/state_controller.js new file mode 100644 index 0000000..b0f3949 --- /dev/null +++ b/_packs/controllers/state_controller.js @@ -0,0 +1,91 @@ +import { CartBaseController } from './cart_base_controller' + +/* + * Populates a state field where users can type to filter and select + * from a predefined list. It waits for an `cart:country:update` event + * to become populated. + */ +export default class extends CartBaseController { + // All are required! + static targets = [ 'id', 'list', 'name' ] + + connect () { + window.addEventListener('cart:country:update', async event => { + if (this.data.get('group') !== event.detail.group) return + + this.idTarget.value = '' + this.nameTarget.value = '' + this.listTarget.innerHTML = '' + + const statesRequired = event.detail.data.statesRequired == 'true' + + this.nameTarget.disabled = !statesRequired + this.nameTarget.required = statesRequired + + if (!statesRequired) return + + const states = await this.states(event.detail.iso) + const site = await this.site() + + states.forEach(state => { + let option = document.createElement('option') + option.value = state.attributes.name + option.dataset.id = state.id + + this.listTarget.appendChild(option) + }) + + this.nameTarget.pattern = states.map(x => x.attributes.name).join('|') + this.nameTarget.addEventListener('input', event => this.nameTarget.setCustomValidity('')) + this.nameTarget.addEventListener('invalid', event => this.nameTarget.setCustomValidity(site.i18n.states.validation)) + + if (event.detail.selectedState) { + this.nameTarget.value = event.detail.selectedState + this.nameTarget.dispatchEvent(new Event('change')) + } + }) + + // When the input changes we update the actual value and also the + // state list via an Event + this.nameTarget.addEventListener('change', event => { + const options = Array.from(this.listTarget.options) + const option = options.find(x => x.value == this.nameTarget.value) + + // TODO: If no option is found, mark the field as invalid + if (!option) return + + this.idTarget.value = option.dataset.id + this.idTarget.dispatchEvent(new Event('change')) + }) + } + + /* + * Fetch the state list from storage or from API using a country ISO + * code + */ + async states (countryIso) { + const stateId = `states:${countryIso}` + let states = JSON.parse(this.storageTemp.getItem(stateId)) + + if (states) return states + + // There's no state query, but we can fetch the country and include + // its states. + const response = await this.spree.countries.show(countryIso, { include: 'states' }) + + // TODO: Show error message + if (response.isFail()) { + this.handleFailure(response) + return {} + } + + states = response.success().included + + // Order alphabetically by name + states.sort((x, y) => x.attributes.name > y.attributes.name) + + this.storageTemp.setItem(stateId, JSON.stringify(states)) + + return states + } +} diff --git a/_packs/controllers/stock_controller.js b/_packs/controllers/stock_controller.js new file mode 100644 index 0000000..b8cea93 --- /dev/null +++ b/_packs/controllers/stock_controller.js @@ -0,0 +1,71 @@ +import { Controller } from 'stimulus' + +/* + * Mantiene el stock actualizado, consultando a la API. + * + * * Obtiene todas las variantes en el controlador + * * Consulta a la API + * * Actualiza stock y precio + * * Deshabilita botón si no está en stock + */ +export default class extends Controller { + static targets = [ 'product' ] + + async connect () { + if (this.variant_ids.length === 0) return + + const ids = this.variant_ids.join(',') + const filter = { ids } + let response = await window.spree.products.list({ filter }) + + // TODO: Gestionar errores + if (response.isFail()) { + console.error(response.fail()) + return + } + + this.update_local_products(response.success().data) + + // Recorrer todas las páginas + // XXX: Podríamos usar next pero la página 1 siempre se devuelve a + // sí misma y entraríamos en un loop infinito. + for (let page = 2; page <= response.success().meta.total_pages; page++) { + response = await window.spree.products.list({ filter, page }) + + // TODO: Gestionar errores + if (response.isFail()) { + console.error(response.fail()) + continue + } + + this.update_local_products(response.success().data) + } + } + + /* + * La lista de todas las variantes incluidas en el controlador que no + * estén vacías. + * + * @return [Array] + */ + get variant_ids () { + if (!this._variant_ids) this._variant_ids = [...new Set(this.productTargets.map(p=> p.dataset.cartVariantId).filter(x => x.length > 0))] + + return this._variant_ids + } + + /* + * Los productos pueden estar duplicados así que buscamos todos. + */ + update_local_products (products) { + for (const product of products) { + for (const local of this.productTargets.filter(local => local.dataset.cartVariantId === product.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.querySelectorAll('[data-stock-price]').forEach(price => price.innerText = product.attributes.display_price) + } + } + } +} diff --git a/_packs/endpoints/sutty.js b/_packs/endpoints/sutty.js new file mode 100644 index 0000000..3b9c834 --- /dev/null +++ b/_packs/endpoints/sutty.js @@ -0,0 +1,80 @@ +import Axios from 'axios' +import * as qs from 'qs' + +/* + * XXX: We're copying code from @spree/storefront-api-v2-sdk/src/Http.ts + * because we don't know how to mix Typescript :D + */ +export class Sutty { + constructor (host = 'http://localhost:3000') { + this.host = host + + this.axios = Axios.create({ + baseURL: this.host, + headers: { 'Content-Type': 'application/json' }, + paramsSerializer: params => qs.stringify(params, { arrayFormat: 'brackets' }) + }) + } + + async getCheckoutURL(tokens = {}) { + const headers = this.spreeOrderHeaders(tokens) + const axiosConfig = { + url: 'api/v2/storefront/checkout_redirect.json', + params: {}, + method: 'get', + headers + } + + return await this.axios(axiosConfig) + } + + async assignOrderOwnership(tokens = {}, params = {}) { + const headers = this.spreeOrderHeaders(tokens) + const axiosConfig = { + url: 'api/v2/storefront/assign_order_ownership.json', + params, + method: 'post', + headers + } + + return await this.axios(axiosConfig) + } + + async assignOrderShippingAddress(tokens = {}, params = {}) { + const headers = this.spreeOrderHeaders(tokens) + const axiosConfig = { + url: 'api/v2/storefront/assign_order_shipping_address.json', + params, + method: 'post', + headers + } + + return await this.axios(axiosConfig) + } + + async assignOrderBillingAddress(tokens = {}, params = {}) { + const headers = this.spreeOrderHeaders(tokens) + const axiosConfig = { + url: 'api/v2/storefront/assign_order_billing_address.json', + params, + method: 'post', + headers + } + + return await this.axios(axiosConfig) + } + + spreeOrderHeaders(tokens) { + const header = {} + + if (tokens.orderToken) { + header['X-Spree-Order-Token'] = tokens.orderToken + } + + if (tokens.bearerToken) { + header['Authorization'] = `Bearer ${tokens.bearerToken}` + } + + return header + } +} diff --git a/_packs/entry.js b/_packs/entry.js index 44fd39e..d2be181 100644 --- a/_packs/entry.js +++ b/_packs/entry.js @@ -19,6 +19,12 @@ const application = Application.start() const context = require.context("./controllers", true, /\.js$/) application.load(definitionsFromContext(context)) +import { makeClient } from '@spree/storefront-api-v2-sdk' +import { Sutty } from './endpoints/sutty' + +window.spree = makeClient({ host: window.env.SPREE_URL }) +window.spree.sutty = new Sutty(window.spree.host) + // Prevenir que Turbolinks interfiera con la navegación por anchors // https://github.com/turbolinks/turbolinks/issues/75#issuecomment-445325162 document.addEventListener('turbolinks:click', event => { diff --git a/env.js b/env.js index 6210493..74d8c59 100644 --- a/env.js +++ b/env.js @@ -4,5 +4,6 @@ window.env = { AIRBRAKE_PROJECT_ID: {{ site.env.AIRBRAKE_PROJECT_ID | default: 0 }}, AIRBRAKE_PROJECT_KEY: '{{ site.env.AIRBRAKE_PROJECT_KEY }}', - JEKYLL_ENV: '{{ site.env.JEKYLL_ENV }}' + JEKYLL_ENV: '{{ site.env.JEKYLL_ENV }}', + SPREE_URL: '{{ site.env.SPREE_URL }}' } diff --git a/package.json b/package.json index ae1140a..6dcf2f0 100644 --- a/package.json +++ b/package.json @@ -12,6 +12,7 @@ "@babel/core": "^7.10.4", "@babel/plugin-proposal-class-properties": "^7.10.4", "@babel/preset-env": "^7.10.4", + "@spree/storefront-api-v2-sdk": "~4.4", "axe-core": "^4.1.2", "babel-loader": "^8.1.0", "core-js": "^3.6.5", diff --git a/sutty-base-jekyll-theme.gemspec b/sutty-base-jekyll-theme.gemspec index 11f23b0..d1e8b24 100644 --- a/sutty-base-jekyll-theme.gemspec +++ b/sutty-base-jekyll-theme.gemspec @@ -68,6 +68,8 @@ Gem::Specification.new do |spec| spec.add_runtime_dependency 'jekyll-commonmark', '~> 1.3' spec.add_runtime_dependency 'jekyll-dotenv', '>= 0.2' spec.add_runtime_dependency 'jekyll-feed', '~> 0.15' + spec.add_runtime_dependency 'jekyll-spree-client', '~> 0' + spec.add_runtime_dependency 'jekyll-write-and-commit-changes', '~> 0' spec.add_runtime_dependency 'jekyll-ignore-layouts', '~> 0' # Dependencias de desarrollo diff --git a/yarn.lock b/yarn.lock index ca44c87..a033e23 100644 --- a/yarn.lock +++ b/yarn.lock @@ -885,6 +885,15 @@ resolved "https://registry.yarnpkg.com/@sindresorhus/is/-/is-0.14.0.tgz#9fb3a3cf3132328151f353de4632e01e52102bea" integrity sha512-9NET910DNaIPngYnLLPeg+Ogzqsi9uM4mSboU5y6p8S5DzMTVEsJZrawi+BoDNUVBa2DhJqQYUFvMDfgU062LQ== +"@spree/storefront-api-v2-sdk@~4.4": + version "4.4.4" + resolved "https://registry.yarnpkg.com/@spree/storefront-api-v2-sdk/-/storefront-api-v2-sdk-4.4.4.tgz#c3527e9e08d7c436892bdece614ce1b66093b1bf" + integrity sha512-rkBBGYhnup0VGOltyQb+uycT9+M32LZpA9K5SYukvexbkCCCj0JqMJlY9ojirwsDh/q71icRZ+PAjDfypbbm6g== + dependencies: + axios "^0.21.1" + lodash "^4.17.20" + qs "^6.6.0" + "@stimulus/core@^1.1.1": version "1.1.1" resolved "https://registry.yarnpkg.com/@stimulus/core/-/core-1.1.1.tgz#42b0cfe5b73ca492f41de64b77a03980bae92c82" @@ -1383,6 +1392,13 @@ axe-core@^4.1.2: resolved "https://registry.yarnpkg.com/axe-core/-/axe-core-4.2.1.tgz#2e50bcf10ee5b819014f6e342e41e45096239e34" integrity sha512-evY7DN8qSIbsW2H/TWQ1bX3sXN1d4MNb5Vb4n7BzPuCwRHdkZ1H2eNLuSh73EoQqkGKUtju2G2HCcjCfhvZIAA== +axios@^0.21.1: + version "0.21.1" + resolved "https://registry.yarnpkg.com/axios/-/axios-0.21.1.tgz#22563481962f4d6bde9a76d516ef0e5d3c09b2b8" + integrity sha512-dKQiRHxGD9PPRIUNIWvZhPTPpl1rf/OxTYKsqKUDjBwYylTvV7SjSHJb9ratfyzM6wCdLCOYLzs73qpg5c4iGA== + dependencies: + follow-redirects "^1.10.0" + babel-loader@^8.1.0: version "8.2.2" resolved "https://registry.yarnpkg.com/babel-loader/-/babel-loader-8.2.2.tgz#9363ce84c10c9a40e6c753748e1441b60c8a0b81" @@ -3004,7 +3020,7 @@ flush-write-stream@^1.0.0, flush-write-stream@^1.0.2: inherits "^2.0.3" readable-stream "^2.3.6" -follow-redirects@^1.0.0: +follow-redirects@^1.0.0, follow-redirects@^1.10.0: version "1.14.1" resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.14.1.tgz#d9114ded0a1cfdd334e164e6662ad02bfd91ff43" integrity sha512-HWqDgT7ZEkqRzBvc2s64vSZ/hfOceEol3ac/7tKwzuvEyWx3/4UegXh5oBOIotkGsObyk3xznnSRVADBgWSQVg== @@ -4248,7 +4264,7 @@ lodash.uniq@^4.5.0: resolved "https://registry.yarnpkg.com/lodash.uniq/-/lodash.uniq-4.5.0.tgz#d0225373aeb652adc1bc82e4945339a842754773" integrity sha1-0CJTc662Uq3BvILklFM5qEJ1R3M= -lodash@^4.17.11, lodash@^4.17.14: +lodash@^4.17.11, lodash@^4.17.14, lodash@^4.17.20: version "4.17.21" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== @@ -4732,6 +4748,11 @@ object-copy@^0.1.0: define-property "^0.2.5" kind-of "^3.0.3" +object-inspect@^1.9.0: + version "1.10.3" + resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.10.3.tgz#c2aa7d2d09f50c99375704f7a0adf24c5782d369" + integrity sha512-e5mCJlSH7poANfC8z8S9s9S2IN5/4Zb3aZ33f5s8YqoazCFzNLloLU8r5VCG+G7WoqLvAAZoVMcy3tp/3X0Plw== + object-is@^1.0.1: version "1.1.5" resolved "https://registry.yarnpkg.com/object-is/-/object-is-1.1.5.tgz#b9deeaa5fc7f1846a0faecdceec138e5778f53ac" @@ -5196,6 +5217,13 @@ qs@6.7.0: resolved "https://registry.yarnpkg.com/qs/-/qs-6.7.0.tgz#41dc1a015e3d581f1621776be31afb2876a9b1bc" integrity sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ== +qs@^6.6.0: + version "6.10.1" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.10.1.tgz#4931482fa8d647a5aab799c5271d2133b981fb6a" + integrity sha512-M528Hph6wsSVOBiYUnGf+K/7w0hNshs/duGsNXPUCLH5XAqjEtiPGwNONLV0tBH8NoGb0mvD5JubnUTrujKDTg== + dependencies: + side-channel "^1.0.4" + querystring-es3@^0.2.0: version "0.2.1" resolved "https://registry.yarnpkg.com/querystring-es3/-/querystring-es3-0.2.1.tgz#9ec61f79049875707d69414596fd907a4d711e73" @@ -5839,6 +5867,15 @@ shebang-regex@^1.0.0: resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-1.0.0.tgz#da42f49740c0b42db2ca9728571cb190c98efea3" integrity sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM= +side-channel@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.0.4.tgz#efce5c8fdc104ee751b25c58d4290011fa5ea2cf" + integrity sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw== + dependencies: + call-bind "^1.0.0" + get-intrinsic "^1.0.2" + object-inspect "^1.9.0" + signal-exit@^3.0.0, signal-exit@^3.0.2: version "3.0.3" resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.3.tgz#a1410c2edd8f077b08b4e253c8eacfcaf057461c" From 3a6df70928cb9fa9461a6a5484ef07160bc007c2 Mon Sep 17 00:00:00 2001 From: f Date: Tue, 1 Jun 2021 18:37:48 -0300 Subject: [PATCH 02/60] faltantes --- _layouts/default.html | 4 ++ assets/templates/cart.html | 65 ++++++++++++++++++++++++++ assets/templates/payment_methods.html | 41 ++++++++++++++++ assets/templates/recover_order.html | 11 +++++ assets/templates/shipping_methods.html | 36 ++++++++++++++ 5 files changed, 157 insertions(+) create mode 100644 assets/templates/cart.html create mode 100644 assets/templates/payment_methods.html create mode 100644 assets/templates/recover_order.html create mode 100644 assets/templates/shipping_methods.html diff --git a/_layouts/default.html b/_layouts/default.html index 3aec209..69665ef 100644 --- a/_layouts/default.html +++ b/_layouts/default.html @@ -46,6 +46,10 @@ {% feed_meta %} +
+ {%- include_cached notification.html -%} +
+ {%- include_cached menu.html active_cache_key=page.layout %}
diff --git a/assets/templates/cart.html b/assets/templates/cart.html new file mode 100644 index 0000000..8c77090 --- /dev/null +++ b/assets/templates/cart.html @@ -0,0 +1,65 @@ +{% for product in products %} +
+
+
+ {{ product.title }} + +
+

{{ product.title }}

+

{{ product.extra | join: ', ' }}

+
+
+
+ +
+
+
+

+ {{ site.cart.price }}: + + {{ product.line_item.attributes.price }} {{ product.line_item.attributes.currency }} + +

+
+ +
+
+ + +
+
+ +
+

+ {{ site.cart.subtotal }}: + + {{ product.line_item.attributes.discounted_amount }} + + + {{ product.line_item.attributes.currency }} +

+
+ +
+ +
+
+
+
+{% endfor %} diff --git a/assets/templates/payment_methods.html b/assets/templates/payment_methods.html new file mode 100644 index 0000000..d294a7a --- /dev/null +++ b/assets/templates/payment_methods.html @@ -0,0 +1,41 @@ +
+ {% for payment_method in payment_methods %} +
+ + + +
+ {% endfor %} + + +
diff --git a/assets/templates/recover_order.html b/assets/templates/recover_order.html new file mode 100644 index 0000000..819fa22 --- /dev/null +++ b/assets/templates/recover_order.html @@ -0,0 +1,11 @@ + diff --git a/assets/templates/shipping_methods.html b/assets/templates/shipping_methods.html new file mode 100644 index 0000000..d871224 --- /dev/null +++ b/assets/templates/shipping_methods.html @@ -0,0 +1,36 @@ +
+ + +
+ {% for shipping_rate in shipping_rates %} +
+
+ + + +
+
+ {% endfor %} + +
+ +
+
+
From 590bfef22298ef49897baca455358fb8f2e2b9dc Mon Sep 17 00:00:00 2001 From: f Date: Wed, 2 Jun 2021 15:17:22 -0300 Subject: [PATCH 03/60] notificaciones --- _includes/notification.html | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 _includes/notification.html diff --git a/_includes/notification.html b/_includes/notification.html new file mode 100644 index 0000000..b74b4c9 --- /dev/null +++ b/_includes/notification.html @@ -0,0 +1,5 @@ +
+
From 7f9d24cbd23e723238bf3ff36ba5f582e49773ec Mon Sep 17 00:00:00 2001 From: f Date: Wed, 2 Jun 2021 15:37:19 -0300 Subject: [PATCH 04/60] provincia / estado --- _includes/state.html | 52 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 52 insertions(+) create mode 100644 _includes/state.html diff --git a/_includes/state.html b/_includes/state.html new file mode 100644 index 0000000..11724f4 --- /dev/null +++ b/_includes/state.html @@ -0,0 +1,52 @@ +{% if include.form %} + {%- assign name = include.field[0] | append: ']' | prepend: '[' | prepend: include.form -%} + {%- assign id = include.field[1].id | default: name | replace: '[', '_' | remove: ']' -%} +{% else %} + {%- assign name = include.field[0] -%} + {%- assign id = include.field[1].id | default: name %} +{% endif %} + +{%- assign label = include.field[1].label[site.locale] -%} +{%- assign help = include.field[1].help[site.locale] -%} +{%- assign error = include.field[1].error[site.locale] -%} +{%- assign autocomplete = include.field[1].autocomplete -%} + +
+ + + + + + + {%- if error -%} +
{{ error }}
+ {%- endif -%} + + {%- if help -%} + + {{ help }} + + {%- endif -%} + + + +
From 30028361b76f8a840f00faface5652049c26dd53 Mon Sep 17 00:00:00 2001 From: f Date: Wed, 2 Jun 2021 15:39:45 -0300 Subject: [PATCH 05/60] traducciones del sitio --- assets/data/site.json | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 assets/data/site.json diff --git a/assets/data/site.json b/assets/data/site.json new file mode 100644 index 0000000..991b49c --- /dev/null +++ b/assets/data/site.json @@ -0,0 +1,25 @@ +--- +--- + +{% comment %} +Genera un site.json con las traducciones del sitio y los datos de los +pasos del carrito. No los extraemos directamente con el filtro +`jsonify` porque extraen demasiada información y el JSON se rompe. + +TODO: El contenido de los pasos del carrito no aparece. +{% endcomment %} + +{%- assign steps = 'cart,shipment,payment,confirmation' | split: ',' -%} + +{ + {%- for step in steps %} + {%- assign step_page = site.posts | find: 'layout', step -%} + "{{ step }}": { + {%- for attribute in site.data.layouts[step] -%} + {%- assign attribute_key = attribute[0] -%} + "{{ attribute_key }}": {{ step_page[attribute_key] | jsonify }}{% unless forloop.last %},{% endunless %} + {%- endfor -%} + }, + {% endfor %} + "i18n": {{ site.i18n | jsonify }} +} From 7550f6ee9b6bdb4bae9bce0d1e5d564306059d56 Mon Sep 17 00:00:00 2001 From: f Date: Fri, 9 Jul 2021 18:46:31 -0300 Subject: [PATCH 06/60] =?UTF-8?q?Solo=20hacer=20b=C3=BAsquedas=20de=20text?= =?UTF-8?q?o=20plano?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit fixes sutty/sutty#889 fixes sutty/sutty#899 fixes sutty/sutty#1475 fixes sutty/sutty#1476 fixes sutty/sutty#1937 fixes sutty/sutty#2044 fixes sutty/sutty#2107 fixes sutty/sutty#2164 fixes sutty/sutty#2193 --- _packs/controllers/search_controller.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/_packs/controllers/search_controller.js b/_packs/controllers/search_controller.js index 59305a5..eae213c 100644 --- a/_packs/controllers/search_controller.js +++ b/_packs/controllers/search_controller.js @@ -12,7 +12,7 @@ export default class extends Controller { if (!this.hasQTarget) return if (!this.qTarget.value.trim().length === 0) return - return this.qTarget.value.trim().replace(':', '') + return this.qTarget.value.trim().replaceAll(/[^a-z0-9 ]/gi, '') } connect () { From 11e7d7288df30cc1da2d53d5142193e938fe6e08 Mon Sep 17 00:00:00 2001 From: f Date: Fri, 9 Jul 2021 19:05:53 -0300 Subject: [PATCH 07/60] No fallar al vaciar storage --- _packs/controllers/order_controller.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/_packs/controllers/order_controller.js b/_packs/controllers/order_controller.js index dea675f..5e8edb6 100644 --- a/_packs/controllers/order_controller.js +++ b/_packs/controllers/order_controller.js @@ -23,7 +23,7 @@ export default class extends CartBaseController { */ subscribe () { window.addEventListener('storage', async event => { - if (!event.key.startsWith('cart:item:')) return + if (!event.key?.startsWith('cart:item:')) return const products = this.products const site = await this.site() From a5a9991d68bc944d27fe5470c3b7685c0fdc13db Mon Sep 17 00:00:00 2001 From: magush27 Date: Tue, 17 Aug 2021 16:38:39 -0300 Subject: [PATCH 08/60] creo campos de personalizacion --- _data/layouts/theme.yml | 162 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 162 insertions(+) create mode 100644 _data/layouts/theme.yml diff --git a/_data/layouts/theme.yml b/_data/layouts/theme.yml new file mode 100644 index 0000000..3ac90ba --- /dev/null +++ b/_data/layouts/theme.yml @@ -0,0 +1,162 @@ +--- +title: + type: string + required: true + label: + en: Title + es: Título + help: + en: '' + es: '' +enable_rounded: + type: boolean + label: + en: 'Yes' + es: Sí + help: + en: Enable or disable this option + es: Habilita o deshabilita esta opción +enable_shadows: + type: boolean + label: + en: 'Yes' + es: Sí + help: + en: Enable or disable this option + es: Habilita o deshabilita esta opción +body_bg: + type: color + label: + en: Color + es: Color + help: + en: Pick a color + es: Elige un color +body_color: + type: color + label: + en: Color + es: Color + help: + en: Pick a color + es: Elige un color +link_color: + type: color + label: + en: Color + es: Color + help: + en: Pick a color + es: Elige un color +link_hover_color: + type: color + label: + en: Color + es: Color + help: + en: Pick a color + es: Elige un color +component_active_color: + type: color + label: + en: Color + es: Color + help: + en: Pick a color + es: Elige un color +h1_font_size: + type: number + label: + en: Number + es: Número + help: + en: '' + es: '' +h2_font_size: + type: number + label: + en: Number + es: Número + help: + en: '' + es: '' +h3_font_size: + type: number + label: + en: Number + es: Número + help: + en: '' + es: '' +h4_font_size: + type: number + label: + en: Number + es: Número + help: + en: '' + es: '' +h5_font_size: + type: number + label: + en: Number + es: Número + help: + en: '' + es: '' +h6_font_size: + type: number + label: + en: Number + es: Número + help: + en: '' + es: '' +mark_bg: + type: color + label: + en: Color + es: Color + help: + en: Pick a color + es: Elige un color +navbar_light_color: + type: color + label: + en: Color + es: Color + help: + en: Pick a color + es: Elige un color +navbar_light_hover_color: + type: color + label: + en: Color + es: Color + help: + en: Pick a color + es: Elige un color +navbar_light_active_color: + type: color + label: + en: Color + es: Color + help: + en: Pick a color + es: Elige un color +order: + type: order + label: + en: Order + es: Orden + help: + en: Position in articles list + es: La posición del artículo en la lista de artículos +draft: + type: boolean + label: + en: Draft + es: Borrador + help: + en: This post isn't ready to be published yet + es: Este artículo aun no está listo para publicar From 56d991130f3284f8e11bb1b8230dc7eabfc79311 Mon Sep 17 00:00:00 2001 From: f Date: Tue, 17 Aug 2021 17:00:19 -0300 Subject: [PATCH 09/60] Cargar los valores del theme en el SCSS Faltan los valores por defecto --- assets/css/styles.scss | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/assets/css/styles.scss b/assets/css/styles.scss index f822e0e..cbb3685 100644 --- a/assets/css/styles.scss +++ b/assets/css/styles.scss @@ -11,6 +11,26 @@ /// /// @todo Mover a su propio SCSS {% assign about = site.posts | find: 'layout', 'about' %} +{% assign theme = site.posts | find: 'layout', 'theme' %} +{% assign theme_defaults = site.data.layouts.theme %} + +$enable-rounded: {{ theme.enable_rounded }}; +$enable-shadows: {{ theme.enable_shadows }}; +$body-bg: {{ theme.body_bg }}; +$body-color: {{ theme.body_color }}; +$link-color: {{ theme.link_color }}; +$link-hover-color: {{ theme.link_hover_color }}; +$component-active-color: {{ theme.component_active_color }}; +$h1-font-size: {{ theme.h1_font_size }}rem; +$h2-font-size: {{ theme.h2_font_size }}rem; +$h3-font-size: {{ theme.h3_font_size }}rem; +$h4-font-size: {{ theme.h4_font_size }}rem; +$h5-font-size: {{ theme.h5_font_size }}rem; +$h6-font-size: {{ theme.h6_font_size }}rem; +$mark-bg: {{ theme.mark_bg }}; +$navbar-light-color: {{ theme.navbar_light_color }}; +$navbar-light-hover-color: {{ theme.navbar_light_hover_color }}; +$navbar-light-active-color: {{ theme.navbar_light_active_color }}; /// El modo debug se desactiva en producción $debug: {{ jekyll.environment | not: 'production' }}; From 9cbaba6ba828fb82f1aa0e87e35cead2577a18b8 Mon Sep 17 00:00:00 2001 From: f Date: Sat, 11 Sep 2021 15:47:43 -0300 Subject: [PATCH 10/60] =?UTF-8?q?Generar=20la=20personalizaci=C3=B3n=20din?= =?UTF-8?q?=C3=A1micamente?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- _data/en.yml | 1 + _data/es.yml | 1 + _data/layouts/theme.yml | 199 +++++++++++++++++++++++++--------------- assets/css/styles.scss | 64 +++++++------ 4 files changed, 158 insertions(+), 107 deletions(-) diff --git a/_data/en.yml b/_data/en.yml index 60a0649..f236e60 100644 --- a/_data/en.yml +++ b/_data/en.yml @@ -65,6 +65,7 @@ layouts: post: Article menu: Menu about: About this site + theme: Customize theme menu: title: Menu share: diff --git a/_data/es.yml b/_data/es.yml index 358e94b..f56b2e4 100644 --- a/_data/es.yml +++ b/_data/es.yml @@ -51,6 +51,7 @@ layouts: post: Artículo menu: Menú about: Información del sitio + theme: Personalizar plantilla menu: title: Menú share: diff --git a/_data/layouts/theme.yml b/_data/layouts/theme.yml index 3ac90ba..e9bba09 100644 --- a/_data/layouts/theme.yml +++ b/_data/layouts/theme.yml @@ -6,144 +6,195 @@ title: en: Title es: Título help: - en: '' - es: '' + en: 'The name you want to give to this customization' + es: 'El nombre que quieras darle a esta personalización' enable_rounded: type: boolean label: - en: 'Yes' - es: Sí + en: 'Rounded corners' + es: 'Esquinas redondeadas' help: - en: Enable or disable this option - es: Habilita o deshabilita esta opción + en: 'For buttons, form inputs, etc.' + es: 'De los botones, campos de formularios, etc.' + default: + es: true + en: true enable_shadows: type: boolean label: - en: 'Yes' - es: Sí + en: 'Shadows' + es: 'Sombras' help: - en: Enable or disable this option - es: Habilita o deshabilita esta opción + en: 'Shadows behind elements' + es: 'Sombras en los elementos' + default: + es: true + en: true body_bg: type: color label: - en: Color - es: Color + en: 'Background color' + es: 'Color de fondo' help: - en: Pick a color - es: Elige un color + en: "Site's background color" + es: 'Color de fondo del sitio' + default: + es: '#FFFFFF' + en: '#FFFFFF' body_color: type: color label: - en: Color - es: Color + en: 'Text color' + es: 'Color del texto' help: - en: Pick a color - es: Elige un color + en: '' + es: '' + default: + es: '#212529' + en: '#212529' +primary: + type: color + label: + en: 'Primary color' + es: 'Color principal' + help: + en: 'Highlights certain elements' + es: 'Resalta algunos elementos' + default: + es: '#007bff' + en: '#007bff' link_color: type: color label: - en: Color - es: Color + en: 'Link color' + es: 'Color de los vínculos' help: - en: Pick a color - es: Elige un color + en: '' + es: '' + default: + es: '#007bff' + en: '#007bff' link_hover_color: type: color label: - en: Color - es: Color + en: 'Link color when selected' + es: 'Color de los vínculos al seleccionarlos' help: - en: Pick a color - es: Elige un color -component_active_color: - type: color - label: - en: Color - es: Color - help: - en: Pick a color - es: Elige un color + en: '' + es: '' + default: + es: '#0056b3' + en: '#0056b3' h1_font_size: type: number label: - en: Number - es: Número + en: 'Height for first level headings' + es: 'Altura de los títulos de primer nivel' help: - en: '' - es: '' + en: 'Proportional to font base height. For instance, 2 is double height.' + es: 'En proporción al alto base de la tipografía. Por ejemplo 2 es el doble.' + default: + es: 2.5 + en: 2.5 h2_font_size: type: number label: - en: Number - es: Número + en: 'Height for second level headings' + es: 'Altura de los títulos de segundo nivel' help: - en: '' - es: '' + en: 'Proportional to font base height. For instance, 2 is double height.' + es: 'En proporción al alto base de la tipografía. Por ejemplo 2 es el doble.' + default: + es: 2 + en: 2 h3_font_size: type: number label: - en: Number - es: Número + en: 'Height for third level headings' + es: 'Altura de los títulos de tercer nivel' help: - en: '' - es: '' + en: 'Proportional to font base height. For instance, 2 is double height.' + es: 'En proporción al alto base de la tipografía. Por ejemplo 2 es el doble.' + default: + es: 1.75 + en: 1.75 h4_font_size: type: number label: - en: Number - es: Número + en: 'Height for fourth level headings' + es: 'Altura de los títulos de cuarto nivel' help: - en: '' - es: '' + en: 'Proportional to font base height. For instance, 2 is double height.' + es: 'En proporción al alto base de la tipografía. Por ejemplo 2 es el doble.' + default: + es: 1.5 + en: 1.5 h5_font_size: type: number label: - en: Number - es: Número + en: 'Height for fifth level headings' + es: 'Altura de los títulos de quinto nivel' help: - en: '' - es: '' + en: 'Proportional to font base height. For instance, 2 is double height.' + es: 'En proporción al alto base de la tipografía. Por ejemplo 2 es el doble.' + default: + es: 1.25 + en: 1.25 h6_font_size: type: number label: - en: Number - es: Número + en: 'Height for sixth level headings' + es: 'Altura de los títulos de sexto nivel' help: - en: '' - es: '' + en: 'Proportional to font base height. For instance, 2 is double height.' + es: 'En proporción al alto base de la tipografía. Por ejemplo 2 es el doble.' + default: + es: 1 + en: 1 mark_bg: type: color label: - en: Color - es: Color + en: 'Highlight color' + es: 'Color de resaltado' help: - en: Pick a color - es: Elige un color + en: 'Default color for highlighted text' + es: 'Color por defecto para el texto resaltado' + default: + es: '#fcf8e3' + en: '#fcf8e3' navbar_light_color: type: color label: - en: Color - es: Color + en: 'Navigation bar item color' + es: 'Color de ítem en la barra de navegación' help: - en: Pick a color - es: Elige un color + en: 'Text and icons' + es: 'Texto e íconos' + default: + es: '#ced4da' + en: '#ced4da' navbar_light_hover_color: type: color label: - en: Color - es: Color + en: 'Navigation bar item color when selected' + es: 'Color de ítem seleccionado en la barra de navegación' help: - en: Pick a color - es: Elige un color + en: 'Text and icons' + es: 'Texto e íconos' + default: + es: '#6c757d' + en: '#6c757d' navbar_light_active_color: type: color label: - en: Color - es: Color + en: 'Navigation bar item color when active' + es: 'Color de ítem activo en la barra de navegación' help: - en: Pick a color - es: Elige un color + en: 'Text and icons' + es: 'Texto e íconos' + default: + es: '#212529' + en: '#212529' order: type: order label: diff --git a/assets/css/styles.scss b/assets/css/styles.scss index cbb3685..256a14f 100644 --- a/assets/css/styles.scss +++ b/assets/css/styles.scss @@ -6,31 +6,41 @@ /// @group Principal //// -/// Traemos el primer artículo de tipo `about` para obtener los valores +/// Traemos el primer artículo de tipo `theme` para obtener los valores /// personalizados. -/// -/// @todo Mover a su propio SCSS -{% assign about = site.posts | find: 'layout', 'about' %} + +{% comment %} + Los artículos con layout `theme` contienen variables utilizadas por + Bootstrap que pueden ser redefinidas por les usuaries de Sutty a través + del panel. +{% endcomment %} {% assign theme = site.posts | find: 'layout', 'theme' %} {% assign theme_defaults = site.data.layouts.theme %} +{% comment %} + Ignorar estos campos que no son variables. +{% endcomment %} +{% assign ignored_keys = 'title,draft,order' | split: ',' %} +{% comment %} + Cada variable de Bootstrap viene desde la definición de `theme`. Por + convención usamos snake_case, pero Bootstrap usa guión medio, así que + las convertimos. -$enable-rounded: {{ theme.enable_rounded }}; -$enable-shadows: {{ theme.enable_shadows }}; -$body-bg: {{ theme.body_bg }}; -$body-color: {{ theme.body_color }}; -$link-color: {{ theme.link_color }}; -$link-hover-color: {{ theme.link_hover_color }}; -$component-active-color: {{ theme.component_active_color }}; -$h1-font-size: {{ theme.h1_font_size }}rem; -$h2-font-size: {{ theme.h2_font_size }}rem; -$h3-font-size: {{ theme.h3_font_size }}rem; -$h4-font-size: {{ theme.h4_font_size }}rem; -$h5-font-size: {{ theme.h5_font_size }}rem; -$h6-font-size: {{ theme.h6_font_size }}rem; -$mark-bg: {{ theme.mark_bg }}; -$navbar-light-color: {{ theme.navbar_light_color }}; -$navbar-light-hover-color: {{ theme.navbar_light_hover_color }}; -$navbar-light-active-color: {{ theme.navbar_light_active_color }}; + Utilizamos los valores que vengan del artículo y si no existen, usamos + los valores por defecto de sutty-base. De lo contrario mantenemos los + de Bootstrap. +{% endcomment %} +{% for variable in theme_defaults %} +{% assign key = variable[0] %} +{% if ignored_keys contains key %}{% continue %}{% endif %} +{% assign default_value = variable[1].default[site.locale] %} +{% assign variable_name = key | replace: '_', '-' %} +{% if theme[key] or default_value %} +{% comment %} + Generamos una definición de variable de SASS +{% endcomment %} +${{ variable_name }}: {{ theme[key] | default: default_value }}; +{% endif %} +{% endfor %} /// El modo debug se desactiva en producción $debug: {{ jekyll.environment | not: 'production' }}; @@ -44,18 +54,6 @@ $bezier: cubic-bezier(0.75, 0, 0.25, 1); /// Redefinir la tipografía aquí, o borrar si usamos las de Bootstrap $font-family-sans-serif: sans-serif; -/// El color primario de la paleta, se trae desde el `about` o sea usa -/// el color por defecto. -/// -/// @link _data/layouts/about.yml -$primary: {{ about.primary | default: site.data.layouts.about.primary.default[site.locale] }}; - -/// El color secundario de la paleta, se trae desde el `about` o sea usa -/// el color por defecto. -/// -/// @link _data/layouts/about.yml -$secondary: {{ about.secondary | default: site.data.layouts.about.secondary.default[site.locale] }}; - /// Agregamos los colores propios de la plantilla aquí. Bootstrap los /// agrega a su propia paleta de colores. Si usamos el mismo nombre From a49678a02e863463ec3d3499a60e33b237247889 Mon Sep 17 00:00:00 2001 From: f Date: Sat, 11 Sep 2021 15:48:43 -0300 Subject: [PATCH 11/60] Algunas variables necesitan una unidad --- _data/layouts/theme.yml | 6 ++++++ assets/css/styles.scss | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/_data/layouts/theme.yml b/_data/layouts/theme.yml index e9bba09..418f2ad 100644 --- a/_data/layouts/theme.yml +++ b/_data/layouts/theme.yml @@ -87,6 +87,7 @@ link_hover_color: en: '#0056b3' h1_font_size: type: number + unit: rem label: en: 'Height for first level headings' es: 'Altura de los títulos de primer nivel' @@ -98,6 +99,7 @@ h1_font_size: en: 2.5 h2_font_size: type: number + unit: rem label: en: 'Height for second level headings' es: 'Altura de los títulos de segundo nivel' @@ -109,6 +111,7 @@ h2_font_size: en: 2 h3_font_size: type: number + unit: rem label: en: 'Height for third level headings' es: 'Altura de los títulos de tercer nivel' @@ -120,6 +123,7 @@ h3_font_size: en: 1.75 h4_font_size: type: number + unit: rem label: en: 'Height for fourth level headings' es: 'Altura de los títulos de cuarto nivel' @@ -131,6 +135,7 @@ h4_font_size: en: 1.5 h5_font_size: type: number + unit: rem label: en: 'Height for fifth level headings' es: 'Altura de los títulos de quinto nivel' @@ -142,6 +147,7 @@ h5_font_size: en: 1.25 h6_font_size: type: number + unit: rem label: en: 'Height for sixth level headings' es: 'Altura de los títulos de sexto nivel' diff --git a/assets/css/styles.scss b/assets/css/styles.scss index 256a14f..a6b3a9c 100644 --- a/assets/css/styles.scss +++ b/assets/css/styles.scss @@ -38,7 +38,7 @@ {% comment %} Generamos una definición de variable de SASS {% endcomment %} -${{ variable_name }}: {{ theme[key] | default: default_value }}; +${{ variable_name }}: {{ theme[key] | default: default_value }}{{ variable[1].unit }}; {% endif %} {% endfor %} From 1ccaf8ce64209137ee981001bd8b70654cbd70f0 Mon Sep 17 00:00:00 2001 From: f Date: Sat, 11 Sep 2021 16:13:38 -0300 Subject: [PATCH 12/60] =?UTF-8?q?Ignorar=20m=C3=A1s=20campos?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- assets/css/styles.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/assets/css/styles.scss b/assets/css/styles.scss index 105ae6f..bf231dc 100644 --- a/assets/css/styles.scss +++ b/assets/css/styles.scss @@ -19,7 +19,7 @@ {% comment %} Ignorar estos campos que no son variables. {% endcomment %} -{% assign ignored_keys = 'title,draft,order' | split: ',' %} +{% assign ignored_keys = 'title,draft,order,last_modified_at,uuid,layout,liquid,usuaries' | split: ',' %} {% comment %} Cada variable de Bootstrap viene desde la definición de `theme`. Por convención usamos snake_case, pero Bootstrap usa guión medio, así que From df141cc8962dce7f7c185366a136ccf1a1b0112a Mon Sep 17 00:00:00 2001 From: f Date: Sat, 11 Sep 2021 16:17:50 -0300 Subject: [PATCH 13/60] =?UTF-8?q?Soporte=20para=20tipograf=C3=ADas?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- _data/layouts/theme.yml | 36 ++++++++++++++++++ _sass/fonts.scss | 29 ++++++++++++++ assets/css/styles.scss | 3 -- .../v27/KFOjCnqEu92Fr1Mu51TzBhc9-subset.woff2 | Bin 0 -> 17452 bytes .../v27/KFOkCnqEu92Fr1MmgWxP-subset.woff2 | Bin 0 -> 16212 bytes .../v27/KFOkCnqEu92Fr1Mu52xP-subset.woff2 | Bin 0 -> 17756 bytes .../v27/KFOlCnqEu92Fr1MmWUlvAw-subset.woff2 | Bin 0 -> 16264 bytes .../v27/KFOmCnqEu92Fr1Me5Q-subset.woff2 | Bin 0 -> 16100 bytes 8 files changed, 65 insertions(+), 3 deletions(-) create mode 100644 assets/fonts/roboto/v27/KFOjCnqEu92Fr1Mu51TzBhc9-subset.woff2 create mode 100644 assets/fonts/roboto/v27/KFOkCnqEu92Fr1MmgWxP-subset.woff2 create mode 100644 assets/fonts/roboto/v27/KFOkCnqEu92Fr1Mu52xP-subset.woff2 create mode 100644 assets/fonts/roboto/v27/KFOlCnqEu92Fr1MmWUlvAw-subset.woff2 create mode 100644 assets/fonts/roboto/v27/KFOmCnqEu92Fr1Me5Q-subset.woff2 diff --git a/_data/layouts/theme.yml b/_data/layouts/theme.yml index 418f2ad..b71233e 100644 --- a/_data/layouts/theme.yml +++ b/_data/layouts/theme.yml @@ -8,6 +8,42 @@ title: help: en: 'The name you want to give to this customization' es: 'El nombre que quieras darle a esta personalización' +font_family_sans_serif: + type: predefined_value + label: + en: Select a typography for the site + es: Selecciona una tipografía para el sitio + help: + en: 'If you want us to add support for a typography, please send us an e-mail' + es: 'Si quieres que agreguemos una tipografía, por favor envíanos un e-mail' + default: + en: sans-serif + es: sans-serif + values: + en: + sans-serif: 'Sans Serif' + Roboto: 'Roboto' + es: + sans-serif: 'Sans Serif' + Roboto: 'Roboto' +headings_font_family: + type: predefined_value + label: + en: Select a typography for the headings + es: Selecciona una tipografía para los títulos del sitio + help: + en: 'If you want us to add support for a typography, please send us an e-mail' + es: 'Si quieres que agreguemos una tipografía, por favor envíanos un e-mail' + default: + en: sans-serif + es: sans-serif + values: + en: + sans-serif: 'Sans Serif' + Roboto: 'Roboto' + es: + sans-serif: 'Sans Serif' + Roboto: 'Roboto' enable_rounded: type: boolean label: diff --git a/_sass/fonts.scss b/_sass/fonts.scss index e69de29..21aa3b5 100644 --- a/_sass/fonts.scss +++ b/_sass/fonts.scss @@ -0,0 +1,29 @@ +// https://fonts.googleapis.com/css2?family=Roboto:ital,wght@0,400;0,700;1,400;1,700&display=swap +@font-face { + font-family: 'Roboto'; + font-style: italic; + font-weight: 400; + font-display: swap; + src: url(../fonts/roboto/v27/KFOkCnqEu92Fr1Mu52xP-subset.woff2) format('woff2'); +} +@font-face { + font-family: 'Roboto'; + font-style: italic; + font-weight: 700; + font-display: swap; + src: url(../fonts/roboto/v27/KFOjCnqEu92Fr1Mu51TzBhc9-subset.woff2) format('woff2'); +} +@font-face { + font-family: 'Roboto'; + font-style: normal; + font-weight: 400; + font-display: swap; + src: url(../fonts/roboto/v27/KFOmCnqEu92Fr1Me5Q-subset.woff2) format('woff2'); +} +@font-face { + font-family: 'Roboto'; + font-style: normal; + font-weight: 700; + font-display: swap; + src: url(../fonts/roboto/v27/KFOlCnqEu92Fr1MmWUlvAw-subset.woff2) format('woff2'); +} diff --git a/assets/css/styles.scss b/assets/css/styles.scss index bf231dc..24e6023 100644 --- a/assets/css/styles.scss +++ b/assets/css/styles.scss @@ -52,9 +52,6 @@ $vendor-prefixes: ("", "-webkit-", "-ms-", "-o-", "-moz-"); /// para generar animaciones. $bezier: cubic-bezier(0.75, 0, 0.25, 1); -/// Redefinir la tipografía aquí, o borrar si usamos las de Bootstrap -$font-family-sans-serif: sans-serif; - /// Agregamos los colores propios de la plantilla aquí. Bootstrap los /// agrega a su propia paleta de colores. Si usamos el mismo nombre /// podemos redefinir el color. diff --git a/assets/fonts/roboto/v27/KFOjCnqEu92Fr1Mu51TzBhc9-subset.woff2 b/assets/fonts/roboto/v27/KFOjCnqEu92Fr1Mu51TzBhc9-subset.woff2 new file mode 100644 index 0000000000000000000000000000000000000000..72f9cbffcaf84dcacfa81bca55949825f36248e1 GIT binary patch literal 17452 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 literal 0 HcmV?d00001 diff --git a/assets/fonts/roboto/v27/KFOkCnqEu92Fr1MmgWxP-subset.woff2 b/assets/fonts/roboto/v27/KFOkCnqEu92Fr1MmgWxP-subset.woff2 new file mode 100644 index 0000000000000000000000000000000000000000..bddfb4e0f60ff9f3b1b095dacd8b8947ebec2595 GIT binary patch literal 16212 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 literal 0 HcmV?d00001 diff --git a/assets/fonts/roboto/v27/KFOlCnqEu92Fr1MmWUlvAw-subset.woff2 b/assets/fonts/roboto/v27/KFOlCnqEu92Fr1MmWUlvAw-subset.woff2 new file mode 100644 index 0000000000000000000000000000000000000000..3ac3e9cfae06bbc123e34c61d60551a1c772d604 GIT binary patch literal 16264 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 literal 0 HcmV?d00001 diff --git a/assets/fonts/roboto/v27/KFOmCnqEu92Fr1Me5Q-subset.woff2 b/assets/fonts/roboto/v27/KFOmCnqEu92Fr1Me5Q-subset.woff2 new file mode 100644 index 0000000000000000000000000000000000000000..4b12e9d2ccc146e0f20064a6ad3da40afa2927d9 GIT binary patch literal 16100 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 q+=!OsDP>e}ia^&0(mp#TgL^`vNOz Date: Sat, 11 Sep 2021 19:28:44 -0300 Subject: [PATCH 14/60] =?UTF-8?q?Permitir=20n=C3=BAmeros=20decimales?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- _data/layouts/theme.yml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/_data/layouts/theme.yml b/_data/layouts/theme.yml index b71233e..3768ab5 100644 --- a/_data/layouts/theme.yml +++ b/_data/layouts/theme.yml @@ -122,7 +122,7 @@ link_hover_color: es: '#0056b3' en: '#0056b3' h1_font_size: - type: number + type: float unit: rem label: en: 'Height for first level headings' @@ -134,7 +134,7 @@ h1_font_size: es: 2.5 en: 2.5 h2_font_size: - type: number + type: float unit: rem label: en: 'Height for second level headings' @@ -146,7 +146,7 @@ h2_font_size: es: 2 en: 2 h3_font_size: - type: number + type: float unit: rem label: en: 'Height for third level headings' @@ -158,7 +158,7 @@ h3_font_size: es: 1.75 en: 1.75 h4_font_size: - type: number + type: float unit: rem label: en: 'Height for fourth level headings' @@ -170,7 +170,7 @@ h4_font_size: es: 1.5 en: 1.5 h5_font_size: - type: number + type: float unit: rem label: en: 'Height for fifth level headings' @@ -182,7 +182,7 @@ h5_font_size: es: 1.25 en: 1.25 h6_font_size: - type: number + type: float unit: rem label: en: 'Height for sixth level headings' From 4840d59e163b263a721f475f836514c4eed83ecf Mon Sep 17 00:00:00 2001 From: f Date: Sat, 18 Sep 2021 17:00:46 -0300 Subject: [PATCH 15/60] Eliminar caracteres especiales de Lunr https://lunrjs.com/guides/searching.html --- _packs/controllers/search_controller.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/_packs/controllers/search_controller.js b/_packs/controllers/search_controller.js index eae213c..af861f2 100644 --- a/_packs/controllers/search_controller.js +++ b/_packs/controllers/search_controller.js @@ -12,7 +12,7 @@ export default class extends Controller { if (!this.hasQTarget) return if (!this.qTarget.value.trim().length === 0) return - return this.qTarget.value.trim().replaceAll(/[^a-z0-9 ]/gi, '') + return this.qTarget.value.trim().replaceAll(/[:~\*\^\+\-]/gi, '') } connect () { From 9d2d811b10eb42cbe3faa21974d9052bb9b4c746 Mon Sep 17 00:00:00 2001 From: f Date: Sun, 19 Sep 2021 12:56:05 -0300 Subject: [PATCH 16/60] Ignorar reportes de error de bots closes sutty/sutty#2740 --- _packs/entry.js | 26 +++++++++++++++++--------- package.json | 1 + yarn.lock | 5 +++++ 3 files changed, 23 insertions(+), 9 deletions(-) diff --git a/_packs/entry.js b/_packs/entry.js index 5de0945..237ef10 100644 --- a/_packs/entry.js +++ b/_packs/entry.js @@ -1,15 +1,23 @@ +import BotDetector from 'device-detector-js/dist/parsers/bot' import { Notifier } from '@airbrake/browser' -window.airbrake = new Notifier({ - projectId: window.env.AIRBRAKE_PROJECT_ID, - projectKey: window.env.AIRBRAKE_PROJECT_KEY, - host: 'https://panel.sutty.nl' -}) +if ('userAgent' in navigator) { + window.bot_detector = new BotDetector + const bot = window.bot_detector.parse(navigator.userAgent) -console.originalError = console.error -console.error = (...e) => { - window.airbrake.notify(e.join(' ')) - return console.originalError(...e) + if (!bot) { + window.airbrake = new Notifier({ + projectId: window.env.AIRBRAKE_PROJECT_ID, + projectKey: window.env.AIRBRAKE_PROJECT_KEY, + host: 'https://panel.sutty.nl' + }) + + console.originalError = console.error + console.error = (...e) => { + window.airbrake.notify(e.join(' ')) + return console.originalError(...e) + } + } } import 'core-js/stable' diff --git a/package.json b/package.json index 895cf0f..edbf46f 100644 --- a/package.json +++ b/package.json @@ -16,6 +16,7 @@ "axe-core": "^4.1.2", "babel-loader": "^8.1.0", "core-js": "^3.6.5", + "device-detector-js": "^2.2.10", "dotenv-webpack": "^6.0.0", "liquidjs": "^9.14.0", "regenerator-runtime": "^0.13.5", diff --git a/yarn.lock b/yarn.lock index d441339..b63a829 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2519,6 +2519,11 @@ detect-node@^2.0.4: resolved "https://registry.yarnpkg.com/detect-node/-/detect-node-2.1.0.tgz#c9c70775a49c3d03bc2c06d9a73be550f978f8b1" integrity sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g== +device-detector-js@^2.2.10: + version "2.2.10" + resolved "https://registry.yarnpkg.com/device-detector-js/-/device-detector-js-2.2.10.tgz#a8fd47837ce89024d7647a4ddf18154d7a920538" + integrity sha512-zLcDSU10WIqbARXecaVJJxx0ZuGWq+MVhj9f9qehdBCFr9RMa5mQGTt2IZNIgKuCIind/j/DzRDViEdc2FfBGQ== + diffie-hellman@^5.0.0: version "5.0.3" resolved "https://registry.yarnpkg.com/diffie-hellman/-/diffie-hellman-5.0.3.tgz#40e8ee98f55a2149607146921c63e1ae5f3d2875" From dd230c2fa1446bbbe7083ab3d802fa89bafc0034 Mon Sep 17 00:00:00 2001 From: Nulo Date: Mon, 20 Sep 2021 14:18:31 -0300 Subject: [PATCH 17/60] No chequear si existe navigator.userAgent MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ¡Existe desde IE 4! --- _packs/entry.js | 26 ++++++++++++-------------- 1 file changed, 12 insertions(+), 14 deletions(-) diff --git a/_packs/entry.js b/_packs/entry.js index 237ef10..5a612e6 100644 --- a/_packs/entry.js +++ b/_packs/entry.js @@ -1,22 +1,20 @@ import BotDetector from 'device-detector-js/dist/parsers/bot' import { Notifier } from '@airbrake/browser' -if ('userAgent' in navigator) { - window.bot_detector = new BotDetector - const bot = window.bot_detector.parse(navigator.userAgent) +window.bot_detector = new BotDetector +const bot = window.bot_detector.parse(navigator.userAgent) - if (!bot) { - window.airbrake = new Notifier({ - projectId: window.env.AIRBRAKE_PROJECT_ID, - projectKey: window.env.AIRBRAKE_PROJECT_KEY, - host: 'https://panel.sutty.nl' - }) +if (!bot) { + window.airbrake = new Notifier({ + projectId: window.env.AIRBRAKE_PROJECT_ID, + projectKey: window.env.AIRBRAKE_PROJECT_KEY, + host: 'https://panel.sutty.nl' + }) - console.originalError = console.error - console.error = (...e) => { - window.airbrake.notify(e.join(' ')) - return console.originalError(...e) - } + console.originalError = console.error + console.error = (...e) => { + window.airbrake.notify(e.join(' ')) + return console.originalError(...e) } } From 06b3d8d69b429dc075b8373807c249ffa3b11fd7 Mon Sep 17 00:00:00 2001 From: Nulo Date: Mon, 20 Sep 2021 15:38:26 -0300 Subject: [PATCH 18/60] Chequear si es bot, no si es parseado --- _packs/entry.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/_packs/entry.js b/_packs/entry.js index 5a612e6..bee006a 100644 --- a/_packs/entry.js +++ b/_packs/entry.js @@ -2,9 +2,9 @@ import BotDetector from 'device-detector-js/dist/parsers/bot' import { Notifier } from '@airbrake/browser' window.bot_detector = new BotDetector -const bot = window.bot_detector.parse(navigator.userAgent) +const device = window.bot_detector.parse(navigator.userAgent) -if (!bot) { +if (!device.bot) { window.airbrake = new Notifier({ projectId: window.env.AIRBRAKE_PROJECT_ID, projectKey: window.env.AIRBRAKE_PROJECT_KEY, From 357ddcf07c45aea101ae91afd2289f03beb614c4 Mon Sep 17 00:00:00 2001 From: f Date: Mon, 20 Sep 2021 22:27:01 -0300 Subject: [PATCH 19/60] Revert "Chequear si es bot, no si es parseado" This reverts commit 06b3d8d69b429dc075b8373807c249ffa3b11fd7. --- _packs/entry.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/_packs/entry.js b/_packs/entry.js index bee006a..5a612e6 100644 --- a/_packs/entry.js +++ b/_packs/entry.js @@ -2,9 +2,9 @@ import BotDetector from 'device-detector-js/dist/parsers/bot' import { Notifier } from '@airbrake/browser' window.bot_detector = new BotDetector -const device = window.bot_detector.parse(navigator.userAgent) +const bot = window.bot_detector.parse(navigator.userAgent) -if (!device.bot) { +if (!bot) { window.airbrake = new Notifier({ projectId: window.env.AIRBRAKE_PROJECT_ID, projectKey: window.env.AIRBRAKE_PROJECT_KEY, From 99167e402ad6c043e54c87bc006f420002aaca3c Mon Sep 17 00:00:00 2001 From: f Date: Wed, 29 Sep 2021 12:26:57 -0300 Subject: [PATCH 20/60] Incrustar URLs de forma segura closes #14 --- _config.yml | 1 + sutty-base-jekyll-theme.gemspec | 1 + 2 files changed, 2 insertions(+) diff --git a/_config.yml b/_config.yml index 651d41e..7274273 100644 --- a/_config.yml +++ b/_config.yml @@ -11,6 +11,7 @@ plugins: - jekyll-data - jekyll-seo-tag - jekyll-images +- jekyll-embed-urls - sutty-liquid markdown: CommonMark commonmark: diff --git a/sutty-base-jekyll-theme.gemspec b/sutty-base-jekyll-theme.gemspec index 11f23b0..30e797f 100644 --- a/sutty-base-jekyll-theme.gemspec +++ b/sutty-base-jekyll-theme.gemspec @@ -69,6 +69,7 @@ Gem::Specification.new do |spec| spec.add_runtime_dependency 'jekyll-dotenv', '>= 0.2' spec.add_runtime_dependency 'jekyll-feed', '~> 0.15' spec.add_runtime_dependency 'jekyll-ignore-layouts', '~> 0' + spec.add_runtime_dependency 'jekyll-embed-urls', '~> 0' # Dependencias de desarrollo spec.add_development_dependency 'bundler', '~> 2.1' From f6adc934e0701abcc35621a61b23ff375a4e9cab Mon Sep 17 00:00:00 2001 From: Maki Date: Wed, 20 Oct 2021 14:40:17 -0300 Subject: [PATCH 21/60] base --- _data/layouts/cuidado.yml | 26 ++++++++++++++++++++++++++ _data/layouts/libro.yml | 26 ++++++++++++++++++++++++++ _data/layouts/lugar.yml | 26 ++++++++++++++++++++++++++ _data/layouts/quienes_somos.yml | 26 ++++++++++++++++++++++++++ _data/layouts/video.yml | 26 ++++++++++++++++++++++++++ _layouts/cuidado.html | 4 ++++ _layouts/libro.html | 4 ++++ _layouts/lugar.html | 4 ++++ _layouts/quienes_somos.html | 4 ++++ _layouts/video.html | 4 ++++ 10 files changed, 150 insertions(+) create mode 100644 _data/layouts/cuidado.yml create mode 100644 _data/layouts/libro.yml create mode 100644 _data/layouts/lugar.yml create mode 100644 _data/layouts/quienes_somos.yml create mode 100644 _data/layouts/video.yml create mode 100644 _layouts/cuidado.html create mode 100644 _layouts/libro.html create mode 100644 _layouts/lugar.html create mode 100644 _layouts/quienes_somos.html create mode 100644 _layouts/video.html diff --git a/_data/layouts/cuidado.yml b/_data/layouts/cuidado.yml new file mode 100644 index 0000000..8744e1c --- /dev/null +++ b/_data/layouts/cuidado.yml @@ -0,0 +1,26 @@ +--- +title: + type: string + required: true + label: + es: Título + en: Title + help: + es: '' + en: '' +order: + type: order + label: + es: Orden + en: Order + help: + es: La posición del artículo en la lista de artículos + en: Position in articles list +draft: + type: boolean + label: + es: Borrador + en: Draft + help: + es: Este artículo aun no está listo para publicar + en: This post isn't ready to be published yet diff --git a/_data/layouts/libro.yml b/_data/layouts/libro.yml new file mode 100644 index 0000000..8744e1c --- /dev/null +++ b/_data/layouts/libro.yml @@ -0,0 +1,26 @@ +--- +title: + type: string + required: true + label: + es: Título + en: Title + help: + es: '' + en: '' +order: + type: order + label: + es: Orden + en: Order + help: + es: La posición del artículo en la lista de artículos + en: Position in articles list +draft: + type: boolean + label: + es: Borrador + en: Draft + help: + es: Este artículo aun no está listo para publicar + en: This post isn't ready to be published yet diff --git a/_data/layouts/lugar.yml b/_data/layouts/lugar.yml new file mode 100644 index 0000000..8744e1c --- /dev/null +++ b/_data/layouts/lugar.yml @@ -0,0 +1,26 @@ +--- +title: + type: string + required: true + label: + es: Título + en: Title + help: + es: '' + en: '' +order: + type: order + label: + es: Orden + en: Order + help: + es: La posición del artículo en la lista de artículos + en: Position in articles list +draft: + type: boolean + label: + es: Borrador + en: Draft + help: + es: Este artículo aun no está listo para publicar + en: This post isn't ready to be published yet diff --git a/_data/layouts/quienes_somos.yml b/_data/layouts/quienes_somos.yml new file mode 100644 index 0000000..8744e1c --- /dev/null +++ b/_data/layouts/quienes_somos.yml @@ -0,0 +1,26 @@ +--- +title: + type: string + required: true + label: + es: Título + en: Title + help: + es: '' + en: '' +order: + type: order + label: + es: Orden + en: Order + help: + es: La posición del artículo en la lista de artículos + en: Position in articles list +draft: + type: boolean + label: + es: Borrador + en: Draft + help: + es: Este artículo aun no está listo para publicar + en: This post isn't ready to be published yet diff --git a/_data/layouts/video.yml b/_data/layouts/video.yml new file mode 100644 index 0000000..8744e1c --- /dev/null +++ b/_data/layouts/video.yml @@ -0,0 +1,26 @@ +--- +title: + type: string + required: true + label: + es: Título + en: Title + help: + es: '' + en: '' +order: + type: order + label: + es: Orden + en: Order + help: + es: La posición del artículo en la lista de artículos + en: Position in articles list +draft: + type: boolean + label: + es: Borrador + en: Draft + help: + es: Este artículo aun no está listo para publicar + en: This post isn't ready to be published yet diff --git a/_layouts/cuidado.html b/_layouts/cuidado.html new file mode 100644 index 0000000..1ab753a --- /dev/null +++ b/_layouts/cuidado.html @@ -0,0 +1,4 @@ +--- +layout: default +--- + diff --git a/_layouts/libro.html b/_layouts/libro.html new file mode 100644 index 0000000..1ab753a --- /dev/null +++ b/_layouts/libro.html @@ -0,0 +1,4 @@ +--- +layout: default +--- + diff --git a/_layouts/lugar.html b/_layouts/lugar.html new file mode 100644 index 0000000..1ab753a --- /dev/null +++ b/_layouts/lugar.html @@ -0,0 +1,4 @@ +--- +layout: default +--- + diff --git a/_layouts/quienes_somos.html b/_layouts/quienes_somos.html new file mode 100644 index 0000000..1ab753a --- /dev/null +++ b/_layouts/quienes_somos.html @@ -0,0 +1,4 @@ +--- +layout: default +--- + diff --git a/_layouts/video.html b/_layouts/video.html new file mode 100644 index 0000000..1ab753a --- /dev/null +++ b/_layouts/video.html @@ -0,0 +1,4 @@ +--- +layout: default +--- + From cd91bb02408abd824ef4db83a8c826dcfb7bcd20 Mon Sep 17 00:00:00 2001 From: f Date: Wed, 27 Oct 2021 15:07:43 -0300 Subject: [PATCH 22/60] =?UTF-8?q?layout=20del=20men=C3=BA=20en=20ingl?= =?UTF-8?q?=C3=A9s?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- _data/layouts/menu.yml | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/_data/layouts/menu.yml b/_data/layouts/menu.yml index 279f4eb..e3974b0 100644 --- a/_data/layouts/menu.yml +++ b/_data/layouts/menu.yml @@ -3,7 +3,7 @@ title: type: 'string' required: true label: - en: '' + en: 'Item name' es: 'Nombre del ítem' help: en: '' @@ -11,21 +11,29 @@ title: post: type: 'belongs_to' label: - en: '' + en: 'Link to this post' es: 'Artículo' help: en: '' es: 'Si el ítem lleva a un artículo fijo, asociarlo aquí' +link: + type: 'string' + label: + en: 'Link' + es: 'Vínculo' + help: + en: "If this item is a regular link, add it here" + es: 'Si el ítem lleva a una página o sección especial, asociarla aquí' item: type: 'belongs_to' inverse: items filter: layout: menu label: - en: '' + en: 'Main item' es: 'Ítem anterior' help: - en: '' + en: "If you're building a dropdown menu, add the main item here" es: 'Si es un sub ítem, asociar el ítem superior aquí' items: type: 'has_many' @@ -33,19 +41,11 @@ items: filter: layout: menu label: - en: '' + en: 'Sub items' es: 'Sub ítemes' help: - en: '' + en: "If you're building a dropdown menu, add the sub items here" es: 'Si el ítem tiene sub ítems, asociarlos aquí' -link: - type: 'string' - label: - en: '' - es: 'Vínculo' - help: - en: '' - es: 'Si el ítem lleva a una página o sección especial, asociarla aquí' categories: type: 'array' label: From efea1fe88d1f262237461a0028168b0f803d789e Mon Sep 17 00:00:00 2001 From: f Date: Wed, 27 Oct 2021 15:09:48 -0300 Subject: [PATCH 23/60] =?UTF-8?q?los=20elementos=20con=20la=20clase=20cont?= =?UTF-8?q?ent=20tienen=20m=C3=A1rgenes=20internos?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- _sass/content.scss | 11 +++++++++++ assets/css/styles.scss | 1 + 2 files changed, 12 insertions(+) create mode 100644 _sass/content.scss diff --git a/_sass/content.scss b/_sass/content.scss new file mode 100644 index 0000000..6914190 --- /dev/null +++ b/_sass/content.scss @@ -0,0 +1,11 @@ +/// Todos los elementos dentro de .content tienen margen inferior, salvo +/// el último, para no modificar el padding y margin del contenedor. +.content { + & > * { + margin-bottom: 1rem; + + &:last-child { + margin-bottom: 0; + } + } +} diff --git a/assets/css/styles.scss b/assets/css/styles.scss index 406eb99..7ab6cf4 100644 --- a/assets/css/styles.scss +++ b/assets/css/styles.scss @@ -132,6 +132,7 @@ $label-margin-bottom: 0; @import "snap"; @import "editor"; @import "menu"; +@import "content"; /// La barra de progreso de Turbo tiene el color primario /// de la paleta, definido por Bootstrap o por nosotres. From 8ee4dc7cf67f81cf924329e9af6c349cba908655 Mon Sep 17 00:00:00 2001 From: f Date: Wed, 27 Oct 2021 15:22:44 -0300 Subject: [PATCH 24/60] incorporar las traducciones en env.js --- env.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/env.js b/env.js index 6210493..b38f2f9 100644 --- a/env.js +++ b/env.js @@ -6,3 +6,7 @@ window.env = { AIRBRAKE_PROJECT_KEY: '{{ site.env.AIRBRAKE_PROJECT_KEY }}', JEKYLL_ENV: '{{ site.env.JEKYLL_ENV }}' } + +window.site = { + "i18n": {{ site.i18n | jsonify }} +} From d452a85d09129877fa0fe313f7004944decbd45c Mon Sep 17 00:00:00 2001 From: f Date: Wed, 27 Oct 2021 15:23:50 -0300 Subject: [PATCH 25/60] buscar las traducciones en window --- _packs/controllers/notification_controller.js | 16 +--------------- _packs/controllers/search_controller.js | 11 +---------- 2 files changed, 2 insertions(+), 25 deletions(-) diff --git a/_packs/controllers/notification_controller.js b/_packs/controllers/notification_controller.js index 17eaaa7..d00bfd4 100644 --- a/_packs/controllers/notification_controller.js +++ b/_packs/controllers/notification_controller.js @@ -19,7 +19,7 @@ export default class extends Controller { if (!response.ok) return - data.site = await this.site() + data.site = window.site const template = await response.text() const html = await this.engine.parseAndRender(template, data) @@ -63,18 +63,4 @@ export default class extends Controller { return window.liquid } - - /* - * Site config (actually just translation strings) - * - * @return [Object] - */ - async site () { - if (!window.site) { - const data = await fetch('assets/data/site.json') - window.site = await data.json() - } - - return window.site - } } diff --git a/_packs/controllers/search_controller.js b/_packs/controllers/search_controller.js index e85de43..0303601 100644 --- a/_packs/controllers/search_controller.js +++ b/_packs/controllers/search_controller.js @@ -47,7 +47,7 @@ export default class extends Controller { const main = document.querySelector('main') const results = window.index.search(q).map(r => window.data.find(a => a.id == r.ref)) - const site = await this.site() + const site = window.site const request = await fetch('assets/templates/results.html') const template = await request.text() const html = await this.engine.parseAndRender(template, { q, site, results }) @@ -98,13 +98,4 @@ export default class extends Controller { return window.liquid } - - async site () { - if (!window.site) { - const data = await fetch('assets/data/site.json') - window.site = await data.json() - } - - return window.site - } } From fd5763c0cd49283d267b6c28b40f6bdf7bf6d949 Mon Sep 17 00:00:00 2001 From: f Date: Wed, 27 Oct 2021 15:32:59 -0300 Subject: [PATCH 26/60] =?UTF-8?q?desde=20tintalimon:=20no=20fallar=20si=20?= =?UTF-8?q?el=20producto=20no=20est=C3=A1=20en=20storage?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- _packs/controllers/cart_controller.js | 32 +++++++++++++++++++++------ 1 file changed, 25 insertions(+), 7 deletions(-) diff --git a/_packs/controllers/cart_controller.js b/_packs/controllers/cart_controller.js index e711bd8..705181b 100644 --- a/_packs/controllers/cart_controller.js +++ b/_packs/controllers/cart_controller.js @@ -34,11 +34,14 @@ export default class extends CartBaseController { if (quantity < 1) return; const orderToken = await this.tokenGetOrCreate() + const product = this.product + + if (!product) return event.target.disabled = true const response = await this.spree.cart.setQuantity({ orderToken }, { - line_item_id: this.product.line_item.id, + line_item_id: product.line_item.id, quantity, include: 'line_items' }) @@ -60,7 +63,7 @@ export default class extends CartBaseController { if (!this.hasSubtotalTarget) return - this.subtotalTarget.innerText = this.product.line_item.attributes.discounted_amount + this.subtotalTarget.innerText = product.line_item.attributes.discounted_amount }) } @@ -110,7 +113,13 @@ export default class extends CartBaseController { } get product () { - return JSON.parse(this.storage.getItem(this.storageId)) + const product = JSON.parse(this.storage.getItem(this.storageId)) + + if (!product) { + console.error("El producto es nulo!", this.storageId, this.storage.length, this.cart) + } + + return product } /* @@ -183,10 +192,13 @@ export default class extends CartBaseController { * item is removed, it removes itself from the page and the storage. */ async remove () { - if (!this.product.line_item) return + const product = this.product + + if (!product) return + if (!product.line_item) return const orderToken = this.token - const response = await this.spree.cart.removeItem({ orderToken }, this.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()) { this.handleFailure(response) @@ -225,14 +237,18 @@ export default class extends CartBaseController { * Recovers the order if something failed */ async recover () { + console.error('Recuperando pedido', this.token) + // Removes the failing token this.storage.removeItem('token') + // Get a new token and cart + await this.tokenGetOrCreate() + // Stores the previous cart const cart = this.cart - // Get a new token and cart - await this.tokenGetOrCreate() + if (!cart) return // Add previous items and their quantities to the new cart by // mimicking user's actions @@ -243,6 +259,8 @@ export default class extends CartBaseController { const product = this.product + if (!product) continue + this.data.set('image', product.image) this.data.set('title', product.title) this.data.set('extra', product.extra.join('|')) From 4404e4535b1db28649d13a08f1844bc6a11cfc32 Mon Sep 17 00:00:00 2001 From: librenauta Date: Wed, 27 Oct 2021 15:35:32 -0300 Subject: [PATCH 27/60] =?UTF-8?q?tipograf=C3=ADas=20responsive=20por=20def?= =?UTF-8?q?ecto?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- assets/css/styles.scss | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/assets/css/styles.scss b/assets/css/styles.scss index 406eb99..3071a36 100644 --- a/assets/css/styles.scss +++ b/assets/css/styles.scss @@ -77,8 +77,8 @@ $spacers: ( 10: ($spacer * 8), 11: ($spacer * 9), 12: ($spacer * 10), - 13: ($spacer * 11), - 14: ($spacer * 12), + 13: ($spacer * 11), + 14: ($spacer * 12), 15: ($spacer * 13), ); @@ -114,6 +114,9 @@ $paragraph-margin-bottom: 0; $headings-margin-bottom: 0; $label-margin-bottom: 0; +///tipografías responsive +$enable-responsive-font-sizes: true; + /// Redefinir variables de Boostrap acá. Se las puede ver en /// node_modules/bootstrap/scss/_variables.scss /// From bb485918bc8e6df444729ef1e894a065a67e5192 Mon Sep 17 00:00:00 2001 From: f Date: Wed, 27 Oct 2021 15:47:33 -0300 Subject: [PATCH 28/60] de periferica: las redirecciones por turbo generan errores de cors --- _packs/controllers/cart_payment_methods_controller.js | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/_packs/controllers/cart_payment_methods_controller.js b/_packs/controllers/cart_payment_methods_controller.js index e6b5217..81d2e05 100644 --- a/_packs/controllers/cart_payment_methods_controller.js +++ b/_packs/controllers/cart_payment_methods_controller.js @@ -80,10 +80,6 @@ export default class extends CartBaseController { if (checkoutUrls.data.length > 0) redirectUrl = checkoutUrls.data[0] - try { - Turbolinks.visit(redirectUrl) - } catch { - window.location = redirectUrl - } + window.location = redirectUrl } } From 8cdc6d44ec909df55223d1135b33b484fc9b3954 Mon Sep 17 00:00:00 2001 From: f Date: Wed, 27 Oct 2021 15:47:58 -0300 Subject: [PATCH 29/60] de periferica: actualizar stock y precios dinamicamente --- _includes/cart_controller.html | 1 + _layouts/default.html | 2 +- _packs/controllers/stock_controller.js | 73 +++++++++++++++++--------- 3 files changed, 51 insertions(+), 25 deletions(-) diff --git a/_includes/cart_controller.html b/_includes/cart_controller.html index 115249b..424c86d 100644 --- a/_includes/cart_controller.html +++ b/_includes/cart_controller.html @@ -1,5 +1,6 @@ data-controller="cart" data-target="stock.product" +data-sku="{{ include.product.sku }}" data-cart-url="{{ include.product.url }}" data-cart-variant-id="{{ include.product.variant_id }}" data-cart-image="{{ include.product.image.path | thumbnail: 212, 300 }}" diff --git a/_layouts/default.html b/_layouts/default.html index 69665ef..834dba4 100644 --- a/_layouts/default.html +++ b/_layouts/default.html @@ -52,7 +52,7 @@ {%- include_cached menu.html active_cache_key=page.layout %} -
+
{{ content }}
diff --git a/_packs/controllers/stock_controller.js b/_packs/controllers/stock_controller.js index b8cea93..d716913 100644 --- a/_packs/controllers/stock_controller.js +++ b/_packs/controllers/stock_controller.js @@ -12,59 +12,84 @@ export default class extends Controller { static targets = [ 'product' ] async connect () { - if (this.variant_ids.length === 0) return + const all_skus = this.skus - const ids = this.variant_ids.join(',') - const filter = { ids } - let response = await window.spree.products.list({ filter }) + if (all_skus.length === 0) return - // TODO: Gestionar errores - if (response.isFail()) { - console.error(response.fail()) - return - } + // El paginado es para prevenir que la petición se haga muy grande y + // falle entera. + const pages = Math.ceil(all_skus.length / this.per_page) - this.update_local_products(response.success().data) + let start = 0 + let end = this.per_page - // Recorrer todas las páginas - // XXX: Podríamos usar next pero la página 1 siempre se devuelve a - // sí misma y entraríamos en un loop infinito. - for (let page = 2; page <= response.success().meta.total_pages; page++) { - response = await window.spree.products.list({ filter, page }) + for (let local_page = 1; local_page <= pages; local_page++) { + const skus = all_skus.slice(start, end).join(',') + + start = this.per_page * local_page + end = start + this.per_page + + const filter = { skus } + let response = await window.spree.products.list({ filter }) - // TODO: Gestionar errores if (response.isFail()) { console.error(response.fail()) - continue + return } this.update_local_products(response.success().data) + + // Recorrer todas las páginas + // XXX: Podríamos usar next pero la página 1 siempre se devuelve a + // sí misma y entraríamos en un loop infinito. + for (let page = 2; page <= response.success().meta.total_pages; page++) { + response = await window.spree.products.list({ filter, page }) + + if (response.isFail()) { + console.error(response.fail()) + continue + } + + this.update_local_products(response.success().data) + } } } /* * La lista de todas las variantes incluidas en el controlador que no - * estén vacías. + * estén vacías. Usamos los SKUs porque no tenemos forma de filtrar + * por ID. * * @return [Array] */ - get variant_ids () { - if (!this._variant_ids) this._variant_ids = [...new Set(this.productTargets.map(p=> p.dataset.cartVariantId).filter(x => x.length > 0))] + get skus () { + return [...new Set(this.productTargets.map(p=> p.dataset.sku).filter(x => x.length > 0))] + } - return this._variant_ids + /* + * La cantidad de productos por página que vamos a pedir + */ + get per_page () { + if (!this._per_page) { + this._per_page = parseInt(this.element.dataset.perPage) + if (isNaN(this._per_page)) this._per_page = 100 + } + + return this._per_page } /* * Los productos pueden estar duplicados así que buscamos todos. */ update_local_products (products) { - for (const product of products) { - for (const local of this.productTargets.filter(local => local.dataset.cartVariantId === product.id)) { + for (const local of this.productTargets) { + for (const product of products.filter(p => 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.querySelectorAll('[data-stock-price]').forEach(price => price.innerText = product.attributes.display_price) + 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) } } } From 087f1436c586e65ce6d2f0008c31038fc30841bc Mon Sep 17 00:00:00 2001 From: f Date: Wed, 27 Oct 2021 15:57:53 -0300 Subject: [PATCH 30/60] no es necesario llamar a airbrake directo --- _packs/controllers/cart_base_controller.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/_packs/controllers/cart_base_controller.js b/_packs/controllers/cart_base_controller.js index 5458396..1dcd725 100644 --- a/_packs/controllers/cart_base_controller.js +++ b/_packs/controllers/cart_base_controller.js @@ -123,7 +123,7 @@ export class CartBaseController extends Controller { const data = { type: 'primary' } let template = 'alert' - if (!window.airbrake) window.airbrake.notify(response.fail()) + console.error(response.fail()) const site = await this.site() From fffc5e0267d301bc5af661c140cca49d95d251d0 Mon Sep 17 00:00:00 2001 From: f Date: Wed, 27 Oct 2021 15:58:32 -0300 Subject: [PATCH 31/60] de almacoop: reiniciar el contador del carrito al confirmar --- _packs/controllers/cart_confirmation_controller.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/_packs/controllers/cart_confirmation_controller.js b/_packs/controllers/cart_confirmation_controller.js index 74e4d48..1b138ca 100644 --- a/_packs/controllers/cart_confirmation_controller.js +++ b/_packs/controllers/cart_confirmation_controller.js @@ -4,7 +4,10 @@ export default class extends CartBaseController { static targets = [ 'order' ] async connect () { - if (this.clear) this.storage.clear() + if (this.clear) { + this.storage.clear() + window.dispatchEvent(new CustomEvent('cart:counter', { detail: { item_count: 0 }})) + } if (!this.template) return From 2ebc2db286b7601ca408a1e70d07f7b3a4b3c131 Mon Sep 17 00:00:00 2001 From: f Date: Wed, 27 Oct 2021 15:58:51 -0300 Subject: [PATCH 32/60] =?UTF-8?q?de=20almacoop:=20recordar=20la=20p=C3=A1g?= =?UTF-8?q?ina=20de=20confirmaci=C3=B3n=20al=20volverla=20a=20cargar?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../cart_confirmation_controller.js | 20 ++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/_packs/controllers/cart_confirmation_controller.js b/_packs/controllers/cart_confirmation_controller.js index 1b138ca..08e5287 100644 --- a/_packs/controllers/cart_confirmation_controller.js +++ b/_packs/controllers/cart_confirmation_controller.js @@ -11,16 +11,18 @@ export default class extends CartBaseController { if (!this.template) return - const order = this.cart.data.attributes - const products = this.products - const site = await this.site() - const shipping_address = JSON.parse(this.storage.getItem('shipping_address')) + if (this.storage.cart) { - const data = { - order, - products, - site, - shipping_address + const order = this.cart.data.attributes + const products = this.products + const site = await this.site() + const shipping_address = JSON.parse(this.storage.getItem('shipping_address')) + + const data = { order, products, site, shipping_address } + + this.storage.setItem('confirmation', JSON.stringify(data)) + } else { + data = JSON.parse(this.storage.getItem('confirmation')) } this.render(data) From a1fd00908b17c9e4c5611573ebcf2a5d167483e7 Mon Sep 17 00:00:00 2001 From: f Date: Wed, 27 Oct 2021 15:59:32 -0300 Subject: [PATCH 33/60] de almacoop: soportar instrucciones especiales --- .../cart_payment_methods_controller.js | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/_packs/controllers/cart_payment_methods_controller.js b/_packs/controllers/cart_payment_methods_controller.js index 81d2e05..788012a 100644 --- a/_packs/controllers/cart_payment_methods_controller.js +++ b/_packs/controllers/cart_payment_methods_controller.js @@ -4,7 +4,7 @@ import { CartBaseController } from './cart_base_controller' * Retrieves payment methods and redirect to external checkouts */ export default class extends CartBaseController { - static targets = [ 'form', 'submit' ] + static targets = [ 'form', 'submit', 'specialInstructions' ] async connect () { const orderToken = this.token @@ -42,17 +42,21 @@ export default class extends CartBaseController { const payment_method_id = this.formTarget.elements.payment_method_id.value const orderToken = this.token + const special_instructions = this.specialInstructionsTarget.value.trim() // XXX: Currently SpreeClient expects us to send payment source // attributes as if it were a credit card. let response = await this.spree.checkout.orderUpdate({ orderToken }, { - order: { payments_attributes: [{ payment_method_id }] }, - payment_source: { - [payment_method_id]: { - name: 'Pepitx', - month: 12, - year: 2020 + order: { + special_instructions, + payments_attributes: [{ payment_method_id }] }, + payment_source: { + [payment_method_id]: { + name: 'Pepitx', + month: 12, + year: 2020 + } } } }) From bba8bfa9aa3769dfd3c4fa66453e76f7d09e5110 Mon Sep 17 00:00:00 2001 From: f Date: Wed, 27 Oct 2021 16:12:18 -0300 Subject: [PATCH 34/60] de tintalimon: pague lo que pueda --- _includes/donacion.html | 125 +++++++++++ _includes/pay_what_you_can_controller.html | 13 ++ .../pay_what_you_can_controller.js | 206 ++++++++++++++++++ 3 files changed, 344 insertions(+) create mode 100644 _includes/donacion.html create mode 100644 _includes/pay_what_you_can_controller.html create mode 100644 _packs/controllers/pay_what_you_can_controller.js diff --git a/_includes/donacion.html b/_includes/donacion.html new file mode 100644 index 0000000..5164821 --- /dev/null +++ b/_includes/donacion.html @@ -0,0 +1,125 @@ +{% comment %} +Traer la donación pasada como parámetro o la que se haya establecido por +defecto para el layout correspondiente. + +product = producto +page = post que incluye esta donación +icon = ícono del botón +class = clases extra para el botón +boton = texto opcional para el botón +{% endcomment %} +{%- assign product = include.product | default: include.page -%} +{%- assign donacion = include.page.donacion | default: product.donacion -%} + + + + +
+ + + + + +
diff --git a/_includes/pay_what_you_can_controller.html b/_includes/pay_what_you_can_controller.html new file mode 100644 index 0000000..eabed0f --- /dev/null +++ b/_includes/pay_what_you_can_controller.html @@ -0,0 +1,13 @@ +data-controller="pay-what-you-can" +data-pay-what-you-can-download-value="{{ include.product.url | present }}" +data-pay-what-you-can-url-value="{{ include.url | default: include.product.url }}" +data-pay-what-you-can-variant-id-value="{{ include.product.variant_id }}" +data-pay-what-you-can-firstname-value="{{ include.product.title }}" +data-pay-what-you-can-currency-value="ARS" +data-pay-what-you-can-price-value="1" +data-image="{{ include.product.image.path | thumbnail: 300 }}" +data-title="{{ include.product.title }}" +data-price="{{ include.product.price }}" +data-in-stock="{{ include.product.in_stock }}" +data-stock="{{ include.product.stock }}" +data-extra="{{ include.extra | join: '|' }}" diff --git a/_packs/controllers/pay_what_you_can_controller.js b/_packs/controllers/pay_what_you_can_controller.js new file mode 100644 index 0000000..9602774 --- /dev/null +++ b/_packs/controllers/pay_what_you_can_controller.js @@ -0,0 +1,206 @@ +import { CartBaseController } from './cart_base_controller' + +/* + * Al pagar lo que podamos, primero hay que crear una orden y luego + * contactarse con la APIv2 para generar la variante con el precio que + * queramos agregar. Agregamos la variante al carrito y lanzamos el + * proceso de pago. + */ +export default class extends CartBaseController { + static targets = [ 'form' ] + static values = { + variantId: Number, + currency: String, + price: Number, + firstname: String + } + + connect () { + this.paymentMethodByCurrency = { + ARS: 'Spree::PaymentMethod::MercadoPago', + USD: 'Spree::PaymentMethod::Paypal' + } + } + + store (event) { + const target = event.currentTarget || event.target + + this[`${target.dataset.name}Value`] = target.value + } + + set formDisable (disable) { + this.formTarget.elements.forEach(x => x.disabled = disable) + } + + /* + * Realiza todos los pasos: + * + * * Crear pedido + * * Crear variante con el monto y moneda + * * Agregar al pedido + * * Agregar dirección al pedido + * * Obtener métodos de envío + * * Obtener métodos de pago + * * Pagar + * * Reenviar a confirmación + * * Ejecutar el pago (si aplica) + */ + async pay (event = undefined) { + if (event) { + event.preventDefault() + event.stopPropagation() + } + + if (!this.formTarget.checkValidity()) { + this.formTarget.classList.add('was-validated') + return + } + + this.formDisable = true + + // Crear pedido. Todos los pedidos van a ser hechos desde + // Argentina, no hay forma de cambiar esto. + const orderToken = await this.tempCartCreate() + const quantity = 1 + const include = 'line_items' + const currency = this.currencyValue + const price = this.priceValue + const email = 'noreply@sutty.nl' + const firstname = this.firstnameValue + const lastname = '-' + const address1 = '-' + const country_id = 250 // XXX: Internet + const city = '-' + const phone = '11111111' + const zipcode = '1111' + const ship_address_attributes = { firstname, lastname, 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 + + // Crear la variante + const payWhatYouCanResponse = await this.spree.sutty.payWhatYouCan({ orderToken }, { variant_id, price, currency, quantity }) + + variant_id = payWhatYouCanResponse.data.id + + if (!variant_id) { + this.formDisable = false + console.error('No se pudo generar la variante', { variant_id, price, currency, quantity }) + return + } + + // Configurar la moneda del pedido + let response = await this.spree.sutty.updateOrder({ orderToken }, { currency, confirmation_delivered, custom_return_url }) + + if (response.status > 299) { + console.error(response) + this.formDisable = false + return + } + + // Agregar al carrito + response = await this.spree.cart.addItem({ orderToken }, { variant_id, quantity, include }) + + if (response.isFail()) { + this.handleFailure(response) + this.formDisable = false + return + } + + // Actualizar la dirección + response = await this.spree.checkout.orderUpdate({ orderToken }, { order: { email, ship_address_attributes, bill_address_attributes }}) + + if (response.isFail()) { + this.handleFailure(response) + this.formDisable = false + return + } + + // Obtener los medios de envío + response = await this.spree.checkout.shippingMethods({ orderToken }, { include: 'shipping_rates' }) + + if (response.isFail()) { + this.handleFailure(response) + this.formDisable = false + return + } + + // Elegir medio de envío + response = await this.spree.checkout.orderUpdate({ orderToken }, { + order: { + shipments_attributes: [{ + id: response.success().data[0].id, + selected_shipping_rate_id: response.success().included.filter(x => x.type == 'shipping_rate')[0].id + }] + } + }) + + // Elegir medio de pago + response = await this.spree.checkout.paymentMethods({ orderToken }) + + if (response.isFail()) { + this.handleFailure(response) + this.formDisable = false + return + } + + const payment_method_id = response.success().data.find(x => this.paymentMethodByCurrency[this.currencyValue] == x.attributes.type).id + + response = await this.spree.checkout.orderUpdate({ orderToken }, + { + order: { payments_attributes: [{ payment_method_id }] }, + payment_source: { + [payment_method_id]: { + name: 'Pepitx', + month: 12, + year: 2021 + } + } + }) + + if (response.isFail()) { + this.handleFailure(response) + this.formDisable = false + return + } + + response = await this.spree.checkout.complete({ orderToken }) + + if (response.isFail()) { + this.handleFailure(response) + this.formDisable = false + return + } + + // Reenviar al medio de pago + const checkoutUrls = await this.spree.sutty.getCheckoutURL({ orderToken }) + const redirectUrl = checkoutUrls.data[0] + + Turbolinks.visit(redirectUrl) + + // Volver + } + + async tempCartCreate () { + const response = await this.spree.cart.create() + + // If we fail here it's probably a server error, so we inform the + // user. + if (response.isFail()) { + this.handleFailure(response) + return + } + + return response.success().data.attributes.token + } + + // @return [String] + customReturnUrl () { + const url = new URL(window.location.href) + url.searchParams.set('open', '') + + return url.toString() + } +} From c227fbe7220707c880fcd6e923ca782ea6a77fe7 Mon Sep 17 00:00:00 2001 From: f Date: Wed, 27 Oct 2021 16:16:14 -0300 Subject: [PATCH 35/60] =?UTF-8?q?cup=C3=B3n=20de=20descuento?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- _data/layouts/payment.yml | 12 ++++ _packs/controllers/cart_coupon_controller.js | 64 ++++++++++++++++++++ assets/templates/payment_methods.html | 21 +++++++ 3 files changed, 97 insertions(+) create mode 100644 _packs/controllers/cart_coupon_controller.js diff --git a/_data/layouts/payment.yml b/_data/layouts/payment.yml index 2fd1705..da9c652 100644 --- a/_data/layouts/payment.yml +++ b/_data/layouts/payment.yml @@ -60,6 +60,18 @@ special_instructions_help: default: es: 'Horas específicas de entrega, etc.' en: 'Specific delivery hours, etc.' +promo_code: + type: 'string' + required: true + label: + es: 'Código de descuento' + en: 'Coupon code' + help: + es: '' + en: '' + default: + es: '¿Tenés un cupón de descuento?' + en: '' permalink: type: 'permalink' required: true diff --git a/_packs/controllers/cart_coupon_controller.js b/_packs/controllers/cart_coupon_controller.js new file mode 100644 index 0000000..15edb3d --- /dev/null +++ b/_packs/controllers/cart_coupon_controller.js @@ -0,0 +1,64 @@ +import { CartBaseController } from './cart_base_controller' + +/* + * Retrieves shipping methods + */ +export default class extends CartBaseController { + static targets = [ 'couponCodeInvalid', 'preDiscount', 'total' ] + + connect () { + this.couponCode.addEventListener('input', event => { + this.couponCode.parentElement.classList.remove('was-validated') + this.couponCode.setCustomValidity('') + }) + } + + get couponCode () { + if (!this._couponCode) this._couponCode = this.element.elements.coupon_code + + return this._couponCode + } + + get couponCodeInvalid () { + return this.hasCouponCodeInvalidTarget ? this.couponCodeInvalidTarget : document.querySelector('#coupon-code-invalid') + } + + get preDiscount () { + return this.hasPreDiscountTarget ? this.preDiscountTarget : document.querySelector('#pre-discount') + } + + get total () { + return this.hasTotalTarget ? this.totalTarget : document.querySelector('#total') + } + + set total (total) { + this.total.innerHTML = total + } + + async apply (event = undefined) { + event?.preventDefault() + event?.stopPropagation() + + const orderToken = this.token + const coupon_code = this.couponCode.value + const include = 'line_items' + + const response = await window.spree.cart.applyCouponCode({ orderToken }, { coupon_code, include }) + + this.element.elements.forEach(x => x.disabled = true) + + if (response.isFail()) { + this.couponCodeInvalid.innerHTML = response.fail().summary + this.couponCode.setCustomValidity(response.fail().summary) + this.couponCode.parentElement.classList.add('was-validated') + + this.element.elements.forEach(x => x.disabled = false) + + return + } + + this.cart = response + this.total = response.success().data.attributes.total + this.preDiscount.classList.remove('d-none') + } +} diff --git a/assets/templates/payment_methods.html b/assets/templates/payment_methods.html index d294a7a..322eceb 100644 --- a/assets/templates/payment_methods.html +++ b/assets/templates/payment_methods.html @@ -1,3 +1,5 @@ +
+
@@ -31,6 +33,25 @@
+
+ {{ site.i18n.cart.layouts.payment.promo_code }} + +
+ {% comment %} + Estos elementos pertenecen al formulario de cupones + {% endcomment %} +
+ + +
+
+ +
+ +
+
+
+ Date: Thu, 28 Oct 2021 09:36:49 -0300 Subject: [PATCH 36/60] layout de producto --- _data/layouts/cart.yml | 24 +++++++++ _data/layouts/product.yml | 14 ++--- _layouts/product.html | 110 ++++++++++++++++++++++++++++++++++++++ _sass/helpers.scss | 12 +++++ 4 files changed, 153 insertions(+), 7 deletions(-) create mode 100644 _layouts/product.html diff --git a/_data/layouts/cart.yml b/_data/layouts/cart.yml index 022177c..3ecb444 100644 --- a/_data/layouts/cart.yml +++ b/_data/layouts/cart.yml @@ -156,6 +156,30 @@ remove: default: es: 'Quitar del carrito' en: 'Remove product' +length_unit: + type: 'string' + required: true + label: + es: 'Unidad de medida de los productos' + en: 'Measurement unit for products' + help: + es: '' + en: '' + default: + es: 'mm' + en: 'mm' +weight_unit: + type: 'string' + required: true + label: + es: 'Unidad de medida para el peso de los productos' + en: 'Measurement unit for products weight' + help: + es: '' + en: '' + default: + es: 'gr' + en: 'gr' back: type: 'string' required: true diff --git a/_data/layouts/product.yml b/_data/layouts/product.yml index d14a7cb..7263585 100644 --- a/_data/layouts/product.yml +++ b/_data/layouts/product.yml @@ -94,8 +94,8 @@ width: es: 'Ancho' en: 'Width' help: - es: 'En milímetros' - en: 'In millimeters' + es: 'En la unidad de medida que configuraste en el carrito' + en: 'In measurement units configured in cart' height: type: 'number' writable: 'once' @@ -103,17 +103,17 @@ height: es: 'Alto' en: 'Height' help: - es: 'En milímetros' - en: 'In millimeters' + es: 'En la unidad de medida que configuraste en el carrito' + en: 'In measurement units configured in cart' depth: type: 'number' writable: 'once' label: - es: 'Profundidad (Lomo)' + es: 'Profundidad' en: 'Depth' help: - es: 'En milímetros' - en: 'In millimeters' + es: 'En la unidad de medida que configuraste en el carrito' + en: 'In measurement units configured in cart' weight: type: 'number' required: true diff --git a/_layouts/product.html b/_layouts/product.html new file mode 100644 index 0000000..19d7f94 --- /dev/null +++ b/_layouts/product.html @@ -0,0 +1,110 @@ +--- +layout: default +--- + +
+
+
+ + {{ page.image.description | default: page.title }} + + +
+

{{ site.i18n.libro.ficha }}

+ +
    +
  • + +
  • + + {% if page.width > 0 %} +
  • + {{ site.data.layouts.product.width.label[site.lang }} + {{ page.width }} + {{ site.cart.length_unit | default: site.data.layouts.cart.length_unit.default[site.lang] }} +
  • + {% endif %} + + {% if page.height > 0 %} +
  • + {{ site.data.layouts.product.height.label[site.lang }} + {{ page.height }} + {{ site.cart.length_unit | default: site.data.layouts.cart.length_unit.default[site.lang] }} +
  • + {% endif %} + + {% if page.depth > 0 %} +
  • + {{ site.data.layouts.product.depth.label[site.lang }} + {{ page.depth }} + {{ site.cart.length_unit | default: site.data.layouts.cart.length_unit.default[site.lang] }} +
  • + {% endif %} + + {% if page.weight > 0 %} +
  • + {{ site.data.layouts.product.weight.label[site.lang }} + {{ page.weight }} + {{ site.cart.weight_unit | default: site.data.layouts.cart.weight_unit.default[site.lang] }} +
  • + {% endif %} +
+ +
+
+ + + + + + {% if page.in_stock %} + + {% else %} + + {% endif %} + +

+ {{ page.price | floor }} + {{ site.cart.currency }} +

+ + +
+
+
+
+ +
+
+

{{ page.title }}

+

{{ page.description }}

+
+ +
+ {{ content }} +
+
+
+ + + + +
diff --git a/_sass/helpers.scss b/_sass/helpers.scss index e6170da..f162021 100644 --- a/_sass/helpers.scss +++ b/_sass/helpers.scss @@ -37,6 +37,18 @@ $directions: (top, right, bottom, left); &::-webkit-scrollbar { display: none; } } + :enabled { + .show-when-disabled#{$infix} { + display: none !important; + } + } + + :disabled { + .hide-when-disabled#{$infix} { + display: none !important; + } + } + /// Un elemento cuadrado /// /// @example html From 8bcb4f066f8ae8b17dc70e6238708e5a97422c0c Mon Sep 17 00:00:00 2001 From: f Date: Wed, 27 Oct 2021 15:18:05 -0300 Subject: [PATCH 37/60] las plantillas de liquid.js se incorporan en env.js --- assets/templates/alert.html | 14 ++++++++------ assets/templates/results.html | 34 ++++++++++++++++++---------------- env.js | 17 +++++++++++++++++ 3 files changed, 43 insertions(+), 22 deletions(-) diff --git a/assets/templates/alert.html b/assets/templates/alert.html index b520708..1ef923d 100644 --- a/assets/templates/alert.html +++ b/assets/templates/alert.html @@ -1,7 +1,9 @@ - +{%- endraw -%} diff --git a/assets/templates/results.html b/assets/templates/results.html index 2473966..65c0e11 100644 --- a/assets/templates/results.html +++ b/assets/templates/results.html @@ -1,17 +1,19 @@ -
-
- {% for item in results %} - + {% endfor %} +
+
+{%- endraw -%} diff --git a/env.js b/env.js index b38f2f9..711fa4f 100644 --- a/env.js +++ b/env.js @@ -10,3 +10,20 @@ window.env = { window.site = { "i18n": {{ site.i18n | jsonify }} } + +{%- comment -%} + Para agregar plantillas que se procesan con JS, las agregamos en + Liquid dentro de assets/templates/ y luego las importamos acá, de forma + que estén disponibles para JS sin tener que descargarlas. + + Es importante que la plantilla esté envuelta por el tag `{% raw %}`, + para que no sea procesado en el momento de generar env.js, sino en el + navegador. +{%- endcomment -%} +{%- capture alert %}{% include_relative assets/templates/alert.html %}{% endcapture -%} +{%- capture results %}{% include_relative assets/templates/results.html %}{% endcapture -%} + +window.templates = { + "alert": {{ alert | markdownify }}, + "results": {{ results | markdownify }}, +} From e0d6870fd7a5f5c0add95d95856d084e7a2e31f4 Mon Sep 17 00:00:00 2001 From: f Date: Wed, 27 Oct 2021 15:18:35 -0300 Subject: [PATCH 38/60] no cachear env.js --- _layouts/default.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/_layouts/default.html b/_layouts/default.html index 3aec209..e7ccf9f 100644 --- a/_layouts/default.html +++ b/_layouts/default.html @@ -24,7 +24,7 @@ si no existe. {% endcomment %} - + {% include_cached pack.html %} {% comment %} From c41e9df446253724bb6cdb8c7141dc337cd253d3 Mon Sep 17 00:00:00 2001 From: f Date: Wed, 27 Oct 2021 15:21:16 -0300 Subject: [PATCH 39/60] buscar las plantillas en window --- _packs/controllers/notification_controller.js | 6 +----- _packs/controllers/search_controller.js | 3 +-- 2 files changed, 2 insertions(+), 7 deletions(-) diff --git a/_packs/controllers/notification_controller.js b/_packs/controllers/notification_controller.js index d00bfd4..edb3299 100644 --- a/_packs/controllers/notification_controller.js +++ b/_packs/controllers/notification_controller.js @@ -15,13 +15,9 @@ export default class extends Controller { * nothing if the template isn't found. */ async render (name, data = {}) { - const response = await fetch(this.template(name)) - - if (!response.ok) return - data.site = window.site - const template = await response.text() + const template = window.templates.alert const html = await this.engine.parseAndRender(template, data) this.element.innerHTML = html diff --git a/_packs/controllers/search_controller.js b/_packs/controllers/search_controller.js index 0303601..5608ab2 100644 --- a/_packs/controllers/search_controller.js +++ b/_packs/controllers/search_controller.js @@ -48,8 +48,7 @@ export default class extends Controller { const main = document.querySelector('main') const results = window.index.search(q).map(r => window.data.find(a => a.id == r.ref)) const site = window.site - const request = await fetch('assets/templates/results.html') - const template = await request.text() + const template = window.templates.results const html = await this.engine.parseAndRender(template, { q, site, results }) const title = `${site.i18n.search.title} - ${q}` const query = new URLSearchParams({ q }) From cefd4765f7c3977e36b8f71781168d5692be80f6 Mon Sep 17 00:00:00 2001 From: f Date: Wed, 27 Oct 2021 16:47:18 -0300 Subject: [PATCH 40/60] jsonify --- env.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/env.js b/env.js index 711fa4f..fa90467 100644 --- a/env.js +++ b/env.js @@ -24,6 +24,6 @@ window.site = { {%- capture results %}{% include_relative assets/templates/results.html %}{% endcapture -%} window.templates = { - "alert": {{ alert | markdownify }}, - "results": {{ results | markdownify }}, + "alert": {{ alert | jsonify }}, + "results": {{ results | jsonify }}, } From 867d746244e66153da16130cd6b450cdcf2004c2 Mon Sep 17 00:00:00 2001 From: f Date: Thu, 28 Oct 2021 10:00:00 -0300 Subject: [PATCH 41/60] enviar plantillas a env.js --- _layouts/payment.html | 2 +- _packs/controllers/cart_confirmation_controller.js | 13 +++++-------- .../controllers/cart_payment_methods_controller.js | 3 +-- _packs/controllers/cart_shipping_controller.js | 3 +-- _packs/controllers/notification_controller.js | 9 --------- _packs/controllers/order_controller.js | 6 +++--- assets/templates/cart.html | 2 ++ assets/templates/payment_methods.html | 2 ++ assets/templates/recover_order.html | 2 ++ assets/templates/shipping_methods.html | 2 ++ env.js | 8 ++++++++ 11 files changed, 27 insertions(+), 25 deletions(-) diff --git a/_layouts/payment.html b/_layouts/payment.html index af3ff98..b3a72e3 100644 --- a/_layouts/payment.html +++ b/_layouts/payment.html @@ -16,6 +16,6 @@ layout: default data-controller="cart-payment-methods" data-cart-payment-methods-next-url="{{ site.confirmation.url }}" data-cart-payment-methods-back-url="{{ site.shipment.url }}" - data-cart-payment-methods-template="assets/templates/payment_methods.html"> + data-cart-payment-methods-template="payment_methods">
diff --git a/_packs/controllers/cart_confirmation_controller.js b/_packs/controllers/cart_confirmation_controller.js index 08e5287..0faf726 100644 --- a/_packs/controllers/cart_confirmation_controller.js +++ b/_packs/controllers/cart_confirmation_controller.js @@ -12,7 +12,6 @@ export default class extends CartBaseController { if (!this.template) return if (this.storage.cart) { - const order = this.cart.data.attributes const products = this.products const site = await this.site() @@ -29,16 +28,14 @@ export default class extends CartBaseController { } render (data = {}) { - fetch(this.template).then(r => r.text()).then(template => { - this.engine.parseAndRender(template, data).then(html => this.orderTarget.innerHTML = html) - }) - } - - get template () { - return this.element.dataset.template + this.engine.parseAndRender(this.template, data).then(html => this.orderTarget.innerHTML = html) } get clear () { return this.element.dataset.clear } + + get template () { + return window.templates[this.element.dataset.template] + } } diff --git a/_packs/controllers/cart_payment_methods_controller.js b/_packs/controllers/cart_payment_methods_controller.js index 788012a..48f02e9 100644 --- a/_packs/controllers/cart_payment_methods_controller.js +++ b/_packs/controllers/cart_payment_methods_controller.js @@ -25,8 +25,7 @@ export default class extends CartBaseController { } async render (data = {}) { - const request = await fetch(this.data.get('template')) - const template = await request.text() + const template = window.templates[this.data.get('template')] this.element.innerHTML = await this.engine.parseAndRender(template, data) diff --git a/_packs/controllers/cart_shipping_controller.js b/_packs/controllers/cart_shipping_controller.js index d78d906..975663f 100644 --- a/_packs/controllers/cart_shipping_controller.js +++ b/_packs/controllers/cart_shipping_controller.js @@ -102,8 +102,7 @@ export default class extends CartBaseController { } async render (data = {}) { - const request = await fetch(this.data.get('template')) - const template = await request.text() + const template = window.templates[this.data.get('template')] this.methodsTarget.innerHTML = await this.engine.parseAndRender(template, data) this.ratesTarget.addEventListener('formdata', event => this.processShippingRate(event.formData)) diff --git a/_packs/controllers/notification_controller.js b/_packs/controllers/notification_controller.js index edb3299..4ef5456 100644 --- a/_packs/controllers/notification_controller.js +++ b/_packs/controllers/notification_controller.js @@ -24,15 +24,6 @@ export default class extends Controller { this.show() } - /* - * Gets the template path from a name - * - * @return [String] - */ - template (name) { - return this.data.get('templates') + name + '.html' - } - /* * Shows the notification */ diff --git a/_packs/controllers/order_controller.js b/_packs/controllers/order_controller.js index 5e8edb6..442e12e 100644 --- a/_packs/controllers/order_controller.js +++ b/_packs/controllers/order_controller.js @@ -43,9 +43,9 @@ export default class extends CartBaseController { * Download the item template and render the order */ render (data = {}) { - fetch(this.data.get('itemTemplate')).then(r => r.text()).then(template => { - this.engine.parseAndRender(template, data).then(html => this.cartTarget.innerHTML = html) - }) + const template = window.templates[this.data.get('itemTemplate')] + + this.engine.parseAndRender(template, data).then(html => this.cartTarget.innerHTML = html) } /* diff --git a/assets/templates/cart.html b/assets/templates/cart.html index 8c77090..6c3ecca 100644 --- a/assets/templates/cart.html +++ b/assets/templates/cart.html @@ -1,3 +1,4 @@ +{%- raw -%} {% for product in products %}
{% endfor %} +{%- endraw -%} diff --git a/assets/templates/payment_methods.html b/assets/templates/payment_methods.html index 322eceb..e83f8a2 100644 --- a/assets/templates/payment_methods.html +++ b/assets/templates/payment_methods.html @@ -1,3 +1,4 @@ +{%- raw -%}
+{%- endraw -%} diff --git a/assets/templates/recover_order.html b/assets/templates/recover_order.html index 819fa22..44bb347 100644 --- a/assets/templates/recover_order.html +++ b/assets/templates/recover_order.html @@ -1,3 +1,4 @@ +{%- raw -%} +{%- endraw -%} diff --git a/assets/templates/shipping_methods.html b/assets/templates/shipping_methods.html index d871224..2bb7464 100644 --- a/assets/templates/shipping_methods.html +++ b/assets/templates/shipping_methods.html @@ -1,3 +1,4 @@ +{%- raw -%}
@@ -34,3 +35,4 @@
+{%- endraw -%} diff --git a/env.js b/env.js index 4f4b266..f698d7b 100644 --- a/env.js +++ b/env.js @@ -23,8 +23,16 @@ window.site = { {%- endcomment -%} {%- capture alert %}{% include_relative assets/templates/alert.html %}{% endcapture -%} {%- capture results %}{% include_relative assets/templates/results.html %}{% endcapture -%} +{%- capture cart %}{% include_relative assets/templates/cart.html %}{% endcapture -%} +{%- capture payment_methods %}{% include_relative assets/templates/payment_methods.html %}{% endcapture -%} +{%- capture recover_order %}{% include_relative assets/templates/recover_order.html %}{% endcapture -%} +{%- capture shipping_methods %}{% include_relative assets/templates/shipping_methods.html %}{% endcapture -%} window.templates = { "alert": {{ alert | jsonify }}, "results": {{ results | jsonify }}, + "cart": {{ cart | jsonify }}, + "payment_methods": {{ payment_methods | jsonify }}, + "recover_order": {{ recover_order | jsonify }}, + "shipping_methods": {{ shipping_methods | jsonify }}, } From e86f14983b2908efc516d4717f3637e424771986 Mon Sep 17 00:00:00 2001 From: f Date: Thu, 28 Oct 2021 10:00:13 -0300 Subject: [PATCH 42/60] =?UTF-8?q?renderizar=20la=20confirmaci=C3=B3n?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- _layouts/confirmation.html | 8 +++- assets/templates/confirmation.html | 67 ++++++++++++++++++++++++++++++ 2 files changed, 74 insertions(+), 1 deletion(-) create mode 100644 assets/templates/confirmation.html diff --git a/_layouts/confirmation.html b/_layouts/confirmation.html index 265be89..5857fe8 100644 --- a/_layouts/confirmation.html +++ b/_layouts/confirmation.html @@ -2,7 +2,11 @@ layout: default --- -
+
+

{{ page.title }}

@@ -11,6 +15,8 @@ layout: default {{ content }} +
+

{{ page.back }} diff --git a/assets/templates/confirmation.html b/assets/templates/confirmation.html new file mode 100644 index 0000000..605b1ef --- /dev/null +++ b/assets/templates/confirmation.html @@ -0,0 +1,67 @@ +{%- raw -%} +
+
+
{{ site.confirmation.number }}
+
{{ order.number }}
+ +
{{ site.confirmation.delivery }}
+
{{ shipping_address.address1 }}, {{ shipping_address.city }} ({{ shipping_address.zipcode }})
+ +
{{ site.confirmation.email }}
+
{{ order.email }}
+
+
+ +
+

{{ site.confirmation.your_order }}

+
+
+

{{ site.cart.product }}

+
+ +
+

{{ site.cart.price }}

+
+ +
+

{{ site.cart.subtotal }}

+
+ + {% for product in products %} +
+

{{ site.cart.product }}

+

{{ product.title }}

+

{{ product.extra | join: ',' }}

+
+ +
+

{{ site.cart.price }}

+

+ {{ product.line_item.attributes.price }} + {{ product.line_item.attributes.currency }} +

+
+ +
+

{{ site.cart.subtotal }}

+

+ {{ product.line_item.attributes.discounted_amount }} + {{ product.line_item.attributes.currency }} +

+
+ {% endfor %} + +
+

+ {{ site.cart.subtotal }} +

+
+ +
+

+ {{ order.total }} +

+
+
+
+{%- endraw -%} From 16a42269913c89b2585bb930834b64d41bcd465c Mon Sep 17 00:00:00 2001 From: f Date: Thu, 28 Oct 2021 10:49:00 -0300 Subject: [PATCH 43/60] agregar traducciones del carrito en env --- env.js | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/env.js b/env.js index f698d7b..0050c7f 100644 --- a/env.js +++ b/env.js @@ -8,7 +8,22 @@ window.env = { SPREE_URL: '{{ site.env.SPREE_URL }}' } +{% comment %} +Genera un site.json con las traducciones del sitio y los datos de los +pasos del carrito. No los extraemos directamente con el filtro +`jsonify` porque extraen demasiada información y el JSON se rompe. +{% endcomment %} +{%- assign steps = 'cart,shipment,payment,confirmation' | split: ',' -%} + window.site = { + {%- for step in steps -%} + "{{ step }}": { + {%- for attribute in site.data.layouts[step] -%} + {%- assign attribute_key = attribute[0] -%} + "{{ attribute_key }}": {{ site[step][attribute_key] | jsonify }}{% unless forloop.last %},{% endunless %} + {%- endfor -%} + }, + {%- endfor -%} "i18n": {{ site.i18n | jsonify }} } From 25ffcd8ab9b16ac678d142cff79c72247efad1e6 Mon Sep 17 00:00:00 2001 From: f Date: Thu, 28 Oct 2021 12:19:32 -0300 Subject: [PATCH 44/60] el tag raw no soporta eliminar espacio alrededor --- assets/templates/alert.html | 4 ++-- assets/templates/cart.html | 4 ++-- assets/templates/confirmation.html | 4 ++-- assets/templates/payment_methods.html | 4 ++-- assets/templates/recover_order.html | 4 ++-- assets/templates/results.html | 4 ++-- assets/templates/shipping_methods.html | 4 ++-- env.js | 2 +- 8 files changed, 15 insertions(+), 15 deletions(-) diff --git a/assets/templates/alert.html b/assets/templates/alert.html index 1ef923d..2e3d840 100644 --- a/assets/templates/alert.html +++ b/assets/templates/alert.html @@ -1,4 +1,4 @@ -{%- raw -%} +{% raw %} -{%- endraw -%} +{% endraw %} diff --git a/assets/templates/cart.html b/assets/templates/cart.html index 6c3ecca..e7612e5 100644 --- a/assets/templates/cart.html +++ b/assets/templates/cart.html @@ -1,4 +1,4 @@ -{%- raw -%} +{% raw %} {% for product in products %}
{% endfor %} -{%- endraw -%} +{% endraw %} diff --git a/assets/templates/confirmation.html b/assets/templates/confirmation.html index 605b1ef..9efc5cf 100644 --- a/assets/templates/confirmation.html +++ b/assets/templates/confirmation.html @@ -1,4 +1,4 @@ -{%- raw -%} +{% raw %}
{{ site.confirmation.number }}
@@ -64,4 +64,4 @@
-{%- endraw -%} +{% endraw %} diff --git a/assets/templates/payment_methods.html b/assets/templates/payment_methods.html index e83f8a2..450b47f 100644 --- a/assets/templates/payment_methods.html +++ b/assets/templates/payment_methods.html @@ -1,4 +1,4 @@ -{%- raw -%} +{% raw %}
-{%- endraw -%} +{% endraw %} diff --git a/assets/templates/recover_order.html b/assets/templates/recover_order.html index 44bb347..29e372f 100644 --- a/assets/templates/recover_order.html +++ b/assets/templates/recover_order.html @@ -1,4 +1,4 @@ -{%- raw -%} +{% raw %} -{%- endraw -%} +{% endraw %} diff --git a/assets/templates/results.html b/assets/templates/results.html index 65c0e11..31d7d80 100644 --- a/assets/templates/results.html +++ b/assets/templates/results.html @@ -1,4 +1,4 @@ -{%- raw -%} +{% raw %}
{% for item in results %} @@ -16,4 +16,4 @@ {% endfor %}
-{%- endraw -%} +{% endraw %} diff --git a/assets/templates/shipping_methods.html b/assets/templates/shipping_methods.html index 2bb7464..a2e5d5e 100644 --- a/assets/templates/shipping_methods.html +++ b/assets/templates/shipping_methods.html @@ -1,4 +1,4 @@ -{%- raw -%} +{% raw %}
@@ -35,4 +35,4 @@
-{%- endraw -%} +{% endraw %} diff --git a/env.js b/env.js index 0050c7f..ea9850c 100644 --- a/env.js +++ b/env.js @@ -32,7 +32,7 @@ window.site = { Liquid dentro de assets/templates/ y luego las importamos acá, de forma que estén disponibles para JS sin tener que descargarlas. - Es importante que la plantilla esté envuelta por el tag `{% raw %}`, + Es importante que la plantilla esté envuelta por el tag `raw`, para que no sea procesado en el momento de generar env.js, sino en el navegador. {%- endcomment -%} From c4525cff61e026e04858ce4001cb1083f9b03938 Mon Sep 17 00:00:00 2001 From: f Date: Thu, 28 Oct 2021 13:10:57 -0300 Subject: [PATCH 45/60] pasar de turbolinks a turbo --- _layouts/default.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/_layouts/default.html b/_layouts/default.html index b6867c0..8f7dff2 100644 --- a/_layouts/default.html +++ b/_layouts/default.html @@ -46,7 +46,7 @@ {% feed_meta %} -
+
{%- include_cached notification.html -%}
From f701c18ac57e3863031f20cc044a678cf8fb74f5 Mon Sep 17 00:00:00 2001 From: f Date: Thu, 28 Oct 2021 13:41:57 -0300 Subject: [PATCH 46/60] typos --- _layouts/product.html | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/_layouts/product.html b/_layouts/product.html index 19d7f94..20d8060 100644 --- a/_layouts/product.html +++ b/_layouts/product.html @@ -28,7 +28,7 @@ layout: default {% if page.width > 0 %}
  • - {{ site.data.layouts.product.width.label[site.lang }} + {{ site.data.layouts.product.width.label[site.lang] }} {{ page.width }} {{ site.cart.length_unit | default: site.data.layouts.cart.length_unit.default[site.lang] }}
  • @@ -36,7 +36,7 @@ layout: default {% if page.height > 0 %}
  • - {{ site.data.layouts.product.height.label[site.lang }} + {{ site.data.layouts.product.height.label[site.lang] }} {{ page.height }} {{ site.cart.length_unit | default: site.data.layouts.cart.length_unit.default[site.lang] }}
  • @@ -44,7 +44,7 @@ layout: default {% if page.depth > 0 %}
  • - {{ site.data.layouts.product.depth.label[site.lang }} + {{ site.data.layouts.product.depth.label[site.lang] }} {{ page.depth }} {{ site.cart.length_unit | default: site.data.layouts.cart.length_unit.default[site.lang] }}
  • @@ -52,7 +52,7 @@ layout: default {% if page.weight > 0 %}
  • - {{ site.data.layouts.product.weight.label[site.lang }} + {{ site.data.layouts.product.weight.label[site.lang] }} {{ page.weight }} {{ site.cart.weight_unit | default: site.data.layouts.cart.weight_unit.default[site.lang] }}
  • From 8cee02a7131366a3837df5d5becf5ddcb8d3c841 Mon Sep 17 00:00:00 2001 From: f Date: Thu, 28 Oct 2021 14:26:34 -0300 Subject: [PATCH 47/60] =?UTF-8?q?previsualizaci=C3=B3n=20de=20producto?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- _includes/product.html | 38 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 _includes/product.html diff --git a/_includes/product.html b/_includes/product.html new file mode 100644 index 0000000..1b669f6 --- /dev/null +++ b/_includes/product.html @@ -0,0 +1,38 @@ +
    +
    + + + + + + + + +
    + + +

    + {{ include.product.price }} + {{ site.cart.currency }} +

    + + +
    +
    +
    From ff1bc216106d6c626e1140f662ed3731202ccd09 Mon Sep 17 00:00:00 2001 From: f Date: Thu, 28 Oct 2021 14:59:19 -0300 Subject: [PATCH 48/60] usar window.site --- _packs/controllers/cart_base_controller.js | 16 +--------------- .../controllers/cart_confirmation_controller.js | 2 +- _packs/controllers/cart_controller.js | 2 +- .../cart_payment_methods_controller.js | 2 +- .../cart_paypal_confirmation_controller.js | 2 +- _packs/controllers/cart_shipping_controller.js | 2 +- _packs/controllers/country_controller.js | 2 +- _packs/controllers/order_controller.js | 4 ++-- _packs/controllers/state_controller.js | 2 +- 9 files changed, 10 insertions(+), 24 deletions(-) diff --git a/_packs/controllers/cart_base_controller.js b/_packs/controllers/cart_base_controller.js index 1dcd725..879d30d 100644 --- a/_packs/controllers/cart_base_controller.js +++ b/_packs/controllers/cart_base_controller.js @@ -84,20 +84,6 @@ export class CartBaseController extends Controller { return window.liquid } - /* - * Site config (actually just translation strings) - * - * @return [Object] - */ - async site () { - if (!window.site) { - const data = await fetch('assets/data/site.json') - window.site = await data.json() - } - - return window.site - } - /* * Updates the item counter */ @@ -125,7 +111,7 @@ export class CartBaseController extends Controller { console.error(response.fail()) - const site = await this.site() + const site = window.site switch (response.fail().name) { case 'MisconfigurationError': diff --git a/_packs/controllers/cart_confirmation_controller.js b/_packs/controllers/cart_confirmation_controller.js index 0faf726..c04d763 100644 --- a/_packs/controllers/cart_confirmation_controller.js +++ b/_packs/controllers/cart_confirmation_controller.js @@ -14,7 +14,7 @@ export default class extends CartBaseController { if (this.storage.cart) { const order = this.cart.data.attributes const products = this.products - const site = await this.site() + const site = window.site const shipping_address = JSON.parse(this.storage.getItem('shipping_address')) const data = { order, products, site, shipping_address } diff --git a/_packs/controllers/cart_controller.js b/_packs/controllers/cart_controller.js index 705181b..97b43e2 100644 --- a/_packs/controllers/cart_controller.js +++ b/_packs/controllers/cart_controller.js @@ -181,7 +181,7 @@ export default class extends CartBaseController { this.fireCajon() if (floating_alert) { - const site = await this.site() + const site = window.site const content = site.cart.added window.dispatchEvent(new CustomEvent('floating:alert', { detail: { content }})) } diff --git a/_packs/controllers/cart_payment_methods_controller.js b/_packs/controllers/cart_payment_methods_controller.js index 48f02e9..c9f882e 100644 --- a/_packs/controllers/cart_payment_methods_controller.js +++ b/_packs/controllers/cart_payment_methods_controller.js @@ -16,7 +16,7 @@ export default class extends CartBaseController { } const payment_methods = response.success().data - const site = await this.site() + const site = window.site const cart = this.cart const next = { url: this.data.get('nextUrl') } const back = { url: this.data.get('backUrl') } diff --git a/_packs/controllers/cart_paypal_confirmation_controller.js b/_packs/controllers/cart_paypal_confirmation_controller.js index 3410eb3..9ac2777 100644 --- a/_packs/controllers/cart_paypal_confirmation_controller.js +++ b/_packs/controllers/cart_paypal_confirmation_controller.js @@ -12,7 +12,7 @@ export default class extends CartBaseController { async connect () { if (this.params.PayerID === undefined) return - this.site = await this.site() + this.site = window.site this.element.innerHTML = this.site.i18n.cart.layouts.paypal.confirming fetch(this.executeURL) diff --git a/_packs/controllers/cart_shipping_controller.js b/_packs/controllers/cart_shipping_controller.js index 975663f..64cc33f 100644 --- a/_packs/controllers/cart_shipping_controller.js +++ b/_packs/controllers/cart_shipping_controller.js @@ -68,7 +68,7 @@ export default class extends CartBaseController { const shipping_rates = shippingMethods.included.filter(x => x.type == 'shipping_rate') // XXX: No hay varios paquetes const shipping_method = shippingMethods.data[0] - const site = await this.site() + const site = window.site await this.render({ shipping_method, shipping_rates, site }) diff --git a/_packs/controllers/country_controller.js b/_packs/controllers/country_controller.js index 3f8476f..6becb09 100644 --- a/_packs/controllers/country_controller.js +++ b/_packs/controllers/country_controller.js @@ -23,7 +23,7 @@ export default class extends CartBaseController { this.listTarget.appendChild(option) }) - const site = await this.site() + const site = window.site // Only allow names on this list this.nameTarget.pattern = countries.map(x => x.attributes.name).join('|') diff --git a/_packs/controllers/order_controller.js b/_packs/controllers/order_controller.js index 442e12e..08b7c50 100644 --- a/_packs/controllers/order_controller.js +++ b/_packs/controllers/order_controller.js @@ -10,7 +10,7 @@ export default class extends CartBaseController { async connect () { const products = this.products - const site = await this.site() + const site = window.site this.render({ products, site }) this.subtotalUpdate() @@ -26,7 +26,7 @@ export default class extends CartBaseController { if (!event.key?.startsWith('cart:item:')) return const products = this.products - const site = await this.site() + const site = window.site this.render({ products, site }) this.subtotalUpdate() diff --git a/_packs/controllers/state_controller.js b/_packs/controllers/state_controller.js index b0f3949..e84a0e8 100644 --- a/_packs/controllers/state_controller.js +++ b/_packs/controllers/state_controller.js @@ -25,7 +25,7 @@ export default class extends CartBaseController { if (!statesRequired) return const states = await this.states(event.detail.iso) - const site = await this.site() + const site = window.site states.forEach(state => { let option = document.createElement('option') From 28bb7ae4cf5ce711b2f616040be75760c51850c3 Mon Sep 17 00:00:00 2001 From: f Date: Fri, 29 Oct 2021 14:22:12 -0300 Subject: [PATCH 49/60] Revert "base" This reverts commit f6adc934e0701abcc35621a61b23ff375a4e9cab. --- _data/layouts/cuidado.yml | 26 -------------------------- _data/layouts/libro.yml | 26 -------------------------- _data/layouts/lugar.yml | 26 -------------------------- _data/layouts/quienes_somos.yml | 26 -------------------------- _data/layouts/video.yml | 26 -------------------------- _layouts/cuidado.html | 4 ---- _layouts/libro.html | 4 ---- _layouts/lugar.html | 4 ---- _layouts/quienes_somos.html | 4 ---- _layouts/video.html | 4 ---- 10 files changed, 150 deletions(-) delete mode 100644 _data/layouts/cuidado.yml delete mode 100644 _data/layouts/libro.yml delete mode 100644 _data/layouts/lugar.yml delete mode 100644 _data/layouts/quienes_somos.yml delete mode 100644 _data/layouts/video.yml delete mode 100644 _layouts/cuidado.html delete mode 100644 _layouts/libro.html delete mode 100644 _layouts/lugar.html delete mode 100644 _layouts/quienes_somos.html delete mode 100644 _layouts/video.html diff --git a/_data/layouts/cuidado.yml b/_data/layouts/cuidado.yml deleted file mode 100644 index 8744e1c..0000000 --- a/_data/layouts/cuidado.yml +++ /dev/null @@ -1,26 +0,0 @@ ---- -title: - type: string - required: true - label: - es: Título - en: Title - help: - es: '' - en: '' -order: - type: order - label: - es: Orden - en: Order - help: - es: La posición del artículo en la lista de artículos - en: Position in articles list -draft: - type: boolean - label: - es: Borrador - en: Draft - help: - es: Este artículo aun no está listo para publicar - en: This post isn't ready to be published yet diff --git a/_data/layouts/libro.yml b/_data/layouts/libro.yml deleted file mode 100644 index 8744e1c..0000000 --- a/_data/layouts/libro.yml +++ /dev/null @@ -1,26 +0,0 @@ ---- -title: - type: string - required: true - label: - es: Título - en: Title - help: - es: '' - en: '' -order: - type: order - label: - es: Orden - en: Order - help: - es: La posición del artículo en la lista de artículos - en: Position in articles list -draft: - type: boolean - label: - es: Borrador - en: Draft - help: - es: Este artículo aun no está listo para publicar - en: This post isn't ready to be published yet diff --git a/_data/layouts/lugar.yml b/_data/layouts/lugar.yml deleted file mode 100644 index 8744e1c..0000000 --- a/_data/layouts/lugar.yml +++ /dev/null @@ -1,26 +0,0 @@ ---- -title: - type: string - required: true - label: - es: Título - en: Title - help: - es: '' - en: '' -order: - type: order - label: - es: Orden - en: Order - help: - es: La posición del artículo en la lista de artículos - en: Position in articles list -draft: - type: boolean - label: - es: Borrador - en: Draft - help: - es: Este artículo aun no está listo para publicar - en: This post isn't ready to be published yet diff --git a/_data/layouts/quienes_somos.yml b/_data/layouts/quienes_somos.yml deleted file mode 100644 index 8744e1c..0000000 --- a/_data/layouts/quienes_somos.yml +++ /dev/null @@ -1,26 +0,0 @@ ---- -title: - type: string - required: true - label: - es: Título - en: Title - help: - es: '' - en: '' -order: - type: order - label: - es: Orden - en: Order - help: - es: La posición del artículo en la lista de artículos - en: Position in articles list -draft: - type: boolean - label: - es: Borrador - en: Draft - help: - es: Este artículo aun no está listo para publicar - en: This post isn't ready to be published yet diff --git a/_data/layouts/video.yml b/_data/layouts/video.yml deleted file mode 100644 index 8744e1c..0000000 --- a/_data/layouts/video.yml +++ /dev/null @@ -1,26 +0,0 @@ ---- -title: - type: string - required: true - label: - es: Título - en: Title - help: - es: '' - en: '' -order: - type: order - label: - es: Orden - en: Order - help: - es: La posición del artículo en la lista de artículos - en: Position in articles list -draft: - type: boolean - label: - es: Borrador - en: Draft - help: - es: Este artículo aun no está listo para publicar - en: This post isn't ready to be published yet diff --git a/_layouts/cuidado.html b/_layouts/cuidado.html deleted file mode 100644 index 1ab753a..0000000 --- a/_layouts/cuidado.html +++ /dev/null @@ -1,4 +0,0 @@ ---- -layout: default ---- - diff --git a/_layouts/libro.html b/_layouts/libro.html deleted file mode 100644 index 1ab753a..0000000 --- a/_layouts/libro.html +++ /dev/null @@ -1,4 +0,0 @@ ---- -layout: default ---- - diff --git a/_layouts/lugar.html b/_layouts/lugar.html deleted file mode 100644 index 1ab753a..0000000 --- a/_layouts/lugar.html +++ /dev/null @@ -1,4 +0,0 @@ ---- -layout: default ---- - diff --git a/_layouts/quienes_somos.html b/_layouts/quienes_somos.html deleted file mode 100644 index 1ab753a..0000000 --- a/_layouts/quienes_somos.html +++ /dev/null @@ -1,4 +0,0 @@ ---- -layout: default ---- - diff --git a/_layouts/video.html b/_layouts/video.html deleted file mode 100644 index 1ab753a..0000000 --- a/_layouts/video.html +++ /dev/null @@ -1,4 +0,0 @@ ---- -layout: default ---- - From 9a75dcadb77fcdbd26bd3ec8665b9f61f0c06d4a Mon Sep 17 00:00:00 2001 From: f Date: Fri, 29 Oct 2021 19:12:51 -0300 Subject: [PATCH 50/60] colores responsive les faltaba el infix --- _sass/helpers.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/_sass/helpers.scss b/_sass/helpers.scss index f162021..d0ffd72 100644 --- a/_sass/helpers.scss +++ b/_sass/helpers.scss @@ -327,7 +327,7 @@ $directions: (top, right, bottom, left); /// /// @example html ///
    - .#{$color} { + .#{$color}#{$infix} { color: var(--#{$color}); &:focus { From 5bf30932b386b2180956c449680dc6ce002d41da Mon Sep 17 00:00:00 2001 From: librenauta Date: Tue, 9 Nov 2021 17:13:41 -0300 Subject: [PATCH 51/60] @import fonts en style.scss --- assets/css/styles.scss | 1 + 1 file changed, 1 insertion(+) diff --git a/assets/css/styles.scss b/assets/css/styles.scss index 24e5c08..a755a31 100644 --- a/assets/css/styles.scss +++ b/assets/css/styles.scss @@ -136,6 +136,7 @@ $enable-responsive-font-sizes: true; @import "editor"; @import "menu"; @import "content"; +@import "fonts"; /// La barra de progreso de Turbo tiene el color primario /// de la paleta, definido por Bootstrap o por nosotres. From cbbbd328eabf17fe8ab316169b20f186dde2d047 Mon Sep 17 00:00:00 2001 From: f Date: Wed, 10 Nov 2021 19:47:05 -0300 Subject: [PATCH 52/60] mejorar el reporte de errores --- _packs/controllers/cart_base_controller.js | 29 +++++++++++++++------- 1 file changed, 20 insertions(+), 9 deletions(-) diff --git a/_packs/controllers/cart_base_controller.js b/_packs/controllers/cart_base_controller.js index 879d30d..da43477 100644 --- a/_packs/controllers/cart_base_controller.js +++ b/_packs/controllers/cart_base_controller.js @@ -108,14 +108,14 @@ export class CartBaseController extends Controller { async handleFailure (response) { const data = { type: 'primary' } let template = 'alert' - - console.error(response.fail()) + let notify = true const site = window.site + const fail = response.fail() - switch (response.fail().name) { + switch (fail.name) { case 'MisconfigurationError': - data.content = response.fail().message + data.content = fail.message break case 'NoResponseError': data.content = site.i18n.alerts.no_response_error @@ -125,7 +125,7 @@ export class CartBaseController extends Controller { break case 'BasicSpreeError': // XXX: The order is missing, we need to start a new one - if (response.fail().serverResponse.status === 404) { + if (fail.serverResponse.status === 404) { template = 'recover_order' data.content = site.i18n.alerts.recover_order } else { @@ -134,15 +134,26 @@ export class CartBaseController extends Controller { break case 'ExpandedSpreeError': - data.content = response.fail().summary + const content = [] + // XXX: La API devuelve los mensajes de error tal cual en la + // llave `error` pero el cliente solo nos da acceso a `errors`. + for (const field of Object.keys(fail.errors)) { + if (!site.i18n.errors?.fields[field]) { + console.error('El campo no tiene traducción', field) + } + + content.push(`${site.i18n.errors?.fields[field]}: ${fail.errors[field].join(', ')}`) + } + + data.content = content.join('. ') + notify = false break default: - data.content = response.fail().message + data.content = fail.message } - console.error(response.fail().name, data.content) - + if (notify) console.error(fail.name, data.content) window.dispatchEvent(new CustomEvent('notification', { detail: { template, data } })) } From 6fcf8a428425ebf22cc9d25797020739bce990c4 Mon Sep 17 00:00:00 2001 From: f Date: Fri, 12 Nov 2021 14:01:49 -0300 Subject: [PATCH 53/60] grillas! --- _sass/helpers.scss | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/_sass/helpers.scss b/_sass/helpers.scss index d0ffd72..673cc14 100644 --- a/_sass/helpers.scss +++ b/_sass/helpers.scss @@ -26,6 +26,39 @@ $directions: (top, right, bottom, left); /// `-md`, `-lg`, `-xl`. $infix: breakpoint-infix($breakpoint, $grid-breakpoints); +/// Grilla en CSS, soporta armar una cantidad de columnas, indicar las +/// columnas que ocupan los elementos descendientes e incluso +/// solapamiento. +/// +/// @example html +///
    +///
    +///
    +///
    + .d#{$infix}-grid { + display: grid !important; + + @each $spacer, $_ in $spacers { + &.grid-cols#{$infix}-#{$spacer} { + grid-template-columns: repeat($spacer, 1fr) !important; + } + + & > .grid-row#{$infix}-#{$spacer} { + grid-row: $spacer !important; + } + + & > .grid-col#{$infix}-#{$spacer} { + grid-column: $spacer !important; + } + + @each $spacer_to, $_ in $spacers { + & > .grid-col#{$infix}-#{$spacer}-to-#{$spacer_to} { + grid-column: #{$spacer} / #{$spacer_to} !important; + } + } + } + } + /// Ocultar la barra de scroll, útil para sliders horizontales. /// /// @example html From cd44ab2a3c5ff5c2012199dca8a4ee92a8772609 Mon Sep 17 00:00:00 2001 From: Nulo Date: Thu, 18 Nov 2021 12:47:13 -0300 Subject: [PATCH 54/60] env.js: poner puntos y comas en variables MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit En ciertos casos, esto podía generar JS inválido rompiendo el sitio. --- env.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/env.js b/env.js index ea9850c..cdd0683 100644 --- a/env.js +++ b/env.js @@ -6,7 +6,7 @@ window.env = { AIRBRAKE_PROJECT_KEY: '{{ site.env.AIRBRAKE_PROJECT_KEY }}', JEKYLL_ENV: '{{ site.env.JEKYLL_ENV }}', SPREE_URL: '{{ site.env.SPREE_URL }}' -} +}; {% comment %} Genera un site.json con las traducciones del sitio y los datos de los @@ -25,7 +25,7 @@ window.site = { }, {%- endfor -%} "i18n": {{ site.i18n | jsonify }} -} +}; {%- comment -%} Para agregar plantillas que se procesan con JS, las agregamos en @@ -50,4 +50,4 @@ window.templates = { "payment_methods": {{ payment_methods | jsonify }}, "recover_order": {{ recover_order | jsonify }}, "shipping_methods": {{ shipping_methods | jsonify }}, -} +}; From 02f27f281e1b15489be4ec52c03478e0633e4ee4 Mon Sep 17 00:00:00 2001 From: f Date: Mon, 22 Nov 2021 14:45:20 -0300 Subject: [PATCH 55/60] make prettier --- Makefile | 4 ++++ package.json | 1 + yarn.lock | 5 +++++ 3 files changed, 10 insertions(+) diff --git a/Makefile b/Makefile index ef48681..bf0f79f 100644 --- a/Makefile +++ b/Makefile @@ -49,6 +49,10 @@ npx: ## Correr comandos con npx (args="run") npm: ## Correr comandos con npm (args="install -g paquete") $(MAKE) hain args="npm $(args)" +prettier: ## Arreglar JS (por ahora) + $(MAKE) yarn args="prettier --write _packs/" + + serve: /etc/hosts $(hain)/run/nginx/nginx.pid ## Servidor de desarrollo @echo "Iniciado servidor web en https://$(domain):4000/" diff --git a/package.json b/package.json index 85888a8..9ccebe5 100644 --- a/package.json +++ b/package.json @@ -20,6 +20,7 @@ "device-detector-js": "^2.2.10", "dotenv-webpack": "^6.0.0", "liquidjs": "^9.14.0", + "prettier": "^2.4.1", "regenerator-runtime": "^0.13.5", "sassdoc": "^2.7.3", "sassdoc-theme-herman": "^4.0.2", diff --git a/yarn.lock b/yarn.lock index af4b4c9..cffdfb6 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5161,6 +5161,11 @@ prepend-http@^2.0.0: resolved "https://registry.yarnpkg.com/prepend-http/-/prepend-http-2.0.0.tgz#e92434bfa5ea8c19f41cdfd401d741a3c819d897" 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: version "2.0.1" resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2" From ef29848cfa31addf20b5eb7c8168b8ea3d7dfba7 Mon Sep 17 00:00:00 2001 From: f Date: Mon, 22 Nov 2021 14:46:18 -0300 Subject: [PATCH 56/60] chequear el formato --- Makefile | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Makefile b/Makefile index bf0f79f..dc44c94 100644 --- a/Makefile +++ b/Makefile @@ -52,6 +52,8 @@ npm: ## Correr comandos con npm (args="install -g paquete") 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 @echo "Iniciado servidor web en https://$(domain):4000/" From ccdf831dacd276cb458550f0a129e14aeac4bb28 Mon Sep 17 00:00:00 2001 From: f Date: Mon, 22 Nov 2021 14:51:50 -0300 Subject: [PATCH 57/60] prettificar --- _packs/controllers/cart_base_controller.js | 222 ++++++++------- .../cart_confirmation_controller.js | 46 ++-- _packs/controllers/cart_contact_controller.js | 26 +- _packs/controllers/cart_controller.js | 259 ++++++++++-------- _packs/controllers/cart_counter_controller.js | 29 +- _packs/controllers/cart_coupon_controller.js | 77 +++--- .../cart_payment_methods_controller.js | 96 +++---- .../cart_paypal_confirmation_controller.js | 44 ++- .../controllers/cart_shipping_controller.js | 117 ++++---- _packs/controllers/contact_controller.js | 36 +-- _packs/controllers/country_controller.js | 102 +++---- .../controllers/floating_alert_controller.js | 22 +- _packs/controllers/menu_controller.js | 24 +- _packs/controllers/notification_controller.js | 42 +-- _packs/controllers/order_controller.js | 66 ++--- .../pay_what_you_can_controller.js | 232 +++++++++------- _packs/controllers/postal_code_controller.js | 190 ++++++++++++- _packs/controllers/scroll_controller.js | 34 ++- _packs/controllers/search_controller.js | 100 +++---- _packs/controllers/share_controller.js | 26 +- _packs/controllers/slider_controller.js | 65 +++-- _packs/controllers/state_controller.js | 94 ++++--- _packs/controllers/stock_controller.js | 88 +++--- _packs/endpoints/sutty.js | 71 ++--- _packs/entry.js | 93 ++++--- 25 files changed, 1290 insertions(+), 911 deletions(-) diff --git a/_packs/controllers/cart_base_controller.js b/_packs/controllers/cart_base_controller.js index da43477..f489a61 100644 --- a/_packs/controllers/cart_base_controller.js +++ b/_packs/controllers/cart_base_controller.js @@ -1,13 +1,13 @@ -import { Controller } from 'stimulus' -import { Liquid } from 'liquidjs' +import { Controller } from "stimulus"; +import { Liquid } from "liquidjs"; /* * Base controller, shared methods go here and other classes extend from * this. */ export class CartBaseController extends Controller { - get spree () { - return window.spree + get spree() { + return window.spree; } /* @@ -15,40 +15,42 @@ export class CartBaseController extends Controller { * * @return [Array] */ - get products () { - if (!this.cart) return [] + get products() { + 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 */ - get storage () { - return window.localStorage + get storage() { + return window.localStorage; } /* * Temporary storage */ - get storageTemp () { - return window.sessionStorage + get storageTemp() { + return window.sessionStorage; } - get cart () { - return JSON.parse(this.storage.getItem('cart')) + get cart() { + return JSON.parse(this.storage.getItem("cart")); } - set cart (response) { - this.storage.setItem('cart', JSON.stringify(response.success())) + set cart(response) { + this.storage.setItem("cart", JSON.stringify(response.success())); } - get email () { - return this.storageTemp.getItem('email') + get email() { + return this.storageTemp.getItem("email"); } - set email (email) { - this.storageTemp.setItem('email', email) + set email(email) { + this.storageTemp.setItem("email", email); } /* @@ -56,8 +58,8 @@ export class CartBaseController extends Controller { * * @return [String] */ - get token () { - return this.storage.getItem('token') + get token() { + return this.storage.getItem("token"); } /* @@ -65,12 +67,12 @@ export class CartBaseController extends Controller { * * @return [String] */ - get bearerToken () { - return this.storageTemp.getItem('bearerToken') + get bearerToken() { + return this.storageTemp.getItem("bearerToken"); } - set bearerToken (token) { - this.storageTemp.setItem('bearerToken', token) + set bearerToken(token) { + this.storageTemp.setItem("bearerToken", token); } /* @@ -78,20 +80,22 @@ export class CartBaseController extends Controller { * * @return Liquid */ - get engine () { - if (!window.liquid) window.liquid = new Liquid() + get engine() { + if (!window.liquid) window.liquid = new Liquid(); - return window.liquid + return window.liquid; } /* * Updates the item counter */ - counterUpdate () { - const item_count = this.cart.data.attributes.item_count + counterUpdate() { + const item_count = this.cart.data.attributes.item_count; - window.dispatchEvent(new CustomEvent('cart:counter', { detail: { item_count }})) - this.storage.setItem('cart:counter', item_count) + window.dispatchEvent( + 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] */ - idFromInputName (input) { - const matches = input.name.match(/\[([^\]]+)\]$/) + idFromInputName(input) { + const matches = input.name.match(/\[([^\]]+)\]$/); - return (matches === null) ? input.name : matches[1] + return matches === null ? input.name : matches[1]; } - async handleFailure (response) { - const data = { type: 'primary' } - let template = 'alert' - let notify = true + async handleFailure(response) { + const data = { type: "primary" }; + let template = "alert"; + let notify = true; - const site = window.site - const fail = response.fail() + const site = window.site; + const fail = response.fail(); switch (fail.name) { - case 'MisconfigurationError': - data.content = fail.message - break - case 'NoResponseError': - data.content = site.i18n.alerts.no_response_error - break - case 'SpreeError': - data.content = site.i18n.alerts.spree_error - break - case 'BasicSpreeError': + case "MisconfigurationError": + data.content = fail.message; + break; + case "NoResponseError": + data.content = site.i18n.alerts.no_response_error; + break; + case "SpreeError": + data.content = site.i18n.alerts.spree_error; + break; + case "BasicSpreeError": // XXX: The order is missing, we need to start a new one if (fail.serverResponse.status === 404) { - template = 'recover_order' - data.content = site.i18n.alerts.recover_order + template = "recover_order"; + data.content = site.i18n.alerts.recover_order; } else { - data.content = response.fail().summary + data.content = response.fail().summary; } - break - case 'ExpandedSpreeError': - const content = [] + break; + case "ExpandedSpreeError": + const content = []; // XXX: La API devuelve los mensajes de error tal cual en la // llave `error` pero el cliente solo nos da acceso a `errors`. for (const field of Object.keys(fail.errors)) { 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('. ') - notify = false + data.content = content.join(". "); + notify = false; - break + break; default: - data.content = fail.message + data.content = fail.message; } - if (notify) console.error(fail.name, data.content) - window.dispatchEvent(new CustomEvent('notification', { detail: { template, data } })) + if (notify) console.error(fail.name, data.content); + window.dispatchEvent( + new CustomEvent("notification", { detail: { template, data } }) + ); } - set formDisabled (state) { - if (!this.hasFormTarget) return + set formDisabled(state) { + if (!this.hasFormTarget) return; - this.formTarget.elements.forEach(x => x.disabled = !!state) + this.formTarget.elements.forEach((x) => (x.disabled = !!state)); } - assignShippingAddress () { - if (!this.bearerToken) return + assignShippingAddress() { + if (!this.bearerToken) return; - const bearerToken = this.bearerToken - const orderToken = this.token + const bearerToken = this.bearerToken; + const orderToken = this.token; - this.spree.sutty.assignOrderShippingAddress({ bearerToken }, { orderToken }) + this.spree.sutty.assignOrderShippingAddress( + { bearerToken }, + { orderToken } + ); } - notify (data = {}) { - window.dispatchEvent(new CustomEvent('notification', { detail: { template: 'alert', data } })) + notify(data = {}) { + window.dispatchEvent( + new CustomEvent("notification", { detail: { template: "alert", data } }) + ); } /* @@ -181,66 +196,75 @@ export class CartBaseController extends Controller { * * @param [String] URL */ - visit (url) { + visit(url) { try { - Turbolinks.visit(url) + Turbolinks.visit(url); } catch { - window.location = url + window.location = url; } } - async firstAddress (bearerToken) { + async firstAddress(bearerToken) { if (!this._firstAddress) { - const addresses = await this.spree.account.addressesList({ bearerToken }) + const addresses = await this.spree.account.addressesList({ bearerToken }); if (addresses.isFail()) { - this.handleFailure(addresses) - return + this.handleFailure(addresses); + return; } // XXX: Asumimos que si se registró tiene una dirección que vamos // 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) { - const updateAddress = await this.spree.account.updateAddress({ bearerToken }, id, { address }) + const updateAddress = await this.spree.account.updateAddress( + { bearerToken }, + id, + { address } + ); if (updateAddress.isFail()) { - this.handleFailure(updateAddress) - return + this.handleFailure(updateAddress); + return; } - return updateAddress.success() + return updateAddress.success(); } 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()) { - this.handleFailure(shippingMethods) - return + this.handleFailure(shippingMethods); + return; } - return shippingMethods.success() + return shippingMethods.success(); } - fireCajon (state = 'open', cajon = 'cajon') { - window.dispatchEvent(new CustomEvent('cajon', { detail: { cajon, state }})) + fireCajon(state = "open", cajon = "cajon") { + window.dispatchEvent( + new CustomEvent("cajon", { detail: { cajon, state } }) + ); } - formDataToObject (formData) { - const object = {} + formDataToObject(formData) { + const object = {}; 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; } } diff --git a/_packs/controllers/cart_confirmation_controller.js b/_packs/controllers/cart_confirmation_controller.js index c04d763..d58e4c2 100644 --- a/_packs/controllers/cart_confirmation_controller.js +++ b/_packs/controllers/cart_confirmation_controller.js @@ -1,41 +1,47 @@ -import { CartBaseController } from './cart_base_controller' +import { CartBaseController } from "./cart_base_controller"; export default class extends CartBaseController { - static targets = [ 'order' ] + static targets = ["order"]; - async connect () { + async connect() { if (this.clear) { - this.storage.clear() - window.dispatchEvent(new CustomEvent('cart:counter', { detail: { item_count: 0 }})) + this.storage.clear(); + window.dispatchEvent( + new CustomEvent("cart:counter", { detail: { item_count: 0 } }) + ); } - if (!this.template) return + if (!this.template) return; if (this.storage.cart) { - const order = this.cart.data.attributes - const products = this.products - const site = window.site - const shipping_address = JSON.parse(this.storage.getItem('shipping_address')) + const order = this.cart.data.attributes; + const products = this.products; + const site = window.site; + 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 { - data = JSON.parse(this.storage.getItem('confirmation')) + data = JSON.parse(this.storage.getItem("confirmation")); } - this.render(data) + this.render(data); } - render (data = {}) { - this.engine.parseAndRender(this.template, data).then(html => this.orderTarget.innerHTML = html) + render(data = {}) { + this.engine + .parseAndRender(this.template, data) + .then((html) => (this.orderTarget.innerHTML = html)); } - get clear () { - return this.element.dataset.clear + get clear() { + return this.element.dataset.clear; } - get template () { - return window.templates[this.element.dataset.template] + get template() { + return window.templates[this.element.dataset.template]; } } diff --git a/_packs/controllers/cart_contact_controller.js b/_packs/controllers/cart_contact_controller.js index b663c3e..8eae1fd 100644 --- a/_packs/controllers/cart_contact_controller.js +++ b/_packs/controllers/cart_contact_controller.js @@ -1,24 +1,24 @@ -import { CartBaseController } from './cart_base_controller' +import { CartBaseController } from "./cart_base_controller"; export default class extends CartBaseController { - static targets = [ 'form', 'username' ] + static targets = ["form", "username"]; - connect () { - if (!this.hasUsernameTarget) return - if (!this.hasFormTarget) return + connect() { + if (!this.hasUsernameTarget) return; + if (!this.hasFormTarget) return; - this.formTarget.addEventListener('focusout', event => { + this.formTarget.addEventListener("focusout", (event) => { if (!this.formTarget.checkValidity()) { - this.formTarget.classList.add('was-validated') - return + this.formTarget.classList.add("was-validated"); + return; } - this.formTarget.classList.remove('was-validated') + this.formTarget.classList.remove("was-validated"); - const username = this.usernameTarget.value.trim() - if (username.length === 0) return + const username = this.usernameTarget.value.trim(); + if (username.length === 0) return; - this.email = username - }) + this.email = username; + }); } } diff --git a/_packs/controllers/cart_controller.js b/_packs/controllers/cart_controller.js index 97b43e2..b83a150 100644 --- a/_packs/controllers/cart_controller.js +++ b/_packs/controllers/cart_controller.js @@ -1,4 +1,4 @@ -import { CartBaseController } from './cart_base_controller' +import { CartBaseController } from "./cart_base_controller"; /* * Manages the cart and its contents. @@ -17,10 +17,10 @@ import { CartBaseController } from './cart_base_controller' */ export default class extends CartBaseController { - static targets = [ 'quantity', 'subtotal', 'addedQuantity' ] + static targets = ["quantity", "subtotal", "addedQuantity"]; - connect () { - if (!this.hasQuantityTarget) return + connect() { + if (!this.hasQuantityTarget) return; /* * 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. */ - this.quantityTarget.addEventListener('change', async (event) => { - const quantity = event.target.value + this.quantityTarget.addEventListener("change", async (event) => { + const quantity = event.target.value; if (quantity < 1) return; - const orderToken = await this.tokenGetOrCreate() - const product = this.product + const orderToken = await this.tokenGetOrCreate(); + 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 }, { - line_item_id: product.line_item.id, - quantity, - include: 'line_items' - }) + const response = await this.spree.cart.setQuantity( + { orderToken }, + { + line_item_id: product.line_item.id, + quantity, + include: "line_items", + } + ); - event.target.disabled = false - event.target.focus() + event.target.disabled = false; + event.target.focus(); // If we're failing here it could be due to a missing order, so we // ask the user to decide what they want to do about it if (response.isFail()) { - this.handleFailure(response) - return + this.handleFailure(response); + return; } - this.cart = response - this.subtotalUpdate() - this.counterUpdate() - await this.itemStore() + this.cart = response; + this.subtotalUpdate(); + this.counterUpdate(); + 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 () { - window.dispatchEvent(new Event('cart:subtotal:update')) + subtotalUpdate() { + window.dispatchEvent(new Event("cart:subtotal:update")); } /* @@ -76,20 +80,20 @@ export default class extends CartBaseController { * * @return [String] */ - async cartCreate () { - const response = await this.spree.cart.create() + async cartCreate() { + const response = await this.spree.cart.create(); // If we fail here it's probably a server error, so we inform the // user. if (response.isFail()) { - this.handleFailure(response) - return + this.handleFailure(response); + return; } - this.cart = response - this.storage.setItem('token', response.success().data.attributes.token) + this.cart = response; + 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] */ - async tokenGetOrCreate () { - let token = this.storage.getItem('token') + async tokenGetOrCreate() { + 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] */ - get variantId () { - return this.data.get('variantId') + get variantId() { + return this.data.get("variantId"); } - get product () { - const product = JSON.parse(this.storage.getItem(this.storageId)) + get product() { + const product = JSON.parse(this.storage.getItem(this.storageId)); if (!product) { - console.error("El producto es nulo!", this.storageId, this.storage.length, this.cart) + 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] */ - findLineItem () { - const line_item = this.cart.included.find(x => (x.type === 'line_item' && x.relationships.variant.data.id == this.variantId)) + findLineItem() { + 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 () { - return `cart:item:${this.variantId}` + get storageId() { + return `cart:item:${this.variantId}`; } /* @@ -143,17 +156,20 @@ export default class extends CartBaseController { * * @see {./order_controller.js} */ - itemStore () { - this.storage.setItem(this.storageId, JSON.stringify({ - variant_id: this.variantId, - line_item: this.findLineItem(), - image: this.data.get('image'), - title: this.data.get('title'), - url: this.data.get('url'), - stock: this.data.get('stock'), - in_stock: this.data.get('inStock'), - extra: this.data.get('extra') ? this.data.get('extra').split('|') : [] - })) + itemStore() { + this.storage.setItem( + this.storageId, + JSON.stringify({ + variant_id: this.variantId, + line_item: this.findLineItem(), + image: this.data.get("image"), + title: this.data.get("title"), + url: this.data.get("url"), + stock: this.data.get("stock"), + in_stock: this.data.get("inStock"), + extra: this.data.get("extra") ? this.data.get("extra").split("|") : [], + }) + ); } /* @@ -164,26 +180,31 @@ export default class extends CartBaseController { * The item needs a variant ID to be added. */ async add(event, quantity = 1, floating_alert = true) { - const addedQuantity = this.addedQuantity() - if (addedQuantity > 1) quantity = addedQuantity + const addedQuantity = this.addedQuantity(); + if (addedQuantity > 1) quantity = addedQuantity; - const orderToken = await this.tokenGetOrCreate() - const response = await this.spree.cart.addItem({ orderToken }, { variant_id: this.variantId, quantity, include: 'line_items' }) + const orderToken = await this.tokenGetOrCreate(); + const response = await this.spree.cart.addItem( + { orderToken }, + { variant_id: this.variantId, quantity, include: "line_items" } + ); if (response.isFail()) { - this.handleFailure(response) - return + this.handleFailure(response); + return; } - this.cart = response - this.itemStore() - this.counterUpdate() - this.fireCajon() + this.cart = response; + this.itemStore(); + this.counterUpdate(); + this.fireCajon(); if (floating_alert) { - const site = window.site - const content = site.cart.added - window.dispatchEvent(new CustomEvent('floating:alert', { detail: { content }})) + const site = window.site; + const content = site.cart.added; + 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 * item is removed, it removes itself from the page and the storage. */ - async remove () { - const product = this.product + async remove() { + const product = this.product; - if (!product) return - if (!product.line_item) return + if (!product) return; + if (!product.line_item) return; - const orderToken = this.token - const response = await this.spree.cart.removeItem({ orderToken }, product.line_item.id, { include: 'line_items' }) + const orderToken = this.token; + const response = await this.spree.cart.removeItem( + { orderToken }, + product.line_item.id, + { include: "line_items" } + ); if (response.isFail()) { - this.handleFailure(response) - return + this.handleFailure(response); + return; } - this.cart = response - this.storage.removeItem(this.storageId) - this.element.remove() - this.subtotalUpdate() - this.counterUpdate() + this.cart = response; + this.storage.removeItem(this.storageId); + this.element.remove(); + this.subtotalUpdate(); + this.counterUpdate(); } /* * Shows variants */ - async variants () { - const template = 'variants' + async variants() { + const template = "variants"; const data = { product: { - variant_id: this.data.get('variantId'), - digital_variant_id: this.data.get('digitalVariantId'), - image: this.data.get('image'), - title: this.data.get('title'), - price: this.data.get('price'), - digital_price: this.data.get('digitalPrice'), - in_stock: this.data.get('inStock'), - extra: this.data.get('extra').split('|') - } - } + variant_id: this.data.get("variantId"), + digital_variant_id: this.data.get("digitalVariantId"), + image: this.data.get("image"), + title: this.data.get("title"), + price: this.data.get("price"), + digital_price: this.data.get("digitalPrice"), + in_stock: this.data.get("inStock"), + extra: this.data.get("extra").split("|"), + }, + }; - window.dispatchEvent(new CustomEvent('notification', { detail: { template, data } })) + window.dispatchEvent( + new CustomEvent("notification", { detail: { template, data } }) + ); } /* * Recovers the order if something failed */ - async recover () { - console.error('Recuperando pedido', this.token) + async recover() { + console.error("Recuperando pedido", this.token); // Removes the failing token - this.storage.removeItem('token') + this.storage.removeItem("token"); // Get a new token and cart - await this.tokenGetOrCreate() + await this.tokenGetOrCreate(); // 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 // mimicking user's actions // // XXX: We don't use forEach because it's not async for (const variant of cart.data.relationships.variants.data) { - this.data.set('variantId', variant.id) + 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('title', product.title) - this.data.set('extra', product.extra.join('|')) + this.data.set("image", product.image); + this.data.set("title", product.title); + this.data.set("extra", product.extra.join("|")); - await this.add(null, product.line_item.attributes.quantity, false) + await this.add(null, product.line_item.attributes.quantity, false); } } /* * Si le compradore aumenta la cantidad antes de agregar */ - addedQuantity () { - if (!this.hasAddedQuantityTarget) return 0 + addedQuantity() { + 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; } } diff --git a/_packs/controllers/cart_counter_controller.js b/_packs/controllers/cart_counter_controller.js index 0db1abd..0daf064 100644 --- a/_packs/controllers/cart_counter_controller.js +++ b/_packs/controllers/cart_counter_controller.js @@ -1,25 +1,28 @@ -import { CartBaseController } from './cart_base_controller' +import { CartBaseController } from "./cart_base_controller"; export default class extends CartBaseController { - static targets = [ 'counter' ] + static targets = ["counter"]; - connect () { + connect() { if (!this.hasCounterTarget) { - console.error("Missing counter target") - return + console.error("Missing counter target"); + return; } - window.addEventListener('cart:counter', event => this.counter = event.detail.item_count) - window.addEventListener('storage', event => { - if (event.key == 'cart:counter') this.counter = event.newValue - }) + window.addEventListener( + "cart:counter", + (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) { - this.counterTarget.innerText = quantity + set counter(quantity) { + this.counterTarget.innerText = quantity; } } diff --git a/_packs/controllers/cart_coupon_controller.js b/_packs/controllers/cart_coupon_controller.js index 15edb3d..e51f63a 100644 --- a/_packs/controllers/cart_coupon_controller.js +++ b/_packs/controllers/cart_coupon_controller.js @@ -1,64 +1,73 @@ -import { CartBaseController } from './cart_base_controller' +import { CartBaseController } from "./cart_base_controller"; /* * Retrieves shipping methods */ export default class extends CartBaseController { - static targets = [ 'couponCodeInvalid', 'preDiscount', 'total' ] + static targets = ["couponCodeInvalid", "preDiscount", "total"]; - connect () { - this.couponCode.addEventListener('input', event => { - this.couponCode.parentElement.classList.remove('was-validated') - this.couponCode.setCustomValidity('') - }) + connect() { + this.couponCode.addEventListener("input", (event) => { + this.couponCode.parentElement.classList.remove("was-validated"); + this.couponCode.setCustomValidity(""); + }); } - get couponCode () { - if (!this._couponCode) this._couponCode = this.element.elements.coupon_code + get couponCode() { + if (!this._couponCode) this._couponCode = this.element.elements.coupon_code; - return this._couponCode + return this._couponCode; } - get couponCodeInvalid () { - return this.hasCouponCodeInvalidTarget ? this.couponCodeInvalidTarget : document.querySelector('#coupon-code-invalid') + get couponCodeInvalid() { + return this.hasCouponCodeInvalidTarget + ? this.couponCodeInvalidTarget + : document.querySelector("#coupon-code-invalid"); } - get preDiscount () { - return this.hasPreDiscountTarget ? this.preDiscountTarget : document.querySelector('#pre-discount') + get preDiscount() { + return this.hasPreDiscountTarget + ? this.preDiscountTarget + : document.querySelector("#pre-discount"); } - get total () { - return this.hasTotalTarget ? this.totalTarget : document.querySelector('#total') + get total() { + return this.hasTotalTarget + ? this.totalTarget + : document.querySelector("#total"); } - set total (total) { - this.total.innerHTML = total + set total(total) { + this.total.innerHTML = total; } - async apply (event = undefined) { - event?.preventDefault() - event?.stopPropagation() + async apply(event = undefined) { + event?.preventDefault(); + event?.stopPropagation(); - const orderToken = this.token - const coupon_code = this.couponCode.value - const include = 'line_items' + const orderToken = this.token; + const coupon_code = this.couponCode.value; + 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()) { - this.couponCodeInvalid.innerHTML = response.fail().summary - this.couponCode.setCustomValidity(response.fail().summary) - this.couponCode.parentElement.classList.add('was-validated') + this.couponCodeInvalid.innerHTML = response.fail().summary; + this.couponCode.setCustomValidity(response.fail().summary); + 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.total = response.success().data.attributes.total - this.preDiscount.classList.remove('d-none') + this.cart = response; + this.total = response.success().data.attributes.total; + this.preDiscount.classList.remove("d-none"); } } diff --git a/_packs/controllers/cart_payment_methods_controller.js b/_packs/controllers/cart_payment_methods_controller.js index c9f882e..a294639 100644 --- a/_packs/controllers/cart_payment_methods_controller.js +++ b/_packs/controllers/cart_payment_methods_controller.js @@ -1,88 +1,92 @@ -import { CartBaseController } from './cart_base_controller' +import { CartBaseController } from "./cart_base_controller"; /* * Retrieves payment methods and redirect to external checkouts */ export default class extends CartBaseController { - static targets = [ 'form', 'submit', 'specialInstructions' ] + static targets = ["form", "submit", "specialInstructions"]; - async connect () { - const orderToken = this.token - const response = await this.spree.checkout.paymentMethods({ orderToken }) + async connect() { + const orderToken = this.token; + const response = await this.spree.checkout.paymentMethods({ orderToken }); if (response.isFail()) { - this.handleFailure(response) - return + this.handleFailure(response); + return; } - const payment_methods = response.success().data - const site = window.site - const cart = this.cart - const next = { url: this.data.get('nextUrl') } - const back = { url: this.data.get('backUrl') } + const payment_methods = response.success().data; + const site = window.site; + const cart = this.cart; + const next = { url: this.data.get("nextUrl") }; + 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 = {}) { - const template = window.templates[this.data.get('template')] + async render(data = {}) { + 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 - this.formTarget.elements.forEach(p => p.addEventListener('change', e => this.submitTarget.disabled = false)) + if (!this.hasSubmitTarget) return; + this.formTarget.elements.forEach((p) => + p.addEventListener("change", (e) => (this.submitTarget.disabled = false)) + ); } - async pay (event) { - event.preventDefault() - event.stopPropagation() + async pay(event) { + event.preventDefault(); + event.stopPropagation(); - this.formDisabled = true + this.formDisabled = true; - const payment_method_id = this.formTarget.elements.payment_method_id.value - const orderToken = this.token - const special_instructions = this.specialInstructionsTarget.value.trim() + const payment_method_id = this.formTarget.elements.payment_method_id.value; + const orderToken = this.token; + const special_instructions = this.specialInstructionsTarget.value.trim(); // XXX: Currently SpreeClient expects us to send payment source // 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: { special_instructions, - payments_attributes: [{ payment_method_id }] }, + payments_attributes: [{ payment_method_id }], payment_source: { [payment_method_id]: { - name: 'Pepitx', + name: "Pepitx", month: 12, - year: 2020 - } - } - } - }) + year: 2020, + }, + }, + }, + } + ); if (response.isFail()) { - this.handleFailure(response) - this.formDisabled = false - return + this.handleFailure(response); + this.formDisabled = false; + return; } - this.cart = response + this.cart = response; - response = await this.spree.checkout.complete({ orderToken }) + response = await this.spree.checkout.complete({ orderToken }); if (response.isFail()) { - this.handleFailure(response) - this.formDisabled = false - return + this.handleFailure(response); + this.formDisabled = false; + return; } - this.cart = response + this.cart = response; - const checkoutUrls = await this.spree.sutty.getCheckoutURL({ orderToken }) - let redirectUrl = this.data.get('nextUrl') + const checkoutUrls = await this.spree.sutty.getCheckoutURL({ orderToken }); + 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; } } diff --git a/_packs/controllers/cart_paypal_confirmation_controller.js b/_packs/controllers/cart_paypal_confirmation_controller.js index 9ac2777..85c9f8a 100644 --- a/_packs/controllers/cart_paypal_confirmation_controller.js +++ b/_packs/controllers/cart_paypal_confirmation_controller.js @@ -1,4 +1,4 @@ -import { CartBaseController } from './cart_base_controller' +import { CartBaseController } from "./cart_base_controller"; /* * Replaces checkout.js. @@ -9,15 +9,22 @@ import { CartBaseController } from './cart_base_controller' * discarded. */ export default class extends CartBaseController { - async connect () { - if (this.params.PayerID === undefined) return + async connect() { + if (this.params.PayerID === undefined) return; - this.site = window.site - this.element.innerHTML = this.site.i18n.cart.layouts.paypal.confirming + this.site = window.site; + this.element.innerHTML = this.site.i18n.cart.layouts.paypal.confirming; fetch(this.executeURL) - .then(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) + .then( + (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] */ - get params () { - if (this._params) return this._params + get 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. */ - get executeURL () { - return [ window.spree.host, 'paypal', 'execute', this.params.orderId, this.params.paymentId, this.params.PayerID ].join('/') + get executeURL() { + return [ + window.spree.host, + "paypal", + "execute", + this.params.orderId, + this.params.paymentId, + this.params.PayerID, + ].join("/"); } } diff --git a/_packs/controllers/cart_shipping_controller.js b/_packs/controllers/cart_shipping_controller.js index 64cc33f..403b211 100644 --- a/_packs/controllers/cart_shipping_controller.js +++ b/_packs/controllers/cart_shipping_controller.js @@ -1,110 +1,127 @@ -import { CartBaseController } from './cart_base_controller' +import { CartBaseController } from "./cart_base_controller"; export default class extends CartBaseController { - static targets = [ 'methods', 'rates', 'form' ] + static targets = ["methods", "rates", "form"]; - connect () { - this.formTarget.addEventListener('formdata', event => this.processShippingAddress(event.formData)) + connect() { + this.formTarget.addEventListener("formdata", (event) => + this.processShippingAddress(event.formData) + ); } - async rates (event) { - event.preventDefault() - event.stopPropagation() + async rates(event) { + event.preventDefault(); + event.stopPropagation(); if (!this.formTarget.checkValidity()) { - this.adressTarget.classList.add('was-validated') - return + this.adressTarget.classList.add("was-validated"); + return; } - this.formTarget.classList.remove('was-validated') + this.formTarget.classList.remove("was-validated"); // FormDataEvent es muy reciente if (window.FormDataEvent) { // Esto lanza el evento formdata en el formulario - new FormData(event.target) + new FormData(event.target); } else { // Fallback - this.processShippingAddress(new FormData(event.target)) + this.processShippingAddress(new FormData(event.target)); } } - payment (event) { - event.preventDefault() - event.stopPropagation() + payment(event) { + event.preventDefault(); + event.stopPropagation(); // FormDataEvent es muy reciente if (window.FormDataEvent) { // Esto lanza el evento formdata en el formulario - new FormData(event.target) + new FormData(event.target); } else { - this.processShippingRate(new FormData(event.target)) + this.processShippingRate(new FormData(event.target)); } } - async processShippingAddress (formData) { - this.formDisabled = true + async processShippingAddress(formData) { + this.formDisabled = true; - const email = this.email - const orderToken = this.token + const email = this.email; + const orderToken = this.token; - const ship_address_attributes = this.formDataToObject(formData) - const bill_address_attributes = ship_address_attributes + const ship_address_attributes = this.formDataToObject(formData); + 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()) { - this.handleFailure(response) - this.formDisabled = false - return + this.handleFailure(response); + this.formDisabled = false; + return; } - const shippingMethods = await this.shippingMethods(orderToken) + const shippingMethods = await this.shippingMethods(orderToken); if (!shippingMethods) { - this.formDisabled = false - return + this.formDisabled = false; + 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 - const shipping_method = shippingMethods.data[0] - const site = window.site + const shipping_method = shippingMethods.data[0]; + 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}`) - if (nextStep) nextStep.scrollIntoView() + const nextStep = document.querySelector( + `#${this.element.dataset.scrollTo}` + ); + if (nextStep) nextStep.scrollIntoView(); } - async processShippingRate (formData) { - const rate = this.formDataToObject(formData) - const orderToken = this.token + async processShippingRate(formData) { + const rate = this.formDataToObject(formData); + const orderToken = this.token; // XXX: Deshabilitar el formulario después del evento FormData, de // 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()) { - this.handleFailure(response) - return + this.handleFailure(response); + return; } - this.cart = response + this.cart = response; // Continue to next step try { - Turbolinks.visit(this.data.get('next')) + Turbolinks.visit(this.data.get("next")); } catch { - window.location = this.data.get('next') + window.location = this.data.get("next"); } } - async render (data = {}) { - const template = window.templates[this.data.get('template')] + async render(data = {}) { + const template = window.templates[this.data.get("template")]; - this.methodsTarget.innerHTML = await this.engine.parseAndRender(template, data) - this.ratesTarget.addEventListener('formdata', event => this.processShippingRate(event.formData)) + this.methodsTarget.innerHTML = await this.engine.parseAndRender( + template, + data + ); + this.ratesTarget.addEventListener("formdata", (event) => + this.processShippingRate(event.formData) + ); } } diff --git a/_packs/controllers/contact_controller.js b/_packs/controllers/contact_controller.js index 108119d..593d302 100644 --- a/_packs/controllers/contact_controller.js +++ b/_packs/controllers/contact_controller.js @@ -1,42 +1,42 @@ -import { Controller } from 'stimulus' +import { Controller } from "stimulus"; /* * Sólo permite enviar el formulario de contacto después de unos * segundos, para evitar el spam. */ export default class extends Controller { - static targets = [ 'submit' ] + static targets = ["submit"]; - connect () { - if (!this.hasSubmitTarget) return + connect() { + 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 // permite enviar. this._interval = setInterval(() => { - const delay = this.delay + const delay = this.delay; if (this.delay == 0) { - clearInterval(this._interval) - this.submitTarget.disabled = false - this.submitTarget.value = this._value + clearInterval(this._interval); + this.submitTarget.disabled = false; + this.submitTarget.value = this._value; } else { - this.delay = delay - 1 + this.delay = delay - 1; } - }, 1000) + }, 1000); } - get delay () { - const delay = parseInt(this.element.dataset.delay) + get delay() { + const delay = parseInt(this.element.dataset.delay); - return isNaN(delay) ? 0 : delay + return isNaN(delay) ? 0 : delay; } - set delay (value) { - this.element.dataset.delay = value - this.submitTarget.value = `${this._value} (${value})` + set delay(value) { + this.element.dataset.delay = value; + this.submitTarget.value = `${this._value} (${value})`; } } diff --git a/_packs/controllers/country_controller.js b/_packs/controllers/country_controller.js index 6becb09..3b89fff 100644 --- a/_packs/controllers/country_controller.js +++ b/_packs/controllers/country_controller.js @@ -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 @@ -6,96 +6,104 @@ import { CartBaseController } from './cart_base_controller' */ export default class extends CartBaseController { // All are required! - static targets = [ 'id', 'iso', 'list', 'name' ] + static targets = ["id", "iso", "list", "name"]; - async connect () { - const countries = await this.countries() + async connect() { + const countries = await this.countries(); - countries.forEach(country => { - const option = document.createElement('option') + countries.forEach((country) => { + const option = document.createElement("option"); - option.value = country.attributes.name - option.dataset.id = country.id - option.dataset.iso = country.attributes.iso - option.dataset.statesRequired = country.attributes.states_required - option.dataset.zipcodeRequired = country.attributes.zipcode_required + option.value = country.attributes.name; + option.dataset.id = country.id; + option.dataset.iso = country.attributes.iso; + option.dataset.statesRequired = country.attributes.states_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 - this.nameTarget.pattern = countries.map(x => x.attributes.name).join('|') - this.nameTarget.addEventListener('input', event => this.nameTarget.setCustomValidity('')) - this.nameTarget.addEventListener('invalid', event => this.nameTarget.setCustomValidity(site.i18n.countries.validation)) + this.nameTarget.pattern = countries.map((x) => x.attributes.name).join("|"); + this.nameTarget.addEventListener("input", (event) => + 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 // state list via an Event - this.nameTarget.addEventListener('change', event => { - const value = this.nameTarget.value.trim() + this.nameTarget.addEventListener("change", (event) => { + const value = this.nameTarget.value.trim(); - if (value === '') return + if (value === "") return; - const options = Array.from(this.nameTarget.list.options) - const option = options.find(x => x.value == value) + const options = Array.from(this.nameTarget.list.options); + const option = options.find((x) => x.value == value); // TODO: If no option is found, mark the field as invalid - if (!option) return + if (!option) return; - this.idTarget.value = option.dataset.id - this.isoTarget.value = option.dataset.iso + this.idTarget.value = option.dataset.id; + this.isoTarget.value = option.dataset.iso; - this.idTarget.dispatchEvent(new Event('change')) - this.isoTarget.dispatchEvent(new Event('change')) + this.idTarget.dispatchEvent(new Event("change")); + this.isoTarget.dispatchEvent(new Event("change")); - this.dispatchChangedEvent(option.dataset) + this.dispatchChangedEvent(option.dataset); // XXX: Prevent mixing data - delete this.nameTarget.dataset.selectedState - delete this.nameTarget.dataset.selectedZipcode - }) + delete this.nameTarget.dataset.selectedState; + delete this.nameTarget.dataset.selectedZipcode; + }); // The input is disabled at this point - this.nameTarget.disabled = false + this.nameTarget.disabled = false; // 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 * subscribe to changes. */ - dispatchChangedEvent (data = {}) { - const event = new CustomEvent('cart:country:update', { + dispatchChangedEvent(data = {}) { + const event = new CustomEvent("cart:country:update", { detail: { id: this.idTarget.value, iso: this.isoTarget.value, - group: this.data.get('group'), + group: this.data.get("group"), selectedState: this.nameTarget.dataset.selectedState, selectedZipcode: this.nameTarget.dataset.selectedZipcode, - data - } - }) + data, + }, + }); - window.dispatchEvent(event) + window.dispatchEvent(event); } /* * Fetch the country list from storage or from API */ - async countries () { - const countries = JSON.parse(this.storageTemp.getItem('countries')) + async 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 - 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; } } diff --git a/_packs/controllers/floating_alert_controller.js b/_packs/controllers/floating_alert_controller.js index d50317e..7509d26 100644 --- a/_packs/controllers/floating_alert_controller.js +++ b/_packs/controllers/floating_alert_controller.js @@ -1,18 +1,18 @@ -import { Controller } from 'stimulus' +import { Controller } from "stimulus"; export default class extends Controller { - static targets = [ 'content' ] + static targets = ["content"]; - connect () { - window.addEventListener('toast', event => { - this.contentTarget.innerText = event.detail.content - this.element.classList.toggle('hide') - this.element.classList.toggle('show') + connect() { + window.addEventListener("toast", (event) => { + this.contentTarget.innerText = event.detail.content; + this.element.classList.toggle("hide"); + this.element.classList.toggle("show"); setTimeout(() => { - this.element.classList.toggle('hide') - this.element.classList.toggle('show') - }, 3000) - }) + this.element.classList.toggle("hide"); + this.element.classList.toggle("show"); + }, 3000); + }); } } diff --git a/_packs/controllers/menu_controller.js b/_packs/controllers/menu_controller.js index 3fe8794..c05585d 100644 --- a/_packs/controllers/menu_controller.js +++ b/_packs/controllers/menu_controller.js @@ -1,29 +1,31 @@ -import { Controller } from 'stimulus' +import { Controller } from "stimulus"; export default class extends Controller { - static targets = [ 'item' ] + static targets = ["item"]; - connect () { - window.addEventListener('scroll:section', event => this.update(event.detail.id)) + connect() { + window.addEventListener("scroll:section", (event) => + this.update(event.detail.id) + ); } - get items () { + get items() { if (!this._items) { - this._items = {} + this._items = {}; 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)) { - 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"); } } diff --git a/_packs/controllers/notification_controller.js b/_packs/controllers/notification_controller.js index 4ef5456..9dbff80 100644 --- a/_packs/controllers/notification_controller.js +++ b/_packs/controllers/notification_controller.js @@ -1,43 +1,47 @@ -import { Controller } from 'stimulus' -import { Liquid } from 'liquidjs' +import { Controller } from "stimulus"; +import { Liquid } from "liquidjs"; /* * Waits for notifications and shows them by fetching and rendering * a template. */ export default class extends Controller { - connect () { - window.addEventListener('notification', async event => await this.render(event.detail.template, event.detail.data)) + connect() { + window.addEventListener( + "notification", + async (event) => + await this.render(event.detail.template, event.detail.data) + ); } /* * Renders and replaces notification contents and then shows it. Does * nothing if the template isn't found. */ - async render (name, data = {}) { - data.site = window.site + async render(name, data = {}) { + data.site = window.site; - const template = window.templates.alert - const html = await this.engine.parseAndRender(template, data) + const template = window.templates.alert; + const html = await this.engine.parseAndRender(template, data); - this.element.innerHTML = html - this.show() + this.element.innerHTML = html; + this.show(); } /* * Shows the notification */ - show () { - this.element.classList.add('show') - this.element.classList.remove('hide') + show() { + this.element.classList.add("show"); + this.element.classList.remove("hide"); } /* * Hides the notification */ - hide () { - this.element.classList.add('hide') - this.element.classList.remove('show') + hide() { + this.element.classList.add("hide"); + this.element.classList.remove("show"); } /* @@ -45,9 +49,9 @@ export default class extends Controller { * * @return Liquid */ - get engine () { - if (!window.liquid) window.liquid = new Liquid() + get engine() { + if (!window.liquid) window.liquid = new Liquid(); - return window.liquid + return window.liquid; } } diff --git a/_packs/controllers/order_controller.js b/_packs/controllers/order_controller.js index 08b7c50..7bedf71 100644 --- a/_packs/controllers/order_controller.js +++ b/_packs/controllers/order_controller.js @@ -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 @@ -6,46 +6,48 @@ import { CartBaseController } from './cart_base_controller' * Liquid. */ export default class extends CartBaseController { - static targets = [ 'cart', 'subtotal', 'itemCount' ] + static targets = ["cart", "subtotal", "itemCount"]; - async connect () { - const products = this.products - const site = window.site + async connect() { + const products = this.products; + const site = window.site; - this.render({ products, site }) - this.subtotalUpdate() - this.itemCountUpdate() - this.subscribe() + this.render({ products, site }); + this.subtotalUpdate(); + this.itemCountUpdate(); + this.subscribe(); } /* * Subscribe to change on the storage to update the cart. */ - subscribe () { - window.addEventListener('storage', async event => { - if (!event.key?.startsWith('cart:item:')) return + subscribe() { + window.addEventListener("storage", async (event) => { + if (!event.key?.startsWith("cart:item:")) return; - const products = this.products - const site = window.site + const products = this.products; + const site = window.site; - this.render({ products, site }) - this.subtotalUpdate() - this.itemCountUpdate() - }) + this.render({ products, site }); + this.subtotalUpdate(); + this.itemCountUpdate(); + }); - window.addEventListener('cart:subtotal:update', event => { - this.itemCountUpdate() - this.subtotalUpdate() - }) + window.addEventListener("cart:subtotal:update", (event) => { + this.itemCountUpdate(); + this.subtotalUpdate(); + }); } /* * Download the item template and render the order */ - render (data = {}) { - const template = window.templates[this.data.get('itemTemplate')] + render(data = {}) { + 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 */ - subtotalUpdate () { - if (!this.cart) return + subtotalUpdate() { + if (!this.cart) return; - this.subtotalTarget.innerText = this.cart.data.attributes.display_total + this.subtotalTarget.innerText = this.cart.data.attributes.display_total; } - itemCountUpdate () { - if (!this.hasItemCountTarget) return - if (!this.cart) return + itemCountUpdate() { + if (!this.hasItemCountTarget) return; + if (!this.cart) return; - this.itemCountTarget.innerText = this.cart.data.attributes.item_count + this.itemCountTarget.innerText = this.cart.data.attributes.item_count; } } diff --git a/_packs/controllers/pay_what_you_can_controller.js b/_packs/controllers/pay_what_you_can_controller.js index 9602774..053674e 100644 --- a/_packs/controllers/pay_what_you_can_controller.js +++ b/_packs/controllers/pay_what_you_can_controller.js @@ -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 @@ -7,29 +7,29 @@ import { CartBaseController } from './cart_base_controller' * proceso de pago. */ export default class extends CartBaseController { - static targets = [ 'form' ] + static targets = ["form"]; static values = { variantId: Number, currency: String, price: Number, - firstname: String - } + firstname: String, + }; - connect () { + connect() { this.paymentMethodByCurrency = { - ARS: 'Spree::PaymentMethod::MercadoPago', - USD: 'Spree::PaymentMethod::Paypal' - } + ARS: "Spree::PaymentMethod::MercadoPago", + USD: "Spree::PaymentMethod::Paypal", + }; } - store (event) { - const target = event.currentTarget || event.target + store(event) { + const target = event.currentTarget || event.target; - this[`${target.dataset.name}Value`] = target.value + this[`${target.dataset.name}Value`] = target.value; } - set formDisable (disable) { - this.formTarget.elements.forEach(x => x.disabled = disable) + set formDisable(disable) { + this.formTarget.elements.forEach((x) => (x.disabled = disable)); } /* @@ -45,162 +45,204 @@ export default class extends CartBaseController { * * Reenviar a confirmación * * Ejecutar el pago (si aplica) */ - async pay (event = undefined) { + async pay(event = undefined) { if (event) { - event.preventDefault() - event.stopPropagation() + event.preventDefault(); + event.stopPropagation(); } if (!this.formTarget.checkValidity()) { - this.formTarget.classList.add('was-validated') - return + this.formTarget.classList.add("was-validated"); + return; } - this.formDisable = true + this.formDisable = true; // Crear pedido. Todos los pedidos van a ser hechos desde // Argentina, no hay forma de cambiar esto. - const orderToken = await this.tempCartCreate() - const quantity = 1 - const include = 'line_items' - const currency = this.currencyValue - const price = this.priceValue - const email = 'noreply@sutty.nl' - const firstname = this.firstnameValue - const lastname = '-' - const address1 = '-' - const country_id = 250 // XXX: Internet - const city = '-' - const phone = '11111111' - const zipcode = '1111' - const ship_address_attributes = { firstname, lastname, address1, city, country_id, zipcode, phone } - const bill_address_attributes = ship_address_attributes - const confirmation_delivered = true - const custom_return_url = this.customReturnUrl() + const orderToken = await this.tempCartCreate(); + const quantity = 1; + const include = "line_items"; + const currency = this.currencyValue; + const price = this.priceValue; + const email = "noreply@sutty.nl"; + const firstname = this.firstnameValue; + const lastname = "-"; + const address1 = "-"; + const country_id = 250; // XXX: Internet + const city = "-"; + const phone = "11111111"; + const zipcode = "1111"; + const ship_address_attributes = { + firstname, + lastname, + 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 - 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) { - this.formDisable = false - console.error('No se pudo generar la variante', { variant_id, price, currency, quantity }) - return + this.formDisable = false; + console.error("No se pudo generar la variante", { + variant_id, + price, + currency, + quantity, + }); + return; } // 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) { - console.error(response) - this.formDisable = false - return + console.error(response); + this.formDisable = false; + return; } // 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()) { - this.handleFailure(response) - this.formDisable = false - return + this.handleFailure(response); + this.formDisable = false; + return; } // 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()) { - this.handleFailure(response) - this.formDisable = false - return + this.handleFailure(response); + this.formDisable = false; + return; } // 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()) { - this.handleFailure(response) - this.formDisable = false - return + this.handleFailure(response); + this.formDisable = false; + return; } // Elegir medio de envío - response = await this.spree.checkout.orderUpdate({ orderToken }, { - order: { - shipments_attributes: [{ - id: response.success().data[0].id, - selected_shipping_rate_id: response.success().included.filter(x => x.type == 'shipping_rate')[0].id - }] + response = await this.spree.checkout.orderUpdate( + { orderToken }, + { + order: { + shipments_attributes: [ + { + id: response.success().data[0].id, + selected_shipping_rate_id: response + .success() + .included.filter((x) => x.type == "shipping_rate")[0].id, + }, + ], + }, } - }) + ); // Elegir medio de pago - response = await this.spree.checkout.paymentMethods({ orderToken }) + response = await this.spree.checkout.paymentMethods({ orderToken }); if (response.isFail()) { - this.handleFailure(response) - this.formDisable = false - return + this.handleFailure(response); + this.formDisable = false; + 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 }] }, payment_source: { [payment_method_id]: { - name: 'Pepitx', + name: "Pepitx", month: 12, - year: 2021 - } - } - }) + year: 2021, + }, + }, + } + ); if (response.isFail()) { - this.handleFailure(response) - this.formDisable = false - return + this.handleFailure(response); + this.formDisable = false; + return; } - response = await this.spree.checkout.complete({ orderToken }) + response = await this.spree.checkout.complete({ orderToken }); if (response.isFail()) { - this.handleFailure(response) - this.formDisable = false - return + this.handleFailure(response); + this.formDisable = false; + return; } // Reenviar al medio de pago - const checkoutUrls = await this.spree.sutty.getCheckoutURL({ orderToken }) - const redirectUrl = checkoutUrls.data[0] + const checkoutUrls = await this.spree.sutty.getCheckoutURL({ orderToken }); + const redirectUrl = checkoutUrls.data[0]; - Turbolinks.visit(redirectUrl) + Turbolinks.visit(redirectUrl); // Volver } - async tempCartCreate () { - const response = await this.spree.cart.create() + async tempCartCreate() { + const response = await this.spree.cart.create(); // If we fail here it's probably a server error, so we inform the // user. if (response.isFail()) { - this.handleFailure(response) - return + this.handleFailure(response); + return; } - return response.success().data.attributes.token + return response.success().data.attributes.token; } // @return [String] - customReturnUrl () { - const url = new URL(window.location.href) - url.searchParams.set('open', '') + customReturnUrl() { + const url = new URL(window.location.href); + url.searchParams.set("open", ""); - return url.toString() + return url.toString(); } } diff --git a/_packs/controllers/postal_code_controller.js b/_packs/controllers/postal_code_controller.js index 9a65db5..4e26707 100644 --- a/_packs/controllers/postal_code_controller.js +++ b/_packs/controllers/postal_code_controller.js @@ -1,11 +1,11 @@ -import { Controller } from 'stimulus' +import { Controller } from "stimulus"; /* * Subscribes to the country change event and changes the validation * pattern of its input. */ export default class extends Controller { - static targets = [ 'code' ] + static targets = ["code"]; /* * 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} */ - 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 () { - window.addEventListener('cart:country:update', event => { - if (this.data.get('group') !== event.detail.group) return + connect() { + window.addEventListener("cart:country:update", (event) => { + 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.disabled = !zipcodeRequired - this.codeTarget.required = zipcodeRequired + this.codeTarget.value = ""; + this.codeTarget.disabled = !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) { - this.codeTarget.value = event.detail.selectedZipcode - this.codeTarget.dispatchEvent(new Event('change')) + this.codeTarget.value = event.detail.selectedZipcode; + this.codeTarget.dispatchEvent(new Event("change")); } - }) + }); } } diff --git a/_packs/controllers/scroll_controller.js b/_packs/controllers/scroll_controller.js index e6a6874..9e39831 100644 --- a/_packs/controllers/scroll_controller.js +++ b/_packs/controllers/scroll_controller.js @@ -1,4 +1,4 @@ -import { Controller } from 'stimulus' +import { Controller } from "stimulus"; /* * 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ú. */ export default class extends Controller { - static targets = [ 'section' ] + static targets = ["section"]; - connect () { + connect() { for (const section of this.sectionTargets) { - this.observer.observe(section) + this.observer.observe(section); } } /* * Solo nos interesa la primera */ - get observer () { - if (!this._observer) this._observer = new IntersectionObserver((entries, observer) => this.update(entries), this.options) + get observer() { + if (!this._observer) + this._observer = new IntersectionObserver( + (entries, observer) => this.update(entries), + this.options + ); - return this._observer + return this._observer; } - get options () { - if (!this._options) this._options = { threshold: 0, rootMargin: '0px' } + get options() { + if (!this._options) this._options = { threshold: 0, rootMargin: "0px" }; - return this._options + return this._options; } - update (entries) { - const section = entries.find(x => x.isIntersecting) + update(entries) { + 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 } }) + ); } } diff --git a/_packs/controllers/search_controller.js b/_packs/controllers/search_controller.js index 5608ab2..fcc8449 100644 --- a/_packs/controllers/search_controller.js +++ b/_packs/controllers/search_controller.js @@ -1,90 +1,96 @@ -import { Controller } from 'stimulus' -import { Liquid } from 'liquidjs' +import { Controller } from "stimulus"; +import { Liquid } from "liquidjs"; -const lunr = require("lunr") -require("lunr-languages/lunr.stemmer.support")(lunr) -require("lunr-languages/lunr.es")(lunr) +const lunr = require("lunr"); +require("lunr-languages/lunr.stemmer.support")(lunr); +require("lunr-languages/lunr.es")(lunr); export default class extends Controller { - static targets = [ 'q' ] + static targets = ["q"]; - get q () { - if (!this.hasQTarget) return - if (!this.qTarget.value.trim().length === 0) return + get q() { + if (!this.hasQTarget) return; + if (!this.qTarget.value.trim().length === 0) return; - return this.qTarget.value.trim().replaceAll(/[:~\*\^\+\-]/gi, '') + return this.qTarget.value.trim().replaceAll(/[:~\*\^\+\-]/gi, ""); } - connect () { - const q = new URLSearchParams(window.location.search).get('q')?.trim() + connect() { + const q = new URLSearchParams(window.location.search).get("q")?.trim(); if (q) { - this.qTarget.value = q - this.search() + this.qTarget.value = q; + this.search(); } } - async search (event) { + async search(event) { // Detiene el envío del formulario if (event) { - event.preventDefault() - event.stopPropagation() + event.preventDefault(); + event.stopPropagation(); } - this.formDisable = true + this.formDisable = true; // 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 if (q) { // Trae el índice de búsqueda - await this.fetch() + await this.fetch(); // Hasta que no haya índice no buscamos nada, esto evita que se // aprete enter dos veces y se fallen cosas. - if (!window.index) return + if (!window.index) return; } - const main = document.querySelector('main') - const results = window.index.search(q).map(r => window.data.find(a => a.id == r.ref)) - const site = window.site - const template = window.templates.results - const html = await this.engine.parseAndRender(template, { q, site, results }) - const title = `${site.i18n.search.title} - ${q}` - const query = new URLSearchParams({ q }) + const main = document.querySelector("main"); + const results = window.index + .search(q) + .map((r) => window.data.find((a) => a.id == r.ref)); + const site = window.site; + const template = window.templates.results; + 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()}`) - document.title = title + window.history.pushState({ q }, title, `?${query.toString()}`); + document.title = title; - main.innerHTML = html - this.formDisable = false + main.innerHTML = html; + this.formDisable = false; } - async fetch () { + async fetch() { // Solo permite descargar uno a la vez - if (this.fetching) return + if (this.fetching) return; - this.fetching = true - let response + this.fetching = true; + let response; // Si no existe el índice, lo descarga y procesa Lunr if (!window.data) { - response = await fetch('data.json') - window.data = await response.json() + response = await fetch("data.json"); + window.data = await response.json(); } if (!window.index) { - response = await fetch('idx.json') - const idx = await response.json() - window.index = lunr.Index.load(idx) + response = await fetch("idx.json"); + const idx = await response.json(); + window.index = lunr.Index.load(idx); } // Permitir volver a ejecutar - this.fetching = false + this.fetching = false; } - set formDisable (disable) { - this.element.elements.forEach(x => x.disabled = disable) + set formDisable(disable) { + this.element.elements.forEach((x) => (x.disabled = disable)); } /* @@ -92,9 +98,9 @@ export default class extends Controller { * * @return Liquid */ - get engine () { - if (!window.liquid) window.liquid = new Liquid() + get engine() { + if (!window.liquid) window.liquid = new Liquid(); - return window.liquid + return window.liquid; } } diff --git a/_packs/controllers/share_controller.js b/_packs/controllers/share_controller.js index b0d1486..4f45767 100644 --- a/_packs/controllers/share_controller.js +++ b/_packs/controllers/share_controller.js @@ -1,26 +1,26 @@ -import { Controller } from 'stimulus' +import { Controller } from "stimulus"; export default class extends Controller { static values = { title: String, text: String, - url: String - } + url: String, + }; - async share (event = undefined) { - event?.preventDefault() - event?.stopPropagation() + async share(event = undefined) { + event?.preventDefault(); + event?.stopPropagation(); - const title = this.titleValue - const text = this.textValue - const url = this.urlValue - const data = { title, text, url } + const title = this.titleValue; + const text = this.textValue; + const url = this.urlValue; + const data = { title, text, url }; - if ('share' in navigator) { + if ("share" in navigator) { if (navigator.canShare(data)) { - navigator.share(data) + navigator.share(data); } else { - console.error('No se puede compartir', data) + console.error("No se puede compartir", data); } } } diff --git a/_packs/controllers/slider_controller.js b/_packs/controllers/slider_controller.js index 60a31c2..6103210 100644 --- a/_packs/controllers/slider_controller.js +++ b/_packs/controllers/slider_controller.js @@ -1,64 +1,73 @@ -import { Controller } from 'stimulus' +import { Controller } from "stimulus"; /* * Slider con scroll automático */ export default class extends Controller { - static targets = [ 'control' ] + static targets = ["control"]; - connect () { - this.active(this.controlTargets.find(x => x.href.endsWith(window.location.hash))) + connect() { + 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 () { - const duration = parseInt(this.data.get('duration')) + get duration() { + const duration = parseInt(this.data.get("duration")); - return isNaN(duration) ? 15 : duration + return isNaN(duration) ? 15 : duration; } - disconnect () { - clearInterval(this.interval) + disconnect() { + clearInterval(this.interval); } - active (control) { - if (!control) return + active(control) { + if (!control) return; - this.controlTargets.forEach(other => other.classList.toggle('active', control.href === other.href)) - this.current = this.controlTargets.indexOf(control) + this.controlTargets.forEach((other) => + 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 // 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 () { - return parseInt(this.data.get('current')) || 0 + get current() { + return parseInt(this.data.get("current")) || 0; } - set current (value) { - this.data.set('current', value) + set current(value) { + this.data.set("current", value); } - get next () { - const next = this.current + 1 + get next() { + 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(); return ( bounding.top >= 0 && bounding.left >= 0 && - bounding.bottom <= (window.innerHeight || document.documentElement.clientHeight) && - bounding.right <= (window.innerWidth || document.documentElement.clientWidth) + bounding.bottom <= + (window.innerHeight || document.documentElement.clientHeight) && + bounding.right <= + (window.innerWidth || document.documentElement.clientWidth) ); - }; + } } diff --git a/_packs/controllers/state_controller.js b/_packs/controllers/state_controller.js index e84a0e8..25bb5f6 100644 --- a/_packs/controllers/state_controller.js +++ b/_packs/controllers/state_controller.js @@ -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 @@ -7,85 +7,91 @@ import { CartBaseController } from './cart_base_controller' */ export default class extends CartBaseController { // All are required! - static targets = [ 'id', 'list', 'name' ] + static targets = ["id", "list", "name"]; - connect () { - window.addEventListener('cart:country:update', async event => { - if (this.data.get('group') !== event.detail.group) return + connect() { + window.addEventListener("cart:country:update", async (event) => { + if (this.data.get("group") !== event.detail.group) return; - this.idTarget.value = '' - this.nameTarget.value = '' - this.listTarget.innerHTML = '' + this.idTarget.value = ""; + this.nameTarget.value = ""; + this.listTarget.innerHTML = ""; - const statesRequired = event.detail.data.statesRequired == 'true' + const statesRequired = event.detail.data.statesRequired == "true"; - this.nameTarget.disabled = !statesRequired - this.nameTarget.required = statesRequired + this.nameTarget.disabled = !statesRequired; + this.nameTarget.required = statesRequired; - if (!statesRequired) return + if (!statesRequired) return; - const states = await this.states(event.detail.iso) - const site = window.site + const states = await this.states(event.detail.iso); + const site = window.site; - states.forEach(state => { - let option = document.createElement('option') - option.value = state.attributes.name - option.dataset.id = state.id + states.forEach((state) => { + let option = document.createElement("option"); + option.value = state.attributes.name; + 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.addEventListener('input', event => this.nameTarget.setCustomValidity('')) - this.nameTarget.addEventListener('invalid', event => this.nameTarget.setCustomValidity(site.i18n.states.validation)) + this.nameTarget.pattern = states.map((x) => x.attributes.name).join("|"); + this.nameTarget.addEventListener("input", (event) => + this.nameTarget.setCustomValidity("") + ); + this.nameTarget.addEventListener("invalid", (event) => + this.nameTarget.setCustomValidity(site.i18n.states.validation) + ); if (event.detail.selectedState) { - this.nameTarget.value = event.detail.selectedState - this.nameTarget.dispatchEvent(new Event('change')) + this.nameTarget.value = event.detail.selectedState; + this.nameTarget.dispatchEvent(new Event("change")); } - }) + }); // When the input changes we update the actual value and also the // state list via an Event - this.nameTarget.addEventListener('change', event => { - const options = Array.from(this.listTarget.options) - const option = options.find(x => x.value == this.nameTarget.value) + this.nameTarget.addEventListener("change", (event) => { + const options = Array.from(this.listTarget.options); + const option = options.find((x) => x.value == this.nameTarget.value); // 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.dispatchEvent(new Event('change')) - }) + this.idTarget.value = option.dataset.id; + this.idTarget.dispatchEvent(new Event("change")); + }); } /* * Fetch the state list from storage or from API using a country ISO * code */ - async states (countryIso) { - const stateId = `states:${countryIso}` - let states = JSON.parse(this.storageTemp.getItem(stateId)) + async states(countryIso) { + const stateId = `states:${countryIso}`; + 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 // 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 if (response.isFail()) { - this.handleFailure(response) - return {} + this.handleFailure(response); + return {}; } - states = response.success().included + states = response.success().included; // 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; } } diff --git a/_packs/controllers/stock_controller.js b/_packs/controllers/stock_controller.js index d716913..736a480 100644 --- a/_packs/controllers/stock_controller.js +++ b/_packs/controllers/stock_controller.js @@ -1,4 +1,4 @@ -import { Controller } from 'stimulus' +import { Controller } from "stimulus"; /* * Mantiene el stock actualizado, consultando a la API. @@ -9,48 +9,48 @@ import { Controller } from 'stimulus' * * Deshabilita botón si no está en stock */ export default class extends Controller { - static targets = [ 'product' ] + static targets = ["product"]; - async connect () { - const all_skus = this.skus + async connect() { + 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 // 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 end = this.per_page + let start = 0; + let end = this.per_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 - end = start + this.per_page + start = this.per_page * local_page; + end = start + this.per_page; - const filter = { skus } - let response = await window.spree.products.list({ filter }) + const filter = { skus }; + let response = await window.spree.products.list({ filter }); if (response.isFail()) { - console.error(response.fail()) - return + console.error(response.fail()); + return; } - this.update_local_products(response.success().data) + this.update_local_products(response.success().data); // Recorrer todas las páginas // XXX: Podríamos usar next pero la página 1 siempre se devuelve a // sí misma y entraríamos en un loop infinito. 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()) { - console.error(response.fail()) - continue + console.error(response.fail()); + 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] */ - get skus () { - return [...new Set(this.productTargets.map(p=> p.dataset.sku).filter(x => x.length > 0))] + get skus() { + 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 */ - get per_page () { + get per_page() { if (!this._per_page) { - this._per_page = parseInt(this.element.dataset.perPage) - if (isNaN(this._per_page)) this._per_page = 100 + this._per_page = parseInt(this.element.dataset.perPage); + 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. */ - update_local_products (products) { + update_local_products(products) { for (const local of this.productTargets) { - for (const product of products.filter(p => local.dataset.cartVariantId === p.relationships.default_variant.data.id)) { - local.dataset.cartInStock = product.attributes.in_stock - local.dataset.cartPrice = product.attributes.price + for (const product of products.filter( + (p) => + 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.querySelectorAll('[data-stock-price]').forEach(price => price.innerText = parseInt(product.attributes.price)) - local.querySelectorAll('[data-stock-currency]').forEach(currency => currency.innerText = product.attributes.currency) + local + .querySelectorAll("[data-stock-add]") + .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) + ); } } } diff --git a/_packs/endpoints/sutty.js b/_packs/endpoints/sutty.js index 3b9c834..136f96c 100644 --- a/_packs/endpoints/sutty.js +++ b/_packs/endpoints/sutty.js @@ -1,80 +1,81 @@ -import Axios from 'axios' -import * as qs from 'qs' +import Axios from "axios"; +import * as qs from "qs"; /* * XXX: We're copying code from @spree/storefront-api-v2-sdk/src/Http.ts * because we don't know how to mix Typescript :D */ export class Sutty { - constructor (host = 'http://localhost:3000') { - this.host = host + constructor(host = "http://localhost:3000") { + this.host = host; this.axios = Axios.create({ baseURL: this.host, - headers: { 'Content-Type': 'application/json' }, - paramsSerializer: params => qs.stringify(params, { arrayFormat: 'brackets' }) - }) + headers: { "Content-Type": "application/json" }, + paramsSerializer: (params) => + qs.stringify(params, { arrayFormat: "brackets" }), + }); } async getCheckoutURL(tokens = {}) { - const headers = this.spreeOrderHeaders(tokens) + const headers = this.spreeOrderHeaders(tokens); const axiosConfig = { - url: 'api/v2/storefront/checkout_redirect.json', + url: "api/v2/storefront/checkout_redirect.json", params: {}, - method: 'get', - headers - } + method: "get", + headers, + }; - return await this.axios(axiosConfig) + return await this.axios(axiosConfig); } async assignOrderOwnership(tokens = {}, params = {}) { - const headers = this.spreeOrderHeaders(tokens) + const headers = this.spreeOrderHeaders(tokens); const axiosConfig = { - url: 'api/v2/storefront/assign_order_ownership.json', + url: "api/v2/storefront/assign_order_ownership.json", params, - method: 'post', - headers - } + method: "post", + headers, + }; - return await this.axios(axiosConfig) + return await this.axios(axiosConfig); } async assignOrderShippingAddress(tokens = {}, params = {}) { - const headers = this.spreeOrderHeaders(tokens) + const headers = this.spreeOrderHeaders(tokens); const axiosConfig = { - url: 'api/v2/storefront/assign_order_shipping_address.json', + url: "api/v2/storefront/assign_order_shipping_address.json", params, - method: 'post', - headers - } + method: "post", + headers, + }; - return await this.axios(axiosConfig) + return await this.axios(axiosConfig); } async assignOrderBillingAddress(tokens = {}, params = {}) { - const headers = this.spreeOrderHeaders(tokens) + const headers = this.spreeOrderHeaders(tokens); const axiosConfig = { - url: 'api/v2/storefront/assign_order_billing_address.json', + url: "api/v2/storefront/assign_order_billing_address.json", params, - method: 'post', - headers - } + method: "post", + headers, + }; - return await this.axios(axiosConfig) + return await this.axios(axiosConfig); } spreeOrderHeaders(tokens) { - const header = {} + const header = {}; if (tokens.orderToken) { - header['X-Spree-Order-Token'] = tokens.orderToken + header["X-Spree-Order-Token"] = tokens.orderToken; } if (tokens.bearerToken) { - header['Authorization'] = `Bearer ${tokens.bearerToken}` + header["Authorization"] = `Bearer ${tokens.bearerToken}`; } - return header + return header; } } diff --git a/_packs/entry.js b/_packs/entry.js index 514c165..0665332 100644 --- a/_packs/entry.js +++ b/_packs/entry.js @@ -1,25 +1,25 @@ -import BotDetector from 'device-detector-js/dist/parsers/bot' -import { Notifier } from '@airbrake/browser' +import BotDetector from "device-detector-js/dist/parsers/bot"; +import { Notifier } from "@airbrake/browser"; -window.bot_detector = new BotDetector -const bot = window.bot_detector.parse(navigator.userAgent) +window.bot_detector = new BotDetector(); +const bot = window.bot_detector.parse(navigator.userAgent); if (!bot) { window.airbrake = new Notifier({ projectId: window.env.AIRBRAKE_PROJECT_ID, 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) => { - window.airbrake.notify(e.join(' ')) - return console.originalError(...e) - } + window.airbrake.notify(e.join(" ")); + return console.originalError(...e); + }; } -import 'core-js/stable' -import 'regenerator-runtime/runtime' +import "core-js/stable"; +import "regenerator-runtime/runtime"; // 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". @@ -27,46 +27,49 @@ import 'regenerator-runtime/runtime' // Cambiamos de turbolinks a turbo porque turbo soporta la función // fetch(), que luego es interceptada por el SW para obtener las // direcciones localmente. -import * as Turbo from "@hotwired/turbo" -Turbo.start() +import * as Turbo from "@hotwired/turbo"; +Turbo.start(); -import { Application } from 'stimulus' -import { definitionsFromContext } from "stimulus/webpack-helpers" +import { Application } from "stimulus"; +import { definitionsFromContext } from "stimulus/webpack-helpers"; -const application = Application.start() -const context = require.context("./controllers", true, /\.js$/) -application.load(definitionsFromContext(context)) +const application = Application.start(); +const context = require.context("./controllers", true, /\.js$/); +application.load(definitionsFromContext(context)); -import { makeClient } from '@spree/storefront-api-v2-sdk' -import { Sutty } from './endpoints/sutty' +import { makeClient } from "@spree/storefront-api-v2-sdk"; +import { Sutty } from "./endpoints/sutty"; -window.spree = makeClient({ host: window.env.SPREE_URL }) -window.spree.sutty = new Sutty(window.spree.host) +window.spree = makeClient({ host: window.env.SPREE_URL }); +window.spree.sutty = new Sutty(window.spree.host); try { - window.axe = require('axe-core/axe') -} catch(e) {} + window.axe = require("axe-core/axe"); +} 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.querySelectorAll("a[href^='http://'],a[href^='https://'],a[href^='//']").forEach(a => { - a.rel = "noopener" - a.target = "_blank" - }) +document.addEventListener("turbo:load", (event) => { + document + .querySelectorAll("a[href^='http://'],a[href^='https://'],a[href^='//']") + .forEach((a) => { + a.rel = "noopener"; + a.target = "_blank"; + }); - if (!window.axe) return + if (!window.axe) return; - window.axe.run().then(results => { - results.violations.forEach(violation => { - violation.nodes.forEach(node => { - node.target.forEach(target => { - document.querySelectorAll(target).forEach(element => { - element.classList.add('inaccesible') - element.ariaLabel = node.failureSummary - }) - }) - }) - }) - }) -}) + window.axe.run().then((results) => { + results.violations.forEach((violation) => { + violation.nodes.forEach((node) => { + node.target.forEach((target) => { + document.querySelectorAll(target).forEach((element) => { + element.classList.add("inaccesible"); + element.ariaLabel = node.failureSummary; + }); + }); + }); + }); + }); +}); From 588c608fa295b8bd706500206bc179bdf629325c Mon Sep 17 00:00:00 2001 From: Nulo Date: Thu, 25 Nov 2021 17:53:32 +0000 Subject: [PATCH 58/60] Arreglar floating-alert --- _includes/floating_alert.html | 4 ++-- _layouts/default.html | 1 + .../controllers/floating_alert_controller.js | 22 ++++++++++++++----- assets/css/styles.scss | 1 + 4 files changed, 21 insertions(+), 7 deletions(-) diff --git a/_includes/floating_alert.html b/_includes/floating_alert.html index 87a5ed9..5a5043c 100644 --- a/_includes/floating_alert.html +++ b/_includes/floating_alert.html @@ -1,6 +1,6 @@
    -
    +
    diff --git a/_layouts/default.html b/_layouts/default.html index 8f7dff2..8994f1f 100644 --- a/_layouts/default.html +++ b/_layouts/default.html @@ -57,5 +57,6 @@ {% include_cached footer.html %} + {% include_cached floating_alert.html %} diff --git a/_packs/controllers/floating_alert_controller.js b/_packs/controllers/floating_alert_controller.js index 7509d26..dd6ffed 100644 --- a/_packs/controllers/floating_alert_controller.js +++ b/_packs/controllers/floating_alert_controller.js @@ -6,13 +6,25 @@ export default class extends Controller { connect() { window.addEventListener("toast", (event) => { this.contentTarget.innerText = event.detail.content; - this.element.classList.toggle("hide"); - this.element.classList.toggle("show"); + this.set(true); - setTimeout(() => { - this.element.classList.toggle("hide"); - this.element.classList.toggle("show"); + if (this.interval) { + clearTimeout(this.interval); + } + this.interval = setTimeout(() => { + this.set(false); + this.interval = null; }, 3000); }); } + + set(show) { + if (show) { + this.element.classList.remove("hide"); + this.element.classList.add("show"); + } else { + this.element.classList.add("hide"); + this.element.classList.remove("show"); + } + } } diff --git a/assets/css/styles.scss b/assets/css/styles.scss index a755a31..6868ad7 100644 --- a/assets/css/styles.scss +++ b/assets/css/styles.scss @@ -137,6 +137,7 @@ $enable-responsive-font-sizes: true; @import "menu"; @import "content"; @import "fonts"; +@import "floating_alert"; /// La barra de progreso de Turbo tiene el color primario /// de la paleta, definido por Bootstrap o por nosotres. From 01bbbf9a29f48cece40d6e728e4b30e30a66a63a Mon Sep 17 00:00:00 2001 From: Nulo Date: Thu, 25 Nov 2021 17:54:28 +0000 Subject: [PATCH 59/60] floating_alert: agregar ejemplo de uso --- _packs/controllers/floating_alert_controller.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/_packs/controllers/floating_alert_controller.js b/_packs/controllers/floating_alert_controller.js index dd6ffed..c00ee8c 100644 --- a/_packs/controllers/floating_alert_controller.js +++ b/_packs/controllers/floating_alert_controller.js @@ -1,5 +1,9 @@ import { Controller } from "stimulus"; +// Ejemplo de uso: +// window.dispatchEvent( +// new CustomEvent("toast", { detail: { content: "¡Hola, usuarix!" } }) +// ); export default class extends Controller { static targets = ["content"]; From 3a0c41b736429f8919dcad1e86878c509cc36d0f Mon Sep 17 00:00:00 2001 From: Nulo Date: Thu, 25 Nov 2021 21:40:44 +0000 Subject: [PATCH 60/60] Limpiar eventos cuando el elemento o controlador que los escucha desaparece Squashed commit of the following: commit 482eea28821868f03ace33562e7bd34ab9a4478f Merge: 5f48528 1c128f2 Author: f Date: Thu Nov 25 18:31:35 2021 -0300 Merge branch 'master' into limpiar-eventos commit 5f48528c28b0709bd859a4dc52a830f60bfedc6e Author: f Date: Thu Nov 25 18:23:23 2021 -0300 pretty commit 70d05bc90a6cb64d1c4bfc39f48388af3fbc3c18 Merge: c4f33c0 ff1bc21 Author: Nulo Date: Thu Oct 28 16:46:31 2021 -0300 Merge branch 'master' into limpiar-eventos commit c4f33c084058002a10fc0ec2137ffe045826cfd2 Author: f Date: Thu Oct 28 14:52:41 2021 -0300 limpiar eventos --- _packs/controllers/cart_contact_controller.js | 28 +++-- _packs/controllers/cart_controller.js | 68 ++++++----- _packs/controllers/cart_counter_controller.js | 25 +++-- _packs/controllers/cart_coupon_controller.js | 16 ++- .../cart_payment_methods_controller.js | 16 ++- .../controllers/cart_shipping_controller.js | 14 ++- _packs/controllers/country_controller.js | 75 ++++++++----- .../controllers/floating_alert_controller.js | 30 +++-- _packs/controllers/menu_controller.js | 14 ++- _packs/controllers/order_controller.js | 46 +++++--- _packs/controllers/postal_code_controller.js | 42 ++++--- _packs/controllers/state_controller.js | 106 ++++++++++-------- 12 files changed, 306 insertions(+), 174 deletions(-) diff --git a/_packs/controllers/cart_contact_controller.js b/_packs/controllers/cart_contact_controller.js index 8eae1fd..2ee4c9f 100644 --- a/_packs/controllers/cart_contact_controller.js +++ b/_packs/controllers/cart_contact_controller.js @@ -4,21 +4,29 @@ export default class extends CartBaseController { static targets = ["form", "username"]; connect() { + this.focusout_event = this._focusout_event.bind(this); + if (!this.hasUsernameTarget) return; if (!this.hasFormTarget) return; - this.formTarget.addEventListener("focusout", (event) => { - if (!this.formTarget.checkValidity()) { - this.formTarget.classList.add("was-validated"); - return; - } + this.formTarget.addEventListener("focusout", this.focusout_event); + } - this.formTarget.classList.remove("was-validated"); + disconnect() { + this.formTarget.removeEventListener("focusout", this.focusout_event); + } - const username = this.usernameTarget.value.trim(); - if (username.length === 0) return; + _focusout_event(event) { + if (!this.formTarget.checkValidity()) { + this.formTarget.classList.add("was-validated"); + return; + } - this.email = username; - }); + this.formTarget.classList.remove("was-validated"); + + const username = this.usernameTarget.value.trim(); + if (username.length === 0) return; + + this.email = username; } } diff --git a/_packs/controllers/cart_controller.js b/_packs/controllers/cart_controller.js index b83a150..e23115e 100644 --- a/_packs/controllers/cart_controller.js +++ b/_packs/controllers/cart_controller.js @@ -22,53 +22,61 @@ export default class extends CartBaseController { connect() { if (!this.hasQuantityTarget) return; + this.change_event = this._change_event.bind(this); + /* * When the quantity selector changes, we update the order to have * that amount of items. * * TODO: Go back to previous amount if there's not enough. */ - this.quantityTarget.addEventListener("change", async (event) => { - const quantity = event.target.value; + this.quantityTarget.addEventListener("change", this.change_event); + } - if (quantity < 1) return; + disconnect() { + this.quantityTarget.removeEventListener("change", this.change_event); + } - const orderToken = await this.tokenGetOrCreate(); - const product = this.product; + async _change_event(event) { + const quantity = event.target.value; - if (!product) return; + if (quantity < 1) return; - event.target.disabled = true; + const orderToken = await this.tokenGetOrCreate(); + const product = this.product; - const response = await this.spree.cart.setQuantity( - { orderToken }, - { - line_item_id: product.line_item.id, - quantity, - include: "line_items", - } - ); + if (!product) return; - event.target.disabled = false; - event.target.focus(); + event.target.disabled = true; - // If we're failing here it could be due to a missing order, so we - // ask the user to decide what they want to do about it - if (response.isFail()) { - this.handleFailure(response); - return; + const response = await this.spree.cart.setQuantity( + { orderToken }, + { + line_item_id: product.line_item.id, + quantity, + include: "line_items", } + ); - this.cart = response; - this.subtotalUpdate(); - this.counterUpdate(); - await this.itemStore(); + event.target.disabled = false; + event.target.focus(); - if (!this.hasSubtotalTarget) return; + // If we're failing here it could be due to a missing order, so we + // ask the user to decide what they want to do about it + if (response.isFail()) { + this.handleFailure(response); + return; + } - this.subtotalTarget.innerText = - product.line_item.attributes.discounted_amount; - }); + this.cart = response; + this.subtotalUpdate(); + this.counterUpdate(); + await this.itemStore(); + + if (!this.hasSubtotalTarget) return; + + this.subtotalTarget.innerText = + product.line_item.attributes.discounted_amount; } subtotalUpdate() { diff --git a/_packs/controllers/cart_counter_controller.js b/_packs/controllers/cart_counter_controller.js index 0daf064..94d30d3 100644 --- a/_packs/controllers/cart_counter_controller.js +++ b/_packs/controllers/cart_counter_controller.js @@ -9,20 +9,31 @@ export default class extends CartBaseController { return; } - window.addEventListener( - "cart:counter", - (event) => (this.counter = event.detail.item_count) - ); - window.addEventListener("storage", (event) => { - if (event.key == "cart:counter") this.counter = event.newValue; - }); + this.cart_count_event = this._cart_count_event.bind(this); + this.storage_event = this._storage_event.bind(this); + + window.addEventListener("cart:counter", this.cart_count_event); + window.addEventListener("storage", this.storage_event); if (!this.cart) return; this.counter = this.cart.data.attributes.item_count; } + disconnect() { + window.removeEventListener("cart:counter", this.cart_count_event); + window.removeEventListener("storage", this.storage_event); + } + set counter(quantity) { this.counterTarget.innerText = quantity; } + + _cart_count_event(event) { + this.counter = event.detail.item_count; + } + + _storage_event(event) { + if (event.key == "cart:counter") this.counter = event.newValue; + } } diff --git a/_packs/controllers/cart_coupon_controller.js b/_packs/controllers/cart_coupon_controller.js index e51f63a..dcf12f6 100644 --- a/_packs/controllers/cart_coupon_controller.js +++ b/_packs/controllers/cart_coupon_controller.js @@ -7,10 +7,18 @@ export default class extends CartBaseController { static targets = ["couponCodeInvalid", "preDiscount", "total"]; connect() { - this.couponCode.addEventListener("input", (event) => { - this.couponCode.parentElement.classList.remove("was-validated"); - this.couponCode.setCustomValidity(""); - }); + this.input_event = this._input_event.bind(this); + + this.couponCode.addEventListener("input", this.input_event); + } + + disconnect() { + this.couponCode.removeEventListener("input", this.input_event); + } + + _input_event(event) { + this.couponCode.parentElement.classList.remove("was-validated"); + this.couponCode.setCustomValidity(""); } get couponCode() { diff --git a/_packs/controllers/cart_payment_methods_controller.js b/_packs/controllers/cart_payment_methods_controller.js index a294639..c9ed0e9 100644 --- a/_packs/controllers/cart_payment_methods_controller.js +++ b/_packs/controllers/cart_payment_methods_controller.js @@ -10,6 +10,8 @@ export default class extends CartBaseController { const orderToken = this.token; const response = await this.spree.checkout.paymentMethods({ orderToken }); + this.change_event = this._change_event.bind(this); + if (response.isFail()) { this.handleFailure(response); return; @@ -31,10 +33,22 @@ export default class extends CartBaseController { if (!this.hasSubmitTarget) return; this.formTarget.elements.forEach((p) => - p.addEventListener("change", (e) => (this.submitTarget.disabled = false)) + p.addEventListener("change", this.change_event) ); } + disconnect() { + if (!this.hasSubmitTarget) return; + + this.formTarget.elements.forEach((p) => + p.removeEventListener("change", this.change_event) + ); + } + + _change_event(event) { + this.submitTarget.disabled = false; + } + async pay(event) { event.preventDefault(); event.stopPropagation(); diff --git a/_packs/controllers/cart_shipping_controller.js b/_packs/controllers/cart_shipping_controller.js index 403b211..593d7ee 100644 --- a/_packs/controllers/cart_shipping_controller.js +++ b/_packs/controllers/cart_shipping_controller.js @@ -4,9 +4,17 @@ export default class extends CartBaseController { static targets = ["methods", "rates", "form"]; connect() { - this.formTarget.addEventListener("formdata", (event) => - this.processShippingAddress(event.formData) - ); + this.formdata_event = this._formdata_event.bind(this); + + this.formTarget.addEventListener("formdata", this.formdata_event); + } + + disconnect() { + this.formTarget.removeEventListener("formdata", this.formdata_event); + } + + _formdata_event(event) { + this.processShippingAddress(event.formData); } async rates(event) { diff --git a/_packs/controllers/country_controller.js b/_packs/controllers/country_controller.js index 3b89fff..f1adccb 100644 --- a/_packs/controllers/country_controller.js +++ b/_packs/controllers/country_controller.js @@ -23,42 +23,18 @@ export default class extends CartBaseController { this.listTarget.appendChild(option); }); - const site = window.site; + this.input_event = this._input_event.bind(this); + this.invalid_event = this._invalid_event.bind(this); + this.change_event = this._change_event.bind(this); // Only allow names on this list this.nameTarget.pattern = countries.map((x) => x.attributes.name).join("|"); - this.nameTarget.addEventListener("input", (event) => - this.nameTarget.setCustomValidity("") - ); - this.nameTarget.addEventListener("invalid", (event) => - this.nameTarget.setCustomValidity(site.i18n.countries.validation) - ); + this.nameTarget.addEventListener("input", this.input_event); + this.nameTarget.addEventListener("invalid", this.invalid_event); // When the input changes we update the actual value and also the // state list via an Event - this.nameTarget.addEventListener("change", (event) => { - const value = this.nameTarget.value.trim(); - - if (value === "") return; - - const options = Array.from(this.nameTarget.list.options); - const option = options.find((x) => x.value == value); - - // TODO: If no option is found, mark the field as invalid - if (!option) return; - - this.idTarget.value = option.dataset.id; - this.isoTarget.value = option.dataset.iso; - - this.idTarget.dispatchEvent(new Event("change")); - this.isoTarget.dispatchEvent(new Event("change")); - - this.dispatchChangedEvent(option.dataset); - - // XXX: Prevent mixing data - delete this.nameTarget.dataset.selectedState; - delete this.nameTarget.dataset.selectedZipcode; - }); + this.nameTarget.addEventListener("change", this.change_event); // The input is disabled at this point this.nameTarget.disabled = false; @@ -67,6 +43,45 @@ export default class extends CartBaseController { this.nameTarget.dispatchEvent(new CustomEvent("change")); } + disconnect() { + this.nameTarget.removeEventListener("input", this.input_event); + this.nameTarget.removeEventListener("invalid", this.invalid_event); + this.nameTarget.removeEventListener("change", this.change_event); + } + + _input_event(event) { + this.nameTarget.setCustomValidity(""); + } + + _invalid_event(event) { + const site = window.site; + this.nameTarget.setCustomValidity(site.i18n.countries.validation); + } + + _change_event(event) { + const value = this.nameTarget.value.trim(); + + if (value === "") return; + + const options = Array.from(this.nameTarget.list.options); + const option = options.find((x) => x.value == value); + + // TODO: If no option is found, mark the field as invalid + if (!option) return; + + this.idTarget.value = option.dataset.id; + this.isoTarget.value = option.dataset.iso; + + this.idTarget.dispatchEvent(new Event("change")); + this.isoTarget.dispatchEvent(new Event("change")); + + this.dispatchChangedEvent(option.dataset); + + // XXX: Prevent mixing data + delete this.nameTarget.dataset.selectedState; + delete this.nameTarget.dataset.selectedZipcode; + } + /* * Sends a `cart:country:update` event so other controllers can * subscribe to changes. diff --git a/_packs/controllers/floating_alert_controller.js b/_packs/controllers/floating_alert_controller.js index c00ee8c..71dd9aa 100644 --- a/_packs/controllers/floating_alert_controller.js +++ b/_packs/controllers/floating_alert_controller.js @@ -8,18 +8,26 @@ export default class extends Controller { static targets = ["content"]; connect() { - window.addEventListener("toast", (event) => { - this.contentTarget.innerText = event.detail.content; - this.set(true); + this.toast_event = this._toast_event.bind(this); - if (this.interval) { - clearTimeout(this.interval); - } - this.interval = setTimeout(() => { - this.set(false); - this.interval = null; - }, 3000); - }); + window.addEventListener("toast", this.toast_event); + } + + disconnect() { + window.removeEventListener("toast", this.toast_event); + } + + _toast_event(event) { + this.contentTarget.innerText = event.detail.content; + this.set(true); + + if (this.interval) { + clearTimeout(this.interval); + } + this.interval = setTimeout(() => { + this.set(false); + this.interval = null; + }, 3000); } set(show) { diff --git a/_packs/controllers/menu_controller.js b/_packs/controllers/menu_controller.js index c05585d..2082fe5 100644 --- a/_packs/controllers/menu_controller.js +++ b/_packs/controllers/menu_controller.js @@ -4,9 +4,17 @@ export default class extends Controller { static targets = ["item"]; connect() { - window.addEventListener("scroll:section", (event) => - this.update(event.detail.id) - ); + this.scroll_section_event = this._scroll_section_event.bind(this); + + window.addEventListener("scroll:section", this.scroll_section_event); + } + + disconnect() { + window.removeEventListener("scroll:section", this.scroll_section_event); + } + + _scroll_section_event(event) { + this.update(event.detail.id); } get items() { diff --git a/_packs/controllers/order_controller.js b/_packs/controllers/order_controller.js index 7bedf71..5091a00 100644 --- a/_packs/controllers/order_controller.js +++ b/_packs/controllers/order_controller.js @@ -15,28 +15,40 @@ export default class extends CartBaseController { this.render({ products, site }); this.subtotalUpdate(); this.itemCountUpdate(); - this.subscribe(); + + this.storage_event = this._storage_event.bind(this); + this.cart_subtotal_update_event = + this._cart_subtotal_update_event.bind(this); + + window.addEventListener("storage", this.storage_event); + window.addEventListener( + "cart:subtotal:update", + this.cart_subtotal_update_event + ); } - /* - * Subscribe to change on the storage to update the cart. - */ - subscribe() { - window.addEventListener("storage", async (event) => { - if (!event.key?.startsWith("cart:item:")) return; + disconnect() { + window.removeEventListener("storage", this.storage_event); + window.removeEventListener( + "cart:subtotal:update", + this.cart_subtotal_update_event + ); + } - const products = this.products; - const site = window.site; + async _storage_event(event) { + if (!event.key?.startsWith("cart:item:")) return; - this.render({ products, site }); - this.subtotalUpdate(); - this.itemCountUpdate(); - }); + const products = this.products; + const site = window.site; - window.addEventListener("cart:subtotal:update", (event) => { - this.itemCountUpdate(); - this.subtotalUpdate(); - }); + this.render({ products, site }); + this.subtotalUpdate(); + this.itemCountUpdate(); + } + + _cart_subtotal_update_event(event) { + this.itemCountUpdate(); + this.subtotalUpdate(); } /* diff --git a/_packs/controllers/postal_code_controller.js b/_packs/controllers/postal_code_controller.js index 4e26707..1b639d0 100644 --- a/_packs/controllers/postal_code_controller.js +++ b/_packs/controllers/postal_code_controller.js @@ -175,24 +175,38 @@ export default class extends Controller { }; connect() { - window.addEventListener("cart:country:update", (event) => { - if (this.data.get("group") !== event.detail.group) return; + this.cart_country_update_event = this._cart_country_update_event.bind(this); - const zipcodeRequired = event.detail.data.zipcodeRequired == "true"; + window.addEventListener( + "cart:country:update", + this.cart_country_update_event + ); + } - this.codeTarget.value = ""; - this.codeTarget.disabled = !zipcodeRequired; - this.codeTarget.required = zipcodeRequired; + disconnect() { + window.removeEventListener( + "cart:country:update", + this.cart_country_update_event + ); + } - if (!zipcodeRequired) return; + _cart_country_update_event(event) { + if (this.data.get("group") !== event.detail.group) return; - this.codeTarget.pattern = - this.postal_codes[event.detail.iso.toLowerCase()] || ".*"; + const zipcodeRequired = event.detail.data.zipcodeRequired == "true"; - if (event.detail.selectedZipcode) { - this.codeTarget.value = event.detail.selectedZipcode; - this.codeTarget.dispatchEvent(new Event("change")); - } - }); + this.codeTarget.value = ""; + this.codeTarget.disabled = !zipcodeRequired; + this.codeTarget.required = zipcodeRequired; + + if (!zipcodeRequired) return; + + this.codeTarget.pattern = + this.postal_codes[event.detail.iso.toLowerCase()] || ".*"; + + if (event.detail.selectedZipcode) { + this.codeTarget.value = event.detail.selectedZipcode; + this.codeTarget.dispatchEvent(new Event("change")); + } } } diff --git a/_packs/controllers/state_controller.js b/_packs/controllers/state_controller.js index 25bb5f6..cdfa11a 100644 --- a/_packs/controllers/state_controller.js +++ b/_packs/controllers/state_controller.js @@ -10,57 +10,75 @@ export default class extends CartBaseController { static targets = ["id", "list", "name"]; connect() { - window.addEventListener("cart:country:update", async (event) => { - if (this.data.get("group") !== event.detail.group) return; + this.cart_country_update_event = this._cart_country_update_event.bind(this); + this.change_event = this._change_event.bind(this); - this.idTarget.value = ""; - this.nameTarget.value = ""; - this.listTarget.innerHTML = ""; - - const statesRequired = event.detail.data.statesRequired == "true"; - - this.nameTarget.disabled = !statesRequired; - this.nameTarget.required = statesRequired; - - if (!statesRequired) return; - - const states = await this.states(event.detail.iso); - const site = window.site; - - states.forEach((state) => { - let option = document.createElement("option"); - option.value = state.attributes.name; - option.dataset.id = state.id; - - this.listTarget.appendChild(option); - }); - - this.nameTarget.pattern = states.map((x) => x.attributes.name).join("|"); - this.nameTarget.addEventListener("input", (event) => - this.nameTarget.setCustomValidity("") - ); - this.nameTarget.addEventListener("invalid", (event) => - this.nameTarget.setCustomValidity(site.i18n.states.validation) - ); - - if (event.detail.selectedState) { - this.nameTarget.value = event.detail.selectedState; - this.nameTarget.dispatchEvent(new Event("change")); - } - }); + window.addEventListener( + "cart:country:update", + this.cart_country_update_event + ); // When the input changes we update the actual value and also the // state list via an Event - this.nameTarget.addEventListener("change", (event) => { - const options = Array.from(this.listTarget.options); - const option = options.find((x) => x.value == this.nameTarget.value); + this.nameTarget.addEventListener("change", this.change_event); + } - // TODO: If no option is found, mark the field as invalid - if (!option) return; + disconnect() { + window.removeEventListener( + "cart:country:update", + this.cart_country_update_event + ); + this.nameTarget.removeEventListener("change", this.change_event); + } - this.idTarget.value = option.dataset.id; - this.idTarget.dispatchEvent(new Event("change")); + async _cart_country_update_event(event) { + if (this.data.get("group") !== event.detail.group) return; + + this.idTarget.value = ""; + this.nameTarget.value = ""; + this.listTarget.innerHTML = ""; + + const statesRequired = event.detail.data.statesRequired == "true"; + + this.nameTarget.disabled = !statesRequired; + this.nameTarget.required = statesRequired; + + if (!statesRequired) return; + + const states = await this.states(event.detail.iso); + const site = window.site; + + states.forEach((state) => { + let option = document.createElement("option"); + option.value = state.attributes.name; + option.dataset.id = state.id; + + this.listTarget.appendChild(option); }); + + this.nameTarget.pattern = states.map((x) => x.attributes.name).join("|"); + this.nameTarget.addEventListener("input", (event) => + this.nameTarget.setCustomValidity("") + ); + this.nameTarget.addEventListener("invalid", (event) => + this.nameTarget.setCustomValidity(site.i18n.states.validation) + ); + + if (event.detail.selectedState) { + this.nameTarget.value = event.detail.selectedState; + this.nameTarget.dispatchEvent(new Event("change")); + } + } + + _change_event(event) { + const options = Array.from(this.listTarget.options); + const option = options.find((x) => x.value == this.nameTarget.value); + + // TODO: If no option is found, mark the field as invalid + if (!option) return; + + this.idTarget.value = option.dataset.id; + this.idTarget.dispatchEvent(new Event("change")); } /*