My personal site. theclashfruit.me
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat: posting comment

I need to add serverside validation plus perhaps a captcha..

+169 -5
+14 -4
app/(main)/post/[slug]/page.tsx
··· 1 1 import { db } from '@/lib/db/drizzle'; 2 2 import { commentsTable, postsTable, usersTable } from '@/lib/db/schema'; 3 3 4 - import rehypeStarryNight from 'rehype-starry-night'; 5 - import remarkGfm from 'remark-gfm'; 6 - 7 4 import { eq } from 'drizzle-orm'; 8 5 9 6 import { MDXRemote } from 'next-mdx-remote-client/rsc'; ··· 13 10 14 11 import Comment, { CommentWithReplies } from '@/components/Comment'; 15 12 13 + import Form from 'next/form'; 16 14 import type { Metadata } from 'next'; 15 + import CommentForm from '@/components/CommentForm'; 17 16 18 17 export async function generateMetadata({ 19 18 params ··· 71 70 <div> 72 71 <h2>Comments</h2> 73 72 73 + <CommentForm post={post.id} /> 74 + 75 + <hr 76 + style={{ 77 + border: 'none', 78 + borderBottom: '1px solid var(--outlineVariant)', 79 + margin: '16px 8px' 80 + }} 81 + /> 82 + 74 83 {comments && 75 84 comments.map((c) => ( 76 85 <Comment ··· 101 110 .select() 102 111 .from(commentsTable) 103 112 .leftJoin(usersTable, eq(commentsTable.author, usersTable.id)) 104 - .where(eq(commentsTable.post, id)); 113 + .where(eq(commentsTable.post, id)) 114 + .orderBy(usersTable.id); 105 115 106 116 const map = new Map<string, CommentWithReplies>(); 107 117 const roots: CommentWithReplies[] = [];
+9 -1
components/Comment.tsx
··· 1 + 'use client'; 2 + 1 3 import { commentsTable, usersTable } from '@/lib/db/schema'; 2 4 3 5 import { Reply } from 'lucide-react'; 4 6 5 7 import styles from '@/styles/components/Comment.module.scss'; 8 + import CommentForm from './CommentForm'; 9 + import { useState } from 'react'; 6 10 7 11 export type CommentWithReplies = typeof commentsTable.$inferSelect & { 8 12 user: typeof usersTable.$inferSelect | null; ··· 24 28 comment.user?.username ?? 25 29 'Unknown User'; 26 30 31 + const [open, setOpen] = useState(false); 32 + 27 33 return ( 28 34 <div className={styles.comment} data-root={isRoot}> 29 35 <div className={styles.content}> ··· 38 44 <p>{comment.content}</p> 39 45 40 46 <div className={styles.actions}> 41 - <button> 47 + <button onClick={() => setOpen(!open)}> 42 48 <Reply /> Reply 43 49 </button> 44 50 </div> 51 + 52 + {open && <CommentForm reply={comment.id} post={comment.post!} />} 45 53 </div> 46 54 47 55 {comment.replies.length > 0 && (
+46
components/CommentForm.tsx
··· 1 + 'use client'; 2 + 3 + import Form from 'next/form'; 4 + 5 + import { createCommentAction } from '@/lib/actions'; 6 + 7 + import styles from '@/styles/components/CommentForm.module.scss'; 8 + 9 + export default function CommentForm({ 10 + reply, 11 + post 12 + }: { 13 + reply?: bigint; 14 + post: bigint; 15 + }) { 16 + return ( 17 + <div className={styles.commentForm}> 18 + <Form action={createCommentAction}> 19 + <input type="hidden" name="post" value={post.toString()} /> 20 + {reply && <input type="hidden" name="reply" value={reply.toString()} />} 21 + 22 + <div> 23 + <input 24 + type="text" 25 + id="name" 26 + name="name" 27 + placeholder="Display Name" 28 + pattern=".{3,64}" 29 + required 30 + /> 31 + </div> 32 + <div> 33 + <textarea 34 + id="content" 35 + name="content" 36 + placeholder="Content" 37 + rows={4} 38 + required 39 + /> 40 + </div> 41 + 42 + <button type="submit">{reply ? 'Reply' : 'Comment'}</button> 43 + </Form> 44 + </div> 45 + ); 46 + }
+29
lib/actions.ts
··· 1 + 'use server'; 2 + 3 + import { db } from '@/lib/db/drizzle'; 4 + import { commentsTable } from './db/schema'; 5 + 6 + export const createCommentAction = async (formData: FormData) => { 7 + const reply = formData.get('reply') ?? false; 8 + 9 + const data = { 10 + post: BigInt(formData.get('post')!.toString()), 11 + name: formData.get('name')!.toString(), 12 + content: formData.get('content')!.toString() 13 + }; 14 + 15 + if (reply) { 16 + await db.insert(commentsTable).values({ 17 + displayName: data.name, 18 + content: data.content, 19 + post: data.post, 20 + parent: BigInt(reply.toString()) 21 + }); 22 + } else { 23 + await db.insert(commentsTable).values({ 24 + displayName: data.name, 25 + content: data.content, 26 + post: data.post 27 + }); 28 + } 29 + };
+71
styles/components/CommentForm.module.scss
··· 1 + .commentForm { 2 + padding: 16px; 3 + 4 + background: var(--surfaceContainerLow); 5 + 6 + border: 1px solid var(--outlineVariant); 7 + border-radius: 16px; 8 + 9 + > form { 10 + display: flex; 11 + 12 + flex-direction: column; 13 + 14 + gap: 8px; 15 + } 16 + 17 + h6 { 18 + margin: 0; 19 + } 20 + 21 + textarea, 22 + input { 23 + padding: 8px; 24 + 25 + background: none; 26 + 27 + outline: 1px solid transparent; 28 + outline-offset: -1px; 29 + 30 + border: 1px solid var(--outlineVariant); 31 + border-radius: 8px; 32 + 33 + width: 100%; 34 + max-width: 100%; 35 + min-width: 100%; 36 + 37 + transition: 150ms outline; 38 + 39 + &:hover, 40 + &:active, 41 + &:focus { 42 + outline-width: 2px; 43 + outline-color: var(--primary); 44 + } 45 + } 46 + 47 + button { 48 + cursor: pointer; 49 + 50 + padding: 8px; 51 + 52 + background: var(--primary); 53 + color: var(--onPrimary); 54 + 55 + border: 1px solid var(--primary); 56 + border-radius: 8px; 57 + 58 + transition: 150ms; 59 + 60 + &:hover { 61 + filter: brightness(1.2); 62 + } 63 + } 64 + } 65 + 66 + .line { 67 + border: none; 68 + border-bottom: 1px solid var(--outlineVariant); 69 + 70 + margin: 16px 8px; 71 + }