Merge pull request #58 from aaronpo97/dev

feat: add user functionality (profiles, avatars)
This commit is contained in:
Aaron Po
2023-11-20 14:58:00 -05:00
committed by GitHub
68 changed files with 3183 additions and 2633 deletions

View File

@@ -163,7 +163,7 @@ SPARKPOST_SENDER_ADDRESS=" > .env
database used for migrations. database used for migrations.
- `SHADOW_DATABASE_URL` is a connection string for a secondary database used for - `SHADOW_DATABASE_URL` is a connection string for a secondary database used for
migrations to detect schema drift. migrations to detect schema drift.
- You can create a free account [here](https://neon.tech) - You can create a free account [here](https://neon.tech).
- Consult the [docs](https://neon.tech/docs/guides/prisma) for more information. - Consult the [docs](https://neon.tech/docs/guides/prisma) for more information.
- `MAPBOX_ACCESS_TOKEN` and `NEXT_PUBLIC_MAPBOX_ACCESS_TOKEN` are the access tokens for - `MAPBOX_ACCESS_TOKEN` and `NEXT_PUBLIC_MAPBOX_ACCESS_TOKEN` are the access tokens for
your Mapbox account. your Mapbox account.

1648
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -21,9 +21,9 @@
"@mapbox/search-js-react": "^1.0.0-beta.17", "@mapbox/search-js-react": "^1.0.0-beta.17",
"@next/bundle-analyzer": "^13.4.10", "@next/bundle-analyzer": "^13.4.10",
"@prisma/client": "^5.0.0", "@prisma/client": "^5.0.0",
"@react-email/components": "^0.0.7", "@react-email/components": "^0.0.11",
"@react-email/render": "^0.0.7", "@react-email/render": "^0.0.9",
"@react-email/tailwind": "^0.0.8", "@react-email/tailwind": "^0.0.12",
"@vercel/analytics": "^1.1.0", "@vercel/analytics": "^1.1.0",
"argon2": "^0.31.1", "argon2": "^0.31.1",
"cloudinary": "^1.41.0", "cloudinary": "^1.41.0",
@@ -44,14 +44,13 @@
"react": "^18.2.0", "react": "^18.2.0",
"react-daisyui": "^4.0.0", "react-daisyui": "^4.0.0",
"react-dom": "^18.2.0", "react-dom": "^18.2.0",
"react-email": "^1.9.4", "react-email": "^1.9.5",
"react-hook-form": "^7.45.2", "react-hook-form": "^7.45.2",
"react-hot-toast": "^2.4.1", "react-hot-toast": "^2.4.1",
"react-icons": "^4.10.1", "react-icons": "^4.10.1",
"react-intersection-observer": "^9.5.2", "react-intersection-observer": "^9.5.2",
"react-map-gl": "^7.1.2", "react-map-gl": "^7.1.2",
"react-responsive-carousel": "^3.2.23", "react-responsive-carousel": "^3.2.23",
"sparkpost": "^2.1.4",
"swr": "^2.2.0", "swr": "^2.2.0",
"theme-change": "^2.5.0", "theme-change": "^2.5.0",
"zod": "^3.21.4" "zod": "^3.21.4"

3018
schema.svg

File diff suppressed because it is too large Load Diff

Before

Width:  |  Height:  |  Size: 198 KiB

After

Width:  |  Height:  |  Size: 237 KiB

View File

@@ -0,0 +1,27 @@
import { FC } from 'react';
import Image from 'next/image';
import { z } from 'zod';
import GetUserSchema from '@/services/User/schema/GetUserSchema';
interface UserAvatarProps {
user: {
username: z.infer<typeof GetUserSchema>['username'];
userAvatar: z.infer<typeof GetUserSchema>['userAvatar'];
id: z.infer<typeof GetUserSchema>['id'];
};
}
const UserAvatar: FC<UserAvatarProps> = ({ user }) => {
const { userAvatar } = user;
return !userAvatar ? null : (
<Image
src={userAvatar.path}
alt="user avatar"
width={1000}
height={1000}
className="h-full w-full object-cover mask mask-circle ring ring-primary ring-offset-base-100 ring-offset-2"
/>
);
};
export default UserAvatar;

View File

@@ -4,9 +4,9 @@ import { FC, useState } from 'react';
import { useInView } from 'react-intersection-observer'; import { useInView } from 'react-intersection-observer';
import { z } from 'zod'; import { z } from 'zod';
import CreateCommentValidationSchema from '@/services/schema/CommentSchema/CreateCommentValidationSchema'; import CreateCommentValidationSchema from '@/services/schema/CommentSchema/CreateCommentValidationSchema';
import CommentContentBody from './CommentContentBody'; import CommentContentBody from './CommentContentBody';
import EditCommentBody from './EditCommentBody'; import EditCommentBody from './EditCommentBody';
import UserAvatar from '../Account/UserAvatar';
interface CommentCardProps { interface CommentCardProps {
comment: z.infer<typeof CommentQueryResult>; comment: z.infer<typeof CommentQueryResult>;
@@ -29,18 +29,26 @@ const CommentCardBody: FC<CommentCardProps> = ({
const [inEditMode, setInEditMode] = useState(false); const [inEditMode, setInEditMode] = useState(false);
return ( return (
<div ref={ref}> <div ref={ref} className="flex">
{!inEditMode ? ( <div className="w-[12%] py-4 justify-center">
<CommentContentBody comment={comment} setInEditMode={setInEditMode} /> <div className="px-1">
) : ( <UserAvatar user={comment.postedBy} />
<EditCommentBody </div>
comment={comment} </div>
mutate={mutate}
setInEditMode={setInEditMode} <div className="w-[88%] h-full">
handleDeleteRequest={handleDeleteRequest} {!inEditMode ? (
handleEditRequest={handleEditRequest} <CommentContentBody comment={comment} setInEditMode={setInEditMode} />
/> ) : (
)} <EditCommentBody
comment={comment}
mutate={mutate}
setInEditMode={setInEditMode}
handleDeleteRequest={handleDeleteRequest}
handleEditRequest={handleEditRequest}
/>
)}
</div>
</div> </div>
); );
}; };

View File

@@ -19,42 +19,47 @@ const CommentContentBody: FC<CommentContentBodyProps> = ({ comment, setInEditMod
const timeDistance = useTimeDistance(new Date(comment.createdAt)); const timeDistance = useTimeDistance(new Date(comment.createdAt));
return ( return (
<div className="card-body animate-in fade-in-10"> <div className="pr-3 py-4 animate-in fade-in-10 space-y-1">
<div className="flex flex-row justify-between"> <div className="space-y-2">
<div> <div className="flex flex-row justify-between">
<p className="font-semibold sm:text-2xl"> <div>
<Link href={`/users/${comment.postedBy.id}`} className="link-hover link"> <p className="font-semibold sm:text-2xl">
{comment.postedBy.username} <Link href={`/users/${comment.postedBy.id}`} className="link-hover link">
</Link> {comment.postedBy.username}
</p> </Link>
<span className="italic"> </p>
posted{' '} <span className="italic">
<time posted{' '}
className="tooltip tooltip-bottom" <time
data-tip={format(new Date(comment.createdAt), 'MM/dd/yyyy')} className="tooltip tooltip-bottom"
> data-tip={format(new Date(comment.createdAt), 'MM/dd/yyyy')}
{timeDistance} >
</time>{' '} {timeDistance}
ago </time>{' '}
</span> ago
</span>
</div>
{user && (
<CommentCardDropdown comment={comment} setInEditMode={setInEditMode} />
)}
</div>
<div className="space-y-1">
<Rating value={comment.rating}>
{Array.from({ length: 5 }).map((val, index) => (
<Rating.Item
name="rating-1"
className="mask mask-star cursor-default"
disabled
aria-disabled
key={index}
/>
))}
</Rating>
</div> </div>
{user && <CommentCardDropdown comment={comment} setInEditMode={setInEditMode} />}
</div> </div>
<div>
<div className="space-y-1"> <p className="text-sm">{comment.content}</p>
<Rating value={comment.rating}>
{Array.from({ length: 5 }).map((val, index) => (
<Rating.Item
name="rating-1"
className="mask mask-star cursor-default"
disabled
aria-disabled
key={index}
/>
))}
</Rating>
<p>{comment.content}</p>
</div> </div>
</div> </div>
); );

View File

@@ -81,7 +81,7 @@ const EditCommentBody: FC<EditCommentBodyProps> = ({
}; };
return ( return (
<div className="card-body animate-in fade-in-10"> <div className="pr-3 py-4 animate-in fade-in-10">
<form onSubmit={handleSubmit(onEdit)} className="space-y-3"> <form onSubmit={handleSubmit(onEdit)} className="space-y-3">
<div> <div>
<FormInfo> <FormInfo>

View File

@@ -18,8 +18,9 @@ const BeerCard: FC<{ post: z.infer<typeof BeerPostQueryResult> }> = ({ post }) =
<Image <Image
src={post.beerImages[0].path} src={post.beerImages[0].path}
alt={post.name} alt={post.name}
width="1029" width="3000"
height="110" height="3000"
className="h-full object-cover"
/> />
)} )}
</figure> </figure>

View File

@@ -0,0 +1,50 @@
import useTimeDistance from '@/hooks/utilities/useTimeDistance';
import useGetUsersFollowedByUser from '@/hooks/data-fetching/user-follows/useGetUsersFollowedByUser';
import useGetUsersFollowingUser from '@/hooks/data-fetching/user-follows/useGetUsersFollowingUser';
import { FC } from 'react';
import { z } from 'zod';
import { format } from 'date-fns';
import GetUserSchema from '@/services/User/schema/GetUserSchema';
import UserAvatar from '../Account/UserAvatar';
interface UserHeaderProps {
user: z.infer<typeof GetUserSchema>;
followerCount: ReturnType<typeof useGetUsersFollowingUser>['followerCount'];
followingCount: ReturnType<typeof useGetUsersFollowedByUser>['followingCount'];
}
const UserHeader: FC<UserHeaderProps> = ({ user, followerCount, followingCount }) => {
const timeDistance = useTimeDistance(new Date(user.createdAt));
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
className="tooltip tooltip-bottom"
data-tip={format(new Date(user.createdAt), 'MM/dd/yyyy')}
>
{`${timeDistance} ago`}
</span>
)}
</span>
</div>
</header>
);
};
export default UserHeader;

View File

@@ -1,6 +0,0 @@
import SparkPost from 'sparkpost';
import { SPARKPOST_API_KEY } from '../env';
const client = new SparkPost(SPARKPOST_API_KEY);
export default client;

View File

@@ -1,5 +1,4 @@
import { SPARKPOST_SENDER_ADDRESS } from '../env'; import { SPARKPOST_API_KEY, SPARKPOST_SENDER_ADDRESS } from '../env';
import client from './client';
interface EmailParams { interface EmailParams {
address: string; address: string;
@@ -11,10 +10,26 @@ interface EmailParams {
const sendEmail = async ({ address, text, html, subject }: EmailParams) => { const sendEmail = async ({ address, text, html, subject }: EmailParams) => {
const from = SPARKPOST_SENDER_ADDRESS; const from = SPARKPOST_SENDER_ADDRESS;
await client.transmissions.send({ const data = {
content: { from, html, subject, text },
recipients: [{ address }], recipients: [{ address }],
content: { from, subject, text, html },
};
const transmissionsEndpoint = 'https://api.sparkpost.com/api/v1/transmissions';
const response = await fetch(transmissionsEndpoint, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Accept: 'application/json',
Authorization: SPARKPOST_API_KEY,
},
body: JSON.stringify(data),
}); });
if (response.status !== 200) {
throw new Error(`Sparkpost API returned status code ${response.status}`);
}
}; };
export default sendEmail; export default sendEmail;

View File

@@ -46,8 +46,6 @@ const useBeerPostsByBeerStyle = ({
fetcher, fetcher,
); );
console.log(error);
const beerPosts = data?.flatMap((d) => d.beerPosts) ?? []; const beerPosts = data?.flatMap((d) => d.beerPosts) ?? [];
const pageCount = data?.[0].pageCount ?? 0; const pageCount = data?.[0].pageCount ?? 0;
const isLoadingMore = size > 0 && data && typeof data[size - 1] === 'undefined'; const isLoadingMore = size > 0 && data && typeof data[size - 1] === 'undefined';

View File

@@ -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 useGetUsersFollowedByUser = ({
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 useGetUsersFollowedByUser;

View File

@@ -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;

View File

@@ -59,9 +59,9 @@ const getAll = async (
pageSize: parseInt(page_size, 10), pageSize: parseInt(page_size, 10),
}); });
const pageCount = await DBClient.instance.beerComment.count({ where: { beerPostId } }); const count = await DBClient.instance.beerComment.count({ where: { beerPostId } });
res.setHeader('X-Total-Count', pageCount); res.setHeader('X-Total-Count', count);
res.status(200).json({ res.status(200).json({
message: 'Beer comments fetched successfully', message: 'Beer comments fetched successfully',

View File

@@ -20,6 +20,7 @@ const getBeerPosts = async (
const pageSize = parseInt(req.query.page_size, 10); const pageSize = parseInt(req.query.page_size, 10);
const beerPosts = await getAllBeerPosts({ pageNum, pageSize }); const beerPosts = await getAllBeerPosts({ pageNum, pageSize });
const beerPostCount = await DBClient.instance.beerPost.count(); const beerPostCount = await DBClient.instance.beerPost.count();
res.setHeader('X-Total-Count', beerPostCount); res.setHeader('X-Total-Count', beerPostCount);

View File

@@ -31,7 +31,16 @@ const search = async (req: SearchAPIRequest, res: NextApiResponse) => {
postedBy: { select: { username: true, id: true } }, postedBy: { select: { username: true, id: true } },
brewery: { select: { name: true, id: true } }, brewery: { select: { name: true, id: true } },
style: { select: { name: true, id: true, description: true } }, style: { select: { name: true, id: true, description: true } },
beerImages: { select: { alt: true, path: true, caption: true, id: true } }, beerImages: {
select: {
alt: true,
path: true,
caption: true,
id: true,
createdAt: true,
updatedAt: true,
},
},
}, },
where: { where: {
OR: [ OR: [

View File

@@ -19,15 +19,15 @@ const getAllBeersByBeerStyle = async (
// eslint-disable-next-line @typescript-eslint/naming-convention // eslint-disable-next-line @typescript-eslint/naming-convention
const { page_size, page_num, id } = req.query; const { page_size, page_num, id } = req.query;
const beers = getBeerPostsByBeerStyleId({ const beers = await getBeerPostsByBeerStyleId({
pageNum: parseInt(page_num, 10), pageNum: parseInt(page_num, 10),
pageSize: parseInt(page_size, 10), pageSize: parseInt(page_size, 10),
styleId: id, styleId: id,
}); });
const pageCount = await DBClient.instance.beerPost.count({ where: { styleId: id } }); const count = await DBClient.instance.beerPost.count({ where: { styleId: id } });
res.setHeader('X-Total-Count', pageCount); res.setHeader('X-Total-Count', count);
res.status(200).json({ res.status(200).json({
message: 'Beers fetched successfully', message: 'Beers fetched successfully',

View File

@@ -59,11 +59,11 @@ const getAll = async (
pageSize: parseInt(page_size, 10), pageSize: parseInt(page_size, 10),
}); });
const pageCount = await DBClient.instance.beerStyleComment.count({ const count = await DBClient.instance.beerStyleComment.count({
where: { beerStyleId }, where: { beerStyleId },
}); });
res.setHeader('X-Total-Count', pageCount); res.setHeader('X-Total-Count', count);
res.status(200).json({ res.status(200).json({
message: 'Beer comments fetched successfully', message: 'Beer comments fetched successfully',

View File

@@ -34,15 +34,24 @@ const getAllBeersByBrewery = async (
postedBy: { select: { username: true, id: true } }, postedBy: { select: { username: true, id: true } },
brewery: { select: { name: true, id: true } }, brewery: { select: { name: true, id: true } },
style: { select: { name: true, id: true, description: true } }, style: { select: { name: true, id: true, description: true } },
beerImages: { select: { alt: true, path: true, caption: true, id: true } }, beerImages: {
select: {
alt: true,
path: true,
caption: true,
id: true,
createdAt: true,
updatedAt: true,
},
},
}, },
}); });
const pageCount = await DBClient.instance.beerPost.count({ const count = await DBClient.instance.beerPost.count({
where: { breweryId: id }, where: { breweryId: id },
}); });
res.setHeader('X-Total-Count', pageCount); res.setHeader('X-Total-Count', count);
res.status(200).json({ res.status(200).json({
message: 'Beers fetched successfully', message: 'Beers fetched successfully',

View File

@@ -67,11 +67,11 @@ const getAll = async (
pageSize: parseInt(page_size, 10), pageSize: parseInt(page_size, 10),
}); });
const pageCount = await DBClient.instance.breweryComment.count({ const count = await DBClient.instance.breweryComment.count({
where: { breweryPostId }, where: { breweryPostId },
}); });
res.setHeader('X-Total-Count', pageCount); res.setHeader('X-Total-Count', count);
res.status(200).json({ res.status(200).json({
message: 'Beer comments fetched successfully', message: 'Beer comments fetched successfully',

View File

@@ -33,7 +33,7 @@ const createBreweryPost = async (
const [latitude, longitude] = geocoded.center; const [latitude, longitude] = geocoded.center;
const location = await DBClient.instance.location.create({ const location = await DBClient.instance.breweryLocation.create({
data: { data: {
address, address,
city, city,

View 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;

View 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;

View File

@@ -1,5 +1,84 @@
import { FC } from 'react'; import useMediaQuery from '@/hooks/utilities/useMediaQuery';
import findUserById from '@/services/User/findUserById';
import GetUserSchema from '@/services/User/schema/GetUserSchema';
const UserInfoPage: FC = () => null; import Head from 'next/head';
import { FC } from 'react';
import { z } from 'zod';
import withPageAuthRequired from '@/util/withPageAuthRequired';
import UserHeader from '@/components/UserPage/UserHeader';
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>;
}
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`;
const { followingCount } = useGetUsersFollowedByUser({
userId: user.id,
pageSize: 10,
});
const { followerCount } = useGetUsersFollowingUser({
userId: user.id,
pageSize: 10,
});
return (
<>
<Head>
<title>{title}</title>
<meta name="description" content="User information" />
</Head>
<>
<main className="h-full mb-12 mt-10 flex w-full items-center justify-center">
<div className="h-full w-11/12 space-y-3 xl:w-9/12 2xl:w-8/12">
<UserHeader
user={user}
followerCount={followerCount}
followingCount={followingCount}
/>
{isDesktop ? (
<div className="h-64 flex space-x-3">
<div className="h-full w-5/12">
<div className="h-full card">
<div className="card-body">
<h2 className="text-2xl font-bold">About Me</h2>
<p>{user.bio}</p>
</div>
</div>
</div>
<div className="w-7/12">
<div className="h-full card">
<div className="h-full card-body"></div>
</div>
</div>
</div>
) : (
<></>
)}
</div>
</main>
</>
</>
);
};
export default UserInfoPage; export default UserInfoPage;
export const getServerSideProps = withPageAuthRequired<UserInfoPageProps>(
async (context) => {
const { id } = context.params!;
const user = await findUserById(id as string);
return user
? { props: { user: JSON.parse(JSON.stringify(user)) } }
: { notFound: true };
},
);

View File

@@ -0,0 +1,57 @@
/*
Warnings:
- You are about to drop the `Location` table. If the table is not empty, all the data it contains will be lost.
*/
-- DropForeignKey
ALTER TABLE "BreweryPost" DROP CONSTRAINT "BreweryPost_locationId_fkey";
-- DropForeignKey
ALTER TABLE "Location" DROP CONSTRAINT "Location_postedById_fkey";
-- AlterTable
ALTER TABLE "User" ADD COLUMN "bio" TEXT;
-- DropTable
DROP TABLE "Location";
-- CreateTable
CREATE TABLE "UserAvatar" (
"id" TEXT NOT NULL,
"path" TEXT NOT NULL,
"alt" TEXT NOT NULL,
"caption" TEXT NOT NULL,
"userId" TEXT NOT NULL,
"createdAt" TIMESTAMPTZ(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMPTZ(3),
CONSTRAINT "UserAvatar_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "BreweryLocation" (
"id" TEXT NOT NULL,
"city" TEXT NOT NULL,
"stateOrProvince" TEXT,
"country" TEXT,
"coordinates" DOUBLE PRECISION[],
"address" TEXT NOT NULL,
"postedById" TEXT NOT NULL,
"createdAt" TIMESTAMPTZ(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMPTZ(3),
CONSTRAINT "BreweryLocation_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE UNIQUE INDEX "UserAvatar_userId_key" ON "UserAvatar"("userId");
-- AddForeignKey
ALTER TABLE "UserAvatar" ADD CONSTRAINT "UserAvatar_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "BreweryLocation" ADD CONSTRAINT "BreweryLocation_postedById_fkey" FOREIGN KEY ("postedById") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "BreweryPost" ADD CONSTRAINT "BreweryPost_locationId_fkey" FOREIGN KEY ("locationId") REFERENCES "BreweryLocation"("id") ON DELETE RESTRICT ON UPDATE CASCADE;

View File

@@ -0,0 +1,14 @@
-- CreateTable
CREATE TABLE "UserFollow" (
"followerId" TEXT NOT NULL,
"followingId" TEXT NOT NULL,
"followedAt" TIMESTAMPTZ(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "UserFollow_pkey" PRIMARY KEY ("followerId","followingId")
);
-- AddForeignKey
ALTER TABLE "UserFollow" ADD CONSTRAINT "UserFollow_followerId_fkey" FOREIGN KEY ("followerId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "UserFollow" ADD CONSTRAINT "UserFollow_followingId_fkey" FOREIGN KEY ("followingId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;

View File

@@ -29,19 +29,44 @@ model User {
accountIsVerified Boolean @default(false) accountIsVerified Boolean @default(false)
dateOfBirth DateTime dateOfBirth DateTime
role Role @default(USER) role Role @default(USER)
bio String?
beerPosts BeerPost[] beerPosts BeerPost[]
beerStyles BeerStyle[] beerStyles BeerStyle[]
breweryPosts BreweryPost[] breweryPosts BreweryPost[]
beerComments BeerComment[] beerComments BeerComment[]
breweryComments BreweryComment[] breweryComments BreweryComment[]
BeerPostLikes BeerPostLike[] beerPostLikes BeerPostLike[]
BeerImage BeerImage[] beerImages BeerImage[]
BreweryImage BreweryImage[] breweryImages BreweryImage[]
BreweryPostLike BreweryPostLike[] breweryPostLikes BreweryPostLike[]
Location Location[] locations BreweryLocation[]
Glassware Glassware[] glasswares Glassware[]
BeerStyleLike BeerStyleLike[] beerStyleLikes BeerStyleLike[]
BeerStyleComment BeerStyleComment[] beerStyleComments BeerStyleComment[]
userAvatar UserAvatar?
followedBy UserFollow[] @relation("following")
following UserFollow[] @relation("follower")
}
model UserFollow {
follower User @relation("follower", fields: [followerId], references: [id])
followerId String
following User @relation("following", fields: [followingId], references: [id])
followingId String
followedAt DateTime @default(now()) @db.Timestamptz(3)
@@id([followerId, followingId])
}
model UserAvatar {
id String @id @default(cuid())
path String
alt String
caption String
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
userId String @unique
createdAt DateTime @default(now()) @db.Timestamptz(3)
updatedAt DateTime? @updatedAt @db.Timestamptz(3)
} }
model BeerPost { model BeerPost {
@@ -60,7 +85,7 @@ model BeerPost {
updatedAt DateTime? @updatedAt @db.Timestamptz(3) updatedAt DateTime? @updatedAt @db.Timestamptz(3)
beerComments BeerComment[] beerComments BeerComment[]
beerImages BeerImage[] beerImages BeerImage[]
BeerPostLikes BeerPostLike[] beerPostLikes BeerPostLike[]
} }
model BeerPostLike { model BeerPostLike {
@@ -108,8 +133,8 @@ model BeerStyle {
abvRange Float[] abvRange Float[]
ibuRange Float[] ibuRange Float[]
beerPosts BeerPost[] beerPosts BeerPost[]
BeerStyleLike BeerStyleLike[] beerStyleLike BeerStyleLike[]
BeerStyleComment BeerStyleComment[] beerStyleComment BeerStyleComment[]
} }
model BeerStyleLike { model BeerStyleLike {
@@ -142,10 +167,10 @@ model Glassware {
updatedAt DateTime? @updatedAt @db.Timestamptz(3) updatedAt DateTime? @updatedAt @db.Timestamptz(3)
postedBy User @relation(fields: [postedById], references: [id], onDelete: Cascade) postedBy User @relation(fields: [postedById], references: [id], onDelete: Cascade)
postedById String postedById String
BeerStyle BeerStyle[] beerStyle BeerStyle[]
} }
model Location { model BreweryLocation {
id String @id @default(cuid()) id String @id @default(cuid())
city String city String
stateOrProvince String? stateOrProvince String?
@@ -154,7 +179,7 @@ model Location {
address String address String
postedBy User @relation(fields: [postedById], references: [id], onDelete: Cascade) postedBy User @relation(fields: [postedById], references: [id], onDelete: Cascade)
postedById String postedById String
BreweryPost BreweryPost? breweryPost BreweryPost?
createdAt DateTime @default(now()) @db.Timestamptz(3) createdAt DateTime @default(now()) @db.Timestamptz(3)
updatedAt DateTime? @updatedAt @db.Timestamptz(3) updatedAt DateTime? @updatedAt @db.Timestamptz(3)
} }
@@ -162,7 +187,7 @@ model Location {
model BreweryPost { model BreweryPost {
id String @id @default(cuid()) id String @id @default(cuid())
name String name String
location Location @relation(fields: [locationId], references: [id]) location BreweryLocation @relation(fields: [locationId], references: [id])
locationId String @unique locationId String @unique
beers BeerPost[] beers BeerPost[]
description String description String

View File

@@ -3,6 +3,7 @@ import { z } from 'zod';
import { hashPassword } from '../../../config/auth/passwordFns'; import { hashPassword } from '../../../config/auth/passwordFns';
import DBClient from '../../DBClient'; import DBClient from '../../DBClient';
import GetUserSchema from '../../../services/User/schema/GetUserSchema'; import GetUserSchema from '../../../services/User/schema/GetUserSchema';
import imageUrls from '../util/imageUrls';
const createAdminUser = async () => { const createAdminUser = async () => {
const hash = await hashPassword('Pas!3word'); const hash = await hashPassword('Pas!3word');
@@ -15,6 +16,14 @@ const createAdminUser = async () => {
dateOfBirth: new Date('1990-01-01'), dateOfBirth: new Date('1990-01-01'),
role: 'ADMIN', role: 'ADMIN',
hash, hash,
userAvatar: {
create: {
path: imageUrls[Math.floor(Math.random() * imageUrls.length)],
alt: 'Admin User',
caption: 'Admin User',
createdAt: new Date(),
},
},
}, },
select: { select: {
id: true, id: true,
@@ -27,6 +36,8 @@ const createAdminUser = async () => {
accountIsVerified: true, accountIsVerified: true,
updatedAt: true, updatedAt: true,
role: true, role: true,
bio: true,
userAvatar: true,
}, },
}); });

View File

@@ -1,13 +1,13 @@
// eslint-disable-next-line import/no-extraneous-dependencies // eslint-disable-next-line import/no-extraneous-dependencies
import { faker } from '@faker-js/faker'; import { faker } from '@faker-js/faker';
import { Location, User } from '@prisma/client'; import { BreweryLocation, User } from '@prisma/client';
import DBClient from '../../DBClient'; import DBClient from '../../DBClient';
interface CreateNewBreweryPostsArgs { interface CreateNewBreweryPostsArgs {
numberOfPosts: number; numberOfPosts: number;
joinData: { joinData: {
users: User[]; users: User[];
locations: Location[]; locations: BreweryLocation[];
}; };
} }

View File

@@ -47,9 +47,9 @@ const createNewLocations = async ({
}); });
} }
await prisma.location.createMany({ data: locationData, skipDuplicates: true }); await prisma.breweryLocation.createMany({ data: locationData, skipDuplicates: true });
return prisma.location.findMany(); return prisma.breweryLocation.findMany();
}; };
export default createNewLocations; export default createNewLocations;

View File

@@ -0,0 +1,39 @@
import { User } from '@prisma/client';
import DBClient from '../../DBClient';
import imageUrls from '../util/imageUrls';
interface CreateNewUserAvatarsArgs {
joinData: { users: User[] };
}
interface UserAvatarData {
path: string;
alt: string;
caption: string;
userId: string;
createdAt: Date;
}
const createNewUserAvatars = async ({
joinData: { users },
}: CreateNewUserAvatarsArgs) => {
const userAvatars: UserAvatarData[] = [];
users.forEach((user) => {
const path = imageUrls[Math.floor(Math.random() * imageUrls.length)];
userAvatars.push({
path,
alt: `${user.firstName} ${user.lastName}`,
caption: `${user.firstName} ${user.lastName}`,
userId: user.id,
createdAt: new Date(),
});
});
await DBClient.instance.userAvatar.createMany({ data: userAvatars });
return DBClient.instance.userAvatar.findMany({
where: { user: { role: { not: 'ADMIN' } } },
});
};
export default createNewUserAvatars;

View File

@@ -0,0 +1,49 @@
// eslint-disable-next-line import/no-extraneous-dependencies
import { faker } from '@faker-js/faker';
import type { User } from '@prisma/client';
import DBClient from '../../DBClient';
interface CreateNewUserFollowsArgs {
joinData: { users: User[] };
}
interface UserFollowData {
followerId: string;
followingId: string;
followedAt: Date;
}
const createNewUserFollows = async ({
joinData: { users },
}: CreateNewUserFollowsArgs) => {
const userFollows: UserFollowData[] = [];
users.forEach((user) => {
// Get 20 random users to follow.
const randomUsers = users
.filter((randomUser) => randomUser.id !== user.id)
.sort(() => Math.random() - Math.random())
.slice(0, 100);
// 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() }),
},
]);
userFollows.push(...data);
});
await DBClient.instance.userFollow.createMany({
data: userFollows,
skipDuplicates: true,
});
return DBClient.instance.userFollow.findMany();
};
export default createNewUserFollows;

View File

@@ -18,6 +18,7 @@ interface UserData {
hash: string; hash: string;
accountIsVerified: boolean; accountIsVerified: boolean;
role: 'USER' | 'ADMIN'; role: 'USER' | 'ADMIN';
bio: string;
} }
const createNewUsers = async ({ numberOfUsers }: CreateNewUsersArgs) => { const createNewUsers = async ({ numberOfUsers }: CreateNewUsersArgs) => {
@@ -54,6 +55,7 @@ const createNewUsers = async ({ numberOfUsers }: CreateNewUsersArgs) => {
const dateOfBirth = faker.date.birthdate({ mode: 'age', min: 19 }); const dateOfBirth = faker.date.birthdate({ mode: 'age', min: 19 });
const createdAt = faker.date.past({ years: 4 }); const createdAt = faker.date.past({ years: 4 });
const bio = faker.lorem.paragraphs(3).replace(/\n/g, ' ');
const user: UserData = { const user: UserData = {
firstName, firstName,
@@ -63,6 +65,7 @@ const createNewUsers = async ({ numberOfUsers }: CreateNewUsersArgs) => {
dateOfBirth, dateOfBirth,
createdAt, createdAt,
hash, hash,
bio,
accountIsVerified: true, accountIsVerified: true,
role: 'USER', role: 'USER',
}; };
@@ -71,7 +74,9 @@ const createNewUsers = async ({ numberOfUsers }: CreateNewUsersArgs) => {
} }
await prisma.user.createMany({ data, skipDuplicates: true }); await prisma.user.createMany({ data, skipDuplicates: true });
return prisma.user.findMany(); return prisma.user.findMany({
where: { role: { not: 'ADMIN' } },
});
}; };
export default createNewUsers; export default createNewUsers;

View File

@@ -18,6 +18,8 @@ import logger from '../../config/pino/logger';
import createAdminUser from './create/createAdminUser'; import createAdminUser from './create/createAdminUser';
import createNewBeerStyleComments from './create/createNewBeerStyleComments'; import createNewBeerStyleComments from './create/createNewBeerStyleComments';
import createNewBeerStyleLikes from './create/createNewBeerStyleLikes'; import createNewBeerStyleLikes from './create/createNewBeerStyleLikes';
import createNewUserAvatars from './create/createNewUserAvatars';
import createNewUserFollows from './create/createNewUserFollows';
(async () => { (async () => {
try { try {
@@ -33,6 +35,12 @@ import createNewBeerStyleLikes from './create/createNewBeerStyleLikes';
const users = await createNewUsers({ numberOfUsers: 10000 }); const users = await createNewUsers({ numberOfUsers: 10000 });
logger.info('Users created successfully.'); logger.info('Users created successfully.');
const userAvatars = await createNewUserAvatars({ joinData: { users } });
logger.info('User avatars created successfully.');
const userFollows = await createNewUserFollows({ joinData: { users } });
logger.info('User follows created successfully.');
const locations = await createNewLocations({ const locations = await createNewLocations({
numberOfLocations: 500, numberOfLocations: 500,
joinData: { users }, joinData: { users },
@@ -103,6 +111,8 @@ import createNewBeerStyleLikes from './create/createNewBeerStyleLikes';
logger.info('Database seeded successfully.'); logger.info('Database seeded successfully.');
logger.info({ logger.info({
numberOfUsers: users.length, numberOfUsers: users.length,
numberOfUserAvatars: userAvatars.length,
numberOfUserFollows: userFollows.length,
numberOfBreweryPosts: breweryPosts.length, numberOfBreweryPosts: breweryPosts.length,
numberOfBeerPosts: beerPosts.length, numberOfBeerPosts: beerPosts.length,
numberOfBeerStyles: beerStyles.length, numberOfBeerStyles: beerStyles.length,

View File

@@ -27,7 +27,7 @@ const createNewBeerComment = async ({
id: true, id: true,
content: true, content: true,
rating: true, rating: true,
postedBy: { select: { id: true, username: true } }, postedBy: { select: { id: true, username: true, userAvatar: true } },
createdAt: true, createdAt: true,
updatedAt: true, updatedAt: true,
}, },

View File

@@ -22,7 +22,9 @@ const editBeerCommentById = async ({
rating: true, rating: true,
createdAt: true, createdAt: true,
updatedAt: true, updatedAt: true,
postedBy: { select: { id: true, username: true, createdAt: true } }, postedBy: {
select: { id: true, username: true, createdAt: true, userAvatar: true },
},
}, },
}); });
}; };

View File

@@ -17,7 +17,9 @@ const findBeerCommentById = async ({
rating: true, rating: true,
createdAt: true, createdAt: true,
updatedAt: true, updatedAt: true,
postedBy: { select: { id: true, username: true, createdAt: true } }, postedBy: {
select: { id: true, username: true, createdAt: true, userAvatar: true },
},
}, },
}); });
}; };

View File

@@ -24,7 +24,9 @@ const getAllBeerComments = async ({
rating: true, rating: true,
createdAt: true, createdAt: true,
updatedAt: true, updatedAt: true,
postedBy: { select: { id: true, username: true, createdAt: true } }, postedBy: {
select: { id: true, username: true, createdAt: true, userAvatar: true },
},
}, },
}); });
}; };

View File

@@ -36,7 +36,16 @@ const createNewBeerPost = ({
ibu: true, ibu: true,
createdAt: true, createdAt: true,
updatedAt: true, updatedAt: true,
beerImages: { select: { id: true, path: true, caption: true, alt: true } }, beerImages: {
select: {
alt: true,
path: true,
caption: true,
id: true,
createdAt: true,
updatedAt: true,
},
},
brewery: { select: { id: true, name: true } }, brewery: { select: { id: true, name: true } },
style: { select: { id: true, name: true, description: true } }, style: { select: { id: true, name: true, description: true } },
postedBy: { select: { id: true, username: true } }, postedBy: { select: { id: true, username: true } },

View File

@@ -19,7 +19,16 @@ const deleteBeerPostById = ({
id: true, id: true,
name: true, name: true,
updatedAt: true, updatedAt: true,
beerImages: { select: { id: true, path: true, caption: true, alt: true } }, beerImages: {
select: {
alt: true,
path: true,
caption: true,
id: true,
createdAt: true,
updatedAt: true,
},
},
style: { select: { id: true, name: true, description: true } }, style: { select: { id: true, name: true, description: true } },
postedBy: { select: { id: true, username: true } }, postedBy: { select: { id: true, username: true } },
brewery: { select: { id: true, name: true } }, brewery: { select: { id: true, name: true } },

View File

@@ -25,7 +25,16 @@ const editBeerPostById = ({
ibu: true, ibu: true,
createdAt: true, createdAt: true,
updatedAt: true, updatedAt: true,
beerImages: { select: { id: true, path: true, caption: true, alt: true } }, beerImages: {
select: {
alt: true,
path: true,
caption: true,
id: true,
createdAt: true,
updatedAt: true,
},
},
brewery: { select: { id: true, name: true } }, brewery: { select: { id: true, name: true } },
style: { select: { id: true, name: true, description: true } }, style: { select: { id: true, name: true, description: true } },
postedBy: { select: { id: true, username: true } }, postedBy: { select: { id: true, username: true } },

View File

@@ -25,7 +25,16 @@ const getAllBeerPosts = ({
style: { select: { name: true, id: true, description: true } }, style: { select: { name: true, id: true, description: true } },
brewery: { select: { name: true, id: true } }, brewery: { select: { name: true, id: true } },
postedBy: { select: { id: true, username: true } }, postedBy: { select: { id: true, username: true } },
beerImages: { select: { path: true, caption: true, id: true, alt: true } }, beerImages: {
select: {
alt: true,
path: true,
caption: true,
id: true,
createdAt: true,
updatedAt: true,
},
},
}, },
take: pageSize, take: pageSize,
skip: (pageNum - 1) * pageSize, skip: (pageNum - 1) * pageSize,

View File

@@ -19,7 +19,16 @@ const getBeerPostById = async (
postedBy: { select: { username: true, id: true } }, postedBy: { select: { username: true, id: true } },
brewery: { select: { name: true, id: true } }, brewery: { select: { name: true, id: true } },
style: { select: { name: true, id: true, description: true } }, style: { select: { name: true, id: true, description: true } },
beerImages: { select: { alt: true, path: true, caption: true, id: true } }, beerImages: {
select: {
alt: true,
path: true,
caption: true,
id: true,
createdAt: true,
updatedAt: true,
},
},
}, },
where: { id }, where: { id },
}); });

View File

@@ -28,7 +28,16 @@ const getBeerPostsByBeerStyleId = async ({
postedBy: { select: { username: true, id: true } }, postedBy: { select: { username: true, id: true } },
brewery: { select: { name: true, id: true } }, brewery: { select: { name: true, id: true } },
style: { select: { name: true, id: true, description: true } }, style: { select: { name: true, id: true, description: true } },
beerImages: { select: { alt: true, path: true, caption: true, id: true } }, beerImages: {
select: {
alt: true,
path: true,
caption: true,
id: true,
createdAt: true,
updatedAt: true,
},
},
}, },
}); });

View File

@@ -28,7 +28,16 @@ const getBeerPostsByBeerStyleId = async ({
postedBy: { select: { username: true, id: true } }, postedBy: { select: { username: true, id: true } },
brewery: { select: { name: true, id: true } }, brewery: { select: { name: true, id: true } },
style: { select: { name: true, id: true, description: true } }, style: { select: { name: true, id: true, description: true } },
beerImages: { select: { alt: true, path: true, caption: true, id: true } }, beerImages: {
select: {
alt: true,
path: true,
caption: true,
id: true,
createdAt: true,
updatedAt: true,
},
},
}, },
}); });

View File

@@ -37,7 +37,16 @@ const getBeerRecommendations = async ({
style: { select: { name: true, id: true, description: true } }, style: { select: { name: true, id: true, description: true } },
brewery: { select: { name: true, id: true } }, brewery: { select: { name: true, id: true } },
postedBy: { select: { id: true, username: true } }, postedBy: { select: { id: true, username: true } },
beerImages: { select: { path: true, caption: true, id: true, alt: true } }, beerImages: {
select: {
alt: true,
path: true,
caption: true,
id: true,
createdAt: true,
updatedAt: true,
},
},
}, },
take, take,
skip, skip,

View File

@@ -1,3 +1,4 @@
import ImageQueryValidationSchema from '@/services/schema/ImageSchema/ImageQueryValidationSchema';
import { z } from 'zod'; import { z } from 'zod';
const BeerPostQueryResult = z.object({ const BeerPostQueryResult = z.object({
@@ -5,9 +6,7 @@ const BeerPostQueryResult = z.object({
name: z.string(), name: z.string(),
brewery: z.object({ id: z.string(), name: z.string() }), brewery: z.object({ id: z.string(), name: z.string() }),
description: z.string(), description: z.string(),
beerImages: z.array( beerImages: z.array(ImageQueryValidationSchema),
z.object({ path: z.string(), caption: z.string(), id: z.string(), alt: z.string() }),
),
ibu: z.number(), ibu: z.number(),
abv: z.number(), abv: z.number(),
style: z.object({ id: z.string(), name: z.string(), description: z.string() }), style: z.object({ id: z.string(), name: z.string(), description: z.string() }),

View File

@@ -27,9 +27,11 @@ const createNewBeerStyleComment = async ({
id: true, id: true,
content: true, content: true,
rating: true, rating: true,
postedBy: { select: { id: true, username: true } },
createdAt: true, createdAt: true,
updatedAt: true, updatedAt: true,
postedBy: {
select: { id: true, username: true, createdAt: true, userAvatar: true },
},
}, },
}); });
}; };

View File

@@ -24,7 +24,9 @@ const getAllBeerStyleComments = async ({
rating: true, rating: true,
createdAt: true, createdAt: true,
updatedAt: true, updatedAt: true,
postedBy: { select: { id: true, username: true, createdAt: true } }, postedBy: {
select: { id: true, username: true, createdAt: true, userAvatar: true },
},
}, },
}); });
}; };

View File

@@ -27,9 +27,11 @@ const createNewBreweryComment = async ({
id: true, id: true,
content: true, content: true,
rating: true, rating: true,
postedBy: { select: { id: true, username: true } },
createdAt: true, createdAt: true,
updatedAt: true, updatedAt: true,
postedBy: {
select: { id: true, username: true, createdAt: true, userAvatar: true },
},
}, },
}); });
}; };

View File

@@ -23,7 +23,9 @@ const getAllBreweryComments = async ({
rating: true, rating: true,
createdAt: true, createdAt: true,
updatedAt: true, updatedAt: true,
postedBy: { select: { id: true, username: true, createdAt: true } }, postedBy: {
select: { id: true, username: true, createdAt: true, userAvatar: true },
},
}, },
orderBy: { createdAt: 'desc' }, orderBy: { createdAt: 'desc' },
}); });

View File

@@ -37,7 +37,16 @@ const createNewBreweryPost = async ({
createdAt: true, createdAt: true,
dateEstablished: true, dateEstablished: true,
postedBy: { select: { id: true, username: true } }, postedBy: { select: { id: true, username: true } },
breweryImages: { select: { path: true, caption: true, id: true, alt: true } }, breweryImages: {
select: {
path: true,
caption: true,
id: true,
alt: true,
createdAt: true,
updatedAt: true,
},
},
location: { location: {
select: { select: {
city: true, city: true,

View File

@@ -29,7 +29,16 @@ const getAllBreweryPosts = async ({
description: true, description: true,
name: true, name: true,
postedBy: { select: { username: true, id: true } }, postedBy: { select: { username: true, id: true } },
breweryImages: { select: { path: true, caption: true, id: true, alt: true } }, breweryImages: {
select: {
path: true,
caption: true,
id: true,
alt: true,
createdAt: true,
updatedAt: true,
},
},
createdAt: true, createdAt: true,
dateEstablished: true, dateEstablished: true,
}, },

View File

@@ -19,7 +19,16 @@ const getBreweryPostById = async (id: string) => {
}, },
description: true, description: true,
name: true, name: true,
breweryImages: { select: { path: true, caption: true, id: true, alt: true } }, breweryImages: {
select: {
path: true,
caption: true,
id: true,
alt: true,
createdAt: true,
updatedAt: true,
},
},
postedBy: { select: { username: true, id: true } }, postedBy: { select: { username: true, id: true } },
createdAt: true, createdAt: true,
dateEstablished: true, dateEstablished: true,

View File

@@ -1,3 +1,4 @@
import ImageQueryValidationSchema from '@/services/schema/ImageSchema/ImageQueryValidationSchema';
import { z } from 'zod'; import { z } from 'zod';
const BreweryPostQueryResult = z.object({ const BreweryPostQueryResult = z.object({
@@ -12,9 +13,7 @@ const BreweryPostQueryResult = z.object({
stateOrProvince: z.string().nullable(), stateOrProvince: z.string().nullable(),
}), }),
postedBy: z.object({ id: z.string(), username: z.string() }), postedBy: z.object({ id: z.string(), username: z.string() }),
breweryImages: z.array( breweryImages: z.array(ImageQueryValidationSchema),
z.object({ path: z.string(), caption: z.string(), id: z.string(), alt: z.string() }),
),
createdAt: z.coerce.date(), createdAt: z.coerce.date(),
dateEstablished: z.coerce.date(), dateEstablished: z.coerce.date(),
}); });

View File

@@ -33,6 +33,8 @@ const createNewUser = async ({
accountIsVerified: true, accountIsVerified: true,
updatedAt: true, updatedAt: true,
role: true, role: true,
userAvatar: true,
bio: true,
}, },
}); });

View File

@@ -17,6 +17,8 @@ const deleteUserById = async (id: string) => {
accountIsVerified: true, accountIsVerified: true,
updatedAt: true, updatedAt: true,
role: true, role: true,
userAvatar: true,
bio: true,
}, },
}); });

View File

@@ -17,6 +17,17 @@ const findUserById = async (id: string) => {
accountIsVerified: true, accountIsVerified: true,
updatedAt: true, updatedAt: true,
role: true, role: true,
userAvatar: {
select: {
path: true,
alt: true,
caption: true,
createdAt: true,
id: true,
updatedAt: true,
},
},
bio: true,
}, },
}); });

View File

@@ -1,3 +1,4 @@
import ImageQueryValidationSchema from '@/services/schema/ImageSchema/ImageQueryValidationSchema';
import { z } from 'zod'; import { z } from 'zod';
const GetUserSchema = z.object({ const GetUserSchema = z.object({
@@ -11,6 +12,8 @@ const GetUserSchema = z.object({
dateOfBirth: z.coerce.date(), dateOfBirth: z.coerce.date(),
accountIsVerified: z.boolean(), accountIsVerified: z.boolean(),
role: z.enum(['USER', 'ADMIN']), role: z.enum(['USER', 'ADMIN']),
bio: z.string().nullable(),
userAvatar: ImageQueryValidationSchema.nullable(),
}); });
export default GetUserSchema; export default GetUserSchema;

View File

@@ -17,6 +17,17 @@ const updateUserToBeConfirmedById = async (id: string) => {
updatedAt: true, updatedAt: true,
dateOfBirth: true, dateOfBirth: true,
role: true, role: true,
bio: true,
userAvatar: {
select: {
id: true,
path: true,
alt: true,
caption: true,
createdAt: true,
updatedAt: true,
},
},
}, },
}); });

View 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;

View 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;

View 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;

View File

@@ -1,15 +1,17 @@
import { z } from 'zod'; import { z } from 'zod';
import ImageQueryValidationSchema from '../ImageSchema/ImageQueryValidationSchema';
const CommentQueryResult = z.object({ const CommentQueryResult = z.object({
id: z.string().cuid(), id: z.string().cuid(),
content: z.string().min(1).max(500), content: z.string().min(1).max(500),
rating: z.number().int().min(1).max(5), rating: z.number().int().min(1).max(5),
createdAt: z.coerce.date(), createdAt: z.coerce.date(),
updatedAt: z.coerce.date().nullable(),
postedBy: z.object({ postedBy: z.object({
id: z.string().cuid(), id: z.string().cuid(),
username: z.string().min(1).max(50), username: z.string().min(1).max(50),
userAvatar: ImageQueryValidationSchema.nullable(),
}), }),
updatedAt: z.coerce.date().nullable(),
}); });
export default CommentQueryResult; export default CommentQueryResult;

View File

@@ -0,0 +1,12 @@
import { z } from 'zod';
const ImageQueryValidationSchema = z.object({
id: z.string().cuid(),
path: z.string().url(),
alt: z.string(),
caption: z.string(),
createdAt: z.coerce.date(),
updatedAt: z.coerce.date().nullable(),
});
export default ImageQueryValidationSchema;