From 85b09af105fb2dcf626a5b7339bed45c53b873b2 Mon Sep 17 00:00:00 2001 From: Margarita Golubeva Date: Tue, 25 Jun 2024 03:43:27 +0300 Subject: [PATCH 1/3] feat: lock buttons for server response --- src/entities/Coupon/view/CouponView.ts | 11 +++- .../ProductCard/model/ProductCardModel.ts | 12 +++- .../AddressAdd/model/AddressAddModel.ts | 11 ++-- .../AddressEdit/model/AddressEditModel.ts | 9 ++- .../PasswordEdit/model/PasswordEditModel.ts | 45 ++++++++------- .../PasswordEdit/view/PasswordEditView.ts | 13 +---- .../model/PersonalInfoEditModel.ts | 9 ++- .../model/WishlistButtonModel.ts | 55 +++++++++++-------- .../view/wishlistButtonView.module.scss | 5 ++ src/pages/CartPage/view/CartPageView.ts | 22 ++++---- src/shared/Confirm/model/ConfirmModel.ts | 30 +++++----- src/shared/Modal/model/ModalModel.ts | 4 -- src/shared/Modal/view/ModalView.ts | 4 -- src/shared/utils/messageTemplates.ts | 5 ++ src/shared/utils/userMessage.ts | 1 - src/widgets/LoginForm/model/LoginFormModel.ts | 35 ++++++------ .../ProductInfo/model/ProductInfoModel.ts | 16 +++--- .../ProductOrder/view/ProductOrderView.ts | 45 +++++++++------ .../view/productOrderView.module.scss | 5 ++ .../model/RegistrationFormModel.ts | 34 ++++++------ src/widgets/UserInfo/model/UserInfoModel.ts | 3 + 21 files changed, 211 insertions(+), 163 deletions(-) diff --git a/src/entities/Coupon/view/CouponView.ts b/src/entities/Coupon/view/CouponView.ts index 60b6a1f4..2ebaa00d 100644 --- a/src/entities/Coupon/view/CouponView.ts +++ b/src/entities/Coupon/view/CouponView.ts @@ -1,6 +1,7 @@ import type { DeleteCallback } from '@/entities/Summary/model/SummaryModel'; import type { CartCoupon } from '@/shared/types/cart'; +import ButtonModel from '@/shared/Button/model/ButtonModel.ts'; import createBaseElement from '@/shared/utils/createBaseElement.ts'; import { minusCartPrice } from '@/shared/utils/messageTemplates.ts'; @@ -35,9 +36,13 @@ class CouponView { }); const couponWrap = createBaseElement({ cssClasses: [styles.coupon], tag: 'div' }); - const deleteCoupon = createBaseElement({ cssClasses: [styles.deleteCoupon], tag: 'button' }); - deleteCoupon.addEventListener('click', () => this.deleteCallback(this.coupon.coupon)); - couponWrap.append(deleteCoupon, couponTitle); + const deleteCoupon = new ButtonModel({ classes: [styles.deleteCoupon] }); + deleteCoupon.getHTML().addEventListener('click', async () => { + deleteCoupon.setDisabled(); + await this.deleteCallback(this.coupon.coupon); + deleteCoupon.setEnabled(); + }); + couponWrap.append(deleteCoupon.getHTML(), couponTitle); this.view.append(couponWrap, this.couponValue); return this.view; diff --git a/src/entities/ProductCard/model/ProductCardModel.ts b/src/entities/ProductCard/model/ProductCardModel.ts index 911a87b0..eb5dcc22 100644 --- a/src/entities/ProductCard/model/ProductCardModel.ts +++ b/src/entities/ProductCard/model/ProductCardModel.ts @@ -12,7 +12,7 @@ import { PAGE_ID } from '@/shared/constants/pages.ts'; import { SEARCH_PARAMS_FIELD } from '@/shared/constants/product.ts'; import * as buildPath from '@/shared/utils/buildPathname.ts'; import getLanguageValue from '@/shared/utils/getLanguageValue.ts'; -import { productAddedToCartMessage } from '@/shared/utils/messageTemplates.ts'; +import { productAddedToCartMessage, productNotAddedToCartMessage } from '@/shared/utils/messageTemplates.ts'; import { showErrorMessage, showSuccessMessage } from '@/shared/utils/userMessage.ts'; import ProductInfoModel from '@/widgets/ProductInfo/model/ProductInfoModel.ts'; @@ -48,7 +48,10 @@ class ProductCardModel { showSuccessMessage(productAddedToCartMessage(this.getProductMeta().name)); this.view.getAddToCartButton().setDisabled(); }) - .catch(showErrorMessage); + .catch(() => { + showErrorMessage(productNotAddedToCartMessage(this.getProductMeta().name)); + this.view.getAddToCartButton().setEnabled(); + }); } private getProductMeta(): AddCartItem { @@ -97,7 +100,10 @@ class ProductCardModel { this.view .getAddToCartButton() .getHTML() - .addEventListener('click', () => this.addProductToCartHandler()); + .addEventListener('click', () => { + this.view.getAddToCartButton().setDisabled(); + this.addProductToCartHandler(); + }); } private setCardHandler(): void { diff --git a/src/features/AddressAdd/model/AddressAddModel.ts b/src/features/AddressAdd/model/AddressAddModel.ts index b7e73108..1595e73b 100644 --- a/src/features/AddressAdd/model/AddressAddModel.ts +++ b/src/features/AddressAdd/model/AddressAddModel.ts @@ -43,7 +43,6 @@ class AddressAddModel { if (this.shouldSetDefaultAddress()) { actions.push(this.getDefaultAddressAction(newAddress.id)); } - await getCustomerModel().editCustomer(actions, updatedUser); this.handleSuccess(); } @@ -74,7 +73,6 @@ class AddressAddModel { private async createNewAddress(): Promise { const loader = new LoaderModel(LOADER_SIZE.SMALL).getHTML(); this.view.getSaveChangesButton().getHTML().append(loader); - try { const user = await getCustomerModel().getCurrentUser(); if (user) { @@ -147,7 +145,6 @@ class AddressAddModel { const cancelButton = this.view.getCancelButton().getHTML(); cancelButton.addEventListener('click', () => { modal.hide(); - modal.removeContent(); }); return true; } @@ -164,8 +161,12 @@ class AddressAddModel { } private setSubmitFormHandler(): boolean { - const submitButton = this.view.getSaveChangesButton().getHTML(); - submitButton.addEventListener('click', () => this.createNewAddress()); + const submitButton = this.view.getSaveChangesButton(); + submitButton.getHTML().addEventListener('click', async () => { + submitButton.setDisabled(); + await this.createNewAddress(); + submitButton.setEnabled(); + }); return true; } diff --git a/src/features/AddressEdit/model/AddressEditModel.ts b/src/features/AddressEdit/model/AddressEditModel.ts index 6677cfbb..21635469 100644 --- a/src/features/AddressEdit/model/AddressEditModel.ts +++ b/src/features/AddressEdit/model/AddressEditModel.ts @@ -113,7 +113,6 @@ class AddressEditModel { const cancelButton = this.view.getCancelButton().getHTML(); cancelButton.addEventListener('click', () => { modal.hide(); - modal.removeContent(); }); return true; } @@ -130,8 +129,12 @@ class AddressEditModel { } private setSubmitFormHandler(): boolean { - const submitButton = this.view.getSaveChangesButton().getHTML(); - submitButton.addEventListener('click', () => this.editAddressInfo()); + const submitButton = this.view.getSaveChangesButton(); + submitButton.getHTML().addEventListener('click', async () => { + submitButton.setDisabled(); + await this.editAddressInfo(); + submitButton.setEnabled(); + }); return true; } diff --git a/src/features/PasswordEdit/model/PasswordEditModel.ts b/src/features/PasswordEdit/model/PasswordEditModel.ts index fc878af2..dcb566dd 100644 --- a/src/features/PasswordEdit/model/PasswordEditModel.ts +++ b/src/features/PasswordEdit/model/PasswordEditModel.ts @@ -17,6 +17,16 @@ class PasswordEditModel { this.init(); } + private clearInputFields(): void { + this.view.getInputFields().forEach((inputField) => { + inputField.getView().getInput().setValue(''); + const errorField = inputField.getView().getErrorField(); + if (errorField?.textContent) { + errorField.textContent = ''; + } + }); + } + private init(): void { this.view.getInputFields().forEach((inputField) => this.setInputFieldHandlers(inputField)); this.setPreventDefaultToForm(); @@ -26,7 +36,7 @@ class PasswordEditModel { this.setCancelButtonHandler(); } - private async saveNewPassword(): Promise { + private async saveNewPassword(): Promise { const loader = new LoaderModel(LOADER_SIZE.SMALL).getHTML(); this.view.getSubmitButton().getHTML().append(loader); try { @@ -41,7 +51,7 @@ class PasswordEditModel { this.view.getNewPasswordField().getView().getValue(), ); showSuccessMessage(SERVER_MESSAGE_KEY.PASSWORD_CHANGED); - modal.hide(); + this.clearInputFields(); } catch { showErrorMessage(SERVER_MESSAGE_KEY.PASSWORD_NOT_CHANGED); } @@ -51,64 +61,61 @@ class PasswordEditModel { showErrorMessage(error); } finally { loader.remove(); + modal.hide(); } - return true; } - private setCancelButtonHandler(): boolean { + private setCancelButtonHandler(): void { this.view .getCancelButton() .getHTML() .addEventListener('click', () => { + this.clearInputFields(); modal.hide(); }); - return true; } - private setInputFieldHandlers(inputField: InputFieldModel): boolean { + private setInputFieldHandlers(inputField: InputFieldModel): void { const inputHTML = inputField.getView().getInput().getHTML(); inputHTML.addEventListener('input', () => this.switchSubmitFormButtonAccess()); - return true; } - private setPreventDefaultToForm(): boolean { + private setPreventDefaultToForm(): void { this.view.getHTML().addEventListener('submit', (event) => event.preventDefault()); - return true; } - private setSubmitFormHandler(): boolean { - const submitButton = this.view.getSubmitButton().getHTML(); - submitButton.addEventListener('click', this.saveNewPassword.bind(this)); - return true; + private setSubmitFormHandler(): void { + const submitButton = this.view.getSubmitButton(); + submitButton.getHTML().addEventListener('click', async () => { + submitButton.setDisabled(); + await this.saveNewPassword(); + }); } - private setSwitchNewPasswordVisibilityHandler(): boolean { + private setSwitchNewPasswordVisibilityHandler(): void { this.view.getShowNewPasswordElement().addEventListener('click', () => { const input = this.view.getNewPasswordField().getView().getInput().getHTML(); input.type = input.type === INPUT_TYPE.PASSWORD ? INPUT_TYPE.TEXT : INPUT_TYPE.PASSWORD; input.placeholder = input.type === INPUT_TYPE.PASSWORD ? PASSWORD_TEXT.HIDDEN : PASSWORD_TEXT.SHOWN; this.view.switchPasswordElementSVG(input.type, this.view.getShowNewPasswordElement()); }); - return true; } - private setSwitchOldPasswordVisibilityHandler(): boolean { + private setSwitchOldPasswordVisibilityHandler(): void { this.view.getShowOldPasswordElement().addEventListener('click', () => { const input = this.view.getOldPasswordField().getView().getInput().getHTML(); input.type = input.type === INPUT_TYPE.PASSWORD ? INPUT_TYPE.TEXT : INPUT_TYPE.PASSWORD; input.placeholder = input.type === INPUT_TYPE.PASSWORD ? PASSWORD_TEXT.HIDDEN : PASSWORD_TEXT.SHOWN; this.view.switchPasswordElementSVG(input.type, this.view.getShowOldPasswordElement()); }); - return true; } - private switchSubmitFormButtonAccess(): boolean { + private switchSubmitFormButtonAccess(): void { if (this.view.getInputFields().every((inputField) => inputField.getIsValid())) { this.view.getSubmitButton().setEnabled(); } else { this.view.getSubmitButton().setDisabled(); } - return true; } public getHTML(): HTMLFormElement { diff --git a/src/features/PasswordEdit/view/PasswordEditView.ts b/src/features/PasswordEdit/view/PasswordEditView.ts index 3ab8182e..89404b20 100644 --- a/src/features/PasswordEdit/view/PasswordEditView.ts +++ b/src/features/PasswordEdit/view/PasswordEditView.ts @@ -32,8 +32,8 @@ class PasswordEditView { constructor() { this.submitButton = this.createSubmitButton(); this.cancelButton = this.createCancelButton(); - this.showOldPasswordElement = this.createShowOldPasswordElement(); - this.showNewPasswordElement = this.createShowNewPasswordElement(); + this.showOldPasswordElement = this.createShowPasswordElement(); + this.showNewPasswordElement = this.createShowPasswordElement(); this.oldPasswordField = this.createOldPasswordField(); this.newPasswordField = this.createNewPasswordField(); this.view = this.createHTML(); @@ -93,14 +93,7 @@ class PasswordEditView { return this.oldPasswordField; } - private createShowNewPasswordElement(): HTMLDivElement { - return createBaseElement({ - cssClasses: [styles.showPasswordElement], - tag: 'div', - }); - } - - private createShowOldPasswordElement(): HTMLDivElement { + private createShowPasswordElement(): HTMLDivElement { return createBaseElement({ cssClasses: [styles.showPasswordElement], tag: 'div', diff --git a/src/features/PersonalInfoEdit/model/PersonalInfoEditModel.ts b/src/features/PersonalInfoEdit/model/PersonalInfoEditModel.ts index 83d4fcd9..9aad8c1d 100644 --- a/src/features/PersonalInfoEdit/model/PersonalInfoEditModel.ts +++ b/src/features/PersonalInfoEdit/model/PersonalInfoEditModel.ts @@ -45,7 +45,6 @@ class PersonalInfoEditModel { ], user, ); - modal.hide(); EventMediatorModel.getInstance().notify(MEDIATOR_EVENT.REDRAW_USER_INFO, ''); showSuccessMessage(SERVER_MESSAGE_KEY.PERSONAL_INFO_CHANGED); } @@ -53,6 +52,7 @@ class PersonalInfoEditModel { showErrorMessage(error); } finally { loader.remove(); + modal.hide(); } } @@ -111,8 +111,11 @@ class PersonalInfoEditModel { } private setSubmitFormHandler(): boolean { - const submitButton = this.view.getSaveChangesButton().getHTML(); - submitButton.addEventListener('click', () => this.editPersonalInfo()); + const submitButton = this.view.getSaveChangesButton(); + submitButton.getHTML().addEventListener('click', async () => { + submitButton.setDisabled(); + await this.editPersonalInfo(); + }); return true; } diff --git a/src/features/WishlistButton/model/WishlistButtonModel.ts b/src/features/WishlistButton/model/WishlistButtonModel.ts index 6d86ad7c..946a23af 100644 --- a/src/features/WishlistButton/model/WishlistButtonModel.ts +++ b/src/features/WishlistButton/model/WishlistButtonModel.ts @@ -21,26 +21,26 @@ class WishlistButtonModel { this.init(); } - private addProductToWishListHandler(): void { - getShoppingListModel() - .addProduct(this.params.id) - .then(() => { - showSuccessMessage(productAddedToWishListMessage(getLanguageValue(this.params.name))); - this.view.switchStateWishListButton(true); - EventMediatorModel.getInstance().notify(MEDIATOR_EVENT.CHANGE_WISHLIST_BUTTON, ''); - }) - .catch(showErrorMessage); + private async addProductToWishListHandler(): Promise { + try { + await getShoppingListModel().addProduct(this.params.id); + showSuccessMessage(productAddedToWishListMessage(getLanguageValue(this.params.name))); + this.view.switchStateWishListButton(true); + EventMediatorModel.getInstance().notify(MEDIATOR_EVENT.CHANGE_WISHLIST_BUTTON, ''); + } catch (error) { + showErrorMessage(error); + } } - private deleteProductToWishListHandler(productInWishList: ShoppingListProduct): void { - getShoppingListModel() - .deleteProduct(productInWishList) - .then(() => { - showSuccessMessage(productRemovedFromWishListMessage(getLanguageValue(this.params.name))); - this.view.switchStateWishListButton(false); - EventMediatorModel.getInstance().notify(MEDIATOR_EVENT.CHANGE_WISHLIST_BUTTON, this.params.key); - }) - .catch(showErrorMessage); + private async deleteProductToWishListHandler(productInWishList: ShoppingListProduct): Promise { + try { + await getShoppingListModel().deleteProduct(productInWishList); + showSuccessMessage(productRemovedFromWishListMessage(getLanguageValue(this.params.name))); + this.view.switchStateWishListButton(false); + EventMediatorModel.getInstance().notify(MEDIATOR_EVENT.CHANGE_WISHLIST_BUTTON, this.params.key); + } catch (error) { + showErrorMessage(error); + } } private hasProductInWishList(shoppingList: ShoppingList): void { @@ -59,12 +59,19 @@ class WishlistButtonModel { private setButtonHandler(): void { const switchToWishListButton = this.view.getHTML(); switchToWishListButton.getHTML().addEventListener('click', async () => { - const shoppingList = await getShoppingListModel().getShoppingList(); - const productInWishList = shoppingList.products.find((product) => product.productId === this.params.id); - if (productInWishList) { - this.deleteProductToWishListHandler(productInWishList); - } else { - this.addProductToWishListHandler(); + try { + switchToWishListButton.setDisabled(); + const shoppingList = await getShoppingListModel().getShoppingList(); + const productInWishList = shoppingList.products.find((product) => product.productId === this.params.id); + if (productInWishList) { + await this.deleteProductToWishListHandler(productInWishList); + } else { + await this.addProductToWishListHandler(); + } + } catch (error) { + showErrorMessage(error); + } finally { + switchToWishListButton.setEnabled(); } }); } diff --git a/src/features/WishlistButton/view/wishlistButtonView.module.scss b/src/features/WishlistButton/view/wishlistButtonView.module.scss index d6a71771..1a5445f1 100644 --- a/src/features/WishlistButton/view/wishlistButtonView.module.scss +++ b/src/features/WishlistButton/view/wishlistButtonView.module.scss @@ -35,6 +35,11 @@ } } + &:disabled { + filter: opacity(0.5); + pointer-events: none; + } + &.inWishList { @media (hover: hover) { &:hover { diff --git a/src/pages/CartPage/view/CartPageView.ts b/src/pages/CartPage/view/CartPageView.ts index 69fb60b0..8839cc32 100644 --- a/src/pages/CartPage/view/CartPageView.ts +++ b/src/pages/CartPage/view/CartPageView.ts @@ -35,7 +35,7 @@ class CartPageView { private clearCallback: ClearCallback; - private couponButton: HTMLButtonElement; + private couponButton: ButtonModel; private discountList: HTMLUListElement; @@ -205,11 +205,10 @@ class CartPageView { }); } - private createCouponButton(): HTMLButtonElement { - return createBaseElement({ - cssClasses: [styles.button, styles.applyBtn], - innerContent: CART_PAGE_TITLE.BUTTON_COUPON[this.language], - tag: 'button', + private createCouponButton(): ButtonModel { + return new ButtonModel({ + classes: [styles.button, styles.applyBtn], + text: CART_PAGE_TITLE.BUTTON_COUPON[this.language], }); } @@ -222,13 +221,14 @@ class CartPageView { couponInput.getHTML().classList.add(styles.couponInput); this.textElement.push({ element: couponInput.getHTML(), textItem: CART_PAGE_TITLE.INPUT_COUPON }); - this.textElement.push({ element: this.couponButton, textItem: CART_PAGE_TITLE.BUTTON_COUPON }); - this.couponButton.addEventListener('click', (evn: Event) => { - evn.preventDefault(); + this.textElement.push({ element: this.couponButton.getHTML(), textItem: CART_PAGE_TITLE.BUTTON_COUPON }); + this.couponButton.getHTML().addEventListener('click', () => { + this.couponButton.setDisabled(); this.addDiscountCallback(couponInput.getHTML().value); couponInput.getHTML().value = ''; + this.couponButton.setEnabled(); }); - couponWrap.append(couponInput.getHTML(), this.couponButton); + couponWrap.append(couponInput.getHTML(), this.couponButton.getHTML()); return couponWrap; } @@ -317,7 +317,7 @@ class CartPageView { } public getCouponButton(): HTMLButtonElement { - return this.couponButton; + return this.couponButton.getHTML(); } public getHTML(): HTMLDivElement { diff --git a/src/shared/Confirm/model/ConfirmModel.ts b/src/shared/Confirm/model/ConfirmModel.ts index 5471ba1d..f60d84cf 100644 --- a/src/shared/Confirm/model/ConfirmModel.ts +++ b/src/shared/Confirm/model/ConfirmModel.ts @@ -27,21 +27,21 @@ class ConfirmModel { } private setConfirmButtonHandler(): void { - this.view - .getConfirmButton() - .getHTML() - .addEventListener('click', async () => { - const loader = new LoaderModel(LOADER_SIZE.SMALL).getHTML(); - this.view.getConfirmButton().getHTML().append(loader); - try { - await this.callback(); - } catch (error) { - showErrorMessage(error); - } finally { - loader.remove(); - } - modal.hide(); - }); + const confirmButton = this.view.getConfirmButton(); + confirmButton.getHTML().addEventListener('click', async () => { + const loader = new LoaderModel(LOADER_SIZE.SMALL).getHTML(); + confirmButton.getHTML().append(loader); + confirmButton.setDisabled(); + try { + await this.callback(); + } catch (error) { + showErrorMessage(error); + } finally { + loader.remove(); + confirmButton.setEnabled(); + } + modal.hide(); + }); } public getHTML(): HTMLDivElement { diff --git a/src/shared/Modal/model/ModalModel.ts b/src/shared/Modal/model/ModalModel.ts index a711ee94..05781a8a 100644 --- a/src/shared/Modal/model/ModalModel.ts +++ b/src/shared/Modal/model/ModalModel.ts @@ -19,10 +19,6 @@ class ModalModel { } } - public removeContent(): void { - this.view.removeContent(); - } - public setContent(content: HTMLElement): void { this.view.setContent(content); } diff --git a/src/shared/Modal/view/ModalView.ts b/src/shared/Modal/view/ModalView.ts index 4926adf8..ad8480b6 100644 --- a/src/shared/Modal/view/ModalView.ts +++ b/src/shared/Modal/view/ModalView.ts @@ -90,10 +90,6 @@ class ModalView { document.body.classList.remove('stop-scroll'); } - public removeContent(): void { - clearOutElement(this.modalContent); - } - public setContent(content: HTMLElement): void { clearOutElement(this.modalContent); this.modalContent.append(content); diff --git a/src/shared/utils/messageTemplates.ts b/src/shared/utils/messageTemplates.ts index 5e3f0631..8399c3d1 100644 --- a/src/shared/utils/messageTemplates.ts +++ b/src/shared/utils/messageTemplates.ts @@ -24,6 +24,8 @@ export const discountPercent = (currentVariant: Variant): string => export const productAddedToCartMessage = (name: string): string => textTemplate(name, SERVER_MESSAGE[getCurrentLanguage()].SUCCESSFUL_ADD_PRODUCT_TO_CART); +export const productNotAddedToCartMessage = (name: string): string => `Failed to add ${name} to cart`; + export const productRemovedFromCartMessage = (name: string): string => textTemplate(name, SERVER_MESSAGE[getCurrentLanguage()].SUCCESSFUL_DELETE_PRODUCT_FROM_CART); @@ -58,6 +60,9 @@ export const userInfoDateOfBirth = (date: string): string => export const createGreetingMessage = (name: string): string => textTemplate(`Hi, ${name}!`, SERVER_MESSAGE[getCurrentLanguage()].SUCCESSFUL_LOGIN); +export const createRegistrationMessage = (name: string): string => + textTemplate(`Hi, ${name}!`, SERVER_MESSAGE[getCurrentLanguage()].SUCCESSFUL_REGISTRATION); + const maxLengthMessageRu = (maxLength: number): string => textTemplate('Максимальная длина не должна превышать', maxLength, ' символов'); diff --git a/src/shared/utils/userMessage.ts b/src/shared/utils/userMessage.ts index 90e0948b..c2abebca 100644 --- a/src/shared/utils/userMessage.ts +++ b/src/shared/utils/userMessage.ts @@ -19,7 +19,6 @@ export const showErrorMessage = (param: unknown): boolean => { if (typeof param === 'string') { return serverMessageModel.showServerMessage({ - key: SERVER_MESSAGE_KEY.BAD_REQUEST, message: param, status: MESSAGE_STATUS.ERROR, }); diff --git a/src/widgets/LoginForm/model/LoginFormModel.ts b/src/widgets/LoginForm/model/LoginFormModel.ts index 000e300f..acadf717 100644 --- a/src/widgets/LoginForm/model/LoginFormModel.ts +++ b/src/widgets/LoginForm/model/LoginFormModel.ts @@ -33,9 +33,9 @@ class LoginFormModel { } private loginUser(userLoginData: UserCredentials): void { - this.view.getSubmitFormButton().setDisabled(); + const submitButton = this.view.getSubmitFormButton(); const loader = new LoaderModel(LOADER_SIZE.SMALL).getHTML(); - this.view.getSubmitFormButton().getHTML().append(loader); + submitButton.getHTML().append(loader); getCustomerModel() .hasEmail(userLoginData.email) .then((response) => { @@ -43,15 +43,20 @@ class LoginFormModel { this.loginUserHandler(userLoginData); } else { showErrorMessage(SERVER_MESSAGE_KEY.INVALID_EMAIL); + submitButton.setEnabled(); } }) - .catch((error) => showErrorMessage(error)) + .catch((error) => { + showErrorMessage(error); + submitButton.setEnabled(); + }) .finally(() => loader.remove()); } private loginUserHandler(userLoginData: UserCredentials): void { + const submitButton = this.view.getSubmitFormButton(); const loader = new LoaderModel(LOADER_SIZE.SMALL).getHTML(); - this.view.getSubmitFormButton().getHTML().append(loader); + submitButton.getHTML().append(loader); getCustomerModel() .authCustomer(userLoginData) .then((data) => { @@ -65,36 +70,34 @@ class LoginFormModel { }) .catch(() => { showErrorMessage(SERVER_MESSAGE_KEY.INCORRECT_PASSWORD); + submitButton.setEnabled(); }) .finally(() => loader.remove()); } - private setInputFieldHandlers(inputField: InputFieldModel): boolean { + private setInputFieldHandlers(inputField: InputFieldModel): void { const inputHTML = inputField.getView().getInput().getHTML(); - inputHTML.addEventListener('input', () => this.switchSubmitFormButtonAccess()); - return true; } - private setPreventDefaultToForm(): boolean { + private setPreventDefaultToForm(): void { this.getHTML().addEventListener('submit', (event) => event.preventDefault()); - return true; } - private setSubmitFormHandler(): boolean { - const submitButton = this.view.getSubmitFormButton().getHTML(); - submitButton.addEventListener('click', () => this.loginUser(this.credentialsWrapper.getFormCredentials())); - return true; + private setSubmitFormHandler(): void { + const submitButton = this.view.getSubmitFormButton(); + submitButton.getHTML().addEventListener('click', () => { + submitButton.setDisabled(); + this.loginUser(this.credentialsWrapper.getFormCredentials()); + }); } - private switchSubmitFormButtonAccess(): boolean { + private switchSubmitFormButtonAccess(): void { if (this.credentialsWrapper.getInputFields().every((inputField) => inputField.getIsValid())) { this.view.getSubmitFormButton().setEnabled(); } else { this.view.getSubmitFormButton().setDisabled(); } - - return true; } public getFirstInputField(): InputFieldModel { diff --git a/src/widgets/ProductInfo/model/ProductInfoModel.ts b/src/widgets/ProductInfo/model/ProductInfoModel.ts index 5d1c7186..4094d132 100644 --- a/src/widgets/ProductInfo/model/ProductInfoModel.ts +++ b/src/widgets/ProductInfo/model/ProductInfoModel.ts @@ -55,8 +55,6 @@ class ProductInfoModel { } private addProductToCart(): void { - const loader = new LoaderModel(LOADER_SIZE.EXTRA_SMALL).getHTML(); - this.view.getSwitchToCartButton().getHTML().append(loader); getCartModel() .addProductToCart({ name: getLanguageValue(this.params.name), @@ -70,7 +68,9 @@ class ProductInfoModel { EventMediatorModel.getInstance().notify(MEDIATOR_EVENT.REDRAW_PRODUCTS, ''); }) .catch(showErrorMessage) - .finally(() => loader.remove()); + .finally(() => { + this.view.getSwitchToCartButton().setEnabled(); + }); } private checkPath(savedPath: string): string { @@ -90,8 +90,6 @@ class ProductInfoModel { private deleteProductFromCart(cart: Cart): void { const currentProduct = cart.products.find((product) => product.key === this.params.key); if (currentProduct) { - const loader = new LoaderModel(LOADER_SIZE.EXTRA_SMALL).getHTML(); - this.view.getSwitchToCartButton().getHTML().append(loader); getCartModel() .deleteProductFromCart(currentProduct) .then(() => { @@ -100,7 +98,9 @@ class ProductInfoModel { EventMediatorModel.getInstance().notify(MEDIATOR_EVENT.REDRAW_PRODUCTS, ''); }) .catch(showErrorMessage) - .finally(() => loader.remove()); + .finally(() => { + this.view.getSwitchToCartButton().setEnabled(); + }); } } @@ -195,8 +195,10 @@ class ProductInfoModel { private switchToCartButtonHandler(): void { const switchToCartButton = this.view.getSwitchToCartButton(); - switchToCartButton.getHTML().addEventListener('click', () => { + const loader = new LoaderModel(LOADER_SIZE.EXTRA_SMALL).getHTML(); + switchToCartButton.getHTML().append(loader); + switchToCartButton.setDisabled(); getCartModel() .getCart() .then((cart) => { diff --git a/src/widgets/ProductOrder/view/ProductOrderView.ts b/src/widgets/ProductOrder/view/ProductOrderView.ts index 3889af7f..3d145e93 100644 --- a/src/widgets/ProductOrder/view/ProductOrderView.ts +++ b/src/widgets/ProductOrder/view/ProductOrderView.ts @@ -3,6 +3,7 @@ import type { LanguageChoiceType } from '@/shared/constants/common.ts'; import type { CartProduct } from '@/shared/types/cart'; import type { languageVariants } from '@/shared/types/common'; +import ButtonModel from '@/shared/Button/model/ButtonModel.ts'; import LinkModel from '@/shared/Link/model/LinkModel.ts'; import { TABLET_WIDTH } from '@/shared/constants/common.ts'; import SVG_DETAIL from '@/shared/constants/svg.ts'; @@ -38,7 +39,7 @@ const TITLE = { class ProductOrderView { private callback: CallbackActive; - private deleteButton: HTMLButtonElement; + private deleteButton: ButtonModel; private language: LanguageChoiceType; @@ -72,17 +73,21 @@ class ProductOrderView { innerContent: this.productItem.quantity.toString(), tag: 'p', }); - this.deleteButton = createBaseElement({ cssClasses: [styles.deleteButton], tag: 'button' }); + this.deleteButton = new ButtonModel({ classes: [styles.deleteButton] }); this.view = this.createHTML(); } private createDeleCell(): HTMLTableCellElement { const tdDelete = createBaseElement({ cssClasses: [styles.td, styles.deleteCell, styles.hide], tag: 'td' }); - this.deleteButton.addEventListener('click', () => this.callback(CartActive.DELETE)); - tdDelete.append(this.deleteButton); + this.deleteButton.getHTML().addEventListener('click', async () => { + this.deleteButton.setDisabled(); + await this.callback(CartActive.DELETE); + this.deleteButton.setEnabled(); + }); + tdDelete.append(this.deleteButton.getHTML()); const svg = document.createElementNS(SVG_DETAIL.SVG_URL, 'svg'); svg.append(createSVGUse(SVG_DETAIL.DELETE)); - this.deleteButton.append(svg); + this.deleteButton.getHTML().append(svg); return tdDelete; } @@ -145,19 +150,25 @@ class ProductOrderView { cssClasses: [styles.td, styles.quantityCell, styles.quantityText], tag: 'td', }); - const plusButton = createBaseElement({ - cssClasses: [styles.quantityCell, styles.quantityButton], - innerContent: TITLE.PLUS, - tag: 'button', + const plusButton = new ButtonModel({ + classes: [styles.quantityCell, styles.quantityButton], + text: TITLE.PLUS, + }); + const minusButton = new ButtonModel({ + classes: [styles.quantityCell, styles.quantityButton], + text: TITLE.MINUS, + }); + tdQuantity.append(minusButton.getHTML(), this.quantity, plusButton.getHTML()); + plusButton.getHTML().addEventListener('click', async () => { + plusButton.setDisabled(); + await this.callback(CartActive.PLUS); + plusButton.setEnabled(); }); - const minusButton = createBaseElement({ - cssClasses: [styles.quantityCell, styles.quantityButton], - innerContent: TITLE.MINUS, - tag: 'button', + minusButton.getHTML().addEventListener('click', async () => { + minusButton.setDisabled(); + await this.callback(CartActive.MINUS); + minusButton.setEnabled(); }); - tdQuantity.append(minusButton, this.quantity, plusButton); - plusButton.addEventListener('click', () => this.callback(CartActive.PLUS)); - minusButton.addEventListener('click', () => this.callback(CartActive.MINUS)); return tdQuantity; } @@ -205,7 +216,7 @@ class ProductOrderView { } public getDeleteButton(): HTMLButtonElement { - return this.deleteButton; + return this.deleteButton.getHTML(); } public getHTML(): HTMLDivElement { diff --git a/src/widgets/ProductOrder/view/productOrderView.module.scss b/src/widgets/ProductOrder/view/productOrderView.module.scss index 0555b58e..f60f0e39 100644 --- a/src/widgets/ProductOrder/view/productOrderView.module.scss +++ b/src/widgets/ProductOrder/view/productOrderView.module.scss @@ -252,3 +252,8 @@ $color: var(--steam-green-800); justify-content: start; } } + +.disabled { + filter: opacity(0.5); + pointer-events: none; +} diff --git a/src/widgets/RegistrationForm/model/RegistrationFormModel.ts b/src/widgets/RegistrationForm/model/RegistrationFormModel.ts index 6a95c729..7759bd5b 100644 --- a/src/widgets/RegistrationForm/model/RegistrationFormModel.ts +++ b/src/widgets/RegistrationForm/model/RegistrationFormModel.ts @@ -13,6 +13,7 @@ import { SERVER_MESSAGE_KEY } from '@/shared/constants/messages.ts'; import { LOADER_SIZE } from '@/shared/constants/sizes.ts'; import { ADDRESS } from '@/shared/types/address.ts'; import getCurrentLanguage from '@/shared/utils/getCurrentLanguage.ts'; +import { createRegistrationMessage } from '@/shared/utils/messageTemplates.ts'; import { showErrorMessage, showSuccessMessage } from '@/shared/utils/userMessage.ts'; import RegistrationFormView from '../view/RegistrationFormView.ts'; @@ -79,7 +80,7 @@ class RegisterFormModel { }; } - private init(): boolean { + private init(): void { this.getHTML().append(this.credentialsWrapper.getHTML()); this.getHTML().append(this.personalInfoWrapper.getHTML()); @@ -106,13 +107,12 @@ class RegisterFormModel { ); this.credentialsWrapper.getHTML().append(this.credentialsWrapper.getView().getTitle()); - - return true; } private registerUser(): void { + const submitButton = this.view.getSubmitFormButton(); const loader = new LoaderModel(LOADER_SIZE.SMALL).getHTML(); - this.view.getSubmitFormButton().getHTML().append(loader); + submitButton.getHTML().append(loader); const customer = this.getFormUserData(); this.updateUserAddresses(customer); getCustomerModel() @@ -121,37 +121,38 @@ class RegisterFormModel { if (newUserData) { getStore().dispatch(switchIsUserLoggedIn(false)); getStore().dispatch(switchIsUserLoggedIn(true)); - showSuccessMessage(SERVER_MESSAGE_KEY.SUCCESSFUL_REGISTRATION); + showSuccessMessage(createRegistrationMessage(newUserData.firstName)); } }) .catch(() => { showErrorMessage(SERVER_MESSAGE_KEY.USER_EXISTS); + submitButton.setEnabled(); }) .finally(() => loader.remove()); } - private setInputFieldHandlers(inputField: InputFieldModel): boolean { + private setInputFieldHandlers(inputField: InputFieldModel): void { const inputHTML = inputField.getView().getInput().getHTML(); inputHTML.addEventListener('input', () => { this.switchSubmitFormButtonAccess(); }); - return true; } - private setPreventDefaultToForm(): boolean { + private setPreventDefaultToForm(): void { this.getHTML().addEventListener('submit', (event) => { event.preventDefault(); }); - return true; } - private setSubmitFormHandler(): boolean { - const submitButton = this.view.getSubmitFormButton().getHTML(); - submitButton.addEventListener('click', () => this.registerUser()); - return true; + private setSubmitFormHandler(): void { + const submitButton = this.view.getSubmitFormButton(); + submitButton.getHTML().addEventListener('click', () => { + submitButton.setDisabled(); + this.registerUser(); + }); } - private singleAddressCheckBoxHandler(isChecked: boolean): boolean { + private singleAddressCheckBoxHandler(isChecked: boolean): void { if (!isChecked) { window.scrollTo({ top: document.documentElement.scrollHeight, @@ -180,17 +181,14 @@ class RegisterFormModel { }); billingAddressView.switchVisibilityAddressWrapper(isChecked); this.switchSubmitFormButtonAccess(); - - return true; } - private switchSubmitFormButtonAccess(): boolean { + private switchSubmitFormButtonAccess(): void { if (this.inputFields.every((inputField) => inputField.getIsValid())) { this.view.getSubmitFormButton().setEnabled(); } else { this.view.getSubmitFormButton().setDisabled(); } - return true; } private updateUserAddresses(userData: User | null): User | null { diff --git a/src/widgets/UserInfo/model/UserInfoModel.ts b/src/widgets/UserInfo/model/UserInfoModel.ts index a335d3b5..b9ca72ac 100644 --- a/src/widgets/UserInfo/model/UserInfoModel.ts +++ b/src/widgets/UserInfo/model/UserInfoModel.ts @@ -50,6 +50,7 @@ class UserInfoModel { const editInfoButton = this.view.getEditInfoButton(); editInfoButton.getHTML().addEventListener('click', async () => { try { + editInfoButton.setDisabled(); const user = await getCustomerModel().getCurrentUser(); if (user) { @@ -59,6 +60,8 @@ class UserInfoModel { } } catch (error) { showErrorMessage(error); + } finally { + editInfoButton.setEnabled(); } }); return true; From f63845086e16a6d69de3ed602c601dd450f584fe Mon Sep 17 00:00:00 2001 From: Margarita Golubeva Date: Tue, 25 Jun 2024 03:55:48 +0300 Subject: [PATCH 2/3] chore: add .env example file --- .env.example | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 .env.example diff --git a/.env.example b/.env.example new file mode 100644 index 00000000..b43ca999 --- /dev/null +++ b/.env.example @@ -0,0 +1,15 @@ +# Project Configuration +VITE_APP_CTP_PROJECT_KEY=your-project-key +VITE_APP_CTP_CLIENT_SECRET=your-client-secret +VITE_APP_CTP_CLIENT_ID=your-client-id +VITE_APP_CTP_REGION=europe-west1 +VITE_APP_CTP_AUTH_URL=https://auth.europe-west1.gcp.commercetools.com/ +VITE_APP_CTP_API_URL=https://api.europe-west1.gcp.commercetools.com/ +VITE_APP_CTP_SCOPES=manage_project:your-project-key view_audit_log:your-project-key manage_api_clients:your-project-key view_api_clients:your-project-key + +# Application Configuration +VITE_APP_DEFAULT_SEGMENT='/' +VITE_APP_NEXT_SEGMENT=1 +VITE_APP_PATH_SEGMENTS_TO_KEEP=0 +VITE_APP_PROJECT_TITLE=Greenshop +VITE_APP_SEARCH_SEGMENT='?' From 88c9b9f376ee94c0fe1454c4a2eb5b2c2bf49a65 Mon Sep 17 00:00:00 2001 From: Margarita Golubeva Date: Tue, 25 Jun 2024 04:22:46 +0300 Subject: [PATCH 3/3] docs: update readme with up-to-date info --- README.md | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index 64e87d29..86ac3c82 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ Welcome to [Greenshop](https://mad-wizards-greenshop.netlify.app/), your digital - [Key Features](#key-features-of-greenshop-include-%EF%B8%8F) 🗝️ - [Technical Stack](#technical-stack-) 💻 - [How to Run the Project Locally](#how-to-run-the-project-locally-%EF%B8%8F) ⚙️ -- [Availabile Scripts](#availabile-scripts-) 📑 +- [Available Scripts](#available-scripts-) 📑 - [Contact us](#contact-us-) 📩 ### Our Mission 🌸 @@ -21,25 +21,27 @@ Our modern and minimalist website offers a sleek and intuitive shopping experien 🔎 **Comprehensive Product Selection**: Explore our extensive catalog of potted plants, seeds, soil, and gardening essentials, handpicked to ensure the highest quality and variety. +🎨 **Seamless UI/UX**: Enjoy a cohesive and engaging user experience with a beautifully crafted interface that prioritizes ease of use and accessibility. + 🧭 **User-friendly Navigation**: Our intuitive navigation system makes it easy for you to find the plants, seeds, soil, and accessories you need. -🧩 **Elegance and Functionality**: Our intuitive design and intuitive features make it easy to shop for plants, seeds, soil, and accessories. +🧩 **Elegance and Functionality**: Our intuitive design and thoughtful features make it easy to shop for plants, seeds, soil, and accessories. 🖼️ **Responsive Design**: Whether you're browsing on a desktop, tablet, or smartphone, our website adapts seamlessly to provide a visually stunning and immersive experience on any device. -🔐 **Secure checkout process**: Secure checkout ensures you can shop with confidence and peace of mind. - ## Technical Stack 💻 _in our project we used the following technologies:_ -- **Frontend**: Utilizes [HTML](https://www.w3schools.com/html/), [SASS](https://sass-lang.com/), and [Typescript](https://www.typescriptlang.org/) to craft a dynamic and engaging user interface 🎨 -- **Bundling**: Employs [Vite](https://vitejs.dev/) as the bundler, ensuring swift development server startup time and seamless module replacement 🌳 -- **CI/CD**: Integrates [GitHub Actions](https://github.com/features/actions), [Plop](https://plopjs.com/), [Netlify](https://www.netlify.com/) for continuous integration and deployment 🚀 +- **Frontend**: Utilizes [Typescript](https://www.typescriptlang.org/), [HTML](https://www.w3schools.com/html/), [SASS](https://sass-lang.com/), and [modern-normalize](https://github.com/sindresorhus/modern-normalize) to craft a dynamic and engaging user interface 🎨 +- **Backend**: Supported by [CommerceTools](https://commercetools.com/), a leading provider of commerce solutions, offering a robust and scalable platform for creating immersive digital commerce experiences 🌐 +- **CI/CD**: Integrates [GitHub Actions](https://github.com/features/actions) and [Netlify](https://www.netlify.com/) for continuous integration and deployment 🚀 - **Deployment**: Hosted on [Netlify](https://www.netlify.com/), enabling efficient and hassle-free deployment of the application 🌟 - **Code Quality**: Ensured code quality through rigorous checks by [Husky](https://typicode.github.io/husky/), [Prettier](https://prettier.io/), [ESLint](https://eslint.org/), [Perfectionist](https://eslint-plugin-perfectionist.azat.io/), [Stylelint](https://stylelint.io/), [SonarLint](https://www.sonarsource.com/products/sonarlint/), and [EditorConfig](https://editorconfig.org/), maintaining consistency and best practices throughout the codebase 🐶 -- **Testing**: Thorough testing conducted with [Vitest](https://vitest.dev/), ensuring the reliability and robustness of the application's functionalities ⚡ -- **Backend**: Supported by [CommerceTools](https://commercetools.com/), a leading provider of commerce solutions, offering a robust and scalable platform for creating immersive digital commerce experiences 🌐 +- **Testing**: Thorough testing conducted with [Vitest](https://vitest.dev/), [Mock Service Worker](https://mswjs.io/), and [Sinon.js](https://sinonjs.org/), ensuring the reliability and robustness of the application's functionalities ⚡ +- **Bundling**: Employs [Vite](https://vitejs.dev/) as the bundler, ensuring swift development server startup time and seamless module replacement 🌳 +- **Additional Features and Libraries**: [Plop](https://plopjs.com/) for generating components from a template, [Hammer.js](https://hammerjs.github.io/) for touch interactions, [js-cookie](https://github.com/js-cookie/js-cookie) for cookie management, [noUiSlider](https://github.com/leongersen/noUiSlider) for range sliders, [Swiper](https://swiperjs.com/) for image carousel, and [postcode-validator](https://www.npmjs.com/package/postcode-validator) for address validation 📦 +- **Project's Architecture**: Carefully designed and implemented [Feature-Sliced Design](https://feature-sliced.design/) for efficient, scalable, and maintainable development 🌍 ## How to Run the Project Locally ⚙️ @@ -50,7 +52,7 @@ _to run the project locally, you can follow the following steps:_ - Install dependencies: `npm install` - Run the project: `npm run dev` -## Availabile Scripts 📑 +## Available Scripts 📑 _you can run the following scripts in the project directory:_