ECE1724: Advanced Web Development

React Ecosystem and Modern Frameworks

Chen Ying

Assistant Professor, Teaching Stream

Department of Electrical and Computer Engineering

University of Toronto

Lecture 6: Review

  • React Router: <RouterProvider />, createBrowserRouter()

    • Navigation: <Link />, useNavigate()
    • Dynamic Routing: useParams()
  • Data Fetching: fetch() + useEffect()

  • Form Handling

    • Form Submission <form onSubmit={handleSubmit}>
      • preventDefault(): Prevent page reload
    • Form Validation

Lecture 6: Review

  • Multi-Select Dropdown: <select multiple>, <option>

  • Basic Styling

    • Global CSS: Apply everywhere
    • CSS Modules: Component specific

Revisit CSS

Cascading Style Sheets: Describes how HTML elements look

h1 {
  color: blue;
  font-size: 3.2em;
}
  • Selector: Target elements
  • Declaration: What to change
selector {
  property: value;
}

CSS Selector

Selector Example Example Description
element h1 Selects all <h1> elements
.className .title Selects all elements with class="title"
#idName #firstname Selects the element with id="firstname"
* * Selects all elements

In React

<h1 className="title">Hello</h1>

Cascading

Why is it called Cascading Style Sheets?

Because styles can:

  • Override each other
  • Inherit from parent elements
  • Depend on order
h1 { font-size: 3.2em; }
.title { color: green; }
h1 {
  font-size: 3.2em;
  color: blue;
}
.title { color: green; }

CSS Box Model

Every HTML element is a rectangle

<h1>Hello</h1>
  • <h1> is actually a rectangle on the screen

Browsers render everything as boxes

Visualize Box

Each element consists of content (text/image), padding, border, margin

Make Box Visible

h1 {
  font-size: 3.2em;
  color: blue;
  background-color: lightyellow;
  border: 3px solid black;
}
  • Border shows the boundary

Padding

Space appears between content and border

h1 {
  font-size: 3.2em;
  color: blue;
  background-color: lightyellow;
  border: 3px solid black;
  padding: 20px;
}
  • The box becomes larger
  • Background color expands

Margin

Space appears outside the border

h1 {
  font-size: 3.2em;
  color: blue;
  background-color: lightyellow;
  border: 3px solid black;
  padding: 20px;
  margin: 80px;
}
  • Distance between elements increases
  • Background does not expand

Padding vs. Margin

Padding: Space appears between content and border

Margin: Space appears outside the border

global.css
h1 {
  font-size: 3.2em;
  color: blue;
  background-color: lightyellow;
  border: 3px solid black;
}
.box1 {
  padding: 20px;
}
.box2 {
  margin: 80px;
}

Padding vs. Margin

App.tsx
function App() {
  return (
    <div>
      <h1 className="box1">Padding</h1>
      <h1 className="box2">Margin</h1>
    </div>
  );
}
export default App;
  • Padding affects the element itself
  • Margin affects spacing between elements

Layout

How do we place boxes horizontally?

How do we center them?

How do we control spacing?

Flexbox

Most common layout system in modern apps

  • Arranges components in a row or a column
  • Distributes space between them
  • Aligns items along axes

Enable Flexbox

.container {
  display: flex;
}
App.tsx
function App() {
  return (
    <div className="container">
      <h1>Box 1</h1>
      <h1>Box 2</h1>
    </div>
  );
}

export default App;
  • The container becomes a flex container
  • Its children become flex items

Default Behavior

.container {
  display: flex;
}
  • Items are arranged in a row
  • Left to right

Axes

Flexbox always has two axes:

  • Main axis
  • Cross axis

Main Axis vs. Cross Axis

flex-direction: row: Main axis is horizontal

flex-direction: column: Main axis is vertical

Change Direction

Stack items vertically

.container {
  display: flex;
  flex-direction: column;
}

Control Alignment

justify-content controls spacing along main axis

