tienda
This commit is contained in:
parent
5406d28a9d
commit
990deb509d
42 changed files with 2516 additions and 31 deletions
|
@ -58,6 +58,7 @@ locales:
|
|||
- es
|
||||
ignored_layouts:
|
||||
- menu
|
||||
- email
|
||||
linked_fields:
|
||||
- post
|
||||
- item
|
||||
|
|
19
_data/en.yml
19
_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:
|
||||
|
|
19
_data/es.yml
19
_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:
|
||||
|
|
|
@ -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"'
|
||||
|
|
94
_data/forms/shipping_address.yml
Normal file
94
_data/forms/shipping_address.yml
Normal file
|
@ -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'
|
13
_data/forms/user.yml
Normal file
13
_data/forms/user.yml
Normal file
|
@ -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'
|
198
_data/layouts/cart.yml
Normal file
198
_data/layouts/cart.yml
Normal file
|
@ -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'
|
68
_data/layouts/confirmation.yml
Normal file
68
_data/layouts/confirmation.yml
Normal file
|
@ -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'
|
56
_data/layouts/email.yml
Normal file
56
_data/layouts/email.yml
Normal file
|
@ -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: ''
|
114
_data/layouts/payment.yml
Normal file
114
_data/layouts/payment.yml
Normal file
|
@ -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'
|
142
_data/layouts/product.yml
Normal file
142
_data/layouts/product.yml
Normal file
|
@ -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"
|
126
_data/layouts/shipment.yml
Normal file
126
_data/layouts/shipment.yml
Normal file
|
@ -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'
|
8
_includes/cart_add.html
Normal file
8
_includes/cart_add.html
Normal file
|
@ -0,0 +1,8 @@
|
|||
<button
|
||||
{{ include.product.in_stock | value_unless: 'disabled' }}
|
||||
data-action="cart#add"
|
||||
data-stock-add
|
||||
class="btn btn-success btn-block btn-lg">
|
||||
<span class="when-parent-disabled">{{ site.cart.out_of_stock }}</span>
|
||||
<span class="when-parent-enabled">{{ site.cart.add }}</span>
|
||||
</button>
|
10
_includes/cart_controller.html
Normal file
10
_includes/cart_controller.html
Normal file
|
@ -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: '|' }}"
|
53
_includes/country.html
Normal file
53
_includes/country.html
Normal file
|
@ -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 -%}
|
||||
|
||||
<div class="form-group" data-controller="country" data-country-group="{{ include.field[1].group }}">
|
||||
<label for="{{ id }}">
|
||||
{{ label }}
|
||||
{% if include.field[1].required %}*{% endif %}
|
||||
</label>
|
||||
|
||||
<input data-target="country.id" type="hidden" name="{{ name }}" id="{{ id }}" value="" />
|
||||
<input data-target="country.iso" type="hidden" name="{{ include.form }}_ignore_{{ include.field[0] }}_iso" value="" />
|
||||
|
||||
<input
|
||||
data-target="country.name"
|
||||
{% if help %}
|
||||
aria-describedby="help-{{ id }}"
|
||||
{% endif %}
|
||||
{% if include.field[1].required %}
|
||||
required
|
||||
{% endif %}
|
||||
type="{{ include.field[1].type }}"
|
||||
{% if autocomplete %}
|
||||
autocomplete="{{ autocomplete }}"
|
||||
{% endif %}
|
||||
name="{{ include.form }}_ignore_{{ include.field[0] }}"
|
||||
id="{{ include.form }}_ignore_{{ include.field[0] }}"
|
||||
disabled
|
||||
list="list-{{ id }}"
|
||||
class="form-control" />
|
||||
|
||||
{%- if error -%}
|
||||
<div class="invalid-feedback">{{ error }}</div>
|
||||
{%- endif -%}
|
||||
|
||||
{%- if help -%}
|
||||
<small id="help-{{ id }}" class="form-text">
|
||||
{{ help }}
|
||||
</small>
|
||||
{%- endif -%}
|
||||
|
||||
<datalist id="list-{{ id }}" data-target="country.list">
|
||||
</datalist>
|
||||
</div>
|
|
@ -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 -%}
|
||||
|
||||
<div class="form-group">
|
||||
|
@ -11,6 +18,7 @@
|
|||
</label>
|
||||
|
||||
<input
|
||||
{{ include.field[1].extra.input }}
|
||||
{% if help %}
|
||||
aria-describedby="help-{{ id }}"
|
||||
{% endif %}
|
||||
|
@ -25,6 +33,10 @@
|
|||
{% endif %}
|
||||
class="form-control" />
|
||||
|
||||
{%- if error -%}
|
||||
<div class="invalid-feedback">{{ error }}</div>
|
||||
{%- endif -%}
|
||||
|
||||
{%- if help -%}
|
||||
<small id="help-{{ id }}" class="form-text">
|
||||
{{ help }}
|
||||
|
|
45
_includes/postal_code.html
Normal file
45
_includes/postal_code.html
Normal file
|
@ -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 -%}
|
||||
|
||||
<div class="form-group" data-controller="postal-code" data-postal-code-group="{{ include.field[1].group }}">
|
||||
<label for="{{ id }}">
|
||||
{{ label }}
|
||||
{% if include.field[1].required %}*{% endif %}
|
||||
</label>
|
||||
|
||||
<input
|
||||
name="{{ name }}"
|
||||
id="{{ id }}"
|
||||
data-target="postal-code.code"
|
||||
{% if help %}
|
||||
aria-describedby="help-{{ id }}"
|
||||
{% endif %}
|
||||
{% if include.field[1].required %}
|
||||
required
|
||||
{% endif %}
|
||||
type="{{ include.field[1].type }}"
|
||||
{% if autocomplete %}
|
||||
autocomplete="{{ autocomplete }}"
|
||||
{% endif %}
|
||||
class="form-control" />
|
||||
|
||||
{%- if error -%}
|
||||
<div class="invalid-feedback">{{ error }}</div>
|
||||
{%- endif -%}
|
||||
|
||||
{%- if help -%}
|
||||
<small id="help-{{ id }}" class="form-text">
|
||||
{{ help }}
|
||||
</small>
|
||||
{%- endif -%}
|
||||
</div>
|
|
@ -1,5 +1,5 @@
|
|||
<input
|
||||
data-target="contact.submit"
|
||||
{{ include.field[1].extra }}
|
||||
type="submit"
|
||||
class="btn btn-success"
|
||||
value="{{ include.field[1].label[site.locale] }}" />
|
||||
|
|
72
_layouts/cart.html
Normal file
72
_layouts/cart.html
Normal file
|
@ -0,0 +1,72 @@
|
|||
---
|
||||
layout: default
|
||||
---
|
||||
|
||||
<section>
|
||||
<h1>{{ page.title }}</h1>
|
||||
<div class="content">
|
||||
{{ content | replace: '<img ', '<img loading="lazy" ' | replace: '<iframe ', '<iframe loading="lazy" ' }}
|
||||
</div>
|
||||
|
||||
<div
|
||||
data-controller="order"
|
||||
data-order-item-template="assets/templates/cart.html">
|
||||
|
||||
<div class="row no-gutters align-items-center font-weight-bold justify-content-center">
|
||||
<div class="d-none d-lg-block col-lg-4 p-3">
|
||||
<h4 class="m-0">{{ page.product }}</h4>
|
||||
</div>
|
||||
|
||||
<div class="d-none d-lg-block col-lg-8">
|
||||
<div class="row no-gutters align-items-center">
|
||||
<div class="col-lg p-3">
|
||||
<h4 class="m-0">{{ page.price }}</h4>
|
||||
</div>
|
||||
|
||||
<div class="col-lg p-3">
|
||||
<h4 class="m-0">{{ page.quantity }}</h4>
|
||||
</div>
|
||||
|
||||
<div class="col-lg p-3">
|
||||
<h4 class="m-0">{{ page.subtotal }}</h4>
|
||||
</div>
|
||||
|
||||
<div class="col-lg-2 p-3">
|
||||
<h4 class="m-0 sr-only">{{ page.remove }}</h4>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-12" data-target="order.cart">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row no-gutters align-items-end">
|
||||
<div class="order-last order-md-first col-12 col-md-6 col-lg-4 mt-3 mt-md-0">
|
||||
<a href="?" class="btn btn-transparent black border text-uppercase">
|
||||
{{ page.back }}
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="col-12 col-md-6 col-lg-8">
|
||||
<div class="row no-gutters align-items-center">
|
||||
<div class="col-lg"></div>
|
||||
|
||||
<div class="col-6 col-lg text-right">
|
||||
<p class="font-weight-bold">{{ page.subtotal }}</p>
|
||||
</div>
|
||||
|
||||
<div class="col-6 col-lg text-center">
|
||||
<p class="font-weight-bold" data-target="order.subtotal">0</p>
|
||||
</div>
|
||||
|
||||
<div class="col-lg-2"></div>
|
||||
|
||||
<div class="col-12">
|
||||
<a href="{{ site.shipment.url }}" class="btn btn-block btn-primary text-uppercase">{{ site.shipment.title }}</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
17
_layouts/confirmation.html
Normal file
17
_layouts/confirmation.html
Normal file
|
@ -0,0 +1,17 @@
|
|||
---
|
||||
layout: default
|
||||
---
|
||||
|
||||
<section data-controller="cart-confirmation" data-clear="true">
|
||||
<header>
|
||||
<h1>{{ page.title }}</h1>
|
||||
</header>
|
||||
|
||||
<div class="content">
|
||||
{{ content }}
|
||||
</div>
|
||||
|
||||
<p data-controller="cart-paypal-confirmation"></p>
|
||||
|
||||
<a href="?" class="btn btn-primary text-uppercase">{{ page.back }}</a>
|
||||
</section>
|
5
_layouts/page.html
Normal file
5
_layouts/page.html
Normal file
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
layout: default
|
||||
---
|
||||
|
||||
{{ content }}
|
21
_layouts/payment.html
Normal file
21
_layouts/payment.html
Normal file
|
@ -0,0 +1,21 @@
|
|||
---
|
||||
layout: default
|
||||
---
|
||||
|
||||
<section class="">
|
||||
<header class="">
|
||||
<h1>{{ page.title }}</h1>
|
||||
</header>
|
||||
|
||||
<div class="content">
|
||||
{{ content }}
|
||||
</div>
|
||||
|
||||
<div
|
||||
id="cart-payment-methods"
|
||||
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">
|
||||
</div>
|
||||
</section>
|
52
_layouts/shipment.html
Normal file
52
_layouts/shipment.html
Normal file
|
@ -0,0 +1,52 @@
|
|||
---
|
||||
layout: default
|
||||
---
|
||||
|
||||
<section>
|
||||
<header>
|
||||
<h1>{{ page.title }}</h1>
|
||||
</header>
|
||||
|
||||
<div class="content">
|
||||
{{ content }}
|
||||
</div>
|
||||
|
||||
<div id="user" data-controller="cart-contact">
|
||||
<h2>{{ page.user }}</h2>
|
||||
|
||||
<form data-target="cart-contact.form">
|
||||
{% for field in site.data.forms.user %}
|
||||
{% assign template = field[1].type | append: '.html' %}
|
||||
{% include {{ template }} field=field form='user' %}
|
||||
{% endfor %}
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div
|
||||
id="shipping-address"
|
||||
class="mt-3"
|
||||
data-controller="cart-shipping"
|
||||
data-scroll-to="cart-shipping"
|
||||
data-cart-shipping-next="{{ site.payment.url }}"
|
||||
data-cart-shipping-template="assets/templates/shipping_methods.html">
|
||||
|
||||
<h2>{{ page.shipping_address }}</h2>
|
||||
|
||||
<form
|
||||
data-target="cart-shipping.form"
|
||||
data-action="cart-shipping#rates">
|
||||
{% for field in site.data.forms.shipping_address %}
|
||||
{% assign template = field[1].type | append: '.html' %}
|
||||
{% include {{ template }} field=field %}
|
||||
{% endfor %}
|
||||
</form>
|
||||
|
||||
<div id="cart-shipping">
|
||||
<h2 class="mt-3">{{ page.shipping_methods }}</h2>
|
||||
|
||||
<div data-target="cart-shipping.methods">
|
||||
{{ page.shipping_methods_help | markdownify }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
249
_packs/controllers/cart_base_controller.js
Normal file
249
_packs/controllers/cart_base_controller.js
Normal file
|
@ -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
|
||||
}
|
||||
}
|
39
_packs/controllers/cart_confirmation_controller.js
Normal file
39
_packs/controllers/cart_confirmation_controller.js
Normal file
|
@ -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
|
||||
}
|
||||
}
|
24
_packs/controllers/cart_contact_controller.js
Normal file
24
_packs/controllers/cart_contact_controller.js
Normal file
|
@ -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
|
||||
})
|
||||
}
|
||||
}
|
264
_packs/controllers/cart_controller.js
Normal file
264
_packs/controllers/cart_controller.js
Normal file
|
@ -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)
|
||||
}
|
||||
}
|
25
_packs/controllers/cart_counter_controller.js
Normal file
25
_packs/controllers/cart_counter_controller.js
Normal file
|
@ -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
|
||||
}
|
||||
}
|
89
_packs/controllers/cart_payment_methods_controller.js
Normal file
89
_packs/controllers/cart_payment_methods_controller.js
Normal file
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
42
_packs/controllers/cart_paypal_confirmation_controller.js
Normal file
42
_packs/controllers/cart_paypal_confirmation_controller.js
Normal file
|
@ -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('/')
|
||||
}
|
||||
}
|
111
_packs/controllers/cart_shipping_controller.js
Normal file
111
_packs/controllers/cart_shipping_controller.js
Normal file
|
@ -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))
|
||||
}
|
||||
}
|
101
_packs/controllers/country_controller.js
Normal file
101
_packs/controllers/country_controller.js
Normal file
|
@ -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
|
||||
}
|
||||
}
|
68
_packs/controllers/order_controller.js
Normal file
68
_packs/controllers/order_controller.js
Normal file
|
@ -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
|
||||
}
|
||||
}
|
38
_packs/controllers/postal_code_controller.js
Normal file
38
_packs/controllers/postal_code_controller.js
Normal file
|
@ -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'))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
91
_packs/controllers/state_controller.js
Normal file
91
_packs/controllers/state_controller.js
Normal file
|
@ -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
|
||||
}
|
||||
}
|
71
_packs/controllers/stock_controller.js
Normal file
71
_packs/controllers/stock_controller.js
Normal file
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
80
_packs/endpoints/sutty.js
Normal file
80
_packs/endpoints/sutty.js
Normal file
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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 => {
|
||||
|
|
3
env.js
3
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 }}'
|
||||
}
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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
|
||||
|
|
41
yarn.lock
41
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"
|
||||
|
|
Loading…
Reference in a new issue