mirror of
https://github.com/aaronpo97/the-biergarten-app.git
synced 2026-02-16 10:42:08 +00:00
Merge pull request #58 from aaronpo97/dev
feat: add user functionality (profiles, avatars)
This commit is contained in:
@@ -163,7 +163,7 @@ SPARKPOST_SENDER_ADDRESS=" > .env
|
||||
database used for migrations.
|
||||
- `SHADOW_DATABASE_URL` is a connection string for a secondary database used for
|
||||
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.
|
||||
- `MAPBOX_ACCESS_TOKEN` and `NEXT_PUBLIC_MAPBOX_ACCESS_TOKEN` are the access tokens for
|
||||
your Mapbox account.
|
||||
|
||||
1650
package-lock.json
generated
1650
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -21,9 +21,9 @@
|
||||
"@mapbox/search-js-react": "^1.0.0-beta.17",
|
||||
"@next/bundle-analyzer": "^13.4.10",
|
||||
"@prisma/client": "^5.0.0",
|
||||
"@react-email/components": "^0.0.7",
|
||||
"@react-email/render": "^0.0.7",
|
||||
"@react-email/tailwind": "^0.0.8",
|
||||
"@react-email/components": "^0.0.11",
|
||||
"@react-email/render": "^0.0.9",
|
||||
"@react-email/tailwind": "^0.0.12",
|
||||
"@vercel/analytics": "^1.1.0",
|
||||
"argon2": "^0.31.1",
|
||||
"cloudinary": "^1.41.0",
|
||||
@@ -44,14 +44,13 @@
|
||||
"react": "^18.2.0",
|
||||
"react-daisyui": "^4.0.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-email": "^1.9.4",
|
||||
"react-email": "^1.9.5",
|
||||
"react-hook-form": "^7.45.2",
|
||||
"react-hot-toast": "^2.4.1",
|
||||
"react-icons": "^4.10.1",
|
||||
"react-intersection-observer": "^9.5.2",
|
||||
"react-map-gl": "^7.1.2",
|
||||
"react-responsive-carousel": "^3.2.23",
|
||||
"sparkpost": "^2.1.4",
|
||||
"swr": "^2.2.0",
|
||||
"theme-change": "^2.5.0",
|
||||
"zod": "^3.21.4"
|
||||
|
||||
3018
schema.svg
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 |
27
src/components/Account/UserAvatar.tsx
Normal file
27
src/components/Account/UserAvatar.tsx
Normal 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;
|
||||
@@ -4,9 +4,9 @@ import { FC, useState } from 'react';
|
||||
import { useInView } from 'react-intersection-observer';
|
||||
import { z } from 'zod';
|
||||
import CreateCommentValidationSchema from '@/services/schema/CommentSchema/CreateCommentValidationSchema';
|
||||
|
||||
import CommentContentBody from './CommentContentBody';
|
||||
import EditCommentBody from './EditCommentBody';
|
||||
import UserAvatar from '../Account/UserAvatar';
|
||||
|
||||
interface CommentCardProps {
|
||||
comment: z.infer<typeof CommentQueryResult>;
|
||||
@@ -29,7 +29,14 @@ const CommentCardBody: FC<CommentCardProps> = ({
|
||||
const [inEditMode, setInEditMode] = useState(false);
|
||||
|
||||
return (
|
||||
<div ref={ref}>
|
||||
<div ref={ref} className="flex">
|
||||
<div className="w-[12%] py-4 justify-center">
|
||||
<div className="px-1">
|
||||
<UserAvatar user={comment.postedBy} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="w-[88%] h-full">
|
||||
{!inEditMode ? (
|
||||
<CommentContentBody comment={comment} setInEditMode={setInEditMode} />
|
||||
) : (
|
||||
@@ -42,6 +49,7 @@ const CommentCardBody: FC<CommentCardProps> = ({
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -19,7 +19,8 @@ const CommentContentBody: FC<CommentContentBodyProps> = ({ comment, setInEditMod
|
||||
const timeDistance = useTimeDistance(new Date(comment.createdAt));
|
||||
|
||||
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="space-y-2">
|
||||
<div className="flex flex-row justify-between">
|
||||
<div>
|
||||
<p className="font-semibold sm:text-2xl">
|
||||
@@ -39,9 +40,10 @@ const CommentContentBody: FC<CommentContentBodyProps> = ({ comment, setInEditMod
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{user && <CommentCardDropdown comment={comment} setInEditMode={setInEditMode} />}
|
||||
{user && (
|
||||
<CommentCardDropdown comment={comment} setInEditMode={setInEditMode} />
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="space-y-1">
|
||||
<Rating value={comment.rating}>
|
||||
{Array.from({ length: 5 }).map((val, index) => (
|
||||
@@ -54,7 +56,10 @@ const CommentContentBody: FC<CommentContentBodyProps> = ({ comment, setInEditMod
|
||||
/>
|
||||
))}
|
||||
</Rating>
|
||||
<p>{comment.content}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm">{comment.content}</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -81,7 +81,7 @@ const EditCommentBody: FC<EditCommentBodyProps> = ({
|
||||
};
|
||||
|
||||
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">
|
||||
<div>
|
||||
<FormInfo>
|
||||
|
||||
@@ -18,8 +18,9 @@ const BeerCard: FC<{ post: z.infer<typeof BeerPostQueryResult> }> = ({ post }) =
|
||||
<Image
|
||||
src={post.beerImages[0].path}
|
||||
alt={post.name}
|
||||
width="1029"
|
||||
height="110"
|
||||
width="3000"
|
||||
height="3000"
|
||||
className="h-full object-cover"
|
||||
/>
|
||||
)}
|
||||
</figure>
|
||||
|
||||
50
src/components/UserPage/UserHeader.tsx
Normal file
50
src/components/UserPage/UserHeader.tsx
Normal 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;
|
||||
@@ -1,6 +0,0 @@
|
||||
import SparkPost from 'sparkpost';
|
||||
import { SPARKPOST_API_KEY } from '../env';
|
||||
|
||||
const client = new SparkPost(SPARKPOST_API_KEY);
|
||||
|
||||
export default client;
|
||||
@@ -1,5 +1,4 @@
|
||||
import { SPARKPOST_SENDER_ADDRESS } from '../env';
|
||||
import client from './client';
|
||||
import { SPARKPOST_API_KEY, SPARKPOST_SENDER_ADDRESS } from '../env';
|
||||
|
||||
interface EmailParams {
|
||||
address: string;
|
||||
@@ -11,10 +10,26 @@ interface EmailParams {
|
||||
const sendEmail = async ({ address, text, html, subject }: EmailParams) => {
|
||||
const from = SPARKPOST_SENDER_ADDRESS;
|
||||
|
||||
await client.transmissions.send({
|
||||
content: { from, html, subject, text },
|
||||
const data = {
|
||||
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;
|
||||
|
||||
@@ -46,8 +46,6 @@ const useBeerPostsByBeerStyle = ({
|
||||
fetcher,
|
||||
);
|
||||
|
||||
console.log(error);
|
||||
|
||||
const beerPosts = data?.flatMap((d) => d.beerPosts) ?? [];
|
||||
const pageCount = data?.[0].pageCount ?? 0;
|
||||
const isLoadingMore = size > 0 && data && typeof data[size - 1] === 'undefined';
|
||||
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -59,9 +59,9 @@ const getAll = async (
|
||||
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({
|
||||
message: 'Beer comments fetched successfully',
|
||||
|
||||
@@ -20,6 +20,7 @@ const getBeerPosts = async (
|
||||
const pageSize = parseInt(req.query.page_size, 10);
|
||||
|
||||
const beerPosts = await getAllBeerPosts({ pageNum, pageSize });
|
||||
|
||||
const beerPostCount = await DBClient.instance.beerPost.count();
|
||||
|
||||
res.setHeader('X-Total-Count', beerPostCount);
|
||||
|
||||
@@ -31,7 +31,16 @@ const search = async (req: SearchAPIRequest, res: NextApiResponse) => {
|
||||
postedBy: { select: { username: true, id: true } },
|
||||
brewery: { select: { name: true, id: 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: {
|
||||
OR: [
|
||||
|
||||
@@ -19,15 +19,15 @@ const getAllBeersByBeerStyle = async (
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
const { page_size, page_num, id } = req.query;
|
||||
|
||||
const beers = getBeerPostsByBeerStyleId({
|
||||
const beers = await getBeerPostsByBeerStyleId({
|
||||
pageNum: parseInt(page_num, 10),
|
||||
pageSize: parseInt(page_size, 10),
|
||||
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({
|
||||
message: 'Beers fetched successfully',
|
||||
|
||||
@@ -59,11 +59,11 @@ const getAll = async (
|
||||
pageSize: parseInt(page_size, 10),
|
||||
});
|
||||
|
||||
const pageCount = await DBClient.instance.beerStyleComment.count({
|
||||
const count = await DBClient.instance.beerStyleComment.count({
|
||||
where: { beerStyleId },
|
||||
});
|
||||
|
||||
res.setHeader('X-Total-Count', pageCount);
|
||||
res.setHeader('X-Total-Count', count);
|
||||
|
||||
res.status(200).json({
|
||||
message: 'Beer comments fetched successfully',
|
||||
|
||||
@@ -34,15 +34,24 @@ const getAllBeersByBrewery = async (
|
||||
postedBy: { select: { username: true, id: true } },
|
||||
brewery: { select: { name: true, id: 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 },
|
||||
});
|
||||
|
||||
res.setHeader('X-Total-Count', pageCount);
|
||||
res.setHeader('X-Total-Count', count);
|
||||
|
||||
res.status(200).json({
|
||||
message: 'Beers fetched successfully',
|
||||
|
||||
@@ -67,11 +67,11 @@ const getAll = async (
|
||||
pageSize: parseInt(page_size, 10),
|
||||
});
|
||||
|
||||
const pageCount = await DBClient.instance.breweryComment.count({
|
||||
const count = await DBClient.instance.breweryComment.count({
|
||||
where: { breweryPostId },
|
||||
});
|
||||
|
||||
res.setHeader('X-Total-Count', pageCount);
|
||||
res.setHeader('X-Total-Count', count);
|
||||
|
||||
res.status(200).json({
|
||||
message: 'Beer comments fetched successfully',
|
||||
|
||||
@@ -33,7 +33,7 @@ const createBreweryPost = async (
|
||||
|
||||
const [latitude, longitude] = geocoded.center;
|
||||
|
||||
const location = await DBClient.instance.location.create({
|
||||
const location = await DBClient.instance.breweryLocation.create({
|
||||
data: {
|
||||
address,
|
||||
city,
|
||||
|
||||
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;
|
||||
@@ -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 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 };
|
||||
},
|
||||
);
|
||||
|
||||
57
src/prisma/migrations/20231106024511_/migration.sql
Normal file
57
src/prisma/migrations/20231106024511_/migration.sql
Normal 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;
|
||||
14
src/prisma/migrations/20231112221155_/migration.sql
Normal file
14
src/prisma/migrations/20231112221155_/migration.sql
Normal 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;
|
||||
@@ -29,19 +29,44 @@ model User {
|
||||
accountIsVerified Boolean @default(false)
|
||||
dateOfBirth DateTime
|
||||
role Role @default(USER)
|
||||
bio String?
|
||||
beerPosts BeerPost[]
|
||||
beerStyles BeerStyle[]
|
||||
breweryPosts BreweryPost[]
|
||||
beerComments BeerComment[]
|
||||
breweryComments BreweryComment[]
|
||||
BeerPostLikes BeerPostLike[]
|
||||
BeerImage BeerImage[]
|
||||
BreweryImage BreweryImage[]
|
||||
BreweryPostLike BreweryPostLike[]
|
||||
Location Location[]
|
||||
Glassware Glassware[]
|
||||
BeerStyleLike BeerStyleLike[]
|
||||
BeerStyleComment BeerStyleComment[]
|
||||
beerPostLikes BeerPostLike[]
|
||||
beerImages BeerImage[]
|
||||
breweryImages BreweryImage[]
|
||||
breweryPostLikes BreweryPostLike[]
|
||||
locations BreweryLocation[]
|
||||
glasswares Glassware[]
|
||||
beerStyleLikes BeerStyleLike[]
|
||||
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 {
|
||||
@@ -60,7 +85,7 @@ model BeerPost {
|
||||
updatedAt DateTime? @updatedAt @db.Timestamptz(3)
|
||||
beerComments BeerComment[]
|
||||
beerImages BeerImage[]
|
||||
BeerPostLikes BeerPostLike[]
|
||||
beerPostLikes BeerPostLike[]
|
||||
}
|
||||
|
||||
model BeerPostLike {
|
||||
@@ -108,8 +133,8 @@ model BeerStyle {
|
||||
abvRange Float[]
|
||||
ibuRange Float[]
|
||||
beerPosts BeerPost[]
|
||||
BeerStyleLike BeerStyleLike[]
|
||||
BeerStyleComment BeerStyleComment[]
|
||||
beerStyleLike BeerStyleLike[]
|
||||
beerStyleComment BeerStyleComment[]
|
||||
}
|
||||
|
||||
model BeerStyleLike {
|
||||
@@ -142,10 +167,10 @@ model Glassware {
|
||||
updatedAt DateTime? @updatedAt @db.Timestamptz(3)
|
||||
postedBy User @relation(fields: [postedById], references: [id], onDelete: Cascade)
|
||||
postedById String
|
||||
BeerStyle BeerStyle[]
|
||||
beerStyle BeerStyle[]
|
||||
}
|
||||
|
||||
model Location {
|
||||
model BreweryLocation {
|
||||
id String @id @default(cuid())
|
||||
city String
|
||||
stateOrProvince String?
|
||||
@@ -154,7 +179,7 @@ model Location {
|
||||
address String
|
||||
postedBy User @relation(fields: [postedById], references: [id], onDelete: Cascade)
|
||||
postedById String
|
||||
BreweryPost BreweryPost?
|
||||
breweryPost BreweryPost?
|
||||
createdAt DateTime @default(now()) @db.Timestamptz(3)
|
||||
updatedAt DateTime? @updatedAt @db.Timestamptz(3)
|
||||
}
|
||||
@@ -162,7 +187,7 @@ model Location {
|
||||
model BreweryPost {
|
||||
id String @id @default(cuid())
|
||||
name String
|
||||
location Location @relation(fields: [locationId], references: [id])
|
||||
location BreweryLocation @relation(fields: [locationId], references: [id])
|
||||
locationId String @unique
|
||||
beers BeerPost[]
|
||||
description String
|
||||
|
||||
@@ -3,6 +3,7 @@ import { z } from 'zod';
|
||||
import { hashPassword } from '../../../config/auth/passwordFns';
|
||||
import DBClient from '../../DBClient';
|
||||
import GetUserSchema from '../../../services/User/schema/GetUserSchema';
|
||||
import imageUrls from '../util/imageUrls';
|
||||
|
||||
const createAdminUser = async () => {
|
||||
const hash = await hashPassword('Pas!3word');
|
||||
@@ -15,6 +16,14 @@ const createAdminUser = async () => {
|
||||
dateOfBirth: new Date('1990-01-01'),
|
||||
role: 'ADMIN',
|
||||
hash,
|
||||
userAvatar: {
|
||||
create: {
|
||||
path: imageUrls[Math.floor(Math.random() * imageUrls.length)],
|
||||
alt: 'Admin User',
|
||||
caption: 'Admin User',
|
||||
createdAt: new Date(),
|
||||
},
|
||||
},
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
@@ -27,6 +36,8 @@ const createAdminUser = async () => {
|
||||
accountIsVerified: true,
|
||||
updatedAt: true,
|
||||
role: true,
|
||||
bio: true,
|
||||
userAvatar: true,
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
// eslint-disable-next-line import/no-extraneous-dependencies
|
||||
import { faker } from '@faker-js/faker';
|
||||
import { Location, User } from '@prisma/client';
|
||||
import { BreweryLocation, User } from '@prisma/client';
|
||||
import DBClient from '../../DBClient';
|
||||
|
||||
interface CreateNewBreweryPostsArgs {
|
||||
numberOfPosts: number;
|
||||
joinData: {
|
||||
users: User[];
|
||||
locations: Location[];
|
||||
locations: BreweryLocation[];
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
39
src/prisma/seed/create/createNewUserAvatars.ts
Normal file
39
src/prisma/seed/create/createNewUserAvatars.ts
Normal 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;
|
||||
49
src/prisma/seed/create/createNewUserFollows.ts
Normal file
49
src/prisma/seed/create/createNewUserFollows.ts
Normal 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;
|
||||
@@ -18,6 +18,7 @@ interface UserData {
|
||||
hash: string;
|
||||
accountIsVerified: boolean;
|
||||
role: 'USER' | 'ADMIN';
|
||||
bio: string;
|
||||
}
|
||||
|
||||
const createNewUsers = async ({ numberOfUsers }: CreateNewUsersArgs) => {
|
||||
@@ -54,6 +55,7 @@ const createNewUsers = async ({ numberOfUsers }: CreateNewUsersArgs) => {
|
||||
|
||||
const dateOfBirth = faker.date.birthdate({ mode: 'age', min: 19 });
|
||||
const createdAt = faker.date.past({ years: 4 });
|
||||
const bio = faker.lorem.paragraphs(3).replace(/\n/g, ' ');
|
||||
|
||||
const user: UserData = {
|
||||
firstName,
|
||||
@@ -63,6 +65,7 @@ const createNewUsers = async ({ numberOfUsers }: CreateNewUsersArgs) => {
|
||||
dateOfBirth,
|
||||
createdAt,
|
||||
hash,
|
||||
bio,
|
||||
accountIsVerified: true,
|
||||
role: 'USER',
|
||||
};
|
||||
@@ -71,7 +74,9 @@ const createNewUsers = async ({ numberOfUsers }: CreateNewUsersArgs) => {
|
||||
}
|
||||
|
||||
await prisma.user.createMany({ data, skipDuplicates: true });
|
||||
return prisma.user.findMany();
|
||||
return prisma.user.findMany({
|
||||
where: { role: { not: 'ADMIN' } },
|
||||
});
|
||||
};
|
||||
|
||||
export default createNewUsers;
|
||||
|
||||
@@ -18,6 +18,8 @@ import logger from '../../config/pino/logger';
|
||||
import createAdminUser from './create/createAdminUser';
|
||||
import createNewBeerStyleComments from './create/createNewBeerStyleComments';
|
||||
import createNewBeerStyleLikes from './create/createNewBeerStyleLikes';
|
||||
import createNewUserAvatars from './create/createNewUserAvatars';
|
||||
import createNewUserFollows from './create/createNewUserFollows';
|
||||
|
||||
(async () => {
|
||||
try {
|
||||
@@ -33,6 +35,12 @@ import createNewBeerStyleLikes from './create/createNewBeerStyleLikes';
|
||||
const users = await createNewUsers({ numberOfUsers: 10000 });
|
||||
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({
|
||||
numberOfLocations: 500,
|
||||
joinData: { users },
|
||||
@@ -103,6 +111,8 @@ import createNewBeerStyleLikes from './create/createNewBeerStyleLikes';
|
||||
logger.info('Database seeded successfully.');
|
||||
logger.info({
|
||||
numberOfUsers: users.length,
|
||||
numberOfUserAvatars: userAvatars.length,
|
||||
numberOfUserFollows: userFollows.length,
|
||||
numberOfBreweryPosts: breweryPosts.length,
|
||||
numberOfBeerPosts: beerPosts.length,
|
||||
numberOfBeerStyles: beerStyles.length,
|
||||
|
||||
@@ -27,7 +27,7 @@ const createNewBeerComment = async ({
|
||||
id: true,
|
||||
content: true,
|
||||
rating: true,
|
||||
postedBy: { select: { id: true, username: true } },
|
||||
postedBy: { select: { id: true, username: true, userAvatar: true } },
|
||||
createdAt: true,
|
||||
updatedAt: true,
|
||||
},
|
||||
|
||||
@@ -22,7 +22,9 @@ const editBeerCommentById = async ({
|
||||
rating: true,
|
||||
createdAt: true,
|
||||
updatedAt: true,
|
||||
postedBy: { select: { id: true, username: true, createdAt: true } },
|
||||
postedBy: {
|
||||
select: { id: true, username: true, createdAt: true, userAvatar: true },
|
||||
},
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
@@ -17,7 +17,9 @@ const findBeerCommentById = async ({
|
||||
rating: true,
|
||||
createdAt: true,
|
||||
updatedAt: true,
|
||||
postedBy: { select: { id: true, username: true, createdAt: true } },
|
||||
postedBy: {
|
||||
select: { id: true, username: true, createdAt: true, userAvatar: true },
|
||||
},
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
@@ -24,7 +24,9 @@ const getAllBeerComments = async ({
|
||||
rating: true,
|
||||
createdAt: true,
|
||||
updatedAt: true,
|
||||
postedBy: { select: { id: true, username: true, createdAt: true } },
|
||||
postedBy: {
|
||||
select: { id: true, username: true, createdAt: true, userAvatar: true },
|
||||
},
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
@@ -36,7 +36,16 @@ const createNewBeerPost = ({
|
||||
ibu: true,
|
||||
createdAt: 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 } },
|
||||
style: { select: { id: true, name: true, description: true } },
|
||||
postedBy: { select: { id: true, username: true } },
|
||||
|
||||
@@ -19,7 +19,16 @@ const deleteBeerPostById = ({
|
||||
id: true,
|
||||
name: 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 } },
|
||||
postedBy: { select: { id: true, username: true } },
|
||||
brewery: { select: { id: true, name: true } },
|
||||
|
||||
@@ -25,7 +25,16 @@ const editBeerPostById = ({
|
||||
ibu: true,
|
||||
createdAt: 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 } },
|
||||
style: { select: { id: true, name: true, description: true } },
|
||||
postedBy: { select: { id: true, username: true } },
|
||||
|
||||
@@ -25,7 +25,16 @@ const getAllBeerPosts = ({
|
||||
style: { select: { name: true, id: true, description: true } },
|
||||
brewery: { select: { name: true, id: 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,
|
||||
skip: (pageNum - 1) * pageSize,
|
||||
|
||||
@@ -19,7 +19,16 @@ const getBeerPostById = async (
|
||||
postedBy: { select: { username: true, id: true } },
|
||||
brewery: { select: { name: true, id: 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 },
|
||||
});
|
||||
|
||||
@@ -28,7 +28,16 @@ const getBeerPostsByBeerStyleId = async ({
|
||||
postedBy: { select: { username: true, id: true } },
|
||||
brewery: { select: { name: true, id: 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,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -28,7 +28,16 @@ const getBeerPostsByBeerStyleId = async ({
|
||||
postedBy: { select: { username: true, id: true } },
|
||||
brewery: { select: { name: true, id: 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,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -37,7 +37,16 @@ const getBeerRecommendations = async ({
|
||||
style: { select: { name: true, id: true, description: true } },
|
||||
brewery: { select: { name: true, id: 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,
|
||||
skip,
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import ImageQueryValidationSchema from '@/services/schema/ImageSchema/ImageQueryValidationSchema';
|
||||
import { z } from 'zod';
|
||||
|
||||
const BeerPostQueryResult = z.object({
|
||||
@@ -5,9 +6,7 @@ const BeerPostQueryResult = z.object({
|
||||
name: z.string(),
|
||||
brewery: z.object({ id: z.string(), name: z.string() }),
|
||||
description: z.string(),
|
||||
beerImages: z.array(
|
||||
z.object({ path: z.string(), caption: z.string(), id: z.string(), alt: z.string() }),
|
||||
),
|
||||
beerImages: z.array(ImageQueryValidationSchema),
|
||||
ibu: z.number(),
|
||||
abv: z.number(),
|
||||
style: z.object({ id: z.string(), name: z.string(), description: z.string() }),
|
||||
|
||||
@@ -27,9 +27,11 @@ const createNewBeerStyleComment = async ({
|
||||
id: true,
|
||||
content: true,
|
||||
rating: true,
|
||||
postedBy: { select: { id: true, username: true } },
|
||||
createdAt: true,
|
||||
updatedAt: true,
|
||||
postedBy: {
|
||||
select: { id: true, username: true, createdAt: true, userAvatar: true },
|
||||
},
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
@@ -24,7 +24,9 @@ const getAllBeerStyleComments = async ({
|
||||
rating: true,
|
||||
createdAt: true,
|
||||
updatedAt: true,
|
||||
postedBy: { select: { id: true, username: true, createdAt: true } },
|
||||
postedBy: {
|
||||
select: { id: true, username: true, createdAt: true, userAvatar: true },
|
||||
},
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
@@ -27,9 +27,11 @@ const createNewBreweryComment = async ({
|
||||
id: true,
|
||||
content: true,
|
||||
rating: true,
|
||||
postedBy: { select: { id: true, username: true } },
|
||||
createdAt: true,
|
||||
updatedAt: true,
|
||||
postedBy: {
|
||||
select: { id: true, username: true, createdAt: true, userAvatar: true },
|
||||
},
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
@@ -23,7 +23,9 @@ const getAllBreweryComments = async ({
|
||||
rating: true,
|
||||
createdAt: true,
|
||||
updatedAt: true,
|
||||
postedBy: { select: { id: true, username: true, createdAt: true } },
|
||||
postedBy: {
|
||||
select: { id: true, username: true, createdAt: true, userAvatar: true },
|
||||
},
|
||||
},
|
||||
orderBy: { createdAt: 'desc' },
|
||||
});
|
||||
|
||||
@@ -37,7 +37,16 @@ const createNewBreweryPost = async ({
|
||||
createdAt: true,
|
||||
dateEstablished: 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: {
|
||||
select: {
|
||||
city: true,
|
||||
|
||||
@@ -29,7 +29,16 @@ const getAllBreweryPosts = async ({
|
||||
description: true,
|
||||
name: 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,
|
||||
dateEstablished: true,
|
||||
},
|
||||
|
||||
@@ -19,7 +19,16 @@ const getBreweryPostById = async (id: string) => {
|
||||
},
|
||||
description: 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 } },
|
||||
createdAt: true,
|
||||
dateEstablished: true,
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import ImageQueryValidationSchema from '@/services/schema/ImageSchema/ImageQueryValidationSchema';
|
||||
import { z } from 'zod';
|
||||
|
||||
const BreweryPostQueryResult = z.object({
|
||||
@@ -12,9 +13,7 @@ const BreweryPostQueryResult = z.object({
|
||||
stateOrProvince: z.string().nullable(),
|
||||
}),
|
||||
postedBy: z.object({ id: z.string(), username: z.string() }),
|
||||
breweryImages: z.array(
|
||||
z.object({ path: z.string(), caption: z.string(), id: z.string(), alt: z.string() }),
|
||||
),
|
||||
breweryImages: z.array(ImageQueryValidationSchema),
|
||||
createdAt: z.coerce.date(),
|
||||
dateEstablished: z.coerce.date(),
|
||||
});
|
||||
|
||||
@@ -33,6 +33,8 @@ const createNewUser = async ({
|
||||
accountIsVerified: true,
|
||||
updatedAt: true,
|
||||
role: true,
|
||||
userAvatar: true,
|
||||
bio: true,
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -17,6 +17,8 @@ const deleteUserById = async (id: string) => {
|
||||
accountIsVerified: true,
|
||||
updatedAt: true,
|
||||
role: true,
|
||||
userAvatar: true,
|
||||
bio: true,
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -17,6 +17,17 @@ const findUserById = async (id: string) => {
|
||||
accountIsVerified: true,
|
||||
updatedAt: true,
|
||||
role: true,
|
||||
userAvatar: {
|
||||
select: {
|
||||
path: true,
|
||||
alt: true,
|
||||
caption: true,
|
||||
createdAt: true,
|
||||
id: true,
|
||||
updatedAt: true,
|
||||
},
|
||||
},
|
||||
bio: true,
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import ImageQueryValidationSchema from '@/services/schema/ImageSchema/ImageQueryValidationSchema';
|
||||
import { z } from 'zod';
|
||||
|
||||
const GetUserSchema = z.object({
|
||||
@@ -11,6 +12,8 @@ const GetUserSchema = z.object({
|
||||
dateOfBirth: z.coerce.date(),
|
||||
accountIsVerified: z.boolean(),
|
||||
role: z.enum(['USER', 'ADMIN']),
|
||||
bio: z.string().nullable(),
|
||||
userAvatar: ImageQueryValidationSchema.nullable(),
|
||||
});
|
||||
|
||||
export default GetUserSchema;
|
||||
|
||||
@@ -17,6 +17,17 @@ const updateUserToBeConfirmedById = async (id: string) => {
|
||||
updatedAt: true,
|
||||
dateOfBirth: true,
|
||||
role: true,
|
||||
bio: true,
|
||||
userAvatar: {
|
||||
select: {
|
||||
id: true,
|
||||
path: true,
|
||||
alt: true,
|
||||
caption: true,
|
||||
createdAt: true,
|
||||
updatedAt: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
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;
|
||||
@@ -1,15 +1,17 @@
|
||||
import { z } from 'zod';
|
||||
import ImageQueryValidationSchema from '../ImageSchema/ImageQueryValidationSchema';
|
||||
|
||||
const CommentQueryResult = z.object({
|
||||
id: z.string().cuid(),
|
||||
content: z.string().min(1).max(500),
|
||||
rating: z.number().int().min(1).max(5),
|
||||
createdAt: z.coerce.date(),
|
||||
updatedAt: z.coerce.date().nullable(),
|
||||
postedBy: z.object({
|
||||
id: z.string().cuid(),
|
||||
username: z.string().min(1).max(50),
|
||||
userAvatar: ImageQueryValidationSchema.nullable(),
|
||||
}),
|
||||
updatedAt: z.coerce.date().nullable(),
|
||||
});
|
||||
|
||||
export default CommentQueryResult;
|
||||
|
||||
@@ -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;
|
||||
Reference in New Issue
Block a user