.container {
  display: flex;
  justify-content: center;
}

align-items controls alignment along cross axis

.container {
  display: flex;
  align-items: center;
}

Traditional CSS

Write custom stylesheets

.container {
  display: flex;
  justify-content: center;
  align-items: center;
  padding: 16px;
  background: lightblue;
  border-radius: 8px;
}
  • You define semantic class names
  • Styles live in separate .css files
  • Cascade and specificity matter

Challenges at Scale

Traditional CSS works well

But in large applications:

  • Naming classes becomes difficult
  • Styles can accidentally override each other
  • Refactoring requires searching across files
  • Design consistency depends on discipline

This is a scaling challenge, not a flaw in CSS

Utility-First Approach

Instead of inventing class names, use small single-purpose utility classes

Examples

  • flex
  • justify-center
  • items-center
  • bg-blue-50

This is the philosophy behind Tailwind CSS

Tailwind CSS

A utility-first CSS framework

Instead of writing

.container {
  display: flex;
  justify-content: center;
  align-items: center;
  padding: 16px;
  background: lightblue;
  border-radius: 8px;
}

You write directly in JSX/TSX

<div className="flex justify-center items-center p-4 bg-blue-50 rounded-4xl">
  • No custom class name needed

Tailwind CSS

Tailwind does NOT remove CSS

It generates CSS classes like:

.flex { display: flex; }
.justify-center { justify-content: center; }
.items-center { align-items: center; }
.p-4 { padding: 1rem; }

You are still using CSS, just not writing custom class names

Get Started with Vite

Set up and use Tailwind CSS in Vite-based React projects

Doc: tailwindcss.com/docs/installation/using-vite

  1. Install Tailwind CSS

    npm install tailwindcss @tailwindcss/vite

2. Configure Vite Plugin

Add @tailwindcss/vite plugin to Vite configuration

vite.config.ts
import tailwindcss from "@tailwindcss/vite";
import react from "@vitejs/plugin-react";
import { defineConfig } from "vite";

export default defineConfig({
  plugins: [react(), tailwindcss()],
});

3. Import Tailwind CSS

Add @import "tailwindcss"; to CSS files that import Tailwind CSS

global.css
@import "tailwindcss";

Utility Classes

Combine single-purpose utility classes

App.tsx
function App() {
  return (
    <div className="flex justify-center items-center p-4 bg-blue-50 rounded-4xl">
      <h1 className="text-8xl font-bold text-blue-600 bg-yellow-50 border-8 border-black p-4 m-8">
        Box 1
      </h1>
      <h1 className="text-8xl font-bold text-blue-600 bg-yellow-50 border-8 border-black p-4 m-8">
        Box 2
      </h1>
    </div>
  );
}

export default App;

Utility Classes

<div className="flex justify-center items-center p-4 bg-blue-50 rounded-4xl">
  <h1 className="text-8xl font-bold text-blue-600 bg-yellow-50 border-8 border-black p-4 m-8">
    Box 1
  </h1>
</div>
  • Layout: flex, justify-center, items-center
  • Spacing: p-4, m-8
  • Typography: text-8xl, font-bold
  • Color: bg-blue-50, text-blue-600, bg-yellow-50
  • Border: border-8, border-black

We compose styles instead of inventing class names

Tailwind: Key Features

Utility Classes (p-4, text-8xl, bg-blue-50)

Responsive Design

Responsive Design

Adjust layouts, text, and spacing based on screen size

Every utility class in Tailwind can be conditionally applied at different breakpoints (specific screen widths)

Default Breakpoints

Tailwind supports five default breakpoints

Prefix Min Width Example Device
sm: 40rem (640px) Small tablets
md: 48rem (768px) Tablets
lg: 64rem (1024px) Laptops
xl: 80rem (1280px) Large screens
2xl: 96rem (1536px) Extra large screens

Add Responsiveness

