# 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 (
{children}
);
}
```
- **`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 ``. 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 (
QuickPoll
Kies een poll en stem af in een oogwenk
{polls.map((poll) => {
const totalVotes = poll.votes.reduce((sum, v) => sum + v, 0);
return (
{poll.question}
{poll.options.length} opties
{totalVotes} stemmen
Stemmen →
);
})}
);
}
```
- **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 {
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 {
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 (
{poll.question}
);
}
```
- **`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(null);
const [hasVoted, setHasVoted] = useState(false);
const [isSubmitting, setIsSubmitting] = useState(false);
const [currentPoll, setCurrentPoll] = useState(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 {
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 (
{currentPoll.options.map((option, index) => {
const percentage = getPercentage(currentPoll.votes[index]);
const isSelected = selectedOption === index;
return (
);
})}
{!hasVoted && (
)}
{hasVoted && (
Bedankt voor je stem! Totaal: {totalVotes} stemmen
)}
);
}
```
- **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 (
{[1, 2, 3].map((i) => (
))}
);
}
```
**`src/app/error.tsx`** *(let op: `"use client"` verplicht!)*:
```tsx
"use client";
export default function Error({ error, reset }: { error: Error; reset: () => void }) {
return (
Er ging iets mis!
{error.message}
);
}
```
**`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:** `/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!"