diff --git a/Les06-Compleet.zip b/Les06-Compleet.zip new file mode 100644 index 0000000..f6a9057 Binary files /dev/null and b/Les06-Compleet.zip differ diff --git a/Les06-NextJS-QuickPoll-Part2 2/Les06-Docenttekst.md b/Les06-NextJS-QuickPoll-Part2 2/Les06-Docenttekst.md new file mode 100644 index 0000000..56a5ef1 --- /dev/null +++ b/Les06-NextJS-QuickPoll-Part2 2/Les06-Docenttekst.md @@ -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? diff --git a/Les06-NextJS-QuickPoll-Part2 2/Les06-Lesopdracht.pdf b/Les06-NextJS-QuickPoll-Part2 2/Les06-Lesopdracht.pdf new file mode 100644 index 0000000..46cad4e --- /dev/null +++ b/Les06-NextJS-QuickPoll-Part2 2/Les06-Lesopdracht.pdf @@ -0,0 +1,410 @@ +%PDF-1.4 +% 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@?rNDLtCrcfGendstream +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\a7bVo2`TIL0GLI!,"'Tg,KY[XN@#$hmVBE;rk??dhbcH[JoTlogp@9$bJO7!,tt&U:$ltAEJ/ls9VW=!%8_gF56fataBk0M"g?k&`C_2ujXLE0dFDOYk,XMiendstream +endobj +29 0 obj +<< +/Filter [ /ASCII85Decode /FlateDecode ] /Length 730 +>> +stream +Gatn$d;IYl'Sc(%MK@=q8i=';3!i[R[SE:B:>@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%Ij-+Rq**6C*IA"9-u'rk&3&Ea>HP5;cEkA&!Ym1?Lu6WZC5[BH+1#$,O*]aI3OmjhbF4cl]SBK(Ra];C9V**$G1cjB\\jV9\`Y3B_B_<"f!VpTX]g&<+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",$\^CN?'uXSrfV&B0e&9Nme!4EiD*bCRFs=t`oI%o;B&Jok'Zk#"Y!teQJgmqjAD;m9oMR>,D-P!2E0aP0"T^PX+/VEth.p+eqC<>74Ldk"_PV/!uUcW4RZ5DAt'2@*?>H]<\\Uk8ifi64Z9MKB07Y9:GI*g0-LBIlBm[A(S*h5)$_s@/#00ptUSO#"OpFO`cKKEbuUfSP8%>;9m%FN10~>endstream +endobj +31 0 obj +<< +/Filter [ /ASCII85Decode /FlateDecode ] /Length 236 +>> +stream +Gatmt:CDb>&B4,8.Gh[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![kej&IW_AN;hhD"[%Nd\nP1b^5(U8MR>9R[kRa^diTjP8+Cj4K*:?kLr;R>)$:IRQK%e&"aI:X!JGm#oh&"f;F__;?G`hB8ZcY5>hJG"=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>-U"3X4^jbhIAJ_Es@=o;9agk9GgDV5X(AAP<4Gc#"Hl6d2P/t/#>d:,SHmLoQfS[!Ccl]N8!WQN9[?s@1rH$i`?#NJ<2(00r5?5\Y#1o8)-;DO\"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#u0FmmYbHHtJ-F#fQ#U].ZAC"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[<["[=O%k&TUrVq!$o[A/OX!udDblEA>(_=*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(pfj)`kE((AjCJGO%t$MFK)7nHPYiMm*?("niWobgNIiN#GR"<%8\0&Q+F_YB^Hm[]6_PCu+?*,4Z.\'6Q3)$dC4U\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,'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\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"LhbO./MZfYC30*[#;'?\pu1CpqV>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--Tj!c5[n\QOopcj/nYr&=L"*("#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>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?D`m:OM"?'@\+VoH*hQnVo,@_29@C&3:=B$V?ge%"m.[.j>V"W-"aWbr/qP>tA&&^@Gm$QQpGB+W0e>CbNOuhnb6X)##($$\0]!oBU@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)WCMEYc\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'--1JriL!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/!!#4pLmf_,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`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.,`dAs3\Yga5>R==4(@#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+rme"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]CakrOF(p7=gIDdPRmt&/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;',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(_Rk,K;Q9o@J!X>`i2`o$Z8l/\*-,sD("bWsq>ZsC#7:!-$gBXb[QQ,\KqjWZ,l<(4f$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(=QGFYJFNbWY1E!+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%PND-r!%h`gflWf']XcMoH3+e)oXuqV8kP@L>_E;iDQDuONArg.bE&Gd5+hf5O19Z%>VJ=/aD%KD2+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[d6DV3Fq6>L[dpeA1]X+Dp%]r9?lf4RQ_'-uSp)%Rp<)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]endstream +endobj +43 0 obj +<< +/Filter [ /ASCII85Decode /FlateDecode ] /Length 1256 +>> +stream +Gatm;gN)%,&:O:SD"CQ$P6gHp$e4P<=Ji!`_n_[tF9!hck%Q!827Ze)Zj2ba-$H(;T8g]Uk[!TC@8>OKahE;$qHi?4?%s*DM$!R6J'b/3)`q/69$X,>Bh<6/iT`$_X%VA5<'4(dd'(>6$Nfk$Qd`!`Zm-3!Wkc>PNWLWmGde/1M%95U^["/\7+lT>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[+Yc6SOn19@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!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\;YL29g$#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*&$Bu7G):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;_!*2nh7+\[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"QEQ*L!/5Tm)W_KjQ2m`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+qO.7XK6$9DtGR%WQHS\NJ\?#c!UVhb7\Be6R?7A4VRXa3?J?$TjFWpTl\G#6/D/;$!"g;e\@40oTendstream +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 diff --git a/Les06-NextJS-QuickPoll-Part2 2/Les06-Live-Coding-Guide.md b/Les06-NextJS-QuickPoll-Part2 2/Les06-Live-Coding-Guide.md new file mode 100644 index 0000000..dd88506 --- /dev/null +++ b/Les06-NextJS-QuickPoll-Part2 2/Les06-Live-Coding-Guide.md @@ -0,0 +1,1183 @@ +# Les 6: QuickPoll Live Coding Guide + +> Dit is je stap-voor-stap spiekbriefje. Lees dit op je eigen scherm terwijl je op de beamer codeert. +> Alles staat in de volgorde waarin je het typt. Niets wordt overgeslagen. + +--- + +## STAP 0 — Project aanmaken + +**Terminal:** +```bash +npx create-next-app@latest quickpoll +``` + +**Opties selecteren:** +- TypeScript → Yes +- ESLint → Yes +- Tailwind CSS → Yes +- `src/` directory → Yes +- App Router → Yes +- Import alias → @/* + +```bash +cd quickpoll +npm run dev +``` + +**Check:** localhost:3000 → Next.js standaardpagina + +**Vertel:** "Dit is create-next-app. Geeft ons een compleet project met TypeScript, Tailwind en de App Router. Alles wat we nodig hebben." + +--- + +## STAP 1 — Types + +### 1a. Maak `src/types/index.ts` + +**Vertel:** "Eerst: wat IS een poll? We definiëren dat met een TypeScript interface." + +Typ: +```tsx +export interface Poll { +``` + +**Vertel:** "Een poll heeft een id…" + +```tsx + id: string; +``` + +"…een vraag…" + +```tsx + question: string; +``` + +"…een lijst opties…" + +```tsx + options: string[]; +``` + +"…en stemmen per optie. De index matcht: votes[0] hoort bij options[0]." + +```tsx + votes: number[]; +} +``` + +--- + +## STAP 1 vervolg — Data + +### 1b. Maak `src/lib/data.ts` + +**Vertel:** "Nu onze 'database'. Gewoon een array in het geheugen. Later vervangen we dit door Supabase." + +Typ de import: +```tsx +import type { Poll } from "@/types"; +``` + +**Vertel:** "We maken drie polls aan als testdata." + +```tsx +let polls: Poll[] = [ + { + id: "1", + question: "Wat is je favoriete programmeer taal?", + options: ["JavaScript", "Python", "TypeScript", "Rust"], + votes: [45, 32, 28, 15], + }, + { + id: "2", + question: "Hoe veel uur slaap krijg je per nacht?", + options: ["< 6 uur", "6-8 uur", "8+ uur"], + votes: [12, 68, 35], + }, + { + id: "3", + question: "Welke framework gebruik je het meest?", + options: ["React", "Vue", "Svelte", "Angular"], + votes: [89, 34, 12, 8], + }, +]; +``` + +**Vertel:** "Nu helper functies. Eerst: alle polls ophalen." + +```tsx +export function getPolls(): Poll[] { + return polls; +} +``` + +"Eén poll ophalen op id:" + +```tsx +export function getPollById(id: string): Poll | undefined { + return polls.find((poll) => poll.id === id); +} +``` + +**Vertel:** "Let op het return type: `Poll | undefined`. Misschien bestaat de poll niet — dat moeten we afvangen." + +"En stemmen:" + +```tsx +export function votePoll(pollId: string, optionIndex: number): Poll | undefined { + const poll = polls.find((p) => p.id === pollId); +``` + +"Check of de poll bestaat en de index klopt:" + +```tsx + if (!poll || optionIndex < 0 || optionIndex >= poll.options.length) { + return undefined; + } +``` + +"Stem ophogen en poll teruggeven:" + +```tsx + poll.votes[optionIndex]++; + return poll; +} +``` + +**Checkpoint:** "Heeft iedereen types/index.ts en lib/data.ts?" + +--- + +## STAP 2 — Layout + +### 2a. Vervang `src/app/layout.tsx` + +**Vertel:** "De layout wrapt elke pagina. Navbar en footer staan hier — die veranderen nooit." + +Wis de inhoud. Typ de imports: + +```tsx +import type { Metadata } from "next"; +import Link from "next/link"; +import "./globals.css"; +``` + +**Vertel:** "Link is de Next.js versie van een `` tag. Client-side navigatie, geen page reload." + +SEO metadata: + +```tsx +export const metadata: Metadata = { + title: "QuickPoll", + description: "Een snelle polling app met Next.js", +}; +``` + +**Vertel:** "metadata is voor SEO. Kijk straks in je browser tab." + +De functie: + +```tsx +export default function RootLayout({ + children, +}: Readonly<{ children: React.ReactNode }>) { + return ( + + +``` + +**Vertel:** "Nu de navbar. Simpel: logo en een home link." + +```tsx + +``` + +Main en footer: + +```tsx +
{children}
+ +
+
+ © 2026 QuickPoll — Built with Next.js 15 +
+
+ + + ); +} +``` + +**Vertel:** "`{children}` — hier komt de pagina-inhoud. Elke pagina in de app wordt hier ingeladen." + +**Check:** localhost:3000 → navbar met "QuickPoll" zichtbaar + +--- + +### 2b. Vervang `src/app/page.tsx` + +**Vertel:** "Nu de homepage. Dit is een Server Component — geen 'use client', data direct ophalen." + +Wis de inhoud. Typ: + +```tsx +import Link from "next/link"; +import { getPolls } from "@/lib/data"; +``` + +**Vertel:** "We importeren getPolls uit onze data module. Dat `@/` is een shortcut voor de src/ folder." + +```tsx +export default function Home() { + const polls = getPolls(); +``` + +**Vertel:** "Kijk: we roepen getPolls() gewoon aan. Geen useEffect, geen loading state. Dit is een Server Component — data ophalen is direct." + +De JSX: + +```tsx + return ( +
+
+

QuickPoll

+

+ Kies een poll en stem af in een oogwenk +

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

+ {poll.question} +

+
+ {poll.options.length} opties + {totalVotes} stemmen +
+
+ Stemmen → +
+
+ + ); + })} +
+
+ ); +} +``` + +**Vertel:** "`.reduce()` telt alle stemmen op. En elke card linkt naar `/poll/1`, `/poll/2` etc. Die pagina maken we straks." + +**Check:** localhost:3000 → 3 poll cards zichtbaar + +**Checkpoint:** "Ziet iedereen 3 cards? Mooi." + +--- + +## STAP 3 — GET API Route + +### 3a. Maak `src/app/api/polls/[id]/route.ts` + +**Vertel:** "Nu onze eerste API route. In Next.js is de folder-structuur je URL. Deze folder wordt `/api/polls/1`." + +"Die `[id]` met vierkante haakjes — dat is een dynamic route. Het getal in de URL wordt de id parameter." + +Typ: + +```tsx +import { NextResponse } from "next/server"; +import { getPollById } from "@/lib/data"; +``` + +**Vertel:** "Een API route exporteert functies met HTTP method namen. GET voor data ophalen." + +```tsx +export async function GET( + request: Request, + { params }: { params: Promise<{ id: string }> } +) { +``` + +**Vertel:** "Let op: `Promise<{ id: string }>`. In Next.js 15 zijn params een Promise. Vergeet de await niet!" + +```tsx + const { id } = await params; + const poll = getPollById(id); +``` + +"Als de poll niet bestaat: 404." + +```tsx + if (!poll) { + return NextResponse.json({ error: "Poll not found" }, { status: 404 }); + } +``` + +"Anders: stuur de poll als JSON." + +```tsx + return NextResponse.json(poll); +} +``` + +**Test:** Open browser → `localhost:3000/api/polls/1` → JSON! + +**Vertel:** "Probeer ook `/api/polls/999` — daar krijg je de 404." + +**Checkpoint:** "Werkt de API? Recap klaar. Nu het nieuwe werk." + +--- + +## STAP 4 — POST Vote Route + +### 4a. Maak `src/app/api/polls/[id]/vote/route.ts` + +**Vertel:** "Tot nu toe hadden we GET — data ophalen. Nu POST — data wijzigen. In dit geval: stemmen." + +Typ de imports: + +```tsx +import { NextResponse } from "next/server"; +import { votePoll } from "@/lib/data"; +``` + +**Vertel:** "Eerst definiëren we types voor de parameters en de request body." + +```tsx +interface RouteParams { + params: Promise<{ id: string }>; +} + +interface VoteBody { + optionIndex: number; +} +``` + +**Vertel:** "Een POST route volgt altijd vijf stappen: params, body, validatie, actie, response." + +```tsx +export async function POST( + request: Request, + { params }: RouteParams +): Promise { +``` + +"Stap 1: params uitlezen — welke poll?" + +```tsx + const { id } = await params; +``` + +"Stap 2: body uitlezen — welke optie? `request.json()` leest wat de client meestuurt." + +```tsx + const body: VoteBody = await request.json(); +``` + +"Stap 3: validatie — is optionIndex een nummer?" + +```tsx + if (typeof body.optionIndex !== "number") { + return NextResponse.json( + { error: "optionIndex is verplicht" }, + { status: 400 } + ); + } +``` + +**Vertel:** "400 = bad request. Jij stuurde slechte data." + +"Stap 4: actie uitvoeren." + +```tsx + const updatedPoll = votePoll(id, body.optionIndex); + + if (!updatedPoll) { + return NextResponse.json( + { error: "Poll niet gevonden of ongeldige optie" }, + { status: 404 } + ); + } +``` + +**Vertel:** "404 = niet gevonden. Twee checks: poll bestaat niet, of optie-index ongeldig." + +"Stap 5: response sturen." + +```tsx + return NextResponse.json(updatedPoll); +} +``` + +### 4b. Testen in de browser console + +**Vertel:** "We testen! Open DevTools (F12), ga naar Console, en plak dit:" + +```javascript +fetch('/api/polls/1/vote', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ optionIndex: 0 }) +}).then(r => r.json()).then(console.log) +``` + +**Vertel:** "Zie je? De votes zijn veranderd in de JSON. De API werkt." + +**Checkpoint:** "Heeft iedereen een JSON resultaat in de console?" + +--- + +## STAP 5 — Poll Detail Pagina + +### 5a. Maak `src/app/poll/[id]/page.tsx` + +**Vertel:** "Nu de pagina waar je op een poll stemt. Weer een dynamic route: `/poll/1`, `/poll/2` etc." + +Imports: + +```tsx +import { notFound } from "next/navigation"; +import { getPollById } from "@/lib/data"; +import VoteForm from "@/components/VoteForm"; +import type { Metadata } from "next"; +``` + +**Vertel:** "We importeren VoteForm — dat component bestaat nog niet, dat bouwen we zo." + +Type: + +```tsx +interface PageProps { + params: Promise<{ id: string }>; +} +``` + +**Vertel:** "Eerste nieuw concept: `generateMetadata`. Hiermee krijgt elke poll z'n eigen titel in de browser tab." + +```tsx +export async function generateMetadata({ params }: PageProps): Promise { + const { id } = await params; + const poll = getPollById(id); + + if (!poll) { + return { title: "Poll niet gevonden" }; + } + + return { + title: `${poll.question} — QuickPoll`, + description: `Stem op: ${poll.options.join(", ")}`, + }; +} +``` + +**Vertel:** "Gratis SEO. Google ziet 'Wat is de beste editor?' in plaats van gewoon 'QuickPoll'." + +Nu de pagina zelf: + +```tsx +export default async function PollPage({ params }: PageProps) { + const { id } = await params; + const poll = getPollById(id); +``` + +**Vertel:** "Tweede concept: `notFound()`. Als de poll niet bestaat, roep je dit aan. Next.js toont dan automatisch een 404 pagina." + +```tsx + if (!poll) { + notFound(); + } +``` + +"En we renderen de poll titel en het VoteForm component:" + +```tsx + return ( +
+

+ {poll.question} +

+ +
+ ); +} +``` + +**Vertel:** "Dit is een Server Component die een Client Component rendert. Server haalt data, client doet interactie. Dat is de kern van Next.js." + +**Check:** localhost:3000/poll/1 → error (VoteForm bestaat nog niet, dat klopt!) + +--- + +## ☕ PAUZE — 15 minuten + +**Vertel:** "Pauze! Stap 0-5 staan er. Na de pauze bouwen we het interactieve hart: de VoteForm." + +--- + +## STAP 6 — VoteForm Component + +### 6a. Maak `src/components/VoteForm.tsx` + +**Vertel:** "Dit is het leukste stuk. Een Client Component met interactiviteit." + +"Eerste regel: `'use client'`. Daarmee zeg je tegen Next.js: dit draait in de browser. Alles met useState of onClick MOET dit hebben." + +```tsx +"use client"; +``` + +Imports: + +```tsx +import { useState } from "react"; +import type { Poll } from "@/types"; +``` + +Props interface: + +```tsx +interface VoteFormProps { + poll: Poll; +} +``` + +**Vertel:** "We ontvangen een poll als prop van de Server Component." + +Functie openen: + +```tsx +export default function VoteForm({ poll }: VoteFormProps) { +``` + +### 6b. State variabelen + +**Vertel:** "We hebben vier stukken state nodig. Ik leg ze één voor één uit." + +"Welke optie is geselecteerd? `null` want er kan nog niks geselecteerd zijn:" + +```tsx + const [selectedOption, setSelectedOption] = useState(null); +``` + +"Is er al gestemd?" + +```tsx + const [hasVoted, setHasVoted] = useState(false); +``` + +"Zijn we bezig met het versturen? Om dubbel klikken te voorkomen:" + +```tsx + const [isSubmitting, setIsSubmitting] = useState(false); +``` + +"De huidige poll data — die updaten we na het stemmen:" + +```tsx + const [currentPoll, setCurrentPoll] = useState(poll); +``` + +**Checkpoint:** "Heeft iedereen de vier useState regels?" + +### 6c. Helper functies + +**Vertel:** "Totaal stemmen berekenen:" + +```tsx + const totalVotes: number = currentPoll.votes.reduce( + (sum, v) => sum + v, + 0 + ); +``` + +"Percentage per optie:" + +```tsx + function getPercentage(votes: number): number { + if (totalVotes === 0) return 0; + return Math.round((votes / totalVotes) * 100); + } +``` + +### 6d. handleVote functie + +**Vertel:** "Nu de stem-functie. Dit is dezelfde fetch als we net in de console testten — maar nu in een component." + +```tsx + async function handleVote(): Promise { +``` + +"Guard clause: als er niks geselecteerd is of we al bezig zijn, stop." + +```tsx + if (selectedOption === null || isSubmitting) return; + setIsSubmitting(true); +``` + +"De fetch naar onze POST route:" + +```tsx + const response = await fetch(`/api/polls/${currentPoll.id}/vote`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ optionIndex: selectedOption }), + }); +``` + +"Als het lukt: update de poll data en zet hasVoted op true." + +```tsx + if (response.ok) { + const updatedPoll: Poll = await response.json(); + setCurrentPoll(updatedPoll); + setHasVoted(true); + } + + setIsSubmitting(false); + } +``` + +**Checkpoint:** "Tot hier mee? Dan gaan we de UI bouwen." + +### 6e. UI — De opties buttons + +**Vertel:** "De return. We mappen over de opties — elke optie wordt een button." + +```tsx + return ( +
+ {currentPoll.options.map((option, index) => { + const percentage = getPercentage(currentPoll.votes[index]); + const isSelected = selectedOption === index; +``` + +**Vertel:** "Elke button. onClick selecteert de optie, maar alleen als je nog niet gestemd hebt." + +```tsx + return ( + + ); + })} +``` + +### 6f. UI — Stem button en bedankt bericht + +**Vertel:** "De stem-knop. Alleen zichtbaar als je nog niet gestemd hebt." + +```tsx + {!hasVoted && ( + + )} +``` + +"En het bedankt bericht na het stemmen:" + +```tsx + {hasVoted && ( +

+ Bedankt voor je stem! Totaal: {totalVotes} stemmen +

+ )} +
+ ); +} +``` + +### 6g. Testen! + +**Test:** localhost:3000/poll/1 + +**Vertel:** "Klik op een optie — paarse border. Klik Stem. Zie je de animatie? De bars vullen zich." + +**Loop rond. Dit is het lastigste stuk. Neem 10 min extra als nodig.** + +**Veelvoorkomende fouten:** +- `"use client"` vergeten → crash +- Fetch URL zonder backticks → werkt niet +- Bestand niet opgeslagen → niks veranderd + +--- + +## STAP 7 — Loading, Error & Not-Found + +**Vertel:** "In gewoon React bouw je dit zelf. In Next.js: maak een bestand aan en het werkt." + +### 7a. Maak `src/app/loading.tsx` + +**Vertel:** "Skeleton loading. Die grijze blokken die pulseren — `animate-pulse` is een Tailwind class." + +```tsx +export default function Loading() { + return ( +
+
+
+
+
+ {[1, 2, 3].map((i) => ( +
+
+
+
+ ))} +
+ ); +} +``` + +### 7b. Maak `src/app/error.tsx` + +**Vertel:** "Error boundary. LET OP: `'use client'` is hier VERPLICHT. Dat is een Next.js vereiste." + +```tsx +"use client"; + +export default function Error({ + error, + reset, +}: { + error: Error; + reset: () => void; +}) { + return ( +
+

+ Er ging iets mis! +

+

{error.message}

+ +
+ ); +} +``` + +**Vertel:** "`reset` herlaadt het component dat faalde. Zo hoeft de user niet de hele pagina te refreshen." + +### 7c. Maak `src/app/not-found.tsx` + +```tsx +import Link from "next/link"; + +export default function NotFound() { + return ( +
+

404

+

Deze pagina bestaat niet.

+ + Terug naar home + +
+ ); +} +``` + +**Test:** localhost:3000/poll/999 → 404 pagina! + +**Vertel:** "Dat werkt door de `notFound()` aanroep in onze poll pagina." + +**Checkpoint:** "Alle drie werkend?" + +--- + +## STAP 8 — Middleware + +### 8a. Maak `src/middleware.ts` + +⚠️ **Let op locatie: `src/middleware.ts` — NIET in app/, NIET in een subfolder!** + +**Vertel:** "Middleware is als een portier. Elke request passeert hier eerst. Nu gebruiken we het voor logging — later bij Supabase voor authenticatie." + +```tsx +import { NextResponse } from "next/server"; +import type { NextRequest } from "next/server"; + +export function middleware(request: NextRequest): NextResponse { + const start = Date.now(); + + console.log(`[${request.method}] ${request.nextUrl.pathname}`); + + const response = NextResponse.next(); + response.headers.set("x-request-time", String(Date.now() - start)); + + return response; +} +``` + +**Vertel:** "`NextResponse.next()` betekent: alles oké, ga door naar de route." + +"De matcher bepaalt welke routes de middleware raakt:" + +```tsx +export const config = { + matcher: ["/api/:path*", "/poll/:path*"], +}; +``` + +**Vertel:** "Alleen API routes en poll pagina's. De homepage skipt de middleware." + +### 8b. Testen + +**Vertel:** "Open je terminal waar `npm run dev` draait. Klik op een poll in de browser." + +**In de terminal:** `[GET] /poll/1` verschijnt. + +**Vertel:** "En in DevTools: Network tab → klik op een request → Headers → `x-request-time`. Dat is de verwerkingstijd die onze middleware toevoegt." + +--- + +## ✅ APP COMPLEET! + +**Vertel:** "De hele app werkt. Van nul naar werkend in één les." + +--- + +## BONUS — Nieuwe Poll Aanmaken (als je tijd overhebt) + +> ~20 minuten. Doe dit klassikaal als je voor 2:45 klaar bent. + +**Vertel:** "We gaan een pagina maken waar je zelf een nieuwe poll kunt aanmaken. Twee dingen nodig: een API route die polls aanmaakt, en een pagina met een formulier." + +### Bonus A. Voeg `createPoll` toe aan `src/lib/data.ts` + +**Vertel:** "Eerst een helper functie in onze data module. Open `data.ts` en voeg onderaan toe:" + +```tsx +export function createPoll(question: string, options: string[]): Poll { + const newPoll: Poll = { + id: String(nextId++), + question, + options, + votes: new Array(options.length).fill(0), + }; + polls.push(newPoll); + return newPoll; +} +``` + +**Vertel:** "`new Array(options.length).fill(0)` — maakt een array met nullen, even lang als het aantal opties. Elke optie begint met 0 stemmen." + +### Bonus B. Maak `src/app/api/polls/route.ts` + +**Vertel:** "Nu de API route. Let op: dit is `api/polls/route.ts`, niet in de `[id]` folder. Dit wordt `POST /api/polls`." + +```tsx +import { NextResponse } from "next/server"; +import { createPoll } from "@/lib/data"; +``` + +**Vertel:** "We definiëren een type voor de request body." + +```tsx +interface CreatePollBody { + question: string; + options: string[]; +} +``` + +"De POST handler:" + +```tsx +export async function POST(request: Request): Promise { +``` + +"Body uitlezen:" + +```tsx + const body: CreatePollBody = await request.json(); +``` + +"Validatie — vraag moet er zijn, en minimaal 2 opties:" + +```tsx + if (!body.question || !body.options || body.options.length < 2) { + return NextResponse.json( + { error: "Vraag en minimaal 2 opties zijn verplicht" }, + { status: 400 } + ); + } +``` + +"Poll aanmaken en teruggeven:" + +```tsx + const newPoll = createPoll(body.question, body.options); + return NextResponse.json(newPoll, { status: 201 }); +} +``` + +**Vertel:** "201 = created. Standaard HTTP status als je iets nieuws aanmaakt." + +### Bonus C. Maak `src/app/create/page.tsx` + +**Vertel:** "Nu het formulier. Dit is een Client Component — we hebben state nodig voor de inputs." + +```tsx +"use client"; + +import { useState } from "react"; +import { useRouter } from "next/navigation"; +``` + +**Vertel:** "`useRouter` is voor navigatie vanuit een Client Component. Straks sturen we de user terug naar home." + +```tsx +export default function CreatePollPage() { + const router = useRouter(); + const [question, setQuestion] = useState(""); + const [options, setOptions] = useState(["", ""]); + const [isSubmitting, setIsSubmitting] = useState(false); +``` + +**Vertel:** "Drie stukken state. De vraag, de opties (begint met 2 lege strings), en een submit flag." + +"Een functie om een optie toe te voegen:" + +```tsx + function addOption() { + setOptions([...options, ""]); + } +``` + +"En een functie om een optie te updaten op een specifieke index:" + +```tsx + function updateOption(index: number, value: string) { + const newOptions = [...options]; + newOptions[index] = value; + setOptions(newOptions); + } +``` + +**Vertel:** "Nu de submit functie. Filtert lege opties weg, POST naar de API, redirect naar home." + +```tsx + async function handleSubmit(e: React.FormEvent) { + e.preventDefault(); + setIsSubmitting(true); + + const filledOptions = options.filter((opt) => opt.trim() !== ""); + + if (!question.trim() || filledOptions.length < 2) { + alert("Vul een vraag in en minimaal 2 opties."); + setIsSubmitting(false); + return; + } + + const response = await fetch("/api/polls", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ question, options: filledOptions }), + }); + + if (response.ok) { + router.push("/"); + } + + setIsSubmitting(false); + } +``` + +**Vertel:** "`e.preventDefault()` voorkomt dat de pagina herlaadt. `router.push('/')` stuurt je terug naar de homepage." + +"Nu de JSX. Een form met inputs:" + +```tsx + return ( +
+

+ Nieuwe Poll Aanmaken +

+ +
+
+ + setQuestion(e.target.value)} + placeholder="Stel je vraag..." + className="w-full p-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent" + /> +
+``` + +"De opties — we mappen over de options array:" + +```tsx +
+ +
+ {options.map((option, index) => ( + updateOption(index, e.target.value)} + placeholder={`Optie ${index + 1}`} + className="w-full p-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent" + /> + ))} +
+``` + +"Knop om meer opties toe te voegen:" + +```tsx + +
+``` + +"En de submit knop:" + +```tsx + +
+
+ ); +} +``` + +### Bonus D. Link toevoegen in layout + +**Vertel:** "Nog even een link in de navbar. Open `layout.tsx` en voeg toe naast de Home link:" + +```tsx + + Nieuwe Poll + +``` + +### Bonus E. Testen! + +**Test:** localhost:3000/create → vul een vraag in → voeg opties toe → klik aanmaken → redirect naar home → nieuwe poll staat er! + +**Vertel:** "Klaar! De hele app is compleet. Je kunt polls bekijken, stemmen, en nieuwe polls aanmaken." + +--- + +## ✅ HELEMAAL KLAAR! + +**Samenvatting wat we gebouwd hebben:** +1. Types & data (TypeScript interfaces, in-memory database) +2. Layout & homepage (Server Component, Link navigatie) +3. GET API route (folder = URL, dynamic routes) +4. POST API route (stemmen, validatie, status codes) +5. Poll detail pagina (generateMetadata, notFound) +6. VoteForm (Client Component, useState, fetch, animaties) +7. Loading, Error, Not-Found (speciale bestanden) +8. Middleware (request interceptie) +9. Bonus: Create poll (form, POST, redirect) + +**De Next.js flow:** +Route (folder) → Page (server) → Client Component (interactie) → API Route (data) → Response + +**Volgende les:** Tailwind CSS & shadcn/ui diff --git a/Les06-NextJS-QuickPoll-Part2 2/Les06-Slide-Overzicht.md b/Les06-NextJS-QuickPoll-Part2 2/Les06-Slide-Overzicht.md new file mode 100644 index 0000000..0c80161 --- /dev/null +++ b/Les06-NextJS-QuickPoll-Part2 2/Les06-Slide-Overzicht.md @@ -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 ( + + + +
{children}
+
+
+ © 2026 QuickPoll — Built with Next.js 15 +
+
+ + + ); +} +``` + +- **`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!" diff --git a/Les06-NextJS-QuickPoll-Part2 2/Les06-Slides.pptx b/Les06-NextJS-QuickPoll-Part2 2/Les06-Slides.pptx new file mode 100644 index 0000000..55252d0 Binary files /dev/null and b/Les06-NextJS-QuickPoll-Part2 2/Les06-Slides.pptx differ diff --git a/Les06-NextJS-QuickPoll-Part2 2/demo-zips/INDEX.md b/Les06-NextJS-QuickPoll-Part2 2/demo-zips/INDEX.md new file mode 100644 index 0000000..b885ac2 --- /dev/null +++ b/Les06-NextJS-QuickPoll-Part2 2/demo-zips/INDEX.md @@ -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) diff --git a/Les06-NextJS-QuickPoll-Part2 2/demo-zips/QUICK-START.md b/Les06-NextJS-QuickPoll-Part2 2/demo-zips/QUICK-START.md new file mode 100644 index 0000000..12a83b4 --- /dev/null +++ b/Les06-NextJS-QuickPoll-Part2 2/demo-zips/QUICK-START.md @@ -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! diff --git a/Les06-NextJS-QuickPoll-Part2 2/demo-zips/README.md b/Les06-NextJS-QuickPoll-Part2 2/demo-zips/README.md new file mode 100644 index 0000000..1d26e7e --- /dev/null +++ b/Les06-NextJS-QuickPoll-Part2 2/demo-zips/README.md @@ -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 diff --git a/Les06-NextJS-QuickPoll-Part2 2/demo-zips/TEACHER-GUIDE.md b/Les06-NextJS-QuickPoll-Part2 2/demo-zips/TEACHER-GUIDE.md new file mode 100644 index 0000000..162fbc6 --- /dev/null +++ b/Les06-NextJS-QuickPoll-Part2 2/demo-zips/TEACHER-GUIDE.md @@ -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! 🎉 diff --git a/Les06-NextJS-QuickPoll-Part2 2/demo-zips/bonus.zip b/Les06-NextJS-QuickPoll-Part2 2/demo-zips/bonus.zip new file mode 100644 index 0000000..489f7f0 Binary files /dev/null and b/Les06-NextJS-QuickPoll-Part2 2/demo-zips/bonus.zip differ diff --git a/Les06-NextJS-QuickPoll-Part2 2/demo-zips/quickpoll-demo-complete.zip b/Les06-NextJS-QuickPoll-Part2 2/demo-zips/quickpoll-demo-complete.zip new file mode 100644 index 0000000..9395caa Binary files /dev/null and b/Les06-NextJS-QuickPoll-Part2 2/demo-zips/quickpoll-demo-complete.zip differ diff --git a/Les06-NextJS-QuickPoll-Part2 2/demo-zips/stap-0.zip b/Les06-NextJS-QuickPoll-Part2 2/demo-zips/stap-0.zip new file mode 100644 index 0000000..69852fd Binary files /dev/null and b/Les06-NextJS-QuickPoll-Part2 2/demo-zips/stap-0.zip differ diff --git a/Les06-NextJS-QuickPoll-Part2 2/demo-zips/stap-1.zip b/Les06-NextJS-QuickPoll-Part2 2/demo-zips/stap-1.zip new file mode 100644 index 0000000..f7b3892 Binary files /dev/null and b/Les06-NextJS-QuickPoll-Part2 2/demo-zips/stap-1.zip differ diff --git a/Les06-NextJS-QuickPoll-Part2 2/demo-zips/stap-2.zip b/Les06-NextJS-QuickPoll-Part2 2/demo-zips/stap-2.zip new file mode 100644 index 0000000..af20edb Binary files /dev/null and b/Les06-NextJS-QuickPoll-Part2 2/demo-zips/stap-2.zip differ diff --git a/Les06-NextJS-QuickPoll-Part2 2/demo-zips/stap-3.zip b/Les06-NextJS-QuickPoll-Part2 2/demo-zips/stap-3.zip new file mode 100644 index 0000000..738da91 Binary files /dev/null and b/Les06-NextJS-QuickPoll-Part2 2/demo-zips/stap-3.zip differ diff --git a/Les06-NextJS-QuickPoll-Part2 2/demo-zips/stap-4.zip b/Les06-NextJS-QuickPoll-Part2 2/demo-zips/stap-4.zip new file mode 100644 index 0000000..0ce7241 Binary files /dev/null and b/Les06-NextJS-QuickPoll-Part2 2/demo-zips/stap-4.zip differ diff --git a/Les06-NextJS-QuickPoll-Part2 2/demo-zips/stap-5.zip b/Les06-NextJS-QuickPoll-Part2 2/demo-zips/stap-5.zip new file mode 100644 index 0000000..25e5e09 Binary files /dev/null and b/Les06-NextJS-QuickPoll-Part2 2/demo-zips/stap-5.zip differ diff --git a/Les06-NextJS-QuickPoll-Part2 2/demo-zips/stap-6.zip b/Les06-NextJS-QuickPoll-Part2 2/demo-zips/stap-6.zip new file mode 100644 index 0000000..c46f5b6 Binary files /dev/null and b/Les06-NextJS-QuickPoll-Part2 2/demo-zips/stap-6.zip differ diff --git a/Les06-NextJS-QuickPoll-Part2 2/demo-zips/stap-7.zip b/Les06-NextJS-QuickPoll-Part2 2/demo-zips/stap-7.zip new file mode 100644 index 0000000..b9edaa0 Binary files /dev/null and b/Les06-NextJS-QuickPoll-Part2 2/demo-zips/stap-7.zip differ diff --git a/Les06-NextJS-QuickPoll-Part2 2/demo-zips/stap-8.zip b/Les06-NextJS-QuickPoll-Part2 2/demo-zips/stap-8.zip new file mode 100644 index 0000000..0ccdc0a Binary files /dev/null and b/Les06-NextJS-QuickPoll-Part2 2/demo-zips/stap-8.zip differ diff --git a/Les06-NextJS-QuickPoll-Part2/Les06-Docenttekst.md b/Les06-NextJS-QuickPoll-Part2/Les06-Docenttekst.md new file mode 100644 index 0000000..56a5ef1 --- /dev/null +++ b/Les06-NextJS-QuickPoll-Part2/Les06-Docenttekst.md @@ -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? diff --git a/Les06-NextJS-QuickPoll-Part2/Les06-Lesopdracht.pdf b/Les06-NextJS-QuickPoll-Part2/Les06-Lesopdracht.pdf new file mode 100644 index 0000000..46cad4e --- /dev/null +++ b/Les06-NextJS-QuickPoll-Part2/Les06-Lesopdracht.pdf @@ -0,0 +1,410 @@ +%PDF-1.4 +% 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@?rNDLtCrcfGendstream +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\a7bVo2`TIL0GLI!,"'Tg,KY[XN@#$hmVBE;rk??dhbcH[JoTlogp@9$bJO7!,tt&U:$ltAEJ/ls9VW=!%8_gF56fataBk0M"g?k&`C_2ujXLE0dFDOYk,XMiendstream +endobj +29 0 obj +<< +/Filter [ /ASCII85Decode /FlateDecode ] /Length 730 +>> +stream +Gatn$d;IYl'Sc(%MK@=q8i=';3!i[R[SE:B:>@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%Ij-+Rq**6C*IA"9-u'rk&3&Ea>HP5;cEkA&!Ym1?Lu6WZC5[BH+1#$,O*]aI3OmjhbF4cl]SBK(Ra];C9V**$G1cjB\\jV9\`Y3B_B_<"f!VpTX]g&<+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",$\^CN?'uXSrfV&B0e&9Nme!4EiD*bCRFs=t`oI%o;B&Jok'Zk#"Y!teQJgmqjAD;m9oMR>,D-P!2E0aP0"T^PX+/VEth.p+eqC<>74Ldk"_PV/!uUcW4RZ5DAt'2@*?>H]<\\Uk8ifi64Z9MKB07Y9:GI*g0-LBIlBm[A(S*h5)$_s@/#00ptUSO#"OpFO`cKKEbuUfSP8%>;9m%FN10~>endstream +endobj +31 0 obj +<< +/Filter [ /ASCII85Decode /FlateDecode ] /Length 236 +>> +stream +Gatmt:CDb>&B4,8.Gh[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![kej&IW_AN;hhD"[%Nd\nP1b^5(U8MR>9R[kRa^diTjP8+Cj4K*:?kLr;R>)$:IRQK%e&"aI:X!JGm#oh&"f;F__;?G`hB8ZcY5>hJG"=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>-U"3X4^jbhIAJ_Es@=o;9agk9GgDV5X(AAP<4Gc#"Hl6d2P/t/#>d:,SHmLoQfS[!Ccl]N8!WQN9[?s@1rH$i`?#NJ<2(00r5?5\Y#1o8)-;DO\"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#u0FmmYbHHtJ-F#fQ#U].ZAC"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[<["[=O%k&TUrVq!$o[A/OX!udDblEA>(_=*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(pfj)`kE((AjCJGO%t$MFK)7nHPYiMm*?("niWobgNIiN#GR"<%8\0&Q+F_YB^Hm[]6_PCu+?*,4Z.\'6Q3)$dC4U\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,'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\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"LhbO./MZfYC30*[#;'?\pu1CpqV>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--Tj!c5[n\QOopcj/nYr&=L"*("#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>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?D`m:OM"?'@\+VoH*hQnVo,@_29@C&3:=B$V?ge%"m.[.j>V"W-"aWbr/qP>tA&&^@Gm$QQpGB+W0e>CbNOuhnb6X)##($$\0]!oBU@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)WCMEYc\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'--1JriL!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/!!#4pLmf_,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`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.,`dAs3\Yga5>R==4(@#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+rme"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]CakrOF(p7=gIDdPRmt&/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;',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(_Rk,K;Q9o@J!X>`i2`o$Z8l/\*-,sD("bWsq>ZsC#7:!-$gBXb[QQ,\KqjWZ,l<(4f$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(=QGFYJFNbWY1E!+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%PND-r!%h`gflWf']XcMoH3+e)oXuqV8kP@L>_E;iDQDuONArg.bE&Gd5+hf5O19Z%>VJ=/aD%KD2+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[d6DV3Fq6>L[dpeA1]X+Dp%]r9?lf4RQ_'-uSp)%Rp<)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]endstream +endobj +43 0 obj +<< +/Filter [ /ASCII85Decode /FlateDecode ] /Length 1256 +>> +stream +Gatm;gN)%,&:O:SD"CQ$P6gHp$e4P<=Ji!`_n_[tF9!hck%Q!827Ze)Zj2ba-$H(;T8g]Uk[!TC@8>OKahE;$qHi?4?%s*DM$!R6J'b/3)`q/69$X,>Bh<6/iT`$_X%VA5<'4(dd'(>6$Nfk$Qd`!`Zm-3!Wkc>PNWLWmGde/1M%95U^["/\7+lT>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[+Yc6SOn19@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!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\;YL29g$#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*&$Bu7G):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;_!*2nh7+\[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"QEQ*L!/5Tm)W_KjQ2m`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+qO.7XK6$9DtGR%WQHS\NJ\?#c!UVhb7\Be6R?7A4VRXa3?J?$TjFWpTl\G#6/D/;$!"g;e\@40oTendstream +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 diff --git a/Les06-NextJS-QuickPoll-Part2/Les06-Live-Coding-Guide.md b/Les06-NextJS-QuickPoll-Part2/Les06-Live-Coding-Guide.md new file mode 100644 index 0000000..dd88506 --- /dev/null +++ b/Les06-NextJS-QuickPoll-Part2/Les06-Live-Coding-Guide.md @@ -0,0 +1,1183 @@ +# Les 6: QuickPoll Live Coding Guide + +> Dit is je stap-voor-stap spiekbriefje. Lees dit op je eigen scherm terwijl je op de beamer codeert. +> Alles staat in de volgorde waarin je het typt. Niets wordt overgeslagen. + +--- + +## STAP 0 — Project aanmaken + +**Terminal:** +```bash +npx create-next-app@latest quickpoll +``` + +**Opties selecteren:** +- TypeScript → Yes +- ESLint → Yes +- Tailwind CSS → Yes +- `src/` directory → Yes +- App Router → Yes +- Import alias → @/* + +```bash +cd quickpoll +npm run dev +``` + +**Check:** localhost:3000 → Next.js standaardpagina + +**Vertel:** "Dit is create-next-app. Geeft ons een compleet project met TypeScript, Tailwind en de App Router. Alles wat we nodig hebben." + +--- + +## STAP 1 — Types + +### 1a. Maak `src/types/index.ts` + +**Vertel:** "Eerst: wat IS een poll? We definiëren dat met een TypeScript interface." + +Typ: +```tsx +export interface Poll { +``` + +**Vertel:** "Een poll heeft een id…" + +```tsx + id: string; +``` + +"…een vraag…" + +```tsx + question: string; +``` + +"…een lijst opties…" + +```tsx + options: string[]; +``` + +"…en stemmen per optie. De index matcht: votes[0] hoort bij options[0]." + +```tsx + votes: number[]; +} +``` + +--- + +## STAP 1 vervolg — Data + +### 1b. Maak `src/lib/data.ts` + +**Vertel:** "Nu onze 'database'. Gewoon een array in het geheugen. Later vervangen we dit door Supabase." + +Typ de import: +```tsx +import type { Poll } from "@/types"; +``` + +**Vertel:** "We maken drie polls aan als testdata." + +```tsx +let polls: Poll[] = [ + { + id: "1", + question: "Wat is je favoriete programmeer taal?", + options: ["JavaScript", "Python", "TypeScript", "Rust"], + votes: [45, 32, 28, 15], + }, + { + id: "2", + question: "Hoe veel uur slaap krijg je per nacht?", + options: ["< 6 uur", "6-8 uur", "8+ uur"], + votes: [12, 68, 35], + }, + { + id: "3", + question: "Welke framework gebruik je het meest?", + options: ["React", "Vue", "Svelte", "Angular"], + votes: [89, 34, 12, 8], + }, +]; +``` + +**Vertel:** "Nu helper functies. Eerst: alle polls ophalen." + +```tsx +export function getPolls(): Poll[] { + return polls; +} +``` + +"Eén poll ophalen op id:" + +```tsx +export function getPollById(id: string): Poll | undefined { + return polls.find((poll) => poll.id === id); +} +``` + +**Vertel:** "Let op het return type: `Poll | undefined`. Misschien bestaat de poll niet — dat moeten we afvangen." + +"En stemmen:" + +```tsx +export function votePoll(pollId: string, optionIndex: number): Poll | undefined { + const poll = polls.find((p) => p.id === pollId); +``` + +"Check of de poll bestaat en de index klopt:" + +```tsx + if (!poll || optionIndex < 0 || optionIndex >= poll.options.length) { + return undefined; + } +``` + +"Stem ophogen en poll teruggeven:" + +```tsx + poll.votes[optionIndex]++; + return poll; +} +``` + +**Checkpoint:** "Heeft iedereen types/index.ts en lib/data.ts?" + +--- + +## STAP 2 — Layout + +### 2a. Vervang `src/app/layout.tsx` + +**Vertel:** "De layout wrapt elke pagina. Navbar en footer staan hier — die veranderen nooit." + +Wis de inhoud. Typ de imports: + +```tsx +import type { Metadata } from "next"; +import Link from "next/link"; +import "./globals.css"; +``` + +**Vertel:** "Link is de Next.js versie van een `
` tag. Client-side navigatie, geen page reload." + +SEO metadata: + +```tsx +export const metadata: Metadata = { + title: "QuickPoll", + description: "Een snelle polling app met Next.js", +}; +``` + +**Vertel:** "metadata is voor SEO. Kijk straks in je browser tab." + +De functie: + +```tsx +export default function RootLayout({ + children, +}: Readonly<{ children: React.ReactNode }>) { + return ( + + +``` + +**Vertel:** "Nu de navbar. Simpel: logo en een home link." + +```tsx + +``` + +Main en footer: + +```tsx +
{children}
+ +
+
+ © 2026 QuickPoll — Built with Next.js 15 +
+
+ + + ); +} +``` + +**Vertel:** "`{children}` — hier komt de pagina-inhoud. Elke pagina in de app wordt hier ingeladen." + +**Check:** localhost:3000 → navbar met "QuickPoll" zichtbaar + +--- + +### 2b. Vervang `src/app/page.tsx` + +**Vertel:** "Nu de homepage. Dit is een Server Component — geen 'use client', data direct ophalen." + +Wis de inhoud. Typ: + +```tsx +import Link from "next/link"; +import { getPolls } from "@/lib/data"; +``` + +**Vertel:** "We importeren getPolls uit onze data module. Dat `@/` is een shortcut voor de src/ folder." + +```tsx +export default function Home() { + const polls = getPolls(); +``` + +**Vertel:** "Kijk: we roepen getPolls() gewoon aan. Geen useEffect, geen loading state. Dit is een Server Component — data ophalen is direct." + +De JSX: + +```tsx + return ( +
+
+

QuickPoll

+

+ Kies een poll en stem af in een oogwenk +

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

+ {poll.question} +

+
+ {poll.options.length} opties + {totalVotes} stemmen +
+
+ Stemmen → +
+
+ + ); + })} +
+
+ ); +} +``` + +**Vertel:** "`.reduce()` telt alle stemmen op. En elke card linkt naar `/poll/1`, `/poll/2` etc. Die pagina maken we straks." + +**Check:** localhost:3000 → 3 poll cards zichtbaar + +**Checkpoint:** "Ziet iedereen 3 cards? Mooi." + +--- + +## STAP 3 — GET API Route + +### 3a. Maak `src/app/api/polls/[id]/route.ts` + +**Vertel:** "Nu onze eerste API route. In Next.js is de folder-structuur je URL. Deze folder wordt `/api/polls/1`." + +"Die `[id]` met vierkante haakjes — dat is een dynamic route. Het getal in de URL wordt de id parameter." + +Typ: + +```tsx +import { NextResponse } from "next/server"; +import { getPollById } from "@/lib/data"; +``` + +**Vertel:** "Een API route exporteert functies met HTTP method namen. GET voor data ophalen." + +```tsx +export async function GET( + request: Request, + { params }: { params: Promise<{ id: string }> } +) { +``` + +**Vertel:** "Let op: `Promise<{ id: string }>`. In Next.js 15 zijn params een Promise. Vergeet de await niet!" + +```tsx + const { id } = await params; + const poll = getPollById(id); +``` + +"Als de poll niet bestaat: 404." + +```tsx + if (!poll) { + return NextResponse.json({ error: "Poll not found" }, { status: 404 }); + } +``` + +"Anders: stuur de poll als JSON." + +```tsx + return NextResponse.json(poll); +} +``` + +**Test:** Open browser → `localhost:3000/api/polls/1` → JSON! + +**Vertel:** "Probeer ook `/api/polls/999` — daar krijg je de 404." + +**Checkpoint:** "Werkt de API? Recap klaar. Nu het nieuwe werk." + +--- + +## STAP 4 — POST Vote Route + +### 4a. Maak `src/app/api/polls/[id]/vote/route.ts` + +**Vertel:** "Tot nu toe hadden we GET — data ophalen. Nu POST — data wijzigen. In dit geval: stemmen." + +Typ de imports: + +```tsx +import { NextResponse } from "next/server"; +import { votePoll } from "@/lib/data"; +``` + +**Vertel:** "Eerst definiëren we types voor de parameters en de request body." + +```tsx +interface RouteParams { + params: Promise<{ id: string }>; +} + +interface VoteBody { + optionIndex: number; +} +``` + +**Vertel:** "Een POST route volgt altijd vijf stappen: params, body, validatie, actie, response." + +```tsx +export async function POST( + request: Request, + { params }: RouteParams +): Promise { +``` + +"Stap 1: params uitlezen — welke poll?" + +```tsx + const { id } = await params; +``` + +"Stap 2: body uitlezen — welke optie? `request.json()` leest wat de client meestuurt." + +```tsx + const body: VoteBody = await request.json(); +``` + +"Stap 3: validatie — is optionIndex een nummer?" + +```tsx + if (typeof body.optionIndex !== "number") { + return NextResponse.json( + { error: "optionIndex is verplicht" }, + { status: 400 } + ); + } +``` + +**Vertel:** "400 = bad request. Jij stuurde slechte data." + +"Stap 4: actie uitvoeren." + +```tsx + const updatedPoll = votePoll(id, body.optionIndex); + + if (!updatedPoll) { + return NextResponse.json( + { error: "Poll niet gevonden of ongeldige optie" }, + { status: 404 } + ); + } +``` + +**Vertel:** "404 = niet gevonden. Twee checks: poll bestaat niet, of optie-index ongeldig." + +"Stap 5: response sturen." + +```tsx + return NextResponse.json(updatedPoll); +} +``` + +### 4b. Testen in de browser console + +**Vertel:** "We testen! Open DevTools (F12), ga naar Console, en plak dit:" + +```javascript +fetch('/api/polls/1/vote', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ optionIndex: 0 }) +}).then(r => r.json()).then(console.log) +``` + +**Vertel:** "Zie je? De votes zijn veranderd in de JSON. De API werkt." + +**Checkpoint:** "Heeft iedereen een JSON resultaat in de console?" + +--- + +## STAP 5 — Poll Detail Pagina + +### 5a. Maak `src/app/poll/[id]/page.tsx` + +**Vertel:** "Nu de pagina waar je op een poll stemt. Weer een dynamic route: `/poll/1`, `/poll/2` etc." + +Imports: + +```tsx +import { notFound } from "next/navigation"; +import { getPollById } from "@/lib/data"; +import VoteForm from "@/components/VoteForm"; +import type { Metadata } from "next"; +``` + +**Vertel:** "We importeren VoteForm — dat component bestaat nog niet, dat bouwen we zo." + +Type: + +```tsx +interface PageProps { + params: Promise<{ id: string }>; +} +``` + +**Vertel:** "Eerste nieuw concept: `generateMetadata`. Hiermee krijgt elke poll z'n eigen titel in de browser tab." + +```tsx +export async function generateMetadata({ params }: PageProps): Promise { + const { id } = await params; + const poll = getPollById(id); + + if (!poll) { + return { title: "Poll niet gevonden" }; + } + + return { + title: `${poll.question} — QuickPoll`, + description: `Stem op: ${poll.options.join(", ")}`, + }; +} +``` + +**Vertel:** "Gratis SEO. Google ziet 'Wat is de beste editor?' in plaats van gewoon 'QuickPoll'." + +Nu de pagina zelf: + +```tsx +export default async function PollPage({ params }: PageProps) { + const { id } = await params; + const poll = getPollById(id); +``` + +**Vertel:** "Tweede concept: `notFound()`. Als de poll niet bestaat, roep je dit aan. Next.js toont dan automatisch een 404 pagina." + +```tsx + if (!poll) { + notFound(); + } +``` + +"En we renderen de poll titel en het VoteForm component:" + +```tsx + return ( +
+

+ {poll.question} +

+ +
+ ); +} +``` + +**Vertel:** "Dit is een Server Component die een Client Component rendert. Server haalt data, client doet interactie. Dat is de kern van Next.js." + +**Check:** localhost:3000/poll/1 → error (VoteForm bestaat nog niet, dat klopt!) + +--- + +## ☕ PAUZE — 15 minuten + +**Vertel:** "Pauze! Stap 0-5 staan er. Na de pauze bouwen we het interactieve hart: de VoteForm." + +--- + +## STAP 6 — VoteForm Component + +### 6a. Maak `src/components/VoteForm.tsx` + +**Vertel:** "Dit is het leukste stuk. Een Client Component met interactiviteit." + +"Eerste regel: `'use client'`. Daarmee zeg je tegen Next.js: dit draait in de browser. Alles met useState of onClick MOET dit hebben." + +```tsx +"use client"; +``` + +Imports: + +```tsx +import { useState } from "react"; +import type { Poll } from "@/types"; +``` + +Props interface: + +```tsx +interface VoteFormProps { + poll: Poll; +} +``` + +**Vertel:** "We ontvangen een poll als prop van de Server Component." + +Functie openen: + +```tsx +export default function VoteForm({ poll }: VoteFormProps) { +``` + +### 6b. State variabelen + +**Vertel:** "We hebben vier stukken state nodig. Ik leg ze één voor één uit." + +"Welke optie is geselecteerd? `null` want er kan nog niks geselecteerd zijn:" + +```tsx + const [selectedOption, setSelectedOption] = useState(null); +``` + +"Is er al gestemd?" + +```tsx + const [hasVoted, setHasVoted] = useState(false); +``` + +"Zijn we bezig met het versturen? Om dubbel klikken te voorkomen:" + +```tsx + const [isSubmitting, setIsSubmitting] = useState(false); +``` + +"De huidige poll data — die updaten we na het stemmen:" + +```tsx + const [currentPoll, setCurrentPoll] = useState(poll); +``` + +**Checkpoint:** "Heeft iedereen de vier useState regels?" + +### 6c. Helper functies + +**Vertel:** "Totaal stemmen berekenen:" + +```tsx + const totalVotes: number = currentPoll.votes.reduce( + (sum, v) => sum + v, + 0 + ); +``` + +"Percentage per optie:" + +```tsx + function getPercentage(votes: number): number { + if (totalVotes === 0) return 0; + return Math.round((votes / totalVotes) * 100); + } +``` + +### 6d. handleVote functie + +**Vertel:** "Nu de stem-functie. Dit is dezelfde fetch als we net in de console testten — maar nu in een component." + +```tsx + async function handleVote(): Promise { +``` + +"Guard clause: als er niks geselecteerd is of we al bezig zijn, stop." + +```tsx + if (selectedOption === null || isSubmitting) return; + setIsSubmitting(true); +``` + +"De fetch naar onze POST route:" + +```tsx + const response = await fetch(`/api/polls/${currentPoll.id}/vote`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ optionIndex: selectedOption }), + }); +``` + +"Als het lukt: update de poll data en zet hasVoted op true." + +```tsx + if (response.ok) { + const updatedPoll: Poll = await response.json(); + setCurrentPoll(updatedPoll); + setHasVoted(true); + } + + setIsSubmitting(false); + } +``` + +**Checkpoint:** "Tot hier mee? Dan gaan we de UI bouwen." + +### 6e. UI — De opties buttons + +**Vertel:** "De return. We mappen over de opties — elke optie wordt een button." + +```tsx + return ( +
+ {currentPoll.options.map((option, index) => { + const percentage = getPercentage(currentPoll.votes[index]); + const isSelected = selectedOption === index; +``` + +**Vertel:** "Elke button. onClick selecteert de optie, maar alleen als je nog niet gestemd hebt." + +```tsx + return ( + + ); + })} +``` + +### 6f. UI — Stem button en bedankt bericht + +**Vertel:** "De stem-knop. Alleen zichtbaar als je nog niet gestemd hebt." + +```tsx + {!hasVoted && ( + + )} +``` + +"En het bedankt bericht na het stemmen:" + +```tsx + {hasVoted && ( +

+ Bedankt voor je stem! Totaal: {totalVotes} stemmen +

+ )} +
+ ); +} +``` + +### 6g. Testen! + +**Test:** localhost:3000/poll/1 + +**Vertel:** "Klik op een optie — paarse border. Klik Stem. Zie je de animatie? De bars vullen zich." + +**Loop rond. Dit is het lastigste stuk. Neem 10 min extra als nodig.** + +**Veelvoorkomende fouten:** +- `"use client"` vergeten → crash +- Fetch URL zonder backticks → werkt niet +- Bestand niet opgeslagen → niks veranderd + +--- + +## STAP 7 — Loading, Error & Not-Found + +**Vertel:** "In gewoon React bouw je dit zelf. In Next.js: maak een bestand aan en het werkt." + +### 7a. Maak `src/app/loading.tsx` + +**Vertel:** "Skeleton loading. Die grijze blokken die pulseren — `animate-pulse` is een Tailwind class." + +```tsx +export default function Loading() { + return ( +
+
+
+
+
+ {[1, 2, 3].map((i) => ( +
+
+
+
+ ))} +
+ ); +} +``` + +### 7b. Maak `src/app/error.tsx` + +**Vertel:** "Error boundary. LET OP: `'use client'` is hier VERPLICHT. Dat is een Next.js vereiste." + +```tsx +"use client"; + +export default function Error({ + error, + reset, +}: { + error: Error; + reset: () => void; +}) { + return ( +
+

+ Er ging iets mis! +

+

{error.message}

+ +
+ ); +} +``` + +**Vertel:** "`reset` herlaadt het component dat faalde. Zo hoeft de user niet de hele pagina te refreshen." + +### 7c. Maak `src/app/not-found.tsx` + +```tsx +import Link from "next/link"; + +export default function NotFound() { + return ( +
+

404

+

Deze pagina bestaat niet.

+ + Terug naar home + +
+ ); +} +``` + +**Test:** localhost:3000/poll/999 → 404 pagina! + +**Vertel:** "Dat werkt door de `notFound()` aanroep in onze poll pagina." + +**Checkpoint:** "Alle drie werkend?" + +--- + +## STAP 8 — Middleware + +### 8a. Maak `src/middleware.ts` + +⚠️ **Let op locatie: `src/middleware.ts` — NIET in app/, NIET in een subfolder!** + +**Vertel:** "Middleware is als een portier. Elke request passeert hier eerst. Nu gebruiken we het voor logging — later bij Supabase voor authenticatie." + +```tsx +import { NextResponse } from "next/server"; +import type { NextRequest } from "next/server"; + +export function middleware(request: NextRequest): NextResponse { + const start = Date.now(); + + console.log(`[${request.method}] ${request.nextUrl.pathname}`); + + const response = NextResponse.next(); + response.headers.set("x-request-time", String(Date.now() - start)); + + return response; +} +``` + +**Vertel:** "`NextResponse.next()` betekent: alles oké, ga door naar de route." + +"De matcher bepaalt welke routes de middleware raakt:" + +```tsx +export const config = { + matcher: ["/api/:path*", "/poll/:path*"], +}; +``` + +**Vertel:** "Alleen API routes en poll pagina's. De homepage skipt de middleware." + +### 8b. Testen + +**Vertel:** "Open je terminal waar `npm run dev` draait. Klik op een poll in de browser." + +**In de terminal:** `[GET] /poll/1` verschijnt. + +**Vertel:** "En in DevTools: Network tab → klik op een request → Headers → `x-request-time`. Dat is de verwerkingstijd die onze middleware toevoegt." + +--- + +## ✅ APP COMPLEET! + +**Vertel:** "De hele app werkt. Van nul naar werkend in één les." + +--- + +## BONUS — Nieuwe Poll Aanmaken (als je tijd overhebt) + +> ~20 minuten. Doe dit klassikaal als je voor 2:45 klaar bent. + +**Vertel:** "We gaan een pagina maken waar je zelf een nieuwe poll kunt aanmaken. Twee dingen nodig: een API route die polls aanmaakt, en een pagina met een formulier." + +### Bonus A. Voeg `createPoll` toe aan `src/lib/data.ts` + +**Vertel:** "Eerst een helper functie in onze data module. Open `data.ts` en voeg onderaan toe:" + +```tsx +export function createPoll(question: string, options: string[]): Poll { + const newPoll: Poll = { + id: String(nextId++), + question, + options, + votes: new Array(options.length).fill(0), + }; + polls.push(newPoll); + return newPoll; +} +``` + +**Vertel:** "`new Array(options.length).fill(0)` — maakt een array met nullen, even lang als het aantal opties. Elke optie begint met 0 stemmen." + +### Bonus B. Maak `src/app/api/polls/route.ts` + +**Vertel:** "Nu de API route. Let op: dit is `api/polls/route.ts`, niet in de `[id]` folder. Dit wordt `POST /api/polls`." + +```tsx +import { NextResponse } from "next/server"; +import { createPoll } from "@/lib/data"; +``` + +**Vertel:** "We definiëren een type voor de request body." + +```tsx +interface CreatePollBody { + question: string; + options: string[]; +} +``` + +"De POST handler:" + +```tsx +export async function POST(request: Request): Promise { +``` + +"Body uitlezen:" + +```tsx + const body: CreatePollBody = await request.json(); +``` + +"Validatie — vraag moet er zijn, en minimaal 2 opties:" + +```tsx + if (!body.question || !body.options || body.options.length < 2) { + return NextResponse.json( + { error: "Vraag en minimaal 2 opties zijn verplicht" }, + { status: 400 } + ); + } +``` + +"Poll aanmaken en teruggeven:" + +```tsx + const newPoll = createPoll(body.question, body.options); + return NextResponse.json(newPoll, { status: 201 }); +} +``` + +**Vertel:** "201 = created. Standaard HTTP status als je iets nieuws aanmaakt." + +### Bonus C. Maak `src/app/create/page.tsx` + +**Vertel:** "Nu het formulier. Dit is een Client Component — we hebben state nodig voor de inputs." + +```tsx +"use client"; + +import { useState } from "react"; +import { useRouter } from "next/navigation"; +``` + +**Vertel:** "`useRouter` is voor navigatie vanuit een Client Component. Straks sturen we de user terug naar home." + +```tsx +export default function CreatePollPage() { + const router = useRouter(); + const [question, setQuestion] = useState(""); + const [options, setOptions] = useState(["", ""]); + const [isSubmitting, setIsSubmitting] = useState(false); +``` + +**Vertel:** "Drie stukken state. De vraag, de opties (begint met 2 lege strings), en een submit flag." + +"Een functie om een optie toe te voegen:" + +```tsx + function addOption() { + setOptions([...options, ""]); + } +``` + +"En een functie om een optie te updaten op een specifieke index:" + +```tsx + function updateOption(index: number, value: string) { + const newOptions = [...options]; + newOptions[index] = value; + setOptions(newOptions); + } +``` + +**Vertel:** "Nu de submit functie. Filtert lege opties weg, POST naar de API, redirect naar home." + +```tsx + async function handleSubmit(e: React.FormEvent) { + e.preventDefault(); + setIsSubmitting(true); + + const filledOptions = options.filter((opt) => opt.trim() !== ""); + + if (!question.trim() || filledOptions.length < 2) { + alert("Vul een vraag in en minimaal 2 opties."); + setIsSubmitting(false); + return; + } + + const response = await fetch("/api/polls", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ question, options: filledOptions }), + }); + + if (response.ok) { + router.push("/"); + } + + setIsSubmitting(false); + } +``` + +**Vertel:** "`e.preventDefault()` voorkomt dat de pagina herlaadt. `router.push('/')` stuurt je terug naar de homepage." + +"Nu de JSX. Een form met inputs:" + +```tsx + return ( +
+

+ Nieuwe Poll Aanmaken +

+ +
+
+ + setQuestion(e.target.value)} + placeholder="Stel je vraag..." + className="w-full p-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent" + /> +
+``` + +"De opties — we mappen over de options array:" + +```tsx +
+ +
+ {options.map((option, index) => ( + updateOption(index, e.target.value)} + placeholder={`Optie ${index + 1}`} + className="w-full p-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent" + /> + ))} +
+``` + +"Knop om meer opties toe te voegen:" + +```tsx + +
+``` + +"En de submit knop:" + +```tsx + +
+
+ ); +} +``` + +### Bonus D. Link toevoegen in layout + +**Vertel:** "Nog even een link in de navbar. Open `layout.tsx` en voeg toe naast de Home link:" + +```tsx + + Nieuwe Poll + +``` + +### Bonus E. Testen! + +**Test:** localhost:3000/create → vul een vraag in → voeg opties toe → klik aanmaken → redirect naar home → nieuwe poll staat er! + +**Vertel:** "Klaar! De hele app is compleet. Je kunt polls bekijken, stemmen, en nieuwe polls aanmaken." + +--- + +## ✅ HELEMAAL KLAAR! + +**Samenvatting wat we gebouwd hebben:** +1. Types & data (TypeScript interfaces, in-memory database) +2. Layout & homepage (Server Component, Link navigatie) +3. GET API route (folder = URL, dynamic routes) +4. POST API route (stemmen, validatie, status codes) +5. Poll detail pagina (generateMetadata, notFound) +6. VoteForm (Client Component, useState, fetch, animaties) +7. Loading, Error, Not-Found (speciale bestanden) +8. Middleware (request interceptie) +9. Bonus: Create poll (form, POST, redirect) + +**De Next.js flow:** +Route (folder) → Page (server) → Client Component (interactie) → API Route (data) → Response + +**Volgende les:** Tailwind CSS & shadcn/ui diff --git a/Les06-NextJS-QuickPoll-Part2/Les06-Slide-Overzicht.md b/Les06-NextJS-QuickPoll-Part2/Les06-Slide-Overzicht.md new file mode 100644 index 0000000..0c80161 --- /dev/null +++ b/Les06-NextJS-QuickPoll-Part2/Les06-Slide-Overzicht.md @@ -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 ( + + + +
{children}
+
+
+ © 2026 QuickPoll — Built with Next.js 15 +
+
+ + + ); +} +``` + +- **`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!" diff --git a/Les06-NextJS-QuickPoll-Part2/Les06-Slides.pptx b/Les06-NextJS-QuickPoll-Part2/Les06-Slides.pptx new file mode 100644 index 0000000..55252d0 Binary files /dev/null and b/Les06-NextJS-QuickPoll-Part2/Les06-Slides.pptx differ diff --git a/Les06-NextJS-QuickPoll-Part2/demo-zips/INDEX.md b/Les06-NextJS-QuickPoll-Part2/demo-zips/INDEX.md new file mode 100644 index 0000000..b885ac2 --- /dev/null +++ b/Les06-NextJS-QuickPoll-Part2/demo-zips/INDEX.md @@ -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) diff --git a/Les06-NextJS-QuickPoll-Part2/demo-zips/PROJECT-SUMMARY.md b/Les06-NextJS-QuickPoll-Part2/demo-zips/PROJECT-SUMMARY.md new file mode 100644 index 0000000..0c3cc18 --- /dev/null +++ b/Les06-NextJS-QuickPoll-Part2/demo-zips/PROJECT-SUMMARY.md @@ -0,0 +1,264 @@ +# QuickPoll Next.js 15 Demo Project - Summary + +## Project Description + +QuickPoll is a complete polling application built with **Next.js 15**, **TypeScript**, and **Tailwind CSS**. Designed specifically for live-coding classroom demonstrations, it includes git tags at each development step. + +## Technology Stack + +- **Next.js 15** - React framework with App Router +- **TypeScript** - Fully typed JavaScript +- **Tailwind CSS** - Utility-first CSS framework +- **In-Memory Data** - Mock "database" for demo purposes +- **React Hooks** - useState for client-side state management + +## Project Structure + +``` +quickpoll-demo/ +├── src/ +│ ├── app/ # Next.js App Router +│ │ ├── page.tsx # Homepage with poll list +│ │ ├── layout.tsx # Root layout with navbar +│ │ ├── globals.css # Global Tailwind styles +│ │ ├── loading.tsx # Loading skeleton +│ │ ├── error.tsx # Error boundary +│ │ ├── not-found.tsx # 404 page +│ │ ├── api/ # API route handlers +│ │ │ └── polls/ +│ │ │ ├── route.ts # GET all, POST create +│ │ │ ├── [id]/ +│ │ │ │ ├── route.ts # GET single poll +│ │ │ │ ├── vote/route.ts # POST vote +│ │ │ │ └── not-found.tsx +│ │ ├── poll/[id]/ # Dynamic poll detail page +│ │ │ └── page.tsx +│ │ └── create/ # Create new poll page +│ │ └── page.tsx +│ ├── components/ # Reusable components +│ │ └── VoteForm.tsx # Voting component +│ ├── lib/ # Utilities and data +│ │ └── data.ts # Mock poll data & functions +│ ├── types/ # TypeScript interfaces +│ │ └── index.ts # Poll, VoteBody, CreatePollBody +│ └── middleware.ts # Request logging middleware +├── .cursorrules # Code style guidelines +├── package.json +├── tsconfig.json +└── tailwind.config.ts +``` + +## Core Features + +### 1. **Display Polls** (Homepage) +- Show all available polls as cards +- Display question, option count, total votes +- Click to navigate to poll detail page + +### 2. **Poll Details** (Dynamic Page) +- Show full poll question and options +- Real-time voting results with percentage bars +- Server-rendered for SEO + +### 3. **Vote on Polls** (VoteForm Component) +- Select an option +- Submit vote via POST API +- See results immediately after voting +- Purple theme with gradient progress bars + +### 4. **Create Polls** (Bonus Step) +- Form to create new polls +- Input validation +- Dynamic option fields (add/remove) +- Automatic redirect to new poll + +### 5. **API Routes** +- `GET /api/polls` - Get all polls +- `GET /api/polls/[id]` - Get single poll +- `POST /api/polls` - Create new poll +- `POST /api/polls/[id]/vote` - Vote on poll + +### 6. **Middleware** +- Log all requests with timing +- Track API and poll page access + +### 7. **Error Handling** +- Loading skeleton (Suspense) +- Error boundary component +- 404 pages (global and poll-specific) + +## Git Tags for Progression + +| Tag | Step | What's Added | +|-----|------|-------------| +| `stap-0` | Project Setup | Types, data, file structure, .cursorrules | +| `stap-1` | Layout & Navigation | Navbar, layout, metadata | +| `stap-2` | Homepage | Poll cards grid, Links | +| `stap-3` | API Routes | GET all, GET single, POST create | +| `stap-4` | Vote API & Demo | POST vote, Demo form at /demo | +| `stap-5` | Poll Detail Page | Dynamic [id] route, metadata | +| `stap-6` | VoteForm Component | Client component with voting logic | +| `stap-7` | Error Handling | loading.tsx, error.tsx, not-found.tsx | +| `stap-8` | Middleware | Request logging middleware | +| `bonus` | Create Poll | Full form, navbar link, remove demo | + +## How to Use in Class + +### Before Class +```bash +unzip quickpoll-demo-complete.zip +cd quickpoll-demo +npm install +npm run dev +``` + +### During Class +```bash +# Jump to any step +git checkout stap-3 + +# View what changed +git diff stap-2 stap-3 + +# Go to final version +git checkout bonus +``` + +### Demo the Voting API (stap-4) +1. Navigate to http://localhost:3000/demo +2. Show the form (projector) +3. Open DevTools Console to show logging +4. Open DevTools Network tab to show POST request +5. Display the fetch code example + +## Teaching Points + +### TypeScript & Types +- Define interfaces for data structures +- Export from `src/types/index.ts` +- Use types in function parameters + +### Next.js Features +- App Router with file-based routing +- Dynamic routes: `[id]` creates `/poll/1`, `/poll/2`, etc. +- Server Components by default +- Client Components with "use client" directive +- API routes in `/app/api` +- Special files: `layout.tsx`, `not-found.tsx`, `error.tsx`, `loading.tsx` + +### React Patterns +- Server Components (default, no interactivity) +- Client Components with useState +- Component composition (VoteForm in PollDetailPage) +- Fetch API for server communication + +### Tailwind CSS +- Utility-first approach +- Responsive design (md:, lg: prefixes) +- Color system (purple-500, purple-600, purple-700) +- Hover states and transitions + +### Form Handling +- Controlled inputs with useState +- Validation before submit +- Loading states during submission +- Error handling and feedback + +## Sample Data + +Three pre-loaded polls: + +1. **"Wat is je favoriete programmeer taal?"** + - JavaScript, Python, TypeScript, Rust + - Sample votes: 45, 32, 28, 15 + +2. **"Hoe veel uur slaap krijg je per nacht?"** + - < 6 uur, 6-8 uur, 8+ uur + - Sample votes: 12, 68, 35 + +3. **"Welke framework gebruik je het meest?"** + - React, Vue, Svelte, Angular + - Sample votes: 89, 34, 12, 8 + +## Color Scheme + +**Purple Theme for QuickPoll:** +- `#7c3aed` - purple-500 (main color) +- `#a855f7` - purple-600 (hover) +- `#9333ea` - purple-700 (pressed) + +**Neutral Colors:** +- Gray-50 to Gray-900 for text and backgrounds +- White for cards and backgrounds +- Red-600 for error states +- Blue-50 for information + +## Building & Deployment + +### Build for Production +```bash +npm run build +npm start +``` + +### Build Verification +```bash +npm run build +``` + +Output shows: +- Static pages: `/`, `/create` +- Dynamic pages: `/poll/[id]` +- API routes: `/api/polls`, `/api/polls/[id]`, etc. +- Middleware (Proxy) + +## Files Per Step + +Each zip file contains the complete project at that step: +- All source code +- package.json with dependencies +- Configuration files +- **Excludes:** `node_modules/`, `.next/`, `.git/` + +The `quickpoll-demo-complete.zip` includes: +- Full git history +- All commits and tags +- Ability to `git checkout` any step + +## Typical Class Flow + +1. **stap-0 to stap-2** - Show basic structure and homepage (15 min) +2. **stap-3** - Explain API routes (10 min) +3. **stap-4** - Demo the voting API in action (15 min) ⭐ Key demo point +4. **stap-5 to stap-6** - Build the voting interface (15 min) +5. **stap-7 to stap-8** - Error handling and middleware (10 min) +6. **bonus** - Create poll feature (optional, 10 min) + +## Key Takeaways for Students + +- Next.js makes building full-stack apps easy +- Server Components are the default (simpler, faster) +- API routes live alongside your pages +- TypeScript catches errors early +- Tailwind makes styling fast and consistent +- Git tags let you track feature development + +## Common Questions + +**Q: Is this production-ready?** +A: No - it uses in-memory data. For production, add a real database like PostgreSQL or MongoDB. + +**Q: Can I add authentication?** +A: Yes - add NextAuth.js or similar. Future lesson! + +**Q: How do I deploy this?** +A: Deploy to Vercel (created by Next.js team) or any Node.js hosting. See https://nextjs.org/docs/app/building-your-application/deploying + +**Q: Can students modify the code?** +A: Absolutely! Use it as a starting point for assignments. + +--- + +**Created:** March 2026 +**Next.js Version:** 15.x +**Node Version:** 18+ recommended diff --git a/Les06-NextJS-QuickPoll-Part2/demo-zips/QUICK-START.md b/Les06-NextJS-QuickPoll-Part2/demo-zips/QUICK-START.md new file mode 100644 index 0000000..12a83b4 --- /dev/null +++ b/Les06-NextJS-QuickPoll-Part2/demo-zips/QUICK-START.md @@ -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! diff --git a/Les06-NextJS-QuickPoll-Part2/demo-zips/README.md b/Les06-NextJS-QuickPoll-Part2/demo-zips/README.md new file mode 100644 index 0000000..1d26e7e --- /dev/null +++ b/Les06-NextJS-QuickPoll-Part2/demo-zips/README.md @@ -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 diff --git a/Les06-NextJS-QuickPoll-Part2/demo-zips/TEACHER-GUIDE.md b/Les06-NextJS-QuickPoll-Part2/demo-zips/TEACHER-GUIDE.md new file mode 100644 index 0000000..162fbc6 --- /dev/null +++ b/Les06-NextJS-QuickPoll-Part2/demo-zips/TEACHER-GUIDE.md @@ -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! 🎉 diff --git a/Les06-NextJS-QuickPoll-Part2/demo-zips/bonus.zip b/Les06-NextJS-QuickPoll-Part2/demo-zips/bonus.zip new file mode 100644 index 0000000..489f7f0 Binary files /dev/null and b/Les06-NextJS-QuickPoll-Part2/demo-zips/bonus.zip differ diff --git a/Les06-NextJS-QuickPoll-Part2/demo-zips/quickpoll-demo-complete.zip b/Les06-NextJS-QuickPoll-Part2/demo-zips/quickpoll-demo-complete.zip new file mode 100644 index 0000000..9395caa Binary files /dev/null and b/Les06-NextJS-QuickPoll-Part2/demo-zips/quickpoll-demo-complete.zip differ diff --git a/Les06-NextJS-QuickPoll-Part2/demo-zips/stap-0.zip b/Les06-NextJS-QuickPoll-Part2/demo-zips/stap-0.zip new file mode 100644 index 0000000..69852fd Binary files /dev/null and b/Les06-NextJS-QuickPoll-Part2/demo-zips/stap-0.zip differ diff --git a/Les06-NextJS-QuickPoll-Part2/demo-zips/stap-1.zip b/Les06-NextJS-QuickPoll-Part2/demo-zips/stap-1.zip new file mode 100644 index 0000000..f7b3892 Binary files /dev/null and b/Les06-NextJS-QuickPoll-Part2/demo-zips/stap-1.zip differ diff --git a/Les06-NextJS-QuickPoll-Part2/demo-zips/stap-2.zip b/Les06-NextJS-QuickPoll-Part2/demo-zips/stap-2.zip new file mode 100644 index 0000000..af20edb Binary files /dev/null and b/Les06-NextJS-QuickPoll-Part2/demo-zips/stap-2.zip differ diff --git a/Les06-NextJS-QuickPoll-Part2/demo-zips/stap-3.zip b/Les06-NextJS-QuickPoll-Part2/demo-zips/stap-3.zip new file mode 100644 index 0000000..738da91 Binary files /dev/null and b/Les06-NextJS-QuickPoll-Part2/demo-zips/stap-3.zip differ diff --git a/Les06-NextJS-QuickPoll-Part2/demo-zips/stap-4.zip b/Les06-NextJS-QuickPoll-Part2/demo-zips/stap-4.zip new file mode 100644 index 0000000..0ce7241 Binary files /dev/null and b/Les06-NextJS-QuickPoll-Part2/demo-zips/stap-4.zip differ diff --git a/Les06-NextJS-QuickPoll-Part2/demo-zips/stap-5.zip b/Les06-NextJS-QuickPoll-Part2/demo-zips/stap-5.zip new file mode 100644 index 0000000..25e5e09 Binary files /dev/null and b/Les06-NextJS-QuickPoll-Part2/demo-zips/stap-5.zip differ diff --git a/Les06-NextJS-QuickPoll-Part2/demo-zips/stap-6.zip b/Les06-NextJS-QuickPoll-Part2/demo-zips/stap-6.zip new file mode 100644 index 0000000..c46f5b6 Binary files /dev/null and b/Les06-NextJS-QuickPoll-Part2/demo-zips/stap-6.zip differ diff --git a/Les06-NextJS-QuickPoll-Part2/demo-zips/stap-7.zip b/Les06-NextJS-QuickPoll-Part2/demo-zips/stap-7.zip new file mode 100644 index 0000000..b9edaa0 Binary files /dev/null and b/Les06-NextJS-QuickPoll-Part2/demo-zips/stap-7.zip differ diff --git a/Les06-NextJS-QuickPoll-Part2/demo-zips/stap-8.zip b/Les06-NextJS-QuickPoll-Part2/demo-zips/stap-8.zip new file mode 100644 index 0000000..0ccdc0a Binary files /dev/null and b/Les06-NextJS-QuickPoll-Part2/demo-zips/stap-8.zip differ diff --git a/Les06-NextJS-QuickPoll-Part2/demo-zips/zi1XoQtN b/Les06-NextJS-QuickPoll-Part2/demo-zips/zi1XoQtN new file mode 100644 index 0000000..b075266 Binary files /dev/null and b/Les06-NextJS-QuickPoll-Part2/demo-zips/zi1XoQtN differ diff --git a/Les06-NextJS-QuickPoll-Part2/demo-zips/zi4mPzTQ b/Les06-NextJS-QuickPoll-Part2/demo-zips/zi4mPzTQ new file mode 100644 index 0000000..6682bd8 Binary files /dev/null and b/Les06-NextJS-QuickPoll-Part2/demo-zips/zi4mPzTQ differ diff --git a/Les06-NextJS-QuickPoll-Part2/demo-zips/zi7KvzJN b/Les06-NextJS-QuickPoll-Part2/demo-zips/zi7KvzJN new file mode 100644 index 0000000..c7bad0a Binary files /dev/null and b/Les06-NextJS-QuickPoll-Part2/demo-zips/zi7KvzJN differ diff --git a/Les06-NextJS-QuickPoll-Part2/demo-zips/ziMHBssm b/Les06-NextJS-QuickPoll-Part2/demo-zips/ziMHBssm new file mode 100644 index 0000000..477fde2 Binary files /dev/null and b/Les06-NextJS-QuickPoll-Part2/demo-zips/ziMHBssm differ diff --git a/Les06-NextJS-QuickPoll-Part2/demo-zips/ziPStmfI b/Les06-NextJS-QuickPoll-Part2/demo-zips/ziPStmfI new file mode 100644 index 0000000..915ca3c Binary files /dev/null and b/Les06-NextJS-QuickPoll-Part2/demo-zips/ziPStmfI differ diff --git a/Les06-NextJS-QuickPoll-Part2/demo-zips/ziReGaBK b/Les06-NextJS-QuickPoll-Part2/demo-zips/ziReGaBK new file mode 100644 index 0000000..d796b9e Binary files /dev/null and b/Les06-NextJS-QuickPoll-Part2/demo-zips/ziReGaBK differ diff --git a/Les06-NextJS-QuickPoll-Part2/demo-zips/ziauujj7 b/Les06-NextJS-QuickPoll-Part2/demo-zips/ziauujj7 new file mode 100644 index 0000000..32e8e93 Binary files /dev/null and b/Les06-NextJS-QuickPoll-Part2/demo-zips/ziauujj7 differ diff --git a/Les06-NextJS-QuickPoll-Part2/demo-zips/zigANmHW b/Les06-NextJS-QuickPoll-Part2/demo-zips/zigANmHW new file mode 100644 index 0000000..d9bfdaf Binary files /dev/null and b/Les06-NextJS-QuickPoll-Part2/demo-zips/zigANmHW differ diff --git a/Les06-NextJS-QuickPoll-Part2/demo-zips/zitZdV9D b/Les06-NextJS-QuickPoll-Part2/demo-zips/zitZdV9D new file mode 100644 index 0000000..9ca200d Binary files /dev/null and b/Les06-NextJS-QuickPoll-Part2/demo-zips/zitZdV9D differ diff --git a/Les06-NextJS-QuickPoll-Part2/demo-zips/zivUja03 b/Les06-NextJS-QuickPoll-Part2/demo-zips/zivUja03 new file mode 100644 index 0000000..f7a6ddc Binary files /dev/null and b/Les06-NextJS-QuickPoll-Part2/demo-zips/zivUja03 differ diff --git a/Les06-NextJS-QuickPoll-Part2/demo-zips/ziz6IP7H b/Les06-NextJS-QuickPoll-Part2/demo-zips/ziz6IP7H new file mode 100644 index 0000000..f15559b Binary files /dev/null and b/Les06-NextJS-QuickPoll-Part2/demo-zips/ziz6IP7H differ diff --git a/Les06-NextJS-QuickPoll-Part2/les6-quickpoll-starter.zip b/Les06-NextJS-QuickPoll-Part2/les6-quickpoll-starter.zip new file mode 100644 index 0000000..6f94d0f Binary files /dev/null and b/Les06-NextJS-QuickPoll-Part2/les6-quickpoll-starter.zip differ diff --git a/Les06-NextJS-QuickPoll-Part2/quickpoll-les6-starter-bundle.zip b/Les06-NextJS-QuickPoll-Part2/quickpoll-les6-starter-bundle.zip new file mode 100644 index 0000000..e69de29 diff --git a/Les06-NextJS-QuickPoll-Part2/quickpoll-les6-starter.zip b/Les06-NextJS-QuickPoll-Part2/quickpoll-les6-starter.zip new file mode 100644 index 0000000..e69de29 diff --git a/Les06-NextJS-QuickPoll-Part2/quickpoll-les6-starter/.cursorrules b/Les06-NextJS-QuickPoll-Part2/quickpoll-les6-starter/.cursorrules new file mode 100644 index 0000000..7de9420 --- /dev/null +++ b/Les06-NextJS-QuickPoll-Part2/quickpoll-les6-starter/.cursorrules @@ -0,0 +1,6 @@ +You are a Next.js 15 expert using App Router with TypeScript. +Use server components by default. +Use "use client" only when needed for interactivity. +Always define TypeScript interfaces for props, params, and API bodies. +Use Tailwind CSS for styling. +Use the @/ import alias for all local imports. diff --git a/Les06-NextJS-QuickPoll-Part2/quickpoll-les6-starter/.gitignore b/Les06-NextJS-QuickPoll-Part2/quickpoll-les6-starter/.gitignore new file mode 100644 index 0000000..5ef6a52 --- /dev/null +++ b/Les06-NextJS-QuickPoll-Part2/quickpoll-les6-starter/.gitignore @@ -0,0 +1,41 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.* +.yarn/* +!.yarn/patches +!.yarn/plugins +!.yarn/releases +!.yarn/versions + +# testing +/coverage + +# next.js +/.next/ +/out/ + +# production +/build + +# misc +.DS_Store +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* +.pnpm-debug.log* + +# env files (can opt-in for committing if needed) +.env* + +# vercel +.vercel + +# typescript +*.tsbuildinfo +next-env.d.ts diff --git a/Les06-NextJS-QuickPoll-Part2/quickpoll-les6-starter/README.md b/Les06-NextJS-QuickPoll-Part2/quickpoll-les6-starter/README.md new file mode 100644 index 0000000..e215bc4 --- /dev/null +++ b/Les06-NextJS-QuickPoll-Part2/quickpoll-les6-starter/README.md @@ -0,0 +1,36 @@ +This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app). + +## Getting Started + +First, run the development server: + +```bash +npm run dev +# or +yarn dev +# or +pnpm dev +# or +bun dev +``` + +Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. + +You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. + +This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel. + +## Learn More + +To learn more about Next.js, take a look at the following resources: + +- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. +- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. + +You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome! + +## Deploy on Vercel + +The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. + +Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details. diff --git a/Les06-NextJS-QuickPoll-Part2/quickpoll-les6-starter/next.config.ts b/Les06-NextJS-QuickPoll-Part2/quickpoll-les6-starter/next.config.ts new file mode 100644 index 0000000..e9ffa30 --- /dev/null +++ b/Les06-NextJS-QuickPoll-Part2/quickpoll-les6-starter/next.config.ts @@ -0,0 +1,7 @@ +import type { NextConfig } from "next"; + +const nextConfig: NextConfig = { + /* config options here */ +}; + +export default nextConfig; diff --git a/Les06-NextJS-QuickPoll-Part2/quickpoll-les6-starter/package-lock.json b/Les06-NextJS-QuickPoll-Part2/quickpoll-les6-starter/package-lock.json new file mode 100644 index 0000000..480ab5e --- /dev/null +++ b/Les06-NextJS-QuickPoll-Part2/quickpoll-les6-starter/package-lock.json @@ -0,0 +1,1664 @@ +{ + "name": "quickpoll", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "quickpoll", + "version": "0.1.0", + "dependencies": { + "next": "16.1.6", + "react": "19.2.3", + "react-dom": "19.2.3" + }, + "devDependencies": { + "@tailwindcss/postcss": "^4", + "@types/node": "^20", + "@types/react": "^19", + "@types/react-dom": "^19", + "tailwindcss": "^4", + "typescript": "^5" + } + }, + "node_modules/@alloc/quick-lru": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", + "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.8.1.tgz", + "integrity": "sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@img/colour": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.1.0.tgz", + "integrity": "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@img/sharp-darwin-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.5.tgz", + "integrity": "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-arm64": "1.2.4" + } + }, + "node_modules/@img/sharp-darwin-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.5.tgz", + "integrity": "sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-x64": "1.2.4" + } + }, + "node_modules/@img/sharp-libvips-darwin-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.4.tgz", + "integrity": "sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-darwin-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.4.tgz", + "integrity": "sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.4.tgz", + "integrity": "sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==", + "cpu": [ + "arm" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.4.tgz", + "integrity": "sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-ppc64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.2.4.tgz", + "integrity": "sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==", + "cpu": [ + "ppc64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-riscv64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.2.4.tgz", + "integrity": "sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==", + "cpu": [ + "riscv64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-s390x": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.2.4.tgz", + "integrity": "sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==", + "cpu": [ + "s390x" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.4.tgz", + "integrity": "sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.4.tgz", + "integrity": "sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.4.tgz", + "integrity": "sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-linux-arm": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.5.tgz", + "integrity": "sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==", + "cpu": [ + "arm" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.5.tgz", + "integrity": "sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-ppc64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.34.5.tgz", + "integrity": "sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==", + "cpu": [ + "ppc64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-ppc64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-riscv64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.34.5.tgz", + "integrity": "sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==", + "cpu": [ + "riscv64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-riscv64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-s390x": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.5.tgz", + "integrity": "sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==", + "cpu": [ + "s390x" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-s390x": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.5.tgz", + "integrity": "sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-x64": "1.2.4" + } + }, + "node_modules/@img/sharp-linuxmusl-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.5.tgz", + "integrity": "sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-arm64": "1.2.4" + } + }, + "node_modules/@img/sharp-linuxmusl-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.5.tgz", + "integrity": "sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-x64": "1.2.4" + } + }, + "node_modules/@img/sharp-wasm32": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.5.tgz", + "integrity": "sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==", + "cpu": [ + "wasm32" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", + "optional": true, + "dependencies": { + "@emnapi/runtime": "^1.7.0" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.5.tgz", + "integrity": "sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-ia32": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.5.tgz", + "integrity": "sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==", + "cpu": [ + "ia32" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.5.tgz", + "integrity": "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@next/env": { + "version": "16.1.6", + "resolved": "https://registry.npmjs.org/@next/env/-/env-16.1.6.tgz", + "integrity": "sha512-N1ySLuZjnAtN3kFnwhAwPvZah8RJxKasD7x1f8shFqhncnWZn4JMfg37diLNuoHsLAlrDfM3g4mawVdtAG8XLQ==", + "license": "MIT" + }, + "node_modules/@next/swc-darwin-arm64": { + "version": "16.1.6", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-16.1.6.tgz", + "integrity": "sha512-wTzYulosJr/6nFnqGW7FrG3jfUUlEf8UjGA0/pyypJl42ExdVgC6xJgcXQ+V8QFn6niSG2Pb8+MIG1mZr2vczw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-darwin-x64": { + "version": "16.1.6", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-16.1.6.tgz", + "integrity": "sha512-BLFPYPDO+MNJsiDWbeVzqvYd4NyuRrEYVB5k2N3JfWncuHAy2IVwMAOlVQDFjj+krkWzhY2apvmekMkfQR0CUQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-arm64-gnu": { + "version": "16.1.6", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-16.1.6.tgz", + "integrity": "sha512-OJYkCd5pj/QloBvoEcJ2XiMnlJkRv9idWA/j0ugSuA34gMT6f5b7vOiCQHVRpvStoZUknhl6/UxOXL4OwtdaBw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-arm64-musl": { + "version": "16.1.6", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-16.1.6.tgz", + "integrity": "sha512-S4J2v+8tT3NIO9u2q+S0G5KdvNDjXfAv06OhfOzNDaBn5rw84DGXWndOEB7d5/x852A20sW1M56vhC/tRVbccQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-x64-gnu": { + "version": "16.1.6", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-16.1.6.tgz", + "integrity": "sha512-2eEBDkFlMMNQnkTyPBhQOAyn2qMxyG2eE7GPH2WIDGEpEILcBPI/jdSv4t6xupSP+ot/jkfrCShLAa7+ZUPcJQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-x64-musl": { + "version": "16.1.6", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-16.1.6.tgz", + "integrity": "sha512-oicJwRlyOoZXVlxmIMaTq7f8pN9QNbdes0q2FXfRsPhfCi8n8JmOZJm5oo1pwDaFbnnD421rVU409M3evFbIqg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-arm64-msvc": { + "version": "16.1.6", + "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-16.1.6.tgz", + "integrity": "sha512-gQmm8izDTPgs+DCWH22kcDmuUp7NyiJgEl18bcr8irXA5N2m2O+JQIr6f3ct42GOs9c0h8QF3L5SzIxcYAAXXw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-x64-msvc": { + "version": "16.1.6", + "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-16.1.6.tgz", + "integrity": "sha512-NRfO39AIrzBnixKbjuo2YiYhB6o9d8v/ymU9m/Xk8cyVk+k7XylniXkHwjs4s70wedVffc6bQNbufk5v0xEm0A==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@swc/helpers": { + "version": "0.5.15", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz", + "integrity": "sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.8.0" + } + }, + "node_modules/@tailwindcss/node": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.2.1.tgz", + "integrity": "sha512-jlx6sLk4EOwO6hHe1oCGm1Q4AN/s0rSrTTPBGPM0/RQ6Uylwq17FuU8IeJJKEjtc6K6O07zsvP+gDO6MMWo7pg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/remapping": "^2.3.5", + "enhanced-resolve": "^5.19.0", + "jiti": "^2.6.1", + "lightningcss": "1.31.1", + "magic-string": "^0.30.21", + "source-map-js": "^1.2.1", + "tailwindcss": "4.2.1" + } + }, + "node_modules/@tailwindcss/oxide": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.2.1.tgz", + "integrity": "sha512-yv9jeEFWnjKCI6/T3Oq50yQEOqmpmpfzG1hcZsAOaXFQPfzWprWrlHSdGPEF3WQTi8zu8ohC9Mh9J470nT5pUw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 20" + }, + "optionalDependencies": { + "@tailwindcss/oxide-android-arm64": "4.2.1", + "@tailwindcss/oxide-darwin-arm64": "4.2.1", + "@tailwindcss/oxide-darwin-x64": "4.2.1", + "@tailwindcss/oxide-freebsd-x64": "4.2.1", + "@tailwindcss/oxide-linux-arm-gnueabihf": "4.2.1", + "@tailwindcss/oxide-linux-arm64-gnu": "4.2.1", + "@tailwindcss/oxide-linux-arm64-musl": "4.2.1", + "@tailwindcss/oxide-linux-x64-gnu": "4.2.1", + "@tailwindcss/oxide-linux-x64-musl": "4.2.1", + "@tailwindcss/oxide-wasm32-wasi": "4.2.1", + "@tailwindcss/oxide-win32-arm64-msvc": "4.2.1", + "@tailwindcss/oxide-win32-x64-msvc": "4.2.1" + } + }, + "node_modules/@tailwindcss/oxide-android-arm64": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.2.1.tgz", + "integrity": "sha512-eZ7G1Zm5EC8OOKaesIKuw77jw++QJ2lL9N+dDpdQiAB/c/B2wDh0QPFHbkBVrXnwNugvrbJFk1gK2SsVjwWReg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-darwin-arm64": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.2.1.tgz", + "integrity": "sha512-q/LHkOstoJ7pI1J0q6djesLzRvQSIfEto148ppAd+BVQK0JYjQIFSK3JgYZJa+Yzi0DDa52ZsQx2rqytBnf8Hw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-darwin-x64": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.2.1.tgz", + "integrity": "sha512-/f/ozlaXGY6QLbpvd/kFTro2l18f7dHKpB+ieXz+Cijl4Mt9AI2rTrpq7V+t04nK+j9XBQHnSMdeQRhbGyt6fw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-freebsd-x64": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.2.1.tgz", + "integrity": "sha512-5e/AkgYJT/cpbkys/OU2Ei2jdETCLlifwm7ogMC7/hksI2fC3iiq6OcXwjibcIjPung0kRtR3TxEITkqgn0TcA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.2.1.tgz", + "integrity": "sha512-Uny1EcVTTmerCKt/1ZuKTkb0x8ZaiuYucg2/kImO5A5Y/kBz41/+j0gxUZl+hTF3xkWpDmHX+TaWhOtba2Fyuw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-gnu": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.2.1.tgz", + "integrity": "sha512-CTrwomI+c7n6aSSQlsPL0roRiNMDQ/YzMD9EjcR+H4f0I1SQ8QqIuPnsVp7QgMkC1Qi8rtkekLkOFjo7OlEFRQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-musl": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.2.1.tgz", + "integrity": "sha512-WZA0CHRL/SP1TRbA5mp9htsppSEkWuQ4KsSUumYQnyl8ZdT39ntwqmz4IUHGN6p4XdSlYfJwM4rRzZLShHsGAQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-gnu": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.2.1.tgz", + "integrity": "sha512-qMFzxI2YlBOLW5PhblzuSWlWfwLHaneBE0xHzLrBgNtqN6mWfs+qYbhryGSXQjFYB1Dzf5w+LN5qbUTPhW7Y5g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-musl": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.2.1.tgz", + "integrity": "sha512-5r1X2FKnCMUPlXTWRYpHdPYUY6a1Ar/t7P24OuiEdEOmms5lyqjDRvVY1yy9Rmioh+AunQ0rWiOTPE8F9A3v5g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.2.1.tgz", + "integrity": "sha512-MGFB5cVPvshR85MTJkEvqDUnuNoysrsRxd6vnk1Lf2tbiqNlXpHYZqkqOQalydienEWOHHFyyuTSYRsLfxFJ2Q==", + "bundleDependencies": [ + "@napi-rs/wasm-runtime", + "@emnapi/core", + "@emnapi/runtime", + "@tybys/wasm-util", + "@emnapi/wasi-threads", + "tslib" + ], + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.8.1", + "@emnapi/runtime": "^1.8.1", + "@emnapi/wasi-threads": "^1.1.0", + "@napi-rs/wasm-runtime": "^1.1.1", + "@tybys/wasm-util": "^0.10.1", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.2.1.tgz", + "integrity": "sha512-YlUEHRHBGnCMh4Nj4GnqQyBtsshUPdiNroZj8VPkvTZSoHsilRCwXcVKnG9kyi0ZFAS/3u+qKHBdDc81SADTRA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-win32-x64-msvc": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.2.1.tgz", + "integrity": "sha512-rbO34G5sMWWyrN/idLeVxAZgAKWrn5LiR3/I90Q9MkA67s6T1oB0xtTe+0heoBvHSpbU9Mk7i6uwJnpo4u21XQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/postcss": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/postcss/-/postcss-4.2.1.tgz", + "integrity": "sha512-OEwGIBnXnj7zJeonOh6ZG9woofIjGrd2BORfvE5p9USYKDCZoQmfqLcfNiRWoJlRWLdNPn2IgVZuWAOM4iTYMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@alloc/quick-lru": "^5.2.0", + "@tailwindcss/node": "4.2.1", + "@tailwindcss/oxide": "4.2.1", + "postcss": "^8.5.6", + "tailwindcss": "4.2.1" + } + }, + "node_modules/@types/node": { + "version": "20.19.37", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.37.tgz", + "integrity": "sha512-8kzdPJ3FsNsVIurqBs7oodNnCEVbni9yUEkaHbgptDACOPW04jimGagZ51E6+lXUwJjgnBw+hyko/lkFWCldqw==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@types/react": { + "version": "19.2.14", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz", + "integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==", + "dev": true, + "license": "MIT", + "dependencies": { + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "19.2.3", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz", + "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^19.2.0" + } + }, + "node_modules/baseline-browser-mapping": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.0.tgz", + "integrity": "sha512-lIyg0szRfYbiy67j9KN8IyeD7q7hcmqnJ1ddWmNt19ItGpNN64mnllmxUNFIOdOm6by97jlL6wfpTTJrmnjWAA==", + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001777", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001777.tgz", + "integrity": "sha512-tmN+fJxroPndC74efCdp12j+0rk0RHwV5Jwa1zWaFVyw2ZxAuPeG8ZgWC3Wz7uSjT3qMRQ5XHZ4COgQmsCMJAQ==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/client-only": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz", + "integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==", + "license": "MIT" + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "devOptional": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/enhanced-resolve": { + "version": "5.20.0", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.20.0.tgz", + "integrity": "sha512-/ce7+jQ1PQ6rVXwe+jKEg5hW5ciicHwIQUagZkp6IufBoY3YDgdTTY1azVs0qoRgVmvsNB+rbjLJxDAeHHtwsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.3.0" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/jiti": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz", + "integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==", + "dev": true, + "license": "MIT", + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, + "node_modules/lightningcss": { + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.31.1.tgz", + "integrity": "sha512-l51N2r93WmGUye3WuFoN5k10zyvrVs0qfKBhyC5ogUQ6Ew6JUSswh78mbSO+IU3nTWsyOArqPCcShdQSadghBQ==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.31.1", + "lightningcss-darwin-arm64": "1.31.1", + "lightningcss-darwin-x64": "1.31.1", + "lightningcss-freebsd-x64": "1.31.1", + "lightningcss-linux-arm-gnueabihf": "1.31.1", + "lightningcss-linux-arm64-gnu": "1.31.1", + "lightningcss-linux-arm64-musl": "1.31.1", + "lightningcss-linux-x64-gnu": "1.31.1", + "lightningcss-linux-x64-musl": "1.31.1", + "lightningcss-win32-arm64-msvc": "1.31.1", + "lightningcss-win32-x64-msvc": "1.31.1" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.31.1.tgz", + "integrity": "sha512-HXJF3x8w9nQ4jbXRiNppBCqeZPIAfUo8zE/kOEGbW5NZvGc/K7nMxbhIr+YlFlHW5mpbg/YFPdbnCh1wAXCKFg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.31.1.tgz", + "integrity": "sha512-02uTEqf3vIfNMq3h/z2cJfcOXnQ0GRwQrkmPafhueLb2h7mqEidiCzkE4gBMEH65abHRiQvhdcQ+aP0D0g67sg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.31.1.tgz", + "integrity": "sha512-1ObhyoCY+tGxtsz1lSx5NXCj3nirk0Y0kB/g8B8DT+sSx4G9djitg9ejFnjb3gJNWo7qXH4DIy2SUHvpoFwfTA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.31.1.tgz", + "integrity": "sha512-1RINmQKAItO6ISxYgPwszQE1BrsVU5aB45ho6O42mu96UiZBxEXsuQ7cJW4zs4CEodPUioj/QrXW1r9pLUM74A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.31.1.tgz", + "integrity": "sha512-OOCm2//MZJ87CdDK62rZIu+aw9gBv4azMJuA8/KB74wmfS3lnC4yoPHm0uXZ/dvNNHmnZnB8XLAZzObeG0nS1g==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.31.1.tgz", + "integrity": "sha512-WKyLWztD71rTnou4xAD5kQT+982wvca7E6QoLpoawZ1gP9JM0GJj4Tp5jMUh9B3AitHbRZ2/H3W5xQmdEOUlLg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.31.1.tgz", + "integrity": "sha512-mVZ7Pg2zIbe3XlNbZJdjs86YViQFoJSpc41CbVmKBPiGmC4YrfeOyz65ms2qpAobVd7WQsbW4PdsSJEMymyIMg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.31.1.tgz", + "integrity": "sha512-xGlFWRMl+0KvUhgySdIaReQdB4FNudfUTARn7q0hh/V67PVGCs3ADFjw+6++kG1RNd0zdGRlEKa+T13/tQjPMA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.31.1.tgz", + "integrity": "sha512-eowF8PrKHw9LpoZii5tdZwnBcYDxRw2rRCyvAXLi34iyeYfqCQNA9rmUM0ce62NlPhCvof1+9ivRaTY6pSKDaA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.31.1.tgz", + "integrity": "sha512-aJReEbSEQzx1uBlQizAOBSjcmr9dCdL3XuC/6HLXAxmtErsj2ICo5yYggg1qOODQMtnjNQv2UHb9NpOuFtYe4w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.31.1.tgz", + "integrity": "sha512-I9aiFrbd7oYHwlnQDqr1Roz+fTz61oDDJX7n9tYF9FJymH1cIN1DtKw3iYt6b8WZgEjoNwVSncwF4wx/ZedMhw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/next": { + "version": "16.1.6", + "resolved": "https://registry.npmjs.org/next/-/next-16.1.6.tgz", + "integrity": "sha512-hkyRkcu5x/41KoqnROkfTm2pZVbKxvbZRuNvKXLRXxs3VfyO0WhY50TQS40EuKO9SW3rBj/sF3WbVwDACeMZyw==", + "license": "MIT", + "dependencies": { + "@next/env": "16.1.6", + "@swc/helpers": "0.5.15", + "baseline-browser-mapping": "^2.8.3", + "caniuse-lite": "^1.0.30001579", + "postcss": "8.4.31", + "styled-jsx": "5.1.6" + }, + "bin": { + "next": "dist/bin/next" + }, + "engines": { + "node": ">=20.9.0" + }, + "optionalDependencies": { + "@next/swc-darwin-arm64": "16.1.6", + "@next/swc-darwin-x64": "16.1.6", + "@next/swc-linux-arm64-gnu": "16.1.6", + "@next/swc-linux-arm64-musl": "16.1.6", + "@next/swc-linux-x64-gnu": "16.1.6", + "@next/swc-linux-x64-musl": "16.1.6", + "@next/swc-win32-arm64-msvc": "16.1.6", + "@next/swc-win32-x64-msvc": "16.1.6", + "sharp": "^0.34.4" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.1.0", + "@playwright/test": "^1.51.1", + "babel-plugin-react-compiler": "*", + "react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", + "react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", + "sass": "^1.3.0" + }, + "peerDependenciesMeta": { + "@opentelemetry/api": { + "optional": true + }, + "@playwright/test": { + "optional": true + }, + "babel-plugin-react-compiler": { + "optional": true + }, + "sass": { + "optional": true + } + } + }, + "node_modules/next/node_modules/postcss": { + "version": "8.4.31", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", + "integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.6", + "picocolors": "^1.0.0", + "source-map-js": "^1.0.2" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/postcss": { + "version": "8.5.8", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.8.tgz", + "integrity": "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/react": { + "version": "19.2.3", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.3.tgz", + "integrity": "sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.2.3", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.3.tgz", + "integrity": "sha512-yELu4WmLPw5Mr/lmeEpox5rw3RETacE++JgHqQzd2dg+YbJuat3jH4ingc+WPZhxaoFzdv9y33G+F7Nl5O0GBg==", + "license": "MIT", + "dependencies": { + "scheduler": "^0.27.0" + }, + "peerDependencies": { + "react": "^19.2.3" + } + }, + "node_modules/scheduler": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", + "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "license": "ISC", + "optional": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/sharp": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.5.tgz", + "integrity": "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==", + "hasInstallScript": true, + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "@img/colour": "^1.0.0", + "detect-libc": "^2.1.2", + "semver": "^7.7.3" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-darwin-arm64": "0.34.5", + "@img/sharp-darwin-x64": "0.34.5", + "@img/sharp-libvips-darwin-arm64": "1.2.4", + "@img/sharp-libvips-darwin-x64": "1.2.4", + "@img/sharp-libvips-linux-arm": "1.2.4", + "@img/sharp-libvips-linux-arm64": "1.2.4", + "@img/sharp-libvips-linux-ppc64": "1.2.4", + "@img/sharp-libvips-linux-riscv64": "1.2.4", + "@img/sharp-libvips-linux-s390x": "1.2.4", + "@img/sharp-libvips-linux-x64": "1.2.4", + "@img/sharp-libvips-linuxmusl-arm64": "1.2.4", + "@img/sharp-libvips-linuxmusl-x64": "1.2.4", + "@img/sharp-linux-arm": "0.34.5", + "@img/sharp-linux-arm64": "0.34.5", + "@img/sharp-linux-ppc64": "0.34.5", + "@img/sharp-linux-riscv64": "0.34.5", + "@img/sharp-linux-s390x": "0.34.5", + "@img/sharp-linux-x64": "0.34.5", + "@img/sharp-linuxmusl-arm64": "0.34.5", + "@img/sharp-linuxmusl-x64": "0.34.5", + "@img/sharp-wasm32": "0.34.5", + "@img/sharp-win32-arm64": "0.34.5", + "@img/sharp-win32-ia32": "0.34.5", + "@img/sharp-win32-x64": "0.34.5" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/styled-jsx": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.1.6.tgz", + "integrity": "sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA==", + "license": "MIT", + "dependencies": { + "client-only": "0.0.1" + }, + "engines": { + "node": ">= 12.0.0" + }, + "peerDependencies": { + "react": ">= 16.8.0 || 17.x.x || ^18.0.0-0 || ^19.0.0-0" + }, + "peerDependenciesMeta": { + "@babel/core": { + "optional": true + }, + "babel-plugin-macros": { + "optional": true + } + } + }, + "node_modules/tailwindcss": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.2.1.tgz", + "integrity": "sha512-/tBrSQ36vCleJkAOsy9kbNTgaxvGbyOamC30PRePTQe/o1MFwEKHQk4Cn7BNGaPtjp+PuUrByJehM1hgxfq4sw==", + "dev": true, + "license": "MIT" + }, + "node_modules/tapable": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.0.tgz", + "integrity": "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + } + } +} diff --git a/Les06-NextJS-QuickPoll-Part2/quickpoll-les6-starter/package.json b/Les06-NextJS-QuickPoll-Part2/quickpoll-les6-starter/package.json new file mode 100644 index 0000000..031dcdd --- /dev/null +++ b/Les06-NextJS-QuickPoll-Part2/quickpoll-les6-starter/package.json @@ -0,0 +1,23 @@ +{ + "name": "quickpoll", + "version": "0.1.0", + "private": true, + "scripts": { + "dev": "next dev", + "build": "next build", + "start": "next start" + }, + "dependencies": { + "next": "16.1.6", + "react": "19.2.3", + "react-dom": "19.2.3" + }, + "devDependencies": { + "@tailwindcss/postcss": "^4", + "@types/node": "^20", + "@types/react": "^19", + "@types/react-dom": "^19", + "tailwindcss": "^4", + "typescript": "^5" + } +} diff --git a/Les06-NextJS-QuickPoll-Part2/quickpoll-les6-starter/postcss.config.mjs b/Les06-NextJS-QuickPoll-Part2/quickpoll-les6-starter/postcss.config.mjs new file mode 100644 index 0000000..61e3684 --- /dev/null +++ b/Les06-NextJS-QuickPoll-Part2/quickpoll-les6-starter/postcss.config.mjs @@ -0,0 +1,7 @@ +const config = { + plugins: { + "@tailwindcss/postcss": {}, + }, +}; + +export default config; diff --git a/Les06-NextJS-QuickPoll-Part2/quickpoll-les6-starter/public/file.svg b/Les06-NextJS-QuickPoll-Part2/quickpoll-les6-starter/public/file.svg new file mode 100644 index 0000000..004145c --- /dev/null +++ b/Les06-NextJS-QuickPoll-Part2/quickpoll-les6-starter/public/file.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/Les06-NextJS-QuickPoll-Part2/quickpoll-les6-starter/public/globe.svg b/Les06-NextJS-QuickPoll-Part2/quickpoll-les6-starter/public/globe.svg new file mode 100644 index 0000000..567f17b --- /dev/null +++ b/Les06-NextJS-QuickPoll-Part2/quickpoll-les6-starter/public/globe.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/Les06-NextJS-QuickPoll-Part2/quickpoll-les6-starter/public/next.svg b/Les06-NextJS-QuickPoll-Part2/quickpoll-les6-starter/public/next.svg new file mode 100644 index 0000000..5174b28 --- /dev/null +++ b/Les06-NextJS-QuickPoll-Part2/quickpoll-les6-starter/public/next.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/Les06-NextJS-QuickPoll-Part2/quickpoll-les6-starter/public/vercel.svg b/Les06-NextJS-QuickPoll-Part2/quickpoll-les6-starter/public/vercel.svg new file mode 100644 index 0000000..7705396 --- /dev/null +++ b/Les06-NextJS-QuickPoll-Part2/quickpoll-les6-starter/public/vercel.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/Les06-NextJS-QuickPoll-Part2/quickpoll-les6-starter/public/window.svg b/Les06-NextJS-QuickPoll-Part2/quickpoll-les6-starter/public/window.svg new file mode 100644 index 0000000..b2b2a44 --- /dev/null +++ b/Les06-NextJS-QuickPoll-Part2/quickpoll-les6-starter/public/window.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/Les06-NextJS-QuickPoll-Part2/quickpoll-les6-starter/src/app/api/polls/[id]/route.ts b/Les06-NextJS-QuickPoll-Part2/quickpoll-les6-starter/src/app/api/polls/[id]/route.ts new file mode 100644 index 0000000..229a48a --- /dev/null +++ b/Les06-NextJS-QuickPoll-Part2/quickpoll-les6-starter/src/app/api/polls/[id]/route.ts @@ -0,0 +1,27 @@ +import { NextResponse } from "next/server"; +import { getPollById } from "@/lib/data"; +import type { Poll } from "@/types"; + +interface RouteParams { + params: Promise<{ id: string }>; +} + +// STAP 3: GET /api/polls/[id] — enkele poll ophalen +// (Dit bestand is COMPLEET van Les 5. Dit is stap 3 van Les 5.) + +export async function GET( + request: Request, + { params }: RouteParams +): Promise { + const { id } = await params; + const poll = getPollById(id); + + if (!poll) { + return NextResponse.json( + { error: "Poll niet gevonden" }, + { status: 404 } + ); + } + + return NextResponse.json(poll); +} diff --git a/Les06-NextJS-QuickPoll-Part2/quickpoll-les6-starter/src/app/api/polls/[id]/vote/route.ts b/Les06-NextJS-QuickPoll-Part2/quickpoll-les6-starter/src/app/api/polls/[id]/vote/route.ts new file mode 100644 index 0000000..a68e040 --- /dev/null +++ b/Les06-NextJS-QuickPoll-Part2/quickpoll-les6-starter/src/app/api/polls/[id]/vote/route.ts @@ -0,0 +1,28 @@ +import { NextResponse } from "next/server"; +import { votePoll } from "@/lib/data"; + +interface RouteParams { + params: Promise<{ id: string }>; +} + +interface VoteBody { + optionIndex: number; +} + +// STAP 4: POST /api/polls/[id]/vote — stem uitbrengen +// +// Wat moet je doen? +// 1. Haal het id op uit params (await!) +// 2. Lees de request body en cast naar VoteBody +// 3. Valideer: is optionIndex een number? +// 4. Roep votePoll(id, body.optionIndex) aan +// 5. Als het resultaat undefined is: return 404 +// 6. Anders: return de geüpdatete poll als JSON + +export async function POST( + request: Request, + { params }: RouteParams +): Promise { + // Implementeer je POST handler hier + return NextResponse.json({ error: "Nog niet geimplementeerd" }, { status: 501 }); +} diff --git a/Les06-NextJS-QuickPoll-Part2/quickpoll-les6-starter/src/app/api/polls/route.ts b/Les06-NextJS-QuickPoll-Part2/quickpoll-les6-starter/src/app/api/polls/route.ts new file mode 100644 index 0000000..0f647ef --- /dev/null +++ b/Les06-NextJS-QuickPoll-Part2/quickpoll-les6-starter/src/app/api/polls/route.ts @@ -0,0 +1,24 @@ +import { NextResponse } from "next/server"; +import { getPolls, createPoll } from "@/lib/data"; +import type { Poll, CreatePollBody } from "@/types"; + +// GET /api/polls — alle polls ophalen +export async function GET(): Promise> { + const polls = getPolls(); + return NextResponse.json(polls); +} + +// POST /api/polls — nieuwe poll aanmaken +export async function POST(request: Request): Promise { + const body: CreatePollBody = await request.json(); + + if (!body.question || !body.options || body.options.length < 2) { + return NextResponse.json( + { error: "Vraag en minstens 2 opties zijn verplicht" }, + { status: 400 } + ); + } + + const newPoll = createPoll(body.question, body.options); + return NextResponse.json(newPoll, { status: 201 }); +} diff --git a/Les06-NextJS-QuickPoll-Part2/quickpoll-les6-starter/src/app/create/page.tsx b/Les06-NextJS-QuickPoll-Part2/quickpoll-les6-starter/src/app/create/page.tsx new file mode 100644 index 0000000..f27f872 --- /dev/null +++ b/Les06-NextJS-QuickPoll-Part2/quickpoll-les6-starter/src/app/create/page.tsx @@ -0,0 +1,32 @@ +"use client"; + +import { useState } from "react"; +import { useRouter } from "next/navigation"; + +// BONUS: Maak een formulier om een nieuwe poll aan te maken +// +// Benodigde state: +// - question: string +// - options: string[] (start met ["", ""]) +// - isSubmitting: boolean +// - error: string | null +// +// Wat moet je bouwen? +// 1. Een input voor de vraag +// 2. Inputs voor de opties (minimaal 2, maximaal 6) +// 3. Knoppen om opties toe te voegen/verwijderen +// 4. Een submit knop die POST naar /api/polls +// 5. Na success: redirect naar / met router.push("/") + +export default function CreatePollPage() { + return ( +
+

