Getting Started with Next.js

This is the first tutorial in a set of tutorials on building a full stack web app with Next.js. To learn how to use Next.js, let's create a clone of x.com. We won't include all the features, it will be a simplified version, a mock version, but it will have all the important features and a similar look and feel. Let's call this clone threads.

In this tutorial, we will:

  • Setup a new project from scratch
  • Create different pages and routes
  • Navigate between those pages

Basically, we're going to learn how to use Next.js to serve HTML pages to the browser. This might sound basic, because it is, but Next is a very powerful framework that does some very sophisticated and impressive things. Let's start with the basics and work our way up from there.

Here's an example of what we will build in this tutorial:

Avoid looking at the source code until the you've completed the tutorial for yourself. It exists as a reference for you to look at if you get stuck or want to compare your code to mine, but you need to try to build it yourself first.

Setup

step 1:

Create a new Next.js project using create-next-app

npx create-next-app@latest
npx create-next-app@latest
npx create-next-app@latest
npx create-next-app@latest
step 2:

When prompted, answer the prompts like this:

What is your project named? threads
Would you like to use TypeScript? Yes
Would you like to use ESLint? Yes
Would you like to use Tailwind CSS? Yes
Would you like to use `src/` directory? Yes
Would you like to use App Router? (recommended) Yes
Would you like to customize the default import alias? No
What is your project named? threads
Would you like to use TypeScript? Yes
Would you like to use ESLint? Yes
Would you like to use Tailwind CSS? Yes
Would you like to use `src/` directory? Yes
Would you like to use App Router? (recommended) Yes
Would you like to customize the default import alias? No
What is your project named? threads
Would you like to use TypeScript? Yes
Would you like to use ESLint? Yes
Would you like to use Tailwind CSS? Yes
Would you like to use `src/` directory? Yes
Would you like to use App Router? (recommended) Yes
Would you like to customize the default import alias? No
What is your project named? threads
Would you like to use TypeScript? Yes
Would you like to use ESLint? Yes
Would you like to use Tailwind CSS? Yes
Would you like to use `src/` directory? Yes
Would you like to use App Router? (recommended) Yes
Would you like to customize the default import alias? No
  • The project name doesn't matter, you can really use whatever you like here.
  • Yes to TypeScript, get used to it. It doesn't matter what your (or my) opinions are anymore, TS is taking over in web dev and to not use it in a project like this is to go against the grain.
  • Yes to ESLint because it's nice to have a tool tell you that you're coding wrong. (https://www.youtube.com/watch?v=sSJBeWPIipQ&t=0s&ab_channel=JSWORLDConference)
  • Tailwind is popular and seems to make developers happy, so select Yes. You can select no to tailwind and use plain old css, sass, css modules, or whatever you like for CSS. Just be careful about CSS-in-js which are not supported in Server Components.
  • Yes to src/, it makes organizing code easier which becomes more helpful when your project grows.
  • Yes to App Router. This is the most important thing to say yes to. If you selected no here, give up and start again.
  • No to customizing the default alias, the default is good. This let's you import files from src/ using @/ instead of having any of those ../../ go up a parent directory or serveral nonsense.
step 3:

From inside the project's directory, run the development server.

npm run dev
npm run dev
npm run dev
npm run dev

Open http://localhost:3000 in your web browser.

Congrats, you're now staring at the Next.js marketing page. Let's dig into the source code and update this page.

The code for this home page is in /src/app/page.tsx which contains a lot of code we just don't care about right now.

step 5:

Replace the contents of /src/app/page.tsx with the following:

src/app/page.tsx
export default function Home() {
return (
<main className="text-center mt-10">
<h1>Threads</h1>
<p>Threads is a clone of x.com</p>
</main>
)
}
src/app/page.tsx
export default function Home() {
return (
<main className="text-center mt-10">
<h1>Threads</h1>
<p>Threads is a clone of x.com</p>
</main>
)
}
src/app/page.tsx
export default function Home() {
return (
<main className="text-center mt-10">
<h1>Threads</h1>
<p>Threads is a clone of x.com</p>
</main>
)
}
src/app/page.tsx
export default function Home() {
return (
<main className="text-center mt-10">
<h1>Threads</h1>
<p>Threads is a clone of x.com</p>
</main>
)
}

Go back to the browser and you'll see the updated content. No need to re run the next server or refresh the browser page because Next.js's dev server uses fast refresh. Just save a file and see the updates immediately. Feel free to play around with the JSX syntax a bit.

TSX

We're using .tsx extension so that we can write JSX inside our TypeScript files. You will learn more about JSX in the Introduction to JSX section. For now, just know that it's a way to write HTML-like markup inside of a JavaScript or TypeScript file.

App Router

All of the application source code is inside of the /src directory. Everything else is configuration, tooling, or static assets that we can ignore right now.

  • public
  • src
    • app
      • page.tsx
      • layout.tsx
      • globals.css
  • next.config.js
  • tailwind.config.ts
  • tsconfig.json
  • ...

Inside of /src there is an app directory where we can setup all the routes for our app. This paradigm is called App Router and here are the rules:

  • Folders inside of app are used to define routes.
  • Files are used to create UI.
  • src
    • app(/ route)
      • page.tsx(UI)
      • create-post(/create-post route)
        • page.tsx(UI)

Home Route /

Right now, there are no folders inside of app, so we're in the / route which maps to the url: http://localhost:3000/. In the web browser, we see the home page which is the contents of src/app/page.tsx.

localhost:3000/

  • src
    • app(/)
      • page.tsx(UI)

Create Post Route

Next, we will create a new route for localhost:3000/create-post

step 1:

Create a new folder inside of app called create-post.

  • src
    • app
      • page.tsx
      • create-post(/create-post route)

We have now defined a new route that maps to the url: localhost:3000/create-post

But visiting that url gives us a 404 error. That's because there's no file inside of create-post to generate the UI.

step 1:

Create a new file inside of create-post called page.tsx.

  • src
    • app
      • page.tsx
      • create-post(/create-post route)
        • page.tsx(UI)
step 1:

And add the following code to page.tsx:

src/app/create-post/page.tsx
export default function CreatePost() {
return (
<main className="text-center mt-10">
<h1>Create Post</h1>
<p>TODO: create post form</p>
</main>
)
}
src/app/create-post/page.tsx
export default function CreatePost() {
return (
<main className="text-center mt-10">
<h1>Create Post</h1>
<p>TODO: create post form</p>
</main>
)
}
src/app/create-post/page.tsx
export default function CreatePost() {
return (
<main className="text-center mt-10">
<h1>Create Post</h1>
<p>TODO: create post form</p>
</main>
)
}
src/app/create-post/page.tsx
export default function CreatePost() {
return (
<main className="text-center mt-10">
<h1>Create Post</h1>
<p>TODO: create post form</p>
</main>
)
}

Now you can visit localhost:3000/create-post to see the new page.

Dynamic Routes

When we eventually create a new post, we'll display a list of posts on the home page. Clicking on a post will take you to a url like localhost:3000/post/123 where 123 is the id of the post. This is called a dynamic route because the url is dynamic, it changes based on the post id.

step 1:

Create a new folder inside of app called post.

Create a new folder inside of post called [id].

Create a new file inside of /post/[id] called page.tsx.

  • src
    • app
      • post
        • [id](/post/[id] route)
          • page.tsx(UI)
    • ...

If we named the directory id instead of [id], the url will be localhost:3000/post/id but id needs to be dynamic, not the literal value id. By wrapping the folder name id in square brackets [id], we've created a dynamic route, where any value can be used in place of id.

This means we have a route that maps to localhost:3000/post/any-value. Eventually we will make sure that this route only works with a valid post id, but for now any value will work.

step 1:

Add the following code to the page.tsx file:

post/[id]/page.tsx
export default function PostPage({ params }: { params: { id: string } }) {
return (
<main className="text-center mt-10">
<h1>Post {params.id}</h1>
<p>TODO: display post</p>
</main>
);
}
post/[id]/page.tsx
export default function PostPage({ params }: { params: { id: string } }) {
return (
<main className="text-center mt-10">
<h1>Post {params.id}</h1>
<p>TODO: display post</p>
</main>
);
}
post/[id]/page.tsx
export default function PostPage({ params }: { params: { id: string } }) {
return (
<main className="text-center mt-10">
<h1>Post {params.id}</h1>
<p>TODO: display post</p>
</main>
);
}
post/[id]/page.tsx
export default function PostPage({ params }: { params: { id: string } }) {
return (
<main className="text-center mt-10">
<h1>Post {params.id}</h1>
<p>TODO: display post</p>
</main>
);
}

This page is slightly different because the function takes a params object as an argument. This object contains the dynamic route parameters. In this case, the id of the post.

It is important to know that the name of the directory without the square brackets will be the name of the param in the params object. So if we had a folder called [pancakes], the params object would have look like { params: { pancakes: string } }.

  • src
    • app
      • post
        • [pancakes]
          • page.tsx
    • ...
post/[id]/page.tsx
function PostPage({ params }: { params: { pancakes: string } }) {
return (
<main className="text-center mt-10">
<h1>Post {params.pancakes}</h1>
<p>TODO: display post</p>
</main>
);
}
post/[id]/page.tsx
function PostPage({ params }: { params: { pancakes: string } }) {
return (
<main className="text-center mt-10">
<h1>Post {params.pancakes}</h1>
<p>TODO: display post</p>
</main>
);
}
post/[id]/page.tsx
function PostPage({ params }: { params: { pancakes: string } }) {
return (
<main className="text-center mt-10">
<h1>Post {params.pancakes}</h1>
<p>TODO: display post</p>
</main>
);
}
post/[id]/page.tsx
function PostPage({ params }: { params: { pancakes: string } }) {
return (
<main className="text-center mt-10">
<h1>Post {params.pancakes}</h1>
<p>TODO: display post</p>
</main>
);
}

Please don't name the folder [pancakes], but hopefully you get the point.

Visit localhost:3000/post/any-value in your browser and change the value of any-value to something else. You'll see the page update with the new value.

page.tsx

As you might have guessed, page.tsx is kind of a special file that's used to render the UI for a specific route.

The name of the folder maps to the url in the browser, but the file name, page.tsx, does not. Next knows to look for a file called page inside of the folder and use that to generate the UI.

Next has some special file conventions that we use to render UI. There's layout, page, loading, not-found, error , global-error, route, template, and default. You don't need to learn all of those right now, learn them as you need them.

not-found.tsx

Visit a route that doesn't exist, something like localhost:3000/search. You'll see a very generic Next.js 404 page. It's really nice that Next gives us a 404 html page and doesn't fail or error out in a different way, however, we might want our own 404 page that we can customize and style.

step 1:

Create a new file inside of app called not-found.tsx and add the following content:

not-found.tsx
export default function NotFound() {
return (
<main className="text-center mt-10">
<h1>404</h1>
<p>A custom not found page</p>
</main>
);
}
not-found.tsx
export default function NotFound() {
return (
<main className="text-center mt-10">
<h1>404</h1>
<p>A custom not found page</p>
</main>
);
}
not-found.tsx
export default function NotFound() {
return (
<main className="text-center mt-10">
<h1>404</h1>
<p>A custom not found page</p>
</main>
);
}
not-found.tsx
export default function NotFound() {
return (
<main className="text-center mt-10">
<h1>404</h1>
<p>A custom not found page</p>
</main>
);
}

Now visiting a URL that doesn't exist will show our custom not-found page.

We can add a not-found page to any route. For example, we could add a not-found.tsx file to the /post/[id] route if we wanted a custom 404 page when someone is trying to find a specific post that doesn't exist. Only adding a not-found file to the root will give us a custom 404 page for all routes which is good enough for now.

Root Layout

The app directory came already setup with a file called app.tsx and a file called layout.tsx. Let's take a look at the layout file.

layout.tsx
import "./globals.css"; // styles for tailwind
import type { Metadata } from "next"; // meta data
import { Inter } from "next/font/google"; // font

// import the inter font
const inter = Inter({ subsets: ["latin"] });

// specify some meta data
export const metadata: Metadata = {
title: "Create Next App",
description: "Generated by create next app",
};

// define the layout
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="en">
<body className={inter.className}>{children}</body>
</html>
);
}
layout.tsx
import "./globals.css"; // styles for tailwind
import type { Metadata } from "next"; // meta data
import { Inter } from "next/font/google"; // font

