From 1d579400dfc99a780c2bb4ed47a70e50f9d7e5ac Mon Sep 17 00:00:00 2001 From: Meg G <146496794+stardustmeg@users.noreply.github.com> Date: Sat, 25 May 2024 15:33:09 +0300 Subject: [PATCH 01/28] chore(RSS-ECOMM-4_00): update sprint number (#309) chore: update sprint number --- .github/pull_request_template.md | 4 ++-- .validate-branch-namerc.cjs | 2 ++ 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 8e1c0deb..df4c3fbe 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -3,10 +3,10 @@ 📍 conforms with the following format: - [ ] prefix (following the [convention](https://www.conventionalcommits.org/en/v1.0.0-beta.2/): `feat`, `fix`, `hotfix`, `chore`, `refactor`, `revert`, `docs`, `style`, `test`) -- [ ] sprint and issue number (e.g. `RSS-ECOMM-3_01`, where `3` - is the sprint number and `01` - is the issue number) +- [ ] sprint and issue number (e.g. `RSS-ECOMM-4_01`, where `4` - is the sprint number and `01` - is the issue number) - [ ] short description -👀 Example: `feat(RSS-ECOMM-3_01): description` +👀 Example: `feat(RSS-ECOMM-4_01): description` ## PR Description 🧙‍♂️ diff --git a/.validate-branch-namerc.cjs b/.validate-branch-namerc.cjs index 15ea94ee..39e9fe8b 100644 --- a/.validate-branch-namerc.cjs +++ b/.validate-branch-namerc.cjs @@ -7,3 +7,5 @@ module.exports = { // "feat(RSS-ECOMM-1_01)/addNewProduct" // where 1 is the sprint number and 01 is the issue number // "fix(RSS-ECOMM-2_15)/addCorrectProduct" // where 2 is the sprint number and 15 is the issue number +// "chore(RSS-ECOMM-3_01)/addNewProduct" // where 3 is the sprint number and 01 is the issue number +// "refactor(RSS-ECOMM-4_01)/addNewProduct" // where 4 is the sprint number and 01 is the issue number From 275ba40267368d7f4c23405486be4cf26f29e830 Mon Sep 17 00:00:00 2001 From: Meg G <146496794+stardustmeg@users.noreply.github.com> Date: Sat, 25 May 2024 18:52:38 +0300 Subject: [PATCH 02/28] refactor(RSS-ECOMM-4_30): update styles (#311) refactor(RSS-ECOMM-3_58): separate styles in mixins (#310) * docs(RSS-ECOMM-2_37): Update pipeline scripts and CD for a new sprint (#199) * docs: update the PR template * chore: add a job to generate PR titles * chore: remove unnecessary paths from configs * chore: remove unused package * chore: remove a job from CI * refactor(RSS-ECOMM-3_27): remove redundancies (#202) * refactor: remove redundant tag constant * refactor: replace redundant event names * feat(RSS-ECOMM-3_03): add filters (#201) * feat: add size product count request * feat: add filter, edit sort, edit get product request * feat: add search options * refactor(RSS-ECOMM-2_53): registration form (#204) * refactor: separate validators into separate functions * refactor: separate address logic into Address component * refactor: change getIsValid method * fix: replace type for authCustomer method * refactor: separate disabling into ButtonView * fix: pattern for validate * feat: types for Address component * feat: create messageTemplate * refactor: create every inputField separately * refactor: create every inputField separately * fix: header styles * fix(RSS-ECOMM-2_54): visualisation forms (#206) * fix: layout registration form * fix: layout login form * fix: layout address * fix: layout countryChoice * fix: layout login and registration pages * refactor: remove the mandatory field symbol from field signatures in forms * feat: add formatted text function * fix: remove formatting from fields that should not be formatted * feat: formatting of address fields * refactor(RSS-ECOMM-2_55): router component (#207) * refactor: rewrite Router component * fix: navigation links * fix: choosing country * refactor: country lang choice based on input * refactor: visual layout of pages * fix: update store * refactor: remove redundant code * fix: styles * feat: add init method for App component * Apply suggestions from code review --------- * feat(RSS-ECOMM-2_99): edit api client (#209) feat: add save token, add auth and anonym client * feat(RSS-ECOMM-3_29): pages lazy loading (#210) * feat: implement lazy loading for pages * fix: add a catch block * feat(RSS-ECOMM-3_30): implement language switch (#212) * feat: implement switching language on registration checkboxes * feat: switch titles language * feat: implement label text switching * test: observe current language * feat: add language choice to user messages * fix: showPasswordElement position * refactor: update text content in Russian * feat: add a logo for language switch * fix(RSS-ECOMM-3_31): rerouting to main (#213) fix: rerouting to main * feat(RSS-ECOMM-3_25): create pages components (#214) * feat: add base page components * docs: update PR template * fix: link styles * fix: button naming * Apply suggestions from code review --------- * feat(RSS-ECOMM-3_19): implement routing for all pages (#215) feat: implement routing for all pages * feat(RSS-ECOMM-3_21): implement navigation (#217) * feat: implement navigation to catalog and cart pages * fix: footer styles * fix: header styles * feat: implement navigation to other pages * feat: check auth user to profile page * feat: check auth user with init app * feat: visually profile button * feat(RSS-ECOMM-3_34): add catalog component (#218) feat: add catalog component * feat(RSS-ECOMM-3_33): implement burger menu (#221) feat: implement burger menu * feat(RSS-ECOMM-3_01): implement product list (#223) * refactor: api types * feat: implement product list * feat(RSS-ECOMM-3_36): implement theme app (#225) * feat: implement app theme change * feat: save current app theme in store * fix: update store actions * feat(RSS-ECOMM-3_02): display price and discount (#226) * fix: api types for product * feat: display price and discount on product card * fix: rename SizeType * feat(RSS-ECOMM-3_99): add blog page (#228) * feat: add blog page * feat: add blog page, posts, widget * refactor: delete comment and magic numbers * fix: mock data * feat(RSS-ECOMM-3_03): implement filtering product list (#227) * feat: expand loder options * fix: styles product card * feat: implement saving Set into LS Co-authored-by: Meg G. * feat: get and draw product items * feat: create ProductFilters component * feat: implement price filter * feat: implement size filter * feat: filters reset button * fix: burger styles * fix: lost title * feat(RSS-ECOMM-3_98): add all data in one request (#229) * feat: add all data in one request * feat: add refresh token, fix auth, refactor client * fix(RSS-ECOMM-3_40): handle popstate event (#231) fix: handle popstate event * feat(RSS-ECOMM-3_37): implement switching user locale (#233) * feat: add a user notification on switching language * feat: implement switching locale * feat(RSS-ECOMM-3_97): add filter Object, make filer with OR concatenation (#230) * feat: add filter Object, make filer with OR concatenation * refactor: delete comment * feat: add categoryCount and SizeCount to Products response * feat(RSS-ECOMM-3_03): implement meta filters (#235) * fix: router * feat: implement meta filters * Apply suggestions from code review --------- * test(RSS-ECOMM-3_24): cover main codebase (#236) test(RSS-ECOMM-3_24): cover main codebase * refactor(RSS-ECOMM-3_41): logic input components (#237) * feat: create method to add data attributes for input * refactor: remove unused data for creating input fields * refactor: delete unused options for address component * feat(RSS-ECOMM-3_14): display user info (#234) * feat: add user info menu * refactor: replace recurring code with a util function * feat: add common utils * feat: display user info * refactor: remove the language switch * chore: complete merge * refactor: remove sending request until user is logged in on refresh * refactor: move language choice from buttons constants * feat: add modal * fix: remove unused var * fix: modal styles * feat: create separate entities for user info and user addresses * refactor: remove redundant code * refactor: add comments * feat(RSS-ECOMM-3_04): implement product sorts (#238) * chore: integrate plop to create component folder * chore: add templates folder * docs: update readme * chore: update configs * chore: update configs * feat: implement products sorting * Apply suggestions from code review * fix: after merge --------- * feat(RSS-ECOMM-3_96): add cart and shop list (#239) * feat: add cart, refactor root, delete product from store * feat: add and delete cart item * feat: add anonymId to store, merge anonym cart after login user * feat: create create for new customer, getCart * feat: add shoppingList * feat: create shopping list after registration * feat: getCategories function * feat(RSS-ECOMM-3_95): add price range (#240) feat: add price range * feat(RSS-ECOMM-3_42): transfer styles to clamp (#242) * refactor: transfer to clamp * refactor: remove background with unused pictures * refactor: remove commented out code * chore(RSS-ECOMM-3_43): modernise the creation of a component via plopfile (#243) chore: modernise the creation of a component via plopfile * chore(RSS-ECOMM-3_44): update display select list in terminal (#244) chore: update display select list in terminal * fix(RSS-ECOMM-3_45): styles (#245) * fix: sorting wrapper style * fix: error and burger z-indices * chore(RSS-ECOMM-3_45): update readme (#246) * docs: update readme * chore: update vite configs to minify build code * fix: typo * feat(RSS-ECOMM-3_05): implement product search (#247) * feat: create product search component * feat: implement product search * feat(RSS-ECOMM-3_94): update user addresses; save token to store (#248) * feat: move save token from cook to store * feat: add shipping billing address to user * feat(RSS-ECOMM-3_46): implement custom scroll (#249) feat: implement custom scroll * fix(RSS-ECOMM-3_47): display products with selected filters (#250) fix: display products with selected filters * feat(RSS-ECOMM-3_94): add id filter for products (#251) feat: add id filter for products * feat(RSS-ECOMM-3_48): implement buttons to product card (#252) * feat: implement buttons to product card * feat: getters * feat(RSS-ECOMM-3_48): translate filters, sorting and search (#254) feat: translate filters, sorting and search * feat(RSS-ECOMM-3_49): add product to cart and wishlist (#255) feat: add product to cart and wishlist * fix(RSS-ECOMM-3_50): incoming product card parameters (#256) * fix: incoming product card parameters * fix: catalog styles * feat(RSS-ECOMM-3_93): add getCurrentUser (#257) feat: add getCurrentUser * fix(RSS-ECOMM-3_51): router component (#258) * fix: routing to detailed product page * fix: redirect to default page * feat(RSS-ECOMM-3_92): add parent to category (#260) * refactor(RSS-ECOMM-3_52): router component (#261) * refactor: get rid of asynchronous methods in the router * refactor: add / at the end of each route * feat: add search queries to url * fix: router test * fix: update netlify * fix: update netlify * fix: update netlify * fix: update netlify * fix: router * fix: router * feat(RSS-ECOMM-3_91): update auth client after password change (#263) feat: update auth client after password change * feat(RSS-ECOMM-3_91): add slug for category and product (#262) feat: add slug for category and product * feat(RSS-ECOMM-3_15): update user info (#253) * refactor: separate PersonalInfoComponent * refactor: separate credentials into a single component * refactor: remove user from store * feat: add a request before showing personal info * feat: implement changing password * fix: switch language for anonymous user * refactor: create one instance of modal content * fix: navigation styles * fix: redirect to main on logo * feat(RSS-ECOMM-3_08): implement breadcrumb navigation (#289) * feat: implement event mediator component * feat: implement search params in url * refactor: remove user from state * feat: implement breadcrumbs * Update src/shared/constants/sorting.ts * fix: prettier --------- * feat(RSS-ECOMM-3_09): display product details on product page (#294) feat: display product details on product page * feat(RSS_ECOMM-3_90): add key request (#295) feat: add key request * feat(RSS-ECOMM-3_89): add adapt Product (#296) feat: add adapt Product * feat(RSS-ECOMM-3_10): implement slider on product page (#297) feat: implement slider on product page * fix(RSS-ECOMM-3_53): routing on product page (#298) * fix: routing on product page * fix: search params * feat(RSS-ECOMM-3_13): implement product modal slider (#299) feat: implement product modal slider * feat(RSS-ECOMM-3_90) cart (#300) * feat: add cart page * feat: badge for cart * feat: add actions for cart products * feat: merge cart after registration * feat(RSS-ECOMM-3_16): implement personal info edit (#301) * feat: close modal on click on background * feat: implement personal info edit * feat: implement changing component styles * refactor(RSS-ECOMM-3_54): router component (#303) * refactor: make Router singleton * refactor: remove redundant router field * feat: separate product price in component * refactor: styles * Apply suggestions from code review * fix: prettier --------- * feat(RSS-ECOMM-4_08): implement cart button to product page (#304) * feat: add modal window closing on click outside the window * feat: add a modal window with product details * feat: add loader in product modal slider * feat: implement cart button to product page * Apply suggestions from code review --------- * fix(RSS-ECOMM-3_55): deploy (#305) fix: deploy * feat(RSS-ECOMM-3_56): add progress bar to server message (#307) feat: add progress bar to server message * refactor: separate style buttons in mixin * fix: delete navigation from footer --------- Co-authored-by: Max <133934232+Kleostro@users.noreply.github.com> Co-authored-by: Yuliya Kursevich <54816946+YulikK@users.noreply.github.com> --- src/app/styles/mixins.scss | 33 +++++++++++++ .../view/productCardView.module.scss | 13 ++++- .../view/productModalSliderView.module.scss | 25 +++++++--- .../view/PersonalInfoEditView.ts | 1 + .../view/personalInfoEditView.scss | 26 ++++++++++ .../view/productFiltersView.module.scss | 21 ++------- src/shared/Modal/view/modalView.module.scss | 1 - src/shared/constants/messages.ts | 3 ++ src/widgets/Footer/model/FooterModel.ts | 8 ---- .../Header/view/headerView.module.scss | 28 ++--------- .../LoginForm/view/loginForm.module.scss | 30 ++---------- .../ProductInfo/view/ProductInfoView.ts | 14 ++++-- .../ProductInfo/view/productInfoView.scss | 33 +++++-------- .../view/RegistrationFormView.ts | 1 + .../view/registrationForm.module.scss | 47 ++++++------------- 15 files changed, 142 insertions(+), 142 deletions(-) create mode 100644 src/app/styles/mixins.scss create mode 100644 src/features/PersonalInfoEdit/view/personalInfoEditView.scss diff --git a/src/app/styles/mixins.scss b/src/app/styles/mixins.scss new file mode 100644 index 00000000..475fee90 --- /dev/null +++ b/src/app/styles/mixins.scss @@ -0,0 +1,33 @@ +@mixin green-btn { + display: flex; + align-items: center; + justify-content: center; + justify-self: center; + border-radius: var(--medium-br); + padding: calc(var(--small-offset) / 3) var(--small-offset); + width: max-content; + font: var(--regular-font); + letter-spacing: var(--one); + color: var(--white); + background-color: var(--steam-green-800); + transition: + color 0.2s, + background-color 0.2s, + transform 0.2s; + + @media (hover: hover) { + &:hover { + background-color: var(--steam-green-700); + } + } + + /* stylelint-disable-next-line order/order */ + &:active { + transform: scale(0.9); + } + + &:disabled { + background-color: var(--noble-gray-300); + pointer-events: none; + } +} diff --git a/src/entities/ProductCard/view/productCardView.module.scss b/src/entities/ProductCard/view/productCardView.module.scss index 63c44d99..27dd52ac 100644 --- a/src/entities/ProductCard/view/productCardView.module.scss +++ b/src/entities/ProductCard/view/productCardView.module.scss @@ -20,6 +20,11 @@ transform: scale(0.98); } } + + /* stylelint-disable-next-line order/order */ + &:active { + transform: scale(0.95); + } } .productImageWrapper { @@ -72,7 +77,9 @@ width: var(--small-offset); height: var(--small-offset); background-color: var(--noble-gray-1000); - transition: outline 0.2s; + transition: + transform 0.2s, + outline 0.2s; backdrop-filter: blur(10px); svg { @@ -81,6 +88,10 @@ fill: var(--noble-gray-800); transition: fill 0.2s; } + + &:active { + transform: scale(0.9); + } } .addToCartButton, diff --git a/src/entities/ProductModalSlider/view/productModalSliderView.module.scss b/src/entities/ProductModalSlider/view/productModalSliderView.module.scss index a60743df..5dcfc78e 100644 --- a/src/entities/ProductModalSlider/view/productModalSliderView.module.scss +++ b/src/entities/ProductModalSlider/view/productModalSliderView.module.scss @@ -23,25 +23,32 @@ .modalCloseButton { position: absolute; - right: 3rem; - top: 2rem; + right: 1rem; + top: 0.3rem; z-index: 1; font: var(--extra-bold-font); font-weight: 400; color: var(--steam-green-800); - transition: color 0.2s; + transition: + color 0.2s, + transform 0.2s; @media (hover: hover) { &:hover { color: var(--steam-green-700); } } + + /* stylelint-disable-next-line order/order */ + &:active { + transform: scale(0.9); + } } .navigationWrapper { position: absolute; - left: -1.5rem; - right: -1.5rem; + left: -3.5rem; + right: -3.5rem; top: 50%; z-index: 1; display: flex; @@ -62,7 +69,8 @@ background-color: var(--noble-gray-1000); transition: color 0.2s, - border-color 0.2s; + border-color 0.2s, + transform 0.2s; @media (hover: hover) { &:hover { @@ -70,6 +78,11 @@ color: var(--steam-green-700); } } + + /* stylelint-disable-next-line order/order */ + &:active { + transform: scale(0.9); + } } .nextSlideButton { diff --git a/src/features/PersonalInfoEdit/view/PersonalInfoEditView.ts b/src/features/PersonalInfoEdit/view/PersonalInfoEditView.ts index 510b9c62..f6b25789 100644 --- a/src/features/PersonalInfoEdit/view/PersonalInfoEditView.ts +++ b/src/features/PersonalInfoEdit/view/PersonalInfoEditView.ts @@ -8,6 +8,7 @@ import { EMAIL_VALIDATE } from '@/shared/constants/forms/validationParams.ts'; import createBaseElement from '@/shared/utils/createBaseElement.ts'; import styles from './personalInfoEditView.module.scss'; +import './personalInfoEditView.scss'; class PersonalInfoEditView { private cancelButton: ButtonModel; diff --git a/src/features/PersonalInfoEdit/view/personalInfoEditView.scss b/src/features/PersonalInfoEdit/view/personalInfoEditView.scss new file mode 100644 index 00000000..a5654174 --- /dev/null +++ b/src/features/PersonalInfoEdit/view/personalInfoEditView.scss @@ -0,0 +1,26 @@ +.modalForm { + .personalDataWrapper { + grid-row: 1; + padding: 0; + width: 100%; + + @media (max-width: 768px) { + grid-template-columns: repeat(1, 1fr); + grid-template-rows: repeat(2, auto); + } + } + + .label:last-child { + grid-column: 2 span; + width: 100%; + + @media (max-width: 768px) { + grid-column: 1; + grid-row: 4; + } + } + + .input { + padding-right: 0; + } +} diff --git a/src/features/ProductFilters/view/productFiltersView.module.scss b/src/features/ProductFilters/view/productFiltersView.module.scss index 338ea2d4..912efb87 100644 --- a/src/features/ProductFilters/view/productFiltersView.module.scss +++ b/src/features/ProductFilters/view/productFiltersView.module.scss @@ -1,3 +1,5 @@ +@import 'src/app/styles/mixins'; + .defaultFilters { outline: var(--two) solid var(--noble-white-200); border-radius: var(--medium-br); @@ -190,25 +192,10 @@ } .resetFiltersButton { - display: flex; + @include green-btn; + margin: calc(var(--extra-small-offset) / 2) auto; border-radius: var(--medium-br); padding: calc(var(--small-offset) / 3) var(--small-offset); font: var(--regular-font); - letter-spacing: var(--one); - 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); - } - } } diff --git a/src/shared/Modal/view/modalView.module.scss b/src/shared/Modal/view/modalView.module.scss index 65cd53f3..e60982dd 100644 --- a/src/shared/Modal/view/modalView.module.scss +++ b/src/shared/Modal/view/modalView.module.scss @@ -43,7 +43,6 @@ top: 50%; border: var(--two) solid var(--noble-black-500); border-radius: var(--border-radius); - padding: var(--small-offset); font: var(--small-font); letter-spacing: var(--one); color: var(--steam-green-500); diff --git a/src/shared/constants/messages.ts b/src/shared/constants/messages.ts index ca58306c..8f30f5e5 100644 --- a/src/shared/constants/messages.ts +++ b/src/shared/constants/messages.ts @@ -27,6 +27,7 @@ export const SERVER_MESSAGE = { PERSONAL_INFO_CHANGED: 'Personal information has been changed successfully', SUCCESSFUL_ADD_PRODUCT_TO_CART: 'Product has been added successfully to your cart', SUCCESSFUL_ADD_PRODUCT_TO_WISHLIST: 'Product has been added successfully to your wishlist', + SUCCESSFUL_COPY_TO_CLIPBOARD: 'SKU has been copied to clipboard', SUCCESSFUL_DELETE_PRODUCT_FROM_CART: 'Product has been deleted successfully from your cart', SUCCESSFUL_DELETE_PRODUCT_FROM_WISHLIST: 'Product has been deleted successfully from your wishlist', SUCCESSFUL_LOGIN: 'Welcome to our store. Enjoy shopping!', @@ -47,6 +48,7 @@ export const SERVER_MESSAGE = { PERSONAL_INFO_CHANGED: 'Персональные данные были успешно изменены', SUCCESSFUL_ADD_PRODUCT_TO_CART: 'Товар был успешно добавлен в корзину', SUCCESSFUL_ADD_PRODUCT_TO_WISHLIST: 'Товар был успешно добавлен в избранное', + SUCCESSFUL_COPY_TO_CLIPBOARD: 'SKU успешно скопирован в буфер обмена', SUCCESSFUL_DELETE_PRODUCT_FROM_CART: 'Товар был успешно удален из корзины', SUCCESSFUL_DELETE_PRODUCT_FROM_WISHLIST: 'Товар был успешно удален из избранного', SUCCESSFUL_LOGIN: 'Добро пожаловать в наш магазин. Приятных покупок!', @@ -69,6 +71,7 @@ export const SERVER_MESSAGE_KEYS = { PERSONAL_INFO_CHANGED: 'PERSONAL_INFO_CHANGED', SUCCESSFUL_ADD_PRODUCT_TO_CART: 'SUCCESSFUL_ADD_PRODUCT_TO_CART', SUCCESSFUL_ADD_PRODUCT_TO_WISHLIST: 'SUCCESSFUL_ADD_PRODUCT_TO_WISHLIST', + SUCCESSFUL_COPY_TO_CLIPBOARD: 'SUCCESSFUL_COPY_TO_CLIPBOARD', SUCCESSFUL_DELETE_PRODUCT_FROM_CART: 'SUCCESSFUL_DELETE_PRODUCT_FROM_CART', SUCCESSFUL_DELETE_PRODUCT_FROM_WISHLIST: 'SUCCESSFUL_DELETE_PRODUCT_FROM_WISHLIST', SUCCESSFUL_LOGIN: 'SUCCESSFUL_LOGIN', diff --git a/src/widgets/Footer/model/FooterModel.ts b/src/widgets/Footer/model/FooterModel.ts index dfa9cf8b..bcccbc71 100644 --- a/src/widgets/Footer/model/FooterModel.ts +++ b/src/widgets/Footer/model/FooterModel.ts @@ -3,14 +3,6 @@ import FooterView from '../view/FooterView.ts'; class FooterModel { private view = new FooterView(); - constructor() { - this.init(); - } - - private init(): boolean { - return true; - } - public getHTML(): HTMLElement { return this.view.getHTML(); } diff --git a/src/widgets/Header/view/headerView.module.scss b/src/widgets/Header/view/headerView.module.scss index f1ed92d4..05e9aa44 100644 --- a/src/widgets/Header/view/headerView.module.scss +++ b/src/widgets/Header/view/headerView.module.scss @@ -1,3 +1,5 @@ +@import 'src/app/styles/mixins'; + .header { position: sticky; left: 0; @@ -62,31 +64,9 @@ } .logoutButton { - order: 4; - border-radius: var(--medium-br); - padding: calc(var(--small-offset) / 3) var(--small-offset); - font: var(--regular-font); - letter-spacing: var(--one); - 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); - } - } + @include green-btn; - &:disabled { - background-color: var(--noble-gray-300); - pointer-events: none; - } + order: 4; } .cartLink, diff --git a/src/widgets/LoginForm/view/loginForm.module.scss b/src/widgets/LoginForm/view/loginForm.module.scss index 9a36618e..07e68979 100644 --- a/src/widgets/LoginForm/view/loginForm.module.scss +++ b/src/widgets/LoginForm/view/loginForm.module.scss @@ -1,35 +1,13 @@ +@import 'src/app/styles/mixins'; + .submitFormButton { - display: flex; + @include green-btn; + place-self: center center; - align-items: center; - justify-content: center; grid-column: 2 span; grid-row: 2; - border-radius: var(--small-br); padding: calc(var(--extra-small-offset) / 2) var(--small-offset); - width: max-content; font: var(--bold-font); - letter-spacing: var(--one); - 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; - } } .loginForm { diff --git a/src/widgets/ProductInfo/view/ProductInfoView.ts b/src/widgets/ProductInfo/view/ProductInfoView.ts index 72966ca2..7c3a6cc4 100644 --- a/src/widgets/ProductInfo/view/ProductInfoView.ts +++ b/src/widgets/ProductInfo/view/ProductInfoView.ts @@ -236,7 +236,7 @@ class ProductInfoView { const currentSKU = new InputModel({ autocomplete: AUTOCOMPLETE_OPTION.ON, - id: 'currentSKU', + id: '', placeholder: '', type: INPUT_TYPE.TEXT, value: this.params.key, @@ -248,9 +248,15 @@ class ProductInfoView { svg.append(createSVGUse(SVG_DETAILS.COPY)); svg.addEventListener('click', () => { - currentSKU.getHTML().select(); - document.execCommand('copy'); - serverMessageModel.showServerMessage(SERVER_MESSAGE_KEYS.COPY_TO_CLIPBOARD, MESSAGE_STATUS.SUCCESS); + window.navigator.clipboard + .writeText(currentSKU.getValue()) + .then(() => + serverMessageModel.showServerMessage( + SERVER_MESSAGE_KEYS.SUCCESSFUL_COPY_TO_CLIPBOARD, + MESSAGE_STATUS.SUCCESS, + ), + ) + .catch(() => serverMessageModel.showServerMessage(SERVER_MESSAGE_KEYS.BAD_REQUEST, MESSAGE_STATUS.ERROR)); }); this.SKUSpan.append(currentSKU.getHTML(), svg); diff --git a/src/widgets/ProductInfo/view/productInfoView.scss b/src/widgets/ProductInfo/view/productInfoView.scss index 7169db59..6564408d 100644 --- a/src/widgets/ProductInfo/view/productInfoView.scss +++ b/src/widgets/ProductInfo/view/productInfoView.scss @@ -1,3 +1,5 @@ +@import 'src/app/styles/mixins'; + .wrapper { display: flex; align-items: center; @@ -178,28 +180,9 @@ } .switchToCartButton { - display: flex; - align-items: center; - border-radius: var(--medium-br); - padding: calc(var(--small-offset) / 3) calc(var(--small-offset) / 2); - max-width: max-content; - font: var(--regular-font); - letter-spacing: var(--one); - color: var(--white); - background-color: var(--steam-green-800); - transition: - color 0.2s, - background-color 0.2s; - - &:focus { - background-color: var(--steam-green-700); - } + @include green-btn; - @media (hover: hover) { - &:hover { - background-color: var(--steam-green-700); - } - } + padding: calc(var(--small-offset) / 3) calc(var(--small-offset) / 2); } .buttonsWrapper { @@ -217,7 +200,9 @@ width: var(--small-offset); height: var(--small-offset); background-color: var(--noble-gray-1000); - transition: outline 0.2s; + transition: + transform 0.2s, + outline 0.2s; backdrop-filter: blur(10px); svg { @@ -227,6 +212,10 @@ transition: fill 0.2s; } + &:active { + transform: scale(0.9); + } + @media (hover: hover) { &:hover { outline: var(--two) solid var(--red-power-600); diff --git a/src/widgets/RegistrationForm/view/RegistrationFormView.ts b/src/widgets/RegistrationForm/view/RegistrationFormView.ts index 6f96c5a0..cf0754e7 100644 --- a/src/widgets/RegistrationForm/view/RegistrationFormView.ts +++ b/src/widgets/RegistrationForm/view/RegistrationFormView.ts @@ -31,6 +31,7 @@ class RegistrationFormView { attrs: { type: BUTTON_TYPE.SUBMIT, }, + classes: [styles.submitFormButton], text: BUTTON_TEXT[getStore().getState().currentLanguage].REGISTRATION, }); observeCurrentLanguage(this.submitFormButton.getHTML(), BUTTON_TEXT, BUTTON_TEXT_KEYS.REGISTRATION); diff --git a/src/widgets/RegistrationForm/view/registrationForm.module.scss b/src/widgets/RegistrationForm/view/registrationForm.module.scss index 24358a93..90ac5e1b 100644 --- a/src/widgets/RegistrationForm/view/registrationForm.module.scss +++ b/src/widgets/RegistrationForm/view/registrationForm.module.scss @@ -1,3 +1,5 @@ +@import 'src/app/styles/mixins'; + .registrationForm { display: grid; grid-column: 2 span; @@ -7,42 +9,21 @@ width: 100%; gap: var(--extra-small-offset); - button { - display: flex; - align-items: center; - justify-content: center; - justify-self: center; - grid-column: 2 span; - grid-row: 5; - border-radius: var(--small-br); - padding: calc(var(--extra-small-offset) / 2) var(--small-offset); - width: max-content; - max-height: var(--small-offset); // 40px - font: var(--bold-font); - letter-spacing: var(--one); - color: var(--white); - background-color: var(--steam-green-800); - transition: - color 0.2s, - background-color 0.2s; - - @media (max-width: 768px) { - grid-column: 1; - } + @media (max-width: 768px) { + grid-template-columns: repeat(1, 1fr); + } +} - @media (hover: hover) { - &:hover { - background-color: var(--steam-green-700); - } - } +.submitFormButton { + @include green-btn; - &:disabled { - background-color: var(--noble-gray-300); - pointer-events: none; - } - } + grid-column: 2 span; + grid-row: 5; + padding: calc(var(--extra-small-offset) / 2) var(--small-offset); + max-height: var(--small-offset); + font: var(--bold-font); @media (max-width: 768px) { - grid-template-columns: repeat(1, 1fr); + grid-column: 1; } } From b8e23ec70975f112104f6cf3abeefb5a5520c4a4 Mon Sep 17 00:00:00 2001 From: Yuliya Kursevich <54816946+YulikK@users.noreply.github.com> Date: Sun, 26 May 2024 14:45:23 +0200 Subject: [PATCH 03/28] feat(RSS-ECOMM-4_99): add navigate and languages to footer (#312) * feat: add navigate and languages * feat: translate for social and pay * feat: styles --- public/img/png/instagram.png | Bin 1309 -> 899 bytes public/img/png/instagram_.png | Bin 0 -> 1153 bytes public/img/png/linkedin.png | Bin 416 -> 388 bytes public/img/png/meta.png | Bin 2091 -> 1354 bytes public/img/png/pay-ae.png | Bin 0 -> 765 bytes public/img/png/pay-mc.png | Bin 0 -> 747 bytes public/img/png/pay-pp.png | Bin 0 -> 944 bytes public/img/png/pay-visa.png | Bin 0 -> 989 bytes public/img/png/twitterx.png | Bin 1296 -> 842 bytes public/img/png/union.png | Bin 439 -> 764 bytes src/shared/API/product/model/ProductModel.ts | 2 +- src/shared/types/common.ts | 4 + src/widgets/Footer/model/FooterModel.ts | 112 +++++++ src/widgets/Footer/view/FooterView.ts | 315 +++++++++++++++--- .../Footer/view/footerView.module.scss | 82 ++++- 15 files changed, 462 insertions(+), 53 deletions(-) create mode 100644 public/img/png/instagram_.png create mode 100644 public/img/png/pay-ae.png create mode 100644 public/img/png/pay-mc.png create mode 100644 public/img/png/pay-pp.png create mode 100644 public/img/png/pay-visa.png create mode 100644 src/shared/types/common.ts diff --git a/public/img/png/instagram.png b/public/img/png/instagram.png index 0cfa8b9cee470620b82d58fb85cba422287eab75..94d29c5f112abcbcb6276ad7b0204ec8524287ed 100644 GIT binary patch literal 899 zcmV-}1AP36P)ZQdh#x@fOysb)r*Oyblwz`S`P&L`!sKtUA9GFXUpzF>`UIIoq6+pGvBs#OcBl}|&m=-sfc4ryFD${@r~4Jd#5pT-fmU~xt2?NwHsY`uNx3> zXR{(;H!Fj|xMSbNg+1G9coCZbS8^p0AO#x?;Vo-aY#S$vcKzuo&vBwiztMlxCLw`k zgB1Wwpwo8`>f+)-u}5+0lA~mG2bf$U5(J(p`C-ra3NStalk0(kEl0^mmZ5QUC#i4z z36h%z!Q;<-H+Z}L@SLOM6OQsTNBPTh4K=1=hW^_{0^^T$=mbbM(SEF18Q}=+efqRn zLyUBcrQK>i1QcvUHSdAfF(jd3D@uZWb1!q7{E2MgzW(giNbgQI`*@3Z1^9b{%@tMhk zCh?P!Hludu=x#HHQ$;%L$jd+As+TG$+r-?keD_KlJSnNrnq-g^y^QX2lNA`s8!+$k z5{hfF32djg)x1IT9ceBbm))tvePm{T-MwigF-O6xyuov2P|*74w}BkX>l|Uer}Fx- z|G*J7$^snWWBQVjD`h2bSt(h8s>D=irOZI5W6vM?6d=VSt&gsG73g$6^7XB6eY1|F Z{RItLO#i(@C#(Pf002ovPDHLkV1hwqqIUoQ literal 1309 zcmV+&1>*XNP)*s@(=KfL<|3vmJfK$PVDf7|R;z$sbj$Lu{#og+700tEkLX=h0b(^0P zDZv1tZ$ijiTO}{S2pJgzQQIzc<~ETMa{0yQ=gr-}exa+V!8Ph$cwRc`Ust>g1B96D z^22N23tQas=d}zhA?IHK#(noP=}imi#?>7WTs|u>N^l+zc-a@E&b~k%f4cq;DqIdd z(b`4|k7tH{UDYzq;lNjh5fZV zTQetjM%9nX<5afmLh>qO!cUuclF|R+u*HwBZc;_FgU2#O1t*O)BGFoDe!NP`XIEDN zVV16OQN?@N=WHCO|FNnfIu>{d;R3QE?1_DDm}a@%7c)M>S0t3C=>E4=9}D0EPEk(9 zXgCY+@nN}s8sn6vDqvLs?7s^pZ1X`Qk(^dFJC*5k>-jR4diy*QJB%ibTJhr-dzeuD zcyxRe*BzPW8cbVOrzr1VnG{5AAEo(4*rc$Q>>lusIIl#HMK{I<`}g!Iuz z`Kai715C>YER@0Pt0L(A1z8Q7!&f5Ica zC!64iA7M(oM`2XK4;sSMXl?Wm=@Le3qlY{t9#IV&iFKHIcsRW!wp>XlbS+X6nBr%s z7K>&>HGHW2s_b&tSw4S{aR2ikOKq<#{DOC*yq$Z)hNf6B>ihh@Kq4 z-_RwRTngs1)G5|(ommvYg`g`W<{n;>m)eaPUY3^!{xM` zi|Yc+r*Sh@v?4YpFw50a6S^&BV#{FaCuJyN7hwY1#hrEo4f&NM0aSL$W*t?;rdSi$ zDc0Sleu39@MX)B=?|qUJo{}S0k(b?9#QHX-SdtT%n%fq+&RN73&rj)#=XRhBTUQ$Y zwIay0ZB6;^GR{E#bJVq67`LOYmJhDgX7+XJsQWc< z^8qstrcqCcS#wT71`t96-{arF%W`%}*L*0s9pr+f9n|vpPm{Gu3&$dk4 z!MSB>KDJo}FIh;BfoVDId@98|J9Dj|VZ*6?-zCua9@AVac=d_1Zi52;#HP5w3#N{f zlHmnY$CJLWr(ni;AYME+3ztw{02fI~@ahw1sY;lGsHVOYHG$Qn*Bj=NB}e{G3x@M+ zIo-Sr6>l@oZ!Dw5WAsZ)Dc10Q!VCBiHVle5<|aT{BYJYc)Cs%;vDwMQgo2sO*VBLE zb~tvLD*4)hdKCL8QSC^=!Q74K1~l^){U~bDNMjUyF`fJn#^^$uI?+cTq$~Ih6mu1x T4+=+W00000NkvXXu0mjf$RZ^> literal 0 HcmV?d00001 diff --git a/public/img/png/linkedin.png b/public/img/png/linkedin.png index 417c3c4d4d7116d5881f6bc5da80604417462d51..0c3cec3ee9c56319c005a7a0edce57fc8f590f84 100644 GIT binary patch delta 376 zcmV-;0f+ve1B3%1iBL{Q4GJ0x0000DNk~Le0000W0000W2nGNE0CReJ^pPPKe*rd0 zL_t(|ob8l9OM^iS#@|Ij92CLtprS4=ZiSM2brC^&$+ZYBwc;e`ROsyHQbZ6f(&So7 zJBZePKPl4k{yCSzy-V+bEV+dH@mz9I6cqH2N*`C7%9<8f8Ua%~?p1f9BBiWpDWSY$k(4}2q9Q<95;}c8gQPEO-xnAy@ z!_$u5j+Rb}@k}u`X8}TYiL++v%+?NIs5iixYY==_OkdNj0ij3eR!a*3fW?Q@5Hb$t z0GaJjVk%&%lh4n3>DDU%I94_njQbuPpyY@3<8&eMgq{FQMGy7r(Q9J`PApwG0G>Sp z;7hiDn#0rTr|}pc(h|bxF6qp_r81Y z9*4HK-$I6)0PIyy-Y9F-|IhBpKZl zOOTF~1|qKuTb5`2ZQpHYoUh`~#dZ7EZ7w9qxhhSOa+kpnO9Zr4Wx=EZgfA0%3QTaR01E|p5XuUWIDZ8ENkl*M*Vg^Q#Yp^VOkeh4yhc(P1 z7|tZ3q(^siEtg3VN}do^v7>oSp>phTX5@^;_zgB!FMj~d8^kpHQo16#oB2dl>_A&I zpIk;CQ-yF1-;yy}ksqZO2Kk*OB!2Mz^Ez-aBQMoAC7cvhaepgLW)ow=iIyS;w0M;c z5jh<@k^*#$Yx%u}-Q2py6xkCCuOo zmSuwHCV%FD=ZL|w=-~=hh*SBBT;NB?A7dJBCzCt;#MOBJJnSJ8+k{iDTnJymDI#2N zqt9M5n1Q3zWK3kQ4aDsX5fViW2VA*IS420t$q^hSgw&I8Q>jz3byBqgCbmj)G6VDN zTx^`^FybPz5&P$cc4Ey>OspJx*VSO(jOYxM{C@!dF6bk>sSslZj=S;}H5^C^k^gM! zn6Wn^)ZRj;Tn%y^j>lo3L~qO;0gwe`UOeN%+gBBx)cqDzM)qzrK1N)Ams~~Q#n&*V z;AbqKSY`tq&Jr7S(Y34PI{YyqM9I;{1k43O$O&g5$^I;e9We5s<)ZXm_yZlDY*~hx zhJW7@N{#GyF+znWUS^HNCv0#93#=+0ao8w7DFl9POu%o5WSJB%XElHc$F)?+@+ca` zBB+SBsZ+6DYy|K&Cn9HQeF9OVU2bz`M4et?a{#+NL;4G1>r>%}C_qLod)+Njz#ktsMsZ1|V!6Czop2!lm!mX@E<4#+u|m&ba^hO2ZHgBpH( z`4PMn7b7*+RWvY66^gY>HwIQHrr_`7sla@3A$$#qu&2R?2TMrYD3;-ctRmLMh+bT4 z3p1WF(C0*TwN{ffGW@`4@gSE5Tz_lX={soyKe))F#uIQNQ?Ro3+_M#661$3F3g)ek z-gsd(Jy6?;3|!yTLT_1&+?V+&Rl&8=BJ9Z!xAJmII9-I<401^Yw6(H@Swt0t06Tw0z6E%kZP{O|*qJ(e`S{pGTjvJR{}EksKpOjvSsN Z{s)DKBwc?VMxX!y002ovPDHLkV1n*Vfmi?l delta 2058 zcmV+l2=({M3ab#1IDZG-Nkl!WxNtm7Po=q4Y*^q@_{nNC*zI}T7-rb$K zE1~#hPSsTH_U%6B^yzcH^L1z0L?$wkiA-c7UxG+;=+a$-Vt>np(g$7A9J)07qGk3} zah2>-AuatwIQl?Z`V>Fm$V#VQ5dF^X)_+=?o9e^d8B6#3wa_ut_W^9cR;%B+@`7yu zQIb|YEe)MxW6_|a4{KwmgjKu7>BJmq>AiFb4vI9^fKh+* zztWNS>IICo&;eb-sxEZH#;Vh{^n2FPsAii`>#DZKEE^ zvTRe$L4Pxxr%Y|bo3I9J*U~91y_D7>d=Zx348b`vauYLF{ebf^Ex=ZH|K-BcJ0V@p z%sARVy;xj3<>s!FPXAUqS_$i(O;0d4#ZAJ}S$u2vXn&e6(y2cLv|51<=$c+EuFZ{l z2sT21-VuXhE3bo3T(|Ib?Opsg+Z3Jc(SdYaT7UI=sF#1|gJRpNQv}Kc=tOE@4Sy4s z+QF&Qd;4=&;2Y+|0`3<-tn1596x(1ctov(4Uyly(Ce!icH0XZL!Mlz>`PJy8)sKeM z=zB-j5gX38Ux78I8t!w4XcmtUdT`9-mp%w{tBCtM@h4Cj-C;r+<<1 zGCqCunmAcFnhQ1X>o|F#*usS+O>uow!+)(o@pWP7<3KU~nf4$L3?*Q^(p2BnBHhUK z)ao9-gvaxcuJAqCwfa{b38bYw!0;Q{2Bd9L3gV$sSLp}6-!xTO;6u=t$FZKiI|To2 zKY#+}v`X1)S(w$u;{>TB9i8*M%oZK42XRtw=Q)6GM!=?wL583r&8kJOK0>s1Bd97S$pss8_j2+6#fVCtEZADj#eM{Hv63io9GpTVrxkt)pD9%fabtJ zc4pGh6r-f14}YqRvegeqJwu|16nnGcm{YGZf!7pa6AxUKd1v$j^$WR`V;>1-jtJSA+DF1M>Ke57^E|ZNwh@RtUza zXn(%7IW*sM8AD{c2m}w~;}A<2^+W)L6SJSqUOc4xB}dAgn&P?mJ4COa(sQ34;Y6jI z+p(^#L9vy|XU$j7u5r98IDeJOkz3^*0oPhTLU;keT6I26LGHM?Or`e|{LL3E%&9t~ zLyhS(Y0pW8`yG6XgFc1TdF%n~)^qaieqj6o2Zu60~YB}Xum4=D? zI)ayM-roQNiV4v7HEw|Z?i0S(YXmT!adX1N;YR5B63LWUn_h=IxqqeSIOc7^>!3n zFt(67;@_Ww(oqP6ZyY>f=vNi}OM1IBw8ZEn+}xc$K<3cZ!UVo$RAdAAyDWBnWXOFi z{h22KiaF){cRWb+)PD>UKEN}MH9FAiv_h1Z(|$CX(W>QeZ`o7%=Fnfl<465%z>uvx zF>x~o)$vcEWx=0Ki@R;~>{`n3#96ts;AR+*0~elgo2Z{U*4A%|t?uDV+C3tFX9wvl z?;bCv?EuAJUeIU-dnhAn>A4Wmr&EQ~i)so@%onz!w?(hmy??41hE%ApZ6chG(9z^1 zQ^DOSsnhg^SRVCEzjJTa^d*w-7Dmr+_@?K>X5Jrlhp*T+bYFSfRLk(axteT;XxffuQ^E?`gwA*dksww`3L0i zh-LL&CYga9KYumu{kDlf2Xx8dtNu}MYe{d1ZTR3Duu-oK3tv4kQmHh0nh|TgA0T+G z_o&Y+vV0%&@V@>}pMj5!6r#&}RPmHT8DH#%PI$O;YEQjcT}ZhLMAE4}Xa}$v7(@@g oz^9afOk^SxnaD)4FI)ZxTLsDINqYIe00000NkvXXt^-0~g1Ueq+5i9m diff --git a/public/img/png/pay-ae.png b/public/img/png/pay-ae.png new file mode 100644 index 0000000000000000000000000000000000000000..1910764d656fae5d775c82db25570b1a4dd12813 GIT binary patch literal 765 zcmVLwYZmcbMSE7gmJwtHIQ4Xyj%joJS$p7fwDDu~i7N(0 z0ag=bWQsC6u6Sw;ke7e-rM2;w$5v^a%+WItjFs?Ua%+! z2EPNlyFJL4WjKFiaA7n|7rr*WR%76uCq=~`zT-#xMK_kBC8P=!d|S@x8X)l__W~OC z_Ih#j{ycIe8AVBT{ob3#_Dk-@KV-cV>vX|$!z<|Q@gkM4&`OZSe;YX7w?KcNe~IDr zvmpIXvO4Kc|5Xe1eNx+U(4}Txk`?T`;cNU$Ohm3F%epT;GZfMc?7Hf~R3JmMH@8+m zNm9^xea_H;YcY%cH+>rSbbD}iC`4OvBQ4P})4?2ebj>zqfQTd*{u+nTII*i2pu;rCh3O$X9M3uIog** zV0D}bBnT(e>X7GTvjsMbG>eCB`|BFeLo6%d;IDZ^^jjfG0>Y8RqyywGlFpz3mW vgxr9cEtu0Nq)6ts6wkoGz`(#@X)*i+nHg@duH!xH00000NkvXXu0mjf9^qa} literal 0 HcmV?d00001 diff --git a/public/img/png/pay-mc.png b/public/img/png/pay-mc.png new file mode 100644 index 0000000000000000000000000000000000000000..06679ce80bc19034200a8cf5d4e907b4d220352c GIT binary patch literal 747 zcmVJNRCt{2n9XYwK^Vq26jAX|J^9hJ)_5o){t4NSX6>P?Cew;| z_TcJ&A3gM_S3eLvscjL`T{kLe6#N$id(lhQBzREKQqqIqD>G?ITitA?Gc_BT z2cFAjhdjUGotbw>l7xhWgoK3tCxxT024!SFQ&4e5K}GP1S7c-tvPKE zgNwi6jIIFdGY>xZf-8W}$_D;!`$szf=mkqG0D8=Xnehaabp;$X2y5Ye#5MIOEa@uw zZ4_Bs$6PdFCXs~leI1-a2-eE|#7-eZp5+rM^3JXUWQ-Kl)FS|4sjchEVMrO7=9sBz zs98e*Z}+x!%{mU5cxHD1H1r(i#wI+2Z}zWn0G2Wl&)~4lbGI*nHevGxpbd}x?&=xp z@UZSDw=VMoz-2=l{trLcLk*FRXW0Spg2juI86X+}ctZzJw+3AocEA*JUrqXNxuXULk;C=EmH-OT;vupsL)MM=B(=rNB12}~c8$d(e&krCXQ3Eu}2md`l zqu9@_gjP$3*Z>@LfEu9wFvty{vdCCMxZmMcLf87}7x7)jMVvQo@&mX-f0$)ix6w$< zgsMWf6aAF?;Paya=JKgoL+l4xvz16w&-RD&Cz%0KH*(!=<~E$Aul>h7n>lPPX2a95 zN$Pfb`O2vsb)p*YcDm)XG2PLcz#lZ%&&@W+#IsN}hdqN=&Ed9X!v&zVuyf-{DCrth z)F3(RR^38;WaVl2MkQA$t*I36hh z7Z8w(<&jl&cs&mj)qv+b0wbyma2B*nEdkCgVQDm|E5O4t5=4Anz1?WGe$FskUA|WI z$+q{5r1UY8Qq73UR^W0)1XO?(P%}wj*~V*-20p_rP}*r{WbFD?8cBHpWJhXX=q)cJ zSssp)1k$p^r!??JlXXg4HSpt4ftixyr zJGg9L=$8N|zyrkj&#%X{<73!!8~szjJx(@fzFz{YfUc4Rl5%TWs02*b@u;&s!*dGA zO1-}VJtTqU>n}CZ2qbO4*)SKprxZIie8T<*v77*>NdoDsE82X8PtUCAj;!kcN)(Xr z1pVq;8n%$&;|d!lGfd|vd^&knS)I|RJ9^2hKO^9v7T|bvoFrh_UDxgld|GD32VGfL zzgO_G`%b_u^c@i3eUd=Rrt4nKztLR&$&i2Np;nyb%$MivKq)1@D}ocyC`lkSTbk_z z`ffCr52a^Sb|i1EhWf)zkF^(+37^1aPibw=byWEJy-h2;xxjQKovk!r-i?pa28vx8 zDgpPmD+yk|ulb8w@Qk}e=T1NyPJkjwU~$pw=PC-^poaH!fcytSSJH)4866ov0p-Js zsNt99-HUPm3Lyz-D;=*?fWHMq9PQWY$68Phc>}{~M#5u(+^~#cfN+PB`I!dCpHvXYkP{ S)wg#50000Am9cLDqRaBAZHQaSqeGC zhACq2kRpgl4uaM+i+BZB!u0$iAQmi3uG;K60R|hbd4oj-d z@G4b7^?CL74KF3O!p=Yym=~AG|FB_!*o0x$z=pesL3bi0EqVdk$jeSVM5!S(#GyNPLWS`2V3vJ?s$teari_lMOIn`lh6 zWe5xOc|k8&{)y8g>03V?zmxHk>M`0edG>V*DX3+`ukIjQ;fZYZQ5P^3C+T%Tt zR;DNI7V|#vLnoPo!21B(Y@VQ#IRe))DGdP>Ti)C7qQF|bDBhtpRF5fP%Y8?{i|k@T zi>P(hYYh;V)lEgMGcFS~lf#gNKzNqDr(;o~1GX_?xA|N*OmtW)jXEPaZM>;5^{}@i zSz+r$V3dS=3CSOc_%00rcM=&RpkxsMmO|_IS%o*qoB^Nj*4#m*E8#vlC4g^__kR(^-H0=Dkt$zK00000 LNkvXXu0mjf7Uj?` literal 0 HcmV?d00001 diff --git a/public/img/png/twitterx.png b/public/img/png/twitterx.png index 406ebec172459920de6287e1da26d8109bf7105b..1e5a11588dcd114b8993eb6f1df289dbfb2ab0ff 100644 GIT binary patch delta 834 zcmV-I1HJr^3d#l{iBL{Q4GJ0x0000DNk~Le0000m0000m2nGNE09OL}hmj!{e**kT zL_t(|ob8#e}#{NP(Bv- zhuPbk;0GDPR5Jx1IX8aU2^3dbcccH-3qnY))tfnnhz@!mMT%J?aH zg`)Ts6ugqxNW1Yg^x3sme=)TD+Zvo?2=(CeDsZw@Fob7z^&mR0f4aSoX_#oVTk&+W zB2N_4CQZAvsD~Ou`KFoo<+W+GB~Z;T2Dxk%3DEg!|6!6A7|H>wdeCs6qzPW<+o=+x z77>{Ds{Q9ln&_?Y4yxf>XeTs$Hgi8o69t5umbLJNqz$T;M}ttyf77Ciq>0W^zMCO@ zv?%CVS5nj&)&fXDIgW~R%1U9<4i)EK^eDhaYRHEa;gv-c7Tn56OI8UJ)et60syNqd zC`^)2aY+{Ky28Y`G^G%)#GKz^=0DozfUN~LYFjW0~g3rsa` zs=FuSO_5B&J*!cp8uRud5G+c}G~cv?j_PPEv;!LE%vFEn1I?ce4dZ3W+*T6@n1^Ei z6k*{b;YD`<#ju%mT+2W@Esq7ANyBW5r*B4)k*%7OGBPqU7|5pl1BlN;Ra`Lr?EnA( M07*qoM6N<$f|@>qq5uE@ delta 1291 zcmV+m1@!vL29OFNiBL{Q4GJ0x0000DNk~Le0000o0000o2nGNE03JVxv5_Gce+0rw zL_t(|ob6fpZyQAv9&?ua5)>-Vatm=oDFtLTJF^Zwf?6^2#%Um;pmN_Jgeau6MJk|D zwF27E!`*pfCzwK78u*nzE$Hpe+9~lkn|PgA$e)y7k}q%H?7VsJn{OHGqmMrN_&=b) zjoK{`{>t=YVLCwdWS$8KgfNZIe-XZYe0;}+iwYD(FI^`L{F-$!+yJgSHgrWEizuOb zj3fM+EJBSJ>+LLQMuT0@^g|j`l<9S(bju;2)l?EBJ81tkv0C< z*w}dj;mgK(i(qSR$%kx>cQL5<&gn?;ljX?zW_;}pFXGwDU zdny|rFRaQn9MxIJ{aKii%et2Q+=s&sS7JwGU0&3RLkS6ls9n8>M&3ocmd>ETyqYZs1}ET7e_`QFTxNFoK_4$b z^{z|yQ1PFnLBwax!cT5$pQVn-mJFq7mFG+BLe=2DZ2q#F6u%5#Vsb3YV-@vGyJ{oltK-2)yU2PCVwUsa zWchyIyg0|)FnWOX3W7u=o^gx#MW8ZHp1(MsxzVmcEGc*-Z*4dqMosdXUM1}CBdPio zM&9B3Utx7@ZcwSQ1zeAFN9=qwZjLa;Be=hjB&xUE?|)3yZ!2@7A?eLBiAcJeq;rAZ z{=iAKevUH|f0r3)G4=Ai=7xbkin!%ww&c>p>YdHQ-TEw$Hn6x`Z#o}vIIHtP#$J4PiCskfe?*3zKI}6$^0}+0&W8o`8r!}k zt03Jj8lQ77XPFz^s8?9th$nGs@gPiAGv()UaJe?16?gi?}%%AqG-`r>m zs$V(}jZX=c(G@Ibkfh1F5Yel5mp=OFqYt(i{0p?*8(XbQe1iZ0002ovPDHLkV1jy9 Bg=PQ% diff --git a/public/img/png/union.png b/public/img/png/union.png index 9dd1fd59a8c1b9d29845fc6a4f8ac60df4ab5630..c573203c2383d75318d05608d1a3bcc62f0445d2 100644 GIT binary patch delta 755 zcmVW* ztff}`fo=2B){cs;wtrtGLwAc7GgD@!*+M?}PRmTbFE4K=`65I_L_|bHL_|K++O_`X z8l3~9hb8OVz-kh#e%LVh6s(?+e@t(|(kEcO0*2NdCDTt}+V0ZQ_wH|oSTcS9LvO&K zd;W>E`T-c!jTMx2#-UcC&WJKlU)6&RgLkE+cVKDrFHwg?Y3Um& zf$>KO@$g^j1-Y!w=Qm;WFc{hjFP!#Qh;dOSY9P}@yWKt}EiHu>3_co~f67q}bs@M= z&g7SDBNlJ=fY9yis!X2r8(}>jTqI|+u|}6NjNtvyA|VGQQ^RXSGCeCah`nX|4e{?DRBMq^?B1}nDM$+oEYyFL78Y?;u7}^v<^m%5j z*e+M(#9sg4?@B(3a7ac*H||wzgpbV9+7dgyrNwBfZFYKJ$N+bZtb1n+m_C+mq{S1Z z|NTPN4j3%W9fc2~G&JWmA}ziuYNXo-E@&v(0CbEny~wDDo-Q=Nf3m)wrXv*@fW3-5 z@*6QURkesR*fcniBRvT(6bhe5thyRnB;?@8lD^_MlBj`Rje?*^?>^jrYFqZtAq3!A?LM%d(Xeu3iVYz l=-uH)L_|bHL_~;v_ya3KWeeQud<*~p002ovPDHLkV1hF)S~LIv delta 428 zcmV;d0aO0`1-AnsiBL{Q4GJ0x0000DNk~Le0000J0000E2nGNE0L>UDiIE`}e*gh< zQb$4nuFf3k00004XF*Lt006O%3;baP0003@NklBb5Qb-F_b_*w7^Q_4 zT7Sb4KVoSkeiZx=OIu^7wP-{t3llmmrKP@B+bcN|+1+tgqId;I2t3K=<~BR;&c3sY zAROGE*B!YS*j4>M0*A~P!7LEcf5`_^L{7u7^zwYvPI0f(tb&0HSOyVa8xAjt95i5A z7|M2Yt3vFc3M;^Tjq4f^w}JU@Qhaz-WD-!6&CIkevQ{@3xi&2h%bJ;UMyntg^qQT- z$A?eHHDjYKEo5i$9Bz%nGxijybn{9vNKwZWC2{svCl WelrjJaJv8i002ovPDHLk0$_p{g|Kx1 diff --git a/src/shared/API/product/model/ProductModel.ts b/src/shared/API/product/model/ProductModel.ts index 3a0e8588..f30c6867 100644 --- a/src/shared/API/product/model/ProductModel.ts +++ b/src/shared/API/product/model/ProductModel.ts @@ -300,7 +300,7 @@ export class ProductModel { return null; } - public async getCategories(): Promise { + public async getCategories(): Promise { if (!this.categories.length) { const data = await this.root.getCategories(); return this.getCategoriesFromData(data); diff --git a/src/shared/types/common.ts b/src/shared/types/common.ts new file mode 100644 index 00000000..60ad1b62 --- /dev/null +++ b/src/shared/types/common.ts @@ -0,0 +1,4 @@ +export type languageVariants = { + en: string; + ru: string; +}; diff --git a/src/widgets/Footer/model/FooterModel.ts b/src/widgets/Footer/model/FooterModel.ts index bcccbc71..5bce38cf 100644 --- a/src/widgets/Footer/model/FooterModel.ts +++ b/src/widgets/Footer/model/FooterModel.ts @@ -1,8 +1,120 @@ +import type { languageVariants } from '@/shared/types/common.ts'; +import type { Category } from '@/shared/types/product.ts'; + +import getProductModel from '@/shared/API/product/model/ProductModel.ts'; +import observeStore, { selectCurrentLanguage } from '@/shared/Store/observer.ts'; +import { PAGE_ID } from '@/shared/constants/pages.ts'; +import { buildPathName } from '@/shared/utils/buildPathname.ts'; +import showErrorMessage from '@/shared/utils/userMessage.ts'; + import FooterView from '../view/FooterView.ts'; +export type Link = { + href?: string; + name: languageVariants; +}; + +const GENERAL_LINKS: Link[] = [ + { + href: PAGE_ID.USER_PROFILE_PAGE, + name: { + en: 'My account', + ru: 'Мой профиль', + }, + }, + { + href: PAGE_ID.ABOUT_US_PAGE, + name: { + en: 'About us', + ru: 'О нас', + }, + }, + { + href: PAGE_ID.BLOG, + name: { + en: 'Blog', + ru: 'Блог', + }, + }, + { + name: { + en: 'Career', + ru: 'Вакансии', + }, + }, + { + name: { + en: 'Specials', + ru: 'Сотрудничество', + }, + }, +]; + +const HELP_LINKS: Link[] = [ + { + name: { + en: 'Help Center', + ru: 'Помощь', + }, + }, + { + name: { + en: 'How to Buy', + ru: 'Покупки', + }, + }, + { + name: { + en: 'Delivery', + ru: 'Доставка', + }, + }, + { + name: { + en: 'Product Policy', + ru: 'Политика', + }, + }, + { + name: { + en: 'How to Return', + ru: 'Возврат', + }, + }, +]; + +function generateRandomCategoryLink(categoriesArr: Category[]): Link[] { + const result: Link[] = []; + for (let i = 0; i < 5; i += 1) { + const randomIndex = Math.floor(Math.random() * categoriesArr.length); + const category = categoriesArr[randomIndex]; + categoriesArr.splice(randomIndex, 1); + result.push({ + href: buildPathName(PAGE_ID.CATALOG_PAGE, null, { category: [category.id] }), + name: { + en: category.name[0].value, + ru: category.name[1].value, + }, + }); + } + return result; +} + class FooterModel { private view = new FooterView(); + constructor() { + this.init().catch(showErrorMessage); + } + + private async init(): Promise { + const categories: Category[] = await getProductModel().getCategories(); + const categoryLink: Link[] = generateRandomCategoryLink([...categories]); + this.view.addNavLists(GENERAL_LINKS, HELP_LINKS, categoryLink); + observeStore(selectCurrentLanguage, () => this.view.updateLanguage()); + return true; + } + public getHTML(): HTMLElement { return this.view.getHTML(); } diff --git a/src/widgets/Footer/view/FooterView.ts b/src/widgets/Footer/view/FooterView.ts index 432cb5f6..b1d432d3 100644 --- a/src/widgets/Footer/view/FooterView.ts +++ b/src/widgets/Footer/view/FooterView.ts @@ -1,17 +1,26 @@ +import type { LanguageChoiceType } from '@/shared/constants/common'; +import type { languageVariants } from '@/shared/types/common'; + +import RouterModel from '@/app/Router/model/RouterModel.ts'; import InputFieldModel from '@/entities/InputField/model/InputFieldModel.ts'; +import LinkModel from '@/shared/Link/model/LinkModel.ts'; +import getStore from '@/shared/Store/Store.ts'; import * as FORM_FIELDS from '@/shared/constants/forms/fieldParams.ts'; import * as FORM_VALIDATION from '@/shared/constants/forms/validationParams.ts'; import createBaseElement from '@/shared/utils/createBaseElement.ts'; +import type { Link } from '../model/FooterModel'; + import styles from './footerView.module.scss'; type Goal = { alt: string; - description: string; + description: languageVariants; + id: string; imgH: number; imgW: number; src: string; - title: string; + title: languageVariants; }; type Contact = { @@ -20,35 +29,62 @@ type Contact = { src: string; }; -type Social = { +type Img = { alt: string; src: string; }; +type GoalElement = { + goalDescription: HTMLParagraphElement; + goalItem: Goal; + goalTitle: HTMLParagraphElement; +}; + const GOALS: Goal[] = [ { alt: 'Garden Care', - description: 'Provide expert tips and tools for maintaining a healthy and beautiful garden', + description: { + en: 'Provide expert tips and tools for maintaining a healthy and beautiful garden', + ru: 'Для Вас советы экспертов и инструменты для поддержания здорового и красивого сада', + }, + id: 'goal_1', imgH: 93, imgW: 61, src: '/img/png/plant-01.png', - title: 'Garden Care', + title: { + en: 'Garden Care', + ru: 'Уход за садом', + }, }, { alt: 'Plant Renovation', - description: 'Offer solutions and products to revive and rejuvenate struggling plants', + description: { + en: 'Offer solutions and products to revive and rejuvenate struggling plants', + ru: 'Предлагаем решения и продукты для восстановления и омоложения растений, переживающих трудности', + }, + id: 'goal_2', imgH: 87, imgW: 68, src: '/img/png/plant-02.png', - title: 'Plant Renovation', + title: { + en: 'Plant Renovation', + ru: 'Обновление растений', + }, }, { alt: 'Watering Garden', - description: 'Ensure optimal hydration with efficient and innovative watering systems and advice', + description: { + en: 'Ensure optimal hydration with efficient and innovative watering systems and advice', + ru: 'Помогаем обеспечить оптимальный уход с помощью эффективных и инновационных систем полива и наших рекомендаций', + }, + id: 'goal_3', imgH: 85, imgW: 83, src: '/img/png/plant-03.png', - title: 'Watering Garden', + title: { + en: 'Watering Garden', + ru: 'Полив сада', + }, }, ]; const CONTACTS: Contact[] = [ @@ -69,7 +105,7 @@ const CONTACTS: Contact[] = [ }, ]; -const SOCIAL: Social[] = [ +const SOCIAL: Img[] = [ { alt: 'link meta', src: '/img/png/meta.png', @@ -92,12 +128,85 @@ const SOCIAL: Social[] = [ }, ]; +const PAY: Img[] = [ + { + alt: 'pay PayPal', + src: '/img/png/pay-pp.png', + }, + { + alt: 'pay MasterCard', + src: '/img/png/pay-mc.png', + }, + { + alt: 'pay Visa', + src: '/img/png/pay-visa.png', + }, + { + alt: 'pay American Express', + src: '/img/png/pay-ae.png', + }, +]; + +type textElementsType = { + element: HTMLAnchorElement | HTMLButtonElement | HTMLInputElement | HTMLParagraphElement | HTMLUListElement; + textItem: languageVariants; +}; +const FOOTER_PAGE = { + NAV_CATEGORY: { + en: 'Categories', + ru: 'Категории', + }, + NAV_GENERAL: { + en: 'General', + ru: 'Главная', + }, + NAV_HELP: { + en: 'Help & Guide', + ru: 'FAQ', + }, + PAY: { + en: 'We accept', + ru: 'Мы принимаем', + }, + SOCIAL: { + en: 'Social Media', + ru: 'Соцсети', + }, + SUB_BTN: { + en: 'Join', + ru: 'С нами', + }, + SUB_DESCRIPTION: { + en: 'Subscribe to our newsletter for updates on new arrivals, exclusive discounts, and notifications about our latest blog articles. Stay informed and enhance your gardening experience with our expert tips and offers!', + ru: 'Подпишитесь на нашу рассылку, чтобы получать обновления о новых поступлениях, эксклюзивные скидки и уведомления о наших последних статьях в блоге. Будьте в курсе и улучшайте свой опыт садоводства с нашими экспертными советами и предложениями!', + }, + SUB_PLACEHOLDER: { + en: 'enter your email address...', + ru: 'введите ваш email...', + }, + SUB_TITLE: { + en: 'Would you like to join newsletters?', + ru: 'Хотите подписаться на нашу рассылку?', + }, +}; + class FooterView { private footer: HTMLElement; + private goals: GoalElement[] = []; + + private language: LanguageChoiceType; + + private navList: HTMLUListElement[] = []; + + private navListWrap: HTMLDivElement; + + private textElements: textElementsType[] = []; + private wrapper: HTMLDivElement; constructor() { + this.language = getStore().getState().currentLanguage; this.wrapper = this.createWrapper(); this.footer = this.createHTML(); const blockGoals = this.createGoalsHTML(); @@ -105,8 +214,11 @@ class FooterView { const goalsSubWrap = this.createWrapHTML(); goalsSubWrap.append(blockGoals, blockSubscribe); const contactWrap = this.createContactHTML(); + const navigateWrap = this.createNavigateHTML(); const socialWrap = this.createSocialHTML(); - this.wrapper.append(goalsSubWrap, contactWrap, socialWrap); + this.navListWrap = this.createNavListWrapHTML(); + navigateWrap.append(this.navListWrap, socialWrap); + this.wrapper.append(goalsSubWrap, contactWrap, navigateWrap); } private createContactHTML(): HTMLDivElement { @@ -122,8 +234,6 @@ class FooterView { cssClasses: [styles.logoImage], tag: 'img', }); - logoImg.style.width = `150px`; - logoImg.style.height = `34px`; const wrapContactItems = createBaseElement({ cssClasses: [styles.contactItemsWrap], @@ -147,8 +257,6 @@ class FooterView { cssClasses: [styles.contactIcon], tag: 'img', }); - icon.style.width = `20px`; - icon.style.height = `20px`; const tittle = createBaseElement({ cssClasses: [styles.contactText], innerContent: contact.description, @@ -167,33 +275,27 @@ class FooterView { cssClasses: [styles.goalImage], tag: 'img', }); - goalImg.style.width = `${goalItem.imgW}px`; - goalImg.style.height = `${goalItem.imgH}px`; - const goalImgWrap = createBaseElement({ - cssClasses: [styles.goalImgWrap], - tag: 'div', - }); + const goalImgWrap = createBaseElement({ cssClasses: [styles.goalImgWrap], tag: 'div' }); goalImgWrap.append(goalImg); - const goalTextWrap = createBaseElement({ - cssClasses: [styles.goalTextWrap], - tag: 'div', - }); + const goalTextWrap = createBaseElement({ cssClasses: [styles.goalTextWrap], tag: 'div' }); const title = createBaseElement({ cssClasses: [styles.goalTitle], - innerContent: goalItem.title, + innerContent: goalItem.title[this.language], tag: 'p', }); const description = createBaseElement({ cssClasses: [styles.goalsDescription], - innerContent: goalItem.description, + innerContent: goalItem.description[this.language], tag: 'p', }); goalTextWrap.append(title, description); - const goal = createBaseElement({ - cssClasses: [styles.goal], - tag: 'div', - }); + const goal = createBaseElement({ cssClasses: [styles.goal], tag: 'div' }); goal.append(goalImgWrap, goalTextWrap); + this.goals.push({ + goalDescription: description, + goalItem, + goalTitle: title, + }); return goal; } @@ -204,7 +306,9 @@ class FooterView { }); GOALS.forEach((goal) => { - wrap.append(this.createGoalHTML(goal)); + const goalHTML = this.createGoalHTML(goal); + + wrap.append(goalHTML); }); return wrap; @@ -220,6 +324,29 @@ class FooterView { return this.footer; } + private createLink(linkParams: Link): LinkModel { + const link = new LinkModel({ + attrs: { + ...(linkParams.href && { href: linkParams.href }), + }, + classes: [styles.link], + text: linkParams.name[this.language], + }); + + if (linkParams.href) { + link.getHTML().addEventListener('click', (event) => { + event.preventDefault(); + if (linkParams.href) { + RouterModel.getInstance().navigateTo(linkParams.href); + } + }); + } else { + link.getHTML().classList.add(styles.inactive); + } + + return link; + } + private createLinkHTML(): HTMLDivElement { const wrap = createBaseElement({ cssClasses: [styles.socialWrap], @@ -227,7 +354,7 @@ class FooterView { }); const title = createBaseElement({ cssClasses: [styles.subTitle], - innerContent: 'Social Media', + innerContent: FOOTER_PAGE.SOCIAL[this.language], tag: 'p', }); const buttonWrap = createBaseElement({ @@ -251,17 +378,91 @@ class FooterView { buttonWrap.append(link); }); wrap.append(title, buttonWrap); + this.textElements.push({ element: title, textItem: FOOTER_PAGE.SOCIAL }); + return wrap; + } + + private createNavListHTML(title: languageVariants, navItems: Link[]): HTMLUListElement { + const navList = createBaseElement({ + cssClasses: [styles.navTitle], + tag: 'ul', + }); + + const titleEl = createBaseElement({ + cssClasses: [styles.navTitle], + innerContent: title[this.language], + tag: 'p', + }); + navList.append(titleEl); + navItems.forEach((navItem) => { + const navItemHTML = createBaseElement({ + cssClasses: [styles.navItem], + tag: 'li', + }); + const link = this.createLink(navItem); + navItemHTML.append(link.getHTML()); + navList.append(navItemHTML); + this.textElements.push({ element: link.getHTML(), textItem: navItem.name }); + }); + this.textElements.push({ element: titleEl, textItem: title }); + return navList; + } + + private createNavListWrapHTML(): HTMLDivElement { + return createBaseElement({ + cssClasses: [styles.navListWrap], + tag: 'div', + }); + } + + private createNavigateHTML(): HTMLDivElement { + const wrap = createBaseElement({ + cssClasses: [styles.navigateWrap], + tag: 'div', + }); + return wrap; + } + + private createPayHTML(): HTMLDivElement { + const wrap = createBaseElement({ + cssClasses: [styles.payWrap], + tag: 'div', + }); + const title = createBaseElement({ + cssClasses: [styles.payTitle], + innerContent: FOOTER_PAGE.PAY[this.language], + tag: 'p', + }); + const buttonWrap = createBaseElement({ + cssClasses: [styles.payImgWrap], + tag: 'div', + }); + PAY.forEach((pay) => + buttonWrap.append( + createBaseElement({ + attributes: { + alt: pay.alt, + src: pay.src, + }, + cssClasses: [styles.payImg], + tag: 'img', + }), + ), + ); + wrap.append(title, buttonWrap); + this.textElements.push({ element: title, textItem: FOOTER_PAGE.PAY }); return wrap; } private createSocialHTML(): HTMLDivElement { const wrap = createBaseElement({ - cssClasses: [styles.socialSubWrap], + cssClasses: [styles.socialPayWrap], tag: 'div', }); const blockSocial = this.createLinkHTML(); - wrap.append(blockSocial); + const blockPay = this.createPayHTML(); + wrap.append(blockSocial, blockPay); return wrap; } @@ -272,10 +473,16 @@ class FooterView { }); const email = new InputFieldModel(FORM_FIELDS.EMAIL, FORM_VALIDATION.EMAIL_VALIDATE); const inputHTML = email.getView().getInput().getHTML(); - inputHTML.placeholder = 'enter your email address...'; + inputHTML.placeholder = FOOTER_PAGE.SUB_PLACEHOLDER[this.language]; inputHTML.classList.add(styles.subInput); - const submit = createBaseElement({ cssClasses: [styles.subButton], innerContent: 'Join', tag: 'button' }); + const submit = createBaseElement({ + cssClasses: [styles.subButton], + innerContent: FOOTER_PAGE.SUB_BTN[this.language], + tag: 'button', + }); form.append(inputHTML, submit); + this.textElements.push({ element: submit, textItem: FOOTER_PAGE.SUB_BTN }); + this.textElements.push({ element: inputHTML, textItem: FOOTER_PAGE.SUB_PLACEHOLDER }); return form; } @@ -286,17 +493,17 @@ class FooterView { }); const title = createBaseElement({ cssClasses: [styles.subTitle], - innerContent: 'Would you like to join newsletters?', + innerContent: FOOTER_PAGE.SUB_TITLE[this.language], tag: 'p', }); const description = createBaseElement({ cssClasses: [styles.subDescription], - innerContent: - 'Subscribe to our newsletter for updates on new arrivals, exclusive discounts, and notifications about our latest blog articles. Stay informed and enhance your gardening experience with our expert tips and offers!', + innerContent: FOOTER_PAGE.SUB_DESCRIPTION[this.language], tag: 'p', }); - + this.textElements.push({ element: title, textItem: FOOTER_PAGE.SUB_TITLE }); + this.textElements.push({ element: description, textItem: FOOTER_PAGE.SUB_DESCRIPTION }); const form = this.createSubFormHtml(); wrap.append(title, form, description); return wrap; @@ -320,12 +527,34 @@ class FooterView { return this.wrapper; } + public addNavLists(generalLinks: Link[], helpLinks: Link[], categoryLinks: Link[]): void { + const generalNav = this.createNavListHTML(FOOTER_PAGE.NAV_GENERAL, generalLinks); + const helpNav = this.createNavListHTML(FOOTER_PAGE.NAV_HELP, helpLinks); + const categoryNav = this.createNavListHTML(FOOTER_PAGE.NAV_CATEGORY, categoryLinks); + this.navList.push(generalNav, helpNav, categoryNav); + this.navListWrap.append(generalNav, helpNav, categoryNav); + } + public getHTML(): HTMLElement { return this.footer; } - public getWrapper(): HTMLDivElement { - return this.wrapper; + public updateLanguage(): void { + this.language = getStore().getState().currentLanguage; + this.goals.forEach((goalEl) => { + const title = goalEl.goalTitle; + const description = goalEl.goalDescription; + title.textContent = goalEl.goalItem.title[this.language]; + description.textContent = goalEl.goalItem.description[this.language]; + }); + this.textElements.forEach((el) => { + const elHTML = el.element; + if (elHTML instanceof HTMLInputElement) { + elHTML.placeholder = el.textItem[this.language]; + } else { + elHTML.textContent = el.textItem[this.language]; + } + }); } } diff --git a/src/widgets/Footer/view/footerView.module.scss b/src/widgets/Footer/view/footerView.module.scss index b104fda1..deea8c56 100644 --- a/src/widgets/Footer/view/footerView.module.scss +++ b/src/widgets/Footer/view/footerView.module.scss @@ -18,7 +18,7 @@ flex-direction: column; } -.socialSubWrap, +.navigateWrap, .goalsSubWrap { display: flex; flex-wrap: wrap; @@ -28,8 +28,28 @@ background-color: var(--white-tr); } -.socialSubWrap { +.socialPayWrap { + display: flex; + flex-direction: column; justify-content: space-around; + padding: var(--extra-small-offset); + gap: var(--extra-small-offset); +} + +.navListWrap, +.navigateWrap { + justify-content: space-between; + gap: var(--extra-small-offset); + + @media (max-width: 768px) { + justify-content: space-evenly; + } +} + +.navListWrap { + display: flex; + flex-grow: 1; + flex-wrap: wrap; } .goalsWrap { @@ -66,7 +86,7 @@ gap: var(--tiny-offset); &:not(:last-child) { - border-right: 1px solid var(--steam-green-350); + border-right: var(--one) solid var(--steam-green-350); } @media (max-width: 768px) { @@ -113,6 +133,7 @@ color: var(--black); } +.navItem, .goalsDescription, .subDescription { padding: var(--tiny-offset) 0; @@ -133,6 +154,8 @@ max-width: calc(var(--extra-large-offset) * 3); } +.navTitle, +.payTitle, .subTitle { padding: var(--tiny-offset) 0 var(--extra-small-offset); font: var(--medium-bold-font); @@ -175,14 +198,14 @@ padding: var(--extra-small-offset); width: 100%; background-color: var(--steam-green-350); - gap: var(--extra-small-offset); + gap: 10%; } .contactItemsWrap { display: flex; flex: 1 1 auto; flex-wrap: wrap; - justify-content: space-between; + justify-content: space-evenly; gap: var(--extra-small-offset); @media (max-width: 768px) { @@ -192,13 +215,13 @@ .logoImage { margin: 0 auto; - width: 150px; - height: 34px; + width: calc(var(--extra-large-offset) * 1.5); + height: calc(var(--extra-small-offset) * 1.7); } .contactItem { display: flex; - flex-basis: 30%; + flex-basis: 25%; align-items: center; gap: var(--tiny-offset); } @@ -223,9 +246,50 @@ display: flex; align-items: center; justify-content: center; - border: 1px solid var(--steam-green-300); + border: var(--one) solid var(--steam-green-300); border-radius: var(--small-br); padding: var(--tiny-offset); width: var(--small-offset); height: var(--small-offset); + cursor: pointer; + + &:hover { + background-color: var(--steam-green-400); + } +} + +.payImgWrap { + display: flex; + align-items: center; + gap: var(--tiny-offset); +} + +.payImg { + width: var(--small-offset); + height: var(--small-offset); +} + +.link { + cursor: pointer; + + &:hover { + color: var(--steam-green-800); + } +} + +.inactive { + opacity: 0.5; + cursor: default; + + &:hover { + color: var(--noble-gray-700); + } +} + +.navTitle { + flex-basis: 30%; + + @media (max-width: 768px) { + flex-basis: max-content; + } } From 20ec7742ecbce701348dd7f2ad8217fc3151488a Mon Sep 17 00:00:00 2001 From: Max <133934232+Kleostro@users.noreply.github.com> Date: Sun, 26 May 2024 16:14:30 +0300 Subject: [PATCH 04/28] feat(RSS-ECOMM-4_31): adaptive (#314) * feat: add scroll top on every page * feat: adaptive noUiSlider * feat: add adaptive up to 380px --- src/app/styles/noUiSlider.scss | 19 ++++++- .../view/navigationView.module.scss | 6 +++ .../ProductPrice/view/productPriceView.scss | 6 ++- .../view/breadcrumbsView.module.scss | 4 ++ .../view/passwordEditView.module.scss | 28 ++-------- .../view/personalInfoEditView.module.scss | 27 ++-------- .../view/productFiltersView.module.scss | 2 +- src/pages/AboutUsPage/view/AboutUsPageView.ts | 1 + src/pages/Blog/Post/view/PostView.ts | 1 + src/pages/Blog/PostList/view/PostListView.ts | 2 + .../Blog/PostWidget/view/PostWidgetView.ts | 2 + src/pages/CartPage/view/CartPageView.ts | 1 + src/pages/CatalogPage/view/CatalogPageView.ts | 1 + src/pages/LoginPage/view/LoginPageView.ts | 1 + src/pages/MainPage/view/MainPageView.ts | 1 + .../NotFoundPage/view/NotFoundPageView.ts | 1 + src/pages/ProductPage/view/ProductPageView.ts | 1 + .../view/RegistrationPageView.ts | 1 + .../view/UserProfilePageView.ts | 1 + .../view/userProfilePageView.module.scss | 28 ++-------- src/shared/Modal/view/modalView.module.scss | 1 + .../Header/view/headerView.module.scss | 34 ++++++++++-- .../ProductInfo/view/productInfoView.scss | 52 +++++++++++++++++-- .../UserInfo/view/userInfoView.module.scss | 30 +++-------- 24 files changed, 143 insertions(+), 108 deletions(-) diff --git a/src/app/styles/noUiSlider.scss b/src/app/styles/noUiSlider.scss index ad64af9d..b8b14d5b 100644 --- a/src/app/styles/noUiSlider.scss +++ b/src/app/styles/noUiSlider.scss @@ -14,6 +14,13 @@ top: -0.3rem; width: var(--extra-small-offset); height: var(--extra-small-offset); + + @media (max-width: 768px) { + right: -0.5rem; + top: -0.2rem; + width: calc(var(--extra-small-offset) / 1.5); + height: calc(var(--extra-small-offset) / 1.5); + } } .noUi-handle { @@ -21,7 +28,7 @@ border: var(--two) solid var(--noble-white-100); border-radius: 50%; box-shadow: none; - background-color: var(--steam-green-800); + background-color: var(--steam-green-700); -webkit-backface-visibility: hidden; backface-visibility: hidden; transition: background-color 0.3; @@ -64,8 +71,12 @@ .noUi-connect { height: 0.5rem; - background: var(--steam-green-800); + background: var(--steam-green-700); cursor: pointer; + + @media (max-width: 768px) { + height: 0.4rem; + } } .noUi-base, @@ -91,6 +102,10 @@ .noUi-horizontal { height: 0.5rem; + + @media (max-width: 768px) { + height: 0.4rem; + } } .noUi-state-tap .noUi-connect, diff --git a/src/entities/Navigation/view/navigationView.module.scss b/src/entities/Navigation/view/navigationView.module.scss index 908a3d45..cc39eae7 100644 --- a/src/entities/Navigation/view/navigationView.module.scss +++ b/src/entities/Navigation/view/navigationView.module.scss @@ -6,6 +6,12 @@ order: 2; margin: 0 auto; height: calc(var(--extra-small-offset) * 3.5); // 70px + + @media (max-width: 768px) { + align-self: auto; + justify-content: space-between; + margin: 0; + } } .link { diff --git a/src/entities/ProductPrice/view/productPriceView.scss b/src/entities/ProductPrice/view/productPriceView.scss index 71666994..9e36a732 100644 --- a/src/entities/ProductPrice/view/productPriceView.scss +++ b/src/entities/ProductPrice/view/productPriceView.scss @@ -37,6 +37,10 @@ justify-content: start; order: 2; margin-top: 0; - margin-bottom: var(--extra-small-offset); + + @media (max-width: 768px) { + align-items: center; + justify-content: center; + } } } diff --git a/src/features/Breadcrumbs/view/breadcrumbsView.module.scss b/src/features/Breadcrumbs/view/breadcrumbsView.module.scss index 83709183..dbc2bcc8 100644 --- a/src/features/Breadcrumbs/view/breadcrumbsView.module.scss +++ b/src/features/Breadcrumbs/view/breadcrumbsView.module.scss @@ -3,6 +3,10 @@ align-items: center; margin-bottom: var(--medium-offset); gap: var(--tiny-offset); + + @media (max-width: 768px) { + margin-bottom: var(--small-offset); + } } .link { diff --git a/src/features/PasswordEdit/view/passwordEditView.module.scss b/src/features/PasswordEdit/view/passwordEditView.module.scss index eff20751..2dda3dab 100644 --- a/src/features/PasswordEdit/view/passwordEditView.module.scss +++ b/src/features/PasswordEdit/view/passwordEditView.module.scss @@ -1,3 +1,5 @@ +@import 'src/app/styles/mixins'; + .wrapper { display: grid; place-items: center center; @@ -12,33 +14,11 @@ .saveChangesButton, .cancelButton { + @include green-btn; + 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: var(--one); - 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; - } } .saveChangesButton { diff --git a/src/features/PersonalInfoEdit/view/personalInfoEditView.module.scss b/src/features/PersonalInfoEdit/view/personalInfoEditView.module.scss index 9137de11..dd753ca5 100644 --- a/src/features/PersonalInfoEdit/view/personalInfoEditView.module.scss +++ b/src/features/PersonalInfoEdit/view/personalInfoEditView.module.scss @@ -1,3 +1,5 @@ +@import 'src/app/styles/mixins'; + .wrapper { display: grid; place-items: center center; @@ -29,33 +31,12 @@ .saveChangesButton, .cancelButton { + @include green-btn; + margin: 0 auto; - border-radius: var(--medium-br); padding: calc(var(--small-offset) / 3) var(--small-offset); width: 100%; height: max-content; - font: var(--regular-font); - letter-spacing: var(--one); - 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; - } } .saveChangesButton { diff --git a/src/features/ProductFilters/view/productFiltersView.module.scss b/src/features/ProductFilters/view/productFiltersView.module.scss index 912efb87..4b8c5310 100644 --- a/src/features/ProductFilters/view/productFiltersView.module.scss +++ b/src/features/ProductFilters/view/productFiltersView.module.scss @@ -160,7 +160,7 @@ max-width: calc(var(--extra-large-offset) * 3.5); font: var(--regular-font); letter-spacing: var(--one); - color: var(--steam-green-800); + color: var(--steam-green-400); background-color: var(--noble-white-200); transition: border-color 0.2s, diff --git a/src/pages/AboutUsPage/view/AboutUsPageView.ts b/src/pages/AboutUsPage/view/AboutUsPageView.ts index b05ea098..d0a0f4ef 100644 --- a/src/pages/AboutUsPage/view/AboutUsPageView.ts +++ b/src/pages/AboutUsPage/view/AboutUsPageView.ts @@ -11,6 +11,7 @@ class AboutUsPageView { this.parent = parent; this.parent.innerHTML = ''; this.page = this.createHTML(); + window.scrollTo(0, 0); } private createHTML(): HTMLDivElement { diff --git a/src/pages/Blog/Post/view/PostView.ts b/src/pages/Blog/Post/view/PostView.ts index 5a804bdc..f8b698f6 100644 --- a/src/pages/Blog/Post/view/PostView.ts +++ b/src/pages/Blog/Post/view/PostView.ts @@ -23,6 +23,7 @@ export default class PostView { this.card.addEventListener('click', () => this.onPostClick()); observeStore(selectCurrentLanguage, () => this.updateLanguage()); + window.scrollTo(0, 0); } private createCardHTML(): HTMLLIElement { diff --git a/src/pages/Blog/PostList/view/PostListView.ts b/src/pages/Blog/PostList/view/PostListView.ts index edcab59b..83eae653 100644 --- a/src/pages/Blog/PostList/view/PostListView.ts +++ b/src/pages/Blog/PostList/view/PostListView.ts @@ -28,6 +28,7 @@ export default class PostListView { this.description = this.createPageDescription(); this.page = this.createHTML(); + window.scrollTo(0, 0); } private createHTML(): HTMLDivElement { @@ -81,6 +82,7 @@ export default class PostListView { public openPost(post: BlogPostView): void { this.parent.innerHTML = ''; this.parent.append(post.getPostHTML()); + window.scrollTo(0, 0); } public updateLanguage(): boolean { diff --git a/src/pages/Blog/PostWidget/view/PostWidgetView.ts b/src/pages/Blog/PostWidget/view/PostWidgetView.ts index 10de4158..d5d00ebf 100644 --- a/src/pages/Blog/PostWidget/view/PostWidgetView.ts +++ b/src/pages/Blog/PostWidget/view/PostWidgetView.ts @@ -27,6 +27,7 @@ export default class PostWidgetView { this.description = this.createPageDescription(); this.page = this.createHTML(); + window.scrollTo(0, 0); } private createHTML(): HTMLDivElement { @@ -80,6 +81,7 @@ export default class PostWidgetView { public openPost(post: BlogPostView): void { this.parent.innerHTML = ''; this.parent.append(post.getPostHTML()); + window.scrollTo(0, 0); } public updateLanguage(): boolean { diff --git a/src/pages/CartPage/view/CartPageView.ts b/src/pages/CartPage/view/CartPageView.ts index a69b704d..7897f6ae 100644 --- a/src/pages/CartPage/view/CartPageView.ts +++ b/src/pages/CartPage/view/CartPageView.ts @@ -29,6 +29,7 @@ class CartPageView { this.productWrap.classList.add(styles.products); this.totalWrap = this.createWrapHTML(); this.totalWrap.classList.add(styles.total); + window.scrollTo(0, 0); } private addTableHeader(): void { diff --git a/src/pages/CatalogPage/view/CatalogPageView.ts b/src/pages/CatalogPage/view/CatalogPageView.ts index b189648f..15a4e713 100644 --- a/src/pages/CatalogPage/view/CatalogPageView.ts +++ b/src/pages/CatalogPage/view/CatalogPageView.ts @@ -11,6 +11,7 @@ class CatalogPageView { this.parent = parent; this.parent.innerHTML = ''; this.page = this.createHTML(); + window.scrollTo(0, 0); } private createHTML(): HTMLDivElement { diff --git a/src/pages/LoginPage/view/LoginPageView.ts b/src/pages/LoginPage/view/LoginPageView.ts index e6b37a04..9beb1616 100644 --- a/src/pages/LoginPage/view/LoginPageView.ts +++ b/src/pages/LoginPage/view/LoginPageView.ts @@ -44,6 +44,7 @@ class LoginPageView { this.linksWrapper = this.createLinksWrapper(); this.authWrapper = this.createAuthWrapper(); this.page = this.createHTML(); + window.scrollTo(0, 0); } private createAuthDescription(): HTMLHeadingElement { diff --git a/src/pages/MainPage/view/MainPageView.ts b/src/pages/MainPage/view/MainPageView.ts index cd6bbd83..4f57f37b 100644 --- a/src/pages/MainPage/view/MainPageView.ts +++ b/src/pages/MainPage/view/MainPageView.ts @@ -11,6 +11,7 @@ class MainPageView { this.parent = parent; this.parent.innerHTML = ''; this.page = this.createHTML(); + window.scrollTo(0, 0); } private createHTML(): HTMLDivElement { diff --git a/src/pages/NotFoundPage/view/NotFoundPageView.ts b/src/pages/NotFoundPage/view/NotFoundPageView.ts index 4ec17f5b..b62f09ec 100644 --- a/src/pages/NotFoundPage/view/NotFoundPageView.ts +++ b/src/pages/NotFoundPage/view/NotFoundPageView.ts @@ -30,6 +30,7 @@ class NotFoundPageView { this.description = this.createPageDescription(); this.toMainButton = this.createToMainButton(); this.page = this.createHTML(); + window.scrollTo(0, 0); } private createHTML(): HTMLDivElement { diff --git a/src/pages/ProductPage/view/ProductPageView.ts b/src/pages/ProductPage/view/ProductPageView.ts index e2009f4c..b75c70c3 100644 --- a/src/pages/ProductPage/view/ProductPageView.ts +++ b/src/pages/ProductPage/view/ProductPageView.ts @@ -20,6 +20,7 @@ class ProductPageView { this.fullDescription = this.createFullDescription(); this.fullDescriptionWrapper = this.createFullDescriptionWrapper(); this.page = this.createHTML(); + window.scrollTo(0, 0); } private createFullDescription(): HTMLParagraphElement { diff --git a/src/pages/RegistrationPage/view/RegistrationPageView.ts b/src/pages/RegistrationPage/view/RegistrationPageView.ts index 6b9ed460..fec17b5f 100644 --- a/src/pages/RegistrationPage/view/RegistrationPageView.ts +++ b/src/pages/RegistrationPage/view/RegistrationPageView.ts @@ -44,6 +44,7 @@ class RegistrationPageView { this.linksWrapper = this.createLinksWrapper(); this.authWrapper = this.createAuthWrapper(); this.page = this.createHTML(); + window.scrollTo(0, 0); } private createAuthDescription(): HTMLHeadingElement { diff --git a/src/pages/UserProfilePage/view/UserProfilePageView.ts b/src/pages/UserProfilePage/view/UserProfilePageView.ts index ad1c3470..10007139 100644 --- a/src/pages/UserProfilePage/view/UserProfilePageView.ts +++ b/src/pages/UserProfilePage/view/UserProfilePageView.ts @@ -48,6 +48,7 @@ class UserProfilePageView { this.setLinksHandlers(); this.userProfileWrapper = this.createUserProfileWrapper(); this.page = this.createHTML(); + window.scrollTo(0, 0); } private createAccountMenu(): HTMLUListElement { diff --git a/src/pages/UserProfilePage/view/userProfilePageView.module.scss b/src/pages/UserProfilePage/view/userProfilePageView.module.scss index 4885fc0e..6cf4ec28 100644 --- a/src/pages/UserProfilePage/view/userProfilePageView.module.scss +++ b/src/pages/UserProfilePage/view/userProfilePageView.module.scss @@ -1,3 +1,5 @@ +@import 'src/app/styles/mixins'; + .userProfilePage { position: relative; display: block; @@ -55,32 +57,10 @@ } .logoutButton { + @include green-btn; + margin: var(--small-offset) auto; - border-radius: var(--medium-br); padding: calc(var(--small-offset) / 3) var(--small-offset); - max-width: max-content; - font: var(--regular-font); - letter-spacing: var(--one); - 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 { diff --git a/src/shared/Modal/view/modalView.module.scss b/src/shared/Modal/view/modalView.module.scss index e60982dd..41701cd3 100644 --- a/src/shared/Modal/view/modalView.module.scss +++ b/src/shared/Modal/view/modalView.module.scss @@ -43,6 +43,7 @@ top: 50%; border: var(--two) solid var(--noble-black-500); border-radius: var(--border-radius); + max-width: 95%; font: var(--small-font); letter-spacing: var(--one); color: var(--steam-green-500); diff --git a/src/widgets/Header/view/headerView.module.scss b/src/widgets/Header/view/headerView.module.scss index 05e9aa44..743ac766 100644 --- a/src/widgets/Header/view/headerView.module.scss +++ b/src/widgets/Header/view/headerView.module.scss @@ -5,7 +5,7 @@ left: 0; right: 0; top: 0; - z-index: 10; + z-index: 1; border-bottom: 1px solid var(--steam-green-300); width: 100%; background-color: var(--noble-gray-1000); @@ -17,6 +17,11 @@ align-items: center; margin: 0 auto; padding: 0 var(--small-offset); + + @media (max-width: 768px) { + display: grid; + padding: 0; + } } .navigationWrapper { @@ -37,8 +42,18 @@ backdrop-filter: blur(10px); gap: var(--medium-offset); + @media (max-width: 768px) { + top: calc(var(--extra-small-offset) * 6.1); + grid-row: 2; + width: 45%; + } + &.open { transform: translateX(-500%); + + @media (max-width: 768px) { + transform: translateX(-220%); + } } } @@ -61,6 +76,12 @@ } } } + + @media (max-width: 768px) { + grid-row: 1; + margin-top: calc(var(--tiny-offset) * 1.5); + margin-left: calc(var(--extra-small-offset) * 1.5); + } } .logoutButton { @@ -135,11 +156,16 @@ .burgerButton { position: absolute; - right: calc(var(--tiny-offset) * 1.5); + right: calc(var(--extra-small-offset) * 1.5); z-index: 10; order: 4; width: calc(var(--tiny-offset) * 2.5); height: var(--extra-small-offset); + + @media (max-width: 768px) { + top: calc(var(--extra-small-offset) * 1.5); + grid-row: 1; + } } .burgerLine { @@ -212,8 +238,8 @@ } .burgerButton.open .burgerLine:nth-child(1) { - left: var(--five); // 5px - top: calc(var(--tiny-offset) * 1.5); // 15px + left: calc(var(--five) + 0.04rem); + top: calc(var(--tiny-offset) * 1.1); width: var(--extra-small-offset); // 20px transform: rotate(90deg); transition-delay: 150ms; diff --git a/src/widgets/ProductInfo/view/productInfoView.scss b/src/widgets/ProductInfo/view/productInfoView.scss index 6564408d..80343177 100644 --- a/src/widgets/ProductInfo/view/productInfoView.scss +++ b/src/widgets/ProductInfo/view/productInfoView.scss @@ -6,25 +6,42 @@ justify-content: center; margin-bottom: var(--small-offset); gap: var(--small-offset); + + @media (max-width: 768px) { + flex-direction: column; + } } .leftWrapper { display: flex; max-width: 50%; + + @media (max-width: 768px) { + max-width: 100%; + } } .rightWrapper { display: flex; flex-direction: column; width: 40%; + gap: var(--extra-small-offset); + + @media (max-width: 768px) { + width: 100%; + gap: var(--tiny-offset); + } } .title { order: 1; - margin-bottom: var(--extra-small-offset); font: var(--medium-font); letter-spacing: var(--one); color: var(--steam-green-500); + + @media (max-width: 768px) { + text-align: center; + } } .smallSlider { @@ -77,25 +94,38 @@ font: var(--regular-font); letter-spacing: var(--one); color: var(--steam-green-400); + + @media (max-width: 768px) { + text-align: center; + } } .shortDescription, .fullDescription { - margin-bottom: var(--extra-small-offset); font: var(--regular-font); color: var(--noble-gray-800); + + @media (max-width: 768px) { + text-align: center; + } } .sizesWrapper { display: grid; order: 4; grid-template-columns: max-content; - margin-bottom: var(--extra-small-offset); width: max-content; font: var(--regular-font); letter-spacing: var(--one); color: var(--steam-green-400); gap: var(--tiny-offset); + + @media (max-width: 768px) { + display: flex; + align-items: center; + align-self: center; + justify-content: center; + } } .sizeButton { @@ -123,6 +153,10 @@ color: var(--steam-green-400); } } + + @media (max-width: 768px) { + grid-row: none; + } } .selected { @@ -134,10 +168,13 @@ /* stylelint-disable-next-line selector-class-pattern */ .SKUSpan, .categoriesSpan { - margin-bottom: var(--extra-small-offset); font: var(--regular-font); letter-spacing: var(--one); color: var(--noble-gray-800); + + @media (max-width: 768px) { + align-self: center; + } } /* stylelint-disable-next-line selector-class-pattern */ @@ -183,14 +220,19 @@ @include green-btn; padding: calc(var(--small-offset) / 3) calc(var(--small-offset) / 2); + width: 100%; + max-width: 12rem; } .buttonsWrapper { display: flex; align-items: center; order: 5; - margin-bottom: var(--extra-small-offset); gap: var(--tiny-offset); + + @media (max-width: 768px) { + align-self: center; + } } .switchToWishListButton { diff --git a/src/widgets/UserInfo/view/userInfoView.module.scss b/src/widgets/UserInfo/view/userInfoView.module.scss index 4d0aeac5..fc443fa5 100644 --- a/src/widgets/UserInfo/view/userInfoView.module.scss +++ b/src/widgets/UserInfo/view/userInfoView.module.scss @@ -1,3 +1,5 @@ +@import 'src/app/styles/mixins'; + .userInfoWrapper { position: relative; display: grid; @@ -30,6 +32,8 @@ } .editPasswordButton { + @include green-btn; + z-index: 2; grid-column: 2; grid-row: 1; @@ -37,7 +41,6 @@ margin-left: auto; border-radius: 50%; padding: calc(var(--tiny-offset) * 1.5); - width: max-content; height: max-content; background-color: var(--noble-white-100); transition: 0.2s; @@ -60,35 +63,14 @@ } .editInfoButton { + @include green-btn; + grid-column: 2 span; grid-row: 6; 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: var(--one); - 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 { From 43da85258fadb988f86b50b425089e70103e9a2c Mon Sep 17 00:00:00 2001 From: Yuliya Kursevich <54816946+YulikK@users.noreply.github.com> Date: Sun, 26 May 2024 15:51:53 +0200 Subject: [PATCH 05/28] feat(RSS-ECOMM-4_98): add level to product (#313) feat: add level to product --- src/shared/API/product/model/ProductModel.ts | 18 ++++++++++++++++-- src/shared/API/types/type.ts | 1 + src/shared/types/product.ts | 8 ++++++++ src/shared/utils/size.ts | 18 +++++++++++++++--- 4 files changed, 40 insertions(+), 5 deletions(-) diff --git a/src/shared/API/product/model/ProductModel.ts b/src/shared/API/product/model/ProductModel.ts index f30c6867..e5474f93 100644 --- a/src/shared/API/product/model/ProductModel.ts +++ b/src/shared/API/product/model/ProductModel.ts @@ -1,4 +1,4 @@ -import type { Category, Product, SizeType, localization } from '@/shared/types/product.ts'; +import type { Category, LevelType, Product, SizeType, localization } from '@/shared/types/product.ts'; import type { Attribute as AttributeResponse, CategoryPagedQueryResponse, @@ -12,7 +12,7 @@ import type { } from '@commercetools/platform-sdk'; import { PRICE_FRACTIONS } from '@/shared/constants/product.ts'; -import getSize from '@/shared/utils/size.ts'; +import { getLevel, getSize } from '@/shared/utils/size.ts'; import { Attribute, @@ -109,6 +109,13 @@ export class ProductModel { return []; } + private adaptLevel(attribute: AttributeResponse): LevelType | null { + if (Array.isArray(attribute.value) && attribute.value.length && isAttributePlainEnumValue(attribute.value[0])) { + return getLevel(attribute.value[0].key); + } + return null; + } + private adaptPrice(variant: ProductVariant): number { let price = 0; @@ -132,6 +139,7 @@ export class ProductModel { id: product.id || '', images: [], key: product.key ?? '', + level: null, name: [], slug: [], variant: [], @@ -154,6 +162,7 @@ export class ProductModel { const variants = [...response.variants, response.masterVariant]; variants.forEach((variant) => { let size: SizeType | null = null; + let level: LevelType | null = null; if (variant.attributes?.length) { variant.attributes.forEach((attribute) => { @@ -163,6 +172,11 @@ export class ProductModel { if (attribute.name === Attribute.SIZE) { size = this.adaptSize(attribute); } + if (attribute.name === Attribute.LEVEL) { + level = this.adaptLevel(attribute); + const productEl = product; + productEl.level = level; + } }); } diff --git a/src/shared/API/types/type.ts b/src/shared/API/types/type.ts index 85c98fef..573fe0c6 100644 --- a/src/shared/API/types/type.ts +++ b/src/shared/API/types/type.ts @@ -2,6 +2,7 @@ import type { Category, Product, SizeType } from '@/shared/types/product.ts'; export const Attribute = { FULL_DESCRIPTION: 'full_description', + LEVEL: 'level', SIZE: 'size', } as const; diff --git a/src/shared/types/product.ts b/src/shared/types/product.ts index f218ef01..1443ab42 100644 --- a/src/shared/types/product.ts +++ b/src/shared/types/product.ts @@ -18,7 +18,14 @@ export const SIZE = { XL: 'XL', } as const; +export const LEVEL = { + 1: '1', + 2: '2', + 3: '3', +} as const; + export type SizeType = (typeof SIZE)[keyof typeof SIZE]; +export type LevelType = (typeof LEVEL)[keyof typeof LEVEL]; export interface Variant { discount: number; @@ -34,6 +41,7 @@ export interface Product { id: string; images: string[]; key: string; + level: LevelType | null; name: localization[]; slug: localization[]; variant: Variant[]; diff --git a/src/shared/utils/size.ts b/src/shared/utils/size.ts index aaa810d0..2d36465b 100644 --- a/src/shared/utils/size.ts +++ b/src/shared/utils/size.ts @@ -1,8 +1,8 @@ -import type { SizeType } from '../types/product.ts'; +import type { LevelType, SizeType } from '../types/product.ts'; -import { SIZE } from '../types/product.ts'; +import { LEVEL, SIZE } from '../types/product.ts'; -export default function getSize(sizeString: string): SizeType | null { +export function getSize(sizeString: string): SizeType | null { const sizeValues = Object.values(SIZE); const foundValue = sizeValues.find((value) => value.toLowerCase() === sizeString.toLowerCase()); @@ -13,3 +13,15 @@ export default function getSize(sizeString: string): SizeType | null { return null; } + +export function getLevel(levelString: string): LevelType | null { + const levelValues = Object.values(LEVEL); + + const foundValue = levelValues.find((value) => value.toLowerCase() === levelString.toLowerCase()); + + if (foundValue) { + return foundValue; + } + + return null; +} From cfd3da8abc5e1e8569fe57eac0090b8dfa25c6d7 Mon Sep 17 00:00:00 2001 From: Yuliya Kursevich <54816946+YulikK@users.noreply.github.com> Date: Sun, 26 May 2024 20:05:25 +0200 Subject: [PATCH 06/28] feat(RSS-ECOMM-4_97): add address actions (#315) feat: add address actions --- src/shared/API/customer/model/CustomerModel.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/shared/API/customer/model/CustomerModel.ts b/src/shared/API/customer/model/CustomerModel.ts index fdd70986..cea8dd91 100644 --- a/src/shared/API/customer/model/CustomerModel.ts +++ b/src/shared/API/customer/model/CustomerModel.ts @@ -30,6 +30,14 @@ export class CustomerModel { return { action: 'addAddress', address: CustomerModel.adaptAddressToServer(address) }; } + public static actionAddBillingAddress(addressId: string): MyCustomerUpdateAction { + return { action: 'addBillingAddressId', addressId }; + } + + public static actionAddShippingAddress(addressId: string): MyCustomerUpdateAction { + return { action: 'addShippingAddressId', addressId }; + } + public static actionEditAddress(address: Address): MyCustomerUpdateAction { return { action: 'changeAddress', address: CustomerModel.adaptAddressToServer(address), addressId: address.id }; } From 5a0e33a30f8d93348d82c02d1b4e37fa4daf62b6 Mon Sep 17 00:00:00 2001 From: Yuliya Kursevich <54816946+YulikK@users.noreply.github.com> Date: Sun, 26 May 2024 20:09:55 +0200 Subject: [PATCH 07/28] feat(RSS-ECOMM-4_96): add total count for products (#316) feat: add total count for products --- src/shared/API/product/model/ProductModel.ts | 10 ++++++++++ src/shared/API/types/type.ts | 1 + 2 files changed, 11 insertions(+) diff --git a/src/shared/API/product/model/ProductModel.ts b/src/shared/API/product/model/ProductModel.ts index e5474f93..91b7b14f 100644 --- a/src/shared/API/product/model/ProductModel.ts +++ b/src/shared/API/product/model/ProductModel.ts @@ -296,6 +296,14 @@ export class ProductModel { return category; } + private getTotalFromData(data: ClientResponse): number { + let total = 0; + if (isClientResponse(data) && isProductProjectionPagedQueryResponse(data.body)) { + total = data.body.total || 0; + } + return total; + } + public adaptLocalizationValue(data: LocalizedString | undefined): localization[] { const result: localization[] = []; Object.entries(data || {}).forEach(([language, value]) => { @@ -341,11 +349,13 @@ export class ProductModel { const sizeCount = this.getSizeProductCountFromData(data); const categoryCount = this.getCategoriesProductCountFromData(data); const priceRange = this.getPriceRangeFromData(data); + const total = this.getTotalFromData(data); const result: ProductWithCount = { categoryCount, priceRange, products, sizeCount, + total, }; return result; } diff --git a/src/shared/API/types/type.ts b/src/shared/API/types/type.ts index 573fe0c6..b6653d00 100644 --- a/src/shared/API/types/type.ts +++ b/src/shared/API/types/type.ts @@ -77,4 +77,5 @@ export type ProductWithCount = { priceRange: PriceRange; products: Product[]; sizeCount: SizeProductCount[]; + total: number; }; From 7cfd1761224fab375325a948925a3d5cbc4bacb0 Mon Sep 17 00:00:00 2001 From: Max <133934232+Kleostro@users.noreply.github.com> Date: Mon, 27 May 2024 19:14:00 +0300 Subject: [PATCH 08/28] feat(RSS-ECOMM-4_03): implement effecting loading products (#318) * feat: separate control search params in router * feat: add pagination component * refactor: remove redundant fields in store --- src/app/App/model/AppModel.ts | 4 +- src/app/Router/model/RouterModel.ts | 35 ++- .../model/ProductModalSliderModel.ts | 9 +- .../view/ProductModalSliderView.ts | 4 - .../view/productModalSliderView.module.scss | 20 ++ .../Pagination/model/PaginationModel.ts | 22 ++ .../Pagination/view/PaginationView.ts | 124 ++++++++++ .../view/paginationView.module.scss | 54 +++++ .../model/ProductFiltersModel.ts | 134 +---------- .../ProductFilters/view/ProductFiltersView.ts | 179 +++++++++++---- .../ProductSearch/model/ProductSearchModel.ts | 20 +- .../ProductSearch/view/ProductSearchView.ts | 7 + .../ProductSorts/model/ProductSortsModel.ts | 40 +--- .../ProductSorts/view/ProductSortsView.ts | 51 +++-- .../CatalogPage/model/CatalogPageModel.ts | 8 +- src/shared/Store/actions.ts | 24 -- src/shared/Store/reducer.ts | 20 -- src/shared/Store/test.spec.ts | 20 -- src/shared/constants/events.ts | 1 + src/shared/constants/initialState.ts | 11 - src/shared/constants/product.ts | 2 + src/shared/types/productFilters.ts | 1 + src/widgets/Catalog/model/CatalogModel.ts | 213 +++++++++++------- .../ProductInfo/model/ProductInfoModel.ts | 8 +- 24 files changed, 603 insertions(+), 408 deletions(-) create mode 100644 src/features/Pagination/model/PaginationModel.ts create mode 100644 src/features/Pagination/view/PaginationView.ts create mode 100644 src/features/Pagination/view/paginationView.module.scss diff --git a/src/app/App/model/AppModel.ts b/src/app/App/model/AppModel.ts index 9ef6b96e..98e1e269 100644 --- a/src/app/App/model/AppModel.ts +++ b/src/app/App/model/AppModel.ts @@ -32,9 +32,9 @@ class AppModel { const { default: CartPageModel } = await import('@/pages/CartPage/model/CartPageModel.ts'); return new CartPageModel(this.appView.getHTML()); }, - [PAGE_ID.CATALOG_PAGE]: async (params: PageParams): Promise => { + [PAGE_ID.CATALOG_PAGE]: async (): Promise => { const { default: CatalogPageModel } = await import('@/pages/CatalogPage/model/CatalogPageModel.ts'); - return new CatalogPageModel(this.appView.getHTML(), params); + return new CatalogPageModel(this.appView.getHTML()); }, [PAGE_ID.DEFAULT_PAGE]: async (): Promise => { const { default: MainPageModel } = await import('@/pages/MainPage/model/MainPageModel.ts'); diff --git a/src/app/Router/model/RouterModel.ts b/src/app/Router/model/RouterModel.ts index dbacb44c..42882c3f 100644 --- a/src/app/Router/model/RouterModel.ts +++ b/src/app/Router/model/RouterModel.ts @@ -9,7 +9,6 @@ const PROJECT_TITLE = import.meta.env.VITE_APP_PROJECT_TITLE; const DEFAULT_SEGMENT = import.meta.env.VITE_APP_DEFAULT_SEGMENT; const NEXT_SEGMENT = import.meta.env.VITE_APP_NEXT_SEGMENT; const PATH_SEGMENTS_TO_KEEP = import.meta.env.VITE_APP_PATH_SEGMENTS_TO_KEEP; -const SEARCH_SEGMENT = import.meta.env.VITE_APP_SEARCH_SEGMENT; class RouterModel { private static router: RouterModel; @@ -46,10 +45,42 @@ class RouterModel { }); } + public static appendSearchParams(key: string, value: string): void { + const url = new URL(decodeURIComponent(window.location.href)); + url.searchParams.append(key, value); + const path = url.pathname + encodeURIComponent(url.search); + window.history.pushState({ path: path.slice(1) }, '', path); + } + + public static clearSearchParams(): void { + const url = new URL(decodeURIComponent(window.location.href)); + const path = `${DEFAULT_SEGMENT}${url.pathname.split(DEFAULT_SEGMENT)[NEXT_SEGMENT]}${DEFAULT_SEGMENT}`; + window.history.pushState({ path: path.slice(1) }, '', path); + } + + public static deleteSearchParams(key: string): void { + const url = new URL(decodeURIComponent(window.location.href)); + url.searchParams.delete(key); + const path = url.pathname + encodeURIComponent(url.search); + window.history.pushState({ path: path.slice(1) }, '', path); + } + public static getInstance(): RouterModel { return RouterModel.router; } + public static getSearchParams(): URLSearchParams { + return new URL(decodeURIComponent(window.location.href)).searchParams; + } + + public static setSearchParams(key: string, value: string): void { + const url = new URL(decodeURIComponent(window.location.href)); + url.searchParams.delete(key); + url.searchParams.set(key, value); + const path = url.pathname + encodeURIComponent(url.search); + window.history.pushState({ path: path.slice(1) }, '', path); + } + private async checkPageAndParams( currentPage: string, path: string, @@ -57,7 +88,6 @@ class RouterModel { const hasRoute = this.routes.has(currentPage); const decodePath = decodeURIComponent(path); const id = decodePath.split(DEFAULT_SEGMENT).slice(PATH_SEGMENTS_TO_KEEP, -NEXT_SEGMENT)[NEXT_SEGMENT]; - const searchParams = decodeURIComponent(decodePath).split(SEARCH_SEGMENT)[NEXT_SEGMENT]; const title = `${PROJECT_TITLE} | ${hasRoute ? formattedText(currentPage === PAGE_ID.DEFAULT_PAGE ? PAGE_ID.MAIN_PAGE.slice(PATH_SEGMENTS_TO_KEEP, -NEXT_SEGMENT) : currentPage.slice(PATH_SEGMENTS_TO_KEEP, -NEXT_SEGMENT)) : PAGE_ID.NOT_FOUND_PAGE.slice(PATH_SEGMENTS_TO_KEEP, -NEXT_SEGMENT)}`; document.title = title; @@ -71,7 +101,6 @@ class RouterModel { params: { [currentPage.slice(PATH_SEGMENTS_TO_KEEP, -NEXT_SEGMENT)]: { id: id ?? null, - searchParams: searchParams ?? null, }, }, }; diff --git a/src/entities/ProductModalSlider/model/ProductModalSliderModel.ts b/src/entities/ProductModalSlider/model/ProductModalSliderModel.ts index 9737f4f1..1629a884 100644 --- a/src/entities/ProductModalSlider/model/ProductModalSliderModel.ts +++ b/src/entities/ProductModalSlider/model/ProductModalSliderModel.ts @@ -13,6 +13,8 @@ const SLIDER_DELAY = 5000; const SLIDER_PER_VIEW = 1; class ProductModalSliderModel { + private modalSlider: Swiper | null = null; + private view: ProductModalSliderView; constructor(params: ProductInfoParams) { @@ -21,7 +23,7 @@ class ProductModalSliderModel { } private init(): void { - const modalSlider = new Swiper(this.view.getModalSlider(), { + this.modalSlider = new Swiper(this.view.getModalSlider(), { autoplay: { delay: SLIDER_DELAY, }, @@ -34,12 +36,15 @@ class ProductModalSliderModel { }, slidesPerView: SLIDER_PER_VIEW, }); - modalSlider.autoplay.start(); } public getHTML(): HTMLDivElement { return this.view.getHTML(); } + + public getModalSlider(): Swiper | null { + return this.modalSlider; + } } export default ProductModalSliderModel; diff --git a/src/entities/ProductModalSlider/view/ProductModalSliderView.ts b/src/entities/ProductModalSlider/view/ProductModalSliderView.ts index a2f1da7b..5260351e 100644 --- a/src/entities/ProductModalSlider/view/ProductModalSliderView.ts +++ b/src/entities/ProductModalSlider/view/ProductModalSliderView.ts @@ -8,7 +8,6 @@ import createBaseElement from '@/shared/utils/createBaseElement.ts'; import styles from './productModalSliderView.module.scss'; -const BIG_SLIDER_WIDTH = 40; const CLOSE_BUTTON_CONTENT = 'x'; class ProductModalSliderView { @@ -65,9 +64,6 @@ class ProductModalSliderView { tag: 'div', }); - const maxWidth = BIG_SLIDER_WIDTH; - slider.style.maxWidth = `${maxWidth}rem`; - slider.append(this.createModalSliderWrapper()); return slider; } diff --git a/src/entities/ProductModalSlider/view/productModalSliderView.module.scss b/src/entities/ProductModalSlider/view/productModalSliderView.module.scss index 5dcfc78e..d0d71c4f 100644 --- a/src/entities/ProductModalSlider/view/productModalSliderView.module.scss +++ b/src/entities/ProductModalSlider/view/productModalSliderView.module.scss @@ -2,23 +2,43 @@ margin: 0; width: 100%; min-height: 40rem; + max-width: 40rem; + + @media (max-width: 768px) { + min-height: 24rem; + max-width: 24rem; + } } .modalSliderWrapper { height: auto; min-height: 40rem; + + @media (max-width: 768px) { + min-height: 24rem; + } } .modalSliderImage { width: 100%; height: 100%; min-height: 40rem; + + @media (max-width: 768px) { + min-height: 24rem; + } } .modalSliderSlide { /* stylelint-disable-next-line declaration-no-important */ width: 40rem !important; min-height: 40rem; + + @media (max-width: 768px) { + /* stylelint-disable-next-line declaration-no-important */ + width: 24rem !important; + min-height: 24rem; + } } .modalCloseButton { diff --git a/src/features/Pagination/model/PaginationModel.ts b/src/features/Pagination/model/PaginationModel.ts new file mode 100644 index 00000000..b4a13931 --- /dev/null +++ b/src/features/Pagination/model/PaginationModel.ts @@ -0,0 +1,22 @@ +import PaginationView from '../view/PaginationView.ts'; + +class PaginationModel { + private view: PaginationView; + + constructor( + productInfo: { productTotalCount: number; productsPerPageCount: number }, + callback: (page: string) => void, + ) { + this.view = new PaginationView(productInfo, callback); + } + + public getHTML(): HTMLDivElement { + return this.view.getHTML(); + } + + public getView(): PaginationView { + return this.view; + } +} + +export default PaginationModel; diff --git a/src/features/Pagination/view/PaginationView.ts b/src/features/Pagination/view/PaginationView.ts new file mode 100644 index 00000000..4fd9b409 --- /dev/null +++ b/src/features/Pagination/view/PaginationView.ts @@ -0,0 +1,124 @@ +import RouterModel from '@/app/Router/model/RouterModel.ts'; +import ButtonModel from '@/shared/Button/model/ButtonModel.ts'; +import { DEFAULT_PAGE, SEARCH_PARAMS_FIELD } from '@/shared/constants/product.ts'; +import createBaseElement from '@/shared/utils/createBaseElement.ts'; + +import styles from './paginationView.module.scss'; + +const PREV_TEXT = '<'; +const NEXT_TEXT = '>'; + +class PaginationView { + private callback: (page: string) => void; + + private nextPageButton: ButtonModel; + + private pageButtons: ButtonModel[] = []; + + private prevPageButton: ButtonModel; + + private productInfo: { productTotalCount: number; productsPerPageCount: number }; + + private view: HTMLDivElement; + + constructor( + productInfo: { productTotalCount: number; productsPerPageCount: number }, + callback: (page: string) => void, + ) { + this.productInfo = productInfo; + this.callback = callback; + this.nextPageButton = this.createNextPageButton(); + this.prevPageButton = this.createPrevPageButton(); + this.view = this.createHTML(); + } + + private createHTML(): HTMLDivElement { + this.view = createBaseElement({ + cssClasses: [styles.pagination], + tag: 'div', + }); + + this.redrawPagination(); + + return this.view; + } + + private createNextPageButton(): ButtonModel { + this.nextPageButton = new ButtonModel({ + classes: [styles.pageButton], + text: NEXT_TEXT, + }); + + this.nextPageButton.getHTML().addEventListener('click', () => { + const currentPage = RouterModel.getSearchParams().get(SEARCH_PARAMS_FIELD.PAGE); + if (currentPage) { + this.callback(String(Number(currentPage) + DEFAULT_PAGE)); + } else { + this.callback(String(DEFAULT_PAGE + DEFAULT_PAGE)); + } + }); + + return this.nextPageButton; + } + + private createPageButton(page: number): HTMLButtonElement { + const btn = new ButtonModel({ + classes: [styles.pageButton], + text: page.toString(), + }); + + btn.getHTML().addEventListener('click', () => this.callback(page.toString())); + + this.pageButtons.push(btn); + + return btn.getHTML(); + } + + private createPrevPageButton(): ButtonModel { + this.prevPageButton = new ButtonModel({ + classes: [styles.pageButton], + text: PREV_TEXT, + }); + + this.prevPageButton.getHTML().addEventListener('click', () => { + const currentPage = RouterModel.getSearchParams().get(SEARCH_PARAMS_FIELD.PAGE); + if (currentPage) { + this.callback(String(Number(currentPage) - DEFAULT_PAGE)); + } + }); + return this.prevPageButton; + } + + private redrawPagination(): void { + if (this.productInfo.productTotalCount > this.productInfo.productsPerPageCount) { + const pagesCount = Math.ceil(this.productInfo.productTotalCount / this.productInfo.productsPerPageCount); + const pages = Array(pagesCount) + .fill(0) + .map((_, index) => index + DEFAULT_PAGE) + .map(this.createPageButton.bind(this)); + this.view.append(this.prevPageButton.getHTML(), ...pages, this.nextPageButton.getHTML()); + } + } + + private switchStateNavigationButtons(page: number): void { + const prevButton = this.prevPageButton.getHTML(); + const nextButton = this.nextPageButton.getHTML(); + + prevButton.disabled = page === DEFAULT_PAGE; + nextButton.disabled = page === this.pageButtons.length; + } + + public getHTML(): HTMLDivElement { + return this.view; + } + + public setSelectedButton(page: number): void { + this.pageButtons.forEach((btn) => { + btn.getHTML().classList.remove(styles.active); + }); + this.pageButtons[page - DEFAULT_PAGE]?.getHTML().classList.add(styles.active); + this.switchStateNavigationButtons(page); + } +} + +export default PaginationView; diff --git a/src/features/Pagination/view/paginationView.module.scss b/src/features/Pagination/view/paginationView.module.scss new file mode 100644 index 00000000..ff1c12ca --- /dev/null +++ b/src/features/Pagination/view/paginationView.module.scss @@ -0,0 +1,54 @@ +.pagination { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: var(--tiny-offset); +} + +.pageButton { + display: flex; + align-items: center; + justify-content: center; + margin: var(--tiny-offset) 0; + outline: var(--two) solid var(--noble-gray-300); + border-radius: var(--small-br); + width: var(--extra-small-offset); + height: var(--extra-small-offset); + font: var(--regular-font); + color: var(--noble-gray-300); + background-color: var(--noble-gray-1000); + transition: + color 0.2s, + outline 0.2s, + transform 0.2s, + opacity 0.2s; + user-select: none; + + @media (hover: hover) { + &:hover { + outline: var(--two) solid var(--steam-green-400); + color: var(--steam-green-400); + } + } + + /* stylelint-disable-next-line order/order */ + &:active { + transform: scale(0.9); + } + + &:disabled { + opacity: 0.5; + pointer-events: none; + } + + &:nth-child(1) { + margin-left: var(--tiny-offset); + } +} + +.active { + outline: var(--two) solid var(--steam-green-400); + color: var(--steam-green-400); + background-color: var(--noble-white-100); + pointer-events: none; +} diff --git a/src/features/ProductFilters/model/ProductFiltersModel.ts b/src/features/ProductFilters/model/ProductFiltersModel.ts index 1a943905..fb6019f4 100644 --- a/src/features/ProductFilters/model/ProductFiltersModel.ts +++ b/src/features/ProductFilters/model/ProductFiltersModel.ts @@ -1,142 +1,16 @@ -import type LinkModel from '@/shared/Link/model/LinkModel.ts'; import type ProductFiltersParams from '@/shared/types/productFilters.ts'; -import type { SelectedFilters } from '@/shared/types/productFilters.ts'; - -import RouterModel from '@/app/Router/model/RouterModel.ts'; -import getStore from '@/shared/Store/Store.ts'; -import { setSelectedFilters } from '@/shared/Store/actions.ts'; -import observeStore, { selectCurrentLanguage } from '@/shared/Store/observer.ts'; -import { META_FILTERS } from '@/shared/constants/filters.ts'; -import { SEARCH_PARAMS_FIELD } from '@/shared/constants/product.ts'; import ProductFiltersView from '../view/ProductFiltersView.ts'; -const DEFAULT_SEGMENT = import.meta.env.VITE_APP_DEFAULT_SEGMENT; -const NEXT_SEGMENT = import.meta.env.VITE_APP_NEXT_SEGMENT; - class ProductFiltersModel { - private selectedFilters: SelectedFilters = { - category: new Set(), - metaFilter: META_FILTERS.en.ALL_PRODUCTS, - price: { - max: 0, - min: 0, - }, - size: null, - }; - private view: ProductFiltersView; - constructor(params: ProductFiltersParams | null) { - this.view = new ProductFiltersView(params); - this.init(); - } - - private init(): void { - this.selectedFilters = getStore().getState().selectedFilters ?? this.selectedFilters; - this.initCategoryFilters(); - this.initPriceFilters(); - this.initSizeFilters(); - this.setMetaLinksHandlers(); - this.setResetFiltersButtonHandler(); - - observeStore(selectCurrentLanguage, () => { - this.selectedFilters = getStore().getState().selectedFilters ?? this.selectedFilters; - }); - } - - private initCategoryFilters(): void { - this.setCategoryLinksHandlers(); - const categoryLinks = this.view.getCategoryLinks(); - this.selectedFilters.category.forEach((categoryID) => { - const currentLink = categoryLinks.find((link) => link.getHTML().id === categoryID); - if (currentLink) { - this.view.switchSelectedFilter(currentLink, true); - } - }); - } - - private initPriceFilters(): void { - this.view.getPriceSlider().on('set', (values) => { - const [min, max] = values; - this.selectedFilters.price = { - max: +max, - min: +min, - }; - const url = new URL(decodeURIComponent(window.location.href)); - url.searchParams.delete(SEARCH_PARAMS_FIELD.MIN_PRICE); - url.searchParams.set(SEARCH_PARAMS_FIELD.MIN_PRICE, String(this.selectedFilters.price.min)); - url.searchParams.delete(SEARCH_PARAMS_FIELD.MAX_PRICE); - url.searchParams.set(SEARCH_PARAMS_FIELD.MAX_PRICE, String(this.selectedFilters.price.max)); - const path = url.pathname + encodeURIComponent(url.search); - RouterModel.getInstance().navigateTo(path.slice(1)); - }); - } - - private initSizeFilters(): void { - this.setSizeLinksHandlers(); - const sizeLinks = this.view.getSizeLinks(); - const currentLink = sizeLinks.find((link) => link.getHTML().id === this.selectedFilters.size); - if (currentLink) { - this.view.switchSelectedFilter(currentLink, true); - } - } - - private setCategoryLinksHandlers(): void { - this.view.getCategoryLinks().forEach((categoryLink) => { - categoryLink.getHTML().addEventListener('click', () => { - this.view.switchSelectedFilter(categoryLink); - }); - }); - } - - private setMetaLinksHandlers(): void { - const activeMetaLink = this.view - .getMetaLinks() - .find((link) => link.getHTML().id === this.selectedFilters.metaFilter); - if (activeMetaLink) { - this.switchLinkState(activeMetaLink); - } - this.view.getMetaLinks().forEach((metaLink) => { - metaLink.getHTML().addEventListener('click', () => { - this.switchLinkState(metaLink); - this.selectedFilters.metaFilter = metaLink.getHTML().id; - getStore().dispatch(setSelectedFilters(this.selectedFilters)); - }); - }); - } - - private setResetFiltersButtonHandler(): void { - const filtersResetButton = this.view.getFiltersResetButton(); - filtersResetButton.getHTML().addEventListener('click', () => { - const url = new URL(decodeURIComponent(window.location.href)); - const path = `${DEFAULT_SEGMENT}${url.pathname.split(DEFAULT_SEGMENT)[NEXT_SEGMENT]}${DEFAULT_SEGMENT}`; - RouterModel.getInstance().navigateTo(path.slice(1)); - }); - } - - private setSizeLinksHandlers(): void { - this.view.getSizeLinks().forEach((sizeLink) => { - sizeLink.getHTML().addEventListener('click', () => { - this.view.getSizeLinks().forEach((link) => this.view.switchSelectedFilter(link, false)); - this.view.switchSelectedFilter(sizeLink, true); - }); - }); - } - - private switchLinkState(metaLink: LinkModel): void { - this.view.getMetaLinks().forEach((link) => { - this.view.switchSelectedFilter(link, false); - }); - this.view.switchSelectedFilter(metaLink, true); - } - - public getDefaultFilters(): HTMLDivElement { - return this.view.getDefaultFilters(); + constructor(params: ProductFiltersParams | null, callback: () => void) { + this.view = new ProductFiltersView(params, callback); } - public getMetaFilters(): HTMLDivElement { - return this.view.getMetaFilters(); + public getView(): ProductFiltersView { + return this.view; } public updateParams(params: ProductFiltersParams | null): void { diff --git a/src/features/ProductFilters/view/ProductFiltersView.ts b/src/features/ProductFilters/view/ProductFiltersView.ts index 3fbaba91..68b4e197 100644 --- a/src/features/ProductFilters/view/ProductFiltersView.ts +++ b/src/features/ProductFilters/view/ProductFiltersView.ts @@ -4,12 +4,14 @@ import type ProductFiltersParams from '@/shared/types/productFilters'; import RouterModel from '@/app/Router/model/RouterModel.ts'; import ButtonModel from '@/shared/Button/model/ButtonModel.ts'; +import EventMediatorModel from '@/shared/EventMediator/model/EventMediatorModel.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 observeStore, { selectCurrentLanguage } from '@/shared/Store/observer.ts'; import { BUTTON_TEXT } from '@/shared/constants/buttons.ts'; import { AUTOCOMPLETE_OPTION, LANGUAGE_CHOICE } from '@/shared/constants/common.ts'; +import MEDIATOR_EVENT from '@/shared/constants/events.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 { SEARCH_PARAMS_FIELD } from '@/shared/constants/product.ts'; @@ -22,6 +24,8 @@ const BASE_PRODUCT_COUNT = '(0)'; const INPUT_PRICE_STEP = 5; class ProductFiltersView { + private callback: () => void; + private categoryCountSpan: HTMLSpanElement[] = []; private categoryLinks: LinkModel[] = []; @@ -48,8 +52,9 @@ class ProductFiltersView { private sizesList: HTMLUListElement; - constructor(params: ProductFiltersParams | null) { + constructor(params: ProductFiltersParams | null, callback: () => void) { this.params = params; + this.callback = callback; this.categoryList = this.createCategoryList(); this.priceSlider = this.createPriceSlider(); this.sizesList = this.createSizesList(); @@ -60,14 +65,19 @@ class ProductFiltersView { } private categoryClickHandler(parentCategory: { category: Category; count: number } | null): void { - const url = new URL(decodeURIComponent(window.location.href)); - if (url.searchParams.has(SEARCH_PARAMS_FIELD.CATEGORY)) { - url.searchParams.delete(SEARCH_PARAMS_FIELD.CATEGORY); + const searchParams = RouterModel.getSearchParams(); + if ( + searchParams.has(SEARCH_PARAMS_FIELD.CATEGORY) && + searchParams.get(SEARCH_PARAMS_FIELD.CATEGORY) === parentCategory?.category.parent?.id + ) { + RouterModel.deleteSearchParams(SEARCH_PARAMS_FIELD.CATEGORY); } else { - url.searchParams.set(SEARCH_PARAMS_FIELD.CATEGORY, parentCategory?.category.parent?.id ?? ''); + RouterModel.setSearchParams(SEARCH_PARAMS_FIELD.CATEGORY, parentCategory?.category.parent?.id ?? ''); } - const path = url.pathname + url.search; - RouterModel.getInstance().navigateTo(path.slice(1)); + + RouterModel.deleteSearchParams(SEARCH_PARAMS_FIELD.PAGE); + + this.callback(); } private createCategoryItems(subcategories: Map): void { @@ -115,6 +125,7 @@ class ProductFiltersView { categoryLink.getHTML().addEventListener('click', (event) => { event.preventDefault(); + this.switchSelectedFilter(categoryLink); }); const innerContent = category.count ? `(${category.count})` : ''; @@ -252,11 +263,11 @@ class ProductFiltersView { link.getHTML().addEventListener('click', (event) => { event.preventDefault(); - const url = new URL(decodeURIComponent(window.location.href)); - url.searchParams.delete(SEARCH_PARAMS_FIELD.META); - url.searchParams.set(SEARCH_PARAMS_FIELD.META, id); - const path = url.pathname + encodeURIComponent(url.search); - RouterModel.getInstance().navigateTo(path.slice(1)); + RouterModel.setSearchParams(SEARCH_PARAMS_FIELD.META, id); + RouterModel.deleteSearchParams(SEARCH_PARAMS_FIELD.PAGE); + this.metaLinks.forEach((link) => this.switchSelectedFilter(link, false)); + this.switchSelectedFilter(link, true); + this.callback(); }); this.metaLinks.push(link); @@ -282,8 +293,8 @@ class ProductFiltersView { tag: 'span', }); - const minPrice = getStore().getState().selectedFilters?.price?.min.toFixed(2) ?? ''; - const maxPrice = getStore().getState().selectedFilters?.price?.max.toFixed(2) ?? ''; + const minPrice = this.params?.priceRange?.min.toFixed(2) ?? ''; + const maxPrice = this.params?.priceRange?.max.toFixed(2) ?? ''; const from = PRICE_RANGE_LABEL[getStore().getState().currentLanguage].FROM; const to = PRICE_RANGE_LABEL[getStore().getState().currentLanguage].TO; @@ -304,9 +315,8 @@ class ProductFiltersView { } private createPriceSlider(): noUiSlider.API { - const { max, min } = getStore().getState().selectedFilters?.price ?? { max: 0, min: 0 }; - const SLIDER_START_MIN = min; - const SLIDER_START_MAX = max; + const min = this.params?.priceRange?.min ?? 0; + const max = this.params?.priceRange?.max ?? 0; const slider = createBaseElement({ cssClasses: [styles.slider], tag: 'div', @@ -317,7 +327,24 @@ class ProductFiltersView { connect: true, keyboardSupport: true, range: this.params?.priceRange ?? { max: 0, min: 0 }, - start: [SLIDER_START_MIN, SLIDER_START_MAX], + start: [min, max], + }); + + this.priceSlider.on('change', (values) => { + const [min, max] = values; + this.priceInputs.get(PRICE_RANGE_LABEL[getStore().getState().currentLanguage].FROM)?.setValue(String(min)); + this.priceInputs.get(PRICE_RANGE_LABEL[getStore().getState().currentLanguage].TO)?.setValue(String(max)); + RouterModel.deleteSearchParams(SEARCH_PARAMS_FIELD.MIN_PRICE); + RouterModel.deleteSearchParams(SEARCH_PARAMS_FIELD.MAX_PRICE); + RouterModel.setSearchParams(SEARCH_PARAMS_FIELD.MIN_PRICE, String(min)); + RouterModel.setSearchParams(SEARCH_PARAMS_FIELD.MAX_PRICE, String(max)); + this.callback(); + }); + + this.priceSlider.on('slide', (values) => { + const [min, max] = values; + this.priceInputs.get(PRICE_RANGE_LABEL[getStore().getState().currentLanguage].FROM)?.setValue(String(min)); + this.priceInputs.get(PRICE_RANGE_LABEL[getStore().getState().currentLanguage].TO)?.setValue(String(max)); }); return this.priceSlider; @@ -356,6 +383,21 @@ class ProductFiltersView { text: BUTTON_TEXT[getStore().getState().currentLanguage].RESET, }); + this.resetFiltersButton.getHTML().addEventListener('click', () => { + this.sizeLinks.forEach((link) => this.switchSelectedFilter(link, false)); + this.categoryLinks.forEach((link) => this.switchSelectedFilter(link, false)); + this.metaLinks.forEach((link) => { + this.switchSelectedFilter(link, false); + if (link.getHTML().id === META_FILTERS.en.ALL_PRODUCTS) { + this.switchSelectedFilter(link, true); + } + }); + + RouterModel.clearSearchParams(); + EventMediatorModel.getInstance().notify(MEDIATOR_EVENT.CLEAR_CATALOG_SEARCH, ''); + this.callback(); + }); + return this.resetFiltersButton; } @@ -371,11 +413,11 @@ class ProductFiltersView { sizeLink.getHTML().addEventListener('click', (event) => { event.preventDefault(); - const url = new URL(decodeURIComponent(window.location.href)); - url.searchParams.delete(SEARCH_PARAMS_FIELD.SIZE); - url.searchParams.set(SEARCH_PARAMS_FIELD.SIZE, size.size); - const path = url.pathname + encodeURIComponent(url.search); - RouterModel.getInstance().navigateTo(path.slice(1)); + RouterModel.deleteSearchParams(SEARCH_PARAMS_FIELD.PAGE); + RouterModel.setSearchParams(SEARCH_PARAMS_FIELD.SIZE, size.size); + this.sizeLinks.forEach((link) => this.switchSelectedFilter(link, false)); + this.switchSelectedFilter(sizeLink, true); + this.callback(); }); const span = createBaseElement({ @@ -478,38 +520,46 @@ class ProductFiltersView { const fromInput = this.priceInputs.get(PRICE_RANGE_LABEL[getStore().getState().currentLanguage].FROM); const toInput = this.priceInputs.get(PRICE_RANGE_LABEL[getStore().getState().currentLanguage].TO); - this.priceSlider.on('update', (values) => { - const [min, max] = values; - fromInput?.setValue(String(min)); - toInput?.setValue(String(max)); - }); - - fromInput?.getHTML().addEventListener('change', () => { - this.priceSlider.set([fromInput.getValue(), toInput?.getValue() ?? 0]); - }); - toInput?.getHTML().addEventListener('change', () => { - this.priceSlider.set([fromInput?.getValue() ?? 0, toInput.getValue()]); - }); + fromInput?.getHTML().addEventListener('change', () => this.updateSelectedPrice(fromInput, toInput)); + toInput?.getHTML().addEventListener('change', () => this.updateSelectedPrice(fromInput, toInput)); } private subcategoryClickHandler(subcategory: { category: Category; count: number }): void { - const url = new URL(decodeURIComponent(window.location.href)); - const currentSubcategories = url.searchParams.getAll(SEARCH_PARAMS_FIELD.SUBCATEGORY); - const currentSubcategory = currentSubcategories.find((id) => id === subcategory.category.id); + const currentSubcategories = RouterModel.getSearchParams().getAll(SEARCH_PARAMS_FIELD.SUBCATEGORY); + const currentSubcategory = currentSubcategories?.find((id) => id === subcategory.category.id); + const currentLink = this.categoryLinks.find((link) => link.getHTML().id === subcategory.category.id); + RouterModel.deleteSearchParams(SEARCH_PARAMS_FIELD.PAGE); if (currentSubcategory) { const filteredSubcategories = currentSubcategories.filter((id) => id !== currentSubcategory); if (!filteredSubcategories.length) { - url.searchParams.delete(SEARCH_PARAMS_FIELD.SUBCATEGORY); + RouterModel.deleteSearchParams(SEARCH_PARAMS_FIELD.SUBCATEGORY); + if (currentLink) { + this.switchSelectedFilter(currentLink, false); + } } else { - url.searchParams.delete(SEARCH_PARAMS_FIELD.SUBCATEGORY); - filteredSubcategories.forEach((id) => url.searchParams.append(SEARCH_PARAMS_FIELD.SUBCATEGORY, id)); + RouterModel.deleteSearchParams(SEARCH_PARAMS_FIELD.SUBCATEGORY); + filteredSubcategories.forEach((id) => RouterModel.appendSearchParams(SEARCH_PARAMS_FIELD.SUBCATEGORY, id)); + filteredSubcategories.forEach((id) => { + const currentLink = this.categoryLinks.find((link) => link.getHTML().id === id); + if (currentLink) { + this.switchSelectedFilter(currentLink, true); + } + }); } } else { - url.searchParams.append(SEARCH_PARAMS_FIELD.SUBCATEGORY, subcategory.category.id); + RouterModel.appendSearchParams(SEARCH_PARAMS_FIELD.SUBCATEGORY, subcategory.category.id); + if (currentLink) { + this.switchSelectedFilter(currentLink, true); + } } - const path = url.pathname + encodeURIComponent(url.search); - RouterModel.getInstance().navigateTo(path.slice(1)); + this.callback(); + } + + private updateSelectedPrice(from: InputModel | null = null, to: InputModel | null = null): void { + RouterModel.setSearchParams(SEARCH_PARAMS_FIELD.MIN_PRICE, from?.getValue() ?? '0'); + RouterModel.setSearchParams(SEARCH_PARAMS_FIELD.MAX_PRICE, to?.getValue() ?? '0'); + this.callback(); } public getCategoryLinks(): LinkModel[] { @@ -544,12 +594,53 @@ class ProductFiltersView { return this.sizeLinks; } + public setInitialActiveFilters(activeFilters: { + categoryLinks: string[]; + metaLinks: string[]; + sizeLinks: string[]; + }): void { + this.sizeLinks.forEach((link) => this.switchSelectedFilter(link, false)); + this.categoryLinks.forEach((link) => this.switchSelectedFilter(link, false)); + this.metaLinks.forEach((link) => this.switchSelectedFilter(link, false)); + + activeFilters.categoryLinks.forEach((id) => { + const currentLink = this.categoryLinks.find((link) => link.getHTML().id === id); + if (currentLink) { + this.switchSelectedFilter(currentLink, true); + } + }); + activeFilters.sizeLinks.forEach((id) => { + const currentLink = this.sizeLinks.find((link) => link.getHTML().id === id); + if (currentLink) { + this.switchSelectedFilter(currentLink, true); + } + }); + activeFilters.metaLinks.forEach((id) => { + const currentLink = this.metaLinks.find((link) => link.getHTML().id === id); + if (currentLink) { + this.switchSelectedFilter(currentLink, true); + } + }); + } + public switchSelectedFilter(filterLink: LinkModel, toggle?: boolean): void { filterLink.getHTML().classList.toggle(styles.activeLink, toggle); } public updateParams(params: ProductFiltersParams | null): void { this.params = params; + this.priceSlider.updateOptions( + { + range: { max: params?.priceRange?.max ?? 0, min: params?.priceRange?.min ?? 0 }, + start: [params?.priceRange?.min ?? 0, params?.priceRange?.max ?? 0], + }, + true, + ); + const fromInput = this.priceInputs.get(PRICE_RANGE_LABEL[getStore().getState().currentLanguage].FROM); + const toInput = this.priceInputs.get(PRICE_RANGE_LABEL[getStore().getState().currentLanguage].TO); + fromInput?.setValue(this.params?.priceRange?.min.toFixed(2) ?? ''); + toInput?.setValue(this.params?.priceRange?.max.toFixed(2) ?? ''); + this.categoryCountSpan.forEach((span) => { const currentSpan = span; currentSpan.innerText = BASE_PRODUCT_COUNT; diff --git a/src/features/ProductSearch/model/ProductSearchModel.ts b/src/features/ProductSearch/model/ProductSearchModel.ts index 948ef3b8..faae9a5f 100644 --- a/src/features/ProductSearch/model/ProductSearchModel.ts +++ b/src/features/ProductSearch/model/ProductSearchModel.ts @@ -1,29 +1,33 @@ +import RouterModel from '@/app/Router/model/RouterModel.ts'; import EventMediatorModel from '@/shared/EventMediator/model/EventMediatorModel.ts'; -import getStore from '@/shared/Store/Store.ts'; -import { setSearchValue } from '@/shared/Store/actions.ts'; import MEDIATOR_EVENT from '@/shared/constants/events.ts'; +import { SEARCH_PARAMS_FIELD } from '@/shared/constants/product.ts'; import debounce from '@/shared/utils/debounce.ts'; import ProductSearchView from '../view/ProductSearchView.ts'; -const SEARCH_DELAY = 800; +const SEARCH_DELAY = 300; class ProductSearchModel { - private eventMediator = EventMediatorModel.getInstance(); + private callback: () => void; private view = new ProductSearchView(); - constructor() { + constructor(callback: () => void) { + this.callback = callback; this.init(); } private handleSearchInput(): void { - getStore().dispatch(setSearchValue(this.view.getSearchField().getValue())); - this.eventMediator.notify(MEDIATOR_EVENT.REDRAW_PRODUCTS, this.view.getSearchField().getValue()); + RouterModel.deleteSearchParams(SEARCH_PARAMS_FIELD.PAGE); + RouterModel.setSearchParams(SEARCH_PARAMS_FIELD.SEARCH, this.view.getSearchField().getValue()); + this.callback(); } private init(): void { - getStore().dispatch(setSearchValue('')); + EventMediatorModel.getInstance().subscribe(MEDIATOR_EVENT.CLEAR_CATALOG_SEARCH, () => + this.view.getSearchField().setValue(''), + ); this.setSearchFieldHandler(); } diff --git a/src/features/ProductSearch/view/ProductSearchView.ts b/src/features/ProductSearch/view/ProductSearchView.ts index 612d5a69..d6b2de53 100644 --- a/src/features/ProductSearch/view/ProductSearchView.ts +++ b/src/features/ProductSearch/view/ProductSearchView.ts @@ -1,8 +1,10 @@ +import RouterModel from '@/app/Router/model/RouterModel.ts'; import InputModel from '@/shared/Input/model/InputModel.ts'; import getStore from '@/shared/Store/Store.ts'; import observeStore, { selectCurrentLanguage } from '@/shared/Store/observer.ts'; import { AUTOCOMPLETE_OPTION } from '@/shared/constants/common.ts'; import { INPUT_TYPE } from '@/shared/constants/forms.ts'; +import { SEARCH_PARAMS_FIELD } from '@/shared/constants/product.ts'; import { TEXT } from '@/shared/constants/sorting.ts'; import createBaseElement from '@/shared/utils/createBaseElement.ts'; @@ -35,6 +37,11 @@ class ProductSearchView { type: INPUT_TYPE.SEARCH, }); + const initialValue = RouterModel.getSearchParams().get(SEARCH_PARAMS_FIELD.SEARCH); + if (initialValue) { + this.searchField.setValue(initialValue); + } + observeStore(selectCurrentLanguage, () => { this.searchField.getHTML().placeholder = TEXT[getStore().getState().currentLanguage].SEARCH; }); diff --git a/src/features/ProductSorts/model/ProductSortsModel.ts b/src/features/ProductSorts/model/ProductSortsModel.ts index 76efb917..b0b6f5d7 100644 --- a/src/features/ProductSorts/model/ProductSortsModel.ts +++ b/src/features/ProductSorts/model/ProductSortsModel.ts @@ -1,44 +1,10 @@ -import type { SelectedSorting } from '@/shared/types/productSorting.ts'; - -import RouterModel from '@/app/Router/model/RouterModel.ts'; -import getStore from '@/shared/Store/Store.ts'; -import { DATA_KEYS } from '@/shared/constants/common.ts'; -import { SEARCH_PARAMS_FIELD } from '@/shared/constants/product.ts'; - import ProductSortsView from '../view/ProductSortsView.ts'; class ProductSortsModel { - private selectedSorting: SelectedSorting = { - direction: '', - field: '', - }; - - private view = new ProductSortsView(); - - constructor() { - this.init(); - } - - private init(): void { - this.selectedSorting = getStore().getState().selectedSorting ?? this.selectedSorting; - this.setSortingLinksHandlers(); - } + private view: ProductSortsView; - private setSortingLinksHandlers(): void { - const sortingLinks = this.view.getSortingLinks(); - sortingLinks.forEach((link) => { - link.getHTML().addEventListener('click', () => { - this.selectedSorting.field = link.getHTML().id; - this.selectedSorting.direction = String(link.getHTML().getAttribute(DATA_KEYS.DIRECTION)); - const url = new URL(decodeURIComponent(window.location.href)); - url.searchParams.delete(SEARCH_PARAMS_FIELD.FIELD); - url.searchParams.set(SEARCH_PARAMS_FIELD.FIELD, this.selectedSorting.field); - url.searchParams.delete(SEARCH_PARAMS_FIELD.DIRECTION); - url.searchParams.set(SEARCH_PARAMS_FIELD.DIRECTION, this.selectedSorting.direction); - const path = url.pathname + encodeURIComponent(url.search); - RouterModel.getInstance().navigateTo(path.slice(1)); - }); - }); + constructor(callback: () => void) { + this.view = new ProductSortsView(callback); } public getHTML(): HTMLDivElement { diff --git a/src/features/ProductSorts/view/ProductSortsView.ts b/src/features/ProductSorts/view/ProductSortsView.ts index 9601c1f0..1bd56190 100644 --- a/src/features/ProductSorts/view/ProductSortsView.ts +++ b/src/features/ProductSorts/view/ProductSortsView.ts @@ -1,8 +1,10 @@ +import RouterModel from '@/app/Router/model/RouterModel.ts'; import { SortDirection } from '@/shared/API/types/type.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 { DATA_KEYS } from '@/shared/constants/common.ts'; +import { SEARCH_PARAMS_FIELD } from '@/shared/constants/product.ts'; import { SORTING_ID, TEXT } from '@/shared/constants/sorting.ts'; import createBaseElement from '@/shared/utils/createBaseElement.ts'; import { isKeyOfSortField } from '@/shared/utils/isKeyOf.ts'; @@ -10,6 +12,8 @@ import { isKeyOfSortField } from '@/shared/utils/isKeyOf.ts'; import styles from './productSortsView.module.scss'; class ProductSortsView { + private callback: () => void; + private currentSortingSpan: HTMLSpanElement; private sortingList: HTMLUListElement; @@ -20,7 +24,8 @@ class ProductSortsView { private sortingWrapper: HTMLDivElement; - constructor() { + constructor(callback: () => void) { + this.callback = callback; this.currentSortingSpan = this.createCurrentSortingSpan(); this.sortingListTitle = this.createSortingListTitle(); this.sortingList = this.createSortingList(); @@ -40,21 +45,27 @@ class ProductSortsView { } private createCurrentSortingSpan(): HTMLSpanElement { - const { currentLanguage, selectedSorting } = getStore().getState(); - const upperText = selectedSorting?.field.toUpperCase() ?? TEXT[currentLanguage].DEFAULT; + const selectedSorting = RouterModel.getSearchParams().get(SEARCH_PARAMS_FIELD.FIELD); + + const upperText = selectedSorting + ? selectedSorting.toUpperCase() + : TEXT[getStore().getState().currentLanguage].DEFAULT.toUpperCase(); if (isKeyOfSortField(upperText)) { this.currentSortingSpan = createBaseElement({ cssClasses: [styles.currentSortingSpan], - innerContent: TEXT[currentLanguage][upperText], + innerContent: TEXT[getStore().getState().currentLanguage][upperText], tag: 'span', }); } observeStore(selectCurrentLanguage, () => { - const { currentLanguage, selectedSorting } = getStore().getState(); - const upperText = selectedSorting?.field.toUpperCase() ?? TEXT[currentLanguage].DEFAULT; + const selectedSorting = RouterModel.getSearchParams().get(SEARCH_PARAMS_FIELD.FIELD); + const upperText = selectedSorting + ? selectedSorting.toUpperCase() + : TEXT[getStore().getState().currentLanguage].DEFAULT.toUpperCase(); + if (isKeyOfSortField(upperText)) { - this.currentSortingSpan.innerText = TEXT[currentLanguage][upperText]; + this.currentSortingSpan.innerText = TEXT[getStore().getState().currentLanguage][upperText]; } }); @@ -93,7 +104,15 @@ class ProductSortsView { } this.sortingListLinks.forEach((link) => link.getHTML().classList.remove(styles.activeLink)); link.getHTML().classList.add(styles.activeLink); + this.currentSortingSpan.innerText = text; + + RouterModel.setSearchParams(SEARCH_PARAMS_FIELD.FIELD, link.getHTML().id); + RouterModel.setSearchParams( + SEARCH_PARAMS_FIELD.DIRECTION, + String(link.getHTML().getAttribute(DATA_KEYS.DIRECTION)), + ); + this.callback(); }); this.sortingListLinks.push(link); @@ -116,19 +135,15 @@ class ProductSortsView { const priceLink = this.createSortingLink('', TEXT[getStore().getState().currentLanguage].PRICE, SORTING_ID.PRICE); const nameLink = this.createSortingLink('', TEXT[getStore().getState().currentLanguage].NAME, SORTING_ID.NAME); - const currentLink = this.sortingListLinks.find( - (link) => link.getHTML().id === getStore().getState().selectedSorting?.field, - ); + const initialField = RouterModel.getSearchParams().get(SEARCH_PARAMS_FIELD.FIELD); + const initialDirection = RouterModel.getSearchParams().get(SEARCH_PARAMS_FIELD.DIRECTION); + const currentLink = this.sortingListLinks.find((link) => link.getHTML().id === initialField); currentLink?.getHTML().classList.add(styles.activeLink); - currentLink - ?.getHTML() - .classList.toggle(styles.pass, getStore().getState().selectedSorting?.direction === SortDirection.DESC); - currentLink - ?.getHTML() - .classList.toggle(styles.hight, getStore().getState().selectedSorting?.direction === SortDirection.DESC); - if (currentLink) { - currentLink.getHTML().dataset.direction = getStore().getState().selectedSorting?.direction; + if (currentLink && initialField) { + currentLink?.getHTML().classList.toggle(styles.pass, initialDirection === SortDirection.DESC); + currentLink?.getHTML().classList.toggle(styles.hight, initialDirection === SortDirection.DESC); + currentLink.getHTML().dataset.field = initialField; } observeStore(selectCurrentLanguage, () => { diff --git a/src/pages/CatalogPage/model/CatalogPageModel.ts b/src/pages/CatalogPage/model/CatalogPageModel.ts index a30f750a..e1c0dad7 100644 --- a/src/pages/CatalogPage/model/CatalogPageModel.ts +++ b/src/pages/CatalogPage/model/CatalogPageModel.ts @@ -1,4 +1,4 @@ -import type { Page, PageParams } from '@/shared/types/page.ts'; +import type { Page } from '@/shared/types/page.ts'; import getStore from '@/shared/Store/Store.ts'; import { setCurrentPage } from '@/shared/Store/actions.ts'; @@ -8,13 +8,13 @@ import CatalogModel from '@/widgets/Catalog/model/CatalogModel.ts'; import CatalogPageView from '../view/CatalogPageView.ts'; class CatalogPageModel implements Page { - private catalog: CatalogModel; + private catalog: CatalogModel = new CatalogModel(); private view: CatalogPageView; - constructor(parent: HTMLDivElement, params: PageParams) { + constructor(parent: HTMLDivElement) { this.view = new CatalogPageView(parent); - this.catalog = new CatalogModel(params.catalog?.searchParams || ''); + this.init(); } diff --git a/src/shared/Store/actions.ts b/src/shared/Store/actions.ts index ba48fbcc..ad93ae92 100644 --- a/src/shared/Store/actions.ts +++ b/src/shared/Store/actions.ts @@ -2,8 +2,6 @@ import type { TokenStore } from '@commercetools/sdk-client-v2'; import type { LanguageChoiceType } from '../constants/common.ts'; import type { PageIdType } from '../constants/pages.ts'; -import type { SelectedFilters } from '../types/productFilters'; -import type { SelectedSorting } from '../types/productSorting.ts'; const ACTION = { SET_ANONYM_TOKEN: 'setAnonymToken', @@ -13,9 +11,6 @@ const ACTION = { SET_BILLING_COUNTRY: 'setBillingCountry', SET_CURRENT_LANGUAGE: 'setCurrentLanguage', SET_CURRENT_PAGE: 'setCurrentPage', - SET_SEARCH_VALUE: 'setSearchValue', - SET_SELECTED_FILTERS: 'setSelectedFilters', - SET_SELECTED_SORTING: 'setSelectedSorting', SET_SHIPPING_COUNTRY: 'setShippingCountry', SWITCH_APP_THEME: 'switchAppTheme', SWITCH_IS_USER_LOGGED_IN: 'switchIsUserLoggedIn', @@ -92,22 +87,3 @@ export const setCurrentPage = (value: PageIdType): ActionWithPayload => ({ type: ACTION.SWITCH_APP_THEME, }); - -export const setSelectedFilters = ( - value: SelectedFilters | null, -): ActionWithPayload => ({ - payload: value, - type: ACTION.SET_SELECTED_FILTERS, -}); - -export const setSelectedSorting = ( - value: SelectedSorting | null, -): ActionWithPayload => ({ - payload: value, - type: ACTION.SET_SELECTED_SORTING, -}); - -export const setSearchValue = (value: string): ActionWithPayload => ({ - payload: value, - type: ACTION.SET_SEARCH_VALUE, -}); diff --git a/src/shared/Store/reducer.ts b/src/shared/Store/reducer.ts index 6ecbf2da..d028af27 100644 --- a/src/shared/Store/reducer.ts +++ b/src/shared/Store/reducer.ts @@ -3,8 +3,6 @@ import type { TokenStore } from '@commercetools/sdk-client-v2'; import type { LanguageChoiceType } from '../constants/common.ts'; import type { PageIdType } from '../constants/pages.ts'; -import type { SelectedFilters } from '../types/productFilters.ts'; -import type { SelectedSorting } from '../types/productSorting.ts'; import type * as actions from './actions.ts'; import type { Reducer } from './types.ts'; @@ -18,9 +16,6 @@ export interface State { currentPage: PageIdType; isAppThemeLight: boolean; isUserLoggedIn: boolean; - searchValue: string; - selectedFilters: SelectedFilters | null; - selectedSorting: SelectedSorting | null; shippingCountry: string; } @@ -79,21 +74,6 @@ export const rootReducer: Reducer = (state: State, action: Action ...state, isAppThemeLight: !state.isAppThemeLight, }; - case 'setSelectedFilters': - return { - ...state, - selectedFilters: action.payload, - }; - case 'setSelectedSorting': - return { - ...state, - selectedSorting: action.payload, - }; - case 'setSearchValue': - return { - ...state, - searchValue: action.payload, - }; default: return state; } diff --git a/src/shared/Store/test.spec.ts b/src/shared/Store/test.spec.ts index 7f2e9d98..feaca518 100644 --- a/src/shared/Store/test.spec.ts +++ b/src/shared/Store/test.spec.ts @@ -1,7 +1,5 @@ -import type { SelectedFilters } from '../types/productFilters.ts'; import type { State } from './reducer.ts'; -import { META_FILTERS } from '../constants/filters.ts'; import { PAGE_ID } from '../constants/pages.ts'; import getStore, { Store } from './Store.ts'; import * as actions from './actions.ts'; @@ -65,9 +63,6 @@ vi.mock('./Store.ts', async (importOriginal) => { currentPage: '/', isAppThemeLight: true, isUserLoggedIn: false, - searchValue: '', - selectedFilters: null, - selectedSorting: null, shippingCountry: '', }), }; @@ -183,9 +178,6 @@ describe('rootReducer', () => { currentPage: '/', isAppThemeLight: true, isUserLoggedIn: false, - searchValue: '', - selectedFilters: null, - selectedSorting: null, shippingCountry: '', }; }); @@ -230,16 +222,4 @@ describe('rootReducer', () => { const newState = rootReducer(initialState, action); expect(newState.isAppThemeLight).toEqual(!initialState.isAppThemeLight); }); - - it('should handle setSelectedFilters action', () => { - const filters: SelectedFilters = { - category: new Set(), - metaFilter: META_FILTERS.en.NEW_ARRIVALS, - price: null, - size: null, - }; - const action = actions.setSelectedFilters(filters); - const newState = rootReducer(initialState, action); - expect(newState.selectedFilters).toEqual(filters); - }); }); diff --git a/src/shared/constants/events.ts b/src/shared/constants/events.ts index 55c71f75..068c834e 100644 --- a/src/shared/constants/events.ts +++ b/src/shared/constants/events.ts @@ -1,4 +1,5 @@ const MEDIATOR_EVENT = { + CLEAR_CATALOG_SEARCH: 'CLEAR_CATALOG_SEARCH', REDRAW_PRODUCTS: 'REDRAW_PRODUCTS', REDRAW_USER_ADDRESS: 'REDRAW_USER_ADDRESS', REDRAW_USER_INFO: 'REDRAW_USER_INFO', diff --git a/src/shared/constants/initialState.ts b/src/shared/constants/initialState.ts index 57cdf32b..28067d7e 100644 --- a/src/shared/constants/initialState.ts +++ b/src/shared/constants/initialState.ts @@ -12,17 +12,6 @@ const initialState: State = { currentPage: PAGE_ID.DEFAULT_PAGE, isAppThemeLight: true, isUserLoggedIn: false, - searchValue: '', - selectedFilters: { - category: new Set(), - metaFilter: 'All products', - price: null, - size: null, - }, - selectedSorting: { - direction: 'asc', - field: 'price', - }, shippingCountry: '', }; diff --git a/src/shared/constants/product.ts b/src/shared/constants/product.ts index 2993c391..bc832051 100644 --- a/src/shared/constants/product.ts +++ b/src/shared/constants/product.ts @@ -21,6 +21,8 @@ export const SEARCH_PARAMS_FIELD = { MAX_PRICE: 'price-max', META: 'meta', MIN_PRICE: 'price-min', + PAGE: 'page', + SEARCH: 'search', SIZE: 'size', SUBCATEGORY: 'subcategory', } as const; diff --git a/src/shared/types/productFilters.ts b/src/shared/types/productFilters.ts index bf74f327..233e3e25 100644 --- a/src/shared/types/productFilters.ts +++ b/src/shared/types/productFilters.ts @@ -12,6 +12,7 @@ interface ProductFiltersParams { priceRange: PriceRange | null; products: Product[] | null; sizes: SizeProductCount[] | null; + totalProductCount: number; } export interface SelectedFilters { diff --git a/src/widgets/Catalog/model/CatalogModel.ts b/src/widgets/Catalog/model/CatalogModel.ts index e4496a0b..0e08b1c6 100644 --- a/src/widgets/Catalog/model/CatalogModel.ts +++ b/src/widgets/Catalog/model/CatalogModel.ts @@ -1,7 +1,11 @@ -import type { OptionsRequest, SortOptions } from '@/shared/API/types/type.ts'; +import type { OptionsRequest, PriceRange, SortOptions } from '@/shared/API/types/type.ts'; import type ProductFiltersParams from '@/shared/types/productFilters.ts'; +import type { SelectedFilters } from '@/shared/types/productFilters.ts'; +import type { SelectedSorting } from '@/shared/types/productSorting.ts'; +import RouterModel from '@/app/Router/model/RouterModel.ts'; import ProductCardModel from '@/entities/ProductCard/model/ProductCardModel.ts'; +import PaginationModel from '@/features/Pagination/model/PaginationModel.ts'; import ProductFiltersModel from '@/features/ProductFilters/model/ProductFiltersModel.ts'; import ProductSearchModel from '@/features/ProductSearch/model/ProductSearchModel.ts'; import ProductSortsModel from '@/features/ProductSorts/model/ProductSortsModel.ts'; @@ -10,13 +14,10 @@ import getProductModel from '@/shared/API/product/model/ProductModel.ts'; import FilterProduct from '@/shared/API/product/utils/filter.ts'; import getShoppingListModel from '@/shared/API/shopping-list/model/ShoppingListModel.ts'; import { FilterFields, SortDirection, SortFields } from '@/shared/API/types/type.ts'; -import EventMediatorModel from '@/shared/EventMediator/model/EventMediatorModel.ts'; import LoaderModel from '@/shared/Loader/model/LoaderModel.ts'; import getStore from '@/shared/Store/Store.ts'; -import { setSelectedFilters, setSelectedSorting } from '@/shared/Store/actions.ts'; -import MEDIATOR_EVENT from '@/shared/constants/events.ts'; import { META_FILTERS } from '@/shared/constants/filters.ts'; -import { SEARCH_PARAMS_FIELD } from '@/shared/constants/product.ts'; +import { DEFAULT_PAGE, PRODUCT_LIMIT, SEARCH_PARAMS_FIELD } from '@/shared/constants/product.ts'; import { LOADER_SIZE } from '@/shared/constants/sizes.ts'; import { SORTING_ID } from '@/shared/constants/sorting.ts'; import showErrorMessage from '@/shared/utils/userMessage.ts'; @@ -24,6 +25,10 @@ import showErrorMessage from '@/shared/utils/userMessage.ts'; import CatalogView from '../view/CatalogView.ts'; class CatalogModel { + private currentSize: null | string = null; + + private pagination: PaginationModel | null = null; + private productFilters: ProductFiltersModel | null = null; private productSearch: ProductSearchModel | null = null; @@ -32,8 +37,8 @@ class CatalogModel { private view = new CatalogView(); - constructor(searchParams: string) { - this.init(searchParams).catch(showErrorMessage); + constructor() { + this.init(); } private addCurrentMetaFilter(filter: FilterProduct, metaFilter: string): FilterProduct { @@ -49,62 +54,127 @@ class CatalogModel { } } - private decodeSearchParams(searchParams: string, productData: ProductFiltersParams): void { - const params = new URLSearchParams(searchParams); - const searchCategory = params.getAll(SEARCH_PARAMS_FIELD.CATEGORY); - searchCategory.push(...params.getAll(SEARCH_PARAMS_FIELD.SUBCATEGORY)); - const categories = new Set(searchCategory); - const searchMetaFilter = params.get(SEARCH_PARAMS_FIELD.META) ?? META_FILTERS.en.ALL_PRODUCTS; - const searchSize = params.get(SEARCH_PARAMS_FIELD.SIZE) ?? null; - const maxPrice = parseFloat(params.get(SEARCH_PARAMS_FIELD.MAX_PRICE) ?? '0'); - const minPrice = parseFloat(params.get(SEARCH_PARAMS_FIELD.MIN_PRICE) ?? '0'); - const searchPrice = { - max: maxPrice !== 0 ? maxPrice : productData.priceRange?.max || 0, - min: minPrice !== 0 ? minPrice : productData.priceRange?.min || 0, + private decodeSearchParams(priceRange: PriceRange): { + page: string; + searchValue: null | string; + selectedFilters: SelectedFilters; + selectedSorting?: SelectedSorting; + } { + const searchParams = RouterModel.getSearchParams(); + const searchCategory = searchParams.getAll(SEARCH_PARAMS_FIELD.CATEGORY); + searchCategory.push(...searchParams.getAll(SEARCH_PARAMS_FIELD.SUBCATEGORY)); + const category = new Set(searchCategory); + const metaFilter = searchParams.get(SEARCH_PARAMS_FIELD.META) ?? META_FILTERS.en.ALL_PRODUCTS; + const size = searchParams.get(SEARCH_PARAMS_FIELD.SIZE) ?? null; + const price = { + max: parseFloat(searchParams.get(SEARCH_PARAMS_FIELD.MAX_PRICE) ?? priceRange.max.toString()), + min: parseFloat(searchParams.get(SEARCH_PARAMS_FIELD.MIN_PRICE) ?? priceRange.min.toString()), }; - const searchField = params.get(SEARCH_PARAMS_FIELD.FIELD); - const searchDirection = params.get(SEARCH_PARAMS_FIELD.DIRECTION); + const field = searchParams.get(SEARCH_PARAMS_FIELD.FIELD); + const direction = searchParams.get(SEARCH_PARAMS_FIELD.DIRECTION); + const searchValue = searchParams.get(SEARCH_PARAMS_FIELD.SEARCH) ?? null; + const page = searchParams.get(SEARCH_PARAMS_FIELD.PAGE) ?? DEFAULT_PAGE.toString(); + const selectedFilters = { + category, + metaFilter, + price, + size, + }; - getStore().dispatch( - setSelectedFilters({ category: categories, metaFilter: searchMetaFilter, price: searchPrice, size: searchSize }), - ); + return { + page, + searchValue, + selectedFilters, + ...(field && direction && { selectedSorting: { direction, field } }), + }; + } - if (searchField && searchDirection) { - getStore().dispatch(setSelectedSorting({ direction: searchDirection, field: searchField })); + private async drawProducts(): Promise { + const productList = this.view.getItemsList(); + productList.innerHTML = ''; + const options = await this.getOptions(); + const productsInfo = await this.getProductsInfo(options); + if (productsInfo?.products?.length) { + const shoppingList = await getShoppingListModel().getShoppingList(); + const cart = await getCartModel().getCart(); + productList.innerHTML = ''; + productsInfo.products.forEach((productData) => { + const product = new ProductCardModel(productData, this.currentSize, shoppingList, cart); + productList.append(product.getHTML()); + }); + this.view.switchEmptyList(!productsInfo?.products?.length); + this.pagination?.getHTML().remove(); + this.pagination = new PaginationModel( + { productTotalCount: productsInfo?.totalProductCount ?? 0, productsPerPageCount: PRODUCT_LIMIT }, + this.setCurrentPage.bind(this), + ); + this.pagination.getView().setSelectedButton(options.page ?? DEFAULT_PAGE); + this.view.getRightTopWrapper().append(this.pagination.getHTML()); } + + this.productFilters?.updateParams(productsInfo); + this.view.switchEmptyList(!productsInfo?.products?.length); } - private getOptions(): OptionsRequest { - const { category, metaFilter, price, size } = getStore().getState().selectedFilters || {}; - const { currentLanguage, searchValue } = getStore().getState(); + private async getOptions(): Promise { + let result = {}; + const priceRange = await getProductModel().getPriceRange(); + + const { page, searchValue, selectedFilters, selectedSorting } = this.decodeSearchParams(priceRange); + this.productFilters?.getView().setInitialActiveFilters({ + categoryLinks: Array.from(selectedFilters.category), + metaLinks: selectedFilters.metaFilter ? [selectedFilters.metaFilter] : [], + sizeLinks: selectedFilters.size ? [selectedFilters.size] : [], + }); + const { currentLanguage } = getStore().getState(); const filter = new FilterProduct(); - category?.forEach((categoryID) => filter.addFilter(FilterFields.CATEGORY, categoryID)); - if (price && price.max > price.min) { - filter.addFilter(FilterFields.PRICE, price); + selectedFilters.category.forEach((categoryID) => filter.addFilter(FilterFields.CATEGORY, categoryID)); + + if (selectedFilters.price && selectedFilters.price.max > selectedFilters.price.min) { + filter.addFilter(FilterFields.PRICE, selectedFilters.price); } - if (size) { - filter.addFilter(FilterFields.SIZE, size); + if (selectedFilters.size) { + this.currentSize = selectedFilters.size; + filter.addFilter(FilterFields.SIZE, selectedFilters.size); } - this.addCurrentMetaFilter(filter, metaFilter ?? META_FILTERS.en.ALL_PRODUCTS); - const currentSort = this.getSelectedSorting(); + this.addCurrentMetaFilter(filter, selectedFilters.metaFilter ?? META_FILTERS.en.ALL_PRODUCTS); + + const currentSort = this.getSelectedSorting(selectedSorting ?? null); if (currentSort) { - return { filter: filter.getFilter(), search: { locale: currentLanguage, value: searchValue }, sort: currentSort }; + result = { + filter: filter.getFilter(), + page: Number(page), + search: { locale: currentLanguage, value: searchValue }, + sort: currentSort ?? null, + }; + } else { + result = { + filter: filter.getFilter(), + page: Number(page), + search: { locale: currentLanguage, value: searchValue }, + }; } - return { filter: filter.getFilter() }; + return result; } - private async getProductItems(options: OptionsRequest): Promise { + private async getProductsInfo(options: OptionsRequest): Promise { const productList = this.view.getItemsList(); const loader = new LoaderModel(LOADER_SIZE.EXTRA_LARGE); loader.setAbsolutePosition(); productList.append(loader.getHTML()); try { - const { categoryCount, priceRange, products, sizeCount } = await getProductModel().getProducts(options); - return { categoriesProductCount: categoryCount, priceRange, products, sizes: sizeCount }; + const { categoryCount, priceRange, products, sizeCount, total } = await getProductModel().getProducts(options); + return { + categoriesProductCount: categoryCount, + priceRange, + products, + sizes: sizeCount, + totalProductCount: total, + }; } catch { showErrorMessage(); } finally { @@ -113,8 +183,11 @@ class CatalogModel { return null; } - private getSelectedSorting(): SortOptions | null { - const { direction, field } = getStore().getState().selectedSorting || {}; + private getSelectedSorting(selectedSorting: SelectedSorting | null): SortOptions | null { + if (!selectedSorting) { + return null; + } + const { direction, field } = selectedSorting; const currentDirection = direction === SortDirection.ASC ? SortDirection.ASC : SortDirection.DESC; const currentField = field === SortFields.NAME ? SortFields.NAME : SortFields.PRICE; @@ -127,47 +200,33 @@ class CatalogModel { return { direction: currentDirection, field: currentField, locale: getStore().getState().currentLanguage }; } - private async init(searchParams: string): Promise { - const categories = await getProductModel().getCategories(); - if (categories) { - const productData = await this.getProductItems({}); - if (productData && productData?.products?.length) { - EventMediatorModel.getInstance().subscribe(MEDIATOR_EVENT.REDRAW_PRODUCTS, this.redrawProductList.bind(this)); - this.decodeSearchParams(searchParams, productData); - this.initSettingComponents(productData); - this.redrawProductList().catch(showErrorMessage); - } - this.view.switchEmptyList(!productData?.products?.length); - this.productFilters?.updateParams(productData); - } + private init(): void { + this.getProductsInfo({}) + .then((productsInfo) => { + this.initSettingComponents(productsInfo); + this.drawProducts().catch(showErrorMessage); + }) + .catch(showErrorMessage); } private initSettingComponents(data: ProductFiltersParams | null): void { - this.productFilters = new ProductFiltersModel(data); - this.productSorting = new ProductSortsModel(); - this.productSearch = new ProductSearchModel(); - this.view.getLeftWrapper().append(this.productFilters.getDefaultFilters()); + this.productFilters = new ProductFiltersModel(data, this.drawProducts.bind(this)); + this.productSorting = new ProductSortsModel(this.drawProducts.bind(this)); + this.productSearch = new ProductSearchModel(this.drawProducts.bind(this)); + + this.view.getLeftWrapper().append(this.productFilters.getView().getDefaultFilters()); this.view .getRightTopWrapper() - .append(this.productFilters.getMetaFilters(), this.productSorting.getHTML(), this.productSearch.getHTML()); + .append( + this.productFilters.getView().getMetaFilters(), + this.productSorting.getHTML(), + this.productSearch.getHTML(), + ); } - private async redrawProductList(): Promise { - const currentSize = getStore().getState().selectedFilters?.size ?? null; - const productList = this.view.getItemsList(); - productList.innerHTML = ''; - const productItems = await this.getProductItems(this.getOptions()); - if (productItems?.products?.length) { - const shoppingList = await getShoppingListModel().getShoppingList(); - const cart = await getCartModel().getCart(); - productList.innerHTML = ''; - productItems.products.forEach((productData) => { - const product = new ProductCardModel(productData, currentSize, shoppingList, cart); - productList.append(product.getHTML()); - }); - } - this.productFilters?.updateParams(productItems); - this.view.switchEmptyList(!productItems?.products?.length); + private setCurrentPage(page: string): void { + RouterModel.setSearchParams(SEARCH_PARAMS_FIELD.PAGE, page); + this.drawProducts().catch(showErrorMessage); } public getHTML(): HTMLDivElement { diff --git a/src/widgets/ProductInfo/model/ProductInfoModel.ts b/src/widgets/ProductInfo/model/ProductInfoModel.ts index 8d4f0362..820f4dc0 100644 --- a/src/widgets/ProductInfo/model/ProductInfoModel.ts +++ b/src/widgets/ProductInfo/model/ProductInfoModel.ts @@ -131,12 +131,12 @@ class ProductInfoModel { }); this.bigSlider.autoplay.start(); - const modalSlider = new ProductModalSliderModel(this.params).getHTML(); - - this.view.getBigSliderSlides().forEach((slide) => { + this.view.getBigSliderSlides().forEach((slide, index) => { slide.addEventListener('click', () => { + const modalSlider = new ProductModalSliderModel(this.params); modal.show(); - modal.setContent(modalSlider); + modalSlider.getModalSlider()?.slideTo(index); + modal.setContent(modalSlider.getHTML()); }); }); From 2435860d6d7f56008a09ea75c3ebc94be81989ed Mon Sep 17 00:00:00 2001 From: Max <133934232+Kleostro@users.noreply.github.com> Date: Mon, 27 May 2024 19:59:03 +0300 Subject: [PATCH 09/28] feat(RSS-ECOMM-4_32): display difficultyPlants (#319) * feat: display difficultyPlants * fix: position difficulty plants --- .../ProductPage/model/ProductPageModel.ts | 4 +- src/shared/constants/product.ts | 2 + src/shared/constants/svg.ts | 1 + src/shared/img/svg/leaves.svg | 16 +------ .../ProductInfo/model/ProductInfoModel.ts | 28 ++++++++++-- .../ProductInfo/view/ProductInfoView.ts | 43 +++++++++++++++++-- .../ProductInfo/view/productInfoView.scss | 25 +++++++++++ 7 files changed, 97 insertions(+), 22 deletions(-) diff --git a/src/pages/ProductPage/model/ProductPageModel.ts b/src/pages/ProductPage/model/ProductPageModel.ts index 96896cf7..f30079a1 100644 --- a/src/pages/ProductPage/model/ProductPageModel.ts +++ b/src/pages/ProductPage/model/ProductPageModel.ts @@ -2,6 +2,7 @@ import type { BreadCrumbLink } from '@/shared/types/link.ts'; import type { Page, PageParams } from '@/shared/types/page.ts'; import type { Product, localization } from '@/shared/types/product.ts'; +import RouterModel from '@/app/Router/model/RouterModel.ts'; import BreadcrumbsModel from '@/features/Breadcrumbs/model/BreadcrumbsModel.ts'; import getProductModel from '@/shared/API/product/model/ProductModel.ts'; import getStore from '@/shared/Store/Store.ts'; @@ -57,8 +58,7 @@ class ProductPageModel implements Page { } private init(params: PageParams): void { - const searchParams = new URLSearchParams(params.product?.searchParams); - const currentSize = searchParams.get(SEARCH_PARAMS_FIELD.SIZE); + const currentSize = RouterModel.getSearchParams().get(SEARCH_PARAMS_FIELD.SIZE); getProductModel() .getProductByKey(params.product?.id ?? '') diff --git a/src/shared/constants/product.ts b/src/shared/constants/product.ts index bc832051..d6f1df7f 100644 --- a/src/shared/constants/product.ts +++ b/src/shared/constants/product.ts @@ -29,11 +29,13 @@ export const SEARCH_PARAMS_FIELD = { export const PRODUCT_INFO_TEXT = { en: { + DIFFICULTY: 'Difficulty: ', FULL_DESCRIPTION: 'Full description:', SHORT_DESCRIPTION: 'Short description:', SIZE: 'Size:', }, ru: { + DIFFICULTY: 'Сложность: ', FULL_DESCRIPTION: 'Полное описание:', SHORT_DESCRIPTION: 'Краткое описание:', SIZE: 'Размер:', diff --git a/src/shared/constants/svg.ts b/src/shared/constants/svg.ts index 19992a72..20620dd5 100644 --- a/src/shared/constants/svg.ts +++ b/src/shared/constants/svg.ts @@ -9,6 +9,7 @@ const SVG_DETAILS = { FILL_HEART: 'heartFill', GO_DETAILS: 'arrow', KEY: 'key', + LEAVES: 'leaves', LIGHT: 'light', LOGO: 'logo', OPEN_EYE: 'openEye', diff --git a/src/shared/img/svg/leaves.svg b/src/shared/img/svg/leaves.svg index 44a0d821..50b5f595 100644 --- a/src/shared/img/svg/leaves.svg +++ b/src/shared/img/svg/leaves.svg @@ -1,14 +1,2 @@ - - - - - - - - - - - - - - + +leaf diff --git a/src/widgets/ProductInfo/model/ProductInfoModel.ts b/src/widgets/ProductInfo/model/ProductInfoModel.ts index 820f4dc0..78c6ce94 100644 --- a/src/widgets/ProductInfo/model/ProductInfoModel.ts +++ b/src/widgets/ProductInfo/model/ProductInfoModel.ts @@ -2,6 +2,7 @@ import type { Cart } from '@/shared/types/cart.ts'; import type { ProductInfoParams, Variant } from '@/shared/types/product.ts'; import type { ShoppingListProduct } from '@/shared/types/shopping-list.ts'; +import RouterModel from '@/app/Router/model/RouterModel.ts'; import ProductModalSliderModel from '@/entities/ProductModalSlider/model/ProductModalSliderModel.ts'; import ProductPriceModel from '@/entities/ProductPrice/model/ProductPriceModel.ts'; import getCartModel from '@/shared/API/cart/model/CartModel.ts'; @@ -12,7 +13,9 @@ import modal from '@/shared/Modal/model/ModalModel.ts'; import serverMessageModel from '@/shared/ServerMessage/model/ServerMessageModel.ts'; import MEDIATOR_EVENT from '@/shared/constants/events.ts'; import { MESSAGE_STATUS, SERVER_MESSAGE_KEYS } from '@/shared/constants/messages.ts'; +import { PAGE_ID } from '@/shared/constants/pages.ts'; import { LOADER_SIZE } from '@/shared/constants/sizes.ts'; +import { buildPathName } from '@/shared/utils/buildPathname.ts'; import showErrorMessage from '@/shared/utils/userMessage.ts'; import Swiper from 'swiper'; import 'swiper/css'; @@ -143,6 +146,23 @@ class ProductInfoModel { this.view.getRightWrapper().append(this.price.getHTML()); this.switchToCartButtonHandler(); this.switchToWishListButtonHandler(); + this.setSizeButtonHandler(); + } + + private setSizeButtonHandler(): void { + this.view.getSizeButtons().forEach((sizeButton) => { + sizeButton.getHTML().addEventListener('click', () => { + const currentVariant = this.params.variant.find(({ size }) => size === sizeButton.getHTML().textContent); + + if (currentVariant) { + const path = `${buildPathName(PAGE_ID.PRODUCT_PAGE, this.params.key, { + size: [currentVariant.size ?? this.params.variant[0].size], + })}`; + RouterModel.getInstance().navigateTo(path); + modal.hide(); + } + }); + }); } private switchToCartButtonHandler(): void { @@ -152,10 +172,12 @@ class ProductInfoModel { getCartModel() .getCart() .then((cart) => { - if (cart.products.every((product) => product.key !== this.params.key)) { - this.addProductToCart(); - } else { + if ( + cart.products.find((product) => product.key === this.params.key && product.size === this.params.currentSize) + ) { this.deleteProductFromCart(cart); + } else { + this.addProductToCart(); } }) .catch(showErrorMessage); diff --git a/src/widgets/ProductInfo/view/ProductInfoView.ts b/src/widgets/ProductInfo/view/ProductInfoView.ts index 7c3a6cc4..8dbd3c90 100644 --- a/src/widgets/ProductInfo/view/ProductInfoView.ts +++ b/src/widgets/ProductInfo/view/ProductInfoView.ts @@ -42,6 +42,8 @@ class ProductInfoView { private shortDescription: HTMLParagraphElement; + private sizeButtons: ButtonModel[] = []; + private smallSlider: HTMLDivElement; private switchToCartButton: ButtonModel; @@ -164,6 +166,23 @@ class ProductInfoView { return this.categoriesSpan; } + private createDifficultyPoints(): HTMLSpanElement[] { + const difficultyPoints: HTMLSpanElement[] = []; + for (let index = 0; index < Number(this.params.level); index += 1) { + const difficultyPoint = createBaseElement({ + cssClasses: ['difficultyPoint'], + tag: 'span', + }); + + const svg = document.createElementNS(SVG_DETAILS.SVG_URL, 'svg'); + svg.append(createSVGUse(SVG_DETAILS.LEAVES)); + difficultyPoint.append(svg); + + difficultyPoints.push(difficultyPoint); + } + return difficultyPoints; + } + private createHTML(): HTMLDivElement { this.view = createBaseElement({ cssClasses: ['wrapper'], @@ -216,6 +235,17 @@ class ProductInfoView { } }); + if (this.params.level) { + const difficultySpan = createBaseElement({ + cssClasses: ['difficultySpan'], + innerContent: PRODUCT_INFO_TEXT[getStore().getState().currentLanguage].DIFFICULTY, + tag: 'span', + }); + + difficultySpan.append(...this.createDifficultyPoints()); + this.rightWrapper.append(difficultySpan); + } + shortDescriptionWrapper.append(this.shortDescription); this.rightWrapper.append(this.title, shortDescriptionWrapper); @@ -287,6 +317,7 @@ class ProductInfoView { button.setDisabled(); button.getHTML().classList.add('selected'); } + this.sizeButtons.push(button); return button; } @@ -393,12 +424,14 @@ class ProductInfoView { getCartModel() .getCart() .then((cart) => { - if (cart.products.every((product) => product.key !== this.params.key)) { + if ( + cart.products.find((product) => product.key === this.params.key && product.size === this.params.currentSize) + ) { this.switchToCartButton.getHTML().textContent = - BUTTON_TEXT[getStore().getState().currentLanguage].ADD_PRODUCT; + BUTTON_TEXT[getStore().getState().currentLanguage].DELETE_PRODUCT; } else { this.switchToCartButton.getHTML().textContent = - BUTTON_TEXT[getStore().getState().currentLanguage].DELETE_PRODUCT; + BUTTON_TEXT[getStore().getState().currentLanguage].ADD_PRODUCT; } }) .catch(showErrorMessage); @@ -430,6 +463,10 @@ class ProductInfoView { return this.rightWrapper; } + public getSizeButtons(): ButtonModel[] { + return this.sizeButtons; + } + public getSmallSlider(): HTMLDivElement { return this.smallSlider; } diff --git a/src/widgets/ProductInfo/view/productInfoView.scss b/src/widgets/ProductInfo/view/productInfoView.scss index 80343177..fa838219 100644 --- a/src/widgets/ProductInfo/view/productInfoView.scss +++ b/src/widgets/ProductInfo/view/productInfoView.scss @@ -301,3 +301,28 @@ .hidden { display: none; } + +.difficultySpan { + display: flex; + align-items: center; + order: 3; + font: var(--regular-font); + letter-spacing: var(--one); + color: var(--steam-green-400); + gap: var(--one); + + @media (max-width: 768px) { + align-self: center; + } +} + +.difficultyPoint { + width: var(--extra-small-offset); + height: var(--extra-small-offset); + + svg { + width: var(--extra-small-offset); + height: var(--extra-small-offset); + fill: var(--steam-green-400); + } +} From 7b6e156998ce33f8683bf28c2ed6ddfc1a4a78a4 Mon Sep 17 00:00:00 2001 From: Meg G <146496794+stardustmeg@users.noreply.github.com> Date: Tue, 28 May 2024 19:25:02 +0300 Subject: [PATCH 10/28] feat(RSS-ECOMM-3_18): implement addresses management (#317) * feat: implement adding address * feat: implement adding billing and shipping address * feat: add default address * feat: add styles to add address form * feat: add mixins * feat: implement deleting address * refactor: add error message to a message notification * refactor: remove determining address type to a separate function * feat: implement redrawing after changes * fix: determine address type --- src/app/styles/mixins.scss | 52 +++++ .../UserAddress/model/UserAddressModel.ts | 119 +++++++++-- .../UserAddress/view/UserAddressView.ts | 131 +++++++++--- .../view/userAddressView.module.scss | 88 ++++---- .../AddressAdd/model/AddressAddModel.ts | 196 ++++++++++++++++++ .../AddressAdd/view/AddressAddView.ts | 60 ++++++ .../view/addressAddView.module.scss | 35 ++++ .../PasswordEdit/model/PasswordEditModel.ts | 4 +- .../view/passwordEditView.module.scss | 5 - .../model/PersonalInfoEditModel.ts | 4 +- .../model/UserProfilePageModel.ts | 2 +- .../view/userProfilePageView.module.scss | 2 +- src/shared/constants/buttons.ts | 3 - src/shared/constants/events.ts | 2 +- src/shared/constants/forms.ts | 30 +++ src/shared/constants/messages.ts | 6 + src/shared/constants/svg.ts | 4 + src/shared/constants/tooltip.ts | 20 +- src/shared/img/svg/bill.svg | 1 + src/shared/img/svg/delivery.svg | 1 + src/shared/img/svg/edit.svg | 6 +- src/shared/img/svg/truck.svg | 10 + src/shared/img/svg/wallet.svg | 7 + src/shared/utils/determineNewAddress.ts | 127 ++++++++++++ src/shared/utils/hasValue.ts | 5 + src/shared/utils/messageTemplates.ts | 4 +- src/shared/utils/userMessage.ts | 6 +- src/widgets/Catalog/model/CatalogModel.ts | 4 +- src/widgets/Header/model/HeaderModel.ts | 2 +- src/widgets/LoginForm/model/LoginFormModel.ts | 2 +- .../UserAddresses/model/UserAddressesModel.ts | 75 +++++-- .../UserAddresses/view/UserAddressesView.ts | 70 ++++++- .../view/userAddressesView.module.scss | 40 ++-- src/widgets/UserInfo/model/UserInfoModel.ts | 26 +-- .../UserInfo/view/userInfoView.module.scss | 36 +--- 35 files changed, 972 insertions(+), 213 deletions(-) create mode 100644 src/features/AddressAdd/model/AddressAddModel.ts create mode 100644 src/features/AddressAdd/view/AddressAddView.ts create mode 100644 src/features/AddressAdd/view/addressAddView.module.scss create mode 100644 src/shared/img/svg/bill.svg create mode 100644 src/shared/img/svg/delivery.svg create mode 100644 src/shared/img/svg/truck.svg create mode 100644 src/shared/img/svg/wallet.svg create mode 100644 src/shared/utils/determineNewAddress.ts create mode 100644 src/shared/utils/hasValue.ts diff --git a/src/app/styles/mixins.scss b/src/app/styles/mixins.scss index 475fee90..8d43d332 100644 --- a/src/app/styles/mixins.scss +++ b/src/app/styles/mixins.scss @@ -31,3 +31,55 @@ pointer-events: none; } } + +@mixin round-btn { + display: flex; + align-items: center; + justify-content: center; + justify-self: center; + border-radius: 50%; + padding: calc(var(--tiny-offset) * 1.5); + width: max-content; + background-color: var(--noble-white-100); + transition: + color 0.2s, + background-color 0.2s, + transform 0.2s; + + @media (hover: hover) { + &:hover { + background-color: var(--white-tr); + + svg { + fill: var(--steam-green-800); + stroke: var(--steam-green-800); + } + } + /* stylelint-disable-next-line order/order */ + &:active { + transform: scale(1.1); + } + + &:disabled { + background-color: var(--noble-gray-300); + pointer-events: none; + } + } +} + +@mixin logo { + display: flex; + align-items: center; + justify-content: center; + justify-self: center; + + svg { + width: var(--small-offset); + height: var(--small-offset); + fill: var(--noble-gray-900); + stroke: var(--noble-gray-900); + transition: + fill 0.2s, + stroke 0.2s; + } +} diff --git a/src/entities/UserAddress/model/UserAddressModel.ts b/src/entities/UserAddress/model/UserAddressModel.ts index 4af86fb4..f93b2982 100644 --- a/src/entities/UserAddress/model/UserAddressModel.ts +++ b/src/entities/UserAddress/model/UserAddressModel.ts @@ -1,11 +1,13 @@ +/* eslint-disable max-lines-per-function */ import type { Address, User } from '@/shared/types/user.ts'; -// import AddressEditModel from '@/features/AddressEdit/model/AddressEditModel.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 modal from '@/shared/Modal/model/ModalModel.ts'; import serverMessageModel from '@/shared/ServerMessage/model/ServerMessageModel.ts'; -import { USER_ADDRESS_TYPE, type UserAddressType } from '@/shared/constants/forms.ts'; +import MEDIATOR_EVENT from '@/shared/constants/events.ts'; +import { ADDRESS_TYPE, type AddressTypeType } from '@/shared/constants/forms.ts'; import { MESSAGE_STATUS, SERVER_MESSAGE_KEYS } from '@/shared/constants/messages.ts'; import { LOADER_SIZE } from '@/shared/constants/sizes.ts'; import showErrorMessage from '@/shared/utils/userMessage.ts'; @@ -13,15 +15,90 @@ import showErrorMessage from '@/shared/utils/userMessage.ts'; import UserAddressView from '../view/UserAddressView.ts'; class UserAddressModel { + private currentAddress: Address; + + private labels: Map; + private view: UserAddressView; - constructor(user: User, address: Address, type: UserAddressType, defaultAddressId: string) { - this.view = new UserAddressView(user, address, type, defaultAddressId); - this.setDeleteButtonHandler(address, type); - this.setEditButtonHandler(address, type); + constructor(user: User, address: Address, activeTypes: AddressTypeType[], inactiveTypes?: AddressTypeType[]) { + this.currentAddress = address; + this.view = new UserAddressView(user.locale, address, activeTypes, inactiveTypes); + this.labels = this.view.getLabels(); + this.setEditButtonHandler(address); + this.setDeleteButtonHandler(address); + this.setLabelClickHandler(); + } + + private async handleAddressType(user: User, activeType: AddressTypeType, inactive: boolean): Promise { + const customerModel = getCustomerModel(); + + if (inactive) { + switch (activeType) { + case ADDRESS_TYPE.BILLING: + await customerModel.editCustomer([CustomerModel.actionAddBillingAddress(this.currentAddress.id)], user); + break; + + case ADDRESS_TYPE.SHIPPING: + await customerModel.editCustomer([CustomerModel.actionAddShippingAddress(this.currentAddress.id)], user); + break; + + case ADDRESS_TYPE.DEFAULT_BILLING: + await customerModel.editCustomer( + [CustomerModel.actionEditDefaultBillingAddress(this.currentAddress.id)], + user, + ); + break; + + case ADDRESS_TYPE.DEFAULT_SHIPPING: + await customerModel.editCustomer( + [CustomerModel.actionEditDefaultShippingAddress(this.currentAddress.id)], + user, + ); + break; + + default: + break; + } + } else { + switch (activeType) { + case ADDRESS_TYPE.BILLING: + await customerModel.editCustomer([CustomerModel.actionRemoveBillingAddress(this.currentAddress)], user); + break; + + case ADDRESS_TYPE.SHIPPING: + await customerModel.editCustomer([CustomerModel.actionRemoveShippingAddress(this.currentAddress)], user); + break; + + // TBD Check the adding/removing default address + // case ADDRESS_TYPE.DEFAULT_BILLING: + // await customerModel.editCustomer([CustomerModel.actionEditDefaultBillingAddress(null)], user); + // break; + + // case ADDRESS_TYPE.DEFAULT_SHIPPING: + // await customerModel.editCustomer([CustomerModel.actionEditDefaultShippingAddress(null)], user); + // break; + + default: + break; + } + } } - private setDeleteButtonHandler(address: Address, type: UserAddressType): void { + private async labelClickHandler(activeType: AddressTypeType, inactive?: boolean): Promise { + try { + const user = await getCustomerModel().getCurrentUser(); + if (user) { + await this.handleAddressType(user, activeType, inactive ?? false); + EventMediatorModel.getInstance().notify(MEDIATOR_EVENT.REDRAW_USER_ADDRESSES, ''); + serverMessageModel.showServerMessage(SERVER_MESSAGE_KEYS.ADDRESS_STATUS_CHANGED, MESSAGE_STATUS.SUCCESS); + } + } catch (error) { + showErrorMessage(error); + } + } + + private setDeleteButtonHandler(address: Address): void { this.view .getDeleteButton() .getHTML() @@ -32,22 +109,15 @@ class UserAddressModel { const user = await getCustomerModel().getCurrentUser(); if (user) { try { - if (type === USER_ADDRESS_TYPE.BILLING) { - await getCustomerModel().editCustomer([CustomerModel.actionRemoveBillingAddress(address)], user); - await getCustomerModel().editCustomer([CustomerModel.actionRemoveAddress(address)], user); - // TBD Check requests to delete address - } - if (type === USER_ADDRESS_TYPE.SHIPPING) { - await getCustomerModel().editCustomer([CustomerModel.actionRemoveShippingAddress(address)], user); - } + await getCustomerModel().editCustomer([CustomerModel.actionRemoveAddress(address)], user); + EventMediatorModel.getInstance().notify(MEDIATOR_EVENT.REDRAW_USER_ADDRESSES, ''); serverMessageModel.showServerMessage(SERVER_MESSAGE_KEYS.ADDRESS_DELETED, MESSAGE_STATUS.SUCCESS); - this.view.getHTML().remove(); } catch (error) { - showErrorMessage(); + showErrorMessage(error); } } } catch (error) { - showErrorMessage(); + showErrorMessage(error); } finally { loader.remove(); } @@ -55,7 +125,7 @@ class UserAddressModel { } // eslint-disable-next-line @typescript-eslint/no-unused-vars - private setEditButtonHandler(_address: Address, _type: UserAddressType): void { + private setEditButtonHandler(_address: Address): void { this.view .getEditButton() .getHTML() @@ -66,15 +136,22 @@ class UserAddressModel { return; } modal.show(); - // modal.setContent(new AddressEditModel( address, _type).getHTML()); } catch (error) { - showErrorMessage(); + showErrorMessage(error); } finally { modal.hide(); } }); } + private setLabelClickHandler(): void { + this.labels.forEach((value: { inactive?: boolean; type: AddressTypeType }, label: HTMLDivElement) => { + label.addEventListener('click', async () => { + await this.labelClickHandler(value.type, value.inactive); + }); + }); + } + public getHTML(): HTMLLIElement { return this.view.getHTML(); } diff --git a/src/entities/UserAddress/view/UserAddressView.ts b/src/entities/UserAddress/view/UserAddressView.ts index cff58393..4967a597 100644 --- a/src/entities/UserAddress/view/UserAddressView.ts +++ b/src/entities/UserAddress/view/UserAddressView.ts @@ -1,25 +1,24 @@ -import type { Address, User } from '@/shared/types/user'; +/* eslint-disable max-lines-per-function */ +import type { TooltipTextKeysType } from '@/shared/constants/tooltip.ts'; +import type { Address } from '@/shared/types/user'; import ButtonModel from '@/shared/Button/model/ButtonModel.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_ADDRESS_TYPE, type UserAddressType } from '@/shared/constants/forms.ts'; +import { ADDRESS_TYPE, type AddressTypeType } from '@/shared/constants/forms.ts'; import SVG_DETAILS from '@/shared/constants/svg.ts'; -import TOOLTIP_TEXT from '@/shared/constants/tooltip.ts'; +import TOOLTIP_TEXT, { TOOLTIP_TEXT_KEYS } from '@/shared/constants/tooltip.ts'; import createBaseElement from '@/shared/utils/createBaseElement.ts'; import createSVGUse from '@/shared/utils/createSVGUse.ts'; import findKeyByValue from '@/shared/utils/findKeyByValue.ts'; -import { - addressMessage, - addressTemplate, - defaultBillingAddress, - defaultShippingAddress, -} from '@/shared/utils/messageTemplates.ts'; +import { addressTemplate } from '@/shared/utils/messageTemplates.ts'; import styles from './userAddressView.module.scss'; class UserAddressView { + private currentAddress: Address; + private deleteButton: ButtonModel; private deleteLogo: HTMLDivElement; @@ -28,31 +27,26 @@ class UserAddressView { private editLogo: HTMLDivElement; + private labels: Map = new Map(); + private view: HTMLLIElement; - constructor(user: User, address: Address, type: UserAddressType, defaultAddressId: string) { + constructor(locale: string, address: Address, types: AddressTypeType[], inactiveTypes?: AddressTypeType[]) { + this.currentAddress = address; this.deleteLogo = this.createDeleteLogo(); - this.editLogo = this.createEditLogo(); this.deleteButton = this.createDeleteButton(); + this.editLogo = this.createEditLogo(); this.editButton = this.createEditButton(); - this.view = this.createHTML(user, address, type, defaultAddressId); + this.view = this.createHTML(locale, types, inactiveTypes); } - private createAddress(user: User, address: Address, type: UserAddressType, defaultAddressId: string): string { - const { locale } = user; + private createAddressText(locale: string): string { + const { city, country, postalCode, streetName } = this.currentAddress; - const country = findKeyByValue(COUNTRIES_LIST[locale], address.country); - const standartAddressText = addressTemplate(address.streetName, address.city, country, address.postalCode); - let addressText = addressMessage(type, standartAddressText); + const countryKey = findKeyByValue(COUNTRIES_LIST[locale], country); + const standardAddressText = addressTemplate(streetName, city, countryKey, postalCode); - if (defaultAddressId === address.id) { - if (type === USER_ADDRESS_TYPE.BILLING) { - addressText = defaultBillingAddress(standartAddressText); - } else if (type === USER_ADDRESS_TYPE.SHIPPING) { - addressText = defaultShippingAddress(standartAddressText); - } - } - return addressText; + return standardAddressText; } private createDeleteButton(): ButtonModel { @@ -101,21 +95,96 @@ class UserAddressView { return this.editLogo; } - private createHTML(user: User, address: Address, type: UserAddressType, defaultAddressId: string): HTMLLIElement { + private createHTML(locale: string, activeTypes: AddressTypeType[], inactiveTypes?: AddressTypeType[]): HTMLLIElement { this.view = createBaseElement({ - cssClasses: [styles.info], + cssClasses: [styles.addressItem], tag: 'li', }); const addressText = createBaseElement({ cssClasses: [styles.addressText], - innerContent: this.createAddress(user, address, type, defaultAddressId), - tag: 'div', + innerContent: this.createAddressText(locale), + tag: 'span', + }); + + activeTypes.forEach((type) => { + this.setActiveAddressLabel(type); }); + if (inactiveTypes) { + inactiveTypes.forEach((type) => { + this.setActiveAddressLabel(type, true); + }); + } + this.view.append(addressText, this.editButton.getHTML(), this.deleteButton.getHTML()); return this.view; } + private createLabel(text: string, additionalStyles: string[], titleKey: TooltipTextKeysType): HTMLDivElement { + const label = createBaseElement({ + cssClasses: [styles.addressType, ...additionalStyles], + innerContent: text, + tag: 'div', + title: TOOLTIP_TEXT[getStore().getState().currentLanguage][titleKey], + }); + + observeStore(selectCurrentLanguage, () => { + label.title = TOOLTIP_TEXT[getStore().getState().currentLanguage][titleKey]; + }); + + return label; + } + + private setActiveAddressLabel(ActiveType: AddressTypeType, inactive?: boolean): void { + let addressType = null; + switch (ActiveType) { + case ADDRESS_TYPE.BILLING: + addressType = this.createLabel(ActiveType, [styles.billing], TOOLTIP_TEXT_KEYS.EDIT_BILLING_ADDRESS); + this.view.append(addressType); + break; + + case ADDRESS_TYPE.SHIPPING: + addressType = this.createLabel(ActiveType, [styles.shipping], TOOLTIP_TEXT_KEYS.EDIT_SHIPPING_ADDRESS); + this.view.append(addressType); + break; + + case ADDRESS_TYPE.DEFAULT_BILLING: + addressType = this.createLabel( + ActiveType, + [styles.defaultBilling], + TOOLTIP_TEXT_KEYS.EDIT_DEFAULT_BILLING_ADDRESS, + ); + this.view.append(addressType); + break; + + case ADDRESS_TYPE.DEFAULT_SHIPPING: + addressType = this.createLabel( + ActiveType, + [styles.defaultShipping], + TOOLTIP_TEXT_KEYS.EDIT_DEFAULT_SHIPPING_ADDRESS, + ); + this.view.append(addressType); + break; + default: + break; + } + + if (addressType) { + this.labels.set(addressType, { + inactive, + type: ActiveType, + }); + } + + if (inactive) { + addressType?.classList.add(styles.inactive); + } + } + + public getCurrentAddress(): Address { + return this.currentAddress; + } + public getDeleteButton(): ButtonModel { return this.deleteButton; } @@ -127,6 +196,10 @@ class UserAddressView { public getHTML(): HTMLLIElement { return this.view; } + + public getLabels(): Map { + return this.labels; + } } export default UserAddressView; diff --git a/src/entities/UserAddress/view/userAddressView.module.scss b/src/entities/UserAddress/view/userAddressView.module.scss index 18ce13ff..5ec45c17 100644 --- a/src/entities/UserAddress/view/userAddressView.module.scss +++ b/src/entities/UserAddress/view/userAddressView.module.scss @@ -1,55 +1,69 @@ -.info { +@import 'src/app/styles/mixins'; + +.addressItem { + display: flex; + align-items: center; + justify-content: flex-start; grid-column: 2 span; max-width: 100%; word-break: break-all; } -.deleteLogo, -.editLogo { - display: flex; - margin: 0 auto; - - svg { - z-index: 1; - width: var(--extra-small-offset); - height: var(--extra-small-offset); - fill: var(--noble-gray-900); - transition: 0.2s; +.addressType { + margin: var(--tiny-offset) var(--five); + border-radius: var(--medium-br); + padding: var(--tiny-offset); + font: var(--small-font); + background-color: var(--steam-green-900); + transition: scale 0.2s; + + &:hover { + cursor: pointer; + scale: 1.1; } } -.deleteButton, -.editButton { - display: flex; +.billing { + background-color: #d5e4f9; +} + +.shipping { + background-color: #e9fffe; +} + +.defaultBilling { + background-color: #dfd6ef; +} + +.defaultShipping { + background-color: #fee1c9; +} + +.inactive { + background-color: var(--noble-gray-tr-800); +} + +.editLogo, +.deleteLogo { + @include logo; + + width: calc(var(--extra-small-offset) * 1.5); + height: calc(var(--extra-small-offset) * 1.5); +} + +.editButton, +.deleteButton { + @include green-btn; + margin: var(--tiny-offset); - border-radius: var(--medium-br); - padding: var(--tiny-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); - } + padding: var(--five); @media (hover: hover) { &:hover { - background-color: var(--steam-green-700); - svg { fill: var(--noble-gray-1000); + transition: fill 0.2s; } } } - - &:disabled { - background-color: var(--noble-gray-300); - pointer-events: none; - } } diff --git a/src/features/AddressAdd/model/AddressAddModel.ts b/src/features/AddressAdd/model/AddressAddModel.ts new file mode 100644 index 00000000..86c3468f --- /dev/null +++ b/src/features/AddressAdd/model/AddressAddModel.ts @@ -0,0 +1,196 @@ +import type InputFieldModel from '@/entities/InputField/model/InputFieldModel.ts'; +import type { AddressType } from '@/shared/types/address.ts'; +import type { Address, User } from '@/shared/types/user.ts'; +import type { MyCustomerUpdateAction } from '@commercetools/platform-sdk'; + +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 modal from '@/shared/Modal/model/ModalModel.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 { 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'; +import showErrorMessage from '@/shared/utils/userMessage.ts'; + +import AddressAddView from '../view/AddressAddView.ts'; + +class AddressAddModel { + private addressType: AddressType; + + private inputFields: InputFieldModel[] = []; + + private newAddress: AddressModel; + + private view = new AddressAddView(); + + constructor(type: AddressType, options: Record) { + this.addressType = type; + this.newAddress = new AddressModel(this.addressType, options); + this.init(); + } + + private async addAddressType(): Promise { + try { + const updatedUser = await getCustomerModel().getCurrentUser(); + if (updatedUser) { + const newAddress = this.getNewAddress(updatedUser); + if (newAddress?.id) { + const actions = this.getAddressActions(newAddress.id); + if (this.shouldSetDefaultAddress()) { + actions.push(this.getDefaultAddressAction(newAddress.id)); + } + + await getCustomerModel().editCustomer(actions, updatedUser); + this.handleSuccess(); + } + } + } catch (error) { + showErrorMessage(error); + } + } + + private createAddress(user: User): Address { + const store = getStore().getState(); + const { email, firstName, lastName } = user; + const { city, postalCode, streetName } = this.getFormAddressData(); + return { + city, + country: this.addressType === ADDRESS_TYPE.BILLING ? store.billingCountry : store.shippingCountry, + email, + firstName, + id: '', + lastName, + postalCode, + state: '', + streetName, + streetNumber: '', + }; + } + + private async createNewAddress(): Promise { + const loader = new LoaderModel(LOADER_SIZE.SMALL).getHTML(); + this.view.getSaveChangesButton().getHTML().append(loader); + + try { + const user = await getCustomerModel().getCurrentUser(); + if (user) { + const address = this.createAddress(user); + await this.saveAddress(user, address); + await this.addAddressType(); + } + } catch (error) { + showErrorMessage(error); + } finally { + loader.remove(); + } + } + + private getAddressActions(addressId: string): MyCustomerUpdateAction[] { + const addAddressAction = + this.addressType === ADDRESS_TYPE.BILLING + ? CustomerModel.actionAddBillingAddress(addressId) + : CustomerModel.actionAddShippingAddress(addressId); + + return [addAddressAction]; + } + + private getDefaultAddressAction(addressId: string): MyCustomerUpdateAction { + return this.addressType === ADDRESS_TYPE.BILLING + ? CustomerModel.actionEditDefaultBillingAddress(addressId) + : CustomerModel.actionEditDefaultShippingAddress(addressId); + } + + private getFormAddressData(): Record { + return { + city: formattedText(this.newAddress.getView().getCityField().getView().getInput().getValue()), + country: this.newAddress.getView().getCountryField().getView().getInput().getValue(), + postalCode: this.newAddress.getView().getPostalCodeField().getView().getInput().getValue(), + streetName: formattedText(this.newAddress.getView().getStreetField().getView().getInput().getValue()), + }; + } + + private getNewAddress(user: User): Address | null { + return user.addresses[user.addresses.length - 1] || null; + } + + private handleSuccess(): void { + EventMediatorModel.getInstance().notify(MEDIATOR_EVENT.REDRAW_USER_ADDRESSES, ''); + serverMessageModel.showServerMessage(SERVER_MESSAGE_KEYS.ADDRESS_ADDED, MESSAGE_STATUS.SUCCESS); + modal.hide(); + } + + private init(): void { + this.getHTML().append(this.newAddress.getHTML()); + this.inputFields = [ + this.newAddress.getView().getCityField(), + this.newAddress.getView().getCountryField(), + this.newAddress.getView().getPostalCodeField(), + this.newAddress.getView().getStreetField(), + ]; + + this.inputFields.forEach((inputField) => this.setInputFieldHandlers(inputField)); + this.setPreventDefaultToForm(); + this.setSubmitFormHandler(); + this.setCancelButtonHandler(); + this.view.getHTML().append(this.newAddress.getHTML()); + } + + private async saveAddress(user: User, address: Address): Promise { + await getCustomerModel().editCustomer([CustomerModel.actionAddAddress(address)], user); + } + + private setCancelButtonHandler(): boolean { + const cancelButton = this.view.getCancelButton().getHTML(); + cancelButton.addEventListener('click', () => { + modal.hide(); + }); + return true; + } + + private setInputFieldHandlers(inputField: InputFieldModel): boolean { + if (inputField?.getView && typeof inputField.getView === 'function') { + const inputHTML = inputField.getView().getInput().getHTML(); + inputHTML.addEventListener('input', () => { + this.switchSubmitFormButtonAccess(); + }); + } + return true; + } + + private setPreventDefaultToForm(): boolean { + this.getHTML().addEventListener('submit', (event) => { + event.preventDefault(); + }); + return true; + } + + private setSubmitFormHandler(): boolean { + const submitButton = this.view.getSaveChangesButton().getHTML(); + submitButton.addEventListener('click', () => this.createNewAddress()); + return true; + } + + private shouldSetDefaultAddress(): boolean { + return this.newAddress.getView().getAddressByDefaultCheckBox()?.getHTML().checked || false; + } + + private switchSubmitFormButtonAccess(): boolean { + if (this.inputFields.every((inputField) => inputField.getIsValid())) { + this.view.getSaveChangesButton().setEnabled(); + } else { + this.view.getSaveChangesButton().setDisabled(); + } + return true; + } + + public getHTML(): HTMLFormElement { + return this.view.getHTML(); + } +} + +export default AddressAddModel; diff --git a/src/features/AddressAdd/view/AddressAddView.ts b/src/features/AddressAdd/view/AddressAddView.ts new file mode 100644 index 00000000..c0c8b9db --- /dev/null +++ b/src/features/AddressAdd/view/AddressAddView.ts @@ -0,0 +1,60 @@ +import ButtonModel from '@/shared/Button/model/ButtonModel.ts'; +import getStore from '@/shared/Store/Store.ts'; +import { BUTTON_TEXT } from '@/shared/constants/buttons.ts'; +import createBaseElement from '@/shared/utils/createBaseElement.ts'; + +import styles from './addressAddView.module.scss'; + +class AddressAddView { + private cancelButton: ButtonModel; + + private saveChangesButton: ButtonModel; + + private view: HTMLFormElement; + + constructor() { + this.saveChangesButton = this.createSaveChangesButton(); + this.cancelButton = this.createCancelButton(); + this.view = this.createHTML(); + } + + private createCancelButton(): ButtonModel { + this.cancelButton = new ButtonModel({ + classes: [styles.cancelButton], + text: BUTTON_TEXT[getStore().getState().currentLanguage].CANCEL, + }); + return this.cancelButton; + } + + private createHTML(): HTMLFormElement { + this.view = createBaseElement({ + cssClasses: [styles.wrapper], + tag: 'form', + }); + this.view.append(this.cancelButton.getHTML(), this.saveChangesButton.getHTML()); + return this.view; + } + + private createSaveChangesButton(): ButtonModel { + this.saveChangesButton = new ButtonModel({ + classes: [styles.saveChangesButton], + text: BUTTON_TEXT[getStore().getState().currentLanguage].SAVE_CHANGES, + }); + this.saveChangesButton.setDisabled(); + return this.saveChangesButton; + } + + public getCancelButton(): ButtonModel { + return this.cancelButton; + } + + public getHTML(): HTMLFormElement { + return this.view; + } + + public getSaveChangesButton(): ButtonModel { + return this.saveChangesButton; + } +} + +export default AddressAddView; diff --git a/src/features/AddressAdd/view/addressAddView.module.scss b/src/features/AddressAdd/view/addressAddView.module.scss new file mode 100644 index 00000000..71fcc608 --- /dev/null +++ b/src/features/AddressAdd/view/addressAddView.module.scss @@ -0,0 +1,35 @@ +@import 'src/app/styles/mixins'; + +.wrapper { + display: grid; + place-items: center center; + grid-template-rows: repeat(auto, auto); + border-bottom: var(--tiny-offset) solid var(--steam-green-800); + padding: var(--small-offset); + height: max-content; + background-color: var(--noble-white-100); +} + +.saveChangesButton, +.cancelButton { + @include green-btn; + + grid-row: 5; + margin-top: var(--tiny-offset); +} + +.cancelButton { + grid-column: 1; + + @media (max-width: 768px) { + grid-row: 6; + } +} + +.saveChangesButton { + grid-column: 2; + + @media (max-width: 768px) { + grid-column: 1; + } +} diff --git a/src/features/PasswordEdit/model/PasswordEditModel.ts b/src/features/PasswordEdit/model/PasswordEditModel.ts index 9ff865de..e976fc01 100644 --- a/src/features/PasswordEdit/model/PasswordEditModel.ts +++ b/src/features/PasswordEdit/model/PasswordEditModel.ts @@ -58,8 +58,8 @@ class PasswordEditModel { } } }); - } catch { - showErrorMessage(); + } catch (error) { + showErrorMessage(error); } finally { loader.remove(); } diff --git a/src/features/PasswordEdit/view/passwordEditView.module.scss b/src/features/PasswordEdit/view/passwordEditView.module.scss index 2dda3dab..98b65c81 100644 --- a/src/features/PasswordEdit/view/passwordEditView.module.scss +++ b/src/features/PasswordEdit/view/passwordEditView.module.scss @@ -15,14 +15,9 @@ .saveChangesButton, .cancelButton { @include green-btn; - - margin: 0 auto; - padding: calc(var(--small-offset) / 3) var(--small-offset); - height: max-content; } .saveChangesButton { - display: flex; grid-row: 3; } diff --git a/src/features/PersonalInfoEdit/model/PersonalInfoEditModel.ts b/src/features/PersonalInfoEdit/model/PersonalInfoEditModel.ts index 8f2e87b6..08ddedbc 100644 --- a/src/features/PersonalInfoEdit/model/PersonalInfoEditModel.ts +++ b/src/features/PersonalInfoEdit/model/PersonalInfoEditModel.ts @@ -47,11 +47,11 @@ class PersonalInfoEditModel { user, ); modal.hide(); - serverMessageModel.showServerMessage(SERVER_MESSAGE_KEYS.PERSONAL_INFO_CHANGED, MESSAGE_STATUS.SUCCESS); EventMediatorModel.getInstance().notify(MEDIATOR_EVENT.REDRAW_USER_INFO, ''); + serverMessageModel.showServerMessage(SERVER_MESSAGE_KEYS.PERSONAL_INFO_CHANGED, MESSAGE_STATUS.SUCCESS); } } catch (error) { - showErrorMessage(); + showErrorMessage(error); } finally { loader.remove(); } diff --git a/src/pages/UserProfilePage/model/UserProfilePageModel.ts b/src/pages/UserProfilePage/model/UserProfilePageModel.ts index 3aa90d64..68a3dc2a 100644 --- a/src/pages/UserProfilePage/model/UserProfilePageModel.ts +++ b/src/pages/UserProfilePage/model/UserProfilePageModel.ts @@ -43,7 +43,7 @@ class UserProfilePageModel implements Page { getStore().dispatch(setCurrentPage(PAGE_ID.USER_PROFILE_PAGE)); } } catch (error) { - showErrorMessage(); + showErrorMessage(error); } } diff --git a/src/pages/UserProfilePage/view/userProfilePageView.module.scss b/src/pages/UserProfilePage/view/userProfilePageView.module.scss index 6cf4ec28..07614830 100644 --- a/src/pages/UserProfilePage/view/userProfilePageView.module.scss +++ b/src/pages/UserProfilePage/view/userProfilePageView.module.scss @@ -44,7 +44,7 @@ padding: var(--small-offset); width: 25%; height: fit-content; - background-color: var(--white-tr); + background-color: var(--white); gap: var(--extra-small-offset); } diff --git a/src/shared/constants/buttons.ts b/src/shared/constants/buttons.ts index 7523bdc3..0035db11 100644 --- a/src/shared/constants/buttons.ts +++ b/src/shared/constants/buttons.ts @@ -13,7 +13,6 @@ export const BUTTON_TEXT = { EDIT_INFO: 'Edit', LOG_OUT: 'Log out', LOGIN: 'Login', - NEW_ADDRESS: 'New address', REGISTRATION: 'Register', RESET: 'Reset', SAVE_CHANGES: 'Save changes', @@ -26,7 +25,6 @@ export const BUTTON_TEXT = { EDIT_INFO: 'Редактировать', LOG_OUT: 'Выйти', LOGIN: 'Войти', - NEW_ADDRESS: 'Новый адрес', REGISTRATION: 'Регистрация', RESET: 'Сбросить', SAVE_CHANGES: 'Сохранить', @@ -41,7 +39,6 @@ export const BUTTON_TEXT_KEYS = { EDIT_INFO: 'EDIT_INFO', LOG_OUT: 'LOG_OUT', LOGIN: 'LOGIN', - NEW_ADDRESS: 'NEW_ADDRESS', REGISTRATION: 'REGISTRATION', RESET: 'RESET', SAVE_CHANGES: 'SAVE_CHANGES', diff --git a/src/shared/constants/events.ts b/src/shared/constants/events.ts index 068c834e..64ee591b 100644 --- a/src/shared/constants/events.ts +++ b/src/shared/constants/events.ts @@ -1,7 +1,7 @@ const MEDIATOR_EVENT = { CLEAR_CATALOG_SEARCH: 'CLEAR_CATALOG_SEARCH', REDRAW_PRODUCTS: 'REDRAW_PRODUCTS', - REDRAW_USER_ADDRESS: 'REDRAW_USER_ADDRESS', + REDRAW_USER_ADDRESSES: 'REDRAW_USER_ADDRESSES', REDRAW_USER_INFO: 'REDRAW_USER_INFO', } as const; diff --git a/src/shared/constants/forms.ts b/src/shared/constants/forms.ts index bcc37bd5..b8ceb4a2 100644 --- a/src/shared/constants/forms.ts +++ b/src/shared/constants/forms.ts @@ -29,6 +29,10 @@ export const FORM_TEXT_KEYS = { SINGLE_ADDRESS: 'SINGLE_ADDRESS', } as const; +export const DEFAULT_ADDRESS = { + setDefault: true, +}; + export type FormTextKeysType = (typeof FORM_TEXT_KEYS)[keyof typeof FORM_TEXT_KEYS]; export const USER_COUNTRY_ADDRESS = { @@ -43,6 +47,32 @@ export const USER_ADDRESS_TYPE = { export type UserAddressType = (typeof USER_ADDRESS_TYPE)[keyof typeof USER_ADDRESS_TYPE]; +export const ADDRESS_TYPE = { + BILLING: 'billing', + DEFAULT_BILLING: 'default billing', + DEFAULT_SHIPPING: 'default shipping', + SHIPPING: 'shipping', +} as const; + +export type AddressTypeType = (typeof ADDRESS_TYPE)[keyof typeof ADDRESS_TYPE]; + +export const LABEL_TYPE = { + en: { + BILLING: 'billing', + DEFAULT_BILLING: 'default billing', + DEFAULT_SHIPPING: 'default shipping', + SHIPPING: 'shipping', + }, + ru: { + BILLING: 'Выставление счетов', + DEFAULT_BILLING: 'По умолчанию для выставления счетов', + DEFAULT_SHIPPING: 'По умолчанию для доставки', + SHIPPING: 'Доставка', + }, +} as const; + +export type LabelTypeType = (typeof LABEL_TYPE)[keyof typeof LABEL_TYPE]; + export const USER_POSTAL_CODE = { BILLING_POSTAL_CODE: 'billing_PostalCode', POSTAL_CODE: 'postalCode', diff --git a/src/shared/constants/messages.ts b/src/shared/constants/messages.ts index 8f30f5e5..6522a92c 100644 --- a/src/shared/constants/messages.ts +++ b/src/shared/constants/messages.ts @@ -14,8 +14,10 @@ export type MessageStatusKeysType = (typeof MESSAGE_STATUS_KEYS)[keyof typeof ME export const SERVER_MESSAGE = { en: { + ADDRESS_ADDED: 'Address has been added successfully', ADDRESS_CHANGED: 'Address has been changed successfully', ADDRESS_DELETED: 'Address has been deleted successfully', + ADDRESS_STATUS_CHANGED: 'Address status has been changed successfully', BAD_REQUEST: 'Sorry, something went wrong. Try again later.', COPY_TO_CLIPBOARD: 'SKU copied to clipboard', GREETING: 'Hi! Welcome to our store. Enjoy shopping!', @@ -35,8 +37,10 @@ export const SERVER_MESSAGE = { USER_EXISTS: 'User with this email already exists, please check your email', }, ru: { + ADDRESS_ADDED: 'Адрес был успешно добавлен', ADDRESS_CHANGED: 'Адрес был успешно изменен', ADDRESS_DELETED: 'Адрес был успешно удален', + ADDRESS_STATUS_CHANGED: 'Статус адреса был успешно изменен', BAD_REQUEST: 'Извините, что-то пошло не так. Попробуйте позже.', COPY_TO_CLIPBOARD: 'SKU скопирован в буфер обмена', GREETING: 'Здравствуйте! Добро пожаловать в наш магазин. Приятных покупок!', @@ -58,8 +62,10 @@ export const SERVER_MESSAGE = { } as const; export const SERVER_MESSAGE_KEYS = { + ADDRESS_ADDED: 'ADDRESS_ADDED', ADDRESS_CHANGED: 'ADDRESS_CHANGED', ADDRESS_DELETED: 'ADDRESS_DELETED', + ADDRESS_STATUS_CHANGED: 'ADDRESS_STATUS_CHANGED', BAD_REQUEST: 'BAD_REQUEST', COPY_TO_CLIPBOARD: 'COPY_TO_CLIPBOARD', GREETING: 'GREETING', diff --git a/src/shared/constants/svg.ts b/src/shared/constants/svg.ts index 20620dd5..9e170d12 100644 --- a/src/shared/constants/svg.ts +++ b/src/shared/constants/svg.ts @@ -1,10 +1,12 @@ const SVG_DETAILS = { + BILL: 'bill', BIN: 'bin', CART: 'cart', CLOSE_EYE: 'closeEye', COPY: 'copy', DARK: 'dark', DELETE: 'delete', + DELIVERY: 'delivery', EDIT: 'edit', FILL_HEART: 'heartFill', GO_DETAILS: 'arrow', @@ -19,6 +21,8 @@ const SVG_DETAILS = { en: 'en', ru: 'ru', }, + TRUCK: 'truck', + WALLET: 'wallet', } as const; export default SVG_DETAILS; diff --git a/src/shared/constants/tooltip.ts b/src/shared/constants/tooltip.ts index afdf16ab..fabd927b 100644 --- a/src/shared/constants/tooltip.ts +++ b/src/shared/constants/tooltip.ts @@ -1,20 +1,38 @@ -const TOOLTIP_TEXT = { +const TOOLTIP_TEXT: Record> = { en: { + ADD_BILLING_ADDRESS: 'Add new billing address', + ADD_SHIPPING_ADDRESS: 'Add new shipping address', DELETE_ADDRESS: 'Delete address', EDIT_ADDRESS: 'Edit address', + EDIT_BILLING_ADDRESS: 'Edit billing address', + EDIT_DEFAULT_BILLING_ADDRESS: 'Edit default billing address', + EDIT_DEFAULT_SHIPPING_ADDRESS: 'Edit default shipping address', EDIT_PASSWORD: 'Edit password', + EDIT_SHIPPING_ADDRESS: 'Edit shipping address', }, ru: { + ADD_BILLING_ADDRESS: 'Добавить новый адрес выставления счетов', + ADD_SHIPPING_ADDRESS: 'Добавить новый адрес доставки', DELETE_ADDRESS: 'Удалить адрес', EDIT_ADDRESS: 'Изменить адрес', + EDIT_BILLING_ADDRESS: 'Изменить адрес выставления счетов', + EDIT_DEFAULT_BILLING_ADDRESS: 'Изменить адрес выставления счетов по умолчанию', + EDIT_DEFAULT_SHIPPING_ADDRESS: 'Изменить адрес доставки по умолчанию', EDIT_PASSWORD: 'Изменить пароль', + EDIT_SHIPPING_ADDRESS: 'Изменить адрес доставки', }, } as const; export const TOOLTIP_TEXT_KEYS = { + ADD_BILLING_ADDRESS: 'ADD_BILLING_ADDRESS', + ADD_SHIPPING_ADDRESS: 'ADD_SHIPPING_ADDRESS', DELETE_ADDRESS: 'DELETE_ADDRESS', EDIT_ADDRESS: 'EDIT_ADDRESS', + EDIT_BILLING_ADDRESS: 'EDIT_BILLING_ADDRESS', + EDIT_DEFAULT_BILLING_ADDRESS: 'EDIT_DEFAULT_BILLING_ADDRESS', + EDIT_DEFAULT_SHIPPING_ADDRESS: 'EDIT_DEFAULT_SHIPPING_ADDRESS', EDIT_PASSWORD: 'EDIT_PASSWORD', + EDIT_SHIPPING_ADDRESS: 'EDIT_SHIPPING_ADDRESS', }; export type TooltipTextKeysType = (typeof TOOLTIP_TEXT_KEYS)[keyof typeof TOOLTIP_TEXT_KEYS]; diff --git a/src/shared/img/svg/bill.svg b/src/shared/img/svg/bill.svg new file mode 100644 index 00000000..1b08f6b7 --- /dev/null +++ b/src/shared/img/svg/bill.svg @@ -0,0 +1 @@ + diff --git a/src/shared/img/svg/delivery.svg b/src/shared/img/svg/delivery.svg new file mode 100644 index 00000000..060676e7 --- /dev/null +++ b/src/shared/img/svg/delivery.svg @@ -0,0 +1 @@ + diff --git a/src/shared/img/svg/edit.svg b/src/shared/img/svg/edit.svg index d2886684..897e0654 100644 --- a/src/shared/img/svg/edit.svg +++ b/src/shared/img/svg/edit.svg @@ -1,5 +1 @@ - - - - - + diff --git a/src/shared/img/svg/truck.svg b/src/shared/img/svg/truck.svg new file mode 100644 index 00000000..4872b693 --- /dev/null +++ b/src/shared/img/svg/truck.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/src/shared/img/svg/wallet.svg b/src/shared/img/svg/wallet.svg new file mode 100644 index 00000000..23a30d27 --- /dev/null +++ b/src/shared/img/svg/wallet.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/src/shared/utils/determineNewAddress.ts b/src/shared/utils/determineNewAddress.ts new file mode 100644 index 00000000..8f686b30 --- /dev/null +++ b/src/shared/utils/determineNewAddress.ts @@ -0,0 +1,127 @@ +/* eslint-disable max-lines-per-function */ +import type UserAddressModel from '@/entities/UserAddress/model/UserAddressModel'; + +import type { Address, User } from '../types/user'; + +import { ADDRESS_TYPE, type AddressTypeType } from '../constants/forms.ts'; + +const determineNewAddress = ( + addressesContainsID: (array: Address[]) => boolean, + defaultContainsID: (defaultAddress: Address | null) => boolean, + user: User, + createAddress: (activeTypes: AddressTypeType[], inactiveTypes?: AddressTypeType[]) => UserAddressModel, +): UserAddressModel => { + const { billingAddress, defaultBillingAddressId, defaultShippingAddressId, shippingAddress } = user; + switch (true) { + case defaultContainsID(defaultBillingAddressId) && + defaultContainsID(defaultShippingAddressId) && + addressesContainsID(billingAddress) && + addressesContainsID(shippingAddress): // billing, shipping, defaultBilling, defaultShipping + return createAddress([ + ADDRESS_TYPE.SHIPPING, + ADDRESS_TYPE.BILLING, + ADDRESS_TYPE.DEFAULT_BILLING, + ADDRESS_TYPE.DEFAULT_SHIPPING, + ]); + + case addressesContainsID(billingAddress) && + defaultContainsID(defaultBillingAddressId) && + defaultContainsID(defaultShippingAddressId): // billing, defaultBilling, defaultShipping + return createAddress( + [ADDRESS_TYPE.BILLING, ADDRESS_TYPE.DEFAULT_BILLING, ADDRESS_TYPE.DEFAULT_SHIPPING], + [ADDRESS_TYPE.SHIPPING], + ); + + case addressesContainsID(shippingAddress) && + defaultContainsID(defaultBillingAddressId) && + defaultContainsID(defaultShippingAddressId): // shipping, defaultBilling, defaultShipping + return createAddress( + [ADDRESS_TYPE.SHIPPING, ADDRESS_TYPE.DEFAULT_BILLING, ADDRESS_TYPE.DEFAULT_SHIPPING], + [ADDRESS_TYPE.BILLING], + ); + + case addressesContainsID(shippingAddress) && + addressesContainsID(billingAddress) && + defaultContainsID(defaultBillingAddressId): // billing, shipping, defaultBilling + return createAddress( + [ADDRESS_TYPE.BILLING, ADDRESS_TYPE.SHIPPING, ADDRESS_TYPE.DEFAULT_BILLING], + [ADDRESS_TYPE.DEFAULT_SHIPPING], + ); + + case addressesContainsID(billingAddress) && + defaultContainsID(defaultShippingAddressId) && + addressesContainsID(shippingAddress): // billing, shipping, defaultShipping + return createAddress( + [ADDRESS_TYPE.BILLING, ADDRESS_TYPE.SHIPPING, ADDRESS_TYPE.DEFAULT_SHIPPING], + [ADDRESS_TYPE.DEFAULT_BILLING], + ); + + case defaultContainsID(defaultBillingAddressId) && defaultContainsID(defaultShippingAddressId): // defaultBilling, defaultShipping + return createAddress( + [ADDRESS_TYPE.DEFAULT_BILLING, ADDRESS_TYPE.DEFAULT_SHIPPING], + [ADDRESS_TYPE.BILLING, ADDRESS_TYPE.SHIPPING], + ); + + case addressesContainsID(billingAddress) && addressesContainsID(shippingAddress): // billing, shipping + return createAddress( + [ADDRESS_TYPE.BILLING, ADDRESS_TYPE.SHIPPING], + [ADDRESS_TYPE.DEFAULT_BILLING, ADDRESS_TYPE.DEFAULT_SHIPPING], + ); + + case addressesContainsID(billingAddress) && defaultContainsID(defaultBillingAddressId): // billing, defaultBilling + return createAddress( + [ADDRESS_TYPE.BILLING, ADDRESS_TYPE.DEFAULT_BILLING], + [ADDRESS_TYPE.SHIPPING, ADDRESS_TYPE.DEFAULT_SHIPPING], + ); + + case addressesContainsID(billingAddress) && defaultContainsID(defaultShippingAddressId): // billing, defaultShipping + return createAddress( + [ADDRESS_TYPE.BILLING, ADDRESS_TYPE.DEFAULT_BILLING], + [ADDRESS_TYPE.SHIPPING, ADDRESS_TYPE.DEFAULT_SHIPPING], + ); + + case addressesContainsID(shippingAddress) && defaultContainsID(defaultShippingAddressId): // shipping, defaultShipping + return createAddress( + [ADDRESS_TYPE.SHIPPING, ADDRESS_TYPE.DEFAULT_SHIPPING], + [ADDRESS_TYPE.BILLING, ADDRESS_TYPE.DEFAULT_BILLING], + ); + + case addressesContainsID(shippingAddress) && defaultContainsID(defaultBillingAddressId): // shipping, defaultBilling + return createAddress( + [ADDRESS_TYPE.SHIPPING, ADDRESS_TYPE.DEFAULT_SHIPPING], + [ADDRESS_TYPE.BILLING, ADDRESS_TYPE.DEFAULT_BILLING], + ); + + case addressesContainsID(billingAddress): // billing + return createAddress( + [ADDRESS_TYPE.BILLING], + [ADDRESS_TYPE.DEFAULT_SHIPPING, ADDRESS_TYPE.SHIPPING, ADDRESS_TYPE.DEFAULT_BILLING], + ); + + case addressesContainsID(shippingAddress): // shipping + return createAddress( + [ADDRESS_TYPE.SHIPPING], + [ADDRESS_TYPE.DEFAULT_SHIPPING, ADDRESS_TYPE.BILLING, ADDRESS_TYPE.DEFAULT_BILLING], + ); + + case defaultContainsID(defaultBillingAddressId): // defaultBilling + return createAddress( + [ADDRESS_TYPE.DEFAULT_BILLING], + [ADDRESS_TYPE.SHIPPING, ADDRESS_TYPE.BILLING, ADDRESS_TYPE.DEFAULT_SHIPPING], + ); + + case defaultContainsID(defaultShippingAddressId): // defaultShipping + return createAddress( + [ADDRESS_TYPE.DEFAULT_SHIPPING], + [ADDRESS_TYPE.SHIPPING, ADDRESS_TYPE.BILLING, ADDRESS_TYPE.DEFAULT_BILLING], + ); + + default: // None + return createAddress( + [], + [ADDRESS_TYPE.SHIPPING, ADDRESS_TYPE.BILLING, ADDRESS_TYPE.DEFAULT_BILLING, ADDRESS_TYPE.DEFAULT_SHIPPING], + ); + } +}; + +export default determineNewAddress; diff --git a/src/shared/utils/hasValue.ts b/src/shared/utils/hasValue.ts new file mode 100644 index 00000000..176d0203 --- /dev/null +++ b/src/shared/utils/hasValue.ts @@ -0,0 +1,5 @@ +export const arrayContainsObjectWithValue = (arr: T[], key: K, value: T[K]): boolean => + arr.some((obj) => obj[key] === value); + +export const objectHasPropertyValue = (obj: T | null, key: K, value: T[K]): boolean => + obj !== null && obj !== undefined && obj[key] === value; diff --git a/src/shared/utils/messageTemplates.ts b/src/shared/utils/messageTemplates.ts index 426c608f..dd3095d5 100644 --- a/src/shared/utils/messageTemplates.ts +++ b/src/shared/utils/messageTemplates.ts @@ -79,13 +79,13 @@ export const minLengthMessage = (minLength: number): string => ? minLengthMessageEn(minLength) : minLengthMessageRu(minLength); -export function addressMessage(type: string, text: string): string { +export function addressMessage(text: string, type?: string): string { switch (type) { case ADDRESS_TYPE.BILLING: return billingAddressMessage(text); case ADDRESS_TYPE.SHIPPING: return shippingAddressMessage(text); default: - return ''; + return text; } } diff --git a/src/shared/utils/userMessage.ts b/src/shared/utils/userMessage.ts index d3fbd7ef..86b8442f 100644 --- a/src/shared/utils/userMessage.ts +++ b/src/shared/utils/userMessage.ts @@ -1,7 +1,9 @@ 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); +const showErrorMessage = (error: unknown): boolean => + error instanceof Error + ? serverMessageModel.showServerMessage(SERVER_MESSAGE_KEYS.BAD_REQUEST, MESSAGE_STATUS.ERROR, error.message) + : 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 0e08b1c6..1f9e044e 100644 --- a/src/widgets/Catalog/model/CatalogModel.ts +++ b/src/widgets/Catalog/model/CatalogModel.ts @@ -175,8 +175,8 @@ class CatalogModel { sizes: sizeCount, totalProductCount: total, }; - } catch { - showErrorMessage(); + } catch (error) { + showErrorMessage(error); } finally { loader.getHTML().remove(); } diff --git a/src/widgets/Header/model/HeaderModel.ts b/src/widgets/Header/model/HeaderModel.ts index 322f8582..ca76a5b2 100644 --- a/src/widgets/Header/model/HeaderModel.ts +++ b/src/widgets/Header/model/HeaderModel.ts @@ -118,7 +118,7 @@ class HeaderModel { serverMessageModel.showServerMessage(SERVER_MESSAGE_KEYS.LANGUAGE_CHANGED, MESSAGE_STATUS.SUCCESS); } } catch (error) { - showErrorMessage(); + showErrorMessage(error); } } else { getStore().dispatch(setCurrentLanguage(newLanguage)); diff --git a/src/widgets/LoginForm/model/LoginFormModel.ts b/src/widgets/LoginForm/model/LoginFormModel.ts index 0cb93755..d871f922 100644 --- a/src/widgets/LoginForm/model/LoginFormModel.ts +++ b/src/widgets/LoginForm/model/LoginFormModel.ts @@ -46,7 +46,7 @@ class LoginFormModel { serverMessageModel.showServerMessage(SERVER_MESSAGE_KEYS.INVALID_EMAIL, MESSAGE_STATUS.ERROR); } }) - .catch(() => showErrorMessage()) + .catch((error) => showErrorMessage(error)) .finally(() => loader.remove()); } diff --git a/src/widgets/UserAddresses/model/UserAddressesModel.ts b/src/widgets/UserAddresses/model/UserAddressesModel.ts index dbbf6d9f..ef6390b8 100644 --- a/src/widgets/UserAddresses/model/UserAddressesModel.ts +++ b/src/widgets/UserAddresses/model/UserAddressesModel.ts @@ -1,35 +1,74 @@ -import type { UserAddressType } from '@/shared/constants/forms.ts'; +import type { AddressTypeType } from '@/shared/constants/forms.ts'; import type { Address, User } from '@/shared/types/user.ts'; import UserAddressModel from '@/entities/UserAddress/model/UserAddressModel.ts'; +import AddressAddModel from '@/features/AddressAdd/model/AddressAddModel.ts'; +import getCustomerModel from '@/shared/API/customer/model/CustomerModel.ts'; +import EventMediatorModel from '@/shared/EventMediator/model/EventMediatorModel.ts'; import modal from '@/shared/Modal/model/ModalModel.ts'; -import { USER_ADDRESS_TYPE } from '@/shared/constants/forms.ts'; +import MEDIATOR_EVENT from '@/shared/constants/events.ts'; +import { ADDRESS_TYPE, DEFAULT_ADDRESS } from '@/shared/constants/forms.ts'; +import clearOutElement from '@/shared/utils/clearOutElement.ts'; +import determineNewAddress from '@/shared/utils/determineNewAddress.ts'; +import { arrayContainsObjectWithValue, objectHasPropertyValue } from '@/shared/utils/hasValue.ts'; +import showErrorMessage from '@/shared/utils/userMessage.ts'; import UserAddressesView from '../view/UserAddressesView.ts'; class UserAddressesModel { - private currentUser: User; - private view: UserAddressesView; constructor(user: User) { - this.currentUser = user; this.view = new UserAddressesView(); - this.createCurrentAddresses(); - this.setCreateBillingAddressHandler(); - this.setCreateShippingAddressHandler(); + this.init(user); } - private createAddresses(addresses: Address[], type: UserAddressType, defaultAddressId = ''): void { + private createCurrentAddresses(user: User): void { + const { addresses } = user; + addresses.forEach((address) => { - this.getHTML().append(new UserAddressModel(this.currentUser, address, type, defaultAddressId).getHTML()); + const KEY_TO_FIND = 'id'; + const addressesContainsID = (array: Address[]): boolean => + arrayContainsObjectWithValue(array, KEY_TO_FIND, address.id); + const defaultContainsID = (defaultAddress: Address | null): boolean => { + if (!defaultAddress) { + return false; + } + return objectHasPropertyValue(defaultAddress, KEY_TO_FIND, address.id); + }; + + const createAddress = (activeTypes: AddressTypeType[], inactiveTypes?: AddressTypeType[]): UserAddressModel => + new UserAddressModel(user, address, activeTypes, inactiveTypes); + + const newAddress = determineNewAddress(addressesContainsID, defaultContainsID, user, createAddress); + + if (newAddress) { + this.view.getAddressesListWrapper().append(newAddress.getHTML()); + } }); } - private createCurrentAddresses(): void { - const { billingAddress, defaultBillingAddressId, defaultShippingAddressId, shippingAddress } = this.currentUser; - this.createAddresses(billingAddress, USER_ADDRESS_TYPE.BILLING, defaultBillingAddressId?.id); - this.createAddresses(shippingAddress, USER_ADDRESS_TYPE.SHIPPING, defaultShippingAddressId?.id); + private init(user: User): void { + this.createCurrentAddresses(user); + this.setCreateBillingAddressHandler(); + this.setCreateShippingAddressHandler(); + + EventMediatorModel.getInstance().subscribe( + MEDIATOR_EVENT.REDRAW_USER_ADDRESSES, + this.redrawUserAddresses.bind(this), + ); + } + + private async redrawUserAddresses(): Promise { + try { + const user = await getCustomerModel().getCurrentUser(); + if (user) { + clearOutElement(this.view.getAddressesListWrapper()); + this.createCurrentAddresses(user); + } + } catch (error) { + showErrorMessage(error); + } } private setCreateBillingAddressHandler(): void { @@ -37,7 +76,9 @@ class UserAddressesModel { .getCreateBillingAddressButton() .getHTML() .addEventListener('click', () => { - modal.show(); // TBD Add new address feature + const newAddressForm = new AddressAddModel(ADDRESS_TYPE.BILLING, DEFAULT_ADDRESS).getHTML(); + modal.show(); + modal.setContent(newAddressForm); }); } @@ -46,7 +87,9 @@ class UserAddressesModel { .getCreateShippingAddressButton() .getHTML() .addEventListener('click', () => { - modal.show(); // TBD Add new address feature + const newAddressForm = new AddressAddModel(ADDRESS_TYPE.SHIPPING, DEFAULT_ADDRESS).getHTML(); + modal.show(); + modal.setContent(newAddressForm); }); } diff --git a/src/widgets/UserAddresses/view/UserAddressesView.ts b/src/widgets/UserAddresses/view/UserAddressesView.ts index 38760ca6..e84f7dde 100644 --- a/src/widgets/UserAddresses/view/UserAddressesView.ts +++ b/src/widgets/UserAddresses/view/UserAddressesView.ts @@ -1,31 +1,63 @@ import ButtonModel from '@/shared/Button/model/ButtonModel.ts'; import getStore from '@/shared/Store/Store.ts'; -import { BUTTON_TEXT, BUTTON_TEXT_KEYS } from '@/shared/constants/buttons.ts'; +import observeStore, { selectCurrentLanguage } from '@/shared/Store/observer.ts'; +import SVG_DETAILS from '@/shared/constants/svg.ts'; +import TOOLTIP_TEXT from '@/shared/constants/tooltip.ts'; import createBaseElement from '@/shared/utils/createBaseElement.ts'; -import observeCurrentLanguage from '@/shared/utils/observeCurrentLanguage.ts'; +import createSVGUse from '@/shared/utils/createSVGUse.ts'; import styles from './userAddressesView.module.scss'; class UserAddressView { + private addressesListWrapper: HTMLUListElement; + private addressesWrapper: HTMLDivElement; + private billingLogo: HTMLDivElement; + private createBillingAddressButton: ButtonModel; private createShippingAddressButton: ButtonModel; + private shippingLogo: HTMLDivElement; + constructor() { + this.billingLogo = this.createBillingLogo(); + this.shippingLogo = this.createShippingLogo(); this.createBillingAddressButton = this.createCreateBillingAddressButton(); this.createShippingAddressButton = this.createCreateShippingAddressButton(); + this.addressesListWrapper = this.createAddressesListWrapper(); this.addressesWrapper = this.createHTML(); } + private createAddressesListWrapper(): HTMLUListElement { + this.addressesListWrapper = createBaseElement({ + cssClasses: [styles.addressesListWrapper], + tag: 'ul', + }); + return this.addressesListWrapper; + } + + private createBillingLogo(): HTMLDivElement { + this.billingLogo = createBaseElement({ cssClasses: [styles.billingLogo], tag: 'div' }); + const svg = document.createElementNS(SVG_DETAILS.SVG_URL, 'svg'); + svg.append(createSVGUse(SVG_DETAILS.BILL)); + this.billingLogo.append(svg); + return this.billingLogo; + } + private createCreateBillingAddressButton(): ButtonModel { this.createBillingAddressButton = new ButtonModel({ classes: [styles.createAddressButton], - text: BUTTON_TEXT[getStore().getState().currentLanguage].NEW_ADDRESS, + title: TOOLTIP_TEXT[getStore().getState().currentLanguage].ADD_BILLING_ADDRESS, }); - observeCurrentLanguage(this.createBillingAddressButton.getHTML(), BUTTON_TEXT, BUTTON_TEXT_KEYS.NEW_ADDRESS); + this.createBillingAddressButton.getHTML().append(this.billingLogo); + + observeStore(selectCurrentLanguage, () => { + this.createBillingAddressButton.getHTML().title = + TOOLTIP_TEXT[getStore().getState().currentLanguage].ADD_BILLING_ADDRESS; + }); return this.createBillingAddressButton; } @@ -33,9 +65,16 @@ class UserAddressView { private createCreateShippingAddressButton(): ButtonModel { this.createShippingAddressButton = new ButtonModel({ classes: [styles.createAddressButton], - text: BUTTON_TEXT[getStore().getState().currentLanguage].NEW_ADDRESS, + title: TOOLTIP_TEXT[getStore().getState().currentLanguage].ADD_SHIPPING_ADDRESS, }); - observeCurrentLanguage(this.createShippingAddressButton.getHTML(), BUTTON_TEXT, BUTTON_TEXT_KEYS.NEW_ADDRESS); + + this.createShippingAddressButton.getHTML().append(this.shippingLogo); + + observeStore(selectCurrentLanguage, () => { + this.createShippingAddressButton.getHTML().title = + TOOLTIP_TEXT[getStore().getState().currentLanguage].ADD_SHIPPING_ADDRESS; + }); + return this.createShippingAddressButton; } @@ -44,11 +83,26 @@ class UserAddressView { cssClasses: [styles.addressesWrapper, styles.hidden], tag: 'div', }); - - this.addressesWrapper.append(this.createBillingAddressButton.getHTML(), this.createShippingAddressButton.getHTML()); + this.addressesWrapper.append( + this.createBillingAddressButton.getHTML(), + this.createShippingAddressButton.getHTML(), + this.addressesListWrapper, + ); return this.addressesWrapper; } + private createShippingLogo(): HTMLDivElement { + this.shippingLogo = createBaseElement({ cssClasses: [styles.shippingLogo], tag: 'div' }); + const svg = document.createElementNS(SVG_DETAILS.SVG_URL, 'svg'); + svg.append(createSVGUse(SVG_DETAILS.DELIVERY)); + this.shippingLogo.append(svg); + return this.shippingLogo; + } + + public getAddressesListWrapper(): HTMLUListElement { + return this.addressesListWrapper; + } + public getCreateBillingAddressButton(): ButtonModel { return this.createBillingAddressButton; } diff --git a/src/widgets/UserAddresses/view/userAddressesView.module.scss b/src/widgets/UserAddresses/view/userAddressesView.module.scss index 7cd18f31..6d9eff5a 100644 --- a/src/widgets/UserAddresses/view/userAddressesView.module.scss +++ b/src/widgets/UserAddresses/view/userAddressesView.module.scss @@ -1,3 +1,5 @@ +@import 'src/app/styles/mixins'; + .addressesWrapper { position: relative; display: grid; @@ -8,14 +10,18 @@ padding: var(--small-offset); width: 60%; height: max-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); + background-color: var(--steam-green-900); gap: var(--small-offset); } +.shippingLogo, +.billingLogo { + @include logo; +} + .hidden { display: none; opacity: 0; @@ -23,31 +29,11 @@ } .createAddressButton { - 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); - } + @include round-btn; - @media (hover: hover) { - &:hover { - background-color: var(--steam-green-700); - } - } + padding: var(--extra-small-offset); +} - &:disabled { - background-color: var(--noble-gray-300); - pointer-events: none; - } +.addressesListWrapper { + grid-column: 2 span; } diff --git a/src/widgets/UserInfo/model/UserInfoModel.ts b/src/widgets/UserInfo/model/UserInfoModel.ts index 4a22a891..4e5d53a2 100644 --- a/src/widgets/UserInfo/model/UserInfoModel.ts +++ b/src/widgets/UserInfo/model/UserInfoModel.ts @@ -32,18 +32,18 @@ class UserInfoModel { EventMediatorModel.getInstance().subscribe(MEDIATOR_EVENT.REDRAW_USER_INFO, this.redrawUserInfo.bind(this)); } - private redrawUserInfo(): void { - getCustomerModel() - .getCurrentUser() - .then((user) => { - if (user) { - this.view.getFirstName().textContent = userInfoName(user.firstName); - this.view.getLastName().textContent = userInfoLastName(user.lastName); - this.view.getEmail().textContent = userInfoEmail(user.email); - this.view.getBirthDate().textContent = userInfoDateOfBirth(user.birthDate); - } - }) - .catch(showErrorMessage); + private async redrawUserInfo(): Promise { + try { + const user = await getCustomerModel().getCurrentUser(); + if (user) { + this.view.getFirstName().textContent = userInfoName(user.firstName); + this.view.getLastName().textContent = userInfoLastName(user.lastName); + this.view.getEmail().textContent = userInfoEmail(user.email); + this.view.getBirthDate().textContent = userInfoDateOfBirth(user.birthDate); + } + } catch (error) { + showErrorMessage(error); + } } private setEditInfoButtonHandler(): boolean { @@ -58,7 +58,7 @@ class UserInfoModel { modal.setContent(personalInfo.getHTML()); } } catch (error) { - showErrorMessage(); + showErrorMessage(error); } }); return true; diff --git a/src/widgets/UserInfo/view/userInfoView.module.scss b/src/widgets/UserInfo/view/userInfoView.module.scss index fc443fa5..ff7ac705 100644 --- a/src/widgets/UserInfo/view/userInfoView.module.scss +++ b/src/widgets/UserInfo/view/userInfoView.module.scss @@ -9,51 +9,25 @@ margin: 0 var(--small-offset); padding: var(--small-offset); width: 60%; - box-shadow: var(--mellow-shadow-100); font: var(--extra-regular-font); line-break: break-all; letter-spacing: var(--one); color: var(--noble-gray-900); - background-color: var(--noble-gray-tr-800); + background-color: var(--steam-green-900); gap: var(--small-offset); } .keyLogo { - display: flex; - margin: 0 auto; - - svg { - z-index: 1; - width: var(--small-offset); - height: var(--small-offset); - fill: var(--noble-gray-900); - transition: 0.2s; - } + @include logo; } .editPasswordButton { - @include green-btn; + @include round-btn; - z-index: 2; grid-column: 2; grid-row: 1; margin-right: 0; margin-left: auto; - border-radius: 50%; - padding: calc(var(--tiny-offset) * 1.5); - height: max-content; - background-color: var(--noble-white-100); - transition: 0.2s; - - @media (hover: hover) { - &:hover { - background-color: var(--white-tr); - - svg { - fill: var(--steam-green-800); - } - } - } } .info { @@ -67,10 +41,6 @@ grid-column: 2 span; grid-row: 6; - margin: 0 auto; - border-radius: var(--medium-br); - padding: calc(var(--small-offset) / 3) var(--small-offset); - height: max-content; } .hidden { From 5c48af6a5a1e1e5ee3e07d02ee6c0440495b21ad Mon Sep 17 00:00:00 2001 From: Max <133934232+Kleostro@users.noreply.github.com> Date: Wed, 29 May 2024 01:29:26 +0300 Subject: [PATCH 11/28] fix(RSS-ECOMM-4_33): reloading product page (#320) fix: reloading product page --- src/app/Router/model/RouterModel.ts | 19 +++++++----- .../Pagination/view/PaginationView.ts | 7 ++--- .../model/ProductFiltersModel.ts | 4 --- .../ProductFilters/view/ProductFiltersView.ts | 30 +++++++++++-------- .../ProductSorts/view/ProductSortsView.ts | 25 ++++++---------- src/shared/Store/Store.ts | 6 +++- src/shared/Store/actions.ts | 4 ++- src/shared/Store/observer.ts | 2 +- src/shared/Store/reducer.ts | 2 +- src/shared/constants/product.ts | 10 +++++++ src/shared/utils/isKeyOf.ts | 4 --- src/widgets/Catalog/model/CatalogModel.ts | 10 +++---- .../ProductInfo/model/ProductInfoModel.ts | 18 ++++++----- .../ProductInfo/view/ProductInfoView.ts | 24 +++++++++++++-- 14 files changed, 98 insertions(+), 67 deletions(-) diff --git a/src/app/Router/model/RouterModel.ts b/src/app/Router/model/RouterModel.ts index 42882c3f..80c3ee34 100644 --- a/src/app/Router/model/RouterModel.ts +++ b/src/app/Router/model/RouterModel.ts @@ -1,5 +1,6 @@ import type { PageParams, PagesType } from '@/shared/types/page'; +import getStore from '@/shared/Store/Store.ts'; import { PAGE_ID } from '@/shared/constants/pages.ts'; import { isValidPath, isValidState } from '@/shared/types/validation/paths.ts'; import formattedText from '@/shared/utils/formattedText.ts'; @@ -118,14 +119,16 @@ class RouterModel { public navigateTo(path: string): void { const currentPage = path.split(DEFAULT_SEGMENT)[PATH_SEGMENTS_TO_KEEP] + DEFAULT_SEGMENT || PAGE_ID.DEFAULT_PAGE; - this.checkPageAndParams(currentPage, path) - .then((check) => { - if (check) { - this.routes.get(currentPage)?.(check.params).catch(showErrorMessage); - history.pushState({ path }, '', `/${path}`); - } - }) - .catch(showErrorMessage); + if (currentPage !== getStore().getState().currentPage) { + this.checkPageAndParams(currentPage, path) + .then((check) => { + if (check) { + this.routes.get(currentPage)?.(check.params).catch(showErrorMessage); + } + }) + .catch(showErrorMessage); + } + history.pushState({ path }, '', `/${path}`); } } diff --git a/src/features/Pagination/view/PaginationView.ts b/src/features/Pagination/view/PaginationView.ts index 4fd9b409..b49380f2 100644 --- a/src/features/Pagination/view/PaginationView.ts +++ b/src/features/Pagination/view/PaginationView.ts @@ -101,11 +101,8 @@ class PaginationView { } private switchStateNavigationButtons(page: number): void { - const prevButton = this.prevPageButton.getHTML(); - const nextButton = this.nextPageButton.getHTML(); - - prevButton.disabled = page === DEFAULT_PAGE; - nextButton.disabled = page === this.pageButtons.length; + this.prevPageButton.getHTML().disabled = page === DEFAULT_PAGE; + this.nextPageButton.getHTML().disabled = page === this.pageButtons.length; } public getHTML(): HTMLDivElement { diff --git a/src/features/ProductFilters/model/ProductFiltersModel.ts b/src/features/ProductFilters/model/ProductFiltersModel.ts index fb6019f4..7b44af5d 100644 --- a/src/features/ProductFilters/model/ProductFiltersModel.ts +++ b/src/features/ProductFilters/model/ProductFiltersModel.ts @@ -12,10 +12,6 @@ class ProductFiltersModel { public getView(): ProductFiltersView { return this.view; } - - public updateParams(params: ProductFiltersParams | null): void { - this.view.updateParams(params); - } } export default ProductFiltersModel; diff --git a/src/features/ProductFilters/view/ProductFiltersView.ts b/src/features/ProductFilters/view/ProductFiltersView.ts index 68b4e197..14986f86 100644 --- a/src/features/ProductFilters/view/ProductFiltersView.ts +++ b/src/features/ProductFilters/view/ProductFiltersView.ts @@ -1,4 +1,4 @@ -import type { SizeProductCount } from '@/shared/API/types/type'; +import type { PriceRange, SizeProductCount } from '@/shared/API/types/type'; import type { Category } from '@/shared/types/product'; import type ProductFiltersParams from '@/shared/types/productFilters'; @@ -629,17 +629,6 @@ class ProductFiltersView { public updateParams(params: ProductFiltersParams | null): void { this.params = params; - this.priceSlider.updateOptions( - { - range: { max: params?.priceRange?.max ?? 0, min: params?.priceRange?.min ?? 0 }, - start: [params?.priceRange?.min ?? 0, params?.priceRange?.max ?? 0], - }, - true, - ); - const fromInput = this.priceInputs.get(PRICE_RANGE_LABEL[getStore().getState().currentLanguage].FROM); - const toInput = this.priceInputs.get(PRICE_RANGE_LABEL[getStore().getState().currentLanguage].TO); - fromInput?.setValue(this.params?.priceRange?.min.toFixed(2) ?? ''); - toInput?.setValue(this.params?.priceRange?.max.toFixed(2) ?? ''); this.categoryCountSpan.forEach((span) => { const currentSpan = span; @@ -651,6 +640,23 @@ class ProductFiltersView { }); this.redrawProductsCount(); } + + public updatePriceRange(params: PriceRange): void { + if (this.params) { + this.params.priceRange = params; + } + this.priceSlider.updateOptions( + { + range: { max: params.max ?? 0, min: params.min ?? 0 }, + start: [params.min ?? 0, params.max ?? 0], + }, + true, + ); + const fromInput = this.priceInputs.get(PRICE_RANGE_LABEL[getStore().getState().currentLanguage].FROM); + const toInput = this.priceInputs.get(PRICE_RANGE_LABEL[getStore().getState().currentLanguage].TO); + fromInput?.setValue(params.min.toFixed(2) ?? ''); + toInput?.setValue(params.max.toFixed(2) ?? ''); + } } export default ProductFiltersView; diff --git a/src/features/ProductSorts/view/ProductSortsView.ts b/src/features/ProductSorts/view/ProductSortsView.ts index 1bd56190..0f2fedce 100644 --- a/src/features/ProductSorts/view/ProductSortsView.ts +++ b/src/features/ProductSorts/view/ProductSortsView.ts @@ -7,7 +7,6 @@ import { DATA_KEYS } from '@/shared/constants/common.ts'; import { SEARCH_PARAMS_FIELD } from '@/shared/constants/product.ts'; import { SORTING_ID, TEXT } from '@/shared/constants/sorting.ts'; import createBaseElement from '@/shared/utils/createBaseElement.ts'; -import { isKeyOfSortField } from '@/shared/utils/isKeyOf.ts'; import styles from './productSortsView.module.scss'; @@ -47,26 +46,20 @@ class ProductSortsView { private createCurrentSortingSpan(): HTMLSpanElement { const selectedSorting = RouterModel.getSearchParams().get(SEARCH_PARAMS_FIELD.FIELD); - const upperText = selectedSorting - ? selectedSorting.toUpperCase() - : TEXT[getStore().getState().currentLanguage].DEFAULT.toUpperCase(); - if (isKeyOfSortField(upperText)) { - this.currentSortingSpan = createBaseElement({ - cssClasses: [styles.currentSortingSpan], - innerContent: TEXT[getStore().getState().currentLanguage][upperText], - tag: 'span', - }); - } + this.currentSortingSpan = createBaseElement({ + cssClasses: [styles.currentSortingSpan], + innerContent: selectedSorting + ? selectedSorting.toUpperCase() + : TEXT[getStore().getState().currentLanguage].DEFAULT.toUpperCase(), + tag: 'span', + }); observeStore(selectCurrentLanguage, () => { const selectedSorting = RouterModel.getSearchParams().get(SEARCH_PARAMS_FIELD.FIELD); - const upperText = selectedSorting + + this.currentSortingSpan.innerText = selectedSorting ? selectedSorting.toUpperCase() : TEXT[getStore().getState().currentLanguage].DEFAULT.toUpperCase(); - - if (isKeyOfSortField(upperText)) { - this.currentSortingSpan.innerText = TEXT[getStore().getState().currentLanguage][upperText]; - } }); return this.currentSortingSpan; diff --git a/src/shared/Store/Store.ts b/src/shared/Store/Store.ts index 3f4fe52e..9b2d996c 100644 --- a/src/shared/Store/Store.ts +++ b/src/shared/Store/Store.ts @@ -4,6 +4,7 @@ import type { Reducer, ReduxStore } from './types'; import initialState from '../constants/initialState.ts'; import { parseToLoad } from '../services/helper.ts'; import { STORAGE_KEY, saveCurrentStateToLocalStorage } from '../services/localStorage.ts'; +import { setCurrentPage } from './actions.ts'; import { rootReducer } from './reducer.ts'; export class Store implements ReduxStore { @@ -28,7 +29,10 @@ export class Store implements ReduxStore { this.rootReducer = rootReducer; // If you have unexpected bugs related to State of the app, or you need to clear out Local Storage to start afresh, comment out the next line, go to the browser tab, clear out the storage manually, and update the page one more time. Then come back here and uncomment it - window.addEventListener('beforeunload', () => saveCurrentStateToLocalStorage(this.state)); + window.addEventListener('beforeunload', () => { + getStore().dispatch(setCurrentPage(null)); + saveCurrentStateToLocalStorage(this.state); + }); } public dispatch(action: A): A { diff --git a/src/shared/Store/actions.ts b/src/shared/Store/actions.ts index ad93ae92..a173276a 100644 --- a/src/shared/Store/actions.ts +++ b/src/shared/Store/actions.ts @@ -79,7 +79,9 @@ export const switchIsUserLoggedIn = ( type: ACTION.SWITCH_IS_USER_LOGGED_IN, }); -export const setCurrentPage = (value: PageIdType): ActionWithPayload => ({ +export const setCurrentPage = ( + value: PageIdType | null, +): ActionWithPayload => ({ payload: value, type: ACTION.SET_CURRENT_PAGE, }); diff --git a/src/shared/Store/observer.ts b/src/shared/Store/observer.ts index 8267f062..c514c9da 100644 --- a/src/shared/Store/observer.ts +++ b/src/shared/Store/observer.ts @@ -54,6 +54,6 @@ export const selectCurrentLanguage = (state: State): string => state.currentLang export const selectIsUserLoggedIn = (state: State): boolean => state.isUserLoggedIn; -export const selectCurrentPage = (state: State): string => state.currentPage; +export const selectCurrentPage = (state: State): null | string => state.currentPage; export default observeStore; diff --git a/src/shared/Store/reducer.ts b/src/shared/Store/reducer.ts index d028af27..2040f620 100644 --- a/src/shared/Store/reducer.ts +++ b/src/shared/Store/reducer.ts @@ -13,7 +13,7 @@ export interface State { authToken: TokenStore | null; billingCountry: string; currentLanguage: LanguageChoiceType; - currentPage: PageIdType; + currentPage: PageIdType | null; isAppThemeLight: boolean; isUserLoggedIn: boolean; shippingCountry: string; diff --git a/src/shared/constants/product.ts b/src/shared/constants/product.ts index d6f1df7f..a4e4f5e3 100644 --- a/src/shared/constants/product.ts +++ b/src/shared/constants/product.ts @@ -29,15 +29,25 @@ export const SEARCH_PARAMS_FIELD = { export const PRODUCT_INFO_TEXT = { en: { + CATEGORY: 'Categories: ', DIFFICULTY: 'Difficulty: ', FULL_DESCRIPTION: 'Full description:', SHORT_DESCRIPTION: 'Short description:', SIZE: 'Size:', }, ru: { + CATEGORY: 'Категории: ', DIFFICULTY: 'Сложность: ', FULL_DESCRIPTION: 'Полное описание:', SHORT_DESCRIPTION: 'Краткое описание:', SIZE: 'Размер:', }, } as const; + +export const PRODUCT_INFO_TEXT_KEYS = { + CATEGORY: 'CATEGORY', + DIFFICULTY: 'DIFFICULTY', + FULL_DESCRIPTION: 'FULL_DESCRIPTION', + SHORT_DESCRIPTION: 'SHORT_DESCRIPTION', + SIZE: 'SIZE', +} as const; diff --git a/src/shared/utils/isKeyOf.ts b/src/shared/utils/isKeyOf.ts index c09a3278..378cd137 100644 --- a/src/shared/utils/isKeyOf.ts +++ b/src/shared/utils/isKeyOf.ts @@ -1,11 +1,7 @@ import type MetaFilters from '../types/productFilters.ts'; import type { UserCredentials } from '../types/user'; -import { TEXT_KEYS, type TextKeysType } from '../constants/sorting.ts'; - export const isKeyOfUserData = (context: UserCredentials, key: string): key is keyof UserCredentials => Object.hasOwnProperty.call(context, key); export const isKeyOfMetaFilters = (context: MetaFilters, key: string): key is keyof MetaFilters => key in context; - -export const isKeyOfSortField = (key: string): key is TextKeysType => key in TEXT_KEYS; diff --git a/src/widgets/Catalog/model/CatalogModel.ts b/src/widgets/Catalog/model/CatalogModel.ts index 1f9e044e..c3cba80d 100644 --- a/src/widgets/Catalog/model/CatalogModel.ts +++ b/src/widgets/Catalog/model/CatalogModel.ts @@ -95,25 +95,25 @@ class CatalogModel { productList.innerHTML = ''; const options = await this.getOptions(); const productsInfo = await this.getProductsInfo(options); + this.pagination?.getHTML().remove(); if (productsInfo?.products?.length) { const shoppingList = await getShoppingListModel().getShoppingList(); const cart = await getCartModel().getCart(); - productList.innerHTML = ''; productsInfo.products.forEach((productData) => { const product = new ProductCardModel(productData, this.currentSize, shoppingList, cart); productList.append(product.getHTML()); }); this.view.switchEmptyList(!productsInfo?.products?.length); - this.pagination?.getHTML().remove(); this.pagination = new PaginationModel( - { productTotalCount: productsInfo?.totalProductCount ?? 0, productsPerPageCount: PRODUCT_LIMIT }, + { productTotalCount: productsInfo?.totalProductCount, productsPerPageCount: PRODUCT_LIMIT }, this.setCurrentPage.bind(this), ); this.pagination.getView().setSelectedButton(options.page ?? DEFAULT_PAGE); this.view.getRightTopWrapper().append(this.pagination.getHTML()); } - - this.productFilters?.updateParams(productsInfo); + this.productFilters?.getView().updateParams(productsInfo); + const priceRange = await getProductModel().getPriceRange(); + this.productFilters?.getView().updatePriceRange(priceRange); this.view.switchEmptyList(!productsInfo?.products?.length); } diff --git a/src/widgets/ProductInfo/model/ProductInfoModel.ts b/src/widgets/ProductInfo/model/ProductInfoModel.ts index 78c6ce94..9ccce257 100644 --- a/src/widgets/ProductInfo/model/ProductInfoModel.ts +++ b/src/widgets/ProductInfo/model/ProductInfoModel.ts @@ -154,13 +154,17 @@ class ProductInfoModel { sizeButton.getHTML().addEventListener('click', () => { const currentVariant = this.params.variant.find(({ size }) => size === sizeButton.getHTML().textContent); - if (currentVariant) { - const path = `${buildPathName(PAGE_ID.PRODUCT_PAGE, this.params.key, { - size: [currentVariant.size ?? this.params.variant[0].size], - })}`; - RouterModel.getInstance().navigateTo(path); - modal.hide(); - } + const path = `${buildPathName(PAGE_ID.PRODUCT_PAGE, this.params.key, { + size: [currentVariant?.size ?? this.params.variant[0].size], + })}`; + RouterModel.getInstance().navigateTo(path); + modal.hide(); + this.currentVariant = currentVariant ?? this.params.variant[0]; + this.params.currentSize = currentVariant?.size ?? this.params.variant[0].size; + this.view.updateParams(this.params); + this.price.getHTML().remove(); + this.price = new ProductPriceModel(this.currentVariant); + this.view.getRightWrapper().append(this.price.getHTML()); }); }); } diff --git a/src/widgets/ProductInfo/view/ProductInfoView.ts b/src/widgets/ProductInfo/view/ProductInfoView.ts index 8dbd3c90..0cbfbcf2 100644 --- a/src/widgets/ProductInfo/view/ProductInfoView.ts +++ b/src/widgets/ProductInfo/view/ProductInfoView.ts @@ -12,11 +12,12 @@ import { BUTTON_TEXT } from '@/shared/constants/buttons.ts'; import { AUTOCOMPLETE_OPTION, LANGUAGE_CHOICE } from '@/shared/constants/common.ts'; import { INPUT_TYPE } from '@/shared/constants/forms.ts'; import { MESSAGE_STATUS, SERVER_MESSAGE_KEYS } from '@/shared/constants/messages.ts'; -import { PRODUCT_INFO_TEXT } from '@/shared/constants/product.ts'; +import { PRODUCT_INFO_TEXT, PRODUCT_INFO_TEXT_KEYS } from '@/shared/constants/product.ts'; import { LOADER_SIZE } from '@/shared/constants/sizes.ts'; import SVG_DETAILS from '@/shared/constants/svg.ts'; import createBaseElement from '@/shared/utils/createBaseElement.ts'; import createSVGUse from '@/shared/utils/createSVGUse.ts'; +import observeCurrentLanguage from '@/shared/utils/observeCurrentLanguage.ts'; import showErrorMessage from '@/shared/utils/userMessage.ts'; import './productInfoView.scss'; @@ -136,10 +137,12 @@ class ProductInfoView { private createCategoriesSpan(): HTMLSpanElement { this.categoriesSpan = createBaseElement({ cssClasses: ['categoriesSpan'], - innerContent: 'Categories: ', + innerContent: PRODUCT_INFO_TEXT[getStore().getState().currentLanguage].CATEGORY, tag: 'span', }); + observeCurrentLanguage(this.categoriesSpan, PRODUCT_INFO_TEXT, PRODUCT_INFO_TEXT_KEYS.CATEGORY); + const category = this.params.category[0].parent?.name[Number(getStore().getState().currentLanguage === LANGUAGE_CHOICE.RU)].value; const subcategory = @@ -242,6 +245,8 @@ class ProductInfoView { tag: 'span', }); + observeCurrentLanguage(difficultySpan, PRODUCT_INFO_TEXT, PRODUCT_INFO_TEXT_KEYS.DIFFICULTY); + difficultySpan.append(...this.createDifficultyPoints()); this.rightWrapper.append(difficultySpan); } @@ -317,6 +322,16 @@ class ProductInfoView { button.setDisabled(); button.getHTML().classList.add('selected'); } + + button.getHTML().addEventListener('click', () => { + this.sizeButtons.forEach((btn) => { + btn.setEnabled(); + btn.getHTML().classList.remove('selected'); + }); + button.getHTML().classList.add('selected'); + button.setDisabled(); + }); + this.sizeButtons.push(button); return button; } @@ -490,6 +505,11 @@ class ProductInfoView { this.switchToCartButton.getHTML().textContent = BUTTON_TEXT[getStore().getState().currentLanguage].ADD_PRODUCT; } } + + public updateParams(params: ProductInfoParams): void { + this.params = params; + this.hasProductInToCart(); + } } export default ProductInfoView; From b6c685f04b9952b90720072acf696b0c4017927b Mon Sep 17 00:00:00 2001 From: Yuliya Kursevich <54816946+YulikK@users.noreply.github.com> Date: Wed, 29 May 2024 14:16:00 +0200 Subject: [PATCH 12/28] feat(RSS-ECOMM-4_10)/cart (#321) * feat: modify quantity * feat: delete button for mobile * feat: add clear cart * feat: language and message * fix: src img --- package.json | 2 + src/app/styles/variables.scss | 2 +- src/pages/CartPage/model/CartPageModel.ts | 69 +++++- src/pages/CartPage/view/CartPageView.ts | 232 ++++++++++++++---- .../CartPage/view/cartPageView.module.scss | 69 ++++-- src/shared/API/cart/CartApi.ts | 23 ++ src/shared/API/cart/model/CartModel.ts | 42 ++++ src/shared/constants/common.ts | 2 + src/shared/constants/messages.ts | 6 + src/shared/types/cart.ts | 8 + .../Footer/view/footerView.module.scss | 1 - .../Header/view/headerView.module.scss | 10 +- .../ProductOrder/model/ProductOrderModel.ts | 92 +++---- .../ProductOrder/view/ProductOrderView.ts | 131 +++++++--- .../view/productOrderView.module.scss | 71 ++++-- 15 files changed, 593 insertions(+), 167 deletions(-) diff --git a/package.json b/package.json index a67e8a92..136cf842 100644 --- a/package.json +++ b/package.json @@ -64,8 +64,10 @@ "@commercetools/sdk-client-v2": "^2.5.0", "@commercetools/sdk-middleware-auth": "^7.0.1", "@commercetools/sdk-middleware-http": "^7.0.4", + "@types/hammerjs": "^2.0.45", "@types/js-cookie": "^3.0.6", "autoprefixer": "^10.4.19", + "hammerjs": "^2.0.8", "isomorphic-fetch": "^3.0.0", "js-cookie": "^3.0.5", "materialize-css": "^1.0.0-rc.2", diff --git a/src/app/styles/variables.scss b/src/app/styles/variables.scss index 80f0fe34..6fc08288 100644 --- a/src/app/styles/variables.scss +++ b/src/app/styles/variables.scss @@ -23,7 +23,7 @@ // shadows --mellow-shadow-050: 0 0 20px rgb(0 0 0 / 6%); --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, + --mellow-shadow-200: hsl(0deg 0% 100% / 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, diff --git a/src/pages/CartPage/model/CartPageModel.ts b/src/pages/CartPage/model/CartPageModel.ts index 62a39a0f..3938c237 100644 --- a/src/pages/CartPage/model/CartPageModel.ts +++ b/src/pages/CartPage/model/CartPageModel.ts @@ -4,6 +4,7 @@ import type { Page } from '@/shared/types/page.ts'; import getCartModel from '@/shared/API/cart/model/CartModel.ts'; import getStore from '@/shared/Store/Store.ts'; import { setCurrentPage } from '@/shared/Store/actions.ts'; +import observeStore, { selectCurrentLanguage } from '@/shared/Store/observer.ts'; import { PAGE_ID } from '@/shared/constants/pages.ts'; import showErrorMessage from '@/shared/utils/userMessage.ts'; import ProductOrderModel from '@/widgets/ProductOrder/model/ProductOrderModel.ts'; @@ -11,14 +12,59 @@ import ProductOrderModel from '@/widgets/ProductOrder/model/ProductOrderModel.ts import CartPageView from '../view/CartPageView.ts'; class CartPageModel implements Page { + private addDiscountHandler = async (discountCode: string): Promise => { + if (discountCode.trim()) { + this.cart = await getCartModel().addCoupon(discountCode); + this.view.updateTotal(this.cart); + + getCartModel() + .addCoupon(discountCode) + .then((cart) => { + this.cart = cart; + this.view.updateTotal(this.cart); + }) + .catch(showErrorMessage); + } + }; + private cart: Cart | null = null; + private changeProductHandler = (cart: Cart): void => { + this.cart = cart; + this.productsItem = this.productsItem.filter((productItem) => { + const searchEl = this.cart?.products.find((item) => item.lineItemId === productItem.getProduct().lineItemId); + if (!searchEl) { + productItem.getHTML().remove(); + return false; + } + return true; + }); + + if (!this.productsItem.length) { + this.view.renderEmpty(); + } + this.view.updateTotal(this.cart); + }; + + private clearCart = async (): Promise => { + this.cart = await getCartModel().clearCart(); + this.productsItem = this.productsItem.filter((productItem) => { + const searchEl = this.cart?.products.find((item) => item.lineItemId === productItem.getProduct().lineItemId); + if (!searchEl) { + productItem.getHTML().remove(); + return false; + } + return true; + }); + this.renderCart(); + }; + private productsItem: ProductOrderModel[] = []; private view: CartPageView; constructor(parent: HTMLDivElement) { - this.view = new CartPageView(parent); + this.view = new CartPageView(parent, this.clearCart, this.addDiscountHandler); this.init().catch(showErrorMessage); } @@ -26,11 +72,24 @@ class CartPageModel implements Page { private async init(): Promise { getStore().dispatch(setCurrentPage(PAGE_ID.CART_PAGE)); this.cart = await getCartModel().addProductInfo(); - this.cart.products.forEach((product) => { - this.productsItem.push(new ProductOrderModel(product)); - }); + this.renderCart(); + observeStore(selectCurrentLanguage, () => this.view.updateLanguage()); + } + + private renderCart(): void { + if (this.cart) { + this.cart.products.forEach((product) => { + this.productsItem.push(new ProductOrderModel(product, this.changeProductHandler)); + }); - this.view.renderCart(this.productsItem); + if (this.productsItem.length) { + this.view.renderCart(this.productsItem); + this.view.updateTotal(this.cart); + } else { + this.view.renderEmpty(); + this.view.updateTotal(this.cart); + } + } } public getHTML(): HTMLDivElement { diff --git a/src/pages/CartPage/view/CartPageView.ts b/src/pages/CartPage/view/CartPageView.ts index 7897f6ae..38a581b5 100644 --- a/src/pages/CartPage/view/CartPageView.ts +++ b/src/pages/CartPage/view/CartPageView.ts @@ -1,12 +1,55 @@ +import type { LanguageChoiceType } from '@/shared/constants/common'; +import type { Cart } from '@/shared/types/cart'; +import type { languageVariants } from '@/shared/types/common'; import type ProductOrderModel from '@/widgets/ProductOrder/model/ProductOrderModel'; +import RouterModel from '@/app/Router/model/RouterModel.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 { INPUT_TYPE } from '@/shared/constants/forms.ts'; +import { PAGE_ID } from '@/shared/constants/pages.ts'; import createBaseElement from '@/shared/utils/createBaseElement.ts'; import styles from './cartPageView.module.scss'; +type ClearCallback = () => void; +type DiscountCallback = (discount: string) => void; +type textElementsType = { + element: HTMLAnchorElement | HTMLButtonElement | HTMLParagraphElement | HTMLTableCellElement; + textItem: languageVariants; +}; + +const TITTLE = { + BUTTON_CHECKOUT: { en: 'Proceed To Checkout', ru: 'Оформить заказ' }, + BUTTON_COUPON: { en: 'Apply', ru: 'Применить' }, + CART_TOTAL: { en: 'Cart Totals', ru: 'Итого по корзине' }, + CLEAR: { en: 'Clear all', ru: 'Очистить' }, + CONTINUE: { en: 'Continue Shopping', ru: 'Продолжить покупки' }, + COUPON_APPLY: { en: 'Coupon Apply', ru: 'Применить купон' }, + COUPON_DISCOUNT: { en: 'Coupon Discount', ru: 'Скидка по купону' }, + EMPTY: { + en: `Oops! Looks like you haven't added the item to your cart yet.`, + ru: `Ой! Похоже, вы еще не добавили товар в корзину.`, + }, + INPUT_COUPON: { en: 'Enter coupon here...', ru: 'Введите купон здесь...' }, + PRICE: { en: 'Price', ru: 'Цена' }, + PRODUCT: { en: 'Product', ru: 'Продукт' }, + QUANTITY: { en: 'Quantity', ru: 'Количество' }, + SUBTOTAL: { en: 'Subtotal', ru: 'Сумма' }, + TOTAL: { en: 'Total', ru: 'Итого' }, +}; class CartPageView { + private addDiscountCallback: DiscountCallback; + + private clearCallback: ClearCallback; + + private discount: HTMLParagraphElement; + + private empty: HTMLDivElement; + + private language: LanguageChoiceType; + private page: HTMLDivElement; private parent: HTMLDivElement; @@ -15,20 +58,35 @@ class CartPageView { private productWrap: HTMLDivElement; + private subTotal: HTMLParagraphElement; + private table: HTMLTableElement | null = null; private tableBody: HTMLTableSectionElement | null = null; + private textElement: textElementsType[] = []; + + private total: HTMLParagraphElement; + private totalWrap: HTMLDivElement; - constructor(parent: HTMLDivElement) { + constructor(parent: HTMLDivElement, clearCallback: ClearCallback, addDiscountCallback: DiscountCallback) { + this.language = getStore().getState().currentLanguage; this.parent = parent; this.parent.innerHTML = ''; + this.clearCallback = clearCallback; + this.addDiscountCallback = addDiscountCallback; this.page = this.createPageHTML(); this.productWrap = this.createWrapHTML(); this.productWrap.classList.add(styles.products); + this.subTotal = createBaseElement({ cssClasses: [styles.totalTitle], tag: 'p' }); + this.total = createBaseElement({ cssClasses: [styles.totalPrice], tag: 'p' }); + this.discount = createBaseElement({ cssClasses: [styles.title], tag: 'p' }); this.totalWrap = this.createWrapHTML(); this.totalWrap.classList.add(styles.total); + this.page.append(this.productWrap); + this.page.append(this.totalWrap); + this.empty = this.createEmptyHTML(); window.scrollTo(0, 0); } @@ -38,26 +96,30 @@ class CartPageView { const tr = createBaseElement({ cssClasses: [styles.tr, styles.head], tag: 'tr' }); const thImage = createBaseElement({ cssClasses: [styles.th, styles.imgCell, styles.mainText], - innerContent: 'Product', + innerContent: TITTLE.PRODUCT[this.language], tag: 'th', }); + this.textElement.push({ element: thImage, textItem: TITTLE.PRODUCT }); const thProduct = createBaseElement({ cssClasses: [styles.th, styles.nameCell, styles.mainText], tag: 'th' }); const thPrice = createBaseElement({ cssClasses: [styles.th, styles.priceCell, styles.mainText], - innerContent: 'Price', + innerContent: TITTLE.PRICE[this.language], tag: 'th', }); + this.textElement.push({ element: thPrice, textItem: TITTLE.PRICE }); const thQuantity = createBaseElement({ cssClasses: [styles.th, styles.quantityCell, styles.mainText], - innerContent: 'Quantity', + innerContent: TITTLE.QUANTITY[this.language], tag: 'th', }); + this.textElement.push({ element: thQuantity, textItem: TITTLE.QUANTITY }); const thTotal = createBaseElement({ cssClasses: [styles.th, styles.totalCell, styles.mainText], - innerContent: 'Total', + innerContent: TITTLE.TOTAL[this.language], tag: 'th', }); - const thDelete = createBaseElement({ cssClasses: [styles.th, styles.deleteCell, styles.mainText], tag: 'th' }); + this.textElement.push({ element: thTotal, textItem: TITTLE.TOTAL }); + const thDelete = this.createDeleCell(); this.tableBody = createBaseElement({ cssClasses: [styles.tbody], tag: 'tbody' }); this.table.append(thead, this.tableBody); thead.append(tr); @@ -65,31 +127,31 @@ class CartPageView { this.productWrap.append(this.table); } - private addTotalInfo(totalPriceSum: number): void { + private addTotalInfo(): void { const title = createBaseElement({ cssClasses: [styles.totalTitle, styles.border, styles.mobileHide], - innerContent: 'Cart Totals', + innerContent: TITTLE.CART_TOTAL[this.language], tag: 'p', }); + this.textElement.push({ element: title, textItem: TITTLE.CART_TOTAL }); const couponTitle = createBaseElement({ cssClasses: [styles.title, styles.mobileHide], - innerContent: 'Coupon Apply', + innerContent: TITTLE.COUPON_APPLY[this.language], tag: 'p', }); + this.textElement.push({ element: couponTitle, textItem: TITTLE.COUPON_APPLY }); const couponWrap = this.createCouponHTML(); - const subtotalWrap = this.createSubtotalHTML(totalPriceSum); + const subtotalWrap = this.createSubtotalHTML(); const discountWrap = this.createDiscountHTML(); - const totalWrap = this.createTotalHTML(totalPriceSum); + const totalWrap = this.createTotalHTML(); const finalButton = createBaseElement({ - cssClasses: [styles.button], - innerContent: 'Proceed To Checkout', + cssClasses: [styles.button, styles.checkoutBtn], + innerContent: TITTLE.BUTTON_CHECKOUT[this.language], tag: 'button', }); - const continueLink = createBaseElement({ - cssClasses: [styles.continue, styles.mobileHide], - innerContent: 'Continue Shopping', - tag: 'a', - }); + this.textElement.push({ element: finalButton, textItem: TITTLE.BUTTON_CHECKOUT }); + const continueLink = this.createCatalogLinkHTML(); + continueLink.getHTML().classList.add(styles.mobileHide); this.totalWrap.append( title, couponTitle, @@ -98,32 +160,94 @@ class CartPageView { discountWrap, totalWrap, finalButton, - continueLink, + continueLink.getHTML(), ); } + private createCatalogLinkHTML(): LinkModel { + const link = new LinkModel({ + attrs: { + href: PAGE_ID.CATALOG_PAGE, + }, + classes: [styles.continue], + text: TITTLE.CONTINUE[this.language], + }); + this.textElement.push({ element: link.getHTML(), textItem: TITTLE.CONTINUE }); + + link.getHTML().addEventListener('click', (event) => { + event.preventDefault(); + RouterModel.getInstance().navigateTo(PAGE_ID.CATALOG_PAGE); + }); + return link; + } + private createCouponHTML(): HTMLDivElement { const couponWrap = createBaseElement({ cssClasses: [styles.totalWrap], tag: 'div' }); const couponInput = new InputModel({ autocomplete: 'off', id: 'coupon', - placeholder: 'Enter coupon here...', + placeholder: TITTLE.INPUT_COUPON[this.language], type: INPUT_TYPE.TEXT, }); - couponInput.getHTML().classList.add('couponInput'); - const couponButton = createBaseElement({ cssClasses: [styles.button], innerContent: 'Apply', tag: 'button' }); + couponInput.getHTML().classList.add(styles.couponInput); + this.textElement.push({ element: couponInput.getHTML(), textItem: TITTLE.INPUT_COUPON }); + const couponButton = createBaseElement({ + cssClasses: [styles.button, styles.applyBtn], + innerContent: TITTLE.BUTTON_COUPON[this.language], + tag: 'button', + }); + this.textElement.push({ element: couponButton, textItem: TITTLE.BUTTON_COUPON }); + couponButton.addEventListener('click', (evn: Event) => { + evn.preventDefault(); + this.addDiscountCallback(couponInput.getHTML().value); + couponInput.getHTML().value = ''; + }); couponWrap.append(couponInput.getHTML(), couponButton); return couponWrap; } + private createDeleCell(): HTMLTableCellElement { + const tdDelete = createBaseElement({ cssClasses: [styles.th, styles.deleteCell, styles.mainText], tag: 'th' }); + const clear = new LinkModel({ + classes: [styles.continue, styles.clear], + text: TITTLE.CLEAR[this.language], + }); + + this.textElement.push({ element: clear.getHTML(), textItem: TITTLE.CLEAR }); + clear.getHTML().addEventListener('click', (event) => { + event.preventDefault(); + this.clearCallback(); + }); + tdDelete.append(clear.getHTML()); + return tdDelete; + } + private createDiscountHTML(): HTMLDivElement { const discountWrap = createBaseElement({ cssClasses: [styles.totalWrap], tag: 'div' }); - const discountTitle = createBaseElement({ cssClasses: [styles.title], innerContent: 'Coupon Discount', tag: 'p' }); - const discountValue = createBaseElement({ cssClasses: [styles.title], innerContent: '(-) 00.00', tag: 'p' }); - discountWrap.append(discountTitle, discountValue); + const discountTitle = createBaseElement({ + cssClasses: [styles.title], + innerContent: TITTLE.COUPON_DISCOUNT[this.language], + tag: 'p', + }); + discountWrap.append(discountTitle, this.discount); + this.textElement.push({ element: discountTitle, textItem: TITTLE.COUPON_DISCOUNT }); return discountWrap; } + private createEmptyHTML(): HTMLDivElement { + const empty = createBaseElement({ cssClasses: [styles.empty, styles.hide], tag: 'div' }); + const emptyTitle = createBaseElement({ + cssClasses: [styles.emptyTitle], + innerContent: TITTLE.EMPTY[this.language], + tag: 'p', + }); + this.textElement.push({ element: emptyTitle, textItem: TITTLE.EMPTY }); + const continueLink = this.createCatalogLinkHTML(); + empty.append(emptyTitle, continueLink.getHTML()); + this.page.append(empty); + return empty; + } + private createPageHTML(): HTMLDivElement { this.page = createBaseElement({ cssClasses: [styles.cartPage], @@ -135,27 +259,27 @@ class CartPageView { return this.page; } - private createSubtotalHTML(totalPriceSum: number): HTMLDivElement { + private createSubtotalHTML(): HTMLDivElement { const subtotalWrap = createBaseElement({ cssClasses: [styles.totalWrap], tag: 'div' }); - const subtotalTitle = createBaseElement({ cssClasses: [styles.title], innerContent: 'Subtotal', tag: 'p' }); - const subtotalValue = createBaseElement({ - cssClasses: [styles.totalTitle], - innerContent: `$ ${totalPriceSum.toFixed(2)}`, + const subtotalTitle = createBaseElement({ + cssClasses: [styles.title], + innerContent: TITTLE.SUBTOTAL[this.language], tag: 'p', }); - subtotalWrap.append(subtotalTitle, subtotalValue); + subtotalWrap.append(subtotalTitle, this.subTotal); + this.textElement.push({ element: subtotalTitle, textItem: TITTLE.SUBTOTAL }); return subtotalWrap; } - private createTotalHTML(totalPriceSum: number): HTMLDivElement { + private createTotalHTML(): HTMLDivElement { const totalWrap = createBaseElement({ cssClasses: [styles.totalWrap], tag: 'div' }); - const totalTitle = createBaseElement({ cssClasses: [styles.totalTitle], innerContent: 'Total', tag: 'p' }); - const totalValue = createBaseElement({ - cssClasses: [styles.totalPrice], - innerContent: `$ ${totalPriceSum.toFixed(2)}`, + const totalTitle = createBaseElement({ + cssClasses: [styles.totalTitle], + innerContent: TITTLE.TOTAL[this.language], tag: 'p', }); - totalWrap.append(totalTitle, totalValue); + totalWrap.append(totalTitle, this.total); + this.textElement.push({ element: totalTitle, textItem: TITTLE.TOTAL }); return totalWrap; } @@ -165,8 +289,6 @@ class CartPageView { tag: 'div', }); - this.page.append(wrap); - return wrap; } @@ -177,12 +299,40 @@ class CartPageView { public renderCart(productsItem: ProductOrderModel[]): void { this.productWrap.innerHTML = ''; this.totalWrap.innerHTML = ''; + this.productWrap.classList.remove(styles.hide); + this.totalWrap.classList.remove(styles.hide); + this.empty.classList.add(styles.hide); this.productRow.map((productEl) => productEl.remove()); this.productRow = []; this.addTableHeader(); productsItem.forEach((productEl) => this.tableBody?.append(productEl.getHTML())); - const totalPriceSum = productsItem.reduce((sum, product) => sum + product.getProduct().totalPrice, 0); - this.addTotalInfo(totalPriceSum); + this.addTotalInfo(); + } + + public renderEmpty(): void { + this.productWrap.innerHTML = ''; + this.totalWrap.innerHTML = ''; + this.productWrap.classList.add(styles.hide); + this.totalWrap.classList.add(styles.hide); + this.empty.classList.remove(styles.hide); + } + + public updateLanguage(): void { + this.language = getStore().getState().currentLanguage; + this.textElement.forEach((textEl) => { + const elHTML = textEl.element; + if (elHTML instanceof HTMLInputElement) { + elHTML.placeholder = textEl.textItem[this.language]; + } else { + elHTML.textContent = textEl.textItem[this.language]; + } + }); + } + + public updateTotal(cart: Cart): void { + this.subTotal.innerHTML = `$ ${(cart.total + cart.discounts).toFixed(2)}`; + this.discount.innerHTML = `-$ ${cart.discounts.toFixed(2)}`; + this.total.innerHTML = `$ ${cart.total.toFixed(2)}`; } } export default CartPageView; diff --git a/src/pages/CartPage/view/cartPageView.module.scss b/src/pages/CartPage/view/cartPageView.module.scss index 48a23966..b7acab42 100644 --- a/src/pages/CartPage/view/cartPageView.module.scss +++ b/src/pages/CartPage/view/cartPageView.module.scss @@ -1,9 +1,15 @@ +@import 'src/app/styles/mixins'; + .cartPage { display: flex; flex-grow: 1; padding: 0 var(--small-offset); animation: show 0.2s ease-out forwards; gap: 2%; + + @media (max-width: 768px) { + flex-direction: column; + } } @keyframes show { @@ -29,16 +35,16 @@ flex-shrink: 1; @media (max-width: 768px) { - position: fixed; + position: sticky; left: 0; bottom: 0; z-index: 100; margin-bottom: var(--tiny-offset); - border-radius: var(--small-offset) var(--small-offset) 0 0; + border-radius: var(--large-br); padding: var(--small-offset); width: 100%; - box-shadow: var(--mellow-shadow-600); - background-color: var(--noble-gray-1000); + box-shadow: var(--mellow-shadow-700); + background-color: var(--white-tr); backdrop-filter: blur(10px); } } @@ -49,10 +55,6 @@ .thead { width: 100%; - - @media (max-width: 768px) { - display: none; - } } .mainText { @@ -65,12 +67,16 @@ padding: var(--tiny-offset) 0; font: var(--bold-font); color: var(--noble-gray-400); + + @media (max-width: 768px) { + display: none; + } } .tr { display: grid; grid-gap: 0; - grid-template-columns: calc(var(--tiny-offset) * 7) 2fr 1fr 1fr 1fr calc(var(--tiny-offset) * 7); + grid-template-columns: calc(var(--tiny-offset) * 10) 2fr 1fr 1fr 1fr calc(var(--tiny-offset) * 7); width: 100%; @media (max-width: 768px) { @@ -89,6 +95,7 @@ border-bottom: var(--one) solid var(--steam-green-800); } +.emptyTitle, .totalTitle { padding: var(--tiny-offset) 0; font: var(--bold-font); @@ -109,7 +116,7 @@ display: flex; align-items: stretch; justify-content: space-between; - max-width: calc(var(--extra-large-offset) * 3.8); // 380px + max-width: calc(var(--extra-large-offset) * 3.8); @media (max-width: 768px) { width: 100%; @@ -125,18 +132,28 @@ .couponInput { flex-grow: 1; - border: 1px solid var(--steam-green-800); - border-radius: 3px 0 0 3px; - padding: var(--tiny-offset) 0; + border: var(--one) solid var(--steam-green-800); + border-radius: var(--small-br) 0 0 var(--small-br); + padding: var(--tiny-offset); font: var(--regular-font); color: var(--noble-gray-800); } .button { + @include green-btn; + padding: var(--tiny-offset); - font: var(--bold-font); color: var(--noble-gray-200); - background-color: var(--steam-green-800); +} + +.applyBtn { + border-radius: 0 var(--small-br) var(--small-br) 0; +} + +.checkoutBtn { + margin: 0 auto; + border-radius: var(--small-br); + color: var(--noble-gray-200); } .continue { @@ -144,6 +161,7 @@ padding: var(--tiny-offset) 0; font: var(--regular-font); color: var(--steam-green-800); + cursor: pointer; } .mobileHide { @@ -151,3 +169,24 @@ display: none; } } + +.deleteCell { + @media (max-width: 768px) { + display: flex; + align-items: center; + justify-content: center; + grid-area: 1/3; + } +} + +.empty { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + width: 100%; +} + +.hide { + display: none; +} diff --git a/src/shared/API/cart/CartApi.ts b/src/shared/API/cart/CartApi.ts index 7700a227..8181a130 100644 --- a/src/shared/API/cart/CartApi.ts +++ b/src/shared/API/cart/CartApi.ts @@ -4,8 +4,11 @@ import type { Cart as CartResponse, ClientResponse, MyCartDraft, + MyCartUpdateAction, } from '@commercetools/platform-sdk'; +import serverMessageModel from '@/shared/ServerMessage/model/ServerMessageModel.ts'; +import { MESSAGE_STATUS, SERVER_MESSAGE_KEYS } from '@/shared/constants/messages.ts'; import { CURRENCY } from '@/shared/constants/product.ts'; import getApiClient, { type ApiClient } from '../sdk/client.ts'; @@ -108,6 +111,26 @@ export class CartApi { const data = await this.client.apiRoot().me().carts().get().execute(); return data; } + + public updateCart(cart: Cart, actions: MyCartUpdateAction[]): Promise | boolean> { + return this.client + .apiRoot() + .me() + .carts() + .withId({ ID: cart.id }) + .post({ + body: { + actions, + version: cart.version, + }, + }) + .execute() + .then((data) => { + serverMessageModel.showServerMessage(SERVER_MESSAGE_KEYS.SUCCESSFUL_ADD_COUPON_TO_CART, MESSAGE_STATUS.SUCCESS); + return data; + }) + .catch(() => serverMessageModel.showServerMessage(SERVER_MESSAGE_KEYS.INVALID_COUPON, MESSAGE_STATUS.ERROR)); + } } const createCartApi = (): CartApi => new CartApi(); diff --git a/src/shared/API/cart/model/CartModel.ts b/src/shared/API/cart/model/CartModel.ts index f17df8e7..cace2cfa 100644 --- a/src/shared/API/cart/model/CartModel.ts +++ b/src/shared/API/cart/model/CartModel.ts @@ -4,6 +4,7 @@ import type { Cart as CartResponse, ClientResponse, LineItem, + MyCartUpdateAction, } from '@commercetools/platform-sdk'; import getStore from '@/shared/Store/Store.ts'; @@ -35,9 +36,12 @@ export class CartModel { getStore().dispatch(setAnonymousCartId(data.id)); getStore().dispatch(setAnonymousId(data.anonymousId)); } + const discount = data.discountOnTotalPrice?.discountedAmount?.centAmount; return { + discounts: discount ? discount / PRICE_FRACTIONS : 0, id: data.id, products: data.lineItems.map((lineItem) => this.adaptLineItem(lineItem)), + total: data.totalPrice.centAmount / PRICE_FRACTIONS || 0, version: data.version, }; } @@ -70,8 +74,10 @@ export class CartModel { private getCartFromData(data: ClientResponse): Cart { let cart: Cart = { + discounts: 0, id: '', products: [], + total: 0, version: 0, }; if (isClientResponse(data) && isCart(data.body)) { @@ -82,6 +88,24 @@ export class CartModel { return cart; } + public async addCoupon(discountCode: string): Promise { + if (!this.cart) { + this.cart = await this.getCart(); + } + const action: MyCartUpdateAction[] = [ + { + action: 'addDiscountCode', + code: discountCode, + }, + ]; + const data = await this.root.updateCart(this.cart, action); + if (isClientResponse(data)) { + this.cart = this.getCartFromData(data); + } + + return this.cart; + } + public async addProductInfo(): Promise { if (!this.cart) { this.cart = await this.getCart(); @@ -93,6 +117,7 @@ export class CartModel { }); const opt: OptionsRequest = { filter: filter.getFilter(), + limit: this.cart.products.length, }; const products = await getProductModel().getProducts(opt); @@ -130,6 +155,23 @@ export class CartModel { return true; } + public async clearCart(): Promise { + if (!this.cart) { + this.cart = await this.getCart(); + } + const actions: MyCartUpdateAction[] = this.cart?.products.map((lineItem) => ({ + action: 'removeLineItem', + lineItemId: lineItem.lineItemId, + })); + const data = await this.root.updateCart(this.cart, actions); + if (isClientResponse(data)) { + this.cart = this.getCartFromData(data); + } + + this.dispatchUpdate(); + return this.cart; + } + public async create(): Promise { const newCart = await this.root.create(); this.cart = this.getCartFromData(newCart); diff --git a/src/shared/constants/common.ts b/src/shared/constants/common.ts index f69d1107..38bbd56e 100644 --- a/src/shared/constants/common.ts +++ b/src/shared/constants/common.ts @@ -13,4 +13,6 @@ export const DATA_KEYS = { DIRECTION: 'data-direction', } as const; +export const TABLET_WIDTH = 768; + export type LanguageChoiceType = (typeof LANGUAGE_CHOICE)[keyof typeof LANGUAGE_CHOICE]; diff --git a/src/shared/constants/messages.ts b/src/shared/constants/messages.ts index 6522a92c..e7aed233 100644 --- a/src/shared/constants/messages.ts +++ b/src/shared/constants/messages.ts @@ -22,11 +22,13 @@ export const SERVER_MESSAGE = { COPY_TO_CLIPBOARD: 'SKU copied to clipboard', GREETING: 'Hi! Welcome to our store. Enjoy shopping!', INCORRECT_PASSWORD: 'Please, enter a correct password', + INVALID_COUPON: 'Invalid coupon', INVALID_EMAIL: "User with this email doesn't exist. Please, register first", LANGUAGE_CHANGED: 'Language preferences have been updated successfully', PASSWORD_CHANGED: 'Your password has been changed successfully', PASSWORD_NOT_CHANGED: 'Your password has not been changed. Please, try again', PERSONAL_INFO_CHANGED: 'Personal information has been changed successfully', + SUCCESSFUL_ADD_COUPON_TO_CART: 'Coupon has been added to your cart successfully', SUCCESSFUL_ADD_PRODUCT_TO_CART: 'Product has been added successfully to your cart', SUCCESSFUL_ADD_PRODUCT_TO_WISHLIST: 'Product has been added successfully to your wishlist', SUCCESSFUL_COPY_TO_CLIPBOARD: 'SKU has been copied to clipboard', @@ -45,11 +47,13 @@ export const SERVER_MESSAGE = { COPY_TO_CLIPBOARD: 'SKU скопирован в буфер обмена', GREETING: 'Здравствуйте! Добро пожаловать в наш магазин. Приятных покупок!', INCORRECT_PASSWORD: 'Пожалуйста, введите правильный пароль', + INVALID_COUPON: 'Неверный купон', INVALID_EMAIL: 'Пользователь с таким адресом не существует. Пожалуйста, сначала зарегистрируйтесь', LANGUAGE_CHANGED: 'Настройки языка успешно обновлены', PASSWORD_CHANGED: 'Ваш пароль был успешно изменен', PASSWORD_NOT_CHANGED: 'Ваш пароль не был изменен. Пожалуйста, попробуйте ещё раз', PERSONAL_INFO_CHANGED: 'Персональные данные были успешно изменены', + SUCCESSFUL_ADD_COUPON_TO_CART: 'Купон был успешно добавлен в корзину', SUCCESSFUL_ADD_PRODUCT_TO_CART: 'Товар был успешно добавлен в корзину', SUCCESSFUL_ADD_PRODUCT_TO_WISHLIST: 'Товар был успешно добавлен в избранное', SUCCESSFUL_COPY_TO_CLIPBOARD: 'SKU успешно скопирован в буфер обмена', @@ -70,11 +74,13 @@ export const SERVER_MESSAGE_KEYS = { COPY_TO_CLIPBOARD: 'COPY_TO_CLIPBOARD', GREETING: 'GREETING', INCORRECT_PASSWORD: 'INCORRECT_PASSWORD', + INVALID_COUPON: 'INVALID_COUPON', INVALID_EMAIL: 'INVALID_EMAIL', LANGUAGE_CHANGED: 'LANGUAGE_CHANGED', PASSWORD_CHANGED: 'PASSWORD_CHANGED', PASSWORD_NOT_CHANGED: 'PASSWORD_NOT_CHANGED', PERSONAL_INFO_CHANGED: 'PERSONAL_INFO_CHANGED', + SUCCESSFUL_ADD_COUPON_TO_CART: 'SUCCESSFUL_ADD_COUPON_TO_CART', SUCCESSFUL_ADD_PRODUCT_TO_CART: 'SUCCESSFUL_ADD_PRODUCT_TO_CART', SUCCESSFUL_ADD_PRODUCT_TO_WISHLIST: 'SUCCESSFUL_ADD_PRODUCT_TO_WISHLIST', SUCCESSFUL_COPY_TO_CLIPBOARD: 'SUCCESSFUL_COPY_TO_CLIPBOARD', diff --git a/src/shared/types/cart.ts b/src/shared/types/cart.ts index e46e8c2f..ca9b7f28 100644 --- a/src/shared/types/cart.ts +++ b/src/shared/types/cart.ts @@ -1,8 +1,10 @@ import type { SizeType, localization } from './product.ts'; export interface Cart { + discounts: number; id: string; products: CartProduct[]; + total: number; version: number; } @@ -28,3 +30,9 @@ export interface EditCartItem { lineId: string; quantity: number; } + +export enum CartActive { + DELETE = 'delete', + MINUS = 'minus', + PLUS = 'plus', +} diff --git a/src/widgets/Footer/view/footerView.module.scss b/src/widgets/Footer/view/footerView.module.scss index deea8c56..e9cb0599 100644 --- a/src/widgets/Footer/view/footerView.module.scss +++ b/src/widgets/Footer/view/footerView.module.scss @@ -10,7 +10,6 @@ align-items: center; justify-content: center; margin: 0 auto; - padding: 0 var(--small-offset); } .socialWrap { diff --git a/src/widgets/Header/view/headerView.module.scss b/src/widgets/Header/view/headerView.module.scss index 743ac766..bc77a222 100644 --- a/src/widgets/Header/view/headerView.module.scss +++ b/src/widgets/Header/view/headerView.module.scss @@ -449,15 +449,15 @@ .badgeWrap { position: absolute; - right: -5px; - top: 2px; + right: -10%; + top: 1%; display: flex; align-items: center; justify-content: center; - border: 2px solid var(--noble-gray-1000); + border: var(--one) solid var(--noble-gray-1000); border-radius: 100%; - width: 16px; - height: 16px; + width: calc(var(--tiny-offset) * 2); + height: calc(var(--tiny-offset) * 2); font: var(--regular-font); background-color: var(--steam-green-800); } diff --git a/src/widgets/ProductOrder/model/ProductOrderModel.ts b/src/widgets/ProductOrder/model/ProductOrderModel.ts index 8b1982c2..a431ad65 100644 --- a/src/widgets/ProductOrder/model/ProductOrderModel.ts +++ b/src/widgets/ProductOrder/model/ProductOrderModel.ts @@ -1,44 +1,29 @@ -import type { CartProduct, EditCartItem } from '@/shared/types/cart.ts'; +import type { Cart, CartProduct, EditCartItem } from '@/shared/types/cart.ts'; import getCartModel from '@/shared/API/cart/model/CartModel.ts'; +import observeStore, { selectCurrentLanguage } from '@/shared/Store/observer.ts'; +import { CartActive } from '@/shared/types/cart.ts'; import ProductOrderView from '../view/ProductOrderView.ts'; -type CallbackQuantity = () => Promise; - -export type CallbackList = { - delete: CallbackQuantity; - minus: CallbackQuantity; - plus: CallbackQuantity; -}; +type Callback = (cart: Cart) => void; class ProductOrderModel { + private callback: Callback; + private productItem: CartProduct; private view: ProductOrderView; - constructor(productItem: CartProduct) { + constructor(productItem: CartProduct, callback: Callback) { + this.callback = callback; this.productItem = productItem; - const callbackList: CallbackList = { - delete: this.deleteClickHandler.bind(this), - minus: this.minusClickHandler.bind(this), - plus: this.plusClickHandler.bind(this), - }; - this.view = new ProductOrderView(this.productItem, callbackList); + this.view = new ProductOrderView(this.productItem, this.updateProductHandler.bind(this)); this.init(); } - private init(): void {} - - public async deleteClickHandler(): Promise { - const cart = await getCartModel().deleteProductFromCart(this.productItem); - const updateItem = cart.products.find((item) => item.lineItemId === this.productItem.lineItemId); - if (updateItem) { - this.productItem = updateItem; - this.view.updateQuantity(this.productItem.quantity); - } else { - this.getHTML().remove(); - } + private init(): void { + observeStore(selectCurrentLanguage, () => this.view.updateLanguage()); } public getHTML(): HTMLDivElement { @@ -49,31 +34,48 @@ class ProductOrderModel { return this.productItem; } - public async minusClickHandler(): Promise { - const active: EditCartItem = { - lineId: this.productItem.lineItemId, - quantity: this.productItem.quantity - 1, - }; - const cart = await getCartModel().editProductCount(active); - const updateItem = cart.products.find((item) => item.lineItemId === this.productItem.lineItemId); + public async updateProductHandler(active: CartActive): Promise { + let updateItem: CartProduct | undefined; + let cart: Cart | null = null; + switch (active) { + case CartActive.DELETE: { + cart = await getCartModel().deleteProductFromCart(this.productItem); + updateItem = cart.products.find((item) => item.lineItemId === this.productItem.lineItemId); + break; + } + + case CartActive.MINUS: { + const active: EditCartItem = { + lineId: this.productItem.lineItemId, + quantity: this.productItem.quantity - 1, + }; + cart = await getCartModel().editProductCount(active); + updateItem = cart.products.find((item) => item.lineItemId === this.productItem.lineItemId); + break; + } + case CartActive.PLUS: { + const active: EditCartItem = { + lineId: this.productItem.lineItemId, + quantity: this.productItem.quantity + 1, + }; + cart = await getCartModel().editProductCount(active); + updateItem = cart.products.find((item) => item.lineItemId === this.productItem.lineItemId); + break; + } + + default: + break; + } + if (updateItem) { this.productItem = updateItem; - this.view.updateQuantity(this.productItem.quantity); + this.view.updateInfo(this.productItem); } else { this.getHTML().remove(); } - } - public async plusClickHandler(): Promise { - const active: EditCartItem = { - lineId: this.productItem.lineItemId, - quantity: this.productItem.quantity + 1, - }; - const cart = await getCartModel().editProductCount(active); - const updateItem = cart.products.find((item) => item.lineItemId === this.productItem.lineItemId); - if (updateItem) { - this.productItem = updateItem; - this.view.updateQuantity(this.productItem.quantity); + if (cart) { + this.callback(cart); } } } diff --git a/src/widgets/ProductOrder/view/ProductOrderView.ts b/src/widgets/ProductOrder/view/ProductOrderView.ts index 31c5e3c9..eddeb246 100644 --- a/src/widgets/ProductOrder/view/ProductOrderView.ts +++ b/src/widgets/ProductOrder/view/ProductOrderView.ts @@ -1,36 +1,79 @@ +import type { LanguageChoiceType } from '@/shared/constants/common.ts'; import type { CartProduct } from '@/shared/types/cart'; +import type { languageVariants } from '@/shared/types/common'; import getStore from '@/shared/Store/Store.ts'; -import { LANGUAGE_CHOICE } from '@/shared/constants/common.ts'; +import { LANGUAGE_CHOICE, TABLET_WIDTH } from '@/shared/constants/common.ts'; import SVG_DETAILS from '@/shared/constants/svg.ts'; +import { CartActive } from '@/shared/types/cart.ts'; import createBaseElement from '@/shared/utils/createBaseElement.ts'; import createSVGUse from '@/shared/utils/createSVGUse.ts'; - -import type { CallbackList } from '../model/ProductOrderModel'; +import Hammer from 'hammerjs'; import styles from './productOrderView.module.scss'; +type CallbackActive = (active: CartActive) => Promise; + +type textElementsType = { + element: HTMLTableCellElement; + textItem: languageVariants; +}; + +const TITTLE = { + MINUS: '-', + NAME: { + en: '', + ru: '', + }, + PLUS: '+', + SIZE: { + en: 'Size', + ru: 'Размер', + }, +}; class ProductOrderView { - private callbackList: CallbackList; + private callback: CallbackActive; + + private language: LanguageChoiceType; + + private price: HTMLTableCellElement; + + private productItem: CartProduct; private quantity: HTMLParagraphElement; + private textElement: textElementsType[] = []; + + private total: HTMLTableCellElement; + private view: HTMLTableRowElement; - constructor(productItem: CartProduct, callbackList: CallbackList) { - this.callbackList = callbackList; + constructor(productItem: CartProduct, callback: CallbackActive) { + this.productItem = productItem; + this.language = getStore().getState().currentLanguage; + this.callback = callback; this.quantity = createBaseElement({ cssClasses: [styles.quantityCell, styles.quantityText], - innerContent: productItem.quantity.toString(), + innerContent: this.productItem.quantity.toString(), tag: 'p', }); - this.view = this.createHTML(productItem); + this.price = createBaseElement({ + cssClasses: [styles.td, styles.priceCell, styles.priceText], + innerContent: `$${this.productItem.price.toFixed(2)}`, + tag: 'td', + }); + this.total = createBaseElement({ + cssClasses: [styles.td, styles.totalCell, styles.totalText], + innerContent: `$${this.productItem.totalPrice.toFixed(2)}`, + tag: 'td', + }); + this.view = this.createHTML(); } private createDeleCell(): HTMLTableCellElement { - const tdDelete = createBaseElement({ cssClasses: [styles.td, styles.deleteCell], tag: 'td' }); + const tdDelete = createBaseElement({ cssClasses: [styles.td, styles.deleteCell, styles.hide], tag: 'td' }); const deleteButton = createBaseElement({ cssClasses: [styles.deleteButton], tag: 'button' }); - deleteButton.addEventListener('click', () => this.callbackList.delete()); + deleteButton.addEventListener('click', () => this.callback(CartActive.DELETE)); tdDelete.append(deleteButton); const svg = document.createElementNS(SVG_DETAILS.SVG_URL, 'svg'); svg.append(createSVGUse(SVG_DETAILS.DELETE)); @@ -38,40 +81,45 @@ class ProductOrderView { return tdDelete; } - private createHTML(productItem: CartProduct): HTMLTableRowElement { + private createHTML(): HTMLTableRowElement { this.view = createBaseElement({ cssClasses: [styles.tr, styles.trProduct], tag: 'tr' }); - const imgCell = this.createImgCell(productItem); + const imgCell = this.createImgCell(); const tdProduct = createBaseElement({ cssClasses: [styles.td, styles.nameCell, styles.mainText], - innerContent: productItem.name[Number(getStore().getState().currentLanguage === LANGUAGE_CHOICE.RU)].value, + innerContent: this.productItem.name[Number(getStore().getState().currentLanguage === LANGUAGE_CHOICE.RU)].value, tag: 'td', }); const tdSize = createBaseElement({ cssClasses: [styles.td, styles.sizeCell, styles.sizeText], - innerContent: productItem.size ? `Size: ${productItem.size}` : '', - tag: 'td', - }); - const tdPrice = createBaseElement({ - cssClasses: [styles.td, styles.priceCell, styles.priceText], - innerContent: `$${productItem.price.toFixed(2)}`, + innerContent: this.productItem.size ? `${TITTLE.SIZE[this.language]}: ${this.productItem.size}` : '', tag: 'td', }); + this.textElement.push({ element: tdSize, textItem: TITTLE.SIZE }); + this.textElement.push({ element: tdProduct, textItem: TITTLE.NAME }); const quantityCell = this.createQuantityCell(); - const tdTotal = createBaseElement({ - cssClasses: [styles.td, styles.totalCell, styles.totalText], - innerContent: `$${productItem.totalPrice.toFixed(2)}`, - tag: 'td', - }); const deleteCell = this.createDeleCell(); - this.view.append(imgCell, tdProduct, tdSize, tdPrice, quantityCell, tdTotal, deleteCell); + this.view.append(imgCell, tdProduct, tdSize, this.price, quantityCell, this.total, deleteCell); + const animation = new Hammer(this.view); + animation.on('swipeleft', () => { + if (window.innerWidth <= TABLET_WIDTH) { + this.view.style.transform = 'translateX(-100px)'; + deleteCell.classList.remove(styles.hide); + } + }); + animation.on('swiperight', () => { + if (window.innerWidth <= TABLET_WIDTH) { + this.view.style.transform = 'none'; + deleteCell.classList.add(styles.hide); + } + }); return this.view; } - private createImgCell(productItem: CartProduct): HTMLTableCellElement { + private createImgCell(): HTMLTableCellElement { const tdImage = createBaseElement({ cssClasses: [styles.td, styles.imgCell], tag: 'td' }); const img = createBaseElement({ cssClasses: [styles.img], tag: 'img' }); - img.src = productItem.images; - img.alt = productItem.name[Number(getStore().getState().currentLanguage === LANGUAGE_CHOICE.RU)].value; + img.src = this.productItem.images; + img.alt = this.productItem.name[Number(getStore().getState().currentLanguage === LANGUAGE_CHOICE.RU)].value; tdImage.append(img); return tdImage; } @@ -83,17 +131,17 @@ class ProductOrderView { }); const plusButton = createBaseElement({ cssClasses: [styles.quantityCell, styles.quantityButton], - innerContent: '+', + innerContent: TITTLE.PLUS, tag: 'button', }); const minusButton = createBaseElement({ cssClasses: [styles.quantityCell, styles.quantityButton], - innerContent: '-', + innerContent: TITTLE.MINUS, tag: 'button', }); tdQuantity.append(minusButton, this.quantity, plusButton); - plusButton.addEventListener('click', () => this.callbackList.plus()); - minusButton.addEventListener('click', () => this.callbackList.minus()); + plusButton.addEventListener('click', () => this.callback(CartActive.PLUS)); + minusButton.addEventListener('click', () => this.callback(CartActive.MINUS)); return tdQuantity; } @@ -101,8 +149,23 @@ class ProductOrderView { return this.view; } - public updateQuantity(quantity: number): void { - this.quantity.textContent = quantity.toString(); + public updateInfo(productItem: CartProduct): void { + this.productItem = productItem; + this.quantity.textContent = this.productItem.quantity.toString(); + this.price.textContent = `$${this.productItem.price.toFixed(2)}`; + this.total.textContent = `$${this.productItem.totalPrice.toFixed(2)}`; + } + + public updateLanguage(): void { + this.language = getStore().getState().currentLanguage; + this.textElement.forEach((textEl) => { + const elHTML = textEl.element; + if (textEl.textItem === TITTLE.SIZE) { + elHTML.textContent = this.productItem.size ? `${TITTLE.SIZE[this.language]}: ${this.productItem.size}` : ''; + } else if (textEl.textItem === TITTLE.NAME) { + elHTML.textContent = this.productItem.name[Number(this.language === LANGUAGE_CHOICE.RU)].value; + } + }); } } diff --git a/src/widgets/ProductOrder/view/productOrderView.module.scss b/src/widgets/ProductOrder/view/productOrderView.module.scss index b78e8a62..f9c21c9f 100644 --- a/src/widgets/ProductOrder/view/productOrderView.module.scss +++ b/src/widgets/ProductOrder/view/productOrderView.module.scss @@ -6,11 +6,32 @@ display: flex; align-items: center; justify-content: center; + padding: 0; +} + +.mainText { + padding: var(--tiny-offset); + font: var(--extra-font); + color: var(--noble-gray-800); } .deleteCell { grid-area: 2 / 6 / 4 / 7; + @media (max-width: 768px) { + position: absolute; + right: -10%; + top: 50%; + display: flex; + align-items: center; + justify-content: center; + grid-column: auto; + grid-row: auto; + transform: translate(calc(var(--small-offset) / 2), calc(var(--small-offset) / 2 * -1)); + } +} + +.hide { @media (max-width: 768px) { display: none; } @@ -22,6 +43,12 @@ height: var(--extra-small-offset); stroke: var(--noble-gray-800); transition: fill 0.2s; + + @media (max-width: 768px) { + width: var(--small-offset); + height: var(--small-offset); + stroke: var(--steam-green-800); + } } &:hover { @@ -34,33 +61,37 @@ .tr { display: grid; grid-gap: 0; - grid-template-columns: 70px 2fr 1fr 1fr 1fr 70px; + grid-template-columns: calc(var(--tiny-offset) * 10) 2fr 1fr 1fr 1fr calc(var(--tiny-offset) * 7); margin-bottom: var(--tiny-offset); width: 100%; @media (max-width: 768px) { - grid-template-columns: 100px 2fr 1fr; + grid-template-columns: calc(var(--tiny-offset) * 10) 2fr 1fr; } } .trProduct { - background-color: var(--noble-white-200); + background-color: var(--white); + transition: transform 0.3s ease-out; @media (max-width: 768px) { + position: relative; + z-index: 10; margin-bottom: var(--extra-small-offset); - border-radius: 14px; - box-shadow: var(--mellow-shadow-100); + border-radius: var(--medium-br); + box-shadow: var(--mellow-shadow-050); } } .img { - max-width: 70px; - max-height: 70px; + display: block; + width: 100%; + height: 100%; @media (max-width: 768px) { - border-radius: 14px 0 0 14px; - max-width: 100px; - max-height: 100px; + border-radius: var(--medium-br) 0 0 var(--medium-br); + width: 100%; + height: 100%; } } @@ -71,6 +102,8 @@ @media (max-width: 768px) { grid-area: 1 / 2 / 2 / 3; + padding: var(--tiny-offset); + padding-bottom: 0; } } @@ -118,12 +151,6 @@ } } -.mainText { - padding: var(--tiny-offset); - font: var(--extra-font); - color: var(--noble-gray-800); -} - .quantityText { padding: var(--tiny-offset); font: var(--regular-font); @@ -132,7 +159,7 @@ .totalText { padding: var(--tiny-offset); - font: var(--regular-font); + font: var(--bold-font); color: var(--steam-green-800); } @@ -147,12 +174,16 @@ font: var(--regular-font); text-align: left; color: var(--noble-gray-700); + + @media (max-width: 768px) { + padding-top: 0; + } } .quantityButton { - border-radius: 29px; - width: 22px; - height: 25px; + border-radius: 50%; + width: calc(var(--tiny-offset) * 2.5); + height: calc(var(--tiny-offset) * 2.5); color: var(--noble-gray-200); background-color: var(--steam-green-800); } From 20fedc3fe2925da382819191e8c588d5d19e4bbc Mon Sep 17 00:00:00 2001 From: Yuliya Kursevich <54816946+YulikK@users.noreply.github.com> Date: Wed, 29 May 2024 16:12:30 +0200 Subject: [PATCH 13/28] feat(RSS-ECOMM-4_97): filter range (#323) * feat: filter Type * feat: catalog filter * feat: filter price range --- src/shared/API/cart/model/CartModel.ts | 2 +- src/shared/API/product/ProductApi.ts | 14 ++++++++++---- src/shared/API/product/utils/filter.ts | 19 +++++++++++++++---- src/shared/API/types/type.ts | 4 +++- src/widgets/Catalog/model/CatalogModel.ts | 4 ++-- 5 files changed, 31 insertions(+), 12 deletions(-) diff --git a/src/shared/API/cart/model/CartModel.ts b/src/shared/API/cart/model/CartModel.ts index cace2cfa..72a80a4f 100644 --- a/src/shared/API/cart/model/CartModel.ts +++ b/src/shared/API/cart/model/CartModel.ts @@ -116,7 +116,7 @@ export class CartModel { filter.addFilter(FilterFields.ID, product.productId); }); const opt: OptionsRequest = { - filter: filter.getFilter(), + filter, limit: this.cart.products.length, }; diff --git a/src/shared/API/product/ProductApi.ts b/src/shared/API/product/ProductApi.ts index b147b9b2..26888454 100644 --- a/src/shared/API/product/ProductApi.ts +++ b/src/shared/API/product/ProductApi.ts @@ -5,12 +5,14 @@ import type { ProductProjectionPagedSearchResponse, } from '@commercetools/platform-sdk'; -import { DEFAULT_PAGE, MAX_PRICE, MIN_PRICE, PRODUCT_LIMIT } from '@/shared/constants/product.ts'; +import { DEFAULT_PAGE, MAX_PRICE, MIN_PRICE, PRICE_FRACTIONS, PRODUCT_LIMIT } from '@/shared/constants/product.ts'; import getApiClient, { type ApiClient } from '../sdk/client.ts'; import { type OptionsRequest } from '../types/type.ts'; import makeSortRequest from './utils/sort.ts'; +const FACET_ADD = 1; + export class ProductApi { private client: ApiClient; @@ -45,6 +47,10 @@ export class ProductApi { public async getProducts(options?: OptionsRequest): Promise> { const { filter, limit = PRODUCT_LIMIT, page = DEFAULT_PAGE, search, sort } = options || {}; + const filterQuery = filter?.getFilter(); + const priceRange = filter?.getPriceRange(); + const min = Math.round((priceRange?.min ?? MIN_PRICE) * PRICE_FRACTIONS - FACET_ADD); + const max = Math.round((priceRange?.max ?? MAX_PRICE) * PRICE_FRACTIONS + FACET_ADD); const data = await this.client .apiRoot() @@ -55,7 +61,7 @@ export class ProductApi { facet: [ `categories.id counting products`, `variants.attributes.size.key`, - `variants.price.centAmount:range(${MIN_PRICE} to ${MAX_PRICE})`, + `variants.price.centAmount:range(${min} to ${max})`, ], limit, markMatchingVariants: true, @@ -63,8 +69,8 @@ export class ProductApi { ...(search && { [`text.${search.locale}`]: search.value }), ...(search && { fuzzy: true }), ...(sort && { sort: makeSortRequest(sort) }), - ...(filter && { 'filter.query': filter }), - ...(filter && { 'filter.facets': filter }), + ...(filterQuery && { 'filter.query': filterQuery }), + ...(filterQuery && { 'filter.facets': filterQuery }), withTotal: true, }, }) diff --git a/src/shared/API/product/utils/filter.ts b/src/shared/API/product/utils/filter.ts index 3110d636..7ae4d56e 100644 --- a/src/shared/API/product/utils/filter.ts +++ b/src/shared/API/product/utils/filter.ts @@ -1,4 +1,4 @@ -import { PRICE_FRACTIONS } from '@/shared/constants/product.ts'; +import { MAX_PRICE, MIN_PRICE, PRICE_FRACTIONS } from '@/shared/constants/product.ts'; import type { FilterFieldsType, PriceRange } from '../../types/type.ts'; @@ -11,7 +11,10 @@ export default class FilterProduct { private newArrival = ''; - private price = ''; + private price: PriceRange = { + max: MAX_PRICE, + min: MIN_PRICE, + }; private sale = ''; @@ -42,7 +45,8 @@ export default class FilterProduct { break; case FilterFields.PRICE: if (value && typeof value !== 'string') { - this.price = `${field}: range(${value.min * PRICE_FRACTIONS} to ${value.max * PRICE_FRACTIONS})`; + this.price.max = value.max; + this.price.min = value.min; } break; default: @@ -66,11 +70,18 @@ export default class FilterProduct { result.push(this.newArrival); } if (this.price) { - result.push(this.price); + // `${field}: range(${value.min * PRICE_FRACTIONS} to ${value.max * PRICE_FRACTIONS})`; + result.push( + `${FilterFields.PRICE}: range(${Math.round(this.price.min * PRICE_FRACTIONS)} to ${Math.round(this.price.max * PRICE_FRACTIONS)})`, + ); } if (this.sale) { result.push(this.sale); } return result; } + + public getPriceRange(): PriceRange { + return this.price; + } } diff --git a/src/shared/API/types/type.ts b/src/shared/API/types/type.ts index b6653d00..a9591dc3 100644 --- a/src/shared/API/types/type.ts +++ b/src/shared/API/types/type.ts @@ -1,5 +1,7 @@ import type { Category, Product, SizeType } from '@/shared/types/product.ts'; +import type FilterProduct from '../product/utils/filter'; + export const Attribute = { FULL_DESCRIPTION: 'full_description', LEVEL: 'level', @@ -50,7 +52,7 @@ export type SearchOptions = { }; export type OptionsRequest = { - filter?: string[]; + filter?: FilterProduct; limit?: number; page?: number; search?: SearchOptions; diff --git a/src/widgets/Catalog/model/CatalogModel.ts b/src/widgets/Catalog/model/CatalogModel.ts index c3cba80d..4b46a8d3 100644 --- a/src/widgets/Catalog/model/CatalogModel.ts +++ b/src/widgets/Catalog/model/CatalogModel.ts @@ -144,14 +144,14 @@ class CatalogModel { const currentSort = this.getSelectedSorting(selectedSorting ?? null); if (currentSort) { result = { - filter: filter.getFilter(), + filter, page: Number(page), search: { locale: currentLanguage, value: searchValue }, sort: currentSort ?? null, }; } else { result = { - filter: filter.getFilter(), + filter, page: Number(page), search: { locale: currentLanguage, value: searchValue }, }; From 5677dfe40bbe9cdfc2895df26321795f92b4779a Mon Sep 17 00:00:00 2001 From: Meg G <146496794+stardustmeg@users.noreply.github.com> Date: Wed, 29 May 2024 18:49:50 +0300 Subject: [PATCH 14/28] feat(RSS-ECOMM-4_33): implement editing address (#322) * feat: implement Confirm component * fix: styles for responsive * feat: add confirmation message * fix: dark theme variable * feat: implement changing address fields * feat: add styles to edit address window * feat: add remove content method to modal --- src/app/styles/variables.scss | 5 +- src/entities/Address/model/AddressModel.ts | 18 +- src/entities/Address/view/AddressView.ts | 28 +- .../Address/view/addressView.module.scss | 1 + .../UserAddress/model/UserAddressModel.ts | 63 ++--- .../UserAddress/view/UserAddressView.ts | 13 +- .../AddressAdd/model/AddressAddModel.ts | 3 +- .../AddressAdd/view/AddressAddView.ts | 2 +- .../AddressEdit/model/AddressEditModel.ts | 265 ++++++++++-------- .../AddressEdit/view/AddressEditView.ts | 4 +- .../view/addressEditView.module.scss | 55 +--- .../CountryChoice/model/CountryChoiceModel.ts | 35 ++- .../model/PersonalInfoEditModel.ts | 1 + src/shared/Confirm/model/ConfirmModel.ts | 54 ++++ src/shared/Confirm/view/ConfirmView.ts | 79 ++++++ .../Confirm/view/confirmView.module.scss | 41 +++ src/shared/Modal/model/ModalModel.ts | 4 + src/shared/Modal/view/ModalView.ts | 7 +- src/shared/Store/actions.ts | 6 + src/shared/Store/observer.ts | 2 + src/shared/Store/reducer.ts | 6 + src/shared/Store/test.spec.ts | 2 + src/shared/constants/buttons.ts | 6 + src/shared/constants/confirmUserMessage.ts | 17 ++ src/shared/constants/forms/text.ts | 3 + src/shared/constants/initialState.ts | 1 + src/shared/constants/tooltip.ts | 32 ++- src/shared/types/address.ts | 5 +- .../Header/view/headerView.module.scss | 22 +- .../model/RegistrationFormModel.ts | 22 +- .../UserAddresses/model/UserAddressesModel.ts | 3 +- 31 files changed, 545 insertions(+), 260 deletions(-) create mode 100644 src/shared/Confirm/model/ConfirmModel.ts create mode 100644 src/shared/Confirm/view/ConfirmView.ts create mode 100644 src/shared/Confirm/view/confirmView.module.scss create mode 100644 src/shared/constants/confirmUserMessage.ts diff --git a/src/app/styles/variables.scss b/src/app/styles/variables.scss index 6fc08288..34a70466 100644 --- a/src/app/styles/variables.scss +++ b/src/app/styles/variables.scss @@ -46,9 +46,9 @@ // 1440px body.light { // colors + --black: #000; --white: #fff; --white-tr: #ffffffa9; - --black: #000; --noble-white-100: #f5f5f5; --noble-white-200: #f0f0f0; --noble-gray-200: #eaeaea; @@ -86,6 +86,8 @@ --noble-gray-600: #cdcdcd; --noble-gray-700: #b0b0b0; --noble-gray-800: #c4c4c4; + --noble-gray-tr-800: #acacacbd; + --noble-gray-900: #1d1d1d; --noble-gray-1000: #1a1a1ab5; --red-power-600: #d0302f; --steam-green-300: #46a35880; @@ -94,6 +96,7 @@ --steam-green-500: #b6f09c; --steam-green-700: #70d27a; --steam-green-800: #46a358; + --steam-green-900: #46a3581a; --steam-green-gr-800: #c5e1cb2b; } } diff --git a/src/entities/Address/model/AddressModel.ts b/src/entities/Address/model/AddressModel.ts index b105c11c..4e161abc 100644 --- a/src/entities/Address/model/AddressModel.ts +++ b/src/entities/Address/model/AddressModel.ts @@ -13,7 +13,7 @@ class AddressModel { private view: AddressView; - constructor(addressType: AddressType, options: AddressOptions) { + constructor(options: AddressOptions, addressType: AddressType = ADDRESS_TYPE.GENERAL) { this.addressType = addressType; this.view = new AddressView(addressType, options); this.init(); @@ -26,9 +26,22 @@ class AddressModel { public getAddressData(personalData: PersonalData): Address { const store = getStore().getState(); + let country: string; + + switch (this.addressType) { + case ADDRESS_TYPE.BILLING: + country = store.billingCountry; + break; + case ADDRESS_TYPE.SHIPPING: + country = store.shippingCountry; + break; + default: + country = store.defaultCountry; + break; + } const addressData: Address = { city: formattedText(this.view.getCityField().getView().getValue()), - country: this.addressType === ADDRESS_TYPE.BILLING ? store.billingCountry : store.shippingCountry, + country, email: personalData.email, firstName: personalData.firstName, id: '', @@ -38,6 +51,7 @@ class AddressModel { 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 939fdb71..596c9af3 100644 --- a/src/entities/Address/view/AddressView.ts +++ b/src/entities/Address/view/AddressView.ts @@ -212,19 +212,29 @@ class AddressView { } private createTitle(): HTMLHeadingElement { + let titleText: string; + let key: string; + switch (this.addressType) { + case ADDRESS_TYPE.BILLING: + titleText = TITLE_TEXT[getStore().getState().currentLanguage].BILLING_ADDRESS; + key = TITLE_TEXT_KEYS.BILLING_ADDRESS; + break; + case ADDRESS_TYPE.SHIPPING: + titleText = TITLE_TEXT[getStore().getState().currentLanguage].SHIPPING_ADDRESS; + key = TITLE_TEXT_KEYS.SHIPPING_ADDRESS; + break; + default: + titleText = TITLE_TEXT[getStore().getState().currentLanguage].ADDRESS; + key = TITLE_TEXT_KEYS.ADDRESS; + break; + } + const title = createBaseElement({ cssClasses: [styles.title], - innerContent: - this.addressType === ADDRESS_TYPE.SHIPPING - ? TITLE_TEXT[getStore().getState().currentLanguage].SHIPPING_ADDRESS - : TITLE_TEXT[getStore().getState().currentLanguage].BILLING_ADDRESS, + innerContent: titleText, tag: 'h3', }); - observeCurrentLanguage( - title, - TITLE_TEXT, - this.addressType === ADDRESS_TYPE.SHIPPING ? TITLE_TEXT_KEYS.SHIPPING_ADDRESS : TITLE_TEXT_KEYS.BILLING_ADDRESS, - ); + observeCurrentLanguage(title, TITLE_TEXT, key); return title; } diff --git a/src/entities/Address/view/addressView.module.scss b/src/entities/Address/view/addressView.module.scss index 23290767..61196719 100644 --- a/src/entities/Address/view/addressView.module.scss +++ b/src/entities/Address/view/addressView.module.scss @@ -40,6 +40,7 @@ .shippingAddressWrapper { grid-row: 3; + animation: show 0.5s ease-in forwards; } .billingAddressWrapper { diff --git a/src/entities/UserAddress/model/UserAddressModel.ts b/src/entities/UserAddress/model/UserAddressModel.ts index f93b2982..d5eba45f 100644 --- a/src/entities/UserAddress/model/UserAddressModel.ts +++ b/src/entities/UserAddress/model/UserAddressModel.ts @@ -1,15 +1,14 @@ -/* eslint-disable max-lines-per-function */ import type { Address, User } from '@/shared/types/user.ts'; +import AddressEditModel from '@/features/AddressEdit/model/AddressEditModel.ts'; import getCustomerModel, { CustomerModel } from '@/shared/API/customer/model/CustomerModel.ts'; +import ConfirmModel from '@/shared/Confirm/model/ConfirmModel.ts'; import EventMediatorModel from '@/shared/EventMediator/model/EventMediatorModel.ts'; -import LoaderModel from '@/shared/Loader/model/LoaderModel.ts'; import modal from '@/shared/Modal/model/ModalModel.ts'; import serverMessageModel from '@/shared/ServerMessage/model/ServerMessageModel.ts'; import MEDIATOR_EVENT from '@/shared/constants/events.ts'; import { ADDRESS_TYPE, type AddressTypeType } from '@/shared/constants/forms.ts'; import { MESSAGE_STATUS, SERVER_MESSAGE_KEYS } from '@/shared/constants/messages.ts'; -import { LOADER_SIZE } from '@/shared/constants/sizes.ts'; import showErrorMessage from '@/shared/utils/userMessage.ts'; import UserAddressView from '../view/UserAddressView.ts'; @@ -30,6 +29,23 @@ class UserAddressModel { this.setLabelClickHandler(); } + private async deleteAddress(address: Address): Promise { + try { + const user = await getCustomerModel().getCurrentUser(); + if (user) { + try { + await getCustomerModel().editCustomer([CustomerModel.actionRemoveAddress(address)], user); + EventMediatorModel.getInstance().notify(MEDIATOR_EVENT.REDRAW_USER_ADDRESSES, ''); + serverMessageModel.showServerMessage(SERVER_MESSAGE_KEYS.ADDRESS_DELETED, MESSAGE_STATUS.SUCCESS); + } catch (error) { + showErrorMessage(error); + } + } + } catch (error) { + showErrorMessage(error); + } + } + private async handleAddressType(user: User, activeType: AddressTypeType, inactive: boolean): Promise { const customerModel = getCustomerModel(); @@ -63,22 +79,15 @@ class UserAddressModel { } else { switch (activeType) { case ADDRESS_TYPE.BILLING: + case ADDRESS_TYPE.DEFAULT_BILLING: await customerModel.editCustomer([CustomerModel.actionRemoveBillingAddress(this.currentAddress)], user); break; case ADDRESS_TYPE.SHIPPING: + case ADDRESS_TYPE.DEFAULT_SHIPPING: await customerModel.editCustomer([CustomerModel.actionRemoveShippingAddress(this.currentAddress)], user); break; - // TBD Check the adding/removing default address - // case ADDRESS_TYPE.DEFAULT_BILLING: - // await customerModel.editCustomer([CustomerModel.actionEditDefaultBillingAddress(null)], user); - // break; - - // case ADDRESS_TYPE.DEFAULT_SHIPPING: - // await customerModel.editCustomer([CustomerModel.actionEditDefaultShippingAddress(null)], user); - // break; - default: break; } @@ -102,30 +111,14 @@ class UserAddressModel { this.view .getDeleteButton() .getHTML() - .addEventListener('click', async () => { - const loader = new LoaderModel(LOADER_SIZE.SMALL).getHTML(); - this.view.getDeleteButton().getHTML().append(loader); - try { - const user = await getCustomerModel().getCurrentUser(); - if (user) { - try { - await getCustomerModel().editCustomer([CustomerModel.actionRemoveAddress(address)], user); - EventMediatorModel.getInstance().notify(MEDIATOR_EVENT.REDRAW_USER_ADDRESSES, ''); - serverMessageModel.showServerMessage(SERVER_MESSAGE_KEYS.ADDRESS_DELETED, MESSAGE_STATUS.SUCCESS); - } catch (error) { - showErrorMessage(error); - } - } - } catch (error) { - showErrorMessage(error); - } finally { - loader.remove(); - } + .addEventListener('click', () => { + const confirmModel = new ConfirmModel(() => this.deleteAddress(address)); + modal.setContent(confirmModel.getHTML()); + modal.show(); }); } - // eslint-disable-next-line @typescript-eslint/no-unused-vars - private setEditButtonHandler(_address: Address): void { + private setEditButtonHandler(address: Address): void { this.view .getEditButton() .getHTML() @@ -135,11 +128,11 @@ class UserAddressModel { if (!user) { return; } + const newAddressEditForm = new AddressEditModel(address, user).getHTML(); modal.show(); + modal.setContent(newAddressEditForm); } catch (error) { showErrorMessage(error); - } finally { - modal.hide(); } }); } diff --git a/src/entities/UserAddress/view/UserAddressView.ts b/src/entities/UserAddress/view/UserAddressView.ts index 4967a597..4288a7cf 100644 --- a/src/entities/UserAddress/view/UserAddressView.ts +++ b/src/entities/UserAddress/view/UserAddressView.ts @@ -117,6 +117,11 @@ class UserAddressView { } this.view.append(addressText, this.editButton.getHTML(), this.deleteButton.getHTML()); + + observeStore(selectCurrentLanguage, () => { + addressText.innerText = this.createAddressText(getStore().getState().currentLanguage); + }); + return this.view; } @@ -139,12 +144,12 @@ class UserAddressView { let addressType = null; switch (ActiveType) { case ADDRESS_TYPE.BILLING: - addressType = this.createLabel(ActiveType, [styles.billing], TOOLTIP_TEXT_KEYS.EDIT_BILLING_ADDRESS); + addressType = this.createLabel(ActiveType, [styles.billing], TOOLTIP_TEXT_KEYS.SWITCH_BILLING_ADDRESS); this.view.append(addressType); break; case ADDRESS_TYPE.SHIPPING: - addressType = this.createLabel(ActiveType, [styles.shipping], TOOLTIP_TEXT_KEYS.EDIT_SHIPPING_ADDRESS); + addressType = this.createLabel(ActiveType, [styles.shipping], TOOLTIP_TEXT_KEYS.SWITCH_SHIPPING_ADDRESS); this.view.append(addressType); break; @@ -152,7 +157,7 @@ class UserAddressView { addressType = this.createLabel( ActiveType, [styles.defaultBilling], - TOOLTIP_TEXT_KEYS.EDIT_DEFAULT_BILLING_ADDRESS, + TOOLTIP_TEXT_KEYS.SWITCH_DEFAULT_BILLING_ADDRESS, ); this.view.append(addressType); break; @@ -161,7 +166,7 @@ class UserAddressView { addressType = this.createLabel( ActiveType, [styles.defaultShipping], - TOOLTIP_TEXT_KEYS.EDIT_DEFAULT_SHIPPING_ADDRESS, + TOOLTIP_TEXT_KEYS.SWITCH_DEFAULT_SHIPPING_ADDRESS, ); this.view.append(addressType); break; diff --git a/src/features/AddressAdd/model/AddressAddModel.ts b/src/features/AddressAdd/model/AddressAddModel.ts index 86c3468f..7b031bdd 100644 --- a/src/features/AddressAdd/model/AddressAddModel.ts +++ b/src/features/AddressAdd/model/AddressAddModel.ts @@ -30,7 +30,7 @@ class AddressAddModel { constructor(type: AddressType, options: Record) { this.addressType = type; - this.newAddress = new AddressModel(this.addressType, options); + this.newAddress = new AddressModel(options, this.addressType); this.init(); } @@ -148,6 +148,7 @@ class AddressAddModel { const cancelButton = this.view.getCancelButton().getHTML(); cancelButton.addEventListener('click', () => { modal.hide(); + modal.removeContent(); }); return true; } diff --git a/src/features/AddressAdd/view/AddressAddView.ts b/src/features/AddressAdd/view/AddressAddView.ts index c0c8b9db..bb26f80e 100644 --- a/src/features/AddressAdd/view/AddressAddView.ts +++ b/src/features/AddressAdd/view/AddressAddView.ts @@ -38,7 +38,7 @@ class AddressAddView { private createSaveChangesButton(): ButtonModel { this.saveChangesButton = new ButtonModel({ classes: [styles.saveChangesButton], - text: BUTTON_TEXT[getStore().getState().currentLanguage].SAVE_CHANGES, + text: BUTTON_TEXT[getStore().getState().currentLanguage].ADD_ADDRESS, }); this.saveChangesButton.setDisabled(); return this.saveChangesButton; diff --git a/src/features/AddressEdit/model/AddressEditModel.ts b/src/features/AddressEdit/model/AddressEditModel.ts index f91accbd..b80ffdf1 100644 --- a/src/features/AddressEdit/model/AddressEditModel.ts +++ b/src/features/AddressEdit/model/AddressEditModel.ts @@ -1,115 +1,160 @@ -// import type InputFieldModel from '@/entities/InputField/model/InputFieldModel.ts'; -// import type { UserAddressType } from '@/shared/constants/forms.ts'; -// import type { Address } from '@/shared/types/user.ts'; - -// import AddressModel from '@/entities/Address/model/AddressModel.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 modal from '@/shared/Modal/model/ModalModel.ts'; -// import serverMessageModel from '@/shared/ServerMessage/model/ServerMessageModel.ts'; -// import MEDIATOR_EVENT from '@/shared/constants/events.ts'; -// import { MESSAGE_STATUS, SERVER_MESSAGE_KEYS } from '@/shared/constants/messages.ts'; -// import { LOADER_SIZE } from '@/shared/constants/sizes.ts'; -// import showErrorMessage from '@/shared/utils/userMessage.ts'; - -// import AddressEditView from '../view/AddressEditView.ts'; +import type InputFieldModel from '@/entities/InputField/model/InputFieldModel.ts'; +import type { Address, 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 modal from '@/shared/Modal/model/ModalModel.ts'; +import serverMessageModel from '@/shared/ServerMessage/model/ServerMessageModel.ts'; +import getStore from '@/shared/Store/Store.ts'; +import { setBillingCountry } from '@/shared/Store/actions.ts'; +import COUNTRIES_LIST from '@/shared/constants/countriesList.ts'; +import MEDIATOR_EVENT from '@/shared/constants/events.ts'; +import { MESSAGE_STATUS, SERVER_MESSAGE_KEYS } from '@/shared/constants/messages.ts'; +import { LOADER_SIZE } from '@/shared/constants/sizes.ts'; +import findKeyByValue from '@/shared/utils/findKeyByValue.ts'; +import formattedText from '@/shared/utils/formattedText.ts'; +import getCountryIndex from '@/shared/utils/getCountryIndex.ts'; +import showErrorMessage from '@/shared/utils/userMessage.ts'; + +import AddressEditView from '../view/AddressEditView.ts'; class AddressEditModel { - // private address: AddressModel; - // private currentAddress: Address; - // private inputFields: InputFieldModel[] = []; - // private view = new AddressEditView(); - // constructor(address: Address, type: UserAddressType) { - // this.address = new AddressModel(type, { setDefault: true }); - // this.currentAddress = address; - // this.init(); - // } - // private async editPersonalInfo(): Promise { - // const loader = new LoaderModel(LOADER_SIZE.SMALL).getHTML(); - // this.view.getSaveChangesButton().getHTML().append(loader); - // try { - // const user = await getCustomerModel().getCurrentUser(); - // if (user) { - // // const {} = this.getFormPersonalData(); - // await getCustomerModel().editCustomer( - // [ - // // CustomerModel - // ], - // user, - // ); - // modal.hide(); - // serverMessageModel.showServerMessage(SERVER_MESSAGE_KEYS.ADDRESS_CHANGED, MESSAGE_STATUS.SUCCESS); - // EventMediatorModel.getInstance().notify(MEDIATOR_EVENT.REDRAW_USER_ADDRESS, ''); - // } - // } catch (error) { - // showErrorMessage(); - // } finally { - // loader.remove(); - // } - // } - // private getFormPersonalData(): Record { - // return { - // // dateOfBirth: this.personalInfo.getView().getDateOfBirthField().getView().getInput().getValue(), - // // email: this.view.getEmailField().getView().getInput().getValue(), - // // firstName: formattedText(this.personalInfo.getView().getFirstNameField().getView().getInput().getValue()), - // // lastName: formattedText(this.personalInfo.getView().getLastNameField().getView().getInput().getValue()), - // }; - // } - // private init(): void { - // this.initiateFieldsValues(); - // // this.getHTML().append(this.personalInfo.getHTML()); - // this.inputFields = [ - // this.personalInfo.getView().getFirstNameField(), - // this.personalInfo.getView().getLastNameField(), - // this.personalInfo.getView().getDateOfBirthField(), - // ]; - // this.inputFields.forEach((inputField) => this.setInputFieldHandlers(inputField)); - // this.setPreventDefaultToForm(); - // this.setSubmitFormHandler(); - // this.setCancelButtonHandler(); - // } - // private initiateFieldsValues(): void { - // this.personalInfo.getView().getFirstNameField().getView().getInput().setValue(this.currentUser.firstName); - // this.personalInfo.getView().getLastNameField().getView().getInput().setValue(this.currentUser.lastName); - // this.personalInfo.getView().getDateOfBirthField().getView().getInput().setValue(this.currentUser.birthDate); - // } - // private setCancelButtonHandler(): boolean { - // const cancelButton = this.view.getCancelButton().getHTML(); - // cancelButton.addEventListener('click', () => { - // modal.hide(); - // }); - // return true; - // } - // private setInputFieldHandlers(inputField: InputFieldModel): boolean { - // const inputHTML = inputField.getView().getInput().getHTML(); - // inputHTML.addEventListener('input', () => { - // this.switchSubmitFormButtonAccess(); - // }); - // return true; - // } - // private setPreventDefaultToForm(): boolean { - // this.getHTML().addEventListener('submit', (event) => { - // event.preventDefault(); - // }); - // return true; - // } - // private setSubmitFormHandler(): boolean { - // const submitButton = this.view.getSaveChangesButton().getHTML(); - // submitButton.addEventListener('click', () => this.editPersonalInfo()); - // return true; - // } - // private switchSubmitFormButtonAccess(): boolean { - // if (this.inputFields.every((inputField) => inputField.getIsValid())) { - // this.view.getSaveChangesButton().setEnabled(); - // } else { - // this.view.getSaveChangesButton().setDisabled(); - // } - // return true; - // } - // public getHTML(): HTMLFormElement { - // return this.view.getHTML(); - // } + private currentAddress: Address; + + private inputFields: InputFieldModel[] = []; + + private newAddress: AddressModel; + + private user: User; + + private view = new AddressEditView(); + + constructor(address: Address, user: User) { + this.user = user; + this.currentAddress = address; + this.newAddress = new AddressModel({}); + this.init(); + } + + private createAddress(user: User): Address { + const { email, firstName, lastName } = user; + const { city, country, postalCode, streetName } = this.getFormAddressData(); + return { + city, + country: getCountryIndex(formattedText(country)), + email, + firstName, + id: this.currentAddress.id, + lastName, + postalCode, + state: '', + streetName, + streetNumber: '', + }; + } + + private async editAddressInfo(): Promise { + const loader = new LoaderModel(LOADER_SIZE.SMALL).getHTML(); + this.view.getSaveChangesButton().getHTML().append(loader); + try { + const user = await getCustomerModel().getCurrentUser(); + if (user) { + await getCustomerModel().editCustomer([CustomerModel.actionEditAddress(this.createAddress(user))], user); + modal.hide(); + serverMessageModel.showServerMessage(SERVER_MESSAGE_KEYS.ADDRESS_CHANGED, MESSAGE_STATUS.SUCCESS); + EventMediatorModel.getInstance().notify(MEDIATOR_EVENT.REDRAW_USER_ADDRESSES, ''); + } + } catch (error) { + showErrorMessage(error); + } finally { + loader.remove(); + } + } + + private getFormAddressData(): Record { + return { + city: formattedText(this.newAddress.getView().getCityField().getView().getInput().getValue()), + country: this.newAddress.getView().getCountryField().getView().getInput().getValue(), + postalCode: this.newAddress.getView().getPostalCodeField().getView().getInput().getValue(), + streetName: formattedText(this.newAddress.getView().getStreetField().getView().getInput().getValue()), + }; + } + + private init(): void { + this.initiateFieldsValues(); + this.getHTML().append(this.newAddress.getHTML()); + this.inputFields = [ + this.newAddress.getView().getCityField(), + this.newAddress.getView().getCountryField(), + this.newAddress.getView().getPostalCodeField(), + this.newAddress.getView().getStreetField(), + ]; + this.inputFields.forEach((inputField) => this.setInputFieldHandlers(inputField)); + this.setPreventDefaultToForm(); + this.setSubmitFormHandler(); + this.setCancelButtonHandler(); + this.view.getHTML().append(this.newAddress.getHTML()); + getStore().dispatch(setBillingCountry(this.currentAddress.country)); + } + + private initiateFieldsValues(): void { + const { locale } = this.user; + const currentCountry = findKeyByValue(COUNTRIES_LIST[locale], this.currentAddress.country); + this.newAddress.getView().getCityField().getView().getInput().setValue(this.currentAddress.city); + this.newAddress + .getView() + .getCountryField() + .getView() + .getInput() + .setValue(currentCountry ?? ''); + this.newAddress.getView().getStreetField().getView().getInput().setValue(this.currentAddress.streetName); + this.newAddress.getView().getPostalCodeField().getView().getInput().setValue(this.currentAddress.postalCode); + } + + private setCancelButtonHandler(): boolean { + const cancelButton = this.view.getCancelButton().getHTML(); + cancelButton.addEventListener('click', () => { + modal.hide(); + modal.removeContent(); + }); + return true; + } + + private setInputFieldHandlers(inputField: InputFieldModel): boolean { + const inputHTML = inputField.getView().getInput().getHTML(); + inputHTML.addEventListener('input', () => { + this.switchSubmitFormButtonAccess(); + }); + return true; + } + + private setPreventDefaultToForm(): boolean { + this.getHTML().addEventListener('submit', (event) => { + event.preventDefault(); + }); + return true; + } + + private setSubmitFormHandler(): boolean { + const submitButton = this.view.getSaveChangesButton().getHTML(); + submitButton.addEventListener('click', () => this.editAddressInfo()); + return true; + } + + private switchSubmitFormButtonAccess(): boolean { + if (this.inputFields.every((inputField) => inputField.getIsValid())) { + this.view.getSaveChangesButton().setEnabled(); + } else { + this.view.getSaveChangesButton().setDisabled(); + } + return true; + } + + public getHTML(): HTMLFormElement { + return this.view.getHTML(); + } } export default AddressEditModel; diff --git a/src/features/AddressEdit/view/AddressEditView.ts b/src/features/AddressEdit/view/AddressEditView.ts index b108f31b..ce2963a5 100644 --- a/src/features/AddressEdit/view/AddressEditView.ts +++ b/src/features/AddressEdit/view/AddressEditView.ts @@ -28,9 +28,11 @@ class AddressEditView { private createHTML(): HTMLFormElement { this.view = createBaseElement({ - cssClasses: [styles.style], + cssClasses: [styles.wrapper], tag: 'form', }); + + this.view.append(this.cancelButton.getHTML(), this.saveChangesButton.getHTML()); return this.view; } diff --git a/src/features/AddressEdit/view/addressEditView.module.scss b/src/features/AddressEdit/view/addressEditView.module.scss index 0abd8a96..71fcc608 100644 --- a/src/features/AddressEdit/view/addressEditView.module.scss +++ b/src/features/AddressEdit/view/addressEditView.module.scss @@ -1,68 +1,35 @@ +@import 'src/app/styles/mixins'; + .wrapper { display: grid; place-items: center center; - grid-template-columns: repeat(2, 1fr); - grid-template-rows: repeat(3, auto); + grid-template-rows: repeat(auto, auto); border-bottom: var(--tiny-offset) solid var(--steam-green-800); padding: var(--small-offset); height: max-content; - max-width: max-content; background-color: var(--noble-white-100); - gap: calc(var(--extra-small-offset) * 1.5) var(--extra-small-offset); - - @media (max-width: 768px) { - grid-template-columns: repeat(1, 1fr); - grid-template-rows: repeat(4, auto); - padding: var(--medium-offset); - } } .saveChangesButton, .cancelButton { - margin: 0 auto; - border-radius: var(--medium-br); - padding: calc(var(--small-offset) / 3) var(--small-offset); - width: 100%; - height: 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; + @include green-btn; - &:focus { - background-color: var(--steam-green-700); - } + grid-row: 5; + margin-top: var(--tiny-offset); +} - @media (hover: hover) { - &:hover { - background-color: var(--steam-green-700); - } - } +.cancelButton { + grid-column: 1; - &:disabled { - background-color: var(--noble-gray-300); - pointer-events: none; + @media (max-width: 768px) { + grid-row: 6; } } .saveChangesButton { - display: flex; grid-column: 2; - grid-row: 3; @media (max-width: 768px) { grid-column: 1; } } - -.cancelButton { - grid-column: 1; - grid-row: 3; - - @media (max-width: 768px) { - grid-row: 4; - } -} diff --git a/src/features/CountryChoice/model/CountryChoiceModel.ts b/src/features/CountryChoice/model/CountryChoiceModel.ts index abf02d41..5225d997 100644 --- a/src/features/CountryChoice/model/CountryChoiceModel.ts +++ b/src/features/CountryChoice/model/CountryChoiceModel.ts @@ -1,8 +1,9 @@ import getStore from '@/shared/Store/Store.ts'; -import { setBillingCountry, setShippingCountry } from '@/shared/Store/actions.ts'; +import { setBillingCountry, setDefaultCountry, setShippingCountry } from '@/shared/Store/actions.ts'; import observeStore, { selectBillingCountry, selectCurrentLanguage, + selectDefaultCountry, selectShippingCountry, } from '@/shared/Store/observer.ts'; import { DATA_KEYS } from '@/shared/constants/common.ts'; @@ -21,10 +22,19 @@ class CountryChoiceModel { this.setCountryItemsHandlers(input); this.setInputHandler(input); - const action = - input.getAttribute(DATA_KEYS.ADDRESS_TYPE) === USER_ADDRESS_TYPE.BILLING - ? selectBillingCountry - : selectShippingCountry; + let action; + + switch (input.getAttribute(DATA_KEYS.ADDRESS_TYPE)) { + case USER_ADDRESS_TYPE.BILLING: + action = selectBillingCountry; + break; + case USER_ADDRESS_TYPE.SHIPPING: + action = selectShippingCountry; + break; + default: + action = selectDefaultCountry; + break; + } observeStore(action, () => { const event = new Event('input'); @@ -66,7 +76,20 @@ class CountryChoiceModel { element instanceof HTMLDivElement ? formattedText(element.textContent ?? '') : formattedText(element.value), ); - const action = key === USER_ADDRESS_TYPE.BILLING ? setBillingCountry : setShippingCountry; + let action; + + switch (key) { + case USER_ADDRESS_TYPE.BILLING: + action = setBillingCountry; + break; + case USER_ADDRESS_TYPE.SHIPPING: + action = setShippingCountry; + break; + default: + action = setDefaultCountry; + break; + } + getStore().dispatch(action(currentCountryIndex)); return true; } diff --git a/src/features/PersonalInfoEdit/model/PersonalInfoEditModel.ts b/src/features/PersonalInfoEdit/model/PersonalInfoEditModel.ts index 08ddedbc..d055e70b 100644 --- a/src/features/PersonalInfoEdit/model/PersonalInfoEditModel.ts +++ b/src/features/PersonalInfoEdit/model/PersonalInfoEditModel.ts @@ -92,6 +92,7 @@ class PersonalInfoEditModel { const cancelButton = this.view.getCancelButton().getHTML(); cancelButton.addEventListener('click', () => { modal.hide(); + modal.removeContent(); }); return true; } diff --git a/src/shared/Confirm/model/ConfirmModel.ts b/src/shared/Confirm/model/ConfirmModel.ts new file mode 100644 index 00000000..ff5d9d1e --- /dev/null +++ b/src/shared/Confirm/model/ConfirmModel.ts @@ -0,0 +1,54 @@ +import LoaderModel from '@/shared/Loader/model/LoaderModel.ts'; +import modal from '@/shared/Modal/model/ModalModel.ts'; +import getStore from '@/shared/Store/Store.ts'; +import { USER_MESSAGE } from '@/shared/constants/confirmUserMessage.ts'; +import { LOADER_SIZE } from '@/shared/constants/sizes.ts'; +import showErrorMessage from '@/shared/utils/userMessage.ts'; + +import ConfirmView from '../view/ConfirmView.ts'; + +class ConfirmModel { + private callback: () => Promise | void; + + private view: ConfirmView; + + constructor(callback: () => Promise | void) { + this.callback = callback; + this.view = new ConfirmView(USER_MESSAGE[getStore().getState().currentLanguage].DELETE_ADDRESS); + this.setCancelButtonHandler(); + this.setConfirmButtonHandler(); + } + + private setCancelButtonHandler(): void { + this.view + .getCancelButton() + .getHTML() + .addEventListener('click', () => { + modal.hide(); + }); + } + + private setConfirmButtonHandler(): void { + this.view + .getConfirmButton() + .getHTML() + .addEventListener('click', async () => { + const loader = new LoaderModel(LOADER_SIZE.SMALL).getHTML(); + this.view.getConfirmButton().getHTML().append(loader); + try { + await this.callback(); + } catch (error) { + showErrorMessage(error); + } finally { + loader.remove(); + } + modal.hide(); + }); + } + + public getHTML(): HTMLDivElement { + return this.view.getHTML(); + } +} + +export default ConfirmModel; diff --git a/src/shared/Confirm/view/ConfirmView.ts b/src/shared/Confirm/view/ConfirmView.ts new file mode 100644 index 00000000..bcec0507 --- /dev/null +++ b/src/shared/Confirm/view/ConfirmView.ts @@ -0,0 +1,79 @@ +import ButtonModel from '@/shared/Button/model/ButtonModel.ts'; +import getStore from '@/shared/Store/Store.ts'; +import { BUTTON_TEXT, BUTTON_TEXT_KEYS } from '@/shared/constants/buttons.ts'; +import createBaseElement from '@/shared/utils/createBaseElement.ts'; +import observeCurrentLanguage from '@/shared/utils/observeCurrentLanguage.ts'; + +import styles from './confirmView.module.scss'; + +class ConfirmView { + private cancelButton: ButtonModel; + + private confirmButton: ButtonModel; + + private userMessage: HTMLSpanElement; + + private view: HTMLDivElement; + + constructor(userMessage: string) { + this.userMessage = this.createUserMessage(userMessage); + this.cancelButton = this.createCancelButton(); + this.confirmButton = this.createConfirmButton(); + this.view = this.createHTML(); + } + + private createCancelButton(): ButtonModel { + this.cancelButton = new ButtonModel({ + classes: [styles.cancelButton], + text: BUTTON_TEXT[getStore().getState().currentLanguage].CANCEL, + }); + + observeCurrentLanguage(this.cancelButton.getHTML(), BUTTON_TEXT, BUTTON_TEXT_KEYS.CANCEL); + + return this.cancelButton; + } + + private createConfirmButton(): ButtonModel { + this.confirmButton = new ButtonModel({ + classes: [styles.confirmButton], + text: BUTTON_TEXT[getStore().getState().currentLanguage].CONFIRM, + }); + + observeCurrentLanguage(this.confirmButton.getHTML(), BUTTON_TEXT, BUTTON_TEXT_KEYS.CONFIRM); + + return this.confirmButton; + } + + private createHTML(): HTMLDivElement { + this.view = createBaseElement({ + cssClasses: [styles.wrapper], + tag: 'div', + }); + this.view.append(this.userMessage, this.cancelButton.getHTML(), this.confirmButton.getHTML()); + return this.view; + } + + private createUserMessage(message: string): HTMLSpanElement { + this.userMessage = createBaseElement({ + cssClasses: [styles.userMessage], + innerContent: message, + tag: 'span', + }); + + return this.userMessage; + } + + public getCancelButton(): ButtonModel { + return this.cancelButton; + } + + public getConfirmButton(): ButtonModel { + return this.confirmButton; + } + + public getHTML(): HTMLDivElement { + return this.view; + } +} + +export default ConfirmView; diff --git a/src/shared/Confirm/view/confirmView.module.scss b/src/shared/Confirm/view/confirmView.module.scss new file mode 100644 index 00000000..0b03f335 --- /dev/null +++ b/src/shared/Confirm/view/confirmView.module.scss @@ -0,0 +1,41 @@ +@import 'src/app/styles/mixins'; + +.wrapper { + display: grid; + grid-column: 2 span; + grid-template-rows: repeat((2, max-content)); + margin: var(--small-offset) auto; + border-bottom: var(--tiny-offset) solid var(--steam-green-800); + padding: var(--small-offset); + width: 100%; + min-height: calc(var(--extra-large-offset) * 2.1); + max-width: calc(var(--extra-large-offset) * 4); + background-color: var(--noble-white-100); + gap: var(--extra-small-offset); +} + +.userMessage { + grid-column: 2 span; + grid-row: 1; + padding: var(--extra-small-offset); + font: var(--extra-regular-font); + letter-spacing: var(--one); + text-align: center; + color: var(--steam-green-800); + cursor: default; +} + +.cancelButton, +.confirmButton { + @include green-btn; + + grid-row: 2; +} + +.cancelButton { + grid-column: 1; +} + +.confirmButton { + grid-column: 2; +} diff --git a/src/shared/Modal/model/ModalModel.ts b/src/shared/Modal/model/ModalModel.ts index efd461eb..0226ae95 100644 --- a/src/shared/Modal/model/ModalModel.ts +++ b/src/shared/Modal/model/ModalModel.ts @@ -11,6 +11,10 @@ class ModalModel { this.view.hide(); } + public removeContent(): void { + this.view.removeContent(); + } + public setContent(content: HTMLElement): void { this.view.setContent(content); } diff --git a/src/shared/Modal/view/ModalView.ts b/src/shared/Modal/view/ModalView.ts index 53040e4f..41527372 100644 --- a/src/shared/Modal/view/ModalView.ts +++ b/src/shared/Modal/view/ModalView.ts @@ -1,3 +1,4 @@ +import clearOutElement from '@/shared/utils/clearOutElement.ts'; import createBaseElement from '@/shared/utils/createBaseElement.ts'; import styles from './modalView.module.scss'; @@ -76,8 +77,12 @@ class ModalView { document.body.classList.remove('stop-scroll'); } + public removeContent(): void { + clearOutElement(this.modalContent); + } + public setContent(content: HTMLElement): void { - this.modalContent.innerHTML = ''; + clearOutElement(this.modalContent); this.modalContent.append(content); } diff --git a/src/shared/Store/actions.ts b/src/shared/Store/actions.ts index a173276a..10bb4fac 100644 --- a/src/shared/Store/actions.ts +++ b/src/shared/Store/actions.ts @@ -11,6 +11,7 @@ const ACTION = { SET_BILLING_COUNTRY: 'setBillingCountry', SET_CURRENT_LANGUAGE: 'setCurrentLanguage', SET_CURRENT_PAGE: 'setCurrentPage', + SET_DEFAULT_COUNTRY: 'setDefaultCountry', SET_SHIPPING_COUNTRY: 'setShippingCountry', SWITCH_APP_THEME: 'switchAppTheme', SWITCH_IS_USER_LOGGED_IN: 'switchIsUserLoggedIn', @@ -65,6 +66,11 @@ export const setShippingCountry = (value: string): ActionWithPayload => ({ + payload: value, + type: ACTION.SET_DEFAULT_COUNTRY, +}); + export const setCurrentLanguage = ( value: LanguageChoiceType, ): ActionWithPayload => ({ diff --git a/src/shared/Store/observer.ts b/src/shared/Store/observer.ts index c514c9da..0d874897 100644 --- a/src/shared/Store/observer.ts +++ b/src/shared/Store/observer.ts @@ -50,6 +50,8 @@ export const selectBillingCountry = (state: State): string => state.billingCount export const selectShippingCountry = (state: State): string => state.shippingCountry; +export const selectDefaultCountry = (state: State): string => state.defaultCountry; + export const selectCurrentLanguage = (state: State): string => state.currentLanguage; export const selectIsUserLoggedIn = (state: State): boolean => state.isUserLoggedIn; diff --git a/src/shared/Store/reducer.ts b/src/shared/Store/reducer.ts index 2040f620..4e74adb7 100644 --- a/src/shared/Store/reducer.ts +++ b/src/shared/Store/reducer.ts @@ -14,6 +14,7 @@ export interface State { billingCountry: string; currentLanguage: LanguageChoiceType; currentPage: PageIdType | null; + defaultCountry: string; isAppThemeLight: boolean; isUserLoggedIn: boolean; shippingCountry: string; @@ -54,6 +55,11 @@ export const rootReducer: Reducer = (state: State, action: Action ...state, billingCountry: action.payload, }; + case 'setDefaultCountry': + return { + ...state, + defaultCountry: action.payload, + }; case 'setCurrentLanguage': return { ...state, diff --git a/src/shared/Store/test.spec.ts b/src/shared/Store/test.spec.ts index feaca518..678e2a49 100644 --- a/src/shared/Store/test.spec.ts +++ b/src/shared/Store/test.spec.ts @@ -61,6 +61,7 @@ vi.mock('./Store.ts', async (importOriginal) => { billingCountry: '', currentLanguage: 'en', currentPage: '/', + defaultCountry: '', isAppThemeLight: true, isUserLoggedIn: false, shippingCountry: '', @@ -176,6 +177,7 @@ describe('rootReducer', () => { billingCountry: '', currentLanguage: 'en', currentPage: '/', + defaultCountry: '', isAppThemeLight: true, isUserLoggedIn: false, shippingCountry: '', diff --git a/src/shared/constants/buttons.ts b/src/shared/constants/buttons.ts index 0035db11..f17587d8 100644 --- a/src/shared/constants/buttons.ts +++ b/src/shared/constants/buttons.ts @@ -6,9 +6,11 @@ export const BUTTON_TYPE = { export const BUTTON_TEXT = { en: { + ADD_ADDRESS: 'Add address', ADD_PRODUCT: 'Add to cart', BACK_TO_MAIN: 'Back to main', CANCEL: 'Cancel', + CONFIRM: 'Confirm', DELETE_PRODUCT: 'Remove from cart', EDIT_INFO: 'Edit', LOG_OUT: 'Log out', @@ -18,9 +20,11 @@ export const BUTTON_TEXT = { SAVE_CHANGES: 'Save changes', }, ru: { + ADD_ADDRESS: 'Добавить адрес', ADD_PRODUCT: 'Добавить в корзину', BACK_TO_MAIN: 'Вернуться на главную', CANCEL: 'Отмена', + CONFIRM: 'Подтвердить', DELETE_PRODUCT: 'Удалить из корзины', EDIT_INFO: 'Редактировать', LOG_OUT: 'Выйти', @@ -32,9 +36,11 @@ export const BUTTON_TEXT = { } as const; export const BUTTON_TEXT_KEYS = { + ADD_ADDRESS: 'ADD_ADDRESS', ADD_PRODUCT: 'ADD_PRODUCT', BACK_TO_MAIN: 'BACK_TO_MAIN', CANCEL: 'CANCEL', + CONFIRM: 'CONFIRM', DELETE_PRODUCT: 'DELETE_PRODUCT', EDIT_INFO: 'EDIT_INFO', LOG_OUT: 'LOG_OUT', diff --git a/src/shared/constants/confirmUserMessage.ts b/src/shared/constants/confirmUserMessage.ts new file mode 100644 index 00000000..0f225c48 --- /dev/null +++ b/src/shared/constants/confirmUserMessage.ts @@ -0,0 +1,17 @@ +export const USER_MESSAGE = { + en: { + CONFIRM: 'Are you sure you want to proceed?', + DELETE_ADDRESS: 'Are you sure you want to delete this address?', + }, + ru: { + CONFIRM: 'Вы уверены, что хотите продолжить?', + DELETE_ADDRESS: 'Вы уверены, что хотите удалить этот адрес?', + }, +} as const; + +export const USER_MESSAGE_KEYS = { + CONFIRM: 'CONFIRM', + DELETE_ADDRESS: 'DELETE_ADDRESS', +}; + +export type UserMessageKeysType = (typeof USER_MESSAGE_KEYS)[keyof typeof USER_MESSAGE_KEYS]; diff --git a/src/shared/constants/forms/text.ts b/src/shared/constants/forms/text.ts index 7b26a950..55e4ef7a 100644 --- a/src/shared/constants/forms/text.ts +++ b/src/shared/constants/forms/text.ts @@ -1,11 +1,13 @@ export const TITLE_TEXT = { en: { + ADDRESS: 'Address', BILLING_ADDRESS: 'Billing Address', CREDENTIALS: 'Credentials', PERSONAL: 'Personal', SHIPPING_ADDRESS: 'Shipping Address', }, ru: { + ADDRESS: 'Адрес', BILLING_ADDRESS: 'Адрес выставления счетов', CREDENTIALS: 'Логин и пароль', PERSONAL: 'Личные данные', @@ -14,6 +16,7 @@ export const TITLE_TEXT = { } as const; export const TITLE_TEXT_KEYS = { + ADDRESS: 'ADDRESS', BILLING_ADDRESS: 'BILLING_ADDRESS', CREDENTIALS: 'CREDENTIALS', PERSONAL: 'PERSONAL', diff --git a/src/shared/constants/initialState.ts b/src/shared/constants/initialState.ts index 28067d7e..6b374acd 100644 --- a/src/shared/constants/initialState.ts +++ b/src/shared/constants/initialState.ts @@ -10,6 +10,7 @@ const initialState: State = { billingCountry: '', currentLanguage: 'en', currentPage: PAGE_ID.DEFAULT_PAGE, + defaultCountry: '', isAppThemeLight: true, isUserLoggedIn: false, shippingCountry: '', diff --git a/src/shared/constants/tooltip.ts b/src/shared/constants/tooltip.ts index fabd927b..f364d439 100644 --- a/src/shared/constants/tooltip.ts +++ b/src/shared/constants/tooltip.ts @@ -2,24 +2,27 @@ const TOOLTIP_TEXT: Record> = { en: { ADD_BILLING_ADDRESS: 'Add new billing address', ADD_SHIPPING_ADDRESS: 'Add new shipping address', - DELETE_ADDRESS: 'Delete address', + DELETE_ADDRESS: 'Delete address completely', EDIT_ADDRESS: 'Edit address', - EDIT_BILLING_ADDRESS: 'Edit billing address', - EDIT_DEFAULT_BILLING_ADDRESS: 'Edit default billing address', - EDIT_DEFAULT_SHIPPING_ADDRESS: 'Edit default shipping address', EDIT_PASSWORD: 'Edit password', - EDIT_SHIPPING_ADDRESS: 'Edit shipping address', + EDIT_SHIPPING_ADDRESS: 'Switch shipping address status', + SWITCH_ADDRESS_STATUS: 'Change address status', + SWITCH_BILLING_ADDRESS: 'Switch billing address status', + SWITCH_DEFAULT_BILLING_ADDRESS: 'Switch default billing address status', + SWITCH_DEFAULT_SHIPPING_ADDRESS: 'Switch default shipping address status', + SWITCH_SHIPPING_ADDRESS: 'Switch shipping address status', }, ru: { ADD_BILLING_ADDRESS: 'Добавить новый адрес выставления счетов', ADD_SHIPPING_ADDRESS: 'Добавить новый адрес доставки', - DELETE_ADDRESS: 'Удалить адрес', + DELETE_ADDRESS: 'Удалить адрес полностью', EDIT_ADDRESS: 'Изменить адрес', - EDIT_BILLING_ADDRESS: 'Изменить адрес выставления счетов', - EDIT_DEFAULT_BILLING_ADDRESS: 'Изменить адрес выставления счетов по умолчанию', - EDIT_DEFAULT_SHIPPING_ADDRESS: 'Изменить адрес доставки по умолчанию', EDIT_PASSWORD: 'Изменить пароль', - EDIT_SHIPPING_ADDRESS: 'Изменить адрес доставки', + SWITCH_ADDRESS_STATUS: 'Изменить статус адреса', + SWITCH_BILLING_ADDRESS: 'Изменить статус адреса выставления счетов', + SWITCH_DEFAULT_BILLING_ADDRESS: 'Изменить статус адреса выставления счетов по умолчанию', + SWITCH_DEFAULT_SHIPPING_ADDRESS: 'Изменить статус адреса доставки по умолчанию', + SWITCH_SHIPPING_ADDRESS: 'Изменить статус адреса доставки', }, } as const; @@ -28,11 +31,12 @@ export const TOOLTIP_TEXT_KEYS = { ADD_SHIPPING_ADDRESS: 'ADD_SHIPPING_ADDRESS', DELETE_ADDRESS: 'DELETE_ADDRESS', EDIT_ADDRESS: 'EDIT_ADDRESS', - EDIT_BILLING_ADDRESS: 'EDIT_BILLING_ADDRESS', - EDIT_DEFAULT_BILLING_ADDRESS: 'EDIT_DEFAULT_BILLING_ADDRESS', - EDIT_DEFAULT_SHIPPING_ADDRESS: 'EDIT_DEFAULT_SHIPPING_ADDRESS', EDIT_PASSWORD: 'EDIT_PASSWORD', - EDIT_SHIPPING_ADDRESS: 'EDIT_SHIPPING_ADDRESS', + SWITCH_ADDRESS_STATUS: 'SWITCH_ADDRESS_STATUS', + SWITCH_BILLING_ADDRESS: 'SWITCH_BILLING_ADDRESS', + SWITCH_DEFAULT_BILLING_ADDRESS: 'SWITCH_DEFAULT_BILLING_ADDRESS', + SWITCH_DEFAULT_SHIPPING_ADDRESS: 'SWITCH_DEFAULT_SHIPPING_ADDRESS', + SWITCH_SHIPPING_ADDRESS: 'SWITCH_SHIPPING_ADDRESS', }; export type TooltipTextKeysType = (typeof TOOLTIP_TEXT_KEYS)[keyof typeof TOOLTIP_TEXT_KEYS]; diff --git a/src/shared/types/address.ts b/src/shared/types/address.ts index 9e4c587f..dbb0d38d 100644 --- a/src/shared/types/address.ts +++ b/src/shared/types/address.ts @@ -1,12 +1,13 @@ export const ADDRESS_TYPE = { BILLING: 'billing', + GENERAL: 'general', SHIPPING: 'shipping', } as const; -export const SINGLE_ADDRESS = 'asBilling'; - export type AddressType = (typeof ADDRESS_TYPE)[keyof typeof ADDRESS_TYPE]; +export const SINGLE_ADDRESS = 'asBilling'; + export interface AddressOptions { setAsBilling?: boolean; setDefault?: boolean; diff --git a/src/widgets/Header/view/headerView.module.scss b/src/widgets/Header/view/headerView.module.scss index bc77a222..ec4834d8 100644 --- a/src/widgets/Header/view/headerView.module.scss +++ b/src/widgets/Header/view/headerView.module.scss @@ -239,7 +239,7 @@ .burgerButton.open .burgerLine:nth-child(1) { left: calc(var(--five) + 0.04rem); - top: calc(var(--tiny-offset) * 1.1); + top: calc(var(--tiny-offset) * 1.1); // 11px width: var(--extra-small-offset); // 20px transform: rotate(90deg); transition-delay: 150ms; @@ -256,7 +256,7 @@ .burgerButton.open .burgerLine:nth-child(3) { left: calc(var(--tiny-offset) * 1.3); // 13px top: calc(var(--tiny-offset) * 1.7); // 17px - width: calc(var(--tiny-offset) * 1.4); // 14px + width: calc(var(--tiny-offset) * 1.5); // 15px transform: rotate(-45deg); transition-delay: 100ms; } @@ -289,13 +289,7 @@ + .switchThemeLabelSpan::before { background-color: #a7e599; - transform: translate(calc(var(--small-offset) + var(--small-offset) + calc(var(--two) * 2)), -50%); - } - - @media (max-width: 768px) { - + .switchThemeLabelSpan::before { - transform: translate(calc(var(--small-offset) + var(--small-offset) - calc(var(--two) * 2)), -50%); - } + transform: translate(calc(var(--small-offset) + var(--small-offset) + calc(var(--two) * 0.9)), -50%); } } } @@ -355,10 +349,6 @@ height: calc(calc(var(--small-offset) / 1.5) + var(--extra-small-offset)); cursor: pointer; - @media (max-width: 768px) { - width: var(--large-offset); - } - &:disabled { background-color: var(--noble-gray-300); pointer-events: none; @@ -379,12 +369,6 @@ background-color: #c4c4c4a8; transform: translate(calc(var(--small-offset) + calc(var(--one) * 3.5)), -50%); } - - @media (max-width: 768px) { - + .switchLanguageLabelSpan::before { - transform: translate(calc(var(--small-offset) - calc(var(--one) * 3.5)), -50%); - } - } } } diff --git a/src/widgets/RegistrationForm/model/RegistrationFormModel.ts b/src/widgets/RegistrationForm/model/RegistrationFormModel.ts index 17f79169..322d2e3c 100644 --- a/src/widgets/RegistrationForm/model/RegistrationFormModel.ts +++ b/src/widgets/RegistrationForm/model/RegistrationFormModel.ts @@ -17,14 +17,20 @@ import { ADDRESS_TYPE } from '@/shared/types/address.ts'; import RegistrationFormView from '../view/RegistrationFormView.ts'; class RegisterFormModel { - private addressWrappers: Record = { - [ADDRESS_TYPE.BILLING]: new AddressModel(ADDRESS_TYPE.BILLING, { - setDefault: true, - }), - [ADDRESS_TYPE.SHIPPING]: new AddressModel(ADDRESS_TYPE.SHIPPING, { - setAsBilling: true, - setDefault: true, - }), + private addressWrappers: Record, AddressModel> = { + [ADDRESS_TYPE.BILLING]: new AddressModel( + { + setDefault: true, + }, + ADDRESS_TYPE.BILLING, + ), + [ADDRESS_TYPE.SHIPPING]: new AddressModel( + { + setAsBilling: true, + setDefault: true, + }, + ADDRESS_TYPE.SHIPPING, + ), }; private credentialsWrapper = new CredentialsModel(); diff --git a/src/widgets/UserAddresses/model/UserAddressesModel.ts b/src/widgets/UserAddresses/model/UserAddressesModel.ts index ef6390b8..33f164a0 100644 --- a/src/widgets/UserAddresses/model/UserAddressesModel.ts +++ b/src/widgets/UserAddresses/model/UserAddressesModel.ts @@ -16,10 +16,9 @@ import showErrorMessage from '@/shared/utils/userMessage.ts'; import UserAddressesView from '../view/UserAddressesView.ts'; class UserAddressesModel { - private view: UserAddressesView; + private view = new UserAddressesView(); constructor(user: User) { - this.view = new UserAddressesView(); this.init(user); } From 014986d55b673ef02c60550b8e7d6c96bd20ada5 Mon Sep 17 00:00:00 2001 From: Max <133934232+Kleostro@users.noreply.github.com> Date: Fri, 31 May 2024 15:03:28 +0300 Subject: [PATCH 15/28] fix(RSS-ECOMM-4_34): catalog (#324) * fix: country validation * fix: last link in breadcrumbs * fix: range price slider --- .../view/productCardView.module.scss | 3 -- .../UserAddress/model/UserAddressModel.ts | 2 +- .../view/breadcrumbsView.module.scss | 2 -- .../CountryChoice/model/CountryChoiceModel.ts | 2 ++ .../ProductFilters/view/ProductFiltersView.ts | 34 +++++++++---------- .../ProductPage/model/ProductPageModel.ts | 4 +-- .../view/userProfilePageView.module.scss | 8 ++--- src/widgets/Catalog/model/CatalogModel.ts | 17 ++++------ .../ProductInfo/view/ProductInfoView.ts | 2 +- .../ProductInfo/view/productInfoView.scss | 2 +- 10 files changed, 34 insertions(+), 42 deletions(-) diff --git a/src/entities/ProductCard/view/productCardView.module.scss b/src/entities/ProductCard/view/productCardView.module.scss index 27dd52ac..79afd1df 100644 --- a/src/entities/ProductCard/view/productCardView.module.scss +++ b/src/entities/ProductCard/view/productCardView.module.scss @@ -77,9 +77,6 @@ width: var(--small-offset); height: var(--small-offset); background-color: var(--noble-gray-1000); - transition: - transform 0.2s, - outline 0.2s; backdrop-filter: blur(10px); svg { diff --git a/src/entities/UserAddress/model/UserAddressModel.ts b/src/entities/UserAddress/model/UserAddressModel.ts index d5eba45f..fdbb09ad 100644 --- a/src/entities/UserAddress/model/UserAddressModel.ts +++ b/src/entities/UserAddress/model/UserAddressModel.ts @@ -46,6 +46,7 @@ class UserAddressModel { } } + // eslint-disable-next-line max-lines-per-function private async handleAddressType(user: User, activeType: AddressTypeType, inactive: boolean): Promise { const customerModel = getCustomerModel(); @@ -87,7 +88,6 @@ class UserAddressModel { case ADDRESS_TYPE.DEFAULT_SHIPPING: await customerModel.editCustomer([CustomerModel.actionRemoveShippingAddress(this.currentAddress)], user); break; - default: break; } diff --git a/src/features/Breadcrumbs/view/breadcrumbsView.module.scss b/src/features/Breadcrumbs/view/breadcrumbsView.module.scss index dbc2bcc8..af4de312 100644 --- a/src/features/Breadcrumbs/view/breadcrumbsView.module.scss +++ b/src/features/Breadcrumbs/view/breadcrumbsView.module.scss @@ -53,8 +53,6 @@ .active { color: var(--steam-green-800); opacity: 1; - cursor: auto; - pointer-events: none; &::after { opacity: 1; diff --git a/src/features/CountryChoice/model/CountryChoiceModel.ts b/src/features/CountryChoice/model/CountryChoiceModel.ts index 5225d997..9a0ff6db 100644 --- a/src/features/CountryChoice/model/CountryChoiceModel.ts +++ b/src/features/CountryChoice/model/CountryChoiceModel.ts @@ -64,6 +64,8 @@ class CountryChoiceModel { if (currentItem.textContent) { inputHTML.value = currentItem.textContent; this.setCountryToStore(currentItem, inputHTML.getAttribute(DATA_KEYS.ADDRESS_TYPE) ?? ''); + const event = new Event('input'); + input.dispatchEvent(event); this.view.hideCountryChoice(); } }); diff --git a/src/features/ProductFilters/view/ProductFiltersView.ts b/src/features/ProductFilters/view/ProductFiltersView.ts index 14986f86..c792a326 100644 --- a/src/features/ProductFilters/view/ProductFiltersView.ts +++ b/src/features/ProductFilters/view/ProductFiltersView.ts @@ -1,4 +1,4 @@ -import type { PriceRange, SizeProductCount } from '@/shared/API/types/type'; +import type { SizeProductCount } from '@/shared/API/types/type'; import type { Category } from '@/shared/types/product'; import type ProductFiltersParams from '@/shared/types/productFilters'; @@ -263,6 +263,7 @@ class ProductFiltersView { link.getHTML().addEventListener('click', (event) => { event.preventDefault(); + RouterModel.setSearchParams(SEARCH_PARAMS_FIELD.META, id); RouterModel.deleteSearchParams(SEARCH_PARAMS_FIELD.PAGE); this.metaLinks.forEach((link) => this.switchSelectedFilter(link, false)); @@ -556,6 +557,19 @@ class ProductFiltersView { this.callback(); } + private updatePriceRange(): void { + this.priceSlider.updateOptions( + { + start: [this.params?.priceRange?.min ?? 0, this.params?.priceRange?.max ?? 0], + }, + true, + ); + const fromInput = this.priceInputs.get(PRICE_RANGE_LABEL[getStore().getState().currentLanguage].FROM); + const toInput = this.priceInputs.get(PRICE_RANGE_LABEL[getStore().getState().currentLanguage].TO); + fromInput?.setValue((this.params?.priceRange?.min ?? 0).toFixed(2)); + toInput?.setValue((this.params?.priceRange?.max ?? 0).toFixed(2)); + } + private updateSelectedPrice(from: InputModel | null = null, to: InputModel | null = null): void { RouterModel.setSearchParams(SEARCH_PARAMS_FIELD.MIN_PRICE, from?.getValue() ?? '0'); RouterModel.setSearchParams(SEARCH_PARAMS_FIELD.MAX_PRICE, to?.getValue() ?? '0'); @@ -638,25 +652,9 @@ class ProductFiltersView { const currentSpan = span; currentSpan.innerText = BASE_PRODUCT_COUNT; }); + this.updatePriceRange(); this.redrawProductsCount(); } - - public updatePriceRange(params: PriceRange): void { - if (this.params) { - this.params.priceRange = params; - } - this.priceSlider.updateOptions( - { - range: { max: params.max ?? 0, min: params.min ?? 0 }, - start: [params.min ?? 0, params.max ?? 0], - }, - true, - ); - const fromInput = this.priceInputs.get(PRICE_RANGE_LABEL[getStore().getState().currentLanguage].FROM); - const toInput = this.priceInputs.get(PRICE_RANGE_LABEL[getStore().getState().currentLanguage].TO); - fromInput?.setValue(params.min.toFixed(2) ?? ''); - toInput?.setValue(params.max.toFixed(2) ?? ''); - } } export default ProductFiltersView; diff --git a/src/pages/ProductPage/model/ProductPageModel.ts b/src/pages/ProductPage/model/ProductPageModel.ts index f30079a1..6ba758e5 100644 --- a/src/pages/ProductPage/model/ProductPageModel.ts +++ b/src/pages/ProductPage/model/ProductPageModel.ts @@ -47,9 +47,9 @@ class ProductPageModel implements Page { }); } - if (subcategory) { + if (subcategory && category) { links.push({ - link: '', + link: buildPathName(PAGE_ID.CATALOG_PAGE, null, { category: [category.id], subcategory: [subcategory.id] }), name: subcategory.name[0].value, }); } diff --git a/src/pages/UserProfilePage/view/userProfilePageView.module.scss b/src/pages/UserProfilePage/view/userProfilePageView.module.scss index 07614830..e0bada7c 100644 --- a/src/pages/UserProfilePage/view/userProfilePageView.module.scss +++ b/src/pages/UserProfilePage/view/userProfilePageView.module.scss @@ -45,7 +45,6 @@ width: 25%; height: fit-content; background-color: var(--white); - gap: var(--extra-small-offset); } .accountMenuItem { @@ -69,7 +68,7 @@ align-items: center; align-self: center; width: 100%; - height: 2.5rem; + height: 4rem; font: var(--regular-font); letter-spacing: var(--one); text-align: center; @@ -81,7 +80,7 @@ content: ''; position: absolute; left: 0; - bottom: -5px; + bottom: 1rem; width: 100%; height: var(--two); background-color: currentcolor; @@ -90,7 +89,8 @@ transform-origin: center; transition: transform 0.2s, - opacity 0.2s; + opacity 0.2s, + background-color 0.2s; } @media (hover: hover) { diff --git a/src/widgets/Catalog/model/CatalogModel.ts b/src/widgets/Catalog/model/CatalogModel.ts index 4b46a8d3..36cc289e 100644 --- a/src/widgets/Catalog/model/CatalogModel.ts +++ b/src/widgets/Catalog/model/CatalogModel.ts @@ -1,4 +1,4 @@ -import type { OptionsRequest, PriceRange, SortOptions } from '@/shared/API/types/type.ts'; +import type { OptionsRequest, SortOptions } from '@/shared/API/types/type.ts'; import type ProductFiltersParams from '@/shared/types/productFilters.ts'; import type { SelectedFilters } from '@/shared/types/productFilters.ts'; import type { SelectedSorting } from '@/shared/types/productSorting.ts'; @@ -54,7 +54,7 @@ class CatalogModel { } } - private decodeSearchParams(priceRange: PriceRange): { + private decodeSearchParams(): { page: string; searchValue: null | string; selectedFilters: SelectedFilters; @@ -67,8 +67,8 @@ class CatalogModel { const metaFilter = searchParams.get(SEARCH_PARAMS_FIELD.META) ?? META_FILTERS.en.ALL_PRODUCTS; const size = searchParams.get(SEARCH_PARAMS_FIELD.SIZE) ?? null; const price = { - max: parseFloat(searchParams.get(SEARCH_PARAMS_FIELD.MAX_PRICE) ?? priceRange.max.toString()), - min: parseFloat(searchParams.get(SEARCH_PARAMS_FIELD.MIN_PRICE) ?? priceRange.min.toString()), + max: parseFloat(searchParams.get(SEARCH_PARAMS_FIELD.MAX_PRICE) ?? '0'), + min: parseFloat(searchParams.get(SEARCH_PARAMS_FIELD.MIN_PRICE) ?? '0'), }; const field = searchParams.get(SEARCH_PARAMS_FIELD.FIELD); @@ -93,7 +93,7 @@ class CatalogModel { private async drawProducts(): Promise { const productList = this.view.getItemsList(); productList.innerHTML = ''; - const options = await this.getOptions(); + const options = this.getOptions(); const productsInfo = await this.getProductsInfo(options); this.pagination?.getHTML().remove(); if (productsInfo?.products?.length) { @@ -112,16 +112,13 @@ class CatalogModel { this.view.getRightTopWrapper().append(this.pagination.getHTML()); } this.productFilters?.getView().updateParams(productsInfo); - const priceRange = await getProductModel().getPriceRange(); - this.productFilters?.getView().updatePriceRange(priceRange); this.view.switchEmptyList(!productsInfo?.products?.length); } - private async getOptions(): Promise { + private getOptions(): OptionsRequest { let result = {}; - const priceRange = await getProductModel().getPriceRange(); - const { page, searchValue, selectedFilters, selectedSorting } = this.decodeSearchParams(priceRange); + const { page, searchValue, selectedFilters, selectedSorting } = this.decodeSearchParams(); this.productFilters?.getView().setInitialActiveFilters({ categoryLinks: Array.from(selectedFilters.category), metaLinks: selectedFilters.metaFilter ? [selectedFilters.metaFilter] : [], diff --git a/src/widgets/ProductInfo/view/ProductInfoView.ts b/src/widgets/ProductInfo/view/ProductInfoView.ts index 0cbfbcf2..4968af4c 100644 --- a/src/widgets/ProductInfo/view/ProductInfoView.ts +++ b/src/widgets/ProductInfo/view/ProductInfoView.ts @@ -205,7 +205,7 @@ class ProductInfoView { private createProductTitle(): HTMLHeadingElement { this.title = createBaseElement({ - cssClasses: ['title'], + cssClasses: ['productTitle'], innerContent: this.params.name[Number(getStore().getState().currentLanguage === LANGUAGE_CHOICE.RU)].value, tag: 'h3', }); diff --git a/src/widgets/ProductInfo/view/productInfoView.scss b/src/widgets/ProductInfo/view/productInfoView.scss index fa838219..2fac3382 100644 --- a/src/widgets/ProductInfo/view/productInfoView.scss +++ b/src/widgets/ProductInfo/view/productInfoView.scss @@ -33,7 +33,7 @@ } } -.title { +.productTitle { order: 1; font: var(--medium-font); letter-spacing: var(--one); From 77232bf3d6a434504d6ef178dd5c448db87a741e Mon Sep 17 00:00:00 2001 From: Meg G <146496794+stardustmeg@users.noreply.github.com> Date: Fri, 31 May 2024 18:07:26 +0300 Subject: [PATCH 16/28] feat(RSS-ECOMM-4_35): update address editing (#325) * feat: update errorMessage to include a custom message * feat: update edit to remove default only * fix: typo in title * feat: add loader for address request * refactor: replace bin picture --- .../UserAddress/model/UserAddressModel.ts | 80 +++++++++---------- .../UserAddress/view/UserAddressView.ts | 10 ++- src/pages/Blog/Post/view/PostView.ts | 12 +-- src/pages/Blog/PostList/view/PostListView.ts | 4 +- .../Blog/PostWidget/view/PostWidgetView.ts | 4 +- src/pages/Blog/data/posts.ts | 16 ++-- src/pages/CartPage/view/CartPageView.ts | 62 +++++++------- .../API/customer/model/CustomerModel.ts | 4 +- src/shared/constants/blog.ts | 2 +- src/shared/constants/pages.ts | 8 +- src/shared/img/svg/delete.svg | 8 +- src/shared/utils/userMessage.ts | 17 +++- src/widgets/Footer/view/FooterView.ts | 4 +- .../ProductOrder/view/ProductOrderView.ts | 18 ++--- .../UserAddresses/model/UserAddressesModel.ts | 10 ++- .../UserAddresses/view/UserAddressesView.ts | 4 + .../view/userAddressesView.module.scss | 5 ++ 17 files changed, 149 insertions(+), 119 deletions(-) diff --git a/src/entities/UserAddress/model/UserAddressModel.ts b/src/entities/UserAddress/model/UserAddressModel.ts index fdbb09ad..dbc0f92e 100644 --- a/src/entities/UserAddress/model/UserAddressModel.ts +++ b/src/entities/UserAddress/model/UserAddressModel.ts @@ -4,23 +4,34 @@ import AddressEditModel from '@/features/AddressEdit/model/AddressEditModel.ts'; import getCustomerModel, { CustomerModel } from '@/shared/API/customer/model/CustomerModel.ts'; import ConfirmModel from '@/shared/Confirm/model/ConfirmModel.ts'; import EventMediatorModel from '@/shared/EventMediator/model/EventMediatorModel.ts'; +import LoaderModel from '@/shared/Loader/model/LoaderModel.ts'; import modal from '@/shared/Modal/model/ModalModel.ts'; import serverMessageModel from '@/shared/ServerMessage/model/ServerMessageModel.ts'; import MEDIATOR_EVENT from '@/shared/constants/events.ts'; import { ADDRESS_TYPE, type AddressTypeType } from '@/shared/constants/forms.ts'; import { MESSAGE_STATUS, SERVER_MESSAGE_KEYS } from '@/shared/constants/messages.ts'; +import { LOADER_SIZE } from '@/shared/constants/sizes.ts'; import showErrorMessage from '@/shared/utils/userMessage.ts'; import UserAddressView from '../view/UserAddressView.ts'; class UserAddressModel { + private callback: (isDisabled: boolean) => void; + private currentAddress: Address; private labels: Map; private view: UserAddressView; - constructor(user: User, address: Address, activeTypes: AddressTypeType[], inactiveTypes?: AddressTypeType[]) { + constructor( + user: User, + address: Address, + activeTypes: AddressTypeType[], + callback: (isDisabled: boolean) => void, + inactiveTypes?: AddressTypeType[], + ) { + this.callback = callback; this.currentAddress = address; this.view = new UserAddressView(user.locale, address, activeTypes, inactiveTypes); this.labels = this.view.getLabels(); @@ -50,51 +61,33 @@ class UserAddressModel { private async handleAddressType(user: User, activeType: AddressTypeType, inactive: boolean): Promise { const customerModel = getCustomerModel(); - if (inactive) { - switch (activeType) { - case ADDRESS_TYPE.BILLING: - await customerModel.editCustomer([CustomerModel.actionAddBillingAddress(this.currentAddress.id)], user); - break; - - case ADDRESS_TYPE.SHIPPING: - await customerModel.editCustomer([CustomerModel.actionAddShippingAddress(this.currentAddress.id)], user); - break; - - case ADDRESS_TYPE.DEFAULT_BILLING: - await customerModel.editCustomer( - [CustomerModel.actionEditDefaultBillingAddress(this.currentAddress.id)], - user, - ); - break; - - case ADDRESS_TYPE.DEFAULT_SHIPPING: - await customerModel.editCustomer( - [CustomerModel.actionEditDefaultShippingAddress(this.currentAddress.id)], - user, - ); - break; - - default: - break; - } - } else { - switch (activeType) { - case ADDRESS_TYPE.BILLING: - case ADDRESS_TYPE.DEFAULT_BILLING: - await customerModel.editCustomer([CustomerModel.actionRemoveBillingAddress(this.currentAddress)], user); - break; - - case ADDRESS_TYPE.SHIPPING: - case ADDRESS_TYPE.DEFAULT_SHIPPING: - await customerModel.editCustomer([CustomerModel.actionRemoveShippingAddress(this.currentAddress)], user); - break; - default: - break; - } + const actions = { + [ADDRESS_TYPE.BILLING]: inactive + ? CustomerModel.actionAddBillingAddress(this.currentAddress.id) + : CustomerModel.actionRemoveBillingAddress(this.currentAddress), + [ADDRESS_TYPE.DEFAULT_BILLING]: inactive + ? CustomerModel.actionEditDefaultBillingAddress(this.currentAddress.id) + : CustomerModel.actionEditDefaultBillingAddress(undefined), + [ADDRESS_TYPE.DEFAULT_SHIPPING]: inactive + ? CustomerModel.actionEditDefaultShippingAddress(this.currentAddress.id) + : CustomerModel.actionEditDefaultShippingAddress(undefined), + [ADDRESS_TYPE.SHIPPING]: inactive + ? CustomerModel.actionAddShippingAddress(this.currentAddress.id) + : CustomerModel.actionRemoveShippingAddress(this.currentAddress), + }; + + const action = actions[activeType]; + + if (action) { + await customerModel.editCustomer([action], user); } } private async labelClickHandler(activeType: AddressTypeType, inactive?: boolean): Promise { + const loader = new LoaderModel(LOADER_SIZE.LARGE); + this.callback(true); + loader.setAbsolutePosition(); + this.view.getHTML().append(loader.getHTML()); try { const user = await getCustomerModel().getCurrentUser(); if (user) { @@ -104,6 +97,9 @@ class UserAddressModel { } } catch (error) { showErrorMessage(error); + } finally { + loader.getHTML().remove(); + this.callback(false); } } diff --git a/src/entities/UserAddress/view/UserAddressView.ts b/src/entities/UserAddress/view/UserAddressView.ts index 4288a7cf..ff52f9b6 100644 --- a/src/entities/UserAddress/view/UserAddressView.ts +++ b/src/entities/UserAddress/view/UserAddressView.ts @@ -67,7 +67,7 @@ class UserAddressView { private createDeleteLogo(): HTMLDivElement { this.deleteLogo = createBaseElement({ cssClasses: [styles.deleteLogo], tag: 'div' }); const svg = document.createElementNS(SVG_DETAILS.SVG_URL, 'svg'); - svg.append(createSVGUse(SVG_DETAILS.BIN)); + svg.append(createSVGUse(SVG_DETAILS.DELETE)); this.deleteLogo.append(svg); return this.deleteLogo; } @@ -205,6 +205,14 @@ class UserAddressView { public getLabels(): Map { return this.labels; } + + public setDisabled(): void { + this.view.classList.add(styles.disabled); + } + + public setEnabled(): void { + this.view.classList.remove(styles.disabled); + } } export default UserAddressView; diff --git a/src/pages/Blog/Post/view/PostView.ts b/src/pages/Blog/Post/view/PostView.ts index f8b698f6..1647263e 100644 --- a/src/pages/Blog/Post/view/PostView.ts +++ b/src/pages/Blog/Post/view/PostView.ts @@ -45,10 +45,10 @@ export default class PostView { const content = `
- ${this.post.tittle[ln]} + ${this.post.title[ln]}

${this.post.date[ln]} | ${read}

-

${this.post.tittle[ln]}

+

${this.post.title[ln]}

${this.post.shortDescription[ln]}

${readMore}

@@ -80,16 +80,16 @@ export default class PostView { this.post.content[ln] = this.post.content[ln].replace(/
/g, `

`); this.post.content[ln] = this.post.content[ln].replace(/