+ Nieuwe Poll Aanmaken +

+

+ Bonus: bouw hier het create formulier +

+
+ ); +} diff --git a/Les06-NextJS-QuickPoll-Part2/quickpoll-les6-starter/src/app/error.tsx b/Les06-NextJS-QuickPoll-Part2/quickpoll-les6-starter/src/app/error.tsx new file mode 100644 index 0000000..8d17fc4 --- /dev/null +++ b/Les06-NextJS-QuickPoll-Part2/quickpoll-les6-starter/src/app/error.tsx @@ -0,0 +1,30 @@ +"use client"; + +// STAP 7: Error boundary +// +// Dit bestand vangt fouten op in de route. +// MOET een client component zijn ("use client" staat al bovenaan). +// +// Props die je krijgt: +// - error: Error — het error object met .message +// - reset: () => void — functie om de pagina opnieuw te proberen +// +// Bouw een nette error pagina met: +// - Een titel "Er ging iets mis!" +// - De error message +// - Een "Probeer opnieuw" knop die reset() aanroept + +export default function Error({ + error, + reset, +}: { + error: Error; + reset: () => void; +}) { + return ( +
+

Er ging iets mis: {error.message}

+ +
+ ); +} diff --git a/Les06-NextJS-QuickPoll-Part2/quickpoll-les6-starter/src/app/favicon.ico b/Les06-NextJS-QuickPoll-Part2/quickpoll-les6-starter/src/app/favicon.ico new file mode 100644 index 0000000..718d6fe Binary files /dev/null and b/Les06-NextJS-QuickPoll-Part2/quickpoll-les6-starter/src/app/favicon.ico differ diff --git a/Les06-NextJS-QuickPoll-Part2/quickpoll-les6-starter/src/app/globals.css b/Les06-NextJS-QuickPoll-Part2/quickpoll-les6-starter/src/app/globals.css new file mode 100644 index 0000000..f1d8c73 --- /dev/null +++ b/Les06-NextJS-QuickPoll-Part2/quickpoll-les6-starter/src/app/globals.css @@ -0,0 +1 @@ +@import "tailwindcss"; diff --git a/Les06-NextJS-QuickPoll-Part2/quickpoll-les6-starter/src/app/layout.tsx b/Les06-NextJS-QuickPoll-Part2/quickpoll-les6-starter/src/app/layout.tsx new file mode 100644 index 0000000..3252dda --- /dev/null +++ b/Les06-NextJS-QuickPoll-Part2/quickpoll-les6-starter/src/app/layout.tsx @@ -0,0 +1,36 @@ +import type { Metadata } from "next"; +import Link from "next/link"; +import "./globals.css"; + +export const metadata: Metadata = { + title: "QuickPoll — Stem op alles", + description: "Maak en deel polls met je vrienden", +}; + +export default function RootLayout({ + children, +}: { + children: React.ReactNode; +}) { + return ( + + + {/* + STAP 1: Bouw hier een navigatiebalk met: + - Logo/titel "QuickPoll" (links) die linkt naar / + - Een link naar / ("Polls") + - Een link naar /create ("Nieuwe Poll") + + Tip: gebruik van "next/link", niet
+ Tip: gebruik Tailwind classes voor styling + */} + +
{children}
+ +
+ © 2025 QuickPoll — NOVI Hogeschool Les 5 +
+ + + ); +} diff --git a/Les06-NextJS-QuickPoll-Part2/quickpoll-les6-starter/src/app/loading.tsx b/Les06-NextJS-QuickPoll-Part2/quickpoll-les6-starter/src/app/loading.tsx new file mode 100644 index 0000000..b70a23c --- /dev/null +++ b/Les06-NextJS-QuickPoll-Part2/quickpoll-les6-starter/src/app/loading.tsx @@ -0,0 +1,17 @@ +// STAP 7: Loading state +// +// Dit bestand wordt automatisch getoond terwijl een pagina laadt. +// Bouw een skeleton loader met Tailwind's animate-pulse class. +// +// Voorbeeld: +//
+//
+//
+ +export default function Loading() { + return ( +
+

Laden...

+
+ ); +} diff --git a/Les06-NextJS-QuickPoll-Part2/quickpoll-les6-starter/src/app/not-found.tsx b/Les06-NextJS-QuickPoll-Part2/quickpoll-les6-starter/src/app/not-found.tsx new file mode 100644 index 0000000..bd09789 --- /dev/null +++ b/Les06-NextJS-QuickPoll-Part2/quickpoll-les6-starter/src/app/not-found.tsx @@ -0,0 +1,18 @@ +import Link from "next/link"; + +// STAP 7: Not found pagina +// +// Wordt getoond als een pagina niet bestaat. +// Bouw een nette 404 pagina met een link terug naar home. + +export default function NotFound() { + return ( +
+

