Developing Full Stack Next.js Web Applications

Chapter 1 — Building Next.js User Interfaces with HTML

Dr. Jose Annunziato

Slides

The foundation of our modern digital landscape is the Internet, a global array of interconnected computer networks. It originated in the early 1960s as a research project commissioned by the Advanced Research Projects Agency (ARPA), the research arm of the United States Department of Defense (DoD). The initial goal was to build ARPANET, a robust, decentralized, and redundant communication infrastructure capable of maintaining connectivity even in the event of major disruptions. By the 1980s, the standardization of protocols like TCP/IP allowed these isolated military and academic networks to proliferate and connect worldwide, establishing the underlying network of networks upon which modern digital communication relies.

The World Wide Web was invented in 1989 by British computer scientist Sir Tim Berners-Lee during his tenure at CERN, the European particle physics laboratory in Switzerland. The objective was to enable the efficient sharing and linking of research documents over the existing Internet. In March 1989, Sir Tim Berners-Lee proposed a system of hypertext documents connected via hyperlinks, which users access through Uniform Resource Locators (URLs) using the HyperText Transfer Protocol (HTTP). By 1990, he had created the first web browser (named WorldWideWeb), a web server, and the foundational HyperText Markup Language (HTML) language to create HTTP web pages. Browsers and servers connect to one another over the internet in a client-server architecture (Figure 1.1). Sir Tim Berners-Lee made the Web public in 1991, and on April 30, 1993, CERN released the technology into the public domain, facilitating explosive growth. Presently, billions of static and dynamic pages power various systems ranging from simple sites to complex applications.

Client-server architecture: a browser UI with HTML, CSS, JavaScript, and React talks HTTP to a Node.js server with Express, REST APIs, session, and Mongoose; the server reads static files, the cloud, and a database
Figure 1.1 — The Client Server Architecture

Web pages are comprised of plain text documents formatted with HTML, a dialect of XML (eXtensible Markup Language). HTML is a computer language utilized to format the content displayed in web pages, including properties such as foreground and background color, white spaces, text alignment, font, lists, tables, and forms. Browsers establish a network connection with servers to send HTTP Requests for HTML documents. These requests rely on Uniform Resource Locators (URLs). A URL such as http://www.nasa.gov names the protocol (http — Hypertext Transfer Protocol), then the server (www.nasa.gov). That hostname is mapped to an IP address so the browser can find the machine on the network. A path after the server name pinpoints a specific document. Local development uses the same pattern: http://localhost:3000/labs/lab1 — here http is still the protocol, localhost is the hostname for your own machine, 3000 is the port where the Next.js dev server listens, and /labs/lab1 is the path to Lab 1. Servers locate the requested documents, and then respond with the document's content. Browsers parse HTML documents to create in-memory object representations known as the DOM (Document Object Model). The DOM consists of a hierarchical data structure where each node is configured to render content in a specific format and style. This chapter examines using HTML for formatting web pages and creating user interfaces.

Plain HTML documents are static: the same document does not change over time, and does not depend on the data or user interactions. The Web quickly reached the limit of what could be render on a screen. — a list of courses that never changes, a heading that never knows who signed in. In the early 1990s, servers began to compute HTML on request so the content could be data driven and interact with users. Scripts on the server — CGI, then languages such as PHP — could assemble a finished document and send it to the browser. Every click still meant a full round trip for a new page. JavaScript, created by Brendan Eich at Netscape in 1995, brought that computation into the browser. HTML documents download .js files from the server; the browser runs them to manipulate the DOM and build dynamic user interfaces without a full round trip for every change. The same language now also runs on the server — in Figure 1.1, the client box is HTML, CSS, JavaScript, and React; the Node.js box is JavaScript too. This course uses both: later chapters generate HTML on the server and still use the browser for clicks, state, and anything that reads the address bar. TypeScript is a version of JavaScript developed by Microsoft that adds static types and is quickly becoming the preferred language for web development. Later chapters discuss programming server-side logic, including API (Application Programming Interface) routes and database interactions with MongoDB.

React is a popular JavaScript library developed by Meta for building dynamic and interactive user interfaces. It promotes a component-based architecture where developers break down complex UIs into small and reusable pieces of code called components. React efficiently manages the state of these components, ensuring that the user interface stays in sync with underlying data changes and user interaction. Developers use React to build Single Page Applications (SPAs), which provide a seamless, fluid user experience by updating only the necessary parts of the page without requiring full-page reloads — unlike traditional multi-page sites, where every link often fetches an entirely new HTML document from the server. Its declarative nature simplifies the process of creating complex web interfaces by describing what the UI should look like for a given state.

Next.js is a powerful framework designed to simplify the construction of full-stack web applications. Built on top of React, it provides an all-in-one solution that includes robust features such as server-side rendering (SSR), static site generation (SSG), and seamless API endpoint integration. Next.js extends React by offering built-in routing, data fetching, and performance optimizations, allowing developers to create highly scalable applications that interact efficiently with backend resources, such as MongoDB, over HTTP.

This chapter describes how to install and configure a local development environment for building Next.js applications. Development is done in the local environment and then shared in a remote GitHub source repository. The source in GitHub is then deployed to a remote server hosted on Vercel which is optimized for Next.js and provides seamless serverless deployment. This chapter introduces creating a Next.js application and explores building user interfaces using HTML and JavaScript. Various HTML elements are described to render user interface content, such as headings, paragraphs, lists, tables, and form elements. All sections in this chapter contain exercises that introduce basic HTML elements and concepts, giving an opportunity to learn and practice HTML skills. The exercises provide detailed instructions to successfully accomplish the tasks. Make sure to complete all exercises described in the book.

The Kambaz sections in each chapter contain exercises that ask readers to build a fully functional web application inspired by a popular Learning Management System (LMS) with a similar name. The exercises provide sample code and requirements but deliberately leave out steps where the reader is expected to experiment and discover how to implement the requirements using the skills learned in prior sections. This chapter focuses on using plain HTML within Next.js components to implement a draft, rough prototype of various Kambaz screens, which at first won't look like the target product in Figure 1a–1d. Later chapters will continue working on the Kambaz application, introducing Cascading Style Sheets (CSS) to style the Web pages so they look more like these screen shots, and integrating MongoDB for data persistence.

Kambaz Dashboard target screenshot
Figure 1a — Dashboard Screen
Kambaz Modules target screenshot
Figure 1b — Modules Screen
Kambaz Assignments target screenshot
Figure 1c — Assignments Screen
Kambaz Assignment Editor target screenshot
Figure 1d — Assignment Editor

1.1 Learning Objectives

By the end of this chapter, you will be able to:

  • Understand the fundamentals of HTML and how it structures web content.
  • Set up a development environment for Next.js applications.
  • Install Claude Code in the IDE and sign in with a Claude account.
  • Create and organize Next.js components using JSX.
  • Use Chrome DevTools to inspect and manipulate the DOM.
  • Implement headings, paragraphs, lists, tables, and images.
  • Build interactive web forms with different input types.
  • Pass props into custom components and wrap nested content with children.
  • Implement navigation in a Next.js SPA using built-in routing.
  • Develop a structured approach to building UIs in Next.js.

Those objectives are best achieved by building along with the narration — each lab component and Kambaz screen as it appears — rather than reading first and coding later. Glance at the Lab 1 checklist in §1.3.12 and the Kambaz checklist in §1.4.9 so the expected coverage is visible from the start. Those lists are recaps, not a reason to skip ahead: work through each section, then use them to confirm what stuck.

1.2 Setting Up the Development Environment

Slides

HTML practice in this book happens inside Next.js components, so first install the tools that run the app on your machine. Later sections assume earlier ones, so it helps to keep the project in step as you read.

1.2.1 Installing Node.js Slides

Node.js is a JavaScript runtime that lets you run JavaScript outside the browser — typically from a terminal or console on your computer. It is essential for Next.js development: it powers the local development server, installs and manages project dependencies, and later enables server-side features such as API routes.

Installing Node.js also gives you two companion tools, npm (Node Package Manager) and npx. Think of them as the JavaScript ecosystem's equivalent of build and package tools you may already know: mvn for Java (Maven) or pip for Python. With npm you install libraries, run project scripts (for example npm run dev), and manage versions listed in package.json. With npx you can run a one-off package command without installing it globally first — which is how we will scaffold the app with npx create-next-app in §1.2.4.

In this chapter we use Node.js mainly to create and host the React user interface. Later chapters will use the same runtime to implement HTTP servers and REST APIs (Representational State Transfer), and to integrate databases such as MongoDB. Getting a solid Node.js install now sets you up for both the front end and the back end of the stack.

Navigate to https://nodejs.org/, download the latest LTS (Long Term Support) version for your operating system (recommended: version 24.x or later as of 2026), and install it. Restart your computer if prompted. Confirm the install by typing node -v in a console or terminal. The output should show the installed version (for example v24.19.0). Your exact version may differ, but it should be at least 20.9 or later, as required by Next.js.

node -v
v24.19.0

1.2.2 Installing an Integrated Development Environment (IDE)

You can edit Next.js projects in any text editor, but an Integrated Development Environment (IDE) makes the work much smoother: syntax highlighting, autocomplete, inline errors, debugging, and a built-in terminal in one place. For this course, Visual Studio Code (VS Code) or Cursor is highly recommended. Both have strong support for JavaScript, TypeScript, React, and Next.js — including IntelliSense suggestions and debugging integrations. Cursor is built on VS Code, so the menus and extensions match the screenshots in this book.

Download and install VS Code from https://code.visualstudio.com or Cursor from https://cursor.com. Once installed, open the Extensions view and add a few useful packages: ESLint for catching common code problems, Prettier for consistent formatting, and browser React Developer Tools for inspecting component trees while you run the app.

You will run many commands — npm run dev, git, and others — from a terminal. Prefer the integrated terminal inside the IDE (in VS Code or Cursor: Terminal → New Terminal) so you stay in the same window as your files and do not have to switch back and forth to a separate console.

1.2.3 Adding Claude to the IDE

Claude is an AI assistant from Anthropic. In this course you can use it inside the editor to explain code, draft a first version, and hunt down errors — still read what it writes, and keep the book and labs as the source of truth. If your school, employer, or a personal Claude plan already includes access, sign in with that account. Otherwise start at claude.ai. Claude Code — the editor extension we install next — expects a Claude subscription (Pro, Max, Team, or Enterprise) or a Claude Console account, not a one-off API key for this setup.

Open the Extensions view (Cmd+Shift+X on macOS, Ctrl+Shift+X on Windows or Linux), search for Claude Code, and install the one published by Anthropic. The same extension works in VS Code and in Cursor. Direct install links are in the Claude Code for VS Code docs. If the spark icon does not appear, reload the window from the Command Palette: Developer: Reload Window.

Open Claude from the Command Palette (Cmd+Shift+P / Ctrl+Shift+P), type Claude Code, and choose Open in New Tab. The first time, click Sign in and finish authorization in the browser. In Cursor, Claude Code is separate from Cursor's built-in chat — install and sign in even if Cursor AI already works.

1.2.4 Creating a Next.js Application Slides

With npx — the tool that shipped with your Node.js install — you can scaffold a new Next.js project from a maintained template that already follows current best practices.

Start by creating a place on disk for your coursework. From your home directory (~), make a folder for the year, the term, and the course. On macOS use Terminal; on Windows use Command Prompt or PowerShell. For example, to create ~/2049/winter/webdev:

cd ~
mkdir 2049
mkdir 2049/winter
mkdir 2049/winter/webdev
cd 2049/winter/webdev

You can pick another location if you prefer. Keep names lowercase, avoid spaces, and nest the project only under directories that follow the same rules — that prevents a lot of path and tooling headaches later.

From that folder (or from the IDE's integrated terminal), create the app with:

npx create-next-app@16.3 webdev-client

The @16.3 pins the starter project to Next.js 16.3, the version this book is tested with. Don't use @latest: since October 6, 2026 it installs Next.js 16.4, whose starter turns on Cache Components, and the Kambaz pages you build later in this chapter then fail npm run build and every Vercel deploy, even though npm run dev still works.

The first time you run this, npm may ask permission to download the create-next-app package. Accept and continue (the last number may be higher; any 16.3.x is fine):

Need to install the following packages:
  create-next-app@16.3.8
Ok to proceed? (y)

Next comes one question. Keep the highlighted answer, Yes, use recommended defaults, and press Enter. The defaults are what this course uses: TypeScript, ESLint, no React Compiler, Tailwind CSS, no src/ directory, the App Router, the @/* import alias, and an AGENTS.md file (with a CLAUDE.md that points to it) holding Next.js notes for AI coding assistants. Turbopack is already the default bundler, so there is no separate question for it.

? Would you like to use the recommended Next.js defaults? › - Use arrow-keys. Return to submit.
❯   Yes, use recommended defaults
    TypeScript, ESLint, No React Compiler, Tailwind CSS, No src/ directory, App Router, AGENTS.md
    No, customize settings

If you have run create-next-app on this computer before, the list has a third choice in the middle, No, reuse previous settings. Skip it and keep Yes, use recommended defaults.

If you pick No, customize settings instead, you answer each setting yourself. Pressing Enter on every question keeps the same answers as the defaults; these are the answers to give:

✔ Would you like to use the recommended Next.js defaults? › No, customize settings
✔ Would you like to use TypeScript? … Yes
✔ Which linter would you like to use? › ESLint
✔ Would you like to use React Compiler? … No
✔ Would you like to use Tailwind CSS? … Yes
✔ Would you like your code inside a `src/` directory? … No
✔ Would you like to use App Router? (recommended) … Yes
✔ Would you like to customize the import alias (`@/*` by default)? … No
✔ Would you like to include AGENTS.md to guide coding agents to write up-to-date Next.js code? … Yes

Created your project on or after October 6, 2026? Open package.json. If it says "next": "16.4.0", your project is on Next.js 16.4. (Projects created on October 6 before 2:35 PM ET got 16.3.8 and don't need this fix.) On 16.4, npm run dev works, but npm run build and Vercel deploys fail once you add the book's code. The first error you see is usually Next.js encountered URL data usePathname() in a Client Component outside of <Suspense>; further down the log there is also Next.js encountered uncached or runtime data during prerendering.

To fix it, open next.config.ts and delete both the cacheComponents: true, and the partialPrefetching: true, lines. Delete both: removing only the first one fails with partialPrefetching requires cacheComponents to be enabled. Leave everything else in the file alone and commit the change. After that, npm run build and Vercel deploys of the book's code work again.

next.config.ts
const nextConfig: NextConfig = {
  /* config options here */
  cacheComponents: true,    // delete this line
  partialPrefetching: true, // delete this line
  // ... leave the rest of this file alone
};

Wait while dependencies install (react, react-dom, next, TypeScript types, Tailwind, ESLint, and related packages). When it finishes you should see a success message and a new webdev-client directory. If it also says A new version of create-next-app is available, ignore it: updating create-next-app puts you back on Next.js 16.4. Change into that directory and start the development server:

cd webdev-client
npm run dev

The console should report that Next.js is ready and print a local URL, usually http://localhost:3000. Open that URL in Google Chrome and confirm the default Next.js starter page (logo and getting-started content) appears. Stop the server anytime with Ctrl+C.

Figure 1.2 — Default Next.js project running in a browser
Next.js

Default starter app at http://localhost:3000

You should see the Next.js logo and welcome content in Chrome.

You can start the app from any terminal, but prefer running npm run dev from the IDE: open the webdev-client folder in VS Code (or Cursor), show the terminal with View → Terminal if needed, and run the command there. Other browsers and editors are fine, but this course assumes Google Chrome and VS Code or Cursor unless noted otherwise.

1.2.5 Creating Pages and Routes with the App Router

Next.js user interfaces are written as JavaScript functions (or classes) called components. A component computes HTML: it can choose markup, text, or structure from data instead of returning the same static document every time. To make that easier to write, the syntax blurs the line between JavaScript and HTML — you put HTML-like markup right in the function. That mix is called JSX (JavaScript XML). Files that use JSX typically end in .jsx. Chapter 3 covers JavaScript in more depth; for now, focus on the HTML-like markup inside each component.

TypeScript is a superset of JavaScript that adds static typing. It has become the preferred language for many React and Next.js projects because types catch mistakes early, make larger codebases easier to refactor, and improve editor tooling (autocomplete, jump-to-definition, and clearer errors). Next.js supports TypeScript out of the box, and it is what this course uses. Component files therefore end in .tsx. When people say "JSX" in conversation, they often mean the markup syntax itself, whether the file is .jsx or .tsx.

Before you create Lab 1, it helps to know how the browser address bar connects to files on disk. A URL path (for example /labs/lab1) is what the user opens. In Next.js we call that a route: a destination your app knows how to render. You do not register routes in a central config file for the basics of this course. Instead, the shape of folders under app/ defines the routes.

That folder-based system is the App Router — Next.js 's current routing model. It is part of the recommended defaults you accepted in create-next-app (the defaults line lists App Router; if you customized, you answered Yes to "Would you like to use App Router? (recommended)"). An older Next.js style put routes under a pages/ directory (the Pages Router). This course uses the App Router exclusively: look for an app/ folder, not pages/.

Inside app/, the filename page.tsx is reserved. When a folder contains page.tsx, Next.js exposes that folder as a public route. Nested folders become nested path segments:

  • app/page.tsx → / (the site root)
  • app/labs/page.tsx → /labs
  • app/labs/lab1/page.tsx → /labs/lab1

In the IDE Explorer, open the app directory and create Lab 1 at app/labs/lab1/page.tsx with the following source:

Lab1app/labs/lab1/page.tsx
export default function Lab1() {
  return (
    <div id="wd-lab1">
      <h2>Lab 1</h2>
    </div>
  );
}

Lab1 is a default-exported React component. Because the file is named page.tsx under app/labs/lab1/, the App Router registers the route /labs/lab1. The function returns a div (division element) that contains an h2 heading with the text "Lab 1". Save the file, keep npm run dev running, and open http://localhost:3000/labs/lab1 in the browser to confirm it renders.

Lab1app/labs/lab1/page.tsx

Lab 1

As you add HTML examples in the next sections, keep page.tsx as a thin page that imports smaller components (one file per exercise) instead of pasting everything into this one file. Those exercise files — for example HeadingTags.tsx later in Lab 1 — are components, not new routes. Only page.tsx creates a URL; other .tsx files are imported into the page when you need them. Lab 1 stays a single URL while its content grows.

The starter project also loads Tailwind CSS through app/globals.css. For the HTML exercises in this chapter we want the browser's default styling, so comment out that import in app/layout.tsx and leave the rest of the file alone:

RootLayoutapp/layout.tsx
import type { Metadata } from "next";
import { Geist, Geist_Mono } from "next/font/google";
// import "./globals.css";
// ... leave the rest of this file alone

Later chapters return to CSS and Tailwind when we start styling Kambaz and the labs more deliberately.

1.3 Introduction to HTML

SlidesVideos

HTML (HyperText Markup Language) is a specialized dialect of XML (eXtensible Markup Language) designed for structuring and formatting plain text so web browsers can interpret and render it with specific styles, layouts, and interactivity. In Next.js you write that HTML as JSX inside React components (in .tsx files — see §1.2.5). For this chapter, focus on the tags and structure; treat the surrounding component syntax as the container that delivers HTML to the browser. A coverage checklist is in §1.3.12 — use it after you have walked through the tags, not instead of building them as you read.

Consider the following snippet, which marks the text "Labs" as a level-1 heading so the browser typically shows it large and bold:

