mirror of
https://github.com/Radiquum/AniX.git
synced 2025-04-05 15:54:39 +00:00
feat: add profile friend view
This commit is contained in:
parent
0730b7c7d4
commit
256ecea885
5 changed files with 441 additions and 59 deletions
|
@ -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: {
|
||||
|
|
|
@ -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" && <ProfileActivityFriends content={props.friendsPreview || []} />}
|
||||
{tab == "friends" && (
|
||||
<ProfileActivityFriends
|
||||
token={props.token}
|
||||
content={props.friendsPreview || []}
|
||||
isMyProfile={props.isMyProfile}
|
||||
profile_id={props.profile_id}
|
||||
/>
|
||||
)}
|
||||
{tab == "videos" && <>videos</>}
|
||||
</Card>
|
||||
);
|
||||
|
|
|
@ -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 (
|
||||
<div className="max-w-full">
|
||||
<Swiper
|
||||
modules={[Navigation, Mousewheel, Scrollbar]}
|
||||
spaceBetween={8}
|
||||
slidesPerView={"auto"}
|
||||
direction={"horizontal"}
|
||||
mousewheel={{
|
||||
enabled: true,
|
||||
sensitivity: 4,
|
||||
}}
|
||||
scrollbar={{
|
||||
enabled: true,
|
||||
draggable: true,
|
||||
}}
|
||||
allowTouchMove={true}
|
||||
style={
|
||||
{
|
||||
"--swiper-scrollbar-bottom": "0",
|
||||
} as React.CSSProperties
|
||||
}
|
||||
>
|
||||
{props.content &&
|
||||
props.content.length > 0 &&
|
||||
props.content.map((profile) => {
|
||||
return (
|
||||
<SwiperSlide
|
||||
key={`friend-prev-${profile.id}`}
|
||||
style={{ width: "fit-content" }}
|
||||
className="px-2 py-4"
|
||||
>
|
||||
<Link href={`/profile/${profile.id}`}>
|
||||
<div className="flex items-center gap-2">
|
||||
<Avatar
|
||||
img={profile.avatar}
|
||||
size="md"
|
||||
rounded={true}
|
||||
bordered={true}
|
||||
color={profile.is_online ? "success" : "light"}
|
||||
className="flex-shrink-0"
|
||||
/>
|
||||
<p className="text-lg">{profile.login}</p>
|
||||
</div>
|
||||
</Link>
|
||||
</SwiperSlide>
|
||||
);
|
||||
})}
|
||||
{props.content && props.content.length > 0 ?
|
||||
<SwiperSlide style={{ width: "fit-content" }} className="px-2 py-4">
|
||||
<Button>
|
||||
<p className="text-lg">Все друзья</p>
|
||||
<span className="w-8 h-8 iconify mdi--arrow-right dark:fill-white"></span>
|
||||
</Button>
|
||||
</SwiperSlide>
|
||||
: <p className="text-lg">У пользователя нет друзей</p>}
|
||||
</Swiper>
|
||||
</div>
|
||||
<>
|
||||
<div className="max-w-full">
|
||||
<Swiper
|
||||
modules={[Navigation, Mousewheel, Scrollbar]}
|
||||
spaceBetween={8}
|
||||
slidesPerView={"auto"}
|
||||
direction={"horizontal"}
|
||||
mousewheel={{
|
||||
enabled: true,
|
||||
sensitivity: 4,
|
||||
}}
|
||||
scrollbar={{
|
||||
enabled: true,
|
||||
draggable: true,
|
||||
}}
|
||||
allowTouchMove={true}
|
||||
style={
|
||||
{
|
||||
"--swiper-scrollbar-bottom": "0",
|
||||
} as React.CSSProperties
|
||||
}
|
||||
>
|
||||
{props.content &&
|
||||
props.content.length > 0 &&
|
||||
props.content.map((profile) => {
|
||||
return (
|
||||
<SwiperSlide
|
||||
key={`friend-prev-${profile.id}`}
|
||||
style={{ width: "fit-content" }}
|
||||
className="px-2 py-4"
|
||||
>
|
||||
<Link href={`/profile/${profile.id}`}>
|
||||
<div className="flex items-center gap-2">
|
||||
<Avatar
|
||||
img={profile.avatar}
|
||||
size="md"
|
||||
rounded={true}
|
||||
bordered={true}
|
||||
color={profile.is_online ? "success" : "light"}
|
||||
className="flex-shrink-0"
|
||||
/>
|
||||
<p className="text-lg">{profile.login}</p>
|
||||
</div>
|
||||
</Link>
|
||||
</SwiperSlide>
|
||||
);
|
||||
})}
|
||||
{(props.content && props.content.length > 0) || props.isMyProfile ?
|
||||
<SwiperSlide style={{ width: "fit-content" }} className="px-2 py-4">
|
||||
<Button onClick={() => setIsFriendModalOpen(true)}>
|
||||
<p className="text-lg">Все друзья {props.isMyProfile ? "и заявки" : ""}</p>
|
||||
<span className="w-8 h-8 iconify mdi--arrow-right dark:fill-white"></span>
|
||||
</Button>
|
||||
</SwiperSlide>
|
||||
: <p className="text-lg">У пользователя нет друзей</p>}
|
||||
</Swiper>
|
||||
</div>
|
||||
<ProfileFriendModal
|
||||
isOpen={isFriendModalOpen}
|
||||
setIsOpen={setIsFriendModalOpen}
|
||||
token={props.token}
|
||||
isMyProfile={props.isMyProfile}
|
||||
profile_id={props.profile_id}
|
||||
></ProfileFriendModal>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
346
app/components/Profile/Profile.FriendsModal.tsx
Normal file
346
app/components/Profile/Profile.FriendsModal.tsx
Normal file
|
@ -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<any>(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 (
|
||||
<>
|
||||
<Modal
|
||||
dismissible
|
||||
show={props.isOpen}
|
||||
onClose={() => props.setIsOpen(false)}
|
||||
size={"4xl"}
|
||||
>
|
||||
<ModalHeader>Друзья</ModalHeader>
|
||||
<div
|
||||
className="flex flex-col gap-4 p-4 overflow-y-auto"
|
||||
onScroll={handleScroll}
|
||||
ref={modalRef}
|
||||
>
|
||||
{props.isMyProfile && (
|
||||
<>
|
||||
<div className="flex flex-col gap-2">
|
||||
<p className="text-lg font-semibold">Входящие заявки</p>
|
||||
{(
|
||||
requestInUsersData &&
|
||||
requestInUsersData.content &&
|
||||
requestInUsersData.content.length > 0
|
||||
) ?
|
||||
requestInUsersData.content.map((user) => {
|
||||
return (
|
||||
<div
|
||||
className="flex items-center justify-between gap-2"
|
||||
key={`friend_req_in-${user.id}`}
|
||||
>
|
||||
<Link href={`/profile/${user.id}`}>
|
||||
<div className="flex gap-2">
|
||||
<Avatar
|
||||
alt=""
|
||||
img={user.avatar}
|
||||
rounded={true}
|
||||
size={"md"}
|
||||
bordered={true}
|
||||
color={user.is_online ? "success" : "light"}
|
||||
className="flex-shrink-0"
|
||||
/>
|
||||
<div className="flex flex-col gap-1">
|
||||
<p className="text-lg font-semibold">
|
||||
{user.login}
|
||||
</p>
|
||||
<p>Друзей: {user.friend_count}</p>
|
||||
</div>
|
||||
</div>
|
||||
</Link>
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
color="blue"
|
||||
onClick={() => _acceptRequestIn(user.id)}
|
||||
>
|
||||
Принять
|
||||
</Button>
|
||||
<Button
|
||||
color="light"
|
||||
onClick={() => _hideRequestIn(user.id)}
|
||||
>
|
||||
Скрыть
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})
|
||||
: <p className="text-sm">Нет входящих заявок</p>}
|
||||
</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
<p className="text-lg font-semibold">Исходящие заявки</p>
|
||||
{(
|
||||
requestOutUsersData &&
|
||||
requestOutUsersData.content &&
|
||||
requestOutUsersData.content.length > 0
|
||||
) ?
|
||||
requestOutUsersData.content.map((user) => {
|
||||
return (
|
||||
<div
|
||||
className="flex items-center justify-between gap-2"
|
||||
key={`friend_req_out-${user.id}`}
|
||||
>
|
||||
<Link href={`/profile/${user.id}`}>
|
||||
<div className="flex gap-2">
|
||||
<Avatar
|
||||
alt=""
|
||||
img={user.avatar}
|
||||
rounded={true}
|
||||
size={"md"}
|
||||
bordered={true}
|
||||
color={user.is_online ? "success" : "light"}
|
||||
className="flex-shrink-0"
|
||||
/>
|
||||
<div className="flex flex-col gap-1">
|
||||
<p className="text-lg font-semibold">
|
||||
{user.login}
|
||||
</p>
|
||||
<p>Друзей: {user.friend_count}</p>
|
||||
</div>
|
||||
</div>
|
||||
</Link>
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
color="light"
|
||||
onClick={() => _cancelRequestOut(user.id)}
|
||||
>
|
||||
Отменить
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})
|
||||
: <p className="text-sm">Нет исходящих заявок</p>}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
<div className="flex flex-col gap-2">
|
||||
<p className="text-lg font-semibold">Все друзья</p>
|
||||
{friends && friends.length > 0 ?
|
||||
friends.map((user) => {
|
||||
return (
|
||||
<div
|
||||
className="flex items-center justify-between gap-2"
|
||||
key={`friend-${user.id}`}
|
||||
>
|
||||
<Link href={`/profile/${user.id}`}>
|
||||
<div className="flex gap-2">
|
||||
<Avatar
|
||||
alt=""
|
||||
img={user.avatar}
|
||||
rounded={true}
|
||||
size={"md"}
|
||||
bordered={true}
|
||||
color={user.is_online ? "success" : "light"}
|
||||
className="flex-shrink-0"
|
||||
/>
|
||||
<div className="flex flex-col gap-1">
|
||||
<p className="text-lg font-semibold">{user.login}</p>
|
||||
<p>Друзей: {user.friend_count}</p>
|
||||
</div>
|
||||
</div>
|
||||
</Link>
|
||||
</div>
|
||||
);
|
||||
})
|
||||
: <p className="text-sm">Нет друзей</p>}
|
||||
</div>
|
||||
{isLoading && <Spinner />}
|
||||
</div>
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -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 && (
|
||||
|
|
Loading…
Add table
Reference in a new issue