404

+

Deze pagina bestaat niet.

+ + Terug naar home + +
+ ); +} diff --git a/Les06-NextJS-QuickPoll-Part2/quickpoll-les6-starter/src/app/page.tsx b/Les06-NextJS-QuickPoll-Part2/quickpoll-les6-starter/src/app/page.tsx new file mode 100644 index 0000000..9ddc506 --- /dev/null +++ b/Les06-NextJS-QuickPoll-Part2/quickpoll-les6-starter/src/app/page.tsx @@ -0,0 +1,31 @@ +import Link from "next/link"; +import { getPolls } from "@/lib/data"; +import type { Poll } from "@/types"; + +export const dynamic = "force-dynamic"; + +export default function HomePage() { + // STAP 2: Haal alle polls op met getPolls() + // Dit is een Server Component — je kunt gewoon functies aanroepen! + + return ( +
+

Actieve Polls

+

Klik op een poll om te stemmen

+ +
+ {/* + STAP 2: Map over de polls en toon voor elke poll: + - De vraag (poll.question) + - Het aantal opties en stemmen + - De opties als tags/badges + - Wrap het in een naar /poll/{poll.id} + + Tip: maak een helper functie voor het totaal aantal stemmen: + const totalVotes = (poll: Poll): number => + poll.votes.reduce((sum, v) => sum + v, 0); + */} +
+
+ ); +} diff --git a/Les06-NextJS-QuickPoll-Part2/quickpoll-les6-starter/src/app/poll/[id]/error.tsx b/Les06-NextJS-QuickPoll-Part2/quickpoll-les6-starter/src/app/poll/[id]/error.tsx new file mode 100644 index 0000000..f3a2d67 --- /dev/null +++ b/Les06-NextJS-QuickPoll-Part2/quickpoll-les6-starter/src/app/poll/[id]/error.tsx @@ -0,0 +1,28 @@ +"use client"; + +// STAP 7: Error boundary voor poll detail pagina +// +// Dit bestand vangt fouten op in deze route. + +export default function ErrorPoll({ + error, + reset, +}: { + error: Error & { digest?: string }; + reset: () => void; +}) { + return ( +
+

