diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 015f5b67..13d03bf4 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -3,7 +3,7 @@ 📍 conforms with the following format: - [ ] prefix (following the [convention](https://www.conventionalcommits.org/en/v1.0.0-beta.2/): `feat`, `fix`, `hotfix`, `chore`, `refactor`, `revert`, `docs`, `style`, `test`) -- [ ] sprint and issue number (e.g. `RSS-ECOMM-2_01`, where `2` - is the sprint number and `01` - is the issue number) +- [ ] sprint and issue number (e.g. `RSS-ECOMM-3_01`, where `3` - is the sprint number and `01` - is the issue number) - [ ] short description 👀 Example: `feat(RSS-ECOMM-2_01): description` @@ -16,6 +16,10 @@ _Add a comprehensive description of the changes in the PR_ 🤔 Provide affected modules or areas +#### Styles 🎨 + +Provide any style changes details + #### Testing Strategy 🧼 Describe the testing strategy for the changes @@ -38,6 +42,8 @@ Specify if any documentation updates are required and provide details on what ne [![Pull Request Labeler](https://github.com/stardustmeg/greenshop/actions/workflows/labeler.yml/badge.svg)](https://github.com/stardustmeg/greenshop/actions/workflows/labeler.yml) - [x] My code doesn't generate any errors or warnings ⛓️ [![Continuous Integration](https://github.com/stardustmeg/greenshop/actions/workflows/ci.yml/badge.svg)](https://github.com/stardustmeg/greenshop/actions/workflows/ci.yml) +- [x] My code builds successfully ⚙️ + [![Netlify Status](https://api.netlify.com/api/v1/badges/6c9181d3-e996-4070-b82e-6a351e8fa037/deploy-status)](https://app.netlify.com/sites/mad-wizards-greenshop-develop/deploys) ## Self-Check 🌟 @@ -45,5 +51,4 @@ Specify if any documentation updates are required and provide details on what ne - [ ] I wrote a comprehensive description for the PR 📜 - [ ] I have performed a self-review of my own code ✅ - [ ] I have commented my code where needed 📝 -- [ ] My code successfully builds ⚙️ - [ ] I am happy with my PR and ready to merge ❤️‍🔥 diff --git a/package.json b/package.json index 9e7d9354..1b0a7210 100644 --- a/package.json +++ b/package.json @@ -65,9 +65,9 @@ "@commercetools/sdk-middleware-http": "^7.0.4", "autoprefixer": "^10.4.19", "isomorphic-fetch": "^3.0.0", + "materialize-css": "^1.0.0-rc.2", "modern-normalize": "^2.0.0", "postcode-validator": "^3.8.20", - "translate": "^3.0.0", "vite-plugin-checker": "^0.6.4", "vite-plugin-image-optimizer": "^1.1.7", "vite-plugin-svg-spriter": "^1.0.0", diff --git a/src/app/App/view/AppView.ts b/src/app/App/view/AppView.ts index 49b58b13..32f56c03 100644 --- a/src/app/App/view/AppView.ts +++ b/src/app/App/view/AppView.ts @@ -1,4 +1,3 @@ -import TAG_NAME from '@/shared/constants/tags.ts'; import createBaseElement from '@/shared/utils/createBaseElement.ts'; import styles from './appView.module.scss'; @@ -13,7 +12,7 @@ class AppView { private createHTML(): HTMLDivElement { this.pagesContainer = createBaseElement({ cssClasses: [styles.siteWrapper], - tag: TAG_NAME.DIV, + tag: 'div', }); return this.pagesContainer; diff --git a/src/app/Router/model/RouterModel.ts b/src/app/Router/model/RouterModel.ts index f1f6c0b4..5fe29a95 100644 --- a/src/app/Router/model/RouterModel.ts +++ b/src/app/Router/model/RouterModel.ts @@ -1,7 +1,7 @@ import type { Page } from '@/shared/types/common.ts'; import EventMediatorModel from '@/shared/EventMediator/model/EventMediatorModel.ts'; -import { EVENT_NAME, MEDIATOR_EVENT } from '@/shared/constants/events.ts'; +import MEDIATOR_EVENT from '@/shared/constants/events.ts'; import { PAGE_ID } from '@/shared/constants/pages.ts'; const DEFAULT_SEGMENT = import.meta.env.VITE_APP_DEFAULT_SEGMENT; @@ -15,7 +15,7 @@ class RouterModel { private pages: Map = new Map(); constructor() { - document.addEventListener(EVENT_NAME.DOM_CONTENT_LOADED, () => { + document.addEventListener('DOMContentLoaded', () => { const currentPath = window.location.pathname .split(DEFAULT_SEGMENT) .slice(PATH_SEGMENTS_TO_KEEP + NEXT_SEGMENT) diff --git a/src/entities/Address/model/AddressModel.ts b/src/entities/Address/model/AddressModel.ts new file mode 100644 index 00000000..0a46ac44 --- /dev/null +++ b/src/entities/Address/model/AddressModel.ts @@ -0,0 +1,51 @@ +import type { Address, PersonalData } from '@/shared/types/user.ts'; + +import CountryChoiceModel from '@/features/CountryChoice/model/CountryChoiceModel.ts'; +import getStore from '@/shared/Store/Store.ts'; +import { ADDRESS_TYPE, type AddressOptions, type AddressType } from '@/shared/types/address.ts'; + +import AddressView from '../view/AddressView.ts'; + +class AddressModel { + private addressType: AddressType; + + private view: AddressView; + + constructor(addressType: AddressType, options: AddressOptions) { + this.addressType = addressType; + this.view = new AddressView(addressType, options); + this.init(); + } + + private init(): boolean { + this.getHTML().append(new CountryChoiceModel(this.view.getCountryField().getView().getInput().getHTML()).getHTML()); + return true; + } + + public getAddressData(personalData: PersonalData): Address { + const store = getStore().getState(); + const addressData: Address = { + city: this.view.getCityField().getView().getValue(), + country: this.addressType === ADDRESS_TYPE.BILLING ? store.billingCountry : store.shippingCountry, + email: personalData.email, + firstName: personalData.firstName, + id: '', + lastName: personalData.lastName, + postalCode: this.view.getPostalCodeField().getView().getValue(), + state: '', + streetName: this.view.getStreetField().getView().getValue(), + streetNumber: '', + }; + return addressData; + } + + public getHTML(): HTMLDivElement { + return this.view.getHTML(); + } + + public getView(): AddressView { + return this.view; + } +} + +export default AddressModel; diff --git a/src/entities/Address/view/AddressView.ts b/src/entities/Address/view/AddressView.ts new file mode 100644 index 00000000..85c1e353 --- /dev/null +++ b/src/entities/Address/view/AddressView.ts @@ -0,0 +1,250 @@ +import InputFieldModel from '@/entities/InputField/model/InputFieldModel.ts'; +import InputModel from '@/shared/Input/model/InputModel.ts'; +import { FORM_TEXT, INPUT_TYPE } from '@/shared/constants/forms.ts'; +import { TITLE_TEXT } from '@/shared/constants/forms/register/constant.ts'; +import * as FORM_FIELDS from '@/shared/constants/forms/register/fieldParams.ts'; +import * as FORM_VALIDATION from '@/shared/constants/forms/register/validationParams.ts'; +import { ADDRESS_TYPE, type AddressOptions, type AddressType, SINGLE_ADDRESS } from '@/shared/types/address.ts'; +import createBaseElement from '@/shared/utils/createBaseElement.ts'; + +import styles from './addressView.module.scss'; + +class AddressView { + private address: HTMLDivElement; + + private addressAsBillingCheckBox: InputModel | null = null; + + private addressByDefaultCheckBox: InputModel | null = null; + + private addressType: AddressType; + + private cityField: InputFieldModel; + + private countryField: InputFieldModel; + + private inputFields: InputFieldModel[] = []; + + private options: AddressOptions; + + private postalCodeField: InputFieldModel; + + private streetField: InputFieldModel; + + constructor(addressType: AddressType, options: AddressOptions) { + this.addressType = addressType; + this.options = options; + this.streetField = this.createStreetField(); + this.cityField = this.createCityField(); + this.countryField = this.createCountryField(); + this.postalCodeField = this.createPostalCodeField(); + this.address = this.createHTML(); + } + + private appendInputFields(): void { + this.inputFields.forEach((inputField) => { + const inputFieldElement = inputField.getView().getHTML(); + if (inputFieldElement instanceof HTMLLabelElement) { + inputFieldElement.classList.add(styles.label); + this.address.append(inputFieldElement); + } else if (inputFieldElement instanceof InputModel) { + this.address.append(inputFieldElement.getHTML()); + } + }); + } + + private createAddressAsBillingCheckbox(innerContent: string): HTMLLabelElement { + const checkboxLabel = createBaseElement({ + attributes: { + for: SINGLE_ADDRESS, + }, + cssClasses: [styles.checkboxLabel], + tag: 'label', + }); + + const checkBoxText = createBaseElement({ + cssClasses: [styles.checkboxText], + innerContent, + tag: 'span', + }); + + this.addressAsBillingCheckBox = new InputModel({ + autocomplete: FORM_FIELDS.CHECKBOX.AUTOCOMPLETE, + id: SINGLE_ADDRESS, + placeholder: '', + type: INPUT_TYPE.CHECK_BOX, + }); + + checkboxLabel.append(checkBoxText, this.addressAsBillingCheckBox.getHTML()); + + return checkboxLabel; + } + + private createAddressByDefaultCheckbox(innerContent: string): HTMLLabelElement { + const checkboxLabel = createBaseElement({ + attributes: { + for: this.addressType === ADDRESS_TYPE.SHIPPING ? ADDRESS_TYPE.SHIPPING : ADDRESS_TYPE.BILLING, + }, + cssClasses: [styles.checkboxLabel], + tag: 'label', + }); + + const checkBoxText = createBaseElement({ + cssClasses: [styles.checkboxText], + innerContent, + tag: 'span', + }); + + this.addressByDefaultCheckBox = new InputModel({ + autocomplete: FORM_FIELDS.CHECKBOX.AUTOCOMPLETE, + id: this.addressType === ADDRESS_TYPE.SHIPPING ? ADDRESS_TYPE.SHIPPING : ADDRESS_TYPE.BILLING, + placeholder: '', + type: INPUT_TYPE.CHECK_BOX, + }); + + checkboxLabel.append(checkBoxText, this.addressByDefaultCheckBox.getHTML()); + + return checkboxLabel; + } + + private createCityField(): InputFieldModel { + if (this.addressType === ADDRESS_TYPE.SHIPPING) { + this.cityField = new InputFieldModel( + FORM_FIELDS.SHIPPING_ADDRESS_CITY, + FORM_VALIDATION.SHIPPING_ADDRESS_CITY_VALIDATE, + ); + } else { + this.cityField = new InputFieldModel( + FORM_FIELDS.BILLING_ADDRESS_CITY, + FORM_VALIDATION.BILLING_ADDRESS_CITY_VALIDATE, + ); + } + + this.inputFields.push(this.cityField); + + return this.cityField; + } + + private createCountryField(): InputFieldModel { + if (this.addressType === ADDRESS_TYPE.SHIPPING) { + this.countryField = new InputFieldModel( + FORM_FIELDS.SHIPPING_ADDRESS_COUNTRY, + FORM_VALIDATION.SHIPPING_ADDRESS_COUNTRY_VALIDATE, + ); + } else { + this.countryField = new InputFieldModel( + FORM_FIELDS.BILLING_ADDRESS_COUNTRY, + FORM_VALIDATION.BILLING_ADDRESS_COUNTRY_VALIDATE, + ); + } + + this.inputFields.push(this.countryField); + + return this.countryField; + } + + private createHTML(): HTMLDivElement { + this.address = createBaseElement({ + cssClasses: [ + styles.address, + this.addressType === ADDRESS_TYPE.SHIPPING ? styles.shippingAddressWrapper : styles.billingAddressWrapper, + ], + tag: 'div', + }); + + this.address.append(this.createTitle()); + + this.appendInputFields(); + + if (this.options.setDefault) { + this.address.append(this.createAddressByDefaultCheckbox(FORM_TEXT.DEFAULT_ADDRESS)); + } + if (this.options.setAsBilling) { + this.address.append(this.createAddressAsBillingCheckbox(FORM_TEXT.SINGLE_ADDRESS)); + } + + return this.address; + } + + private createPostalCodeField(): InputFieldModel { + if (this.addressType === ADDRESS_TYPE.SHIPPING) { + this.postalCodeField = new InputFieldModel( + FORM_FIELDS.SHIPPING_ADDRESS_POSTAL_CODE, + FORM_VALIDATION.SHIPPING_ADDRESS_POSTAL_CODE_VALIDATE, + ); + } else { + this.postalCodeField = new InputFieldModel( + FORM_FIELDS.BILLING_ADDRESS_POSTAL_CODE, + FORM_VALIDATION.BILLING_ADDRESS_POSTAL_CODE_VALIDATE, + ); + } + + this.inputFields.push(this.postalCodeField); + + return this.postalCodeField; + } + + private createStreetField(): InputFieldModel { + if (this.addressType === ADDRESS_TYPE.SHIPPING) { + this.streetField = new InputFieldModel( + FORM_FIELDS.SHIPPING_ADDRESS_STREET, + FORM_VALIDATION.SHIPPING_ADDRESS_STREET_VALIDATE, + ); + } else { + this.streetField = new InputFieldModel( + FORM_FIELDS.BILLING_ADDRESS_STREET, + FORM_VALIDATION.BILLING_ADDRESS_STREET_VALIDATE, + ); + } + + this.inputFields.push(this.streetField); + + return this.streetField; + } + + private createTitle(): HTMLHeadingElement { + return createBaseElement({ + cssClasses: [styles.title], + innerContent: + this.addressType === ADDRESS_TYPE.SHIPPING ? TITLE_TEXT.en.SHIPPING_ADDRESS : TITLE_TEXT.en.BILLING_ADDRESS, + tag: 'h3', + }); + } + + public getAddressAsBillingCheckBox(): InputModel | null { + return this.addressAsBillingCheckBox; + } + + public getAddressByDefaultCheckBox(): InputModel | null { + return this.addressByDefaultCheckBox; + } + + public getCityField(): InputFieldModel { + return this.cityField; + } + + public getCountryField(): InputFieldModel { + return this.countryField; + } + + public getHTML(): HTMLDivElement { + return this.address; + } + + public getInputFields(): InputFieldModel[] { + return this.inputFields; + } + + public getPostalCodeField(): InputFieldModel { + return this.postalCodeField; + } + + public getStreetField(): InputFieldModel { + return this.streetField; + } + + public switchVisibilityAddressWrapper(isVisible: boolean): void { + this.address.classList.toggle(styles.hidden, isVisible); + } +} + +export default AddressView; diff --git a/src/entities/Address/view/addressView.module.scss b/src/entities/Address/view/addressView.module.scss new file mode 100644 index 00000000..0a5ec29f --- /dev/null +++ b/src/entities/Address/view/addressView.module.scss @@ -0,0 +1,126 @@ +.address { + display: grid; + grid-column: 2 span; + grid-template-columns: repeat(2, 1fr); + padding: var(--extra-small-offset); + gap: calc(var(--extra-small-offset) * 1.5) var(--extra-small-offset); + + .title { + position: relative; + justify-self: center; + grid-column: 2 span; + width: max-content; + font: var(--medium-font); + letter-spacing: 1px; + text-align: center; + color: var(--noble-gray-500); + + &::after { + content: ''; + position: absolute; + left: 50%; + bottom: -10px; + border-radius: var(--medium-br); + width: 100%; + height: 2px; + background-color: var(--steam-green-800); + transform: translateX(-50%); + } + + @media (max-width: 768px) { + grid-column: 1 span; + } + } + + @media (max-width: 768px) { + grid-column: 1 span; + grid-template-columns: repeat(1, 1fr); + } +} + +.shippingAddressWrapper { + grid-row: 3; +} + +.billingAddressWrapper { + grid-row: 4; + transition: transform 0.2s; + animation: show 0.5s ease-in forwards; +} + +.hidden { + animation: hide 0.5s ease-in forwards; +} + +@keyframes hide { + 0% { + transform: scale(1); + } + + 100% { + display: none; + transform: scale(0); + } +} + +@keyframes show { + 0% { + display: grid; + transform: scale(0); + } + + 100% { + transform: scale(1); + } +} + +.checkboxLabel { + display: flex; + flex-direction: row; + align-items: center; + justify-content: end; + grid-column: 2; + cursor: pointer; + gap: calc(var(--extra-small-offset) / 4); + + input { + cursor: pointer; + } + + @media (hover: hover) { + &:hover { + .checkboxText { + color: var(--steam-green-800); + } + } + } + + @media (max-width: 768px) { + grid-column: 1 span; + } + + &:nth-child(1) { + grid-row: 3; + } + + &:nth-child(2) { + grid-row: 4; + } +} + +.checkboxText { + font: var(--regular-font); + letter-spacing: 1px; + color: var(--noble-gray-800); + transition: color 0.2s; +} + +.label { + position: relative; + display: flex; + flex-direction: column; + font: var(--regular-font); + letter-spacing: 1px; + color: var(--noble-gray-800); + gap: calc(var(--extra-small-offset) / 2); +} diff --git a/src/entities/InputField/model/InputFieldModel.ts b/src/entities/InputField/model/InputFieldModel.ts index 95c08fd5..8bef048e 100644 --- a/src/entities/InputField/model/InputFieldModel.ts +++ b/src/entities/InputField/model/InputFieldModel.ts @@ -2,14 +2,10 @@ import type { InputFieldParams, InputFieldValidatorParams } from '@/shared/types import InputFieldValidatorModel from '@/features/InputFieldValidator/model/InputFieldValidatorModel.ts'; import observeStore, { selectCurrentLanguage } from '@/shared/Store/observer.ts'; -import { EVENT_NAME } from '@/shared/constants/events.ts'; -import { INPUT_TYPE, PASSWORD_TEXT } from '@/shared/constants/forms.ts'; import InputFieldView from '../view/InputFieldView.ts'; class InputFieldModel { - private isValid = false; - private validator: InputFieldValidatorModel | null = null; private view: InputFieldView; @@ -18,57 +14,41 @@ class InputFieldModel { this.view = new InputFieldView(inputFieldParams); if (validParams) { - this.validator = new InputFieldValidatorModel(validParams, this.isValid); + this.validator = new InputFieldValidatorModel(validParams); this.setInputHandler(); } observeStore(selectCurrentLanguage, () => this.inputHandler()); - - this.setSwitchPasswordVisibilityHandler(); } private inputHandler(): boolean { const errorField = this.view.getErrorField(); - const errors = this.validator?.validate(this.view.getValue()); - if (errors === true) { - if (errorField) { - errorField.textContent = ''; - } - this.isValid = true; - } else { - if (errorField && errors) { - const [firstError] = errors; - errorField.textContent = firstError; - } - this.isValid = false; + + if (!this.validator || !errorField) { + return true; } - return true; - } + const errors = this.validator.validate(this.view.getValue()); + errorField.textContent = ''; - private setInputHandler(): boolean { - const input = this.view.getInput().getHTML(); - input.addEventListener(EVENT_NAME.INPUT, () => this.inputHandler()); - - return true; - } + if (errors instanceof Array) { + const [firstError] = errors; + errorField.textContent = firstError; + return false; + } - private setSwitchPasswordVisibilityHandler(): boolean { - const button = this.view.getShowPasswordButton().getHTML(); - button.addEventListener(EVENT_NAME.CLICK, () => this.switchPasswordVisibilityHandler()); return true; } - private switchPasswordVisibilityHandler(): boolean { + private setInputHandler(): boolean { const input = this.view.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.switchPasswordButtonSVG(input.type); + input.addEventListener('input', () => this.inputHandler()); + return true; } public getIsValid(): boolean { - return this.isValid; + return this.inputHandler(); } public getView(): InputFieldView { diff --git a/src/entities/InputField/view/InputFieldView.ts b/src/entities/InputField/view/InputFieldView.ts index 1ab649b1..82659dfd 100644 --- a/src/entities/InputField/view/InputFieldView.ts +++ b/src/entities/InputField/view/InputFieldView.ts @@ -1,12 +1,9 @@ import type { InputFieldParams, InputParams, LabelParams } from '@/shared/types/form'; -import ButtonModel from '@/shared/Button/model/ButtonModel.ts'; import InputModel from '@/shared/Input/model/InputModel.ts'; -import { INPUT_TYPE } from '@/shared/constants/forms.ts'; -import SVG_DETAILS from '@/shared/constants/svg.ts'; -import TAG_NAME from '@/shared/constants/tags.ts'; import createBaseElement from '@/shared/utils/createBaseElement.ts'; -import createSVGUse from '@/shared/utils/createSVGUse.ts'; + +import styles from './inputFieldView.module.scss'; class InputFieldView { private errorField: HTMLSpanElement | null = null; @@ -17,17 +14,15 @@ class InputFieldView { private label: HTMLLabelElement | null = null; - private showPasswordButton: ButtonModel; - constructor(params: InputFieldParams) { this.input = this.createInput(params.inputParams); - this.showPasswordButton = this.createShowPasswordButton(); this.inputField = this.createHTML(params); } private createErrorField(): HTMLSpanElement { this.errorField = createBaseElement({ - tag: TAG_NAME.SPAN, + cssClasses: [styles.error], + tag: 'span', }); return this.errorField; } @@ -42,16 +37,11 @@ class InputFieldView { this.inputField = this.input; } - if (this.getInput().getHTML().type === INPUT_TYPE.PASSWORD) { - this.label?.append(this.showPasswordButton.getHTML()); - } - return this.inputField; } private createInput(inputParams: InputParams): InputModel { this.input = new InputModel(inputParams); - // TBD fix show password on pressing enter return this.input; } @@ -62,18 +52,12 @@ class InputFieldView { for: htmlFor, }, innerContent: text || '', - tag: TAG_NAME.LABEL, + tag: 'label', }); return this.label; } - private createShowPasswordButton(): ButtonModel { - this.showPasswordButton = new ButtonModel({}); - this.switchPasswordButtonSVG(INPUT_TYPE.PASSWORD); - return this.showPasswordButton; - } - public getErrorField(): HTMLSpanElement | null { return this.errorField; } @@ -86,24 +70,12 @@ class InputFieldView { return this.input; } - public getShowPasswordButton(): ButtonModel { - return this.showPasswordButton; - } - public getValue(): string { if (this.inputField instanceof InputModel) { return this.inputField.getValue(); } return this.input.getValue(); } - - public switchPasswordButtonSVG(type: string): SVGSVGElement { - const svg = document.createElementNS(SVG_DETAILS.SVG_URL, TAG_NAME.SVG); - this.showPasswordButton.getHTML().innerHTML = ''; - svg.append(createSVGUse(type === INPUT_TYPE.PASSWORD ? SVG_DETAILS.CLOSE_EYE : SVG_DETAILS.OPEN_EYE)); - this.showPasswordButton.getHTML().append(svg); - return svg; - } } export default InputFieldView; diff --git a/src/entities/InputField/view/inputFieldView.module.scss b/src/entities/InputField/view/inputFieldView.module.scss new file mode 100644 index 00000000..a5dcc353 --- /dev/null +++ b/src/entities/InputField/view/inputFieldView.module.scss @@ -0,0 +1,6 @@ +.error { + font: var(--regular-font); + letter-spacing: 1px; + text-align: center; + color: var(--red-power-600); +} diff --git a/src/entities/Navigation/model/NavigationModel.ts b/src/entities/Navigation/model/NavigationModel.ts index 555eaa11..30249641 100644 --- a/src/entities/Navigation/model/NavigationModel.ts +++ b/src/entities/Navigation/model/NavigationModel.ts @@ -3,7 +3,7 @@ import type RouterModel from '@/app/Router/model/RouterModel'; import EventMediatorModel from '@/shared/EventMediator/model/EventMediatorModel.ts'; import getStore from '@/shared/Store/Store.ts'; import observeStore, { selectCurrentUser } from '@/shared/Store/observer.ts'; -import { EVENT_NAME, MEDIATOR_EVENT } from '@/shared/constants/events.ts'; +import MEDIATOR_EVENT from '@/shared/constants/events.ts'; import { PAGE_ID } from '@/shared/constants/pages.ts'; import NavigationView from '../view/NavigationView.ts'; @@ -46,7 +46,7 @@ class NavigationModel { private setNavigationLinksHandlers(): boolean { const navigationLinks = this.view.getNavigationLinks(); navigationLinks.forEach((link, route) => { - link.getHTML().addEventListener(EVENT_NAME.CLICK, (event) => { + link.getHTML().addEventListener('click', (event) => { event.preventDefault(); this.router.navigateTo(route); }); diff --git a/src/entities/Navigation/view/NavigationView.ts b/src/entities/Navigation/view/NavigationView.ts index 05c32877..6839365b 100644 --- a/src/entities/Navigation/view/NavigationView.ts +++ b/src/entities/Navigation/view/NavigationView.ts @@ -1,7 +1,6 @@ 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 TAG_NAME from '@/shared/constants/tags.ts'; import createBaseElement from '@/shared/utils/createBaseElement.ts'; import observeCurrentLanguage from '@/shared/utils/observeCurrentLanguage.ts'; @@ -28,7 +27,7 @@ class NavigationView { private createHTML(): HTMLElement { this.navigation = createBaseElement({ cssClasses: [styles.navigation], - tag: TAG_NAME.NAV, + tag: 'nav', }); this.navigation.append(this.toMainLink.getHTML(), this.toLoginLink.getHTML(), this.toRegisterLink.getHTML()); return this.navigation; diff --git a/src/features/CountryChoice/model/CountryChoiceModel.ts b/src/features/CountryChoice/model/CountryChoiceModel.ts index c41f3111..e93baf62 100644 --- a/src/features/CountryChoice/model/CountryChoiceModel.ts +++ b/src/features/CountryChoice/model/CountryChoiceModel.ts @@ -6,7 +6,6 @@ import observeStore, { selectShippingCountry, } from '@/shared/Store/observer.ts'; import COUNTRIES_LIST from '@/shared/constants/countriesList.ts'; -import { EVENT_NAME } from '@/shared/constants/events.ts'; import { BILLING_ADDRESS_COUNTRY } from '@/shared/constants/forms/register/fieldParams.ts'; import getCountryIndex from '@/shared/utils/getCountryIndex.ts'; @@ -23,7 +22,7 @@ class CountryChoiceModel { const action = input.id === BILLING_ADDRESS_COUNTRY.inputParams.id ? selectBillingCountry : selectShippingCountry; observeStore(action, () => { - const event = new Event(EVENT_NAME.INPUT); + const event = new Event('input'); input.dispatchEvent(event); }); } @@ -46,11 +45,11 @@ class CountryChoiceModel { this.view.getCountryItems().forEach((countryItem) => { const currentItem = countryItem; this.observeCurrentLanguage(currentItem); - currentItem.addEventListener(EVENT_NAME.CLICK, () => { + currentItem.addEventListener('click', () => { if (currentItem.textContent) { inputHTML.value = currentItem.textContent; this.setCountryToStore(currentItem, inputHTML.id); - const event = new Event(EVENT_NAME.INPUT); + const event = new Event('input'); inputHTML.dispatchEvent(event); this.view.hideCountryChoice(); } @@ -70,8 +69,8 @@ class CountryChoiceModel { } private setInputHandler(input: HTMLInputElement): boolean { - input.addEventListener(EVENT_NAME.FOCUS, () => this.view.showCountryChoice()); - input.addEventListener(EVENT_NAME.INPUT, () => { + input.addEventListener('focus', () => this.view.showCountryChoice()); + input.addEventListener('input', () => { this.view.switchVisibilityCountryItems(input); this.setCountryToStore(input, input.id); }); diff --git a/src/features/CountryChoice/view/CountryChoiceView.ts b/src/features/CountryChoice/view/CountryChoiceView.ts index 2c7b9570..bb846c15 100644 --- a/src/features/CountryChoice/view/CountryChoiceView.ts +++ b/src/features/CountryChoice/view/CountryChoiceView.ts @@ -1,8 +1,6 @@ import getStore from '@/shared/Store/Store.ts'; import COUNTRIES_LIST from '@/shared/constants/countriesList.ts'; -import { EVENT_NAME } from '@/shared/constants/events.ts'; import { KEYBOARD_KEYS } from '@/shared/constants/keyboard.ts'; -import TAG_NAME from '@/shared/constants/tags.ts'; import createBaseElement from '@/shared/utils/createBaseElement.ts'; import styles from './countryChoiceView.module.scss'; @@ -18,7 +16,7 @@ class CountryChoiceView { this.countryDropList = this.createCountryDropList(); this.countryChoice = this.createHTML(); - document.addEventListener(EVENT_NAME.CLICK, (event) => { + document.addEventListener('click', (event) => { if (!this.countryDropList.classList.contains(styles.hidden) && event.target !== input) { this.hideCountryChoice(); } else { @@ -26,7 +24,7 @@ class CountryChoiceView { } }); - document.addEventListener(EVENT_NAME.KEYDOWN, (event) => { + document.addEventListener('keydown', (event) => { if (event.key === KEYBOARD_KEYS.TAB && !this.getHTML().classList.contains(styles.hidden)) { this.hideCountryChoice(); } @@ -36,7 +34,7 @@ class CountryChoiceView { private createCountryDropList(): HTMLDivElement { this.countryDropList = createBaseElement({ cssClasses: [styles.countryDropList], - tag: TAG_NAME.DIV, + tag: 'div', }); const { currentLanguage } = getStore().getState(); @@ -53,7 +51,7 @@ class CountryChoiceView { attributes: { id: countryCode }, cssClasses: [styles.countryItem], innerContent: countryName, - tag: TAG_NAME.DIV, + tag: 'div', }); this.countryItems.push(countryItem); @@ -64,7 +62,7 @@ class CountryChoiceView { private createHTML(): HTMLDivElement { this.countryChoice = createBaseElement({ cssClasses: [styles.countryChoice, styles.hidden], - tag: TAG_NAME.DIV, + tag: 'div', }); this.countryChoice.append(this.countryDropList); diff --git a/src/features/InputFieldValidator/model/InputFieldValidatorModel.ts b/src/features/InputFieldValidator/model/InputFieldValidatorModel.ts index 4162c8f1..fc9b253f 100644 --- a/src/features/InputFieldValidator/model/InputFieldValidatorModel.ts +++ b/src/features/InputFieldValidator/model/InputFieldValidatorModel.ts @@ -1,176 +1,30 @@ import type { InputFieldValidatorParams } from '@/shared/types/form'; -import getStore from '@/shared/Store/Store.ts'; -import { LANGUAGE_CHOICE } from '@/shared/constants/buttons.ts'; -import COUNTRIES_LIST from '@/shared/constants/countriesList.ts'; -import { USER_POSTAL_CODE } from '@/shared/constants/forms.ts'; -import { ERROR_MESSAGE } from '@/shared/constants/messages.ts'; -import { postcodeValidator } from 'postcode-validator'; +import * as Validator from '../validators/validators.ts'; class InputFieldValidatorModel { - private isValid: boolean; + private isValid = false; private validParams; - constructor(validParams: InputFieldValidatorParams, isValid: boolean) { + constructor(validParams: InputFieldValidatorParams) { this.validParams = validParams; - this.isValid = isValid; - } - - private checkMaxAge(value: string): boolean | string { - const { currentLanguage } = getStore().getState(); - const today = new Date(); - const birthDate = new Date(value); - const age = today.getFullYear() - birthDate.getFullYear(); - if (this.validParams.validBirthday && age > this.validParams.validBirthday.maxAge) { - const errorMessage = - currentLanguage === LANGUAGE_CHOICE.EN - ? `You must be at most ${this.validParams.validBirthday.maxAge} years old` - : `Вам должно быть не более ${this.validParams.validBirthday.maxAge} лет`; - return errorMessage; - } - return true; - } - - private checkMaxLength(value: string): boolean | string { - const { currentLanguage } = getStore().getState(); - if (this.validParams.maxLength && value.length > this.validParams.maxLength) { - const errorMessage = - currentLanguage === LANGUAGE_CHOICE.EN - ? `Max length should not exceed ${this.validParams.maxLength}` - : `Максимальная длина не должна превышать ${this.validParams.maxLength} символов`; - return errorMessage; - } - return true; - } - - private checkMinAge(value: string): boolean | string { - const { currentLanguage } = getStore().getState(); - const today = new Date(); - const birthDate = new Date(value); - const age = today.getFullYear() - birthDate.getFullYear(); - if (this.validParams.validBirthday && age < this.validParams.validBirthday.minAge) { - const errorMessage = - currentLanguage === LANGUAGE_CHOICE.EN - ? `You must be at least ${this.validParams.validBirthday.minAge} years old` - : `Вам должно быть не менее ${this.validParams.validBirthday.minAge} лет`; - return errorMessage; - } - return true; - } - - private checkMinLength(value: string): boolean | string { - const { currentLanguage } = getStore().getState(); - if (this.validParams.minLength && value.length < this.validParams.minLength) { - const errorMessage = - currentLanguage === LANGUAGE_CHOICE.EN - ? `Min length should be at least ${this.validParams.minLength}` - : `Минимальная длина должна быть не менее ${this.validParams.minLength} символов`; - return errorMessage; - } - return true; - } - - private checkNotSpecialSymbols(value: string): boolean | string { - const { currentLanguage } = getStore().getState(); - if (this.validParams.notSpecialSymbols && !this.validParams.notSpecialSymbols.pattern.test(value)) { - const errorMessage = this.validParams.notSpecialSymbols.messages[currentLanguage]; - return errorMessage; - } - return true; - } - - private checkRequired(value: string): boolean | string { - const { currentLanguage } = getStore().getState(); - if (this.validParams.required && value.trim() === '') { - return ERROR_MESSAGE[currentLanguage].REQUIRED_FIELD; - } - return true; - } - - private checkRequiredSymbols(value: string): boolean | string { - const { currentLanguage } = getStore().getState(); - if (this.validParams.requiredSymbols && !this.validParams.requiredSymbols.pattern.test(value)) { - const errorMessage = this.validParams.requiredSymbols.messages[currentLanguage]; - return errorMessage; - } - return true; - } - - private checkValidAge(value: string): boolean | string { - const { currentLanguage } = getStore().getState(); - if (this.validParams.validBirthday && !this.validParams.validBirthday.pattern.test(value)) { - const errorMessage = this.validParams.validBirthday.messages[currentLanguage]; - return errorMessage; - } - return true; - } - - private checkValidCountry(value: string): boolean | string { - if (this.validParams.validCountry) { - const { currentLanguage } = getStore().getState(); - if ( - !Object.keys(COUNTRIES_LIST[currentLanguage]).find( - (countryName) => countryName.toLowerCase() === value.toLowerCase(), - ) - ) { - return ERROR_MESSAGE[currentLanguage].INVALID_COUNTRY; - } - } - return true; - } - - private checkValidMail(value: string): boolean | string { - const { currentLanguage } = getStore().getState(); - if (this.validParams.validMail && !this.validParams.validMail.pattern.test(value)) { - const errorMessage = this.validParams.validMail.messages[currentLanguage]; - return errorMessage; - } - return true; - } - - private checkValidPostalCode(value: string): boolean | string { - const { currentLanguage } = getStore().getState(); - if (this.validParams.validPostalCode) { - const { billingCountry, shippingCountry } = getStore().getState(); - const currentCountry = this.validParams.key === USER_POSTAL_CODE.POSTAL_CODE ? shippingCountry : billingCountry; - try { - const result = postcodeValidator(value, currentCountry); - if (!result) { - return ERROR_MESSAGE[currentLanguage].INVALID_POSTAL_CODE; - } - } catch (error) { - return ERROR_MESSAGE[currentLanguage].WRONG_REGION; - } - } - return true; - } - - private checkWhitespace(value: string): boolean | string { - const { currentLanguage } = getStore().getState(); - if (this.validParams.notWhitespace && !this.validParams.notWhitespace.pattern.test(value)) { - const errorMessage = this.validParams.notWhitespace.messages[currentLanguage]; - return errorMessage; - } - - return true; } public validate(value: string): boolean | string[] { const errors = [ - this.checkRequired(value), - this.checkRequired(value), - this.checkWhitespace(value), - this.checkNotSpecialSymbols(value), - this.checkMinLength(value), - this.checkMaxLength(value), - this.checkRequiredSymbols(value), - this.checkValidMail(value), - this.checkValidAge(value), - this.checkMinAge(value), - this.checkMaxAge(value), - this.checkValidCountry(value), - this.checkValidPostalCode(value), + Validator.checkRequired(value, this.validParams), + Validator.checkWhitespace(value, this.validParams), + Validator.checkNotSpecialSymbols(value, this.validParams), + Validator.checkMinLength(value, this.validParams), + Validator.checkMaxLength(value, this.validParams), + Validator.checkRequiredSymbols(value, this.validParams), + Validator.checkValidMail(value, this.validParams), + Validator.checkValidAge(value, this.validParams), + Validator.checkMinAge(value, this.validParams), + Validator.checkMaxAge(value, this.validParams), + Validator.checkValidCountry(value, this.validParams), + Validator.checkValidPostalCode(value, this.validParams), ]; const errorMessages: string[] = []; diff --git a/src/features/InputFieldValidator/test/InputFieldValidator.spec.ts b/src/features/InputFieldValidator/test/InputFieldValidator.spec.ts index 275ed8f4..69608d33 100644 --- a/src/features/InputFieldValidator/test/InputFieldValidator.spec.ts +++ b/src/features/InputFieldValidator/test/InputFieldValidator.spec.ts @@ -22,7 +22,7 @@ const validatorParams: InputFieldValidatorParams = { }, }; -const validator = new InputFieldValidatorModel(validatorParams, true); +const validator = new InputFieldValidatorModel(validatorParams); describe('Checking InputFieldValidatorModel', () => { it('InputFieldValidatorModel instance should be defined', () => { diff --git a/src/features/InputFieldValidator/validators/validators.ts b/src/features/InputFieldValidator/validators/validators.ts new file mode 100644 index 00000000..97eafcfc --- /dev/null +++ b/src/features/InputFieldValidator/validators/validators.ts @@ -0,0 +1,114 @@ +import type { InputFieldValidatorParams } from '@/shared/types/form.ts'; + +import getStore from '@/shared/Store/Store.ts'; +import COUNTRIES_LIST from '@/shared/constants/countriesList.ts'; +import { USER_POSTAL_CODE } from '@/shared/constants/forms.ts'; +import { ERROR_MESSAGE } from '@/shared/constants/messages.ts'; +import { maxAgeMessage, maxLengthMessage, minAgeMessage, minLengthMessage } from '@/shared/utils/messageTemplate.ts'; +import { postcodeValidator } from 'postcode-validator'; + +export const checkMaxLength = (value: string, validParams: InputFieldValidatorParams): boolean | string => { + if (validParams.maxLength && value.length > validParams.maxLength) { + return maxLengthMessage(validParams.maxLength); + } + return true; +}; + +export const checkMaxAge = (value: string, validParams: InputFieldValidatorParams): boolean | string => { + const today = new Date(); + const birthDate = new Date(value); + const age = today.getFullYear() - birthDate.getFullYear(); + if (validParams.validBirthday && age > validParams.validBirthday.maxAge) { + return maxAgeMessage(validParams.validBirthday.maxAge); + } + return true; +}; + +export const checkMinAge = (value: string, validParams: InputFieldValidatorParams): boolean | string => { + const today = new Date(); + const birthDate = new Date(value); + const age = today.getFullYear() - birthDate.getFullYear(); + if (validParams.validBirthday && age < validParams.validBirthday.minAge) { + return minAgeMessage(validParams.validBirthday.minAge); + } + return true; +}; + +export const checkMinLength = (value: string, validParams: InputFieldValidatorParams): boolean | string => { + if (validParams.minLength && value.length < validParams.minLength) { + return minLengthMessage(validParams.minLength); + } + return true; +}; + +export const checkNotSpecialSymbols = (value: string, validParams: InputFieldValidatorParams): boolean | string => { + if (validParams.notSpecialSymbols && !validParams.notSpecialSymbols.pattern.test(value)) { + return validParams.notSpecialSymbols.messages[getStore().getState().currentLanguage]; + } + return true; +}; + +export const checkRequired = (value: string, validParams: InputFieldValidatorParams): boolean | string => { + if (validParams.required && value.trim() === '') { + return ERROR_MESSAGE[getStore().getState().currentLanguage].REQUIRED_FIELD; + } + return true; +}; + +export const checkRequiredSymbols = (value: string, validParams: InputFieldValidatorParams): boolean | string => { + if (validParams.requiredSymbols && !validParams.requiredSymbols.pattern.test(value)) { + return validParams.requiredSymbols.messages[getStore().getState().currentLanguage]; + } + return true; +}; + +export const checkValidAge = (value: string, validParams: InputFieldValidatorParams): boolean | string => { + if (validParams.validBirthday && !validParams.validBirthday.pattern.test(value)) { + return validParams.validBirthday.messages[getStore().getState().currentLanguage]; + } + return true; +}; + +export const checkValidCountry = (value: string, validParams: InputFieldValidatorParams): boolean | string => { + if (validParams.validCountry) { + if ( + !Object.keys(COUNTRIES_LIST[getStore().getState().currentLanguage]).find( + (countryName) => countryName.toLowerCase() === value.toLowerCase(), + ) + ) { + return ERROR_MESSAGE[getStore().getState().currentLanguage].INVALID_COUNTRY; + } + } + return true; +}; + +export const checkValidMail = (value: string, validParams: InputFieldValidatorParams): boolean | string => { + if (validParams.validMail && !validParams.validMail.pattern.test(value)) { + return validParams.validMail.messages[getStore().getState().currentLanguage]; + } + return true; +}; + +export const checkValidPostalCode = (value: string, validParams: InputFieldValidatorParams): boolean | string => { + if (validParams.validPostalCode) { + const { billingCountry, shippingCountry } = getStore().getState(); + const currentCountry = validParams.key === USER_POSTAL_CODE.POSTAL_CODE ? shippingCountry : billingCountry; + try { + const result = postcodeValidator(value, currentCountry); + if (!result) { + return ERROR_MESSAGE[getStore().getState().currentLanguage].INVALID_POSTAL_CODE; + } + } catch (error) { + return ERROR_MESSAGE[getStore().getState().currentLanguage].WRONG_REGION; + } + } + return true; +}; + +export const checkWhitespace = (value: string, validParams: InputFieldValidatorParams): boolean | string => { + if (validParams.notWhitespace && !validParams.notWhitespace.pattern.test(value)) { + return validParams.notWhitespace.messages[getStore().getState().currentLanguage]; + } + + return true; +}; diff --git a/src/pages/LoginPage/model/LoginPageModel.ts b/src/pages/LoginPage/model/LoginPageModel.ts index 6f6f0315..31f21f18 100644 --- a/src/pages/LoginPage/model/LoginPageModel.ts +++ b/src/pages/LoginPage/model/LoginPageModel.ts @@ -3,7 +3,7 @@ import type { Page } from '@/shared/types/common.ts'; import EventMediatorModel from '@/shared/EventMediator/model/EventMediatorModel.ts'; import getStore from '@/shared/Store/Store.ts'; -import { EVENT_NAME, MEDIATOR_EVENT } from '@/shared/constants/events.ts'; +import MEDIATOR_EVENT from '@/shared/constants/events.ts'; import { PAGE_ID, PAGE_LINK_TEXT, PAGE_LINK_TEXT_KEYS } from '@/shared/constants/pages.ts'; import observeCurrentLanguage from '@/shared/utils/observeCurrentLanguage.ts'; import LoginFormModel from '@/widgets/LoginForm/model/LoginFormModel.ts'; @@ -54,8 +54,8 @@ class LoginPageModel implements Page { observeCurrentLanguage(registerLinkCopy, PAGE_LINK_TEXT, PAGE_LINK_TEXT_KEYS.REGISTRATION); - registerLink.addEventListener(EVENT_NAME.CLICK, (event) => this.registerLinkHandler(event)); - registerLinkCopy.addEventListener(EVENT_NAME.CLICK, (event) => this.registerLinkHandler(event)); + registerLink.addEventListener('click', (event) => this.registerLinkHandler(event)); + registerLinkCopy.addEventListener('click', (event) => this.registerLinkHandler(event)); toRegisterPageWrapper.append(registerLinkCopy); } diff --git a/src/pages/LoginPage/view/LoginPageView.ts b/src/pages/LoginPage/view/LoginPageView.ts index d055b4a9..897221b4 100644 --- a/src/pages/LoginPage/view/LoginPageView.ts +++ b/src/pages/LoginPage/view/LoginPageView.ts @@ -10,7 +10,6 @@ import { PAGE_LINK_TEXT, PAGE_LINK_TEXT_KEYS, } from '@/shared/constants/pages.ts'; -import TAG_NAME from '@/shared/constants/tags.ts'; import createBaseElement from '@/shared/utils/createBaseElement.ts'; import observeCurrentLanguage from '@/shared/utils/observeCurrentLanguage.ts'; @@ -52,7 +51,7 @@ class LoginPageView { this.authDescription = createBaseElement({ cssClasses: [styles.authDescription], innerContent: PAGE_DESCRIPTION[currentLanguage].LOGIN, - tag: TAG_NAME.H3, + tag: 'h3', }); observeCurrentLanguage(this.authDescription, PAGE_DESCRIPTION, PAGE_DESCRIPTION_KEYS.LOGIN); @@ -63,7 +62,7 @@ class LoginPageView { private createAuthWrapper(): HTMLDivElement { this.authWrapper = createBaseElement({ cssClasses: [styles.authWrapper], - tag: TAG_NAME.DIV, + tag: 'div', }); this.authWrapper.append(this.linksWrapper, this.authDescription, this.createToRegisterPageWrapper()); @@ -73,7 +72,7 @@ class LoginPageView { private createDesignElement(): HTMLDivElement { this.designElement = createBaseElement({ cssClasses: [styles.designElement], - tag: TAG_NAME.DIV, + tag: 'div', }); return this.designElement; } @@ -81,7 +80,7 @@ class LoginPageView { private createHTML(): HTMLDivElement { this.page = createBaseElement({ cssClasses: [styles.loginPage], - tag: TAG_NAME.DIV, + tag: 'div', }); this.page.append(this.authWrapper); @@ -93,7 +92,7 @@ class LoginPageView { private createLinksWrapper(): HTMLDivElement { this.linksWrapper = createBaseElement({ cssClasses: [styles.linksWrapper], - tag: TAG_NAME.DIV, + tag: 'div', }); this.linksWrapper.append(this.loginSpan, this.designElement, this.registerLink.getHTML()); @@ -105,7 +104,7 @@ class LoginPageView { this.loginSpan = createBaseElement({ cssClasses: [styles.loginSpan], innerContent: PAGE_LINK_TEXT[currentLanguage].LOGIN, - tag: TAG_NAME.SPAN, + tag: 'span', }); observeCurrentLanguage(this.loginSpan, PAGE_LINK_TEXT, PAGE_LINK_TEXT_KEYS.LOGIN); @@ -133,7 +132,7 @@ class LoginPageView { this.toRegisterPageWrapper = createBaseElement({ cssClasses: [styles.toRegisterPageWrapper], innerContent: PAGE_ANSWER[currentLanguage].LOGIN, - tag: TAG_NAME.SPAN, + tag: 'span', }); observeCurrentLanguage(this.toRegisterPageWrapper, PAGE_ANSWER, PAGE_ANSWER_KEYS.LOGIN); diff --git a/src/pages/MainPage/model/MainPageModel.ts b/src/pages/MainPage/model/MainPageModel.ts index 75454894..496a74a9 100644 --- a/src/pages/MainPage/model/MainPageModel.ts +++ b/src/pages/MainPage/model/MainPageModel.ts @@ -3,7 +3,7 @@ import type { Page } from '@/shared/types/common.ts'; import EventMediatorModel from '@/shared/EventMediator/model/EventMediatorModel.ts'; import observeStore, { selectCurrentUser } from '@/shared/Store/observer.ts'; -import { MEDIATOR_EVENT } from '@/shared/constants/events.ts'; +import MEDIATOR_EVENT from '@/shared/constants/events.ts'; import { PAGE_ID } from '@/shared/constants/pages.ts'; import MainPageView from '../view/MainPageView.ts'; diff --git a/src/pages/MainPage/view/MainPageView.ts b/src/pages/MainPage/view/MainPageView.ts index 3cda999f..655797e4 100644 --- a/src/pages/MainPage/view/MainPageView.ts +++ b/src/pages/MainPage/view/MainPageView.ts @@ -1,5 +1,4 @@ import { PAGE_TIMEOUT_DURATION } from '@/shared/constants/animations.ts'; -import TAG_NAME from '@/shared/constants/tags.ts'; import createBaseElement from '@/shared/utils/createBaseElement.ts'; import styles from './mainPageView.module.scss'; @@ -17,7 +16,7 @@ class MainPageView { private createHTML(): HTMLDivElement { this.page = createBaseElement({ cssClasses: [styles.mainPage], - tag: TAG_NAME.DIV, + tag: 'div', }); this.parent.append(this.page); diff --git a/src/pages/NotFoundPage/model/NotFoundPageModel.ts b/src/pages/NotFoundPage/model/NotFoundPageModel.ts index 619f32cc..26a8ed41 100644 --- a/src/pages/NotFoundPage/model/NotFoundPageModel.ts +++ b/src/pages/NotFoundPage/model/NotFoundPageModel.ts @@ -4,7 +4,7 @@ import type { Page } from '@/shared/types/common.ts'; import EventMediatorModel from '@/shared/EventMediator/model/EventMediatorModel.ts'; import getStore from '@/shared/Store/Store.ts'; import observeStore, { selectCurrentLanguage } from '@/shared/Store/observer.ts'; -import { EVENT_NAME, MEDIATOR_EVENT } from '@/shared/constants/events.ts'; +import MEDIATOR_EVENT from '@/shared/constants/events.ts'; import { PAGE_DESCRIPTION, PAGE_ID } from '@/shared/constants/pages.ts'; import NotFoundPageView from '../view/NotFoundPageView.ts'; @@ -62,7 +62,7 @@ class NotFoundPageModel implements Page { private toMainButtonHandler(): boolean { const toMainButton = this.view.getToMainButton().getHTML(); - toMainButton.addEventListener(EVENT_NAME.CLICK, this.router.navigateTo.bind(this.router, PAGE_ID.MAIN_PAGE)); + toMainButton.addEventListener('click', this.router.navigateTo.bind(this.router, PAGE_ID.MAIN_PAGE)); return true; } diff --git a/src/pages/NotFoundPage/view/NotFoundPageView.ts b/src/pages/NotFoundPage/view/NotFoundPageView.ts index 07830405..98f7c49d 100644 --- a/src/pages/NotFoundPage/view/NotFoundPageView.ts +++ b/src/pages/NotFoundPage/view/NotFoundPageView.ts @@ -4,7 +4,6 @@ import { PAGE_TIMEOUT_DURATION } from '@/shared/constants/animations.ts'; import { BUTTON_TEXT, BUTTON_TEXT_KEYS } from '@/shared/constants/buttons.ts'; import { PAGE_DESCRIPTION_KEYS } from '@/shared/constants/pages.ts'; import SVG_DETAILS from '@/shared/constants/svg.ts'; -import TAG_NAME from '@/shared/constants/tags.ts'; import createBaseElement from '@/shared/utils/createBaseElement.ts'; import createSVGUse from '@/shared/utils/createSVGUse.ts'; import observeCurrentLanguage from '@/shared/utils/observeCurrentLanguage.ts'; @@ -36,7 +35,7 @@ class NotFoundPageView { private createHTML(): HTMLDivElement { this.page = createBaseElement({ cssClasses: [styles.notFoundPage], - tag: TAG_NAME.DIV, + tag: 'div', }); this.page.append(this.logo, this.title, this.description, this.toMainButton.getHTML()); @@ -48,14 +47,14 @@ class NotFoundPageView { private createPageDescription(): HTMLParagraphElement { this.description = createBaseElement({ cssClasses: [styles.pageDescription], - tag: TAG_NAME.P, + tag: 'p', }); return this.description; } private createPageLogo(): HTMLDivElement { - this.logo = createBaseElement({ cssClasses: [styles.pageLogo], tag: TAG_NAME.DIV }); - const svg = document.createElementNS(SVG_DETAILS.SVG_URL, TAG_NAME.SVG); + this.logo = createBaseElement({ cssClasses: [styles.pageLogo], tag: 'div' }); + const svg = document.createElementNS(SVG_DETAILS.SVG_URL, 'svg'); svg.append(createSVGUse(SVG_DETAILS.LOGO)); this.logo.append(svg); return this.logo; @@ -65,7 +64,7 @@ class NotFoundPageView { this.title = createBaseElement({ cssClasses: [styles.pageTitle], innerContent: PAGE_DESCRIPTION_KEYS[404], - tag: TAG_NAME.H1, + tag: 'h1', }); return this.title; } diff --git a/src/pages/RegistrationPage/model/RegistrationPageModel.ts b/src/pages/RegistrationPage/model/RegistrationPageModel.ts index 670c6c9d..03298892 100644 --- a/src/pages/RegistrationPage/model/RegistrationPageModel.ts +++ b/src/pages/RegistrationPage/model/RegistrationPageModel.ts @@ -2,7 +2,7 @@ import type RouterModel from '@/app/Router/model/RouterModel.ts'; import type { Page } from '@/shared/types/common.ts'; import EventMediatorModel from '@/shared/EventMediator/model/EventMediatorModel.ts'; -import { EVENT_NAME, MEDIATOR_EVENT } from '@/shared/constants/events.ts'; +import MEDIATOR_EVENT from '@/shared/constants/events.ts'; import { PAGE_ID, PAGE_LINK_TEXT, PAGE_LINK_TEXT_KEYS } from '@/shared/constants/pages.ts'; import observeCurrentLanguage from '@/shared/utils/observeCurrentLanguage.ts'; import RegisterFormModel from '@/widgets/RegistrationForm/model/RegistrationFormModel.ts'; @@ -42,8 +42,8 @@ class RegistrationPageModel implements Page { observeCurrentLanguage(loginLinkCopy, PAGE_LINK_TEXT, PAGE_LINK_TEXT_KEYS.LOGIN); - loginLink.addEventListener(EVENT_NAME.CLICK, (event) => this.loginLinkHandler(event)); - loginLinkCopy.addEventListener(EVENT_NAME.CLICK, (event) => this.loginLinkHandler(event)); + loginLink.addEventListener('click', (event) => this.loginLinkHandler(event)); + loginLinkCopy.addEventListener('click', (event) => this.loginLinkHandler(event)); toLoginPageWrapper.append(loginLinkCopy); } diff --git a/src/pages/RegistrationPage/view/RegistrationPageView.ts b/src/pages/RegistrationPage/view/RegistrationPageView.ts index 6603f2fd..a43f4bd7 100644 --- a/src/pages/RegistrationPage/view/RegistrationPageView.ts +++ b/src/pages/RegistrationPage/view/RegistrationPageView.ts @@ -10,7 +10,6 @@ import { PAGE_LINK_TEXT, PAGE_LINK_TEXT_KEYS, } from '@/shared/constants/pages.ts'; -import TAG_NAME from '@/shared/constants/tags.ts'; import createBaseElement from '@/shared/utils/createBaseElement.ts'; import observeCurrentLanguage from '@/shared/utils/observeCurrentLanguage.ts'; @@ -52,7 +51,7 @@ class RegistrationPageView { this.authDescription = createBaseElement({ cssClasses: [styles.authDescription], innerContent: PAGE_DESCRIPTION[currentLanguage].REGISTRATION, - tag: TAG_NAME.H3, + tag: 'h3', }); observeCurrentLanguage(this.authDescription, PAGE_DESCRIPTION, PAGE_DESCRIPTION_KEYS.REGISTRATION); @@ -63,7 +62,7 @@ class RegistrationPageView { private createAuthWrapper(): HTMLDivElement { this.authWrapper = createBaseElement({ cssClasses: [styles.authWrapper], - tag: TAG_NAME.DIV, + tag: 'div', }); this.authWrapper.append(this.linksWrapper, this.authDescription, this.toLoginPageWrapper); @@ -73,7 +72,7 @@ class RegistrationPageView { private createDesignElement(): HTMLDivElement { this.designElement = createBaseElement({ cssClasses: [styles.designElement], - tag: TAG_NAME.DIV, + tag: 'div', }); return this.designElement; } @@ -81,7 +80,7 @@ class RegistrationPageView { private createHTML(): HTMLDivElement { this.page = createBaseElement({ cssClasses: [styles.registrationPage], - tag: TAG_NAME.DIV, + tag: 'div', }); this.page.append(this.authWrapper); @@ -93,7 +92,7 @@ class RegistrationPageView { private createLinksWrapper(): HTMLDivElement { this.linksWrapper = createBaseElement({ cssClasses: [styles.linksWrapper], - tag: TAG_NAME.DIV, + tag: 'div', }); this.linksWrapper.append(this.loginLink.getHTML(), this.designElement, this.registerSpan); @@ -120,7 +119,7 @@ class RegistrationPageView { this.registerSpan = createBaseElement({ cssClasses: [styles.registerSpan], innerContent: PAGE_LINK_TEXT[currentLanguage].REGISTRATION, - tag: TAG_NAME.SPAN, + tag: 'span', }); observeCurrentLanguage(this.registerSpan, PAGE_LINK_TEXT, PAGE_LINK_TEXT_KEYS.REGISTRATION); @@ -133,7 +132,7 @@ class RegistrationPageView { this.toLoginPageWrapper = createBaseElement({ cssClasses: [styles.toLoginPageWrapper], innerContent: PAGE_ANSWER[currentLanguage].REGISTRATION, - tag: TAG_NAME.SPAN, + tag: 'span', }); observeCurrentLanguage(this.toLoginPageWrapper, PAGE_ANSWER, PAGE_ANSWER_KEYS.REGISTRATION); diff --git a/src/shared/API/customer/model/CustomerModel.ts b/src/shared/API/customer/model/CustomerModel.ts index 5e972cb6..ae008726 100644 --- a/src/shared/API/customer/model/CustomerModel.ts +++ b/src/shared/API/customer/model/CustomerModel.ts @@ -1,4 +1,4 @@ -import type { Address, User, UserLoginData } from '@/shared/types/user.ts'; +import type { Address, User, UserCredentials } from '@/shared/types/user.ts'; import type { Address as AddressResponse, BaseAddress, @@ -171,7 +171,7 @@ export class CustomerModel { return customer; } - public async authCustomer(userLoginData: UserLoginData): Promise { + public async authCustomer(userLoginData: UserCredentials): Promise { const data = await this.root.authenticateUser(userLoginData); return this.getCustomerFromData(data); } diff --git a/src/shared/API/product/model/ProductModel.ts b/src/shared/API/product/model/ProductModel.ts index 99c46e33..e14757b7 100644 --- a/src/shared/API/product/model/ProductModel.ts +++ b/src/shared/API/product/model/ProductModel.ts @@ -5,18 +5,25 @@ import type { CategoryReference, ClientResponse, LocalizedString, - ProductPagedQueryResponse, + ProductProjection, + ProductProjectionPagedQueryResponse, ProductProjectionPagedSearchResponse, - Product as ProductResponse, ProductVariant, } from '@commercetools/platform-sdk'; import getStore from '@/shared/Store/Store.ts'; import { setCategories, setProducts } from '@/shared/Store/actions.ts'; +import { PRICE_FRACTIONS } from '@/shared/constants/product.ts'; import getSize from '@/shared/utils/size.ts'; import getRoot, { type RootApi } from '../../sdk/root.ts'; -import { Attribute, type CategoriesProductCount, type OptionsRequest, type PriceRange } from '../../types/type.ts'; +import { + Attribute, + type CategoriesProductCount, + type OptionsRequest, + type PriceRange, + type SizeProductCount, +} from '../../types/type.ts'; import { isAttributePlainEnumValue, isCategoryPagedQueryResponse, @@ -24,15 +31,12 @@ import { isFacetRange, isFacetTerm, isLocalizationObj, - isProductPagedQueryResponse, + isProductProjectionPagedQueryResponse, isProductProjectionPagedSearchResponse, - isProductResponse, isRangeFacetResult, isTermFacetResult, } from '../../types/validation.ts'; -const PRICE_FRACTIONS = 100; - export class ProductModel { private root: RootApi; @@ -112,12 +116,12 @@ export class ProductModel { return price; } - private adaptProductPagedQueryToClient(data: ProductPagedQueryResponse): Product[] { - const response = data.results.map((product) => this.adaptProductToClient(product)); + private adaptProductProjectionPagedQueryToClient(data: ProductProjectionPagedQueryResponse): Product[] { + const response = data.results.map((product) => this.adaptProductProjectionToClient(product)); return response; } - private adaptProductToClient(product: ProductResponse): Product { + private adaptProductProjectionToClient(product: ProductProjection): Product { const result: Product = { category: [], description: [], @@ -128,9 +132,9 @@ export class ProductModel { name: [], variant: [], }; - result.category.push(...this.adaptCategoryReference(product.masterData.staged.categories)); - result.description.push(...this.adaptLocalizationValue(product.masterData.staged.description)); - result.name.push(...this.adaptLocalizationValue(product.masterData.staged.name)); + result.category.push(...this.adaptCategoryReference(product.categories)); + result.description.push(...this.adaptLocalizationValue(product.description)); + result.name.push(...this.adaptLocalizationValue(product.name)); Object.assign(result, this.adaptVariants(result, product)); @@ -148,10 +152,8 @@ export class ProductModel { return null; } - private adaptVariants(product: Product, response: ProductResponse): Product { - const variants = response.masterData.staged.variants.length - ? response.masterData.staged.variants - : [response.masterData.staged.masterVariant]; + private adaptVariants(product: Product, response: ProductProjection): Product { + const variants = [...response.variants, response.masterVariant]; variants.forEach((variant) => { let size: Size | null = null; @@ -243,20 +245,37 @@ export class ProductModel { return priceRange; } - private getProductFromData(data: ClientResponse): Product | null { - let product: Product | null = null; - if (isClientResponse(data) && isProductResponse(data.body)) { - product = this.adaptProductToClient(data.body); + private getProductsFromData(data: ClientResponse): Product[] | null { + let productList: Product[] | null = null; + if (isClientResponse(data) && isProductProjectionPagedQueryResponse(data.body)) { + productList = this.adaptProductProjectionPagedQueryToClient(data.body); } - return product; + return productList; } - private getProductsFromData(data: ClientResponse): Product[] | null { - let productList: Product[] | null = null; - if (isClientResponse(data) && isProductPagedQueryResponse(data.body)) { - productList = this.adaptProductPagedQueryToClient(data.body); + private getSizeProductCountFromData(data: ClientResponse): SizeProductCount[] { + const category: SizeProductCount[] = []; + if ( + isClientResponse(data) && + isProductProjectionPagedSearchResponse(data.body) && + 'variants.attributes.size.key' in data.body.facets + ) { + const categoriesFacet = data.body.facets['variants.attributes.size.key']; + if (isTermFacetResult(categoriesFacet)) { + categoriesFacet.terms.forEach((term) => { + if (isFacetTerm(term) && typeof term.term === 'string') { + const currentSize = getSize(term.term); + if (currentSize) { + category.push({ + count: term.count || 0, + size: currentSize, + }); + } + } + }); + } } - return productList; + return category; } public async getCategories(): Promise { @@ -274,11 +293,6 @@ export class ProductModel { return this.getPriceRangeFromData(data); } - public async getProductById(id: string): Promise { - const data = await this.root.getProductByID(id); - return this.getProductFromData(data); - } - public async getProducts(options?: OptionsRequest): Promise { const data = await this.root.getProducts(options); const products = this.getProductsFromData(data); @@ -287,6 +301,11 @@ export class ProductModel { } return products; } + + public async getSizeProductCount(): Promise { + const data = await this.root.getSizeProductCount(); + return this.getSizeProductCountFromData(data); + } } const createProductModel = (): ProductModel => new ProductModel(); diff --git a/src/shared/API/product/tests/Product.spec.ts b/src/shared/API/product/tests/Product.spec.ts index 88b22ca7..0ab234bb 100644 --- a/src/shared/API/product/tests/Product.spec.ts +++ b/src/shared/API/product/tests/Product.spec.ts @@ -58,33 +58,6 @@ describe('Checking Product Model', () => { }); } }); - - it('should get the product by id', async () => { - const product = await productModel.getProductById('e5381b83-a7c4-4060-9fdb-f4123db54dd2'); - if (product) { - expect(product).toBeDefined(); - expect(typeof product.id).toBe('string'); - expect(typeof product.key).toBe('string'); - expect(Array.isArray(product.name)).toBe(true); - product.name.forEach((localization: localization) => { - expect(typeof localization.language).toBe('string'); - expect(typeof localization.value).toBe('string'); - }); - expect(Array.isArray(product.description)).toBe(true); - product.description.forEach((localization: localization) => { - expect(typeof localization.language).toBe('string'); - expect(typeof localization.value).toBe('string'); - }); - expect(Array.isArray(product.fullDescription)).toBe(true); - product.fullDescription.forEach((localization: localization) => { - expect(typeof localization.language).toBe('string'); - expect(typeof localization.value).toBe('string'); - }); - expect(Array.isArray(product.images)).toBe(true); - expect(Array.isArray(product.category)).toBe(true); - expect(Array.isArray(product.variant)).toBe(true); - } - }); }); describe('getSize function', () => { diff --git a/src/shared/API/product/utils/filter.ts b/src/shared/API/product/utils/filter.ts new file mode 100644 index 00000000..de4ce17a --- /dev/null +++ b/src/shared/API/product/utils/filter.ts @@ -0,0 +1,33 @@ +import { PRICE_FRACTIONS } from '@/shared/constants/product.ts'; + +import { FilterFields, type PriceRange } from '../../types/type.ts'; + +export default function addFilter(field: FilterFields, value?: PriceRange | string): string { + let result = ''; + switch (field) { + case FilterFields.CATEGORY: + if (typeof value === 'string') { + result = `${field}:subtree("${value}")`; + } + break; + case FilterFields.NEW_ARRIVAL: + result = field; + break; + case FilterFields.SALE: + result = field; + break; + case FilterFields.SIZE: + if (typeof value === 'string') { + result = `${field}:"${value}"`; + } + break; + case FilterFields.PRICE: + if (value && typeof value !== 'string') { + result = `${field}: range(${value.min * PRICE_FRACTIONS} to ${value.max * PRICE_FRACTIONS})`; + } + break; + default: + result = ''; + } + return result; +} diff --git a/src/shared/API/product/utils/sort.ts b/src/shared/API/product/utils/sort.ts new file mode 100644 index 00000000..6d95dd22 --- /dev/null +++ b/src/shared/API/product/utils/sort.ts @@ -0,0 +1,5 @@ +import type { SortOptions } from '../../types/type.ts'; + +export default function makeSortRequest(sortOptions: SortOptions): string { + return `${sortOptions.field}${sortOptions.locale ? `.${sortOptions.locale}` : ''} ${sortOptions.direction}`; +} diff --git a/src/shared/API/sdk/root.ts b/src/shared/API/sdk/root.ts index 127539b6..af0afc5b 100644 --- a/src/shared/API/sdk/root.ts +++ b/src/shared/API/sdk/root.ts @@ -1,5 +1,6 @@ -import type { User, UserLoginData } from '@/shared/types/user.ts'; +import type { User, UserCredentials } from '@/shared/types/user.ts'; +import { DEFAULT_PAGE, MAX_PRICE, MIN_PRICE, PRODUCT_LIMIT } from '@/shared/constants/product.ts'; import { type ByProjectKeyRequestBuilder, type CategoryPagedQueryResponse, @@ -9,21 +10,18 @@ import { type CustomerSignInResult, type CustomerUpdateAction, type Product, - type ProductPagedQueryResponse, + type ProductProjectionPagedQueryResponse, type ProductProjectionPagedSearchResponse, createApiBuilderFromCtpClient, } from '@commercetools/platform-sdk'; import { type Client } from '@commercetools/sdk-client-v2'; -import type { OptionsRequest, SortOptions } from '../types/type.ts'; +import type { OptionsRequest } from '../types/type.ts'; +import makeSortRequest from '../product/utils/sort.ts'; import client from './client.ts'; type Nullable = T | null; -const PRODUCT_LIMIT = 9; -const DEFAULT_PAGE = 1; -const MIN_PRICE = 0; -const MAX_PRICE = 1000000; export type Credentials = { clientID: Nullable; @@ -62,15 +60,11 @@ export class RootApi { this.connection = this.root(this.client, projectKey); } - private makeSortRequest(sortOptions: SortOptions): string { - return `${sortOptions.field}${sortOptions.locale ? `.${sortOptions.locale}` : ''} ${sortOptions.direction}`; - } - private root(client: Client, projectKey: string): ByProjectKeyRequestBuilder { return createApiBuilderFromCtpClient(client).withProjectKey({ projectKey }); } - public async authenticateUser(userLoginData: UserLoginData): Promise> { + public async authenticateUser(userLoginData: UserCredentials): Promise> { const data = await this.connection.login().post({ body: userLoginData }).execute(); return data; } @@ -154,19 +148,21 @@ export class RootApi { return data; } - public async getProducts(options?: OptionsRequest): Promise> { - const { limit = PRODUCT_LIMIT, page = DEFAULT_PAGE, sort } = options || {}; + public async getProducts(options?: OptionsRequest): Promise> { + const { filter, limit = PRODUCT_LIMIT, page = DEFAULT_PAGE, search, sort } = options || {}; const data = await this.connection - .products() + .productProjections() + .search() .get({ queryArgs: { limit, + markMatchingVariants: true, offset: (page - 1) * PRODUCT_LIMIT, - ...(sort && { sort: this.makeSortRequest(sort) }), - // where: `masterData(staged(variants(prices(value(centAmount >= 5000))) or - // masterVariant(prices(value(centAmount >= 5000)))))`, - // where: `key = 10594917538474`, + ...(search && { [`text.${search.locale}`]: search.value }), + ...(search && { fuzzy: true }), + ...(sort && { sort: makeSortRequest(sort) }), + ...(filter && { filter }), withTotal: true, }, }) @@ -174,6 +170,20 @@ export class RootApi { return data; } + public async getSizeProductCount(): Promise> { + const data = await this.connection + .productProjections() + .search() + .get({ + queryArgs: { + facet: [`variants.attributes.size.key`], + limit: 0, + }, + }) + .execute(); + return data; + } + public async registrationUser(userData: User): Promise> { const userCredentials = { email: userData.email, diff --git a/src/shared/API/types/type.ts b/src/shared/API/types/type.ts index fef9ad95..ef89c925 100644 --- a/src/shared/API/types/type.ts +++ b/src/shared/API/types/type.ts @@ -1,13 +1,21 @@ -import { type Category } from '@/shared/types/product.ts'; +import { type Category, type Size } from '@/shared/types/product.ts'; export const Attribute = { FULL_DESCRIPTION: 'full_description', SIZE: 'size', }; +export enum FilterFields { + CATEGORY = 'categories.id', + NEW_ARRIVAL = 'variants.attributes.new_arrival:true', + PRICE = 'variants.price.centAmount', + SALE = 'variants.prices.discounted:exists', + SIZE = 'variants.attributes.size.key', +} + export enum SortFields { - NAME = 'masterData.staged.name', - PRICE = 'masterData.staged.variants.price.value', + NAME = 'name', + PRICE = 'price', } export enum SortDirection { @@ -21,9 +29,16 @@ export type SortOptions = { locale?: string; }; +export type SearchOptions = { + locale: string; + value: string; +}; + export type OptionsRequest = { + filter?: string[]; limit?: number; page?: number; + search?: SearchOptions; sort?: SortOptions; }; @@ -36,3 +51,8 @@ export type CategoriesProductCount = { category: Category; count: number; }; + +export type SizeProductCount = { + count: number; + size: Size; +}; diff --git a/src/shared/API/types/validation.ts b/src/shared/API/types/validation.ts index f9965a22..d5bd9a31 100644 --- a/src/shared/API/types/validation.ts +++ b/src/shared/API/types/validation.ts @@ -10,6 +10,7 @@ import type { LocalizedString, Product, ProductPagedQueryResponse, + ProductProjectionPagedQueryResponse, RangeFacetResult, TermFacetResult, } from '@commercetools/platform-sdk'; @@ -125,6 +126,21 @@ export function isProductProjectionPagedSearchResponse(data: unknown): data is P return Boolean(typeof data === 'object' && data && 'facets' in data && typeof data.facets === 'object'); } +export function isProductProjectionPagedQueryResponse(data: unknown): data is ProductProjectionPagedQueryResponse { + 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), + ); +} + export function isRangeFacetResult(data: unknown): data is RangeFacetResult { return Boolean( typeof data === 'object' && data && 'ranges' in data && Array.isArray(data.ranges) && data.ranges.length, @@ -161,9 +177,6 @@ export function isFacetTerm(data: unknown): data is FacetTerm { data && 'term' in data && typeof data.term === 'string' && - 'count' in data && - typeof data.count === 'number' && - 'productCount' in data && - typeof data.productCount === 'number', + ('count' in data || 'productCount' in data), ); } diff --git a/src/shared/Button/model/ButtonModel.ts b/src/shared/Button/model/ButtonModel.ts index e78e1570..8c4a972a 100644 --- a/src/shared/Button/model/ButtonModel.ts +++ b/src/shared/Button/model/ButtonModel.ts @@ -1,7 +1,5 @@ import type { ButtonAttributes } from '@/shared/types/button.ts'; -import { IS_DISABLED } from '@/shared/constants/buttons.ts'; - import ButtonView from '../view/ButtonView.ts'; class ButtonModel { @@ -16,12 +14,12 @@ class ButtonModel { } public setDisabled(): boolean { - this.view.getHTML().disabled = IS_DISABLED.DISABLED; + this.view.setDisabled(); return this.view.getHTML().disabled; } public setEnabled(): boolean { - this.view.getHTML().disabled = IS_DISABLED.ENABLED; + this.view.setEnabled(); return this.view.getHTML().disabled; } } diff --git a/src/shared/Button/view/ButtonView.ts b/src/shared/Button/view/ButtonView.ts index 7515d5f5..18079008 100644 --- a/src/shared/Button/view/ButtonView.ts +++ b/src/shared/Button/view/ButtonView.ts @@ -1,6 +1,6 @@ import type { ButtonAttributes } from '@/shared/types/button.ts'; -import TAG_NAME from '@/shared/constants/tags.ts'; +import { IS_DISABLED } from '@/shared/constants/buttons.ts'; import createBaseElement from '@/shared/utils/createBaseElement.ts'; class ButtonView { @@ -15,7 +15,7 @@ class ButtonView { attributes: params.attrs, cssClasses: params.classes, innerContent: params.text, - tag: TAG_NAME.BUTTON, + tag: 'button', }); if (params.action) { @@ -28,6 +28,16 @@ class ButtonView { public getHTML(): HTMLButtonElement { return this.button; } + + public setDisabled(): boolean { + this.button.disabled = IS_DISABLED.DISABLED; + return this.button.disabled; + } + + public setEnabled(): boolean { + this.button.disabled = IS_DISABLED.ENABLED; + return this.button.disabled; + } } export default ButtonView; diff --git a/src/shared/Input/view/InputView.ts b/src/shared/Input/view/InputView.ts index 923b7307..4d6bdce8 100644 --- a/src/shared/Input/view/InputView.ts +++ b/src/shared/Input/view/InputView.ts @@ -1,6 +1,5 @@ import type { InputParams } from '@/shared/types/form'; -import TAG_NAME from '@/shared/constants/tags.ts'; import createBaseElement from '@/shared/utils/createBaseElement.ts'; class InputView { @@ -19,7 +18,7 @@ class InputView { placeholder: attrs.placeholder || '', type: attrs.type, }, - tag: TAG_NAME.INPUT, + tag: 'input', }); return this.input; diff --git a/src/shared/Link/view/LinkView.ts b/src/shared/Link/view/LinkView.ts index e75ee000..fdc9fc70 100644 --- a/src/shared/Link/view/LinkView.ts +++ b/src/shared/Link/view/LinkView.ts @@ -1,6 +1,5 @@ import type { LinkAttributes } from '@/shared/types/link.ts'; -import TAG_NAME from '@/shared/constants/tags.ts'; import createBaseElement from '@/shared/utils/createBaseElement.ts'; import styles from './linkView.module.scss'; @@ -17,7 +16,7 @@ class LinkView { attributes: params.attrs, cssClasses: params.classes, innerContent: params.text, - tag: TAG_NAME.A, + tag: 'a', }); return this.link; diff --git a/src/shared/Loader/view/LoaderView.ts b/src/shared/Loader/view/LoaderView.ts index eeca6420..b0cf276d 100644 --- a/src/shared/Loader/view/LoaderView.ts +++ b/src/shared/Loader/view/LoaderView.ts @@ -1,7 +1,6 @@ import type { SizesType } from '@/shared/constants/sizes.ts'; import { SIZES } from '@/shared/constants/sizes.ts'; -import TAG_NAME from '@/shared/constants/tags.ts'; import createBaseElement from '@/shared/utils/createBaseElement.ts'; import styles from './loaderView.module.scss'; @@ -16,7 +15,7 @@ class LoaderView { private createHTML(size: SizesType): HTMLDivElement { this.loader = createBaseElement({ cssClasses: [styles.loader], - tag: TAG_NAME.DIV, + tag: 'div', }); this.selectSize(size); diff --git a/src/shared/ServerMessage/view/ServerMessageView.ts b/src/shared/ServerMessage/view/ServerMessageView.ts index 841722a1..490b7e3e 100644 --- a/src/shared/ServerMessage/view/ServerMessageView.ts +++ b/src/shared/ServerMessage/view/ServerMessageView.ts @@ -1,6 +1,5 @@ import SERVER_MESSAGE_ANIMATE_DETAILS from '@/shared/constants/animations.ts'; import { MESSAGE_STATUS, type MessageStatusType } from '@/shared/constants/messages.ts'; -import TAG_NAME from '@/shared/constants/tags.ts'; import createBaseElement from '@/shared/utils/createBaseElement.ts'; import styles from './serverMessageView.module.scss'; @@ -18,7 +17,7 @@ class ServerMessageView { private createHTML(): HTMLDivElement { this.serverWrapper = createBaseElement({ cssClasses: [styles.serverMessageWrapper], - tag: TAG_NAME.DIV, + tag: 'div', }); this.serverWrapper.append(this.serverMessage); @@ -29,7 +28,7 @@ class ServerMessageView { private createServerMessage(): HTMLSpanElement { this.serverMessage = createBaseElement({ cssClasses: [styles.serverMessage], - tag: TAG_NAME.SPAN, + tag: 'span', }); return this.serverMessage; diff --git a/src/shared/Store/Store.ts b/src/shared/Store/Store.ts index 1eef1517..12b9440f 100644 --- a/src/shared/Store/Store.ts +++ b/src/shared/Store/Store.ts @@ -2,7 +2,6 @@ import type { Action, State } from './reducer.ts'; import type { Reducer, ReduxStore } from './types'; -import { EVENT_NAME } from '../constants/events.ts'; import initialState from '../constants/initialState.ts'; import { STORAGE_KEY, saveCurrentStateToLocalStorage } from '../services/localStorage.ts'; import { rootReducer } from './reducer.ts'; @@ -28,7 +27,7 @@ export class Store implements ReduxStore { this.state = structuredClone(stateToSet); this.rootReducer = rootReducer; - window.addEventListener(EVENT_NAME.BEFOREUNLOAD, () => saveCurrentStateToLocalStorage(this.state)); + window.addEventListener('beforeunload', () => saveCurrentStateToLocalStorage(this.state)); } public dispatch(action: A): A { diff --git a/src/shared/constants/events.ts b/src/shared/constants/events.ts index ddfb3fcd..bada8622 100644 --- a/src/shared/constants/events.ts +++ b/src/shared/constants/events.ts @@ -1,34 +1,6 @@ -export const EVENT_NAME = { - ANIMATIONEND: 'animationend', - ANIMATIONITERATION: 'animationiteration', - ANIMATIONSTART: 'animationstart', - BEFOREUNLOAD: 'beforeunload', - BLUR: 'blur', - CHANGE: 'change', - CLICK: 'click', - CLOSE: 'close', - CONTEXTMENU: 'contextmenu', - DOM_CONTENT_LOADED: 'DOMContentLoaded', - ERROR: 'error', - FOCUS: 'focus', - HASHCHANGE: 'hashchange', - INPUT: 'input', - KEYDOWN: 'keydown', - KEYUP: 'keyup', - LOAD: 'load', - MESSAGE: 'message', - MOUSEENTER: 'mouseenter', - MOUSELEAVE: 'mouseleave', - MOUSEWHEEL: 'mousewheel', - OPEN: 'open', - POPSTATE: 'popstate', - RESIZE: 'resize', - SCROLL: 'scroll', - SUBMIT: 'submit', - TRANSITIONEND: 'transitionend', -} as const; - -export const MEDIATOR_EVENT = { +const MEDIATOR_EVENT = { CHANGE_PAGE: 'changePage', USER_LOGIN: 'userLogin', } as const; + +export default MEDIATOR_EVENT; diff --git a/src/shared/constants/forms/login/validationParams.ts b/src/shared/constants/forms/login/validationParams.ts index 0fa23703..136bdf88 100644 --- a/src/shared/constants/forms/login/validationParams.ts +++ b/src/shared/constants/forms/login/validationParams.ts @@ -1,6 +1,6 @@ import KEY from './constants.ts'; -const EMAIL_FIELD_VALIDATE = { +export const EMAIL_FIELD_VALIDATE = { key: `${KEY}email`, notWhitespace: { messages: { en: 'Email must not contain white spaces', ru: 'Почтовый адрес не может содержать пробелы' }, @@ -16,7 +16,7 @@ const EMAIL_FIELD_VALIDATE = { }, } as const; -const PASSWORD_FIELD_VALIDATE = { +export const PASSWORD_FIELD_VALIDATE = { key: `${KEY}password`, minLength: 8, notWhitespace: { @@ -33,6 +33,6 @@ const PASSWORD_FIELD_VALIDATE = { }, } as const; -const INPUT_FIELD_VALIDATION = [EMAIL_FIELD_VALIDATE, PASSWORD_FIELD_VALIDATE]; +const INPUT_VALIDATION = [EMAIL_FIELD_VALIDATE, PASSWORD_FIELD_VALIDATE]; -export default INPUT_FIELD_VALIDATION; +export default INPUT_VALIDATION; diff --git a/src/shared/constants/forms/register/validationParams.ts b/src/shared/constants/forms/register/validationParams.ts index 04b138c2..948e18c5 100644 --- a/src/shared/constants/forms/register/validationParams.ts +++ b/src/shared/constants/forms/register/validationParams.ts @@ -1,4 +1,4 @@ -const EMAIL_VALIDATE = { +export const EMAIL_VALIDATE = { key: 'registration_email', notWhitespace: { messages: { en: 'Email must not contain white spaces', ru: 'Почтовый адрес не может содержать пробелы' }, @@ -14,7 +14,7 @@ const EMAIL_VALIDATE = { }, } as const; -const PASSWORD_VALIDATE = { +export const PASSWORD_VALIDATE = { key: 'registration_password', minLength: 8, notWhitespace: { @@ -31,7 +31,7 @@ const PASSWORD_VALIDATE = { }, } as const; -const FIRST_NAME_VALIDATE = { +export const FIRST_NAME_VALIDATE = { key: 'firstName', minLength: 1, notSpecialSymbols: { @@ -51,7 +51,7 @@ const FIRST_NAME_VALIDATE = { required: true, } as const; -const LAST_NAME_VALIDATE = { +export const LAST_NAME_VALIDATE = { key: 'lastName', minLength: 1, notSpecialSymbols: { @@ -71,7 +71,7 @@ const LAST_NAME_VALIDATE = { required: true, } as const; -const BIRTHDAY_VALIDATE = { +export const BIRTHDAY_VALIDATE = { key: 'birthDate', required: true, validBirthday: { diff --git a/src/shared/constants/product.ts b/src/shared/constants/product.ts new file mode 100644 index 00000000..2ca7b8fb --- /dev/null +++ b/src/shared/constants/product.ts @@ -0,0 +1,5 @@ +export const PRICE_FRACTIONS = 100; +export const PRODUCT_LIMIT = 9; +export const DEFAULT_PAGE = 1; +export const MIN_PRICE = 0; +export const MAX_PRICE = 1000000; diff --git a/src/shared/constants/tags.ts b/src/shared/constants/tags.ts deleted file mode 100644 index ccac45ae..00000000 --- a/src/shared/constants/tags.ts +++ /dev/null @@ -1,54 +0,0 @@ -const TAG_NAME = { - A: 'a', - ADDRESS: 'address', - ARTICLE: 'article', - ASIDE: 'aside', - AUDIO: 'audio', - BLOCKQUOTE: 'blockquote', - BUTTON: 'button', - CANVAS: 'canvas', - DETAILS: 'details', - DIV: 'div', - DIVIDER: 'hr', - FOOTER: 'footer', - FORM: 'form', - H1: 'h1', - H2: 'h2', - H3: 'h3', - H4: 'h4', - H5: 'h5', - H6: 'h6', - HEADER: 'header', - I: 'i', - IMG: 'img', - INPUT: 'input', - LABEL: 'label', - LI: 'li', - MAIN: 'main', - MAP: 'map', - MARK: 'mark', - NAV: 'nav', - OL: 'ol', - P: 'p', - PRE: 'pre', - SECTION: 'section', - SELECT: 'select', - SOURCE: 'source', - SPAN: 'span', - STRONG: 'strong', - SUMMARY: 'summary', - SVG: 'svg', - TABLE: 'table', - TBODY: 'tbody', - TD: 'td', - TEXTAREA: 'textarea', - TFOOT: 'tfoot', - TH: 'th', - THEAD: 'thead', - TR: 'tr', - TRACK: 'track', - UL: 'ul', - VIDEO: 'video', -} as const; - -export default TAG_NAME; diff --git a/src/shared/types/address.ts b/src/shared/types/address.ts new file mode 100644 index 00000000..d2c85e45 --- /dev/null +++ b/src/shared/types/address.ts @@ -0,0 +1,15 @@ +export const ADDRESS_TYPE = { + BILLING: 'billing', + SHIPPING: 'shipping', +} as const; + +export const SINGLE_ADDRESS = 'asBilling'; + +export type AddressType = (typeof ADDRESS_TYPE)[keyof typeof ADDRESS_TYPE]; + +export interface AddressOptions { + delete?: boolean; + edit?: boolean; + setAsBilling?: boolean; + setDefault?: boolean; +} diff --git a/src/shared/types/user.ts b/src/shared/types/user.ts index c6127070..db570573 100644 --- a/src/shared/types/user.ts +++ b/src/shared/types/user.ts @@ -1,19 +1,21 @@ -export interface UserLoginData { +export interface UserCredentials { email: string; password: string; } -export interface User { +export interface PersonalData { + email: string; + firstName: string; + lastName: string; +} + +export interface User extends PersonalData, UserCredentials { addresses: Address[]; birthDate: string; defaultBillingAddressId: Address | null; defaultShippingAddressId: Address | null; - email: string; - firstName: string; id: string; - lastName: string; locale: string; - password: string; version: number; } @@ -24,11 +26,8 @@ export interface FormAddress { streetName: string; } -export interface Address extends FormAddress { - email: string; - firstName: string; +export interface Address extends FormAddress, PersonalData { id: string; - lastName: string; state: string; streetNumber: string; } diff --git a/src/shared/types/validation/test.spec.ts b/src/shared/types/validation/test.spec.ts index b006436b..404911e4 100644 --- a/src/shared/types/validation/test.spec.ts +++ b/src/shared/types/validation/test.spec.ts @@ -1,12 +1,12 @@ import * as User from './user.ts'; describe('test', () => { - it('isUserLoginData', () => { - expect(User.isUserLoginData({ email: 'a', password: 'b' })).toBe(true); - expect(User.isUserLoginData({ email: 1, password: 'b' })).toBe(false); - expect(User.isUserLoginData({ email: 'a', password: 2 })).toBe(false); - expect(User.isUserLoginData({ email: true, password: null })).toBe(false); - expect(User.isUserLoginData({ email: {}, password: [] })).toBe(false); + it('isUserCredentialsData', () => { + expect(User.isUserCredentialsData({ email: 'a', password: 'b' })).toBe(true); + expect(User.isUserCredentialsData({ email: 1, password: 'b' })).toBe(false); + expect(User.isUserCredentialsData({ email: 'a', password: 2 })).toBe(false); + expect(User.isUserCredentialsData({ email: true, password: null })).toBe(false); + expect(User.isUserCredentialsData({ email: {}, password: [] })).toBe(false); }); it('isFormAddress', () => { diff --git a/src/shared/types/validation/user.ts b/src/shared/types/validation/user.ts index 12e46e0f..06744afe 100644 --- a/src/shared/types/validation/user.ts +++ b/src/shared/types/validation/user.ts @@ -1,6 +1,6 @@ -import type { Address, FormAddress, User, UserLoginData } from '../user'; +import type { Address, FormAddress, User, UserCredentials } from '../user'; -export const isUserLoginData = (data: unknown): data is UserLoginData => +export const isUserCredentialsData = (data: unknown): data is UserCredentials => typeof data === 'object' && data !== null && 'email' in data && diff --git a/src/shared/utils/isKeyOfUserData.ts b/src/shared/utils/isKeyOfUserData.ts index 403b36dc..f8350d0b 100644 --- a/src/shared/utils/isKeyOfUserData.ts +++ b/src/shared/utils/isKeyOfUserData.ts @@ -1,6 +1,6 @@ -import type { UserLoginData } from '../types/user'; +import type { UserCredentials } from '../types/user'; -const isKeyOfUserData = (context: UserLoginData, key: string): key is keyof UserLoginData => +const isKeyOfUserData = (context: UserCredentials, key: string): key is keyof UserCredentials => Object.hasOwnProperty.call(context, key); export default isKeyOfUserData; diff --git a/src/shared/utils/messageTemplate.ts b/src/shared/utils/messageTemplate.ts new file mode 100644 index 00000000..c1375ef1 --- /dev/null +++ b/src/shared/utils/messageTemplate.ts @@ -0,0 +1,46 @@ +import getStore from '../Store/Store.ts'; +import { LANGUAGE_CHOICE } from '../constants/buttons.ts'; + +const messageTemplate = (beginning: string, variable: number | string, end: string): string => { + const start = beginning ? `${beginning} ` : ''; + const ending = end ? `${end}` : ''; + return `${start}${variable}${ending}`; +}; + +export const greeting = (name: string): string => messageTemplate('Hi, ', name, '!'); + +const maxLengthMessageRu = (maxLength: number): string => + messageTemplate('Максимальная длина не должна превышать', maxLength, ' символов'); + +const maxLengthMessageEn = (maxLength: number): string => + messageTemplate('Maximum length should not exceed', maxLength, ' characters'); + +export const maxLengthMessage = (maxLength: number): string => + getStore().getState().currentLanguage === LANGUAGE_CHOICE.EN + ? maxLengthMessageEn(maxLength) + : maxLengthMessageRu(maxLength); + +const maxAgeRu = (maxAge: number): string => messageTemplate('Вам должно быть не более', maxAge, ' лет'); + +const maxAgeEn = (maxAge: number): string => messageTemplate('You must be at most', maxAge, ' years old'); + +export const maxAgeMessage = (maxAge: number): string => + getStore().getState().currentLanguage === LANGUAGE_CHOICE.EN ? maxAgeEn(maxAge) : maxAgeRu(maxAge); + +const minAgeRu = (minAge: number): string => messageTemplate('Вам должно быть не менее', minAge, ' лет'); + +const minAgeEn = (minAge: number): string => messageTemplate('You must be at least', minAge, ' years old'); + +export const minAgeMessage = (minAge: number): string => + getStore().getState().currentLanguage === LANGUAGE_CHOICE.EN ? minAgeEn(minAge) : minAgeRu(minAge); + +const minLengthMessageRu = (minLength: number): string => + messageTemplate('Минимальная длина должна быть не менее', minLength, ' символов'); + +const minLengthMessageEn = (minLength: number): string => + messageTemplate('Minimum length should be at least', minLength, ' characters'); + +export const minLengthMessage = (minLength: number): string => + getStore().getState().currentLanguage === LANGUAGE_CHOICE.EN + ? minLengthMessageEn(minLength) + : minLengthMessageRu(minLength); diff --git a/src/widgets/Footer/view/FooterView.ts b/src/widgets/Footer/view/FooterView.ts index fa72034c..d3359cba 100644 --- a/src/widgets/Footer/view/FooterView.ts +++ b/src/widgets/Footer/view/FooterView.ts @@ -1,4 +1,3 @@ -import TAG_NAME from '@/shared/constants/tags.ts'; import createBaseElement from '@/shared/utils/createBaseElement.ts'; import styles from './footerView.module.scss'; @@ -13,7 +12,7 @@ class FooterView { private createHTML(): HTMLElement { this.footer = createBaseElement({ cssClasses: [styles.footer], - tag: TAG_NAME.FOOTER, + tag: 'footer', }); return this.footer; } diff --git a/src/widgets/Header/model/HeaderModel.ts b/src/widgets/Header/model/HeaderModel.ts index ba687d35..06adcbbe 100644 --- a/src/widgets/Header/model/HeaderModel.ts +++ b/src/widgets/Header/model/HeaderModel.ts @@ -5,7 +5,6 @@ import getStore from '@/shared/Store/Store.ts'; import { setCurrentUser } from '@/shared/Store/actions.ts'; import observeStore, { selectCurrentUser } from '@/shared/Store/observer.ts'; // import { LANGUAGE_CHOICE } from '@/shared/constants/buttons.ts'; -import { EVENT_NAME } from '@/shared/constants/events.ts'; import { PAGE_ID } from '@/shared/constants/pages.ts'; import HeaderView from '../view/HeaderView.ts'; @@ -71,7 +70,7 @@ class HeaderModel { private setLogoHandler(): boolean { const logo = this.view.getLinkLogo().getHTML(); - logo.addEventListener(EVENT_NAME.CLICK, (event) => { + logo.addEventListener('click', (event) => { event.preventDefault(); this.router.navigateTo(PAGE_ID.DEFAULT_PAGE); }); @@ -80,7 +79,7 @@ class HeaderModel { private setLogoutButtonHandler(): boolean { const logoutButton = this.view.getLogoutButton(); - logoutButton.getHTML().addEventListener(EVENT_NAME.CLICK, () => { + logoutButton.getHTML().addEventListener('click', () => { this.logoutHandler(); logoutButton.setDisabled(); }); diff --git a/src/widgets/Header/view/HeaderView.ts b/src/widgets/Header/view/HeaderView.ts index 71e21e60..7e3cef95 100644 --- a/src/widgets/Header/view/HeaderView.ts +++ b/src/widgets/Header/view/HeaderView.ts @@ -4,7 +4,6 @@ import getStore from '@/shared/Store/Store.ts'; import { BUTTON_TEXT, BUTTON_TEXT_KEYS } from '@/shared/constants/buttons.ts'; import { PAGE_ID } from '@/shared/constants/pages.ts'; import SVG_DETAILS from '@/shared/constants/svg.ts'; -import TAG_NAME from '@/shared/constants/tags.ts'; import createBaseElement from '@/shared/utils/createBaseElement.ts'; import createSVGUse from '@/shared/utils/createSVGUse.ts'; import observeCurrentLanguage from '@/shared/utils/observeCurrentLanguage.ts'; @@ -39,7 +38,7 @@ class HeaderView { private createHTML(): HTMLElement { this.header = createBaseElement({ cssClasses: [styles.header], - tag: TAG_NAME.HEADER, + tag: 'header', }); this.header.append(this.linkLogo.getHTML(), this.logoutButton.getHTML()); @@ -54,7 +53,7 @@ class HeaderView { classes: [styles.logo], }); - const svg = document.createElementNS(SVG_DETAILS.SVG_URL, TAG_NAME.SVG); + const svg = document.createElementNS(SVG_DETAILS.SVG_URL, 'svg'); svg.append(createSVGUse(SVG_DETAILS.LOGO)); this.linkLogo.getHTML().append(svg); return this.linkLogo; diff --git a/src/widgets/Header/view/headerView.module.scss b/src/widgets/Header/view/headerView.module.scss index d80b4572..eba35c1f 100644 --- a/src/widgets/Header/view/headerView.module.scss +++ b/src/widgets/Header/view/headerView.module.scss @@ -7,7 +7,7 @@ display: grid; align-items: center; justify-content: space-between; - grid-template-columns: repeat(4, max-content); + grid-template-columns: repeat(3, max-content); margin-bottom: var(--medium-offset); border-bottom: 10px solid var(--steam-green-800); border-radius: var(--medium-br); @@ -37,7 +37,7 @@ .logoutButton, .changeLanguageButton { - grid-column: 4; + grid-column: 3; border-radius: var(--small-br); padding: calc(var(--extra-small-offset) / 2) var(--extra-small-offset); font: var(--regular-font); diff --git a/src/widgets/LoginForm/model/LoginFormModel.ts b/src/widgets/LoginForm/model/LoginFormModel.ts index 2cea6108..f4d021c4 100644 --- a/src/widgets/LoginForm/model/LoginFormModel.ts +++ b/src/widgets/LoginForm/model/LoginFormModel.ts @@ -1,5 +1,5 @@ import type InputFieldModel from '@/entities/InputField/model/InputFieldModel.ts'; -import type { User, UserLoginData } from '@/shared/types/user.ts'; +import type { UserCredentials } from '@/shared/types/user.ts'; import getCustomerModel from '@/shared/API/customer/model/CustomerModel.ts'; import EventMediatorModel from '@/shared/EventMediator/model/EventMediatorModel.ts'; @@ -7,76 +7,54 @@ import LoaderModel from '@/shared/Loader/model/LoaderModel.ts'; import serverMessageModel from '@/shared/ServerMessage/model/ServerMessageModel.ts'; import getStore from '@/shared/Store/Store.ts'; import { setCurrentUser } from '@/shared/Store/actions.ts'; -import { EVENT_NAME, MEDIATOR_EVENT } from '@/shared/constants/events.ts'; -import KEY from '@/shared/constants/forms/login/constants.ts'; +import MEDIATOR_EVENT from '@/shared/constants/events.ts'; +import { INPUT_TYPE, PASSWORD_TEXT } from '@/shared/constants/forms.ts'; import { MESSAGE_STATUS, SERVER_MESSAGE } from '@/shared/constants/messages.ts'; import { SIZES } from '@/shared/constants/sizes.ts'; -import { isUserLoginData } from '@/shared/types/validation/user.ts'; -import isKeyOfUserData from '@/shared/utils/isKeyOfUserData.ts'; +import { isUserCredentialsData } from '@/shared/types/validation/user.ts'; +import { greeting } from '@/shared/utils/messageTemplate.ts'; import LoginFormView from '../view/LoginFormView.ts'; class LoginFormModel { private eventMediator = EventMediatorModel.getInstance(); - private inputFields: InputFieldModel[] = []; - - private isValidInputFields: Record = {}; - - private userData: UserLoginData = { - email: '', - password: '', - }; - private view: LoginFormView = new LoginFormView(); constructor() { this.init(); } - private async checkHasEmailHandler(email: string): Promise { - const response = await getCustomerModel().hasEmail(email); - return response; - } - private createGreetingMessage(name: string): string { - const greeting = `Welcome, ${name}! ${SERVER_MESSAGE.SUCCESSFUL_LOGIN}`; - return greeting; + const greetingMessage = `${greeting(name)} ${SERVER_MESSAGE.SUCCESSFUL_LOGIN}`; + return greetingMessage; } - private getFormData(): UserLoginData { - this.inputFields.forEach((inputField) => { - const input = inputField.getView().getInput(); - const inputHTML = input.getHTML(); - const inputValue = input.getValue(); - - const key = inputHTML.id.replace(KEY, ''); - - if (isKeyOfUserData(this.userData, key)) { - this.userData[key] = inputValue; - this.isValidInputFields[inputHTML.id] = false; - } - - input.clear(); - }); + private getFormData(): UserCredentials { + const userData: UserCredentials = { + email: this.view.getEmailField().getView().getValue(), + password: this.view.getPasswordField().getView().getValue(), + }; + this.view.getInputFields().forEach((inputField) => inputField.getView().getInput().clear()); this.view.getSubmitFormButton().setDisabled(); - return this.userData; + return userData; } private init(): boolean { - this.inputFields = this.view.getInputFields(); - this.inputFields.forEach((inputField) => this.setInputFieldHandlers(inputField)); + this.view.getInputFields().forEach((inputField) => this.setInputFieldHandlers(inputField)); this.setPreventDefaultToForm(); this.setSubmitFormHandler(); this.subscribeToEventMediator(); + this.setSwitchPasswordVisibilityHandler(); return true; } - private loginUser(userLoginData: UserLoginData): void { + private loginUser(userLoginData: UserCredentials): void { const loader = new LoaderModel(SIZES.MEDIUM).getHTML(); this.view.getSubmitFormButton().getHTML().append(loader); - this.checkHasEmailHandler(userLoginData.email) + getCustomerModel() + .hasEmail(userLoginData.email) .then((response) => { if (response) { this.loginUserHandler(userLoginData); @@ -90,7 +68,7 @@ class LoginFormModel { .finally(() => loader.remove()); } - private loginUserHandler(userLoginData: UserLoginData): void { + private loginUserHandler(userLoginData: UserCredentials): void { const loader = new LoaderModel(SIZES.MEDIUM).getHTML(); this.view.getSubmitFormButton().getHTML().append(loader); getCustomerModel() @@ -109,31 +87,35 @@ class LoginFormModel { private setInputFieldHandlers(inputField: InputFieldModel): boolean { const inputHTML = inputField.getView().getInput().getHTML(); - this.isValidInputFields[inputHTML.id] = false; - inputHTML.addEventListener(EVENT_NAME.INPUT, () => { - this.isValidInputFields[inputHTML.id] = inputField.getIsValid(); - this.switchSubmitFormButtonAccess(); - }); + + inputHTML.addEventListener('input', () => this.switchSubmitFormButtonAccess()); return true; } private setPreventDefaultToForm(): boolean { - this.getHTML().addEventListener(EVENT_NAME.SUBMIT, (event) => event.preventDefault()); + this.getHTML().addEventListener('submit', (event) => event.preventDefault()); return true; } private setSubmitFormHandler(): boolean { const submitButton = this.view.getSubmitFormButton().getHTML(); - submitButton.addEventListener(EVENT_NAME.CLICK, () => { - const formData = this.getFormData(); - this.loginUser(formData); + submitButton.addEventListener('click', () => this.loginUser(this.getFormData())); + return true; + } + + private setSwitchPasswordVisibilityHandler(): boolean { + this.view.getShowPasswordElement().addEventListener('click', () => { + const input = this.view.getPasswordField().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); }); return true; } private subscribeToEventMediator(): boolean { this.eventMediator.subscribe(MEDIATOR_EVENT.USER_LOGIN, (userLoginData) => { - if (isUserLoginData(userLoginData)) { + if (isUserCredentialsData(userLoginData)) { this.loginUser(userLoginData); } }); @@ -141,9 +123,8 @@ class LoginFormModel { } private switchSubmitFormButtonAccess(): boolean { - if (Object.values(this.isValidInputFields).every((value) => value)) { + if (this.view.getInputFields().every((inputField) => inputField.getIsValid())) { this.view.getSubmitFormButton().setEnabled(); - this.view.getSubmitFormButton().getHTML().focus(); } else { this.view.getSubmitFormButton().setDisabled(); } @@ -152,7 +133,7 @@ class LoginFormModel { } public getFirstInputField(): InputFieldModel { - return this.inputFields[0]; + return this.view.getInputFields()[0]; } public getHTML(): HTMLFormElement { diff --git a/src/widgets/LoginForm/view/LoginFormView.ts b/src/widgets/LoginForm/view/LoginFormView.ts index ff6ac327..4d33d056 100644 --- a/src/widgets/LoginForm/view/LoginFormView.ts +++ b/src/widgets/LoginForm/view/LoginFormView.ts @@ -2,31 +2,47 @@ import InputFieldModel from '@/entities/InputField/model/InputFieldModel.ts'; import ButtonModel from '@/shared/Button/model/ButtonModel.ts'; import getStore from '@/shared/Store/Store.ts'; import { BUTTON_TEXT, BUTTON_TEXT_KEYS, BUTTON_TYPE } from '@/shared/constants/buttons.ts'; +import { INPUT_TYPE } from '@/shared/constants/forms.ts'; import * as FORM_INPUTS from '@/shared/constants/forms/login/fieldParams.ts'; import * as FORM_VALIDATION from '@/shared/constants/forms/login/validationParams.ts'; -import TAG_NAME from '@/shared/constants/tags.ts'; +import SVG_DETAILS from '@/shared/constants/svg.ts'; import createBaseElement from '@/shared/utils/createBaseElement.ts'; +import createSVGUse from '@/shared/utils/createSVGUse.ts'; import observeCurrentLanguage from '@/shared/utils/observeCurrentLanguage.ts'; import styles from './loginForm.module.scss'; class LoginFormView { + private emailField: InputFieldModel; + private form: HTMLFormElement; private inputFields: InputFieldModel[] = []; + private passwordField: InputFieldModel; + + private showPasswordElement: HTMLDivElement; + private submitFormButton: ButtonModel; constructor() { - this.inputFields = this.createInputFields(); + this.showPasswordElement = this.createShowPasswordElement(); + this.emailField = this.createEmailField(); + this.passwordField = this.createPasswordField(); this.submitFormButton = this.createSubmitFormButton(); this.form = this.createHTML(); } + private createEmailField(): InputFieldModel { + this.emailField = new InputFieldModel(FORM_INPUTS.EMAIL_FIELD, FORM_VALIDATION.EMAIL_FIELD_VALIDATE); + this.inputFields.push(this.emailField); + return this.emailField; + } + private createHTML(): HTMLFormElement { this.form = createBaseElement({ cssClasses: [styles.loginForm], - tag: TAG_NAME.FORM, + tag: 'form', }); this.inputFields.forEach((inputField) => { @@ -43,21 +59,23 @@ class LoginFormView { return this.form; } - private createInputFields(): InputFieldModel[] { - FORM_INPUTS.INPUT_FIELD.forEach((inputFieldParams) => { - const currentValidateParams = FORM_VALIDATION.default.find( - (validParams) => validParams.key === inputFieldParams.inputParams.id, - ); + private createPasswordField(): InputFieldModel { + this.passwordField = new InputFieldModel(FORM_INPUTS.PASSWORD_FIELD, FORM_VALIDATION.PASSWORD_FIELD_VALIDATE); + this.inputFields.push(this.passwordField); + const inputElement = this.passwordField.getView().getHTML(); + if (inputElement instanceof HTMLLabelElement) { + inputElement.append(this.showPasswordElement); + } + return this.passwordField; + } - if (currentValidateParams) { - const inputField = new InputFieldModel(inputFieldParams, currentValidateParams); - this.inputFields.push(inputField); - } else { - this.inputFields.push(new InputFieldModel(inputFieldParams, null)); - } + private createShowPasswordElement(): HTMLDivElement { + this.showPasswordElement = createBaseElement({ + cssClasses: [styles.showPasswordElement], + tag: 'div', }); - - return this.inputFields; + this.switchPasswordElementSVG(INPUT_TYPE.PASSWORD); + return this.showPasswordElement; } private createSubmitFormButton(): ButtonModel { @@ -77,6 +95,10 @@ class LoginFormView { return this.submitFormButton; } + public getEmailField(): InputFieldModel { + return this.emailField; + } + public getHTML(): HTMLFormElement { return this.form; } @@ -85,9 +107,25 @@ class LoginFormView { return this.inputFields; } + public getPasswordField(): InputFieldModel { + return this.passwordField; + } + + public getShowPasswordElement(): HTMLDivElement { + return this.showPasswordElement; + } + public getSubmitFormButton(): ButtonModel { return this.submitFormButton; } + + public switchPasswordElementSVG(type: string): SVGSVGElement { + const svg = document.createElementNS(SVG_DETAILS.SVG_URL, 'svg'); + this.showPasswordElement.innerHTML = ''; + svg.append(createSVGUse(type === INPUT_TYPE.PASSWORD ? SVG_DETAILS.CLOSE_EYE : SVG_DETAILS.OPEN_EYE)); + this.showPasswordElement.append(svg); + return svg; + } } export default LoginFormView; diff --git a/src/widgets/LoginForm/view/loginForm.module.scss b/src/widgets/LoginForm/view/loginForm.module.scss index c1ac616e..1129902c 100644 --- a/src/widgets/LoginForm/view/loginForm.module.scss +++ b/src/widgets/LoginForm/view/loginForm.module.scss @@ -12,41 +12,6 @@ display: flex; flex-direction: column; gap: calc(var(--extra-small-offset) / 2); - - button { - position: absolute; - right: 0; - top: 7px; - border-radius: var(--small-br); - padding: calc(var(--extra-small-offset) / 8); - width: 24px; - height: 24px; - background-color: transparent; - transform: translate(-12%, 0); - - svg { - width: 20px; - height: 20px; - stroke: var(--noble-gray-600); - transition: stroke 0.2s; - } - - @media (hover: hover) { - &:hover { - background-color: var(--noble-gray-200); - - svg { - stroke: var(--steam-green-800); - } - } - } - } - - @media (max-width: 768px) { - button { - top: 2px; - } - } } input { @@ -84,9 +49,11 @@ .submitFormButton { display: flex; align-items: center; + align-self: center; justify-content: center; border-radius: var(--small-br); padding: calc(var(--extra-small-offset) / 2) var(--small-offset); + width: max-content; font: var(--bold-font); letter-spacing: 1px; color: var(--white); @@ -110,3 +77,44 @@ pointer-events: none; } } + +.showPasswordElement { + position: absolute; + right: 0; + top: 7px; + border-radius: var(--small-br); + padding: calc(var(--extra-small-offset) / 8); + width: 24px; + height: 24px; + background-color: transparent; + transform: translate(-12%, 0); + + svg { + width: 20px; + height: 20px; + stroke: var(--noble-gray-600); + transition: stroke 0.2s; + } + + &:focus { + background-color: var(--noble-gray-200); + + svg { + stroke: var(--steam-green-800); + } + } + + @media (hover: hover) { + &:hover { + background-color: var(--noble-gray-200); + + svg { + stroke: var(--steam-green-800); + } + } + } + + @media (max-width: 768px) { + top: 2px; + } +} diff --git a/src/widgets/RegistrationForm/model/RegistrationFormModel.ts b/src/widgets/RegistrationForm/model/RegistrationFormModel.ts index 7edf640f..10886f94 100644 --- a/src/widgets/RegistrationForm/model/RegistrationFormModel.ts +++ b/src/widgets/RegistrationForm/model/RegistrationFormModel.ts @@ -1,44 +1,37 @@ import type InputFieldModel from '@/entities/InputField/model/InputFieldModel.ts'; -import type { Address, User } from '@/shared/types/user.ts'; +import type { AddressType } from '@/shared/types/address.ts'; +import type { Address, PersonalData, User, UserCredentials } from '@/shared/types/user.ts'; -import CountryChoiceModel from '@/features/CountryChoice/model/CountryChoiceModel.ts'; +import AddressModel from '@/entities/Address/model/AddressModel.ts'; import getCustomerModel, { CustomerModel } from '@/shared/API/customer/model/CustomerModel.ts'; import EventMediatorModel from '@/shared/EventMediator/model/EventMediatorModel.ts'; import LoaderModel from '@/shared/Loader/model/LoaderModel.ts'; import serverMessageModel from '@/shared/ServerMessage/model/ServerMessageModel.ts'; import getStore from '@/shared/Store/Store.ts'; -import { setCurrentUser } from '@/shared/Store/actions.ts'; -import { EVENT_NAME, MEDIATOR_EVENT } from '@/shared/constants/events.ts'; -import * as CONSTANT_FORMS from '@/shared/constants/forms.ts'; -import * as FORM_CONSTANT from '@/shared/constants/forms/register/constant.ts'; -import * as FORM_FIELDS from '@/shared/constants/forms/register/fieldParams.ts'; +import { setBillingCountry, setCurrentUser } from '@/shared/Store/actions.ts'; +import MEDIATOR_EVENT from '@/shared/constants/events.ts'; +import { INPUT_TYPE, PASSWORD_TEXT } from '@/shared/constants/forms.ts'; import { MESSAGE_STATUS, SERVER_MESSAGE } from '@/shared/constants/messages.ts'; import { SIZES } from '@/shared/constants/sizes.ts'; -import isKeyOfUserData from '@/shared/utils/isKeyOfUserData.ts'; +import { ADDRESS_TYPE } from '@/shared/types/address.ts'; import RegistrationFormView from '../view/RegistrationFormView.ts'; class RegisterFormModel { + private addressWrappers: Record = { + [ADDRESS_TYPE.BILLING]: new AddressModel(ADDRESS_TYPE.BILLING, { + setDefault: true, + }), + [ADDRESS_TYPE.SHIPPING]: new AddressModel(ADDRESS_TYPE.SHIPPING, { + setAsBilling: true, + setDefault: true, + }), + }; + private eventMediator = EventMediatorModel.getInstance(); private inputFields: InputFieldModel[] = []; - private isValidInputFields: Record = {}; - - private userData: User = { - addresses: [], - birthDate: '', - defaultBillingAddressId: null, - defaultShippingAddressId: null, - email: '', - firstName: '', - id: '', - lastName: '', - locale: '', - password: '', - version: 0, - }; - private view: RegistrationFormView = new RegistrationFormView(); constructor() { @@ -56,35 +49,6 @@ class RegisterFormModel { return currentUserData; } - private createBillingCountryChoice(): boolean { - return this.createCountryChoice( - FORM_FIELDS.BILLING_ADDRESS_COUNTRY.inputParams.id, - this.view.getBillingAddressWrapper(), - ); - } - - private createCountryChoice(inputFieldId: string, wrapper: HTMLElement): boolean { - const addressInput = this.view - .getInputFields() - .find((inputField) => inputField.getView().getInput().getHTML().id === inputFieldId) - ?.getView() - .getInput() - .getHTML(); - - if (addressInput) { - const countryChoiceModel = new CountryChoiceModel(addressInput); - wrapper.append(countryChoiceModel.getHTML()); - } - return true; - } - - private createShippingCountryChoice(): boolean { - return this.createCountryChoice( - FORM_FIELDS.SHIPPING_ADDRESS_COUNTRY.inputParams.id, - this.view.getShippingAddressWrapper(), - ); - } - private async editDefaultBillingAddress(addressId: string, userData: User | null): Promise { let currentUserData = userData; if (currentUserData) { @@ -107,97 +71,67 @@ class RegisterFormModel { return currentUserData; } - private getAddressData(key: string): Address { - const addressData = - key === CONSTANT_FORMS.USER_ADDRESS_TYPE.BILLING - ? this.getAddressDataHandler( - [ - FORM_FIELDS.BILLING_ADDRESS_CITY, - FORM_FIELDS.BILLING_ADDRESS_POSTAL_CODE, - FORM_FIELDS.BILLING_ADDRESS_STREET, - ], - CONSTANT_FORMS.USER_COUNTRY_ADDRESS.BILLING, - ) - : this.getAddressDataHandler( - [ - FORM_FIELDS.SHIPPING_ADDRESS_CITY, - FORM_FIELDS.SHIPPING_ADDRESS_POSTAL_CODE, - FORM_FIELDS.SHIPPING_ADDRESS_STREET, - ], - CONSTANT_FORMS.USER_COUNTRY_ADDRESS.SHIPPING, - ); - - return addressData; + private getCredentialsData(): UserCredentials { + return { + email: this.view.getEmailField().getView().getValue(), + password: this.view.getPasswordField().getView().getValue(), + }; } - private getAddressDataHandler( - [city, postalCode, street]: { inputParams: { id: string } }[], - country: string, - ): Address { - const store = getStore().getState(); - const addressData: Address = { - city: this.getAddressValue(city), - country: country === CONSTANT_FORMS.USER_COUNTRY_ADDRESS.BILLING ? store.billingCountry : store.shippingCountry, - email: this.userData.email, - firstName: this.userData.firstName, + private getFormUserData(): User { + const userData: User = { + addresses: [], + birthDate: this.view.getDateOfBirthField().getView().getValue(), + defaultBillingAddressId: null, + defaultShippingAddressId: null, + email: this.view.getEmailField().getView().getValue(), + firstName: this.view.getFirstNameField().getView().getValue(), id: '', - lastName: this.userData.lastName, - postalCode: this.getAddressValue(postalCode), - state: '', - streetName: this.getAddressValue(street), - streetNumber: '', + lastName: this.view.getLastNameField().getView().getValue(), + locale: '', + password: this.view.getPasswordField().getView().getValue(), + version: 0, }; - return addressData; - } - private getAddressValue(field: { inputParams: { id: string } }): string { - return ( - this.inputFields - .find((inputField) => inputField.getView().getInput().getHTML().id === field.inputParams.id) - ?.getView() - .getValue() || '' - ); + this.view.getSubmitFormButton().setDisabled(); + return userData; } - private getFormUserData(): User { - this.inputFields.forEach((inputField) => { - const input = inputField.getView().getInput(); - const inputHTML = input.getHTML(); - const inputValue = input.getValue(); - - const key = inputHTML.id.replace(FORM_CONSTANT.KEY, ''); - - if (isKeyOfUserData(this.userData, key)) { - this.userData[key] = inputValue; - this.isValidInputFields[inputHTML.id] = false; - input.clear(); - } - }); - - this.view.getSubmitFormButton().setDisabled(); - return this.userData; + private getPersonalData(): PersonalData { + return { + email: this.view.getEmailField().getView().getValue(), + firstName: this.view.getFirstNameField().getView().getValue(), + lastName: this.view.getLastNameField().getView().getValue(), + }; } private init(): boolean { this.inputFields = this.view.getInputFields(); + Object.values(this.addressWrappers) + .reverse() + .forEach((addressWrapper) => { + this.inputFields.push(...addressWrapper.getView().getInputFields()); + this.getHTML().append(addressWrapper.getHTML()); + }); this.inputFields.forEach((inputField) => this.setInputFieldHandlers(inputField)); this.setPreventDefaultToForm(); this.setSubmitFormHandler(); - this.createBillingCountryChoice(); - this.createShippingCountryChoice(); - const checkboxSingleAddress = this.view.getSingleAddressCheckBox().getHTML(); - checkboxSingleAddress.addEventListener(EVENT_NAME.CHANGE, () => + const checkboxSingleAddress = this.addressWrappers[ADDRESS_TYPE.SHIPPING] + .getView() + .getAddressAsBillingCheckBox() + ?.getHTML(); + checkboxSingleAddress?.addEventListener('change', () => this.singleAddressCheckBoxHandler(checkboxSingleAddress.checked), ); + this.setSwitchPasswordVisibilityHandler(); return true; } private registerUser(): void { const loader = new LoaderModel(SIZES.MEDIUM).getHTML(); this.view.getSubmitFormButton().getHTML().append(loader); - this.getFormUserData(); getCustomerModel() - .registerNewCustomer(this.userData) + .registerNewCustomer(this.getFormUserData()) .then((newUserData) => { if (newUserData) { this.successfulUserRegistration(newUserData); @@ -211,23 +145,40 @@ class RegisterFormModel { } private resetInputFieldsValidation(): void { - Object.entries(this.isValidInputFields).forEach(([key]) => { - this.isValidInputFields[key] = false; - }); + const checkboxSingleAddress = this.addressWrappers[ADDRESS_TYPE.SHIPPING] + .getView() + .getAddressAsBillingCheckBox() + ?.getHTML(); + const checkboxDefaultShippingAddress = this.addressWrappers[ADDRESS_TYPE.SHIPPING] + .getView() + .getAddressByDefaultCheckBox() + ?.getHTML(); + const checkboxDefaultBillingAddress = this.addressWrappers[ADDRESS_TYPE.BILLING] + .getView() + .getAddressByDefaultCheckBox() + ?.getHTML(); + if (checkboxSingleAddress) { + checkboxSingleAddress.checked = false; + } + if (checkboxDefaultShippingAddress) { + checkboxDefaultShippingAddress.checked = false; + } + if (checkboxDefaultBillingAddress) { + checkboxDefaultBillingAddress.checked = false; + } + this.addressWrappers[ADDRESS_TYPE.BILLING].getView().switchVisibilityAddressWrapper(false); } private setInputFieldHandlers(inputField: InputFieldModel): boolean { const inputHTML = inputField.getView().getInput().getHTML(); - this.isValidInputFields[inputHTML.id] = false; - inputHTML.addEventListener(EVENT_NAME.INPUT, () => { - this.isValidInputFields[inputHTML.id] = inputField.getIsValid(); + inputHTML.addEventListener('input', () => { this.switchSubmitFormButtonAccess(); }); return true; } private setPreventDefaultToForm(): boolean { - this.getHTML().addEventListener(EVENT_NAME.SUBMIT, (event) => { + this.getHTML().addEventListener('submit', (event) => { event.preventDefault(); }); @@ -236,54 +187,61 @@ class RegisterFormModel { private setSubmitFormHandler(): boolean { const submitButton = this.view.getSubmitFormButton().getHTML(); - submitButton.addEventListener(EVENT_NAME.CLICK, () => this.registerUser()); + submitButton.addEventListener('click', () => this.registerUser()); return true; } - private singleAddressCheckBoxHandler(isChecked: boolean): boolean { - const billingAddressFieldID = [ - FORM_FIELDS.BILLING_ADDRESS_COUNTRY.inputParams.id, - FORM_FIELDS.BILLING_ADDRESS_STREET.inputParams.id, - FORM_FIELDS.BILLING_ADDRESS_CITY.inputParams.id, - FORM_FIELDS.BILLING_ADDRESS_POSTAL_CODE.inputParams.id, - ]; - - this.view.switchVisibilityBillingAddressWrapper(isChecked); + private setSwitchPasswordVisibilityHandler(): boolean { + this.view.getShowPasswordElement().addEventListener('click', () => { + const input = this.view.getPasswordField().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); + }); + return true; + } + private singleAddressCheckBoxHandler(isChecked: boolean): boolean { if (!isChecked) { window.scrollTo({ top: document.documentElement.scrollHeight, }); } - billingAddressFieldID.forEach((id) => { - this.inputFields - .find((inputField) => inputField.getView().getInput().getHTML().id === id) - ?.getView() - .getInput() - .clear(); - this.isValidInputFields[id] = isChecked; - }); + const billingAddressView = this.addressWrappers[ADDRESS_TYPE.BILLING].getView(); + const shippingAddress = this.addressWrappers[ADDRESS_TYPE.SHIPPING]; + shippingAddress + .getView() + .getInputFields() + .forEach((inputFields) => { + const inputElement = inputFields.getView(); + const billingInput = billingAddressView + .getInputFields() + .find( + (inputField) => + inputField.getView().getInput().getHTML().placeholder === inputElement.getInput().getHTML().placeholder, + ); + if (billingInput) { + billingInput.getView().getInput().getHTML().value = inputElement.getInput().getHTML().value; + } + getStore().dispatch(setBillingCountry(getStore().getState().shippingCountry)); + }); + billingAddressView.switchVisibilityAddressWrapper(isChecked); this.switchSubmitFormButtonAccess(); return true; } private successfulUserRegistration(newUserData: User): void { - const userDataWithLogin = { - email: this.userData.email, - password: this.userData.password, - }; - this.eventMediator.notify(MEDIATOR_EVENT.USER_LOGIN, userDataWithLogin); + this.eventMediator.notify(MEDIATOR_EVENT.USER_LOGIN, this.getCredentialsData()); this.updateUserData(newUserData).catch(() => { serverMessageModel.showServerMessage(SERVER_MESSAGE.BAD_REQUEST, MESSAGE_STATUS.ERROR); }); - this.resetInputFieldsValidation(); } private switchSubmitFormButtonAccess(): boolean { - if (Object.values(this.isValidInputFields).every((value) => value)) { + if (this.inputFields.every((inputField) => inputField.getIsValid())) { this.view.getSubmitFormButton().setEnabled(); this.view.getSubmitFormButton().getHTML().focus(); } else { @@ -298,9 +256,9 @@ class RegisterFormModel { if (currentUserData) { currentUserData = await getCustomerModel().editCustomer( [ - CustomerModel.actionEditFirstName(this.userData.firstName), - CustomerModel.actionEditLastName(this.userData.lastName), - CustomerModel.actionEditDateOfBirth(this.userData.birthDate), + CustomerModel.actionEditFirstName(this.view.getFirstNameField().getView().getValue()), + CustomerModel.actionEditLastName(this.view.getLastNameField().getView().getValue()), + CustomerModel.actionEditDateOfBirth(this.view.getDateOfBirthField().getView().getValue()), ], currentUserData, ); @@ -310,59 +268,53 @@ class RegisterFormModel { } private async updateUserAddresses(userData: User | null): Promise { - const shippingAddress = this.getAddressData(CONSTANT_FORMS.USER_ADDRESS_TYPE.SHIPPING); - const billingAddress = this.getAddressData(CONSTANT_FORMS.USER_ADDRESS_TYPE.BILLING); - const checkboxSingleAddress = this.view.getSingleAddressCheckBox().getHTML(); - const checkboxDefaultShippingAddress = this.view.getCheckboxDefaultShippingAddress().getHTML(); - const checkboxDefaultBillingAddress = this.view.getCheckboxDefaultBillingAddress().getHTML(); + const { billing, shipping } = this.addressWrappers; + const personalData = this.getPersonalData(); + const checkboxSingleAddress = shipping.getView().getAddressAsBillingCheckBox()?.getHTML(); + const checkboxDefaultShippingAddress = shipping.getView().getAddressByDefaultCheckBox()?.getHTML(); + const checkboxDefaultBillingAddress = billing.getView().getAddressByDefaultCheckBox()?.getHTML(); + let currentUserData = userData; - currentUserData = await this.addAddress(shippingAddress, currentUserData); + currentUserData = await this.addAddress(shipping.getAddressData(personalData), currentUserData); + if (!currentUserData) { + return null; + } + const shippingAddressID = currentUserData.addresses[currentUserData.addresses.length - 1].id; - if (checkboxDefaultShippingAddress.checked && currentUserData) { - currentUserData = await this.editDefaultShippingAddress( - currentUserData.addresses[currentUserData.addresses.length - 1].id, - currentUserData, - ); + if (checkboxDefaultShippingAddress?.checked) { + currentUserData = await this.editDefaultShippingAddress(shippingAddressID, currentUserData); } - if (checkboxSingleAddress.checked && checkboxDefaultShippingAddress.checked) { - currentUserData = await this.addAddress(shippingAddress, currentUserData); - if (currentUserData) { - currentUserData = await this.editDefaultBillingAddress( - currentUserData.addresses[currentUserData.addresses.length - 1].id, - currentUserData, - ); - } + if (checkboxSingleAddress?.checked && checkboxDefaultShippingAddress?.checked) { + currentUserData = await this.editDefaultBillingAddress(shippingAddressID, currentUserData); return currentUserData; } - if (checkboxDefaultBillingAddress.checked) { - currentUserData = await this.addAddress(billingAddress, currentUserData); - if (currentUserData) { - currentUserData = await this.editDefaultBillingAddress( - currentUserData.addresses[currentUserData.addresses.length - 1].id, - currentUserData, - ); - } + currentUserData = await this.addAddress(billing.getAddressData(personalData), currentUserData); + if (!currentUserData) { + return null; + } + const billingAddressID = currentUserData.addresses[currentUserData.addresses.length - 1].id; + + if (checkboxDefaultBillingAddress?.checked) { + currentUserData = await this.editDefaultBillingAddress(billingAddressID, currentUserData); } return currentUserData; } - private async updateUserData(newUserData: User): Promise { + private async updateUserData(newUserData: User): Promise { let currentUserData: User | null = newUserData; currentUserData = await this.updatePersonalData(currentUserData); - if (currentUserData) { - this.userData = currentUserData; - } - currentUserData = await this.updateUserAddresses(currentUserData); - getStore().dispatch(setCurrentUser(this.userData)); - return this.userData; + getStore().dispatch(setCurrentUser(currentUserData)); + this.inputFields.forEach((inputField) => inputField.getView().getInput().clear()); + this.resetInputFieldsValidation(); + return currentUserData; } public getFirstInputField(): InputFieldModel { diff --git a/src/widgets/RegistrationForm/view/RegistrationFormView.ts b/src/widgets/RegistrationForm/view/RegistrationFormView.ts index 783b24f8..9c8e91a6 100644 --- a/src/widgets/RegistrationForm/view/RegistrationFormView.ts +++ b/src/widgets/RegistrationForm/view/RegistrationFormView.ts @@ -1,235 +1,128 @@ -import type { InputParams } from '@/shared/types/form'; - import InputFieldModel from '@/entities/InputField/model/InputFieldModel.ts'; import ButtonModel from '@/shared/Button/model/ButtonModel.ts'; import InputModel from '@/shared/Input/model/InputModel.ts'; import getStore from '@/shared/Store/Store.ts'; import { BUTTON_TEXT, BUTTON_TEXT_KEYS, BUTTON_TYPE } from '@/shared/constants/buttons.ts'; -import { FORM_TEXT, INPUT_TYPE } from '@/shared/constants/forms.ts'; +import { INPUT_TYPE } from '@/shared/constants/forms.ts'; import * as FORM_CONSTANT from '@/shared/constants/forms/register/constant.ts'; import * as FORM_FIELDS from '@/shared/constants/forms/register/fieldParams.ts'; import * as FORM_VALIDATION from '@/shared/constants/forms/register/validationParams.ts'; -import TAG_NAME from '@/shared/constants/tags.ts'; +import SVG_DETAILS from '@/shared/constants/svg.ts'; import createBaseElement from '@/shared/utils/createBaseElement.ts'; +import createSVGUse from '@/shared/utils/createSVGUse.ts'; import observeCurrentLanguage from '@/shared/utils/observeCurrentLanguage.ts'; import styles from './registrationForm.module.scss'; class RegistrationFormView { - private billingAddressWrapper: HTMLDivElement; - - private checkboxDefaultBillingAddress: InputModel; + private credentialsWrapper: HTMLDivElement; - private checkboxDefaultShippingAddress: InputModel; + private dateOfBirthField: InputFieldModel; - private checkboxSingleAddress: InputModel; + private emailField: InputFieldModel; - private credentialsWrapper: HTMLDivElement; + private firstNameField: InputFieldModel; private form: HTMLFormElement; private inputFields: InputFieldModel[] = []; + private lastNameField: InputFieldModel; + + private passwordField: InputFieldModel; + private personalDataWrapper: HTMLDivElement; - private shippingAddressWrapper: HTMLDivElement; + private showPasswordElement: HTMLDivElement; private submitFormButton: ButtonModel; constructor() { - this.inputFields = this.createInputFields(); + this.showPasswordElement = this.createShowPasswordElement(); + this.emailField = this.createEmailField(); + this.passwordField = this.createPasswordField(); + this.firstNameField = this.createFirstNameField(); + this.lastNameField = this.createLastNameField(); + this.dateOfBirthField = this.createDateOfBirthField(); this.credentialsWrapper = this.createCredentialsWrapper(); this.personalDataWrapper = this.createPersonalDataWrapper(); - this.checkboxSingleAddress = this.createCheckboxSingleAddress(); - this.checkboxDefaultShippingAddress = this.createCheckboxDefaultShippingAddress(); - this.shippingAddressWrapper = this.createShippingAddressWrapper(); - this.checkboxDefaultBillingAddress = this.createCheckboxDefaultBillingAddress(); - this.billingAddressWrapper = this.createBillingAddressWrapper(); this.submitFormButton = this.createSubmitFormButton(); this.form = this.createHTML(); } - private createBillingAddressWrapper(): HTMLDivElement { - const copyInputFields = this.inputFields; - const filteredInputFields = copyInputFields.filter( - (inputField) => - inputField.getView().getInput().getHTML().id === FORM_FIELDS.BILLING_ADDRESS_STREET.inputParams.id || - inputField.getView().getInput().getHTML().id === FORM_FIELDS.BILLING_ADDRESS_CITY.inputParams.id || - inputField.getView().getInput().getHTML().id === FORM_FIELDS.BILLING_ADDRESS_COUNTRY.inputParams.id || - inputField.getView().getInput().getHTML().id === FORM_FIELDS.BILLING_ADDRESS_POSTAL_CODE.inputParams.id, - ); - - this.billingAddressWrapper = this.createWrapperElement( - FORM_CONSTANT.TITLE_TEXT_KEYS.BILLING_ADDRESS, - [styles.billingAddressWrapper], - filteredInputFields, + private createCredentialsWrapper(): HTMLDivElement { + this.credentialsWrapper = this.createWrapperElement( + FORM_CONSTANT.TITLE_TEXT_KEYS.CREDENTIALS, + [styles.credentialsWrapper], + [this.emailField, this.passwordField], ); - const checkBoxLabel = createBaseElement({ - cssClasses: [styles.checkboxLabel], - tag: TAG_NAME.LABEL, - }); - - const checkBoxText = createBaseElement({ - cssClasses: [styles.checkboxText], - innerContent: FORM_TEXT.DEFAULT_ADDRESS, - tag: TAG_NAME.SPAN, - }); - - checkBoxLabel.append(checkBoxText, this.checkboxDefaultBillingAddress.getHTML()); - - this.billingAddressWrapper.append(checkBoxLabel); - - return this.billingAddressWrapper; - } - - private createCheckBoxLabel(innerContent: string, checkBoxElement: HTMLInputElement): HTMLLabelElement { - const checkboxLabel = createBaseElement({ - cssClasses: [styles.checkboxLabel], - tag: TAG_NAME.LABEL, - }); - - const checkBoxText = createBaseElement({ - cssClasses: [styles.checkboxText], - innerContent, - tag: TAG_NAME.SPAN, - }); - - checkboxLabel.append(checkBoxText, checkBoxElement); - return checkboxLabel; + return this.credentialsWrapper; } - private createCheckboxDefaultBillingAddress(): InputModel { - const checkboxParams: InputParams = { - autocomplete: FORM_FIELDS.CHECKBOX.AUTOCOMPLETE, - id: FORM_FIELDS.CHECKBOX.BILLING_ID, - placeholder: '', - type: INPUT_TYPE.CHECK_BOX, - }; - this.checkboxDefaultBillingAddress = new InputModel(checkboxParams); - return this.checkboxDefaultBillingAddress; + private createDateOfBirthField(): InputFieldModel { + this.dateOfBirthField = new InputFieldModel(FORM_FIELDS.BIRTHDAY, FORM_VALIDATION.BIRTHDAY_VALIDATE); + this.inputFields.push(this.dateOfBirthField); + return this.dateOfBirthField; } - private createCheckboxDefaultShippingAddress(): InputModel { - const checkboxParams: InputParams = { - autocomplete: FORM_FIELDS.CHECKBOX.AUTOCOMPLETE, - id: FORM_FIELDS.CHECKBOX.SHIPPING_ID, - placeholder: '', - type: INPUT_TYPE.CHECK_BOX, - }; - this.checkboxDefaultShippingAddress = new InputModel(checkboxParams); - return this.checkboxDefaultShippingAddress; + private createEmailField(): InputFieldModel { + this.emailField = new InputFieldModel(FORM_FIELDS.EMAIL, FORM_VALIDATION.EMAIL_VALIDATE); + this.inputFields.push(this.emailField); + return this.emailField; } - private createCheckboxSingleAddress(): InputModel { - const checkboxParams: InputParams = { - autocomplete: FORM_FIELDS.CHECKBOX.AUTOCOMPLETE, - id: FORM_FIELDS.CHECKBOX.SINGLE_ID, - placeholder: '', - type: INPUT_TYPE.CHECK_BOX, - }; - this.checkboxSingleAddress = new InputModel(checkboxParams); - - return this.checkboxSingleAddress; - } - - private createCredentialsWrapper(): HTMLDivElement { - const copyInputFields = this.inputFields; - const filteredInputFields = copyInputFields.filter( - (inputField) => - inputField.getView().getInput().getHTML().id === FORM_FIELDS.PASSWORD.inputParams.id || - inputField.getView().getInput().getHTML().id === FORM_FIELDS.EMAIL.inputParams.id, - ); - - this.credentialsWrapper = this.createWrapperElement( - FORM_CONSTANT.TITLE_TEXT_KEYS.CREDENTIALS, - [styles.credentialsWrapper], - filteredInputFields, - ); - - return this.credentialsWrapper; + private createFirstNameField(): InputFieldModel { + this.firstNameField = new InputFieldModel(FORM_FIELDS.FIRST_NAME, FORM_VALIDATION.FIRST_NAME_VALIDATE); + this.inputFields.push(this.firstNameField); + return this.firstNameField; } private createHTML(): HTMLFormElement { this.form = createBaseElement({ cssClasses: [styles.registrationForm], - tag: TAG_NAME.FORM, + tag: 'form', }); - this.form.append( - this.credentialsWrapper, - this.personalDataWrapper, - this.shippingAddressWrapper, - this.billingAddressWrapper, - this.submitFormButton.getHTML(), - ); + this.form.append(this.credentialsWrapper, this.personalDataWrapper, this.submitFormButton.getHTML()); return this.form; } - private createInputFields(): InputFieldModel[] { - FORM_FIELDS.INPUT.forEach((inputFieldParams) => { - const currentValidateParams = FORM_VALIDATION.INPUT_VALIDATION.find( - (validParams) => validParams.key === inputFieldParams.inputParams.id, - ); - - if (currentValidateParams) { - const inputField = new InputFieldModel(inputFieldParams, currentValidateParams); - this.inputFields.push(inputField); - } else { - this.inputFields.push(new InputFieldModel(inputFieldParams, null)); - } - }); + private createLastNameField(): InputFieldModel { + this.lastNameField = new InputFieldModel(FORM_FIELDS.LAST_NAME, FORM_VALIDATION.LAST_NAME_VALIDATE); + this.inputFields.push(this.lastNameField); + return this.lastNameField; + } - return this.inputFields; + private createPasswordField(): InputFieldModel { + this.passwordField = new InputFieldModel(FORM_FIELDS.PASSWORD, FORM_VALIDATION.PASSWORD_VALIDATE); + this.inputFields.push(this.passwordField); + const inputElement = this.passwordField.getView().getHTML(); + if (inputElement instanceof HTMLLabelElement) { + inputElement.append(this.showPasswordElement); + } + return this.passwordField; } private createPersonalDataWrapper(): HTMLDivElement { - const copyInputFields = this.inputFields; - const filteredInputFields = copyInputFields.filter( - (inputField) => - inputField.getView().getInput().getHTML().id === FORM_FIELDS.FIRST_NAME.inputParams.id || - inputField.getView().getInput().getHTML().id === FORM_FIELDS.LAST_NAME.inputParams.id || - inputField.getView().getInput().getHTML().id === FORM_FIELDS.BIRTHDAY.inputParams.id, - ); + const currentInputFields = [this.firstNameField, this.lastNameField, this.dateOfBirthField]; this.personalDataWrapper = this.createWrapperElement( FORM_CONSTANT.TITLE_TEXT_KEYS.PERSONAL, [styles.personalDataWrapper], - filteredInputFields, + currentInputFields, ); return this.personalDataWrapper; } - private createShippingAddressWrapper(): HTMLDivElement { - const copyInputFields = this.inputFields; - const filteredInputFields = copyInputFields.filter( - (inputField) => - inputField.getView().getInput().getHTML().id === FORM_FIELDS.SHIPPING_ADDRESS_STREET.inputParams.id || - inputField.getView().getInput().getHTML().id === FORM_FIELDS.SHIPPING_ADDRESS_CITY.inputParams.id || - inputField.getView().getInput().getHTML().id === FORM_FIELDS.SHIPPING_ADDRESS_COUNTRY.inputParams.id || - inputField.getView().getInput().getHTML().id === FORM_FIELDS.SHIPPING_ADDRESS_POSTAL_CODE.inputParams.id, - ); - - this.shippingAddressWrapper = this.createWrapperElement( - FORM_CONSTANT.TITLE_TEXT_KEYS.SHIPPING_ADDRESS, - [styles.shippingAddressWrapper], - filteredInputFields, - ); - - const settingsAddressWrapper = createBaseElement({ - cssClasses: [styles.settingsAddressWrapper], - tag: TAG_NAME.DIV, + private createShowPasswordElement(): HTMLDivElement { + this.showPasswordElement = createBaseElement({ + cssClasses: [styles.showPasswordElement], + tag: 'div', }); - - settingsAddressWrapper.append( - this.createCheckBoxLabel(FORM_TEXT.DEFAULT_ADDRESS, this.checkboxDefaultShippingAddress.getHTML()), - this.createCheckBoxLabel(FORM_TEXT.SINGLE_ADDRESS, this.checkboxSingleAddress.getHTML()), - ); - - this.shippingAddressWrapper.append(settingsAddressWrapper); - - return this.shippingAddressWrapper; + this.switchPasswordElementSVG(INPUT_TYPE.PASSWORD); + return this.showPasswordElement; } private createSubmitFormButton(): ButtonModel { @@ -256,12 +149,12 @@ class RegistrationFormView { const { currentLanguage } = getStore().getState(); const wrapperElement = createBaseElement({ cssClasses, - tag: TAG_NAME.DIV, + tag: 'div', }); const titleElement = createBaseElement({ cssClasses: [styles.title], innerContent: FORM_CONSTANT.TITLE_TEXT[currentLanguage][title], - tag: TAG_NAME.H3, + tag: 'h3', }); wrapperElement.append(titleElement); observeCurrentLanguage(titleElement, FORM_CONSTANT.TITLE_TEXT, title); @@ -269,6 +162,7 @@ class RegistrationFormView { inputFields.forEach((inputField) => { const inputFieldElement = inputField.getView().getHTML(); if (inputFieldElement instanceof HTMLLabelElement) { + inputFieldElement.classList.add(styles.label); wrapperElement.append(inputFieldElement); } else if (inputFieldElement instanceof InputModel) { wrapperElement.append(inputFieldElement.getHTML()); @@ -277,16 +171,16 @@ class RegistrationFormView { return wrapperElement; } - public getBillingAddressWrapper(): HTMLDivElement { - return this.billingAddressWrapper; + public getDateOfBirthField(): InputFieldModel { + return this.dateOfBirthField; } - public getCheckboxDefaultBillingAddress(): InputModel { - return this.checkboxDefaultBillingAddress; + public getEmailField(): InputFieldModel { + return this.emailField; } - public getCheckboxDefaultShippingAddress(): InputModel { - return this.checkboxDefaultShippingAddress; + public getFirstNameField(): InputFieldModel { + return this.firstNameField; } public getHTML(): HTMLFormElement { @@ -297,20 +191,28 @@ class RegistrationFormView { return this.inputFields; } - public getShippingAddressWrapper(): HTMLDivElement { - return this.shippingAddressWrapper; + public getLastNameField(): InputFieldModel { + return this.lastNameField; + } + + public getPasswordField(): InputFieldModel { + return this.passwordField; } - public getSingleAddressCheckBox(): InputModel { - return this.checkboxSingleAddress; + public getShowPasswordElement(): HTMLDivElement { + return this.showPasswordElement; } public getSubmitFormButton(): ButtonModel { return this.submitFormButton; } - public switchVisibilityBillingAddressWrapper(isVisible: boolean): void { - this.billingAddressWrapper.classList.toggle(styles.hidden, isVisible); + public switchPasswordElementSVG(type: string): SVGSVGElement { + const svg = document.createElementNS(SVG_DETAILS.SVG_URL, 'svg'); + this.showPasswordElement.innerHTML = ''; + svg.append(createSVGUse(type === INPUT_TYPE.PASSWORD ? SVG_DETAILS.CLOSE_EYE : SVG_DETAILS.OPEN_EYE)); + this.showPasswordElement.append(svg); + return svg; } } diff --git a/src/widgets/RegistrationForm/view/registrationForm.module.scss b/src/widgets/RegistrationForm/view/registrationForm.module.scss index cd0fe977..47c086d3 100644 --- a/src/widgets/RegistrationForm/view/registrationForm.module.scss +++ b/src/widgets/RegistrationForm/view/registrationForm.module.scss @@ -7,36 +7,7 @@ width: 100%; gap: var(--extra-small-offset); - .checkboxLabel { - display: flex; - flex-direction: row; - align-items: center; - justify-content: end; - cursor: pointer; - user-select: none; - gap: calc(var(--extra-small-offset) / 4); - - input { - cursor: pointer; - } - - @media (hover: hover) { - &:hover { - .checkboxText { - color: var(--steam-green-800); - } - } - } - } - - .checkboxText { - font: var(--regular-font); - letter-spacing: 1px; - color: var(--noble-gray-800); - transition: color 0.2s; - } - - label { + .label { position: relative; display: flex; flex-direction: column; @@ -44,48 +15,46 @@ letter-spacing: 1px; color: var(--noble-gray-800); gap: calc(var(--extra-small-offset) / 2); + } - button { - position: absolute; - right: 0; - top: 34px; - border-radius: var(--small-br); - padding: calc(var(--extra-small-offset) / 8); - width: 24px; - height: 24px; - background-color: transparent; - transform: translate(-12%, 0); + .showPasswordElement { + position: absolute; + right: 0; + top: 34px; + border-radius: var(--small-br); + padding: calc(var(--extra-small-offset) / 8); + width: 24px; + height: 24px; + background-color: transparent; + transform: translate(-12%, 0); + + svg { + width: 20px; + height: 20px; + stroke: var(--noble-gray-600); + transition: stroke 0.2s; + } + + &:focus { + background-color: var(--noble-gray-200); svg { - width: 20px; - height: 20px; - stroke: var(--noble-gray-600); - transition: stroke 0.2s; + stroke: var(--steam-green-800); } + } - &:focus { + @media (hover: hover) { + &:hover { background-color: var(--noble-gray-200); svg { stroke: var(--steam-green-800); } } - - @media (hover: hover) { - &:hover { - background-color: var(--noble-gray-200); - - svg { - stroke: var(--steam-green-800); - } - } - } } @media (max-width: 768px) { - button { - top: 24px; - } + top: 24px; } } @@ -140,21 +109,16 @@ } } - span { - font: var(--regular-font); - letter-spacing: 1px; - text-align: center; - color: var(--red-power-600); - } - button { display: flex; align-items: center; justify-content: center; + justify-self: center; grid-column: 2 span; grid-row: 5; border-radius: var(--small-br); padding: calc(var(--extra-small-offset) / 2) var(--small-offset); + width: max-content; max-height: 40px; font: var(--bold-font); letter-spacing: 1px; @@ -186,25 +150,34 @@ } .credentialsWrapper, -.personalDataWrapper, -.shippingAddressWrapper, -.billingAddressWrapper { +.personalDataWrapper { display: grid; grid-template-columns: repeat(2, 1fr); - border: 2px solid var(--noble-gray-200); - border-right: 0; - border-left: 0; - border-radius: var(--small-br); padding: var(--extra-small-offset); - gap: var(--extra-small-offset); + gap: calc(var(--extra-small-offset) * 1.5) var(--extra-small-offset); .title { + position: relative; + justify-self: center; grid-column: 2 span; + width: max-content; font: var(--medium-font); letter-spacing: 1px; text-align: center; color: var(--noble-gray-500); + &::after { + content: ''; + position: absolute; + left: 50%; + bottom: -10px; + border-radius: var(--medium-br); + width: 100%; + height: 2px; + background-color: var(--steam-green-800); + transform: translateX(-50%); + } + @media (max-width: 768px) { grid-column: 1 span; } @@ -226,44 +199,6 @@ grid-row: 2; } -.shippingAddressWrapper { - grid-column: 2 span; - grid-row: 3; -} - -.billingAddressWrapper { - grid-column: 2 span; - grid-row: 4; - transition: transform 0.2s; - animation: show 0.5s ease-in forwards; -} - -.hidden { - animation: hide 0.5s ease-in forwards; -} - -@keyframes hide { - 0% { - transform: scale(1); - } - - 100% { - display: none; - transform: scale(0); - } -} - -@keyframes show { - 0% { - display: grid; - transform: scale(0); - } - - 100% { - transform: scale(1); - } -} - .settingsAddressWrapper { display: flex; flex-direction: column; diff --git a/tsconfig.json b/tsconfig.json index 6a81bb46..e6d0350c 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -24,20 +24,7 @@ /* Path aliases */ "baseUrl": "", "paths": { - "@/*": ["src/*"], - "@app/*": ["src/app/*"], - "@entities/*": ["src/entities/*"], - "@features/*": ["src/features/*"], - "@pages/*": ["src/pages/*"], - "@widgets/*": ["src/widgets/*"], - "@shared/*": ["src/shared/*"], - "@utils/*": ["src/shared/utils/*"], - "@constants/*": ["src/shared/constants/*"], - "@API/*": ["src/shared/api/*"], - "@EventMediator/*": ["src/shared/EventMediator/*"], - "@img/*": ["src/shared/img/*"], - "@Store/*": ["src/shared/Store/*"], - "@types/*": ["src/shared/types/*"] + "@/*": ["src/*"] }, /* Testing */