// import the inter font
const inter = Inter({ subsets: ["latin"] });

// specify some meta data
export const metadata: Metadata = {
title: "Create Next App",
description: "Generated by create next app",
};

// define the layout
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="en">
<body className={inter.className}>{children}</body>
</html>
);
}
layout.tsx
import "./globals.css"; // styles for tailwind
import type { Metadata } from "next"; // meta data
import { Inter } from "next/font/google"; // font

// import the inter font
const inter = Inter({ subsets: ["latin"] });

// specify some meta data
export const metadata: Metadata = {
title: "Create Next App",
description: "Generated by create next app",
};

// define the layout
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="en">
<body className={inter.className}>{children}</body>
</html>
);
}
layout.tsx
import "./globals.css"; // styles for tailwind
import type { Metadata } from "next"; // meta data
import { Inter } from "next/font/google"; // font

// import the inter font
const inter = Inter({ subsets: ["latin"] });

// specify some meta data
export const metadata: Metadata = {
title: "Create Next App",
description: "Generated by create next app",
};

// define the layout
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="en">
<body className={inter.className}>{children}</body>
</html>
);
}

The main thing we're going to focus on right now is the RootLayout function at the bottom. This file also contains the default tailwind styles, an imported font, and some [meta data] (https://nextjs.org/docs/app/building-your-application/optimizing/metadata). We'll address these things later on, for now just look at the RootLayout.

