# Les 6: Next.js — QuickPoll Compleet - Slide Overzicht

> Versie 3 — **Van scratch tot werkende app in één les.** Geen apart theorieblok.
>
> Stap 0-3 = recap (sneller, studenten hebben dit al gezien in Les 5).
> Stap 4-8 = nieuw materiaal.
> Theorie wordt uitgelegd **op het moment dat we het tegenkomen.**

---

## Slide 1: Titel + Plan

### Op de Slide
- Titel: **Les 6: QuickPoll — Van Scratch tot Werkend**
- Subtitel: "De hele app, stap voor stap"
- **Les 6 van 18**
- Next.js logo

- **Plan vandaag:**
  - Stap 0-3: Project opzetten, types, layout, homepage, GET route *(recap)*
  - Stap 4: POST vote route *(nieuw)*
  - Stap 5: Poll detail pagina *(nieuw)*
  - Stap 6: VoteForm component *(nieuw)*
  - Stap 7: Loading, Error & Not-Found *(nieuw)*
  - Stap 8: Middleware *(nieuw)*

### Docentnotities
"Welkom terug! Vorige les hebben jullie kennis gemaakt met Next.js. Vandaag bouwen we de hele QuickPoll app van scratch tot werkend. Ik code, jullie volgen mee."

"Stap 0-3 kennen jullie al — dat gaat lekker snel. Stap 4-8 is nieuw, daar nemen we de tijd voor. Laten we beginnen."

---

## Slide 2: Stap 0 — Project Aanmaken

