feat: add beer style likes

This commit is contained in:
Aaron William Po
2023-10-23 22:50:43 -04:00
parent c8e8207e30
commit 6bd2d4713e
11 changed files with 383 additions and 8 deletions

View File

@@ -8,6 +8,8 @@ import { FaRegEdit } from 'react-icons/fa';
import { z } from 'zod';
import useTimeDistance from '@/hooks/utilities/useTimeDistance';
import BeerStyleQueryResult from '@/services/BeerStyles/schema/BeerStyleQueryResult';
import useBeerStyleLikeCount from '@/hooks/data-fetching/beer-style-likes/useBeerStyleLikeCount';
import BeerStyleLikeButton from './BeerStyleLikeButton';
interface BeerInfoHeaderProps {
beerStyle: z.infer<typeof BeerStyleQueryResult>;
@@ -21,7 +23,7 @@ const BeerStyleHeader: FC<BeerInfoHeaderProps> = ({ beerStyle }) => {
const idMatches = user && beerStyle.postedBy.id === user.id;
const isPostOwner = !!(user && idMatches);
// const { likeCount, mutate } = useBeerStyleLikeCount(beerStyle.id);
const { likeCount, mutate } = useBeerStyleLikeCount(beerStyle.id);
return (
<article className="card flex flex-col justify-center bg-base-300">
@@ -82,14 +84,18 @@ const BeerStyleHeader: FC<BeerInfoHeaderProps> = ({ beerStyle }) => {
<span className="text-sm font-bold italic">{beerStyle.glassware.name}</span>
</div>
<div className="flex justify-between">
<div>
{(!!likeCount || likeCount === 0) && (
<span>
Liked by {likeCount}
{likeCount !== 1 ? ' users' : ' user'}
</span>
)}
</div>
<div className="card-actions items-end">
{/* {user && (
<BeerStyleLikeButton
beerStyle={beerStyle}
likeCount={likeCount}
mutate={mutate}
/>
)} */}
{user && (
<BeerStyleLikeButton beerStyleId={beerStyle.id} mutateCount={mutate} />
)}
</div>
</div>
</div>

View File

@@ -0,0 +1,34 @@
import { FC, useEffect, useState } from 'react';
import useGetBeerPostLikeCount from '@/hooks/data-fetching/beer-likes/useBeerPostLikeCount';
import useCheckIfUserLikesBeerStyle from '@/hooks/data-fetching/beer-style-likes/useCheckIfUserLikesBeerPost';
import sendBeerStyleLikeRequest from '@/requests/BeerStyleLike/sendBeerStyleLikeRequest';
import LikeButton from '../ui/LikeButton';
const BeerStyleLikeButton: FC<{
beerStyleId: string;
mutateCount: ReturnType<typeof useGetBeerPostLikeCount>['mutate'];
}> = ({ beerStyleId, mutateCount }) => {
const { isLiked, mutate: mutateLikeStatus } = useCheckIfUserLikesBeerStyle(beerStyleId);
const [loading, setLoading] = useState(true);
useEffect(() => {
setLoading(false);
}, [isLiked]);
const handleLike = async () => {
try {
setLoading(true);
await sendBeerStyleLikeRequest(beerStyleId);
await Promise.all([mutateCount(), mutateLikeStatus()]);
setLoading(false);
} catch (e) {
setLoading(false);
}
};
return <LikeButton isLiked={!!isLiked} handleLike={handleLike} loading={loading} />;
};
export default BeerStyleLikeButton;

View File

@@ -0,0 +1,52 @@
import APIResponseValidationSchema from '@/validation/APIResponseValidationSchema';
import { z } from 'zod';
import useSWR from 'swr';
/**
* Custom hook to fetch the like count for a beer style from the server.
*
* @param beerStyleId - The ID of the beer style to fetch the like count for.
* @returns An object with the following properties:
*
* - `error`: The error that occurred while fetching the like count.
* - `isLoading`: A boolean indicating whether the like count is being fetched.
* - `mutate`: A function to mutate the like count.
* - `likeCount`: The like count for the beer style.
*/
const useGetBeerStyleLikeCount = (beerStyleId: string) => {
const { error, mutate, data, isLoading } = useSWR(
`/api/beers/styles/${beerStyleId}/like`,
async (url) => {
const response = await fetch(url);
const json = await response.json();
const parsed = APIResponseValidationSchema.safeParse(json);
if (!parsed.success) {
throw new Error('Failed to parse API response');
}
const parsedPayload = z
.object({
likeCount: z.number(),
})
.safeParse(parsed.data.payload);
if (!parsedPayload.success) {
throw new Error('Failed to parse API response payload');
}
return parsedPayload.data.likeCount;
},
);
return {
error: error as unknown,
isLoading,
mutate,
likeCount: data as number | undefined,
};
};
export default useGetBeerStyleLikeCount;

View File

@@ -0,0 +1,57 @@
import UserContext from '@/contexts/UserContext';
import APIResponseValidationSchema from '@/validation/APIResponseValidationSchema';
import { useContext } from 'react';
import useSWR from 'swr';
import { z } from 'zod';
/**
* A custom React hook that checks if the current user has liked a beer style by fetching
* data from the server.
*
* @param beerStyleId The ID of the beer style to check for likes.
* @returns An object with the following properties:
*
* - `error`: The error that occurred while fetching the data.
* - `isLoading`: A boolean indicating whether the data is being fetched.
* - `mutate`: A function to mutate the data.
* - `isLiked`: A boolean indicating whether the current user has liked the beer style.
*/
const useCheckIfUserLikesBeerStyle = (beerStyleId: string) => {
const { user } = useContext(UserContext);
const { data, error, isLoading, mutate } = useSWR(
`/api/beers/styles/${beerStyleId}/like/is-liked`,
async (url) => {
if (!user) {
throw new Error('User is not logged in.');
}
const response = await fetch(url);
const json = await response.json();
const parsed = APIResponseValidationSchema.safeParse(json);
if (!parsed.success) {
throw new Error('Invalid API response.');
}
const { payload } = parsed.data;
const parsedPayload = z.object({ isLiked: z.boolean() }).safeParse(payload);
if (!parsedPayload.success) {
throw new Error('Invalid API response.');
}
const { isLiked } = parsedPayload.data;
return isLiked;
},
);
return {
isLiked: data,
error: error as unknown,
isLoading,
mutate,
};
};
export default useCheckIfUserLikesBeerStyle;

View File

@@ -0,0 +1,86 @@
import { createRouter } from 'next-connect';
import { z } from 'zod';
import { NextApiRequest, NextApiResponse } from 'next';
import APIResponseValidationSchema from '@/validation/APIResponseValidationSchema';
import validateRequest from '@/config/nextConnect/middleware/validateRequest';
import { UserExtendedNextApiRequest } from '@/config/auth/types';
import ServerError from '@/config/util/ServerError';
import getCurrentUser from '@/config/nextConnect/middleware/getCurrentUser';
import NextConnectOptions from '@/config/nextConnect/NextConnectOptions';
import getBeerStyleById from '@/services/BeerStyles/getBeerStyleById';
import findBeerStyleLikeById from '@/services/BeerStyleLike/findBeerStyleLikeById';
import getBeerStyleLikeCount from '@/services/BeerStyleLike/getBeerStyleLikeCount';
import createBeerStyleLike from '@/services/BeerStyleLike/createBeerStyleLike';
import removeBeerStyleLikeById from '@/services/BeerStyleLike/removeBeerStyleLikeById';
const sendLikeRequest = async (
req: UserExtendedNextApiRequest,
res: NextApiResponse<z.infer<typeof APIResponseValidationSchema>>,
) => {
const user = req.user!;
const id = req.query.id as string;
const beerStyle = await getBeerStyleById(id);
if (!beerStyle) {
throw new ServerError('Could not find a beer style with that id', 404);
}
const alreadyLiked = await findBeerStyleLikeById({
beerStyleId: beerStyle.id,
likedById: user.id,
});
const jsonResponse = {
success: true as const,
message: '',
statusCode: 200 as const,
};
if (alreadyLiked) {
await removeBeerStyleLikeById({ beerStyleLikeId: alreadyLiked.id });
jsonResponse.message = 'Successfully unliked beer style.';
} else {
await createBeerStyleLike({ beerStyleId: beerStyle.id, user });
jsonResponse.message = 'Successfully liked beer style.';
}
res.status(200).json(jsonResponse);
};
const getLikeCount = async (
req: NextApiRequest,
res: NextApiResponse<z.infer<typeof APIResponseValidationSchema>>,
) => {
const id = req.query.id as string;
const likeCount = await getBeerStyleLikeCount({ beerStyleId: id });
res.status(200).json({
success: true,
message: 'Successfully retrieved like count.',
statusCode: 200,
payload: { likeCount },
});
};
const router = createRouter<
UserExtendedNextApiRequest,
NextApiResponse<z.infer<typeof APIResponseValidationSchema>>
>();
router.post(
getCurrentUser,
validateRequest({ querySchema: z.object({ id: z.string().cuid() }) }),
sendLikeRequest,
);
router.get(
validateRequest({ querySchema: z.object({ id: z.string().cuid() }) }),
getLikeCount,
);
const handler = router.handler(NextConnectOptions);
export default handler;

View File

@@ -0,0 +1,53 @@
import getCurrentUser from '@/config/nextConnect/middleware/getCurrentUser';
import { UserExtendedNextApiRequest } from '@/config/auth/types';
import NextConnectOptions from '@/config/nextConnect/NextConnectOptions';
import validateRequest from '@/config/nextConnect/middleware/validateRequest';
import APIResponseValidationSchema from '@/validation/APIResponseValidationSchema';
import { NextApiResponse } from 'next';
import { createRouter } from 'next-connect';
import { z } from 'zod';
import DBClient from '@/prisma/DBClient';
interface FindBeerStyleLikeByIdArgs {
beerStyleId: string;
likedById: string;
}
const findBeerStyleLikeById = async ({
beerStyleId,
likedById,
}: FindBeerStyleLikeByIdArgs) => {
return DBClient.instance.beerStyleLike.findFirst({
where: { beerStyleId, likedById },
});
};
const checkIfLiked = async (
req: UserExtendedNextApiRequest,
res: NextApiResponse<z.infer<typeof APIResponseValidationSchema>>,
) => {
const user = req.user!;
const beerStyleId = req.query.id as string;
const alreadyLiked = await findBeerStyleLikeById({ beerStyleId, likedById: user.id });
res.status(200).json({
success: true,
message: alreadyLiked ? 'Beer style is liked.' : 'Beer style is not liked.',
statusCode: 200,
payload: { isLiked: !!alreadyLiked },
});
};
const router = createRouter<
UserExtendedNextApiRequest,
NextApiResponse<z.infer<typeof APIResponseValidationSchema>>
>();
router.get(
getCurrentUser,
validateRequest({ querySchema: z.object({ id: z.string().cuid() }) }),
checkIfLiked,
);
const handler = router.handler(NextConnectOptions);
export default handler;

View File

@@ -0,0 +1,31 @@
import APIResponseValidationSchema from '@/validation/APIResponseValidationSchema';
/**
* Sends a POST request to the server to like or unlike a beer post.
*
* @param beerStyleId The ID of the beer post to like or unlike.
* @returns An object containing a success boolean and a message string.
* @throws An error if the response is not ok or if the API response is invalid.
*/
const sendBeerStyleLikeRequest = async (beerStyleId: string) => {
const response = await fetch(`/api/beers/styles/${beerStyleId}/like`, {
method: 'POST',
});
if (!response.ok) {
throw new Error('Something went wrong.');
}
const data = await response.json();
const parsed = APIResponseValidationSchema.safeParse(data);
if (!parsed.success) {
throw new Error('Invalid API response.');
}
const { success, message } = parsed.data;
return { success, message };
};
export default sendBeerStyleLikeRequest;

View File

@@ -0,0 +1,18 @@
import { z } from 'zod';
import DBClient from '@/prisma/DBClient';
import GetUserSchema from '@/services/User/schema/GetUserSchema';
interface CreateBeerStyleLikeArgs {
beerStyleId: string;
user: z.infer<typeof GetUserSchema>;
}
const createBeerStyleLike = async ({ beerStyleId, user }: CreateBeerStyleLikeArgs) => {
return DBClient.instance.beerStyleLike.create({
data: {
beerStyleId,
likedById: user.id,
},
});
};
export default createBeerStyleLike;

View File

@@ -0,0 +1,16 @@
import DBClient from '@/prisma/DBClient';
interface FindBeerStyleLikeByIdArgs {
beerStyleId: string;
likedById: string;
}
const findBeerStyleLikeById = async ({
beerStyleId,
likedById,
}: FindBeerStyleLikeByIdArgs) => {
return DBClient.instance.beerStyleLike.findFirst({
where: { beerStyleId, likedById },
});
};
export default findBeerStyleLikeById;

View File

@@ -0,0 +1,10 @@
import DBClient from '@/prisma/DBClient';
interface GetBeerStyleLikeCountArgs {
beerStyleId: string;
}
const getBeerStyleLikeCount = async ({ beerStyleId }: GetBeerStyleLikeCountArgs) => {
return DBClient.instance.beerStyleLike.count({ where: { beerStyleId } });
};
export default getBeerStyleLikeCount;

View File

@@ -0,0 +1,12 @@
import DBClient from '@/prisma/DBClient';
interface RemoveBeerStyleLikeByIdArgs {
beerStyleLikeId: string;
}
const removeBeerStyleLikeById = async ({
beerStyleLikeId,
}: RemoveBeerStyleLikeByIdArgs) => {
return DBClient.instance.beerStyleLike.delete({ where: { id: beerStyleLikeId } });
};
export default removeBeerStyleLikeById;