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

1184 lines
28 KiB
Markdown

# 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:**
```bash
npx create-next-app@latest quickpoll
```
**Opties selecteren:**
- 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 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:
```tsx
export interface Poll {
```
**Vertel:** "Een poll heeft een id…"
```tsx
id: string;
```
"…een vraag…"
```tsx
question: string;
```
"…een lijst opties…"
```tsx
options: string[];
```
"…en stemmen per optie. De index matcht: votes[0] hoort bij options[0]."
```tsx
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:
```tsx
import type { Poll } from "@/types";
```
**Vertel:** "We maken drie polls aan als testdata."
```tsx
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."
```tsx
export function getPolls(): Poll[] {
return polls;
}
```
"Eén poll ophalen op id:"
```tsx
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:"
```tsx
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:"
```tsx
if (!poll || optionIndex < 0 || optionIndex >= poll.options.length) {
return undefined;
}
```
"Stem ophogen en poll teruggeven:"
```tsx
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:
```tsx
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:
```tsx
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:
```tsx
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."
```tsx
<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:
```tsx
<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:
```tsx
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."
```tsx
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:
```tsx
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."
```tsx
<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:
```tsx
import { NextResponse } from "next/server";
import { getPollById } from "@/lib/data";
```
**Vertel:** "Een API route exporteert functies met HTTP method namen. GET voor data ophalen."
```tsx
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!"
```tsx
const { id } = await params;
const poll = getPollById(id);
```
"Als de poll niet bestaat: 404."
```tsx
if (!poll) {
return NextResponse.json({ error: "Poll not found" }, { status: 404 });
}
```
"Anders: stuur de poll als JSON."
```tsx
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:
```tsx
import { NextResponse } from "next/server";
import { votePoll } from "@/lib/data";
```
**Vertel:** "Eerst definiëren we types voor de parameters en de request body."
```tsx
interface RouteParams {
params: Promise<{ id: string }>;
}
interface VoteBody {
optionIndex: number;
}
```
**Vertel:** "Een POST route volgt altijd vijf stappen: params, body, validatie, actie, response."
```tsx
export async function POST(
request: Request,
{ params }: RouteParams
): Promise<NextResponse> {
```
"Stap 1: params uitlezen — welke poll?"
```tsx
const { id } = await params;
```
"Stap 2: body uitlezen — welke optie? `request.json()` leest wat de client meestuurt."
```tsx
const body: VoteBody = await request.json();
```
"Stap 3: validatie — is optionIndex een nummer?"
```tsx
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."
```tsx
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."
```tsx
return NextResponse.json(updatedPoll);
}
```
### 4b. Testen in de browser console
**Vertel:** "We testen! Open DevTools (F12), ga naar Console, en plak dit:"
```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)
```
**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:
```tsx
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:
```tsx
interface PageProps {
params: Promise<{ id: string }>;
}
```
**Vertel:** "Eerste nieuw concept: `generateMetadata`. Hiermee krijgt elke poll z'n eigen titel in de browser tab."
```tsx
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:
```tsx
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."
```tsx
if (!poll) {
notFound();
}
```
"En we renderen de poll titel en het VoteForm component:"
```tsx
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."
```tsx
"use client";
```
Imports:
```tsx
import { useState } from "react";
import type { Poll } from "@/types";
```
Props interface:
```tsx
interface VoteFormProps {
poll: Poll;
}
```
**Vertel:** "We ontvangen een poll als prop van de Server Component."
Functie openen:
```tsx
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:"
```tsx
const [selectedOption, setSelectedOption] = useState<number | null>(null);
```
"Is er al gestemd?"
```tsx
const [hasVoted, setHasVoted] = useState<boolean>(false);
```
"Zijn we bezig met het versturen? Om dubbel klikken te voorkomen:"
```tsx
const [isSubmitting, setIsSubmitting] = useState<boolean>(false);
```
"De huidige poll data — die updaten we na het stemmen:"
```tsx
const [currentPoll, setCurrentPoll] = useState<Poll>(poll);
```
**Checkpoint:** "Heeft iedereen de vier useState regels?"
### 6c. Helper functies
**Vertel:** "Totaal stemmen berekenen:"
```tsx
const totalVotes: number = currentPoll.votes.reduce(
(sum, v) => sum + v,
0
);
```
"Percentage per optie:"
```tsx
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."
```tsx
async function handleVote(): Promise<void> {
```
"Guard clause: als er niks geselecteerd is of we al bezig zijn, stop."
```tsx
if (selectedOption === null || isSubmitting) return;
setIsSubmitting(true);
```
"De fetch naar onze POST route:"
```tsx
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."
```tsx
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."
```tsx
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."
```tsx
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."
```tsx
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."
```tsx
{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:
```tsx
<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."
```tsx
{!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:"
```tsx
{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."
```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>
);
}
```
### 7b. Maak `src/app/error.tsx`
**Vertel:** "Error boundary. LET OP: `'use client'` is hier VERPLICHT. Dat is een Next.js vereiste."
```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>
);
}
```
**Vertel:** "`reset` herlaadt het component dat faalde. Zo hoeft de user niet de hele pagina te refreshen."
### 7c. Maak `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:** 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."
```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;
}
```
**Vertel:** "`NextResponse.next()` betekent: alles oké, ga door naar de route."
"De matcher bepaalt welke routes de middleware raakt:"
```tsx
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:"
```tsx
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`."
```tsx
import { NextResponse } from "next/server";
import { createPoll } from "@/lib/data";
```
**Vertel:** "We definiëren een type voor de request body."
```tsx
interface CreatePollBody {
question: string;
options: string[];
}
```
"De POST handler:"
```tsx
export async function POST(request: Request): Promise<NextResponse> {
```
"Body uitlezen:"
```tsx
const body: CreatePollBody = await request.json();
```
"Validatie — vraag moet er zijn, en minimaal 2 opties:"
```tsx
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:"
```tsx
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."
```tsx
"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."
```tsx
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:"
```tsx
function addOption() {
setOptions([...options, ""]);
}
```
"En een functie om een optie te updaten op een specifieke index:"
```tsx
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."
```tsx
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:"
```tsx
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:"
```tsx
<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:"
```tsx
<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:"
```tsx
<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>
);
}
```
### Bonus D. Link toevoegen in layout
**Vertel:** "Nog even een link in de navbar. Open `layout.tsx` en voeg toe naast de Home link:"
```tsx
<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