Project scaffold
In this section, we will add a scaffold to the project. We will mock all interactions with the Rell dapp and focus on the page's layout and design.
Overview
The design of our dapp would consist of three pages
- Home page - News feed
- New post page - Create a new post on your feed
- Users page - List of users in the system with follow/unfollow capability
Set up the basic layout
Let's kickstart our app by establishing a fundamental layout structure. In src/app/layout.tsx
, we will encase the {children}
element within a main div that will serve as the foundation for all our pages.
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="en">
<body className={inter.className}>
<main className="flex min-h-screen justify-center bg-gray-100 text-black">
<div className="container">{children}</div>
</main>
</body>
</html>
);
}
To ensure consistency across our pages, we begin by clearing out the code in src/app/page.tsx
:
"use client";
export default function Home() {
return <></>;
}
Following this, we create additional pages by generating src/app/new-post/page.tsx
and src/app/users/page.tsx
. In these new files, we initialize them with empty divs:
"use client";
export default function NewPostPage() {
return <></>;
}
To guarantee that all our components are executed on the client side, we include the directive "use client";
at the beginning of each file.
Add navigation
Our next step is to enhance navigation by introducing a navigation component that allows easy movement between our pages. We'll achieve this by creating a new component in src/components/NavBar.tsx
:
import Link from "next/link";
export default function NavBar() {
return (
<nav className="py-2 px-6">
<ul className="flex justify-between">
<li>
<Link href="/" className="text-xl font-extrabold">
News feed dapp
</Link>
</li>
<div className="flex gap-4 font-semibold">
<li>
<Link href="/new-post" className="hover:text-gray-600">
New Post
</Link>
</li>
<li>
<Link href="/users" className="hover:text-gray-600">
Users
</Link>
</li>
<li>
<Link href="/" className="hover:text-gray-600">
Feed
</Link>
</li>
</div>
</ul>
</nav>
);
}
This navigation bar comprises a title in the upper-left corner that leads to the home page and three clickable links: "New Post," "Users," and "Feed," each corresponding to their respective pages. To integrate this navigation component into our layout, open layout.tsx
:
<div className="container">
<NavBar />
{children}
</div>
By including the NavBar
component within our layout, we ensure that navigation is available on all pages. Add the import import NavBar from "@/components/NavBar";
to the file.
You can verify the capability by running the app; clicking these links will update the URL accordingly.
$ npm run dev
Enhance the news feed (home) page
Let's improve the news feed page by adding more context and structure. We'll create a component in src/components/NewsFeed.tsx
:
export default function NewsFeed() {
return (
<div className="p-4 md:p-8">
<div className="flex justify-between items-center">
<h1 className="text-2xl font-bold">User name</h1>
<div className="flex text-center">
{/* Followers Box */}
<div className="bg-white m-1 p-2 rounded-lg shadow">
<h3 className="text-lg font-semibold">Followers</h3>
<p className="text-3xl font-bold">0</p>
</div>
{/* Following Box */}
<div className="bg-white m-1 p-2 rounded-lg shadow">
<h3 className="text-lg font-semibold">Following</h3>
<p className="text-3xl font-bold">0</p>
</div>
</div>
</div>
{/* News Feed */}
<div className="bg-white p-4 rounded-lg shadow">
<ul>
<li key={0} className="mb-4">
<div className="flex">
<div className="font-semibold">User1</div>
<div className="text-gray-500 text-sm ml-2">
{new Date().toLocaleString()}
</div>
</div>
<div className="mt-2">Some content</div>
{/* Add a horizontal line between posts */}
{<hr className="my-4 border-t border-gray-300" />}
</li>
</ul>
</div>
</div>
);
}
We've enhanced the news feed page to include:
- User information with the user's name, followers count, and following count.
- Improved styling for the followers and following boxes.
- A sample post with a user profile picture, username, timestamp, and content.
You can now include this component in the home page src/app/page.tsx
:
import NewsFeed from "@/components/NewsFeed";
export default function Home() {
return <NewsFeed />;
}
This improved news feed page provides a better user experience and serves as a foundation for displaying posts and user interactions.
New post page
A page that creates a new post to the news feed requires a free text field and a button to handle the requests. We create the following component src/components/NewPost.tsx
. This component facilitates the creation of new posts and features a user-friendly interface.
import { useRouter } from "next/navigation";
import { useState } from "react";
export default function NewPost() {
// Step 1: Initialize state variables
const router = useRouter();
const [isLoading, setIsLoading] = useState(false);
const [content, setContent] = useState("");
// Step 2: Handle text area content change
const handleContentChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
setContent(e.target.value);
};
// Step 3: Handle form submission
const onSubmit = async (data: string) => {
try {
if (data.trim() !== "") {
setIsLoading(true);
// Step 4: Content submission (will be replaced later)
router.push("/");
}
} catch (error) {
console.error(error);
} finally {
// Step 5: Reset state and loading indicator
setContent("");
setIsLoading(false);
}
};
// Render the component
return (
<div className="p-6">
<textarea
className="w-full p-2 border rounded"
rows={4}
placeholder="Write your post..."
value={content}
onChange={handleContentChange}
/>
<button
className={`${
isLoading ? "bg-gray-500" : "bg-blue-500 hover:bg-blue-600"
} w-32 hover:cursor-pointer text-white font-bold py-2 px-4 rounded float-right`}
onClick={() => onSubmit(content)}
disabled={isLoading}
>
{isLoading ? "Posting..." : "Post"}
</button>
</div>
);
}
Let's break down what each step does:
Step 1: Initialize state variables
- We begin by initializing essential state variables:
router
: We use Next.js's router to manage navigation within the app.isLoading
: This variable tracks whether the component is currently in a loading state, ensuring a smooth user experience.content
: This state variable holds the user's input, the text content of the post they want to create.
Step 2: Handle text area content changes
- The
handleContentChange
function responds to user input in the text area. It updates thecontent
state with the text entered by the user.
Step 3: Handle form submission
- We've implemented an asynchronous
onSubmit
function to manage the content submission process. While our current example simulates content submission withrouter.push('/')
, you should replace this placeholder logic with your content submission mechanism, such as API requests.
Step 4: Submit content
- In this step, we've used
router.push('/')
as a temporary method to simulate content submission. Once your app integrates with a backend server, remember to replace this with your actual content submission logic.
Step 5: Reset state and loading indicator
- After the content submission is either successful or encounters an error, the
onSubmit
function ensures that the component's state is appropriately reset, including clearing thecontent
field and settingisLoading
back tofalse
.
Rendering the Component
- The
NewPost
component is neatly organized, featuring:- A text area where users can input their post content.
- A button for submitting the post.
- The appearance and behavior of the button change dynamically based on the
isLoading
state, providing visual feedback to the user.
To use the component, we include it in our page as follows:
import NewPost from "@/components/NewPost";
export default function NewPostPage() {
return <NewPost />;
}
User list page
The final page we create will consist of a list of all active users in the system and the ability to follow and unfollow them. We model a user according to the struct user_dto
we created and add a component representing a user item.
User item component
To represent individual users effectively, we create a component called UserItem
. This component encapsulates the visual representation of each user, including their name and the ability to follow or unfollow them.
import { useState } from "react";
export type UsersDto = {
name: string;
id: Buffer;
};
export default function UserItem({ user }: { user: UsersDto }) {
// Step 1: Initialize state variables
const [isLoading, setIsLoading] = useState<boolean>(false);
const [isFollowing, setIsFollowing] = useState<boolean>(false);
// Step 2: Handle follow/unfollow click
const handleFollowClick = async (userId: Buffer, follow: boolean) => {
try {
setIsLoading(true);
// Step 3: Handle follow/unfollow logic (Will be replaced later)
console.log("Following " + userId.toString("hex") + ": " + follow);
setIsFollowing(follow);
} catch (error) {
console.log(error);
} finally {
// Step 4: Reset the loading indicator
setIsLoading(false);
}
};
// Render the component
return (
<div className="flex justify-between mb-4">
<div className="flex items-center">
{/* User Avatar or Image */}
<div className="w-10 h-10 bg-gray-300 rounded-full mr-4 flex justify-center items-center">
{user.name[0]}
</div>
<span className="text-lg font-semibold">{user.name}</span>
</div>
<button
className={`${
isLoading ? "bg-gray-500" : "bg-blue-500 hover:bg-blue-600"
} w-32 hover:cursor-pointer text-white font-bold py-2 px-4 rounded float-right`}
disabled={isLoading}
onClick={() => handleFollowClick(user.id, !isFollowing)}
>
{isLoading ? "Loading..." : isFollowing ? "Following" : "Follow"}
</button>
</div>
);
}
Let's explore how it works:
Step 1: Initializing State Variables
- We begin by initializing two critical state variables:
isLoading
: This variable tracks whether any operation related to the user (such as following or unfollowing) is currently in progress.isFollowing
: It indicates whether the currently logged-in user is following the displayed user. This state variable provides immediate feedback about the user's following status.
Step 2: Handling Follow/Unfollow Interaction
- The
handleFollowClick
function manages follow-and-unfollow interactions. It is triggered when the user clicks the follow/unfollow button. It setsisLoading
to true to indicate that the operation is underway. Remember that the logic within this function is a placeholder and should be replaced with real interactions with your backend or database.
Step 3: Updating Follow Status
- Within the
handleFollowClick
function, we temporarily log information about the follow/unfollow action. You must replace this placeholder logic with actual interactions with your data.
Rendering the UserItem
component
- The
UserItem
component's visual representation is organized as follows:- User avatar or image: A circular element displaying the user's initials provides a visual identifier.
- User name: The user's name is displayed prominently next to the avatar.
- Follow/unfollow button: This button enables users to follow or unfollow the displayed user. Its appearance and behavior adapt based on the
isLoading
andisFollowing
state variables, providing immediate user feedback.
UsersList
component
To aggregate and display multiple user items, we create a component called UsersList
. This component utilizes the UserItem
component to render a list of users dynamically.
import UserItem, { UsersDto } from "./UserItem";
export type GetUsersReturnType = {
pointer: number;
users: UsersDto[];
};
export default function UsersList() {
// Define an example array of users
const users: GetUsersReturnType | undefined = {
pointer: 0,
users: [{ name: "User1", id: Buffer.from("AB", "hex") }],
};
return (
<div className="p-4 md:p-8">
<ul>
{users && users.users.length > 0 ? (
users.users.map((user, index) => (
<li key={user.id.toString()}>
{/* Render the UserItem component for each user */}
<UserItem user={user} />
{/* Add a horizontal line between user items */}
{index < users.users.length - 1 && (
<hr className="my-4 border-t border-gray-300" />
)}
</li>
))
) : (
<></>
)}
</ul>
</div>
);
}
Here's how it functions:
- We define the structure of user data using the
GetUsersReturnType
type. This type reflects the format of user data retrieved from the backend or database. - In our example, we've provided a placeholder array of users. You must replace this with the actual data obtained from your system.
- The component maps through the user array and renders the
UserItem
component for each user in the list. Horizontal lines separate each user item for clarity.
Finally, we include the component in the users
page:
import UsersList from "@/components/UserList";
export default function UsersPage() {
return <UsersList />;
}
With these components in place, your app is well-equipped to handle user interactions and facilitate user engagement effectively.