Skip to content

Commit

Permalink
End of part 25
Browse files Browse the repository at this point in the history
  • Loading branch information
codinginflow committed Jul 12, 2024
1 parent 2e55958 commit 5d8beaa
Show file tree
Hide file tree
Showing 5 changed files with 218 additions and 0 deletions.
12 changes: 12 additions & 0 deletions prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ model User {
posts Post[]
following Follow[] @relation("Following")
followers Follow[] @relation("Followers")
likes Like[]
createdAt DateTime @default(now())
Expand Down Expand Up @@ -59,6 +60,7 @@ model Post {
userId String
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
attachments Media[]
likes Like[]
createdAt DateTime @default(now())
Expand All @@ -81,3 +83,13 @@ enum MediaType {
IMAGE
VIDEO
}

model Like {
userId String
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
postId String
post Post @relation(fields: [postId], references: [id], onDelete: Cascade)
@@unique([userId, postId])
@@map("likes")
}
105 changes: 105 additions & 0 deletions src/app/api/posts/[postId]/likes/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
import { validateRequest } from "@/auth";
import prisma from "@/lib/prisma";
import { LikeInfo } from "@/lib/types";

export async function GET(
req: Request,
{ params: { postId } }: { params: { postId: string } },
) {
try {
const { user: loggedInUser } = await validateRequest();

if (!loggedInUser) {
return Response.json({ error: "Unauthorized" }, { status: 401 });
}

const post = await prisma.post.findUnique({
where: { id: postId },
select: {
likes: {
where: {
userId: loggedInUser.id,
},
select: {
userId: true,
},
},
_count: {
select: {
likes: true,
},
},
},
});

if (!post) {
return Response.json({ error: "Post not found" }, { status: 404 });
}

const data: LikeInfo = {
likes: post._count.likes,
isLikedByUser: !!post.likes.length,
};
return Response.json(data);
} catch (error) {
console.error(error);
return Response.json({ error: "Internal server error" }, { status: 500 });
}
}

export async function POST(
req: Request,
{ params: { postId } }: { params: { postId: string } },
) {
try {
const { user: loggedInUser } = await validateRequest();

if (!loggedInUser) {
return Response.json({ error: "Unauthorized" }, { status: 401 });
}

await prisma.like.upsert({
where: {
userId_postId: {
userId: loggedInUser.id,
postId,
},
},
create: {
userId: loggedInUser.id,
postId,
},
update: {},
});

return new Response();
} catch (error) {
console.error(error);
return Response.json({ error: "Internal server error" }, { status: 500 });
}
}

export async function DELETE(
req: Request,
{ params: { postId } }: { params: { postId: string } },
) {
try {
const { user: loggedInUser } = await validateRequest();

if (!loggedInUser) {
return Response.json({ error: "Unauthorized" }, { status: 401 });
}

await prisma.like.deleteMany({
where: {
userId: loggedInUser.id,
postId,
},
});

return new Response();
} catch (error) {
console.error(error);
return Response.json({ error: "Internal server error" }, { status: 500 });
}
}
74 changes: 74 additions & 0 deletions src/components/posts/LikeButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import kyInstance from "@/lib/ky";
import { LikeInfo } from "@/lib/types";
import { cn } from "@/lib/utils";
import {
QueryKey,
useMutation,
useQuery,
useQueryClient,
} from "@tanstack/react-query";
import { Heart } from "lucide-react";
import { useToast } from "../ui/use-toast";

interface LikeButtonProps {
postId: string;
initialState: LikeInfo;
}

export default function LikeButton({ postId, initialState }: LikeButtonProps) {
const { toast } = useToast();

const queryClient = useQueryClient();

const queryKey: QueryKey = ["like-info", postId];

const { data } = useQuery({
queryKey,
queryFn: () =>
kyInstance.get(`/api/posts/${postId}/likes`).json<LikeInfo>(),
initialData: initialState,
staleTime: Infinity,
});

const { mutate } = useMutation({
mutationFn: () =>
data.isLikedByUser
? kyInstance.delete(`/api/posts/${postId}/likes`)
: kyInstance.post(`/api/posts/${postId}/likes`),
onMutate: async () => {
await queryClient.cancelQueries({ queryKey });

const previousState = queryClient.getQueryData<LikeInfo>(queryKey);

queryClient.setQueryData<LikeInfo>(queryKey, () => ({
likes:
(previousState?.likes || 0) + (previousState?.isLikedByUser ? -1 : 1),
isLikedByUser: !previousState?.isLikedByUser,
}));

return { previousState };
},
onError(error, variables, context) {
queryClient.setQueryData(queryKey, context?.previousState);
console.error(error);
toast({
variant: "destructive",
description: "Something went wrong. Please try again.",
});
},
});

return (
<button onClick={() => mutate()} className="flex items-center gap-2">
<Heart
className={cn(
"size-5",
data.isLikedByUser && "fill-red-500 text-red-500",
)}
/>
<span className="text-sm font-medium tabular-nums">
{data.likes} <span className="hidden sm:inline">likes</span>
</span>
</button>
);
}
9 changes: 9 additions & 0 deletions src/components/posts/Post.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import Link from "next/link";
import Linkify from "../Linkify";
import UserAvatar from "../UserAvatar";
import UserTooltip from "../UserTooltip";
import LikeButton from "./LikeButton";
import PostMoreButton from "./PostMoreButton";

interface PostProps {
Expand Down Expand Up @@ -58,6 +59,14 @@ export default function Post({ post }: PostProps) {
{!!post.attachments.length && (
<MediaPreviews attachments={post.attachments} />
)}
<hr className="text-muted-foreground" />
<LikeButton
postId={post.id}
initialState={{
likes: post._count.likes,
isLikedByUser: post.likes.some((like) => like.userId === user.id),
}}
/>
</article>
);
}
Expand Down
18 changes: 18 additions & 0 deletions src/lib/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,19 @@ export function getPostDataInclude(loggedInUserId: string) {
select: getUserDataSelect(loggedInUserId),
},
attachments: true,
likes: {
where: {
userId: loggedInUserId,
},
select: {
userId: true,
},
},
_count: {
select: {
likes: true,
},
},
} satisfies Prisma.PostInclude;
}

Expand All @@ -51,3 +64,8 @@ export interface FollowerInfo {
followers: number;
isFollowedByUser: boolean;
}

export interface LikeInfo {
likes: number;
isLikedByUser: boolean;
}

0 comments on commit 5d8beaa

Please sign in to comment.