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 00000000..fb40784a
--- /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 00000000..15e82588
--- /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 00000000..47d81496
--- /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 00000000..759e171d
--- /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 00000000..783690df
--- /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 00000000..11be4b36
--- /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 00000000..2ee2227a
--- /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 00000000..01dfef8a
--- /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 00000000..179deb8d
--- /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 00000000..9c9ff17e
--- /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 00000000..f69e7415
--- /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 00000000..301cb53d
--- /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 00000000..94b1b5c3
--- /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 00000000..92a1f9f6
--- /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 00000000..95205902
--- /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 00000000..6adccb4c
--- /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 00000000..e6f29d95
--- /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 00000000..d56cc64f
--- /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 00000000..7a4546a8
--- /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 00000000..e69de29b
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 00000000..3a3ee6db
--- /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 00000000..e69de29b
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 00000000..ae85d251
--- /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 5544aa3a..13379f66 100644
--- a/client/src/app/(main)/layout.module.scss
+++ b/client/src/app/(main)/layout.module.scss
@@ -1,29 +1,30 @@
.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;
-}
-.content_zone {
- padding-left: 88px;
+ padding: 48px 55px;
+ @media (width <= 1120px) {
+ padding: 48px 24px;
+ }
- @media screen and (max-width: 768px) {
- padding-left: 0;
+ @media (width <= 580px) {
+ padding: 24px;
}
}
+
+.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 f487f025..d3594de7 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 (
);
diff --git a/client/src/app/(main)/ui/cards/cards.module.scss b/client/src/app/(main)/ui/cards/cards.module.scss
index e45207e2..992bcb8b 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 839cf975..29e99a91 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 00000000..69448c90
--- /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 00000000..b60c92fe
--- /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 00000000..f2131c8b
--- /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 00000000..f5abecea
--- /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 00000000..8e9ab6aa
--- /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 00000000..b366a359
--- /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 00000000..d1e76b3a
--- /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 00000000..eb9bd506
--- /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 251e2512..5777ed8c 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 00000000..cc98d227
--- /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 00000000..ab3877b7
--- /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 72e46522..2a706c43 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/image-loader/image-loader.module.scss b/client/src/shared/ui/image-loader/image-loader.module.scss
index 4863c1bb..0e23a780 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/skeleton/skeleton.tsx b/client/src/shared/ui/skeleton/skeleton.tsx
index 209328a1..e0fb0385 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 377d8d71..103aaf5d 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 3222a596..734c2a46 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 && (
+
+ )}