From da6b2430389176142888438aa2e087348fee1e3c Mon Sep 17 00:00:00 2001 From: Nikita Mashchenko <52038455+nmashchenko@users.noreply.github.com> Date: Sat, 23 Dec 2023 23:21:03 -0600 Subject: [PATCH] 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 --- client/next.config.js | 1 + client/package.json | 1 + client/src/app/(main)/layout.module.scss | 15 +- client/src/app/(main)/page.tsx | 80 ++++- .../src/app/(main)/ui/cards/cards.module.scss | 35 +++ client/src/app/(main)/ui/cards/cards.tsx | 89 ++++++ .../ui/users-not-found/users-not-found.tsx | 23 ++ client/src/entities/session/api/useGetMe.tsx | 2 + .../session/api/useGetNotifications.tsx | 2 + .../src/entities/session/api/useGetUsers.tsx | 36 +-- .../frameworks-layout-config.tsx | 16 - .../frameworks-layout/frameworks-layout.tsx | 28 -- .../ui/icon-layout/icon-layout-config.tsx | 15 + .../user/ui/icon-layout/icon-layout.tsx | 21 ++ .../language-layout-config.tsx | 15 - .../ui/language-layout/language-layout.tsx | 20 -- .../ui/text-layout/text-layout-config.tsx | 16 + .../user/ui/text-layout/text-layout.tsx | 26 ++ .../user/ui/user-card/user-card.module.scss | 6 +- .../entities/user/ui/user-card/user-card.tsx | 77 ++--- .../shared/assets/illustrations/astronaut.tsx | 113 +++++++ .../src/shared/assets/illustrations/index.ts | 1 + .../src/shared/lib/utils/get-age/get-age.ts | 5 +- .../get-country-flag/get-country-flag.test.ts | 4 +- .../get-country-flag/get-country-flag.tsx | 4 +- client/src/shared/ui/index.ts | 1 + .../ui/search-bar/hooks/useTrackFiltersArr.ts | 2 +- .../ui/search-bar/search-bar.module.scss | 3 +- .../ui/modal-button/modal-button.module.scss | 10 +- .../ui/modal-button/modal-button.tsx | 2 +- client/src/shared/ui/skeleton/index.ts | 1 + .../shared/ui/skeleton/skeleton.module.scss | 59 ---- .../shared/ui/skeleton/skeleton.stories.tsx | 218 -------------- .../src/shared/ui/skeleton/skeleton.test.tsx | 31 -- client/src/shared/ui/skeleton/skeleton.tsx | 166 ++-------- .../info-modal/user/desktop/desktop.tsx | 67 +--- .../info-modal/user/phone/phone.module.scss | 7 - .../modals/info-modal/user/phone/phone.tsx | 75 +---- .../user/ui/icon-layout/icon-layout.tsx | 31 ++ .../text-layout/text-layout.module.scss} | 0 .../user/ui/text-layout/text-layout.tsx | 32 ++ .../lib/hooks/useListenToNotifications.tsx | 2 +- .../ui/sidebar-profile/sidebar-profile.tsx | 2 +- .../sidebar/ui/sidebar/sidebar.module.scss | 2 +- client/yarn.lock | 10 + server/package.json | 2 +- ...ateUser.ts => 1703373105991-CreateUser.ts} | 22 +- .../database/seeds/user/user-seed.service.ts | 285 +++++++++++++++++- .../src/modules/users/entities/user.entity.ts | 2 +- server/src/modules/users/users.controller.ts | 8 +- server/src/modules/users/users.service.ts | 6 +- server/yarn.lock | 10 +- 52 files changed, 920 insertions(+), 787 deletions(-) create mode 100644 client/src/app/(main)/ui/cards/cards.module.scss create mode 100644 client/src/app/(main)/ui/cards/cards.tsx create mode 100644 client/src/app/(main)/ui/users-not-found/users-not-found.tsx delete mode 100644 client/src/entities/user/ui/frameworks-layout/frameworks-layout-config.tsx delete mode 100644 client/src/entities/user/ui/frameworks-layout/frameworks-layout.tsx create mode 100644 client/src/entities/user/ui/icon-layout/icon-layout-config.tsx create mode 100644 client/src/entities/user/ui/icon-layout/icon-layout.tsx delete mode 100644 client/src/entities/user/ui/language-layout/language-layout-config.tsx delete mode 100644 client/src/entities/user/ui/language-layout/language-layout.tsx create mode 100644 client/src/entities/user/ui/text-layout/text-layout-config.tsx create mode 100644 client/src/entities/user/ui/text-layout/text-layout.tsx create mode 100644 client/src/shared/assets/illustrations/astronaut.tsx create mode 100644 client/src/shared/ui/skeleton/index.ts delete mode 100644 client/src/shared/ui/skeleton/skeleton.module.scss delete mode 100644 client/src/shared/ui/skeleton/skeleton.stories.tsx delete mode 100644 client/src/shared/ui/skeleton/skeleton.test.tsx create mode 100644 client/src/widgets/modals/info-modal/user/ui/icon-layout/icon-layout.tsx rename client/src/widgets/modals/info-modal/user/{desktop/desktop.module.scss => ui/text-layout/text-layout.module.scss} (100%) create mode 100644 client/src/widgets/modals/info-modal/user/ui/text-layout/text-layout.tsx rename server/src/libs/database/migrations/{1703217496755-CreateUser.ts => 1703373105991-CreateUser.ts} (90%) diff --git a/client/next.config.js b/client/next.config.js index 36570ae1b..3e9fb68c9 100644 --- a/client/next.config.js +++ b/client/next.config.js @@ -8,6 +8,7 @@ module.exports = { 'localhost', 'picsum.photos', 'source.unsplash.com', + 'upload.wikimedia.org', ], remotePatterns: [ { diff --git a/client/package.json b/client/package.json index 226a7680b..073e393b2 100644 --- a/client/package.json +++ b/client/package.json @@ -47,6 +47,7 @@ "react-content-loader": "^6.2.1", "react-dom": "18.2.0", "react-hook-form": "^7.45.4", + "react-loading-skeleton": "^3.3.1", "react-modern-drawer": "^1.2.2", "react-particles": "^2.12.2", "react-responsive-modal": "^6.4.2", diff --git a/client/src/app/(main)/layout.module.scss b/client/src/app/(main)/layout.module.scss index e19c563e6..5544aa3a0 100644 --- a/client/src/app/(main)/layout.module.scss +++ b/client/src/app/(main)/layout.module.scss @@ -1,14 +1,14 @@ .container { height: 100dvh; width: 100%; - padding: 48px 55px; + padding: 48px 0; @media (width <= 1120px) { - padding: 48px 24px; + padding: 48px 0; } @media (width <= 580px) { - padding: 24px; + padding: 28px; } } @@ -16,7 +16,14 @@ width: 100%; min-height: 100%; display: flex; - justify-content: center; align-items: center; flex-direction: column; } + +.content_zone { + padding-left: 88px; + + @media screen and (max-width: 768px) { + padding-left: 0; + } +} diff --git a/client/src/app/(main)/page.tsx b/client/src/app/(main)/page.tsx index 73ba35a47..5d8fd1917 100644 --- a/client/src/app/(main)/page.tsx +++ b/client/src/app/(main)/page.tsx @@ -1,22 +1,76 @@ 'use client'; - -import { Typography } from '@/shared/ui'; -import { useGetScreenWidth } from '@/shared/lib'; +import { Flex, SearchBar } from '@/shared/ui'; +import { countries, specialities } from '@/shared/constant'; +import { LogoBig } from '@/shared/assets'; +import { useGetUsers } from '@/entities/session'; +import { useState } from 'react'; +import { Cards } from '@/app/(main)/ui/cards/cards'; +import styles from './layout.module.scss'; +import { UserInfoModal } from '@/widgets'; +import { IUserResponse } from '@teameights/types'; export default function Home() { - const width = useGetScreenWidth(); + const [filters, setFilters] = useState(); + const { fetchNextPage, hasNextPage, isFetchingNextPage, data, ...result } = useGetUsers(filters); + const [open, setOpen] = useState(false); + const [selectedUser, setSelectedUser] = useState(); + + const handleModalOpen = (user: IUserResponse) => { + setSelectedUser(user); + setOpen(true); + }; return ( <> - - We are working hard to deliver teameights on NextJS/TS soon! - - -
The screen width is: {width}
- - - Get to login - + setOpen(false)} user={selectedUser} /> + + + { + setFilters(filterValues); + }} + /> + + ); } diff --git a/client/src/app/(main)/ui/cards/cards.module.scss b/client/src/app/(main)/ui/cards/cards.module.scss new file mode 100644 index 000000000..e45207e25 --- /dev/null +++ b/client/src/app/(main)/ui/cards/cards.module.scss @@ -0,0 +1,35 @@ +.cards_zone { + padding-left: 88px; + + @media screen and (max-width: 768px) { + padding-left: 0; + } +} + +.cards { + display: grid; + row-gap: 50px; + margin-top: 15px; + width: 100%; + max-width: 1196px; + grid-template-columns: repeat(4, 1fr); + justify-items: center; + + @media screen and (max-width: 1440px) { + max-width: 826px; + grid-template-columns: repeat(3, 1fr); + } + @media screen and (max-width: 1024px) { + max-width: 770px; + grid-template-columns: repeat(3, 1fr); + } + + @media screen and (max-width: 900px) { + max-width: 526px; + grid-template-columns: repeat(2, 1fr); + } + + @media screen and (max-width: 600px) { + grid-template-columns: repeat(1, 1fr); + } +} diff --git a/client/src/app/(main)/ui/cards/cards.tsx b/client/src/app/(main)/ui/cards/cards.tsx new file mode 100644 index 000000000..6d37f6051 --- /dev/null +++ b/client/src/app/(main)/ui/cards/cards.tsx @@ -0,0 +1,89 @@ +import { InfinityPaginationResultType, IUserResponse } from '@teameights/types'; +import { FC, useCallback, useRef } from 'react'; +import { Flex, CardSkeleton } from '@/shared/ui'; +import { UserCard } from '@/entities/user'; +import styles from './cards.module.scss'; +import { UsersNotFound } from '../users-not-found/users-not-found'; +import { + FetchNextPageOptions, + InfiniteData, + InfiniteQueryObserverResult, +} from '@tanstack/query-core'; + +interface CardsProps { + onCardClick: (user: IUserResponse) => void; + data?: InfiniteData>; + isLoading: boolean; + isFetchingNextPage: boolean; + hasNextPage: boolean; + fetchNextPage: ( + options?: FetchNextPageOptions | undefined + ) => Promise< + InfiniteQueryObserverResult< + InfiniteData, unknown>, + Error + > + >; +} +export const Cards: FC = ({ + onCardClick, + data, + isLoading, + isFetchingNextPage, + hasNextPage, + fetchNextPage, +}) => { + const intObserver = useRef(); + + const lastUserRef = useCallback( + (user: HTMLDivElement) => { + if (isFetchingNextPage) { + return; + } + + if (intObserver.current) { + intObserver.current.disconnect(); + } + + intObserver.current = new IntersectionObserver( + usersPerPage => { + if (usersPerPage[0].isIntersecting && hasNextPage) { + fetchNextPage(); + } + }, + { threshold: 0.9 } + ); + + if (user) { + intObserver.current.observe(user); + } + }, + [isFetchingNextPage, fetchNextPage, hasNextPage] + ); + + const content = data?.pages.map(pg => { + const usersPerPage = pg.data; + + return usersPerPage.map((user, index) => { + if (usersPerPage.length === index + 1) { + return ( + onCardClick(user)} /> + ); + } + + return onCardClick(user)} />; + }); + }); + + return ( + + + {!isLoading && !data?.pages.length && } +
+ {content} + {(isLoading || isFetchingNextPage) && } +
+
+
+ ); +}; diff --git a/client/src/app/(main)/ui/users-not-found/users-not-found.tsx b/client/src/app/(main)/ui/users-not-found/users-not-found.tsx new file mode 100644 index 000000000..63b161b78 --- /dev/null +++ b/client/src/app/(main)/ui/users-not-found/users-not-found.tsx @@ -0,0 +1,23 @@ +import { Flex, Typography } from '@/shared/ui'; +import { AstronautIllustration } from '@/shared/assets'; + +export const UsersNotFound = () => { + return ( + + + + + No results found :( + We can’t find any item matching your search + + + + ); +}; diff --git a/client/src/entities/session/api/useGetMe.tsx b/client/src/entities/session/api/useGetMe.tsx index bf86c6e1c..388cbffb6 100644 --- a/client/src/entities/session/api/useGetMe.tsx +++ b/client/src/entities/session/api/useGetMe.tsx @@ -13,5 +13,7 @@ export const useGetMe = () => { }, refetchOnMount: false, refetchOnWindowFocus: false, + retry: 1, + retryDelay: 5000, }); }; diff --git a/client/src/entities/session/api/useGetNotifications.tsx b/client/src/entities/session/api/useGetNotifications.tsx index 38b8e7f2b..e923f64b1 100644 --- a/client/src/entities/session/api/useGetNotifications.tsx +++ b/client/src/entities/session/api/useGetNotifications.tsx @@ -14,5 +14,7 @@ export const useGetNotifications = () => { }, refetchOnMount: false, refetchOnWindowFocus: false, + retry: 1, + retryDelay: 5000, }); }; diff --git a/client/src/entities/session/api/useGetUsers.tsx b/client/src/entities/session/api/useGetUsers.tsx index 81c107960..efd2743c5 100644 --- a/client/src/entities/session/api/useGetUsers.tsx +++ b/client/src/entities/session/api/useGetUsers.tsx @@ -1,30 +1,24 @@ 'use client'; -import { useQuery } from '@tanstack/react-query'; -import { IUserProtectedResponse } from '@teameights/types'; +import { useInfiniteQuery } from '@tanstack/react-query'; +import { InfinityPaginationResultType, IUserResponse } from '@teameights/types'; import { API } from '@/shared/api'; import { API_USERS } from '@/shared/constant'; -interface IQueryParams { - page?: number; - limit?: number; - filters?: string | null; - sort?: string; -} - -export const useGetUsers = ({ page, limit, filters, sort }: IQueryParams) => { - const queryString = [ - page && `page=${page}`, - limit && `limit=${limit}`, - filters && `filters=${filters}`, - sort && `sort=${sort}`, - ].join(''); - - return useQuery({ - queryKey: ['useGetUsers', queryString], - queryFn: async () => { - const { data } = await API.get(`${API_USERS}?${queryString}`); +export const useGetUsers = (filters?: string | null) => { + return useInfiniteQuery({ + queryKey: ['useGetUsers', filters], + queryFn: async ({ queryKey, pageParam }) => { + let url = `${API_USERS}?page=${pageParam}&limit=9`; + if (queryKey[1]) { + url = `${url}&filters=${queryKey[1]}`; + } + const { data } = await API.get>(url); return data; }, + initialPageParam: 1, + getNextPageParam: (lastPage, allPages) => { + return lastPage.hasNextPage ? allPages.length + 1 : undefined; + }, refetchOnMount: false, refetchOnWindowFocus: false, }); diff --git a/client/src/entities/user/ui/frameworks-layout/frameworks-layout-config.tsx b/client/src/entities/user/ui/frameworks-layout/frameworks-layout-config.tsx deleted file mode 100644 index c137958a0..000000000 --- a/client/src/entities/user/ui/frameworks-layout/frameworks-layout-config.tsx +++ /dev/null @@ -1,16 +0,0 @@ -type BadgeFrameworkType = 'full' | 'half' | 'empty' | 'extra'; -type BadgeFrameworkLayout = BadgeFrameworkType[]; - -interface badgeFrameworkLayoutConfig { - readonly default: Readonly; - - readonly [badgeCount: number]: Readonly; -} - -export const badgeFrameworkLayoutConfig: badgeFrameworkLayoutConfig = { - 1: ['empty', 'full'], - 2: ['full', 'full'], - 3: ['half', 'half', 'full'], - 4: ['half', 'half', 'half', 'half'], - default: ['half', 'half', 'half', 'extra'], -} as const; diff --git a/client/src/entities/user/ui/frameworks-layout/frameworks-layout.tsx b/client/src/entities/user/ui/frameworks-layout/frameworks-layout.tsx deleted file mode 100644 index 5ab1331d6..000000000 --- a/client/src/entities/user/ui/frameworks-layout/frameworks-layout.tsx +++ /dev/null @@ -1,28 +0,0 @@ -import styles from '../user-card/user-card.module.scss'; -import { BadgeText } from '@/shared/ui'; -import { badgeFrameworkLayoutConfig } from './frameworks-layout-config'; - -type BadgeFrameworksProps = { - frameworks: string[]; -}; - -export const BadgeFrameworksLayout: React.FC = ({ frameworks }) => { - const layout = - badgeFrameworkLayoutConfig[frameworks.length] || badgeFrameworkLayoutConfig.default; - - const isOneFramework = frameworks.length === 1; - - return ( -
- {layout.map((size, index) => ( - - ))} -
- ); -}; diff --git a/client/src/entities/user/ui/icon-layout/icon-layout-config.tsx b/client/src/entities/user/ui/icon-layout/icon-layout-config.tsx new file mode 100644 index 000000000..e70d2699c --- /dev/null +++ b/client/src/entities/user/ui/icon-layout/icon-layout-config.tsx @@ -0,0 +1,15 @@ +type IconType = 'single' | 'more' | 'empty'; +type IconLayout = IconType[]; + +interface IconLayoutConfig { + readonly default: Readonly; + + readonly [iconCount: number]: Readonly; +} + +export const iconLayoutConfig: IconLayoutConfig = { + 1: ['single', 'empty'], + 2: ['single', 'single'], + 3: ['single', 'more'], + default: ['single', 'more'], +} as const; diff --git a/client/src/entities/user/ui/icon-layout/icon-layout.tsx b/client/src/entities/user/ui/icon-layout/icon-layout.tsx new file mode 100644 index 000000000..0db624c6e --- /dev/null +++ b/client/src/entities/user/ui/icon-layout/icon-layout.tsx @@ -0,0 +1,21 @@ +import styles from '../user-card/user-card.module.scss'; +import { BadgeIcon } from '@/shared/ui'; +import { iconLayoutConfig } from './icon-layout-config'; +import { FC } from 'react'; + +interface IconProps { + icons: string[]; +} + +export const IconLayout: FC = ({ icons }) => { + const layout = iconLayoutConfig[icons.length] || iconLayoutConfig.default; + + return ( +
+ {layout.map((type, index) => { + if (type === 'more') return ; + return icons[index] && ; + })} +
+ ); +}; diff --git a/client/src/entities/user/ui/language-layout/language-layout-config.tsx b/client/src/entities/user/ui/language-layout/language-layout-config.tsx deleted file mode 100644 index c626d1d76..000000000 --- a/client/src/entities/user/ui/language-layout/language-layout-config.tsx +++ /dev/null @@ -1,15 +0,0 @@ -type LanguageType = 'single' | 'more' | 'empty'; -type LanguageLayout = LanguageType[]; - -interface LanguageLayoutConfig { - readonly default: Readonly; - - readonly [languageCount: number]: Readonly; -} - -export const languageLayoutConfig: LanguageLayoutConfig = { - 1: ['single', 'empty'], - 2: ['single', 'single'], - 3: ['single', 'more'], - default: ['single', 'more'], -} as const; diff --git a/client/src/entities/user/ui/language-layout/language-layout.tsx b/client/src/entities/user/ui/language-layout/language-layout.tsx deleted file mode 100644 index 21412549f..000000000 --- a/client/src/entities/user/ui/language-layout/language-layout.tsx +++ /dev/null @@ -1,20 +0,0 @@ -import styles from '../user-card/user-card.module.scss'; -import { BadgeIcon } from '@/shared/ui'; -import { languageLayoutConfig } from './language-layout-config'; - -interface ProgrammingLanguagesProps { - languages: string[]; -} - -export const ProgrammingLanguagesLayout: React.FC = ({ languages }) => { - const layout = languageLayoutConfig[languages.length] || languageLayoutConfig.default; - - return ( -
- {layout.map((type, index) => { - if (type === 'more') return ; - return languages[index] && ; - })} -
- ); -}; diff --git a/client/src/entities/user/ui/text-layout/text-layout-config.tsx b/client/src/entities/user/ui/text-layout/text-layout-config.tsx new file mode 100644 index 000000000..c54987a10 --- /dev/null +++ b/client/src/entities/user/ui/text-layout/text-layout-config.tsx @@ -0,0 +1,16 @@ +type TextType = 'full' | 'half' | 'empty' | 'extra'; +type TextLayout = TextType[]; + +interface TextLayoutConfig { + readonly default: Readonly; + + readonly [badgeCount: number]: Readonly; +} + +export const textLayoutConfig: TextLayoutConfig = { + 1: ['empty', 'full'], + 2: ['full', 'full'], + 3: ['half', 'half', 'full'], + 4: ['half', 'half', 'half', 'half'], + default: ['half', 'half', 'half', 'extra'], +} as const; diff --git a/client/src/entities/user/ui/text-layout/text-layout.tsx b/client/src/entities/user/ui/text-layout/text-layout.tsx new file mode 100644 index 000000000..7178bb9b5 --- /dev/null +++ b/client/src/entities/user/ui/text-layout/text-layout.tsx @@ -0,0 +1,26 @@ +import styles from '../user-card/user-card.module.scss'; +import { BadgeText } from '@/shared/ui'; +import { textLayoutConfig } from './text-layout-config'; +import { FC } from 'react'; + +type TextProps = { + texts: string[]; +}; + +export const TextLayout: FC = ({ texts }) => { + const layout = textLayoutConfig[texts.length] || textLayoutConfig.default; + + const isOneText = texts.length === 1; + + return ( +
+ {layout.map((size, index) => ( + + ))} +
+ ); +}; diff --git a/client/src/entities/user/ui/user-card/user-card.module.scss b/client/src/entities/user/ui/user-card/user-card.module.scss index f56004853..16eb07ab0 100644 --- a/client/src/entities/user/ui/user-card/user-card.module.scss +++ b/client/src/entities/user/ui/user-card/user-card.module.scss @@ -56,7 +56,7 @@ } } -.languagesContainer { +.icons_container { display: flex; gap: 10px; width: 100%; @@ -79,7 +79,7 @@ gap: 8px; } - .role { + .speciality { font: var(--font-body-s); color: var(--grey-normal-color); flex-basis: 34px; @@ -90,7 +90,7 @@ } } -.badgeContainer { +.text_container { display: flex; flex-wrap: wrap; align-self: stretch; diff --git a/client/src/entities/user/ui/user-card/user-card.tsx b/client/src/entities/user/ui/user-card/user-card.tsx index e57e6c433..a4dc7b982 100644 --- a/client/src/entities/user/ui/user-card/user-card.tsx +++ b/client/src/entities/user/ui/user-card/user-card.tsx @@ -1,50 +1,59 @@ import styles from './user-card.module.scss'; -import { ProgrammingLanguagesLayout } from '../language-layout/language-layout'; -import { BadgeFrameworksLayout } from '../frameworks-layout/frameworks-layout'; import { IUserResponse } from '@teameights/types'; import { calculateAge } from '@/shared/lib'; import { ImageLoader } from '@/shared/ui'; import { CrownIcon28 } from '@/shared/assets'; import { countryFlags } from '@/shared/constant'; +import { forwardRef } from 'react'; +import { IconLayout } from '../icon-layout/icon-layout'; +import { TextLayout } from '../text-layout/text-layout'; interface UserCardProps { user: IUserResponse; onClick?: () => void; } -export const UserCard: React.FC = ({ - user: { country, photo, skills, isLeader, fullName, role, dateOfBirth }, - onClick, -}) => { - const years = calculateAge(dateOfBirth); +export const UserCard = forwardRef( + ( + { user: { country, photo, skills, isLeader, fullName, speciality, dateOfBirth }, onClick }, + ref + ) => { + const years = calculateAge(dateOfBirth); - return ( -
-
-
- {/* TODO: Починить это опсле фикса типов с фотками*/} - - {isLeader && } + return ( +
+
+
+ + {isLeader && } +
+ {skills?.programmingLanguages && } + {skills?.designerTools && } + {skills?.projectManagerTools && }
- {skills?.programmingLanguages && ( - - )} -
-
-
- {fullName}, {years} - +
+
+ {fullName}, {years} + +
+
{speciality}
-
{role.name}
+ {skills?.frameworks && } + {skills?.methodologies && } + {skills?.fields && }
- {skills?.frameworks && } -
- ); -}; + ); + } +); diff --git a/client/src/shared/assets/illustrations/astronaut.tsx b/client/src/shared/assets/illustrations/astronaut.tsx new file mode 100644 index 000000000..582aff86c --- /dev/null +++ b/client/src/shared/assets/illustrations/astronaut.tsx @@ -0,0 +1,113 @@ +import { FC, SVGProps } from 'react'; +export const AstronautIllustration: FC> = props => { + return ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ); +}; diff --git a/client/src/shared/assets/illustrations/index.ts b/client/src/shared/assets/illustrations/index.ts index 3d67601b9..4bc2fc426 100644 --- a/client/src/shared/assets/illustrations/index.ts +++ b/client/src/shared/assets/illustrations/index.ts @@ -1,3 +1,4 @@ export { EmailIllustration } from './email'; export { LoginIllustration } from './login'; export { PlanetIllustration } from './planet'; +export { AstronautIllustration } from './astronaut'; diff --git a/client/src/shared/lib/utils/get-age/get-age.ts b/client/src/shared/lib/utils/get-age/get-age.ts index d6c0ff34f..e53ccee8c 100644 --- a/client/src/shared/lib/utils/get-age/get-age.ts +++ b/client/src/shared/lib/utils/get-age/get-age.ts @@ -1,4 +1,7 @@ -export const calculateAge = (birthDate: string | Date): number => { +export const calculateAge = (birthDate: string | Date | undefined): number => { + if (!birthDate) { + return -999; + } const birthDateObj = typeof birthDate === 'string' ? new Date(birthDate) : birthDate; const currentDate = new Date(); diff --git a/client/src/shared/lib/utils/get-country-flag/get-country-flag.test.ts b/client/src/shared/lib/utils/get-country-flag/get-country-flag.test.ts index a0845af73..24807ec20 100644 --- a/client/src/shared/lib/utils/get-country-flag/get-country-flag.test.ts +++ b/client/src/shared/lib/utils/get-country-flag/get-country-flag.test.ts @@ -14,10 +14,10 @@ describe('getCountryFlag', () => { }); it('should get nothing', () => { - expect(getCountryFlag('')).toBe(''); + expect(getCountryFlag('')).toBe(undefined); }); it('should get nothing for random words', () => { - expect(getCountryFlag('adfasreavxgeag')).toBe(''); + expect(getCountryFlag('adfasreavxgeag')).toBe(undefined); }); }); diff --git a/client/src/shared/lib/utils/get-country-flag/get-country-flag.tsx b/client/src/shared/lib/utils/get-country-flag/get-country-flag.tsx index d4e9d5a10..e10b53090 100644 --- a/client/src/shared/lib/utils/get-country-flag/get-country-flag.tsx +++ b/client/src/shared/lib/utils/get-country-flag/get-country-flag.tsx @@ -1,11 +1,11 @@ import { countryFlags } from '@/shared/constant'; -export const getCountryFlag = (countryName: string = ''): string => { +export const getCountryFlag = (countryName: string = ''): string | undefined => { const normalizedCountryName = countryName.trim(); if (normalizedCountryName in countryFlags) { return countryFlags[normalizedCountryName]; } - return ''; + return undefined; }; diff --git a/client/src/shared/ui/index.ts b/client/src/shared/ui/index.ts index 4e33a67bc..3d5775b41 100644 --- a/client/src/shared/ui/index.ts +++ b/client/src/shared/ui/index.ts @@ -16,3 +16,4 @@ export * from './search-bar'; export * from './progress-bar'; export * from './logo'; export * from './need-help'; +export * from './skeleton'; diff --git a/client/src/shared/ui/search-bar/hooks/useTrackFiltersArr.ts b/client/src/shared/ui/search-bar/hooks/useTrackFiltersArr.ts index ae94ed702..65099b143 100644 --- a/client/src/shared/ui/search-bar/hooks/useTrackFiltersArr.ts +++ b/client/src/shared/ui/search-bar/hooks/useTrackFiltersArr.ts @@ -26,7 +26,7 @@ export const useTrackFilterArr = ( case 'multiple': case 'checkbox': if (curr.filterValue.length) { - acc[curr.value] = curr.filterValue.map(item => item.value); + acc[curr.value] = curr.filterValue.map(item => item.label); } return acc; 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 7e94fde37..75bd9131d 100644 --- a/client/src/shared/ui/search-bar/search-bar.module.scss +++ b/client/src/shared/ui/search-bar/search-bar.module.scss @@ -1,8 +1,7 @@ .searchbar { width: 100%; max-width: 1176px; - padding-left: 24px; - padding-right: 24px; + padding: 0 24px; &_content { min-height: 40px; diff --git a/client/src/shared/ui/search-bar/ui/modal-button/modal-button.module.scss b/client/src/shared/ui/search-bar/ui/modal-button/modal-button.module.scss index 55bfb8f99..6f9bf5a76 100644 --- a/client/src/shared/ui/search-bar/ui/modal-button/modal-button.module.scss +++ b/client/src/shared/ui/search-bar/ui/modal-button/modal-button.module.scss @@ -1,12 +1,10 @@ .modal_button { position: absolute; - z-index: 80; - top: 36px; - right: 30px; + z-index: 150; + top: 32px; + right: 0; display: none; - width: 40px; - height: 40px; - padding: 8px; + padding: 8px 24px 8px 8px; cursor: pointer; @media (max-width: 768px) { 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 4c81c2940..1388a1984 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 @@ -18,7 +18,7 @@ export const ModalButton: FC = ({ onOpen, onClose }) => { return ( - + ); }; diff --git a/client/src/shared/ui/skeleton/index.ts b/client/src/shared/ui/skeleton/index.ts new file mode 100644 index 000000000..e07d8fca2 --- /dev/null +++ b/client/src/shared/ui/skeleton/index.ts @@ -0,0 +1 @@ +export { CardSkeleton } from './skeleton'; diff --git a/client/src/shared/ui/skeleton/skeleton.module.scss b/client/src/shared/ui/skeleton/skeleton.module.scss deleted file mode 100644 index 783919c77..000000000 --- a/client/src/shared/ui/skeleton/skeleton.module.scss +++ /dev/null @@ -1,59 +0,0 @@ -@keyframes react-loading-skeleton { - 100% { - transform: translateX(100%); - } -} - -.reactLoadingSkeleton { - --base-color: #313131; - --highlight-color: #525252; - --animation-duration: 1.5s; - --animation-direction: normal; - --pseudo-element-display: block; /* Enable animation */ - - background-color: var(--base-color); - - width: 100%; - border-radius: 0.25rem; - display: inline-flex; - line-height: 1; - - position: relative; - user-select: none; - overflow: hidden; - z-index: 1; /* Necessary for overflow: hidden to work correctly in Safari */ -} - -.reactLoadingSkeleton::after { - content: ' '; - display: var(--pseudo-element-display); - position: absolute; - top: 0; - left: 0; - right: 0; - height: 100%; - background-repeat: no-repeat; - background-image: linear-gradient( - 90deg, - var(--base-color), - var(--highlight-color), - var(--base-color) - ); - transform: translateX(-100%); - - animation-name: react-loading-skeleton; - animation-direction: var(--animation-direction); - animation-duration: var(--animation-duration); - animation-timing-function: ease-in-out; - animation-iteration-count: infinite; -} - -/*@media (prefers-reduced-motion) {*/ -/* .reactLoadingSkeleton {*/ -/* --pseudo-element-display: none; !* Disable animation *!*/ -/* }*/ -/*}*/ - -.baseSpan { - line-height: 1; -} diff --git a/client/src/shared/ui/skeleton/skeleton.stories.tsx b/client/src/shared/ui/skeleton/skeleton.stories.tsx deleted file mode 100644 index 4e751bdc1..000000000 --- a/client/src/shared/ui/skeleton/skeleton.stories.tsx +++ /dev/null @@ -1,218 +0,0 @@ -import React from 'react'; -import { Meta, StoryObj } from '@storybook/react'; -import { Skeleton, SkeletonProps } from './skeleton'; - -// Default props for Skeleton -const skeletonProps: SkeletonProps = { - count: 1, - inline: false, - height: '100px', - width: '100%', - borderRadius: '4px', -}; - -// Defining meta information for Storybook -type Story = StoryObj; -const SkeletonTemplate: Story = { - render: args => ( -
- -
- ), -}; - -export const Playground = { ...SkeletonTemplate }; -Playground.args = skeletonProps; - -// Skeleton with multiple elements -export const MultipleElements = { ...SkeletonTemplate }; -MultipleElements.args = { - ...skeletonProps, - count: 5, -}; - -// Inline Skeleton elements -export const InlineElements = { ...SkeletonTemplate }; -InlineElements.args = { - ...skeletonProps, - inline: true, -}; - -// Skeleton with custom dimensions -export const CustomDimensions = { ...SkeletonTemplate }; -CustomDimensions.args = { - ...skeletonProps, - height: '50px', - width: '50px', -}; - -// Skeleton with rounded corners -export const RoundedCorners = { ...SkeletonTemplate }; -RoundedCorners.args = { - ...skeletonProps, - borderRadius: '50%', -}; - -// Skeleton with custom style -export const CustomStyle = { ...SkeletonTemplate }; -CustomStyle.args = { - ...skeletonProps, - style: { backgroundColor: 'lightgray' }, -}; - -// Skeleton with custom wrapper -export const CustomWrapper = { ...SkeletonTemplate }; -CustomWrapper.args = { - ...skeletonProps, - wrapper: ({ children }) => ( -
{children}
- ), -}; - -// Skeleton representing a loading text line -export const TextLine = { ...SkeletonTemplate }; -TextLine.args = { - ...skeletonProps, - width: '80%', - height: '16px', -}; - -// Skeleton representing a loading avatar -export const Avatar = { ...SkeletonTemplate }; -Avatar.args = { - ...skeletonProps, - width: '48px', - height: '48px', - borderRadius: '50%', -}; - -// Skeleton representing a loading card -export const Card = { ...SkeletonTemplate }; -Card.args = { - ...skeletonProps, - width: '300px', - height: '400px', - borderRadius: '8px', -}; - -// Skeleton representing a loading button -export const Button = { ...SkeletonTemplate }; -Button.args = { - ...skeletonProps, - width: '120px', - height: '36px', - borderRadius: '4px', -}; - -// Skeleton representing a loading image -export const Image = { ...SkeletonTemplate }; -Image.args = { - ...skeletonProps, - width: '200px', - height: '200px', - borderRadius: '8px', -}; - -// Skeleton representing loading list items -export const ListItems = { ...SkeletonTemplate }; -ListItems.args = { - ...skeletonProps, - count: 5, -}; - -const skeletonArgTypes = { - count: { - control: 'number', - description: 'The number of skeleton elements to render.', - defaultValue: 1, - table: { - type: { summary: 'number' }, - defaultValue: { summary: 1 }, - }, - }, - inline: { - control: 'boolean', - description: 'Whether the skeleton elements should be rendered inline.', - defaultValue: false, - table: { - type: { summary: 'boolean' }, - defaultValue: { summary: false }, - }, - }, - wrapper: { - control: 'object', - description: 'An optional wrapper component for the skeleton elements.', - table: { - type: { summary: 'React.FunctionComponent' }, - }, - }, - className: { - control: 'text', - description: 'A custom class name for the skeleton elements.', - table: { - type: { summary: 'string' }, - }, - }, - height: { - control: 'text', - description: 'The height of the skeleton elements.', - defaultValue: '20px', - table: { - type: { summary: 'string | number' }, - defaultValue: { summary: '20px' }, - }, - }, - width: { - control: 'text', - description: 'The width of the skeleton elements.', - defaultValue: '100%', - table: { - type: { summary: 'string | number' }, - defaultValue: { summary: '100%' }, - }, - }, - borderRadius: { - control: 'text', - description: 'The border radius of the skeleton elements.', - defaultValue: '4px', - table: { - type: { summary: 'string' }, - defaultValue: { summary: '4px' }, - }, - }, - containerTestId: { - control: 'text', - description: 'The data-testid attribute for the container element.', - table: { - type: { summary: 'string' }, - }, - }, - skeletonTestId: { - control: 'text', - description: 'The data-testid attribute for the skeleton elements.', - table: { - type: { summary: 'string' }, - }, - }, - containerClassName: { - control: 'text', - description: 'A custom class name for the container element.', - table: { - type: { summary: 'string' }, - }, - }, - style: { - control: 'object', - description: 'Custom styles for the skeleton elements.', - table: { - type: { summary: 'CSSProperties' }, - }, - }, -}; - -export default { - title: 'shared/Skeleton', - component: Skeleton, - tags: ['autodocs'], - argTypes: skeletonArgTypes, -} as Meta; diff --git a/client/src/shared/ui/skeleton/skeleton.test.tsx b/client/src/shared/ui/skeleton/skeleton.test.tsx deleted file mode 100644 index f8b792797..000000000 --- a/client/src/shared/ui/skeleton/skeleton.test.tsx +++ /dev/null @@ -1,31 +0,0 @@ -import { render, screen } from '@testing-library/react'; - -import { Skeleton } from './skeleton'; - -describe('Skeleton component', () => { - it('should render with provided height and width', () => { - const height = 100; - const width = 200; - render(); - const skeletonContainer = screen.getByTestId('skeleton123-test'); - expect(skeletonContainer).toHaveStyle(`height: ${height}px`); - expect(skeletonContainer).toHaveStyle(`width: ${width}px`); - }); - it('should render with provided border radius', () => { - const borderRadius = '50%'; - render(); - const skeletonContainer = screen.getByTestId('skeleton1234-test'); - expect(skeletonContainer).toHaveStyle(`border-radius: ${borderRadius}`); - }); - it('should render with the provided className', () => { - const className = 'custom-class'; - render(); - const skeletonContainer = screen.getByTestId('skeleton12345-test'); - expect(skeletonContainer).toHaveClass(className); - }); - it('should render without any provided props', () => { - render(); - const skeletonContainer = screen.getByTestId('skeleton123456-test'); - expect(skeletonContainer).toBeInTheDocument(); - }); -}); diff --git a/client/src/shared/ui/skeleton/skeleton.tsx b/client/src/shared/ui/skeleton/skeleton.tsx index b0724ecb8..209328a1d 100644 --- a/client/src/shared/ui/skeleton/skeleton.tsx +++ b/client/src/shared/ui/skeleton/skeleton.tsx @@ -1,144 +1,32 @@ -import type { CSSProperties, PropsWithChildren } from 'react'; -import React, { memo } from 'react'; +import Skeleton from 'react-loading-skeleton'; +import { FC, PropsWithChildren } from 'react'; +import 'react-loading-skeleton/dist/skeleton.css'; +import { Flex } from '@/shared/ui'; -import cls from './skeleton.module.scss'; -import { clsx } from 'clsx'; - -export interface SkeletonProps { - /** - * The number of skeleton elements to render - */ - count?: number; - /** - * [props.inline=false] - Whether the skeleton elements should be rendered inline - */ - inline?: boolean; - /** - * [props.wrapper] - An optional wrapper component for the skeleton elements - */ - wrapper?: React.FunctionComponent>; - /** - * [props.className] - A custom class name for the skeleton elements - */ - className?: string; - /** - * [props.height] - The height of the skeleton elements - */ - height?: string | number; - /** - * [props.width] - The width of the skeleton elements - */ - width?: string | number; - /** - * [props.borderRadius] - The border radius of the skeleton elements - */ - borderRadius?: string; - /** - * [props.containerTestId] - The data-testid attribute for the container element - */ - containerTestId?: string; - /** - * [props.skeletonTestId] - The data-testid attribute for the skeleton elements - */ - skeletonTestId?: string; - /** - * [props.containerClassName] - A custom class name for the container element - */ - containerClassName?: string; - /** - * [props.style] - Custom styles for the skeleton elements - */ - style?: CSSProperties; +interface CardSkeletonProps { + cards: number; } -/** - * `Skeleton` is a presentational component that renders a skeleton screen placeholder UI. - * This is typically used to indicate that content is being loaded and provides a better user experience by reducing the perceived loading time. - * - * The skeleton elements are customizable in terms of dimensions, appearance, and behavior. - * - * Example: - * - * ```tsx - * // Basic usage - * - * ``` - * - * ```tsx - * // Inline skeleton elements with custom dimensions - * - * ``` - * - * ```tsx - * // Skeleton elements with custom wrapper component - * const Wrapper = ({ children }) =>
{children}
; - * - * ``` - * - * ```tsx - * // Skeleton elements with custom styles and class names - * - * ``` - */ -export const Skeleton = memo((props: SkeletonProps) => { - const { - skeletonTestId, - containerClassName, - count = 1, - containerTestId, - wrapper: Wrapper, - inline, - className, - borderRadius, - height, - width, - style, - } = props; - - const styles: CSSProperties = { - width, - height, - borderRadius, - }; - - const elements: React.ReactElement[] = []; - - const countCeil = Math.ceil(count); - - for (let i = 0; i < countCeil; i++) { - const thisStyle = { ...styles, ...style }; - const skeletonSpan = ( - - ‌ - - ); - - if (inline) { - elements.push(skeletonSpan); - } else { - elements.push( - - {skeletonSpan} -
-
- ); - } - } - +function Box({ children }: PropsWithChildren) { return ( - - {Wrapper - ? elements.map((element, index) => {element}) - : elements} - + + {children} + ); -}); +} + +export const CardSkeleton: FC = ({ cards }) => { + return Array(cards) + .fill(0) + .map((item, i) => ( + + )); +}; 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 a95b4cd1a..a89fd232e 100644 --- a/client/src/widgets/modals/info-modal/user/desktop/desktop.tsx +++ b/client/src/widgets/modals/info-modal/user/desktop/desktop.tsx @@ -1,21 +1,14 @@ -import { BadgeText, BadgeIcon, Button, Modal, Typography, Flex, ImageLoader } from '@/shared/ui'; -import styles from './desktop.module.scss'; +import { Button, Modal, Typography, Flex, ImageLoader } from '@/shared/ui'; import { FC } from 'react'; import { ArrowRightIcon, UserPlusIcon, 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'; export const UserDesktop: FC = ({ user, isOpenModal, handleClose }) => { const age = user?.dateOfBirth ? calculateAge(user.dateOfBirth) : null; - // const showInviteButton = () => { - // if (user?.team) { - // if (!user.team.members?.some(member => member.id === user.id)) { - // return true; - // } - // } - // - // return false; - // }; + return ( <> @@ -25,7 +18,7 @@ export const UserDesktop: FC = ({ user, isOpenModal, handleC crownSize={28} width={70} height={70} - src={user?.photo?.path || ''} + src={user?.photo?.path ?? '/images/placeholder.png'} alt='User image' borderRadius='50%' /> @@ -37,7 +30,7 @@ export const UserDesktop: FC = ({ user, isOpenModal, handleC @@ -57,58 +50,18 @@ export const UserDesktop: FC = ({ user, isOpenModal, handleC {user?.description} )} - {/* Grid with frameworks || fields || methodologies */} - {user?.skills?.frameworks && ( -
- {user?.skills?.frameworks.map((framework, index) => ( - - ))} -
- )} - {user?.skills?.fields && ( -
- {user?.skills?.fields.map((field, index) => )} -
- )} - {user?.skills?.methodologies && ( -
- {user?.skills?.methodologies.map((methodology, index) => ( - - ))} -
- )} - {/*Flexbox with designerTools || languages || projectManagerTools*/} - {user?.skills?.programmingLanguages && ( - - {user?.skills?.programmingLanguages.map((language, index) => ( - - ))} - - )} - {user?.skills?.designerTools && ( - - {user?.skills?.designerTools.map((tool, index) => ( - - ))} - - )} - {user?.skills?.projectManagerTools && ( - - {user?.skills?.projectManagerTools.map((tool, index) => ( - - ))} - - )} + + { - } - diff --git a/client/src/widgets/modals/info-modal/user/phone/phone.module.scss b/client/src/widgets/modals/info-modal/user/phone/phone.module.scss index eb133fcc7..82d0afc63 100644 --- a/client/src/widgets/modals/info-modal/user/phone/phone.module.scss +++ b/client/src/widgets/modals/info-modal/user/phone/phone.module.scss @@ -1,10 +1,3 @@ -.grid_container { - display: grid; - grid-template-columns: repeat(3, 1fr); - grid-row-gap: 10px; - grid-column-gap: 8px; -} - .container { overflow: auto; } 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 8ae383c93..85ce22c46 100644 --- a/client/src/widgets/modals/info-modal/user/phone/phone.tsx +++ b/client/src/widgets/modals/info-modal/user/phone/phone.tsx @@ -1,22 +1,15 @@ -import { ArrowLeftIcon, ArrowRightIcon } from '@/shared/assets'; -import { BadgeText, BadgeIcon, Button, Drawer, Flex, Typography } from '@/shared/ui'; +import { ArrowLeftIcon, ArrowRightIcon, ChatCircleDotsIcon, UserPlusIcon } from '@/shared/assets'; +import { Button, Drawer, Flex, Typography } from '@/shared/ui'; import { FC } from 'react'; import styles from './phone.module.scss'; import { calculateAge, getCountryFlag } from '@/shared/lib'; 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'; export const UserPhone: FC = ({ user, isOpenModal, handleClose }) => { const age = user?.dateOfBirth ? calculateAge(user.dateOfBirth) : null; - // const showInviteButton = () => { - // if (user?.team) { - // if (!user.team.members?.some(member => member.id === user.id)) { - // return true; - // } - // } - // - // return false; - // }; return ( <> @@ -44,7 +37,7 @@ export const UserPhone: FC = ({ user, isOpenModal, handleClo crownSize={28} width={70} height={70} - src={user?.photo?.path || ''} + src={user?.photo?.path || '/images/placeholder.png'} alt='User image' borderRadius='50%' /> @@ -57,7 +50,7 @@ export const UserPhone: FC = ({ user, isOpenModal, handleClo @@ -76,68 +69,20 @@ export const UserPhone: FC = ({ user, isOpenModal, handleClo { } {user?.description} - - {/*User is developer:*/} - {user?.skills?.frameworks && ( -
- {user?.skills?.frameworks.map((framework: string, index: number) => ( - - ))} -
- )} - {/*User is designer:*/} - {user?.skills?.fields && ( -
- {user?.skills?.fields.map((field: string, index: number) => ( - - ))} -
- )} - {/*User is project manager:*/} - {user?.skills?.methodologies && ( -
- {user?.skills?.methodologies.map((methodology: string, index: number) => ( - - ))} -
- )} - - {/*User is developer:*/} - {user?.skills?.programmingLanguages && ( - - {user?.skills.programmingLanguages.map((language: string, index: number) => ( - - ))} - - )} - {/*User is designer:*/} - {user?.skills?.designerTools && ( - - {user?.skills.designerTools.map((tool: string, index: number) => ( - - ))} - - )} - - {/*User is project manager:*/} - {user?.skills?.projectManagerTools && ( - - {user?.skills.projectManagerTools.map((tool: string, index: number) => ( - - ))} - - )} -
+ + diff --git a/client/src/widgets/modals/info-modal/user/ui/icon-layout/icon-layout.tsx b/client/src/widgets/modals/info-modal/user/ui/icon-layout/icon-layout.tsx new file mode 100644 index 000000000..15e565426 --- /dev/null +++ b/client/src/widgets/modals/info-modal/user/ui/icon-layout/icon-layout.tsx @@ -0,0 +1,31 @@ +import { BadgeIcon, Flex } from '@/shared/ui'; +import { ISkills, Nullable } from '@teameights/types'; +import { FC } from 'react'; + +interface IconLayout { + skills?: Nullable; +} + +export const IconLayout: FC = ({ skills }) => { + return ( + <> + {skills?.programmingLanguages && ( + + {skills?.programmingLanguages.map((language, index) => ( + + ))} + + )} + {skills?.designerTools && ( + + {skills?.designerTools.map((tool, index) => )} + + )} + {skills?.projectManagerTools && ( + + {skills?.projectManagerTools.map((tool, index) => )} + + )} + + ); +}; diff --git a/client/src/widgets/modals/info-modal/user/desktop/desktop.module.scss b/client/src/widgets/modals/info-modal/user/ui/text-layout/text-layout.module.scss similarity index 100% rename from client/src/widgets/modals/info-modal/user/desktop/desktop.module.scss rename to client/src/widgets/modals/info-modal/user/ui/text-layout/text-layout.module.scss diff --git a/client/src/widgets/modals/info-modal/user/ui/text-layout/text-layout.tsx b/client/src/widgets/modals/info-modal/user/ui/text-layout/text-layout.tsx new file mode 100644 index 000000000..8c51410a9 --- /dev/null +++ b/client/src/widgets/modals/info-modal/user/ui/text-layout/text-layout.tsx @@ -0,0 +1,32 @@ +import { BadgeText } from '@/shared/ui'; +import { ISkills, Nullable } from '@teameights/types'; +import styles from './text-layout.module.scss'; +import { FC } from 'react'; + +interface TextLayout { + skills?: Nullable; +} + +export const TextLayout: FC = ({ skills }) => { + return ( + <> + {skills?.frameworks && ( +
+ {skills?.frameworks.map((framework, index) => )} +
+ )} + {skills?.fields && ( +
+ {skills?.fields.map((field, index) => )} +
+ )} + {skills?.methodologies && ( +
+ {skills?.methodologies.map((methodology, index) => ( + + ))} +
+ )} + + ); +}; diff --git a/client/src/widgets/sidebar/lib/hooks/useListenToNotifications.tsx b/client/src/widgets/sidebar/lib/hooks/useListenToNotifications.tsx index 7d1db68bc..3e9ef115b 100644 --- a/client/src/widgets/sidebar/lib/hooks/useListenToNotifications.tsx +++ b/client/src/widgets/sidebar/lib/hooks/useListenToNotifications.tsx @@ -29,5 +29,5 @@ export const useSocketConnection = (user?: IUserProtectedResponse) => { socket.off(`notification-${user.id}`, handleNotification); socket.disconnect(); }; - }, [user]); + }, [user, queryClient]); }; diff --git a/client/src/widgets/sidebar/ui/sidebar-profile/sidebar-profile.tsx b/client/src/widgets/sidebar/ui/sidebar-profile/sidebar-profile.tsx index 8738f9bdd..8131c9e9e 100644 --- a/client/src/widgets/sidebar/ui/sidebar-profile/sidebar-profile.tsx +++ b/client/src/widgets/sidebar/ui/sidebar-profile/sidebar-profile.tsx @@ -77,7 +77,7 @@ export const SidebarProfile: React.FC = props => { return (
=16.8.0" + checksum: 0de3437a5da8b7133bf86043e4e002e5422b50cd71b9a650f2947a89ace39be8b7c61a098f1d7dd0be559dc3ac293d60697fdae23cb09c3905b2952e1a68693d + languageName: node + linkType: hard + "react-modern-drawer@npm:^1.2.2": version: 1.2.2 resolution: "react-modern-drawer@npm:1.2.2" diff --git a/server/package.json b/server/package.json index 36a230de3..77f21a8df 100644 --- a/server/package.json +++ b/server/package.json @@ -72,7 +72,7 @@ "typeorm": "0.3.17" }, "devDependencies": { - "@faker-js/faker": "^8.1.0", + "@faker-js/faker": "^8.3.1", "@nestjs/cli": "10.1.17", "@nestjs/schematics": "10.0.2", "@nestjs/testing": "10.2.5", diff --git a/server/src/libs/database/migrations/1703217496755-CreateUser.ts b/server/src/libs/database/migrations/1703373105991-CreateUser.ts similarity index 90% rename from server/src/libs/database/migrations/1703217496755-CreateUser.ts rename to server/src/libs/database/migrations/1703373105991-CreateUser.ts index 8c7eec3df..5989e7d2d 100644 --- a/server/src/libs/database/migrations/1703217496755-CreateUser.ts +++ b/server/src/libs/database/migrations/1703373105991-CreateUser.ts @@ -1,7 +1,7 @@ import { MigrationInterface, QueryRunner } from 'typeorm'; -export class CreateUser1703217496755 implements MigrationInterface { - name = 'CreateUser1703217496755'; +export class CreateUser1703373105991 implements MigrationInterface { + name = 'CreateUser1703373105991'; public async up(queryRunner: QueryRunner): Promise { await queryRunner.query( @@ -16,6 +16,9 @@ export class CreateUser1703217496755 implements MigrationInterface { await queryRunner.query( `CREATE TABLE "universities" ("id" SERIAL NOT NULL, "university" character varying NOT NULL, "degree" character varying NOT NULL, "major" character varying NOT NULL, "admissionDate" date NOT NULL, "graduationDate" date, "userId" integer, CONSTRAINT "PK_8da52f2cee6b407559fdbabf59e" PRIMARY KEY ("id"))` ); + await queryRunner.query( + `CREATE TABLE "jobs" ("id" SERIAL NOT NULL, "title" character varying NOT NULL, "company" character varying NOT NULL, "startDate" date NOT NULL, "endDate" date, "userId" integer, CONSTRAINT "PK_cf0a6c42b72fcc7f7c237def345" PRIMARY KEY ("id"))` + ); await queryRunner.query( `CREATE TABLE "projects" ("id" SERIAL NOT NULL, "title" character varying NOT NULL, "link" character varying NOT NULL, "userId" integer, CONSTRAINT "PK_6271df0a7aed1d6c0691ce6ac50" PRIMARY KEY ("id"))` ); @@ -32,7 +35,7 @@ export class CreateUser1703217496755 implements MigrationInterface { `CREATE TABLE "notification" ("id" SERIAL NOT NULL, "read" boolean NOT NULL DEFAULT false, "type" "public"."notification_type_enum" NOT NULL, "data" jsonb NOT NULL, "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "updatedAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "deletedAt" TIMESTAMP WITH TIME ZONE, "receiverId" integer, CONSTRAINT "PK_705b6c7cdf9b2c2ff7ac7872cb7" PRIMARY KEY ("id"))` ); await queryRunner.query( - `CREATE TABLE "user" ("id" SERIAL NOT NULL, "email" character varying, "password" character varying, "username" character varying, "provider" character varying NOT NULL DEFAULT 'email', "socialId" character varying, "fullName" character varying, "isLeader" boolean, "country" character varying, "dateOfBirth" date, "speciality" character varying, "description" character varying, "experience" character varying, "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "updatedAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "deletedAt" TIMESTAMP WITH TIME ZONE, "photoId" uuid, "roleId" integer, "statusId" integer, "skillsId" integer, "linksId" integer, CONSTRAINT "UQ_e12875dfb3b1d92d7d7c5377e22" UNIQUE ("email"), CONSTRAINT "UQ_78a916df40e02a9deb1c4b75edb" UNIQUE ("username"), CONSTRAINT "REL_0e51e612eb9ed2fa5ac4f44c7e" UNIQUE ("skillsId"), CONSTRAINT "REL_c5a79824fd8a241f5a7ec428b3" UNIQUE ("linksId"), CONSTRAINT "PK_cace4a159ff9f2512dd42373760" PRIMARY KEY ("id"))` + `CREATE TABLE "user" ("id" SERIAL NOT NULL, "email" character varying, "password" character varying, "username" character varying, "provider" character varying NOT NULL DEFAULT 'email', "socialId" character varying, "fullName" character varying, "isLeader" boolean, "country" character varying, "dateOfBirth" TIMESTAMP WITH TIME ZONE, "speciality" character varying, "description" character varying, "experience" character varying, "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "updatedAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "deletedAt" TIMESTAMP WITH TIME ZONE, "photoId" uuid, "roleId" integer, "statusId" integer, "skillsId" integer, "linksId" integer, CONSTRAINT "UQ_e12875dfb3b1d92d7d7c5377e22" UNIQUE ("email"), CONSTRAINT "UQ_78a916df40e02a9deb1c4b75edb" UNIQUE ("username"), CONSTRAINT "REL_0e51e612eb9ed2fa5ac4f44c7e" UNIQUE ("skillsId"), CONSTRAINT "REL_c5a79824fd8a241f5a7ec428b3" UNIQUE ("linksId"), CONSTRAINT "PK_cace4a159ff9f2512dd42373760" PRIMARY KEY ("id"))` ); await queryRunner.query( `CREATE INDEX "IDX_9bd2fe7a8e694dedc4ec2f666f" ON "user" ("socialId") ` @@ -44,9 +47,6 @@ export class CreateUser1703217496755 implements MigrationInterface { `CREATE INDEX "IDX_8bceb9ec5c48c54f7a3f11f31b" ON "user" ("isLeader") ` ); await queryRunner.query(`CREATE INDEX "IDX_5cb2b3e0419a73a360d327d497" ON "user" ("country") `); - await queryRunner.query( - `CREATE TABLE "jobs" ("id" SERIAL NOT NULL, "title" character varying NOT NULL, "company" character varying NOT NULL, "startDate" date NOT NULL, "endDate" date, "userId" integer, CONSTRAINT "PK_cf0a6c42b72fcc7f7c237def345" PRIMARY KEY ("id"))` - ); await queryRunner.query( `CREATE TABLE "session" ("id" SERIAL NOT NULL, "createdAt" TIMESTAMP NOT NULL DEFAULT now(), "deletedAt" TIMESTAMP, "userId" integer, CONSTRAINT "PK_f55da76ac1c3ac420f444d2ff11" PRIMARY KEY ("id"))` ); @@ -56,6 +56,9 @@ export class CreateUser1703217496755 implements MigrationInterface { await queryRunner.query( `ALTER TABLE "universities" ADD CONSTRAINT "FK_a8ad75b47a153c0d91f8360c9fb" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE NO ACTION ON UPDATE NO ACTION` ); + await queryRunner.query( + `ALTER TABLE "jobs" ADD CONSTRAINT "FK_79ae682707059d5f7655db4212a" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE NO ACTION ON UPDATE NO ACTION` + ); await queryRunner.query( `ALTER TABLE "projects" ADD CONSTRAINT "FK_361a53ae58ef7034adc3c06f09f" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE NO ACTION ON UPDATE NO ACTION` ); @@ -77,9 +80,6 @@ export class CreateUser1703217496755 implements MigrationInterface { await queryRunner.query( `ALTER TABLE "user" ADD CONSTRAINT "FK_c5a79824fd8a241f5a7ec428b3e" FOREIGN KEY ("linksId") REFERENCES "links"("id") ON DELETE NO ACTION ON UPDATE NO ACTION` ); - await queryRunner.query( - `ALTER TABLE "jobs" ADD CONSTRAINT "FK_79ae682707059d5f7655db4212a" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE NO ACTION ON UPDATE NO ACTION` - ); await queryRunner.query( `ALTER TABLE "session" ADD CONSTRAINT "FK_3d2f174ef04fb312fdebd0ddc53" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE NO ACTION ON UPDATE NO ACTION` ); @@ -89,7 +89,6 @@ export class CreateUser1703217496755 implements MigrationInterface { await queryRunner.query( `ALTER TABLE "session" DROP CONSTRAINT "FK_3d2f174ef04fb312fdebd0ddc53"` ); - await queryRunner.query(`ALTER TABLE "jobs" DROP CONSTRAINT "FK_79ae682707059d5f7655db4212a"`); await queryRunner.query(`ALTER TABLE "user" DROP CONSTRAINT "FK_c5a79824fd8a241f5a7ec428b3e"`); await queryRunner.query(`ALTER TABLE "user" DROP CONSTRAINT "FK_0e51e612eb9ed2fa5ac4f44c7e1"`); await queryRunner.query(`ALTER TABLE "user" DROP CONSTRAINT "FK_dc18daa696860586ba4667a9d31"`); @@ -101,12 +100,12 @@ export class CreateUser1703217496755 implements MigrationInterface { await queryRunner.query( `ALTER TABLE "projects" DROP CONSTRAINT "FK_361a53ae58ef7034adc3c06f09f"` ); + await queryRunner.query(`ALTER TABLE "jobs" DROP CONSTRAINT "FK_79ae682707059d5f7655db4212a"`); await queryRunner.query( `ALTER TABLE "universities" DROP CONSTRAINT "FK_a8ad75b47a153c0d91f8360c9fb"` ); await queryRunner.query(`DROP INDEX "public"."IDX_3d2f174ef04fb312fdebd0ddc5"`); await queryRunner.query(`DROP TABLE "session"`); - await queryRunner.query(`DROP TABLE "jobs"`); await queryRunner.query(`DROP INDEX "public"."IDX_5cb2b3e0419a73a360d327d497"`); await queryRunner.query(`DROP INDEX "public"."IDX_8bceb9ec5c48c54f7a3f11f31b"`); await queryRunner.query(`DROP INDEX "public"."IDX_035190f70c9aff0ef331258d28"`); @@ -117,6 +116,7 @@ export class CreateUser1703217496755 implements MigrationInterface { await queryRunner.query(`DROP TABLE "skills"`); await queryRunner.query(`DROP TABLE "links"`); await queryRunner.query(`DROP TABLE "projects"`); + await queryRunner.query(`DROP TABLE "jobs"`); await queryRunner.query(`DROP TABLE "universities"`); await queryRunner.query(`DROP TABLE "file"`); await queryRunner.query(`DROP TABLE "status"`); diff --git a/server/src/libs/database/seeds/user/user-seed.service.ts b/server/src/libs/database/seeds/user/user-seed.service.ts index 2cc606bee..40a41e657 100644 --- a/server/src/libs/database/seeds/user/user-seed.service.ts +++ b/server/src/libs/database/seeds/user/user-seed.service.ts @@ -4,6 +4,9 @@ import { RoleEnum } from 'src/libs/database/metadata/roles/roles.enum'; import { StatusEnum } from 'src/libs/database/metadata/statuses/statuses.enum'; import { User } from 'src/modules/users/entities/user.entity'; import { Repository } from 'typeorm'; +import { faker } from '@faker-js/faker'; +import { specialityValues } from '../../../../utils/types/specialities.type'; +import { experienceValues } from '../../../../utils/types/experiences.type'; @Injectable() export class UserSeedService { @@ -12,6 +15,247 @@ export class UserSeedService { private repository: Repository ) {} + private shuffleArray = (array: T[]): T[] => { + let currentIndex = array.length, + randomIndex; + + // While there remain elements to shuffle... + while (currentIndex !== 0) { + // Pick a remaining element... + randomIndex = Math.floor(Math.random() * currentIndex); + currentIndex--; + + // And swap it with the current element. + [array[currentIndex], array[randomIndex]] = [array[randomIndex], array[currentIndex]]; + } + + return array; + }; + + private getRandomItemFromArray = (array: string[]): string => { + const randomIndex = faker.number.int({ min: 0, max: array.length - 1 }); + return array[randomIndex]; + }; + + private getRandomBadgeText = ( + min: number, + max: number, + type: 'developer' | 'designer' | 'pm' = 'developer' + ) => { + let data; + + const methodologies = [ + 'Agile', + 'CPM', + 'Kanban', + 'Lean', + 'PRINCE2', + 'PRISM', + 'PMBOK', + 'Scrum', + 'Six Sigma', + 'Waterfall', + ]; + const fields = [ + '3D', + 'Graphic', + 'Motion', + 'SMM', + 'UX', + 'Game', + 'Illustration', + 'Product', + 'UI', + 'Web', + ]; + + const frameworks = [ + 'Node.js', + 'Ruby', + 'Angular', + 'Hadoop', + 'Hive', + 'Snowflake', + 'Ember', + 'Kafka', + 'Django', + 'Docker', + 'Redux', + 'Spring', + 'Spark', + 'Backbone', + 'jQuery', + 'MUI', + 'ASP.NET', + 'NumPy', + 'Flutter', + 'React N.', + 'Flask', + 'Bootstrap', + 'GraphQL', + 'Laravel', + 'PyTorch', + 'Express', + 'Android', + 'AWS', + 'Cypress', + 'Electron', + 'Embedded C', + 'Ionic', + 'IOS', + 'Keras', + 'Kotlin M.', + 'Kubernetes', + 'MXNet', + 'Next.js', + 'Playwright', + 'Qt', + 'React.js', + 'SaaS', + 'Scrapy', + 'Selenium', + 'Svelte', + 'Tensor F.', + 'Terraform', + 'TestCafe', + 'Vue.js', + 'Xamarin', + 'Wordpress', + ]; + + if (type === 'pm') { + data = methodologies; + } else if (type === 'designer') { + data = fields; + } else { + data = frameworks; + } + + const shuffledIcons = this.shuffleArray(data); + + const randomLength = Math.floor(Math.random() * (max - min + 1)) + min; + + return shuffledIcons.slice(0, randomLength); + }; + + private getRandomBadgeIcon = ( + min: number, + max: number, + type: 'developer' | 'designer' | 'pm' = 'developer' + ): string[] => { + let data; + + const managerTools = [ + 'Asana', + 'ClickUp', + 'JIRA', + 'Microsoft Project', + 'Miro', + 'Notion', + 'Slack', + 'Trello', + ]; + + const designerTools = [ + '3ds Max', + 'After Effects', + 'Aseprite', + 'Animate', + 'Blender 3D', + 'Cinema 4D', + 'Figma', + 'Firefly', + 'Framer', + 'Houdini', + 'Illustrator', + 'InDesign', + 'InVision', + 'Lottie', + 'Maya', + 'Midjourney', + 'Modo', + 'Photoshop', + 'Premier Pro', + 'Procreate', + 'Proto Pie', + 'Readymag', + 'Shopify', + 'Sketch', + 'Spline', + 'Substance 3D Designer', + 'Vectary', + 'Webflow', + 'Weblium', + 'XD', + 'ZBrush', + ]; + + const programmingLanguages = [ + 'Assembly', + 'Bash', + 'C', + 'C#', + 'C++', + 'CSS', + 'Dart', + 'Go', + 'HTML', + 'Java', + 'JavaScript', + 'Julia', + 'Kotlin', + 'Lua', + 'MATLAB', + 'Perl', + 'PHP', + 'Python', + 'R', + 'Ruby', + 'Rust', + 'Scala', + 'SQL', + 'Swift', + 'TypeScript', + ]; + + if (type === 'pm') { + data = managerTools; + } else if (type === 'designer') { + data = designerTools; + } else { + data = programmingLanguages; + } + + const shuffledIcons = this.shuffleArray(data); + + const randomLength = Math.floor(Math.random() * (max - min + 1)) + min; + + return shuffledIcons.slice(0, randomLength); + }; + + generateMockSkills = (type: 'developer' | 'designer' | 'pm') => { + const randomBadgeIcons = this.getRandomBadgeIcon(1, 5, type); + const randomBadgeTexts = this.getRandomBadgeText(1, 6, type); + + switch (type) { + case 'developer': + return { + programmingLanguages: randomBadgeIcons, + frameworks: randomBadgeTexts, + }; + case 'designer': + return { + designerTools: randomBadgeIcons, + fields: randomBadgeTexts, + }; + case 'pm': + return { + projectManagerTools: randomBadgeIcons, + methodologies: randomBadgeTexts, + }; + } + }; + async run() { const countAdmin = await this.repository.count({ where: { @@ -75,33 +319,46 @@ export class UserSeedService { ); } - const countUser = await this.repository.count({ - where: { + await this.repository.save( + this.repository.create({ + fullName: 'John Deer', + email: 'john.doe@example.com', + password: 'secret', role: { id: RoleEnum.user, + name: 'Admin', }, - }, - }); + status: { + id: StatusEnum.active, + name: 'Active', + }, + }) + ); + + for (let i = 0; i < 50; i++) { + const randomSpeciality = this.getRandomItemFromArray(['developer', 'designer', 'pm']) as + | 'developer' + | 'designer' + | 'pm'; - if (!countUser) { await this.repository.save( this.repository.create({ - fullName: 'John Deer', - email: 'john.doe@example.com', - password: 'secret', + username: faker.internet.userName(), + fullName: faker.person.firstName(), role: { id: RoleEnum.user, - name: 'Admin', }, status: { id: StatusEnum.active, name: 'Active', }, - speciality: 'Project Manager', - skills: { - projectManagerTools: ['Notion'], - methodologies: ['Agile'], - }, + isLeader: faker.datatype.boolean(), + country: faker.location.country(), + dateOfBirth: faker.date.birthdate({ min: 18, max: 70, mode: 'age' }), + speciality: this.getRandomItemFromArray(specialityValues), + description: faker.datatype.boolean() ? faker.lorem.sentence({ min: 10, max: 50 }) : null, + experience: this.getRandomItemFromArray(experienceValues), + skills: this.generateMockSkills(randomSpeciality), }) ); } diff --git a/server/src/modules/users/entities/user.entity.ts b/server/src/modules/users/entities/user.entity.ts index 7b422f4e2..6389ab528 100644 --- a/server/src/modules/users/entities/user.entity.ts +++ b/server/src/modules/users/entities/user.entity.ts @@ -99,7 +99,7 @@ export class User extends EntityHelper { @Index() country?: string | null; - @Column({ type: 'date', nullable: true }) + @Column({ type: 'timestamptz', nullable: true }) dateOfBirth?: Date | null; @Column({ type: String, nullable: true }) diff --git a/server/src/modules/users/users.controller.ts b/server/src/modules/users/users.controller.ts index c51e2c471..5b59c0688 100644 --- a/server/src/modules/users/users.controller.ts +++ b/server/src/modules/users/users.controller.ts @@ -53,11 +53,11 @@ export class UsersController { @HttpCode(HttpStatus.OK) async findAll(@Query() query: QueryUserDto): Promise> { const page = query?.page ?? 1; - let limit = query?.limit ?? 10; + const limit = query?.limit ?? 9; - if (limit > 50) { - limit = 50; - } + // if (limit > 50) { + // limit = 50; + // } return infinityPagination( await this.usersService.findManyWithPagination({ diff --git a/server/src/modules/users/users.service.ts b/server/src/modules/users/users.service.ts index ab9fce314..0cc76bdd6 100644 --- a/server/src/modules/users/users.service.ts +++ b/server/src/modules/users/users.service.ts @@ -2,7 +2,7 @@ import { Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { EntityCondition } from 'src/utils/types/entity-condition.type'; import { IPaginationOptions } from 'src/utils/types/pagination-options'; -import { ArrayOverlap, DeepPartial, FindOptionsWhere, In, Like, Repository } from 'typeorm'; +import { ArrayOverlap, DeepPartial, FindOptionsWhere, ILike, In, Like, Repository } from 'typeorm'; import { CreateUserDto } from './dto/create-user.dto'; import { User } from './entities/user.entity'; import { NullableType } from 'src/utils/types/nullable.type'; @@ -31,8 +31,8 @@ export class UsersService { const where: FindOptionsWhere = {}; if (filterOptions) { - where.fullName = filterOptions?.fullName && Like(`%${filterOptions.fullName}%`); - where.username = filterOptions?.username && Like(`%${filterOptions.username}%`); + where.fullName = filterOptions?.fullName && ILike(`%${filterOptions.fullName}%`); + where.username = filterOptions?.username && ILike(`%${filterOptions.username}%`); where.isLeader = filterOptions?.isLeader && filterOptions.isLeader; where.country = filterOptions?.countries && In(filterOptions.countries); diff --git a/server/yarn.lock b/server/yarn.lock index d18c78b8a..b229cba3a 100644 --- a/server/yarn.lock +++ b/server/yarn.lock @@ -1730,10 +1730,10 @@ __metadata: languageName: node linkType: hard -"@faker-js/faker@npm:^8.1.0": - version: 8.1.0 - resolution: "@faker-js/faker@npm:8.1.0" - checksum: 76036cbad2f0735fe2a2834bb3e16233e7c1aa4998cf90dbd097631465f3fcd4e7022c901f80b6de1c25b47154880f06916609a81dacb039a25f9cb000a3ab4e +"@faker-js/faker@npm:^8.3.1": + version: 8.3.1 + resolution: "@faker-js/faker@npm:8.3.1" + checksum: 33efe912411fe61f43b313784a9ce041dfbfb54bc3b2f7b923a547f97beb6b3763534f9c37af25ab330982e6b36e6f7b040dfb35c115deb8c789d8ada413bd94 languageName: node linkType: hard @@ -10579,7 +10579,7 @@ __metadata: resolution: "teameights-server@workspace:." dependencies: "@aws-sdk/client-s3": 3.414.0 - "@faker-js/faker": ^8.1.0 + "@faker-js/faker": ^8.3.1 "@nestjs/cli": 10.1.17 "@nestjs/common": 10.2.5 "@nestjs/config": 3.1.1