diff --git a/public/img/png/expand-arrow.png b/public/img/png/expand-arrow.png new file mode 100644 index 00000000..ba917d13 Binary files /dev/null and b/public/img/png/expand-arrow.png differ diff --git a/src/app/App/model/AppModel.ts b/src/app/App/model/AppModel.ts index 0f0d2f63..7a5c2222 100644 --- a/src/app/App/model/AppModel.ts +++ b/src/app/App/model/AppModel.ts @@ -71,6 +71,10 @@ class AppModel { const { default: UserProfilePageModel } = await import('@/pages/UserProfilePage/model/UserProfilePageModel.ts'); return new UserProfilePageModel(this.appView.getHTML()); }, + [PAGE_ID.WISHLIST_PAGE]: async (): Promise => { + const { default: WishlistPageModel } = await import('@/pages/WishlistPage/model/WishlistPageModel.ts'); + return new WishlistPageModel(this.appView.getHTML()); + }, }; const routes = new Map Promise>(); diff --git a/src/app/Router/model/RouterModel.ts b/src/app/Router/model/RouterModel.ts index 10ac20f7..48bc56e1 100644 --- a/src/app/Router/model/RouterModel.ts +++ b/src/app/Router/model/RouterModel.ts @@ -102,7 +102,7 @@ class RouterModel { return { hasRoute, - params: { [currentPage.slice(PATH_SEGMENTS_TO_KEEP)]: { id } }, + params: { [currentPage]: { id } }, }; } diff --git a/src/entities/Navigation/view/NavigationView.ts b/src/entities/Navigation/view/NavigationView.ts index 6a421fde..811538c4 100644 --- a/src/entities/Navigation/view/NavigationView.ts +++ b/src/entities/Navigation/view/NavigationView.ts @@ -1,6 +1,7 @@ import LinkModel from '@/shared/Link/model/LinkModel.ts'; import getStore from '@/shared/Store/Store.ts'; -import { PAGE_ID, PAGE_LINK_TEXT, PAGE_LINK_TEXT_KEYS } from '@/shared/constants/pages.ts'; +import { PAGE_LINK_TEXT, PAGE_LINK_TEXT_KEYS } from '@/shared/constants/links.ts'; +import { PAGE_ID } from '@/shared/constants/pages.ts'; import createBaseElement from '@/shared/utils/createBaseElement.ts'; import observeCurrentLanguage from '@/shared/utils/observeCurrentLanguage.ts'; diff --git a/src/entities/ProductCard/model/ProductCardModel.ts b/src/entities/ProductCard/model/ProductCardModel.ts index 2ba4158b..ef3b80c0 100644 --- a/src/entities/ProductCard/model/ProductCardModel.ts +++ b/src/entities/ProductCard/model/ProductCardModel.ts @@ -8,8 +8,7 @@ import getCartModel from '@/shared/API/cart/model/CartModel.ts'; import modal from '@/shared/Modal/model/ModalModel.ts'; import getStore from '@/shared/Store/Store.ts'; import { LANGUAGE_CHOICE } from '@/shared/constants/common.ts'; -import { PAGE_ID } from '@/shared/constants/pages.ts'; -import { buildPathName } from '@/shared/utils/buildPathname.ts'; +import * as buildPath from '@/shared/utils/buildPathname.ts'; import { productAddedToCartMessage } from '@/shared/utils/messageTemplates.ts'; import { showErrorMessage, showSuccessMessage } from '@/shared/utils/userMessage.ts'; import ProductInfoModel from '@/widgets/ProductInfo/model/ProductInfoModel.ts'; @@ -34,7 +33,7 @@ class ProductCardModel { this.currentSize = currentSize ?? this.params.variant[0].size; this.currentVariant = this.params.variant.find(({ size }) => size === currentSize) ?? this.params.variant[0]; this.view = new ProductCardView(params, currentSize); - this.price = new ProductPriceModel(this.currentVariant); + this.price = new ProductPriceModel({ new: this.currentVariant.discount, old: this.currentVariant.price }); this.wishlistButton = new WishlistButtonModel(this.params); this.init(cart); } @@ -62,10 +61,9 @@ class ProductCardModel { const goDetailsPageLink = this.view.getGoDetailsPageLink(); goDetailsPageLink.getHTML().addEventListener('click', (event) => { event.preventDefault(); - const path = buildPathName(PAGE_ID.PRODUCT_PAGE, this.params.key, { + const path = buildPath.productPathWithIDAndQuery(this.params.key, { size: [this.currentSize ?? this.params.variant[0].size], }); - RouterModel.getInstance().navigateTo(path); }); } diff --git a/src/entities/ProductCard/view/ProductCardView.ts b/src/entities/ProductCard/view/ProductCardView.ts index efdcd475..7cae8f2d 100644 --- a/src/entities/ProductCard/view/ProductCardView.ts +++ b/src/entities/ProductCard/view/ProductCardView.ts @@ -7,11 +7,10 @@ import getStore from '@/shared/Store/Store.ts'; import observeStore, { selectCurrentLanguage } from '@/shared/Store/observer.ts'; import { MORE_TEXT } from '@/shared/constants/buttons.ts'; import { LANGUAGE_CHOICE } from '@/shared/constants/common.ts'; -import { PAGE_ID } from '@/shared/constants/pages.ts'; import { PRODUCT_INFO_TEXT } from '@/shared/constants/product.ts'; import { LOADER_SIZE } from '@/shared/constants/sizes.ts'; import SVG_DETAILS from '@/shared/constants/svg.ts'; -import { buildPathName } from '@/shared/utils/buildPathname.ts'; +import * as buildPath from '@/shared/utils/buildPathname.ts'; import createBaseElement from '@/shared/utils/createBaseElement.ts'; import createSVGUse from '@/shared/utils/createSVGUse.ts'; @@ -135,9 +134,7 @@ class ProductCardView { } private createGoDetailsPageLink(): LinkModel { - const href = `${buildPathName(PAGE_ID.PRODUCT_PAGE, this.params.key, { - size: [this.currentSize ?? this.params.variant[0].size], - })}`; + const href = `${buildPath.productPathWithIDAndQuery(this.params.key, { size: [this.currentSize ?? this.params.variant[0].size] })}`; this.goDetailsPageLink = new LinkModel({ attrs: { diff --git a/src/entities/ProductCard/view/productCardView.module.scss b/src/entities/ProductCard/view/productCardView.module.scss index 5d3fee0a..91ecf2b0 100644 --- a/src/entities/ProductCard/view/productCardView.module.scss +++ b/src/entities/ProductCard/view/productCardView.module.scss @@ -135,6 +135,7 @@ width: 100%; max-width: 90%; font: var(--regular-font); + line-height: 150%; letter-spacing: var(--one); text-align: left; text-overflow: ellipsis; diff --git a/src/entities/ProductPrice/model/ProductPriceModel.ts b/src/entities/ProductPrice/model/ProductPriceModel.ts index 95103a9c..84a8e4ec 100644 --- a/src/entities/ProductPrice/model/ProductPriceModel.ts +++ b/src/entities/ProductPrice/model/ProductPriceModel.ts @@ -1,12 +1,10 @@ -import type { Variant } from '@/shared/types/product.ts'; - import ProductPriceView from '../view/ProductPriceView.ts'; class ProductPriceModel { private view: ProductPriceView; - constructor(currentVariant: Variant) { - this.view = new ProductPriceView(currentVariant); + constructor(params: { new: number; old: number }) { + this.view = new ProductPriceView(params); this.init(); } @@ -15,6 +13,10 @@ class ProductPriceModel { public getHTML(): HTMLDivElement { return this.view.getHTML(); } + + public updatePrice(params: { new: number; old: number }): void { + this.view.updatesPrice(params); + } } export default ProductPriceModel; diff --git a/src/entities/ProductPrice/view/ProductPriceView.ts b/src/entities/ProductPrice/view/ProductPriceView.ts index a9fe893a..6226b399 100644 --- a/src/entities/ProductPrice/view/ProductPriceView.ts +++ b/src/entities/ProductPrice/view/ProductPriceView.ts @@ -1,5 +1,3 @@ -import type { Variant } from '@/shared/types/product'; - import createBaseElement from '@/shared/utils/createBaseElement.ts'; import './productPriceView.scss'; @@ -7,30 +5,28 @@ import './productPriceView.scss'; class ProductPriceView { private basicPrice: HTMLSpanElement; - private currentVariant: Variant; - private oldPrice: HTMLSpanElement; + private params: { new: number; old: number }; + private view: HTMLDivElement; - constructor(currentVariant: Variant) { - this.currentVariant = currentVariant; + constructor(params: { new: number; old: number }) { + this.params = params; this.basicPrice = this.createBasicPrice(); this.oldPrice = this.createOldPrice(); this.view = this.createHTML(); } private createBasicPrice(): HTMLSpanElement { - const innerContent = this.currentVariant.discount - ? `$${this.currentVariant.discount.toFixed(2)}` - : `$${this.currentVariant.price?.toFixed(2)}`; + const innerContent = this.getBasePrice(); // this.params.new ? `$${this.params.new.toFixed(2)}` : `$${this.params.old?.toFixed(2)}`; this.basicPrice = createBaseElement({ cssClasses: ['basicPrice'], innerContent, tag: 'span', }); - if (!this.currentVariant.discount) { + if (!this.params.new) { this.basicPrice.classList.add('gray'); } @@ -48,7 +44,7 @@ class ProductPriceView { } private createOldPrice(): HTMLSpanElement { - const innerContent = this.currentVariant.discount ? `$${this.currentVariant.price?.toFixed(2)}` : ''; + const innerContent = this.getOldPrice(); // this.params.new ? `$${this.params.old.toFixed(2)}` : ''; this.oldPrice = createBaseElement({ cssClasses: ['oldPrice'], innerContent, @@ -58,9 +54,28 @@ class ProductPriceView { return this.oldPrice; } + private getBasePrice(): string { + return this.params.new ? `$${this.params.new.toFixed(2)}` : `$${this.params.old?.toFixed(2)}`; + } + + private getOldPrice(): string { + return this.params.new ? `$${this.params.old.toFixed(2)}` : ''; + } + public getHTML(): HTMLDivElement { return this.view; } + + public updatesPrice(params: { new: number; old: number }): void { + this.params = params; + this.basicPrice.textContent = this.getBasePrice(); + this.oldPrice.textContent = this.getOldPrice(); + if (!this.params.new) { + this.basicPrice.classList.add('gray'); + } else { + this.basicPrice.classList.remove('gray'); + } + } } export default ProductPriceView; diff --git a/src/features/Breadcrumbs/model/BreadcrumbsModel.ts b/src/features/Breadcrumbs/model/BreadcrumbsModel.ts index 09fb53e7..9ea617b1 100644 --- a/src/features/Breadcrumbs/model/BreadcrumbsModel.ts +++ b/src/features/Breadcrumbs/model/BreadcrumbsModel.ts @@ -1,17 +1,29 @@ -import type { BreadCrumbLink } from '@/shared/types/link.ts'; +import type { BreadcrumbLink } from '@/shared/types/link.ts'; import BreadcrumbsView from '../view/BreadcrumbsView.ts'; class BreadcrumbsModel { + private breadcrumbLinksData: BreadcrumbLink[] = []; + private view: BreadcrumbsView; - constructor(navigationLinks: BreadCrumbLink[]) { - this.view = new BreadcrumbsView(navigationLinks); + constructor() { + this.view = new BreadcrumbsView(this.breadcrumbLinksData); + } + + public addBreadcrumbLinks(linkData: BreadcrumbLink[]): void { + this.breadcrumbLinksData.push(...linkData); + this.view.drawLinks(); } public getHTML(): HTMLDivElement { return this.view.getHTML(); } + + public removeBreadcrumbLink(linkData: BreadcrumbLink): void { + this.breadcrumbLinksData = this.breadcrumbLinksData.filter((link) => link !== linkData); + this.view.drawLinks(); + } } export default BreadcrumbsModel; diff --git a/src/features/Breadcrumbs/view/BreadcrumbsView.ts b/src/features/Breadcrumbs/view/BreadcrumbsView.ts index 2ffbff95..d5d92cf4 100644 --- a/src/features/Breadcrumbs/view/BreadcrumbsView.ts +++ b/src/features/Breadcrumbs/view/BreadcrumbsView.ts @@ -1,4 +1,4 @@ -import type { BreadCrumbLink } from '@/shared/types/link'; +import type { BreadcrumbLink } from '@/shared/types/link'; import RouterModel from '@/app/Router/model/RouterModel.ts'; import LinkModel from '@/shared/Link/model/LinkModel.ts'; @@ -10,52 +10,53 @@ import styles from './breadcrumbsView.module.scss'; const DELIMITER = '>'; class BreadcrumbsView { + private breadcrumbLinkData: BreadcrumbLink[] = []; + private view: HTMLDivElement; - constructor(navigationLinks: BreadCrumbLink[]) { - this.view = this.createHTML(navigationLinks); + constructor(breadcrumbLinkData: BreadcrumbLink[]) { + this.breadcrumbLinkData = breadcrumbLinkData; + this.view = this.createHTML(); } - private createHTML(navigationLinks: BreadCrumbLink[]): HTMLDivElement { + private createHTML(): HTMLDivElement { this.view = createBaseElement({ cssClasses: [styles.breadcrumbs], tag: 'div', }); - navigationLinks.forEach((linkParams) => { - this.createLink(linkParams).getHTML(); - }); - - this.view.lastChild?.remove(); - this.view.lastElementChild?.classList.add(styles.active); + this.drawLinks(); return this.view; } - private createLink(linkParams: BreadCrumbLink): LinkModel { - const link = new LinkModel({ - attrs: { - href: linkParams.link, - }, - classes: [styles.link], - text: formattedText(linkParams.name), - }); - - link.getHTML().addEventListener('click', (event) => { - event.preventDefault(); - RouterModel.getInstance().navigateTo(linkParams.link); - }); - - const delimiter = createBaseElement({ - cssClasses: [styles.delimiter], - innerContent: DELIMITER, - tag: 'span', + public drawLinks(): void { + this.view.innerHTML = ''; + this.breadcrumbLinkData.forEach((linkParams) => { + const link = new LinkModel({ + attrs: { + href: linkParams.link, + }, + classes: [styles.link], + text: formattedText(linkParams.name), + }); + + link.getHTML().addEventListener('click', (event) => { + event.preventDefault(); + RouterModel.getInstance().navigateTo(linkParams.link); + }); + + const delimiter = createBaseElement({ + cssClasses: [styles.delimiter], + innerContent: DELIMITER, + tag: 'span', + }); + + this.view.append(link.getHTML(), delimiter); }); - this.view.append(link.getHTML()); - this.view.append(delimiter); - - return link; + this.view.lastChild?.remove(); + this.view.lastElementChild?.classList.add(styles.active); } public getHTML(): HTMLDivElement { diff --git a/src/features/WishlistButton/model/WishlistButtonModel.ts b/src/features/WishlistButton/model/WishlistButtonModel.ts index a8839170..e9bc0161 100644 --- a/src/features/WishlistButton/model/WishlistButtonModel.ts +++ b/src/features/WishlistButton/model/WishlistButtonModel.ts @@ -3,8 +3,10 @@ import type { Product } from '@/shared/types/product.ts'; import type { ShoppingList, ShoppingListProduct } from '@/shared/types/shopping-list.ts'; import getShoppingListModel from '@/shared/API/shopping-list/model/ShoppingListModel.ts'; +import EventMediatorModel from '@/shared/EventMediator/model/EventMediatorModel.ts'; import getStore from '@/shared/Store/Store.ts'; import { LANGUAGE_CHOICE } from '@/shared/constants/common.ts'; +import MEDIATOR_EVENT from '@/shared/constants/events.ts'; import { productAddedToWishListMessage, productRemovedFromWishListMessage } from '@/shared/utils/messageTemplates.ts'; import { showErrorMessage, showSuccessMessage } from '@/shared/utils/userMessage.ts'; @@ -30,6 +32,7 @@ class WishlistButtonModel { ), ); this.view.switchStateWishListButton(true); + EventMediatorModel.getInstance().notify(MEDIATOR_EVENT.REDRAW_WISHLIST, ''); }) .catch(showErrorMessage); } @@ -44,6 +47,7 @@ class WishlistButtonModel { ), ); this.view.switchStateWishListButton(false); + EventMediatorModel.getInstance().notify(MEDIATOR_EVENT.REDRAW_WISHLIST, ''); }) .catch(showErrorMessage); } diff --git a/src/features/WishlistButton/view/wishlistButtonView.module.scss b/src/features/WishlistButton/view/wishlistButtonView.module.scss index 5769ba6a..f1403e09 100644 --- a/src/features/WishlistButton/view/wishlistButtonView.module.scss +++ b/src/features/WishlistButton/view/wishlistButtonView.module.scss @@ -22,10 +22,10 @@ @media (hover: hover) { &:hover { - outline: calc(var(--one) * 1.5) solid var(--red-power-600); + outline: calc(var(--one) * 1.5) solid var(--steam-green-800); svg { - fill: var(--red-power-600); + fill: var(--steam-green-800); } } } @@ -44,9 +44,9 @@ } .inWishList { - outline: calc(var(--one) * 1.5) solid var(--red-power-600); + outline: calc(var(--one) * 1.5) solid var(--steam-green-800); svg { - fill: var(--red-power-600); + fill: var(--steam-green-800); } } diff --git a/src/pages/CartPage/model/CartPageModel.ts b/src/pages/CartPage/model/CartPageModel.ts index 2052d82c..111a7906 100644 --- a/src/pages/CartPage/model/CartPageModel.ts +++ b/src/pages/CartPage/model/CartPageModel.ts @@ -1,4 +1,3 @@ -import type { Cart } from '@/shared/types/cart.ts'; import type { Page } from '@/shared/types/page.ts'; import getCartModel from '@/shared/API/cart/model/CartModel.ts'; @@ -9,6 +8,7 @@ import observeStore, { selectCurrentLanguage } from '@/shared/Store/observer.ts' import { SERVER_MESSAGE_KEYS } from '@/shared/constants/messages.ts'; import { PAGE_ID } from '@/shared/constants/pages.ts'; import { LOADER_SIZE } from '@/shared/constants/sizes.ts'; +import { type Cart, CartActive } from '@/shared/types/cart.ts'; import { promoCodeAppliedMessage } from '@/shared/utils/messageTemplates.ts'; import { showErrorMessage, showSuccessMessage } from '@/shared/utils/userMessage.ts'; import ProductOrderModel from '@/widgets/ProductOrder/model/ProductOrderModel.ts'; @@ -38,6 +38,14 @@ class CartPageModel implements Page { if (cart) { showSuccessMessage(promoCodeAppliedMessage(discountCode)); this.cart = cart; + this.productsItem.forEach((productItem) => { + const idLine = productItem.getProduct().lineItemId; + const updateLine = this.cart?.products.find((item) => item.lineItemId === idLine); + if (updateLine) { + productItem.setProduct(updateLine); + productItem.updateProductHandler(CartActive.UPDATE).catch(showErrorMessage); + } + }); this.view.updateTotal(this.cart); } }) diff --git a/src/pages/CartPage/view/CartPageView.ts b/src/pages/CartPage/view/CartPageView.ts index 38597fe8..153e2fac 100644 --- a/src/pages/CartPage/view/CartPageView.ts +++ b/src/pages/CartPage/view/CartPageView.ts @@ -52,7 +52,9 @@ class CartPageView { private couponButton: HTMLButtonElement; - private discount: HTMLParagraphElement; + private discountList: HTMLUListElement; + + private discountTotal: HTMLElement; private empty: HTMLDivElement; @@ -76,6 +78,8 @@ class CartPageView { private total: HTMLParagraphElement; + private totalDiscountTitle: HTMLParagraphElement; + private totalWrap: HTMLDivElement; constructor(parent: HTMLDivElement, clearCallback: ClearCallback, addDiscountCallback: DiscountCallback) { @@ -89,13 +93,19 @@ class CartPageView { 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.discountTotal = createBaseElement({ cssClasses: [styles.couponsWrap], tag: 'summary' }); + this.discountList = createBaseElement({ cssClasses: [styles.couponsList], tag: 'ul' }); this.couponButton = createBaseElement({ cssClasses: [styles.button, styles.applyBtn], innerContent: TITLE.BUTTON_COUPON[this.language], tag: 'button', }); - this.clear = new ButtonModel({ classes: [styles.clear], text: TITLE.CLEAR[this.language] }); + this.totalDiscountTitle = createBaseElement({ + cssClasses: [styles.title], + innerContent: TITLE.COUPON_DISCOUNT[this.language], + tag: 'p', + }); + this.clear = new ButtonModel({ classes: [styles.continue, styles.clear], text: TITLE.CLEAR[this.language] }); this.totalWrap = this.createWrapHTML(); this.totalWrap.classList.add(styles.total); this.page.append(this.productWrap); @@ -230,15 +240,10 @@ class CartPageView { return tdDelete; } - private createDiscountHTML(): HTMLDivElement { - const discountWrap = createBaseElement({ cssClasses: [styles.totalWrap], tag: 'div' }); - const discountTitle = createBaseElement({ - cssClasses: [styles.title], - innerContent: TITLE.COUPON_DISCOUNT[this.language], - tag: 'p', - }); - discountWrap.append(discountTitle, this.discount); - this.textElement.push({ element: discountTitle, textItem: TITLE.COUPON_DISCOUNT }); + private createDiscountHTML(): HTMLDetailsElement { + const discountWrap = createBaseElement({ cssClasses: [styles.totalWrap], tag: 'details' }); + discountWrap.append(this.discountTotal, this.discountList); + this.textElement.push({ element: this.totalDiscountTitle, textItem: TITLE.COUPON_DISCOUNT }); return discountWrap; } @@ -311,6 +316,8 @@ class CartPageView { public renderCart(productsItem: ProductOrderModel[]): void { this.productWrap.innerHTML = ''; this.totalWrap.innerHTML = ''; + this.discountTotal.innerHTML = ''; + this.discountList.innerHTML = ''; this.productWrap.classList.remove(styles.hide); this.totalWrap.classList.remove(styles.hide); this.empty.classList.add(styles.hide); @@ -324,6 +331,8 @@ class CartPageView { public renderEmpty(): void { this.productWrap.innerHTML = ''; this.totalWrap.innerHTML = ''; + this.discountTotal.innerHTML = ''; + this.discountList.innerHTML = ''; this.productWrap.classList.add(styles.hide); this.totalWrap.classList.add(styles.hide); this.empty.classList.remove(styles.hide); @@ -342,8 +351,37 @@ class CartPageView { } public updateTotal(cart: Cart): void { - this.subTotal.innerHTML = cartPrice((cart.total + cart.discounts).toFixed(2)); - this.discount.innerHTML = cartPrice(cart.discounts.toFixed(2)); + this.discountTotal.innerHTML = ''; + this.discountList.innerHTML = ''; + let totalDiscount = 0; + cart.discounts.forEach((discount) => { + const couponItem = createBaseElement({ cssClasses: [styles.couponWrap], tag: 'li' }); + const couponTitle = createBaseElement({ + cssClasses: [styles.title], + innerContent: discount.coupon.code, + tag: 'p', + }); + const couponValue = createBaseElement({ + cssClasses: [styles.title], + innerContent: `-$ ${discount.value.toFixed(2)}`, + tag: 'p', + }); + couponItem.append(couponTitle, couponValue); + this.discountList.append(couponItem); + totalDiscount += discount.value; + }); + if (totalDiscount) { + const totalDiscountWrap = createBaseElement({ cssClasses: [styles.couponWrap], tag: 'div' }); + const totalDiscountValue = createBaseElement({ + cssClasses: [styles.title, styles.totalDiscount], + innerContent: cartPrice(totalDiscount.toFixed(2)), + tag: 'p', + }); + totalDiscountWrap.append(this.totalDiscountTitle, totalDiscountValue); + this.discountTotal.append(totalDiscountWrap); + } + const subTotal = cart.total + totalDiscount; + this.subTotal.innerHTML = cartPrice(subTotal.toFixed(2)); this.total.innerHTML = cartPrice(cart.total.toFixed(2)); } } diff --git a/src/pages/CartPage/view/cartPageView.module.scss b/src/pages/CartPage/view/cartPageView.module.scss index cbe7f7c1..d8f566f7 100644 --- a/src/pages/CartPage/view/cartPageView.module.scss +++ b/src/pages/CartPage/view/cartPageView.module.scss @@ -111,6 +111,7 @@ padding: var(--tiny-offset) 0; font: var(--regular-font); color: var(--noble-gray-800); + transition: all 0.2s; } .totalWrap { @@ -125,6 +126,24 @@ } } +.couponsWrap { + display: flex; + align-items: center; + justify-content: space-between; + cursor: pointer; + + &:hover p { + color: var(--steam-green-800); + } +} + +.couponWrap { + display: flex; + align-items: center; + justify-content: space-between; + width: 100%; +} + .totalPrice { padding: var(--tiny-offset) 0; font: var(--bold-font); @@ -212,3 +231,12 @@ $color: var(--steam-green-800); .hide { display: none; } + +.couponsWrap:not(:empty)::after { + content: ''; + display: inline-block; + width: var(--tiny-offset); + height: var(--tiny-offset); + background: url('/img/png/expand-arrow.png') no-repeat center center; + background-size: contain; +} diff --git a/src/pages/LoginPage/model/LoginPageModel.ts b/src/pages/LoginPage/model/LoginPageModel.ts index a9d23cb6..96ef500f 100644 --- a/src/pages/LoginPage/model/LoginPageModel.ts +++ b/src/pages/LoginPage/model/LoginPageModel.ts @@ -4,7 +4,8 @@ import RouterModel from '@/app/Router/model/RouterModel.ts'; import getStore from '@/shared/Store/Store.ts'; import { setCurrentPage } from '@/shared/Store/actions.ts'; import observeStore, { selectIsUserLoggedIn } from '@/shared/Store/observer.ts'; -import { PAGE_ID, PAGE_LINK_TEXT, PAGE_LINK_TEXT_KEYS } from '@/shared/constants/pages.ts'; +import { PAGE_LINK_TEXT, PAGE_LINK_TEXT_KEYS } from '@/shared/constants/links.ts'; +import { PAGE_ID } from '@/shared/constants/pages.ts'; import observeCurrentLanguage from '@/shared/utils/observeCurrentLanguage.ts'; import LoginFormModel from '@/widgets/LoginForm/model/LoginFormModel.ts'; diff --git a/src/pages/LoginPage/view/LoginPageView.ts b/src/pages/LoginPage/view/LoginPageView.ts index 9beb1616..30dfd7ae 100644 --- a/src/pages/LoginPage/view/LoginPageView.ts +++ b/src/pages/LoginPage/view/LoginPageView.ts @@ -1,13 +1,12 @@ import LinkModel from '@/shared/Link/model/LinkModel.ts'; import getStore from '@/shared/Store/Store.ts'; +import { PAGE_LINK_TEXT, PAGE_LINK_TEXT_KEYS } from '@/shared/constants/links.ts'; import { PAGE_ANSWER, PAGE_ANSWER_KEYS, PAGE_DESCRIPTION, PAGE_DESCRIPTION_KEYS, PAGE_ID, - PAGE_LINK_TEXT, - PAGE_LINK_TEXT_KEYS, } from '@/shared/constants/pages.ts'; import createBaseElement from '@/shared/utils/createBaseElement.ts'; import observeCurrentLanguage from '@/shared/utils/observeCurrentLanguage.ts'; diff --git a/src/pages/ProductPage/model/ProductPageModel.ts b/src/pages/ProductPage/model/ProductPageModel.ts index f325afeb..152217e4 100644 --- a/src/pages/ProductPage/model/ProductPageModel.ts +++ b/src/pages/ProductPage/model/ProductPageModel.ts @@ -1,4 +1,4 @@ -import type { BreadCrumbLink } from '@/shared/types/link.ts'; +import type { BreadcrumbLink } from '@/shared/types/link.ts'; import type { Page, PageParams } from '@/shared/types/page.ts'; import type { Product, localization } from '@/shared/types/product.ts'; @@ -11,13 +11,15 @@ import observeStore, { selectCurrentLanguage } from '@/shared/Store/observer.ts' import { LANGUAGE_CHOICE } from '@/shared/constants/common.ts'; import { PAGE_ID } from '@/shared/constants/pages.ts'; import { SEARCH_PARAMS_FIELD } from '@/shared/constants/product.ts'; -import { buildPathName } from '@/shared/utils/buildPathname.ts'; +import * as buildPath from '@/shared/utils/buildPathname.ts'; import { showErrorMessage } from '@/shared/utils/userMessage.ts'; import ProductInfoModel from '@/widgets/ProductInfo/model/ProductInfoModel.ts'; import ProductPageView from '../view/ProductPageView.ts'; class ProductPageModel implements Page { + private breadcrumbs = new BreadcrumbsModel(); + private view: ProductPageView; constructor(parent: HTMLDivElement, params: PageParams) { @@ -25,31 +27,24 @@ class ProductPageModel implements Page { this.init(params); } - private createNavigationLinks(currentProduct: Product): BreadCrumbLink[] { + private createBreadcrumbLinks(currentProduct: Product): BreadcrumbLink[] { const category = currentProduct.category[0].parent; const subcategory = currentProduct.category[0]; - const links = [ - { - link: buildPathName(PAGE_ID.MAIN_PAGE, null, null), - name: PAGE_ID.MAIN_PAGE.toString(), - }, - { - link: buildPathName(PAGE_ID.CATALOG_PAGE, null, null), - name: PAGE_ID.CATALOG_PAGE.toString(), - }, + const links: BreadcrumbLink[] = [ + { link: PAGE_ID.MAIN_PAGE, name: PAGE_ID.MAIN_PAGE.toString() }, + { link: PAGE_ID.CATALOG_PAGE, name: PAGE_ID.CATALOG_PAGE.toString() }, ]; if (category) { links.push({ - link: buildPathName(PAGE_ID.CATALOG_PAGE, null, { category: [category.id] }), - + link: buildPath.catalogPathWithIDAndQuery(null, { category: [category.id] }), name: category.name[0].value, }); } if (subcategory && category) { links.push({ - link: buildPathName(PAGE_ID.CATALOG_PAGE, null, { category: [category.id], subcategory: [subcategory.id] }), + link: buildPath.catalogPathWithIDAndQuery(null, { category: [category.id], subcategory: [subcategory.id] }), name: subcategory.name[0].value, }); } @@ -82,8 +77,8 @@ class ProductPageModel implements Page { } private initBreadcrumbs(currentProduct: Product): void { - const links = this.createNavigationLinks(currentProduct); - this.getHTML().append(new BreadcrumbsModel(links).getHTML()); + this.breadcrumbs.addBreadcrumbLinks(this.createBreadcrumbLinks(currentProduct)); + this.getHTML().append(this.breadcrumbs.getHTML()); } private observeLanguage(fullDescription: localization[]): void { diff --git a/src/pages/ProductPage/view/productPageView.module.scss b/src/pages/ProductPage/view/productPageView.module.scss index 05e281d9..c96e8261 100644 --- a/src/pages/ProductPage/view/productPageView.module.scss +++ b/src/pages/ProductPage/view/productPageView.module.scss @@ -19,11 +19,13 @@ .fullDescriptionWrapper { font: var(--regular-font); + line-height: 170%; letter-spacing: var(--one); color: var(--steam-green-400); } .fullDescription { font: var(--regular-font); + line-height: 170%; color: var(--noble-gray-800); } diff --git a/src/pages/RegistrationPage/model/RegistrationPageModel.ts b/src/pages/RegistrationPage/model/RegistrationPageModel.ts index db00a3c5..a9239305 100644 --- a/src/pages/RegistrationPage/model/RegistrationPageModel.ts +++ b/src/pages/RegistrationPage/model/RegistrationPageModel.ts @@ -4,7 +4,8 @@ import RouterModel from '@/app/Router/model/RouterModel.ts'; import getStore from '@/shared/Store/Store.ts'; import { setCurrentPage } from '@/shared/Store/actions.ts'; import observeStore, { selectIsUserLoggedIn } from '@/shared/Store/observer.ts'; -import { PAGE_ID, PAGE_LINK_TEXT, PAGE_LINK_TEXT_KEYS } from '@/shared/constants/pages.ts'; +import { PAGE_LINK_TEXT, PAGE_LINK_TEXT_KEYS } from '@/shared/constants/links.ts'; +import { PAGE_ID } from '@/shared/constants/pages.ts'; import observeCurrentLanguage from '@/shared/utils/observeCurrentLanguage.ts'; import RegisterFormModel from '@/widgets/RegistrationForm/model/RegistrationFormModel.ts'; diff --git a/src/pages/RegistrationPage/view/RegistrationPageView.ts b/src/pages/RegistrationPage/view/RegistrationPageView.ts index fec17b5f..757fd9be 100644 --- a/src/pages/RegistrationPage/view/RegistrationPageView.ts +++ b/src/pages/RegistrationPage/view/RegistrationPageView.ts @@ -1,13 +1,12 @@ import LinkModel from '@/shared/Link/model/LinkModel.ts'; import getStore from '@/shared/Store/Store.ts'; +import { PAGE_LINK_TEXT, PAGE_LINK_TEXT_KEYS } from '@/shared/constants/links.ts'; import { PAGE_ANSWER, PAGE_ANSWER_KEYS, PAGE_DESCRIPTION, PAGE_DESCRIPTION_KEYS, PAGE_ID, - PAGE_LINK_TEXT, - PAGE_LINK_TEXT_KEYS, } from '@/shared/constants/pages.ts'; import createBaseElement from '@/shared/utils/createBaseElement.ts'; import observeCurrentLanguage from '@/shared/utils/observeCurrentLanguage.ts'; diff --git a/src/pages/WishlistPage/model/WishlistPageModel.ts b/src/pages/WishlistPage/model/WishlistPageModel.ts new file mode 100644 index 00000000..33b595bd --- /dev/null +++ b/src/pages/WishlistPage/model/WishlistPageModel.ts @@ -0,0 +1,44 @@ +import type { BreadcrumbLink } from '@/shared/types/link.ts'; +import type { Page } from '@/shared/types/page.ts'; + +import BreadcrumbsModel from '@/features/Breadcrumbs/model/BreadcrumbsModel.ts'; +import getStore from '@/shared/Store/Store.ts'; +import { setCurrentPage } from '@/shared/Store/actions.ts'; +import { PAGE_ID } from '@/shared/constants/pages.ts'; + +import WishlistPageView from '../view/WishlistPageView.ts'; + +class WishlistPageModel implements Page { + private breadcrumbs = new BreadcrumbsModel(); + + private view: WishlistPageView; + + constructor(parent: HTMLDivElement) { + this.view = new WishlistPageView(parent); + this.init(); + } + + private createBreadcrumbLinksData(): BreadcrumbLink[] { + return [ + { link: PAGE_ID.MAIN_PAGE, name: PAGE_ID.MAIN_PAGE.toString() }, + { link: PAGE_ID.CATALOG_PAGE, name: PAGE_ID.CATALOG_PAGE.toString() }, + { link: PAGE_ID.WISHLIST_PAGE, name: PAGE_ID.WISHLIST_PAGE.toString() }, + ]; + } + + private init(): void { + this.initBreadcrumbs(); + getStore().dispatch(setCurrentPage(PAGE_ID.WISHLIST_PAGE)); + } + + private initBreadcrumbs(): void { + this.breadcrumbs.addBreadcrumbLinks(this.createBreadcrumbLinksData()); + this.getHTML().append(this.breadcrumbs.getHTML()); + } + + public getHTML(): HTMLDivElement { + return this.view.getHTML(); + } +} + +export default WishlistPageModel; diff --git a/src/pages/WishlistPage/view/WishlistPageView.ts b/src/pages/WishlistPage/view/WishlistPageView.ts new file mode 100644 index 00000000..eaef7f82 --- /dev/null +++ b/src/pages/WishlistPage/view/WishlistPageView.ts @@ -0,0 +1,115 @@ +import type { OptionsRequest, ProductWithCount } from '@/shared/API/types/type.ts'; +import type { Cart } from '@/shared/types/cart.ts'; +import type { ShoppingList } from '@/shared/types/shopping-list'; + +import ProductCardModel from '@/entities/ProductCard/model/ProductCardModel.ts'; +import getCartModel from '@/shared/API/cart/model/CartModel.ts'; +import getProductModel from '@/shared/API/product/model/ProductModel.ts'; +import FilterProduct from '@/shared/API/product/utils/filter.ts'; +import getShoppingListModel from '@/shared/API/shopping-list/model/ShoppingListModel.ts'; +import { FilterFields } from '@/shared/API/types/type.ts'; +import EventMediatorModel from '@/shared/EventMediator/model/EventMediatorModel.ts'; +import LoaderModel from '@/shared/Loader/model/LoaderModel.ts'; +import getStore from '@/shared/Store/Store.ts'; +import observeStore, { selectCurrentLanguage } from '@/shared/Store/observer.ts'; +import MEDIATOR_EVENT from '@/shared/constants/events.ts'; +import { EMPTY_PRODUCT } from '@/shared/constants/product.ts'; +import { LOADER_SIZE } from '@/shared/constants/sizes.ts'; +import createBaseElement from '@/shared/utils/createBaseElement.ts'; +import { showErrorMessage } from '@/shared/utils/userMessage.ts'; + +import styles from './wishlistPageView.module.scss'; + +class WishlistPageView { + private page: HTMLDivElement; + + private parent: HTMLDivElement; + + private wishlist: HTMLUListElement; + + constructor(parent: HTMLDivElement) { + this.parent = parent; + this.parent.innerHTML = ''; + this.wishlist = this.createWishlist(); + this.page = this.createHTML(); + window.scrollTo(0, 0); + } + + private createHTML(): HTMLDivElement { + this.page = createBaseElement({ + cssClasses: [styles.wishlistPage], + tag: 'div', + }); + + this.page.append(this.wishlist); + this.parent.append(this.page); + + return this.page; + } + + private createWishlist(): HTMLUListElement { + this.wishlist = createBaseElement({ + cssClasses: [styles.wishlist], + tag: 'ul', + }); + + this.drawWishlist().catch(showErrorMessage); + EventMediatorModel.getInstance().subscribe(MEDIATOR_EVENT.REDRAW_WISHLIST, this.drawWishlist.bind(this)); + observeStore(selectCurrentLanguage, () => { + if (this.wishlist.classList.contains(styles.emptyList)) { + this.wishlist.textContent = EMPTY_PRODUCT[getStore().getState().currentLanguage].EMPTY; + } + }); + + return this.wishlist; + } + + private drawWishlistItems(shoppingList: ShoppingList, cart: Cart, products: ProductWithCount): void { + shoppingList.products.forEach((product) => { + const currentProduct = products.products.find((item) => item.id === product.productId); + if (currentProduct) { + this.wishlist.append(new ProductCardModel(currentProduct, null, cart).getHTML()); + } + }); + } + + public async drawWishlist(): Promise { + this.wishlist.innerHTML = ''; + const loader = new LoaderModel(LOADER_SIZE.EXTRA_LARGE); + loader.setAbsolutePosition(); + this.wishlist.append(loader.getHTML()); + const [shoppingList, cart] = await Promise.all([ + getShoppingListModel().getShoppingList(), + getCartModel().getCart(), + ]); + + const filter = new FilterProduct(); + shoppingList.products.forEach((product) => { + filter.addFilter(FilterFields.ID, product.productId); + }); + const options: OptionsRequest = { + filter, + limit: shoppingList.products.length, + }; + const products = await getProductModel().getProducts(options); + loader.getHTML().remove(); + this.drawWishlistItems(shoppingList, cart, products); + this.switchEmptyList(!shoppingList.products.length); + } + + public getHTML(): HTMLDivElement { + return this.page; + } + + public getWishlist(): HTMLUListElement { + return this.wishlist; + } + + public switchEmptyList(isEmpty: boolean): void { + this.wishlist.classList.toggle(styles.emptyList, isEmpty); + if (isEmpty) { + this.wishlist.textContent = EMPTY_PRODUCT[getStore().getState().currentLanguage].EMPTY; + } + } +} +export default WishlistPageView; diff --git a/src/pages/WishlistPage/view/wishlistPageView.module.scss b/src/pages/WishlistPage/view/wishlistPageView.module.scss new file mode 100644 index 00000000..86570218 --- /dev/null +++ b/src/pages/WishlistPage/view/wishlistPageView.module.scss @@ -0,0 +1,71 @@ +@import 'src/app/styles/mixins'; + +.wishlistPage { + position: relative; + display: flex; + flex-direction: column; + padding: 0 var(--small-offset); + animation: show 0.2s ease-out forwards; +} + +@keyframes show { + 0% { + opacity: 0; + } + + 100% { + display: flex; + opacity: 1; + } +} + +.wishlist { + position: relative; + display: grid; + align-items: stretch; + justify-content: center; + order: 2; + grid-template-columns: repeat(3, auto); + height: max-content; + min-height: 20.438rem; + font-size: var(--regular-font); + letter-spacing: var(--one); + text-align: center; + color: var(--steam-green-500); + gap: var(--small-offset); + + @media (max-width: 970px) { + grid-template-columns: repeat(2, max-content); + } + + @media (max-width: 690px) { + grid-template-columns: repeat(1, 1fr); + } + + @media (min-width: 5300px) { + grid-template-columns: repeat(4, max-content); + } + + @media (min-width: 6600px) { + grid-template-columns: repeat(5, max-content); + } + + &.emptyList { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding-top: 12rem; + height: 100%; + min-height: 20.438rem; + background: url('../../../shared/img/png/notFound.png'); + background-position: center 0; + background-size: calc(var(--extra-large-offset) * 2); + background-repeat: no-repeat; + gap: var(--tiny-offset); + } +} + +.goToCatalogLink { + @include link(calc(var(--extra-small-offset) / 2) 0); +} diff --git a/src/shared/API/cart/model/CartModel.ts b/src/shared/API/cart/model/CartModel.ts index e26cf5ef..bcb2cff5 100644 --- a/src/shared/API/cart/model/CartModel.ts +++ b/src/shared/API/cart/model/CartModel.ts @@ -1,4 +1,4 @@ -import type { AddCartItem, Cart, CartProduct, EditCartItem } from '@/shared/types/cart.ts'; +import type { AddCartItem, Cart, CartCoupon, CartProduct, EditCartItem } from '@/shared/types/cart.ts'; import type { CartPagedQueryResponse, Cart as CartResponse, @@ -15,6 +15,7 @@ import { showErrorMessage } from '@/shared/utils/userMessage.ts'; import type { OptionsRequest } from '../../types/type.ts'; +import getDiscountModel from '../../discount/model/DiscountModel.ts'; import getProductModel from '../../product/model/ProductModel.ts'; import FilterProduct from '../../product/utils/filter.ts'; import { Attribute, FilterFields } from '../../types/type.ts'; @@ -23,6 +24,7 @@ import getCartApi, { type CartApi } from '../CartApi.ts'; enum ACTIONS { addDiscountCode = 'addDiscountCode', + removeDiscountCode = 'removeDiscountCode', removeLineItem = 'removeLineItem', setAnonymousId = 'setAnonymousId', } @@ -55,10 +57,22 @@ export class CartModel { if (data.anonymousId && !authToken && !anonymousId) { getStore().dispatch(setAnonymousId(data.anonymousId)); } - const discount = data.discountOnTotalPrice?.discountedAmount?.centAmount; + const discounts: CartCoupon[] = []; + if (data.discountOnTotalPrice && data.discountOnTotalPrice.includedDiscounts.length) { + const allDiscounts = getDiscountModel().getAllCoupons(); + data.discountOnTotalPrice.includedDiscounts.forEach((discount) => { + const findDiscount = allDiscounts.find((el) => el.id === discount.discount.id); + if (findDiscount && discount.discountedAmount.centAmount > 0) { + discounts.push({ + coupon: findDiscount, + value: discount.discountedAmount.centAmount / PRICE_FRACTIONS || 0, + }); + } + }); + } return { anonymousId: data.anonymousId || null, - discounts: discount ? discount / PRICE_FRACTIONS : 0, + discounts, id: data.id, products: data.lineItems.map((lineItem) => this.adaptLineItem(lineItem)), total: data.totalPrice.centAmount / PRICE_FRACTIONS || 0, @@ -70,16 +84,25 @@ export class CartModel { const price = product.price.discounted?.value.centAmount ? product.price.discounted?.value.centAmount : product.price.value.centAmount; + const priceCoupon = + product.discountedPricePerQuantity.length && + product.discountedPricePerQuantity[0].discountedPrice.value.centAmount + ? product.discountedPricePerQuantity[0].discountedPrice.value.centAmount + : 0; const result: CartProduct = { images: product.variant.images?.length ? product.variant.images[0].url : '', key: product.productKey || '', lineItemId: product.id, name: [], price: price / PRICE_FRACTIONS || 0, + priceCouponDiscount: priceCoupon / PRICE_FRACTIONS || 0, productId: product.productId || '', quantity: product.quantity || 0, size: null, - totalPrice: product.totalPrice.centAmount / PRICE_FRACTIONS || 0, + totalPrice: priceCoupon + ? (price * product.quantity) / PRICE_FRACTIONS || 0 + : product.totalPrice.centAmount / PRICE_FRACTIONS || 0, + totalPriceCouponDiscount: priceCoupon ? product.totalPrice.centAmount / PRICE_FRACTIONS || 0 : 0, }; result.name.push(...getProductModel().adaptLocalizationValue(product.name)); if (product.variant.attributes) { @@ -118,7 +141,7 @@ export class CartModel { private getCartFromData(data: CartResponse | ClientResponse): Cart { let cart: Cart = { anonymousId: null, - discounts: 0, + discounts: [], id: '', products: [], total: 0, @@ -249,6 +272,24 @@ export class CartModel { }); } + public async deleteCoupon(id: string): Promise { + if (!this.cart) { + this.cart = await this.getCart(); + } + const action: MyCartUpdateAction[] = [ + { + action: ACTIONS.removeDiscountCode, + discountCode: { + id, + typeId: 'discount-code', + }, + }, + ]; + const data = await this.root.updateCart(this.cart, action); + this.cart = this.getCartFromData(data); + return this.cart; + } + public async deleteProductFromCart(products: CartProduct): Promise { if (!this.cart) { this.cart = await this.getCart(); diff --git a/src/shared/API/discount/DiscountApi.ts b/src/shared/API/discount/DiscountApi.ts new file mode 100644 index 00000000..30511ba4 --- /dev/null +++ b/src/shared/API/discount/DiscountApi.ts @@ -0,0 +1,24 @@ +import type { ClientResponse, DiscountCodePagedQueryResponse } from '@commercetools/platform-sdk'; + +import getApiClient, { type ApiClient } from '../sdk/client.ts'; + +export class DiscountApi { + private client: ApiClient; + + constructor() { + this.client = getApiClient(); + } + + public async getCoupons(): Promise> { + const data = await this.client.apiRoot().discountCodes().get().execute(); + return data; + } +} + +const createDiscountApi = (): DiscountApi => new DiscountApi(); + +const discountApi = createDiscountApi(); + +export default function getDiscountApi(): DiscountApi { + return discountApi; +} diff --git a/src/shared/API/discount/model/DiscountModel.ts b/src/shared/API/discount/model/DiscountModel.ts new file mode 100644 index 00000000..9104d855 --- /dev/null +++ b/src/shared/API/discount/model/DiscountModel.ts @@ -0,0 +1,53 @@ +import type { Coupon } from '@/shared/types/cart.ts'; +import type { ClientResponse, DiscountCode, DiscountCodePagedQueryResponse } from '@commercetools/platform-sdk'; + +import { showErrorMessage } from '@/shared/utils/userMessage.ts'; + +import { isClientResponse, isDiscountCodePagedQueryResponse } from '../../types/validation.ts'; +import getDiscountApi, { type DiscountApi } from '../DiscountApi.ts'; + +export class DiscountModel { + private coupons: Coupon[] = []; + + private root: DiscountApi; + + constructor() { + this.root = getDiscountApi(); + this.init().catch(showErrorMessage); + } + + private adaptCoupon(data: DiscountCode): Coupon { + return { + code: data.code, + id: data.cartDiscounts[0].id, + }; + } + + private getCouponsFromData(data: ClientResponse): Coupon[] { + const coupons: Coupon[] = []; + if (isClientResponse(data) && isDiscountCodePagedQueryResponse(data.body)) { + coupons.push(...data.body.results.map((el) => this.adaptCoupon(el))); + } + return coupons; + } + + private async init(): Promise { + if (!this.coupons.length) { + const data = await this.root.getCoupons(); + this.coupons = this.getCouponsFromData(data); + } + return this.coupons; + } + + public getAllCoupons(): Coupon[] { + return this.coupons; + } +} + +const createDiscountModel = (): DiscountModel => new DiscountModel(); + +const discountModel = createDiscountModel(); + +export default function getDiscountModel(): DiscountModel { + return discountModel; +} diff --git a/src/shared/API/sdk/client.ts b/src/shared/API/sdk/client.ts index 91e9b139..6b6edbc1 100644 --- a/src/shared/API/sdk/client.ts +++ b/src/shared/API/sdk/client.ts @@ -119,7 +119,6 @@ export class ApiClient { anonymousId, }, }; - client.withAnonymousSessionFlow(anonymOptions); getStore().dispatch(setAnonymousId(anonymousId)); this.anonymConnection = this.getConnection(client.build()); diff --git a/src/shared/API/types/validation.ts b/src/shared/API/types/validation.ts index 2dd9e480..51c14a93 100644 --- a/src/shared/API/types/validation.ts +++ b/src/shared/API/types/validation.ts @@ -7,6 +7,7 @@ import type { Customer, CustomerPagedQueryResponse, CustomerSignInResult, + DiscountCodePagedQueryResponse, ErrorResponse, FacetRange, FacetTerm, @@ -298,3 +299,18 @@ export function isShoppingListPagedQueryResponse(data: unknown): data is Shoppin Array.isArray(data.results), ); } + +export function isDiscountCodePagedQueryResponse(data: unknown): data is DiscountCodePagedQueryResponse { + return Boolean( + typeof data === 'object' && + data && + 'count' in data && + typeof data.count === 'number' && + 'limit' in data && + typeof data.limit === 'number' && + 'total' in data && + typeof data.total === 'number' && + 'results' in data && + Array.isArray(data.results), + ); +} diff --git a/src/shared/constants/events.ts b/src/shared/constants/events.ts index 64ee591b..3fdb46ef 100644 --- a/src/shared/constants/events.ts +++ b/src/shared/constants/events.ts @@ -3,6 +3,7 @@ const MEDIATOR_EVENT = { REDRAW_PRODUCTS: 'REDRAW_PRODUCTS', REDRAW_USER_ADDRESSES: 'REDRAW_USER_ADDRESSES', REDRAW_USER_INFO: 'REDRAW_USER_INFO', + REDRAW_WISHLIST: 'REDRAW_WISHLIST', } as const; export default MEDIATOR_EVENT; diff --git a/src/shared/constants/links.ts b/src/shared/constants/links.ts index e6c783dd..86a3744b 100644 --- a/src/shared/constants/links.ts +++ b/src/shared/constants/links.ts @@ -10,3 +10,31 @@ export const USER_PROFILE_MENU_LINK = { export const LINK_DETAILS = { BLANK: '_blank', } as const; + +export const PAGE_LINK_TEXT = { + en: { + ABOUT: 'About us', + BLOG: 'Blog', + CATALOG: 'Catalog', + LOGIN: 'Login', + MAIN: 'Main', + REGISTRATION: 'Register', + }, + ru: { + ABOUT: 'О нас', + BLOG: 'Блог', + CATALOG: 'Каталог', + LOGIN: 'Вход', + MAIN: 'Главная', + REGISTRATION: 'Регистрация', + }, +} as const; + +export const PAGE_LINK_TEXT_KEYS = { + ABOUT: 'ABOUT', + BLOG: 'BLOG', + CATALOG: 'CATALOG', + LOGIN: 'LOGIN', + MAIN: 'MAIN', + REGISTRATION: 'REGISTRATION', +} as const; diff --git a/src/shared/constants/pages.ts b/src/shared/constants/pages.ts index 6309345b..7299ecfc 100644 --- a/src/shared/constants/pages.ts +++ b/src/shared/constants/pages.ts @@ -33,34 +33,6 @@ export const PAGE_TITLE: Record> = { }, } as const; -export const PAGE_LINK_TEXT = { - en: { - ABOUT: 'About us', - BLOG: 'Blog', - CATALOG: 'Catalog', - LOGIN: 'Login', - MAIN: 'Main', - REGISTRATION: 'Register', - }, - ru: { - ABOUT: 'О нас', - BLOG: 'Блог', - CATALOG: 'Каталог', - LOGIN: 'Вход', - MAIN: 'Главная', - REGISTRATION: 'Регистрация', - }, -} as const; - -export const PAGE_LINK_TEXT_KEYS = { - ABOUT: 'ABOUT', - BLOG: 'BLOG', - CATALOG: 'CATALOG', - LOGIN: 'LOGIN', - MAIN: 'MAIN', - REGISTRATION: 'REGISTRATION', -} as const; - export const PAGE_DESCRIPTION = { en: { 404: 'This is not the page you are looking for. Please go back to the main page.', diff --git a/src/shared/types/cart.ts b/src/shared/types/cart.ts index 3377c37e..937548cc 100644 --- a/src/shared/types/cart.ts +++ b/src/shared/types/cart.ts @@ -2,7 +2,7 @@ import type { SizeType, localization } from './product.ts'; export interface Cart { anonymousId: null | string; - discounts: number; + discounts: CartCoupon[]; id: string; products: CartProduct[]; total: number; @@ -15,10 +15,12 @@ export interface CartProduct { lineItemId: string; name: localization[]; price: number; + priceCouponDiscount: number; productId: string; quantity: number; size: SizeType | null; totalPrice: number; + totalPriceCouponDiscount: number; } export interface AddCartItem { @@ -37,4 +39,15 @@ export enum CartActive { DELETE = 'delete', MINUS = 'minus', PLUS = 'plus', + UPDATE = 'update', +} + +export interface Coupon { + code: string; + id: string; +} + +export interface CartCoupon { + coupon: Coupon; + value: number; } diff --git a/src/shared/types/link.ts b/src/shared/types/link.ts index 73a546ea..82f1c654 100644 --- a/src/shared/types/link.ts +++ b/src/shared/types/link.ts @@ -4,7 +4,7 @@ export interface LinkAttributes { text?: string; } -export interface BreadCrumbLink { +export interface BreadcrumbLink { link: string; name: string; } diff --git a/src/shared/utils/buildPathname.ts b/src/shared/utils/buildPathname.ts index 4f41af0a..1bae68aa 100644 --- a/src/shared/utils/buildPathname.ts +++ b/src/shared/utils/buildPathname.ts @@ -1,11 +1,30 @@ -// eslint-disable-next-line import/prefer-default-export -export const buildPathName = ( +import { PAGE_ID } from '../constants/pages.ts'; + +type QueryParamsType = { + [key: string]: (null | string)[]; +}; + +type BuildQuery = (queryParams: QueryParamsType | null) => string; + +type BuildPathWithID = (endpoint: null | string, id: null | string) => string; +type BuildPathWithIDAndQuery = ( endpoint: null | string, id: null | string, - queryParams: { [key: string]: (null | string)[] } | null, -): string => { + queryParams: QueryParamsType | null, +) => string; +type BuildPathWithIDAndQueryAndHash = ( + endpoint: null | string, + id: null | string, + queryParams: QueryParamsType | null, + hash: null | string, +) => string; + +export const buildPathWithID: BuildPathWithID = (endpoint, id = null) => + `${endpoint ? `${endpoint}` : ''}${id ? `/${id}` : ''}`; + +export const buildQuery: BuildQuery = (queryParams) => { if (!queryParams) { - return `${endpoint ? `${endpoint}` : ''}${id ? `${id} ` : ''}`; + return ''; } const queryString = Object.entries(queryParams) @@ -13,5 +32,40 @@ export const buildPathName = ( .map(([key, values]) => `${key}=${values.filter(Boolean).join('_')}`) .join('&'); - return `${endpoint ? `${endpoint}` : ''}${id ? `/${id}` : ''}${queryString ? `${`?${queryString}`}` : ''}`; + return queryString ? `?${queryString}` : ''; }; + +export const buildPathWithIDAndQuery: BuildPathWithIDAndQuery = (endpoint, id = null, queryParams = null) => { + const pathWithID = buildPathWithID(endpoint, id); + const queryPart = buildQuery(queryParams); + return `${pathWithID}${queryPart}`; +}; + +export const buildPathWithIDAndQueryAndHash: BuildPathWithIDAndQueryAndHash = ( + endpoint, + id = null, + queryParams = null, + hash = null, +) => { + const pathWithIDAndQuery = buildPathWithIDAndQuery(endpoint, id, queryParams); + const hashPart = hash ? `#${hash}` : ''; + return `${pathWithIDAndQuery}${hashPart}`; +}; + +export const mainPathWithID = buildPathWithID.bind(null, PAGE_ID.MAIN_PAGE); +export const catalogPathWithID = buildPathWithID.bind(null, PAGE_ID.CATALOG_PAGE); +export const productPathWithID = buildPathWithID.bind(null, PAGE_ID.PRODUCT_PAGE); +export const wishlistPathWithID = buildPathWithID.bind(null, PAGE_ID.WISHLIST_PAGE); +export const cartPathWithID = buildPathWithID.bind(null, PAGE_ID.CART_PAGE); + +export const mainPathWithIDAndQuery = buildPathWithIDAndQuery.bind(null, PAGE_ID.MAIN_PAGE); +export const catalogPathWithIDAndQuery = buildPathWithIDAndQuery.bind(null, PAGE_ID.CATALOG_PAGE); +export const productPathWithIDAndQuery = buildPathWithIDAndQuery.bind(null, PAGE_ID.PRODUCT_PAGE); +export const wishlistPathWithIDAndQuery = buildPathWithIDAndQuery.bind(null, PAGE_ID.WISHLIST_PAGE); +export const cartPathWithIDAndQuery = buildPathWithIDAndQuery.bind(null, PAGE_ID.CART_PAGE); + +export const mainPathWithIDAndQueryAndHash = buildPathWithIDAndQueryAndHash.bind(null, PAGE_ID.MAIN_PAGE); +export const catalogPathWithIDAndQueryAndHash = buildPathWithIDAndQueryAndHash.bind(null, PAGE_ID.CATALOG_PAGE); +export const productPathWithIDAndQueryAndHash = buildPathWithIDAndQueryAndHash.bind(null, PAGE_ID.PRODUCT_PAGE); +export const wishlistPathWithIDAndQueryAndHash = buildPathWithIDAndQueryAndHash.bind(null, PAGE_ID.WISHLIST_PAGE); +export const cartPathWithIDAndQueryAndHash = buildPathWithIDAndQueryAndHash.bind(null, PAGE_ID.CART_PAGE); diff --git a/src/widgets/Catalog/view/catalogView.module.scss b/src/widgets/Catalog/view/catalogView.module.scss index 7741d873..50446512 100644 --- a/src/widgets/Catalog/view/catalogView.module.scss +++ b/src/widgets/Catalog/view/catalogView.module.scss @@ -46,10 +46,10 @@ justify-content: center; order: 2; grid-template-columns: repeat(3, auto); - grid-template-rows: max-content max-content; + grid-template-rows: repeat(3, max-content); margin-bottom: var(--tiny-offset); height: max-content; - min-height: calc(var(--extra-large-offset) * 13.5); + min-height: calc(var(--extra-large-offset) * 13); font-size: var(--regular-font); letter-spacing: var(--one); text-align: center; @@ -77,8 +77,9 @@ align-items: center; justify-content: center; height: 100%; + min-height: calc(var(--extra-large-offset) * 7); background: url('../../../shared/img/png/notFound.png'); - background-position: center 0; + background-position: center 3rem; background-size: calc(var(--extra-large-offset) * 3); background-repeat: no-repeat; } diff --git a/src/widgets/Footer/model/FooterModel.ts b/src/widgets/Footer/model/FooterModel.ts index da34ecf9..57a20c3a 100644 --- a/src/widgets/Footer/model/FooterModel.ts +++ b/src/widgets/Footer/model/FooterModel.ts @@ -4,7 +4,7 @@ import type { Category } from '@/shared/types/product.ts'; import getProductModel from '@/shared/API/product/model/ProductModel.ts'; import observeStore, { selectCurrentLanguage } from '@/shared/Store/observer.ts'; import { PAGE_ID } from '@/shared/constants/pages.ts'; -import { buildPathName } from '@/shared/utils/buildPathname.ts'; +import * as buildPath from '@/shared/utils/buildPathname.ts'; import { showErrorMessage } from '@/shared/utils/userMessage.ts'; import FooterView from '../view/FooterView.ts'; @@ -92,7 +92,7 @@ function generateRandomCategoryLink(categoriesArr: Category[]): Link[] { const category = subCategory[randomIndex]; subCategory.splice(randomIndex, 1); result.push({ - href: buildPathName(PAGE_ID.CATALOG_PAGE, null, { subcategory: [category.id] }), + href: buildPath.catalogPathWithIDAndQuery(null, { subcategory: [category.id] }), name: { en: category.name[0].value, ru: category.name[1].value, diff --git a/src/widgets/Header/model/HeaderModel.ts b/src/widgets/Header/model/HeaderModel.ts index f4f3ac50..d4588c06 100644 --- a/src/widgets/Header/model/HeaderModel.ts +++ b/src/widgets/Header/model/HeaderModel.ts @@ -16,7 +16,7 @@ import HeaderView from '../view/HeaderView.ts'; class HeaderModel { private cartChangeHandler = (cart: Cart): boolean => { - this.view.updateCartCount(cart.products.length); + this.view.updateCartCount(cart.products.reduce((acc, item) => acc + item.quantity, 0)); return true; }; @@ -51,7 +51,7 @@ class HeaderModel { this.setLogoHandler(); this.observeCurrentUser(); this.setLogoutButtonHandler(); - this.setCartLinkHandler(); + this.setLinksHandler(); this.observeCartChange(); this.setChangeLanguageCheckboxHandler(); } @@ -77,14 +77,6 @@ class HeaderModel { }); } - private setCartLinkHandler(): void { - const logo = this.view.getToCartLink().getHTML(); - logo.addEventListener('click', (event) => { - event.preventDefault(); - RouterModel.getInstance().navigateTo(PAGE_ID.CART_PAGE); - }); - } - private setChangeLanguageCheckboxHandler(): void { const switchLanguageCheckbox = this.view.getSwitchLanguageCheckbox().getHTML(); switchLanguageCheckbox.addEventListener('click', async () => { @@ -109,6 +101,23 @@ class HeaderModel { }); } + private setLinksHandler(): void { + this.view + .getToCartLink() + .getHTML() + .addEventListener('click', (event) => { + event.preventDefault(); + RouterModel.getInstance().navigateTo(PAGE_ID.CART_PAGE); + }); + this.view + .getToWishlistLink() + .getHTML() + .addEventListener('click', (event) => { + event.preventDefault(); + RouterModel.getInstance().navigateTo(PAGE_ID.WISHLIST_PAGE); + }); + } + private setLogoHandler(): void { const logo = this.view.getLinkLogo().getHTML(); logo.addEventListener('click', (event) => { diff --git a/src/widgets/Header/view/HeaderView.ts b/src/widgets/Header/view/HeaderView.ts index f3bc749c..4d9368a4 100644 --- a/src/widgets/Header/view/HeaderView.ts +++ b/src/widgets/Header/view/HeaderView.ts @@ -41,6 +41,8 @@ class HeaderView { private toProfileLink: LinkModel; + private toWishlistLink: LinkModel; + private wrapper: HTMLDivElement; constructor() { @@ -50,6 +52,8 @@ class HeaderView { this.cartBadgeWrap = this.createBadgeWrap(); this.cartBadge = this.createBadge(); + this.toWishlistLink = this.createToWishlistLink(); + this.toProfileLink = this.createToProfileLink(); this.switchThemeCheckbox = this.createSwitchThemeCheckbox(); this.switchLanguageCheckbox = this.createSwitchLanguageCheckbox(); @@ -174,6 +178,7 @@ class HeaderView { this.logoutButton.getHTML(), this.toCartLink.getHTML(), this.toProfileLink.getHTML(), + this.toWishlistLink.getHTML(), this.createSwitchThemeLabel(), ); @@ -310,6 +315,32 @@ class HeaderView { return this.toProfileLink; } + private createToWishlistLink(): LinkModel { + this.toWishlistLink = new LinkModel({ + attrs: { + href: PAGE_ID.CART_PAGE, + }, + classes: [styles.cartLink], + }); + + const svg = document.createElementNS(SVG_DETAILS.SVG_URL, 'svg'); + svg.append(createSVGUse(SVG_DETAILS.FILL_HEART)); + + this.toWishlistLink.getHTML().append(svg); + + this.toWishlistLink + .getHTML() + .classList.toggle(styles.cartLinkActive, getStore().getState().currentPage === PAGE_ID.WISHLIST_PAGE); + + observeStore(selectCurrentPage, () => + this.toWishlistLink + .getHTML() + .classList.toggle(styles.cartLinkActive, getStore().getState().currentPage === PAGE_ID.WISHLIST_PAGE), + ); + + return this.toWishlistLink; + } + private createWrapper(): HTMLDivElement { this.wrapper = createBaseElement({ cssClasses: [styles.wrapper], @@ -352,6 +383,10 @@ class HeaderView { return this.toProfileLink; } + public getToWishlistLink(): LinkModel { + return this.toWishlistLink; + } + public getWrapper(): HTMLDivElement { return this.wrapper; } diff --git a/src/widgets/ProductInfo/model/ProductInfoModel.ts b/src/widgets/ProductInfo/model/ProductInfoModel.ts index 76a6dfb5..f424f524 100644 --- a/src/widgets/ProductInfo/model/ProductInfoModel.ts +++ b/src/widgets/ProductInfo/model/ProductInfoModel.ts @@ -12,9 +12,8 @@ import modal from '@/shared/Modal/model/ModalModel.ts'; import getStore from '@/shared/Store/Store.ts'; import { LANGUAGE_CHOICE } from '@/shared/constants/common.ts'; import MEDIATOR_EVENT from '@/shared/constants/events.ts'; -import { PAGE_ID } from '@/shared/constants/pages.ts'; import { LOADER_SIZE } from '@/shared/constants/sizes.ts'; -import { buildPathName } from '@/shared/utils/buildPathname.ts'; +import * as buildPath from '@/shared/utils/buildPathname.ts'; import { productAddedToCartMessage, productRemovedFromCartMessage } from '@/shared/utils/messageTemplates.ts'; import { showErrorMessage, showSuccessMessage } from '@/shared/utils/userMessage.ts'; import Swiper from 'swiper'; @@ -48,7 +47,7 @@ class ProductInfoModel { this.view = new ProductInfoView(this.params); this.currentVariant = this.params.variant.find(({ size }) => size === this.params.currentSize) ?? this.params.variant[0]; - this.price = new ProductPriceModel(this.currentVariant); + this.price = new ProductPriceModel({ new: this.currentVariant.discount, old: this.currentVariant.price }); this.wishlistButton = new WishlistButtonModel(this.params); this.init(); } @@ -135,17 +134,14 @@ class ProductInfoModel { this.view.getSizeButtons().forEach((sizeButton) => { sizeButton.getHTML().addEventListener('click', () => { const currentVariant = this.params.variant.find(({ size }) => size === sizeButton.getHTML().textContent); - - const path = `${buildPathName(PAGE_ID.PRODUCT_PAGE, this.params.key, { - size: [currentVariant?.size ?? this.params.variant[0].size], - })}`; + const path = `${buildPath.productPathWithIDAndQuery(this.params.key, { size: [currentVariant?.size ?? this.params.variant[0].size] })}`; RouterModel.getInstance().navigateTo(path); modal.hide(); this.currentVariant = currentVariant ?? this.params.variant[0]; this.params.currentSize = currentVariant?.size ?? this.params.variant[0].size; this.view.updateParams(this.params); this.price.getHTML().remove(); - this.price = new ProductPriceModel(this.currentVariant); + this.price = new ProductPriceModel({ new: this.currentVariant.discount, old: this.currentVariant.price }); this.view.getRightWrapper().append(this.price.getHTML()); }); }); diff --git a/src/widgets/ProductInfo/view/productInfoView.scss b/src/widgets/ProductInfo/view/productInfoView.scss index bb890343..3eddf82b 100644 --- a/src/widgets/ProductInfo/view/productInfoView.scss +++ b/src/widgets/ProductInfo/view/productInfoView.scss @@ -92,6 +92,7 @@ .shortDescriptionWrapper { order: 3; font: var(--regular-font); + line-height: 170%; letter-spacing: var(--one); color: var(--steam-green-400); @@ -103,6 +104,7 @@ .shortDescription, .fullDescription { font: var(--regular-font); + line-height: 170%; color: var(--noble-gray-800); @media (max-width: 768px) { diff --git a/src/widgets/ProductOrder/model/ProductOrderModel.ts b/src/widgets/ProductOrder/model/ProductOrderModel.ts index 636144ee..b6c5367f 100644 --- a/src/widgets/ProductOrder/model/ProductOrderModel.ts +++ b/src/widgets/ProductOrder/model/ProductOrderModel.ts @@ -1,5 +1,6 @@ import type { Cart, CartProduct, EditCartItem } from '@/shared/types/cart.ts'; +import ProductPriceModel from '@/entities/ProductPrice/model/ProductPriceModel.ts'; import getCartModel from '@/shared/API/cart/model/CartModel.ts'; import LoaderModel from '@/shared/Loader/model/LoaderModel.ts'; import getStore from '@/shared/Store/Store.ts'; @@ -17,14 +18,26 @@ type Callback = (cart: Cart) => void; class ProductOrderModel { private callback: Callback; + private price: ProductPriceModel; + private productItem: CartProduct; + private total: ProductPriceModel; + private view: ProductOrderView; constructor(productItem: CartProduct, callback: Callback) { this.callback = callback; this.productItem = productItem; - this.view = new ProductOrderView(this.productItem, this.updateProductHandler.bind(this)); + this.price = new ProductPriceModel({ new: this.productItem.priceCouponDiscount, old: this.productItem.price }); + this.total = new ProductPriceModel({ + new: + this.productItem.totalPrice === this.productItem.totalPriceCouponDiscount + ? 0 + : this.productItem.totalPriceCouponDiscount, + old: this.productItem.totalPrice, + }); + this.view = new ProductOrderView(this.productItem, this.price, this.total, this.updateProductHandler.bind(this)); this.init(); } @@ -94,6 +107,11 @@ class ProductOrderModel { return this.productItem; } + public setProduct(product: CartProduct): CartProduct { + this.productItem = product; + return this.productItem; + } + public async updateProductHandler(active: CartActive): Promise { switch (active) { case CartActive.DELETE: { @@ -108,6 +126,10 @@ class ProductOrderModel { await this.activePlus(); break; } + case CartActive.UPDATE: { + this.updateView(this.productItem); + break; + } default: break; } diff --git a/src/widgets/ProductOrder/view/ProductOrderView.ts b/src/widgets/ProductOrder/view/ProductOrderView.ts index 898867a2..10081c36 100644 --- a/src/widgets/ProductOrder/view/ProductOrderView.ts +++ b/src/widgets/ProductOrder/view/ProductOrderView.ts @@ -1,3 +1,4 @@ +import type ProductPriceModel from '@/entities/ProductPrice/model/ProductPriceModel'; import type { LanguageChoiceType } from '@/shared/constants/common.ts'; import type { CartProduct } from '@/shared/types/cart'; import type { languageVariants } from '@/shared/types/common'; @@ -5,10 +6,9 @@ import type { languageVariants } from '@/shared/types/common'; import LinkModel from '@/shared/Link/model/LinkModel.ts'; import getStore from '@/shared/Store/Store.ts'; import { LANGUAGE_CHOICE, TABLET_WIDTH } from '@/shared/constants/common.ts'; -import { PAGE_ID } from '@/shared/constants/pages.ts'; import SVG_DETAILS from '@/shared/constants/svg.ts'; import { CartActive } from '@/shared/types/cart.ts'; -import { buildPathName } from '@/shared/utils/buildPathname.ts'; +import * as buildPath from '@/shared/utils/buildPathname.ts'; import createBaseElement from '@/shared/utils/createBaseElement.ts'; import createSVGUse from '@/shared/utils/createSVGUse.ts'; import Hammer from 'hammerjs'; @@ -41,7 +41,7 @@ class ProductOrderView { private language: LanguageChoiceType; - private price: HTMLTableCellElement; + private priceElement: ProductPriceModel; private productItem: CartProduct; @@ -49,12 +49,21 @@ class ProductOrderView { private textElement: textElementsType[] = []; - private total: HTMLTableCellElement; + private totalElement: ProductPriceModel; private view: HTMLTableRowElement; - constructor(productItem: CartProduct, callback: CallbackActive) { + constructor( + productItem: CartProduct, + priceElement: ProductPriceModel, + totalElement: ProductPriceModel, + callback: CallbackActive, + ) { this.productItem = productItem; + this.priceElement = priceElement; + this.totalElement = totalElement; + this.totalElement.getHTML().classList.add(styles.priceElement); + this.priceElement.getHTML().classList.add(styles.priceElement); this.language = getStore().getState().currentLanguage; this.callback = callback; this.quantity = createBaseElement({ @@ -62,16 +71,6 @@ class ProductOrderView { innerContent: this.productItem.quantity.toString(), tag: 'p', }); - 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.deleteButton = createBaseElement({ cssClasses: [styles.deleteButton], tag: 'button' }); this.view = this.createHTML(); } @@ -89,21 +88,15 @@ class ProductOrderView { private createHTML(): HTMLTableRowElement { this.view = createBaseElement({ cssClasses: [styles.tr, styles.trProduct], tag: 'tr' }); const imgCell = this.createImgCell(); - const tdProduct = createBaseElement({ - cssClasses: [styles.td, styles.nameCell, styles.mainText], - 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: this.productItem.size ? `${TITLE.SIZE[this.language]}: ${this.productItem.size}` : '', - tag: 'td', - }); + const tdProduct = this.createTdProduct(); + const tdSize = this.createTdSize(); + const tdPrice = this.createTdPrice(); this.textElement.push({ element: tdSize, textItem: TITLE.SIZE }); this.textElement.push({ element: tdProduct, textItem: TITLE.NAME }); + const tdTotal = this.createTdTotal(); const quantityCell = this.createQuantityCell(); const deleteCell = this.createDeleCell(); - this.view.append(imgCell, tdProduct, tdSize, this.price, quantityCell, this.total, deleteCell); + this.view.append(imgCell, tdProduct, tdSize, tdPrice, quantityCell, tdTotal, deleteCell); const animation = new Hammer(this.view); animation.on('swipeleft', () => { if (window.innerWidth <= TABLET_WIDTH) { @@ -124,9 +117,7 @@ class ProductOrderView { private createImgCell(): HTMLTableCellElement { const tdImage = createBaseElement({ cssClasses: [styles.td, styles.imgCell], tag: 'td' }); - const href = `${buildPathName(PAGE_ID.PRODUCT_PAGE, this.productItem.key, { - size: [this.productItem.size], - })}`; + const href = `${buildPath.productPathWithIDAndQuery(this.productItem.key, { size: [this.productItem.size] })}`; const link = new LinkModel({ attrs: { href, @@ -162,6 +153,40 @@ class ProductOrderView { return tdQuantity; } + private createTdPrice(): HTMLTableCellElement { + const td = createBaseElement({ + cssClasses: [styles.td, styles.priceCell, styles.priceText], + tag: 'td', + }); + td.append(this.priceElement.getHTML()); + return td; + } + + private createTdProduct(): HTMLTableCellElement { + return createBaseElement({ + cssClasses: [styles.td, styles.nameCell, styles.mainText], + innerContent: this.productItem.name[Number(getStore().getState().currentLanguage === LANGUAGE_CHOICE.RU)].value, + tag: 'td', + }); + } + + private createTdSize(): HTMLTableCellElement { + return createBaseElement({ + cssClasses: [styles.td, styles.sizeCell, styles.sizeText], + innerContent: this.productItem.size ? `${TITLE.SIZE[this.language]}: ${this.productItem.size}` : '', + tag: 'td', + }); + } + + private createTdTotal(): HTMLTableCellElement { + const td = createBaseElement({ + cssClasses: [styles.td, styles.totalCell, styles.totalText], + tag: 'td', + }); + td.append(this.totalElement.getHTML()); + return td; + } + public getDeleteButton(): HTMLButtonElement { return this.deleteButton; } @@ -173,8 +198,15 @@ class ProductOrderView { 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)}`; + + this.priceElement.updatePrice({ new: this.productItem.priceCouponDiscount, old: this.productItem.price }); + this.totalElement.updatePrice({ + new: + this.productItem.totalPrice === this.productItem.totalPriceCouponDiscount + ? 0 + : this.productItem.totalPriceCouponDiscount, + old: this.productItem.totalPrice, + }); } public updateLanguage(): void { diff --git a/src/widgets/ProductOrder/view/productOrderView.module.scss b/src/widgets/ProductOrder/view/productOrderView.module.scss index 4ff26fe2..08e66e71 100644 --- a/src/widgets/ProductOrder/view/productOrderView.module.scss +++ b/src/widgets/ProductOrder/view/productOrderView.module.scss @@ -141,6 +141,10 @@ } .priceCell { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; grid-area: 2 / 3 / 4 / 4; @media (max-width: 768px) { @@ -168,8 +172,9 @@ .quantityText { padding: var(--tiny-offset); - font: var(--regular-font); - color: var(--noble-gray-800); + font: var(--medium-font); + letter-spacing: var(--one); + color: var(--noble-gray-700); } .totalText { @@ -178,12 +183,21 @@ color: var(--steam-green-800); } +.priceDiscountText, .priceText { padding: var(--tiny-offset) 0; font: var(--regular-font); color: var(--noble-gray-700); } +.priceDiscountText { + color: var(--steam-green-700); +} + +.priceDiscountText:empty { + display: none; +} + .sizeText { padding: var(--tiny-offset); font: var(--regular-font); @@ -199,10 +213,11 @@ @include green-btn; border-radius: 50%; - padding: 0; + padding: var(--tiny-offset); width: calc(var(--tiny-offset) * 2.5); height: calc(var(--tiny-offset) * 2.5); - font: var(--regular-font); + font: var(--medium-font); + letter-spacing: var(--one); } .mobileHide { @@ -210,3 +225,21 @@ display: none; } } + +.discount { + text-decoration: line-through; +} + +.priceElement { + flex-direction: column; + margin: 0; + + span:empty { + display: none; + } + + @media (max-width: 768px) { + flex-direction: row; + justify-content: start; + } +}