diff --git a/src/app/App/model/AppModel.ts b/src/app/App/model/AppModel.ts index cdeb1b30..74212226 100644 --- a/src/app/App/model/AppModel.ts +++ b/src/app/App/model/AppModel.ts @@ -2,6 +2,7 @@ import type { Page } from '@/shared/types/common.ts'; import RouterModel from '@/app/Router/model/RouterModel.ts'; +import modal from '@/shared/Modal/model/ModalModel.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'; @@ -65,7 +66,7 @@ class AppModel { }, [PAGE_ID.USER_PROFILE_PAGE]: async (): Promise => { const { default: UserProfilePageModel } = await import('@/pages/UserProfilePage/model/UserProfilePageModel.ts'); - return new UserProfilePageModel(this.appView.getHTML()); + return new UserProfilePageModel(this.appView.getHTML(), this.router); }, }; @@ -81,6 +82,7 @@ class AppModel { .getHTML() .insertAdjacentElement('beforebegin', new HeaderModel(this.router, this.appView.getHTML()).getHTML()); this.appView.getHTML().insertAdjacentElement('afterend', new FooterModel(this.router).getHTML()); + this.appView.getHTML().insertAdjacentElement('afterend', modal.getHTML()); const routes = await this.createRoutes(); this.router.setRoutes(routes); diff --git a/src/app/App/view/AppView.ts b/src/app/App/view/AppView.ts index 32f56c03..6411e3c2 100644 --- a/src/app/App/view/AppView.ts +++ b/src/app/App/view/AppView.ts @@ -14,7 +14,6 @@ class AppView { cssClasses: [styles.siteWrapper], tag: 'div', }); - return this.pagesContainer; } diff --git a/src/app/App/view/appView.module.scss b/src/app/App/view/appView.module.scss index fc88aeff..b2ce601b 100644 --- a/src/app/App/view/appView.module.scss +++ b/src/app/App/view/appView.module.scss @@ -8,4 +8,8 @@ width: 100%; min-height: calc(100vh - 141px); max-width: 1440px; + background-image: url('../../../shared/img/tree.png'); + background-position: right bottom; + background-size: 60dvh; + background-repeat: no-repeat; } diff --git a/src/app/styles/variables.scss b/src/app/styles/variables.scss index 94b60bec..5d1af8e1 100644 --- a/src/app/styles/variables.scss +++ b/src/app/styles/variables.scss @@ -11,6 +11,14 @@ --medium-br: 10px; --large-br: 20px; + // shadows + --mellow-shadow-100: rgb(0 0 0 / 19%) 0 10px 20px, rgb(0 0 0 / 23%) 0 6px 6px; + --mellow-shadow-200: rgb(255 255 255 / 10%) 0 1px 1px 0 inset, rgb(50 50 93 / 25%) 0 50px 100px -20px, + rgb(0 0 0 / 30%) 0 30px 60px -30px; + --mellow-shadow-300: rgb(0 0 0 / 25%) 0 14px 28px, rgb(0 0 0 / 22%) 0 10px 10px; + --mellow-shadow-400: rgb(0 0 0 / 9%) 0 2px 1px, rgb(0 0 0 / 9%) 0 4px 2px, rgb(0 0 0 / 9%) 0 8px 4px, + rgb(0 0 0 / 9%) 0 16px 8px, rgb(0 0 0 / 9%) 0 32px 16px; + // fonts --regular-font: 400 13px 'Cerapro', sans-serif; --extra-regular-font: 400 24px 'Cerapro', sans-serif; @@ -24,6 +32,7 @@ body.light { // colors --white: #fff; + --white-tr: #ffffffa9; --black: #000; --noble-white-100: #f5f5f5; --noble-white-200: #f0f0f0; @@ -34,6 +43,8 @@ --noble-gray-600: #a5a5a5; --noble-gray-700: #727272; --noble-gray-800: #3d3d3d; + --noble-gray-tr-800: #acacacbd; + --noble-gray-900: #1d1d1d; --noble-gray-1000: #1a1a1ab5; --red-power-600: #d0302f; --steam-green-300: #46a35880; diff --git a/src/entities/Navigation/model/NavigationModel.ts b/src/entities/Navigation/model/NavigationModel.ts index 506d19f8..cbeb1272 100644 --- a/src/entities/Navigation/model/NavigationModel.ts +++ b/src/entities/Navigation/model/NavigationModel.ts @@ -1,10 +1,9 @@ import type RouterModel from '@/app/Router/model/RouterModel'; -import serverMessageModel from '@/shared/ServerMessage/model/ServerMessageModel.ts'; import getStore from '@/shared/Store/Store.ts'; -import observeStore, { selectCurrentPage, selectCurrentUser } from '@/shared/Store/observer.ts'; -import { MESSAGE_STATUS, SERVER_MESSAGE } from '@/shared/constants/messages.ts'; +import observeStore, { selectCurrentPage, selectIsUserLoggedIn } from '@/shared/Store/observer.ts'; import { PAGE_ID } from '@/shared/constants/pages.ts'; +import showErrorMessage from '@/shared/utils/userMessage.ts'; import NavigationView from '../view/NavigationView.ts'; @@ -37,7 +36,7 @@ class NavigationModel { } private observeState(): boolean { - observeStore(selectCurrentUser, () => this.checkCurrentUser()); + observeStore(selectIsUserLoggedIn, () => this.checkCurrentUser()); observeStore(selectCurrentPage, () => this.switchLinksState()); return true; } @@ -50,10 +49,7 @@ class NavigationModel { try { await this.router.navigateTo(route); } catch { - serverMessageModel.showServerMessage( - SERVER_MESSAGE[getStore().getState().currentLanguage].BAD_REQUEST, - MESSAGE_STATUS.ERROR, - ); + showErrorMessage(); } }); }); diff --git a/src/entities/ProductCard/view/ProductCardView.ts b/src/entities/ProductCard/view/ProductCardView.ts index 274b0111..99a39692 100644 --- a/src/entities/ProductCard/view/ProductCardView.ts +++ b/src/entities/ProductCard/view/ProductCardView.ts @@ -5,7 +5,8 @@ import LinkModel from '@/shared/Link/model/LinkModel.ts'; import LoaderModel from '@/shared/Loader/model/LoaderModel.ts'; import getStore from '@/shared/Store/Store.ts'; import observeStore, { selectCurrentLanguage } from '@/shared/Store/observer.ts'; -import { LANGUAGE_CHOICE, MORE_TEXT } from '@/shared/constants/buttons.ts'; +import { MORE_TEXT } from '@/shared/constants/buttons.ts'; +import { LANGUAGE_CHOICE } from '@/shared/constants/common.ts'; import { PAGE_ID } from '@/shared/constants/pages.ts'; import { LOADER_SIZE } from '@/shared/constants/sizes.ts'; import createBaseElement from '@/shared/utils/createBaseElement.ts'; diff --git a/src/entities/UserAddress/model/UserAddressModel.ts b/src/entities/UserAddress/model/UserAddressModel.ts new file mode 100644 index 00000000..a042ed0f --- /dev/null +++ b/src/entities/UserAddress/model/UserAddressModel.ts @@ -0,0 +1,19 @@ +import UserAddressView from '../view/UserAddressView.ts'; + +class UserAddressModel { + private view = new UserAddressView(); + + public getHTML(): HTMLElement { + return this.view.getHTML(); + } + + public hide(): void { + this.view.hide(); + } + + public show(): void { + this.view.show(); + } +} + +export default UserAddressModel; diff --git a/src/entities/UserAddress/view/UserAddressView.ts b/src/entities/UserAddress/view/UserAddressView.ts new file mode 100644 index 00000000..1e56fc5a --- /dev/null +++ b/src/entities/UserAddress/view/UserAddressView.ts @@ -0,0 +1,72 @@ +import getStore from '@/shared/Store/Store.ts'; +import COUNTRIES_LIST from '@/shared/constants/countriesList.ts'; +import createBaseElement from '@/shared/utils/createBaseElement.ts'; +import findKeyByValue from '@/shared/utils/findKeyByValue.ts'; +import { defaultBillingAddress, defaultShippingAddress } from '@/shared/utils/messageTemplates.ts'; + +import styles from './userAddressView.module.scss'; + +class UserAddressView { + private addressesWrapper: HTMLDivElement; + + constructor() { + this.addressesWrapper = this.createAddresses(); + } + + private createAddressElement( + text: string, + tag: keyof HTMLElementTagNameMap = 'li', + classes: string[] = [styles.info], + ): HTMLElement { + return createBaseElement({ + cssClasses: classes, + innerContent: text, + tag, + }); + } + + private createAddresses(): HTMLDivElement { + const { currentUser } = getStore().getState(); + if (!currentUser) { + return createBaseElement({ + cssClasses: [styles.addressesWrapper, styles.hidden], + tag: 'div', + }); + } + const { addresses, defaultBillingAddressId, defaultShippingAddressId, locale } = currentUser; + + this.addressesWrapper = createBaseElement({ + cssClasses: [styles.addressesWrapper, styles.hidden], + tag: 'div', + }); + addresses.forEach((address) => { + const country = findKeyByValue(COUNTRIES_LIST[locale], address.country); + const standartAddressText = `${address.streetName}, ${address.city}, ${country}, ${address.postalCode}`; + let addressText = `${standartAddressText}`; + + if (defaultBillingAddressId?.id === address.id) { + addressText = defaultBillingAddress(standartAddressText); + } + if (defaultShippingAddressId?.id === address.id) { + addressText = defaultShippingAddress(standartAddressText); + } + const addressWrapper = this.createAddressElement(addressText); + this.addressesWrapper.append(addressWrapper); + }); + return this.addressesWrapper; + } + + public getHTML(): HTMLDivElement { + return this.addressesWrapper; + } + + public hide(): void { + this.addressesWrapper.classList.add(styles.hidden); + } + + public show(): void { + this.addressesWrapper.classList.remove(styles.hidden); + } +} + +export default UserAddressView; diff --git a/src/entities/UserAddress/view/userAddressView.module.scss b/src/entities/UserAddress/view/userAddressView.module.scss new file mode 100644 index 00000000..5edc77f0 --- /dev/null +++ b/src/entities/UserAddress/view/userAddressView.module.scss @@ -0,0 +1,69 @@ +.info { + grid-column: 2 span; + max-width: 100%; + word-break: break-all; + + @media (max-width: 810px) { + font: var(--medium-font); + } + + @media (max-width: 500px) { + font: var(--regular-font); + } +} + +.addressesWrapper { + position: relative; + display: grid; + align-items: center; + grid-template-columns: repeat(2, 1fr); + grid-template-rows: repeat(5, auto); + margin: 0 var(--small-offset); + padding: var(--small-offset); + min-width: 40%; + max-width: fit-content; + box-shadow: var(--mellow-shadow-100); + font: var(--extra-regular-font); + letter-spacing: 1px; + color: var(--noble-gray-900); + background-color: var(--noble-gray-tr-800); + gap: var(--small-offset); +} + +.editInfoButton { + grid-column: 2 span; + grid-row: 5; + margin: 0 auto; + border-radius: var(--medium-br); + padding: calc(var(--small-offset) / 3) var(--small-offset); + height: max-content; + max-width: max-content; + font: var(--regular-font); + letter-spacing: 1px; + color: var(--white); + background-color: var(--steam-green-800); + transition: + color 0.2s, + background-color 0.2s; + + &:focus { + background-color: var(--steam-green-700); + } + + @media (hover: hover) { + &:hover { + background-color: var(--steam-green-700); + } + } + + &:disabled { + background-color: var(--noble-gray-300); + pointer-events: none; + } +} + +.hidden { + display: none; // TBD check to replace + opacity: 0; + visibility: hidden; +} diff --git a/src/entities/UserInfo/model/UserInfoModel.ts b/src/entities/UserInfo/model/UserInfoModel.ts new file mode 100644 index 00000000..ab306c62 --- /dev/null +++ b/src/entities/UserInfo/model/UserInfoModel.ts @@ -0,0 +1,32 @@ +import UserInfoView from '../view/UserInfoView.ts'; + +class UserInfoModel { + private view = new UserInfoView(); + + constructor() { + this.view.show(); + this.setEditInfoButtonHandler(); + } + + private setEditInfoButtonHandler(): boolean { + const editInfoButton = this.view.getEditInfoButton(); + editInfoButton.getHTML().addEventListener('click', () => { + // TBD Replace with edit action + }); + return true; + } + + public getHTML(): HTMLDivElement { + return this.view.getHTML(); + } + + public hide(): void { + this.view.hide(); + } + + public show(): void { + this.view.show(); + } +} + +export default UserInfoModel; diff --git a/src/entities/UserInfo/view/UserInfoView.ts b/src/entities/UserInfo/view/UserInfoView.ts new file mode 100644 index 00000000..57ade70b --- /dev/null +++ b/src/entities/UserInfo/view/UserInfoView.ts @@ -0,0 +1,101 @@ +import ButtonModel from '@/shared/Button/model/ButtonModel.ts'; +import getStore from '@/shared/Store/Store.ts'; +import observeStore, { selectCurrentLanguage } from '@/shared/Store/observer.ts'; +import { BUTTON_TEXT, BUTTON_TEXT_KEYS } from '@/shared/constants/buttons.ts'; +import clearOutElement from '@/shared/utils/clearOutElement.ts'; +import createBaseElement from '@/shared/utils/createBaseElement.ts'; +import { userInfoDateOfBirth, userInfoEmail, userInfoLastName, userInfoName } from '@/shared/utils/messageTemplates.ts'; +import observeCurrentLanguage from '@/shared/utils/observeCurrentLanguage.ts'; + +import styles from './userInfoView.module.scss'; + +class UserInfoView { + private editInfoButton: ButtonModel; + + private userInfoWrapper: HTMLDivElement; + + constructor() { + this.userInfoWrapper = this.createUserInfoWrapper(); + this.editInfoButton = this.createEditInfoButton(); + this.showUserInfo(); + } + + private createEditInfoButton(): ButtonModel { + this.editInfoButton = new ButtonModel({ + classes: [styles.editInfoButton], + text: BUTTON_TEXT[getStore().getState().currentLanguage].EDIT_INFO, + }); + + observeCurrentLanguage(this.editInfoButton.getHTML(), BUTTON_TEXT, BUTTON_TEXT_KEYS.EDIT_INFO); + + return this.editInfoButton; + } + + private createUserElement( + text: string, + tag: keyof HTMLElementTagNameMap = 'li', + classes: string[] = [styles.info], + ): HTMLElement { + return createBaseElement({ + cssClasses: classes, + innerContent: text, + tag, + }); + } + + private createUserInfoWrapper(): HTMLDivElement { + this.userInfoWrapper = createBaseElement({ + cssClasses: [styles.userInfoWrapper, styles.hidden], + tag: 'div', + }); + return this.userInfoWrapper; + } + + private showUserInfo(): boolean { + const { currentUser } = getStore().getState(); + if (!currentUser) { + return false; + } + + const { birthDate, email, firstName, lastName } = currentUser; + + const nameWrapper = document.createDocumentFragment(); + nameWrapper.append( + this.createUserElement(userInfoName(firstName)), + this.createUserElement(userInfoLastName(lastName)), + this.editInfoButton.getHTML(), + ); + + const userInfoWrapper = document.createDocumentFragment(); + userInfoWrapper.append( + nameWrapper, + this.createUserElement(userInfoDateOfBirth(birthDate)), + this.createUserElement(userInfoEmail(email)), + ); + this.userInfoWrapper.append(userInfoWrapper); + + observeStore(selectCurrentLanguage, () => { + clearOutElement(this.userInfoWrapper); + this.showUserInfo(); + }); + + return true; + } + + public getEditInfoButton(): ButtonModel { + return this.editInfoButton; + } + + public getHTML(): HTMLDivElement { + return this.userInfoWrapper; + } + + public hide(): void { + this.userInfoWrapper.classList.add(styles.hidden); + } + + public show(): void { + this.userInfoWrapper.classList.remove(styles.hidden); + } +} +export default UserInfoView; diff --git a/src/entities/UserInfo/view/userInfoView.module.scss b/src/entities/UserInfo/view/userInfoView.module.scss new file mode 100644 index 00000000..499ce850 --- /dev/null +++ b/src/entities/UserInfo/view/userInfoView.module.scss @@ -0,0 +1,69 @@ +.editInfoButton { + grid-column: 2 span; + grid-row: 5; + margin: 0 auto; + border-radius: var(--medium-br); + padding: calc(var(--small-offset) / 3) var(--small-offset); + height: max-content; + max-width: max-content; + font: var(--regular-font); + letter-spacing: 1px; + color: var(--white); + background-color: var(--steam-green-800); + transition: + color 0.2s, + background-color 0.2s; + + &:focus { + background-color: var(--steam-green-700); + } + + @media (hover: hover) { + &:hover { + background-color: var(--steam-green-700); + } + } + + &:disabled { + background-color: var(--noble-gray-300); + pointer-events: none; + } +} + +.info { + grid-column: 2 span; + max-width: 100%; + word-break: break-all; + + @media (max-width: 810px) { + font: var(--medium-font); + } + + @media (max-width: 500px) { + font: var(--regular-font); + } +} + +.userInfoWrapper { + position: relative; + display: grid; + align-items: center; + grid-template-columns: repeat(2, 1fr); + grid-template-rows: repeat(5, auto); + margin: 0 var(--small-offset); + padding: var(--small-offset); + min-width: 40%; + max-width: fit-content; + box-shadow: var(--mellow-shadow-100); + font: var(--extra-regular-font); + letter-spacing: 1px; + color: var(--noble-gray-900); + background-color: var(--noble-gray-tr-800); + gap: var(--small-offset); +} + +.hidden { + display: none; + opacity: 0; + visibility: hidden; +} diff --git a/src/features/InputFieldValidator/validators/validators.ts b/src/features/InputFieldValidator/validators/validators.ts index 507ad1fb..7b9c0a1f 100644 --- a/src/features/InputFieldValidator/validators/validators.ts +++ b/src/features/InputFieldValidator/validators/validators.ts @@ -6,7 +6,7 @@ import COUNTRIES_LIST from '@/shared/constants/countriesList.ts'; import { USER_ADDRESS_TYPE } 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 { maxAgeMessage, maxLengthMessage, minAgeMessage, minLengthMessage } from '@/shared/utils/messageTemplates.ts'; import { postcodeValidator } from 'postcode-validator'; export const checkMaxLength = (value: string, validParams: InputFieldValidatorParams): boolean | string => { diff --git a/src/features/ProductFilters/view/ProductFiltersView.ts b/src/features/ProductFilters/view/ProductFiltersView.ts index c57fa855..a5317b13 100644 --- a/src/features/ProductFilters/view/ProductFiltersView.ts +++ b/src/features/ProductFilters/view/ProductFiltersView.ts @@ -7,8 +7,8 @@ import InputModel from '@/shared/Input/model/InputModel.ts'; import LinkModel from '@/shared/Link/model/LinkModel.ts'; import getStore from '@/shared/Store/Store.ts'; import observeStore, { selectCurrentLanguage } from '@/shared/Store/observer.ts'; -import { BUTTON_TEXT, LANGUAGE_CHOICE } from '@/shared/constants/buttons.ts'; -import { AUTOCOMPLETE_OPTION } from '@/shared/constants/common.ts'; +import { BUTTON_TEXT } from '@/shared/constants/buttons.ts'; +import { AUTOCOMPLETE_OPTION, LANGUAGE_CHOICE } from '@/shared/constants/common.ts'; import { META_FILTERS, META_FILTERS_ID, PRICE_RANGE_LABEL, TITLE } from '@/shared/constants/filters.ts'; import { INPUT_TYPE } from '@/shared/constants/forms.ts'; import { PAGE_ID } from '@/shared/constants/pages.ts'; diff --git a/src/global.d.ts b/src/global.d.ts index f4364473..690879fa 100644 --- a/src/global.d.ts +++ b/src/global.d.ts @@ -12,6 +12,11 @@ declare module '*.svg' { export default content; } +declare module '*.png' { + const content: string; + export default content; +} + interface ImportMeta { env: { [key: string]: boolean | string | undefined; diff --git a/src/pages/LoginPage/model/LoginPageModel.ts b/src/pages/LoginPage/model/LoginPageModel.ts index c7f9b8bc..aecaea30 100644 --- a/src/pages/LoginPage/model/LoginPageModel.ts +++ b/src/pages/LoginPage/model/LoginPageModel.ts @@ -2,13 +2,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 serverMessageModel from '@/shared/ServerMessage/model/ServerMessageModel.ts'; import getStore from '@/shared/Store/Store.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 observeStore, { selectIsUserLoggedIn } from '@/shared/Store/observer.ts'; import { PAGE_ID, PAGE_LINK_TEXT, PAGE_LINK_TEXT_KEYS } from '@/shared/constants/pages.ts'; import observeCurrentLanguage from '@/shared/utils/observeCurrentLanguage.ts'; +import showErrorMessage from '@/shared/utils/userMessage.ts'; import LoginFormModel from '@/widgets/LoginForm/model/LoginFormModel.ts'; import LoginPageView from '../view/LoginPageView.ts'; @@ -34,10 +33,7 @@ class LoginPageModel implements Page { await this.router.navigateTo(PAGE_ID.MAIN_PAGE); return currentUser; } catch (error) { - serverMessageModel.showServerMessage( - SERVER_MESSAGE[getStore().getState().currentLanguage].BAD_REQUEST, - MESSAGE_STATUS.ERROR, - ); + showErrorMessage(); return null; } } @@ -47,16 +43,11 @@ class LoginPageModel implements Page { private init(): boolean { getStore().dispatch(setCurrentPage(PAGE_ID.LOGIN_PAGE)); - this.checkAuthUser().catch(() => { - serverMessageModel.showServerMessage( - SERVER_MESSAGE[getStore().getState().currentLanguage].BAD_REQUEST, - MESSAGE_STATUS.ERROR, - ); - }); + this.checkAuthUser().catch(() => showErrorMessage()); this.view.getAuthWrapper().append(this.loginForm.getHTML()); this.loginForm.getFirstInputField().getView().getInput().getHTML().focus(); this.setRegisterLinkHandler(); - observeStore(selectCurrentUser, () => this.checkAuthUser()); + observeStore(selectIsUserLoggedIn, () => this.checkAuthUser()); return true; } @@ -65,10 +56,7 @@ class LoginPageModel implements Page { try { await this.router.navigateTo(PAGE_ID.REGISTRATION_PAGE); } catch (error) { - serverMessageModel.showServerMessage( - SERVER_MESSAGE[getStore().getState().currentLanguage].BAD_REQUEST, - MESSAGE_STATUS.ERROR, - ); + showErrorMessage(); } } diff --git a/src/pages/RegistrationPage/model/RegistrationPageModel.ts b/src/pages/RegistrationPage/model/RegistrationPageModel.ts index 951ed963..a488a868 100644 --- a/src/pages/RegistrationPage/model/RegistrationPageModel.ts +++ b/src/pages/RegistrationPage/model/RegistrationPageModel.ts @@ -1,13 +1,12 @@ import type RouterModel from '@/app/Router/model/RouterModel.ts'; import type { Page } from '@/shared/types/common.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 showErrorMessage from '@/shared/utils/userMessage.ts'; import RegisterFormModel from '@/widgets/RegistrationForm/model/RegistrationFormModel.ts'; import RegistrationPageView from '../view/RegistrationPageView.ts'; @@ -38,10 +37,7 @@ class RegistrationPageModel implements Page { try { await this.router.navigateTo(PAGE_ID.LOGIN_PAGE); } catch { - serverMessageModel.showServerMessage( - SERVER_MESSAGE[getStore().getState().currentLanguage].BAD_REQUEST, - MESSAGE_STATUS.ERROR, - ); + showErrorMessage(); } } diff --git a/src/pages/UserProfilePage/model/UserProfilePageModel.ts b/src/pages/UserProfilePage/model/UserProfilePageModel.ts index 6c531b1d..a1b44e63 100644 --- a/src/pages/UserProfilePage/model/UserProfilePageModel.ts +++ b/src/pages/UserProfilePage/model/UserProfilePageModel.ts @@ -1,23 +1,89 @@ +import type RouterModel from '@/app/Router/model/RouterModel.ts'; import type { Page } from '@/shared/types/common.ts'; +import UserAddressModel from '@/entities/UserAddress/model/UserAddressModel.ts'; +import UserInfoModel from '@/entities/UserInfo/model/UserInfoModel.ts'; +import getCustomerModel from '@/shared/API/customer/model/CustomerModel.ts'; import getStore from '@/shared/Store/Store.ts'; -import { setCurrentPage } from '@/shared/Store/actions.ts'; +import { setCurrentPage, setCurrentUser, switchIsUserLoggedIn } from '@/shared/Store/actions.ts'; import { PAGE_ID } from '@/shared/constants/pages.ts'; +import showErrorMessage from '@/shared/utils/userMessage.ts'; import UserProfilePageView from '../view/UserProfilePageView.ts'; class UserProfilePageModel implements Page { + private addresses = new UserAddressModel(); + + private router: RouterModel; + + private userInfo = new UserInfoModel(); + private view: UserProfilePageView; - constructor(parent: HTMLDivElement) { + constructor(parent: HTMLDivElement, router: RouterModel) { + this.router = router; this.view = new UserProfilePageView(parent); + + this.setAddressesLinkHandler(); + this.setPersonalInfoLinkHandler(); this.init(); } + private addressesLinkHandler(): void { + this.userInfo.hide(); + this.addresses.show(); + } + private init(): void { + this.view.getUserProfileWrapper().append(this.userInfo.getHTML(), this.addresses.getHTML()); + this.setAccountLogoutButtonHandler(); getStore().dispatch(setCurrentPage(PAGE_ID.USER_PROFILE_PAGE)); } + private async logoutHandler(): Promise { + localStorage.clear(); + getStore().dispatch(setCurrentUser(null)); + getStore().dispatch(switchIsUserLoggedIn(false)); + try { + getCustomerModel().logout(); + await this.router.navigateTo(PAGE_ID.LOGIN_PAGE); + } catch { + showErrorMessage(); + } + return true; + } + + private personalInfoLinkHandler(): void { + this.addresses.hide(); + this.userInfo.show(); + } + + private setAccountLogoutButtonHandler(): boolean { + const logoutButton = this.view.getAccountLogoutButton(); + logoutButton.getHTML().addEventListener('click', async () => { + try { + await this.logoutHandler(); + } catch { + showErrorMessage(); + } + }); + return true; + } + + private setAddressesLinkHandler(): void { + const addressesLink = this.view.getAddressesLink(); + addressesLink.getHTML().addEventListener('click', () => { + this.addressesLinkHandler(); + }); + } + + private setPersonalInfoLinkHandler(): void { + const personalInfoLink = this.view.getPersonalInfoLink(); + personalInfoLink.getHTML().addEventListener('click', () => { + this.personalInfoLinkHandler(); + }); + } + public getHTML(): HTMLDivElement { return this.view.getHTML(); } diff --git a/src/pages/UserProfilePage/view/UserProfilePageView.ts b/src/pages/UserProfilePage/view/UserProfilePageView.ts index c2e4a0de..ad1c3470 100644 --- a/src/pages/UserProfilePage/view/UserProfilePageView.ts +++ b/src/pages/UserProfilePage/view/UserProfilePageView.ts @@ -1,31 +1,237 @@ +import ButtonModel from '@/shared/Button/model/ButtonModel.ts'; +import LinkModel from '@/shared/Link/model/LinkModel.ts'; +import getStore from '@/shared/Store/Store.ts'; +import { BUTTON_TEXT, BUTTON_TEXT_KEYS } from '@/shared/constants/buttons.ts'; +import { USER_PROFILE_MENU_LINK } from '@/shared/constants/links.ts'; +import { USER_INFO_MENU_LINK, USER_INFO_MENU_LINK_KEYS } from '@/shared/constants/pages.ts'; +import clearOutElement from '@/shared/utils/clearOutElement.ts'; import createBaseElement from '@/shared/utils/createBaseElement.ts'; +import observeCurrentLanguage from '@/shared/utils/observeCurrentLanguage.ts'; import styles from './userProfilePageView.module.scss'; class UserProfilePageView { + private accountLogoutButton: ButtonModel; + + private accountMenu: HTMLUListElement; + + private addressesLink: LinkModel; + + private links: LinkModel[] = []; + + private ordersLink: LinkModel; + private page: HTMLDivElement; private parent: HTMLDivElement; + private personalInfoLink: LinkModel; + + private supportLink: LinkModel; + + private userProfileWrapper: HTMLDivElement; + + private wishListLink: LinkModel; + constructor(parent: HTMLDivElement) { this.parent = parent; - this.parent.innerHTML = ''; + clearOutElement(this.parent); + + this.accountLogoutButton = this.createLogoutButton(); + this.personalInfoLink = this.createPersonalInfoLink(); + this.addressesLink = this.createAddressesLink(); + this.ordersLink = this.createOrdersLink(); + this.supportLink = this.createSupportLink(); + this.wishListLink = this.createWishListLink(); + this.accountMenu = this.createAccountMenu(); + + this.setLinksHandlers(); + this.userProfileWrapper = this.createUserProfileWrapper(); this.page = this.createHTML(); } + private createAccountMenu(): HTMLUListElement { + this.accountMenu = createBaseElement({ + cssClasses: [styles.accountMenu], + tag: 'ul', + }); + + this.links.forEach((link) => { + const li = createBaseElement({ + cssClasses: [styles.accountMenuItem], + tag: 'li', + }); + li.append(link.getHTML()); + + this.accountMenu.append(li); + }); + this.accountMenu.append(this.accountLogoutButton.getHTML()); + return this.accountMenu; + } + + private createAddressesLink(): LinkModel { + this.addressesLink = new LinkModel({ + attrs: { + href: USER_PROFILE_MENU_LINK.ADDRESSES, + }, + classes: [styles.link], + text: USER_INFO_MENU_LINK[getStore().getState().currentLanguage].ADDRESSES, + }); + this.links.push(this.addressesLink); + + observeCurrentLanguage(this.addressesLink.getHTML(), USER_INFO_MENU_LINK, USER_INFO_MENU_LINK_KEYS.ADDRESSES); + + return this.addressesLink; + } + private createHTML(): HTMLDivElement { this.page = createBaseElement({ cssClasses: [styles.userProfilePage], tag: 'div', }); + this.page.append(this.userProfileWrapper); this.parent.append(this.page); return this.page; } + private createLogoutButton(): ButtonModel { + this.accountLogoutButton = new ButtonModel({ + classes: [styles.logoutButton], + text: BUTTON_TEXT[getStore().getState().currentLanguage].LOG_OUT, + }); + + observeCurrentLanguage(this.accountLogoutButton.getHTML(), BUTTON_TEXT, BUTTON_TEXT_KEYS.LOG_OUT); + + return this.accountLogoutButton; + } + + private createOrdersLink(): LinkModel { + this.ordersLink = new LinkModel({ + attrs: { + href: USER_PROFILE_MENU_LINK.ORDERS, + }, + classes: [styles.link], + text: USER_INFO_MENU_LINK[getStore().getState().currentLanguage].ORDERS, + }); + this.links.push(this.ordersLink); + + observeCurrentLanguage(this.ordersLink.getHTML(), USER_INFO_MENU_LINK, USER_INFO_MENU_LINK_KEYS.ORDERS); + + return this.ordersLink; + } + + private createPersonalInfoLink(): LinkModel { + this.personalInfoLink = new LinkModel({ + attrs: { + href: USER_PROFILE_MENU_LINK.PERSONAL_INFO, + }, + classes: [styles.link], + text: USER_INFO_MENU_LINK[getStore().getState().currentLanguage].PERSONAL_INFO, + }); + this.links.push(this.personalInfoLink); + + observeCurrentLanguage( + this.personalInfoLink.getHTML(), + USER_INFO_MENU_LINK, + USER_INFO_MENU_LINK_KEYS.PERSONAL_INFO, + ); + this.personalInfoLink.getHTML().classList.add(styles.active); + this.personalInfoLink.setDisabled(); + return this.personalInfoLink; + } + + private createSupportLink(): LinkModel { + this.supportLink = new LinkModel({ + attrs: { + href: USER_PROFILE_MENU_LINK.SUPPORT, + }, + classes: [styles.link], + text: USER_INFO_MENU_LINK[getStore().getState().currentLanguage].SUPPORT, + }); + this.links.push(this.supportLink); + + observeCurrentLanguage(this.supportLink.getHTML(), USER_INFO_MENU_LINK, USER_INFO_MENU_LINK_KEYS.SUPPORT); + + return this.supportLink; + } + + private createUserProfileWrapper(): HTMLDivElement { + this.userProfileWrapper = createBaseElement({ + cssClasses: [styles.userProfileWrapper], + tag: 'div', + }); + + this.userProfileWrapper.append(this.accountMenu); + return this.userProfileWrapper; + } + + private createWishListLink(): LinkModel { + this.wishListLink = new LinkModel({ + attrs: { + href: USER_PROFILE_MENU_LINK.WISHLIST, + }, + classes: [styles.link], + text: USER_INFO_MENU_LINK[getStore().getState().currentLanguage].WISHLIST, + }); + this.links.push(this.wishListLink); + + observeCurrentLanguage(this.wishListLink.getHTML(), USER_INFO_MENU_LINK, USER_INFO_MENU_LINK_KEYS.WISHLIST); + + return this.wishListLink; + } + + private setLinksHandlers(): boolean { + this.links.forEach((link) => { + link.getHTML().addEventListener('click', (event) => { + event.preventDefault(); + // TBD replace route with route + const route = link.getHTML().attributes.getNamedItem('href')?.value; + if (route) { + this.switchActiveLink(route); + } + }); + }); + return true; + } + + private switchActiveLink(route: string): void { + this.links.forEach((link) => { + link.getHTML().classList.remove(styles.active); + link.setEnabled(); + }); + + const currentLink = this.links.find((link) => link.getHTML().attributes.getNamedItem('href')?.value === route); + + if (currentLink) { + currentLink.getHTML().classList.add(styles.active); + currentLink.setDisabled(); + } + } + + public getAccountLogoutButton(): ButtonModel { + return this.accountLogoutButton; + } + + public getAddressesLink(): LinkModel { + return this.addressesLink; + } + public getHTML(): HTMLDivElement { return this.page; } + + public getLinks(): LinkModel[] { + return this.links; + } + + public getPersonalInfoLink(): LinkModel { + return this.personalInfoLink; + } + + public getUserProfileWrapper(): HTMLDivElement { + return this.userProfileWrapper; + } } export default UserProfilePageView; diff --git a/src/pages/UserProfilePage/view/userProfilePageView.module.scss b/src/pages/UserProfilePage/view/userProfilePageView.module.scss index 0e6a610d..2dd29a2e 100644 --- a/src/pages/UserProfilePage/view/userProfilePageView.module.scss +++ b/src/pages/UserProfilePage/view/userProfilePageView.module.scss @@ -5,13 +5,131 @@ animation: show 0.2s ease-out forwards; } -@keyframes show { - 0% { +.userProfileWrapper { + display: flex; + flex-flow: row nowrap; + justify-content: space-evenly; +} + +.addressesWrapper { + position: relative; + display: grid; + align-items: center; + grid-template-columns: repeat(2, 1fr); + grid-template-rows: repeat(6, auto); + margin: 0 var(--small-offset); + padding: var(--small-offset); + min-width: 40%; + max-width: fit-content; + box-shadow: var(--mellow-shadow-100); + font: var(--extra-regular-font); + letter-spacing: 1px; + color: var(--noble-gray-900); + background-color: var(--noble-gray-tr-800); + gap: var(--small-offset); +} + +.address { + grid-column: 2 span; + max-width: 100%; + word-break: break-all; + + @media (max-width: 810px) { + font: var(--medium-font); + } + + @media (max-width: 500px) { + font: var(--regular-font); + } +} + +.accountMenu { + display: flex; + flex-flow: column wrap; + padding: var(--small-offset); + height: fit-content; + max-width: fit-content; + background-color: var(--white-tr); + gap: var(--small-offset); +} + +.accountMenuItem { + width: fit-content; + height: fit-content; +} + +.logoutButton { + border-radius: var(--medium-br); + padding: calc(var(--small-offset) / 3) var(--small-offset); + font: var(--regular-font); + letter-spacing: 1px; + color: var(--white); + background-color: var(--steam-green-800); + transition: + color 0.2s, + background-color 0.2s; + + &:focus { + background-color: var(--steam-green-700); + } + + @media (hover: hover) { + &:hover { + background-color: var(--steam-green-700); + } + } + + &:disabled { + background-color: var(--noble-gray-300); + pointer-events: none; + } +} + +.link { + position: relative; + display: flex; + align-items: center; + height: 100%; + font: var(--regular-font); + letter-spacing: 1px; + text-transform: uppercase; + color: var(--noble-gray-800); + transition: color 0.2s; + + &::after { + content: ''; + position: absolute; + left: 0; + bottom: -15px; + width: 100%; + height: 2px; + background-color: currentcolor; opacity: 0; + transform: scaleX(0); + transform-origin: center; + transition: + transform 0.2s, + opacity 0.2s; } - 100% { - display: block; + @media (hover: hover) { + &:hover { + color: var(--steam-green-800); + + &::after { + opacity: 1; + transform: scaleX(1); + } + } + } +} + +.active { + color: var(--steam-green-800); + opacity: 1; + + &::after { opacity: 1; + transform: scaleX(1); } } diff --git a/src/shared/API/product/model/ProductModel.ts b/src/shared/API/product/model/ProductModel.ts index 05621b40..e57b56d8 100644 --- a/src/shared/API/product/model/ProductModel.ts +++ b/src/shared/API/product/model/ProductModel.ts @@ -12,7 +12,7 @@ import type { } from '@commercetools/platform-sdk'; import getStore from '@/shared/Store/Store.ts'; -import { setCategories, setProducts } from '@/shared/Store/actions.ts'; +import { setCategories } from '@/shared/Store/actions.ts'; import { PRICE_FRACTIONS } from '@/shared/constants/product.ts'; import getSize from '@/shared/utils/size.ts'; @@ -295,7 +295,8 @@ export class ProductModel { const sizeCount = this.getSizeProductCountFromData(data); const categoryCount = this.getCategoriesProductCountFromData(data); if (products) { - getStore().dispatch(setProducts(products)); + // TBD Remove when possible to improve performance + // getStore().dispatch(setProducts(products)); } const result: ProductWithCount = { categoryCount, diff --git a/src/shared/Modal/model/ModalModel.ts b/src/shared/Modal/model/ModalModel.ts new file mode 100644 index 00000000..d34f79c1 --- /dev/null +++ b/src/shared/Modal/model/ModalModel.ts @@ -0,0 +1,25 @@ +import ModalView from '../view/ModalView.ts'; + +class ModalModel { + private view = new ModalView(); + + public getHTML(): HTMLDivElement { + return this.view.getHTML(); + } + + public hide(): void { + this.view.hide(); + } + + public setContent(content: HTMLDivElement): void { + this.view.setContent(content); + } + + public show(): void { + this.view.show(); + } +} + +const modal = new ModalModel(); + +export default modal; diff --git a/src/shared/Modal/view/ModalView.ts b/src/shared/Modal/view/ModalView.ts new file mode 100644 index 00000000..c0d57bc8 --- /dev/null +++ b/src/shared/Modal/view/ModalView.ts @@ -0,0 +1,80 @@ +import createBaseElement from '@/shared/utils/createBaseElement.ts'; + +import styles from './modalView.module.scss'; + +class ModalView { + private modal: HTMLDivElement; + + private modalContent: HTMLDivElement; + + private modalOverlay: HTMLDivElement; + + constructor() { + this.modalContent = this.createModalContent(); + this.modalOverlay = this.createModalOverlay(); + this.modal = this.createHTML(); + } + + private createHTML(): HTMLDivElement { + this.modal = createBaseElement({ + cssClasses: [styles.modal, styles.modal_hidden], + tag: 'div', + }); + + this.modalOverlay.append(this.modalContent); + this.modal.append(this.modalOverlay); + + return this.modal; + } + + private createModalContent(): HTMLDivElement { + this.modalContent = createBaseElement({ + cssClasses: [styles.modalContent, styles.modalContent_hidden], + tag: 'div', + }); + + return this.modalContent; + } + + private createModalOverlay(): HTMLDivElement { + this.modalOverlay = createBaseElement({ + cssClasses: [styles.modalOverlay, styles.modalOverlay_hidden], + tag: 'div', + }); + + return this.modalOverlay; + } + + public getHTML(): HTMLDivElement { + return this.modal; + } + + public getModalContent(): HTMLDivElement { + return this.modalContent; + } + + public getModalOverlay(): HTMLDivElement { + return this.modalOverlay; + } + + public hide(): void { + this.modal.classList.add(styles.modal_hidden); + this.modalOverlay.classList.add(styles.modalOverlay_hidden); + this.modalContent.classList.add(styles.modalContent_hidden); + document.body.classList.remove('stop-scroll'); + } + + public setContent(content: HTMLDivElement): void { + this.modalContent.innerHTML = ''; + this.modalContent.append(content); + } + + public show(): void { + this.modal.classList.remove(styles.modal_hidden); + this.modalOverlay.classList.remove(styles.modalOverlay_hidden); + this.modalContent.classList.remove(styles.modalContent_hidden); + document.body.classList.add('stop-scroll'); + } +} + +export default ModalView; diff --git a/src/shared/Modal/view/modalView.module.scss b/src/shared/Modal/view/modalView.module.scss new file mode 100644 index 00000000..4cfb54c8 --- /dev/null +++ b/src/shared/Modal/view/modalView.module.scss @@ -0,0 +1,64 @@ +.modal { + position: fixed; + left: 0; + top: 0; + width: 100%; + height: 100%; + opacity: 1; + visibility: visible; + transition: + opacity 0.3s, + visibility 0.3s; + + &_hidden { + opacity: 0; + visibility: hidden; + } +} + +.modalOverlay { + position: absolute; + left: 0; + top: 0; + width: 100%; + height: 100%; + background-color: #27272760; + opacity: 1; + visibility: visible; + transition: + opacity 0.3s, + visibility 0.3s; + + &_hidden { + opacity: 0; + visibility: hidden; + } +} + +.modalContent { + position: absolute; + left: 50%; + top: 50%; + border: 2px solid var(--noble-black-500); + border-radius: var(--border-radius); + padding: var(--small-offset); + font: var(--small-font); + letter-spacing: 1px; + color: var(--steam-green-500); + background-color: var(--noble-black-700); + opacity: 1; + visibility: visible; + transform: translate(-50%, -50%); + transition: + opacity 0.3s, + visibility 0.3s; + + &_hidden { + opacity: 0; + visibility: hidden; + } +} + +.stopScroll { + overflow-y: hidden; +} diff --git a/src/shared/ServerMessage/model/ServerMessageModel.ts b/src/shared/ServerMessage/model/ServerMessageModel.ts index fcb78dce..96a78000 100644 --- a/src/shared/ServerMessage/model/ServerMessageModel.ts +++ b/src/shared/ServerMessage/model/ServerMessageModel.ts @@ -1,12 +1,12 @@ -import type { MessageStatusType } from '@/shared/constants/messages.ts'; +import type { MessageStatusType, ServerMessageKeysType } from '@/shared/constants/messages.ts'; import ServerMessageView from '../view/ServerMessageView.ts'; class ServerMessageModel { private view: ServerMessageView = new ServerMessageView(); - public showServerMessage(message: string, status: MessageStatusType): boolean { - return this.view.setServerMessage(message, status); + public showServerMessage(key: ServerMessageKeysType, status: MessageStatusType, message?: string): boolean { + return this.view.setServerMessage(key, status, message); } } diff --git a/src/shared/ServerMessage/view/ServerMessageView.ts b/src/shared/ServerMessage/view/ServerMessageView.ts index 490b7e3e..0bc2739f 100644 --- a/src/shared/ServerMessage/view/ServerMessageView.ts +++ b/src/shared/ServerMessage/view/ServerMessageView.ts @@ -1,5 +1,8 @@ +import type { MessageStatusType, ServerMessageKeysType } from '@/shared/constants/messages.ts'; + +import getStore from '@/shared/Store/Store.ts'; import SERVER_MESSAGE_ANIMATE_DETAILS from '@/shared/constants/animations.ts'; -import { MESSAGE_STATUS, type MessageStatusType } from '@/shared/constants/messages.ts'; +import { MESSAGE_STATUS, SERVER_MESSAGE } from '@/shared/constants/messages.ts'; import createBaseElement from '@/shared/utils/createBaseElement.ts'; import styles from './serverMessageView.module.scss'; @@ -46,10 +49,13 @@ class ServerMessageView { return this.serverWrapper; } - public setServerMessage(message: string, status: MessageStatusType): boolean { + public setServerMessage(keyOrMessage: ServerMessageKeysType, status: MessageStatusType, message?: string): boolean { this.serverWrapper.classList.toggle(styles.error, status === MESSAGE_STATUS.ERROR); this.serverWrapper.classList.toggle(styles.success, status === MESSAGE_STATUS.SUCCESS); - this.serverMessage.textContent = message; + + this.serverMessage.textContent = SERVER_MESSAGE[getStore().getState().currentLanguage][keyOrMessage]; + this.serverMessage.textContent = message || this.serverMessage.textContent; + this.startAnimation(); return true; } diff --git a/src/shared/Store/actions.ts b/src/shared/Store/actions.ts index 931a4a16..37f3b31b 100644 --- a/src/shared/Store/actions.ts +++ b/src/shared/Store/actions.ts @@ -1,4 +1,4 @@ -import type { LanguageChoiceType } from '../constants/buttons.ts'; +import type { LanguageChoiceType } from '../constants/common.ts'; import type { PageIdType } from '../constants/pages.ts'; import type { Category, Product } from '../types/product'; import type { SelectedFilters } from '../types/productFilters'; diff --git a/src/shared/Store/reducer.ts b/src/shared/Store/reducer.ts index e443dc01..e78a50fc 100644 --- a/src/shared/Store/reducer.ts +++ b/src/shared/Store/reducer.ts @@ -1,5 +1,5 @@ /* eslint-disable max-lines-per-function */ -import type { LanguageChoiceType } from '../constants/buttons.ts'; +import type { LanguageChoiceType } from '../constants/common.ts'; import type { PageIdType } from '../constants/pages.ts'; import type { Category, Product } from '../types/product.ts'; import type { SelectedFilters } from '../types/productFilters.ts'; diff --git a/src/shared/constants/buttons.ts b/src/shared/constants/buttons.ts index df51cd02..762c8fcd 100644 --- a/src/shared/constants/buttons.ts +++ b/src/shared/constants/buttons.ts @@ -7,6 +7,7 @@ export const BUTTON_TYPE = { export const BUTTON_TEXT = { en: { BACK_TO_MAIN: 'Back to main', + EDIT_INFO: 'Edit', LOG_OUT: 'Log out', LOGIN: 'Login', REGISTRATION: 'Register', @@ -14,6 +15,7 @@ export const BUTTON_TEXT = { }, ru: { BACK_TO_MAIN: 'Вернуться на главную', + EDIT_INFO: 'Редактировать', LOG_OUT: 'Выйти', LOGIN: 'Войти', REGISTRATION: 'Регистрация', @@ -23,6 +25,7 @@ export const BUTTON_TEXT = { export const BUTTON_TEXT_KEYS = { BACK_TO_MAIN: 'BACK_TO_MAIN', + EDIT_INFO: 'EDIT_INFO', LOG_OUT: 'LOG_OUT', LOGIN: 'LOGIN', REGISTRATION: 'REGISTRATION', @@ -36,13 +39,6 @@ export const IS_DISABLED = { ENABLED: false, } as const; -export const LANGUAGE_CHOICE = { - EN: 'en', - RU: 'ru', -} as const; - -export type LanguageChoiceType = (typeof LANGUAGE_CHOICE)[keyof typeof LANGUAGE_CHOICE]; - export const MORE_TEXT = { en: { HIDE: 'Hide', MORE: 'More' }, ru: { HIDE: 'Скрыть', MORE: 'Подробнее' }, diff --git a/src/shared/constants/common.ts b/src/shared/constants/common.ts index abfa72e6..2045744c 100644 --- a/src/shared/constants/common.ts +++ b/src/shared/constants/common.ts @@ -1,5 +1,11 @@ -// eslint-disable-next-line import/prefer-default-export export const AUTOCOMPLETE_OPTION = { OFF: 'off', ON: 'on', } as const; + +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/links.ts b/src/shared/constants/links.ts new file mode 100644 index 00000000..5b8bb976 --- /dev/null +++ b/src/shared/constants/links.ts @@ -0,0 +1,8 @@ +/* eslint-disable import/prefer-default-export */ +export const USER_PROFILE_MENU_LINK = { + ADDRESSES: '#addresses', + ORDERS: '#orders', + PERSONAL_INFO: '#personal-info', + SUPPORT: '#support', + WISHLIST: '#wishlist', +} as const; diff --git a/src/shared/constants/messages.ts b/src/shared/constants/messages.ts index 91dc3e69..ed5c59f6 100644 --- a/src/shared/constants/messages.ts +++ b/src/shared/constants/messages.ts @@ -5,22 +5,31 @@ export const MESSAGE_STATUS = { export type MessageStatusType = (typeof MESSAGE_STATUS)[keyof typeof MESSAGE_STATUS]; +export const MESSAGE_STATUS_KEYS = { + ERROR: 'ERROR', + SUCCESS: 'SUCCESS', +} as const; + +export type MessageStatusKeysType = (typeof MESSAGE_STATUS_KEYS)[keyof typeof MESSAGE_STATUS_KEYS]; + export const SERVER_MESSAGE = { en: { BAD_REQUEST: 'Sorry, something went wrong. Try again later.', + GREETING: 'Hi! Welcome to our store. Enjoy shopping!', INCORRECT_PASSWORD: 'Please, enter a correct password', INVALID_EMAIL: "User with this email doesn't exist. Please, register first", LANGUAGE_CHANGED: 'Language preferences have been updated successfully', - SUCCESSFUL_LOGIN: 'Enjoy shopping!', + SUCCESSFUL_LOGIN: 'Welcome to our store. Enjoy shopping!', SUCCESSFUL_REGISTRATION: 'Your registration was successful', USER_EXISTS: 'User with this email already exists, please check your email', }, ru: { BAD_REQUEST: 'Извините, что-то пошло не так. Попробуйте позже.', + GREETING: 'Здравствуйте! Добро пожаловать в наш магазин. Приятных покупок!', INCORRECT_PASSWORD: 'Пожалуйста, введите правильный пароль', INVALID_EMAIL: 'Пользователь с таким адресом не существует. Пожалуйста, сначала зарегистрируйтесь', LANGUAGE_CHANGED: 'Настройки языка успешно обновлены', - SUCCESSFUL_LOGIN: 'Приятных покупок!', + SUCCESSFUL_LOGIN: 'Добро пожаловать в наш магазин. Приятных покупок!', SUCCESSFUL_REGISTRATION: 'Регистрация прошла успешно', USER_EXISTS: 'Пользователь с таким адресом уже существует, пожалуйста, проверьте свою почту', }, @@ -28,6 +37,7 @@ export const SERVER_MESSAGE = { export const SERVER_MESSAGE_KEYS = { BAD_REQUEST: 'BAD_REQUEST', + GREETING: 'GREETING', INCORRECT_PASSWORD: 'INCORRECT_PASSWORD', INVALID_EMAIL: 'INVALID_EMAIL', LANGUAGE_CHANGED: 'LANGUAGE_CHANGED', @@ -36,7 +46,7 @@ export const SERVER_MESSAGE_KEYS = { USER_EXISTS: 'USER_EXISTS', } as const; -export type ServerMessageKey = (typeof SERVER_MESSAGE_KEYS)[keyof typeof SERVER_MESSAGE_KEYS]; +export type ServerMessageKeysType = (typeof SERVER_MESSAGE_KEYS)[keyof typeof SERVER_MESSAGE_KEYS]; export const ERROR_MESSAGE = { en: { diff --git a/src/shared/constants/pages.ts b/src/shared/constants/pages.ts index 060f087b..1c77e282 100644 --- a/src/shared/constants/pages.ts +++ b/src/shared/constants/pages.ts @@ -100,4 +100,61 @@ export const PAGE_ID = { USER_PROFILE_PAGE: 'profile', } as const; +export const USER_INFO_TEXT = { + en: { + DATE_OF_BIRTH: 'Date of Birth: ', + DEFAULT_BILLING_ADDRESS: ' (default billing)', + DEFAULT_SHIPPING_ADDRESS: ' (default shipping)', + EMAIL: 'Email: ', + LAST_NAME: 'Last Name: ', + NAME: 'First Name: ', + }, + ru: { + DATE_OF_BIRTH: 'Дата рождения: ', + DEFAULT_BILLING_ADDRESS: ' (по умолчанию - для оплаты)', + DEFAULT_SHIPPING_ADDRESS: ' (по умолчанию - для доставки)', + EMAIL: 'Электронная почта: ', + LAST_NAME: 'Фамилия: ', + NAME: 'Имя: ', + }, +} as const; + +export const USER_INFO_TEXT_KEYS = { + DATE_OF_BIRTH: 'DATE_OF_BIRTH', + DEFAULT_BILLING_ADDRESS: 'DEFAULT_BILLING_ADDRESS', + DEFAULT_SHIPPING_ADDRESS: 'DEFAULT_SHIPPING_ADDRESS', + EMAIL: 'EMAIL', + LAST_NAME: 'LAST_NAME', + NAME: 'NAME', +} as const; + +export type UserInfoTextKeysType = (typeof USER_INFO_TEXT_KEYS)[keyof typeof USER_INFO_TEXT_KEYS]; + +export const USER_INFO_MENU_LINK = { + en: { + ADDRESSES: 'Addresses', + ORDERS: 'Orders', + PERSONAL_INFO: 'Personal Info', + SUPPORT: 'Support', + WISHLIST: 'Wishlist', + }, + ru: { + ADDRESSES: 'Адреса', + ORDERS: 'Заказы', + PERSONAL_INFO: 'Персональные данные', + SUPPORT: 'Поддержка', + WISHLIST: 'Избранное', + }, +} as const; + +export const USER_INFO_MENU_LINK_KEYS = { + ADDRESSES: 'ADDRESSES', + ORDERS: 'ORDERS', + PERSONAL_INFO: 'PERSONAL_INFO', + SUPPORT: 'SUPPORT', + WISHLIST: 'WISHLIST', +} as const; + +export type UserInfoMenuLinkKeysType = (typeof USER_INFO_MENU_LINK_KEYS)[keyof typeof USER_INFO_MENU_LINK_KEYS]; + export type PageIdType = (typeof PAGE_ID)[keyof typeof PAGE_ID]; diff --git a/src/shared/img/plant.png b/src/shared/img/plant.png new file mode 100644 index 00000000..bd05b115 Binary files /dev/null and b/src/shared/img/plant.png differ diff --git a/src/shared/img/tree.png b/src/shared/img/tree.png new file mode 100644 index 00000000..eadbbbae Binary files /dev/null and b/src/shared/img/tree.png differ diff --git a/src/shared/types/validation/language.ts b/src/shared/types/validation/language.ts new file mode 100644 index 00000000..9c911268 --- /dev/null +++ b/src/shared/types/validation/language.ts @@ -0,0 +1,8 @@ +import type { LanguageChoiceType } from '@/shared/constants/common'; + +import { LANGUAGE_CHOICE } from '@/shared/constants/common.ts'; + +const isLanguageChoiceType = (locale: string): locale is LanguageChoiceType => + locale === LANGUAGE_CHOICE.EN || locale === LANGUAGE_CHOICE.RU; + +export default isLanguageChoiceType; diff --git a/src/shared/utils/findKeyByValue.ts b/src/shared/utils/findKeyByValue.ts new file mode 100644 index 00000000..5fbe7cc0 --- /dev/null +++ b/src/shared/utils/findKeyByValue.ts @@ -0,0 +1,7 @@ +/* eslint-disable @typescript-eslint/no-unused-vars */ +function findKeyByValue(obj: { [key: string]: T }, value: T): null | string { + const foundEntry = Object.entries(obj).find(([_key, val]) => val === value); + return foundEntry ? foundEntry[0] : null; +} + +export default findKeyByValue; diff --git a/src/shared/utils/getCountryIndex.ts b/src/shared/utils/getCountryIndex.ts index 840594fe..cea2136b 100644 --- a/src/shared/utils/getCountryIndex.ts +++ b/src/shared/utils/getCountryIndex.ts @@ -1,6 +1,4 @@ -import type { LanguageChoiceType } from '../constants/buttons.ts'; - -import { LANGUAGE_CHOICE } from '../constants/buttons.ts'; +import { LANGUAGE_CHOICE, type LanguageChoiceType } from '../constants/common.ts'; import COUNTRIES_LIST from '../constants/countriesList.ts'; export const checkInputLanguage = (text: string): LanguageChoiceType => diff --git a/src/shared/utils/messageTemplate.ts b/src/shared/utils/messageTemplate.ts deleted file mode 100644 index 6399aced..00000000 --- a/src/shared/utils/messageTemplate.ts +++ /dev/null @@ -1,52 +0,0 @@ -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} ` : ''; - const ending = end ? `${end}` : ''; - return `${start}${variable}${ending}`; -}; - -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, ' символов'); - -const maxLengthMessageEn = (maxLength: number): string => - messageTemplate('Maximum length should not exceed', maxLength, ' characters'); - -export const maxLengthMessage = (maxLength: number): string => - getStore().getState().currentLanguage === LANGUAGE_CHOICE.EN - ? maxLengthMessageEn(maxLength) - : maxLengthMessageRu(maxLength); - -const maxAgeRu = (maxAge: number): string => messageTemplate('Вам должно быть не более', maxAge, ' лет'); - -const maxAgeEn = (maxAge: number): string => messageTemplate('You must be at most', maxAge, ' years old'); - -export const maxAgeMessage = (maxAge: number): string => - getStore().getState().currentLanguage === LANGUAGE_CHOICE.EN ? maxAgeEn(maxAge) : maxAgeRu(maxAge); - -const minAgeRu = (minAge: number): string => messageTemplate('Вам должно быть не менее', minAge, ' лет'); - -const minAgeEn = (minAge: number): string => messageTemplate('You must be at least', minAge, ' years old'); - -export const minAgeMessage = (minAge: number): string => - getStore().getState().currentLanguage === LANGUAGE_CHOICE.EN ? minAgeEn(minAge) : minAgeRu(minAge); - -const minLengthMessageRu = (minLength: number): string => - messageTemplate('Минимальная длина должна быть не менее', minLength, ' символов'); - -const minLengthMessageEn = (minLength: number): string => - messageTemplate('Minimum length should be at least', minLength, ' characters'); - -export const minLengthMessage = (minLength: number): string => - getStore().getState().currentLanguage === LANGUAGE_CHOICE.EN - ? minLengthMessageEn(minLength) - : minLengthMessageRu(minLength); diff --git a/src/shared/utils/messageTemplates.ts b/src/shared/utils/messageTemplates.ts new file mode 100644 index 00000000..fa56d403 --- /dev/null +++ b/src/shared/utils/messageTemplates.ts @@ -0,0 +1,70 @@ +import getStore from '../Store/Store.ts'; +import { LANGUAGE_CHOICE } from '../constants/common.ts'; +import { SERVER_MESSAGE } from '../constants/messages.ts'; +import { PAGE_DESCRIPTION, USER_INFO_TEXT } from '../constants/pages.ts'; + +const textTemplate = (beginning: string, variable: number | string, end?: string): string => { + const start = beginning ? `${beginning} ` : ''; + const ending = end ? `${end}` : ''; + return `${start}${variable}${ending}`; +}; + +export const userInfoName = (name: string): string => + textTemplate(USER_INFO_TEXT[getStore().getState().currentLanguage].NAME, name); + +export const userInfoLastName = (name: string): string => + textTemplate(USER_INFO_TEXT[getStore().getState().currentLanguage].LAST_NAME, name); + +export const userInfoEmail = (email: string): string => + textTemplate(USER_INFO_TEXT[getStore().getState().currentLanguage].EMAIL, email); + +export const userInfoDateOfBirth = (date: string): string => + textTemplate(USER_INFO_TEXT[getStore().getState().currentLanguage].DATE_OF_BIRTH, date); + +export const greeting = (name: string): string => + textTemplate(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 => + textTemplate('Максимальная длина не должна превышать', maxLength, ' символов'); + +const maxLengthMessageEn = (maxLength: number): string => + textTemplate('Maximum length should not exceed', maxLength, ' characters'); + +export const maxLengthMessage = (maxLength: number): string => + getStore().getState().currentLanguage === LANGUAGE_CHOICE.EN + ? maxLengthMessageEn(maxLength) + : maxLengthMessageRu(maxLength); + +const maxAgeRu = (maxAge: number): string => textTemplate('Вам должно быть не более', maxAge, ' лет'); + +export const defaultBillingAddress = (address: string): string => + textTemplate('', address, USER_INFO_TEXT[getStore().getState().currentLanguage].DEFAULT_BILLING_ADDRESS); + +export const defaultShippingAddress = (address: string): string => + textTemplate('', address, USER_INFO_TEXT[getStore().getState().currentLanguage].DEFAULT_SHIPPING_ADDRESS); + +const maxAgeEn = (maxAge: number): string => textTemplate('You must be at most', maxAge, ' years old'); + +export const maxAgeMessage = (maxAge: number): string => + getStore().getState().currentLanguage === LANGUAGE_CHOICE.EN ? maxAgeEn(maxAge) : maxAgeRu(maxAge); + +const minAgeRu = (minAge: number): string => textTemplate('Вам должно быть не менее', minAge, ' лет'); + +const minAgeEn = (minAge: number): string => textTemplate('You must be at least', minAge, ' years old'); + +export const minAgeMessage = (minAge: number): string => + getStore().getState().currentLanguage === LANGUAGE_CHOICE.EN ? minAgeEn(minAge) : minAgeRu(minAge); + +const minLengthMessageRu = (minLength: number): string => + textTemplate('Минимальная длина должна быть не менее', minLength, ' символов'); + +const minLengthMessageEn = (minLength: number): string => + textTemplate('Minimum length should be at least', minLength, ' characters'); + +export const minLengthMessage = (minLength: number): string => + getStore().getState().currentLanguage === LANGUAGE_CHOICE.EN + ? minLengthMessageEn(minLength) + : minLengthMessageRu(minLength); diff --git a/src/shared/utils/showBadRequestMessage.ts b/src/shared/utils/showBadRequestMessage.ts deleted file mode 100644 index 434a565b..00000000 --- a/src/shared/utils/showBadRequestMessage.ts +++ /dev/null @@ -1,11 +0,0 @@ -import serverMessageModel from '../ServerMessage/model/ServerMessageModel.ts'; -import getStore from '../Store/Store.ts'; -import { MESSAGE_STATUS, SERVER_MESSAGE } from '../constants/messages.ts'; - -const showBadRequestMessage = (): boolean => - serverMessageModel.showServerMessage( - SERVER_MESSAGE[getStore().getState().currentLanguage].BAD_REQUEST, - MESSAGE_STATUS.ERROR, - ); - -export default showBadRequestMessage; diff --git a/src/shared/utils/userMessage.ts b/src/shared/utils/userMessage.ts new file mode 100644 index 00000000..d3fbd7ef --- /dev/null +++ b/src/shared/utils/userMessage.ts @@ -0,0 +1,7 @@ +import serverMessageModel from '../ServerMessage/model/ServerMessageModel.ts'; +import { MESSAGE_STATUS, SERVER_MESSAGE_KEYS } from '../constants/messages.ts'; + +const showErrorMessage = (): boolean => + serverMessageModel.showServerMessage(SERVER_MESSAGE_KEYS.BAD_REQUEST, MESSAGE_STATUS.ERROR); + +export default showErrorMessage; diff --git a/src/widgets/Catalog/model/CatalogModel.ts b/src/widgets/Catalog/model/CatalogModel.ts index 2ea46d5e..a742b42f 100644 --- a/src/widgets/Catalog/model/CatalogModel.ts +++ b/src/widgets/Catalog/model/CatalogModel.ts @@ -16,7 +16,7 @@ import observeStore, { } from '@/shared/Store/observer.ts'; import { META_FILTERS } from '@/shared/constants/filters.ts'; import { LOADER_SIZE } from '@/shared/constants/sizes.ts'; -import showBadRequestMessage from '@/shared/utils/showBadRequestMessage.ts'; +import showErrorMessage from '@/shared/utils/userMessage.ts'; import CatalogView from '../view/CatalogView.ts'; @@ -53,7 +53,7 @@ class CatalogModel { const priceRange = await getProductModel().getPriceRange(); return { categoriesProductCount: categoryCount, priceRange, products, sizes: sizeCount }; } catch { - showBadRequestMessage(); + showErrorMessage(); } finally { loader.getHTML().remove(); } @@ -89,7 +89,7 @@ class CatalogModel { this.view.getLeftWrapper().append(this.productFilters.getDefaultFilters()); this.view.getRightWrapper().append(this.productFilters.getMetaFilters()); }) - .catch(() => showBadRequestMessage()); + .catch(() => showErrorMessage()); observeSetInStore(selectSelectedFiltersCategory, () => this.redrawProductList(this.getSelectedFilters())); observeStore(selectSelectedFiltersPrice, () => this.redrawProductList(this.getSelectedFilters())); @@ -111,7 +111,7 @@ class CatalogModel { this.view.switchEmptyList(!data?.products?.length); this.productFilters?.updateParams(data); }) - .catch(() => showBadRequestMessage()); + .catch(() => showErrorMessage()); } public getHTML(): HTMLDivElement { diff --git a/src/widgets/Header/model/HeaderModel.ts b/src/widgets/Header/model/HeaderModel.ts index 20a04949..7fc9c4c9 100644 --- a/src/widgets/Header/model/HeaderModel.ts +++ b/src/widgets/Header/model/HeaderModel.ts @@ -1,14 +1,13 @@ import type RouterModel from '@/app/Router/model/RouterModel.ts'; import NavigationModel from '@/entities/Navigation/model/NavigationModel.ts'; -import getCustomerModel, { CustomerModel } from '@/shared/API/customer/model/CustomerModel.ts'; -import serverMessageModel from '@/shared/ServerMessage/model/ServerMessageModel.ts'; +import getCustomerModel from '@/shared/API/customer/model/CustomerModel.ts'; import getStore from '@/shared/Store/Store.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 { MESSAGE_STATUS, SERVER_MESSAGE, SERVER_MESSAGE_KEYS } from '@/shared/constants/messages.ts'; +import { setCurrentLanguage, setCurrentUser, switchIsUserLoggedIn } from '@/shared/Store/actions.ts'; +import observeStore, { selectIsUserLoggedIn } from '@/shared/Store/observer.ts'; +import { LANGUAGE_CHOICE } from '@/shared/constants/common.ts'; import { PAGE_ID } from '@/shared/constants/pages.ts'; +import showErrorMessage from '@/shared/utils/userMessage.ts'; import HeaderView from '../view/HeaderView.ts'; @@ -31,14 +30,7 @@ class HeaderModel { 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, - ), - ); + this.router.navigateTo(PAGE_ID.LOGIN_PAGE).catch(() => showErrorMessage()); return false; } return true; @@ -73,20 +65,18 @@ class HeaderModel { private async logoutHandler(): Promise { localStorage.clear(); getStore().dispatch(setCurrentUser(null)); + getStore().dispatch(switchIsUserLoggedIn(false)); try { getCustomerModel().logout(); await this.router.navigateTo(PAGE_ID.LOGIN_PAGE); } catch { - serverMessageModel.showServerMessage( - SERVER_MESSAGE[getStore().getState().currentLanguage].BAD_REQUEST, - MESSAGE_STATUS.ERROR, - ); + showErrorMessage(); } return true; } private observeCurrentUser(): boolean { - observeStore(selectCurrentUser, () => { + observeStore(selectIsUserLoggedIn, () => { this.checkCurrentUser(); }); return true; @@ -94,42 +84,36 @@ class HeaderModel { private setCartLinkHandler(): boolean { const logo = this.view.getToCartLink().getHTML(); - logo.addEventListener('click', async (event) => { + logo.addEventListener('click', (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, - ); - } + this.router.navigateTo(PAGE_ID.CART_PAGE).catch(() => showErrorMessage()); }); return true; } private setChangeLanguageCheckboxHandler(): boolean { const switchLanguageCheckbox = this.view.getSwitchLanguageCheckbox().getHTML(); - switchLanguageCheckbox.addEventListener('click', async () => { - const { currentUser } = getStore().getState(); - + switchLanguageCheckbox.addEventListener('click', () => { + const { + currentLanguage, + // TBD Uncomment when user is logged in on page reload + // currentUser + } = getStore().getState(); + const newLanguage = currentLanguage === LANGUAGE_CHOICE.EN ? LANGUAGE_CHOICE.RU : LANGUAGE_CHOICE.EN; try { - if (currentUser) { - const newLanguage = currentUser.locale === LANGUAGE_CHOICE.EN ? LANGUAGE_CHOICE.RU : LANGUAGE_CHOICE.EN; - const newUser = await getCustomerModel().editCustomer( - [CustomerModel.actionSetLocale(newLanguage)], - currentUser, - ); - getStore().dispatch(setCurrentLanguage(newLanguage)); - serverMessageModel.showServerMessage(SERVER_MESSAGE_KEYS.LANGUAGE_CHANGED, MESSAGE_STATUS.SUCCESS); - getStore().dispatch(setCurrentUser(newUser)); - } + // if (currentUser) { + // const newLanguage = currentUser.locale === LANGUAGE_CHOICE.EN ? LANGUAGE_CHOICE.RU : LANGUAGE_CHOICE.EN; + // const newUser = await getCustomerModel().editCustomer( + // [CustomerModel.actionSetLocale(newLanguage)], + // currentUser, + // ); + // getStore().dispatch(setCurrentLanguage(newLanguage)); + // serverMessageModel.showServerMessage(SERVER_MESSAGE_KEYS.LANGUAGE_CHANGED, MESSAGE_STATUS.SUCCESS); + // getStore().dispatch(setCurrentUser(newUser)); + // } + getStore().dispatch(setCurrentLanguage(newLanguage)); } catch { - // TBD Change to showError - serverMessageModel.showServerMessage( - SERVER_MESSAGE[getStore().getState().currentLanguage].BAD_REQUEST, - MESSAGE_STATUS.ERROR, - ); + showErrorMessage(); } }); @@ -143,10 +127,7 @@ class HeaderModel { try { await this.router.navigateTo(PAGE_ID.DEFAULT_PAGE); } catch { - serverMessageModel.showServerMessage( - SERVER_MESSAGE[getStore().getState().currentLanguage].BAD_REQUEST, - MESSAGE_STATUS.ERROR, - ); + showErrorMessage(); } }); return true; @@ -158,10 +139,7 @@ class HeaderModel { try { await this.logoutHandler(); } catch { - serverMessageModel.showServerMessage( - SERVER_MESSAGE[getStore().getState().currentLanguage].BAD_REQUEST, - MESSAGE_STATUS.ERROR, - ); + showErrorMessage(); } logoutButton.setDisabled(); }); @@ -172,13 +150,9 @@ class HeaderModel { const logo = this.view.getToProfileLink().getHTML(); logo.addEventListener('click', (event) => { event.preventDefault(); + // TBD remove unnecessary check (we don't show this logo when user is not logged in) ?? if (this.checkAuthUser()) { - this.router.navigateTo(PAGE_ID.USER_PROFILE_PAGE).catch(() => { - serverMessageModel.showServerMessage( - SERVER_MESSAGE[getStore().getState().currentLanguage].BAD_REQUEST, - MESSAGE_STATUS.ERROR, - ); - }); + this.router.navigateTo(PAGE_ID.USER_PROFILE_PAGE).catch(() => showErrorMessage()); } }); return true; diff --git a/src/widgets/Header/view/HeaderView.ts b/src/widgets/Header/view/HeaderView.ts index db00bb55..40481b32 100644 --- a/src/widgets/Header/view/HeaderView.ts +++ b/src/widgets/Header/view/HeaderView.ts @@ -1,13 +1,13 @@ -import type { LanguageChoiceType } from '@/shared/constants/buttons.ts'; +import type { LanguageChoiceType } from '@/shared/constants/common.ts'; import ButtonModel from '@/shared/Button/model/ButtonModel.ts'; import InputModel from '@/shared/Input/model/InputModel.ts'; import LinkModel from '@/shared/Link/model/LinkModel.ts'; import getStore from '@/shared/Store/Store.ts'; import { switchAppTheme } from '@/shared/Store/actions.ts'; -import observeStore, { selectCurrentPage, selectCurrentUser } from '@/shared/Store/observer.ts'; -import { BUTTON_TEXT, BUTTON_TEXT_KEYS, LANGUAGE_CHOICE } from '@/shared/constants/buttons.ts'; -import { AUTOCOMPLETE_OPTION } from '@/shared/constants/common.ts'; +import observeStore, { selectCurrentPage, selectIsUserLoggedIn } from '@/shared/Store/observer.ts'; +import { BUTTON_TEXT, BUTTON_TEXT_KEYS } from '@/shared/constants/buttons.ts'; +import { AUTOCOMPLETE_OPTION, LANGUAGE_CHOICE } from '@/shared/constants/common.ts'; import { INPUT_TYPE } from '@/shared/constants/forms.ts'; import { PAGE_ID } from '@/shared/constants/pages.ts'; import APP_THEME from '@/shared/constants/styles.ts'; @@ -268,7 +268,7 @@ class HeaderView { this.toProfileLink.getHTML().classList.add(styles.hidden); } - observeStore(selectCurrentUser, () => + observeStore(selectIsUserLoggedIn, () => this.toProfileLink.getHTML().classList.toggle(styles.hidden, getStore().getState().currentUser === null), ); diff --git a/src/widgets/LoginForm/model/LoginFormModel.ts b/src/widgets/LoginForm/model/LoginFormModel.ts index bccccc4b..e4aff4ad 100644 --- a/src/widgets/LoginForm/model/LoginFormModel.ts +++ b/src/widgets/LoginForm/model/LoginFormModel.ts @@ -5,11 +5,13 @@ import getCustomerModel from '@/shared/API/customer/model/CustomerModel.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 { setCurrentLanguage, 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 { MESSAGE_STATUS, SERVER_MESSAGE_KEYS } from '@/shared/constants/messages.ts'; import { LOADER_SIZE } from '@/shared/constants/sizes.ts'; -import { createGreetingMessage } from '@/shared/utils/messageTemplate.ts'; +import isLanguageChoiceType from '@/shared/types/validation/language.ts'; +import { createGreetingMessage } from '@/shared/utils/messageTemplates.ts'; +import showErrorMessage from '@/shared/utils/userMessage.ts'; import LoginFormView from '../view/LoginFormView.ts'; @@ -46,18 +48,10 @@ class LoginFormModel { if (response) { this.loginUserHandler(userLoginData); } else { - serverMessageModel.showServerMessage( - SERVER_MESSAGE[getStore().getState().currentLanguage].INVALID_EMAIL, - MESSAGE_STATUS.ERROR, - ); + serverMessageModel.showServerMessage(SERVER_MESSAGE_KEYS.INVALID_EMAIL, MESSAGE_STATUS.ERROR); } }) - .catch(() => { - serverMessageModel.showServerMessage( - SERVER_MESSAGE[getStore().getState().currentLanguage].BAD_REQUEST, - MESSAGE_STATUS.ERROR, - ); - }) + .catch(() => showErrorMessage()) .finally(() => loader.remove()); } @@ -69,14 +63,19 @@ class LoginFormModel { .then((data) => { if (data) { getStore().dispatch(setCurrentUser(data)); - serverMessageModel.showServerMessage(createGreetingMessage(), MESSAGE_STATUS.SUCCESS); + getStore().dispatch(switchIsUserLoggedIn(true)); + if (isLanguageChoiceType(data.locale)) { + getStore().dispatch(setCurrentLanguage(data.locale)); + } + serverMessageModel.showServerMessage( + SERVER_MESSAGE_KEYS.GREETING, + MESSAGE_STATUS.SUCCESS, + createGreetingMessage(), + ); } }) .catch(() => { - serverMessageModel.showServerMessage( - SERVER_MESSAGE[getStore().getState().currentLanguage].INCORRECT_PASSWORD, - MESSAGE_STATUS.ERROR, - ); + serverMessageModel.showServerMessage(SERVER_MESSAGE_KEYS.INCORRECT_PASSWORD, MESSAGE_STATUS.ERROR); }) .finally(() => loader.remove()); } diff --git a/src/widgets/RegistrationForm/model/RegistrationFormModel.ts b/src/widgets/RegistrationForm/model/RegistrationFormModel.ts index c6c1ba38..fe08105e 100644 --- a/src/widgets/RegistrationForm/model/RegistrationFormModel.ts +++ b/src/widgets/RegistrationForm/model/RegistrationFormModel.ts @@ -9,7 +9,7 @@ import serverMessageModel from '@/shared/ServerMessage/model/ServerMessageModel. import getStore from '@/shared/Store/Store.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 { MESSAGE_STATUS, SERVER_MESSAGE_KEYS } from '@/shared/constants/messages.ts'; import { LOADER_SIZE } from '@/shared/constants/sizes.ts'; import { ADDRESS_TYPE } from '@/shared/types/address.ts'; import formattedText from '@/shared/utils/formattedText.ts'; @@ -95,17 +95,11 @@ class RegisterFormModel { if (newUserData) { getStore().dispatch(setCurrentUser(newUserData)); getStore().dispatch(switchIsUserLoggedIn(true)); - serverMessageModel.showServerMessage( - SERVER_MESSAGE[getStore().getState().currentLanguage].SUCCESSFUL_REGISTRATION, - MESSAGE_STATUS.SUCCESS, - ); + serverMessageModel.showServerMessage(SERVER_MESSAGE_KEYS.SUCCESSFUL_REGISTRATION, MESSAGE_STATUS.SUCCESS); } }) .catch(() => { - serverMessageModel.showServerMessage( - SERVER_MESSAGE[getStore().getState().currentLanguage].USER_EXISTS, - MESSAGE_STATUS.ERROR, - ); + serverMessageModel.showServerMessage(SERVER_MESSAGE_KEYS.USER_EXISTS, MESSAGE_STATUS.ERROR); }) .finally(() => loader.remove()); }