From 0dc2934e056b91d526e6f2a292c5bd78746c9a66 Mon Sep 17 00:00:00 2001
From: Stevan Freeborn <65925598+StevanFreeborn@users.noreply.github.com>
Date: Wed, 13 Sep 2023 01:20:58 -0500
Subject: [PATCH 4/5] chore: add todo
---
src/app/posts/[id]/page.tsx | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/app/posts/[id]/page.tsx b/src/app/posts/[id]/page.tsx
index 8a56fca..4e618e4 100644
--- a/src/app/posts/[id]/page.tsx
+++ b/src/app/posts/[id]/page.tsx
@@ -79,7 +79,7 @@ export default function PostPage({
className='rounded-full object-cover border-4 border-primary-accent'
/>
-
+ {/* TODO: Display replies as paged query. Should show latest replies with option to load more */}
Date: Thu, 14 Sep 2023 21:47:40 -0500
Subject: [PATCH 5/5] feat: support replies
---
convex/posts.ts | 45 +++++++++++++++++
convex/schema.ts | 4 +-
convex/users.ts | 2 +
src/app/posts/[id]/edit/page.tsx | 3 +-
src/app/posts/[id]/page.tsx | 22 +++++++--
src/app/types/index.ts | 1 +
src/components/Editor.tsx | 10 +++-
src/components/Loader.tsx | 43 ++++++++++++++++
src/components/Post.tsx | 74 +++++-----------------------
src/components/PostActionButtons.tsx | 31 ++++++++++++
src/components/PostActionModal.tsx | 70 ++++++++++++++++++++++++++
src/components/PostReplies.tsx | 42 ++++++++++++++++
src/components/Reply.tsx | 54 ++++++++++++++++++++
src/components/UserPosts.tsx | 40 +++++----------
14 files changed, 344 insertions(+), 97 deletions(-)
create mode 100644 src/components/Loader.tsx
create mode 100644 src/components/PostActionButtons.tsx
create mode 100644 src/components/PostActionModal.tsx
create mode 100644 src/components/PostReplies.tsx
create mode 100644 src/components/Reply.tsx
diff --git a/convex/posts.ts b/convex/posts.ts
index 174ac24..fb87802 100644
--- a/convex/posts.ts
+++ b/convex/posts.ts
@@ -1,6 +1,7 @@
import { paginationOptsValidator } from 'convex/server';
import { v } from 'convex/values';
import { PostWithUserDto } from '../src/app/types';
+import { Id } from './_generated/dataModel';
import { mutation, query } from './_generated/server';
import { userQuery } from './users';
@@ -86,7 +87,51 @@ export const getPostById = query({
_creationTime: user._creationTime,
clerkUsername: user.clerkUser.username,
clerkImageUrl: user.clerkUser.image_url,
+ clerkUserId: user.clerkUser.id,
},
};
},
});
+
+export const getRepliesByParentId = query({
+ args: { id: v.id('posts'), paginationOpts: paginationOptsValidator },
+ handler: async (ctx, args) => {
+ const replies = await ctx.db
+ .query('posts')
+ .withIndex('by_parent_id', q => q.eq('parentPostId', args.id))
+ .order('desc')
+ .paginate(args.paginationOpts);
+
+ const repliesWithUserData = await Promise.all(
+ replies.page.map(async reply => {
+ const user = await ctx.db.get(reply.userId);
+
+ if (user === null) {
+ return {
+ ...reply,
+ user: {
+ _id: '' as Id<'users'>,
+ _creationTime: 0,
+ clerkUsername: null,
+ clerkImageUrl: '',
+ clerkUserId: '',
+ },
+ };
+ }
+
+ return {
+ ...reply,
+ user: {
+ _id: user._id,
+ _creationTime: user._creationTime,
+ clerkUsername: user.clerkUser.username,
+ clerkImageUrl: user.clerkUser.image_url,
+ clerkUserId: user.clerkUser.id,
+ },
+ };
+ })
+ );
+
+ return { ...replies, page: repliesWithUserData };
+ },
+});
diff --git a/convex/schema.ts b/convex/schema.ts
index 7fa6d9c..4431f79 100644
--- a/convex/schema.ts
+++ b/convex/schema.ts
@@ -39,7 +39,9 @@ export default defineSchema(
parentPostId: v.optional(v.id('posts')),
userId: v.id('users'),
content: v.array(v.string()),
- }).index('by_user_id', ['userId']),
+ })
+ .index('by_user_id', ['userId'])
+ .index('by_parent_id', ['parentPostId']),
},
{ schemaValidation: false }
);
diff --git a/convex/users.ts b/convex/users.ts
index 137fd71..bccc86d 100644
--- a/convex/users.ts
+++ b/convex/users.ts
@@ -28,6 +28,7 @@ export const getUserByClerkId = query({
_creationTime: user._creationTime,
clerkUsername: user.clerkUser.username,
clerkImageUrl: user.clerkUser.image_url,
+ clerkUserId: user.clerkUser.id,
};
},
});
@@ -46,6 +47,7 @@ export const getUserById = query({
_creationTime: user._creationTime,
clerkUsername: user.clerkUser.username,
clerkImageUrl: user.clerkUser.image_url,
+ clerkUserId: user.clerkUser.id,
};
},
});
diff --git a/src/app/posts/[id]/edit/page.tsx b/src/app/posts/[id]/edit/page.tsx
index 8ffdbb8..214f965 100644
--- a/src/app/posts/[id]/edit/page.tsx
+++ b/src/app/posts/[id]/edit/page.tsx
@@ -45,8 +45,7 @@ export default function EditPostPage({ params }: { params: { id: string } }) {
case 'CANNOT_POST_ON_BEHALF_OF_ANOTHER_USER':
throw Error('Unable to update post');
default:
- 'use server';
- router.push('/');
+ router.push(`/posts/${post?._id}`);
return;
}
}
diff --git a/src/app/posts/[id]/page.tsx b/src/app/posts/[id]/page.tsx
index 4e618e4..b5b12c0 100644
--- a/src/app/posts/[id]/page.tsx
+++ b/src/app/posts/[id]/page.tsx
@@ -2,11 +2,13 @@
import Editor, { SubmitActionParams } from '@/components/Editor';
import Post from '@/components/Post';
+import PostReplies from '@/components/PostReplies';
import SpinningLoader from '@/components/SpinningLoader';
import { useRouter } from '@/hooks';
import { useUser } from '@clerk/nextjs';
import { useMutation, useQuery } from 'convex/react';
import Image from 'next/image';
+import { useEffect, useRef } from 'react';
import { api } from '../../../../convex/_generated/api';
import { Id } from '../../../../convex/_generated/dataModel';
@@ -15,14 +17,22 @@ export default function PostPage({
searchParams,
}: {
params: { id: string };
- searchParams: { reply?: boolean };
+ searchParams: { [key: string]: string | string[] | undefined };
}) {
+ const replyRef = useRef(null);
const user = useUser();
const router = useRouter();
const post = useQuery(api.posts.getPostById, {
id: params.id as Id<'posts'>,
});
const createOrUpdatePost = useMutation(api.posts.createOrUpdatePost);
+ const isReply = searchParams.reply === 'true';
+
+ useEffect(() => {
+ if (replyRef.current !== null && isReply) {
+ replyRef.current.scrollIntoView();
+ }
+ }, [isReply, post]);
async function submitAction({
clerkUserId,
@@ -69,6 +79,9 @@ export default function PostPage({
showReply={false}
/>
+
- {/* TODO: Display replies as paged query. Should show latest replies with option to load more */}
-
diff --git a/src/app/types/index.ts b/src/app/types/index.ts
index 2ddc91d..7fc5b34 100644
--- a/src/app/types/index.ts
+++ b/src/app/types/index.ts
@@ -5,6 +5,7 @@ export type UserDto = {
_creationTime: number;
clerkUsername: string | null;
clerkImageUrl: string;
+ clerkUserId: string;
};
export type PostWithUserDto = Doc<'posts'> & { user: UserDto };
diff --git a/src/components/Editor.tsx b/src/components/Editor.tsx
index 9a3878f..47cdb8a 100644
--- a/src/components/Editor.tsx
+++ b/src/components/Editor.tsx
@@ -8,7 +8,7 @@ import { languages } from '@codemirror/language-data';
import { Compartment, EditorState, Text } from '@codemirror/state';
import { EditorView, keymap } from '@codemirror/view';
import { basicSetup } from 'codemirror';
-import { FormEvent, useState } from 'react';
+import { FormEvent, useEffect, useState } from 'react';
import { Doc, Id } from '../../convex/_generated/dataModel';
import PostContent from './PostContent';
@@ -24,6 +24,7 @@ export default function Editor({
parentPostId,
post,
submitAction,
+ autofocus = true,
}: {
clerkUserId: string;
parentPostId?: Id<'posts'>;
@@ -34,6 +35,7 @@ export default function Editor({
post,
currentDoc,
}: SubmitActionParams) => Promise
;
+ autofocus?: boolean;
}) {
const editorTheme = new Compartment();
const [currentDoc, setCurrentDoc] = useState(post?.content ?? ['']);
@@ -58,6 +60,12 @@ export default function Editor({
extensions,
});
+ useEffect(() => {
+ if (editorView !== null && autofocus) {
+ editorView.focus();
+ }
+ }, [autofocus, editorView]);
+
async function handleSubmit(e: FormEvent) {
e.preventDefault();
diff --git a/src/components/Loader.tsx b/src/components/Loader.tsx
new file mode 100644
index 0000000..7293e4f
--- /dev/null
+++ b/src/components/Loader.tsx
@@ -0,0 +1,43 @@
+import SpinningLoader from './SpinningLoader';
+
+export default function Loader({
+ isLoading,
+ status,
+ loadButtonClickHandler,
+}: {
+ isLoading: boolean;
+ status: string;
+ loadButtonClickHandler: () => void;
+}) {
+ if (isLoading) {
+ return (
+
+
+ Loading...
+
+ );
+ }
+
+ if (status !== 'CanLoadMore') {
+ return;
+ }
+
+ return (
+
+
+
+ );
+}
diff --git a/src/components/Post.tsx b/src/components/Post.tsx
index d4bbad4..fda7603 100644
--- a/src/components/Post.tsx
+++ b/src/components/Post.tsx
@@ -1,15 +1,15 @@
'use client';
import { PostWithUserDto } from '@/app/types';
+import { useUser } from '@clerk/nextjs';
import { Text } from '@codemirror/state';
import { useMutation } from 'convex/react';
import Image from 'next/image';
import Link from 'next/link';
-import { useEffect, useRef, useState } from 'react';
-import { BiDotsHorizontalRounded, BiSolidLike } from 'react-icons/bi';
-import { BsFillReplyFill } from 'react-icons/bs';
import { GoDotFill } from 'react-icons/go';
import { api } from '../../convex/_generated/api';
+import PostActionButton from './PostActionButtons';
+import PostActionModal from './PostActionModal';
import PostContent from './PostContent';
export default function Post({
@@ -21,9 +21,8 @@ export default function Post({
limit?: boolean;
showReply?: boolean;
}) {
+ const { isLoaded, user, isSignedIn } = useUser();
const deletePostById = useMutation(api.posts.deletePostById);
- const modalRef = useRef(null);
- const [modalOpen, setModalOpen] = useState(false);
const username = post.user.clerkUsername ?? post.user._id;
const createdPostDate = new Date(post._creationTime);
const postYear = createdPostDate.getFullYear();
@@ -34,25 +33,7 @@ export default function Post({
const content = limit ? post.content.slice(0, 10) : post.content;
const postLink = `/posts/${post._id}`;
- // TODO: Add reply and like buttons
-
- useEffect(() => {
- function handleClickOutside(e: MouseEvent) {
- if (
- modalOpen &&
- modalRef.current &&
- modalRef.current.contains(e.target as Node) === false
- ) {
- setModalOpen(false);
- }
- }
-
- document.addEventListener('click', handleClickOutside);
- return () => document.removeEventListener('click', handleClickOutside);
- }, [modalOpen]);
-
async function handleDeleteClick() {
- setModalOpen(false);
const result = confirm('Are you sure you want to delete this post?');
if (result === false) {
@@ -89,36 +70,9 @@ export default function Post({