diff --git a/.env b/.env index c5b0643c..c9511bb3 100644 --- a/.env +++ b/.env @@ -1,11 +1,10 @@ - VITE_APP_CTP_PROJECT_KEY=green-shop -VITE_APP_CTP_CLIENT_SECRET=wzD7hatuZ8giFcGL5h6htyGtNlAl1chR -VITE_APP_CTP_CLIENT_ID=LG1VRj2-lAjAdT3Wa0m9AXou +VITE_APP_CTP_CLIENT_SECRET=Nlpz1PodM9gnmEPEwHw91jhXv77qU4ww +VITE_APP_CTP_CLIENT_ID=BPJMQ0wyC-4dA_1sd04SlJQx VITE_APP_CTP_REGION=europe-west1 VITE_APP_CTP_AUTH_URL=https://auth.europe-west1.gcp.commercetools.com VITE_APP_CTP_API_URL=https://api.europe-west1.gcp.commercetools.com -VITE_APP_CTP_SCOPES=manage_orders:green-shop manage_payments:green-shop manage_types:green-shop view_shopping_lists:green-shop manage_customers:green-shop manage_my_orders:green-shop view_orders:green-shop manage_quotes:green-shop view_discount_codes:green-shop view_quote_requests:green-shop manage_order_edits:green-shop manage_products:green-shop manage_quote_requests:green-shop view_quotes:green-shop view_api_clients:green-shop view_order_edits:green-shop manage_shipping_methods:green-shop manage_cart_discounts:green-shop manage_my_shopping_lists:green-shop view_products:green-shop view_categories:green-shop manage_my_payments:green-shop manage_my_profile:green-shop manage_discount_codes:green-shop manage_categories:green-shop manage_shopping_lists:green-shop manage_extensions:green-shop +VITE_APP_CTP_SCOPES=manage_products:green-shop manage_sessions:green-shop manage_my_quotes:green-shop manage_customer_groups:green-shop manage_payments:green-shop manage_checkout_payment_intents:green-shop manage_api_clients:green-shop manage_order_edits:green-shop manage_project:green-shop manage_orders:green-shop create_anonymous_token:green-shop manage_business_units:green-shop manage_associate_roles:green-shop manage_product_selections:green-shop view_api_clients:green-shop view_audit_log:green-shop manage_cart_discounts:green-shop manage_my_shopping_lists:green-shop manage_connectors:green-shop manage_customers:green-shop introspect_oauth_tokens:green-shop manage_my_orders:green-shop manage_my_payments:green-shop manage_my_profile:green-shop manage_connectors_deployments:green-shop manage_audit_log:green-shop manage_attribute_groups:green-shop manage_my_quote_requests:green-shop manage_discount_codes:green-shop manage_categories:green-shop manage_extensions:green-shop manage_my_business_units:green-shop manage_import_containers:green-shop VITE_APP_DEFAULT_SEGMENT='/' VITE_APP_NEXT_SEGMENT=1 VITE_APP_PATH_SEGMENTS_TO_KEEP=0 diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 13d03bf4..8e1c0deb 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -6,7 +6,7 @@ - [ ] 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` +👀 Example: `feat(RSS-ECOMM-3_01): description` ## PR Description 🧙‍♂️ diff --git a/.validate-branch-namerc.cjs b/.validate-branch-namerc.cjs index 3b33abf1..c00c295e 100644 --- a/.validate-branch-namerc.cjs +++ b/.validate-branch-namerc.cjs @@ -1,5 +1,6 @@ module.exports = { - pattern: /^(feat|fix|hotfix|chore|refactor|revert|docs|style|test|)\(RSS-ECOMM-\d{1}_\d{2}\)\/[a-z]+[a-zA-Z0-9]*$/, + pattern: + /^sprint-3|^(feat|fix|hotfix|chore|refactor|revert|docs|style|test|)\(RSS-ECOMM-\d{1}_\d{2}\)\/[a-z]+[a-zA-Z0-9]*$/, errorMsg: 'Please use correct branch name', }; diff --git a/package.json b/package.json index 1b0a7210..204f3752 100644 --- a/package.json +++ b/package.json @@ -63,8 +63,10 @@ "@commercetools/sdk-client-v2": "^2.5.0", "@commercetools/sdk-middleware-auth": "^7.0.1", "@commercetools/sdk-middleware-http": "^7.0.4", + "@types/js-cookie": "^3.0.6", "autoprefixer": "^10.4.19", "isomorphic-fetch": "^3.0.0", + "js-cookie": "^3.0.5", "materialize-css": "^1.0.0-rc.2", "modern-normalize": "^2.0.0", "postcode-validator": "^3.8.20", diff --git a/src/app/App/model/AppModel.ts b/src/app/App/model/AppModel.ts index cb2a7014..5afc6d2a 100644 --- a/src/app/App/model/AppModel.ts +++ b/src/app/App/model/AppModel.ts @@ -1,10 +1,7 @@ +/* eslint-disable max-lines-per-function */ import type { Page } from '@/shared/types/common.ts'; import RouterModel from '@/app/Router/model/RouterModel.ts'; -import LoginPageModel from '@/pages/LoginPage/model/LoginPageModel.ts'; -import MainPageModel from '@/pages/MainPage/model/MainPageModel.ts'; -import NotFoundPageModel from '@/pages/NotFoundPage/model/NotFoundPageModel.ts'; -import RegistrationPageModel from '@/pages/RegistrationPage/model/RegistrationPageModel.ts'; import { PAGE_ID } from '@/shared/constants/pages.ts'; import FooterModel from '@/widgets/Footer/model/FooterModel.ts'; import HeaderModel from '@/widgets/Header/model/HeaderModel.ts'; @@ -17,31 +14,79 @@ class AppModel { private router = new RouterModel(); constructor() { - this.router.setPages(this.initPages()); + this.initialize() + .then() + .catch(() => { + throw new Error('AppModel initialization error'); + }); } - private initPages(): Map { - const root = this.getHTML(); - root.append(new HeaderModel(this.router).getHTML()); - const loginPage = new LoginPageModel(root, this.router); - const mainPage = new MainPageModel(root, this.router); - const registrationPage = new RegistrationPageModel(root, this.router); - const notFoundPage = new NotFoundPageModel(root, this.router); - const pages: Map = new Map( - Object.entries({ - [PAGE_ID.DEFAULT_PAGE]: mainPage, - [PAGE_ID.LOGIN_PAGE]: loginPage, - [PAGE_ID.MAIN_PAGE]: mainPage, - [PAGE_ID.NOT_FOUND_PAGE]: notFoundPage, - [PAGE_ID.REGISTRATION_PAGE]: registrationPage, - }), - ); - root.append(new FooterModel(this.router).getHTML()); - return pages; + private createRoutes(): Promise Promise>> { + const routesMap = { + [PAGE_ID.ABOUT_US_PAGE]: async (): Promise => { + const { default: AboutUsPageModel } = await import('@/pages/AboutUsPage/model/AboutUsPageModel.ts'); + return new AboutUsPageModel(this.appView.getHTML()); + }, + [PAGE_ID.CART_PAGE]: async (): Promise => { + const { default: CartPageModel } = await import('@/pages/CartPage/model/CartPageModel.ts'); + return new CartPageModel(this.appView.getHTML()); + }, + [PAGE_ID.CATALOG_PAGE]: async (): Promise => { + const { default: CatalogPageModel } = await import('@/pages/CatalogPage/model/CatalogPageModel.ts'); + return new CatalogPageModel(this.appView.getHTML()); + }, + [PAGE_ID.DEFAULT_PAGE]: async (): Promise => { + const { default: MainPageModel } = await import('@/pages/MainPage/model/MainPageModel.ts'); + return new MainPageModel(this.appView.getHTML(), this.router); + }, + [PAGE_ID.ITEM_PAGE]: async (): Promise => { + const { default: ItemPageModel } = await import('@/pages/ItemPage/model/ItemPageModel.ts'); + return new ItemPageModel(this.appView.getHTML()); + }, + [PAGE_ID.LOGIN_PAGE]: async (): Promise => { + const { default: LoginPageModel } = await import('@/pages/LoginPage/model/LoginPageModel.ts'); + return new LoginPageModel(this.appView.getHTML(), this.router); + }, + [PAGE_ID.MAIN_PAGE]: async (): Promise => { + const { default: MainPageModel } = await import('@/pages/MainPage/model/MainPageModel.ts'); + return new MainPageModel(this.appView.getHTML(), this.router); + }, + [PAGE_ID.NOT_FOUND_PAGE]: async (): Promise => { + const { default: NotFoundPageModel } = await import('@/pages/NotFoundPage/model/NotFoundPageModel.ts'); + return new NotFoundPageModel(this.appView.getHTML(), this.router); + }, + [PAGE_ID.REGISTRATION_PAGE]: async (): Promise => { + const { default: RegistrationPageModel } = await import( + '@/pages/RegistrationPage/model/RegistrationPageModel.ts' + ); + return new RegistrationPageModel(this.appView.getHTML(), this.router); + }, + [PAGE_ID.USER_PROFILE_PAGE]: async (): Promise => { + const { default: UserProfilePageModel } = await import('@/pages/UserProfilePage/model/UserProfilePageModel.ts'); + return new UserProfilePageModel(this.appView.getHTML()); + }, + }; + + const routes = new Map Promise>(); + Object.entries(routesMap).forEach(([key, value]) => routes.set(key, value)); + + return Promise.resolve(routes); + } + + private async initialize(): Promise { + document.body.append(this.appView.getHTML()); + this.appView.getHTML().insertAdjacentElement('beforebegin', new HeaderModel(this.router).getHTML()); + this.appView.getHTML().insertAdjacentElement('afterend', new FooterModel(this.router).getHTML()); + + const routes = await this.createRoutes(); + this.router.setRoutes(routes); } - public getHTML(): HTMLDivElement { - return this.appView.getHTML(); + public start(): boolean { + if (!this.appView.getHTML()) { + return false; + } + return true; } } diff --git a/src/app/App/tests/App.spec.ts b/src/app/App/tests/App.spec.ts index 4ddd25c6..af57744d 100644 --- a/src/app/App/tests/App.spec.ts +++ b/src/app/App/tests/App.spec.ts @@ -3,7 +3,7 @@ import AppModel from '../model/AppModel.ts'; const app = new AppModel(); describe('Checking AppModel class', () => { - it('the getHTML method should return HTMLDivElement', () => { - expect(app.getHTML()).toBeInstanceOf(HTMLDivElement); + it('application successfully created', () => { + expect(app.start()).toBe(true); }); }); diff --git a/src/app/App/view/appView.module.scss b/src/app/App/view/appView.module.scss index f4a6d30e..fc88aeff 100644 --- a/src/app/App/view/appView.module.scss +++ b/src/app/App/view/appView.module.scss @@ -2,9 +2,10 @@ position: relative; display: flex; flex-direction: column; - justify-content: space-between; + justify-content: center; margin: 0 auto; + padding: 60px 0; width: 100%; - min-height: 100vh; + min-height: calc(100vh - 141px); max-width: 1440px; } diff --git a/src/app/Router/model/RouterModel.ts b/src/app/Router/model/RouterModel.ts index 5fe29a95..1095a859 100644 --- a/src/app/Router/model/RouterModel.ts +++ b/src/app/Router/model/RouterModel.ts @@ -1,7 +1,5 @@ import type { Page } from '@/shared/types/common.ts'; -import EventMediatorModel from '@/shared/EventMediator/model/EventMediatorModel.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; @@ -10,53 +8,46 @@ const PATH_SEGMENTS_TO_KEEP = import.meta.env.VITE_APP_PATH_SEGMENTS_TO_KEEP; const PROJECT_TITLE = import.meta.env.VITE_APP_PROJECT_TITLE; class RouterModel { - private eventMediator = EventMediatorModel.getInstance(); - - private pages: Map = new Map(); + private routes: Map Promise> = new Map(); constructor() { - document.addEventListener('DOMContentLoaded', () => { + document.addEventListener('DOMContentLoaded', async () => { const currentPath = window.location.pathname .split(DEFAULT_SEGMENT) .slice(PATH_SEGMENTS_TO_KEEP + NEXT_SEGMENT) .join(DEFAULT_SEGMENT); - this.handleRequest(currentPath); + await this.navigateTo(currentPath); }); } - private handleRequest(path: string): null | string { - const pathParts = path.split(DEFAULT_SEGMENT); - const hasRoute = this.pages.has(pathParts.join('')); - if (!hasRoute) { - document.title = `${PROJECT_TITLE} | ${PAGE_ID.NOT_FOUND_PAGE}`; - this.eventMediator.notify(MEDIATOR_EVENT.CHANGE_PAGE, PAGE_ID.NOT_FOUND_PAGE); - return null; - } - document.title = `${PROJECT_TITLE} | ${pathParts.join('')}`; - this.eventMediator.notify(MEDIATOR_EVENT.CHANGE_PAGE, pathParts.join('')); - return pathParts.join(''); + private changeAppTitle(path: string, hasRoute: boolean): void { + const title = `${PROJECT_TITLE} | ${hasRoute ? path : PAGE_ID.NOT_FOUND_PAGE}`; + document.title = title; } - public navigateTo(route: string): History { - if (this.pages.has(route)) { - const pathnameApp = window.location.pathname - .split(DEFAULT_SEGMENT) - .slice(NEXT_SEGMENT, PATH_SEGMENTS_TO_KEEP + NEXT_SEGMENT) - .join(DEFAULT_SEGMENT); - const url = `${pathnameApp}/${route}`; - const titleRoute = route === '' ? PAGE_ID.MAIN_PAGE : route; - document.title = `${PROJECT_TITLE} | ${titleRoute}`; + public async navigateTo(path: string): Promise { + const pathnameApp = window.location.pathname + .split(DEFAULT_SEGMENT) + .slice(NEXT_SEGMENT, PATH_SEGMENTS_TO_KEEP + NEXT_SEGMENT) + .join(DEFAULT_SEGMENT); + const url = `${pathnameApp}/${path}`; + history.pushState(path, '', url); - history.pushState(route, '', url); + const pathParts = url.split(DEFAULT_SEGMENT); + const hasRoute = this.routes.has(pathParts[1]); + this.changeAppTitle(pathParts[1], hasRoute); - this.eventMediator.notify(MEDIATOR_EVENT.CHANGE_PAGE, route); + if (!hasRoute) { + await this.routes.get(PAGE_ID.NOT_FOUND_PAGE)?.(); + return; } - return window.history; + + await this.routes.get(pathParts[1])?.(); } - public setPages(pages: Map): Map { - this.pages = pages; - return this.pages; + public setRoutes(routes: Map Promise>): Map Promise> { + this.routes = routes; + return this.routes; } } diff --git a/src/app/styles/common.scss b/src/app/styles/common.scss index 0fb605e7..8d42555c 100644 --- a/src/app/styles/common.scss +++ b/src/app/styles/common.scss @@ -5,6 +5,8 @@ html, body { display: flex; + flex-direction: column; + align-items: center; margin: 0 auto; width: 100%; min-width: 320px; diff --git a/src/app/styles/variables.scss b/src/app/styles/variables.scss index 2d710107..a1a72362 100644 --- a/src/app/styles/variables.scss +++ b/src/app/styles/variables.scss @@ -23,6 +23,7 @@ --noble-gray-700: #727272; --noble-gray-800: #3d3d3d; --red-power-600: #d0302f; + --steam-green-300: #46a35880; --steam-green-400: #c8f4b4; --steam-green-500: #b6f09c; --steam-green-700: #70d27a; @@ -47,5 +48,6 @@ --small-br: 5px; --medium-br: 5px; --large-br: 10px; + --regular-font: 400 10px 'Cerapro', sans-serif; } } diff --git a/src/entities/Address/model/AddressModel.ts b/src/entities/Address/model/AddressModel.ts index 0a46ac44..6391c0a5 100644 --- a/src/entities/Address/model/AddressModel.ts +++ b/src/entities/Address/model/AddressModel.ts @@ -3,6 +3,7 @@ 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 formattedText from '@/shared/utils/formattedText.ts'; import AddressView from '../view/AddressView.ts'; @@ -25,7 +26,7 @@ class AddressModel { public getAddressData(personalData: PersonalData): Address { const store = getStore().getState(); const addressData: Address = { - city: this.view.getCityField().getView().getValue(), + city: formattedText(this.view.getCityField().getView().getValue()), country: this.addressType === ADDRESS_TYPE.BILLING ? store.billingCountry : store.shippingCountry, email: personalData.email, firstName: personalData.firstName, @@ -33,7 +34,7 @@ class AddressModel { lastName: personalData.lastName, postalCode: this.view.getPostalCodeField().getView().getValue(), state: '', - streetName: this.view.getStreetField().getView().getValue(), + streetName: formattedText(this.view.getStreetField().getView().getValue()), streetNumber: '', }; return addressData; diff --git a/src/entities/Address/view/AddressView.ts b/src/entities/Address/view/AddressView.ts index 85c1e353..53644708 100644 --- a/src/entities/Address/view/AddressView.ts +++ b/src/entities/Address/view/AddressView.ts @@ -1,11 +1,13 @@ 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 getStore from '@/shared/Store/Store.ts'; +import { FORM_TEXT, FORM_TEXT_KEYS, INPUT_TYPE } from '@/shared/constants/forms.ts'; +import { TITLE_TEXT, TITLE_TEXT_KEYS } 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 observeCurrentLanguage from '@/shared/utils/observeCurrentLanguage.ts'; import styles from './addressView.module.scss'; @@ -43,8 +45,10 @@ class AddressView { private appendInputFields(): void { this.inputFields.forEach((inputField) => { const inputFieldElement = inputField.getView().getHTML(); + const inputHTML = inputField.getView().getInput().getHTML(); if (inputFieldElement instanceof HTMLLabelElement) { inputFieldElement.classList.add(styles.label); + inputHTML.classList.add(styles.input); this.address.append(inputFieldElement); } else if (inputFieldElement instanceof InputModel) { this.address.append(inputFieldElement.getHTML()); @@ -52,7 +56,7 @@ class AddressView { }); } - private createAddressAsBillingCheckbox(innerContent: string): HTMLLabelElement { + private createAddressAsBillingCheckbox(): HTMLLabelElement { const checkboxLabel = createBaseElement({ attributes: { for: SINGLE_ADDRESS, @@ -63,9 +67,10 @@ class AddressView { const checkBoxText = createBaseElement({ cssClasses: [styles.checkboxText], - innerContent, + innerContent: FORM_TEXT[getStore().getState().currentLanguage].SINGLE_ADDRESS, tag: 'span', }); + observeCurrentLanguage(checkBoxText, FORM_TEXT, FORM_TEXT_KEYS.SINGLE_ADDRESS); this.addressAsBillingCheckBox = new InputModel({ autocomplete: FORM_FIELDS.CHECKBOX.AUTOCOMPLETE, @@ -79,7 +84,7 @@ class AddressView { return checkboxLabel; } - private createAddressByDefaultCheckbox(innerContent: string): HTMLLabelElement { + private createAddressByDefaultCheckbox(): HTMLLabelElement { const checkboxLabel = createBaseElement({ attributes: { for: this.addressType === ADDRESS_TYPE.SHIPPING ? ADDRESS_TYPE.SHIPPING : ADDRESS_TYPE.BILLING, @@ -88,11 +93,22 @@ class AddressView { tag: 'label', }); + const textContent = + this.addressType === ADDRESS_TYPE.SHIPPING + ? FORM_TEXT[getStore().getState().currentLanguage].DEFAULT_SHIPPING_ADDRESS + : FORM_TEXT[getStore().getState().currentLanguage].DEFAULT_BILLING_ADDRESS; const checkBoxText = createBaseElement({ cssClasses: [styles.checkboxText], - innerContent, + innerContent: textContent, tag: 'span', }); + observeCurrentLanguage( + checkBoxText, + FORM_TEXT, + this.addressType === ADDRESS_TYPE.SHIPPING + ? FORM_TEXT_KEYS.DEFAULT_SHIPPING_ADDRESS + : FORM_TEXT_KEYS.DEFAULT_BILLING_ADDRESS, + ); this.addressByDefaultCheckBox = new InputModel({ autocomplete: FORM_FIELDS.CHECKBOX.AUTOCOMPLETE, @@ -156,10 +172,10 @@ class AddressView { this.appendInputFields(); if (this.options.setDefault) { - this.address.append(this.createAddressByDefaultCheckbox(FORM_TEXT.DEFAULT_ADDRESS)); + this.address.append(this.createAddressByDefaultCheckbox()); } if (this.options.setAsBilling) { - this.address.append(this.createAddressAsBillingCheckbox(FORM_TEXT.SINGLE_ADDRESS)); + this.address.append(this.createAddressAsBillingCheckbox()); } return this.address; @@ -202,12 +218,20 @@ class AddressView { } private createTitle(): HTMLHeadingElement { - return createBaseElement({ + const title = createBaseElement({ cssClasses: [styles.title], innerContent: - this.addressType === ADDRESS_TYPE.SHIPPING ? TITLE_TEXT.en.SHIPPING_ADDRESS : TITLE_TEXT.en.BILLING_ADDRESS, + this.addressType === ADDRESS_TYPE.SHIPPING + ? TITLE_TEXT[getStore().getState().currentLanguage].SHIPPING_ADDRESS + : TITLE_TEXT[getStore().getState().currentLanguage].BILLING_ADDRESS, tag: 'h3', }); + observeCurrentLanguage( + title, + TITLE_TEXT, + this.addressType === ADDRESS_TYPE.SHIPPING ? TITLE_TEXT_KEYS.SHIPPING_ADDRESS : TITLE_TEXT_KEYS.BILLING_ADDRESS, + ); + return title; } public getAddressAsBillingCheckBox(): InputModel | null { diff --git a/src/entities/Address/view/addressView.module.scss b/src/entities/Address/view/addressView.module.scss index 0a5ec29f..198bf550 100644 --- a/src/entities/Address/view/addressView.module.scss +++ b/src/entities/Address/view/addressView.module.scss @@ -111,6 +111,7 @@ .checkboxText { font: var(--regular-font); letter-spacing: 1px; + text-align: end; color: var(--noble-gray-800); transition: color 0.2s; } @@ -124,3 +125,54 @@ color: var(--noble-gray-800); gap: calc(var(--extra-small-offset) / 2); } + +.input { + border: 1px solid var(--noble-gray-200); + border-radius: var(--small-br); + padding: calc(var(--extra-small-offset) / 2) var(--extra-small-offset); + font: var(--regular-font); + letter-spacing: 1px; + color: var(--noble-gray-800); + transition: border 0.2s; + cursor: text; + + &::placeholder { + color: var(--noble-gray-600); + } + + &:focus { + border: 1px solid var(--steam-green-800); + } + + @media (hover: hover) { + &:hover { + border: 1px solid var(--steam-green-800); + } + } + + &[type='date'] { + -webkit-appearance: none; + appearance: none; + padding-right: 0; + } + + &[type='date']::-webkit-calendar-picker-indicator { + background-image: url('../../../shared/img/svg/calendar.svg'); + background-position: -3px 0; + cursor: pointer; + } + + &[type='date']::-webkit-inner-spin-button, + &[type='date']::-webkit-outer-spin-button { + display: none; + } + + &::-webkit-datetime-edit-day-field:focus, + &::-webkit-datetime-edit-month-field:focus, + &::-webkit-datetime-edit-year-field:focus { + outline: none; + border-radius: var(--small-br); + color: var(--steam-green-800); + background-color: var(--noble-gray-200); + } +} diff --git a/src/entities/InputField/view/InputFieldView.ts b/src/entities/InputField/view/InputFieldView.ts index 82659dfd..58c9329c 100644 --- a/src/entities/InputField/view/InputFieldView.ts +++ b/src/entities/InputField/view/InputFieldView.ts @@ -1,6 +1,8 @@ import type { InputFieldParams, InputParams, LabelParams } from '@/shared/types/form'; import InputModel from '@/shared/Input/model/InputModel.ts'; +import getStore from '@/shared/Store/Store.ts'; +import observeStore, { selectCurrentLanguage } from '@/shared/Store/observer.ts'; import createBaseElement from '@/shared/utils/createBaseElement.ts'; import styles from './inputFieldView.module.scss'; @@ -14,6 +16,8 @@ class InputFieldView { private label: HTMLLabelElement | null = null; + private labelText: HTMLSpanElement | null = null; + constructor(params: InputFieldParams) { this.input = this.createInput(params.inputParams); this.inputField = this.createHTML(params); @@ -32,7 +36,8 @@ class InputFieldView { if (labelParams) { this.inputField = this.createLabel(labelParams); this.errorField = this.createErrorField(); - this.label?.append(this.input.getHTML(), this.errorField); + this.labelText = this.createLabelText(labelParams); + this.label?.append(this.labelText, this.input.getHTML(), this.errorField); } else { this.inputField = this.input; } @@ -46,18 +51,36 @@ class InputFieldView { } private createLabel(labelParams: LabelParams): HTMLLabelElement { - const { for: htmlFor, text } = labelParams; + const { for: htmlFor } = labelParams; + this.label = createBaseElement({ attributes: { for: htmlFor, }, - innerContent: text || '', tag: 'label', }); return this.label; } + private createLabelText(labelParams: LabelParams): HTMLSpanElement { + const labelText = createBaseElement({ + cssClasses: [styles.labelText], + tag: 'span', + }); + + const updateLabelText = (): void => { + if (labelParams?.text) { + labelText.textContent = labelParams.text[getStore().getState().currentLanguage]; + } + }; + + updateLabelText(); + + observeStore(selectCurrentLanguage, updateLabelText); + return labelText; + } + public getErrorField(): HTMLSpanElement | null { return this.errorField; } diff --git a/src/entities/Navigation/model/NavigationModel.ts b/src/entities/Navigation/model/NavigationModel.ts index 30249641..a448c25f 100644 --- a/src/entities/Navigation/model/NavigationModel.ts +++ b/src/entities/Navigation/model/NavigationModel.ts @@ -1,16 +1,14 @@ import type RouterModel from '@/app/Router/model/RouterModel'; -import EventMediatorModel from '@/shared/EventMediator/model/EventMediatorModel.ts'; +import serverMessageModel from '@/shared/ServerMessage/model/ServerMessageModel.ts'; import getStore from '@/shared/Store/Store.ts'; -import observeStore, { selectCurrentUser } from '@/shared/Store/observer.ts'; -import MEDIATOR_EVENT from '@/shared/constants/events.ts'; +import observeStore, { selectCurrentPage, selectCurrentUser } from '@/shared/Store/observer.ts'; +import { MESSAGE_STATUS, SERVER_MESSAGE } from '@/shared/constants/messages.ts'; import { PAGE_ID } from '@/shared/constants/pages.ts'; import NavigationView from '../view/NavigationView.ts'; class NavigationModel { - private eventMediator = EventMediatorModel.getInstance(); - private router: RouterModel; private view = new NavigationView(); @@ -33,38 +31,46 @@ class NavigationModel { private init(): boolean { this.setNavigationLinksHandlers(); - this.observeCurrentUser(); - this.subscribeToEventMediator(); + this.switchLinksState(); + this.observeState(); return true; } - private observeCurrentUser(): boolean { - observeStore(selectCurrentUser, () => this.checkCurrentUser.bind(this)); + private observeState(): boolean { + observeStore(selectCurrentUser, () => this.checkCurrentUser()); + observeStore(selectCurrentPage, () => this.switchLinksState()); return true; } private setNavigationLinksHandlers(): boolean { const navigationLinks = this.view.getNavigationLinks(); navigationLinks.forEach((link, route) => { - link.getHTML().addEventListener('click', (event) => { + link.getHTML().addEventListener('click', async (event) => { event.preventDefault(); - this.router.navigateTo(route); + try { + await this.router.navigateTo(route); + } catch { + serverMessageModel.showServerMessage( + SERVER_MESSAGE[getStore().getState().currentLanguage].BAD_REQUEST, + MESSAGE_STATUS.ERROR, + ); + } }); }); return true; } - private subscribeToEventMediator(): boolean { - this.eventMediator.subscribe(MEDIATOR_EVENT.CHANGE_PAGE, (route) => { - const currentRoute = route === '' ? PAGE_ID.MAIN_PAGE : route; - const navigationLinks = this.view.getNavigationLinks(); - const currentLink = navigationLinks.get(String(currentRoute)); - navigationLinks.forEach((link) => link.setEnabled()); - this.checkCurrentUser(); - currentLink?.setDisabled(); - this.view.switchActiveLink(String(currentRoute)); - }); + private switchLinksState(): boolean { + const { currentPage } = getStore().getState(); + const currentPath = currentPage === '' ? PAGE_ID.MAIN_PAGE : currentPage; + const navigationLinks = this.view.getNavigationLinks(); + const currentLink = navigationLinks.get(String(currentPath)); + navigationLinks.forEach((link) => link.setEnabled()); + this.checkCurrentUser(); + currentLink?.setDisabled(); + this.view.switchActiveLink(String(currentPath)); + return true; } diff --git a/src/entities/Navigation/view/NavigationView.ts b/src/entities/Navigation/view/NavigationView.ts index 6839365b..6874485e 100644 --- a/src/entities/Navigation/view/NavigationView.ts +++ b/src/entities/Navigation/view/NavigationView.ts @@ -11,6 +11,10 @@ class NavigationView { private navigationLinks: Map = new Map(); + private toAboutLink: LinkModel; + + private toCatalogLink: LinkModel; + private toLoginLink: LinkModel; private toMainLink: LinkModel; @@ -21,6 +25,8 @@ class NavigationView { this.toMainLink = this.createToMainLink(); this.toLoginLink = this.createToLoginLink(); this.toRegisterLink = this.createToRegisterLink(); + this.toCatalogLink = this.createToCatalogLink(); + this.toAboutLink = this.createToAboutLink(); this.navigation = this.createHTML(); } @@ -29,18 +35,53 @@ class NavigationView { cssClasses: [styles.navigation], tag: 'nav', }); - this.navigation.append(this.toMainLink.getHTML(), this.toLoginLink.getHTML(), this.toRegisterLink.getHTML()); + this.navigation.append( + this.toLoginLink.getHTML(), + this.toRegisterLink.getHTML(), + this.toMainLink.getHTML(), + this.toCatalogLink.getHTML(), + this.toAboutLink.getHTML(), + ); return this.navigation; } + private createToAboutLink(): LinkModel { + this.toAboutLink = new LinkModel({ + attrs: { + href: PAGE_ID.ABOUT_US_PAGE, + }, + classes: [styles.link], + text: PAGE_LINK_TEXT[getStore().getState().currentLanguage].ABOUT, + }); + + observeCurrentLanguage(this.toAboutLink.getHTML(), PAGE_LINK_TEXT, PAGE_LINK_TEXT_KEYS.ABOUT); + + this.navigationLinks.set(PAGE_ID.ABOUT_US_PAGE, this.toAboutLink); + return this.toAboutLink; + } + + private createToCatalogLink(): LinkModel { + this.toCatalogLink = new LinkModel({ + attrs: { + href: PAGE_ID.CATALOG_PAGE, + }, + classes: [styles.link], + text: PAGE_LINK_TEXT[getStore().getState().currentLanguage].CATALOG, + }); + + observeCurrentLanguage(this.toCatalogLink.getHTML(), PAGE_LINK_TEXT, PAGE_LINK_TEXT_KEYS.CATALOG); + + this.navigationLinks.set(PAGE_ID.CATALOG_PAGE, this.toCatalogLink); + return this.toCatalogLink; + } + private createToLoginLink(): LinkModel { - const { currentLanguage } = getStore().getState(); this.toLoginLink = new LinkModel({ attrs: { href: PAGE_ID.LOGIN_PAGE, }, classes: [styles.link], - text: PAGE_LINK_TEXT[currentLanguage].LOGIN, + text: PAGE_LINK_TEXT[getStore().getState().currentLanguage].LOGIN, }); observeCurrentLanguage(this.toLoginLink.getHTML(), PAGE_LINK_TEXT, PAGE_LINK_TEXT_KEYS.LOGIN); @@ -50,13 +91,12 @@ class NavigationView { } private createToMainLink(): LinkModel { - const { currentLanguage } = getStore().getState(); this.toMainLink = new LinkModel({ attrs: { href: PAGE_ID.MAIN_PAGE, }, classes: [styles.link], - text: PAGE_LINK_TEXT[currentLanguage].MAIN, + text: PAGE_LINK_TEXT[getStore().getState().currentLanguage].MAIN, }); observeCurrentLanguage(this.toMainLink.getHTML(), PAGE_LINK_TEXT, PAGE_LINK_TEXT_KEYS.MAIN); @@ -66,13 +106,12 @@ class NavigationView { } private createToRegisterLink(): LinkModel { - const { currentLanguage } = getStore().getState(); this.toRegisterLink = new LinkModel({ attrs: { href: PAGE_ID.REGISTRATION_PAGE, }, classes: [styles.link], - text: PAGE_LINK_TEXT[currentLanguage].REGISTRATION, + text: PAGE_LINK_TEXT[getStore().getState().currentLanguage].REGISTRATION, }); observeCurrentLanguage(this.toRegisterLink.getHTML(), PAGE_LINK_TEXT, PAGE_LINK_TEXT_KEYS.REGISTRATION); diff --git a/src/entities/Navigation/view/navigationView.module.scss b/src/entities/Navigation/view/navigationView.module.scss index 4657a9cf..d241dfc4 100644 --- a/src/entities/Navigation/view/navigationView.module.scss +++ b/src/entities/Navigation/view/navigationView.module.scss @@ -1,14 +1,19 @@ .navigation { display: flex; + align-items: center; align-self: center; justify-content: center; - grid-column: 2; - grid-row: 1; - gap: var(--extra-small-offset); + order: 2; + margin: 0 auto; + height: 70px; } .link { position: relative; + display: flex; + align-items: center; + padding: 0 calc(var(--extra-small-offset) / 2); + height: 100%; font: var(--regular-font); letter-spacing: 1px; text-transform: uppercase; @@ -19,7 +24,7 @@ content: ''; position: absolute; left: 0; - bottom: -4px; + bottom: -1px; width: 100%; height: 2px; background-color: currentcolor; diff --git a/src/features/CountryChoice/model/CountryChoiceModel.ts b/src/features/CountryChoice/model/CountryChoiceModel.ts index e93baf62..5a8adf80 100644 --- a/src/features/CountryChoice/model/CountryChoiceModel.ts +++ b/src/features/CountryChoice/model/CountryChoiceModel.ts @@ -7,6 +7,7 @@ import observeStore, { } from '@/shared/Store/observer.ts'; import COUNTRIES_LIST from '@/shared/constants/countriesList.ts'; import { BILLING_ADDRESS_COUNTRY } from '@/shared/constants/forms/register/fieldParams.ts'; +import formattedText from '@/shared/utils/formattedText.ts'; import getCountryIndex from '@/shared/utils/getCountryIndex.ts'; import CountryChoiceView from '../view/CountryChoiceView.ts'; @@ -60,7 +61,7 @@ class CountryChoiceModel { private setCountryToStore(element: HTMLDivElement | HTMLInputElement, key: string): boolean { const currentCountryIndex = getCountryIndex( - element instanceof HTMLDivElement ? element.textContent || '' : element.value, + element instanceof HTMLDivElement ? formattedText(element.textContent ?? '') : formattedText(element.value), ); const action = key === BILLING_ADDRESS_COUNTRY.inputParams.id ? setBillingCountry : setShippingCountry; diff --git a/src/features/CountryChoice/view/CountryChoiceView.ts b/src/features/CountryChoice/view/CountryChoiceView.ts index bb846c15..7be9d1b4 100644 --- a/src/features/CountryChoice/view/CountryChoiceView.ts +++ b/src/features/CountryChoice/view/CountryChoiceView.ts @@ -37,9 +37,7 @@ class CountryChoiceView { tag: 'div', }); - const { currentLanguage } = getStore().getState(); - - Object.entries(COUNTRIES_LIST[currentLanguage]).forEach(([countryName, countryCode]) => + Object.entries(COUNTRIES_LIST[getStore().getState().currentLanguage]).forEach(([countryName, countryCode]) => this.countryDropList.append(this.createCountryItem(countryName, countryCode)), ); diff --git a/src/features/CountryChoice/view/countryChoiceView.module.scss b/src/features/CountryChoice/view/countryChoiceView.module.scss index 3e71d321..cbf2392c 100644 --- a/src/features/CountryChoice/view/countryChoiceView.module.scss +++ b/src/features/CountryChoice/view/countryChoiceView.module.scss @@ -3,6 +3,7 @@ z-index: 5; grid-column: 1; grid-row: 4; + transform: translateY(-127%); transition: opacity 0.2s, visibility 0.2s; @@ -17,7 +18,7 @@ padding: calc(var(--extra-small-offset) / 4) calc(var(--extra-small-offset) / 2); font: var(--regular-font); letter-spacing: 1px; - text-align: end; + text-align: start; color: var(--noble-gray-800); transition: color 0.2s, diff --git a/src/features/InputFieldValidator/model/InputFieldValidatorModel.ts b/src/features/InputFieldValidator/model/InputFieldValidatorModel.ts index fc9b253f..493a5119 100644 --- a/src/features/InputFieldValidator/model/InputFieldValidatorModel.ts +++ b/src/features/InputFieldValidator/model/InputFieldValidatorModel.ts @@ -16,9 +16,9 @@ class InputFieldValidatorModel { Validator.checkRequired(value, this.validParams), Validator.checkWhitespace(value, this.validParams), Validator.checkNotSpecialSymbols(value, this.validParams), + Validator.checkRequiredSymbols(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), diff --git a/src/features/InputFieldValidator/validators/validators.ts b/src/features/InputFieldValidator/validators/validators.ts index 97eafcfc..a0608673 100644 --- a/src/features/InputFieldValidator/validators/validators.ts +++ b/src/features/InputFieldValidator/validators/validators.ts @@ -1,9 +1,11 @@ import type { InputFieldValidatorParams } from '@/shared/types/form.ts'; import getStore from '@/shared/Store/Store.ts'; +import observeStore, { selectCurrentLanguage } from '@/shared/Store/observer.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 { checkInputLanguage } from '@/shared/utils/getCountryIndex.ts'; import { maxAgeMessage, maxLengthMessage, minAgeMessage, minLengthMessage } from '@/shared/utils/messageTemplate.ts'; import { postcodeValidator } from 'postcode-validator'; @@ -72,13 +74,14 @@ export const checkValidAge = (value: string, validParams: InputFieldValidatorPar export const checkValidCountry = (value: string, validParams: InputFieldValidatorParams): boolean | string => { if (validParams.validCountry) { if ( - !Object.keys(COUNTRIES_LIST[getStore().getState().currentLanguage]).find( + !Object.keys(COUNTRIES_LIST[checkInputLanguage(value)]).find( (countryName) => countryName.toLowerCase() === value.toLowerCase(), ) ) { return ERROR_MESSAGE[getStore().getState().currentLanguage].INVALID_COUNTRY; } } + observeStore(selectCurrentLanguage, () => checkValidCountry(value, validParams)); return true; }; @@ -109,6 +112,5 @@ export const checkWhitespace = (value: string, validParams: InputFieldValidatorP if (validParams.notWhitespace && !validParams.notWhitespace.pattern.test(value)) { return validParams.notWhitespace.messages[getStore().getState().currentLanguage]; } - return true; }; diff --git a/src/main.ts b/src/main.ts index a827903a..2559ce8f 100644 --- a/src/main.ts +++ b/src/main.ts @@ -2,4 +2,4 @@ import AppModel from '@/app/App/model/AppModel.ts'; import '@/styles.scss'; const myApp = new AppModel(); -document.body.append(myApp.getHTML()); +myApp.start(); diff --git a/src/pages/AboutUsPage/model/AboutUsPageModel.ts b/src/pages/AboutUsPage/model/AboutUsPageModel.ts new file mode 100644 index 00000000..a26ce9f9 --- /dev/null +++ b/src/pages/AboutUsPage/model/AboutUsPageModel.ts @@ -0,0 +1,26 @@ +import type { Page } from '@/shared/types/common.ts'; + +import getStore from '@/shared/Store/Store.ts'; +import { setCurrentPage } from '@/shared/Store/actions.ts'; +import { PAGE_ID } from '@/shared/constants/pages.ts'; + +import AboutUsPageView from '../view/AboutUsPageView.ts'; + +class AboutUsPageModel implements Page { + private view: AboutUsPageView; + + constructor(parent: HTMLDivElement) { + this.view = new AboutUsPageView(parent); + this.init(); + } + + private init(): void { + getStore().dispatch(setCurrentPage(PAGE_ID.ABOUT_US_PAGE)); + } + + public getHTML(): HTMLDivElement { + return this.view.getHTML(); + } +} + +export default AboutUsPageModel; diff --git a/src/pages/AboutUsPage/view/AboutUsPageView.ts b/src/pages/AboutUsPage/view/AboutUsPageView.ts new file mode 100644 index 00000000..b05ea098 --- /dev/null +++ b/src/pages/AboutUsPage/view/AboutUsPageView.ts @@ -0,0 +1,31 @@ +import createBaseElement from '@/shared/utils/createBaseElement.ts'; + +import styles from './aboutUsPageView.module.scss'; + +class AboutUsPageView { + private page: HTMLDivElement; + + private parent: HTMLDivElement; + + constructor(parent: HTMLDivElement) { + this.parent = parent; + this.parent.innerHTML = ''; + this.page = this.createHTML(); + } + + private createHTML(): HTMLDivElement { + this.page = createBaseElement({ + cssClasses: [styles.aboutUsPage], + tag: 'div', + }); + + this.parent.append(this.page); + + return this.page; + } + + public getHTML(): HTMLDivElement { + return this.page; + } +} +export default AboutUsPageView; diff --git a/src/pages/AboutUsPage/view/aboutUsPageView.module.scss b/src/pages/AboutUsPage/view/aboutUsPageView.module.scss new file mode 100644 index 00000000..10a217f4 --- /dev/null +++ b/src/pages/AboutUsPage/view/aboutUsPageView.module.scss @@ -0,0 +1,17 @@ +.aboutUsPage { + position: relative; + display: block; + padding: 0 var(--small-offset); + animation: show 0.2s ease-out forwards; +} + +@keyframes show { + 0% { + opacity: 0; + } + + 100% { + display: block; + opacity: 1; + } +} diff --git a/src/pages/CartPage/model/CartPageModel.ts b/src/pages/CartPage/model/CartPageModel.ts new file mode 100644 index 00000000..c9203868 --- /dev/null +++ b/src/pages/CartPage/model/CartPageModel.ts @@ -0,0 +1,26 @@ +import type { Page } from '@/shared/types/common.ts'; + +import getStore from '@/shared/Store/Store.ts'; +import { setCurrentPage } from '@/shared/Store/actions.ts'; +import { PAGE_ID } from '@/shared/constants/pages.ts'; + +import CartPageView from '../view/CartPageView.ts'; + +class CartPageModel implements Page { + private view: CartPageView; + + constructor(parent: HTMLDivElement) { + this.view = new CartPageView(parent); + this.init(); + } + + private init(): void { + getStore().dispatch(setCurrentPage(PAGE_ID.CART_PAGE)); + } + + public getHTML(): HTMLDivElement { + return this.view.getHTML(); + } +} + +export default CartPageModel; diff --git a/src/pages/CartPage/view/CartPageView.ts b/src/pages/CartPage/view/CartPageView.ts new file mode 100644 index 00000000..2fc3fff2 --- /dev/null +++ b/src/pages/CartPage/view/CartPageView.ts @@ -0,0 +1,31 @@ +import createBaseElement from '@/shared/utils/createBaseElement.ts'; + +import styles from './cartPageView.module.scss'; + +class CartPageView { + private page: HTMLDivElement; + + private parent: HTMLDivElement; + + constructor(parent: HTMLDivElement) { + this.parent = parent; + this.parent.innerHTML = ''; + this.page = this.createHTML(); + } + + private createHTML(): HTMLDivElement { + this.page = createBaseElement({ + cssClasses: [styles.cartPage], + tag: 'div', + }); + + this.parent.append(this.page); + + return this.page; + } + + public getHTML(): HTMLDivElement { + return this.page; + } +} +export default CartPageView; diff --git a/src/pages/CartPage/view/cartPageView.module.scss b/src/pages/CartPage/view/cartPageView.module.scss new file mode 100644 index 00000000..e6c0fc48 --- /dev/null +++ b/src/pages/CartPage/view/cartPageView.module.scss @@ -0,0 +1,17 @@ +.cartPage { + position: relative; + display: block; + padding: 0 var(--small-offset); + animation: show 0.2s ease-out forwards; +} + +@keyframes show { + 0% { + opacity: 0; + } + + 100% { + display: block; + opacity: 1; + } +} diff --git a/src/pages/CatalogPage/model/CatalogPageModel.ts b/src/pages/CatalogPage/model/CatalogPageModel.ts new file mode 100644 index 00000000..6ca615e3 --- /dev/null +++ b/src/pages/CatalogPage/model/CatalogPageModel.ts @@ -0,0 +1,30 @@ +import type { Page } from '@/shared/types/common.ts'; + +import getStore from '@/shared/Store/Store.ts'; +import { setCurrentPage } from '@/shared/Store/actions.ts'; +import { PAGE_ID } from '@/shared/constants/pages.ts'; +import CatalogModel from '@/widgets/Catalog/model/CatalogModel.ts'; + +import CatalogPageView from '../view/CatalogPageView.ts'; + +class CatalogPageModel implements Page { + private catalog = new CatalogModel(); + + private view: CatalogPageView; + + constructor(parent: HTMLDivElement) { + this.view = new CatalogPageView(parent); + this.init(); + } + + private init(): void { + getStore().dispatch(setCurrentPage(PAGE_ID.CATALOG_PAGE)); + this.getHTML().append(this.catalog.getHTML()); + } + + public getHTML(): HTMLDivElement { + return this.view.getHTML(); + } +} + +export default CatalogPageModel; diff --git a/src/pages/CatalogPage/view/CatalogPageView.ts b/src/pages/CatalogPage/view/CatalogPageView.ts new file mode 100644 index 00000000..b189648f --- /dev/null +++ b/src/pages/CatalogPage/view/CatalogPageView.ts @@ -0,0 +1,31 @@ +import createBaseElement from '@/shared/utils/createBaseElement.ts'; + +import styles from './catalogPageView.module.scss'; + +class CatalogPageView { + private page: HTMLDivElement; + + private parent: HTMLDivElement; + + constructor(parent: HTMLDivElement) { + this.parent = parent; + this.parent.innerHTML = ''; + this.page = this.createHTML(); + } + + private createHTML(): HTMLDivElement { + this.page = createBaseElement({ + cssClasses: [styles.catalogPage], + tag: 'div', + }); + + this.parent.append(this.page); + + return this.page; + } + + public getHTML(): HTMLDivElement { + return this.page; + } +} +export default CatalogPageView; diff --git a/src/pages/CatalogPage/view/catalogPageView.module.scss b/src/pages/CatalogPage/view/catalogPageView.module.scss new file mode 100644 index 00000000..19cec491 --- /dev/null +++ b/src/pages/CatalogPage/view/catalogPageView.module.scss @@ -0,0 +1,17 @@ +.catalogPage { + position: relative; + display: block; + padding: 0 var(--small-offset); + animation: show 0.2s ease-out forwards; +} + +@keyframes show { + 0% { + opacity: 0; + } + + 100% { + display: block; + opacity: 1; + } +} diff --git a/src/pages/ItemPage/model/ItemPageModel.ts b/src/pages/ItemPage/model/ItemPageModel.ts new file mode 100644 index 00000000..7616e05a --- /dev/null +++ b/src/pages/ItemPage/model/ItemPageModel.ts @@ -0,0 +1,26 @@ +import type { Page } from '@/shared/types/common.ts'; + +import getStore from '@/shared/Store/Store.ts'; +import { setCurrentPage } from '@/shared/Store/actions.ts'; +import { PAGE_ID } from '@/shared/constants/pages.ts'; + +import ItemPageView from '../view/ItemPageView.ts'; + +class ItemPageModel implements Page { + private view: ItemPageView; + + constructor(parent: HTMLDivElement) { + this.view = new ItemPageView(parent); + this.init(); + } + + private init(): void { + getStore().dispatch(setCurrentPage(PAGE_ID.ITEM_PAGE)); + } + + public getHTML(): HTMLDivElement { + return this.view.getHTML(); + } +} + +export default ItemPageModel; diff --git a/src/pages/ItemPage/view/ItemPageView.ts b/src/pages/ItemPage/view/ItemPageView.ts new file mode 100644 index 00000000..9508e3cb --- /dev/null +++ b/src/pages/ItemPage/view/ItemPageView.ts @@ -0,0 +1,31 @@ +import createBaseElement from '@/shared/utils/createBaseElement.ts'; + +import styles from './itemPageView.module.scss'; + +class ItemPageView { + private page: HTMLDivElement; + + private parent: HTMLDivElement; + + constructor(parent: HTMLDivElement) { + this.parent = parent; + this.parent.innerHTML = ''; + this.page = this.createHTML(); + } + + private createHTML(): HTMLDivElement { + this.page = createBaseElement({ + cssClasses: [styles.itemPage], + tag: 'div', + }); + + this.parent.append(this.page); + + return this.page; + } + + public getHTML(): HTMLDivElement { + return this.page; + } +} +export default ItemPageView; diff --git a/src/pages/ItemPage/view/itemPageView.module.scss b/src/pages/ItemPage/view/itemPageView.module.scss new file mode 100644 index 00000000..5e3f8fc9 --- /dev/null +++ b/src/pages/ItemPage/view/itemPageView.module.scss @@ -0,0 +1,17 @@ +.itemPage { + position: relative; + display: block; + padding: 0 var(--small-offset); + animation: show 0.2s ease-out forwards; +} + +@keyframes show { + 0% { + opacity: 0; + } + + 100% { + display: block; + opacity: 1; + } +} diff --git a/src/pages/LoginPage/model/LoginPageModel.ts b/src/pages/LoginPage/model/LoginPageModel.ts index 31f21f18..c7f9b8bc 100644 --- a/src/pages/LoginPage/model/LoginPageModel.ts +++ b/src/pages/LoginPage/model/LoginPageModel.ts @@ -1,9 +1,12 @@ import type RouterModel from '@/app/Router/model/RouterModel.ts'; import type { Page } from '@/shared/types/common.ts'; +import type { User } from '@/shared/types/user.ts'; -import EventMediatorModel from '@/shared/EventMediator/model/EventMediatorModel.ts'; +import serverMessageModel from '@/shared/ServerMessage/model/ServerMessageModel.ts'; import getStore from '@/shared/Store/Store.ts'; -import MEDIATOR_EVENT from '@/shared/constants/events.ts'; +import { setCurrentPage } from '@/shared/Store/actions.ts'; +import observeStore, { selectCurrentUser } from '@/shared/Store/observer.ts'; +import { MESSAGE_STATUS, SERVER_MESSAGE } from '@/shared/constants/messages.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'; @@ -11,8 +14,6 @@ import LoginFormModel from '@/widgets/LoginForm/model/LoginFormModel.ts'; import LoginPageView from '../view/LoginPageView.ts'; class LoginPageModel implements Page { - private eventMediator = EventMediatorModel.getInstance(); - private loginForm = new LoginFormModel(); private router: RouterModel; @@ -25,26 +26,50 @@ class LoginPageModel implements Page { this.init(); } - private checkAuthUser(): boolean { - if (!getStore().getState().currentUser) { - this.view.show(); - this.loginForm.getFirstInputField().getView().getInput().getHTML().focus(); - return false; + private async checkAuthUser(): Promise { + const { currentUser } = getStore().getState(); + + if (currentUser) { + try { + await this.router.navigateTo(PAGE_ID.MAIN_PAGE); + return currentUser; + } catch (error) { + serverMessageModel.showServerMessage( + SERVER_MESSAGE[getStore().getState().currentLanguage].BAD_REQUEST, + MESSAGE_STATUS.ERROR, + ); + return null; + } } - this.router.navigateTo(PAGE_ID.MAIN_PAGE); - return true; + + return null; } private init(): boolean { - this.subscribeToEventMediator(); + getStore().dispatch(setCurrentPage(PAGE_ID.LOGIN_PAGE)); + this.checkAuthUser().catch(() => { + serverMessageModel.showServerMessage( + SERVER_MESSAGE[getStore().getState().currentLanguage].BAD_REQUEST, + MESSAGE_STATUS.ERROR, + ); + }); this.view.getAuthWrapper().append(this.loginForm.getHTML()); + this.loginForm.getFirstInputField().getView().getInput().getHTML().focus(); this.setRegisterLinkHandler(); + observeStore(selectCurrentUser, () => this.checkAuthUser()); return true; } - private registerLinkHandler(event: Event): void { + private async registerLinkHandler(event: Event): Promise { event.preventDefault(); - this.router.navigateTo(PAGE_ID.REGISTRATION_PAGE); + try { + await this.router.navigateTo(PAGE_ID.REGISTRATION_PAGE); + } catch (error) { + serverMessageModel.showServerMessage( + SERVER_MESSAGE[getStore().getState().currentLanguage].BAD_REQUEST, + MESSAGE_STATUS.ERROR, + ); + } } private setRegisterLinkHandler(): void { @@ -59,20 +84,6 @@ class LoginPageModel implements Page { toRegisterPageWrapper.append(registerLinkCopy); } - private subscribeToEventMediator(): void { - this.eventMediator.subscribe(MEDIATOR_EVENT.CHANGE_PAGE, (route) => this.switchPageVisibility(route)); - } - - private switchPageVisibility(route: unknown): boolean { - if (route === PAGE_ID.LOGIN_PAGE) { - this.checkAuthUser(); - } else { - this.view.hide(); - return false; - } - return true; - } - public getHTML(): HTMLDivElement { return this.view.getHTML(); } diff --git a/src/pages/LoginPage/view/LoginPageView.ts b/src/pages/LoginPage/view/LoginPageView.ts index 897221b4..e6b37a04 100644 --- a/src/pages/LoginPage/view/LoginPageView.ts +++ b/src/pages/LoginPage/view/LoginPageView.ts @@ -1,6 +1,5 @@ import LinkModel from '@/shared/Link/model/LinkModel.ts'; import getStore from '@/shared/Store/Store.ts'; -import { PAGE_TIMEOUT_DURATION } from '@/shared/constants/animations.ts'; import { PAGE_ANSWER, PAGE_ANSWER_KEYS, @@ -36,6 +35,7 @@ class LoginPageView { constructor(parent: HTMLDivElement) { this.parent = parent; + this.parent.innerHTML = ''; this.toRegisterPageWrapper = this.createToRegisterPageWrapper(); this.loginSpan = this.createLoginSpan(); this.designElement = this.createDesignElement(); @@ -47,15 +47,12 @@ class LoginPageView { } private createAuthDescription(): HTMLHeadingElement { - const { currentLanguage } = getStore().getState(); this.authDescription = createBaseElement({ cssClasses: [styles.authDescription], - innerContent: PAGE_DESCRIPTION[currentLanguage].LOGIN, + innerContent: PAGE_DESCRIPTION[getStore().getState().currentLanguage].LOGIN, tag: 'h3', }); - observeCurrentLanguage(this.authDescription, PAGE_DESCRIPTION, PAGE_DESCRIPTION_KEYS.LOGIN); - return this.authDescription; } @@ -100,10 +97,9 @@ class LoginPageView { } private createLoginSpan(): HTMLSpanElement { - const { currentLanguage } = getStore().getState(); this.loginSpan = createBaseElement({ cssClasses: [styles.loginSpan], - innerContent: PAGE_LINK_TEXT[currentLanguage].LOGIN, + innerContent: PAGE_LINK_TEXT[getStore().getState().currentLanguage].LOGIN, tag: 'span', }); @@ -113,13 +109,12 @@ class LoginPageView { } private createRegisterLink(): LinkModel { - const { currentLanguage } = getStore().getState(); this.registerLink = new LinkModel({ attrs: { href: PAGE_ID.REGISTRATION_PAGE, }, classes: [styles.registerLink], - text: PAGE_LINK_TEXT[currentLanguage].REGISTRATION, + text: PAGE_LINK_TEXT[getStore().getState().currentLanguage].REGISTRATION, }); observeCurrentLanguage(this.registerLink.getHTML(), PAGE_LINK_TEXT, PAGE_LINK_TEXT_KEYS.REGISTRATION); @@ -128,10 +123,9 @@ class LoginPageView { } private createToRegisterPageWrapper(): HTMLSpanElement { - const { currentLanguage } = getStore().getState(); this.toRegisterPageWrapper = createBaseElement({ cssClasses: [styles.toRegisterPageWrapper], - innerContent: PAGE_ANSWER[currentLanguage].LOGIN, + innerContent: PAGE_ANSWER[getStore().getState().currentLanguage].LOGIN, tag: 'span', }); @@ -155,17 +149,5 @@ class LoginPageView { public getToRegisterPageWrapper(): HTMLSpanElement { return this.toRegisterPageWrapper; } - - public hide(): boolean { - this.page.classList.add(styles.loginPage_hidden); - return true; - } - - public show(): boolean { - setTimeout(() => { - this.page.classList.remove(styles.loginPage_hidden); - }, PAGE_TIMEOUT_DURATION); - return true; - } } export default LoginPageView; diff --git a/src/pages/LoginPage/view/loginPageView.module.scss b/src/pages/LoginPage/view/loginPageView.module.scss index 0d92bfeb..16e565c4 100644 --- a/src/pages/LoginPage/view/loginPageView.module.scss +++ b/src/pages/LoginPage/view/loginPageView.module.scss @@ -3,10 +3,6 @@ display: block; padding: 0 var(--small-offset); animation: show 0.2s ease-out forwards; - - &_hidden { - animation: hide 0.2s ease-in forwards; - } } @keyframes show { @@ -20,17 +16,6 @@ } } -@keyframes hide { - 0% { - opacity: 1; - } - - 100% { - display: none; - opacity: 0; - } -} - .authWrapper { display: grid; grid-template-columns: repeat(2, 1fr); @@ -120,6 +105,7 @@ grid-column: 2 span; grid-row: 4; font: var(--regular-font); + letter-spacing: 1px; text-align: center; color: var(--steam-green-800); gap: calc(var(--extra-small-offset) / 4); diff --git a/src/pages/MainPage/model/MainPageModel.ts b/src/pages/MainPage/model/MainPageModel.ts index 496a74a9..f46e71a7 100644 --- a/src/pages/MainPage/model/MainPageModel.ts +++ b/src/pages/MainPage/model/MainPageModel.ts @@ -1,15 +1,15 @@ 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 observeStore, { selectCurrentUser } from '@/shared/Store/observer.ts'; -import MEDIATOR_EVENT from '@/shared/constants/events.ts'; +import NavigationModel from '@/entities/Navigation/model/NavigationModel.ts'; +import getStore from '@/shared/Store/Store.ts'; +import { setCurrentPage } from '@/shared/Store/actions.ts'; import { PAGE_ID } from '@/shared/constants/pages.ts'; import MainPageView from '../view/MainPageView.ts'; class MainPageModel implements Page { - private eventMediator = EventMediatorModel.getInstance(); + private navigation: NavigationModel; private router: RouterModel; @@ -18,33 +18,13 @@ class MainPageModel implements Page { constructor(parent: HTMLDivElement, router: RouterModel) { this.router = router; this.view = new MainPageView(parent); + this.navigation = new NavigationModel(this.router); this.init(); } private init(): void { - this.subscribeToEventMediator(); - this.subscribeToStore(); - } - - private subscribeToEventMediator(): void { - this.eventMediator.subscribe(MEDIATOR_EVENT.CHANGE_PAGE, (route) => this.switchPageVisibility(route)); - } - - private subscribeToStore(): boolean { - observeStore(selectCurrentUser, () => { - this.router.navigateTo(PAGE_ID.MAIN_PAGE); - }); - return true; - } - - private switchPageVisibility(route: unknown): boolean { - if (route === PAGE_ID.MAIN_PAGE || route === PAGE_ID.DEFAULT_PAGE) { - this.view.show(); - } else { - this.view.hide(); - return false; - } - return true; + this.getHTML().append(this.navigation.getHTML()); + getStore().dispatch(setCurrentPage(PAGE_ID.MAIN_PAGE)); } public getHTML(): HTMLDivElement { diff --git a/src/pages/MainPage/view/MainPageView.ts b/src/pages/MainPage/view/MainPageView.ts index 655797e4..cd6bbd83 100644 --- a/src/pages/MainPage/view/MainPageView.ts +++ b/src/pages/MainPage/view/MainPageView.ts @@ -1,4 +1,3 @@ -import { PAGE_TIMEOUT_DURATION } from '@/shared/constants/animations.ts'; import createBaseElement from '@/shared/utils/createBaseElement.ts'; import styles from './mainPageView.module.scss'; @@ -10,6 +9,7 @@ class MainPageView { constructor(parent: HTMLDivElement) { this.parent = parent; + this.parent.innerHTML = ''; this.page = this.createHTML(); } @@ -27,17 +27,5 @@ class MainPageView { public getHTML(): HTMLDivElement { return this.page; } - - public hide(): boolean { - this.page.classList.add(styles.mainPage_hidden); - return true; - } - - public show(): boolean { - setTimeout(() => { - this.page.classList.remove(styles.mainPage_hidden); - }, PAGE_TIMEOUT_DURATION); - return true; - } } export default MainPageView; diff --git a/src/pages/MainPage/view/mainPageView.module.scss b/src/pages/MainPage/view/mainPageView.module.scss index df1156b7..de4c2be5 100644 --- a/src/pages/MainPage/view/mainPageView.module.scss +++ b/src/pages/MainPage/view/mainPageView.module.scss @@ -3,10 +3,6 @@ display: block; padding: 0 var(--small-offset); animation: show 0.2s ease-out forwards; - - &_hidden { - animation: hide 0.2s ease-in forwards; - } } @keyframes show { @@ -19,14 +15,3 @@ opacity: 1; } } - -@keyframes hide { - 0% { - opacity: 1; - } - - 100% { - display: none; - opacity: 0; - } -} diff --git a/src/pages/NotFoundPage/model/NotFoundPageModel.ts b/src/pages/NotFoundPage/model/NotFoundPageModel.ts index 26a8ed41..d92c4213 100644 --- a/src/pages/NotFoundPage/model/NotFoundPageModel.ts +++ b/src/pages/NotFoundPage/model/NotFoundPageModel.ts @@ -1,17 +1,14 @@ 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 getStore from '@/shared/Store/Store.ts'; +import { setCurrentPage } from '@/shared/Store/actions.ts'; import observeStore, { selectCurrentLanguage } from '@/shared/Store/observer.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'; class NotFoundPageModel implements Page { - private eventMediator = EventMediatorModel.getInstance(); - private router: RouterModel; private view: NotFoundPageView; @@ -32,9 +29,10 @@ class NotFoundPageModel implements Page { } private init(): boolean { - this.subscribeToEventMediator(); + getStore().dispatch(setCurrentPage(PAGE_ID.NOT_FOUND_PAGE)); this.toMainButtonHandler(); this.observeStoreLanguage(); + this.view.setPageDescription(this.createPageDescription()); return true; } @@ -45,21 +43,6 @@ class NotFoundPageModel implements Page { return true; } - private subscribeToEventMediator(): void { - this.eventMediator.subscribe(MEDIATOR_EVENT.CHANGE_PAGE, (route) => this.switchPageVisibility(route)); - } - - private switchPageVisibility(route: unknown): boolean { - if (route === PAGE_ID.NOT_FOUND_PAGE) { - this.view.show(); - this.view.setPageDescription(this.createPageDescription()); - } else { - this.view.hide(); - return false; - } - return true; - } - private toMainButtonHandler(): boolean { const toMainButton = this.view.getToMainButton().getHTML(); toMainButton.addEventListener('click', this.router.navigateTo.bind(this.router, PAGE_ID.MAIN_PAGE)); diff --git a/src/pages/NotFoundPage/view/NotFoundPageView.ts b/src/pages/NotFoundPage/view/NotFoundPageView.ts index 98f7c49d..2058e14f 100644 --- a/src/pages/NotFoundPage/view/NotFoundPageView.ts +++ b/src/pages/NotFoundPage/view/NotFoundPageView.ts @@ -1,6 +1,5 @@ import ButtonModel from '@/shared/Button/model/ButtonModel.ts'; import getStore from '@/shared/Store/Store.ts'; -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'; @@ -70,10 +69,9 @@ class NotFoundPageView { } private createToMainButton(): ButtonModel { - const { currentLanguage } = getStore().getState(); this.toMainButton = new ButtonModel({ classes: [styles.toMainButton], - text: BUTTON_TEXT[currentLanguage].BACK_TO_MAIN, + text: BUTTON_TEXT[getStore().getState().currentLanguage].BACK_TO_MAIN, }); observeCurrentLanguage(this.toMainButton.getHTML(), BUTTON_TEXT, BUTTON_TEXT_KEYS.BACK_TO_MAIN); return this.toMainButton; @@ -87,22 +85,10 @@ class NotFoundPageView { return this.toMainButton; } - public hide(): boolean { - this.page.classList.add(styles.notFoundPage_hidden); - return true; - } - public setPageDescription(text: string): HTMLParagraphElement { this.description.innerText = text; return this.description; } - - public show(): boolean { - setTimeout(() => { - this.page.classList.remove(styles.notFoundPage_hidden); - }, PAGE_TIMEOUT_DURATION); - return true; - } } export default NotFoundPageView; diff --git a/src/pages/NotFoundPage/view/notFoundPageView.module.scss b/src/pages/NotFoundPage/view/notFoundPageView.module.scss index e3fcc2af..1a10ac8a 100644 --- a/src/pages/NotFoundPage/view/notFoundPageView.module.scss +++ b/src/pages/NotFoundPage/view/notFoundPageView.module.scss @@ -2,18 +2,16 @@ position: relative; display: flex; flex-direction: column; - margin: 0 auto; + align-self: center; + margin: 0 var(--small-offset); border-bottom: calc(var(--extra-small-offset) / 2) solid var(--steam-green-800); - padding: calc(var(--extra-large-offset) / 2) var(--extra-large-offset); + border-radius: var(--medium-br); + padding: var(--small-offset); padding-bottom: calc(var(--extra-large-offset) / 2 + calc(var(--extra-small-offset) / 2)); max-width: 500px; background-color: var(--white); animation: show 0.2s ease-out forwards; gap: var(--extra-small-offset); - - &_hidden { - animation: hide 0.2s ease-in forwards; - } } @keyframes show { @@ -27,17 +25,6 @@ } } -@keyframes hide { - 0% { - opacity: 1; - } - - 100% { - display: none; - opacity: 0; - } -} - .pageLogo { display: flex; margin: 0 auto; @@ -65,8 +52,10 @@ } .toMainButton { + align-self: 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); diff --git a/src/pages/RegistrationPage/model/RegistrationPageModel.ts b/src/pages/RegistrationPage/model/RegistrationPageModel.ts index 03298892..951ed963 100644 --- a/src/pages/RegistrationPage/model/RegistrationPageModel.ts +++ b/src/pages/RegistrationPage/model/RegistrationPageModel.ts @@ -1,8 +1,11 @@ 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 MEDIATOR_EVENT from '@/shared/constants/events.ts'; +import serverMessageModel from '@/shared/ServerMessage/model/ServerMessageModel.ts'; +import getStore from '@/shared/Store/Store.ts'; +import { setCurrentPage } from '@/shared/Store/actions.ts'; +import observeStore, { selectIsUserLoggedIn } from '@/shared/Store/observer.ts'; +import { MESSAGE_STATUS, SERVER_MESSAGE } from '@/shared/constants/messages.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'; @@ -10,8 +13,6 @@ import RegisterFormModel from '@/widgets/RegistrationForm/model/RegistrationForm import RegistrationPageView from '../view/RegistrationPageView.ts'; class RegistrationPageModel implements Page { - private eventMediator = EventMediatorModel.getInstance(); - private registerForm = new RegisterFormModel(); private router: RouterModel; @@ -25,14 +26,23 @@ class RegistrationPageModel implements Page { } private init(): void { + getStore().dispatch(setCurrentPage(PAGE_ID.REGISTRATION_PAGE)); this.view.getAuthWrapper().append(this.registerForm.getHTML()); - this.subscribeToEventMediator(); + this.registerForm.getFirstInputField().getView().getInput().getHTML().focus(); + observeStore(selectIsUserLoggedIn, () => this.router.navigateTo(PAGE_ID.MAIN_PAGE)); this.setLoginLinkHandler(); } - private loginLinkHandler(event: Event): void { + private async loginLinkHandler(event: Event): Promise { event.preventDefault(); - this.router.navigateTo(PAGE_ID.LOGIN_PAGE); + try { + await this.router.navigateTo(PAGE_ID.LOGIN_PAGE); + } catch { + serverMessageModel.showServerMessage( + SERVER_MESSAGE[getStore().getState().currentLanguage].BAD_REQUEST, + MESSAGE_STATUS.ERROR, + ); + } } private setLoginLinkHandler(): void { @@ -47,21 +57,6 @@ class RegistrationPageModel implements Page { toLoginPageWrapper.append(loginLinkCopy); } - private subscribeToEventMediator(): void { - this.eventMediator.subscribe(MEDIATOR_EVENT.CHANGE_PAGE, (route) => this.switchPageVisibility(route)); - } - - private switchPageVisibility(route: unknown): boolean { - if (route === PAGE_ID.REGISTRATION_PAGE) { - this.view.show(); - this.registerForm.getFirstInputField().getView().getInput().getHTML().focus(); - } else { - this.view.hide(); - return false; - } - return true; - } - public getHTML(): HTMLDivElement { return this.view.getHTML(); } diff --git a/src/pages/RegistrationPage/view/RegistrationPageView.ts b/src/pages/RegistrationPage/view/RegistrationPageView.ts index a43f4bd7..6b9ed460 100644 --- a/src/pages/RegistrationPage/view/RegistrationPageView.ts +++ b/src/pages/RegistrationPage/view/RegistrationPageView.ts @@ -1,6 +1,5 @@ import LinkModel from '@/shared/Link/model/LinkModel.ts'; import getStore from '@/shared/Store/Store.ts'; -import { PAGE_TIMEOUT_DURATION } from '@/shared/constants/animations.ts'; import { PAGE_ANSWER, PAGE_ANSWER_KEYS, @@ -36,6 +35,7 @@ class RegistrationPageView { constructor(parent: HTMLDivElement) { this.parent = parent; + this.parent.innerHTML = ''; this.toLoginPageWrapper = this.createToLoginPageWrapper(); this.registerSpan = this.createRegisterSpan(); this.designElement = this.createDesignElement(); @@ -47,10 +47,9 @@ class RegistrationPageView { } private createAuthDescription(): HTMLHeadingElement { - const { currentLanguage } = getStore().getState(); this.authDescription = createBaseElement({ cssClasses: [styles.authDescription], - innerContent: PAGE_DESCRIPTION[currentLanguage].REGISTRATION, + innerContent: PAGE_DESCRIPTION[getStore().getState().currentLanguage].REGISTRATION, tag: 'h3', }); @@ -100,13 +99,12 @@ class RegistrationPageView { } private createLoginLink(): LinkModel { - const { currentLanguage } = getStore().getState(); this.loginLink = new LinkModel({ attrs: { href: PAGE_ID.LOGIN_PAGE, }, classes: [styles.loginLink], - text: PAGE_LINK_TEXT[currentLanguage].LOGIN, + text: PAGE_LINK_TEXT[getStore().getState().currentLanguage].LOGIN, }); observeCurrentLanguage(this.loginLink.getHTML(), PAGE_LINK_TEXT, PAGE_LINK_TEXT_KEYS.LOGIN); @@ -115,10 +113,9 @@ class RegistrationPageView { } private createRegisterSpan(): HTMLSpanElement { - const { currentLanguage } = getStore().getState(); this.registerSpan = createBaseElement({ cssClasses: [styles.registerSpan], - innerContent: PAGE_LINK_TEXT[currentLanguage].REGISTRATION, + innerContent: PAGE_LINK_TEXT[getStore().getState().currentLanguage].REGISTRATION, tag: 'span', }); @@ -128,10 +125,9 @@ class RegistrationPageView { } private createToLoginPageWrapper(): HTMLSpanElement { - const { currentLanguage } = getStore().getState(); this.toLoginPageWrapper = createBaseElement({ cssClasses: [styles.toLoginPageWrapper], - innerContent: PAGE_ANSWER[currentLanguage].REGISTRATION, + innerContent: PAGE_ANSWER[getStore().getState().currentLanguage].REGISTRATION, tag: 'span', }); @@ -155,17 +151,5 @@ class RegistrationPageView { public getToLoginPageWrapper(): HTMLSpanElement { return this.toLoginPageWrapper; } - - public hide(): boolean { - this.page.classList.add(styles.registrationPage_hidden); - return true; - } - - public show(): boolean { - setTimeout(() => { - this.page.classList.remove(styles.registrationPage_hidden); - }, PAGE_TIMEOUT_DURATION); - return true; - } } export default RegistrationPageView; diff --git a/src/pages/RegistrationPage/view/registrationPageView.module.scss b/src/pages/RegistrationPage/view/registrationPageView.module.scss index 014f647c..4ef414b6 100644 --- a/src/pages/RegistrationPage/view/registrationPageView.module.scss +++ b/src/pages/RegistrationPage/view/registrationPageView.module.scss @@ -3,10 +3,6 @@ display: block; padding: 0 var(--small-offset); animation: show 0.2s ease-out forwards; - - &_hidden { - animation: hide 0.2s ease-in forwards; - } } @keyframes show { @@ -20,17 +16,6 @@ } } -@keyframes hide { - 0% { - opacity: 1; - } - - 100% { - display: none; - opacity: 0; - } -} - .authWrapper { display: grid; grid-template-columns: repeat(2, 1fr); @@ -125,6 +110,7 @@ grid-column: 2 span; grid-row: 4; font: var(--regular-font); + letter-spacing: 1px; text-align: center; color: var(--steam-green-800); gap: calc(var(--extra-small-offset) / 4); diff --git a/src/pages/UserProfilePage/model/UserProfilePageModel.ts b/src/pages/UserProfilePage/model/UserProfilePageModel.ts new file mode 100644 index 00000000..6c531b1d --- /dev/null +++ b/src/pages/UserProfilePage/model/UserProfilePageModel.ts @@ -0,0 +1,26 @@ +import type { Page } from '@/shared/types/common.ts'; + +import getStore from '@/shared/Store/Store.ts'; +import { setCurrentPage } from '@/shared/Store/actions.ts'; +import { PAGE_ID } from '@/shared/constants/pages.ts'; + +import UserProfilePageView from '../view/UserProfilePageView.ts'; + +class UserProfilePageModel implements Page { + private view: UserProfilePageView; + + constructor(parent: HTMLDivElement) { + this.view = new UserProfilePageView(parent); + this.init(); + } + + private init(): void { + getStore().dispatch(setCurrentPage(PAGE_ID.USER_PROFILE_PAGE)); + } + + public getHTML(): HTMLDivElement { + return this.view.getHTML(); + } +} + +export default UserProfilePageModel; diff --git a/src/pages/UserProfilePage/view/UserProfilePageView.ts b/src/pages/UserProfilePage/view/UserProfilePageView.ts new file mode 100644 index 00000000..c2e4a0de --- /dev/null +++ b/src/pages/UserProfilePage/view/UserProfilePageView.ts @@ -0,0 +1,31 @@ +import createBaseElement from '@/shared/utils/createBaseElement.ts'; + +import styles from './userProfilePageView.module.scss'; + +class UserProfilePageView { + private page: HTMLDivElement; + + private parent: HTMLDivElement; + + constructor(parent: HTMLDivElement) { + this.parent = parent; + this.parent.innerHTML = ''; + this.page = this.createHTML(); + } + + private createHTML(): HTMLDivElement { + this.page = createBaseElement({ + cssClasses: [styles.userProfilePage], + tag: 'div', + }); + + this.parent.append(this.page); + + return this.page; + } + + public getHTML(): HTMLDivElement { + return this.page; + } +} +export default UserProfilePageView; diff --git a/src/pages/UserProfilePage/view/userProfilePageView.module.scss b/src/pages/UserProfilePage/view/userProfilePageView.module.scss new file mode 100644 index 00000000..0e6a610d --- /dev/null +++ b/src/pages/UserProfilePage/view/userProfilePageView.module.scss @@ -0,0 +1,17 @@ +.userProfilePage { + position: relative; + display: block; + padding: 0 var(--small-offset); + animation: show 0.2s ease-out forwards; +} + +@keyframes show { + 0% { + opacity: 0; + } + + 100% { + display: block; + opacity: 1; + } +} diff --git a/src/shared/API/customer/model/CustomerModel.ts b/src/shared/API/customer/model/CustomerModel.ts index ae008726..872e5389 100644 --- a/src/shared/API/customer/model/CustomerModel.ts +++ b/src/shared/API/customer/model/CustomerModel.ts @@ -6,7 +6,7 @@ import type { Customer, CustomerPagedQueryResponse, CustomerSignInResult, - CustomerUpdateAction, + MyCustomerUpdateAction, } from '@commercetools/platform-sdk'; import getRoot, { type RootApi } from '../../sdk/root.ts'; @@ -24,51 +24,51 @@ export class CustomerModel { this.root = getRoot(); } - public static actionAddAddress(address: Address): CustomerUpdateAction { + public static actionAddAddress(address: Address): MyCustomerUpdateAction { return { action: 'addAddress', address: CustomerModel.adaptAddressToServer(address) }; } - public static actionEditAddress(address: Address): CustomerUpdateAction { + public static actionEditAddress(address: Address): MyCustomerUpdateAction { return { action: 'changeAddress', address: CustomerModel.adaptAddressToServer(address), addressId: address.id }; } - public static actionEditDateOfBirth(dateOfBirth: string): CustomerUpdateAction { + public static actionEditDateOfBirth(dateOfBirth: string): MyCustomerUpdateAction { return { action: 'setDateOfBirth', dateOfBirth }; } - public static actionEditDefaultBillingAddress(addressId: string): CustomerUpdateAction { + public static actionEditDefaultBillingAddress(addressId: string): MyCustomerUpdateAction { return { action: 'setDefaultBillingAddress', addressId }; } - public static actionEditDefaultShippingAddress(addressId: string): CustomerUpdateAction { + public static actionEditDefaultShippingAddress(addressId: string): MyCustomerUpdateAction { return { action: 'setDefaultShippingAddress', addressId }; } - public static actionEditEmail(email: string): CustomerUpdateAction { + public static actionEditEmail(email: string): MyCustomerUpdateAction { return { action: 'changeEmail', email }; } - public static actionEditFirstName(firstName: string): CustomerUpdateAction { + public static actionEditFirstName(firstName: string): MyCustomerUpdateAction { return { action: 'setFirstName', firstName }; } - public static actionEditLastName(lastName: string): CustomerUpdateAction { + public static actionEditLastName(lastName: string): MyCustomerUpdateAction { return { action: 'setLastName', lastName }; } - public static actionRemoveAddress(address: Address): CustomerUpdateAction { + public static actionRemoveAddress(address: Address): MyCustomerUpdateAction { return { action: 'removeAddress', addressId: address.id }; } - public static actionRemoveBillingAddress(address: Address): CustomerUpdateAction { + public static actionRemoveBillingAddress(address: Address): MyCustomerUpdateAction { return { action: 'removeBillingAddressId', addressId: address.id }; } - public static actionRemoveShippingAddress(address: Address): CustomerUpdateAction { + public static actionRemoveShippingAddress(address: Address): MyCustomerUpdateAction { return { action: 'removeShippingAddressId', addressId: address.id }; } - public static actionSetLocale(locale: string): CustomerUpdateAction { + public static actionSetLocale(locale: string): MyCustomerUpdateAction { return { action: 'setLocale', locale }; } @@ -181,18 +181,13 @@ export class CustomerModel { return this.getCustomerFromData(data) !== null; } - public async editCustomer(actions: CustomerUpdateAction[], customer: User): Promise { - const data = await this.root.editCustomer(actions, customer.version, customer.id); + public async editCustomer(actions: MyCustomerUpdateAction[], customer: User): Promise { + const data = await this.root.editCustomer(actions, customer.version); return this.getCustomerFromData(data); } public async editPassword(customer: User, currentPassword: string, newPassword: string): Promise { - const data = await this.root.editPassword(customer.id, customer.version, currentPassword, newPassword); - return this.getCustomerFromData(data); - } - - public async getCustomerByID(id: string): Promise { - const data = await this.root.getCustomerByID(id); + const data = await this.root.editPassword(customer.version, currentPassword, newPassword); return this.getCustomerFromData(data); } @@ -201,6 +196,10 @@ export class CustomerModel { return this.getCustomerFromData(data); } + public logout(): boolean { + return this.root.logoutUser(); + } + public async registerNewCustomer(userData: User): Promise { const data = await this.root.registrationUser(userData); return this.getCustomerFromData(data); diff --git a/src/shared/API/customer/tests/Customer.spec.ts b/src/shared/API/customer/tests/Customer.spec.ts index da249c1d..a1970053 100644 --- a/src/shared/API/customer/tests/Customer.spec.ts +++ b/src/shared/API/customer/tests/Customer.spec.ts @@ -74,8 +74,22 @@ describe('Checking Customer Model', () => { expect(customer).toHaveProperty('version'); }); + it('should authenticate the customer', async () => { + auth = await customerModel.authCustomer(user); + expect(typeof auth).toBe('object'); + expect(auth).toHaveProperty('addresses'); + expect(auth).toHaveProperty('defaultBillingAddressId'); + expect(auth).toHaveProperty('defaultShippingAddressId'); + expect(auth).toHaveProperty('email'); + expect(auth).toHaveProperty('firstName'); + expect(auth).toHaveProperty('id'); + expect(auth).toHaveProperty('lastName'); + expect(auth).toHaveProperty('password'); + expect(auth).toHaveProperty('version'); + }); + it('should edit the customer', async () => { - if (customer) { + if (auth) { editCustomer = await customerModel.editCustomer( [ CustomerModel.actionEditFirstName('John'), @@ -84,7 +98,7 @@ describe('Checking Customer Model', () => { CustomerModel.actionAddAddress(address), CustomerModel.actionEditDateOfBirth('1990-01-01'), ], - customer, + auth, ); expect(typeof editCustomer).toBe('object'); expect(editCustomer).toHaveProperty('addresses'); @@ -128,39 +142,9 @@ describe('Checking Customer Model', () => { } }); - it('should found the customer by id', async () => { - if (customer) { - const getCustomer = await customerModel.getCustomerByID(customer.id); - expect(typeof getCustomer).toBe('object'); - expect(getCustomer).toHaveProperty('addresses'); - expect(getCustomer).toHaveProperty('defaultBillingAddressId'); - expect(getCustomer).toHaveProperty('defaultShippingAddressId'); - expect(getCustomer).toHaveProperty('email'); - expect(getCustomer).toHaveProperty('firstName'); - expect(getCustomer).toHaveProperty('id'); - expect(getCustomer).toHaveProperty('lastName'); - expect(getCustomer).toHaveProperty('password'); - expect(getCustomer).toHaveProperty('version'); - } - }); - - it('should authenticate the customer', async () => { - auth = await customerModel.authCustomer({ email: 'test-test-test@example.com', password: 'Qqq11' }); - expect(typeof auth).toBe('object'); - expect(auth).toHaveProperty('addresses'); - expect(auth).toHaveProperty('defaultBillingAddressId'); - expect(auth).toHaveProperty('defaultShippingAddressId'); - expect(auth).toHaveProperty('email'); - expect(auth).toHaveProperty('firstName'); - expect(auth).toHaveProperty('id'); - expect(auth).toHaveProperty('lastName'); - expect(auth).toHaveProperty('password'); - expect(auth).toHaveProperty('version'); - }); - it('should edit the customer password', async () => { - if (auth) { - editPassword = await customerModel.editPassword(auth, 'Qqq11', 'Qqq11'); + if (editAddress) { + editPassword = await customerModel.editPassword(editAddress, 'Qqq11', 'Qqq11'); expect(typeof editPassword).toBe('object'); expect(editPassword).toHaveProperty('addresses'); expect(editPassword).toHaveProperty('defaultBillingAddressId'); diff --git a/src/shared/API/sdk/client.ts b/src/shared/API/sdk/client.ts index 365ff021..bbb8766a 100644 --- a/src/shared/API/sdk/client.ts +++ b/src/shared/API/sdk/client.ts @@ -1,34 +1,167 @@ +import type { UserCredentials } from '@/shared/types/user'; + +import { type ByProjectKeyRequestBuilder, createApiBuilderFromCtpClient } from '@commercetools/platform-sdk'; import { type AuthMiddlewareOptions, type Client, ClientBuilder, type HttpMiddlewareOptions, + type Middleware, + type PasswordAuthMiddlewareOptions, + type RefreshAuthMiddlewareOptions, + createAuthForAnonymousSessionFlow, + createAuthForClientCredentialsFlow, + createAuthForPasswordFlow, } from '@commercetools/sdk-client-v2'; +import { TokenType } from '../types/type.ts'; +import getTokenCache from './token-cache/token-cache.ts'; + const URL_AUTH = 'https://auth.europe-west1.gcp.commercetools.com'; const URL_HTTP = 'https://api.europe-west1.gcp.commercetools.com'; +const USE_SAVE_TOKEN = false; const httpMiddlewareOptions: HttpMiddlewareOptions = { fetch, host: URL_HTTP, }; -export default function client(projectKey: string, clientID: string, clientSecret: string, scopes: string): Client { - const scopesArr = scopes.split(','); - const authMiddlewareOptions: AuthMiddlewareOptions = { - credentials: { - clientId: clientID, - clientSecret, - }, - fetch, - host: URL_AUTH, - projectKey, - scopes: scopesArr, - }; - - return new ClientBuilder() - .withProjectKey(projectKey) - .withHttpMiddleware(httpMiddlewareOptions) - .withClientCredentialsFlow(authMiddlewareOptions) - .build(); +export default class ApiClient { + private adminConnection: ByProjectKeyRequestBuilder; + + private anonymConnection: ByProjectKeyRequestBuilder | null = null; + + private authConnection: ByProjectKeyRequestBuilder | null = null; + + private clientID: string; + + private clientSecret: string; + + private isAuth = false; + + private projectKey: string; + + private scopes: string[]; + + constructor(projectKey: string, clientID: string, clientSecret: string, scopes: string) { + this.projectKey = projectKey; + this.clientID = clientID; + this.clientSecret = clientSecret; + this.scopes = scopes.split(','); + + this.anonymConnection = this.createAnonymConnection(); + + const adminOptions = createAuthForClientCredentialsFlow({ + credentials: { + clientId: this.clientID, + clientSecret: this.clientSecret, + }, + fetch, + host: URL_AUTH, + projectKey: this.projectKey, + scopes: this.scopes, + }); + + const adminClient = this.getAdminClient(adminOptions); + + this.adminConnection = this.getConnection(adminClient); + } + + private getAdminClient(middleware: Middleware): Client { + return new ClientBuilder() + .withProjectKey(this.projectKey) + .withHttpMiddleware(httpMiddlewareOptions) + .withMiddleware(middleware) + .build(); + } + + private getAuthOption(credentials: UserCredentials): Middleware { + const { email, password } = credentials || { email: '', password: '' }; + const defaultOptions = this.getDefaultOptions(TokenType.AUTH); + const authOptions: PasswordAuthMiddlewareOptions = { + ...defaultOptions, + credentials: { + ...defaultOptions.credentials, + user: { + password, + username: email, + }, + }, + }; + return createAuthForPasswordFlow(authOptions); + } + + private getClient(middleware: Middleware, token: TokenType): Client { + const defaultOptions = this.getDefaultOptions(token); + const opt: RefreshAuthMiddlewareOptions = { + ...defaultOptions, + refreshToken: token, + }; + return new ClientBuilder() + .withProjectKey(this.projectKey) + .withHttpMiddleware(httpMiddlewareOptions) + .withMiddleware(middleware) + .withRefreshTokenFlow(opt) + .build(); + } + + private getConnection(client: Client): ByProjectKeyRequestBuilder { + return createApiBuilderFromCtpClient(client).withProjectKey({ projectKey: this.projectKey }); + } + + private getDefaultOptions(tokenType: TokenType): AuthMiddlewareOptions { + return { + credentials: { + clientId: this.clientID, + clientSecret: this.clientSecret, + }, + fetch, + host: URL_AUTH, + projectKey: this.projectKey, + scopes: this.scopes, + tokenCache: USE_SAVE_TOKEN ? getTokenCache(tokenType) : undefined, + }; + } + + public adminRoot(): ByProjectKeyRequestBuilder { + return this.adminConnection; + } + + public apiRoot(): ByProjectKeyRequestBuilder { + let client = this.authConnection && this.isAuth ? this.authConnection : this.anonymConnection; + if (!client) { + client = this.createAnonymConnection(); + } + return client; + } + + public approveAuth(): boolean { + this.isAuth = true; + return this.isAuth; + } + + public createAnonymConnection(): ByProjectKeyRequestBuilder { + const defaultOptions = this.getDefaultOptions(TokenType.ANONYM); + const anonymOptions = createAuthForAnonymousSessionFlow(defaultOptions); + const anonymClient = this.getClient(anonymOptions, TokenType.ANONYM); + this.anonymConnection = this.getConnection(anonymClient); + + return this.anonymConnection; + } + + public createAuthConnection(credentials: UserCredentials): ByProjectKeyRequestBuilder { + if (!this.authConnection || (this.authConnection && !this.isAuth)) { + const authOptions = this.getAuthOption(credentials); + const authClient = this.getClient(authOptions, TokenType.AUTH); + this.authConnection = this.getConnection(authClient); + } + return this.authConnection; + } + + public deleteAuthConnection(): boolean { + this.authConnection = null; + this.isAuth = false; + this.anonymConnection = this.createAnonymConnection(); + return this.authConnection === null; + } } diff --git a/src/shared/API/sdk/root.ts b/src/shared/API/sdk/root.ts index af0afc5b..04caa26a 100644 --- a/src/shared/API/sdk/root.ts +++ b/src/shared/API/sdk/root.ts @@ -2,24 +2,22 @@ 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, type ClientResponse, type Customer, type CustomerPagedQueryResponse, type CustomerSignInResult, - type CustomerUpdateAction, + type MyCustomerUpdateAction, type Product, type ProductProjectionPagedQueryResponse, type ProductProjectionPagedSearchResponse, - createApiBuilderFromCtpClient, } from '@commercetools/platform-sdk'; -import { type Client } from '@commercetools/sdk-client-v2'; - -import type { OptionsRequest } from '../types/type.ts'; import makeSortRequest from '../product/utils/sort.ts'; -import client from './client.ts'; +import { type OptionsRequest, TokenType } from '../types/type.ts'; +import { isErrorResponse } from '../types/validation.ts'; +import ApiClient from './client.ts'; +import getTokenCache from './token-cache/token-cache.ts'; type Nullable = T | null; @@ -36,9 +34,7 @@ const clientID = import.meta.env.VITE_APP_CTP_CLIENT_ID; const clientSecret = import.meta.env.VITE_APP_CTP_CLIENT_SECRET; export class RootApi { - private client: Client; - - private connection: ByProjectKeyRequestBuilder; + private client: ApiClient; private credentials: Credentials; @@ -50,60 +46,56 @@ export class RootApi { scopes, }; - this.client = client( + this.client = new ApiClient( this.credentials.projectKey || '', this.credentials.clientID || '', this.credentials.clientSecret || '', this.credentials.scopes || '', ); - - this.connection = this.root(this.client, projectKey); - } - - private root(client: Client, projectKey: string): ByProjectKeyRequestBuilder { - return createApiBuilderFromCtpClient(client).withProjectKey({ projectKey }); } public async authenticateUser(userLoginData: UserCredentials): Promise> { - const data = await this.connection.login().post({ body: userLoginData }).execute(); + this.client.createAuthConnection(userLoginData); + const data = await this.client.apiRoot().me().login().post({ body: userLoginData }).execute(); + if (!isErrorResponse(data)) { + this.client.approveAuth(); + } return data; } public async deleteCustomer(ID: string, version: number): Promise> { - const data = await this.connection.customers().withId({ ID }).delete({ queryArgs: { version } }).execute(); + const data = await this.client.adminRoot().customers().withId({ ID }).delete({ queryArgs: { version } }).execute(); + this.logoutUser(); return data; } - public async editCustomer( - actions: CustomerUpdateAction[], - version: number, - ID: string, - ): Promise> { - const data = await this.connection.customers().withId({ ID }).post({ body: { actions, version } }).execute(); + public async editCustomer(actions: MyCustomerUpdateAction[], version: number): Promise> { + const data = await this.client.apiRoot().me().post({ body: { actions, version } }).execute(); return data; } public async editPassword( - id: string, version: number, currentPassword: string, newPassword: string, ): Promise> { - const data = await this.connection - .customers() + const data = await this.client + .apiRoot() + .me() .password() - .post({ body: { currentPassword, id, newPassword, version } }) + .post({ body: { currentPassword, newPassword, version } }) .execute(); return data; } public async getCategories(): Promise> { - const data = await this.connection.categories().get().execute(); + const data = await this.client.apiRoot().categories().get().execute(); return data; } public async getCategoriesProductCount(): Promise> { - const data = await this.connection + const data = await this.client + .apiRoot() .productProjections() .search() .get({ @@ -117,20 +109,17 @@ export class RootApi { } public async getCustomerByEmail(email: string): Promise> { - const data = await this.connection + const data = await this.client + .apiRoot() .customers() .get({ queryArgs: { where: `email="${email}"` } }) .execute(); return data; } - public async getCustomerByID(ID: string): Promise> { - const data = await this.connection.customers().withId({ ID }).get().execute(); - return data; - } - public async getPriceRange(): Promise> { - const data = await this.connection + const data = await this.client + .apiRoot() .productProjections() .search() .get({ @@ -144,14 +133,15 @@ export class RootApi { } public async getProductByID(ID: string): Promise> { - const data = await this.connection.products().withId({ ID }).get().execute(); + const data = await this.client.apiRoot().products().withId({ ID }).get().execute(); return data; } public async getProducts(options?: OptionsRequest): Promise> { const { filter, limit = PRODUCT_LIMIT, page = DEFAULT_PAGE, search, sort } = options || {}; - const data = await this.connection + const data = await this.client + .apiRoot() .productProjections() .search() .get({ @@ -171,7 +161,8 @@ export class RootApi { } public async getSizeProductCount(): Promise> { - const data = await this.connection + const data = await this.client + .apiRoot() .productProjections() .search() .get({ @@ -184,12 +175,22 @@ export class RootApi { return data; } + public logoutUser(): boolean { + getTokenCache(TokenType.AUTH).clear(); + return this.client.deleteAuthConnection(); + } + public async registrationUser(userData: User): Promise> { const userCredentials = { email: userData.email, password: userData.password, }; - const data = await this.connection.customers().post({ body: userCredentials }).execute(); + + const data = await this.client.apiRoot().me().signup().post({ body: userCredentials }).execute(); + if (!isErrorResponse(data)) { + this.client.createAuthConnection(userData); + this.client.approveAuth(); + } return data; } } diff --git a/src/shared/API/sdk/token-cache/token-cache.ts b/src/shared/API/sdk/token-cache/token-cache.ts new file mode 100644 index 00000000..1cbc74d5 --- /dev/null +++ b/src/shared/API/sdk/token-cache/token-cache.ts @@ -0,0 +1,66 @@ +import type { TokenCache, TokenStore } from '@commercetools/sdk-client-v2'; + +import Cookies from 'js-cookie'; + +import { TokenType } from '../../types/type.ts'; + +export class MyTokenCache implements TokenCache { + private myCache: TokenStore = { + expirationTime: 0, + refreshToken: undefined, + token: '', + }; + + private name: string; + + constructor(name: string) { + this.name = name; + const token = Cookies.get(`${this.name}-token`); + const expirationTime = Number(Cookies.get(`${this.name}-expirationTime`)); + const refreshToken = Cookies.get(`${this.name}-refreshToken`); + if (token && refreshToken) { + this.myCache = { expirationTime, refreshToken, token }; + } + } + + private saveToken(): void { + if (this.myCache.token) { + Cookies.set(`${this.name}-token`, this.myCache.token); + Cookies.set(`${this.name}-expirationTime`, this.myCache.expirationTime.toString()); + Cookies.set(`${this.name}-refreshToken`, this.myCache.refreshToken || ''); + } + } + + public clear(): void { + this.myCache = { + expirationTime: 0, + refreshToken: undefined, + token: '', + }; + Cookies.remove(`${this.name}-token`); + Cookies.remove(`${this.name}-expirationTime`); + Cookies.remove(`${this.name}-refreshToken`); + } + + public get(): TokenStore { + return this.myCache; + } + + public isExist(): boolean { + return this.myCache.token !== '' || this.myCache.refreshToken !== undefined; + } + + public set(newCache: TokenStore): void { + Object.assign(this.myCache, newCache); + this.saveToken(); + } +} + +const createTokenCache = (name: string): MyTokenCache => new MyTokenCache(name); + +const anonymTokenCache = createTokenCache(TokenType.ANONYM); +const authTokenCache = createTokenCache(TokenType.AUTH); + +export default function getTokenCache(tokenType?: TokenType): MyTokenCache { + return tokenType === TokenType.AUTH || authTokenCache.isExist() ? authTokenCache : anonymTokenCache; +} diff --git a/src/shared/API/types/type.ts b/src/shared/API/types/type.ts index ef89c925..fc4c83d1 100644 --- a/src/shared/API/types/type.ts +++ b/src/shared/API/types/type.ts @@ -5,6 +5,11 @@ export const Attribute = { SIZE: 'size', }; +export enum TokenType { + ANONYM = 'anonym', + AUTH = 'auth', +} + export enum FilterFields { CATEGORY = 'categories.id', NEW_ARRIVAL = 'variants.attributes.new_arrival:true', diff --git a/src/shared/API/types/validation.ts b/src/shared/API/types/validation.ts index d5bd9a31..ce2bf6ec 100644 --- a/src/shared/API/types/validation.ts +++ b/src/shared/API/types/validation.ts @@ -5,6 +5,7 @@ import type { Customer, CustomerPagedQueryResponse, CustomerSignInResult, + ErrorResponse, FacetRange, FacetTerm, LocalizedString, @@ -180,3 +181,15 @@ export function isFacetTerm(data: unknown): data is FacetTerm { ('count' in data || 'productCount' in data), ); } + +export function isErrorResponse(data: unknown): data is ErrorResponse { + return Boolean( + typeof data === 'object' && + data && + 'statusCode' in data && + typeof data.statusCode === 'number' && + data.statusCode >= 400 && + 'message' in data && + typeof data.message === 'string', + ); +} diff --git a/src/shared/EventMediator/model/EventMediatorModel.ts b/src/shared/EventMediator/model/EventMediatorModel.ts deleted file mode 100644 index 1d0924dd..00000000 --- a/src/shared/EventMediator/model/EventMediatorModel.ts +++ /dev/null @@ -1,46 +0,0 @@ -import type { ListenerCallback } from '@/shared/types/action'; - -class EventMediatorModel { - private listeners: Map>> = new Map(); - - private static mediator = new EventMediatorModel(); - - public static getInstance(): EventMediatorModel { - return EventMediatorModel.mediator; - } - - public notify(eventName: string, params: T): void { - const eventListeners = this.listeners.get(eventName); - if (eventListeners) { - eventListeners.forEach((listener) => listener(params)); - } - } - - public subscribe(eventName: string, listener: ListenerCallback): void { - if (this.listeners.has(eventName)) { - const listeners = this.listeners.get(eventName); - listeners?.push(listener); - } else { - const newListeners = []; - newListeners.push(listener); - this.listeners.set(eventName, newListeners); - } - } - - public unsubscribe(eventName: string, listener: ListenerCallback): void { - if (this.listeners.has(eventName)) { - const listeners = this.listeners.get(eventName); - const index = listeners?.findIndex((l) => l.toString() === listener.toString()); - - if (index !== undefined && index !== -1) { - listeners?.splice(index, 1); - - if (listeners) { - this.listeners.set(eventName, listeners); - } - } - } - } -} - -export default EventMediatorModel; diff --git a/src/shared/Loader/view/loaderView.module.scss b/src/shared/Loader/view/loaderView.module.scss index 61c8d172..b9ec0cf7 100644 --- a/src/shared/Loader/view/loaderView.module.scss +++ b/src/shared/Loader/view/loaderView.module.scss @@ -7,26 +7,26 @@ } .small { - border: 2px solid var(--noble-gray-200); - border-top: 2px solid var(--steam-green-800); - width: 10px; - height: 10px; -} - -.medium { border: 4px solid var(--noble-gray-200); border-top: 4px solid var(--steam-green-800); width: 20px; height: 20px; } -.large { +.medium { border: 8px solid var(--noble-gray-200); border-top: 8px solid var(--steam-green-800); width: 30px; height: 30px; } +.large { + border: 10px solid var(--noble-gray-200); + border-top: 10px solid var(--steam-green-800); + width: 40px; + height: 40px; +} + @keyframes spin { 0% { transform: rotate(0deg); diff --git a/src/shared/Store/actions.ts b/src/shared/Store/actions.ts index 77494a3d..532b5134 100644 --- a/src/shared/Store/actions.ts +++ b/src/shared/Store/actions.ts @@ -5,9 +5,11 @@ const ACTION = { SET_BILLING_COUNTRY: 'setBillingCountry', SET_CATEGORIES: 'setCategories', SET_CURRENT_LANGUAGE: 'setCurrentLanguage', + SET_CURRENT_PAGE: 'setCurrentPage', SET_CURRENT_USER: 'setCurrentUser', SET_PRODUCTS: 'setProducts', SET_SHIPPING_COUNTRY: 'setShippingCountry', + SWITCH_IS_USER_LOGGED_IN: 'switchIsUserLoggedIn', } as const; export type ActionType = (typeof ACTION)[keyof typeof ACTION]; @@ -48,3 +50,15 @@ export const setCurrentLanguage = ( payload: value, type: ACTION.SET_CURRENT_LANGUAGE, }); + +export const switchIsUserLoggedIn = ( + value: boolean, +): ActionWithPayload => ({ + payload: value, + type: ACTION.SWITCH_IS_USER_LOGGED_IN, +}); + +export const setCurrentPage = (value: string): ActionWithPayload => ({ + payload: value, + type: ACTION.SET_CURRENT_PAGE, +}); diff --git a/src/shared/Store/observer.ts b/src/shared/Store/observer.ts index 2739e96e..2fe20901 100644 --- a/src/shared/Store/observer.ts +++ b/src/shared/Store/observer.ts @@ -26,4 +26,8 @@ export const selectShippingCountry = (state: State): string => state.shippingCou export const selectCurrentLanguage = (state: State): string => state.currentLanguage; +export const selectIsUserLoggedIn = (state: State): boolean => state.isUserLoggedIn; + +export const selectCurrentPage = (state: State): string => state.currentPage; + export default observeStore; diff --git a/src/shared/Store/reducer.ts b/src/shared/Store/reducer.ts index 7b1cbd17..42db3f10 100644 --- a/src/shared/Store/reducer.ts +++ b/src/shared/Store/reducer.ts @@ -8,7 +8,9 @@ export interface State { billingCountry: string; categories: Category[]; currentLanguage: 'en' | 'ru'; + currentPage: string; // TBD Specify type currentUser: User | null; + isUserLoggedIn: boolean; products: Product[]; shippingCountry: string; } @@ -48,6 +50,17 @@ export const rootReducer: Reducer = (state: State, action: Action ...state, currentLanguage: action.payload, }; + + case 'switchIsUserLoggedIn': + return { + ...state, + isUserLoggedIn: action.payload, + }; + case 'setCurrentPage': + return { + ...state, + currentPage: action.payload, + }; default: return state; } diff --git a/src/shared/Store/test.spec.ts b/src/shared/Store/test.spec.ts index e4edd73d..12dd0a27 100644 --- a/src/shared/Store/test.spec.ts +++ b/src/shared/Store/test.spec.ts @@ -91,7 +91,9 @@ vi.mock('./Store.ts', async (importOriginal) => { billingCountry: '', categories: [], currentLanguage: 'en', + currentPage: '', currentUser: null, + isUserLoggedIn: false, products: [], shippingCountry: '', }), @@ -135,20 +137,22 @@ it('observeStore should call select and onChange when state changes', () => { billingCountry: '', categories: [], currentLanguage: 'en', + currentPage: 'main', currentUser: mockUser, + isUserLoggedIn: false, products: [], shippingCountry: '', }; const mockOnChange = vitest.fn(); - const selelectCurrentUserSpy = vitest.spyOn(actions, 'setCurrentUser'); + const selectCurrentUserSpy = vitest.spyOn(actions, 'setCurrentUser'); const mockSelect = vitest.fn(() => selectCurrentUser(mockState)); const unsubscribe = observeStore(mockSelect, mockOnChange); expect(selectCurrentUser(mockState)).toBe(mockUser); actions.setCurrentUser(mockUser); - expect(selelectCurrentUserSpy).toHaveBeenCalledWith(mockUser); + expect(selectCurrentUserSpy).toHaveBeenCalledWith(mockUser); unsubscribe(); }); @@ -161,7 +165,9 @@ describe('rootReducer', () => { billingCountry: '', categories: [], currentLanguage: 'en', + currentPage: '', currentUser: null, + isUserLoggedIn: false, products: [], shippingCountry: '', }; diff --git a/src/shared/constants/animations.ts b/src/shared/constants/animations.ts index b74c279b..5023ac2d 100644 --- a/src/shared/constants/animations.ts +++ b/src/shared/constants/animations.ts @@ -12,6 +12,4 @@ const SERVER_MESSAGE_ANIMATE_DETAILS = { params: SERVER_MESSAGE_ANIMATE_PARAMS, }; -export const PAGE_TIMEOUT_DURATION = 200; - export default SERVER_MESSAGE_ANIMATE_DETAILS; diff --git a/src/shared/constants/buttons.ts b/src/shared/constants/buttons.ts index b5d91fa5..359e7490 100644 --- a/src/shared/constants/buttons.ts +++ b/src/shared/constants/buttons.ts @@ -13,9 +13,9 @@ export const BUTTON_TEXT = { }, ru: { BACK_TO_MAIN: 'Вернуться на главную', - LOG_OUT: 'Выйти', - LOGIN: 'Войти', - REGISTRATION: 'Зарегистрироваться', + LOG_OUT: 'Выйти', + LOGIN: 'Войти', + REGISTRATION: 'Регистрация', }, } as const; @@ -37,3 +37,5 @@ export const LANGUAGE_CHOICE = { EN: 'en', RU: 'ru', } as const; + +export type LanguageChoiceType = (typeof LANGUAGE_CHOICE)[keyof typeof LANGUAGE_CHOICE]; diff --git a/src/shared/constants/events.ts b/src/shared/constants/events.ts index bada8622..ddfb3fcd 100644 --- a/src/shared/constants/events.ts +++ b/src/shared/constants/events.ts @@ -1,6 +1,34 @@ -const MEDIATOR_EVENT = { +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 = { CHANGE_PAGE: 'changePage', USER_LOGIN: 'userLogin', } as const; - -export default MEDIATOR_EVENT; diff --git a/src/shared/constants/forms.ts b/src/shared/constants/forms.ts index d0342ecf..dded1808 100644 --- a/src/shared/constants/forms.ts +++ b/src/shared/constants/forms.ts @@ -11,9 +11,25 @@ export const INPUT_TYPE = { } as const; export const FORM_TEXT = { - DEFAULT_ADDRESS: 'Use as default address', - SINGLE_ADDRESS: 'Use as billing address', -}; + en: { + DEFAULT_BILLING_ADDRESS: 'Use as default for billing', + DEFAULT_SHIPPING_ADDRESS: 'Use as default for shipping', + SINGLE_ADDRESS: 'Use shipping address as billing', + }, + ru: { + DEFAULT_BILLING_ADDRESS: 'Использовать по умолчанию для выставления счетов', + DEFAULT_SHIPPING_ADDRESS: 'Использовать по умолчанию для доставки', + SINGLE_ADDRESS: 'Использовать адрес доставки для выставления счетов', + }, +} as const; + +export const FORM_TEXT_KEYS = { + DEFAULT_BILLING_ADDRESS: 'DEFAULT_BILLING_ADDRESS', + DEFAULT_SHIPPING_ADDRESS: 'DEFAULT_SHIPPING_ADDRESS', + SINGLE_ADDRESS: 'SINGLE_ADDRESS', +} as const; + +export type FormTextKeysType = (typeof FORM_TEXT_KEYS)[keyof typeof FORM_TEXT_KEYS]; export const USER_COUNTRY_ADDRESS = { BILLING: 'billingCountry', diff --git a/src/shared/constants/forms/login/fieldParams.ts b/src/shared/constants/forms/login/fieldParams.ts index e10374f4..f2144161 100644 --- a/src/shared/constants/forms/login/fieldParams.ts +++ b/src/shared/constants/forms/login/fieldParams.ts @@ -9,7 +9,10 @@ export const EMAIL_FIELD = { }, labelParams: { for: `${KEY}email`, - text: '', + text: { + en: '', + ru: '', + }, }, } as const; @@ -22,7 +25,10 @@ export const PASSWORD_FIELD = { }, labelParams: { for: `${KEY}password`, - text: '', + text: { + en: '', + ru: '', + }, }, } as const; diff --git a/src/shared/constants/forms/login/validationParams.ts b/src/shared/constants/forms/login/validationParams.ts index 136bdf88..1ca8baf5 100644 --- a/src/shared/constants/forms/login/validationParams.ts +++ b/src/shared/constants/forms/login/validationParams.ts @@ -3,14 +3,14 @@ import KEY from './constants.ts'; export const EMAIL_FIELD_VALIDATE = { key: `${KEY}email`, notWhitespace: { - messages: { en: 'Email must not contain white spaces', ru: 'Почтовый адрес не может содержать пробелы' }, + messages: { en: 'Email must not contain white spaces', ru: 'Адрес электронной почты не должен содержать пробелы' }, pattern: /^\S+$/, }, required: true, validMail: { messages: { en: 'Enter correct email (user@example.com)', - ru: 'Введите корректный почтовый адрес (user@example.com)', + ru: 'Введите правильный адрес электронной почты (user@example.com)', }, pattern: /^([a-z0-9_-]+\.)*[a-z0-9_-]+@[a-z0-9_-]+(\.[a-z0-9_-]+)*\.[a-z]{2,6}$/, }, @@ -20,14 +20,14 @@ export const PASSWORD_FIELD_VALIDATE = { key: `${KEY}password`, minLength: 8, notWhitespace: { - messages: { en: 'Password must not contain white spaces', ru: 'Пароль не может содержать пробелы' }, + messages: { en: 'Password must not contain white spaces', ru: 'Пароль не должен содержать пробелы' }, pattern: /^\S+$/, }, required: true, requiredSymbols: { messages: { en: 'Password must contain English letters, at least one letter in upper and lower case and at least one number', - ru: 'Пароль должен содержать английские буквы, хотя бы одну букву в верхнем регистре и в нижнем регистре и хотя бы одну цифру', + ru: 'Пароль должен содержать английские буквы, как минимум одну букву в верхнем и нижнем регистре, а также хотя бы одну цифру', }, pattern: /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d).+/, }, diff --git a/src/shared/constants/forms/register/constant.ts b/src/shared/constants/forms/register/constant.ts index 0bada779..c46be981 100644 --- a/src/shared/constants/forms/register/constant.ts +++ b/src/shared/constants/forms/register/constant.ts @@ -2,10 +2,10 @@ export const KEY = 'registration_'; export const TITLE_TEXT = { en: { - BILLING_ADDRESS: 'Billing address', + BILLING_ADDRESS: 'Billing Address', CREDENTIALS: 'Credentials', PERSONAL: 'Personal', - SHIPPING_ADDRESS: 'Shipping address', + SHIPPING_ADDRESS: 'Shipping Address', }, ru: { BILLING_ADDRESS: 'Адрес выставления счетов', diff --git a/src/shared/constants/forms/register/fieldParams.ts b/src/shared/constants/forms/register/fieldParams.ts index 489ba1eb..3e795a89 100644 --- a/src/shared/constants/forms/register/fieldParams.ts +++ b/src/shared/constants/forms/register/fieldParams.ts @@ -7,7 +7,10 @@ export const EMAIL = { }, labelParams: { for: 'registration_email', - text: 'Email *', + text: { + en: 'Email', + ru: 'Электронная почта', + }, }, } as const; @@ -20,7 +23,10 @@ export const PASSWORD = { }, labelParams: { for: 'registration_password', - text: 'Password *', + text: { + en: 'Password', + ru: 'Пароль', + }, }, } as const; @@ -33,7 +39,10 @@ export const FIRST_NAME = { }, labelParams: { for: 'firstName', - text: 'First name *', + text: { + en: 'First name', + ru: 'Имя', + }, }, } as const; @@ -46,7 +55,10 @@ export const LAST_NAME = { }, labelParams: { for: 'lastName', - text: 'Last name *', + text: { + en: 'Last name', + ru: 'Фамилия', + }, }, } as const; @@ -60,7 +72,10 @@ export const BIRTHDAY = { }, labelParams: { for: 'birthDate', - text: 'Date of Birth *', + text: { + en: 'Date of birth', + ru: 'Дата рождения', + }, }, } as const; @@ -73,7 +88,10 @@ export const SHIPPING_ADDRESS_STREET = { }, labelParams: { for: 'address', - text: 'Address *', + text: { + en: 'Address', + ru: 'Адрес', + }, }, } as const; @@ -86,7 +104,10 @@ export const SHIPPING_ADDRESS_CITY = { }, labelParams: { for: 'city', - text: 'City *', + text: { + en: 'City', + ru: 'Город', + }, }, } as const; @@ -99,7 +120,10 @@ export const SHIPPING_ADDRESS_COUNTRY = { }, labelParams: { for: 'shippingCountry', - text: 'Country *', + text: { + en: 'Country', + ru: 'Страна', + }, }, } as const; @@ -112,7 +136,10 @@ export const SHIPPING_ADDRESS_POSTAL_CODE = { }, labelParams: { for: 'postalCode', - text: 'Postal code *', + text: { + en: 'Postal code', + ru: 'Почтовый индекс', + }, }, } as const; @@ -125,7 +152,10 @@ export const BILLING_ADDRESS_STREET = { }, labelParams: { for: 'billing_address', - text: 'Address *', + text: { + en: 'Address', + ru: 'Адрес', + }, }, } as const; @@ -138,7 +168,10 @@ export const BILLING_ADDRESS_CITY = { }, labelParams: { for: 'billing_city', - text: 'City *', + text: { + en: 'City', + ru: 'Город', + }, }, } as const; @@ -151,7 +184,10 @@ export const BILLING_ADDRESS_COUNTRY = { }, labelParams: { for: 'billing_country', - text: 'Country *', + text: { + en: 'Country', + ru: 'Страна', + }, }, } as const; @@ -164,7 +200,10 @@ export const BILLING_ADDRESS_POSTAL_CODE = { }, labelParams: { for: 'billing_postalCode', - text: 'Postal code *', + text: { + en: 'Postal code', + ru: 'Почтовый индекс', + }, }, } as const; diff --git a/src/shared/constants/forms/register/validationParams.ts b/src/shared/constants/forms/register/validationParams.ts index 948e18c5..a88fbb31 100644 --- a/src/shared/constants/forms/register/validationParams.ts +++ b/src/shared/constants/forms/register/validationParams.ts @@ -1,14 +1,14 @@ export const EMAIL_VALIDATE = { key: 'registration_email', notWhitespace: { - messages: { en: 'Email must not contain white spaces', ru: 'Почтовый адрес не может содержать пробелы' }, + messages: { en: 'Email must not contain white spaces', ru: 'Адрес электронной почты не должен содержать пробелы' }, pattern: /^\S+$/, }, required: true, validMail: { messages: { en: 'Enter correct email (user@example.com)', - ru: 'Введите корректный почтовый адрес (user@example.com)', + ru: 'Введите правильный адрес электронной почты (user@example.com)', }, pattern: /^([a-z0-9_-]+\.)*[a-z0-9_-]+@[a-z0-9_-]+(\.[a-z0-9_-]+)*\.[a-z]{2,6}$/, }, @@ -18,14 +18,14 @@ export const PASSWORD_VALIDATE = { key: 'registration_password', minLength: 8, notWhitespace: { - messages: { en: 'Password must not contain white spaces', ru: 'Пароль не может содержать пробелы' }, + messages: { en: 'Password must not contain white spaces', ru: 'Пароль не должен содержать пробелы' }, pattern: /^\S+$/, }, required: true, requiredSymbols: { messages: { en: 'Password must contain English letters, at least one letter in upper and lower case and at least one number', - ru: 'Пароль должен содержать английские буквы, хотя бы одну букву в верхнем регистре и в нижнем регистре и хотя бы одну цифру', + ru: 'Пароль должен содержать английские буквы, как минимум одну букву в верхнем и нижнем регистре, а также хотя бы одну цифру', }, pattern: /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d).+/, }, @@ -39,12 +39,12 @@ export const FIRST_NAME_VALIDATE = { en: 'First name must contain only letters', ru: 'Имя должно содержать только буквы', }, - pattern: /^[a-zA-Z]*$/, + pattern: /^[a-zA-Zа-яА-я\s]*$/, }, notWhitespace: { messages: { en: 'First name must not contain white spaces', - ru: 'Имя не может содержать пробелы', + ru: 'Имя не должно содержать пробелы', }, pattern: /^\S+$/, }, @@ -59,12 +59,12 @@ export const LAST_NAME_VALIDATE = { en: 'Last name must contain only letters', ru: 'Фамилия должна содержать только буквы', }, - pattern: /^[a-zA-Z]*$/, + pattern: /^[a-zA-Zа-яА-я\s]*$/, }, notWhitespace: { messages: { en: 'Last name must not contain white spaces', - ru: 'Фамилия не может содержать пробелы', + ru: 'Фамилия не должна содержать пробелы', }, pattern: /^\S+$/, }, @@ -78,7 +78,7 @@ export const BIRTHDAY_VALIDATE = { maxAge: 120, messages: { en: 'Enter correct birthday (01.01.2000)', - ru: 'Введите корректный день рождения (01.01.2000)', + ru: 'Введите правильную дату рождения (01.01.2000)', }, minAge: 18, pattern: /^\d{4}-\d{2}-\d{2}$/, @@ -99,7 +99,7 @@ export const SHIPPING_ADDRESS_CITY_VALIDATE = { en: 'City must contain only letters', ru: 'Город должен содержать только буквы', }, - pattern: /^[a-zA-Z]*$/, + pattern: /^[a-zA-Zа-яА-я\s]*$/, }, required: true, @@ -131,7 +131,7 @@ export const BILLING_ADDRESS_CITY_VALIDATE = { en: 'City must contain only letters', ru: 'Город должен содержать только буквы', }, - pattern: /^[a-zA-Z]*$/, + pattern: /^[a-zA-Zа-яА-я\s]*$/, }, required: true, }; diff --git a/src/shared/constants/initialState.ts b/src/shared/constants/initialState.ts index e70a5248..a4036265 100644 --- a/src/shared/constants/initialState.ts +++ b/src/shared/constants/initialState.ts @@ -4,7 +4,9 @@ const initialState: State = { billingCountry: '', categories: [], currentLanguage: 'en', + currentPage: '', currentUser: null, + isUserLoggedIn: false, products: [], shippingCountry: '', }; diff --git a/src/shared/constants/messages.ts b/src/shared/constants/messages.ts index 48706d4f..37ed82f8 100644 --- a/src/shared/constants/messages.ts +++ b/src/shared/constants/messages.ts @@ -6,12 +6,22 @@ export const MESSAGE_STATUS = { export type MessageStatusType = (typeof MESSAGE_STATUS)[keyof typeof MESSAGE_STATUS]; export const SERVER_MESSAGE = { - BAD_REQUEST: 'Sorry, something went wrong. Try again later.', - INCORRECT_PASSWORD: 'Please, enter a correct password', - INVALID_EMAIL: "User with this email doesn't exist. Please, register first", - SUCCESSFUL_LOGIN: 'Enjoy shopping!', - SUCCESSFUL_REGISTRATION: 'Your registration was successful', - USER_EXISTS: 'User with this email already exists, please check your email', + en: { + BAD_REQUEST: 'Sorry, something went wrong. Try again later.', + INCORRECT_PASSWORD: 'Please, enter a correct password', + INVALID_EMAIL: "User with this email doesn't exist. Please, register first", + SUCCESSFUL_LOGIN: 'Enjoy shopping!', + SUCCESSFUL_REGISTRATION: 'Your registration was successful', + USER_EXISTS: 'User with this email already exists, please check your email', + }, + ru: { + BAD_REQUEST: 'Извините, что-то пошло не так. Попробуйте позже.', + INCORRECT_PASSWORD: 'Пожалуйста, введите правильный пароль', + INVALID_EMAIL: 'Пользователь с таким адресом не существует. Пожалуйста, сначала зарегистрируйтесь', + SUCCESSFUL_LOGIN: 'Приятных покупок!', + SUCCESSFUL_REGISTRATION: 'Регистрация прошла успешно', + USER_EXISTS: 'Пользователь с таким адресом уже существует, пожалуйста, проверьте свою почту', + }, } as const; export const ERROR_MESSAGE = { @@ -22,10 +32,10 @@ export const ERROR_MESSAGE = { WRONG_REGION: "Sorry, we don't deliver to your region yet", }, ru: { - INVALID_COUNTRY: 'Неверный код страны', + INVALID_COUNTRY: 'Неверная страна', INVALID_POSTAL_CODE: 'Неверный почтовый индекс', REQUIRED_FIELD: 'Поле обязательно для заполнения', - WRONG_REGION: 'Извините, мы не доставляем в вашу область', + WRONG_REGION: 'Извините, но мы еще не доставляем в ваш регион', }, } as const; diff --git a/src/shared/constants/pages.ts b/src/shared/constants/pages.ts index 4d148022..87f4d57c 100644 --- a/src/shared/constants/pages.ts +++ b/src/shared/constants/pages.ts @@ -1,10 +1,14 @@ export const PAGE_LINK_TEXT = { en: { + ABOUT: 'About us', + CATALOG: 'Catalog', LOGIN: 'Login', MAIN: 'Main', REGISTRATION: 'Register', }, ru: { + ABOUT: 'О нас', + CATALOG: 'Каталог', LOGIN: 'Вход', MAIN: 'Главная', REGISTRATION: 'Регистрация', @@ -12,6 +16,8 @@ export const PAGE_LINK_TEXT = { } as const; export const PAGE_LINK_TEXT_KEYS = { + ABOUT: 'ABOUT', + CATALOG: 'CATALOG', LOGIN: 'LOGIN', MAIN: 'MAIN', REGISTRATION: 'REGISTRATION', @@ -25,10 +31,10 @@ export const PAGE_DESCRIPTION = { REGISTRATION: 'Enter your information to register.', }, ru: { - 404: 'Это не та страница, которую ты ищешь. Пожалуйста, вернись на главную страницу.', + 404: 'Это не та страница, которую вы ищете. Пожалуйста, вернитесь на главную страницу.', GREETING: 'Привет, ', - LOGIN: 'Введите почту и пароль для входа.', - REGISTRATION: 'Введите информацию о себе для регистрации.', + LOGIN: 'Введите свой адрес электронной почты и пароль для входа.', + REGISTRATION: 'Введите свои данные для регистрации.', }, } as const; @@ -58,9 +64,14 @@ export const PAGE_ANSWER_KEYS = { } as const; export const PAGE_ID = { + ABOUT_US_PAGE: 'about', + CART_PAGE: 'cart', + CATALOG_PAGE: 'catalog', DEFAULT_PAGE: '', + ITEM_PAGE: 'item', LOGIN_PAGE: 'login', MAIN_PAGE: 'main', NOT_FOUND_PAGE: '404', REGISTRATION_PAGE: 'register', + USER_PROFILE_PAGE: 'profile', } as const; diff --git a/src/shared/constants/svg.ts b/src/shared/constants/svg.ts index 0204c3fe..73a63103 100644 --- a/src/shared/constants/svg.ts +++ b/src/shared/constants/svg.ts @@ -1,9 +1,14 @@ const SVG_DETAILS = { + CART: 'cart', CLOSE_EYE: 'closeEye', LOGO: 'logo', OPEN_EYE: 'openEye', - + PROFILE: 'userCircle', SVG_URL: 'http://www.w3.org/2000/svg', + SWITCH_LANGUAGE: { + en: 'en', + ru: 'ru', + }, } as const; export default SVG_DETAILS; diff --git a/src/shared/img/svg/cart.svg b/src/shared/img/svg/cart.svg new file mode 100644 index 00000000..8ced04de --- /dev/null +++ b/src/shared/img/svg/cart.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/shared/img/svg/cat.svg b/src/shared/img/svg/cat.svg deleted file mode 100644 index d95e2f33..00000000 --- a/src/shared/img/svg/cat.svg +++ /dev/null @@ -1,256 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/shared/img/svg/dark.svg b/src/shared/img/svg/dark.svg new file mode 100644 index 00000000..52a09c06 --- /dev/null +++ b/src/shared/img/svg/dark.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/shared/img/svg/drop.svg b/src/shared/img/svg/drop.svg new file mode 100644 index 00000000..42d5a616 --- /dev/null +++ b/src/shared/img/svg/drop.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/src/shared/img/svg/en.svg b/src/shared/img/svg/en.svg new file mode 100644 index 00000000..e45598c9 --- /dev/null +++ b/src/shared/img/svg/en.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/shared/img/svg/heartFill.svg b/src/shared/img/svg/heartFill.svg new file mode 100644 index 00000000..a966541e --- /dev/null +++ b/src/shared/img/svg/heartFill.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/shared/img/svg/heartOutline.svg b/src/shared/img/svg/heartOutline.svg new file mode 100644 index 00000000..f03bfb5c --- /dev/null +++ b/src/shared/img/svg/heartOutline.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/shared/img/svg/house.svg b/src/shared/img/svg/house.svg new file mode 100644 index 00000000..05834259 --- /dev/null +++ b/src/shared/img/svg/house.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/shared/img/svg/light.svg b/src/shared/img/svg/light.svg new file mode 100644 index 00000000..f22381e4 --- /dev/null +++ b/src/shared/img/svg/light.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/src/shared/img/svg/plant.svg b/src/shared/img/svg/plant.svg new file mode 100644 index 00000000..cecc5817 --- /dev/null +++ b/src/shared/img/svg/plant.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/src/shared/img/svg/ru.svg b/src/shared/img/svg/ru.svg new file mode 100644 index 00000000..2fee31a3 --- /dev/null +++ b/src/shared/img/svg/ru.svg @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/shared/img/svg/search.svg b/src/shared/img/svg/search.svg new file mode 100644 index 00000000..05834259 --- /dev/null +++ b/src/shared/img/svg/search.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/shared/img/svg/settings.svg b/src/shared/img/svg/settings.svg new file mode 100644 index 00000000..05834259 --- /dev/null +++ b/src/shared/img/svg/settings.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/shared/img/svg/userCircle.svg b/src/shared/img/svg/userCircle.svg new file mode 100644 index 00000000..d4b2c770 --- /dev/null +++ b/src/shared/img/svg/userCircle.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/shared/types/form.ts b/src/shared/types/form.ts index b4838a8f..d703acf4 100644 --- a/src/shared/types/form.ts +++ b/src/shared/types/form.ts @@ -8,7 +8,10 @@ export interface InputParams { export interface LabelParams { for: string; - text: null | string; + text: { + en: string; + ru: string; + } | null; } export interface InputFieldParams { diff --git a/src/shared/utils/clearOutElement.ts b/src/shared/utils/clearOutElement.ts new file mode 100644 index 00000000..13c9aa14 --- /dev/null +++ b/src/shared/utils/clearOutElement.ts @@ -0,0 +1,10 @@ +const clearOutElement = (...args: HTMLElement[]): void => { + args.forEach((element) => { + if (element instanceof HTMLElement) { + const currentElement = element; + currentElement.innerHTML = ''; + } + }); +}; + +export default clearOutElement; diff --git a/src/shared/utils/formattedText.ts b/src/shared/utils/formattedText.ts new file mode 100644 index 00000000..651faaed --- /dev/null +++ b/src/shared/utils/formattedText.ts @@ -0,0 +1,8 @@ +const formattedText = (text: string): string => + text + .trim() + .split(' ') + .map((word) => (word[0] ? word[0].toUpperCase() + word.slice(1).toLowerCase() : '')) + .join(' '); + +export default formattedText; diff --git a/src/shared/utils/getCountryIndex.ts b/src/shared/utils/getCountryIndex.ts index 34f36250..840594fe 100644 --- a/src/shared/utils/getCountryIndex.ts +++ b/src/shared/utils/getCountryIndex.ts @@ -1,8 +1,13 @@ -import getStore from '../Store/Store.ts'; +import type { LanguageChoiceType } from '../constants/buttons.ts'; + +import { LANGUAGE_CHOICE } from '../constants/buttons.ts'; import COUNTRIES_LIST from '../constants/countriesList.ts'; +export const checkInputLanguage = (text: string): LanguageChoiceType => + /[a-zA-Z]/.test(text) ? LANGUAGE_CHOICE.EN : LANGUAGE_CHOICE.RU; + export default function getCountryIndex(country: string): string { - const { currentLanguage } = getStore().getState(); + const currentLanguage = checkInputLanguage(country); const countryIndex: string = COUNTRIES_LIST[currentLanguage][country]; return countryIndex; } diff --git a/src/shared/utils/messageTemplate.ts b/src/shared/utils/messageTemplate.ts index c1375ef1..6399aced 100644 --- a/src/shared/utils/messageTemplate.ts +++ b/src/shared/utils/messageTemplate.ts @@ -1,5 +1,7 @@ import getStore from '../Store/Store.ts'; import { LANGUAGE_CHOICE } from '../constants/buttons.ts'; +import { SERVER_MESSAGE } from '../constants/messages.ts'; +import { PAGE_DESCRIPTION } from '../constants/pages.ts'; const messageTemplate = (beginning: string, variable: number | string, end: string): string => { const start = beginning ? `${beginning} ` : ''; @@ -7,7 +9,11 @@ const messageTemplate = (beginning: string, variable: number | string, end: stri return `${start}${variable}${ending}`; }; -export const greeting = (name: string): string => messageTemplate('Hi, ', name, '!'); +export const greeting = (name: string): string => + messageTemplate(PAGE_DESCRIPTION[getStore().getState().currentLanguage].GREETING, name, '!'); + +export const createGreetingMessage = (): string => + `${greeting(getStore().getState().currentUser?.firstName ?? '')} ${SERVER_MESSAGE[getStore().getState().currentLanguage].SUCCESSFUL_LOGIN}`; const maxLengthMessageRu = (maxLength: number): string => messageTemplate('Максимальная длина не должна превышать', maxLength, ' символов'); diff --git a/src/shared/utils/observeCurrentLanguage.ts b/src/shared/utils/observeCurrentLanguage.ts index 61fa8de5..f770bc77 100644 --- a/src/shared/utils/observeCurrentLanguage.ts +++ b/src/shared/utils/observeCurrentLanguage.ts @@ -1,11 +1,11 @@ import getStore from '../Store/Store.ts'; import observeStore, { selectCurrentLanguage } from '../Store/observer.ts'; -function observeCurrentLanguage( +const observeCurrentLanguage = ( el: HTMLElement | Node, map: Record>, text: string, -): boolean { +): boolean => { const element = el; const textNode = [...element.childNodes].find((child) => child.nodeType === Node.TEXT_NODE); @@ -15,6 +15,6 @@ function observeCurrentLanguage( }); } return true; -} +}; export default observeCurrentLanguage; diff --git a/src/shared/utils/tests.spec.ts b/src/shared/utils/tests.spec.ts index da0336d0..3fd4bd99 100644 --- a/src/shared/utils/tests.spec.ts +++ b/src/shared/utils/tests.spec.ts @@ -5,6 +5,7 @@ import createSVGUse from './createSVGUse.ts'; import getCountryIndex from './getCountryIndex.ts'; import isKeyOfUserData from './isKeyOfUserData.ts'; import { isNotNullable, isNullable } from './isNullable.ts'; +import observeCurrentLanguage from './observeCurrentLanguage.ts'; import { a, div, h2, h3, iconFromCode, img, input, label, main, span } from './tags.ts'; const baseElement = new BaseElement( @@ -251,3 +252,9 @@ describe('Checking isKeyOfUserData function', () => { expect(isKeyOfUserData(userData, 'email')).toBe(true); }); }); + +describe('Checking observeCurrentLanguage function', () => { + it('should return true', () => { + expect(observeCurrentLanguage(document.body, {}, 'test')).toBe(true); + }); +}); diff --git a/src/widgets/Catalog/model/CatalogModel.ts b/src/widgets/Catalog/model/CatalogModel.ts new file mode 100644 index 00000000..67dd9966 --- /dev/null +++ b/src/widgets/Catalog/model/CatalogModel.ts @@ -0,0 +1,11 @@ +import CatalogView from '../view/CatalogView.ts'; + +class CatalogModel { + private view = new CatalogView(); + + public getHTML(): HTMLDivElement { + return this.view.getHTML(); + } +} + +export default CatalogModel; diff --git a/src/widgets/Catalog/view/CatalogView.ts b/src/widgets/Catalog/view/CatalogView.ts new file mode 100644 index 00000000..86397c82 --- /dev/null +++ b/src/widgets/Catalog/view/CatalogView.ts @@ -0,0 +1,41 @@ +import createBaseElement from '@/shared/utils/createBaseElement.ts'; + +import styles from './catalogView.module.scss'; + +class CatalogView { + private catalog: HTMLDivElement; + + private itemsList: HTMLUListElement; + + constructor() { + this.itemsList = this.createItemsList(); + this.catalog = this.createHTML(); + } + + private createHTML(): HTMLDivElement { + this.catalog = createBaseElement({ + cssClasses: [styles.catalog], + tag: 'div', + }); + this.catalog.append(this.itemsList); + return this.catalog; + } + + private createItemsList(): HTMLUListElement { + this.itemsList = createBaseElement({ + cssClasses: [styles.itemsList], + tag: 'ul', + }); + return this.itemsList; + } + + public getHTML(): HTMLDivElement { + return this.catalog; + } + + public getItemsList(): HTMLUListElement { + return this.itemsList; + } +} + +export default CatalogView; diff --git a/src/widgets/Catalog/view/catalogView.module.scss b/src/widgets/Catalog/view/catalogView.module.scss new file mode 100644 index 00000000..3c4d88cc --- /dev/null +++ b/src/widgets/Catalog/view/catalogView.module.scss @@ -0,0 +1,12 @@ +// .catalog { +// display: flex; +// width: 500px; +// height: 500px; +// background-color: var(--noble-gray-300); +// } + +// .itemsList { +// width: 500px; +// height: 500px; +// background-color: var(--noble-gray-800); +// } diff --git a/src/widgets/Footer/model/FooterModel.ts b/src/widgets/Footer/model/FooterModel.ts index 32072b08..6a434ed7 100644 --- a/src/widgets/Footer/model/FooterModel.ts +++ b/src/widgets/Footer/model/FooterModel.ts @@ -15,7 +15,7 @@ class FooterModel { } private init(): boolean { - this.getHTML().append(this.navigation.getHTML()); + this.view.getWrapper().append(this.navigation.getHTML()); return true; } diff --git a/src/widgets/Footer/view/FooterView.ts b/src/widgets/Footer/view/FooterView.ts index d3359cba..77d5cb9b 100644 --- a/src/widgets/Footer/view/FooterView.ts +++ b/src/widgets/Footer/view/FooterView.ts @@ -5,7 +5,10 @@ import styles from './footerView.module.scss'; class FooterView { private footer: HTMLElement; + private wrapper: HTMLDivElement; + constructor() { + this.wrapper = this.createWrapper(); this.footer = this.createHTML(); } @@ -14,12 +17,27 @@ class FooterView { cssClasses: [styles.footer], tag: 'footer', }); + + this.footer.append(this.wrapper); return this.footer; } + private createWrapper(): HTMLDivElement { + this.wrapper = createBaseElement({ + cssClasses: [styles.wrapper], + tag: 'div', + }); + + return this.wrapper; + } + public getHTML(): HTMLElement { return this.footer; } + + public getWrapper(): HTMLDivElement { + return this.wrapper; + } } export default FooterView; diff --git a/src/widgets/Footer/view/footerView.module.scss b/src/widgets/Footer/view/footerView.module.scss index 5b748941..25683554 100644 --- a/src/widgets/Footer/view/footerView.module.scss +++ b/src/widgets/Footer/view/footerView.module.scss @@ -1,10 +1,13 @@ .footer { + border-top: 1px solid var(--steam-green-300); + padding: 0 var(--small-offset); + width: 100%; + background-color: var(--white); +} + +.wrapper { display: flex; align-items: center; justify-content: center; - margin-top: var(--medium-offset); - border-top: 10px solid var(--steam-green-800); - border-radius: var(--medium-br); - padding: calc(var(--small-offset) / 2) var(--small-offset); - background-color: var(--white); + max-width: 1440px; } diff --git a/src/widgets/Header/model/HeaderModel.ts b/src/widgets/Header/model/HeaderModel.ts index 06adcbbe..fb1606a7 100644 --- a/src/widgets/Header/model/HeaderModel.ts +++ b/src/widgets/Header/model/HeaderModel.ts @@ -1,10 +1,13 @@ import type RouterModel from '@/app/Router/model/RouterModel.ts'; import NavigationModel from '@/entities/Navigation/model/NavigationModel.ts'; +import getCustomerModel from '@/shared/API/customer/model/CustomerModel.ts'; +import serverMessageModel from '@/shared/ServerMessage/model/ServerMessageModel.ts'; import getStore from '@/shared/Store/Store.ts'; -import { setCurrentUser } from '@/shared/Store/actions.ts'; +import { setCurrentLanguage, setCurrentUser } from '@/shared/Store/actions.ts'; import observeStore, { selectCurrentUser } from '@/shared/Store/observer.ts'; -// import { LANGUAGE_CHOICE } from '@/shared/constants/buttons.ts'; +import { LANGUAGE_CHOICE } from '@/shared/constants/buttons.ts'; +import { MESSAGE_STATUS, SERVER_MESSAGE } from '@/shared/constants/messages.ts'; import { PAGE_ID } from '@/shared/constants/pages.ts'; import HeaderView from '../view/HeaderView.ts'; @@ -22,31 +25,59 @@ class HeaderModel { this.init(); } + private checkAuthUser(): boolean { + const { currentUser } = getStore().getState(); + if (!currentUser) { + this.router + .navigateTo(PAGE_ID.LOGIN_PAGE) + .catch(() => + serverMessageModel.showServerMessage( + SERVER_MESSAGE[getStore().getState().currentLanguage].BAD_REQUEST, + MESSAGE_STATUS.ERROR, + ), + ); + return false; + } + return true; + } + private checkCurrentUser(): boolean { const { currentUser } = getStore().getState(); const logoutButton = this.view.getLogoutButton(); if (currentUser) { + this.view.getToProfileLink().setEnabled(); logoutButton.setEnabled(); } else { logoutButton.setDisabled(); + this.view.getToProfileLink().setDisabled(); } return true; } private init(): boolean { - this.getHTML().append(this.navigation.getHTML()); + this.view.getWrapper().append(this.navigation.getHTML()); this.checkCurrentUser(); this.setLogoHandler(); this.observeCurrentUser(); this.setLogoutButtonHandler(); - // this.setChangeLanguageButtonHandler(); + this.setCartLinkHandler(); + this.setProfileLinkHandler(); + this.setChangeLanguageButtonHandler(); return true; } - private logoutHandler(): boolean { + private async logoutHandler(): Promise { localStorage.clear(); getStore().dispatch(setCurrentUser(null)); - this.router.navigateTo(PAGE_ID.LOGIN_PAGE); + try { + getCustomerModel().logout(); + await this.router.navigateTo(PAGE_ID.LOGIN_PAGE); + } catch { + serverMessageModel.showServerMessage( + SERVER_MESSAGE[getStore().getState().currentLanguage].BAD_REQUEST, + MESSAGE_STATUS.ERROR, + ); + } return true; } @@ -57,35 +88,80 @@ class HeaderModel { return true; } - // private setChangeLanguageButtonHandler(): boolean { - // const changeLanguageButton = this.view.getChangeLanguageButton(); - // changeLanguageButton.getHTML().addEventListener(EVENT_NAME.CLICK, () => { - // const { currentLanguage } = getStore().getState(); - // const newLanguage = currentLanguage === LANGUAGE_CHOICE.EN ? LANGUAGE_CHOICE.RU : LANGUAGE_CHOICE.EN; - // changeLanguageButton.getHTML().innerText = newLanguage; - // getStore().dispatch(setCurrentLanguage(newLanguage)); - // }); - // return true; - // } + private setCartLinkHandler(): boolean { + const logo = this.view.getToCartLink().getHTML(); + logo.addEventListener('click', async (event) => { + event.preventDefault(); + try { + await this.router.navigateTo(PAGE_ID.CART_PAGE); + } catch { + serverMessageModel.showServerMessage( + SERVER_MESSAGE[getStore().getState().currentLanguage].BAD_REQUEST, + MESSAGE_STATUS.ERROR, + ); + } + }); + return true; + } + + private setChangeLanguageButtonHandler(): boolean { + const switchLanguageButton = this.view.getSwitchLanguageButton().getHTML(); + switchLanguageButton.addEventListener('click', () => { + const newLanguage = + getStore().getState().currentLanguage === LANGUAGE_CHOICE.EN ? LANGUAGE_CHOICE.RU : LANGUAGE_CHOICE.EN; + getStore().dispatch(setCurrentLanguage(newLanguage)); + }); + return true; + } private setLogoHandler(): boolean { const logo = this.view.getLinkLogo().getHTML(); - logo.addEventListener('click', (event) => { + logo.addEventListener('click', async (event) => { event.preventDefault(); - this.router.navigateTo(PAGE_ID.DEFAULT_PAGE); + try { + await this.router.navigateTo(PAGE_ID.DEFAULT_PAGE); + } catch { + serverMessageModel.showServerMessage( + SERVER_MESSAGE[getStore().getState().currentLanguage].BAD_REQUEST, + MESSAGE_STATUS.ERROR, + ); + } }); return true; } private setLogoutButtonHandler(): boolean { const logoutButton = this.view.getLogoutButton(); - logoutButton.getHTML().addEventListener('click', () => { - this.logoutHandler(); + logoutButton.getHTML().addEventListener('click', async () => { + try { + await this.logoutHandler(); + } catch { + serverMessageModel.showServerMessage( + SERVER_MESSAGE[getStore().getState().currentLanguage].BAD_REQUEST, + MESSAGE_STATUS.ERROR, + ); + } logoutButton.setDisabled(); }); return true; } + private setProfileLinkHandler(): boolean { + const logo = this.view.getToProfileLink().getHTML(); + logo.addEventListener('click', (event) => { + event.preventDefault(); + if (this.checkAuthUser()) { + this.router.navigateTo(PAGE_ID.USER_PROFILE_PAGE).catch(() => { + serverMessageModel.showServerMessage( + SERVER_MESSAGE[getStore().getState().currentLanguage].BAD_REQUEST, + MESSAGE_STATUS.ERROR, + ); + }); + } + }); + return true; + } + public getHTML(): HTMLElement { return this.view.getHTML(); } diff --git a/src/widgets/Header/view/HeaderView.ts b/src/widgets/Header/view/HeaderView.ts index 7e3cef95..10fc3a63 100644 --- a/src/widgets/Header/view/HeaderView.ts +++ b/src/widgets/Header/view/HeaderView.ts @@ -1,9 +1,11 @@ import ButtonModel from '@/shared/Button/model/ButtonModel.ts'; import LinkModel from '@/shared/Link/model/LinkModel.ts'; import getStore from '@/shared/Store/Store.ts'; +import observeStore, { selectCurrentLanguage, selectCurrentPage, selectCurrentUser } from '@/shared/Store/observer.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 clearOutElement from '@/shared/utils/clearOutElement.ts'; import createBaseElement from '@/shared/utils/createBaseElement.ts'; import createSVGUse from '@/shared/utils/createSVGUse.ts'; import observeCurrentLanguage from '@/shared/utils/observeCurrentLanguage.ts'; @@ -11,7 +13,7 @@ import observeCurrentLanguage from '@/shared/utils/observeCurrentLanguage.ts'; import styles from './headerView.module.scss'; class HeaderView { - // private changeLanguageButton: ButtonModel; + private burgerButton: ButtonModel; private header: HTMLElement; @@ -19,21 +21,68 @@ class HeaderView { private logoutButton: ButtonModel; + private navigationWrapper: HTMLDivElement; + + private switchLanguageButton: ButtonModel; + + private toCartLink: LinkModel; + + private toProfileLink: LinkModel; + + private wrapper: HTMLDivElement; + constructor() { this.logoutButton = this.createLogoutButton(); this.linkLogo = this.createLinkLogo(); - // this.changeLanguageButton = this.createChangeLanguageButton(); + this.toCartLink = this.createToCartLink(); + this.toProfileLink = this.createToProfileLink(); + this.switchLanguageButton = this.createSwitchLanguageButton(); + this.navigationWrapper = this.createNavigationWrapper(); + this.burgerButton = this.createBurgerButton(); + this.wrapper = this.createWrapper(); this.header = this.createHTML(); + + document.addEventListener('click', ({ target }) => { + if ( + target !== this.navigationWrapper && + this.burgerButton.getHTML().classList.contains(styles.open) && + target !== this.burgerButton.getHTML() + ) { + this.burgerButton.getHTML().classList.toggle(styles.open); + this.navigationWrapper.classList.toggle(styles.open); + document.body.classList.toggle(styles.stopScroll); + } + }); } - // private createChangeLanguageButton(): ButtonModel { - // const { currentLanguage } = getStore().getState(); - // this.changeLanguageButton = new ButtonModel({ - // classes: [styles.changeLanguageButton], - // text: currentLanguage, - // }); - // return this.changeLanguageButton; - // } + private createBurgerButton(): ButtonModel { + this.burgerButton = new ButtonModel({ + classes: [styles.burgerButton], + }); + + const burgerLine1 = createBaseElement({ + cssClasses: [styles.burgerLine], + tag: 'span', + }); + const burgerLine2 = createBaseElement({ + cssClasses: [styles.burgerLine], + tag: 'span', + }); + const burgerLine3 = createBaseElement({ + cssClasses: [styles.burgerLine], + tag: 'span', + }); + + this.burgerButton.getHTML().addEventListener('click', () => { + this.burgerButton.getHTML().classList.toggle(styles.open); + this.navigationWrapper.classList.toggle(styles.open); + document.body.classList.toggle(styles.stopScroll); + }); + + this.burgerButton.getHTML().append(burgerLine1, burgerLine2, burgerLine3); + + return this.burgerButton; + } private createHTML(): HTMLElement { this.header = createBaseElement({ @@ -41,10 +90,16 @@ class HeaderView { tag: 'header', }); - this.header.append(this.linkLogo.getHTML(), this.logoutButton.getHTML()); + this.header.append(this.wrapper); return this.header; } + private createLanguageButtonSVG(): SVGSVGElement { + const svg = document.createElementNS(SVG_DETAILS.SVG_URL, 'svg'); + svg.append(createSVGUse(SVG_DETAILS.SWITCH_LANGUAGE[getStore().getState().currentLanguage])); + return svg; + } + private createLinkLogo(): LinkModel { this.linkLogo = new LinkModel({ attrs: { @@ -60,10 +115,9 @@ class HeaderView { } private createLogoutButton(): ButtonModel { - const { currentLanguage } = getStore().getState(); this.logoutButton = new ButtonModel({ classes: [styles.logoutButton], - text: BUTTON_TEXT[currentLanguage].LOG_OUT, + text: BUTTON_TEXT[getStore().getState().currentLanguage].LOG_OUT, }); observeCurrentLanguage(this.logoutButton.getHTML(), BUTTON_TEXT, BUTTON_TEXT_KEYS.LOG_OUT); @@ -71,9 +125,107 @@ class HeaderView { return this.logoutButton; } - // public getChangeLanguageButton(): ButtonModel { - // return this.changeLanguageButton; - // } + private createNavigationWrapper(): HTMLDivElement { + this.navigationWrapper = createBaseElement({ + cssClasses: [styles.navigationWrapper], + tag: 'div', + }); + this.navigationWrapper.append( + this.switchLanguageButton.getHTML(), + this.logoutButton.getHTML(), + this.toCartLink.getHTML(), + this.toProfileLink.getHTML(), + ); + + return this.navigationWrapper; + } + + private createSwitchLanguageButton(): ButtonModel { + this.switchLanguageButton = new ButtonModel({ + classes: [styles.switchLanguageButton], + }); + + this.switchLanguageButton.getHTML().append(this.createLanguageButtonSVG()); + + observeStore(selectCurrentLanguage, () => { + clearOutElement(this.switchLanguageButton.getHTML()); + this.switchLanguageButton.getHTML().append(this.createLanguageButtonSVG()); + }); + + return this.switchLanguageButton; + } + + private createToCartLink(): LinkModel { + this.toCartLink = new LinkModel({ + attrs: { + href: PAGE_ID.CART_PAGE, + }, + classes: [styles.cartLink], + }); + + const svg = document.createElementNS(SVG_DETAILS.SVG_URL, 'svg'); + svg.append(createSVGUse(SVG_DETAILS.CART)); + this.toCartLink.getHTML().append(svg); + + this.toCartLink + .getHTML() + .classList.toggle(styles.cartLinkActive, getStore().getState().currentPage === PAGE_ID.CART_PAGE); + + observeStore(selectCurrentPage, () => + this.toCartLink + .getHTML() + .classList.toggle(styles.cartLinkActive, getStore().getState().currentPage === PAGE_ID.CART_PAGE), + ); + + return this.toCartLink; + } + + private createToProfileLink(): LinkModel { + this.toProfileLink = new LinkModel({ + attrs: { + href: PAGE_ID.USER_PROFILE_PAGE, + }, + classes: [styles.profileLink], + }); + + const svg = document.createElementNS(SVG_DETAILS.SVG_URL, 'svg'); + svg.append(createSVGUse(SVG_DETAILS.PROFILE)); + this.toProfileLink.getHTML().append(svg); + + if (!getStore().getState().currentUser) { + this.toProfileLink.getHTML().classList.add(styles.hidden); + } + + observeStore(selectCurrentUser, () => + this.toProfileLink.getHTML().classList.toggle(styles.hidden, getStore().getState().currentUser === null), + ); + + this.toProfileLink + .getHTML() + .classList.toggle(styles.profileLinkActive, getStore().getState().currentPage === PAGE_ID.USER_PROFILE_PAGE); + + observeStore(selectCurrentPage, () => + this.toProfileLink + .getHTML() + .classList.toggle(styles.profileLinkActive, getStore().getState().currentPage === PAGE_ID.USER_PROFILE_PAGE), + ); + + return this.toProfileLink; + } + + private createWrapper(): HTMLDivElement { + this.wrapper = createBaseElement({ + cssClasses: [styles.wrapper], + tag: 'div', + }); + + this.wrapper.append(this.linkLogo.getHTML(), this.navigationWrapper, this.burgerButton.getHTML()); + return this.wrapper; + } + + public getBurgerButton(): ButtonModel { + return this.burgerButton; + } public getHTML(): HTMLElement { return this.header; @@ -86,6 +238,30 @@ class HeaderView { public getLogoutButton(): ButtonModel { return this.logoutButton; } + + public getSwitchLanguageButton(): ButtonModel { + return this.switchLanguageButton; + } + + public getToCartLink(): LinkModel { + return this.toCartLink; + } + + public getToProfileLink(): LinkModel { + return this.toProfileLink; + } + + public getWrapper(): HTMLDivElement { + return this.wrapper; + } + + public hideNavigationWrapper(): void { + this.navigationWrapper.classList.add(styles.hidden); + } + + public showNavigationWrapper(): void { + this.navigationWrapper.classList.remove(styles.hidden); + } } export default HeaderView; diff --git a/src/widgets/Header/view/headerView.module.scss b/src/widgets/Header/view/headerView.module.scss index eba35c1f..e634af5a 100644 --- a/src/widgets/Header/view/headerView.module.scss +++ b/src/widgets/Header/view/headerView.module.scss @@ -4,20 +4,73 @@ right: 0; top: 0; z-index: 1; - display: grid; - align-items: center; - justify-content: space-between; - 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); - padding: calc(var(--small-offset) / 4) var(--small-offset); + border-bottom: 1px solid var(--steam-green-300); width: 100%; background-color: var(--white); } +.wrapper { + display: flex; + align-items: center; + margin: 0 auto; + padding: 0 var(--small-offset); + max-width: 1440px; + + @media (max-width: 576px) { + padding: 0 var(--extra-small-offset); + } +} + +.navigationWrapper { + position: fixed; + right: -100%; + top: 0; + z-index: 2; + display: flex; + flex-direction: column; + align-items: center; + order: 3; + padding: 70px var(--extra-small-offset) 0; + width: 20%; + height: 100%; + background-color: #8c998f2b; + transform: none; + transition: transform 0.3s; + backdrop-filter: blur(2px); + gap: var(--medium-offset); + + &.open { + transform: translateX(-500%); + } +} + +.switchLanguageButton { + display: flex; + align-items: center; + order: 3; + overflow: hidden; + border: 2px solid var(--noble-gray-300); + border-radius: 50%; + width: 30px; + height: 30px; + transition: border-color 0.2s; + + svg { + width: 30px; + height: 30px; + } + + @media (hover: hover) { + &:hover { + border-color: var(--steam-green-800); + } + } +} + .logo { - grid-column: 1; + order: 1; + width: 40px; + height: 40px; svg { width: 40px; @@ -33,11 +86,20 @@ } } } + + @media (max-width: 576px) { + width: 30px; + height: 30px; + + svg { + width: 30px; + height: 30px; + } + } } -.logoutButton, -.changeLanguageButton { - grid-column: 3; +.logoutButton { + order: 4; border-radius: var(--small-br); padding: calc(var(--extra-small-offset) / 2) var(--extra-small-offset); font: var(--regular-font); @@ -64,6 +126,170 @@ } } -.changeLanguageButton { - grid-column: 3; +.cartLink, +.profileLink { + display: flex; + align-items: center; + justify-content: center; + + svg { + width: 24px; + height: 24px; + fill: var(--noble-gray-800); + transition: fill 0.2s; + } + + @media (hover: hover) { + &:hover { + svg { + fill: var(--steam-green-800); + } + } + } +} + +.cartLink { + order: 1; +} + +.cartLinkActive { + pointer-events: none; + + svg { + fill: var(--steam-green-800); + } +} + +.profileLink { + order: 2; + + svg { + stroke: var(--noble-gray-800); + transition: stroke 0.2s; + } + + @media (hover: hover) { + &:hover { + svg { + stroke: var(--steam-green-800); + } + } + } +} + +.profileLinkActive { + pointer-events: none; + + svg { + stroke: var(--steam-green-800); + } +} + +.hidden { + display: none; +} + +.burgerButton { + position: relative; + z-index: 10; + order: 4; + width: 25px; + height: 20px; +} + +.burgerLine { + position: absolute; + border-radius: 2px; + background-color: var(--steam-green-800); + transition: 0.3s cubic-bezier(0.8, 0.5, 0.2, 1.4); + pointer-events: none; +} + +.burgerButton:hover .burgerLine:nth-child(1), +.burgerButton:hover .burgerLine:nth-child(2), +.burgerButton:hover .burgerLine:nth-child(3) { + background-color: var(--steam-green-700); +} + +.burgerButton:not(.open):hover .burgerLine:nth-child(1), +.burgerButton:not(.open):hover .burgerLine:nth-child(2), +.burgerButton:not(.open):hover .burgerLine:nth-child(3) { + left: 0; + display: block; + width: 100%; + height: 2px; + transition: 0.3s cubic-bezier(0.8, 0.5, 0.2, 1.4); +} + +.burgerButton:not(.open):hover .burgerLine:nth-child(1) { + top: -2px; +} + +.burgerButton:not(.open):hover .burgerLine:nth-child(2) { + top: 9px; +} + +.burgerButton:not(.open):hover .burgerLine:nth-child(3) { + bottom: -2px; +} + +.burgerButton > .burgerLine:nth-child(1), +.burgerButton > .burgerLine:nth-child(2), +.burgerButton > .burgerLine:nth-child(3) { + position: absolute; + left: 0; + display: block; + width: 100%; + height: 2px; +} + +.burgerButton > .burgerLine:nth-child(1) { + top: 0; +} + +.burgerButton > .burgerLine:nth-child(2) { + top: 9px; +} + +.burgerButton > .burgerLine:nth-child(3) { + bottom: 0; +} + +.burgerButton.open { + transform: rotate(-90deg); +} + +.burgerButton.open .burgerLine:nth-child(1), +.burgerButton.open .burgerLine:nth-child(2), +.burgerButton.open .burgerLine:nth-child(3) { + background-color: var(--steam-green-800); + transition: 0.3s cubic-bezier(0.8, 0.5, 0.2, 1.4); +} + +.burgerButton.open .burgerLine:nth-child(1) { + left: 5px; + top: 12px; + width: 20px; + transform: rotate(90deg); + transition-delay: 150ms; +} + +.burgerButton.open .burgerLine:nth-child(2) { + left: 3px; + top: 17px; + width: 15px; + transform: rotate(45deg); + transition-delay: 50ms; +} + +.burgerButton.open .burgerLine:nth-child(3) { + left: 13px; + top: 17px; + width: 14px; + transform: rotate(-45deg); + transition-delay: 100ms; +} + +.stopScroll { + overflow: hidden; } diff --git a/src/widgets/LoginForm/model/LoginFormModel.ts b/src/widgets/LoginForm/model/LoginFormModel.ts index f4d021c4..d5b7f336 100644 --- a/src/widgets/LoginForm/model/LoginFormModel.ts +++ b/src/widgets/LoginForm/model/LoginFormModel.ts @@ -2,42 +2,29 @@ import type InputFieldModel from '@/entities/InputField/model/InputFieldModel.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'; 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 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 { isUserCredentialsData } from '@/shared/types/validation/user.ts'; -import { greeting } from '@/shared/utils/messageTemplate.ts'; +import { createGreetingMessage } from '@/shared/utils/messageTemplate.ts'; import LoginFormView from '../view/LoginFormView.ts'; class LoginFormModel { - private eventMediator = EventMediatorModel.getInstance(); - private view: LoginFormView = new LoginFormView(); constructor() { this.init(); } - private createGreetingMessage(name: string): string { - const greetingMessage = `${greeting(name)} ${SERVER_MESSAGE.SUCCESSFUL_LOGIN}`; - return greetingMessage; - } - 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 userData; } @@ -45,13 +32,13 @@ class LoginFormModel { this.view.getInputFields().forEach((inputField) => this.setInputFieldHandlers(inputField)); this.setPreventDefaultToForm(); this.setSubmitFormHandler(); - this.subscribeToEventMediator(); this.setSwitchPasswordVisibilityHandler(); return true; } private loginUser(userLoginData: UserCredentials): void { - const loader = new LoaderModel(SIZES.MEDIUM).getHTML(); + this.view.getSubmitFormButton().setDisabled(); + const loader = new LoaderModel(SIZES.SMALL).getHTML(); this.view.getSubmitFormButton().getHTML().append(loader); getCustomerModel() .hasEmail(userLoginData.email) @@ -59,28 +46,37 @@ class LoginFormModel { if (response) { this.loginUserHandler(userLoginData); } else { - serverMessageModel.showServerMessage(SERVER_MESSAGE.INVALID_EMAIL, MESSAGE_STATUS.ERROR); + serverMessageModel.showServerMessage( + SERVER_MESSAGE[getStore().getState().currentLanguage].INVALID_EMAIL, + MESSAGE_STATUS.ERROR, + ); } }) .catch(() => { - serverMessageModel.showServerMessage(SERVER_MESSAGE.BAD_REQUEST, MESSAGE_STATUS.ERROR); + serverMessageModel.showServerMessage( + SERVER_MESSAGE[getStore().getState().currentLanguage].BAD_REQUEST, + MESSAGE_STATUS.ERROR, + ); }) .finally(() => loader.remove()); } private loginUserHandler(userLoginData: UserCredentials): void { - const loader = new LoaderModel(SIZES.MEDIUM).getHTML(); + const loader = new LoaderModel(SIZES.SMALL).getHTML(); this.view.getSubmitFormButton().getHTML().append(loader); getCustomerModel() .authCustomer(userLoginData) .then((data) => { - getStore().dispatch(setCurrentUser(data)); if (data) { - serverMessageModel.showServerMessage(this.createGreetingMessage(data.firstName), MESSAGE_STATUS.SUCCESS); + getStore().dispatch(setCurrentUser(data)); + serverMessageModel.showServerMessage(createGreetingMessage(), MESSAGE_STATUS.SUCCESS); } }) .catch(() => { - serverMessageModel.showServerMessage(SERVER_MESSAGE.INCORRECT_PASSWORD, MESSAGE_STATUS.ERROR); + serverMessageModel.showServerMessage( + SERVER_MESSAGE[getStore().getState().currentLanguage].INCORRECT_PASSWORD, + MESSAGE_STATUS.ERROR, + ); }) .finally(() => loader.remove()); } @@ -113,15 +109,6 @@ class LoginFormModel { return true; } - private subscribeToEventMediator(): boolean { - this.eventMediator.subscribe(MEDIATOR_EVENT.USER_LOGIN, (userLoginData) => { - if (isUserCredentialsData(userLoginData)) { - this.loginUser(userLoginData); - } - }); - return true; - } - private switchSubmitFormButtonAccess(): boolean { if (this.view.getInputFields().every((inputField) => inputField.getIsValid())) { this.view.getSubmitFormButton().setEnabled(); diff --git a/src/widgets/LoginForm/view/LoginFormView.ts b/src/widgets/LoginForm/view/LoginFormView.ts index 4d33d056..e6f62e84 100644 --- a/src/widgets/LoginForm/view/LoginFormView.ts +++ b/src/widgets/LoginForm/view/LoginFormView.ts @@ -79,13 +79,12 @@ class LoginFormView { } private createSubmitFormButton(): ButtonModel { - const { currentLanguage } = getStore().getState(); this.submitFormButton = new ButtonModel({ attrs: { type: BUTTON_TYPE.SUBMIT, }, classes: [styles.submitFormButton], - text: BUTTON_TEXT[currentLanguage].LOGIN, + text: BUTTON_TEXT[getStore().getState().currentLanguage].LOGIN, }); observeCurrentLanguage(this.submitFormButton.getHTML(), BUTTON_TEXT, BUTTON_TEXT_KEYS.LOGIN); diff --git a/src/widgets/LoginForm/view/loginForm.module.scss b/src/widgets/LoginForm/view/loginForm.module.scss index 1129902c..66481a1a 100644 --- a/src/widgets/LoginForm/view/loginForm.module.scss +++ b/src/widgets/LoginForm/view/loginForm.module.scss @@ -81,13 +81,14 @@ .showPasswordElement { position: absolute; right: 0; - top: 7px; + top: 17px; border-radius: var(--small-br); padding: calc(var(--extra-small-offset) / 8); width: 24px; height: 24px; background-color: transparent; transform: translate(-12%, 0); + cursor: pointer; svg { width: 20px; @@ -115,6 +116,6 @@ } @media (max-width: 768px) { - top: 2px; + top: 7px; } } diff --git a/src/widgets/RegistrationForm/model/RegistrationFormModel.ts b/src/widgets/RegistrationForm/model/RegistrationFormModel.ts index 10886f94..4c54e0a9 100644 --- a/src/widgets/RegistrationForm/model/RegistrationFormModel.ts +++ b/src/widgets/RegistrationForm/model/RegistrationFormModel.ts @@ -1,19 +1,18 @@ import type InputFieldModel from '@/entities/InputField/model/InputFieldModel.ts'; import type { AddressType } from '@/shared/types/address.ts'; -import type { Address, PersonalData, User, UserCredentials } from '@/shared/types/user.ts'; +import type { Address, PersonalData, User } from '@/shared/types/user.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 { setBillingCountry, setCurrentUser } from '@/shared/Store/actions.ts'; -import MEDIATOR_EVENT from '@/shared/constants/events.ts'; +import { setBillingCountry, setCurrentUser, switchIsUserLoggedIn } from '@/shared/Store/actions.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 { ADDRESS_TYPE } from '@/shared/types/address.ts'; +import formattedText from '@/shared/utils/formattedText.ts'; import RegistrationFormView from '../view/RegistrationFormView.ts'; @@ -28,8 +27,6 @@ class RegisterFormModel { }), }; - private eventMediator = EventMediatorModel.getInstance(); - private inputFields: InputFieldModel[] = []; private view: RegistrationFormView = new RegistrationFormView(); @@ -71,13 +68,6 @@ class RegisterFormModel { return currentUserData; } - private getCredentialsData(): UserCredentials { - return { - email: this.view.getEmailField().getView().getValue(), - password: this.view.getPasswordField().getView().getValue(), - }; - } - private getFormUserData(): User { const userData: User = { addresses: [], @@ -85,9 +75,9 @@ class RegisterFormModel { defaultBillingAddressId: null, defaultShippingAddressId: null, email: this.view.getEmailField().getView().getValue(), - firstName: this.view.getFirstNameField().getView().getValue(), + firstName: formattedText(this.view.getFirstNameField().getView().getValue()), id: '', - lastName: this.view.getLastNameField().getView().getValue(), + lastName: formattedText(this.view.getLastNameField().getView().getValue()), locale: '', password: this.view.getPasswordField().getView().getValue(), version: 0, @@ -100,8 +90,8 @@ class RegisterFormModel { private getPersonalData(): PersonalData { return { email: this.view.getEmailField().getView().getValue(), - firstName: this.view.getFirstNameField().getView().getValue(), - lastName: this.view.getLastNameField().getView().getValue(), + firstName: formattedText(this.view.getFirstNameField().getView().getValue()), + lastName: formattedText(this.view.getLastNameField().getView().getValue()), }; } @@ -128,47 +118,28 @@ class RegisterFormModel { } private registerUser(): void { - const loader = new LoaderModel(SIZES.MEDIUM).getHTML(); + const loader = new LoaderModel(SIZES.SMALL).getHTML(); this.view.getSubmitFormButton().getHTML().append(loader); getCustomerModel() .registerNewCustomer(this.getFormUserData()) .then((newUserData) => { if (newUserData) { this.successfulUserRegistration(newUserData); - serverMessageModel.showServerMessage(SERVER_MESSAGE.SUCCESSFUL_REGISTRATION, MESSAGE_STATUS.SUCCESS); + serverMessageModel.showServerMessage( + SERVER_MESSAGE[getStore().getState().currentLanguage].SUCCESSFUL_REGISTRATION, + MESSAGE_STATUS.SUCCESS, + ); } }) .catch(() => { - serverMessageModel.showServerMessage(SERVER_MESSAGE.USER_EXISTS, MESSAGE_STATUS.ERROR); + serverMessageModel.showServerMessage( + SERVER_MESSAGE[getStore().getState().currentLanguage].USER_EXISTS, + MESSAGE_STATUS.ERROR, + ); }) .finally(() => loader.remove()); } - private resetInputFieldsValidation(): void { - 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(); inputHTML.addEventListener('input', () => { @@ -221,6 +192,7 @@ class RegisterFormModel { (inputField) => inputField.getView().getInput().getHTML().placeholder === inputElement.getInput().getHTML().placeholder, ); + if (billingInput) { billingInput.getView().getInput().getHTML().value = inputElement.getInput().getHTML().value; } @@ -234,16 +206,22 @@ class RegisterFormModel { } private successfulUserRegistration(newUserData: User): void { - this.eventMediator.notify(MEDIATOR_EVENT.USER_LOGIN, this.getCredentialsData()); - this.updateUserData(newUserData).catch(() => { - serverMessageModel.showServerMessage(SERVER_MESSAGE.BAD_REQUEST, MESSAGE_STATUS.ERROR); - }); + const loader = new LoaderModel(SIZES.SMALL).getHTML(); + this.view.getSubmitFormButton().getHTML().append(loader); + this.updateUserData(newUserData) + .then(() => getStore().dispatch(switchIsUserLoggedIn(true))) + .catch(() => { + serverMessageModel.showServerMessage( + SERVER_MESSAGE[getStore().getState().currentLanguage].BAD_REQUEST, + MESSAGE_STATUS.ERROR, + ); + }) + .finally(() => loader.remove()); } private switchSubmitFormButtonAccess(): boolean { if (this.inputFields.every((inputField) => inputField.getIsValid())) { this.view.getSubmitFormButton().setEnabled(); - this.view.getSubmitFormButton().getHTML().focus(); } else { this.view.getSubmitFormButton().setDisabled(); } @@ -256,8 +234,8 @@ class RegisterFormModel { if (currentUserData) { currentUserData = await getCustomerModel().editCustomer( [ - CustomerModel.actionEditFirstName(this.view.getFirstNameField().getView().getValue()), - CustomerModel.actionEditLastName(this.view.getLastNameField().getView().getValue()), + CustomerModel.actionEditFirstName(formattedText(this.view.getFirstNameField().getView().getValue())), + CustomerModel.actionEditLastName(formattedText(this.view.getLastNameField().getView().getValue())), CustomerModel.actionEditDateOfBirth(this.view.getDateOfBirthField().getView().getValue()), ], currentUserData, @@ -280,7 +258,7 @@ class RegisterFormModel { if (!currentUserData) { return null; } - const shippingAddressID = currentUserData.addresses[currentUserData.addresses.length - 1].id; + const shippingAddressID = currentUserData.addresses.at(-1)?.id ?? ''; if (checkboxDefaultShippingAddress?.checked) { currentUserData = await this.editDefaultShippingAddress(shippingAddressID, currentUserData); @@ -295,7 +273,7 @@ class RegisterFormModel { if (!currentUserData) { return null; } - const billingAddressID = currentUserData.addresses[currentUserData.addresses.length - 1].id; + const billingAddressID = currentUserData.addresses.at(-1)?.id ?? ''; if (checkboxDefaultBillingAddress?.checked) { currentUserData = await this.editDefaultBillingAddress(billingAddressID, currentUserData); @@ -312,8 +290,6 @@ class RegisterFormModel { currentUserData = await this.updateUserAddresses(currentUserData); getStore().dispatch(setCurrentUser(currentUserData)); - this.inputFields.forEach((inputField) => inputField.getView().getInput().clear()); - this.resetInputFieldsValidation(); return currentUserData; } diff --git a/src/widgets/RegistrationForm/view/RegistrationFormView.ts b/src/widgets/RegistrationForm/view/RegistrationFormView.ts index 9c8e91a6..6d634d44 100644 --- a/src/widgets/RegistrationForm/view/RegistrationFormView.ts +++ b/src/widgets/RegistrationForm/view/RegistrationFormView.ts @@ -126,12 +126,11 @@ class RegistrationFormView { } private createSubmitFormButton(): ButtonModel { - const { currentLanguage } = getStore().getState(); this.submitFormButton = new ButtonModel({ attrs: { type: BUTTON_TYPE.SUBMIT, }, - text: BUTTON_TEXT[currentLanguage].REGISTRATION, + text: BUTTON_TEXT[getStore().getState().currentLanguage].REGISTRATION, }); observeCurrentLanguage(this.submitFormButton.getHTML(), BUTTON_TEXT, BUTTON_TEXT_KEYS.REGISTRATION); @@ -146,14 +145,13 @@ class RegistrationFormView { cssClasses: string[], inputFields: InputFieldModel[], ): HTMLDivElement { - const { currentLanguage } = getStore().getState(); const wrapperElement = createBaseElement({ cssClasses, tag: 'div', }); const titleElement = createBaseElement({ cssClasses: [styles.title], - innerContent: FORM_CONSTANT.TITLE_TEXT[currentLanguage][title], + innerContent: FORM_CONSTANT.TITLE_TEXT[getStore().getState().currentLanguage][title], tag: 'h3', }); wrapperElement.append(titleElement); @@ -161,8 +159,10 @@ class RegistrationFormView { inputFields.forEach((inputField) => { const inputFieldElement = inputField.getView().getHTML(); + const inputHTML = inputField.getView().getInput().getHTML(); if (inputFieldElement instanceof HTMLLabelElement) { inputFieldElement.classList.add(styles.label); + inputHTML.classList.add(styles.input); wrapperElement.append(inputFieldElement); } else if (inputFieldElement instanceof InputModel) { wrapperElement.append(inputFieldElement.getHTML()); diff --git a/src/widgets/RegistrationForm/view/registrationForm.module.scss b/src/widgets/RegistrationForm/view/registrationForm.module.scss index 47c086d3..3fc6024f 100644 --- a/src/widgets/RegistrationForm/view/registrationForm.module.scss +++ b/src/widgets/RegistrationForm/view/registrationForm.module.scss @@ -17,98 +17,6 @@ gap: calc(var(--extra-small-offset) / 2); } - .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 { - 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: 24px; - } - } - - input { - border: 1px solid var(--noble-gray-200); - border-radius: var(--small-br); - padding: calc(var(--extra-small-offset) / 2) var(--extra-small-offset); - font: var(--regular-font); - letter-spacing: 1px; - color: var(--noble-gray-800); - transition: border 0.2s; - cursor: text; - - &::placeholder { - color: var(--noble-gray-600); - } - - &:focus { - border: 1px solid var(--steam-green-800); - } - - @media (hover: hover) { - &:hover { - border: 1px solid var(--steam-green-800); - } - } - - &[type='date'] { - -webkit-appearance: none; - appearance: none; - padding-right: 0; - } - - &[type='date']::-webkit-calendar-picker-indicator { - background-image: url('../../../shared/img/svg/calendar.svg'); - background-position: -3px 0; - cursor: pointer; - } - - &[type='date']::-webkit-inner-spin-button, - &[type='date']::-webkit-outer-spin-button { - display: none; - } - - &::-webkit-datetime-edit-day-field:focus, - &::-webkit-datetime-edit-month-field:focus, - &::-webkit-datetime-edit-year-field:focus { - outline: none; - border-radius: var(--small-br); - color: var(--steam-green-800); - background-color: var(--noble-gray-200); - } - } - button { display: flex; align-items: center; @@ -149,6 +57,100 @@ } } +.input { + border: 1px solid var(--noble-gray-200); + border-radius: var(--small-br); + padding: calc(var(--extra-small-offset) / 2) var(--extra-small-offset); + padding-right: calc(var(--small-offset) * 1.5); + font: var(--regular-font); + letter-spacing: 1px; + color: var(--noble-gray-800); + transition: border 0.2s; + cursor: text; + + &::placeholder { + color: var(--noble-gray-600); + } + + &:focus { + border: 1px solid var(--steam-green-800); + } + + @media (hover: hover) { + &:hover { + border: 1px solid var(--steam-green-800); + } + } + + &[type='date'] { + -webkit-appearance: none; + appearance: none; + padding-right: 0; + } + + &[type='date']::-webkit-calendar-picker-indicator { + background-image: url('../../../shared/img/svg/calendar.svg'); + background-position: -3px 0; + cursor: pointer; + } + + &[type='date']::-webkit-inner-spin-button, + &[type='date']::-webkit-outer-spin-button { + display: none; + } + + &::-webkit-datetime-edit-day-field:focus, + &::-webkit-datetime-edit-month-field:focus, + &::-webkit-datetime-edit-year-field:focus { + outline: none; + border-radius: var(--small-br); + color: var(--steam-green-800); + background-color: var(--noble-gray-200); + } +} + +.showPasswordElement { + position: absolute; + right: 0; + top: 33px; + border-radius: var(--small-br); + padding: calc(var(--extra-small-offset) / 8); + width: 24px; + height: 24px; + background-color: transparent; + transform: translate(-12%, 0); + cursor: pointer; + + 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: 18px; + } +} + .credentialsWrapper, .personalDataWrapper { display: grid;