From 0a0d5e183cca8d6969d02170f9c4f673808d23c9 Mon Sep 17 00:00:00 2001 From: Meg G <146496794+stardustmeg@users.noreply.github.com> Date: Tue, 4 Jun 2024 01:17:28 +0300 Subject: [PATCH] chore: update production deploy with implemented features from [sprint_4] (#334) * chore(RSS-ECOMM-4_00): update sprint number (#309) chore: update sprint number * 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> * 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 * feat(RSS-ECOMM-4_31): adaptive (#314) * feat: add scroll top on every page * feat: adaptive noUiSlider * feat: add adaptive up to 380px * feat(RSS-ECOMM-4_98): add level to product (#313) feat: add level to product * feat(RSS-ECOMM-4_97): add address actions (#315) feat: add address actions * feat(RSS-ECOMM-4_96): add total count for products (#316) feat: add total count for products * 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 * feat(RSS-ECOMM-4_32): display difficultyPlants (#319) * feat: display difficultyPlants * fix: position difficulty plants * 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 * fix(RSS-ECOMM-4_33): reloading product page (#320) fix: reloading product page * 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 * feat(RSS-ECOMM-4_97): filter range (#323) * feat: filter Type * feat: catalog filter * feat: filter price range * 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 * fix(RSS-ECOMM-4_34): catalog (#324) * fix: country validation * fix: last link in breadcrumbs * fix: range price slider * 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 * fix(RSS-ECOMM-4_36): user addresses (#326) * fix: validation postal code * fix: display user addresses * feat(RSS-ECOMM-4_37): display product discount (#327) * feat: display product discount * fix: submit form addresses * feat: increase the interactive area of the description button on the product card * fix(RSS-ECOMM-4_96): cart blog footer (#328) * fix: color for dark theme * fix: subcategory link in footer navigation * fix: delete submit for subscribe button * fix: scroll after swipe * fix: price with discount for cart * fix: sort price and cart message * Merge branch 'sprint-4' into fix(RSS-ECOMM-4_96)/cartBlogFooter * refactor: delete comments * feat(RSS-ECOMM-4_37): update app themes (#329) * fix: replace notFound image * feat: implement redirection for unauth user * feat: translate app title * refactor: update setting app title * refactor: break down into smaller functions * refactor: update blog titles * feat: update themes colors * refactor: make two tumblers same size * fix: addresses colors * fix: footer logo margin * fix: discount label text color * feat(RSS-ECOMM-4_38): scroll to top button (#330) * feat: implement scrollToTop button * fix: binding context * refactor: update confirm component * refactor: update address edit buttons * fix: table heading color * refactor(RSS-ECOMM-4_39): search params (#331) * feat(RSS-ECOMM-4_95): add confirm and link (#333) * feat: add confirm and link * fix: cart request * refactor(RSS-ECOMM-4_40): update password edit form (#332) * refactor: replace div with form * feat: add mixin for repeated links * refactor: update footer links * fix: footer paddings * fix: add semantics to contact information * fix: add transition to blog articles shadow * refactor: replace to catalog link with mixin * refactor: discard redundant user check * chore: update CI/CD for sprint 5 * chore: solve conflicts * fix: after merge develop * fix: footer input not required * fix: validation params --------- Co-authored-by: Max <133934232+Kleostro@users.noreply.github.com> Co-authored-by: Yuliya Kursevich <54816946+YulikK@users.noreply.github.com> --- .github/labeler.yml | 3 + .github/pull_request_template.md | 4 +- .github/workflows/ci.yml | 1 + .validate-branch-namerc.cjs | 6 +- package.json | 4 + 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/app/App/model/AppModel.ts | 8 +- src/app/Router/model/RouterModel.ts | 78 ++-- src/app/styles/mixins.scss | 173 +++++++- src/app/styles/noUiSlider.scss | 21 +- src/app/styles/variables.scss | 129 +++--- src/entities/Address/model/AddressModel.ts | 18 +- src/entities/Address/view/AddressView.ts | 28 +- .../Address/view/addressView.module.scss | 1 + .../view/navigationView.module.scss | 55 +-- .../ProductCard/view/ProductCardView.ts | 32 ++ .../view/productCardView.module.scss | 41 +- .../model/ProductModalSliderModel.ts | 9 +- .../view/ProductModalSliderView.ts | 13 +- .../view/productModalSliderView.module.scss | 52 ++- .../ProductPrice/view/productPriceView.scss | 6 +- .../UserAddress/model/UserAddressModel.ts | 151 +++++-- .../UserAddress/view/UserAddressView.ts | 294 ++++++++++--- .../view/userAddressView.module.scss | 155 +++++-- .../AddressAdd/model/AddressAddModel.ts | 191 +++++++++ .../AddressAdd/view/AddressAddView.ts | 63 +++ .../view/addressAddView.module.scss | 35 ++ .../AddressEdit/model/AddressEditModel.ts | 258 +++++++----- .../AddressEdit/view/AddressEditView.ts | 4 +- .../view/addressEditView.module.scss | 55 +-- .../view/breadcrumbsView.module.scss | 46 +-- .../CountryChoice/model/CountryChoiceModel.ts | 35 +- .../view/countryChoiceView.module.scss | 4 +- .../Pagination/model/PaginationModel.ts | 22 + .../Pagination/view/PaginationView.ts | 121 ++++++ .../view/paginationView.module.scss | 54 +++ .../PasswordEdit/model/PasswordEditModel.ts | 91 ++-- .../PasswordEdit/view/PasswordEditView.ts | 41 +- .../view/passwordEditView.module.scss | 35 +- .../model/PersonalInfoEditModel.ts | 5 +- .../view/PersonalInfoEditView.ts | 2 +- .../view/personalInfoEditView.module.scss | 27 +- .../model/ProductFiltersModel.ts | 138 +------ .../ProductFilters/view/ProductFiltersView.ts | 187 +++++++-- .../view/productFiltersView.module.scss | 2 +- .../ProductSearch/model/ProductSearchModel.ts | 20 +- .../ProductSearch/view/ProductSearchView.ts | 7 + .../ProductSorts/model/ProductSortsModel.ts | 40 +- .../ProductSorts/view/ProductSortsView.ts | 62 +-- src/pages/AboutUsPage/view/AboutUsPageView.ts | 1 + src/pages/Blog/Post/view/PostView.ts | 13 +- src/pages/Blog/Post/view/post.module.scss | 4 + src/pages/Blog/PostList/view/PostListView.ts | 6 +- .../Blog/PostWidget/view/PostWidgetView.ts | 6 +- src/pages/Blog/data/posts.ts | 34 +- src/pages/CartPage/model/CartPageModel.ts | 87 +++- src/pages/CartPage/view/CartPageView.ts | 249 +++++++++-- .../CartPage/view/cartPageView.module.scss | 89 +++- .../CatalogPage/model/CatalogPageModel.ts | 8 +- src/pages/CatalogPage/view/CatalogPageView.ts | 1 + src/pages/LoginPage/view/LoginPageView.ts | 1 + .../LoginPage/view/loginPageView.module.scss | 36 +- src/pages/MainPage/view/MainPageView.ts | 1 + .../NotFoundPage/view/NotFoundPageView.ts | 1 + .../ProductPage/model/ProductPageModel.ts | 8 +- src/pages/ProductPage/view/ProductPageView.ts | 1 + .../view/RegistrationPageView.ts | 1 + .../view/registrationPageView.module.scss | 36 +- .../model/UserProfilePageModel.ts | 43 +- .../view/UserProfilePageView.ts | 1 + .../view/userProfilePageView.module.scss | 90 +--- src/shared/API/cart/CartApi.ts | 65 ++- src/shared/API/cart/model/CartModel.ts | 186 +++++++-- src/shared/API/customer/CustomerApi.ts | 19 +- .../API/customer/model/CustomerModel.ts | 72 +++- src/shared/API/product/ProductApi.ts | 42 +- src/shared/API/product/model/ProductModel.ts | 144 +++++-- src/shared/API/product/utils/filter.ts | 18 +- src/shared/API/sdk/client.ts | 33 +- src/shared/API/sdk/token-cache/token-cache.ts | 2 +- .../API/shopping-list/ShoppingListApi.ts | 53 ++- .../shopping-list/model/ShoppingListModel.ts | 106 ++++- src/shared/API/types/type.ts | 6 +- src/shared/Confirm/model/ConfirmModel.ts | 52 +++ src/shared/Confirm/view/ConfirmView.ts | 79 ++++ .../Confirm/view/confirmView.module.scss | 41 ++ src/shared/Loader/view/loaderView.module.scss | 3 +- src/shared/Modal/model/ModalModel.ts | 4 + src/shared/Modal/view/ModalView.ts | 7 +- src/shared/Modal/view/modalView.module.scss | 3 +- .../ScrollToTop/model/ScrollToTopModel.ts | 27 ++ .../ScrollToTop/view/ScrollToTopView.ts | 44 ++ .../view/scrollToTopView.module.scss | 45 ++ .../view/serverMessageView.module.scss | 4 +- src/shared/Store/Store.ts | 6 +- src/shared/Store/actions.ts | 42 +- src/shared/Store/observer.ts | 4 +- src/shared/Store/reducer.ts | 34 +- src/shared/Store/test.spec.ts | 30 +- src/shared/constants/blog.ts | 2 +- src/shared/constants/buttons.ts | 9 +- src/shared/constants/common.ts | 4 + src/shared/constants/confirmUserMessage.ts | 19 + src/shared/constants/events.ts | 3 +- src/shared/constants/filters.ts | 6 +- src/shared/constants/forms.ts | 45 ++ src/shared/constants/forms/fieldParams.ts | 15 + src/shared/constants/forms/text.ts | 3 + .../constants/forms/validationParams.ts | 4 + src/shared/constants/initialState.ts | 13 +- src/shared/constants/messages.ts | 43 +- src/shared/constants/pages.ts | 65 ++- src/shared/constants/product.ts | 17 + src/shared/constants/svg.ts | 7 + src/shared/constants/tooltip.ts | 31 +- src/shared/img/png/notFound.png | Bin 108475 -> 101813 bytes src/shared/img/svg/arrowUp.svg | 4 +- src/shared/img/svg/bill.svg | 1 + src/shared/img/svg/delete.svg | 8 +- src/shared/img/svg/delivery.svg | 1 + src/shared/img/svg/edit.svg | 6 +- src/shared/img/svg/leaves.svg | 16 +- src/shared/img/svg/notFound.svg | 1 + src/shared/img/svg/truck.svg | 10 + src/shared/img/svg/wallet.svg | 7 + src/shared/types/address.ts | 5 +- src/shared/types/cart.ts | 8 + src/shared/types/common.ts | 4 + src/shared/types/product.ts | 8 + src/shared/types/productFilters.ts | 1 + src/shared/utils/buildPathname.ts | 2 +- src/shared/utils/determineNewAddress.ts | 127 ++++++ src/shared/utils/hasValue.ts | 5 + src/shared/utils/isKeyOf.ts | 5 +- src/shared/utils/messageTemplates.ts | 4 +- src/shared/utils/setPageTitle.ts | 34 ++ src/shared/utils/size.ts | 18 +- src/shared/utils/userMessage.ts | 13 +- src/widgets/Catalog/model/CatalogModel.ts | 209 ++++++---- src/widgets/Footer/model/FooterModel.ts | 113 +++++ src/widgets/Footer/view/FooterView.ts | 388 +++++++++++++++--- .../Footer/view/footerView.module.scss | 143 +++++-- src/widgets/Header/model/HeaderModel.ts | 23 +- .../Header/view/headerView.module.scss | 102 ++--- src/widgets/LoginForm/model/LoginFormModel.ts | 2 +- .../ProductInfo/model/ProductInfoModel.ts | 40 +- .../ProductInfo/view/ProductInfoView.ts | 69 +++- .../ProductInfo/view/productInfoView.scss | 91 +++- .../ProductOrder/model/ProductOrderModel.ts | 124 ++++-- .../ProductOrder/view/ProductOrderView.ts | 160 ++++++-- .../view/productOrderView.module.scss | 86 ++-- .../model/RegistrationFormModel.ts | 22 +- .../UserAddresses/model/UserAddressesModel.ts | 87 +++- .../UserAddresses/view/UserAddressesView.ts | 74 +++- .../view/userAddressesView.module.scss | 52 +-- src/widgets/UserInfo/model/UserInfoModel.ts | 26 +- .../UserInfo/view/userInfoView.module.scss | 69 +--- 166 files changed, 5252 insertions(+), 1917 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/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/features/Pagination/model/PaginationModel.ts create mode 100644 src/features/Pagination/view/PaginationView.ts create mode 100644 src/features/Pagination/view/paginationView.module.scss 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/ScrollToTop/model/ScrollToTopModel.ts create mode 100644 src/shared/ScrollToTop/view/ScrollToTopView.ts create mode 100644 src/shared/ScrollToTop/view/scrollToTopView.module.scss create mode 100644 src/shared/constants/confirmUserMessage.ts 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/notFound.svg create mode 100644 src/shared/img/svg/truck.svg create mode 100644 src/shared/img/svg/wallet.svg create mode 100644 src/shared/types/common.ts create mode 100644 src/shared/utils/determineNewAddress.ts create mode 100644 src/shared/utils/hasValue.ts create mode 100644 src/shared/utils/setPageTitle.ts diff --git a/.github/labeler.yml b/.github/labeler.yml index 26585235..1f864ee7 100644 --- a/.github/labeler.yml +++ b/.github/labeler.yml @@ -48,3 +48,6 @@ sprint3: sprint4: - head-branch: 'ECOMM-4' + +sprint5: + - head-branch: 'ECOMM-5' diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 8e1c0deb..ba4b9165 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-5_01`, where `5` - is the sprint number and `01` - is the issue number) - [ ] short description -πŸ‘€ Example: `feat(RSS-ECOMM-3_01): description` +πŸ‘€ Example: `feat(RSS-ECOMM-5_01): description` ## PR Description πŸ§™β€β™‚οΈ diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 560f20e6..83fa1194 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -7,6 +7,7 @@ on: - sprint-2 - sprint-3 - sprint-4 + - sprint-5 jobs: format: diff --git a/.validate-branch-namerc.cjs b/.validate-branch-namerc.cjs index 15ea94ee..2c0fefe4 100644 --- a/.validate-branch-namerc.cjs +++ b/.validate-branch-namerc.cjs @@ -1,5 +1,6 @@ module.exports = { - pattern: /^(feat|fix|hotfix|chore|refactor|revert|docs|style|test|)\(RSS-ECOMM-\d_\d{2}\)\/[a-z]+[a-zA-Z0-9]*$/, + pattern: + /^sprint-4|^(feat|fix|hotfix|chore|refactor|revert|docs|style|test|)\(RSS-ECOMM-\d_\d{2}\)\/[a-z]+[a-zA-Z0-9]*$/, errorMsg: 'Please use correct branch name', }; @@ -7,3 +8,6 @@ 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 +// "revert(RSS-ECOMM-5_01)/addNewProduct" // where 5 is the sprint number and 01 is the issue number diff --git a/package.json b/package.json index a67e8a92..363df682 100644 --- a/package.json +++ b/package.json @@ -64,8 +64,11 @@ "@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", + "@types/uuid": "^9.0.8", "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", @@ -75,6 +78,7 @@ "postcode-validator": "^3.8.20", "sharp": "^0.33.3", "swiper": "^11.1.3", + "uuid": "^9.0.1", "vite-plugin-checker": "^0.6.4", "vite-plugin-image-optimizer": "^1.1.7", "vite-plugin-svg-spriter": "^1.0.0", 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/app/App/model/AppModel.ts b/src/app/App/model/AppModel.ts index 9ef6b96e..0adc04b7 100644 --- a/src/app/App/model/AppModel.ts +++ b/src/app/App/model/AppModel.ts @@ -3,6 +3,7 @@ import type { Page, PageParams, PagesType } from '@/shared/types/page.ts'; import RouterModel from '@/app/Router/model/RouterModel.ts'; import modal from '@/shared/Modal/model/ModalModel.ts'; +import ScrollToTopModel from '@/shared/ScrollToTop/model/ScrollToTopModel.ts'; import { PAGE_ID } from '@/shared/constants/pages.ts'; import FooterModel from '@/widgets/Footer/model/FooterModel.ts'; import HeaderModel from '@/widgets/Header/model/HeaderModel.ts'; @@ -32,9 +33,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'); @@ -77,7 +78,8 @@ class AppModel { private async initialize(): Promise { const routes = await this.createRoutes(); const router = new RouterModel(routes); - document.body.append(this.appView.getHTML()); + const scrollToTop = new ScrollToTopModel(); + document.body.append(this.appView.getHTML(), scrollToTop.getHTML().getHTML()); this.appView.getHTML().insertAdjacentElement('beforebegin', new HeaderModel(this.appView.getHTML()).getHTML()); this.appView.getHTML().insertAdjacentElement('afterend', new FooterModel().getHTML()); this.appView.getHTML().insertAdjacentElement('afterend', modal.getHTML()); diff --git a/src/app/Router/model/RouterModel.ts b/src/app/Router/model/RouterModel.ts index dbacb44c..49b0d4f4 100644 --- a/src/app/Router/model/RouterModel.ts +++ b/src/app/Router/model/RouterModel.ts @@ -1,11 +1,12 @@ import type { PageParams, PagesType } from '@/shared/types/page'; +import getStore from '@/shared/Store/Store.ts'; +import observeStore, { selectCurrentLanguage } from '@/shared/Store/observer.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'; +import setPageTitle from '@/shared/utils/setPageTitle.ts'; import showErrorMessage from '@/shared/utils/userMessage.ts'; -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; @@ -23,17 +24,19 @@ class RouterModel { this.routes = routes; document.addEventListener('DOMContentLoaded', () => { - const currentPath = window.location.pathname.slice(NEXT_SEGMENT).split(DEFAULT_SEGMENT) || PAGE_ID.DEFAULT_PAGE; + const currentPath = + (window.location.pathname + window.location.search).slice(NEXT_SEGMENT).split(DEFAULT_SEGMENT) || + PAGE_ID.DEFAULT_PAGE; this.navigateTo(currentPath.join(DEFAULT_SEGMENT)); }); window.addEventListener('popstate', (event) => { const currentState: unknown = event.state; - let currentPage = ''; + let [currentPage] = ''; let currentPath = ''; if (isValidState(currentState)) { - currentPage = currentState.path.split(DEFAULT_SEGMENT)[PATH_SEGMENTS_TO_KEEP] + DEFAULT_SEGMENT; + [currentPage] = currentState.path.split(DEFAULT_SEGMENT)[PATH_SEGMENTS_TO_KEEP].split(SEARCH_SEGMENT); currentPath = currentState.path; } @@ -46,20 +49,51 @@ 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 + url.search.toString(); + window.history.pushState({ path: path.slice(NEXT_SEGMENT) }, '', 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(NEXT_SEGMENT) }, '', path); + } + + public static deleteSearchParams(key: string): void { + const url = new URL(decodeURIComponent(window.location.href)); + url.searchParams.delete(key); + const path = url.pathname + url.search.toString(); + window.history.pushState({ path: path.slice(NEXT_SEGMENT) }, '', 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 + url.search.toString(); + window.history.pushState({ path: path.slice(NEXT_SEGMENT) }, '', path); + } + private async checkPageAndParams( currentPage: string, path: string, ): Promise<{ hasRoute: boolean; params: PageParams } | null> { 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; + const id = path.split(DEFAULT_SEGMENT)[NEXT_SEGMENT]?.split(SEARCH_SEGMENT)[PATH_SEGMENTS_TO_KEEP] || null; + + setPageTitle(currentPage, hasRoute); + observeStore(selectCurrentLanguage, () => this.checkPageAndParams(currentPage, path)); if (!hasRoute) { await this.routes.get(PAGE_ID.NOT_FOUND_PAGE)?.({}); @@ -68,12 +102,7 @@ class RouterModel { return { hasRoute, - params: { - [currentPage.slice(PATH_SEGMENTS_TO_KEEP, -NEXT_SEGMENT)]: { - id: id ?? null, - searchParams: searchParams ?? null, - }, - }, + params: { [currentPage.slice(PATH_SEGMENTS_TO_KEEP)]: { id } }, }; } @@ -88,15 +117,14 @@ 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); + const currentPage = + path.split(DEFAULT_SEGMENT)[PATH_SEGMENTS_TO_KEEP].split(SEARCH_SEGMENT)[PATH_SEGMENTS_TO_KEEP] || + PAGE_ID.DEFAULT_PAGE; + + if (currentPage !== getStore().getState().currentPage || currentPage === PAGE_ID.DEFAULT_PAGE) { + this.handleRequest(currentPage, path); + } + history.pushState({ path }, '', `/${path}`); } } diff --git a/src/app/styles/mixins.scss b/src/app/styles/mixins.scss index 475fee90..be64c27c 100644 --- a/src/app/styles/mixins.scss +++ b/src/app/styles/mixins.scss @@ -1,3 +1,63 @@ +$transition-duration: 0.2s; +$transform-scale: 0.9; +$transform-scale-active: 0.9; +$filter-opacity: 0.5; +$filter-brightness: 1.3; +$basic-font: var(--regular-font); +$one: var(--one); +$two: var(--two); +$active-color: var(--steam-green-800); + +@mixin link($padding: 0 calc(var(--extra-small-offset) / 2), $color: var(--noble-gray-800)) { + position: relative; + display: flex; + align-items: center; + padding: $padding; + height: 100%; + font: $basic-font; + letter-spacing: $one; + text-transform: uppercase; + color: $color; + transition: color $transition-duration; + + &::after { + content: ''; + position: absolute; + left: 0; + bottom: calc($one * -1); + width: 100%; + height: $two; + background-color: currentcolor; + opacity: 0; + transform: scaleX(0); + transform-origin: center; + transition: + transform $transition-duration, + opacity $transition-duration; + } + + @media (hover: hover) { + &:hover { + color: $active-color; + + &::after { + opacity: 1; + transform: scaleX(1); + } + } + } +} + +@mixin active { + color: $active-color; + opacity: 1; + + &::after { + opacity: 1; + transform: scaleX(1); + } +} + @mixin green-btn { display: flex; align-items: center; @@ -6,28 +66,121 @@ 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); + font: $basic-font; + letter-spacing: $one; + color: var(--button-white); + background-color: $active-color; transition: - color 0.2s, - background-color 0.2s, - transform 0.2s; + filter $transition-duration, + color $transition-duration, + background-color $transition-duration, + transform $transition-duration; + + svg { + fill: var(--noble-gray-900); + stroke: var(--noble-gray-900); + transition: + fill $transition-duration, + stroke $transition-duration; + } @media (hover: hover) { &:hover { - background-color: var(--steam-green-700); + filter: brightness($filter-brightness); + + svg { + fill: var(--noble-gray-1000); + stroke: var(--noble-gray-1000); + } } } /* stylelint-disable-next-line order/order */ &:active { - transform: scale(0.9); + transform: scale($transform-scale-active); } &:disabled { - background-color: var(--noble-gray-300); + filter: opacity($filter-opacity); pointer-events: none; } } + +@mixin round-btn( + $btn-padding: calc(var(--tiny-offset) * 1.5), + $btn-bg: var(--noble-white-100), + $btn-hover-bg: var(--white-tr), + $btn-fill: $active-color, + $btn-stroke: $active-color +) { + display: flex; + align-items: center; + justify-content: center; + justify-self: center; + border-radius: 50%; + padding: $btn-padding; + width: fit-content; + background-color: $btn-bg; + transition: + filter $transition-duration, + color $transition-duration, + background-color $transition-duration, + transform $transition-duration, + opacity $transition-duration; + + @media (hover: hover) { + &:hover { + background-color: $btn-hover-bg; + + svg { + fill: $btn-fill; + stroke: $btn-stroke; + } + } + /* stylelint-disable-next-line order/order */ + &:active { + transform: scale($transform-scale-active); + } + + &:disabled { + filter: opacity($filter-opacity); + pointer-events: none; + } + } +} + +@mixin svg-logo( + $logo-width: var(--small-offset), + $logo-height: var(--small-offset), + $logo-fill: var(--noble-gray-900), + $logo-stroke: var(--noble-gray-900) +) { + display: flex; + align-items: center; + justify-content: center; + justify-self: center; + + svg { + width: $logo-width; + height: $logo-height; + fill: $logo-fill; + stroke: $logo-stroke; + transition: + fill $transition-duration, + stroke $transition-duration; + } +} + +@mixin gradient-outline-linear($border-width, $color-center, $color-outer, $direction: to right) { + border: $border-width solid transparent; + border-image: linear-gradient($direction, $color-center 50%, $color-outer 100%) 1; + border-image-slice: 1; +} + +@mixin gradient-border-bottom-linear($border-width, $color-center, $color-outer, $direction: to right) { + border-bottom: $border-width solid transparent; + background-image: linear-gradient($direction, $color-center, $color-outer); + background-position: 0 calc(100% + #{$border-width}); + background-size: 100% $border-width; + background-repeat: no-repeat; +} diff --git a/src/app/styles/noUiSlider.scss b/src/app/styles/noUiSlider.scss index ad64af9d..c1ec9a2a 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, @@ -85,12 +96,16 @@ .noUi-target { position: relative; border-radius: 0.25rem; - background: var(--steam-green-400); + background: var(--steam-green-1100); cursor: pointer; } .noUi-horizontal { height: 0.5rem; + + @media (max-width: 768px) { + height: 0.4rem; + } } .noUi-state-tap .noUi-connect, diff --git a/src/app/styles/variables.scss b/src/app/styles/variables.scss index 80f0fe34..fc6de1ac 100644 --- a/src/app/styles/variables.scss +++ b/src/app/styles/variables.scss @@ -1,6 +1,6 @@ :root { // body wrapper - --wrapper-width: calc(var(--large-offset) * 18); + --wrapper-width: calc(var(--large-offset) * 18); // 1440px // small vars --one: 0.0625rem; // 1px @@ -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, @@ -43,57 +43,90 @@ --black-font: 900 2.1875rem 'Cerapro', sans-serif; // 35px --extra-black-font: 900 2.5rem 'Cerapro', sans-serif; // 40px - // 1440px body.light { // colors - --white: #fff; - --white-tr: #ffffffa9; - --black: #000; - --noble-white-100: #f5f5f5; - --noble-white-200: #f0f0f0; - --noble-gray-200: #eaeaea; - --noble-gray-300: #cbcbcb; - --noble-gray-400: #c4c4c4; - --noble-gray-500: #acacac; - --noble-gray-600: #a5a5a5; - --noble-gray-700: #727272; - --noble-gray-800: #3d3d3d; - --noble-gray-tr-800: #acacacbd; - --noble-gray-900: #1d1d1d; - --noble-gray-1000: #1a1a1ab5; - --red-power-600: #d0302f; - --steam-green-300: #46a35880; - --steam-green-350: #46a35937; - --steam-green-400: #c8f4b4; - --steam-green-500: #b6f09c; - --steam-green-700: #70d27a; - --steam-green-800: #46a358; - --steam-green-900: #46a3581a; - --steam-green-gr-800: #8c998f2b; + // basic colors: + --white: #fff; // table background, buttons text, blog post footer, blog text background, user menu, form background + --button-white: #fff; // buttons text + + // backgrounds: + --noble-white-100: #f5f5f5; // page background, edit forms background + --noble-white-200: #f0f0f0; // catalog filters background and product cards + --white-tr: #ffffffa9; // footer background, modal background, pagination background, card buttons background + --noble-gray-200: #eaeaea; // loader / input basic + --noble-gray-1000: #1a1a1ab5; // header background + --steam-green-900: #46a3581a; // blog background, personal info background + --steam-green-1000: #46a35937; // footer links background, pics round thingies + + // tranparent colors: + --noble-gray-tr-800: #acacacbd; // not active labels, tumblers + --noble-gray-tr-900: #c4c4c4a8; // not active labels, tumblers + --steam-green-300: #46a35880; // header outline, social media buttons outline + + // outline: + --noble-gray-300: #cbcbcb; // item cards and pagination outline + + // text colors: + --black: #000; // blog titles, footer titles + --red-power-600: #d0302f; // error messages, form errors + --noble-gray-400: #c4c4c4; // item cards description + --noble-gray-500: #acacac; // form titles + --noble-gray-600: #a5a5a5; // placeholders + --noble-gray-700: #727272; // blog/footer text + --noble-gray-800: #3d3d3d; // links, labels, base text, disabled svg, filters, descriptions + --noble-gray-900: #1d1d1d; // user profile text, buttons + --noble-gray-1100: #c4c4c4; // navigation links + + // highlight colors: + --steam-green-400: #4b8f3a; // price inputs, filter highlight, pagination active, product info titles + --steam-green-500: #5a9f50; // new price, product name, filter titles + --steam-green-700: #70d27a; // price slider, more, added to wishlist + --steam-green-800: #46a358; // main green: buttons, logo, active links, burger, scroll, blog posts details, cart price, active svg + --steam-green-1100: #a7e599; // theme tumbler round thingy } body.dark { // colors - --black: #fff; - --white: #1a1a1a; - --white-tr: #1a1a1ab0; - --noble-white-100: #2b2b2b; - --noble-white-200: #4a4a4a; - --noble-gray-200: #f6f6f6; - --noble-gray-300: #dcdbdb; - --noble-gray-400: #ddd; - --noble-gray-500: #cfcfcf; - --noble-gray-600: #cdcdcd; - --noble-gray-700: #b0b0b0; - --noble-gray-800: #c4c4c4; - --noble-gray-1000: #1a1a1ab5; - --red-power-600: #d0302f; - --steam-green-300: #46a35880; - --steam-green-350: #46a35937; - --steam-green-400: #c8f4b4; - --steam-green-500: #b6f09c; - --steam-green-700: #70d27a; - --steam-green-800: #46a358; - --steam-green-gr-800: #c5e1cb2b; + // colors that mustn't change + --button-white: #fff; // buttons text + --red-power-600: #d0302f; // error messages, form errors + --steam-green-300: #46a35880; // header outline, social media buttons outline + --steam-green-800: #46a358; // main green: buttons, logo, active links, burger, scroll, blog posts details, cart price, active svg + --steam-green-900: #46a3581a; // blog background, personal info background + --steam-green-1000: #46a35937; // footer links background, pics round thingies + --steam-green-1100: #a7e599; // theme tumbler round thingy + --noble-gray-1000: #1a1a1ab5; // header background + --noble-gray-tr-900: #c4c4c4a8; // not active labels, tumblers + --noble-gray-1100: #ccc; // navigation links + + // basic colors: + + --white: #000; // table background, buttons text, blog post footer, blog text background, user menu, form background + + // backgrounds: + --noble-white-100: #2b2b2b; // page background, edit forms background + --noble-white-200: #4a4a4a; // catalog filters background and product cards + --white-tr: #1a1a1ab5; // footer background, modal background, pagination background, card buttons background + --noble-gray-200: #f6f6f6; // loader / input basic + + // tranparent colors: + --noble-gray-tr-800: #acacacbd; // not active labels, tumblers + + // outline: + --noble-gray-300: #dcdbdb; // item cards and pagination outline + + // text colors: + --black: #fff; // blog titles, footer titles + --noble-gray-400: #ddd; // item cards description + --noble-gray-500: #cfcfcf; // form titles + --noble-gray-600: #cdcdcd; // placeholders + --noble-gray-700: #b0b0b0; // blog/footer text + --noble-gray-800: #c4c4c4; // links, labels, base text, disabled svg, filters, descriptions + --noble-gray-900: #f5f5f5; // user profile text, buttons + + // highlight colors: + --steam-green-400: #c8f4b4; // price inputs, filter highlight, pagination active, product info titles + --steam-green-500: #b6f09c; // new price, product name, filter titles + --steam-green-700: #70d27a; // price slider, more, added to wishlist } } 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/Navigation/view/navigationView.module.scss b/src/entities/Navigation/view/navigationView.module.scss index 908a3d45..e16ac383 100644 --- a/src/entities/Navigation/view/navigationView.module.scss +++ b/src/entities/Navigation/view/navigationView.module.scss @@ -1,3 +1,5 @@ +@import 'src/app/styles/mixins'; + .navigation { display: flex; align-items: center; @@ -6,54 +8,21 @@ order: 2; margin: 0 auto; height: calc(var(--extra-small-offset) * 3.5); // 70px -} - -.link { - position: relative; - display: flex; - align-items: center; - padding: 0 calc(var(--extra-small-offset) / 2); - height: 100%; - font: var(--regular-font); - letter-spacing: 1px; - text-transform: uppercase; - color: var(--noble-gray-800); - transition: color 0.2s; - &::after { - content: ''; - position: absolute; - left: 0; - bottom: calc(var(--one) * -1); - width: 100%; - height: var(--two); - background-color: currentcolor; - opacity: 0; - transform: scaleX(0); - transform-origin: center; - transition: - transform 0.2s, - opacity 0.2s; + @media (max-width: 768px) { + align-self: auto; + justify-content: space-between; + margin: 0; } +} - @media (hover: hover) { - &:hover { - color: var(--steam-green-800); +$color: var(--noble-gray-1100); +$padding: 0 calc(var(--extra-small-offset) / 2); - &::after { - opacity: 1; - transform: scaleX(1); - } - } - } +.link { + @include link($padding, $color); } .active { - color: var(--steam-green-800); - opacity: 1; - - &::after { - opacity: 1; - transform: scaleX(1); - } + @include active; } diff --git a/src/entities/ProductCard/view/ProductCardView.ts b/src/entities/ProductCard/view/ProductCardView.ts index 598b13f7..b600eca8 100644 --- a/src/entities/ProductCard/view/ProductCardView.ts +++ b/src/entities/ProductCard/view/ProductCardView.ts @@ -8,6 +8,7 @@ import observeStore, { selectCurrentLanguage } from '@/shared/Store/observer.ts' import { MORE_TEXT } from '@/shared/constants/buttons.ts'; import { LANGUAGE_CHOICE } from '@/shared/constants/common.ts'; import { PAGE_ID } from '@/shared/constants/pages.ts'; +import { PRODUCT_INFO_TEXT } from '@/shared/constants/product.ts'; import { LOADER_SIZE } from '@/shared/constants/sizes.ts'; import SVG_DETAILS from '@/shared/constants/svg.ts'; import { buildPathName } from '@/shared/utils/buildPathname.ts'; @@ -25,6 +26,8 @@ class ProductCardView { private currentSize: null | string; + private discountLabel: HTMLSpanElement; + private goDetailsPageLink: LinkModel; private moreButton: ButtonModel; @@ -51,6 +54,7 @@ class ProductCardView { this.goDetailsPageLink = this.createGoDetailsPageLink(); this.buttonsWrapper = this.createButtonsWrapper(); this.productImage = this.createProductImage(); + this.discountLabel = this.createDiscountLabel(); this.productImageWrapper = this.createProductImageWrapper(); this.productName = this.createProductName(); this.productShortDescription = this.createProductShortDescription(); @@ -113,6 +117,30 @@ class ProductCardView { return this.buttonsWrapper; } + private createDiscountLabel(): HTMLSpanElement { + const currentVariant = this.params.variant.find(({ size }) => size === this.currentSize) ?? this.params.variant[0]; + const innerContent = `${Math.round((1 - currentVariant.discount / currentVariant.price) * 100)}%`; + this.discountLabel = createBaseElement({ + cssClasses: [styles.discountLabel], + innerContent, + tag: 'span', + }); + + const discountSpan = createBaseElement({ + cssClasses: [styles.discountSpan], + innerContent: PRODUCT_INFO_TEXT[getStore().getState().currentLanguage].DISCOUNT_LABEL, + tag: 'span', + }); + + observeStore(selectCurrentLanguage, () => { + discountSpan.textContent = PRODUCT_INFO_TEXT[getStore().getState().currentLanguage].DISCOUNT_LABEL; + }); + + this.discountLabel.append(discountSpan); + + return this.discountLabel; + } + private createGoDetailsPageLink(): LinkModel { const href = `${buildPathName(PAGE_ID.PRODUCT_PAGE, this.params.key, { size: [this.currentSize ?? this.params.variant[0].size], @@ -181,6 +209,10 @@ class ProductCardView { this.productImage.classList.remove(styles.hidden); loader.remove(); }); + + if (this.params.variant.some(({ discount }) => discount)) { + this.productImageWrapper.append(this.discountLabel); + } return this.productImageWrapper; } diff --git a/src/entities/ProductCard/view/productCardView.module.scss b/src/entities/ProductCard/view/productCardView.module.scss index 27dd52ac..08118b0f 100644 --- a/src/entities/ProductCard/view/productCardView.module.scss +++ b/src/entities/ProductCard/view/productCardView.module.scss @@ -4,7 +4,7 @@ flex-direction: column; align-items: center; justify-self: center; - outline: var(--two) solid var(--noble-gray-300); + outline: calc(var(--one) * 1.5) solid var(--noble-gray-300); border-radius: var(--medium-br); min-height: calc(var(--extra-small-offset) * 17.5); // 350px max-width: calc(var(--extra-large-offset) * 2.5); // 250px @@ -16,7 +16,7 @@ @media (hover: hover) { &:hover { - outline: var(--two) solid var(--steam-green-800); + outline: calc(var(--one) * 1.5) solid var(--steam-green-800); transform: scale(0.98); } } @@ -71,15 +71,13 @@ .switchToWishListButton, .goDetailsPageLink { position: relative; - outline: var(--two) solid var(--noble-gray-700); + outline: calc(var(--one) * 1.5) solid var(--noble-gray-700); border-radius: var(--medium-br); padding: var(--tiny-offset); width: var(--small-offset); height: var(--small-offset); - background-color: var(--noble-gray-1000); - transition: - transform 0.2s, - outline 0.2s; + background-color: var(--white-tr); + transition: outline 0.2s; backdrop-filter: blur(10px); svg { @@ -98,7 +96,7 @@ .goDetailsPageLink { @media (hover: hover) { &:hover { - outline: var(--two) solid var(--steam-green-400); + outline: calc(var(--one) * 1.5) solid var(--steam-green-400); svg { fill: var(--steam-green-400); @@ -115,7 +113,7 @@ } .inWishList { - outline: var(--two) solid var(--red-power-600); + outline: calc(var(--one) * 1.5) solid var(--red-power-600); svg { fill: var(--red-power-600); @@ -125,7 +123,7 @@ .switchToWishListButton { @media (hover: hover) { &:hover { - outline: var(--two) solid var(--red-power-600); + outline: calc(var(--one) * 1.5) solid var(--red-power-600); svg { fill: var(--red-power-600); @@ -136,7 +134,7 @@ &.inWishList { @media (hover: hover) { &:hover { - outline: var(--two) solid var(--noble-gray-700); + outline: calc(var(--one) * 1.5) solid var(--noble-gray-700); svg { fill: var(--noble-gray-700); @@ -151,6 +149,7 @@ flex-grow: 1; flex-direction: column; align-items: center; + border-radius: var(--medium-br); padding: calc(var(--extra-small-offset) / 2) calc(var(--extra-small-offset) / 4); width: 100%; background-color: var(--noble-white-200); @@ -175,7 +174,7 @@ letter-spacing: var(--one); text-align: left; text-overflow: ellipsis; - color: var(--noble-gray-400); + color: var(--noble-gray-700); -webkit-box-orient: vertical; -webkit-line-clamp: 2; @@ -192,6 +191,7 @@ order: 4; margin-top: calc(var(--extra-small-offset) * (-0.2)); margin-right: var(--five); + padding: var(--five); font: var(--regular-font); letter-spacing: var(--one); color: var(--steam-green-700); @@ -207,3 +207,20 @@ .hidden { display: none; } + +.discountLabel { + position: absolute; + left: 0; + top: calc(var(--two) * 5); + z-index: 1; + display: flex; + border-radius: 0 var(--five) var(--five) 0; + padding: calc(var(--tiny-offset) / 2); + font: var(--regular-font); + letter-spacing: var(--one); + text-align: center; + text-transform: uppercase; + color: var(--steam-green-1100); + background-color: var(--steam-green-800); + gap: var(--two); +} 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..a069842c 100644 --- a/src/entities/ProductModalSlider/view/ProductModalSliderView.ts +++ b/src/entities/ProductModalSlider/view/ProductModalSliderView.ts @@ -4,11 +4,12 @@ import ButtonModel from '@/shared/Button/model/ButtonModel.ts'; import LoaderModel from '@/shared/Loader/model/LoaderModel.ts'; import modal from '@/shared/Modal/model/ModalModel.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 styles from './productModalSliderView.module.scss'; -const BIG_SLIDER_WIDTH = 40; const CLOSE_BUTTON_CONTENT = 'x'; class ProductModalSliderView { @@ -65,9 +66,6 @@ class ProductModalSliderView { tag: 'div', }); - const maxWidth = BIG_SLIDER_WIDTH; - slider.style.maxWidth = `${maxWidth}rem`; - slider.append(this.createModalSliderWrapper()); return slider; } @@ -123,6 +121,10 @@ class ProductModalSliderView { this.nextSlideButton = new ButtonModel({ classes: [styles.nextSlideButton], }); + + const svg = document.createElementNS(SVG_DETAILS.SVG_URL, 'svg'); + svg.append(createSVGUse(SVG_DETAILS.ARROW_UP)); + this.nextSlideButton.getHTML().append(svg); return this.nextSlideButton; } @@ -130,6 +132,9 @@ class ProductModalSliderView { this.prevSlideButton = new ButtonModel({ classes: [styles.prevSlideButton], }); + const svg = document.createElementNS(SVG_DETAILS.SVG_URL, 'svg'); + svg.append(createSVGUse(SVG_DETAILS.ARROW_UP)); + this.prevSlideButton.getHTML().append(svg); return this.prevSlideButton; } diff --git a/src/entities/ProductModalSlider/view/productModalSliderView.module.scss b/src/entities/ProductModalSlider/view/productModalSliderView.module.scss index 5dcfc78e..1888039c 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 { @@ -58,8 +78,9 @@ .nextSlideButton, .prevSlideButton { - position: relative; - display: block; + display: flex; + align-items: center; + justify-content: center; border: var(--one) solid var(--noble-gray-200); border-radius: 50%; width: 3rem; @@ -72,10 +93,21 @@ border-color 0.2s, transform 0.2s; + svg { + width: 2.5rem; + height: 2.5rem; + stroke: var(--steam-green-800); + transition: stroke 0.2s; + } + @media (hover: hover) { &:hover { border-color: var(--steam-green-400); color: var(--steam-green-700); + + svg { + stroke: var(--steam-green-1100); + } } } @@ -86,22 +118,14 @@ } .nextSlideButton { - &::after { - content: '↑'; - position: absolute; - left: 50%; - top: 50%; - transform: translate(-50%, -50%) rotate(90deg); + svg { + transform: rotate(90deg); } } .prevSlideButton { - &::after { - content: '↑'; - position: absolute; - left: 50%; - top: 50%; - transform: translate(-50%, -50%) rotate(-90deg); + svg { + transform: rotate(-90deg); } } 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/entities/UserAddress/model/UserAddressModel.ts b/src/entities/UserAddress/model/UserAddressModel.ts index 4af86fb4..77e2ad1a 100644 --- a/src/entities/UserAddress/model/UserAddressModel.ts +++ b/src/entities/UserAddress/model/UserAddressModel.ts @@ -1,11 +1,17 @@ import type { Address, User } from '@/shared/types/user.ts'; -// import AddressEditModel from '@/features/AddressEdit/model/AddressEditModel.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 { USER_ADDRESS_TYPE, type UserAddressType } from '@/shared/constants/forms.ts'; +import getStore from '@/shared/Store/Store.ts'; +import { setBillingCountry } from '@/shared/Store/actions.ts'; +import { USER_MESSAGE } from '@/shared/constants/confirmUserMessage.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,49 +19,107 @@ 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, type: UserAddressType, defaultAddressId: string) { - this.view = new UserAddressView(user, address, type, defaultAddressId); - this.setDeleteButtonHandler(address, type); - this.setEditButtonHandler(address, type); + constructor( + address: Address, + activeTypes: AddressTypeType[], + callback: (isDisabled: boolean) => void, + inactiveTypes?: AddressTypeType[], + ) { + this.callback = callback; + this.currentAddress = address; + this.view = new UserAddressView(address, activeTypes, inactiveTypes); + this.labels = this.view.getLabels(); + this.setEditButtonHandler(address); + this.setDeleteButtonHandler(address); + this.setLabelClickHandler(); } - private setDeleteButtonHandler(address: Address, type: UserAddressType): void { - this.view - .getDeleteButton() - .getHTML() - .addEventListener('click', async () => { - const loader = new LoaderModel(LOADER_SIZE.SMALL).getHTML(); - this.view.getDeleteButton().getHTML().append(loader); + private async deleteAddress(address: Address): Promise { + try { + const user = await getCustomerModel().getCurrentUser(); + if (user) { try { - 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); - } - serverMessageModel.showServerMessage(SERVER_MESSAGE_KEYS.ADDRESS_DELETED, MESSAGE_STATUS.SUCCESS); - this.view.getHTML().remove(); - } catch (error) { - showErrorMessage(); - } - } + 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(); - } finally { - loader.remove(); + showErrorMessage(error); } + } + } catch (error) { + showErrorMessage(error); + } + } + + private async handleAddressType(user: User, activeType: AddressTypeType, inactive: boolean): Promise { + const customerModel = getCustomerModel(); + + 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.MEDIUM); + loader.setAbsolutePosition(); + this.callback(true); + this.view.toggleState(true); + this.getHTML().append(loader.getHTML()); + 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); + } finally { + this.view.toggleState(false); + loader.getHTML().remove(); + } + } + + private setDeleteButtonHandler(address: Address): void { + this.view + .getDeleteButton() + .getHTML() + .addEventListener('click', () => { + const confirmModel = new ConfirmModel( + () => this.deleteAddress(address), + USER_MESSAGE[getStore().getState().currentLanguage].DELETE_ADDRESS, + ); + modal.setContent(confirmModel.getHTML()); + modal.show(); }); } - // 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() @@ -65,16 +129,25 @@ class UserAddressModel { if (!user) { return; } + + getStore().dispatch(setBillingCountry(address.country)); + const newAddressEditForm = new AddressEditModel(address, user).getHTML(); modal.show(); - // modal.setContent(new AddressEditModel( address, _type).getHTML()); + modal.setContent(newAddressEditForm); } catch (error) { - showErrorMessage(); - } finally { - modal.hide(); + showErrorMessage(error); } }); } + 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..ff17f573 100644 --- a/src/entities/UserAddress/view/UserAddressView.ts +++ b/src/entities/UserAddress/view/UserAddressView.ts @@ -1,58 +1,103 @@ -import type { Address, User } from '@/shared/types/user'; +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_TEXT, 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 styles from './userAddressView.module.scss'; class UserAddressView { - private deleteButton: ButtonModel; + private citySpan: HTMLSpanElement; + + private countrySpan: HTMLSpanElement; + + private currentAddress: Address; - private deleteLogo: HTMLDivElement; + private deleteButton: ButtonModel; private editButton: ButtonModel; - private editLogo: HTMLDivElement; + private labels: Map = new Map(); + + private labelsWrapper: HTMLDivElement; + + private postalCodeSpan: HTMLSpanElement; + + private streetNameSpan: HTMLSpanElement; private view: HTMLLIElement; - constructor(user: User, address: Address, type: UserAddressType, defaultAddressId: string) { - this.deleteLogo = this.createDeleteLogo(); - this.editLogo = this.createEditLogo(); + constructor(address: Address, types: AddressTypeType[], inactiveTypes?: AddressTypeType[]) { + this.currentAddress = address; this.deleteButton = this.createDeleteButton(); this.editButton = this.createEditButton(); - this.view = this.createHTML(user, address, type, defaultAddressId); + this.citySpan = this.createCitySpan(); + this.countrySpan = this.createCountrySpan(); + this.postalCodeSpan = this.createPostalCodeSpan(); + this.streetNameSpan = this.createStreetNameSpan(); + this.labelsWrapper = this.createLabelsWrapper(); + this.view = this.createHTML(types, inactiveTypes); + } + + private createCitySpan(): HTMLSpanElement { + this.citySpan = createBaseElement({ + cssClasses: [styles.citySpan], + innerContent: ADDRESS_TEXT[getStore().getState().currentLanguage].CITY, + tag: 'span', + }); + + observeStore(selectCurrentLanguage, () => { + const text = ADDRESS_TEXT[getStore().getState().currentLanguage].CITY; + const textNode = [...this.citySpan.childNodes].find((child) => child.nodeType === Node.TEXT_NODE); + if (textNode) { + textNode.textContent = text; + } + }); + + const accentSpan = createBaseElement({ + cssClasses: [styles.accentSpan], + innerContent: this.currentAddress.city, + tag: 'span', + }); + + this.citySpan.append(accentSpan); + return this.citySpan; } - private createAddress(user: User, address: Address, type: UserAddressType, defaultAddressId: string): string { - const { locale } = user; + private createCountrySpan(): HTMLSpanElement { + this.countrySpan = createBaseElement({ + cssClasses: [styles.countrySpan], + innerContent: ADDRESS_TEXT[getStore().getState().currentLanguage].COUNTRY, + tag: 'span', + }); + + const accentSpan = createBaseElement({ + cssClasses: [styles.accentSpan], + innerContent: + findKeyByValue(COUNTRIES_LIST[getStore().getState().currentLanguage], this.currentAddress.country) ?? '', + tag: 'span', + }); - const country = findKeyByValue(COUNTRIES_LIST[locale], address.country); - const standartAddressText = addressTemplate(address.streetName, address.city, country, address.postalCode); - let addressText = addressMessage(type, standartAddressText); + this.countrySpan.append(accentSpan); + observeStore(selectCurrentLanguage, () => { + accentSpan.innerText = + findKeyByValue(COUNTRIES_LIST[getStore().getState().currentLanguage], this.currentAddress.country) ?? ''; - if (defaultAddressId === address.id) { - if (type === USER_ADDRESS_TYPE.BILLING) { - addressText = defaultBillingAddress(standartAddressText); - } else if (type === USER_ADDRESS_TYPE.SHIPPING) { - addressText = defaultShippingAddress(standartAddressText); + const text = ADDRESS_TEXT[getStore().getState().currentLanguage].COUNTRY; + const textNode = [...this.countrySpan.childNodes].find((child) => child.nodeType === Node.TEXT_NODE); + if (textNode) { + textNode.textContent = text; } - } - return addressText; + }); + return this.countrySpan; } private createDeleteButton(): ButtonModel { @@ -61,7 +106,9 @@ class UserAddressView { title: TOOLTIP_TEXT[getStore().getState().currentLanguage].DELETE_ADDRESS, }); - this.deleteButton.getHTML().append(this.deleteLogo); + const svg = document.createElementNS(SVG_DETAILS.SVG_URL, 'svg'); + svg.append(createSVGUse(SVG_DETAILS.DELETE)); + this.deleteButton.getHTML().append(svg); observeStore(selectCurrentLanguage, () => { this.deleteButton.getHTML().title = TOOLTIP_TEXT[getStore().getState().currentLanguage].DELETE_ADDRESS; @@ -70,21 +117,15 @@ class UserAddressView { return this.deleteButton; } - 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)); - this.deleteLogo.append(svg); - return this.deleteLogo; - } - private createEditButton(): ButtonModel { this.editButton = new ButtonModel({ classes: [styles.editButton], title: TOOLTIP_TEXT[getStore().getState().currentLanguage].EDIT_ADDRESS, }); - this.editButton.getHTML().append(this.editLogo); + const svg = document.createElementNS(SVG_DETAILS.SVG_URL, 'svg'); + svg.append(createSVGUse(SVG_DETAILS.EDIT)); + this.editButton.getHTML().append(svg); observeStore(selectCurrentLanguage, () => { this.editButton.getHTML().title = TOOLTIP_TEXT[getStore().getState().currentLanguage].EDIT_ADDRESS; @@ -93,29 +134,162 @@ class UserAddressView { return this.editButton; } - private createEditLogo(): HTMLDivElement { - this.editLogo = createBaseElement({ cssClasses: [styles.editLogo], tag: 'div' }); - const svg = document.createElementNS(SVG_DETAILS.SVG_URL, 'svg'); - svg.append(createSVGUse(SVG_DETAILS.EDIT)); - this.editLogo.append(svg); - return this.editLogo; - } - - private createHTML(user: User, address: Address, type: UserAddressType, defaultAddressId: string): HTMLLIElement { + private createHTML(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), + + activeTypes.forEach((type) => { + this.setActiveAddressLabel(type); + }); + + if (inactiveTypes) { + inactiveTypes.forEach((type) => { + this.setActiveAddressLabel(type, true); + }); + } + + const addressTextWrapper = createBaseElement({ + cssClasses: [styles.addressTextWrapper], + tag: 'div', + }); + addressTextWrapper.append(this.countrySpan, this.citySpan, this.streetNameSpan, this.postalCodeSpan); + + const buttonsWrapper = createBaseElement({ + cssClasses: [styles.buttonsWrapper], tag: 'div', }); + buttonsWrapper.append(this.editButton.getHTML(), this.deleteButton.getHTML()); + + this.view.append(this.labelsWrapper, addressTextWrapper, buttonsWrapper); - 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 createLabelsWrapper(): HTMLDivElement { + this.labelsWrapper = createBaseElement({ + cssClasses: [styles.labelsWrapper], + tag: 'div', + }); + return this.labelsWrapper; + } + + private createPostalCodeSpan(): HTMLSpanElement { + this.postalCodeSpan = createBaseElement({ + cssClasses: [styles.postalCodeSpan], + innerContent: ADDRESS_TEXT[getStore().getState().currentLanguage].POSTAL_CODE, + tag: 'span', + }); + + observeStore(selectCurrentLanguage, () => { + const text = ADDRESS_TEXT[getStore().getState().currentLanguage].POSTAL_CODE; + const textNode = [...this.postalCodeSpan.childNodes].find((child) => child.nodeType === Node.TEXT_NODE); + if (textNode) { + textNode.textContent = text; + } + }); + + const accentSpan = createBaseElement({ + cssClasses: [styles.accentSpan], + innerContent: this.currentAddress.postalCode, + tag: 'span', + }); + + this.postalCodeSpan.append(accentSpan); + return this.postalCodeSpan; + } + + private createStreetNameSpan(): HTMLSpanElement { + this.streetNameSpan = createBaseElement({ + cssClasses: [styles.streetNameSpan], + innerContent: ADDRESS_TEXT[getStore().getState().currentLanguage].STREET, + tag: 'span', + }); + + observeStore(selectCurrentLanguage, () => { + const text = ADDRESS_TEXT[getStore().getState().currentLanguage].STREET; + const textNode = [...this.streetNameSpan.childNodes].find((child) => child.nodeType === Node.TEXT_NODE); + if (textNode) { + textNode.textContent = text; + } + }); + + const accentSpan = createBaseElement({ + cssClasses: [styles.accentSpan], + innerContent: this.currentAddress.streetName, + tag: 'span', + }); + + this.streetNameSpan.append(accentSpan); + return this.streetNameSpan; + } + + 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.SWITCH_BILLING_ADDRESS); + this.labelsWrapper.append(addressType); + break; + + case ADDRESS_TYPE.SHIPPING: + addressType = this.createLabel(ActiveType, [styles.shipping], TOOLTIP_TEXT_KEYS.SWITCH_SHIPPING_ADDRESS); + this.labelsWrapper.append(addressType); + break; + + case ADDRESS_TYPE.DEFAULT_BILLING: + addressType = this.createLabel( + ActiveType, + [styles.defaultBilling], + TOOLTIP_TEXT_KEYS.SWITCH_DEFAULT_BILLING_ADDRESS, + ); + this.labelsWrapper.append(addressType); + break; + + case ADDRESS_TYPE.DEFAULT_SHIPPING: + addressType = this.createLabel( + ActiveType, + [styles.defaultShipping], + TOOLTIP_TEXT_KEYS.SWITCH_DEFAULT_SHIPPING_ADDRESS, + ); + this.labelsWrapper.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 +301,22 @@ class UserAddressView { public getHTML(): HTMLLIElement { return this.view; } + + public getLabels(): Map { + return this.labels; + } + + public setDisabled(): void { + this.view.classList.add(styles.disabled); + } + + public setEnabled(): void { + this.view.classList.remove(styles.disabled); + } + + public toggleState(isDisabled: boolean): void { + this.view.classList.toggle(styles.disabled, isDisabled); + } } export default UserAddressView; diff --git a/src/entities/UserAddress/view/userAddressView.module.scss b/src/entities/UserAddress/view/userAddressView.module.scss index 18ce13ff..e2dc0918 100644 --- a/src/entities/UserAddress/view/userAddressView.module.scss +++ b/src/entities/UserAddress/view/userAddressView.module.scss @@ -1,55 +1,134 @@ -.info { - grid-column: 2 span; - max-width: 100%; - word-break: break-all; +@import 'src/app/styles/mixins'; + +.addressItem { + position: relative; + display: grid; + align-items: center; + border-radius: var(--medium-br); + padding: var(--extra-small-offset); + background: var(--white-tr); + gap: var(--tiny-offset); } -.deleteLogo, -.editLogo { +.labelsWrapper { 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; - } + flex-wrap: wrap; + grid-row: 1; + gap: var(--tiny-offset); } -.deleteButton, -.editButton { +.addressType { display: flex; - margin: var(--tiny-offset); + align-items: center; + justify-content: center; 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; + min-width: 8rem; + font: var(--small-font); + text-align: center; + color: var(--black); + background-color: var(--steam-green-900); + transition: scale 0.2s; - &:focus { - background-color: var(--steam-green-700); + &:hover { + cursor: pointer; + scale: 1.05; } - @media (hover: hover) { - &:hover { - background-color: var(--steam-green-700); + &:active { + scale: 0.95; + } +} + +.billing { + order: 1; + background-color: var(--steam-green-1000); +} + +.shipping { + order: 2; + background-color: var(--steam-green-1000); +} + +.defaultBilling { + order: 3; + background-color: var(--steam-green-300); +} + +.defaultShipping { + order: 4; + background-color: var(--steam-green-300); +} + +.inactive { + filter: grayscale(1); + transition: + filter 0.2s, + scale 0.2s; +} - svg { - fill: var(--noble-gray-1000); - } +.editButton, +.deleteButton { + @include green-btn; + + padding: 0; + width: calc(var(--extra-small-offset) * 1.5); + height: calc(var(--extra-small-offset) * 1.5); + background-color: transparent; + + svg { + width: calc(var(--extra-small-offset) * 1.5); + height: calc(var(--extra-small-offset) * 1.5); + + &:hover { + fill: var(--steam-green-800); + stroke: var(--steam-green-800); } } - &:disabled { - background-color: var(--noble-gray-300); - pointer-events: none; + &:hover { + filter: brightness(1); + } +} + +.buttonsWrapper { + display: flex; + grid-row: 1; + margin-left: auto; + gap: var(--tiny-offset); + + @media (max-width: 768px) { + flex-direction: column; + grid-row: 2; + } +} + +.addressTextWrapper { + display: flex; + flex-direction: column; + grid-row: 2; + gap: var(--tiny-offset); +} + +.citySpan, +.streetNameSpan, +.postalCodeSpan, +.countrySpan { + font: var(--regular-font); + letter-spacing: var(--one); + word-break: break-all; + color: var(--steam-green-400); +} + +.disabled { + pointer-events: none; + + &::after { + content: ''; + position: absolute; + z-index: 1; + border-radius: var(--medium-br); + backdrop-filter: blur(5px); + inset: 0; } } diff --git a/src/features/AddressAdd/model/AddressAddModel.ts b/src/features/AddressAdd/model/AddressAddModel.ts new file mode 100644 index 00000000..56a73890 --- /dev/null +++ b/src/features/AddressAdd/model/AddressAddModel.ts @@ -0,0 +1,191 @@ +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(options, this.addressType); + 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(); + 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.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..7ffc48e5 --- /dev/null +++ b/src/features/AddressAdd/view/AddressAddView.ts @@ -0,0 +1,63 @@ +import ButtonModel from '@/shared/Button/model/ButtonModel.ts'; +import getStore from '@/shared/Store/Store.ts'; +import { BUTTON_TEXT, BUTTON_TYPE } 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.saveChangesButton.getHTML(), this.cancelButton.getHTML()); + return this.view; + } + + private createSaveChangesButton(): ButtonModel { + this.saveChangesButton = new ButtonModel({ + attrs: { + type: BUTTON_TYPE.SUBMIT, + }, + classes: [styles.saveChangesButton], + text: BUTTON_TEXT[getStore().getState().currentLanguage].ADD_ADDRESS, + }); + 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/AddressEdit/model/AddressEditModel.ts b/src/features/AddressEdit/model/AddressEditModel.ts index f91accbd..185c1294 100644 --- a/src/features/AddressEdit/model/AddressEditModel.ts +++ b/src/features/AddressEdit/model/AddressEditModel.ts @@ -1,115 +1,153 @@ -// 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 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()); + } + + 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..96fac017 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.saveChangesButton.getHTML(), this.cancelButton.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/Breadcrumbs/view/breadcrumbsView.module.scss b/src/features/Breadcrumbs/view/breadcrumbsView.module.scss index 83709183..89200a9a 100644 --- a/src/features/Breadcrumbs/view/breadcrumbsView.module.scss +++ b/src/features/Breadcrumbs/view/breadcrumbsView.module.scss @@ -1,56 +1,26 @@ +@import 'src/app/styles/mixins'; + .breadcrumbs { display: flex; align-items: center; margin-bottom: var(--medium-offset); gap: var(--tiny-offset); + + @media (max-width: 768px) { + margin-bottom: var(--small-offset); + } } .link { - position: relative; - display: flex; - align-items: center; + @include link; + padding: var(--tiny-offset) calc(var(--tiny-offset) / 2); - height: 100%; - font: var(--regular-font); font-weight: 100; - letter-spacing: var(--one); - text-transform: uppercase; - color: var(--noble-gray-800); - transition: color 0.2s; - - &::after { - content: ''; - position: absolute; - left: 0; - bottom: calc(var(--one) * -1); - width: 100%; - height: var(--two); - background-color: currentcolor; - opacity: 0; - transform: scaleX(0); - transform-origin: center; - transition: - transform 0.2s, - opacity 0.2s; - } - - @media (hover: hover) { - &:hover { - color: var(--steam-green-800); - - &::after { - opacity: 1; - transform: scaleX(1); - } - } - } } .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 abf02d41..9f207305 100644 --- a/src/features/CountryChoice/model/CountryChoiceModel.ts +++ b/src/features/CountryChoice/model/CountryChoiceModel.ts @@ -1,10 +1,6 @@ import getStore from '@/shared/Store/Store.ts'; -import { setBillingCountry, setShippingCountry } from '@/shared/Store/actions.ts'; -import observeStore, { - selectBillingCountry, - selectCurrentLanguage, - selectShippingCountry, -} from '@/shared/Store/observer.ts'; +import { setBillingCountry, setDefaultCountry, setShippingCountry } from '@/shared/Store/actions.ts'; +import observeStore, { selectCurrentLanguage } from '@/shared/Store/observer.ts'; import { DATA_KEYS } from '@/shared/constants/common.ts'; import COUNTRIES_LIST from '@/shared/constants/countriesList.ts'; import { USER_ADDRESS_TYPE } from '@/shared/constants/forms.ts'; @@ -20,16 +16,6 @@ class CountryChoiceModel { this.view = new CountryChoiceView(input); this.setCountryItemsHandlers(input); this.setInputHandler(input); - - const action = - input.getAttribute(DATA_KEYS.ADDRESS_TYPE) === USER_ADDRESS_TYPE.BILLING - ? selectBillingCountry - : selectShippingCountry; - - observeStore(action, () => { - const event = new Event('input'); - input.dispatchEvent(event); - }); } private observeCurrentLanguage(item: HTMLDivElement): boolean { @@ -54,6 +40,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(); } }); @@ -66,7 +54,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/CountryChoice/view/countryChoiceView.module.scss b/src/features/CountryChoice/view/countryChoiceView.module.scss index fbfc65ba..92f9146e 100644 --- a/src/features/CountryChoice/view/countryChoiceView.module.scss +++ b/src/features/CountryChoice/view/countryChoiceView.module.scss @@ -84,8 +84,8 @@ border: 1px solid var(--noble-gray-300); border-radius: var(--small-br); width: 100%; - min-height: calc(var(--extra-large-offset) * 2); // 200px - max-height: calc(var(--extra-large-offset) * 2); // 200px + min-height: calc(var(--extra-large-offset) * 1.3); + max-height: calc(var(--extra-large-offset) * 1.3); background-color: var(--white); opacity: 1; visibility: visible; 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..b49380f2 --- /dev/null +++ b/src/features/Pagination/view/PaginationView.ts @@ -0,0 +1,121 @@ +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 { + this.prevPageButton.getHTML().disabled = page === DEFAULT_PAGE; + this.nextPageButton.getHTML().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..2aac1f73 --- /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: calc(var(--one) * 1.5) 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-700); + background-color: var(--white-tr); + transition: + color 0.2s, + outline 0.2s, + transform 0.2s, + opacity 0.2s; + user-select: none; + + @media (hover: hover) { + &:hover { + outline: calc(var(--one) * 1.5) 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: calc(var(--one) * 1.5) solid var(--steam-green-400); + color: var(--steam-green-400); + background-color: var(--noble-white-100); + pointer-events: none; +} diff --git a/src/features/PasswordEdit/model/PasswordEditModel.ts b/src/features/PasswordEdit/model/PasswordEditModel.ts index 9ff865de..0446de62 100644 --- a/src/features/PasswordEdit/model/PasswordEditModel.ts +++ b/src/features/PasswordEdit/model/PasswordEditModel.ts @@ -1,3 +1,5 @@ +import type InputFieldModel from '@/entities/InputField/model/InputFieldModel.ts'; + import getCustomerModel from '@/shared/API/customer/model/CustomerModel.ts'; import LoaderModel from '@/shared/Loader/model/LoaderModel.ts'; import modal from '@/shared/Modal/model/ModalModel.ts'; @@ -17,10 +19,41 @@ class PasswordEditModel { } private init(): void { + this.view.getInputFields().forEach((inputField) => this.setInputFieldHandlers(inputField)); + this.setPreventDefaultToForm(); this.setSwitchOldPasswordVisibilityHandler(); this.setSwitchNewPasswordVisibilityHandler(); + this.setSubmitFormHandler(); this.setCancelButtonHandler(); - this.setSaveChangesButtonHandler(); + } + + private async saveNewPassword(): Promise { + const loader = new LoaderModel(LOADER_SIZE.SMALL).getHTML(); + this.view.getSubmitButton().getHTML().append(loader); + try { + await getCustomerModel() + .getCurrentUser() + .then(async (user) => { + if (user) { + try { + await getCustomerModel().editPassword( + user, + this.view.getOldPasswordField().getView().getValue(), + this.view.getNewPasswordField().getView().getValue(), + ); + serverMessageModel.showServerMessage(SERVER_MESSAGE_KEYS.PASSWORD_CHANGED, MESSAGE_STATUS.SUCCESS); + modal.hide(); + } catch { + serverMessageModel.showServerMessage(SERVER_MESSAGE_KEYS.PASSWORD_NOT_CHANGED, MESSAGE_STATUS.ERROR); + } + } + }); + } catch (error) { + showErrorMessage(error); + } finally { + loader.remove(); + } + return true; } private setCancelButtonHandler(): boolean { @@ -33,37 +66,20 @@ class PasswordEditModel { return true; } - private setSaveChangesButtonHandler(): boolean { - this.view - .getSaveChangesButton() - .getHTML() - .addEventListener('click', async () => { - const loader = new LoaderModel(LOADER_SIZE.SMALL).getHTML(); - this.view.getSaveChangesButton().getHTML().append(loader); - try { - await getCustomerModel() - .getCurrentUser() - .then(async (user) => { - if (user) { - try { - await getCustomerModel().editPassword( - user, - this.view.getOldPasswordField().getView().getValue(), - this.view.getNewPasswordField().getView().getValue(), - ); - serverMessageModel.showServerMessage(SERVER_MESSAGE_KEYS.PASSWORD_CHANGED, MESSAGE_STATUS.SUCCESS); - modal.hide(); - } catch { - serverMessageModel.showServerMessage(SERVER_MESSAGE_KEYS.PASSWORD_NOT_CHANGED, MESSAGE_STATUS.ERROR); - } - } - }); - } catch { - showErrorMessage(); - } finally { - loader.remove(); - } - }); + private setInputFieldHandlers(inputField: InputFieldModel): boolean { + const inputHTML = inputField.getView().getInput().getHTML(); + inputHTML.addEventListener('input', () => this.switchSubmitFormButtonAccess()); + return true; + } + + private setPreventDefaultToForm(): boolean { + this.view.getHTML().addEventListener('submit', (event) => event.preventDefault()); + return true; + } + + private setSubmitFormHandler(): boolean { + const submitButton = this.view.getSubmitButton().getHTML(); + submitButton.addEventListener('click', this.saveNewPassword.bind(this)); return true; } @@ -87,7 +103,16 @@ class PasswordEditModel { return true; } - public getHTML(): HTMLDivElement { + private switchSubmitFormButtonAccess(): boolean { + if (this.view.getInputFields().every((inputField) => inputField.getIsValid())) { + this.view.getSubmitButton().setEnabled(); + } else { + this.view.getSubmitButton().setDisabled(); + } + return true; + } + + public getHTML(): HTMLFormElement { return this.view.getHTML(); } diff --git a/src/features/PasswordEdit/view/PasswordEditView.ts b/src/features/PasswordEdit/view/PasswordEditView.ts index 9f4fef42..84268183 100644 --- a/src/features/PasswordEdit/view/PasswordEditView.ts +++ b/src/features/PasswordEdit/view/PasswordEditView.ts @@ -21,16 +21,16 @@ class PasswordEditView { private oldPasswordField: InputFieldModel; - private saveChangesButton: ButtonModel; - private showNewPasswordElement: HTMLDivElement; private showOldPasswordElement: HTMLDivElement; - private view: HTMLDivElement; + private submitButton: ButtonModel; + + private view: HTMLFormElement; constructor() { - this.saveChangesButton = this.createSaveChangesButton(); + this.submitButton = this.createSubmitButton(); this.cancelButton = this.createCancelButton(); this.showOldPasswordElement = this.createShowOldPasswordElement(); this.showNewPasswordElement = this.createShowNewPasswordElement(); @@ -47,10 +47,10 @@ class PasswordEditView { return this.cancelButton; } - private createHTML(): HTMLDivElement { + private createHTML(): HTMLFormElement { this.view = createBaseElement({ cssClasses: [styles.wrapper], - tag: 'div', + tag: 'form', }); this.inputFields.forEach((inputField) => { @@ -65,7 +65,7 @@ class PasswordEditView { } }); - this.view.append(this.cancelButton.getHTML(), this.saveChangesButton.getHTML()); + this.view.append(this.submitButton.getHTML(), this.cancelButton.getHTML()); return this.view; } @@ -89,14 +89,6 @@ class PasswordEditView { return this.oldPasswordField; } - private createSaveChangesButton(): ButtonModel { - this.saveChangesButton = new ButtonModel({ - classes: [styles.saveChangesButton], - text: BUTTON_TEXT[getStore().getState().currentLanguage].SAVE_CHANGES, - }); - return this.saveChangesButton; - } - private createShowNewPasswordElement(): HTMLDivElement { this.showNewPasswordElement = createBaseElement({ cssClasses: [styles.showPasswordElement], @@ -115,11 +107,20 @@ class PasswordEditView { return this.showOldPasswordElement; } + private createSubmitButton(): ButtonModel { + this.submitButton = new ButtonModel({ + classes: [styles.submitButton], + text: BUTTON_TEXT[getStore().getState().currentLanguage].SAVE_CHANGES, + }); + this.submitButton.setDisabled(); + return this.submitButton; + } + public getCancelButton(): ButtonModel { return this.cancelButton; } - public getHTML(): HTMLDivElement { + public getHTML(): HTMLFormElement { return this.view; } @@ -135,10 +136,6 @@ class PasswordEditView { return this.oldPasswordField; } - public getSaveChangesButton(): ButtonModel { - return this.saveChangesButton; - } - public getShowNewPasswordElement(): HTMLDivElement { return this.showNewPasswordElement; } @@ -147,6 +144,10 @@ class PasswordEditView { return this.showOldPasswordElement; } + public getSubmitButton(): ButtonModel { + return this.submitButton; + } + public switchPasswordElementSVG(type: string, el: HTMLDivElement): SVGSVGElement { const element = el; const svg = document.createElementNS(SVG_DETAILS.SVG_URL, 'svg'); diff --git a/src/features/PasswordEdit/view/passwordEditView.module.scss b/src/features/PasswordEdit/view/passwordEditView.module.scss index eff20751..90b26ecd 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; @@ -10,39 +12,12 @@ gap: var(--extra-small-offset); } -.saveChangesButton, +.submitButton, .cancelButton { - 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; - } + @include green-btn; } -.saveChangesButton { - display: flex; +.submitButton { grid-row: 3; } diff --git a/src/features/PersonalInfoEdit/model/PersonalInfoEditModel.ts b/src/features/PersonalInfoEdit/model/PersonalInfoEditModel.ts index 8f2e87b6..d055e70b 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(); } @@ -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/features/PersonalInfoEdit/view/PersonalInfoEditView.ts b/src/features/PersonalInfoEdit/view/PersonalInfoEditView.ts index f6b25789..49ffbfb4 100644 --- a/src/features/PersonalInfoEdit/view/PersonalInfoEditView.ts +++ b/src/features/PersonalInfoEdit/view/PersonalInfoEditView.ts @@ -55,7 +55,7 @@ class PersonalInfoEditView { this.view.append(inputFieldElement.getHTML()); } - this.view.append(this.cancelButton.getHTML(), this.saveChangesButton.getHTML()); + this.view.append(this.saveChangesButton.getHTML(), this.cancelButton.getHTML()); return this.view; } 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/model/ProductFiltersModel.ts b/src/features/ProductFilters/model/ProductFiltersModel.ts index 1a943905..7b44af5d 100644 --- a/src/features/ProductFilters/model/ProductFiltersModel.ts +++ b/src/features/ProductFilters/model/ProductFiltersModel.ts @@ -1,146 +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(); - } - - public getMetaFilters(): HTMLDivElement { - return this.view.getMetaFilters(); + constructor(params: ProductFiltersParams | null, callback: () => void) { + this.view = new ProductFiltersView(params, callback); } - public updateParams(params: ProductFiltersParams | null): void { - this.view.updateParams(params); + public getView(): ProductFiltersView { + return this.view; } } diff --git a/src/features/ProductFilters/view/ProductFiltersView.ts b/src/features/ProductFilters/view/ProductFiltersView.ts index 3fbaba91..ae915464 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,12 @@ 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 +294,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 +316,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 +328,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 +384,25 @@ 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(); + }); + + observeStore(selectCurrentLanguage, () => { + this.resetFiltersButton.getHTML().textContent = BUTTON_TEXT[getStore().getState().currentLanguage].RESET; + }); + return this.resetFiltersButton; } @@ -371,11 +418,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 +525,59 @@ 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 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'); + this.callback(); } public getCategoryLinks(): LinkModel[] { @@ -544,12 +612,42 @@ 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.categoryCountSpan.forEach((span) => { const currentSpan = span; currentSpan.innerText = BASE_PRODUCT_COUNT; @@ -558,6 +656,7 @@ class ProductFiltersView { const currentSpan = span; currentSpan.innerText = BASE_PRODUCT_COUNT; }); + this.updatePriceRange(); this.redrawProductsCount(); } } 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/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..0f2fedce 100644 --- a/src/features/ProductSorts/view/ProductSortsView.ts +++ b/src/features/ProductSorts/view/ProductSortsView.ts @@ -1,15 +1,18 @@ +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'; import styles from './productSortsView.module.scss'; class ProductSortsView { + private callback: () => void; + private currentSortingSpan: HTMLSpanElement; private sortingList: HTMLUListElement; @@ -20,7 +23,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,22 +44,22 @@ class ProductSortsView { } private createCurrentSortingSpan(): HTMLSpanElement { - const { currentLanguage, selectedSorting } = getStore().getState(); - const upperText = selectedSorting?.field.toUpperCase() ?? TEXT[currentLanguage].DEFAULT; - if (isKeyOfSortField(upperText)) { - this.currentSortingSpan = createBaseElement({ - cssClasses: [styles.currentSortingSpan], - innerContent: TEXT[currentLanguage][upperText], - tag: 'span', - }); - } + const selectedSorting = RouterModel.getSearchParams().get(SEARCH_PARAMS_FIELD.FIELD); + + this.currentSortingSpan = createBaseElement({ + cssClasses: [styles.currentSortingSpan], + innerContent: selectedSorting + ? selectedSorting.toUpperCase() + : TEXT[getStore().getState().currentLanguage].DEFAULT.toUpperCase(), + tag: 'span', + }); observeStore(selectCurrentLanguage, () => { - const { currentLanguage, selectedSorting } = getStore().getState(); - const upperText = selectedSorting?.field.toUpperCase() ?? TEXT[currentLanguage].DEFAULT; - if (isKeyOfSortField(upperText)) { - this.currentSortingSpan.innerText = TEXT[currentLanguage][upperText]; - } + const selectedSorting = RouterModel.getSearchParams().get(SEARCH_PARAMS_FIELD.FIELD); + + this.currentSortingSpan.innerText = selectedSorting + ? selectedSorting.toUpperCase() + : TEXT[getStore().getState().currentLanguage].DEFAULT.toUpperCase(); }); return this.currentSortingSpan; @@ -93,7 +97,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 +128,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/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..1647263e 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 { @@ -44,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}

@@ -79,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(/