diff --git a/src/templates/_components/app/line-item-cart.twig b/src/templates/_components/app/line-item-cart.twig index 8923bbd..9e6cb62 100644 --- a/src/templates/_components/app/line-item-cart.twig +++ b/src/templates/_components/app/line-item-cart.twig @@ -1,9 +1,12 @@ -{% set minQty = lineItem.purchasable.minQty %} -{% set maxQty = lineItem.purchasable.maxQty %} -{% set stock = lineItem.purchasable.stock %} -{% set unlimitedStock = lineItem.purchasable.hasUnlimitedStock ? 1 : 0 %} -{% set lineSubtotal = lineItem.subtotalAsCurrency() %} +{# Get preview images for lineItems #} + +{# +=============================================================================================================================== +Commented out temporarily (It's currently difficult to tinker around i.e passing the lineItem variable from javascript to twig ) +I can't think of anyother way we can be able to guarantee reactivity of all items in the cart without ensuring all items in the +Cart are controlled from Petite Vue +=============================================================================================================================== {% set imageUrl = null %} {% set imageSrcSet = null %} @@ -19,167 +22,180 @@ {% set imageUrl = previewImage.getUrl(preview) %} {% set imageSrcSet = previewImage.getSrcSet(['80', '200']) %} {% endif %} +#} -
- -
-
- {{ 'Image of'|t('foster-checkout') ~ ' ' ~ lineItem.description }} -
-
- -
- -
- -
-

{{ lineItem.description }}

-

{{ 'SKU'|t('foster-checkout') }}: {{ lineItem.sku }}

- {% if lineItem.purchasable.onPromotion %} -

- {{ lineItem.purchasable.priceAsCurrency }} {{ lineItem.purchasable.salePriceAsCurrency }} -

- {% else %} -

{{ lineItem.purchasable.priceAsCurrency }}

- {% endif %} -
+{% set imageUrl = null %} +{% set imageSrcSet = null %} -
- -
- {{ csrfInput() }} - {{ actionInput('commerce/cart/update-cart') }} -
- - +
  • +
    +
    +
    + {{ 'Image of'|t('foster-checkout') ~ ' ' ~ +
    +
    + +
    + +
    + +
    +

    +

    {{ 'SKU'|t('foster-checkout') }}: ${ lineItem.sku }

    + + +
    - - - - + + + + + + +
    + + +
    +

    {{ 'You must purchase at least ${min} of this item' | t('foster-checkout') }}

    + +

    {{ 'You may only purchase ${max} of this item' | t('foster-checkout') }}

    + +

    {{ 'Only ${stock} in stock' | t('foster-checkout') }}

    +
    + + +
    + +
    - - -
    -

    {{ 'You must purchase at least {qty} of this item' | t('foster-checkout', params = { qty: minQty }) }}

    - -

    {{ 'You may only purchase {qty} of this item' | t('foster-checkout', params = { qty: maxQty}) }}

    - -

    {{ 'Only {qty} in stock' | t('foster-checkout', params = { qty: stock }) }}

    -
    - - -
    - {% if lineItem.onPromotion or lineItem.discount or lineItem.purchasable.onPromotion %} -

    {{ (lineItem.purchasable.promotionalPrice * lineItem.qty)|commerceCurrency(cart.paymentCurrency) }}

    -
    {{ lineItem.subtotalAsCurrency() }}
    - {% else %} -

    {{ lineItem.subtotalAsCurrency() }} -

    - {% endif %}
    -
  • + {# + ====================================================================== + Commented out temporarily (Most properties are only available in twig ) + ====================================================================== + + #} -
    +
    -
    + + + diff --git a/src/templates/_components/app/summary-cart.twig b/src/templates/_components/app/summary-cart.twig index d69dbea..653f337 100644 --- a/src/templates/_components/app/summary-cart.twig +++ b/src/templates/_components/app/summary-cart.twig @@ -1,4 +1,4 @@ -
    +

    {{ 'Order Summary'|t('foster-checkout') }}

    @@ -6,18 +6,18 @@
    {{ 'Subtotal'|t('foster-checkout') }}
    -
    {{ cart.itemSubtotalAsCurrency }}
    +
    {% if cart.totalDiscount != 0 %}
    {{ 'Discount'|t('foster-checkout') }}
    -
    {{ cart.totalDiscountAsCurrency }}
    +
    {{ 'Subtotal After Discount'|t('foster-checkout') }}
    -
    {{ cart.totalAsCurrency }}
    +
    {% endif %} @@ -34,14 +34,14 @@ {% else %}
    {{ 'Shipping'|t('foster-checkout') }}
    -
    {{ cart.totalShippingCostAsCurrency }}
    +
    {% endif %}
    {{ 'Estimated Tax'|t('foster-checkout') }}
    -
    {{ cart.totalTaxAsCurrency }}
    +
    {% include 'foster-checkout/_components/app/coupon-code' %} @@ -49,12 +49,12 @@ {% if step|default('') != 'payment' %}
    {{ 'Estimated Total'|t('foster-checkout') }}:
    -
    {{ cart.totalPriceAsCurrency }}
    +
    {% else %}
    {{ 'Total'|t('foster-checkout') }}:
    -
    {{ cart.totalPriceAsCurrency }}
    +
    {% endif %} diff --git a/src/templates/cart/index.twig b/src/templates/cart/index.twig index c8bb4c0..72bc468 100644 --- a/src/templates/cart/index.twig +++ b/src/templates/cart/index.twig @@ -3,8 +3,8 @@ {% block body %} {% if cart.lineItems|length > 0 %} -
    @@ -18,7 +18,7 @@ class="text-gray-500" title="{{ 'There are {qty} items in your cart'|t('foster-checkout', params = { qty: cart.totalQty }) }}" > - ({{ cart.totalQty }}) + (${App.cartTotalQty})

    @@ -31,13 +31,7 @@
      - {% for lineItem in cart.lineItems %} -
    • - {% include 'foster-checkout/_components/app/line-item-cart' with { - lineItem: lineItem - } %} -
    • - {% endfor %} + {% include 'foster-checkout/_components/app/line-item-cart' %}
    diff --git a/src/web/assets/checkout/dist/js/app.js b/src/web/assets/checkout/dist/js/app.js index 727f9d7..57cce96 100644 --- a/src/web/assets/checkout/dist/js/app.js +++ b/src/web/assets/checkout/dist/js/app.js @@ -1,200 +1,218 @@ -import { createApp, reactive } from 'https://unpkg.com/petite-vue?module'; +import { createApp, reactive } from "https://unpkg.com/petite-vue?module"; const App = reactive({ - loaded: false, + loaded: false, - init() { - this.loaded = true; - Hide(); - } + cart: [], + + get cartTotalQty() { + const totalQty = this.cart?.lineItems?.length + ? this.cart?.lineItems.reduce((accumulator, lineItem) => { + return accumulator + lineItem.qty; + }, 0) + : 0; + return totalQty; + }, + + init() { + this.loaded = true; + Hide(); + }, }); const ClearableInput = (props) => { - return { - name: props.name, - value: props.value, - errors: props.errors, - showButton: false, - input() { - this.showButton = this.value !== ''; - this.errors = []; - }, - focus() { - this.showButton = this.value !== ''; - }, - blur($el) { - const button = $el.closest('div').querySelector('button'); - this.showButton = (button === document.activeElement) && (this.value !== ''); - }, - clear($el) { - this.value = ''; - this.errors = []; - const input = $el.closest('div').querySelector('input.js-clearable, textarea.js-clearable'); - input.focus(); - }, - } + return { + name: props.name, + value: props.value, + errors: props.errors, + showButton: false, + input() { + this.showButton = this.value !== ""; + this.errors = []; + }, + focus() { + this.showButton = this.value !== ""; + }, + blur($el) { + const button = $el.closest("div").querySelector("button"); + this.showButton = button === document.activeElement && this.value !== ""; + }, + clear($el) { + this.value = ""; + this.errors = []; + const input = $el + .closest("div") + .querySelector("input.js-clearable, textarea.js-clearable"); + input.focus(); + }, + }; }; const LineItem = (props) => { - return { - id: props.lineItemId, - qty: props.qty, - min: props.min, - max: props.max, - stock: props.stock, - lineSubtotal: props.lineSubtotal, - unlimitedStock: props.unlimitedStock, - showErrorMaxMessage: props.showErrorMaxMessage, - showErrorMinMessage: props.showErrorMinMessage, - showErrorStockMessage: props.showErrorStockMessage, - input() { - this.qty = this.qty.replace(/\D/g,''); - this.updateQty(); - }, - increment() { - this.removeMessages() - this.qty++; - this.updateQty(); - }, - decrement() { - this.removeMessages() - if(this.qty === 1) { - this.remove(); - } else { - this.qty = this.qty > 1 ? (this.qty - 1) : 1; - this.updateQty(); - } - }, - async remove() { - const form = document.querySelector(`#lineItemQty-${props.id}`); - const formData = new FormData(form) - formData.set(`lineItems[${props.id}][remove]`, true); + return { + id: props.lineItemId, + qty: props.qty, + min: props.min, + max: props.max, + stock: props.stock, + lineSubtotal: props.lineSubtotal, + unlimitedStock: props.unlimitedStock, + showErrorMaxMessage: props.showErrorMaxMessage, + showErrorMinMessage: props.showErrorMinMessage, + showErrorStockMessage: props.showErrorStockMessage, + input() { + this.qty = this.qty.replace(/\D/g, ""); + this.updateQty(); + }, + increment() { + this.removeMessages(); + this.qty++; + this.updateQty(); + }, + decrement() { + this.removeMessages(); + if (this.qty === 1) { + this.remove(); + } else { + this.qty = this.qty > 1 ? this.qty - 1 : 1; + this.updateQty(); + } + }, + async remove() { + const form = document.querySelector(`#lineItemQty-${props.id}`); + const formData = new FormData(form); + formData.set(`lineItems[${props.id}][remove]`, true); - await fetch('/actions/commerce/cart/update-cart', { - method: 'POST', - headers: { - 'Accept': 'application/json', - 'X-Requested-With': 'XMLHttpRequest' - }, - body: formData, - }) - .then(response => { - if (!response.ok) { - throw new Error('Network response was not ok'); - } - return response.json(); - }) - .then(data => { - // we should only do this if the ajax operation was successful - const container = form.closest('article'); - container.remove(); - }) - .catch(error => { - console.error('Error:', error); - }); + await fetch("/actions/commerce/cart/update-cart", { + method: "POST", + headers: { + Accept: "application/json", + "X-Requested-With": "XMLHttpRequest", + }, + body: formData, + }) + .then((response) => { + if (!response.ok) { + throw new Error("Network response was not ok"); + } + return response.json(); + }) + .then((data) => { + // we should only do this if the ajax operation was successful + const container = form.closest("article"); + container.remove(); + }) + .catch((error) => { + console.error("Error:", error); + }); + }, + blur() { + this.qty = this.qty === "" ? 0 : this.qty; + }, + removeMessages() { + this.showErrorMaxMessage = false; + this.showErrorMinMessage = false; + this.showErrorStockMessage = false; + }, + async updateQty() { + if (this.unlimitedStock === 0 && this.qty > this.stock) { + this.qty = this.stock; + this.showErrorStockMessage = true; + } else if (this.qty < this.min && this.min != 0) { + this.qty = this.min; + this.showErrorMinMessage = true; + } else if (this.unlimitedStock && this.max && this.qty > this.max) { + this.qty = this.max; + this.showErrorMaxMessage = true; + } else { + props.qty = this.qty; + // UpdateQty(props); - }, - blur() { - this.qty = this.qty === '' ? 0 : this.qty; - }, - removeMessages() { - this.showErrorMaxMessage = false; - this.showErrorMinMessage = false; - this.showErrorStockMessage = false; - }, - async updateQty() { - if(this.unlimitedStock === 0 && this.qty > this.stock) { - this.qty = this.stock; - this.showErrorStockMessage = true; - } else if(this.qty < this.min && this.min != 0) { - this.qty = this.min; - this.showErrorMinMessage = true; - } else if(this.unlimitedStock && this.max && this.qty > this.max) { - this.qty = this.max; - this.showErrorMaxMessage = true; - } else { - props.qty = this.qty; - // UpdateQty(props); + const form = document.querySelector(`#lineItemQty-${props.id}`); - const form = document.querySelector(`#lineItemQty-${props.id}`); - const formData = new FormData(form) - formData.set(`lineItems[${props.id}][qty]`, props.qty); + const formData = new FormData(form); + formData.set(`lineItems[${props.id}][qty]`, props.qty); - await fetch('/actions/commerce/cart/update-cart', { - method: 'POST', - headers: { - 'Accept': 'application/json', - 'X-Requested-With': 'XMLHttpRequest' - }, - body: formData, - }) - .then(response => { - if (!response.ok) { - throw new Error('Network response was not ok'); - } - return response.json(); - }) - .then(data => { - let item = data.cart.lineItems.filter((lineItem) => lineItem.id === props.id) - this.lineSubtotal = item[0].subtotalAsCurrency; - }) - .catch(error => { - console.error('Error:', error); - }); - } - } - } + await fetch("/actions/commerce/cart/update-cart", { + method: "POST", + headers: { + Accept: "application/json", + "X-Requested-With": "XMLHttpRequest", + }, + body: formData, + }) + .then((response) => { + if (!response.ok) { + throw new Error("Network response was not ok"); + } + return response.json(); + }) + .then((data) => { + // Assign cart response to reactive state + App.cart = data.cart; + let item = data.cart.lineItems.filter( + (lineItem) => lineItem.id === props.id, + ); + this.lineSubtotal = item[0].subtotalAsCurrency; + }) + .catch((error) => { + console.error("Error:", error); + }); + } + }, + }; }; - const FocusModal = (props) => { - return { - isOpen: props.isOpen, - toggleModal() { - this.isOpen = !this.isOpen; - }, - mounted() { - const modal = this.$refs.modal; - const focusableEls = modal.querySelectorAll('a[href]:not([disabled]), button:not([disabled]), textarea:not([disabled]), input[type="text"]:not([disabled]), input[type="radio"]:not([disabled]), input[type="checkbox"]:not([disabled]), select:not([disabled])'); - const firstFocusableEl = focusableEls[0]; - const lastFocusableEl = focusableEls[focusableEls.length - 1]; - const KEYCODE_TAB = 9; + return { + isOpen: props.isOpen, + toggleModal() { + this.isOpen = !this.isOpen; + }, + mounted() { + const modal = this.$refs.modal; + const focusableEls = modal.querySelectorAll( + 'a[href]:not([disabled]), button:not([disabled]), textarea:not([disabled]), input[type="text"]:not([disabled]), input[type="radio"]:not([disabled]), input[type="checkbox"]:not([disabled]), select:not([disabled])', + ); + const firstFocusableEl = focusableEls[0]; + const lastFocusableEl = focusableEls[focusableEls.length - 1]; + const KEYCODE_TAB = 9; - modal.addEventListener('keydown', (evt) => { - const isTab = (evt.key === 'Tab' || evt.keyCode === KEYCODE_TAB); - if (isTab) { - if (evt.shiftKey) { - if (document.activeElement === firstFocusableEl) { - lastFocusableEl.focus(); - evt.preventDefault(); - } - } else { - if (document.activeElement === lastFocusableEl) { - firstFocusableEl.focus(); - evt.preventDefault(); - } - } - } - }); - } - } + modal.addEventListener("keydown", (evt) => { + const isTab = evt.key === "Tab" || evt.keyCode === KEYCODE_TAB; + if (isTab) { + if (evt.shiftKey) { + if (document.activeElement === firstFocusableEl) { + lastFocusableEl.focus(); + evt.preventDefault(); + } + } else { + if (document.activeElement === lastFocusableEl) { + firstFocusableEl.focus(); + evt.preventDefault(); + } + } + } + }); + }, + }; }; // Hide any elements that we only show if JS doesn't load const Hide = () => { - const els = document.querySelectorAll('.js-hide') - els.forEach((el) => el.classList.add('hidden')); -} + const els = document.querySelectorAll(".js-hide"); + els.forEach((el) => el.classList.add("hidden")); +}; // Initialize Petite Vue and mount createApp({ - // Set delimiters in Petite Vue so they do not interfere with Twig delimiters - $delimiters: ['${', '}'], + // Set delimiters in Petite Vue so they do not interfere with Twig delimiters + $delimiters: ["${", "}"], - // Include the modules above - App, - ClearableInput, - FocusModal, - LineItem, - Hide + // Include the modules above + App, + ClearableInput, + FocusModal, + LineItem, + Hide, }).mount();