fix: add lesson 6

This commit is contained in:
2026-03-17 17:24:10 +01:00
parent 9ffdecf2c4
commit 8df4087cfd
91 changed files with 10392 additions and 0 deletions

View File

@@ -0,0 +1,306 @@
# Les 6: Next.js — QuickPoll Compleet - Docenttekst
**NOVI Hogeschool | Instructeur: Tim | Duur: 180 minuten**
---
## VOORBEREIDING
**Checklist (30 minuten voor les):**
- [ ] Demo project klaarzetten: `quickpoll-demo` uitpakken, `npm install`
- [ ] `git checkout stap-0` → startpunt
- [ ] Test: `git checkout bonus` → volledige app draait
- [ ] Terug naar `git checkout stap-0`
- [ ] Cursor openen, terminal font minimaal 16pt
- [ ] Browser met `localhost:3000` open
- [ ] **Scherm delen:** Teams → deel alleen het Cursor-venster
- [ ] Browser/notities op apart Space (voor stiekem opzoeken)
- [ ] Losse stap-zipjes als backup
**Tempo-indicatie:**
| Blok | Tijd | Inhoud |
|------|------|--------|
| Stap 0-3 (recap) | 0:00-0:40 | Project, types, layout, homepage, GET route |
| Stap 4 | 0:40-1:00 | POST vote route |
| Stap 5 | 1:00-1:15 | Poll detail pagina |
| **PAUZE** | **1:15-1:30** | |
| Stap 6 | 1:30-2:10 | VoteForm (logica + UI) |
| Stap 7 | 2:10-2:30 | Loading, Error, Not-Found |
| Stap 8 | 2:30-2:45 | Middleware |
| Afsluiting | 2:45-3:00 | Huiswerk, vragen |
---
## STAP 0-3: RECAP (0:00-0:40)
*Dit gaat snel — studenten kennen het al. Doel: iedereen op hetzelfde punt krijgen.*
### Slide 1: Titel + Plan (0:00-0:02)
> "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 — dat gaat snel. Stap 4-8 is nieuw, daar nemen we de tijd voor."
---
### Slide 2: Stap 0 — Project Aanmaken (0:02-0:07)
*Tim opent terminal.*
> "Stap 0: `npx create-next-app@latest quickpoll`. TypeScript, Tailwind, App Router — alles yes."
*Tim wacht tot iedereen `npm run dev` draait.*
> "Zie je de Next.js standaard pagina? Door."
**⚡ Snelheid:** Studenten die dit al hebben kunnen alvast door naar stap 1.
---
### Slide 3: Stap 1 — Types & Data (0:07-0:15)
> "Twee bestanden: `src/types/index.ts` met de Poll interface, en `src/lib/data.ts` met onze in-memory database."
*Tim typt de interface.*
> "Een poll: id, question, options, votes. De votes array loopt parallel met options — index 0 van votes hoort bij index 0 van options."
*Tim typt data.ts.*
> "Drie helper functies: getPolls, getPollById, votePoll. Dit is onze 'database' — later vervangen we dit door Supabase."
*Checkpoint: "Beide bestanden staan er? Door."*
---
### Slide 4: Stap 2 — Layout (0:15-0:22)
> "De layout wrapt elke pagina. Navbar, main content, footer. Verandert nooit."
*Tim vervangt layout.tsx.*
> "`Link` is Next.js client-side navigatie — geen page reload. `metadata` is je SEO titel."
**⚡ Tip:** Niet te lang stilstaan, dit kennen ze.
---
### Slide 5: Stap 2 vervolg — Homepage (0:22-0:30)
> "De homepage: een Server Component die alle polls ophaalt en cards rendert."
*Tim vervangt page.tsx.*
> "Geen `'use client'`, geen useEffect, geen loading state. Server Component haalt data direct op. Dat is het voordeel."
> "De `.map()` rendert een card per poll. `.reduce()` telt totaal stemmen."
*Checkpoint: "3 cards op localhost:3000? Mooi."*
---
### Slide 6: Stap 3 — GET API Route (0:30-0:40)
> "Onze eerste API route. De folder-structuur IS je URL: `api/polls/[id]/route.ts` wordt `/api/polls/1`."
*Tim maakt het bestand.*
> "Dynamic route: `[id]` met vierkante haakjes. De waarde uit de URL wordt de `id` parameter."
> "`await params` — Next.js 15 ding. Params zijn een Promise. Vergeet de await niet."
*Tim test in browser: localhost:3000/api/polls/1.*
> "JSON! Test ook `/api/polls/999` — 404. Mooi, recap klaar. Nu het nieuwe werk."
---
## STAP 4-5: NIEUW MATERIAAL DEEL 1 (0:40-1:15)
### Slide 7: Stap 4 — POST Vote Route (0:40-1:00)
**💡 Theorie-moment: POST vs GET**
> "GET leest data, POST wijzigt data. Het patroon voor elke POST route: params, body, validatie, actie, response. Vijf stappen, altijd."
*Tim maakt src/app/api/polls/[id]/vote/route.ts.*
> "Nieuwe folder: `vote/` in de `[id]/` folder. Zo krijg je `/api/polls/1/vote`."
*Tim typt de code langzaam.*
> "`request.json()` leest de body — wat de client stuurt. Hier: welke optie."
> "Twee error checks: 400 als data ongeldig, 404 als poll niet bestaat. Altijd beide."
*Tim opent browser console, test met fetch.*
> "Plak dit in je console. Zie je? Votes veranderd in de JSON. Onze stem-API werkt."
*Checkpoint: "Heeft iedereen JSON in de console? Mooi."*
---
### Slide 8: Stap 5 — Poll Detail Pagina (1:00-1:15)
**💡 Theorie-moment: generateMetadata & notFound**
> "`generateMetadata` — speciale Next.js functie voor dynamische SEO. Elke poll krijgt z'n eigen browser tab titel."
> "`notFound()` — roep je aan als data niet bestaat. Next.js toont automatisch de 404 pagina."
*Tim maakt src/app/poll/[id]/page.tsx.*
> "Dit is de combinatie: Server Component haalt data, rendert Client Component (`VoteForm`). Server = data, client = interactie."
> "We importeren VoteForm maar die bestaat nog niet — dat bouwen we na de pauze."
*Checkpoint: "/poll/1 geeft een error — dat klopt, VoteForm bestaat nog niet."*
---
## PAUZE (1:15-1:30)
### Slide 9: Pauze
> "Pauze! 15 minuten. Stap 0-5 staan er. Na de pauze bouwen we het interactieve hart: de VoteForm."
*Tim loopt rond en helpt studenten die achterlopen.*
---
## STAP 6: VOTEFORM (1:30-2:10)
*Dit is het langste en lastigste stuk. Neem de tijd.*
### Slide 10: Stap 6 deel 1 — Logica (1:30-1:50)
**💡 Theorie-moment: Client Components & useState**
> "`'use client'` bovenaan — dit component draait in de browser. Alles met useState, onClick, of interactiviteit MOET een Client Component zijn."
*Tim maakt src/components/VoteForm.tsx.*
> "Vier states."
*Tim typt elke state en legt uit.*
> "`selectedOption: number | null` — null want er kan nog niks geselecteerd zijn."
> "`hasVoted: boolean` — toggle na stemmen."
> "`isSubmitting: boolean` — voorkom dubbel klikken."
> "`currentPoll: Poll` — de actuele data, updated na stemmen."
*Checkpoint: "Heeft iedereen de vier useState regels?"*
*Tim typt handleVote.*
> "De handleVote functie: dezelfde fetch als in de console bij stap 4. POST naar de API, update state als het lukt."
*Checkpoint: "Tot hier mee?"*
---
### Slide 11: Stap 6 deel 2 — UI (1:50-2:10)
*Tim typt de JSX rustig, stopt na elk blok.*
> "De UI heeft twee toestanden. Vóór stemmen: selecteer een optie. Na stemmen: percentage bars."
*Tim typt de map over options.*
> "Elke optie is een button. onClick selecteert de optie — maar alleen als je nog niet gestemd hebt."
> "De conditional classes: `isSelected ? 'border-purple-500' : 'border-gray-200'`. Ternary operators in className — zo style je conditioneel in Tailwind."
*Tim typt de percentage bar.*
> "De percentage bar: een div met `absolute inset-0`, breedte is percentage, `transition-all duration-500` animeert. Pure CSS."
*Tim typt stem-knop en bedankt-bericht.*
> "Stem-knop verdwijnt na stemmen. Bedankt-bericht verschijnt. Allebei conditional rendering met `hasVoted`."
*Tim test: localhost:3000/poll/1 → klik optie → stem.*
> "Werkt het? Klik een optie, klik Stem, zie de animatie!"
*Tim loopt rond en helpt. **Neem hier 10-15 min extra als nodig.** Dit is het lastigste stuk.*
**Waar lopen studenten vast?**
- `"use client"` vergeten → crash
- `selectedOption` niet nullable (`number` i.p.v. `number | null`)
- Fetch URL zonder backticks (template literal)
- Tailwind class typos
- Vergeten om het bestand op te slaan
**Snelle fix:** "Check je terminal — TypeScript error? Heb je `'use client'` bovenaan?"
---
## STAP 7-8: FINISHING TOUCHES (2:10-2:45)
### Slide 12: Stap 7 — Loading, Error & Not-Found (2:10-2:30)
**💡 Theorie-moment: Speciale bestanden**
> "In gewoon React bouw je dit zelf. In Next.js: maak een bestand aan en het werkt."
*Tim maakt loading.tsx.*
> "Skeleton loading. `animate-pulse` — Tailwind class. Vijf regels, professioneel resultaat."
*Tim maakt error.tsx.*
> "Error boundary. **`'use client'` is verplicht!** Error boundaries draaien altijd in de browser. `reset` herlaadt het gefaalde component."
*Tim maakt not-found.tsx.*
> "404 pagina. Test: `/poll/999`. Daar is 'm. Werkt door `notFound()` in de poll pagina."
*Checkpoint: "Alle drie aangemaakt en werkend?"*
---
### Slide 13: Stap 8 — Middleware (2:30-2:45)
**💡 Theorie-moment: Middleware**
> "Middleware is een portier. Elke request passeert hier eerst."
*Tim maakt src/middleware.ts.*
> "Let op de locatie: `src/middleware.ts`. Niet in `app/`, niet in een subfolder. Dit is het enige bestand waar de locatie uitmaakt."
> "De `matcher` bepaalt welke routes het raakt. API routes en poll pagina's — de homepage skipt het."
> "Open je terminal. Klik op een poll. Zie je `[GET] /poll/1`? Dat is middleware in actie."
> "Nu loggen we alleen. Bij Supabase wordt dit authenticatie: is de user ingelogd? Zo niet, redirect."
*Checkpoint: "Logs in je terminal? App compleet!"*
---
## AFSLUITING (2:45-3:00)
### Slide 14: Huiswerk & Afsluiting
> "Wat hebben we vandaag gedaan? De hele QuickPoll app van scratch: types, layout, homepage, API routes, detail pagina, VoteForm met animaties, loading states, middleware."
> "De Next.js flow: Route → Page → Client Component → API Route → Response. Dat is het hele framework in één zin."
> "Huiswerk: app niet af? Afmaken. Wel af? Probeer de bonus: een '/create' pagina met een form en POST naar `/api/polls`."
> "Volgende les: Tailwind CSS en shadcn/ui. Dan geven we QuickPoll een styling upgrade."
> "In één les van nul naar werkende app. Goed gedaan!"
---
## POST-SESSIE
**Na afloop checken:**
- [ ] Hoeveel studenten hebben werkende VoteForm?
- [ ] Was het tempo goed? (stap 0-3 snel genoeg, stap 4-8 genoeg tijd?)
- [ ] Terugkerende problemen notieren voor v2
- [ ] Was van-scratch beter dan doorgaan waar Les 5 stopte?

