mirror of
https://github.com/aaronpo97/the-biergarten-app.git
synced 2026-02-16 18:52:06 +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 { z } from 'zod';
|
||||||
import useTimeDistance from '@/hooks/utilities/useTimeDistance';
|
import useTimeDistance from '@/hooks/utilities/useTimeDistance';
|
||||||
import BeerStyleQueryResult from '@/services/BeerStyles/schema/BeerStyleQueryResult';
|
import BeerStyleQueryResult from '@/services/BeerStyles/schema/BeerStyleQueryResult';
|
||||||
|
import useBeerStyleLikeCount from '@/hooks/data-fetching/beer-style-likes/useBeerStyleLikeCount';
|
||||||
|
import BeerStyleLikeButton from './BeerStyleLikeButton';
|
||||||
|
|
||||||
interface BeerInfoHeaderProps {
|
interface BeerInfoHeaderProps {
|
||||||
beerStyle: z.infer<typeof BeerStyleQueryResult>;
|
beerStyle: z.infer<typeof BeerStyleQueryResult>;
|
||||||
@@ -21,7 +23,7 @@ const BeerStyleHeader: FC<BeerInfoHeaderProps> = ({ beerStyle }) => {
|
|||||||
const idMatches = user && beerStyle.postedBy.id === user.id;
|
const idMatches = user && beerStyle.postedBy.id === user.id;
|
||||||
const isPostOwner = !!(user && idMatches);
|
const isPostOwner = !!(user && idMatches);
|
||||||
|
|
||||||
// const { likeCount, mutate } = useBeerStyleLikeCount(beerStyle.id);
|
const { likeCount, mutate } = useBeerStyleLikeCount(beerStyle.id);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<article className="card flex flex-col justify-center bg-base-300">
|
<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>
|
<span className="text-sm font-bold italic">{beerStyle.glassware.name}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex justify-between">
|
<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">
|
<div className="card-actions items-end">
|
||||||
{/* {user && (
|
{user && (
|
||||||
<BeerStyleLikeButton
|
<BeerStyleLikeButton beerStyleId={beerStyle.id} mutateCount={mutate} />
|
||||||
beerStyle={beerStyle}
|
)}
|
||||||
likeCount={likeCount}
|
|
||||||
mutate={mutate}
|
|
||||||
/>
|
|
||||||
)} */}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</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