Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(RSS-ECOMM-5_87): add BroadcastChannel for synchronization #376

Merged
merged 2 commits into from
Jun 25, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/global.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ interface ImportMeta {
MODE: 'development' | 'production';
PROD: boolean;
VITE_APP_CTP_API_URL: string;
VITE_APP_CTP_AUTH_UR: string;
VITE_APP_CTP_AUTH_URL: string;
VITE_APP_CTP_CLIENT_ID: string;
VITE_APP_CTP_CLIENT_SECRET: string;
VITE_APP_CTP_PROJECT_KEY: string;
Expand Down
152 changes: 113 additions & 39 deletions src/pages/CartPage/model/CartPageModel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import type { Page } from '@/shared/types/page.ts';
import SummaryModel from '@/entities/Summary/model/SummaryModel.ts';
import getCartModel from '@/shared/API/cart/model/CartModel.ts';
import getCustomerModel from '@/shared/API/customer/model/CustomerModel.ts';
import { isChannelMessage } from '@/shared/API/types/validation.ts';
import LoaderModel from '@/shared/Loader/model/LoaderModel.ts';
import getStore from '@/shared/Store/Store.ts';
import { setCurrentPage } from '@/shared/Store/actions.ts';
Expand All @@ -12,6 +13,7 @@ import { SERVER_MESSAGE_KEY } from '@/shared/constants/messages.ts';
import { PAGE_ID } from '@/shared/constants/pages.ts';
import { LOADER_SIZE } from '@/shared/constants/sizes.ts';
import { CartActive } from '@/shared/types/cart.ts';
import { ChannelMessage } from '@/shared/types/channel.ts';
import { promoCodeAppliedMessage, promoCodeDeleteMessage } from '@/shared/utils/messageTemplates.ts';
import { showErrorMessage, showSuccessMessage } from '@/shared/utils/userMessage.ts';
import ProductOrderModel from '@/widgets/ProductOrder/model/ProductOrderModel.ts';
Expand All @@ -29,26 +31,45 @@ class CartPageModel implements Page {

private cartCouponSummary: SummaryModel;

private channel: BroadcastChannel;

private productCouponSummary: SummaryModel;

private productsItem: ProductOrderModel[] = [];

private view: CartPageView;

constructor(parent: HTMLDivElement) {
this.channel = new BroadcastChannel(`${import.meta.env.VITE_APP_CTP_PROJECT_KEY}`);
this.channel.onmessage = this.onChannelMessage.bind(this);
this.cartCouponSummary = new SummaryModel(TITLE_SUMMARY.cart, this.deleteDiscountHandler.bind(this));
this.productCouponSummary = new SummaryModel(TITLE_SUMMARY.product, this.deleteDiscountHandler.bind(this));
this.view = new CartPageView(
parent,
this.cartCouponSummary,
this.productCouponSummary,
this.clearCart.bind(this),
this.clearCartHandler.bind(this),
this.addDiscountHandler.bind(this),
);

this.init().catch(showErrorMessage);
}

private addDiscount(cart: Cart): void {
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.cartCouponSummary.update(this.cart.discountsCart);
this.productCouponSummary.update(this.cart.discountsProduct);
this.view.updateTotal(this.cart);
}

private async addDiscountHandler(discountCode: string): Promise<void> {
if (discountCode.trim()) {
if (discountCode.trim() === HAPPY_BIRTHDAY) {
Expand All @@ -61,36 +82,46 @@ class CartPageModel implements Page {
.then((cart) => {
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.cartCouponSummary.update(this.cart.discountsCart);
this.productCouponSummary.update(this.cart.discountsProduct);
this.view.updateTotal(this.cart);
this.channel.postMessage({ cart, type: ChannelMessage.ADD_DISCOUNT });
this.addDiscount(cart);
}
})
.catch(showErrorMessage)
.finally(() => loader.remove());
}
}

private changeProductHandler(cart: Cart): void {
private addProduct(cart: Cart): void {
this.cart = cart;
const newItem = new ProductOrderModel(
this.cart.products[this.cart.products.length - 1],
this.changeProductHandler.bind(this),
);
this.productsItem.push(newItem);
if (this.productsItem.length > 1) {
this.view.addItem(newItem);
this.cartCouponSummary.update(this.cart.discountsCart);
this.productCouponSummary.update(this.cart.discountsProduct);
this.view.updateTotal(this.cart);
} else {
this.view.renderCart(this.productsItem);
}
}

private changeProduct(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.setProduct(searchEl);
productItem.updateProductHandler(CartActive.UPDATE).catch(showErrorMessage);
}
if (!searchEl) {
productItem.getHTML().remove();
return false;
}
return true;
});

if (!this.productsItem.length) {
this.view.renderEmpty();
}
Expand All @@ -99,6 +130,11 @@ class CartPageModel implements Page {
this.view.updateTotal(this.cart);
}

private changeProductHandler(cart: Cart): void {
this.changeProduct(cart);
this.channel.postMessage({ cart, type: ChannelMessage.ITEM_CHANGE });
}

private async checkBirthday(): Promise<void> {
if (!getStore().getState().isUserLoggedIn) {
throw showErrorMessage(SERVER_MESSAGE_KEY.COUPON_NEED_LOGIN);
Expand All @@ -124,28 +160,48 @@ class CartPageModel implements Page {
}
}

private async clearCart(): Promise<void> {
private clearCart(cart: Cart | null): void {
this.cart = cart;
showSuccessMessage(SERVER_MESSAGE_KEY.SUCCESSFUL_CLEAR_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;
});
this.renderCart();
}

private async clearCartHandler(): Promise<void> {
await getCartModel()
.clearCart()
.then((cart) => {
this.cart = cart;
showSuccessMessage(SERVER_MESSAGE_KEY.SUCCESSFUL_CLEAR_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;
});
this.renderCart();
this.channel.postMessage({ cart, type: ChannelMessage.CLEAR_CART });
this.clearCart(cart);
})
.catch((error: Error) => {
showErrorMessage(error);
return this.cart;
});
}

private deleteDiscount(cart: Cart): void {
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.cartCouponSummary.update(this.cart.discountsCart);
this.productCouponSummary.update(this.cart.discountsProduct);
this.view.updateTotal(this.cart);
}

private async deleteDiscountHandler(coupon: Coupon): Promise<void> {
const loader = new LoaderModel(LOADER_SIZE.SMALL).getHTML();
this.view.getCouponButton().append(loader);
Expand All @@ -154,18 +210,8 @@ class CartPageModel implements Page {
.then((cart) => {
if (cart) {
showSuccessMessage(promoCodeDeleteMessage(coupon.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.cartCouponSummary.update(this.cart.discountsCart);
this.productCouponSummary.update(this.cart.discountsProduct);
this.view.updateTotal(this.cart);
this.channel.postMessage({ cart, type: ChannelMessage.DELETE_COUPON });
this.deleteDiscount(cart);
}
})
.catch(showErrorMessage)
Expand All @@ -179,6 +225,34 @@ class CartPageModel implements Page {
observeStore(selectCurrentLanguage, () => this.view.updateLanguage());
}

private onChannelMessage(event: MessageEvent): void {
if (isChannelMessage(event.data)) {
switch (event.data.type) {
case ChannelMessage.ADD_DISCOUNT:
this.addDiscount(event.data.cart);
break;
case ChannelMessage.ADD_PRODUCT:
this.addProduct(event.data.cart);
break;
case ChannelMessage.DELETE_COUPON:
this.deleteDiscount(event.data.cart);
break;
case ChannelMessage.CLEAR_CART:
this.clearCart(event.data.cart);
break;
case ChannelMessage.ITEM_CHANGE:
this.changeProduct(event.data.cart);
break;
default:
break;
}

if (event.data.cart) {
getCartModel().dispatchUpdate(event.data.cart);
}
}
}

private renderCart(): void {
if (this.cart) {
this.cart.products.forEach((product) => {
Expand Down
10 changes: 9 additions & 1 deletion src/pages/CartPage/view/CartPageView.ts
Original file line number Diff line number Diff line change
Expand Up @@ -316,6 +316,11 @@ class CartPageView {
});
}

public addItem(productsItem: ProductOrderModel): void {
this.productRow.push(productsItem.getHTML());
this.tableBody?.append(productsItem.getHTML());
}

public getCouponButton(): HTMLButtonElement {
return this.couponButton;
}
Expand All @@ -332,7 +337,10 @@ class CartPageView {
this.productRow.map((productEl) => productEl.remove());
this.productRow = [];
this.addTableHeader();
productsItem.forEach((productEl) => this.tableBody?.append(productEl.getHTML()));
productsItem.forEach((productEl) => {
this.productRow.push(productEl.getHTML());
this.tableBody?.append(productEl.getHTML());
});
this.addTotalInfo();
}

Expand Down
17 changes: 11 additions & 6 deletions src/shared/API/cart/model/CartModel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import type {
import getStore from '@/shared/Store/Store.ts';
import { setAnonymousCartId, setAnonymousId } from '@/shared/Store/actions.ts';
import { PRICE_FRACTIONS } from '@/shared/constants/product.ts';
import { ChannelMessage } from '@/shared/types/channel.ts';
import { showErrorMessage } from '@/shared/utils/userMessage.ts';

import type { OptionsRequest } from '../../types/type.ts';
Expand All @@ -36,9 +37,12 @@ type Callback = (cart: Cart) => boolean;
export class CartModel {
private callback: Callback[] = [];

private channel: BroadcastChannel;

private root: CartApi;

constructor() {
this.channel = new BroadcastChannel(`${import.meta.env.VITE_APP_CTP_PROJECT_KEY}`);
this.root = new CartApi();
this.getCart()
.then((cart) => {
Expand Down Expand Up @@ -163,10 +167,6 @@ export class CartModel {
await Promise.all(otherCarts.map((id) => this.root.deleteCart(id)));
}

private dispatchUpdate(cart: Cart): void {
this.callback.forEach((callback) => callback(cart));
}

private async getAnonymousCart(anonymousCartId: string): Promise<Cart | null> {
const data = await this.root.getAnonymCart(anonymousCartId);
if (!data.body.customerId) {
Expand Down Expand Up @@ -283,6 +283,7 @@ export class CartModel {
const data = await this.root.updateCart(cart, actions);
const result = this.getCartFromData(data);
this.dispatchUpdate(result);
this.channel.postMessage({ cart: result, type: ChannelMessage.ADD_PRODUCT });
return result;
}

Expand Down Expand Up @@ -341,10 +342,14 @@ export class CartModel {
];
const data = await this.root.updateCart(cart, actions);
const result = this.getCartFromData(data);
this.dispatchUpdate(cart);
this.dispatchUpdate(result);
return result;
}

public dispatchUpdate(cart: Cart): void {
this.callback.forEach((callback) => callback(cart));
}

public async editProductCount(editCartItem: EditCartItem): Promise<Cart> {
const cart = await this.getCart();

Expand All @@ -358,7 +363,7 @@ export class CartModel {

const data = await this.root.updateCart(cart, actions);
const result = this.getCartFromData(data);
this.dispatchUpdate(cart);
this.dispatchUpdate(result);
return result;
}

Expand Down
Loading