From 40515b3099a6d81d2dc7784dc14981946bf187e4 Mon Sep 17 00:00:00 2001 From: Stevan Freeborn <65925598+StevanFreeborn@users.noreply.github.com> Date: Thu, 14 Sep 2023 23:02:47 -0500 Subject: [PATCH] feat: add likes + add ability for user to like a posts + add ability for user ot remove a like + add display of reply and like count --- .vscode/settings.json | 3 +- convex/_generated/api.d.ts | 2 + convex/likes.ts | 99 ++++++++++++++++++++++++++++ convex/posts.ts | 12 ++++ convex/schema.ts | 6 ++ src/app/posts/[id]/page.tsx | 2 +- src/components/PostActionButtons.tsx | 45 ++++++++++--- src/components/Reply.tsx | 4 +- 8 files changed, 161 insertions(+), 12 deletions(-) create mode 100644 convex/likes.ts diff --git a/.vscode/settings.json b/.vscode/settings.json index fbb8ede..5b7c996 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -9,6 +9,7 @@ "rehype", "signup", "svix", - "tailwindcss" + "tailwindcss", + "totp" ] } diff --git a/convex/_generated/api.d.ts b/convex/_generated/api.d.ts index 8f182c9..12f244c 100644 --- a/convex/_generated/api.d.ts +++ b/convex/_generated/api.d.ts @@ -15,6 +15,7 @@ import type { FunctionReference, } from "convex/server"; import type * as http from "../http"; +import type * as likes from "../likes"; import type * as posts from "../posts"; import type * as users from "../users"; @@ -28,6 +29,7 @@ import type * as users from "../users"; */ declare const fullApi: ApiFromModules<{ http: typeof http; + likes: typeof likes; posts: typeof posts; users: typeof users; }>; diff --git a/convex/likes.ts b/convex/likes.ts new file mode 100644 index 0000000..80946e4 --- /dev/null +++ b/convex/likes.ts @@ -0,0 +1,99 @@ +import { v } from 'convex/values'; +import { mutation, query } from './_generated/server'; +import { userQuery } from './users'; + +export const addLike = mutation({ + args: { postId: v.id('posts') }, + handler: async (ctx, args) => { + const clerkUser = await ctx.auth.getUserIdentity(); + + if (clerkUser === null) { + return; + } + + const user = await userQuery(ctx, clerkUser.subject); + + if (user === null) { + return; + } + + const existingLike = await ctx.db + .query('likes') + .withIndex('by_user_post_id', q => + q.eq('userId', user._id).eq('postId', args.postId) + ) + .unique(); + + if (existingLike !== null) { + return; + } + + await ctx.db.insert('likes', { postId: args.postId, userId: user._id }); + }, +}); + +export const removeLike = mutation({ + args: { postId: v.id('posts') }, + handler: async (ctx, args) => { + const clerkUser = await ctx.auth.getUserIdentity(); + + if (clerkUser === null) { + return; + } + + const user = await userQuery(ctx, clerkUser.subject); + + if (user === null) { + return; + } + + const existingLike = await ctx.db + .query('likes') + .withIndex('by_user_post_id', q => + q.eq('userId', user._id).eq('postId', args.postId) + ) + .unique(); + + if (existingLike === null) { + return; + } + + await ctx.db.delete(existingLike._id); + }, +}); + +export const getLike = query({ + args: { postId: v.id('posts') }, + handler: async (ctx, args) => { + const clerkUser = await ctx.auth.getUserIdentity(); + + if (clerkUser === null) { + return; + } + + const user = await userQuery(ctx, clerkUser.subject); + + if (user === null) { + return; + } + + return await ctx.db + .query('likes') + .withIndex('by_post_user_id', q => + q.eq('postId', args.postId).eq('userId', user._id) + ) + .unique(); + }, +}); + +export const getPostLikeCount = query({ + args: { postId: v.id('posts') }, + handler: async (ctx, args) => { + const likes = await ctx.db + .query('likes') + .withIndex('by_post_user_id', q => q.eq('postId', args.postId)) + .collect(); + + return likes.length; + }, +}); diff --git a/convex/posts.ts b/convex/posts.ts index fb87802..c443cea 100644 --- a/convex/posts.ts +++ b/convex/posts.ts @@ -135,3 +135,15 @@ export const getRepliesByParentId = query({ return { ...replies, page: repliesWithUserData }; }, }); + +export const getPostReplyCount = query({ + args: { postId: v.id('posts') }, + handler: async (ctx, args) => { + const replies = await ctx.db + .query('posts') + .withIndex('by_parent_id', q => q.eq('parentPostId', args.postId)) + .collect(); + + return replies.length; + }, +}); diff --git a/convex/schema.ts b/convex/schema.ts index 4431f79..3e8033d 100644 --- a/convex/schema.ts +++ b/convex/schema.ts @@ -42,6 +42,12 @@ export default defineSchema( }) .index('by_user_id', ['userId']) .index('by_parent_id', ['parentPostId']), + likes: defineTable({ + postId: v.id('posts'), + userId: v.id('users'), + }) + .index('by_user_post_id', ['userId', 'postId']) + .index('by_post_user_id', ['postId', 'userId']), }, { schemaValidation: false } ); diff --git a/src/app/posts/[id]/page.tsx b/src/app/posts/[id]/page.tsx index b5b12c0..bce3146 100644 --- a/src/app/posts/[id]/page.tsx +++ b/src/app/posts/[id]/page.tsx @@ -32,7 +32,7 @@ export default function PostPage({ if (replyRef.current !== null && isReply) { replyRef.current.scrollIntoView(); } - }, [isReply, post]); + }, [isReply, user]); async function submitAction({ clerkUserId, diff --git a/src/components/PostActionButtons.tsx b/src/components/PostActionButtons.tsx index 4df11e6..2dfa179 100644 --- a/src/components/PostActionButtons.tsx +++ b/src/components/PostActionButtons.tsx @@ -1,6 +1,8 @@ +import { useMutation, useQuery } from 'convex/react'; import Link from 'next/link'; import { BiSolidLike } from 'react-icons/bi'; import { BsFillReplyFill } from 'react-icons/bs'; +import { api } from '../../convex/_generated/api'; import { Id } from '../../convex/_generated/dataModel'; export default function PostActionButton({ @@ -10,22 +12,49 @@ export default function PostActionButton({ postId: Id<'posts'>; showReply: boolean; }) { + const like = useQuery(api.likes.getLike, { postId }); + const likeCount = useQuery(api.likes.getPostLikeCount, { postId }); + const replyCount = useQuery(api.posts.getPostReplyCount, { postId }); + const removeLike = useMutation(api.likes.removeLike); + const addLike = useMutation(api.likes.addLike); + const isLiked = like !== null; + + async function handleLikeClick() { + if (isLiked) { + await removeLike({ postId }); + return; + } + + await addLike({ postId }); + } + return (
- {showReply ? ( + {showReply && replyCount !== undefined ? ( +
+ {replyCount} +
) : null} - + {like !== undefined && likeCount !== undefined ? ( + + ) : null}
); } diff --git a/src/components/Reply.tsx b/src/components/Reply.tsx index 5977c1a..0576b24 100644 --- a/src/components/Reply.tsx +++ b/src/components/Reply.tsx @@ -27,7 +27,7 @@ export default function Reply({ post }: { post: PostWithUserDto }) {
-
+
{`${username} `} {`replied on ${month} ${day}, ${year}`} @@ -37,7 +37,7 @@ export default function Reply({ post }: { post: PostWithUserDto }) { ) : null}
-
+