mirror of
https://github.com/aaronpo97/the-biergarten-app.git
synced 2026-02-16 02:39:03 +00:00
feat: add beer style likes
This commit is contained in:
@@ -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>
|
||||
|
||||
34
src/components/BeerStyleById/BeerStyleLikeButton.tsx
Normal file
34
src/components/BeerStyleById/BeerStyleLikeButton.tsx
Normal 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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
86
src/pages/api/beers/styles/[id]/like/index.ts
Normal file
86
src/pages/api/beers/styles/[id]/like/index.ts
Normal 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;
|
||||
53
src/pages/api/beers/styles/[id]/like/is-liked.ts
Normal file
53
src/pages/api/beers/styles/[id]/like/is-liked.ts
Normal 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;
|
||||
31
src/requests/BeerStyleLike/sendBeerStyleLikeRequest.ts
Normal file
31
src/requests/BeerStyleLike/sendBeerStyleLikeRequest.ts
Normal 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;
|
||||
18
src/services/BeerStyleLike/createBeerStyleLike.ts
Normal file
18
src/services/BeerStyleLike/createBeerStyleLike.ts
Normal 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;
|
||||
16
src/services/BeerStyleLike/findBeerStyleLikeById.ts
Normal file
16
src/services/BeerStyleLike/findBeerStyleLikeById.ts
Normal 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;
|
||||
10
src/services/BeerStyleLike/getBeerStyleLikeCount.ts
Normal file
10
src/services/BeerStyleLike/getBeerStyleLikeCount.ts
Normal 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;
|
||||
12
src/services/BeerStyleLike/removeBeerStyleLikeById.ts
Normal file
12
src/services/BeerStyleLike/removeBeerStyleLikeById.ts
Normal 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;
|
||||
Reference in New Issue
Block a user