Build NextShop with Next.js

Build and deploy a Firestore-powered Next.js storefront with a shared cart.

Introduction

30 Second Summary

Shopping online feels effortless when every product opens cleanly. The experience falls apart as soon as the basket forgets a choice.

In this project, you will rebuild NextShop as a responsive Next.js storefront powered by live Cloud Firestore data. You will publish the finished shop to Vercel through GitHub.

What You'll Build

You'll share a dark storefront that carries a visitor from browsing live products to reviewing a calculated cart total.

By the end of this project, you'll have:

  • A responsive product catalog that keeps every card readable from one-column phone layouts to three-column desktop layouts.
  • A Firestore-backed shopping flow where /products/wireless-headphones opens the matching item. The cart shows quantity 2 after the same product is added twice.
  • A public storefront on Vercel connected to GitHub for new deployments. Visitors can browse the live catalog while Firestore blocks browser-side writes.
  • Secret Mission: Add category buttons that narrow the visible catalog using Firestore equality queries.

Are there any prerequisites?

Basic familiarity with JavaScript or React helps. Step 1 covers every required Windows tool plus account setup.

Before We Start

This is your moment to commit to rebuilding NextShop from scratch. A clear goal keeps each technical choice focused on a storefront where visitors can browse products and use a shopping cart.

Set Up the Next.js Project

NextShop needs a dependable local toolchain before its storefront can run. Windows PowerShell gives you one place to verify every command.

A pinned Node.js environment keeps the new Next.js scaffold consistent. This preserves the approachable JavaScript structure while replacing the outdated setup.

In This Step, Get Ready To:
  • Verify Node.js, npm, Visual Studio Code, and Git on Windows.
  • Prepare your GitHub, Firebase, and Vercel accounts.
  • Create and run the pinned NextShop scaffold.
Verify the Windows Toolchain

Node.js runs the development tools behind NextShop. The required LTS release also installs npm for managing the project's packages.

  • Press the Windows key to open the Windows search bar.
  • Type PowerShell and press Enter to open it.
  • Check the installed Node.js and npm versions by running these commands:
node -v
npm -v

What Do These Commands Check?

  • The first command prints your installed Node.js version.
  • The second command confirms that npm is available in the same PowerShell session.

✔️ I See the Required LTS Version

Node.js is ready when the first line shows v24.21.0. You should also see an npm version number on the second line.

ⓧ I See an Older or Different Version

Your scaffold needs the pinned Node.js v24.21.0 LTS release. Installing it aligns your local runtime with the rest of this project.

  • Download the Windows installer for Node.js v24.21.0 LTS from the official Node.js download page.
  • Run the downloaded installer.
  • Keep the default installation options.
  • Close PowerShell after the installation finishes.
  • Reopen PowerShell through the Windows search bar.
  • Verify Node.js and npm again by running these commands:
node -v
npm -v

What Should You See?

The first line should now show v24.21.0. The second line should contain an npm version number.

ⓧ Command Not Found

Node.js is missing or unavailable from your PowerShell path. The official Windows installer adds Node.js and npm together.

  • Download the Windows installer for Node.js v24.21.0 LTS from the official Node.js download page.
  • Run the downloaded installer.
  • Keep the default installation options.
  • Close PowerShell after the installation finishes.
  • Reopen PowerShell through the Windows search bar.
  • Verify Node.js and npm by running these commands:
node -v
npm -v

What Should You See?

The first line should show v24.21.0. The second line should contain an npm version number.

The remaining tools give you an editor for the scaffold and version control for deployment later.

  • Check whether the Visual Studio Code command line is available by running this command:
code --version

What Does This Command Check?

This prints the installed Visual Studio Code version. It also proves PowerShell can launch the editor from a project folder.

✔️ I See a Version Number

Visual Studio Code is installed and available from PowerShell. Keep this window open for the Git check.

ⓧ Command Not Found

The recommended Windows User Setup installs Visual Studio Code without requiring administrator permissions. Its default setup also adds the editor command to your path.

  • Download the User Setup from the official Visual Studio Code Windows guide.
  • Run the downloaded installer.
  • Keep the default setup options.
  • Close PowerShell after the installation finishes.
  • Reopen PowerShell through the Windows search bar.
  • Verify the editor command by running:
code --version

What Should You See?

You should see a Visual Studio Code version followed by its build details. This confirms the editor command is ready.

  • Check whether Git is available by running this command:
git --version

What Does This Command Check?

This prints the installed Git version. A version result confirms npm can access Git when a package or workflow requires it.

✔️ I See a Git Version

Git is installed and available from PowerShell. Your Windows development toolchain is ready.

ⓧ Command Not Found

Git needs to be installed before you can send NextShop to GitHub later. The official Windows installer configures the command-line tool.

  • Download Git from the official Git for Windows page.
  • Run the downloaded installer.
  • Keep the default setup options.
  • Close PowerShell after the installation finishes.
  • Reopen PowerShell through the Windows search bar.
  • Verify Git by running this command:
git --version

What Should You See?

You should see a line containing the installed Git version. This confirms PowerShell can find the Git executable.

Still Missing a Tool?

Close every PowerShell window after an installation. A new PowerShell session reloads the updated command path.

Revisit the outcome tab for the missing tool if its command still fails. Confirm that you used the official Windows installer.

Help me diagnose my Windows development tool setup.

Prepare Your Service Accounts

Your local shop later stores products with Firebase and publishes through Vercel. A GitHub account connects your local project to that deployment flow.

  • Use the official GitHub site to create an account or sign in.
  • Confirm the GitHub home page shows your signed-in profile.
  • Use the Firebase console to create an account or sign in.
  • Confirm the Firebase console opens under your account.
  • Use the official Vercel site to create an account or sign in.
  • Confirm the Vercel dashboard opens under your account.
Scaffold and Run NextShop

The scaffold creates the project structure and installs its starting dependencies in one pass. The pinned command selects JavaScript while preserving the planned App Router and src/ layout.

  • Return to the PowerShell window from the tool checks.
  • Move to your Desktop by running this command:
cd ~/Desktop

Why Start on the Desktop?

This sets a predictable location for the new folder. You can find nextshop-rebuilt from PowerShell or Windows File Explorer.

The first scaffold can take a few minutes while npm downloads the required packages. PowerShell may ask for permission to download the scaffolding package before it begins.

  • Create the pinned NextShop project by running this command:
npx create-next-app@16.3.4 nextshop-rebuilt --js --tailwind --eslint --app --src-dir --import-alias "@/*" --use-npm --yes