### Op de Slide
- **Terminal:**
```bash
npx create-next-app@latest quickpoll
```
- **Opties:**
  - TypeScript? → **Yes**
  - ESLint? → **Yes**
  - Tailwind CSS? → **Yes**
  - `src/` directory? → **Yes**
  - App Router? → **Yes**
  - Import alias? → **@/***

```bash
cd quickpoll
npm run dev
```

- **Check:** `localhost:3000` → Next.js standaard pagina

### Docentnotities
*Tim opent terminal, typt het commando.*

"Stap 0 kennen jullie. `create-next-app` met TypeScript, Tailwind en App Router. Selecteer dezelfde opties als op het scherm."

*Tim wacht tot iedereen `npm run dev` draait.*

"Zie je de Next.js pagina? Dan gaan we door."

---

## Slide 3: Stap 1 — Types & Data

### Op de Slide
- **Maak `src/types/index.ts`:**

```tsx
export interface Poll {
  id: string;
  question: string;
  options: string[];
  votes: number[];
}
```

- **Maak `src/lib/data.ts`:**

```tsx
import type { Poll } from "@/types";

let polls: Poll[] = [
  {
    id: "1",
    question: "Wat is je favoriete programmeer taal?",
    options: ["JavaScript", "Python", "TypeScript", "Rust"],
    votes: [45, 32, 28, 15],
  },
  {
    id: "2",
    question: "Hoe veel uur slaap krijg je per nacht?",
    options: ["< 6 uur", "6-8 uur", "8+ uur"],
    votes: [12, 68, 35],
  },
  {
    id: "3",
    question: "Welke framework gebruik je het meest?",
    options: ["React", "Vue", "Svelte", "Angular"],
    votes: [89, 34, 12, 8],
  },
];

let nextId = 4;

export function getPolls(): Poll[] {
  return polls;
}

export function getPollById(id: string): Poll | undefined {
  return polls.find((poll) => poll.id === id);
}

export function votePoll(pollId: string, optionIndex: number): Poll | undefined {
  const poll = polls.find((p) => p.id === pollId);
  if (!poll || optionIndex < 0 || optionIndex >= poll.options.length) return undefined;
  poll.votes[optionIndex]++;
  return poll;
}
```

### Docentnotities
**💡 Theorie-moment: TypeScript interfaces in Next.js**

"Stap 1: de basis. Een `Poll` interface en in-memory data. Herkennen jullie dit van Les 4?"

*Tim typt de interface.*

"Een poll heeft een id, vraag, opties, en stemmen. Die `votes` array loopt parallel met `options` — index 0 van votes hoort bij index 0 van options."

*Tim typt data.ts.*

"Dit is onze 'database' — gewoon een array in het geheugen. `getPolls`, `getPollById`, `votePoll`. Straks bij de Supabase lessen vervangen we dit door een echte database."

*Checkpoint: "Heeft iedereen beide bestanden?"*

---

## Slide 4: Stap 2 — Layout & Homepage

### Op de Slide
- **Vervang `src/app/layout.tsx`:**

```tsx
import type { Metadata } from "next";
import Link from "next/link";
import "./globals.css";

export const metadata: Metadata = {
  title: "QuickPoll",
  description: "Een snelle polling app met Next.js",
};

export default function RootLayout({
  children,
}: Readonly<{ children: React.ReactNode }>) {
  return (
    <html lang="nl">
      <body className="bg-gray-50 min-h-screen flex flex-col">
        <nav className="bg-white shadow-sm border-b border-gray-200">
          <div className="container mx-auto px-4 py-4 flex items-center gap-8">
            <Link href="/"
              className="text-2xl font-bold text-purple-600 hover:text-purple-700 transition-colors">
              QuickPoll
            </Link>
            <Link href="/"
              className="text-gray-700 hover:text-purple-600 transition-colors font-medium">
              Home
            </Link>
          </div>
        </nav>
        <main className="flex-1">{children}</main>
        <footer className="bg-white border-t border-gray-200 mt-12">
          <div className="container mx-auto px-4 py-6 text-center text-gray-600 text-sm">
            © 2026 QuickPoll — Built with Next.js 15
          </div>
        </footer>
      </body>
    </html>
  );
}
```

- **`Link`** — client-side navigatie (geen page reload)
- **`metadata`** — SEO titel & beschrijving
- **`{children}`** — hier komt de pagina-inhoud

### Docentnotities
**💡 Theorie-moment: Layout & Link**

"Stap 2: de layout. Dit bestand wrapt ELKE pagina in je app. De navbar en footer staan hier — die veranderen nooit."

"`Link` is de Next.js versie van `<a>`. Het verschil: Link navigeert client-side — geen page reload, veel sneller."

"`metadata` is voor SEO. Kijk in je browser tab: 'QuickPoll'."

*Tim wacht even, dan door naar de homepage.*

---

## Slide 5: Stap 2 vervolg — Homepage

### Op de Slide
- **Vervang `src/app/page.tsx`:**

```tsx
import Link from "next/link";
import { getPolls } from "@/lib/data";

export default function Home() {
  const polls = getPolls();

  return (
    <div className="container mx-auto py-12 px-4">
      <div className="mb-12">
        <h1 className="text-4xl font-bold text-gray-900">QuickPoll</h1>
        <p className="text-gray-600 mt-2">Kies een poll en stem af in een oogwenk</p>
      </div>

      <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
        {polls.map((poll) => {
          const totalVotes = poll.votes.reduce((sum, v) => sum + v, 0);
          return (
            <Link key={poll.id} href={`/poll/${poll.id}`}>
              <div className="bg-white rounded-lg border border-gray-200 p-6
                hover:shadow-lg transition-shadow cursor-pointer h-full">
                <h2 className="text-lg font-semibold text-gray-900 mb-4">
                  {poll.question}
                </h2>
                <div className="flex items-center justify-between text-sm text-gray-600">
                  <span>{poll.options.length} opties</span>
                  <span>{totalVotes} stemmen</span>
                </div>
                <div className="mt-4 text-purple-600 font-semibold text-sm">
                  Stemmen →
                </div>
              </div>
            </Link>
          );
        })}
      </div>
    </div>
  );
}
```

- **Dit is een Server Component** — geen `"use client"`, data direct ophalen
- **`.map()`** — render een card per poll
- **`.reduce()`** — tel totaal stemmen
- **Check:** `localhost:3000` → 3 poll cards

### Docentnotities
**💡 Theorie-moment: Server Components**

"Dit is een Server Component. Geen `'use client'` bovenaan — dus deze code draait op de server. Je kunt direct data ophalen, geen useEffect, geen loading state nodig."

"De homepage haalt alle polls op en rendert een card per poll. De `.map()` loop kennen jullie uit JavaScript — zelfde verhaal, maar dan in JSX."

*Checkpoint: "Zie je 3 cards op localhost:3000? Mooi."*

---

## Slide 6: Stap 3 — GET API Route

### Op de Slide
- **Maak `src/app/api/polls/[id]/route.ts`:**

```tsx
import { NextResponse } from "next/server";
import { getPollById } from "@/lib/data";

export async function GET(
  request: Request,
  { params }: { params: Promise<{ id: string }> }
) {
  const { id } = await params;
  const poll = getPollById(id);

  if (!poll) {
    return NextResponse.json({ error: "Poll not found" }, { status: 404 });
  }

  return NextResponse.json(poll);
}
```

- **Folder = URL:** `api/polls/[id]/route.ts` → `/api/polls/1`
- **`[id]`** — dynamic route parameter
- **`await params`** — Next.js 15 pattern (params is een Promise)
- **`NextResponse.json()`** — stuur JSON response
- **Test:** `localhost:3000/api/polls/1` → JSON in browser

### Docentnotities
**💡 Theorie-moment: API Routes & Dynamic Routes**

"Stap 3: onze eerste API route. In Next.js is de folder-structuur je URL. `api/polls/[id]/route.ts` wordt `/api/polls/1`."

"Die `[id]` met vierkante haakjes is een dynamic route. Het getal in de URL wordt de `id` parameter."

"Let op: `await params` — in Next.js 15 zijn params een Promise. Vergeet de `await` niet, anders werkt het niet."

*Tim opent browser, gaat naar localhost:3000/api/polls/1.*

"JSON! Onze API werkt. Probeer `/api/polls/999` — daar krijg je een 404 error."

*Checkpoint: "Werkt je API? Dan zijn we klaar met de recap. Nu het nieuwe werk."*

---

## Slide 7: Stap 4 — POST /api/polls/[id]/vote

### Op de Slide
- **Maak `src/app/api/polls/[id]/vote/route.ts`:**

```tsx
import { NextResponse } from "next/server";
import { votePoll } from "@/lib/data";

interface RouteParams {
  params: Promise<{ id: string }>;
}

interface VoteBody {
  optionIndex: number;
}

export async function POST(
  request: Request,
  { params }: RouteParams
): Promise<NextResponse> {
  const { id } = await params;
  const body: VoteBody = await request.json();

  if (typeof body.optionIndex !== "number") {
    return NextResponse.json(
      { error: "optionIndex is verplicht" },
      { status: 400 }
    );
  }

  const updatedPoll = votePoll(id, body.optionIndex);

  if (!updatedPoll) {
    return NextResponse.json(
      { error: "Poll niet gevonden of ongeldige optie" },
      { status: 404 }
    );
  }

  return NextResponse.json(updatedPoll);
}
```

- **Test met browser console:**
```javascript
fetch('/api/polls/1/vote', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({ optionIndex: 0 })
}).then(r => r.json()).then(console.log)
```

### Docentnotities
**💡 Theorie-moment: POST vs GET**

"Nu begint het nieuwe werk. GET = data ophalen, POST = data wijzigen. Vijf stappen in elke POST route: params, body, validatie, actie, response."

*Tim typt langzaam, stopt na elke sectie.*

"`request.json()` leest de body — wat de client meestuurt. Hier de `optionIndex`: welke optie gestemd is."

"Twee error checks: 400 als de data ongeldig is, 404 als de poll niet bestaat. Altijd beide afvangen."

*Tim opent console, test de fetch.*

"Plak dit in je console. Zie je? Votes veranderd. Onze stem-API werkt."

*Checkpoint: "Heeft iedereen een JSON resultaat?"*

---

## Slide 8: Stap 5 — Poll Detail Pagina

### Op de Slide
- **Maak `src/app/poll/[id]/page.tsx`:**

```tsx
import { notFound } from "next/navigation";
import { getPollById } from "@/lib/data";
import VoteForm from "@/components/VoteForm";
import type { Metadata } from "next";

interface PageProps {
  params: Promise<{ id: string }>;
}

export async function generateMetadata({ params }: PageProps): Promise<Metadata> {
  const { id } = await params;
  const poll = getPollById(id);
  if (!poll) return { title: "Poll niet gevonden" };
  return {
    title: `${poll.question} — QuickPoll`,
    description: `Stem op: ${poll.options.join(", ")}`,
  };
}

export default async function PollPage({ params }: PageProps) {
  const { id } = await params;
  const poll = getPollById(id);

  if (!poll) {
    notFound();
  }

  return (
    <div className="max-w-2xl mx-auto py-12 px-4">
      <h1 className="text-2xl font-bold text-gray-900 mb-6">
        {poll.question}
      </h1>
      <VoteForm poll={poll} />
    </div>
  );
}
```

- **`generateMetadata`** — dynamische titel per poll in browser tab
- **`notFound()`** — toont 404 als poll niet bestaat
- **Server Component** die Client Component (`VoteForm`) rendert

### Docentnotities
**💡 Theorie-moment: generateMetadata & notFound**

"`generateMetadata`: elke poll krijgt z'n eigen titel. Open straks `/poll/1` en kijk naar je browser tab."

"`notFound()`: als de poll niet bestaat, roep je dit aan. Next.js toont dan automatisch een 404 pagina. Geen if/else redirect nodig."

"En kijk: Server Component rendert een Client Component. Server haalt data, client doet interactie. Dat is de kern van Next.js."

*Checkpoint: "/poll/1 geeft een error — VoteForm bestaat nog niet. Dat klopt."*

---

## Slide 9: Pauze ☕

### Op de Slide
- **PAUZE — 15 minuten**
- Check: stap 0-5 af? Alles draait?
- Na de pauze: VoteForm (het leukste stuk)

### Docentnotities
"Pauze! We zijn halverwege. Stap 0-5 staan er. Na de pauze bouwen we de VoteForm — dat is het interactieve hart van de app."

*Tim loopt rond, helpt waar nodig.*

---

## Slide 10: Stap 6 — VoteForm (Deel 1: Logica)

### Op de Slide
- **Maak `src/components/VoteForm.tsx`:**

```tsx
"use client";

import { useState } from "react";
import type { Poll } from "@/types";

interface VoteFormProps {
  poll: Poll;
}

export default function VoteForm({ poll }: VoteFormProps) {
  const [selectedOption, setSelectedOption] = useState<number | null>(null);
  const [hasVoted, setHasVoted] = useState<boolean>(false);
  const [isSubmitting, setIsSubmitting] = useState<boolean>(false);
  const [currentPoll, setCurrentPoll] = useState<Poll>(poll);

  const totalVotes: number = currentPoll.votes.reduce(
    (sum, v) => sum + v, 0
  );

  function getPercentage(votes: number): number {
    if (totalVotes === 0) return 0;
    return Math.round((votes / totalVotes) * 100);
  }

  async function handleVote(): Promise<void> {
    if (selectedOption === null || isSubmitting) return;
    setIsSubmitting(true);

    const response = await fetch(`/api/polls/${currentPoll.id}/vote`, {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ optionIndex: selectedOption }),
    });

    if (response.ok) {
      const updatedPoll: Poll = await response.json();
      setCurrentPoll(updatedPoll);
      setHasVoted(true);
    }

    setIsSubmitting(false);
  }

  // UI op volgende slide...
```

- **`"use client"`** — dit component draait in de browser
- **4 state variables** met TypeScript types
- **`handleVote()`** — POST naar de API route uit stap 4

### Docentnotities
**💡 Theorie-moment: Client Components & useState**

"Dit is het hart van de app. `'use client'` bovenaan — alles met interactiviteit moet een Client Component zijn."

"Vier states: welke optie geselecteerd (`number | null`), al gestemd, bezig met submitten, en de huidige poll data."

"`handleVote` is dezelfde fetch als we in stap 4 in de console testten. Nu zit het in een component."

*Checkpoint: "Heeft iedereen de vier useState regels en de handleVote functie?"*

---

## Slide 11: Stap 6 — VoteForm (Deel 2: UI)

### Op de Slide
```tsx
  return (
    <div className="space-y-3">
      {currentPoll.options.map((option, index) => {
        const percentage = getPercentage(currentPoll.votes[index]);
        const isSelected = selectedOption === index;

        return (
          <button
            key={index}
            onClick={() => !hasVoted && setSelectedOption(index)}
            disabled={hasVoted}
            className={`w-full text-left p-4 rounded-lg border-2 transition-all
              relative overflow-hidden ${
              hasVoted
                ? "border-gray-200 cursor-default"
                : isSelected
                  ? "border-purple-500 bg-purple-50"
                  : "border-gray-200 hover:border-purple-300 cursor-pointer"
            }`}
          >
            {hasVoted && (
              <div
                className="absolute inset-0 bg-purple-100 transition-all duration-500"
                style={{ width: `${percentage}%` }}
              />
            )}
            <div className="relative flex justify-between items-center">
              <span className="font-medium">{option}</span>
              {hasVoted && (
                <span className="text-sm font-semibold text-purple-700">
                  {percentage}%
                </span>
              )}
            </div>
          </button>
        );
      })}

      {!hasVoted && (
        <button
          onClick={handleVote}
          disabled={selectedOption === null || isSubmitting}
          className="w-full bg-purple-600 text-white py-3 rounded-lg font-medium
            hover:bg-purple-700 disabled:bg-gray-300 transition-colors mt-4"
        >
          {isSubmitting ? "Bezig met stemmen..." : "Stem!"}
        </button>
      )}

      {hasVoted && (
        <p className="text-center text-green-600 font-medium mt-4">
          Bedankt voor je stem! Totaal: {totalVotes} stemmen
        </p>
      )}
    </div>
  );
}
```

- **Twee UI states:** vóór stemmen (selecteer) en na stemmen (percentage bars)
- **Percentage bar:** `absolute inset-0` + `width: percentage%` + `transition-all`
- **Conditional classes:** ternary in className
- **Test:** `localhost:3000/poll/1` → stem en zie de animatie!

### Docentnotities
"De UI heeft twee toestanden. Vóór stemmen: klik een optie, paarse border. Na stemmen: geanimeerde percentage bars."

"De percentage bar: een div die de achtergrond vult. De breedte is het percentage. `transition-all duration-500` animeert het. Pure CSS via Tailwind."

*Tim opent /poll/1, klikt een optie, stemt.*

"Werkt het? Klik op een optie, klik Stem. Zie je de animatie?"

*Tim loopt rond. Dit is het lastigste stuk — neem 10-15 min extra als nodig.*

---

## Slide 12: Stap 7 — Loading, Error & Not-Found

### Op de Slide
**`src/app/loading.tsx`:**
```tsx
export default function Loading() {
  return (
    <div className="container mx-auto py-12 px-4 space-y-4">
      <div className="animate-pulse">
        <div className="h-8 bg-gray-200 rounded w-1/3 mb-2" />
        <div className="h-4 bg-gray-200 rounded w-1/2 mb-8" />
      </div>
      {[1, 2, 3].map((i) => (
        <div key={i} className="animate-pulse bg-white rounded-xl border border-gray-200 p-6">
          <div className="h-5 bg-gray-200 rounded w-3/4 mb-3" />
          <div className="h-4 bg-gray-200 rounded w-1/4" />
        </div>
      ))}
    </div>
  );
}
```

**`src/app/error.tsx`** *(let op: `"use client"` verplicht!)*:
```tsx
"use client";
export default function Error({ error, reset }: { error: Error; reset: () => void }) {
  return (
    <div className="text-center py-16">
      <h2 className="text-2xl font-bold text-red-600 mb-4">Er ging iets mis!</h2>
      <p className="text-gray-600 mb-6">{error.message}</p>
      <button onClick={() => reset()}
        className="bg-purple-600 text-white px-6 py-3 rounded-lg hover:bg-purple-700">
        Probeer opnieuw
      </button>
    </div>
  );
}
```

**`src/app/not-found.tsx`:**
```tsx
import Link from "next/link";
export default function NotFound() {
  return (
    <div className="text-center py-16">
      <h2 className="text-4xl font-bold text-gray-900 mb-4">404</h2>
      <p className="text-gray-600 mb-6">Deze pagina bestaat niet.</p>
      <Link href="/" className="bg-purple-600 text-white px-6 py-3 rounded-lg
        hover:bg-purple-700 inline-block">Terug naar home</Link>
    </div>
  );
}
```

- **Test:** `/poll/999` → not-found pagina

### Docentnotities
**💡 Theorie-moment: Speciale bestanden**

"In gewoon React bouw je dit zelf. In Next.js maak je een bestand aan en het werkt."

"Drie bestanden: `loading.tsx` met skeleton UI (`animate-pulse`), `error.tsx` als error boundary (**altijd `'use client'`!**), en `not-found.tsx` voor 404's."

"Test: ga naar `/poll/999`. Daar is je 404. Dat werkt door `notFound()` in de poll pagina."

---

## Slide 13: Stap 8 — Middleware

### Op de Slide
- **Maak `src/middleware.ts`** *(in src/ root, NIET in een folder!)*:

```tsx
import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";

export function middleware(request: NextRequest): NextResponse {
  const start = Date.now();
  console.log(`[${request.method}] ${request.nextUrl.pathname}`);

  const response = NextResponse.next();
  response.headers.set("x-request-time", String(Date.now() - start));
  return response;
}

export const config = {
  matcher: ["/api/:path*", "/poll/:path*"],
};
```

- **Locatie:** altijd `src/middleware.ts` — nergens anders
- **`matcher`** — welke routes de middleware triggert
- **Test:** terminal logs + DevTools Headers → `x-request-time`

### Docentnotities
**💡 Theorie-moment: Middleware**

"Middleware is een portier — elke request passeert hier eerst. Nu loggen we alleen, maar bij Supabase wordt dit authenticatie: is de user ingelogd?"

"Let op de locatie: `src/middleware.ts`. Niet in `app/`, niet in een subfolder. Dit is het enige bestand in Next.js waar de locatie echt uitmaakt."

"Open je terminal. Klik op een poll. Zie je `[GET] /poll/1`? Dat is middleware."

---

## Slide 14: Huiswerk & Afsluiting

### Op de Slide
- **Wat we gebouwd hebben:** De hele QuickPoll app van scratch
  - Types & in-memory data
  - Layout, homepage, API routes (GET + POST)
  - Poll detail pagina met dynamic metadata
  - VoteForm met interactieve UI
  - Loading, Error, Not-Found states
  - Middleware

- **De Next.js flow:**
  Route (folder) → Page (server) → Client Component (interactie) → API Route (data) → Response

- **Huiswerk:**
  - App niet af? → Afmaken
  - App af? → Bonus: "Nieuwe Poll Aanmaken" pagina (`/create`)
  - Zet je code op GitHub

- **Volgende les:** Tailwind CSS & shadcn/ui

### Docentnotities
"In één les van nul naar werkende app. Dat is Next.js. Routing, server components, client components, API routes, middleware — jullie snappen de kern."

"Bonus: maak een `/create` pagina met een form en POST naar `/api/polls`. Goed oefenmateriaal."

"Volgende les: styling met Tailwind en shadcn/ui."

"Goed gedaan!"
