Task 3: UI Components for Comments In this task, we will create UI components for managing comments in the Posts app. We will create components for adding, editing, and deleting comments. We will also organize the project structure to separate components related to posts and comments.
Step 1: Organize the project structure
Currently, the web/src/components
directory contains all the components used in the app:
.
├── add-post.tsx
├── delete-post-dialog.tsx
├── edit-post.tsx
├── feed.tsx
├── header.tsx
├── post-actions.tsx
├── post.tsx
├── posts.tsx
├── sidebar.tsx
└── ui
├── alert-dialog.tsx
├── button.tsx
├── buttonVariants.tsx
├── label.tsx
├── textarea.tsx
├── toast.tsx
├── toaster.tsx
└── use-toast.ts
Let’s organize the components into separate directories based on their functionality.
.
├── layout
│ ├── feed.tsx
│ ├── header.tsx
│ └── sidebar.tsx
├── post
│ ├── add-post.tsx
│ ├── delete-post-dialog.tsx
│ ├── edit-post.tsx
│ ├── post-actions.tsx
│ ├── post.tsx
│ └── posts.tsx
└── ui
├── alert-dialog.tsx
├── button.tsx
├── buttonVariants.tsx
├── label.tsx
├── textarea.tsx
├── toast.tsx
├── toaster.tsx
└── use-toast.ts
Notice that we’ve created two new directories: layout
and post
. The layout
directory contains components related to the layout of the app, such as the header and sidebar. The post
directory contains components related to posts, such as adding, editing, and deleting posts.
Make sure to update the imports in the files that reference these components. In VSCode, if you move a file to a new directory, it will automatically update the imports for you.
Run the application to ensure that the changes did not break anything.
Create a new directory comments
inside the components
directory. This directory will contain all the components related to comments.
.
├── comment
│ ├── add-comment.tsx
│ ├── comment-actions.tsx
│ ├── comment.tsx
│ ├── comments.tsx
│ ├── delete-comment-dialog.tsx
│ └── edit-comment.tsx
Create the files for the components. For simplicity, we are reusing the same components we had for posts. We will customize them later, as needed.
Add the following code to the web/src/components/comments/delete-comment-dialog.tsx
file:
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
AlertDialogTrigger,
} from "@/components/ui/alert-dialog" ;
import { Button } from "@/components/ui/button" ;
import { TrashIcon } from "@radix-ui/react-icons" ;
import useMutationComments from "@/hooks/use-mutation-comments" ;
const DeleteCommentDialog = ({
postId,
commentId,
} : {
postId : string ;
commentId : string ;
}) => {
const { deleteCommentById } = useMutationComments (postId);
return (
< AlertDialog >
< AlertDialogTrigger asChild >
< Button variant ={ "ghost" } size ={ "icon" } >
< TrashIcon className = "w-4 h-4" />
</ Button >
</ AlertDialogTrigger >
< AlertDialogContent >
< AlertDialogHeader >
< AlertDialogTitle >Are you absolutely sure?</ AlertDialogTitle >
< AlertDialogDescription >
This action cannot be undone. This will permanently delete your
comment and remove it from our servers.
</ AlertDialogDescription >
</ AlertDialogHeader >
< AlertDialogFooter >
< AlertDialogCancel >Cancel</ AlertDialogCancel >
< AlertDialogAction onClick ={ () => deleteCommentById (commentId) } >
Continue
</ AlertDialogAction >
</ AlertDialogFooter >
</ AlertDialogContent >
</ AlertDialog >
);
};
export default DeleteCommentDialog;
Add the following code to the web/src/components/comments/edit-comment.tsx
file:
import { useEffect, useState } from "react" ;
import { Label } from "@/components/ui/label" ;
import { Textarea } from "@/components/ui/textarea" ;
import { Button } from "@/components/ui/button" ;
import { CommentType } from "@/data/types" ;
import { useToast } from "@/components/ui/use-toast" ;
import useMutationComment from "@/hooks/use-mutation-comments" ;
const EditComment = ({
comment,
setIsEditing,
} : {
comment : CommentType ;
setIsEditing : ( flag : boolean ) => void ;
}) => {
const [ id , setId ] = useState ( "" );
const [ postId , setPostId ] = useState ( "" );
const [ content , setContent ] = useState ( "" );
const { updateComment } = useMutationComment (postId);
const { toast } = useToast ();
useEffect (() => {
if (comment) {
comment.id !== id && setId (comment.id);
comment.content !== content && setContent (comment.content);
comment.postId !== postId && setPostId (comment.postId);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [comment]);
const cleanUp = () => {
setIsEditing ( false );
};
const saveComment = async () => {
if ( ! content) {
toast ({
variant: "destructive" ,
title: "Sorry! Comment cannot be empty! 🙁" ,
description: `Please enter the content of your comment.` ,
});
} else {
await updateComment (id, content);
cleanUp ();
}
};
const handleSave = async ( e : React . MouseEvent < HTMLButtonElement >) => {
e. preventDefault ();
saveComment ();
};
const handleSaveOnEnter = ( e : React . KeyboardEvent < HTMLTextAreaElement >) => {
if (e.key === "Enter" ) {
e. preventDefault ();
saveComment ();
}
};
const handleCancel = () => {
cleanUp ();
};
return (
< form className = "grid w-full gap-1.5 p-4 border-b" >
< Label htmlFor = "content" >Edit your comment</ Label >
< Textarea
id = "content"
placeholder = "Type your comment here."
value ={ content }
onKeyDown ={ handleSaveOnEnter }
onChange ={ ( e ) => setContent (e.target.value) }
/>
< div className = "flex justify-end gap-3" >
< Button type = "reset" variant ={ "secondary" } onClick ={ handleCancel } >
Cancel
</ Button >
< Button type = "submit" onClick ={ handleSave } >
Post
</ Button >
</ div >
</ form >
);
};
export default EditComment;
Add the following code to the web/src/lib/store.ts
file:
export const $showAddComment = atom ( false );
export function toggleAddComment () {
$showAddComment. set ( ! $showAddComment. get ());
}
This state will be used to toggle the visibility of the AddComment
component.
Add the following code to the web/src/components/comments/add-comment.tsx
file:
import { useState } from "react" ;
import { Textarea } from "@/components/ui/textarea" ;
import { Button } from "@/components/ui/button" ;
import { Label } from "@/components/ui/label" ;
import { toggleAddComment } from "@/lib/store" ;
import useMutationComments from "@/hooks/use-mutation-comments" ;
import { useToast } from "@/components/ui/use-toast" ;
const AddComment = ({ postId } : { postId : string }) => {
const [ content , setContent ] = useState ( "" );
const { addNewComment } = useMutationComments (postId);
const { toast } = useToast ();
const cleanUp = () => {
setContent ( "" );
toggleAddComment ();
};
const saveComment = async () => {
if ( ! content) {
toast ({
variant: "destructive" ,
title: "Sorry! Content cannot be empty! 🙁" ,
description: `Please enter the content of your comment.` ,
});
} else {
await addNewComment (content);
cleanUp ();
}
};
const handleSave = async ( e : React . MouseEvent < HTMLButtonElement >) => {
e. preventDefault ();
saveComment ();
};
const handleSaveOnEnter = ( e : React . KeyboardEvent < HTMLTextAreaElement >) => {
if (e.key === "Enter" ) {
e. preventDefault ();
saveComment ();
}
};
const handleCancel = () => {
cleanUp ();
};
return (
< form className = "grid w-full gap-1.5 p-4 border-b" >
< Label htmlFor = "content" className = "text-sm" >
Your comment
</ Label >
< Textarea
id = "content"
placeholder = "Type your message here."
value ={ content }
onKeyDown ={ handleSaveOnEnter }
onChange ={ ( e ) => setContent (e.target.value) }
/>
< div className = "flex justify-end gap-3" >
< Button type = "reset" variant ={ "secondary" } onClick ={ handleCancel } >
Cancel
</ Button >
< Button type = "submit" onClick ={ handleSave } >
Post
</ Button >
</ div >
</ form >
);
};
export default AddComment;
Add the following code to the web/src/components/comments/comment-actions.tsx
file:
import { Button } from "@/components/ui/button" ;
import { CommentType } from "@/data/types" ;
import { Pencil2Icon } from "@radix-ui/react-icons" ;
import DeleteCommentDialog from "./delete-comment-dialog" ;
const CommentActions = ({
comment,
setIsEditing,
} : {
comment : CommentType ;
setIsEditing : ( flag : boolean ) => void ;
}) => {
return (
< div className = "flex justify-end" >
< Button
variant ={ "ghost" }
size ={ "icon" }
onClick ={ () => setIsEditing ( true ) }
>
< Pencil2Icon className = "w-4 h-4" />
</ Button >
< DeleteCommentDialog commentId ={ comment.id } postId ={ comment.postId } />
</ div >
);
};
export default CommentActions;
Add the following code to the web/src/components/comments/comment.tsx
file:
import type { CommentType } from "@/data/types" ;
import CommentActions from "./comment-actions" ;
import { useState } from "react" ;
import EditComment from "./edit-comment" ;
const Comment = ({ comment } : { comment : CommentType }) => {
const [ isEditing , setIsEditing ] = useState ( false );
if (isEditing) {
return < EditComment comment ={ comment } setIsEditing ={ setIsEditing } />;
}
return (
< div className = "p-1 border-b" >
< div className = "flex items-center justify-between pl-4" >
< h4 className = "text-xs text-muted-foreground" >
{new Date (comment.date). toLocaleString ( "en-US" , {
month: "short" ,
day: "numeric" ,
year: "numeric" ,
hour: "numeric" ,
minute: "numeric" ,
}) }
</ h4 >
< CommentActions comment ={ comment } setIsEditing ={ setIsEditing } />
</ div >
< p className = "p-4" > { comment.content } </ p >
</ div >
);
};
export default Comment;
Add the following code to the web/src/components/comments/comments.tsx
file:
import Comment from "./comment" ;
import useQueryComments from "@/hooks/use-query-comments" ;
const Comments = ({ postId } : { postId : string }) => {
const { comments } = useQueryComments (postId);
return (
< div className = "" >
{ comments. map (( comment ) => (
< Comment comment ={ comment } key ={ comment.id } />
)) }
</ div >
);
};
export default Comments;
We are going to update the web app to show comments instead of posts for now. We will add the ability to switch between posts and comments later.
Update the Sidebar
component, in web/src/components/layout/sidebar.tsx
, to show the “Make a Comment” button instead of the “Make a Post” button.
import {
HomeIcon,
MagnifyingGlassIcon,
PlusCircledIcon,
} from "@radix-ui/react-icons" ;
import { Button } from "@/components/ui/button" ;
import { useStore } from "@nanostores/react" ;
import {
$showAddPost,
$showAddComment, // 👀 Look here
toggleAddPost,
toggleAddComment, // 👀 Look here
} from "@/lib/store" ;
const Sidebar = () => {
const showAddPost = useStore ($showAddPost);
const showAddComment = useStore ($showAddComment); // 👀 Look here
return (
< div className = "flex flex-col items-end p-2 space-y-2" >
< Button aria-label ={ "Home" } variant = "ghost" size = "icon" >
< HomeIcon className = "w-5 h-5" />
</ Button >
< Button aria-label ={ "Search" } variant = "ghost" size = "icon" >
< MagnifyingGlassIcon className = "w-5 h-5" />
</ Button >
{ /* !showAddPost && (
<Button
aria-label={"Make a Post"}
variant="default"
size="icon"
onClick={() => {
toggleAddPost();
}}
>
<PlusCircledIcon className="w-5 h-5" />
</Button>
) */ }
{ /* 👆 Look here 👇*/ }
{! showAddComment && (
< Button
aria-label ={ "Make a Comment" }
variant = "destructive"
size = "icon"
onClick ={ () => {
toggleAddComment ();
} }
>
< PlusCircledIcon className = "w-5 h-5" />
</ Button >
) }
</ div >
);
};
export default Sidebar;
Update the Feed
component
Update the Feed
component, in web/src/components/layout/feed.tsx
, to show the AddComment
and Comments
components instead of the AddPost
and Posts
components.
import Header from "./header" ;
import { useStore } from "@nanostores/react" ;
import { $showAddPost, $showAddComment } from "@/lib/store" ; // 👀 Look here
import AddPost from "../post/add-post" ;
import Posts from "../post/posts" ;
import AddComment from "../comment/add-comment" ; // 👀 Look here
import Comments from "../comment/comments" ; // 👀 Look here
const Feed = () => {
const showNewPostEditor = useStore ($showAddPost);
const showNewCommentEditor = useStore ($showAddComment); // 👀 Look here
return (
< div className = "flex flex-col w-full min-h-screen border-x" >
< Header />
{ /* showNewPostEditor && <AddPost /> */ }
{ /* <Posts /> */ }
{ /* 👆 Look here 👇 */ }
{ showNewCommentEditor && < AddComment postId = "1" /> }
< Comments postId = "1" />
</ div >
);
};
export default Feed;
Step 4: Test the application
Run the application and visit the home page. You should see the comments for the post with ID 1
. You can add, edit, and delete comments.