Skip to content

Commit

Permalink
feat(RSS-ECOMM-5_98)/coupon product (#350)
Browse files Browse the repository at this point in the history
  • Loading branch information
YulikK authored Jun 7, 2024
1 parent 19314f0 commit 8ec81b2
Show file tree
Hide file tree
Showing 16 changed files with 389 additions and 56 deletions.
Binary file added public/img/png/expand-arrow.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 4 additions & 0 deletions src/entities/ProductPrice/model/ProductPriceModel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,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;
23 changes: 21 additions & 2 deletions src/entities/ProductPrice/view/ProductPriceView.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ class ProductPriceView {
}

private createBasicPrice(): HTMLSpanElement {
const innerContent = this.params.new ? `$${this.params.new.toFixed(2)}` : `$${this.params.old?.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,
Expand All @@ -44,7 +44,7 @@ class ProductPriceView {
}

private createOldPrice(): HTMLSpanElement {
const innerContent = this.params.new ? `$${this.params.old.toFixed(2)}` : '';
const innerContent = this.getOldPrice(); // this.params.new ? `$${this.params.old.toFixed(2)}` : '';
this.oldPrice = createBaseElement({
cssClasses: ['oldPrice'],
innerContent,
Expand All @@ -54,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;
10 changes: 9 additions & 1 deletion src/pages/CartPage/model/CartPageModel.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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';
Expand Down Expand Up @@ -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);
}
})
Expand Down
66 changes: 52 additions & 14 deletions src/pages/CartPage/view/CartPageView.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ const TITLE = {
CLEAR: { en: 'Clear all', ru: 'Очистить' },
CONTINUE: { en: 'Continue Shopping', ru: 'Продолжить покупки' },
COUPON_APPLY: { en: 'Coupon Apply', ru: 'Применить купон' },
COUPON_DISCOUNT: { en: 'Coupon Discount', ru: 'Скидка по купону' },
COUPON_DISCOUNT: { en: 'Cart Coupons', ru: 'Скидки на корзину' },
EMPTY: {
en: `Oops! Looks like you haven't added the item to your cart yet.`,
ru: `Ой! Похоже, вы еще не добавили товар в корзину.`,
Expand All @@ -50,7 +50,9 @@ class CartPageView {

private couponButton: HTMLButtonElement;

private discount: HTMLParagraphElement;
private discountList: HTMLUListElement;

private discountTotal: HTMLElement;

private empty: HTMLDivElement;

Expand All @@ -74,6 +76,8 @@ class CartPageView {

private total: HTMLParagraphElement;

private totalDiscountTitle: HTMLParagraphElement;

private totalWrap: HTMLDivElement;

constructor(parent: HTMLDivElement, clearCallback: ClearCallback, addDiscountCallback: DiscountCallback) {
Expand All @@ -87,12 +91,18 @@ 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.totalDiscountTitle = createBaseElement({
cssClasses: [styles.title],
innerContent: TITLE.COUPON_DISCOUNT[this.language],
tag: 'p',
});
this.clear = new LinkModel({ classes: [styles.continue, styles.clear], text: TITLE.CLEAR[this.language] });
this.totalWrap = this.createWrapHTML();
this.totalWrap.classList.add(styles.total);
Expand Down Expand Up @@ -229,15 +239,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;
}

Expand Down Expand Up @@ -310,6 +315,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);
Expand All @@ -323,6 +330,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);
Expand All @@ -341,8 +350,37 @@ class CartPageView {
}

public updateTotal(cart: Cart): void {
this.subTotal.innerHTML = `$ ${(cart.total + cart.discounts).toFixed(2)}`;
this.discount.innerHTML = `-$ ${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: `-$ ${totalDiscount.toFixed(2)}`,
tag: 'p',
});
totalDiscountWrap.append(this.totalDiscountTitle, totalDiscountValue);
this.discountTotal.append(totalDiscountWrap);
}
const subTotal = cart.total + totalDiscount;
this.subTotal.innerHTML = `$ ${subTotal.toFixed(2)}`;
this.total.innerHTML = `$ ${cart.total.toFixed(2)}`;
}
}
Expand Down
28 changes: 28 additions & 0 deletions src/pages/CartPage/view/cartPageView.module.scss
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,7 @@
padding: var(--tiny-offset) 0;
font: var(--regular-font);
color: var(--noble-gray-800);
transition: all 0.2s;
}

.totalWrap {
Expand All @@ -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);
Expand Down Expand Up @@ -207,3 +226,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;
}
51 changes: 46 additions & 5 deletions src/shared/API/cart/model/CartModel.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -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';
Expand All @@ -23,6 +24,7 @@ import getCartApi, { type CartApi } from '../CartApi.ts';

enum ACTIONS {
addDiscountCode = 'addDiscountCode',
removeDiscountCode = 'removeDiscountCode',
removeLineItem = 'removeLineItem',
setAnonymousId = 'setAnonymousId',
}
Expand Down Expand Up @@ -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,
Expand All @@ -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) {
Expand Down Expand Up @@ -118,7 +141,7 @@ export class CartModel {
private getCartFromData(data: CartResponse | ClientResponse<CartPagedQueryResponse | CartResponse>): Cart {
let cart: Cart = {
anonymousId: null,
discounts: 0,
discounts: [],
id: '',
products: [],
total: 0,
Expand Down Expand Up @@ -249,6 +272,24 @@ export class CartModel {
});
}

public async deleteCoupon(id: string): Promise<Cart> {
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<Cart | null> {
if (!this.cart) {
this.cart = await this.getCart();
Expand Down
24 changes: 24 additions & 0 deletions src/shared/API/discount/DiscountApi.ts
Original file line number Diff line number Diff line change
@@ -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<ClientResponse<DiscountCodePagedQueryResponse>> {
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;
}
Loading

0 comments on commit 8ec81b2

Please sign in to comment.