Building Storefront Shell
Welcome to the Storefront Shell
Welcome to the first lesson of your storefront frontend journey. We are not starting from a blank screen here — the backend for this project already exists, and throughout the course you will gradually connect this frontend to the available API endpoints. That means your focus in this lesson is not server logic, database design, or backend architecture, but the frontend structure that gives the whole app a polished, scalable foundation.
In this lesson, you will build the application shell and the first customer-facing page. That includes setting up the global layout in src/app/layout.tsx, composing a reusable shell with shared navigation and footer, introducing low-level layout helpers such as PageContainer, and establishing a small set of reusable UI primitives. By the end, the app will already feel like a real storefront, even before live product data arrives in the next unit.
A big theme of this lesson is separation of responsibilities. The root layout is responsible for global page composition. The shell is responsible for persistent visual framing. Reusable UI components are responsible for consistent presentation. And the route file itself should stay very small, delegating actual page composition to a dedicated component. That separation will make the rest of the course much easier to build and understand.
Building the Global Composition Point
In a Next.js App Router project, src/app/layout.tsx is the global layout file. This is where you define the outer document structure and wrap all route content with shared application-wide pieces. In this lesson, that means importing the Tailwind stylesheet, setting base body styling, and composing AppProviders with AppShell so every route inherits the same structure automatically.
Here is the complete layout file:
-
The imports at the top show the three main concerns of this file: typing with React, visual composition through
AppShell, and application infrastructure throughAppProviders. The Tailwind stylesheet import is also placed here so the entire application has access to the same utility classes without individual pages needing to import styling manually. -
RootLayoutreceives achildrenprop typed asReactNode, which is React’s broad type for anything that can be rendered. In the App Router,childrenrepresents the active route content, so whatever page the user visits will be inserted into this layout automatically. -
The
<html lang="en">wrapper defines the document root and also provides useful semantic information for the browser, accessibility tools, and search engines. Even though this can feel boilerplate at first, this is part of what makes the layout the true global entry point of the app. -
The
<head>includes a Tailwind CDN script. In a production-grade setup, Tailwind is often fully compiled into the project pipeline, but in a learning environment like this, keeping it here helps ensure the styling system is available immediately and consistently. -
The
<body>classes create the baseline visual tone of the entire storefront.min-h-screenensures the app can stretch to the full viewport height, the background and text colors establish the neutral storefront palette, andantialiasedimproves text rendering so the UI feels cleaner and more polished. -
AppProvidersandAppShellare intentionally nested in that order because they solve different problems.AppProvidersis the infrastructure wrapper, whileAppShellis the visual frame. Keeping those roles separate makes the code easier to reason about and prevents layout concerns from leaking into provider setup. -
Wrapping
childrenwithAppShellmeans every route automatically gets the same shared navigation, main content framing, and footer. This is one of the biggest benefits of a proper shell: once it exists, new pages inherit the structure without repeating wrapper code in every route file.
This file is important because it acts as the global composition point, not the place where all visual details should live. It decides how the application is assembled at the highest level, then delegates the visual framing to reusable components.
Keeping Providers Lightweight and Focused
The layout file uses AppProviders, so it is worth understanding why that wrapper exists even though it currently looks very small. The file src/components/providers/AppProviders.tsx is deliberately lightweight because providers are infrastructure, not the focus of this unit.
-
The
'use client'directive marks this component as a client component, which is important because many real-world provider setups depend on client-side React features such as context, state, or effects. Even though this particular version is simple, the file is being prepared to grow later without forcing you to change the layout structure. -
The component receives
childrenand simply returns them inside a React fragment. This means it adds no visual structure and no extra DOM wrapper, which is exactly what we want here because providers should not accidentally become layout containers. -
The key architectural lesson is that provider setup should not become a dumping ground for shell or page logic. By keeping it minimal now, the codebase stays easier to scale later when things like cart state, query clients, or theme context are added.
Framing Every Page with AppShell
Now that the global layout composes the app, the next piece is the reusable shell itself. The file src/components/layout/AppShell.tsx is responsible for the persistent page frame: navigation at the top, page content in the middle, and footer at the bottom.
-
This component accepts
children, which represent the route-specific content inserted from the layout. That makes the shell reusable across the whole application, since it does not care which page is active — it only cares about framing that page consistently. -
NavbarandFooterare rendered in a fixed order around the page content. This is what makes the shell valuable: it centralizes persistent UI once, instead of asking every page to re-render shared scaffolding. -
The use of a semantic
<main>tag is important. It separates the primary page content from the persistent shell elements, which improves both accessibility and clarity. Later route files can stay tiny because the shell has already handled the outer structure. -
The outer
<div>applies the same background and text palette used by the root layout, reinforcing the storefront’s consistent visual language. Even though the body also has baseline styling, repeating critical shell styling here makes the shell self-contained and visually dependable.
This file is a good example of a reusable visual frame. It is not the global document root — that is still layout.tsx — but it is the reusable UI structure that all pages live inside.
Solving Shared Width and Padding Once
Before diving into the visible components, it helps to understand the quiet utility that keeps the layout consistent: src/components/layout/PageContainer.tsx. Its role is not to be flashy. Its job is to solve max-width and responsive horizontal padding once so other components do not each invent their own container classes.
-
The component accepts both
childrenand an optionalclassName. This is a very common React pattern for low-level layout components: provide stable baseline styling, then allow consumers to add local flex, grid, spacing, or alignment classes without replacing the core container behavior. -
The base class list defines the important layout rules for the whole app.
mx-autocenters the container,w-fulllets it expand naturally,max-w-7xllimits the content width on large screens, and the responsivepx-*classes ensure content never touches the screen edge. -
clsxis used to merge the base container classes with any additional classes passed in from the parent. That keeps the component flexible while still protecting the shared max-width and padding logic. -
This kind of component often looks small, but it has a big architectural payoff. Once
PageContainerexists, the navbar, footer, hero section, and future pages can all share the same horizontal rhythm, which makes the whole app feel more intentional and consistent.
Creating Early Navigation That Feels Intentional
The file src/components/layout/Navbar.tsx is one of the key visible pieces of the storefront shell. It defines the brand area and the small early navigation for the routes that actually exist right now. This is not meant to be a final full sitemap — it is intentionally focused on the pages learners can use in this unit.
Here is the complete navbar:
-
The
'use client'directive is required here becauseusePathname()is a client-side navigation hook. In other words, this component needs access to the current URL path on the client so it can style the active route correctly. -
navItemsis stored as a small configuration array rather than hardcoding separate links directly in the JSX. This makes the component easier to extend later and keeps the navigation structure readable and declarative. -
usePathname()returns the current pathname, such as/or/shop. This is the core piece of information the navbar needs to determine which link should appear active. -
The
activelogic is written carefully so the root route and grouped routes both behave correctly. For/, the match must be exact. For a route like/shop, we also want future nested paths such as/shop/somethingto still count as active, which is whypathname.startsWith(item.href)is used when the item is not the root route. -
The outer
<header>usessticky top-0so navigation remains visible as users scroll. That small detail has a strong UX effect because it keeps the app feeling anchored and makes it easier to move around once more routes are added. -
PageContaineris used inside the navbar so the nav width and horizontal padding match the rest of the application. This is one of the main architectural wins of the shell: the navbar does not have to reinvent layout spacing because the shared container already solves it. -
The brand area combines a circular visual mark with the storefront title and subtitle. It is more than decoration — it establishes identity and gives the top of the app a deliberate, finished look rather than feeling like temporary scaffolding.
-
The link styling uses
clsxto switch between active and inactive visual states. The active route gets a filled dark pill, while inactive links remain lighter and only become more prominent on hover. This gives users a clear sense of location without overcomplicating the component.
A strong navbar does not just help users navigate. It also communicates that the app already has structure and direction, even at this early stage.
Reinforcing the Shell with a Supportive Footer
The footer in src/components/layout/Footer.tsx is intentionally simple, but it still plays an important role. It reinforces the brand, echoes the same early route list as the navbar, and helps the shell feel complete instead of abruptly ending after page content.
Here is the full footer component:
-
The footer imports
Linkfor route navigation andPageContainerfor shared width and padding. Just like the navbar, it benefits from the container component instead of redefining its own layout rules. -
The outer
<footer>uses a top border and white background to visually separate it from the main page area. That contrast helps the footer feel like a distinct but related section of the shell. -
The internal grid splits the footer into two areas: a brand/message column and a small navigation column. This gives the footer structure without making it overly busy or heavy for an early-stage storefront.
-
The first column reinforces the product identity through the storefront name and a concise description. Even small copy like this matters because it supports the impression that the app is already a real product space, not just a collection of disconnected exercises.
-
The second column echoes the same available routes exposed in the navbar. That consistency is important — the shell should feel coordinated, and the footer should support the navigation story instead of introducing links to pages that do not exist yet.
A good footer at this stage is not about stuffing in extra content. It is about making the shell feel intentional and complete.
Designing a Reusable Button Primitive
One of the first shared UI primitives in the project is src/components/ui/Button.tsx. Instead of repeating long strings of utility classes every time you need a button, this component centralizes common button behavior and styling. That makes the UI easier to read, easier to maintain, and less likely to drift visually as more pages are added.
Here is the full file:
-
ButtonPropsextendsButtonHTMLAttributes<HTMLButtonElement>, which means the custom button still accepts normal HTML button props likeonClick,disabled,type, and others. This is a great example of building a reusable abstraction without losing the native button API. -
The custom props
variantandsizelet the component control appearance through a small, readable interface. Instead of memorizing utility class combinations all over the app, consumers can simply write things likevariant="secondary"orsize="lg". -
forwardRefis used so parent components can still pass a ref through to the underlying<button>element if needed. That is useful for advanced interactions, focus management, or integrations with other UI patterns later, and it makes the component more robust than a simple wrapper. -
The
baseclass string contains the shared interactive behavior every button should have. That includes rounded styling, centering, transition behavior, keyboard focus treatment, and disabled-state handling. Putting these in one place ensures that all buttons across the app feel consistently clickable and accessible. -
The
stylesobject maps eachvariantto a specific visual treatment. This makes the component easy to extend and easy to understand: the prop value directly selects the style family instead of requiring a long conditional block in the JSX. -
The
sizesobject does the same thing for button dimensions and text sizing. This keeps size differences predictable and standardized, which matters as the app grows and buttons appear in more contexts such as hero sections, cards, dialogs, or forms. -
In the returned JSX,
clsxmerges the base classes, the selected variant classes, the selected size classes, and any extraclassNamecoming from the consumer. This preserves flexibility without forcing every consumer to rebuild the entire button style system.
This component is a strong example of a shared primitive doing two jobs at once: reducing repeated markup now and protecting the UI from inconsistency later.
Standardizing Page Introductions with SectionHeading
The file src/components/ui/SectionHeading.tsx provides a reusable way to introduce sections of a page. Its job is to give you a consistent pattern for an eyebrow label, title, description, and optional action area. This becomes especially valuable as more pages are added and you want section intros to feel related without being identical.
Here is the full component:
-
The component accepts a combination of required and optional props.
titleis required because every section heading needs a clear main label, whileeyebrow,description, andactionare optional so the component can stay flexible across different page contexts. -
actionis typed asReactNode, which means it can hold anything renderable, such as a button, link, filter control, or another small UI element. That makes the component more future-proof because the heading can support interaction as well as text. -
The
alignprop allows the component to adapt between left-aligned and centered layouts. This is a small but useful form of flexibility because a shop-page heading and a centered marketing section might use the same structural component but need different alignment behavior. -
clsxis used to conditionally apply the centered alignment classes only whenalign === 'center'. This keeps the JSX readable and avoids duplicating the whole component structure for minor layout differences. -
Conditional rendering is used for
eyebrowanddescription, so those pieces only appear when they are actually passed in. That makes the component clean to consume because pages can provide only the pieces they need without leaving awkward empty space. -
The typography choices communicate hierarchy clearly: the eyebrow is small, uppercase, and high-tracking; the title is large and serif-based; the description is restrained and readable. This creates a shared presentation language that multiple sections can reuse across the app.
Components like this are not about clever logic. They are about protecting consistency and making page composition easier as the application expands.
Keeping the Home Route Tiny and Focused
In the App Router, the route file for the home page lives at src/app/page.tsx. One of the architectural goals in this project is to keep route files very small and move real page composition into dedicated components.
Here is the entire route file:
-
This file does exactly one thing: it maps the root route
/to theHomePageClientcomponent. That might seem almost too small, but that is actually a good sign. It means the route file is staying focused on routing instead of becoming a dumping ground for page markup. -
The
@alias keeps the import path clean and readable. In larger projects, path aliases make component organization much easier to follow because you do not have to count long chains of../../..segments. -
By delegating the actual page composition to
HomePageClient, the route remains easy to scan and maintain. This pattern becomes even more valuable later when routes need loaders, metadata, or route-specific logic without mixing that with large blocks of JSX.
Building the First Customer-Facing Page
The file src/components/home/HomePageClient.tsx is the first true storefront page learners encounter. It is responsible for composing the landing experience using the shell, the shared container, and the reusable button. Even though this page is mostly static for now, it still teaches important layout and composition skills.
Here is the full component:
-
This is marked with
'use client', which means it is rendered as a client component. Even though the current version is mostly presentational, keeping this page component on the client side leaves room for future interactive behavior without changing its basic role in the app. -
PageContaineris used here to give the home page the same max-width and horizontal padding as the navbar and footer. This is exactly why low-level layout components are so valuable: they quietly create consistency across very different parts of the UI. -
The main section uses a responsive grid so the landing page can present two coordinated areas: a strong content column on the left and a supporting visual card on the right. On smaller screens, the layout naturally stacks, while on large screens it becomes a two-column hero.
-
The left column establishes a clear content hierarchy through eyebrow text, a prominent
h1, supporting copy, and a call-to-action. This is a common pattern in marketing-oriented hero sections because it gives the user orientation, confidence, and a clear next step. -
The CTA uses a
Linkaround the sharedButtoncomponent to navigate to/shop. That is an important distinction: the navigation behavior comes from Next.jsLink, while the visual appearance and button styling come from the sharedButtonprimitive. -
The right-side card previews the customer journey in small steps: finding a product, opening a detail page, and eventually moving into cart and checkout work. This matters because it hints at the overall direction of the course and tells learners that the storefront structure they are building now will support richer customer flows later.
-
The visual styling of the card uses layered rounded surfaces, borders, and shadows to create depth without overwhelming the page. Since this is the first customer-facing screen, it sets the tone for the rest of the storefront and shows that even static pages can feel polished and intentional.
This component is a strong example of separating route ownership from page composition. The route file stays tiny, while the real UI lives in a dedicated page component designed for readability and reuse.
Preparing the Shop Page for the Next Unit
Even though the open files are the main focus of this lesson, the shell also supports another early route: src/app/shop/page.tsx. This page is intentionally a placeholder right now, but it still uses shared primitives and reinforces the project’s structure.
-
This page shows why the shell and shared primitives matter. Even before live data arrives, the page already feels structurally consistent because it inherits the navbar and footer from the shell, uses
PageContainerfor layout width, and relies on shared UI components instead of ad hoc markup. -
SectionHeadingis already doing real work here by introducing the page with a consistent eyebrow, title, and description. That demonstrates the value of building primitives before dynamic screens are added. -
The placeholder content is not wasted effort. It gives learners a place to verify that the shell, spacing, active nav states, and shared UI all feel correct before layering in real product fetching next.
Establishing Reusable Async State Components
This lesson also introduces small reusable UI building blocks for states that product-driven screens will need later: empty, loading, and error states. Even though they are not yet driving live catalog pages, preparing them now keeps future work more organized and visually consistent.
First, here is src/components/ui/EmptyState.tsx:
-
EmptyStateprovides a standard way to communicate that there is simply nothing to display, which is different from an error or a loading scenario. That distinction matters because the user experience should clearly reflect whether data is absent, still being fetched, or failed to load. -
The optional
actionprop allows the component to include a follow-up button or link when appropriate. This makes the component flexible enough to support different empty-state situations without changing its interface.
Next, here is src/components/ui/LoadingState.tsx:
-
This component uses lightweight skeleton-style shapes to suggest loading content without forcing the user to stare at a blank page. That makes async pages feel more alive and gives better visual feedback while data is being fetched.
-
The optional
messageprop keeps the component reusable across contexts. A product list, an order details page, and a customer dashboard might all want slightly different messaging while still sharing the same loading presentation.
Finally, here is src/components/ui/ErrorState.tsx:
-
ErrorStateseparates error presentation from page-specific logic. That is valuable because many routes will eventually need to surface failures, but they should not each invent different wording patterns, layouts, or retry buttons. -
The optional
onRetrycallback controls whether the retry button appears. This is a nice example of a predictable component interface: if retry behavior exists, the button shows up automatically; if not, the component still works as a pure error display. -
Reusing the shared
Buttoncomponent here keeps even state UIs visually aligned with the rest of the storefront. This reinforces the broader lesson that shared primitives reduce both code repetition and visual drift.
Supporting Display Consistency with Formatting Utilities
The storefront will eventually display money, timestamps, statuses, inventory messages, and order actions. The file src/lib/utils/format.ts prepares those concerns in a central place so individual pages do not each have to solve them on their own.
-
formatMoney()converts cents into a properly formatted currency string. This is especially useful because backend systems often store money as integer cents for correctness, but the UI needs a human-friendly value like$12.99. -
formatDateTime()standardizes date formatting in one place. That avoids each screen inventing its own date display style and keeps customer-facing timestamps consistent. -
formatTaxRate()turns basis points into a percentage string. This is a small but useful example of transforming backend-friendly data into presentation-friendly data. -
getInventoryLabel()encodes business-oriented display rules for product availability. Instead of scattering checks like “out of stock” or “only 3 left” across multiple components, the app can use one shared helper and know the wording stays consistent. -
titleCaseStatus()converts underscore-based status strings into readable labels. This becomes especially useful when backend values likepending_paymentorarchived_itemneed to appear in the UI. -
canPayOrder()andcanCancelOrder()are tiny but important examples of shared state rules. Even though they are simple, moving these checks into a utility file makes future order-related screens easier to read and keeps behavioral decisions centralized.
Utilities like these help the UI layer stay focused on rendering instead of repeatedly transforming raw data.
Displaying Statuses with a Shared Badge
The last small piece worth highlighting is src/components/ui/StatusBadge.tsx, which prepares a reusable visual treatment for statuses that appear later in product and order screens.
-
The component accepts a raw
statusstring and then computes a color tone based on that value. This keeps status styling centralized so different screens do not create slightly different badge treatments for the same meaning. -
The fallback neutral tone is also important. It means the component can still render sensibly even when it receives an unexpected or less common status instead of failing or requiring every possible case up front.
-
titleCaseStatus(status)ensures that the text shown to the user is more readable than the raw status value. This is another good example of separating backend-facing values from polished UI presentation. -
The badge shape, spacing, uppercase text, and letter spacing create a consistent micro-pattern for statuses. Small visual primitives like this help the UI feel cohesive once more data-driven screens arrive.
Recap
In this lesson, you built the structural and visual foundation of the storefront. You set up src/app/layout.tsx as the global composition point, kept AppProviders intentionally lightweight, and used AppShell to frame every route with a shared navbar, semantic main area, and footer.
You also introduced the components that make the shell feel deliberate rather than improvised. PageContainer solves shared width and padding once. Navbar and Footer consume that container so they align with the rest of the app. Button and SectionHeading establish reusable UI patterns. HomePageClient becomes the first real customer-facing page, while the shop placeholder proves that the shell already supports additional routes cleanly.
Finally, you prepared the project for later dynamic pages by introducing empty, loading, and error state components, as well as shared formatting utilities and a reusable status badge. Even though the live catalog is not connected yet, the app now has a strong frontend foundation. In the next unit, that foundation will matter immediately, because instead of first solving layout and consistency problems, you will be able to focus on bringing real product data into an already polished storefront.