View File

@@ -0,0 +1,410 @@
%PDF-1.4
%<25><><EFBFBD><EFBFBD> ReportLab Generated PDF document (opensource)
1 0 obj
<<
/F1 2 0 R /F2 3 0 R /F3 6 0 R /F4 22 0 R
>>
endobj
2 0 obj
<<
/BaseFont /Helvetica /Encoding /WinAnsiEncoding /Name /F1 /Subtype /Type1 /Type /Font
>>
endobj
3 0 obj
<<
/BaseFont /Helvetica-Bold /Encoding /WinAnsiEncoding /Name /F2 /Subtype /Type1 /Type /Font
>>
endobj
4 0 obj
<<
/Contents 27 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 26 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
5 0 obj
<<
/Contents 28 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 26 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
6 0 obj
<<
/BaseFont /Courier /Encoding /WinAnsiEncoding /Name /F3 /Subtype /Type1 /Type /Font
>>
endobj
7 0 obj
<<
/Contents 29 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 26 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
8 0 obj
<<
/Contents 30 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 26 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
9 0 obj
<<
/Contents 31 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 26 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
10 0 obj
<<
/Contents 32 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 26 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
11 0 obj
<<
/Contents 33 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 26 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
12 0 obj
<<
/Contents 34 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 26 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
13 0 obj
<<
/Contents 35 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 26 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
14 0 obj
<<
/Contents 36 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 26 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
15 0 obj
<<
/Contents 37 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 26 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
16 0 obj
<<
/Contents 38 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 26 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
17 0 obj
<<
/Contents 39 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 26 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
18 0 obj
<<
/Contents 40 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 26 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
19 0 obj
<<
/Contents 41 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 26 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
20 0 obj
<<
/Contents 42 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 26 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
21 0 obj
<<
/Contents 43 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 26 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
22 0 obj
<<
/BaseFont /ZapfDingbats /Name /F4 /Subtype /Type1 /Type /Font
>>
endobj
23 0 obj
<<
/Contents 44 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 26 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
24 0 obj
<<
/PageMode /UseNone /Pages 26 0 R /Type /Catalog
>>
endobj
25 0 obj
<<
/Author (\(anonymous\)) /CreationDate (D:20260317132407+01'00') /Creator (\(unspecified\)) /Keywords () /ModDate (D:20260317132407+01'00') /Producer (ReportLab PDF Library - \(opensource\))
/Subject (\(unspecified\)) /Title (Les 6: QuickPoll \204 Van Scratch tot Werkend) /Trapped /False
>>
endobj
26 0 obj
<<
/Count 18 /Kids [ 4 0 R 5 0 R 7 0 R 8 0 R 9 0 R 10 0 R 11 0 R 12 0 R 13 0 R 14 0 R
15 0 R 16 0 R 17 0 R 18 0 R 19 0 R 20 0 R 21 0 R 23 0 R ] /Type /Pages
>>
endobj
27 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 442
>>
stream
Gat%^gJ3Ad&;KY!MRc\rf:?>GG\PZ@d\r.pH;,frg&iqG5/1#IV^P7K$!H1npA61$K&d<_Ha3danSiW3J->il:^I;S#^8Jb"F*He-T":>!\9fI6YUgQ0`!;-S>cU[5U^9dC(Nd6B`o#FV"gI-)XsG&Et$-??SVfTTCrseHNlh]ZjQO65Cp.L\cm-aUT]_hrfSiKkMSS@?bi>(A+EJ-S<3k1oCUK&WX:FC&o)kGcq*I:>O\CD^ZjPb49EWFKI&a8<^bYAnDe`sY=TcOaUF'.J@@A-VlNDVd#B,e^ed!km8]6MIK,(RWXR7]$oA<3VD>YU@?rNDLtCrcfG<uqG$1,tX2^m?V;FU"-PU:[ePOiHJ^\rkCCP#8LaRdP<Z4c[j%"@8VWg",AQ_s+DX7Y?;/NTP-VQC#X!0rpc!71(YiDINCTojf!u;$sA!5/,~>endstream
endobj
28 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 656
>>
stream
Gau1,d;IDe'Sc)R'^#g7#`OgS3t?HRCB8-cKgf8>`bJ5iB]:X=2iX`u,V9d0g8Yme/4GHegZSHsfEZuV_"Kf@!3S)%2aTb1fag?i,\/hfbQa+1ga8k+$<*K^Os#YrN\le<1MO3e'JL[CW=W/];G#&2o)YtMTaWMX:^I/S2^O,j$:K)6Y54\bJ(F+"9:.`LEN9Jg*[#gc<<#R#(P1SiB\a<f%`B[s*rk=5>7bVo2`TIL0GLI!,"'Tg,KY[XN@#$hmVBE;rk??dhbcH[JoTlogp@9$bJO7!,tt&U:$ltAEJ/ls9VW=!%8_gF56fataBk0M"g?k<f(LA8Y$%jFIX7R-&EnmUFQm`1MdP(hs2c'pC$4mu"`5MPj9HA3&H_nZ`*J(X_[J0<TA^`GS;+50).hh+pbE;0IdlHZ*H64p_08]=)hDiK/@kLgJ7oR\\Q_9?jVk$jF+(i(,U(NqX4lOt(CR`r[ETLA[e%/=kL.>&`C_2ujXLE0dFDOYk,XMi<PSiH/:Hd*qFnIM<K+ASgoF(4E<u33S*W+-]Ue0Gr!a(8b#`\^h"pm7aKNe?\[:UG0o@("eiX*3d5G:t0DTFmE6ksRfA9NWWE![CBMtc-)u+?r.poG\?-CuWenoU"jgJ;6JPIdXQ<J.P@IOM%OUTD~>endstream
endobj
29 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 730
>>
stream
Gatn$d;IYl'Sc(%MK@=q8i=';3!i[R[SE:B:><k7,];DqPsF=Ga3]!*^I(8[>@GYkZ4u!^M>dC&NfP&SCohno-,A7L"s4[9%d=L3Jd=nci?mqDH>FNL@M5t7-4iV)";,5?*DR7[9-/dP30-JKJ0[5h\;YduiL6k("rAn3$P&]0\);YI6%@s\"Zh;iY+"V!_mM7rRVQsSkmZ!u28N@1q^=l-P(IZhi`j8FNFIGq3/lrL$F-9iTl_NdTt"$9%U'f*\R%J:BDpo5X9Q:j;T_8(4\R]6Wo?;`lss+!.Pn'\eADct!@>"8%I<un^'j-S#!Lgh)F3)6ToBn!T-/2YH$sC'aJH39Wk_L!@Bn%_\ZLa`"#>j-+Rq**6C*IA"9-u'rk&3&Ea>HP5;cEkA&!Ym1?Lu6WZC5[BH+1#$,O*]aI3OmjhbF4cl]SBK(Ra];<DobLX6%hFS*?6\c[#p:Q0BO]$8LI3.X&s6Rj`1F#/AkUX?>C9V**$G1cjB\\jV9\`Y3B_B_<"f!VpTX]g&<N^\h$S_tS_><+6Y7BUABpTFc;j7?d>A3b`C":(!#.$?LHJ'0cW,-Ioj'>;g_AP6l&eCq%OgCC/J6t@8Y-KXXm@UK)iPhibTs39YAZ7Eb1etD]^^t9:Td]D[pd'B$8(.O(!WFuALHedA#eh&E/CV\[,nf#^bmWG'poD-'t%a4LF[$Z/XLP%liAZD+u[3#U;dQd@hmhZ3[m.L~>endstream
endobj
30 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 1340
>>
stream
Gatn'?$"^h'Sc)T.o*Y=5XU4u9-8&A[[B$PGES@>B%dX^`>TLC,:"J8LcrTS]k8XDAIjH2Zbq_R`(K3TcMO2S,Q^/Y8H!b<"F)_U_-6to_Jf^4HQ7e/i/cj=k7dEjP"2)dE=bJS&s%`1_;k#t4HSpTKh!eI;KM3_/HtF/Z["Y?BILZk1us7=(Xna]TOJDe$S>FF:18CV;C25H^Ip/*0YDtq:F>p.r9PZdU.*uP8d+ST8Do78a#NiO$LmshiOFH>0@FbkdU77U9=Y'$74G`EV3]e6AKYOglT?gcf3#OqR84SOV[.K]6-"@+$A+^arrSh](*NUmgTq]gf&2?+ebE]1):Xn'"OV-N>oj4Z@da&NLkC4DoXM^1rZX(6#T6#fTO.f$gJqQn82@<)R@G]#OJaOT[@dqVB+W2W'M;XVg&,K@^`m_5piD.`9(M>`K]gifpj?Y)QeU`!9:P]5EYa.qCPC0BC3cW_]BI2(o^7cq'=%$N&[?sh_!&B60"dW"<s)AkXGeVsm.lkLL["ToK(&ESZ8S^3-LeE_krm7JHjQ?NRhF2TaIg8rr,#t6S0=e&_[8\,@YT)K$"krt?mMF%"H,SnI-*%Q=NEP!dm"n=8+*2C=N*?$8?Co!gaJQ3F^?=c.p*P^]TC=5;V3_#9Rj>,$\^CN?'uXSrfV&B0e&9Nme!4EiD*bCRFs=t`oI%o<N/'H2O&bS[6>;B&Jok'Zk#"Y!teQJgmqjAD;m9oMR>,D-P!2E0aP0"T^PX+/VEth.p+<e6<+asV@r(H7?QBLNFc.(OA&EP[?X*]gt(='MC/+YjmXQYEC_+[q[*BlBZq%#N4,5'MgEQ9P:UgRafR?)\fsC@0Ip5/^n")7>eqC<>74Ldk"_PV/!uUcW4RZ5DAt'2@*?><Tt]U,!p8dJW02ih`AKPnC`T@K.=??FCRO1GP*<I5r*:cCmp`Cs?#T\-c)XgtH%O'('Y3m)'Vca0Fpdk2A-ERVPGsNnB])X`2GXUceWTY[E7cbeKUJfDI6PWtP2"?'G(bme9@(M^orQ:_7`jjDD<Bbsg`?-4,I'\:_4])@$DjdICW:CaqrPGj2\s_d,S%S[*YQ#in:C5E\N?FD>H]<\\Uk8ifi64Z9MKB07Y9:GI*g0-LBIlBm[A(S*h5)$_s@/#00ptUSO#"OpFO`cKKEbuUfSP8%>;9m%F<L=.^mIZ\8)+;h9!m:ja<sQMi(5VG9s3PU$(Roj7YX^EIbP2"a'k8J)(d#2T,:"B7a&Mo-OON`%qoe1!nK7:!0OW'SPm6P8T=k5Lc3m%CB"<YOIdGNQ&L<k)Q;Zs)iC@RQKOm$t`hGZFR[jpRZB@ahM)>N10~>endstream
endobj
31 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 236
>>
stream
Gatmt:CDb>&B4,8.Gh<B2FghSknXs9?%TaYVpmEM2:Ld;GAi_W6jF5X0JNO`mkFs3K8+b+J[N=79-=`l""C3Wp=L4%'D0sW=p>[r0d?!X5B;VLGV$t3J@gt22pr&=06N*5,$=G`bdsl\1;ekg&8i#G2/1f&'tPkdZb/noeK!]YZfodANdpgXSd\u\U6#o?VY?jfgY>gk2Y&sjc57Wh0_HB24(H?h-6p9SE"HqeRs,#~>endstream
endobj
32 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 1445
>>
stream
Gaua?>E@c%'Z]+o;]J5+`Dk+$NcUtlW[dD7&(=C"b9IO5&ehR7gE`X(6@AO]%`:PE@#X/`KbPkm*WL/-ro[_nT(jFN9e1bOVVp;S5T$@+5f=g"]n[-IS#744+%]oh5Y7>.'HtMX4=''GIuB0In16<+!fZ5GpC7<!jO5(]LH#XV_)NV>![kej&IW_AN;hhD"[%Nd\nP1b^5(U8MR>9R[kRa^diTjP8+Cj4K*:?kLr;R>)$:<b#bsZ/F&;'h0bHD)2@^Hkhk_rB>IRQK%e&"aI:X!JGm#oh&"f;F__;?G`hB8ZcY5>hJG"=<o"8)A^-)J2%!]?Dc[,M`_M64km[Xlng,DgAnu/M@VW5HN/V>YQ_)T[fVm(eBf2Q/.CsB)S1mktp1t$eL59<\3V7>,C$'W.]MWM?ng!"0`4)*U<6(lDJ(e0WB9"%cbBiFcKe^P@p?T)s,>7KT=S5\s0QY[)]a&S6crn3O4oqM]k1$:k@ldN"MH-t+Circ?UY?>!`BE_(,k9'o/7/7rfbPBS%nbM>FC8^)Yrd-8NN4Vp+(3EPs=KLde)G^OA+l:L63_nb(F/!bZPZc%ANNENHA(Q"dfu#$F.pNGM].n\B1i)AsUD=pL4FD#))+Y3-\YH;<9u^R"%/FK0Rbd8m/VqW];oe[5X'mp7=>iRe9XV@RP#nJNFEKdPlI%^O`FTYpR3n=\f`olfpAG)qfYj6AG2CW+*I(Bq[Crq9<+$me/5!?O(HZK:+ng?5`1e[T(\$$f_&;l<iIYG(^4LXZAZW6$=(!4nC=m$.`<iE_1n5==l_%G9FN#24f/A2'A!8Ds]tcJ%.N+Nm>>-U"3X4^jbhIAJ_Es@=o;9agk9GgDV5X(A<S*P\%R':!8>AP<4Gc#"Hl6d2P/t/#>d<Pil\EKVi<jGcY>:,SHmLoQfS<G=5c7hqIEi/2e%Lrdp"=cR6<K&N/_'SsAD,!:;(-H(@=>[!Ccl]N8!WQN9[?s@1rH$i`?#NJ<2(00r5?5\Y#1o8)-;DO\<B*CB\]:eRG.<7!oq#[ason'^c1J2[i3MCDt$;*Vm.4.Nr%d@Dh=2:0Yl1hM`g8bJ)'UB(o5kHg#g*qV&^B6qYRIKj1kt?'V9fFk[hR0I(Y#!!H/fd$T[6&](Li"T3hcJgm+cB>"h6SNr=cP%/O50.NMoofmI4u(ulKo?!:gRaha,r4l)!\N.G6]"_-rQM>j8\^(L$m9'Q3**6"%Sc6[ee(m*dt5cZeM,5kT!r,6E:]IPb!(YR(,2M*Ct^YJ[Ie+T4;$3c8@i2r/r;peY0@3fcE4st[k/Pee\0PrugGot^OU?J*kgC6=+p9Q9!E5+L4rctD[2?-l2l;5<@c)T&kAs`>-R"t#$(R`khI\eIoV^0g!Wd#hjbC#nl>VYU+du*fR7;*AnZ1@k/UiG09epd@ebV6[,29j$8>F8^rr&Fu:qu\AKLEl~>endstream
endobj
33 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 224
>>
stream
Gaua84U]+\&;KrWMK`kWdTlC'iZRIG,%U&;-l"f,i9#O`g8]s.kYR@n,Bq6m:IkbKK=\^6J5nq@+]+#;";tp8[TFns&bJ2"K!Z4tKF_7$YDD@d>>e%Y]98]T_*GG?E4DJB1LJ&DATGZuNR0+R/'+a'b3q@=)/7[eFrWkA(I+,dg`H3es1T2Q7An=d6[e9%b7T2=s6DU#iKMSX#YRCMnG@\Y*jkKjH2~>endstream
endobj
34 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 929
>>
stream
Gatn%gN)"%&;KY!MCho*e$13%nu7CY1R!!FBp1CD?0R%E,&msu,E!PanEsT:i0#u0Fm<B8gFV&s=9!$%!l5\T5=[M[r<1o:'I\TE!gGbdP0\*/oPStsA][;0d)_7j#cQbeK]<%$B8;7+$NrGJ"^FlC+r50'7UPk.:"m1pKkKV$!@*j;`[V4GF!D(^@eb&,^k^B+36UJSm6Pb%aihj*=+!/b^r7W%AQ7SdL\`8%3MPGKkl6@P)[)+_1f;g0?_mT#8nGD[?298'#":_HQB2o5[o[UMZIjcOo:1>mYbHHtJ-<!*IHL;cEtpB<LGa7S8$4BMI&Cikl[DLIhq^<na-=p!qN:6"^/)5d@L?<AgE\$CC83otCKaWbVR%Ju"r;0Zm`h.LM``L)/7G8-fDQ+"\u!ia9(u[sO9_k6YY/P&0oc&@LZ=]d]fSqAgp&]a$HisP8Z:R<ENAu*>F#fQ#U].ZAC<T@b%Nc=+Q]CT&?Tj4W\cH#2&EGIgp9^`4lN/=](NEN&-`LjUmA>"Q8,JE@lVHp9?ZH?)8'[t(6FPd.#h!.93=*uhQ8DRj3mAV2V`r(o,^=7G33dtPud-2X^qAGjIJuh.SghK(1DEMD+L\5$9:#eD)b),_`]LeZFdo*hr0DLJ`5NU7;KGHU-"WRA`c%#FB1HSAY.T)[d^lWc2s6]\W3@jm1j0BJOr&O`dLrmPdt2XBkN!N/'T&,RhK[<["<IF`*%q\8)7n*,!I'Qh6k%NmW>[=O%k&TUrVq!$o[A/OX!udDblEA>(_<r+PGc]1;UD32'?pUHp1;NgSo/_?5l#lN,1@:;q?'1oaJI524bLAe>=*g/%Et%*R7_@e;V-E(gIqW`3)2H7rip@@iP.J+WE3`[0VWq-PW8`2(1,PXhk#IlUM;2VTQ\V_9r)#1#0pQkpY'WK+rXH!$riS=T~>endstream
endobj
35 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 1261
>>
stream
GauHL?#SIU'Re<2\FkcN,*PQTqXWcWkcb(<\i*37]i9>T/D=-;M^h/n#n6k.>tL'V+S![Y>[\mQBNk0"n=F\S8V,fX[_LuEJ6*&e^J^^5Op+\ME(Jj$0?Mnf1O>G1,KH^%&47:4`sW3mn);:sNrg*ni%PpeT7R`_R,_]+ndM-^q_7g'%>J9+8/$T[-G\\GB,0nb>e[gFi(p<U>fj)`kE((AjCJGO%t$MFK)7nHPYiMm*?("niWobgNIiN#GR"<%8\0&Q+F_YB^Hm[]6_PCu+?*,4Z.\'6Q3)$d<f3_%VZtL1cNso7)JIGm+CRLNS3['=@"TB&7kOF<c/#J$n>C4U\R*nUq]c'`]gXF,FsW0O:I>WO^kmYn*efP`p(L-a`\e_oh1MY[9NoNVVNOQh%c3()I3n/NQF26+//472.WNh;Kt8"6oXt:rOP'liY]kq7p7QkTf>W`=B0%ic'LX>$oXR-3V&!g.bn%t-bZ)$M@:t+RP6$JP,'<q_GfO70UhZ[!N016:mc6J*TmPN[O\\hJW[O4<n&BhChFtFip(D:^&rrO83YMB%>Ltjl/",:am$f*O9-.7#6C,BO\[\+q\_Z$HA[!P27)%cSpu)O#ncO4[+RWDDaQiYC%Z>0aD[:(c6TQrPb3`bq&g+TWel0*DrjXWfn(C?n*H\$1rH.6S"sB9r?V/S9pQT+J3&leXFGN(9R:GT\<YRPpM&BirZ9fW]1QW0/mA4WSj9G!]qUnC\&08'tq,#6i76XGM7464o%kX*N"f)f[?O=8VQf&9'$!thffFYEu1eQ(gG*iO.X@W`3<TTu.jZoRA%W3rKFs*$4C'p^Z2an2.,$!r<^="7Y)n6"Em,d"4$[S\Z=RnHq?squag2f`i/11(Re7<u\r4?W)M]Ho;phb]oWQ<g`Fk6;gl(lBRSk-tZ1='Pch<=^S!2>m14l9iPrU9UU\BRYBT\jpnb!KcDMHbr7;OT&doBa$A:q>@L\K8/F\7TGGm%pDlqH/->gp+q,/f`3Z7oD'LM1sio$"q!@paci6Zc$QBiJT@"f]*@-"?RAr_?P_B=Ii\)K529XjXl9kHlO=,4Q1&n>6mB!V3CH-hM&D)0&m<QRq$]#ZipV9>"LhbO./MZfYC30*[#;'?\p<M$I*)dj.@WhB1r2/K&,3!nRPYKSRbG(Egn$j)gNIC_ker,>u1CpqV>AR]724EpF//]ULPsc0Bp"JXrnS>qRU/G#'BK0T^!99*mQF=n'V#.&'=:AjmoAu^tCqo$N2=~>endstream
endobj
36 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 1051
>>
stream
Gau1-Df=)Y&B<Vr;]HN*3i"+-j8<V"#U,Rg"DEUK2ZsF?EJku[6K2mRVLEa7BiYQke.P6Q)C9CjhjpBdc3+4HOT%2B!1qNQ5:8]EbSNO3a'elb?Hu?\T9*J,9>--Tj!c5[n\QOopcj/nYr&=L"<OBGM[4Q+3b:9F]0*nia_hud\ABO?d(h1uWEEX,?LnAq)-k[&#qptoZ@iV"P_u*Mr%#u5)2<A13Q/b.I1AS8KMO`D#>*("#B/)d*XCG?@&Un"e=P\0kMMpDpb`=O:B6[,YS9^:>`1Q9U>VJm:nVQO?E[=jNO.*%"_SZu8J%\PoC.8QfaY/].(`-N#lt-"q`q_NpNb6_%49U).#Ukb"sZ7dXcJ+Se(@=RR/@O1o=5fJRrK\q[Q]AO#7YuU=Z(?&iHCP^fYF5`.i9_?f"U-FElk_je`a7AGs^>>/VQ*gI"(\P-egGlPaB)rJe_=N]#OSCCN3[ieW)#\nV%TJGQ-D[.l>7NUZn7;@+odhZG3#T8APo6d=(g2!8I)2nGXCB1+Os1"j_n-)fa9V9o&'EDgBm,*?hrZ7*duEZG#%F/8!8n<iRH^[a1O_09:nc^P>>DOmiErC(]c\96bZUE`fDBDCXnBR:YF=f[83>;!nr)S(Z,p>9K_mZ[G?a5.J4K.2hgoDCr)%GA`f"W\^4kNRaO"^ZJE)ag.k8-ursM>8#CKOCQT<Z`u'e;]D+NU$4-O)a?#1fdX9pY?HWOdScE@#,D1ETOcrVYA.&C21ges+a#0shk<4u]J[4UpQar9c#"T@4q@$^1B1s25MHd5^_ri"2+>?D`m:OM"?'@\+VoH*hQnVo,@_29@C&3:=B$V?ge%"m.[.j>V"W-"aWbr/qP>tA&&^@Gm$QQpGB+W0e>CbNOuhnb6X)##($$\0]!o<bB;:?f-eB$C/tlHEpe'/bge;\\Aj'Rtr?K;=3<SE4o*a6le9GHg.2TD?PKUNgTXpZ3p4VDSS^MuY;iAU.GK7WF4oQ7,++$[e0>BU@RoGpN$/'iI:W/dhg.,T+Y91>35q@Qol2POkE:O+6a9-G~>endstream
endobj
37 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 1454
>>
stream
GauI7D/\/e&BE\k;]La)OLnI)WCM<Y0J$MV%.03$`njAkW^q65<_jGZ00tci^8#5>EYc\8`XN,]Ml*T5h>Qf&(BQM/8G>%l!.J1'F[gmX==f$5V@/<+nC-C\o+B[BF@4M%1OfTp5YNQ0$/UGEj,IUE/\J[q9'*b;)$Ocu?SgNbVh#("V1IqE0P>Liie*[=0WmfKK0P.K.`'E1VL[-qW2l)H![?D0pq:))28#u-Z=F<#O0EjTZ$s[DF(h^868Joo*j1sM[tE$r!5&_gI./br/#bVrmG<02DnM.k:V^'==XdGd-R*&9#$eAV)&N`.+NZG;pmPhEK3Vm*Es5C\0a$/?-0Xq>lsSK8q&*EMDOi=u?Zn2C&o1n;f%/HMi4$Cdh_:>uRDV&QB`nYt\o[dX48bLbL;$IWg4B5E2Q0(JZ"Y9m#Jr);Fr-94bLsnkPA_2g!ikY8=m$[4#=Rc5q$>0@.U^[YP_Kg2([B!PG;Vabb::gCSf]Mob12='"2T%qIE41%Q>scr>eb*TR_uf?-e5,g)LXL34Jq*\U/+(9$BMO2QfI&L>\uUi[ZaP1Q>rB%=LKMV\>gOC',gO@GJ]g;7P(".`68V0dWpSF`u(cK)ZrQ-Q!D,nSE2:6/A7u.kV7$'Y'--1J<M-@4$qu>riL!pCY$9'P\m#d9AfJ^OWg'cWIbsLhq)_m.d#0eBkPWDc,F!>Y,a$bEVY9;)<[Nh);nBJ#%[Y3]e2&`ZLXkKidA-&h\kF,\d_6bVST\blU#7)l'L_LB#lWtfH:hsd=_Uuf;&17Df;DL7%?o(EAV?>YITs7B#E%T@?I8!DX0rKaP^oT$u7CD-Q?CoY/!!#4pLm<M`]BhTp5]BL'MJ#)sF?M6W(R+ifZ/*7P)9i(j-F[6XSE=l1U<5?$:XU/U[qf0p^A3$.p*:L5E>f_,bQRX08pL*rA,:/$l5/16%6PX2#XQ*s#a-/[eimScA?ci*t8!4q:EdeE,Tp@(QGbp)a(&*H8Hn69dOKDrWUt=Yki#hj"ML\kD93o\9!lWtROk7g"6*C[boQ^TA/4ifF\_XE^>SUMn.rbO`<fdVVQQ8.<P.NG(0p`pRiC9+H6uJ;4@H\'6(''\)@)GcQbm8[HE)DHaIAlaZ"@i+\hnZgV0/j#Fa3oQLB,T5fV+<@3lo9!`PoUQ+Af("\j"HT\dO\f\E./:Fm9V>5(XR#'&[#u*_;1t8FkJr*Kmc=(a=EOD14fGY<<%puS$AIZ)]H^L=(3),^uo`@Y>$1c#WSS(MF'@$BpX)\9&.Wh7GSR,C^h&LmX](j8*U;;N*Q?RA&Wk.O]02m$ULF4D,0q,+=])!,Bd"3[r-VpRZ6E0^"3MAj5Lq:aAXX7ZQ8Ssh@1Yi5:X]$2ed`^Ua&OU+Ki1uAX2&DEPT1!jXT?%k3Wj2?qoJ;qns$;,YT"^)[O>+W'-dTObAbDaaBPb7UXbNUc!.8^k0)~>endstream
endobj
38 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 1416
>>
stream
Gau`TqeP4]&H4hB`Pc#6;^t5JH<>@V(327YEjXGV>7,KrZF.,`dAs<J*[gm#GP$\u8]^%]j"M=lhUR"Bq<"07AROJP2;e!.i-YT=(CV.9aT@i>3\Yga5>R==4(@#<a#@9^oS,%:0[h(F%/8`<\<1X"KtA#V]\\!Y_1>h`dnIgVpqR&so7>pW0Sb&>THUk!rhTsBn4)nKVSknM'6oumH'X=e_%`6RT>//Jr-J25.82qBjP(U#+8C*gQ*ZD6H(4\]SJlGVPr[*Iq_e$F#GFbXeojRQ6):39;7_E3=&G8!+B"Sl9&ZTN.bc^agN@71:G:Y\"ULT;k2NQko;mN3+FLip]`Zp^*k]I/KAXJQ)VD?L4md%CZFh3a:HZ7;jMo(.^nC]pA$-nC2_^;FUA-<2`4/n0_-@iOL>?YFrpaPrhN3qPWog.B8>Go^*1L6pbTt9-rFs$[r=1$1m+q:J`Z7CE1b]DeZ?XV+PCd&\3<BYU!lhGYlM]jO-]jq_d,EYtfaT9=nPA2=Il1'4F=a#TI]m2,p5$Tt'pg*[F!X-]6d]3=hCo,Qj)I^-*Ft[iNJ.!VT/rZMOaGcg4/AY0[C@;[NN:YLL4,!HS[7s**6k\-'ahm>+rme<U9736S?tMOBYa3&\60)[\];['^Jc^)s1!/M%&5#6`?H:'%j'+!?k\FZVR0<(\&P/$@o2/[)6m;:L)LQCX@1Bf7_I4N`p"m5b9StO"pZH,[#!ui)$-Iq+q@Ro,9XT1EY2rn`[V<_Tp&<3:/0V^iWVP4YJ[r9_7dZ:m/0IYAC:b&+t-sOHPr8.UuV/^=*(k/nl"VT0O2fLJE,*5I(%h!f("D`a6h:7aK0.-N:pOdCUqA0D$Xm&jB7]s+a'=7cmeP^C9?;P3h3@DXWC'J<WU5)iX9kX0AEmi?UA7L_5t`;;@2]:YQbs\Ct"%$d*VLNBQtZ$@4?ik"slTgP;j8>"f>,j0c$DhDPL%17WA$tg:$J9Dm!KMcWT!K+2Q5D(o(["U!lc=G/WP(/aC0r\LG(I=PUa5Z;As.hO"Ul.n$C%LYBtG#)^lK\r1ZmV92=s(mW2aY("n2V]Ca<i`^9PLMtQPr\MNTqrHNroO-/GFc&##&-<mHn,XQupTe/DZqHtkYG2;GkYt\8kIF=-`Qo,VcKI]?B.hbe?r?3j`X)e>krOF(<g;94Z%%'#@1B@\cfA8u`9?N(b66If*NXiLN(&_#a(^5"G<;?[EH7?(XK7&0YWVC<F>p7=gIDdPRmt&/<tY.mq5]O3).@6^E;V8"8Yf=6\rQsW\bHjj:"/G0%V1nu,fdTN3rI.MB,9U6W(%3-l.g_G&\E>AreeBT2`Hpcki.aiC(0C%/7cr1V8Vf*Tj=B$8"XSQ+Dk%Fmp(N3HEE39P,TR[7o1YILhl`"CFG9[aeP%C3;O_?<*"8~>endstream
endobj
39 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 226
>>
stream
Gatmt57>=^&B4BkMDqsRBs,^qU,WF%EY1(<.8#;G3k5"A]sK75'fq##*$P'gpKe,_Mu%MP!NUaF+]+#7"DOhI2HU3((%dsE?5fh\%Hf6E3CV0^1P>-&L)KGF.`+VQ_8gDU8i"U^ZH@G%)6_oVG!`XY&^JITbKuT($aZeEjVh(,op>OTh.+GgQ3N,t/q6P+1a[M'6[PFH=B!)Po*L#F9.'H2^CX3'8:i#~>endstream
endobj
40 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 1340
>>
stream
Gaua?gMZ%0&;KZF'XPM%llqT]ME3%XJ[S+"!^Y2PHEX?CO=f267]3)]`.m5pmmp&u^tK7*@9OL%+Go1ZEB$>b&ciAiV#?os!5D9:Hp`E\R*:+8PJQ;qXs[jhccC0#k*lheg]0HtR'pW>m"^Y,,MA*2]ec=gkrZVQ5cnWTPPHj(*&&i7Qp\XtBY[G8/7A6E7O<9qE*G_MOtY,>>)5M\%rbT@j//dHGQa3`0_qt^q1udq4NuL=0aT-9hDL.*Gs4Sq]_#K/4%*WhC--"-RrSTo]]2Y!I]6_Dn5r"WnlZU%*2^B3;',<K*Xfp`r4@O3P#iG,FLGcWY1qAFDd]heK<Ic^M>n46L@O0==c(fk.a)hcL!u::&jgC>T,r7"AW]@OKF7s$2diLo'B^XMlgGajL)i!_(O$hr2XFH`G.oL8+X-">W(nY_l[1#mUjZ!rE#=UCXha:+g5lH([E'L2.`a[=T'VTE]-4WjVnld9;_'ecP=M":;,uF@)8@)#dF*7pkV@j.Bgf?[TNi?oHFYlbT^2-S![jc3bSfM6L36phF]rc9T4l6(<A-c*^GFl_iPTbG/4?j#W+CNn2AmRRP@9OLJ.e^uFH!$\qNdsPJ,Q9ZR_K,e83!uod9gK$Pur(E^X"(>_Rk,K;Q9o@J!X>`i2`o$Z8l/\*-,sD("bWsq>ZsC#7:!-$gBXb[QQ,\KqjWZ,l<(4f<P#1mW[n/=(B<)'a-+g9i,8HR3.e+p"Ya$2fnA'"'q4F2o]]I3'&-?D5nioX-]qu=Q?Y?9,!o?&)E4aUu4\/6VS@S:JZf6)S@rCK,ru;@;VG"^VWI.2RZOJWJu'\<3SoP=Sc\LeT2;7ipq5'4)jqYhcBE>$WobHJ'lO>ZD4p[egbQ5XN)NA04CX<*c57k'(p4)[d.^ebBpfjgL7ZRn&[bj`-8DNEaIhTQkjBEm4H.a"78ho;Pl+fY6?KH%0R:r/'*n=K7Z59Qfp*CgHAE+4e]IKbAW]G2-m^?OPu?CR\4eI&nJYTU'9Eo3hoSu)DofI(=QGFYJ<U+>FNbWY1E!+Tl?T$?XMsa"VSLa=c5&sD'7T;52@TL[rpAg-uG^5UI(rX0CSoP`KYEE7@>O9ISp`$oOiT]@-[>rg,$L1YGB\"(4RW;8VoWHTi),/@u5]hkn)0hH*5=jk(PYSNJZFN94hUP7(=[9$I4dDNen"-I@AjB20`mBPH8n>[n6Q!F&?I%aK_\a.:0&78`Gh5=D^hQlR=3,=31*AX6M&k6>^R54\QNoJj%feg,*rAoaK^h\5;(SK.bVp1*85IlrsSB*nEV!#N_.te@2P0euQr[>OA][j-^lk]6k^HZt"`pIfRh8c78~>endstream
endobj
41 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 345
>>
stream
Gatn`9hWAX(^KPW5K07d3h^D^2LRsU6."'lKcG<9?.RR!c)r!lr9gGQMTCGj66IF)cguLH&=@2hpI,2>$2>#6$6Zc"mW"$tmY1=GdFUs]H4a_.L*R5A0O^d:e@%jJ7;CAJQmF2:2bha<6YWNihn^eGb1nBi6okut!h?0kY0g9j6n(o;]92I^iYCHfZCb./jN#q,rrR5drBpNj+e5$iF8Z#"hJ27OA6N>?m2il*DO7eMUT!e%iA#93dN7NIkk<&jYGE0oL1YNf?L"]Gg\US`1A4p=%`]3!Se'IrNRYar`;!D.YGH_q$B5k+WD;/rS7T]HJ%`AC!7T"+q!j,loa0H1l$<~>endstream
endobj
42 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 1049
>>
stream
Gatm;CN%ZU'`GaQEN/_pU!8RfGL;'H$jd4ONrgCOdR&97L*^j&\D(EVJ*5tAG!dddO;03B'$'/TT4@beK1d#!mo]tdHjmno*4UOh%(OW2(r(XNn\N0Fmp#WC$[<:2&4N+7OiuuRqTSjs'N"j(Lr^)dYXBa#Fee((e0j.j:rakJ#S:kU+[cFjI+%6liXriJVpb^N;MBl[rX%U4%JC%<m_edO>PND-r!%h`gflWf']XcMoH3+e)oXuqV8kP@L>_E;iDQDuONArg.bE&<B_ZbA31!OCBr_'^S%]n>Gd5+hf5O19Z%>VJ=/aD%KD2<VQt^75KL3uIGr:(^+O>+3*/:T.q]!'E[_]ZRj%d@&7tUC%FU4L@A]&OM#)m(dW"ltJJ;0cn#YI^;"RfZhNZkS+8`<>Y;cErn_Sg)ia+M&t_Ka[_P-ii=gEMK!+!hHW2QUecUqBr)o'Ze>iO/Z@ohngJ_A[#YQ8#9$jfh3CmL5[*0TTJn`:U5ZDV;F[q/::^pqX*<_A[d6DV<?:<Mjh?_(I8PViUi@[]%<AJH.@3`c#tNmi)@N@4u^?JSM-+8jd\%02mk3P#i<0EbOGW[hl?)Lb+Y81,Epq^-03p]jW5PKq,<\5>3Fq6>L[dpeA1]X+Dp%]r9?lf4RQ_'-uSp)%Rp<)<tI+RgbkCM,^Okd*WuC(Te,e[_S,ngR?Y`1^l>Hp/3cX543h]ep3+),6N2:?A^p\79eom\NR.S\=7@_lV&GfXb16b]CnRj:p3j+9r.m%T@cfog'#_"pD!9FHVs"9Tl-aUI?r2nSf4^(IV:A:AZE'?WY'Ut%+AcO*&WZ.PJIh>L.bY`S*05Ee&;"J-"UqV%8X3ilG!4/nT`g(PYb_*mY^).nohaE)Wpes=6(3uB*R_XKZ-7Dm:GH,dC+TT40^qiCl3AlSN/#ZMVsJaCIj?bG6NAZV@"5OJVr76dI%`L_Ys3)YS&T]//.t+OdJ%LGIFM/Q&FII!*USnk.]TYRMFP,@)so5I'LKR]KImS'l7s>WI]<B)nFD?'pZK=^N6LU4u'jh++1G.Mu~>endstream
endobj
43 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 1256
>>
stream
Gatm;gN)%,&:O:SD"CQ$P<oCsca967R\Y'RESOa>6gHp$e4P<=Ji!`_n_[tF9!hck%Q!827Ze)<SAh(P,mTK;]_TinJ<r'Z^>Zj2ba-$H(;T8g]Uk[!TC@8>OKahE;$q<eR3-dG`mJ?5,a!mM?p_dtaFU03%Ho]^F[Gi`h)'66S>Hi?4?%s*DM$!R6J'b/3)`q/69$X,>Bh<6/iT`$_X%VA5<'4(dd'(>6$Nfk$Qd`!`Zm-3!Wkc>PNWL<TERYe]p<F9jC<`k=OoO>WmGde/1M%95U^["/\7+l<F?CoK9q&N)q<1=JFl!j6r='WrW"!^!T2*fr0PBVQ";raQ5?jhTFY(fnh/ksT"3SnD\;UDLC%U4rr-@2^Iu8mU1rTB0#7\I8\'ENfu"7HQ-#mkLH]J,jF1j$g*3lRSae^D5h,0Pm4';2'FmZ%PR-DA<0"YKLlsCKYuPthitu)9cp'/>T>A0Bgq`%4]#Mlmh(pHOpKLoeKcQUX10`K6pqr>M,*)NiP=c;cUZ'fpJp#2=l,3eboG'nE?b%&+glGQ*DUFMMLb<'LjKD`!4'h;Gn(*&D`Bi2*(jRn;2YU>e-@RW70MT00Z!\YV&4j[le;m$sY?$J&!QbgCcCXP&bN.si,SZjBY84B(>^rh^#R8$^P[q2bEeWCa(KiP7>1,sIMU2$B`;j*J8QL9W4KutGo@XA--EC]dnjZDIQaMOD_X4(L>u;Fu\ASC2UanK`qk94V%KYTUG?A(JcGm(4B:Gs&*MYCgCaCY?'MHO5X]=Hu>CtpX"6bj'U*T):'9M#Cf1.&aeao=)s*8HuU"r0>>"[hVU=iu+m61_I[+Y<Q!U:%jC'$^TkU-s4?,.j>c6SOn19@Fd1:U-b1Y\$OG*BqZ/,[S-X_%PH;Rsicro3d#!%EgR]Y>Ts86L^ua#]('i2tFsQ?hd)GO;3jrET6(FePu&;fjR/-mc$.f0t_F9a2kuPii;!"1:bD3s7Z6N=V]5/FuXHW(FmHD/f=BPJ-uZBeTD4rZ(U_m>:eOW>g(%q(Os0@`WiC:X>df,51n\.6gD8JB.Ap1K@HXN8,dlKq@2:Du!8W"Ur?q]'#JVHfA(QIi#IU$Y^.M>Z4k<cp(uGg7,Os!*'KD*^h5?44VJ'HG$5=lt][)Z>!qOs*2k_I5AnB-0M/g)t#m.8i1l96d47#aiK9f_mubs#!RF-k*"5m1_k7qg+'dVrFWCrFRA7QUiLBaL`\(>$^]4`;fhAHL%++"oXk-sJWha$BqKG~>endstream
endobj
44 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 1552
>>
stream
Gb!#\gN)%,&:Ml+D"Z.'2i?PLb&ifd:7APmlc*Z\;YL29<t]$V:]XD$gZcIsTpleON1#eUm:aL,OUL-<_n@%WJPRArpmY=bY6N@#_ghnE(]fQ-#R6(RHLTk=#Ab+mAR<USP!)?7k(Te\BH975Kl$?b[(:i3E,0f=!XO2JCL35O5oP@l"DJQ+nFhIILc3[kkROu"LF&CG;HCjnHjalYEqTbC;]%W?qu99\gFu%c(']g'/>g$#9DgW)BIEYF-+(Rfn0/+`!Fu+lV?>QNJ67iZ==ai5A$$#4KsKS;7-Ju#Ot8lR*oKPX!(,A;6PIUUJMLH&m(C@NC6U`melq<%N6p]#&-er9:uFl8/38!-:9n7He!DN(Xt-EjBMh*&$Bu<H<R$c(Wu(7;/Tb`[X%/`D0N<+XS4g$h7`qsS7?aq^^&E/icGK!jkp4d"$^bWc6Z"L8iJ_PYR>7G):s1MR2[HmTh2Apo&lonYItK0-+8??@3>WHs#bT"KpBWFR2`8D1Db_C^^rVlZ#AMU7Dk+e9J=Yg#=VZ(9M_#ktP-PTOHhMK7VM1IY&oIg)8^72?k%?A:Y_M3YM(pHV,k+]Ndtc5lhW%?QId_MEUN=$GUBS5rH!-YkbrQ(!-=BmqpJ4QB*;,^'.F>L'kD.f=mj-_>Mt1,oJr5s8K*M;f;_!*2n<lRVKGAQU$Md"r1W,A:H&F/5p&^:dbs`["ankh9idH=S%`X;L$]uO=5Scdm47AJX8m8>h7+\[Y9c+>Zl8lD^-HVQ"9\k&DO-FtuGVN.N;B3/`&m<[QbV4!;BsX7WZ3f/`c4bOr1OG7?PR@6)%QW-b@go\W)o>@]IT0i"$>r./:+QK66HLl\D*;YeS9A/#OqPFa\5Yh%)D=#RL,c(gRsZiA`nY-D)i;Is->2[8N=q"<jFpGD2[@;#-=Bp,Ee)&Rr61%dC#N%.0_:Ga%@WC_aX5pZ:D^*+*F"rCokS4F:I3AHl=*UL>QEQ*L!/5Tm)W_K<mTA+],#N8,VQ@'lbZmU%0ILi-b1!KgDe_</-)4a1+cJpZ`d0q!9XH\8ooXG&__<&cbT,1Fe4"@5m5fH1`CC6A><BcW*=3LpAFq&8*YPXRbphgLJ"+75Lf0ITC_0fD#G7SWl)#8dJ;J]m.)GaY>jQ2m`KckB.LA8Z5[o&Z"SG&Dqp(U*,REHf-f/1Y=!6QONnZ,D%db$F`(kd/SmRj9llmQ41WT6Wd&P>&L7264nN.>l>lj2NTgkgjM0V@Fedd_BF_spm\]ITpqELNB,0W6\u..L1RaqbfPFV+<SfcEf)bdB\&2]8qW6Yk%km??`mVfKBWO$8UUd`AfX5+uV^tR$b15:u^=UC;2&_<VS;)#SrkD:?\_.37f$XQaocu,W)NTW!D\MnkdSY&2S6k0BR,0la2Klo^nB^^1X&/@**`[Rq*^Otq4FV<%VDhWd>qO.7XK6$9DtGR%WQHS\NJ\?#c!UVhb7\Be6R?7A4VRXa3?J?$TjFWpTl\G#6/D/;$!"g;e\@40oT<FIZ24NKKNPd[]&gI+21l8'D<XG+Zj(s_&0C`WCP&BZ~>endstream
endobj
xref
0 45
0000000000 65535 f
0000000061 00000 n
0000000123 00000 n
0000000230 00000 n
0000000342 00000 n
0000000547 00000 n
0000000752 00000 n
0000000857 00000 n
0000001062 00000 n
0000001267 00000 n
0000001472 00000 n
0000001678 00000 n
0000001884 00000 n
0000002090 00000 n
0000002296 00000 n
0000002502 00000 n
0000002708 00000 n
0000002914 00000 n
0000003120 00000 n
0000003326 00000 n
0000003532 00000 n
0000003738 00000 n
0000003944 00000 n
0000004028 00000 n
0000004234 00000 n
0000004304 00000 n
0000004617 00000 n
0000004796 00000 n
0000005329 00000 n
0000006076 00000 n
0000006897 00000 n
0000008329 00000 n
0000008656 00000 n
0000010193 00000 n
0000010508 00000 n
0000011528 00000 n
0000012881 00000 n
0000014024 00000 n
0000015570 00000 n
0000017078 00000 n
0000017395 00000 n
0000018827 00000 n
0000019263 00000 n
0000020404 00000 n
0000021752 00000 n
trailer
<<
/ID
[<4ba4cdaa716430c5e4593eab67d6901b><4ba4cdaa716430c5e4593eab67d6901b>]
% ReportLab generated PDF document -- digest (opensource)
/Info 25 0 R
/Root 24 0 R
/Size 45
>>
startxref
23396
%%EOF

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,744 @@
# 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!"

Binary file not shown.

View File

@@ -0,0 +1,336 @@
# QuickPoll Next.js Demo - Complete File Index
## 📍 START HERE
1. **For Tim (Teacher)**: Read `QUICK-START.md` first
2. **For Understanding the Project**: Read `README.md`
3. **For Complete Teaching Guide**: Read `TEACHER-GUIDE.md`
4. **For Technical Details**: Read `PROJECT-SUMMARY.md`
---
## 📦 Project Files
### Complete Project with Git History
```
quickpoll-demo-complete.zip ~160 KB
└─ Full project with all 10 git tags
Extract this and use git checkout to jump between steps
```
### Individual Step Archives
```
stap-0.zip ~32 KB Project setup + types + data
stap-1.zip ~32 KB Layout & Navigation
stap-2.zip ~33 KB Homepage with poll cards
stap-3.zip ~34 KB API routes (GET/POST)
stap-4.zip ~37 KB Vote API + demo form ⭐
stap-5.zip ~39 KB Poll detail page
stap-6.zip ~40 KB VoteForm component
stap-7.zip ~43 KB Error handling pages
stap-8.zip ~43 KB Middleware
bonus.zip ~44 KB Create poll page
```
**Total Project Size**: ~588 KB (all files)
---
## 📚 Documentation
### Quick Start (3 pages, 7.1 KB)
File: `QUICK-START.md`
- For Tim before class
- Setup instructions
- Key demo points
- Troubleshooting
- Recommended read time: 10 minutes
### Main README (3 pages, 7.2 KB)
File: `README.md`
- Project overview
- Getting started
- Key features
- Technology stack
- Common questions
### Complete Teacher Guide (5 pages, 8.5 KB)
File: `TEACHER-GUIDE.md`
- Complete lesson breakdown
- What to show in each step
- DevTools demonstration strategy
- Key concepts to teach
- Tips for live coding
### Project Summary (4 pages, 7.9 KB)
File: `PROJECT-SUMMARY.md`
- Technical architecture
- Git tag progression
- Teaching points
- Sample data
- Build information
---
## 🎓 Learning Path
### Beginner Path (Individual Zips)
If you want to start from scratch for each step:
```
1. Extract stap-0.zip
2. npm install && npm run dev
3. Do the live-coding
4. Extract stap-1.zip (new terminal)
5. Repeat...
```
**Pros**: Each step is fresh, you can code along
**Cons**: More setup, can't jump back easily
### Advanced Path (Complete Project)
If you want flexible jumping between steps:
```
1. Extract quickpoll-demo-complete.zip
2. npm install
3. npm run dev
4. git checkout stap-0 (or any step)
5. Browser auto-refreshes
6. git checkout stap-5 (jump forward)
```
**Pros**: One setup, jump anywhere, see git history
**Cons**: Less live-coding from scratch
**Recommended**: Use Complete Project with git checkout
---
## 📋 File Structure in Zips
Each zip contains a Next.js project with this structure:
```
quickpoll-demo/
├── src/
│ ├── app/ # Next.js App Router
│ │ ├── page.tsx # Homepage
│ │ ├── layout.tsx # Root layout with navbar
│ │ ├── globals.css # Tailwind imports
│ │ ├── loading.tsx # Loading skeleton
│ │ ├── error.tsx # Error boundary
│ │ ├── not-found.tsx # 404 page
│ │ ├── api/
│ │ │ └── polls/
│ │ │ ├── route.ts # GET all, POST create
│ │ │ └── [id]/
│ │ │ ├── route.ts # GET single poll
│ │ │ ├── vote/route.ts # POST vote ⭐
│ │ │ └── not-found.tsx
│ │ ├── poll/[id]/
│ │ │ ├── page.tsx # Dynamic detail page
│ │ │ └── not-found.tsx
│ │ └── create/
│ │ └── page.tsx # Create poll form (bonus)
│ ├── components/
│ │ └── VoteForm.tsx # Voting component
│ ├── lib/
│ │ └── data.ts # Mock data & functions
│ ├── types/
│ │ └── index.ts # TypeScript interfaces
│ └── middleware.ts # Request logging
├── .cursorrules # Code style guidelines
├── package.json
├── tsconfig.json
├── tailwind.config.ts
└── .gitignore
```
---
## ⭐ Key Files to Show Students
### Data & Types
- `src/types/index.ts` - Poll interface, VoteBody interface
- `src/lib/data.ts` - Sample polls, getPolls(), votePoll()
### API Routes
- `src/app/api/polls/route.ts` - GET all, POST create
- `src/app/api/polls/[id]/route.ts` - GET single
- `src/app/api/polls/[id]/vote/route.ts` - POST vote (Key demo point!)
### Pages
- `src/app/page.tsx` - Homepage with poll cards
- `src/app/poll/[id]/page.tsx` - Poll detail page with voting
### Components
- `src/components/VoteForm.tsx` - Client component with voting logic
### Layout & Styling
- `src/app/layout.tsx` - Navbar and global structure
- `src/app/globals.css` - Tailwind imports (minimal)
---
## 🔧 System Requirements
- **Node.js**: 18 or higher
- **npm**: 9 or higher
- **Disk Space**: ~500 MB for node_modules (generated during npm install)
- **Browser**: Any modern browser with DevTools (for demos)
- **Projector**: Recommended for classroom (test beforehand!)
---
## 🚀 Quick Commands Reference
### Setup
```bash
unzip quickpoll-demo-complete.zip
cd quickpoll-demo
npm install
npm run dev
```
### During Class
```bash
git tag -l # List all tags
git checkout stap-4 # Jump to step 4
git diff stap-2 stap-3 # See what changed
git log --oneline # Show commits
```
### Build & Test
```bash
npm run build # Production build
npm run dev -- -p 3001 # Different port
```
### Browser
```
http://localhost:3000 # Homepage
http://localhost:3000/poll/1 # Poll detail
http://localhost:3000/create # Create form (bonus)
http://localhost:3000/demo # Demo form (stap-4 only)
```
### DevTools (F12)
- Console: See console.log statements
- Network: See API requests
- Elements: Inspect HTML/CSS
---
## 📊 Project Statistics
| Metric | Value |
|--------|-------|
| Git Commits | 10 |
| Git Tags | 9 (stap-0 through stap-8, plus bonus) |
| TypeScript Files | 11 |
| Components | 2 (VoteForm + pages) |
| API Routes | 4 |
| Total Lines of Code | ~1,200 |
| Build Time | ~1.3 seconds |
| Estimated Teaching Time | 60-75 minutes |
---
## ✅ Verification Checklist
Before teaching, verify:
- [ ] All 10 zip files present and readable
- [ ] quickpoll-demo-complete.zip contains .git folder
- [ ] All 4 markdown documentation files present
- [ ] npm version 9+ installed
- [ ] Node 18+ installed
- [ ] Can extract and npm install without errors
- [ ] npm run dev starts without errors
- [ ] Homepage loads at http://localhost:3000
- [ ] All git tags present (`git tag -l`)
- [ ] Can checkout different tags without errors
- [ ] DevTools work correctly (F12)
- [ ] Network tab shows API requests
- [ ] Console.log appears in DevTools console
---
## 🎯 Recommended Usage
### For First-Time Teachers
1. Read QUICK-START.md (10 min)
2. Follow setup instructions
3. Test each step beforehand
4. Use git checkout to jump between steps
5. Use projector for display
### For Experienced Teachers
1. Extract complete project
2. Review TEACHER-GUIDE.md for key points
3. Customize as needed
4. Focus on stap-4 demo form
### For Students (After Class)
1. Extract any zip file
2. Run npm install && npm run dev
3. Explore the code
4. Try modifying things
5. See changes in real-time
---
## 🆘 Common Issues
| Problem | Solution |
|---------|----------|
| "npm: command not found" | Install Node.js from nodejs.org |
| "Port 3000 in use" | Use `npm run dev -- -p 3001` |
| Git tags not showing | You extracted individual zip, not complete |
| Changes not showing | Hard refresh browser: Ctrl+Shift+R |
| node_modules missing | Run `npm install` |
| TypeScript errors | All should build successfully |
---
## 📞 File Organization
All files are in one directory:
```
/demo-output/
├── (4 markdown files - documentation)
├── (10 zip files - project archives)
└── (this index file)
```
For best organization, create this structure:
```
QuickPoll-Demo/
├── Zips/
│ ├── stap-0.zip through stap-8.zip
│ ├── bonus.zip
│ └── quickpoll-demo-complete.zip
├── Documentation/
│ ├── QUICK-START.md
│ ├── TEACHER-GUIDE.md
│ ├── PROJECT-SUMMARY.md
│ ├── README.md
│ └── INDEX.md
└── Projects/
└── (extract zips here as needed)
```
---
## 🎉 You're Ready!
Everything is prepared for live-coding demonstrations. Start with QUICK-START.md and you'll have a successful classroom session.
Good luck teaching Next.js! 🚀
---
**Created**: March 17, 2026
**Files**: 15 total (4 docs + 11 zips)
**Total Size**: 588 KB
**Format**: Standard zip archives + Markdown
**Compatibility**: All platforms (Windows, Mac, Linux)

View File

@@ -0,0 +1,304 @@
# QuickPoll Demo - Quick Start Guide
## For Tim - Before Your Class
### 1⃣ Prepare Your Setup (30 minutes before class)
```bash
# Extract the complete project
unzip quickpoll-demo-complete.zip
# Enter directory
cd quickpoll-demo
# Install dependencies
npm install
# Test that it runs
npm run dev
```
Should see:
```
- ready started server on 0.0.0.0:3000, url: http://localhost:3000
```
### 2⃣ Check All Git Tags Are Present
```bash
git tag -l
```
You should see:
```
bonus
stap-0
stap-1
stap-2
stap-3
stap-4
stap-5
stap-6
stap-7
stap-8
```
### 3⃣ Test Jumping Between Steps
```bash
# Go to step 2
git checkout stap-2
# Refresh browser - should show just poll cards
# Look at src/app/page.tsx - should be simple
# Go to final version
git checkout bonus
# Refresh browser - should show full app with "Nieuwe Poll" link
# Look at src/app/create/page.tsx - should exist
```
### 4⃣ Check Your Display Setup
- Open browser with http://localhost:3000
- Test font size on projector (should be easily readable)
- Test network tab in DevTools (for stap-4 demo)
- Test VS Code has good contrast
---
## During Your Class
### Starting the Session
```bash
# Make sure you're at the beginning
git checkout stap-0
npm run dev
```
Then open http://localhost:3000 in your browser.
### Jumping Between Steps
When you want to show a new step:
```bash
# Stop the dev server (Ctrl+C)
git checkout stap-3
# Restart dev server
npm run dev
# Browser should hot-reload automatically
# If not, refresh manually (Ctrl+R or Cmd+R)
```
### Key Demo Points
#### ⭐ Step 4 - The Voting API Demo (Most Important)
1. Navigate to http://localhost:3000/demo
2. Open DevTools (F12)
3. Go to Console tab - show logging
4. Go to Network tab
5. Fill the form with:
- Poll ID: `1`
- Option Index: `0`
6. Click "Stem" button
7. Watch the Network tab to show POST request
8. Show the JSON response
9. Show console logs
This is your most visual demonstration!
#### Step 1 - Layout
Show the navbar:
- Point out the "QuickPoll" branding (purple)
- Show the links being added
#### Step 2 - Homepage
Show poll cards appearing:
- Click one to navigate to detail page
- Show responsive grid
#### Step 3 - API Routes
Show file structure in VS Code:
- `src/app/api/polls/route.ts` - GET all, POST create
- `src/app/api/polls/[id]/route.ts` - GET single
- Point out `params: Promise<{ id: string }>` - Next.js 15 pattern
#### Step 5-6 - Poll Detail & Voting
Show the full flow:
1. Homepage → Click a poll
2. Vote on the poll
3. See results appear
4. Show purple gradient progress bars
#### Step 7-8 - Error Handling & Middleware
Show what happens when things break:
- Try navigating to non-existent poll (404 page)
- Point out loading skeleton briefly
- Show middleware logging in terminal
### Stopping the Dev Server
```bash
Ctrl+C
```
---
## Troubleshooting During Class
| Problem | Solution |
|---------|----------|
| Port 3000 in use | `npm run dev -- -p 3001` |
| Browser shows old code | Hard refresh: `Ctrl+Shift+R` or `Cmd+Shift+R` |
| Git checkout fails | Run `git status` to check for uncommitted changes, then `git stash` |
| DevTools won't show Network tab | Close and reopen DevTools (F12) |
| Terminal shows errors | Try: `npm install` then `npm run dev` again |
| Can't see projector clearly | Zoom in: `Ctrl/Cmd + +` in browser |
---
## File Locations You'll Reference
```
quickpoll-demo/
├── src/types/index.ts ← Show for TypeScript interfaces
├── src/lib/data.ts ← Show for sample data
├── src/app/layout.tsx ← Show for navbar
├── src/app/page.tsx ← Show for homepage
├── src/app/api/polls/ ← Show for API routes
├── src/app/poll/[id]/page.tsx ← Show for dynamic routing
├── src/components/VoteForm.tsx ← Show for client component
├── src/app/create/page.tsx ← Show for form handling
└── src/middleware.ts ← Show for middleware concept
```
---
## Live-Coding Tips
### Before You Type
- Take a screenshot for reference
- Know where you're going
- Have keyboard shortcuts ready
### While You Type
- Type **slowly** - let students follow
- **Talk through** what you're doing
- **Pause** at key lines to explain
- **Ask questions** - make it interactive
### Show Results
- Run frequently
- Show in browser on projector
- Use DevTools to demonstrate
- Let students ask questions
---
## Sample Dialog When Showing Step 4
> "Okay, we have our API route that accepts votes. But how does it work? Let me show you with this demo form."
>
> *Navigate to /demo*
>
> "Here we have a simple form. Let me open DevTools to show what happens behind the scenes."
>
> *Open DevTools, go to Console tab*
>
> "When I click 'Stem', two things happen:
> 1. The browser makes a request to our API
> 2. We get back the updated poll with new vote counts"
>
> *Click "Stem" button*
>
> "Look at the Console - you can see exactly what happened. Now let me show you in the Network tab..."
>
> *Go to Network tab*
>
> "Here's our POST request. The body shows we sent the option index (0), and the response shows the entire poll with updated vote counts."
---
## After Class
### Save Your Progress
The project uses git, so all your changes are tracked. If you made modifications, you can:
```bash
# See what you changed
git status
# Commit your changes
git add .
git commit -m "My live-coding edits"
```
### For Student Assignments
You can:
1. Share the `stap-5.zip` as a starting point
2. Have students add the VoteForm (stap-6 solution)
3. Or start with complete project and have them add features
### Deploy (Optional)
To deploy this app:
```bash
# Sign up at vercel.com
# Install Vercel CLI
npm i -g vercel
# Deploy
vercel
```
---
## Key Concepts to Emphasize
**Server Components** - Default in Next.js, simpler, faster
**Client Components** - Use "use client" only when needed
**API Routes** - Backend code in `/api` folder
**Dynamic Routes** - `[id]` creates flexible routing
**TypeScript** - Catch errors before they happen
**Tailwind CSS** - Write styles in HTML with utility classes
**Fetch API** - How frontend talks to backend
---
## Questions Students Will Ask
**"Why use Next.js instead of just React?"**
> Next.js gives you Server Components, API routes, better file structure, and automatic optimization. React is just the UI library - Next.js is the complete framework.
**"Can we use a database instead of in-memory data?"**
> Yes! Add PostgreSQL or MongoDB. That's another lesson though.
**"How long would this take to build from scratch?"**
> About 1-2 hours for a developer. We're doing it in steps so you can understand each concept.
**"Can we deploy this?"**
> Absolutely - to Vercel in 1 command. That's our bonus if we have time.
---
## You've Got This! 🎉
The project is ready. You have:
- ✅ Complete, working Next.js 15 app
- ✅ Git tags at every step
- ✅ Demo form for showing APIs
- ✅ Clear progression from basic to advanced
- ✅ Purple theme ready for classroom
- ✅ Console logs for debugging
Just follow this guide, and your students will love seeing a real app built step-by-step!

View File

@@ -0,0 +1,278 @@
# QuickPoll Next.js 15 Demo Project
A complete, step-by-step Next.js 15 polling application built for live-coding classroom demonstrations. Includes git tags at each development step for easy navigation during teaching.
## 📦 What's Included
### Zip Files (Individual Steps)
- `stap-0.zip` - Project setup + types + data
- `stap-1.zip` - Layout & Navigation
- `stap-2.zip` - Homepage with poll cards
- `stap-3.zip` - API routes (GET, POST)
- `stap-4.zip` - Vote API + demo form ⭐
- `stap-5.zip` - Poll detail page
- `stap-6.zip` - VoteForm component
- `stap-7.zip` - Error handling & loading states
- `stap-8.zip` - Middleware
- `bonus.zip` - Create poll page
### Complete Project
- `quickpoll-demo-complete.zip` - Full project with complete git history
### Documentation
- `QUICK-START.md` - Instructions for Tim (start here!)
- `TEACHER-GUIDE.md` - Comprehensive teaching guide
- `PROJECT-SUMMARY.md` - Technical overview
- `README.md` - This file
## 🚀 Getting Started
### Step 1: Extract & Setup
```bash
unzip quickpoll-demo-complete.zip
cd quickpoll-demo
npm install
npm run dev
```
### Step 2: Navigate to App
Open http://localhost:3000 in your browser
### Step 3: Jump Between Steps (During Teaching)
```bash
# View all available steps
git tag -l
# Jump to any step
git checkout stap-3
# See what changed in this step
git diff stap-2 stap-3
# Go to final version
git checkout bonus
```
## ✨ Key Features
- **9 Development Steps** - Logical progression from setup to full app
- **API Routes** - Backend routes in `/app/api`
- **Dynamic Routing** - `/poll/[id]` for individual poll pages
- **Real-Time Voting** - Vote and see results update instantly
- **Server Components** - Default Next.js 15 pattern
- **Client Components** - Interactive VoteForm with state
- **Error Handling** - Loading skeletons, error boundaries, 404 pages
- **TypeScript** - Fully typed throughout
- **Tailwind CSS** - Purple-themed responsive design
- **Middleware** - Request logging and timing
- **Demo Form** - Testing interface for API endpoints (stap-4)
## 📚 Documentation
### For Teachers
Start with **QUICK-START.md** - has everything you need before class
### For Understanding the Project
Read **PROJECT-SUMMARY.md** - complete technical overview
### For Teaching Details
See **TEACHER-GUIDE.md** - tips, key concepts, troubleshooting
## 🎯 Typical Class Flow
| Step | Duration | Focus |
|------|----------|-------|
| stap-0 to stap-2 | 15 min | File structure, homepage, cards |
| stap-3 | 10 min | API routes concept |
| stap-4 | 15 min | ⭐ Vote API demo (show DevTools!) |
| stap-5 to stap-6 | 15 min | Dynamic pages, voting interface |
| stap-7 to stap-8 | 10 min | Error handling, middleware |
| bonus | 10 min | Form handling, creating polls |
**Total: ~75 minutes for complete lesson**
## 💡 Key Teaching Moments
### stap-4: The Demo Form
This is your star moment! Show:
1. Demo form at http://localhost:3000/demo
2. Open DevTools Console - show logging
3. Switch to Network tab - show POST request
4. Click "Stem" and watch the request happen
5. Show the JSON response with updated votes
### stap-6: VoteForm Component
Explain:
- Why "use client" is needed (state management)
- How useState tracks selected option
- How fetch() sends vote to API
- How component updates when response comes back
### All Steps
Keep showing the running app - live feedback is key!
## 📋 What's in Each Step
### stap-0: Project Setup
- Create Next.js with TypeScript + Tailwind
- Define Poll interfaces
- Create mock data (3 sample polls)
- Set up folder structure
### stap-1: Layout & Navigation
- Add navbar with branding
- Set up global layout
- Add metadata
### stap-2: Homepage
- Display all polls as cards
- Show poll stats (option count, vote count)
- Add links to individual polls
### stap-3: API Routes
- `GET /api/polls` - fetch all polls
- `GET /api/polls/[id]` - fetch single poll
- `POST /api/polls` - create new poll
### stap-4: Vote API + Demo
- `POST /api/polls/[id]/vote` - record a vote
- Demo form at `/demo` for testing
### stap-5: Poll Detail Page
- Dynamic route `/poll/[id]`
- Dynamic metadata for SEO
- Show poll questions and options
### stap-6: VoteForm Component
- Client component with voting logic
- Real-time results with percentage bars
- Purple gradient progress bars
### stap-7: Error Handling
- Loading skeleton (Suspense)
- Error boundary component
- 404 pages
### stap-8: Middleware
- Request logging
- Timing middleware
- Route matching
### bonus: Create Poll
- Form to create new polls
- Dynamic option inputs
- Form validation
- Navigation after creation
## 🛠 Technology Stack
- **Next.js 15** - React framework
- **TypeScript** - Type-safe JavaScript
- **Tailwind CSS** - Utility-first CSS
- **React Hooks** - useState for interactivity
- **Fetch API** - For API calls
- **Git** - Version control with tags
## 🎨 Design System
**Purple Theme:**
- Primary: `#7c3aed` (purple-500)
- Hover: `#a855f7` (purple-600)
- Active: `#9333ea` (purple-700)
**Neutral:**
- Light backgrounds: Gray-50
- Text: Gray-900
- Borders: Gray-200
- Errors: Red-600
## ✅ Build & Deployment
### Verify Build
```bash
npm run build
```
### Deploy to Vercel
```bash
npm i -g vercel
vercel
```
One command deployment! (You need a Vercel account)
## 📌 Important Notes
### In-Memory Data
This project uses in-memory JavaScript arrays for data. In production, you'd use:
- PostgreSQL
- MongoDB
- Supabase
- Firebase
### No Authentication
This project doesn't have user accounts. Real apps would have:
- User authentication
- User-specific voting history
- Admin panels
### No Database
Each time you restart the app, votes reset. That's fine for demos!
## 🤔 Common Questions
**Q: Is this production-ready?**
A: No - it's designed for learning. Add a database for production.
**Q: Can I modify it for my class?**
A: Yes! Use it as a starting point for assignments.
**Q: How do I extend it?**
A: Add a real database (PostgreSQL with Prisma is popular), add authentication, add user accounts, etc.
**Q: Where can students learn more?**
A: https://nextjs.org/learn - official Next.js tutorial
## 📞 Support
Each zip file is independent and self-contained. You can:
1. Extract any `stap-X.zip` individually
2. Run `npm install` and `npm run dev`
3. See the app at that stage of development
The `quickpoll-demo-complete.zip` has git history, so you can:
1. `git log --oneline` - see all commits
2. `git checkout stap-3` - jump to any step
3. `git diff stap-2 stap-3` - see what changed
## 🎓 For Students
After the lesson, students can:
1. Clone the repo and explore the code
2. Modify styling with Tailwind
3. Add new features (poll deletion, editing, etc.)
4. Connect to a real database
5. Deploy to Vercel
This is a great foundation for learning full-stack web development!
## 📝 Notes
- All code in English (variable names, comments)
- UI text in Dutch (button labels, messages)
- Fully typed TypeScript throughout
- Comprehensive comments for teaching
- Clean, readable code style
## 🎉 Ready to Teach!
You have everything you need. Good luck with your live-coding demonstration!
Start with **QUICK-START.md** for immediate instructions.
---
**Version:** 1.0
**Next.js:** 15.x
**Node:** 18+ recommended
**Date Created:** March 2026

View File

@@ -0,0 +1,311 @@
# QuickPoll Next.js Demo - Live Coding Guide for Tim
## Overview
This is a complete, step-by-step Next.js 15 project built for live-coding demonstrations. Each step has a git tag, allowing you to jump between different stages of the application during class.
## File Structure
```
demo-output/
├── stap-0.zip # Project setup + types + data
├── stap-1.zip # Layout & Navigation
├── stap-2.zip # Homepage with poll cards
├── stap-3.zip # API routes (GET all, GET single, POST create)
├── stap-4.zip # POST vote route + demo form
├── stap-5.zip # Poll detail page
├── stap-6.zip # VoteForm component
├── stap-7.zip # Loading, error, not-found pages
├── stap-8.zip # Middleware
├── bonus.zip # Create poll page
└── quickpoll-demo-complete.zip # Full project with complete git history
```
## How to Use in Live-Coding Class
### 1. **Setup Before Class**
Extract `quickpoll-demo-complete.zip`:
```bash
unzip quickpoll-demo-complete.zip
cd quickpoll-demo
npm install
```
### 2. **Jump Between Steps During Class**
Use git checkout to jump to any step:
```bash
# Go to beginning (step 0)
git checkout stap-0
# Progress to step 1
git checkout stap-1
# Jump to step 4 to show the demo form
git checkout stap-4
# Go back to the final version
git checkout bonus
```
### 3. **Run the Application**
```bash
npm run dev
```
Then open `http://localhost:3000` in your browser and use the projector to show students.
### 4. **Show the Demo Form (stap-4)**
When you checkout `stap-4`, a demo page is available at `http://localhost:3000/demo`:
- Show students how to use the form to test the vote API
- Open DevTools (F12) → Network tab to show the POST request
- Open Console to show the console.log messages
- Display the fetch code example to teach how API calls work
## What Each Step Covers
### stap-0: Project Setup
- Create Next.js project with TypeScript and Tailwind
- Set up types and data structures
- Initial folder structure with in-memory "database"
- Includes `.cursorrules` for consistent code style
**Files to show:**
- `src/types/index.ts` - Poll and VoteBody interfaces
- `src/lib/data.ts` - Sample polls data and functions
### stap-1: Layout & Navigation
- Add navbar with QuickPoll branding
- Add navigation links (Home, Nieuwe Poll in bonus)
- Update metadata and HTML structure
- Global Tailwind styling
**Files to show:**
- `src/app/layout.tsx` - Layout component with navbar
### stap-2: Homepage
- Display all polls as cards
- Show poll question, number of options, total votes
- Add hover effects and links to poll detail pages
**Files to show:**
- `src/app/page.tsx` - Homepage with poll cards grid
- Tailwind classes for responsive design
### stap-3: API Routes
- Create `GET /api/polls` - fetch all polls
- Create `GET /api/polls/[id]` - fetch single poll
- Create `POST /api/polls` - create new poll
- Use Next.js 15 async params pattern
**Files to show:**
- `src/app/api/polls/route.ts`
- `src/app/api/polls/[id]/route.ts`
- Point out `params: Promise<{ id: string }>` pattern
### stap-4: Vote API Route + Demo Form
- Create `POST /api/polls/[id]/vote` - vote on a poll
- Input validation and error handling
- **Demo form at `/demo`** - interactive testing page
- Console.log statements for teaching
- Display fetch code example
**Files to show:**
- `src/app/api/polls/[id]/vote/route.ts`
- `src/app/demo/page.tsx` - the demo form (use projector!)
- DevTools Console to show logging
- DevTools Network tab to show POST requests
### stap-5: Poll Detail Page
- Create `src/app/poll/[id]/page.tsx` (Server Component)
- Add `generateMetadata()` for dynamic SEO
- Use `notFound()` for missing polls
- Placeholder for VoteForm (comes next)
**Files to show:**
- `src/app/poll/[id]/page.tsx`
- Point out that this is a Server Component (no "use client")
### stap-6: VoteForm Component
- Create `src/components/VoteForm.tsx` ("use client")
- Implement voting with state management
- Show results after voting with percentage bars
- Purple theme (#7c3aed) with gradient progress bars
- Real-time vote count updates
**Files to show:**
- `src/components/VoteForm.tsx`
- Explain useState and fetch inside a component
- Show console.log for vote submissions
- Update to `src/app/poll/[id]/page.tsx` to use VoteForm
### stap-7: Error Handling Pages
- Create `src/app/loading.tsx` - skeleton loader
- Create `src/app/error.tsx` - error boundary
- Create `src/app/not-found.tsx` - global 404 page
- Create `src/app/poll/[id]/not-found.tsx` - poll-specific 404
**Files to show:**
- Explain Suspense boundaries and error handling
- Show how Next.js automatically uses these files
### stap-8: Middleware
- Create `src/middleware.ts` - request logging
- Log request timing and paths
- Use matcher for selective routes
**Files to show:**
- `src/middleware.ts`
- Point out the matcher configuration
### bonus: Create Poll Page
- Create `src/app/create/page.tsx` - full form with validation
- Dynamic option inputs (add/remove)
- POST to `/api/polls` and redirect
- Remove demo page (was temporary for stap-4)
- Add "Nieuwe Poll" link to navbar
**Files to show:**
- `src/app/create/page.tsx` - form implementation
- Error handling and validation
## Teaching Tips
### Code Highlighting
- Use VS Code theme with good contrast on projector
- Increase font size (at least 16-18pt)
- Use "Zen Mode" or "Fullscreen" for less distraction
### Live Coding Strategy
1. **Type slowly** - Let students follow along
2. **Explain as you type** - Narrate what you're doing
3. **Run often** - Show working results frequently
4. **Use DevTools** - Show Network and Console tabs
5. **Ask questions** - Make it interactive
### DevTools Demonstrations
**When showing stap-4 demo:**
1. Open http://localhost:3000/demo
2. Open DevTools (F12)
3. Go to Console tab - show console.log messages
4. Go to Network tab
5. Fill the form and click "Stem"
6. Show the POST request in Network tab
7. Show the JSON response
8. Check the Console for detailed logging
### Key Concepts to Teach
- **Server Components vs Client Components** - explain when to use "use client"
- **Dynamic Routes** - show how `[id]` creates dynamic pages
- **API Routes** - show how `/api` routes handle requests
- **Middleware** - show how to run code before requests
- **Tailwind CSS** - explain utility-first CSS
- **State Management** - useState in VoteForm
- **Fetch API** - how components communicate with API
### Projector Tips
- Test the layout on a real projector before class
- Use a light theme for better visibility
- Zoom in on code snippets (Ctrl/Cmd + in browser)
- Keep demos simple - don't try to build everything from scratch
## File Sizes
- `stap-0.zip` to `stap-8.zip`: ~32-43 KB each
- `bonus.zip`: ~44 KB
- `quickpoll-demo-complete.zip`: ~160 KB (includes full git history)
## Git Commands for Students
After you show them the steps, students can explore:
```bash
# See all commits
git log --oneline
# See all tags
git tag -l
# Jump to a specific step
git checkout stap-3
# See what changed in this step
git diff stap-2 stap-3
# Go back to latest
git checkout bonus
```
## Troubleshooting
### Port 3000 already in use
```bash
npm run dev -- -p 3001
```
### Changes not showing
```bash
# Hard refresh in browser
Ctrl+Shift+R (Windows/Linux)
Cmd+Shift+R (Mac)
```
### Git checkout doesn't work
```bash
# Make sure you're in the project directory
cd quickpoll-demo
# See current status
git status
# If you have uncommitted changes
git stash
# Then try checkout again
git checkout stap-5
```
### Build errors
```bash
# Clear Next.js cache
rm -rf .next
# Reinstall dependencies
npm install
# Try building
npm run build
```
## Project Structure Notes
- **src/app/** - Next.js App Router pages and layouts
- **src/components/** - Reusable React components
- **src/lib/** - Utility functions and mock data
- **src/types/** - TypeScript interfaces
- **src/middleware.ts** - Request middleware
- **public/** - Static assets
## Additional Resources
- Next.js 15 docs: https://nextjs.org/docs
- Tailwind CSS: https://tailwindcss.com/docs
- React: https://react.dev
## Next Steps After the Demo
Students can:
1. Modify poll questions and options
2. Add new features (e.g., poll deletion, editing)
3. Connect to a real database (PostgreSQL, MongoDB, etc.)
4. Add authentication
5. Deploy to Vercel
Good luck with your live-coding demonstration! 🎉

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.