diff --git a/.eslintcache b/.eslintcache deleted file mode 100644 index cd006ada..00000000 --- a/.eslintcache +++ /dev/null @@ -1 +0,0 @@ -[{"/Users/martibelegu/Documents/Xfive/xcorner/assets/js/theme/common/hooks/faceted-search.js":"1","/Users/martibelegu/Documents/Xfive/xcorner/assets/js/theme/common/faceted-search.js":"2","/Users/martibelegu/Documents/Xfive/xcorner/assets/js/theme/custom/category-filters.js":"3","/Users/martibelegu/Documents/Xfive/xcorner/assets/js/theme/common/carousel.js":"4","/Users/martibelegu/Documents/Xfive/xcorner/assets/js/theme/common/swiper-carousel/index.js":"5"},{"size":754,"mtime":1707849697163,"results":"6","hashOfConfig":"7"},{"size":15986,"mtime":1707835370540,"results":"8","hashOfConfig":"7"},{"size":1062,"mtime":1707834649422,"results":"9","hashOfConfig":"7"},{"size":2785,"mtime":1708944127430,"results":"10","hashOfConfig":"11"},{"size":2190,"mtime":1708944127430,"results":"12","hashOfConfig":"11"},{"filePath":"13","messages":"14","suppressedMessages":"15","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},"1gd3vyx",{"filePath":"16","messages":"17","suppressedMessages":"18","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"19","messages":"20","suppressedMessages":"21","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"22","messages":"23","suppressedMessages":"24","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},"1hal5ky",{"filePath":"25","messages":"26","suppressedMessages":"27","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},"/Users/martibelegu/Documents/Xfive/xcorner/assets/js/theme/common/hooks/faceted-search.js",[],[],"/Users/martibelegu/Documents/Xfive/xcorner/assets/js/theme/common/faceted-search.js",[],["28","29"],"/Users/martibelegu/Documents/Xfive/xcorner/assets/js/theme/custom/category-filters.js",[],[],"/Users/martibelegu/Documents/Xfive/xcorner/assets/js/theme/common/carousel.js",[],["30","31","32","33","34","35","36"],"/Users/martibelegu/Documents/Xfive/xcorner/assets/js/theme/common/swiper-carousel/index.js",[],["37","38","39","40"],{"ruleId":"41","severity":2,"message":"42","line":236,"column":17,"nodeType":"43","messageId":"44","endLine":236,"endColumn":25,"suppressions":"45"},{"ruleId":"41","severity":2,"message":"42","line":238,"column":17,"nodeType":"43","messageId":"44","endLine":238,"endColumn":25,"suppressions":"46"},{"ruleId":"47","severity":2,"message":"48","line":5,"column":8,"nodeType":"49","endLine":5,"endColumn":21,"suppressions":"50"},{"ruleId":"47","severity":2,"message":"51","line":6,"column":8,"nodeType":"49","endLine":6,"endColumn":32,"suppressions":"52"},{"ruleId":"47","severity":2,"message":"53","line":7,"column":8,"nodeType":"49","endLine":7,"endColumn":31,"suppressions":"54"},{"ruleId":"55","severity":2,"message":"56","line":34,"column":9,"nodeType":"57","messageId":"58","endLine":34,"endColumn":58,"suppressions":"59"},{"ruleId":"55","severity":2,"message":"56","line":92,"column":9,"nodeType":"57","messageId":"58","endLine":92,"endColumn":58,"suppressions":"60"},{"ruleId":"55","severity":2,"message":"56","line":96,"column":9,"nodeType":"57","messageId":"58","endLine":96,"endColumn":53,"suppressions":"61"},{"ruleId":"55","severity":2,"message":"56","line":100,"column":9,"nodeType":"57","messageId":"58","endLine":100,"endColumn":64,"suppressions":"62"},{"ruleId":"47","severity":2,"message":"48","line":5,"column":8,"nodeType":"49","endLine":5,"endColumn":21,"suppressions":"63"},{"ruleId":"47","severity":2,"message":"51","line":6,"column":8,"nodeType":"49","endLine":6,"endColumn":32,"suppressions":"64"},{"ruleId":"47","severity":2,"message":"53","line":7,"column":8,"nodeType":"49","endLine":7,"endColumn":31,"suppressions":"65"},{"ruleId":"47","severity":2,"message":"66","line":8,"column":8,"nodeType":"49","endLine":8,"endColumn":25,"suppressions":"67"},"no-param-reassign","Assignment to property of function parameter '$element'.","Identifier","assignmentToFunctionParamProp",["68"],["69"],"import/no-unresolved","Unable to resolve path to module 'swiper/scss'.","Literal",["70"],"Unable to resolve path to module 'swiper/scss/navigation'.",["71"],"Unable to resolve path to module 'swiper/css/pagination'.",["72"],"no-new","Do not use 'new' for side effects.","ExpressionStatement","noNewStatement",["73"],["74"],["75"],["76"],["77"],["78"],["79"],"Unable to resolve path to module 'swiper/css/grid'.",["80"],{"kind":"81","justification":"82"},{"kind":"81","justification":"82"},{"kind":"81","justification":"82"},{"kind":"81","justification":"82"},{"kind":"81","justification":"82"},{"kind":"81","justification":"82"},{"kind":"81","justification":"82"},{"kind":"81","justification":"82"},{"kind":"81","justification":"82"},{"kind":"81","justification":"82"},{"kind":"81","justification":"82"},{"kind":"81","justification":"82"},{"kind":"81","justification":"82"},"directive",""] \ No newline at end of file diff --git a/assets/icons/decrease-quantity.svg b/assets/icons/decrease-quantity.svg new file mode 100644 index 00000000..a3b8095b --- /dev/null +++ b/assets/icons/decrease-quantity.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/icons/increase-quantity.svg b/assets/icons/increase-quantity.svg new file mode 100644 index 00000000..f48a512d --- /dev/null +++ b/assets/icons/increase-quantity.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/icons/star-filled.svg b/assets/icons/star-filled.svg new file mode 100644 index 00000000..06dba74d --- /dev/null +++ b/assets/icons/star-filled.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/icons/star-unfilled.svg b/assets/icons/star-unfilled.svg new file mode 100644 index 00000000..271f39a5 --- /dev/null +++ b/assets/icons/star-unfilled.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/icons/trash.svg b/assets/icons/trash.svg new file mode 100644 index 00000000..1a5f51f4 --- /dev/null +++ b/assets/icons/trash.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/img/icon-sprite.svg b/assets/img/icon-sprite.svg index 53e6139c..c043e003 100644 --- a/assets/img/icon-sprite.svg +++ b/assets/img/icon-sprite.svg @@ -1 +1,116 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/assets/js/theme/cart.js b/assets/js/theme/cart.js index a0fdd6f8..274075ef 100644 --- a/assets/js/theme/cart.js +++ b/assets/js/theme/cart.js @@ -10,6 +10,7 @@ import CartItemDetails from './common/cart-item-details' import q$, { q$$ } from './global/selector' import trigger from './common/utils/trigger' import toggle from './global/toggle' +import addReviewsToCartItems from './custom/add-reviews-to-cart-items' export default class Cart extends PageManager { onReady() { @@ -30,6 +31,10 @@ export default class Cart extends PageManager { this.setApplePaySupport() this.bindEvents() + + const { storefrontApiToken } = this.context + + addReviewsToCartItems(storefrontApiToken) } setApplePaySupport() { @@ -261,15 +266,16 @@ export default class Cart extends PageManager { let preVal // cart update - q$('.js-cart-update', this.$cartContent)?.addEventListener('click', (event) => { - const $target = event.currentTarget + q$$('.js-cart-update', this.$cartContent)?.forEach(($btn) => { + $btn.addEventListener('click', (event) => { + const $target = event.currentTarget - event.preventDefault() + event.preventDefault() - // update cart quantity - cartUpdate($target) + // update cart quantity + cartUpdate($target) + }) }) - // cart qty manually updates q$$('.js-cart-item-qty-input', this.$cartContent).forEach(($input) => { $input.addEventListener('focus', function onQtyFocus() { diff --git a/assets/js/theme/cart/shipping-estimator.js b/assets/js/theme/cart/shipping-estimator.js index 5fd0138b..1699c666 100644 --- a/assets/js/theme/cart/shipping-estimator.js +++ b/assets/js/theme/cart/shipping-estimator.js @@ -27,7 +27,7 @@ export default class ShippingEstimator { tap: announceInputErrorMessage, }) - q$('.js-shipping-estimate-submit', this.$element)?.addEventListener('click', (event) => { + q$('.js-shipping-estimate-submit', this.$element).addEventListener('click', (event) => { // estimator error messages are being injected in html as a result // of user submit; clearing and adding role on submit provides // regular announcement of these error messages @@ -140,7 +140,10 @@ export default class ShippingEstimator { // When you change a country, you swap the state/province between an input and a select dropdown // Not all countries require the province to be filled // We have to remove this class when we swap since nod validation doesn't cleanup for us - q$(this.shippingEstimator).querySelector('.js-form-field-success').classList.remove('js-form-field-success') + const shippingEstimator = q$(this.shippingEstimator) + if (shippingEstimator) { + shippingEstimator.querySelector('.js-form-field-success')?.classList.remove('js-form-field-success') + } }) } @@ -177,7 +180,7 @@ export default class ShippingEstimator { event.preventDefault() utils.api.cart.getShippingQuotes(params, 'cart/shipping-quotes', (err, response) => { - q$('.js-shipping-quotes').innerHTML = response.content + q$('.js-shipping-quotes').innerHTML = response?.content // bind the select button q$('.js-select-shipping-quote').addEventListener('click', (clickEvent) => { diff --git a/assets/js/theme/common/state-country.js b/assets/js/theme/common/state-country.js index c61b4e0f..0eae5eec 100644 --- a/assets/js/theme/common/state-country.js +++ b/assets/js/theme/common/state-country.js @@ -8,26 +8,27 @@ import q$, { q$$ } from '../global/selector' * If there are no options from bcapp, a text field will be sent. This will create a select element to hold options after the remote request. * @returns {HTMLElement} */ -function makeStateRequired(stateElement, context) { +function makeStateRequired(stateElement) { /* eslint-disable no-param-reassign */ - stateElement.innerHTML = ` - - ` + const selectElement = document.createElement('select') + + selectElement.className = 'c-form__input c-form__input--select u-width-full' + + selectElement.id = stateElement.id + selectElement.name = stateElement.getAttribute('name') + selectElement.setAttribute('data-label', stateElement.dataset.label) + selectElement.setAttribute('data-field-type', stateElement.dataset.fieldType) + + stateElement.replaceWith(selectElement) const $hiddenInput = q$$('[name*="FormFieldIsText"]') $hiddenInput.forEach(($hi) => $hi.remove()) const $newElement = q$('[data-field-type="State"]') const $prevElement = $newElement.previousElementSibling - if ($prevElement.querySelector('small') === null) { + if ($prevElement?.querySelector('small') === null) { // String is injected from localizer - $prevElement.insertAdjacentHTML('beforeend', `${context.required}`) + $prevElement.insertAdjacentHTML('beforeend', `*`) } else { $prevElement.querySelector('small').style.display = 'block' } @@ -43,22 +44,25 @@ function makeStateRequired(stateElement, context) { */ function makeStateOptional(stateElement) { /* eslint-disable no-param-reassign */ - stateElement.innerHTML = ` - - ` + const inputElement = document.createElement('input') + inputElement.type = 'text' + + inputElement.className = 'js-form-input c-form__input u-width-full' + + inputElement.id = stateElement.id + inputElement.name = stateElement.getAttribute('name') + inputElement.setAttribute('data-label', stateElement.dataset.label) + inputElement.setAttribute('data-field-type', stateElement.dataset.fieldType) + + stateElement.replaceWith(inputElement) const $newElement = q$('[data-field-type="State"]') if ($newElement !== null) { insertStateHiddenField($newElement) - $newElement.previousElementSibling.querySelector('small').style.display = 'none' + if ($newElement.previousElementSibling?.querySelector('small')) { + $newElement.previousElementSibling.querySelector('small').style.display = 'none' + } } return $newElement @@ -75,7 +79,7 @@ function addOptions(statesArray, $selectElement, options) { container.push(``) - if (!isEmpty($selectElement)) { + if (isEmpty($selectElement)) { statesArray.states.forEach((stateObj) => { if (options.useIdForStates) { container.push(``) diff --git a/assets/js/theme/common/utils/form-utils.js b/assets/js/theme/common/utils/form-utils.js index e1986e57..e97efffa 100644 --- a/assets/js/theme/common/utils/form-utils.js +++ b/assets/js/theme/common/utils/form-utils.js @@ -321,13 +321,14 @@ const Validators = { * @param field */ cleanUpStateValidation: (field) => { - const $fieldClassElement = q$(`[data-type="${field.dataset.fieldType}"]`) - - Object.keys(nod.classes).forEach((value) => { - if ($fieldClassElement.classList.contains(nod.classes[value])) { - $fieldClassElement.classList.remove(nod.classes[value]) - } - }) + if (field.dataset.fieldType) { + const $fieldClassElement = q$(`[data-field-type="${field.dataset.fieldType}"]`) + Object.keys(nod.classes).forEach((value) => { + if ($fieldClassElement.classList.contains(nod.classes[value])) { + $fieldClassElement.classList.remove(nod.classes[value]) + } + }) + } }, } diff --git a/assets/js/theme/custom/add-reviews-to-cart-items.js b/assets/js/theme/custom/add-reviews-to-cart-items.js new file mode 100644 index 00000000..b8d93be4 --- /dev/null +++ b/assets/js/theme/custom/add-reviews-to-cart-items.js @@ -0,0 +1,56 @@ +/** + * Add reviews to cart items via GraphQL + */ +export default function addReviewsToCartItems(storefrontApiToken) { + const cartItems = document.querySelectorAll('.js-item-row') + if (cartItems.length > 0) { + cartItems.forEach((product) => { + const productId = product.getAttribute('data-type-product-id') + fetch('/graphql', { + method: 'POST', + credentials: 'same-origin', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${storefrontApiToken}`, + }, + body: JSON.stringify({ + query: ` + query getProductRating { + site { + product(entityId: ${productId}) { + reviewSummary { + numberOfReviews + summationOfRatings + } + } + } + } + `, + }), + }) + .then((res) => res.json()) + .then((data) => { + const summationOfRatings = data?.data?.site?.product?.reviewSummary?.summationOfRatings + const numberOfReviews = data?.data?.site?.product?.reviewSummary?.numberOfReviews + const ratingElement = document.querySelector(`[data-type-product-id="${productId}"] .c-cart__item-rating`) + const ratingLink = document.querySelector(`[data-type-product-id="${productId}"] .c-cart__item-rating-link`) + + ratingLink.innerHTML = `${numberOfReviews} reviews` + + if (ratingElement) { + ratingElement.setAttribute('data-product-rating', summationOfRatings) + const filledStars = ratingElement.querySelectorAll('.c-cart-item__filled-stars svg') + const unfilledStars = ratingElement.querySelectorAll('.c-cart-item__unfilled-stars svg') + + for (let i = 0; i < summationOfRatings; i++) { + filledStars[i].classList.remove('u-hidden') + } + + for (let i = 0; i < 5 - summationOfRatings; i++) { + unfilledStars[i].classList.remove('u-hidden') + } + } + }) + }) + } +} diff --git a/assets/scss/components/_cart.scss b/assets/scss/components/_cart.scss new file mode 100644 index 00000000..fcbd6ca5 --- /dev/null +++ b/assets/scss/components/_cart.scss @@ -0,0 +1,178 @@ +.c-cart__totals { + display: flex; + flex-direction: column; + align-items: flex-end; +} + +.c-cart__content { + margin-bottom: rem-calc(24px); +} + +.c-cart__item-wrapper { + display: flex; + flex-direction: column; + gap: 3rem; +} + +.c-cart__item { + display: flex; + flex-direction: column; + gap: rem-calc(24px); + + @include bp(small) { + flex-direction: row; + gap: rem-calc(48px); + } +} + +.c-cart__item-image-wrapper { + position: relative; + display: block; + aspect-ratio: 17.75 / 26.5; + width: 50%; + height: auto; + max-width: 17.75rem; + object-fit: cover; + + @include bp(small) { + width: 100%; + } +} + +.c-cart__item-image { + position: absolute; + width: 100%; + height: 100%; + object-fit: cover; +} + +.c-cart__item-information { + display: flex; + flex-direction: column; + gap: 1.5rem; +} + +.c-cart__item-title { + color: $color-black; + font-family: $font-semi-bold; + font-size: 2rem; + font-style: normal; + font-weight: 600; + line-height: 2.5rem; + text-decoration: none; +} + +.c-cart__item-options { + display: flex; + flex-direction: column; + gap: 0.5rem; +} + +.c-cart__option-name { + color: $color-black; + font-family: $font-semi-bold; + font-size: 1rem; + font-style: normal; + font-weight: 600; + line-height: 1.5rem; + margin: 0; +} + +.c-cart__option-value { + color: $color-neutrals-500; + font-family: $font-regular; + font-size: 1rem; + font-style: normal; + font-weight: 400; + line-height: 1.5rem; + margin: 0; +} + +.c-cart__item-actions { + display: flex; + justify-content: space-between; + align-items: center; + gap: 1.5rem; + width: fit-content; +} + +.c-cart__item-inputs { + padding: 1rem; + display: flex; + justify-content: space-between; + align-items: center; + gap: 0.5rem; + background-color: $color-white; + border-radius: 0.25rem; + border: 1px solid $color-neutrals-75; +} + +.c-cart__button-remove { + display: flex; + justify-content: center; + align-items: center; + background: none; + border: 0; + outline: none; + padding: 0; + margin: 0; + cursor: pointer; +} + +.c-cart__item-qty-input { + color: $color-neutrals-500; + text-align: center; + font-family: $font-regular; + font-size: 1rem; + font-style: normal; + font-weight: 400; + line-height: 1.5rem; + width: 1.75rem; + border: 0; +} + +.c-cart__item-qty-btn { + border: 0; + background: none; + outline: none; + cursor: pointer; + display: flex; + justify-content: center; + align-items: center; +} + +.c-cart__checkout-button-wrapper { + display: flex; + flex-direction: column; + align-items: flex-end; +} + +.c-cart__item-rating-wrapper { + display: flex; + align-items: center; + gap: rem-calc(8px); +} + +.c-cart__item-rating-link { + color: $color-primary-300; + font-family: $font-semi-bold; + font-size: rem-calc(14px); + line-height: rem-calc(16px); + font-weight: 600; + letter-spacing: 0; + text-align: center; + + &:hover { + color: $color-primary-300; + } +} + +.c-cart__item-head { + display: flex; + flex-direction: column; + gap: rem-calc(12px); +} + +.c-cart__icon { + max-width: rem-calc(24px); +} diff --git a/assets/scss/components/_components.scss b/assets/scss/components/_components.scss index f34a1234..2f918c12 100644 --- a/assets/scss/components/_components.scss +++ b/assets/scss/components/_components.scss @@ -43,3 +43,9 @@ @import 'cta-panel'; @import 'product-compare'; @import 'blog'; +@import 'cart'; +@import 'totals'; +@import 'shipping-estimator'; +@import 'coupon-form'; +@import 'order-total'; +@import 'star'; diff --git a/assets/scss/components/_coupon-form.scss b/assets/scss/components/_coupon-form.scss new file mode 100644 index 00000000..c69e7775 --- /dev/null +++ b/assets/scss/components/_coupon-form.scss @@ -0,0 +1,5 @@ +.c-coupon-form { + display: flex; + flex-direction: column; + gap: rem-calc(12px); +} diff --git a/assets/scss/components/_order-total.scss b/assets/scss/components/_order-total.scss new file mode 100644 index 00000000..afe570b0 --- /dev/null +++ b/assets/scss/components/_order-total.scss @@ -0,0 +1,6 @@ +.c-order-total { + display: flex; + justify-content: center; + align-items: center; + gap: 1.5rem; +} \ No newline at end of file diff --git a/assets/scss/components/_shipping-estimator.scss b/assets/scss/components/_shipping-estimator.scss new file mode 100644 index 00000000..d73dbae5 --- /dev/null +++ b/assets/scss/components/_shipping-estimator.scss @@ -0,0 +1,7 @@ +.c-shipping-estimator { + display: flex; + flex-direction: column; + align-items: flex-end; + text-align: right; + gap: rem-calc(12px); +} diff --git a/assets/scss/components/_star.scss b/assets/scss/components/_star.scss new file mode 100644 index 00000000..3c924c27 --- /dev/null +++ b/assets/scss/components/_star.scss @@ -0,0 +1,5 @@ +.c-star { + max-width: rem-calc(12px); + max-height: rem-calc(12px); + fill: transparent; +} diff --git a/assets/scss/components/_totals.scss b/assets/scss/components/_totals.scss new file mode 100644 index 00000000..6bb573ce --- /dev/null +++ b/assets/scss/components/_totals.scss @@ -0,0 +1,47 @@ +.c-totals__subtotal { + display: flex; + justify-content: flex-end; + align-items: center; + gap: rem-calc(24px); +} + +.c-totals__shipping-total { + display: flex; + justify-content: flex-end; + align-items: center; + gap: rem-calc(24px); + margin-bottom: rem-calc(12px); +} + +.c-totals__label { + color: $color-neutrals-300; + font-family: $font-semi-bold; + font-size: rem-calc(20px); + line-height: rem-calc(24px); + font-weight: 600; + letter-spacing: 0; + text-align: center; +} + +.c-totals__value { + @extend .c-totals__label; + color: $color-black; +} + +.c-totals__shipping-estimator { + margin-bottom: rem-calc(24px); +} + +.c-totals__cart-total { + font-size: rem-calc(32px); + line-height: rem-calc(40px); + text-align: right; + margin-block: rem-calc(24px); +} + +.c-totals__checkout-button { + display: flex; + justify-content: center; + align-items: center; + gap: rem-calc(8px); +} diff --git a/lang/en.json b/lang/en.json index fa9a8ab1..5ff7e34c 100755 --- a/lang/en.json +++ b/lang/en.json @@ -48,7 +48,7 @@ "multiple": "check out with multiple addresses", "or": "or" }, - "button": "Check out", + "button": "Go to checkout", "empty_cart": "Your cart is empty", "title": "Click here to proceed to checkout", "item": "Item", @@ -68,6 +68,7 @@ "checkout_multiple": "or check out with multiple addresses", "view_cart": "View Cart" }, + "title": "Cart", "label": "Your Cart ({quantity, plural, one {# item} other {# items}})", "is_empty": "Your cart is empty", "invalid_entry_message": "[ENTRY] is not a valid entry", diff --git a/templates/components/cart/content.html b/templates/components/cart/content.html index 24b23492..5d1a232d 100644 --- a/templates/components/cart/content.html +++ b/templates/components/cart/content.html @@ -1,192 +1,141 @@
{{brand.name}}
- {{/if}} - diff --git a/templates/components/cart/coupon-input.html b/templates/components/cart/coupon-input.html index 69b7010b..4063b006 100644 --- a/templates/components/cart/coupon-input.html +++ b/templates/components/cart/coupon-input.html @@ -1,20 +1,7 @@ -