Part 3— Setting Up a Front End App with Next.js and React to Build a Post List Page with Hard-coded Data

Loi Le
9 min readOct 19, 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 >

Welcome to the UI part of the tutorial. This part we will do step by step to build a frontend application that use the API that we completed from part 1 . We will learn how to structure the project as well as refactoring mindset while develop new features.

We plan to use Next.js and React ecosystem. Next.js is a famous React framework that supports both static site generation (SSG) and server-side rendering (SSR) for optimal performance and user experience. So, This is a great way to create a fast, dynamic and SEO-friendly website as our Blog platform

Up and Running

Make sure you have the Node.js 16.8 or later and npm or yarn installed.

For this article, we use yarn to install the packages. However, feel free to use npm or any other packaging manager you are comfortable with.

Now, create project with:

npx create-next-app@latest lani_blog_web

When you install the app, you will be promt to select the list of options, I suggest you to select as below:

  • Would you like to use TypeScript with this project? No / Yes
  • Would you like to use ESLint with this project? No / Yes
  • Would you like to use Tailwind CSS with this project? No / Yes
  • Would you like to use `src/` directory with this project? No / Yes
  • Use App Router (recommended)? No / Yes
  • Would you like to customize the default import alias? No / Yes

Now you can run your app:

$ cd lani_blog_web
$ yarn dev

Then Access your app via: http://localhost:3000 and see the result

Getting started is easy!! Now will will update our home page. But firstly, please update the src/app/globals.css file to remove unused style that generated default by Next.js and keep tailwind CSS configuration only:

@tailwind base;
@tailwind components;
@tailwind utilities;

Build the home page that show list of posts

Firstly, we decide that the home page will show this list of posts. So, we will create new page for the post list src/app/posts/page.tsx with the content that I prepare for you:

const postList = [
{
id: "1",
title: "Lorem ipsum dolor sit amet, consectetur adipiscing elit sed do eiusmod",
description: "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Bibendum at varius vel pharetra. Scelerisque viverra mauris in aliquam sem fringilla.",
createdAt: new Date("2023-08-01"),
createdBy: "Lani",
},
{
id: "2",
title: "Lorem ipsum dolor sit amet, consectetur adipiscing elit",
description: "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ipsum dolor sit amet consectetur adipiscing elit ut. Convallis aenean et tortor at risus viverra adipiscing. Tempor orci dapibus ultrices in iaculis nunc sed augue.",
createdAt: new Date("2023-08-02"),
createdBy: "Lani",
},
];

export default function PostList() {
return (
<div className="min-h-full">
<header className="border-b border-gray-200 bg-white">
<div className="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
<div className="flex h-16 justify-between">
<div className="flex items-center">
<div className=" text-lg md:text-3xl font-semibold">
Lani Blog
</div>
</div>
</div>
</div>
</header>
<main>
<div className="bg-white py-24">
<div className="mx-auto max-w-7xl px-6 lg:px-8">
<div className="mx-auto lg:mx-0 w-full md:flex justify-between items-center">
<div className="">
<h2 className="text-3xl font-bold tracking-tight text-gray-900 sm:text-4xl">
From the blog
</h2>
<p className="mt-2 text-lg leading-8 text-gray-600">
Learn how to grow your business with our expert advice.
</p>
</div>
</div>
<div className="mx-auto max-w-7xl border-t border-gray-200 pt-10 mt-10 sm:mt-16 sm:pt-16 lg:mx-0 lg:max-w-none">
{postList.length === 0 ? (
<>
<div className="mt-6 text-gray-600 text-lg text-center">
There is no posts created. Please create the first one.
</div>
</>
) : (
<div className="grid grid-cols-1 gap-x-8 gap-y-16 w-full lg:grid-cols-3">
{postList.map((post) => (
<div key={post.id} className="max-w-xl">
<div className="flex items-center gap-x-4 text-xs">
<time dateTime={post.createdAt?.toString()} className="text-gray-500">
{post.createdAt.toLocaleDateString()}
</time>
</div>
<div className="group relative">
<h3 className="mt-3 text-lg font-semibold leading-6 text-gray-900 group-hover:text-gray-600">
{post.title}
</h3>
<p className="mt-5 line-clamp-3 text-sm leading-6 text-gray-600">
{post.description}
</p>
</div>
</div>
))}
</div>
)}
</div>
</div>
</div>
</main>
</div>
);
}

