-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #67 from game-node-app/dev
Dev
- Loading branch information
Showing
10 changed files
with
362 additions
and
76 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,53 @@ | ||
import { | ||
CancelablePromise, | ||
FollowInfoRequestDto, | ||
FollowInfoResponseDto, | ||
FollowService, | ||
} from "@/wrapper/server"; | ||
import { | ||
keepPreviousData, | ||
useInfiniteQuery, | ||
useQuery, | ||
useQueryClient, | ||
} from "@tanstack/react-query"; | ||
import { ExtendedUseInfiniteQueryResult } from "@/util/types/ExtendedUseQueryResult"; | ||
|
||
export function useInfiniteFollowInfo( | ||
dto: Omit<FollowInfoRequestDto, "offset">, | ||
): ExtendedUseInfiniteQueryResult<FollowInfoResponseDto> { | ||
const limit = dto.limit || 10; | ||
const queryClient = useQueryClient(); | ||
const queryKey = ["user", "follow", "info", dto]; | ||
const invalidate = () => { | ||
queryClient.invalidateQueries({ | ||
queryKey, | ||
}); | ||
queryClient.resetQueries({ | ||
queryKey, | ||
}); | ||
}; | ||
return { | ||
...useInfiniteQuery({ | ||
queryKey, | ||
queryFn: async ({ pageParam }) => { | ||
return FollowService.followControllerGetFollowInfo({ | ||
...dto, | ||
offset: pageParam, | ||
}) as CancelablePromise<FollowInfoResponseDto>; | ||
}, | ||
initialPageParam: 0, | ||
getNextPageParam: ( | ||
lastPage, | ||
allPages, | ||
lastPageParam, | ||
allPageParams, | ||
) => { | ||
return limit + lastPageParam; | ||
}, | ||
placeholderData: keepPreviousData, | ||
staleTime: Infinity, | ||
}), | ||
invalidate, | ||
queryKey, | ||
}; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,82 @@ | ||
import React from "react"; | ||
import useUserId from "@/components/auth/hooks/useUserId"; | ||
import { useFollowStatus } from "@/components/follow/hooks/useFollowStatus"; | ||
import { useMutation } from "@tanstack/react-query"; | ||
import { FollowInfoRequestDto, FollowService } from "@/wrapper/server"; | ||
import { useInfiniteFollowInfo } from "@/components/follow/hooks/useInfiniteFollowInfo"; | ||
import criteria = FollowInfoRequestDto.criteria; | ||
import { ActionIcon, Button, Group, Tooltip } from "@mantine/core"; | ||
import { IconX } from "@tabler/icons-react"; | ||
|
||
interface Props { | ||
targetUserId: string; | ||
withUnfollowButton?: boolean; | ||
} | ||
|
||
const UserFollowActions = ({ | ||
targetUserId, | ||
withUnfollowButton = true, | ||
}: Props) => { | ||
const ownUserId = useUserId(); | ||
/* | ||
Checks if current logged-in user is following target user | ||
*/ | ||
const ownToTargetFollowStatus = useFollowStatus(ownUserId, targetUserId); | ||
const isFollowing = ownToTargetFollowStatus.data?.isFollowing ?? false; | ||
|
||
const shouldShowFollowButton = | ||
ownUserId != undefined && ownUserId !== targetUserId; | ||
|
||
const followMutation = useMutation({ | ||
mutationFn: async (action: "register" | "remove") => { | ||
if (action === "register") { | ||
if (isFollowing) return; | ||
|
||
await FollowService.followControllerRegisterFollow({ | ||
followedUserId: targetUserId, | ||
}); | ||
|
||
return; | ||
} | ||
|
||
await FollowService.followControllerRemoveFollow({ | ||
followedUserId: targetUserId, | ||
}); | ||
}, | ||
onSettled: () => { | ||
ownToTargetFollowStatus.invalidate(); | ||
}, | ||
}); | ||
|
||
if (!shouldShowFollowButton) return null; | ||
|
||
return ( | ||
<Group wrap={"nowrap"}> | ||
<Button | ||
disabled={isFollowing} | ||
loading={followMutation.isPending} | ||
onClick={() => { | ||
followMutation.mutate("register"); | ||
}} | ||
> | ||
{isFollowing ? "Following" : "Follow"} | ||
</Button> | ||
{isFollowing && withUnfollowButton && ( | ||
<Tooltip label={"Unfollow this user"}> | ||
<ActionIcon | ||
loading={followMutation.isPending} | ||
variant="default" | ||
size="lg" | ||
onClick={() => { | ||
followMutation.mutate("remove"); | ||
}} | ||
> | ||
<IconX color="red" /> | ||
</ActionIcon> | ||
</Tooltip> | ||
)} | ||
</Group> | ||
); | ||
}; | ||
|
||
export default UserFollowActions; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,28 @@ | ||
import React from "react"; | ||
import { Group, GroupProps } from "@mantine/core"; | ||
import { UserAvatarGroup } from "@/components/general/input/UserAvatarGroup"; | ||
import ProfileFollowActions from "@/components/profile/view/ProfileFollowActions"; | ||
import UserFollowActions from "@/components/follow/input/UserFollowActions"; | ||
|
||
interface Props extends GroupProps { | ||
userId: string; | ||
} | ||
|
||
const UserFollowGroup = ({ userId, ...groupProps }: Props) => { | ||
return ( | ||
<Group className={"w-full justify-between flex-nowrap"} {...groupProps}> | ||
<UserAvatarGroup | ||
userId={userId} | ||
groupProps={{ | ||
wrap: "nowrap", | ||
}} | ||
/> | ||
<UserFollowActions | ||
targetUserId={userId} | ||
withUnfollowButton={false} | ||
/> | ||
</Group> | ||
); | ||
}; | ||
|
||
export default UserFollowGroup; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,94 @@ | ||
import React, { useCallback, useEffect, useMemo, useState } from "react"; | ||
import { FollowInfoRequestDto, PaginationInfo } from "@/wrapper/server"; | ||
import { Divider, Group, Skeleton, Stack, Text } from "@mantine/core"; | ||
import { useInfiniteFollowInfo } from "@/components/follow/hooks/useInfiniteFollowInfo"; | ||
import UserFollowGroup from "@/components/follow/input/UserFollowGroup"; | ||
import { useIntersection } from "@mantine/hooks"; | ||
import { TBasePaginationRequest } from "@/util/types/pagination"; | ||
|
||
interface Props { | ||
criteria: FollowInfoRequestDto.criteria; | ||
targetUserId: string; | ||
} | ||
|
||
const DEFAULT_LIMIT = 10; | ||
|
||
const FollowInfoList = ({ criteria, targetUserId }: Props) => { | ||
const { entry, ref } = useIntersection({ | ||
threshold: 1, | ||
root: document.getElementById(`${criteria}-intersection-root`), | ||
}); | ||
|
||
const { data, fetchNextPage, isLoading, isFetching, isError } = | ||
useInfiniteFollowInfo({ | ||
criteria, | ||
targetUserId, | ||
limit: 10, | ||
orderBy: { | ||
createdAt: "DESC", | ||
}, | ||
}); | ||
const items = useMemo(() => { | ||
if (data == undefined) return null; | ||
const userIds = data.pages.flatMap((response) => { | ||
return response.data; | ||
}); | ||
return userIds.map((userId) => { | ||
return <UserFollowGroup key={userId} userId={userId} mb={"sm"} />; | ||
}); | ||
}, [data]); | ||
|
||
const isEmpty = !isLoading && (items == undefined || items.length === 0); | ||
|
||
const buildItemsSkeletons = useCallback(() => { | ||
return new Array(5).fill(0).map((v, i) => { | ||
return ( | ||
<Group key={i} mb={"sm"}> | ||
<Skeleton className={"rounded-xl h-9 w-9"} /> | ||
<Skeleton className={"h-6 w-1/2"} /> | ||
</Group> | ||
); | ||
}); | ||
}, []); | ||
|
||
useEffect(() => { | ||
const minimumIntersectionTime = 3000; | ||
const lastElement = data?.pages[data?.pages.length - 1]; | ||
const hasNextPage = lastElement?.pagination.hasNextPage ?? false; | ||
const canFetchNextPage = | ||
lastElement && hasNextPage && !isLoading && !isFetching && !isError; | ||
|
||
if ( | ||
canFetchNextPage && | ||
entry?.isIntersecting && | ||
entry.time > minimumIntersectionTime | ||
) { | ||
fetchNextPage().then(); | ||
} | ||
}, [ | ||
entry, | ||
isLoading, | ||
isFetching, | ||
isError, | ||
fetchNextPage, | ||
isEmpty, | ||
data?.pages, | ||
]); | ||
|
||
return ( | ||
<Stack w={"100%"} id={`${criteria}-intersection-root`}> | ||
{isEmpty && ( | ||
<Text className={"text-center"} c={"red"}> | ||
{criteria === "following" | ||
? "User is not following anyone." | ||
: "User has no followers."} | ||
</Text> | ||
)} | ||
{items} | ||
{(isLoading || isFetching) && buildItemsSkeletons()} | ||
{!isEmpty && <div id={"last-element-tracker"} ref={ref} />} | ||
</Stack> | ||
); | ||
}; | ||
|
||
export default FollowInfoList; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,35 @@ | ||
import React from "react"; | ||
import { Modal, ScrollArea } from "@mantine/core"; | ||
import { BaseModalProps } from "@/util/types/modal-props"; | ||
import FollowInfoList from "@/components/follow/list/FollowInfoList"; | ||
import { FollowInfoRequestDto } from "@/wrapper/server"; | ||
import useUserProfile from "@/components/profile/hooks/useUserProfile"; | ||
import CenteredLoading from "@/components/general/CenteredLoading"; | ||
|
||
interface Props extends BaseModalProps { | ||
targetUserId: string; | ||
criteria: FollowInfoRequestDto.criteria; | ||
} | ||
|
||
const FollowInfoListModal = ({ | ||
opened, | ||
onClose, | ||
targetUserId, | ||
criteria, | ||
}: Props) => { | ||
const title = criteria === "followers" ? `Followers` : `Following`; | ||
return ( | ||
<Modal opened={opened} onClose={onClose} title={title}> | ||
<Modal.Body p={0}> | ||
<ScrollArea h={400}> | ||
<FollowInfoList | ||
criteria={criteria} | ||
targetUserId={targetUserId} | ||
/> | ||
</ScrollArea> | ||
</Modal.Body> | ||
</Modal> | ||
); | ||
}; | ||
|
||
export default FollowInfoListModal; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.