From 7c801e8a87c4abf022306ad1d24f16d7b8e3ebbc Mon Sep 17 00:00:00 2001 From: Nikita Mashchenko <52038455+nmashchenko@users.noreply.github.com> Date: Sat, 23 Mar 2024 02:03:56 -0700 Subject: [PATCH] feat: project done (#149) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: 🚑 ci (#102) * docs: update README.md * docs: update README.md * fix: :ambulance: fix ci don't work in develop and master branches --------- Co-authored-by: Nikita Mashchenko <52038455+nmashchenko@users.noreply.github.com> * feature: ✨ storybook pallete icons docs (#106) * feat: add colors story * feat: add loading story * feat: add iconography stories and change stories settings * feat: ✨ shared flex icon wrapper (#107) * feat: :sparkles: add flex component * feat: :sparkles: add flex component and docs * docs: :memo: add docs for Flex component * fix: in flex width auto -> 100% * fix: in flex width auto -> 100% in storybook * docs: :memo: add .md doc for how to use storybook (#108) * feature: ✨ add util - get-elapsed-time (#111) * feature: ✨ add util - get-past-time * refactor: :recycle: rename get-past-time => get-elapsed-time. And add docs * feat: ✨ add useGetScreenWidth hook (#112) * feature: ✨ add useGetScreenWidth hook * fix: :bug: get window width in server-side * test: ✅ init jest, and add 1 test, add to ci (#113) * test: init jest, and add 1 test, add to ci * test: init jest, and add 1 test, add to ci * fix: tests for get elapsed time * feat: ✨ add skeleton (#117) * feat: ✨ add skeleton * docs: :memo: add docs for skeleton * docs: :memo: add more story cases for storybook * fix: :adhesive_bandage: fix tests for get elapsed time * fix: shared components * docs: updated skeleton * feat: added teameights-types * feat: added loader * feat: added drawer, sonner * feat: settings are added (#123) * feat: settings are added * fix: props are renamed * feat: added select-autocomplete & debounce * feat: new badge component and updated iconwrapper (#125) * feat: new badge component and updated iconwrapper * fix: stories * fix: badge type added * fix: styles/logic --------- Co-authored-by: Nikita Mashchenko * refactor: added new backend (#101) * feat: added new server version * refactor: remove client * ci: added ci for backend * feat: added github auth / removed others * refactor: ♻️ absolute paths (#98) * refactor: ♻️ rename absolute paths * fix: :ambulance: fix storybook alias paths * fix: delete .idea directory * refactor: removed all server * docs: update core info * docs: fixed auth * fix: gtihub return entity * ci: fixed tests * ci: added api in docker * ci: fixed backend tests * fix: crlf disabled * ci: disabled api for local * feat: extended user schema, tests, dtos, etc * ci: fixed tests runner * feature: backend architecture (#120) * fix: refactor arch signature for actual Module struct * fix: connect docker & upload container structs * upload: reload references by migration * upd: fix docker * upload: production docker without nodeEnd, should fix it * feature: docker works * fix docs * ci: added backend branch * ci: fix branch name * fix: tests command * fix: added .env copying step * fix: doc & docker reference * fix: tests & docs --------- Co-authored-by: Nikita Mashchenko * fix: issues, password, username updates * fix: tests, ci, env-example * ci: temporary yarnpath fix for tests * feat: added universityData * fix: linter * fix: fixed docs & nested validation * feat: added all required relations for base user * feature: backend api docker (#122) * fix: autobuild project gun * fix: docs & workflow * fix: gitflow * fix: test gitflow * fix: test gitflow * fix: test gitflow * fix: test gitflow * fix: test gitflow * fix: logs * fix: sed * fix: sed * fix: compose postgres * v3.6.3 * v3.6.4 * fix: yarn * fix: postgres volumes * fix: production condinion toggle * docs: fixed descriptions * feat: merged refactor/backend * docs: fixed local setup * fix: removed old files * fix: merge test * fix: docker-compose dev virtual volume * fix: default starter prepare * fix: db-prepare env point * fix: sed checker * fix: docs & code --------- Co-authored-by: Nikita Mashchenko * feat: added filters query for users --------- Co-authored-by: Nikita Mashchenko <52038455+exortme1ster@users.noreply.github.com> Co-authored-by: Pikj [JReydman] Reyderman <58705342+jreydman@users.noreply.github.com> * fix: port for server * feat: updated auth based on new design * fix: design issues * feat: updated experience type * feat: added @teameights/types * feature: reusable modals (#121) * feat: added initial info-modal * fix: cleanup & useless styles * fix: fixed some styles, returned action modal * fix: changed the approach to using styles * fix: flex issue * fix: test version teameights-types * fix: types * fix: added type * fix: lib issues, reusability * fix: added new modal window * fix: badges and modals * fix: added logic for mobile user * fix: added new phone action modal * fix: added new phone action modal * fix: code issues * fix: changed folder location * fix: some styles * fix: added storybook for modal * fix: added storybook for modal * Revert "fix: added storybook for modal" This reverts commit c5e7773c4e5cf6d25ef83ddb866da5fa20a6f4d6. * fix: added for test new function * fix: added new modal for a test * fix: changed mock data * fix: changed mock data * fix: fixed new error * fix: previous issues and code * fix: namings * fix: namings * fix: added new modal that fixes freezes * feat: added image loader, fix issues with modals * feat: added flags & fixed issues --------- Co-authored-by: Nikita Mashchenko * fix: add events for flex component (#128) Co-authored-by: “merankori” * fix: 🐛 flex (#130) * fix: add events for flex component * fix: add rest props for Flex component --------- Co-authored-by: “merankori” * refactor(mocks): ✨ enhance user mock data generation with notificatfications (#129) * refactor(mocks): ✨ enhance user mock data generation with notifications and team * refactor(mocks): ♻️ convert functions to arrow functions * test * fix(eslint) * save changes * fix: refactor * fix: date type * fix: upgrade types --------- Co-authored-by: Nikita Mashchenko * feat: new search bar (#133) 🤟🏻 * feat: add border disable for ui select * feat: add filter-select draft functionality * fix: closing select menu and codestyle * fix: conflicts with develop * fix: removed server actions * fix: imports * fix: select and input border * fix: filter-select * fix: some fixes for select * fix: interfaces of searchbar * feat: add draft search-input functionality * fix: filter type in filter select * fix: some fixes for selects * feat: add draft functionality for tags-list * feat: add placeholders and tags-list for searchbar * feat: add outside click closing for multiple-tag * fix: fix text-filter clearing * fix: some tag fixes * feat: add clear all button in tags list * fix: change file structure * feat: add select indicator toggle * fix: fix search-bar styles * feat: add api request handler for search-input * fix: move tag in entities * fix: move tag in features * feat: add some filters for search-bar on page * fix: tags-list filtersArr typization * feat: replace react hook form functionality from search bar with native react * fix: checkbox-list functionality typization * fix: codestyle fixes * feat: add query string functionality for search-bar * fix: replace flex divs with Flex component * feat: add search-bar in storybook * feat: add multiple value filter functionality * fix: add multiple filter in stories / fix timer of search-bar * fix: codestyle fix in tag-menu * fix: destructurize fix * fix: state changing in search-bar * feat: add custom styles support in shared select * fix: margins and paddings of search-bar * fix: design fixes * fix: class naming fixes * fix: menu columns in search-bar input-select * fix: style fixes * fix: clear logs and fix naming of callbacks * fix: codereview fixes --------- Co-authored-by: Nikita Mashchenko Co-authored-by: skiselev * feat: added new icons/skills (#132) * feat: added new icons/files * feat: changed concentration to speciality * feat: added support for specialities * fix: import * fix: server support for updates * fix: docker * fix: prettier * feat: added isActive for badge * fix: added todos for backend * fix: final adjustments * fix: mocks, user entity, etc * fix: fallback * feat: transfer old react-query hooks (#109) * feat: added initial setup * fix: style * feat: added auth queries * feat: added password reset hooks * feat: folders refactor * fix: added server fixes * fix: removed bugged test * feat: changed hooks, consts & logout * fix: error handling * feat: fix merge issues * fix: deployment issue * fix: other issues * docs: update pull_request_template.md * fix: server cleanup (#136) * feat: new filters approach * fix: old issues * feat: new jwt approach to hash * fix: small issues on frontend * fix: lint errors * feat: added base layout for registration; refactored and added some s… (#134) * feat: added base layout for registration; refactored and added some shared component * fix: resolved tsc issue * fix: revert * fix: resolved conflicts * Revert "fix: revert" This reverts commit a2f4c0040fce93071332a7c5f0a50ffa186bd69a. * fix: inline styles --------- Co-authored-by: Nikita Mashchenko * fix: icons cleanup (#137) * fix: added new branch * fix: arrows * fix: cleaned up the icons on the new branch * fix: new names for icons and illustrations * Revert "fix: new names for icons and illustrations" This reverts commit 446473877f4c20652b1999acb0ae5bdd4c8f874e. * fix: changed the names of icons and illustrations * fix: some small issues * fix: lint --------- Co-authored-by: Nikita Mashchenko * feature: transfer user card functionality (#110) * feat(user-card): move user-card component from previuos branch * fix(docs): change .MD to .md * feat(and-more): add new component * feat(and-more): add counter * feat(and-more): {finaly} add counting of languages * fix(user-card): names of files * fix(user-card & and-more): delete comments * fix(user-card): move inline styles to separate file * fix(user-card): change props using + minor changes * feat(user-card): move 'and-more' feature to badge-framework component * feat(user-card): move 'and-more' feature to badge-languages component * fix(user-card): and-more button size in badge-framework component * fix(user-card): displaying of 2 languages * fix(and-more): delete unused component * fix(user-card): use css variables * fix(user-card): delete inline styles and move to scss file * fix(badge-framework): replace img tag with Image * refactor(user-card): new polymorphic function to use render badges * fix(user-card): all problems * fix(user-card): delete unused part of code * fix(user-card): linter * fix(user-card): fix placement of elements * feat(user-card): crown for leaders * feat: add new logic for render langs and frameworks * refactor: refactor * refactor: refactor * refactor: refactor * refactor: refactor * refactor: refactor scss * refactor: refactor * refactor: refactor * feat: :sparkles: add animation for crown * feat: :sparkles: add animation for card * feat: :sparkles: add types * feat: :sparkles: add types * feat: :sparkles: add @teameights/types * Delete node_modules directory * fix: save changes * fix: fix css * refactor: refactor * refactor: refactor * fix(user-card): crown * feat(user-card): hover effect * feat(user-card): use entities instead of widgets * feat(user-card): add controller for storybook * feat(user-card): storybook params * feat(user-card): add onClick param for useCallback hooks * save changes * feat(user-card): adjust badgeFrameworkLayoutConfig and Image component in card module * feat(user-card): add hover effect and cursor pointer to user card component * feat(user-card): add hover effect and cursor pointer to user card component * feat(user-card): update UserCard component to display user's date of birth and role name * feat(user-card): use generateMockUser function for default user in UserCardPreview story * feat(user-card): update generateMockFileEntity to include random query parameter for image path 🚀 * feat(user-card): calculate and display user's age in UserCard component 🎂 * feat(user-card): add new mock data generation functions and update Home component to use them 🚀 * feat(user-card): update file paths and import styles to use consistent naming convention 🔄📦 * feat(user-card): add new logic for render langs and frameworks 🌐✨ * fix * fix * feat(user-card): restyle badge-language * fix: adjust code to changes from merge * fix(user-card): update user for storybook to match user type * feat(user-card): add country flag icon --------- Co-authored-by: Berezhnev Vladimir Co-authored-by: Sivritkin Dmitriy Co-authored-by: Romas Bitinas <93491714+pupixipup@users.noreply.github.com> * feat: mobile search (#139) * feat: add context for search-bar props * feat: add draft functionality of mobile-search * fix: refactor * feat: add portaling for search select * fix: icons naming * fix: style fixes * feat: add useGetUsers hook * feat: add drawer in modal and optimize modal button * fix: getting data and modal button fixes * fix: drawer class types * fix: codestyle fixes * feat: move search-bar in shared * fix: refactor and codestyle fixes * fix: small issue * fix: use client --------- Co-authored-by: “merankori” Co-authored-by: Nikita Mashchenko * feat: added server notifications (#140) * feat: added notifications * fix: previous issues with notifications * fix: lint issues * feat: added ws support * fix: lint * feat: ✨ create sidebar (#114) * add: :bento: add unregistered user image to public * add: :bento: icons * fix: git stash * feat: ✨ add types for user, team, role, notification * git stash * git stash * feat: ✨ add sidebar * fix: :adhesive_bandage: fix build * fix: :adhesive_bandage: fix build * docs: :memo: add storybook for notification-item and notification-content * docs: :memo: JSDoc for notification-content * docs: :memo: JSDoc for notification-list, add stories, add tests for sort-notifications * fix: save changes * refactor: with new types `notification-item` * refactor: with new types `notification-list` * refactor: with new types `notification-modal` * refactor: with new types `sidebar` * refactor: with new types `sidebar-profile` * fix save * refactor(sidebar): 📦♻️ update typings to use `@teameights/types` * refactor code * save changes * refactor: :recycle: replace hardcoded mock data with generateMockUser and other mock data generators in sidebar and notification components * refactor(sidebar): :art: update styles and typography in sidebar components * feat(icons): :sparkles: update burger-clo se icon in shared assets * feat(sidebar): :sparkles: add user state and image loading skeleton in sidebar-profile * feat(sidebar-profile): :sparkles: replace Image with ImageLoader in sidebar-profile component * feat: merged develop * fix: storybook * feat: added server * fix: linter * fix: reading notifications issues * fix: small issues * feat: added websocket * fix: socket issue * fix: linter issue * fix: sockets / timings issues --------- Co-authored-by: Nikita Mashchenko * feat: added main page and connected everything (#141) * feat: added initial implementation * Revert "feat: added initial implementation" This reverts commit 83f61523ff87965f429ffa168300d383ca8e7670. * feat: added initial screen with issues * feat: added server loading & filters, etc * feat: final touches before merging * feat: added multi-step signup (#138) * feat: added new registration step - Account-Type * feat: added new step * fix: small fixes in styles * fix: removed inline styles * fix: added new step * fix: added new step * feat: added icons and updated the input-link * fix: some fixes with icons * feat: added new step Specialty * feat: add step 4 of onboarding * fix: minor lang selection update * fix: minor issues & reusability * fix: disable select if no empty cells * feat: add recommended languages * feat: add search to languages * wip: implement form * feat: bind speciality step to the form * fix: update effect dependencies list * feat: update next step logics * feat: update select-fields layout * refactor: apply Prettier * fix: speciality to occupation conversion * typo: update language label * feat: add selected field component * fix: remove step 1 recommendations on step 2 * feat: new approach wip * feat: wip (need to fix errors) * fix: handle key prop error * fix: final client cleanup * feat: add responsiveness to ActionSection * feat: add responsiveness to AccountType * feat: restyle Options * feat: add responsiveness to steps * fix: change recommended_icons height * feat: refactor + illustrations * fix: server stuff to support registration * fix: minor issues * feat: everything working now * fix: mocks * fix: lint issues * fix: backend lint * feat: add illustration resizing * feat: add illustration resizing * fix: update icons size * fix: rename Javascript to JavaScript * fix: styles * feat: last touches * fix: lint * fix: lint part 2 * fix: seeds * fix: small updates * fix: remove console log * fix: removed broken tests --------- Co-authored-by: Romas Bitinas <93491714+pupixipup@users.noreply.github.com> Co-authored-by: Nikita Mashchenko * fix: heroku procfile * fix: disable npm for heroku * Feature/friends (#143) * feat: create friendship system * test: add e2e tests for friendship functionality * wip: add basic profile layout * feat: add profile list icons * feat: add icons * feat: add icons * feat: add social links * feat: add logo * fix: layout behind aside menu * feat: add back button * feat: add age * fix: add migration * fix: remove profile id at the button link * fix: toggle leader icon based on fetch data * feat: add empty placeholder for about section * update: add check friendship status * feat: add [username] to the path * Revert "feat: add [username] to the path" This reverts commit 5027de5a1cccebf80a120fd54769c0725336dca1. * fix: change route to the profile page * feat: add empty state * update: reworked notification update * update: updated friendship delete * fix: incorrect image reference * fix: case when links are missing * fix: minor ui changes * refactor: run prettier * feat: add skills (except projects & tournaments) * feat: add friends button logics * refactor: remove eslint errors * fix: back icon position * refactor: run Prettier * fix: specialty field to match updated type * fix: skills to match updated structure * fix: replace useGetMe with useGetUser * feat: add Friendship seed * fix: remove nav placeholder shrinking * fix: profile nav button path * fix: full navbar size * wip: add list of friends * feat: add friends modal * feat: add responsiveness * refactor: move logics to page.tsx * feat: add friend removal * refactor: remove unused import * fix: end -> flex-end * refactor: split ui to folders * fix: remove icon overlay * fix: back icon position * fix: small updates * fix: prettier * fix: backend lint * update: added username check * update: added username check * update: updated username check * feat: friend button states * fix: display correct skills * refactor: apply prettier * feat: add friend button (at user modal) * feat: add friend notification * feat: add profile link to user modal * refactor: add friend notification interface * feat: add width prop to friend btn * feat: add friend button to infoModal * feat: * fix: fixed friendship status receiving * fix: cleanup * fix: generation --------- Co-authored-by: Romas Bitinas <93491714+pupixipup@users.noreply.github.com> Co-authored-by: Nikita Mashchenko * Fix/search bar (#144) * fix: some search bar style fixes * feat: add reducer and timer disabling for searchbar * fix: fix bad setState inside render * fix: style fixes for mobile filters * fix: add portal for drawer * fix: fixes for tags * fix: filter opening --------- Co-authored-by: Nikita Mashchenko --------- Co-authored-by: Sivritkin Dmitriy <129217598+velenyx@users.noreply.github.com> Co-authored-by: dmtrack Co-authored-by: Nikita Mashchenko <52038455+exortme1ster@users.noreply.github.com> Co-authored-by: Pikj [JReydman] Reyderman <58705342+jreydman@users.noreply.github.com> Co-authored-by: LanselonX <101521725+LanselonX@users.noreply.github.com> Co-authored-by: MelancholicCode <96338248+MelancholicCode@users.noreply.github.com> Co-authored-by: “merankori” Co-authored-by: Michael Maliar <61254973+mikhail2404@users.noreply.github.com> Co-authored-by: Berezhnev Vladimir Co-authored-by: Sivritkin Dmitriy Co-authored-by: Romas Bitinas <93491714+pupixipup@users.noreply.github.com> Co-authored-by: merankori <96338248+merankori@users.noreply.github.com> Co-authored-by: Ivan Sai <46083400+Ivan-Sai@users.noreply.github.com> --- client/.husky/commit-msg | 4 - client/.husky/pre-commit | 4 - client/.husky/pre-push | 4 - .../[username]/profile/layout.module.scss | 55 ++ .../app/(main)/[username]/profile/layout.tsx | 24 + .../[username]/profile/lib/profile-context.ts | 3 + .../profile/lib/useGetUserByName.ts | 8 + .../app/(main)/[username]/profile/page.tsx | 46 ++ .../profile/ui/about/about.module.scss | 3 + .../[username]/profile/ui/about/about.tsx | 58 ++ .../profile/ui/card/card.module.scss | 6 + .../[username]/profile/ui/card/card.tsx | 14 + .../profile/ui/fields/education.tsx | 35 ++ .../profile/ui/fields/fields.module.scss | 16 + .../[username]/profile/ui/fields/fields.tsx | 46 ++ .../[username]/profile/ui/fields/skills.tsx | 36 ++ .../profile/ui/fields/work-experience.tsx | 29 + .../profile/ui/friends/friends-modal.tsx | 60 ++ .../profile/ui/friends/friends.module.scss | 32 ++ .../[username]/profile/ui/friends/friends.tsx | 95 ++++ .../profile/ui/header/header.module.scss | 28 + .../[username]/profile/ui/header/header.tsx | 75 +++ .../profile/ui/list/list.module.scss | 0 .../[username]/profile/ui/list/list.tsx | 30 + .../[username]/profile/ui/row/row.module.scss | 0 .../(main)/[username]/profile/ui/row/row.tsx | 16 + client/src/app/(main)/layout.module.scss | 33 +- client/src/app/(main)/layout.tsx | 1 + client/src/app/(main)/page.tsx | 3 + .../src/app/(main)/ui/cards/cards.module.scss | 10 +- client/src/entities/session/api/index.ts | 1 + .../src/entities/session/api/useAddFriend.tsx | 22 + .../entities/session/api/useGetFriends.tsx | 26 + .../session/api/useGetFriendshipStatus.tsx | 22 + .../api/useHandleFriendshipRequest.tsx | 30 + .../entities/session/api/useRemoveFriend.tsx | 19 + .../features/friend-button/friend-button.tsx | 79 +++ client/src/features/friend-button/index.ts | 1 + client/src/shared/assets/icons/cake.tsx | 51 ++ client/src/shared/assets/icons/index.ts | 3 + client/src/shared/assets/icons/map-pin.tsx | 20 + client/src/shared/assets/icons/star.tsx | 20 + client/src/shared/constant/server-routes.ts | 1 + .../shared/ui/checkbox/checkbox.module.scss | 4 + client/src/shared/ui/drawer/drawer.tsx | 29 +- .../ui/image-loader/image-loader.module.scss | 2 +- .../shared/ui/search-bar/actions/actions.ts | 38 ++ .../src/shared/ui/search-bar/actions/index.ts | 9 + .../src/shared/ui/search-bar/actions/types.ts | 39 ++ .../src/shared/ui/search-bar/hooks/index.ts | 1 + .../ui/search-bar/hooks/useFilterReducer.ts | 132 +++++ .../ui/search-bar/hooks/useTrackFiltersArr.ts | 77 +-- .../ui/search-bar/search-bar.module.scss | 6 + .../ui/search-bar/search-bar.stories.tsx | 8 +- .../src/shared/ui/search-bar/search-bar.tsx | 66 ++- .../src/shared/ui/search-bar/types/types.ts | 10 +- .../ui/filter-menu/filter-menu.module.scss | 9 +- .../search-bar/ui/filter-menu/filter-menu.tsx | 23 +- .../ui/modal-button/modal-button.tsx | 10 +- .../shared/ui/search-bar/ui/modal/modal.tsx | 69 +-- .../ui/search-input/search-input.tsx | 13 +- .../search-select/search-select.module.scss | 25 +- .../ui/search-select/search-select.tsx | 19 +- .../ui/search-tag-menu/search-tag-menu.tsx | 14 +- .../ui/tag-list/tag-list.module.scss | 27 +- .../ui/search-bar/ui/tag-list/tag-list.tsx | 118 ++-- .../src/shared/ui/search-bar/ui/tag/tag.tsx | 12 +- .../ui/text-input/text-input.module.scss | 5 +- .../search-bar/ui/text-input/text-input.tsx | 1 + client/src/shared/ui/skeleton/skeleton.tsx | 13 +- .../info-modal/user/desktop/desktop.tsx | 26 +- .../modals/info-modal/user/phone/phone.tsx | 25 +- .../sidebar/config/getSidebarItems.tsx | 2 +- .../src/widgets/sidebar/interfaces/index.ts | 1 + .../sidebar/interfaces/notification.ts | 17 + .../notification-item/friend-notification.tsx | 59 ++ .../notification-item.module.scss | 4 + .../notification-item/notification-item.tsx | 8 +- .../sidebar/ui/sidebar/sidebar.module.scss | 4 +- server/Procfile | 3 +- server/package.json | 4 +- server/src/app.module.ts | 2 + .../1706204678430-CreateFriendship.ts | 28 + .../friendship/friendship-seed.module.ts | 12 + .../friendship/friendship-seed.service.ts | 36 ++ server/src/libs/database/seeds/run-seed.ts | 2 + server/src/libs/database/seeds/seed.module.ts | 2 + server/src/modules/auth/base/auth.service.ts | 31 +- .../modules/auth/base/dto/auth-update.dto.ts | 2 + .../friendship/dto/query-friends.dto.ts | 56 ++ .../friendship/dto/update-status.dto.ts | 10 + .../friendship/entities/friendship.entity.ts | 35 ++ .../friendship/friendship.controller.ts | 98 ++++ .../modules/friendship/friendship.module.ts | 16 + .../modules/friendship/friendship.service.ts | 521 ++++++++++++++++++ .../friendship/types/friendship.types.ts | 11 + .../dto/create-notification.dto.ts | 51 +- .../dto/update-friend-request-status.dto.ts | 14 + .../notifications/notifications.controller.ts | 20 +- .../notifications/notifications.service.ts | 126 ++++- .../notifications/types/notification.type.ts | 35 +- .../src/modules/users/entities/user.entity.ts | 8 +- server/src/modules/users/users.service.ts | 85 +++ server/test/friendship/friendship.e2e-spec.ts | 159 ++++++ server/test/user/users.e2e-spec.ts | 5 +- 105 files changed, 3024 insertions(+), 342 deletions(-) delete mode 100755 client/.husky/commit-msg delete mode 100755 client/.husky/pre-commit delete mode 100755 client/.husky/pre-push create mode 100644 client/src/app/(main)/[username]/profile/layout.module.scss create mode 100644 client/src/app/(main)/[username]/profile/layout.tsx create mode 100644 client/src/app/(main)/[username]/profile/lib/profile-context.ts create mode 100644 client/src/app/(main)/[username]/profile/lib/useGetUserByName.ts create mode 100644 client/src/app/(main)/[username]/profile/page.tsx create mode 100644 client/src/app/(main)/[username]/profile/ui/about/about.module.scss create mode 100644 client/src/app/(main)/[username]/profile/ui/about/about.tsx create mode 100644 client/src/app/(main)/[username]/profile/ui/card/card.module.scss create mode 100644 client/src/app/(main)/[username]/profile/ui/card/card.tsx create mode 100644 client/src/app/(main)/[username]/profile/ui/fields/education.tsx create mode 100644 client/src/app/(main)/[username]/profile/ui/fields/fields.module.scss create mode 100644 client/src/app/(main)/[username]/profile/ui/fields/fields.tsx create mode 100644 client/src/app/(main)/[username]/profile/ui/fields/skills.tsx create mode 100644 client/src/app/(main)/[username]/profile/ui/fields/work-experience.tsx create mode 100644 client/src/app/(main)/[username]/profile/ui/friends/friends-modal.tsx create mode 100644 client/src/app/(main)/[username]/profile/ui/friends/friends.module.scss create mode 100644 client/src/app/(main)/[username]/profile/ui/friends/friends.tsx create mode 100644 client/src/app/(main)/[username]/profile/ui/header/header.module.scss create mode 100644 client/src/app/(main)/[username]/profile/ui/header/header.tsx create mode 100644 client/src/app/(main)/[username]/profile/ui/list/list.module.scss create mode 100644 client/src/app/(main)/[username]/profile/ui/list/list.tsx create mode 100644 client/src/app/(main)/[username]/profile/ui/row/row.module.scss create mode 100644 client/src/app/(main)/[username]/profile/ui/row/row.tsx create mode 100644 client/src/entities/session/api/useAddFriend.tsx create mode 100644 client/src/entities/session/api/useGetFriends.tsx create mode 100644 client/src/entities/session/api/useGetFriendshipStatus.tsx create mode 100644 client/src/entities/session/api/useHandleFriendshipRequest.tsx create mode 100644 client/src/entities/session/api/useRemoveFriend.tsx create mode 100644 client/src/features/friend-button/friend-button.tsx create mode 100644 client/src/features/friend-button/index.ts create mode 100644 client/src/shared/assets/icons/cake.tsx create mode 100644 client/src/shared/assets/icons/map-pin.tsx create mode 100644 client/src/shared/assets/icons/star.tsx create mode 100644 client/src/shared/ui/search-bar/actions/actions.ts create mode 100644 client/src/shared/ui/search-bar/actions/index.ts create mode 100644 client/src/shared/ui/search-bar/actions/types.ts create mode 100644 client/src/shared/ui/search-bar/hooks/useFilterReducer.ts create mode 100644 client/src/widgets/sidebar/interfaces/index.ts create mode 100644 client/src/widgets/sidebar/interfaces/notification.ts create mode 100644 client/src/widgets/sidebar/ui/notification-item/friend-notification.tsx create mode 100644 server/src/libs/database/migrations/1706204678430-CreateFriendship.ts create mode 100644 server/src/libs/database/seeds/friendship/friendship-seed.module.ts create mode 100644 server/src/libs/database/seeds/friendship/friendship-seed.service.ts create mode 100644 server/src/modules/friendship/dto/query-friends.dto.ts create mode 100644 server/src/modules/friendship/dto/update-status.dto.ts create mode 100644 server/src/modules/friendship/entities/friendship.entity.ts create mode 100644 server/src/modules/friendship/friendship.controller.ts create mode 100644 server/src/modules/friendship/friendship.module.ts create mode 100644 server/src/modules/friendship/friendship.service.ts create mode 100644 server/src/modules/friendship/types/friendship.types.ts create mode 100644 server/src/modules/notifications/dto/update-friend-request-status.dto.ts create mode 100644 server/test/friendship/friendship.e2e-spec.ts diff --git a/client/.husky/commit-msg b/client/.husky/commit-msg deleted file mode 100755 index 30faab02a..000000000 --- a/client/.husky/commit-msg +++ /dev/null @@ -1,4 +0,0 @@ -#!/bin/sh -. "$(dirname "$0")/_/husky.sh" - -cd ./client && npx --no -- commitlint --edit $1 diff --git a/client/.husky/pre-commit b/client/.husky/pre-commit deleted file mode 100755 index b6dd6c3f3..000000000 --- a/client/.husky/pre-commit +++ /dev/null @@ -1,4 +0,0 @@ -#!/bin/sh -. "$(dirname "$0")/_/husky.sh" - -cd ./client && yarn lint \ No newline at end of file diff --git a/client/.husky/pre-push b/client/.husky/pre-push deleted file mode 100755 index eced198ea..000000000 --- a/client/.husky/pre-push +++ /dev/null @@ -1,4 +0,0 @@ -#!/bin/sh -. "$(dirname "$0")/_/husky.sh" - -cd ./client && yarn build \ No newline at end of file diff --git a/client/src/app/(main)/[username]/profile/layout.module.scss b/client/src/app/(main)/[username]/profile/layout.module.scss new file mode 100644 index 000000000..fb40784ac --- /dev/null +++ b/client/src/app/(main)/[username]/profile/layout.module.scss @@ -0,0 +1,55 @@ + +.container { + display: flex; + width: 100%; + height: 100%; + position: relative; + @media (max-width: 790px) { + position: unset; + } + .body { + width: 100%; + } +} + +.back { + position: absolute; + + left: 0; + display: flex; + align-items: center; + gap: 6px; + &:hover { + opacity: .7; + transition: .3s; + } + + @media (max-width: 790px) { + top: 46px; + right: 15px; + left: unset; + } +} + +.profile_row { + @media (max-width: 1045px) { + flex-direction: column; + width: 100%; + & > div { + width: 100%; + } + } +} + +.sm_card { + width: 40%; +} + +.lg_card { + width: 60%; +} + +.list_card { + width: 40%; + gap: 18px; +} diff --git a/client/src/app/(main)/[username]/profile/layout.tsx b/client/src/app/(main)/[username]/profile/layout.tsx new file mode 100644 index 000000000..15e825884 --- /dev/null +++ b/client/src/app/(main)/[username]/profile/layout.tsx @@ -0,0 +1,24 @@ +'use client'; +import styles from './layout.module.scss'; +import { Flex, Typography } from '@/shared/ui'; +import { ArrowLeftIcon, LogoBig } from '@/shared/assets'; +import { useRouter } from 'next/navigation'; + +export default function Layout({ children }: { children: React.ReactNode }) { + const router = useRouter(); + + return ( +
+ + + + + + {children} + +
+ ); +} diff --git a/client/src/app/(main)/[username]/profile/lib/profile-context.ts b/client/src/app/(main)/[username]/profile/lib/profile-context.ts new file mode 100644 index 000000000..47d814965 --- /dev/null +++ b/client/src/app/(main)/[username]/profile/lib/profile-context.ts @@ -0,0 +1,3 @@ +import { createContext } from 'react'; + +export const ProfileContext = createContext(false); diff --git a/client/src/app/(main)/[username]/profile/lib/useGetUserByName.ts b/client/src/app/(main)/[username]/profile/lib/useGetUserByName.ts new file mode 100644 index 000000000..759e171d7 --- /dev/null +++ b/client/src/app/(main)/[username]/profile/lib/useGetUserByName.ts @@ -0,0 +1,8 @@ +import { useGetUsers } from '@/entities/session'; +import { IUserResponse } from '@teameights/types'; + +export const useGetUserByName = (username: string): { data: IUserResponse | undefined } => { + const users = useGetUsers(JSON.stringify({ username: username })); + + return { data: users?.data?.pages[0]?.data[0] }; +}; diff --git a/client/src/app/(main)/[username]/profile/page.tsx b/client/src/app/(main)/[username]/profile/page.tsx new file mode 100644 index 000000000..783690dfe --- /dev/null +++ b/client/src/app/(main)/[username]/profile/page.tsx @@ -0,0 +1,46 @@ +'use client'; +import styles from './layout.module.scss'; +import { useGetMe } from '@/entities/session'; +import { Header } from './ui/header/header'; +import { CardSkeleton, Flex } from '@/shared/ui'; +import { List } from './ui/list/list'; +import { About } from './ui/about/about'; +import { useParams } from 'next/navigation'; +import { Friends } from './ui/friends/friends'; +import { Fields } from './ui/fields/fields'; +import { useGetUserByName } from './lib/useGetUserByName'; +import { ProfileContext } from './lib/profile-context'; + +export default function Page() { + const { data: me } = useGetMe(); + const { username } = useParams(); + const { data: user } = useGetUserByName(username as string); + const isMyProf = me?.username === username; + + let body = ( + + + + + ); + + if (user) { + body = ( + <> +
+ + + + + + + + + + + + ); + } + + return {body} ; +} diff --git a/client/src/app/(main)/[username]/profile/ui/about/about.module.scss b/client/src/app/(main)/[username]/profile/ui/about/about.module.scss new file mode 100644 index 000000000..11be4b362 --- /dev/null +++ b/client/src/app/(main)/[username]/profile/ui/about/about.module.scss @@ -0,0 +1,3 @@ +.container { + +} diff --git a/client/src/app/(main)/[username]/profile/ui/about/about.tsx b/client/src/app/(main)/[username]/profile/ui/about/about.tsx new file mode 100644 index 000000000..2ee2227a6 --- /dev/null +++ b/client/src/app/(main)/[username]/profile/ui/about/about.tsx @@ -0,0 +1,58 @@ +import { Card } from '../card/card'; +import { Flex, Typography } from '@/shared/ui'; +import { GithubIcon } from '@/shared/assets/icons/github-icon'; +import { BehanceIcon } from '@/shared/assets/icons/behance'; +import { TelegramIcon } from '@/shared/assets/icons/telegram'; +import { LinkedinIcon } from '@/shared/assets/icons/linkedin'; +import { useParams } from 'next/navigation'; +import { useGetUserByName } from '../../lib/useGetUserByName'; +import styles from '../../layout.module.scss'; + +export const About = () => { + const { username } = useParams(); + const { data: user } = useGetUserByName(username as string); + + const linksPresent = user?.links && Object.keys(user.links).length; + const descPresent = typeof user?.description === 'string'; + return ( + + + + About + + {!descPresent && ( + + No description added. + + )} + + {descPresent && {user?.description}} + + {linksPresent && ( + + {user?.links?.github && ( + + + + )} + {user?.links?.behance && ( + + + + )} + {user?.links?.telegram && ( + + + + )} + {user?.links?.linkedIn && ( + + + + )} + + )} + + + ); +}; diff --git a/client/src/app/(main)/[username]/profile/ui/card/card.module.scss b/client/src/app/(main)/[username]/profile/ui/card/card.module.scss new file mode 100644 index 000000000..01dfef8a2 --- /dev/null +++ b/client/src/app/(main)/[username]/profile/ui/card/card.module.scss @@ -0,0 +1,6 @@ +.card { + background: rgba(26, 28, 34, 1); + border-radius: 15px; + overflow: hidden; + padding: 32px; +} diff --git a/client/src/app/(main)/[username]/profile/ui/card/card.tsx b/client/src/app/(main)/[username]/profile/ui/card/card.tsx new file mode 100644 index 000000000..179deb8d9 --- /dev/null +++ b/client/src/app/(main)/[username]/profile/ui/card/card.tsx @@ -0,0 +1,14 @@ +import { ReactNode } from 'react'; +import styles from './card.module.scss'; +import clsx from 'clsx'; + +interface CardProps { + children: ReactNode; + className?: string; + borderRadius?: string; +} + +export const Card = ({ children, className }: CardProps) => { + const cls = clsx(styles.card, className); + return
{children}
; +}; diff --git a/client/src/app/(main)/[username]/profile/ui/fields/education.tsx b/client/src/app/(main)/[username]/profile/ui/fields/education.tsx new file mode 100644 index 000000000..9c9ff17e3 --- /dev/null +++ b/client/src/app/(main)/[username]/profile/ui/fields/education.tsx @@ -0,0 +1,35 @@ +import { Flex, Typography } from '@/shared/ui'; +import { useParams } from 'next/navigation'; +import { useGetUserByName } from '../../lib/useGetUserByName'; + +export const Education = () => { + const { username } = useParams(); + const { data: user } = useGetUserByName(username as string); + const universities = user?.universities; + if (!universities) return No information; + return ( + + {universities.map((education, i) => { + const start = new Date(education.admissionDate).getFullYear(); + const end = education.graduationDate + ? new Date(education.graduationDate).getFullYear() + : 'Present'; + return ( + + + + {education.name ?? (education as unknown as { university: string }).university} + + + {education.degree} in {education.major} + + + + {start} - {end} + + + ); + })} + + ); +}; diff --git a/client/src/app/(main)/[username]/profile/ui/fields/fields.module.scss b/client/src/app/(main)/[username]/profile/ui/fields/fields.module.scss new file mode 100644 index 000000000..f69e74159 --- /dev/null +++ b/client/src/app/(main)/[username]/profile/ui/fields/fields.module.scss @@ -0,0 +1,16 @@ +.selected { + border-bottom: 1px solid #5bd424; +} + +.field_text { + transition: 0.3s; + transition-property: color; +} + +.fields_container { + min-height: 150px; +} + +.nav_bar { + overflow-x: auto; +} \ No newline at end of file diff --git a/client/src/app/(main)/[username]/profile/ui/fields/fields.tsx b/client/src/app/(main)/[username]/profile/ui/fields/fields.tsx new file mode 100644 index 000000000..301cb53df --- /dev/null +++ b/client/src/app/(main)/[username]/profile/ui/fields/fields.tsx @@ -0,0 +1,46 @@ +import { Card } from '../card/card'; +import { Flex, Typography } from '@/shared/ui'; +import { useState } from 'react'; +import styles from './fields.module.scss'; +import { Skills } from './skills'; +import { WorkExperience } from './work-experience'; +import { Education } from './education'; +import layoutStyles from '../../layout.module.scss'; +export const Fields = () => { + const [field, setField] = useState('Skills'); + + const fields = { + Skills: , + Projects: null, + 'Work experience': , + Education: , + Tournaments: null, + }; + + return ( + + + + {Object.keys(fields).map(key => { + const classProps = field === key ? { className: styles.selected } : {}; + return ( + + ); + })} + + {fields[field]} + + + ); +}; diff --git a/client/src/app/(main)/[username]/profile/ui/fields/skills.tsx b/client/src/app/(main)/[username]/profile/ui/fields/skills.tsx new file mode 100644 index 000000000..94b1b5c31 --- /dev/null +++ b/client/src/app/(main)/[username]/profile/ui/fields/skills.tsx @@ -0,0 +1,36 @@ +import { BadgeText, Flex, Typography } from '@/shared/ui'; +import { useGetUserByName } from '../../lib/useGetUserByName'; +import { useParams } from 'next/navigation'; +import { BadgeIcon } from '@/shared/ui'; + +export const Skills = () => { + const { username } = useParams(); + const { data: user } = useGetUserByName(username as string); + const skills = { + coreTools: { + badge: ({ data }: { data: string }) => , + title: 'Core Tools', + }, + additionalTools: { + badge: BadgeText, + title: 'Additional Tools', + }, + }; + return ( + + {user!.skills && + Object.entries(skills).map(skill => { + const skillName = skill[0] as keyof typeof skills; + const Badge = skills[skillName].badge; + return ( + + {skills[skillName].title} + + {user?.skills![skillName]?.map((lang: string) => )} + + + ); + })} + + ); +}; diff --git a/client/src/app/(main)/[username]/profile/ui/fields/work-experience.tsx b/client/src/app/(main)/[username]/profile/ui/fields/work-experience.tsx new file mode 100644 index 000000000..92a1f9f66 --- /dev/null +++ b/client/src/app/(main)/[username]/profile/ui/fields/work-experience.tsx @@ -0,0 +1,29 @@ +import { Flex, Typography } from '@/shared/ui'; +import { useParams } from 'next/navigation'; +import { useGetUserByName } from '../../lib/useGetUserByName'; +export const WorkExperience = () => { + const { username } = useParams(); + const { data: user } = useGetUserByName(username as string); + const jobs = user?.jobs; + return ( + + {jobs?.map((job, i: number) => { + const start = new Date(job.startDate).getFullYear(); + const end = job.endDate ? new Date(job.endDate).getFullYear() : 'Present'; + return ( + + + {job.company} + + {job.title} + + + + {start} - {end} + + + ); + })} + + ); +}; diff --git a/client/src/app/(main)/[username]/profile/ui/friends/friends-modal.tsx b/client/src/app/(main)/[username]/profile/ui/friends/friends-modal.tsx new file mode 100644 index 000000000..952059026 --- /dev/null +++ b/client/src/app/(main)/[username]/profile/ui/friends/friends-modal.tsx @@ -0,0 +1,60 @@ +import { Flex, ImageLoader, Typography } from '@/shared/ui'; +import styles from './friends.module.scss'; +import { Modal } from '@/shared/ui'; +import { getCountryFlag } from '@/shared/lib'; +import { IUserBase } from '@teameights/types'; + +interface FriendsModalProps { + friendsList: IUserBase[]; + isFriendsModalOpen: boolean; + setFriendsModal: (state: boolean) => void; +} + +export const FriendsModal = ({ + friendsList, + isFriendsModalOpen, + setFriendsModal, +}: FriendsModalProps) => { + return ( + setFriendsModal(false)} isOpen={isFriendsModalOpen}> + + + Friends + + + {friendsList.map(friend => { + return ( + + + + + + {friend.username ?? 'usernamehey'} + + + + + {friend.skills?.speciality} + + + + ); + })} + + + + ); +}; diff --git a/client/src/app/(main)/[username]/profile/ui/friends/friends.module.scss b/client/src/app/(main)/[username]/profile/ui/friends/friends.module.scss new file mode 100644 index 000000000..6adccb4ce --- /dev/null +++ b/client/src/app/(main)/[username]/profile/ui/friends/friends.module.scss @@ -0,0 +1,32 @@ +.avatar { + width: 40px; + height: 40px; + border-radius: 50%; + border: 2px solid rgba(26, 28, 34, 1); +} + +.friend { + margin-left: -10px; + &:first-of-type { + margin-left: 0; + } +} + +.friends_container { + max-width: 100%; + width: 100%; + height: 40px; + overflow: hidden; +} + +.friends_label { + margin-left: 3px; +} + +.friends_list { + overflow-y: scroll; +} + +.friends_list_item:first-of-type { + padding-top: 10px; +} \ No newline at end of file diff --git a/client/src/app/(main)/[username]/profile/ui/friends/friends.tsx b/client/src/app/(main)/[username]/profile/ui/friends/friends.tsx new file mode 100644 index 000000000..e6f29d959 --- /dev/null +++ b/client/src/app/(main)/[username]/profile/ui/friends/friends.tsx @@ -0,0 +1,95 @@ +import { useGetFriends } from '@/entities/session'; +import { Card } from '../card/card'; +import { useParams } from 'next/navigation'; +import { useGetUserByName } from '../../lib/useGetUserByName'; +import { CardSkeleton, Flex, ImageLoader, Typography } from '@/shared/ui'; +import styles from './friends.module.scss'; +import { ArrowRightIcon } from '@/shared/assets'; +import { useState } from 'react'; +import layoutStyles from '../../layout.module.scss'; +import { FriendsModal } from './friends-modal'; +import { IUserBase } from '@teameights/types'; + +export const Friends = () => { + const { username } = useParams(); + const { data: user } = useGetUserByName(username as string); + const { data: friends } = useGetFriends(user!.id); + const friendshipList = friends?.data; + const [isFriendsModalOpen, setFriendsModal] = useState(false); + + if (!friends || !friendshipList) { + return ; + } + + let friendsContainer = ( + + List is empty. + + ); + if (friendshipList.length) { + const friendsList = friendshipList.reduce((accumulator, friendship) => { + if (friendship.status === 'accepted') { + const { receiver, creator } = friendship; + const friend = receiver.id !== user?.id ? receiver : creator; + + const existingFriendIndex = accumulator.findIndex(item => item?.id === friend.id); + // filter out duplicates, since there can be two similar friendships with different creators + if (existingFriendIndex === -1) { + accumulator.push(friend); + } + } + return accumulator; + }, []); + const noun = friendsList.length === 1 ? 'friend' : 'friends'; + friendsContainer = ( + + + + + {friendsList.length} + {noun} + + + + + {friendsList.slice(0, 8).map(friend => ( + + + + ))} + + + ); + } + return ( + + + + Friends + + {friendsContainer} + + + ); +}; diff --git a/client/src/app/(main)/[username]/profile/ui/header/header.module.scss b/client/src/app/(main)/[username]/profile/ui/header/header.module.scss new file mode 100644 index 000000000..d56cc64f2 --- /dev/null +++ b/client/src/app/(main)/[username]/profile/ui/header/header.module.scss @@ -0,0 +1,28 @@ +.container { + width: 100%; + overflow: hidden; + border-radius: 15px; + background: rgba(67, 71, 82); + flex-shrink: 0; +} + +.background { + height: 130px; +} + +.header { + background: rgba(26, 28, 34, 1); + padding: 24px 32px 32px 32px; + min-height: 118px; + gap: 30px; + + .interactable { + align-self: start; + } +} +.profile { + margin-top: -44px; + display: flex; + align-items: flex-end; + gap: 24px; +} diff --git a/client/src/app/(main)/[username]/profile/ui/header/header.tsx b/client/src/app/(main)/[username]/profile/ui/header/header.tsx new file mode 100644 index 000000000..7a4546a84 --- /dev/null +++ b/client/src/app/(main)/[username]/profile/ui/header/header.tsx @@ -0,0 +1,75 @@ +'use client'; +import styles from './header.module.scss'; +import { useGetMe } from '@/entities/session'; +import { ChatCircleDotsIcon, PlusIcon } from '@/shared/assets'; +import { Button, CardSkeleton, Flex, ImageLoader, Typography } from '@/shared/ui'; +import { useParams } from 'next/navigation'; +import { useGetUserByName } from '../../lib/useGetUserByName'; +import { useContext } from 'react'; +import { ProfileContext } from '@/app/(main)/[username]/profile/lib/profile-context'; +import { FriendButton } from '@/features/friend-button'; +export const Header = () => { + const { username } = useParams(); + const { data: me } = useGetMe(); + const { data: user } = useGetUserByName(username as string); + const isMyProfile = useContext(ProfileContext); + + if (!user) { + return ; + } + + let interactions = ( + + ); + + if (!isMyProfile) { + interactions = ( + + + + + ); + } + + // Prohibit any interactions with a profile if a user is not logged in + if (!me) { + interactions = <>; + } + + const name = user.username ? '@' + user.username : ''; + return ( +
+
+
+ +
+
+ +
+ +
{user.fullName}
+ + {name} + +
+
+ {interactions} +
+
+
+ ); +}; diff --git a/client/src/app/(main)/[username]/profile/ui/list/list.module.scss b/client/src/app/(main)/[username]/profile/ui/list/list.module.scss new file mode 100644 index 000000000..e69de29bb diff --git a/client/src/app/(main)/[username]/profile/ui/list/list.tsx b/client/src/app/(main)/[username]/profile/ui/list/list.tsx new file mode 100644 index 000000000..3a3ee6db7 --- /dev/null +++ b/client/src/app/(main)/[username]/profile/ui/list/list.tsx @@ -0,0 +1,30 @@ +import { Card } from '../card/card'; +import { Star, Cake, MapPin, UserIcon } from '@/shared/assets'; +// import { Row } from '@/app/(main)/user/[username]/profile/ui/row'; +import { Row } from '../row/row'; +import { Flex } from '@/shared/ui'; +import { calculateAge } from '@/shared/lib'; +import { useParams } from 'next/navigation'; +import styles from '../../layout.module.scss'; +import { useGetUserByName } from '../../lib/useGetUserByName'; + +export const List = () => { + const { username } = useParams(); + const { data: user } = useGetUserByName(username as string); + + let age = ''; + if (user?.dateOfBirth) { + age = calculateAge(user.dateOfBirth).toString(); + } + + return ( + + + } text={user?.skills?.speciality ?? ''} /> + } text={user?.experience ?? ''} /> + } text={user?.country ?? ''} /> + {age && } text={`${age} years old`} />} + + + ); +}; diff --git a/client/src/app/(main)/[username]/profile/ui/row/row.module.scss b/client/src/app/(main)/[username]/profile/ui/row/row.module.scss new file mode 100644 index 000000000..e69de29bb diff --git a/client/src/app/(main)/[username]/profile/ui/row/row.tsx b/client/src/app/(main)/[username]/profile/ui/row/row.tsx new file mode 100644 index 000000000..ae85d2513 --- /dev/null +++ b/client/src/app/(main)/[username]/profile/ui/row/row.tsx @@ -0,0 +1,16 @@ +import { ReactNode } from 'react'; +import { Flex, Typography } from '@/shared/ui'; + +interface RowProps { + icon: ReactNode; + text: string; +} + +export const Row = ({ icon, text }: RowProps) => { + return ( + +
{icon}
+ {text} +
+ ); +}; diff --git a/client/src/app/(main)/layout.module.scss b/client/src/app/(main)/layout.module.scss index 5544aa3a0..8c85114ef 100644 --- a/client/src/app/(main)/layout.module.scss +++ b/client/src/app/(main)/layout.module.scss @@ -1,29 +1,34 @@ .container { height: 100dvh; width: 100%; - padding: 48px 0; - - @media (width <= 1120px) { - padding: 48px 0; - } - - @media (width <= 580px) { - padding: 28px; - } + + display: flex; } .children { width: 100%; min-height: 100%; display: flex; - align-items: center; flex-direction: column; + + padding: 48px 55px; + @media (width <= 1120px) { + padding: 48px 24px; + } + + @media (width <= 580px) { + padding: 24px; + } } .content_zone { - padding-left: 88px; + margin-bottom: 48px; +} - @media screen and (max-width: 768px) { - padding-left: 0; +.placeholder { + width: 93px; + flex-shrink: 0; + @media (max-width: 768px) { + display: none; } -} +} \ No newline at end of file diff --git a/client/src/app/(main)/layout.tsx b/client/src/app/(main)/layout.tsx index f487f0259..d3594de7a 100644 --- a/client/src/app/(main)/layout.tsx +++ b/client/src/app/(main)/layout.tsx @@ -16,6 +16,7 @@ export default function AuthLayout({ children }: { children: ReactNode }) { return (
+
{children}
); diff --git a/client/src/app/(main)/page.tsx b/client/src/app/(main)/page.tsx index e2bf0300d..0210a8c94 100644 --- a/client/src/app/(main)/page.tsx +++ b/client/src/app/(main)/page.tsx @@ -49,6 +49,7 @@ export default function Home() { type: 'checkbox', placeholder: 'Search by countries', optionsArr: countries, + oneItemName: 'country', filterValue: [], }, { @@ -57,6 +58,7 @@ export default function Home() { type: 'checkbox', placeholder: 'Search by specialty', optionsArr: specialities, + oneItemName: 'speciality', filterValue: [], }, { @@ -65,6 +67,7 @@ export default function Home() { type: 'checkbox', placeholder: 'Search by focus', optionsArr: focusesValues, + oneItemName: 'focus', filterValue: [], }, ]} diff --git a/client/src/app/(main)/ui/cards/cards.module.scss b/client/src/app/(main)/ui/cards/cards.module.scss index e45207e25..992bcb8b5 100644 --- a/client/src/app/(main)/ui/cards/cards.module.scss +++ b/client/src/app/(main)/ui/cards/cards.module.scss @@ -1,9 +1,9 @@ .cards_zone { - padding-left: 88px; - - @media screen and (max-width: 768px) { - padding-left: 0; - } + //padding-left: 88px; + // + //@media screen and (max-width: 768px) { + // padding-left: 0; + //} } .cards { diff --git a/client/src/entities/session/api/index.ts b/client/src/entities/session/api/index.ts index 839cf9753..29e99a91c 100644 --- a/client/src/entities/session/api/index.ts +++ b/client/src/entities/session/api/index.ts @@ -1,3 +1,4 @@ +export { useGetFriends } from './useGetFriends'; /* Here will be imports for session hooks */ export { useConfirmEmail } from './useConfirmEmail'; export { useForgotPassword } from './useForgotPassword'; diff --git a/client/src/entities/session/api/useAddFriend.tsx b/client/src/entities/session/api/useAddFriend.tsx new file mode 100644 index 000000000..69448c90a --- /dev/null +++ b/client/src/entities/session/api/useAddFriend.tsx @@ -0,0 +1,22 @@ +import { useMutation, useQueryClient } from '@tanstack/react-query'; +import { API } from '@/shared/api'; +import { API_FRIENDSHIP } from '@/shared/constant'; +import { toast } from 'sonner'; + +export const useAddFriend = (userId: number | undefined, receiverId: number) => { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: async () => + await API.post(`${API_FRIENDSHIP}/${receiverId}?user={"id":"${userId}"}`), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['useGetFriends'] }); + queryClient.invalidateQueries({ queryKey: ['useGetFriendshipStatus', receiverId] }); + + toast('Request is sent'); + }, + onError: err => { + console.log(err); + toast(`Error occurred: ${err}`); + }, + }); +}; diff --git a/client/src/entities/session/api/useGetFriends.tsx b/client/src/entities/session/api/useGetFriends.tsx new file mode 100644 index 000000000..b60c92fec --- /dev/null +++ b/client/src/entities/session/api/useGetFriends.tsx @@ -0,0 +1,26 @@ +'use client'; +import { useQuery } from '@tanstack/react-query'; +import { API } from '@/shared/api'; +import { API_FRIENDSHIP } from '@/shared/constant'; +import { IUserBase, Timestamps } from '@teameights/types'; + +interface IFriendshipResponse extends Timestamps { + id: number; + status: 'accepted' | 'rejected' | 'pending'; + creator: IUserBase; + receiver: IUserBase; +} + +export const useGetFriends = (userId: number) => { + return useQuery({ + queryKey: ['useGetFriends', userId], + queryFn: async () => { + const { data } = await API.get<{ data: Array }>( + `${API_FRIENDSHIP}/${userId}` + ); + return data; + }, + refetchOnMount: false, + refetchOnWindowFocus: false, + }); +}; diff --git a/client/src/entities/session/api/useGetFriendshipStatus.tsx b/client/src/entities/session/api/useGetFriendshipStatus.tsx new file mode 100644 index 000000000..f2131c8bd --- /dev/null +++ b/client/src/entities/session/api/useGetFriendshipStatus.tsx @@ -0,0 +1,22 @@ +'use client'; +import { useQuery } from '@tanstack/react-query'; +import { API } from '@/shared/api'; +import { API_FRIENDSHIP } from '@/shared/constant'; + +interface IFriendshipStatus { + status: 'none' | 'friends' | 'requested' | 'toRespond'; +} + +export const useGetFriendshipStatus = (id: number) => { + return useQuery({ + queryKey: ['useGetFriendshipStatus', id], + queryFn: async () => { + const { data } = await API.get(`${API_FRIENDSHIP}/status/${id}`); + return data; + }, + refetchOnMount: false, + refetchOnWindowFocus: false, + retry: 1, + retryDelay: 5000, + }); +}; diff --git a/client/src/entities/session/api/useHandleFriendshipRequest.tsx b/client/src/entities/session/api/useHandleFriendshipRequest.tsx new file mode 100644 index 000000000..f5abeceac --- /dev/null +++ b/client/src/entities/session/api/useHandleFriendshipRequest.tsx @@ -0,0 +1,30 @@ +import { useMutation, useQueryClient } from '@tanstack/react-query'; +import { API } from '@/shared/api'; +import { API_FRIENDSHIP } from '@/shared/constant'; +import { toast } from 'sonner'; + +export const useHandleFriendshipRequest = ( + userId: number | undefined, + receiverId: number, + status: 'rejected' | 'accepted' +) => { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: async () => + await API.patch(`${API_FRIENDSHIP}/${receiverId}?user={"id":"${userId}"}`, { + status, + }), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['useGetFriends'] }); + queryClient.invalidateQueries({ queryKey: ['useGetFriendshipStatus', receiverId] }); + if (status === 'accepted') { + toast('New friend added'); + } else { + toast('Friend request declined'); + } + }, + onError: err => { + toast(`Error occurred: ${err}`); + }, + }); +}; diff --git a/client/src/entities/session/api/useRemoveFriend.tsx b/client/src/entities/session/api/useRemoveFriend.tsx new file mode 100644 index 000000000..8e9ab6aa6 --- /dev/null +++ b/client/src/entities/session/api/useRemoveFriend.tsx @@ -0,0 +1,19 @@ +import { useMutation, useQueryClient } from '@tanstack/react-query'; +import { API } from '@/shared/api'; +import { API_FRIENDSHIP } from '@/shared/constant'; +import { toast } from 'sonner'; + +export const useRemoveFriend = (userId: number) => { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: async () => await API.delete(`${API_FRIENDSHIP}/${userId}`), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['useGetFriends'] }); + queryClient.invalidateQueries({ queryKey: ['useGetFriendshipStatus', userId] }); + toast('User is removed from the friends list'); + }, + onError: err => { + toast(`Error occurred: ${err}`); + }, + }); +}; diff --git a/client/src/features/friend-button/friend-button.tsx b/client/src/features/friend-button/friend-button.tsx new file mode 100644 index 000000000..b366a359b --- /dev/null +++ b/client/src/features/friend-button/friend-button.tsx @@ -0,0 +1,79 @@ +import { useAddFriend } from '@/entities/session/api/useAddFriend'; +import { useRemoveFriend } from '@/entities/session/api/useRemoveFriend'; +import { Button } from '@/shared/ui'; +import { UserPlusIcon } from '@/shared/assets'; +import { useHandleFriendshipRequest } from '@/entities/session/api/useHandleFriendshipRequest'; +import { useGetFriendshipStatus } from '@/entities/session/api/useGetFriendshipStatus'; + +interface FriendButtonProps { + myId?: number; + userId: number; + short?: boolean; + size?: 'm' | 'l' | 's'; + width?: string; +} + +function getText(text: string, short: boolean) { + if (short) return text; + return text + ' friend'; +} + +export const FriendButton = ({ + myId, + userId, + short = false, + size = 'm', + width, +}: FriendButtonProps) => { + const { mutate: addFriend } = useAddFriend(myId, userId); + const { mutate: removeFriend } = useRemoveFriend(userId); + const { mutate: declineFriend } = useHandleFriendshipRequest(myId, userId, 'rejected'); + const { mutate: acceptFriend } = useHandleFriendshipRequest(myId, userId, 'accepted'); + const isMyProfile = myId === userId; + const { data } = useGetFriendshipStatus(userId); + + const friendStatus = data?.status; + + if (!myId || isMyProfile) { + return null; // Hide friend button if user not logged in or it's their profile + } + + switch (friendStatus) { + case 'none': { + return ( + + ); + } + case 'requested': { + return ( + + ); + } + case 'toRespond': { + return ( + <> + + + + ); + } + case 'friends': { + return ( + + ); + } + default: + return null; + } +}; diff --git a/client/src/features/friend-button/index.ts b/client/src/features/friend-button/index.ts new file mode 100644 index 000000000..d1e76b3aa --- /dev/null +++ b/client/src/features/friend-button/index.ts @@ -0,0 +1 @@ +export { FriendButton } from './friend-button'; diff --git a/client/src/shared/assets/icons/cake.tsx b/client/src/shared/assets/icons/cake.tsx new file mode 100644 index 000000000..eb9bd5067 --- /dev/null +++ b/client/src/shared/assets/icons/cake.tsx @@ -0,0 +1,51 @@ +import { SVGPropsWithSize } from '@/shared/types/svg-props-with-size'; +import { FC } from 'react'; + +export const Cake: FC = ({ size = '24', ...rest }) => { + return ( + + + + + + + + ); +}; diff --git a/client/src/shared/assets/icons/index.ts b/client/src/shared/assets/icons/index.ts index 251e25121..5777ed8ca 100644 --- a/client/src/shared/assets/icons/index.ts +++ b/client/src/shared/assets/icons/index.ts @@ -1,3 +1,6 @@ +export { Star } from './star'; +export { Cake } from './cake'; +export { MapPin } from './map-pin'; export { CheckIcon } from './check'; export { CrossIcon } from './cross'; export { EyeIcon } from './eye'; diff --git a/client/src/shared/assets/icons/map-pin.tsx b/client/src/shared/assets/icons/map-pin.tsx new file mode 100644 index 000000000..cc98d2275 --- /dev/null +++ b/client/src/shared/assets/icons/map-pin.tsx @@ -0,0 +1,20 @@ +import { SVGPropsWithSize } from '@/shared/types/svg-props-with-size'; +import { FC } from 'react'; + +export const MapPin: FC = ({ size = '24', ...rest }) => { + return ( + + + + ); +}; diff --git a/client/src/shared/assets/icons/star.tsx b/client/src/shared/assets/icons/star.tsx new file mode 100644 index 000000000..ab3877b7a --- /dev/null +++ b/client/src/shared/assets/icons/star.tsx @@ -0,0 +1,20 @@ +import { SVGPropsWithSize } from '@/shared/types/svg-props-with-size'; +import { FC } from 'react'; + +export const Star: FC = ({ size = '24', ...rest }) => { + return ( + + + + ); +}; diff --git a/client/src/shared/constant/server-routes.ts b/client/src/shared/constant/server-routes.ts index 72e465222..2a706c437 100644 --- a/client/src/shared/constant/server-routes.ts +++ b/client/src/shared/constant/server-routes.ts @@ -5,6 +5,7 @@ export const API_EMAIL_CONFIRM = '/auth/email/confirm'; export const API_FORGOT_PASSWORD = '/auth/forgot/password'; export const API_RESET_PASSWORD = '/auth/reset/password'; export const API_ME = '/auth/me'; +export const API_FRIENDSHIP = '/friendship'; export const API_REFRESH = '/auth/refresh'; export const API_LOGOUT = '/auth/logout'; export const API_GOOGLE_LOGIN = '/auth/google/login'; diff --git a/client/src/shared/ui/checkbox/checkbox.module.scss b/client/src/shared/ui/checkbox/checkbox.module.scss index 6ab24d13c..3ca8c88f0 100644 --- a/client/src/shared/ui/checkbox/checkbox.module.scss +++ b/client/src/shared/ui/checkbox/checkbox.module.scss @@ -12,6 +12,10 @@ } .label { + width: calc(100% - 26px); + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; color: var(--white-color); font: var(--font-body-weight-m) var(--font-body-size-m) / var(--font-body-line-m) var(--font-rubik); diff --git a/client/src/shared/ui/drawer/drawer.tsx b/client/src/shared/ui/drawer/drawer.tsx index 45ad98ca0..01dcd3fad 100644 --- a/client/src/shared/ui/drawer/drawer.tsx +++ b/client/src/shared/ui/drawer/drawer.tsx @@ -37,6 +37,7 @@ import React, { FC, PropsWithChildren } from 'react'; import DrawerComponent from 'react-modern-drawer'; import 'react-modern-drawer/dist/index.css'; +import { Portal } from '../portal'; interface DrawerProps { open: boolean; @@ -65,18 +66,20 @@ export const Drawer: FC> = props => { } as React.CSSProperties; return ( - - {children} - + + + {children} + + ); }; diff --git a/client/src/shared/ui/image-loader/image-loader.module.scss b/client/src/shared/ui/image-loader/image-loader.module.scss index 4863c1bbd..0e23a7804 100644 --- a/client/src/shared/ui/image-loader/image-loader.module.scss +++ b/client/src/shared/ui/image-loader/image-loader.module.scss @@ -1,7 +1,7 @@ .crown_container { position: absolute; transform: rotate(30deg) translateX(-15%) translateY(-255%); - z-index: 1000; + z-index: 1; svg { width: 100%; diff --git a/client/src/shared/ui/search-bar/actions/actions.ts b/client/src/shared/ui/search-bar/actions/actions.ts new file mode 100644 index 000000000..728c19a6f --- /dev/null +++ b/client/src/shared/ui/search-bar/actions/actions.ts @@ -0,0 +1,38 @@ +import { MultiValue } from 'react-select'; +import { Action, ActionTypesEnum } from './types'; +import { IOptionItem } from '../types'; + +export const changeFilterValue = ( + filterIndex: number, + newValue: string | MultiValue | [number, number] | null +): Action => ({ + type: ActionTypesEnum.CHANGE_FILTER_VALUE, + payload: { + filterIndex, + newValue, + }, +}); + +export const clearOneMultipleOption = ( + filterIndex: number, + optionIndex: number +): Action => ({ + type: ActionTypesEnum.CLEAR_ONE_MULTIPLE_OPTION, + payload: { filterIndex, optionIndex }, +}); + +export const clearAllExceptOneMultipleOptions = ( + filterIndex: number +): Action => ({ + type: ActionTypesEnum.CLEAR_ALL_EXCEPT_ONE_MULTIPLE_OPTIONS, + payload: filterIndex, +}); + +export const clearFilter = (filterIndex: number): Action => ({ + type: ActionTypesEnum.CLEAR_FILTER, + payload: filterIndex, +}); + +export const clearAllFilters = (): Action => ({ + type: ActionTypesEnum.CLEAR_ALL_FILTERS, +}); diff --git a/client/src/shared/ui/search-bar/actions/index.ts b/client/src/shared/ui/search-bar/actions/index.ts new file mode 100644 index 000000000..b2660c547 --- /dev/null +++ b/client/src/shared/ui/search-bar/actions/index.ts @@ -0,0 +1,9 @@ +export { + changeFilterValue, + clearOneMultipleOption, + clearAllExceptOneMultipleOptions, + clearFilter, + clearAllFilters, +} from './actions'; +export type { Action } from './types'; +export { ActionTypesEnum } from './types'; diff --git a/client/src/shared/ui/search-bar/actions/types.ts b/client/src/shared/ui/search-bar/actions/types.ts new file mode 100644 index 000000000..54f61a0d5 --- /dev/null +++ b/client/src/shared/ui/search-bar/actions/types.ts @@ -0,0 +1,39 @@ +import { MultiValue } from 'react-select'; +import { IOptionItem } from '../types'; + +export enum ActionTypesEnum { + CHANGE_FILTER_VALUE = 'CHANGE_FILTER_VALUE', + CLEAR_ONE_MULTIPLE_OPTION = 'CLEAR_ONE_MULTIPLE_OPTION', + CLEAR_ALL_EXCEPT_ONE_MULTIPLE_OPTIONS = 'CLEAR_ALL_EXCEPT_ONE_MULTIPLE_OPTIONS', + CLEAR_FILTER = 'CLEAR_FILTER', + CLEAR_ALL_FILTERS = 'CLEAR_ALL_FILTERS', +} + +type ActionsWithoutPayload = ActionTypesEnum.CLEAR_ALL_FILTERS; + +interface ActionWithoutPayload { + type: ActionsWithoutPayload; +} + +export interface ActionWithPayload { + type: T; + payload: ActionsPayloads[T]; +} + +interface ActionsPayloads { + [ActionTypesEnum.CHANGE_FILTER_VALUE]: { + filterIndex: number; + newValue: string | MultiValue | [number, number] | null; + }; + [ActionTypesEnum.CLEAR_ONE_MULTIPLE_OPTION]: { + filterIndex: number; + optionIndex: number; + }; + [ActionTypesEnum.CLEAR_ALL_EXCEPT_ONE_MULTIPLE_OPTIONS]: number; + [ActionTypesEnum.CLEAR_FILTER]: number; + [ActionTypesEnum.CLEAR_ALL_FILTERS]: never; +} + +export type Action = T extends ActionsWithoutPayload + ? ActionWithoutPayload + : ActionWithPayload; diff --git a/client/src/shared/ui/search-bar/hooks/index.ts b/client/src/shared/ui/search-bar/hooks/index.ts index 20b654d16..93645ce21 100644 --- a/client/src/shared/ui/search-bar/hooks/index.ts +++ b/client/src/shared/ui/search-bar/hooks/index.ts @@ -1,2 +1,3 @@ export { useTrackFilterArr } from './useTrackFiltersArr'; export { useFilters } from './useFilters'; +export { useFilterReducer } from './useFilterReducer'; diff --git a/client/src/shared/ui/search-bar/hooks/useFilterReducer.ts b/client/src/shared/ui/search-bar/hooks/useFilterReducer.ts new file mode 100644 index 000000000..a769d79b0 --- /dev/null +++ b/client/src/shared/ui/search-bar/hooks/useFilterReducer.ts @@ -0,0 +1,132 @@ +import { useReducer } from 'react'; +import { Filter, IFilterState } from '../types'; +import { ActionTypesEnum, Action } from '../actions'; + +const reducer = (state: IFilterState, action: Action) => { + switch (action.type) { + case ActionTypesEnum.CLEAR_ONE_MULTIPLE_OPTION: { + const { filterIndex, optionIndex } = action.payload; + const filter = state.filterArr[filterIndex]; + + if (filter.type === 'checkbox' || filter.type === 'multiple') { + const newFilterValue = filter.filterValue.filter((item, i) => i !== optionIndex); + + return { + isTimerDisabled: false, + filterArr: state.filterArr.map((item, i) => { + if (filterIndex === i) { + item.filterValue = newFilterValue; + } + + return item; + }), + }; + } + + return state; + } + + case ActionTypesEnum.CLEAR_ALL_EXCEPT_ONE_MULTIPLE_OPTIONS: { + const filterIndex = action.payload; + const filter = state.filterArr[filterIndex]; + + if (filter.type === 'checkbox' || filter.type === 'multiple') { + const newFilterValue = [filter.filterValue[0]]; + + return { + isTimerDisabled: true, + filterArr: state.filterArr.map((item, i) => { + if (filterIndex === i) { + item.filterValue = newFilterValue; + } + + return item; + }), + }; + } + + return state; + } + + case ActionTypesEnum.CHANGE_FILTER_VALUE: { + const { filterIndex, newValue } = action.payload; + + return { + isTimerDisabled: false, + filterArr: state.filterArr.map((item, i) => { + if (filterIndex === i) { + item.filterValue = newValue; + } + + return item; + }), + }; + } + + case ActionTypesEnum.CLEAR_FILTER: { + const filterIndex = action.payload; + + return { + isTimerDisabled: false, + filterArr: state.filterArr.map((item, index) => { + if (index === filterIndex) { + switch (item.type) { + case 'text': + item.filterValue = ''; + + return item; + + case 'multiple': + case 'checkbox': + item.filterValue = []; + + return item; + + case 'range': + item.filterValue = null; + + return item; + } + } + + return item; + }), + }; + } + + case ActionTypesEnum.CLEAR_ALL_FILTERS: { + return { + isTimerDisabled: false, + filterArr: state.filterArr.map(item => { + switch (item.type) { + case 'text': + item.filterValue = ''; + + return item; + + case 'multiple': + case 'checkbox': + item.filterValue = []; + + return item; + + case 'range': + item.filterValue = null; + + return item; + } + }), + }; + } + + default: + return state; + } +}; + +export const useFilterReducer = (initialState: Filter[]) => { + return useReducer(reducer, { + isTimerDisabled: false, + filterArr: initialState, + }); +}; diff --git a/client/src/shared/ui/search-bar/hooks/useTrackFiltersArr.ts b/client/src/shared/ui/search-bar/hooks/useTrackFiltersArr.ts index 2c5400c0e..20dbb8e49 100644 --- a/client/src/shared/ui/search-bar/hooks/useTrackFiltersArr.ts +++ b/client/src/shared/ui/search-bar/hooks/useTrackFiltersArr.ts @@ -1,46 +1,55 @@ import { useEffect, useRef } from 'react'; import { Filter, IFilterParams } from '../types'; -export const useTrackFilterArr = ( - filterArr: Filter[], - onChange: (filterValues: string | null) => void -) => { - const timerRef = useRef | null>(null); - useEffect(() => { - if (timerRef.current) { - clearTimeout(timerRef.current); - } +const getFilterValues = (filterArr: Filter[]) => + filterArr.reduce<{ + [key: string]: string | string[] | [number, number]; + }>((acc, { type, value, filterValue }) => { + switch (type) { + case 'text': + if (filterValue.length) { + acc[value] = filterValue; + } - timerRef.current = setTimeout(() => { - const filterValues: IFilterParams = filterArr.reduce<{ - [key: string]: string | string[] | [number, number]; - }>((acc, curr) => { - switch (curr.type) { - case 'text': - if (curr.filterValue.length) { - acc[curr.value] = curr.filterValue; - } + return acc; + + case 'multiple': + case 'checkbox': + if (filterValue.length) { + acc[value] = filterValue.map(item => item.label); + } - return acc; + return acc; - case 'multiple': - case 'checkbox': - if (curr.filterValue.length) { - acc[curr.value] = curr.filterValue.map(item => item.label); - } + case 'range': + if (filterValue?.length) { + acc[value] = filterValue; + } - return acc; + return acc; + } + }, {}); - case 'range': - if (curr.filterValue?.length) { - acc[curr.value] = curr.filterValue; - } +export const useTrackFilterArr = ( + { isTimerDisabled, filterArr }: { isTimerDisabled: boolean; filterArr: Filter[] }, + onChange: (filterValues: string | null) => void +) => { + const filterValues: IFilterParams = getFilterValues(filterArr); - return acc; - } - }, {}); + const timerRef = useRef | null>(null); + useEffect(() => { + if (isTimerDisabled) { onChange(Object.keys(filterValues).length ? JSON.stringify(filterValues) : null); - }, 500); - }, [filterArr, onChange]); + } else { + if (timerRef.current) { + clearTimeout(timerRef.current); + } + + timerRef.current = setTimeout( + () => onChange(Object.keys(filterValues).length ? JSON.stringify(filterValues) : null), + 500 + ); + } + }, [isTimerDisabled, filterArr, onChange, filterValues]); }; diff --git a/client/src/shared/ui/search-bar/search-bar.module.scss b/client/src/shared/ui/search-bar/search-bar.module.scss index 75bd9131d..a20955ed8 100644 --- a/client/src/shared/ui/search-bar/search-bar.module.scss +++ b/client/src/shared/ui/search-bar/search-bar.module.scss @@ -25,4 +25,10 @@ @media screen and (max-width: 900px) { max-width: 546px; } + + @media (max-width: 768px) { + &_hidden { + display: none; + } + } } diff --git a/client/src/shared/ui/search-bar/search-bar.stories.tsx b/client/src/shared/ui/search-bar/search-bar.stories.tsx index 3aae5a49a..76251b6fc 100644 --- a/client/src/shared/ui/search-bar/search-bar.stories.tsx +++ b/client/src/shared/ui/search-bar/search-bar.stories.tsx @@ -32,13 +32,14 @@ export const SearchBar_default = () => { { label: 'Ukraine', value: 'ua' }, { label: 'Korea', value: 'kr' }, ], + oneItemName: 'country', filterValue: [], }, { - label: 'Specialty', - value: 'specialty', + label: 'Specialities', + value: 'specialities', type: 'multiple', - placeholder: 'Search by specialty', + placeholder: 'Search by speciality', optionsArr: [ { label: 'Mobile Developer', @@ -57,6 +58,7 @@ export const SearchBar_default = () => { value: 'fullstack', }, ], + oneItemName: 'speciality', filterValue: [], }, ]} diff --git a/client/src/shared/ui/search-bar/search-bar.tsx b/client/src/shared/ui/search-bar/search-bar.tsx index 0019d3bb0..2c4fbfe7a 100644 --- a/client/src/shared/ui/search-bar/search-bar.tsx +++ b/client/src/shared/ui/search-bar/search-bar.tsx @@ -7,10 +7,11 @@ import { FilterSelect } from './ui/filter-select'; import { SearchInput } from './ui/search-input'; import { TagList } from './ui/tag-list'; import { Flex } from '@/shared/ui'; -import { useTrackFilterArr } from './hooks'; +import { useFilterReducer, useTrackFilterArr } from './hooks'; import { SearchContext } from './contexts'; import { ModalButton } from './ui/modal-button'; import { Modal } from './ui/modal'; +import clsx from 'clsx'; /** * Search-bar Component @@ -45,39 +46,84 @@ interface SearchBarProps { } export const SearchBar: FC = ({ initialFiltersState, onChange }) => { - const [filterArr, setFilterArr] = useState(initialFiltersState); + const [filterState, dispatch] = useFilterReducer(initialFiltersState); const [filterIndex, setFilterIndex] = useState(0); const [isModalOpened, setIsModalOpened] = useState(false); - useTrackFilterArr(filterArr, onChange); + const [isFilterOpened, setIsFilterOpened] = useState(false); + useTrackFilterArr(filterState, onChange); - const onOpen = () => { + const { filterArr } = filterState; + + const onOpenModal = () => { setIsModalOpened(true); }; - const onClose = () => { + const onCloseModal = () => { setIsModalOpened(false); }; + const onOpenFilter = () => { + setIsFilterOpened(true); + }; + + const onCloseFilter = () => { + setIsFilterOpened(false); + }; + + const onOpenModalWithoutFilter = () => { + onCloseFilter(); + onOpenModal(); + }; + + const onOpenModalWithFilter = (value: string) => { + const newFilterIndex = filterArr.findIndex(filter => filter.value === value); + setFilterIndex(newFilterIndex); + + onOpenFilter(); + onOpenModal(); + }; + + const isShowTagList = filterArr.some(item => { + switch (item.type) { + case 'text': + case 'checkbox': + case 'multiple': + return item.filterValue.length; + } + }); + return ( - + - + - + - + ); }; diff --git a/client/src/shared/ui/search-bar/types/types.ts b/client/src/shared/ui/search-bar/types/types.ts index c271993e0..7583eb3c7 100644 --- a/client/src/shared/ui/search-bar/types/types.ts +++ b/client/src/shared/ui/search-bar/types/types.ts @@ -1,5 +1,6 @@ import { Dispatch, SetStateAction } from 'react'; import { MultiValue } from 'react-select'; +import { Action, ActionTypesEnum } from '../actions'; interface IFilter { label: string; @@ -21,12 +22,14 @@ export interface ICheckboxFilter extends IFilter { type: 'checkbox'; optionsArr: IOptionItem[]; filterValue: MultiValue; + oneItemName: string; } export interface IMultipleFilter extends IFilter { type: 'multiple'; optionsArr: IOptionItem[]; filterValue: MultiValue; + oneItemName: string; } export interface IRangeFilter extends IFilter { @@ -44,7 +47,12 @@ export interface IFilterParams { export interface SearchContextType { filterArr: Filter[]; - setFilterArr: Dispatch>; + dispatch: Dispatch>; filterIndex: number; setFilterIndex: Dispatch>; } + +export interface IFilterState { + isTimerDisabled: boolean; + filterArr: Filter[]; +} diff --git a/client/src/shared/ui/search-bar/ui/filter-menu/filter-menu.module.scss b/client/src/shared/ui/search-bar/ui/filter-menu/filter-menu.module.scss index 63c8f2178..e1ef1b0cf 100644 --- a/client/src/shared/ui/search-bar/ui/filter-menu/filter-menu.module.scss +++ b/client/src/shared/ui/search-bar/ui/filter-menu/filter-menu.module.scss @@ -7,7 +7,10 @@ } .menu_wrapper { - height: 100%; - margin-bottom: 10px; - overflow: hidden; + height: calc(100vh - 222px); + overflow: auto; + + &_short { + height: calc(100vh - 271px); + } } diff --git a/client/src/shared/ui/search-bar/ui/filter-menu/filter-menu.tsx b/client/src/shared/ui/search-bar/ui/filter-menu/filter-menu.tsx index fd97f53d3..97661665b 100644 --- a/client/src/shared/ui/search-bar/ui/filter-menu/filter-menu.tsx +++ b/client/src/shared/ui/search-bar/ui/filter-menu/filter-menu.tsx @@ -3,17 +3,36 @@ import { SearchInput } from '../search-input'; import { TagList } from '../tag-list'; import styles from './filter-menu.module.scss'; import { useState } from 'react'; +import { useFilters } from '../../hooks'; +import clsx from 'clsx'; export const FilterMenu = () => { const [menuWrapper, setMenuWrapper] = useState(null); + const { filterArr, filterIndex } = useFilters(); + + const isSelect = + filterArr[filterIndex].type === 'multiple' || filterArr[filterIndex].type === 'checkbox'; + const isFilterNotEmpty = !!( + filterArr[filterIndex].type === 'multiple' || + (filterArr[filterIndex].type === 'checkbox' && filterArr[filterIndex].filterValue?.length) + ); return ( - -
+ + + + {isSelect && ( +
+ )}
); }; diff --git a/client/src/shared/ui/search-bar/ui/modal-button/modal-button.tsx b/client/src/shared/ui/search-bar/ui/modal-button/modal-button.tsx index 1388a1984..32fbb9b0b 100644 --- a/client/src/shared/ui/search-bar/ui/modal-button/modal-button.tsx +++ b/client/src/shared/ui/search-bar/ui/modal-button/modal-button.tsx @@ -1,4 +1,4 @@ -import { FC } from 'react'; +import { FC, useEffect } from 'react'; import { Flex } from '@/shared/ui'; import { SearchIcon } from '@/shared/assets'; import { useGetScreenWidth } from '@/shared/lib'; @@ -12,9 +12,11 @@ interface ModalButtonProps { export const ModalButton: FC = ({ onOpen, onClose }) => { const screenWidth = useGetScreenWidth(); - if (screenWidth > 768) { - onClose(); - } + useEffect(() => { + if (screenWidth > 768) { + onClose(); + } + }, [screenWidth, onClose]); return ( diff --git a/client/src/shared/ui/search-bar/ui/modal/modal.tsx b/client/src/shared/ui/search-bar/ui/modal/modal.tsx index 1d3231f99..260d68773 100644 --- a/client/src/shared/ui/search-bar/ui/modal/modal.tsx +++ b/client/src/shared/ui/search-bar/ui/modal/modal.tsx @@ -1,4 +1,4 @@ -import { FC, useState } from 'react'; +import { FC } from 'react'; import { useFilters } from '../../hooks'; import clsx from 'clsx'; import { CrossIcon } from '@/shared/assets'; @@ -6,77 +6,42 @@ import { Drawer, Flex, Typography } from '@/shared/ui'; import { FilterMenu } from '../filter-menu'; import { ModalMenu } from '../modal-menu'; import styles from './modal.module.scss'; +import { clearAllFilters, clearFilter } from '../../actions'; interface ModalProps { isOpened: boolean; onClose: () => void; + isFilterOpened: boolean; + onOpenFilter: () => void; + onCloseFilter: () => void; } -export const Modal: FC = ({ isOpened, onClose }) => { - const { filterArr, setFilterArr, filterIndex, setFilterIndex } = useFilters(); +export const Modal: FC = ({ + isOpened, + onClose, + isFilterOpened, + onOpenFilter, + onCloseFilter, +}) => { + const { filterArr, dispatch, filterIndex, setFilterIndex } = useFilters(); const currentFilter = filterArr[filterIndex]; - const [isFilterOpened, setIsFilterOpened] = useState(false); const handleOpenFilter = (index: number) => { - setIsFilterOpened(true); + onOpenFilter(); setFilterIndex(index); }; const leftButtonHandler = () => { if (isFilterOpened) { - setFilterArr(prev => - prev.map((item, index) => { - if (index === filterIndex) { - switch (item.type) { - case 'text': - item.filterValue = ''; - - return item; - - case 'multiple': - case 'checkbox': - item.filterValue = []; - - return item; - - case 'range': - item.filterValue = null; - - return item; - } - } - - return item; - }) - ); + dispatch(clearFilter(filterIndex)); } else { - setFilterArr(prev => - prev.map(item => { - switch (item.type) { - case 'text': - item.filterValue = ''; - - return item; - - case 'multiple': - case 'checkbox': - item.filterValue = []; - - return item; - - case 'range': - item.filterValue = null; - - return item; - } - }) - ); + dispatch(clearAllFilters()); } }; const handleRightButtonClick = () => { if (isFilterOpened) { - setIsFilterOpened(false); + onCloseFilter(); } else { onClose(); } diff --git a/client/src/shared/ui/search-bar/ui/search-input/search-input.tsx b/client/src/shared/ui/search-bar/ui/search-input/search-input.tsx index 4620c63ee..f893e62d4 100644 --- a/client/src/shared/ui/search-bar/ui/search-input/search-input.tsx +++ b/client/src/shared/ui/search-bar/ui/search-input/search-input.tsx @@ -4,25 +4,18 @@ import { TextInput } from '../text-input'; import { SearchSelect } from '../search-select'; import { useFilters } from '../../hooks'; import { FC } from 'react'; +import { changeFilterValue } from '../../actions'; interface SearchInputProps { menuWrapper?: HTMLElement | null; } export const SearchInput: FC = ({ menuWrapper }) => { - const { filterArr, setFilterArr, filterIndex } = useFilters(); + const { filterArr, dispatch, filterIndex } = useFilters(); const currentFilter = filterArr[filterIndex]; const onChange = (newValue: string | MultiValue | [number, number] | null) => { - setFilterArr(prev => { - return prev.map((item, i) => { - if (filterIndex === i) { - item.filterValue = newValue; - } - - return item; - }); - }); + dispatch(changeFilterValue(filterIndex, newValue)); }; switch (currentFilter.type) { diff --git a/client/src/shared/ui/search-bar/ui/search-select/search-select.module.scss b/client/src/shared/ui/search-bar/ui/search-select/search-select.module.scss index 85d85ec97..a5c1f1eb0 100644 --- a/client/src/shared/ui/search-bar/ui/search-select/search-select.module.scss +++ b/client/src/shared/ui/search-bar/ui/search-select/search-select.module.scss @@ -8,10 +8,29 @@ } .menu_list { - display: grid; - grid-template-columns: auto auto; - column-gap: 32px; width: calc(100% + 36px); + display: flex; + flex-wrap: wrap; + + .option { + width: calc((100% - 1 * 32px) / 2); + + &:nth-child(2n) { + margin-left: 32px; + } + + @media (max-width: 768px) { + width: 100%; + + &:nth-child(2n) { + margin-left: 0; + } + } + } + + @media (max-width: 768px) { + width: 100%; + } } .search_icon_wrapper { diff --git a/client/src/shared/ui/search-bar/ui/search-select/search-select.tsx b/client/src/shared/ui/search-bar/ui/search-select/search-select.tsx index 75abc63c6..54165a6bd 100644 --- a/client/src/shared/ui/search-bar/ui/search-select/search-select.tsx +++ b/client/src/shared/ui/search-bar/ui/search-select/search-select.tsx @@ -39,6 +39,9 @@ export const SearchSelect: FC = ({ className={styles.select} menuIsOpen={menuWrapper ? true : undefined} styles={{ + control: () => ({ + padding: '8px 11px', + }), menuList: () => ({ padding: '8px 0', ...(menuWrapper ? { boxShadow: 'none' } : {}), @@ -46,21 +49,31 @@ export const SearchSelect: FC = ({ ...(menuWrapper ? { menu: () => ({ - background: 'var(--grey-dark-color)', maxHeight: 'none', height: '100%', + paddingTop: 0, + boxShadow: 'none', + overflow: 'auto', + borderRadius: '5px', position: 'static', }), + menuList: () => ({ + maxHeight: 'none', + borderRadius: '0', + background: 'none', + boxShadow: 'none', + }), menuPortal: () => ({ - height: '100%', - width: 'auto', position: 'static', + width: 'auto', + height: '100%', }), } : {}), }} classNames={{ menuList: () => styles.menu_list, + option: () => styles.option, }} value={value} controlShouldRenderValue={false} diff --git a/client/src/shared/ui/search-bar/ui/search-tag-menu/search-tag-menu.tsx b/client/src/shared/ui/search-bar/ui/search-tag-menu/search-tag-menu.tsx index c0857c922..33cfb6b3a 100644 --- a/client/src/shared/ui/search-bar/ui/search-tag-menu/search-tag-menu.tsx +++ b/client/src/shared/ui/search-bar/ui/search-tag-menu/search-tag-menu.tsx @@ -8,15 +8,15 @@ import { Flex } from '@/shared/ui'; interface SearchTagMenuProps { filterItem: ICheckboxFilter | IMultipleFilter; filterIndex: number; - onClearOption: (filterIndex: number, index: number) => void; - onClearAllOptions: (filterIndex: number) => void; + onClearOneOption: (filterIndex: number, index: number) => void; + onClearAllExceptOneOptions: (filterIndex: number) => void; } export const SearchTagMenu: FC = ({ filterItem, filterIndex, - onClearOption, - onClearAllOptions, + onClearOneOption, + onClearAllExceptOneOptions, }) => { const [isListOpened, setIsListOpened] = useState(false); const filterListRef = useClickOutside(() => setIsListOpened(false)); @@ -26,7 +26,7 @@ export const SearchTagMenu: FC = ({
setIsListOpened(true)}> +{filterItem.filterValue.length - 1}{' '} - {filterItem.filterValue.length > 2 ? 'items' : 'item'} + {filterItem.filterValue.length > 2 ? filterItem.value : filterItem.oneItemName}
{isListOpened ? ( @@ -36,7 +36,7 @@ export const SearchTagMenu: FC = ({ onClearOption(filterIndex, index + 1)} + onClick={() => onClearOneOption(filterIndex, index + 1)} > {item.label} @@ -48,7 +48,7 @@ export const SearchTagMenu: FC = ({ align='center' height='fit-content' padding='4px 8px' - onClick={() => onClearAllOptions(filterIndex)} + onClick={() => onClearAllExceptOneOptions(filterIndex)} className={styles.clear_all_button} >

Clear All

diff --git a/client/src/shared/ui/search-bar/ui/tag-list/tag-list.module.scss b/client/src/shared/ui/search-bar/ui/tag-list/tag-list.module.scss index ed5e62f5d..ac52b7b9c 100644 --- a/client/src/shared/ui/search-bar/ui/tag-list/tag-list.module.scss +++ b/client/src/shared/ui/search-bar/ui/tag-list/tag-list.module.scss @@ -3,15 +3,40 @@ display: flex; gap: 8px; flex-wrap: wrap; + + @media (max-width: 768px) { + overflow: auto; + flex-wrap: nowrap; + } } &_wrapper { border-radius: 5px; overflow: hidden; + min-width: 100px; } } -.checkboxFilterTag { +.checkbox_filter_tag { display: flex; gap: 8px; + + &_mobile { + display: none; + + @media (max-width: 768px) { + display: flex; + gap: 8px; + } + } + + @media (max-width: 768px) { + display: none; + } +} + +.first_tag_option { + @media (max-width: 768px) { + display: none; + } } diff --git a/client/src/shared/ui/search-bar/ui/tag-list/tag-list.tsx b/client/src/shared/ui/search-bar/ui/tag-list/tag-list.tsx index c5ea2a6db..00c8aa225 100644 --- a/client/src/shared/ui/search-bar/ui/tag-list/tag-list.tsx +++ b/client/src/shared/ui/search-bar/ui/tag-list/tag-list.tsx @@ -1,16 +1,27 @@ -import { FC } from 'react'; +import { FC, Fragment } from 'react'; import styles from './tag-list.module.scss'; import { Tag } from '../tag'; import { SearchTagMenu } from '../search-tag-menu'; import { useFilters } from '../../hooks'; import { Filter } from '../../types'; +import { + clearFilter, + clearOneMultipleOption, + clearAllExceptOneMultipleOptions, +} from '../../actions'; interface TagListProps { isOnlyCurrentFilterTags?: boolean; + isFilterMenu?: boolean; + onOpenFilter?: (value: string) => void; } -export const TagList: FC = ({ isOnlyCurrentFilterTags = false }) => { - const { filterArr, setFilterArr, filterIndex } = useFilters(); +export const TagList: FC = ({ + isOnlyCurrentFilterTags = false, + isFilterMenu = false, + onOpenFilter, +}) => { + const { filterArr, dispatch, filterIndex } = useFilters(); const currentFilter = filterArr[filterIndex]; if (!filterArr.length) { @@ -18,55 +29,15 @@ export const TagList: FC = ({ isOnlyCurrentFilterTags = false }) = } const handleClearTextFilter = (filterIndex: number) => { - setFilterArr(prev => - prev.map((item, index) => { - if (filterIndex === index) { - item.filterValue = ''; - } - - return item; - }) - ); + dispatch(clearFilter(filterIndex)); }; - const handleClearMultipleOption = (filterIndex: number, index: number) => { - setFilterArr(prev => { - const filter = prev[filterIndex]; - - if (filter.type === 'checkbox' || filter.type === 'multiple') { - const newFilterValue = filter.filterValue.filter((item, i) => i !== index); - - return prev.map((item, i) => { - if (filterIndex === i) { - item.filterValue = newFilterValue; - } - - return item; - }); - } - - return prev; - }); + const handleClearOneMultipleOption = (filterIndex: number, optionIndex: number) => { + dispatch(clearOneMultipleOption(filterIndex, optionIndex)); }; - const handleClearAllMultipleOptions = (filterIndex: number) => { - setFilterArr(prev => { - const filter = prev[filterIndex]; - - if (filter.type === 'checkbox' || filter.type === 'multiple') { - const newFilterValue = [filter.filterValue[0]]; - - return prev.map((item, i) => { - if (filterIndex === i) { - item.filterValue = newFilterValue; - } - - return item; - }); - } - - return prev; - }); + const handleClearAllExceptOneMultipleOptions = (filterIndex: number) => { + dispatch(clearAllExceptOneMultipleOptions(filterIndex)); }; const renderTag = (filterItem: Filter, index: number) => { @@ -83,20 +54,45 @@ export const TagList: FC = ({ isOnlyCurrentFilterTags = false }) = case 'multiple': case 'checkbox': if (filterItem.filterValue.length) { - return ( -
  • - handleClearMultipleOption(index, 0)}> - {filterItem.filterValue[0].label} - - {filterItem.filterValue.length > 1 && ( - - )} + return isFilterMenu ? ( +
  • + {filterItem.filterValue.map((option, optionIndex) => ( + handleClearOneMultipleOption(index, optionIndex)} + > + {option.label} + + ))}
  • + ) : ( + +
  • + handleClearOneMultipleOption(index, 0)}> + {filterItem.filterValue[0].label} + + + {filterItem.filterValue.length > 1 && ( + + )} +
  • + +
  • onOpenFilter(filterItem.value) : undefined} + className={styles.checkbox_filter_tag_mobile} + key={filterItem.value} + > + + {filterItem.filterValue.length} {filterItem.value} + +
  • +
    ); } diff --git a/client/src/shared/ui/search-bar/ui/tag/tag.tsx b/client/src/shared/ui/search-bar/ui/tag/tag.tsx index d8f5886e2..1547fed14 100644 --- a/client/src/shared/ui/search-bar/ui/tag/tag.tsx +++ b/client/src/shared/ui/search-bar/ui/tag/tag.tsx @@ -8,6 +8,7 @@ interface TagProps { isWithCross?: boolean; isFilledWhileHover?: boolean; isRounded?: boolean; + className?: string; onClick?: () => void; } @@ -16,15 +17,20 @@ export const Tag: FC> = ({ isWithCross = false, isFilledWhileHover = false, isRounded = true, + className, children, }) => { return ( = ({ defaultValue, placeholder, onCha return ( handleChange(e.target.value)} placeholder={placeholder} diff --git a/client/src/shared/ui/skeleton/skeleton.tsx b/client/src/shared/ui/skeleton/skeleton.tsx index 209328a1d..e0fb0385f 100644 --- a/client/src/shared/ui/skeleton/skeleton.tsx +++ b/client/src/shared/ui/skeleton/skeleton.tsx @@ -4,7 +4,10 @@ import 'react-loading-skeleton/dist/skeleton.css'; import { Flex } from '@/shared/ui'; interface CardSkeletonProps { - cards: number; + cards?: number; + height?: number | string; + width?: number | string; + borderRadius?: number; } function Box({ children }: PropsWithChildren) { @@ -15,7 +18,7 @@ function Box({ children }: PropsWithChildren) { ); } -export const CardSkeleton: FC = ({ cards }) => { +export const CardSkeleton: FC = ({ cards, borderRadius, height, width }) => { return Array(cards) .fill(0) .map((item, i) => ( @@ -24,9 +27,9 @@ export const CardSkeleton: FC = ({ cards }) => { wrapper={Box} baseColor='#313131' highlightColor='#525252' - width={230} - height={280} - borderRadius={15} + width={width ?? 230} + height={height ?? 280} + borderRadius={borderRadius ?? 15} /> )); }; diff --git a/client/src/widgets/modals/info-modal/user/desktop/desktop.tsx b/client/src/widgets/modals/info-modal/user/desktop/desktop.tsx index 377d8d71a..103aaf5d5 100644 --- a/client/src/widgets/modals/info-modal/user/desktop/desktop.tsx +++ b/client/src/widgets/modals/info-modal/user/desktop/desktop.tsx @@ -1,13 +1,16 @@ import { Button, Modal, Typography, Flex, ImageLoader } from '@/shared/ui'; import { FC } from 'react'; -import { ArrowRightIcon, UserPlusIcon, ChatCircleDotsIcon } from '@/shared/assets'; +import { ArrowRightIcon, ChatCircleDotsIcon } from '@/shared/assets'; import { calculateAge, getCountryFlag } from '@/shared/lib'; import { InfoModalUserProps } from '../interfaces'; import { IconLayout } from '../ui/icon-layout/icon-layout'; import { TextLayout } from '../ui/text-layout/text-layout'; +import { FriendButton } from '@/features/friend-button'; +import { useGetMe } from '@/entities/session'; export const UserDesktop: FC = ({ user, isOpenModal, handleClose }) => { const age = user?.dateOfBirth ? calculateAge(user.dateOfBirth) : null; + const { data: me } = useGetMe(); return ( <> @@ -53,24 +56,19 @@ export const UserDesktop: FC = ({ user, isOpenModal, handleC - - { - - } - + + {user?.id && } - - + + + diff --git a/client/src/widgets/modals/info-modal/user/phone/phone.tsx b/client/src/widgets/modals/info-modal/user/phone/phone.tsx index 3222a5965..734c2a467 100644 --- a/client/src/widgets/modals/info-modal/user/phone/phone.tsx +++ b/client/src/widgets/modals/info-modal/user/phone/phone.tsx @@ -1,4 +1,4 @@ -import { ArrowLeftIcon, ArrowRightIcon, ChatCircleDotsIcon, UserPlusIcon } from '@/shared/assets'; +import { ArrowLeftIcon, ArrowRightIcon, ChatCircleDotsIcon } from '@/shared/assets'; import { Button, Drawer, Flex, Typography } from '@/shared/ui'; import { FC } from 'react'; import styles from './phone.module.scss'; @@ -7,10 +7,12 @@ import { InfoModalUserProps } from '../interfaces'; import { ImageLoader } from '@/shared/ui/image-loader/image-loader'; import { IconLayout } from '../ui/icon-layout/icon-layout'; import { TextLayout } from '../ui/text-layout/text-layout'; +import { FriendButton } from '@/features/friend-button'; +import { useGetMe } from '@/entities/session'; export const UserPhone: FC = ({ user, isOpenModal, handleClose }) => { const age = user?.dateOfBirth ? calculateAge(user.dateOfBirth) : null; - + const { data: me } = useGetMe(); return ( <> @@ -26,10 +28,12 @@ export const UserPhone: FC = ({ user, isOpenModal, handleClo Back - + + + @@ -66,12 +70,9 @@ export const UserPhone: FC = ({ user, isOpenModal, handleClo - { - - } + {user?.id && ( + + )}