Skip to content

Commit

Permalink
fix(RSS-ECOMM-5_26): lock buttons for response (#377)
Browse files Browse the repository at this point in the history
* feat: lock buttons for server response

* chore: add .env example file

* docs: update readme with up-to-date info
  • Loading branch information
stardustmeg authored Jun 25, 2024
1 parent 5bd4710 commit 80790a1
Show file tree
Hide file tree
Showing 23 changed files with 238 additions and 173 deletions.
15 changes: 15 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -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='?'
22 changes: 12 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 🌸
Expand All @@ -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 ⚙️

Expand All @@ -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:_

Expand Down
11 changes: 8 additions & 3 deletions src/entities/Coupon/view/CouponView.ts
Original file line number Diff line number Diff line change
@@ -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';

Expand Down Expand Up @@ -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;
Expand Down
12 changes: 9 additions & 3 deletions src/entities/ProductCard/model/ProductCardModel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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 {
Expand Down
11 changes: 6 additions & 5 deletions src/features/AddressAdd/model/AddressAddModel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,6 @@ class AddressAddModel {
if (this.shouldSetDefaultAddress()) {
actions.push(this.getDefaultAddressAction(newAddress.id));
}

await getCustomerModel().editCustomer(actions, updatedUser);
this.handleSuccess();
}
Expand Down Expand Up @@ -74,7 +73,6 @@ class AddressAddModel {
private async createNewAddress(): Promise<void> {
const loader = new LoaderModel(LOADER_SIZE.SMALL).getHTML();
this.view.getSaveChangesButton().getHTML().append(loader);

try {
const user = await getCustomerModel().getCurrentUser();
if (user) {
Expand Down Expand Up @@ -147,7 +145,6 @@ class AddressAddModel {
const cancelButton = this.view.getCancelButton().getHTML();
cancelButton.addEventListener('click', () => {
modal.hide();
modal.removeContent();
});
return true;
}
Expand All @@ -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;
}

Expand Down
9 changes: 6 additions & 3 deletions src/features/AddressEdit/model/AddressEditModel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,6 @@ class AddressEditModel {
const cancelButton = this.view.getCancelButton().getHTML();
cancelButton.addEventListener('click', () => {
modal.hide();
modal.removeContent();
});
return true;
}
Expand All @@ -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;
}

Expand Down
45 changes: 26 additions & 19 deletions src/features/PasswordEdit/model/PasswordEditModel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -26,7 +36,7 @@ class PasswordEditModel {
this.setCancelButtonHandler();
}

private async saveNewPassword(): Promise<boolean> {
private async saveNewPassword(): Promise<void> {
const loader = new LoaderModel(LOADER_SIZE.SMALL).getHTML();
this.view.getSubmitButton().getHTML().append(loader);
try {
Expand All @@ -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);
}
Expand All @@ -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 {
Expand Down
13 changes: 3 additions & 10 deletions src/features/PasswordEdit/view/PasswordEditView.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -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',
Expand Down
9 changes: 6 additions & 3 deletions src/features/PersonalInfoEdit/model/PersonalInfoEditModel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,14 +45,14 @@ class PersonalInfoEditModel {
],
user,
);
modal.hide();
EventMediatorModel.getInstance().notify(MEDIATOR_EVENT.REDRAW_USER_INFO, '');
showSuccessMessage(SERVER_MESSAGE_KEY.PERSONAL_INFO_CHANGED);
}
} catch (error) {
showErrorMessage(error);
} finally {
loader.remove();
modal.hide();
}
}

Expand Down Expand Up @@ -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;
}

Expand Down
Loading

0 comments on commit 80790a1

Please sign in to comment.