<h1>Labs</h1>

The browser shows "Labs" as a large, bold level-1 heading — the visual cue that this is a top-level title:

Labs

In that code, <h1> and </h1> are called tags. <h1> is the opening tag, and </h1> is the closing tag. The text "Labs" between them is the body (or content) of the tag. Tags add semantic meaning — for example, h1 signals a top-level heading, which browsers style with a larger font and bold weight by default.

When a browser parses this HTML (or JSX in a Next.js component), it builds an in-memory tree called the DOM (Document Object Model). Each tag becomes a node in that tree. The DOM is what the browser uses to paint the page, and JavaScript — including React in Next.js — can programmatically update nodes later without reloading the whole page.

We often say "tag" and "element" interchangeably, but there is a small distinction. A tag is the textual syntax in your source (for example <h1>), while an element is the fuller idea — the tag, optional attributes (settings on the opening tag — introduced with id in §1.3.1), its body, and the DOM node that results. Either word is fine day to day; the distinction mainly helps when you inspect the page in tools like Chrome DevTools.

1.3.1 Structuring Web Content with the HTML Heading, Div, and Span Tags

SlidesVideos

The <h1> through <h6> tags format section titles so they render larger and bolder than the plain text that follows. Text documents are often broken up into several sections and subsections, and each section is usually prefaced with a short title that summarizes the topic it precedes. <h1> is the largest heading; <h6> is the smallest.

Another common element is the <div> tag (division tag) — a generic container used to group elements together. Unlike headings, <div> does not add much inherent visual styling beyond behaving like a block. It starts on a new line and stretches as wide as its parent. Its job is grouping a heading, paragraph, and image as one unit so you can style or lay them out together later with CSS.

The opposite of a block is an inline element. Inline tags sit in the line of text without breaking to a new row — the way a word sits among other words. The generic inline container is the <span> tag. Later you will meet other inline tags such as a (links in §1.3.9) and many form controls. Headings, p, div, lists, and form are blocks; span, a, and strong are inline. Chapter 2 lets you change that default with the CSS display property (§2.1.12); for now, notice the difference in the heading example below — the div and h4 each start on their own line, while the span stays in its sentence.

For Lab 1, keep each HTML topic in its own component under app/labs/lab1/, then import those components into app/labs/lab1/page.tsx. Start with headings by creating app/labs/lab1/HeadingTags.tsx with a div that holds an h4 plus the explanatory text about heading tags:

HeadingTagsapp/labs/lab1/HeadingTags.tsx
export default function HeadingTags() {
  return (
    <div id="wd-h-tag">
      <h4>Heading Tags</h4>
      Text documents are often broken up into several sections and subsections.
      Each section is usually prefaced with a short title or heading that
      attempts to summarize the topic of the section it precedes. For instance
      this paragraph is preceded by the heading Heading Tags. The font of the
      section headings are usually larger and bolder than their subsection
      headings. This document uses headings to introduce topics such as HTML
      Documents, HTML Tags, Heading Tags, etc. HTML heading tags can be used
      to format plain text so that it renders in a browser as large headings.
      There are 6 heading tags for different sizes: h1, h2, h3, h4, h5, and
      h6. Tag h1 is the largest heading and h6 is the smallest heading. A{" "}
      <span id="wd-inline-span">span</span> sits in this sentence without
      starting a new line.
    </div>
  );
}

Notice id="wd-h-tag" on the opening <div>. That is an attribute — a name/value pair written inside the opening tag that configures the element. Here the name is id and the value is wd-h-tag. An id gives the element a unique name on the page — useful for styling, testing, and (later) linking to a spot in the document. You will see many more attributes as you add images, forms, and links. The pattern is always name="value" on the opening tag.

The h4 stands out above the body text, and the div keeps them grouped as one block — nothing flashy yet, just structure:

HeadingTagsapp/labs/lab1/HeadingTags.tsx

Heading Tags

Text documents are often broken up into several sections and subsections. Each section is usually prefaced with a short title or heading that attempts to summarize the topic of the section it precedes. For instance this paragraph is preceded by the heading Heading Tags. The font of the section headings are usually larger and bolder than their subsection headings. This document uses headings to introduce topics such as HTML Documents, HTML Tags, Heading Tags, etc. HTML heading tags can be used to format plain text so that it renders in a browser as large headings. There are 6 heading tags for different sizes: h1, h2, h3, h4, h5, and h6. Tag h1 is the largest heading and h6 is the smallest heading. A span sits in this sentence without starting a new line.

That markup is the book sample. Keep its text — the h4 titled Heading Tags and the paragraph under it — unless a step says to replace it. Do not erase the book sample every time you add something.

As practice, add h1 through h6 in the same wd-h-tag division, after the sample paragraph, so you can see the six sizes. Leave the sample h4 and its paragraph in place. Add this practice block before With AI. That later prompt refers to these practice headings. Your personal h4 in On your own stays in its own section:

<h1>h1</h1>
<h2>h2</h2>
<h3>h3</h3>
<h4>h4</h4>
<h5>h5</h5>
<h6>h6</h6>

Try Chrome DevTools. With Lab 1 open in Chrome, press F12 (or Cmd+Option+I on macOS / Ctrl+Shift+I on Windows) to open Developer Tools. Choose the Elements panel — that shows the live DOM tree the browser built from your JSX. Use Find (Cmd+F / Ctrl+F inside the panel) and search for wd-h-tag. You should land on the <div id="wd-h-tag"> node. Click it and glance at the styles and attributes on the right. This habit — find an id, inspect the node — is how you debug markup for the rest of the course.

Next, wire the component into Lab 1. In app/labs/lab1/page.tsx, add an h3 for "HTML Examples", import HeadingTags, and render it. The import line brings the default export from HeadingTags.tsx into this file so you can use <HeadingTags /> like an HTML tag:

Lab1app/labs/lab1/page.tsx
import HeadingTags from "./HeadingTags";

export default function Lab1() {
  return (
    <div id="wd-lab1">
      <h2>Lab 1</h2>
      <h3>HTML Examples</h3>
      <HeadingTags />
      {/* do the next exercise here */}
    </div>
  );
}

At /labs/lab1 you should see Lab 1, HTML Examples, and Heading Tags appear as successively smaller headings, the original sample paragraph still inside the wd-h-tag division, and the h1–h6 you added as practice after that paragraph. Leave the {/* do the next exercise here */} comment as a marker for the following sections — you will replace it by importing more components the same way.

1.3.2 Formatting Vertical Spacing with the HTML Paragraph Tag

Videos

The <p> tag wraps a block of text so the browser adds vertical space around it. Browsers ignore extra spaces, tabs, and newlines in your source, so without paragraph tags those blocks blend together. Create app/labs/lab1/ParagraphTag.tsx for this section; you will import it into Lab 1 once it is finished.

Without paragraph tags around later blocks, the browser treats the text as one contiguous stream that flows left to right and wraps only when it runs out of horizontal space. That is inline layout behavior. Even if you put blank lines between paragraphs in the source, those breaks are ignored and the text blends together:

ParagraphTagapp/labs/lab1/ParagraphTag.tsx
export default function ParagraphTag() {
  return (
    <div id="wd-p-tag">
      <h4>Paragraph Tag</h4>
      <p id="wd-p-1">...</p>
      This is the first paragraph. The paragraph tag is used to format
      vertical gaps between long pieces of text like this one.

      This is the second paragraph. Even though there is a deliberate white
      gap between the paragraph above and this paragraph, by default browsers
      render them as one contiguous piece of text as shown here on the right.

      This is the third paragraph. Wrap each paragraph with the paragraph tag
      to tell browsers to render the gaps.
    </div>
  );
}

Those blank lines in the source disappear in the browser — the three chunks blend into one continuous stream of text:

ParagraphTagapp/labs/lab1/ParagraphTag.tsx

Paragraph Tag

This is a paragraph. We often separate a long set of sentences with vertical spaces to make the text easier to read. Browsers ignore vertical white spaces and render all the text as one single set of sentences. To force the browser to add vertical spacing, wrap the paragraphs you want to separate with the paragraph tag

This is the first paragraph. The paragraph tag is used to format vertical gaps between long pieces of text like this one. This is the second paragraph. Even though there is a deliberate white gap between the paragraph above and this paragraph, by default browsers render them as one contiguous piece of text as shown here on the right. This is the third paragraph. Wrap each paragraph with the paragraph tag to tell browsers to render the gaps.

To get the intended vertical spacing, wrap each paragraph so the browser adds margin above and below. Paragraph and heading tags are block elements. They take the full width of their parent and add vertical space before and after their content. By combining inline and block layout, you can build clearer document structures.

ParagraphTagapp/labs/lab1/ParagraphTag.tsx
export default function ParagraphTag() {
  return (
    <div id="wd-p-tag">
      <h4>Paragraph Tag</h4>
      <p id="wd-p-1">
        This is a paragraph. We often separate a long set of sentences with
        vertical spaces to make the text easier to read. Browsers ignore
        vertical white spaces and render all the text as one single set of
        sentences. To force the browser to add vertical spacing, wrap the
        paragraphs you want to separate with the paragraph tag
      </p>
      <p id="wd-p-2">
        This is the first paragraph. The paragraph tag is used to format
        vertical gaps between long pieces of text like this one.
      </p>
      <p id="wd-p-3">
        This is the second paragraph. Even though there is a deliberate white
        gap between the paragraph above and this paragraph, by default
        browsers render them as one contiguous piece of text as shown here on
        the right.
      </p>
      <p id="wd-p-4">
        This is the third paragraph. Wrap each paragraph with the paragraph
        tag to tell browsers to render the gaps.
      </p>
    </div>
  );
}

With each block wrapped in <p>, the browser adds vertical gaps so the three paragraphs read as separate units:

ParagraphTagapp/labs/lab1/ParagraphTag.tsx

Paragraph Tag

This is a paragraph. We often separate a long set of sentences with vertical spaces to make the text easier to read. Browsers ignore vertical white spaces and render all the text as one single set of sentences. To force the browser to add vertical spacing, wrap the paragraphs you want to separate with the paragraph tag

This is the first paragraph. The paragraph tag is used to format vertical gaps between long pieces of text like this one.

This is the second paragraph. Even though there is a deliberate white gap between the paragraph above and this paragraph, by default browsers render them as one contiguous piece of text as shown here on the right.

This is the third paragraph. Wrap each paragraph with the paragraph tag to tell browsers to render the gaps.

Now import ParagraphTag into Lab 1 the same way you imported HeadingTags:

Lab1app/labs/lab1/page.tsx
import HeadingTags from "./HeadingTags";
import ParagraphTag from "./ParagraphTag";

export default function Lab1() {
  return (
    <div id="wd-lab1">
      <h2>Lab 1</h2>
      <h3>HTML Examples</h3>
      <HeadingTags />
      <ParagraphTag />
      {/* do the next exercise here */}
    </div>
  );
}

1.3.3 Listing Content with HTML List Tags

SlidesVideos

The <ol>, <ul>, and <li> tags group related items into a collection the reader can scan as a unit — steps in a recipe, titles on a shelf, anything that belongs together rather than in a free-flowing paragraph. There are two primary types: ordered and unordered. Ordered lists (<ol>) are for sequences where order matters — for example, procedural steps. Unordered lists (<ul>) are for collections where order does not change the meaning; the browser uses bullets instead of numbers. Each item in either list is wrapped in a <li>. If you type numbered lines without list tags, the browser still treats them as ordinary text and blends them into one paragraph, just like unwrapped paragraphs earlier.

Create ListTags.tsx and import it into page.tsx the same way. First write the pancake steps below as plain text (no <ol> yet):

ListTagsapp/labs/lab1/ListTags.tsx
export default function ListTags() {
  return (
    <div id="wd-lists">
      <h4>List Tags</h4>
      <h5>Ordered List Tag</h5>
      How to make pancakes:
      1. Mix dry ingredients.
      2. Add wet ingredients.
      3. Stir to combine.
      4. Heat a skillet or griddle.
      5. Pour batter onto the skillet.
      6. Cook until bubbly on top.
      7. Flip and cook the other side.
      8. Serve and enjoy!
    </div>
  );
}

The numbered steps do not look like a list — they run together on one flowing line, the same way unwrapped paragraphs did:

ListTagsapp/labs/lab1/ListTags.tsx

List Tags

Ordered List Tag
How to make pancakes: 1. Mix dry ingredients. 2. Add wet ingredients. 3. Stir to combine. 4. Heat a skillet or griddle. 5. Pour batter onto the skillet. 6. Cook until bubbly on top. 7. Flip and cook the other side. 8. Serve and enjoy!

The intended formatting can be achieved by wrapping the whole list in <ol> / </ol>, and each step in <li> / </li>. The browser numbers the items for you, so the sequence stays correct even if you add or remove steps. You might want to remove the unnecessary numbers added earlier.

ListTagsapp/labs/lab1/ListTags.tsx
export default function ListTags() {
  return (
    <div id="wd-lists">
      <h4>List Tags</h4>
      <h5>Ordered List Tag</h5>
      How to make pancakes:
      <ol id="wd-pancakes">
        <li>Mix dry ingredients.</li>
        <li>Add wet ingredients.</li>
        <li>Stir to combine.</li>
        <li>Heat a skillet or griddle.</li>
        <li>Pour batter onto the skillet.</li>
        <li>Cook until bubbly on top.</li>
        <li>Flip and cook the other side.</li>
        <li>Serve and enjoy!</li>
      </ol>
    </div>
  );
}

Both <ol> and <li> are block elements, so items stack vertically across the width of their container. Now each step sits on its own line with automatic numbering:

ListTagsapp/labs/lab1/ListTags.tsx

List Tags

Ordered List Tag
How to make pancakes:
  1. Mix dry ingredients.
  2. Add wet ingredients.
  3. Stir to combine.
  4. Heat a skillet or griddle.
  5. Pour batter onto the skillet.
  6. Cook until bubbly on top.
  7. Flip and cook the other side.
  8. Serve and enjoy!

Unordered lists use the same <li> items inside <ul> instead of <ol>. <ul> is a block element too. Continue in the same ListTags.tsx file. After the pancake list, add an unordered list of my favorite books:

ListTagsapp/labs/lab1/ListTags.tsx
How to make pancakes:
<ol id="wd-pancakes">
  {/* pancake steps */}
</ol>
<h5>Unordered List Tag</h5>
My favorite books (in no particular order)
<ul id="wd-my-books">
  <li>Dune</li>
  <li>Lord of the Rings</li>
  <li>Ender&apos;s Game</li>
  <li>Red Mars</li>
  <li>The Forever War</li>
</ul>

The books appear as a bulleted list — no numbers, and no blending into a single paragraph:

ListTagsapp/labs/lab1/ListTags.tsx

List Tags

Ordered List Tag
How to make pancakes:
  1. Mix dry ingredients.
  2. Add wet ingredients.
  3. Stir to combine.
  4. Heat a skillet or griddle.
  5. Pour batter onto the skillet.
  6. Cook until bubbly on top.
  7. Flip and cook the other side.
  8. Serve and enjoy!
Unordered List Tag
My favorite books (in no particular order)
  • Dune
  • Lord of the Rings
  • Ender's Game
  • Red Mars
  • The Forever War
ListTagsapp/labs/lab1/ListTags.tsx

List Tags

Ordered List Tag
How to make pancakes:
  1. Mix dry ingredients.
  2. Add wet ingredients.
  3. Stir to combine.
  4. Heat a skillet or griddle.
  5. Pour batter onto the skillet.
  6. Cook until bubbly on top.
  7. Flip and cook the other side.
  8. Serve and enjoy!
My favorite recipe:
  1. Boil water and cook pasta until al dente.
  2. Sauté garlic in olive oil, then add crushed tomatoes.
  3. Toss pasta with sauce and top with grated Parmesan.
Unordered List Tag
My favorite books (in no particular order)
  • Dune
  • Lord of the Rings
  • Ender's Game
  • Red Mars
  • The Forever War
Your favorite books (in no particular order)
  • The Pragmatic Programmer
  • Clean Code
  • Designing Data-Intensive Applications

1.3.4 Tabulating Data with the HTML Table Tags

The <table> tag organizes data into rows and columns. HTML began as a way to share scientific documents among physicists, and those documents often included structured measurements — speed, temperature, location — presented in tabular form, which is why the tag was added in the mid-1990s. For example, quiz grades over a semester can look like this:

QuizTopicDateGrade
Q1HTML2/3/2185
Q2CSS2/10/2190
Q3JavaScript2/17/2195
Average90

Several things to note:

  1. The first row is formatted as headings for each column.
  2. There are data rows — one quiz per row.
  3. Data under the same column shares the same kind of value, e.g. name, date, grade, etc.
  4. The last row is formatted as a footer.
  5. The first three columns of the footer are merged into one cell (cells can span to adjacent rows or columns).

HTML tables use nested tags:

  • table — declares the table
  • thead — heading section
  • tbody — main data rows
  • tfoot — footer section
  • tr — a row
  • th — a heading cell
  • td — a data cell

Those tags define structure. Presentational attributes on the table and its cells control size, borders, merging, and alignment — useful now, before CSS in Chapter 2. In JSX some names are camelCase because they map to DOM properties:

  • border — on table. Pixel thickness of the grid. Use 0 for none, 1 for a thin line (border={1}). Larger integers draw thicker lines.
  • width — on table, td, or th. Size as a percentage of the parent ("100%", "25%") or in pixels ("200"). width="100%" stretches the table across its container.
  • colSpan — on td or th (HTML colspan). How many columns this cell occupies. A positive integer; default is 1. The Average row uses colSpan={3} so the label covers Quiz, Topic, and Date.
  • rowSpan — on td or th (HTML rowspan). How many rows this cell occupies. A positive integer; default is 1. Use it when one label should sit beside several rows — for example a unit that covers Q1–Q3.
  • align — on td, th, or tr. Horizontal placement of the cell's content: "left", "center", "right" (also "justify"). Default for data cells is left; heading cells often default to center. Numbers read more clearly when right-aligned; short labels and dates often sit in the center.
  • valign — on td, th, or tr. Vertical placement when a row is taller than its content: "top", "middle", "bottom" (also "baseline"). Default is middle. Layout tables later use valign="top" so navigation stays at the top of a tall cell.

Put them on the quiz table: full width, a visible border, Average spanning three columns, Topic and Date centered, grade numbers right-aligned. Create Tables.tsx, import it into page.tsx, and include Q1–Q3 with sample dates and scores:

Tablesapp/labs/lab1/Tables.tsx
export default function Tables() {
  return (
    <div id="wd-tables">
      <h4>Table Tag</h4>
      <table border={1} width="100%">
        <thead>
          <tr>
            <th>Quiz</th>
            <th align="center">Topic</th>
            <th align="center">Date</th>
            <th>Grade</th>
          </tr>
        </thead>
        <tbody>
          <tr>
            <td>Q1</td>
            <td align="center">HTML</td>
            <td align="center">2/3/21</td>
            <td align="right">85</td>
          </tr>
          <tr>
            <td>Q2</td>
            <td align="center">CSS</td>
            <td align="center">2/10/21</td>
            <td align="right">90</td>
          </tr>
          <tr>
            <td>Q3</td>
            <td align="center">JavaScript</td>
            <td align="center">2/17/21</td>
            <td align="right">95</td>
          </tr>
        </tbody>
        <tfoot>
          <tr>
            <td colSpan={3}>Average</td>
            <td align="right">90</td>
          </tr>
        </tfoot>
      </table>
    </div>
  );
}

