From f15621c8f1d4355272b25b74fe2b57c64b472861 Mon Sep 17 00:00:00 2001 From: Stevan Freeborn <65925598+StevanFreeborn@users.noreply.github.com> Date: Fri, 15 Sep 2023 13:12:55 -0500 Subject: [PATCH 1/7] fix: count background color --- src/components/PostActionButtons.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/PostActionButtons.tsx b/src/components/PostActionButtons.tsx index 2dfa179..59a626c 100644 --- a/src/components/PostActionButtons.tsx +++ b/src/components/PostActionButtons.tsx @@ -36,7 +36,7 @@ export default function PostActionButton({ href={{ pathname: `/posts/${postId}`, query: { reply: true } }} > -
+
{replyCount}
@@ -50,7 +50,7 @@ export default function PostActionButton({ }`} > {' '} -
+
{likeCount}
From 508bf2113d99d897b47fac70072ac672054139cd Mon Sep 17 00:00:00 2001 From: Stevan Freeborn <65925598+StevanFreeborn@users.noreply.github.com> Date: Fri, 15 Sep 2023 13:26:19 -0500 Subject: [PATCH 2/7] feat: wrap reply and post user images in link to user profile --- src/components/Post.tsx | 16 +++++++++------- src/components/Reply.tsx | 17 ++++++++++------- src/components/UserPosts.tsx | 6 +++--- 3 files changed, 22 insertions(+), 17 deletions(-) diff --git a/src/components/Post.tsx b/src/components/Post.tsx index fda7603..973008b 100644 --- a/src/components/Post.tsx +++ b/src/components/Post.tsx @@ -46,13 +46,15 @@ export default function Post({ return (
- user profile image + + user profile image +
diff --git a/src/components/Reply.tsx b/src/components/Reply.tsx index 0576b24..40ff1b7 100644 --- a/src/components/Reply.tsx +++ b/src/components/Reply.tsx @@ -2,6 +2,7 @@ import { PostWithUserDto } from '@/app/types'; import { useUser } from '@clerk/nextjs'; import { Text } from '@codemirror/state'; import Image from 'next/image'; +import Link from 'next/link'; import PostActionButton from './PostActionButtons'; import PostActionModal from './PostActionModal'; import PostContent from './PostContent'; @@ -17,13 +18,15 @@ export default function Reply({ post }: { post: PostWithUserDto }) { return (
- user profile image + + user profile image +
diff --git a/src/components/UserPosts.tsx b/src/components/UserPosts.tsx index 01bb3fe..2f6c32f 100644 --- a/src/components/UserPosts.tsx +++ b/src/components/UserPosts.tsx @@ -8,7 +8,7 @@ import Loader from './Loader'; import Post from './Post'; export default function UserPosts({ user }: { user: UserDto }) { - const PAGE_SIZE = 10; + const PAGE_SIZE = 1; const pager = usePaginatedQuery( api.posts.getUsersPostById, { @@ -42,12 +42,12 @@ export default function UserPosts({ user }: { user: UserDto }) { } return ( -
+
{postsWithUser.map(post => (
From bc86873d8993b43e79d15f63e902e66c356fa011 Mon Sep 17 00:00:00 2001 From: Stevan Freeborn <65925598+StevanFreeborn@users.noreply.github.com> Date: Fri, 15 Sep 2023 14:24:35 -0500 Subject: [PATCH 3/7] style: small change --- convex/schema.ts | 6 ++++++ public/next.svg | 1 - public/vercel.svg | 1 - src/components/PostActionButtons.tsx | 2 +- 4 files changed, 7 insertions(+), 3 deletions(-) delete mode 100644 public/next.svg delete mode 100644 public/vercel.svg diff --git a/convex/schema.ts b/convex/schema.ts index 3e8033d..374df05 100644 --- a/convex/schema.ts +++ b/convex/schema.ts @@ -48,6 +48,12 @@ export default defineSchema( }) .index('by_user_post_id', ['userId', 'postId']) .index('by_post_user_id', ['postId', 'userId']), + follows: defineTable({ + following: v.id('users'), // person being followed + follower: v.id('users'), // person following + }) + .index('by_following', ['following']) + .index('by_follower', ['follower']), }, { schemaValidation: false } ); diff --git a/public/next.svg b/public/next.svg deleted file mode 100644 index 5174b28..0000000 --- a/public/next.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/public/vercel.svg b/public/vercel.svg deleted file mode 100644 index d2f8422..0000000 --- a/public/vercel.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/src/components/PostActionButtons.tsx b/src/components/PostActionButtons.tsx index 59a626c..fd57b15 100644 --- a/src/components/PostActionButtons.tsx +++ b/src/components/PostActionButtons.tsx @@ -49,7 +49,7 @@ export default function PostActionButton({ isLiked ? 'text-primary-accent' : '' }`} > - {' '} +
{likeCount}
From 2a1b46f06d54b5ce000a728ac6c10acca5c49ea1 Mon Sep 17 00:00:00 2001 From: Stevan Freeborn <65925598+StevanFreeborn@users.noreply.github.com> Date: Fri, 15 Sep 2023 17:02:20 -0500 Subject: [PATCH 4/7] feat: add ability to follow and unfollow a user on their profile page --- convex/_generated/api.d.ts | 2 + convex/follows.ts | 116 +++++++++++++++++++++++++++++++++ convex/posts.ts | 12 ++++ convex/schema.ts | 3 +- src/components/Loader.tsx | 4 +- src/components/UserProfile.tsx | 98 ++++++++++++++++++++++++---- 6 files changed, 221 insertions(+), 14 deletions(-) create mode 100644 convex/follows.ts diff --git a/convex/_generated/api.d.ts b/convex/_generated/api.d.ts index 12f244c..e85cab7 100644 --- a/convex/_generated/api.d.ts +++ b/convex/_generated/api.d.ts @@ -14,6 +14,7 @@ import type { FilterApi, FunctionReference, } from "convex/server"; +import type * as follows from "../follows"; import type * as http from "../http"; import type * as likes from "../likes"; import type * as posts from "../posts"; @@ -28,6 +29,7 @@ import type * as users from "../users"; * ``` */ declare const fullApi: ApiFromModules<{ + follows: typeof follows; http: typeof http; likes: typeof likes; posts: typeof posts; diff --git a/convex/follows.ts b/convex/follows.ts new file mode 100644 index 0000000..38895ed --- /dev/null +++ b/convex/follows.ts @@ -0,0 +1,116 @@ +import { v } from 'convex/values'; +import { mutation, query } from './_generated/server'; +import { userQuery } from './users'; + +export const addFollow = mutation({ + args: { followingId: v.id('users') }, + 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 existingFollow = await ctx.db + .query('follows') + .withIndex('by_following_follower_id', q => + q.eq('following', args.followingId).eq('follower', user._id) + ) + .unique(); + + if (existingFollow !== null) { + return; + } + + await ctx.db.insert('follows', { + following: args.followingId, + follower: user._id, + }); + }, +}); + +export const removeFollow = mutation({ + args: { followingId: v.id('users') }, + 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 existingFollow = await ctx.db + .query('follows') + .withIndex('by_following_follower_id', q => + q.eq('following', args.followingId).eq('follower', user._id) + ) + .unique(); + + if (existingFollow === null) { + return; + } + + await ctx.db.delete(existingFollow._id); + }, +}); + +export const getUserFollowerCount = query({ + args: { userId: v.id('users') }, + handler: async (ctx, args) => { + const followers = await ctx.db + .query('follows') + .withIndex('by_following', q => q.eq('following', args.userId)) + .collect(); + + return followers.length; + }, +}); + +export const getUserFollowingCount = query({ + args: { userId: v.id('users') }, + handler: async (ctx, args) => { + const followings = await ctx.db + .query('follows') + .withIndex('by_follower', q => q.eq('follower', args.userId)) + .collect(); + + return followings.length; + }, +}); + +export const getFollowing = query({ + args: { followingId: v.id('users') }, + handler: async (ctx, args) => { + const currentClerkUser = await ctx.auth.getUserIdentity(); + + if (currentClerkUser == null) { + return false; + } + + const user = await userQuery(ctx, currentClerkUser.subject); + + if (user == null) { + return false; + } + + const following = await ctx.db + .query('follows') + .withIndex('by_following_follower_id', q => + q.eq('following', args.followingId).eq('follower', user._id) + ) + .unique(); + + return following !== null; + }, +}); diff --git a/convex/posts.ts b/convex/posts.ts index c443cea..f4b45ac 100644 --- a/convex/posts.ts +++ b/convex/posts.ts @@ -147,3 +147,15 @@ export const getPostReplyCount = query({ return replies.length; }, }); + +export const getUserPostCount = query({ + args: { userId: v.id('users') }, + handler: async (ctx, args) => { + const posts = await ctx.db + .query('posts') + .withIndex('by_user_id', q => q.eq('userId', args.userId)) + .collect(); + + return posts.length; + }, +}); diff --git a/convex/schema.ts b/convex/schema.ts index 374df05..8d40c56 100644 --- a/convex/schema.ts +++ b/convex/schema.ts @@ -53,7 +53,8 @@ export default defineSchema( follower: v.id('users'), // person following }) .index('by_following', ['following']) - .index('by_follower', ['follower']), + .index('by_follower', ['follower']) + .index('by_following_follower_id', ['following', 'follower']), }, { schemaValidation: false } ); diff --git a/src/components/Loader.tsx b/src/components/Loader.tsx index 7293e4f..0681dce 100644 --- a/src/components/Loader.tsx +++ b/src/components/Loader.tsx @@ -11,7 +11,7 @@ export default function Loader({ }) { if (isLoading) { return ( -
+
Loading...
@@ -25,7 +25,7 @@ export default function Loader({ return (
+ ) : null}
-
+
+
+ {countDataLoaded ? ( + <> +
+ {postCount}{' '} + posts +
+
+ + {followerCount} + {' '} + followers +
+
+ + {followingCount} + {' '} + following +
+ + ) : null} +
+
); } From 31dd29a9c22c8e0cec4e752659185982f23614c1 Mon Sep 17 00:00:00 2001 From: Stevan Freeborn <65925598+StevanFreeborn@users.noreply.github.com> Date: Fri, 15 Sep 2023 17:05:32 -0500 Subject: [PATCH 5/7] fix: only include parent posts in user post count --- convex/posts.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/convex/posts.ts b/convex/posts.ts index f4b45ac..2b04cb7 100644 --- a/convex/posts.ts +++ b/convex/posts.ts @@ -154,6 +154,7 @@ export const getUserPostCount = query({ const posts = await ctx.db .query('posts') .withIndex('by_user_id', q => q.eq('userId', args.userId)) + .filter(q => q.eq(q.field('parentPostId'), undefined)) .collect(); return posts.length; From 638ba3ce5cc626906bd02ccaaf58430ba8f26b6b Mon Sep 17 00:00:00 2001 From: Stevan Freeborn <65925598+StevanFreeborn@users.noreply.github.com> Date: Fri, 15 Sep 2023 17:52:01 -0500 Subject: [PATCH 6/7] feat: display all posts on home page --- convex/posts.ts | 43 +++++++++++++++++++++++++++++++++++ src/app/page.tsx | 11 ++++++++- src/components/AllPosts.tsx | 39 +++++++++++++++++++++++++++++++ src/components/Navbar.tsx | 14 ++++++++++++ src/components/UserButton.tsx | 3 ++- src/components/UserPosts.tsx | 2 +- 6 files changed, 109 insertions(+), 3 deletions(-) create mode 100644 src/components/AllPosts.tsx diff --git a/convex/posts.ts b/convex/posts.ts index 2b04cb7..c96b296 100644 --- a/convex/posts.ts +++ b/convex/posts.ts @@ -160,3 +160,46 @@ export const getUserPostCount = query({ return posts.length; }, }); + +export const getAllPostsWithUser = query({ + args: { paginationOpts: paginationOptsValidator }, + handler: async (ctx, args) => { + const posts = await ctx.db + .query('posts') + .filter(q => q.eq(q.field('parentPostId'), undefined)) + .order('desc') + .paginate(args.paginationOpts); + + const postsWithUser = await Promise.all( + posts.page.map(async post => { + const user = await ctx.db.get(post.userId); + + if (user === null) { + return { + ...post, + user: { + _id: '' as Id<'users'>, + _creationTime: 0, + clerkUsername: null, + clerkImageUrl: '', + clerkUserId: '', + }, + }; + } + + return { + ...post, + user: { + _id: user._id, + _creationTime: user._creationTime, + clerkUsername: user.clerkUser.username, + clerkImageUrl: user.clerkUser.image_url, + clerkUserId: user.clerkUser.id, + }, + }; + }) + ); + + return { ...posts, page: postsWithUser }; + }, +}); diff --git a/src/app/page.tsx b/src/app/page.tsx index 1cdf8fa..0f8b653 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -1,3 +1,12 @@ +import AllPosts from '@/components/AllPosts'; + export default function Home() { - return
; + return ( +
+
+

Home

+ +
+
+ ); } diff --git a/src/components/AllPosts.tsx b/src/components/AllPosts.tsx new file mode 100644 index 0000000..87ef44a --- /dev/null +++ b/src/components/AllPosts.tsx @@ -0,0 +1,39 @@ +'use client'; + +import { usePaginatedQuery } from 'convex/react'; +import { api } from '../../convex/_generated/api'; +import Loader from './Loader'; +import Post from './Post'; + +export default function AllPosts() { + const PAGE_SIZE = 10; + const pager = usePaginatedQuery( + api.posts.getAllPostsWithUser, + {}, + { initialNumItems: PAGE_SIZE } + ); + + function handleLoadMoreClick() { + pager.loadMore(PAGE_SIZE); + } + + return ( +
+
+ {pager.results.map(post => ( +
+ +
+ ))} +
+ +
+ ); +} diff --git a/src/components/Navbar.tsx b/src/components/Navbar.tsx index 5d15516..8c49641 100644 --- a/src/components/Navbar.tsx +++ b/src/components/Navbar.tsx @@ -3,6 +3,7 @@ import { SignedIn, SignedOut, useUser } from '@clerk/nextjs'; import Link from 'next/link'; import { useEffect, useState } from 'react'; +import { AiFillHome } from 'react-icons/ai'; import { BiLogIn, BiPlus } from 'react-icons/bi'; import { BsPersonPlusFill } from 'react-icons/bs'; import { ImProfile } from 'react-icons/im'; @@ -64,6 +65,19 @@ export default function Navbar() { />
+
  • +
    +
    + +
    + setNavOpen(false)} + href='/' + > + Home + +
    +
  • diff --git a/src/components/UserButton.tsx b/src/components/UserButton.tsx index 9323c32..b6dd0e7 100644 --- a/src/components/UserButton.tsx +++ b/src/components/UserButton.tsx @@ -1,8 +1,9 @@ 'use client'; import { useMountedEffect } from '@/hooks'; import { UserButton as ClerkUserButton } from '@clerk/nextjs'; +import { ReactNode } from 'react'; -export default function UserButton() { +export default function UserButton({ children }: { children?: ReactNode }) { const { mounted } = useMountedEffect(); if (mounted === false) { diff --git a/src/components/UserPosts.tsx b/src/components/UserPosts.tsx index 2f6c32f..ef35256 100644 --- a/src/components/UserPosts.tsx +++ b/src/components/UserPosts.tsx @@ -8,7 +8,7 @@ import Loader from './Loader'; import Post from './Post'; export default function UserPosts({ user }: { user: UserDto }) { - const PAGE_SIZE = 1; + const PAGE_SIZE = 10; const pager = usePaginatedQuery( api.posts.getUsersPostById, { From fa43c4943f504a8f7324d7cc45b5b979f25ec869 Mon Sep 17 00:00:00 2001 From: Stevan Freeborn <65925598+StevanFreeborn@users.noreply.github.com> Date: Fri, 15 Sep 2023 17:58:22 -0500 Subject: [PATCH 7/7] feat: delete user followers and followings when user deleted --- convex/users.ts | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/convex/users.ts b/convex/users.ts index 0e11f13..b90f9c1 100644 --- a/convex/users.ts +++ b/convex/users.ts @@ -93,6 +93,15 @@ export const deleteUser = internalMutation({ .withIndex('by_user_post_id', q => q.eq('userId', userRecord._id)) .collect(); + const userFollowers = await ctx.db + .query('follows') + .withIndex('by_following', q => q.eq('following', userRecord._id)) + .collect(); + const userFollowings = await ctx.db + .query('follows') + .withIndex('by_follower', q => q.eq('follower', userRecord._id)) + .collect(); + await Promise.all( userPosts.map(async post => await ctx.db.delete(post._id)) ); @@ -101,6 +110,14 @@ export const deleteUser = internalMutation({ userLikes.map(async like => await ctx.db.delete(like._id)) ); + await Promise.all( + userFollowers.map(async follower => await ctx.db.delete(follower._id)) + ); + + await Promise.all( + userFollowings.map(async following => await ctx.db.delete(following._id)) + ); + await ctx.db.delete(userRecord._id); }, });