What Does This Command Create?

  • The pinned create-next-app version generates a Next.js 16.3.4 project named nextshop-rebuilt.
  • The scaffold uses JavaScript with Tailwind CSS and ESLint.
  • The App Router lives under src/app.
  • The @/* alias points imports toward the src/ directory.
  • The command uses npm and accepts the selected defaults automatically.

You should see output confirming that the app was created inside the nextshop-rebuilt folder.

Scaffold Command Failed?

Confirm the Node.js check shows v24.21.0. An older runtime can prevent the pinned scaffold from running.

Check your internet connection if npm cannot download packages. Remove any incomplete nextshop-rebuilt folder before trying the scaffold again.

Help me troubleshoot the Next.js scaffold command.

  • Enter the generated project folder and open it in Visual Studio Code by running these commands:
cd nextshop-rebuilt
code .

What Do These Commands Do?

The first command moves PowerShell into nextshop-rebuilt. The second opens that current folder as a Visual Studio Code workspace.

  • Confirm the Visual Studio Code file sidebar contains the src folder.
  • Confirm PowerShell now shows nextshop-rebuilt as the current folder.

Before you start the server, what do you expect the pinned scaffold to show in your browser?

  • Start the local development server by running this command:
npm run dev

What Does This Command Do?

The npm development script starts the Next.js server with Turbopack. PowerShell stays attached to the running process so it can display requests and errors.

Keep this PowerShell window running while you work. Press Ctrl+C when you need to stop the server.

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

You should see the current Next.js starter page. That page proves the pinned scaffold can compile and serve locally.

Starter Page Not Loading?

Check that PowerShell is still running the development server from the nextshop-rebuilt folder. Read the latest terminal output for a compilation or port error.

Confirm the browser address uses port 3000. Keep the PowerShell window open while loading the page.

Help me get the Next.js starter page running.

✔️ Awesome, I've Got Everything!

Your Next.js starter is running. Keep PowerShell and Visual Studio Code open for the next step.

ⓧ I'd Like to Double Check the Full Code

This setup step uses the generated scaffold. Compare your project state with this checklist:

  • The nextshop-rebuilt folder contains a JavaScript Next.js 16.3.4 project.
  • The scaffold includes Tailwind CSS, ESLint, the App Router, and a src/ directory.
  • The import alias is configured as @/*.
  • The local development server is running from PowerShell.
  • The starter page loads at http://localhost:3000.

Your pinned Next.js project is running from the exact structure the storefront needs. Next, you will replace the starter screen with the first visible version of NextShop.

Build the Static Storefront

Your local environment is ready. The Next.js development server already serves the starter page.

That generic starter does not look like the storefront you promised. A hardcoded catalog gives you a visible version to shape before backend setup begins.

In this step, get ready to:
  • Replace the starter homepage with a temporary hardcoded catalog.
  • Establish the dark NextShop color foundation.
  • Confirm the product grid responds to different browser widths.
Build the temporary product catalog

A local product array lets you shape the shopping experience without waiting for a database connection. Each object supplies the details displayed by one card.

  • In the VS Code Explorer sidebar, select src/app/page.js.
  • Replace its contents by pasting the code below:
const products = [
  // Keep this first catalog local so the storefront is visible immediately.
  { name: "Wireless Headphones", category: "Audio", description: "Immersive sound with all-day comfort.", price: 129.99, badge: "WH" },
  { name: "City Sneakers", category: "Footwear", description: "Everyday cushioning for busy streets.", price: 89, badge: "CS" },
  { name: "Travel Backpack", category: "Accessories", description: "Organized storage for quick getaways.", price: 119, badge: "TB" },
];

// Format every product price with the same US currency rules.
const currency = new Intl.NumberFormat("en-US", {
  style: "currency",
  currency: "USD",
});

export default function HomePage() {
  return (
    <div className="space-y-10">
      <section className="max-w-3xl space-y-4"><p className="text-sm font-bold uppercase tracking-[0.25em] text-cyan-300">Fresh finds, simple shopping</p><h1 className="text-4xl font-black tracking-tight text-white sm:text-6xl">Products worth making room for.</h1><p className="text-lg leading-8 text-slate-400">Browse a focused collection built for everyday life.</p></section>
      <section className="grid gap-6 sm:grid-cols-2 lg:grid-cols-3">
        {products.map((product) => (
          <article key={product.name} className="group overflow-hidden rounded-3xl border border-slate-800 bg-slate-900 transition hover:-translate-y-1 hover:border-cyan-400/60">
            <div className="flex aspect-[4/3] items-center justify-center bg-gradient-to-br from-cyan-500/20 to-violet-500/20 text-5xl font-black text-cyan-100">{product.badge}</div>
            <div className="space-y-3 p-5"><p className="text-xs font-bold uppercase tracking-[0.2em] text-cyan-300">{product.category}</p><h2 className="text-xl font-bold text-white">{product.name}</h2><p className="text-sm leading-6 text-slate-400">{product.description}</p><p className="text-lg font-black text-white">{currency.format(product.price)}</p></div>
          </article>
        ))}
      </section>
    </div>
  );
}

What does this code do?

  • The products array holds three temporary product records. This keeps the visual build independent from the future data source.
  • The currency formatter converts each numeric price into a consistent USD display.
  • The Tailwind CSS classes arrange the cards in a responsive grid. They also style each card with dark surfaces plus cyan accents.
  • Save src/app/page.js.
  • Refresh the browser page.

You should see a NextShop heading above three product cards. The cards show Wireless Headphones, City Sneakers, and Travel Backpack.

That is your first storefront view working. Each product now has a visible badge plus a formatted USD price.

Still seeing the starter page?

Confirm that you replaced every starter line in src/app/page.js. A leftover import can prevent the new component from compiling.

If the browser cannot connect, switch back to the PowerShell window from earlier. Restart the development server with the same command from the previous step.

Help me debug my static NextShop homepage.

✔️ Awesome, I've got everything!

Great. Double check that you saved src/app/page.js.

ⓧ I'd like to double check the full code

const products = [
  // Keep this first catalog local so the storefront is visible immediately.
  { name: "Wireless Headphones", category: "Audio", description: "Immersive sound with all-day comfort.", price: 129.99, badge: "WH" },
  { name: "City Sneakers", category: "Footwear", description: "Everyday cushioning for busy streets.", price: 89, badge: "CS" },
  { name: "Travel Backpack", category: "Accessories", description: "Organized storage for quick getaways.", price: 119, badge: "TB" },
];

// Format every product price with the same US currency rules.
const currency = new Intl.NumberFormat("en-US", {
  style: "currency",
  currency: "USD",
});

export default function HomePage() {
  return (
    <div className="space-y-10">
      <section className="max-w-3xl space-y-4"><p className="text-sm font-bold uppercase tracking-[0.25em] text-cyan-300">Fresh finds, simple shopping</p><h1 className="text-4xl font-black tracking-tight text-white sm:text-6xl">Products worth making room for.</h1><p className="text-lg leading-8 text-slate-400">Browse a focused collection built for everyday life.</p></section>
      <section className="grid gap-6 sm:grid-cols-2 lg:grid-cols-3">
        {products.map((product) => (
          <article key={product.name} className="group overflow-hidden rounded-3xl border border-slate-800 bg-slate-900 transition hover:-translate-y-1 hover:border-cyan-400/60">
            <div className="flex aspect-[4/3] items-center justify-center bg-gradient-to-br from-cyan-500/20 to-violet-500/20 text-5xl font-black text-cyan-100">{product.badge}</div>
            <div className="space-y-3 p-5"><p className="text-xs font-bold uppercase tracking-[0.2em] text-cyan-300">{product.category}</p><h2 className="text-xl font-bold text-white">{product.name}</h2><p className="text-sm leading-6 text-slate-400">{product.description}</p><p className="text-lg font-black text-white">{currency.format(product.price)}</p></div>
          </article>
        ))}
      </section>
    </div>
  );
}
Create the dark color foundation

The card classes already define the main storefront surfaces. The global stylesheet now gives the entire page a matching dark canvas.

  • In the VS Code Explorer sidebar, select src/app/globals.css.
  • Replace its contents by pasting the code below:
@import "tailwindcss";

:root {
  color-scheme: dark;
  --background: #07111f;
  --foreground: #e2e8f0;
}

* {
  box-sizing: border-box;
}

html {
  min-width: 320px;
  background: var(--background);
}

body {
  min-height: 100vh;
  margin: 0;
  background:
    radial-gradient(circle at top left, rgba(34, 211, 238, 0.1), transparent 30rem),
    var(--background);
  color: var(--foreground);
  font-family: Arial, Helvetica, sans-serif;
}

button,
a {
  cursor: pointer;
}

What does this stylesheet do?

  • The Tailwind import keeps the utility classes in src/app/page.js available.
  • The shared color values establish a dark navy background. They also establish a light foreground color.
  • The radial gradient adds a cyan glow near the top corner. The solid background remains visible across the rest of the page.
  • The global sizing rule keeps element dimensions predictable across the storefront.
  • Save src/app/globals.css.
  • Refresh the browser page.

You should see a dark navy page with a soft cyan glow near the top corner. The light text remains readable against each dark product card.

Missing the dark background?

Confirm that the Tailwind import remains the first line of src/app/globals.css. Check that you edited the file inside src/app.

Save the stylesheet again if VS Code still shows its unsaved-file indicator.

Help me fix the NextShop global styles.

✔️ Awesome, I've got everything!

The dark foundation is in place. Keep the development server running for the responsive check.

ⓧ I'd like to double check the full code

@import "tailwindcss";

:root {
  color-scheme: dark;
  --background: #07111f;
  --foreground: #e2e8f0;
}

* {
  box-sizing: border-box;
}

html {
  min-width: 320px;
  background: var(--background);
}

body {
  min-height: 100vh;
  margin: 0;
  background:
    radial-gradient(circle at top left, rgba(34, 211, 238, 0.1), transparent 30rem),
    var(--background);
  color: var(--foreground);
  font-family: Arial, Helvetica, sans-serif;
}

button,
a {
  cursor: pointer;
}
Test the responsive catalog

A responsive grid changes its column count as space becomes available. Each product card keeps the same content while the surrounding layout adapts.

Before you resize the browser, how many columns do you expect at narrow, medium, and wide widths?

  • Switch back to the browser tab showing http://localhost:3000.
  • Narrow the browser window to a phone-like width.

You should see the three cards arranged in one column. Every card remains fully readable.

  • Widen the browser window to a medium width.

You should see the catalog change to two columns. The third card moves onto the next row.

  • Expand the browser window to a wide desktop width.

You should see all three cards in one row. Each card displays a badge plus its product name.

The category appears above the description. A formatted USD price appears at the bottom.

That is your first complete NextShop win. The catalog stays readable from one column through three columns.

Cards staying in one column?

Confirm that the product grid classes are still attached to the section containing the mapped cards. Check that src/app/globals.css begins with the Tailwind import.

Make the browser substantially wider before checking the desktop layout. A narrow editor beside the browser can reduce the available viewport.

Help me debug the responsive product grid.

Your static storefront now gives the project a clear visual target. Next, you will replace the local product array with documents loaded from Cloud Firestore.

Load Products from Firestore

Your responsive catalog already proves that the storefront layout works. However, every product still lives inside a hardcoded Next.js page.

This step moves the catalog into Cloud Firestore so product updates come from one data source. Firebase Security Rules keep the catalog publicly readable while blocking browser-side writes.

In this step, get ready to:
  • Create a Firebase project with a production-mode Firestore database.
  • Protect and populate the products collection.
  • Replace the hardcoded catalog with products loaded from Firestore.
Create the Firebase data source

The Firebase JavaScript SDK connects your browser-based storefront directly to Firestore. Version 12.19.0 uses the modular imports shown throughout this project.

  • Open the Firebase console in your browser.
  • Sign in to your Firebase account if prompted.
  • Create a Firebase project for the NextShop catalog.
  • Create a Standard edition Cloud Firestore database inside that project.
  • Select production mode for database access.
  • Complete the database creation flow.

You should see the new Firestore database inside your Firebase project. Production mode begins with browser access blocked until your rules grant it.

Why Start in Production Mode?

Production mode begins with mobile and web access denied. You can grant the catalog only the read access it needs instead of exposing every document to changes.

  • Return to the Firebase project overview.
  • Select the Web icon.
  • Enter a nickname for the NextShop web app.
  • Select Register app.
  • Keep the Firebase configuration object visible for the environment file.

The registered web app gives NextShop the six configuration values it needs to find your Firebase project.

  • Create a second PowerShell terminal in Visual Studio Code while the development server remains in its current terminal.
  • Install the Firebase JavaScript SDK by running:
npm install firebase@12.19.0 --save

What Does This Command Do?

The command adds Firebase JavaScript SDK version 12.19.0 to the project dependencies. The --save flag records it in package.json.

You should see the installation finish with a package summary. The dependency is now available to your application code.

Did the Firebase Installation Fail?

Check that the PowerShell prompt is inside the nextshop-rebuilt folder. Confirm the first terminal still owns the running development server.

Use the installation output to identify a network issue or a package resolution problem. Help me troubleshoot the Firebase SDK installation.

Firebase web configuration identifies your project. Security Rules still decide which data the browser can access.

  • Create .env.local.example inside the nextshop-rebuilt folder using the VS Code file sidebar.
  • Add the environment variable template by pasting this code:
NEXT_PUBLIC_FIREBASE_API_KEY=your-api-key-here
NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN=your-auth-domain-here
NEXT_PUBLIC_FIREBASE_PROJECT_ID=your-project-id-here
NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET=your-storage-bucket-here
NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID=your-messaging-sender-id-here
NEXT_PUBLIC_FIREBASE_APP_ID=your-app-id-here

How Does the Environment File Work?

The NEXT_PUBLIC_ prefix makes these values available to browser code during the Next.js build. The file belongs in nextshop-rebuilt beside package.json.

The example file documents every required setting with safe placeholders. Your local file receives the real values from the registered Firebase web app.

Keep Local Values Local

Firebase web configuration values identify your project. Keep .env.local out of Git so each deployment environment supplies its own configuration.

  • Save .env.local.example.
  • Duplicate .env.local.example as .env.local in the same folder.
  • Replace the six placeholder values in .env.local with the matching values from the Firebase configuration object.
  • Save .env.local.

✔️ Awesome, I've got everything!

Your example file contains safe placeholders. Your local file contains the six matching Firebase values.

ⓧ I'd like to double check the full code

NEXT_PUBLIC_FIREBASE_API_KEY=your-api-key-here
NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN=your-auth-domain-here
NEXT_PUBLIC_FIREBASE_PROJECT_ID=your-project-id-here
NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET=your-storage-bucket-here
NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID=your-messaging-sender-id-here
NEXT_PUBLIC_FIREBASE_APP_ID=your-app-id-here
  • Create a lib folder inside src using the VS Code file sidebar.
  • Create src/lib/firebase.js inside that folder.
  • Initialize Firebase by pasting this code:
import { getApp, getApps, initializeApp } from "firebase/app";
import { getFirestore } from "firebase/firestore";

const firebaseConfig = {
  apiKey: process.env.NEXT_PUBLIC_FIREBASE_API_KEY,
  authDomain: process.env.NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN,
  projectId: process.env.NEXT_PUBLIC_FIREBASE_PROJECT_ID,
  storageBucket: process.env.NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET,
  messagingSenderId: process.env.NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID,
  appId: process.env.NEXT_PUBLIC_FIREBASE_APP_ID,
};

const app = getApps().length > 0 ? getApp() : initializeApp(firebaseConfig);

export const db = getFirestore(app);

What Does This Code Do?

  • The firebaseConfig object reads the six values from .env.local.
  • The getApps() guard reuses an existing Firebase app during development reloads.
  • The exported db value gives the product functions access to Firestore.
  • Save src/lib/firebase.js.

✔️ Awesome, I've got everything!

Your Firebase module now reads the environment values and exports the Firestore database connection.

ⓧ I'd like to double check the full code

import { getApp, getApps, initializeApp } from "firebase/app";
import { getFirestore } from "firebase/firestore";

const firebaseConfig = {
  apiKey: process.env.NEXT_PUBLIC_FIREBASE_API_KEY,
  authDomain: process.env.NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN,
  projectId: process.env.NEXT_PUBLIC_FIREBASE_PROJECT_ID,
  storageBucket: process.env.NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET,
  messagingSenderId: process.env.NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID,
  appId: process.env.NEXT_PUBLIC_FIREBASE_APP_ID,
};

const app = getApps().length > 0 ? getApp() : initializeApp(firebaseConfig);

export const db = getFirestore(app);
  • Switch back to the PowerShell terminal running the development server.
  • Stop the current server with Ctrl+C.
  • Restart the server so Next.js reads .env.local by running:
npm run dev

Why Restart the Server?

Next.js loads environment files when the development process starts. Restarting makes the six new values available to firebaseConfig.

Your static catalog should still load at http://localhost:3000. This confirms the server restarted successfully.

Did the Server Stop Loading?

Check that .env.local sits beside package.json. Confirm every environment variable has a value after the equals sign.

Check src/lib/firebase.js against the full-code tab. Help me fix my Firebase initialization.

Protect and populate the catalog

Firestore uses Security Rules to decide which browser requests can reach a document. NextShop grants public reads only inside the products collection.

  • Create firestore.rules inside the nextshop-rebuilt folder using the VS Code file sidebar.
  • Define the catalog access rules by pasting this code:
rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    match /products/{productId} {
      allow read: if true;
      allow write: if false;
    }
  }
}

What Do These Rules Protect?

  • The /products/{productId} match applies the policy to every document in the catalog.
  • The read condition lets visitors load product documents.
  • The write condition prevents browser code from creating or changing products.
  • Save firestore.rules.
  • Return to the Firebase console.
  • Select Databases & Storage.
  • Select Firestore.
  • Select Rules.
  • Replace the rules editor contents with the code from firestore.rules.
  • Select Publish.

The published rules now show public reads with browser writes denied. New queries can take up to one minute to receive a rule update.

Did the Rules Fail to Publish?

Compare every brace in the console editor with firestore.rules. Confirm the products path is plural.

Use the console's highlighted line to locate a syntax mismatch. Help me troubleshoot my Firestore rules.

✔️ Awesome, I've got everything!

Your local rules file matches the policy published to Firestore.

ⓧ I'd like to double check the full code

rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    match /products/{productId} {
      allow read: if true;
      allow write: if false;
    }
  }
}

How Should the Product Documents Look?

Each document stores the same content already shown by its matching static card. The document ID also becomes its slug value.

  • Use string fields for name, slug, description, category, and badge.
  • Use a number field for price.
  • Create a Firestore collection named products.
  • Create a document with the ID wireless-headphones.
  • Populate that document from the Wireless Headphones card using the required schema.
  • Create a document with the ID city-sneakers.
  • Populate that document from the City Sneakers card using the required schema.
  • Create a document with the ID travel-backpack.
  • Populate that document from the Travel Backpack card using the required schema.

You should see three documents inside products. Each document should display all six fields when selected.

Is a Product Missing Data?

Compare the document ID with its slug field. Check that price uses a number type.

Check the field spelling against the schema above. Help me check my Firestore product documents.

The application needs one data module that converts Firestore documents into product objects. It also needs functions for loading the full catalog or one matching slug.

  • Create src/lib/products.js using the VS Code file sidebar.
  • Add the Firestore product queries by pasting this code:
import { collection, getDocs, query, where } from "firebase/firestore";
import { db } from "@/lib/firebase";

function toProduct(document) {
  return {
    id: document.id,
    ...document.data(),
  };
}

export async function getProducts() {
  const snapshot = await getDocs(collection(db, "products"));
  return snapshot.docs.map(toProduct);
}

export async function getProductBySlug(slug) {
  const productsRef = collection(db, "products");
  const productQuery = query(productsRef, where("slug", "==", slug));
  const snapshot = await getDocs(productQuery);

  if (snapshot.empty) {
    return null;
  }

  return toProduct(snapshot.docs[0]);
}

What Does This Code Do?

  • The toProduct() helper combines each Firestore document ID with its stored fields.
  • The getProducts() function reads every document in the products collection.
  • The getProductBySlug() function uses an equality query to find one matching product.
  • Save src/lib/products.js.
  • Confirm the VS Code file sidebar lists firebase.js and products.js inside src/lib.

Is the Product Module in the Wrong Folder?

Confirm the full path is src/lib/products.js. The @/lib/firebase import depends on the @/* alias pointing to src.

Compare the imports with the full-code tab. Help me fix my Firestore product module.

✔️ Awesome, I've got everything!

Your product module now supports both the full catalog and a single slug query.

ⓧ I'd like to double check the full code

import { collection, getDocs, query, where } from "firebase/firestore";
import { db } from "@/lib/firebase";

function toProduct(document) {
  return {
    id: document.id,
    ...document.data(),
  };
}

export async function getProducts() {
  const snapshot = await getDocs(collection(db, "products"));
  return snapshot.docs.map(toProduct);
}

export async function getProductBySlug(slug) {
  const productsRef = collection(db, "products");
  const productQuery = query(productsRef, where("slug", "==", slug));
  const snapshot = await getDocs(productQuery);

  if (snapshot.empty) {
    return null;
  }

  return toProduct(snapshot.docs[0]);
}
Render the live product grid

The catalog fetch runs in a React client component because it uses state and an effect. The component also displays loading and error states around the Firestore request.

  • Create a components folder inside src using the VS Code file sidebar.
  • Create src/components/ProductGrid.js inside that folder.
  • Begin the component by pasting this first connected section:
"use client";

import Link from "next/link";
import { useEffect, useState } from "react";
import { getProducts } from "@/lib/products";

const currency = new Intl.NumberFormat("en-US", {
  style: "currency",
  currency: "USD",
});

export default function ProductGrid() {
  const [products, setProducts] = useState([]);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState("");

  useEffect(() => {
    let cancelled = false;

    async function loadProducts() {
      try {
        const nextProducts = await getProducts();
        if (!cancelled) {
          setProducts(nextProducts);
        }

What Does the First Section Do?

This is the first half of one component. The next block closes the effect and adds the rendered product cards before you save the file.

The state values track the products and the current request status. The effect starts one Firestore read when the component loads.

  • Append the second section directly below the first block:
      } catch (loadError) {
        console.error(loadError);
        if (!cancelled) {
          setError("The catalog could not be loaded.");
        }
      } finally {
        if (!cancelled) {
          setLoading(false);
        }
      }
    }

    loadProducts();

    return () => {
      cancelled = true;
    };
  }, []);

  if (loading) return <p className="text-slate-400">Loading products...</p>;
  if (error) return <p className="text-rose-300">{error}</p>;

  return <div className="grid gap-6 sm:grid-cols-2 lg:grid-cols-3">{products.map((product) => <Link key={product.id} href={`/products/${product.slug}`} className="group overflow-hidden rounded-3xl border border-slate-800 bg-slate-900 transition hover:-translate-y-1 hover:border-cyan-400/60"><div className="flex aspect-[4/3] items-center justify-center bg-gradient-to-br from-cyan-500/20 to-violet-500/20 text-5xl font-black text-cyan-100">{product.badge}</div><div className="space-y-3 p-5"><p className="text-xs font-bold uppercase tracking-[0.2em] text-cyan-300">{product.category}</p><h2 className="text-xl font-bold text-white">{product.name}</h2><p className="text-sm leading-6 text-slate-400">{product.description}</p><p className="text-lg font-black text-white">{currency.format(product.price)}</p></div></Link>)}</div>;
}

What Completes the Component?

The cleanup flag prevents an old request from updating state after the component leaves the page. The loading and error branches give every request a visible state.

Each product becomes a responsive card linked to its own slug URL. The existing currency formatter displays every numeric price in USD.

  • Save src/components/ProductGrid.js.

The VS Code file sidebar should now list ProductGrid.js inside src/components.

Does the Product Grid Show a Syntax Problem?

Confirm the second code block begins directly after the closing brace for the successful request branch. Check that the final line closes the ProductGrid function.

Compare the complete file with the full-code tab. Help me assemble ProductGrid.js correctly.

✔️ Awesome, I've got everything!

Your product grid now handles the Firestore request and renders linked catalog cards.

ⓧ I'd like to double check the full code

"use client";

import Link from "next/link";
import { useEffect, useState } from "react";
import { getProducts } from "@/lib/products";

const currency = new Intl.NumberFormat("en-US", {
  style: "currency",
  currency: "USD",
});

export default function ProductGrid() {
  const [products, setProducts] = useState([]);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState("");

  useEffect(() => {
    let cancelled = false;

    async function loadProducts() {
      try {
        const nextProducts = await getProducts();
        if (!cancelled) {
          setProducts(nextProducts);
        }
      } catch (loadError) {
        console.error(loadError);
        if (!cancelled) {
          setError("The catalog could not be loaded.");
        }
      } finally {
        if (!cancelled) {
          setLoading(false);
        }
      }
    }

    loadProducts();

    return () => {
      cancelled = true;
    };
  }, []);

  if (loading) return <p className="text-slate-400">Loading products...</p>;
  if (error) return <p className="text-rose-300">{error}</p>;

  return <div className="grid gap-6 sm:grid-cols-2 lg:grid-cols-3">{products.map((product) => <Link key={product.id} href={`/products/${product.slug}`} className="group overflow-hidden rounded-3xl border border-slate-800 bg-slate-900 transition hover:-translate-y-1 hover:border-cyan-400/60"><div className="flex aspect-[4/3] items-center justify-center bg-gradient-to-br from-cyan-500/20 to-violet-500/20 text-5xl font-black text-cyan-100">{product.badge}</div><div className="space-y-3 p-5"><p className="text-xs font-bold uppercase tracking-[0.2em] text-cyan-300">{product.category}</p><h2 className="text-xl font-bold text-white">{product.name}</h2><p className="text-sm leading-6 text-slate-400">{product.description}</p><p className="text-lg font-black text-white">{currency.format(product.price)}</p></div></Link>)}</div>;
}

The homepage can now delegate the catalog to ProductGrid. Its remaining markup introduces the shop above the live cards.

  • Replace the temporary contents of src/app/page.js with this code:
import ProductGrid from "@/components/ProductGrid";

export default function HomePage() {
  return (
    <div className="space-y-10">
      <section className="max-w-3xl space-y-4">
        <p className="text-sm font-bold uppercase tracking-[0.25em] text-cyan-300">Fresh finds, simple shopping</p>
        <h1 className="text-4xl font-black tracking-tight text-white sm:text-6xl">Products worth making room for.</h1>
        <p className="text-lg leading-8 text-slate-400">Browse a focused collection loaded directly from Cloud Firestore.</p>
      </section>
      <ProductGrid />
    </div>
  );
}

What Does This Code Do?

The homepage keeps the NextShop introduction while replacing its hardcoded product array with ProductGrid. The grid now owns loading data and rendering cards.

  • Save src/app/page.js.

✔️ Awesome, I've got everything!

Your homepage now imports the Firestore-backed product grid.

ⓧ I'd like to double check the full code

import ProductGrid from "@/components/ProductGrid";

export default function HomePage() {
  return (
    <div className="space-y-10">
      <section className="max-w-3xl space-y-4">
        <p className="text-sm font-bold uppercase tracking-[0.25em] text-cyan-300">Fresh finds, simple shopping</p>
        <h1 className="text-4xl font-black tracking-tight text-white sm:text-6xl">Products worth making room for.</h1>
        <p className="text-lg leading-8 text-slate-400">Browse a focused collection loaded directly from Cloud Firestore.</p>
      </section>
      <ProductGrid />
    </div>
  );
}

Before you refresh, do you expect the catalog to preserve the same three cards after its data source changes?

  • Return to http://localhost:3000 in your browser.
  • Refresh the page.

You should see Wireless Headphones, City Sneakers, and Travel Backpack loaded as responsive cards. Each card should show its badge, category, name, description, and USD price.

That is the data handoff complete. Your visible catalog now comes from the three Firestore documents.

Does the Catalog Fail to Load?

Check that the six values in .env.local match the registered Firebase web app. Confirm the development server was restarted after that file was created.

Check that the published rules allow reads from /products/{productId}. Confirm all three documents are inside the products collection.

Use the browser console output to narrow down the failing layer. Help me debug my Firestore catalog.

Before you select a card, what do you think its product URL can display with the current files?

  • Select the Wireless Headphones card.

You should reach /products/wireless-headphones and see a 404 page. This is the intended shortfall because the dynamic product route does not exist yet.

Your catalog now reads protected product data from Firestore. Next, you will build the dynamic product route and connect those cards to a shared shopping cart.

Add Product Pages and Cart

Your Cloud Firestore catalog now shows live products. Every card still reaches the 404 you saw earlier because the shop needs a dynamic route for each product.

In this step, you'll add product pages. React Context will hold the cart while visitors move between routes without refreshing.

In this step, get ready to:
  • Share cart quantities across every route.
  • Turn each product slug into a working detail page.
  • Build a cart page that shows a complete editable order.
Create shared cart state

The cart must be available from product pages plus the cart page. A provider stores one shared list of items above every route.

  • Create a context folder inside src using the file sidebar in Visual Studio Code.
  • Create CartContext.js inside src/context.
  • Add the cart provider to CartContext.js by entering this first code chunk:
"use client";

import { createContext, useContext, useState } from "react";

const CartContext = createContext(null);

export function CartProvider({ children }) {
  const [items, setItems] = useState([]);

  function addToCart(product) {
    setItems((currentItems) => {
      const existingItem = currentItems.find((item) => item.id === product.id);
      if (existingItem) {
        return currentItems.map((item) => item.id === product.id ? { ...item, quantity: item.quantity + 1 } : item);
      }
      return [...currentItems, { ...product, quantity: 1 }];
    });
  }

  function removeFromCart(productId) {
    setItems((currentItems) => currentItems.filter((item) => item.id !== productId));
  }

  const totalItems = items.reduce((sum, item) => sum + item.quantity, 0);
  const totalPrice = items.reduce((sum, item) => sum + item.price * item.quantity, 0);

  return <CartContext.Provider value={{ items, addToCart, removeFromCart, totalItems, totalPrice }}>{children}</CartContext.Provider>;
}

What does this code do?

  • The items state stores every selected product plus its quantity.
  • The addToCart() function increases the quantity when the product already exists.
  • The removeFromCart() function creates a new array without the selected product.
  • The two reduce() calculations keep the item count plus order price current.
  • Save CartContext.js.
  • Confirm the file sidebar shows CartContext.js inside src/context.

Can't find the context file?

Check that context sits directly inside src. The full path must be src/context/CartContext.js.

Make sure the filename uses the capital letters shown above. Import paths must match the file name.

Help me check my cart context file path.

  • Add the context access helper at the bottom of CartContext.js by entering this code:
export function useCart() {
  const context = useContext(CartContext);
  if (!context) throw new Error("useCart must be used inside CartProvider");
  return context;
}

How does the helper work?

The useCart() helper gives components access to the shared cart value. Its guard catches any component placed outside CartProvider.

  • Save CartContext.js.
  • Confirm the file ends with the complete useCart() function.

Seeing a context syntax error?

Check that useCart() sits after the closing brace for CartProvider. Keep both functions at the top level of the file.

Help me fix my cart context syntax.

✔️ Awesome, I've got everything!

Your shared cart state is ready. Double check that CartContext.js is saved.

ⓧ I'd like to double check the full code

"use client";

import { createContext, useContext, useState } from "react";

const CartContext = createContext(null);

export function CartProvider({ children }) {
  const [items, setItems] = useState([]);

  function addToCart(product) {
    setItems((currentItems) => {
      const existingItem = currentItems.find((item) => item.id === product.id);
      if (existingItem) {
        return currentItems.map((item) => item.id === product.id ? { ...item, quantity: item.quantity + 1 } : item);
      }
      return [...currentItems, { ...product, quantity: 1 }];
    });
  }

  function removeFromCart(productId) {
    setItems((currentItems) => currentItems.filter((item) => item.id !== productId));
  }

  const totalItems = items.reduce((sum, item) => sum + item.quantity, 0);
  const totalPrice = items.reduce((sum, item) => sum + item.price * item.quantity, 0);

  return <CartContext.Provider value={{ items, addToCart, removeFromCart, totalItems, totalPrice }}>{children}</CartContext.Provider>;
}

export function useCart() {
  const context = useContext(CartContext);
  if (!context) throw new Error("useCart must be used inside CartProvider");
  return context;
}

Visitors need a visible way to reach the cart. The header will read totalItems from the shared context on every route.

  • Create Header.js inside src/components.
  • Build the shared navigation by entering this code:
"use client";

import Link from "next/link";
import { useCart } from "@/context/CartContext";

export default function Header() {
  const { totalItems } = useCart();
  return <header className="border-b border-slate-800 bg-slate-950/95"><nav className="mx-auto flex max-w-6xl items-center justify-between px-6 py-4"><Link href="/" className="text-xl font-black tracking-tight text-white">NextShop</Link><Link href="/cart" className="rounded-full border border-cyan-400/40 px-4 py-2 text-sm font-semibold text-cyan-200">Cart ({totalItems})</Link></nav></header>;
}

What does the header show?

The NextShop link returns visitors to the catalog. The cart link displays totalItems inside its label.

  • Save Header.js.
  • Confirm the file sidebar shows Header.js inside src/components.

Is the header import unresolved?

Check that Header.js imports useCart from @/context/CartContext. Confirm that CartContext.js is saved.

Help me resolve the header import.

✔️ Awesome, I've got everything!

The header component is ready for the root layout.

ⓧ I'd like to double check the full code

"use client";

import Link from "next/link";
import { useCart } from "@/context/CartContext";

export default function Header() {
  const { totalItems } = useCart();
  return <header className="border-b border-slate-800 bg-slate-950/95"><nav className="mx-auto flex max-w-6xl items-center justify-between px-6 py-4"><Link href="/" className="text-xl font-black tracking-tight text-white">NextShop</Link><Link href="/cart" className="rounded-full border border-cyan-400/40 px-4 py-2 text-sm font-semibold text-cyan-200">Cart ({totalItems})</Link></nav></header>;
}

The root layout surrounds every page in the app. Placing CartProvider there gives the header plus every route access to the same cart.

  • Select src/app/layout.js in the file sidebar.
  • Replace its contents with this code:
import "./globals.css";
import Header from "@/components/Header";
import { CartProvider } from "@/context/CartContext";

export const metadata = {
  title: "NextShop",
  description: "A Firestore-powered Next.js storefront",
};

export default function RootLayout({ children }) {
  return <html lang="en"><body><CartProvider><Header /><main className="mx-auto max-w-6xl px-6 py-10">{children}</main></CartProvider></body></html>;
}

Why wrap the root layout?

One CartProvider instance wraps the header plus each page. Client-side navigation therefore keeps one cart state alive across routes.

  • Save src/app/layout.js.
  • Switch back to the browser from earlier.

You'll see a dark header above the catalog. It shows NextShop on the left plus Cart (0) on the right.

Don't see the header?

Check the development server for an import error. Confirm that the layout imports Header plus CartProvider from their exact paths.

Help me render the shared header.

✔️ Awesome, I've got everything!

The shared header is visible. Your cart count can now appear on every route.

ⓧ I'd like to double check the full code

import "./globals.css";
import Header from "@/components/Header";
import { CartProvider } from "@/context/CartContext";

export const metadata = {
  title: "NextShop",
  description: "A Firestore-powered Next.js storefront",
};

export default function RootLayout({ children }) {
  return <html lang="en"><body><CartProvider><Header /><main className="mx-auto max-w-6xl px-6 py-10">{children}</main></CartProvider></body></html>;
}
Add dynamic product pages

Each catalog card already contains a unique slug. The product detail component uses that slug to load one document through getProductBySlug().

  • Create ProductDetails.js inside src/components.
  • Build the product detail interface by entering this code:
"use client";

import Link from "next/link";
import { useEffect, useState } from "react";
import { useCart } from "@/context/CartContext";
import { getProductBySlug } from "@/lib/products";

const currency = new Intl.NumberFormat("en-US", { style: "currency", currency: "USD" });

export default function ProductDetails({ slug }) {
  const [product, setProduct] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState("");
  const { addToCart } = useCart();
  useEffect(() => { let cancelled = false; async function loadProduct() { try { const nextProduct = await getProductBySlug(slug); if (!cancelled) setProduct(nextProduct); } catch (loadError) { console.error(loadError); if (!cancelled) setError("This product could not be loaded."); } finally { if (!cancelled) setLoading(false); } } loadProduct(); return () => { cancelled = true; }; }, [slug]);
  if (loading) return <p className="text-slate-400">Loading product...</p>;
  if (error) return <p className="text-rose-300">{error}</p>;
  if (!product) return <div className="space-y-4"><h1 className="text-3xl font-black">Product not found</h1><Link href="/" className="text-cyan-300 underline">Return to the catalog</Link></div>;
  return <article className="mx-auto grid max-w-4xl overflow-hidden rounded-3xl border border-slate-800 bg-slate-900 md:grid-cols-2"><div className="flex min-h-80 items-center justify-center bg-gradient-to-br from-cyan-500/20 to-violet-500/20 text-7xl font-black text-cyan-100">{product.badge}</div><div className="flex flex-col justify-center space-y-5 p-8"><p className="text-xs font-bold uppercase tracking-[0.2em] text-cyan-300">{product.category}</p><h1 className="text-4xl font-black text-white">{product.name}</h1><p className="leading-7 text-slate-300">{product.description}</p><p className="text-3xl font-black text-white">{currency.format(product.price)}</p><button type="button" onClick={() => addToCart(product)} className="rounded-2xl bg-cyan-400 px-5 py-3 font-black text-slate-950 transition hover:bg-cyan-300">Add to Cart</button></div></article>;
}

What does this component do?

  • The effect loads the product whose slug came from the URL.
  • The loading state gives the Firestore request time to finish.
  • The missing-product state offers a route back to the catalog.
  • The Add to Cart button passes the loaded product to addToCart().
  • Save ProductDetails.js.
  • Confirm the file sidebar shows ProductDetails.js inside src/components.

Is the product component failing to compile?

Check the imports for useCart plus getProductBySlug. Both identifiers already exist in the files referenced by their import paths.

Help me compile the product details component.

✔️ Awesome, I've got everything!

The client component is ready to receive a product slug.

ⓧ I'd like to double check the full code

"use client";

import Link from "next/link";
import { useEffect, useState } from "react";
import { useCart } from "@/context/CartContext";
import { getProductBySlug } from "@/lib/products";

const currency = new Intl.NumberFormat("en-US", { style: "currency", currency: "USD" });

export default function ProductDetails({ slug }) {
  const [product, setProduct] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState("");
  const { addToCart } = useCart();
  useEffect(() => { let cancelled = false; async function loadProduct() { try { const nextProduct = await getProductBySlug(slug); if (!cancelled) setProduct(nextProduct); } catch (loadError) { console.error(loadError); if (!cancelled) setError("This product could not be loaded."); } finally { if (!cancelled) setLoading(false); } } loadProduct(); return () => { cancelled = true; }; }, [slug]);
  if (loading) return <p className="text-slate-400">Loading product...</p>;
  if (error) return <p className="text-rose-300">{error}</p>;
  if (!product) return <div className="space-y-4"><h1 className="text-3xl font-black">Product not found</h1><Link href="/" className="text-cyan-300 underline">Return to the catalog</Link></div>;
  return <article className="mx-auto grid max-w-4xl overflow-hidden rounded-3xl border border-slate-800 bg-slate-900 md:grid-cols-2"><div className="flex min-h-80 items-center justify-center bg-gradient-to-br from-cyan-500/20 to-violet-500/20 text-7xl font-black text-cyan-100">{product.badge}</div><div className="flex flex-col justify-center space-y-5 p-8"><p className="text-xs font-bold uppercase tracking-[0.2em] text-cyan-300">{product.category}</p><h1 className="text-4xl font-black text-white">{product.name}</h1><p className="leading-7 text-slate-300">{product.description}</p><p className="text-3xl font-black text-white">{currency.format(product.price)}</p><button type="button" onClick={() => addToCart(product)} className="rounded-2xl bg-cyan-400 px-5 py-3 font-black text-slate-950 transition hover:bg-cyan-300">Add to Cart</button></div></article>;
}

The route page connects the URL segment to ProductDetails. Its folder name captures the slug from every product URL.

  • Create a products folder inside src/app.
  • Create a [slug] folder inside src/app/products.

The file sidebar now shows the nested route path src/app/products/[slug].

  • Create page.js inside src/app/products/[slug].
  • Connect the route to the product component by entering this code:
import ProductDetails from "@/components/ProductDetails";

export default async function ProductPage({ params }) {
  const { slug } = await params;
  return <ProductDetails slug={slug} />;
}

Why split the route?

The route page awaits params before passing slug into the client component. ProductDetails can then use state plus effects for Firestore loading.

  • Save src/app/products/[slug]/page.js.
  • Switch back to the catalog in your browser.
  • Select the Wireless Headphones card.

You'll see the Wireless Headphones detail page with its badge, category, description, price, plus an Add to Cart button.

Still seeing the 404 page?

Check that the route file is located at src/app/products/[slug]/page.js. Keep the square brackets in the folder name.

Confirm the product document uses wireless-headphones as its slug value.

Help me fix the dynamic product route.

✔️ Awesome, I've got everything!

That closes the 404 gap. One dynamic route can now display every product in the catalog.

ⓧ I'd like to double check the full code

import ProductDetails from "@/components/ProductDetails";

export default async function ProductPage({ params }) {
  const { slug } = await params;
  return <ProductDetails slug={slug} />;
}
Build and verify the cart

The cart page reads the same shared item list as the header. It calculates line prices from each quantity before displaying the full order total.

  • Create a cart folder inside src/app.
  • Create page.js inside src/app/cart.
  • Build the cart interface by entering this code:
"use client";

import Link from "next/link";
import { useCart } from "@/context/CartContext";

const currency = new Intl.NumberFormat("en-US", { style: "currency", currency: "USD" });

export default function CartPage() {
  const { items, removeFromCart, totalPrice } = useCart();
  if (items.length === 0) return <section className="mx-auto max-w-xl rounded-3xl border border-slate-800 bg-slate-900 p-10 text-center"><h1 className="text-3xl font-black text-white">Your cart is empty</h1><p className="mt-3 text-slate-400">Add a product to start an order.</p><Link href="/" className="mt-6 inline-block rounded-2xl bg-cyan-400 px-5 py-3 font-black text-slate-950">Browse products</Link></section>;
  return <section className="mx-auto max-w-3xl space-y-6"><h1 className="text-4xl font-black text-white">Your Cart</h1><div className="space-y-4">{items.map((item) => <article key={item.id} className="flex flex-col gap-4 rounded-3xl border border-slate-800 bg-slate-900 p-5 sm:flex-row sm:items-center sm:justify-between"><div><p className="text-lg font-bold text-white">{item.name}</p><p className="text-sm text-slate-400">Quantity: {item.quantity}</p></div><div className="flex items-center gap-4"><p className="font-black text-cyan-200">{currency.format(item.price * item.quantity)}</p><button type="button" onClick={() => removeFromCart(item.id)} className="text-sm font-bold text-rose-300 hover:text-rose-200">Remove</button></div></article>)}</div><div className="flex items-center justify-between border-t border-slate-800 pt-6"><p className="text-xl font-bold text-slate-300">Order total</p><p className="text-3xl font-black text-white">{currency.format(totalPrice)}</p></div></section>;
}

How does the cart page work?

  • An empty items array displays a link back to the catalog.
  • Each populated cart line shows the saved quantity.
  • Each line price multiplies item.price by item.quantity.
  • The Remove button calls removeFromCart() with the selected product ID.
  • Save src/app/cart/page.js.
  • Select Cart (0) in the header.

You'll see an empty-cart panel with a Browse products link. This proves the cart route can read the shared empty state.

Can't open the cart page?

Check that the file path is src/app/cart/page.js. Confirm that the page imports useCart from @/context/CartContext.

Help me load the cart route.

✔️ Awesome, I've got everything!

The cart route is working. It can now display either an empty state or a calculated order.

ⓧ I'd like to double check the full code

"use client";

import Link from "next/link";
import { useCart } from "@/context/CartContext";

const currency = new Intl.NumberFormat("en-US", { style: "currency", currency: "USD" });

export default function CartPage() {
  const { items, removeFromCart, totalPrice } = useCart();
  if (items.length === 0) return <section className="mx-auto max-w-xl rounded-3xl border border-slate-800 bg-slate-900 p-10 text-center"><h1 className="text-3xl font-black text-white">Your cart is empty</h1><p className="mt-3 text-slate-400">Add a product to start an order.</p><Link href="/" className="mt-6 inline-block rounded-2xl bg-cyan-400 px-5 py-3 font-black text-slate-950">Browse products</Link></section>;
  return <section className="mx-auto max-w-3xl space-y-6"><h1 className="text-4xl font-black text-white">Your Cart</h1><div className="space-y-4">{items.map((item) => <article key={item.id} className="flex flex-col gap-4 rounded-3xl border border-slate-800 bg-slate-900 p-5 sm:flex-row sm:items-center sm:justify-between"><div><p className="text-lg font-bold text-white">{item.name}</p><p className="text-sm text-slate-400">Quantity: {item.quantity}</p></div><div className="flex items-center gap-4"><p className="font-black text-cyan-200">{currency.format(item.price * item.quantity)}</p><button type="button" onClick={() => removeFromCart(item.id)} className="text-sm font-bold text-rose-300 hover:text-rose-200">Remove</button></div></article>)}</div><div className="flex items-center justify-between border-t border-slate-800 pt-6"><p className="text-xl font-bold text-slate-300">Order total</p><p className="text-3xl font-black text-white">{currency.format(totalPrice)}</p></div></section>;
}

The remove control should return the cart to its empty state. Test that state change before the final quantity check.

  • Select Browse products.
  • Select the Wireless Headphones card.
  • Click Add to Cart once.
  • Select Cart (1) in the header.
  • Click Remove on the Wireless Headphones line.

You'll see the empty-cart panel again. The shared context has removed the selected product.

How long does the cart last?

The cart lives in React memory. A full browser refresh starts it empty again.

Client-side links preserve the cart because the same provider remains mounted while you move between routes.

Before the final check, what quantity do you expect after adding the same product twice?

  • Select Browse products.
  • Select the Wireless Headphones card.
  • Click Add to Cart twice.
  • Select Cart (2) in the header.

You'll see one Wireless Headphones line with quantity 2. Its line total is double the product price.

Your local storefront now supports product browsing plus an editable cart. Next, you'll verify the database rules before publishing NextShop.

Secure and Deploy NextShop

Your local NextShop now has working product pages and a shared cart. A localhost storefront still cannot be opened by anyone else.

Your Cloud Firestore rules must keep catalog reads available while blocking browser writes. Publishing the existing source through GitHub gives Vercel a safe path to turn that local flow into a shareable production demo.

In this step, get ready to:
  • Verify the Cloud Firestore security boundary in Rules Playground.
  • Publish the existing project to GitHub without .env.local.
  • Deploy the storefront to Vercel with its production environment variables.
Verify the Firestore security boundary

Cloud Firestore Security Rules form the boundary between the public storefront and the database. Rules Playground lets you confirm that visitors can read the products collection without gaining write access.

  • Return to the Firebase console project from earlier.
  • Select Databases & Storage.
  • Select Firestore.
  • Select Rules.
  • Open Rules Playground.
  • Configure an unauthenticated read against an existing document in the products collection.

Before you run the first simulation, do you expect a visitor to read an existing product? Hold that prediction while you test it.

  • Run the unauthenticated read simulation.

The read simulation should succeed. This proves the public storefront can retrieve product documents.

  • Configure an unauthenticated write against the same product document.

Before you run the second simulation, do you expect an unauthenticated browser request to change the product? Keep your prediction in mind.

  • Run the unauthenticated write simulation.

The write simulation should fail. Your catalog stays publicly readable while browser-side changes remain blocked.

Do the rule results look wrong?

Confirm the read simulation targets a document inside products. Confirm the write simulation targets that same collection.

Check that the published rules contain the public read rule and denied write rule from the earlier Firestore step.

Help me troubleshoot my Firestore Rules Playground results.

Publish the source to GitHub

Git records a local snapshot of the finished storefront. The GitHub repository gives Vercel the source it needs for deployment.

Publishing code can feel risky when local configuration sits beside it. The scaffold created by create-next-app already ignores environment files.

  • Return to your GitHub account from the setup step.
  • Create a new empty repository named nextshop-rebuilt.
  • Leave repository initialization disabled so the repository stays empty.
  • Copy the repository URL for the next command.
  • Switch back to VS Code from earlier.
  • Create a second PowerShell terminal inside the open nextshop-rebuilt project.
  • Stage the existing project files and create the deployment commit by running these commands:
git add .
git commit -m "Build NextShop"

What do these commands do?

  • The first command stages the current project files for the commit.
  • The second command records the staged snapshot with the message Build NextShop.

You should see a commit summary listing the recorded file changes. The source is now ready to send to GitHub.

Could not create the commit?

Confirm the PowerShell terminal belongs to the nextshop-rebuilt folder. A terminal opened elsewhere cannot find this project's Git history.

If Git requests your author identity, follow its configuration guidance before running the commit commands again.

Help me create the NextShop Git commit.

  • Connect the local repository to GitHub and push main by replacing REMOTE-URL with the copied repository URL in these commands:
git remote add origin REMOTE-URL
git push -u origin main

How does the push work?

  • The first command assigns the GitHub repository URL to the remote name origin.
  • The second command uploads main and establishes its upstream branch.

You should see upload progress followed by confirmation that main now tracks the remote branch.

  • Return to the new GitHub repository page.
  • Refresh the repository page.

The repository should show your existing NextShop source on main. The .env.local file should be absent.

This step publishes the cumulative app without changing any source files.

Could not push to GitHub?

Check that you replaced REMOTE-URL with the URL from the empty repository. Confirm that the destination repository does not contain an automatically created file.

Complete any GitHub authentication prompt before running the push commands again.

Help me push NextShop to GitHub.

Deploy the production storefront

Vercel builds the Next.js app from the connected GitHub repository. Browser-accessible environment variables are inlined during the production build.

  • Return to the Vercel account from the setup step.
  • Select New Project to start importing a repository.
  • Authorize Vercel to access the GitHub repository if an authorization screen appears.
  • Select the nextshop-rebuilt repository as the project source.
  • Expand Environment Variables to reveal the name and value fields.

Why is the Firebase API key public?

Seeing an API key in browser configuration can feel risky. Firebase web API keys identify the project.

Security Rules enforce database authorization. Your successful read test and failed write test confirmed that boundary before deployment.

  • Create the production environment variable NEXT_PUBLIC_FIREBASE_API_KEY with its matching value from .env.local.
  • Create the production environment variable NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN with its matching value from .env.local.

You should now see the first two environment variable names saved in the project configuration.

  • Create the production environment variable NEXT_PUBLIC_FIREBASE_PROJECT_ID with its matching value from .env.local.
  • Create the production environment variable NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET with its matching value from .env.local.

The project configuration should now list four saved environment variable names.

  • Create the production environment variable NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID with its matching value from .env.local.
  • Create the production environment variable NEXT_PUBLIC_FIREBASE_APP_ID with its matching value from .env.local.

All six NEXT_PUBLIC_FIREBASE_* names should now be listed in the Vercel project configuration.

  • Start the production deployment by selecting Deploy.

You should see the build progress complete successfully. Vercel then provides a public production URL for NextShop.

Did the deployment or catalog fail?

  • Check that all six environment variable names match .env.local exactly.
  • Check that each Vercel value matches the corresponding local value.
  • Create a new deployment after changing an environment variable because existing deployments keep their previous values.

Help me troubleshoot the Vercel deployment.

The cart still uses in-memory state, so this production check follows client-side navigation. Before you open production, which parts do you expect to behave exactly like localhost?

  • Open the generated Vercel production URL.
  • Select the Wireless Headphones product card.
  • Click Add to Cart twice.
  • Select Cart in the header without refreshing the page.

You should see three Firestore-backed product cards on the production catalog. The /products/wireless-headphones route should display the product details.

The /cart page should show one Wireless Headphones line with quantity 2. Its line total should be double the single-product price.

That completes the production loop. Your secured NextShop is now live and ready to share.

Secret mission

Add Category Filtering

Your live catalog already reads from Firestore. Add four category buttons that narrow the visible products with equality queries. Then publish the extension through the existing GitHub-to-Vercel workflow.

Clean Up Your Resources

Clean Up Your Resources

This personal learning demo is designed to stay at no cost within the Firebase Spark plan plus the Vercel Hobby plan. Decide whether to keep your resources running, pause the local server, or delete everything.

Resources you used:

  • A local nextshop-rebuilt folder containing the complete NextShop project.
  • A Firebase project containing your Standard Cloud Firestore database. This database holds the products collection under published read-only rules.
  • A GitHub repository containing the category-filtering commit on main. The repository excludes .env.local.
  • A Vercel project containing the six Firebase environment variables. Its production deployment serves the filtered NextShop catalog.

Keep everything running

No action needed. Choose this if you want to keep demonstrating NextShop or continue improving it.

  • Keep the Vercel production deployment available for your personal learning demo.
  • Keep the Firebase project active for catalog reads.
  • Keep the GitHub repository as the source for future deployments.
  • Keep the local nextshop-rebuilt folder for future development.
  • Monitor Firebase usage if real visitors begin browsing the catalog.
  • Monitor Vercel usage if real traffic reaches the deployment.

Pause - I'll come back to this later

Shut down the local development server to free the PowerShell session. Your project files remain available for your next coding session.

  • Check whether the PowerShell terminal is still occupied by the local development server.
  • Press Ctrl+C if the local development server is still running.

PowerShell returns to its prompt when the local server stops. Your files remain inside nextshop-rebuilt.

Your Vercel deployment stays live. Your Firebase catalog remains available.

Delete - I don't want to use this again

Remove every project resource for a clean start.

This is the irreversible option. Work through one confirmation at a time.

Vercel project:

  • Open the settings page for your NextShop project from the Vercel dashboard.
  • Use the project deletion control to remove the Vercel project.
  • Complete the confirmation prompt.
  • Reload the former production URL to verify the deletion.

The former production URL should no longer serve NextShop. Your public storefront is now offline.

Firebase project:

  • Open the settings page for your Firebase project from the Firebase console.
  • Use the project deletion control to remove the Firebase project.
  • Complete the confirmation prompt.
  • Return to the Firebase project list to verify the deletion.

The Firebase project should no longer appear in your project list. This deletion also removes the Firestore database.

GitHub repository:

  • Open the settings page for the GitHub repository.
  • Use the repository deletion control to remove the repository.
  • Complete the confirmation prompt.
  • Reload the former repository URL to verify the deletion.

The repository URL should no longer display your NextShop source. The remote copy is now gone.

Local project folder:

  • Switch back to the PowerShell terminal if the local development server is running.
  • Press Ctrl+C to stop the local process.
  • Locate the nextshop-rebuilt folder in Windows File Explorer.
  • Select the nextshop-rebuilt folder.
  • Press Shift+Delete to remove the folder permanently.
  • Complete the deletion confirmation prompt.
  • Return to the parent folder to verify the deletion.

The nextshop-rebuilt folder should no longer appear. Your local NextShop files have now been removed.

Nice Work!

Nice Work!

You did it! Your complete NextShop storefront is now live on Vercel.

You've learned how to:

  • Built a responsive Next.js catalog that loads product cards from a read-only Cloud Firestore collection.
  • Created dynamic product pages with one [slug] route for every Firestore product.
  • Delivered a shared React Context cart that keeps quantities, removals, line totals, and the order total consistent across routes.
  • Secret Mission: Added category filtering backed by Firestore equality queries.

Ready to quiz yourself?