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.
|
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
1648
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",
|
"@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
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 { 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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
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 { 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;
|
||||||
|
|||||||
@@ -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';
|
||||||
|
|||||||
@@ -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),
|
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',
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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: [
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
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 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)
|
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
|
||||||
|
|||||||
@@ -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,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -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[];
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
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;
|
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;
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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 },
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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 },
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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 },
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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 } },
|
||||||
|
|||||||
@@ -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 } },
|
||||||
|
|||||||
@@ -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 } },
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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 },
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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,
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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() }),
|
||||||
|
|||||||
@@ -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 },
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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 },
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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 },
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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' },
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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(),
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -33,6 +33,8 @@ const createNewUser = async ({
|
|||||||
accountIsVerified: true,
|
accountIsVerified: true,
|
||||||
updatedAt: true,
|
updatedAt: true,
|
||||||
role: true,
|
role: true,
|
||||||
|
userAvatar: true,
|
||||||
|
bio: true,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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,
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
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 { 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;
|
||||||
|
|||||||
@@ -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