This is the Root Layout and it is a required file and function in the root of your Next app. This layout defines UI that is shared between all of the pages, so it must define the <html> and <body> tags. It's also responsible for rendering the pages using the children prop.

At a bare minimum, the function would look like this:

export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html>
<body>
{children}
</body>
<html>
)
}
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html>
<body>
{children}
</body>
<html>
)
}
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html>
<body>
{children}
</body>
<html>
)
}
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html>
<body>
{children}
</body>
<html>
)
}

We don't add a <head> tag because next manages that for us with the use of Metadata, but we are responsible for adding in the html and body tags. {children} will be the current page that is being rendered, and is currently the only thing that changes when we visit a different page.

step 1:

Update the RootLayout to include a placeholder for a nav bar:

layout.tsx
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html lang="en">
<body className={inter.className}>

<nav className="text-lg text-center">
This is going to be the site navigation
</nav>

{children}
</body>
</html>
)
}
layout.tsx
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html lang="en">
<body className={inter.className}>

<nav className="text-lg text-center">
This is going to be the site navigation
</nav>

{children}
</body>
</html>
)
}
layout.tsx
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html lang="en">
<body className={inter.className}>

<nav className="text-lg text-center">
This is going to be the site navigation
</nav>

{children}
</body>
</html>
)
}
layout.tsx
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html lang="en">
<body className={inter.className}>

