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/convex/users.ts b/convex/users.ts index bccc86d..0e11f13 100644 --- a/convex/users.ts +++ b/convex/users.ts @@ -88,13 +88,18 @@ export const deleteUser = internalMutation({ .withIndex('by_user_id', q => q.eq('userId', userRecord._id)) .collect(); - const userPostDeletePromises = []; + const userLikes = await ctx.db + .query('likes') + .withIndex('by_user_post_id', q => q.eq('userId', userRecord._id)) + .collect(); - for (const post of userPosts) { - userPostDeletePromises.push(ctx.db.delete(post._id)); - } + await Promise.all( + userPosts.map(async post => await ctx.db.delete(post._id)) + ); - await Promise.all(userPostDeletePromises); + await Promise.all( + userLikes.map(async like => await ctx.db.delete(like._id)) + ); await ctx.db.delete(userRecord._id); }, 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}
-
+