Day Four
Building the Profile Page & Global Layout
Section titled “Building the Profile Page & Global Layout”Welcome to Day Four! Today, we’ll refactor our frontend to use a more powerful and scalable architecture. We will create a global “app shell” with our main navigation, and then build the primary user view: a /profile page that displays a grid of posts. This structure is much closer to a real-world application.
Milestones ✅
Section titled “Milestones ✅”1. API Service and Client-Side Schemas
Section titled “1. API Service and Client-Side Schemas”Before we build the UI, we need the tools to communicate with our backend and validate the data we receive.
-
Create an Axios instance for API calls
Terminal window mkdir -p app/servicestouch app/services/api.tsapp/services/api.ts import axios from "axios";// We define the base URL of our backend API.export const api = axios.create({baseURL: "http://localhost:3000", // Your Fastify backend address}); -
Define the Post Schema with Zod
Terminal window mkdir -p app/schemastouch app/schemas/post.schema.tsapp/schemas/post.schema.ts import { z } from "zod";// Zod schema for a single post objectexport const postSchema = z.object({id: z.number(),img_url: z.string().url(),caption: z.string().nullable(),created_at: z.string(),});// Zod schema for an array of postsexport const postsSchema = z.array(postSchema);// We infer the TypeScript type from the Zod schema.export type Post = z.infer<typeof postSchema>;
2. Create Reusable UI Components
Section titled “2. Create Reusable UI Components”Next, we’ll create the core, reusable visual pieces of our application.
-
Create the
HeadercomponentTerminal window mkdir -p app/componentstouch app/components/Header.tsxapp/components/Header.tsx export function Header() {return (<header className="sticky top-0 z-50 w-full border-b bg-white"><nav className="container mx-auto flex items-center justify-between px-4 py-3"><h1 className="text-xl font-bold">Instagram</h1><div className="text-xl">❤️</div></nav></header>);} -
Create the
BottomNavcomponent with LinksTerminal window touch app/components/BottomNav.tsxapp/components/BottomNav.tsx import { Link } from "react-router";export function BottomNav() {return (<footer className="fixed bottom-0 left-0 z-50 w-full h-16 bg-white border-t"><div className="grid h-full max-w-lg grid-cols-5 mx-auto font-medium"><Linkto="/home"className="inline-flex flex-col items-center justify-center px-5">🏠</Link><div className="inline-flex flex-col items-center justify-center px-5">🔍</div><Linkto="/home"className="inline-flex flex-col items-center justify-center px-5">➕</Link><Linkto="/"className="inline-flex flex-col items-center justify-center px-5">Reels</Link><Linkto="/profile"className="inline-flex flex-col items-center justify-center px-5">👤</Link></div></footer>);} -
Create the
PostCardcomponentTerminal window touch app/components/PostCard.tsxapp/components/PostCard.tsx import type { Post } from "~/schemas/post.schema";export function PostCard({ post }: { post: Post }) {return (<div className="w-full max-w-lg mx-auto rounded-lg overflow-hidden border bg-white mb-6"><div className="p-4"><p className="font-bold">webeet_user</p></div><imgsrc={post.img_url}alt={post.caption || "Instagram post"}className="w-full h-auto aspect-square object-cover"/><div className="p-4"><p><span className="font-bold mr-2">webeet_user</span>{post.caption}</p></div></div>);}
3. Creating the Global App Shell
Section titled “3. Creating the Global App Shell”Now we will modify our root layout to include the Header and BottomNav, making them appear on every page.
-
Update
app/root.tsxapp/root.tsx (Updated) import {isRouteErrorResponse,Links,Meta,Outlet,Scripts,ScrollRestoration,useRouteError,} from "react-router";import stylesheet from "./app.css?url";import { Header } from "./components/Header";import { BottomNav } from "./components/BottomNav";export function links() {return [{ rel: "stylesheet", href: stylesheet }];}export function Layout({ children }: { children: React.ReactNode }) {return (<html lang="en" className="min-h-screen"><head><meta charSet="utf-8" /><meta name="viewport" content="width=device-width, initial-scale=1" /><Meta /><Links /></head><body className="min-h-screen bg-gray-50 text-gray-800">{children}<ScrollRestoration /><Scripts /></body></html>);}export default function App() {return (<><Header /><main className="container mx-auto p-4"><Outlet /></main><BottomNav /></>);}// ... ErrorBoundary() function remains the same
4. Creating the Profile Page with Nested Routes
Section titled “4. Creating the Profile Page with Nested Routes”Finally, we’ll build the profile section, which consists of a layout route and a child route for our posts grid.
-
Create an Index Route to Redirect Users
Terminal window touch app/routes/_index.tsxapp/routes/_index.tsx import { redirect } from "react-router";export async function loader() {return redirect("/profile/posts/grid");} -
Create the Profile Layout Route
Terminal window touch app/routes/profile.tsxapp/routes/profile.tsx import { NavLink, Outlet } from "react-router";export default function ProfileLayout() {const activeLinkStyle = {borderBottom: "2px solid black",fontWeight: "bold",};return (<div><div className="flex justify-center items-center border-b mb-4"><NavLinkto="/profile/posts/grid"className="flex-1 text-center p-4"style={({ isActive }) => (isActive ? activeLinkStyle : undefined)}>Posts</NavLink><NavLinkto="/profile/reels/grid"className="flex-1 text-center p-4"style={({ isActive }) => (isActive ? activeLinkStyle : undefined)}>Reels</NavLink></div><main><Outlet /></main></div>);} -
Create the Posts Grid Route
Terminal window touch app/routes/profile.posts.grid.tsxapp/routes/profile.posts.grid.tsx import { useLoaderData } from "react-router";import { api } from "~/services/api";import { postsSchema, type Post } from "~/schemas/post.schema";import { PostCard } from "~/components/PostCard";export async function loader() {try {const response = await api.get("/posts");return postsSchema.parse(response.data);} catch (error) {console.error("Failed to load posts:", error);throw new Response("Could not load posts.", { status: 500 });}}export default function PostsGrid() {const posts = useLoaderData() as Post[];return (<div className="grid grid-cols-1 md:grid-cols-3 gap-4">{posts.map((post) => (<PostCard key={post.id} post={post} />))}</div>);}
Verification
Section titled “Verification”- Start your backend and frontend servers.
- Navigate to
http://localhost:5173/.
Conclusions
Section titled “Conclusions”Today you’ve implemented a professional frontend architecture.
- The App Shell Pattern: By moving the
HeaderandBottomNavtoroot.tsx, you’ve created a global layout. This is efficient and ensures a consistent look and feel across the entire application. - Nested Layouts & Routes: The
profile.tsxroute acts as a layout for a specific section of your app, providing shared UI like sub-navigation. Its children, likeprofile.posts.grid.tsx, render inside its<Outlet />. This is a powerful pattern for organizing complex applications. - Programmatic Redirects: Using a
loadertoredirectis a clean, server-side-friendly way to guide users to the correct starting page of your app. - Component-Based Architecture: You defined small, reusable pieces (
Header,PostCard) and then composed them together to build complex pages. This is the heart of the React development model.