From 256ecea8852686436163c61afe783fdb7ad407d4 Mon Sep 17 00:00:00 2001 From: Radiquum Date: Fri, 4 Apr 2025 14:15:47 +0500 Subject: [PATCH] feat: add profile friend view --- app/api/config.ts | 8 + app/components/Profile/Profile.Activity.tsx | 11 +- .../Profile/Profile.ActivityFriends.tsx | 133 ++++--- .../Profile/Profile.FriendsModal.tsx | 346 ++++++++++++++++++ app/pages/Profile.tsx | 2 + 5 files changed, 441 insertions(+), 59 deletions(-) create mode 100644 app/components/Profile/Profile.FriendsModal.tsx diff --git a/app/api/config.ts b/app/api/config.ts index a4fc159..083ce23 100644 --- a/app/api/config.ts +++ b/app/api/config.ts @@ -19,6 +19,14 @@ export const ENDPOINTS = { history: `${API_PREFIX}/history`, favorite: `${API_PREFIX}/favorite`, blocklist: `${API_PREFIX}/profile/blocklist`, + friend: { + list: `${API_PREFIX}/profile/friend/all`, + add: `${API_PREFIX}/profile/friend/request/send`, + remove: `${API_PREFIX}/profile/friend/request/remove`, + hide: `${API_PREFIX}/profile/friend/request/hide`, + in: `${API_PREFIX}/profile/friend/requests/in`, + out: `${API_PREFIX}/profile/friend/requests/out`, + }, settings: { my: `${API_PREFIX}/profile/preference/my`, login: { diff --git a/app/components/Profile/Profile.Activity.tsx b/app/components/Profile/Profile.Activity.tsx index 9438cec..4a349e4 100644 --- a/app/components/Profile/Profile.Activity.tsx +++ b/app/components/Profile/Profile.Activity.tsx @@ -15,6 +15,8 @@ export function ProfileActivity(props: { collectionPreview: any; friendsCount: number; friendsPreview: any; + token: string; + isMyProfile: boolean; }) { const [tab, setTab] = useState< "collections" | "comments" | "friends" | "videos" @@ -91,7 +93,14 @@ export function ProfileActivity(props: { /> )} {tab == "comments" && <>comments} - {tab == "friends" && } + {tab == "friends" && ( + + )} {tab == "videos" && <>videos} ); diff --git a/app/components/Profile/Profile.ActivityFriends.tsx b/app/components/Profile/Profile.ActivityFriends.tsx index 78e2ac2..7f67712 100644 --- a/app/components/Profile/Profile.ActivityFriends.tsx +++ b/app/components/Profile/Profile.ActivityFriends.tsx @@ -4,67 +4,84 @@ import "swiper/css/navigation"; import "swiper/css/mousewheel"; import "swiper/css/scrollbar"; import { Navigation, Mousewheel, Scrollbar } from "swiper/modules"; -import { CollectionLink } from "../CollectionLink/CollectionLink"; import Link from "next/link"; import { Avatar, Button } from "flowbite-react"; +import { useState } from "react"; +import { ProfileFriendModal } from "./Profile.FriendsModal"; + +export const ProfileActivityFriends = (props: { + content: any; + token: string; + isMyProfile: boolean; + profile_id: number; +}) => { + const [isFriendModalOpen, setIsFriendModalOpen] = useState(false); -export const ProfileActivityFriends = (props: { content: any }) => { return ( -
- - {props.content && - props.content.length > 0 && - props.content.map((profile) => { - return ( - - -
- -

{profile.login}

-
- -
- ); - })} - {props.content && props.content.length > 0 ? - - - - :

У пользователя нет друзей

} -
-
+ <> +
+ + {props.content && + props.content.length > 0 && + props.content.map((profile) => { + return ( + + +
+ +

{profile.login}

+
+ +
+ ); + })} + {(props.content && props.content.length > 0) || props.isMyProfile ? + + + + :

У пользователя нет друзей

} +
+
+ + ); }; diff --git a/app/components/Profile/Profile.FriendsModal.tsx b/app/components/Profile/Profile.FriendsModal.tsx new file mode 100644 index 0000000..fed9122 --- /dev/null +++ b/app/components/Profile/Profile.FriendsModal.tsx @@ -0,0 +1,346 @@ +import { ENDPOINTS } from "#/api/config"; +import { tryCatchAPI, unixToDate, useSWRfetcher } from "#/api/utils"; +import { + Avatar, + Button, + Modal, + ModalHeader, + useThemeMode, +} from "flowbite-react"; +import { useCallback, useEffect, useState } from "react"; +import useSWRInfinite from "swr/infinite"; +import { Spinner } from "../Spinner/Spinner"; +import { toast } from "react-toastify"; +import useSWR, { mutate } from "swr"; +import Link from "next/link"; + +export const ProfileFriendModal = (props: { + isOpen: boolean; + setIsOpen: (isOpen: boolean) => void; + token: string; + isMyProfile: boolean; + profile_id: number; +}) => { + const [currentRef, setCurrentRef] = useState(null); + const theme = useThemeMode(); + const [actionsDisabled, setActionsDisabled] = useState(false); + // const [requestInUsers, setRequestInUsers] = useState([]); + // const [requestOutUsers, setRequestOutUsers] = useState([]); + const [friends, setFriends] = useState([]); + + const modalRef = useCallback((ref) => { + setCurrentRef(ref); + }, []); + + const useFetchRequests = (url: string) => { + const { data, error, isLoading } = useSWR(url, useSWRfetcher); + return [data, error, isLoading]; + }; + + const [requestInUsersData, requestInUsersError, requestInUsersIsLoading] = + useFetchRequests( + props.isMyProfile ? + `${ENDPOINTS.user.friend.in}/last?token=${props.token}&count=8` + : "" + ); + + const [requestOutUsersData, requestOutUsersError, requestOutUsersIsLoading] = + useFetchRequests( + props.isMyProfile ? + `${ENDPOINTS.user.friend.out}/last?token=${props.token}&count=8` + : "" + ); + + async function _hideRequestIn(profile_id) { + const tid = toast.loading("Скрываем заявку...", { + position: "bottom-center", + hideProgressBar: true, + closeOnClick: false, + pauseOnHover: false, + draggable: false, + theme: theme.mode == "light" ? "light" : "dark", + }); + + let url = `${ENDPOINTS.user.friend.hide}/${profile_id}?token=${props.token}`; + const { data, error } = await tryCatchAPI(fetch(url)); + if (error) { + toast.update(tid, { + render: "Ошибка скрытия заявки", + type: "error", + autoClose: 2500, + isLoading: false, + closeOnClick: true, + draggable: true, + }); + return; + } + + toast.update(tid, { + render: "Заявка скрыта", + type: "success", + autoClose: 2500, + isLoading: false, + closeOnClick: true, + draggable: true, + }); + mutate(`${ENDPOINTS.user.friend.in}/last?token=${props.token}&count=8`); + } + + async function _acceptRequestIn(profile_id) { + const tid = toast.loading("Принимаем запрос...", { + position: "bottom-center", + hideProgressBar: true, + closeOnClick: false, + pauseOnHover: false, + draggable: false, + theme: theme.mode == "light" ? "light" : "dark", + }); + + let url = `${ENDPOINTS.user.friend.add}/${profile_id}?token=${props.token}`; + const { data, error } = await tryCatchAPI(fetch(url)); + if (error) { + toast.update(tid, { + render: "Ошибка приёма запроса", + type: "error", + autoClose: 2500, + isLoading: false, + closeOnClick: true, + draggable: true, + }); + return; + } + + toast.update(tid, { + render: "Запрос принят", + type: "success", + autoClose: 2500, + isLoading: false, + closeOnClick: true, + draggable: true, + }); + mutate(`${ENDPOINTS.user.friend.in}/last?token=${props.token}&count=8`); + } + + async function _cancelRequestOut(profile_id) { + const tid = toast.loading("Отменяем запрос...", { + position: "bottom-center", + hideProgressBar: true, + closeOnClick: false, + pauseOnHover: false, + draggable: false, + theme: theme.mode == "light" ? "light" : "dark", + }); + + let url = `${ENDPOINTS.user.friend.remove}/${profile_id}?token=${props.token}`; + const { data, error } = await tryCatchAPI(fetch(url)); + if (error) { + toast.update(tid, { + render: "Ошибка отмена запроса", + type: "error", + autoClose: 2500, + isLoading: false, + closeOnClick: true, + draggable: true, + }); + return; + } + + toast.update(tid, { + render: "Запрос отменён", + type: "success", + autoClose: 2500, + isLoading: false, + closeOnClick: true, + draggable: true, + }); + mutate(`${ENDPOINTS.user.friend.out}/last?token=${props.token}&count=8`); + } + + const getKey = (pageIndex: number, previousPageData: any) => { + if (previousPageData && !previousPageData.content.length) return null; + let url = `${ENDPOINTS.user.friend.list}/${props.profile_id}/${pageIndex}?token=${props.token}`; + return url; + }; + + const { data, error, isLoading, size, setSize } = useSWRInfinite( + getKey, + useSWRfetcher, + { initialSize: 2 } + ); + + useEffect(() => { + if (data) { + let allFriends = []; + for (let i = 0; i < data.length; i++) { + allFriends.push(...data[i].content); + } + setFriends(allFriends); + } + }, [data]); + + const [scrollPosition, setScrollPosition] = useState(0); + function handleScroll() { + const height = currentRef.scrollHeight - currentRef.clientHeight; + const windowScroll = currentRef.scrollTop; + const scrolled = (windowScroll / height) * 100; + setScrollPosition(Math.floor(scrolled)); + } + + useEffect(() => { + if (scrollPosition >= 95 && scrollPosition <= 96) { + setSize(size + 1); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [scrollPosition]); + + return ( + <> + props.setIsOpen(false)} + size={"4xl"} + > + Друзья +
+ {props.isMyProfile && ( + <> +
+

Входящие заявки

+ {( + requestInUsersData && + requestInUsersData.content && + requestInUsersData.content.length > 0 + ) ? + requestInUsersData.content.map((user) => { + return ( +
+ +
+ +
+

+ {user.login} +

+

Друзей: {user.friend_count}

+
+
+ +
+ + +
+
+ ); + }) + :

Нет входящих заявок

} +
+
+

Исходящие заявки

+ {( + requestOutUsersData && + requestOutUsersData.content && + requestOutUsersData.content.length > 0 + ) ? + requestOutUsersData.content.map((user) => { + return ( +
+ +
+ +
+

+ {user.login} +

+

Друзей: {user.friend_count}

+
+
+ +
+ +
+
+ ); + }) + :

Нет исходящих заявок

} +
+ + )} +
+

Все друзья

+ {friends && friends.length > 0 ? + friends.map((user) => { + return ( +
+ +
+ +
+

{user.login}

+

Друзей: {user.friend_count}

+
+
+ +
+ ); + }) + :

Нет друзей

} +
+ {isLoading && } +
+
+ + ); +}; diff --git a/app/pages/Profile.tsx b/app/pages/Profile.tsx index 5721be6..d5ee3d2 100644 --- a/app/pages/Profile.tsx +++ b/app/pages/Profile.tsx @@ -134,6 +134,8 @@ export const ProfilePage = (props: any) => { collectionPreview={user.collections_preview || []} friendsCount={user.friend_count} friendsPreview={user.friends_preview || []} + token={authUser.token} + isMyProfile={isMyProfile || false} /> )} {!user.is_stats_hidden && (