From b6c685f04b9952b90720072acf696b0c4017927b Mon Sep 17 00:00:00 2001 From: Yuliya Kursevich <54816946+YulikK@users.noreply.github.com> Date: Wed, 29 May 2024 14:16:00 +0200 Subject: [PATCH] feat(RSS-ECOMM-4_10)/cart (#321) * feat: modify quantity * feat: delete button for mobile * feat: add clear cart * feat: language and message * fix: src img --- package.json | 2 + src/app/styles/variables.scss | 2 +- src/pages/CartPage/model/CartPageModel.ts | 69 +++++- src/pages/CartPage/view/CartPageView.ts | 232 ++++++++++++++---- .../CartPage/view/cartPageView.module.scss | 69 ++++-- src/shared/API/cart/CartApi.ts | 23 ++ src/shared/API/cart/model/CartModel.ts | 42 ++++ src/shared/constants/common.ts | 2 + src/shared/constants/messages.ts | 6 + src/shared/types/cart.ts | 8 + .../Footer/view/footerView.module.scss | 1 - .../Header/view/headerView.module.scss | 10 +- .../ProductOrder/model/ProductOrderModel.ts | 92 +++---- .../ProductOrder/view/ProductOrderView.ts | 131 +++++++--- .../view/productOrderView.module.scss | 71 ++++-- 15 files changed, 593 insertions(+), 167 deletions(-) diff --git a/package.json b/package.json index a67e8a92..136cf842 100644 --- a/package.json +++ b/package.json @@ -64,8 +64,10 @@ "@commercetools/sdk-client-v2": "^2.5.0", "@commercetools/sdk-middleware-auth": "^7.0.1", "@commercetools/sdk-middleware-http": "^7.0.4", + "@types/hammerjs": "^2.0.45", "@types/js-cookie": "^3.0.6", "autoprefixer": "^10.4.19", + "hammerjs": "^2.0.8", "isomorphic-fetch": "^3.0.0", "js-cookie": "^3.0.5", "materialize-css": "^1.0.0-rc.2", diff --git a/src/app/styles/variables.scss b/src/app/styles/variables.scss index 80f0fe34..6fc08288 100644 --- a/src/app/styles/variables.scss +++ b/src/app/styles/variables.scss @@ -23,7 +23,7 @@ // shadows --mellow-shadow-050: 0 0 20px rgb(0 0 0 / 6%); --mellow-shadow-100: rgb(0 0 0 / 19%) 0 10px 20px, rgb(0 0 0 / 23%) 0 6px 6px; - --mellow-shadow-200: rgb(255 255 255 / 10%) 0 1px 1px 0 inset, rgb(50 50 93 / 25%) 0 50px 100px -20px, + --mellow-shadow-200: hsl(0deg 0% 100% / 10%) 0 1px 1px 0 inset, rgb(50 50 93 / 25%) 0 50px 100px -20px, rgb(0 0 0 / 30%) 0 30px 60px -30px; --mellow-shadow-300: rgb(0 0 0 / 25%) 0 14px 28px, rgb(0 0 0 / 22%) 0 10px 10px; --mellow-shadow-400: rgb(0 0 0 / 9%) 0 2px 1px, rgb(0 0 0 / 9%) 0 4px 2px, rgb(0 0 0 / 9%) 0 8px 4px, diff --git a/src/pages/CartPage/model/CartPageModel.ts b/src/pages/CartPage/model/CartPageModel.ts index 62a39a0f..3938c237 100644 --- a/src/pages/CartPage/model/CartPageModel.ts +++ b/src/pages/CartPage/model/CartPageModel.ts @@ -4,6 +4,7 @@ import type { Page } from '@/shared/types/page.ts'; import getCartModel from '@/shared/API/cart/model/CartModel.ts'; import getStore from '@/shared/Store/Store.ts'; import { setCurrentPage } from '@/shared/Store/actions.ts'; +import observeStore, { selectCurrentLanguage } from '@/shared/Store/observer.ts'; import { PAGE_ID } from '@/shared/constants/pages.ts'; import showErrorMessage from '@/shared/utils/userMessage.ts'; import ProductOrderModel from '@/widgets/ProductOrder/model/ProductOrderModel.ts'; @@ -11,14 +12,59 @@ import ProductOrderModel from '@/widgets/ProductOrder/model/ProductOrderModel.ts import CartPageView from '../view/CartPageView.ts'; class CartPageModel implements Page { + private addDiscountHandler = async (discountCode: string): Promise => { + if (discountCode.trim()) { + this.cart = await getCartModel().addCoupon(discountCode); + this.view.updateTotal(this.cart); + + getCartModel() + .addCoupon(discountCode) + .then((cart) => { + this.cart = cart; + this.view.updateTotal(this.cart); + }) + .catch(showErrorMessage); + } + }; + private cart: Cart | null = null; + private changeProductHandler = (cart: Cart): void => { + this.cart = cart; + this.productsItem = this.productsItem.filter((productItem) => { + const searchEl = this.cart?.products.find((item) => item.lineItemId === productItem.getProduct().lineItemId); + if (!searchEl) { + productItem.getHTML().remove(); + return false; + } + return true; + }); + + if (!this.productsItem.length) { + this.view.renderEmpty(); + } + this.view.updateTotal(this.cart); + }; + + private clearCart = async (): Promise => { + this.cart = await getCartModel().clearCart(); + this.productsItem = this.productsItem.filter((productItem) => { + const searchEl = this.cart?.products.find((item) => item.lineItemId === productItem.getProduct().lineItemId); + if (!searchEl) { + productItem.getHTML().remove(); + return false; + } + return true; + }); + this.renderCart(); + }; + private productsItem: ProductOrderModel[] = []; private view: CartPageView; constructor(parent: HTMLDivElement) { - this.view = new CartPageView(parent); + this.view = new CartPageView(parent, this.clearCart, this.addDiscountHandler); this.init().catch(showErrorMessage); } @@ -26,11 +72,24 @@ class CartPageModel implements Page { private async init(): Promise { getStore().dispatch(setCurrentPage(PAGE_ID.CART_PAGE)); this.cart = await getCartModel().addProductInfo(); - this.cart.products.forEach((product) => { - this.productsItem.push(new ProductOrderModel(product)); - }); + this.renderCart(); + observeStore(selectCurrentLanguage, () => this.view.updateLanguage()); + } + + private renderCart(): void { + if (this.cart) { + this.cart.products.forEach((product) => { + this.productsItem.push(new ProductOrderModel(product, this.changeProductHandler)); + }); - this.view.renderCart(this.productsItem); + if (this.productsItem.length) { + this.view.renderCart(this.productsItem); + this.view.updateTotal(this.cart); + } else { + this.view.renderEmpty(); + this.view.updateTotal(this.cart); + } + } } public getHTML(): HTMLDivElement { diff --git a/src/pages/CartPage/view/CartPageView.ts b/src/pages/CartPage/view/CartPageView.ts index 7897f6ae..38a581b5 100644 --- a/src/pages/CartPage/view/CartPageView.ts +++ b/src/pages/CartPage/view/CartPageView.ts @@ -1,12 +1,55 @@ +import type { LanguageChoiceType } from '@/shared/constants/common'; +import type { Cart } from '@/shared/types/cart'; +import type { languageVariants } from '@/shared/types/common'; import type ProductOrderModel from '@/widgets/ProductOrder/model/ProductOrderModel'; +import RouterModel from '@/app/Router/model/RouterModel.ts'; import InputModel from '@/shared/Input/model/InputModel.ts'; +import LinkModel from '@/shared/Link/model/LinkModel.ts'; +import getStore from '@/shared/Store/Store.ts'; import { INPUT_TYPE } from '@/shared/constants/forms.ts'; +import { PAGE_ID } from '@/shared/constants/pages.ts'; import createBaseElement from '@/shared/utils/createBaseElement.ts'; import styles from './cartPageView.module.scss'; +type ClearCallback = () => void; +type DiscountCallback = (discount: string) => void; +type textElementsType = { + element: HTMLAnchorElement | HTMLButtonElement | HTMLParagraphElement | HTMLTableCellElement; + textItem: languageVariants; +}; + +const TITTLE = { + BUTTON_CHECKOUT: { en: 'Proceed To Checkout', ru: 'Оформить заказ' }, + BUTTON_COUPON: { en: 'Apply', ru: 'Применить' }, + CART_TOTAL: { en: 'Cart Totals', ru: 'Итого по корзине' }, + CLEAR: { en: 'Clear all', ru: 'Очистить' }, + CONTINUE: { en: 'Continue Shopping', ru: 'Продолжить покупки' }, + COUPON_APPLY: { en: 'Coupon Apply', ru: 'Применить купон' }, + COUPON_DISCOUNT: { en: 'Coupon Discount', ru: 'Скидка по купону' }, + EMPTY: { + en: `Oops! Looks like you haven't added the item to your cart yet.`, + ru: `Ой! Похоже, вы еще не добавили товар в корзину.`, + }, + INPUT_COUPON: { en: 'Enter coupon here...', ru: 'Введите купон здесь...' }, + PRICE: { en: 'Price', ru: 'Цена' }, + PRODUCT: { en: 'Product', ru: 'Продукт' }, + QUANTITY: { en: 'Quantity', ru: 'Количество' }, + SUBTOTAL: { en: 'Subtotal', ru: 'Сумма' }, + TOTAL: { en: 'Total', ru: 'Итого' }, +}; class CartPageView { + private addDiscountCallback: DiscountCallback; + + private clearCallback: ClearCallback; + + private discount: HTMLParagraphElement; + + private empty: HTMLDivElement; + + private language: LanguageChoiceType; + private page: HTMLDivElement; private parent: HTMLDivElement; @@ -15,20 +58,35 @@ class CartPageView { private productWrap: HTMLDivElement; + private subTotal: HTMLParagraphElement; + private table: HTMLTableElement | null = null; private tableBody: HTMLTableSectionElement | null = null; + private textElement: textElementsType[] = []; + + private total: HTMLParagraphElement; + private totalWrap: HTMLDivElement; - constructor(parent: HTMLDivElement) { + constructor(parent: HTMLDivElement, clearCallback: ClearCallback, addDiscountCallback: DiscountCallback) { + this.language = getStore().getState().currentLanguage; this.parent = parent; this.parent.innerHTML = ''; + this.clearCallback = clearCallback; + this.addDiscountCallback = addDiscountCallback; this.page = this.createPageHTML(); this.productWrap = this.createWrapHTML(); this.productWrap.classList.add(styles.products); + this.subTotal = createBaseElement({ cssClasses: [styles.totalTitle], tag: 'p' }); + this.total = createBaseElement({ cssClasses: [styles.totalPrice], tag: 'p' }); + this.discount = createBaseElement({ cssClasses: [styles.title], tag: 'p' }); this.totalWrap = this.createWrapHTML(); this.totalWrap.classList.add(styles.total); + this.page.append(this.productWrap); + this.page.append(this.totalWrap); + this.empty = this.createEmptyHTML(); window.scrollTo(0, 0); } @@ -38,26 +96,30 @@ class CartPageView { const tr = createBaseElement({ cssClasses: [styles.tr, styles.head], tag: 'tr' }); const thImage = createBaseElement({ cssClasses: [styles.th, styles.imgCell, styles.mainText], - innerContent: 'Product', + innerContent: TITTLE.PRODUCT[this.language], tag: 'th', }); + this.textElement.push({ element: thImage, textItem: TITTLE.PRODUCT }); const thProduct = createBaseElement({ cssClasses: [styles.th, styles.nameCell, styles.mainText], tag: 'th' }); const thPrice = createBaseElement({ cssClasses: [styles.th, styles.priceCell, styles.mainText], - innerContent: 'Price', + innerContent: TITTLE.PRICE[this.language], tag: 'th', }); + this.textElement.push({ element: thPrice, textItem: TITTLE.PRICE }); const thQuantity = createBaseElement({ cssClasses: [styles.th, styles.quantityCell, styles.mainText], - innerContent: 'Quantity', + innerContent: TITTLE.QUANTITY[this.language], tag: 'th', }); + this.textElement.push({ element: thQuantity, textItem: TITTLE.QUANTITY }); const thTotal = createBaseElement({ cssClasses: [styles.th, styles.totalCell, styles.mainText], - innerContent: 'Total', + innerContent: TITTLE.TOTAL[this.language], tag: 'th', }); - const thDelete = createBaseElement({ cssClasses: [styles.th, styles.deleteCell, styles.mainText], tag: 'th' }); + this.textElement.push({ element: thTotal, textItem: TITTLE.TOTAL }); + const thDelete = this.createDeleCell(); this.tableBody = createBaseElement({ cssClasses: [styles.tbody], tag: 'tbody' }); this.table.append(thead, this.tableBody); thead.append(tr); @@ -65,31 +127,31 @@ class CartPageView { this.productWrap.append(this.table); } - private addTotalInfo(totalPriceSum: number): void { + private addTotalInfo(): void { const title = createBaseElement({ cssClasses: [styles.totalTitle, styles.border, styles.mobileHide], - innerContent: 'Cart Totals', + innerContent: TITTLE.CART_TOTAL[this.language], tag: 'p', }); + this.textElement.push({ element: title, textItem: TITTLE.CART_TOTAL }); const couponTitle = createBaseElement({ cssClasses: [styles.title, styles.mobileHide], - innerContent: 'Coupon Apply', + innerContent: TITTLE.COUPON_APPLY[this.language], tag: 'p', }); + this.textElement.push({ element: couponTitle, textItem: TITTLE.COUPON_APPLY }); const couponWrap = this.createCouponHTML(); - const subtotalWrap = this.createSubtotalHTML(totalPriceSum); + const subtotalWrap = this.createSubtotalHTML(); const discountWrap = this.createDiscountHTML(); - const totalWrap = this.createTotalHTML(totalPriceSum); + const totalWrap = this.createTotalHTML(); const finalButton = createBaseElement({ - cssClasses: [styles.button], - innerContent: 'Proceed To Checkout', + cssClasses: [styles.button, styles.checkoutBtn], + innerContent: TITTLE.BUTTON_CHECKOUT[this.language], tag: 'button', }); - const continueLink = createBaseElement({ - cssClasses: [styles.continue, styles.mobileHide], - innerContent: 'Continue Shopping', - tag: 'a', - }); + this.textElement.push({ element: finalButton, textItem: TITTLE.BUTTON_CHECKOUT }); + const continueLink = this.createCatalogLinkHTML(); + continueLink.getHTML().classList.add(styles.mobileHide); this.totalWrap.append( title, couponTitle, @@ -98,32 +160,94 @@ class CartPageView { discountWrap, totalWrap, finalButton, - continueLink, + continueLink.getHTML(), ); } + private createCatalogLinkHTML(): LinkModel { + const link = new LinkModel({ + attrs: { + href: PAGE_ID.CATALOG_PAGE, + }, + classes: [styles.continue], + text: TITTLE.CONTINUE[this.language], + }); + this.textElement.push({ element: link.getHTML(), textItem: TITTLE.CONTINUE }); + + link.getHTML().addEventListener('click', (event) => { + event.preventDefault(); + RouterModel.getInstance().navigateTo(PAGE_ID.CATALOG_PAGE); + }); + return link; + } + private createCouponHTML(): HTMLDivElement { const couponWrap = createBaseElement({ cssClasses: [styles.totalWrap], tag: 'div' }); const couponInput = new InputModel({ autocomplete: 'off', id: 'coupon', - placeholder: 'Enter coupon here...', + placeholder: TITTLE.INPUT_COUPON[this.language], type: INPUT_TYPE.TEXT, }); - couponInput.getHTML().classList.add('couponInput'); - const couponButton = createBaseElement({ cssClasses: [styles.button], innerContent: 'Apply', tag: 'button' }); + couponInput.getHTML().classList.add(styles.couponInput); + this.textElement.push({ element: couponInput.getHTML(), textItem: TITTLE.INPUT_COUPON }); + const couponButton = createBaseElement({ + cssClasses: [styles.button, styles.applyBtn], + innerContent: TITTLE.BUTTON_COUPON[this.language], + tag: 'button', + }); + this.textElement.push({ element: couponButton, textItem: TITTLE.BUTTON_COUPON }); + couponButton.addEventListener('click', (evn: Event) => { + evn.preventDefault(); + this.addDiscountCallback(couponInput.getHTML().value); + couponInput.getHTML().value = ''; + }); couponWrap.append(couponInput.getHTML(), couponButton); return couponWrap; } + private createDeleCell(): HTMLTableCellElement { + const tdDelete = createBaseElement({ cssClasses: [styles.th, styles.deleteCell, styles.mainText], tag: 'th' }); + const clear = new LinkModel({ + classes: [styles.continue, styles.clear], + text: TITTLE.CLEAR[this.language], + }); + + this.textElement.push({ element: clear.getHTML(), textItem: TITTLE.CLEAR }); + clear.getHTML().addEventListener('click', (event) => { + event.preventDefault(); + this.clearCallback(); + }); + tdDelete.append(clear.getHTML()); + return tdDelete; + } + private createDiscountHTML(): HTMLDivElement { const discountWrap = createBaseElement({ cssClasses: [styles.totalWrap], tag: 'div' }); - const discountTitle = createBaseElement({ cssClasses: [styles.title], innerContent: 'Coupon Discount', tag: 'p' }); - const discountValue = createBaseElement({ cssClasses: [styles.title], innerContent: '(-) 00.00', tag: 'p' }); - discountWrap.append(discountTitle, discountValue); + const discountTitle = createBaseElement({ + cssClasses: [styles.title], + innerContent: TITTLE.COUPON_DISCOUNT[this.language], + tag: 'p', + }); + discountWrap.append(discountTitle, this.discount); + this.textElement.push({ element: discountTitle, textItem: TITTLE.COUPON_DISCOUNT }); return discountWrap; } + private createEmptyHTML(): HTMLDivElement { + const empty = createBaseElement({ cssClasses: [styles.empty, styles.hide], tag: 'div' }); + const emptyTitle = createBaseElement({ + cssClasses: [styles.emptyTitle], + innerContent: TITTLE.EMPTY[this.language], + tag: 'p', + }); + this.textElement.push({ element: emptyTitle, textItem: TITTLE.EMPTY }); + const continueLink = this.createCatalogLinkHTML(); + empty.append(emptyTitle, continueLink.getHTML()); + this.page.append(empty); + return empty; + } + private createPageHTML(): HTMLDivElement { this.page = createBaseElement({ cssClasses: [styles.cartPage], @@ -135,27 +259,27 @@ class CartPageView { return this.page; } - private createSubtotalHTML(totalPriceSum: number): HTMLDivElement { + private createSubtotalHTML(): HTMLDivElement { const subtotalWrap = createBaseElement({ cssClasses: [styles.totalWrap], tag: 'div' }); - const subtotalTitle = createBaseElement({ cssClasses: [styles.title], innerContent: 'Subtotal', tag: 'p' }); - const subtotalValue = createBaseElement({ - cssClasses: [styles.totalTitle], - innerContent: `$ ${totalPriceSum.toFixed(2)}`, + const subtotalTitle = createBaseElement({ + cssClasses: [styles.title], + innerContent: TITTLE.SUBTOTAL[this.language], tag: 'p', }); - subtotalWrap.append(subtotalTitle, subtotalValue); + subtotalWrap.append(subtotalTitle, this.subTotal); + this.textElement.push({ element: subtotalTitle, textItem: TITTLE.SUBTOTAL }); return subtotalWrap; } - private createTotalHTML(totalPriceSum: number): HTMLDivElement { + private createTotalHTML(): HTMLDivElement { const totalWrap = createBaseElement({ cssClasses: [styles.totalWrap], tag: 'div' }); - const totalTitle = createBaseElement({ cssClasses: [styles.totalTitle], innerContent: 'Total', tag: 'p' }); - const totalValue = createBaseElement({ - cssClasses: [styles.totalPrice], - innerContent: `$ ${totalPriceSum.toFixed(2)}`, + const totalTitle = createBaseElement({ + cssClasses: [styles.totalTitle], + innerContent: TITTLE.TOTAL[this.language], tag: 'p', }); - totalWrap.append(totalTitle, totalValue); + totalWrap.append(totalTitle, this.total); + this.textElement.push({ element: totalTitle, textItem: TITTLE.TOTAL }); return totalWrap; } @@ -165,8 +289,6 @@ class CartPageView { tag: 'div', }); - this.page.append(wrap); - return wrap; } @@ -177,12 +299,40 @@ class CartPageView { public renderCart(productsItem: ProductOrderModel[]): void { this.productWrap.innerHTML = ''; this.totalWrap.innerHTML = ''; + this.productWrap.classList.remove(styles.hide); + this.totalWrap.classList.remove(styles.hide); + this.empty.classList.add(styles.hide); this.productRow.map((productEl) => productEl.remove()); this.productRow = []; this.addTableHeader(); productsItem.forEach((productEl) => this.tableBody?.append(productEl.getHTML())); - const totalPriceSum = productsItem.reduce((sum, product) => sum + product.getProduct().totalPrice, 0); - this.addTotalInfo(totalPriceSum); + this.addTotalInfo(); + } + + public renderEmpty(): void { + this.productWrap.innerHTML = ''; + this.totalWrap.innerHTML = ''; + this.productWrap.classList.add(styles.hide); + this.totalWrap.classList.add(styles.hide); + this.empty.classList.remove(styles.hide); + } + + public updateLanguage(): void { + this.language = getStore().getState().currentLanguage; + this.textElement.forEach((textEl) => { + const elHTML = textEl.element; + if (elHTML instanceof HTMLInputElement) { + elHTML.placeholder = textEl.textItem[this.language]; + } else { + elHTML.textContent = textEl.textItem[this.language]; + } + }); + } + + public updateTotal(cart: Cart): void { + this.subTotal.innerHTML = `$ ${(cart.total + cart.discounts).toFixed(2)}`; + this.discount.innerHTML = `-$ ${cart.discounts.toFixed(2)}`; + this.total.innerHTML = `$ ${cart.total.toFixed(2)}`; } } export default CartPageView; diff --git a/src/pages/CartPage/view/cartPageView.module.scss b/src/pages/CartPage/view/cartPageView.module.scss index 48a23966..b7acab42 100644 --- a/src/pages/CartPage/view/cartPageView.module.scss +++ b/src/pages/CartPage/view/cartPageView.module.scss @@ -1,9 +1,15 @@ +@import 'src/app/styles/mixins'; + .cartPage { display: flex; flex-grow: 1; padding: 0 var(--small-offset); animation: show 0.2s ease-out forwards; gap: 2%; + + @media (max-width: 768px) { + flex-direction: column; + } } @keyframes show { @@ -29,16 +35,16 @@ flex-shrink: 1; @media (max-width: 768px) { - position: fixed; + position: sticky; left: 0; bottom: 0; z-index: 100; margin-bottom: var(--tiny-offset); - border-radius: var(--small-offset) var(--small-offset) 0 0; + border-radius: var(--large-br); padding: var(--small-offset); width: 100%; - box-shadow: var(--mellow-shadow-600); - background-color: var(--noble-gray-1000); + box-shadow: var(--mellow-shadow-700); + background-color: var(--white-tr); backdrop-filter: blur(10px); } } @@ -49,10 +55,6 @@ .thead { width: 100%; - - @media (max-width: 768px) { - display: none; - } } .mainText { @@ -65,12 +67,16 @@ padding: var(--tiny-offset) 0; font: var(--bold-font); color: var(--noble-gray-400); + + @media (max-width: 768px) { + display: none; + } } .tr { display: grid; grid-gap: 0; - grid-template-columns: calc(var(--tiny-offset) * 7) 2fr 1fr 1fr 1fr calc(var(--tiny-offset) * 7); + grid-template-columns: calc(var(--tiny-offset) * 10) 2fr 1fr 1fr 1fr calc(var(--tiny-offset) * 7); width: 100%; @media (max-width: 768px) { @@ -89,6 +95,7 @@ border-bottom: var(--one) solid var(--steam-green-800); } +.emptyTitle, .totalTitle { padding: var(--tiny-offset) 0; font: var(--bold-font); @@ -109,7 +116,7 @@ display: flex; align-items: stretch; justify-content: space-between; - max-width: calc(var(--extra-large-offset) * 3.8); // 380px + max-width: calc(var(--extra-large-offset) * 3.8); @media (max-width: 768px) { width: 100%; @@ -125,18 +132,28 @@ .couponInput { flex-grow: 1; - border: 1px solid var(--steam-green-800); - border-radius: 3px 0 0 3px; - padding: var(--tiny-offset) 0; + border: var(--one) solid var(--steam-green-800); + border-radius: var(--small-br) 0 0 var(--small-br); + padding: var(--tiny-offset); font: var(--regular-font); color: var(--noble-gray-800); } .button { + @include green-btn; + padding: var(--tiny-offset); - font: var(--bold-font); color: var(--noble-gray-200); - background-color: var(--steam-green-800); +} + +.applyBtn { + border-radius: 0 var(--small-br) var(--small-br) 0; +} + +.checkoutBtn { + margin: 0 auto; + border-radius: var(--small-br); + color: var(--noble-gray-200); } .continue { @@ -144,6 +161,7 @@ padding: var(--tiny-offset) 0; font: var(--regular-font); color: var(--steam-green-800); + cursor: pointer; } .mobileHide { @@ -151,3 +169,24 @@ display: none; } } + +.deleteCell { + @media (max-width: 768px) { + display: flex; + align-items: center; + justify-content: center; + grid-area: 1/3; + } +} + +.empty { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + width: 100%; +} + +.hide { + display: none; +} diff --git a/src/shared/API/cart/CartApi.ts b/src/shared/API/cart/CartApi.ts index 7700a227..8181a130 100644 --- a/src/shared/API/cart/CartApi.ts +++ b/src/shared/API/cart/CartApi.ts @@ -4,8 +4,11 @@ import type { Cart as CartResponse, ClientResponse, MyCartDraft, + MyCartUpdateAction, } from '@commercetools/platform-sdk'; +import serverMessageModel from '@/shared/ServerMessage/model/ServerMessageModel.ts'; +import { MESSAGE_STATUS, SERVER_MESSAGE_KEYS } from '@/shared/constants/messages.ts'; import { CURRENCY } from '@/shared/constants/product.ts'; import getApiClient, { type ApiClient } from '../sdk/client.ts'; @@ -108,6 +111,26 @@ export class CartApi { const data = await this.client.apiRoot().me().carts().get().execute(); return data; } + + public updateCart(cart: Cart, actions: MyCartUpdateAction[]): Promise | boolean> { + return this.client + .apiRoot() + .me() + .carts() + .withId({ ID: cart.id }) + .post({ + body: { + actions, + version: cart.version, + }, + }) + .execute() + .then((data) => { + serverMessageModel.showServerMessage(SERVER_MESSAGE_KEYS.SUCCESSFUL_ADD_COUPON_TO_CART, MESSAGE_STATUS.SUCCESS); + return data; + }) + .catch(() => serverMessageModel.showServerMessage(SERVER_MESSAGE_KEYS.INVALID_COUPON, MESSAGE_STATUS.ERROR)); + } } const createCartApi = (): CartApi => new CartApi(); diff --git a/src/shared/API/cart/model/CartModel.ts b/src/shared/API/cart/model/CartModel.ts index f17df8e7..cace2cfa 100644 --- a/src/shared/API/cart/model/CartModel.ts +++ b/src/shared/API/cart/model/CartModel.ts @@ -4,6 +4,7 @@ import type { Cart as CartResponse, ClientResponse, LineItem, + MyCartUpdateAction, } from '@commercetools/platform-sdk'; import getStore from '@/shared/Store/Store.ts'; @@ -35,9 +36,12 @@ export class CartModel { getStore().dispatch(setAnonymousCartId(data.id)); getStore().dispatch(setAnonymousId(data.anonymousId)); } + const discount = data.discountOnTotalPrice?.discountedAmount?.centAmount; return { + discounts: discount ? discount / PRICE_FRACTIONS : 0, id: data.id, products: data.lineItems.map((lineItem) => this.adaptLineItem(lineItem)), + total: data.totalPrice.centAmount / PRICE_FRACTIONS || 0, version: data.version, }; } @@ -70,8 +74,10 @@ export class CartModel { private getCartFromData(data: ClientResponse): Cart { let cart: Cart = { + discounts: 0, id: '', products: [], + total: 0, version: 0, }; if (isClientResponse(data) && isCart(data.body)) { @@ -82,6 +88,24 @@ export class CartModel { return cart; } + public async addCoupon(discountCode: string): Promise { + if (!this.cart) { + this.cart = await this.getCart(); + } + const action: MyCartUpdateAction[] = [ + { + action: 'addDiscountCode', + code: discountCode, + }, + ]; + const data = await this.root.updateCart(this.cart, action); + if (isClientResponse(data)) { + this.cart = this.getCartFromData(data); + } + + return this.cart; + } + public async addProductInfo(): Promise { if (!this.cart) { this.cart = await this.getCart(); @@ -93,6 +117,7 @@ export class CartModel { }); const opt: OptionsRequest = { filter: filter.getFilter(), + limit: this.cart.products.length, }; const products = await getProductModel().getProducts(opt); @@ -130,6 +155,23 @@ export class CartModel { return true; } + public async clearCart(): Promise { + if (!this.cart) { + this.cart = await this.getCart(); + } + const actions: MyCartUpdateAction[] = this.cart?.products.map((lineItem) => ({ + action: 'removeLineItem', + lineItemId: lineItem.lineItemId, + })); + const data = await this.root.updateCart(this.cart, actions); + if (isClientResponse(data)) { + this.cart = this.getCartFromData(data); + } + + this.dispatchUpdate(); + return this.cart; + } + public async create(): Promise { const newCart = await this.root.create(); this.cart = this.getCartFromData(newCart); diff --git a/src/shared/constants/common.ts b/src/shared/constants/common.ts index f69d1107..38bbd56e 100644 --- a/src/shared/constants/common.ts +++ b/src/shared/constants/common.ts @@ -13,4 +13,6 @@ export const DATA_KEYS = { DIRECTION: 'data-direction', } as const; +export const TABLET_WIDTH = 768; + export type LanguageChoiceType = (typeof LANGUAGE_CHOICE)[keyof typeof LANGUAGE_CHOICE]; diff --git a/src/shared/constants/messages.ts b/src/shared/constants/messages.ts index 6522a92c..e7aed233 100644 --- a/src/shared/constants/messages.ts +++ b/src/shared/constants/messages.ts @@ -22,11 +22,13 @@ export const SERVER_MESSAGE = { COPY_TO_CLIPBOARD: 'SKU copied to clipboard', GREETING: 'Hi! Welcome to our store. Enjoy shopping!', INCORRECT_PASSWORD: 'Please, enter a correct password', + INVALID_COUPON: 'Invalid coupon', INVALID_EMAIL: "User with this email doesn't exist. Please, register first", LANGUAGE_CHANGED: 'Language preferences have been updated successfully', PASSWORD_CHANGED: 'Your password has been changed successfully', PASSWORD_NOT_CHANGED: 'Your password has not been changed. Please, try again', PERSONAL_INFO_CHANGED: 'Personal information has been changed successfully', + SUCCESSFUL_ADD_COUPON_TO_CART: 'Coupon has been added to your cart successfully', SUCCESSFUL_ADD_PRODUCT_TO_CART: 'Product has been added successfully to your cart', SUCCESSFUL_ADD_PRODUCT_TO_WISHLIST: 'Product has been added successfully to your wishlist', SUCCESSFUL_COPY_TO_CLIPBOARD: 'SKU has been copied to clipboard', @@ -45,11 +47,13 @@ export const SERVER_MESSAGE = { COPY_TO_CLIPBOARD: 'SKU скопирован в буфер обмена', GREETING: 'Здравствуйте! Добро пожаловать в наш магазин. Приятных покупок!', INCORRECT_PASSWORD: 'Пожалуйста, введите правильный пароль', + INVALID_COUPON: 'Неверный купон', INVALID_EMAIL: 'Пользователь с таким адресом не существует. Пожалуйста, сначала зарегистрируйтесь', LANGUAGE_CHANGED: 'Настройки языка успешно обновлены', PASSWORD_CHANGED: 'Ваш пароль был успешно изменен', PASSWORD_NOT_CHANGED: 'Ваш пароль не был изменен. Пожалуйста, попробуйте ещё раз', PERSONAL_INFO_CHANGED: 'Персональные данные были успешно изменены', + SUCCESSFUL_ADD_COUPON_TO_CART: 'Купон был успешно добавлен в корзину', SUCCESSFUL_ADD_PRODUCT_TO_CART: 'Товар был успешно добавлен в корзину', SUCCESSFUL_ADD_PRODUCT_TO_WISHLIST: 'Товар был успешно добавлен в избранное', SUCCESSFUL_COPY_TO_CLIPBOARD: 'SKU успешно скопирован в буфер обмена', @@ -70,11 +74,13 @@ export const SERVER_MESSAGE_KEYS = { COPY_TO_CLIPBOARD: 'COPY_TO_CLIPBOARD', GREETING: 'GREETING', INCORRECT_PASSWORD: 'INCORRECT_PASSWORD', + INVALID_COUPON: 'INVALID_COUPON', INVALID_EMAIL: 'INVALID_EMAIL', LANGUAGE_CHANGED: 'LANGUAGE_CHANGED', PASSWORD_CHANGED: 'PASSWORD_CHANGED', PASSWORD_NOT_CHANGED: 'PASSWORD_NOT_CHANGED', PERSONAL_INFO_CHANGED: 'PERSONAL_INFO_CHANGED', + SUCCESSFUL_ADD_COUPON_TO_CART: 'SUCCESSFUL_ADD_COUPON_TO_CART', SUCCESSFUL_ADD_PRODUCT_TO_CART: 'SUCCESSFUL_ADD_PRODUCT_TO_CART', SUCCESSFUL_ADD_PRODUCT_TO_WISHLIST: 'SUCCESSFUL_ADD_PRODUCT_TO_WISHLIST', SUCCESSFUL_COPY_TO_CLIPBOARD: 'SUCCESSFUL_COPY_TO_CLIPBOARD', diff --git a/src/shared/types/cart.ts b/src/shared/types/cart.ts index e46e8c2f..ca9b7f28 100644 --- a/src/shared/types/cart.ts +++ b/src/shared/types/cart.ts @@ -1,8 +1,10 @@ import type { SizeType, localization } from './product.ts'; export interface Cart { + discounts: number; id: string; products: CartProduct[]; + total: number; version: number; } @@ -28,3 +30,9 @@ export interface EditCartItem { lineId: string; quantity: number; } + +export enum CartActive { + DELETE = 'delete', + MINUS = 'minus', + PLUS = 'plus', +} diff --git a/src/widgets/Footer/view/footerView.module.scss b/src/widgets/Footer/view/footerView.module.scss index deea8c56..e9cb0599 100644 --- a/src/widgets/Footer/view/footerView.module.scss +++ b/src/widgets/Footer/view/footerView.module.scss @@ -10,7 +10,6 @@ align-items: center; justify-content: center; margin: 0 auto; - padding: 0 var(--small-offset); } .socialWrap { diff --git a/src/widgets/Header/view/headerView.module.scss b/src/widgets/Header/view/headerView.module.scss index 743ac766..bc77a222 100644 --- a/src/widgets/Header/view/headerView.module.scss +++ b/src/widgets/Header/view/headerView.module.scss @@ -449,15 +449,15 @@ .badgeWrap { position: absolute; - right: -5px; - top: 2px; + right: -10%; + top: 1%; display: flex; align-items: center; justify-content: center; - border: 2px solid var(--noble-gray-1000); + border: var(--one) solid var(--noble-gray-1000); border-radius: 100%; - width: 16px; - height: 16px; + width: calc(var(--tiny-offset) * 2); + height: calc(var(--tiny-offset) * 2); font: var(--regular-font); background-color: var(--steam-green-800); } diff --git a/src/widgets/ProductOrder/model/ProductOrderModel.ts b/src/widgets/ProductOrder/model/ProductOrderModel.ts index 8b1982c2..a431ad65 100644 --- a/src/widgets/ProductOrder/model/ProductOrderModel.ts +++ b/src/widgets/ProductOrder/model/ProductOrderModel.ts @@ -1,44 +1,29 @@ -import type { CartProduct, EditCartItem } from '@/shared/types/cart.ts'; +import type { Cart, CartProduct, EditCartItem } from '@/shared/types/cart.ts'; import getCartModel from '@/shared/API/cart/model/CartModel.ts'; +import observeStore, { selectCurrentLanguage } from '@/shared/Store/observer.ts'; +import { CartActive } from '@/shared/types/cart.ts'; import ProductOrderView from '../view/ProductOrderView.ts'; -type CallbackQuantity = () => Promise; - -export type CallbackList = { - delete: CallbackQuantity; - minus: CallbackQuantity; - plus: CallbackQuantity; -}; +type Callback = (cart: Cart) => void; class ProductOrderModel { + private callback: Callback; + private productItem: CartProduct; private view: ProductOrderView; - constructor(productItem: CartProduct) { + constructor(productItem: CartProduct, callback: Callback) { + this.callback = callback; this.productItem = productItem; - const callbackList: CallbackList = { - delete: this.deleteClickHandler.bind(this), - minus: this.minusClickHandler.bind(this), - plus: this.plusClickHandler.bind(this), - }; - this.view = new ProductOrderView(this.productItem, callbackList); + this.view = new ProductOrderView(this.productItem, this.updateProductHandler.bind(this)); this.init(); } - private init(): void {} - - public async deleteClickHandler(): Promise { - const cart = await getCartModel().deleteProductFromCart(this.productItem); - const updateItem = cart.products.find((item) => item.lineItemId === this.productItem.lineItemId); - if (updateItem) { - this.productItem = updateItem; - this.view.updateQuantity(this.productItem.quantity); - } else { - this.getHTML().remove(); - } + private init(): void { + observeStore(selectCurrentLanguage, () => this.view.updateLanguage()); } public getHTML(): HTMLDivElement { @@ -49,31 +34,48 @@ class ProductOrderModel { return this.productItem; } - public async minusClickHandler(): Promise { - const active: EditCartItem = { - lineId: this.productItem.lineItemId, - quantity: this.productItem.quantity - 1, - }; - const cart = await getCartModel().editProductCount(active); - const updateItem = cart.products.find((item) => item.lineItemId === this.productItem.lineItemId); + public async updateProductHandler(active: CartActive): Promise { + let updateItem: CartProduct | undefined; + let cart: Cart | null = null; + switch (active) { + case CartActive.DELETE: { + cart = await getCartModel().deleteProductFromCart(this.productItem); + updateItem = cart.products.find((item) => item.lineItemId === this.productItem.lineItemId); + break; + } + + case CartActive.MINUS: { + const active: EditCartItem = { + lineId: this.productItem.lineItemId, + quantity: this.productItem.quantity - 1, + }; + cart = await getCartModel().editProductCount(active); + updateItem = cart.products.find((item) => item.lineItemId === this.productItem.lineItemId); + break; + } + case CartActive.PLUS: { + const active: EditCartItem = { + lineId: this.productItem.lineItemId, + quantity: this.productItem.quantity + 1, + }; + cart = await getCartModel().editProductCount(active); + updateItem = cart.products.find((item) => item.lineItemId === this.productItem.lineItemId); + break; + } + + default: + break; + } + if (updateItem) { this.productItem = updateItem; - this.view.updateQuantity(this.productItem.quantity); + this.view.updateInfo(this.productItem); } else { this.getHTML().remove(); } - } - public async plusClickHandler(): Promise { - const active: EditCartItem = { - lineId: this.productItem.lineItemId, - quantity: this.productItem.quantity + 1, - }; - const cart = await getCartModel().editProductCount(active); - const updateItem = cart.products.find((item) => item.lineItemId === this.productItem.lineItemId); - if (updateItem) { - this.productItem = updateItem; - this.view.updateQuantity(this.productItem.quantity); + if (cart) { + this.callback(cart); } } } diff --git a/src/widgets/ProductOrder/view/ProductOrderView.ts b/src/widgets/ProductOrder/view/ProductOrderView.ts index 31c5e3c9..eddeb246 100644 --- a/src/widgets/ProductOrder/view/ProductOrderView.ts +++ b/src/widgets/ProductOrder/view/ProductOrderView.ts @@ -1,36 +1,79 @@ +import type { LanguageChoiceType } from '@/shared/constants/common.ts'; import type { CartProduct } from '@/shared/types/cart'; +import type { languageVariants } from '@/shared/types/common'; import getStore from '@/shared/Store/Store.ts'; -import { LANGUAGE_CHOICE } from '@/shared/constants/common.ts'; +import { LANGUAGE_CHOICE, TABLET_WIDTH } from '@/shared/constants/common.ts'; import SVG_DETAILS from '@/shared/constants/svg.ts'; +import { CartActive } from '@/shared/types/cart.ts'; import createBaseElement from '@/shared/utils/createBaseElement.ts'; import createSVGUse from '@/shared/utils/createSVGUse.ts'; - -import type { CallbackList } from '../model/ProductOrderModel'; +import Hammer from 'hammerjs'; import styles from './productOrderView.module.scss'; +type CallbackActive = (active: CartActive) => Promise; + +type textElementsType = { + element: HTMLTableCellElement; + textItem: languageVariants; +}; + +const TITTLE = { + MINUS: '-', + NAME: { + en: '', + ru: '', + }, + PLUS: '+', + SIZE: { + en: 'Size', + ru: 'Размер', + }, +}; class ProductOrderView { - private callbackList: CallbackList; + private callback: CallbackActive; + + private language: LanguageChoiceType; + + private price: HTMLTableCellElement; + + private productItem: CartProduct; private quantity: HTMLParagraphElement; + private textElement: textElementsType[] = []; + + private total: HTMLTableCellElement; + private view: HTMLTableRowElement; - constructor(productItem: CartProduct, callbackList: CallbackList) { - this.callbackList = callbackList; + constructor(productItem: CartProduct, callback: CallbackActive) { + this.productItem = productItem; + this.language = getStore().getState().currentLanguage; + this.callback = callback; this.quantity = createBaseElement({ cssClasses: [styles.quantityCell, styles.quantityText], - innerContent: productItem.quantity.toString(), + innerContent: this.productItem.quantity.toString(), tag: 'p', }); - this.view = this.createHTML(productItem); + this.price = createBaseElement({ + cssClasses: [styles.td, styles.priceCell, styles.priceText], + innerContent: `$${this.productItem.price.toFixed(2)}`, + tag: 'td', + }); + this.total = createBaseElement({ + cssClasses: [styles.td, styles.totalCell, styles.totalText], + innerContent: `$${this.productItem.totalPrice.toFixed(2)}`, + tag: 'td', + }); + this.view = this.createHTML(); } private createDeleCell(): HTMLTableCellElement { - const tdDelete = createBaseElement({ cssClasses: [styles.td, styles.deleteCell], tag: 'td' }); + const tdDelete = createBaseElement({ cssClasses: [styles.td, styles.deleteCell, styles.hide], tag: 'td' }); const deleteButton = createBaseElement({ cssClasses: [styles.deleteButton], tag: 'button' }); - deleteButton.addEventListener('click', () => this.callbackList.delete()); + deleteButton.addEventListener('click', () => this.callback(CartActive.DELETE)); tdDelete.append(deleteButton); const svg = document.createElementNS(SVG_DETAILS.SVG_URL, 'svg'); svg.append(createSVGUse(SVG_DETAILS.DELETE)); @@ -38,40 +81,45 @@ class ProductOrderView { return tdDelete; } - private createHTML(productItem: CartProduct): HTMLTableRowElement { + private createHTML(): HTMLTableRowElement { this.view = createBaseElement({ cssClasses: [styles.tr, styles.trProduct], tag: 'tr' }); - const imgCell = this.createImgCell(productItem); + const imgCell = this.createImgCell(); const tdProduct = createBaseElement({ cssClasses: [styles.td, styles.nameCell, styles.mainText], - innerContent: productItem.name[Number(getStore().getState().currentLanguage === LANGUAGE_CHOICE.RU)].value, + innerContent: this.productItem.name[Number(getStore().getState().currentLanguage === LANGUAGE_CHOICE.RU)].value, tag: 'td', }); const tdSize = createBaseElement({ cssClasses: [styles.td, styles.sizeCell, styles.sizeText], - innerContent: productItem.size ? `Size: ${productItem.size}` : '', - tag: 'td', - }); - const tdPrice = createBaseElement({ - cssClasses: [styles.td, styles.priceCell, styles.priceText], - innerContent: `$${productItem.price.toFixed(2)}`, + innerContent: this.productItem.size ? `${TITTLE.SIZE[this.language]}: ${this.productItem.size}` : '', tag: 'td', }); + this.textElement.push({ element: tdSize, textItem: TITTLE.SIZE }); + this.textElement.push({ element: tdProduct, textItem: TITTLE.NAME }); const quantityCell = this.createQuantityCell(); - const tdTotal = createBaseElement({ - cssClasses: [styles.td, styles.totalCell, styles.totalText], - innerContent: `$${productItem.totalPrice.toFixed(2)}`, - tag: 'td', - }); const deleteCell = this.createDeleCell(); - this.view.append(imgCell, tdProduct, tdSize, tdPrice, quantityCell, tdTotal, deleteCell); + this.view.append(imgCell, tdProduct, tdSize, this.price, quantityCell, this.total, deleteCell); + const animation = new Hammer(this.view); + animation.on('swipeleft', () => { + if (window.innerWidth <= TABLET_WIDTH) { + this.view.style.transform = 'translateX(-100px)'; + deleteCell.classList.remove(styles.hide); + } + }); + animation.on('swiperight', () => { + if (window.innerWidth <= TABLET_WIDTH) { + this.view.style.transform = 'none'; + deleteCell.classList.add(styles.hide); + } + }); return this.view; } - private createImgCell(productItem: CartProduct): HTMLTableCellElement { + private createImgCell(): HTMLTableCellElement { const tdImage = createBaseElement({ cssClasses: [styles.td, styles.imgCell], tag: 'td' }); const img = createBaseElement({ cssClasses: [styles.img], tag: 'img' }); - img.src = productItem.images; - img.alt = productItem.name[Number(getStore().getState().currentLanguage === LANGUAGE_CHOICE.RU)].value; + img.src = this.productItem.images; + img.alt = this.productItem.name[Number(getStore().getState().currentLanguage === LANGUAGE_CHOICE.RU)].value; tdImage.append(img); return tdImage; } @@ -83,17 +131,17 @@ class ProductOrderView { }); const plusButton = createBaseElement({ cssClasses: [styles.quantityCell, styles.quantityButton], - innerContent: '+', + innerContent: TITTLE.PLUS, tag: 'button', }); const minusButton = createBaseElement({ cssClasses: [styles.quantityCell, styles.quantityButton], - innerContent: '-', + innerContent: TITTLE.MINUS, tag: 'button', }); tdQuantity.append(minusButton, this.quantity, plusButton); - plusButton.addEventListener('click', () => this.callbackList.plus()); - minusButton.addEventListener('click', () => this.callbackList.minus()); + plusButton.addEventListener('click', () => this.callback(CartActive.PLUS)); + minusButton.addEventListener('click', () => this.callback(CartActive.MINUS)); return tdQuantity; } @@ -101,8 +149,23 @@ class ProductOrderView { return this.view; } - public updateQuantity(quantity: number): void { - this.quantity.textContent = quantity.toString(); + public updateInfo(productItem: CartProduct): void { + this.productItem = productItem; + this.quantity.textContent = this.productItem.quantity.toString(); + this.price.textContent = `$${this.productItem.price.toFixed(2)}`; + this.total.textContent = `$${this.productItem.totalPrice.toFixed(2)}`; + } + + public updateLanguage(): void { + this.language = getStore().getState().currentLanguage; + this.textElement.forEach((textEl) => { + const elHTML = textEl.element; + if (textEl.textItem === TITTLE.SIZE) { + elHTML.textContent = this.productItem.size ? `${TITTLE.SIZE[this.language]}: ${this.productItem.size}` : ''; + } else if (textEl.textItem === TITTLE.NAME) { + elHTML.textContent = this.productItem.name[Number(this.language === LANGUAGE_CHOICE.RU)].value; + } + }); } } diff --git a/src/widgets/ProductOrder/view/productOrderView.module.scss b/src/widgets/ProductOrder/view/productOrderView.module.scss index b78e8a62..f9c21c9f 100644 --- a/src/widgets/ProductOrder/view/productOrderView.module.scss +++ b/src/widgets/ProductOrder/view/productOrderView.module.scss @@ -6,11 +6,32 @@ display: flex; align-items: center; justify-content: center; + padding: 0; +} + +.mainText { + padding: var(--tiny-offset); + font: var(--extra-font); + color: var(--noble-gray-800); } .deleteCell { grid-area: 2 / 6 / 4 / 7; + @media (max-width: 768px) { + position: absolute; + right: -10%; + top: 50%; + display: flex; + align-items: center; + justify-content: center; + grid-column: auto; + grid-row: auto; + transform: translate(calc(var(--small-offset) / 2), calc(var(--small-offset) / 2 * -1)); + } +} + +.hide { @media (max-width: 768px) { display: none; } @@ -22,6 +43,12 @@ height: var(--extra-small-offset); stroke: var(--noble-gray-800); transition: fill 0.2s; + + @media (max-width: 768px) { + width: var(--small-offset); + height: var(--small-offset); + stroke: var(--steam-green-800); + } } &:hover { @@ -34,33 +61,37 @@ .tr { display: grid; grid-gap: 0; - grid-template-columns: 70px 2fr 1fr 1fr 1fr 70px; + grid-template-columns: calc(var(--tiny-offset) * 10) 2fr 1fr 1fr 1fr calc(var(--tiny-offset) * 7); margin-bottom: var(--tiny-offset); width: 100%; @media (max-width: 768px) { - grid-template-columns: 100px 2fr 1fr; + grid-template-columns: calc(var(--tiny-offset) * 10) 2fr 1fr; } } .trProduct { - background-color: var(--noble-white-200); + background-color: var(--white); + transition: transform 0.3s ease-out; @media (max-width: 768px) { + position: relative; + z-index: 10; margin-bottom: var(--extra-small-offset); - border-radius: 14px; - box-shadow: var(--mellow-shadow-100); + border-radius: var(--medium-br); + box-shadow: var(--mellow-shadow-050); } } .img { - max-width: 70px; - max-height: 70px; + display: block; + width: 100%; + height: 100%; @media (max-width: 768px) { - border-radius: 14px 0 0 14px; - max-width: 100px; - max-height: 100px; + border-radius: var(--medium-br) 0 0 var(--medium-br); + width: 100%; + height: 100%; } } @@ -71,6 +102,8 @@ @media (max-width: 768px) { grid-area: 1 / 2 / 2 / 3; + padding: var(--tiny-offset); + padding-bottom: 0; } } @@ -118,12 +151,6 @@ } } -.mainText { - padding: var(--tiny-offset); - font: var(--extra-font); - color: var(--noble-gray-800); -} - .quantityText { padding: var(--tiny-offset); font: var(--regular-font); @@ -132,7 +159,7 @@ .totalText { padding: var(--tiny-offset); - font: var(--regular-font); + font: var(--bold-font); color: var(--steam-green-800); } @@ -147,12 +174,16 @@ font: var(--regular-font); text-align: left; color: var(--noble-gray-700); + + @media (max-width: 768px) { + padding-top: 0; + } } .quantityButton { - border-radius: 29px; - width: 22px; - height: 25px; + border-radius: 50%; + width: calc(var(--tiny-offset) * 2.5); + height: calc(var(--tiny-offset) * 2.5); color: var(--noble-gray-200); background-color: var(--steam-green-800); }