Apply the same align on every Topic, Date, and grade number through Q3. Leave the Grade heading with the default alignment. Quiz stays left; Average stays left under the merged cells; the average number lines up with the scores:

Tablesapp/labs/lab1/Tables.tsx

Table Tag

QuizTopicDateGrade
Q1HTML2/3/2185
Q2CSS2/10/2190
Q3JavaScript2/17/2195
Average90

1.3.5 Image Tag

The <img> tag places pictures in an HTML document, whether they live at a remote URL on the internet or as files in your project. Attributes on the tag change what and how the picture displays. The src attribute points at the file, either a remote URL or a local path. The optional attributes width and height set the display size; if you provide only one, the other scales proportionally. alt holds a short text description when the image cannot load. That description matters for accessibility: it helps people and tools that cannot see the picture understand what it shows.

<img> is a void element: it has no body and no closing tag in HTML. In JSX you write it as a self-closing tag ending in />. The same pattern applies to other void elements you will meet next, including <br /> (a line break) and <input />. Void elements cannot wrap children, so attributes carry all of their configuration.

<img
  src="my-picture.jpg"
  width="200px"
  height="300px"
/>
{/* src references a local or remote image.
    width / height configure size; one alone scales the other */}

Remote images use absolute URLs, while local images in Next.js usually go under public/images and are referenced from the site root (for example /images/teslabot.jpg). Download a Tesla Bot picture and save it as public/images/teslabot.jpg, then create Images.tsx, import it into page.tsx, and add both a remote Starship image and the local bot:

Imagesapp/labs/lab1/Images.tsx
export default function Images() {
  return (
    <div id="wd-images">
      <h4>Image tag</h4>
      Loading an image from the internet:
      <br />
      <img
        id="wd-starship"
        width="400px"
        alt="Starship"
        src="https://www.staradvertiser.com/wp-content/uploads/2021/08/web1_Starship-gap2.jpg"
      />
      <br />
      Loading a local image:
      <br />
      <img
        id="wd-teslabot"
        src="/images/teslabot.jpg"
        height="200px"
        alt="Tesla Bot (Optimus) humanoid robot"
      />
    </div>
  );
}

Two images stack in the page flow, Starship from a remote URL, then Tesla Bot from /images/teslabot.jpg in your public folder:

Imagesapp/labs/lab1/Images.tsx

Image tag

Loading an image from the internet:
Starship
Loading a local image:
Tesla Bot (Optimus) humanoid robot

1.3.6 Creating Web Forms

SlidesVideos

The <form> tag is different from the static markup so far — headings, paragraphs, lists, tables, and images that the browser displays. Forms let users interact with the interface, in particular to enter data such as a username, password, biography, or preferred options. That input is what applications later validate, store, or send to a server for long term storage and later retrieval.

The following sections present several kinds of input, each in turn: text fields, multi-line text areas, radio buttons, checkboxes, dropdowns, typed inputs such as email, number, range, and date, and buttons that save or cancel. A <form> wraps those controls — <input>, <textarea>, <select>, <button>, and related options — so users can fill in and submit information as one unit.

Until now each Lab 1 topic was a single file next to page.tsx. Forms is a cluster of related components — enough that dumping them all in app/labs/lab1/ would bury the rest of the lab. Put the form work in its own folder, app/labs/lab1/forms/, so related files sit together and the lab folder stays easy to browse. That folder is only for organization: do not add a page.tsx inside it, or Next.js would create a /labs/lab1/forms route. Inside the folder, files still import each other the same way (import TextFields from "./TextFields"). From page.tsx you reach one level down: import Forms from "./forms/Forms".

Each form topic lives in its own file under that folder — for example TextFields.tsx, Textarea.tsx, and so on.Forms.tsx assembles them, and page.tsx imports Forms.

1.3.6.1 Text Fields Videos

The <input> tag is the most common way to collect short strings such as usernames, passwords, names, and similar values.

In React, form fields come in two flavors. An uncontrolled input gets an initial value and then the browser owns what the user types — React does not track every keystroke — while a controlled input is the opposite: React state holds the current text, and every change goes through onChange so your code stays in sync. For these early HTML labs, stay uncontrolled with defaultValue; controlled inputs return when you learn state in later chapters — if you set value without that state wiring, the field looks frozen because React keeps forcing the same text back onto the input. Later chapters discuss controlled inputs in detail.

Useful attributes include:

  • id — a unique identifier for the field so CSS, JavaScript, and other elements can refer to it.
  • type — kind of input such as text, password, email, date, number, range, etc. The default is text so if you want a simple single-line text, omit it (or use type="text"). Use type="password" to mask characters as the user types. Later sections introduce other types such as email and date.
  • placeholder — hint text shown inside an empty field (for example jdoe). It disappears when the user starts typing and is not submitted as the field's value.
  • title — advisory text, often shown as a tooltip on hover. Useful for extra guidance beyond the caption next to the field.
  • defaultValue — initial text for an uncontrolled field; the user can edit freely afterward. Prefer this for this chapter.
  • value — current text for a controlled field (paired with state and onChange). Skip it for now.

Beside each field, add a <label> element for the visible caption (for example "Username:"). Give the label a htmlFor attribute whose value matches the input's id. That links the two. Clicking the label focuses the field, and screen readers can announce the connection. In plain HTML the attribute is named for; in JSX it must be htmlFor because for is a reserved word in JavaScript.

Associating labels this way is good practice because it enlarges the clickable area — especially on small mobile screens, where tapping the label text is often easier than hitting a narrow input. Later examples with radio buttons and checkboxes make the point even clearer. Their controls are tiny, so being able to tap the adjacent label text is a real usability win.

Create app/labs/lab1/forms/TextFields.tsx with labeled text inputs for username, password, and name fields. The component returns a fragment (<>…</>) — a React wrapper that groups several sibling tags without adding an extra <div> to the DOM:

TextFieldsapp/labs/lab1/forms/TextFields.tsx
export default function TextFields() {
  return (
    <>
      <h5>Text Fields</h5>
      <label htmlFor="wd-text-fields-username">Username:</label>
      <input placeholder="jdoe" id="wd-text-fields-username" /> <br />
      <label htmlFor="wd-text-fields-password">Password:</label>
      <input
        type="password"
        defaultValue="123@#$asd"
        id="wd-text-fields-password"
      />
      <br />
      <label htmlFor="wd-text-fields-first-name">First name:</label>
      <input type="text" title="John" id="wd-text-fields-first-name" />{" "}
      <br />
      <label htmlFor="wd-text-fields-last-name">Last name:</label>
      <input
        type="text"
        placeholder="Doe"
        defaultValue="Wonderland"
        title="The last name"
        id="wd-text-fields-last-name"
      />
    </>
  );
}

Try clicking a label to focus its field, hover for title tooltips, type in the Username field and notice the placeholder text disappears, and notice the password hides its value while last name shows the default value Wonderland:

TextFieldsapp/labs/lab1/forms/TextFields.tsx
Text Fields



Wire TextFields into a new app/labs/lab1/forms/Forms.tsx that wraps everything in #wd-forms and a <form id="wd-text-fields">. Leave room for the later form components:

Formsapp/labs/lab1/forms/Forms.tsx
import TextFields from "./TextFields";

export default function Forms() {
  return (
    <div id="wd-forms">
      <h4>Form Elements</h4>
      <form id="wd-text-fields">
        <TextFields />
        {/* add the next form components here */}
      </form>
    </div>
  );
}

Then import Forms into page.tsx from the new folder — import Forms from "./forms/Forms" — not the same one-level ./HeadingTags path as the earlier Lab 1 components.

1.3.6.2 Textarea

Use <textarea> for longer multi-line text such as a biography. Unlike the void <input> (§1.3.5 /§1.3.6.1), a textarea is not void in HTML — it has an opening and closing tag, and the browser uses the text between those tags as the starting value:

<textarea id="wd-textarea" cols="30" rows="10">
Lorem ipsum dolor sit amet...
</textarea>

That HTML pattern does not carry over to JSX. React models every form field the same way it models <input>, so the current text is a prop (defaultValue or value), not child nodes. Through React 18, body text still rendered and you only got a development warning; React 19, which this course uses, throws: Use the defaultValue or value props instead of setting children on <textarea>. Put the biography on defaultValue instead — the same idea as on <input>, and what Lab 1 uses.

Create Textarea.tsx in the same forms folder, with the cols and rows attributes for the visible size:

Textareaapp/labs/lab1/forms/Textarea.tsx
export default function Textarea() {
  return (
    <>
      <h5>Text boxes</h5>
      <label>Biography:</label>
      <br />
      <textarea
        id="wd-textarea"
        cols={30}
        rows={10}
        defaultValue="Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum."
      />
    </>
  );
}

A multi-line box appears with room for longer text — taller and wider than a single-line input, sized by rows and cols:

Textareaapp/labs/lab1/forms/Textarea.tsx
Text boxes

Import Textarea into Forms.tsx the same way you imported TextFields, and place <Textarea /> inside the form.

1.3.6.3 Radio Buttons Videos

The <input type="radio"> tag lets the user pick one option from a set, so choices in the same group are mutually exclusive — selecting the Comedy option clears the Drama option. The browser groups radios that share the same name attribute: give each option its own id (and usually a value when you submit the form later), but reuse one name for the whole group.

Create RadioButtons.tsx in forms with a favorite-genre radio group. Here each caption sits next to its input and uses htmlFor to match the input's id:

RadioButtonsapp/labs/lab1/forms/RadioButtons.tsx
export default function RadioButtons() {
  return (
    <>
      <h5 id="wd-radio-buttons">Radio buttons</h5>
      <label>Favorite movie genre:</label>
      <br />
      <input type="radio" name="radio-genre" id="wd-radio-comedy" />
      <label htmlFor="wd-radio-comedy">Comedy</label>
      <br />
      <input type="radio" name="radio-genre" id="wd-radio-drama" />
      <label htmlFor="wd-radio-drama">Drama</label>
      <br />
      <input type="radio" name="radio-genre" id="wd-radio-scifi" />
      <label htmlFor="wd-radio-scifi">Science Fiction</label>
      <br />
      <input type="radio" name="radio-genre" id="wd-radio-fantasy" />
      <label htmlFor="wd-radio-fantasy">Fantasy</label>
    </>
  );
}

Four genre radios share one name, so picking Comedy clears Drama — only one choice stays selected:

RadioButtonsapp/labs/lab1/forms/RadioButtons.tsx
Radio buttons




Import RadioButtons into Forms.tsx and add <RadioButtons /> inside the form.

Mutual exclusion applies within a name group, not across the whole page. Add a second independent choice to the same file — for example how often someone watches movies — with a different name for the second set. Selecting Weekly does not clear Comedy, because radio-frequency and radio-genre are separate groups:

RadioButtonsapp/labs/lab1/forms/RadioButtons.tsx
export default function RadioButtons() {
  return (
    <>
      <h5 id="wd-radio-buttons">Radio buttons</h5>
      <label>Favorite movie genre:</label>
      <br />
      <input type="radio" name="radio-genre" id="wd-radio-comedy" />
      <label htmlFor="wd-radio-comedy">Comedy</label>
      <br />
      <input type="radio" name="radio-genre" id="wd-radio-drama" />
      <label htmlFor="wd-radio-drama">Drama</label>
      <br />
      <input type="radio" name="radio-genre" id="wd-radio-scifi" />
      <label htmlFor="wd-radio-scifi">Science Fiction</label>
      <br />
      <input type="radio" name="radio-genre" id="wd-radio-fantasy" />
      <label htmlFor="wd-radio-fantasy">Fantasy</label>
      <br />
      <label>How often do you watch movies?</label>
      <br />
      <input type="radio" name="radio-frequency" id="wd-radio-daily" />
      <label htmlFor="wd-radio-daily">Daily</label>
      <br />
      <input type="radio" name="radio-frequency" id="wd-radio-weekly" />
      <label htmlFor="wd-radio-weekly">Weekly</label>
      <br />
      <input type="radio" name="radio-frequency" id="wd-radio-rarely" />
      <label htmlFor="wd-radio-rarely">Rarely</label>
    </>
  );
}

Two groups, two names: pick a genre and a frequency and both selections stay selected at once:

RadioButtonsapp/labs/lab1/forms/RadioButtons.tsx
Radio buttons








Labels work with radios in two common ways. You already used the first: keep the <input> and <label> as siblings and connect them with htmlFor / id. Alternatively, wrap the input inside the label — then you can omit htmlFor and id, because nesting creates the association. Wrapping is compact; separate labels are more flexible when the caption and control are not next to each other (for example labels in one column and radios in another). Clicking either the text or the circle still selects the option. Illustrative — optional to add to your project:

RadioLabelPatterns
{/* Sibling label + htmlFor */}
<input type="radio" name="radio-beside" id="wd-radio-beside-yes" />
<label htmlFor="wd-radio-beside-yes">Yes</label>

{/* Wrapping label — no htmlFor needed */}
<label>
  <input type="radio" name="radio-wrap" /> Yes
</label>

{/* Separate placement still works with htmlFor */}
<label htmlFor="wd-radio-distant-a">Option A</label>
{/* ... elsewhere in the layout ... */}
<input type="radio" name="radio-distant" id="wd-radio-distant-a" />

Same radios, three label styles — click the label text in each row and the matching radio should select:

RadioLabelPatterns
Label next to the input (uses htmlFor)

Label wrapping the input (no htmlFor needed)

Separate label and input (not side by side)

With htmlFor, the caption and control do not have to sit next to each other:

For Lab 1, keep both groups in RadioButtons.tsx with sibling labels and htmlFor. The other label patterns are here so you understand how grouping and labeling work when you design denser layouts later.

1.3.6.4 Checkboxes Videos

The <input type="checkbox"> tag uses the same label pattern as radio buttons, but each box can be selected independently. Unlike radios, choosing Comedy does not clear Drama — the user can pick several genres at once. Create Checkboxes.tsx:

Checkboxesapp/labs/lab1/forms/Checkboxes.tsx
export default function Checkboxes() {
  return (
    <>
      <h5 id="wd-checkboxes">Checkboxes</h5>
      <label>Favorite movie genre:</label>
      <br />
      <input type="checkbox" name="check-genre" id="wd-chkbox-comedy" />
      <label htmlFor="wd-chkbox-comedy">Comedy</label>
      <br />
      <input type="checkbox" name="check-genre" id="wd-chkbox-drama" />
      <label htmlFor="wd-chkbox-drama">Drama</label>
      <br />
      <input type="checkbox" name="check-genre" id="wd-chkbox-scifi" />
      <label htmlFor="wd-chkbox-scifi">Science Fiction</label>
      <br />
      <input type="checkbox" name="check-genre" id="wd-chkbox-fantasy" />
      <label htmlFor="wd-chkbox-fantasy">Fantasy</label>
    </>
  );
}

Unlike radios, each box is independent — Comedy and Drama can both stay checked:

Checkboxesapp/labs/lab1/forms/Checkboxes.tsx
Checkboxes




Import Checkboxes into Forms.tsx and add <Checkboxes /> inside the form.

1.3.6.5 Dropdowns Videos

Use <select> when the user can pick from a fixed list of choices. Each choice is rendered as an <option> inside that list. The text between the option tags is what people see (for example "Science Fiction"); the value attribute is what the form actually records when that option is selected and is what would be sent to the server.

Option values are typically short tokens — stable identifiers that match something in a database, an API, or application logic — rather than the display label. They are often written in capitals (for example SCIFI, COMEDY) so they read clearly as codes, not prose. The visible label can stay human-friendly and even change later without breaking stored data.

On the <select> itself, the defaultValue attribute sets the initial selection (uncontrolled, as with text fields). That value must match one of the option values — for example defaultValue="SCIFI" selects the Science Fiction option. Prefer defaultValue here; controlled value + state comes later.

Start Dropdowns.tsx with a single-choice list:

Dropdownsapp/labs/lab1/forms/Dropdowns.tsx
export default function Dropdowns() {
  return (
    <>
      <h4 id="wd-dropdowns">Dropdowns</h4>
      <h5>Select one</h5>
      <label htmlFor="wd-select-one-genre">Favorite movie genre: </label>
      <br />
      <select id="wd-select-one-genre" defaultValue="SCIFI">
        <option value="COMEDY">Comedy</option>
        <option value="DRAMA">Drama</option>
        <option value="SCIFI">Science Fiction</option>
        <option value="FANTASY">Fantasy</option>
      </select>
    </>
  );
}

A compact dropdown opens to the genre list; Science Fiction starts selected because defaultValue matches SCIFI:

Dropdownsapp/labs/lab1/forms/Dropdowns.tsx

Dropdowns

Select one

Sometimes the user needs more than one selection from the same list, which you enable by adding the multiple attribute to the <select>. Then defaultValue (or value) can be an array of tokens — for example defaultValue={["COMEDY", "SCIFI"]} — so more than one option starts selected.

In the browser, use Shift to select a contiguous range of options, and Command (macOS) or Control (Windows / Linux) to add or remove individual options without clearing the rest.

Extend Dropdowns.tsx with a second list for selecting many genres:

Dropdownsapp/labs/lab1/forms/Dropdowns.tsx
export default function Dropdowns() {
  return (
    <>
      <h4 id="wd-dropdowns">Dropdowns</h4>
      <h5>Select one</h5>
      <label htmlFor="wd-select-one-genre">Favorite movie genre: </label>
      <br />
      <select id="wd-select-one-genre" defaultValue="SCIFI">
        <option value="COMEDY">Comedy</option>
        <option value="DRAMA">Drama</option>
        <option value="SCIFI">Science Fiction</option>
        <option value="FANTASY">Fantasy</option>
      </select>
      <h5>Select many</h5>
      <label htmlFor="wd-select-many-genre">Favorite movie genres: </label>
      <br />
      <select
        multiple
        id="wd-select-many-genre"
        defaultValue={["COMEDY", "SCIFI"]}
      >
        <option value="COMEDY">Comedy</option>
        <option value="DRAMA">Drama</option>
        <option value="SCIFI">Science Fiction</option>
        <option value="FANTASY">Fantasy</option>
      </select>
    </>
  );
}

The multi list shows several options at once, with Comedy and Science Fiction preselected. Shift- and Command-/Control-click to change the set:

Dropdownsapp/labs/lab1/forms/Dropdowns.tsx

Dropdowns

Select one

Select many

Import Dropdowns into Forms.tsx and add <Dropdowns /> inside the form.

1.3.6.6 Other Field Types Videos

Plain type="text" accepts almost any string. HTML also provides strongly typed input types that expect a particular kind of data — email addresses, numbers, dates, and more. Prefer these when you can. They nudge users toward valid input (and on phones often show a specialized keyboard), help the browser validate before submit, and reduce mistakes you would otherwise catch only in JavaScript later.

Build them one at a time in OtherFieldTypes.tsx, then import that component into Forms.tsx.

Email

Use type="email" for email addresses. Browsers check for a basic address shape (something like name@domain), and on many mobile devices the on-screen keyboard emphasizes @ and . so typing an address is easier than with a full text keyboard.

OtherFieldTypesapp/labs/lab1/forms/OtherFieldTypes.tsx
export default function OtherFieldTypes() {
  return (
    <>
      <h4>Other HTML field types</h4>
      <label htmlFor="wd-text-fields-email">Email: </label>
      <input
        type="email"
        placeholder="jdoe@somewhere.com"
        id="wd-text-fields-email"
      />
      <br />
    </>
  );
}

