Files
novi-lessons/Les06-NextJS-QuickPoll-Part2/Les06-Live-Coding-Guide.md
2026-03-17 17:24:10 +01:00

28 KiB

Les 6: QuickPoll Live Coding Guide

Dit is je stap-voor-stap spiekbriefje. Lees dit op je eigen scherm terwijl je op de beamer codeert. Alles staat in de volgorde waarin je het typt. Niets wordt overgeslagen.


STAP 0 — Project aanmaken

Terminal:

npx create-next-app@latest quickpoll

Opties selecteren:

  • TypeScript → Yes
  • ESLint → Yes
  • Tailwind CSS → Yes
  • src/ directory → Yes
  • App Router → Yes
  • Import alias → @/*
cd quickpoll
npm run dev

Check: localhost:3000 → Next.js standaardpagina

Vertel: "Dit is create-next-app. Geeft ons een compleet project met TypeScript, Tailwind en de App Router. Alles wat we nodig hebben."


STAP 1 — Types

1a. Maak src/types/index.ts

Vertel: "Eerst: wat IS een poll? We definiëren dat met een TypeScript interface."

Typ:

export interface Poll {

Vertel: "Een poll heeft een id…"

  id: string;

"…een vraag…"

  question: string;

"…een lijst opties…"

  options: string[];

"…en stemmen per optie. De index matcht: votes[0] hoort bij options[0]."

  votes: number[];
}

STAP 1 vervolg — Data

1b. Maak src/lib/data.ts

Vertel: "Nu onze 'database'. Gewoon een array in het geheugen. Later vervangen we dit door Supabase."

Typ de import:

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

Vertel: "We maken drie polls aan als testdata."

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],
  },
];

Vertel: "Nu helper functies. Eerst: alle polls ophalen."

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

"Eén poll ophalen op id:"

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

Vertel: "Let op het return type: Poll | undefined. Misschien bestaat de poll niet — dat moeten we afvangen."

"En stemmen:"

export function votePoll(pollId: string, optionIndex: number): Poll | undefined {
  const poll = polls.find((p) => p.id === pollId);

"Check of de poll bestaat en de index klopt:"

  if (!poll || optionIndex < 0 || optionIndex >= poll.options.length) {
    return undefined;
  }

"Stem ophogen en poll teruggeven:"

  poll.votes[optionIndex]++;
  return poll;
}

Checkpoint: "Heeft iedereen types/index.ts en lib/data.ts?"


STAP 2 — Layout

2a. Vervang src/app/layout.tsx

Vertel: "De layout wrapt elke pagina. Navbar en footer staan hier — die veranderen nooit."

Wis de inhoud. Typ de imports:

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

Vertel: "Link is de Next.js versie van een <a> tag. Client-side navigatie, geen page reload."

SEO metadata:

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

Vertel: "metadata is voor SEO. Kijk straks in je browser tab."

De functie:

export default function RootLayout({
  children,
}: Readonly<{ children: React.ReactNode }>) {
  return (
    <html lang="nl">
      <body className="bg-gray-50 min-h-screen flex flex-col">

Vertel: "Nu de navbar. Simpel: logo en een home link."

        <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 en footer:

        <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>
  );
}

Vertel: "{children} — hier komt de pagina-inhoud. Elke pagina in de app wordt hier ingeladen."

Check: localhost:3000 → navbar met "QuickPoll" zichtbaar


2b. Vervang src/app/page.tsx

Vertel: "Nu de homepage. Dit is een Server Component — geen 'use client', data direct ophalen."

Wis de inhoud. Typ:

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

Vertel: "We importeren getPolls uit onze data module. Dat @/ is een shortcut voor de src/ folder."

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

Vertel: "Kijk: we roepen getPolls() gewoon aan. Geen useEffect, geen loading state. Dit is een Server Component — data ophalen is direct."

De JSX:

  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>

Vertel: "Nu de poll cards. We mappen over de polls array."

      <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>
  );
}

Vertel: ".reduce() telt alle stemmen op. En elke card linkt naar /poll/1, /poll/2 etc. Die pagina maken we straks."

Check: localhost:3000 → 3 poll cards zichtbaar

Checkpoint: "Ziet iedereen 3 cards? Mooi."


STAP 3 — GET API Route

3a. Maak src/app/api/polls/[id]/route.ts

Vertel: "Nu onze eerste API route. In Next.js is de folder-structuur je URL. Deze folder wordt /api/polls/1."

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

Typ:

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

Vertel: "Een API route exporteert functies met HTTP method namen. GET voor data ophalen."

export async function GET(
  request: Request,
  { params }: { params: Promise<{ id: string }> }
) {

Vertel: "Let op: Promise<{ id: string }>. In Next.js 15 zijn params een Promise. Vergeet de await niet!"

  const { id } = await params;
  const poll = getPollById(id);

"Als de poll niet bestaat: 404."

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

"Anders: stuur de poll als JSON."

  return NextResponse.json(poll);
}

Test: Open browser → localhost:3000/api/polls/1 → JSON!

Vertel: "Probeer ook /api/polls/999 — daar krijg je de 404."

Checkpoint: "Werkt de API? Recap klaar. Nu het nieuwe werk."


STAP 4 — POST Vote Route

4a. Maak src/app/api/polls/[id]/vote/route.ts

Vertel: "Tot nu toe hadden we GET — data ophalen. Nu POST — data wijzigen. In dit geval: stemmen."

Typ de imports:

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

Vertel: "Eerst definiëren we types voor de parameters en de request body."

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

interface VoteBody {
  optionIndex: number;
}

Vertel: "Een POST route volgt altijd vijf stappen: params, body, validatie, actie, response."

export async function POST(
  request: Request,
  { params }: RouteParams
): Promise<NextResponse> {

"Stap 1: params uitlezen — welke poll?"

  const { id } = await params;

"Stap 2: body uitlezen — welke optie? request.json() leest wat de client meestuurt."

  const body: VoteBody = await request.json();

"Stap 3: validatie — is optionIndex een nummer?"

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

Vertel: "400 = bad request. Jij stuurde slechte data."

"Stap 4: actie uitvoeren."

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

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

Vertel: "404 = niet gevonden. Twee checks: poll bestaat niet, of optie-index ongeldig."

"Stap 5: response sturen."

  return NextResponse.json(updatedPoll);
}

4b. Testen in de browser console

Vertel: "We testen! Open DevTools (F12), ga naar Console, en plak dit:"

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

Vertel: "Zie je? De votes zijn veranderd in de JSON. De API werkt."

Checkpoint: "Heeft iedereen een JSON resultaat in de console?"


STAP 5 — Poll Detail Pagina

5a. Maak src/app/poll/[id]/page.tsx

Vertel: "Nu de pagina waar je op een poll stemt. Weer een dynamic route: /poll/1, /poll/2 etc."

Imports:

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

Vertel: "We importeren VoteForm — dat component bestaat nog niet, dat bouwen we zo."

Type:

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

Vertel: "Eerste nieuw concept: generateMetadata. Hiermee krijgt elke poll z'n eigen titel in de browser tab."

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(", ")}`,
  };
}

Vertel: "Gratis SEO. Google ziet 'Wat is de beste editor?' in plaats van gewoon 'QuickPoll'."

Nu de pagina zelf:

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

Vertel: "Tweede concept: notFound(). Als de poll niet bestaat, roep je dit aan. Next.js toont dan automatisch een 404 pagina."

  if (!poll) {
    notFound();
  }

"En we renderen de poll titel en het VoteForm component:"

  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>
  );
}

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

Check: localhost:3000/poll/1 → error (VoteForm bestaat nog niet, dat klopt!)


PAUZE — 15 minuten

Vertel: "Pauze! Stap 0-5 staan er. Na de pauze bouwen we het interactieve hart: de VoteForm."


STAP 6 — VoteForm Component

6a. Maak src/components/VoteForm.tsx

Vertel: "Dit is het leukste stuk. Een Client Component met interactiviteit."

"Eerste regel: 'use client'. Daarmee zeg je tegen Next.js: dit draait in de browser. Alles met useState of onClick MOET dit hebben."

"use client";

Imports:

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

Props interface:

interface VoteFormProps {
  poll: Poll;
}

Vertel: "We ontvangen een poll als prop van de Server Component."

Functie openen:

export default function VoteForm({ poll }: VoteFormProps) {

6b. State variabelen

Vertel: "We hebben vier stukken state nodig. Ik leg ze één voor één uit."

"Welke optie is geselecteerd? null want er kan nog niks geselecteerd zijn:"

  const [selectedOption, setSelectedOption] = useState<number | null>(null);

"Is er al gestemd?"

  const [hasVoted, setHasVoted] = useState<boolean>(false);

"Zijn we bezig met het versturen? Om dubbel klikken te voorkomen:"

  const [isSubmitting, setIsSubmitting] = useState<boolean>(false);

"De huidige poll data — die updaten we na het stemmen:"

  const [currentPoll, setCurrentPoll] = useState<Poll>(poll);

Checkpoint: "Heeft iedereen de vier useState regels?"

6c. Helper functies

Vertel: "Totaal stemmen berekenen:"

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

"Percentage per optie:"

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

6d. handleVote functie

Vertel: "Nu de stem-functie. Dit is dezelfde fetch als we net in de console testten — maar nu in een component."

  async function handleVote(): Promise<void> {

"Guard clause: als er niks geselecteerd is of we al bezig zijn, stop."

    if (selectedOption === null || isSubmitting) return;
    setIsSubmitting(true);

"De fetch naar onze POST route:"

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

"Als het lukt: update de poll data en zet hasVoted op true."

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

    setIsSubmitting(false);
  }

Checkpoint: "Tot hier mee? Dan gaan we de UI bouwen."

6e. UI — De opties buttons

Vertel: "De return. We mappen over de opties — elke optie wordt een button."

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

Vertel: "Elke button. onClick selecteert de optie, maar alleen als je nog niet gestemd hebt."

        return (
          <button
            key={index}
            onClick={() => !hasVoted && setSelectedOption(index)}
            disabled={hasVoted}

Vertel: "Nu de conditional classes. Dit is hoe je in Tailwind conditioneel stylet — ternary operators."

            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"
            }`}
          >

Vertel: "De percentage bar. Een div die de achtergrond paars vult. De breedte is het percentage."

            {hasVoted && (
              <div
                className="absolute inset-0 bg-purple-100 transition-all duration-500"
                style={{ width: `${percentage}%` }}
              />
            )}

Vertel: "transition-all duration-500 zorgt voor de animatie. Pure CSS via Tailwind."

De tekst met 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>
        );
      })}

6f. UI — Stem button en bedankt bericht

Vertel: "De stem-knop. Alleen zichtbaar als je nog niet gestemd hebt."

      {!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>
      )}

"En het bedankt bericht na het stemmen:"

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

6g. Testen!

Test: localhost:3000/poll/1

Vertel: "Klik op een optie — paarse border. Klik Stem. Zie je de animatie? De bars vullen zich."

Loop rond. Dit is het lastigste stuk. Neem 10 min extra als nodig.

Veelvoorkomende fouten:

  • "use client" vergeten → crash
  • Fetch URL zonder backticks → werkt niet
  • Bestand niet opgeslagen → niks veranderd

STAP 7 — Loading, Error & Not-Found

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

7a. Maak src/app/loading.tsx

Vertel: "Skeleton loading. Die grijze blokken die pulseren — animate-pulse is een Tailwind class."

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>
  );
}

7b. Maak src/app/error.tsx

Vertel: "Error boundary. LET OP: 'use client' is hier VERPLICHT. Dat is een Next.js vereiste."

"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>
  );
}

Vertel: "reset herlaadt het component dat faalde. Zo hoeft de user niet de hele pagina te refreshen."

7c. Maak src/app/not-found.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: localhost:3000/poll/999 → 404 pagina!

Vertel: "Dat werkt door de notFound() aanroep in onze poll pagina."

Checkpoint: "Alle drie werkend?"


STAP 8 — Middleware

8a. Maak src/middleware.ts

⚠️ Let op locatie: src/middleware.ts — NIET in app/, NIET in een subfolder!

Vertel: "Middleware is als een portier. Elke request passeert hier eerst. Nu gebruiken we het voor logging — later bij Supabase voor authenticatie."

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;
}

Vertel: "NextResponse.next() betekent: alles oké, ga door naar de route."

"De matcher bepaalt welke routes de middleware raakt:"

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

Vertel: "Alleen API routes en poll pagina's. De homepage skipt de middleware."

8b. Testen

Vertel: "Open je terminal waar npm run dev draait. Klik op een poll in de browser."

In de terminal: [GET] /poll/1 verschijnt.

Vertel: "En in DevTools: Network tab → klik op een request → Headers → x-request-time. Dat is de verwerkingstijd die onze middleware toevoegt."


APP COMPLEET!

Vertel: "De hele app werkt. Van nul naar werkend in één les."


BONUS — Nieuwe Poll Aanmaken (als je tijd overhebt)

~20 minuten. Doe dit klassikaal als je voor 2:45 klaar bent.

Vertel: "We gaan een pagina maken waar je zelf een nieuwe poll kunt aanmaken. Twee dingen nodig: een API route die polls aanmaakt, en een pagina met een formulier."

Bonus A. Voeg createPoll toe aan src/lib/data.ts

Vertel: "Eerst een helper functie in onze data module. Open data.ts en voeg onderaan toe:"

export function createPoll(question: string, options: string[]): Poll {
  const newPoll: Poll = {
    id: String(nextId++),
    question,
    options,
    votes: new Array(options.length).fill(0),
  };
  polls.push(newPoll);
  return newPoll;
}

Vertel: "new Array(options.length).fill(0) — maakt een array met nullen, even lang als het aantal opties. Elke optie begint met 0 stemmen."

Bonus B. Maak src/app/api/polls/route.ts

Vertel: "Nu de API route. Let op: dit is api/polls/route.ts, niet in de [id] folder. Dit wordt POST /api/polls."

import { NextResponse } from "next/server";
import { createPoll } from "@/lib/data";

Vertel: "We definiëren een type voor de request body."

interface CreatePollBody {
  question: string;
  options: string[];
}

"De POST handler:"

export async function POST(request: Request): Promise<NextResponse> {

"Body uitlezen:"

  const body: CreatePollBody = await request.json();

"Validatie — vraag moet er zijn, en minimaal 2 opties:"

  if (!body.question || !body.options || body.options.length < 2) {
    return NextResponse.json(
      { error: "Vraag en minimaal 2 opties zijn verplicht" },
      { status: 400 }
    );
  }

"Poll aanmaken en teruggeven:"

  const newPoll = createPoll(body.question, body.options);
  return NextResponse.json(newPoll, { status: 201 });
}

Vertel: "201 = created. Standaard HTTP status als je iets nieuws aanmaakt."

Bonus C. Maak src/app/create/page.tsx

Vertel: "Nu het formulier. Dit is een Client Component — we hebben state nodig voor de inputs."

"use client";

import { useState } from "react";
import { useRouter } from "next/navigation";

Vertel: "useRouter is voor navigatie vanuit een Client Component. Straks sturen we de user terug naar home."

export default function CreatePollPage() {
  const router = useRouter();
  const [question, setQuestion] = useState("");
  const [options, setOptions] = useState(["", ""]);
  const [isSubmitting, setIsSubmitting] = useState(false);

Vertel: "Drie stukken state. De vraag, de opties (begint met 2 lege strings), en een submit flag."

"Een functie om een optie toe te voegen:"

  function addOption() {
    setOptions([...options, ""]);
  }

"En een functie om een optie te updaten op een specifieke index:"

  function updateOption(index: number, value: string) {
    const newOptions = [...options];
    newOptions[index] = value;
    setOptions(newOptions);
  }

Vertel: "Nu de submit functie. Filtert lege opties weg, POST naar de API, redirect naar home."

  async function handleSubmit(e: React.FormEvent) {
    e.preventDefault();
    setIsSubmitting(true);

    const filledOptions = options.filter((opt) => opt.trim() !== "");

    if (!question.trim() || filledOptions.length < 2) {
      alert("Vul een vraag in en minimaal 2 opties.");
      setIsSubmitting(false);
      return;
    }

    const response = await fetch("/api/polls", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ question, options: filledOptions }),
    });

    if (response.ok) {
      router.push("/");
    }

    setIsSubmitting(false);
  }

Vertel: "e.preventDefault() voorkomt dat de pagina herlaadt. router.push('/') stuurt je terug naar de homepage."

"Nu de JSX. Een form met inputs:"

  return (
    <div className="max-w-2xl mx-auto py-12 px-4">
      <h1 className="text-2xl font-bold text-gray-900 mb-6">
        Nieuwe Poll Aanmaken
      </h1>

      <form onSubmit={handleSubmit} className="space-y-6">
        <div>
          <label className="block text-sm font-medium text-gray-700 mb-2">
            Vraag
          </label>
          <input
            type="text"
            value={question}
            onChange={(e) => setQuestion(e.target.value)}
            placeholder="Stel je vraag..."
            className="w-full p-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent"
          />
        </div>

"De opties — we mappen over de options array:"

        <div>
          <label className="block text-sm font-medium text-gray-700 mb-2">
            Opties
          </label>
          <div className="space-y-3">
            {options.map((option, index) => (
              <input
                key={index}
                type="text"
                value={option}
                onChange={(e) => updateOption(index, e.target.value)}
                placeholder={`Optie ${index + 1}`}
                className="w-full p-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent"
              />
            ))}
          </div>

"Knop om meer opties toe te voegen:"

          <button
            type="button"
            onClick={addOption}
            className="mt-3 text-purple-600 hover:text-purple-700 font-medium text-sm"
          >
            + Optie toevoegen
          </button>
        </div>

"En de submit knop:"

        <button
          type="submit"
          disabled={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"
        >
          {isSubmitting ? "Aanmaken..." : "Poll Aanmaken"}
        </button>
      </form>
    </div>
  );
}

Vertel: "Nog even een link in de navbar. Open layout.tsx en voeg toe naast de Home link:"

<Link
  href="/create"
  className="text-gray-700 hover:text-purple-600 transition-colors font-medium"
>
  Nieuwe Poll
</Link>

Bonus E. Testen!

Test: localhost:3000/create → vul een vraag in → voeg opties toe → klik aanmaken → redirect naar home → nieuwe poll staat er!

Vertel: "Klaar! De hele app is compleet. Je kunt polls bekijken, stemmen, en nieuwe polls aanmaken."


HELEMAAL KLAAR!

Samenvatting wat we gebouwd hebben:

  1. Types & data (TypeScript interfaces, in-memory database)
  2. Layout & homepage (Server Component, Link navigatie)
  3. GET API route (folder = URL, dynamic routes)
  4. POST API route (stemmen, validatie, status codes)
  5. Poll detail pagina (generateMetadata, notFound)
  6. VoteForm (Client Component, useState, fetch, animaties)
  7. Loading, Error, Not-Found (speciale bestanden)
  8. Middleware (request interceptie)
  9. Bonus: Create poll (form, POST, redirect)

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

Volgende les: Tailwind CSS & shadcn/ui