<nav className="text-lg text-center">
This is going to be the site navigation
</nav>

{children}
</body>
</html>
)
}

Now try navigating between the different pages home, /create-post, /post/123. Notice how the layout code is always the same, but the page changes.

layout.tsx

Just like with the not-found.tsx file, you can also include a layout.tsx file in any of the child directories. For example, we could add a layout.tsx file to the /post or /create-post folder if we wanted to have a nested layout for those child routes. Nested route We don't need to do that right now, but it's good to know that we can.

Components (NavBar)

We've only looked at the special files that Next uses to render UI, but we can also create our own components and use them in our pages.

step 1:

Create a new file /src/app/nav-bar.tsx with the following content:

src/app/nav-bar.tsx
export default function NavBar() {
return (
<nav className="text-lg text-center">
This is going to be the site navigation
</nav>
);
}
src/app/nav-bar.tsx
export default function NavBar() {
return (
<nav className="text-lg text-center">
This is going to be the site navigation
</nav>
);
}
src/app/nav-bar.tsx
export default function NavBar() {
return (
<nav className="text-lg text-center">
This is going to be the site navigation
</nav>
);
}
src/app/nav-bar.tsx
export default function NavBar() {
return (
<nav className="text-lg text-center">
This is going to be the site navigation
</nav>
);
}
step 1:

Update the RootLayout to use the new NavBar component:

layout.tsx
import NavBar from "./nav-bar"

export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html lang="en">
<body className={inter.className}>

<NavBar />

{children}
</body>
</html>
)
}
layout.tsx
import NavBar from "./nav-bar"

export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html lang="en">
<body className={inter.className}>

<NavBar />

{children}
</body>
</html>
)
}
layout.tsx
import NavBar from "./nav-bar"

export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html lang="en">
<body className={inter.className}>

<NavBar />

{children}
</body>
</html>
)
}
layout.tsx
import NavBar from "./nav-bar"

export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html lang="en">
<body className={inter.className}>

<NavBar />

{children}
</body>
</html>
)
}

The UI of the site has not changed, we've just refactored the nav bar code into it's own file. As our code grows, refactoring code into smaller components will help us keep our code organized and easier to maintain.