<h1 className="text-4xl md:text-6xl lg:text-8xl font-bold text-blue-600 bg-yellow-50 border-8 border-black p-4 m-8">
  Box 1
</h1>
  • text-4xl: Default text size on all screens
  • md:text-6xl: Changes to text-6xl on screens 768px and larger
  • lg:text-8xl: Changes to text-8xl on screens 1024px and larger

Live Demo

App.tsx
function App() {
  return (
    <div className="flex justify-center items-center p-4 bg-blue-50 rounded-4xl">
      <h1 className="text-4xl md:text-6xl lg:text-8xl font-bold text-blue-600 bg-yellow-50 border-8 border-black p-4 m-8">
        Box 1
      </h1>
      <h1 className="text-4xl md:text-6xl lg:text-8xl font-bold text-blue-600 bg-yellow-50 border-8 border-black p-4 m-8">
        Box 2
      </h1>
    </div>
  );
}

export default App;

Long className Strings

Utility classes can become long

App.tsx
function App() {
  const titleClass =
    "text-4xl md:text-6xl lg:text-8xl font-bold text-blue-600 bg-yellow-50 border-8 border-black p-4 m-8";

  return (
    <div className="flex justify-center items-center p-4 bg-blue-50 rounded-4xl">
      <h1 className={titleClass}>Box 1</h1>
      <h1 className={titleClass}>Box 2</h1>
    </div>
  );
}

export default App;

Extract to Component

components/Title.tsx
function Title({ children }: { children: React.ReactNode }) {
  return (
    <h1 className="text-8xl font-bold text-blue-600 bg-yellow-50 border-8 border-black p-4 m-8">
      {children}
    </h1>
  );
}

export default Title;

Extract to Component

App.tsx
import Title from "./components/Title";

function App() {
  return (
    <div className="flex justify-center items-center p-4 bg-blue-50 rounded-4xl">
      <Title>Box 1</Title>
      <Title>Box 2</Title>
    </div>
  );
}

export default App;

Tailwind: Key Features

Utility Classes (p-4, text-8xl, bg-blue-50)

Responsive Design (sm:p-8, lg:text-8xl)

Dynamic Styling

Dynamic Styling

Utility classes work naturally with props

components/Title.tsx
type TitleProps = {
  variant?: "primary" | "secondary";
  children: React.ReactNode;
};

function Title({ variant = "primary", children }: TitleProps) {
  const variantStyles = {
    primary: "bg-yellow-50 border-yellow-500",
    secondary: "bg-green-50 border-green-800",
  };

  return (
    <h1
      className={`text-4xl md:text-6xl lg:text-8xl font-bold text-blue-600 border-8 p-4 m-8 ${
        variantStyles[variant]
      }`}
    >
      {children}
    </h1>
  );
}

export default Title;

Dynamic Styling

Use in App.jsx

App.jsx
import Title from "./components/Title";

function App() {
  return (
    <div className="flex justify-center items-center p-4 bg-blue-50 rounded-4xl">
      <Title>Box 1</Title>
      <Title variant="secondary">Box 2</Title>
    </div>
  );
}

export default App;

Tailwind: Key Features

Utility Classes (p-4, text-8xl, bg-blue-50)

Responsive Design (sm:p-8, lg:text-8xl)

Dynamic Styling

Theme Customization

Theme Customization

Instead of hardcoding colors everywhere

Define design tokens once

global.css
@import "tailwindcss";

@theme {
  --color-primary: #fefce8;
  --color-secondary: #f0fdf4;
}

Now you can use

bg-primary
text-primary
bg-secondary
text-secondary

Theme Variables

global.css
@theme {
  --color-primary: #fefce8;
}

Theme variables are defined in namespaces

Each namespace corresponds to one or more utility class

Recap: Tailwind CSS

A utility-first CSS framework with pre-built single-purpose classes for rapid UI development

  • Utility Classes (p-4, text-8xl, bg-blue-50)
  • Responsive Design (sm:p-8, lg:text-8xl)
  • Dynamic Styling
  • Customization with Theme Variables (@theme)

