Project scaffold
In this section, we will explore the project scaffold. We'll check how to interact with the Rell dapp while focusing on the layout and design of the pages.
Overview
Our dapp will comprise three main pages:
- Home page - This page will display the news feed.
- New post page - Users can create a new post for their feed here.
- Users page - This page will list all users in the system and enable follow/unfollow capabilities.
Basic layout
Let's establish a foundational layout structure. In src/app/layout.tsx, the {children} element will sit within a main div, serving as the base 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>
);
}
This layout ensures a clean and responsive design while maintaining consistency across all pages.
Navigation
A navigation component enables easy movement between our pages.
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>
);
}
You can verify its functionality by running the app; clicking these links will update the URL accordingly.
$ npm run dev
News feed (home) page
Let’s explore the 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>
);
}
New post page
The new post page allows users to create a post for the news feed. It includes a free text field and a button to handle requests.
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>
);
}
Here’s a breakdown of each step:
Step 1: Initialize state variables
- Start by initializing essential state variables:
router: Utilize Next.js's router to manage in-app navigation.isLoading: Track whether the component is currently loading to ensure a smooth user experience.content: Hold the user's input for the text content of the post.
Step 2: Handle text area content changes
- Implement the
handleContentChangefunction to respond to user input in the text area. This function updates thecontentstate with the text entered by the user.
Step 3: Handle form submission
- Create an asynchronous
onSubmitfunction to manage content submission. While the current example simulates content submission usingrouter.push('/'), you should replace this placeholder with your content submission mechanism, such as API requests.
Step 4: Submit content
- In this step, we temporarily use
router.push('/')to simulate content submission. Once your app connects to a backend server, replace this with your actual content submission logic.
Step 5: Reset state and loading indicator
- After submitting content—whether successful or not—the
onSubmitfunction resets the component’s state, clearing thecontentfield and settingisLoadingback tofalse.
Rendering the NewPost component
- The
NewPostcomponent is well-organized, featuring:- A text area for users to input their post content.
- A button for submitting the post.
- The button's appearance and behavior change dynamically based on the
isLoadingstate, providing visual feedback to users.
To use the component, include it in your page as follows:
import NewPost from "@/components/NewPost";
export default function NewPostPage() {
return <NewPost />;
}
User list page
The UserItem component
To effectively represent individual users, we created the UserItem component. This component encapsulates the visual representation of each user, including their name and options 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 examine how it works:
Step 1: Initialize state variables
- Initialize two critical state variables:
isLoading: Track whether any user-related operation (like following or unfollowing) is in progress.isFollowing: Indicate whether the currently logged-in user follows the displayed user, providing immediate feedback about their following status.
Step 2: Handle follow/unfollow interaction
- The
handleFollowClickfunction manages follow/unfollow interactions. Trigger this function when the user clicks the follow/unfollow button. SetisLoadingto true to indicate that the operation is underway. Remember to replace this placeholder logic with real interactions with your backend or database.
Step 3: Update follow status
- Within the
handleFollowClickfunction, log the follow/unfollow action temporarily. Replace this logic with actual data interactions.
Rendering the UserItem component
- The
UserItemcomponent displays:- A user avatar or image: A circular element shows the user’s initials for visual identification.
- A user name: Display the user’s name prominently next to the avatar.
- A follow/unfollow button: This button lets users follow or unfollow the displayed user. Its appearance and behavior adapt based on the
isLoadingandisFollowingstate variables, providing immediate feedback.
The UsersList component
To aggregate and display multiple user items, we created the UsersList component. This component uses the UserItem component to dynamically render a list of users.
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 works:
- We define the structure of user data using the
GetUsersReturnTypetype, which reflects the format of user data retrieved from the backend or database. - In our example, we provide a placeholder array of users, which you should replace with the actual data obtained from your system.
- The component maps through the user array and renders the
UserItemcomponent for each user in the list. We use horizontal lines to separate each user item for better 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 set up to handle user interactions and foster engagement effectively.