mirror of
https://github.com/aaronpo97/the-biergarten-app.git
synced 2026-02-16 10:42:08 +00:00
Feat: add user header with follow info
This commit is contained in:
@@ -19,7 +19,7 @@ const UserAvatar: FC<UserAvatarProps> = ({ user }) => {
|
||||
alt="user avatar"
|
||||
width={1000}
|
||||
height={1000}
|
||||
className="h-full w-full"
|
||||
className="h-full w-full object-cover mask mask-circle ring ring-primary ring-offset-base-100 ring-offset-2"
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -31,7 +31,7 @@ const CommentCardBody: FC<CommentCardProps> = ({
|
||||
return (
|
||||
<div ref={ref} className="flex">
|
||||
<div className="w-[12%] py-4 justify-center">
|
||||
<div className="px-1 mask mask-circle">
|
||||
<div className="px-1">
|
||||
<UserAvatar user={comment.postedBy} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
import FollowInfoSchema from '@/services/UserFollows/schema/FollowInfoSchema';
|
||||
import APIResponseValidationSchema from '@/validation/APIResponseValidationSchema';
|
||||
import useSWRInfinite from 'swr/infinite';
|
||||
import { z } from 'zod';
|
||||
|
||||
const useGetUsersFollowingUser = ({
|
||||
pageSize,
|
||||
userId,
|
||||
}: {
|
||||
pageSize: number;
|
||||
userId: string;
|
||||
}) => {
|
||||
const fetcher = async (url: string) => {
|
||||
const response = await fetch(url);
|
||||
if (!response.ok) {
|
||||
throw new Error(response.statusText);
|
||||
}
|
||||
|
||||
const json = await response.json();
|
||||
const count = response.headers.get('X-Total-Count');
|
||||
|
||||
const parsed = APIResponseValidationSchema.safeParse(json);
|
||||
if (!parsed.success) {
|
||||
throw new Error('API response validation failed');
|
||||
}
|
||||
|
||||
const parsedPayload = z.array(FollowInfoSchema).safeParse(parsed.data.payload);
|
||||
if (!parsedPayload.success) {
|
||||
throw new Error('API response validation failed');
|
||||
}
|
||||
|
||||
const pageCount = Math.ceil(parseInt(count as string, 10) / pageSize);
|
||||
|
||||
return { following: parsedPayload.data, pageCount, followingCount: count };
|
||||
};
|
||||
|
||||
const { data, error, isLoading, setSize, size } = useSWRInfinite(
|
||||
(index) =>
|
||||
`/api/users/${userId}/following?page_num=${index + 1}&page_size=${pageSize}`,
|
||||
fetcher,
|
||||
{ parallel: true },
|
||||
);
|
||||
|
||||
const following = data?.flatMap((d) => d.following) ?? [];
|
||||
const followingCount = data?.[0].followingCount ?? 0;
|
||||
|
||||
const pageCount = data?.[0].pageCount ?? 0;
|
||||
const isLoadingMore = size > 0 && data && typeof data[size - 1] === 'undefined';
|
||||
const isAtEnd = !(size < data?.[0].pageCount!);
|
||||
|
||||
return {
|
||||
following,
|
||||
followingCount,
|
||||
pageCount,
|
||||
size,
|
||||
setSize,
|
||||
isLoading,
|
||||
isLoadingMore,
|
||||
isAtEnd,
|
||||
error: error as unknown,
|
||||
};
|
||||
};
|
||||
|
||||
export default useGetUsersFollowingUser;
|
||||
@@ -0,0 +1,64 @@
|
||||
import FollowInfoSchema from '@/services/UserFollows/schema/FollowInfoSchema';
|
||||
import APIResponseValidationSchema from '@/validation/APIResponseValidationSchema';
|
||||
import useSWRInfinite from 'swr/infinite';
|
||||
import { z } from 'zod';
|
||||
|
||||
const useGetUsersFollowingUser = ({
|
||||
pageSize,
|
||||
userId,
|
||||
}: {
|
||||
pageSize: number;
|
||||
userId: string;
|
||||
}) => {
|
||||
const fetcher = async (url: string) => {
|
||||
const response = await fetch(url);
|
||||
if (!response.ok) {
|
||||
throw new Error(response.statusText);
|
||||
}
|
||||
|
||||
const json = await response.json();
|
||||
const count = response.headers.get('X-Total-Count');
|
||||
|
||||
const parsed = APIResponseValidationSchema.safeParse(json);
|
||||
if (!parsed.success) {
|
||||
throw new Error('API response validation failed');
|
||||
}
|
||||
|
||||
const parsedPayload = z.array(FollowInfoSchema).safeParse(parsed.data.payload);
|
||||
if (!parsedPayload.success) {
|
||||
throw new Error('API response validation failed');
|
||||
}
|
||||
|
||||
const pageCount = Math.ceil(parseInt(count as string, 10) / pageSize);
|
||||
|
||||
return { followers: parsedPayload.data, pageCount, followerCount: count };
|
||||
};
|
||||
|
||||
const { data, error, isLoading, setSize, size } = useSWRInfinite(
|
||||
(index) =>
|
||||
`/api/users/${userId}/followers?page_num=${index + 1}&page_size=${pageSize}`,
|
||||
fetcher,
|
||||
{ parallel: true },
|
||||
);
|
||||
|
||||
const followers = data?.flatMap((d) => d.followers) ?? [];
|
||||
const followerCount = data?.[0].followerCount ?? 0;
|
||||
|
||||
const pageCount = data?.[0].pageCount ?? 0;
|
||||
const isLoadingMore = size > 0 && data && typeof data[size - 1] === 'undefined';
|
||||
const isAtEnd = !(size < data?.[0].pageCount!);
|
||||
|
||||
return {
|
||||
followers,
|
||||
followerCount,
|
||||
pageCount,
|
||||
size,
|
||||
setSize,
|
||||
isLoading,
|
||||
isLoadingMore,
|
||||
isAtEnd,
|
||||
error: error as unknown,
|
||||
};
|
||||
};
|
||||
|
||||
export default useGetUsersFollowingUser;
|
||||
70
src/pages/api/users/[id]/followers.ts
Normal file
70
src/pages/api/users/[id]/followers.ts
Normal file
@@ -0,0 +1,70 @@
|
||||
import { UserExtendedNextApiRequest } from '@/config/auth/types';
|
||||
import NextConnectOptions from '@/config/nextConnect/NextConnectOptions';
|
||||
import validateRequest from '@/config/nextConnect/middleware/validateRequest';
|
||||
import ServerError from '@/config/util/ServerError';
|
||||
import DBClient from '@/prisma/DBClient';
|
||||
import findUserById from '@/services/User/findUserById';
|
||||
import getUsersFollowingUser from '@/services/UserFollows/getUsersFollowingUser';
|
||||
import APIResponseValidationSchema from '@/validation/APIResponseValidationSchema';
|
||||
|
||||
import { NextApiResponse } from 'next';
|
||||
import { createRouter } from 'next-connect';
|
||||
import { z } from 'zod';
|
||||
|
||||
interface GetUserFollowInfoRequest extends UserExtendedNextApiRequest {
|
||||
query: { id: string; page_size: string; page_num: string };
|
||||
}
|
||||
|
||||
const router = createRouter<
|
||||
GetUserFollowInfoRequest,
|
||||
NextApiResponse<z.infer<typeof APIResponseValidationSchema>>
|
||||
>();
|
||||
|
||||
const getFollowingInfo = async (
|
||||
req: GetUserFollowInfoRequest,
|
||||
res: NextApiResponse<z.infer<typeof APIResponseValidationSchema>>,
|
||||
) => {
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
const { id, page_num, page_size } = req.query;
|
||||
|
||||
const user = await findUserById(id);
|
||||
if (!user) {
|
||||
throw new ServerError('User not found', 404);
|
||||
}
|
||||
|
||||
const pageNum = parseInt(page_num, 10);
|
||||
const pageSize = parseInt(page_size, 10);
|
||||
|
||||
const following = await getUsersFollowingUser({
|
||||
userId: id,
|
||||
pageNum,
|
||||
pageSize,
|
||||
});
|
||||
const followingCount = await DBClient.instance.userFollow.count({
|
||||
where: { following: { id } },
|
||||
});
|
||||
|
||||
res.setHeader('X-Total-Count', followingCount);
|
||||
|
||||
res.json({
|
||||
message: 'Retrieved users that are followed by queried user',
|
||||
payload: following,
|
||||
success: true,
|
||||
statusCode: 200,
|
||||
});
|
||||
};
|
||||
|
||||
router.get(
|
||||
validateRequest({
|
||||
querySchema: z.object({
|
||||
id: z.string().cuid(),
|
||||
page_size: z.string().regex(/^\d+$/),
|
||||
page_num: z.string().regex(/^\d+$/),
|
||||
}),
|
||||
}),
|
||||
getFollowingInfo,
|
||||
);
|
||||
|
||||
const handler = router.handler(NextConnectOptions);
|
||||
|
||||
export default handler;
|
||||
70
src/pages/api/users/[id]/following.ts
Normal file
70
src/pages/api/users/[id]/following.ts
Normal file
@@ -0,0 +1,70 @@
|
||||
import { UserExtendedNextApiRequest } from '@/config/auth/types';
|
||||
import NextConnectOptions from '@/config/nextConnect/NextConnectOptions';
|
||||
import validateRequest from '@/config/nextConnect/middleware/validateRequest';
|
||||
import ServerError from '@/config/util/ServerError';
|
||||
import DBClient from '@/prisma/DBClient';
|
||||
import findUserById from '@/services/User/findUserById';
|
||||
import getUsersFollowedByUser from '@/services/UserFollows/getUsersFollowedByUser';
|
||||
import APIResponseValidationSchema from '@/validation/APIResponseValidationSchema';
|
||||
|
||||
import { NextApiResponse } from 'next';
|
||||
import { createRouter } from 'next-connect';
|
||||
import { z } from 'zod';
|
||||
|
||||
interface GetUserFollowInfoRequest extends UserExtendedNextApiRequest {
|
||||
query: { id: string; page_size: string; page_num: string };
|
||||
}
|
||||
|
||||
const router = createRouter<
|
||||
GetUserFollowInfoRequest,
|
||||
NextApiResponse<z.infer<typeof APIResponseValidationSchema>>
|
||||
>();
|
||||
|
||||
const getFollowingInfo = async (
|
||||
req: GetUserFollowInfoRequest,
|
||||
res: NextApiResponse<z.infer<typeof APIResponseValidationSchema>>,
|
||||
) => {
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
const { id, page_num, page_size } = req.query;
|
||||
|
||||
const user = await findUserById(id);
|
||||
if (!user) {
|
||||
throw new ServerError('User not found', 404);
|
||||
}
|
||||
|
||||
const pageNum = parseInt(page_num, 10);
|
||||
const pageSize = parseInt(page_size, 10);
|
||||
|
||||
const following = await getUsersFollowedByUser({
|
||||
userId: id,
|
||||
pageNum,
|
||||
pageSize,
|
||||
});
|
||||
const followingCount = await DBClient.instance.userFollow.count({
|
||||
where: { follower: { id } },
|
||||
});
|
||||
|
||||
res.setHeader('X-Total-Count', followingCount);
|
||||
|
||||
res.json({
|
||||
message: 'Retrieved users that are followed by queried user',
|
||||
payload: following,
|
||||
success: true,
|
||||
statusCode: 200,
|
||||
});
|
||||
};
|
||||
|
||||
router.get(
|
||||
validateRequest({
|
||||
querySchema: z.object({
|
||||
id: z.string().cuid(),
|
||||
page_size: z.string().regex(/^\d+$/),
|
||||
page_num: z.string().regex(/^\d+$/),
|
||||
}),
|
||||
}),
|
||||
getFollowingInfo,
|
||||
);
|
||||
|
||||
const handler = router.handler(NextConnectOptions);
|
||||
|
||||
export default handler;
|
||||
@@ -7,7 +7,9 @@ import { GetServerSideProps } from 'next';
|
||||
import Head from 'next/head';
|
||||
import { FC } from 'react';
|
||||
import { z } from 'zod';
|
||||
import Image from 'next/image';
|
||||
import UserAvatar from '@/components/Account/UserAvatar';
|
||||
import useGetUsersFollowedByUser from '@/hooks/data-fetching/user-follows/useGetUsersFollowedByUser';
|
||||
import useGetUsersFollowingUser from '@/hooks/data-fetching/user-follows/useGetUsersFollowingUser';
|
||||
|
||||
interface UserInfoPageProps {
|
||||
user: z.infer<typeof GetUserSchema>;
|
||||
@@ -16,17 +18,33 @@ interface UserInfoPageProps {
|
||||
const UserHeader: FC<{ user: z.infer<typeof GetUserSchema> }> = ({ user }) => {
|
||||
const timeDistance = useTimeDistance(new Date(user.createdAt));
|
||||
|
||||
return (
|
||||
<article className="card flex flex-col justify-center bg-base-300">
|
||||
<div className="card-body">
|
||||
<header className="flex justify-between">
|
||||
<div className="space-y-2">
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold lg:text-4xl">
|
||||
{user.firstName} {user.lastName}
|
||||
</h1>
|
||||
const { followingCount } = useGetUsersFollowedByUser({
|
||||
userId: user.id,
|
||||
pageSize: 10,
|
||||
});
|
||||
|
||||
<h3 className="italic">
|
||||
const { followerCount } = useGetUsersFollowingUser({
|
||||
userId: user.id,
|
||||
pageSize: 10,
|
||||
});
|
||||
|
||||
return (
|
||||
<header className="card text-center items-center">
|
||||
<div className="card-body items-center w-full">
|
||||
<div className="w-40 h-40">
|
||||
<UserAvatar user={user} />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold lg:text-4xl">{user.username}</h1>
|
||||
</div>
|
||||
|
||||
<div className="flex space-x-3 text-lg font-bold">
|
||||
<span>{followingCount} Following</span>
|
||||
<span>{followerCount} Followers</span>
|
||||
</div>
|
||||
|
||||
<span className="italic">
|
||||
joined{' '}
|
||||
{timeDistance && (
|
||||
<span
|
||||
@@ -36,30 +54,27 @@ const UserHeader: FC<{ user: z.infer<typeof GetUserSchema> }> = ({ user }) => {
|
||||
{`${timeDistance} ago`}
|
||||
</span>
|
||||
)}
|
||||
</h3>
|
||||
</div>
|
||||
</span>
|
||||
</div>
|
||||
</header>
|
||||
</div>
|
||||
</article>
|
||||
);
|
||||
};
|
||||
|
||||
const UserInfoPage: FC<UserInfoPageProps> = ({ user }) => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const isDesktop = useMediaQuery('(min-width: 1024px)');
|
||||
const title = `${user.username} | The Biergarten App`;
|
||||
|
||||
return (
|
||||
<>
|
||||
<Head>
|
||||
<title>{user ? `${user.firstName} ${user.lastName}` : 'User Info'}</title>
|
||||
<title>{title}</title>
|
||||
<meta name="description" content="User information" />
|
||||
</Head>
|
||||
<>
|
||||
<main className="mb-12 mt-10 flex w-full items-center justify-center">
|
||||
<Image src={user.userAvatar!.path} alt="avatar" width={200} height={200} />
|
||||
<div className="w-11/12 space-y-3 xl:w-9/12 2xl:w-8/12">
|
||||
<UserHeader user={user} />
|
||||
|
||||
{isDesktop ? <></> : <> </>}
|
||||
</div>
|
||||
</main>
|
||||
</>
|
||||
|
||||
@@ -24,20 +24,15 @@ const createNewUserFollows = async ({
|
||||
const randomUsers = users
|
||||
.filter((randomUser) => randomUser.id !== user.id)
|
||||
.sort(() => Math.random() - Math.random())
|
||||
.slice(0, 20);
|
||||
.slice(0, 100);
|
||||
|
||||
// Get the user to follow the random users, and the random users to follow the user.
|
||||
// Get the user to follow the random users
|
||||
const data = randomUsers.flatMap((randomUser) => [
|
||||
{
|
||||
followerId: user.id,
|
||||
followingId: randomUser.id,
|
||||
followedAt: faker.date.between({ from: user.createdAt, to: new Date() }),
|
||||
},
|
||||
{
|
||||
followerId: randomUser.id,
|
||||
followingId: user.id,
|
||||
followedAt: faker.date.between({ from: randomUser.createdAt, to: new Date() }),
|
||||
},
|
||||
]);
|
||||
|
||||
userFollows.push(...data);
|
||||
|
||||
27
src/services/UserFollows/getUsersFollowedByUser.ts
Normal file
27
src/services/UserFollows/getUsersFollowedByUser.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import DBClient from '@/prisma/DBClient';
|
||||
import { z } from 'zod';
|
||||
import FollowInfoSchema from './schema/FollowInfoSchema';
|
||||
|
||||
interface GetFollowingInfoByUserIdArgs {
|
||||
userId: string;
|
||||
pageNum: number;
|
||||
pageSize: number;
|
||||
}
|
||||
const getUsersFollowedByUser = async ({
|
||||
userId,
|
||||
pageNum,
|
||||
pageSize,
|
||||
}: GetFollowingInfoByUserIdArgs): Promise<z.infer<typeof FollowInfoSchema>[]> => {
|
||||
const usersFollowedByQueriedUser = await DBClient.instance.userFollow.findMany({
|
||||
take: pageSize,
|
||||
skip: (pageNum - 1) * pageSize,
|
||||
where: { following: { id: userId } },
|
||||
select: {
|
||||
follower: { select: { username: true, userAvatar: true, id: true } },
|
||||
},
|
||||
});
|
||||
|
||||
return usersFollowedByQueriedUser.map((u) => u.follower);
|
||||
};
|
||||
|
||||
export default getUsersFollowedByUser;
|
||||
27
src/services/UserFollows/getUsersFollowingUser.ts
Normal file
27
src/services/UserFollows/getUsersFollowingUser.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import DBClient from '@/prisma/DBClient';
|
||||
import { z } from 'zod';
|
||||
import FollowInfoSchema from './schema/FollowInfoSchema';
|
||||
|
||||
interface GetFollowingInfoByUserIdArgs {
|
||||
userId: string;
|
||||
pageNum: number;
|
||||
pageSize: number;
|
||||
}
|
||||
const getUsersFollowingUser = async ({
|
||||
userId,
|
||||
pageNum,
|
||||
pageSize,
|
||||
}: GetFollowingInfoByUserIdArgs): Promise<z.infer<typeof FollowInfoSchema>[]> => {
|
||||
const usersFollowingQueriedUser = await DBClient.instance.userFollow.findMany({
|
||||
take: pageSize,
|
||||
skip: (pageNum - 1) * pageSize,
|
||||
where: { follower: { id: userId } },
|
||||
select: {
|
||||
following: { select: { username: true, userAvatar: true, id: true } },
|
||||
},
|
||||
});
|
||||
|
||||
return usersFollowingQueriedUser.map((u) => u.following);
|
||||
};
|
||||
|
||||
export default getUsersFollowingUser;
|
||||
9
src/services/UserFollows/schema/FollowInfoSchema.ts
Normal file
9
src/services/UserFollows/schema/FollowInfoSchema.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import GetUserSchema from '@/services/User/schema/GetUserSchema';
|
||||
|
||||
const FollowInfoSchema = GetUserSchema.pick({
|
||||
userAvatar: true,
|
||||
id: true,
|
||||
username: true,
|
||||
});
|
||||
|
||||
export default FollowInfoSchema;
|
||||
Reference in New Issue
Block a user