Tailwind → Components

Tailwind enables composing styles with utility classes

But real apps repeat the same UI patterns

  • The same layout patterns
  • The same typography
  • The same long className strings

So instead of repeating utilities everywhere, we extract them into reusable components

Extract to Component

components/Title.tsx
type TitleProps = {
  variant?: "primary" | "secondary";
  children: React.ReactNode;
};

function Title({ variant = "primary", children }: TitleProps) {
  const variantStyles = {
    primary: "bg-yellow-50 border-yellow-500",
    secondary: "bg-green-50 border-green-800",
  };

  return (
    <h1
      className={`text-4xl md:text-6xl lg:text-8xl font-bold text-blue-600 border-8 p-4 m-8 ${
        variantStyles[variant]
      }`}
    >
      {children}
    </h1>
  );
}

export default Title;

Extract to Component

We kept Tailwind’s benefits:

  • Utility classes
  • Responsive variants (md:, lg:)
  • Easy customization

And we gained:

  • Reuse across the app
  • A single place to update styles
  • Design decisions encoded as props

shadcn/ui

A collection of accessible, Tailwind-styled components

  • Not a traditional black-box library
    • Copy-paste components into your code for full control
  • Beautiful defaults
  • Fully customizable

Benefits:

  • Fast UI prototyping with Tailwind’s power
  • Tweakable to match your design

Tailwind vs. shadcn/ui

Tailwind: Utilities to build anything

shadcn/ui: Well-designed, reusable components built from those utilities


Think of it as: Utilities → Extract patterns → Standardized components

shadcn/ui: Setup

ui.shadcn.com/docs/installation

  • Supports many frameworks: Vite, Next.js, …

Install and configure shadcn/ui for Vite

  1. Add Tailwind CSS

    npm install tailwindcss @tailwindcss/vite
  2. Add @import "tailwindcss";

    global.css
    @import "tailwindcss";

shadcn/ui: Setup

  1. Edit tsconfig.json
tsconfig.json
{
  "files": [],
  "references": [
    { "path": "./tsconfig.app.json" },
    { "path": "./tsconfig.node.json" }
  ],
  "compilerOptions": {
    "baseUrl": ".",
    "paths": {
      "@/*": ["./src/*"]
    }
  }
}

shadcn/ui: Setup

  1. Edit tsconfig.app.json
tsconfig.app.json
{
  "compilerOptions": {
    "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
    "target": "ES2022",
    "useDefineForClassFields": true,
    "lib": ["ES2022", "DOM", "DOM.Iterable"],
    "module": "ESNext",
    "types": ["vite/client"],
    "skipLibCheck": true,

    /* Bundler mode */
    "moduleResolution": "bundler",
    "allowImportingTsExtensions": true,
    "verbatimModuleSyntax": true,
    "moduleDetection": "force",
    "noEmit": true,
    "jsx": "react-jsx",

    /* Linting */
    "strict": true,
    "noUnusedLocals": true,
    "noUnusedParameters": true,
    "erasableSyntaxOnly": true,
    "noFallthroughCasesInSwitch": true,
    "noUncheckedSideEffectImports": true,

    "baseUrl": ".",
    "paths": {
      "@/*": ["./src/*"]
    }
  },
  "include": ["src"]
}

shadcn/ui: Setup

  1. Update vite.config.ts
vite.config.ts
import path from "node:path";
import tailwindcss from "@tailwindcss/vite";
import react from "@vitejs/plugin-react";
import { defineConfig } from "vite";

export default defineConfig({
  plugins: [react(), tailwindcss()],
  resolve: {
    alias: {
      "@": path.resolve(__dirname, "./src"),
    },
  },
});