An email field with a sample placeholder — on a phone (or emulator), tap it and notice the keyboard emphasizes @ and .:

OtherFieldTypesapp/labs/lab1/forms/OtherFieldTypes.tsx

Other HTML field types


Number

Use type="number" for numeric values such as a salary. On many mobiles the field brings up a numeric keypad. The optional attributes min and max limit the allowed range (for example min={0} to reject negative salaries), and some browsers also provide step buttons for incrementing the value.

OtherFieldTypesapp/labs/lab1/forms/OtherFieldTypes.tsx
export default function OtherFieldTypes() {
  return (
    <>
      <h4>Other HTML field types</h4>
      <label htmlFor="wd-text-fields-email">Email: </label>
      <input
        type="email"
        placeholder="jdoe@somewhere.com"
        id="wd-text-fields-email"
      />
      <br />
      <label htmlFor="wd-text-fields-salary-start">Starting salary: </label>
      <input
        type="number"
        defaultValue="100000"
        placeholder="1000"
        min={0}
        id="wd-text-fields-salary-start"
      />
      <br />
    </>
  );
}

Email plus a number field for starting salary — steppers or a numeric keypad often appear, and min={0} blocks negatives:

OtherFieldTypesapp/labs/lab1/forms/OtherFieldTypes.tsx

Other HTML field types



Range

Use type="range" for a value chosen along a continuum — ratings, volume, or similar. It renders as a slider. Pair it with min, max, and often defaultValue (or value) so the thumb starts in a sensible place. Here a rating runs from 1 to 5 with a default of 4.

OtherFieldTypesapp/labs/lab1/forms/OtherFieldTypes.tsx
export default function OtherFieldTypes() {
  return (
    <>
      <h4>Other HTML field types</h4>
      {/* ... email and number fields ... */}
      <label htmlFor="wd-text-fields-rating">Rating: </label>
      <input
        type="range"
        defaultValue="4"
        min="1"
        max="5"
        id="wd-text-fields-rating"
      />
      <br />
    </>
  );
}

A slider joins the typed fields — drag it and it stays between 1 and 5:

OtherFieldTypesapp/labs/lab1/forms/OtherFieldTypes.tsx

Other HTML field types




Date

Use type="date" for calendar dates. The value uses a fixed YYYY-MM-DD format (year-month-day), even if the browser displays dates according to the user's locale. On many phones, tapping the field opens a native date picker rather than a text keyboard — another reason typed fields reduce entry mistakes. min and max (also in YYYY-MM-DD form) restrict which dates are allowed.

OtherFieldTypesapp/labs/lab1/forms/OtherFieldTypes.tsx
export default function OtherFieldTypes() {
  return (
    <>
      <h4>Other HTML field types</h4>
      <label htmlFor="wd-text-fields-email">Email: </label>
      <input
        type="email"
        placeholder="jdoe@somewhere.com"
        id="wd-text-fields-email"
      />
      <br />
      <label htmlFor="wd-text-fields-salary-start">Starting salary: </label>
      <input
        type="number"
        defaultValue="100000"
        placeholder="1000"
        min={0}
        id="wd-text-fields-salary-start"
      />
      <br />
      <label htmlFor="wd-text-fields-rating">Rating: </label>
      <input
        type="range"
        defaultValue="4"
        min="1"
        max="5"
        id="wd-text-fields-rating"
      />
      <br />
      <label htmlFor="wd-text-fields-dob">Date of birth: </label>
      <input
        type="date"
        defaultValue="2000-01-21"
        min="1900-01-01"
        max="2025-12-31"
        id="wd-text-fields-dob"
      />
      <br />
    </>
  );
}

The full set includes a date of birth control — open it and try a day outside the min/max range if your browser enforces those bounds:

OtherFieldTypesapp/labs/lab1/forms/OtherFieldTypes.tsx

Other HTML field types





HTML defines many more input types than this lab covers. Explore these on your own when you need them:

  • tel — telephone numbers (mobile phone keypad)
  • url — web addresses
  • search — search boxes (often with clear affordances)
  • time, datetime-local, month, week — other date/time pickers
  • color — color picker
  • file — file upload
  • hidden — values included on submit but not shown

MDN's documentation for the <input> element is a good place to browse the full list and try examples in the browser.

Import OtherFieldTypes into Forms.tsx and add <OtherFieldTypes /> inside the form. Buttons come next, then the completed Forms.tsx.

1.3.6.7 Buttons Videos

The <button> tag gives a form a way to submit or cancel. Prefer it over <input type="submit"> or <input type="button">, because the label is nested text (so later you can put an icon inside) and the type attribute says what the click should do.

Always write type explicitly. Inside a <form>, a <button> with no type defaults to submit and the browser sends the form — which reloads the page unless you stop it. Use type="submit" for Save (or Sign in). Use type="button" for Cancel and for every Kambaz action that is not sending a form — Go, Publish, + Assignment, and the rest in §1.4.3.

Create Buttons.tsx with both kinds:

Buttonsapp/labs/lab1/forms/Buttons.tsx
export default function Buttons() {
  return (
    <>
      <h4>Buttons</h4>
      <button id="wd-html-button-save" type="submit">
        Save
      </button>
      <button id="wd-html-button-cancel" type="button">
        Cancel
      </button>
    </>
  );
}

Save submits the form; Cancel does not:

Buttonsapp/labs/lab1/forms/Buttons.tsx

Buttons

Import Buttons into Forms.tsx. Clicking Save would reload Lab 1 (and this book page) unless the form stops that default. The onSubmit handler below is a sneak preview of event handling from later chapters: event.preventDefault() keeps you on the page so the live demo is safe to click. Because the handler is a function, the file must start with "use client" — Server Components cannot pass event handlers. Chapter 3 explains that directive; for now it is the switch that makes Save work without a reload. When all field components are wired in, Forms.tsx should look like this:

Formsapp/labs/lab1/forms/Forms.tsx
"use client";

import TextFields from "./TextFields";
import Textarea from "./Textarea";
import RadioButtons from "./RadioButtons";
import Checkboxes from "./Checkboxes";
import Dropdowns from "./Dropdowns";
import OtherFieldTypes from "./OtherFieldTypes";
import Buttons from "./Buttons";

export default function Forms() {
  return (
    <div id="wd-forms">
      <h4>Form Elements</h4>
      <form
        id="wd-text-fields"
        onSubmit={(event) => {
          event.preventDefault();
        }}
      >
        <TextFields />
        <Textarea />
        <RadioButtons />
        <Checkboxes />
        <Dropdowns />
        <OtherFieldTypes />
        <Buttons />
      </form>
    </div>
  );
}

All the field components assemble into one form — text, textarea, radios, checkboxes, dropdowns, typed inputs, and buttons in order. Click Save here; the page should stay put:

Formsapp/labs/lab1/forms/Forms.tsx

Form Elements

Text Fields



Text boxes

Radio buttons








Checkboxes




Dropdowns

Select one

Select many

Other HTML field types





Buttons

1.3.7 Parameterizing Components with Props

Videos

Since §1.3.1 you have been configuring HTML tags with attributes — id, then src/alt on images, type/name on form controls, and more. Your own React components can accept values the same way; in React those values are called props (short for properties). You declare them as parameters on the function, then pass them as attributes when you use the component — including the paragraph text itself via a text prop. Stay with self-closing tags for now; nested content and children come in the next section.

To make the highlight visible we need a little styling. This chapter is about HTML structure, not CSS — so treat the inline style object below as a sneak preview. Those property names (backgroundColor, borderColor, borderWidth, borderRadius) are CSS written in JavaScript camelCase.Chapter 2 covers CSS in detail; for now just copy the pattern and focus on how props flow into the component.

Create HighlightedParagraph.tsx. Define a component that takes text, backgroundColor, borderColor, borderWidth, and borderRadius as props, applies the styles on a <p>, and displays text. Show a few variations with different attribute values and import the lab into page.tsx:

HighlightedParagraphapp/labs/lab1/HighlightedParagraph.tsx
function HighlightedParagraph({
  text = "This paragraph is highlighted using component props.",
  backgroundColor = "lightyellow",
  borderColor = "orange",
  borderWidth = 2,
  borderRadius = 8,
}: {
  text?: string;
  backgroundColor?: string;
  borderColor?: string;
  borderWidth?: string | number;
  borderRadius?: string | number;
}) {
  return (
    <p
      style={{
        backgroundColor,
        borderColor,
        borderWidth,
        borderStyle: "solid",
        borderRadius,
        padding: "0.5rem 0.75rem",
      }}
    >
      {text}
    </p>
  );
}

export default function HighlightedParagraphLab() {
  return (
    <div id="wd-highlighted-paragraph">
      <h3>Highlighted Paragraph</h3>
      <HighlightedParagraph text="Default highlight: light yellow background, orange border." />
      <HighlightedParagraph
        text="Custom props: light blue background, navy border, thicker width, more rounding."
        backgroundColor="lightblue"
        borderColor="navy"
        borderWidth={4}
        borderRadius={16}
      />
      <HighlightedParagraph
        text="Another variation: misty rose background, crimson border, square corners."
        backgroundColor="#ffe4e1"
        borderColor="crimson"
        borderWidth="3px"
        borderRadius="0px"
      />
    </div>
  );
}

Three highlighted paragraphs, each with different style props — change a color or radius in the code and that paragraph updates:

HighlightedParagraphapp/labs/lab1/HighlightedParagraph.tsx

Highlighted Paragraph

Default highlight: light yellow background, orange border.

Custom props: light blue background, navy border, thicker width, more rounding.

Another variation: misty rose background, crimson border, square corners.

1.3.8 Wrapping Content with Children

Videos

In §1.3.7 every value — including the paragraph wording — arrived as an attribute. That works for a string, but not when you want to wrap arbitrary markup (headings, paragraphs, lists, and so on). In React, content nested between a component's opening and closing tags arrives as the special prop children. That is how you build reusable wrappers.

Create HighlightedBox.tsx with the same style props as HighlightedParagraph, but drop text. Render a <div> that displays children instead. Import it into page.tsx after the paragraph lab:

HighlightedBoxapp/labs/lab1/HighlightedBox.tsx
import type { ReactNode } from "react";

function HighlightedBox({
  backgroundColor = "lightyellow",
  borderColor = "orange",
  borderWidth = 2,
  borderRadius = 8,
  children,
}: {
  backgroundColor?: string;
  borderColor?: string;
  borderWidth?: string | number;
  borderRadius?: string | number;
  children?: ReactNode;
}) {
  return (
    <div
      style={{
        backgroundColor,
        borderColor,
        borderWidth,
        borderStyle: "solid",
        borderRadius,
        padding: "0.75rem 1rem",
        marginBottom: "0.75rem",
      }}
    >
      {children}
    </div>
  );
}

export default function HighlightedBoxLab() {
  return (
    <div id="wd-highlighted-box">
      <h3>Highlighted Box</h3>
      <HighlightedBox
        backgroundColor="lavender"
        borderColor="purple"
        borderWidth={3}
        borderRadius={12}
      >
        <h4>Callout</h4>
        <p>
          This box wraps <strong>any</strong>{" "}children — headings, paragraphs,
          lists, and more.
        </p>
        <ul>
          <li>backgroundColor</li>
          <li>borderColor</li>
          <li>borderWidth</li>
          <li>borderRadius</li>
        </ul>
      </HighlightedBox>
      <HighlightedBox
        backgroundColor="#e8f5e9"
        borderColor="green"
        borderWidth={2}
        borderRadius={20}
      >
        <p>
          A second box with different style props wrapping different content.
        </p>
      </HighlightedBox>
    </div>
  );
}

Two boxes share the same wrapper idea: border and background stay with the box, while nested headings, paragraphs, and lists (the children) differ — the pattern layouts reuse in §1.3.11:

HighlightedBoxapp/labs/lab1/HighlightedBox.tsx

Highlighted Box

Callout

This box wraps any children — headings, paragraphs, lists, and more.

  • backgroundColor
  • borderColor
  • borderWidth
  • borderRadius

A second box with different style props wrapping different content.

1.3.9 Implementing Navigation with the Anchor Tag

SlidesVideos

The <a> tag creates hyperlinks — the ability to navigate from one document on the Web to another, or to a specific place within a document. The "Hyper" in HyperText Markup Language refers to that idea. Its href attribute, short for hypertext reference, holds the destination address the browser should load or jump to when the user follows the link.

