From 9ffdecf2c442c7073ad89e4b3b44439db7df49ae Mon Sep 17 00:00:00 2001 From: Tim Rijkse Date: Wed, 11 Mar 2026 14:07:00 +0100 Subject: [PATCH] fix: les 6 --- .../Les04-Docenttekst.md | 1696 +++++++---------- .../Les04-Lesopdracht.pdf | 52 +- .../Les04-Slide-Overzicht.md | 1684 +++++++++++----- .../les4-typescript-escaperoom-v2.zip | Bin 0 -> 9263 bytes Les04-TypeScript-Fundamentals/ziluoAeV | Bin 0 -> 9263 bytes Les05-NextJS-Basics/Les05-Docenttekst.md | 672 +++++++ Les05-NextJS-Basics/Les05-Lesopdracht.pdf | 296 +++ Les05-NextJS-Basics/Les05-Slide-Overzicht.md | 974 ++++++++++ .../les5-quickpoll-starter.zip | Bin 0 -> 40143 bytes .../les5-quickpoll-voorbeeld.zip | Bin 0 -> 40483 bytes Les05-NextJS-Basics/quickpoll 2/.cursorrules | 6 + Les05-NextJS-Basics/quickpoll 2/.gitignore | 41 + Les05-NextJS-Basics/quickpoll 2/README.md | 36 + .../quickpoll 2/next.config.ts | 7 + .../quickpoll 2/package-lock.json | 1664 ++++++++++++++++ Les05-NextJS-Basics/quickpoll 2/package.json | 23 + .../quickpoll 2/postcss.config.mjs | 7 + .../quickpoll 2/public/file.svg | 1 + .../quickpoll 2/public/globe.svg | 1 + .../quickpoll 2/public/next.svg | 1 + .../quickpoll 2/public/vercel.svg | 1 + .../quickpoll 2/public/window.svg | 1 + .../src/app/api/polls/[id]/route.ts | 24 + .../src/app/api/polls/[id]/vote/route.ts | 37 + .../quickpoll 2/src/app/api/polls/route.ts | 24 + .../quickpoll 2/src/app/create/page.tsx | 144 ++ .../quickpoll 2/src/app/error.tsx | 24 + .../quickpoll 2/src/app/favicon.ico | Bin 0 -> 25931 bytes .../quickpoll 2/src/app/globals.css | 1 + .../quickpoll 2/src/app/layout.tsx | 46 + .../quickpoll 2/src/app/loading.tsx | 24 + .../quickpoll 2/src/app/not-found.tsx | 18 + .../quickpoll 2/src/app/page.tsx | 57 + .../src/app/poll/[id]/not-found.tsx | 20 + .../quickpoll 2/src/app/poll/[id]/page.tsx | 40 + .../quickpoll 2/src/components/VoteForm.tsx | 128 ++ .../quickpoll 2/src/lib/data.ts | 55 + .../quickpoll 2/src/middleware.ts | 17 + .../quickpoll 2/src/types/index.ts | 11 + Les05-NextJS-Basics/quickpoll 2/tsconfig.json | 34 + .../quickpoll-starter/.cursorrules | 6 + .../quickpoll-starter/.gitignore | 41 + .../quickpoll-starter/README.md | 36 + .../quickpoll-starter/next.config.ts | 7 + .../quickpoll-starter/package-lock.json | 1664 ++++++++++++++++ .../quickpoll-starter/package.json | 23 + .../quickpoll-starter/postcss.config.mjs | 7 + .../quickpoll-starter/public/file.svg | 1 + .../quickpoll-starter/public/globe.svg | 1 + .../quickpoll-starter/public/next.svg | 1 + .../quickpoll-starter/public/vercel.svg | 1 + .../quickpoll-starter/public/window.svg | 1 + .../src/app/api/polls/[id]/route.ts | 24 + .../src/app/api/polls/[id]/vote/route.ts | 28 + .../src/app/api/polls/route.ts | 24 + .../quickpoll-starter/src/app/create/page.tsx | 32 + .../quickpoll-starter/src/app/error.tsx | 30 + .../quickpoll-starter/src/app/favicon.ico | Bin 0 -> 25931 bytes .../quickpoll-starter/src/app/globals.css | 1 + .../quickpoll-starter/src/app/layout.tsx | 36 + .../quickpoll-starter/src/app/loading.tsx | 17 + .../quickpoll-starter/src/app/not-found.tsx | 18 + .../quickpoll-starter/src/app/page.tsx | 31 + .../src/app/poll/[id]/not-found.tsx | 17 + .../src/app/poll/[id]/page.tsx | 41 + .../src/components/VoteForm.tsx | 89 + .../quickpoll-starter/src/lib/data.ts | 55 + .../quickpoll-starter/src/middleware.ts | 17 + .../quickpoll-starter/src/types/index.ts | 11 + .../quickpoll-starter/tsconfig.json | 34 + Les05-NextJS-Basics/quickpoll/.cursorrules | 6 + Les05-NextJS-Basics/quickpoll/.gitignore | 41 + Les05-NextJS-Basics/quickpoll/README.md | 36 + Les05-NextJS-Basics/quickpoll/next.config.ts | 7 + .../quickpoll/package-lock.json | 1664 ++++++++++++++++ Les05-NextJS-Basics/quickpoll/package.json | 23 + .../quickpoll/postcss.config.mjs | 7 + Les05-NextJS-Basics/quickpoll/public/file.svg | 1 + .../quickpoll/public/globe.svg | 1 + Les05-NextJS-Basics/quickpoll/public/next.svg | 1 + .../quickpoll/public/vercel.svg | 1 + .../quickpoll/public/window.svg | 1 + .../quickpoll/src/app/api/polls/[id]/route.ts | 24 + .../src/app/api/polls/[id]/vote/route.ts | 37 + .../quickpoll/src/app/api/polls/route.ts | 24 + .../quickpoll/src/app/create/page.tsx | 144 ++ .../quickpoll/src/app/error.tsx | 24 + .../quickpoll/src/app/favicon.ico | Bin 0 -> 25931 bytes .../quickpoll/src/app/globals.css | 1 + .../quickpoll/src/app/layout.tsx | 46 + .../quickpoll/src/app/loading.tsx | 24 + .../quickpoll/src/app/not-found.tsx | 18 + .../quickpoll/src/app/page.tsx | 57 + .../quickpoll/src/app/poll/[id]/not-found.tsx | 20 + .../quickpoll/src/app/poll/[id]/page.tsx | 40 + .../quickpoll/src/components/VoteForm.tsx | 128 ++ Les05-NextJS-Basics/quickpoll/src/lib/data.ts | 55 + .../quickpoll/src/middleware.ts | 17 + .../quickpoll/src/types/index.ts | 11 + Les05-NextJS-Basics/quickpoll/tsconfig.json | 34 + Samenvattingen/Les05-Samenvatting.md | 240 +-- Samenvattingen/Les06-Samenvatting.md | 368 +--- Samenvattingen/Les07-Samenvatting.md | 632 +++--- Samenvattingen/Les08-Samenvatting.md | 505 +++-- Samenvattingen/Les09-Samenvatting.md | 325 +--- Samenvattingen/Les10-Samenvatting.md | 170 +- Samenvattingen/Les11-Samenvatting.md | 308 ++- Samenvattingen/Les12-Samenvatting.md | 320 ++-- Samenvattingen/Les13-Samenvatting.md | 469 ++--- Samenvattingen/Les14-Samenvatting.md | 592 +++--- Samenvattingen/Les15-Samenvatting.md | 376 +--- Samenvattingen/Les16-Samenvatting.md | 372 +--- Samenvattingen/Les17-Samenvatting.md | 576 ++---- Samenvattingen/Les18-Samenvatting.md | 2 +- readme.md | 397 ++-- v1-feedback.md | 59 +- v2/README.md | 27 + 117 files changed, 13198 insertions(+), 5194 deletions(-) create mode 100644 Les04-TypeScript-Fundamentals/les4-typescript-escaperoom-v2.zip create mode 100644 Les04-TypeScript-Fundamentals/ziluoAeV create mode 100644 Les05-NextJS-Basics/Les05-Docenttekst.md create mode 100644 Les05-NextJS-Basics/Les05-Lesopdracht.pdf create mode 100644 Les05-NextJS-Basics/Les05-Slide-Overzicht.md create mode 100644 Les05-NextJS-Basics/les5-quickpoll-starter.zip create mode 100644 Les05-NextJS-Basics/les5-quickpoll-voorbeeld.zip create mode 100644 Les05-NextJS-Basics/quickpoll 2/.cursorrules create mode 100644 Les05-NextJS-Basics/quickpoll 2/.gitignore create mode 100644 Les05-NextJS-Basics/quickpoll 2/README.md create mode 100644 Les05-NextJS-Basics/quickpoll 2/next.config.ts create mode 100644 Les05-NextJS-Basics/quickpoll 2/package-lock.json create mode 100644 Les05-NextJS-Basics/quickpoll 2/package.json create mode 100644 Les05-NextJS-Basics/quickpoll 2/postcss.config.mjs create mode 100644 Les05-NextJS-Basics/quickpoll 2/public/file.svg create mode 100644 Les05-NextJS-Basics/quickpoll 2/public/globe.svg create mode 100644 Les05-NextJS-Basics/quickpoll 2/public/next.svg create mode 100644 Les05-NextJS-Basics/quickpoll 2/public/vercel.svg create mode 100644 Les05-NextJS-Basics/quickpoll 2/public/window.svg create mode 100644 Les05-NextJS-Basics/quickpoll 2/src/app/api/polls/[id]/route.ts create mode 100644 Les05-NextJS-Basics/quickpoll 2/src/app/api/polls/[id]/vote/route.ts create mode 100644 Les05-NextJS-Basics/quickpoll 2/src/app/api/polls/route.ts create mode 100644 Les05-NextJS-Basics/quickpoll 2/src/app/create/page.tsx create mode 100644 Les05-NextJS-Basics/quickpoll 2/src/app/error.tsx create mode 100644 Les05-NextJS-Basics/quickpoll 2/src/app/favicon.ico create mode 100644 Les05-NextJS-Basics/quickpoll 2/src/app/globals.css create mode 100644 Les05-NextJS-Basics/quickpoll 2/src/app/layout.tsx create mode 100644 Les05-NextJS-Basics/quickpoll 2/src/app/loading.tsx create mode 100644 Les05-NextJS-Basics/quickpoll 2/src/app/not-found.tsx create mode 100644 Les05-NextJS-Basics/quickpoll 2/src/app/page.tsx create mode 100644 Les05-NextJS-Basics/quickpoll 2/src/app/poll/[id]/not-found.tsx create mode 100644 Les05-NextJS-Basics/quickpoll 2/src/app/poll/[id]/page.tsx create mode 100644 Les05-NextJS-Basics/quickpoll 2/src/components/VoteForm.tsx create mode 100644 Les05-NextJS-Basics/quickpoll 2/src/lib/data.ts create mode 100644 Les05-NextJS-Basics/quickpoll 2/src/middleware.ts create mode 100644 Les05-NextJS-Basics/quickpoll 2/src/types/index.ts create mode 100644 Les05-NextJS-Basics/quickpoll 2/tsconfig.json create mode 100644 Les05-NextJS-Basics/quickpoll-starter/.cursorrules create mode 100644 Les05-NextJS-Basics/quickpoll-starter/.gitignore create mode 100644 Les05-NextJS-Basics/quickpoll-starter/README.md create mode 100644 Les05-NextJS-Basics/quickpoll-starter/next.config.ts create mode 100644 Les05-NextJS-Basics/quickpoll-starter/package-lock.json create mode 100644 Les05-NextJS-Basics/quickpoll-starter/package.json create mode 100644 Les05-NextJS-Basics/quickpoll-starter/postcss.config.mjs create mode 100644 Les05-NextJS-Basics/quickpoll-starter/public/file.svg create mode 100644 Les05-NextJS-Basics/quickpoll-starter/public/globe.svg create mode 100644 Les05-NextJS-Basics/quickpoll-starter/public/next.svg create mode 100644 Les05-NextJS-Basics/quickpoll-starter/public/vercel.svg create mode 100644 Les05-NextJS-Basics/quickpoll-starter/public/window.svg create mode 100644 Les05-NextJS-Basics/quickpoll-starter/src/app/api/polls/[id]/route.ts create mode 100644 Les05-NextJS-Basics/quickpoll-starter/src/app/api/polls/[id]/vote/route.ts create mode 100644 Les05-NextJS-Basics/quickpoll-starter/src/app/api/polls/route.ts create mode 100644 Les05-NextJS-Basics/quickpoll-starter/src/app/create/page.tsx create mode 100644 Les05-NextJS-Basics/quickpoll-starter/src/app/error.tsx create mode 100644 Les05-NextJS-Basics/quickpoll-starter/src/app/favicon.ico create mode 100644 Les05-NextJS-Basics/quickpoll-starter/src/app/globals.css create mode 100644 Les05-NextJS-Basics/quickpoll-starter/src/app/layout.tsx create mode 100644 Les05-NextJS-Basics/quickpoll-starter/src/app/loading.tsx create mode 100644 Les05-NextJS-Basics/quickpoll-starter/src/app/not-found.tsx create mode 100644 Les05-NextJS-Basics/quickpoll-starter/src/app/page.tsx create mode 100644 Les05-NextJS-Basics/quickpoll-starter/src/app/poll/[id]/not-found.tsx create mode 100644 Les05-NextJS-Basics/quickpoll-starter/src/app/poll/[id]/page.tsx create mode 100644 Les05-NextJS-Basics/quickpoll-starter/src/components/VoteForm.tsx create mode 100644 Les05-NextJS-Basics/quickpoll-starter/src/lib/data.ts create mode 100644 Les05-NextJS-Basics/quickpoll-starter/src/middleware.ts create mode 100644 Les05-NextJS-Basics/quickpoll-starter/src/types/index.ts create mode 100644 Les05-NextJS-Basics/quickpoll-starter/tsconfig.json create mode 100644 Les05-NextJS-Basics/quickpoll/.cursorrules create mode 100644 Les05-NextJS-Basics/quickpoll/.gitignore create mode 100644 Les05-NextJS-Basics/quickpoll/README.md create mode 100644 Les05-NextJS-Basics/quickpoll/next.config.ts create mode 100644 Les05-NextJS-Basics/quickpoll/package-lock.json create mode 100644 Les05-NextJS-Basics/quickpoll/package.json create mode 100644 Les05-NextJS-Basics/quickpoll/postcss.config.mjs create mode 100644 Les05-NextJS-Basics/quickpoll/public/file.svg create mode 100644 Les05-NextJS-Basics/quickpoll/public/globe.svg create mode 100644 Les05-NextJS-Basics/quickpoll/public/next.svg create mode 100644 Les05-NextJS-Basics/quickpoll/public/vercel.svg create mode 100644 Les05-NextJS-Basics/quickpoll/public/window.svg create mode 100644 Les05-NextJS-Basics/quickpoll/src/app/api/polls/[id]/route.ts create mode 100644 Les05-NextJS-Basics/quickpoll/src/app/api/polls/[id]/vote/route.ts create mode 100644 Les05-NextJS-Basics/quickpoll/src/app/api/polls/route.ts create mode 100644 Les05-NextJS-Basics/quickpoll/src/app/create/page.tsx create mode 100644 Les05-NextJS-Basics/quickpoll/src/app/error.tsx create mode 100644 Les05-NextJS-Basics/quickpoll/src/app/favicon.ico create mode 100644 Les05-NextJS-Basics/quickpoll/src/app/globals.css create mode 100644 Les05-NextJS-Basics/quickpoll/src/app/layout.tsx create mode 100644 Les05-NextJS-Basics/quickpoll/src/app/loading.tsx create mode 100644 Les05-NextJS-Basics/quickpoll/src/app/not-found.tsx create mode 100644 Les05-NextJS-Basics/quickpoll/src/app/page.tsx create mode 100644 Les05-NextJS-Basics/quickpoll/src/app/poll/[id]/not-found.tsx create mode 100644 Les05-NextJS-Basics/quickpoll/src/app/poll/[id]/page.tsx create mode 100644 Les05-NextJS-Basics/quickpoll/src/components/VoteForm.tsx create mode 100644 Les05-NextJS-Basics/quickpoll/src/lib/data.ts create mode 100644 Les05-NextJS-Basics/quickpoll/src/middleware.ts create mode 100644 Les05-NextJS-Basics/quickpoll/src/types/index.ts create mode 100644 Les05-NextJS-Basics/quickpoll/tsconfig.json diff --git a/Les04-TypeScript-Fundamentals/Les04-Docenttekst.md b/Les04-TypeScript-Fundamentals/Les04-Docenttekst.md index 3e98660..1628e20 100644 --- a/Les04-TypeScript-Fundamentals/Les04-Docenttekst.md +++ b/Les04-TypeScript-Fundamentals/Les04-Docenttekst.md @@ -1,339 +1,252 @@ -# Les 4: TypeScript Fundamentals -## Docenttekst (Spreektekst voor Tim) - -> **Totale lesduur:** 3 uur (180 minuten) -> **Totale spreektijd:** ~55 minuten (verdeeld over blokken) -> **Hands-on praktijk:** ~75 minuten -> **Pauze:** 15 minuten (10:15-10:30) +# Les 04: TypeScript Fundamentals - Docenttekst +**NOVI Hogeschool | Instructeur: Tim | Duur: 180 minuten** --- -## BLOK 1: WELKOM & TERUGBLIK (10 min) +## BLOK 1: HET PROBLEEM & DE OPLOSSING (0:00-0:20) -### Slide 1: Titel - Les 4: TypeScript Fundamentals -**[09:00 - 09:02]** +### Slide 1: Titel +**Tijd**: 0:00-0:02 -"Goedemorgen! Goed om jullie weer te zien. We gaan vandaag het allerbelangrijkste ding leren die elke moderne JavaScript developer moet weten." +*Tim staat enthousiast op, handen in de lucht* -_[Energiek, maak oogcontact, glimlach. Kijk rond de zaal.]_ +> "Yo! Welkom in les 4! Wie hier heeft al gehoord van TypeScript? Of eigenlijk... wie denkt dat JavaScript gewoon super is en er niks aan mankeert?" -"Vorige week: debug challenge. Wie had de hard versie gedaan? Kom eens van je stoel!" +*Tim loopt langs de eerste rij, maakt oogcontact, grijnst* -_[Laat studenten die de hard versie hebben gedaan opstaan. Applaudisseer.]_ +> "Ja ja, we gaan die illusie vandaag helemaal doorprikken. Want TypeScript? Dat gaat je leven redden. Serieus. Laten we beginnen!" -"Was het lastig? Ja? Precies. Dat is waarom we vandaag TypeScript gaan leren. TypeScript helpt dit probleem op te lossen." +*Tim geeft een enthousiaste thumbs-up* --- ### Slide 2: Planning Vandaag -**[09:02 - 09:05]** +**Tijd**: 0:02-0:04 -"Dit is de planning voor vandaag. Luister goed." +*Tim wijst naar het scherm met een pointer* -_[Wijs naar elke blok op het scherm.]_ +> "Oké, hier's het plan voor vandaag. We beginnen met: waarom TypeScript eigenlijk? Waar los je mee op? Dan gaan we de basis door – types, interfaces, allemaal die leuke dingen. Daarna wat geavanceerdere spullen. EN – en dit is het leukste deel – we gaan een escaperoom doen. Ja echt. Jullie moeten codeerpuzzels oplosten in TypeScript. Is wel gemotiveerd." -"**Eerste helft (tot 10:15):** -- We gaan begrijpen WAAROM TypeScript nodig is (live demo!) -- Basic types leren -- Interfaces en Type Aliases -- Functies typen -- Intro TypeScript Escaperoom +*Tim knipoogt* -**Pauze (10:15-10:30)** +> "En op het einde: huiswerk. Helaas. Maar niet eng, je krijgt files, je converteert ze. Zien we na de pauze." -**Tweede helft (10:30-12:00):** -- Jullie gaan aan de slag met de Escaperoom (75 minuten) -- Ik loop rond en help -- Afsluiting: recap, huiswerk, preview Les 5" - -"Deze les is 50-50: theorie en praxis. Niet veel slides. Veel live coding. Veel doen. Klaar?" - -_[Oogcontact.]_ +*Tim slaat handen in elkaar* --- -### Slide 3: Terugblik Les 3 - Cursor & Debugging -**[09:05 - 09:10]** +### Slide 3: Terugblik Les 3 +**Tijd**: 0:04-0:07 -"Oké, vorige week: Cursor. We zetten breakpoints, we keken naar variabelen, we snapten wat er misgaat. Super belangrijk." +*Tim loopt naar het whiteboard, pakt een marker* -_[Wacht even, laat dit bezinken.]_ +> "Vorige week: debugging. We hadden het over proactieve debugging versus reactieve debugging, remember? Je zoekt niet na je code kapot gaat, je bouwt het op zodat het nóóit kapot kan gaan." -"Maar: dat was reactive debugging. We zoeken NADAT er al iets fout is gegaan." +*Tim schrijft op het bord: "PROACTIVE > REACTIVE"* -_[Pauzeer. Maak oogcontact.]_ +> "TypeScript is de ultieme proactieve debugging tool. Jullie zullen zien. Voordat je code draait, zegt TypeScript al: 'Yo, dit gaat niet goed aflopen, hier's waarom.' Die is je bodyguard." -"TypeScript is anders. TypeScript is **preventief**. We stoppen fouten VOORDAT ze ontstaan — nog voor je code runt." - -"Ik ga je vandaag laten zien hoe dit werkt. En daarna ga JIJ het voelen. Klaar?" - -_[Glimlach.]_ +*Tim zet de marker terug* --- -## BLOK 2: WAAROM TYPESCRIPT? (15 min) - ### Slide 4: Het Probleem met JavaScript -**[09:10 - 09:13]** +**Tijd**: 0:07-0:12 -"Hier is het probleem. Kijk naar je scherm." +*Tim opent Cursor, navigeert naar een nieuw bestand* -_[Toon de JavaScript code in Cursor. Zorg dat je beamer werkt.]_ +> "Oké, stel: je hebt een functie. Een super simpele functie. Berekent totale prijs. Volgt mij?" + +*Tim typt langzaam in Cursor* ```javascript function calculateTotal(price, quantity) { return price * quantity; } -const total = calculateTotal("25.99", 3); -console.log(total); // "25.99" + "25.99" + "25.99" = "25.9925.9925.99" +// Iemand roept aan: +calculateTotal("29.99", 5); ``` -"Ik roep `calculateTotal` aan met een string in plaats van een getal. Wat zegt JavaScript?" +*Tim voert het uit in de Node REPL (als beschikbaar) of toont de output* -_[Pauzeer. Wacht op reacties.]_ +> "Zien wat er gebeurt? String keer getal? JavaScript zegt: 'Sure thing bro!' En je krijgt... NaN. Not a Number. En jij zoekt een uur naar wat je fout hebt gedaan." -"Niks. Geen fout. Cursor denkt: alles oké." +*Tim richt zich rechtstreeks tot de klas, voert ernstig* -_[Voer het uit.]_ +> "In echt code? Dit gebeurt VEEL. Soms ziet je het meteen, soms niet. En dan zit je client in productie en die zegt: 'Waarom berekent je site niet goed?' Oeps." -"Maar als je het draait? Kijk naar het output. Compleet verkeerd! JavaScript voert het uit en geeft je het verkeerde antwoord." - -"Waarom? Omdat JavaScript **dynamisch getypeerd** is. Het zegt: 'Jij bent verantwoordelijk voor types, niet ik. Ik ga gewoon doen wat je zegt.'" - -_[Maak oogcontact.]_ - -"Dit soort fouten kunnen weken werk kosten. Je debuggt je code, je kijkt naar je network requests, je denkt: backend is kapot. Nee. De bug zat hier." +*Tim slaat tegen het bureaublad voor effect* --- ### Slide 5: De Oplossing: TypeScript -**[09:13 - 09:16]** +**Tijd**: 0:12-0:16 -"Nu met TypeScript. Zelfde functie." +*Tim opent een nieuw TypeScript-bestand in Cursor* -_[Maak een nieuw bestand demo.ts. Schrijf dezelfde code, maar met types.]_ +> "Nu... we doen hetzelfde. Maar met TypeScript." + +*Tim typt* ```typescript function calculateTotal(price: number, quantity: number): number { return price * quantity; } -const total = calculateTotal("25.99", 3); +// Iemand probeert: +calculateTotal("29.99", 5); ``` -_[Wacht. Kijk naar het scherm.]_ +*Tim hoort op het scherm de rode squiggle onder de "29.99" – dit is live in Cursor* -"Kijk. Rode squiggle onder de string. TypeScript ziet het METEEN. VOORDAT je code runt." +> "BOOM. Voordat je code draait, zegt TypeScript al: 'Nee. Dit parameter? Dit moet een number zijn. Je geeft een string. Ga weg.' En jij denkt: 'Oh jee, thank God!'" -"TypeScript zegt: 'Wacht even... je geeft me een string en ik verwacht een number. Nope!'" +*Tim wijst naar de error* -_[Klik op de error om de boodschap te tonen.]_ - -"The error message: 'Argument of type string is not assignable to parameter of type number.'" - -"Dit gebeurt VOORDAT je code draait. Je ziet het rode squiggle in Cursor. Je fix het meteen. Geen runtime bugs. Geen mysterieuze errors in je logs later." - -_[Maak oogcontact. Glimlach.]_ - -"Dit is waarom TypeScript zo powerful is. Het voelt als een vriend die voortdurend over je schouder meekijkt en zegt: 'Ehm, dit gaat fout...'" +> "Dit is het genie van TypeScript. Je bugs gaan weg. Niet in productie. HIER. In development. Terwijl jij nog aan het typen bent." --- ### Slide 6: Waarom TypeScript? - 4 Voordelen -**[09:16 - 09:20]** +**Tijd**: 0:16-0:18 -"Waarom gebruiken we dit eigenlijk? Vier grote wins:" +*Tim schuift naar een nieuwe slide met 4 bolletjes* -_[Toon de slide. Ga door elk voordeel.]_ +> "Vier redenen waarom TypeScript je leven beter maakt. Één: Fouten voorkomen. We hebben het zojuist gezien. Twee: Automatische documentatie. Je types vertellen je precies wat een functie verwacht. Drie: Autocomplete. Cursor gaat je helpen, je hoeft niet meer te googlen wat een object kan doen. Vier: Refactoring. Als jij een ding verandert, TypeScript zegt je overal anders waar jij ook moet veranderen." -**Voordeel 1: Fouten Voorkomen** +*Tim steekt vier vingers op* -"Dit hebben we net gezien. Je code runt niet meer met domme type fouten. TypeScript vangt ze in je editor." - -**Voordeel 2: Betere Documentatie** - -"Types zijn eigenlijk documentatie. Kijk naar een functie — je ziet exact wat het verwacht en geeft terug. Geen giswerk meer." - -**Voordeel 3: Autocomplete & IntelliSense** - -"Dit is echt magisch. Jullie gaan dit ervaren vandaag. Je typt `person.` en Cursor weet EXACT welke properties beschikbaar zijn. Veel sneller coderen. Nul typos." - -**Voordeel 4: Veilig Refactoring** - -"Stel: je hebt 50 plaatsen in je code die een bepaalde functie aanroepen. Je verandert die functie. TypeScript zegt meteen: 'Hé, je hebt nu 37 plaatsen die breken'. Je fix ze allemaal op een rij. Zonder TypeScript? Goedemiddag, productie bugs!" - -_[Pauzeer.]_ - -"Dit is waarom Netflix, Google, Microsoft — allemaal gebruiken TypeScript." +> "En als ik heel eerlijk ben? TypeScript voelt als hacken met een safety net. Je bent niet bang meer." --- ### Slide 7: TypeScript in het Ecosysteem -**[09:20 - 09:25]** +**Tijd**: 0:18-0:20 -"Hier is iets belangrijk: TypeScript is niet meer iets speciaals. Het is de industrie standaard." +*Tim toont logos op het scherm: Next.js, React, Vue, Deno* -_[Toon de slide met de logo's.]_ +> "En hé, dit is niet niche. Next.js? TypeScript. React? Iedereen doet het in TypeScript. Vue? Hetzelfde verhaal. Deno – dat is als Node.js 2.0 – voelt zich TypeScript-native. Dit is niet 'ooh TypeScript is hip', dit is: de hele industrie bouwt alles in TypeScript. Als je TypeScript kan, ben je gold." -"Next.js? Jullie gaan dat volgende les gebruiken. TypeScript is er standaard in gebakken. React? Alle Enterprise React projecten gebruiken TypeScript. Vue? Svelte? Allemaal excellent support." +*Tim sluit het scherm* -"Zelfs Node.js heeft TypeScript support gebuild. Deno — die is van de maker van Node — is TypeScript first. Geen JavaScript, direct TypeScript." - -_[Maak oogcontact.]_ - -"Dit zegt: als je modern wilt programmeren, moet je TypeScript kennen. Het is niet optional meer. Het is expected. Iedereen bij NOVI, in de industrie, overal — zij gebruiken TypeScript." - -_[Pauzeer.]_ - -"Goed. Nu gaan we het leren." +> "Dus laten we je goed maken in TypeScript. Kom op!" --- -## BLOK 3: BASIC TYPES & INFERENCE (15 min) +## BLOK 2: TYPE BASICS (0:20-0:45) -### Slide 8: Basic Types - De Fundamenten -**[09:25 - 09:30]** +### Slide 8: Basic Types +**Tijd**: 0:20-0:25 -"Oké, de absolute basisvaardigheden. TypeScript heeft standaard types." +*Tim opent Cursor en maakt een nieuw TypeScript-bestand* -_[Open Cursor. Maak een nieuw bestand types-demo.ts.]_ +> "Types. De basis. Laten we beginnen met het eenvoudigste. String. Number. Boolean." + +*Tim typt* ```typescript -let name: string = "Tim"; +let name: string = "Alice"; let age: number = 25; -let isStudent: boolean = true; +let isActive: boolean = true; ``` -"String — tekst. Easy. Number — getallen, zowel integers als floats. Boolean — true of false. Dit zijn de drie basis types." +*Tim hovert over elk variable* -_[Toon wat er gebeurt als je verkeerd type toewijst.]_ +> "Zie je? Ik zeg: deze variable is een string. Deze is een number. Deze is een boolean. TypeScript checkt nu dat je niet per ongeluk een number aan de string geeft." + +*Tim typt verder* ```typescript -let name: string = "Tim"; -name = 42; // RODE SQUIGGLE +name = 42; // ERROR! +age = "not a number"; // ERROR! ``` -"Als je later probeert 42 in `name` te stoppen, TypeScript zegt: nope, name is string, geen number." +*Tim toont hoe Cursor rode squiggles geeft* -_[Hover over de error.]_ +> "Bam. TypeScript zegt nee." -"Nu arrays. Een array is een collection. TypeScript wil weten WELK TYPE in de array zit." +*Tim haalt adem* -```typescript -let scores: number[] = [90, 85, 88]; -let tags: string[] = ["typescript", "react", "javascript"]; -``` +> "En dan: any. any is als... jij zegt tegen TypeScript: 'Jij gaat mijn type niet controleren.' Dat kan. Maar dat is niet slim. any is de vijand. Vermijd any. Serieus." -"Je zegt: 'Dit is een array van numbers.' Of 'Dit is een array van strings.'" - -_[Demo het omgekeerde:]_ - -```typescript -let scores: number[] = [90, 85, 88]; -scores.push("oops"); // RODE SQUIGGLE -``` - -"Probeer een string in een number array te stoppen? TypeScript ziet het meteen." - -_[Toon de error message.]_ - -"Dit is het patroon: je zegt welk type, TypeScript controleert." +*Tim schrijft in grote letters op het whiteboard: "ANY = VIJAND"* --- -### Slide 9: Arrays - Collecties Typen -**[09:30 - 09:33]** +### Slide 9: Arrays +**Tijd**: 0:25-0:28 -"Arrays komen in twee notaties. Beide doen hetzelfde." +*Tim gaat terug naar Cursor* + +> "Arrays. Super belangrijk omdat jullie arrays elke dag gebruiken." + +*Tim typt* ```typescript -// Notatie 1: type[] -const numbers: number[] = [1, 2, 3]; - -// Notatie 2: Array -const names: Array = ["Alice", "Bob"]; - -// Beide zijn hetzelfde +let numbers: number[] = [1, 2, 3]; +let names: Array = ["Alice", "Bob"]; ``` -"De eerste notatie is populairder. Sneller om te typen." +> "Twee manieren om het te zeggen. number[] met vierkante haken. Of Array met angle brackets. Allebei goed. Eerst is makkelijker." -_[Demo arrays van objecten.]_ +*Tim toont wat er fout gaat* ```typescript -interface User { - id: number; - name: string; +let mixed: number[] = [1, "twee", 3]; // ERROR! +``` + +> "Als je zegt 'dit is een array van numbers', mag je geen string toevoegen. TypeScript zal je tegenwerken." + +*Tim voelt een student aarzelen* + +> "En als je écht mixed stuff in je array wilt, dan zeggen we: number | string. Daar komen we later op terug. Maar voor nu: arrays met one type. Keep it clean." + +--- + +### Slide 10: Type Inference +**Tijd**: 0:28-0:32 + +*Tim opent een nieuw stukje code* + +> "Cool feature: type inference. Dat wil zeggen: TypeScript is slim genoeg om types zelf uit te vinden." + +*Tim typt* + +```typescript +let count = 5; +// TypeScript ZIET: dit is een number +count = "hello"; // ERROR! Jij zei niet number, maar TypeScript WEET het. +``` + +*Tim hovert over `count`* + +> "Hover over count. Zie je? Cursor zegt: 'let count: number'. TypeScript wist het. Jij hoefde het niet te zeggen." + +*Tim toont nog een voorbeeld* + +```typescript +function getAge(name: string) { + // Wat geeft deze functie terug? TypeScript weet het niet... + return 25; + // AH! Jij returnt een number. Dus TypeScript: return type is number. } - -const users: User[] = [ - { id: 1, name: "Alice" }, - { id: 2, name: "Bob" } -]; ``` -"Je kan ook arrays van complexere types hebben. Arrays van User objecten. Handig." +> "Met functies is het omgekeerd. Jij zegt de parameters explicit. Maar de return type – eh, dat kan TypeScript ook uitvinden. Maar wij willen het explicit. Dat is schoner, beter voor je team." -_[Pauzeer.]_ - -"Onthoud: elke array moet een type hebben. Geen gemengde arrays." +*Tim schrijft op het bord: "Parameters explicit. Return types explicit."* --- -### Slide 10: Type Inference - TypeScript Raadt -**[09:33 - 09:40]** +### Slide 11: Interfaces +**Tijd**: 0:32-0:37 -"Nu het interessante deel. TypeScript is slim." +*Tim maakt een nieuw bestand* -```typescript -let message = "Hello"; -``` +> "Interfaces. Dit is waar TypeScript echt powerful wordt. Een interface is als een contract. Je zegt: 'Dit object moet deze shape hebben.'" -"Geen type annotering. Maar kijk..." - -_[Hover over `message` in je editor. Tooltip verschijnt.]_ - -"TypeScript weet het al! Zonder dat ik het zei. Omdat ik `"Hello"` toewijzig — een string — TypeScript snapt: oh, dit is een string variable." - -"Dit heet **type inference**." - -```typescript -let count = 5; // TypeScript: number -let active = true; // TypeScript: boolean -let items = [1, 2, 3]; // TypeScript: number[] -``` - -"Dit is geniaal. Je hoeft niet ALLES expliciet te typen. TypeScript is slim genoeg om het zelf uit te vogelen." - -_[Maak oogcontact.]_ - -"Regel: **Bij variabelen: let TypeScript infer. Bij functies: je MOET typen.**" - -```typescript -// GUT: TypeScript raadt het return type -function add(a: number, b: number): number { - return a + b; -} - -const result = add(5, 3); // TypeScript weet: result is number - -// SLECHT: Redundant -const result: number = add(5, 3); -``` - -"Functies zijn waar types het meest helpen. Daar moet je explicit zijn. Variables? Let TypeScript denken." - ---- - -## BLOK 4: INTERFACES & TYPE ALIASES (15 min) - -### Slide 11: Interfaces - Object Vormen Definiëren -**[09:40 - 09:47]** - -"Wat als je een object hebt? Hoe beschrijf je dat?" - -_[Live coding in Cursor.]_ +*Tim typt in Cursor* ```typescript interface User { @@ -344,937 +257,694 @@ interface User { const user: User = { id: 1, - name: "Anna", - email: "anna@novi.nl" + name: "Alice", + email: "alice@example.com" }; ``` -"Interface zeggen: 'Een User object heeft PRECIES deze properties, met DEZE types.' Niks meer, niks minder." +*Tim hovert over `user`* -_[Demo autocomplete.]_ +> "Je zegt: user is van type User. TypeScript checkt nu: has het deze object alle properties? Ja. Zijn ze het juiste type? Ja. Oké!" + +*Tim breekt het intentioneel* ```typescript -const user: User = { - // Typ hier... - // Cursor suggereert: id, name, email +const badUser: User = { + id: "one", // ERROR! Dit moet een number zijn + name: "Bob" + // ERROR! email is missing! }; ``` -"Cursor weet: User heeft deze velden. Alleen deze. Als je iets vergeet..." +*Tim wijst naar de errors* -```typescript -const user: User = { - id: 1, - name: "Anna" - // ❌ RODE SQUIGGLE — email is missing -}; -``` +> "TypeScript ziet allebei. Wrong type. Missing property. Dit is GOLD. Jij bouwt je object perfect voordat je het zelfs probeert." -"TypeScript ziet het. Je MOET alles invullen dat nodig is." +*Tim gaat verder* -_[Toon een compleet voorbeeld.]_ - -```typescript -const user: User = { - id: 1, - name: "Anna", - email: "anna@novi.nl" -}; - -user.id; // number -user.name; // string -user.email; // string -user.phone; // ❌ RODE SQUIGGLE — bestaat niet -``` - -"Zien? Cursor weet: User heeft deze velden. Alleen deze. Geen typos." - -_[Maak oogcontact.]_ - -"Dit is het hart van TypeScript: interfaces. Je beschrijft de SHAPE van je objecten. Iedereen snapt het. Geen verwarring meer." +> "En – let op – als jij een typo maakt in property name, ziet TypeScript dat ook. Je typt 'emial' in plaats van 'email'? TypeScript zegt: 'Yo, dit property bestaat niet in User.' Boom." --- -### Slide 12: Optional Properties - De ? Syntax -**[09:47 - 09:50]** +### Slide 12: Optional Properties +**Tijd**: 0:37-0:40 -"Wat als niet alles verplicht is?" +*Tim voegt toe aan het Cursor-bestand* + +> "Maar soms. Soms is iets optional. Misschien hebben we de email niet altijd." + +*Tim typt* ```typescript -interface User { +interface Product { id: number; - name: string; - email: string; - phone?: string; // OPTIONEEL + title: string; + description?: string; // Optional! Met ? } -const user1: User = { +const product: Product = { id: 1, - name: "Anna", - email: "anna@novi.nl" - // phone niet nodig -}; - -const user2: User = { - id: 2, - name: "Bob", - email: "bob@novi.nl", - phone: "+31612345678" + title: "Laptop" + // description is optional, no error }; ``` -"Zie je dat vraagteken? `phone?: string`. Dat zegt: 'Dit property mag er zijn, maar het hoeft niet.'" +*Tim hovert over de ?* -"Zo maak je flexible interfaces. Sommige dingen zijn altijd nodig (naam, email), andere zijn nice-to-have (telefoon)." +> "Die vraagteken daar? Dat zegt: dit property mag er zijn, maar hoeft niet. TypeScript is oké met beide." -_[Pauzeer.]_ +*Tim toont wat er gebeurt als je het probeert te gebruiken* -"Dit is realistisch. Je kan niet altijd alles invullen. Dus zeg je tegen TypeScript: deze dingen zijn optioneel. Je geeft flexibiliteit zonder je type safety te verliezen." +```typescript +console.log(product.description.toUpperCase()); // ERROR! +// description is undefined, je kan niet .toUpperCase() op undefined doen +``` + +> "Maar let op – als je het gebruikt, zegt TypeScript: 'Dit kan undefined zijn. Jij moet checken eerst.'" + +*Tim wijst naar code* + +> "Dus je doet: if (product.description) { ... } Dan ben je safe." --- -### Slide 13: Type Aliases - De `type` Keyword -**[09:50 - 09:55]** +### Slide 13: Interface vs Type +**Tijd**: 0:40-0:43 -"Nu `type`. Dit is eigenlijk meer flexibel dan interface." +*Tim toont twee code-blokken naast elkaar* -```typescript -type Status = "pending" | "approved" | "rejected"; - -const orderStatus: Status = "pending"; // ✓ oké -const orderStatus: Status = "shipped"; // ❌ RODE SQUIGGLE -``` - -"Type aliases: je definieert een naam voor iets. Hier: Status kan ALLEEN deze drie strings zijn. Niet willekeurig. DEZE drie." - -"Dit heet **union type** — een waarde kan ÉÉN van verschillende opties zijn." - -```typescript -type Response = string | number | boolean; - -const answer: Response = "yes"; // ✓ -const answer: Response = 42; // ✓ -const answer: Response = true; // ✓ -const answer: Response = {}; // ❌ RODE SQUIGGLE — object mag niet -``` - -"Type is ook voor object shapes:" - -```typescript -type Address = { - street: string; - city: string; - zipCode: string; -}; - -type Employee = { - id: number; - name: string; - address: Address; - status: Status; -}; -``` - -_[Pauzeer.]_ - -"Type is flexibeler dan interface. Je kan unions maken, je kan primitives combineren. Maar: in de praktijk? Interface en type worden vaak door elkaar gebruikt." - ---- - -### Slide 14: Union Types - Multiple Mogelijkheden -**[09:55 - 10:00]** - -"Union types zijn heel nuttig. Ze zeggen: 'Dit kan dit type zijn, óf dit type.'" - -```typescript -type Status = "pending" | "approved" | "rejected"; - -function handleStatus(status: Status) { - if (status === "pending") { - console.log("Wachten op review..."); - } else if (status === "approved") { - console.log("Goedgekeurd!"); - } else if (status === "rejected") { - console.log("Afgewezen, je kan opnieuw proberen."); - } -} - -// Dit gaat: -handleStatus("approved"); - -// Dit gaat NIET: -handleStatus("cancelled"); -// ❌ ERROR: Argument of type '"cancelled"' is not assignable -``` - -"Een status kan `pending`, `approved`, of `rejected` zijn. Niet meer, niet minder. Als je `cancelled` probeert? Nee, TypeScript blokkeert het." - -"Dit is super veilig. Je ziet een status, je weet precies welke mogelijkheden er zijn. Geen gegooi met magic strings." - -_[Demo ook type unions:]_ - -```typescript -type Result = string | number; - -const value: Result = 42; // OK -const text: Result = "hello"; // OK -const arr: Result = []; // ❌ FOUT -``` - -"Je kan ook types unionen: `string | number` betekent 'dit kan een string zijn OF een number.'" - ---- - -### Slide 15: Interface vs Type - Wanneer Welke? -**[10:00 - 10:05]** - -"Dus: interface of type?" - -_[Toon de twee kolommen.]_ - -"**Interface:** -- Voor object shapes -- Kan geextend worden -- Traditioneel OOP approach -- Foutmeldingen zijn duidelijker - -**Type:** -- Voor alles (objects, unions, primitives) -- Meer flexibel -- Modern, functioneel approach -- Kan unioned worden" - -_[Pauzeer.]_ - -"Praktisch antwoord: **Interface voor objecten. Type voor alles anders.**" +> "Vraag die iedereen heeft: interface of type? Ze zien er hetzelfde uit." ```typescript // Interface interface User { - id: number; name: string; } -// Type — alles ander +// Type +type User = { + name: string; +}; +``` + +> "Ze doen hetzelfde. Maar er zijn subtiele verschillen. Interface kan je 'uitbreiden' – we zien dat later. Type is voor alles – strings, numbers, unions, van alles wat." + +*Tim schrijft op het bord* + +> "Rule of thumb: interfaces voor objects. Types voor alles. En wat jullie in productiecode zien? Bedrijven hebben hun voorkeur. Soms interface, soms type. Allebei oké." + +*Tim haalt schouders op* + +> "Voor nu: gebruiken wat aanvoelt goed. Je zal beide zien." + +--- + +### Slide 14: Type Aliases & Union Types +**Tijd**: 0:43-0:45 + +*Tim opent een nieuwe slide met code* + +> "Type aliases. Ermee kan je een type een naam geven. Super handig voor... dingen die je veel gebruikt." + +*Tim typt* + +```typescript type Status = "pending" | "approved" | "rejected"; -type ID = number | string; -type Config = { host: string; port: number }; + +const orderStatus: Status = "pending"; // OK +const badStatus: Status = "shipped"; // ERROR! Dit is niet in de union ``` -"Eerlijk? Voor 90% van wat jullie gaan doen, is het niet superkritiek. Kies er één en wees consistent. Ik zeg: interface voor objects, type voor alles. Maar beide zijn oké." +*Tim wijst naar de | characters* -_[Maak oogcontact.]_ +> "Die pipe symbols? Die zeggen: OR. Dit kan dit zijn, OF dit, OF dit. Niets anders. En als je iets anders probeert... TypeScript zegt nee." -"Belangrijk: beide zijn TypeScript. Allebei geven TypeScript dezelfde informatie. Het is 'preference'." +*Tim toont een pratisch voorbeeld* + +```typescript +type HttpMethod = "GET" | "POST" | "PUT" | "DELETE"; + +function makeRequest(method: HttpMethod) { + // ... +} + +makeRequest("GET"); // OK +makeRequest("PATCH"); // ERROR! +``` + +> "Dit is ook wel literal types heten. Maar ja, types die specifieke waarden zijn. Super powerful voor je code te constrainen. Geen more typo's met strings." --- -## BLOK 5: FUNCTIES & ERRORS (10 min) +## BLOK 3: GEAVANCEERDE TYPES (0:45-1:05) -### Slide 16: Functies Typen - Parameters & Return -**[10:05 - 10:10]** +### Slide 15: Literal Types +**Tijd**: 0:45-0:49 -"Functies typen is makkelijk: zeg wat elk parameter is, en wat je teruggeeft." +*Tim maakt een nieuw bestand met voorbeelden* + +> "Literal types. Het gaat erom: types kunnen niet alleen 'number' zijn, maar ook 'dit specifieke getal.'" + +*Tim typt voorbeelden* ```typescript -// Regular function -function add(a: number, b: number): number { - return a + b; +// String literals +type Direction = "north" | "south" | "east" | "west"; + +// Number literals +type DiceRoll = 1 | 2 | 3 | 4 | 5 | 6; + +// Even een praktisch voorbeeld +type HttpMethod = "GET" | "POST" | "DELETE"; + +function apiCall(url: string, method: HttpMethod) { + // ... } -// Arrow function -const multiply = (a: number, b: number): number => a * b; - -// With console.log (return type: void) -function logUser(user: User): void { - console.log(user.name); -} - -// Optional parameters -function greet(name: string, greeting?: string): string { - return greeting ? greeting + ", " + name : "Hello, " + name; -} +apiCall("/users", "GET"); // OK +apiCall("/users", "PATCH"); // ERROR! PATCH is niet in HttpMethod ``` -"Pattern: types IN (parameters), type OUT (return type). Altijd. Functies zijn waar types het meest helpen." +*Tim wijst naar de errors* -"`add` neemt twee numbers, geeft een number terug. Simpel." +> "Dit is so powerful. Jij zegt niet 'geef me een string.' Jij zegt 'geef me exáct deze strings.' TypeScript guard je tegen typo's en logische fouten. Geen 'GETT' more, geen rare method names." -"`greet` heeft een optionele parameter — dat `greeting?`. Die zeggen: 'Dit kan weggelaten worden'." +*Tim gaat terug naar Direction* -"`logUser` geeft niets terug — `void`. Dat is belangrijk. `void` zegt: 'Deze functie returnt niks, het doet alleen iets.'" - -_[Pauzeer.]_ - -"Dit is essentieel. Iedere functie die je schrijft moet getypeerd zijn. Geen uitzonderingen." +> "En dezen is handig voor game dev. Direction – north, south, oost, west. Gamer gaat niet random toestand intypen. Jij defines precies wat possible is." --- -### Slide 17: Veelvoorkomende Errors - Top 3 -**[10:10 - 10:15]** +### Slide 16: Type Narrowing +**Tijd**: 0:49-0:54 -"Je gaat deze drie errors constant zien. Leer ze kennen." +*Tim opent een ander Cursor-bestand* -_[Toon Slide 17.]_ +> "Type narrowing. Dit is een KEY concept. Je hebt een variable die meerdere types kan zijn. Maar als jij een check doet, wordt TypeScript slimmer." -**Error 1: Type 'X' is not assignable to type 'Y'** +*Tim typt* ```typescript -let count: number = "hello"; // ❌ RODE SQUIGGLE -// Type 'string' is not assignable to type 'number' -``` - -"Vertaald: je probeert een string in iets te stoppen dat number moet zijn. Mismatch. Fix: zorg dat je types overeenstemmen." - -**Error 2: Property 'X' does not exist on type 'Y'** - -```typescript -const user = { name: "Tim", age: 25 }; -console.log(user.emali); // ❌ RODE SQUIGGLE -// Property 'emali' does not exist -``` - -"Typo. Je schreef 'emali' maar het property heet 'email'. Fix: spel het goed." - -**Error 3: Object is possibly 'undefined'** - -```typescript -function getUser(): User | undefined { - // misschien geeft dit undefined terug -} - -const user = getUser(); -console.log(user.name); // ❌ RODE SQUIGGLE -// Object is possibly 'undefined' -``` - -"TypeScript zegt: dit kan undefined zijn. Je mag niet zomaar `.name` eroppen. Wat als het undefined is? Fix: check eerst." - -```typescript -const user = getUser(); -if (user) { - console.log(user.name); // ✓ nu weet TypeScript: user is niet undefined +function processInput(input: string | number) { + // Hier: input kan string OF number zijn + console.log(input.toUpperCase()); // ERROR! number heeft geen toUpperCase } ``` -_[Maak oogcontact.]_ +*Tim wijst naar de error* -"Deze fouten gaan jullie VEEL tegenkomen. Dat is GOED! TypeScript beschermt je. Elke rode squiggle is TypeScript die zegt: 'Hé, ik help je.' Niet iets wat boos is. Het is helpful." +> "TypeScript ziet: dit kan beide types zijn. Als je .toUpperCase() roept, kan het breken als input een number is. Dus error." ---- - -## BLOK 6: ESCAPEROOM INTRO (10 min) - -### Slide 18: Cursor + TypeScript - Superkrachten -**[10:15 - 10:17]** - -_[Toon Slide 18.]_ - -"Dit is waarom Cursor zo goed is. Cursor en TypeScript samen zijn echt magisch." - -"**Voordeel 1: IntelliSense Autocomplete** - -Stel: je hebt een User object. Je typt `user.` en Cursor weet EXACT welke properties die User heeft. Je hoeft niet te gokken. Je ziet de lijst. Autocomplete. Sneller coderen. Nul fouten." - -"**Voordeel 2: Error Underlines** - -Je maakt een fout. Meteen rode squiggle. Je hoeft niet in tests of bij runtime te wachten. Je ziet het direct. En Cursor kan je sometimes helpen met een fix knop." - -"**Voordeel 3: Go to Definition** - -Wil je een type zien? Click je erop met Cmd. Cursor springt je naar die definition. Zo leer je ook dingen — je ziet hoe andere types gebouwd zijn." - -_[Pauzeer.]_ - -"Dit is waarom professionele developers TypeScript gebruiken. Cursor en TypeScript samen zijn een superkracht." - ---- - -### Slide 19: Hands-On - TypeScript Escaperoom Introductie -**[10:17 - 10:20]** - -"Oké, theorie klaar. Nu gaan jullie zelf TypeScript leren door TE DOEN." - -_[Toon Slide 19.]_ - -"Hier is het concept: 8 kamers. Elke kamer heeft TypeScript puzzels. Jij lost ze op." - -``` -Room 1: Basic Types (Easy) -Room 2: Arrays & Interfaces (Easy) -Room 3: Optional Properties (Medium) -Room 4: Union Types (Medium) -Room 5: Function Types (Medium-Hard) -Room 6: Complex Interfaces (Hard) -Room 7: Mixed Errors (Very Hard) -Room 8: Real-World Scenario (Very Hard) -``` - -"Als je een kamer goed solved, krijg je een escape code fragment. Alle 8 fragments bij elkaar? Je hebt het complete escape code." - -_[Toon een voorbeeld van een TODO-comment:]_ - -```typescript -// Room 1: Basic Types -// TODO: Create a variable 'age' of type number and assign it the value 25 -// TODO: Create a variable 'name' of type string and assign it "Alice" -// TODO: Create a variable 'isActive' of type boolean and assign it true - -// Solution: -let age: number = 25; -let name: string = "Alice"; -let isActive: boolean = true; - -// If correct, you'll see: ESCAPE_CODE_1: ABC123XYZ -``` - -"Je opent een TypeScript bestand. Er staan 'TODO' comments in. Jij vult aan wat ontbreekt. Je runt het. TypeScript compiler zegt: 'Fout!' of 'Oké!' Als het oké is, je krijgt een code." - ---- - -### Slide 20: Escaperoom Rules & Structuur -**[10:20 - 10:25]** - -_[Toon Slide 20 met de regels.]_ - -"Stappen om aan de slag te gaan:" - -"1. **Download de zip**: [link naar Canvas/moodle] -2. **Extract het**: `unzip typescript-escaperoom.zip` -3. **Open in Cursor**: `cursor .` (in de folder) -4. **Install packages**: `npm install` -5. **Begin met kamer 1**: Open `room-1.ts` -6. **Lees de TODOs**, vul aan, sla op (Ctrl+S of Cmd+S) -7. **Run**: `npm run room:1` (of wat de instructie zegt) -8. **Escape code?** Als je het goed hebt: je krijgt een code te zien" - -"Alle 8 kamers doen? Einde van de les heb je het master escape code." - -_[Pauzeer.]_ - -"Maar EERST: zet de AI uit in Cursor. Dit is heel belangrijk. Open je Command Palette met Cmd+Shift+P, en type 'Disable Cursor Tab'. Dit is een TypeScript oefening — jullie moeten zelf de errors lezen en begrijpen. Als AI het voor je oplost, leer je niks." - -_[Wacht tot iedereen dit heeft gedaan. Loop even rond om te checken.]_ - -"Paar tips terwijl je aan het werk bent:" - -"1. **Hover over errors** — plaats je cursor op een rode squiggle, en je ziet uitleg -2. **Lees de error messages** — TypeScript vertelt je precies wat er fout is. Leer die taal lezen! -3. **Denk eerst zelf** — probeer EERST zelf uit te vogelen. Pas dan vragen stellen. -4. **Lees de TODOs** — de TODOs zijn heel precies. Lees goed wat je moet doen." - -_[Kijk rond de zaal.]_ - -"Vragen? Ik loop rond. Steek je hand op. We doen dit samen." - ---- - -## PAUZE (15 minuten) -**[10:25 - 10:40]** - -Studenten gaan naar buiten / hebben een break. Jij: -- Zorg dat je scherm klaar is voor de volgende ronde -- Controleer je eigen laptop/connectie -- Heb de Escaperoom zip klaar -- Bereid je voor op rondlopen (notieblok, pen) - ---- - -## BLOK 7: HANDS-ON PRAKTIJK (75 minuten) -**[10:40 - 11:55]** - -### Setup en Start -**[10:40 - 10:45]** - -"Welkom terug. Iedereen klaar? Download de zip, extract, `npm install`, open in Cursor?" - -_[Controleer dat iedereen de eerste room open heeft.]_ - -"Oké, we starten met Room 1 op hetzelfde moment. 3... 2... 1... GO!" - -_[Start hen allemaal op hetzelfde moment. Dit creëert momentum.]_ - ---- - -### Rondlopen & Helpen -**[10:45 - 11:45]** - -**Je rol is drievoudig: Observeren, Helpen (zonder antwoord), Stimuleren.** - -#### A. Observeren - -Loop door de zaal. Kijk naar schermen. Wie zit vast? Wie is snel klaar? Noteer mentaal waar de bottlenecks zijn. - -#### B. Helpen (zonder antwoord geven!) - -**Dit is BELANGRIJK. Je helpt, maar je geeft niet het antwoord.** - -**Voorbeeld — student zit vast op Room 3:** - -❌ FOUT: -"Oké, je schrijft `let age: string = 25;` — nee, dit is fout. Je moet `number` schrijven, niet `string`." - -✓ GOED: -"Oké, laten we kijken. De TODO zegt: 'Create age of type number.' Wat zegt TypeScript nu? Kijk op je scherm... ah, zie je dat: 'Type string is not assignable to type number.' Wat denk jij: is 25 een string of een number?" - -Student: "Oh, 25 is een number!" - -"Precies. Dus welk type moet age zijn?" - -Student: "Number!" - -"Yes! Probeer het." - -Dit is Socratisch onderwijs. Je stelt vragen, student vindt antwoord. - -#### C. Stimuleren - -**Voor trage studenten:** -- Rond 11:00, check: "Hoe gaat het? Stuck? Laten we naar de error kijken. Wat zegt TypeScript?" -- Rond 11:20: "Je bent op Room 4. Goed! Volgende." - -**Voor snelle studenten:** -- Rond 11:00: "Helemaal klaar? Nice! Hier: bonus challenge." (zie hieronder) -- Zorg dat ze niet *wachten* — ze kunnen altijd meer leren. - -#### D. Veelvoorkomende Problemen & Oplossingen - -**Probleem 1: "Ik snap niet wat de TODO vraagt"** - -Lees de TODO samen met de student hardop voor. Zeg in je eigen woorden wat het vraagt. Een TODO als: - -```typescript -// TODO: Create an interface User with properties id (number), name (string), email (string) -``` - -Jij: "Dus: je maakt een interface. Het heet User. Het heeft drie properties: id die een number is, name die string is, email die string is. Wat schrijf je?" - -**Probleem 2: "Ik krijg rood overal, ik snap het niet"** - -"Laten we stap voor stap gaan. Wat is de EERSTE rode squiggle?" - -Focus op één fout tegelijk. Niet alles tegelijk. - -**Probleem 3: "Kan ik Cursor gebruiken?"** - -Ja, maar: - -"Je mag Cursor Cmd+K gebruiken om het uit te leggen of te fixen. Maar: probeer EERST zelf. Cursor is je trainer, niet je antwoordboek." - -**Probleem 4: "Ik ben in een ander room en ik snap er niks van"** - -"Laten we teruggaan naar Room 5 of 6 (wat je wél snapte). Wat zat daar? Oké, dit room is hetzelfde idee, maar..." - -Haak aan bij wat ze al kennen. - -#### E. Check-in Momenten - -Plan drie korte check-ins: - -**11:00 — Eerste Check (halveway)** -- Iedereen op Room 2 of 3? -- Wie has probleem en waar? -- Energieniveau OK? - -"Iedereen, 2 seconden. Hoe ver zijn we? Room 1? 2? Mooi! Wie zit vast? [student steekt hand op] Laten we daar even naar kijken." - -**11:20 — Tweede Check** -- Iedereen voortgang? -- Zijn studenten "stuck in a loop" (dezelfde error, keer op keer)? - -**11:45 — Laatste Check** -- 10 minuten tot einde. Wie is klaar? Wie heeft nog 1-2 kamers? -- Snelle studenten: "Alle 8 gedaan? Epic! Hier extra challenge." - -#### F. Bonus Challenges (voor Snelle Studenten) - -Voor studenten die alle 8 kamers hebben gedaan: - -**Bonus 1: Generics** - -```typescript -function getFirstElement(arr: T[]): T { - return arr[0]; -} - -const first = getFirstElement([1, 2, 3]); // number -const firstStr = getFirstElement(["a", "b"]); // string -``` - -"Dit is Les 5 material. Maar je bent snel. Probeer het. Vraag Cursor!" - -**Bonus 2: Advanced Interfaces** - -```typescript -interface Animal { - name: string; -} - -interface Dog extends Animal { - breed: string; -} -``` - -**Bonus 3: Type Guards** +*Tim voegt toe* ```typescript function processInput(input: string | number) { if (typeof input === "string") { - console.log(input.toUpperCase()); + // Hier: TypeScript WEET: dit is een string + console.log(input.toUpperCase()); // OK! } else { - console.log(input + 10); + // Hier: TypeScript WEET: dit is een number + console.log(input.toFixed(2)); // OK! } } ``` ---- +*Tim hovert over `input` in elk blok en toont hoe het type is 'narrowed'* -### Energy Management +> "Genius! Jij doet een typeof check. TypeScript ziet die check en zegt: 'Ah, jij hebt gechecked. Ik weet nu dat het een string is. Jij mag .toUpperCase() roepen.' TypeScript wordt slimmer samen met jou." -Dit is een lange blok (75 min). Zorg voor energie: +*Tim benadrukt dit* -- **Rond 11:15**: "Iedereen, 30 seconden stretchen. Sta op als je zit." -- **Rond 11:45**: "Laatste 10 minuten! Zet ineens aan!" +> "Dit gaat jullie life easier maken. Veel safer code. Minder bugs." --- -## BLOK 8: AFSLUITING (15 minuten) -**[11:55 - 12:00]** +### Slide 17: Type Guards +**Tijd**: 0:54-0:58 -### Slide 21: Samenvatting - Key Takeaways -**[11:55 - 11:58]** +*Tim gaat verder op het scherm* -"Oké, allen! Wie heeft het complete escape code? Alle 8 kamers?" +> "Type guards. Gerelateerd aan narrowing. Dit zijn manieren waarop jij TypeScript kunt zeggen: check deze condition, dan weet je meer." -_[Laat studenten hun code tonen. Applaudisseer.]_ - -"Nice! Goed werk. Wie 6 kamers gedaan? 5? 4? Allemaal goed! Volgende week maken jullie het af, en dan gaan we dieper." - -_[Zet dit op het bord of deel-screen.]_ - -**3 Key Takeaways:** - -"1. **Types voorkomen RUNTIME ERRORS** - -Onthouden: je JavaScript code kan 3 uur later breken in productie. TypeScript zegt: nope, hier is het fout, FIX het NU. In je editor." - -"2. **Interfaces beschrijven OBJECTEN** - -Object nodig? Interface. Object zegt: dit object heeft deze form. Dit property, deze type. Cursor gebruikt het om je te helpen." - -"3. **Functies ALTIJD typen** - -Rule: functie parameters IN, functie return type OUT. Altijd annoteren. Variables? Let TypeScript infer. Maar functies? Altijd." - -_[Pauzeer. Maak oogcontact.]_ - -"Dit is jullie basis. Volgende les bouwen we hierop." - ---- - -### Slide 22: Huiswerk & Preview Les 5 -**[11:58 - 12:00]** - -"Huiswerk voor volgende week:" - -_[Schrijf op en stuur ook via email/Teams:]_ - -"**Huiswerk Les 4:** - -1. **Download `les4-huiswerk-js-converter.zip`** van Teams -2. Je krijgt 4 JavaScript bestanden: users, products, orders, utils -3. Zet ze allemaal om naar TypeScript — interfaces schrijven, union types, functies typen -4. De tests staan al in TypeScript — die zijn je hints! -5. `npm run check` moet 0 errors geven, `npm test` moet groen zijn -6. **Geen `any`!** Als ik `any` zie, stuur ik het terug. - -En als je de escaperoom nog niet af hebt: maak die ook af." - -_[Toon Slide 22 preview.]_ - -"**Volgende week: Les 5. TypeScript voor React.**" - -"Jullie weten nu de basis: types, interfaces, union types, functies typen. Volgende week pakken we het volgende level: hoe gebruik je TypeScript in React? Props typen, useState met types, event handlers, API calls. Dat is de laatste stap voordat we in Les 6 met Next.js beginnen." - -_[Maak oogcontact, glimlach.]_ - -"Jullie hebben vandaag veel geleerd. TypeScript lijkt misschien veel regels. Maar denk eraan: elke rode squiggle is TypeScript die zegt: 'Hé, ik help je.' Het is niet boos — het is helpful." - -"Goed gedaan vandaag. Tot volgende keer!" - -_[Applaus.]_ - ---- - -## PRAKTISCHE TIPS VOOR DE DOCENT - -### Setup Checklist (Voor Jezelf) - -- [ ] Cursor geïnstalleerd en werkt -- [ ] Twee voorbeeldbestanden klaar: demo.js en demo.ts (voor Blok 2) -- [ ] TypeScript Escaperoom zip klaar om te delen (via Canvas) -- [ ] `npm install` gedaan in de escaperoom (opdat je het kan runnen) -- [ ] Beamer/screen sharing testen -- [ ] Audio testen (voor demos) -- [ ] Een backup plan: als iemand's installatie niet werkt, heb jij hem al draaien - -### Energie en Ritme - -**Sterke openers:** -- "Vorige week: debug challenge. Wie had het super lastig?" -- "TypeScript. Klinkt ingewikkeld. Is het niet." -- "Fout vangen VOORDAT je code runt. Dat is de superpower." - -**Momentum behouden:** -- Niet te veel slides. Veel live demo. -- Move rond terwijl je praat. Niet statisch. -- Maak oogcontact. Glimlach. Show enthousiasme. - -**Energiedalingen voorkomen:** -- Na live demo (10 min) → iedereen doet iets (hands-on start) -- Pauze op juiste moment (na 55 min) -- Check-ins om focus terug te krijgen - -### Veelgestelde Studentenvragen - -**Q: "Waarom TypeScript en niet JavaScript?"** - -A: "JavaScript is oké voor kleine projecten. TypeScript is oké voor alles, groot en klein. En alle moderne bedrijven gebruiken TypeScript. Future-proofing." - -**Q: "Moet ik elk variable typen?"** - -A: "Nee. Functies: ja. Variables: let TypeScript infer, tenzij onduidelijk. Thumb rule: minder typen = minder code. TypeScript doet het werk." - -**Q: "Interface of Type?"** - -A: "90% van het moment: maakt niet uit. Kies er één, wees consistent. Ik zeg: interface voor objects, type voor alles. Maar beide zijn oké." - -**Q: "Kan ik Cursor gebruiken?"** - -A: "Ja! Zeker! Maar: denk EERST zelf. Cursor is trainer, niet antwoordboek. Denk 5 min, dan Cursor als je vast zit." - -**Q: "Dit is heel veel regels..."** - -A: "Ja, TypeScript has veel regels. Maar: elke regel beschermt je. Elke regel voorkomt bugs. Waard." - -**Q: "Hoe fix ik deze error?"** - -A: "Laat ze Cursor's error message lezen. Die zijn eigenlijk heel helpful. Ze zeggen exact wat het probleem is." - -### Troubleshooting - -**Probleem: Npm install faalt** -- Check: `node -v` en `npm -v` installed? -- Als niet: snel tutorial of use Cursor terminal -- Als ja: `npm cache clean --force` en retry - -**Probleem: Cursor opens niet** -- Reboot? -- Reinstall? -- Fallback: VS Code (gratis, heeft ook TypeScript support) - -**Probleem: TypeScript compiler errors maken niemand blij** -- Normalize het: "Dit is GOED. Dit is TypeScript wat je beschermt." -- Laat zien: hetzelfde in JavaScript zou crash zijn in productie. - -**Probleem: Iemand is ECHT vast en gefrustreerd** -- One-on-one. Lees de TODO samen. Stap voor stap. -- "Dit IS lastig. First time TypeScript. Normal dat je vast zit." -- Geef niet het antwoord; guide ze. -- Zeg: "Room 7-8 is bonus. Finish 1-6, je bent golden." - ---- - -## CODE SNIPPETS KLAAR - -### Blok 2 Demo: JavaScript vs TypeScript - -**JavaScript (demo.js):** -```javascript -function greet(name) { - return "Hello, " + name.toUpperCase(); -} - -console.log(greet("tim")); -console.log(greet(42)); -``` - -Output: Runtime error na eerste call succeeds. - -**TypeScript (demo.ts):** -```typescript -function greet(name: string): string { - return "Hello, " + name.toUpperCase(); -} - -console.log(greet("tim")); -// console.log(greet(42)); // TypeScript error - caught in editor -``` - -Output: 0 errors. - -### Blok 3 Types +*Tim toont verschillende guards* ```typescript -// Basic types -let name: string = "Tim"; -let age: number = 25; -let isStudent: boolean = true; +// typeof guard +if (typeof value === "string") { ... } -// Arrays -let scores: number[] = [90, 85, 88]; -let tags: string[] = ["typescript", "react"]; +// "in" operator - check of property bestaat +if ("name" in user) { ... } -// Type inference -let message = "Hello"; // string -let count = 5; // number +// instanceof - check of het een class instance is +if (value instanceof Date) { ... } -// Functions -function add(a: number, b: number): number { - return a + b; -} - -const result = add(5, 3); // number (inferred) +// Literal check +if (status === "approved") { ... } ``` -### Blok 4 Interfaces & Types +*Tim voegt voorbeelden toe* ```typescript +type Animal = { type: "dog"; bark: () => void } | { type: "cat"; meow: () => void }; + +function makeSound(animal: Animal) { + if (animal.type === "dog") { + animal.bark(); // OK! TypeScript weet dit is een dog + } else { + animal.meow(); // OK! Dit is een cat + } +} +``` + +> "Je ziet? Je definieert types, je doet checks, TypeScript helpt je." + +*Tim slaat zijn handen in elkaar* + +--- + +### Slide 18: Intersection Types +**Tijd**: 0:58-1:01 + +*Tim toont een nieuwe slide* + +> "Intersection types. Veel minder gebruikt dan unions, maar handig als je 'multiple things tegelijk' nodig hebt." + +*Tim typt* + +```typescript +interface Timestamped { + createdAt: Date; + updatedAt: Date; +} + interface User { id: number; name: string; - email: string; - phone?: string; } -const user: User = { +type UserWithTimestamp = User & Timestamped; + +const user: UserWithTimestamp = { id: 1, - name: "Anna", - email: "anna@novi.nl" + name: "Alice", + createdAt: new Date(), + updatedAt: new Date() }; - -type Status = "pending" | "approved" | "rejected"; -type Response = string | number | boolean; - -const orderStatus: Status = "pending"; -const answer: Response = 42; ``` -### Blok 5 Functions & Errors +*Tim wijst naar de &* + +> "Die ampersand? Die zegt: Neem User EN Neem Timestamped. Merge dat. Het object moet alles van both hebben." + +*Tim vergelijkt met union* + +> "Union (|) zegt: dit OR dit. Intersection (&) zegt: dit AND dit. Je hebt alles nodig." + +*Tim geeft een praktisch voorbeeld* + +> "Dit is handig als je, say, een generieke Timestamped mixin hebt die je op alles wil toepassen. Models, users, posts, van alles." + +--- + +### Slide 19: Readonly & Tuples +**Tijd**: 1:01-1:03 + +*Tim opent twee voorbeelden* + +> "Twee korte dingen. Readonly en tuples. Readonly: je zegt 'dit object mag niet veranderd worden.'" + +```typescript +interface Config { + readonly apiUrl: string; + readonly version: string; +} + +const config: Config = { + apiUrl: "https://api.example.com", + version: "1.0.0" +}; + +config.apiUrl = "hacked"; // ERROR! Je kan het niet veranderen +``` + +*Tim wijst* + +> "Protect je data. Iemand gaat accidently de config niet overschrijven." + +*Tim gaat naar tuples* + +```typescript +type Coordinates = [number, number]; +const point: Coordinates = [10, 20]; // OK + +const badPoint: Coordinates = [10, "20"]; // ERROR! +const tooMany: Coordinates = [10, 20, 30]; // ERROR! +``` + +> "Tuple: array met vaste lengte en vaste types. [number, number]. Precies twee numbers. Niet meer, niet minder. Super handig." + +*Tim voegt toe* + +> "Fun fact: useState in React? Dat geeft je een tuple terug. [state, setState]. Dat zien jullie in Les 5." + +--- + +### Slide 20: Functies Typen +**Tijd**: 1:03-1:06 + +*Tim toont volledige function typing* ```typescript -// Regular function function add(a: number, b: number): number { return a + b; } -// Arrow function -const multiply = (a: number, b: number): number => a * b; - -// Void return -function logUser(user: User): void { - console.log(user.name); -} - -// Optional parameters +// Optional parameter function greet(name: string, greeting?: string): string { - return greeting ? greeting + ", " + name : "Hello, " + name; + return greeting ? `${greeting}, ${name}!` : `Hello, ${name}!`; } -// With type guard -function getUser(): User | undefined { - // ... +// Function as parameter +function executeCallback(callback: (value: number) => void): void { + callback(42); } - -const user = getUser(); -if (user) { - console.log(user.name); // ✓ safe -} - -// Common errors: -// Error 1: Type mismatch -let count: number = "hello"; // ❌ - -// Error 2: Missing property -interface Person { - id: number; - name: string; -} -const person: Person = { id: 1 }; // ❌ name missing - -// Error 3: Possibly undefined -const obj = { name: "Alice" }; -console.log(obj.age.toUpperCase()); // ❌ age is undefined ``` ---- +*Tim wijst naar elk stukje* -## POST-LES CHECKLIST +> "Parameters – explicit. Return type – explicit. Optional params met ?. En as laatste: functies als parameters – je zegt 'dit moet een function zijn die een number verwacht en niets teruggeeft (void).'" -**Direct na de les:** -- [ ] Weet jij wie alle 8 kamers gedaan heeft? -- [ ] Noteer eventuele "stuck" punten (welke kamers waren lastig?) -- [ ] Hoe was energie? (1-10?) -- [ ] Feedback verzamelen (ask a few: "Wat was het lastigst vandaag?") +*Tim benadrukt* -**Voorbereiding Les 5:** -- [ ] Huiswerk controleren (wie heeft Escaperoom afgemaakt?) -- [ ] React + TypeScript slides/demo voorbereiding -- [ ] Props-typing voorbeeld code klaar -- [ ] useState example met types klaar -- [ ] Event handler examples klaar +> "Dit klinkt complex, maar het maakt je functie super duidelijk. Wie hem read, weet exact wat je expecteert. Geen surprises." --- -## SAMENVATTING LESDOELEN +### Slide 21: Generics Preview +**Tijd**: 1:06-1:08 -Na deze les kunnen studenten: +*Tim toont een korte intro* -✅ **Begrijpen waarom TypeScript nodig is** -- Runtime errors voorkomen -- Betere IDE ondersteuning -- Code zelf-documenterend -- AI tools helpen beter +```typescript +function firstItem(array: T[]): T { + return array[0]; +} -✅ **Basis types annoteren** -- `string`, `number`, `boolean` -- Arrays: `number[]`, `string[]` -- Type inference snappen +const first = firstItem([1, 2, 3]); // T = number +const firstStr = firstItem(["a", "b"]); // T = string +``` -✅ **Interfaces en Types definiëren** -- Interface voor object shapes -- Type voor unions en andere -- Optional properties +> "Generics. Dat is de magie van TypeScript. Je schrijft code die met ANY type werkt, maar nog steeds type-safe. Dit is een preview. Les 5 gaat volledige focus op generics gaan. Voor nu: 'ooh, interessant.'" -✅ **Functies typen** -- Parameters in, return type out -- Arrow functions -- Void return type -- Optional parameters +*Tim sluit het af* -✅ **TypeScript errors lezen en begrijpen** -- Type mismatch errors -- Property not exist errors -- Possibly undefined errors - -✅ **Cursor gebruiken voor TypeScript hulp** -- Hover over errors -- Cmd+K voor explain/fix +> "Als je denkt 'wow, dat is complex' – ja. Dat is oké. Generics zijn avanceerd. We werken eraan." --- -## EINDE DOCENTTEKST +## PRE-PAUZE WRAP-UP (1:08-1:10) -Dit is je complete guide voor Les 4. Je hebt alles wat je nodig hebt: -- Wat je gaat zeggen (per-slide, met timestamps) -- Hoe je het gaat doen (live demos, interactie) -- Code om te gebruiken -- Praktische tips -- Troubleshooting -- Check-ins om momentum te behouden +### Slide 22: Veelvoorkomende Errors Top 5 +**Tijd**: 1:08-1:10 -**Totale timing: 180 minuten (3 uur) — afgestemd op Les 3 format.** +*Tim toont een slide met de top 5 fouten* -Veel sterkte! TypeScript is een game changer voor je studenten. Ze zullen het voelen. +> "Snelle summary. Top 5 errors die jullie gaan zien. Eén: Type mismatch. Je zegt number, je geeft string. Twee: Missing property. Je hebt een interface, maar je object mist iets. Drie: Possibly undefined. Je probeert een method op iets te roepen dat undefined kan zijn. Vier: Union argument error – je geeft function een type die niet in de union zit. Vijf: Property typo. Je typt 'emial' in plaats van 'email'. TypeScript's got your back op all five." -Vragen? Contact me. Goed onderwijzen! +*Tim wijst naar het scherm* + +> "Als je een error ziet, read de error message. TypeScript is pretty descriptive. Het zal je vertellen wat wrong is." + +--- + +### Slide 23: Cursor + TypeScript Superkrachten +**Tijd**: 1:08-1:10 (snel, ~30 sec)* + +*Tim opent Cursor* + +> "One last thing: Cursor en TypeScript zijn BFFs. IntelliSense – je typt 'user.' en Cursor zegt 'oh, user heeft id, name, email' en autocomplete voorstellen. Go-to-definition – je CMD+click op een type, je gaat naar de definition. Errors inline. TypeScript errors verschijnen right there in je editor. Dit is super powerful workflow." + +*Tim sluit af* + +> "Okay. Pauze. 15 minuten. Stroom jezelf voor de escaperoom!" + +--- + +## PAUZE (1:10-1:25) + +*Tim zit even, drinkt koffie, helpt studenten individueel met vragen* + +--- + +## BLOK 4: ESCAPEROOM (1:25-2:25) + +### Slide 24: Escaperoom Introductie +**Tijd**: 1:25-1:27 + +*Tim staat op, vol energie na pauze* + +> "Oké, moment where the fun starts. Escaperoom. Dit is een serie van 10 TypeScript kamers. Elke kamer is een codeerpuzzel. Jij hebt errors. Jij moet ze fixen. Moeilijkheid loopt op van easy tot heel hard." + +*Tim toont het scherm met 10 kamers* + +> "Kamers 1 tot 6 zijn getarget op basics die we net geleerd hebben. Kamers 7 tot 10 zijn bonus. Ik zou zeggen: doel is minstens 1 tot 6. Als je 7-10 haalt, legendary." + +*Tim wijst naar de timer* + +> "We hebben ruwweg 55 minuten. Dat is harde sprint, dus focus. Let's go." + +--- + +### Slide 25: Escaperoom Rules +**Tijd**: 1:27-1:30 + +*Tim toont de regels groot op het scherm* + +> "Rules. Eén: AI is DISABLED. Dit gaan jullie zien. Jullie doen Cmd+Shift+P, typ 'Disable Cursor Tab', en hit enter. Ik check langs en zorg ervoor dat AI uit is. Dit is practice, niet cheating." + +*Tim steekt zijn vinger op* + +> "Twee: Geen any. Jullie voelen mij al aankomen – any is de vijand. Als je any typt, ik zie dat. Not cool. Type het goed." + +*Tim wijst naar de timer* + +> "Drie: Timing. 55 minuten. Daarna sammelen we en bespreken de solutions. Jullie kunnen mij helpen als je stuck bent – ik loop rond. Vraag vragen." + +*Tim loopt de klas in* + +> "Laten we beginnen. Open de escaperoom. Camera 1." + +**TIM'S ACTIONS DURING ESCAPEROOM:** +- Walk around checking Cursor AI is disabled on each laptop +- Check progress on the kamer slides +- When students are stuck: + - Ask: "What type does this need to be?" + - Ask: "Is this property required?" + - Avoid: giving direct solutions + - Encourage: "You're close. Check the error message." +- Keep energy up: "Nice work on kamer 3!" "Kamer 5 is tricky, that's normal!" +- Note: Students who finish early can move to bonus kamers 7-10 +- If someone is really stuck on basics, help them understand the principle, not the code + +**ESCAPEROOM STRUCTURE (for reference):** +- Kamer 1: Basic type mismatch (easy) +- Kamer 2: Interface missing property (easy) +- Kamer 3: Array typing (easy) +- Kamer 4: Optional properties (medium) +- Kamer 5: Union types (medium) +- Kamer 6: Type narrowing (medium) +- Kamer 7: Intersection types (hard) +- Kamer 8: Type guards (hard) +- Kamer 9: Literal types (hard) +- Kamer 10: Advanced challenge (very hard) + +--- + +## AFSLUITING (2:25-2:45) + +### Slide 26: Samenvatting +**Tijd**: 2:25-2:30 + +*Tim staat vooraan, rustig, reflectief* + +> "Oké. Hoe ging het? Goed? Escaperoom was crazy, ja?" + +*Tim wacht op antwoorden, grijnst* + +> "Here's het important stuff dat we vandaag covered. Eén: TypeScript catches errors voordat ze in production gaan. Preventie is beter dan debugging. Twee: Types zijn contracts – interfaces en types definieert die contract. Drie: Literal types en union types geven je superkrachten voor je data te constrain. Vier: Type narrowing – check je types, TypeScript wordt slimmer. Vijf: Generics – a preview, we doen dit groter in Les 5. En zes: Cursor + TypeScript = superkrachten. IntelliSense, errors inline, alles." + +*Tim telt op z'n vingers* + +> "Onthoud: any is je vijand. Types zijn je vriend. Als TypeScript je een error geeft, say thank you. Die saves je van bugs later." + +*Tim slaat handen in elkaar* + +--- + +### Slide 27: Huiswerk & Preview Les 5 +**Tijd**: 2:30-2:40 + +*Tim opent het huiswerk bestand* + +> "Huiswerk. Jullie krijgen vier JavaScript files: users.js, products.js, orders.js, utils.js. Die zijn heel basaal – geen types. Jullie gaan dat converteren naar TypeScript. Toevoegen types. Interfaces. Allemaal wat we today geleerd hebben." + +*Tim toont de files* + +> "Niet moeilijk – het gaat om oefenen. Convert de files, type alles correctly, en TypeScript errors weg. Ik verwacht dat aanstaande week ingeleverd." + +*Tim gaat naar preview* + +> "Les 5: Generics! We gaan dieper in de magie. Hoe schrijf je code die met multiple types werkt maar nog steeds safe is. React hooks typing – useState, useEffect, alles. Dat is heavy, dus zorg je brain is ready. En we gaan wat React-specific patterns zien." + +*Tim sluit af* + +> "Doen jullie best met huiswerk, dan Les 5 gaat much smoother." + +--- + +### Vragen & Afsluiting +**Tijd**: 2:40-2:45 + +*Tim leunt tegen het bureau* + +> "Vragen? Iets wat niet duidelijk is? TypeScript heeft veel moving parts – oké als je heads spinning." + +*Tim wacht op vragen, beantwoordt ze geduldig* + +*Als geen vragen:* + +> "Cool. Dan think ik het was een solid les. Jullie did good today. Ik'm proud. See you volgende week. Werk dat huiswerk uit, tot ziens!" + +*Tim geeft een salute* + +--- + +--- + +## SETUP CHECKLIST VOOR TIM (VOOR DE LES) + +- [ ] Cursor opgestart en TypeScript extension actief +- [ ] Escaperoom kamers geladen en getest (kamers 1-10) +- [ ] Node.js REPL beschikbaar (of een way to show console.log output) +- [ ] Whiteboard markers klaar (voor notes) +- [ ] Timer gedownload/klaar voor escaperoom (55 minuten) +- [ ] Huiswerk files gereed om te delen (users.js, products.js, orders.js, utils.js) +- [ ] Student laptops: iedereen Cursor geïnstalleerd, TypeScript werkt +- [ ] Backup plan: je telefoon als timer als systeem timer fails + +--- + +## POST-LESSON CHECKLIST + +- [ ] Collect escaperoom progress notes (wie welke kamers haalde) +- [ ] Share huiswerk files via email/platform +- [ ] Prep Les 5 slides (Generics + React typing) +- [ ] Note: Welke students hadden moeite met type narrowing? Extra explain in Les 5. +- [ ] Feedback geven op escaperoom – "Saw you struggle with kamer 5, we'll revisit unions in L5" + +--- + +## VEELGESTELDE VRAGEN & ANTWOORDEN + +**V: Waarom TypeScript en niet JavaScript?** +A: "TypeScript finds bugs BEFORE they reach users. JavaScript is cool, but errors hide. TypeScript is like having a co-pilot checking your code." + +**V: Is any echt slecht?** +A: "Ja. Any means 'disable TypeScript.' Soms je perse any nodig (legacy code), but aim to avoid. Als je any used, je misses het hele point." + +**V: Difference between interface en type?** +A: "Both work for objects. Interface kan je uitbreiden (inheritance). Type is general – unions, literals, anything. For objects, use interface. For specifieke types (unions, literals), use type." + +**V: Waarom TypeScript code langer/chattier is dan JavaScript?** +A: "Ja, more lines. Maar die lines beschermen je. Denk: upfront typing investment >> hours lost debugging. Totally worth it." + +**V: Generics zien complicated. Ga ik het ooit snappen?** +A: "Ja. Les 5 dieper erin. Think: 'code that works with any type but stays safe.' Clicks in na een paar examples. Trust the process." + +**V: Kan ik TypeScript in production gebruiken?** +A: "Yes! Dat doet iedereen. It compiles to JavaScript. Production runs JavaScript, development checks TypeScript. Best of both worlds." + +**V: Wat als ik een library use die geen types hebt?** +A: "Either: DefinitelyTyped (community types), or any as last resort. We see both in industry." + +--- + +## RONDLOPEN TIPS TIJDENS ESCAPEROOM + +- Start met ronde langs alle stations – check iedereen aan het werk is +- Observeren: wie struggles met basic type syntax? Zet mental note. +- Timing checks: "Kamers 1-3 moet max 15 minuten zijn." +- Encouragement: "Kamer 4 is where it gets interesting!" "You're doing great!" +- If someone stuck 5+ minuten op één kamer: "What error does TypeScript give? Read it with me." +- Avoid: "Type it like this." Better: "What does the error say? What type does it need?" +- Celebrate wins: "Yo! Kamer 6 complete! Nice work!" + +--- + +## ENERGY MANAGEMENT TIPS + +- **Opening (0:00-0:20):** HIGH energy. Hype them up. TypeScript solves real problems! +- **Type Basics (0:20-0:45):** Medium energy. Lot of code, keep it engaging with live coding. +- **Advanced Types (0:45-1:05):** Slightly lower – complex concepts. Make sure to pause, check understanding. +- **Pre-Break (1:05-1:10):** Recap energy – build confidence for escaperoom. +- **Pauze:** You rest too. Drink water. +- **Escaperoom (1:25-2:25):** HIGH energy! Walking, encouraging, being present. This is fun! +- **Afsluiting (2:25-2:45):** Wind down. Reflective. Proud of them. Preview excitement for Les 5. + +--- + +## LIVE CODING NOTES + +**Slide 8 (Basic Types):** +- Open Cursor, create `types.ts` +- Type examples slowly, let them see the errors +- Hover to show inferred types +- Make errors intentional – show the red squiggles + +**Slide 10 (Type Inference):** +- Show how Cursor reveals inferred types on hover +- Emphasize: "TypeScript knows, even if you don't say it" + +**Slide 11 (Interfaces):** +- Build interface step-by-step +- Create object that matches, then intentionally break it +- Show missing property and type mismatch errors + +**Slide 16 (Type Narrowing):** +- Start with union type error +- Then add if-check and show how error disappears +- This is the "aha!" moment – make it clear + +**Slides 4-5 (JavaScript vs TypeScript):** +- If running Node available, show actual NaN output in JS +- Show TypeScript error in editor + +--- + +## ESCAPEROOM EXPECTED COMPLETION RATES + +Based on difficulty: +- **Kamers 1-3:** 90%+ students finish +- **Kamer 4:** 80%+ finish +- **Kamers 5-6:** 60%+ finish +- **Kamers 7-9:** 20-30% finish (bonus) +- **Kamer 10:** <10% finish (ultra-bonus) + +If most students stuck on kamer 4+, may need extra support in Les 5 before moving to generics. + +--- + +## CURRICULUM ALIGNMENT + +- **This Lesson (Les 04):** Types, interfaces, basic constraints +- **Next Lesson (Les 05):** Generics, React typing (useState, etc), advanced patterns +- **Later:** Advanced types (conditional types, utility types, type helpers) +- **Real-world skill:** Full TypeScript mastery for production React/Next.js apps + +--- + +**END OF DOCENTTEKST** + +--- + +*Vragen or opmerkingen? Contact Tim or course coordinator.* diff --git a/Les04-TypeScript-Fundamentals/Les04-Lesopdracht.pdf b/Les04-TypeScript-Fundamentals/Les04-Lesopdracht.pdf index 2cd9b5f..aebc797 100644 --- a/Les04-TypeScript-Fundamentals/Les04-Lesopdracht.pdf +++ b/Les04-TypeScript-Fundamentals/Les04-Lesopdracht.pdf @@ -122,7 +122,7 @@ endobj endobj 17 0 obj << -/Author (\(anonymous\)) /CreationDate (D:20260303132400+00'00') /Creator (\(unspecified\)) /Keywords () /ModDate (D:20260303132400+00'00') /Producer (ReportLab PDF Library - \(opensource\)) +/Author (\(anonymous\)) /CreationDate (D:20260305162606+00'00') /Creator (\(unspecified\)) /Keywords () /ModDate (D:20260305162606+00'00') /Producer (ReportLab PDF Library - \(opensource\)) /Subject (\(unspecified\)) /Title (\(anonymous\)) /Trapped /False >> endobj @@ -133,59 +133,59 @@ endobj endobj 19 0 obj << -/Filter [ /ASCII85Decode /FlateDecode ] /Length 1195 +/Filter [ /ASCII85Decode /FlateDecode ] /Length 1201 >> stream -Gau`R95iQE&AJ$C9Q*Z@'6*''2G:ib7oqr;lk4^qW8Z^8YJJ-#6rRdVu'9YnUWFl/)Yi-c%&c6uKnnL[R435cUeNCOL(g7B"Apjt4+I@j'U!k^G&H>0#"IfEhE\.gJ&J1qc0J-(R0`:59g3Pn;7^5Ihu\A/js0POb,6J):u6j,?&n3EM\pd.ur:E]YE>LtR2(Mt+&\'CXW_D!Ja_JG6T+oH+nr5OV-rk/9m1<1[O\!R%>*2dl1?9=ea,'54nI#1:$4,b@!'X"N6_F/c0Y]i&pnPV+7bh;q4cTaZH&4IH/1#tGK>Ib@(^7BeOY.OY:BI4!9rNl4N43mhUNt2`m+@8;]BUHK_lW=*[Bs7"Rkt:2pG#;Xa4VrEd`oe%3%CMl0a?.0aN@5:CsL((Bkbjm2*Z]-Tfq.q)dBb+"[4;F>"s+LD`:>af&cY+e7l?bmo6=0@?h?B;e^k9S=Wrmhoq.A+]\eO9CcS[kuB-YPnXrq-7,nA!57An#F+E"-X7WL55.&,,4=Uf2mtBq[9i_a,(AaLjGa2=$@&Ydd^8nOD]j@]gf@E1MSpef'G%M!=4R.<0H7/%'e!%po[&<_J46K`F0r\T4dR0^C(C?:0D4O9!A"nT>:@_Y@1.I`?SogF2W,U?(\l`UU;PlV^lu/^cQSf[j=hF=YDcSk#];pR5dF_/tADj9bW>H9lpXgY;=X-=B<_J2fREZIA)KPD-#Nf\t\3h1LWup#p!__"BPH[X3qTZ(f(lsIhT[s]>UEUmKrdWDeD&Qc2`=N@6ch#h44Is`X?C;Ai#=OBh5DY,MrVk)qYZ@QP'CtRMn#BIWRAsPIO8;A7lQklq(j)Xm*9Iq/->\E)o`7^WZLr140^2j)]dfe9/1)/2LS1eCCsdQ-YFB'9!rc`6\+YLl\Z9$iEW:pW4S8-X!=:knTR1/m=?CI=@Pg?R=UY/qDbr4nZcg,/iNpPV>23hN+h.5(=lQIdkZ30eq?PD-D&i~>endstream +Gau`R968iG&AII3Ci4:56i[c$_-B#r,<*oXE]g&]M_9qobEPRb"t?DAoZ).^;R@]QEA!Z>B'^*NbP`LZ;%+k%R/8ru!'\?jm[O=<&DmL%6pQ,+PBc^Z@bjVTLbCcDd.`A04MYa-l^.;_lh;Jk6n$0p!!qb]#T/YZ7fo;l(F+;QlauX3`!*TbJZGXO/;+IO6"Ic_#S4j&N%];Wi]Y6[XRt,TbuQ\qWd1nuesPG"i[)N]_:u_W&?]AV;^tP,0B=i)1/RrKcbiR2$6PXW7(+K\OabZP37%SY<'uWn.KW[_?'WM=elgh14sYij\9r(.$h;O1Cn-b"e%)Vn-h&d2a0uf(%H52CT9:t34B!:^Y2OckncrHh1g?dM3n<>rb3s+M)2g):>r7/JR@Tj7X=^lHjJO[hW@#gRV7O.i5&?00L*^?#'GYGI'):#[^$+'<`#$QD66>m8TRtN]\.S^ko1O;0QTpFb+&Z(Oo`/dGlkS]CY`7Y*W(uq)H*NrOh/-,:I!ZXH+(t(g!["Tme)0kL%GSjKKSCs=#06I,^OP/kmUCsNel?PEWm;Ttl*:N">3IFpNIT;AJ$Y:F8e@>e"Qj;lm90CKmD42Rq7Ear2;*W*OupG,q0dh:4g+kj5`01,.1-dD6g`qm;o_30)6,V/l.q0I*op]$fE=id$T\GN8@ko6+rI=G/FVibfO61eWRj?f.1>'/JP?<(tFkBHGmN^<;E:T6qL1>pWkCW/&,urI1imBEtF]2b-NBPOdNJBFR%pJ;UH]bp3H]e1aV%=fhT*Q^8\A/S$7Ik?Y0Z[@HDJMr8UL-CU*)*R8\q!Q\UMGendstream endobj 20 0 obj << -/Filter [ /ASCII85Decode /FlateDecode ] /Length 887 +/Filter [ /ASCII85Decode /FlateDecode ] /Length 774 >> stream -Gau0A9iKe#&A@7.CcS=G#YC=mM-HZ-V8KYoG^ARm0o6/CNbrUqd/M6(nh=@0=_(m6m?QXDp;_Dmb%@&mtB"VBSAIS1M2o9CgLZaDc/rfjj_<3C$IYt[kq'ubbIr#(YX,l:p(p"Kh`,*^4:$5r=&G:>r#e?0K\VZ2c;-qtS9_-qYZ1o4?9^IO`K#iD&UAQ9N]OC8b##1CS/'bT[OIsOOU\"n*gL%h-a>HJ>oA+&Qlb$F\GPFHC99sZj?EY2I4\j^US>dn-toLhG4Go[$[BprVCZK63'3WG6IQapL'&1>h7Ym6/ZN@l'2O%DWA$`HNP*'6AoYa6&@BKk&ED(=U.*"p(l7at3!d@P'?M^m$o\S)YnPUu)076g/!ncD>)F?CrL.kk%Z^.?&riX=AoK2tee&W:iNH`Q5-\7ZcRLl'4FS$q0mpI7EgV*[Wf!g2>L$`GV@C$R+$R3@e&#]LT7)bF>qF_HC4uK:F#JIpKk!@YSIdTd[3#Ga6QZScXDX'1RHP[uhLY~>endstream +Gau0@9i'Ou&A@P9(cZ@uF`#0+iDq@,WDEsHlCm2MZGYZJRtN&6.)2?FS6`Q2-l7]9=IpCkhg=Cg!)>oRs(i!\SA:K[!ec6f(5--(P&iTW-4^bE6lO)i,:-H/Qkls'D3N/pr/ob/+"!4r*?[@%8EOOUcAJJno,P#i)-0k`YfJpb7t#W&d,D]d*?-_[`l;I>@jQY;j%MkI;3i/Q1+>8k*-=k@#R**&mD08Kh/cGEMh\_DaFk8]`&&ENSH@@o?'g\p<(l]]paAnt@_t]YhpC"XZ13=*`e.D(k365hiFGqIAIW"m@--.VE>H]7Tc@-<36SgihFm@%jm;L%cJQ'c(MboUlj?=]f2A*SU&T2Lr\r`12`\#Y-gAnT3*du$k#)%gdC@p*5H&6CGF74(!eILWh,\+I>E@f]TcN#W2I7fTR=*>hW)j.5*V=?I.V;GE(QC_//Sl[Wd/5nsUCqbu!coh(OTf@.cNYj='YuY]`A/4uR21>adoOmWKT_MG?;:k\]05IM12&:I4HW%$XLmTU@!YO@cp[;&GC#3n>0Lcs\3J]HlP+dl4F1a*XR!00i*&_\Za9nnd*i4Y3%H`X`I&1ko6UmCSI_uUCA%@X3QsV&_Co;";OXo6bGN+-]hnR2*s9F-%%OkZ,#a)H,V9_d"AR_f>6JMJd9[BsEFTfh$$Epbh="Tq#=^#6>QHMPVfC(q:n=uL`PMML8*fLp"g_VE\/Y"M?!o_:XoD~>endstream endobj 21 0 obj << -/Filter [ /ASCII85Decode /FlateDecode ] /Length 1127 +/Filter [ /ASCII85Decode /FlateDecode ] /Length 1175 >> stream -Gb"/fbAu;j'Sc?E`Q=-NVQM2f(V4?6[&(;\::nV;0oFuk&s3WojQV#-9L9!hLRND8nVf3.oUhoIaRX6V":prn_Z@PZ!$AsPTF?EVT\SN<*'lZ"4>^a!e+GR)^3SB&m\"6:@(/6N_h`SCfCFj0fKKb'H7gd=#6SXG[(j&10"3"6LL;LqHbLn$#Nt#"Sn0p/+/_RkS(YRto%`24eO18502q[SoD)O"IAfXOos%\lbIpTs-(`FqqV1FQ9u>b`l1unVmH$-leq<&V?uK'HbpM^OTTQm(15J,tVn>PV)L4c@a!-,-mA!`_S#m'6fbSeB]>",5V]F"'cV!9!HeGKFYnZ&LRfn!KR*&kOJ"6hj':9(`BnlSK8[JM4%%[^0hi1V](%gUO<#(MEOd]X?g27->H"0<`%hlNl(7>Bd"pS8.E7G8t[nBZD\5BOlDVt(g)TY]DG9nLFUH"/-18t8UNkEN-(!TM$g_B$nY%Mg1WZFh`T0Omrc]a=b9?N:T4a1s<")14:-m9!ErO=?kkf*;0.a,&VXJu@#S0f?;880+W\>F\D[1`h-)JqsTUFMEa-9")5n"<+]@Cn+c3_mqWZ\@9T\V#qdh89=USfdeH#D=3-qk]63nPV6T7dC-X:g>=X*;,]O++j)Bk4G[X[/8Q-oseZn?2A-TT-'72r0q_G;oc.E?KIRWt2d3sc'C8<)MffBBj)jRbqI5D^VQKQgVo-]Ma_MNrFoq`s1#8u#qo~>endstream +Gau`ShfG8V&:W5m,h]J>i'c\k0S@`Xp[.CXH`'XR#[XmK-Zaes1&US]"j.t%!m2MnnJ%^BF>KX1:D#Rh?jo7%LBdjtAKuR6bkJn]bi^#N-Y+Aj%$Q]nZcj)W[ScN]a7T`F^aQ^=KqS*mDG=Aj&Y$;I#rZeN^($$GJo4W0Eu9CaIZL@u?UFZ2i`EPVa/?68YQg`i3m6@R=;pjplO6'rk[KljpWg%L%TrEt\`JC]DYCXPa`>;)^po@,U2#G^PIpe7RS=#>?P[GAR$culkV@Q=5[,`;?SG9NF>m])._7cA?sFc^s#/M*@c_`W1-^eUi9);Zu?=VsuTi-hd!=f,,o+]gmm[\2[uWf%H`AEi6*C[I+artbo7tTa4h]T8f;;[VO/e\DjFOpk,Pd2m21,VOgZuk)tR1sgHEmQ%]dQrh>%[5KjT;-/FR76Y4op,;&Usk9L2SIpSN(e*@4%;]AHWXAL'SVt6XBcX#9Gr4[8I\SDlK9doK'21YSUt",.)"^J:R>>/Wl.V>'HT$[MiO"_Njg5YfLFW7,#p.i?T^s?j'FKF;8F,k-klKH@f+m@bjdhLr83ad];Vn%cdT5a;kH6oCQ1j$G'ud#]gKOuU(=UAGuDfg:\qHa,L6FT*JPI*6]/bmh/5V"3>I@f"j]uH)[DQ=9f@WCh\J7MMlINJo`N5ZDE&('4Bj8Fr%B8=Z\c9&c`%(L6ZCr(1.rb(?$+/MHbP5&Lh(ecWP-pa:hE#6&DW][R-:UI(FN&qN!p$@a%//ie*SMn#jar\b$<',Aib\)f2S91WEH["G;19G5kBFoL;7+HWiXQ&LC^7a-rX\8e-lXo8W>e3g?YZV4^9@q^M%.iI?q[pE11FFZls'Q"Vr>(32j'KR);e!)/^Uf(X"W.G\!lZi8t7nD#DDG!-:+g'bGo5\PH&h@;n'M]nh8W7A3)ILXN.;BN&_#J)/WG3AK3]JuD?gmY(`B,@7+QK(Fo('E$USW\XC^..Xe9K1n'Q@J?FHdD,Ht=;L~>endstream endobj 22 0 obj << -/Filter [ /ASCII85Decode /FlateDecode ] /Length 1082 +/Filter [ /ASCII85Decode /FlateDecode ] /Length 1095 >> stream -Gb!;c?#Q2d'Re<2\;t?jGd]=2qUTEI(2_kT>$q-CLsnBgP@;hFrq]pA7oStIl+1RqMBZPjO3`N,NYt[+=T'2:!,!If4D2GhKG.aF#RK>*l`g,Y`tgINOEh=[5Y@C\I]B'&+C,H+CpZ;n7<218PXI^"la"^M>_)_q!t$I?n"D?a;5:X+$9Y=;?apRB*+#Fn,M/Z@$P+:&;ejS:boGi\FPQE9hofXR>NjJ2[Yq^WKMrd\)F"[F&\&-#d'iRLNQ4M%XAb@l_[k1>jbQlTUuKkHI[EfI*$@o_RV&Lb+a,'cpnl>6gr:?$]*h?l8MG;Fs,S/W8@f,D&48#V?'Xkp9B=J_hep]B^T?k_jpV)#sSo:K[[%eqTuo=qC))MN]`9r[h?MH4n^-&k-\o4]\t8Pf8$tJ-g9hOfEoeOQC5@t)t[]@1lrdmSX9tLn+L+hn$IdH?*NY?cpF`bfWVTQ-HW"P!49!7qU>L?7o8L1:)1a>$rQ9-BLg+NfSFe\Z+;L%g8`[,r%cFD@6$E#muc-T'H"/-<,Q.l.T[tRo;t+uP$;m[mdm5phGUI.fHX=nHkN-n%Nq5U3\sWbInPFoFrpaVP,iH]im/:?gBuI7HZ:MR^grSb#8fc]4Q[J%s)?nED;toJ@<"2CdFLD;[oDU"C2u72PF88/QNgF&K8oZu!c4AWAaE.Bm/>*a\nM%.9p`",$P[3&MDXellPCrAC$TQ)lWF70IIfMm@jZa;+2@/MnV#b;`^[$W~>endstream +Gat=+9p=$c&A@7.oP#h['-YK',gU6$c]iRo^cN'fW2-A[3E*P>'95=TH0M2P%r49?rGdBS^N?ZXt/jk\#R-r(qOOsE.u0Sq3B(?SC_,Z2(.!D+P:>3t74PMq!Vb0(?kPMrn5!CJb^d\]`fga(GrgW&s]YUs&td`"D'p(1a,"O[NGT6d^PGjPL9(>0B8!h2A+/_l`!/:M4!XtrSD1l5q2"Qal?82/j=g:Ac#)G1a*q>]HK[L=QUN&Ttl$b&H!b3N\\iL`@M'=Kcmh;B@="t93Q&*+[o0f8>Y17hoXAqS27B&J@!KCPuYG*1G'"ul_4^sd+D7pkp)Q2'Ye!ns=oPpTtr1gV[H48O_a/.Dmr9.pLj7Cuo46"a8@u?k8ko=7ChpYn*atLVr\ed+_hoY#+S=-$DklEFM57i$;fkuB"&W+kUKL)Nh6aq5YWeoXg%tjjjl3$*?X9K?tElIibld^"U;^'dJ/2B/7WY"M5`D;g:k4T-;=4O7n=7FNM^RM&#Cr_qg[dWd@.R-6agqURJpHC[gN0g0Q<38Xcr$(lW6Pu66p(uX^T;$Z3C*3=TEc+,qb1%endstream endobj 23 0 obj << -/Filter [ /ASCII85Decode /FlateDecode ] /Length 1255 +/Filter [ /ASCII85Decode /FlateDecode ] /Length 1281 >> stream -Gatn&?#SIU'Sc)J/'`J&'f@4bkf^bdPN%j1*9VAk%E$!^e8TF?te:dG402_[rY^0]/515H2H(rLPc6kh?qG(DjZ(A^95$\_B-`%2Z`LL"4'2p;eaWf9=^'-;I+"BN@u,u.W,)!9C\\>!1d+AoHt[N?=*q#]9/:,Xg&I$MLlFSZSuS&-2[hHdgeIXM)=,V)Dl"Eink(B?ETHX,#c0Xok^X`]Nk;Mg(ql+PAV!V7N@]/Yag\B%i%MGCSB/lh^R8V3f^s2i7FUe.MChA%r+@8#8bM,B958Pu'qOrr2EKjpJQ/qg4T&@'Ao_YNDcE8)SI%<6j*iTmXXc:q`q.HqLNtQ?>.Ei!$2._H:>@c\I#^0dFOG_CcBDB`$*XJkqIoWU)sK^"8/eKOa3W$A&L1C;5P@Vt>ntX$pl6].!%;*%B*4.P2`[)3T'\_G.6Vj>j&)?n1U8S4@GrGrulV\D:2"6Y::Mo<#r^SmRVl1L6VoD2%MEAY,k#3K*?dAd>;Q$?_&8omi'H3s=l2#mdB*_Q.c:(UadIi]1#"TC+=l(I%qO*=Uj+SW.$G=crFO$2KB_)r(HiI=[O)')QssK6oW_u]!<3T+8GrX4](`"V;;"`0bKr)/p*&\rT?H_1^2qie$A1K(Q;oG2Zt1]H'd1?X.pHoh52[;R#A[,l`SZ05+no`4so>1s+$m&-Qn()-:He*P\XAM89<:%:#&%6QbpAKPnk1V1QR=IWcDasn`Zb+<+afWVJ@aN5O/r(J`U0@,[!I-XB%E!aR-Yendstream +Gatm;hf%7-&:Vr4EU]/n`/7)_^C1N>M`G!GYK[uhHeAcY;\!Z4E[f[EKDk=%dW4F$(8M@nJB6[iGose^G=N6anKIUEZ3#88JX]jFKYeu<_AicD0oRG\hlfT4)+9K$:l=__@8TF4g(jSmB]t(9BukZ':/#/PCZtSZZIA^o0Ok7&f^V;[NBdkBhr(6k5rTaL@RM.oE2"g7Npd)1H3(!W:KGjBleM]`UX<0<3.a5>IoJH^=?lW9#:$_?'(NAh_KNGmd?`J]_GL$GrjW6lbfABm&0>l,@G\+1Uh&Ii]Unrf=p<+J<"[p7-#=63D>V,/sfWcXZhs'.k$;+W3*;2P7-53?^jFefdh_O%Yj;`o[oSZd`*a/Oc?2TVpZS[eEgTF#a6Q2=QTogUQ0MU't?Eb!X%f,L)K!D84^GZMu+bTtB,G`m,mOpb74Ash:_)'f_:=k\ODQNnIr6YOiBNlRa9QP%7[_Oh0\maQIY_8c7n;Zs9-kE31Ynr$$bdYc[nYc>9BX\Z2Dj_?ERR@FYd6X7qU>EGt!pK5Qr"oJclljbgf8NlhbHGtMAYJmSeXDb^/1rf5SBp1G3GW&nuRpZM3M8]JdVMdLO+PV0cdYBSUUO>kA\pY_h'lks_d:ms_=qY.t1]=*i,/s+K,18k=M/3!"(_G&=q'2Ju(LP&jf93.1"IpgcHfO*#Q?c$hJ-i@il$b3EK=3Zm`*I.gCK&ls:WN&tWZ5Sh08j\Lb5]*+@eR/nI,9Wt2Ge7X(K>K,dA?!'biN`^89I&q(Q$a:\E%m[K'n&jN`YoZ,#t)]i>O7Rb0+?&ki#')_RK[h%t44u]q[]TMXen+^-;%fQKX\G?_HAVh'~>endstream endobj 24 0 obj << -/Filter [ /ASCII85Decode /FlateDecode ] /Length 1087 +/Filter [ /ASCII85Decode /FlateDecode ] /Length 1224 >> stream -Gatm9?$"IU&:O#N=Pa*X<9mcV$POY1B'?(9Gj:4QW34V%16s^)r;+$g'B>CC>\WPs('k8/R6FOlHE`Z`4]Ai00jI58`Qq;TG1U:Woib,n6^gqfo1PsM^]T:YI]WpiL[N1LV?FfpDC+KFU0:.SMk>4%Lm;DL3qGZ$)Q##l\DlVTJKpupXoTf(P(:Je2ea:B1*rHQ+>?aJpUU:OkMH4F[W9,Y(4VX6^,01r;E?^?N)MpkZKVUk2Q3nTOG1XZL;Q6li6)TD%,VdYK\9Wft__mMek9FHf=Eujl#ru*`dG8[cC%gt(!SQT4eOTD=#bS::(RNh0q5:\Tme(`'\RIrG/KX:QLbX?h:@,feDJa@uCC@l&qlPXS=_CC>WUmhR@HD=sjQc"*;WJoh]=$e9Li0K(f3['WrJ^Oiu[b&82oIhAInu%F=($)P2D$3UnL=/ru9j9uo7Fd&dN[%7X&]0pUQkW#Mn\8$#l`,`s\Y_l"D_,Ftfe5cq[6<0FF.J(%%mrI/"Pu-HFk*R4AI+mE/M=0*iYG;r>ZY]C=kHc'R5a5kUJ2&Oj4@6pg([Uur9r]$&$87H*aFW?E5$f&QUj'pZbFLbHX%SCMTOoYK/t8Ts0k#nX@JuT'Z\N)M`;R*HAm>85iCM.pY+8@4ieib>D:ARTdH3.Wt-S]mG-D24bj+?6AP/JdAYY?*:0Mi6Aq3?IT$XP://5p$q&,/KOYJ"(rM-E0a5Ju8q5N^sQAO9=Lo4C*r8$*lfq63_*h5J1`Oa:b8Eb^FpfSchGolXiEeQs)j3)FrWKe"IfCjR!p:k\B3F4eG;.h^'9qDXW$!eRb674N/S%K7G?$f(#8Z:-9]<`g/=.E(b@1i$Qj,endstream +Gatm:?$"IU&:O#N=C.B1UoM.GkCj3RTTGiY4?W;UFjsB)D!,!I0YP7#Y2mMYD0&Ea(j4h-d`Q4K3P,V#MM$W/]E!2G_V#.uN'hn#9G.Zr*+nHW5::9^;o_:>c68>WM,c)\$s,0DF[mlO$Xg4M4o<$G$%EfN8PDPSKJ='Y"[p=-0F@CZ7UNI=r#YYQ4EEeCoa#r-amrNpl[]QU4W>fUm):":46mrrDHYQhVpZmhRO]4Isf25:3L=f3*"H(j73_L+/`I1^A0AT-]h9n*D/iB1;dY6o`8@o"'oh=ILBonY0!-'K1i;5Q.ZjS@oon99_m]=eRVJoWFbPs,Z1.,jAbAG$[c\,;;H99I:P`;@'6?5#Ep5Z7^a&,+:/4h3)eNIN2BH';)RKs%L#dK$`J:5/>($<;Q6nc@Wmu-*d-epX-!(aJYoHi#-uLn5`204Ok4d2JAJ7a*FQL1:p:'d<<`e7/odao*5`4\/T#rh<+$rrtuPTO`sJgK7L1N,=]LMqVW]3jBI>AK"#_90ZbBT9J;Ja(/Xml7i5<82!n\$]MFr"^Q)hu4/_N7rsSaJF0,[ga:2a(Pa]*d(*QM;fJQ!&!_!TObc<8o0eb*-:)2<8i=^Agr)_neAjG\e46%U:uMj7&/UD,b4,%F`ula+8mTSI2W&LZg%c1W.S79=V_;1;$6i%A3.nc?^Fc1ORd@[/odq7IeT-'A_6?)BA^VZ6Oc4hZto5#@SYS+dFSt18XJN,5W.H27Mendstream endobj 25 0 obj << -/Filter [ /ASCII85Decode /FlateDecode ] /Length 1062 +/Filter [ /ASCII85Decode /FlateDecode ] /Length 1075 >> stream -Gaua=?Z4[W'ZJu..ILLH\&2=2U*Qcm"nJQaclgFKD$,O,W:I>i>9Xf>8[*[??."\$H8/qp%0e)GQfL1'ktmTE:-T>NAHQn7_n1s2KS4^R688Z>'qFHNqh`u!9P,hJUnmU@WS'tnYR+S\16$iVRAs?hb@IJ`%Tc@u(`"Jjh^0Jj=Hmhi",F+*99]'6l5"If.0eU]c*k45r[^b7-"8qsDd]#@hVQ)pOFOu,rH.c#*#i5H3Y,WDP+&'?t/,5gDi^_]"T,-lfVcs\-l`p/f6>.C*)EW0n4=oiO#Mh,E6>@ZJkaZeB@+Q,.r\WmlT#FAL.bTS.:=>hb*&@?G;gre2"/TiQ1k$EGXMgfq41NT%213Bg(M6G*8&DU^U^5Z_l3trpDIM+0HCR,f3].[9bUj_kmZ=0DLs/%r.[pn?j.JgL:7#(cC#N#n2hfl6W#LWF$ap"lCo71Ale)eB5q$G^s^bk11iY-b/Fr,(C!0d*i]E,>Da?Sq6cGff"a=GNWRa??.:9b(fEOIT2bj==(1BeA.\a8r7O,Ki=MGXd>$\t>V:F=Epkk?cX-SK&(1rbFl@BZ13KDRI;G*VAffkj%_M@u-1h-_;85C9fh6^:F4,MI\9"PpuL>K>t=%VIRL4DH&XpL6&7VqpsH+fZa]I4bITe-QP*E-G5M;$ZA4aqJ`HANXA+aE;k^JI1WX:H6JHRfe?fNo6V/PhVGHUS[NRZE!I;+tV\Lsu4PJendstream +Gau`R_/A9k&A@rkVO,0UU0NkU,cd758N5I5K5A+r>G126.utG!P,M2t]Zo&P8D.n3N?S0u(n`/banPH"Ajuh]rBqK\)W2fp[oWWaKRWHN8[Q$kVS:EG+;9J\C*5^\E*U8-]/Ds,e%(HV5D/j9=#>F;,@_4_*@@CQ\h8?^`J%EAlE;ps>9??#c/\f"h2Pru9V=J/(NCV)Sr]&j[3j96DJHBR/aTKXME@Mg\+EfG/5j,5$)S8b2fRh"tnN1Z]:$.NFstEM+WdW(anQ3I\).S_;tb7/$_R"pDW_]P%9T_D2J2c$9%0"5rj8mq%&2KF(gF1]Bp$CEQLGc]rWGs;k_b$TVQ%^MqtPH*7\jbeWp_Sk6c'JJSfH4=&gB9+$@(LM&![G\LeYYd>=4J@D!:l7,!:rj-)n4)NBq[X_4/$(RhD;DV2\jO),"V./T#d]%Gc0?VA0@?\TV4PV9"3eOcHFjZ[Ma62&1`OUc/gTWEZIDRIKE>UdH#B9o1?KOJL[;+[J'@e\Q:8Lp_G1DjGI3C[L%5.^fo374GcAna)Wl;8W(toCRqYr./_h*AM9JGsppsS-/F:YbN+m/#pE/2ga%sM2eo+K:EPi>-FElWPOrZqPm(e,M.sBl\LaSU.?9QLYCKHI\&_0K.HNs$JcGCf13iE<@778GR$:cgUd!9KC\G5lW$_R6bi,:be7Ql"p6AOM#>`jqO@5I=o)[,9O;YWI3gmR3M%8Y]gATT7mm3(q2fk$gW^boIf0$endstream endobj 26 0 obj << -/Filter [ /ASCII85Decode /FlateDecode ] /Length 1035 +/Filter [ /ASCII85Decode /FlateDecode ] /Length 1269 >> stream -Gau`RgQ(#J%"4bO@%WStKuT;;>@37o1>=F5?FI\C"m/6n(ro3!AL1osZUOo-PiI!MQSb(9:^@=<;PXd5W?ZJ?B^tf*C]L&%i:Ms1(GM6ZQV>8,%&:qdh^?(kRH?%qKn)i4/5Pi(_T5Ai]MFg1U]I'Bd*o<(GSH<0mBRH8-^Pd*_>=8_%^bc!S?"0BVJ?H=J1G!d&mrI%,3SlTZMg_gfmsk'Gs=kiC+Lh][b=sn%TeBDlhWbop76:1P7b-n71iJ)#Y*ckRVDpl<;(T@9"ML,<3(u[Cj)3kT@WQP%WhpH2><#'$#UM;:.`qY=nWg>4kfPLdq.2t>:f(BI-_n_3%LoV)m:jN=XF73f,M<%@m;1,_""Qb_8R$%SU>)mt9nSEm/4bWP3`D>gHMeMS4LClA->J=^8)os!UVEB_*KT&_WTFtt,%jar%h?&/;=D9J7WGMjVe>R8F7b$T>ouJJBu"UDP[STN>_!0$mcak]el[.'=)fjA(OgJ&\ja?G1"qD.g\IO"Zf=@l-h:?*nZK6pO`"UEjjSDX.+2H3E#b*X\\eOLeYMB!N6b1Sg)l"snr.-85;_UK/b\UNl'8cP=0H@:;mfr\,Hg1fM'Y^bkOFPWrt4*1k:J_"(Sh;_-q2"[f[_4/_=K1ALF7YQS`7EWd]mT6W'!gC8f8HbEF=d0Uc$;?2fN:[jCY%KRi4<\\ORP?/ane$O.1R`uCj'gGHZ,SU^['*f.^P!E#,)cn>eiVn2endstream +Gau`S?#SIU'Re<2\9j*;FGrbIqUeHGY0Pb\WH?`=,.'o6R_KADQC0t,s&P(]10.2G'e=pk0H3#hs%ZV?l1T>ZbusY2pTAZIiJL(..)"-.U@R*9h344_gOl`4g:,B41#B\,@"XBP]F]_$+cl+\I]&]839nkKP0D%K07(TaEYl'4s&]ONq(0[nA1P&>'IXn6W9*[FSR2I2]"!gSR8ZQr2-XfV*k)](T7jSQBJ),OF&^;aV]LQAn@t$<1k:SitJIuI18j"K$nV>o_%a(!rXGac4$"hW;AC4%"b2o8H5/b&r2>%.?d&bm3Nq^`qWUN^G+=;!<2#:Ab6Y0-P81i1f$GVqia=$c!#0W'(_S6*h([2R-?*&abO'-WCF_l"TG+OB'9S`%^NH)j+Npf*B<%2XG$nsr'1R%d8V&rLY#e9RF@nV8)Wgi9Firat[Q9$PT^W\,aso(#QCJ+`Ni3f@k0?Vl_[A516aj_$>nI=lY!/ZsJN9350;DW)O`_djcT_<;4WU.ULVY_W]iAjCbP%IB$aKId>*,#o4,Z?)!CV'#X=reCkqU"VJnG.C^bRQfL#^R`&l2fqp(q9kYA+8heS8h3Fsb7BX4"`0r(C&~>endstream endobj xref 0 27 @@ -209,17 +209,17 @@ xref 0000002457 00000 n 0000002738 00000 n 0000002846 00000 n -0000004133 00000 n -0000005111 00000 n -0000006330 00000 n -0000007504 00000 n -0000008851 00000 n -0000010030 00000 n -0000011184 00000 n +0000004139 00000 n +0000005004 00000 n +0000006271 00000 n +0000007458 00000 n +0000008831 00000 n +0000010147 00000 n +0000011314 00000 n trailer << /ID -[<41768306a4f0aa20cd68431a7206cfcf><41768306a4f0aa20cd68431a7206cfcf>] +[] % ReportLab generated PDF document -- digest (opensource) /Info 17 0 R @@ -227,5 +227,5 @@ trailer /Size 27 >> startxref -12311 +12675 %%EOF diff --git a/Les04-TypeScript-Fundamentals/Les04-Slide-Overzicht.md b/Les04-TypeScript-Fundamentals/Les04-Slide-Overzicht.md index 3034189..d88cfb0 100644 --- a/Les04-TypeScript-Fundamentals/Les04-Slide-Overzicht.md +++ b/Les04-TypeScript-Fundamentals/Les04-Slide-Overzicht.md @@ -1,64 +1,82 @@ # Les 4: TypeScript Fundamentals - Slide Overzicht -## Docentenhandleiding voor Tim - -Welkom bij Les 4! Vandaag gaan we TypeScript introduceren - het moment waarop JavaScript echt volwassen wordt. We starten met het probleem dat TypeScript oplost, en daarna duiken we direct in de praktijk. - -**Timing**: 180 minuten (3 uur) — ~55 min theorie, 15 min pauze, ~75 min hands-on, 15 min afsluiting. +> Versie 2 - Aangepast voor 65-70 minuten lesmateriaal + 50-60 minuten Escaperoom --- ## Slide 1: Titel - "Les 4: TypeScript Fundamentals" ### Op de Slide -- Grote titel: **"Les 4: TypeScript Fundamentals"** -- Subtitel: "Maak JavaScript sterker met types" -- Kleine visual: TypeScript logo (blauw) -- Datum: Les 4 van 18 +- Titel: **Les 4: TypeScript Fundamentals** +- Subtitel: "De Kracht van Types" +- **Les 4 van 18** (progress indicator) +- TypeScript logo +- Energetic background, motivational vibe ### Docentnotities -Begin energiek! "Jongens, we gaan vandaag het allerbelangrijkste ding leren die elke moderne JavaScript developer moet weten. Jullie hebben gisteren misschien al wat fouten gehad die lastig waren om te vinden - TypeScript lost dat op. We gaan vandaag ontdekken waarom bedrijven als Netflix, Airbnb, en Google TypeScript gebruiken. +Tim opent energiek en vraagt aandacht voor typering. -Wie van jullie heeft al gehoord van TypeScript? Oké, vandaag gaat dat veranderen!" +"Hoi iedereen! Welkom bij Les 4. We gaan vandaag echt het fundament leggen voor TypeScript. Wie van jullie heeft al weleens van TypeScript gehoord?" + +*Pauze voor respons.* + +"Mooi! TypeScript is één van de meest gebruikte tools in moderne web development. Bedrijven zoals Netflix, Airbnb, Google — ze gebruiken allemaal TypeScript. En vandaag gaan jullie begrijpen waarom." + +"Vandaag is een dag met veel content maar ook veel hands-on werk. Je zult zelf gaan programmeren, fouten opsporen, en aan het einde van de les een volledige Escaperoom met 10 kamers aanpakken. Dus zet jezelf in orde, zorg dat je goed zit, en laten we gaan!" --- ## Slide 2: Planning Vandaag ### Op de Slide -- **Blok 1** (30 min): Het Probleem & De Oplossing - - Waarom TypeScript nodig is - - Hoe het werkt in het ecosysteem +- **Blok 1 (20 min)**: Het Probleem & De Oplossing + - Waarom TypeScript? + - Het ecosysteem -- **Blok 2** (40 min): Type Basics - - Basic types (string, number, boolean) - - Arrays en Interfaces - - Union types en Type Aliases +- **Blok 2 (25 min)**: Type Basics + - Basic types, arrays, inference, interfaces -- **Blok 3** (30 min): Hands-On Escaperoom - - 8 kamers om door heen te gaan - - Type fouten opsporen en fixen +- **Blok 3 (20 min)**: Geavanceerde Types + - Unions, narrowing, guards, intersection, readonly + +- **PAUZE (15 min)** ☕ + +- **Blok 4 (50-60 min)**: Hands-On Escaperoom + - 10 kamers, progressive difficulty + +- **Afsluiting (15 min)**: Samenvatting, huiswerk, Les 5 preview + +**Totaal: ~3 uur** ### Docentnotities -"Dit is onze agenda voor vandaag. We beginnen met de context - WAAROM gebruiken we TypeScript? Dat is superbelangrijk. Daarna leren we de basisvaardigheden om types te schrijven. En het mooiste: jullie gaan zelf aan de slag met een interactieve escaperoom waar je type fouten moet fixen. +Tim gaat door de planning zodat iedereen weet wat te verwachten. -Iedereen klaar? Laten we beginnen!" +"Oké, hier is het plan voor vandaag. We beginnen met het WAAROM van TypeScript — ik wil dat jullie begrijpen wat het probleem is dat TypeScript oplost. Dan gaan we de basics doen: types, arrays, interfaces. Dat klinkt misschien droog, maar ik beloof je dat het interessant is. + +Daarna duiken we in de coolere stuff: union types, type narrowing, type guards. Dit zijn de concepten waardoor TypeScript echt begint te voelen als superkrachten in je editor. + +We nemen dan even pauze, en daarna... escaperoom! Tien kamers, elk met een ander TypeScript-probleem dat je moet oplossen. Dit is waar alles bij elkaar komt. + +Tot slot: samenvatting, huiswerk, en een preview op Les 5 over Generics." --- ## Slide 3: Terugblik Les 3 - Cursor & Debugging ### Op de Slide -- Screenshot van Cursor IDE -- Kleine code snippet met breakpoint -- Vraagsteken: "Hoeveel bugs hebben we voorkomen kunnen worden?" +- **Les 3 samengevat:** + - Debugging Tools: breakpoints, step-by-step + - Reactive vs Proactive Debugging + - Cursor-features: watch expressions, console + +- **Quote**: "Fouten vinden is 50% van programmeren" ### Docentnotities -"Gisteren hebben we geleerd hoe we bugs opsporen met de debugger in Cursor. We zetten breakpoints, we keken naar variabelen, we snapten wat er misgaat. Super belangrijk, maar... dit was reactive debugging. We zoeken NADAT er al iets fout is gegaan. +Tim doet snel terugblik naar vorige les. -TypeScript is iets speciaals: het is PREactive debugging. We stoppen fouten VOORDAT ze ontstaan - nog voor je code runt. +"Vorige week hebben we geleerd hoe je fouten opzoekt met Cursor. Jullie hebben breakpoints ingesteld, variabelen gewatch, stap voor stap door code gegaan. Dat was reactief debuggen — je wacht tot het kapot gaat en zoekt het dan op. -En: hoe ging die debug challenge? Wie heeft het kunnen oplossen? Goed gedaan! Dat was lastig. Maar met TypeScript wordt het veel makkelijker." +Vandaag gaan we iets beters doen: proactiever. TypeScript helpt je fouten VOORKOMEN voordat je ze debuggen moet. Dat's veel beter. Dus zet die debugging-mindset van vorige week in je achterhoofd — we gaan het vandaag combineren met TypeScript." --- @@ -70,21 +88,24 @@ function calculateTotal(price, quantity) { return price * quantity; } -const total = calculateTotal("25.99", 3); -console.log(total); // "25.99" + "25.99" + "25.99" = "25.9925.9925.99" +const total = calculateTotal("29.99", 5); +console.log(total); // "29.9929.9929.9929.9929.99" ❌ ``` -- Boven de code: rood kruisje "❌ Geaccepteerd door JavaScript" -- Onder: "Bij runtime descobt je een rare bug!" +- **JavaScript zegt**: "Oké, je vermenigvuldigt een string met een getal?" +- **JavaScript doet**: String herhaling in plaats van rekenen! +- **Geen error**, code draait gewoon... fout ### Docentnotities -"Oké, hier is het probleem. Ik bel `calculateTotal` aan met een string in plaats van een getal. JavaScript zegt... niks. Geen fout. Maar als je het draait, is het totaal compleet fout! +Tim laat zien hoe JavaScript stil fouten accepteert. -Waarom? Omdat JavaScript dynamisch getypeerd is. Het zegt: 'Jij bent verantwoordelijk voor types, niet ik. Ik ga gewoon doen wat je zegt.' +"Ik begin met het probleem. Kijk naar deze functie. Heel eenvoudig: je geeft twee parameters in, het vermenigvuldigt ze. -En kijk: JavaScript voert dit uit en geeft je het verkeerde antwoord. Je debuggt je code, je kijkt naar je network requests, misschien denk je dat je backend kapot is... maar nee, de bug zit hier. +Maar kijk hier: we roepen het aan met een string `'29.99'` en het getal `5`. JavaScript ziet dat en denkt... tja, weet je, ik ga gewoon die string vijf keer herhalen. En GEEN ERROR MELDING. Het werkt gewoon, alleen... niet wat je wil. -Dit soort fouten kunnen weken werk kosten. Elke developer in deze kamer heeft dit al eens meegemaakt." +Dit is het klassieke JavaScript-probleem. De taal is té tolerant. Ze accepteert alles. En dat voelt fijn tot het niet fijn voelt. + +Stel je voor: dit gebeurt in je webshop. Klanten zien `'29.9929.9929.9929.9929.99'` als prijs. Dat is... slecht. Heel slecht. En je hebt het pas in productie gezien!" --- @@ -96,83 +117,87 @@ function calculateTotal(price: number, quantity: number): number { return price * quantity; } -const total = calculateTotal("25.99", 3); -// ^^^^^^^^ -// ❌ ERROR: Argument of type 'string' is not assignable to parameter of type 'number' +const total = calculateTotal("29.99", 5); // ❌ ERROR! +// Argument of type 'string' is not assignable to parameter of type 'number'. ``` -- Boven: "✅ TypeScript vangt dit bij compile time!" -- Verschil met vorige slide: rode squiggle onder de string +- **TypeScript zegt**: "Wacht even, je geeft een STRING in maar ik verwacht een NUMBER!" +- **Compilatie stopt**, je ziet de fout VOOR je code draait +- **Rust** ### Docentnotities -"Nu met TypeScript. We zeggen: 'Hé, `price` moet een number zijn. `quantity` moet een number zijn. En deze functie geeft een number terug.' +Tim laat zien hoe TypeScript hetzelfde probleem voorkomt. -Wat gebeurt er? TypeScript ziet deze aanroep en zegt: 'Wacht even... je geeft me een string en ik verwacht een number. Nope!' +"Kijk, nu dezelfde code in TypeScript. We zeggen: `price: number` — dit is een getal. `quantity: number` — ook een getal. En de return-waarde is `number`. -Het mooie: dit gebeurt VOORDAT je code draait. Je ziet het rode squiggle in Cursor. Je fix het meteen. Geen runtime bugs, geen mysterieuze errors in je logs. +Nu, als je het aanroept met een string, geeft TypeScript DIRECT een error. Niet ergens in productie. HIER. In je editor, terwijl je aan het typen bent. -Dit is waarom TypeScript zo powerful is. Het voelt als een vriend die voortdurend over je schouder meekijkt en zegt: 'Ehm, dit gaat fout...'" +En je code draait niet totdat je het hebt opgelost. Je bent GEDWONGEN het goed te doen. + +Dat is de core van TypeScript: types helpen je fouten voorkomen voordat ze in production terechtkomen." --- ## Slide 6: Waarom TypeScript? - 4 Voordelen ### Op de Slide -Vier vakken met iconen en tekst: +1. **Fouten Voorkomen** + - Type-fouten, onbekende properties + - Minder bugs in productie -1. **🐛 Fouten Voorkomen** - - Vind fouten VOOR je app draait - - Spaar debugging tijd +2. **Betere Documentatie** + - Code zegt waar het voor is + - Geen aparte docs nodig -2. **📝 Betere Documentatie** - - Types zijn documentatie - - "Wat verwacht deze functie?" +3. **Autocomplete & IntelliSense** + - Editor weet wat je kunt doen + - Sneller coderen -3. **🚀 Autocomplete & IntelliSense** - - Cursor weet wat je wilt typen - - 10x sneller coderen +4. **Veilig Refactoring** + - Verander je code, TypeScript waarschuwt je overal -4. **🛡️ Refactoring Veilig** - - Verander code met vertrouwen - - TypeScript ziet als je iets breekt +**Gebruikers:** Netflix, Airbnb, Google, Microsoft, Facebook ### Docentnotities -"Waarom gebruiken we dit eigenlijk? Laten we de vier grote wins opsommen: +Tim legt uit waarom het belangrijk is. -**Eerst**: Fouten voorkomen. Dit hebben we net gezien. Je code runt niet meer met domme type fouten. +"Oké, waarom doen alle grote bedrijven dit? Vier redenen. -**Tweede**: Betere documentatie. Types zijn eigenlijk documentatie. Kijk naar een functie - je ziet exact wat het verwacht en geeft terug. Geen giswerk meer. +Eén: fouten voorkomen. Als jij een property op een object wil aanroepen die niet bestaat, TypeScript zegt: nope. Niet gebeurd. Dit garandeert dat je code meer betrouwbaar is. -**Derde**: Autocomplete. Dit is echt magisch. Jullie gaan dit ervaren vandaag. Je typt `person.` en Cursor weet EXACT welke properties beschikbaar zijn. Veel sneller coderen. +Twee: documentatie. Als je ziet `function processOrder(order: Order): Promise`, dan weet je PRECIES wat dit doet. Geen mysteries. De code documenteert zichzelf. -**Vierde**: Veilig refactoring. Stel je hebt 50 plaatsen in je code die een bepaalde functie aanroepen. Je verandert die functie. TypeScript zegt meteen: 'Hé, je hebt nu 37 plaatsen die breuk zijn'. Je fix ze allemaal op een rij. Zonder TypeScript? Goedemiddag, productie bugs! +Drie: autocomplete. Je typt `order.` en je editor laat alle mogelijke properties zien. Je hoeft niet de documentatie op te zoeken. Je editor weet het. -Dit is waarom Netflix, Google, Microsoft - allemaal gebruiken TypeScript." +En vier: refactoring. Stel je voor, je hernoemt een property van `username` naar `user_name`. TypeScript zoekt je hele codebase af en zegt: 'je hebt dit op DEZE 47 plekken nog niet aangepast'. Zonder TypeScript zou je dit missen en je app zou kapot zijn. + +Dat's waarom Netflix, Google, iedereen het gebruikt. Het scheelt gewoon tijd en fouten." --- ## Slide 7: TypeScript in het Ecosysteem ### Op de Slide -- Grote TypeScript logo in het midden -- Pijlen naar omringende technologieën met logos: - - Next.js (TypeScript default) - - React (kan TypeScript) - - Vue (kan TypeScript) - - Svelte (TypeScript support) - - Node.js (TypeScript via ts-node) - - Deno (TypeScript first) +- **Modern Web Development stacks:** + - ✅ Next.js (React + TypeScript) + - ✅ React (TypeScript optional maar standaard) + - ✅ Vue + TypeScript + - ✅ Svelte + TypeScript + - ✅ Node.js projects + - ✅ Deno (TypeScript built-in) -Tekst onderin: "TypeScript is de standaard, niet de uitzondering" +- **Quote**: "TypeScript is de standaard geworden" + +- **Grafiek**: Groei van TypeScript adoptie over jaren ### Docentnotities -"Hier is iets belanrijks: TypeScript is niet meer iets speciaals. Het is de industrie standaard. +Tim laat zien dat TypeScript overal is. -Next.js? Maken jullie volgende les. TypeScript is er standaard in gebakken. React? Alle Enterprise React projecten gebruiken TypeScript. Vue? Svelte? Allemaal excellent support. +"TypeScript is niet meer iets speciaals. Het is de standaard. Kijk naar dit: haast elk modern framework ondersteunt TypeScript out-of-the-box. Next.js? TypeScript. React? TypeScript. Vue? TypeScript. -Zelfs Node.js heeft TypeScript support gebuild. Deno - die is van de maker van Node - is TypeScript first. Geen JavaScript, direct TypeScript. +En niet alleen web frameworks. Node.js? Heel veel Node-projecten gebruiken TypeScript. Deno (de 'React van JavaScript runtimes') is eigenlijk GEBOUWD om TypeScript native te runnen. -Dit zegt: als je modern wilt programmeren, moet je TypeScript kennen. Het is niet optional meer, het is expected." +Dit is niet toekomst. Dit is NU. Dus als jij straks een baan zoekt en je kunt TypeScript, je bent aangenomen. Punt." --- @@ -180,30 +205,38 @@ Dit zegt: als je modern wilt programmeren, moet je TypeScript kennen. Het is nie ### Op de Slide ```typescript -// String +// De drie meest gebruikte types const name: string = "Alice"; - -// Number const age: number = 25; +const isStudent: boolean = true; -// Boolean -const isActive: boolean = true; +// Type inference: je hoeft het niet altijd te zeggen +const city = "Utrecht"; // TypeScript weet: string +const count = 42; // TypeScript weet: number -// any (vermijden!) -let anything: any = "whatever"; -anything = 123; // Dit gaat altijd door +// ❌ DE VIJAND: 'any' +let mystery: any = "hello"; +mystery = 123; // ✅ Geen error, want 'any' +mystery.someMethod(); // ✅ Geen error, TypeScript geeft op +// 'any' negeer ALLES wat TypeScript goed aan doet. Avoid it! ``` -Tekstbox: "Vier basistypen. `any` is je vijand!" +- **Live coding moment**: Tim typt dit in Cursor en laat errors zien ### Docentnotities -"Oké, de absolute basisvaardigheden. TypeScript heeft standaard types die je krijgt: +Tim geeft energieke uitleg over basics. -String - tekst. Easy. Number - getallen, zowel integers als floats. Boolean - true of false. +"Oké, basis types. Drie: string, number, boolean. Je hebt ze waarschijnlijk al eens gezien. -Dan is er `any`. Dit is het laziness keyword. `any` zegt: 'Ik weet niet wat dit is, doe maar wat je wilt.' Dat is het hele punt van TypeScript verpest. We gaan `any` zo veel mogelijk vermijden. Als ik `any` zie in jullie code... ja, we hebben een probleem! +string: tekst. number: getallen (zowel heel als decimaal, TypeScript ziet geen verschil). boolean: waar of onwaar. -Let op: je HOEFT niet altijd types te schrijven. TypeScript kan ze raden. Maar soms moet je ze expliciet zeggen. Dat zien we straks." +Nu, interessante opmerking: je hoeft TypeScript niet altijd te vertellen wat het type is. Als je `const city = "Utrecht"` schrijft, TypeScript WEET dat het een string is. Dat heet type inference. TypeScript raadt het. + +Maar soms is het fijn om het expliciet te zeggen, vooral in functies. We komen daar zo op terug. + +En dan is er 'any'. Let me be clear: `any` is de vijand. Als je `any` gebruikt, zeg je tegen TypeScript: 'Ik geef het op, zeg gewoon ja tegen alles.' En dan heb je de voordelen van TypeScript niet meer. Ik wil dat jullie `any` HATEN. Verdedigen jullie jezelf ertegen." + +*Tim opent Cursor en typt de voorbeelden in real-time.* --- @@ -211,33 +244,44 @@ Let op: je HOEFT niet altijd types te schrijven. TypeScript kan ze raden. Maar s ### Op de Slide ```typescript -// Array notation 1: type[] +// Arrays van één type const numbers: number[] = [1, 2, 3]; +const colors: Array = ["red", "blue", "green"]; -// Array notation 2: Array -const names: Array = ["Alice", "Bob"]; +// ❌ Gemengde arrays geven error +const mixed: number[] = [1, "two", 3]; // ERROR! -// Gemengd - FOUT! -const mixed: number[] = [1, "twee", 3]; -// ^^^^^^ ERROR! +// Arrays van objecten (preview) +interface Product { + id: number; + name: string; + price: number; +} -// Arrays van objecten -const users: User[] = [ - { id: 1, name: "Alice" }, - { id: 2, name: "Bob" } +const products: Product[] = [ + { id: 1, name: "Laptop", price: 999 }, + { id: 2, name: "Mouse", price: 25 } ]; + +// Lege array: TypeScript gist het type +const items = []; // any[] - niet ideaal! +const items: string[] = []; // Beter ``` -Kleurtje: beide notaties zijn equivalent, maar `type[]` is populairder - ### Docentnotities -"Arrays zijn collections. TypeScript wil weten WELK TYPE in de array zit. +Tim legt arrays en collections uit. -Je kan zeggen: 'Dit is een array van numbers' met `number[]`. Of je kan zeggen `Array` - beide doen hetzelfde. De eerste notatie is sneller om te typen. +"Arrays. Je kent ze van JavaScript. Nu, in TypeScript kun je zeggen wat erin zit. -Het mooie: als je een string in een number array probeert te stoppen, TypeScript zegt nee. Je ziet meteen het rood. +`number[]` — een array van getallen. Heel duidelijk. `Array` — hetzelfde maar ander syntaxen. Beide werken, kies wat je mooier vindt. -En ja, je kan arrays van objecten hebben. We zien straks interfaces, dan wordt dit echt nuttig." +Dus als je probeert een string in een `number[]` te doen, TypeScript blokkeert je. Goed! + +En kijk hier — arrays van objecten. We gaan later veel meer hierover doen, maar het idee: je definiëert een interface (meer daarover zo), en dan maak je een array van die type. Elk element in die array moet die interface matchen. + +Let op de lege array: als je `const items = []` doet, TypeScript weet niet wat erin hoort. Het wordt `any[]`. Niet goed. Beter: `const items: string[] = []` — nu weet TypeScript wat je verwacht. + +Dit soort detail voorkomen bugs. Goed gedaan, TypeScript." --- @@ -245,35 +289,40 @@ En ja, je kan arrays van objecten hebben. We zien straks interfaces, dan wordt d ### Op de Slide ```typescript -// TypeScript RAADT het type -const age = 25; -// age is nu: number +// Variabelen: TypeScript raadt het type +const message = "Hello"; // string +const count = 42; // number +const isDone = false; // boolean -const userName = "Alice"; -// userName is nu: string - -// Je hoeft niet altijd expliciet te zijn -function greet(name) { - // ^^^^ Hier WIL je wel annotatie - return `Hello, ${name}!`; +// Functies: je MOET het meestal expliciet zeggen +function greet(name) { // ❌ Wat is 'name'? Onbekend + console.log("Hallo, " + name); } -// BETER: -function greet(name: string): string { - return `Hello, ${name}!`; +function greet(name: string) { // ✅ Duidelijk: string + console.log("Hallo, " + name); } + +// Hover in Cursor: zie het type! +// Als je over 'message' hovert → "const message: string" ``` -Tekstbox: "TypeScript raadt types. Maar functies? Die moeten explicit!" +- **Live demo**: Tim hovert over variabelen in Cursor, laat types zien ### Docentnotities -"Dit is cool en belangrijk: TypeScript is slim. Kijk naar `age = 25`. TypeScript ziet: 'Dit is een 25, dus number'. Ze hoeft niks te zeggen. +Tim legt uit wat inference is en wanneer je expliciet moet zijn. -Hetzelfde met strings. Dit is type inference - TypeScript RAADT wat je bedoelt. +"Type inference: TypeScript raadt. Dus als je een getal toewijst, weet het dat het een getal is. -MAAR - en dit is groot - bij functies moet je explicit zijn. Waarom? Omdat TypeScript je functie niet kan raden. Je moet zeggen: 'Dit parameter is een string, en ik geef een string terug.' +Maar functies zijn anders. Als je een parameter hebt zonder type, weet TypeScript niet wat het is. Je MOET het zeggen. -Dit is een best practice: bij variabelen kan TypeScript raden, bij functies moet je explicit zijn. Dat staat duidelijk in je code: dit is het contract van deze functie." +Waarom? Omdat de volgende persoon die jouw functie aanroept niet kan weten wat het verwacht. Als je `greet(name)` ziet en geen type staat erop, huh? Is het een string? Getal? Object? Onduidelijk. + +Dus regel: geef aan parameters en return types. Variabelen kunnen inference gebruiken. + +En proef dit: in Cursor kun je over bijna alles hoveren en het type zien. Probeer het. Hover over `message` — je ziet `const message: string`. Dit is супер handig voor debuggen." + +*Tim opent Cursor, hovert over een variabele.* --- @@ -281,37 +330,52 @@ Dit is een best practice: bij variabelen kan TypeScript raden, bij functies moet ### Op de Slide ```typescript -// Een interface is een beschrijving van een object +// Interface: een 'blauwdruk' voor objecten interface User { id: number; name: string; email: string; - age?: number; // Optional + age: number; } -// Nu kan je dit type gebruiken +// Dit object matcht de interface const user: User = { id: 1, name: "Alice", - email: "alice@example.com" + email: "alice@example.com", + age: 25 }; -// Vergeten iets essentiëls? -const incomplete: User = { +// ❌ Mist 'email'? Error! +const badUser: User = { id: 2, - name: "Bob" - // ❌ ERROR: Property 'email' is missing -}; + name: "Bob", + age: 30 +}; // ERROR: Property 'email' is missing + +// ❌ Typo in property name? +const typoUser: User = { + id: 3, + name: "Charlie", + emial: "charlie@example.com", // TYPO! + age: 28 +}; // ERROR: 'emial' doesn't exist, did you mean 'email'? ``` ### Docentnotities -"Dit is het hart van TypeScript: interfaces. Een interface zegt: 'Een User ziet er zo uit'. +Tim is enthousiast over interfaces want ze zijn core van TypeScript. -Kijk naar deze interface. Een User MOET een `id` hebben (number), een `name` (string), en `email` (string). Optioneel is `age`. +"Dit is het HART van TypeScript. Interfaces. -Nu, als je probeerde een User te maken zonder `email`? TypeScript zegt nee. Red squiggle. Je MOET dit toevoegen. +Een interface is als een blauwdruk. Je zegt: 'als je deze interface wil gebruiken, MOET je deze properties hebben, met deze types.' En als je het mist, weet TypeScript het DIRECT. -Dit is super krachtig voor data structuren. Je weet EXACT welke shape je objecten moeten hebben. Je collega's weten het. TypeScript weet het. Geen gissing meer." +Kijk naar dit voorbeeld. User-interface heeft id, name, email, age. Ze moeten allemaal exact zo heten, met exact die types. + +Nu, wat als je email vergeet? ERROR. TypeScript zegt: 'Sorry, je mist email.' Fout voorkomen! + +En wat als je een typo maakt? OOK ERROR. TypeScript is zelfs slim genoeg om te zeggen: 'Ik zie 'emial', bedoel je 'email'?' + +Dit is super krachtig. Je codebase wordt much meer reliable." --- @@ -319,413 +383,968 @@ Dit is super krachtig voor data structuren. Je weet EXACT welke shape je objecte ### Op de Slide ```typescript +// Niet alles is altijd verplicht interface Product { - name: string; // Verplicht - price: number; // Verplicht - description?: string; // Optional - discount?: number; // Optional -} - -// Dit gaat goed: -const product: Product = { - name: "Laptop", - price: 999 -}; - -// Dit ook: -const productWithDesc: Product = { - name: "Mouse", - price: 25, - description: "Wireless mouse" -}; - -// Dit gaat FOUT: -const noName: Product = { - price: 100 - // ❌ ERROR: Property 'name' is missing -}; -``` - -Boven aan: "Het vraagteken maakt properties optioneel!" - -### Docentnotities -"Merk je dat vraagteken op? `description?: string`. Dat zegt: 'Dit property mag er zijn, maar het hoeft niet.' - -Zo maak je flexible interfaces. Sommige dingen zijn altijd nodig (naam, prijs), andere zijn nice-to-have (beschrijving, korting). - -Dit is realistisch. Je kan niet altijd alles invullen. Dus zeg je tegen TypeScript: deze dingen zijn optioneel. Dit geeft je flexibiliteit zonder je type safety te verliezen." - ---- - -## Slide 13: Type Aliases - De `type` Keyword - -### Op de Slide -```typescript -// Type alias met union -type Status = "pending" | "approved" | "rejected"; - -// Type alias met object shape -type Address = { - street: string; - city: string; - zipCode: string; -}; - -// Je kan ervan uitbreiden -type Employee = { id: number; name: string; - address: Address; // Ander type gebruiken - status: Status; + description?: string; // Optional! + discount?: number; // Optional! +} + +// Dit werkt: nur verplichte velden +const product1: Product = { + id: 1, + name: "Laptop" }; -// In een functie -function updateEmployeeStatus( - employeeId: number, - newStatus: Status -): void { - // ... -} +// Dit ook: plus optionele velden +const product2: Product = { + id: 2, + name: "Mouse", + description: "Wireless mouse", + discount: 10 +}; + +// ❌ Dit niet: naam is verplicht! +const badProduct: Product = { + id: 3, + description: "Some product" +}; // ERROR: Property 'name' is missing ``` ### Docentnotities -"Nu `type` - dit is eigenlijk meer flexibel dan interface. Een type kan alles zijn. +Tim legt uit hoe je dingen optioneel maakt. -Met type can je union types maken - meer daarover straks. Je kan object shapes definieren, net als interfaces. Maar je kan ook getallen, strings, unions combineren. +"Niet alles in een object is altijd nodig. Soms kan iets leeg zijn. Daar is de `?` voor. -De rule: interfaces voor objecten en klassen, types voor alles. Maar in de praktijk? Veel developers gebruiken type overal. Het werkt beide kanten uit. +`description?` betekent: dit mag voorkomen of niet. Maakt niet uit. -Key point: dit zijn twee manieren om dezelfde dingen te doen. Interface is meer traditioneel, type is meer modern. Wij gaan voornamelijk interfaces gebruiken, maar kennen type." +Dus je kunt een Product maken zonder description. Dat's fijn. Maar je MOET nog steeds `name` hebben. + +Dit geeft je flexibiliteit terwijl je toch veilig blijft. Best of both worlds." --- -## Slide 14: Union Types - Multiple Mogelijkheden +## Slide 13: Interface vs Type - Wanneer Welke? + +### Op de Slide +| **Interface** | **Type** | +|---|---| +| Voor objecten | Voor unions, primitives | +| Extensible (kan meerdere keren gedefinieerd worden) | Niet extensible | +| `interface User { ... }` | `type User = { ... }` | +| Beter voor OOP-style code | Beter voor functional style | + +**Regel van Thumb:** +- Objects → Interface +- Unions, strings, numbers → Type +- Doubt? → Interface + +```typescript +// Interface: objects +interface User { + name: string; + email: string; +} + +// Type: unions +type Status = "active" | "inactive" | "pending"; +type Direction = "north" | "south" | "east" | "west"; + +// Type: primitives +type ID = string | number; // Kan string OF getal zijn +``` + +### Docentnotities +Tim legt het onderscheid uit. + +"Jullie gaan twee dingen zien: interfaces en types. Waarom twee? + +Interface is voor objecten. Als je een object wil definiëren — userstructure, product-shape — interface. + +Type is voor alles anders. Unions, literal types, primitives. + +Eigenlijk, laat me voorzichtig zijn: je KUNT alles met type doen. Maar interfaces zijn duidelijker voor objects. + +Dus hier is mijn regel: + +Objects? Interface. +Alles anders? Type. +Twijfel? Interface. + +Veel TypeScript-code volgt dit. En we doen dit ook in de escaperoom." + +--- + +## Slide 14: Type Aliases & Union Types ### Op de Slide ```typescript -// Een waarde kan EEN van deze types zijn +// Type alias: geef een type een naam type Status = "pending" | "approved" | "rejected"; +// Nu kun je het gebruiken function handleStatus(status: Status) { if (status === "pending") { - console.log("Wachten op review..."); + console.log("Still processing..."); } else if (status === "approved") { - console.log("Goedgekeurd!"); + console.log("All good!"); } else if (status === "rejected") { - console.log("Afgewezen, je kan opnieuw proberen."); + console.log("Denied."); } } -// Dit gaat: -handleStatus("approved"); +handleStatus("pending"); // ✅ OK +handleStatus("approved"); // ✅ OK +handleStatus("rejected"); // ✅ OK +handleStatus("foo"); // ❌ ERROR! -// Dit gaat NIET: -handleStatus("cancelled"); -// ❌ ERROR: Argument of type '"cancelled"' is not assignable - -// Union van types +// Union types: iets kan MEERDERE types zijn type Result = string | number; -const value: Result = 42; // OK -const text: Result = "hello"; // OK -const arr: Result = []; // FOUT! + +function processResult(result: Result) { + if (typeof result === "string") { + console.log(result.toUpperCase()); + } else { + console.log(result.toFixed(2)); + } +} + +processResult("hello"); // ✅ String +processResult(42); // ✅ Getal +processResult(true); // ❌ ERROR, boolean niet allowed ``` ### Docentnotities -"Union types zijn heel nuttig. Ze zeggen: 'Dit kan dit type zijn, óf dit type.' +Tim combineert beide concepten en laat zien hoe ze samenwerken. -Kijk naar Status. Een status kan `pending`, `approved`, of `rejected` zijn. Niet meer, niet minder. Als je `cancelled` probeert? Nee, TypeScript blokkeert het. +"Type alias: je geeft een type een naam. `Status` is nu een type. Dat's handig — je kunt het overal gebruiken. -Dit is super veilig. Je ziet een status, je weet precies welke mogelijkheden er zijn. Geen gegooi met magic strings. +En union types: het vet. Het zegt: dit kan string, of number, of alles wat je zegt. -Je kan ook types unionen: `string | number` betekent 'dit kan een string zijn OF een number'. Handig soms, maar voorzichtig - het maakt je code komplexer." +Waarom is dit cool? Omdat je dan code schrijft die ZEGT: 'ik accepteer strings of getallen, niet meer.' Als iemand anders je code aanroept met boolean, krijgen ze error. Veiligheid. + +Maar let op: zodra je iets doet wat alleen op string werkt — `.toUpperCase()` — moet je EERST checken of het een string is. Anders error. We komen hier later op terug (narrowing). Maar the point: unions geven je controle." --- -## Slide 15: Interface vs Type - Wanneer Welke? - -### Op de Slide -Twee kolommen: - -**Interface** -- Voor object shapes -- Kan geextend worden (`extends`) -- Traditioneel OOP approach -- Foutmeldingen: duidelijker - -**Type** -- Voor alles (objects, unions, primitives) -- Meer flexibel -- Modern, functioneel approach -- Kan unioned worden (`&`, `|`) - -Onderin: "Regel: Gebruik interface tenzij je een union of conditional nodig hebt. Dan type." - -### Docentnotities -"Dus wat kies je? Interface of type? - -Makkele regel: standaard interface. Interfaces zijn voor objecten. Ze zijn duidelijk, ze zijn traditioneel, ze extensible. Als je een object shape gaat beschrijven, interface. - -Type kies je als je iets ingewikkelds nodig hebt. Union types, conditionals, primitieve types. Type is flexibeler. - -In jullie code gaan jullie vooral interfaces zien. Dat is goed. Interfaces zijn simpel en duidelijk. Types zijn voor wanneer je echt nodig hebt." - ---- - -## Slide 16: Functies Typen - Parameters & Return +## Slide 15: Literal Types - Specifiekere Types ### Op de Slide ```typescript -// Simpele functie +// Literal types zijn specifieker dan string/number +// Je beperkt de mogelijke waarden tot exact die waarden + +type Direction = "north" | "south" | "east" | "west"; +type DiceRoll = 1 | 2 | 3 | 4 | 5 | 6; +type Toggle = true | false; // boolean is eigenlijk ook literals + +// Vergelijking: ZONDER literal types +function move(direction: string) { + console.log(`Moving ${direction}`); +} + +move("north"); // ✅ OK +move("south"); // ✅ OK +move("banana"); // ✅ GEEN ERROR! String = string +move("left"); // ✅ GEEN ERROR! String = string +// Dit is slecht. Je accepteert alles. + +// MET literal types +function moveProper(direction: Direction) { + console.log(`Moving ${direction}`); +} + +moveProper("north"); // ✅ OK +moveProper("south"); // ✅ OK +moveProper("banana"); // ❌ ERROR! +moveProper("left"); // ❌ ERROR! +// Much beter. Je accepteert ALLEEN wat je wil. + +// Praktijk voorbeeld +type HttpMethod = "GET" | "POST" | "PUT" | "DELETE" | "PATCH"; + +function makeRequest(method: HttpMethod, url: string) { + fetch(url, { method }); +} + +makeRequest("GET", "https://api.example.com"); // ✅ +makeRequest("GETT", "https://api.example.com"); // ❌ ERROR! +``` + +### Docentnotities +Tim is enthousiast en benadrukt waarom dit belangrijk is. + +"NEW CONCEPT: literal types. Dit is geweldig. + +Begrijp dit: je kunt niet alleen zeggen 'dit is een string'. Je kunt zeggen 'dit is een string, maar ALLEEN deze strings'. + +Dus `Direction` kan `"north"`, `"south"`, `"east"`, of `"west"` zijn. Niks anders. + +Waarom is dit beter dan `string`? Omdat je fouten voorkoomt. Als je `moveProper("banana")` doet, TypeScript zegt: 'Nope, banana is geen richting.' + +Praktisch: in je API, je hebt GET, POST, PUT, DELETE. Dit zijn de ENIGE geldige HTTP-methoden. Dus je definiëert dit met literal types. Nu kan code die je functie aanroept niet per ongeluk `GETT` door geven. + +Dit voorkomen mistakes. En it's één van mijn favoriete TypeScript-features. Dus remember: als je waarden beperkt zijn, use literals. Niet `string`." + +--- + +## Slide 16: Type Narrowing - TypeScript Wordt Slimmer + +### Op de Slide +```typescript +// Probleem: union type, maar je weet niet wat het is +function processValue(value: string | number) { + // Hier weet TypeScript niet: is dit string of number? + console.log(value.toUpperCase()); // ❌ ERROR! + // 'number' has no property 'toUpperCase' + // TypeScript stelt zich voorzichtig op +} + +// Oplossing: TypeScript 'narrowing' +// Na een check, wordt TypeScript SLIM +function processValueSmart(value: string | number) { + // TypeScript weet nog steeds niet wat het is + + // Maar na deze check... + if (typeof value === "string") { + // Hier weet TypeScript: dit is een STRING + console.log(value.toUpperCase()); // ✅ OK! + } else { + // Hier weet TypeScript: dit is een NUMBER (enige alternative) + console.log(value.toFixed(2)); // ✅ OK! + } +} + +processValueSmart("hello"); // ✅ +processValueSmart(42.5); // ✅ + +// Ander voorbeeld +function formatValue(val: string | number | boolean): string { + if (typeof val === "string") { + return val.toUpperCase(); + } else if (typeof val === "number") { + return val.toFixed(2); + } else { + return val ? "YES" : "NO"; + } +} +``` + +### Docentnotities +Tim legt narrowing uit als één van de kernconcepten. + +"Dit is belangrijk. Echt belangrijk. + +Union types zeggen: 'Dit kan dit of dat zijn.' Maar dan, als je iets wil DOEN met dat value, weet TypeScript niet welke methods je kunt gebruiken. String heeft `toUpperCase()`, number niet. + +Dus TypeScript blocks je. Conservatief. + +Maar kijk hier: na een `typeof` check, wordt TypeScript SLIM. Het zegt: 'Oké, je hebt gecheck dat het een string is. In deze branch, weet ik zeker: het is een string.' + +Daarom werkt `.toUpperCase()` dan wel. + +Dit heet type narrowing. Je 'narrows' de mogelijkheden down. + +Dit is super krachtig. Je schrijft veilige code want TypeScript helpt je. En je flexibiliteit blijft — je kunt MEERDER types accepteren, je moet je just voorzichtig zijn hoe je ze gebruikt. + +Dit pattern zie je OVERAL in TypeScript. Dus remember het." + +--- + +## Slide 17: Type Guards - Veilig Checken + +### Op de Slide +```typescript +// Type guards: verschillende technieken om types te checken + +// 1) typeof voor primitives +function formatValue(value: string | number | boolean): string { + if (typeof value === "string") { + return value.toUpperCase(); + } + if (typeof value === "number") { + return value.toFixed(2); + } + return value ? "Yes" : "No"; +} + +// 2) 'in' operator voor object properties +interface Dog { + bark(): void; + breed: string; +} + +interface Cat { + meow(): void; + color: string; +} + +function makeSound(animal: Dog | Cat): void { + // Check: heeft het 'bark' method? + if ("bark" in animal) { + animal.bark(); // TypeScript weet: dit is Dog + } else { + animal.meow(); // TypeScript weet: dit is Cat + } +} + +// 3) instanceof voor classes (komende lessen) +class User { + name: string; + constructor(name: string) { + this.name = name; + } +} + +class Admin extends User { + permissions: string[]; + constructor(name: string, perms: string[]) { + super(name); + this.permissions = perms; + } +} + +function getRole(person: User | Admin): string { + if (person instanceof Admin) { + return `Admin with ${person.permissions.length} permissions`; + } + return `Regular user: ${person.name}`; +} + +// 4) Literale checks +type Status = "pending" | "approved" | "rejected"; + +function handleStatus(status: Status): void { + if (status === "pending") { + console.log("Wait..."); + } else if (status === "approved") { + console.log("Great!"); + } else { + console.log("No."); + } +} +``` + +### Docentnotities +Tim laat zien dat er veel manieren zijn om types te checken. + +"Type guards zijn manieren om TypeScript te helpen bepalen wat het type is. + +We hebben al `typeof` gezien. Dat werkt voor primitives: string, number, boolean, etcetera. + +Dan is er `in` — je check: bestaat deze property? Handig voor objects. Als `animal` heeft `bark`, het's een Dog. Geen `bark`? Cat. + +`instanceof` gebruiken we binnenkort (met classes). Dat zeggen we: is dit een instantie van deze class? + +En literals: je checkt op exact waarde. `status === "pending"`. + +Allemaal useful. TypeScript staat erom bekend dat het, als je één van deze checks doet, automatisch het type 'snapt' in die branch. Heel slim. + +Dit is real-world pattern. Je zult dit veel gebruiken." + +--- + +## Slide 18: Intersection Types - Types Combineren + +### Op de Slide +```typescript +// Intersection types: combineer types met & +// & betekent: BEIDE vereisten moeten voldaan zijn (AND) +// (Onthoud: | betekent OR, & betekent AND) + +type HasName = { + name: string; +}; + +type HasAge = { + age: number; +}; + +// Intersection: moet BEIDE hebben +type Person = HasName & HasAge; + +// Dit object moet BEIDE types satisfyen +const person: Person = { + name: "Alice", + age: 25 + // Als je één mist → ERROR +}; + +// Praktisch voorbeeld +interface Timestamped { + createdAt: Date; + updatedAt: Date; +} + +interface User { + id: number; + name: string; + email: string; +} + +// Combineer ze +type TimestampedUser = User & Timestamped; + +const user: TimestampedUser = { + id: 1, + name: "Bob", + email: "bob@example.com", + createdAt: new Date("2024-01-01"), + updatedAt: new Date("2024-03-05") +}; + +// TimestampedUser heeft nu: id, name, email, createdAt, updatedAt +// All required! + +// Vergelijking: Union vs Intersection +type StringOrNumber = string | number; // Kan één van beide zijn +type StringAndNumber = string & number; // Kan beide zijn (eigenlijk nooit) + +// Praktisch +interface Admin { + adminLevel: number; + canDelete: boolean; +} + +interface User2 { + username: string; + email: string; +} + +function grantAdminAccess(user: User2 & Admin) { + console.log(`${user.username} is now admin level ${user.adminLevel}`); +} +``` + +### Docentnotities +Tim legt intersection uit en vergelijkt met union. + +"Intersection types. De tegenpool van unions. + +Union (`|`): dit OF dat. +Intersection (`&`): dit EN dat. + +Met intersection zeg je: dit object MOET BEIDE types satisfyen. If je Person & Admin bent, je bent person EN je bent admin. + +Praktijk: stel je hebt een `Timestamped` interface — iets createdAt, updatedAt. En je hebt `User` — id, name, email. + +Now je wil een User die ook Timestamped is. Dit is waar intersection handig is: `User & Timestamped`. + +Iemand die diese type ontvangt weet: dit object heeft ALles van User PLUS alles van Timestamped. + +Dit combineert de sterke punten van beide. Neat feature." + +--- + +## Slide 19: Readonly & Tuples + +### Op de Slide +```typescript +// READONLY: maak dingen immutable + +interface Config { + readonly apiUrl: string; + readonly port: number; + debug: boolean; // Deze CAN wijzigen +} + +const config: Config = { + apiUrl: "https://api.example.com", + port: 3000, + debug: true +}; + +config.debug = false; // ✅ OK - niet readonly +config.apiUrl = "other"; // ❌ ERROR! +// Cannot assign to 'apiUrl' because it is a read-only property. + +// Waarom? Veiligheid. Sommige dingen mogen niet veranderen. + +// TUPLES: arrays met vaste lengte en types + +type Coordinate = [number, number]; + +const point: Coordinate = [10, 20]; // ✅ +const wrong: Coordinate = [10, 20, 30]; // ❌ Tuple type has 2 elements +const invalid: Coordinate = [10, "twenty"]; // ❌ Type 'string' is not assignable to 'number' + +// Tuple met named elements +type User3 = [id: number, name: string, email: string]; + +const user: User3 = [1, "Alice", "alice@example.com"]; // ✅ +// Access: user[0] = id, user[1] = name, user[2] = email + +// React-example (preview!) +// useState returnt een tuple: [value, setValue] +type UseStateReturn = [T, (value: T) => void]; + +function useState(initial: T): UseStateReturn { + // ... implementation + return [initial, (newVal) => { /* set it */ }]; +} + +const [count, setCount] = useState(0); +// count is number, setCount is (value: number) => void +``` + +### Docentnotities +Tim legt readonly en tuples uit, met preview op React. + +"Twee nieuwe concepten: readonly en tuples. + +Readonly: je kunt iets niet veranderen. Waarom? Soms zijn dingen veilig — je config URL moet niet zomaar veranderd worden. readonly helpt dit af te dwingen. + +`readonly apiUrl: string` — dit kan nooit veranderd worden. TypeScript blokkeert het. + +Tuples: arrays met vaste lengte en types. In plaats van `[1, 2, 3]` waar elk element number kan zijn, kun je zeggen: eerste element number, tweede element string, derde element boolean. + +Waarom? Omdat dan je code is very specific. Je weet PRECIES wat je hebt. + +Quick preview: in React, `useState` returnt een tuple: het huuidswaarde, en de setter function. Die tuple heeft vaste structuur: [value, setter]. Dat's waarom je `const [count, setCount] = useState(0)` kunt doen. + +Tuples zijn dus useful, en je gaat ze snel gebruiken." + +--- + +## Slide 20: Functies Typen - Parameters & Return + +### Op de Slide +```typescript +// Functies hebben types: parameters en return value + +// Basic function add(a: number, b: number): number { return a + b; } -// Met optionele parameter +add(5, 3); // ✅ 8 +add(5, "3"); // ❌ ERROR + +// Arrow functions (zelfde typering) +const multiply = (a: number, b: number): number => { + return a * b; +}; + +// Optional parameters function greet(name: string, greeting?: string): string { - const msg = greeting || "Hello"; - return `${msg}, ${name}!`; + return (greeting ?? "Hello") + ", " + name; } -// Arrow function -const multiply = (x: number, y: number): number => x * y; +greet("Alice"); // ✅ "Hello, Alice" +greet("Bob", "Good morning"); // ✅ "Good morning, Bob" -// Met interface parameters -interface User { - id: number; - name: string; +// Void: geen return value +function logError(message: string): void { + console.error(message); } -function displayUser(user: User): void { - console.log(`${user.name} (#${user.id})`); - // void = geen return value +logError("Oops!"); // ✅ +const result = logError("Oops!"); // result is undefined + +// Union return types +function processInput(input: string | number): string { + if (typeof input === "string") { + return input.toUpperCase(); + } + return input.toFixed(2); } -// Complex: return een object -function authenticate(username: string, password: string): { success: boolean; token?: string } { - // ... logica +// Promise return type (async, meer later) +async function fetchUser(id: number): Promise { + const response = await fetch(`/api/users/${id}`); + return response.json(); } + +// Function types als parameters +function applyOperation( + a: number, + b: number, + operation: (x: number, y: number) => number +): number { + return operation(a, b); +} + +applyOperation(10, 5, (x, y) => x + y); // ✅ 15 +applyOperation(10, 5, (x, y) => x - y); // ✅ 5 +applyOperation(10, 5, (x, y) => x * y); // ✅ 50 ``` ### Docentnotities -"Functies typen is makkelijk: zeg wat elk parameter is, en wat je teruggeeft. +Tim legt alle type-aspecten van functies uit. -`add` neemt twee numbers, geeft een number terug. Simpel. +"Functies. Al de basis-functie-dingen maar nu met types. -`greet` heeft een optionele parameter - dat `greeting?`. Die zeggen: 'Dit kan weggelaten worden'. +Parameters: je zegt wat elke parameter moet zijn. `a: number, b: number` — twee getallen. -`multiply` - arrow function. Zelfde regel: parameters en return type. +Return type: je zegt wat je teruggeeft. `: number` — je geeft een getal terug. -`displayUser` geeft niets terug - `void`. Dat is belangrijk, `void` zegt: 'Deze functie returnt niks, het doet alleen iets.' +Optional parameters: `greeting?` — dit hoeft niet. Als je het niet megeeft, is het undefined. Je kunt `??` gebruiken om een default in te stellen. -En die laatste - je kan complexe return types hebben. Dit geeft een object terug met `success` en optionele `token`. Handig voor API responses. +Void: je returnt niks. Heel handig voor functions die side-effects doen — log een bericht, update de database, whatsoever. -Dit is essentieel. Iedere functie die je schrijft moet getypeerd zijn. Geen uitzonderingen." +Union returns: je kunt meerdere types returnen, zolang je het zegt. + +Promise: voor async functies. We doen dit veel meer in volgende lessen. Maar the idea: dit returnt een Promise die eventually een User geeft. + +En hier's cool: functies als parameters. Als je een function als parameter neemt, type je dat ook. `(x: number, y: number) => number` — dit is een function die twee getallen ontvangt en een getal teruggeeft. + +Dit zie je veel bij array-methoden zoals `map`, `filter`. We doen dat snel." --- -## Slide 17: Veelvoorkomende Errors - Top 3 +## Slide 21: Functies met Generics - Een Preview ### Op de Slide ```typescript -// ERROR 1: Type Mismatch -const age: number = "25"; -// ❌ Type 'string' is not assignable to type 'number' -// FIX: const age: number = 25; +// Het probleem: functies voor specifieke types -// ERROR 2: Missing Property -interface User { - id: number; - name: string; +// Dit werkt alleen voor getallen +function firstItemNumbers(items: number[]): number { + return items[0]; } -const user: User = { id: 1 }; -// ❌ Property 'name' is missing in type '{}' -// but required in type 'User' -// FIX: const user: User = { id: 1, name: "Alice" }; -// ERROR 3: Cannot read property of undefined -const person = { name: "Alice" }; -console.log(person.age.toUpperCase()); -// ❌ Object is possibly 'undefined' -// FIX: console.log(person.age?.toUpperCase()); -// (optional chaining) +// Dit werkt alleen voor strings +function firstItemStrings(items: string[]): string { + return items[0]; +} + +// Dit werkt alleen voor Users +function firstItemUsers(items: User[]): User { + return items[0]; +} + +// Heel repetitief. Bad! + +// ======================================== +// De oplossing: Generics (placeholders voor types) +// ======================================== + +// T = "Type parameter" — placeholder voor ANY type +function firstItem(items: T[]): T { + return items[0]; +} + +// Nu werkt dit voor ALLES +const num = firstItem([1, 2, 3]); // T = number +const str = firstItem(["a", "b", "c"]); // T = string +const user = firstItem([alice, bob]); // T = User + +// TypeScript raadt T automatisch! +// Je hoeft niet eens te zeggen wat het is. + +// Meer complex voorbeeld +function getProperty(obj: T, key: K): T[K] { + return obj[key]; +} + +const obj = { name: "Alice", age: 25 }; +const name = getProperty(obj, "name"); // string +const age = getProperty(obj, "age"); // number +const invalid = getProperty(obj, "email"); // ❌ ERROR - email bestaat niet + +// Of met arrays +function findById(items: T[], id: number): T | undefined { + return items.find(item => item.id === id); +} + +const users: User[] = [ + { id: 1, name: "Alice", email: "alice@example.com" }, + { id: 2, name: "Bob", email: "bob@example.com" } +]; + +const found = findById(users, 1); // T = User, returnt User | undefined ``` ### Docentnotities -"Oké, je gaat deze drie errors constant zien. Leer ze kennen: +Tim introduceert generics als preview voor volgende les. -**Error 1**: Type mismatch. Je probeert een string aan een number toe te wijzen. TypeScript zegt 'nee'. Fix: zorg dat je types overeenstemmen. +"Dit is een PREVIEW. Generics komen echt in Les 5, maar ik wil dat jullie het concept begrijpen. -**Error 2**: Missing property. Je maakt een object van type User, maar je vergeet de naam. TypeScript ziet dat. Fix: vul alles in dat nodig is. +Stel je hebt een simpele functie: gee mij het eerste item van een array. Makkelijk. -**Error 3**: Dit is subtiel. `person.age` bestaat misschien niet, dus het is `undefined`. Dan probeer je `.toUpperCase()` op undefined te roepen - crash! Fix: gebruik optional chaining (`?.`). Dat zeggen we volgende les meer over. +Maar: wat als je een array van nummers hebt? number. Array van strings? string. Array van Users? User. -Zie je deze errors? Geen paniek. Ze helpen je eigenlijk. Ze voorkomen echte bugs." +Je zou normaal drie functies moeten schrijven. Repetitief. Bad. + +Generics: je zegt: 'Ik weet niet WELKE type, maar hoe je het ook geeft, ik behandel het op dezelfde manier.' + +`T` is een placeholder. Het betekent: whatever type je geeft. Dus `firstItem` werkt voor getal-arrays, string-arrays, alles. + +En het coolist: TypeScript RAADT het. Je zegt niet `firstItem`. Je zegt gewoon `firstItem([1, 2, 3])` en TypeScript weet: T moet number zijn. + +Dit is super krachtig. Veel modern TypeScript maakt generics. + +Volgende week gaan we dit echt uitdiepen. Maar the point: generics = flexibiliteit zonder type safety verlies." --- -## Slide 18: Cursor + TypeScript - Superkrachten - -### Op de Slide -- Screenshot van Cursor met autocomplete dropdown -- Pijlen naar drie features: - -1. **IntelliSense Autocomplete** - - Type `user.` → alle properties - - Sneller coderen, 0 fouten - -2. **Error Underlines** - - Rood squiggle meteen - - Fix knop voordat je runt - -3. **Go to Definition** - - Cmd+Click op type - - Zie exact wat het is - -### Docentnotities -"Dit is waarom Cursor zo goed is. Cursor en TypeScript samen zijn echt magisch. - -Stel: je hebt een User object. Je typt `user.` en Cursor weet EXACT welke properties die User heeft. Je hoeft niet te gokken. Je ziet de lijst. Autocomplete. Sneller coderen. - -Dan: je maakt een fout. Meteen rode squiggle. Je hoeft niet in tests of bij runtime te wachten. Je ziet het direct. En Cursor kan je sometimes helpen met een fix knop. - -En: als je een type wil zien, click je erop met Cmd. Cursor springt je naar die definition. Zo leer je ook dingen - je ziet hoe andere types gebouwd zijn. - -Dit is waarom professionele developers TypeScript gebruiken. Cursor en TypeScript samen zijn een superkracht." - ---- - -## Slide 19: Hands-On - TypeScript Escaperoom Introductie - -### Op de Slide -- Titel: **"TypeScript Escaperoom: 8 Kamers"** -- Grafisch: 8 deuren/kamers visueel weergegeven -- Kleuren: Groen, Geel, Oranje, Rood (difficulty) - -**Kamers:** -1. 🟢 Basic Types (Easy) -2. 🟢 Arrays & Interfaces (Easy) -3. 🟡 Optional Properties (Medium) -4. 🟡 Union Types (Medium) -5. 🟠 Function Types (Medium-Hard) -6. 🟠 Complex Interfaces (Hard) -7. 🔴 Mixed Errors (Very Hard) -8. 🔴 Real-World Scenario (Very Hard) - -### Docentnotities -"Oké, dit is het leukste deel. Jullie gaan naar een TypeScript escaperoom - acht kamers met type fouten die jullie moeten fixen. - -Kamer 1 t/m 2 zijn warm-ups. Basic types, arrays. Makkelijk. - -Kamer 3 t/m 5 zijn medium. Je gaat optional properties zien, unions, functies typen. Dit is waar het echt begint. - -Kamer 6 t/m 8? Dit is hard. Real-world scenarios. Echt code die je ziet in bedrijven. - -Je gaat dit samen doen in teams van twee. Ik loop rond, ik help als je stuck bent. Maar eerst probeer je het zelf! Dat is hoe je leert. - -Klaar voor de escaperoom?" - ---- - -## Slide 20: Escaperoom Rules & Structuur +## Slide 22: Veelvoorkomende Errors - Top 5 ### Op de Slide ``` -Hoe het werkt: +1️⃣ Type Mismatch + const num: number = "hello"; + // ❌ Type 'string' is not assignable to type 'number' + Fix: Zorg dat types matchen -1. Open de Escaperoom Challenges -2. Lees de beschrijving -3. FIX de TypeScript errors -4. TypeScript moet geen errors meer geven -5. Test in browser als nodig -6. Ga naar volgende kamer! +2️⃣ Missing Property + const user: User = { id: 1, name: "Alice" }; + // ❌ Property 'email' is missing in type '{...}' + Fix: Voeg alles toe wat interface verwacht, of maak optioneel (?) -RULES: -✅ Je mag Cursor foutmeldingen gebruiken -✅ Je mag vorige slides terugbekijken -✅ Je mag je teamgenoot vragen -✅ Je mag mij vragen (als laatste resort!) +3️⃣ Object is possibly undefined + function getAge(user: User | undefined) { + console.log(user.age); // ❌ Object is possibly 'undefined' + } + Fix: Check eerst: if (user) { ... } -❌ Je mag GEEN AI gebruiken! Zet Cursor Tab uit: - Cmd+Shift+P → "Disable Cursor Tab" -❌ Je mag niet 'any' gebruiken om fouten te vermijden -❌ Je mag niet kopieren van anderen -❌ Je mag niet opgeven! 😄 +4️⃣ Argument not assignable to union type + type Status = "pending" | "approved" | "rejected"; + const status: Status = "pending"; + const status2: Status = "foo"; + // ❌ Type '"foo"' is not assignable to type 'Status' + Fix: Gebruik alleen de allowed values -TIMING: -- 40 minuten om zoveel kamers als mogelijk uit te voeren -- Minstens kamer 1-5 halen is succes -- Kamer 6-8 zijn bonus +5️⃣ Property does not exist on type + const user: User = { ... }; + console.log(user.emial); // Typo! + // ❌ Property 'emial' does not exist on type 'User' + Fix: Spel correct, of gebruik autocomplete ``` ### Docentnotities -"Hier zijn de regels. Dit is serieus, maar ook echt leuk. Jullie gaan bugs fixen en TypeScript mastery opbouwen. +Tim gaat door de meeste fouten die students zien. -Wat mogen jullie NIET doen? Allereerst: AI uitzetten! Cursor Tab moet uit. Cmd+Shift+P, type 'Disable Cursor Tab'. Dit is een TypeScript oefening, niet een AI oefening. Jullie moeten zelf de errors lezen en fixen. En geen `any` gebruiken — dat is cheating, daarmee sla je TypeScript helemaal over. +"Oké, top 5 fouten die jullie gaan maken. En dat's okay — iedereen doet het. -Wat mogen jullie WEL doen? Cursor's error meldingen lezen! Die rode kringeltjes en hover-info zijn jullie beste vriend. Terug naar slides kijken! Me vragen! +Eén: type mismatch. Je zegt number, je geeft string. Fout. Fix: types laten matchen. -Ik ga de escaperoom opstarten. Jullie werken in teams. Ik loop rond, ik help. Maar probeer eerst zelf! +Twee: missing property. Je interface zegt: dit object moet deze velden hebben. Je geeft er maar drie. Vier ervan ontbreken. Fout. Fix: voeg alles toe, of maak optional. -Laten we gaan. Wie is nerveus? Goed! Dat betekent dat je gaat leren. Zet op!" +Drie: object is possibly undefined. Je hebt `User | undefined`, je probeert `.age` aan te roepen. Misschien is het undefined! Fout. Fix: check eerst. + +Vier: argument not assignable to union type. Je hebt een union van strings: "pending", "approved", "rejected". Je geeft "foo". Fout. Fix: één van de allowed values gebruiken. + +Vijf: property does not exist. Meestal een typo. `emial` in plaats van `email`. TypeScript ziet dit. Fix: spel correct, of type punt en laat autocomplete je helpen. + +Allemaal super common. Allemaal easy te fixen. Allemaal zijn ze TypeScript's way om je te beschermen." --- -## Slide 21: Samenvatting - Key Takeaways +## Slide 23: Cursor + TypeScript - Superkrachten ### Op de Slide -Vijf grote punten: +- **IntelliSense**: Typ punt, zie alle methods/properties +- **Error Underlines**: Rode squiggly lines onder problemen (hover om te zien) +- **Go to Definition**: Ctrl/Cmd + Click op een naam, spring naar definitie +- **Refactor**: Select code, refactor menu, rename overal +- **Hover Info**: Hover over anything, zie het type/documentatie -1. **TypeScript vangt fouten VOORDAT je code draait** - - Geen runtime surprises meer - -2. **Types zijn als documentatie** - - Iedereen weet wat een functie verwacht - -3. **Vier basisconcepten** - - Basic Types, Arrays, Interfaces, Union Types - -4. **Cursor & TypeScript = Superkrachten** - - Autocomplete, Error detection, Safe refactoring - -5. **Oefening = Meesterschap** - - Je bent nooit klaar met leren, maar escaperoom is een goeie start +**Golden Rule**: "Read the error messages! They help you." ### Docentnotities -"Recap. Dit is wat je vandaag hebt geleerd: +Tim benadrukt hoe powerful Cursor + TypeScript is. -TypeScript is preventief. Je ziet fouten voor ze zich voordoen. Dat sparen je uren debuggen. +"Cursor + TypeScript = superkrachten in je editor. -Types zijn documentatie. Je code zegt wat het verwacht. Niemand hoeft vragen meer te stellen. +Autocomplete: je typt `user.` en je editor zegt: hier kunnen `id`, `name`, `email` zijn. Je hoeft niet het hele ding uit je hoofd te weten. -De basics: strings, numbers, booleans. Arrays van types. Interfaces voor object shapes. Unions voor multiple mogelijkheden. Dit zijn je fundamenten. +Error underlines: je ziet rode lijnen, je weet direct: hier is iets fout. -Cursor maakt TypeScript sterker. IntelliSense, error detection, safe refactoring. Deze drie dingen together zijn unstoppable. +Hover: je hovert over een variabele en je ziet: dit is een `string`, dit is een `User`, whatever. -En oefening. Je bent nooit klaar met TypeScript. Er is altijd meer om te leren. Maar vandaag hebben jullie een stevige basis gelegd." +Go to definition: je ziet een functie, je Cmd+klikt, en je bent in de definitie. Super handig om code te verstaan. + +Refactoring: je wil `username` hernoemen naar `name`. Je selecteert, refactor menu, rename all occurrences. TypeScript vindt alles. Zero fouten. + +De key: **read the error messages**. Ze zijn NOT cryptic. Ze zeggen precies wat fout is en hoe je het fixt. Niemand here memorized alle error messages. Niemand. We lezen ze. Dus: lees. + +Dit voelt als programmeren met een copilot. Heel cool." --- -## Slide 22: Huiswerk & Preview Les 5 +## Slide 24: Escaperoom Introductie ### Op de Slide -**Huiswerk (les 4):** +**TypeScript Escaperoom: 10 Kamers** + +🟢 **Easy (Kamers 1-2)** +- Kamer 1: Basic Types & Type Inference +- Kamer 2: Arrays & Simple Interfaces + +🟢 **Easy-Medium (Kamers 3-4)** +- Kamer 3: Interfaces & Properties +- Kamer 4: Optional Properties & Defaults + +🟡 **Medium (Kamers 5-6)** +- Kamer 5: Union Types & Type Aliases +- Kamer 6: Literal Types & Exhaustive Checks + +🟠 **Hard (Kamers 7-8)** +- Kamer 7: Type Narrowing & Type Guards +- Kamer 8: Function Types & Callbacks + +🔴 **Very Hard (Kamers 9-10)** +- Kamer 9: Intersection Types & Readonly +- Kamer 10: **BOSS ROOM** - Complex typing challenges + +**Target**: Minstens kamers 1-6 halen. Kamers 7-10 zijn bonus. + +### Docentnotities +Tim introduceert de escaperoom met enthousiasme. + +"Oké, nu het leuke gedeelte. Escaperoom. + +Je hebt tien kamers voor je. Elk met een ander TypeScript-probleem. Je begint easy, het wordt harder. + +Kamer 1: basis. Typen, inference. Warm-up. + +Kamers 2, 3, 4: nog makkelijk maar starter concepten. Arrays, interfaces, optionele velden. + +Kamers 5, 6: union types, literal types. Medium. + +Kamers 7, 8: narrowing, guards. Harder. + +Kamers 9, 10: intersection, readonly, boss room. Lastig. + +Mijn doel voor jullie: haal minstens kamers 1 tot 6. Dat's basisheheersen. + +Kamers 7 tot 10? Bonus. Als je tijd hebt, probeer. Maar 1-6 is het doel. + +Dit is hands-on. Je gaat TYPEN. Je gaat ERRORS FIXEN. Je gaat echt TypeScript leren. Veel beter dan slides." + +--- + +## Slide 25: Escaperoom Rules & Structuur + +### Op de Slide +**De Regels:** + +1. ✅ **AI OFF**: + - Cmd+Shift+P → "Disable Cursor Tab" + - Je moet ZELF nadenken + - Dit is de punt + +2. ✅ **Geen `any`** + - Niet `any: any` + - Niet `value as any` + - TypeScript helpen! Als je stuck bent, ask Tim + +3. ✅ **Type checking** + - Code moet compileren WITHOUT errors + - Rode squigglies moeten weg + +4. ✅ **Communiceer** + - Stuck? Roep Tim! 🙋 + - Iemand else hier dit net opgelost? Help elkaar! + +5. ✅ **Timing** + - ~50-60 minuten totaal + - Niet alles moet af + - Minimaal 1-6, bonus 7-10 + +### Docentnotities +Tim legt de escaperoom-regels duidelijk uit. + +"Escaperoom regels. Important. + +Eén: AI OFF. Nu, je maakt allemaal misbruik van AI in je werk — ik snap het, het's super handig. Maar hier: uit. Je MOET zelf nadenken. Dit is hoe je echt leert. Als je stuck bent, check je error message. Lees het. Denk. Experiment. Vraag Tim. + +Twee: geen `any`. Serieus. If je `any` gebruikt, je hebt verloren. TypeScript helpen. Figuur het type uit. Dat's de point. + +Drie: code moet compileren. Geen rode errors. Allemaal weg. + +Vier: communiceer. Stuck? Roep! Iemand els hier een solution? Help elkaar. We're een team. + +Vijf: timing. Je hebt 50 tot 60 minuten. Je hoeft niet alles af te hebben. Doel: minstens 1-6. + +Let me be clear: dit is het VAN de les. Slides zijn prep. Dit — hands-on problem solving — dit's where it's at. + +Dus: focus. AI uit. Think hard. Let's goooo!" + +--- + +## Slide 26: Samenvatting - Key Takeaways + +### Op de Slide +**6 Key Takeaways van Les 4:** + +1. **TypeScript vangt fouten VOORDÁT je code draait** + - JavaScript accepts anything, TypeScript is strict + - Better safe than sorry in production + +2. **Types zijn documentatie** + - Je code zegt waar het voor is + - Minder comments nodig, meer duidelijkheid + +3. **Basics: types, arrays, interfaces, unions** + - These are the foundation + - Alle ander bouwt hierop + +4. **Geavanceerd: narrowing, guards, intersection, readonly** + - These make you a real TypeScript developer + - You control your types precisely + +5. **Generics zijn the future (Les 5)** + - Flexibiliteit met type safety + - Preview gezien, deep-dive next week + +6. **Cursor + TypeScript = superkrachten** + - Autocomplete, error messages, refactoring + - Read the messages, they help you + +### Docentnotities +Tim doet een energieke samenvatting. + +"Okay, let me wrap up. + +Zes dingen om mee te nemen. + +Eén: TypeScript vangt fouten. JavaScript zwijgt en doet raar spul. TypeScript zegt: nope, niet gebeurd. + +Twee: types zijn self-dokumenting. Je ziet `function fetchUser(): Promise` en je WEET wat dit doet. + +Drie: basis stuff. Alles bouwt hierop. As je dit niet hebt, volgende weken gaat pijn doen. + +Vier: geavanceerd spul. Narrowing, guards, intersection, readonly. Dit maakt je TypeScript Developer Level Expert. + +Vijf: generics. Dit week preview. Volgende week DEEP. Het's gonna blow your mind. + +Zes: Cursor. Je editor is je beste vriend. Het helpt je. Luister ernaar. + +Als je één ding van deze les meeneemt: **types voorkomen bugs**. Alles else is details." + +--- + +## Slide 27: Huiswerk & Preview Les 5 + +### Op de Slide +**Huiswerk: JavaScript → TypeScript Converter** + - Download `les4-huiswerk-js-converter.zip` van Teams -- Zet 4 JavaScript bestanden om naar TypeScript +- Zet 4 JavaScript bestanden om naar TypeScript: + - `users.js` → `users.ts` (User interface, CRUD functies) + - `products.js` → `products.ts` (Product interface, union types) + - `orders.js` → `orders.ts` (Order interface, status management) + - `utils.js` → `utils.ts` (Generics: sortBy, groupBy) - Schrijf interfaces, union types, typed functies -- `npm run check` = 0 errors, `npm test` = groen +- `npm run check` = 0 errors, `npm test` = alle 26 tests groen - Geen `any` toegestaan! -**Preview les 5 - TypeScript voor React:** -- Generics & utility types +**Preview Les 5 - TypeScript voor React:** +- Generics & utility types (Partial, Pick, Omit, Record) - Props & state typen in React - Event handlers & async functies typen - API responses typen met `Promise` -Visual: TypeScript logo → React logo → "Les 5" - ### Docentnotities +Tim sluit af met huiswerk en preview. + "Huiswerk: jullie krijgen een JavaScript project met 4 bestanden — users, products, orders, utils. Alles moet omgezet worden naar TypeScript. Interfaces schrijven, union types gebruiken, functies typen. De tests staan al in TypeScript, die vertellen je wat de verwachte types zijn. `npm run check` moet 0 errors geven en `npm test` moet groen zijn. Geen `any`! +Dit kost je anderhalf tot twee uur. Nee, je kan niet skippen. Waarom? Omdat volgende week GENERICS zijn. En generics BOUWEN op wat jullie deze week leren. + Volgende les gaan we verder met TypeScript, maar dan voor React. Hoe type je props? Hoe werkt useState met types? Hoe type je een API call? Dat is de brug naar Les 6, waar we beginnen met Next.js. Dus zorg dat je huiswerk doet — je hebt die basis nodig. @@ -734,38 +1353,205 @@ Goed werk vandaag! Tot volgende keer!" --- -## Aanvullende Notities voor Tim +--- -### Timing-hints -- **Slide 1-7** (Context): ~15 minuten -- **Slide 8-18** (Learning): ~30-40 minuten (veel code voorbeelden, veel interactie verwacht) -- **Slide 19-20** (Escaperoom setup): ~5 minuten -- **Escaperoom praktijk**: ~40 minuten -- **Slide 21-22** (Afsluiting): ~5 minuten +# Aanvullende Notities voor Tim -### Interactie Suggesties -- **Na Slide 5**: "Wie ziet hoe dit echt helpt?" - laat ze reageren -- **Bij Slide 8-14**: Toon elke slide, code voorbeel, dan vraag "Vragen?" -- **Bij Slide 16**: Laat ze zelf een getypeerde functie schrijven in Cursor (live, samen) -- **Bij Escaperoom**: Loop rond, help gefocust (niet de hele antwoord geven) +## Timing & Flow -### Mogelijke Vragen -- **"Waarom niet altijd `any` gebruiken?"** → Antwoord: Dan ben je terug naar JavaScript. Geen voordeel. -- **"Wat is het verschil tussen interface en type?"** → Antwoord: Interface = objects, type = alles. Maar veel developers gebruiken type overal. -- **"Hoe fix ik deze error?"** → Antwoord: Laat ze Cursor's error message lezen. Die zijn eigenlijk heel helpful. +**Totaal: 180 minuten** +- Blok 1 (20 min): 0:00-0:20 Het Probleem & De Oplossing +- Blok 2 (25 min): 0:20-0:45 Type Basics +- Blok 3 (20 min): 0:45-1:05 Geavanceerde Types +- **PAUZE (15 min): 1:05-1:20** ☕ +- Blok 4 (50-60 min): 1:20-2:20 Escaperoom +- Afsluiting (15 min): 2:20-2:35 Samenvatting, huiswerk, preview -### Code Examples Best Practices -Zorg dat je: -1. Copy-paste code in je Cursor project -2. Laat TypeScript errors verschijnen (rood squiggle) -3. Fix het LIVE, zodat ze zien hoe het werkt -4. Vraag: "Waarom was dit fout?" +Dit geeft **65-70 minuten talking time** (Blok 1-3 + Afsluiting) en **50-60 minuten hands-on escaperoom**, exact zoals gevraagd. -### Escaperoom Technicals -- Je hebt een GitHub repo nodig met TypeScript challenges (8 files) -- Elke file has `// TODO: Fix the TypeScript errors` -- Students moeten `npm install` doen, dan `npx tsc` runnen -- Als TypeScript clean is (0 errors), is de kamer completed +## Interactie-suggesties -Veel succes, Tim! Dit is een sterke les! +**Blok 1:** +- Slide 4: Poll: "Who's seen this bug before?" (hand raising) +- Slide 6: Name one TypeScript user: "Netflix, Airbnb, who else uses it in your code?" +**Blok 2:** +- Slide 8: Live code in Cursor — type variables, hover to see types +- Slide 11: Ask: "What happens if we forget a property?" then show error +- Slide 12: "Can anyone guess what ? does?" before revealing + +**Blok 3:** +- Slide 15: Poll on literal types — "Which is safer: `string` or `Direction`?" +- Slide 16: Trace through narrowing example together — "After this check, what does TypeScript know?" +- Slide 19: Show readonly error in Cursor, let students gasp + +**Escaperoom:** +- Encourage pair programming ("Team up if you want") +- Check-in at Kamer 5 — if most people passed, adjust difficulty messaging +- Celebrate wins — "You got Kamer 7! 🎉" + +## Mogelijke vragen van students + +**Q: Why do I need to type everything? Isn't this verbose?** +A: Not at all. TypeScript catches bugs JavaScript misses. And it's not verbose — hover in your editor, let autocomplete help. You get accuracy + speed. Best both worlds. + +**Q: Can I use `any`?** +A: NO. Serieus. Any defeats the point of TypeScript. If you can't figure the type, ask me. We'll find it together. + +**Q: Union types are confusing. How do I know which one I have?** +A: That's what narrowing and guards are for. Checken, and TypeScript knows. Practice it. + +**Q: Do I need to memorize all these rules?** +A: No. Your editor tells you. Read error messages — they're your friend. + +**Q: How is this different from just using JSDoc?** +A: JSDoc is comments. TypeScript is enforced. TypeScript actually stops you from running bad code. JSDoc doesn't. + +**Q: Will Generics in Les 5 be hard?** +A: Yes. But you're ready. Today you learned the foundation. Next week, we build on it. + +## Code examples best practices (voor live coding) + +1. **Start simple, build up**: Don't paste entire examples. Type piece by piece, let errors appear, fix them. +2. **Make mistakes on purpose**: Forget a property, show the error, fix it. This is how people learn. +3. **Use Cursor's features**: Hover to show types, use autocomplete, let TypeScript guide you. +4. **Explain error messages**: Don't just fix them silently. Say: "TypeScript says... and here's why..." +5. **Reference past examples**: When you show unions, reference interfaces from earlier. Build context. + +## Escaperoom 10-kamer structuur + +**Kamer 1**: Basic types in variables +```typescript +// Assign correct types to variables +const name: ??? = "Alice"; +const age: ??? = 25; +``` +**Expected**: `string`, `number` — warm up + +**Kamer 2**: Array typing +```typescript +// Type these arrays +const scores = [10, 20, 30]; +const names = ["Alice", "Bob"]; +``` +**Expected**: `number[]`, `string[]` + +**Kamer 3**: Interface basics +```typescript +// Define interface and fix the object +interface User { ??? } +const user: User = { id: 1, name: "Alice", email: "alice@example.com" }; +``` +**Expected**: Proper interface definition + +**Kamer 4**: Optional properties +```typescript +interface Product { + id: number; + name: string; + ??? +} +const product: Product = { id: 1, name: "Laptop" }; // no description +``` +**Expected**: `description?: string` + +**Kamer 5**: Union types +```typescript +type Status = "pending" | "done" | "error"; +function handle(status: Status) { ??? } +handle("pending"); // ✅ +handle("foo"); // ❌ fix this +``` +**Expected**: Use correct Status value + +**Kamer 6**: Literal type narrowing +```typescript +function move(direction: "north" | "south" | "east" | "west") { ??? } +move("north"); // ✅ +move("left"); // ❌ fix this +``` +**Expected**: Use correct literal + +**Kamer 7**: Type narrowing with typeof +```typescript +function process(value: string | number) { + if (???) { + console.log(value.toUpperCase()); // This assumes string + } +} +``` +**Expected**: `typeof value === "string"` + +**Kamer 8**: Type guards with 'in' operator +```typescript +interface Dog { bark(): void; } +interface Cat { meow(): void; } +function sound(animal: Dog | Cat) { + if (???) { + animal.bark(); + } else { + animal.meow(); + } +} +``` +**Expected**: `"bark" in animal` + +**Kamer 9**: Readonly properties +```typescript +interface Config { + readonly apiUrl: string; + debug: boolean; +} +const config: Config = { apiUrl: "...", debug: true }; +config.debug = false; // ✅ +config.apiUrl = "new url"; // ❌ fix this +``` +**Expected**: User realizes `readonly` prevents reassignment, removes second assignment + +**Kamer 10 (BOSS)**: Complex intersection + narrowing +```typescript +interface Timestamped { + createdAt: Date; + updatedAt: Date; +} +interface Entity { + id: number; + name: string; +} + +type Document = Timestamped & Entity; + +function process(doc: Document | undefined) { + if (???) { + console.log(doc.id, doc.createdAt); + } +} +``` +**Expected**: `doc !== undefined` (narrowing to verify it's not undefined before accessing properties) + +## Tips for Tim + +1. **Energy**: Les 4 is heavier than Les 3. Keep energy up during Blok 1-3 so students stay engaged. +2. **Live coding**: Don't just talk. Type. Let students see Cursor's intelligence. This motivates them. +3. **Pauze**: At 1:05, take a real break. Let students stretch, bathroom, water. They'll thank you. +4. **Escaperoom**: Circulate. Don't just sit at the front. Check in on students, offer hints, celebrate wins. +5. **Huiswerk**: Assign clearly. They NEED it for next lesson. +6. **Les 5 preview**: Generics sound scary. Reassure them: "You learned the foundation, next week is the application." + +## Common student errors to watch for + +1. Using `any` when stuck — remind them to ask or use error messages +2. Forgetting optional `?` when a property is sometimes missing +3. Union type usage without narrowing first +4. Spelling errors in property names — "emial" instead of "email" +5. Mixing up interface extends with intersection types (comes up in harder kamers) + +## Success criteria for students + +- By end of Blok 3: Can explain why TypeScript matters +- After escaperoom: Can fix 6-8 kamers +- By Les 5: Did the JS→TS homework (you'll see this) + +--- + +End of Les04-Slide-Overzicht.md diff --git a/Les04-TypeScript-Fundamentals/les4-typescript-escaperoom-v2.zip b/Les04-TypeScript-Fundamentals/les4-typescript-escaperoom-v2.zip new file mode 100644 index 0000000000000000000000000000000000000000..01daf50ce9be0eea0b0bcb42dc9477fe8c346771 GIT binary patch literal 9263 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 literal 0 HcmV?d00001 diff --git a/Les05-NextJS-Basics/Les05-Docenttekst.md b/Les05-NextJS-Basics/Les05-Docenttekst.md new file mode 100644 index 0000000..13882d6 --- /dev/null +++ b/Les05-NextJS-Basics/Les05-Docenttekst.md @@ -0,0 +1,672 @@ +# Les 05: Next.js — Het React Framework - Docenttekst +**NOVI Hogeschool | Instructeur: Tim | Duur: 180 minuten** + +--- + +## VOORBEREIDING + +**Checklist voor Tim (30 minuten voor les):** +- [ ] Cursor openen en `create-next-app` commando klaar hebben +- [ ] Terminal groot genoeg zodat klas kan zien (terminal font minimaal 16pt) +- [ ] Browser met localhost:3000 open +- [ ] Vercel account ingelogd (voorbeeldproject gereed) +- [ ] `.cursorrules` bestand voorbereid +- [ ] Slides open in presentatiemodus +- [ ] WiFi stabiel en npm packages cached +- [ ] Backup laptop met dezelfde setup klaar (plan B) + +**Hardware check:** +- HDMI/USB-C verbinding stabiel +- Scherm groot genoeg voor klas (10+ personen) +- Audio werkt (voor eventuele demo video's) + +--- + +## BLOK 1: HET PROBLEEM & DE OPLOSSING (0:00-0:20) + +### Slide 1: Titel +**Tijd**: 0:00-0:02 + +*Tim staat op, enthousiastisch, maakt oogcontact met de klas* + +> "Welkom bij Les 5! De afgelopen weken hebben jullie HTML, CSS, JavaScript, en TypeScript geleerd. Vandaag maken we een grote stap: we gaan van losse technologieën naar een echt framework. Next.js." + +*Tim loopt even heen en weer* + +> "Na vandaag kun je een complete webapplicatie bouwen. Met routing, API's, middleware — alles. En het mooiste? Het is gebouwd op React, dus alles wat je al kent werkt gewoon." + +*Tim duim enthousiast met twee duimen omhoog* + +--- + +### Slide 2: Planning Vandaag +**Tijd**: 0:02-0:05 + +*Tim wijst naar het scherm met een pointer* + +> "Dit is het plan. Eerst: waarom zou je Next.js gebruiken? Wat mist er in gewoon React? Dan duiken we in routing en project structuur — hoe organiseer je een Next.js app. Daarna TypeScript in Next.js, server en client components, en hoe je data ophaalt." + +*Tim loopt langs de slide* + +> "In het laatste blok: API routes, middleware, environment variables, hoe je deployed op Vercel, en Cursor workflow. Flink programma, maar het is allemaal praktisch." + +*Tim klapt in handen* + +> "Na de pauze gaan we SAMEN de setup doen. Ik loop het voor, jullie volgen mee. Zo heeft iedereen een werkend project staan. Daarna gaan jullie zelfstandig verder bouwen." + +*Tim knikt stellig* + +--- + +### Slide 3: Terugblik Les 4 — TypeScript +**Tijd**: 0:05-0:08 + +*Tim leunt tegen het bureau* + +> "Vorige week hebben jullie TypeScript geleerd. Types, interfaces, unions, narrowing — en natuurlijk die escaperoom. Wie heeft alle 10 kamers gehaald?" + +*Tim steekt hand op, kijkt rond* + +> "Nice. Die TypeScript kennis gaan we vandaag direct gebruiken. Next.js is volledig TypeScript-first. Alles wat je schrijft in Next.js is getypt. Dus jullie zijn al voorbereid." + +*Tim wijst naar zijn hoofd* + +> "Je hebt het juiste mental model al." + +--- + +### Slide 4: Het Probleem met Pure React +**Tijd**: 0:08-0:14 + +*Tim opent Cursor, navigeert naar een browser* + +> "Oké, stel je voor: je bouwt een webshop met React. Klanten moeten producten kunnen vinden via URL — `/products/laptop`. Je wilt dat Google de shop indexeert. Je wilt snelle laadtijden. Je wilt een API backend." + +*Tim telt op zijn vingers* + +> "React kan pagina's renderen. Maar routing? React-router nodig. Server-side rendering? Niet standaard. API? Aparte Express server. Images optimaliseren? Extra library. En alles moet je zelf configureren." + +*Tim doet alsof hij achter een puinhoop zit* + +> "Je bent meer bezig met setup dan met je app bouwen. Dat is het probleem." + +--- + +### Slide 5: Next.js = Het React Framework +**Tijd**: 0:14-0:18 + +*Tim opent de slide met Next.js logo's* + +> "Next.js pakt al die problemen en lost ze op in één framework. Routing? Maak een folder. API? Maak een `route.ts` bestand. Server-side rendering? Standaard aan. TypeScript? Zero configuratie." + +*Tim slaat op het bureau voor nadruk* + +> "Het is gemaakt door Vercel — dat is ook een hosting platform. Dus van development tot deployment zit het allemaal erin." + +*Tim kijkt direct naar de klas* + +> "En het belangrijkste: Next.js is gebouwd OP React. Je schrijft gewoon React components. useState, useEffect, JSX — alles wat je kent werkt gewoon. Next.js voegt er alleen superkrachten aan toe." + +--- + +### Slide 6: Wie Gebruikt Next.js? +**Tijd**: 0:18-0:20 + +*Tim toont een logo wall op het scherm* + +> "Netflix, TikTok, Nike, Notion — ze vertrouwen op Next.js. Waarom? Performance, SEO, developer experience. Snel itereren, snel deployen." + +*Tim wijst naar de logos* + +> "Als jullie straks solliciteren en Next.js op je CV staat, dan weten bedrijven: die kan een complete webapplicatie bouwen." + +--- + +## BLOK 2: APP ROUTER & PROJECT STRUCTUUR (0:20-0:40) + +### Slide 7: create-next-app (Live Demo) +**Tijd**: 0:20-0:28 + +*Tim opent Terminal in Cursor, groot zichtbaar voor de klas* + +> "Laten we het meteen proberen. Ik typ dit commando." + +*Tim typt langzaam: `npx create-next-app@latest demo-app`* + +*Wacht tot het begint en loopt door opties* + +> "TypeScript? Ja, Les 4 was niet voor niks. Tailwind? Ja, maakt styling makkelijker. App Router? Ja — dat is modern. Src directory? Ja, houdt je code netjes." + +*Tim wacht totdat het installeert, ongeveer 30 seconden* + +> "Nu: `cd demo-app && npm run dev`. Boem. Werkende app." + +*Tim opent browser op localhost:3000* + +> "Dat is alles. Geen webpack config, geen babel setup. Het werkt gewoon. Dit is Next.js." + +*Tim toont de default welcome page* + +--- + +### Slide 8: App Router — Folder = Route +**Tijd**: 0:28-0:32 + +*Tim toont de slide met de gouden regel* + +> "Dit is het meest elegante aan Next.js. De gouden regel: elke folder in `app/` is een route. Wil je een `/about` pagina? Maak een `about` folder met een `page.tsx` erin." + +*Tim maakt live in zijn project een nieuwe folder `about/page.tsx` aan* + +> "En klaar. Geen router config, geen complexe imports. Folder = route." + +*Tim navigeert in browser naar `/about` en toont dat het werkt* + +> "En nesting werkt hetzelfde. Dashboard met settings subpagina? `dashboard/settings/page.tsx`. Next.js snapt automatisch de structuur." + +--- + +### Slide 9: Speciale Bestanden +**Tijd**: 0:32-0:35 + +*Tim wijst naar de slide* + +> "Next.js heeft speciale bestanden. `page.tsx` — dat is je pagina. `layout.tsx` — dat is je wrapper. `loading.tsx` — loading spinner. `error.tsx` — error handler. `route.ts` — API endpoint." + +*Tim opent de file explorer en wijst files aan* + +> "Wat heel krachtig is: layouts nesten. Je root layout heeft een navbar. Je blog layout voegt een sidebar toe. Alles composeert vanzelf." + +--- + +### Slide 10: Layouts in Actie +**Tijd**: 0:35-0:38 + +*Tim toont de code slide met RootLayout* + +> "Kijk. De root layout wraps alles. Elke pagina krijgt deze nav en footer. En het mooie: als je navigeert, wordt de layout NIET opnieuw gerenderd. Alleen de `children` wisselt." + +*Tim highlight de `children` prop* + +> "En `Metadata`? Dat is type-safe SEO. Title, description, Open Graph — alles is getypt." + +--- + +### Slide 11: Dynamic Routes +**Tijd**: 0:38-0:41 + +*Tim wijst naar vierkante haken op de slide* + +> "Stel je hebt een blog met 100 posts. Je gaat geen 100 folders aanmaken. Je maakt één folder met vierkante haken: `[slug]`. Dat is een variabele." + +*Tim maakt een `blog/[slug]/page.tsx` folder aan* + +> "Alles wat je in de URL typt wordt beschikbaar als parameter. `/blog/mijn-eerste-post` — slug is `mijn-eerste-post`." + +*Tim loopt de TypeScript typing uit* + +> "En weer TypeScript. `params` is een Promise. Dat is Next.js 15. Je await't params en dan heb je `slug`." + +--- + +### Slide 12: Route Groups +**Tijd**: 0:41-0:44 + +*Tim toont de slide met ronde haken* + +> "Route Groups met ronde haken. `(marketing)` en `(dashboard)`. Ze verschijnen NIET in de URL — het is puur organisatie." + +*Tim maakt folders aan met ronde haken* + +> "Dus `(marketing)/about/page.tsx` wordt gewoon `/about`. En elk group kan zijn eigen layout hebben. Marketing met hero en footer, dashboard met sidebar." + +*Tim knikt* + +> "Hoe grote Next.js apps georganiseerd zijn." + +--- + +### Slide 13: Project Structuur Best Practices +**Tijd**: 0:44-0:47 + +*Tim toont de folder tree* + +> "Hier is hoe je een project organiseert. `app/` is ALLEEN voor routing — geen componenten. Componenten gaan in `components/`. Types in `types/`. Business logic in `lib/`." + +*Tim wijst naar elk onderdeel* + +> "Dit is geen harde regel van Next.js. Maar het is wel de conventie. Als je in een team werkt, weet iedereen meteen waar dingen staan." + +--- + +## BLOK 3: TYPESCRIPT, COMPONENTS & DATA (0:47-1:07) + +### Slide 14: TypeScript in Next.js +**Tijd**: 0:47-0:52 + +*Tim toont de code met PageProps* + +> "Hier komt Les 4 echt samen. Alles in Next.js is getypt. Page props? Getypt. API request bodies? Getypt." + +*Tim highlight de interface* + +> "We definiëren een `PageProps` interface met `params` en `searchParams`. Beiden zijn Promises. SearchParams kan optioneel `query` en `page` bevatten — die vraagtekens zijn optional syntax." + +*Tim tipt op het scherm* + +> "Nu weet TypeScript precies wat er binnenkomt. Typ je `body.titel` in plaats van `body.question`, krijg je direct rood." + +--- + +### Slide 15: Server Components vs Client Components +**Tijd**: 0:52-0:58 + +*Tim toont twee code blokken* + +> "Dit is misschien het belangrijkste concept in Next.js. Standaard is elk component een Server Component. Het draait op de server, niet in de browser. De browser krijgt alleen HTML — geen JavaScript." + +*Tim maakt een gebaar van versturen* + +> "Waarom is dat goed? Performance. Je stuurt minder code naar de browser. En je kunt direct data fetchen — `await fetch()` in je component." + +*Tim wijst naar het tweede blokje* + +> "Maar: server components kunnen geen interactiviteit hebben. Geen useState, geen onClick. Daarvoor heb je Client Components nodig. Je zet `'use client'` bovenaan en dan werkt het zoals je gewend bent." + +*Tim benadrukt* + +> "De truc is: server components voor alles wat kan, en client components alleen voor interactiviteit. Meeste code is server." + +--- + +### Slide 16: Data Fetching in Server Components +**Tijd**: 0:58-1:02 + +*Tim toont de server component met async function* + +> "In gewoon React fetch je data met useEffect. useState voor loading, useState voor error, useState voor data. Drie states voor één ding. In Next.js? Niet nodig." + +*Tim wijst naar `await fetch()`* + +> "Een server component kan async zijn. Je schrijft gewoon `await fetch()`. Component wacht op data en rendert het. Terwijl het wacht, toont Next.js automatisch je `loading.tsx`." + +*Tim highlight het `revalidate` object* + +> "En dat `revalidate: 60`? Dat is caching. Eerste keer fetcht Next.js. Volgende 60 seconden serveert het cache. Dan opnieuw. Nul configuratie." + +--- + +### Slide 17: Server Actions +**Tijd**: 1:02-1:05 + +*Tim toont de form met `action={createPoll}`* + +> "Server Actions zijn relatief nieuw en ze veranderen hoe je met formulieren werkt. In plaats van `onSubmit` met fetch naar een API, gebruik je een `action` die direct een server functie aanroept." + +*Tim wijst naar `'use server'`* + +> "`'use server'` in de functie body. Dat draait op de server. Browser stuurt form, server verwerkt het. Geen API route nodig." + +> "Waarom? Minder code. Geen API route bestand, geen fetch, geen response handling. En het werkt zonder JavaScript in de browser." + +--- + +### Slide 18: Loading & Error States +**Tijd**: 1:05-1:07 + +*Tim toont `loading.tsx` en `error.tsx`* + +> "In gewoon React: eigen loading states beheren. `useState(true)`, dan `if (loading) return `. In Next.js? Maak een `loading.tsx` en klaar." + +*Tim wijst naar error.tsx* + +> "Hetzelfde met errors. `error.tsx` vangt fouten op. Gebruiker ziet nette foutmelding in plaats van crash." + +*Tim slaat handen in elkaar* + +> "En TypeScript typing overal. `error: Error` en `reset: () => void`. Les 4 comes back!" + +--- + +## BLOK 4: API, MIDDLEWARE, DEPLOY & CURSOR (1:07-1:27) + +### Slide 19: next/image, next/link & Metadata +**Tijd**: 1:07-1:11 + +*Tim toont drie imports* + +> "Drie ding die je altijd gebruikt. Een: `next/image`. Gebruik dit in plaats van ``. Next.js optimaliseert automatisch — lazy loading, juiste formaat." + +> "Twee: `next/link`. Gebruik dit in plaats van `` tags. Client-side navigatie — pagina herlaadt niet, veel sneller." + +> "Drie: metadata. Exporteer een `metadata` object. TypeScript helpt met autocomplete — title, description, Open Graph. Alles voor SEO." + +--- + +### Slide 20: Route Handlers — Je Eigen API +**Tijd**: 1:11-1:16 + +*Tim opent een `route.ts` bestand* + +> "Route Handlers zijn API endpoints in je Next.js app. Bestand: `route.ts` — niet `page.tsx`!" + +*Tim toont de GET handler* + +> "Je exporteert functies die GET, POST, PUT, DELETE heten. Dit bestand op `/api/polls/route.ts` betekent: GET /api/polls." + +*Tim navigeert in browser naar `/api/polls`* + +> "En ik krijg JSON terug. Dat is mijn API. In hetzelfde project. Geen CORS issues, geen aparte server." + +*Tim highlight de TypeScript* + +> "En alles is getypt. `Poll` interface, request body getypt, return type getypt." + +--- + +### Slide 21: Dynamic API Routes +**Tijd**: 1:16-1:19 + +*Tim toont `/api/polls/[id]/route.ts`* + +> "Net zoals pagina's kunnen API routes ook dynamisch zijn. `/api/polls/1` geeft poll 1 terug, `/api/polls/2` geeft poll 2." + +*Tim highlight de error handling* + +> "En error handling is essentieel. Poll niet gevonden? 404 status. Alles getypt." + +--- + +### Slide 22: Environment Variables +**Tijd**: 1:19-1:22 + +*Tim toont `.env.local`* + +> "Environment variables zijn hoe je secrets beheert. Database URLs, API keys — niet hardcoden. In `.env.local`." + +*Tim benadrukt* + +> "Twee regels. Een: `NEXT_PUBLIC_` prefix? Beschikbaar in browser. Dus NOOIT secrets daarmee. Twee: zonder prefix? Alleen op server. Perfect voor secrets." + +*Tim wijst* + +> "`.env.local` staat automatisch in `.gitignore`. Komt nooit in Git." + +--- + +### Slide 23: Middleware +**Tijd**: 1:22-1:25 + +*Tim opent het middleware bestand* + +> "Middleware draait VÓÓR elke request. Eén bestand: `middleware.ts` in je `src` root. NIET in `app/`!" + +*Tim loopt door het voorbeeld* + +> "Usecase: logging, auth check, redirects, rate limiting. Elke request gaat langs de beveiliger bij de deur." + +*Tim wijst naar `matcher`* + +> "Met `matcher` bepaal je op welke routes het actief is. Wil je het alleen op API routes? `/api/:path*`." + +--- + +### Slide 24: Deployment op Vercel +**Tijd**: 1:25-1:27 + +*Tim toont Vercel dashboard* + +> "Vercel = makers van Next.js = beste hosting. Push naar GitHub, import project, KLAAR. Automatische deployments bij elke push." + +*Tim toont wat je gratis krijgt* + +> "HTTPS, global CDN, preview deployments per branch. Environment variables in dashboard. Serverless functions voor API routes." + +*Tim knikt* + +> "Voor het huiswerk: deploy op Vercel. Dan heb je een echte URL." + +--- + +### Slide 25: Next.js + Cursor — AI Development +**Tijd**: 1:27-1:30 + +*Tim opent Cursor* + +> "Cursor kent alle Next.js patterns. Zit je in `route.ts`, suggereert Tab-completie GET en POST handlers automatisch." + +*Tim waarschuwt* + +> "Maar: AI is tool, geen vervanger. BEGRIJP wat Next.js doet. Cursor genereert Server Component met useState? JIJ ziet dat is fout." + +*Tim toont `.cursorrules` bestand* + +> "Pro tip: maak `.cursorrules`. Vertel Cursor hoe je wilt coderen. Next.js 15, TypeScript, server components standaard. Veel betere suggesties." + +--- + +### Slide 26: Samenvatting — Next.js in Één Overzicht +**Tijd**: 1:30-1:33 + +*Tim toont de tabel* + +> "Dit is je cheat sheet. Bewaar deze slide. Alles wat je straks nodig hebt: pagina, layout, API route, middleware, loading, error." + +*Tim loopt de tabel door* + +> "Print dit in je hoofd. Server Component standaard. Client met `'use client'` voor interactiviteit. Server Action voor forms. Route Handler voor API." + +*Tim klapt* + +> "Alles getypt met TypeScript. Alles op Vercel. Dit is je complete toolkit." + +--- + +## PAUZE (1:33-1:45) + +*Tim geeft pauzeaankondiging* + +> "15 minuten pauze! Naar het toilet, koffie, stretch. Over 15 minuten gaan we SAMEN de setup doen. Iedereen een werkend project." + +*Tim loopt rond, praat informeel met studenten* + +--- + +## BLOK 5: SAMEN OPZETTEN & BOUWEN (1:45-2:45) + +### Slide 27: Opdracht — QuickPoll App +**Tijd**: 1:45-1:48 + +*Tim herstart na pauze, energiek* + +> "Oké! We gaan QuickPoll bouwen — een poll/stemming app. Alles wat we net geleerd hebben: routing, API's, TypeScript, components." + +*Tim wijst naar de slide* + +> "Aanpak: eerst SAMEN de setup. Ik loop het voor, jullie volgen mee. Daarna gaan jullie zelfstandig verder." + +--- + +### Slide 28: Samen Opzetten (Tim + Klas) +**Tijd**: 1:48-2:03 + +*Tim opent Terminal, grote font, klas kan alles zien* + +> "Open je terminal. We gaan dit samen doen. Typ mee. Stap 1: create-next-app." + +*Tim typt langzaam: `npx create-next-app@latest quickpoll --typescript --tailwind --app --src-dir`* + +*Pauzeert bij elke vlag om uit te leggen* + +> "TypeScript: yes. Tailwind: yes. App Router: yes. Src-dir: yes. Klaar?" + +*Wacht tot iedereen dezelfde output ziet* + +> "Nu: `cd quickpoll && npm run dev`. Check of localhost:3000 werkt op je laptop." + +*Tim wacht 30 seconden* + +> "Iedereen groen licht? Top!" + +*Tim opent zijn VS Code* + +> "Stap 2: types. Dit is de TypeScript mindset. We beginnen ALTIJD met types. Maak `src/types/index.ts`." + +*Tim typt langzaam en klas volgt* + +```typescript +export interface Poll { + id: string; + question: string; + options: string[]; + votes: number[]; +} + +export interface CreatePollBody { + question: string; + options: string[]; +} +``` + +*Pauzeert na elke interface* + +> "Dit zijn je core types. Alles in je app bouwt hierop. TypeScript helpt nu met autocomplete." + +> "Stap 3: folder structuur. Nog geen code — alleen bestanden aanmaken. Zo weet je wat je gaat bouwen." + +*Tim maakt folders aan op scherm, klas volgt in hun eigen IDE* + +``` +src/app/ + ├── page.tsx (al aanwezig) + ├── poll/ + │ └── [id]/ + │ └── page.tsx + └── api/ + └── polls/ + ├── route.ts + └── [id]/ + ├── route.ts + └── vote/ + └── route.ts + +src/components/ + └── (leeg voor nu) + +src/lib/ + └── data.ts + +src/types/ + └── index.ts (net gemaakt) +``` + +*Tim loopt langs elke folder* + +> "Oké, iedereen klaar met setup? Mooi. Nu gaan jullie ZELF verder. Cursor Tab UIT. Zelf typen. Ik loop rond en help waar nodig. Begrenzing: 60 minuten." + +*Tim geeft de opdracht sheet* + +--- + +### Slide 29: Individueel Bouwen (~60 min) +**Tijd**: 2:03-2:43 + +*Tim loopt rond, klas werkt zelfstandig* + +*Tim helpt bij veel voorkomende problemen:* + +- "Vergeten `'use client'` in component? Dat is een Server Component — je kan useState niet gebruiken." +- "API route geeft 404? Check de folder path — `/api/polls/route.ts` geeft `/api/polls`." +- "TypeScript error bij `fetch`? Zet het in een `try/catch` en type de response." + +*Tim monitort voortgang:* +- Na 10 min: "Iedereen verder dan stap 4? In-memory data? Top." +- Na 25 min: "Wie zit al aan stap 6? API routes? Nice!" +- Na 45 min: "Stap 7 is poll detail pagina. Daar is de meeste complexiteit. Zit je vast? Vraag hier." + +*Tim helpt met rondlopen:* + +**Tips voor Rondlopen (Tim geeft deze hints als nodig):** +- Stap 4 (data): Array met 3-4 polls in `lib/data.ts`. IDs als strings. +- Stap 5 (homepage): Import data, `map()` over polls, toon vraag + aantal opties. +- Stap 6 (API routes): GET terug alle polls. POST voegt nieuwe toe. GET met `[id]` terug single poll. +- Stap 7 (detail): Fetch poll data. Map options. VoteForm client component met onClick handler. +- Stap 8 (middleware): Log alle requests. `matcher: ["/api/*"]`. +- Stap 9 (loading): `loading.tsx` met skeleton. Tailwind `animate-pulse`. + +> "Cursor Tab mag nu AAN als je echt vastloopt. Maar geef het eerst 5 minuten zelf." + +> "Vragen? Ik ben hier. Vraag buurman/buurvrouw ook — samen is sterker." + +*Tim circuliert, geeft high-fives aan wie stap 7 afmaken* + +--- + +## BLOK 6: AFSLUITING (2:43-3:00) + +### Slide 30: Huiswerk — Next.js Mini Blog +**Tijd**: 2:43-2:50 + +*Tim verzamelt klas* + +> "Oké, huiswerk! Jullie bouwen een Mini Blog. Waarom een ander thema? Omdat ik wil dat je het ZELF kunt, niet dat je QuickPoll kopieert." + +*Tim loopt langs de eisen* + +> "Homepage met blog posts. Dynamic route `/post/[slug]` voor elke post. API routes: GET alle posts, POST nieuwe. Middleware voor logging. Loading en error states. Minimaal 3 pagina's." + +*Tim benadrukt* + +> "EN: deploy op Vercel. Ik wil werkende URL zien. Push naar GitHub, koppel Vercel, stuur link." + +> "Inleveren: GitHub repo + Vercel URL. Deadline: volgende les. Zorg dat `npm run dev` werkt." + +--- + +### Slide 31: Preview Les 6 +**Tijd**: 2:50-2:55 + +*Tim geeft sneak peek* + +> "Volgende les: verdieping. Database — Prisma of Drizzle. Authentication — NextAuth. Caching en revalidation. Forms met Server Actions verdieping." + +*Tim wijst* + +> "Zorg dat huiswerk af en deployed is. Les 6 bouwt hierop voort." + +--- + +### Slide 32: Afsluiting +**Tijd**: 2:55-3:00 + +*Tim staat vooraan, serieus maar met lach* + +> "Vandaag hebben jullie een COMPLETE Next.js app gebouwd. From scratch. Routing, API's, middleware, TypeScript — het hele pakket. Dat is impressive voor één les." + +*Tim loopt* + +> "Onthoud: Next.js is React met extra's. Alles wat je al kende werkt. Nu heb je een framework dat je leven makkelijker maakt. En Vercel zet het online in twee minuten." + +*Tim maakt oogcontact* + +> "Vragen? Nee? Mooi. Dan zie ik jullie volgende week. Vergeet huiswerk niet — ik wil URL zien!" + +*Tim geeft enthousiaste groet* + +--- + +## POST-SESSIE CHECKLIST + +**Tim controleert (30 minuten na les):** +- [ ] Alle aanwezigen hebben werkend `npm run dev` +- [ ] Typefiles sync'd met GitHub (voor huiswerk) +- [ ] Vercel accounts actief (student weet hoe te deployen) +- [ ] Huiswerkopdracht duidelijk — geen vragen open +- [ ] Slides online gedeeld met klas +- [ ] Feedback verzameld van moeilijke momenten + +**Voorbereiding Les 6:** +- [ ] Prisma setup docs klaar +- [ ] NextAuth.js voorbeeld project +- [ ] Database schema voorbeeld +- [ ] Caching strategieën voorbereiding diff --git a/Les05-NextJS-Basics/Les05-Lesopdracht.pdf b/Les05-NextJS-Basics/Les05-Lesopdracht.pdf new file mode 100644 index 0000000..60579f6 --- /dev/null +++ b/Les05-NextJS-Basics/Les05-Lesopdracht.pdf @@ -0,0 +1,296 @@ +%PDF-1.4 +% ReportLab Generated PDF document (opensource) +1 0 obj +<< +/F1 2 0 R /F2 3 0 R /F3 4 0 R /F4 7 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 +<< +/BaseFont /Helvetica-Oblique /Encoding /WinAnsiEncoding /Name /F3 /Subtype /Type1 /Type /Font +>> +endobj +5 0 obj +<< +/Contents 21 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 20 0 R /Resources << +/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ] +>> /Rotate 0 /Trans << + +>> + /Type /Page +>> +endobj +6 0 obj +<< +/Contents 22 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 20 0 R /Resources << +/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ] +>> /Rotate 0 /Trans << + +>> + /Type /Page +>> +endobj +7 0 obj +<< +/BaseFont /Courier /Encoding /WinAnsiEncoding /Name /F4 /Subtype /Type1 /Type /Font +>> +endobj +8 0 obj +<< +/Contents 23 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 20 0 R /Resources << +/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ] +>> /Rotate 0 /Trans << + +>> + /Type /Page +>> +endobj +9 0 obj +<< +/Contents 24 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 20 0 R /Resources << +/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ] +>> /Rotate 0 /Trans << + +>> + /Type /Page +>> +endobj +10 0 obj +<< +/Contents 25 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 20 0 R /Resources << +/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ] +>> /Rotate 0 /Trans << + +>> + /Type /Page +>> +endobj +11 0 obj +<< +/Contents 26 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 20 0 R /Resources << +/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ] +>> /Rotate 0 /Trans << + +>> + /Type /Page +>> +endobj +12 0 obj +<< +/Contents 27 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 20 0 R /Resources << +/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ] +>> /Rotate 0 /Trans << + +>> + /Type /Page +>> +endobj +13 0 obj +<< +/Contents 28 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 20 0 R /Resources << +/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ] +>> /Rotate 0 /Trans << + +>> + /Type /Page +>> +endobj +14 0 obj +<< +/Contents 29 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 20 0 R /Resources << +/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ] +>> /Rotate 0 /Trans << + +>> + /Type /Page +>> +endobj +15 0 obj +<< +/Contents 30 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 20 0 R /Resources << +/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ] +>> /Rotate 0 /Trans << + +>> + /Type /Page +>> +endobj +16 0 obj +<< +/Contents 31 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 20 0 R /Resources << +/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ] +>> /Rotate 0 /Trans << + +>> + /Type /Page +>> +endobj +17 0 obj +<< +/Contents 32 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 20 0 R /Resources << +/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ] +>> /Rotate 0 /Trans << + +>> + /Type /Page +>> +endobj +18 0 obj +<< +/PageMode /UseNone /Pages 20 0 R /Type /Catalog +>> +endobj +19 0 obj +<< +/Author (\(anonymous\)) /CreationDate (D:20260311071405+01'00') /Creator (\(unspecified\)) /Keywords () /ModDate (D:20260311071405+01'00') /Producer (ReportLab PDF Library - \(opensource\)) + /Subject (\(unspecified\)) /Title (\(anonymous\)) /Trapped /False +>> +endobj +20 0 obj +<< +/Count 12 /Kids [ 5 0 R 6 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 ] /Type /Pages +>> +endobj +21 0 obj +<< +/Filter [ /ASCII85Decode /FlateDecode ] /Length 1904 +>> +stream +Gat=,a`?-*&A@ZcqVi4V:nsGp@+-E5/+Q`$(6`<2--cMo7heL1!^P2b+lAk4llZo_;,l_S3\`rD_Z$>LJe!;1F@C:Hu4ZR]d/U>5jG$mA)HB20HDT'KpYKKs-g/\bq/P;[MECPV.pRonIM:7a9qm(-T4rGhC;.G%l3RBuN3,gsrQot>S_K"seU#Qsh(,BjJY?c@YsPu%=TocgE+\LK?9BE]jbd%%3G&+K6,KTd3->b15;fUBp)baWGl,6K"nAh6o1>O9]*,*?jEC-ian&_Nm^XVQ)Jk$7Pc8S^ie9IO-EF?>B,/Yr_bWWc##K`dcn-#8lsFY[&\Y`Z.X=ig*A4S)8q-%b@rMT(R,fU&!g;9cpsP!DWAb@S!N[PCEq8.+*W(uiE/gj5Hl$tkZ61'lA7]Gl/0_\L6bCfA@\!sbM&51]_=$[<]4n2]3617>s`-@;/r06$0&13)XmmD%_ga<.ftZr%p#s60.MQ@eItFZXH%+1+Ob:;/'X-G;qs2dZPpR#6_/EZ-2KTe@I0ae,A[oeT)4UTS.tYV@Vo=!*9I4"=EE%BK;,hWgUn%Q>cbOHT\lXY5Ne6Z;4i%#MT_XG5`9duW"37@9+'YE856>4/G"W5#5m6]IME8>gQ_=,!-eC_pGD(^JuQ=FtsNQNml"XPdl&#]RY3--k%Ksc?!%LIsJ*0a%2Ij`^j6d.)cIi'%We!HS[nFFMOZYEMWQs*5ELLl927^/P2mM_pG3jsQg#649eO,,"'LZ?W?`.2X7X]9!#=\&AbSN>!N4FeM2&)`>e/W%'!Ym"eiKq>Xq0GEDLnaJ&D1;JWh$RQ\0;i'+p]~>endstream +endobj +22 0 obj +<< +/Filter [ /ASCII85Decode /FlateDecode ] /Length 395 +>> +stream +GarVJb>,r/&A7ljjp75+I`LS?&PlER6H"^R:VdcGW!Bi7aZ&0]B`s\j#ZODoX/ueAM2ed3OZ&#KJuT[<3jcF'akC7Y,:k\XP!$XY&L%_ki;Wp!W>3_T?+\j]UL'\_*D)3a`X0\lnn.:0>E<4E2#&!8(@Cm=R8fJm?0PU;h)*utQnR1\rg`37.==cTXg:\^:rfrN,.rMCj7%@jW5_UgbHUiLgu'aa`H]endstream +endobj +23 0 obj +<< +/Filter [ /ASCII85Decode /FlateDecode ] /Length 1962 +>> +stream +Gat=,gN)%,&:N_CCofa`%%q1I-8*8+%$=o7\'uAEKfQ0`"F.=\2mJ%?F']2sO.=TMu+m^+N<[.ep)o<;*VN6K^uVCU1P2$>+'u,fj(YU>ZE\0<3XHoXBqd0p2Ga4Zi):&erXF@Su+ug75f`i6*-nVcn><#.TgPn$9+G%=f*s$LT3^P\.65pAD!SK5)HH,H@\#kVh^l))&EJ:Q9mE'_ms9hTG?D:^"VS1Uo\0keeg"C"S=_WRlfEu6@=+'%g5?(VnEMP^Dj[-W#u%OaRRR"6<-,-L1E*5;iWaL)`Udu-W>B($4dH]`B!XsSCUL)O*,l;"53h>N`rBP#3C$MF,Lj6H&:ZkS8r2l;04>OCcs<6gahrFMjY9mJ3GoMZJ6#XBZpWV\`$_E-8g]6C99l%ga6qI0-`i(D/"%nAUr*arfr(o[6JH#[<15f$Jous^FjfL&9RoJtX#$g^=Aa,&Jl:A>a4NcLh:$/Q9G35j>M+5EO3u5[U.#ZD^B%L_R=3Ofjm-:=[&-l5-]`[_Acr%Jg4'q&Be!*2=_00U>-_4s@5P"<1]j^8YEEG/?;EQsbkE_nk!XqF/nnd1>]DF\gAiHMQZK8l<+/YNK5F[!$USg?:cc%hmhgUBkr$\.4'A[^Int+!![fNFtfWX!M=6@?mkX?K7tWO^<6QC?5ng?'X!FOo`]/ERe7HiQ5hWtq6#NlNF=p&PMiASG[=a-UK/L*?\"nPi9VH%Bt*86:_QD;\j6_ml:;(t64XGR+-`TKaZ>I:MJoUuVQ#2?hOs%Fl7Jbf.>XC\P2=^iIe)dN41S-j]o.Q]Bh(BUAMaB3!,rtLJu$=Enl:*?lL@0mmC#g3M(&*THZ5@B]%C!-fkl)G%"l=seg*4d0!P>lk4F(M`m[e6IKHsI;"fuEmnB*X;JDV_A1cCbN:X&ZH1@Jd6$duMqS[d'.=htF(QBuKkMOD)i-O]o(mYRbI8-iH1CXSl]>/lnip>%9FcqghM."QWmc/(KCIW6OXMKP6LLDPYd)?h&P$&k`l#-tBT>1;J!WV5%nt8*`-8-\AJ:pK)'3P8:b;i*W~>endstream +endobj +24 0 obj +<< +/Filter [ /ASCII85Decode /FlateDecode ] /Length 1741 +>> +stream +Gatm;D/\/e&H8h>E*7BYX>#/CCa9>')/R:,;1Nmmp*Wjd3IZ$3QT=8)?E':/UNM8*tf7!-g__iC=7)L[bd$,7MRnn6t\=41*ZY`afe[5(j4e1l)2qjq&;`5/%ZeO'"Yu,_!U,L+8_9@/n,HI^>Z<;gX%C+i],et).ZLO#SRhr?Bgh`/M3\BTeo?6P89.H?(J9]K!MfmiAA\MgSW#TD[^"UtAO^Ulu+"Pj8hdYG,!FigP\\:^bcXa0RX`j[XNA#.m"$UO-CR)Xsh_,5XuFSu$&K!Yp5LY,uRIUnOB9-RldTN9`">qUmVI?H*TFT'hoG>s!@)8_R+,WsuMq"MB;4MY>KL5fr;O.Lp^fq0a%n-=_#&1D>83@H=6X;-?J$6IV&DE5qOi.)LCmtBUVXA`uX-#S"j)CC*:Fo,C1T`D5BYMk&53h)JFPm4d8c*Xa`0Ete;EEK),=f%Fr75uE?cl+*DM>s5&OXjc:]>#AP8hM(\)hDm0c/gF1]]?:Ll^A2Uc?n7Q0tQ6*Pc'-nl1k!Jlc-Z^UJ96B,Fboe7u&ZA[pI6(YC8Qk,m`1blCr]4hmgP-'c8b:R9V:fV:/C0=ma4TJP=?>W-6AGniWr"Xp9-cp^N\@\Mg-YAE==%5+q':kl.kr,,!3(Y6RA5,W*M_f[ltNR$QDFe#cHPaoR>tWH,/sWRP*(cJ(_HT4-&G=l%Vp+ClXN]$t!#`QbF,m]NAt(H)L%ma107%)fm=_m:ao')`6'?TG^?^dK_,6.tr,A8kt9W%YBHNs'5#Z$CoAmc=\LHHe4ZZAmWM7,d:?"YUmnAiU[Ses,LhJ?g][-Xo'8FTfP=H3gGFuI,UiFeXie7SQ#;KA<2l@tZ[](9O*W5.6bBrb(R?7>Fa`meUFn^+9;b3QC^HR;Op]-5/s]CLc;i``m9hCc37^c7gZk$T3j%tjM/mYe]D[8=]]_&_>J:ntr;S]Y4s>rli%Ro!Yq@bd16LS+X3^N\+)"CCb@NpUcf_e,\U*DVOoPsle>&Mol6$e)@!HlSH5EJ%Di7?TbkZVe!(bV2NNU1VFtbIh(>d"bp8=TZJs"ZD-/EfKtEqC]"bKj!lp+N8tTAGo6S&qT&t(\er,G]Zk^=\lZ:T+57XZh:'LmJE)\9M6>q4P7JbY,KmRjbRh!!6$2:rFQT^6*a[\9qC"]75+'mGF4RE_mK(g"SY84N]d-ZCu?.HNV4h4Lof/R)Dp;@]+]"SDlGq4&ScrdFWo&N-[
ZEf1>uC78F)3/fEiUj.NIJ-9Z08XHHIs'0\*2`44?Lu^^[dfPp"\=nVb4+-XpFsIiE<;n08B2gMT"Zjs31/Z[,H>GrA?BCo=*V:RG4Sq9X54#HA>9gY2mOm%;ir/Q;1OP>ha0,k")re9=p&j'c"O;LEhU&)~>endstream +endobj +25 0 obj +<< +/Filter [ /ASCII85Decode /FlateDecode ] /Length 1879 +>> +stream +Gatm.KpDp5gc,?e"K`TYMh&UuhpES!2)#,i9GE`4-5(W5S'2/c\F"eWo^uPi/:'NWZZX)hl`-*^?gqY'[odF\s&BAuHr(/H_F/TBjYQ>&@Pf\Fd*8Hhj*7lL$ig9`aG'r;[QCMqWUDZ"IkPP7imm&_n4)R;G_sd`<[i=p4&t!8B5VLBggJtqmIKX5a45$9,erLY`BuX=Z1dLqW[F#lD2KBMj)Cu*a1W2_2n>$CP4GT])F,Ad@LZ9Cu3(I[5I$2D:h;nB9Y[E?m_-6#oemoe(@b*`O?V;Xn'Z[aAtPDJD7%*F`f4=c&?)@KnfYiMN5jgk23p-5;16hcgT$cQJ2i+ldl>P7kAN710E?NX(,UnK3#^3V6[d)[L4:mZZuA<7!Fm+63=:8DG5.)_\#b6!7D=pMMKZ^t22(3JOo%o=MF!K'\E!d;&ZDc0."_2-!Y1m`hI5E4H%Z*X4mkA"HG[%CgL%3^(q6h\D9N_t%4dBE#cM73D9/`;qko!1+fO;.*V6]i&P?o&h!KBpmH[Y]UE$GE<&'WC_@X.X9M)aKY3O:`^s1?O0\H_\ZKp3g=/tY)Q#69i(F:?KX+FDA2ZCC/i?+2D55t>H+6Z/?U4P5qoD*r0b"Y[WV@GWSJ]bdeZa^&\:A?HM[5!cT/`8SM=Zu(g93KaU)[XiHgG1;&$[",*jekLQ4aXmC?gL9X^qbH0pFhM=e4b_ZUY;IdSZ_De`=0[,C\Bmt`fAD#,A-HlddG1[.C#:DYQVq-G2h6kI/ar-!hWQ)7u:>-E8B87M[56Qrs6Q6[3\\tP9`K9Z8bHLZr0?s?oPV6HBZMDJqa0G:"pnp48ehmC"n_7ZQlo!V.nI]3t\m]E&CUMWP]3m8KrJ,8AlU]NJ$&b%+_dRId-F#XDO!%-Fp;e<#["@b>9Eeurno-I'H12Y'C(kFliGCPK)dfnS(VSS1RJj;EU-`dm/?Gj1@EpK#Am#`1>%bt@o-+oC)_?o9a"/9[an2u0);erue]?98Q%XAWJA51B@lU:l`dn4N/g?d'!#'o?0+i\cjB6YGGMRFIduo=J&\e33QtCl3;BB9ZgP*gI0$G/rAa"aX\lPHsbY<(po`$]Ya$_V0+:^_/&M7bUJ9gTAm\8$NLnXg^OoIdRiM2!r_f_M>9DNsRC=P9C]`/XO(RPrbU+"a3[uJ4G__rA0&+gendstream +endobj +26 0 obj +<< +/Filter [ /ASCII85Decode /FlateDecode ] /Length 1670 +>> +stream +Gau`SCN%rc'`G1AEEf)d'6H&[Oa;6hL3Dl-?-t^8Yl^Eh6'3!gM(C]NS?s1-hfhXT/6Qt_[MT2nECbtRLZb7MF2`[hLRI&I$K1^g`\/2`)eS9K38lQ<$SQ,CQ,W#9?(X*ZD1`dH_Ls:URmjjUgA!&#D3#&/,d7B21%L$`=WFoQ/FL);#b7ZOikQq5C[2`'d;#6bchID*QpYDl"#h>DV-]hh2KKoa!FnCqD)BS$C+0aG^&U9au5i%rS=eM-j2:o*ooR-:Al%[XuoIfs<+e0LsjTg%5D2W(e]gnERt$A9KD#$#>g'7khQ*fs-Q[H<2;D_im6BH42,plgXoZsrrG')si72/Z;-4f!:2&L\P=\9kL7ZsCH3!ht5@76hRt,>kI\;=hmG0R=sP&V_S_AI7\,ba$?/3m>BIdeB[uN4.)[W4*p?0H]NfKP>2b0BPc$._-%I6=unA+CuMBDYD"UZ>c'Cd8CIK@:C0^7b*XjWuSq'4:\eH)8m05<>s%C$c<_7W&u[AJ9@67.p:#b*%%5fR!%[&8@PXBiJ;[Ub'J(T(O0g8StZ(&)3kc0akFmrk#]q&0g*h'gO\\T>od#$9L$rDR!#LRoLAeE2DI?q\^PAh3`oq*R)0o"_@f$Q6Y8KD=Zj2+#oq8,k_-`?6uHSqb`m]2`#ir[@g\5J_O-)hDN1E8o:U5TP;QW,G=j2<=WU2(Z`^+kJY[cg/hSm3Gu4WT5<%$)<[p=t16!CCW5[udrE9[G5;_b6jl>bb=d5Bm:PGX.>(505kF4"P'+$TQ^FT\5]QPgSo2rF&Dk3fSL!6.*9&krZL8`Wi\inlE?X^Pl5jn@]%7,VirQ8m=lVm1NeZ@`>/n^gG%P*7S0(>`1Aj7C0')-\7XPjcglOtN43XerN`8(hJnK8KIB/W=ad8aRC8o^>(:X&O.0N:egKdB5QRNG66j*'sBH&J-p64\rpGb\/t1``^!TWh;0Wa$rD45759?]>#s*0U9c@jMIjfTANM4$rFM40hKe0.3pAE**4NI?CNt12Yh=TJQ@FgdL6f;*NZuE;RnRr<*4eH3?b"\HFZ#IhrX2GV<\#DdBr>K*J'43>T$@4A9n)W[&K.PS%+A"0mQ-Dg2_%[s#7oD+gnFWKho8FL/6=b62CQ/]\Q'SY$.C6BXR1(H'3XubkUFuSV`U2'e\8eNOP)":O9g:n1oTY&;2BE'heg[siII.gln%38Gk-V64C[9B<_Z^Rr;W[?1/1U/X99%8k(f\/fBnrendstream +endobj +27 0 obj +<< +/Filter [ /ASCII85Decode /FlateDecode ] /Length 1595 +>> +stream +Gat=+lYkN9&HAoJib"j^'/VNp;_J[#OKRu6+ipE5dK.NT0JXY!OX45+qH/jZI7NR2.>m:H&/(C$o'U_j'*F+&1]P?=^q2%S4u-M(9]s2Ni@mEG%o[7\Z0J.NPHeoXi!,]*a)ga=7C_g?plj@=`uP@0YQKnJZkQNE1mbVSk5/g.rLg%-FJoF'VK7T$R1AJhm;UBk.d#2+$nnN6kK)s-bm&sda7(N0PaoPbg#Wog-'rs4]n>R`k&U!^$[eH=bR_&gT&"U]Aq0c/:23/'_:R=/+#sHn'2RpKbd276<\+*+7]SA+o?lICm%;[l<68"7@2"oMi'%Oi_3j24]+qPES,IIK'a8\4U+4!H#c/FZ2,AB'_tW_/#mLYd6o't21)=-;"N6hGHO4+#a[B'c,#]V(_q95`'c>k4j-&`t!Rd6jdc;5_I@EK"/8o&'O4CL=p^iP-9\*AX[0MW?9H]"VCnsSt$m8Q3h=dNpHARZkfuNW8Aq+[Jb'+i-R^gVm@WRN`!6Y]4!7*G,riAHA*PMluSD@FNfV-]J!`q"j.n#d]rZ#5EA@kB"jGV?Ij$L1uf$K6LFdFh#hlVHdOZ^-cXjppRLA?U`IU;]18Dj_8`u]\`O.Vbq[mDAhPKiaPU>9#T+Nb%q,VhMVoH&nrXu5M\#ab$<^B^7W_Z?#rQg[L/`H6omKc+7=CfVV*%GEuTH[Ygi!T`uP.j-uCnaH]Rjh?CbFm;h('\;kmAoscf``kM&]"Q0DE]Nk+2?T6ARm&AT=,2^2b:oM%^:';`'h(W.26C876_`3DTG)K&V7BD1+\&\WEc(d]#CO24@TStkgPjcUPDO^61lFMm5&Oj-jLDBeh<\7Da.3MHJ5;1>XGGDBZ11mW>GjTP+*qO])D8!NC\(K5r\;>>u$'cP`<`ho:U^QQ](Og>Oh(5dR`3#;_Kg[#^g$!c?J,G9Bodd1jRc1Wf6oD;ed8X@o#Nfd)]GFe^u3C2+S'=]Hbn6T1pnl+ZBm-5FJdT`,pM_qPlk5pi6JChXBepg>&C2a~>endstream +endobj +28 0 obj +<< +/Filter [ /ASCII85Decode /FlateDecode ] /Length 1594 +>> +stream +Gb!ks>Ar7S'RnZ;3%.V=$"fX%86rND6^ouLLFc)!,6^!oJWUgSN@[(FSYdA&hfhUsHr4--!VS>U0[8?4+$Y3:4Cg_IA*u:D!PB>fi.M-k0Y@T:Lk+G5]GZ%)*ihd8&b,tK+c$;116acC,9dISr["(i68BZZ(H'Q!uQKenMlA'MiYHQS3ZPN#)`u2lP)[s!3L<].,F=$=hXR@][J/H(UJCLleL"gNR])/;iR(mMoC(D<+Z-t@+/[qoLVp*@+79`;OkA)1-0+$TGD=HeG345L;:!KBrm8GUGA5#;GVBm-Mto[!H&nMGEF8J\F%'()DX3R%''hSem@h/#A%:DBEK>rTnUdAg5AB(i`gi99ja7.[`TtXcqGa3>%G\^UlJ6OqfVG]eV#"ah&:],U?2qMo_?dfnX_d&@RdEkj*g[HQD=I*9AIQ&;kes4I&iXsR:.ImeXbSDe#4@MA6H=<$n7ibu_@8;\kNi6\dlO58.Q\D$sHp?=4.n^HKh&S6Ypo[S2Z?8e6X3$^ERWB*7)](4karh%=/-EP(!dBiZ]=ilrmhSA)--Gr.RGA:G0dG-:b\_Xj3/d.\CESQl)8oV]1lMPAA7f+-nk3hcA]7`S,.ff!\^E+(Br[(5Wa_LTD)/XWnNNGbWk<-b'*S9qVH=!>o1'9?f>_VBQZZiV>Au..G]Z#eIN^VCU"?g_C5jMlkK71&-3dEDE!X#K*+M9I73qRo/nKBo=`p+26d\0cA#261KWu'nXqE$B>JtB5f1$n>8WB,GN4\YJ-KakobWWo^>Ki-h&bJrZ[Ri&K9L_XLFZBZ=FLsI#n-sD\QVNc.,MQh1k`7a?'J\,MgjB@(^lo1>31##/Do8.U,;V0_8>Zo+m9a)%D3DEsV5*+0D+-CHou6r/Z;,EcNq%,qX`E5tnTEYcmc3,WHDNBS39o+.Id$fu\ilYMjJFH^T,$EZPoWFiNL:jt#$I(eCg&C6O\[F.SeS9,_d>7uD3L$_Mg[a>2#:fm=uBKk2pll:^hP3rV[[L8BLFPM3!(:c=G\"iM@FAP$#)5QP,.O-CS,RDH3*RB/s^3lj.:5X0mE?GjZ@]k9JdfMU8T$WX!^KGL(1eF!HC0!ir~>endstream +endobj +29 0 obj +<< +/Filter [ /ASCII85Decode /FlateDecode ] /Length 1885 +>> +stream +GatU4gN)%,&:N/3Cc%OiSOO;bOVFeEm5RkiUm&enXmm&H%$?W>M^2sKoHbFUHPa=dOEG=oRLt5-mb(n(/64fFpsI9'56c?sLAj[`F9BN]cqW&DnJCtAgpub7oh*t9#I\&km@(%m%5$Ad;,,(hn>C"<6dsILFUcqj);2*TFA1I*G>-al/ITTULp5p<6e`/.k"r+uF`ln-cDkf7he>0\Wt%"(k_d'ufMGXT?$BRfV%!\&W`E7aK!JFJW)G3^8$nO)P:;e%3E(iXr4,Z=Vd&@TTF456^Vh]9o2YA@!i@B2aZ0+D38cGe^5C=6t5JpD7/tZqdQsne,W!(m!gk./I`U*F-JfYq`]p:QI(QdUC$d[iE$dk)QE4W[Vcl(.8?I'?,iu\WLQI%=pF/LNV.[o;F1*u_a?D;QmAUSDAJr1$47J6cRqG7:SLHk[J6>(+dp\JX@Nn9PgTu;7H3p#k3tOH9n5WNcWCC\'G_9(fo1BX>WqFSc,Q,cK3`B&*4MKRlSLehrB%)ih0>V'p'5[XS;lGN/O=F9&GQu\[)0kY8X"eZ/DFFYrDMbFM#Kb>@/&l[3WNb*!5HTO1V[Ja?A?6tP.ueb_Mee(TGtefKOj;mKV^+l7L&r)*.8:#l<<1fi#T(=`u\W(,Q67@YYN_4@%d=*OhEQ$0PW4J[,[Yq8i%36+cV(QkT5$2^-Y]*&T#jI9F1FGa*;8E\nIBBCg6B[;TPcJb/6pgn@r@dlqc[kdI&K[(=oH-h1`h%j[s&d//0gWnqQdHf1;*!H%o6$*VOAOdjV;.\ib"\M(@6_`lQhOi[\M+G`qtKNV1.!Qdi9'XSLunO=^uhY8Orh$rTo9eja%EA@@"3D6q=@I2K*@DS-YlL1d3YA2@h0U[XD-Z9)TFn\*.ZBhueU&&EjA5,;^hQ;+_<*JRp&QIFm,?sVYpWI&CU]EZ2!tdX5V&9#8B;:/*S18F7]3QiS0lXif1Yt0IrOY8DWS^X!<`!Mt@62nfNXjB*n1W%E8CS&>rSmO8KTr\p1T)l@2^3i`U's%8]dT[5A1g'8bC/)RVCAco97+D:D_6<429m(@c=Z(JIcY#eh&KGS[3eZKUVnok4'endstream +endobj +30 0 obj +<< +/Filter [ /ASCII85Decode /FlateDecode ] /Length 564 +>> +stream +Gar>CbAQ&g&A7(hZf`4c-0mMUDY=?=lDG`JXf@[,K$klG5/[;(S+L6H+/pWZo&^hQ>'o/[Y[\K_,0^*15n!7q3de)Y['^blI]hU`R6fF+`sgF2"mSFD]/2un'kCXkCUWWWa9%jf?RU!e,q>+oL'h>)+lHom\m';59bT%_nWu_6@J2i&9[42@mr@R_n4;]h'\6@luo2u_\qa:WbW^:_N!IQE,endstream +endobj +31 0 obj +<< +/Filter [ /ASCII85Decode /FlateDecode ] /Length 1710 +>> +stream +Gb"/'>Ar4d'Roe[39"B.<+5:Kj'/osBm\ZA7C`]40;T0`-8<:035-Z7B!q:@9K9$_F%AQRhN%c*>^k\%lD9umk(D+WnD=Bk]W9_(*boeS,D#?k@gLi,B'&"MH1\/)=)@X)$#4jt:\gXm-`^)k;cW2jG[q[oLILlXO0nZI1*dAju/^"[ThH&c;&lKX&YWgB\E#@tAI<1,k6J9'_-S\VNs^hQK5$`jkm%l9\r"iqoM4nQPRu9lCiMmdMde"m`8YN7K$7WBJPR(.LT*s?Umo.I505=[":]GAOYq?M;(@a-jSc0;$f4Rc!).ga4;V)VH)mMsMa:4VkTd3VE0@Bd81/6b85ZmpY'M8l;KROuIUBDDSG%&9r')>mdX2.tpGr_`8p:?/9$:8?47cX'"MjNpO2M#>VHI)S?%mp+/IP8!6RB_:&tppd1$'!kUO9^60ba`obnfj.joDS*cDDiRlq0dA)M#o$s3Oio`M2OIY'Z./A;8oY8@N,%]UXKiXH2]gEk5$^a#qFIuu8B@(c)2!,J-mCDGQkDJUd?lO?C286d$`^f'aT::ZO(,Bpj^oplWsDBZdXDZ[?nPL3VF8.0&u^jDn1XQ(Oa7hXnUMh@I*aTLFgqlTn;"3(kc,Ml"+[+0obU&]^Sl0j=!krb;B2<`Z]@_nD:[;T/\X?7(a@?JT("5nXQ%0,_iE&n+"^q*pU!iM!]m_rYS=mpqU'<4BXBE*2U$$)sGO8%_pM^tOCRL"8GT)TUA%Rt@o/cDlI,n^V9]cb>O/7B\4hJJ'_$`XZ<<n/=_m_b_%e`D@aVY`I0<^4FnCL_$5Sr_&nF59#XJ>^P)B(e.u-rH$$%W]hVT=-Y2\D's@I\3u+D6>QDCKR4V';,*t$-slAn=BXTU2p&(/0fFB2h@>Ces#bG8Ygus<0H,7endstream +endobj +32 0 obj +<< +/Filter [ /ASCII85Decode /FlateDecode ] /Length 2067 +>> +stream +Gat=,=``=U&:WfGfXd9'P8Q^f$"!%AHKD@E2g/rdm7c=@MLm*W,U&A5SR_6J]AN^Q[Ugd+ND2#@HXO,RYY,07J#rJgB)o\A@If9VZXLEF`X.Fb$+ud3f+r.6H3&"JgBK=lKXngT@M[pRBB(MID4eEK88*buFVIJ![lB7V(ABrN\iPl@_%f=sljW^g3oX0>K(IFoV;AO&9M0pp`+h&p7krd(S,!R'Woq5aF-B*`:4icoXcS;XUs4SZ*kYXR["Q:N3gP8N;/JPF,q[[KO.XKPOE&G]%V9FL#2fg%lCBkBl%.p9Hhl>8fUKS]86*21>7bo2kc*L'8J@O.lYl!,6iTbj9`lr_Ym8@k\qPiU^XTU&_!P&4/\k#^R[U*GXD4[e!#tT(J,):5:gk]E5gmJ(8AfI/5a(#-+"tucS_I0?8mVoY!2YLbR7c[F2_b0.^PUai=3s1/_>YAq.,0EMK9$!i\7#<+Vm>;Lu;5]P?"dDTEUsWj@m[D93oPZ/gU>%(6NK"O1AWFD%)3R"-JKLQuL0EUJ]=^SVCUPG0p$*;a5)2hW\%[gj-R)^-kI!1cA%I^-50Bo@C3?VU6puJ4(4T`W>G^LA#Km]6DegT$)`YtXL?mCE(&KKMDgqqZq2S`RAT*,T4IriUih)uM?f.-'Xo/P;97`.c3h)oL&/;8D&ShQ'_Oa)+-LZbmWU%RDs9A:&kj47F-%\Z(9)O9FuY(>OKLE:iV?bIZ$mY8S/*5iP#(RTL_%Crkc=Ifra"J7L0'=deX;2ZOW0bP+0R7qEbk+"XB1?(RDO6gU4!4d%+F(OhNT%9H(?^=7JBqU[Of(ZHQ8hb#2[*X##[#T0A7HLWLCZ5?gjKX_@W8j=L-MOeU0`kr:`;<+7RIDB!-nL`H7flYleCf(7KdPmjiVVTmjWc2ZpXHE+VYYT;XPQ069A8A:k3-2BU<;7]2h_#n0OOkAi6rlf0l?\_R31nH9<7C!a45BA3PT/AHS`GONFGU5T?L_*K\\B$DX4_afCJi=ZTYGCGcn:thU:gf`%583=9heZr7KD%7qW<5qeHP,13-'VQPMrdW#rHURScmf@]WH9V9ch3/e'-PVo6SNRf.3uX3-^Ia+,>TN#B3#o0l8kG4+;MiUFqOZR@=psp&q=O8"(($r#=@;Q6cj'I>FSr(Pi,G4`*F7,R0ul3MC[9)9/?t.6]"#Y1)Co*&DD^_2Gh_Pei@DP+*g:AH\9b$L1UcZX`Z%KQl&;Z<3JALP1T]Nendstream +endobj +xref +0 33 +0000000000 65535 f +0000000061 00000 n +0000000122 00000 n +0000000229 00000 n +0000000341 00000 n +0000000456 00000 n +0000000661 00000 n +0000000866 00000 n +0000000971 00000 n +0000001176 00000 n +0000001381 00000 n +0000001587 00000 n +0000001793 00000 n +0000001999 00000 n +0000002205 00000 n +0000002411 00000 n +0000002617 00000 n +0000002823 00000 n +0000003029 00000 n +0000003099 00000 n +0000003380 00000 n +0000003518 00000 n +0000005514 00000 n +0000006000 00000 n +0000008054 00000 n +0000009887 00000 n +0000011858 00000 n +0000013620 00000 n +0000015307 00000 n +0000016993 00000 n +0000018970 00000 n +0000019625 00000 n +0000021427 00000 n +trailer +<< +/ID +[<4a95945443b3266c457c35d632c7f433><4a95945443b3266c457c35d632c7f433>] +% ReportLab generated PDF document -- digest (opensource) + +/Info 19 0 R +/Root 18 0 R +/Size 33 +>> +startxref +23586 +%%EOF diff --git a/Les05-NextJS-Basics/Les05-Slide-Overzicht.md b/Les05-NextJS-Basics/Les05-Slide-Overzicht.md new file mode 100644 index 0000000..a67a854 --- /dev/null +++ b/Les05-NextJS-Basics/Les05-Slide-Overzicht.md @@ -0,0 +1,974 @@ +# Les 5: Next.js — Het React Framework - Slide Overzicht (Part 1) + +> Versie 2 — ~45 minuten theorie + 15 min pauze + ~120 min klassikaal bouwen +> +> **Dit is Part 1 van 2.** Part 2 wordt behandeld in Les 6 (Blok 4: advanced features, Stap 4-7 QuickPoll, deployment). + +--- + +## Slide 1: Titel - "Les 5: Next.js — Het React Framework" + +### Op de Slide +- Titel: **Les 5: Next.js — Het React Framework** +- Subtitel: "Van React naar Productie" +- **Les 5 van 18** (progress indicator) +- Next.js logo + Vercel logo +- Dark background, modern feel + +### Docentnotities +Tim opent enthousiast en maakt connectie met vorige lessen. + +"Welkom bij Les 5! De afgelopen weken hebben jullie HTML, CSS, JavaScript, en TypeScript geleerd. Vandaag maken we een grote stap: we gaan van losse technologieën naar een echt framework. Next.js." + +*Pauze voor effect.* + +"Na vandaag kun je een complete webapplicatie bouwen. Met routing, API's, middleware — alles. En het mooiste? Het is gebouwd op React, dus alles wat je al kent werkt gewoon." + +--- + +## Slide 2: Planning Vandaag + +### Op de Slide +- **Blok 1 (15 min)**: Waarom Next.js? + - Het probleem met pure React + - Wat Next.js oplost + +- **Blok 2 (15 min)**: App Router & Project Structuur + - Folder-based routing, layouts, dynamic routes + - Route Groups & best practices + +- **Blok 3 (15 min)**: Server vs Client Components, Data Fetching, Server Actions + - Server Components: async, geen JS naar browser + - Data fetching patterns + - Intro Server Actions + +- **PAUZE (15 min)** ☕ + +- **Klassikaal Bouwen (~120 min)**: QuickPoll App Part 1 (Stap 0-3) + - Tim + klas werken samen + - Studenten volgen mee in hun eigen project + - Cursor Tab-completion mag gebruikt worden + +**Totaal: ~3 uur. Part 1 → Blok 1-3 + Stap 0-3. Part 2 (Les 6) → Blok 4 + Stap 4-7 + Deployment.** + +### Docentnotities +Tim loopt door de planning. + +"Dit is het plan voor vandaag. Drie blokken theorie: waarom Next.js, hoe routing werkt, en de kern—server vs client components en data fetching. Dan pauze." + +"Daarna doen we het ANDERS dan vorige keer. In plaats van dat jullie allemaal zelfstandig werken, gaan we SAMEN een app bouwen. Ik code op het scherm, jullie volgen mee op jullie eigen laptop. Dat voelt beter dan solo werk, toch?" + +"We bouwen QuickPoll Part 1 — dat is de eerste helft van de app. Part 2 met deployment en ingewikkelder features doen we volgende les. Zo hebben jullie vandaag meer tijd om echt te begrijpen wat er gebeurt." + +--- + +## Slide 3: Terugblik Les 4 — TypeScript + +### Op de Slide +- **Les 4 samengevat:** + - TypeScript = JavaScript + Types + - Interfaces, Union Types, Type Narrowing + - Escaperoom: 10 kamers, 49 errors +- **Key takeaway**: Types voorkomen bugs vóórdat je code draait +- **Vandaag**: TypeScript in actie binnen een framework + +### Docentnotities +Tim doet een korte terugblik. + +"Vorige week hebben jullie TypeScript geleerd. Types, interfaces, unions, narrowing — en natuurlijk die escaperoom. Wie heeft alle 10 kamers gehaald?" + +*Pauze voor reacties.* + +"Nice. Die TypeScript kennis gaan we vandaag direct gebruiken. Next.js is volledig TypeScript-first. Alles wat je schrijft in Next.js is getypt. Dus jullie zijn al voorbereid." + +--- + +## Slide 4: Het Probleem met Pure React + +### Op de Slide +- **React is geweldig, MAAR:** + - ❌ Geen ingebouwde routing (react-router nodig) + - ❌ Geen server-side rendering (slecht voor SEO) + - ❌ Geen API routes (aparte backend nodig) + - ❌ Geen file-based structuur (je moet alles zelf organiseren) + - ❌ Geen image optimization + - ❌ Geen built-in performance optimizations + +- **Het resultaat:** + - React app = SPA (Single Page Application) + - Alles draait in de browser + - Google ziet een lege pagina + +### Docentnotities +Tim legt het probleem uit met concrete voorbeelden. + +"Oké, React is fantastisch voor UI bouwen. Maar als je een échte website wilt maken — eentje die Google kan indexeren, die snel laadt, die een API heeft — dan loop je tegen grenzen aan." + +"Stel je bouwt een webshop met React. Je hebt routing nodig: react-router. Je wilt dat Google je producten vindt: SSR, maar React doet dat niet standaard. Je wilt een API voor je producten: dan moet je een aparte Express server opzetten. Je wilt images optimizen: weer een extra library." + +*Tim telt op zijn vingers.* + +"Dus je bent meer bezig met configuratie dan met je app bouwen. En dat is precies het probleem dat Next.js oplost." + +--- + +## Slide 5: Next.js = Het React Framework + +### Op de Slide +- **Next.js biedt alles out-of-the-box:** + - ✅ **File-based Routing** — folder = route + - ✅ **Server-Side Rendering (SSR)** — HTML op de server + - ✅ **API Routes** — backend in hetzelfde project + - ✅ **Middleware** — logic vóór de request + - ✅ **Image Optimization** — next/image + - ✅ **TypeScript-first** — zero config + - ✅ **Built-in CSS/Tailwind** — styling out-of-the-box + +- **Gemaakt door Vercel** — het bedrijf achter de hosting + +### Docentnotities +Tim bouwt enthousiasme op. + +"Next.js pakt al die problemen en lost ze op in één framework. Routing? Maak een folder. API? Maak een `route.ts` bestand. Server-side rendering? Standaard aan. TypeScript? Zero configuratie." + +"Het is gemaakt door Vercel — dat is ook een hosting platform. Dus van development tot deployment, het hele verhaal zit erin." + +"En het belangrijkste: Next.js is gebouwd OP React. Je schrijft gewoon React components. Alles wat je kent — useState, useEffect, JSX — werkt gewoon. Next.js voegt er alleen superkrachten aan toe." + +--- + +## Slide 6: Wie Gebruikt Next.js? + +### Op de Slide +- **Grote bedrijven op Next.js:** + - 🎬 Netflix — marketing pages + - 🎵 TikTok — web app + - 📝 Notion — website & docs + - 🛒 Nike — webshop + - 📊 Twitch — dashboard + - 🏢 Hulu, Hashicorp, Auth0, ... + +- **Waarom?** + - Performance, SEO, Developer Experience + - Snel itereren, snel deployen + +- Logo wall van bedrijven + +### Docentnotities +Tim laat zien dat Next.js geen speelgoed is. + +"Dit zijn geen kleine startups. Netflix, TikTok, Nike — ze vertrouwen op Next.js voor hun websites. Waarom? Omdat het snel is, goed voor SEO, en developers er productief mee zijn." + +"Als jullie straks solliciteren en Next.js op je CV staat, dan weten bedrijven: die kan een complete webapplicatie bouwen. Dat is wat deze les zo waardevol maakt." + +--- + +## Slide 7: create-next-app — Snel Starten + +### Op de Slide +```bash +npx create-next-app@latest mijn-app +``` + +- **Opties die je krijgt:** + - ✅ TypeScript + - ✅ Tailwind CSS + - ✅ App Router + - ✅ `src/` directory + - ❌ ESLint (optioneel) + +- **Project structuur klaar:** +``` +mijn-app/ +├── src/app/ +│ ├── layout.tsx ← root layout +│ ├── page.tsx ← homepage +│ └── globals.css +├── public/ +├── package.json +├── tsconfig.json +└── next.config.ts +``` + +### Docentnotities +Tim toont hoe snel je begint. + +"Create-next-app zet alles voor je klaar. TypeScript, Tailwind, App Router — nul configuratie. Je typt het commando en binnen tien seconden heb je een werkend project." + +"Straks gaan we dit samen doen. Dan zie je: `npm run dev`, dan staat je app op localhost:3000. Klaar." + +--- + +## Slide 8: App Router — Folder = Route + +### Op de Slide +- **De gouden regel:** Elke folder in `app/` is een route + +``` +src/app/ +├── page.tsx → / +├── about/ +│ └── page.tsx → /about +├── blog/ +│ ├── page.tsx → /blog +│ └── [slug]/ +│ └── page.tsx → /blog/mijn-post +├── dashboard/ +│ ├── page.tsx → /dashboard +│ └── settings/ +│ └── page.tsx → /dashboard/settings +``` + +- **Geen react-router nodig!** +- **Nested routes** = nested folders + +### Docentnotities +Tim legt het kernprincipe uit. + +"Dit is het meest elegante aan Next.js. Wil je een `/about` pagina? Maak een `about` folder met een `page.tsx` erin. Klaar. Geen router configuratie, geen ``. Gewoon: folder = route." + +*Tim maakt live een nieuwe folder `about/page.tsx` aan.* + +"En nesting werkt hetzelfde. Dashboard met een settings subpagina? `dashboard/settings/page.tsx`. Next.js snapt automatisch de structuur." + +"Dit is waarom developers Next.js zo fijn vinden. Je ziet de folder structuur en je weet meteen welke pagina's je app heeft." + +--- + +## Slide 9: Speciale Bestanden + +### Op de Slide +- **`page.tsx`** — De pagina zelf (verplicht voor een route) +- **`layout.tsx`** — Wrapper om pagina's (header, footer, sidebar) +- **`loading.tsx`** — Loading state (automatisch getoond) +- **`error.tsx`** — Error boundary (vangt fouten op) +- **`not-found.tsx`** — 404 pagina +- **`route.ts`** — API endpoint (in plaats van page) + +``` +app/ +├── layout.tsx ← wraps ALLES +├── page.tsx ← homepage +├── loading.tsx ← loading spinner +├── error.tsx ← error handler +├── not-found.tsx ← 404 pagina +└── blog/ + ├── layout.tsx ← wraps alleen /blog/* + └── page.tsx ← /blog pagina +``` + +### Docentnotities +Tim legt elk bestand uit. + +"Next.js heeft speciale bestanden die automatisch iets doen. De belangrijkste: `page.tsx` — zonder die is er geen route. Dan `layout.tsx` — dat is je wrapper. Denk aan een template: header bovenaan, footer onderaan, en daartussen wisselt de content." + +"Wat heel krachtig is: layouts nesten. Je root layout heeft misschien een navbar. Je blog layout voegt een sidebar toe. Alles composeert vanzelf." + +"En dan heb je `loading.tsx` en `error.tsx`. Die zijn magisch. Als je pagina data laadt, toont Next.js automatisch je loading component. Geen `useState(true)` en `if (loading)` meer. Het framework doet het voor je." + +--- + +## Slide 10: Layouts in Actie + +### Op de Slide +```tsx +// src/app/layout.tsx — Root Layout +import type { Metadata } from "next"; + +export const metadata: Metadata = { + title: "Mijn App", + description: "Gebouwd met Next.js", +}; + +export default function RootLayout({ + children, +}: { + children: React.ReactNode; +}) { + return ( + + +
+
{children}
+
© 2025 NOVI
+ + + ); +} +``` + +- **`children`** = de pagina die in de layout wordt gerenderd +- Layout blijft staan bij navigatie (geen re-render) +- **`Metadata`** type uit Next.js voor type-safe SEO + +### Docentnotities +Tim toont hoe layout en page samenwerken. + +"Kijk, de root layout wraps alles. Elke pagina in je app krijgt deze nav en footer. En het mooie: als je navigeert van Home naar About, wordt de layout NIET opnieuw gerenderd. Alleen de `children` — de page — wisselt." + +"Let op de TypeScript: we importeren het `Metadata` type van Next.js. Dat zorgt ervoor dat je editor je helpt met de juiste properties. Titel, description, Open Graph — alles is getypt." + +"Dit is een groot verschil met een gewone React app waar je hele component tree opnieuw rendert bij elke route change." + +--- + +## Slide 11: Dynamic Routes + +### Op de Slide +- **Dynamic segments** met vierkante haken: `[param]` + +``` +app/ +└── blog/ + └── [slug]/ + └── page.tsx → /blog/mijn-eerste-post + → /blog/nextjs-is-cool + → /blog/wat-dan-ook +``` + +```tsx +// src/app/blog/[slug]/page.tsx +interface PageProps { + params: Promise<{ slug: string }>; +} + +export default async function BlogPost({ params }: PageProps) { + const { slug } = await params; + return

Blog post: {slug}

; +} +``` + +- **Meerdere params:** `/shop/[category]/[productId]` +- **Catch-all:** `[...slug]` → vangt alles op + +### Docentnotities +Tim legt dynamic routes uit met een concreet voorbeeld. + +"Stel je hebt een blog met 100 posts. Je gaat geen 100 folders aanmaken. In plaats daarvan maak je één folder met vierkante haken: `[slug]`. Dat is een variabele. Alles wat je in de URL typt wordt beschikbaar als parameter." + +"Dus `/blog/mijn-eerste-post` — slug is `mijn-eerste-post`. `/blog/nextjs-is-cool` — slug is `nextjs-is-cool`. Eén page component handelt alle blog posts af." + +"Let op de TypeScript typing: `params` is een Promise met een object. Dat is nieuw in Next.js 15 — je moet params await'en. En kijk: we definiëren een `PageProps` interface. Alles wat je in Les 4 geleerd hebt, gebruiken we hier." + +--- + +## Slide 12: Route Groups + +### Op de Slide +- **Route Groups** met ronde haken: `(naam)` +- Folder voor organisatie, maar NIET in de URL + +``` +src/app/ +├── (marketing)/ +│ ├── layout.tsx ← eigen layout voor marketing +│ ├── page.tsx → / +│ ├── about/ +│ │ └── page.tsx → /about +│ └── pricing/ +│ └── page.tsx → /pricing +├── (dashboard)/ +│ ├── layout.tsx ← eigen layout voor dashboard +│ ├── dashboard/ +│ │ └── page.tsx → /dashboard +│ └── settings/ +│ └── page.tsx → /settings +``` + +- `(marketing)` en `(dashboard)` verschijnen NIET in de URL +- Elk group kan zijn eigen layout hebben +- Handig voor: verschillende layouts, code organisatie + +### Docentnotities +Tim legt route groups uit als organisatie tool. + +"Stel je hebt een marketing website en een dashboard in dezelfde app. De marketing site heeft een heel ander design dan het dashboard. Hoe los je dat op?" + +"Route Groups. Je maakt een folder met ronde haken: `(marketing)` en `(dashboard)`. Het verschil met vierkante haken: ronde haken verschijnen NIET in de URL. Het is puur voor organisatie." + +"Dus `(marketing)/about/page.tsx` wordt gewoon `/about`. Niet `/(marketing)/about`. En elk group kan zijn eigen layout hebben. Marketing met een hero en footer, dashboard met een sidebar en topbar. Allemaal in hetzelfde project." + +"Dit is hoe grote Next.js apps georganiseerd worden. Je scheidt concerns zonder de URL structuur te beïnvloeden." + +--- + +## Slide 13: Project Structuur Best Practices + +### Op de Slide +``` +src/ +├── app/ ← Routes & pages +│ ├── (marketing)/ +│ ├── (dashboard)/ +│ └── api/ +├── components/ ← Herbruikbare UI components +│ ├── ui/ ← Generieke components (Button, Card) +│ └── features/ ← Feature-specifiek (PollCard, VoteForm) +├── lib/ ← Utility functies & helpers +│ ├── utils.ts +│ └── api.ts +├── types/ ← TypeScript type definities +│ └── index.ts +└── middleware.ts ← Middleware (altijd in src root) +``` + +- **Regel 1:** `app/` is alleen voor routing — geen componenten +- **Regel 2:** Gedeelde components in `components/` +- **Regel 3:** Types in `types/` — herbruikbaar across het project +- **Regel 4:** Business logic in `lib/` + +### Docentnotities +Tim deelt praktische structuur tips. + +"Nu je weet hoe routing werkt, is de vraag: waar zet je de rest? Components, types, utility functies? Hier is de conventie die de meeste Next.js projecten volgen." + +*Tim wijst naar de structuur.* + +"De `app/` folder is ALLEEN voor routing. Pagina's, layouts, API routes. Geen losse components. Die gaan in `components/`. Onderscheid tussen generieke UI components — Button, Card, Modal — en feature-specifieke components — PollCard, VoteForm." + +"Types in een `types/` folder. Zo kun je ze importeren vanuit elke plek in je project. En business logic — data fetching, formatters, validators — in `lib/`." + +"Dit is geen harde regel van Next.js. Maar het is wel de conventie. En als je in een team werkt, weet iedereen meteen waar dingen staan." + +--- + +## Slide 14: TypeScript in Next.js + +### Op de Slide +- **Next.js is volledig getypt** — alles heeft types + +```tsx +// Page props zijn getypt +interface PageProps { + params: Promise<{ id: string }>; + searchParams: Promise<{ query?: string; page?: string }>; +} + +export default async function SearchPage({ params, searchParams }: PageProps) { + const { query, page } = await searchParams; + // ... +} +``` + +```tsx +// API Route met getypte request body +interface CreatePollBody { + question: string; + options: string[]; +} + +export async function POST(request: Request) { + const body: CreatePollBody = await request.json(); + // TypeScript checkt nu of body.question en body.options bestaan +} +``` + +- **`Metadata`** type voor SEO +- **`NextRequest`** en **`NextResponse`** voor API/middleware +- Alles uit Les 4 (interfaces, union types) komt hier terug + +### Docentnotities +Tim maakt de connectie met Les 4. + +"Hier komt Les 4 echt samen. Alles in Next.js is getypt. Page props? Getypt. API request bodies? Getypt. Middleware? Getypt." + +"Kijk naar dit voorbeeld. We definiëren een `PageProps` interface met `params` en `searchParams`. Beide zijn Promises — dat is een Next.js 15 patroon. SearchParams kan `query` en `page` bevatten, maar ze zijn optioneel — de vraagtekens. Dat is de optional syntax uit Les 4." + +"En voor API routes: je definieert een interface voor de request body. `CreatePollBody` met `question` en `options`. Nu weet TypeScript precies wat er binnenkomt. Als je per ongeluk `body.titel` typt in plaats van `body.question`, krijg je direct een rode lijn." + +"Dit is waarom we vorige week TypeScript hebben geleerd. Niet als losstaand iets, maar als fundamenteel onderdeel van hoe je Next.js apps bouwt." + +--- + +## Slide 15: Server Components vs Client Components + +### Op de Slide +- **Server Components** (standaard): + - Renderen op de server + - Geen JavaScript naar de browser + - Kunnen direct database/API aanroepen + - Kunnen async zijn + - ❌ Geen useState, useEffect, onClick + +- **Client Components** (`"use client"`): + - Renderen in de browser + - Interactiviteit: useState, useEffect, event handlers + - ❌ Geen directe database/API server-side + +```tsx +// Server Component (standaard) — async allowed! +export default async function ProductList() { + const res = await fetch("https://api.example.com/products"); + const products: Product[] = await res.json(); + return
    {products.map(p =>
  • {p.name}
  • )}
; +} +``` + +```tsx +// Client Component — interactiviteit +"use client"; +import { useState } from "react"; + +export default function LikeButton() { + const [likes, setLikes] = useState(0); + return ; +} +``` + +### Docentnotities +Tim maakt het verschil heel duidelijk. + +"Dit is misschien het belangrijkste concept in Next.js. Standaard is elk component een Server Component. Dat betekent: het draait op de server, niet in de browser. De browser krijgt alleen HTML — geen JavaScript." + +"Waarom is dat goed? Performance. Je stuurt minder code naar de browser. En je kunt direct data fetchen — de server component kan async zijn. Gewoon `await fetch()` in je component." + +"Maar: server components kunnen geen interactiviteit hebben. Geen useState, geen onClick. Daarvoor heb je Client Components nodig. Je zet `'use client'` bovenaan je bestand en dan werkt het zoals je gewend bent van React." + +"De truc is: gebruik server components voor alles wat kan, en client components alleen waar je interactiviteit nodig hebt. De meeste code is server, en kleine interactieve stukjes zijn client." + +"En kijk: `useState(0)` — de TypeScript generic syntax uit Les 4!" + +--- + +## Slide 16: Data Fetching in Server Components + +### Op de Slide +- **Server Components kunnen direct data fetchen** — geen useEffect nodig + +```tsx +// src/app/polls/page.tsx — Server Component +interface Poll { + id: string; + question: string; + options: string[]; + votes: number[]; +} + +async function getPolls(): Promise { + const res = await fetch("https://api.example.com/polls", { + next: { revalidate: 60 }, // Cache voor 60 seconden + }); + return res.json(); +} + +export default async function PollsPage() { + const polls = await getPolls(); + + return ( +
+

Alle Polls

+ {polls.map((poll) => ( + + ))} +
+ ); +} +``` + +- **`next: { revalidate: 60 }`** — cache + automatisch verversen +- **Geen useState, useEffect, loading state nodig!** +- Next.js toont automatisch `loading.tsx` tijdens het fetchen + +### Docentnotities +Tim toont het verschil met klassieke React data fetching. + +"In gewoon React fetch je data met useEffect. useState voor loading, useState voor error, useState voor data. Drie states voor één ding. In Next.js? Niet nodig." + +"Een server component kan async zijn. Je schrijft gewoon `await fetch()`. De component wacht op de data en rendert het resultaat. En terwijl het wacht, toont Next.js automatisch je `loading.tsx`." + +"En dat `revalidate: 60`? Dat is caching. De eerste keer fetcht Next.js de data. De volgende 60 seconden serveert het de cache. Na 60 seconden fetcht het opnieuw. Zero configuratie." + +"Vergelijk dat met React: daar moet je React Query of SWR installeren, loading states beheren, caching instellen... Next.js doet het allemaal voor je." + +--- + +## Slide 17: Server Actions + +### Op de Slide +- **Server Actions** = functies die op de server draaien, aangeroepen vanuit forms +- Geen API route nodig voor form handling! + +```tsx +// src/app/create/page.tsx +export default function CreatePollPage() { + async function createPoll(formData: FormData) { + "use server"; + + const question = formData.get("question") as string; + const options = formData.get("options") as string; + + // Dit draait op de SERVER + console.log("Nieuwe poll:", question); + // Database insert, API call, etc. + } + + return ( +
+ + + +
+ ); +} +``` + +- **`"use server"`** in de functie = draait op de server +- Geen `e.preventDefault()`, geen `fetch()` naar een API +- FormData wordt automatisch doorgestuurd +- Werkt ook zonder JavaScript in de browser! + +### Docentnotities +Tim introduceert Server Actions als moderne form handling. + +"Server Actions zijn relatief nieuw in Next.js en ze veranderen hoe je met formulieren werkt. In plaats van een onSubmit handler met fetch naar een API route, gebruik je een `action` op het form die direct een server functie aanroept." + +"Kijk: `'use server'` in de functie body. Dat vertelt Next.js: dit draait op de server. De browser stuurt het form, de server verwerkt het. Geen API route nodig." + +"Waarom is dit handig? Ten eerste: minder code. Geen API route bestand, geen fetch call, geen response handling. Ten tweede: het werkt zelfs zonder JavaScript in de browser — progressive enhancement." + +"In de opdracht van vandaag gaan jullie nog gewone API routes gebruiken, want dat is belangrijk om te begrijpen. Maar bij het huiswerk mogen jullie Server Actions proberen als alternatief." + +--- + +## Slide 18: Klassikaal Bouwen — QuickPoll App Part 1 + +### Op de Slide +- **Uitleg:** Tim codeert op het scherm, jullie volgen mee in je eigen project +- **Dit is niet solo werk** — we doen het SAMEN +- **Stap 0-3 vandaag:** + - Stap 0: Project Setup + - Stap 1: Layout & Navigatie + - Stap 2: Homepage — Polls Lijst + - Stap 3: API Route — GET Single Poll + +- **Tempo:** Tim pauzeert regelmatig — "Heeft iedereen dit? Steek je hand op" +- **Cursor Tab:** Mag gebruiken (Tab completion), maar NIET Cmd+K (generator) + +### Docentnotities +Tim start het klassikaal bouwen. + +"Oké, nu gaan we bouwen. Dit keer anders dan vorige keer — we werken samen. Ik code op mijn scherm, jullie volgen mee. Als je achterblijft, steek je je hand op en wij wachten." + +"Waarom samen? Omdat jullie vorig keer veel te veel alleen zaten, en dat was niet naar jullie voorkeur. Dit is beter voor iedereen." + +"Cursor mag aan voor Tab completion — dat helpt je typen. Maar Cmd+K mag nog niet. We willen dat je het zelf begrijpt, niet dat Cursor het schrijft." + +"Laten we beginnen." + +--- + +## Slide 19: Stap 0 — Project Setup + +### Op de Slide +- **Unzip starter** (of create-next-app) +```bash +npx create-next-app@latest quickpoll --typescript --tailwind --app --src-dir +cd quickpoll +npm run dev +``` + +- **Folder structuur klaarmaken:** +``` +src/ +├── app/ +│ ├── layout.tsx +│ ├── page.tsx +│ ├── poll/ +│ │ └── [id]/ +│ │ └── page.tsx +│ └── api/ +│ └── polls/ +│ └── [id]/ +│ └── route.ts +├── components/ +│ └── PollCard.tsx +├── lib/ +│ └── data.ts +├── types/ +│ └── index.ts +└── middleware.ts +``` + +- **Types definiëren:** +```tsx +// src/types/index.ts +export interface Poll { + id: string; + question: string; + options: string[]; + votes: number[]; +} +``` + +- **Bezoek `localhost:3000`** — controleer dat het werkt + +### Docentnotities +Tim doet setup live, klas volgt stap voor stap. + +"Stap 0: we zetten het project op. Iedereen typt het create-next-app commando mee." + +*Tim typt het commando, wacht tot iedereen het draait.* + +"Terwijl dat installeert, maken we de folder structuur aan. Niet in code — gewoon de lege folders. Klik op app/, nieuwe folder 'poll'. Daarin '[id]'. Daarin 'page.tsx'. Vervolgens 'api/', daarin 'polls/', daarin '[id]/', daarin 'route.ts'." + +*Tim maakt alles visueel aan.* + +"Nu types. Maak `src/types/index.ts` aan. Dit is waar alle TypeScript interfaces leven." + +*Tim typt de Poll interface.* + +"Klaar? Open je browser op localhost:3000. Je moet de default Next.js pagina zien. Mooi — je project leeft." + +--- + +## Slide 20: Stap 1 — Layout & Navigatie + +### Op de Slide +- **Update root layout met navbar:** + +```tsx +// src/app/layout.tsx +import type { Metadata } from "next"; +import Link from "next/link"; + +export const metadata: Metadata = { + title: "QuickPoll — Stem op alles", + description: "Democratie in je broekzak", +}; + +export default function RootLayout({ + children, +}: { + children: React.ReactNode; +}) { + return ( + + + +
{children}
+ + + ); +} +``` + +- **Test:** Navigatie zichtbaar? Styling oke? + +### Docentnotities +Tim bouwt de layout. + +"Stap 1: layout. Dit is je wrapper — header, footer, sidebar — alles wat op elke pagina hetzelfde is." + +*Tim typt de layout.* + +"Let op: we gebruiken Tailwind classes voor styling. `bg-white`, `border-b`, `max-w-4xl` — allemaal standaard Tailwind. En we gebruiken `next/link` voor de navigatie — geen `` tags." + +"Metadata staat ook hier — Google gaat dat zien. Titel en description." + +*Tim opent localhost:3000.* + +"Zien jullie de navbar? Prima. Dan gaan we verder." + +--- + +## Slide 21: Stap 2 — Homepage & Polls Lijst + +### Op de Slide +- **In-memory data aanmaken:** + +```tsx +// src/lib/data.ts +import { Poll } from "@/types"; + +export const polls: Poll[] = [ + { + id: "1", + question: "Wat is je favoriete programming language?", + options: ["TypeScript", "Python", "Rust", "Go"], + votes: [15, 8, 6, 3], + }, + { + id: "2", + question: "Voorkeur: Dark mode of Light mode?", + options: ["Dark", "Light"], + votes: [22, 8], + }, +]; +``` + +- **Homepage component:** + +```tsx +// src/app/page.tsx +import Link from "next/link"; +import { polls } from "@/lib/data"; + +export default function HomePage() { + return ( +
+

Alle Polls

+
+ {polls.map((poll) => ( + +
+

{poll.question}

+

+ {poll.options.length} opties • {poll.votes.reduce((a, b) => a + b, 0)} stemmen +

+
+ + ))} +
+
+ ); +} +``` + +- **Test:** Zie je 2 poll cards? Kunnen je erop klikken? + +### Docentnotities +Tim bouwt de homepage. + +"Stap 2: we maken data en tonen die op de homepage." + +*Tim maakt eerst data.ts.* + +"In-memory array met poll objects. Dit is waar al onze data leeft. In de echte wereld zou dit een database zijn, maar voor vandaag: JavaScript array." + +*Tim update page.tsx.* + +"Homepage component — geen `'use client'`, dus het is een server component. We fetchen polls, we mappen erover, we renderen kaarten. Link wrapper maakt elke kaart klikbaar." + +*Tim opent localhost:3000.* + +"Mooi! Je ziet twee polls. Ze zijn klikbaar. Maar de poll detail pagina bestaat nog niet — je krijgt 404. Dat doen we straks." + +--- + +## Slide 22: Stap 3 — API Route GET Single Poll + +### Op de Slide +- **Dynamic API route voor 1 poll:** + +```tsx +// src/app/api/polls/[id]/route.ts +import { NextResponse } from "next/server"; +import { polls } from "@/lib/data"; + +interface RouteParams { + params: Promise<{ id: string }>; +} + +export async function GET( + request: Request, + { params }: RouteParams +): Promise { + const { id } = await params; + + const poll = polls.find((p) => p.id === id); + + if (!poll) { + return NextResponse.json( + { error: "Poll niet gevonden" }, + { status: 404 } + ); + } + + return NextResponse.json(poll); +} +``` + +- **Test:** Ga naar `/api/polls/1` — zie je JSON? + +### Docentnotities +Tim bouwt de API route. + +"Stap 3: API route voor één poll. Dit is belangrijk om goed te begrijpen." + +*Tim typt de route.* + +"Map erover die array, vind de poll met het gegeven ID. Niet gevonden? 404. Wel gevonden? Return JSON." + +"Let op: `params` is een Promise in Next.js 15 — je moet het awaiten. Dit is nieuw!" + +*Tim opent `/api/polls/1` in de browser.* + +"Je ziet JSON terug. Mooi. Dit is je API. Nu kunnen client components dit aanroepen en stemmen vastleggen." + +--- + +## Slide 23: Huiswerk & Volgende Les + +### Op de Slide +- **Huiswerk vandaag (voor wie achterblijft):** + - Zorg dat stap 0-3 volledig werkt + - Test alle routes + - Zet je code op GitHub + +- **Volgende les (Les 6) — Part 2:** + - Stap 4: Client-side VoteForm component + - Stap 5: POST /api/polls/[id]/vote + - Stap 6: Poll detail pagina /poll/[id] + - Stap 7: Middleware & loading states + - Blok 4 theorie: advanced features + - Deployment op Vercel + +- **Preview:** Volgende keer maken we het interactief — stemmen opslaan, real-time updates + +### Docentnotities +Tim vat samen en geeft huiswerk. + +"Stap 0-3 zijn klaar. Je hebt een homepage met polls, en een API route die single polls teruggeeft." + +"Huiswerk: maak het af als je nog achterblijft. Zet het op GitHub zodat we volgende les kunnen doorgaan." + +"Volgende les is Part 2. Dan bouwen we de stem-functie — VoteForm component, POST route, alles interactief. We deployen ook op Vercel." + +"Tot volgende les!" + +--- + +## Slide 24: Afsluiting + +### Op de Slide +- **Wat we vandaag gebouwd hebben:** + - Next.js project van nul + - App Router routing — folder = route + - Server Components en data fetching + - API routes met dynamic parameters + - TypeScript interfaces overal + - Tailwind CSS styling + - In-memory data management + +- **Key moments van vandaag:** + - Layout wraps alles (geen re-render bij navigatie) + - Server components: gewoon async/await + - Route handlers: [id]/route.ts = dynamic API + +- **Klaar voor Part 2:** + - Volgende les: client interactiviteit, voting, deployment + +### Docentnotities +Tim sluit enthousiast af. + +"Jongens, wat hebben we gebouwd vandaag! Een echte Next.js app met routing, API's, TypeScript — allemaal samen. Dit is wat developers doen." + +"Je snapt nu hoe Next.js apps werken. Routing via folders, server components standaard, kleine stukjes client code waar je het nodig hebt." + +"Volgende les: we maken het interactief. Dan stemmen mensen echt, en we zien live hoe Vercel deployment werkt." + +"Goed gedaan vandaag! Tot volgende week!" diff --git a/Les05-NextJS-Basics/les5-quickpoll-starter.zip b/Les05-NextJS-Basics/les5-quickpoll-starter.zip new file mode 100644 index 0000000000000000000000000000000000000000..b64f7b5a612a68fe4bbd8f7092a4cb73a1075447 GIT binary patch literal 40143 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

D!z~CrRGShf9tm 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�i!uL^}knQD1=zO5yB-mB^ocNaR6mGHGad0n8%l

vWApd)prP=HXDkLshztC{DMHG+KP%m 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 literal 0 HcmV?d00001 diff --git a/Les05-NextJS-Basics/les5-quickpoll-voorbeeld.zip b/Les05-NextJS-Basics/les5-quickpoll-voorbeeld.zip new file mode 100644 index 0000000000000000000000000000000000000000..7009fd79871b00f11fa12abddef692064d35f809 GIT binary patch literal 40483 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 SYQ>$!4nHB#VQX{){`VioW-o#O literal 0 HcmV?d00001 diff --git a/Les05-NextJS-Basics/quickpoll 2/.cursorrules b/Les05-NextJS-Basics/quickpoll 2/.cursorrules new file mode 100644 index 0000000..7de9420 --- /dev/null +++ b/Les05-NextJS-Basics/quickpoll 2/.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/Les05-NextJS-Basics/quickpoll 2/.gitignore b/Les05-NextJS-Basics/quickpoll 2/.gitignore new file mode 100644 index 0000000..5ef6a52 --- /dev/null +++ b/Les05-NextJS-Basics/quickpoll 2/.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/Les05-NextJS-Basics/quickpoll 2/README.md b/Les05-NextJS-Basics/quickpoll 2/README.md new file mode 100644 index 0000000..e215bc4 --- /dev/null +++ b/Les05-NextJS-Basics/quickpoll 2/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/Les05-NextJS-Basics/quickpoll 2/next.config.ts b/Les05-NextJS-Basics/quickpoll 2/next.config.ts new file mode 100644 index 0000000..e9ffa30 --- /dev/null +++ b/Les05-NextJS-Basics/quickpoll 2/next.config.ts @@ -0,0 +1,7 @@ +import type { NextConfig } from "next"; + +const nextConfig: NextConfig = { + /* config options here */ +}; + +export default nextConfig; diff --git a/Les05-NextJS-Basics/quickpoll 2/package-lock.json b/Les05-NextJS-Basics/quickpoll 2/package-lock.json new file mode 100644 index 0000000..480ab5e --- /dev/null +++ b/Les05-NextJS-Basics/quickpoll 2/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/Les05-NextJS-Basics/quickpoll 2/package.json b/Les05-NextJS-Basics/quickpoll 2/package.json new file mode 100644 index 0000000..031dcdd --- /dev/null +++ b/Les05-NextJS-Basics/quickpoll 2/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/Les05-NextJS-Basics/quickpoll 2/postcss.config.mjs b/Les05-NextJS-Basics/quickpoll 2/postcss.config.mjs new file mode 100644 index 0000000..61e3684 --- /dev/null +++ b/Les05-NextJS-Basics/quickpoll 2/postcss.config.mjs @@ -0,0 +1,7 @@ +const config = { + plugins: { + "@tailwindcss/postcss": {}, + }, +}; + +export default config; diff --git a/Les05-NextJS-Basics/quickpoll 2/public/file.svg b/Les05-NextJS-Basics/quickpoll 2/public/file.svg new file mode 100644 index 0000000..004145c --- /dev/null +++ b/Les05-NextJS-Basics/quickpoll 2/public/file.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/Les05-NextJS-Basics/quickpoll 2/public/globe.svg b/Les05-NextJS-Basics/quickpoll 2/public/globe.svg new file mode 100644 index 0000000..567f17b --- /dev/null +++ b/Les05-NextJS-Basics/quickpoll 2/public/globe.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/Les05-NextJS-Basics/quickpoll 2/public/next.svg b/Les05-NextJS-Basics/quickpoll 2/public/next.svg new file mode 100644 index 0000000..5174b28 --- /dev/null +++ b/Les05-NextJS-Basics/quickpoll 2/public/next.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/Les05-NextJS-Basics/quickpoll 2/public/vercel.svg b/Les05-NextJS-Basics/quickpoll 2/public/vercel.svg new file mode 100644 index 0000000..7705396 --- /dev/null +++ b/Les05-NextJS-Basics/quickpoll 2/public/vercel.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/Les05-NextJS-Basics/quickpoll 2/public/window.svg b/Les05-NextJS-Basics/quickpoll 2/public/window.svg new file mode 100644 index 0000000..b2b2a44 --- /dev/null +++ b/Les05-NextJS-Basics/quickpoll 2/public/window.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/Les05-NextJS-Basics/quickpoll 2/src/app/api/polls/[id]/route.ts b/Les05-NextJS-Basics/quickpoll 2/src/app/api/polls/[id]/route.ts new file mode 100644 index 0000000..155a618 --- /dev/null +++ b/Les05-NextJS-Basics/quickpoll 2/src/app/api/polls/[id]/route.ts @@ -0,0 +1,24 @@ +import { NextResponse } from "next/server"; +import { getPollById } from "@/lib/data"; + +interface RouteParams { + params: Promise<{ id: string }>; +} + +// GET /api/polls/[id] — enkele poll ophalen +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/Les05-NextJS-Basics/quickpoll 2/src/app/api/polls/[id]/vote/route.ts b/Les05-NextJS-Basics/quickpoll 2/src/app/api/polls/[id]/vote/route.ts new file mode 100644 index 0000000..770be98 --- /dev/null +++ b/Les05-NextJS-Basics/quickpoll 2/src/app/api/polls/[id]/vote/route.ts @@ -0,0 +1,37 @@ +import { NextResponse } from "next/server"; +import { votePoll } from "@/lib/data"; + +interface RouteParams { + params: Promise<{ id: string }>; +} + +interface VoteBody { + optionIndex: number; +} + +// POST /api/polls/[id]/vote — stem uitbrengen +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); +} diff --git a/Les05-NextJS-Basics/quickpoll 2/src/app/api/polls/route.ts b/Les05-NextJS-Basics/quickpoll 2/src/app/api/polls/route.ts new file mode 100644 index 0000000..0f647ef --- /dev/null +++ b/Les05-NextJS-Basics/quickpoll 2/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/Les05-NextJS-Basics/quickpoll 2/src/app/create/page.tsx b/Les05-NextJS-Basics/quickpoll 2/src/app/create/page.tsx new file mode 100644 index 0000000..a167fa6 --- /dev/null +++ b/Les05-NextJS-Basics/quickpoll 2/src/app/create/page.tsx @@ -0,0 +1,144 @@ +"use client"; + +import { useState } from "react"; +import { useRouter } from "next/navigation"; + +export default function CreatePollPage() { + const [question, setQuestion] = useState(""); + const [options, setOptions] = useState(["", ""]); + const [isSubmitting, setIsSubmitting] = useState(false); + const [error, setError] = useState(null); + const router = useRouter(); + + function addOption(): void { + if (options.length < 6) { + setOptions([...options, ""]); + } + } + + function removeOption(index: number): void { + if (options.length > 2) { + setOptions(options.filter((_, i) => i !== index)); + } + } + + function updateOption(index: number, value: string): void { + const newOptions = [...options]; + newOptions[index] = value; + setOptions(newOptions); + } + + async function handleSubmit(e: React.FormEvent): Promise { + e.preventDefault(); + setError(null); + + const filledOptions = options.filter((opt) => opt.trim() !== ""); + if (!question.trim() || filledOptions.length < 2) { + setError("Vul een vraag in en minstens 2 opties"); + return; + } + + setIsSubmitting(true); + + const response = await fetch("/api/polls", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + question: question.trim(), + options: filledOptions, + }), + }); + + if (response.ok) { + router.push("/"); + } else { + setError("Er ging iets mis bij het aanmaken van de poll"); + } + + setIsSubmitting(false); + } + + return ( +

+

+ Nieuwe Poll Aanmaken +

+ +
+
+ + ) => + setQuestion(e.target.value) + } + placeholder="Stel je vraag..." + className="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent outline-none" + required + /> +
+ +
+ +
+ {options.map((option, index) => ( +
+ ) => + updateOption(index, e.target.value) + } + placeholder={`Optie ${index + 1}`} + className="flex-1 px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent outline-none" + /> + {options.length > 2 && ( + + )} +
+ ))} +
+ + {options.length < 6 && ( + + )} +
+ + {error && ( +

+ {error} +

+ )} + + +
+
+ ); +} diff --git a/Les05-NextJS-Basics/quickpoll 2/src/app/error.tsx b/Les05-NextJS-Basics/quickpoll 2/src/app/error.tsx new file mode 100644 index 0000000..a3432e5 --- /dev/null +++ b/Les05-NextJS-Basics/quickpoll 2/src/app/error.tsx @@ -0,0 +1,24 @@ +"use client"; + +export default function Error({ + error, + reset, +}: { + error: Error; + reset: () => void; +}) { + return ( +
+

+ Er ging iets mis! +

+

{error.message}

+ +
+ ); +} diff --git a/Les05-NextJS-Basics/quickpoll 2/src/app/favicon.ico b/Les05-NextJS-Basics/quickpoll 2/src/app/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..718d6fea4835ec2d246af9800eddb7ffb276240c GIT binary patch literal 25931 zcmeHv30#a{`}aL_*G&7qml|y<+KVaDM2m#dVr!KsA!#An?kSQM(q<_dDNCpjEux83 zLb9Z^XxbDl(w>%i@8hT6>)&Gu{h#Oeyszu?xtw#Zb1mO{pgX9699l+Qppw7jXaYf~-84xW z)w4x8?=youko|}Vr~(D$UXIbiXABHh`p1?nn8Po~fxRJv}|0e(BPs|G`(TT%kKVJAdg5*Z|x0leQq0 zkdUBvb#>9F()jo|T~kx@OM8$9wzs~t2l;K=woNssA3l6|sx2r3+kdfVW@e^8e*E}v zA1y5{bRi+3Z`uD3{F7LgFJDdvm;nJilkzDku>BwXH(8ItVCXk*-lSJnR?-2UN%hJ){&rlvg`CDTj z)Bzo!3v7Ou#83zEDEFcKt(f1E0~=rqeEbTnMvWR#{+9pg%7G8y>u1OVRUSoox-ovF z2Ydma(;=YuBY(eI|04{hXzZD6_f(v~H;C~y5=DhAC{MMS>2fm~1H_t2$56pc$NH8( z5bH|<)71dV-_oCHIrzrT`2s-5w_+2CM0$95I6X8p^r!gHp+j_gd;9O<1~CEQQGS8) zS9Qh3#p&JM-G8rHekNmKVewU;pJRcTAog68KYo^dRo}(M>36U4Us zfgYWSiHZL3;lpWT=zNAW>Dh#mB!_@Lg%$ms8N-;aPqMn+C2HqZgz&9~Eu z4|Kp<`$q)Uw1R?y(~S>ePdonHxpV1#eSP1B;Ogo+-Pk}6#0GsZZ5!||ev2MGdh}_m z{DeR7?0-1^zVs&`AV6Vt;r3`I`OI_wgs*w=eO%_#7Kepl{B@xiyCANc(l zzIyd4y|c6PXWq9-|KM8(zIk8LPk(>a)zyFWjhT!$HJ$qX1vo@d25W<fvZQ2zUz5WRc(UnFMKHwe1| zWmlB1qdbiA(C0jmnV<}GfbKtmcu^2*P^O?MBLZKt|As~ge8&AAO~2K@zbXelK|4T<{|y4`raF{=72kC2Kn(L4YyenWgrPiv z@^mr$t{#X5VuIMeL!7Ab6_kG$&#&5p*Z{+?5U|TZ`B!7llpVmp@skYz&n^8QfPJzL z0G6K_OJM9x+Wu2gfN45phANGt{7=C>i34CV{Xqlx(fWpeAoj^N0Biu`w+MVcCUyU* zDZuzO0>4Z6fbu^T_arWW5n!E45vX8N=bxTVeFoep_G#VmNlQzAI_KTIc{6>c+04vr zx@W}zE5JNSU>!THJ{J=cqjz+4{L4A{Ob9$ZJ*S1?Ggg3klFp!+Y1@K+pK1DqI|_gq z5ZDXVpge8-cs!o|;K73#YXZ3AShj50wBvuq3NTOZ`M&qtjj#GOFfgExjg8Gn8>Vq5 z`85n+9|!iLCZF5$HJ$Iu($dm?8~-ofu}tEc+-pyke=3!im#6pk_Wo8IA|fJwD&~~F zc16osQ)EBo58U7XDuMexaPRjU@h8tXe%S{fA0NH3vGJFhuyyO!Uyl2^&EOpX{9As0 zWj+P>{@}jxH)8|r;2HdupP!vie{sJ28b&bo!8`D^x}TE$%zXNb^X1p@0PJ86`dZyj z%ce7*{^oo+6%&~I!8hQy-vQ7E)0t0ybH4l%KltWOo~8cO`T=157JqL(oq_rC%ea&4 z2NcTJe-HgFjNg-gZ$6!Y`SMHrlj}Etf7?r!zQTPPSv}{so2e>Fjs1{gzk~LGeesX%r(Lh6rbhSo_n)@@G-FTQy93;l#E)hgP@d_SGvyCp0~o(Y;Ee8{ zdVUDbHm5`2taPUOY^MAGOw*>=s7=Gst=D+p+2yON!0%Hk` zz5mAhyT4lS*T3LS^WSxUy86q&GnoHxzQ6vm8)VS}_zuqG?+3td68_x;etQAdu@sc6 zQJ&5|4(I?~3d-QOAODHpZ=hlSg(lBZ!JZWCtHHSj`0Wh93-Uk)_S%zsJ~aD>{`A0~ z9{AG(e|q3g5B%wYKRxiL2Y$8(4w6bzchKuloQW#e&S3n+P- z8!ds-%f;TJ1>)v)##>gd{PdS2Oc3VaR`fr=`O8QIO(6(N!A?pr5C#6fc~Ge@N%Vvu zaoAX2&(a6eWy_q&UwOhU)|P3J0Qc%OdhzW=F4D|pt0E4osw;%<%Dn58hAWD^XnZD= z>9~H(3bmLtxpF?a7su6J7M*x1By7YSUbxGi)Ot0P77`}P3{)&5Un{KD?`-e?r21!4vTTnN(4Y6Lin?UkSM z`MXCTC1@4A4~mvz%Rh2&EwY))LeoT=*`tMoqcEXI>TZU9WTP#l?uFv+@Dn~b(>xh2 z;>B?;Tz2SR&KVb>vGiBSB`@U7VIWFSo=LDSb9F{GF^DbmWAfpms8Sx9OX4CnBJca3 zlj9(x!dIjN?OG1X4l*imJNvRCk}F%!?SOfiOq5y^mZW)jFL@a|r-@d#f7 z2gmU8L3IZq0ynIws=}~m^#@&C%J6QFo~Mo4V`>v7MI-_!EBMMtb%_M&kvAaN)@ZVw z+`toz&WG#HkWDjnZE!6nk{e-oFdL^$YnbOCN}JC&{$#$O27@|Tn-skXr)2ml2~O!5 zX+gYoxhoc7qoU?C^3~&!U?kRFtnSEecWuH0B0OvLodgUAi}8p1 zrO6RSXHH}DMc$&|?D004DiOVMHV8kXCP@7NKB zgaZq^^O<7PoKEp72kby@W0Z!Y*Ay{&vfg#C&gG@YVR9g?FEocMUi1gSN$+V+ayF45{a zuDZDTN}mS|;BO%gEf}pjBfN2-gIrU#G5~cucA;dokXW89%>AyXJJI z9X4UlIWA|ZYHgbI z5?oFk@A=Ik7lrEQPDH!H+b`7_Y~aDb_qa=B2^Y&Ow41cU=4WDd40dp5(QS-WMN-=Y z9g;6_-JdNU;|6cPwf$ak*aJIcwL@1n$#l~zi{c{EW?T;DaW*E8DYq?Umtz{nJ&w-M zEMyTDrC&9K$d|kZe2#ws6)L=7K+{ zQw{XnV6UC$6-rW0emqm8wJoeZK)wJIcV?dST}Z;G0Arq{dVDu0&4kd%N!3F1*;*pW zR&qUiFzK=@44#QGw7k1`3t_d8&*kBV->O##t|tonFc2YWrL7_eqg+=+k;!F-`^b8> z#KWCE8%u4k@EprxqiV$VmmtiWxDLgnGu$Vs<8rppV5EajBXL4nyyZM$SWVm!wnCj-B!Wjqj5-5dNXukI2$$|Bu3Lrw}z65Lc=1G z^-#WuQOj$hwNGG?*CM_TO8Bg-1+qc>J7k5c51U8g?ZU5n?HYor;~JIjoWH-G>AoUP ztrWWLbRNqIjW#RT*WqZgPJXU7C)VaW5}MiijYbABmzoru6EmQ*N8cVK7a3|aOB#O& zBl8JY2WKfmj;h#Q!pN%9o@VNLv{OUL?rixHwOZuvX7{IJ{(EdPpuVFoQqIOa7giLVkBOKL@^smUA!tZ1CKRK}#SSM)iQHk)*R~?M!qkCruaS!#oIL1c z?J;U~&FfH#*98^G?i}pA{ z9Jg36t4=%6mhY(quYq*vSxptes9qy|7xSlH?G=S@>u>Ebe;|LVhs~@+06N<4CViBk zUiY$thvX;>Tby6z9Y1edAMQaiH zm^r3v#$Q#2T=X>bsY#D%s!bhs^M9PMAcHbCc0FMHV{u-dwlL;a1eJ63v5U*?Q_8JO zT#50!RD619#j_Uf))0ooADz~*9&lN!bBDRUgE>Vud-i5ck%vT=r^yD*^?Mp@Q^v+V zG#-?gKlr}Eeqifb{|So?HM&g91P8|av8hQoCmQXkd?7wIJwb z_^v8bbg`SAn{I*4bH$u(RZ6*xUhuA~hc=8czK8SHEKTzSxgbwi~9(OqJB&gwb^l4+m`k*Q;_?>Y-APi1{k zAHQ)P)G)f|AyjSgcCFps)Fh6Bca*Xznq36!pV6Az&m{O8$wGFD? zY&O*3*J0;_EqM#jh6^gMQKpXV?#1?>$ml1xvh8nSN>-?H=V;nJIwB07YX$e6vLxH( zqYwQ>qxwR(i4f)DLd)-$P>T-no_c!LsN@)8`e;W@)-Hj0>nJ-}Kla4-ZdPJzI&Mce zv)V_j;(3ERN3_@I$N<^|4Lf`B;8n+bX@bHbcZTopEmDI*Jfl)-pFDvo6svPRoo@(x z);_{lY<;);XzT`dBFpRmGrr}z5u1=pC^S-{ce6iXQlLGcItwJ^mZx{m$&DA_oEZ)B{_bYPq-HA zcH8WGoBG(aBU_j)vEy+_71T34@4dmSg!|M8Vf92Zj6WH7Q7t#OHQqWgFE3ARt+%!T z?oLovLVlnf?2c7pTc)~cc^($_8nyKwsN`RA-23ed3sdj(ys%pjjM+9JrctL;dy8a( z@en&CQmnV(()bu|Y%G1-4a(6x{aLytn$T-;(&{QIJB9vMox11U-1HpD@d(QkaJdEb zG{)+6Dos_L+O3NpWo^=gR?evp|CqEG?L&Ut#D*KLaRFOgOEK(Kq1@!EGcTfo+%A&I z=dLbB+d$u{sh?u)xP{PF8L%;YPPW53+@{>5W=Jt#wQpN;0_HYdw1{ksf_XhO4#2F= zyPx6Lx2<92L-;L5PD`zn6zwIH`Jk($?Qw({erA$^bC;q33hv!d!>%wRhj# zal^hk+WGNg;rJtb-EB(?czvOM=H7dl=vblBwAv>}%1@{}mnpUznfq1cE^sgsL0*4I zJ##!*B?=vI_OEVis5o+_IwMIRrpQyT_Sq~ZU%oY7c5JMIADzpD!Upz9h@iWg_>>~j zOLS;wp^i$-E?4<_cp?RiS%Rd?i;f*mOz=~(&3lo<=@(nR!_Rqiprh@weZlL!t#NCc zO!QTcInq|%#>OVgobj{~ixEUec`E25zJ~*DofsQdzIa@5^nOXj2T;8O`l--(QyU^$t?TGY^7#&FQ+2SS3B#qK*k3`ye?8jUYSajE5iBbJls75CCc(m3dk{t?- zopcER9{Z?TC)mk~gpi^kbbu>b-+a{m#8-y2^p$ka4n60w;Sc2}HMf<8JUvhCL0B&Btk)T`ctE$*qNW8L$`7!r^9T+>=<=2qaq-;ll2{`{Rg zc5a0ZUI$oG&j-qVOuKa=*v4aY#IsoM+1|c4Z)<}lEDvy;5huB@1RJPquU2U*U-;gu z=En2m+qjBzR#DEJDO`WU)hdd{Vj%^0V*KoyZ|5lzV87&g_j~NCjwv0uQVqXOb*QrQ zy|Qn`hxx(58c70$E;L(X0uZZ72M1!6oeg)(cdKO ze0gDaTz+ohR-#d)NbAH4x{I(21yjwvBQfmpLu$)|m{XolbgF!pmsqJ#D}(ylp6uC> z{bqtcI#hT#HW=wl7>p!38sKsJ`r8}lt-q%Keqy%u(xk=yiIJiUw6|5IvkS+#?JTBl z8H5(Q?l#wzazujH!8o>1xtn8#_w+397*_cy8!pQGP%K(Ga3pAjsaTbbXJlQF_+m+-UpUUent@xM zg%jqLUExj~o^vQ3Gl*>wh=_gOr2*|U64_iXb+-111aH}$TjeajM+I20xw(((>fej-@CIz4S1pi$(#}P7`4({6QS2CaQS4NPENDp>sAqD z$bH4KGzXGffkJ7R>V>)>tC)uax{UsN*dbeNC*v}#8Y#OWYwL4t$ePR?VTyIs!wea+ z5Urmc)X|^`MG~*dS6pGSbU+gPJoq*^a=_>$n4|P^w$sMBBy@f*Z^Jg6?n5?oId6f{ z$LW4M|4m502z0t7g<#Bx%X;9<=)smFolV&(V^(7Cv2-sxbxopQ!)*#ZRhTBpx1)Fc zNm1T%bONzv6@#|dz(w02AH8OXe>kQ#1FMCzO}2J_mST)+ExmBr9cva-@?;wnmWMOk z{3_~EX_xadgJGv&H@zK_8{(x84`}+c?oSBX*Ge3VdfTt&F}yCpFP?CpW+BE^cWY0^ zb&uBN!Ja3UzYHK-CTyA5=L zEMW{l3Usky#ly=7px648W31UNV@K)&Ub&zP1c7%)`{);I4b0Q<)B}3;NMG2JH=X$U zfIW4)4n9ZM`-yRj67I)YSLDK)qfUJ_ij}a#aZN~9EXrh8eZY2&=uY%2N0UFF7<~%M zsB8=erOWZ>Ct_#^tHZ|*q`H;A)5;ycw*IcmVxi8_0Xk}aJA^ath+E;xg!x+As(M#0=)3!NJR6H&9+zd#iP(m0PIW8$ z1Y^VX`>jm`W!=WpF*{ioM?C9`yOR>@0q=u7o>BP-eSHqCgMDj!2anwH?s%i2p+Q7D zzszIf5XJpE)IG4;d_(La-xenmF(tgAxK`Y4sQ}BSJEPs6N_U2vI{8=0C_F?@7<(G; zo$~G=8p+076G;`}>{MQ>t>7cm=zGtfbdDXm6||jUU|?X?CaE?(<6bKDYKeHlz}DA8 zXT={X=yp_R;HfJ9h%?eWvQ!dRgz&Su*JfNt!Wu>|XfU&68iRikRrHRW|ZxzRR^`eIGt zIeiDgVS>IeExKVRWW8-=A=yA`}`)ZkWBrZD`hpWIxBGkh&f#ijr449~m`j6{4jiJ*C!oVA8ZC?$1RM#K(_b zL9TW)kN*Y4%^-qPpMP7d4)o?Nk#>aoYHT(*g)qmRUb?**F@pnNiy6Fv9rEiUqD(^O zzyS?nBrX63BTRYduaG(0VVG2yJRe%o&rVrLjbxTaAFTd8s;<<@Qs>u(<193R8>}2_ zuwp{7;H2a*X7_jryzriZXMg?bTuegABb^87@SsKkr2)0Gyiax8KQWstw^v#ix45EVrcEhr>!NMhprl$InQMzjSFH54x5k9qHc`@9uKQzvL4ihcq{^B zPrVR=o_ic%Y>6&rMN)hTZsI7I<3&`#(nl+3y3ys9A~&^=4?PL&nd8)`OfG#n zwAMN$1&>K++c{^|7<4P=2y(B{jJsQ0a#U;HTo4ZmWZYvI{+s;Td{Yzem%0*k#)vjpB zia;J&>}ICate44SFYY3vEelqStQWFihx%^vQ@Do(sOy7yR2@WNv7Y9I^yL=nZr3mb zXKV5t@=?-Sk|b{XMhA7ZGB@2hqsx}4xwCW!in#C zI@}scZlr3-NFJ@NFaJlhyfcw{k^vvtGl`N9xSo**rDW4S}i zM9{fMPWo%4wYDG~BZ18BD+}h|GQKc-g^{++3MY>}W_uq7jGHx{mwE9fZiPCoxN$+7 zrODGGJrOkcPQUB(FD5aoS4g~7#6NR^ma7-!>mHuJfY5kTe6PpNNKC9GGRiu^L31uG z$7v`*JknQHsYB!Tm_W{a32TM099djW%5e+j0Ve_ct}IM>XLF1Ap+YvcrLV=|CKo6S zb+9Nl3_YdKP6%Cxy@6TxZ>;4&nTneadr z_ES90ydCev)LV!dN=#(*f}|ZORFdvkYBni^aLbUk>BajeWIOcmHP#8S)*2U~QKI%S zyrLmtPqb&TphJ;>yAxri#;{uyk`JJqODDw%(Z=2`1uc}br^V%>j!gS)D*q*f_-qf8&D;W1dJgQMlaH5er zN2U<%Smb7==vE}dDI8K7cKz!vs^73o9f>2sgiTzWcwY|BMYHH5%Vn7#kiw&eItCqa zIkR2~Q}>X=Ar8W|^Ms41Fm8o6IB2_j60eOeBB1Br!boW7JnoeX6Gs)?7rW0^5psc- zjS16yb>dFn>KPOF;imD}e!enuIniFzv}n$m2#gCCv4jM#ArwlzZ$7@9&XkFxZ4n!V zj3dyiwW4Ki2QG{@i>yuZXQizw_OkZI^-3otXC{!(lUpJF33gI60ak;Uqitp74|B6I zgg{b=Iz}WkhCGj1M=hu4#Aw173YxIVbISaoc z-nLZC*6Tgivd5V`K%GxhBsp@SUU60-rfc$=wb>zdJzXS&-5(NRRodFk;Kxk!S(O(a0e7oY=E( zAyS;Ow?6Q&XA+cnkCb{28_1N8H#?J!*$MmIwLq^*T_9-z^&UE@A(z9oGYtFy6EZef LrJugUA?W`A8`#=m literal 0 HcmV?d00001 diff --git a/Les05-NextJS-Basics/quickpoll 2/src/app/globals.css b/Les05-NextJS-Basics/quickpoll 2/src/app/globals.css new file mode 100644 index 0000000..f1d8c73 --- /dev/null +++ b/Les05-NextJS-Basics/quickpoll 2/src/app/globals.css @@ -0,0 +1 @@ +@import "tailwindcss"; diff --git a/Les05-NextJS-Basics/quickpoll 2/src/app/layout.tsx b/Les05-NextJS-Basics/quickpoll 2/src/app/layout.tsx new file mode 100644 index 0000000..494f806 --- /dev/null +++ b/Les05-NextJS-Basics/quickpoll 2/src/app/layout.tsx @@ -0,0 +1,46 @@ +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 ( + + + +
{children}
+
+ © 2025 QuickPoll — NOVI Hogeschool Les 5 +
+ + + ); +} diff --git a/Les05-NextJS-Basics/quickpoll 2/src/app/loading.tsx b/Les05-NextJS-Basics/quickpoll 2/src/app/loading.tsx new file mode 100644 index 0000000..a1d55a7 --- /dev/null +++ b/Les05-NextJS-Basics/quickpoll 2/src/app/loading.tsx @@ -0,0 +1,24 @@ +export default function Loading() { + return ( +
+
+
+
+
+ {[1, 2, 3].map((i) => ( +
+
+
+
+
+
+
+
+
+ ))} +
+ ); +} diff --git a/Les05-NextJS-Basics/quickpoll 2/src/app/not-found.tsx b/Les05-NextJS-Basics/quickpoll 2/src/app/not-found.tsx new file mode 100644 index 0000000..a5b948a --- /dev/null +++ b/Les05-NextJS-Basics/quickpoll 2/src/app/not-found.tsx @@ -0,0 +1,18 @@ +import Link from "next/link"; + +export default function NotFound() { + return ( +
+

404

+

+ Deze pagina bestaat niet (meer). +

+ + Terug naar home + +
+ ); +} diff --git a/Les05-NextJS-Basics/quickpoll 2/src/app/page.tsx b/Les05-NextJS-Basics/quickpoll 2/src/app/page.tsx new file mode 100644 index 0000000..225a48b --- /dev/null +++ b/Les05-NextJS-Basics/quickpoll 2/src/app/page.tsx @@ -0,0 +1,57 @@ +import Link from "next/link"; +import { getPolls } from "@/lib/data"; +import type { Poll } from "@/types"; + +export const dynamic = "force-dynamic"; + +export default function HomePage() { + const polls: Poll[] = getPolls(); + + const totalVotes = (poll: Poll): number => + poll.votes.reduce((sum, v) => sum + v, 0); + + return ( +
+

Actieve Polls

+

Klik op een poll om te stemmen

+ +
+ {polls.map((poll) => ( + +

+ {poll.question} +

+
+ {poll.options.length} opties + · + {totalVotes(poll)} stemmen +
+
+ {poll.options.map((option, index) => ( + + {option} + + ))} +
+ + ))} +
+ + {polls.length === 0 && ( +
+

Nog geen polls

+ + Maak de eerste! + +
+ )} +
+ ); +} diff --git a/Les05-NextJS-Basics/quickpoll 2/src/app/poll/[id]/not-found.tsx b/Les05-NextJS-Basics/quickpoll 2/src/app/poll/[id]/not-found.tsx new file mode 100644 index 0000000..bfce1cf --- /dev/null +++ b/Les05-NextJS-Basics/quickpoll 2/src/app/poll/[id]/not-found.tsx @@ -0,0 +1,20 @@ +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/Les05-NextJS-Basics/quickpoll 2/src/app/poll/[id]/page.tsx b/Les05-NextJS-Basics/quickpoll 2/src/app/poll/[id]/page.tsx new file mode 100644 index 0000000..bd3aa82 --- /dev/null +++ b/Les05-NextJS-Basics/quickpoll 2/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 }>; +} + +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} +

+ +
+ ); +} diff --git a/Les05-NextJS-Basics/quickpoll 2/src/components/VoteForm.tsx b/Les05-NextJS-Basics/quickpoll 2/src/components/VoteForm.tsx new file mode 100644 index 0000000..457fe5d --- /dev/null +++ b/Les05-NextJS-Basics/quickpoll 2/src/components/VoteForm.tsx @@ -0,0 +1,128 @@ +"use client"; + +import { useState } from "react"; +import { useRouter } from "next/navigation"; +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 router = useRouter(); + + const totalVotes: number = currentPoll.votes.reduce( + (sum, v) => sum + v, + 0 + ); + + 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); + } + + function getPercentage(votes: number): number { + if (totalVotes === 0) return 0; + return Math.round((votes / totalVotes) * 100); + } + + 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 +

+ +
+ )} +
+ ); +} diff --git a/Les05-NextJS-Basics/quickpoll 2/src/lib/data.ts b/Les05-NextJS-Basics/quickpoll 2/src/lib/data.ts new file mode 100644 index 0000000..3e36f41 --- /dev/null +++ b/Les05-NextJS-Basics/quickpoll 2/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/Les05-NextJS-Basics/quickpoll 2/src/middleware.ts b/Les05-NextJS-Basics/quickpoll 2/src/middleware.ts new file mode 100644 index 0000000..76bbfab --- /dev/null +++ b/Les05-NextJS-Basics/quickpoll 2/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/Les05-NextJS-Basics/quickpoll 2/src/types/index.ts b/Les05-NextJS-Basics/quickpoll 2/src/types/index.ts new file mode 100644 index 0000000..73f152f --- /dev/null +++ b/Les05-NextJS-Basics/quickpoll 2/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/Les05-NextJS-Basics/quickpoll 2/tsconfig.json b/Les05-NextJS-Basics/quickpoll 2/tsconfig.json new file mode 100644 index 0000000..cf9c65d --- /dev/null +++ b/Les05-NextJS-Basics/quickpoll 2/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/Les05-NextJS-Basics/quickpoll-starter/.cursorrules b/Les05-NextJS-Basics/quickpoll-starter/.cursorrules new file mode 100644 index 0000000..7de9420 --- /dev/null +++ b/Les05-NextJS-Basics/quickpoll-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/Les05-NextJS-Basics/quickpoll-starter/.gitignore b/Les05-NextJS-Basics/quickpoll-starter/.gitignore new file mode 100644 index 0000000..5ef6a52 --- /dev/null +++ b/Les05-NextJS-Basics/quickpoll-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/Les05-NextJS-Basics/quickpoll-starter/README.md b/Les05-NextJS-Basics/quickpoll-starter/README.md new file mode 100644 index 0000000..e215bc4 --- /dev/null +++ b/Les05-NextJS-Basics/quickpoll-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/Les05-NextJS-Basics/quickpoll-starter/next.config.ts b/Les05-NextJS-Basics/quickpoll-starter/next.config.ts new file mode 100644 index 0000000..e9ffa30 --- /dev/null +++ b/Les05-NextJS-Basics/quickpoll-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/Les05-NextJS-Basics/quickpoll-starter/package-lock.json b/Les05-NextJS-Basics/quickpoll-starter/package-lock.json new file mode 100644 index 0000000..480ab5e --- /dev/null +++ b/Les05-NextJS-Basics/quickpoll-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/Les05-NextJS-Basics/quickpoll-starter/package.json b/Les05-NextJS-Basics/quickpoll-starter/package.json new file mode 100644 index 0000000..031dcdd --- /dev/null +++ b/Les05-NextJS-Basics/quickpoll-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/Les05-NextJS-Basics/quickpoll-starter/postcss.config.mjs b/Les05-NextJS-Basics/quickpoll-starter/postcss.config.mjs new file mode 100644 index 0000000..61e3684 --- /dev/null +++ b/Les05-NextJS-Basics/quickpoll-starter/postcss.config.mjs @@ -0,0 +1,7 @@ +const config = { + plugins: { + "@tailwindcss/postcss": {}, + }, +}; + +export default config; diff --git a/Les05-NextJS-Basics/quickpoll-starter/public/file.svg b/Les05-NextJS-Basics/quickpoll-starter/public/file.svg new file mode 100644 index 0000000..004145c --- /dev/null +++ b/Les05-NextJS-Basics/quickpoll-starter/public/file.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/Les05-NextJS-Basics/quickpoll-starter/public/globe.svg b/Les05-NextJS-Basics/quickpoll-starter/public/globe.svg new file mode 100644 index 0000000..567f17b --- /dev/null +++ b/Les05-NextJS-Basics/quickpoll-starter/public/globe.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/Les05-NextJS-Basics/quickpoll-starter/public/next.svg b/Les05-NextJS-Basics/quickpoll-starter/public/next.svg new file mode 100644 index 0000000..5174b28 --- /dev/null +++ b/Les05-NextJS-Basics/quickpoll-starter/public/next.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/Les05-NextJS-Basics/quickpoll-starter/public/vercel.svg b/Les05-NextJS-Basics/quickpoll-starter/public/vercel.svg new file mode 100644 index 0000000..7705396 --- /dev/null +++ b/Les05-NextJS-Basics/quickpoll-starter/public/vercel.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/Les05-NextJS-Basics/quickpoll-starter/public/window.svg b/Les05-NextJS-Basics/quickpoll-starter/public/window.svg new file mode 100644 index 0000000..b2b2a44 --- /dev/null +++ b/Les05-NextJS-Basics/quickpoll-starter/public/window.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/Les05-NextJS-Basics/quickpoll-starter/src/app/api/polls/[id]/route.ts b/Les05-NextJS-Basics/quickpoll-starter/src/app/api/polls/[id]/route.ts new file mode 100644 index 0000000..23d2baa --- /dev/null +++ b/Les05-NextJS-Basics/quickpoll-starter/src/app/api/polls/[id]/route.ts @@ -0,0 +1,24 @@ +import { NextResponse } from "next/server"; +import { getPollById } from "@/lib/data"; + +interface RouteParams { + params: Promise<{ id: string }>; +} + +// STAP 3: GET /api/polls/[id] — enkele poll ophalen +// +// Wat moet je doen? +// 1. Haal het id op uit params (let op: params is een Promise!) +// 2. Zoek de poll met getPollById(id) +// 3. Als de poll niet bestaat, return een 404 JSON response +// 4. Als de poll wel bestaat, return de poll als JSON +// +// Hint: kijk naar /api/polls/route.ts voor een voorbeeld van NextResponse.json() + +export async function GET( + request: Request, + { params }: RouteParams +): Promise { + // Jouw code hier... + return NextResponse.json({ error: "Nog niet geimplementeerd" }, { status: 501 }); +} diff --git a/Les05-NextJS-Basics/quickpoll-starter/src/app/api/polls/[id]/vote/route.ts b/Les05-NextJS-Basics/quickpoll-starter/src/app/api/polls/[id]/vote/route.ts new file mode 100644 index 0000000..fc10810 --- /dev/null +++ b/Les05-NextJS-Basics/quickpoll-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 +// 2. Lees de request body (request.json()) 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 { + // Jouw code hier... + return NextResponse.json({ error: "Nog niet geimplementeerd" }, { status: 501 }); +} diff --git a/Les05-NextJS-Basics/quickpoll-starter/src/app/api/polls/route.ts b/Les05-NextJS-Basics/quickpoll-starter/src/app/api/polls/route.ts new file mode 100644 index 0000000..0f647ef --- /dev/null +++ b/Les05-NextJS-Basics/quickpoll-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/Les05-NextJS-Basics/quickpoll-starter/src/app/create/page.tsx b/Les05-NextJS-Basics/quickpoll-starter/src/app/create/page.tsx new file mode 100644 index 0000000..f27f872 --- /dev/null +++ b/Les05-NextJS-Basics/quickpoll-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/Les05-NextJS-Basics/quickpoll-starter/src/app/error.tsx b/Les05-NextJS-Basics/quickpoll-starter/src/app/error.tsx new file mode 100644 index 0000000..8d17fc4 --- /dev/null +++ b/Les05-NextJS-Basics/quickpoll-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/Les05-NextJS-Basics/quickpoll-starter/src/app/favicon.ico b/Les05-NextJS-Basics/quickpoll-starter/src/app/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..718d6fea4835ec2d246af9800eddb7ffb276240c GIT binary patch literal 25931 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 LrJugUA?W`A8`#=m literal 0 HcmV?d00001 diff --git a/Les05-NextJS-Basics/quickpoll-starter/src/app/globals.css b/Les05-NextJS-Basics/quickpoll-starter/src/app/globals.css new file mode 100644 index 0000000..f1d8c73 --- /dev/null +++ b/Les05-NextJS-Basics/quickpoll-starter/src/app/globals.css @@ -0,0 +1 @@ +@import "tailwindcss"; diff --git a/Les05-NextJS-Basics/quickpoll-starter/src/app/layout.tsx b/Les05-NextJS-Basics/quickpoll-starter/src/app/layout.tsx new file mode 100644 index 0000000..3252dda --- /dev/null +++ b/Les05-NextJS-Basics/quickpoll-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/Les05-NextJS-Basics/quickpoll-starter/src/app/loading.tsx b/Les05-NextJS-Basics/quickpoll-starter/src/app/loading.tsx new file mode 100644 index 0000000..b70a23c --- /dev/null +++ b/Les05-NextJS-Basics/quickpoll-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/Les05-NextJS-Basics/quickpoll-starter/src/app/not-found.tsx b/Les05-NextJS-Basics/quickpoll-starter/src/app/not-found.tsx new file mode 100644 index 0000000..bd09789 --- /dev/null +++ b/Les05-NextJS-Basics/quickpoll-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/Les05-NextJS-Basics/quickpoll-starter/src/app/page.tsx b/Les05-NextJS-Basics/quickpoll-starter/src/app/page.tsx new file mode 100644 index 0000000..9ddc506 --- /dev/null +++ b/Les05-NextJS-Basics/quickpoll-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/Les05-NextJS-Basics/quickpoll-starter/src/app/poll/[id]/not-found.tsx b/Les05-NextJS-Basics/quickpoll-starter/src/app/poll/[id]/not-found.tsx new file mode 100644 index 0000000..35e5934 --- /dev/null +++ b/Les05-NextJS-Basics/quickpoll-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/Les05-NextJS-Basics/quickpoll-starter/src/app/poll/[id]/page.tsx b/Les05-NextJS-Basics/quickpoll-starter/src/app/poll/[id]/page.tsx new file mode 100644 index 0000000..fb909e7 --- /dev/null +++ b/Les05-NextJS-Basics/quickpoll-starter/src/app/poll/[id]/page.tsx @@ -0,0 +1,41 @@ +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. +// Als de poll niet bestaat, return "Poll niet gevonden". + +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 +// 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) { + // Jouw code hier... + return ( + <div className="max-w-2xl mx-auto"> + <p>Implementeer deze pagina (zie stap 5 in de opdracht)</p> + </div> + ); +} diff --git a/Les05-NextJS-Basics/quickpoll-starter/src/components/VoteForm.tsx b/Les05-NextJS-Basics/quickpoll-starter/src/components/VoteForm.tsx new file mode 100644 index 0000000..c5e0a1e --- /dev/null +++ b/Les05-NextJS-Basics/quickpoll-starter/src/components/VoteForm.tsx @@ -0,0 +1,89 @@ +"use client"; + +import { useState } from "react"; +import { useRouter } from "next/navigation"; +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) +// - isSubmitting: 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 +// 2. Highlight de geselecteerde optie +// 3. Een "Stem!" knop die een POST doet naar /api/polls/{id}/vote +// 4. Na het stemmen: toon de resultaten met percentages +// +// De fetch call voor stemmen: +// 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); +// } + +export default function VoteForm({ poll }: VoteFormProps) { + const [selectedOption, setSelectedOption] = useState<number | null>(null); + const [hasVoted, setHasVoted] = useState<boolean>(false); + const [isSubmitting, setIsSubmitting] = useState<boolean>(false); + const [currentPoll, setCurrentPoll] = useState<Poll>(poll); + const router = useRouter(); + + const totalVotes: number = currentPoll.votes.reduce( + (sum, v) => sum + v, + 0 + ); + + function getPercentage(votes: number): number { + if (totalVotes === 0) return 0; + return Math.round((votes / totalVotes) * 100); + } + + async function handleVote(): Promise<void> { + if (selectedOption === null || isSubmitting) return; + setIsSubmitting(true); + + // STAP 6: Doe hier de fetch call naar de vote API + // Zie de beschrijving hierboven voor de code + + setIsSubmitting(false); + } + + return ( + <div className="space-y-3"> + {/* + STAP 6: Bouw hier de voting interface + + ALS de gebruiker nog NIET gestemd heeft: + - Toon elke optie als een klikbare button + - Highlight de geselecteerde optie (purple border) + - Toon een radio-achtige indicator (gevuld/leeg rondje) + - Toon een "Stem!" knop onderaan + + ALS de gebruiker WEL gestemd heeft: + - Toon elke optie met een percentage balk + - Toon het percentage en aantal stemmen + - Toon "Bedankt voor je stem!" + - Toon een "Terug" link naar / + */} + <p className="text-gray-400 italic"> + Bouw hier de voting interface (zie stap 6) + </p> + </div> + ); +} diff --git a/Les05-NextJS-Basics/quickpoll-starter/src/lib/data.ts b/Les05-NextJS-Basics/quickpoll-starter/src/lib/data.ts new file mode 100644 index 0000000..3e36f41 --- /dev/null +++ b/Les05-NextJS-Basics/quickpoll-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/Les05-NextJS-Basics/quickpoll-starter/src/middleware.ts b/Les05-NextJS-Basics/quickpoll-starter/src/middleware.ts new file mode 100644 index 0000000..76bbfab --- /dev/null +++ b/Les05-NextJS-Basics/quickpoll-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/Les05-NextJS-Basics/quickpoll-starter/src/types/index.ts b/Les05-NextJS-Basics/quickpoll-starter/src/types/index.ts new file mode 100644 index 0000000..73f152f --- /dev/null +++ b/Les05-NextJS-Basics/quickpoll-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/Les05-NextJS-Basics/quickpoll-starter/tsconfig.json b/Les05-NextJS-Basics/quickpoll-starter/tsconfig.json new file mode 100644 index 0000000..cf9c65d --- /dev/null +++ b/Les05-NextJS-Basics/quickpoll-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/Les05-NextJS-Basics/quickpoll/.cursorrules b/Les05-NextJS-Basics/quickpoll/.cursorrules new file mode 100644 index 0000000..7de9420 --- /dev/null +++ b/Les05-NextJS-Basics/quickpoll/.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/Les05-NextJS-Basics/quickpoll/.gitignore b/Les05-NextJS-Basics/quickpoll/.gitignore new file mode 100644 index 0000000..5ef6a52 --- /dev/null +++ b/Les05-NextJS-Basics/quickpoll/.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/Les05-NextJS-Basics/quickpoll/README.md b/Les05-NextJS-Basics/quickpoll/README.md new file mode 100644 index 0000000..e215bc4 --- /dev/null +++ b/Les05-NextJS-Basics/quickpoll/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/Les05-NextJS-Basics/quickpoll/next.config.ts b/Les05-NextJS-Basics/quickpoll/next.config.ts new file mode 100644 index 0000000..e9ffa30 --- /dev/null +++ b/Les05-NextJS-Basics/quickpoll/next.config.ts @@ -0,0 +1,7 @@ +import type { NextConfig } from "next"; + +const nextConfig: NextConfig = { + /* config options here */ +}; + +export default nextConfig; diff --git a/Les05-NextJS-Basics/quickpoll/package-lock.json b/Les05-NextJS-Basics/quickpoll/package-lock.json new file mode 100644 index 0000000..480ab5e --- /dev/null +++ b/Les05-NextJS-Basics/quickpoll/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/Les05-NextJS-Basics/quickpoll/package.json b/Les05-NextJS-Basics/quickpoll/package.json new file mode 100644 index 0000000..031dcdd --- /dev/null +++ b/Les05-NextJS-Basics/quickpoll/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/Les05-NextJS-Basics/quickpoll/postcss.config.mjs b/Les05-NextJS-Basics/quickpoll/postcss.config.mjs new file mode 100644 index 0000000..61e3684 --- /dev/null +++ b/Les05-NextJS-Basics/quickpoll/postcss.config.mjs @@ -0,0 +1,7 @@ +const config = { + plugins: { + "@tailwindcss/postcss": {}, + }, +}; + +export default config; diff --git a/Les05-NextJS-Basics/quickpoll/public/file.svg b/Les05-NextJS-Basics/quickpoll/public/file.svg new file mode 100644 index 0000000..004145c --- /dev/null +++ b/Les05-NextJS-Basics/quickpoll/public/file.svg @@ -0,0 +1 @@ +<svg fill="none" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"><path d="M14.5 13.5V5.41a1 1 0 0 0-.3-.7L9.8.29A1 1 0 0 0 9.08 0H1.5v13.5A2.5 2.5 0 0 0 4 16h8a2.5 2.5 0 0 0 2.5-2.5m-1.5 0v-7H8v-5H3v12a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1M9.5 5V2.12L12.38 5zM5.13 5h-.62v1.25h2.12V5zm-.62 3h7.12v1.25H4.5zm.62 3h-.62v1.25h7.12V11z" clip-rule="evenodd" fill="#666" fill-rule="evenodd"/></svg> \ No newline at end of file diff --git a/Les05-NextJS-Basics/quickpoll/public/globe.svg b/Les05-NextJS-Basics/quickpoll/public/globe.svg new file mode 100644 index 0000000..567f17b --- /dev/null +++ b/Les05-NextJS-Basics/quickpoll/public/globe.svg @@ -0,0 +1 @@ +<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><g clip-path="url(#a)"><path fill-rule="evenodd" clip-rule="evenodd" d="M10.27 14.1a6.5 6.5 0 0 0 3.67-3.45q-1.24.21-2.7.34-.31 1.83-.97 3.1M8 16A8 8 0 1 0 8 0a8 8 0 0 0 0 16m.48-1.52a7 7 0 0 1-.96 0H7.5a4 4 0 0 1-.84-1.32q-.38-.89-.63-2.08a40 40 0 0 0 3.92 0q-.25 1.2-.63 2.08a4 4 0 0 1-.84 1.31zm2.94-4.76q1.66-.15 2.95-.43a7 7 0 0 0 0-2.58q-1.3-.27-2.95-.43a18 18 0 0 1 0 3.44m-1.27-3.54a17 17 0 0 1 0 3.64 39 39 0 0 1-4.3 0 17 17 0 0 1 0-3.64 39 39 0 0 1 4.3 0m1.1-1.17q1.45.13 2.69.34a6.5 6.5 0 0 0-3.67-3.44q.65 1.26.98 3.1M8.48 1.5l.01.02q.41.37.84 1.31.38.89.63 2.08a40 40 0 0 0-3.92 0q.25-1.2.63-2.08a4 4 0 0 1 .85-1.32 7 7 0 0 1 .96 0m-2.75.4a6.5 6.5 0 0 0-3.67 3.44 29 29 0 0 1 2.7-.34q.31-1.83.97-3.1M4.58 6.28q-1.66.16-2.95.43a7 7 0 0 0 0 2.58q1.3.27 2.95.43a18 18 0 0 1 0-3.44m.17 4.71q-1.45-.12-2.69-.34a6.5 6.5 0 0 0 3.67 3.44q-.65-1.27-.98-3.1" fill="#666"/></g><defs><clipPath id="a"><path fill="#fff" d="M0 0h16v16H0z"/></clipPath></defs></svg> \ No newline at end of file diff --git a/Les05-NextJS-Basics/quickpoll/public/next.svg b/Les05-NextJS-Basics/quickpoll/public/next.svg new file mode 100644 index 0000000..5174b28 --- /dev/null +++ b/Les05-NextJS-Basics/quickpoll/public/next.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 394 80"><path fill="#000" d="M262 0h68.5v12.7h-27.2v66.6h-13.6V12.7H262V0ZM149 0v12.7H94v20.4h44.3v12.6H94v21h55v12.6H80.5V0h68.7zm34.3 0h-17.8l63.8 79.4h17.9l-32-39.7 32-39.6h-17.9l-23 28.6-23-28.6zm18.3 56.7-9-11-27.1 33.7h17.8l18.3-22.7z"/><path fill="#000" d="M81 79.3 17 0H0v79.3h13.6V17l50.2 62.3H81Zm252.6-.4c-1 0-1.8-.4-2.5-1s-1.1-1.6-1.1-2.6.3-1.8 1-2.5 1.6-1 2.6-1 1.8.3 2.5 1a3.4 3.4 0 0 1 .6 4.3 3.7 3.7 0 0 1-3 1.8zm23.2-33.5h6v23.3c0 2.1-.4 4-1.3 5.5a9.1 9.1 0 0 1-3.8 3.5c-1.6.8-3.5 1.3-5.7 1.3-2 0-3.7-.4-5.3-1s-2.8-1.8-3.7-3.2c-.9-1.3-1.4-3-1.4-5h6c.1.8.3 1.6.7 2.2s1 1.2 1.6 1.5c.7.4 1.5.5 2.4.5 1 0 1.8-.2 2.4-.6a4 4 0 0 0 1.6-1.8c.3-.8.5-1.8.5-3V45.5zm30.9 9.1a4.4 4.4 0 0 0-2-3.3 7.5 7.5 0 0 0-4.3-1.1c-1.3 0-2.4.2-3.3.5-.9.4-1.6 1-2 1.6a3.5 3.5 0 0 0-.3 4c.3.5.7.9 1.3 1.2l1.8 1 2 .5 3.2.8c1.3.3 2.5.7 3.7 1.2a13 13 0 0 1 3.2 1.8 8.1 8.1 0 0 1 3 6.5c0 2-.5 3.7-1.5 5.1a10 10 0 0 1-4.4 3.5c-1.8.8-4.1 1.2-6.8 1.2-2.6 0-4.9-.4-6.8-1.2-2-.8-3.4-2-4.5-3.5a10 10 0 0 1-1.7-5.6h6a5 5 0 0 0 3.5 4.6c1 .4 2.2.6 3.4.6 1.3 0 2.5-.2 3.5-.6 1-.4 1.8-1 2.4-1.7a4 4 0 0 0 .8-2.4c0-.9-.2-1.6-.7-2.2a11 11 0 0 0-2.1-1.4l-3.2-1-3.8-1c-2.8-.7-5-1.7-6.6-3.2a7.2 7.2 0 0 1-2.4-5.7 8 8 0 0 1 1.7-5 10 10 0 0 1 4.3-3.5c2-.8 4-1.2 6.4-1.2 2.3 0 4.4.4 6.2 1.2 1.8.8 3.2 2 4.3 3.4 1 1.4 1.5 3 1.5 5h-5.8z"/></svg> \ No newline at end of file diff --git a/Les05-NextJS-Basics/quickpoll/public/vercel.svg b/Les05-NextJS-Basics/quickpoll/public/vercel.svg new file mode 100644 index 0000000..7705396 --- /dev/null +++ b/Les05-NextJS-Basics/quickpoll/public/vercel.svg @@ -0,0 +1 @@ +<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1155 1000"><path d="m577.3 0 577.4 1000H0z" fill="#fff"/></svg> \ No newline at end of file diff --git a/Les05-NextJS-Basics/quickpoll/public/window.svg b/Les05-NextJS-Basics/quickpoll/public/window.svg new file mode 100644 index 0000000..b2b2a44 --- /dev/null +++ b/Les05-NextJS-Basics/quickpoll/public/window.svg @@ -0,0 +1 @@ +<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path fill-rule="evenodd" clip-rule="evenodd" d="M1.5 2.5h13v10a1 1 0 0 1-1 1h-11a1 1 0 0 1-1-1zM0 1h16v11.5a2.5 2.5 0 0 1-2.5 2.5h-11A2.5 2.5 0 0 1 0 12.5zm3.75 4.5a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5M7 4.75a.75.75 0 1 1-1.5 0 .75.75 0 0 1 1.5 0m1.75.75a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5" fill="#666"/></svg> \ No newline at end of file diff --git a/Les05-NextJS-Basics/quickpoll/src/app/api/polls/[id]/route.ts b/Les05-NextJS-Basics/quickpoll/src/app/api/polls/[id]/route.ts new file mode 100644 index 0000000..155a618 --- /dev/null +++ b/Les05-NextJS-Basics/quickpoll/src/app/api/polls/[id]/route.ts @@ -0,0 +1,24 @@ +import { NextResponse } from "next/server"; +import { getPollById } from "@/lib/data"; + +interface RouteParams { + params: Promise<{ id: string }>; +} + +// GET /api/polls/[id] — enkele poll ophalen +export async function GET( + request: Request, + { params }: RouteParams +): Promise<NextResponse> { + 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/Les05-NextJS-Basics/quickpoll/src/app/api/polls/[id]/vote/route.ts b/Les05-NextJS-Basics/quickpoll/src/app/api/polls/[id]/vote/route.ts new file mode 100644 index 0000000..770be98 --- /dev/null +++ b/Les05-NextJS-Basics/quickpoll/src/app/api/polls/[id]/vote/route.ts @@ -0,0 +1,37 @@ +import { NextResponse } from "next/server"; +import { votePoll } from "@/lib/data"; + +interface RouteParams { + params: Promise<{ id: string }>; +} + +interface VoteBody { + optionIndex: number; +} + +// POST /api/polls/[id]/vote — stem uitbrengen +export async function POST( + request: Request, + { params }: RouteParams +): Promise<NextResponse> { + const { id } = await params; + const body: VoteBody = await request.json(); + + if (typeof body.optionIndex !== "number") { + return NextResponse.json( + { error: "optionIndex is verplicht" }, + { status: 400 } + ); + } + + const updatedPoll = votePoll(id, body.optionIndex); + + if (!updatedPoll) { + return NextResponse.json( + { error: "Poll niet gevonden of ongeldige optie" }, + { status: 404 } + ); + } + + return NextResponse.json(updatedPoll); +} diff --git a/Les05-NextJS-Basics/quickpoll/src/app/api/polls/route.ts b/Les05-NextJS-Basics/quickpoll/src/app/api/polls/route.ts new file mode 100644 index 0000000..0f647ef --- /dev/null +++ b/Les05-NextJS-Basics/quickpoll/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<NextResponse<Poll[]>> { + const polls = getPolls(); + return NextResponse.json(polls); +} + +// POST /api/polls — nieuwe poll aanmaken +export async function POST(request: Request): Promise<NextResponse> { + 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/Les05-NextJS-Basics/quickpoll/src/app/create/page.tsx b/Les05-NextJS-Basics/quickpoll/src/app/create/page.tsx new file mode 100644 index 0000000..a167fa6 --- /dev/null +++ b/Les05-NextJS-Basics/quickpoll/src/app/create/page.tsx @@ -0,0 +1,144 @@ +"use client"; + +import { useState } from "react"; +import { useRouter } from "next/navigation"; + +export default function CreatePollPage() { + const [question, setQuestion] = useState<string>(""); + const [options, setOptions] = useState<string[]>(["", ""]); + const [isSubmitting, setIsSubmitting] = useState<boolean>(false); + const [error, setError] = useState<string | null>(null); + const router = useRouter(); + + function addOption(): void { + if (options.length < 6) { + setOptions([...options, ""]); + } + } + + function removeOption(index: number): void { + if (options.length > 2) { + setOptions(options.filter((_, i) => i !== index)); + } + } + + function updateOption(index: number, value: string): void { + const newOptions = [...options]; + newOptions[index] = value; + setOptions(newOptions); + } + + async function handleSubmit(e: React.FormEvent<HTMLFormElement>): Promise<void> { + e.preventDefault(); + setError(null); + + const filledOptions = options.filter((opt) => opt.trim() !== ""); + if (!question.trim() || filledOptions.length < 2) { + setError("Vul een vraag in en minstens 2 opties"); + return; + } + + setIsSubmitting(true); + + const response = await fetch("/api/polls", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + question: question.trim(), + options: filledOptions, + }), + }); + + if (response.ok) { + router.push("/"); + } else { + setError("Er ging iets mis bij het aanmaken van de poll"); + } + + setIsSubmitting(false); + } + + return ( + <div className="max-w-2xl mx-auto"> + <h1 className="text-2xl font-bold text-gray-900 mb-6"> + Nieuwe Poll Aanmaken + </h1> + + <form onSubmit={handleSubmit} className="space-y-6"> + <div> + <label + htmlFor="question" + className="block text-sm font-medium text-gray-700 mb-2" + > + Vraag + </label> + <input + id="question" + type="text" + value={question} + onChange={(e: React.ChangeEvent<HTMLInputElement>) => + setQuestion(e.target.value) + } + placeholder="Stel je vraag..." + className="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent outline-none" + required + /> + </div> + + <div> + <label className="block text-sm font-medium text-gray-700 mb-2"> + Opties (minimaal 2, maximaal 6) + </label> + <div className="space-y-3"> + {options.map((option, index) => ( + <div key={index} className="flex gap-2"> + <input + type="text" + value={option} + onChange={(e: React.ChangeEvent<HTMLInputElement>) => + updateOption(index, e.target.value) + } + placeholder={`Optie ${index + 1}`} + className="flex-1 px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent outline-none" + /> + {options.length > 2 && ( + <button + type="button" + onClick={() => removeOption(index)} + className="px-3 text-red-500 hover:bg-red-50 rounded-lg transition-colors" + > + ✕ + </button> + )} + </div> + ))} + </div> + + {options.length < 6 && ( + <button + type="button" + onClick={addOption} + className="mt-3 text-sm text-purple-600 hover:text-purple-800 font-medium" + > + + Optie toevoegen + </button> + )} + </div> + + {error && ( + <p className="text-red-600 text-sm bg-red-50 p-3 rounded-lg"> + {error} + </p> + )} + + <button + type="submit" + disabled={isSubmitting} + className="w-full bg-purple-600 text-white py-3 rounded-lg font-medium hover:bg-purple-700 disabled:bg-gray-300 disabled:cursor-not-allowed transition-colors" + > + {isSubmitting ? "Bezig met aanmaken..." : "Poll Aanmaken"} + </button> + </form> + </div> + ); +} diff --git a/Les05-NextJS-Basics/quickpoll/src/app/error.tsx b/Les05-NextJS-Basics/quickpoll/src/app/error.tsx new file mode 100644 index 0000000..a3432e5 --- /dev/null +++ b/Les05-NextJS-Basics/quickpoll/src/app/error.tsx @@ -0,0 +1,24 @@ +"use client"; + +export default function Error({ + error, + reset, +}: { + error: Error; + reset: () => void; +}) { + return ( + <div className="text-center py-16"> + <h2 className="text-2xl font-bold text-red-600 mb-4"> + Er ging iets mis! + </h2> + <p className="text-gray-600 mb-6">{error.message}</p> + <button + onClick={() => reset()} + className="bg-purple-600 text-white px-6 py-3 rounded-lg hover:bg-purple-700 transition-colors" + > + Probeer opnieuw + </button> + </div> + ); +} diff --git a/Les05-NextJS-Basics/quickpoll/src/app/favicon.ico b/Les05-NextJS-Basics/quickpoll/src/app/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..718d6fea4835ec2d246af9800eddb7ffb276240c GIT binary patch literal 25931 zcmeHv30#a{`}aL_*G&7qml|y<+KVaDM2m#dVr!KsA!#An?kSQM(q<_dDNCpjEux83 zLb9Z^XxbDl(w>%i@8hT6>)&Gu{h#Oeyszu?xtw#Zb1mO<?sK2}EE5RAKnxHU7lft+ zNRAPL3?T?25I&drAjl1ssi=G|D?(7bFsgtO(2o>{pgX9699l+Qppw7jXaYf~-84xW z)w4x8?=youko|}Vr~(D$UX<xm7|19n6Hxvd5m6xx<*9a4%RmR{en}E&p$X-wy5A}T zU0^dwXVA>IbiXABHh`p1?nn8Po~fxRJv}|0e(BPs|G`(TT%kKVJAdg5*Z|x0leQq0 zkdUBvb#>9F()jo|T~kx@OM8$9wzs~t2l;K=woNssA3l6|sx2r3+kdfVW@e^8e*E}v zA1y5{bRi+3Z`uD3{F7LgFJDdvm;nJilkzDku>BwXH(8ItVCXk*-lSJnR?-2UN%<G) zWdETe=&R39RaKR)udn|#TOgZ!e!yM=<=+`Uz{l^5UtkZ2fHDQ;UwMB}v%l$A-`~F- z{Qr^x^CSUf63Sry{6y#+`<sMA?dPFvg)$lC_RkFRKnCi7&P<a6>hJ){&rlvg`CDTj z)Bzo!3v7Ou#83zEDEFcKt(f1E0~=rqeEbTnMvWR#{+9pg%7G8y>u1OVRUSoox-ovF z2Ydma(;=YuBY(eI|04{hXzZD6_f(v~H;C~y5=DhAC{MMS>2fm~1H_t2$56pc$NH8( z5bH|<)71dV-_oCHIrzrT`2s-5w_+2CM0$95I6X8p^r!gHp+j_gd;9O<1~CEQQGS8) zS9Qh3#p&JM-G8rHekNmKVewU;pJRcTAog68KYo^dRo}(M<!8cv(gkb9@A>>36U4Us zfgYWSiHZL3;lpWT=<n~R&zm>zNAW>Dh#mB!_@Lg%$ms8N-;aPqMn+C2HqZgz&9~Eu z4|Kp<`$q)Uw1R?y(~S>ePdonHxpV1#eSP1B;Ogo+-Pk}6#0GsZZ5!||ev2MGdh}_m z{DeR7?0-1^zVs&`AV6<!ZvGbtU{7FdY&`9DeD(=q|M30$GCs(E?S0J1$e@G0#Z=wz zl)*a>Vt;r3`I`OI_wgs*w=eO%_#7Kepl{B<UyBc9U%rn&@xFZ-e{%i>@xiyCANc(l zzIyd4y|c6PXWq9-|KM8(zIk8LPk(>a)zyFWjhT!$HJ$qX1vo@d25W<<x-(q{Yn-pG zKTz?fwGmh&&2-F3f57**)?Xk#p#S9h^DhK{VVKE&0KR^-_MMD9nf@pDACnmVll!kp z3?Tha?LWW70P;AL{}cP~sW|?W|MbA09{7Kt2f!i(y>fvZQ2zUz5WRc(UnFMKHwe1| zWmlB1qdbiA(C0jmnV<}GfbKtmcu^2*P^O?<jWWPHxu*D53Uq)j1!ZtH3Vi&#Nd^rV zj`B>MBLZKt|As~ge8&AAO~2K@zbXelK|4T<{|y4`raF{=72kC2Kn(L4YyenWgrPiv z@^mr$t{#X5VuIMeL!7Ab6_kG$&#&5p*Z{+?5U|TZ`B!7llpVmp@skYz&n^8QfPJzL z0G6K_OJM9x+Wu2gfN45phANGt{7=C>i34CV{Xqlx(fWpeAoj^N0Biu`w+MVcCUyU* zDZuzO0>4Z6fbu^T_arWW5n!E45vX8N=bxTVeFoep_G#VmNlQzAI_KTIc{6>c+04vr zx@W}zE5JNSU>!THJ{J=cqjz+4{L4A{Ob9$ZJ*S1?Ggg3klFp!+Y1@K+pK1DqI|_gq z5ZDXVpge8-cs!o|;K73#YXZ3AShj50wBvuq3NTOZ`M&qtjj#GOFfgExjg8Gn8>Vq5 z`85n+9|!iLCZF5$HJ$Iu($dm?8~-ofu}tEc+-pyke=3!im#6pk_Wo8IA|fJwD&~~F zc16osQ)EBo58U7XDuMexaPRjU@h8tXe%S{fA0NH3vGJFhuyyO!Uyl2^&EOpX{9As0 zWj+P>{@}jxH)8|r;2HdupP!vie{sJ28b&bo!8`D^x}TE$%zXNb^X1p@0PJ86`dZyj z%ce7*{^oo+6%&~I!8hQy-vQ7E)0t0ybH4l%KltWOo~8cO`T=157JqL(oq_rC%ea&4 z2NcTJe-HgFjNg-gZ$6!Y`SMHrlj}Etf7<Kk?_r;;``Uc^3+u}-v3@Q8<@$Nr`<F?K z-%F>?r!zQTPPSv}{so2e>Fjs1{<qUF=hGRSFDG$<z3x<+@%{Vd%a`e+qodRP&D<om zAEn>gzk~LGeesX%r(Lh6rbhSo_n)@@G-FTQy93;l#E)hgP@d_SGvyCp0~o(Y;Ee8{ zdVUDbHm5`2taPUOY^MAGOw*<R_VaVlPH<<CgYr!E->>=s7=Gst=D+p+2yON!0%Hk` zz5mAhyT4lS*T3LS^WSxUy86q&GnoHxzQ6vm8)VS}_zuqG?+3td68_x;etQAdu@sc6 zQJ&5|4(I?~3d-QOAODHpZ=hlSg(lBZ!JZWCtHHSj`0Wh93-Uk)_S%zsJ~aD>{`A0~ z9{AG(e|q3g5B%wYKRxiL2Y$8(4w<boVrLOyLG9R$m+7N>6bzchKuloQW#e&S3n+P- z8!ds-%f;TJ1>)v)##>gd{PdS2Oc3VaR`fr=`O8QIO(6(N!A?pr5C#6fc~Ge@N%Vvu zaoAX2&(a6eWy_q&UwOhU)|P3J0Qc%OdhzW=F4D|pt0E4osw;%<%Dn58hAWD^XnZD= z>9~H(3bmLtxpF?a7su6J7M*x1By7YSUbxGi)Ot0P77`}P<HJ;%@cvfCkvm6xcMjdY zed_u6xK)F%|1Hy`)`e~K(f*MqTJ?92I+4lga{A5`-U@Cab35G6unNk<*dpB|Rtkp; z?32o^yBlJsuA-^abQ~7;%<oa^k<DbKc{lOW2!yM#nEALvv)IhY7b|Wfg(UhtiurTM zY-B6L26$JQo&Kt3nh3JTJ)garEgw^{uEM3__%b$U5{~+aMO*k)6R#grkER2`U6KS- z=j1=QhCkuy%iiHWrqH8CeGNw*C?epTpl2Bo@ugUPKRFeiVHOpL7PHu-SAgX@qmTGH z_%ePz1`io8XDfwLmip;Rn;1yo+3>3{)&5Un{KD?`-e?r21!4vTTnN(4Y6Lin?UkSM z`MXCTC1@4A4~mvz%Rh2&EwY))LeoT=*`tMoqcEXI>TZU9WTP#l?uFv+@Dn~b(>xh2 z;>B?;Tz2SR&KVb>vGiBSB`@U7VIWFSo=LDSb9F{GF^DbmWAfpms8Sx9OX4CnBJca3 zlj9(x!dIjN?OG1X4l*imJNvRCk}F%!?SOfiOq5y^mZW)jFL@<gIi}tCXee1<sGV$i z4r_`X#mEQbiDh!Efji0GjM9z-0bF}p0(*s(OzMJ|;K&OJBar<ARLp}T>a|r-@d#f7 z2gmU8L3IZq0ynIws=}~m^#@&C%J6QFo~Mo4V`>v7MI-_!EBMMtb%_M&kvAaN)@ZVw z+`toz&WG#HkWDjnZE!6nk{e-oFdL^$YnbOCN}JC&{$#$O27@|Tn-skXr)2ml2~O!5 zX+gYoxhoc7qoU?C^3~&!U?kRFtnSEecWuH0B0OvLodgUAi}8p1<ZO0#U-k07ifx!> zrO6RSXHH}D<I*>Mc$&|?D004<Y&c6)m74d`LOLU@ruR+Um4>DiOVMHV8kXCP@7NKB zgaZq^^O<7PoKEp72kby@W0Z!Y*A<g|TlOeriuPP`vK2IntATvs?Iv|J14j&;NFSFo zyJ+sca?G+8C%!b{Sq=6cJJqS>y{&vfg#C&gG@YVR9g?FEocMUi1gSN$+V+ayF45{a zuDZDT<?u;)RfLQwg>N}mS|;BO%gEf}pjBfN2-gIrU#G5~cucA;dokXW89%>AyXJJI z9X4Ul<x{xc_m~`mWBP0<g-{#wm}Vv~Ef3pKWC&N_<~88zSbEk;;+{DnJ9-u&Zc74s zJ6TCQyl_^|5cY;wmDdrU@LTL-3v0H#Ui?8ICQV{imof1MHuM$`e*ux>IWA|ZYHgbI z5?oFk@A=Ik7lrEQPDH!H+b`7_Y~aDb_qa=B2^Y&Ow41cU=4WDd40dp5(QS-WMN-=Y z9g;6_-JdNU;|6cPwf$ak*aJIcwL@1n$#l~zi{c{EW?T;DaW*E8DYq?Umtz{nJ&w-M zEMyT<MDk{HKbd#ckg5-pS_?QUVhZv?&Q-ioBS}$nvBd)nE7YO0deN~G(#zCJAbY$E z!)g3Ytl=_NDUV%pykcE+Q<{EoZ_4FR@&#d<hqs%N>DrC&9K$d|kZe2#ws6)L=7K+{ zQw{XnV6UC$6-rW0emqm8wJoeZK)wJIcV?dST}Z;G0Arq{dVDu0&4kd%N!3F1*;*pW zR&qUiFzK=@44#QGw7k1`3t_d8&*kBV->O##t|tonFc2YWrL7_eqg+=+k;!F-`^b8> z#KWCE8%u4k@EprxqiV$VmmtiWxDLgnGu$Vs<8rppV5E<MCr+anDo)-{XRlCJ;D#M( zT=3WgR02;Nm!54biUb^FtzPh8iGrf412epnki-k+G4mdkzC|lJqaRMbb0~Jjp-{}I z5Do5afZi>ajBXL4nyyZM$SWVm!wnCj-B!Wjqj5-5dNXukI2$$|Bu3Lrw}z65Lc=1G z^-#WuQOj$hwNGG?*CM_TO8Bg-1+qc>J7k5c51U8g?ZU5n?HYor;~JIjoWH-G>AoUP ztrWWLbRNqIjW#RT*WqZgPJXU7C)VaW5}MiijYbABmzoru6EmQ*N8cVK7a3|aOB#O& zBl8JY2WKfmj;h#Q!pN%9o@VNLv{OUL?rixHwOZuvX7{IJ{(EdPpuVFoQqIOa7gi<U zTpbX&UCeYeNu>LVkBOKL@^smUA!tZ1CKRK}#SSM)iQHk)*R~?M!qkCruaS!#oIL1c z<cK@1=jX>?J<BS8bpdt^R+}%A_DEhF^%o}8e!!lc`Y!qU>;U~&FfH#*98^G?i}pA{ z9Jg36t4=%6mhY(quYq*vSxptes9qy|7xSlH?G=S@>u>Ebe;|LVhs~@+06N<4CViBk zUiY$thvX;>Tby6z9Y1e<Q<iIG*|o$r?OTFp`s)@_nHs4LeWbGvg7^}NK)>dAMQaiH zm^r3v#$Q#2T=X>bsY#D%s!bhs^M9PMAcHbCc0FMHV{u-dwlL;a1eJ63v5U*?Q_8JO zT#50!RD619#j_Uf))0ooADz~*9&lN!bBDRUgE>Vud-i5ck%vT=r^yD*^?Mp@Q^v+V zG#-?gKlr}Eeqifb{|So?HM&g91<J5P5=Ly{?(NNY{6`O~L5r@sJe3rNZn06%SLk); z9?hvE^Hr{!*G$<_doyzGn#*z*#}?)8dH=eYTgvc)T~}Jw!kCv68<+KL5{5?EXtDAZ zWeNqp8%KIuBi&icn5s815Vho<+99VW1~m@L8l0=$c`t-L{q))~<!p*~vCdUcBcPz` zyUi}!-k_`G{>P8|av8hQoCmQXkd?7wIJw<dY^{|7OQJUHKB~nksN_|Xy;DL?xjxU^ zbMa`WdfTBnr<wTd$mY&SgJ4U|X``k`#`gN@M+0x2W{YgC3kbLk<uYFJWglkx_)2#b ztRiuA!EK9o)f`I2k)l;Of%E`ff91WlZh8yfRi6#N-mC`Ma(yr~U82SyAhc9B+ur!f zP-3igg*KeYs9mGOAw@OaXYy9DnGjn0<m`JH&Q^h}^!h+uS9Ct*o-oEy(?iT6Yco>b z_^v8bbg`<ZOL)a;i=IdfK0Zvw4nXsoC?eTOMpY)_ptiORm%J(1CD3dE0Z%Vy<2iHp zcp>SAn{I*4bH$u(RZ6*x<DqKJ+5;a6Jq~=Y8V&c?Vsyq88!2nD?H?Eww58Mqt$7R8 z5BMjmKx>UhuA~hc=8czK8SHEKTzSxgbwi~9(OqJB&gwb^l4+m`k*Q;_?>Y-APi1{k zAHQ)P)G)f|AyjSgcCFps)Fh6Bca*Xznq3<?y%xNvu0N78_R?~<RDFQx0ynlRG(E|j zvEGN3bF<E_9p-I!UwQXFqcSGV#e^98tgFqLp+z9eP}y!jNA{)r*a+%M-_20xg?94< zzmM{}syi0cd&P)zywMdS&Y_9k5JDtOM!L)b^2WP!+fHYGv>6!pV6Az&m{O8$wGFD? zY&O*3*J0;_EqM#jh6^gMQKpXV?#1?>$ml1xvh8nSN>-?H=V;nJIwB07YX$e6vLxH( zqYwQ>qxwR(i4f)DLd)-$P>T-no_c!LsN@)8`e;W@)-Hj0>nJ-}Kla4-ZdPJzI&Mce zv)V_j;(3ERN3_@I$N<^|4Lf`B;8n+bX@bHbcZTopEmDI*Jfl)-pFDvo6svPRoo@(x z);_{lY<;);XzT`dBFpRmGrr}z5u1=p<K1~3>C^<jVp}L(pzgMB_Vs-O?{Z?y$8M;) zi@7zwpzV9#m72%En~(9@E)GWV^(~J*@^*K*TE0mynAnGJ5YSLCEnC42H-`tr4L=oW zI}N{xQ$HT8Q6CVHf%RY&xw7!Zj(0xmg(K#UQ4u!ej95z7V4phlcTJ2&AR}$)zV-s! zO7bqY6(=?1t+JCOW_z%HRE>S-{ce6iXQlLGcItwJ^mZx{m$&DA_oEZ)B{_bYPq-HA zcH8WGoBG(aBU_j)vEy+_71T34@4dmSg!|M8Vf92Zj6WH7Q7t#OHQqWgFE3ARt+%!T z?oLovLVlnf?2c7pTc)~cc^($_8nyKwsN`RA-23ed3sdj(ys%pjjM+9JrctL;dy8a( z@en&CQmnV(()bu|Y%G1-4a(6x{aLytn$T-;(&{QIJB9vMox11U-1HpD@d(QkaJdEb zG{)+6Dos_L+O3NpWo^=gR?evp|CqEG?L&Ut#D*KLaRFOgOEK(Kq1@!EGcTfo+%A&I z=dLbB+d$u{sh?u)xP{PF8L%;YPPW53+@{>5W=Jt#wQpN;0_HYdw1{ksf_XhO4#2F= zyPx6Lx2<92L-;L5PD`zn6zwIH`Jk(<gsVPionpJ-imI56$j4P0!br@ny3=!{x2TY^ zCD=)8_PgmN)E!^nczcDGc9Wm7oo5O3@fh=k=kh8J?_3KqEp7JHdv8z_iZ5#KmbiPt z2Bt8Ro^p$7pS!xL3mtj<iN3f}#r6_&$Es0PnJTE?c;0#$%cGdu`T%~`gW;c^VD-S= zrAatMf^%Lzr*wQ4kHSOb?WOUuEsJQ3xr{Imf1t{~iNmRwb_SP9!?FFN=b-E){!8P2 ztWCT~262O8`%?3<W4Wg+ovWY<re)?^kZ|Yi>$?Qw({erA$^bC;q33hv!d!>%wRhj# zal^hk+WGNg;rJtb-EB(?czvOM=H7dl=vblBwAv>}%1@{}mnpUznfq1cE^sgsL0*4I zJ##!*B?=vI_OEVis5o+_IwMIRrpQyT_Sq~ZU%oY7c5JMIADzpD!Upz9h@iWg_>>~j zOLS;wp^i$-E?4<_cp?RiS%Rd?i;f*mOz=~(&3lo<=@(nR!_Rqiprh@weZlL!t#NCc zO!QTcInq|%#>OVgobj{~ixEUec`E25zJ~*DofsQdzIa@5^nOXj2T;8O`l--(QyU<o zeu8G~Z>^$t?TGY^7#&FQ+2SS3B#qK*k3`ye?8jUYSajE5iBbJls75CCc(m3dk{t?- zopcER9{Z?TC)mk~gpi^kbbu>b-+a{m#8-y2^p$ka4n60w;Sc2}HMf<8JUvh<G@KZw z+<GL!lpeahq2+nO{>CL0B&Btk)T`ctE$*qNW8L$`7!r^9T+>=<=2qaq-;ll2{`{Rg zc5a0ZUI$oG&j-qVOuKa=*v4aY#IsoM+1|c4Z)<}lEDvy;5huB@1RJPquU2U*U-;gu z=En2m+qjBzR#DEJDO`WU)hdd{Vj%^0V*KoyZ|5lzV87&g_j~NCjwv0uQVqXOb*QrQ zy|Qn`hxx(58c<SELWpDAg~83oY-J_WoDiI6d7>70$E;L(X0uZZ72M1!6oeg)(cdKO ze0gDaTz+ohR-#d)NbAH4x{I(21yjwvBQfmpLu$)|m{XolbgF!pmsqJ#D}(ylp6uC> z{bqtcI#hT#HW=wl7>p!38sKsJ`r8}lt-q%Keqy%u(xk=yiIJiUw6|5IvkS+#?JTBl z8H5(Q?l#wzazujH!8o>1xtn8#_w+397*<wp?Ryt$UFh41$qd}LyNJ7Oao(Aw2g|wy zH_nZ+R#~EUME^#j4$@^5&>_cy8!pQGP%K(Ga3pAjsaTbbXJlQF_+m+-UpUUent@xM zg%jqLUExj~o^vQ3Gl*>wh=_gOr2*|U64_iXb+-111a<qXXnUI&{l`dM&{4Gw)jZn; zlj{VxW@#OcVE1Y%J*u^Z@H+XSqL6SwA|^jv2RU_+d;O!mk)dw7-m9B4{6*G1zRdR6 zQ}6v&Xt7R2h3Xp}EQk4nF2TULG{Ri=D|JC<a+K7dldN1}CY_f!vK#u}K3`g#TpO&W z;!;64`0$d9raD!VbYP`kuFUasaMh!;&81y}LHS(SuGRxwEn4LZb4DS1j9iAq$MXd@ z(Ebka7_Gc(ljGaJqtI-OzmA@c@sYB$)Vg!RP4~``vaVyRq$rJXRjIPwtepN;(B%wy zmU>H}$TjeajM+I20xw(((>fej-@CIz4S1pi$(#}P7`4({6QS2CaQS4NPENDp>sAqD z$bH4KGzXGffkJ7R>V>)>tC)uax{UsN*dbeNC*v}#8Y#OWYwL4t$ePR?VTyIs!wea+ z5Urmc)X|^`MG~*dS6pGSbU+gPJoq*^a=_>$n4|P^w$sMBBy@f*Z^Jg6?n5?oId6f{ z$LW4M|4m502z0t7g<#Bx%X;9<=)smFolV&(V^(7Cv2-sxbxopQ!)*#ZRhTBpx1)Fc zNm1T%bONzv6@#|dz(w02AH8OXe>kQ#1FMCzO}2J_mST)+ExmBr9cva-@?;wnmWMOk z{3_~EX_xadgJGv&H@zK_8{(x84`}+c?oSBX*Ge3VdfTt&F}yCpFP?CpW+BE^cWY0^ zb&uBN!Ja3UzYHK-CTyA5=L<c0d<h!DNBIa<xax8W3(Ru8L0cVXQ18|Y^|*S%)R96z zBT$(=zQ}2vmt6LzN~Oyf_Y92%P@QOx{7~}5!UIqCdfu?VwC0Nb!2@iiit8-5zUWFG z*G&+GLIU#J;}hvowNJWnglvb^<2q~lS#?ixVtYT@(O3{TC|4kFJYLB*jni-4YZi0> zEMW{l3Usky#ly=7px648W31UNV@K)&Ub&zP1c7%)`{);I4b0Q<)B}3;NMG2JH=X$U zfIW4)4n9ZM`-yRj67I)YSLDK)qfUJ_ij}a#aZN~9EXrh8eZY2&=uY%2N0UFF7<~%M zsB8=erOWZ>Ct_#^tHZ|*q`H;A)5;ycw*I<Cd*bZlOJ9YmRUK2<qXkpRR3nr6r~%Jz z*(8tA&DYO)etdgVmoonqD{*<5Fog4ClIs-~_uhjuZOI}#Wy+ce${%#oyHloXelqfz z8)?D3Y_>cmVxi8_0Xk}aJA^ath+E;xg!x+As(M#0=)3!NJR6H&9+zd#iP(m0PIW8$ z1Y^VX`>jm`W!=WpF*{ioM?C9`yOR>@0q=u7o>BP-eSHqCgMDj!2anwH?s%i2p+Q7D zzszIf5XJpE)IG4;d_(La-xenmF(tgAxK`Y4sQ}BSJEPs6N_U2vI{8=0C_F?@7<(G; zo$~G=8p+076G;`}>{MQ>t>7cm=zGtfbdDXm6||jUU|?X?CaE?(<6bKDYKeHlz}DA8 zXT={X=yp_R;HfJ9h%?eWvQ!dRgz&Su*JfNt!Wu>|XfU<MM~gB&J0gc}IH}?|B4WRK zWPL0FhctFGdMucOFdhrVunIe5)4K^H9IjB#eA)p5w?c#v7kp8jx^~bxxJB{;hPFL9 zkR9Dbpj+T5ZMgHQg|oj*DS;x&jK}1rn&}Shp9sgOI*7puQD-w?3H*cg72;5H(_zW* zApJBIM-p2~F;qWDj!n|Kd=5|T8OPkQ_G;ujgvKybr5@~eci2{8WAz+%NUSp-&eoG! zOGLNLJewWl&1*NT467W3god~fYgX?!f0?NCFnjD$qE-fyQ)|Q_DLc*{olmXSVl$g_ z$vj}o?RatMy(o*j8?q1Mgw{OUOgVR6_qvS<Co*&!cR`ROi|*I`ajyG5s@L8agnX2J zF=DLkMG`z{RP&996y0yAtvJcb<cba?TV#j4VYFPC>&68iRikRrHRW|ZxzRR^`eIGt zIeiDgVS>IeExKVRWW8-=<xUfo0v~z=RA=cFWKXgcMECd}xHp7iqkBanH}TZ0h0rA= zqxUZ>A=<k-RjTtwbJkkep{8z*173wY^e%-U0{Ue!n@wbg^2q)Vx5c(_RfvuR4}XXn z+JE>yA`}`)ZkWBrZD`hpWIxBGkh&f#ijr449~m`j6{4jiJ*C!oVA8ZC?$1RM#K(_b zL9TW)kN*Y4%^-qPpMP7d4)o?Nk#>aoYHT(*g)qmRUb?**F@pnNiy6Fv9rEiUqD(^O zzyS?nBrX63BTRYduaG(0VVG2yJRe%o&rVrLjbxTaAFTd8s;<<@Qs>u(<193R8>}2_ zuwp{7;H2a*X7_jryzriZXMg?bTuegABb^87@SsKkr2)0Gyiax8KQWstw^v<oS3Xw7 zu51m`3~hoyxErcHymdFTZd#AO59{EkuFTcpAR33(3xc{zRnn1~1Ei(i*^HdCvM~;; za&}Uip|u>#ix45EVrcEhr>!NMhprl<CqZuKa#zuI&@zymVzIicetS0bq#u?m(r_@S zJ79bl%4EyHCQ3fK@en+A1@)e}HWLP|gr_zuoA{}Z<(-*53Zu@k+=^%~5F(z$EFLI; z-TQTS8$W|GRbZq93Ha1?lu+`O;rn>$InQMzjSFH54x5k9qHc`@9uKQzvL4ihcq{^B zPrVR=o_ic%Y>6&rMN)hTZsI7I<3&`#(nl+3y3ys9A~<Ao%ZuW})CJ)6^(aRV(gGxR z89#(FDW;GZEAf;rI$+PU)rEV|rASrwP0_mr^Ldv)IuUf1M>&^=4?PL&nd8)`OfG#n zwAMN$1&>K++c{^|7<<q5KGu)u(OEfEJJw2aEi(;x-i=Y=j3ram9H2n-Fuqv0dVlXJ z&WgG5X({!vJFDrEbm+CWDca^zIe2@s1@a;;Y3!U9Q)&P0UXFmCP51_!wvTfAIyR^M z7^R*O@yz1b-s4VC>4P=2y(B{jJsQ0a#U;HTo4ZmWZYvI{+s;Td{Yzem%0*k#)vjpB zia;J&>}ICate44SFYY3vEelqStQWFihx%^vQ@Do(sOy7yR2@WNv7Y9I^yL=nZr3mb zXKV5t@=?-Sk|b{XMhA7ZGB@2hqsx}4xwCW!in#C<kr{U&JG{9FhoZ<aTve_lLz39> zI@}sc<h3gsW}hp-`WUywKA>Zlr3-NFJ@NFaJlhyfcw{k^vvtGl`N9xSo**rDW4S}i zM9{fMPWo%4wYDG~BZ18BD+}h|GQKc-g^{++3MY>}W_uq7jGHx{mwE9fZiPCoxN$+7 zrODGGJrOkcPQUB(FD5aoS4g~7#6NR^ma7-!>mHuJfY5kTe6PpNNKC9GGRiu^L31uG z$7v`*JknQHsYB!Tm_W{a32TM099djW%5e+j0Ve_ct}IM>XLF1Ap+YvcrLV=|CKo6S zb+<Td{{5RWR}u2f(q<b(D$9JsF0OOzJ*+z0P5kc1t}CXlYgua%x*2lSgp|*WS3H-# zdYr7?GQOL18zUS<2|;+vi4|4sQBM2Gs&WVS!D`q5Lz;XR@5rEfa{uG-!q?R8Ncz%( z5K6~LQ@d2wp#)5q4u<ENlFbS)U4o1t9{-d>9Nl3_YdKP6%Cxy@6TxZ>;4&nTneadr z_ES90ydCev)LV!dN=#(*f}|ZORFdvkYBni^aLbUk>BajeWIOcmHP#8S)*2U~QKI%S zyrLmtPqb&TphJ;>yAxri#;{uyk`JJqODDw%(Z=2<VfJZemI(PFAD{6Sm|uE%BTbkl zROsg*MOh20YgGs3H7?@pmQ>`1uc}br^V%>j!gS)D*q*f_-qf8&D;W1dJgQMlaH5er zN2U<%Smb7==vE}dDI8K7cKz!vs^73o9f>2sgiTzWcwY|BMYHH5%Vn7#kiw&eItCqa zIkR2~Q}>X=Ar8W|^Ms41Fm8o6IB2_j60eOeBB1Br!boW7JnoeX6Gs)?7rW0^5psc- zjS16yb>dFn>KPOF;imD}e!enuIniFzv}n$m2#gCCv4jM#ArwlzZ$7@9&XkFxZ4n!V zj3dyiwW4Ki2QG{@i>yuZXQizw_OkZI^-3otXC{!(lUpJF33gI60ak;Uqitp74|B6I zgg{b=Iz}WkhCGj1M<xTd?60J5qsr1Cg7F~~U2N!(@lC<>=hu4#Aw173YxIVbISaoc z-nLZC*6Tgivd5V`K%GxhBsp@SUU60-rfc$=wb>zdJzXS&-5(NRRodFk;Kxk!S(<ov z$YXcI9;^grAyiJ4dWTv3b}K~Ww09(;mLY4+kj|$A?IMr}`7q?mIS1>O(a0e7oY=E( zAyS;Ow?6Q&XA+cnkCb{28_1N8H#?J!*$MmIwLq^*T_9-z^&UE@A(z9oGYtFy6EZef LrJugUA?W`A8`#=m literal 0 HcmV?d00001 diff --git a/Les05-NextJS-Basics/quickpoll/src/app/globals.css b/Les05-NextJS-Basics/quickpoll/src/app/globals.css new file mode 100644 index 0000000..f1d8c73 --- /dev/null +++ b/Les05-NextJS-Basics/quickpoll/src/app/globals.css @@ -0,0 +1 @@ +@import "tailwindcss"; diff --git a/Les05-NextJS-Basics/quickpoll/src/app/layout.tsx b/Les05-NextJS-Basics/quickpoll/src/app/layout.tsx new file mode 100644 index 0000000..494f806 --- /dev/null +++ b/Les05-NextJS-Basics/quickpoll/src/app/layout.tsx @@ -0,0 +1,46 @@ +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 ( + <html lang="nl"> + <body className="min-h-screen bg-gray-50 text-gray-900"> + <nav className="bg-white border-b border-gray-200 shadow-sm"> + <div className="max-w-4xl mx-auto px-4 py-4 flex items-center justify-between"> + <Link href="/" className="text-xl font-bold text-purple-600"> + 🗳️ QuickPoll + </Link> + <div className="flex gap-4 items-center"> + <Link + href="/" + className="text-gray-600 hover:text-purple-600 transition-colors" + > + Polls + </Link> + <Link + href="/create" + className="bg-purple-600 text-white px-4 py-2 rounded-lg hover:bg-purple-700 transition-colors text-sm font-medium" + > + Nieuwe Poll + </Link> + </div> + </div> + </nav> + <main className="max-w-4xl mx-auto px-4 py-8">{children}</main> + <footer className="text-center text-gray-400 text-sm py-8"> + © 2025 QuickPoll — NOVI Hogeschool Les 5 + </footer> + </body> + </html> + ); +} diff --git a/Les05-NextJS-Basics/quickpoll/src/app/loading.tsx b/Les05-NextJS-Basics/quickpoll/src/app/loading.tsx new file mode 100644 index 0000000..a1d55a7 --- /dev/null +++ b/Les05-NextJS-Basics/quickpoll/src/app/loading.tsx @@ -0,0 +1,24 @@ +export default function Loading() { + return ( + <div className="space-y-4"> + <div className="animate-pulse"> + <div className="h-8 bg-gray-200 rounded w-1/3 mb-2" /> + <div className="h-4 bg-gray-200 rounded w-1/2 mb-8" /> + </div> + {[1, 2, 3].map((i) => ( + <div + key={i} + className="animate-pulse bg-white rounded-xl border border-gray-200 p-6" + > + <div className="h-5 bg-gray-200 rounded w-3/4 mb-3" /> + <div className="h-4 bg-gray-200 rounded w-1/4 mb-3" /> + <div className="flex gap-2"> + <div className="h-6 bg-gray-100 rounded-full w-20" /> + <div className="h-6 bg-gray-100 rounded-full w-24" /> + <div className="h-6 bg-gray-100 rounded-full w-16" /> + </div> + </div> + ))} + </div> + ); +} diff --git a/Les05-NextJS-Basics/quickpoll/src/app/not-found.tsx b/Les05-NextJS-Basics/quickpoll/src/app/not-found.tsx new file mode 100644 index 0000000..a5b948a --- /dev/null +++ b/Les05-NextJS-Basics/quickpoll/src/app/not-found.tsx @@ -0,0 +1,18 @@ +import Link from "next/link"; + +export default function NotFound() { + return ( + <div className="text-center py-16"> + <h2 className="text-4xl font-bold text-gray-900 mb-4">404</h2> + <p className="text-gray-600 mb-6"> + Deze pagina bestaat niet (meer). + </p> + <Link + href="/" + className="bg-purple-600 text-white px-6 py-3 rounded-lg hover:bg-purple-700 transition-colors inline-block" + > + Terug naar home + </Link> + </div> + ); +} diff --git a/Les05-NextJS-Basics/quickpoll/src/app/page.tsx b/Les05-NextJS-Basics/quickpoll/src/app/page.tsx new file mode 100644 index 0000000..225a48b --- /dev/null +++ b/Les05-NextJS-Basics/quickpoll/src/app/page.tsx @@ -0,0 +1,57 @@ +import Link from "next/link"; +import { getPolls } from "@/lib/data"; +import type { Poll } from "@/types"; + +export const dynamic = "force-dynamic"; + +export default function HomePage() { + const polls: Poll[] = getPolls(); + + const totalVotes = (poll: Poll): number => + poll.votes.reduce((sum, v) => sum + v, 0); + + return ( + <div> + <h1 className="text-3xl font-bold text-gray-900 mb-2">Actieve Polls</h1> + <p className="text-gray-500 mb-8">Klik op een poll om te stemmen</p> + + <div className="grid gap-4"> + {polls.map((poll) => ( + <Link + key={poll.id} + href={`/poll/${poll.id}`} + className="block bg-white rounded-xl border border-gray-200 p-6 hover:border-purple-300 hover:shadow-md transition-all" + > + <h2 className="text-lg font-semibold text-gray-900 mb-2"> + {poll.question} + </h2> + <div className="flex items-center gap-4 text-sm text-gray-500"> + <span>{poll.options.length} opties</span> + <span>·</span> + <span>{totalVotes(poll)} stemmen</span> + </div> + <div className="flex flex-wrap gap-2 mt-3"> + {poll.options.map((option, index) => ( + <span + key={index} + className="bg-gray-100 text-gray-600 px-3 py-1 rounded-full text-sm" + > + {option} + </span> + ))} + </div> + </Link> + ))} + </div> + + {polls.length === 0 && ( + <div className="text-center py-16 text-gray-400"> + <p className="text-lg">Nog geen polls</p> + <Link href="/create" className="text-purple-600 hover:underline"> + Maak de eerste! + </Link> + </div> + )} + </div> + ); +} diff --git a/Les05-NextJS-Basics/quickpoll/src/app/poll/[id]/not-found.tsx b/Les05-NextJS-Basics/quickpoll/src/app/poll/[id]/not-found.tsx new file mode 100644 index 0000000..bfce1cf --- /dev/null +++ b/Les05-NextJS-Basics/quickpoll/src/app/poll/[id]/not-found.tsx @@ -0,0 +1,20 @@ +import Link from "next/link"; + +export default function PollNotFound() { + return ( + <div className="text-center py-16"> + <h2 className="text-2xl font-bold text-gray-900 mb-4"> + Poll niet gevonden + </h2> + <p className="text-gray-600 mb-6"> + Deze poll bestaat niet of is verwijderd. + </p> + <Link + href="/" + className="bg-purple-600 text-white px-6 py-3 rounded-lg hover:bg-purple-700 transition-colors inline-block" + > + Bekijk alle polls + </Link> + </div> + ); +} diff --git a/Les05-NextJS-Basics/quickpoll/src/app/poll/[id]/page.tsx b/Les05-NextJS-Basics/quickpoll/src/app/poll/[id]/page.tsx new file mode 100644 index 0000000..bd3aa82 --- /dev/null +++ b/Les05-NextJS-Basics/quickpoll/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 }>; +} + +export async function generateMetadata({ params }: PageProps): Promise<Metadata> { + const { id } = await params; + const poll = getPollById(id); + + if (!poll) { + return { title: "Poll niet gevonden" }; + } + + return { + title: `${poll.question} — QuickPoll`, + description: `Stem op: ${poll.options.join(", ")}`, + }; +} + +export default async function PollPage({ params }: PageProps) { + const { id } = await params; + const poll = getPollById(id); + + if (!poll) { + notFound(); + } + + return ( + <div className="max-w-2xl mx-auto"> + <h1 className="text-2xl font-bold text-gray-900 mb-6"> + {poll.question} + </h1> + <VoteForm poll={poll} /> + </div> + ); +} diff --git a/Les05-NextJS-Basics/quickpoll/src/components/VoteForm.tsx b/Les05-NextJS-Basics/quickpoll/src/components/VoteForm.tsx new file mode 100644 index 0000000..457fe5d --- /dev/null +++ b/Les05-NextJS-Basics/quickpoll/src/components/VoteForm.tsx @@ -0,0 +1,128 @@ +"use client"; + +import { useState } from "react"; +import { useRouter } from "next/navigation"; +import type { Poll } from "@/types"; + +interface VoteFormProps { + poll: Poll; +} + +export default function VoteForm({ poll }: VoteFormProps) { + const [selectedOption, setSelectedOption] = useState<number | null>(null); + const [hasVoted, setHasVoted] = useState<boolean>(false); + const [isSubmitting, setIsSubmitting] = useState<boolean>(false); + const [currentPoll, setCurrentPoll] = useState<Poll>(poll); + const router = useRouter(); + + const totalVotes: number = currentPoll.votes.reduce( + (sum, v) => sum + v, + 0 + ); + + async function handleVote(): Promise<void> { + if (selectedOption === null || isSubmitting) return; + + setIsSubmitting(true); + + const response = await fetch(`/api/polls/${currentPoll.id}/vote`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ optionIndex: selectedOption }), + }); + + if (response.ok) { + const updatedPoll: Poll = await response.json(); + setCurrentPoll(updatedPoll); + setHasVoted(true); + } + + setIsSubmitting(false); + } + + function getPercentage(votes: number): number { + if (totalVotes === 0) return 0; + return Math.round((votes / totalVotes) * 100); + } + + return ( + <div className="space-y-3"> + {currentPoll.options.map((option, index) => { + const percentage = getPercentage(currentPoll.votes[index]); + const isSelected = selectedOption === index; + + return ( + <button + key={index} + onClick={() => !hasVoted && setSelectedOption(index)} + disabled={hasVoted} + className={`w-full text-left p-4 rounded-lg border-2 transition-all relative overflow-hidden ${ + hasVoted + ? "border-gray-200 cursor-default" + : isSelected + ? "border-purple-500 bg-purple-50" + : "border-gray-200 hover:border-purple-300 cursor-pointer" + }`} + > + {hasVoted && ( + <div + className="absolute inset-0 bg-purple-100 transition-all duration-500" + style={{ width: `${percentage}%` }} + /> + )} + <div className="relative flex justify-between items-center"> + <div className="flex items-center gap-3"> + {!hasVoted && ( + <div + className={`w-5 h-5 rounded-full border-2 flex items-center justify-center ${ + isSelected + ? "border-purple-500 bg-purple-500" + : "border-gray-300" + }`} + > + {isSelected && ( + <div className="w-2 h-2 rounded-full bg-white" /> + )} + </div> + )} + <span className="font-medium">{option}</span> + </div> + {hasVoted && ( + <span className="text-sm font-semibold text-purple-700"> + {percentage}% ({currentPoll.votes[index]} stemmen) + </span> + )} + </div> + </button> + ); + })} + + {!hasVoted && ( + <button + onClick={handleVote} + disabled={selectedOption === null || isSubmitting} + className="w-full bg-purple-600 text-white py-3 rounded-lg font-medium hover:bg-purple-700 disabled:bg-gray-300 disabled:cursor-not-allowed transition-colors mt-4" + > + {isSubmitting ? "Bezig met stemmen..." : "Stem!"} + </button> + )} + + {hasVoted && ( + <div className="text-center pt-4"> + <p className="text-green-600 font-medium mb-2"> + Bedankt voor je stem! + </p> + <p className="text-sm text-gray-500"> + Totaal: {totalVotes} stemmen + </p> + <button + onClick={() => router.push("/")} + className="mt-4 text-purple-600 hover:underline text-sm" + > + ← Terug naar alle polls + </button> + </div> + )} + </div> + ); +} diff --git a/Les05-NextJS-Basics/quickpoll/src/lib/data.ts b/Les05-NextJS-Basics/quickpoll/src/lib/data.ts new file mode 100644 index 0000000..3e36f41 --- /dev/null +++ b/Les05-NextJS-Basics/quickpoll/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/Les05-NextJS-Basics/quickpoll/src/middleware.ts b/Les05-NextJS-Basics/quickpoll/src/middleware.ts new file mode 100644 index 0000000..76bbfab --- /dev/null +++ b/Les05-NextJS-Basics/quickpoll/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/Les05-NextJS-Basics/quickpoll/src/types/index.ts b/Les05-NextJS-Basics/quickpoll/src/types/index.ts new file mode 100644 index 0000000..73f152f --- /dev/null +++ b/Les05-NextJS-Basics/quickpoll/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/Les05-NextJS-Basics/quickpoll/tsconfig.json b/Les05-NextJS-Basics/quickpoll/tsconfig.json new file mode 100644 index 0000000..cf9c65d --- /dev/null +++ b/Les05-NextJS-Basics/quickpoll/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/Samenvattingen/Les05-Samenvatting.md b/Samenvattingen/Les05-Samenvatting.md index 2ecb342..7638462 100644 --- a/Samenvattingen/Les05-Samenvatting.md +++ b/Samenvattingen/Les05-Samenvatting.md @@ -1,234 +1,58 @@ -# Les 5: TypeScript voor React +# Les 5: Next.js — Het React Framework (Part 1) --- ## Hoofdstuk -**Deel 2: Technical Foundations** (Les 4-9) +**Deel 2: Technical Foundations** (Les 4-8) ## Beschrijving -Verdieping in TypeScript met focus op React-patronen. Studenten leren generics, utility types, en hoe je React components, hooks, events en API calls correct typt. Voorbereiding op Les 6 waar ze met Next.js aan de slag gaan. - -**Voorkennis:** Les 4 (TypeScript Fundamentals) — basic types, interfaces, union types, type aliases, functies typen. +Introductie Next.js voor React developers. App Router, routing, server/client components, data fetching. Hands-on: QuickPoll app Part 1 (stap 0-3) klassikaal bouwen. --- -## Te Behandelen +## Te Behandelen (~45 min theorie) -### Generics (20 min) -- Waarom generics? Herbruikbare, type-safe code -- `Array<T>`, `Promise<T>` — generics die ze al kennen -- Eigen generics schrijven: `function getFirst<T>(items: T[]): T` -- Generics met constraints: `<T extends { id: string }>` -- `keyof` operator: `function getValue<T, K extends keyof T>(obj: T, key: K): T[K]` - -```typescript -// Generic functie -function wrapInArray<T>(value: T): T[] { - return [value]; -} - -wrapInArray("hello"); // string[] -wrapInArray(42); // number[] - -// Met constraint -function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] { - return obj[key]; -} -``` +- Waarom Next.js? React is een library, Next.js is het framework +- Create-next-app setup met TypeScript + Tailwind +- App Router: folder-based routing (page.tsx = route) +- Layouts: root layout, nested layouts +- Dynamic Routes: [id] met Promise-based params (Next.js 15) +- Server Components vs Client Components +- "use client" directive +- Data Fetching in async Server Components +- Server Actions introductie ("use server") +- Route Groups ((marketing)) +- Project structuur best practices --- -### Utility Types (15 min) -- `Partial<T>` — alle properties optioneel (handig voor updates) -- `Pick<T, K>` — selecteer specifieke properties -- `Omit<T, K>` — alles behalve specifieke properties -- `Record<K, V>` — key-value mapping -- Praktisch voorbeeld: `updateUser(id: string, data: Partial<User>)` +## Lesopdracht (120 min, klassikaal) -```typescript -interface User { - id: string; - name: string; - email: string; - age: number; -} +### QuickPoll App Part 1 — samen met Tim -// Partial: voor update functies -function updateUser(id: string, updates: Partial<User>): User { ... } -updateUser("1", { name: "Tim" }); // alleen name updaten +- **Stap 0:** Setup (create-next-app, npm install, dev server) +- **Stap 1:** Layout met navigatie (Tailwind styling) +- **Stap 2:** Homepage met polls lijst (server component) +- **Stap 3:** API route GET single poll (dynamic route, 404 handling) -// Omit: voor create functies (id wordt server-side gegenereerd) -type CreateUserInput = Omit<User, "id">; - -// Pick: voor specifieke views -type UserPreview = Pick<User, "id" | "name">; -``` - ---- - -### React Props Typen (20 min) -- Interface voor component props -- Children typen met `React.ReactNode` -- Callback props: `onClick: () => void`, `onChange: (value: string) => void` -- Spread props en prop forwarding -- Default values met destructuring - -```typescript -interface CardProps { - title: string; - children: React.ReactNode; - variant?: "default" | "highlighted"; - onClose?: () => void; -} - -function Card({ title, children, variant = "default", onClose }: CardProps) { - return ( - <div className={`card card-${variant}`}> - <h2>{title}</h2> - {onClose && <button onClick={onClose}>×</button>} - {children} - </div> - ); -} -``` - ---- - -### useState & useEffect Typen (15 min) -- Type inference bij useState: `useState(0)` → number -- Explicit types: `useState<User | null>(null)` -- Arrays: `useState<Product[]>([])` -- useEffect met async patterns - -```typescript -const [user, setUser] = useState<User | null>(null); -const [products, setProducts] = useState<Product[]>([]); -const [loading, setLoading] = useState(false); // inference: boolean - -useEffect(() => { - async function fetchData() { - setLoading(true); - const response = await fetch("/api/users"); - const data: User[] = await response.json(); - setUsers(data); - setLoading(false); - } - fetchData(); -}, []); -``` - ---- - -### Event Handlers Typen (10 min) -- `React.ChangeEvent<HTMLInputElement>` -- `React.FormEvent<HTMLFormElement>` -- `React.MouseEvent<HTMLButtonElement>` -- Tip: hover in Cursor om het juiste event type te vinden - -```typescript -function SearchForm() { - const [query, setQuery] = useState(""); - - const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => { - setQuery(e.target.value); - }; - - const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => { - e.preventDefault(); - console.log("Searching:", query); - }; - - return ( - <form onSubmit={handleSubmit}> - <input value={query} onChange={handleChange} /> - </form> - ); -} -``` - ---- - -### API Responses & Async Typen (15 min) -- `Promise<T>` voor async functies -- API response types definiëren -- Error handling met types -- Fetch wrapper met generics - -```typescript -interface ApiResponse<T> { - data: T; - status: number; - message: string; -} - -async function fetchApi<T>(url: string): Promise<ApiResponse<T>> { - const response = await fetch(url); - if (!response.ok) { - throw new Error(`HTTP error: ${response.status}`); - } - return response.json(); -} - -// Gebruik -const { data: users } = await fetchApi<User[]>("/api/users"); -const { data: product } = await fetchApi<Product>("/api/products/1"); -``` +Huiswerk: Stap 0-3 zelfstandig afmaken --- ## Tools -- Cursor (Student Plan) +- Next.js 15 +- Cursor - TypeScript -- React (via CDN of Vite) - ---- - -## Lesopdracht (75 min) - -### Typed React Dashboard - -Studenten bouwen een kleine React-app met volledig getypte components: - -**Opdracht:** Bouw een Product Dashboard met: - -1. **`ProductCard` component** — props: Product interface, onAddToCart callback -2. **`ProductList` component** — props: Product[], filterCategory (union type) -3. **`SearchBar` component** — props: query string, onChange handler (getypt event) -4. **`useProducts` custom hook** — fetch products, return `{ products, loading, error }` -5. **Alle types in een apart `types.ts` bestand** - -**Vereisten:** -- Geen `any` toegestaan -- Alle event handlers correct getypt -- useState met expliciete types waar nodig -- Minstens 1 generic functie (bijv. een `sortBy<T>` of `filterBy<T>`) - ---- - -## Huiswerk (2 uur) - -### Extend het Dashboard - -Bouw voort op de lesopdracht: - -1. **Shopping Cart** toevoegen met getypte state (`CartItem[]`) -2. **API simulatie** — maak een `fetchProducts()` functie met `Promise<Product[]>` -3. **Utility types gebruiken** — `Partial<Product>` voor updates, `Omit<Product, "id">` voor create -4. **Bonus: Generic `DataTable<T>` component** — werkt met elke array van objecten - -### Deliverable -- Werkend React project met TypeScript -- Alle components volledig getypt -- `npm run check` = 0 errors +- Tailwind CSS --- ## Leerdoelen + Na deze les kan de student: -- Generics schrijven en toepassen -- Utility types gebruiken (Partial, Pick, Omit, Record) -- React component props correct typen -- useState en useEffect met types gebruiken -- Event handlers typen (ChangeEvent, FormEvent, MouseEvent) -- Async functies en API responses typen met Promise<T> -- Een custom hook schrijven met correcte return types +- Uitleggen wat Next.js toevoegt aan React +- Een Next.js project opzetten met App Router +- Verschil tussen Server en Client Components +- File-based routing gebruiken +- Dynamic routes met parameters maken +- Data fetchen in Server Components diff --git a/Samenvattingen/Les06-Samenvatting.md b/Samenvattingen/Les06-Samenvatting.md index 3a4e87c..a596d0e 100644 --- a/Samenvattingen/Les06-Samenvatting.md +++ b/Samenvattingen/Les06-Samenvatting.md @@ -1,365 +1,59 @@ -# Les 6: Next.js Fundamentals 1 - SSR & Routing +# Les 6: Next.js — QuickPoll Vervolg (Part 2) --- ## Hoofdstuk -**Deel 2: Technical Foundations** (Les 5-9) +**Deel 2: Technical Foundations** (Les 4-8) ## Beschrijving -Introductie tot Next.js voor React developers. Leer wat Server-Side Rendering is, hoe de App Router werkt, en bouw je eerste Next.js applicatie met meerdere pagina's. +Vervolg Next.js: API Routes, Middleware, Deployment. Hands-on: QuickPoll app Part 2 (stap 4-7) klassikaal afbouwen en deployen. --- -## Te Behandelen +## Te Behandelen (~30-40 min) -### Wat is Next.js? - -**React met superpowers:** -- React = library voor UI components -- Next.js = framework dat React complete maakt - -**Next.js voegt toe:** -- Routing (geen extra library nodig) -- Server-Side Rendering (SEO, performance) -- API routes (backend in je frontend project) -- Optimalisaties (images, fonts, bundling) +- Recap Les 5 + Q&A +- API Route Handlers dieper: GET, POST met NextResponse, request body parsen +- Middleware: src/middleware.ts, matcher config, use cases +- Environment Variables: .env.local, NEXT_PUBLIC_ prefix +- Loading, Error, Not-Found special files +- next/image, next/link optimalisaties +- Metadata type voor SEO +- Deployment op Vercel (git push → auto deploy) +- Cursor/AI workflow: .cursorrules, Cmd+K, Cmd+L --- -### Server-Side Rendering (SSR) vs Client-Side Rendering (CSR) +## Lesopdracht (120 min, klassikaal) -**Client-Side Rendering (gewone React):** -``` -1. Browser vraagt pagina -2. Server stuurt lege HTML + JavaScript -3. JavaScript laadt in browser -4. JavaScript rendert de pagina -5. Gebruiker ziet content -``` -**Probleem:** Lege pagina tot JS klaar is. Google ziet ook lege pagina. +### QuickPoll App Part 2 — samen met Tim -**Server-Side Rendering (Next.js):** -``` -1. Browser vraagt pagina -2. Server rendert HTML met content -3. Browser toont direct de content -4. JavaScript laadt (hydration) -5. Pagina wordt interactief -``` -**Voordeel:** Direct content zichtbaar, beter voor SEO. +- **Stap 4:** API POST vote route (validation, votePoll) +- **Stap 5:** Poll detail pagina (server component met data) +- **Stap 6:** VoteForm (client component, fetch, results display) +- **Stap 7:** Loading, error, not-found states +- **Bonus:** Create poll pagina +- **Deploy naar Vercel** ---- - -### De App Router - -Next.js 13+ gebruikt de "App Router" met file-based routing: - -``` -app/ -├── page.tsx → / -├── about/ -│ └── page.tsx → /about -├── products/ -│ ├── page.tsx → /products -│ └── [id]/ -│ └── page.tsx → /products/123 -└── layout.tsx → Wrapper voor alle pagina's -``` - -**De regel:** Elke `page.tsx` wordt een route! - ---- - -### Project Aanmaken - -```bash -npx create-next-app@latest mijn-app - -# Antwoorden: -# ✔ TypeScript? → Yes -# ✔ ESLint? → Yes -# ✔ Tailwind CSS? → Yes -# ✔ `src/` directory? → Yes -# ✔ App Router? → Yes -# ✔ Customize import alias? → No - -cd mijn-app -npm run dev -``` - -Open `http://localhost:3000` - je app draait! - ---- - -### Pagina's Maken - -**Simpele pagina (`app/about/page.tsx`):** -```tsx -export default function AboutPage() { - return ( - <div className="p-8"> - <h1 className="text-3xl font-bold">Over Ons</h1> - <p>Welkom op de about pagina!</p> - </div> - ) -} -``` - -Dat is alles! Ga naar `/about` en je ziet je pagina. - ---- - -### Layouts - -Layouts wrappen pagina's en blijven behouden tijdens navigatie. - -**Root Layout (`app/layout.tsx`):** -```tsx -export default function RootLayout({ - children, -}: { - children: React.ReactNode -}) { - return ( - <html lang="nl"> - <body> - <header className="p-4 bg-blue-500 text-white"> - <nav>Mijn App</nav> - </header> - <main>{children}</main> - <footer className="p-4 bg-gray-200"> - © 2024 - </footer> - </body> - </html> - ) -} -``` - ---- - -### Nested Layouts - -Je kunt layouts nesten voor secties: - -``` -app/ -├── layout.tsx → Root layout -├── page.tsx → Homepage -└── dashboard/ - ├── layout.tsx → Dashboard layout (sidebar) - └── page.tsx → Dashboard pagina -``` - -**Dashboard Layout (`app/dashboard/layout.tsx`):** -```tsx -export default function DashboardLayout({ - children, -}: { - children: React.ReactNode -}) { - return ( - <div className="flex"> - <aside className="w-64 bg-gray-100 p-4"> - <nav>Sidebar hier</nav> - </aside> - <div className="flex-1">{children}</div> - </div> - ) -} -``` - ---- - -### Dynamic Routes - -Voor pagina's met parameters zoals `/products/123`: - -**Folder structuur:** -``` -app/products/[id]/page.tsx -``` - -**De pagina:** -```tsx -interface Props { - params: { id: string } -} - -export default function ProductPage({ params }: Props) { - return ( - <div> - <h1>Product {params.id}</h1> - </div> - ) -} -``` - -Nu werkt `/products/1`, `/products/abc`, etc. - ---- - -### Link Component - -Gebruik `Link` voor client-side navigatie (snel, geen page reload): - -```tsx -import Link from 'next/link' - -export default function Navigation() { - return ( - <nav className="flex gap-4"> - <Link href="/">Home</Link> - <Link href="/about">About</Link> - <Link href="/products">Products</Link> - <Link href="/products/123">Product 123</Link> - </nav> - ) -} -``` - -**Niet doen:** -```tsx -// ❌ Dit werkt maar is langzamer -<a href="/about">About</a> -``` - ---- - -### Special Files - -Next.js heeft speciale bestanden: - -| File | Doel | -|------|------| -| `page.tsx` | De pagina content | -| `layout.tsx` | Wrapper, blijft behouden | -| `loading.tsx` | Loading state | -| `error.tsx` | Error boundary | -| `not-found.tsx` | 404 pagina | - -**Loading state (`app/products/loading.tsx`):** -```tsx -export default function Loading() { - return <div>Laden...</div> -} -``` - ---- - -### Metadata (SEO) - -Voeg metadata toe per pagina: - -```tsx -import type { Metadata } from 'next' - -export const metadata: Metadata = { - title: 'Over Ons | Mijn App', - description: 'Lees meer over ons bedrijf', -} - -export default function AboutPage() { - return <h1>Over Ons</h1> -} -``` +Huiswerk: App afmaken, deployen op Vercel, bonus features --- ## Tools -- Next.js 14 +- Next.js 15 +- Cursor - TypeScript - Tailwind CSS -- OpenCode/WebStorm - Vercel --- -## Lesopdracht (2 uur) - -### Bouw Multi-Page Next.js App - -**Deel 1: Project Setup (20 min)** - -1. `npx create-next-app@latest my-shop` (TypeScript + Tailwind + App Router) -2. Open in editor -3. `npm run dev` -4. Verifieer: `localhost:3000` werkt - -**Deel 2: Pagina's Maken (40 min)** - -Maak deze pagina's: -1. `/` - Homepage met welkomstbericht -2. `/about` - Over ons pagina -3. `/products` - Producten overzicht -4. `/contact` - Contact pagina - -Elke pagina moet unieke content hebben. - -**Deel 3: Layout met Navigatie (30 min)** - -1. Maak Header component met navigatie (gebruik Link) -2. Maak Footer component -3. Voeg beide toe aan root layout -4. Test: navigatie werkt zonder page reload - -**Deel 4: Dynamic Route (30 min)** - -1. Maak `/products/[id]` route -2. Toon product ID op de pagina -3. Maak links naar `/products/1`, `/products/2`, `/products/3` -4. Test: alle links werken - -**Bonus:** Voeg `loading.tsx` toe aan products folder - -### Deliverable -- Werkende Next.js app met 4+ pagina's -- Navigatie met Link component -- Dynamic route die werkt -- Deploy naar Vercel - ---- - -## Huiswerk (2 uur) - -### Uitbreiden en Verdiepen - -**Deel 1: Nested Layout (45 min)** - -1. Maak `/dashboard` section met eigen layout -2. Dashboard layout heeft sidebar -3. Maak `/dashboard/settings` en `/dashboard/profile` pagina's -4. Test: sidebar blijft bij navigatie binnen dashboard - -**Deel 2: Special Files (45 min)** - -1. Maak `loading.tsx` voor products (toon "Laden...") -2. Maak `error.tsx` voor products (toon error message) -3. Maak `not-found.tsx` in app root (custom 404) -4. Test elk van deze states - -**Deel 3: Metadata (30 min)** - -1. Voeg metadata toe aan elke pagina (title, description) -2. Bekijk in browser: `<head>` toont juiste meta tags -3. Test met Lighthouse: SEO score - -### Deliverable -- App met nested layouts -- Special files (loading, error, not-found) -- Metadata op elke pagina -- Screenshot van Lighthouse SEO score - ---- - ## Leerdoelen + Na deze les kan de student: -- Uitleggen wat Next.js toevoegt aan React -- Het verschil tussen SSR en CSR beschrijven -- Een Next.js project opzetten met App Router -- Pagina's maken met file-based routing -- Layouts gebruiken voor herhalende elementen -- Dynamic routes maken met parameters -- Link component gebruiken voor navigatie -- Special files toepassen (loading, error, not-found) -- Metadata toevoegen voor SEO +- API Route Handlers schrijven (GET, POST) +- Middleware implementeren +- Environment variables gebruiken +- Special files (loading, error, not-found) toepassen +- Een Next.js app deployen op Vercel +- Cursor/AI workflow toepassen diff --git a/Samenvattingen/Les07-Samenvatting.md b/Samenvattingen/Les07-Samenvatting.md index de1fc4b..ce0a859 100644 --- a/Samenvattingen/Les07-Samenvatting.md +++ b/Samenvattingen/Les07-Samenvatting.md @@ -1,412 +1,372 @@ -# Les 7: Next.js Fundamentals 2 - API Routes & Data Fetching +# Les 7: Database Principles --- ## Hoofdstuk -**Deel 2: Technical Foundations** (Les 5-9) +**Deel 2: Technical Foundations** (Les 4-8) ## Beschrijving -Leer data fetching in Next.js: Server Components, Client Components, API routes en React Query. Bouw een volledig werkende app met data. +Leer de basisprincipes van relationele databases voordat we Supabase gaan gebruiken. Begrijp tabellen, relaties, keys en normalisatie - essentiële kennis voor elke developer. --- ## Te Behandelen -### Server Components vs Client Components +### Wat is een Relationele Database? -**Server Components (default in Next.js):** -- Renderen op de server -- Geen JavaScript naar de browser -- Kunnen direct data fetchen (async/await) -- Kunnen NIET: useState, useEffect, event handlers +**Een database is:** Een georganiseerde verzameling van data. -**Client Components:** -- Renderen in de browser -- JavaScript naar de browser -- Kunnen interactief zijn -- Markeer met `'use client'` bovenaan +**Relationeel betekent:** Data is opgeslagen in tabellen die aan elkaar gerelateerd zijn. + +**Vergelijk het met Excel:** +- Database = Excel workbook +- Tabel = Excel sheet +- Kolom = Excel kolom (field) +- Rij = Excel rij (record) --- -### Wanneer Wat? +### Tabellen, Kolommen en Rijen -``` -Server Component → Data tonen, geen interactie -Client Component → Interactie nodig (forms, buttons, state) -``` +**Voorbeeld: Users tabel** + +| id | name | email | created_at | +|----|------|-------|------------| +| 1 | Tim | tim@email.com | 2024-01-15 | +| 2 | Anna | anna@email.com | 2024-01-16 | +| 3 | Jan | jan@email.com | 2024-01-17 | + +**Terminologie:** +- **Tabel:** users +- **Kolommen:** id, name, email, created_at +- **Rijen:** 3 records (Tim, Anna, Jan) +- **Cell:** Eén specifieke waarde (bijv. "tim@email.com") + +--- + +### Data Types + +Elke kolom heeft een type: + +| Type | Beschrijving | Voorbeeld | +|------|--------------|-----------| +| `text` / `varchar` | Tekst | "Tim", "Hello world" | +| `integer` / `int` | Hele getallen | 1, 42, -5 | +| `decimal` / `numeric` | Decimalen | 19.99, 3.14 | +| `boolean` | True/False | true, false | +| `timestamp` | Datum + tijd | 2024-01-15 14:30:00 | +| `uuid` | Unieke identifier | a1b2c3d4-e5f6-... | + +**Kies het juiste type:** +- Prijs? → `decimal` (niet `integer`, want centen) +- Is actief? → `boolean` +- Naam? → `text` +- Aantal? → `integer` + +--- + +### Primary Keys + +**Wat:** Een kolom die elke rij UNIEK identificeert. + +**Regels:** +- Moet uniek zijn per rij +- Mag nooit NULL zijn +- Verandert nooit **Voorbeeld:** -```tsx -// Server Component - data ophalen -async function ProductList() { - const products = await fetchProducts() // Direct fetchen! - return <ul>{products.map(p => <li>{p.name}</li>)}</ul> -} +``` +users +------ +id (PRIMARY KEY) | name | email +1 | Tim | tim@email.com +2 | Anna | anna@email.com +``` -// Client Component - interactie -'use client' -function AddToCartButton({ productId }) { - const [added, setAdded] = useState(false) - return <button onClick={() => setAdded(true)}>Add</button> -} +**Waarom niet `email` als primary key?** +- Emails kunnen veranderen +- `id` is stabiel en snel + +--- + +### Foreign Keys + +**Wat:** Een kolom die verwijst naar de primary key van een andere tabel. + +**Voorbeeld: Posts tabel** +``` +posts +------ +id | title | user_id (FOREIGN KEY → users.id) +1 | "Mijn blog" | 1 +2 | "Hello world" | 1 +3 | "Tips" | 2 +``` + +**Wat zegt dit?** +- Post 1 en 2 zijn van user 1 (Tim) +- Post 3 is van user 2 (Anna) + +--- + +### Relatie Types + +**One-to-Many (1:N)** - Meest voorkomend! +``` +Eén user → meerdere posts +Eén category → meerdere products +``` + +**One-to-One (1:1)** - Zeldzaam +``` +Eén user → één profile +``` + +**Many-to-Many (N:N)** - Via tussentabel +``` +Posts ↔ Tags (een post heeft meerdere tags, een tag heeft meerdere posts) ``` --- -### Data Fetching in Server Components +### One-to-Many Voorbeeld -Simpelweg `async/await` gebruiken: +``` +users posts +------ ------ +id | name id | title | user_id +1 | Tim ←────────── 1 | "Blog 1" | 1 +2 | Anna ←────┬───── 2 | "Blog 2" | 1 + └───── 3 | "Tips" | 2 +``` -```tsx -// app/products/page.tsx -interface Product { - id: number - name: string - price: number -} +**Lees:** Tim heeft 2 posts, Anna heeft 1 post. -async function getProducts(): Promise<Product[]> { - const res = await fetch('https://api.example.com/products') - return res.json() -} +--- -export default async function ProductsPage() { - const products = await getProducts() +### Many-to-Many met Tussentabel - return ( - <div> - <h1>Producten</h1> - <ul> - {products.map(product => ( - <li key={product.id}> - {product.name} - €{product.price} - </li> - ))} - </ul> - </div> - ) -} +``` +posts post_tags tags +------ --------- ------ +id | title post_id | tag_id id | name +1 | "React tips" 1 | 1 1 | "react" +2 | "CSS guide" 1 | 2 2 | "frontend" + 2 | 2 3 | "css" + 2 | 3 +``` + +**Lees:** +- Post 1 heeft tags: react, frontend +- Post 2 heeft tags: frontend, css + +--- + +### Normalisatie Basics + +**Probleem: Data duplicatie** +``` +orders (SLECHT) +------ +id | customer_name | customer_email | product_name | price +1 | Tim | tim@email.com | Laptop | 999 +2 | Tim | tim@email.com | Phone | 699 +3 | Anna | anna@email.com | Laptop | 999 +``` + +**Problemen:** +- Tim's email staat 2x (als hij verandert: 2 plekken updaten) +- "Laptop" en prijs staan 2x + +--- + +### Genormaliseerde Versie + +``` +users products orders +------ -------- ------ +id | name | email id | name | price id | user_id | product_id +1 | Tim | tim@... 1 | Laptop | 999 1 | 1 | 1 +2 | Anna | anna@... 2 | Phone | 699 2 | 1 | 2 + 3 | 2 | 1 +``` + +**Voordelen:** +- Elk gegeven staat 1x +- Update op 1 plek +- Minder opslagruimte + +--- + +### NULL Values + +**NULL = "geen waarde" (niet 0, niet "")** + +``` +users +------ +id | name | phone +1 | Tim | 0612345678 +2 | Anna | NULL ← Geen telefoon bekend +``` + +**Wanneer NULL toestaan?** +- Optionele velden (phone, description) +- Niet bij verplichte velden (name, email) + +--- + +### Defaults + +**Automatische waarde als je niks opgeeft:** + +``` +todos +------ +id | title | completed | created_at + | | DEFAULT: false | DEFAULT: now() +``` + +Bij `INSERT INTO todos (title) VALUES ('Test')`: +``` +id | title | completed | created_at +1 | Test | false | 2024-01-15 10:30:00 ``` --- -### API Routes (Route Handlers) +### Database Schema Tekenen -Bouw je eigen API in Next.js: +**Tools:** draw.io, Excalidraw, pen en papier -**Folder structuur:** +**Conventie:** ``` -app/ -└── api/ - └── products/ - └── route.ts → /api/products +┌──────────────┐ ┌──────────────┐ +│ users │ │ posts │ +├──────────────┤ ├──────────────┤ +│ id (PK) │───┐ │ id (PK) │ +│ name │ │ │ title │ +│ email │ └────→│ user_id (FK) │ +│ created_at │ │ content │ +└──────────────┘ │ created_at │ + └──────────────┘ ``` -**GET request (`app/api/products/route.ts`):** -```typescript -import { NextResponse } from 'next/server' - -const products = [ - { id: 1, name: 'Laptop', price: 999 }, - { id: 2, name: 'Phone', price: 699 }, -] - -export async function GET() { - return NextResponse.json(products) -} -``` - -**POST request:** -```typescript -export async function POST(request: Request) { - const body = await request.json() - - const newProduct = { - id: Date.now(), - name: body.name, - price: body.price, - } - - products.push(newProduct) - - return NextResponse.json(newProduct, { status: 201 }) -} -``` - ---- - -### 'use client' Directive - -Wanneer je interactie nodig hebt: - -```tsx -'use client' // MOET bovenaan! - -import { useState } from 'react' - -export function Counter() { - const [count, setCount] = useState(0) - - return ( - <button onClick={() => setCount(count + 1)}> - Count: {count} - </button> - ) -} -``` - ---- - -### React Query (TanStack Query) - -**Waarom React Query?** -- Automatische caching -- Loading en error states -- Refetching (focus, interval) -- Optimistic updates - -**Installatie:** -```bash -npm install @tanstack/react-query -``` - -**Setup Provider (`app/providers.tsx`):** -```tsx -'use client' - -import { QueryClient, QueryClientProvider } from '@tanstack/react-query' -import { useState } from 'react' - -export function Providers({ children }: { children: React.ReactNode }) { - const [queryClient] = useState(() => new QueryClient()) - - return ( - <QueryClientProvider client={queryClient}> - {children} - </QueryClientProvider> - ) -} -``` - -**In Layout:** -```tsx -import { Providers } from './providers' - -export default function RootLayout({ children }) { - return ( - <html> - <body> - <Providers>{children}</Providers> - </body> - </html> - ) -} -``` - ---- - -### useQuery - Data Ophalen - -```tsx -'use client' - -import { useQuery } from '@tanstack/react-query' - -interface Product { - id: number - name: string - price: number -} - -async function fetchProducts(): Promise<Product[]> { - const res = await fetch('/api/products') - return res.json() -} - -export function ProductList() { - const { data, isLoading, error } = useQuery({ - queryKey: ['products'], - queryFn: fetchProducts, - }) - - if (isLoading) return <div>Laden...</div> - if (error) return <div>Error: {error.message}</div> - - return ( - <ul> - {data?.map(product => ( - <li key={product.id}>{product.name}</li> - ))} - </ul> - ) -} -``` - ---- - -### useMutation - Data Wijzigen - -```tsx -'use client' - -import { useMutation, useQueryClient } from '@tanstack/react-query' - -interface NewProduct { - name: string - price: number -} - -async function createProduct(product: NewProduct) { - const res = await fetch('/api/products', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(product), - }) - return res.json() -} - -export function AddProductForm() { - const queryClient = useQueryClient() - - const mutation = useMutation({ - mutationFn: createProduct, - onSuccess: () => { - // Invalidate and refetch - queryClient.invalidateQueries({ queryKey: ['products'] }) - }, - }) - - const handleSubmit = (e: React.FormEvent) => { - e.preventDefault() - mutation.mutate({ name: 'New Product', price: 99 }) - } - - return ( - <form onSubmit={handleSubmit}> - <button type="submit" disabled={mutation.isPending}> - {mutation.isPending ? 'Toevoegen...' : 'Voeg toe'} - </button> - </form> - ) -} -``` - ---- - -### Combineren: Server + Client - -```tsx -// app/products/page.tsx (Server Component) -import { ProductList } from './product-list' -import { AddProductForm } from './add-product-form' - -export default function ProductsPage() { - return ( - <div> - <h1>Producten</h1> - <AddProductForm /> {/* Client Component */} - <ProductList /> {/* Client Component */} - </div> - ) -} -``` +PK = Primary Key +FK = Foreign Key +Pijl = Relatie richting --- ## Tools -- Next.js 14 -- React Query (TanStack Query) -- TypeScript -- OpenCode/WebStorm +- Pen en papier / Excalidraw / draw.io +- Supabase Table Editor (vooruitblik) --- ## Lesopdracht (2 uur) -### Bouw CRUD App met API Routes +### Database Design Oefening -**Deel 1: API Routes (40 min)** +**Deel 1: Blog Database Ontwerpen (45 min)** -1. Maak `app/api/products/route.ts` -2. Implementeer GET (alle producten) -3. Implementeer POST (product toevoegen) -4. Test met browser/Postman: `/api/products` +Ontwerp een database voor een blog met: +- Users (kunnen posts schrijven) +- Posts (hebben een auteur) +- Comments (op posts, door users) -**Deel 2: React Query Setup (20 min)** +Voor elke tabel: +1. Teken de tabel met kolommen +2. Bepaal data types +3. Markeer Primary Keys +4. Markeer Foreign Keys +5. Teken de relaties -1. Installeer `@tanstack/react-query` -2. Maak `app/providers.tsx` -3. Wrap app in `QueryClientProvider` +**Deel 2: Normalisatie Oefening (30 min)** -**Deel 3: Data Tonen met useQuery (30 min)** +Gegeven deze "slechte" tabel: -1. Maak `ProductList` Client Component -2. Gebruik `useQuery` om data te fetchen -3. Toon loading state -4. Toon error state -5. Render product lijst +``` +library +------- +book_title | author_name | author_email | borrower_name | borrowed_date +"1984" | "Orwell" | orwell@... | "Tim" | 2024-01-15 +"1984" | "Orwell" | orwell@... | "Anna" | 2024-01-10 +"Dune" | "Herbert" | herbert@... | "Tim" | 2024-01-12 +``` -**Deel 4: Data Toevoegen met useMutation (30 min)** +Normaliseer naar aparte tabellen: +1. Welke entiteiten zie je? +2. Maak aparte tabellen +3. Voeg relaties toe -1. Maak `AddProductForm` Client Component -2. Gebruik `useMutation` voor POST -3. Invalidate query na success -4. Toon "Adding..." state +**Deel 3: Eindproject Schema (45 min)** + +Ontwerp het database schema voor jouw eindproject: +1. Welke entiteiten heb je nodig? +2. Teken elke tabel met kolommen +3. Bepaal relaties +4. Documenteer je keuzes ### Deliverable -- Werkende API routes (GET, POST) -- ProductList met useQuery -- AddProductForm met useMutation -- Loading en error states +- Blog database schema (tekening) +- Genormaliseerde library database +- Eindproject database schema --- ## Huiswerk (2 uur) -### Volledige CRUD Interface +### Verdieping en Voorbereiding -**Deel 1: PUT en DELETE Routes (45 min)** +**Deel 1: Eindproject Schema Uitwerken (1 uur)** -1. Maak `app/api/products/[id]/route.ts` -2. Implementeer PUT (update product) -3. Implementeer DELETE (verwijder product) -4. Test beide endpoints +Werk je database schema volledig uit: -**Deel 2: Update Functionaliteit (45 min)** +1. **Per tabel:** + - Naam + - Alle kolommen met data types + - Primary key + - Foreign keys + - Defaults + - Nullable fields -1. Maak edit form in ProductList -2. Gebruik useMutation voor PUT -3. Inline editing OF modal -4. Invalidate query na success +2. **Documenteer:** + - Waarom deze structuur? + - Welke relaties? + - Eventuele alternatieve overwegingen -**Deel 3: Delete Functionaliteit (30 min)** +**Deel 2: Supabase Account (30 min)** -1. Voeg delete button toe per product -2. Gebruik useMutation voor DELETE -3. Voeg confirmation dialog toe -4. Invalidate query na success +Bereid je voor op volgende les: +1. Maak account op [supabase.com](https://supabase.com) +2. Verken de interface +3. Bekijk de Table Editor -**Bonus:** Optimistic Updates -- Product direct uit UI verwijderen -- Rollback als server faalt +**Deel 3: Reflectie (30 min)** + +Beantwoord deze vragen (kort): +1. Wat is het verschil tussen primary en foreign key? +2. Waarom normaliseren we data? +3. Wanneer gebruik je one-to-many vs many-to-many? +4. Welke tabellen heeft jouw eindproject nodig? ### Deliverable -- Complete CRUD API (GET, POST, PUT, DELETE) -- UI voor alle operaties -- Error handling -- Optimistic updates (bonus) +- Volledig uitgewerkt database schema voor eindproject +- Supabase account aangemaakt +- Reflectie vragen beantwoord --- ## Leerdoelen Na deze les kan de student: -- Uitleggen wanneer Server vs Client Components -- De 'use client' directive correct gebruiken -- Data fetchen in Server Components met async/await -- API routes maken met Route Handlers -- GET en POST requests implementeren -- React Query installeren en configureren -- useQuery gebruiken voor data fetching -- useMutation gebruiken voor data mutations -- Loading en error states afhandelen -- Query invalidation toepassen +- Uitleggen wat een relationele database is +- Tabellen, kolommen en rijen beschrijven +- De juiste data types kiezen +- Primary keys en hun doel uitleggen +- Foreign keys en relaties begrijpen +- One-to-many en many-to-many relaties herkennen +- Het probleem van data duplicatie identificeren +- Een database normaliseren +- NULL values en defaults begrijpen +- Een database schema ontwerpen en tekenen diff --git a/Samenvattingen/Les08-Samenvatting.md b/Samenvattingen/Les08-Samenvatting.md index 80e9a16..3d07eba 100644 --- a/Samenvattingen/Les08-Samenvatting.md +++ b/Samenvattingen/Les08-Samenvatting.md @@ -1,372 +1,305 @@ -# Les 8: Database Principles +# Les 8: Supabase: Auth & CRUD --- ## Hoofdstuk -**Deel 2: Technical Foundations** (Les 5-9) +**Deel 2: Technical Foundations** (Les 4-8) ## Beschrijving -Leer de basisprincipes van relationele databases voordat we Supabase gaan gebruiken. Begrijp tabellen, relaties, keys en normalisatie - essentiële kennis voor elke developer. +Supabase Authentication en CRUD operaties. Implementeer email/password auth, JWT tokens, Row Level Security (RLS) policies, realtime subscriptions en volledige CRUD functionaliteit in je full-stack app. --- ## Te Behandelen -### Wat is een Relationele Database? +### Wat is Supabase? -**Een database is:** Een georganiseerde verzameling van data. +**Supabase = Database + Auth in één** +- PostgreSQL database (gratis tier: 500MB) +- Ingebouwde authenticatie +- Real-time subscriptions +- File storage +- Auto-generated API -**Relationeel betekent:** Data is opgeslagen in tabellen die aan elkaar gerelateerd zijn. - -**Vergelijk het met Excel:** -- Database = Excel workbook -- Tabel = Excel sheet -- Kolom = Excel kolom (field) -- Rij = Excel rij (record) +**Waarom Supabase voor beginners:** +- Geen eigen server nodig +- Visuele Table Editor (geen SQL kennis nodig) +- Simpele JavaScript SDK +- Gratis tier is ruim voldoende --- -### Tabellen, Kolommen en Rijen +### Supabase Project Aanmaken -**Voorbeeld: Users tabel** +**Stap 1:** Ga naar [supabase.com](https://supabase.com) en maak account -| id | name | email | created_at | -|----|------|-------|------------| -| 1 | Tim | tim@email.com | 2024-01-15 | -| 2 | Anna | anna@email.com | 2024-01-16 | -| 3 | Jan | jan@email.com | 2024-01-17 | +**Stap 2:** Klik "New Project" +- Naam: `todo-app` +- Database Password: (bewaar deze!) +- Region: `West EU (Frankfurt)` (dichtst bij NL) -**Terminologie:** -- **Tabel:** users -- **Kolommen:** id, name, email, created_at -- **Rijen:** 3 records (Tim, Anna, Jan) -- **Cell:** Eén specifieke waarde (bijv. "tim@email.com") +**Stap 3:** Wacht ~2 minuten tot project klaar is + +**Stap 4:** Ga naar Settings → API en kopieer: +- `Project URL` +- `anon public` key --- -### Data Types +### Je Database Schema Implementeren -Elke kolom heeft een type: +In Les 7 heb je een database schema ontworpen. Nu gaan we dat implementeren! -| Type | Beschrijving | Voorbeeld | -|------|--------------|-----------| -| `text` / `varchar` | Tekst | "Tim", "Hello world" | -| `integer` / `int` | Hele getallen | 1, 42, -5 | -| `decimal` / `numeric` | Decimalen | 19.99, 3.14 | -| `boolean` | True/False | true, false | -| `timestamp` | Datum + tijd | 2024-01-15 14:30:00 | -| `uuid` | Unieke identifier | a1b2c3d4-e5f6-... | +**In Supabase Dashboard → Table Editor:** -**Kies het juiste type:** -- Prijs? → `decimal` (niet `integer`, want centen) -- Is actief? → `boolean` -- Naam? → `text` -- Aantal? → `integer` +1. Klik "New Table" +2. Gebruik je schema uit Les 7 +3. Voeg kolommen toe met de juiste types +4. Definieer Primary Keys en Foreign Keys + +**Voorbeeld: todos tabel** + +| Name | Type | Default | Primary | +|------|------|---------|---------| +| id | int8 | - | ✓ (auto) | +| title | text | - | | +| completed | bool | false | | +| created_at | timestamptz | now() | | --- -### Primary Keys +### Environment Variables -**Wat:** Een kolom die elke rij UNIEK identificeert. +**Wat zijn environment variables?** +- Configuratie die NIET in je code hoort +- API keys, database URLs, secrets +- Verschillend per omgeving (lokaal vs productie) -**Regels:** -- Moet uniek zijn per rij -- Mag nooit NULL zijn -- Verandert nooit - -**Voorbeeld:** -``` -users ------- -id (PRIMARY KEY) | name | email -1 | Tim | tim@email.com -2 | Anna | anna@email.com +**Maak `.env.local` in je project root:** +```bash +# .env.local - NOOIT committen naar Git! +NEXT_PUBLIC_SUPABASE_URL=https://xxxxx.supabase.co +NEXT_PUBLIC_SUPABASE_ANON_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.xxxxx ``` -**Waarom niet `email` als primary key?** -- Emails kunnen veranderen -- `id` is stabiel en snel - ---- - -### Foreign Keys - -**Wat:** Een kolom die verwijst naar de primary key van een andere tabel. - -**Voorbeeld: Posts tabel** -``` -posts ------- -id | title | user_id (FOREIGN KEY → users.id) -1 | "Mijn blog" | 1 -2 | "Hello world" | 1 -3 | "Tips" | 2 -``` - -**Wat zegt dit?** -- Post 1 en 2 zijn van user 1 (Tim) -- Post 3 is van user 2 (Anna) - ---- - -### Relatie Types - -**One-to-Many (1:N)** - Meest voorkomend! -``` -Eén user → meerdere posts -Eén category → meerdere products -``` - -**One-to-One (1:1)** - Zeldzaam -``` -Eén user → één profile -``` - -**Many-to-Many (N:N)** - Via tussentabel -``` -Posts ↔ Tags (een post heeft meerdere tags, een tag heeft meerdere posts) +**Maak ook `.env.example` (WEL committen):** +```bash +# .env.example - template voor anderen +NEXT_PUBLIC_SUPABASE_URL=your-supabase-url +NEXT_PUBLIC_SUPABASE_ANON_KEY=your-anon-key ``` --- -### One-to-Many Voorbeeld +### Supabase SDK Installeren -``` -users posts ------- ------ -id | name id | title | user_id -1 | Tim ←────────── 1 | "Blog 1" | 1 -2 | Anna ←────┬───── 2 | "Blog 2" | 1 - └───── 3 | "Tips" | 2 +```bash +npm install @supabase/supabase-js ``` -**Lees:** Tim heeft 2 posts, Anna heeft 1 post. +**Maak `src/lib/supabase.ts`:** +```typescript +import { createClient } from '@supabase/supabase-js' ---- +const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL! +const supabaseAnonKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY! -### Many-to-Many met Tussentabel - -``` -posts post_tags tags ------- --------- ------ -id | title post_id | tag_id id | name -1 | "React tips" 1 | 1 1 | "react" -2 | "CSS guide" 1 | 2 2 | "frontend" - 2 | 2 3 | "css" - 2 | 3 -``` - -**Lees:** -- Post 1 heeft tags: react, frontend -- Post 2 heeft tags: frontend, css - ---- - -### Normalisatie Basics - -**Probleem: Data duplicatie** -``` -orders (SLECHT) ------- -id | customer_name | customer_email | product_name | price -1 | Tim | tim@email.com | Laptop | 999 -2 | Tim | tim@email.com | Phone | 699 -3 | Anna | anna@email.com | Laptop | 999 -``` - -**Problemen:** -- Tim's email staat 2x (als hij verandert: 2 plekken updaten) -- "Laptop" en prijs staan 2x - ---- - -### Genormaliseerde Versie - -``` -users products orders ------- -------- ------ -id | name | email id | name | price id | user_id | product_id -1 | Tim | tim@... 1 | Laptop | 999 1 | 1 | 1 -2 | Anna | anna@... 2 | Phone | 699 2 | 1 | 2 - 3 | 2 | 1 -``` - -**Voordelen:** -- Elk gegeven staat 1x -- Update op 1 plek -- Minder opslagruimte - ---- - -### NULL Values - -**NULL = "geen waarde" (niet 0, niet "")** - -``` -users ------- -id | name | phone -1 | Tim | 0612345678 -2 | Anna | NULL ← Geen telefoon bekend -``` - -**Wanneer NULL toestaan?** -- Optionele velden (phone, description) -- Niet bij verplichte velden (name, email) - ---- - -### Defaults - -**Automatische waarde als je niks opgeeft:** - -``` -todos ------- -id | title | completed | created_at - | | DEFAULT: false | DEFAULT: now() -``` - -Bij `INSERT INTO todos (title) VALUES ('Test')`: -``` -id | title | completed | created_at -1 | Test | false | 2024-01-15 10:30:00 +export const supabase = createClient(supabaseUrl, supabaseAnonKey) ``` --- -### Database Schema Tekenen +### CRUD Operaties -**Tools:** draw.io, Excalidraw, pen en papier - -**Conventie:** -``` -┌──────────────┐ ┌──────────────┐ -│ users │ │ posts │ -├──────────────┤ ├──────────────┤ -│ id (PK) │───┐ │ id (PK) │ -│ name │ │ │ title │ -│ email │ └────→│ user_id (FK) │ -│ created_at │ │ content │ -└──────────────┘ │ created_at │ - └──────────────┘ +**C - Create (toevoegen):** +```typescript +const { data, error } = await supabase + .from('todos') + .insert({ title: 'Nieuwe taak' }) ``` -PK = Primary Key -FK = Foreign Key -Pijl = Relatie richting +**R - Read (ophalen):** +```typescript +const { data, error } = await supabase + .from('todos') + .select('*') + .order('created_at', { ascending: false }) +``` + +**U - Update (wijzigen):** +```typescript +const { data, error } = await supabase + .from('todos') + .update({ completed: true }) + .eq('id', todoId) +``` + +**D - Delete (verwijderen):** +```typescript +const { error } = await supabase + .from('todos') + .delete() + .eq('id', todoId) +``` + +--- + +### Authenticatie met Auth UI + +**Installeer auth packages:** +```bash +npm install @supabase/auth-ui-react @supabase/auth-ui-shared +``` + +**Login component:** +```tsx +import { Auth } from '@supabase/auth-ui-react' +import { ThemeSupa } from '@supabase/auth-ui-shared' +import { supabase } from '@/lib/supabase' + +export function LoginForm() { + return ( + <Auth + supabaseClient={supabase} + appearance={{ theme: ThemeSupa }} + providers={[]} + magicLink={true} + /> + ) +} +``` + +**Huidige user checken:** +```typescript +const { data: { user } } = await supabase.auth.getUser() + +if (user) { + // User is ingelogd + console.log('Logged in as:', user.email) +} else { + // Redirect naar login +} +``` + +--- + +### Deployment naar Vercel + +**Stap 1: Push naar GitHub** +```bash +git add . +git commit -m "Add Supabase integration" +git push +``` + +**Stap 2: Deploy op Vercel** +1. Ga naar [vercel.com](https://vercel.com) +2. "Add New Project" +3. Import je GitHub repo +4. **BELANGRIJK:** Voeg Environment Variables toe! + - `NEXT_PUBLIC_SUPABASE_URL` + - `NEXT_PUBLIC_SUPABASE_ANON_KEY` +5. Klik "Deploy" + +**Stap 3: Supabase Redirect URLs** +1. Ga naar Supabase → Authentication → URL Configuration +2. Voeg toe bij "Redirect URLs": + - `https://jouw-app.vercel.app/**` --- ## Tools -- Pen en papier / Excalidraw / draw.io -- Supabase Table Editor (vooruitblik) +- Supabase +- Next.js +- OpenCode/WebStorm +- Vercel +- Git --- ## Lesopdracht (2 uur) -### Database Design Oefening +### Bouw een Todo App met Supabase -**Deel 1: Blog Database Ontwerpen (45 min)** +**Groepsdiscussie (15 min):** +Bespreek klassikaal de database schemas uit Les 7 - wie heeft welke structuur gekozen en waarom? -Ontwerp een database voor een blog met: -- Users (kunnen posts schrijven) -- Posts (hebben een auteur) -- Comments (op posts, door users) +**Deel 1: Supabase Setup (30 min)** -Voor elke tabel: -1. Teken de tabel met kolommen -2. Bepaal data types -3. Markeer Primary Keys -4. Markeer Foreign Keys -5. Teken de relaties +1. Maak Supabase account en project +2. Maak je tabellen via Table Editor (gebaseerd op Les 7 schema) +3. Kopieer credentials +4. Installeer `@supabase/supabase-js` +5. Maak `src/lib/supabase.ts` +6. Configureer `.env.local` -**Deel 2: Normalisatie Oefening (30 min)** +Test: `npm run dev` werkt zonder errors -Gegeven deze "slechte" tabel: +**Deel 2: CRUD Interface (1 uur)** -``` -library -------- -book_title | author_name | author_email | borrower_name | borrowed_date -"1984" | "Orwell" | orwell@... | "Tim" | 2024-01-15 -"1984" | "Orwell" | orwell@... | "Anna" | 2024-01-10 -"Dune" | "Herbert" | herbert@... | "Tim" | 2024-01-12 -``` +Bouw UI voor todos: +1. Lijst van todos tonen +2. Form om nieuwe todo toe te voegen +3. Checkbox om todo af te vinken +4. Delete button per todo -Normaliseer naar aparte tabellen: -1. Welke entiteiten zie je? -2. Maak aparte tabellen -3. Voeg relaties toe +Gebruik AI hulp voor de components! -**Deel 3: Eindproject Schema (45 min)** +**Deel 3: Authenticatie (30 min)** -Ontwerp het database schema voor jouw eindproject: -1. Welke entiteiten heb je nodig? -2. Teken elke tabel met kolommen -3. Bepaal relaties -4. Documenteer je keuzes +1. Installeer auth packages +2. Maak login pagina met Auth UI +3. Toon alleen app voor ingelogde users +4. Test: login met magic link ### Deliverable -- Blog database schema (tekening) -- Genormaliseerde library database -- Eindproject database schema +- Werkende Todo app lokaal +- GitHub repository met code +- Screenshot van werkende app --- ## Huiswerk (2 uur) -### Verdieping en Voorbereiding +### Deploy naar Productie + Uitbreiden -**Deel 1: Eindproject Schema Uitwerken (1 uur)** +**Deel 1: Deployment (30 min)** -Werk je database schema volledig uit: +1. Push naar GitHub +2. Deploy naar Vercel +3. Configureer env vars in Vercel +4. Voeg Vercel URL toe aan Supabase Redirect URLs +5. Test: app werkt op productie URL! -1. **Per tabel:** - - Naam - - Alle kolommen met data types - - Primary key - - Foreign keys - - Defaults - - Nullable fields +**Deel 2: Features Uitbreiden (1 uur)** -2. **Documenteer:** - - Waarom deze structuur? - - Welke relaties? - - Eventuele alternatieve overwegingen +Voeg toe: +1. Filter buttons: Alle / Actief / Voltooid +2. Sorteer op datum (nieuwste eerst) +3. Loading state tijdens data ophalen +4. Error state bij problemen +5. Empty state: "Geen todos gevonden" -**Deel 2: Supabase Account (30 min)** +**Deel 3: Polish (30 min)** -Bereid je voor op volgende les: -1. Maak account op [supabase.com](https://supabase.com) -2. Verken de interface -3. Bekijk de Table Editor - -**Deel 3: Reflectie (30 min)** - -Beantwoord deze vragen (kort): -1. Wat is het verschil tussen primary en foreign key? -2. Waarom normaliseren we data? -3. Wanneer gebruik je one-to-many vs many-to-many? -4. Welke tabellen heeft jouw eindproject nodig? +1. Styling verbeteren met Tailwind +2. Responsive design (mobile friendly) +3. Kleine animaties (fade in/out) ### Deliverable -- Volledig uitgewerkt database schema voor eindproject -- Supabase account aangemaakt -- Reflectie vragen beantwoord +- Deployed app op Vercel (werkende URL!) +- Alle features werken in productie +- Screenshot van productie app --- ## Leerdoelen Na deze les kan de student: -- Uitleggen wat een relationele database is -- Tabellen, kolommen en rijen beschrijven -- De juiste data types kiezen -- Primary keys en hun doel uitleggen -- Foreign keys en relaties begrijpen -- One-to-many en many-to-many relaties herkennen -- Het probleem van data duplicatie identificeren -- Een database normaliseren -- NULL values en defaults begrijpen -- Een database schema ontwerpen en tekenen +- Een Supabase project aanmaken en configureren +- Database schema implementeren via Table Editor +- Environment variables correct beheren +- De Supabase client installeren en configureren +- CRUD operaties uitvoeren met de Supabase SDK +- Authenticatie implementeren met Auth UI +- Deployen naar Vercel met environment variables +- Database principles uit Les 7 toepassen in de praktijk diff --git a/Samenvattingen/Les09-Samenvatting.md b/Samenvattingen/Les09-Samenvatting.md index 6eb40e4..d613fe1 100644 --- a/Samenvattingen/Les09-Samenvatting.md +++ b/Samenvattingen/Les09-Samenvatting.md @@ -1,305 +1,126 @@ -# Les 9: Supabase Basics +# Les 9: Full-Stack Mini Project --- ## Hoofdstuk -**Deel 2: Technical Foundations** (Les 5-9) +**Deel 3: Integration & AI Tooling** (Les 9-12) ## Beschrijving -Leer werken met Supabase: een complete backend-as-a-service met database en authenticatie. Pas je database schema uit Les 8 toe en bouw je eerste full-stack app. +Combineer alles wat je geleerd hebt (TypeScript, Next.js, Supabase) in een kleine werkende applicatie. Dit is je eerste "echte" full-stack project en voorbereiding op het werken met AI tools. --- ## Te Behandelen -### Wat is Supabase? +### Groepsdiscussie (15 min) +Bespreek klassikaal de Supabase ervaringen uit Les 8 - welke uitdagingen kwamen jullie tegen bij authenticatie en RLS? -**Supabase = Database + Auth in één** -- PostgreSQL database (gratis tier: 500MB) -- Ingebouwde authenticatie -- Real-time subscriptions -- File storage -- Auto-generated API +### Doel van deze les -**Waarom Supabase voor beginners:** -- Geen eigen server nodig -- Visuele Table Editor (geen SQL kennis nodig) -- Simpele JavaScript SDK -- Gratis tier is ruim voldoende +Je hebt nu alle bouwstenen: +- TypeScript (Les 4) +- Next.js met App Router (Les 5-6) +- Supabase database & auth (Les 7-8) + +Vandaag combineren we dit in een **werkende mini-app**. Geen nieuwe concepten - alleen integratie en toepassing. --- -### Supabase Project Aanmaken +### Mini Project: Personal Bookmarks -**Stap 1:** Ga naar [supabase.com](https://supabase.com) en maak account +Een simpele bookmark manager waar je links kunt opslaan. -**Stap 2:** Klik "New Project" -- Naam: `todo-app` -- Database Password: (bewaar deze!) -- Region: `West EU (Frankfurt)` (dichtst bij NL) +**Features:** +- Login met Supabase Auth +- Bookmarks toevoegen (URL + titel) +- Bookmarks bekijken en verwijderen -**Stap 3:** Wacht ~2 minuten tot project klaar is - -**Stap 4:** Ga naar Settings → API en kopieer: -- `Project URL` -- `anon public` key +**Tech stack:** +- Next.js 14 met App Router +- TypeScript +- Tailwind CSS +- Supabase (auth + database) +- React Query --- -### Je Database Schema Implementeren +### Stap-voor-stap -In Les 8 heb je een database schema ontworpen. Nu gaan we dat implementeren! +#### Database Schema -**In Supabase Dashboard → Table Editor:** +**Tabel: bookmarks** +| Kolom | Type | +|-------|------| +| id | uuid (PK) | +| user_id | uuid (FK naar auth.users) | +| url | text | +| title | text | +| created_at | timestamptz | -1. Klik "New Table" -2. Gebruik je schema uit Les 8 -3. Voeg kolommen toe met de juiste types -4. Definieer Primary Keys en Foreign Keys +**RLS:** Users kunnen alleen eigen bookmarks zien, toevoegen en verwijderen. -**Voorbeeld: todos tabel** +#### Wat je bouwt -| Name | Type | Default | Primary | -|------|------|---------|---------| -| id | int8 | - | ✓ (auto) | -| title | text | - | | -| completed | bool | false | | -| created_at | timestamptz | now() | | - ---- - -### Environment Variables - -**Wat zijn environment variables?** -- Configuratie die NIET in je code hoort -- API keys, database URLs, secrets -- Verschillend per omgeving (lokaal vs productie) - -**Maak `.env.local` in je project root:** -```bash -# .env.local - NOOIT committen naar Git! -NEXT_PUBLIC_SUPABASE_URL=https://xxxxx.supabase.co -NEXT_PUBLIC_SUPABASE_ANON_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.xxxxx -``` - -**Maak ook `.env.example` (WEL committen):** -```bash -# .env.example - template voor anderen -NEXT_PUBLIC_SUPABASE_URL=your-supabase-url -NEXT_PUBLIC_SUPABASE_ANON_KEY=your-anon-key -``` - ---- - -### Supabase SDK Installeren - -```bash -npm install @supabase/supabase-js -``` - -**Maak `src/lib/supabase.ts`:** -```typescript -import { createClient } from '@supabase/supabase-js' - -const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL! -const supabaseAnonKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY! - -export const supabase = createClient(supabaseUrl, supabaseAnonKey) -``` - ---- - -### CRUD Operaties - -**C - Create (toevoegen):** -```typescript -const { data, error } = await supabase - .from('todos') - .insert({ title: 'Nieuwe taak' }) -``` - -**R - Read (ophalen):** -```typescript -const { data, error } = await supabase - .from('todos') - .select('*') - .order('created_at', { ascending: false }) -``` - -**U - Update (wijzigen):** -```typescript -const { data, error } = await supabase - .from('todos') - .update({ completed: true }) - .eq('id', todoId) -``` - -**D - Delete (verwijderen):** -```typescript -const { error } = await supabase - .from('todos') - .delete() - .eq('id', todoId) -``` - ---- - -### Authenticatie met Auth UI - -**Installeer auth packages:** -```bash -npm install @supabase/auth-ui-react @supabase/auth-ui-shared -``` - -**Login component:** -```tsx -import { Auth } from '@supabase/auth-ui-react' -import { ThemeSupa } from '@supabase/auth-ui-shared' -import { supabase } from '@/lib/supabase' - -export function LoginForm() { - return ( - <Auth - supabaseClient={supabase} - appearance={{ theme: ThemeSupa }} - providers={[]} - magicLink={true} - /> - ) -} -``` - -**Huidige user checken:** -```typescript -const { data: { user } } = await supabase.auth.getUser() - -if (user) { - // User is ingelogd - console.log('Logged in as:', user.email) -} else { - // Redirect naar login -} -``` - ---- - -### Deployment naar Vercel - -**Stap 1: Push naar GitHub** -```bash -git add . -git commit -m "Add Supabase integration" -git push -``` - -**Stap 2: Deploy op Vercel** -1. Ga naar [vercel.com](https://vercel.com) -2. "Add New Project" -3. Import je GitHub repo -4. **BELANGRIJK:** Voeg Environment Variables toe! - - `NEXT_PUBLIC_SUPABASE_URL` - - `NEXT_PUBLIC_SUPABASE_ANON_KEY` -5. Klik "Deploy" - -**Stap 3: Supabase Redirect URLs** -1. Ga naar Supabase → Authentication → URL Configuration -2. Voeg toe bij "Redirect URLs": - - `https://jouw-app.vercel.app/**` +1. **Login pagina** - Supabase Auth +2. **Dashboard** - Lijst van bookmarks +3. **Add form** - URL + titel invoeren +4. **Delete** - Bookmark verwijderen --- ## Tools -- Supabase -- Next.js -- OpenCode/WebStorm -- Vercel -- Git +- VS Code +- Supabase Dashboard +- Browser DevTools --- -## Lesopdracht (2 uur) +## Lesopdracht (2.5 uur) -### Bouw een Todo App met Supabase +### Bouw de Bookmark Manager -**Groepsdiscussie (15 min):** -Bespreek klassikaal de database schemas uit Les 8 - wie heeft welke structuur gekozen en waarom? +**Checkpoints:** -**Deel 1: Supabase Setup (30 min)** +| Tijd | Wat klaar moet zijn | +|------|---------------------| +| 30 min | Project setup + database schema | +| 60 min | Auth werkt (login/logout) | +| 90 min | Bookmarks toevoegen en bekijken | +| 120 min | Delete werkt | +| 150 min | Styling en testen | -1. Maak Supabase account en project -2. Maak je tabellen via Table Editor (gebaseerd op Les 8 schema) -3. Kopieer credentials -4. Installeer `@supabase/supabase-js` -5. Maak `src/lib/supabase.ts` -6. Configureer `.env.local` - -Test: `npm run dev` werkt zonder errors - -**Deel 2: CRUD Interface (1 uur)** - -Bouw UI voor todos: -1. Lijst van todos tonen -2. Form om nieuwe todo toe te voegen -3. Checkbox om todo af te vinken -4. Delete button per todo - -Gebruik AI hulp voor de components! - -**Deel 3: Authenticatie (30 min)** - -1. Installeer auth packages -2. Maak login pagina met Auth UI -3. Toon alleen app voor ingelogde users -4. Test: login met magic link +**Minimale eisen:** +- [ ] Login/logout werkt +- [ ] Bookmarks toevoegen werkt +- [ ] Bookmarks worden getoond +- [ ] Delete werkt ### Deliverable -- Werkende Todo app lokaal -- GitHub repository met code -- Screenshot van werkende app +- Werkende lokale applicatie +- Screenshot van je app met data --- -## Huiswerk (2 uur) +## Huiswerk (1 uur) -### Deploy naar Productie + Uitbreiden +### Reflectie -**Deel 1: Deployment (30 min)** +Schrijf korte reflectie (200 woorden): +- Wat ging goed bij het integreren? +- Waar liep je vast? +- Wat zou je volgende keer anders doen? -1. Push naar GitHub -2. Deploy naar Vercel -3. Configureer env vars in Vercel -4. Voeg Vercel URL toe aan Supabase Redirect URLs -5. Test: app werkt op productie URL! - -**Deel 2: Features Uitbreiden (1 uur)** - -Voeg toe: -1. Filter buttons: Alle / Actief / Voltooid -2. Sorteer op datum (nieuwste eerst) -3. Loading state tijdens data ophalen -4. Error state bij problemen -5. Empty state: "Geen todos gevonden" - -**Deel 3: Polish (30 min)** - -1. Styling verbeteren met Tailwind -2. Responsive design (mobile friendly) -3. Kleine animaties (fade in/out) +Maak een lijst van 3 verbeterpunten voor je code. ### Deliverable -- Deployed app op Vercel (werkende URL!) -- Alle features werken in productie -- Screenshot van productie app +- Reflectie (200 woorden) +- 3 verbeterpunten --- ## Leerdoelen Na deze les kan de student: -- Een Supabase project aanmaken en configureren -- Database schema implementeren via Table Editor -- Environment variables correct beheren -- De Supabase client installeren en configureren -- CRUD operaties uitvoeren met de Supabase SDK -- Authenticatie implementeren met Auth UI -- Deployen naar Vercel met environment variables -- Database principles uit Les 8 toepassen in de praktijk +- Een complete full-stack applicatie bouwen met Next.js, TypeScript en Supabase +- CRUD operaties implementeren met React Query +- Authenticatie integreren in een applicatie +- Zelfstandig integratieproblemen oplossen diff --git a/Samenvattingen/Les10-Samenvatting.md b/Samenvattingen/Les10-Samenvatting.md index 757f6c7..b8e1979 100644 --- a/Samenvattingen/Les10-Samenvatting.md +++ b/Samenvattingen/Les10-Samenvatting.md @@ -1,126 +1,116 @@ -# Les 10: Full-Stack Mini Project +# Les 10: Styling: Tailwind CSS & shadcn/ui --- ## Hoofdstuk -**Deel 3: Integration & AI Tooling** (Les 10-12) +**Deel 3: Full-Stack Development** (Les 9-12) ## Beschrijving -Combineer alles wat je geleerd hebt (TypeScript, Next.js, Supabase) in een kleine werkende applicatie. Dit is je eerste "echte" full-stack project en voorbereiding op het werken met AI tools. +Styling je applicatie met Tailwind CSS en modern components met shadcn/ui. Utility-first approach, component libraries, themeing en dark mode implementatie. --- -## Te Behandelen +## Te Behandelen (~45 min) -### Groepsdiscussie (15 min) -Bespreek klassikaal de Supabase ervaringen uit Les 9 - welke uitdagingen kwamen jullie tegen bij authenticatie en RLS? - -### Doel van deze les - -Je hebt nu alle bouwstenen: -- TypeScript (Les 5) -- Next.js met App Router (Les 6-7) -- Supabase database & auth (Les 8-9) - -Vandaag combineren we dit in een **werkende mini-app**. Geen nieuwe concepten - alleen integratie en toepassing. - ---- - -### Mini Project: Personal Bookmarks - -Een simpele bookmark manager waar je links kunt opslaan. - -**Features:** -- Login met Supabase Auth -- Bookmarks toevoegen (URL + titel) -- Bookmarks bekijken en verwijderen - -**Tech stack:** -- Next.js 14 met App Router -- TypeScript -- Tailwind CSS -- Supabase (auth + database) -- React Query - ---- - -### Stap-voor-stap - -#### Database Schema - -**Tabel: bookmarks** -| Kolom | Type | -|-------|------| -| id | uuid (PK) | -| user_id | uuid (FK naar auth.users) | -| url | text | -| title | text | -| created_at | timestamptz | - -**RLS:** Users kunnen alleen eigen bookmarks zien, toevoegen en verwijderen. - -#### Wat je bouwt - -1. **Login pagina** - Supabase Auth -2. **Dashboard** - Lijst van bookmarks -3. **Add form** - URL + titel invoeren -4. **Delete** - Bookmark verwijderen +- Waarom Tailwind? Utility-first CSS approach vs traditioneel CSS +- Tailwind in Next.js (meestal al ingesteld) +- Core utilities: spacing, colors, flexbox, grid, responsive (mobile-first) +- Tailwind components: buttons, cards, forms patterns +- Wat is shadcn/ui? Beautifully designed component library +- shadcn/ui installatie en configuratie +- shadcn/ui components: Button, Card, Input, Dialog, etc. +- Custom Tailwind color themes (tailwind.config.ts) +- Dark mode implementation met Tailwind +- Performance: class optimization, purging unused styles --- ## Tools -- VS Code -- Supabase Dashboard -- Browser DevTools +- Tailwind CSS +- shadcn/ui +- Cursor +- TypeScript --- -## Lesopdracht (2.5 uur) +## Lesopdracht (2 uur, klassikaal) -### Bouw de Bookmark Manager +### Styling Je Mini-Project -**Checkpoints:** +**Groepsdiscussie (15 min):** +Bespreek klassikaal de Full-Stack Mini Project ervaringen uit Les 9 - welke onderdelen werkten goed en waar liepen jullie vast? -| Tijd | Wat klaar moet zijn | -|------|---------------------| -| 30 min | Project setup + database schema | -| 60 min | Auth werkt (login/logout) | -| 90 min | Bookmarks toevoegen en bekijken | -| 120 min | Delete werkt | -| 150 min | Styling en testen | +**Deel 1: Tailwind Basics (30 min)** -**Minimale eisen:** -- [ ] Login/logout werkt -- [ ] Bookmarks toevoegen werkt -- [ ] Bookmarks worden getoond -- [ ] Delete werkt +1. Open je mini-project uit Les 9 +2. Refactor bestaande components met Tailwind classes: + - Button: `bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded` + - Card: `bg-white rounded-lg shadow-lg p-6` + - Form inputs: `w-full px-3 py-2 border border-gray-300 rounded-md` +3. Voeg spacing, colors, responsive design toe + +**Deel 2: shadcn/ui Setup & Components (45 min)** + +1. Install shadcn/ui: `npx shadcn-ui@latest init` +2. Install components: `npx shadcn-ui@latest add button input card dialog` +3. Replace je custom components met shadcn versions +4. Test styling en interactiviteit + +**Deel 3: Theme & Dark Mode (30 min)** + +1. Customize Tailwind color scheme in `tailwind.config.ts` +2. Voeg dark mode toggle toe (localStorage + CSS class toggle) +3. Zorg dat je hele app responsive is (mobile-first) ### Deliverable -- Werkende lokale applicatie -- Screenshot van je app met data +- Gerestyled mini-project met Tailwind + shadcn/ui +- Dark mode toggle werkend +- Mobile responsive design +- GitHub commit met improvements --- -## Huiswerk (1 uur) +## Huiswerk (2 uur) -### Reflectie +### Vervolg Styling & Polish -Schrijf korte reflectie (200 woorden): -- Wat ging goed bij het integreren? -- Waar liep je vast? -- Wat zou je volgende keer anders doen? +**Deel 1: Component Library Uitbreiden (1 uur)** -Maak een lijst van 3 verbeterpunten voor je code. +Install en integreer meer shadcn/ui components: +- Select, Tabs, Modal/Dialog +- Forms package voor betere form handling +- Toast notifications +- Zorg dat je hele app consistent gelayout is + +**Deel 2: Custom Theme (30 min)** + +Maak je eigen color palette in `tailwind.config.ts`: +- Primary, secondary, accent colors +- Custom spacing, typography +- Test in light en dark mode + +**Deel 3: Accessibility & Polish (30 min)** + +1. Voeg alt text toe aan images +2. Zorg voor proper heading hierarchy +3. Test keyboard navigation +4. Fix UI inconsistencies ### Deliverable -- Reflectie (200 woorden) -- 3 verbeterpunten +- Compleet gestylde mini-project +- Alle shadcn/ui components correct geintegreerd +- Custom color theme +- GitHub commits met styling improvements --- ## Leerdoelen + Na deze les kan de student: -- Een complete full-stack applicatie bouwen met Next.js, TypeScript en Supabase -- CRUD operaties implementeren met React Query -- Authenticatie integreren in een applicatie -- Zelfstandig integratieproblemen oplossen +- Tailwind utility-first approach begrijpen en toepassen +- shadcn/ui components installeren en gebruiken +- Custom Tailwind themes maken +- Dark mode implementeren +- Responsive design (mobile-first) toepassen +- Professional-looking UI bouwen met componenten +- Het verschil tussen styling approaches begrijpen diff --git a/Samenvattingen/Les11-Samenvatting.md b/Samenvattingen/Les11-Samenvatting.md index ad8d01a..45ee7bd 100644 --- a/Samenvattingen/Les11-Samenvatting.md +++ b/Samenvattingen/Les11-Samenvatting.md @@ -1,237 +1,233 @@ -# Les 11: Hands-on: Van Idee naar Prototype +# Les 11: Van Idee naar Prototype --- ## Hoofdstuk -**Deel 3: AI Tooling & Prototyping** (Les 10-12) +**Deel 3: Full-Stack Development** (Les 9-12) ## Beschrijving -Pas alles wat je hebt geleerd toe in een hands-on sessie. Ga van een vaag idee naar een werkend prototype met behulp van je AI workflow. +Proces van vaag idee naar werkend prototype. Feature breakdown, component thinking, MVP planning, user stories, en voorbereiding op eindproject. Kiezen van eindproject idea en architectuur opzetten. --- -## Te Behandelen +## Te Behandelen (~45 min) + +- Hoe ga je van vaag idee naar concrete features? +- Feature breakdown method en AI-hulp +- Component thinking: UI opdelen in herbruikbare stukken +- MVP (Minimum Viable Product) denken +- User stories schrijven (As a user I want to...) +- Prioritizing features: must-have, nice-to-have, later +- Database schema planning voor je idee +- Project architecture design +- Folder structure planning +- Eindproject idee kiezen (vereenvoudigde versie) + +--- ### Van Idee naar Feature Breakdown -**Het probleem:** Je hebt een idee, maar waar begin je? - -**Stap 1: Beschrijf je idee in 1-2 zinnen** +**Stap 1: Beschrijf je idee in 1 zin** ``` -"Ik wil een app waar je kunt bijhouden welke planten water nodig hebben." +"Ik wil een app waar je recepten kan zoeken op basis van ingrediënten die je hebt" ``` -**Stap 2: Vraag AI om feature breakdown** +**Stap 2: Werk uit wat gebruikers willen** ``` -Prompt: Ik wil een plant watering tracker app bouwen. -Wat zijn de minimale features voor een werkend prototype? -Denk aan: wat moet een gebruiker kunnen doen? +User stories: +- Als gebruiker wil ik ingrediënten kunnen invoeren +- Als gebruiker wil ik recepten suggesties krijgen +- Als gebruiker wil ik recepten kunnen opslaan ``` -**Stap 3: Prioriteer (MVP denken)** -- Wat is essentieel? → In prototype -- Wat is nice-to-have? → Later +**Stap 3: Break down in features** +``` +Core Features: +- Ingredient input form +- Recipe search/filter +- Save favorites +- View saved recipes + +Nice-to-have: +- Meal planning +- Shopping list generator +- Nutritional info +- Export recipes +``` + +**Stap 4: Prioriteer (MVP)** +- ✅ Input form + recipe search +- ✅ Display results +- ✅ Save/favorites +- ❌ Shopping list (later) +- ❌ Meal planning (later) --- ### Component Thinking -**Vraag jezelf af:** -- Welke "blokken" zie ik op het scherm? -- Welke blokken worden herhaald? -- Welke blokken komen op meerdere pagina's? - -**Voorbeeld: Plant Tracker** +**Visualiseer je app:** ``` -Herhaalde componenten: -- PlantCard (naam, foto, laatste water datum) -- WaterButton (markeer als water gegeven) - -Pagina componenten: -- PlantList (toont alle PlantCards) -- AddPlantForm (nieuw plant toevoegen) +App +├── Header +├── SearchForm (input + submit) +├── RecipeList +│ └── RecipeCard (repeated) +│ ├── Title +│ ├── Ingredients +│ ├── Favorite button +│ └── View details +└── Footer ``` +**Questions to ask:** +- Welke componenten worden herhaald? +- Welke componenten zijn child components? +- Welke componenten hebben state? +- Waar gaat data vandaan? + --- -### MVP (Minimum Viable Product) Denken +### MVP Denken -**Wat is MVP?** -De simpelste versie van je app die nog steeds waarde levert. +**MVP = Minimum Viable Product** + +Het eenvoudigst mogelijke product dat nog steeds waarde levert. **❌ Niet MVP:** - Alle features tegelijk -- Perfect design -- Edge cases afhandelen -- Login systeem +- Perfect design met animaties +- Geavanceerde filters +- Social features **✅ Wel MVP:** - Core functionaliteit werkt -- Basis styling +- Basis styling (Tailwind) - Happy path werkt -- Hardcoded data is oké +- Data persists (database) --- -### De Prototype Workflow +### Database Planning +**Voor recipe app:** ``` -1. IDEE (1-2 zinnen) - ↓ -2. FEATURES (AI breakdown) - ↓ -3. PRIORITEER (wat is MVP?) - ↓ -4. COMPONENTS (welke blokken?) - ↓ -5. BOUWEN (tool per stap) - ↓ -6. ITEREREN (feedback → aanpassen) +users table: +- id, email, password_hash, created_at + +recipes table: +- id, title, ingredients, instructions, created_at + +favorites table: +- id, user_id, recipe_id, created_at ``` ---- - -### Voorbeeld: Weer Widget Prototype - -**Stap 1: Idee** -"Simpele weer widget met 3-daagse forecast" - -**Stap 2: AI Feature Breakdown** -``` -Vraag aan ChatGPT: -"Wat zijn de minimale features voor een weer widget met 3-daagse forecast?" - -Antwoord: -- Huidige temperatuur tonen -- Weer icoon (zon, regen, etc.) -- 3-daagse forecast (dag + temp + icoon) -- Locatie tonen -``` - -**Stap 3: MVP Selectie** -- ✅ Huidige temperatuur -- ✅ Weer icoon -- ✅ 3 dagen forecast -- ❌ Locatie selectie (later) -- ❌ Animated icons (later) - -**Stap 4: Components** -``` -WeatherWidget/ -├── CurrentWeather (temp + icoon) -├── ForecastDay (dag + temp + icoon) -└── ForecastList (3x ForecastDay) -``` - -**Stap 5: Bouwen** -1. v0.dev: "Weather widget with current temp and 3 day forecast, minimal design" -2. OpenCode: "Integreer dit in mijn project, maak components in src/components/weather/" - -**Stap 6: Itereren** -- Wat werkt niet? -- Wat kan beter? -- Vraag AI om verbeteringen - ---- - -### Mini-Project Ideeën - -| Project | Core Feature | Complexiteit | -|---------|-------------|--------------| -| **Weer Widget** | 3-daagse forecast | ⭐ | -| **Quiz App** | 3 vragen + score | ⭐ | -| **Recipe Card** | Ingrediënten toggle | ⭐ | -| **Pomodoro Timer** | Start/stop + countdown | ⭐⭐ | -| **Expense Tracker** | Lijst + totaal | ⭐⭐ | +**Relations:** +- users → favorites (one-to-many) +- users → saved_recipes (one-to-many) +- recipes → favorites (one-to-many) --- ## Tools +- Cursor - ChatGPT (voor planning) -- v0.dev (voor prototypes) -- OpenCode/WebStorm (voor implementation) +- Pen & papier (voor sketches) --- -## Lesopdracht (2 uur) +## Lesopdracht (2 uur, klassikaal) -### Bouw Je Mini-Prototype +### Kies je Eindproject & Plan Architectuur **Groepsdiscussie (15 min):** -Bespreek klassikaal de Tool Selection reflecties uit Les 10 - welke workflows werken het beste? +Bespreek klassikaal de styling ervaringen uit Les 10 - welke Tailwind en shadcn/ui patterns werkten goed? -**Deel 1: Planning (30 min)** +**Deel 1: Idee Kiezen (30 min)** -1. Kies een project uit de lijst (of bedenk eigen simpel idee) -2. Schrijf je idee in 1-2 zinnen -3. Vraag ChatGPT om feature breakdown -4. Selecteer MVP features (max 3) -5. Schets de components op papier +Kies één van deze projecten OF je eigen idee (met goedkeuring docent): +- **AI Recipe Generator** - Input: ingredients, Output: recipe suggestions + cooking tips +- **Smart Budget Buddy** - Track expenses, AI insights on spending patterns +- **Travel Planner AI** - Generate itineraries based on preferences +- **Study Buddy AI** - Quiz generation, note-taking, Q&A helper +- **Jouw eigen idee** -**Deel 2: Bouwen (1 uur)** +**Deel 2: Feature Breakdown (30 min)** -1. Genereer UI in v0.dev -2. Open project in OpenCode -3. Integreer en pas aan -4. Zorg dat het werkt (happy path) +Voor je gekozen project: +1. Schrijf project description (2-3 zinnen) +2. Maak 3-5 user stories +3. Break down in core vs nice-to-have features +4. Selecteer MVP (wat is essentieel?) -**Focus op WORKFLOW, niet perfectie!** +**Deel 3: Architecture Planning (45 min)** -**Deel 3: Documentatie (15 min)** +1. Schets database schema +2. List de main components +3. Plan folder structure +4. Identify donde AI integreert (tool calling? Chat? Completion?) -Maak `docs/PROTOTYPE-LOG.md`: -- Je idee -- Feature breakdown -- MVP keuzes -- Prompts die werkten -- Wat ging fout en hoe opgelost +**Deel 4: Start Codebase (15 min)** + +1. Maak GitHub repo +2. Create-next-app setup +3. Push initial commit ### Deliverable -- Werkend prototype (kan simpel zijn) -- `docs/PROTOTYPE-LOG.md` met je proces -- Screenshot van werkend prototype +- Project description document +- Feature breakdown met user stories +- Database schema diagram (hand-drawn ok) +- Component tree visualization +- GitHub repo met initial setup --- ## Huiswerk (2 uur) -### Verbeter en Reflecteer +### Bouw Basis Architecture -**Deel 1: Prototype Verbeteren (1 uur)** +**Deel 1: Folder Structure (30 min)** -1. Fix eventuele bugs -2. Voeg 1 extra feature toe -3. Verbeter styling -4. Handle 1 edge case +Create proper folder structure: +- src/components/ui/ +- src/components/features/ +- src/lib/ +- src/hooks/ +- src/types/ +- docs/ -**Deel 2: Peer Review (30 min)** +**Deel 2: Type Definitions (30 min)** -- Deel je prototype met een klasgenoot -- Krijg feedback -- Geef feedback op hun prototype +Schrijf TypeScript types voor je project: +- User type +- Main data types +- API response types -**Deel 3: Reflectie (30 min)** +**Deel 3: Component Skeleton (1 uur)** -Schrijf "Lessons Learned" document (300 woorden): -- Wat ging goed in je workflow? -- Waar liep je vast? -- Welke tool was het meest nuttig? -- Wat doe je volgende keer anders? -- Hoe voelde het om met AI te bouwen vs alleen? +Maak skeleton components: +- Main layout +- 3-4 main feature components (without logic) +- Proper props interfaces +- Basic Tailwind styling ### Deliverable -- Verbeterd prototype -- Peer review feedback (gegeven en ontvangen) -- Lessons Learned document (300 woorden) +- GitHub commits met folder structure +- types/ folder met je data types +- Skeleton components +- README.md met project description --- ## Leerdoelen + Na deze les kan de student: -- Van een vaag idee naar concrete features gaan -- AI gebruiken voor feature breakdown -- MVP denken toepassen (essentieel vs nice-to-have) -- Een app opdelen in components -- De complete workflow doorlopen (idee → prototype) -- Het bouwproces documenteren -- Reflecteren op wat werkt en wat niet +- Een project idee van vaag naar concreet uitwerken +- User stories schrijven +- Feature breakdown maken +- MVP bepalen +- Database schema ontwerpen +- Component architecture plannen +- Project codebase opzetten +- TypeScript types structureren +- Voorbereiding maken op verdere implementatie diff --git a/Samenvattingen/Les12-Samenvatting.md b/Samenvattingen/Les12-Samenvatting.md index 30b1322..4b0545b 100644 --- a/Samenvattingen/Les12-Samenvatting.md +++ b/Samenvattingen/Les12-Samenvatting.md @@ -1,261 +1,157 @@ -# Les 12: Introduction to Cursor +# Les 12: Project Setup & AI Config (.cursorrules, claude.md) --- ## Hoofdstuk -**Deel 3: AI Tooling & Prototyping** (Les 10-12) +**Deel 3: Full-Stack Development** (Les 9-12) ## Beschrijving -Kennismaking met Cursor - de professionele AI code editor. Leer de core features en ontdek waarom dit de tool is voor serieuze AI-assisted development. +Professional project setup en AI configuration voor optimal developer experience. Setup .cursorrules, claude.md, docs/ folder, folder structure optimalisatie en git best practices. --- -## Te Behandelen +## Te Behandelen (~45 min) -### Groepsdiscussie (15 min) -Bespreek klassikaal de prototype ervaringen uit Les 11 - welke workflow patterns werkten goed? Wat ging fout en hoe loste je dat op? - -### Waarom Cursor? - -**Tot nu toe gebruikten we:** -- OpenCode (gratis, goed voor leren) -- Claude Desktop (voor agents en projects) - -**Cursor is de volgende stap:** -- Purpose-built voor AI-assisted coding -- Professionele editor (gebaseerd op VS Code) -- Superieure AI integratie -- Free tier beschikbaar (voldoende voor de cursus) - ---- - -### Free vs Pro - -| Aspect | Free Tier | Pro ($20/maand) | -|--------|-----------|-----------------| -| Tab completion | ✅ | ✅ | -| CMD+K edits | Beperkt | Onbeperkt | -| Chat | Beperkt | Onbeperkt | -| Composer | Beperkt | Onbeperkt | -| Models | GPT-5, Claude | Alle modellen | - -**Voor deze cursus:** Free tier is voldoende! - ---- - -### Installatie - -1. Ga naar [cursor.sh](https://cursor.sh) -2. Download voor jouw OS -3. Installeer -4. Open Cursor -5. Sign in met GitHub - -**Eerste keer:** -- Cursor vraagt om settings te importeren van VS Code (optioneel) -- Accept default keybindings - ---- - -### Core Features - -#### 1. Tab Completion -AI-powered autocomplete die hele blokken code voorspelt. - -**Hoe het werkt:** -- Begin met typen -- Cursor suggereert code in grijs -- Druk Tab om te accepteren -- Druk Escape om te negeren - -**Tip:** Schrijf een comment over wat je wilt, en Tab completion vult de code in. - -```typescript -// Function that calculates the total price with tax -// [Tab completion vult de functie in] -``` - -#### 2. CMD+K (Inline Editing) -Selecteer code en vraag AI om het aan te passen. - -**Hoe het werkt:** -1. Selecteer code (of zet cursor op een regel) -2. Druk CMD+K (Mac) of Ctrl+K (Windows) -3. Typ je instructie -4. Enter om te genereren -5. Accept of Reject de wijziging - -**Voorbeelden:** -- "Add error handling" -- "Convert to TypeScript" -- "Make this responsive" -- "Add loading state" - -#### 3. Chat (Sidebar) -Converseer met AI over je code. - -**Hoe het werkt:** -1. CMD+Shift+L opent Chat -2. Stel je vraag -3. AI heeft context van je open file - -**Voorbeelden:** -- "Explain what this function does" -- "How can I optimize this?" -- "What's wrong with this code?" - -#### 4. @ Mentions -Refereer naar files, folders, of documentatie. - -**Syntax:** -- `@filename.tsx` - specifieke file -- `@folder/` - hele folder -- `@Docs` - officiële docs zoeken -- `@Web` - web zoeken - -**Voorbeeld:** -``` -@components/Button.tsx - How can I add a loading prop to this? -``` - -#### 5. Composer Mode -Multi-file generatie in één keer. - -**Hoe het werkt:** -1. CMD+I opent Composer -2. Beschrijf wat je wilt bouwen -3. AI genereert meerdere files tegelijk -4. Review en accept changes - -**Wanneer gebruiken:** -- Nieuwe features met meerdere components -- Refactoring over meerdere files -- Boilerplate code genereren - ---- - -### Workflow Vergelijking - -| Taak | OpenCode | Cursor | -|------|----------|--------| -| Snelle fix | Chat | CMD+K | -| Nieuwe component | Chat | Tab completion + CMD+K | -| Multi-file feature | Meerdere chats | Composer | -| Code uitleg | Chat | Chat + @ mentions | -| Refactoring | Chat | CMD+K of Composer | - ---- - -### Keyboard Shortcuts Cheat Sheet - -| Actie | Mac | Windows | -|-------|-----|---------| -| Tab completion accept | Tab | Tab | -| Inline edit | CMD+K | Ctrl+K | -| Open Chat | CMD+Shift+L | Ctrl+Shift+L | -| Open Composer | CMD+I | Ctrl+I | -| Accept suggestion | CMD+Y | Ctrl+Y | -| Reject suggestion | CMD+N | Ctrl+N | +- Waarom goede project structuur cruciaal is voor AI tools +- De ideale folder structuur voor Next.js + AI collaboration +- .cursorrules file: syntax, best practices, examples +- claude.md: project documentation voor Claude +- docs/ folder organization: AI-DECISIONS.md, ARCHITECTURE.md, PROMPT-LOG.md +- .env.local vs .env.example configuratie +- .gitignore en .cursorignore voor AI tools +- .git best practices: meaningful commits, proper history +- README setup met project info +- TypeScript configuration optimization --- ## Tools - Cursor +- Git - GitHub --- -## Lesopdracht (2 uur) +## Lesopdracht (2 uur, klassikaal) -### Cursor Verkennen +### Setup Je Eindproject Proper -**Deel 1: Setup (20 min)** +**Groepsdiscussie (15 min):** +Bespreek klassikaal de architecture planning uit Les 11 - welke design patterns voelen goed voor jullie eindproject? -1. Download en installeer Cursor -2. Sign in met GitHub -3. Open je Todo app project -4. Verken de interface +**Deel 1: Folder Structure (30 min)** -**Deel 2: Tab Completion (30 min)** +Zorg dat je project er zo uitziet: +``` +project/ +├── src/ +│ ├── app/ +│ ├── components/ +│ │ ├── ui/ +│ │ ├── layout/ +│ │ └── features/ +│ ├── lib/ +│ ├── hooks/ +│ └── types/ +├── docs/ +├── public/ +├── .cursorrules +├── .env.local +├── .env.example +├── .gitignore +├── README.md +└── claude.md +``` -Maak nieuwe file `src/components/LoadingSpinner.tsx`: -1. Typ comment: `// Loading spinner component with size prop` -2. Laat Tab completion de rest doen -3. Itereer met meer comments -4. Noteer: hoe goed is de completion? +**Deel 2: .cursorrules Writing (30 min)** -**Deel 3: CMD+K Oefenen (30 min)** +Maak comprehensive `.cursorrules`: +1. Project naam + beschrijving +2. Tech stack (Next.js 14, TypeScript, Tailwind, Supabase) +3. File structure conventions +4. Code conventions (naming, styling, error handling) +5. TypeScript strict rules +6. DO's en DON'Ts specifiek voor je project -Open je TodoList component: -1. Selecteer de list rendering code -2. CMD+K → "Add loading state with skeleton" -3. Selecteer een button -4. CMD+K → "Add disabled state while loading" -5. Selecteer een function -6. CMD+K → "Add try-catch with error toast" +Example snippets: +- "Named exports only, no default exports" +- "All components are functional with TypeScript" +- "Use Tailwind classes, no inline styles" -Noteer wat werkt en wat niet. +**Deel 3: Documentation Files (30 min)** -**Deel 4: Chat + @ Mentions (20 min)** +Maak in docs/ folder: +- `PROJECT-BRIEF.md` - Project overview, features +- `ARCHITECTURE.md` - Component structure, data flow +- `AI-DECISIONS.md` - Start document met AI choices +- `PROMPT-LOG.md` - Template voor prompts die je gebruikt -1. Open Chat (CMD+Shift+L) -2. Vraag: "@TodoList.tsx What could I improve in this component?" -3. Vraag: "@lib/supabase.ts How do I add real-time subscriptions?" -4. Probeer @Docs voor Next.js documentatie +**Deel 4: Git Setup (20 min)** -**Deel 5: Composer Proberen (20 min)** - -1. Open Composer (CMD+I) -2. Vraag: "Create a Settings page with dark mode toggle and notification preferences. Use our existing component style." -3. Review de gegenereerde files -4. Accept of reject +1. Review `.gitignore` (include `.env.local`) +2. Make initial commit: "Initial project setup" +3. Push naar GitHub +4. Verify: `.env.local` NOT in git history ### Deliverable -- Screenshot van werkende Tab completion -- 3 voorbeelden van CMD+K edits -- Notities: wat werkt wel/niet goed +- Complete folder structure +- Comprehensive .cursorrules file +- Documentation files in docs/ +- GitHub repo met clean initial commit +- README.md with project info --- ## Huiswerk (2 uur) -### Bouw Feature met Cursor +### Optimize Configuration & Start Building -**Deel 1: Feature Bouwen (1.5 uur)** +**Deel 1: tsconfig.json Optimization (30 min)** -Voeg "Due Dates" toe aan je Todo app: -1. Elk todo kan een due date hebben -2. Toon due date in de lijst -3. Sorteer op due date -4. Markeer overdue items in rood +1. Enable strict mode in TypeScript +2. Configure path aliases for cleaner imports: + ```json + "@/*": ["./src/*"] + ``` +3. Set proper include/exclude -**Gebruik ALLE Cursor features:** -- Tab completion voor nieuwe code -- CMD+K voor aanpassingen -- Chat voor vragen -- Composer voor multi-file changes +**Deel 2: Tailwind & Component Setup (45 min)** -**Deel 2: Reflectie (30 min)** +1. Customize `tailwind.config.ts` with your colors +2. Setup `globals.css` properly +3. Create 5 base UI components: + - Button.tsx + - Input.tsx + - Card.tsx + - Layout.tsx + - Navigation.tsx -Schrijf vergelijking (400 woorden): -- Cursor vs OpenCode: wat is beter? -- Welke feature gebruik je het meest? -- Is free tier voldoende? -- Ga je overstappen? +**Deel 3: Lib Setup (30 min)** + +Create essential lib files: +- `lib/supabase.ts` - Initialize Supabase client +- `lib/utils.ts` - Utility functions +- `lib/constants.ts` - App constants +- `types/database.ts` - Database types ### Deliverable -- Werkende Due Dates feature -- GitHub commit met de changes -- Reflectie (400 woorden) +- Optimized TypeScript config with path aliases +- Base UI components created +- Lib utilities setup +- Documentation updated with decisions +- GitHub commits with setup progress --- ## Leerdoelen + Na deze les kan de student: -- Cursor installeren en configureren -- Tab completion effectief gebruiken -- CMD+K gebruiken voor inline edits -- Chat gebruiken met @ mentions voor context -- Composer mode gebruiken voor multi-file generatie -- Het verschil beoordelen tussen Cursor en OpenCode -- De juiste Cursor feature kiezen per taak +- Een professional project structure opzetten +- Een effectieve .cursorrules file schrijven +- claude.md projectdocumentatie maken +- AI-DECISIONS.md beginnen en onderhouden +- Git best practices volgen +- TypeScript strict mode configureren +- Path aliases voor cleaner imports opzetten +- Base components en utilities creëren +- Voorbereiding maken op Deel 4 (Advanced AI) diff --git a/Samenvattingen/Les13-Samenvatting.md b/Samenvattingen/Les13-Samenvatting.md index 2348318..11a9f20 100644 --- a/Samenvattingen/Les13-Samenvatting.md +++ b/Samenvattingen/Les13-Samenvatting.md @@ -1,345 +1,298 @@ -# Les 13: Prompt Engineering & Custom GPTs +# Les 13: Vercel AI SDK, Tool Calling & Agents --- ## Hoofdstuk -**Deel 4: Advanced AI Features** (Les 13-18) +**Deel 4: Advanced AI & Deployment** (Les 13-18) ## Beschrijving -Verdiep je in advanced prompt engineering en leer eigen AI assistenten maken met Custom GPTs en Claude Projects. Focus op no-code manieren om AI te personaliseren voor jouw workflow. +Vercel AI SDK fundamentals voor het bouwen van AI-powered features. Stream responses, tool calling, Zod schemas, system prompts, agents met autonome actie. Integreer LLM capabilities in je app. --- -## Te Behandelen +## Te Behandelen (~45 min) -### Groepsdiscussie (15 min) -Bespreek klassikaal de Cursor reflecties uit Les 12 - welke features werken het beste voor welke taken? +- Vercel AI SDK: wat is het en waarom gebruiken? +- Installation en basic setup +- useChat hook voor chat UI state management +- Streaming responses van API +- Tool calling: laat AI externe APIs aanroepen +- Zod schemas voor tool parameters validation +- System prompts schrijven voor AI behavior +- Agent patterns: maxSteps, autonomous execution +- Error handling en edge cases +- Model selection: OpenAI, Claude, Gemini, etc. -### Advanced Prompt Engineering +--- -**Recap van basis technieken (Les 4):** -- Zero-shot vs few-shot prompting -- Chain-of-thought reasoning -- Role prompting +### Vercel AI SDK Basics -**Nieuwe technieken:** +**Wat is het?** +- React library van Vercel voor AI integration +- Streaming responses van LLMs +- Server-side tool calling +- Multi-turn conversations +- Gratis, open-source -#### 1. Structured Output Prompting -``` -Analyseer deze code en geef feedback in dit exacte format: - -## Samenvatting -[1 zin over de code] - -## Sterke punten -- [punt 1] -- [punt 2] - -## Verbeterpunten -- [punt 1 met code voorbeeld] -- [punt 2 met code voorbeeld] - -## Prioriteit -[Hoog/Medium/Laag]: [waarom] -``` - -#### 2. Constraint-Based Prompting -``` -Schrijf een React component met deze constraints: -- Maximaal 50 regels code -- Geen externe dependencies -- TypeScript met strict types -- Alleen Tailwind voor styling -- Inclusief error handling -``` - -#### 3. Iterative Refinement -``` -Stap 1: "Schrijf een basis login form" -Stap 2: "Voeg validatie toe" -Stap 3: "Voeg loading states toe" -Stap 4: "Voeg error handling toe" -Stap 5: "Optimaliseer voor accessibility" +**Installation:** +```bash +npm install ai zod openai ``` --- -### Custom GPTs +### useChat Hook -**Wat zijn Custom GPTs?** -Gespecialiseerde ChatGPT versies met: -- Specifieke instructies (personality, expertise) -- Eigen kennis (uploaded files) -- Optioneel: Actions (API calls) +**Client-side chat state management:** -**Wanneer een Custom GPT maken:** -- Repetitieve taken met dezelfde context -- Specifieke expertise nodig -- Delen met team of anderen +```typescript +'use client' +import { useChat } from 'ai/react' -**Custom GPT maken:** -1. Ga naar chat.openai.com/gpts -2. Klik "Create" -3. Configureer in "Create" tab OF gebruik "Configure" +export function ChatComponent() { + const { messages, input, handleInputChange, handleSubmit } = useChat({ + api: '/api/chat', + }) -**Voorbeeld: Code Review GPT** -``` -Instructions: -Je bent een code reviewer gespecialiseerd in React/Next.js. - -Bij elke code review check je: -1. TypeScript best practices -2. React hooks correct gebruik -3. Performance (unnecessary re-renders) -4. Accessibility basics -5. Error handling - -Geef feedback in dit format: -- ✅ Goed: [wat goed is] -- ⚠️ Suggestie: [verbeterpunten] -- ❌ Issue: [problemen die gefixed moeten worden] -``` - -**Voorbeeld: Project Assistant GPT** -``` -Instructions: -Je bent mijn persoonlijke development assistant voor [project naam]. - -Tech stack: -- Next.js 14 met App Router -- TypeScript strict mode -- Tailwind CSS -- Supabase voor backend - -Je kent de volgende conventies: -- Named exports (geen default exports) -- Nederlandse comments -- Error handling met try/catch - -Wanneer ik code vraag: -1. Vraag eerst om context als die ontbreekt -2. Geef TypeScript code met types -3. Voeg korte uitleg toe + return ( + <div> + {messages.map((msg) => ( + <div key={msg.id}> + <strong>{msg.role}:</strong> {msg.content} + </div> + ))} + <form onSubmit={handleSubmit}> + <input + value={input} + onChange={handleInputChange} + placeholder="Type message..." + /> + <button type="submit">Send</button> + </form> + </div> + ) +} ``` --- -### Claude Projects +### Streaming Responses -**Wat is een Claude Project?** -- Verzameling van context specifiek voor één doel -- Blijft behouden over conversaties -- Kan bestanden bevatten als knowledge base +**API Route met streaming:** -**Wanneer gebruiken:** -- Terugkerend werk aan hetzelfde project -- Consistente coding style nodig -- Documentatie die AI moet kennen +```typescript +import { generateText, streamText } from 'ai' +import { openai } from '@ai-sdk/openai' -**Project aanmaken:** -1. Ga naar claude.ai → Projects -2. Klik "New Project" -3. Voeg project knowledge toe (files, instructies) -4. Start conversaties binnen het project +export async function POST(req: Request) { + const { messages } = await req.json() -**Voorbeeld Project Instructions:** -``` -Je bent een expert React/Next.js developer die mij helpt met [project]. + const result = await streamText({ + model: openai('gpt-4'), + system: 'You are a helpful assistant', + messages, + }) -Technologie stack: -- Next.js 14 met App Router -- TypeScript strict mode -- Tailwind CSS -- Supabase voor backend -- React Query voor data fetching - -Coding conventions: -- Functional components met TypeScript -- Named exports (geen default exports) -- Error handling met try/catch -- Nederlandse comments in code - -Wanneer je code schrijft: -- Gebruik altijd TypeScript types -- Voeg JSDoc comments toe voor complexe functies -- Denk aan edge cases en error handling + return result.toAIStreamResponse() +} ``` -**Project Knowledge toevoegen:** -- Upload je belangrijkste files (schema, README, .cursorrules) -- Upload voorbeeldcode die je stijl toont -- Upload documentatie van libraries die je gebruikt +**Waarom streaming?** +- Responses verschijnen real-time (beter UX) +- Bespaar tokens vs waiting for full response --- -### Custom GPTs vs Claude Projects +### Tool Calling -| Aspect | Custom GPT | Claude Project | -|--------|------------|----------------| -| **Beschikbaar** | ChatGPT Plus ($20/maand) | Claude Pro ($20/maand) | -| **Knowledge** | File uploads (tot 20 files) | File uploads (tot 200k tokens) | -| **Delen** | Kan gepubliceerd worden | Alleen persoonlijk | -| **Actions** | Ja (API calls) | Nee | -| **Context window** | ~128k tokens | ~200k tokens | -| **Beste voor** | Gedeelde tools, API integratie | Persoonlijke projecten, grote codebases | +**Laat AI externe APIs aanroepen:** + +```typescript +import { generateText } from 'ai' +import { openai } from '@ai-sdk/openai' +import { z } from 'zod' + +const tools = { + getWeather: { + description: 'Get weather for a city', + parameters: z.object({ + city: z.string(), + }), + execute: async ({ city }: { city: string }) => { + // Call external API + const response = await fetch(`https://api.weather.com?city=${city}`) + return response.json() + }, + }, +} + +const result = await generateText({ + model: openai('gpt-4'), + tools, + prompt: 'What is the weather in Amsterdam?', +}) +``` --- -### Prompt Templates Library +### Zod Schemas -**Code Generation:** -``` -Context: [beschrijf je project/stack] -Taak: [wat moet er gemaakt worden] -Requirements: -- [requirement 1] -- [requirement 2] -Constraints: -- [constraint 1] -Output: [gewenst format] +**Type-safe tool parameters:** + +```typescript +import { z } from 'zod' + +const SearchProductsSchema = z.object({ + query: z.string().describe('Search query'), + limit: z.number().optional().describe('Max results'), + sortBy: z.enum(['price', 'rating']).optional(), +}) + +type SearchProductsInput = z.infer<typeof SearchProductsSchema> ``` -**Debugging:** -``` -Error message: -[plak error] +--- -Relevante code: -[plak code] +### System Prompts -Wat ik verwacht: -[gewenst gedrag] +**Stuur AI behavior:** -Wat er gebeurt: -[actueel gedrag] +```typescript +const systemPrompt = `You are a helpful recipe assistant. +Your role is to: +1. Suggest recipes based on ingredients +2. Provide cooking instructions +3. Estimate cooking time + +Always be friendly and encouraging.` + +const result = await generateText({ + model: openai('gpt-4'), + system: systemPrompt, + prompt: userMessage, +}) ``` -**Code Review:** -``` -Review de volgende code op: -1. Best practices voor [framework] -2. Potentiële bugs -3. Performance issues -4. Security vulnerabilities +--- -[plak code] +### Agent Patterns -Geef feedback in format: ✅ Goed / ⚠️ Suggestie / ❌ Issue +**Multi-step autonomous execution:** + +```typescript +const result = await generateText({ + model: openai('gpt-4'), + tools: { getWeather, getFlights }, + maxSteps: 3, // Maximum iterations + prompt: 'Plan a trip to Paris next week', +}) ``` -**Refactoring:** -``` -Refactor deze code met de volgende doelen: -- [doel 1, bijv. "verbeter leesbaarheid"] -- [doel 2, bijv. "reduceer duplicatie"] - -Behoud dezelfde functionaliteit. -Leg uit wat je verandert en waarom. - -[plak code] -``` +**Hoe het werkt:** +1. AI bepaalt welke tool nodig is +2. Tool wordt uitgevoerd +3. Result teruggestuurd naar AI +4. AI beslist next stap (repeat tot maxSteps of done) --- ## Tools -- ChatGPT (Custom GPTs) -- Claude (Projects) -- Prompt template documenten +- Vercel AI SDK +- Zod +- OpenAI API (of andere LLM provider) +- Cursor --- -## Lesopdracht (2 uur) +## Lesopdracht (2 uur, klassikaal) -### Bouw Je Eigen AI Assistants +### Bouw Chat Interface met Streaming -**Deel 1: Claude Project (45 min)** +**Groepsdiscussie (15 min):** +Bespreek klassikaal de project setup ervaringen uit Les 12 - hoe goed werken jullie .cursorrules en configuration? -Maak een Claude Project voor je eindproject: -1. Ga naar claude.ai → Projects → New Project -2. Schrijf project instructions: - - Tech stack - - Coding conventions - - Project specifieke regels -3. Upload relevante files (schema, README) -4. Test met 3 verschillende vragen +**Deel 1: Installation & Setup (30 min)** -**Deel 2: Custom GPT (45 min)** +```bash +npm install ai zod openai +``` -Maak een Custom GPT voor code review: -1. Ga naar chat.openai.com/gpts -2. Klik "Create" -3. Schrijf instructions voor jouw stack -4. Test met code uit je project +Create `app/api/chat/route.ts`: +- Setup Vercel AI SDK +- Configure OpenAI model +- Add system prompt -**Deel 3: Vergelijking (30 min)** +**Deel 2: Chat Component (45 min)** -Test dezelfde taak met beide: -- "Review deze component op best practices" -- Noteer verschillen in output -- Welke is beter voor welk doel? +Build `app/page.tsx`: +1. Use useChat hook +2. Render messages list +3. Input form for user messages +4. Display streaming responses + +**Deel 3: Tool Calling (30 min)** + +Add 2 simple tools: +- getTime: return current time +- getRandomNumber: return random number + +Update API route to handle tools with Zod schemas. + +**Deel 4: Testing (15 min)** + +Test chat locally with different prompts that trigger tools. ### Deliverable -- Claude Project URL of screenshot -- Custom GPT (als je ChatGPT Plus hebt) of instructies doc -- Vergelijkingsnotities +- Werkende chat interface with streaming +- 2 integrated tools +- GitHub commit with AI chat feature --- ## Huiswerk (2 uur) -### Optimaliseer Je AI Assistants +### Integreer AI in Eindproject -**Deel 1: Iteratie (1 uur)** +**Deel 1: Project-Specific Tools (1 uur)** -Verbeter je Claude Project: -1. Test met 5 verschillende taken -2. Noteer waar instructies tekortschieten -3. Pas instructies aan -4. Test opnieuw +Add 2-3 tools relevant to your project: +- Recipe Generator: tool to search recipes API +- Budget App: tool to calculate expenses +- Travel Planner: tool to search destinations -Documenteer in `docs/AI-ASSISTANTS.md`: -- Originele instructies -- Wat niet werkte -- Verbeterde instructies -- Resultaat verschil +Define with Zod schemas and execute functions. -**Deel 2: Prompt Library (30 min)** +**Deel 2: System Prompt Tuning (30 min)** -Maak een persoonlijke prompt library: -```markdown -# Mijn Prompt Templates +Write a custom system prompt for your AI: +- Define personality +- Set constraints +- Add context about your app -## Code Generation -[template] +**Deel 3: Integration (30 min)** -## Debugging -[template] - -## Code Review -[template] - -## Refactoring -[template] -``` - -**Deel 3: Reflectie (30 min)** - -Schrijf reflectie (300 woorden): -- Welke AI assistant gebruik je waarvoor? -- Wat is het verschil tussen Claude Projects en Custom GPTs? -- Hoe heeft dit je workflow verbeterd? +Connect AI chat to your main app: +- Add chat page/component +- Integrate with Supabase auth (if needed) +- Test end-to-end ### Deliverable -- Geoptimaliseerde AI assistant instructies -- Prompt library document -- Reflectie (300 woorden) +- AI feature integrated in project +- Custom tools defined +- docs/AI-DECISIONS.md updated with choices +- GitHub commits with AI integration --- ## Leerdoelen + Na deze les kan de student: -- Advanced prompt engineering technieken toepassen (structured output, constraints, iterative refinement) -- Een Custom GPT maken met specifieke instructies en knowledge -- Een Claude Project opzetten met project context -- De juiste tool kiezen (Custom GPT vs Claude Project) per use case -- Een persoonlijke prompt library opbouwen en onderhouden +- Vercel AI SDK installeren en configureren +- useChat hook gebruiken voor chat UI +- Streaming responses implementeren +- Tool calling setup met Zod schemas +- Externe APIs aanroepen via tools +- System prompts schrijven voor AI behavior +- Agent patterns verstaan (maxSteps) +- AI features in een Next.js app integreren +- Tool parameters valideren met Zod diff --git a/Samenvattingen/Les14-Samenvatting.md b/Samenvattingen/Les14-Samenvatting.md index cbd8a9f..d8f4a81 100644 --- a/Samenvattingen/Les14-Samenvatting.md +++ b/Samenvattingen/Les14-Samenvatting.md @@ -1,444 +1,330 @@ -# Les 14: Project Setup & Repository Structure +# Les 14: AI Chat Interface & Streaming --- ## Hoofdstuk -**Deel 4: Advanced AI Features** (Les 13-18) +**Deel 4: Advanced AI & Deployment** (Les 13-18) ## Beschrijving -Leer professionele project setup en repository structuur. Begrijp hoe een goed georganiseerd project AI tools effectiever maakt en samenwerking vergemakkelijkt. +Bouwen van professionele chat interfaces met streaming responses. Message rendering, markdown support, error handling, loading states, en UX patterns voor AI-powered features. --- -## Te Behandelen +## Te Behandelen (~45 min) -### Groepsdiscussie (15 min) -Bespreek klassikaal de AI assistant reflecties uit Les 13 - welke instructies werkten goed en welke niet? - -### Waarom Project Structuur Belangrijk Is - -**Voor jezelf:** -- Sneller code terugvinden -- Makkelijker onderhouden -- Minder bugs door consistentie - -**Voor AI tools:** -- Betere context understanding -- Consistentere code generation -- Cursor/Claude begrijpt je project beter - -**Voor samenwerking:** -- Anderen begrijpen je code sneller -- Standaard conventies = minder discussie -- Onboarding nieuwe developers eenvoudiger +- Chat UI patterns en best practices +- useChat hook deep dive (state, loading, error) +- Streaming response handling en real-time updates +- Message rendering strategies en optimizations +- Markdown rendering in chat messages +- Error handling en error boundaries +- Loading states en skeleton loaders +- User input validation and sanitization +- Accessibility in chat interfaces (ARIA labels) +- Message persistence (localStorage of database) +- Performance optimization --- -### Next.js 14 Project Structuur +### useChat Hook Deep Dive -**Aanbevolen structuur:** -``` -project-root/ -├── src/ -│ ├── app/ # Next.js App Router -│ │ ├── (auth)/ # Route group voor auth pagina's -│ │ │ ├── login/ -│ │ │ └── register/ -│ │ ├── api/ # API routes -│ │ │ └── chat/ -│ │ ├── dashboard/ -│ │ ├── layout.tsx -│ │ ├── page.tsx -│ │ └── globals.css -│ │ -│ ├── components/ # React components -│ │ ├── ui/ # Basis UI components -│ │ │ ├── Button.tsx -│ │ │ ├── Input.tsx -│ │ │ └── Card.tsx -│ │ ├── layout/ # Layout components -│ │ │ ├── Header.tsx -│ │ │ ├── Footer.tsx -│ │ │ └── Sidebar.tsx -│ │ └── features/ # Feature-specifieke components -│ │ ├── auth/ -│ │ └── dashboard/ -│ │ -│ ├── lib/ # Utilities en configuraties -│ │ ├── supabase.ts # Supabase client -│ │ ├── utils.ts # Helper functies -│ │ └── constants.ts # App constanten -│ │ -│ ├── hooks/ # Custom React hooks -│ │ ├── useAuth.ts -│ │ └── useTodos.ts -│ │ -│ └── types/ # TypeScript types -│ ├── database.ts -│ └── api.ts -│ -├── public/ # Static assets -│ ├── images/ -│ └── favicon.ico -│ -├── docs/ # Documentatie -│ ├── PROMPT-LOG.md -│ ├── AI-DECISIONS.md -│ └── PROJECT-BRIEF.md -│ -├── .cursorrules # Cursor AI configuratie -├── .env.local # Environment variables (niet in git!) -├── .env.example # Template voor env vars -├── .gitignore -├── package.json -├── tsconfig.json -├── tailwind.config.ts -└── README.md +**State management met useChat:** + +```typescript +'use client' +import { useChat } from 'ai/react' + +export function ChatComponent() { + const { + messages, // All messages in conversation + input, // Current input text + handleInputChange, // Update input + handleSubmit, // Send message + isLoading, // Is AI responding? + error, // Any errors? + } = useChat({ + api: '/api/chat', + initialMessages: [], // Optional: pre-load messages + }) + + return ( + <> + <div> + {messages.map((msg) => ( + <div key={msg.id}> + <strong>{msg.role}:</strong> {msg.content} + </div> + ))} + </div> + {isLoading && <div>AI is thinking...</div>} + {error && <div>Error: {error.message}</div>} + <form onSubmit={handleSubmit}> + <input + value={input} + onChange={handleInputChange} + disabled={isLoading} + /> + <button type="submit" disabled={isLoading}>Send</button> + </form> + </> + ) +} ``` --- -### Component Organisatie +### Message Rendering Patterns -**UI Components (src/components/ui/):** -- Herbruikbare, generieke components -- Geen business logic -- Props-driven -- Voorbeelden: Button, Input, Modal, Card - -**Layout Components (src/components/layout/):** -- Structurele components -- Meestal één per type -- Voorbeelden: Header, Footer, Sidebar, Navigation - -**Feature Components (src/components/features/):** -- Business logic bevattend -- Specifiek voor één feature -- Groepeer per feature/domein - ---- - -### File Naming Conventions - -**Components:** -``` -✅ Button.tsx # PascalCase -✅ UserProfile.tsx -❌ button.tsx -❌ user-profile.tsx +**Basic pattern:** +```typescript +<div className="space-y-4"> + {messages.map((msg) => ( + <div + key={msg.id} + className={msg.role === 'user' ? 'ml-auto' : 'mr-auto'} + > + <div className="bg-gray-200 p-3 rounded"> + {msg.content} + </div> + </div> + ))} +</div> ``` -**Hooks:** -``` -✅ useAuth.ts # camelCase met 'use' prefix -✅ useTodos.ts -❌ UseAuth.ts -❌ auth-hook.ts +**With markdown rendering:** +```typescript +import ReactMarkdown from 'react-markdown' + +<div className="bg-gray-200 p-3 rounded"> + <ReactMarkdown>{msg.content}</ReactMarkdown> +</div> ``` -**Utilities:** -``` -✅ formatDate.ts # camelCase -✅ utils.ts -✅ constants.ts -``` - -**Types:** -``` -✅ database.ts # camelCase -✅ User.types.ts # optioneel: .types suffix +**With message types:** +```typescript +{messages.map((msg) => ( + <div key={msg.id} className={msg.role === 'user' ? 'user-message' : 'ai-message'}> + {msg.content} + </div> +))} ``` --- -### .cursorrules Setup +### Error Handling -**Maak .cursorrules in project root:** -```markdown -# Project: [Jouw Project Naam] +**Structured error handling:** -## Tech Stack -- Next.js 14 met App Router -- TypeScript (strict mode) -- Tailwind CSS -- Supabase -- React Query +```typescript +try { + const response = await fetch('/api/chat', { + method: 'POST', + body: JSON.stringify({ messages }), + }) -## File Structure -- Components in src/components/ -- UI components in src/components/ui/ -- API routes in src/app/api/ + if (!response.ok) { + throw new Error(`API error: ${response.status}`) + } -## Code Conventions -- Functional components only -- Named exports (geen default exports) -- Props interface boven component -- Nederlandse comments + // Handle streaming... +} catch (error) { + console.error('Chat error:', error) + setError({ + message: 'Failed to send message', + code: error instanceof Error ? error.message : 'unknown' + }) +} +``` -## Naming -- Components: PascalCase (Button.tsx) -- Hooks: camelCase met use prefix (useAuth.ts) -- Utils: camelCase (formatDate.ts) - -## Styling -- Tailwind CSS classes -- Geen inline styles -- Responsive mobile-first - -## TypeScript -- Strict mode -- Geen any types -- Interfaces voor props -- Types voor data - -## Don'ts -- Geen console.log in productie -- Geen hardcoded strings -- Geen unused imports +**Error boundary:** +```typescript +<ErrorBoundary fallback={<div>Chat error occurred</div>}> + <ChatComponent /> +</ErrorBoundary> ``` --- -### Git Best Practices +### Loading States -**Commit Message Format:** -``` -type: korte beschrijving +**Skeleton loader:** +```typescript +function MessageSkeleton() { + return ( + <div className="animate-pulse"> + <div className="bg-gray-300 h-4 rounded w-48 mb-2" /> + <div className="bg-gray-300 h-4 rounded w-64" /> + </div> + ) +} -Types: -- feat: nieuwe feature -- fix: bug fix -- refactor: code verbetering -- docs: documentatie -- style: formatting -- test: tests toevoegen -``` - -**Voorbeelden:** -```bash -git commit -m "feat: add user authentication with Supabase" -git commit -m "fix: resolve hydration error in TodoList" -git commit -m "docs: update README with setup instructions" -``` - -**.gitignore essentials:** -``` -# Dependencies -node_modules/ - -# Environment -.env*.local - -# Next.js -.next/ -out/ - -# IDE -.idea/ -.vscode/ - -# OS -.DS_Store +{isLoading && <MessageSkeleton />} ``` --- -### Environment Variables +### Input Validation -**Structuur:** -```bash -# .env.local (NOOIT committen!) -NEXT_PUBLIC_SUPABASE_URL=https://xxx.supabase.co -NEXT_PUBLIC_SUPABASE_ANON_KEY=eyJhbGci... -OPENAI_API_KEY=sk-... +**Validate before sending:** -# .env.example (WEL committen) -NEXT_PUBLIC_SUPABASE_URL=your-supabase-url -NEXT_PUBLIC_SUPABASE_ANON_KEY=your-anon-key -OPENAI_API_KEY=your-openai-key +```typescript +function handleSubmit(e: React.FormEvent) { + e.preventDefault() + + // Trim whitespace + const trimmedInput = input.trim() + + // Validate non-empty + if (!trimmedInput) { + setError('Message cannot be empty') + return + } + + // Validate length + if (trimmedInput.length > 1000) { + setError('Message too long (max 1000 chars)') + return + } + + // Send message + handleSubmit(e) +} ``` -**Regels:** -- `NEXT_PUBLIC_` prefix = zichtbaar in browser -- Zonder prefix = alleen server-side -- Nooit secrets in `NEXT_PUBLIC_` vars - --- -### README.md Template +### Message Persistence -```markdown -# Project Naam +**Save to localStorage:** -Korte beschrijving van je project. +```typescript +const [messages, setMessages] = useState(() => { + const saved = localStorage.getItem('chat_history') + return saved ? JSON.parse(saved) : [] +}) -## Features -- Feature 1 -- Feature 2 -- Feature 3 +// Save whenever messages change +useEffect(() => { + localStorage.setItem('chat_history', JSON.stringify(messages)) +}, [messages]) +``` -## Tech Stack -- Next.js 14 -- TypeScript -- Tailwind CSS -- Supabase -- Vercel AI SDK - -## Getting Started - -### Prerequisites -- Node.js 18+ -- npm of yarn -- Supabase account - -### Installation - -1. Clone de repository - ```bash - git clone https://github.com/username/project.git - cd project - ``` - -2. Installeer dependencies - ```bash - npm install - ``` - -3. Maak .env.local (zie .env.example) - -4. Start development server - ```bash - npm run dev - ``` - -## Environment Variables -Zie `.env.example` voor benodigde variabelen. - -## Deployment -Deployed op Vercel: [productie-url] - -## Documentatie -- [PROMPT-LOG.md](docs/PROMPT-LOG.md) -- [AI-DECISIONS.md](docs/AI-DECISIONS.md) +**Save to database:** +```typescript +const saveMessage = async (message: Message) => { + await fetch('/api/messages', { + method: 'POST', + body: JSON.stringify({ + content: message.content, + role: message.role, + userId: user.id, + }), + }) +} ``` --- ## Tools +- Vercel AI SDK +- React Markdown - Cursor -- Git -- GitHub +- TypeScript --- -## Lesopdracht (2 uur) +## Lesopdracht (2 uur, klassikaal) -### Setup Je Eindproject +### Build Professional Chat Interface -**Deel 1: Project Structuur (45 min)** +**Groepsdiscussie (15 min):** +Bespreek klassikaal de Vercel AI SDK ervaringen uit Les 13 - welke tool calling patterns werkten goed? -1. Maak nieuw Next.js project: - ```bash - npx create-next-app@latest mijn-eindproject --typescript --tailwind --app - ``` +**Deel 1: Chat Component (45 min)** -2. Maak de mappenstructuur: - - src/components/ui/ - - src/components/layout/ - - src/components/features/ - - src/lib/ - - src/hooks/ - - src/types/ - - docs/ +Build components/ChatInterface.tsx: +1. Use useChat hook +2. Render messages with proper styling +3. User vs AI message styling +4. Input form with validation +5. Tailwind + shadcn/ui components -3. Maak placeholder files: - - src/lib/supabase.ts - - src/lib/utils.ts +**Deel 2: Markdown & Error Handling (30 min)** -**Deel 2: Configuratie (30 min)** +1. Install react-markdown: `npm install react-markdown` +2. Render AI responses with markdown +3. Add error boundary +4. Show error messages to user +5. Proper loading states -1. Maak .cursorrules met jouw conventies -2. Maak .env.example -3. Update .gitignore -4. Maak README.md met template +**Deel 3: UX Improvements (30 min)** -**Deel 3: Git Setup (25 min)** +1. Auto-scroll to latest message +2. Disable input while loading +3. Show message count/token usage +4. Add clear chat history button +5. Save messages to localStorage -1. git init -2. Initial commit met goede message -3. Push naar GitHub -4. Check: .env.local NIET gecommit? +**Deel 4: Testing (15 min)** -**Deel 4: Documentatie Start (20 min)** - -Maak in docs/: -- PROJECT-BRIEF.md (beschrijving eindproject) -- PROMPT-LOG.md (leeg template) -- AI-DECISIONS.md (leeg template) +Test chat interface locally with various inputs and error scenarios. ### Deliverable -- GitHub repository met correcte structuur -- .cursorrules file -- README.md -- docs/ folder met templates +- Werkende chat interface component +- Markdown rendering working +- Error handling implemented +- LocalStorage persistence +- GitHub commit with chat UI --- ## Huiswerk (2 uur) -### Bouw Project Foundation +### Integrate Chat into Your Project -**Deel 1: Base Components (1 uur)** +**Deel 1: Project Integration (1 uur)** -Maak basis UI components met AI hulp: -- src/components/ui/Button.tsx -- src/components/ui/Input.tsx -- src/components/ui/Card.tsx -- src/components/layout/Header.tsx -- src/components/layout/Footer.tsx +1. Add chat component to your app +2. Connect to your API route with tools +3. Style to match your design +4. Test with actual tools/integrations +5. Fix any bugs -Requirements: -- TypeScript interfaces voor props -- Tailwind styling -- Responsive design -- Volg je .cursorrules +**Deel 2: Enhanced Features (30 min)** -**Deel 2: Supabase Setup (30 min)** +Add one of these: +- Message copy button +- Regenerate response option +- Clear history confirmation +- Export chat history +- Message timestamps -1. Maak Supabase project (of hergebruik van Les 9) -2. Configureer src/lib/supabase.ts -3. Voeg env vars toe aan .env.local -4. Test connectie +**Deel 3: Performance & Polish (30 min)** -**Deel 3: Eerste Feature (30 min)** - -Kies je eindproject en implementeer 1 basisfeature: -- Recipe Generator: ingredient input form -- Budget Buddy: expense entry form -- Travel Planner: destination search - -Commit en push! +1. Optimize re-renders (useMemo, useCallback) +2. Virtual scrolling for long chats +3. Better accessibility (keyboard nav) +4. Mobile responsive tweaks +5. Update docs/AI-DECISIONS.md ### Deliverable -- Werkende UI components -- Supabase connectie -- 1 basic feature -- Alle commits met goede messages - ---- - -## 💡 Eindopdracht - -Dit is een goed moment om te starten met **deelopdracht 1** van je eindopdracht. De setup die je vandaag maakt kun je direct gebruiken voor je eindproject. Bespreek je projectidee met de docent als je feedback wilt. +- Chat fully integrated in project +- Enhanced features implemented +- Performance optimized +- GitHub commits with improvements --- ## Leerdoelen + Na deze les kan de student: -- Een professionele project structuur opzetten -- File naming conventions toepassen -- Een effectieve .cursorrules file schrijven -- Git best practices volgen -- Environment variables correct beheren -- Een README.md schrijven -- Project documentatie structureren +- useChat hook volledig begrijpen en gebruiken +- Professionele chat UI patterns implementeren +- Markdown rendering in chat messages +- Error handling en error boundaries toepassen +- Loading states en skeletons bouwen +- User input valideren en sanitizen +- Message persistence (localStorage/DB) +- Accessibility in chat interfaces verbeteren +- Performance optimizations toepassen +- Complete chat feature in Next.js app integreren diff --git a/Samenvattingen/Les15-Samenvatting.md b/Samenvattingen/Les15-Samenvatting.md index fea1246..bcf9ba7 100644 --- a/Samenvattingen/Les15-Samenvatting.md +++ b/Samenvattingen/Les15-Samenvatting.md @@ -1,336 +1,130 @@ -# Les 15: MCP & Context Management +# Les 15: Eindproject Werkdag 1 --- ## Hoofdstuk -**Deel 4: Advanced AI Features** (Les 13-18) +**Deel 4: Advanced AI & Deployment** (Les 13-18) ## Beschrijving -Leer werken met Model Context Protocol (MCP) en geavanceerd context management. Begrijp hoe je AI tools maximale context geeft voor betere resultaten. +Eerste intensieve werkdag aan je eindproject. Focus op core features bouwen, Q&A sessions, individuele hulp, en probleemoplossing. Docent beschikbaar voor guidance en klassikale sparring. --- -## Te Behandelen +## Te Behandelen (~45 min guidance + 120 min work) -### Groepsdiscussie (15 min) -Bespreek klassikaal de project setup ervaringen uit Les 14 - welke structuur conventies werken goed en welke niet? - -### Wat is Context? - -**Context = wat de AI weet over jouw situatie** - -**Soorten context:** -- **Code context:** welke files, welke functies -- **Project context:** tech stack, conventies -- **Taak context:** wat je probeert te bereiken -- **Historische context:** eerdere conversatie - -**Meer context = betere antwoorden** +- Q&A session: wat loop je tegen aan? +- Cursor tips & tricks review +- Debugging strategies en common patterns +- Performance optimization basics +- Deployment troubleshooting preview +- Hands-on assistance beschikbaar +- Code review feedback +- Feature planning discussion --- -### Model Context Protocol (MCP) +## Doel van deze Werkdag -**Wat is MCP?** -- Open protocol van Anthropic -- Standaard manier om AI models context te geven -- Verbindt AI met externe tools en data - -**Waarom MCP?** -- AI kan nu "tools gebruiken" -- Toegang tot je filesystem -- Database queries uitvoeren -- Web searches doen -- En meer... - -**Hoe werkt het:** -``` -Jij → vraag → AI → MCP → Tool → resultaat → AI → antwoord → Jij -``` +- Core features van je eindproject afmaken +- Architecture validatie (is je setup goed?) +- Database integratie testen +- Auth flows uitwerken +- First AI feature implementeren +- Testing en bug fixes --- -### MCP in de Praktijk +### Checklist voor deze Sessie -**Cursor gebruikt MCP onder de hood:** -- `@file` mentions = file context via MCP -- `@codebase` = codebase search via MCP -- `@Docs` = documentation lookup via MCP -- `@Web` = web search via MCP +**Code Quality:** +- [ ] TypeScript geen errors +- [ ] No console.logs in production code +- [ ] Proper error handling +- [ ] Component props typed -**Claude Desktop met MCP:** -- Kan tools uitvoeren -- Filesystem access -- Terminal commands -- Database queries +**Features:** +- [ ] Core feature 1 werkend +- [ ] Core feature 2 werkend +- [ ] Data persistence (Supabase) +- [ ] Loading states visible +- [ ] Error states handled ---- - -### Context Management Strategieën - -**1. Expliciete Context** -Geef context direct in je prompt: -``` -Context: Next.js 14 project met TypeScript en Supabase. -Dit is een e-commerce app voor verkoop van boeken. - -Vraag: Hoe implementeer ik een winkelwagen? -``` - -**2. File Context** -Gebruik @ mentions om relevante files toe te voegen: -``` -@src/types/product.ts -@src/lib/supabase.ts -Hoe maak ik een addToCart functie? -``` - -**3. Project Context** -Gebruik .cursorrules of Claude Project instructions: -``` -# In .cursorrules -Dit project is een e-commerce platform. -Alle prices zijn in cents (niet euros). -``` - -**4. Fresh Context** -Begin nieuwe chat voor nieuw onderwerp: -- Voorkomt context vervuiling -- AI raakt niet in de war met oude info - ---- - -### @ Mentions Strategisch Gebruiken - -**Cursor @ mentions:** - -| Mention | Wanneer | Voorbeeld | -|---------|---------|-----------| -| `@file.tsx` | Specifieke file context | `@Button.tsx hoe voeg ik loading toe?` | -| `@folder/` | Hele folder context | `@components/ welke patterns gebruik ik?` | -| `@codebase` | Zoeken in project | `@codebase waar handle ik auth?` | -| `@Docs` | Officiële documentatie | `@Docs Next.js App Router` | -| `@Web` | Live web search | `@Web Supabase RLS policies` | - -**Best practice:** Combineer mentions voor rijke context: -``` -@src/components/auth/LoginForm.tsx -@src/lib/supabase.ts -@Docs Supabase Auth - -Hoe voeg ik Google OAuth toe aan mijn login? -``` - ---- - -### Claude Projects voor Persistente Context - -**Wat kun je toevoegen aan een Project:** -- Instructies (system prompt) -- Files (code, docs, schema's) -- Kennis die over sessies heen blijft - -**Project Instructions Template:** -```markdown -# Project: [Naam] - -## Tech Stack -- Next.js 14 met App Router -- TypeScript strict -- Tailwind CSS -- Supabase - -## Code Conventies -[jouw conventies] - -## Database Schema -[beschrijf je schema] - -## Current Focus -[waar werk je nu aan] -``` - -**Tip:** Upload je database schema, README, en belangrijke types files. - ---- - -### Context Windows en Limieten - -**Wat is een context window?** -- Maximum hoeveelheid tekst die AI kan "zien" -- Gemeten in tokens (±4 karakters per token) - -**Limieten per model:** -| Model | Context Window | -|-------|---------------| -| GPT-5 | 128K tokens | -| Claude 3 Sonnet | 200K tokens | -| Claude 3 Opus | 200K tokens | - -**Praktisch:** -- Lange conversaties → context vol -- Veel file mentions → context vol -- Start fresh chat wanneer nodig - -**Signs van vol context:** -- AI vergeet eerdere instructies -- Antwoorden worden minder relevant -- AI herhaalt zichzelf - ---- - -### Context Hygiene - -**Do's:** -- Geef alleen relevante context -- Wees specifiek in file mentions -- Start nieuwe chat voor nieuw onderwerp -- Gebruik project-level context voor consistentie - -**Don'ts:** -- Hele codebase als context geven -- Oude irrelevante chats voortzetten -- Te veel files tegelijk mentionen -- Context herhalen die AI al heeft - ---- - -### Debugging met Context - -**Wanneer AI verkeerde antwoorden geeft:** - -1. **Check context:** Heeft AI de juiste files? -2. **Check instructies:** Zijn project rules geladen? -3. **Fresh start:** Begin nieuwe chat -4. **Explicieter:** Voeg meer context toe in prompt - -**Debug prompt:** -``` -Ik merk dat je antwoord niet klopt. -Heb je toegang tot @src/lib/auth.ts? -Dit is de huidige implementatie: [plak code] -Kun je opnieuw kijken? -``` +**Setup:** +- [ ] Project structure clean +- [ ] .cursorrules up-to-date +- [ ] Environment variables configured +- [ ] Git history clean --- ## Tools -- Cursor (@ mentions) -- Claude Projects -- ChatGPT +- Cursor +- Chrome DevTools +- Supabase Dashboard +- GitHub +- Vercel (optional preview) --- -## Lesopdracht (2 uur) +## Lesopdracht (4 uur, hands-on werk) -### Context Management Oefenen +### Intensive Development Session -**Deel 1: @ Mentions Mastery (45 min)** +**Hou je voortgang bij:** -Voer deze taken uit in Cursor: +**Eerste uur (0-60 min):** +- [ ] Maak prioriteiten list van features +- [ ] Fix top 3 bugs van vorige sessies +- [ ] Test alles lokaal +- [ ] Ask docent for guidance if stuck -1. **Single file context:** - - Open je TodoList component - - Vraag: "@TodoList.tsx Hoe kan ik infinite scroll toevoegen?" - - Noteer kwaliteit antwoord +**Twee uur (60-120 min):** +- [ ] Implementeer core feature 1 (CRUD) +- [ ] Implementeer core feature 2 +- [ ] Add loading/error states +- [ ] Styling pass 1 -2. **Multi-file context:** - - Vraag: "@src/components/ @src/types/ Welke types mis ik?" - - Noteer hoe context helpt +**Drie uur (120-180 min):** +- [ ] Implementeer AI feature (tool calling) +- [ ] Test end-to-end flows +- [ ] Fix bugs found +- [ ] Performance check -3. **Docs context:** - - Vraag: "@Docs Supabase realtime Hoe voeg ik real-time updates toe aan mijn todo app?" - - Noteer of antwoord up-to-date is - -4. **Codebase search:** - - Vraag: "@codebase Waar handle ik error states?" - - Noteer of het de juiste plekken vindt - -**Deel 2: Claude Project Setup (30 min)** - -1. Maak Claude Project voor je eindproject -2. Schrijf comprehensive instructions -3. Upload 3-5 belangrijke files: - - Database schema/types - - Main component - - Supabase client -4. Test met 3 vragen - -**Deel 3: Context Vergelijking (45 min)** - -Voer dezelfde taak uit met: -1. Geen context (nieuwe chat, geen mentions) -2. File context (@mentions) -3. Project context (Claude Project) - -Taak: "Implementeer een search feature voor todos" - -Noteer: -- Kwaliteit code -- Relevantie voor jouw project -- Hoeveel aanpassing nodig +**Vier uur (180-240 min):** +- [ ] Polish UI +- [ ] Code cleanup +- [ ] Document decisions +- [ ] Commit and push ### Deliverable -- Notities over @ mentions effectiviteit -- Claude Project met instructions -- Context vergelijkingsnotities +- Werkende core features +- Updated PROMPT-LOG.md (add entries) +- Updated AI-DECISIONS.md +- GitHub commits with progress +- Screenshot of working features --- -## Huiswerk (2 uur) +## Huiswerk -### Optimaliseer Je Context Strategie +Continue building your project for next session. -**Deel 1: .cursorrules Perfectioneren (30 min)** - -Update je .cursorrules met: -- Specifieke file structure info -- Naming conventions met voorbeelden -- Common patterns in je project -- DON'Ts specifiek voor jouw situatie - -**Deel 2: Claude Project Uitbreiden (45 min)** - -1. Upload alle relevante project files -2. Test met complexe vragen: - - "Hoe implementeer ik feature X?" - - "Review mijn auth implementatie" - - "Wat ontbreekt er nog?" -3. Itereer op instructions - -**Deel 3: Context Documentation (45 min)** - -Maak `docs/CONTEXT-GUIDE.md`: -```markdown -# Context Guide voor [Project] - -## Cursor @ Mentions -- Voor UI changes: @src/components/ -- Voor data logic: @src/lib/ @src/hooks/ -- Voor types: @src/types/ - -## Claude Project -- Project URL: [link] -- Uploaded files: [lijst] -- Best practices: [jouw learnings] - -## Common Prompts -[verzameling werkende prompts] -``` - -### Deliverable -- Geoptimaliseerde .cursorrules -- Complete Claude Project -- docs/CONTEXT-GUIDE.md +**Checklist:** +- [ ] Core features complete +- [ ] All changes pushed to GitHub +- [ ] PROMPT-LOG.md updated with prompts used +- [ ] AI-DECISIONS.md updated with choices made +- [ ] Ready for next workday session --- ## Leerdoelen + Na deze les kan de student: -- Uitleggen wat context is en waarom het belangrijk is -- Model Context Protocol (MCP) begrijpen -- @ mentions strategisch gebruiken in Cursor -- Een Claude Project opzetten met effectieve instructions -- Context windows en limieten begrijpen -- Context management best practices toepassen -- Debuggen wanneer AI verkeerde antwoorden geeft door context issues +- Zelfstandig problemen identificeren en fixen +- Cursor effectief gebruiken voor rapid development +- Architectuur decisions valideren en aanpassen +- Debugging strategieën toepassen +- Features end-to-end implementeren +- Development workflows versnellen +- Documentatie up-to-date houden diff --git a/Samenvattingen/Les16-Samenvatting.md b/Samenvattingen/Les16-Samenvatting.md index 622eb2e..43be8e9 100644 --- a/Samenvattingen/Les16-Samenvatting.md +++ b/Samenvattingen/Les16-Samenvatting.md @@ -1,346 +1,122 @@ -# Les 16: Mastering Cursor +# Les 16: Eindproject Werkdag 2 --- ## Hoofdstuk -**Deel 4: Advanced AI Features** (Les 13-18) +**Deel 4: Advanced AI & Deployment** (Les 13-18) ## Beschrijving -Verdieping in Cursor's geavanceerde features. Leer model keuze, Composer Mode, @ mentions, en .cursorrules optimaal gebruiken. +Verlengde werksessie voor je eindproject. Continue Development Day waar je intensief aan je AI-powered applicatie werkt met begeleiding en feedback van de docent. --- ## Te Behandelen ### Groepsdiscussie (15 min) -Bespreek klassikaal de context management ervaringen uit Les 15 - welke strategieën werkten het beste? +Bespreek klassikaal de deployment ervaringen uit Les 15 - welke problemen kwamen jullie tegen en hoe losten jullie die op? -### Model Keuze +### Doel van deze werkdag -**Wanneer welk model?** - -| Model | Gebruik voor | Kosten | -|-------|-------------|--------| -| **Haiku** | Simpele taken, autocomplete | Goedkoop | -| **Sonnet** | Dagelijks werk, de meeste taken | Medium | -| **Opus** | Complexe architectuur, multi-file | Duur | - -**Vuistregels:** -- Tab completion: Haiku (automatisch) -- CMD+K: Sonnet (default) -- Composer: Sonnet of Opus -- Complexe debugging: Opus +Deze les is puur praktisch werken: +- Bugs fixen die je bent tegengekomen +- Features afmaken die nog niet klaar zijn +- Code cleanup en optimalisatie +- Performance verbeteren +- Documentatie aanpassen +- Testen in productie --- -### Composer Mode Diepgaand +### Checklist voor deze sessie -**Wat is Composer?** -Multi-file generatie in één keer. Cursor plant en voert wijzigingen uit over meerdere bestanden. +**Code Quality:** +- [ ] TypeScript geen errors +- [ ] ESLint check passed +- [ ] Geen unused imports/variables +- [ ] Proper error handling overal -**Wanneer Composer gebruiken:** -- Nieuwe feature met meerdere components -- Refactoring over meerdere files -- Boilerplate generatie -- Complexe wijzigingen +**Features:** +- [ ] Alle core features werken +- [ ] AI feature functionaliteit compleet +- [ ] Edge cases afgehandeld +- [ ] Loading states geimplementeerd +- [ ] Error states geimplementeerd -**Composer Workflow:** -1. CMD+I opent Composer -2. Beschrijf je doel duidelijk -3. Voeg context toe met @ mentions -4. Laat Cursor plannen -5. Review het plan -6. Accept of reject per file -7. Itereer met feedback +**Performance:** +- [ ] Lighthouse score > 80 +- [ ] Images optimized +- [ ] Lazy loading waar nodig +- [ ] Bundle size reasonable -**Voorbeeld prompt:** -``` -Create a user profile page with: -- @components/ui/ style components -- Profile header with avatar -- Edit form with validation -- Save to @lib/supabase.ts -- Loading and error states -``` - ---- - -### @ Mentions Systeem - -**Alle types:** - -| Mention | Wat het doet | Voorbeeld | -|---------|--------------|-----------| -| `@file.tsx` | Specifieke file | `@Button.tsx` | -| `@folder/` | Hele folder | `@components/` | -| `@codebase` | Zoek in codebase | `@codebase auth logic` | -| `@Docs` | Officiële docs | `@Docs Next.js routing` | -| `@Web` | Web zoeken | `@Web Supabase auth setup` | - -**Best practices:** -- Wees specifiek met file mentions -- Gebruik folder mentions voor context -- @Docs voor up-to-date informatie -- Combineer mentions voor betere context - ---- - -### .cursorrules Advanced - -**Meerdere rules files:** - -``` -.cursor/ -└── rules/ - ├── general.mdc # Project-brede regels - ├── components.mdc # Component conventies - ├── api.mdc # API route regels - └── testing.mdc # Test conventies -``` - -**Effectieve rules schrijven:** - -```markdown -# Component Rules - -## Structure -Alle components moeten volgen: -1. Props interface bovenaan -2. Component function -3. Named export onderaan - -## Example -\`\`\`tsx -interface ButtonProps { - label: string - onClick: () => void - variant?: 'primary' | 'secondary' -} - -export function Button({ label, onClick, variant = 'primary' }: ButtonProps) { - return ( - <button onClick={onClick} className={...}> - {label} - </button> - ) -} -\`\`\` - -## DON'Ts -- Geen default exports -- Geen inline styles -- Geen any types -``` - ---- - -### Codebase Indexing - -**Hoe Cursor indexeert:** -- Scant alle files in je project -- Bouwt semantic understanding -- Gebruikt voor autocomplete en context - -**Optimaliseren:** -1. Goede `.cursorignore` (node_modules, .next, etc.) -2. Semantische naming -3. Duidelijke file structuur -4. Comments waar nodig - -**Re-indexeren:** -CMD+Shift+P → "Reindex" - ---- - -### Cost Management - -**Token gebruik monitoren:** -- Cursor toont token count in chat -- Check monthly usage in settings - -**Bespaartips:** -1. Gebruik Haiku voor simpele taken -2. Beperk context (niet hele codebase) -3. Wees specifiek in prompts -4. Fresh chat voor nieuwe onderwerpen - -**Free tier strategie:** -- Focus op Tab completion (onbeperkt) -- Gebruik CMD+K spaarzaam -- Composer alleen voor grote taken - ---- - -### Debugging met Cursor - -**AI-Assisted Debugging:** - -**Stap 1: Error identificeren** -``` -@file-met-error.tsx -Ik krijg deze error: [plak error] -Wat gaat er mis? -``` - -**Stap 2: Context toevoegen** -``` -@file-met-error.tsx -@gerelateerde-file.ts -De error treedt op wanneer ik X doe. -Console log toont: [data] -``` - -**Stap 3: Fix implementeren** -- Selecteer code met error -- CMD+K → "Fix this error: [beschrijving]" -- Review en test - ---- - -### Refactoring met Cursor - -**Pattern 1: Extract Component** -``` -Selecteer JSX block → CMD+K -"Extract this into a separate component called ProductCard" -``` - -**Pattern 2: Extract Hook** -``` -Selecteer state + useEffect → CMD+K -"Extract this into a custom hook called useProductData" -``` - -**Pattern 3: Improve Performance** -``` -@Component.tsx -"Optimize this component: -- Add memoization waar nodig -- Fix unnecessary re-renders -- Improve loading performance" -``` +**Deployment:** +- [ ] Builds succesvol lokaal +- [ ] Alle env vars in Vercel +- [ ] Works in production +- [ ] Supabase redirects configured --- ## Tools - Cursor -- Claude models (Haiku/Sonnet/Opus) -- .cursorrules +- Chrome DevTools +- Vercel Dashboard +- Supabase Dashboard +- GitHub --- -## Lesopdracht (2 uur) +## Lesopdracht (4 uur) -### Multi-Step Form Wizard +### Intensive Development Session -**Bouw met Composer:** +**Hou je voortgang bij:** -| Stap | Features | -|------|----------| -| 1 | Personal info (naam, email) | -| 2 | Preferences (theme, notifications) | -| 3 | Review & confirm | -| 4 | Success animation | +**Eerste uur:** +- [ ] Maak prioriteiten list +- [ ] Fix top 3 bugs +- [ ] Test alles lokaal -**Requirements:** -- Progress indicator -- Per-stap validatie -- localStorage persistence -- TypeScript strict -- Tailwind styling -- Mobile responsive +**Twee uur:** +- [ ] Implementeer ontbrekende features +- [ ] Add missing error handling +- [ ] Improve styling/UX -**Process:** +**Drie uur:** +- [ ] Code cleanup +- [ ] Performance optimization +- [ ] Redeploy -**Deel 1: Composer Setup (30 min)** -1. Open Composer (CMD+I) -2. Schrijf comprehensive prompt -3. Include @ mentions naar relevante files -4. Kies Sonnet of Opus - -**Deel 2: Generatie & Review (45 min)** -1. Laat Composer genereren -2. Review elke file -3. Accept wat goed is -4. Reject wat niet past - -**Deel 3: Refinement (45 min)** -1. Gebruik CMD+K voor kleine fixes -2. Chat voor vragen -3. Itereer tot het werkt +**Vier uur:** +- [ ] Final testing in production +- [ ] Screenshot taken +- [ ] Docs updated ### Deliverable -- Werkende form wizard -- Notities: welk model wanneer, hoeveel iteraties +- Werkende app (lokaal en productie) +- Updated PROMPT-LOG.md +- Updated AI-DECISIONS.md +- Screenshot van werkende app --- -## Huiswerk (2 uur) +## Huiswerk -### Perfecte .cursorrules - -**Deel 1: Research (30 min)** -- Zoek 3-5 .cursorrules voorbeelden online -- Analyseer wat ze effectief maakt - -**Deel 2: Write Comprehensive Rules (1 uur)** - -Maak complete .cursorrules voor je eindproject: - -```markdown -# [Project Naam] - -## Tech Stack -[Jouw stack] - -## Code Conventions -[Jouw conventies] - -## File Naming -[Jouw regels] - -## Component Structure -[Jouw patterns] - -## Styling -[Tailwind regels] - -## API Routes -[Route conventies] - -## Error Handling -[Error patterns] - -## DON'Ts -[Wat te vermijden] -``` - -**Deel 3: Test (30 min)** -1. Start nieuw component -2. Vraag Cursor om het te bouwen -3. Check: volgt Cursor je regels? -4. Itereer indien nodig +Continue working on your end project for next session. Make sure to: +- Push all changes to GitHub +- Document what you've done +- List remaining work for next session ### Deliverable -- Complete .cursorrules file -- Screenshot van Cursor die regels volgt -- Korte analyse: wat werkt goed, wat niet - ---- - -## 💡 Eindopdracht Check-in - -Hoe gaat je eindproject? Loop je ergens tegenaan? Dit is een goed moment om vragen te stellen en feedback te krijgen van de docent en klasgenoten. +- All work committed and pushed +- Documentation current +- Task list for Les 17 --- ## Leerdoelen Na deze les kan de student: -- Het juiste Claude model kiezen per taak -- Composer Mode effectief gebruiken voor multi-file features -- @ mentions strategisch inzetten voor context -- Geavanceerde .cursorrules files schrijven -- Codebase indexing optimaliseren -- Token gebruik monitoren en kosten beheren -- AI-assisted debugging toepassen -- Refactoring uitvoeren met Cursor +- Zelfstandig bugs identificeren en fixen +- Code quality verbeteren +- Performance optimisatie toepassen +- Productie deployment testen en valideren +- Documentatie up-to-date houden diff --git a/Samenvattingen/Les17-Samenvatting.md b/Samenvattingen/Les17-Samenvatting.md index 957d096..f808727 100644 --- a/Samenvattingen/Les17-Samenvatting.md +++ b/Samenvattingen/Les17-Samenvatting.md @@ -1,483 +1,207 @@ -# Les 17: Vercel AI SDK, Tool Calling & Agents +# Les 17: Eindproject Polish & Code Review --- ## Hoofdstuk -**Deel 4: Advanced AI Features** (Les 13-18) +**Deel 4: Advanced AI & Deployment** (Les 13-18) ## Beschrijving -Bouw AI-powered features in je apps met de Vercel AI SDK. Leer niet alleen chat interfaces bouwen, maar ook hoe AI externe data kan ophalen via Tool Calling en autonome taken kan uitvoeren als Agent. +Finale polish fase van je eindproject. Focus op code review, peer feedback, en laatste verbeteringen voor inlevering. Voorbereiding op presentatie. --- ## Te Behandelen ### Groepsdiscussie (15 min) -Bespreek klassikaal de Cursor .cursorrules ervaringen uit Les 16 - welke regels zorgen voor betere AI output? +Bespreek klassikaal de werkdag voortgang uit Les 16 - wat hebben jullie afgekregen, wat bleek moeilijker dan verwacht? -### Waarom Vercel AI SDK? +### Code Review Checklist -**Het probleem:** Direct API calls naar OpenAI/Anthropic zijn complex: -- Streaming handmatig implementeren -- Error handling -- State management -- Tool calling implementeren +**TypeScript & Code Quality:** +- [ ] No TypeScript errors +- [ ] No `any` types +- [ ] Props properly typed +- [ ] Error handling complete +- [ ] No console.logs in production -**De oplossing:** Vercel AI SDK -- Simpele React hooks (`useChat`, `useCompletion`) -- Built-in streaming -- Provider-agnostic (OpenAI, Anthropic, etc.) -- **Tool calling out-of-the-box** -- **Agent capabilities met `maxSteps`** +**React Best Practices:** +- [ ] No unnecessary re-renders +- [ ] Keys properly set in lists +- [ ] Hooks rules followed +- [ ] Components split logically +- [ ] Prop drilling minimized + +**Styling & UX:** +- [ ] Responsive design working +- [ ] Mobile friendly +- [ ] Consistent styling +- [ ] Accessible (alt text, labels, etc.) +- [ ] No visual bugs + +**Performance:** +- [ ] Lighthouse > 80 +- [ ] Lazy load images +- [ ] Optimize bundles +- [ ] Fast interactions +- [ ] Minimal flickering --- -### Installatie & Setup +### Peer Review Process -```bash -npm install ai @ai-sdk/openai zod -# zod is nodig voor tool parameter validatie -``` +**Hoe peer review doen:** -**Environment variable:** -```bash -# .env.local -OPENAI_API_KEY=sk-xxxxx -``` +1. **Voorbereiding (10 min)** + - Share productie URL of GitHub link + - List main features + - Highlight AI features + +2. **Review (15 min)** + - Reviewer tests alle features + - Takes notes + - Looks at code (if applicable) + +3. **Feedback (10 min)** + - ✅ Wat werkt goed + - ⚠️ What could improve + - ❌ Any bugs found + +4. **Discussion (5 min)** + - Q&A + - Discuss suggestions + - Agree on priorities --- -### Deel 1: Basic Chat (Herhaling) +### Final Checklist for Submission -#### useChat Hook +**Functionality:** +- [ ] All features work in production +- [ ] Auth flows complete +- [ ] CRUD operations complete +- [ ] AI feature functional +- [ ] No console errors -```tsx -'use client' -import { useChat } from 'ai/react' +**Documentation:** +- [ ] README.md complete +- [ ] PROMPT-LOG.md has 10+ entries +- [ ] AI-DECISIONS.md has 5+ entries +- [ ] .env.example up to date +- [ ] Setup instructions clear -export function ChatComponent() { - const { messages, input, handleInputChange, handleSubmit, isLoading } = useChat() +**Code Quality:** +- [ ] Code is clean and organized +- [ ] Comments where needed +- [ ] Consistent naming +- [ ] No dead code +- [ ] .cursorrules present - return ( - <div className="flex flex-col h-screen"> - <div className="flex-1 overflow-y-auto p-4"> - {messages.map(m => ( - <div key={m.id} className={m.role === 'user' ? 'text-right' : 'text-left'}> - <span className="inline-block p-2 rounded-lg bg-gray-100"> - {m.content} - </span> - </div> - ))} - </div> +**Performance & UX:** +- [ ] Lighthouse score > 80 +- [ ] Loading states visible +- [ ] Error states handled +- [ ] Mobile responsive +- [ ] Fast load times - <form onSubmit={handleSubmit} className="p-4 border-t"> - <input - value={input} - onChange={handleInputChange} - placeholder="Type a message..." - className="w-full p-2 border rounded" - /> - </form> - </div> - ) -} -``` - -#### Basic API Route - -```typescript -// app/api/chat/route.ts -import { openai } from '@ai-sdk/openai' -import { streamText } from 'ai' - -export async function POST(req: Request) { - const { messages } = await req.json() - - const result = streamText({ - model: openai('gpt-4o-mini'), - system: 'You are a helpful assistant.', - messages, - }) - - return result.toDataStreamResponse() -} -``` - ---- - -### Deel 2: Tool Calling - AI + Externe Data - -**Het probleem met basic chat:** -- AI kent alleen zijn training data -- Geen toegang tot realtime informatie -- Kan geen acties uitvoeren - -**De oplossing: Tool Calling** -- Definieer "tools" die AI kan aanroepen -- AI besluit zelf wanneer een tool nodig is -- Tool haalt data op → AI interpreteert resultaat - -#### Voorbeeld: Cocktail Advisor met TheCocktailDB - -```typescript -// app/api/chat/route.ts -import { openai } from '@ai-sdk/openai' -import { streamText, tool } from 'ai' -import { z } from 'zod' - -export async function POST(req: Request) { - const { messages } = await req.json() - - const result = streamText({ - model: openai('gpt-4o-mini'), - system: `Je bent een cocktail expert. - Gebruik de tools om cocktails te zoeken en recepten op te halen. - Geef persoonlijk advies op basis van de resultaten.`, - messages, - tools: { - // Tool 1: Zoek cocktails op ingrediënt - searchByIngredient: tool({ - description: 'Zoek cocktails die een specifiek ingrediënt bevatten', - parameters: z.object({ - ingredient: z.string().describe('Het ingrediënt om op te zoeken, bijv. "rum" of "vodka"') - }), - execute: async ({ ingredient }) => { - const res = await fetch( - `https://www.thecocktaildb.com/api/json/v1/1/filter.php?i=${ingredient}` - ) - const data = await res.json() - return data.drinks?.slice(0, 5) || [] - } - }), - - // Tool 2: Haal cocktail details op - getCocktailDetails: tool({ - description: 'Haal het volledige recept van een cocktail op', - parameters: z.object({ - cocktailId: z.string().describe('Het ID van de cocktail') - }), - execute: async ({ cocktailId }) => { - const res = await fetch( - `https://www.thecocktaildb.com/api/json/v1/1/lookup.php?i=${cocktailId}` - ) - const data = await res.json() - return data.drinks?.[0] || null - } - }), - - // Tool 3: Zoek non-alcoholische opties - searchNonAlcoholic: tool({ - description: 'Zoek non-alcoholische cocktails/mocktails', - parameters: z.object({}), - execute: async () => { - const res = await fetch( - `https://www.thecocktaildb.com/api/json/v1/1/filter.php?a=Non_Alcoholic` - ) - const data = await res.json() - return data.drinks?.slice(0, 5) || [] - } - }) - } - }) - - return result.toDataStreamResponse() -} -``` - -**Wat gebeurt er?** -``` -User: "Ik heb rum en limoen, wat kan ik maken?" - -AI denkt: "Ik moet zoeken op rum" -→ Roept searchByIngredient({ ingredient: "rum" }) aan -→ Krijgt: [{ name: "Mojito", id: "11000" }, { name: "Daiquiri", id: "11006" }, ...] - -AI denkt: "Mojito klinkt goed met limoen, laat me het recept ophalen" -→ Roept getCocktailDetails({ cocktailId: "11000" }) aan -→ Krijgt: { name: "Mojito", ingredients: [...], instructions: "..." } - -AI antwoordt: "Met rum en limoen kun je een heerlijke Mojito maken! - Je hebt nog nodig: verse munt en suiker..." -``` - ---- - -### Deel 3: Agents - Autonome Multi-Step AI - -**Van Tool Calling naar Agent:** -- Tool calling = AI roept 1 tool aan, klaar -- Agent = AI blijft tools aanroepen totdat de taak af is - -**Het verschil is één parameter: `maxSteps`** - -```typescript -const result = streamText({ - model: openai('gpt-4o-mini'), - system: `Je bent een cocktail party planner. - Plan een compleet menu met alle details.`, - messages, - tools: { /* ... tools ... */ }, - maxSteps: 8 // ← Agent mag 8 tool-calls doen -}) -``` - -#### Voorbeeld: Party Planner Agent - -```typescript -// app/api/party-planner/route.ts -import { openai } from '@ai-sdk/openai' -import { streamText, tool } from 'ai' -import { z } from 'zod' - -export async function POST(req: Request) { - const { messages } = await req.json() - - const result = streamText({ - model: openai('gpt-4o'), // Gebruik slimmer model voor agent taken - system: `Je bent een professionele cocktail party planner. - - Wanneer iemand een feest wil plannen: - 1. Zoek eerst cocktails die passen bij de wensen - 2. Haal recepten op van de beste opties - 3. Denk aan non-alcoholische alternatieven - 4. Geef een compleet overzicht met ingrediënten - - Wees proactief en denk mee.`, - messages, - tools: { - searchByIngredient: tool({ - description: 'Zoek cocktails met een ingrediënt', - parameters: z.object({ - ingredient: z.string() - }), - execute: async ({ ingredient }) => { - const res = await fetch( - `https://www.thecocktaildb.com/api/json/v1/1/filter.php?i=${ingredient}` - ) - return res.json() - } - }), - - getCocktailDetails: tool({ - description: 'Haal recept details op', - parameters: z.object({ - cocktailId: z.string() - }), - execute: async ({ cocktailId }) => { - const res = await fetch( - `https://www.thecocktaildb.com/api/json/v1/1/lookup.php?i=${cocktailId}` - ) - return res.json() - } - }), - - searchNonAlcoholic: tool({ - description: 'Zoek mocktails', - parameters: z.object({}), - execute: async () => { - const res = await fetch( - `https://www.thecocktaildb.com/api/json/v1/1/filter.php?a=Non_Alcoholic` - ) - return res.json() - } - }), - - searchByCategory: tool({ - description: 'Zoek cocktails per categorie (Cocktail, Shot, Beer, etc.)', - parameters: z.object({ - category: z.string() - }), - execute: async ({ category }) => { - const res = await fetch( - `https://www.thecocktaildb.com/api/json/v1/1/filter.php?c=${category}` - ) - return res.json() - } - }) - }, - maxSteps: 10 // Agent kan tot 10 tool calls doen - }) - - return result.toDataStreamResponse() -} -``` - -**Agent in actie:** -``` -User: "Plan cocktails voor mijn verjaardagsfeest. - 15 mensen, een paar drinken geen alcohol, - we houden van citrus smaken." - -Agent stappen: -1. searchByIngredient("lemon") → 12 cocktails -2. searchByIngredient("lime") → 15 cocktails -3. searchByIngredient("orange") → 10 cocktails -4. searchNonAlcoholic() → 8 mocktails -5. getCocktailDetails("11000") → Mojito recept -6. getCocktailDetails("11007") → Margarita recept -7. getCocktailDetails("12162") → Virgin Piña Colada recept -8. getCocktailDetails("12316") → Lemonade recept - -Output: Compleet party plan met: -- 3 alcoholische cocktails met citrus -- 2 mocktails voor niet-drinkers -- Gecombineerde ingrediëntenlijst -- Tips voor bereiding -``` - ---- - -### Gratis APIs voor Projecten - -| API | Data | URL | Auth | -|-----|------|-----|------| -| TheCocktailDB | 636 cocktails, recepten | thecocktaildb.com/api.php | Geen (key=1) | -| TheMealDB | 597 recepten, ingrediënten | themealdb.com/api.php | Geen (key=1) | -| Open Trivia DB | 4000+ quiz vragen | opentdb.com/api_config.php | Geen | -| REST Countries | Landen data | restcountries.com | Geen | -| Open Library | Boeken data | openlibrary.org/developers | Geen | - ---- - -### Best Practices - -**Tool Design:** -```typescript -// ✅ Goed: Specifieke, duidelijke tools -searchByIngredient: tool({ - description: 'Zoek cocktails die een specifiek ingrediënt bevatten', - // ... -}) - -// ❌ Slecht: Vage tool -search: tool({ - description: 'Zoek iets', - // ... -}) -``` - -**Agent System Prompts:** -```typescript -// ✅ Goed: Geef duidelijke instructies -system: `Je bent een cocktail expert. - -Wanneer je een vraag krijgt: -1. Zoek eerst relevante cocktails -2. Haal details op van de beste matches -3. Geef persoonlijk advies - -Wees proactief en denk mee met de gebruiker.` - -// ❌ Slecht: Te vaag -system: `Je bent een assistent.` -``` - -**Error Handling in Tools:** -```typescript -execute: async ({ ingredient }) => { - try { - const res = await fetch(`...`) - if (!res.ok) { - return { error: 'Kon geen cocktails vinden' } - } - return res.json() - } catch (error) { - return { error: 'API niet beschikbaar' } - } -} -``` +**Deployment:** +- [ ] Deployed on Vercel +- [ ] Working on production URL +- [ ] Supabase configured +- [ ] Environment variables secure +- [ ] No errors in production --- ## Tools -- Vercel AI SDK (`ai` package) -- Zod (parameter validatie) -- Next.js API Routes -- Externe APIs (TheCocktailDB, TheMealDB, etc.) +- GitHub +- Vercel +- Chrome DevTools +- Cursor +- Peer reviewers --- -## Lesopdracht (2 uur) +## Lesopdracht (3 uur) -### Bouw een AI Agent met Externe Data +### Code Review & Polish Session -**Deel 1: Setup (15 min)** -1. `npm install ai @ai-sdk/openai zod` -2. Voeg `OPENAI_API_KEY` toe aan `.env.local` -3. Kies je API: TheCocktailDB of TheMealDB +**Deel 1: Peer Review (1 uur)** -**Deel 2: Basic Tool Calling (45 min)** -1. Maak `/api/chat/route.ts` -2. Implementeer 2 tools: - - Zoek op ingrediënt - - Haal details op -3. Test: "Wat kan ik maken met [ingrediënt]?" +Work in pairs or small groups: +1. Exchange project URLs/repos +2. Each person reviews another's work +3. Take detailed notes +4. Provide constructive feedback +5. Discuss improvements -**Deel 3: Agent met maxSteps (45 min)** -1. Voeg `maxSteps: 5` toe -2. Voeg een 3e tool toe (bijv. zoek per categorie) -3. Verbeter je system prompt voor agent gedrag -4. Test: "Help me een menu plannen voor..." +**Deel 2: Final Polish (1.5 uur)** -**Deel 4: Frontend (15 min)** -1. Bouw chat UI met `useChat` -2. Voeg loading indicator toe -3. Test de complete flow +Based on feedback: +1. Fix identified bugs +2. Implement suggested improvements +3. Code cleanup +4. Update documentation +5. Final test in production + +**Deel 3: Final Checks (30 min)** + +Go through the submission checklist: +1. Verify all items are done +2. Test everything once more +3. Make final commits +4. Push to GitHub +5. Screenshot for documentation ### Deliverable -- Werkende agent met minimaal 3 tools -- Chat interface -- Screenshot van agent die meerdere tools aanroept +- Peer review feedback received +- All feedback items addressed +- Final production-ready code +- Complete documentation +- Screenshot of final app --- -## Huiswerk (2 uur) +## Huiswerk -### Bouw AI Feature voor Eindproject +**Final submission preparation:** -**Deel 1: Agent Design (30 min)** +1. **Complete ALL documentation:** + - README with features and setup + - PROMPT-LOG.md with 10+ prompts + - AI-DECISIONS.md with 5+ decisions + - Project state documented -Plan je agent voor de eindopdracht: -- Welke externe API gebruik je? -- Welke tools heeft je agent nodig? (minimaal 3) -- Wat is de typische flow? +2. **Final testing:** + - Test all features in production + - Check Lighthouse score + - Verify mobile responsiveness + - Check load times -Documenteer in `docs/AI-DECISIONS.md` +3. **Code review:** + - Ask classmates to review code + - Ask docent for feedback + - Fix any issues found + - Final cleanup -**Deel 2: Implementatie (1 uur)** - -Bouw de agent voor je eindproject: -- Minimaal 3 tools -- `maxSteps` van minimaal 3 -- Goede error handling -- Relevante system prompt - -**Deel 3: Integratie (30 min)** - -Combineer met Supabase: -- Sla user preferences op -- Geef preferences mee als context aan agent -- Sla conversation history op +4. **Prepare for submission:** + - Ensure Git history is clean + - All commits have good messages + - GitHub repo is public/accessible + - Production URL is stable ### Deliverable -- Werkende agent in eindproject -- `docs/AI-DECISIONS.md` met agent design -- Minimaal 5 prompts in `PROMPT-LOG.md` - ---- - -## 💡 Eindopdracht - -Heb je al nagedacht over je AI feature? Dit is het moment om je idee te bespreken met de docent en klasgenoten. Welke externe API ga je gebruiken? Welke tools heeft je agent nodig? +- Final, polished application +- All documentation complete +- Code review completed +- Ready for submission --- ## Leerdoelen Na deze les kan de student: -- Vercel AI SDK installeren en configureren -- Tools definiëren met Zod parameters -- Tool Calling implementeren voor externe API integratie -- Agents bouwen met `maxSteps` voor autonome taken -- De juiste aanpak kiezen (basic chat vs tool calling vs agent) -- Error handling implementeren in tools -- Gratis externe APIs integreren in AI features +- Code review uitvoeren volgens best practices +- Peer feedback ontvangen en implementeren +- Final polish toepassen op projecten +- Production checklist doorlopen +- Professional quality deliverables opleveren +- Zelfstandig werk evalueren en verbeteren diff --git a/Samenvattingen/Les18-Samenvatting.md b/Samenvattingen/Les18-Samenvatting.md index 6a44657..f8981d7 100644 --- a/Samenvattingen/Les18-Samenvatting.md +++ b/Samenvattingen/Les18-Samenvatting.md @@ -3,7 +3,7 @@ --- ## Hoofdstuk -**Deel 4: Advanced AI Features** (Les 13-18) +**Deel 4: Advanced AI & Deployment** (Les 13-18) ## Beschrijving Deploy je eindproject naar productie. Leer environment variables, Vercel deployment, en basis performance optimalisatie. Bespreking van de eindopdracht requirements en afrondende werksessie. diff --git a/readme.md b/readme.md index 7ba8818..f75686d 100644 --- a/readme.md +++ b/readme.md @@ -13,33 +13,46 @@ Een 18-weekse cursus die studenten meeneemt van AI-beginner naar AI-powered deve | **v1** | Gegeven | Originele lessen (Les01, Les02 mappen) | | **[v2](v2/)** | In ontwikkeling | Verbeterde lessen op basis van feedback | -**Feedback & Reflectie:** [v1-feedback.md](v1-feedback.md) - Bevindingen na het geven van les 1, 2 en 3 +**Feedback & Reflectie:** [v1-feedback.md](v1-feedback.md) - Bevindingen na het geven van les 1-3 + +--- + +## Lesformat (Nieuw!) + +**Feedback van studenten:** Het traditionele "1 uur lecture + 2 uur solo werk" werkt niet optimaal. Studenten voelen zich verloren en geven voorkeur aan samenwerken. + +**Nieuw format (toegepast op alle lessen):** +- **~45 minuten:** Theorie met live demo's (docent codeert en verklaart) +- **15 minuten:** Pauze +- **~120 minuten:** Klassikaal bouwen (Tim codeert voor, studenten volgen mee met regelmatige "nu jullie" momenten waar zij zelf code schrijven) + +Dit format betrekent meer interactie, sneller feedback en meer mogelijkheden voor sparring en samenwerken. --- ## Overzicht -> **Let op:** Curriculum v2 - geherstructureerd na feedback les 1-2. Cursor vervangt OpenCode als primaire AI tool. Meer tijd voor Next.js en Supabase. +> **Let op:** Curriculum v2 - geherstructureerd na teaching experience. Old Les 5 (TypeScript voor React/Next.js) is uitgewerkt in de twee Next.js lessen. Old Les 6-8 zijn samengevat in nieuwe Les 5-6. Old Les 9-16 verschuiven naar Les 7-14. Old Les 17 (1 werkdag) is uitgebreid naar 3 werkdagen (Les 15-17). | Les | Onderwerp | Deel | Status | |-----|-----------|------|--------| | 01 | [Introductie tot AI en Large Language Models](Samenvattingen/Les01-Samenvatting.md) | 1 | ✅ Gegeven | | 02 | [AI Code Assistants (OpenCode → Cursor)](Samenvattingen/Les02-Samenvatting.md) | 1 | ✅ Gegeven | | 03 | [Cursor Setup & Basics](Samenvattingen/Les03-Samenvatting.md) | 1 | ✅ Gegeven | -| 04 | [TypeScript Fundamentals](Samenvattingen/Les04-Samenvatting.md) | 2 | 🔨 In ontwikkeling | -| 05 | [TypeScript voor React/Next.js](Samenvattingen/Les05-Samenvatting.md) | 2 | 📋 Samenvatting | -| 06 | [Next.js 1: Pages, Routing & Layouts](Samenvattingen/Les06-Samenvatting.md) | 2 | 📋 Samenvatting | -| 07 | [Next.js 2: Server Components & Data Fetching](Samenvattingen/Les07-Samenvatting.md) | 2 | 📋 Samenvatting | -| 08 | [Next.js 3: API Routes & Server Actions](Samenvattingen/Les08-Samenvatting.md) | 2 | 📋 Samenvatting | -| 09 | [Database Principles & Supabase Setup](Samenvattingen/Les09-Samenvatting.md) | 2 | 📋 Samenvatting | -| 10 | [Supabase: Auth & CRUD](Samenvattingen/Les10-Samenvatting.md) | 2 | 📋 Samenvatting | -| 11 | [Full-Stack Mini Project](Samenvattingen/Les11-Samenvatting.md) | 3 | 📋 Samenvatting | -| 12 | [Styling: Tailwind CSS & shadcn/ui](Samenvattingen/Les12-Samenvatting.md) | 3 | 📋 Samenvatting | -| 13 | [Hands-on: Van Idee naar Prototype](Samenvattingen/Les13-Samenvatting.md) | 3 | 📋 Samenvatting | -| 14 | [Project Setup & AI Config (.cursorrules, claude.md)](Samenvattingen/Les14-Samenvatting.md) | 3 | 📋 Samenvatting | -| 15 | [Vercel AI SDK, Tool Calling & Agents](Samenvattingen/Les15-Samenvatting.md) | 4 | 📋 Samenvatting | -| 16 | [AI Chat Interface & Streaming](Samenvattingen/Les16-Samenvatting.md) | 4 | 📋 Samenvatting | -| 17 | [Eindproject Werkdag](Samenvattingen/Les17-Samenvatting.md) | 4 | 📋 Samenvatting | +| 04 | [TypeScript Fundamentals](Samenvattingen/Les04-Samenvatting.md) | 2 | ✅ Gegeven | +| 05 | [Next.js — Het React Framework (Part 1)](Samenvattingen/Les05-Samenvatting.md) | 2 | ✅ Gegeven (v1) | +| 06 | [Next.js — QuickPoll Vervolg (Part 2)](Samenvattingen/Les06-Samenvatting.md) | 2 | 📋 Gepland | +| 07 | [Database Principles & Supabase Setup](Samenvattingen/Les07-Samenvatting.md) | 2 | 📋 Samenvatting | +| 08 | [Supabase: Auth & CRUD](Samenvattingen/Les08-Samenvatting.md) | 2 | 📋 Samenvatting | +| 09 | [Full-Stack Mini Project](Samenvattingen/Les09-Samenvatting.md) | 3 | 📋 Samenvatting | +| 10 | [Styling: Tailwind CSS & shadcn/ui](Samenvattingen/Les10-Samenvatting.md) | 3 | 📋 Samenvatting | +| 11 | [Van Idee naar Prototype](Samenvattingen/Les11-Samenvatting.md) | 3 | 📋 Samenvatting | +| 12 | [Project Setup & AI Config (.cursorrules, claude.md)](Samenvattingen/Les12-Samenvatting.md) | 3 | 📋 Samenvatting | +| 13 | [Vercel AI SDK, Tool Calling & Agents](Samenvattingen/Les13-Samenvatting.md) | 4 | 📋 Samenvatting | +| 14 | [AI Chat Interface & Streaming](Samenvattingen/Les14-Samenvatting.md) | 4 | 📋 Samenvatting | +| 15 | [Eindproject Werkdag 1](Samenvattingen/Les15-Samenvatting.md) | 4 | 📋 Samenvatting | +| 16 | [Eindproject Werkdag 2](Samenvattingen/Les16-Samenvatting.md) | 4 | 📋 Samenvatting | +| 17 | [Eindproject Polish & Code Review](Samenvattingen/Les17-Samenvatting.md) | 4 | 📋 Samenvatting | | 18 | [Deployment, Presentaties & Evaluatie](Samenvattingen/Les18-Samenvatting.md) | 4 | 📋 Samenvatting | --- @@ -49,9 +62,9 @@ Een 18-weekse cursus die studenten meeneemt van AI-beginner naar AI-powered deve | Deel | Lessen | Tools | Kosten | |------|--------|-------|--------| | Deel 1: AI Foundations | 1-3 | ChatGPT, v0.dev, OpenCode, **Cursor** | Gratis (Cursor Student Plan) | -| Deel 2: Technical Foundations | 4-10 | TypeScript, Next.js, Supabase | Gratis | -| Deel 3: Full-Stack Development | 11-14 | Next.js, Supabase, Tailwind, **shadcn/ui**, Cursor | Gratis | -| Deel 4: Advanced AI & Deployment | 15-18 | Cursor, Vercel AI SDK, Vercel | Gratis (Cursor Student Plan) | +| Deel 2: Technical Foundations | 4-8 | TypeScript, Next.js, Supabase | Gratis | +| Deel 3: Full-Stack Development | 9-12 | Next.js, Supabase, Tailwind, **shadcn/ui**, Cursor | Gratis | +| Deel 4: Advanced AI & Deployment | 13-18 | Cursor, Vercel AI SDK, Vercel | Gratis (Cursor Student Plan) | **Eindopdracht:** Cursor (Student Plan - gratis Pro voor 1 jaar) @@ -212,136 +225,86 @@ Kennismaking met AI, LLMs en de basis van AI-assisted development met Cursor. # Deel 2: Technical Foundations -**7 lessen · 12 EC** +**5 lessen · 10 EC** Stevige technische basis: TypeScript, Next.js, databases en Supabase. --- -### Les 5: TypeScript voor React/Next.js +### Les 5: Next.js — Het React Framework (Part 1) -**Tools:** Cursor, TypeScript, React, Next.js +**Tools:** Next.js 15, Cursor, TypeScript, Tailwind CSS -**Docent vertelt:** -- TypeScript + React: props typen, useState met types -- Event handlers en callback types -- Generics basics (Array<T>, Promise<T>) -- API response types en async functies -- Type narrowing en utility types (Partial, Pick, Omit) -- JS naar TS omzetten in een React project +**Docent vertelt (~45 min, demo-driven):** +- Waarom Next.js? Het probleem met pure React +- Create-next-app en project structuur +- App Router: file-based routing, page.tsx, layout.tsx +- Dynamic routes ([id]) met TypeScript +- Server Components vs Client Components ("use client") +- Data Fetching in Server Components (async components) +- Server Actions introductie +- Route Groups en best practices -**Studenten doen:** -- React components typen met interfaces -- useState en useEffect met types gebruiken -- Event handlers correct typen -- Bestaand React project omzetten naar TypeScript +**Samen bouwen (~120 min, klassikaal):** +- QuickPoll app Part 1 (Stap 0-3 uit lesopdracht) +- Tim + klas: create-next-app, types definiëren, folder structuur +- Klassikaal: layout met navigatie, homepage met polls list +- Klassikaal: API route GET single poll -**Lesopdracht:** Bouw getypte React componenten met Cursor. Type props, state, events en API responses. +**Lesopdracht:** QuickPoll app Part 1 (Stap 0-3 uit lesopdracht) - stap-voor-stap handleiding -**Huiswerk:** Zet een compleet React project om van JavaScript naar TypeScript. Alle componenten, hooks en API calls volledig typen. +**Huiswerk:** Stap 0-3 zelfstandig afmaken als je het niet af hebt -[→ Ga naar Les 5](Samenvattingen/Les05-Samenvatting.md) +**Lesmateriaal:** +- Slide-Overzicht +- Docenttekst +- Les05-Lesopdracht.pdf (stap-voor-stap handleiding) +- les5-quickpoll-starter.zip (scaffolded project) +- les5-quickpoll-voorbeeld.zip (Tim's referentie) + +[→ Ga naar Les 5 Samenvatting](Samenvattingen/Les05-Samenvatting.md) --- -### Les 6: Next.js 1: Pages, Routing & Layouts +### Les 6: Next.js — QuickPoll Vervolg (Part 2) -**Tools:** Next.js 14, OpenCode/WebStorm, Vercel +**Tools:** Next.js 15, Cursor, TypeScript, Tailwind CSS -**Docent vertelt:** -- Wat is Next.js? React framework met superpowers -- Server-Side Rendering (SSR) vs Client-Side Rendering (CSR) -- Waarom SSR? SEO, performance, initial load -- App Router: file-based routing uitgelegd -- Folder structuur: app/, pages, layouts, loading, error -- Dynamic routes: [id], [...slug], [[...optional]] -- Link component en navigation -- Metadata en SEO basics +**Docent vertelt (~30-40 min):** +- Recap Les 5 + Q&A +- API Route Handlers: GET, POST met NextResponse (dieper) +- Middleware: request intercepten, logging, rate limiting +- Environment Variables (.env.local, NEXT_PUBLIC_) +- Loading, Error, Not-Found special files +- next/image, next/link, Metadata +- Deployment op Vercel +- Cursor/AI workflow (.cursorrules, Cmd+K, Cmd+L) -**Studenten doen:** -- Next.js project aanmaken met `npx create-next-app@latest` -- Pagina's maken met App Router -- Layout en nested layouts implementeren -- Dynamic routes bouwen -- Navigation met Link component +**Samen bouwen (~120 min, klassikaal):** +- QuickPoll app Part 2 (Stap 4-7 uit lesopdracht) +- Klassikaal: API POST vote route +- Klassikaal: Poll detail pagina (server component) +- Klassikaal: VoteForm (client component met fetch) +- Klassikaal: Loading, error, not-found states +- Bonus: Create poll pagina +- Deploy naar Vercel -**Lesopdracht:** -1. `npx create-next-app@latest` met TypeScript + Tailwind + App Router -2. Maak 4 pagina's: home, about, products, contact -3. Maak root layout met Header en Footer -4. Maak `/products/[id]` dynamic route -5. Deploy naar Vercel +**Lesopdracht:** QuickPoll app Part 2 (Stap 4-7 uit lesopdracht) -**Huiswerk:** Voeg nested layout toe voor products section, maak loading.tsx en error.tsx, experimenteer met metadata. +**Huiswerk:** App afmaken en deployen op Vercel, bonus features toevoegen -[→ Ga naar Les 6](Samenvattingen/Les06-Samenvatting.md) +**Lesmateriaal:** +- Slide-Overzicht +- Docenttekst +- Les06-Lesopdracht.pdf (stap-voor-stap handleiding) +- les6-quickpoll-starter.zip (scaffolded project) +- les6-quickpoll-voorbeeld.zip (Tim's referentie) + +[→ Ga naar Les 6 Samenvatting](Samenvattingen/Les06-Samenvatting.md) --- -### Les 7: Next.js 2: Server Components & Data Fetching - -**Tools:** Next.js 14, React Query (TanStack Query), Cursor - -**Docent vertelt:** -- Server Components vs Client Components -- 'use client' directive: wanneer en waarom -- Data fetching in Server Components (async/await) -- React Query: waarom, installatie, basics -- useQuery en useMutation hooks -- Caching en revalidation strategies - -**Studenten doen:** -- Server Component met data fetching maken -- Client Component met React Query maken -- useQuery en useMutation implementeren -- Caching strategieën testen - -**Lesopdracht:** -1. Maak Server Component met async data fetching -2. Bouw Client Component met React Query -3. Implementeer useQuery voor producten -4. Voeg useMutation toe voor user interactions -5. Test caching behavior - -**Huiswerk:** Experimenteer met revalidation strategies, voeg error boundaries toe, bouw loading states voor betere UX. - -[→ Ga naar Les 7](Samenvattingen/Les07-Samenvatting.md) - ---- - -### Les 8: Next.js 3: API Routes & Server Actions - -**Tools:** Next.js 14, Cursor, Supabase - -**Docent vertelt:** -- API Routes: Route Handlers in App Router -- HTTP methods: GET, POST, PUT, DELETE -- Server Actions: form actions en mutations -- Request/response handling -- Error handling in API routes -- Middleware basics (authentication) - -**Studenten doen:** -- API routes schrijven voor CRUD operaties -- Server Actions implementeren voor forms -- Request validation toevoegen -- Error handling testen -- Authentication middleware bouwen - -**Lesopdracht:** -1. Maak `/api/items` routes (GET, POST, PUT, DELETE) -2. Bouw Server Actions voor form submissions -3. Voeg request validation toe met Zod -4. Implementeer error handling -5. Test all CRUD operations - -**Huiswerk:** Voeg authentication middleware toe, implementeer optimistic UI updates, schrijf API route tests. - -[→ Ga naar Les 8](Samenvattingen/Les08-Samenvatting.md) - ---- - -### Les 9: Database Principles & Supabase Setup +### Les 7: Database Principles & Supabase Setup **Tools:** Diagramming tool (draw.io/Excalidraw), Supabase, Cursor @@ -369,13 +332,13 @@ Stevige technische basis: TypeScript, Next.js, databases en Supabase. 4. Configureer `.env.local` 5. Test connectie met Supabase client -**Huiswerk:** Refine je database schema, voeg meer relaties toe, dokumenteer decisions, bereid Les 10 vor. +**Huiswerk:** Refine je database schema, voeg meer relaties toe, dokumenteer decisions, bereid Les 8 vor. -[→ Ga naar Les 9](Samenvattingen/Les09-Samenvatting.md) +[→ Ga naar Les 7 Samenvatting](Samenvattingen/Les07-Samenvatting.md) --- -### Les 10: Supabase: Auth & CRUD +### Les 8: Supabase: Auth & CRUD **Tools:** Supabase, Next.js, Cursor, React Query @@ -405,7 +368,7 @@ Stevige technische basis: TypeScript, Next.js, databases en Supabase. **Huiswerk:** Add OAuth provider (Google/GitHub), implementeer logout en password reset, deploy naar Vercel met env secrets. -[→ Ga naar Les 10](Samenvattingen/Les10-Samenvatting.md) +[→ Ga naar Les 8 Samenvatting](Samenvattingen/Les08-Samenvatting.md) --- @@ -417,7 +380,7 @@ Integratie van alle geleerde technieken, styling en voorbereiding op eindproject --- -### Les 11: Full-Stack Mini Project +### Les 9: Full-Stack Mini Project **Tools:** Cursor, Supabase, Browser DevTools @@ -428,7 +391,7 @@ Integratie van alle geleerde technieken, styling en voorbereiding op eindproject - Probleemoplossing in real time **Studenten doen:** -- **Groepsdiscussie:** Bespreek auth & CRUD ervaringen uit Les 10 +- **Groepsdiscussie:** Bespreek auth & CRUD ervaringen uit Les 8 - Mini-project kiezen: Bookmarks, Todo's, Notes, of Recipe Collection - Met Cursor alle onderdelen kombinieren: auth, database, API, UI - Zelfstandig problemen oplossen @@ -438,43 +401,11 @@ Integratie van alle geleerde technieken, styling en voorbereiding op eindproject **Huiswerk:** Verbeterpunten toevoegen, reflectie schrijven (200 woorden), repo opruimen, voorbereiden op styling. -[→ Ga naar Les 11](Samenvattingen/Les11-Samenvatting.md) +[→ Ga naar Les 9 Samenvatting](Samenvattingen/Les09-Samenvatting.md) --- -### Les 11: Hands-on: Van Idee naar Prototype - -**Tools:** OpenCode/WebStorm, ChatGPT (voor planning) - -**Docent vertelt:** -- Hoe ga je van vaag idee naar concrete features? -- Feature breakdown methode -- Component thinking: UI opdelen in herbruikbare stukken -- Minimum Viable Product (MVP) denken -- Korte demo van het proces - -**Studenten doen:** -- **Groepsdiscussie:** Bespreek Tool Selection reflecties - welke workflows werken het beste? -- Kiezen van een simpel project idee -- Met AI features breakdown maken -- Component structuur bedenken -- Prototype bouwen met alles wat ze hebben geleerd -- Focus op WORKFLOW, niet perfectie - -**Voorbeeld projecten:** -- Weer widget met 3-daagse forecast -- Simpele quiz app (3 vragen) -- Recipe card met ingrediënten toggle - -**Lesopdracht:** Kies een mini-project, maak feature breakdown met AI, bouw werkend prototype. Documenteer je proces: welke prompts werkten, waar liep je vast, hoe loste je het op. - -**Huiswerk:** Verbeter je prototype (styling, edge cases), schrijf "Lessons Learned" document (300 woorden). - -[→ Ga naar Les 11](Samenvattingen/Les11-Samenvatting.md) - ---- - -### Les 12: Styling: Tailwind CSS & shadcn/ui +### Les 10: Styling: Tailwind CSS & shadcn/ui **Tools:** Tailwind CSS, shadcn/ui, Cursor @@ -504,11 +435,11 @@ Integratie van alle geleerde technieken, styling en voorbereiding op eindproject **Huiswerk:** Refactor alle components, add animations met Tailwind, explore shadcn variations, dokumenteer styling decisions. -[→ Ga naar Les 12](Samenvattingen/Les12-Samenvatting.md) +[→ Ga naar Les 10 Samenvatting](Samenvattingen/Les10-Samenvatting.md) --- -### Les 13: Hands-on: Van Idee naar Prototype +### Les 11: Van Idee naar Prototype **Tools:** Cursor, ChatGPT (voor planning) @@ -536,21 +467,13 @@ Integratie van alle geleerde technieken, styling en voorbereiding op eindproject **Lesopdracht:** Kies eindproject idee, maak feature breakdown met AI, ontwerp component tree, zet up codebase, bouw eerste 2-3 components. -**Huiswerk:** Bouw meer components, integreer eerste data, documenteer architecture decisions, voorbereiding Les 14. +**Huiswerk:** Bouw meer components, integreer eerste data, documenteer architecture decisions, voorbereiding Les 12. -[→ Ga naar Les 13](Samenvattingen/Les13-Samenvatting.md) +[→ Ga naar Les 11 Samenvatting](Samenvattingen/Les11-Samenvatting.md) --- -# Deel 4: Advanced AI & Deployment - -**4 lessen · 6 EC** - -AI-powered features, streaming, finale afronding en deployment. - ---- - -### Les 14: Project Setup & AI Config (.cursorrules, claude.md) +### Les 12: Project Setup & AI Config (.cursorrules, claude.md) **Tools:** Cursor, Git, GitHub @@ -581,11 +504,19 @@ AI-powered features, streaming, finale afronding en deployment. **Huiswerk:** Polish documentatie, add architecture diagrams, test .cursorrules in Cursor, prepare voor Deel 4. -[→ Ga naar Les 14](Samenvattingen/Les14-Samenvatting.md) +[→ Ga naar Les 12 Samenvatting](Samenvattingen/Les12-Samenvatting.md) --- -### Les 15: Vercel AI SDK, Tool Calling & Agents +# Deel 4: Advanced AI & Deployment + +**6 lessen · 8 EC** + +AI-powered features, streaming, finale afronding en deployment. + +--- + +### Les 13: Vercel AI SDK, Tool Calling & Agents **Tools:** Vercel AI SDK, Zod, Externe APIs, Cursor @@ -614,11 +545,11 @@ AI-powered features, streaming, finale afronding en deployment. **Huiswerk:** Add more tools, implementeer agents met `maxSteps`, documenteer in AI-DECISIONS.md, integreer in eindproject. -[→ Ga naar Les 15](Samenvattingen/Les15-Samenvatting.md) +[→ Ga naar Les 13 Samenvatting](Samenvattingen/Les13-Samenvatting.md) --- -### Les 16: AI Chat Interface & Streaming +### Les 14: AI Chat Interface & Streaming **Tools:** Vercel AI SDK, Cursor, Browser APIs @@ -648,40 +579,106 @@ AI-powered features, streaming, finale afronding en deployment. **Huiswerk:** Add chat features (clear history, export, favorites), deploy naar Vercel, gather user feedback. -[→ Ga naar Les 16](Samenvattingen/Les16-Samenvatting.md) +[→ Ga naar Les 14 Samenvatting](Samenvattingen/Les14-Samenvatting.md) --- -### Les 17: Eindproject Werkdag +### Les 15: Eindproject Werkdag 1 **Tools:** Cursor, Vercel, Supabase, alle vorige tools +**Focus:** Core features bouwen + **Docent vertelt:** - Q&A session: wat loop je tegen aan? - Cursor tips & tricks review - Debugging strategies - Performance optimization basics - Deployment troubleshooting -- Final checklist review **Studenten doen:** - **Volle dag werken aan eindproject** -- Docent beschikbaar voor individuele hulp -- Features afmaken +- Docent beschikbaar voor individuele hulp en klassikaal sparring +- Core features bouwen - Testing en bug fixes -- Voorbereiding voor presentatie -- Code review voorbereiden +- Regelmatig vragen stellen **Lesopdracht:** -1. Alle features afmaken van je eindproject +1. Core features van eindproject afmaken 2. Testing: user flows doorlopen 3. Bug fixes en edge cases -4. Code cleanup en refactoring -5. Deploy naar Vercel +4. Code cleanup +5. Checkpoint: is het werkend? -**Huiswerk:** Laatste fixes, presentatie voorbereiden, documentatie finaliseren, demo video opnemen (optioneel). +**Huiswerk:** Voortgang bijhouden, notities maken voor volgende werkdag, extra features plannen. -[→ Ga naar Les 17](Samenvattingen/Les17-Samenvatting.md) +[→ Ga naar Les 15 Samenvatting](Samenvattingen/Les15-Samenvatting.md) + +--- + +### Les 16: Eindproject Werkdag 2 + +**Tools:** Cursor, Vercel, Supabase, alle vorige tools + +**Focus:** Features afmaken, testing, bug fixes + +**Docent vertelt:** +- Recap vorige werkdag +- Extra features planning +- Performance optimization +- Deployment preparation + +**Studenten doen:** +- **Volle dag werken aan eindproject** +- Docent beschikbaar voor hulp +- Remaining features afmaken +- Uitgebreide testing +- Korte demo's van voortgang aan de klas +- Performance optimization + +**Lesopdracht:** +1. Alle geplande features afmaken +2. Uitgebreide testing en bug fixes +3. Performance audit +4. Code review voorbereiding +5. Presentatie voorbereiding + +**Huiswerk:** Laatste polishing, documentatie finaliseren, code review met klasgenoot. + +[→ Ga naar Les 16 Samenvatting](Samenvattingen/Les16-Samenvatting.md) + +--- + +### Les 17: Eindproject Polish & Code Review + +**Tools:** Cursor, Vercel, GitHub, Supabase + +**Focus:** Code review in paren, UI polish, performance check, presentatie voorbereiding + +**Docent vertelt:** +- Code review best practices +- Performance optimization details +- Presentation tips +- Deployment final checks + +**Studenten doen:** +- Code review in paren (peer feedback) +- UI polish en refinement +- Performance optimization (Lighthouse audit) +- Accessibility audit +- Presentatie voorbereiding +- Final deployment checks + +**Lesopdracht:** +1. Zorg dat iemand anders je code review +2. Voer feedback uit +3. Run Lighthouse en fix issues +4. Accessibility check +5. Deploy final version + +**Huiswerk:** Presentatie oefenen, demo video opnemen (optioneel), laatste tweaks. + +[→ Ga naar Les 17 Samenvatting](Samenvattingen/Les17-Samenvatting.md) --- @@ -727,7 +724,7 @@ AI-powered features, streaming, finale afronding en deployment. **Huiswerk:** N.A. - Course afgerond! -[→ Ga naar Les 18](Samenvattingen/Les18-Samenvatting.md) +[→ Ga naar Les 18 Samenvatting](Samenvattingen/Les18-Samenvatting.md) --- @@ -746,7 +743,9 @@ AI-powered features, streaming, finale afronding en deployment. Elke uitgewerkte les bevat: - `Les[xx]-Slide-Overzicht.md` - Slide deck structuur -- `Les[xx]-Lesplan.md` - Tijdsindeling en activiteiten +- `Les[xx]-Lesplan.md` - Tijdsindeling en activiteiten (nu: ~45 min theorie + 15 min pauze + ~120 min klassikaal bouwen) - `Les[xx]-Docenttekst.md` - Uitgebreide docenthandleiding -- `Les[xx]-Bijlage-A-Lesopdracht.md` - Lesopdracht (2 uur) -- `Les[xx]-Bijlage-B-Huiswerkopdracht.md` - Huiswerkopdracht (2 uur) +- `Les[xx]-Bijlage-A-Lesopdracht.md` - Lesopdracht (klassikaal en huiswerk) +- `Les[xx]-Bijlage-B-Huiswerkopdracht.md` - Huiswerkopdracht + +**Lesformat verandering (v2):** Meer klassikaal bouwen, minder solo werk. Docent codeert voor, studenten volgen mee met regelmatige "nu jullie" momenten. Betere interactie en sparring. diff --git a/v1-feedback.md b/v1-feedback.md index 6b6062a..d6866c6 100644 --- a/v1-feedback.md +++ b/v1-feedback.md @@ -146,6 +146,51 @@ Oorspronkelijk zou les 3 gaan over privacy en security van AI. Tim heeft beslote --- +## Les 4: TypeScript Fundamentals + +### Context +Les over TypeScript basics met een Escaperoom opdracht. + +### Wat ging goed +- TypeScript Escaperoom was effectief als lesvorm +- Studenten begrepen het concept van type safety +- Cursor hielp studenten bij het oplossen van type errors + +### Wat zou anders kunnen in v2 +- Les 4 was goed maar v2 moet de collaborative format meenemen +- Meer live coding demonstraties + +--- + +## Les 5: Next.js — Het React Framework + +### Context +Oorspronkelijk was dit "TypeScript voor React" maar Tim sprong direct naar Next.js. De les behandelde Next.js basics inclusief App Router, Components, Data Fetching, en de start van Server Actions. + +### Wat ging goed +- Next.js content was relevant en studenten waren gemotiveerd +- QuickPoll project was een leuke, concrete opdracht +- Starter zip met scaffolded files was goed ontvangen + +### Wat ging minder +- Theorie was te lang: ~1 uur gepraat maar stopte bij Server Actions (slide 17 van 32), slides 18-32 niet behandeld +- Hands-on opdracht was te moeilijk voor zelfstandig werk: in 1.5 uur kwamen de meeste studenten tot stap 2-3 (van 7 stappen) +- Format "1 uur college + 2 uur solo" werkt niet goed + +### Studentfeedback +- Studenten gaven aan dat ze liever SAMEN willen bouwen +- Meer houvast en meer sparren gewenst +- Klassikaal werken heeft voorkeur boven individueel + +### Aanbevolen verbeteringen v2 +1. Les splitsen over 2 lessen (Les 5 = Part 1, Les 6 = Part 2) +2. Theorie inkorten tot ~45 min (stop bij Server Actions) +3. Hands-on klassikaal doen: Tim codeert voor, studenten volgen mee +4. Overige theorie (API Routes, Middleware, Deployment, Cursor) naar Les 6 +5. Stap 0-3 in Les 5, Stap 4-7 in Les 6 + +--- + ## Conclusie De kern van de lessen (AI concepten, prompting, development workflow) werkt goed. De problemen zitten vooral in: @@ -154,10 +199,18 @@ De kern van de lessen (AI concepten, prompting, development workflow) werkt goed 2. **Tempo:** Te snel door setup-stappen heen 3. **Platform diversiteit:** Windows/Mac verschillen onderschat 4. **Terminal ervaring:** Veel studenten hebben weinig terminal ervaring +5. **Lesformat na Les 5:** Studentfeedback laat duidelijk zien dat klassikaal/collaborative werken effectiever is dan zelfstandig werken -V2 moet focussen op betere voorbereiding en duidelijkere, stapsgewijze instructies met checkpoints. +### Les 4-5 inzichten +Les 4 (TypeScript Escaperoom) werkte goed en moet in v2 behouden blijven. Les 5 heeft echter grote verbeteringen nodig: +- De combinatie van lange theorieblok + korte hands-on time werkt niet +- Studenten prefereren klassikaal leren en samen bouwen +- Content moet gesplitst worden over meerdere lessen met meer praktische focus +- Het format voor alle toekomstige lessen moet aangepast worden naar collaborative/klassikaal werken + +V2 moet focussen op betere voorbereiding, duidelijkere stapsgewijze instructies met checkpoints, en vooral: het overschakelen naar klassikaal/collaborative lesformat voor alle lessen. --- -*Laatste update: februari 2026* -*Gebaseerd op: Les 1, 2 en 3 gegeven aan eerste groep* +*Laatste update: maart 2026* +*Gebaseerd op: Les 1, 2, 3, 4 en 5 gegeven aan eerste groep* diff --git a/v2/README.md b/v2/README.md index 931e33b..e0fe27e 100644 --- a/v2/README.md +++ b/v2/README.md @@ -78,8 +78,35 @@ Verbeterde versie van het lesmateriaal op basis van feedback uit de eerste lesre - Les03-Debug-Challenge-Hard-ANTWOORDEN.md - Les03-Debug-Challenge-SuperHard-ANTWOORDEN.md +### Les 4: TypeScript Fundamentals (v2 verbeteringen) +- Escaperoom opdracht behouden (werkte goed) +- Meer live coding demonstraties toevoegen +- Klassikaal format: Tim lost samen met klas de eerste kamers op + +### Les 5: Next.js — Het React Framework (v2 verbeteringen) +**GROTE WIJZIGING:** Les 5 wordt gesplitst over Les 5 + Les 6 +- Les 5 Part 1: Theorie blok 1-3 (~45 min) + QuickPoll stap 0-3 (klassikaal) +- Les 6 Part 2: Theorie blok 4 (~30 min) + QuickPoll stap 4-7 (klassikaal) +- Originele Les 5 "TypeScript voor React" geschrapt als losse les — TS geïntegreerd in Next.js lessen +- Alle hands-on werk is nu KLASSIKAAL (Tim codeert voor, studenten volgen mee) +- Minder theorie slides, meer demo's +- Curriculum verschuift: oude Les 9-18 worden Les 7-18 + +### Lesformat Wijziging (alle lessen) +Op basis van studentfeedback na Les 5: +- OUD: ~60 min theorie + 15 min pauze + ~105 min zelfstandig werken +- NIEUW: ~45 min theorie (demo-driven) + 15 min pauze + ~120 min klassikaal bouwen +- Studenten gaven aan liever SAMEN te werken +- Meer sparren, meer houvast, minder solo-struggle +- Tim codeert voor op scherm, studenten volgen mee, regelmatig "nu jullie" momenten +- Dit geldt retroactief voor v2 van alle lessen + --- ## Changelog t.o.v. v1 +- **Lesformat:** Alle lessen omgezet naar klassikaal/collaborative format +- **Curriculum:** Les 5-8 geherstructureerd — 4 lessen → 2 lessen, rest verschuift +- **Extra werkdagen:** 3 eindproject werkdagen i.p.v. 1 + Zie [v1-feedback.md](../v1-feedback.md) voor de volledige reflectie.