shadcn/ui: Setup

  1. Initialize shadcn/ui

    npx shadcn@latest init
    • Configure components.json
    • Update src/global.css
    • Add utility helpers src/lib/utils.ts
  2. Add a component

    npx shadcn@latest add button
    • Create src/components/ui/button.tsx
      • You can edit it freely

Use Button

src/App.jsx
import { Button } from "@/components/ui/button";

function App() {
  return (
    <div className="flex justify-center items-center p-4 bg-blue-50 rounded-4xl">
      <Button>Click me</Button>
    </div>
  );
}

export default App;

Use Variants

Variants are just predefined Tailwind class combinations

src/App.jsx
import { Button } from "@/components/ui/button";

function App() {
  return (
    <div className="flex flex-col justify-center items-center p-4 space-y-4 bg-blue-50 rounded-4xl">
      <Button variant="default">Default Button</Button>
      <Button variant="destructive">Destructive Button</Button>
      <Button variant="ghost">Ghost Button</Button>
    </div>
  );
}

export default App;

Customize Button

Customize the component’s appearance with Tailwind CSS

src/App.jsx
import { Button } from "@/components/ui/button";

function App() {
  return (
    <div className="flex flex-col justify-center items-center p-4 space-y-4 bg-blue-50 rounded-4xl">
      <Button variant="default">Default Button</Button>
      <Button className="bg-blue-500 hover:bg-blue-600">Blue Button</Button>
    </div>
  );
}

export default App;

Or modify src/components/ui/button.tsx directly

More Components

Bring It Together

Tailwind provides the styling foundation

shadcn/ui provides reusable, accessible components built on top

Together they form a modern frontend styling architecture

Recap: Modern Styling Solutions

  • Tailwind CSS: A utility-first CSS framework with predefined classes for rapid UI development

  • shadcn/ui: A set of customizable, copy-and-paste components styled with Tailwind CSS

You will use both in Assignment 4

Assignment 4

Full-Stack Next.js application with model styling

  • Modern UI using Tailwind CSS and shadcn/ui
  • Migration from React (Vite) to Next.js

Assignment 4

The schedule has been adjusted to ensure that all required Next.js concepts are covered in lecture before the handout is release

  • Handout Release Date: March 1 March 6
  • Due Date: March 15 March 22

Next.js

A full-stack React framework

  • Built on top of React
  • Largely simplifies the process of building full-stack applications with React
    • Provides routing, rendering, and backend capabilities out of the box
    • Reduces the need for extra libraries (e.g., React Router)

Key Features & Benefits

Next.js adds important capabilities on top of React

  • Full-Stack Applications: Frontend and backend live in the same project
  • Server-Side Rendering (SSR): By default, Next.js (pre-)renders all pages on the server
  • File-Based Routing: Routes are configured via the file system
    • No code-based configuration or extra packages for routing required

Next.js: Project Setup

npx create-next-app@latest my-app --yes
  • --yes skips prompts using saved preferences or defaults