As long as the name of the file, nav-bar.tsx in this case, doesn't conflict with the name of a special file, page.tsx for example, then it won't effect our routes. We can create as many components as we like and import them into the pages, layouts, or other special files so they can be rendered as part of the UI.

step 1:

Update the NavBar component to include links to the home and create post pages:

import Link from "next/link";

export default function NavBar() {
return (
<nav className="w-full">
<ul className="flex justify-between items-center max-w-lg m-auto">
<li>
<Link href="/">Home</Link>
</li>
<li>
<Link href="/create-post">Create Post</Link>
</li>
</ul>
</nav>
);
}
import Link from "next/link";

export default function NavBar() {
return (
<nav className="w-full">
<ul className="flex justify-between items-center max-w-lg m-auto">
<li>
<Link href="/">Home</Link>
</li>
<li>
<Link href="/create-post">Create Post</Link>
</li>
</ul>
</nav>
);
}
import Link from "next/link";

export default function NavBar() {
return (
<nav className="w-full">
<ul className="flex justify-between items-center max-w-lg m-auto">
<li>
<Link href="/">Home</Link>
</li>
<li>
<Link href="/create-post">Create Post</Link>
</li>
</ul>
</nav>
);
}
import Link from "next/link";

export default function NavBar() {
return (
<nav className="w-full">
<ul className="flex justify-between items-center max-w-lg m-auto">
<li>
<Link href="/">Home</Link>
</li>
<li>
<Link href="/create-post">Create Post</Link>
</li>
</ul>
</nav>
);
}

Now we are able to more easily navigate between the pages. Notice that we're using Link instead of a tags. This is because Next has a special Link component that we can use that enables Next to do some optimizations like prefetch and client-side navigation between routes.

Fake Database

Let's setup a fake database so that we can start to build out the UI for the home page and the individual post pages.

Create a new file /src/fakeDatabase.ts with the following content:

export type User = {
id: number;
username: string;
firstName: string;
lastName: string;
avatar: string;
followers: number;
};

export type Media = {
id: number;
type: "image" | "video";
url: string;
width: number;
height: number;
};

export type Post = {
id: number;
user: User;
date: string;
content: string;
likes: number;
replies: number;
replyId?: number;
media?: Media;
};

const users: User[] = [
{
id: 1,
username: "sam",
avatar:
"https://images.clerk.dev/uploaded/img_2UwOmQYFLO3AhjoORmTygZ7OM8Y.png",
firstName: "saM",
lastName: "saM",
followers: 100,
},
];

const posts: Post[] = [
{
id: 1,
user: users[1],
date: "2024-01-01T12:00:00.000Z",
content:
"Just some content to get us started. This is a post with some content. It's not very interesting, but it's a post.",
likes: 10,
replies: 0,
},
{
id: 1,
user: users[1],
date: "2024-01-01T12:00:00.000Z",
content: "This one is slightly more interesting. It has an image.",
likes: 10,
replies: 0,
media: {
id: 1,
type: "image",
url: "https://picsum.photos/seed/picsum/200/300",
width: 200,
height: 300,
},
},
];

export function getPosts(): Post[] {
return posts.filter((post) => !post.replyId);
}

export function getPost(id: number): Post | undefined {
return posts.find((post) => post.id === id);
}

export function getPostResponses(id: number): Post[] {
return posts.filter((post) => post.replyId === id);
}

export function getUser(username: string): User | undefined {
return users.find((user) => user.username === username);
}

export function getPostsForUser(username: string): Post[] {
return posts.filter((post) => post.user.username === username);
}
export type User = {
id: number;
username: string;
firstName: string;
lastName: string;
avatar: string;
followers: number;
};

export type Media = {
id: number;
type: "image" | "video";
url: string;
width: number;
height: number;
};

export type Post = {
id: number;
user: User;
date: string;
content: string;
likes: number;
replies: number;
replyId?: number;
media?: Media;
};

const users: User[] = [
{
id: 1,
username: "sam",
avatar:
"https://images.clerk.dev/uploaded/img_2UwOmQYFLO3AhjoORmTygZ7OM8Y.png",
firstName: "saM",
lastName: "saM",
followers: 100,
},
];