The shape of href matters:

  • Absolute URL — a full address including the scheme and host, such as https://www.lipsum.com or https://github.com/…. Use this for other websites.
  • Relative URL — a path on the same site, such as /labs or /labs/lab1. The browser resolves it against the current origin (for example http://localhost:3000).
  • Fragment (hash) — a leading # plus an element id, such as #wd-anchor-bottom. That scrolls to a target on the same page without loading a new document. Fragments are classic in-page navigation; they are not the same as navigating to a different route.

Optional attributes refine how the link behaves. The target attribute set to "_blank" opens the destination in a new browser tab or window — useful for external sites so users do not lose your app. When you use target="_blank", also set rel="noreferrer" (or at least noopener) so the new page cannot access window.opener — a small but important security habit.

Create AnchorTag.tsx, import it into page.tsx, and add a lipsum link plus a GitHub link with id wd-github:

AnchorTagapp/labs/lab1/AnchorTag.tsx
export default function AnchorTag() {
  return (
    <>
      <h4>Anchor tag</h4>
      Please{" "}
      <a href="https://www.lipsum.com" id="wd-lipsum">
        click here
      </a>{" "}
      to get dummy text
      <br />
      <a href="https://github.com/jannunzi" id="wd-github">
        GitHub
      </a>
    </>
  );
}

Lipsum and GitHub appear as ordinary links — a plain <a> triggers a normal browser navigation (full page load for a new document):

AnchorTagapp/labs/lab1/AnchorTag.tsx

Anchor tag

Please click here to get dummy text
GitHub

The following demo is illustrative — try absolute, relative, hash, and target="_blank" links. Your Lab 1 file only needs the lipsum and GitHub anchors above.

AnchorHrefPatterns
{/* Absolute — another site */}
<a href="https://www.lipsum.com">lipsum.com</a>

{/* Relative — same site */}
<a href="/labs">Back to Labs</a>

{/* Fragment — same page, scroll to id */}
<a href="#wd-anchor-bottom">Jump to bottom</a>

{/* New tab + safer external link */}
<a
  href="https://github.com/jannunzi"
  target="_blank"
  rel="noreferrer"
>
  GitHub (new tab)
</a>

Absolute, relative, hash, and new-tab patterns side by side — use the hash link to jump within the demo without leaving the page:

AnchorHrefPatterns
Absolute URL (another site)
lipsum.com
Relative URL (same site)
Back to Labs
Same-page fragment (hash)
Jump to bottom of this demo
Open in a new tab
GitHub (new tab)

You landed on this paragraph via the hash link above (#wd-anchor-bottom).

1.3.10 Implementing Navigation

Slides

Recall from the chapter introduction: a Single Page Application (SPA) keeps one HTML shell loaded and updates the UI as the user moves around, instead of downloading a brand-new page for every click. Next.js apps are built that way for in-app routes: navigating from Labs to Lab 1 should feel instant, without flashing a full reload.

In §1.2.5 you defined routes by adding page.tsx files under app/ (the App Router). Navigation is how the user moves between those routes. Historically, some SPAs used the URL hash (#/lab1) for client-side routes, because changing the fragment does not reload the document. That trick enabled early SPA navigation, but it is not how the App Router works. Today Next.js uses the browser History API so paths like /labs/lab1 update without a full reload — and without requiring a # prefix. Reserve hash fragments for in-page jumps (as in §1.3.9), not for app routes in this course.

A plain <a href="/labs/lab1"> still works, but it tells the browser to treat the click as a normal document request. Inside a Next.js app, prefer the built-in Link component from next/link. Link renders an anchor under the hood, yet intercepts navigation so React can swap in the next route's UI, prefetch linked pages when helpful, and keep client state where appropriate. Use <a> for true external URLs (or when you explicitly want a full reload); use Link for routes you already created with page.tsx.

Create Lab 2 and Lab 3 as new App Router routes (app/labs/lab2/page.tsx and app/labs/lab3/page.tsx — simple headings are enough), then a Labs index at app/labs/page.tsx that links to each lab with Link:

Labsapp/labs/page.tsx
import Link from "next/link";

export default function Labs() {
  return (
    <div id="wd-labs">
      <h1>Labs</h1>
      <ul>
        <li>
          <Link href="/labs/lab1">Lab 1: HTML Examples</Link>
        </li>
        <li>
          <Link href="/labs/lab2">Lab 2: CSS Basics</Link>
        </li>
        <li>
          <Link href="/labs/lab3">Lab 3: JavaScript Fundamentals</Link>
        </li>
      </ul>
    </div>
  );
}

A Labs index lists Lab 1–3. In the running app, clicking a link changes the URL to a path such as /labs/lab1 without the full-page flash a raw external <a> would cause:

Labsapp/labs/page.tsx

1.3.11 Implementing Layouts

Videos

In the App Router, page.tsx creates a route (§1.2.5); another reserved filename is layout.tsx, which does not create its own URL by itself. Instead it wraps the page.tsx (and nested layouts) in the same folder and below, so shared chrome — navigation, sidebars, headers — belongs in a layout and every child route gets it without copying markup into each page.

Nested folders nest layouts too. The root app/layout.tsx wraps the whole app; app/labs/layout.tsx wraps only routes under /labs. When you open Lab 1, Next.js renders the lab layout around the Lab 1 page.

Layouts receive a prop named children — the same wrapping idea you practiced with HighlightedBox in §1.3.8. You do not pass the page in by hand — the framework renders the matching page.tsx (and nested UI) into children for you. Create app/labs/TOC.tsx for a small table of contents, then app/labs/layout.tsx that places the TOC beside children:

LabsLayoutapp/labs/layout.tsx
import { ReactNode } from "react";
import TOC from "./TOC";

export default function LabsLayout({
  children,
}: Readonly<{ children: ReactNode }>) {
  return (
    <table>
      <tbody>
        <tr>
          <td valign="top" width="100px">
            <TOC />
          </td>
          <td valign="top">{children}</td>
        </tr>
      </tbody>
    </table>
  );
}

With the TOC on the left and page content on the right, the sidebar stays put while the main area (children) swaps as you open different labs:

LabsLayoutapp/labs/layout.tsx

1.3.12 Exercises

Use this checklist to confirm Lab 1 covers every HTML topic in §1.3. It is the same list, in the same order, as the A1 Lab checklist on A1 — one top-level item per Lab section, with nested a / b / c for the Lab component, On your own, and With AI extra, walking §1.3.1–§1.3.11 as you read. Each parent points back to the section where you built the worked example. When you are done, app/labs/lab1/page.tsx should import and render the components in order so the page matches the live demo below.

  1. HeadingTags (§1.3.1)
    1. Lab component — Create HeadingTags.tsx from the book sample (h4 "Heading Tags" and its paragraph). Before With AI, add h1–h6 as practice without erasing that sample text, and import the component on the Lab 1 page.
    2. On your own — Personal heading under wd-your-heading, including a span with id wd-your-span.
    3. With AI — After the practice h1–h6 headings you added, add a sample outline with id wd-ai-headings (h4 Lab notes, h5 What I built, h6 Next step). Keep the book sample text.
  2. ParagraphTag (§1.3.2)
    1. Lab component — Create ParagraphTag.tsx and wrap sample text in paragraph tags for vertical spacing.
    2. On your own — Two personal paragraphs with ids wd-p-your-1 and wd-p-your-2.
    3. With AI — Extra sample paragraph with id wd-ai-p that explains why wrapping text in p creates vertical spacing.
  3. ListTags (§1.3.3)
    1. Lab component — Create ListTags.tsx with the pancake ordered list and the sample book unordered list.
    2. On your own — Favorite recipe ordered list (wd-your-favorite-recipe) and favorites unordered list (wd-your-books).
    3. With AI — Sample HTML-tags list with id wd-ai-html-tags (at least five tags from this chapter).
  4. Tables (§1.3.4)
    1. Lab component — Create Tables.tsx with the quiz grades table (Q1–Q3) and an average row.
    2. On your own — Second personal table with id wd-your-table.
    3. With AI — Quiz rows Q4–Q10 in the sample grades table, with a recalculated average from all ten scores.
  5. Images (§1.3.5)
    1. Lab component — Create Images.tsx with the remote Starship image and the local teslabot image.
    2. On your own — Your image with id wd-your-image.
    3. With AI — Extra sample image with id wd-ai-image from a public URL.
  6. Forms (§1.3.6)
    1. Lab component — Build the form components under app/labs/lab1/forms/ (text, textarea, radio, checkboxes, dropdowns, other types, buttons) and assemble them in Forms.tsx.
    2. On your own — Student Profile form in the single canonical app/labs/lab1/forms/YourForm.tsx with id wd-your-form covering the field types from the chapter, plus Save and Cancel — overwrite that same path and keep that same id; Forms.tsx imports that one YourForm only.
    3. With AI — Overwrite the same file app/labs/lab1/forms/YourForm.tsx and keep id wd-your-form — the same id as On your own. No second file and no new form id (do not invent wd-ai-form, wd-ai-your-form, or similar). Unlike earlier With AI steps that add a new wd-ai-* id, Forms reuses the On-your-own id. Forms.tsx still imports that one YourForm only. Then replace every SAMPLE default with your own details.
  7. HighlightedParagraph (§1.3.7)
    1. Lab component — Create HighlightedParagraph.tsx with text and style props (attributes only) and show a few variations.
    2. On your own — Extra HighlightedParagraph with your text and colors.
    3. With AI — Extra sample HighlightedParagraph (not your personal sentence) with different colors.
  8. HighlightedBox (§1.3.8)
    1. Lab component — Create HighlightedBox.tsx that wraps nested children with the same style props.
    2. On your own — Extra HighlightedBox wrapping your goals list.
    3. With AI — Extra sample HighlightedBox of nested tags (not your personal goals list).
  9. AnchorTag (§1.3.9)
    1. Lab component — Create AnchorTag.tsx with lipsum plus GitHub anchors and import it on the Lab 1 page.
    2. On your own — Personal anchors wd-your-link and wd-your-github.
    3. With AI — Sample docs link with id wd-ai-link (for example MDN table element).
  10. Labs navigation (§1.3.10)
    1. Lab component — Labs index at app/labs/page.tsx lists Lab 1–3 with Link, plus a Kambaz link so graders can reach every required page.
    2. On your own — Create Lab 4 and link to it from the Labs index (id wd-lab4-link).
    3. With AI — Lab 5 placeholder page and a Labs index link to /labs/lab5.
  11. Labs TOC and layout (§1.3.11)
    1. Lab component — Create app/labs/TOC.tsx and app/labs/layout.tsx so the TOC wraps lab pages via children.
    2. On your own — Personal note or link in the labs TOC — your name, a one-line motto, or a link back to the book.
    3. With AI — Chapter 1 link in the labs TOC (id wd-toc-book-link). Your own app has no /book routes, so the link uses the full course-book address, https://kambaz.dev/book/ch1, and opens the chapter. Keep the id wd-toc-book-link on it; the ids help us test your work.

When the checklist is done, Lab 1 should match the core HTML examples below. Your On your own additions and With AI extras appear in the same page (and Labs chrome) beyond what this demo shows. Delivery (Vercel, name, GitHub) and Kambaz screens stay on the A1 page — see §1.4.9 for the Kambaz recap.

Here is the finished app/labs/lab1/page.tsx, importing every Lab 1 component in section order:

Lab1Completeapp/labs/lab1/page.tsx
import HeadingTags from "./HeadingTags";
import ParagraphTag from "./ParagraphTag";
import ListTags from "./ListTags";
import Tables from "./Tables";
import Images from "./Images";
import Forms from "./forms/Forms";
import HighlightedParagraph from "./HighlightedParagraph";
import HighlightedBox from "./HighlightedBox";
import AnchorTag from "./AnchorTag";

export default function Lab1() {
  return (
    <div id="wd-lab1">
      <h2>Lab 1</h2>
      <h3>HTML Examples</h3>
      <HeadingTags />
      <ParagraphTag />
      <ListTags />
      <Tables />
      <Images />
      <Forms />
      <HighlightedParagraph />
      <HighlightedBox />
      <AnchorTag />
    </div>
  );
}
Lab1app/labs/lab1/page.tsx

Lab 1

HTML Examples

Heading Tags

Text documents are often broken up into several sections and subsections. Each section is usually prefaced with a short title or heading that attempts to summarize the topic of the section it precedes. For instance this paragraph is preceded by the heading Heading Tags. The font of the section headings are usually larger and bolder than their subsection headings. This document uses headings to introduce topics such as HTML Documents, HTML Tags, Heading Tags, etc. HTML heading tags can be used to format plain text so that it renders in a browser as large headings. There are 6 heading tags for different sizes: h1, h2, h3, h4, h5, and h6. Tag h1 is the largest heading and h6 is the smallest heading. A span sits in this sentence without starting a new line.

Paragraph Tag

This is a paragraph. We often separate a long set of sentences with vertical spaces to make the text easier to read. Browsers ignore vertical white spaces and render all the text as one single set of sentences. To force the browser to add vertical spacing, wrap the paragraphs you want to separate with the paragraph tag

This is the first paragraph. The paragraph tag is used to format vertical gaps between long pieces of text like this one.

This is the second paragraph. Even though there is a deliberate white gap between the paragraph above and this paragraph, by default browsers render them as one contiguous piece of text as shown here on the right.

This is the third paragraph. Wrap each paragraph with the paragraph tag to tell browsers to render the gaps.

List Tags

Ordered List Tag
How to make pancakes:
  1. Mix dry ingredients.
  2. Add wet ingredients.
  3. Stir to combine.
  4. Heat a skillet or griddle.
  5. Pour batter onto the skillet.
  6. Cook until bubbly on top.
  7. Flip and cook the other side.
  8. Serve and enjoy!
My favorite recipe:
  1. Boil water and cook pasta until al dente.
  2. Sauté garlic in olive oil, then add crushed tomatoes.
  3. Toss pasta with sauce and top with grated Parmesan.
Unordered List Tag
My favorite books (in no particular order)
  • Dune
  • Lord of the Rings
  • Ender's Game
  • Red Mars
  • The Forever War
Your favorite books (in no particular order)
  • The Pragmatic Programmer
  • Clean Code
  • Designing Data-Intensive Applications

Table Tag

QuizTopicDateGrade
Q1HTML2/3/2185
Q2CSS2/10/2190
Q3JavaScript2/17/2195
Average90

Image tag

Loading an image from the internet:
Starship
Loading a local image:
Tesla Bot (Optimus) humanoid robot

Form Elements

Text Fields



Text boxes

Radio buttons








Checkboxes




Dropdowns

Select one

Select many

Other HTML field types





Buttons

Highlighted Paragraph

Default highlight: light yellow background, orange border.

Custom props: light blue background, navy border, thicker width, more rounding.

Another variation: misty rose background, crimson border, square corners.

Highlighted Box

Callout

This box wraps any children — headings, paragraphs, lists, and more.

  • backgroundColor
  • borderColor
  • borderWidth
  • borderRadius

A second box with different style props wrapping different content.

Anchor tag

Please click here to get dummy text
GitHub

When the checklist feels solid, try the self-check in §1.3.13 before you start Kambaz.

1.3.13 Check Your Understanding

Pause and test the HTML topics from this chapter. The practice quiz draws 10 items on those topics — App Router page.tsx, headings and paragraphs, lists and tables, images, form fields, radio name groups and mutual exclusion, checkbox independence, the two label patterns (htmlFor/id vs wrapping), dropdowns, buttons, props and children, Link vs <a>, and layouts. It is a self-check, not part of your course grade. Misses link back to the subsection you should reread; each new attempt draws a different 10.

1.4 Prototyping the React Kambaz User Interface with HTML

Slides

So far the exercises in this chapter practiced HTML pieces in isolation. The following sections put those skills to work building Kambaz — a website inspired by a popular Online Learning Management System (LMS). This chapter focuses on simple prototype screens; later chapters improve the same structure with CSS, state, and data. A single coverage checklist is in §1.4.9 — use it after you have walked through the screens, not instead of building them as you read.

Do all Kambaz work under app/(kambaz). This is still the App Router from §1.2.5 — folders plus page.tsx create routes — with one new wrinkle: parentheses make a route group. A route group organizes files and layouts without appearing in the URL. A page at app/(kambaz)/dashboard/page.tsx is still the route /dashboard, not /(kambaz)/dashboard.

1.4.1 Implementing the Kambaz Landing Page

Slides

A landing page is the primary entry point of a web application — the screen for the root route /. In the App Router that is normally app/page.tsx. To keep Kambaz modular while still owning /, put that homepage inside the (kambaz) route group (app/(kambaz)/page.tsx) instead of leaving a separate root page.tsx beside the group.

Start with a minimal landing page, remove any leftover root app/page.tsx so the route group owns /, and add a Kambaz link in both the Labs TOC and the Labs index page:

KambazLandingapp/(kambaz)/page.tsx
export default function Kambaz() {
  return (
    <div id="wd-kambaz">
      <h1>Kambaz</h1>
    </div>
  );
}
LabsTOCapp/labs/TOC.tsx
import Link from "next/link";

export default function TOC() {
  return (
    <ul>
      <li>
        <Link href="/labs" id="wd-home-link">
          Home
        </Link>
      </li>
      {/* ... lab links ... */}
      <li>
        <Link href="/" id="wd-kambaz-link">
          Kambaz
        </Link>
      </li>
    </ul>
  );
}

At http://localhost:3000 you should see the Kambaz heading and that the Labs TOC link with id wd-kambaz-link reaches it. In the next section you will change this landing page to redirect into Sign in so authentication is the default entry.

Open the live app: /account/signin (Kambaz)

1.4.2 The Kambaz Account Screens

Slides

Account screens manage identity and personal details. Sign up registers a new user, Sign in identifies a returning user, and Profile lets them view and edit account fields. An Account Navigation sidebar keeps those screens a click apart without reloading the whole app.

1.4.2.1 The Sign In Screen

Create app/(kambaz)/account/signin/page.tsx with username and password fields plus links to Profile and Sign up. Use defaultValue for starter credentials — useful while you prototype before real authentication exists.

You will also see className on some tags (for example className="wd-username"). In plain HTML the attribute is named class; in JSX it must be className because class is a reserved word in JavaScript — the same reason labels use htmlFor instead of for. Keep these names as written, especially when you copy the code: the wd-* ids and class names help us test your work, and they also hook up CSS later in Chapter 2; with Tailwind commented out they may not change how the page looks yet.

Create the Sign in screen as follows:

Signinapp/(kambaz)/account/signin/page.tsx
import Link from "next/link";

export default function Signin() {
  return (
    <div id="wd-signin-screen">
      <h3>Sign in</h3>
      <input
        placeholder="username"
        className="wd-username"
        defaultValue="ada"
      />{" "}
      <br />
      <input
        placeholder="password"
        type="password"
        className="wd-password"
        defaultValue="123"
      />{" "}
      <br />
      <Link href="/account/profile" id="wd-signin-btn">
        Sign in
      </Link>{" "}
      <br />
      <Link href="/account/signup" id="wd-signup-link">
        Sign up
      </Link>
    </div>
  );
}

A minimal Sign in screen: username and password with starter defaults, plus links toward Profile and Sign up:

Signinapp/(kambaz)/account/signin/page.tsx

Make Sign in the default for both /account and the Kambaz root by redirecting with redirect from next/navigation. Unlike a Link (which the user clicks), redirect runs when the route renders and immediately sends the browser to another path — useful for default screens:

AccountPageapp/(kambaz)/account/page.tsx
import { redirect } from "next/navigation";

export default function AccountPage() {
  redirect("/account/signin");
}
Kambazapp/(kambaz)/page.tsx
import { redirect } from "next/navigation";

export default function Kambaz() {
  redirect("/account/signin");
}

/account and / should both land on /account/signin.

1.4.2.2 The Sign Up Screen

Sign up mirrors Sign in but adds a password-verification field. Create app/(kambaz)/account/signup/page.tsx:

Signupapp/(kambaz)/account/signup/page.tsx
import Link from "next/link";

export default function Signup() {
  return (
    <div id="wd-signup-screen">
      <h3>Sign up</h3>
      <input
        placeholder="username"
        className="wd-username"
        defaultValue="ada"
      />
      <br />
      <input
        placeholder="password"
        type="password"
        className="wd-password"
        defaultValue="123"
      />
      <br />
      <input
        placeholder="verify password"
        type="password"
        className="wd-password-verify"
      />
      <br />
      <Link href="/account/profile">Sign up</Link>
      <br />
      <Link href="/account/signin">Sign in</Link>
    </div>
  );
}

Sign up mirrors Sign in but adds a second password field for verification:

Signupapp/(kambaz)/account/signup/page.tsx

1.4.2.3 The Profile Screen

Profile shows a fuller set of user fields. Reuse typed inputs from Lab 1 — date, email, and a select for role:

Profileapp/(kambaz)/account/profile/page.tsx
import Link from "next/link";

export default function Profile() {
  return (
    <div id="wd-profile-screen">
      <h3>Profile</h3>
      <input
        defaultValue="alice"
        placeholder="username"
        className="wd-username"
      />
      <br />
      <input
        defaultValue="123"
        placeholder="password"
        type="password"
        className="wd-password"
      />
      <br />
      <input defaultValue="Alice" placeholder="First Name" id="wd-firstname" />
      <br />
      <input
        defaultValue="Wonderland"
        placeholder="Last Name"
        id="wd-lastname"
      />
      <br />
      <input defaultValue="2000-01-01" type="date" id="wd-dob" />
      <br />
      <input defaultValue="alice@wonderland" type="email" id="wd-email" />
      <br />
      <select defaultValue="FACULTY" id="wd-role">
        <option value="USER">User</option>
        <option value="ADMIN">Admin</option>
        <option value="FACULTY">Faculty</option>
        <option value="STUDENT">Student</option>
      </select>
      <br />
      <Link href="/account/signin">Sign out</Link>
    </div>
  );
}

Profile collects identity fields — try the role dropdown and date of birth control:

Profileapp/(kambaz)/account/profile/page.tsx

Profile








Sign out

1.4.2.4 Account Navigation

Create app/(kambaz)/account/Navigation.tsx with links to Signin, Signup, and Profile. Then wrap account routes in app/(kambaz)/account/layout.tsx using a two-column table — navigation on the left, children on the right (same layout idea as Labs in §1.3.11):

AccountNavigationapp/(kambaz)/account/Navigation.tsx
import Link from "next/link";

export default function AccountNavigation() {
  return (
    <div id="wd-account-navigation">
      <Link href="/account/signin">Signin</Link> <br />
      <Link href="/account/signup">Signup</Link> <br />
      <Link href="/account/profile">Profile</Link> <br />
    </div>
  );
}
AccountLayoutapp/(kambaz)/account/layout.tsx
import { ReactNode } from "react";
import AccountNavigation from "./Navigation";

export default function AccountLayout({
  children,
}: Readonly<{ children: ReactNode }>) {
  return (
    <div id="wd-kambaz-account">
      <table>
        <tbody>
          <tr>
            <td valign="top">
              <AccountNavigation />
            </td>
            <td valign="top" width="100%">
              {children}
            </td>
          </tr>
        </tbody>
      </table>
    </div>
  );
}

Three absolute account links — Signin, Signup, and Profile — ready to sit in the layout's left column. Build all three for this chapter. (The finished Kambaz on the course site shows only the links that fit whether you are signed in, for example Signin and Signup when signed out. You add that behavior in a later chapter.)

AccountNavigationapp/(kambaz)/account/Navigation.tsx

With the layout in place, Sign in is the default content beside the sidebar. Nav stays on the left while Sign in fills the right column — then open /account in the running app and click the sidebar so Sign up and Profile swap into that same column. /account and / should redirect to /account/signin.

AccountLayoutapp/(kambaz)/account/layout.tsx

For reference, here are all four account pieces side by side — nav, Sign in, Sign up, and Profile:

AccountScreensDemoapp/book/ch1/embeds/AccountScreensDemo.tsx

Profile








Sign out

1.4.3 Implementing the Dashboard Screen

Slides

The Dashboard lists courses a student is enrolled in (or a faculty member is teaching). Clicking a course navigates to that course's route. For course thumbnails, use the Next.js Image component from next/image instead of a raw <img>. It still needs src, alt, width, and height; Next.js uses those sizes for layout and optimization. For this chapter, treat Image as the preferred way to show local files under public/images/ — look images up online or generate placeholders with an AI tool. Include at least three courses.

Each course repeats the same markup — image, title, subtitle, and a Go button — so extract that block into a CourseCard component that accepts the differing values as props. Use type="button" on Go (§1.3.6.7) so the click does not try to submit a form. Keep the markup plain for now; Chapter 2 will add Tailwind classes to this same component and lay the cards out in a responsive grid.

CourseCardapp/(kambaz)/dashboard/CourseCard.tsx
import Link from "next/link";
import Image from "next/image";

export default function CourseCard({
  id,
  title,
  subtitle,
  image,
}: {
  id: string;
  title: string;
  subtitle: string;
  image: string;
}) {
  return (
    <div className="wd-dashboard-course">
      <Link href={`/courses/${id}/home`} className="wd-dashboard-course-link">
        <Image src={image} width={200} height={150} alt={title} />
        <div>
          <h5>{title}</h5>
          <p className="wd-dashboard-course-title">{subtitle}</p>
          <button type="button">Go</button>
        </div>
      </Link>
    </div>
  );
}

The Dashboard page then mounts one CourseCard per course — at least three in total:

Dashboardapp/(kambaz)/dashboard/page.tsx
import CourseCard from "./CourseCard";

export default function Dashboard() {
  return (
    <div id="wd-dashboard">
      <h1 id="wd-dashboard-title">Dashboard</h1> <hr />
      <h2 id="wd-dashboard-published">Published Courses (3)</h2> <hr />
      <div id="wd-dashboard-courses">
        <CourseCard
          id="1234"
          title="CS1234 React JS"
          subtitle="Full Stack software developer"
          image="/images/reactjs.jpg"
        />
        <CourseCard
          id="2345"
          title="CS2345 Node JS"
          subtitle="Server side JavaScript"
          image="/images/nodejs.jpg"
        />
        <CourseCard
          id="3456"
          title="CS3456 MongoDB"
          subtitle="NoSQL Databases"
          image="/images/mongodb.jpg"
        />
      </div>
    </div>
  );
}

The prototype does not need to match the polished target below yet —Chapter 2 styles CourseCard and the courses container. Point the Sign in button at the Dashboard so a successful "sign in" lands on courses:

Target Kambaz Dashboard with course cards
Figure 1.4.3a — Dashboard Screen
SigninToDashboardapp/(kambaz)/account/signin/page.tsx
<Link href="/dashboard" id="wd-signin-btn">
  Sign in
</Link>

1.4.3.1 Kambaz Navigation Sidebar

Global navigation belongs in app/(kambaz)/Navigation.tsx and the app/(kambaz)/layout.tsx table layout so Account, Dashboard, Calendar, Inbox, and Labs stay visible across screens. Northeastern can stay an external <a> with target="_blank"; in-app destinations use Link:

KambazNavigationapp/(kambaz)/Navigation.tsx
import Link from "next/link";

export default function KambazNavigation() {
  return (
    <div id="wd-kambaz-navigation">
      <a
        href="https://www.northeastern.edu/"
        id="wd-neu-link"
        target="_blank"
        rel="noreferrer"
      >
        Northeastern
      </a>
      <br />
      <Link href="/account" id="wd-account-link">
        Account
      </Link>
      <br />
      <Link href="/dashboard" id="wd-dashboard-link">
        Dashboard
      </Link>
      <br />
      <Link href="/dashboard" id="wd-course-link">
        Courses
      </Link>
      <br />
      <Link href="/calendar" id="wd-calendar-link">
        Calendar
      </Link>
      <br />
      <Link href="/inbox" id="wd-inbox-link">
        Inbox
      </Link>
      <br />
      <Link href="/labs" id="wd-labs-link">
        Labs
      </Link>
      <br />
    </div>
  );
}
KambazLayoutapp/(kambaz)/layout.tsx
import { ReactNode } from "react";
import KambazNavigation from "./Navigation";

export default function KambazLayout({
  children,
}: Readonly<{ children: ReactNode }>) {
  return (
    <table>
      <tbody>
        <tr>
          <td valign="top" width="200">
            <KambazNavigation />
          </td>
          <td valign="top" width="100%">
            {children}
          </td>
        </tr>
      </tbody>
    </table>
  );
}

1.4.3.2 Handling Missing Pages

Calendar and Inbox links will 404 until those pages exist. Handle missing routes gracefully with app/not-found.tsx — another reserved App Router filename (like page.tsx in §1.2.5 and layout.tsx in §1.3.11) — so users can return to the Dashboard. Keep the markup simple; fancy className values are optional placeholders until Chapter 2 styling is on:

NotFoundapp/not-found.tsx
import Link from "next/link";

export default function NotFound() {
  return (
    <div id="wd-not-found">
      <h2>Page Not Found</h2>
      <p>
        The requested page could not be found. Please check the page URL or
        return to the dashboard.
      </p>
      <Link href="/dashboard" id="wd-not-found-dashboard-link">
        Back to Dashboard
      </Link>
    </div>
  );
}

Kambaz chrome on the left, Dashboard courses on the right. Sign in should land here; Account still reaches the account screens; Calendar and Inbox should show the not-found page with Back to Dashboard:

DashboardDemoapp/book/ch1/embeds/DashboardDemo.tsx

1.4.4 Implementing the Courses Screen

Slides

Clicking a course on the Dashboard should open that course's Home screen at /courses/[cid]/home. Same App Router rule as §1.2.5 — a folder with page.tsx is a route — except [cid] is a dynamic segment: Next.js fills cid from whatever appears in that part of the URL (for example 1234 in /courses/1234/home). You do not need a page.tsx directly under [cid] — Home lives in the home folder. Start with a placeholder Home page, then add Course Navigation and a courses layout.

HomePlaceholderapp/(kambaz)/courses/[cid]/home/page.tsx
export default function Home() {
  return (
    <div id="wd-home">
      <h2>Home 1234</h2>
    </div>
  );
}

Point each CourseCard at that route, for example href={`/courses/${id}/home`}.

1.4.4.1 Course Navigation Sidebar

Course Navigation links to Home, Modules, Piazza, Zoom, Assignments, Quizzes, Grades, and People. Implement real screens for Home, Modules, and Assignments in this chapter. For the rest, add simple placeholder pages that only show a heading — for example app/(kambaz)/courses/[cid]/piazza/page.tsx, zoom/page.tsx, quizzes/page.tsx, grades/page.tsx, and people/table/page.tsx.

Pass cid into the navigation so links stay correct for every course. The href values use a JavaScript template literal (backticks): `/courses/${cid}/home` builds a string and inserts the current cid where ${cid} appears. Copy the pattern for now; Chapter 3 covers JavaScript strings in more depth.

CourseNavigationapp/(kambaz)/courses/[cid]/Navigation.tsx
import Link from "next/link";

export default function CourseNavigation({ cid }: { cid: string }) {
  return (
    <div id="wd-courses-navigation">
      <Link href={`/courses/${cid}/home`} id="wd-course-home-link">
        Home
      </Link>{" "}
      <br />
      <Link href={`/courses/${cid}/modules`} id="wd-course-modules-link">
        Modules
      </Link>{" "}
      <br />
      <Link href={`/courses/${cid}/piazza`} id="wd-course-piazza-link">
        Piazza
      </Link>{" "}
      <br />
      <Link href={`/courses/${cid}/zoom`} id="wd-course-zoom-link">
        Zoom
      </Link>{" "}
      <br />
      <Link href={`/courses/${cid}/assignments`} id="wd-course-assignments-link">
        Assignments
      </Link>{" "}
      <br />
      <Link href={`/courses/${cid}/quizzes`} id="wd-course-quizzes-link">
        Quizzes
      </Link>{" "}
      <br />
      <Link href={`/courses/${cid}/grades`} id="wd-course-grades-link">
        Grades
      </Link>{" "}
      <br />
      <Link href={`/courses/${cid}/people/table`} id="wd-course-people-link">
        People
      </Link>{" "}
      <br />
    </div>
  );
}

Wire the sidebar into app/(kambaz)/courses/[cid]/layout.tsx. In the current App Router, layouts (and some pages) receive the dynamic segment through a params prop that is a Promise — so the function is marked async and you await params before reading cid. Copy this shape for now; §6.2.6.1 explains async/await more carefully:

CoursesLayoutapp/(kambaz)/courses/[cid]/layout.tsx
import { ReactNode } from "react";
import CourseNavigation from "./Navigation";

export default async function CoursesLayout({
  children,
  params,
}: Readonly<{
  children: ReactNode;
  params: Promise<{ cid: string }>;
}>) {
  const { cid } = await params;
  return (
    <div id="wd-courses">
      <h2>Courses {cid}</h2>
      <hr />
      <table>
        <tbody>
          <tr>
            <td valign="top" width="200">
              <CourseNavigation cid={cid} />
            </td>
            <td valign="top" width="100%">
              {children}
            </td>
          </tr>
        </tbody>
      </table>
    </div>
  );
}

Opening a course from the Dashboard should show the course id in the heading and the Course Navigation sidebar on the left. Placeholder links such as Piazza should render their simple heading pages.

If npm run dev works but npm run build or your Vercel deploy fails with Next.js encountered URL data usePathname() in a Client Component outside of <Suspense> or, further down the log, Next.js encountered uncached or runtime data during prerendering, your project was created with Next.js 16.4 (package.json says "next": "16.4.0"). Delete both the cacheComponents: true, and partialPrefetching: true, lines from next.config.ts as described in the Next.js 16.4 fix note, then build again.

1.4.5 Implementing the Modules Screen

Slides

When a user opens a course from the Dashboard, the default destination will be that course's Home screen. Home shows the same module list you see under Modules — Week 1 / Lecture 1 material, with nested sections such as LEARNING OBJECTIVES, READING, and SLIDES. Build the Modules screen first, then reuse that component when you assemble Home in §1.4.6.

On screen you effectively have three columns of chrome and content: Kambaz Navigation (from the Kambaz layout), Course Navigation (from the courses layout), and the Modules list in the main area. Focus now on prototyping Modules as nested lists: a top-level list of modules, each containing a nested list of lessons, each lesson containing content items. Include at least Weeks 1–3; expand Week 1 with LEARNING OBJECTIVES, READING, and SLIDES.

Same idea as CourseCard: those nested blocks repeat, so extract them into plain components before you fill the page. Keep the markup unstyled; Chapter 2 will add Tailwind and a small checkmark helper to these same files.

Start with Module: one week's title, plus a children slot where its lessons will nest (the same children pattern from §1.3):

Moduleapp/(kambaz)/courses/[cid]/modules/Module.tsx
import type { ReactNode } from "react";

export default function Module({
  title,
  children,
}: {
  title: string;
  children?: ReactNode;
}) {
  return (
    <li className="wd-module">
      <div className="wd-title">{title}</div>
      <ul className="wd-lessons">{children}</ul>
    </li>
  );
}

Next, Lesson — same shape one level down. Its title is something like LEARNING OBJECTIVES or READING; its children are the content-item lis. Always render the wd-content list (even if empty for now) so you stay with the {children} pattern and avoid new JavaScript conditionals:

Lessonapp/(kambaz)/courses/[cid]/modules/Lesson.tsx
import type { ReactNode } from "react";

export default function Lesson({
  title,
  children,
}: {
  title: string;
  children?: ReactNode;
}) {
  return (
    <li className="wd-lesson">
      <span className="wd-title">{title}</span>
      <ul className="wd-content">{children}</ul>
    </li>
  );
}

Assemble them on the Modules page: a short toolbar on top, then Module elements whose children are Lessons. Expand Week 1 fully; Weeks 2–3 can start thin and grow as you like:

Modulesapp/(kambaz)/courses/[cid]/modules/page.tsx
import Module from "./Module";
import Lesson from "./Lesson";

export default function Modules() {
  return (
    <div>
      <button>Collapse All</button> <button>View Progress</button>{" "}
      <select defaultValue="publish-all">
        <option value="publish-all">Publish All</option>
      </select>{" "}
      <button>+ Module</button>
      <ul id="wd-modules">
        <Module title="Week 1, Lecture 1 - Course Introduction, Syllabus, Agenda">
          <Lesson title="LEARNING OBJECTIVES">
            <li className="wd-content-item">Introduction to the course</li>
            <li className="wd-content-item">Learn what is Web Development</li>
          </Lesson>
          <Lesson title="READING">
            <li className="wd-content-item">
              Full Stack Developer - Chapter 1 - Introduction
            </li>
            <li className="wd-content-item">
              Full Stack Developer - Chapter 2 - Creating User Interfaces
            </li>
          </Lesson>
          <Lesson title="SLIDES">
            <li className="wd-content-item">Introduction to Web Development</li>
            <li className="wd-content-item">
              Creating an HTTP server with Node.js
            </li>
            <li className="wd-content-item">Creating a React Application</li>
          </Lesson>
        </Module>
        <Module title="Week 2">{/* Expand lessons on your own */}</Module>
        <Module title="Week 3" />
      </ul>
    </div>
  );
}

Aim for the structure of the target Modules screen below; this chapter's HTML prototype stays unstyled. The nested list should read as weeks → lessons → content items. Expand Weeks 2–3 with more lessons if you want a fuller prototype:

Target Kambaz Modules screen
Figure 1.4.5a — Modules Screen
ModulesDemoapp/book/ch1/embeds/ModulesDemo.tsx
  • Week 1, Lecture 1 - Course Introduction, Syllabus, Agenda
    • LEARNING OBJECTIVES
      • Introduction to the course
      • Learn what is Web Development
    • READING
      • Full Stack Developer - Chapter 1 - Introduction
      • Full Stack Developer - Chapter 2 - Creating User Interfaces
    • SLIDES
      • Introduction to Web Development
      • Creating an HTTP server with Node.js
      • Creating a React Application
  • Week 2
    • LEARNING OBJECTIVES
      • Learn how to create user interfaces with HTML
  • Week 3
    • LEARNING OBJECTIVES
      • CSS Styling

1.4.6 Implementing the Course Home Screen

The finished Home target shows four columns: Kambaz Navigation, Course Navigation, Modules, and a Course Status sidebar. The first two already come from outer layouts — Home only needs Modules plus Course Status side by side:

Target Kambaz Home screen with Course Status
Figure 1.4.6a — Home Screen

Create app/(kambaz)/courses/[cid]/home/Status.tsx. Start from the stub below, then complete the status actions on your own. Include all of these buttons (labels matter for graders):

  • Unpublish and Publish
  • Import Existing Content
  • Import from Commons
  • Choose Home Page
  • View Course Stream
  • New Announcement
  • New Analytics
  • View Course Notifications
CourseStatusapp/(kambaz)/courses/[cid]/home/Status.tsx
export default function CourseStatus() {
  return (
    <div id="wd-course-status">
      <h2>Course Status</h2>
      <button>Unpublish</button> <button>Publish</button>
      <br />
      <br />
      {/* Complete the remaining status actions on your own */}
      <button>View Course Notifications</button>
    </div>
  );
}

Combine Course Status with Modules in app/(kambaz)/courses/[cid]/home/page.tsx:

Homeapp/(kambaz)/courses/[cid]/home/page.tsx
import Modules from "../modules/page";
import CourseStatus from "./Status";

export default function Home() {
  return (
    <div id="wd-home">
      <table>
        <tbody>
          <tr>
            <td valign="top" width="70%">
              <Modules />
            </td>
            <td valign="top">
              <CourseStatus />
            </td>
          </tr>
        </tbody>
      </table>
    </div>
  );
}

If you still have an early page.tsx directly under courses/[cid]/, delete it — Dashboard and Course Navigation both target /courses/[cid]/home, so that extra page is unused. Home splits the screen: modules on the left, course status actions on the right. Opening a course from the Dashboard should land here:

HomeDemoapp/book/ch1/embeds/HomeDemo.tsx

Courses 1234


  • Week 1, Lecture 1 - Course Introduction, Syllabus, Agenda
    • LEARNING OBJECTIVES
      • Introduction to the course
      • Learn what is Web Development

Course Status








1.4.7 Assignments Screen (On Your Own)

Slides

Build the Assignments screen yourself using the same HTML patterns from Dashboard, Modules, and Home — no line-by-line walkthrough this time. From the Dashboard, open a course, then choose Assignments in Course Navigation. The polished target is below; match the plain HTML demo for this chapter. Exact due dates may differ. Keep the given id and className values: later chapters build on them, and the ids help us test your work. Chapter 2 will style this screen with Tailwind.

Target Kambaz Assignments screen
Figure 1.4.7a — Assignments Screen

What to build

  • Route: app/(kambaz)/courses/[cid]/assignments/page.tsx
  • A search field (id="wd-search-assignment", placeholder Search for Assignments), plus + Group (wd-add-assignment-group) and + Assignment (wd-add-assignment) buttons
  • A group heading id="wd-assignments-title" — text like ASSIGNMENTS 40% of Total with a small + button. Only the ASSIGNMENTS group is required (QUIZZES / EXAMS / PROJECT can wait)
  • A list id="wd-assignment-list" with your assignments, each linking to its editor. Aim for three (A1 ENV + HTML, A2 CSS + TAILWIND, A3 JS + REACT are fine examples)
  • Each row: title link to /courses/${cid}/assignments/${aid} with className="wd-assignment-link", plus a due-date / points blurb underneath. Wrap the row in className="wd-assignment-list-item"
  • Same idea as CourseCard: extract a plain AssignmentItem component for each row (props for cid, aid, title, details) so you are not copying the markup three times
  • The page needs cid from the URL for those links — use the same async / await params shape as the courses layout (copy for now; §6.2.6.1 explains async/await)

Start from these two files. AssignmentItem is complete, and the page already renders one assignment (A1) that links to its editor. Add the rest yourself: three assignments are recommended (A2 and A3 after A1), plus the search field, buttons, and group heading.

AssignmentItemapp/(kambaz)/courses/[cid]/assignments/AssignmentItem.tsx
import Link from "next/link";

export default function AssignmentItem({
  cid,
  aid,
  title,
  details,
}: {
  cid: string;
  aid: string;
  title: string;
  details: string;
}) {
  return (
    <li className="wd-assignment-list-item">
      <Link
        href={`/courses/${cid}/assignments/${aid}`}
        className="wd-assignment-link"
      >
        {title}
      </Link>
      <div>{details}</div>
    </li>
  );
}
Assignmentsapp/(kambaz)/courses/[cid]/assignments/page.tsx
import AssignmentItem from "./AssignmentItem";

export default async function Assignments({
  params,
}: {
  params: Promise<{ cid: string }>;
}) {
  const { cid } = await params;
  return (
    <div id="wd-assignments">
      {/* search input, + Group, + Assignment */}
      {/* h3 wd-assignments-title */}
      <ul id="wd-assignment-list">
        <AssignmentItem
          cid={cid}
          aid="123"
          title="A1 - ENV + HTML"
          details="Multiple Modules | Due May 13 at 11:59pm | 100 pts"
        />
        {/* Add more assignments here (three recommended, like A2, A3), each linking to its editor */}
      </ul>
    </div>
  );
}

The page as written renders one assignment:

Assignmentsapp/(kambaz)/courses/[cid]/assignments/page.tsx

Expected result (plain HTML prototype — styling comes in Chapter 2):

AssignmentsDemoapp/book/ch1/embeds/AssignmentsDemo.tsx

Courses 1234


ASSIGNMENTS 40% of Total

  • A1 - ENV + HTML
    Multiple Modules | Not available until May 6 at 12:00am |
    Due May 13 at 11:59pm | 100 pts
  • A2 - CSS + TAILWIND
    Multiple Modules | Not available until May 13 at 12:00am |
    Due May 20 at 11:59pm | 100 pts
  • A3 - JAVASCRIPT + REACT
    Multiple Modules | Not available until May 20 at 12:00am |
    Due May 27 at 11:59pm | 100 pts

Finished page for lecture: the search field, + Group, + Assignment, the wd-assignments-title heading, and A1–A3 written out with the same AssignmentItem component above.

Assignments (finished)app/(kambaz)/courses/[cid]/assignments/page.tsx
import AssignmentItem from "./AssignmentItem";

export default async function Assignments({
  params,
}: {
  params: Promise<{ cid: string }>;
}) {
  const { cid } = await params;
  return (
    <div id="wd-assignments">
      <input
        placeholder="Search for Assignments"
        id="wd-search-assignment"
      />{" "}
      <button id="wd-add-assignment-group">+ Group</button>{" "}
      <button id="wd-add-assignment">+ Assignment</button>
      <h3 id="wd-assignments-title">
        ASSIGNMENTS 40% of Total <button>+</button>
      </h3>
      <ul id="wd-assignment-list">
        <AssignmentItem
          cid={cid}
          aid="123"
          title="A1 - ENV + HTML"
          details="Multiple Modules | Not available until May 6 at 12:00am | Due May 13 at 11:59pm | 100 pts"
        />
        <AssignmentItem
          cid={cid}
          aid="124"
          title="A2 - CSS + TAILWIND"
          details="Multiple Modules | Not available until May 13 at 12:00am | Due May 20 at 11:59pm | 100 pts"
        />
        <AssignmentItem
          cid={cid}
          aid="125"
          title="A3 - JAVASCRIPT + REACT"
          details="Multiple Modules | Not available until May 20 at 12:00am | Due May 27 at 11:59pm | 100 pts"
        />
      </ul>
    </div>
  );
}
Assignments (finished)app/(kambaz)/courses/[cid]/assignments/page.tsx

ASSIGNMENTS 40% of Total

  • A1 - ENV + HTML
    Multiple Modules | Not available until May 6 at 12:00am | Due May 13 at 11:59pm | 100 pts
  • A2 - CSS + TAILWIND
    Multiple Modules | Not available until May 13 at 12:00am | Due May 20 at 11:59pm | 100 pts
  • A3 - JAVASCRIPT + REACT
    Multiple Modules | Not available until May 20 at 12:00am | Due May 27 at 11:59pm | 100 pts

1.4.8 Assignment Editor Screen (On Your Own)

Clicking an assignment title opens the Assignment Editor at app/(kambaz)/courses/[cid]/assignments/[aid]/page.tsx. Faculty edit the assignment's details there. The polished target is below; this chapter only needs a plain HTML form. Start from the stub (name, description, points), then complete the rest on your own. Use the ids listed: later chapters build on them, and the ids help us test your work.

Target Kambaz Assignment Editor screen
Figure 1.4.8a — Assignment Editor
AssignmentEditorapp/(kambaz)/courses/[cid]/assignments/[aid]/page.tsx
export default function AssignmentEditor() {
  return (
    <div id="wd-assignments-editor">
      <label htmlFor="wd-name">Assignment Name</label>
      <input id="wd-name" defaultValue="A1 - ENV + HTML" /> <br /><br />
      <textarea id="wd-description">
        The assignment is available online Submit a link to the landing page of
      </textarea>                                           <br />
      <table>
        <tbody>
          <tr>
            <td align="right" valign="top">
              <label htmlFor="wd-points">Points</label>
            </td>
            <td>
              <input id="wd-points" defaultValue={100} />
            </td>
          </tr>
          {/* Complete on your own — see checklist below */}
        </tbody>
      </table>
    </div>
  );
}

Complete on your own — add these controls (with htmlFor/id pairs):

  • Assignment Group select — id wd-group (options ASSIGNMENTS, QUIZZES, EXAMS, PROJECT)
  • Display Grade as select — id wd-display-grade-as
  • Submission Type select — id wd-submission-type
  • Online Entry Options checkboxes — ids wd-text-entry, wd-website-url, wd-media-recordings, wd-student-annotation, wd-file-upload
  • Assign section — Assign to (wd-assign-to), Due (wd-due-date), Available from (wd-available-from), Until (wd-available-until)
  • Cancel and Save links back to the assignments list — ids wd-cancel and wd-save

For Display Grade as and Submission Type, the options shown in the figure (for example Percentage and Online) are enough; add more if you like.

Label behavior must work the way Lab 1 forms do:

  • Clicking a label next to or above a text field focuses that field.
  • Clicking a label next to a checkbox toggles the checkbox.
  • Clicking a label above a date input focuses the date field.

For now every assignment can show the same editor content — link each title to /courses/[cid]/assignments/[aid]. Later chapters will load details for a specific aid. The editor below is a labeled form for points, dates, and description — clicking A1 should open it, and Cancel/Save should return to the assignments list.

AssignmentEditorDemoapp/book/ch1/embeds/AssignmentEditorDemo.tsx





Online Entry Options




Assign








Cancel Save

The finished editor behind that demo, with every control from the checklist:

AssignmentEditor (finished)app/(kambaz)/courses/[cid]/assignments/[aid]/page.tsx
import Link from "next/link";

export default function AssignmentEditor() {
  return (
    <div id="wd-assignments-editor">
      <label htmlFor="wd-name">Assignment Name</label>
      <input id="wd-name" defaultValue="A1 - ENV + HTML" /> <br /><br />
      <textarea
        id="wd-description"
        cols={50}
        rows={6}
        defaultValue="The assignment is available online Submit a link to the landing page of your Web application running on Vercel."
      /> <br />
      <table>
        <tbody>
          <tr>
            <td align="right" valign="top">
              <label htmlFor="wd-points">Points</label>
            </td>
            <td>
              <input id="wd-points" defaultValue={100} />
            </td>
          </tr>
          <tr>
            <td align="right" valign="top">
              <label htmlFor="wd-group">Assignment Group</label>
            </td>
            <td>
              <select id="wd-group" defaultValue="ASSIGNMENTS">
                <option value="ASSIGNMENTS">ASSIGNMENTS</option>
                <option value="QUIZZES">QUIZZES</option>
                <option value="EXAMS">EXAMS</option>
                <option value="PROJECT">PROJECT</option>
              </select>
            </td>
          </tr>
          <tr>
            <td align="right" valign="top">
              <label htmlFor="wd-display-grade-as">Display Grade as</label>
            </td>
            <td>
              <select id="wd-display-grade-as" defaultValue="PERCENTAGE">
                <option value="PERCENTAGE">Percentage</option>
                <option value="POINTS">Points</option>
              </select>
            </td>
          </tr>
          <tr>
            <td align="right" valign="top">
              <label htmlFor="wd-submission-type">Submission Type</label>
            </td>
            <td>
              <select id="wd-submission-type" defaultValue="ONLINE">
                <option value="ONLINE">Online</option>
                <option value="ON_PAPER">On Paper</option>
              </select>            <br /><br />
              Online Entry Options <br />
              <input type="checkbox" id="wd-text-entry" />
              <label htmlFor="wd-text-entry">Text Entry</label> <br />
              <input type="checkbox" id="wd-website-url" />
              <label htmlFor="wd-website-url">Website URL</label> <br />
              <input type="checkbox" id="wd-media-recordings" />
              <label htmlFor="wd-media-recordings">Media Recordings</label> <br />
              <input type="checkbox" id="wd-student-annotation" />
              <label htmlFor="wd-student-annotation">Student Annotation</label> <br />
              <input type="checkbox" id="wd-file-upload" />
              <label htmlFor="wd-file-upload">File Uploads</label>
            </td>
          </tr>
          <tr>
            <td align="right" valign="top">
              Assign
            </td>
            <td>
              <label htmlFor="wd-assign-to">Assign to</label>     <br />
              <input id="wd-assign-to" defaultValue="Everyone" /> <br /><br />
              <label htmlFor="wd-due-date">Due</label>            <br />
              <input type="date" id="wd-due-date"
                     defaultValue="2024-05-13" /> <br /><br />
              <table>
                <tbody>
                  <tr>
                    <td>
                      <label htmlFor="wd-available-from">Available from</label> <br />
                      <input type="date" id="wd-available-from"
                             defaultValue="2024-05-06" />
                    </td>
                    <td>
                      <label htmlFor="wd-available-until">Until</label> <br />
                      <input type="date" id="wd-available-until"
                             defaultValue="2024-05-20" />
                    </td>
                  </tr>
                </tbody>
              </table>
            </td>
          </tr>
        </tbody>
      </table>
      <hr />
      <Link href="/courses/1234/assignments" id="wd-cancel">Cancel</Link>{" "}
      <Link href="/courses/1234/assignments" id="wd-save">Save</Link>
    </div>
  );
}

1.4.9 Exercises

Use this checklist to confirm the Kambaz prototype covers every screen in §1.4. Each item points back to the section where you built the worked example. Build the screens in order as you read — this list is for checking coverage, not a substitute for the walkthroughs. Assignments and the Assignment Editor stay On your own: match the ids and interactive demos in those sections.

  1. Create the Kambaz landing page in app/(kambaz)/page.tsx with id wd-kambaz and an h1 heading (§1.4.1).
  2. Remove any leftover root app/page.tsx and ensure the Kambaz folder is named (kambaz) with parentheses so it is a route group (§1.4.1).
  3. Update app/labs/TOC.tsx and app/labs/page.tsx with a Link to / and id wd-kambaz-link (§1.4.1).
  4. Create Sign in in app/(kambaz)/account/signin/page.tsx (§1.4.2.1).
  5. Redirect app/(kambaz)/account/page.tsx and app/(kambaz)/page.tsx to /account/signin (§1.4.2).
  6. Implement Sign up in app/(kambaz)/account/signup/page.tsx (§1.4.2.2).
  7. Create Profile in app/(kambaz)/account/profile/page.tsx with date, email, and role select (§1.4.2.3).
  8. Build app/(kambaz)/account/Navigation.tsx and wrap account routes in app/(kambaz)/account/layout.tsx (nav left, children right) (§1.4.2.4).
  9. Extract CourseCard and implement the Dashboard in app/(kambaz)/dashboard/page.tsx with at least three cards linking to /courses/[cid]/home (§1.4.3).
  10. Create app/(kambaz)/Navigation.tsx and app/(kambaz)/layout.tsx (Kambaz Navigation left, children right). Point Sign in (wd-signin-btn) at /dashboard (§1.4.3.1).
  11. Implement app/not-found.tsx so Calendar and Inbox use it with a link back to the Dashboard (§1.4.3.2).
  12. Add Course Navigation and app/(kambaz)/courses/[cid]/layout.tsx. Include placeholder pages for Piazza, Zoom, Quizzes, Grades, and People (§1.4.4).
  13. Create Module and Lesson, then the Modules page with nested weeks, lessons, and content items (§1.4.5).
  14. Build Course Status and assemble Home (Modules plus Status). Delete any leftover page.tsx directly under courses/[cid]/ (§1.4.6).
  15. Build the Assignments screen (§1.4.7).
  16. Build the Assignment Editor screen (§1.4.8).

1.5 Committing Code to Source Control

Slides

So far the app runs only on your machine. To share it — and to deploy it in the next section — you put a copy of the source on GitHub, a hosting service for Git repositories. Git is the tool that records snapshots of your project (commits) and syncs them with a remote server. Create a public repository named webdev-client on GitHub, then push from your project folder.

Before you commit, confirm the project .gitignore file lists folders that should not be uploaded — especially node_modules (huge, regenerable with npm install) and IDE folders such as .idea. The starter usually includes a suitable .gitignore; do not remove those entries.

The usual flow: git add stages files for the next snapshot, git commit saves that snapshot with a message, git remote add origin … points your local repo at GitHub, and git push uploads commits to the main branch:

git add .
git commit -m "first commit"
git remote add origin https://github.com/<you>/webdev-client.git
git push -u origin main

GitHub no longer accepts account passwords for git push over HTTPS. If authentication fails, create a Personal Access Token (PAT) under GitHub → Settings → Developer settings → Personal access tokens, then paste the token when the terminal asks for a password. Keep the token private — treat it like a password.

Now that the repository exists, put your name, your section, and a link to the repository on the Labs index, so graders can tell whose deploy they are looking at and find its source. Open app/labs/page.tsx and add three things to the page from §1.3.10:

  • An h2 with your full name exactly as it appears in Canvas, first name first and last name second.
  • A paragraph with your course and section from Canvas, such as CS4550 Section 01 or CS5610 Section 09.
  • A link to your webdev-client repository on GitHub with id wd-github. Use your own repository URL, not the one in the sample.

Keep the lab links you already have: your Lab 4 and Lab 5 links go where the comment is, and keep the Kambaz link from §1.4.1. Replace the name, section, and GitHub URL below with your own:

LabsNameGithubapp/labs/page.tsx
import Link from "next/link";

export default function Labs() {
  return (
    <div id="wd-labs">
      <h1>Labs</h1>
      <h2>Jose Annunziato</h2>
      <p>CS4550 Section 01</p>
      <ul>
        <li>
          <Link href="/labs/lab1">Lab 1: HTML Examples</Link>
        </li>
        <li>
          <Link href="/labs/lab2">Lab 2: CSS Basics</Link>
        </li>
        <li>
          <Link href="/labs/lab3">Lab 3: JavaScript Fundamentals</Link>
        </li>
        {/* your Lab 4 / Lab 5 links */}
        <li>
          <Link href="/" id="wd-kambaz-link">
            Kambaz
          </Link>
        </li>
        <li>
          <a
            href="https://github.com/jannunzi/webdev-client"
            id="wd-github"
            target="_blank"
            rel="noreferrer"
          >
            GitHub repository
          </a>
        </li>
      </ul>
    </div>
  );
}

Your name and section sit under the Labs heading, and the GitHub link opens your repository in a new tab:

LabsNameGithubapp/labs/page.tsx

Commit and push the change:

git add .
git commit -m "Add name, section, and GitHub link to Labs"
git push

1.6 Deploying Next.js Projects to the Web

Slides

Deploying means hosting the running app on a public server so anyone with the URL can open it. Create a Vercel account, import the GitHub webdev-client repo, and deploy with the Next.js preset (Vercel usually detects Next.js automatically).

After the first deploy, open the project's settings and disable Deployment Protection (sometimes labeled as a Vercel Authentication / password gate on preview or production URLs). Graders must open your site without logging into Vercel. Hand in A1 on kambaz.dev, not in Canvas: sign in at kambaz.dev/assignments/a1 and submit both the GitHub repository URL and the Vercel deployment URL there. Your grade is posted in Canvas.

1.7 Engineering with AI (optional, not graded)

AI is quickly becoming an indispensable tool in most industries, bringing unprecedented opportunity as well as uncertainty and angst. As software engineers in general, and full stack developers in particular, we must learn to use AI throughout the day-to-day software development lifecycle. This section is the first of several throughout the book that teach industry-leading practices for using AI to build Web applications. To practice these skills, we will build a new feature for Kambaz, the AI-Assisted Course Builder, and learn to work with an AI assistant the way a software engineer does on a professional team. A course author will be able to use the Course Builder to draft a course title from a description, a description from a title, a list of modules, and the lessons inside each module. We will build the feature one layer at a time over the next several chapters, and at each layer AI will take on a different part of the work. In this chapter, you will ask an AI assistant to turn the feature's requirements into a static HTML prototype, and then you will review its markup line by line before you commit it. In Chapter 2, you will ask the assistant to style those controls with Tailwind CSS and to explain the classes it chose, so that you understand every style you accept. In Chapter 3, the assistant will generate a local JSON file of realistic sample suggestions, and you will check that the data matches the shape your components expect. In Chapter 4, the assistant will draft the Zustand client-side store that holds the draft, along with tests that confirm it behaves as specified. A later chapter will call an AI model directly from a Next.js route handler, a function on the server that receives a request and sends back a response, so that the Course Builder can generate real suggestions. Throughout, AI will draft and explain while you decide what ships, which is how production teams at companies such as Amazon and Google use AI assistants.

In a product team, a new feature or bug fix moves through the software development lifecycle, the sequence of stages from the written requirement to production. Production is the environment where the released application runs for its users, separate from the copy on the developer's machine. A product manager writes what the user needs and how the team will recognize finished work, the team tracks that work, and a software engineer designs the change, isolates it, writes the code, and checks it. The engineer then asks other developers to read the change, the project runs its automated checks, and the engineer combines the change into the main line, publishes it, and watches the released application. Across those stages an AI assistant helps the software engineer build a prototype or implement a feature, and it helps write unit tests, which check one piece of code, and integration tests, which check that the pieces work together. The same AI assistant can create a branch, rebase, merge, and open the proposal to merge, review that proposal, draft the requirements and the tracked work item, debug a failure, write documentation, and automate a repeated process such as the script those automated checks run. The software engineer and the product manager stay accountable for the decisions, and the exercises below follow the lifecycle through the requirements, the isolated branch, the implementation, a manual check, and the proposal to merge, stopping before the merge you already practice for this chapter in §1.5 and §1.6.

The lifecycle begins with the product requirements, which a product manager writes so the team agrees on the feature or the bug fix before a software engineer edits any code. A user story is one sentence of that document. It names who the user is, what they can do, and why that capability matters. Acceptance criteria are the checks that define finished work, such as which labels appear on the screen and what stays unchanged when the user clicks. An assistant can draft the stories and the criteria from the product manager's notes. The product manager is accountable for accepting the document the software engineer will build.

The team then files that agreed work as an issue, also called a ticket, in an issue tracker, the shared list where the team records work and its status. Jira is a common tracker, and GitHub Issues and Linear are others. The issue stores the requirement, the acceptance criteria, and a status that moves from to do, to in progress, to in review, and then to done, so a software engineer can pick the issue up and know which feature or bug fix it describes. An AI assistant can draft the issue text from the requirements. The product manager, or the developer who files the issue, sets its scope and assigns it, and the software engineer who takes it is accountable for building what it says.

Design is the stage in which the software engineer decides the shape of the change before editing files. The engineer chooses which screens will change, which files those screens live in, and which requests belong in a later issue. An AI assistant can propose that list of files from the requirements. The engineer selects the design and writes it on the issue, so the implementation has a target the reviewer can compare with the code.

The software engineer then isolates the work on a feature branch, a separate line of Git commits started from main with git checkout -b. The unfinished commits stay off main until code review, using the same commits you already make in §1.5. An AI assistant can create that branch. The software engineer chooses the branch name and remains accountable for every commit on it.

Implementation is the stage in which the code for the issue is written on that branch. An AI assistant can draft the code from the requirements and from the files named in the design. The software engineer then reads the diff, Git's line-by-line record of which lines were added and which were removed, and compares that record with the acceptance criteria. The engineer keeps only the lines they can explain, because those are the lines they will defend in review.

Tests are the stage in which the software engineer checks that the branch behaves as the issue requested. The check may be an automated command, or a manual pass through the screens. An assistant can draft a unit test, an integration test, or a checklist for that pass. The engineer runs the check and judges whether the behavior matches the acceptance criteria, and the engineer is accountable for that judgment.

When the check is satisfactory, the software engineer opens a pull request, the GitHub page that proposes merging the feature branch into main and shows the diff to reviewers. Code review is that reading of the diff. An AI assistant can draft the pull request description and can add review comments. The software engineer who opened the pull request decides which comments match the requirements and which ask for work the issue did not include, and that engineer is accountable for what the pull request contains.

Continuous integration, or CI, is the stage in which the repository runs its build and its tests on the pull request automatically. An assistant can help the software engineer read the log when a check fails, and it can draft the script those checks run. A green result is evidence that the automated steps passed. The engineer decides whether the feature is ready to merge.

Merge is the stage that copies the reviewed feature branch onto main, and deploy is the stage that publishes main to the environment where the released application runs, which you already do for this chapter in §1.6. An AI assistant can draft the short note that describes what changed, and it can run the merge or the deploy when the software engineer asks it to. The engineer who merges and deploys confirms that the released application is the one the reviewers read.

After deploy, the software engineer monitors the released application. Monitoring means reading error logs and failed requests, and checking that the feature still meets the acceptance criteria. An assistant can summarize a log. The engineer decides whether the behavior is an incident and which new issue to file, which starts the lifecycle again.

1.7.1 Owning the code

During implementation, software engineers are responsible for deciding how to best incorporate AI assistants into the process. It is best to think of them as very capable software engineers themselves, but you retain the criteria, decisions, and responsibility. For example, you might restrict AI assistants to generating initial drafts that require human review before merging, or allow them to handle routine refactoring tasks with minimal oversight.The key is establishing clear boundaries and workflows. You must decide what AI assistants are allowed to do, how they participate in the software development cycle, and how to review content generated by assistants. AI assistants can draft prototype implementations from the requirements and the files you include in the prompt. They can also help with code review, documentation, testing strategies, and debugging. However, they should not be given unilateral decision-making authority over architectural choices, security implementations, or critical system components without human verification.

That accountability also covers secrets, the values that would let someone else act as you or read private data. A secret includes the personal access token from §1.5, anything in a .env file, a password, and private details about real people. Keep those values out of the browser chat, out of the command-line tool, and out of the panel in the editor. When the assistant needs to know that a file exists, describe the file in a sentence and leave its contents on your machine, because a value pasted into the conversation has left your machine.

1.7.2 Chat, the CLI, and the IDE

Claude is an AI assistant from Anthropic that a software engineer can use in three places, depending on whether the task is advice, an edit across files, or a suggestion beside one open file. In the browser you talk to Claude and copy the reply into the project, which fits a plan, a checklist, a commit message, or a pull request description. In the terminal you run Claude against the project so it can apply an edit, which fits a change that spans more than one file. In the editor you open Claude next to the file you are changing, which fits a smaller edit. This section assumes you already have a Claude account. A personal account works, and so does a Team or Enterprise account from your organization.

The terminal way of working has a name of its own. Typing a program's name in the terminal, instead of clicking an icon, is a command-line interface, shortened to CLI. The browser, the CLI, and the editor are called surfaces, the places where you type to the same AI assistant. Choose one surface for a task. Repeating the edit on all three bills the account three times and produces three diffs to reconcile.

The browser surface is the chat at claude.ai, which you use when you want text back and you will apply the edit yourself. Start a new chat and paste the requirements document before you ask for the plan or the checklist, so the reply stays on that document. The chat returns text and does not edit the repository. You copy into the project only the parts that match the acceptance criteria.

Claude Code is the CLI surface, the same AI assistant started as a command in your project folder when the edit should change more than one file. Install it from the Claude Code setup page. Then, from the folder that contains your webdev-client project, run claude. The first time, it asks you to sign in in the browser; after that it remembers the account. Name the files to touch, paste the requirements, and read the diff it offers before you accept the edit. Accepting writes the files. Rejecting leaves them as they were.

cd path/to/your/webdev-client
claude

The first line moves the terminal into your project, the same way you cd before npm run dev. Replace path/to/your/webdev-client with the real folder. The second line starts Claude in that folder, so the session is about this project. When it asks which files to change, name the modules page and Module.tsx, because those are the files the issue tells you to edit. Read the diff before you accept it, so the files change only after you have compared them with the acceptance criteria.

When the edit is confined to a file you already have open, use the editor surface, which is the Claude Code extension you installed in §1.2.3. Open the file, start Claude Code the way that section showed, and paste the same requirements document, so the suggestion appears next to the code you are reviewing. In Cursor that panel is still Claude Code, separate from the editor's built-in chat. This exercise uses the Claude account you signed into in §1.2.3.

1.7.3 Token budgeting

Each of those surfaces bills your Claude account by the token, a small chunk of text that Claude reads or writes, so the prompt, the files you attach, and the reply are all counted together. A prompt that carries the requirements document and the two files you are changing costs less than a chat that rereads the whole repository. Most of the repository is files this issue does not name. Send the requirements and those two files, leave the rest out, and attach the same requirements file again when you start a fresh chat, so you do not retype the user stories from memory.

The account also lets you pick a model, the particular assistant that produces the reply. The picker offers a cheaper, faster model and a stronger, slower one. Use the cheaper model for a commit message, for the pull request description, or for a yes-or-no check against the acceptance criteria. Use the stronger model when the edit touches several files and has to keep them consistent.

A cost log is the notebook where you record that choice, one row per task, written before you start the next task. Each row notes the surface, the model, the file names you sent, and whether you kept the result, while you still remember them. If a task takes many retries, stop and narrow the prompt to the files the issue names. Leave the log out of the commit, because it can pick up an account detail that does not belong in the repository.

Copy the header below and replace the two sample rows with your own. The columns are the date, a few words naming the task, the surface, whether you picked the cheaper or the stronger model, the file names you sent, and whether you kept the result:

Cost log
Date       Task                     Tool   Model     Context sent                          Kept?
---------- ------------------------ ------ --------- ------------------------------------- -----
2026-11-02 Add static AI buttons    CLI    stronger  requirements + two module files      yes
2026-11-02 Commit message           chat   cheaper   staged file names only               yes

1.7.4 Requirements for the AI Assisted Course Builder

The implementation follows a requirements document a product manager could review. The sample below is that document for the AI Assisted Course Builder, written with the four parts from the lifecycle. The overview says what this chapter will build and what later chapters will add. Each user story is one sentence in the form "as a course author, I can …, so that …", which describes the capability the course author will have. The acceptance criteria are the checks you will run in the browser. Out of scope, at the bottom of the document, lists the work this chapter does not include.

Save the sample as docs/ai-course-builder.md on the feature branch you create in §1.7.6. Later prompts can then point at that filename, and you can attach the file again instead of pasting the user stories into every chat. When you open the file you should see the overview, five user stories, the acceptance criteria, and the out-of-scope list. The button labels are written out so you can compare them with the screen.

Requirementsdocs/ai-course-builder.md
# AI Assisted Course Builder — static controls

## Overview

Course authors need an AI Assisted Course Builder on Kambaz. This chapter only places the controls. They are static HTML. Clicking a control does not call a model, edit the title or description, or insert a module or lesson.

Later chapters of this same feature style the controls with Tailwind, show sample suggestions from a local JSON file, keep a draft in Zustand, and call Claude from a Next.js route handler. This chapter does none of that. There is no mock AI API.

## User stories

1. As a course author, I can open the course editor, read the title and the description, and see "Generate description from title" and "Generate title from description", so I know where those actions will live.
2. As a course author, I can open Modules and see "AI: suggest modules" beside "+ Module", so I know where a suggested module list will start.
3. As a course author, I can see "AI: next module" after the module list, so I know where one more module will be offered.
4. As a course author, inside every module I can choose a lesson type of Reading, Slides, or Quiz and see "Generate lessons", so I know where a set of lessons of that type will be offered.
5. As a course author, inside every module I can see "AI: next lesson" after that module's lessons, so I know where one more lesson will be offered.

## Acceptance criteria

- New page at app/(kambaz)/courses/[cid]/editor/page.tsx. It shows a Title field, a Description field, and both generate buttons. Clicking a button leaves both fields unchanged.
- Optional ids on that page, used at most once: wd-course-editor, wd-course-title, wd-course-description, wd-generate-description, wd-generate-title.
- The Modules page already in the project stays. Collapse All, View Progress, the publish select, "+ Module", and every existing week, lesson, and content item remain.
- "AI: suggest modules" is the next control after "+ Module".
- "AI: next module" appears once, after the module list, outside every module.
- Optional ids for those two buttons: wd-ai-suggest-modules, wd-ai-next-module.
- Every module shows a "Lesson type" select, then "Generate lessons", then that module's existing lessons, then "AI: next lesson".
- The select options are exactly Reading, Slides, and Quiz. Slides means a slides.js lesson. This chapter does not build the deck.
- Controls that repeat inside every module use a class, so one id is not copied onto every module. Optional classes: wd-lesson-type, wd-generate-lessons, wd-ai-next-lesson.
- Every new button is type="button". No onClick. No fetch. No form element, so Enter in a field does not submit.
- The edited files stay Server Components. No "use client" line.
- Course Navigation is unchanged. The editor URL is /courses/1234/editor.

## Out of scope

- A call to Claude or any other model
- A mock AI service, or any new API route
- API keys, tokens, passwords, or environment files
- Tailwind, Bootstrap, or any other styling
- useState, Zustand, or any other client state
- A JSON file of suggestions
- A Video lesson type
- Edits to Dashboard, assignments, account screens, or Course Navigation
- Deleting or rewriting existing modules and lessons

1.7.5 Course editor starter

The Modules screen from §1.4.5 is already in your project, with the toolbar that contains + Module and the weeks built from Module and Lesson, so you extend that page and Module.tsx and you keep every week you have already typed.

Chapter 1 has no course editor, because the Dashboard in this chapter is a list of course cards and the title and description form arrives later, on that Dashboard, in §4.10.2, which is where that exercise stays. What this section needs today is a short static page with a title field and a description field, so the generate buttons have fields to sit under while you practice the lifecycle.

Create app/(kambaz)/courses/[cid]/editor/page.tsx and paste the starter below. A page.tsx file is the route, the same rule as §1.2.5 and §1.4.4, because the folder path becomes the URL, which makes this file /courses/1234/editor when 1234 is the course id. The page is a Server Component by default, which means the browser receives the HTML and there is no use client line at the top, and that is what you want while the buttons do nothing. Adding onClick would require the file to become a Client Component, a component that runs in the browser, and that behavior belongs in a later chapter. The courses layout from §1.4.4.1 wraps every page under [cid], including this new one, so Course Navigation stays as you built it and you leave that sidebar without an Editor link. Open /courses/1234/editor in the dev server you started with npm run dev.

CourseEditorapp/(kambaz)/courses/[cid]/editor/page.tsx
export default function CourseEditor() {
  return (
    <div id="wd-course-editor">
      <h2>Course Editor</h2>
      <label htmlFor="wd-course-title">Title</label>
      <br />
      <input id="wd-course-title" defaultValue="Web Development" />
      <br />
      <br />
      <label htmlFor="wd-course-description">Description</label>
      <br />
      <textarea
        id="wd-course-description"
        rows={4}
        cols={40}
        defaultValue="Build web applications with HTML."
      />
    </div>
  );
}

The starter is only the fields. The heading reads Course Editor, the label Title is tied to the input by htmlFor matching id="wd-course-title", and the description is a textarea whose defaultValue already holds a sample sentence, which sets that initial text without useState so the page is not blank when you first open it. The id values are suggestions, the same wd- names you have used since the HTML labs, and a control that appears once on the page can carry an id while a control copied into every module should use a class, because an id is unique in the document and a repeated id is invalid HTML.

Try the prompt in §1.7.6 before you paste the finished controls by hand, and use the snippets that follow as the reference you compare the assistant's diff against, copying them only when the diff missed a label. Under the textarea, the two generate buttons read Generate description from title and Generate title from description, and type="button" keeps them from submitting a form. There is no onClick, so clicking either button leaves both fields exactly as they were:

Editor buttons
<button type="button" id="wd-generate-description">
  Generate description from title
</button>{" "}
<button type="button" id="wd-generate-title">
  Generate title from description
</button>

On the Modules page, leave Collapse All, View Progress, and the publish select as you wrote them, and place the new button immediately after the existing + Module button, with the label AI: suggest modules, which is where a suggested list of modules will eventually start. The {" "} between the two buttons is the same space you already use so the labels do not run together:

Modules toolbar
<button>+ Module</button>{" "}
<button type="button" id="wd-ai-suggest-modules">
  AI: suggest modules
</button>

After the module list, which means after the closing </ul> of wd-modules and outside every week, add one button labeled AI: next module. It appears once, under the whole list, because the next module belongs to the course, while a new lesson belongs inside one week:

After the module list
<button type="button" id="wd-ai-next-module">
  AI: next module
</button>

Inside each module the new controls belong on Module, so every week receives them from that one component and you do not copy the markup into page.tsx. Under the title, a label reads Lesson type and wraps a select whose options are exactly Reading, Slides, and Quiz. Slides here means a slides.js lesson, the same kind of deck this book uses for its own slides, and this chapter only shows that option in the select, leaving the deck itself, and any Video option, out of the file. Generate lessons sits beside the select and above the lessons you already wrote, and AI: next lesson sits after wd-lessons, which places it last in the week, under those lessons. The select uses className="wd-lesson-type" and the two buttons use classes as well, because this component renders once per week and an id would be duplicated on every week:

Moduleapp/(kambaz)/courses/[cid]/modules/Module.tsx
import type { ReactNode } from "react";

export default function Module({
  title,
  children,
}: {
  title: string;
  children?: ReactNode;
}) {
  return (
    <li className="wd-module">
      <div className="wd-title">{title}</div>
      <label>
        Lesson type{" "}
        <select className="wd-lesson-type" defaultValue="Reading">
          <option value="Reading">Reading</option>
          <option value="Slides">Slides</option>
          <option value="Quiz">Quiz</option>
        </select>
      </label>{" "}
      <button type="button" className="wd-generate-lessons">
        Generate lessons
      </button>
      <ul className="wd-lessons">{children}</ul>
      <button type="button" className="wd-ai-next-lesson">
        AI: next lesson
      </button>
    </li>
  );
}

If your Module.tsx still matches the component from §1.4.5, the reference above is a safe replacement, because it is that component plus the label, the select, and the two buttons. If you have already changed the file, add those three controls around the title and the children you already render, and leave your own markup in place, so that when you open Modules you still see every week you wrote. The figure below shows one sample module so the new controls are easy to find, and your page keeps the rest of the weeks around it.

AiCourseBuilderDemoapp/book/ch1/embeds/AiCourseBuilderDemo.tsx

Course Editor








  • Week 1, Lecture 1 - Course Introduction, Syllabus, Agenda
    • READING
      • Full Stack Developer - Chapter 1 - Introduction

1.7.6 Exercises

The steps below are the part of the lifecycle you carry out on your own project, from the feature branch through the pull request, and each violet block is a sample prompt of the same kind you have copied since §1.2.3. Add the file names and the mismatch you actually see, read the reply before you keep it, and add a row to the cost log after each prompt you send.

  1. Create the feature branch described above so this work stays off main until the pull request is reviewed. Commit what you want to keep, or stash it, which sets the uncommitted edits aside without making a commit, and then look at git status, which should list nothing left to commit, before git checkout -b creates the branch named feature/ai-buttons and switches you onto it, the name the later prompts refer to:

    git status
    git checkout -b feature/ai-buttons

    After the second command, git status should report that you are on feature/ai-buttons, and everything you commit from here stays off main until you open the pull request.

  2. Save docs/ai-course-builder.md from §1.7.4 and create the course editor file from the starter in §1.7.5 yourself, before you ask the assistant for anything, so the requirements file and the title and description fields already exist when the prompt runs. Leave page.tsx and Module.tsx under modules for the assistant, because those files already contain your weeks and the next step is where you check that the edit keeps them.

  3. Prompt Claude with the requirements, and if you are using the CLI or the editor, let it edit the files and then read the diff before you accept it, while if you are using the browser chat the reply is text that you apply only after you have checked it against the reference snippets. The prompt tells the assistant to show the diff and stop, so the reply is a diff you can read before any further edit.

  4. Review the diff the way a software engineer reviews a teammate's pull request, which means you open every changed file and remove a line you cannot explain, and then run npm run dev and compare both screens with the acceptance criteria, the same check you will ask the AI assistant to make in a moment. On /courses/1234/editor you should see the title, the description, and both generate buttons, and clicking each button leaves the text in place and the address bar unchanged, because the buttons have no handler and no form to submit. On /courses/1234/modules your weeks are still there, including Week 1, with AI: suggest modules beside + Module and AI: next module once after the list, while each module shows the lesson-type select, then Generate lessons, then the lessons you already had, then AI: next lesson, and the select lists only Reading, Slides, and Quiz. The browser console stays quiet, and clicking a new button does not navigate away, so any of that which is missing is a mismatch you fix before you commit.

  5. A commit message is the one-line summary of the diff, written in the imperative mood you already used in §1.5, such as "Add …" where the verb names the change that is happening now. Ask for that line with the cheaper model, then read it, and discard a message that claims an API or a model call, because this commit only adds static buttons. The prompt tells the assistant not to run git commit, so the reply should be the sentence and nothing further.

    Stage only the files you reviewed, and quote any path that contains parentheses, because the shell otherwise treats (kambaz) as a grouping and the add fails. Commit with the message you accepted, then push the branch. git push -u uploads it and remembers origin, the remote you added in §1.5, for later pushes:

    git add docs/ai-course-builder.md "app/(kambaz)/courses/[cid]/editor" "app/(kambaz)/courses/[cid]/modules"
    git commit -m "Add static AI Assisted Course Builder buttons"
    git push -u origin feature/ai-buttons

    Replace the sample message with the line you reviewed, and after the push GitHub has the branch while main on the remote is unchanged.

  6. On GitHub, open a pull request from feature/ai-buttons into main, paste a description you have already read, and leave the pull request open, because this exercise stops at code review and the merge onto main is a later decision.

  7. Ask the AI assistant to review the pull request, and then decide, as the software engineer who opened it, which comments to apply. A comment is valid when it matches docs/ai-course-builder.md or when it points at a bug, such as a deleted week or an id copied onto every module, and a comment is outside this issue when it asks for an API, for styling, for state, or for a Video lesson. Apply only the valid comments, as a new commit on the same branch, and push again so the pull request shows the update.

What the AI assistant got wrong. When you compare the diff with the requirements, look for a click handler, a Tailwind or Bootstrap class, a Video option, a new route, or a rewrite of Week 1, each of which sits outside the acceptance criteria, and remove it, then write down what you changed while you still remember the prompt that produced it.

1.8 Conclusion

By the end of this chapter you should have:

  1. Installed Node.js and created webdev-client.
  2. Completed all Lab 1 HTML exercises.
  3. Prototyped Kambaz screens with HTML and React.
  4. Pushed the project to GitHub.
  5. Added your full name, your section, and a wd-github link to your repository on the Labs page (§1.5).
  6. Deployed to Vercel and submitted both URLs on kambaz.dev/assignments/a1.

Continue practicing in Labs, browse Lab 1 intermediate steps, or open the live Kambaz prototype.

§1.7 is the assistant practice on a feature branch, and it is separate from the checklist above.

1.9 References

The names below are the ones this chapter actually introduced — the Web's history, the HTML you used to prototype screens, the Next.js App Router, and the GitHub and Vercel delivery steps. Each linked term opens the in-book term page for the official site. Explainer videos on those pages are optional reference.

These ideas also matter in this chapter even though they do not have their own term pages yet:

  • HTML headings, paragraphs, lists, tables, images, and forms
  • Props and children for reusable React components
  • App Router pages, layouts, and anchor navigation
  • Git commits, remotes, and personal access tokens
  • Deploying a Next.js app and turning off Deployment Protection

1.10 Tools

Bookmark these official sites and download pages. You used them to install the runtime, create the Next.js application, inspect pages in the browser, commit the project, and put it on the public Web.

  • Node.js — The JavaScript runtime you install so Next.js, npm, and later the Express server can run on your machine.
  • npm — The package manager that ships with Node.js and installs the libraries this course's projects depend on.
  • Visual Studio Code — A free code editor with a large extension ecosystem; a solid default if you are not using Cursor.
  • Cursor — An AI-native editor, based on VS Code, that can read this project and help write or refactor TypeScript.
  • Google Chrome — The browser this course assumes for DevTools, the React extension, and checking your deployed site.
  • Chrome DevTools — Chrome's built-in inspector for HTML, CSS, the console, and the Network panel.
  • Git — The version-control tool you use locally to commit before you push the project to GitHub.
  • GitHub — The host for your remote Git repository and the place Vercel and Render connect when you deploy.
  • Next.js — The React framework this course uses for pages, layouts, and later the HTTP server routes.
  • React — The UI library that turns components and JSX into the screens you build in the labs.
  • Vercel — The host for the Next.js client; connect the GitHub repo here to put the UI on the public Web.

1.11 AI Tools

While the screens are still plain HTML, these AI and design tools are useful for exploring layouts, icons, and prompts before you write every tag by hand. Section 1.7 walks through that habit on a feature branch.

  • v0 — Generates interface mockups and React starting points from a written prompt.
  • Stitch — Google's design tool for turning a product idea into screen layouts you can iterate on.
  • Rocket.new — Builds a working web app from a short description so you can explore structure before coding by hand.
  • Lucide — A consistent open-source icon set you can search and drop into React screens.
  • Galileo AI — Turns a text description into high-fidelity UI designs for early product exploration.
  • Google Prompt Gallery — A public collection of Gemini prompt examples you can remix for writing, coding, and multimodal tasks.
  • shadcn/ui — Copy-and-own React components styled with Tailwind for composing a polished interface.
  • Figma — Collaborative design software for wireframes, mockups, and handing layouts to developers.