Part 9 — Develop the My Post page in your Next.js app

Loi Le
6 min readDec 28, 2023

--

This is a part of the Tutorial to build a blog platform with Elixir Phoenix and Next.js that help you to develop a web application from scratch using modern technologies such as Elixir Phoenix, Next.js, and more.

Index | < Pre | Next >

In the last part, we have used next-auth to implement sign-in, sign-up, and access control for the create post page in the previous part. In this part, we will create the my-posts page that will show the posts made by the current user. To do this, we need to create a new hook file src/app/hooks/useQueryMyPostList.ts

import { useQuery } from "react-query";
import axios from "../axios";
import { useSession } from "next-auth/react";
import { PostDto } from "../models/posts/types";
import { toModel } from "../models/posts";

export const QUERY_MY_POSTS = "QUERY_MY_POSTS";

const queryMyPosts = (accessToken?: string) => async () => {
const response = await axios.get<{
data: PostDto[];
}>("/api/my-posts", {
headers: {
Authorization: accessToken,
},
});
return response.data?.data?.map((postDto) => toModel(postDto));
};

const useQueryMyPosts = () => {
const { data: session } = useSession();
const accessToken = session?.accessToken;

return useQuery([QUERY_MY_POSTS], queryMyPosts(accessToken), {
enabled: !!accessToken,
});
};

export default useQueryMyPosts;

The next step is to create a new page file: src/app/my-posts/page.tsx

"use client";

const MyPosts: React.FC = () => {
return (
<div></div>
);
};

export default MyPosts;

This page will display all of the posts that user created. So, this is the page that we have to protect, if user have not logged in, user can not access this page. This is something like the create post page. So we have to duplicate the logic from src/app/create-post/page.tsx

// ...
const { data: session, status } = useSession();

// ...

useEffect(() => {
if (status === "unauthenticated") {
signIn();
}
}, [status]);

// ...

if (status !== "authenticated") return null;

//...

We should avoid duplicating code. We need to organize these pages that need authentication so that we can apply a common protection logic. Nextjs allows us to do this easily. We can create a new layout file for authenticated pages: /src/app/(authenticated-pages)/layout.tsx

"use client";

import { signIn, useSession } from "next-auth/react";
import Loading from "../components/atoms/Loading";
import { useEffect } from "react";

export default function AuthenticatedLayout({
children,
}: {
children: React.ReactNode;
}) {
const { status } = useSession();

useEffect(() => {
if (status === "unauthenticated") {
signIn();
}
}, [status]);

if (status === "loading")
return (
<div className="my-24">
<Loading />
</div>
);

if (status === "authenticated") {
return <>{children}</>;
}

return null;
}

We transfer the authentication protection logic from the create post page to this layout. This means that any pages that need authentication should be placed in the folder /src/app/(authenticated-pages). These pages will share the same layout that we have created. This allows us to apply a general protection logic that can be reused for other pages. We also need to move the src/app/create-post/page.tsx file to src/app/(authenticated-pages)/create-post/page.tsx and remove the authentication requirement logic. We should also update the file references accordingly.

"use client";
import { useSession } from "next-auth/react";

import useCreatePost, { CreatePostParams } from "@/app/hooks/useCreatePost";
import { useRouter } from "next/navigation";
import { useForm } from "react-hook-form";
import { toast } from "react-toastify";
import * as yup from "yup";
import { yupResolver } from "@hookform/resolvers/yup";
import FormInput from "@/app/components/form/FormInput";
import FormMarkDownInput from "@/app/components/form/FormMarkDownInput";
import Button from "@/app/components/atoms/Button";

