In this task, we will update the UI components to hide the βEditβ and βDeleteβ buttons for posts that are not created by the logged-in user. We will also update the API calls to include the session cookie in the request. Finally, we will add toast messages to prompt the user to sign in or create an account when they try to create a post or comment without being logged in.
Add a new UserType
type to the web/src/data/types.ts
file:
export type PostType = {
id: string;
content: string;
date: string;
author: UserType; // π Look here
};
export type CommentType = {
id: string;
content: string;
date: string;
postId: string;
author: UserType; // π Look here
};
export type UserType = {
id: string;
name: string;
username: string;
};
Notice that the PostType
and CommentType
types now include an author
field of type UserType
.
Create a new Author
component in the web/src/components/shared/author.tsx
file:
import { UserType } from "@/data/types";
import { cn } from "@/lib/utils";
type AuthorProps = {
author: UserType;
className?: string;
};
const Author = ({ author, className }: AuthorProps) => {
return (
<div className={cn("flex items-center gap-1", className)}>
<p className="text-sm font-medium leading-none">{author.name}</p>
<p className="text-sm text-muted-foreground">@{author.username}</p>
</div>
);
};
export default Author;
This component displays the authorβs name and username. You can later customize it to include the authorβs profile picture.
Letβs update the Post
component to use the Author
component. Open the web/src/components/post/post.tsx
file and update it as follows:
import type { PostType as PostType } from "@/data/types";
import PostActions from "./post-actions";
import { useState } from "react";
import EditPost from "./edit-post";
import Author from "../shared/author"; // π Look here
const Post = ({ post }: { post: PostType }) => {
const [isEditing, setIsEditing] = useState(false);
if (isEditing) {
return <EditPost post={post} setIsEditing={setIsEditing} />;
}
return (
<div className="p-1 border-b">
<div className="flex items-center justify-between pl-4 mt-4">
<div>
<h4 className="text-xs text-muted-foreground">
{new Date(post.date).toLocaleString("en-US", {
month: "short",
day: "numeric",
year: "numeric",
hour: "numeric",
minute: "numeric",
})}
</h4>
<Author author={post.author} className="" /> // π Look here
</div>
<PostActions post={post} setIsEditing={setIsEditing} />
</div>
<p className="p-4">{post.content}</p>
</div>
);
};
export default Post;
Run the app and check if the authorβs name and username are displayed correctly.
Currently, the PostActions
component displays the βEditβ and βDeleteβ buttons on all posts. We want to show these buttons only if the logged-in user is the author of the post. Open the web/src/components/post/post-actions.tsx
file and update it as follows:
import { Button } from "@/components/ui/button";
import { PostType } from "@/data/types";
import { Pencil2Icon, ChatBubbleIcon } from "@radix-ui/react-icons";
import DeletePostDialog from "./delete-post-dialog";
import { openPage } from "@nanostores/router";
import { $router } from "@/lib/router";
import useAuth from "@/hooks/use-auth"; // π Look here
const PostActions = ({
post,
setIsEditing,
}: {
post: PostType;
setIsEditing: (flag: boolean) => void;
}) => {
const { user } = useAuth(); // π Look here
const showAction = user && user.username === post.author.username; // π Look here
const navigateToCommentsView = (e: React.MouseEvent<HTMLButtonElement>) => {
e.preventDefault();
openPage($router, "post", { postId: post.id });
};
return (
<div className="flex justify-end">
<Button variant={"ghost"} size={"icon"} onClick={navigateToCommentsView}>
<ChatBubbleIcon className="w-4 h-4" />
</Button>
{showAction && (
<Button
variant={"ghost"}
size={"icon"}
onClick={() => setIsEditing(true)}
>
<Pencil2Icon className="w-4 h-4" />
</Button>
)}
{/* π Look here π */}
{showAction && <DeletePostDialog postId={post.id} />}
</div>
);
};
export default PostActions;
Notice that we use the useAuth
hook to get the logged-in user. We then check if the logged-in user is the author of the post to show the βEditβ and βDeleteβ buttons.
Run the app and check if the βEditβ and βDeleteβ buttons are displayed only for the author of the post.
We have two filters in the header: βMy Postsβ and βAll Posts.β We want to show the βMy Postsβ filter only if the user is logged in. Open the web/src/components/layout/header.tsx
file and update it as follows:
import { Button } from "@/components/ui/button";
import useAuth from "@/hooks/use-auth";
import { $router } from "@/lib/router";
import { cn } from "@/lib/utils";
import { useStore } from "@nanostores/react";
import { useEffect, useState } from "react";
const Header = () => {
const [enableFilter, setEnableFilter] = useState(false);
const page = useStore($router);
const [label, setLabel] = useState<string>("Posts");
const { user } = useAuth();
const showUserFilter = user && user.username;
useEffect(() => {
if (page?.route === "post") {
setLabel("Comments");
} else {
setLabel("Posts");
}
}, [page]);
if (!page) return null;
return (
<div className="flex justify-center gap-3 p-1 border-b">
{showUserFilter && (
<Button
variant={"link"}
className={cn({
underline: showUserFilter && enableFilter,
"text-primary": enableFilter,
"text-primary/60": !enableFilter,
})}
onClick={() => setEnableFilter(true)}
>{`My ${label}`}</Button>
)}
<Button
variant={"link"}
className={cn({
underline: showUserFilter && !enableFilter,
"text-primary": !enableFilter,
"text-primary/60": enableFilter,
})}
disabled={!showUserFilter}
onClick={() => setEnableFilter(false)}
>
{`All ${label}`}
</Button>
</div>
);
};
export default Header;
Try the app now. You should see the βMy Postsβ and βAll Postsβ buttons in the header. The βMy Postsβ button should be hidden if the user is not logged in.
enableFilter
State to a Global StoreLetβs move the enableFilter
state to a global store. Update the web/src/lib/store.ts
file by including the following code:
export const $enableFilter = atom(false); // Enable filter to show my posts/comments only
export function setEnableFilter(enable: boolean) {
$enableFilter.set(enable);
}
Now update the web/src/components/layout/header.tsx
file to use the global store:
+ import { $enableFilter, setEnableFilter } from "@/lib/store";
- const [enableFilter, setEnableFilter] = useState(false);
+ const enableFilter = useStore($enableFilter);
Now the enableFilter
state is managed by the global store.
We need to update the API calls to include the session cookie in the request. This is necessary for the server to authenticate the user.
Add the following settings to all the fetch
calls in web/src/data/api.ts
:
{ credentials: "include" },
For example:
// Fetch all posts
export const fetchPosts = async (
page: number = 1,
limit: number = 20,
): Promise<{ data: PostType[]; total: number }> => {
const response = await fetch(
`${API_URL}/posts?page=${page}&limit=${limit}`,
{ credentials: "include" },
);
if (!response.ok) {
throw new Error(`API request failed! with status: ${response.status}`);
}
const { data, total }: { data: PostType[]; total: number } =
await response.json();
return { data, total };
};
This setting to ensure that the browser sends the cookies with the request. This is necessary for the server to authenticate the user.
Next, we will update the fetchPosts
and fetchComments
functions to accept an optional username
parameter. This will allow us to fetch posts/comments for a specific user.
// Fetch all posts
export const fetchPosts = async (
page: number = 1,
limit: number = 20,
username?: string, // π Look here
): Promise<{ data: PostType[]; total: number }> => {
const response = await fetch(
`${API_URL}/posts?page=${page}&limit=${limit}${
username ? `&username=${username}` : "" // π Look here
}`,
{ credentials: "include" },
);
if (!response.ok) {
throw new Error(`API request failed! with status: ${response.status}`);
}
const { data, total }: { data: PostType[]; total: number } =
await response.json();
return { data, total };
};
// Fetch all comments for a post
export const fetchComments = async (
postId: string,
page: number = 1,
limit: number = 20,
username?: string, // π Look here
): Promise<CommentType[]> => {
const response = await fetch(
`${API_URL}/posts/${postId}/comments?sort=desc&page=${page}&limit=${limit}${
username ? `&username=${username}` : "" // π Look here
}`,
{ credentials: "include" },
);
if (!response.ok) {
throw new Error(`API request failed! with status: ${response.status}`);
}
const { data }: { data: CommentType[] } = await response.json();
return data;
};
With these changes, we can now fetch posts and comments for a specific user.
useQueryPosts
HookUpdate the useQueryPosts
hook in web/src/hooks/use-query-posts.tsx
to include the enableFilter
state from the global store. This will allow us to fetch posts for a specific user when the βMy Postsβ filter is enabled.
import { useEffect, useState } from "react";
import { fetchPosts } from "@/data/api";
import { useStore } from "@nanostores/react";
import {
$posts,
appendPosts,
incrementPage,
setHasMorePosts,
setPosts,
$enableFilter, // π Look here
} from "@/lib/store";
import { toast } from "@/components/ui/use-toast";
import useAuth from "@/hooks/use-auth"; // π Look here
function useQueryPosts() {
const posts = useStore($posts);
const enableFilter = useStore($enableFilter); // π Look here
const [isLoading, setIsLoading] = useState(false);
const { user } = useAuth(); // π Look here
const loadPosts = async (page: number = 1, limit: number = 20) => {
setIsLoading(true);
try {
const { data: fetchedPosts, total } = await fetchPosts(
page,
limit,
enableFilter ? user?.username : undefined, // π Look here
);
setHasMorePosts(posts.length + fetchedPosts.length < total);
if (page === 1) {
setPosts(fetchedPosts);
} else {
appendPosts(fetchedPosts);
incrementPage();
}
} catch (error) {
const errorMessage =
(error as Error).message ?? "Please try again later!";
toast({
variant: "destructive",
title: "Sorry! There was an error reading the posts π",
description: errorMessage,
});
} finally {
setIsLoading(false);
}
};
useEffect(() => {
loadPosts(1); // Reset to first page when filter changes
}, [enableFilter]); // π Look here
return { posts, loadPosts, isLoading };
}
export default useQueryPosts;
Update the Posts
component in web/src/components/post/posts.tsx
to include the enableFilter
state from the global store. This will allow us to reset the infinite scroll state when the βMy Postsβ filter is enabled.
import Post from "./post";
import useQueryPosts from "@/hooks/use-query-posts";
import InfiniteScroll from "@/components/shared/infinite-scroll";
import { useStore } from "@nanostores/react";
import {
$currentPage,
$hasMorePosts,
$enableFilter // π Look here
} from "@/lib/store";
const Posts = () => {
const currentPage = useStore($currentPage);
const hasMorePosts = useStore($hasMorePosts);
const enableFilter = useStore($enableFilter); // π Look here
const { posts, loadPosts, isLoading } = useQueryPosts();
const loadMorePosts = () => {
loadPosts(currentPage + 1);
};
return (
<div className="space-y-4">
<InfiniteScroll
loadMore={loadMorePosts}
hasMore={hasMorePosts}
isLoading={isLoading}
key={enableFilter ? "filtered" : "all"} // π Look here
>
{posts.map((post) => (
<Post key={post.id} post={post} />
))}
</InfiniteScroll>
</div>
);
};
export default Posts;
Adding a key prop to the InfiniteScroll component. This ensures that the component is re-mounted when the filter changes, which resets the infinite scroll state.
useQueryComments
HookUpdate the useQueryComments
hook in web/src/hooks/use-query-comments.tsx
to include the enableFilter
state from the global store. This will allow us to fetch comments for a specific user when the βMy Commentsβ filter is enabled.
import { useEffect } from "react";
import { fetchComments } from "@/data/api";
import { useStore } from "@nanostores/react";
import { setComments, $comments, $enableFilter } from "@/lib/store";
import { toast } from "@/components/ui/use-toast";
import useAuth from "@/hooks/use-auth"; // π Look here
function useQueryComments(postId: string) {
const comments = useStore($comments);
const { user } = useAuth(); // π Look here
const enableFilter = useStore($enableFilter); // π Look here
const loadComments = async (page: number = 1, limit: number = 20) => {
try {
const fetchedComments = await fetchComments(
postId,
page,
limit,
enableFilter ? user?.username : undefined, // π Look here
);
setComments([...fetchedComments]);
} catch (error) {
const errorMessage =
(error as Error).message ?? "Please try again later!";
toast({
variant: "destructive",
title: "Sorry! There was an error reading the comments π",
description: errorMessage,
});
}
};
useEffect(() => {
loadComments(1);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [postId, enableFilter]); // π Look here
return { comments };
}
export default useQueryComments;
We did not implement infinite scroll for comments, so we donβt need to reset the comments when the filter changes.
in the previous steps, weβve hidden the βDeleteβ and βEditβ buttons for posts that are not created by the logged-in user. Moreover, weβve removed the βMy Postsβ filter when the user is not logged in. We can perform similar checks in the Sidebar
component to hide the βMake a Postβ and βMake a Commentβ buttons when the user is not logged in.
However, we want to show a toast message to prompt the user to sign in or create an account when they try to create a post or comment without being logged in. We can use the useAuth
hook to get the logged-in user and show a toast message using the useToast
hook.
Update the web/src/components/layout/sidebar.tsx
file:
import {
ChatBubbleIcon,
HomeIcon,
MagnifyingGlassIcon,
PlusCircledIcon,
} from "@radix-ui/react-icons";
import { Button } from "@/components/ui/button";
import { useStore } from "@nanostores/react";
import {
$showAddPost,
$showAddComment,
toggleAddPost,
toggleAddComment,
} from "@/lib/store";
import { $router } from "@/lib/router";
import { openPage } from "@nanostores/router";
import useAuth from "@/hooks/use-auth"; // π Look here
import { toast } from "@/components/ui/use-toast"; // π Look here
const Sidebar = () => {
const page = useStore($router);
const showAddPost = useStore($showAddPost);
const showAddComment = useStore($showAddComment);
const { user } = useAuth(); // π Look here
// Look here π
const authGuard = () => {
if (user.username) return true;
toast({
variant: "destructive",
title: "Sorry! You need to be signed in to do that π",
description: "Please sign in or create an account to continue.",
});
return false;
};
const navigateHome = (e: React.MouseEvent<HTMLButtonElement>) => {
e.preventDefault();
openPage($router, "home");
};
if (!page) return null;
return (
<div className="flex flex-col items-end p-2 space-y-2">
<Button
aria-label={"Home"}
variant="ghost"
size="icon"
onClick={navigateHome}
>
<HomeIcon className="w-5 h-5" />
</Button>
<Button aria-label={"Search"} variant="ghost" size="icon">
<MagnifyingGlassIcon className="w-5 h-5" />
</Button>
{page.route === "home" && !showAddPost && (
<Button
aria-label={"Make a Post"}
variant="default"
size="icon"
onClick={() => {
authGuard() && toggleAddPost(); // π Look here
}}
>
<PlusCircledIcon className="w-5 h-5" />
</Button>
)}
{page.route === "post" && !showAddComment && (
<Button
aria-label={"Make a Comment"}
variant="default"
size="icon"
onClick={() => {
authGuard() && toggleAddComment(); // π Look here
}}
>
<ChatBubbleIcon className="w-5 h-5" />
</Button>
)}
</div>
);
};
export default Sidebar;
Try the app now: sign out and try to create a post. You should see a toast message prompting you to sign in or create an account.
Likewise, letβs require a user to sign in or create an account to view the comments for a post. Update the web/src/components/post/post-actions.tsx
file:
import { Button } from "@/components/ui/button";
import { PostType } from "@/data/types";
import { Pencil2Icon, ChatBubbleIcon } from "@radix-ui/react-icons";
import DeletePostDialog from "./delete-post-dialog";
import { openPage } from "@nanostores/router";
import { $router } from "@/lib/router";
import useAuth from "@/hooks/use-auth";
import { toast } from "@/components/ui/use-toast"; // π Look here
const PostActions = ({
post,
setIsEditing,
}: {
post: PostType;
setIsEditing: (flag: boolean) => void;
}) => {
const { user } = useAuth();
const showAction = user && user.username === post.author.username;
// Look here π
const authGuard = () => {
if (user.username) return true;
toast({
variant: "destructive",
title: "Sorry! You need to be signed in to do that π",
description: "Please sign in or create an account to continue.",
});
return false;
};
const navigateToCommentsView = (e: React.MouseEvent<HTMLButtonElement>) => {
e.preventDefault();
authGuard() && openPage($router, "post", { postId: post.id }); // π Look here
};
return (
<div className="flex justify-end">
<Button variant={"ghost"} size={"icon"} onClick={navigateToCommentsView}>
<ChatBubbleIcon className="w-4 h-4" />
</Button>
{showAction && (
<Button
variant={"ghost"}
size={"icon"}
onClick={() => setIsEditing(true)}
>
<Pencil2Icon className="w-4 h-4" />
</Button>
)}
{showAction && <DeletePostDialog postId={post.id} />}
</div>
);
};
export default PostActions;
Try the app now: sign out and try to view the comments for a post. You should see a toast message prompting you to sign in or create an account.
In this task, we added authorization to the Posts app. We updated the UI components to hide the βEditβ and βDeleteβ buttons for posts that are not created by the logged-in user. We also updated the API calls to include the session cookie in the request. Finally, we added toast messages to prompt the user to sign in or create an account when they try to create a post or comment without being logged in.