Skip to content

Commit

Permalink
Merge pull request #67 from game-node-app/dev
Browse files Browse the repository at this point in the history
Dev
  • Loading branch information
Lamarcke authored Apr 7, 2024
2 parents 7b34690 + 62abaef commit cc118af
Show file tree
Hide file tree
Showing 10 changed files with 362 additions and 76 deletions.
53 changes: 53 additions & 0 deletions src/components/follow/hooks/useInfiniteFollowInfo.ts
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,
};
}
82 changes: 82 additions & 0 deletions src/components/follow/input/UserFollowActions.tsx
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;
28 changes: 28 additions & 0 deletions src/components/follow/input/UserFollowGroup.tsx
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;
94 changes: 94 additions & 0 deletions src/components/follow/list/FollowInfoList.tsx
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;
35 changes: 35 additions & 0 deletions src/components/follow/modal/FollowInfoListModal.tsx
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;
2 changes: 1 addition & 1 deletion src/components/game/info/GameInfoScore.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ const GameInfoScore = ({ gameId }: Props) => {
const percentage = (v / total) * 100;
const percentageToUse = Number.isNaN(percentage)
? 0
: percentage;
: Math.trunc(percentage);
const lastElement = index + 1 === arr.length;
return (
<Stack
Expand Down
61 changes: 7 additions & 54 deletions src/components/profile/view/ProfileFollowActions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,78 +15,31 @@ import { useMutation } from "@tanstack/react-query";
import { useFollowersCount } from "@/components/follow/hooks/useFollowersCount";
import { FollowService } from "@/wrapper/server";
import { IconX } from "@tabler/icons-react";
import UserFollowActions from "@/components/follow/input/UserFollowActions";

interface Props {
targetUserId: string;
withUnfollowButton?: boolean;
}

const ProfileFollowActions = ({ targetUserId }: Props) => {
const ProfileFollowActions = ({
targetUserId,
withUnfollowButton = true,
}: Props) => {
const ownUserId = useUserId();
/*
Checks if current logged-in user is following target user
*/
const ownToTargetFollowStatus = useFollowStatus(ownUserId, targetUserId);
const targetToOwnFollowStatus = useFollowStatus(targetUserId, ownUserId);
const followersCountQuery = useFollowersCount(targetUserId);

const isFollowing = ownToTargetFollowStatus.data?.isFollowing ?? false;
const isBeingFollowed = targetToOwnFollowStatus.data?.isFollowing ?? false;
const isFollowedBack = isFollowing && isBeingFollowed;

const followMutation = useMutation({
mutationFn: async (action: "register" | "unregister") => {
if (action === "register" && isFollowing) return;
if (action === "register") {
await FollowService.followControllerRegisterFollow({
followedUserId: targetUserId,
});

return;
}

await FollowService.followControllerRemoveFollow({
followedUserId: targetUserId,
});
},
onSettled: () => {
followersCountQuery.invalidate();
ownToTargetFollowStatus.invalidate();
targetToOwnFollowStatus.invalidate();
},
});

return (
<Stack w={"100%"} align={"center"}>
<Group>
<Button
disabled={isFollowing}
loading={followMutation.isPending}
onClick={() => {
followMutation.mutate("register");
}}
>
{isFollowing
? "Following"
: isBeingFollowed
? "Follow Back"
: "Follow"}
</Button>
{isFollowing && (
<Tooltip label={"Unfollow this user"}>
<ActionIcon
loading={followMutation.isPending}
variant="default"
size="lg"
onClick={() => {
followMutation.mutate("unregister");
}}
>
<IconX color="red" />
</ActionIcon>
</Tooltip>
)}
</Group>

<UserFollowActions targetUserId={targetUserId} />
{isBeingFollowed && !isFollowedBack && (
<Text fz={"0.8rem"} c={"dimmed"}>
This user is following you.
Expand Down
Loading

0 comments on commit cc118af

Please sign in to comment.