Merge remote-tracking branch 'origin/master' into webpack-ci
Some checks failed
continuous-integration/woodpecker the build failed
Some checks failed
continuous-integration/woodpecker the build failed
This commit is contained in:
commit
d44c000e65
80 changed files with 4707 additions and 334 deletions
6
Makefile
6
Makefile
|
@ -49,6 +49,12 @@ npx: ## Correr comandos con npx (args="run")
|
||||||
npm: ## Correr comandos con npm (args="install -g paquete")
|
npm: ## Correr comandos con npm (args="install -g paquete")
|
||||||
$(MAKE) hain args="npm $(args)"
|
$(MAKE) hain args="npm $(args)"
|
||||||
|
|
||||||
|
prettier: ## Arreglar JS (por ahora)
|
||||||
|
$(MAKE) yarn args="prettier --write _packs/"
|
||||||
|
|
||||||
|
format-check: ## Verificar JS
|
||||||
|
$(MAKE) yarn args="prettier --check _packs/"
|
||||||
|
|
||||||
serve: /etc/hosts $(hain)/run/nginx/nginx.pid ## Servidor de desarrollo
|
serve: /etc/hosts $(hain)/run/nginx/nginx.pid ## Servidor de desarrollo
|
||||||
@echo "Iniciado servidor web en https://$(domain):4000/"
|
@echo "Iniciado servidor web en https://$(domain):4000/"
|
||||||
|
|
||||||
|
|
|
@ -11,6 +11,7 @@ plugins:
|
||||||
- jekyll-data
|
- jekyll-data
|
||||||
- jekyll-seo-tag
|
- jekyll-seo-tag
|
||||||
- jekyll-images
|
- jekyll-images
|
||||||
|
- jekyll-embed-urls
|
||||||
- sutty-liquid
|
- sutty-liquid
|
||||||
markdown: CommonMark
|
markdown: CommonMark
|
||||||
commonmark:
|
commonmark:
|
||||||
|
@ -58,6 +59,7 @@ locales:
|
||||||
- es
|
- es
|
||||||
ignored_layouts:
|
ignored_layouts:
|
||||||
- menu
|
- menu
|
||||||
|
- email
|
||||||
linked_fields:
|
linked_fields:
|
||||||
- post
|
- post
|
||||||
- item
|
- item
|
||||||
|
|
|
@ -63,8 +63,14 @@ time:
|
||||||
pm: pm
|
pm: pm
|
||||||
layouts:
|
layouts:
|
||||||
post: Article
|
post: Article
|
||||||
|
cart: Cart
|
||||||
|
confirmation: Order confirmation
|
||||||
|
payment: Payment
|
||||||
|
shipment: Shipment
|
||||||
|
email: Confirmation E-mail
|
||||||
menu: Menu
|
menu: Menu
|
||||||
about: About this site
|
about: About this site
|
||||||
|
theme: Customize theme
|
||||||
menu:
|
menu:
|
||||||
title: Menu
|
title: Menu
|
||||||
share:
|
share:
|
||||||
|
|
20
_data/es.yml
20
_data/es.yml
|
@ -1,5 +1,19 @@
|
||||||
---
|
---
|
||||||
locale: Castellano
|
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:
|
date:
|
||||||
format: '%d/%m/%Y'
|
format: '%d/%m/%Y'
|
||||||
abbr_day_names:
|
abbr_day_names:
|
||||||
|
@ -49,8 +63,14 @@ time:
|
||||||
pm: pm
|
pm: pm
|
||||||
layouts:
|
layouts:
|
||||||
post: Artículo
|
post: Artículo
|
||||||
|
cart: Carrito
|
||||||
|
confirmation: Confirmación de compra
|
||||||
|
payment: Pago
|
||||||
|
shipment: Envío
|
||||||
|
email: Correo de confirmación
|
||||||
menu: Menú
|
menu: Menú
|
||||||
about: Información del sitio
|
about: Información del sitio
|
||||||
|
theme: Personalizar plantilla
|
||||||
menu:
|
menu:
|
||||||
title: Menú
|
title: Menú
|
||||||
share:
|
share:
|
||||||
|
|
|
@ -1,40 +1,41 @@
|
||||||
---
|
---
|
||||||
pronouns:
|
pronouns:
|
||||||
type: "string"
|
type: 'string'
|
||||||
autocomplete: "sex"
|
autocomplete: 'sex'
|
||||||
label:
|
label:
|
||||||
es: "Pronombres"
|
es: 'Pronombres'
|
||||||
en: "Pronouns"
|
en: 'Pronouns'
|
||||||
placeholder:
|
placeholder:
|
||||||
es: "¿Qué pronombres usás?"
|
es: '¿Qué pronombres usás?'
|
||||||
en: "What are your pronouns?"
|
en: 'What are your pronouns?'
|
||||||
name:
|
name:
|
||||||
type: "string"
|
type: 'string'
|
||||||
autocomplete: "name"
|
autocomplete: 'name'
|
||||||
label:
|
label:
|
||||||
es: "Nombre"
|
es: 'Nombre'
|
||||||
en: "Name"
|
en: 'Name'
|
||||||
placeholder:
|
placeholder:
|
||||||
es: "Nombre, pseudónimo, alias"
|
es: 'Nombre, pseudónimo, alias'
|
||||||
en: "Name, pseudonym, alias"
|
en: 'Name, pseudonym, alias'
|
||||||
from:
|
from:
|
||||||
type: "email"
|
type: 'email'
|
||||||
autocomplete: "email"
|
autocomplete: 'email'
|
||||||
label:
|
label:
|
||||||
es: "Correo electrónico"
|
es: 'Correo electrónico'
|
||||||
en: "E-mail address"
|
en: 'E-mail address'
|
||||||
body:
|
body:
|
||||||
type: text
|
type: 'text'
|
||||||
label:
|
label:
|
||||||
es: Mensaje
|
es: 'Mensaje'
|
||||||
en: Message
|
en: 'Message'
|
||||||
consent:
|
consent:
|
||||||
type: boolean
|
type: 'boolean'
|
||||||
label:
|
label:
|
||||||
es: Acepto las políticas de privacidad
|
es: 'Acepto las políticas de privacidad'
|
||||||
en: I agree to the privacy policy
|
en: 'I agree to the privacy policy'
|
||||||
submit:
|
submit:
|
||||||
type: submit
|
type: 'submit'
|
||||||
label:
|
label:
|
||||||
es: Enviar
|
es: 'Enviar'
|
||||||
en: Send
|
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'
|
222
_data/layouts/cart.yml
Normal file
222
_data/layouts/cart.yml
Normal file
|
@ -0,0 +1,222 @@
|
||||||
|
---
|
||||||
|
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'
|
||||||
|
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
|
||||||
|
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: ''
|
|
@ -3,7 +3,7 @@ title:
|
||||||
type: 'string'
|
type: 'string'
|
||||||
required: true
|
required: true
|
||||||
label:
|
label:
|
||||||
en: ''
|
en: 'Item name'
|
||||||
es: 'Nombre del ítem'
|
es: 'Nombre del ítem'
|
||||||
help:
|
help:
|
||||||
en: ''
|
en: ''
|
||||||
|
@ -11,21 +11,29 @@ title:
|
||||||
post:
|
post:
|
||||||
type: 'belongs_to'
|
type: 'belongs_to'
|
||||||
label:
|
label:
|
||||||
en: ''
|
en: 'Link to this post'
|
||||||
es: 'Artículo'
|
es: 'Artículo'
|
||||||
help:
|
help:
|
||||||
en: ''
|
en: ''
|
||||||
es: 'Si el ítem lleva a un artículo fijo, asociarlo aquí'
|
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:
|
item:
|
||||||
type: 'belongs_to'
|
type: 'belongs_to'
|
||||||
inverse: items
|
inverse: items
|
||||||
filter:
|
filter:
|
||||||
layout: menu
|
layout: menu
|
||||||
label:
|
label:
|
||||||
en: ''
|
en: 'Main item'
|
||||||
es: 'Ítem anterior'
|
es: 'Ítem anterior'
|
||||||
help:
|
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í'
|
es: 'Si es un sub ítem, asociar el ítem superior aquí'
|
||||||
items:
|
items:
|
||||||
type: 'has_many'
|
type: 'has_many'
|
||||||
|
@ -33,19 +41,11 @@ items:
|
||||||
filter:
|
filter:
|
||||||
layout: menu
|
layout: menu
|
||||||
label:
|
label:
|
||||||
en: ''
|
en: 'Sub items'
|
||||||
es: 'Sub ítemes'
|
es: 'Sub ítemes'
|
||||||
help:
|
help:
|
||||||
en: ''
|
en: "If you're building a dropdown menu, add the sub items here"
|
||||||
es: 'Si el ítem tiene sub ítems, asociarlos aquí'
|
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:
|
categories:
|
||||||
type: 'array'
|
type: 'array'
|
||||||
label:
|
label:
|
||||||
|
|
126
_data/layouts/payment.yml
Normal file
126
_data/layouts/payment.yml
Normal file
|
@ -0,0 +1,126 @@
|
||||||
|
---
|
||||||
|
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.'
|
||||||
|
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
|
||||||
|
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 la unidad de medida que configuraste en el carrito'
|
||||||
|
en: 'In measurement units configured in cart'
|
||||||
|
height:
|
||||||
|
type: 'number'
|
||||||
|
writable: 'once'
|
||||||
|
label:
|
||||||
|
es: 'Alto'
|
||||||
|
en: 'Height'
|
||||||
|
help:
|
||||||
|
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'
|
||||||
|
en: 'Depth'
|
||||||
|
help:
|
||||||
|
es: 'En la unidad de medida que configuraste en el carrito'
|
||||||
|
en: 'In measurement units configured in cart'
|
||||||
|
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'
|
255
_data/layouts/theme.yml
Normal file
255
_data/layouts/theme.yml
Normal file
|
@ -0,0 +1,255 @@
|
||||||
|
---
|
||||||
|
title:
|
||||||
|
type: string
|
||||||
|
required: true
|
||||||
|
label:
|
||||||
|
en: Title
|
||||||
|
es: Título
|
||||||
|
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:
|
||||||
|
en: 'Rounded corners'
|
||||||
|
es: 'Esquinas redondeadas'
|
||||||
|
help:
|
||||||
|
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: 'Shadows'
|
||||||
|
es: 'Sombras'
|
||||||
|
help:
|
||||||
|
en: 'Shadows behind elements'
|
||||||
|
es: 'Sombras en los elementos'
|
||||||
|
default:
|
||||||
|
es: true
|
||||||
|
en: true
|
||||||
|
body_bg:
|
||||||
|
type: color
|
||||||
|
label:
|
||||||
|
en: 'Background color'
|
||||||
|
es: 'Color de fondo'
|
||||||
|
help:
|
||||||
|
en: "Site's background color"
|
||||||
|
es: 'Color de fondo del sitio'
|
||||||
|
default:
|
||||||
|
es: '#FFFFFF'
|
||||||
|
en: '#FFFFFF'
|
||||||
|
body_color:
|
||||||
|
type: color
|
||||||
|
label:
|
||||||
|
en: 'Text color'
|
||||||
|
es: 'Color del texto'
|
||||||
|
help:
|
||||||
|
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: 'Link color'
|
||||||
|
es: 'Color de los vínculos'
|
||||||
|
help:
|
||||||
|
en: ''
|
||||||
|
es: ''
|
||||||
|
default:
|
||||||
|
es: '#007bff'
|
||||||
|
en: '#007bff'
|
||||||
|
link_hover_color:
|
||||||
|
type: color
|
||||||
|
label:
|
||||||
|
en: 'Link color when selected'
|
||||||
|
es: 'Color de los vínculos al seleccionarlos'
|
||||||
|
help:
|
||||||
|
en: ''
|
||||||
|
es: ''
|
||||||
|
default:
|
||||||
|
es: '#0056b3'
|
||||||
|
en: '#0056b3'
|
||||||
|
h1_font_size:
|
||||||
|
type: float
|
||||||
|
unit: rem
|
||||||
|
label:
|
||||||
|
en: 'Height for first level headings'
|
||||||
|
es: 'Altura de los títulos de primer nivel'
|
||||||
|
help:
|
||||||
|
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: float
|
||||||
|
unit: rem
|
||||||
|
label:
|
||||||
|
en: 'Height for second level headings'
|
||||||
|
es: 'Altura de los títulos de segundo nivel'
|
||||||
|
help:
|
||||||
|
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: float
|
||||||
|
unit: rem
|
||||||
|
label:
|
||||||
|
en: 'Height for third level headings'
|
||||||
|
es: 'Altura de los títulos de tercer nivel'
|
||||||
|
help:
|
||||||
|
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: float
|
||||||
|
unit: rem
|
||||||
|
label:
|
||||||
|
en: 'Height for fourth level headings'
|
||||||
|
es: 'Altura de los títulos de cuarto nivel'
|
||||||
|
help:
|
||||||
|
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: float
|
||||||
|
unit: rem
|
||||||
|
label:
|
||||||
|
en: 'Height for fifth level headings'
|
||||||
|
es: 'Altura de los títulos de quinto nivel'
|
||||||
|
help:
|
||||||
|
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: float
|
||||||
|
unit: rem
|
||||||
|
label:
|
||||||
|
en: 'Height for sixth level headings'
|
||||||
|
es: 'Altura de los títulos de sexto nivel'
|
||||||
|
help:
|
||||||
|
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: 'Highlight color'
|
||||||
|
es: 'Color de resaltado'
|
||||||
|
help:
|
||||||
|
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: 'Navigation bar item color'
|
||||||
|
es: 'Color de ítem en la barra de navegación'
|
||||||
|
help:
|
||||||
|
en: 'Text and icons'
|
||||||
|
es: 'Texto e íconos'
|
||||||
|
default:
|
||||||
|
es: '#ced4da'
|
||||||
|
en: '#ced4da'
|
||||||
|
navbar_light_hover_color:
|
||||||
|
type: color
|
||||||
|
label:
|
||||||
|
en: 'Navigation bar item color when selected'
|
||||||
|
es: 'Color de ítem seleccionado en la barra de navegación'
|
||||||
|
help:
|
||||||
|
en: 'Text and icons'
|
||||||
|
es: 'Texto e íconos'
|
||||||
|
default:
|
||||||
|
es: '#6c757d'
|
||||||
|
en: '#6c757d'
|
||||||
|
navbar_light_active_color:
|
||||||
|
type: color
|
||||||
|
label:
|
||||||
|
en: 'Navigation bar item color when active'
|
||||||
|
es: 'Color de ítem activo en la barra de navegación'
|
||||||
|
help:
|
||||||
|
en: 'Text and icons'
|
||||||
|
es: 'Texto e íconos'
|
||||||
|
default:
|
||||||
|
es: '#212529'
|
||||||
|
en: '#212529'
|
||||||
|
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
|
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>
|
11
_includes/cart_controller.html
Normal file
11
_includes/cart_controller.html
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
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 }}"
|
||||||
|
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>
|
125
_includes/donacion.html
Normal file
125
_includes/donacion.html
Normal file
|
@ -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 -%}
|
||||||
|
|
||||||
|
<input data-controller="open" data-action="donacion" type="checkbox" id="donacion" class="d_toggler sr-only">
|
||||||
|
<label class="btn-block" for="donacion">
|
||||||
|
<span class="btn {{ include.class }} btn-block">
|
||||||
|
{{ include.boton | default: donacion.boton }}
|
||||||
|
|
||||||
|
{% if include.icon %}
|
||||||
|
<i class="{{ include.icon }}"></i>
|
||||||
|
{% endif %}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<div class="telon d_share-box"></div>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<form
|
||||||
|
data-pay-what-you-can-target="form"
|
||||||
|
{% include_cached pay_what_you_can_controller.html product=product url=include.page.pdf.path %}
|
||||||
|
style="z-index: 1011"
|
||||||
|
class="d_share-box pointer-event-none d_toggled d-flex align-items-center justify-content-center"
|
||||||
|
itemscope
|
||||||
|
itemtype="https://schema.org/Offer">
|
||||||
|
<meta itemprop="sku" content="{{ product.sku }}" />
|
||||||
|
<meta itemprop="availableDeliveryMethod" content="http://purl.org/goodrelations/v1#DirectDownload" />
|
||||||
|
<meta itemprop="acceptedPaymentMethod" content="http://purl.org/goodrelations/v1#PayPal" />
|
||||||
|
|
||||||
|
<div class="box inside background-white pointer-event-auto row no-gutters align-items-center justify-content-center p-5">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-9 black font-weight-light lead text-md-justify hyphens-md e-content">
|
||||||
|
{{ donacion.content | markdownify }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-3 d-flex justify-content-center">
|
||||||
|
{% if include.page.image.path %}
|
||||||
|
<picture>
|
||||||
|
<source srcset="{{ include.page.image.path | thumbnail: nil, 370 }}" media="(max-width: 767px)" />
|
||||||
|
<img class="cover shadow w-200px" src="{{ include.page.image.path | thumbnail: nil, 300 }}" loading="lazy" />
|
||||||
|
</picture>
|
||||||
|
{% else %}
|
||||||
|
{% include_cached logo_horizontal.svg %}
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-3 mb-3 mb-md-0">
|
||||||
|
<h2>{{ site.i18n.donacion.monto }}</h2>
|
||||||
|
|
||||||
|
<label class="sr-only" for="price">{{ site.i18n.donacion.monto }}</label>
|
||||||
|
<input id="price" name="price" type="number" class="form-control" min="1" value="1" data-name="price" data-action="pay-what-you-can#store" required/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-3 mb-3 mb-md-0">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-6">
|
||||||
|
<input id="currency-ars" type="radio" class="sr-only" name="currency" value="ARS" data-name="currency" data-action="pay-what-you-can#store" required/>
|
||||||
|
<label for="currency-ars" itemprop="priceCurrency" class="d-flex flex-column justify-content-center">
|
||||||
|
<span class="btn border mb-3">ARS</span>
|
||||||
|
|
||||||
|
{% include mp-logo.svg %}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-6">
|
||||||
|
<input id="currency-usd" type="radio" class="sr-only" name="currency" value="USD" data-name="currency" data-action="pay-what-you-can#store" required/>
|
||||||
|
<label for="currency-usd" itemprop="priceCurrency" class="d-flex flex-column justify-content-center">
|
||||||
|
<span class="btn border mb-3">USD</span>
|
||||||
|
|
||||||
|
{% include pp-logo.svg %}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-12 text-center">
|
||||||
|
{% comment %}
|
||||||
|
Este input invisible es para que Bootstrap pueda mostrar el
|
||||||
|
error si querés mandar el formulario sin elegir una moneda.
|
||||||
|
{% endcomment %}
|
||||||
|
<input type="radio" class="d-none" name="currency" value="" />
|
||||||
|
<div class="invalid-feedback">
|
||||||
|
{{ site.i18n.donacion.currency }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p data-controller="cart-paypal-confirmation"></p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-3">
|
||||||
|
<button class="btn btn-primary btn-block mb-2" data-action="pay-what-you-can#pay">
|
||||||
|
<span class="hide-when-disabled">{{ donacion.boton }}</span>
|
||||||
|
<span class="show-when-disabled">
|
||||||
|
<span class="sr-only">{{ site.i18n.donacion.espera }}</span>
|
||||||
|
<i class="fa fa-fw fa-spinner fa-spin"></i>
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{% if include.page.pdf.path %}
|
||||||
|
<div class="d-flex align-items-center justify-content-between">
|
||||||
|
<a
|
||||||
|
download
|
||||||
|
href="{{ include.page.pdf.path | uri_escape }}"
|
||||||
|
class="btn btn-transparent border black btn-block">
|
||||||
|
{{ site.i18n.libro.download }}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-3"></div>
|
||||||
|
|
||||||
|
<div class="col-12">
|
||||||
|
{{ site.i18n.donacion.medios | markdownify | replace: '<p>', '<p class="mb-0 mt-3"><small>' | replace: '</p>', '</small></p>' }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
|
@ -1,6 +1,6 @@
|
||||||
<div
|
<div
|
||||||
data-controller="floating-alert"
|
data-controller="floating-alert"
|
||||||
class="floating-alert floating-alert-bottom floating-alert-default"
|
class="floating-alert floating-alert-bottom floating-alert-default hide"
|
||||||
role="status" aria-live="polite" aria-atomic="true">
|
role="status" aria-live="polite" aria-atomic="true">
|
||||||
<div data-target="toast.content" class="floating-alert-content white background-black-t4"></div>
|
<div data-target="floating-alert.content" class="floating-alert-content white background-black-t4"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,7 +1,14 @@
|
||||||
{%- assign name = include.field[0] -%}
|
{% if include.form %}
|
||||||
{%- assign id = include.field[1].id | default: name -%}
|
{%- 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 label = include.field[1].label[site.locale] -%}
|
||||||
{%- assign help = include.field[1].help[site.locale] -%}
|
{%- assign help = include.field[1].help[site.locale] -%}
|
||||||
|
{%- assign error = include.field[1].error[site.locale] -%}
|
||||||
{%- assign autocomplete = include.field[1].autocomplete -%}
|
{%- assign autocomplete = include.field[1].autocomplete -%}
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
|
@ -11,6 +18,7 @@
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
<input
|
<input
|
||||||
|
{{ include.field[1].extra.input }}
|
||||||
{% if help %}
|
{% if help %}
|
||||||
aria-describedby="help-{{ id }}"
|
aria-describedby="help-{{ id }}"
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
@ -25,6 +33,10 @@
|
||||||
{% endif %}
|
{% endif %}
|
||||||
class="form-control" />
|
class="form-control" />
|
||||||
|
|
||||||
|
{%- if error -%}
|
||||||
|
<div class="invalid-feedback">{{ error }}</div>
|
||||||
|
{%- endif -%}
|
||||||
|
|
||||||
{%- if help -%}
|
{%- if help -%}
|
||||||
<small id="help-{{ id }}" class="form-text">
|
<small id="help-{{ id }}" class="form-text">
|
||||||
{{ help }}
|
{{ help }}
|
||||||
|
|
5
_includes/notification.html
Normal file
5
_includes/notification.html
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
<div
|
||||||
|
data-controller="notification"
|
||||||
|
data-notification-templates="assets/templates/"
|
||||||
|
class="notification fixed-top w-100 fade collapse hide">
|
||||||
|
</div>
|
13
_includes/pay_what_you_can_controller.html
Normal file
13
_includes/pay_what_you_can_controller.html
Normal file
|
@ -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: '|' }}"
|
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>
|
38
_includes/product.html
Normal file
38
_includes/product.html
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
<article
|
||||||
|
{% if include.product.in_stock %}
|
||||||
|
{% include_cached cart_controller.html product=include.product %}
|
||||||
|
{% endif %}
|
||||||
|
class="d-flex flex-column align-items-center flex-grow-1 {{ include.class }}">
|
||||||
|
<div class="d-flex flex-column align-items-center justify-content-between flex-grow-1 position-relative">
|
||||||
|
<a href="{{ include.product.url }}">
|
||||||
|
<picture>
|
||||||
|
<source srcset="{{ include.product.image.path | thumbnail: nil, 370 }}" media="(max-width: 767px)" />
|
||||||
|
|
||||||
|
<img class="shadow" src="{{ include.product.image.path | thumbnail: nil, 300 }}" loading="lazy" />
|
||||||
|
</picture>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<div class="">
|
||||||
|
<div class="flex-fill mt-4 mb-4 black">
|
||||||
|
<p class="font-weight-bold m-0 mb-3">
|
||||||
|
<a href="{{ include.product.url }}">
|
||||||
|
{{ include.product.title }}
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p class="font-weight-bold mb-0">
|
||||||
|
<span data-stock-price>{{ include.product.price }}</span>
|
||||||
|
<span data-stock-currency>{{ site.cart.currency }}</span>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<button
|
||||||
|
class="btn btn-lp text-lowercase p-1 font-weight-bold"
|
||||||
|
{% unless include.product.in_stock %}disabled{% endunless %}
|
||||||
|
{% include cart_add.html %}>
|
||||||
|
<span class="hide-when-disabled">{{ site.cart.add }}</span>
|
||||||
|
<span class="show-when-disabled">{{ site.cart.out_of_stock | default: site.data.layouts.cart.out_of_stock.default[site.locale] }}</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</article>
|
52
_includes/state.html
Normal file
52
_includes/state.html
Normal file
|
@ -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 -%}
|
||||||
|
|
||||||
|
<div class="form-group" data-controller="state" data-state-group="{{ include.field[1].group }}">
|
||||||
|
<label for="{{ id }}">
|
||||||
|
{{ label }}
|
||||||
|
{% if include.field[1].required %}*{% endif %}
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<input data-target="state.id" type="hidden" name="{{ name }}" id="{{ id }}" value="" />
|
||||||
|
|
||||||
|
<input
|
||||||
|
data-target="state.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="state.list">
|
||||||
|
</datalist>
|
||||||
|
</div>
|
|
@ -1,5 +1,5 @@
|
||||||
<input
|
<input
|
||||||
data-target="contact.submit"
|
{{ include.field[1].extra }}
|
||||||
type="submit"
|
type="submit"
|
||||||
class="btn btn-success"
|
class="btn btn-success"
|
||||||
value="{{ include.field[1].label[site.locale] }}" />
|
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>
|
23
_layouts/confirmation.html
Normal file
23
_layouts/confirmation.html
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
---
|
||||||
|
layout: default
|
||||||
|
---
|
||||||
|
|
||||||
|
<section
|
||||||
|
data-controller="cart-confirmation"
|
||||||
|
data-template="confirmation"
|
||||||
|
data-clear="true">
|
||||||
|
|
||||||
|
<header>
|
||||||
|
<h1>{{ page.title }}</h1>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div class="content">
|
||||||
|
{{ content }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div data-target="cart-confirmation.order"></div>
|
||||||
|
|
||||||
|
<p data-controller="cart-paypal-confirmation"></p>
|
||||||
|
|
||||||
|
<a href="?" class="btn btn-primary text-uppercase">{{ page.back }}</a>
|
||||||
|
</section>
|
|
@ -24,7 +24,7 @@
|
||||||
si no existe.
|
si no existe.
|
||||||
{% endcomment %}
|
{% endcomment %}
|
||||||
<link rel="preload" as="script" type="text/javascript" href="env.js" />
|
<link rel="preload" as="script" type="text/javascript" href="env.js" />
|
||||||
<script type="text/javascript" src="env.js"></script>
|
<script type="text/javascript" src="env.js?{{ site.time | date: '%s' }}"></script>
|
||||||
{% include_cached pack.html %}
|
{% include_cached pack.html %}
|
||||||
|
|
||||||
{% comment %}
|
{% comment %}
|
||||||
|
@ -46,12 +46,17 @@
|
||||||
{% feed_meta %}
|
{% feed_meta %}
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
<div data-turbo-permanent>
|
||||||
|
{%- include_cached notification.html -%}
|
||||||
|
</div>
|
||||||
|
|
||||||
{%- include_cached menu.html active_cache_key=page.layout %}
|
{%- include_cached menu.html active_cache_key=page.layout %}
|
||||||
|
|
||||||
<main data-controller="scroll">
|
<main data-controller="scroll stock" data-per-page="100">
|
||||||
{{ content }}
|
{{ content }}
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
{% include_cached footer.html %}
|
{% include_cached footer.html %}
|
||||||
|
{% include_cached floating_alert.html %}
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
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="payment_methods">
|
||||||
|
</div>
|
||||||
|
</section>
|
110
_layouts/product.html
Normal file
110
_layouts/product.html
Normal file
|
@ -0,0 +1,110 @@
|
||||||
|
---
|
||||||
|
layout: default
|
||||||
|
---
|
||||||
|
|
||||||
|
<article
|
||||||
|
itemscope itemtype="http://schema.org/Product"
|
||||||
|
class="w-100 pb-5 h-entry">
|
||||||
|
<div class="m-auto row no-gutters justify-content-center">
|
||||||
|
<div class="col-10 col-lg-6">
|
||||||
|
<picture>
|
||||||
|
<img
|
||||||
|
itemprop="image"
|
||||||
|
class="img-fluid"
|
||||||
|
src="{{ page.image.path | thumbnail: 316 }}"
|
||||||
|
alt="{{ page.image.description | default: page.title }}" />
|
||||||
|
</picture>
|
||||||
|
|
||||||
|
<div class="w-lg-50">
|
||||||
|
<h4 class="sr-only">{{ site.i18n.libro.ficha }}</h4>
|
||||||
|
|
||||||
|
<ul class="border-bottom border-tl list-unstyled pt-5 gray-600">
|
||||||
|
<li>
|
||||||
|
<time
|
||||||
|
class="dt-published"
|
||||||
|
itemprop="datePublished"
|
||||||
|
datetime="{{ page.date | date_to_xmlschema }}">{{ page.date | date_local: '%B %Y' }}</time>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
{% if page.width > 0 %}
|
||||||
|
<li itemprop="width" itemscope="https://schema.org/QuantitativeValue">
|
||||||
|
<span class="font-weight-bold">{{ site.data.layouts.product.width.label[site.lang] }}</span>
|
||||||
|
<span itemprop="value">{{ page.width }}</span>
|
||||||
|
<span itemprop="unitText">{{ site.cart.length_unit | default: site.data.layouts.cart.length_unit.default[site.lang] }}</span>
|
||||||
|
</li>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if page.height > 0 %}
|
||||||
|
<li itemprop="height" itemscope="https://schema.org/QuantitativeValue">
|
||||||
|
<span class="font-weight-bold">{{ site.data.layouts.product.height.label[site.lang] }}</span>
|
||||||
|
<span itemprop="value">{{ page.height }}</span>
|
||||||
|
<span itemprop="unitText">{{ site.cart.length_unit | default: site.data.layouts.cart.length_unit.default[site.lang] }}</span>
|
||||||
|
</li>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if page.depth > 0 %}
|
||||||
|
<li itemprop="depth" itemscope="https://schema.org/QuantitativeValue">
|
||||||
|
<span class="font-weight-bold">{{ site.data.layouts.product.depth.label[site.lang] }}</span>
|
||||||
|
<span itemprop="value">{{ page.depth }}</span>
|
||||||
|
<span itemprop="unitText">{{ site.cart.length_unit | default: site.data.layouts.cart.length_unit.default[site.lang] }}</span>
|
||||||
|
</li>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if page.weight > 0 %}
|
||||||
|
<li itemprop="weight" itemscope="https://schema.org/QuantitativeValue">
|
||||||
|
<span class="font-weight-bold">{{ site.data.layouts.product.weight.label[site.lang] }}</span>
|
||||||
|
<span itemprop="value">{{ page.weight }}</span>
|
||||||
|
<span itemprop="unitText">{{ site.cart.weight_unit | default: site.data.layouts.cart.weight_unit.default[site.lang] }}</span>
|
||||||
|
</li>
|
||||||
|
{% endif %}
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<div itemprop="offers">
|
||||||
|
<div
|
||||||
|
class="d-flex align-items-center justify-content-between mb-3 ml-1"
|
||||||
|
{% include_cached cart_controller.html product=page %}
|
||||||
|
itemscope itemtype="https://schema.org/Offer">
|
||||||
|
|
||||||
|
<meta itemprop="sku" content="{{ page.sku }}" />
|
||||||
|
<meta itemprop="acceptedPaymentMethod" content="http://purl.org/goodrelations/v1#PayPal" />
|
||||||
|
<meta itemprop="availableDeliveryMethod" content="http://purl.org/goodrelations/v1#Mail" />
|
||||||
|
|
||||||
|
{% if page.in_stock %}
|
||||||
|
<meta itemprop="availability" content="https://schema.org/InStock" />
|
||||||
|
{% else %}
|
||||||
|
<meta itemprop="availability" content="https://schema.org/OutOfStock" />
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<p>
|
||||||
|
<span itemprop="price" data-stock-price>{{ page.price | floor }}</span>
|
||||||
|
<span itemprop="priceCurrency" data-stock-currency>{{ site.cart.currency }}</span>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<button
|
||||||
|
data-action="cart#add"
|
||||||
|
{{ page.in_stock | value_if: 'disabled' }}
|
||||||
|
class="btn btn-primary btn-block">
|
||||||
|
<span class="hide-when-disabled">{{ site.cart.add }}</span>
|
||||||
|
<span class="show-when-disabled">{{ site.cart.out_of_stock | default: site.data.layouts.cart.out_of_stock.default[site.locale] }}</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-10 col-lg-6 black mt-5 mt-lg-0">
|
||||||
|
<header>
|
||||||
|
<h1 class="p-name" itemprop="name headline">{{ page.title }}</h1>
|
||||||
|
<p class="lead p-summary" itemprop="description">{{ page.description }}</p>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div class="font-weight-light gray-600 lead text-md-justify hyphens-md e-content" itemprop="abstract">
|
||||||
|
{{ content }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<span hidden itemprop="identifier">{{ page.uuid }}</span>
|
||||||
|
<a hidden class="u-url" itemprop="url" href="{{ page.url | absolute_url }}">{{ page.url | absolute_url }}</a>
|
||||||
|
<meta itemprop="keywords" content="{{ page.categories | join: ',' }}{% unless page.categories == empty %},{% endunless %}{{ page.tags | join: ',' }}"/>
|
||||||
|
</article>
|
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>
|
270
_packs/controllers/cart_base_controller.js
Normal file
270
_packs/controllers/cart_base_controller.js
Normal file
|
@ -0,0 +1,270 @@
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* 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";
|
||||||
|
let notify = true;
|
||||||
|
|
||||||
|
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":
|
||||||
|
// 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;
|
||||||
|
} else {
|
||||||
|
data.content = response.fail().summary;
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
content.push(
|
||||||
|
`${site.i18n.errors?.fields[field]}: ${fail.errors[field].join(
|
||||||
|
", "
|
||||||
|
)}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
data.content = content.join(". ");
|
||||||
|
notify = false;
|
||||||
|
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
data.content = fail.message;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (notify) console.error(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;
|
||||||
|
}
|
||||||
|
}
|
47
_packs/controllers/cart_confirmation_controller.js
Normal file
47
_packs/controllers/cart_confirmation_controller.js
Normal file
|
@ -0,0 +1,47 @@
|
||||||
|
import { CartBaseController } from "./cart_base_controller";
|
||||||
|
|
||||||
|
export default class extends CartBaseController {
|
||||||
|
static targets = ["order"];
|
||||||
|
|
||||||
|
async connect() {
|
||||||
|
if (this.clear) {
|
||||||
|
this.storage.clear();
|
||||||
|
window.dispatchEvent(
|
||||||
|
new CustomEvent("cart:counter", { detail: { item_count: 0 } })
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
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 data = { order, products, site, shipping_address };
|
||||||
|
|
||||||
|
this.storage.setItem("confirmation", JSON.stringify(data));
|
||||||
|
} else {
|
||||||
|
data = JSON.parse(this.storage.getItem("confirmation"));
|
||||||
|
}
|
||||||
|
|
||||||
|
this.render(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
render(data = {}) {
|
||||||
|
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];
|
||||||
|
}
|
||||||
|
}
|
32
_packs/controllers/cart_contact_controller.js
Normal file
32
_packs/controllers/cart_contact_controller.js
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
import { CartBaseController } from "./cart_base_controller";
|
||||||
|
|
||||||
|
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", this.focusout_event);
|
||||||
|
}
|
||||||
|
|
||||||
|
disconnect() {
|
||||||
|
this.formTarget.removeEventListener("focusout", this.focusout_event);
|
||||||
|
}
|
||||||
|
|
||||||
|
_focusout_event(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;
|
||||||
|
}
|
||||||
|
}
|
317
_packs/controllers/cart_controller.js
Normal file
317
_packs/controllers/cart_controller.js
Normal file
|
@ -0,0 +1,317 @@
|
||||||
|
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;
|
||||||
|
|
||||||
|
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", this.change_event);
|
||||||
|
}
|
||||||
|
|
||||||
|
disconnect() {
|
||||||
|
this.quantityTarget.removeEventListener("change", this.change_event);
|
||||||
|
}
|
||||||
|
|
||||||
|
async _change_event(event) {
|
||||||
|
const quantity = event.target.value;
|
||||||
|
|
||||||
|
if (quantity < 1) return;
|
||||||
|
|
||||||
|
const orderToken = await this.tokenGetOrCreate();
|
||||||
|
const product = this.product;
|
||||||
|
|
||||||
|
if (!product) return;
|
||||||
|
|
||||||
|
event.target.disabled = true;
|
||||||
|
|
||||||
|
const response = await this.spree.cart.setQuantity(
|
||||||
|
{ orderToken },
|
||||||
|
{
|
||||||
|
line_item_id: product.line_item.id,
|
||||||
|
quantity,
|
||||||
|
include: "line_items",
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
event.target.disabled = false;
|
||||||
|
event.target.focus();
|
||||||
|
|
||||||
|
// If we're failing here it could be due to a missing order, so we
|
||||||
|
// ask the user to decide what they want to do about it
|
||||||
|
if (response.isFail()) {
|
||||||
|
this.handleFailure(response);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.cart = response;
|
||||||
|
this.subtotalUpdate();
|
||||||
|
this.counterUpdate();
|
||||||
|
await this.itemStore();
|
||||||
|
|
||||||
|
if (!this.hasSubtotalTarget) return;
|
||||||
|
|
||||||
|
this.subtotalTarget.innerText =
|
||||||
|
product.line_item.attributes.discounted_amount;
|
||||||
|
}
|
||||||
|
|
||||||
|
subtotalUpdate() {
|
||||||
|
window.dispatchEvent(new Event("cart:subtotal:update"));
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Creates an order and stores the data into localStorage.
|
||||||
|
*
|
||||||
|
* @return [String]
|
||||||
|
*/
|
||||||
|
async cartCreate() {
|
||||||
|
const response = await this.spree.cart.create();
|
||||||
|
|
||||||
|
// If we fail here it's probably a server error, so we inform the
|
||||||
|
// user.
|
||||||
|
if (response.isFail()) {
|
||||||
|
this.handleFailure(response);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.cart = response;
|
||||||
|
this.storage.setItem("token", response.success().data.attributes.token);
|
||||||
|
|
||||||
|
return this.token;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Gets the order token and creates a cart if it doesn't exist.
|
||||||
|
*
|
||||||
|
* @return [String]
|
||||||
|
*/
|
||||||
|
async tokenGetOrCreate() {
|
||||||
|
let token = this.storage.getItem("token");
|
||||||
|
|
||||||
|
return token || (await this.cartCreate());
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* The variant ID is used to identify products
|
||||||
|
*
|
||||||
|
* @return [String]
|
||||||
|
*/
|
||||||
|
get variantId() {
|
||||||
|
return this.data.get("variantId");
|
||||||
|
}
|
||||||
|
|
||||||
|
get product() {
|
||||||
|
const product = JSON.parse(this.storage.getItem(this.storageId));
|
||||||
|
|
||||||
|
if (!product) {
|
||||||
|
console.error(
|
||||||
|
"El producto es nulo!",
|
||||||
|
this.storageId,
|
||||||
|
this.storage.length,
|
||||||
|
this.cart
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return product;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Obtains the line_item_id by a variant_id by inspecting the cart and
|
||||||
|
* its included items
|
||||||
|
*
|
||||||
|
* @return [Object]
|
||||||
|
*/
|
||||||
|
findLineItem() {
|
||||||
|
const line_item = this.cart.included.find(
|
||||||
|
(x) =>
|
||||||
|
x.type === "line_item" &&
|
||||||
|
x.relationships.variant.data.id == this.variantId
|
||||||
|
);
|
||||||
|
|
||||||
|
return line_item || {};
|
||||||
|
}
|
||||||
|
|
||||||
|
get storageId() {
|
||||||
|
return `cart:item:${this.variantId}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Stores an item for later usage.
|
||||||
|
*
|
||||||
|
* @see {./order_controller.js}
|
||||||
|
*/
|
||||||
|
itemStore() {
|
||||||
|
this.storage.setItem(
|
||||||
|
this.storageId,
|
||||||
|
JSON.stringify({
|
||||||
|
variant_id: this.variantId,
|
||||||
|
line_item: this.findLineItem(),
|
||||||
|
image: this.data.get("image"),
|
||||||
|
title: this.data.get("title"),
|
||||||
|
url: this.data.get("url"),
|
||||||
|
stock: this.data.get("stock"),
|
||||||
|
in_stock: this.data.get("inStock"),
|
||||||
|
extra: this.data.get("extra") ? this.data.get("extra").split("|") : [],
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Adds item to cart. This is meant to be used by an "Add to cart"
|
||||||
|
* button. If the item already exists in the cart it updates the
|
||||||
|
* quantity by +1.
|
||||||
|
*
|
||||||
|
* The item needs a variant ID to be added.
|
||||||
|
*/
|
||||||
|
async add(event, quantity = 1, floating_alert = true) {
|
||||||
|
const addedQuantity = this.addedQuantity();
|
||||||
|
if (addedQuantity > 1) quantity = addedQuantity;
|
||||||
|
|
||||||
|
const orderToken = await this.tokenGetOrCreate();
|
||||||
|
const response = await this.spree.cart.addItem(
|
||||||
|
{ orderToken },
|
||||||
|
{ variant_id: this.variantId, quantity, include: "line_items" }
|
||||||
|
);
|
||||||
|
|
||||||
|
if (response.isFail()) {
|
||||||
|
this.handleFailure(response);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.cart = response;
|
||||||
|
this.itemStore();
|
||||||
|
this.counterUpdate();
|
||||||
|
this.fireCajon();
|
||||||
|
|
||||||
|
if (floating_alert) {
|
||||||
|
const site = window.site;
|
||||||
|
const content = site.cart.added;
|
||||||
|
window.dispatchEvent(
|
||||||
|
new CustomEvent("floating:alert", { detail: { content } })
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Remove the element from the cart. It contacts the API and if the
|
||||||
|
* item is removed, it removes itself from the page and the storage.
|
||||||
|
*/
|
||||||
|
async remove() {
|
||||||
|
const product = this.product;
|
||||||
|
|
||||||
|
if (!product) return;
|
||||||
|
if (!product.line_item) return;
|
||||||
|
|
||||||
|
const orderToken = this.token;
|
||||||
|
const response = await this.spree.cart.removeItem(
|
||||||
|
{ orderToken },
|
||||||
|
product.line_item.id,
|
||||||
|
{ include: "line_items" }
|
||||||
|
);
|
||||||
|
|
||||||
|
if (response.isFail()) {
|
||||||
|
this.handleFailure(response);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.cart = response;
|
||||||
|
this.storage.removeItem(this.storageId);
|
||||||
|
this.element.remove();
|
||||||
|
this.subtotalUpdate();
|
||||||
|
this.counterUpdate();
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Shows variants
|
||||||
|
*/
|
||||||
|
async variants() {
|
||||||
|
const template = "variants";
|
||||||
|
const data = {
|
||||||
|
product: {
|
||||||
|
variant_id: this.data.get("variantId"),
|
||||||
|
digital_variant_id: this.data.get("digitalVariantId"),
|
||||||
|
image: this.data.get("image"),
|
||||||
|
title: this.data.get("title"),
|
||||||
|
price: this.data.get("price"),
|
||||||
|
digital_price: this.data.get("digitalPrice"),
|
||||||
|
in_stock: this.data.get("inStock"),
|
||||||
|
extra: this.data.get("extra").split("|"),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
window.dispatchEvent(
|
||||||
|
new CustomEvent("notification", { detail: { template, data } })
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Recovers the order if something failed
|
||||||
|
*/
|
||||||
|
async recover() {
|
||||||
|
console.error("Recuperando pedido", this.token);
|
||||||
|
|
||||||
|
// Removes the failing token
|
||||||
|
this.storage.removeItem("token");
|
||||||
|
|
||||||
|
// Get a new token and cart
|
||||||
|
await this.tokenGetOrCreate();
|
||||||
|
|
||||||
|
// Stores the previous cart
|
||||||
|
const cart = this.cart;
|
||||||
|
|
||||||
|
if (!cart) return;
|
||||||
|
|
||||||
|
// Add previous items and their quantities to the new cart by
|
||||||
|
// mimicking user's actions
|
||||||
|
//
|
||||||
|
// XXX: We don't use forEach because it's not async
|
||||||
|
for (const variant of cart.data.relationships.variants.data) {
|
||||||
|
this.data.set("variantId", variant.id);
|
||||||
|
|
||||||
|
const product = this.product;
|
||||||
|
|
||||||
|
if (!product) continue;
|
||||||
|
|
||||||
|
this.data.set("image", product.image);
|
||||||
|
this.data.set("title", product.title);
|
||||||
|
this.data.set("extra", product.extra.join("|"));
|
||||||
|
|
||||||
|
await this.add(null, product.line_item.attributes.quantity, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Si le compradore aumenta la cantidad antes de agregar
|
||||||
|
*/
|
||||||
|
addedQuantity() {
|
||||||
|
if (!this.hasAddedQuantityTarget) return 0;
|
||||||
|
|
||||||
|
const addedQuantity = parseInt(this.addedQuantityTarget.value);
|
||||||
|
|
||||||
|
return isNaN(addedQuantity) ? 0 : addedQuantity;
|
||||||
|
}
|
||||||
|
}
|
39
_packs/controllers/cart_counter_controller.js
Normal file
39
_packs/controllers/cart_counter_controller.js
Normal file
|
@ -0,0 +1,39 @@
|
||||||
|
import { CartBaseController } from "./cart_base_controller";
|
||||||
|
|
||||||
|
export default class extends CartBaseController {
|
||||||
|
static targets = ["counter"];
|
||||||
|
|
||||||
|
connect() {
|
||||||
|
if (!this.hasCounterTarget) {
|
||||||
|
console.error("Missing counter target");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
81
_packs/controllers/cart_coupon_controller.js
Normal file
81
_packs/controllers/cart_coupon_controller.js
Normal file
|
@ -0,0 +1,81 @@
|
||||||
|
import { CartBaseController } from "./cart_base_controller";
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Retrieves shipping methods
|
||||||
|
*/
|
||||||
|
export default class extends CartBaseController {
|
||||||
|
static targets = ["couponCodeInvalid", "preDiscount", "total"];
|
||||||
|
|
||||||
|
connect() {
|
||||||
|
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() {
|
||||||
|
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");
|
||||||
|
}
|
||||||
|
}
|
106
_packs/controllers/cart_payment_methods_controller.js
Normal file
106
_packs/controllers/cart_payment_methods_controller.js
Normal file
|
@ -0,0 +1,106 @@
|
||||||
|
import { CartBaseController } from "./cart_base_controller";
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Retrieves payment methods and redirect to external checkouts
|
||||||
|
*/
|
||||||
|
export default class extends CartBaseController {
|
||||||
|
static targets = ["form", "submit", "specialInstructions"];
|
||||||
|
|
||||||
|
async connect() {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
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 });
|
||||||
|
}
|
||||||
|
|
||||||
|
async render(data = {}) {
|
||||||
|
const template = window.templates[this.data.get("template")];
|
||||||
|
|
||||||
|
this.element.innerHTML = await this.engine.parseAndRender(template, data);
|
||||||
|
|
||||||
|
if (!this.hasSubmitTarget) return;
|
||||||
|
this.formTarget.elements.forEach((p) =>
|
||||||
|
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();
|
||||||
|
|
||||||
|
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();
|
||||||
|
|
||||||
|
// 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: {
|
||||||
|
special_instructions,
|
||||||
|
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];
|
||||||
|
|
||||||
|
window.location = redirectUrl;
|
||||||
|
}
|
||||||
|
}
|
60
_packs/controllers/cart_paypal_confirmation_controller.js
Normal file
60
_packs/controllers/cart_paypal_confirmation_controller.js
Normal file
|
@ -0,0 +1,60 @@
|
||||||
|
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 = 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)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* 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("/");
|
||||||
|
}
|
||||||
|
}
|
135
_packs/controllers/cart_shipping_controller.js
Normal file
135
_packs/controllers/cart_shipping_controller.js
Normal file
|
@ -0,0 +1,135 @@
|
||||||
|
import { CartBaseController } from "./cart_base_controller";
|
||||||
|
|
||||||
|
export default class extends CartBaseController {
|
||||||
|
static targets = ["methods", "rates", "form"];
|
||||||
|
|
||||||
|
connect() {
|
||||||
|
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) {
|
||||||
|
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 = window.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 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)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,42 +1,42 @@
|
||||||
import { Controller } from 'stimulus'
|
import { Controller } from "stimulus";
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Sólo permite enviar el formulario de contacto después de unos
|
* Sólo permite enviar el formulario de contacto después de unos
|
||||||
* segundos, para evitar el spam.
|
* segundos, para evitar el spam.
|
||||||
*/
|
*/
|
||||||
export default class extends Controller {
|
export default class extends Controller {
|
||||||
static targets = [ 'submit' ]
|
static targets = ["submit"];
|
||||||
|
|
||||||
connect () {
|
connect() {
|
||||||
if (!this.hasSubmitTarget) return
|
if (!this.hasSubmitTarget) return;
|
||||||
|
|
||||||
this.submitTarget.disabled = true
|
this.submitTarget.disabled = true;
|
||||||
|
|
||||||
this._value = this.submitTarget.value
|
this._value = this.submitTarget.value;
|
||||||
|
|
||||||
// Esperar un minuto desde que se carga la página hasta que se
|
// Esperar un minuto desde que se carga la página hasta que se
|
||||||
// permite enviar.
|
// permite enviar.
|
||||||
this._interval = setInterval(() => {
|
this._interval = setInterval(() => {
|
||||||
const delay = this.delay
|
const delay = this.delay;
|
||||||
|
|
||||||
if (this.delay == 0) {
|
if (this.delay == 0) {
|
||||||
clearInterval(this._interval)
|
clearInterval(this._interval);
|
||||||
this.submitTarget.disabled = false
|
this.submitTarget.disabled = false;
|
||||||
this.submitTarget.value = this._value
|
this.submitTarget.value = this._value;
|
||||||
} else {
|
} else {
|
||||||
this.delay = delay - 1
|
this.delay = delay - 1;
|
||||||
}
|
}
|
||||||
}, 1000)
|
}, 1000);
|
||||||
}
|
}
|
||||||
|
|
||||||
get delay () {
|
get delay() {
|
||||||
const delay = parseInt(this.element.dataset.delay)
|
const delay = parseInt(this.element.dataset.delay);
|
||||||
|
|
||||||
return isNaN(delay) ? 0 : delay
|
return isNaN(delay) ? 0 : delay;
|
||||||
}
|
}
|
||||||
|
|
||||||
set delay (value) {
|
set delay(value) {
|
||||||
this.element.dataset.delay = value
|
this.element.dataset.delay = value;
|
||||||
this.submitTarget.value = `${this._value} (${value})`
|
this.submitTarget.value = `${this._value} (${value})`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
124
_packs/controllers/country_controller.js
Normal file
124
_packs/controllers/country_controller.js
Normal file
|
@ -0,0 +1,124 @@
|
||||||
|
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);
|
||||||
|
});
|
||||||
|
|
||||||
|
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", 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", this.change_event);
|
||||||
|
|
||||||
|
// 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"));
|
||||||
|
}
|
||||||
|
|
||||||
|
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.
|
||||||
|
*/
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,18 +1,42 @@
|
||||||
import { Controller } from 'stimulus'
|
import { Controller } from "stimulus";
|
||||||
|
|
||||||
|
// Ejemplo de uso:
|
||||||
|
// window.dispatchEvent(
|
||||||
|
// new CustomEvent("toast", { detail: { content: "¡Hola, usuarix!" } })
|
||||||
|
// );
|
||||||
export default class extends Controller {
|
export default class extends Controller {
|
||||||
static targets = [ 'content' ]
|
static targets = ["content"];
|
||||||
|
|
||||||
connect () {
|
connect() {
|
||||||
window.addEventListener('toast', event => {
|
this.toast_event = this._toast_event.bind(this);
|
||||||
this.contentTarget.innerText = event.detail.content
|
|
||||||
this.element.classList.toggle('hide')
|
|
||||||
this.element.classList.toggle('show')
|
|
||||||
|
|
||||||
setTimeout(() => {
|
window.addEventListener("toast", this.toast_event);
|
||||||
this.element.classList.toggle('hide')
|
}
|
||||||
this.element.classList.toggle('show')
|
|
||||||
}, 3000)
|
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) {
|
||||||
|
if (show) {
|
||||||
|
this.element.classList.remove("hide");
|
||||||
|
this.element.classList.add("show");
|
||||||
|
} else {
|
||||||
|
this.element.classList.add("hide");
|
||||||
|
this.element.classList.remove("show");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,29 +1,39 @@
|
||||||
import { Controller } from 'stimulus'
|
import { Controller } from "stimulus";
|
||||||
|
|
||||||
export default class extends Controller {
|
export default class extends Controller {
|
||||||
static targets = [ 'item' ]
|
static targets = ["item"];
|
||||||
|
|
||||||
connect () {
|
connect() {
|
||||||
window.addEventListener('scroll:section', event => this.update(event.detail.id))
|
this.scroll_section_event = this._scroll_section_event.bind(this);
|
||||||
|
|
||||||
|
window.addEventListener("scroll:section", this.scroll_section_event);
|
||||||
}
|
}
|
||||||
|
|
||||||
get items () {
|
disconnect() {
|
||||||
|
window.removeEventListener("scroll:section", this.scroll_section_event);
|
||||||
|
}
|
||||||
|
|
||||||
|
_scroll_section_event(event) {
|
||||||
|
this.update(event.detail.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
get items() {
|
||||||
if (!this._items) {
|
if (!this._items) {
|
||||||
this._items = {}
|
this._items = {};
|
||||||
|
|
||||||
for (const item of this.itemTargets) {
|
for (const item of this.itemTargets) {
|
||||||
this._items[item.href.split('#')[1]] = item
|
this._items[item.href.split("#")[1]] = item;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return this._items
|
return this._items;
|
||||||
}
|
}
|
||||||
|
|
||||||
update (id) {
|
update(id) {
|
||||||
for (const item of Object.values(this.items)) {
|
for (const item of Object.values(this.items)) {
|
||||||
item.classList.remove('active')
|
item.classList.remove("active");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.items[id]) this.items[id].classList.add('active')
|
if (this.items[id]) this.items[id].classList.add("active");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,56 +1,47 @@
|
||||||
import { Controller } from 'stimulus'
|
import { Controller } from "stimulus";
|
||||||
import { Liquid } from 'liquidjs'
|
import { Liquid } from "liquidjs";
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Waits for notifications and shows them by fetching and rendering
|
* Waits for notifications and shows them by fetching and rendering
|
||||||
* a template.
|
* a template.
|
||||||
*/
|
*/
|
||||||
export default class extends Controller {
|
export default class extends Controller {
|
||||||
connect () {
|
connect() {
|
||||||
window.addEventListener('notification', async event => await this.render(event.detail.template, event.detail.data))
|
window.addEventListener(
|
||||||
|
"notification",
|
||||||
|
async (event) =>
|
||||||
|
await this.render(event.detail.template, event.detail.data)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Renders and replaces notification contents and then shows it. Does
|
* Renders and replaces notification contents and then shows it. Does
|
||||||
* nothing if the template isn't found.
|
* nothing if the template isn't found.
|
||||||
*/
|
*/
|
||||||
async render (name, data = {}) {
|
async render(name, data = {}) {
|
||||||
const response = await fetch(this.template(name))
|
data.site = window.site;
|
||||||
|
|
||||||
if (!response.ok) return
|
const template = window.templates.alert;
|
||||||
|
const html = await this.engine.parseAndRender(template, data);
|
||||||
|
|
||||||
data.site = await this.site()
|
this.element.innerHTML = html;
|
||||||
|
this.show();
|
||||||
const template = await response.text()
|
|
||||||
const html = await this.engine.parseAndRender(template, data)
|
|
||||||
|
|
||||||
this.element.innerHTML = html
|
|
||||||
this.show()
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Gets the template path from a name
|
|
||||||
*
|
|
||||||
* @return [String]
|
|
||||||
*/
|
|
||||||
template (name) {
|
|
||||||
return this.data.get('templates') + name + '.html'
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Shows the notification
|
* Shows the notification
|
||||||
*/
|
*/
|
||||||
show () {
|
show() {
|
||||||
this.element.classList.add('show')
|
this.element.classList.add("show");
|
||||||
this.element.classList.remove('hide')
|
this.element.classList.remove("hide");
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Hides the notification
|
* Hides the notification
|
||||||
*/
|
*/
|
||||||
hide () {
|
hide() {
|
||||||
this.element.classList.add('hide')
|
this.element.classList.add("hide");
|
||||||
this.element.classList.remove('show')
|
this.element.classList.remove("show");
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
@ -58,23 +49,9 @@ export default class extends Controller {
|
||||||
*
|
*
|
||||||
* @return Liquid
|
* @return Liquid
|
||||||
*/
|
*/
|
||||||
get engine () {
|
get engine() {
|
||||||
if (!window.liquid) window.liquid = new Liquid()
|
if (!window.liquid) window.liquid = new Liquid();
|
||||||
|
|
||||||
return window.liquid
|
return window.liquid;
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
* 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
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
82
_packs/controllers/order_controller.js
Normal file
82
_packs/controllers/order_controller.js
Normal file
|
@ -0,0 +1,82 @@
|
||||||
|
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 = window.site;
|
||||||
|
|
||||||
|
this.render({ products, site });
|
||||||
|
this.subtotalUpdate();
|
||||||
|
this.itemCountUpdate();
|
||||||
|
|
||||||
|
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
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
disconnect() {
|
||||||
|
window.removeEventListener("storage", this.storage_event);
|
||||||
|
window.removeEventListener(
|
||||||
|
"cart:subtotal:update",
|
||||||
|
this.cart_subtotal_update_event
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async _storage_event(event) {
|
||||||
|
if (!event.key?.startsWith("cart:item:")) return;
|
||||||
|
|
||||||
|
const products = this.products;
|
||||||
|
const site = window.site;
|
||||||
|
|
||||||
|
this.render({ products, site });
|
||||||
|
this.subtotalUpdate();
|
||||||
|
this.itemCountUpdate();
|
||||||
|
}
|
||||||
|
|
||||||
|
_cart_subtotal_update_event(event) {
|
||||||
|
this.itemCountUpdate();
|
||||||
|
this.subtotalUpdate();
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Download the item template and render the order
|
||||||
|
*/
|
||||||
|
render(data = {}) {
|
||||||
|
const template = window.templates[this.data.get("itemTemplate")];
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
248
_packs/controllers/pay_what_you_can_controller.js
Normal file
248
_packs/controllers/pay_what_you_can_controller.js
Normal file
|
@ -0,0 +1,248 @@
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
212
_packs/controllers/postal_code_controller.js
Normal file
212
_packs/controllers/postal_code_controller.js
Normal file
|
@ -0,0 +1,212 @@
|
||||||
|
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() {
|
||||||
|
this.cart_country_update_event = this._cart_country_update_event.bind(this);
|
||||||
|
|
||||||
|
window.addEventListener(
|
||||||
|
"cart:country:update",
|
||||||
|
this.cart_country_update_event
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
disconnect() {
|
||||||
|
window.removeEventListener(
|
||||||
|
"cart:country:update",
|
||||||
|
this.cart_country_update_event
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
_cart_country_update_event(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"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,4 +1,4 @@
|
||||||
import { Controller } from 'stimulus'
|
import { Controller } from "stimulus";
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Al navegar por el sitio y llegar a ciertas secciones, se van
|
* Al navegar por el sitio y llegar a ciertas secciones, se van
|
||||||
|
@ -8,34 +8,40 @@ import { Controller } from 'stimulus'
|
||||||
* a medida que van apareciendo secciones actualizamos el menú.
|
* a medida que van apareciendo secciones actualizamos el menú.
|
||||||
*/
|
*/
|
||||||
export default class extends Controller {
|
export default class extends Controller {
|
||||||
static targets = [ 'section' ]
|
static targets = ["section"];
|
||||||
|
|
||||||
connect () {
|
connect() {
|
||||||
for (const section of this.sectionTargets) {
|
for (const section of this.sectionTargets) {
|
||||||
this.observer.observe(section)
|
this.observer.observe(section);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Solo nos interesa la primera
|
* Solo nos interesa la primera
|
||||||
*/
|
*/
|
||||||
get observer () {
|
get observer() {
|
||||||
if (!this._observer) this._observer = new IntersectionObserver((entries, observer) => this.update(entries), this.options)
|
if (!this._observer)
|
||||||
|
this._observer = new IntersectionObserver(
|
||||||
|
(entries, observer) => this.update(entries),
|
||||||
|
this.options
|
||||||
|
);
|
||||||
|
|
||||||
return this._observer
|
return this._observer;
|
||||||
}
|
}
|
||||||
|
|
||||||
get options () {
|
get options() {
|
||||||
if (!this._options) this._options = { threshold: 0, rootMargin: '0px' }
|
if (!this._options) this._options = { threshold: 0, rootMargin: "0px" };
|
||||||
|
|
||||||
return this._options
|
return this._options;
|
||||||
}
|
}
|
||||||
|
|
||||||
update (entries) {
|
update(entries) {
|
||||||
const section = entries.find(x => x.isIntersecting)
|
const section = entries.find((x) => x.isIntersecting);
|
||||||
|
|
||||||
if (!section) return
|
if (!section) return;
|
||||||
|
|
||||||
window.dispatchEvent(new CustomEvent('scroll:section', { detail: { id: section.target.id }}))
|
window.dispatchEvent(
|
||||||
|
new CustomEvent("scroll:section", { detail: { id: section.target.id } })
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,91 +1,96 @@
|
||||||
import { Controller } from 'stimulus'
|
import { Controller } from "stimulus";
|
||||||
import { Liquid } from 'liquidjs'
|
import { Liquid } from "liquidjs";
|
||||||
|
|
||||||
const lunr = require("lunr")
|
const lunr = require("lunr");
|
||||||
require("lunr-languages/lunr.stemmer.support")(lunr)
|
require("lunr-languages/lunr.stemmer.support")(lunr);
|
||||||
require("lunr-languages/lunr.es")(lunr)
|
require("lunr-languages/lunr.es")(lunr);
|
||||||
|
|
||||||
export default class extends Controller {
|
export default class extends Controller {
|
||||||
static targets = [ 'q' ]
|
static targets = ["q"];
|
||||||
|
|
||||||
get q () {
|
get q() {
|
||||||
if (!this.hasQTarget) return
|
if (!this.hasQTarget) return;
|
||||||
if (!this.qTarget.value.trim().length === 0) return
|
if (!this.qTarget.value.trim().length === 0) return;
|
||||||
|
|
||||||
return this.qTarget.value.trim().replace(':', '')
|
return this.qTarget.value.trim().replaceAll(/[:~\*\^\+\-]/gi, "");
|
||||||
}
|
}
|
||||||
|
|
||||||
connect () {
|
connect() {
|
||||||
const q = new URLSearchParams(window.location.search).get('q')?.trim()
|
const q = new URLSearchParams(window.location.search).get("q")?.trim();
|
||||||
|
|
||||||
if (q) {
|
if (q) {
|
||||||
this.qTarget.value = q
|
this.qTarget.value = q;
|
||||||
this.search()
|
this.search();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async search (event) {
|
async search(event) {
|
||||||
// Detiene el envío del formulario
|
// Detiene el envío del formulario
|
||||||
if (event) {
|
if (event) {
|
||||||
event.preventDefault()
|
event.preventDefault();
|
||||||
event.stopPropagation()
|
event.stopPropagation();
|
||||||
}
|
}
|
||||||
|
|
||||||
this.formDisable = true
|
this.formDisable = true;
|
||||||
|
|
||||||
// Obtiene el término de búsqueda
|
// Obtiene el término de búsqueda
|
||||||
const q = this.q
|
const q = this.q;
|
||||||
// Si no hay término de búsqueda, no hay búsqueda
|
// Si no hay término de búsqueda, no hay búsqueda
|
||||||
if (q) {
|
if (q) {
|
||||||
// Trae el índice de búsqueda
|
// Trae el índice de búsqueda
|
||||||
await this.fetch()
|
await this.fetch();
|
||||||
|
|
||||||
// Hasta que no haya índice no buscamos nada, esto evita que se
|
// Hasta que no haya índice no buscamos nada, esto evita que se
|
||||||
// aprete enter dos veces y se fallen cosas.
|
// aprete enter dos veces y se fallen cosas.
|
||||||
if (!window.index) return
|
if (!window.index) return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const main = document.querySelector('main')
|
const main = document.querySelector("main");
|
||||||
const results = window.index.search(q).map(r => window.data.find(a => a.id == r.ref))
|
const results = window.index
|
||||||
const site = await this.site()
|
.search(q)
|
||||||
const request = await fetch('assets/templates/results.html')
|
.map((r) => window.data.find((a) => a.id == r.ref));
|
||||||
const template = await request.text()
|
const site = window.site;
|
||||||
const html = await this.engine.parseAndRender(template, { q, site, results })
|
const template = window.templates.results;
|
||||||
const title = `${site.i18n.search.title} - ${q}`
|
const html = await this.engine.parseAndRender(template, {
|
||||||
const query = new URLSearchParams({ q })
|
q,
|
||||||
|
site,
|
||||||
|
results,
|
||||||
|
});
|
||||||
|
const title = `${site.i18n.search.title} - ${q}`;
|
||||||
|
const query = new URLSearchParams({ q });
|
||||||
|
|
||||||
window.history.pushState({ q }, title, `?${query.toString()}`)
|
window.history.pushState({ q }, title, `?${query.toString()}`);
|
||||||
document.title = title
|
document.title = title;
|
||||||
|
|
||||||
main.innerHTML = html
|
main.innerHTML = html;
|
||||||
this.formDisable = false
|
this.formDisable = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
async fetch () {
|
async fetch() {
|
||||||
// Solo permite descargar uno a la vez
|
// Solo permite descargar uno a la vez
|
||||||
if (this.fetching) return
|
if (this.fetching) return;
|
||||||
|
|
||||||
this.fetching = true
|
this.fetching = true;
|
||||||
let response
|
let response;
|
||||||
|
|
||||||
// Si no existe el índice, lo descarga y procesa Lunr
|
// Si no existe el índice, lo descarga y procesa Lunr
|
||||||
if (!window.data) {
|
if (!window.data) {
|
||||||
response = await fetch('data.json')
|
response = await fetch("data.json");
|
||||||
window.data = await response.json()
|
window.data = await response.json();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!window.index) {
|
if (!window.index) {
|
||||||
response = await fetch('idx.json')
|
response = await fetch("idx.json");
|
||||||
const idx = await response.json()
|
const idx = await response.json();
|
||||||
window.index = lunr.Index.load(idx)
|
window.index = lunr.Index.load(idx);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Permitir volver a ejecutar
|
// Permitir volver a ejecutar
|
||||||
this.fetching = false
|
this.fetching = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
set formDisable (disable) {
|
set formDisable(disable) {
|
||||||
this.element.elements.forEach(x => x.disabled = disable)
|
this.element.elements.forEach((x) => (x.disabled = disable));
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
@ -93,18 +98,9 @@ export default class extends Controller {
|
||||||
*
|
*
|
||||||
* @return Liquid
|
* @return Liquid
|
||||||
*/
|
*/
|
||||||
get engine () {
|
get engine() {
|
||||||
if (!window.liquid) window.liquid = new Liquid()
|
if (!window.liquid) window.liquid = new Liquid();
|
||||||
|
|
||||||
return window.liquid
|
return window.liquid;
|
||||||
}
|
|
||||||
|
|
||||||
async site () {
|
|
||||||
if (!window.site) {
|
|
||||||
const data = await fetch('assets/data/site.json')
|
|
||||||
window.site = await data.json()
|
|
||||||
}
|
|
||||||
|
|
||||||
return window.site
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,26 +1,26 @@
|
||||||
import { Controller } from 'stimulus'
|
import { Controller } from "stimulus";
|
||||||
|
|
||||||
export default class extends Controller {
|
export default class extends Controller {
|
||||||
static values = {
|
static values = {
|
||||||
title: String,
|
title: String,
|
||||||
text: String,
|
text: String,
|
||||||
url: String
|
url: String,
|
||||||
}
|
};
|
||||||
|
|
||||||
async share (event = undefined) {
|
async share(event = undefined) {
|
||||||
event?.preventDefault()
|
event?.preventDefault();
|
||||||
event?.stopPropagation()
|
event?.stopPropagation();
|
||||||
|
|
||||||
const title = this.titleValue
|
const title = this.titleValue;
|
||||||
const text = this.textValue
|
const text = this.textValue;
|
||||||
const url = this.urlValue
|
const url = this.urlValue;
|
||||||
const data = { title, text, url }
|
const data = { title, text, url };
|
||||||
|
|
||||||
if ('share' in navigator) {
|
if ("share" in navigator) {
|
||||||
if (navigator.canShare(data)) {
|
if (navigator.canShare(data)) {
|
||||||
navigator.share(data)
|
navigator.share(data);
|
||||||
} else {
|
} else {
|
||||||
console.error('No se puede compartir', data)
|
console.error("No se puede compartir", data);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,64 +1,73 @@
|
||||||
import { Controller } from 'stimulus'
|
import { Controller } from "stimulus";
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Slider con scroll automático
|
* Slider con scroll automático
|
||||||
*/
|
*/
|
||||||
export default class extends Controller {
|
export default class extends Controller {
|
||||||
static targets = [ 'control' ]
|
static targets = ["control"];
|
||||||
|
|
||||||
connect () {
|
connect() {
|
||||||
this.active(this.controlTargets.find(x => x.href.endsWith(window.location.hash)))
|
this.active(
|
||||||
|
this.controlTargets.find((x) => x.href.endsWith(window.location.hash))
|
||||||
|
);
|
||||||
|
|
||||||
this.interval = setInterval(() => this.inViewport ? this.controlTargets[this.next].click() : null, this.duration * 1000)
|
this.interval = setInterval(
|
||||||
|
() => (this.inViewport ? this.controlTargets[this.next].click() : null),
|
||||||
|
this.duration * 1000
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
get duration () {
|
get duration() {
|
||||||
const duration = parseInt(this.data.get('duration'))
|
const duration = parseInt(this.data.get("duration"));
|
||||||
|
|
||||||
return isNaN(duration) ? 15 : duration
|
return isNaN(duration) ? 15 : duration;
|
||||||
}
|
}
|
||||||
|
|
||||||
disconnect () {
|
disconnect() {
|
||||||
clearInterval(this.interval)
|
clearInterval(this.interval);
|
||||||
}
|
}
|
||||||
|
|
||||||
active (control) {
|
active(control) {
|
||||||
if (!control) return
|
if (!control) return;
|
||||||
|
|
||||||
this.controlTargets.forEach(other => other.classList.toggle('active', control.href === other.href))
|
this.controlTargets.forEach((other) =>
|
||||||
this.current = this.controlTargets.indexOf(control)
|
other.classList.toggle("active", control.href === other.href)
|
||||||
|
);
|
||||||
|
this.current = this.controlTargets.indexOf(control);
|
||||||
}
|
}
|
||||||
|
|
||||||
activate (event) {
|
activate(event) {
|
||||||
// XXX: En Firefox, el target del evento también puede ser el
|
// XXX: En Firefox, el target del evento también puede ser el
|
||||||
// contenido del link :/
|
// contenido del link :/
|
||||||
let t = (event.target.href) ? event.target : event.target.parentElement
|
let t = event.target.href ? event.target : event.target.parentElement;
|
||||||
|
|
||||||
this.active(t)
|
this.active(t);
|
||||||
}
|
}
|
||||||
|
|
||||||
get current () {
|
get current() {
|
||||||
return parseInt(this.data.get('current')) || 0
|
return parseInt(this.data.get("current")) || 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
set current (value) {
|
set current(value) {
|
||||||
this.data.set('current', value)
|
this.data.set("current", value);
|
||||||
}
|
}
|
||||||
|
|
||||||
get next () {
|
get next() {
|
||||||
const next = this.current + 1
|
const next = this.current + 1;
|
||||||
|
|
||||||
return (this.controlTargets[next]) ? next : 0
|
return this.controlTargets[next] ? next : 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
get inViewport () {
|
get inViewport() {
|
||||||
const bounding = this.element.getBoundingClientRect();
|
const bounding = this.element.getBoundingClientRect();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
bounding.top >= 0 &&
|
bounding.top >= 0 &&
|
||||||
bounding.left >= 0 &&
|
bounding.left >= 0 &&
|
||||||
bounding.bottom <= (window.innerHeight || document.documentElement.clientHeight) &&
|
bounding.bottom <=
|
||||||
bounding.right <= (window.innerWidth || document.documentElement.clientWidth)
|
(window.innerHeight || document.documentElement.clientHeight) &&
|
||||||
|
bounding.right <=
|
||||||
|
(window.innerWidth || document.documentElement.clientWidth)
|
||||||
);
|
);
|
||||||
};
|
}
|
||||||
}
|
}
|
||||||
|
|
115
_packs/controllers/state_controller.js
Normal file
115
_packs/controllers/state_controller.js
Normal file
|
@ -0,0 +1,115 @@
|
||||||
|
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() {
|
||||||
|
this.cart_country_update_event = this._cart_country_update_event.bind(this);
|
||||||
|
this.change_event = this._change_event.bind(this);
|
||||||
|
|
||||||
|
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", this.change_event);
|
||||||
|
}
|
||||||
|
|
||||||
|
disconnect() {
|
||||||
|
window.removeEventListener(
|
||||||
|
"cart:country:update",
|
||||||
|
this.cart_country_update_event
|
||||||
|
);
|
||||||
|
this.nameTarget.removeEventListener("change", this.change_event);
|
||||||
|
}
|
||||||
|
|
||||||
|
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"));
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* 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;
|
||||||
|
}
|
||||||
|
}
|
118
_packs/controllers/stock_controller.js
Normal file
118
_packs/controllers/stock_controller.js
Normal file
|
@ -0,0 +1,118 @@
|
||||||
|
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() {
|
||||||
|
const all_skus = this.skus;
|
||||||
|
|
||||||
|
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);
|
||||||
|
|
||||||
|
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(",");
|
||||||
|
|
||||||
|
start = this.per_page * local_page;
|
||||||
|
end = start + this.per_page;
|
||||||
|
|
||||||
|
const filter = { skus };
|
||||||
|
let response = await window.spree.products.list({ filter });
|
||||||
|
|
||||||
|
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 });
|
||||||
|
|
||||||
|
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. Usamos los SKUs porque no tenemos forma de filtrar
|
||||||
|
* por ID.
|
||||||
|
*
|
||||||
|
* @return [Array]
|
||||||
|
*/
|
||||||
|
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() {
|
||||||
|
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 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 = parseInt(product.attributes.price))
|
||||||
|
);
|
||||||
|
local
|
||||||
|
.querySelectorAll("[data-stock-currency]")
|
||||||
|
.forEach(
|
||||||
|
(currency) => (currency.innerText = product.attributes.currency)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
81
_packs/endpoints/sutty.js
Normal file
81
_packs/endpoints/sutty.js
Normal file
|
@ -0,0 +1,81 @@
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,19 +1,25 @@
|
||||||
import { Notifier } from '@airbrake/browser'
|
import BotDetector from "device-detector-js/dist/parsers/bot";
|
||||||
|
import { Notifier } from "@airbrake/browser";
|
||||||
|
|
||||||
window.airbrake = new Notifier({
|
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,
|
projectId: window.env.AIRBRAKE_PROJECT_ID,
|
||||||
projectKey: window.env.AIRBRAKE_PROJECT_KEY,
|
projectKey: window.env.AIRBRAKE_PROJECT_KEY,
|
||||||
host: 'https://panel.sutty.nl'
|
host: "https://panel.sutty.nl",
|
||||||
})
|
});
|
||||||
|
|
||||||
console.originalError = console.error
|
console.originalError = console.error;
|
||||||
console.error = (...e) => {
|
console.error = (...e) => {
|
||||||
window.airbrake.notify(e.join(' '))
|
window.airbrake.notify(e.join(" "));
|
||||||
return console.originalError(...e)
|
return console.originalError(...e);
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
import 'core-js/stable'
|
import "core-js/stable";
|
||||||
import 'regenerator-runtime/runtime'
|
import "regenerator-runtime/runtime";
|
||||||
|
|
||||||
// Turbo acelera la navegación al no tener que recargar todo el JS y CSS
|
// Turbo acelera la navegación al no tener que recargar todo el JS y CSS
|
||||||
// de la página, con lo que se siente más rápida y "nativa".
|
// de la página, con lo que se siente más rápida y "nativa".
|
||||||
|
@ -21,40 +27,49 @@ import 'regenerator-runtime/runtime'
|
||||||
// Cambiamos de turbolinks a turbo porque turbo soporta la función
|
// Cambiamos de turbolinks a turbo porque turbo soporta la función
|
||||||
// fetch(), que luego es interceptada por el SW para obtener las
|
// fetch(), que luego es interceptada por el SW para obtener las
|
||||||
// direcciones localmente.
|
// direcciones localmente.
|
||||||
import * as Turbo from "@hotwired/turbo"
|
import * as Turbo from "@hotwired/turbo";
|
||||||
Turbo.start()
|
Turbo.start();
|
||||||
|
|
||||||
import { Application } from 'stimulus'
|
import { Application } from "stimulus";
|
||||||
import { definitionsFromContext } from "stimulus/webpack-helpers"
|
import { definitionsFromContext } from "stimulus/webpack-helpers";
|
||||||
|
|
||||||
const application = Application.start()
|
const application = Application.start();
|
||||||
const context = require.context("./controllers", true, /\.js$/)
|
const context = require.context("./controllers", true, /\.js$/);
|
||||||
application.load(definitionsFromContext(context))
|
application.load(definitionsFromContext(context));
|
||||||
|
|
||||||
|
import { makeClient } from "@spree/storefront-api-v2-sdk";
|
||||||
|
import { Sutty } from "./endpoints/sutty";
|
||||||
|
|
||||||
|
window.spree = makeClient({ host: window.env.SPREE_URL });
|
||||||
|
window.spree.sutty = new Sutty(window.spree.host);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
window.axe = require('axe-core/axe')
|
window.axe = require("axe-core/axe");
|
||||||
} catch(e) {}
|
} catch (e) {}
|
||||||
|
|
||||||
if (window.axe) window.axe.configure({ locale: require('axe-core/locales/es.json') })
|
if (window.axe)
|
||||||
|
window.axe.configure({ locale: require("axe-core/locales/es.json") });
|
||||||
|
|
||||||
document.addEventListener('turbo:load', event => {
|
document.addEventListener("turbo:load", (event) => {
|
||||||
document.querySelectorAll("a[href^='http://'],a[href^='https://'],a[href^='//']").forEach(a => {
|
document
|
||||||
a.rel = "noopener"
|
.querySelectorAll("a[href^='http://'],a[href^='https://'],a[href^='//']")
|
||||||
a.target = "_blank"
|
.forEach((a) => {
|
||||||
})
|
a.rel = "noopener";
|
||||||
|
a.target = "_blank";
|
||||||
|
});
|
||||||
|
|
||||||
if (!window.axe) return
|
if (!window.axe) return;
|
||||||
|
|
||||||
window.axe.run().then(results => {
|
window.axe.run().then((results) => {
|
||||||
results.violations.forEach(violation => {
|
results.violations.forEach((violation) => {
|
||||||
violation.nodes.forEach(node => {
|
violation.nodes.forEach((node) => {
|
||||||
node.target.forEach(target => {
|
node.target.forEach((target) => {
|
||||||
document.querySelectorAll(target).forEach(element => {
|
document.querySelectorAll(target).forEach((element) => {
|
||||||
element.classList.add('inaccesible')
|
element.classList.add("inaccesible");
|
||||||
element.ariaLabel = node.failureSummary
|
element.ariaLabel = node.failureSummary;
|
||||||
})
|
});
|
||||||
})
|
});
|
||||||
})
|
});
|
||||||
})
|
});
|
||||||
})
|
});
|
||||||
})
|
});
|
||||||
|
|
11
_sass/content.scss
Normal file
11
_sass/content.scss
Normal file
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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');
|
||||||
|
}
|
|
@ -26,6 +26,39 @@ $directions: (top, right, bottom, left);
|
||||||
/// `-md`, `-lg`, `-xl`.
|
/// `-md`, `-lg`, `-xl`.
|
||||||
$infix: breakpoint-infix($breakpoint, $grid-breakpoints);
|
$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
|
||||||
|
/// <div class="d-grid grid-cols-5">
|
||||||
|
/// <div class="grid-row-1 grid-col-1-to-2"></div>
|
||||||
|
/// <div class="grid-row-2 grid-col-2-to-3"></div>
|
||||||
|
/// </div>
|
||||||
|
.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.
|
/// Ocultar la barra de scroll, útil para sliders horizontales.
|
||||||
///
|
///
|
||||||
/// @example html
|
/// @example html
|
||||||
|
@ -37,6 +70,18 @@ $directions: (top, right, bottom, left);
|
||||||
&::-webkit-scrollbar { display: none; }
|
&::-webkit-scrollbar { display: none; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
:enabled {
|
||||||
|
.show-when-disabled#{$infix} {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
:disabled {
|
||||||
|
.hide-when-disabled#{$infix} {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Un elemento cuadrado
|
/// Un elemento cuadrado
|
||||||
///
|
///
|
||||||
/// @example html
|
/// @example html
|
||||||
|
@ -315,7 +360,7 @@ $directions: (top, right, bottom, left);
|
||||||
///
|
///
|
||||||
/// @example html
|
/// @example html
|
||||||
/// <div class="black hover-red"></div>
|
/// <div class="black hover-red"></div>
|
||||||
.#{$color} {
|
.#{$color}#{$infix} {
|
||||||
color: var(--#{$color});
|
color: var(--#{$color});
|
||||||
|
|
||||||
&:focus {
|
&:focus {
|
||||||
|
|
|
@ -6,11 +6,41 @@
|
||||||
/// @group Principal
|
/// @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.
|
/// personalizados.
|
||||||
///
|
|
||||||
/// @todo Mover a su propio SCSS
|
{% comment %}
|
||||||
{% assign about = site.posts | find: 'layout', 'about' %}
|
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,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
|
||||||
|
las convertimos.
|
||||||
|
|
||||||
|
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 }}{{ variable[1].unit }};
|
||||||
|
{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
|
||||||
/// El modo debug se desactiva en producción
|
/// El modo debug se desactiva en producción
|
||||||
$debug: {{ jekyll.environment | not: 'production' }};
|
$debug: {{ jekyll.environment | not: 'production' }};
|
||||||
|
@ -22,21 +52,6 @@ $vendor-prefixes: ("", "-webkit-", "-ms-", "-o-", "-moz-");
|
||||||
/// para generar animaciones.
|
/// para generar animaciones.
|
||||||
$bezier: cubic-bezier(0.75, 0, 0.25, 1);
|
$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
|
/// Agregamos los colores propios de la plantilla aquí. Bootstrap los
|
||||||
/// agrega a su propia paleta de colores. Si usamos el mismo nombre
|
/// agrega a su propia paleta de colores. Si usamos el mismo nombre
|
||||||
/// podemos redefinir el color.
|
/// podemos redefinir el color.
|
||||||
|
@ -99,6 +114,9 @@ $paragraph-margin-bottom: 0;
|
||||||
$headings-margin-bottom: 0;
|
$headings-margin-bottom: 0;
|
||||||
$label-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
|
/// Redefinir variables de Boostrap acá. Se las puede ver en
|
||||||
/// node_modules/bootstrap/scss/_variables.scss
|
/// node_modules/bootstrap/scss/_variables.scss
|
||||||
///
|
///
|
||||||
|
@ -117,6 +135,9 @@ $label-margin-bottom: 0;
|
||||||
@import "snap";
|
@import "snap";
|
||||||
@import "editor";
|
@import "editor";
|
||||||
@import "menu";
|
@import "menu";
|
||||||
|
@import "content";
|
||||||
|
@import "fonts";
|
||||||
|
@import "floating_alert";
|
||||||
|
|
||||||
/// La barra de progreso de Turbo tiene el color primario
|
/// La barra de progreso de Turbo tiene el color primario
|
||||||
/// de la paleta, definido por Bootstrap o por nosotres.
|
/// de la paleta, definido por Bootstrap o por nosotres.
|
||||||
|
|
25
assets/data/site.json
Normal file
25
assets/data/site.json
Normal file
|
@ -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 }}
|
||||||
|
}
|
BIN
assets/fonts/roboto/v27/KFOjCnqEu92Fr1Mu51TzBhc9-subset.woff2
Normal file
BIN
assets/fonts/roboto/v27/KFOjCnqEu92Fr1Mu51TzBhc9-subset.woff2
Normal file
Binary file not shown.
BIN
assets/fonts/roboto/v27/KFOkCnqEu92Fr1MmgWxP-subset.woff2
Normal file
BIN
assets/fonts/roboto/v27/KFOkCnqEu92Fr1MmgWxP-subset.woff2
Normal file
Binary file not shown.
BIN
assets/fonts/roboto/v27/KFOkCnqEu92Fr1Mu52xP-subset.woff2
Normal file
BIN
assets/fonts/roboto/v27/KFOkCnqEu92Fr1Mu52xP-subset.woff2
Normal file
Binary file not shown.
BIN
assets/fonts/roboto/v27/KFOlCnqEu92Fr1MmWUlvAw-subset.woff2
Normal file
BIN
assets/fonts/roboto/v27/KFOlCnqEu92Fr1MmWUlvAw-subset.woff2
Normal file
Binary file not shown.
BIN
assets/fonts/roboto/v27/KFOmCnqEu92Fr1Me5Q-subset.woff2
Normal file
BIN
assets/fonts/roboto/v27/KFOmCnqEu92Fr1Me5Q-subset.woff2
Normal file
Binary file not shown.
|
@ -1,7 +1,9 @@
|
||||||
<div class="alert alert-{{ type }} alert-dismissible" role="alert">
|
{% raw %}
|
||||||
|
<div class="alert alert-{{ type }} alert-dismissible" role="alert">
|
||||||
{{ content }}
|
{{ content }}
|
||||||
|
|
||||||
<button data-action="notification#hide" type="button" class="close" aria-label="{{ site.i18n.close }}">
|
<button data-action="notification#hide" type="button" class="close" aria-label="{{ site.i18n.close }}">
|
||||||
<span aria-hidden="true">×</span>
|
<span aria-hidden="true">×</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
{% endraw %}
|
||||||
|
|
67
assets/templates/cart.html
Normal file
67
assets/templates/cart.html
Normal file
|
@ -0,0 +1,67 @@
|
||||||
|
{% raw %}
|
||||||
|
{% for product in products %}
|
||||||
|
<div
|
||||||
|
class="border-bottom row no-gutters align-items-center justify-content-center mb-3 mb-md-0 pb-3 pb-md-0"
|
||||||
|
data-controller="cart"
|
||||||
|
data-cart-variant-id="{{ product.variant_id }}"
|
||||||
|
data-cart-image="{{ product.image }}"
|
||||||
|
data-cart-title="{{ product.title }}"
|
||||||
|
data-cart-extra="{{ product.extra | join: '|' }}"
|
||||||
|
data-cart-line-item-id="{{ product.line_item.id }}">
|
||||||
|
<div class="col-12 col-md-6 col-lg-4 p-3">
|
||||||
|
<div class="d-flex align-items-center flex-wrap">
|
||||||
|
<img class="mr-3" loading="lazy" src="{{ product.image }}" alt="{{ product.title }}" />
|
||||||
|
|
||||||
|
<div class="gray-600 align-middle mt-3 mt-md-0">
|
||||||
|
<h1 class="f-3 font-weight-bold m-0">{{ product.title }}</h1>
|
||||||
|
<p class="font-weight-light m-0">{{ product.extra | join: ', ' }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-12 col-md-6 col-lg-8">
|
||||||
|
<div class="row no-gutters align-items-center">
|
||||||
|
<div class="col-12 col-lg p-3">
|
||||||
|
<p class="font-weight-light gray-600 m-0 text-center">
|
||||||
|
<strong class="d-lg-none mr-3">{{ site.cart.price }}:</strong>
|
||||||
|
<span>
|
||||||
|
{{ product.line_item.attributes.price }} {{ product.line_item.attributes.currency }}
|
||||||
|
</span>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-12 col-lg p-3">
|
||||||
|
<div class="form-group m-0">
|
||||||
|
<label for="cart-quantity-{{ product.variant_id }}" class="sr-only">{{ site.cart.quantity }}</label>
|
||||||
|
<input
|
||||||
|
class="form-control"
|
||||||
|
aria-describedby="cart-quantity-help-{{ production.variant_id }}"
|
||||||
|
type="number"
|
||||||
|
min="1"
|
||||||
|
data-target="cart.quantity"
|
||||||
|
value="{{ product.line_item.attributes.quantity }}" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-12 col-lg p-3">
|
||||||
|
<p class="font-weight-light gray-600 m-0 text-center">
|
||||||
|
<strong class="d-lg-none mr-3">{{ site.cart.subtotal }}:</strong>
|
||||||
|
<span data-target="cart.subtotal">
|
||||||
|
{{ product.line_item.attributes.discounted_amount }}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
{{ product.line_item.attributes.currency }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-12 col-lg-2 text-center">
|
||||||
|
<button class="btn btn-transparent border-0 gray-600" data-action="cart#remove">
|
||||||
|
<i class="fa fa-trash-o fa-2x fa-fw"></i>
|
||||||
|
<span class="sr-only">{{ site.cart.remove }}</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
{% endraw %}
|
67
assets/templates/confirmation.html
Normal file
67
assets/templates/confirmation.html
Normal file
|
@ -0,0 +1,67 @@
|
||||||
|
{% raw %}
|
||||||
|
<div>
|
||||||
|
<dl>
|
||||||
|
<dt>{{ site.confirmation.number }}</dt>
|
||||||
|
<dd>{{ order.number }}</dd>
|
||||||
|
|
||||||
|
<dt>{{ site.confirmation.delivery }}<dt>
|
||||||
|
<dd><address>{{ shipping_address.address1 }}, {{ shipping_address.city }} ({{ shipping_address.zipcode }})</address></dd>
|
||||||
|
|
||||||
|
<dt>{{ site.confirmation.email }}</dt>
|
||||||
|
<dd>{{ order.email }}</dd>
|
||||||
|
</dl>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="">
|
||||||
|
<h1>{{ site.confirmation.your_order }}</h1>
|
||||||
|
<div class="row no-gutters align-items-center">
|
||||||
|
<div class="d-none d-md-block col-md-4">
|
||||||
|
<h2 class="f-3 font-weight-bold text-uppercase">{{ site.cart.product }}</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="d-none d-md-block col-md-4 pl-3">
|
||||||
|
<h2 class="f-3 font-weight-bold text-uppercase">{{ site.cart.price }}</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="d-none d-md-block col-md-4 pl-3">
|
||||||
|
<h2 class="f-3 font-weight-bold text-uppercase">{{ site.cart.subtotal }}</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% for product in products %}
|
||||||
|
<div class="col-12 col-md-4">
|
||||||
|
<h2 class="f-3 font-weight-bold text-uppercase d-md-none">{{ site.cart.product }}</h2>
|
||||||
|
<h2 class="f-3 font-weight-bold">{{ product.title }}</h2>
|
||||||
|
<p>{{ product.extra | join: ',' }}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-12 col-md-4 pl-md-3">
|
||||||
|
<h2 class="f-3 font-weight-bold text-uppercase d-md-none">{{ site.cart.price }}</h2>
|
||||||
|
<p>
|
||||||
|
{{ product.line_item.attributes.price }}
|
||||||
|
{{ product.line_item.attributes.currency }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-12 col-md-4 pl-md-3">
|
||||||
|
<h2 class="f-3 font-weight-bold text-uppercase d-md-none">{{ site.cart.subtotal }}</h2>
|
||||||
|
<p>
|
||||||
|
{{ product.line_item.attributes.discounted_amount }}
|
||||||
|
{{ product.line_item.attributes.currency }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
|
||||||
|
<div class="col-6 col-md-8 text-right border-top border-red pt-3">
|
||||||
|
<p>
|
||||||
|
{{ site.cart.subtotal }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-6 col-md-4 border-top border-red pt-3 pl-3">
|
||||||
|
<p>
|
||||||
|
{{ order.total }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endraw %}
|
64
assets/templates/payment_methods.html
Normal file
64
assets/templates/payment_methods.html
Normal file
|
@ -0,0 +1,64 @@
|
||||||
|
{% raw %}
|
||||||
|
<form id="coupon" data-controller="cart-coupon" data-action="cart-coupon#apply"></form>
|
||||||
|
|
||||||
|
<form
|
||||||
|
data-action="cart-payment-methods#pay"
|
||||||
|
data-target="cart-payment-methods.form">
|
||||||
|
{% for payment_method in payment_methods %}
|
||||||
|
<div class="custom-control custom-radio mt-3">
|
||||||
|
<input
|
||||||
|
class="custom-control-input"
|
||||||
|
type="radio"
|
||||||
|
data-target="cart-payment-methods.method"
|
||||||
|
name="payment_method_id"
|
||||||
|
id="payment_method_id_{{ payment_method.id }}"
|
||||||
|
value="{{ payment_method.id }}"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<label
|
||||||
|
class="font-weight-bold custom-control-label"
|
||||||
|
for="payment_method_id_{{ payment_method.id }}">
|
||||||
|
<h3>{{ payment_method.attributes.name }}</h3>
|
||||||
|
|
||||||
|
<p class="lead">{{ payment_method.attributes.description }}</p>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
|
||||||
|
<div class="border-top pt-3 d-flex justify-content-between flex-wrap">
|
||||||
|
<div class="order-last order-md-first mt-3 mt-sm-0">
|
||||||
|
<a href="{{ back.url }}"
|
||||||
|
class="btn btn-transparent border border-black black text-uppercase">
|
||||||
|
{{ site.payment.back }}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="order-first order-md-last">
|
||||||
|
<details class="w-100 mb-3">
|
||||||
|
<summary class="blue"><small>{{ site.i18n.cart.layouts.payment.promo_code }}</small></summary>
|
||||||
|
|
||||||
|
<div class="d-flex">
|
||||||
|
{% comment %}
|
||||||
|
Estos elementos pertenecen al formulario de cupones
|
||||||
|
{% endcomment %}
|
||||||
|
<div class="flex-grow-1 mr-2">
|
||||||
|
<label for="coupon_code" class="sr-only">{{ site.payment.promo_code }}</label>
|
||||||
|
<input form="coupon" type="text" id="coupon_code" name="coupon_code" required class="form-control" />
|
||||||
|
<div id="coupon-code-invalid" class="invalid-feedback"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<input form="coupon" type="submit" value="Aplicar" class="btn btn-secondary" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<input type="submit"
|
||||||
|
class="btn btn-primary btn-lg text-uppercase"
|
||||||
|
data-target="cart-payment-methods.submit"
|
||||||
|
disabled
|
||||||
|
value="{{ site.payment.next_step }} {{ cart.data.attributes.total | floor }} {{ cart.data.attributes.currency }}"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
{% endraw %}
|
13
assets/templates/recover_order.html
Normal file
13
assets/templates/recover_order.html
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
{% raw %}
|
||||||
|
<div class="alert alert-{{ type }} alert-dismissible" role="alert" data-controller="cart">
|
||||||
|
{{ content }}
|
||||||
|
|
||||||
|
<button data-action="cart#recover notification#hide" type="button" class="btn btn-primary">
|
||||||
|
{{ site.i18n.recover_order }}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button data-action="notification#hide" type="button" class="close" aria-label="{{ site.i18n.close }}">
|
||||||
|
<span aria-hidden="true">×</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{% endraw %}
|
|
@ -1,4 +1,5 @@
|
||||||
<section class="row no-gutters justify-content-center">
|
{% raw %}
|
||||||
|
<section class="row no-gutters justify-content-center">
|
||||||
<div class="col-10">
|
<div class="col-10">
|
||||||
{% for item in results %}
|
{% for item in results %}
|
||||||
<article id="{{ item.slug }}">
|
<article id="{{ item.slug }}">
|
||||||
|
@ -14,4 +15,5 @@
|
||||||
</article>
|
</article>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
{% endraw %}
|
||||||
|
|
38
assets/templates/shipping_methods.html
Normal file
38
assets/templates/shipping_methods.html
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
{% raw %}
|
||||||
|
<form
|
||||||
|
data-action="cart-shipping#payment"
|
||||||
|
data-target="cart-shipping.rates">
|
||||||
|
<input type="hidden" name="id" value="{{ shipping_method.id }}" />
|
||||||
|
|
||||||
|
<div class="row no-gutters justify-content-center">
|
||||||
|
{% for shipping_rate in shipping_rates %}
|
||||||
|
<div class="col-12">
|
||||||
|
<div class="custom-control custom-radio mt-3">
|
||||||
|
<input
|
||||||
|
class="custom-control-input"
|
||||||
|
type="radio"
|
||||||
|
{% if shipping_rate.attributes.selected %}
|
||||||
|
checked
|
||||||
|
{% endif %}
|
||||||
|
name="selected_shipping_rate_id"
|
||||||
|
id="order_shipments_attributes_selected_shipping_rate_id_{{ shipping_rate.id }}"
|
||||||
|
value="{{ shipping_rate.id }}"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<label
|
||||||
|
class="font-weight-bold custom-control-label"
|
||||||
|
for="order_shipments_attributes_selected_shipping_rate_id_{{ shipping_rate.id }}">
|
||||||
|
<span class="lead">{{ shipping_rate.attributes.name }}</span>
|
||||||
|
<br/>
|
||||||
|
{{ shipping_rate.attributes.display_cost }}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
|
||||||
|
<div class="col-12 mt-3">
|
||||||
|
<input type="submit" class="btn btn-block btn-success" value="{{ site.shipment.next_step }}" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
{% endraw %}
|
49
env.js
49
env.js
|
@ -4,5 +4,50 @@
|
||||||
window.env = {
|
window.env = {
|
||||||
AIRBRAKE_PROJECT_ID: {{ site.env.AIRBRAKE_PROJECT_ID | default: 0 }},
|
AIRBRAKE_PROJECT_ID: {{ site.env.AIRBRAKE_PROJECT_ID | default: 0 }},
|
||||||
AIRBRAKE_PROJECT_KEY: '{{ site.env.AIRBRAKE_PROJECT_KEY }}',
|
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 }}'
|
||||||
|
};
|
||||||
|
|
||||||
|
{% 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 }}
|
||||||
|
};
|
||||||
|
|
||||||
|
{%- 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 -%}
|
||||||
|
{%- 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 }},
|
||||||
|
};
|
||||||
|
|
|
@ -13,11 +13,14 @@
|
||||||
"@babel/plugin-proposal-class-properties": "^7.10.4",
|
"@babel/plugin-proposal-class-properties": "^7.10.4",
|
||||||
"@babel/preset-env": "^7.10.4",
|
"@babel/preset-env": "^7.10.4",
|
||||||
"@hotwired/turbo": "^7.0.0-rc.4",
|
"@hotwired/turbo": "^7.0.0-rc.4",
|
||||||
|
"@spree/storefront-api-v2-sdk": "~4.4",
|
||||||
"axe-core": "^4.1.2",
|
"axe-core": "^4.1.2",
|
||||||
"babel-loader": "^8.1.0",
|
"babel-loader": "^8.1.0",
|
||||||
"core-js": "^3.6.5",
|
"core-js": "^3.6.5",
|
||||||
|
"device-detector-js": "^2.2.10",
|
||||||
"dotenv-webpack": "^6.0.0",
|
"dotenv-webpack": "^6.0.0",
|
||||||
"liquidjs": "^9.14.0",
|
"liquidjs": "^9.14.0",
|
||||||
|
"prettier": "^2.4.1",
|
||||||
"regenerator-runtime": "^0.13.5",
|
"regenerator-runtime": "^0.13.5",
|
||||||
"sassdoc": "^2.7.3",
|
"sassdoc": "^2.7.3",
|
||||||
"sassdoc-theme-herman": "^4.0.2",
|
"sassdoc-theme-herman": "^4.0.2",
|
||||||
|
|
|
@ -68,7 +68,10 @@ Gem::Specification.new do |spec|
|
||||||
spec.add_runtime_dependency 'jekyll-commonmark', '~> 1.3'
|
spec.add_runtime_dependency 'jekyll-commonmark', '~> 1.3'
|
||||||
spec.add_runtime_dependency 'jekyll-dotenv', '>= 0.2'
|
spec.add_runtime_dependency 'jekyll-dotenv', '>= 0.2'
|
||||||
spec.add_runtime_dependency 'jekyll-feed', '~> 0.15'
|
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'
|
spec.add_runtime_dependency 'jekyll-ignore-layouts', '~> 0'
|
||||||
|
spec.add_runtime_dependency 'jekyll-embed-urls', '~> 0'
|
||||||
|
|
||||||
# Dependencias de desarrollo
|
# Dependencias de desarrollo
|
||||||
spec.add_development_dependency 'bundler', '~> 2.1'
|
spec.add_development_dependency 'bundler', '~> 2.1'
|
||||||
|
|
51
yarn.lock
51
yarn.lock
|
@ -890,6 +890,15 @@
|
||||||
resolved "https://registry.yarnpkg.com/@sindresorhus/is/-/is-0.14.0.tgz#9fb3a3cf3132328151f353de4632e01e52102bea"
|
resolved "https://registry.yarnpkg.com/@sindresorhus/is/-/is-0.14.0.tgz#9fb3a3cf3132328151f353de4632e01e52102bea"
|
||||||
integrity sha512-9NET910DNaIPngYnLLPeg+Ogzqsi9uM4mSboU5y6p8S5DzMTVEsJZrawi+BoDNUVBa2DhJqQYUFvMDfgU062LQ==
|
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":
|
"@stimulus/core@^1.1.1":
|
||||||
version "1.1.1"
|
version "1.1.1"
|
||||||
resolved "https://registry.yarnpkg.com/@stimulus/core/-/core-1.1.1.tgz#42b0cfe5b73ca492f41de64b77a03980bae92c82"
|
resolved "https://registry.yarnpkg.com/@stimulus/core/-/core-1.1.1.tgz#42b0cfe5b73ca492f41de64b77a03980bae92c82"
|
||||||
|
@ -1393,6 +1402,13 @@ axe-core@^4.1.2:
|
||||||
resolved "https://registry.yarnpkg.com/axe-core/-/axe-core-4.2.1.tgz#2e50bcf10ee5b819014f6e342e41e45096239e34"
|
resolved "https://registry.yarnpkg.com/axe-core/-/axe-core-4.2.1.tgz#2e50bcf10ee5b819014f6e342e41e45096239e34"
|
||||||
integrity sha512-evY7DN8qSIbsW2H/TWQ1bX3sXN1d4MNb5Vb4n7BzPuCwRHdkZ1H2eNLuSh73EoQqkGKUtju2G2HCcjCfhvZIAA==
|
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:
|
babel-loader@^8.1.0:
|
||||||
version "8.2.2"
|
version "8.2.2"
|
||||||
resolved "https://registry.yarnpkg.com/babel-loader/-/babel-loader-8.2.2.tgz#9363ce84c10c9a40e6c753748e1441b60c8a0b81"
|
resolved "https://registry.yarnpkg.com/babel-loader/-/babel-loader-8.2.2.tgz#9363ce84c10c9a40e6c753748e1441b60c8a0b81"
|
||||||
|
@ -2519,6 +2535,11 @@ detect-node@^2.0.4:
|
||||||
resolved "https://registry.yarnpkg.com/detect-node/-/detect-node-2.1.0.tgz#c9c70775a49c3d03bc2c06d9a73be550f978f8b1"
|
resolved "https://registry.yarnpkg.com/detect-node/-/detect-node-2.1.0.tgz#c9c70775a49c3d03bc2c06d9a73be550f978f8b1"
|
||||||
integrity sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==
|
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:
|
diffie-hellman@^5.0.0:
|
||||||
version "5.0.3"
|
version "5.0.3"
|
||||||
resolved "https://registry.yarnpkg.com/diffie-hellman/-/diffie-hellman-5.0.3.tgz#40e8ee98f55a2149607146921c63e1ae5f3d2875"
|
resolved "https://registry.yarnpkg.com/diffie-hellman/-/diffie-hellman-5.0.3.tgz#40e8ee98f55a2149607146921c63e1ae5f3d2875"
|
||||||
|
@ -3050,7 +3071,7 @@ flush-write-stream@^1.0.0, flush-write-stream@^1.0.2:
|
||||||
inherits "^2.0.3"
|
inherits "^2.0.3"
|
||||||
readable-stream "^2.3.6"
|
readable-stream "^2.3.6"
|
||||||
|
|
||||||
follow-redirects@^1.0.0:
|
follow-redirects@^1.0.0, follow-redirects@^1.10.0:
|
||||||
version "1.14.1"
|
version "1.14.1"
|
||||||
resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.14.1.tgz#d9114ded0a1cfdd334e164e6662ad02bfd91ff43"
|
resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.14.1.tgz#d9114ded0a1cfdd334e164e6662ad02bfd91ff43"
|
||||||
integrity sha512-HWqDgT7ZEkqRzBvc2s64vSZ/hfOceEol3ac/7tKwzuvEyWx3/4UegXh5oBOIotkGsObyk3xznnSRVADBgWSQVg==
|
integrity sha512-HWqDgT7ZEkqRzBvc2s64vSZ/hfOceEol3ac/7tKwzuvEyWx3/4UegXh5oBOIotkGsObyk3xznnSRVADBgWSQVg==
|
||||||
|
@ -4294,7 +4315,7 @@ lodash.uniq@^4.5.0:
|
||||||
resolved "https://registry.yarnpkg.com/lodash.uniq/-/lodash.uniq-4.5.0.tgz#d0225373aeb652adc1bc82e4945339a842754773"
|
resolved "https://registry.yarnpkg.com/lodash.uniq/-/lodash.uniq-4.5.0.tgz#d0225373aeb652adc1bc82e4945339a842754773"
|
||||||
integrity sha1-0CJTc662Uq3BvILklFM5qEJ1R3M=
|
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"
|
version "4.17.21"
|
||||||
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c"
|
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c"
|
||||||
integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==
|
integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==
|
||||||
|
@ -4783,6 +4804,11 @@ object-copy@^0.1.0:
|
||||||
define-property "^0.2.5"
|
define-property "^0.2.5"
|
||||||
kind-of "^3.0.3"
|
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:
|
object-is@^1.0.1:
|
||||||
version "1.1.5"
|
version "1.1.5"
|
||||||
resolved "https://registry.yarnpkg.com/object-is/-/object-is-1.1.5.tgz#b9deeaa5fc7f1846a0faecdceec138e5778f53ac"
|
resolved "https://registry.yarnpkg.com/object-is/-/object-is-1.1.5.tgz#b9deeaa5fc7f1846a0faecdceec138e5778f53ac"
|
||||||
|
@ -5135,6 +5161,11 @@ prepend-http@^2.0.0:
|
||||||
resolved "https://registry.yarnpkg.com/prepend-http/-/prepend-http-2.0.0.tgz#e92434bfa5ea8c19f41cdfd401d741a3c819d897"
|
resolved "https://registry.yarnpkg.com/prepend-http/-/prepend-http-2.0.0.tgz#e92434bfa5ea8c19f41cdfd401d741a3c819d897"
|
||||||
integrity sha1-6SQ0v6XqjBn0HN/UAddBo8gZ2Jc=
|
integrity sha1-6SQ0v6XqjBn0HN/UAddBo8gZ2Jc=
|
||||||
|
|
||||||
|
prettier@^2.4.1:
|
||||||
|
version "2.4.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.4.1.tgz#671e11c89c14a4cfc876ce564106c4a6726c9f5c"
|
||||||
|
integrity sha512-9fbDAXSBcc6Bs1mZrDYb3XKzDLm4EXXL9sC1LqKP5rZkT6KRr/rf9amVUcODVXgguK/isJz0d0hP72WeaKWsvA==
|
||||||
|
|
||||||
process-nextick-args@^2.0.0, process-nextick-args@~2.0.0:
|
process-nextick-args@^2.0.0, process-nextick-args@~2.0.0:
|
||||||
version "2.0.1"
|
version "2.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2"
|
resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2"
|
||||||
|
@ -5247,6 +5278,13 @@ qs@6.7.0:
|
||||||
resolved "https://registry.yarnpkg.com/qs/-/qs-6.7.0.tgz#41dc1a015e3d581f1621776be31afb2876a9b1bc"
|
resolved "https://registry.yarnpkg.com/qs/-/qs-6.7.0.tgz#41dc1a015e3d581f1621776be31afb2876a9b1bc"
|
||||||
integrity sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ==
|
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:
|
querystring-es3@^0.2.0:
|
||||||
version "0.2.1"
|
version "0.2.1"
|
||||||
resolved "https://registry.yarnpkg.com/querystring-es3/-/querystring-es3-0.2.1.tgz#9ec61f79049875707d69414596fd907a4d711e73"
|
resolved "https://registry.yarnpkg.com/querystring-es3/-/querystring-es3-0.2.1.tgz#9ec61f79049875707d69414596fd907a4d711e73"
|
||||||
|
@ -5890,6 +5928,15 @@ shebang-regex@^1.0.0:
|
||||||
resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-1.0.0.tgz#da42f49740c0b42db2ca9728571cb190c98efea3"
|
resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-1.0.0.tgz#da42f49740c0b42db2ca9728571cb190c98efea3"
|
||||||
integrity sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=
|
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:
|
signal-exit@^3.0.0, signal-exit@^3.0.2:
|
||||||
version "3.0.3"
|
version "3.0.3"
|
||||||
resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.3.tgz#a1410c2edd8f077b08b4e253c8eacfcaf057461c"
|
resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.3.tgz#a1410c2edd8f077b08b4e253c8eacfcaf057461c"
|
||||||
|
|
Loading…
Reference in a new issue