The default setup enables

  • TypeScript
  • Tailwind CSS
  • App Router
  • Import alias @/*

Next.js: Project Setup

npx create-next-app@latest my-app --yes

Start the development server:

cd my-app
npm run dev

app/page.tsx

Defines homepage (/)

app/page.tsx
export default function Home() {
  const message = "My First Next.js App";

  return (
    <div className="flex justify-center m-4">
      <h1 className="text-8xl font-bold">{message}</h1>
    </div>
  );
}

This looks like React, but it runs on the server by default

React SPA

In a typical React Single Page Application

  1. Browser requests /

  2. Server returns a mostly empty HTML file:

    <div id="root"></div>
    <script type="module" src="/src/main.tsx"></script>
  3. Browser downloads a large JavaScript bundle

  4. React runs in the browser

  5. React generates the UI

  6. All future navigation happens client-side

React SPA

Rendering happens in the browser

This is called Client-Side Rendering (CSR)

  • Initial HTML is minimal
  • UI appears after JavaScript loads
  • Data fetching typically uses useEffect

Next.js (Default Behavior)

  1. Browser requests /
  2. Server executes your React component
  3. Server generates HTML
  4. Browser receives already-rendered HTML

This is called Server-Side Rendering (SSR)

Server-Side Rendering

By default, Next.js renders all pages on the server

  • The component function executes on the server
  • It generates HTML there
  • Browser receives pre-rendered HTML
  • Client does NOT execute that component’s code, unless specified

Server Components

By default, components are Server Components

  • Render on the server

  • Can use async/await directly

  • Can fetch data without useEffect

    export default async function Page() {
      const data = await fetch("https://api.example.com/posts").then((r) =>
        r.json(),
      );
      return <div>{data.length} posts</div>;
    }
  • Cannot use React hooks like useState or useEffect

  • Cannot access browser-only APIs (e.g., window)

Client Components

A component must be a Client Components if it needs

  • React hooks (e.g., useState, useEffect)
  • Event handlers (e.g., onClick)
  • Browser APIs (e.g., window, document)

To make a component client-side, add

"use client";

at the top of the file

Client Component Example

components/Counter.tsx
"use client";

import { useState } from "react";

export default function Counter() {
  const [count, setCount] = useState(0);

  return (
    <button type="button" onClick={() => setCount(count + 1)}>
      Count: {count}
    </button>
  );
}

This component runs in the browser

Live Demo

app/page.tsx
import Counter from "@/components/Counter";

export default function Home() {
  const message = "My First Next.js App";

  return (
    <div className="flex flex-col justify-center items-center space-y-16 m-4">
      <h1 className="text-8xl font-bold">{message}</h1>
      <Counter />
    </div>
  );
}

SSR and CSR in Next.js

Server-Side Rendering for speed

  • Home stays server-side, rendering fast

Client-Side Rendering for action

  • Counter loads on the client for interactivity

Use shadcn/ui

ui.shadcn.com/docs/installation/next

npx shadcn@latest init
  • Create components.json
  • Update app/global.css
  • Add utility helpers lib/utils.ts

Add a component

npx shadcn@latest add button
  • Create components/ui/button.tsx

Live Demo

app/page.tsx
import { Button } from "@/components/ui/button";

export default function Home() {
  const message = "My First Next.js App";

  return (
    <div className="flex flex-col justify-center items-center space-y-16 m-4">
      <h1 className="text-8xl font-bold">{message}</h1>
      <Button>Click me</Button>
    </div>
  );
}

File-Based Routing

Next.js uses files to define routes

  • Pages Router: Uses pages/ directory, where each .tsx or .js file inside represents a route
    • pages/about.js/about
  • App Router: Uses app/ directory, where each folder inside represents a route

App Router

Each folder in the app/ directory represents a route

Each folder contains a page.tsx, which defines a page (route endpoint)

When users visit a route, Next.js renders the corresponding page.tsx file

  • /: app/page.tsx
  • /about: app/about/page.tsx

Live Demo

app/about/page.tsx
export default function AboutPage() {
  const message = "This is the page of /about";

  return (
    <div className="flex justify-center m-4">
      <h1 className="text-8xl font-bold">{message}</h1>
    </div>
  );
}

Static Route: Fixed path

Routes in App Router

Static Routes: Fixed paths

  • E.g. app/about/page.tsx creates the /about route

Dynamic Routes: Flexible paths, changed based on URL

Review: Dynamic Routes w/ React Router

src/main.tsx
import Profile from "./components/Profile";

const router = createBrowserRouter([
  { path: "/profile/:username", element: <Profile /> },
]);

Use useParams

src/components/Profile.tsx
import { useParams } from "react-router";

function Profile() {
  const { username } = useParams();
  return <h1>Profile of {username}</h1>;
}

export default Profile;

Dynamic Routes in Next.js

Use square brackets ([]) to define dynamic segments

Example: app/posts/[id]/page.tsx creates /posts/1, /posts/2, etc.

Dynamic Route Example

app/posts/[id]/page.tsx
type PostParams = {
  params: { id: string };
};

export default function PostPage({ params }: PostParams) {
  const postId = parseInt(params.id, 10);
  return <h1>Post #{postId}</h1>;
}

params.id: Contains the dynamic segment from the URL

  • Visiting /posts/1params.id = "1"

An Error

Error: Route "/posts/[id]" used `params.id`. `params` is a Promise and must be unwrapped with `await` or `React.use()` before accessing its properties. Learn more: https://nextjs.org/docs/messages/sync-dynamic-apis
    at PostPage (app/posts/[id]/page.tsx:6:34)
  4 |
  5 | function PostPage({ params }: PostParams) {
> 6 |   const postId = parseInt(params.id, 10);
    |                                  ^
  7 |   return <h1>Post #{postId}</h1>;
  8 | }
  9 |

A Shift

Earlier assumptions: params was always synchronously available

In App Router, dynamic route parameters in Server Components are treated as potentially asynchronous

  • Server Components can be async
  • Data fetching happens on server, no useEffect required

params is a Promise in dynamic routes

Corrected Version

app/posts/[id]/page.tsx
type PostParams = {
  params: Promise<{ id: string }>; // params is a Promise
};

export default async function PostPage({ params }: PostParams) {
  const resolvedParams = await params; // Await params to resolve the Promise
  const postId = parseInt(resolvedParams.id, 10);
  return (
    <main className="flex items-center justify-center">
      <h1 className="text-6xl font-bold">Post #{postId}</h1>
    </main>
  );
}

Alternative

app/posts/[id]/page.tsx
type PostParams = {
  params: Promise<{ id: string }>; // params is a Promise
};

export default async function PostPage({ params }: PostParams) {
  const { id } = await params; // Destructure after awaiting
  const postId = parseInt(id, 10);
  return (
    <main className="flex items-center justify-center">
      <h1 className="text-6xl font-bold">Post #{postId}</h1>
    </main>
  );
}

In dynamic route contexts, params is a Promise

Always await params in Server Components

Error Handling with notFound()

Next.js automatically renders built-in 404 page

app/posts/[id]/page.tsx
import { notFound } from "next/navigation";

type PostParams = {
  params: Promise<{ id: string }>;
};

export default async function PostPage({ params }: PostParams) {
  const { id } = await params;
  const postId = Number.parseInt(id, 10);

  if (Number.isNaN(postId)) {
    return notFound();
  }

  return (
    <main className="flex items-center justify-center">
      <h1 className="text-6xl font-bold">Post #{postId}</h1>
    </main>
  );
}

Recap: App Router

  • Static Routes: Fixed paths
    • app/about/page.tsx creates the /about route
  • Dynamic Routes: Flexible paths, changed based on URL
    • app/posts/[id]/page.tsx creates /posts/1, /posts/2, …

Lecture Summary

CSS Foundations

  • Box Model (content, padding, border, margin)
  • Flexbox (layout via parent control)

Tailwind CSS: A utility-first CSS framework

  • Utility Classes (p-4, text-8xl, bg-blue-50)
  • Responsive Design (sm:p-8, lg:text-8xl)
  • Dynamic Styling
  • Customization with Theme Variables (@theme)

Lecture Summary

Modern Styling Solutions

  • Tailwind CSS: A utility-first CSS framework with predefined classes for rapid UI development

  • shadcn/ui: A modern component collection

    • Built on Tailwind
    • Code lives in your repo
    • Fully customizable

Lecture Summary

Next.js A full-stack React framework

  • Server Components (default): Render on the server
    • Can use async/await
    • Cannot use React hooks
  • Client Components ("use client";): Run in browser
    • Enable interactivity
  • App Router: File-based routing
    • Static: Fixed paths (e.g., /about)
    • Dynamic: Variable paths (e.g., /posts/[id])