Next, you the the next.config.js file to make sure user will be redirected to the post list page when they access the home page:

const nextConfig = {
async redirects() {
return [
{
source: "/",
destination: "/posts",
permanent: true,
},
];
},
};

module.exports = nextConfig

Then, you can remove the default home page that Next.js generated for your: src/app/page.tsx

Now you can access our page again, you will see our new home page:

Refactor the code

Lets take a look about our home page component src/app/posts/page.tsx

At the beginning of the file is the hard coding list of posts that we will replace by the data from our server later.

const postList = [
{
id: "1",
title: "Lorem ipsum dolor sit amet, consectetur adipiscing elit sed do eiusmod",
description: "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Bibendum at varius vel pharetra. Scelerisque viverra mauris in aliquam sem fringilla.",
createdAt: new Date("2023-08-01"),
createdBy: "Lani",
},
{
id: "2",
title: "Lorem ipsum dolor sit amet, consectetur adipiscing elit",
description: "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ipsum dolor sit amet consectetur adipiscing elit ut. Convallis aenean et tortor at risus viverra adipiscing. Tempor orci dapibus ultrices in iaculis nunc sed augue.",
createdAt: new Date("2023-08-02"),
createdBy: "Lani",
},
];

After that is the React Component that used Tailwind CSS for styling and this also implemented responsive feature, so it works fine for both Desktop and Mobile. At the beginning of the Component is the header:

<header className="border-b border-gray-200 bg-white">
<div className="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
<div className="flex h-16 justify-between">
<div className="flex items-center">
<div className=" text-lg md:text-3xl font-semibold">
Lani Blog
</div>
</div>
</div>
</div>
</header>

Next, is the main of the page that can divide into 2 parts: and the list of post:

  1. The Blog introduction:
<div>
<h2 className="text-3xl font-bold tracking-tight text-gray-900 sm:text-4xl">
From the blog
</h2>
<p className="mt-2 text-lg leading-8 text-gray-600">
Learn how to grow your business with our expert advice.
</p>
</div>

2. The list of post

<div className="mx-auto max-w-7xl border-t border-gray-200 pt-10 mt-10 sm:mt-16 sm:pt-16 lg:mx-0 lg:max-w-none">
{postList.length === 0 ? (
<>
<div className="mt-6 text-gray-600 text-lg text-center">
There is no posts created. Please create the first one.
</div>
</>
) : (
<div className="grid grid-cols-1 gap-x-8 gap-y-16 w-full lg:grid-cols-3">
{postList.map((post) => (
<div key={post.id} className="max-w-xl">
<div className="flex items-center gap-x-4 text-xs">
<time
dateTime={post.createdAt}
className="text-gray-500"
>
{post.createdAt &&
new Date(post.createdAt).toLocaleDateString()}
</time>
</div>
<div className="group relative">
<h3 className="mt-3 text-lg font-semibold leading-6 text-gray-900 group-hover:text-gray-600">
{post.title}
</h3>
<p className="mt-5 line-clamp-3 text-sm leading-6 text-gray-600">
{post.description}
</p>
</div>
</div>
))}
</div>
)}
</div>

We handled 2 cases here, one for no data and the other for data is not empty

As you can see, there are many concerns in our component. Let’s separate them:

Firstly, about the header, this should be the header for all of pages, so that’s a good idea if we can place them at the layout. Lets move it to src/app/layout.tsx

import "./globals.css";
import { Inter } from "next/font/google";

const inter = Inter({ subsets: ["latin"] });