+ Oeps! Iets ging fout +

+

{error.message}

+ +
+ ); +} diff --git a/Les06-NextJS-QuickPoll-Part2/quickpoll-les6-starter/src/app/poll/[id]/loading.tsx b/Les06-NextJS-QuickPoll-Part2/quickpoll-les6-starter/src/app/poll/[id]/loading.tsx new file mode 100644 index 0000000..d1cb823 --- /dev/null +++ b/Les06-NextJS-QuickPoll-Part2/quickpoll-les6-starter/src/app/poll/[id]/loading.tsx @@ -0,0 +1,20 @@ +// STAP 7: Loading state voor poll detail pagina +// +// Dit bestand wordt automatisch getoond terwijl de poll detail pagina laadt. +// Bouw een skeleton loader die lijkt op de echte content. + +export default function LoadingPoll() { + return ( +
+
+
+ {[1, 2, 3].map((i) => ( +
+ ))} +
+
+ ); +} diff --git a/Les06-NextJS-QuickPoll-Part2/quickpoll-les6-starter/src/app/poll/[id]/not-found.tsx b/Les06-NextJS-QuickPoll-Part2/quickpoll-les6-starter/src/app/poll/[id]/not-found.tsx new file mode 100644 index 0000000..35e5934 --- /dev/null +++ b/Les06-NextJS-QuickPoll-Part2/quickpoll-les6-starter/src/app/poll/[id]/not-found.tsx @@ -0,0 +1,17 @@ +import Link from "next/link"; + +export default function PollNotFound() { + return ( +
+

+ Poll niet gevonden +

+

+ Deze poll bestaat niet of is verwijderd. +

+ + Bekijk alle polls + +
+ ); +} diff --git a/Les06-NextJS-QuickPoll-Part2/quickpoll-les6-starter/src/app/poll/[id]/page.tsx b/Les06-NextJS-QuickPoll-Part2/quickpoll-les6-starter/src/app/poll/[id]/page.tsx new file mode 100644 index 0000000..75f46f9 --- /dev/null +++ b/Les06-NextJS-QuickPoll-Part2/quickpoll-les6-starter/src/app/poll/[id]/page.tsx @@ -0,0 +1,40 @@ +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 }>; +} + +// STAP 5: generateMetadata — dynamische pagina titel +// +// Deze functie genereert de tag voor SEO. +// Haal de poll op en return de vraag als titel. + +export async function generateMetadata({ params }: PageProps): Promise<Metadata> { + const { id } = await params; + const poll = getPollById(id); + + return { + title: poll ? `${poll.question} — QuickPoll` : "Poll niet gevonden", + }; +} + +// STAP 5: PollPage — de poll detail pagina +// +// Wat moet je doen? +// 1. Haal het id op uit params (await!) +// 2. Zoek de poll met getPollById(id) +// 3. Als de poll niet bestaat: roep notFound() aan +// 4. Render de poll vraag als <h1> +// 5. Render de <VoteForm poll={poll} /> component + +export default async function PollPage({ params }: PageProps) { + // Implementeer je pagina hier + return ( + <div className="max-w-2xl mx-auto"> + <p>Implementeer deze pagina (zie stap 5 in de opdracht)</p> + </div> + ); +} diff --git a/Les06-NextJS-QuickPoll-Part2/quickpoll-les6-starter/src/components/VoteForm.tsx b/Les06-NextJS-QuickPoll-Part2/quickpoll-les6-starter/src/components/VoteForm.tsx new file mode 100644 index 0000000..9dfbc53 --- /dev/null +++ b/Les06-NextJS-QuickPoll-Part2/quickpoll-les6-starter/src/components/VoteForm.tsx @@ -0,0 +1,129 @@ +"use client"; + +import { useState } from "react"; +import Link from "next/link"; +import type { Poll } from "@/types"; + +interface VoteFormProps { + poll: Poll; +} + +// STAP 6: VoteForm — de stem interface +// +// Dit is een CLIENT component ("use client" staat bovenaan). +// Hier mag je wel useState en onClick gebruiken! +// +// Benodigde state: +// - selectedOption: number | null (welke optie is geselecteerd) +// - hasVoted: boolean (heeft de gebruiker al gestemd) +// - isLoading: boolean (wordt het formulier verstuurd) +// - currentPoll: Poll (de huidige poll data, update na stemmen) +// +// Wat moet je bouwen? +// 1. Toon alle opties als klikbare knoppen (voor het stemmen) +// 2. Highlight de geselecteerde optie met purple border +// 3. Een "Stem!" knop die een POST doet naar /api/polls/{id}/vote +// 4. Na het stemmen: toon de resultaten met progress bars en percentages +// 5. Toon een "Terug" link naar de homepage + +export function VoteForm({ poll }: VoteFormProps) { + const [selectedOption, setSelectedOption] = useState<number | null>(null); + const [hasVoted, setHasVoted] = useState<boolean>(false); + const [isLoading, setIsLoading] = 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 || isLoading) return; + setIsLoading(true); + + try { + 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); + } + } finally { + setIsLoading(false); + } + } + + // Nog niet gestemd — toon opties + if (!hasVoted) { + return ( + <div className="space-y-4"> + {currentPoll.options.map((option, idx) => ( + <button + key={idx} + onClick={() => setSelectedOption(idx)} + disabled={isLoading} + className={`w-full text-left p-4 rounded-lg border-2 transition ${ + selectedOption === idx + ? "border-purple-500 bg-purple-50" + : "border-gray-200 hover:border-purple-300" + }`} + > + <span className="font-medium">{option}</span> + </button> + ))} + + <button + onClick={handleVote} + disabled={selectedOption === null || isLoading} + className="w-full bg-purple-600 text-white py-3 rounded-lg hover:bg-purple-700 disabled:opacity-50 disabled:cursor-not-allowed transition font-medium" + > + {isLoading ? "Aan het stemmen..." : "Stem!"} + </button> + </div> + ); + } + + // Wel gestemd — toon resultaten + return ( + <div className="space-y-6"> + <p className="text-green-600 font-medium text-lg"> + ✓ Bedankt voor je stem! + </p> + + <div className="space-y-4"> + {currentPoll.options.map((option, idx) => ( + <div key={idx}> + <div className="flex justify-between mb-2"> + <span className="font-medium">{option}</span> + <span className="text-sm text-gray-600"> + {currentPoll.votes[idx]} stemmen ({getPercentage(currentPoll.votes[idx])}%) + </span> + </div> + <div className="w-full bg-gray-200 rounded-full h-3"> + <div + className="bg-purple-600 h-3 rounded-full transition-all" + style={{ + width: `${getPercentage(currentPoll.votes[idx])}%`, + }} + /> + </div> + </div> + ))} + </div> + + <Link + href="/" + className="block text-center text-purple-600 hover:underline mt-6" + > + ← Terug naar polls + </Link> + </div> + ); +} diff --git a/Les06-NextJS-QuickPoll-Part2/quickpoll-les6-starter/src/lib/data.ts b/Les06-NextJS-QuickPoll-Part2/quickpoll-les6-starter/src/lib/data.ts new file mode 100644 index 0000000..3e36f41 --- /dev/null +++ b/Les06-NextJS-QuickPoll-Part2/quickpoll-les6-starter/src/lib/data.ts @@ -0,0 +1,55 @@ +import { Poll } from "@/types"; + +export const polls: Poll[] = [ + { + id: "1", + question: "Wat is de beste code editor?", + options: ["VS Code", "Cursor", "Vim", "WebStorm"], + votes: [12, 25, 5, 3], + }, + { + id: "2", + question: "Wat is de beste programmeertaal?", + options: ["TypeScript", "Python", "Rust", "Go"], + votes: [18, 15, 8, 4], + }, + { + id: "3", + question: "Welk framework heeft de toekomst?", + options: ["Next.js", "Remix", "Astro", "SvelteKit"], + votes: [22, 6, 10, 7], + }, +]; + +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 createPoll(question: string, options: string[]): Poll { + const newPoll: Poll = { + id: String(nextId++), + question, + options, + votes: new Array(options.length).fill(0), + }; + polls.push(newPoll); + return newPoll; +} + +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; +} diff --git a/Les06-NextJS-QuickPoll-Part2/quickpoll-les6-starter/src/middleware.ts b/Les06-NextJS-QuickPoll-Part2/quickpoll-les6-starter/src/middleware.ts new file mode 100644 index 0000000..76bbfab --- /dev/null +++ b/Les06-NextJS-QuickPoll-Part2/quickpoll-les6-starter/src/middleware.ts @@ -0,0 +1,17 @@ +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*"], +}; diff --git a/Les06-NextJS-QuickPoll-Part2/quickpoll-les6-starter/src/types/index.ts b/Les06-NextJS-QuickPoll-Part2/quickpoll-les6-starter/src/types/index.ts new file mode 100644 index 0000000..73f152f --- /dev/null +++ b/Les06-NextJS-QuickPoll-Part2/quickpoll-les6-starter/src/types/index.ts @@ -0,0 +1,11 @@ +export interface Poll { + id: string; + question: string; + options: string[]; + votes: number[]; +} + +export interface CreatePollBody { + question: string; + options: string[]; +} diff --git a/Les06-NextJS-QuickPoll-Part2/quickpoll-les6-starter/tsconfig.json b/Les06-NextJS-QuickPoll-Part2/quickpoll-les6-starter/tsconfig.json new file mode 100644 index 0000000..cf9c65d --- /dev/null +++ b/Les06-NextJS-QuickPoll-Part2/quickpoll-les6-starter/tsconfig.json @@ -0,0 +1,34 @@ +{ + "compilerOptions": { + "target": "ES2017", + "lib": ["dom", "dom.iterable", "esnext"], + "allowJs": true, + "skipLibCheck": true, + "strict": true, + "noEmit": true, + "esModuleInterop": true, + "module": "esnext", + "moduleResolution": "bundler", + "resolveJsonModule": true, + "isolatedModules": true, + "jsx": "react-jsx", + "incremental": true, + "plugins": [ + { + "name": "next" + } + ], + "paths": { + "@/*": ["./src/*"] + } + }, + "include": [ + "next-env.d.ts", + "**/*.ts", + "**/*.tsx", + ".next/types/**/*.ts", + ".next/dev/types/**/*.ts", + "**/*.mts" + ], + "exclude": ["node_modules"] +} diff --git a/Les06-NextJS-QuickPoll-Part2/zih5FOQz b/Les06-NextJS-QuickPoll-Part2/zih5FOQz new file mode 100644 index 0000000..7fcdb1a Binary files /dev/null and b/Les06-NextJS-QuickPoll-Part2/zih5FOQz differ diff --git a/Les06-NextJS-QuickPoll-Part2/zis78A0U b/Les06-NextJS-QuickPoll-Part2/zis78A0U new file mode 100644 index 0000000..e7c208f Binary files /dev/null and b/Les06-NextJS-QuickPoll-Part2/zis78A0U differ diff --git a/Les06-NextJS-QuickPoll-Part2/ziwfW1HM b/Les06-NextJS-QuickPoll-Part2/ziwfW1HM new file mode 100644 index 0000000..1779b67 Binary files /dev/null and b/Les06-NextJS-QuickPoll-Part2/ziwfW1HM differ