# 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 `` 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 ( ``` **Vertel:** "Nu de navbar. Simpel: logo en een home link." ```tsx ``` Main en footer: ```tsx
{children}
); } ``` **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 (

QuickPoll

Kies een poll en stem af in een oogwenk

``` **Vertel:** "Nu de poll cards. We mappen over de polls array." ```tsx
{polls.map((poll) => { const totalVotes = poll.votes.reduce((sum, v) => sum + v, 0); return (

{poll.question}

{poll.options.length} opties {totalVotes} stemmen
Stemmen →
); })}
); } ``` **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 { ``` "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 { 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 (

{poll.question}

); } ``` **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(null); ``` "Is er al gestemd?" ```tsx const [hasVoted, setHasVoted] = useState(false); ``` "Zijn we bezig met het versturen? Om dubbel klikken te voorkomen:" ```tsx const [isSubmitting, setIsSubmitting] = useState(false); ``` "De huidige poll data — die updaten we na het stemmen:" ```tsx const [currentPoll, setCurrentPoll] = useState(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 { ``` "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 (
{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 ( ); })} ``` ### 6f. UI — Stem button en bedankt bericht **Vertel:** "De stem-knop. Alleen zichtbaar als je nog niet gestemd hebt." ```tsx {!hasVoted && ( )} ``` "En het bedankt bericht na het stemmen:" ```tsx {hasVoted && (

Bedankt voor je stem! Totaal: {totalVotes} stemmen

)}
); } ``` ### 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 (
{[1, 2, 3].map((i) => (
))}
); } ``` ### 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 (

Er ging iets mis!

{error.message}

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

404

Deze pagina bestaat niet.

Terug naar home
); } ``` **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 { ``` "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 (

Nieuwe Poll Aanmaken

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" />
``` "De opties — we mappen over de options array:" ```tsx
{options.map((option, index) => ( 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" /> ))}
``` "Knop om meer opties toe te voegen:" ```tsx
``` "En de submit knop:" ```tsx
); } ``` ### 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 Nieuwe Poll ``` ### 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