export const metadata = {
title: "Create Next App",
description: "Generated by create next app",
};

export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="en">
<body className={inter.className}>
<div className="min-h-full">
<header className="border-b border-gray-200 bg-white">
<div className="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
<div className="flex h-16 justify-between">
<div className="flex items-center">
<div className=" text-lg md:text-3xl font-semibold">
Lani Blog
</div>
</div>
</div>
</div>
</header>
{children}
</div>
</body>
</html>
);
}

Next, we should separate the the blog introduction and the empty message and the post item into separated components. These components only belong to the home page instead of common components. So, lets create src/app/posts/Introduction/index.tsx :

const Introduction: React.FC = () => {
return (
<div className="">
<h2 className="text-3xl font-bold tracking-tight text-gray-900 sm:text-4xl">
From the blog
</h2>
<p className="mt-2 text-lg leading-8 text-gray-600">
Learn how to grow your business with our expert advice.
</p>
</div>
);
};

export default Introduction;

then, create src/app/posts/EmptyMessage/index.tsx file:

const EmptyMessage = () => {
return (
<div className="mt-6 text-gray-600 text-lg text-center">
There is no posts created. Please create the first one.
</div>
);
};

export default EmptyMessage;

and create the last one src/app/posts/PostCard/index.tsx file:

interface PostItemProps {
id: string;
title: string;
description: string;
createdAt?: Date;
}

const PostCard: React.FC<PostItemProps> = ({
id,
title,
description,
createdAt,
}) => {
return (
<div key={id} className="max-w-xl">
<div className="flex items-center gap-x-4 text-xs">
<time dateTime={createdAt?.toString()} className="text-gray-500">
{createdAt?.toLocaleDateString()}
</time>
</div>
<div className="group relative">
<h3 className="mt-3 text-lg font-semibold leading-6 text-gray-900 group-hover:text-gray-600">
{title}
</h3>
<p className="mt-5 line-clamp-3 text-sm leading-6 text-gray-600">
{description}
</p>
</div>
</div>
);
};

export default PostCard;

Now, your src/app/posts/page.tsxshould be:

import EmptyMessage from "./EmptyMessage";
import Introduction from "./Introduction";
import PostCard from "./PostCard";

const postList = [
{
id: "1",
title: "Lorem ipsum dolor sit amet, consectetur adipiscing elit sed do eiusmod",
description: "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Bibendum at varius vel pharetra. Scelerisque viverra mauris in aliquam sem fringilla.",
createdAt: new Date("2023-08-01"),
createdBy: "Lani",
},
{
id: "2",
title: "Lorem ipsum dolor sit amet, consectetur adipiscing elit",
description: "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ipsum dolor sit amet consectetur adipiscing elit ut. Convallis aenean et tortor at risus viverra adipiscing. Tempor orci dapibus ultrices in iaculis nunc sed augue.",
createdAt: new Date("2023-08-02"),
createdBy: "Lani",
},
];

export default function PostList() {
return (
<main>
<div className="bg-white py-24">
<div className="mx-auto max-w-7xl px-6 lg:px-8">
<Introduction />
<div className="mx-auto max-w-7xl border-t border-gray-200 pt-10 mt-10 sm:mt-16 sm:pt-16 lg:mx-0 lg:max-w-none">
{postList.length === 0 ? (
<EmptyMessage />
) : (
<div className="grid grid-cols-1 gap-x-8 gap-y-16 w-full lg:grid-cols-3">
{postList.map((post) => (
<PostCard key={post.id} {...post} />
))}
</div>
)}
</div>
</div>
</div>
</main>
);
}

Now our structure seems okay now. These components below is the stateless components:

  • Introduction
  • EmptyMessage
  • PostCard

…and the PostList component take responsibility of layout and provide data.

After the refactoring, we check our page again:

After setting up the Front end project and creating the home page, we have a cool-looking web app. But the data is still hard-coded and we need to get it from the server. That’s what we’ll do in the next step.

Index | < Pre | Next >

--

--