const CreatePost: React.FC = () => {
const { data: session, status } = useSession();

const router = useRouter();
const schema = yup.object().shape({
title: yup.string().nullable().required("Please input title"),
description: yup.string().nullable().required("Please input description"),
content: yup.string().nullable().required("Please input content"),
});

const {
control,
handleSubmit,
formState: { errors },
} = useForm<CreatePostParams>({
resolver: yupResolver(schema),
});

const { mutate: createPost, isLoading: isSubmitting } = useCreatePost(
(data) => {
toast.success("Post created successfully");
data?.id && router.push(`/posts/${data.id}`);
},
() => {
toast.error("Something went wrong while creating post");
}
);

const onSubmit = (values: CreatePostParams) =>
createPost({
...values,
accessToken: session?.accessToken,
});

return (
<div className="flex min-h-full flex-1 flex-col justify-center px-6 py-12 lg:px-8">
<div className="sm:mx-auto sm:w-full sm:max-w-7xl">
<h2 className="mt-10 text-center text-2xl font-bold leading-9 tracking-tight text-gray-900">
Create Post
</h2>
</div>
<div className="mt-10 sm:mx-auto sm:w-full sm:max-w-7xl px-8">
<form className="space-y-6" onSubmit={handleSubmit(onSubmit)}>
<FormInput
label="Title"
name="title"
containerClassName="max-w-md mx-auto"
control={control}
error={errors.title}
/>
<FormInput
containerClassName="max-w-md mx-auto"
label="Description"
name="description"
control={control}
error={errors.description}
/>
<FormMarkDownInput
label={"Content"}
control={control}
name="content"
error={errors.content}
/>
<div className="w-full flex items-center justify-center space-x-2">
<Button
className="!w-32"
variant="white"
onClick={() => router.back()}
>
Back
</Button>
<Button className="!w-32" type="submit" loading={isSubmitting}>
Create Post
</Button>
</div>
</form>
</div>
</div>
);
};

export default CreatePost;

The create post page now has a clear logic. Our code is more organized. We also need to move the my-posts page that we created earlier src/app/my-posts/page.tsx to src/app/(authenticated-pages)/my-posts/page.tsx. This will ensure that only logged-in users can access this page.

"use client";

const MyPosts: React.FC = () => {
return (
<div></div>
);
};

export default MyPosts;

The next step is to add the logic for this page:

"use client";

import Button from "@/app/components/atoms/Button";
import ErrorMessage from "@/app/components/atoms/ErrorMessage";
import Loading from "@/app/components/atoms/Loading";
import useQueryMyPosts from "@/app/hooks/useQueryMyPostList";
import { useRouter } from "next/navigation";

const MyPosts: React.FC = () => {
const router = useRouter();
const { data: posts, isLoading, isError } = useQueryMyPosts();

if (isLoading)
return (
<div className="my-24">
<Loading />
</div>
);

if (isError) return <ErrorMessage />;

return (
<div className="bg-white py-12">
<div className="mx-auto max-w-7xl px-6 lg:px-8">
<h2 className="text-3xl font-bold tracking-tight text-gray-900 sm:text-4xl text-center">
My Posts
</h2>
{!posts || posts?.length === 0 ? (
<div className="text-center">
<div className="mt-6 text-gray-600 text-lg text-center">
There is no posts created. Please create the first one.
</div>
<Button
className="mt-4"
onClick={() => router.push("/create-post")}
>
Create
</Button>
</div>
) : (
<div className="mt-12 mx-auto grid max-w-7xl grid-cols-1 gap-x-8 gap-y-16 lg:mx-0 lg:max-w-none lg:grid-cols-3">
{posts?.map(
(post) =>(
<PostCard key={post.id} {...post} />
)
)}
</div>
)}
</div>
</div>
);
};

export default MyPosts;

We use the useQueryMyPosts hook that we created earlier to fetch the posts list. We handle the loading and error states as well. In the UI, we display the title and the create button that leads to the create post page. We also show a message when there are no posts. Lastly, we use the PostCard component from src/app/posts/PostCard to render the posts list. However, there is a minor but important issue. The PostCard component is currently in the src/app/posts folder, which belongs to the home page domain. We are reusing it in the src/app/(authenticated-pages)/my-posts folder, which is a different domain.

We need to make the PostCard component a common component that can be used in any domain. This is crucial because if we leave it as a domain-specific component, the developer may modify it in the future without realizing that it will affect other domains. Therefore, we move the src/app/posts/PostCard/index.tsx file to src/app/components/PostCard/index.tsx

After that, we also need to update the reference in the src/app/post/PostList/index.tsx file.

...
import PostCard from "@/app/components/PostCard";
...

and src/app/authenticated-pages/my-posts/page.tsx

...
import PostCard from "@/app/components/PostCard";
...

The final step is to include the my-posts page in the account menu of the header. We can do this by modifying the userNavigation variable in the src/app/components/atoms/Header/index.tsx file.

const userNavigation = [
{
name: "My Posts",
onClick: () => {
router.push("/my-posts");
},
},
{
name: "Sign out",
onClick: () => signOut(),
},
];

We added the my-post link to the account menu, which allows logged-in users to see their posts or log out. We can test our app again and it should work as expected.

Great job! We have finished creating the my posts page on the front end. However, we also need to enable user to edit or remove their posts on this page. We will start by working on the back end in the next part.

Index | < Pre | Next>

--

--