const posts: Post[] = [
{
id: 1,
user: users[1],
date: "2024-01-01T12:00:00.000Z",
content:
"Just some content to get us started. This is a post with some content. It's not very interesting, but it's a post.",
likes: 10,
replies: 0,
},
{
id: 1,
user: users[1],
date: "2024-01-01T12:00:00.000Z",
content: "This one is slightly more interesting. It has an image.",
likes: 10,
replies: 0,
media: {
id: 1,
type: "image",
url: "https://picsum.photos/seed/picsum/200/300",
width: 200,
height: 300,
},
},
];

export function getPosts(): Post[] {
return posts.filter((post) => !post.replyId);
}

export function getPost(id: number): Post | undefined {
return posts.find((post) => post.id === id);
}

export function getPostResponses(id: number): Post[] {
return posts.filter((post) => post.replyId === id);
}

export function getUser(username: string): User | undefined {
return users.find((user) => user.username === username);
}

export function getPostsForUser(username: string): Post[] {
return posts.filter((post) => post.user.username === username);
}
export type User = {
id: number;
username: string;
firstName: string;
lastName: string;
avatar: string;
followers: number;
};

export type Media = {
id: number;
type: "image" | "video";
url: string;
width: number;
height: number;
};

export type Post = {
id: number;
user: User;
date: string;
content: string;
likes: number;
replies: number;
replyId?: number;
media?: Media;
};

const users: User[] = [
{
id: 1,
username: "sam",
avatar:
"https://images.clerk.dev/uploaded/img_2UwOmQYFLO3AhjoORmTygZ7OM8Y.png",
firstName: "saM",
lastName: "saM",
followers: 100,
},
];

const posts: Post[] = [
{
id: 1,
user: users[1],
date: "2024-01-01T12:00:00.000Z",
content:
"Just some content to get us started. This is a post with some content. It's not very interesting, but it's a post.",
likes: 10,
replies: 0,
},
{
id: 1,
user: users[1],
date: "2024-01-01T12:00:00.000Z",
content: "This one is slightly more interesting. It has an image.",
likes: 10,
replies: 0,
media: {
id: 1,
type: "image",
url: "https://picsum.photos/seed/picsum/200/300",
width: 200,
height: 300,
},
},
];

export function getPosts(): Post[] {
return posts.filter((post) => !post.replyId);
}

export function getPost(id: number): Post | undefined {
return posts.find((post) => post.id === id);
}

export function getPostResponses(id: number): Post[] {
return posts.filter((post) => post.replyId === id);
}

export function getUser(username: string): User | undefined {
return users.find((user) => user.username === username);
}

export function getPostsForUser(username: string): Post[] {
return posts.filter((post) => post.user.username === username);
}
export type User = {
id: number;
username: string;
firstName: string;
lastName: string;
avatar: string;
followers: number;
};

export type Media = {
id: number;
type: "image" | "video";
url: string;
width: number;
height: number;
};

export type Post = {
id: number;
user: User;
date: string;
content: string;
likes: number;
replies: number;
replyId?: number;
media?: Media;
};

const users: User[] = [
{
id: 1,
username: "sam",
avatar:
"https://images.clerk.dev/uploaded/img_2UwOmQYFLO3AhjoORmTygZ7OM8Y.png",
firstName: "saM",
lastName: "saM",
followers: 100,
},
];

const posts: Post[] = [
{
id: 1,
user: users[1],
date: "2024-01-01T12:00:00.000Z",
content:
"Just some content to get us started. This is a post with some content. It's not very interesting, but it's a post.",
likes: 10,
replies: 0,
},
{
id: 1,
user: users[1],
date: "2024-01-01T12:00:00.000Z",
content: "This one is slightly more interesting. It has an image.",
likes: 10,
replies: 0,
media: {
id: 1,
type: "image",
url: "https://picsum.photos/seed/picsum/200/300",
width: 200,
height: 300,
},
},
];

export function getPosts(): Post[] {
return posts.filter((post) => !post.replyId);
}

export function getPost(id: number): Post | undefined {
return posts.find((post) => post.id === id);
}

export function getPostResponses(id: number): Post[] {
return posts.filter((post) => post.replyId === id);
}

export function getUser(username: string): User | undefined {
return users.find((user) => user.username === username);
}

export function getPostsForUser(username: string): Post[] {
return posts.filter((post) => post.user.username === username);
}

CSS

We already know our project is setup to use Tailwind. Style your project to match the look and feel of x.com or threads.net or https://threads-1xzzkryfs-sa-m.vercel.app

Use a mixture of AI and the https://tailwindcss.com/docs to figure out how to style your project.