fix: les
This commit is contained in:
@@ -1 +0,0 @@
|
||||
,sharp-zealous-planck,claude,21.05.2026 07:44,file:///sessions/sharp-zealous-planck/.config/libreoffice/4;
|
||||
427
Les14-Cursor-Vercel-Deploy/Les14-Docenttekst-Autocue.md
Normal file
427
Les14-Cursor-Vercel-Deploy/Les14-Docenttekst-Autocue.md
Normal file
@@ -0,0 +1,427 @@
|
||||
# Les 14 — Docenttekst (Autocue)
|
||||
|
||||
**Online les via Teams — letterlijk voorleesbaar. Slide-nummers tussen `[SLIDE N]`.**
|
||||
|
||||
> **Setup voor de les:**
|
||||
> - Monitor 1 (privé): dit autocue-document fullscreen
|
||||
> - Monitor 2 (gedeeld in Teams): Cursor + browser + terminal + slides
|
||||
>
|
||||
> **Structuur:** ±1 uur live demo → kwartier pauze → ±1,5 uur "Nu jullie"
|
||||
>
|
||||
> **Voorbereiding — accounts:**
|
||||
> - GitHub ✅ — `gh` CLI ingelogd
|
||||
> - Vercel ✅ — CLI geïnstalleerd (`npm i -g vercel`) + ingelogd (`vercel login`)
|
||||
> - Cursor ✅ — Pro of Business plan, ingelogd
|
||||
>
|
||||
> **Voorbereiding — voorbereide bestanden:**
|
||||
> - Een schone test-folder klaar (`~/web/demo-portfolio` of vergelijkbaar)
|
||||
> - `.cursor/rules/general.mdc` snippet copy-paste klaar
|
||||
> - `AGENTS.md` snippet copy-paste klaar
|
||||
> - Twee feature-prompts copy-paste klaar (hero + about)
|
||||
>
|
||||
> **Voorbereiding — backup:**
|
||||
> - Een eerder gemaakte demo op standby, voor als live demo vastloopt
|
||||
> - Screenshots van Vercel preview-URL's voor het geval Vercel traag is
|
||||
>
|
||||
> **Voor uitgebreide handleiding zie `Les14-Docenttekst.md` (normale versie).**
|
||||
|
||||
---
|
||||
|
||||
## BLOK 1 — Welkom + Vandaag
|
||||
|
||||
`[SLIDE 1]`
|
||||
|
||||
Welkom bij Les 14. Vandaag verlaten we localhost. We gaan een nieuw Next.js-project scaffolden, koppelen aan GitHub en Vercel, en daarna gaan we daar in Cursor features aan toevoegen. Steeds in een eigen feature-branch, met een eigen preview-URL.
|
||||
|
||||
Dit is een live demo van begin tot eind. Ik bouw alles voor jullie ogen op. Stel vragen onderweg — in de chat of zet je microfoon aan. Niet wachten tot het eind.
|
||||
|
||||
`[SLIDE 2]`
|
||||
|
||||
Wat we vandaag in één voorbeeld gaan zien:
|
||||
|
||||
Een vers Next.js-project. Naar GitHub. Naar Vercel. Vercel CLI gebruiken om env vars te beheren. Cursor in detail: chat, plan, build, inline edit, rules, project context, en background agents. Dan twee features bouwen, elke in een eigen branch, met preview-URL. Eindigen met PR's die we mergen naar productie.
|
||||
|
||||
Werkwijze: ik bouw, jullie kijken. Na de pauze doen jullie hetzelfde voor je eigen project.
|
||||
|
||||
## BLOK 2 — Scaffold + Git + Vercel
|
||||
|
||||
`[SLIDE 3]`
|
||||
|
||||
Stap één: scaffolden. We gebruiken `npx create-next-app`. Dat is het officiële commando van het Next.js team. Het stelt je een paar vragen — TypeScript ja, Tailwind ja, App Router ja, src-directory ja — en je hebt binnen 30 seconden een werkende app.
|
||||
|
||||
*(Ik open een terminal, draai create-next-app, doorloop de prompts, en start de dev-server.)*
|
||||
|
||||
Je krijgt: een werkende Next.js 16-app, op localhost. TypeScript en Tailwind preconfigured. ESLint klaar. En de juiste `.gitignore`, dus `.env.local`, `node_modules` en `.next` komen straks niet in Git terecht.
|
||||
|
||||
`[SLIDE 4]`
|
||||
|
||||
Stap twee: Git initialiseren en pushen naar GitHub.
|
||||
|
||||
Vijf commando's. `git init` maakt een lokale repo. `git add .` zet alles klaar. `git commit -m "init"` maakt de eerste snapshot. `git branch -M main` zorgt dat onze branch `main` heet. En dan `gh repo create mijn-portfolio --public --source=. --push` — dat is de GitHub CLI die in één commando een remote-repo aanmaakt, koppelt en pusht.
|
||||
|
||||
*(Ik draai deze commando's. Browser naar github.com om te bevestigen dat de code er staat.)*
|
||||
|
||||
Als je `gh` niet geïnstalleerd hebt: handmatig op github.com een lege repository maken, daarna `git remote add origin` en `git push -u origin main`. Werkt ook prima.
|
||||
|
||||
`[SLIDE 5]`
|
||||
|
||||
Stap drie: Vercel koppelen.
|
||||
|
||||
We openen vercel.com/new. Klikken **Import Git Repository**. Vercel ziet al onze GitHub-repos — we kiezen `mijn-portfolio`. Framework wordt automatisch gedetecteerd als Next.js. Build-commando en output-directory mag je standaard laten.
|
||||
|
||||
Klik Deploy. En dan wachten we ongeveer 45 seconden. Vercel klonet de repo, draait `npm install`, draait `npm run build`, deploy't naar het Edge Network, en geeft een URL terug.
|
||||
|
||||
*(Ik wacht tot de deploy klaar is, open de URL in nieuwe tab.)*
|
||||
|
||||
Daar staat ie. Een verse Next.js-app, live op het internet. Vanaf nu: elke push naar GitHub triggert automatisch een nieuwe deploy. Geen extra werk.
|
||||
|
||||
`[SLIDE 6]`
|
||||
|
||||
Twee soorten URL's krijg je gratis. Productie-URL — die hoort bij `main`. En preview-URL's voor elke andere branch.
|
||||
|
||||
Het formaat van een preview-URL is `<project>-git-<branch>-<scope>.vercel.app`. Hele lange URL's, maar je hoeft ze nooit te onthouden — Vercel post ze automatisch in je Pull Requests.
|
||||
|
||||
Voordeel van preview-URL's: iedereen kan op je werk-in-progress klikken zonder lokale setup. Geen npm install. Geen `.env.local`. Klikken en kijken.
|
||||
|
||||
`[SLIDE 7]`
|
||||
|
||||
Environment variables in de Vercel UI. Onze app heeft er nu nog geen nodig — we hebben geen database, geen API key. Maar voor straks: je gaat in deze opleiding apps bouwen met OpenAI-keys, Supabase-URLs, Stripe-secrets. Die komen allemaal hier.
|
||||
|
||||
Belangrijk: drie environments. Production — actief bij deploys vanaf main. Preview — voor alle andere branches. Development — als je `vercel dev` lokaal draait.
|
||||
|
||||
En de `NEXT_PUBLIC_` prefix: variabelen met die prefix worden in de browser-bundle gestopt. Zonder prefix zijn ze server-only. Dat is het verschil tussen "iedereen kan dit zien" en "alleen mijn API routes kennen dit".
|
||||
|
||||
## BLOK 3 — Vercel CLI
|
||||
|
||||
`[SLIDE 8]`
|
||||
|
||||
Naast het dashboard is er ook een CLI. Die wil ik even laten zien, want voor dagelijks werk is dat veel sneller dan klikken.
|
||||
|
||||
Wat je krijgt: `vercel link` om je lokale folder aan een Vercel-project te koppelen. `vercel env pull` om productie-env-vars naar lokaal te trekken. `vercel env add` en `rm` om vars te beheren zonder dashboard. `vercel logs` voor runtime logs. En `vercel --prod` voor een handmatige productie-deploy.
|
||||
|
||||
Installeren: `npm i -g vercel`, dan `vercel login`. Klaar.
|
||||
|
||||
`[SLIDE 9]`
|
||||
|
||||
`vercel link` doe je één keer per project. Het vraagt naar welk scope — je persoonlijke of een team — en welk project. Daarna maakt het een `.vercel/project.json` aan, en weten alle volgende commando's bij welk Vercel-project ze horen.
|
||||
|
||||
`vercel env pull .env.local` is mijn favoriete commando. Het trekt alle env vars uit je gekoppelde environment en schrijft ze naar `.env.local`. Eén commando — je hoeft je env vars nooit handmatig te kopiëren.
|
||||
|
||||
Tip: standaard pull't hij development env-vars. Wil je preview-vars? `vercel env pull --environment=preview`.
|
||||
|
||||
*(Ik draai `vercel link` en `vercel env pull` — laat de prompt en output zien.)*
|
||||
|
||||
`[SLIDE 10]`
|
||||
|
||||
Env vars vanuit terminal beheren. `vercel env add` vraagt om naam en waarde, en welke environment. Je kunt meerdere environments tegelijk geven — `production preview development` — voor één commando dat alles dekt.
|
||||
|
||||
`vercel env rm` om te verwijderen. `vercel env ls` om te zien wat je hebt.
|
||||
|
||||
Wanneer is dit handig? Als je een API-key roteert. Nieuwe key generen, oude verwijderen, nieuwe toevoegen. Drie commando's in de terminal — geen dashboard nodig.
|
||||
|
||||
## BLOK 4 — Cursor in detail
|
||||
|
||||
`[SLIDE 11]`
|
||||
|
||||
Cursor. Nu het hoofdgerecht van vandaag.
|
||||
|
||||
Wat is Cursor: een VS Code-fork met AI native ingebouwd. Niet een extensie. Het IS de editor. Werkt met Claude, GPT, Gemini en lokale modellen — je kiest per chat welk model.
|
||||
|
||||
Wat krijg je boven gewone VS Code? Chat met je hele codebase als context. Plan-mode — eerst nadenken, dan doen. Build-mode — laat de AI autonoom uitvoeren. Inline edit met Cmd+K. Background agents voor parallel werk. Cursor Rules per project. En tab-completion die meekijkt met je hele bestand, niet alleen je laatste regel.
|
||||
|
||||
`[SLIDE 12]`
|
||||
|
||||
Cursor 3 heeft twee hoofdvensters. Belangrijk om hiermee te beginnen, want het is in versie 3 nieuw.
|
||||
|
||||
Aan de ene kant: het **Editor Window**. Dat is de klassieke VS Code-stijl IDE — file explorer links, editor in het midden, terminal beneden. Daar schrijf je code, review je diffs, gebruik je extensies. Dat is je hoofdwerkplek.
|
||||
|
||||
Aan de andere kant: het **Agents Window**. Dat is een nieuwe interface, een nieuwe interface waar alle agents zichtbaar zijn. Lokale agents, cloud-agents, remote agents, agents die je vanaf je telefoon hebt getriggerd. Alles op één plek.
|
||||
|
||||
Je wisselt tussen beide met **Cmd+Shift+N**. Dat is de belangrijkste sneltoets van vandaag — onthoud die.
|
||||
|
||||
Binnen het Editor Window heb je een chat-paneel — Cmd+L opent het. En in dat chat-paneel kun je kiezen welke modus de AI gebruikt. Eén van die modi is **Agent mode** — daar gaan we het zo over hebben. Plus Cmd+K voor snelle inline-edits in één bestand.
|
||||
|
||||
Gouden regel: code schrijven of één feature bouwen → Editor Window met Agent mode in chat. Meerdere features parallel → Agents Window met background agents.
|
||||
|
||||
`[SLIDE 13]`
|
||||
|
||||
Editor Window in detail. Dit is waar je 80 procent van je tijd doorbrengt.
|
||||
|
||||
Stap één: **Cmd+Shift+N** — als je in Agents Window zat, switcht hij naar het Editor Window. Je ziet nu de klassieke layout: file explorer links, editor in het midden, terminal onderaan.
|
||||
|
||||
Stap twee: dit voelt als VS Code. Cmd+P opent de file-search. Tab accepteert AI auto-completion suggesties. Bekende sneltoetsen werken allemaal.
|
||||
|
||||
Stap drie: voor een snelle inline edit selecteer je code in de editor en druk je **Cmd+K**. Er opent een input-balkje boven je selectie. Je typt wat je wilt — bijvoorbeeld "rename deze variabele naar firstName" — en Cursor toont de diff direct in de editor. **Tab** om te accepteren, **Esc** om te rejecten.
|
||||
|
||||
Stap vier: voor langere taken open je het chat-paneel met **Cmd+L**. Het opent rechts, naast je editor.
|
||||
|
||||
Wanneer gebruik je het Editor Window? Voor je standaard coding-flow. Als je veel files tegelijk wilt zien met split screens. Voor VS Code extensies — linters, formatters, debuggers — die allemaal gewoon werken.
|
||||
|
||||
*(Ik open mijn Editor Window, doe een Cmd+K op een variabele-naam.)*
|
||||
|
||||
`[SLIDE 14]`
|
||||
|
||||
Agent mode. Dit is de AI-modus *binnen* het chat-paneel in het Editor Window.
|
||||
|
||||
Stap één: ik ben in het Editor Window. **Cmd+L** opent het chat-paneel rechts. Stap twee: bovenaan de chat zie ik een mode-selector — een dropdown of een rij icoontjes. Ik kies **Agent**.
|
||||
|
||||
Stap drie: ik typ niet een specifieke kleine wijziging, maar de HELE taak. Bijvoorbeeld: "Bouw een /about pagina met een team-grid in onze huisstijl, baseer de styling op onze hero-section."
|
||||
|
||||
Stap vier: ik druk Enter. De agent start. Stap vijf: in het chat-paneel verschijnt een lopende takenlijst — de agent vinkt zijn eigen stappen af terwijl hij werkt.
|
||||
|
||||
Belangrijk om te weten: in Cursor 3 heeft Agent mode **geen limiet** op het aantal tool-calls per taak. De agent kan dus meerdere files editen, terminal-commando's runnen, en zelfs web-content browsen. Hij vraagt wel altijd eerst voordat hij een terminal-commando uitvoert.
|
||||
|
||||
Stap zes: aan het einde verschijnt een samenvatting van alle wijzigingen, per bestand een diff. Stap zeven: **Keep** of **Accept all** om alles te accepteren, of je accepteert per file.
|
||||
|
||||
Wanneer kies je Agent mode? Voor grotere features die meerdere bestanden raken. Refactors. Nieuwe pagina's bouwen. API-routes opzetten. Als je het plan vertrouwt en aan het eind tijd hebt om goed te reviewen.
|
||||
|
||||
Belangrijke tip: hou je prompt specifiek. "Bouw een about-pagina met team-grid" is goed. "Bouw een mooie website" is veel te open. Dan krijg je een gok.
|
||||
|
||||
*(Ik laat een Agent mode demo zien — kleine maar concrete taak.)*
|
||||
|
||||
`[SLIDE 15]`
|
||||
|
||||
Background agents — het derde stuk. Dit is wat Cursor 3 echt vernieuwt.
|
||||
|
||||
Wat het is: async agents die in geïsoleerde cloud-VMs draaien. Niet in jouw editor — vandaar background. Ze werken parallel in eigen Git branches, en je beheert ze via het **Agents Window**.
|
||||
|
||||
Hoe kom je er? **Cmd+Shift+N** — switcht van Editor Window naar Agents Window. Daar zie je een lijst-overzicht met alle running en done agents.
|
||||
|
||||
Hoe maak je er een aan? Stap één: in het Agents Window klik je op **New agent** of het plusje bovenaan. Stap twee: kies een environment. Local — draait op je laptop. Cloud — draait op Cursor's servers in een geïsoleerde VM. Remote SSH — als je een eigen server hebt. Voor vandaag pakken we cloud, want dat is de meest interessante demo.
|
||||
|
||||
Stap drie: beschrijf de taak. Wees specifiek, alsof je een ticket aanmaakt voor een collega. Stap vier: kies optioneel een branch-naam. Stap vijf: klik **Start**.
|
||||
|
||||
Stap zes: de agent draait nu in een cloud-VM. Je kunt **Cmd+Shift+N** terug doen naar het Editor Window en gewoon verder werken. De agent babysit zichzelf.
|
||||
|
||||
Hoe haal je resultaten op? Stap één: **Cmd+Shift+N** terug naar Agents Window. Stap twee: klare agents staan op **Done**. Stap drie: ik klik op de agent — ik zie de diff, de commit message, en alle stappen die hij heeft genomen.
|
||||
|
||||
Stap vier: **Open in PR** maakt automatisch een Pull Request op GitHub. Stap vijf: Vercel pakt het op en maakt een preview-URL. Stap zes: review in browser, merge op GitHub.
|
||||
|
||||
Wanneer gebruik je dit? Voor twee tot vier parallelle features. Voor lange refactors die je niet wilt babysitten. Of als je vanaf je telefoon, Slack of Linear een taak triggert terwijl je in een vergadering zit.
|
||||
|
||||
*(Ik open Agents Window met Cmd+Shift+N en demonstreer een lege agent.)*
|
||||
|
||||
`[SLIDE 16]`
|
||||
|
||||
Context — hoe vertel je Cursor waar hij naar moet kijken.
|
||||
|
||||
In chat type je een at-teken en je krijgt een autocomplete. at-file:foo.tsx voor één bestand. at-folder:components voor een hele map. at-code:functionName voor een specifieke functie of class. at-docs voor externe documentatie. at-web voor een live web-search. at-git voor git history of blame.
|
||||
|
||||
Voorbeeld: "Refactor at-file UserCard.tsx zodat het at-code useUser uit at-folder hooks gebruikt." Cursor weet nu exact welke bestanden in scope zijn.
|
||||
|
||||
Hou de scope klein. Geen 50 bestanden in één chat — Cursor wordt dan slechter, niet beter.
|
||||
|
||||
`[SLIDE 17]`
|
||||
|
||||
Cursor Rules. Per-project instructies aan de AI.
|
||||
|
||||
Wat zijn ze: Markdown-bestanden in `.cursor/rules/`. Worden aan ELKE prompt toegevoegd als systeem-instructie. Met YAML front-matter kun je zeggen `alwaysApply: true` of een glob meegeven — bijvoorbeeld alleen actief op `**/*.test.ts` bestanden.
|
||||
|
||||
Voorbeeld inhoud: gebruik TypeScript strict mode. Tailwind voor styling, geen CSS modules. Server components by default, `use client` alleen waar nodig. Imports met de `@/` alias.
|
||||
|
||||
Effect: je hoeft niet elke keer "vergeet niet TypeScript te gebruiken" in je chat te typen. Cursor weet het al.
|
||||
|
||||
*(Ik open `.cursor/rules/general.mdc` en plak mijn voorbereide regels.)*
|
||||
|
||||
`[SLIDE 18]`
|
||||
|
||||
Project context. Naast Cursor Rules zet je ook een `AGENTS.md` of `PROJECT.md` in je repo-root.
|
||||
|
||||
Verschil: Cursor Rules zijn instructies — "doe het zo". AGENTS.md is documentatie — "zo zit deze codebase in elkaar". Tech stack en waarom je die keuze hebt gemaakt. Belangrijke patronen, bijvoorbeeld hoe auth werkt of hoe je errors afhandelt. Hoe je tests draait, hoe je deployed. Pointers naar belangrijke bestanden.
|
||||
|
||||
Mooi detail: AGENTS.md wordt ook gelezen door Claude Code en GitHub Copilot. Eén bestand, meerdere AI-tools.
|
||||
|
||||
*(Ik laat mijn voorbereide AGENTS.md zien.)*
|
||||
|
||||
`[SLIDE 19]`
|
||||
|
||||
Zeven tips die je van Cursor-amateur naar Cursor-power-user brengen.
|
||||
|
||||
Eén — Cmd+I voor Composer met multi-file context, niet alleen Cmd+L. Twee — at-everything gebruiken, ook at-recommended laat Cursor zelf bestanden voorstellen. Drie — plan in stappen accepteren, niet alles in één klap. Vier — `alwaysApply: false` rules voor opt-in conventies die je per-chat wilt activeren. Vijf — Auto-mode laat Cursor het beste model per taak kiezen.
|
||||
|
||||
Zes — Cmd+Shift+Enter geeft je een diff-preview voordat je accept. Zeven — Notepads zijn herbruikbare prompt-snippets per project. Handig voor "schrijf een React component met..." die je vaak gebruikt.
|
||||
|
||||
Anti-pattern: alles in één gigantische chat dumpen. Hou je chats focused per feature.
|
||||
|
||||
## BLOK 5 — Live feature-workflow
|
||||
|
||||
`[SLIDE 20]`
|
||||
|
||||
Nu zetten we alles bij elkaar. De workflow voor één feature, van idee tot preview-URL.
|
||||
|
||||
Vijf stappen. Plan in Cursor — beschrijf wat je wilt. Branch — `git checkout -b feature/x`, en Cursor kan dit ook voor je doen. Build — laat Cursor de code schrijven via Plan + Apply. Commit en push — Vercel start automatisch een preview-deploy. En open de preview-URL — test, geef feedback, itereer.
|
||||
|
||||
Cyclus duurt ongeveer tien minuten per feature. Inclusief AI denkwerk. Dat is wat het bouwen versnelt — niet dat AI mooier code schrijft, maar dat de cycle korter is.
|
||||
|
||||
`[SLIDE 21]`
|
||||
|
||||
Live demo één: een hero-section voor onze homepage. Via Plan plus Build.
|
||||
|
||||
*(Ik doe live:)*
|
||||
- `git checkout -b feature/hero`
|
||||
- Cursor chat: *"Plan een hero-section voor de homepage. Gradient achtergrond, een grote titel, ondertitel, twee CTA-knoppen."*
|
||||
- Plan verschijnt — we lezen het door, eventueel kleine tweaks vragen
|
||||
- Apply — Cursor maakt de wijzigingen in `app/page.tsx`
|
||||
- Cmd+K voor inline tweak — bijvoorbeeld kleur van de gradient
|
||||
- `git add . && git commit -m "feat: hero section"`
|
||||
|
||||
Tot zover binnen Cursor. Nu pushen.
|
||||
|
||||
`[SLIDE 22]`
|
||||
|
||||
`git push -u origin feature/hero`.
|
||||
|
||||
Binnen 30 tot 60 seconden begint Vercel een preview-build. Het verschijnt in het dashboard. Als ik nu meteen een PR open op GitHub, post de Vercel-bot zelf de URL als comment.
|
||||
|
||||
*(Ik open de preview-URL.)*
|
||||
|
||||
Daar is mijn hero-section. Live, op een unieke URL. Ik kan deze URL nu sturen naar wie ik maar wil — een collega, een klant — en ze klikken erop en zien exact wat ik heb gebouwd.
|
||||
|
||||
`[SLIDE 23]`
|
||||
|
||||
Live demo twee: een about-pagina. Maar nu via een Background agent. Parallel werken.
|
||||
|
||||
*(Ik doe live:)*
|
||||
- `git checkout -b feature/about`
|
||||
- In Cursor: *"Open background agent: bouw een /about pagina met team-leden in een grid. Gebruik onze hero-section als stijlreferentie."*
|
||||
- Agent start in een eigen tab — ik kan in mijn main editor blijven werken
|
||||
- Ik laat zien dat ik bijvoorbeeld een nieuwe Inline edit doe op een ander bestand
|
||||
- Na een paar minuten meldt de agent zich klaar
|
||||
- Diff review — apply
|
||||
- `git add . && git commit -m "feat: about page"`
|
||||
- `git push -u origin feature/about`
|
||||
|
||||
Tweede preview-URL is binnen een minuut online. Twee features tegelijk, twee preview-URL's, en ik heb intussen nog wat anders gedaan ook.
|
||||
|
||||
`[SLIDE 24]`
|
||||
|
||||
PR's mergen.
|
||||
|
||||
We hebben nu twee branches op GitHub. Op github.com open ik beide PRs. In elke PR staat de Vercel-comment met de preview-URL. We klikken erop, controleren dat alles werkt zoals bedoeld.
|
||||
|
||||
Mergen naar `main`. Vercel detecteert de merge en bouwt automatisch een productie-deploy. Productie-URL update vanzelf.
|
||||
|
||||
Mentaliteit om mee te nemen: main is altijd deploy-baar. Feature branches zijn je experimenteer-ruimte. Niets in main wat niet door een preview is gegaan.
|
||||
|
||||
## BLOK 6 — Recap + Nu jullie
|
||||
|
||||
`[SLIDE 25]`
|
||||
|
||||
Wat we vandaag gedaan hebben.
|
||||
|
||||
Een vers Next.js-project gescaffold met `npx`. Git repo aangemaakt en gepushed naar GitHub. Vercel-project gekoppeld en eerste deploy. Vercel CLI gebruikt om te linken en env vars te pullen. Cursor in detail bekeken — chat, plan, build, inline edit, rules, AGENTS.md, en background agents. Twee features gebouwd in eigen branches met eigen preview-URL's. PR's gemerged naar productie.
|
||||
|
||||
Wat je nu kunt: elke nieuwe app vanaf nul opzetten met deze flow. Cursor en Vercel zijn je standaard-werkbank voor alles wat je verder bouwt in deze opleiding en daarna.
|
||||
|
||||
`[SLIDE 26]`
|
||||
|
||||
Na de pauze gaan jullie hetzelfde doen. De opdracht staat in de Lesopdracht-PDF.
|
||||
|
||||
Kort: nieuw `npx create-next-app`-project voor jezelf — kies een onderwerp dat je leuk vindt. Push naar je eigen GitHub. Vercel project + eerste deploy. `vercel link` plus `vercel env pull`. Open in Cursor — schrijf één eigen `.cursor/rules/general.mdc` en één `AGENTS.md`. Bouw twee features in eigen branches via Cursor. Push beide → twee preview-URL's. Open één PR, deel beide URL's in de chat.
|
||||
|
||||
Je hebt anderhalf uur. Vraag in de chat als je vastloopt — ik kijk mee.
|
||||
|
||||
`[SLIDE 27]`
|
||||
|
||||
Volgende week Les 15: RAG en embeddings.
|
||||
|
||||
We bouwen een PDF Q&A-app. Je laadt een PDF op, en je kunt de AI vragen stellen over de inhoud. Antwoorden komen uit jouw documenten, niet uit de training-data van het model. Dat gaan we from scratch bouwen met embeddings, vector search via pgvector, en context-injectie.
|
||||
|
||||
Voorbereiding: je Next.js-app van vandaag moet live staan op Vercel. Je hebt minstens twee PR's gemerged met preview-URL's. Cursor draait, en je AGENTS.md en `.cursor/rules/` zijn ingevuld.
|
||||
|
||||
Tot volgende week.
|
||||
|
||||
---
|
||||
|
||||
## Bijlage A — `.cursor/rules/general.mdc` (copy-paste klaar)
|
||||
|
||||
```markdown
|
||||
---
|
||||
description: Algemene project-conventies voor mijn-portfolio
|
||||
alwaysApply: true
|
||||
---
|
||||
|
||||
# Code Style
|
||||
- Gebruik TypeScript strict mode
|
||||
- Geen any-types; geef expliciete return types op functies
|
||||
- Tailwind voor styling — geen CSS modules
|
||||
- Server components by default; "use client" alleen waar nodig
|
||||
- Imports: gebruik @/ alias voor src/
|
||||
|
||||
# Bestandsstructuur
|
||||
- Componenten in src/components/<naam>/index.tsx
|
||||
- Page-specifieke componenten in app/<route>/_components/
|
||||
- Hooks in src/hooks/<naam>.ts
|
||||
|
||||
# Algemeen
|
||||
- Geen console.log in committed code
|
||||
- Errors: gebruik next/error of toast (geen alert)
|
||||
- Async: prefer await over .then chains
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Bijlage B — `AGENTS.md` (copy-paste klaar)
|
||||
|
||||
```markdown
|
||||
# Mijn Portfolio — Project Guide
|
||||
|
||||
## Stack
|
||||
- Next.js 16 (App Router)
|
||||
- TypeScript strict
|
||||
- Tailwind CSS v4
|
||||
- Vercel hosting
|
||||
|
||||
## Belangrijke conventies
|
||||
- Server components by default
|
||||
- "use client" alleen voor interactie / hooks
|
||||
- @/ alias verwijst naar src/
|
||||
- ESLint moet groen zijn voor merge
|
||||
|
||||
## Hoe lokaal draaien
|
||||
\`\`\`bash
|
||||
npm install
|
||||
vercel env pull .env.local # synct env vars
|
||||
npm run dev # http://localhost:3000
|
||||
\`\`\`
|
||||
|
||||
## Hoe deployen
|
||||
- Productie: merge naar main → Vercel deploy't automatisch
|
||||
- Preview: push naar feature branch → Vercel maakt preview-URL
|
||||
|
||||
## Belangrijke bestanden
|
||||
- app/layout.tsx — root layout met fonts en theme
|
||||
- app/page.tsx — homepage met hero
|
||||
- src/components/ — herbruikbare UI-componenten
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Bijlage C — Feature-prompts (copy-paste klaar)
|
||||
|
||||
**Feature 1 (Plan + Build):**
|
||||
|
||||
```
|
||||
Plan een hero-section voor de homepage van mijn-portfolio.
|
||||
|
||||
Specs:
|
||||
- Gradient achtergrond (subtiel, twee blauwtinten)
|
||||
- Grote titel: "Tim Rijkse"
|
||||
- Ondertitel: "AI Developer & Docent"
|
||||
- Twee CTA-knoppen: "Projecten" (primair) en "Contact" (secundair)
|
||||
- Responsive: stack op mobiel, naast elkaar op desktop
|
||||
- Gebruik Tailwind, geen custom CSS
|
||||
```
|
||||
|
||||
**Feature 2 (Background agent):**
|
||||
|
||||
```
|
||||
Open background agent.
|
||||
|
||||
Bouw een /about pagina met team-leden in een grid. Specs:
|
||||
- 6 team-leden (mock data is prima)
|
||||
- Grid: 1 kolom op mobiel, 2 op tablet, 3 op desktop
|
||||
- Per persoon: foto-placeholder, naam, rol, korte bio
|
||||
- Stijl: matched onze hero-section (zelfde gradients en typografie)
|
||||
- Header bovenaan met titel "Het team"
|
||||
```
|
||||
289
Les14-Cursor-Vercel-Deploy/Les14-Docenttekst-Autocue.pdf
Normal file
289
Les14-Cursor-Vercel-Deploy/Les14-Docenttekst-Autocue.pdf
Normal file
@@ -0,0 +1,289 @@
|
||||
%PDF-1.4
|
||||
%<25><><EFBFBD><EFBFBD> ReportLab Generated PDF document (opensource)
|
||||
1 0 obj
|
||||
<<
|
||||
/F1 2 0 R /F2 3 0 R /F3 4 0 R /F4 5 0 R /F5 6 0 R /F6 8 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 /Courier /Encoding /WinAnsiEncoding /Name /F3 /Subtype /Type1 /Type /Font
|
||||
>>
|
||||
endobj
|
||||
5 0 obj
|
||||
<<
|
||||
/BaseFont /Symbol /Name /F4 /Subtype /Type1 /Type /Font
|
||||
>>
|
||||
endobj
|
||||
6 0 obj
|
||||
<<
|
||||
/BaseFont /ZapfDingbats /Name /F5 /Subtype /Type1 /Type /Font
|
||||
>>
|
||||
endobj
|
||||
7 0 obj
|
||||
<<
|
||||
/Contents 22 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 21 0 R /Resources <<
|
||||
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
|
||||
>> /Rotate 0 /Trans <<
|
||||
|
||||
>>
|
||||
/Type /Page
|
||||
>>
|
||||
endobj
|
||||
8 0 obj
|
||||
<<
|
||||
/BaseFont /Helvetica-Oblique /Encoding /WinAnsiEncoding /Name /F6 /Subtype /Type1 /Type /Font
|
||||
>>
|
||||
endobj
|
||||
9 0 obj
|
||||
<<
|
||||
/Contents 23 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 21 0 R /Resources <<
|
||||
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
|
||||
>> /Rotate 0 /Trans <<
|
||||
|
||||
>>
|
||||
/Type /Page
|
||||
>>
|
||||
endobj
|
||||
10 0 obj
|
||||
<<
|
||||
/Contents 24 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 21 0 R /Resources <<
|
||||
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
|
||||
>> /Rotate 0 /Trans <<
|
||||
|
||||
>>
|
||||
/Type /Page
|
||||
>>
|
||||
endobj
|
||||
11 0 obj
|
||||
<<
|
||||
/Contents 25 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 21 0 R /Resources <<
|
||||
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
|
||||
>> /Rotate 0 /Trans <<
|
||||
|
||||
>>
|
||||
/Type /Page
|
||||
>>
|
||||
endobj
|
||||
12 0 obj
|
||||
<<
|
||||
/Contents 26 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 21 0 R /Resources <<
|
||||
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
|
||||
>> /Rotate 0 /Trans <<
|
||||
|
||||
>>
|
||||
/Type /Page
|
||||
>>
|
||||
endobj
|
||||
13 0 obj
|
||||
<<
|
||||
/Contents 27 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 21 0 R /Resources <<
|
||||
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
|
||||
>> /Rotate 0 /Trans <<
|
||||
|
||||
>>
|
||||
/Type /Page
|
||||
>>
|
||||
endobj
|
||||
14 0 obj
|
||||
<<
|
||||
/Contents 28 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 21 0 R /Resources <<
|
||||
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
|
||||
>> /Rotate 0 /Trans <<
|
||||
|
||||
>>
|
||||
/Type /Page
|
||||
>>
|
||||
endobj
|
||||
15 0 obj
|
||||
<<
|
||||
/Contents 29 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 21 0 R /Resources <<
|
||||
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
|
||||
>> /Rotate 0 /Trans <<
|
||||
|
||||
>>
|
||||
/Type /Page
|
||||
>>
|
||||
endobj
|
||||
16 0 obj
|
||||
<<
|
||||
/Contents 30 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 21 0 R /Resources <<
|
||||
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
|
||||
>> /Rotate 0 /Trans <<
|
||||
|
||||
>>
|
||||
/Type /Page
|
||||
>>
|
||||
endobj
|
||||
17 0 obj
|
||||
<<
|
||||
/Contents 31 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 21 0 R /Resources <<
|
||||
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
|
||||
>> /Rotate 0 /Trans <<
|
||||
|
||||
>>
|
||||
/Type /Page
|
||||
>>
|
||||
endobj
|
||||
18 0 obj
|
||||
<<
|
||||
/Contents 32 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 21 0 R /Resources <<
|
||||
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
|
||||
>> /Rotate 0 /Trans <<
|
||||
|
||||
>>
|
||||
/Type /Page
|
||||
>>
|
||||
endobj
|
||||
19 0 obj
|
||||
<<
|
||||
/PageMode /UseNone /Pages 21 0 R /Type /Catalog
|
||||
>>
|
||||
endobj
|
||||
20 0 obj
|
||||
<<
|
||||
/Author (NOVI Hogeschool Utrecht) /CreationDate (D:20260612063152+00'00') /Creator (\(unspecified\)) /Keywords () /ModDate (D:20260612063152+00'00') /Producer (ReportLab PDF Library - \(opensource\))
|
||||
/Subject (\(unspecified\)) /Title (Les 14 Autocue) /Trapped /False
|
||||
>>
|
||||
endobj
|
||||
21 0 obj
|
||||
<<
|
||||
/Count 11 /Kids [ 7 0 R 9 0 R 10 0 R 11 0 R 12 0 R 13 0 R 14 0 R 15 0 R 16 0 R 17 0 R
|
||||
18 0 R ] /Type /Pages
|
||||
>>
|
||||
endobj
|
||||
22 0 obj
|
||||
<<
|
||||
/Filter [ /ASCII85Decode /FlateDecode ] /Length 2066
|
||||
>>
|
||||
stream
|
||||
Gb"/'D/\/e&H88.E@=Z-Bk'U#[ejJugLOTp/6s'qr@f$A/R;o,&ue`m^%T?mqi6p=(<9/'2+<'RN\+-SmuHK#g`>X19-;LZ-3_2k+WUOCJ`jr?'"lQt@<`!S_h5]6]XDJ>47U*.GgQ%lV]WGmNMC++O8WB%JJ7\3@,QYn#g=Bhm!rVm.lB$i$W>r9f/G1,X.XNg'kr;_n+Ya.-7H7@=f#QVH!5!Oe2TKOK]*XS*uSQpe)r'1gi10(/Zk_ol<.Cn[qg(IIf$<(ke%Ng(C3#=1?T(#aGR"&-V#p]jnritF"(+L=A9W2%UA'W`NH.t!`]E8Xst,nbrktSBQ\Fr]Jt7`EXM!ug4lHpNjEpr;;=np%f71YH8ra"$d`^Q,:H5B,WRLg"@P)HJ?VPud3&"[P#GrM`)0nl@GbkWqi2c:lY6RE\e=_\TN,jeo\sgA,kBR8WLV=;HK;1r._<eHY*"sq,GJeZmEkdBf$4DqnFdHMK81l'S[g]^GL38`3Wn8tiW21]0G0+o&`Bj0lr$7q>Wt2fmBST/Ze2\OCCkk&\[;D/Fr),&M79/<!d.f&m>'2Cj@QtS96D9=_QQjV9g(UjD5m;X'L^gAn-Pmq")D<":$K,0Gf+*`k>e7<+L<-Re&Vqr`&n2UQ2p/O0H*O/CK46@a"UA2;5#EN1^bp<:;tB6Z.acZl[=/;"*][=X81VfSW/>?)#N@\n;[9O$bk\72W,NBg[G[KC["eVn>4/<^/dlg`&3_G%DKiB'n!"OL#@<iDnFAr:39dB?.=o4f^jW^+QmekE#3QZ_SY[L+8Jc]D0Xe:ZNX9r'-'YGJ&"i%DiH`6X;MPoCUbR+E-uVK,Y,Op7^<hZGJrBl,CW!4G_-9Jb!jn"B9jgK(A^Ler.=7,lcBd%dItS<]l56rj(pc2k@ZHi%qh9Q/1#"K:aMKYN\ht/6OF)^;/)=p%\Zekoj^i7^fQ!!S&*m[(lo0KT<C(Ma6#N]Pmc'p?,hKB0]k;?6!hF7\tY.]Wh@S(3*L%.46saLI'9UZ[h?p^A6nP=Z7ZWq=Vo+oku9RBUW8O?Z!gTn`&3_GNHOcGq-C"J+k?hrkHJj<HA+%=!jtQrgdJtWb&F@k.OhpZe?=3Jo9;S\/>jKA+T%ek"\t(Pi9?P*BkldN9__ktJtPo\V"EDPh%ois-aDSo<]-^_`u,4`/9ct.)+,]p=X#Gr/DkYE<bZAMdBKN%:="\3e8NlSV<!C!_ZeHi,La';DQ./Dl=sDeW&$m%dplc!=mI#e.JrAip@"8160*M+>2O)AR]p='Qi$Pe_?4d'_h,.]DUU@DJu./]rD@VK@<o8C[ET#B\WiqFmL%J\p4-"7,p@KX`%lk.$KiBFh*6?Qqk7b89o1VCcKQ=rp<ec&%/;1i-\<JR=$6A4YD)0>gMu5]^%/Z,b.nm0XtuS)rdRP=bZf1=i!A@!XLtdLEKOaZm\hA+.N$Em>^FA/FLZZ&Z56eq5BMK2Nf:q9G6n)H%fc2_lIjjk?d(7]0_I$[1KfflIm^Of:>C:/j\t$AC!"C32qVHlp:eUpGInp"n(pK/'An!Nf?S0KYFrFSF-\U=gQOEfIV=:sE@*6/9%lU8$-3r>hFW7U=>/SPs45IqAYS4MJSg[V9,ld-dF.rqlaTgE,9%EFR7Ras3=RoVg>![7DX#4V8\Yn>d6lV^Pqpg"ZJ.k/b)Y'dk7T6=4)gl*e&lbXgQn:6WtIGc=G?:%@Nq3]5saqa9VZ=Xn;$mZjjqV(%;:*S]nm+YbC7;cGu^nmdru15rL"qK\4@Wb3<RDP(19lFXcmYJ`?:/bZF&*+@BQ!";uWU/fS13*Pi=a7R5/V7rXUjO.tiL%8mY,(\&!W::9PHbR@5-TqFrNF2V_^mJic9fUJU^R!d2ih"?kA!:9u<!c!,;=g=VlTqMI1j2>:r'Eoapf,ke"q1rKd^c\$b)nAYQ:V[mWuJU>:m[aJ!i=[qN8*'$kRT:%hqLba%?R(7Z(BfMr`L(4Ys<nhQ/TGc(JJ3h%LHf1CC4AX54cX)!/b,N?,eC:2RF!?;b)_Ej?`f_c3Br>>=V^i!I$k##"jQjnh*;g1WpCae~>endstream
|
||||
endobj
|
||||
23 0 obj
|
||||
<<
|
||||
/Filter [ /ASCII85Decode /FlateDecode ] /Length 2400
|
||||
>>
|
||||
stream
|
||||
GatU4fll+>&q0LUeB*W6%#JrTEm?f"YnCiH`#_Qk*l^3#9qhnO!H]QG#&3Dhk>h[i,Z6?2Bq[qhQWe:_]%qf"iO^1<B0"B*>Pe?>54o/&4/`'+)U[q0?/E<jlUAQ+3>k5Ko>,^E:5>%sCtot<*ZdfRWde:E!fQ][lXGR\?F*_:.4^u`D7+!)+jNt4D29(E3l1.3.5P1M-tc>0j-AbLpU:[b&RB,bLCs+<rJ-^c`L:McGjj^'UQiY))cni!3Vo%ZQZW+q)#mrj+*4*)p<OcSSR&hXb<*E\SW^M3Tk0L#0IbPqPYFQ\.Z0W$<S]CK()AgS([UTMS)`2E>";5'4GW;sT(t:1pEn:,1`;557gD^_HeSS\g15.`)H/ITlOD2!jS;Wd%Qp<3Sdq]K-Ct]^:L@JDqj@Z\BeVNCS!f_kD&1U8kU5cfqN9TmZ`dRq)4fS>kjN"E]H?J>h-==#\YS1+/b\E<p@c&+p2D.4f1YP[\Ru8?giuiGVr>-=^ZL-j`a-^Kmn=$`?)<Zf$ARp64>FffRQ4E?]7rQIXt#<@aC_'.Lu'OU\s2iNS^6t&A!^WD1p_J19PeF>N',p@H1EJOMO#Q6<d@)O0sC5>q1'<uMZA4fP[GkLkOqQb2`%?>$rJ%V<B=?ZBYp6?76r#3jk5?sV42U;m/T*2^>P3Z^ij5(Wo/!aBr[0/gL6.8Sll_J?)qbVTCIs"Ba__:cUL]$s5\Y*5Fl&UA;b&4G`Z^9Z4=iK.K(sep_ZGb=^-R=F#G\!$(`l-ST8%#N-Wr*fr`mqT>#/NqtO#Z8Y(54;Z?b>35&#``Vm;`End59<l<e<c;_6VV2t=+M+QGiJe*ur\P=Wp0c9HC=rU7O"s@`GE,o[2a^q:LQ33dLM=rtbf'u+T,&gP-31eAE$AKR]5:?RPo:J&<2rjJfBKB$JOlYk:?u#:p/>ppZN!ALn*e\?kGb-M@d,TBG'XSl.Ro'R!)>>LSf]PO&$!cNBN!:K_BnO@&9EUX`FB!Ha/iHuE"5[)-(7pT,PK0mgRX>jN+8'ZgAYYT'FHEc'1%ZT&\PfC;!7,J]^hJf:H[NPb@u:a3\3lif#2h=O4uu\;_k1h\`VMN&br:]N'+7MqWIiQsU0LfMI+_\C;"kdMe)o"6lOk`Qo!San]G^ZeTqbO&bh,i$;B<V==_uoUBJi1I\R(_B@@]pKd?_B"U7,\?L4;g"U"TE,$D3TAKAAj)M!HAiheUTc&(alK.%uO,SSc"/[e32V*9S)2)N7P9KHPG7O$t3dVh&BJqATm9dd3%"I80)`hsiil^34[Z!i_(ZWEZ#Dn=9!#aA$s!V%6HrU(GLQ_133a=2#F&O*Zj&:^aRYQ'KcW+3F/(XobjSN,5"&=9monMaPOuTEX,q'c`@5YifRG02Cfl.Wst1\Oo:[`s@Jua3_MT1!+;]5O3E3"Kb0B>094<:>Mq<FgOV+*B@)h[4-Edc;b76%*tNK#feMfHMB`45FnT[Va8b=dV[,-K!<5>K@)K^..jBM*[G;3Fqt@2Dg3;Mc^@Q&7-J$BKr\Hl,BT+[>D*unWc@)p#Kj/?XlbL;qIRnrCYg6_eYpAQCtpD<_gQuU.d+3^.H)Sm?Bqu$lOrQPE6KB_1SA'JC)T*c'!HBmGB^^,j!ol:U@NZ22-5W6)p62,raX*dT5>2s3.Hs_POR$?/i*<8bO''KAJ4&EigG=P5WUlDD'\^2k\4W<#ATDu2\O[`3DSYnlbEl5Z>1D?^R=C;S>.Bl7mpZ;r2$SW<6c\SE?]GQ9<*S?:713e1WR%1A65&'$0pbr-%6+DH/"-sZ*RI5BP<t5!.Y;`'5.XS=s;U+9j@/a?o2P,?(Lrs)L*JrKI0$SOn?45bH6Z*-%)Rk;R7OFd,jVQFT7EEW&^:2U;#q!Cq;"ZaFdM,FuuGTY7jJoj([9km\b#(neq9`*%'BMB(11.4DDX=rF_DG/Yk(c34nu)GV@8:i$9V?3/L-nml.^;E[0)t*P#"/=ZEB5>H+o)[!O`)fI5Om8:#GN5hW`.fP;)ZrhT8shrSN9[k8JIaX'23h<um/62/a4/m<<TI!F*;M4-VJo^:ZiH5?8fgf2>&,8rSt4Y<a74*lF^2Oq'rKPUM&<q5<qTqO/^LbO2uVE3KK]W\ULS`d0`.]dG"Zbb<N@eLBoM\u^j_!\&j!_"TlKAe8NTd#gCC8IuYYaeE-QZCBM]BGe>[+h*T9(l94'H*9[)iKH1Rj[T!ah)FBf$kLg/L&YnMInkTZ)90'p,"#p-c:GPMO.qOSM`C0$Yl55;`VVW^sWZoI_1L$SHoXS;GKU-=!6BNJ1ZmIK-8nc6FW's,(QS6#MSj!BR)I8-$SV\YMNV`arnQ._Y'6pDLGZ,&XXb][&K)r[`DRkg*EtP8bJWq\3>@sB(G5eMfm1fK.t]2rr\<o77.~>endstream
|
||||
endobj
|
||||
24 0 obj
|
||||
<<
|
||||
/Filter [ /ASCII85Decode /FlateDecode ] /Length 2042
|
||||
>>
|
||||
stream
|
||||
Gatm<D0+E#&H88.Z'-h\3^uC@0_S0f'>L?>nYC%sl;$Y?k!0:oS94&\J0_nD]6?RG"LaIM%LaTMp@[*OG6ss7J"_32,lm*1GkEtJYQ]Y6K^GEAK57t84T"kU=Ie`dK'm@=1k_'k)tZ292Vk.3K,47^B(Cn6KV_q:OEd0f09[\'h(CH;'KJ'8r-k1Y_lYdO_a]Y2K/O7u;Bj>-Jggu%0G-Xf<2'\*_I*S#+5!=Fk\m(nc1Zll>F'q*2T6X)@*jYeE-.+4U_=tco.u13)1h$M)P*$;Ct=TnnR?%*:>rq;3WVO=E=>2pK,?D@5Nf6bW>IuHABMr4.<4Iq0"!U_)\?_B#1p!t%?3nK;e3(OM+VE^XH9W+Qm&u7YMMa&`CEQX,?1IZ=a[RLjTq*BF0K*uKX<VRea;u=Ke$n$)6U$)N2W-<rp%hGa.!V&fDUVg'a$7EA,5AJiaei%U7r>LBHa6TqlaD(9h\?ZP<CaKQ(r%%(MQR:Rp^,VAcqg$EtHM3(0A8l=UJLIkQ]#)Ti:WPU+._8*2]Nc*p05s<+$Mf:-+t>jAaL(\KH%COp"IN"+S:h@cLSOS!uSY]s*lj)X!KbkjCR:'BXHsLt^YpLO&oNYZW%*@d:Bsg)UHn?8FX=E1]VtY>SS1?X#)em4ktqD4P&6q@4^1TKbP<0!>(I)qjKK[VU3$TVLn+rqlE#IsD42Iqn33FR//\j]Z)QpHmrlAMY+tFJsnM3PB)klT&q,aB?h3/'>5pB1mQLKV`lfmkLmH'pu&B=A2Og[HFb\1M8_Q@$mT:':l$l8Dp7r&gc_*p_jHWT5OM8*P_$+F`g2D:u0HSLaMi^;#Mo4c7ARa'>DRF5ijcPMXV(,9]s390JkP[B/m2)R:ER^CG7`DKD5t_Ki1!Y>\$B*<Y-H5k[9$X3epZ?Z_eno?k']/4f1K_EQ&GDGP?h`p3GtgP+j*pWK\jNgWWKGjIcZK:\'BNLCUX]X\@*4#iM^t;'DXL+g&?6A8p>%`;#1nEe56`[18^F*-EBHdQ7`6-P=m)F3BM3&ZXh>bn?,bR"4pDf9'QurPR$Hl0aN2hC4%B*N-Knn)g.h)P#9R=lr!=[ej=:07&Zo>ca&<mQTohr$%:h._1LF^<QfTR9L2EjGEQ[YsL='I&&Rj,YcnTI'0GU&JlVSZ%J$)OgkK>C?VA'>=uUTEN,UN6d.\@ih!^f]_Sb^[q<1RA;iP.(n>tX9O3<Y2*i^h;Z0-6#sh_/aPFqt9\JA=C;*V`C/JNk4c&:t.9Z6U"S=->H6b!0AQF"\Hn:SPAieq/[3Ijm9MsBQbi0V9/>Qe6$$q?]#f=SLaRQ!MJlm?c4EnoL7/rog6n'03CNE*)9Jqj$rmIB;CU0la'#MZ+,bd(dEh]""Nt+(F)Y<JaAnOA&P/`+N/k>mERH<SgLY6h+$Ndi5%Qnfr*=p$TiTj[LL)k%d2IcVp+eo[g)D6g<LpO_>j85VNglKoAg2Qekp9hZ15$W*L-ebpipJO.#R0+:cS_:jjVSW5S)W!BeT2EL<%8(J[C/X>JeY,5d/PKDc(/5uORMguHrm_*<6*N0X:`1U?ar$mUYaV#!/TW4SNddZNM#'5N+_Z*<fNS*l/3b6i'CZ_#EjTUap958I+Pj,RMP1R8@+d=c%-to0AgA9cM4A=2]QL`egL!=FqhH`^9KKMCZhVYtW9Q(u"YbNW-]ZfU33;4@Zi(o6_O[>7NVY>)o`]W`a_Q.]QYPrQ[+@1tpmH:e9*_Gg^6c?3&Kp9T[6F<WQjPP(o)9J(GH+?*e[X+^'8jsZs(8L_-N+Q]@b[`._O;QiQW0/K2.@/hL-`$!3kRe6SPFIgIp!rLhVO.oN@)ZW@YIc=9T>bu9>677_AX+&L7PMEZi^j-(Zg&'-E!ioj*3ls*_e5M'#1BgeO`K]]A*0_R(D0&]K'VR;7hMi*FiT\Bdr/98)ElpeZ<""V."sC]i`5`XFB*F:GfE>')af=^LbV46(Yd%UV2u8c'E=^:h%8Jps[K<hN3gJR3IQS0d45&Xf7>I:l2Pto/o+'P,3'[F,+o@"4-rKpWJ!H~>endstream
|
||||
endobj
|
||||
25 0 obj
|
||||
<<
|
||||
/Filter [ /ASCII85Decode /FlateDecode ] /Length 2580
|
||||
>>
|
||||
stream
|
||||
GatU4?$G$]&q/*0R$3lJ>DpmqA]HGe:?$/sp%G1#Sb?Ku%j20V&4o+pJl^o=^NTZMU-@qjlf6V071p\dmbG@*&In7dnpcj\khVccr%b"oR-#9GVU:XjGB>orp"riGp;tuIR_9`V`J:qOkiuV:`I#*8N@Fe3q'uk/g5\7cdC]g=%`=r%jOp0B;.8!1Vkp3Age1AP>T59ZO"^.^V"//E^VDXjFSs'DQBDl.*K+*a-u3($-i\3!I4g3p<4GA(a5]gUdamhVRP/CS2`fD3Q+E(RS5Q%02,Zs:Y#/caH(u(@#8]=k82SV*@k)5X]/S1/j.CFmYUl3BYaY;dqU;Vb73k<>9r$2)A0RAoeT>_gV3S9Z/7hCgBo@f(2n$`8s(Y//dn8W62+s?Fq6uf_L^/d9<i'o\@$O>n,$CjUZ+p<@ekYH$I()1k]0$PL_aD)!g.AS*QY!OL$IS_k5Go]M3U7Er&AZD:!<<#ZmaHqq0]bM^-G$^5`^C&*Bq_!r".;/iCC)tZa\m'^]<sM*+i/^LCao^q>:=3:+#ueA='B\?f0VQXi(2Co1u71T^cWc*fW=sX2&^\IYd`u@R+E?Z]3B\rQZRDgj9(1QUB1h6p+,Zs^A2?q^2S6'L1-j,.\3#NHZM!s=[X?b?Ta&l8l9*k`AUd%[8Y833$+[=R0$UR?`VUW:_!%Kl>oa/%'T;h5.l\DV((F,+=j9W$-9JIRJ.eVYZ)tX^m[!i3;_";Xa;AGI%@8g_m$\1D5.I;#3gr*-s:I)]9KqL3+SNfTR"oen;Gudja\R[OMjq&IM5WQ4.()<H(mjF#Ga,;!/a_4JnI?.;t<a"*`TFa1#ICU9>Gme$@sUZ]B>ECVF_#^.k<.1FuS/!=;V&0^!OVd`'G(m"3"B`GUIdIFB7M6ZtVoe-0pOndFZLZLjq;TKn"FA)#VkOUAkb^T^#^H8Cn(GCTpF-dYlde"B\F6/6(b\qX#`Wa4h;@Kr[)N2Nl#pJ];UX6lrk!ZEO=n$s'!;%UYCMn%?/T@dPHp#K.tmDC2;kK9adr[D4GW2ggCJa<4$f!^MlGCpj.VdHGKE'M:Ps['_HIp8D7kh3P-#s*1%F?q/)$&<eRJmj.#jXusWaX=n.Fk!JQJIVTIK:nMSp2WC)-icBrFphCX<*XAP&>i9og@(Gf*];&\:^NtQEH9(=j0Lf+k/&pHYIphB]i3h1=<l`j(R&mWg9FBBR2B^(M]n'q05MXl\`U!T)p<GtRRGL#IH1)a"M#I]V_E,%mYar5Z\V+VA]I-s5k'1Z4>.s-Po>ULd%Ud`&!DFm>bi6`hm\A":,+VOVj?\':n=#8W],IuW8iQg@"K[]"#pfR3@p$:sog.YY5mV:R;@pQ%dF$\6kD3D75'FA-:S%Fa56uI.oia?i&K%#aC#8HDn3dEDI:--T)bN3WZ#pJOpgtEY_l#'fS[8TUX!r^q#E4i_-*W/eNSZOtl\6'+SVO*84->l#c`N!IeTa$*=pg`M,+=FW7$>WV6f,A5Xm\79/M)$s3XP7#]n]"FOEO6S='LS9I6"Kak[US>j66s:M;NMl(&%#0Q0ZJ^WeN,/Kb^]l2Rfr&+X]2,_b6tG]H]MW^L*&Fm&b,u`.XUC%YiAE1nD:Uq3oTn?8ZU3MmCuYC\SIlJNCuUF5L".RHV8J[6hMgT4<[0V]RBl9!=A1U4RGS.78hn-":b-4I%^V[d?$/II1CsXCCCka)l/7i*^bV`7]RuO2V1oFrqal)0@j\209.@Y6%9HPPiL\f?#7U=ilYmm$.al7m6559p@"-bdMB1_#B,6X:rfLN`Tm&pMIfX(@1BJl6#P0#WkjfYg5]_C*15,bO=n=4R89qJbpf)!T9S8j%^,),\]Kl+1urtS+AhiVTa=^o0HE_inXaZ]]\+W%b(as$9pJ%SqiEF?,eZ_or@'u8hAVT`:q0Y_Qmrcg7n[em"%iBCaE?]SB@4A_m[mqf@hX+5Ju[j9G!:L3)6)ELh?rNiTsJT#Z7;'fSApc%m@Re"K-u\J1n@a2aGTfkj+9mQ8j=g$%KlD9RD#<nOp0"kJ+J!_#*1YnB>_bC]HSU]n8PloY[b.-VSuMW/`JPXTkKi6"Gb;_qB"Q1A<>4JS]lD5J=:M!jHH+p9kmH+JMU?p4=qa8'R+_M_jp^=2V,>r)n]V$<U#L*66UNXKX>G^%iRAS5d?t^?!EEneRa8-QA<qg$4-7^Kc*-H\<;r?(n9\5@sG_GU4#gGoKbJP0<B!/K[q%/NTeeWu(j5G^FWBN>;[K7Aoode*D3k1Aj+M"T"*`_nHa#6$VMR]2"5uJ5/&bi#7SQ\S0>`o*N`'naWOLo@31Ie1'R6%M)-HJ:59=-[OSNV9t2A=t!-2akMeZQ:`E.HYgN=D-b>[HaV)#2e;<]NR-Prpt=RgW+8Z#D2j1$f<($'%usRBTIua!8Kk'RNX_jo"V_jOeMSG(Yt\g2mUQ,IrFlMlcMqJQpWjr?&_7S+`,GG/oWlpe*n!4G!2p6@4-?Na3TQi#Ob!fg#75-?3(aZkqeug<_$LLkX4m!J/WO)i]<7`REJ;8EI]1@HiNO"(H&uTlc:[ZL1]iU*@VaOPp\c2anc2L4ofiLrj1P~>endstream
|
||||
endobj
|
||||
26 0 obj
|
||||
<<
|
||||
/Filter [ /ASCII85Decode /FlateDecode ] /Length 2519
|
||||
>>
|
||||
stream
|
||||
Gatm<gN)%.&q/)-FQI's3u1=f?hBLRSBSpXSkM'9,ZVJ0#De$]L_k"1:eLBQHQ&P*ag)S+B?Nlh!C<AuE6d]c%]GG\Jc@Y]LgJob:HoA'0Vl<@%E?e_mV_75h6T><%@W\t_s$H-GWDGgd91QGl&Y?U*ok4*],'6Qn2A&I=6oHmdBNFV;E@qu+]:gGR?P^_=sbMkZ&VjgRm]5>W$Oo*EmbZ%'R;@_73TfA;m"hI9B2`gk\`t9J*6%pM4]PIm#N6HE,b@'&9",<BHuo/KICOX*D'6XP#_iF7XV'(&R&&QE[@5SAP+Js'R;<FXd0ok@Ruf_<(TG5BcZ4-Y9[jHO,2EFT&F3)=0e?Zs8PkZ&"@f=rZPk'N,<&e+[&&YpG.b?j`p+OfnGL.!_O)SRPYuu<`ujU^sbe6=1RYtm+LH/&(j%p%RD-F)<>'W!Uc.IU+EPoAI;9XL6hn^;5"[!Jj/2n.5dQHUq%2B2OK^W8HiElGdKaL3#[q$185;"/n1`<NTcproief&<WZ>FraolA0oW(bhd?,:XrZ*CeODU_#BLskhf+k@!cc$dPLd8$o&/rX'6PI2N_rNY`5lgP,?IM./fjF#Soqk&ORrgn<t\B-7P-EQLoMmUFNk&hN%R?s>KL!lQ-:FurX-^8PS6<to`?1bdNNR,oWUmWS<W\ZfpgiiWR)R1YN<tX-iM8SKOd@p>qpH)JPLqj;Vn&?8TiVGr;BCg)2I7E*?P7Pa`U(`&4(b,nk^M<9cJf&cbn^%ANB/e]Fi"WXNT6eK^FH6U<c&O5K(ZTrs9j/%R^0gmEP=i2`-p"Vo)!e_^B8BT*cdfTU$MO'F]X(7q][c//PUp"qP#ja*4usAllCFOP#A"]PpR;OsSSr9df!#\)"FCO89ZZUJ&1&J(n*jZ,''C$/TF3QLQkXf!tj=jQFqOh!3ZfT&O#,8ak\XG`Jl!R!6ofj-`P9fL/%(iT)]NKJ.`(!n3aBcZ,NRMs5'9)cR-]`gH?Y;BZ&:"Ye2[.-$#gV_%fn]?S4)is*N,KFdt$HpdCqi"13_hg`6<H_?+kVQ%%.;?f3l!Lafp_-IetO1,KSG<`f'8dugr]2m3U!E+@[O:3M1P(>_T*"*ftcrd(;egPRH['$=CViB]dfOB4P1j)rAB&p=s+ieQN*DdW7+%9aged3'>57=[u$P_\1T6(D+)Xq)Z>SZ&mi080jj^6Nf&Y/=N3ZR+PX;iudD4*;4!r2/G5q",9>0:PMHlNb+#A&"6!l@BJULTFA47PFW]K__)7HJV+d1sj_rpq7FA2l(K(FG`t]6dsXN\AG:$d\cu]))1@kKX.>Q>UWuL6Y3KKg%K"M6O-YH;Vc/6"2;8PHg(tNBg9J05tAQhNU1Q>iN*!A]8[r[h]X0?$G8Di6a<dCCumM"k6C.`!bY+3HbmXJ3G-V?+rG_dtb6MQ_#\PR)Pe1,)+Ip5DV1QSVg7[_%iaP=5$ugY+,RF@4aT)3(a]t.,%ago4_UcI=C.c3T='EPXZJhT2IU^'Y:,C=Z;:S>r\Qc@5rCTm"=3$)Vf\1_?$m&[hY4OJ_RQiE=jAG>5L>h8Z]14=O/n4R@&Ck4[SKl)k#$a^b^;d9L=m<>r,UhL_Iu4II#Yp'TS:4Z(F-!PD<Cdh^`_U[]\:mKE>8?&)#EId-c]QZmPoOcLVP7[TDjMO"n(c/u-daSFS>gE[eG;#W@r3+aQf%mT&B'(s.20)\j"Igu7fr?$*)u8,^3(lla%>./_B<J[Y#3*+9na/8D'ji=l^:F8LBVkHMffCGmV,K0nL54ZcL+7qlE<]&M>0]U6*6-i7J#W@G7U]g"^92",csKi&=pB(R"=H4!nO.ee,`5%);"nR#rK[V](qB;Q.F%K^HM/g_Mtm6%+[BpA(4Yf?UOo3`.e*LGph(+Qj9>H?PeLbp=K%G(">D-fNr&DgfcbiKfg5Gd*SV&F!m)cZ,I3%VZt3MY#e9/#K3mBfr%j$dpA<59H_Yh5m,`TqKkWT3hEa2&52]fL_&S3;H#)@&j9[*HaikWkkUACdaG(lk39(d6;iS9=$VL;Tpr2tr\]V3/O-1rcfIrf[U?PV8tL0@o"?eM=X=T`J-4rG!-Q(j'9`,9I`)Of9+p6/:(,>GRF$4XW$mDt/p-=G'f1#[h^RNnuJLH2I\R$=ZKMIL-n,2>?KnLL!Klo5tNa:3d:N06<m?',I6#r20&N`b82Ho=Bmk13b/R\XV4%*tXp6+B].n0!%4g.add0oe2C0C2&XV>W]gE6M+s@GO+C+j]hJNKrj9fT9-(MNo==\b!O!QH+-q:OLj5l\5Ta1L]ao7bYT'$;gQ1VBX7l5rdZG($'TVp2`bZ!`!/<'XlLAtq2\_75sCjmXeWGjH,IB42Ro3Z@H85@`a9[krU36q',s<jn;q3/Ae$mWo3p%ZeMS`)YbD(T&^`7Z:Qtcd83=\6,(#GhAYi,<,rXc.\UWd!iiRD/rgGGekWd#j+7Y]h%fUM-I;F$$^\LM^m>!ac]^oHu?=ib,`nWhp>AMfGoj?h]kf`;hkGDnl"_UhVrr~>endstream
|
||||
endobj
|
||||
27 0 obj
|
||||
<<
|
||||
/Filter [ /ASCII85Decode /FlateDecode ] /Length 2440
|
||||
>>
|
||||
stream
|
||||
GatU4fll+>(4>jXgnQ79-6N?mWT@i)KbGu!*QMWZPDn[u#rK^=!Ap\$&0lW%e89`*83)cK=ZT&>06aC"<FA(?Hj/qNS.iT,d)j$)$::2W'L[C<P8bE9?N'=md&n@BrocI,&*UBMk+<+[&bb0<2?rKR#2#MW4)];_p%^R3D#]tB*WI964fZ,G*p!R=(M8fnA`6?pAOVa&JlG$#d,Y\k#OV4p5H[_Wc&&DU=e2oqKCr1>6Q=l26p]k&?cH%`SX5YDQ8<fI*sme*En_qEam8i?+;bSaBsOPplCkL]k+4%PC[0P%l-junl-IQI2j>4d=>(KA8jeNJTKA"/B/@(?U<&Y[Tt2eum,<P8P'e/ol'*GOc8%PZ>]OBMm7S:fXodq$J>VW"?>D?^1?5SphBP"_P]XH[mH`@OYC2>_q6o"hEEuLmFTqX;)nlgSFEBQiY,B+h5PFa@3au+5Fe)da9MS<a(,U:bj_\fJg49=+fM\jce9E0:[T#[^S1G^EA?)$iYRTi'h;7]PDRN?MdbIL%9&MZuB6>@*>LE(o&t+V<Zb=&`e<Qgh;cpM1Q+!P_+Uf)H;)=iIEMgR/3?"O2]u0iW#<<cu7+J-8_sL=SWnD9H,HgOur2/&cj#!%+]jT@7[7i.e$3A:c+_`*Na<MB'"`+.P<G<sbop<W0%hqcpkJBBd=B>3=jX,kN]R>fYO`ebml9>N:BH&1k6Q#W>I(]N5&'W>eG[n=\5ra-&[gP^lZ=@sDM$<"b+3g/iWR*DQH-.W:b7>VV#f@IcA?agOs4lI9s#A"hr3sR4rMWZ&%SkMDU[QQWMSVRKDsPjW+Y(*=gF'-<W5s!l[9bh)H!8eBT>hs"\#A=1.XJ]$j/676*a_VOX`!W<A>q_e[I&L.@2*i6n`V+^5>+qoF-U#nad.:fU$9^>-QOL:3WMD0,G3O9PH$%YELh^_"BN!i+#T);)1RlTN0;8]!+-jEbSFjm#>J_k#:kXU5qQ1%S?%`"T"Xu_n!mQT=Y@Voh0CM,;9Y!1_ebXU)Q'GSD0,OL5)_7Rg<MF"Amr4dr"kkTNR=UIOj9B%1S"<EH,0?<FGpb<<7e9JA=;ZeRJ?DO.prKA$`:;q-Be%e0+ieEctPT\;0^eVf%,9eN1?'Ef30=k/%Ck6c7C-ieq"S]MaeCo'UjE7M&M"oKl1kOIo>uG4iOV$b()JjIi/ZI@O5_O<`!JDOh9UuH0nb:ZS'HASThtbPNHNpU5mDP!CkmTU.qACOIo:,'^]T&2_rg,#=aTb7udLgL;g0uE`2rDhSX7h,O,T&,[$T@K'hGf%>9()@XunJ@b(j7KC``hAO@8@A^(u:DV2/`jNVgW0<WqaE#^Fu3=LQ2;O,?=\b6pBD2eSb])`"!)OlA4ooQb1Y]Ep2NNi--j2G-?+sUMk5WFTo)JleQodI.gj2G7kn=6us1AmnME>*H=H!`*Q9BZ+[=^nJ#8(QY$:$aT8Eir-NMbK@!I9+G9Sb6!(R_fI$]2PW@9so(W@>DBQBXhR+[biCbl"n:9A_*9tH]F/*^.[(?\Hjg'X$(WP4-a3Fk!DUO!]*$%/-qIUMM[u64@Yl2jY/as_USi0[r[hg2e/^[[[jDH;?fC\daTe,]N9.<.Zt)3XQbO:J$#8'IF)#\,&,#1/rdcg<,?gD\9Sn\W^<D]TQ/8fCr@=s%nNX%;t_Gl*b`029DBcd'E?YM12q[!5MAf&=KIV:7?@c%G7=Hs5i@pq-)e=e?q<BKS#Mgk)T:OebbWOTaaC#PQ1_4,KcF/2%5'mqKrp@$')>qr&#.I5,<ri6hd8%pe4/D21aIQiefUD,an#Q5gWQsH*hbqn8n!qJgRDQ"TjcF()h<8MV[(IMUmYQgF1gJrfhONa_W>4@".+e<rX1?)daAX`Rn(ZQ7^a=k=)%%4=@ppjis)!B?FL[63V%s^_4>74mBh,QJ%FmkdkHTd69ju8!S`ED0T62el<f&2jFM"o'2GLfd$AQS+2Ko'>V2F"B(CO^1STE=4%P\o=@?OW?Kg++2dPNmo(Q)6m'0K'0&-%B$c+[J\/\R82JegLHs8,6-+ds%R5&,)_N-$Ek*LE@RXEtR,As,R/B=1V%THO$SW=951WLV;<5Ma\QPt1o"6mm;eXa]8_FkWgHPl2*C0VDdHVO5QSg&$>9]:MKUf`DJl-S/B/F7u2BDD;f+jOiD9jJtO?1:?0X:Xkun6&3)GhlsB\h84jIl)1kNGOIU>ACqiFbTkYIQPNroo][iMd4%umhX2?.hb$?aC/1jr)P%?e08Nu_^i5e#S8Whg9>7h/iGL&7GlDr5mKRZU=$IAJKqcpk'AiJ(CiKf2!n;i.]&'S*>JGBUN>YU9X=<"0J0hV%c(Ej'#'#F&".?bmE6(6`E*KAE4!3Z^Km>CHedDa<Jb\`"4&b1?`#=#>#fb*%8K]kNH#O)_]UoH2HRQt+jX"#pMKBEhqT$]qEO7?"gJ~>endstream
|
||||
endobj
|
||||
28 0 obj
|
||||
<<
|
||||
/Filter [ /ASCII85Decode /FlateDecode ] /Length 2037
|
||||
>>
|
||||
stream
|
||||
Gb!;dbBD?*&DcM"l=9o"@`=Ml=*EPn6\(9g%#\O0/ZE3goc0rB6b\#L]&dR67h0/o%"VejEd--;Wa#5?L)0a?qR7OK]H"fQ>8qA`L,WMPQ4(T'GV_JYqoGr.lu]At^_teOFh:\hdrO]"FLe5$kG+gJ$8&rCF6EFKkkE>'/>qh+g^[HRI]GVN^`'TU:UQ"l3#S81VDXq?Ref^;Dg&>Y%#,E453EBhd*H`r[V^/']5L'_SaQaEn)iVQhlL_!YPtQGM_kZg/CYP"5JE"C!f./A0i+8BqF[Uk9N/MK5T$CRAn$E/]ZW_d0i9(kZ,1mQVb(j%^IU!mJs$J9PPMI&F;8lg)5nDs]GdA#_^*J^9W[:;>Joc*((Q,YEj%&3](\Oi0q2WYpgZ<-8A7)Z%qZ(ZhJ+/J"G@kj1F9h/`!F3cAV,ko#Rd$^"DP.DKD#?3BYN*S"jJgGkZ:T4`d*k=5>:-<7E%THMMl'n-b"+A'8dB4!r<2#Dqiel%?Y5"\u&HsI0TsOlYm[lpY)YX?H'a=&#NE=&hO`4;"<V"YBZ<2JFd.10Vu&uHq%*dm0`iCYs4KPp3l0I8Rd8o!"5C^/K.E85P!8_VZ3)>*8_kP%'p:?XE&^O0OH^JeTL!_m.=?rGgE`*;['WZodAiD8Z2nhJQhQBKWH`E`'@O8=e<EHm;[aOq?d0!mIK90)fV=^*o<hOE)nlC6lJ$1BU4bBgiU?AQT:Wmnl>##nS-L/]@3kspQje/dK&-AJYFE3>F^!\"UTEELK![Ik!&;Yc]'KrWPhrg.qKDsn=F<7Ki;POUnS]QWQ==Yo;DX,6;fFj9T4iFidY%]OO@g9?gpgjpSE'8=^#K"10ZY"XpU,*pnQ`M01!s*>cuS4F^T3[4j'J1d>3d?clA,?Sqg$QYeE9$Z#dTF]K[RaQ=\g'::gVG))PY-5tk9D9F.iHHODIf'MfQ/#.$uKL)M?GkiAG7X]km3;F84mPsE3ebp7=-@e2W9e;)6+f$YN7Bb]Ms(qgL/oW2XO]q]/V8;8;N1$AN",KXl<$X^_D-sh@]*fSH=b?7Xd=70Ig>]"32G)b'qHg]osM@h4<j&G?>1C]>:&64pKUJ-=E*uS%pHRa``qGR)3M6/N>JG"Q#i(E'^OGueYjC'WDF$&3MXQ(ELTgNHWgJPTk-1),ra!k`W4l?TNfkdiqd1^GWIRmUoVRN4)S]2]!h0.QU#%p.GDRON[`I)X1:&)s!j_Y^i)/r76g,(O:.:"'*#)+7=7MlsSBT.Ng)LmNK?HR6+.1cZaR1Eol6HMf`iscXM.#GPf_r-'Q18n>*)do6n?5/[GeJo'`/<H.d$l;N'Zuf0oF"I*p5UmD5?:7t[mPS:a",Pe7lfpe9Gs;kY+H[dIf-DFTH=)lY$k:IRP5Ng`&_2hlVO_Ha$20Z1-j\'CpjYBI:PROYB)5UVV4on'TqI*tmP+dc+7m\jq(H=spC-`&]X\'<I8%HbG*'J+!W89hd"38-Xp=eV:#@2Xh%mJ>s2`s[mjQ,HE#D"U9^><><Gc=@\=^9r#k1gbLBW)*r1jK*V@aH7L=LpUPuXCk-`fOli*6[=Z7TWb0IrtE/P%c!'I63O6%kf+@1D&S\GJh9/Xk1kTh-8;++^os@-dY<\HC&G[Bu_mW"mdI,_"jSms0k-BenZ/+sAsH/5;W<T/5o60tjB[1*2;X#"m&<eTF1WKJFU&r._,M@,bF%Y&\uVbL<J%_goSTFo-/F]pkr_NECDgn\k:8d$'e<.^1c"OCh?ha,&C(Xk)?ZZL\CW8"BlpZSnGLYr]C)mcW^n+C"2r1X:-Q3h9O,XOS_j;PgWpP'hH7_UG/'h1a)RZRGdW#chsQ\AA\UAqUBZ,:kb#Fb4Q3b_(S0PV<\@Kg[qcpmB3VG#+24H"ODqFhEN$,mI`ehXh(%$f7=1?P_*BX/QCKn3+ZoEZkuL9p20S_I]sD,cX1h04(-[S2?%INk>.lJp/5tP81M%_sU`G\(b<om_,=_n9&qNRF5C1I\-X^W[BHdT0L[K4^"u9(E'mDk?X9,MBm)0'or7oA<fR-~>endstream
|
||||
endobj
|
||||
29 0 obj
|
||||
<<
|
||||
/Filter [ /ASCII85Decode /FlateDecode ] /Length 1624
|
||||
>>
|
||||
stream
|
||||
GatUs>Bedj&BE],.H[TQX\k1ifVD;<ls*_d1i]Jc;"pJ2dK#3DY.@e&i_SJ380AllC7H3$D=-cHk;V^g3d?A/B=o@50qr<a64+qU9do?34W7VS`TZNXUN!nf&-[\p1^rWE3s\UYSK?'b3JaV_]BKo\KT05-=?pJb_G2[],e!^KGkq(2Jti8%:MDY`dEeQJ"('k`'V7KT;>@8f:Wh.DBrOe#e<Hhh+,4Ai*jZ.oeii6SebuVaJLIVMCt7AsO'3?9BZ[EMGPoDbo:hd;46P86N$;;dM9QZ:Yc#X(D2_XFs0@*\&_dZV[J&Z@[fcr5]ML*N"Sl!Ab@8Ht)5Wj>H;fhCa'.*]@a=E*9l6IXeD_ak1=)2(T%U:JeQ_;"j]cO?]2D#1>nD0ig8)#]hFiM7%cM,.Af5&K02#AKf4`Y<'"u^2NB(Q1G8!Cm'[N".VE@f>R)YuJP%2oEJseUG=E.ol`A*ps^3DU5(bDnMP@G?*;eJaeOIEKF76mU_F/*Nfb*S+r1"_em"<r]q<),JciYbcB]_/&TnP\tYrTD_m'*ro6ogI(WfM+<@6^3`hp6lL@N9Hr"%o;q#jdaGWPa9<R-dGWfV7B!nW]ZVg<>-orAFj%!*p/A*$/[L41:u7.?%TH/gT@%daD><apo<C\dU'1Oa3(3bZoFb@[=VGe!@Z6H*D,`_<LJI@RqJf9D5JL_6l:&;5WXtjdQKQ,*.W\@Yk-m7Gc0IEjf4!+;^&ss<NI''n@+o^Ya/B?l>m8!i`&&3KLi+cFQemGc1CZ+o2c18V&p3@&S076O<D;BL[qAnj*R#`fV&JYD8;Jr$>5\D:#f<ST<n;ppLC4#"cNRgK*jl#j@FDn',d,b'l=SE-37;=MQ1t*)sVTMP@daB)b%O.@V+0VA\DH<Z>2&]:_rp<#^QV6pVHVp%Wf!kls."hj^o+NS=[X]DEjBS%3KbY"EASJ=bWP^fYL<o^efF)7KH[;Cfa;_YPVA75`_$4]r7XU*Lh8O[FAsIiH5XG;N9sG9C7.3dl1-RYbQ-M;X<'<?PT`G$h(9ZOXn`..]Q3cYrBZ/]+:k4hXH$=l//9Aa.AN[j%Q.?b7`*-4.4fFmthpt=&FNA'gAr^pN;pIMLqFtJE.m?m[ZOr+F?q-EWE+cq!Eh]Ec!V^"T1_!;ZRD_rTA9Fp:;0\M_QTrki*/'pEQUU4c#b,>g8&h_LQYeImkG/Q*$\("?F<%K\U-2-,/&?o4iXt:GUcPG@ti.l"#O$VC_HGZKQ(,d:R"6A:%fH1O]L0-.nC7"[5T7Hfia=M2b.j^b@kd,5Q$X'"h/s+;AYm9ZcK4Ra],#S,q]LR0rSRX:o4_g\D[Jjl>%6\*BR3"SEUq1%p,g_9-2MoOC(eT2i&5(3Y5=g'I?]\hS;&Hi(*Fh\,oE$eL3VQsDkBT1`iVGKTT,Tt0/8/6p<hP,lEQ5j)oP#IJ3NKf2JS.;D&f9rJ@kF!Hium#Y]d<fB6h=110-%J&^KXPRH$F_?7i]I^DMO:VmCo.S-Z5E2b'_)jf&Lr+Tokk54/;>ArVd;Z]`//go(]flVUdnY)mgP0#0\n>E[;d1)=R@d;T>PLP21\:sDePQ.UG:7m:RoX@sYO*;Hgi>GE(V6*64)WHn^[Y(LKE~>endstream
|
||||
endobj
|
||||
30 0 obj
|
||||
<<
|
||||
/Filter [ /ASCII85Decode /FlateDecode ] /Length 1092
|
||||
>>
|
||||
stream
|
||||
GasanD/Z1/&H9tY)!ub9XqV*["/`\6?!DGYUsnS.jC>#jQ8kjJA$a1^c+i-jOH>ZL_Z_L)0Ri/Pa"nbD0j_6eYA/UR2(UshTFBCUTU]t'rromtkXS$2c]"!f?j-_*1W"iR\1Ss^VF<s_\r/Ns(q<j^+qC-I'^Z(R&^PMB6m3nHNjsn__TB_"*Lnkto/JtBLkAJ<*Gm@Joe^8DeBu9,_C3DF_-,%gV^$hh"ehe0*Dqt-EUl,]^u?eD'u^h(NA!f)j8(3ol*6*eU?t%l10c5&06[IdYBEUXW&C(TEYkNZP\%Y?+E9_H=P%Ms==i=Gq@V<IIcm84,i"%D7^eR\NE=\9_Nj!"=rI^,Z]DE_MoqK$BtTT\4Bq-E<f^k?=Qd.o.m"Du@:Cc/30j?:gp^?ZU\C,5>pj:9;O$g'V\@+(`s!"1^\=HhO;,m@a8?$Wlpek7n+@)&RV*lt"<XSXBDLA+l3Z&=7]q'rT_2&[*bkhJJ]`5p*(MDkTJZP"8$)DIm%iEgHu66jPk5TYoURebIg+TB[PLq9M4#J8_ZJ!lI+P4C0?S*cm;"F`ATi%==<?H>\65p8o1mWkE\'IA&*-g?4\H$FXW)&[6<a/uLQ1)TSbDO2:"N"9[)*>lT[Z"jH7;Jl`bs:D-4gidaKn58?gCO+m3U1$PlGf]LoMF&Z?=]<#*7mlcJrB23oZCN/ELAa>g(]bB%,brG*VMWG<SKY?jfb!EcY%\doN@/3!m"j@*5eS\AV+(YJdoRkFqFa8lD6+,P44^:+k6i<be\rPFOO=#ZWe-nq49>QQMRoT#<`5,:a&gcfu[n(MfYs"7_0913,jmkCM'O"KqC0eiY6]P)E3&iQRGH4E?UH4CLpRp'r-=XjrSJ]*iF0h-,(W2u#5]M@AYJbOU%6]62E1P8Z7JeLtJfK&)$fAaA=SU2QDs9BYd$cIBaV+4.4F]r<XS9U/guRI[1R*c1$]iZCK*&#D%Z_VY63Sn8;F$;85urNuaNNt+Au_D7WX.?J'W[A6@1Dk&;4`Aj.ba5n\'a((JlXIn\mfBQ4CTu$D@;LH+QI/EYa7I&,VPbGD-IaOf6&tUp<omaoi_[E32gas"m~>endstream
|
||||
endobj
|
||||
31 0 obj
|
||||
<<
|
||||
/Filter [ /ASCII85Decode /FlateDecode ] /Length 1699
|
||||
>>
|
||||
stream
|
||||
Gb!#[gN)%,&:N_Cm+=+rP"#u`Ji>hMG3Un?Bq&3sTCbs9JgF#7_2t<0pZoHL<H/YVDJ)"q4I3@m/e3(AB+HHDpnm`a-j@+a_X*!YA[&fLapEjb>PMoHIC*\"dUU3'=\-C!IDRtAk[tKBe'OjDHj7"m1+9S3AoZa^\S)Ks[5`I95o:$=`8Z>b*![,fBX>c)2RiW`9@0etXPW)a/ob.ae=h2X:L)4gTaU:5@C)n&cs&3Rn0>1dnUoc\Z98>sLV4nT*l'&rUW;*b%sKgWNNA[npDe6ghO'foh%#*\[VZYafg^%LmZ<*\1(PAm&E^SG/5qp@_CtFCK6O?BG:JFaX;LQf?(psIddXi*2t@h;pTT&EX_81jNfR>ZBP1AnODp8iHX=QUMs)i-fZ$Uq@3'SJKjWph:%fl0%p"m\Gn.<`d]ut<4!0010(E2eD5/X(Ek.!`37h!l,*);OGMj%)#G&2WPP[>o_"D.;7m>qtA,C5rD>_!s,+65APrd4!,H`YJF8YVIANV*D6esAi=#(B@[YXY_$'EAj1=NKZ7lGBM@Qk`e$X"H\J^.u413>0Xr;[d^-`U].9aod>g"j>J<$c!E@Z5:-,"\TSi2BTUEJ>2A&H[S&VEu<Cn52Zd>F`Qk1CP%W&eO/Q]Iu-Q65363d;1(eD%gfB]T)O3i[k4_<(RPR66Wj'bn6ldVR"j71cu3KiZ]\>0+[_=ie@L&b4Ds&OJmdC$#Bql0K15gGe>BDg4-O,U\S,PE]<"@1?8kYff.)Qb^$1m"s[Fo)'bg!DTAVSATia>8eN9AN?fu?kQ$rG5%M_Ck3>eX,6ako+JJ65BSn:WQ9)l!Y=mM^S^1rTH6[K)k)O<5,N5UtCu9]+U_+^'-.I#3q=951OcR\pOa3ZB`X7K"_iVIY2B+F9VD9/VDgJJKfi5m-e8](Zr,^/TXI/=]QQFQMUgoMkanH]AV,]!e>XfrTJHL`"+CI>^H&Uk`*"-$>)G#'Mkc0`K/jpKBrWTf2LkCGpfjXa9p?d-90![U:CLhm5A9#X#^.*9]M%-%n`p2P+P<4_G>V>SpQA>d9+[hZhR]0@:V4fVQYr(5b6\<AiH7./R<aa#Bbk(dsn@@2o+o"g/dTGeU4'ANRn[d$nSDgu/QY1/'=fdN25-+pRmDGE-^YlFt]`5"incG=s2D]sNlO+]`0DK2mjtWL?5b(<%iH-iY<d&T1W*9\cfaU</1p+T/aZ$bfpPHHNlN[&WdqI4C5eNKAeKe&g__`1+Q>fC!(I+DRn%Sl7qsG.@1k1PunK*hKaNti&$NX>1-L\1&i!6cliafBA2BUhEiaDe9LfMT$fpnSdn>*\!du.5kZuC?p]u>fXr^SVBH](#iZ\Q6nA$L>Pf1iB+RP<o;>,"?C<BI]LE]@Jt>a=;>TB;]E,pS34K^JddZS->:Q1HAcmd!2UN$\T<m8+%]oKXk9RZu>@a$]D!oY!XN913p+NDRd;V!TUPf&7>b+<>6cbi*"KVf7Rl=NZQn;,3O%$GRaH(0ulcDr?mCb!b>>$bq<q.PcnL8Mn%9pN4)r3B^n&cXhi.gLgn^?/Wuj7?"Pa:V<CSMeaSCP_\B&(:VFEoqa#Jn#\.'"O$Rr"1L)t)5)#7HL!W039X0rb2cq7*niQY4D:!EKo'0[RWJ@=JJYOJ%2i.l:1Eg&0Lib*FaEoZa2GqN6W\MRfL)#P^o#HMQfJ>$?N]?4;Z~>endstream
|
||||
endobj
|
||||
32 0 obj
|
||||
<<
|
||||
/Filter [ /ASCII85Decode /FlateDecode ] /Length 644
|
||||
>>
|
||||
stream
|
||||
GasIc9lldX&A@C2m,XCAC^#4eP)l#FRmUiB:418S^tMg?&-\o$k1<[B%)0XtbolE&gUG@10hC;5j0u)j*k1Y.Pl]pLh#W^=_Y-'=fok6G:!ARF9QN?:Elq=_@NM3o@6.o&lm`U96anEPOWYl'iYlPUo:.9FZY%DnWKJEEJYZa&;FV=fjHkZ`F%S2tN29JZQ&SZtQIAeCFXD+JoSGDK0I'G0#omA1=6ed+=i?K"<_8IN)&Iq9f-u4FS)mb8"52L#08C:e)=8348%0/3-FY4I[Tc\T):(R+dHiiR)K(&3fK`lfj1b,'9a,-i9qZ^7I>.(e#6N9iO\-c$JWEs%VG\n_a"i_Tm4ViuojA.,(8*O3gDHkD5X88O*EM^Y2Buklfo!XfMe0Z6hFTbq.0LT*X<G1iHb$8,H%a<C"1EunZaG(h]<3M[RYb9DEo1=JRs9;PcI^uYW-r+%XagpJ0SREK!\jfmp;c+Glfpg`>]>.Xs+qc'f_+)[)qauEf<$hT*k\PX><uam]n4PMH:QirKYmTOqE-rDT&odDruCC+gH]UBH,'c7o8[e*cEDRGKCsjan*"p07`.r3j4o!;^4h'=O20'#B5X:7;n484Z#;&g+,m<\VqKdS%W7l81#H7"r?=N+"1W=U#Q~>endstream
|
||||
endobj
|
||||
xref
|
||||
0 33
|
||||
0000000000 65535 f
|
||||
0000000061 00000 n
|
||||
0000000142 00000 n
|
||||
0000000249 00000 n
|
||||
0000000361 00000 n
|
||||
0000000466 00000 n
|
||||
0000000543 00000 n
|
||||
0000000626 00000 n
|
||||
0000000831 00000 n
|
||||
0000000946 00000 n
|
||||
0000001151 00000 n
|
||||
0000001357 00000 n
|
||||
0000001563 00000 n
|
||||
0000001769 00000 n
|
||||
0000001975 00000 n
|
||||
0000002181 00000 n
|
||||
0000002387 00000 n
|
||||
0000002593 00000 n
|
||||
0000002799 00000 n
|
||||
0000003005 00000 n
|
||||
0000003075 00000 n
|
||||
0000003367 00000 n
|
||||
0000003500 00000 n
|
||||
0000005658 00000 n
|
||||
0000008150 00000 n
|
||||
0000010284 00000 n
|
||||
0000012956 00000 n
|
||||
0000015567 00000 n
|
||||
0000018099 00000 n
|
||||
0000020228 00000 n
|
||||
0000021944 00000 n
|
||||
0000023128 00000 n
|
||||
0000024919 00000 n
|
||||
trailer
|
||||
<<
|
||||
/ID
|
||||
[<ba0bcbc3b3271fcc4856f7b379add7e5><ba0bcbc3b3271fcc4856f7b379add7e5>]
|
||||
% ReportLab generated PDF document -- digest (opensource)
|
||||
|
||||
/Info 20 0 R
|
||||
/Root 19 0 R
|
||||
/Size 33
|
||||
>>
|
||||
startxref
|
||||
25654
|
||||
%%EOF
|
||||
@@ -1,455 +1,166 @@
|
||||
# Les 13 — Docenttekst (Autocue)
|
||||
# Les 14 — Docenttekst (normaal)
|
||||
|
||||
**Online les — letterlijk voorleesbaar. Slide-nummers staan tussen `[SLIDE N]` zodat je weet wanneer door te klikken.**
|
||||
|
||||
**Lesvorm:** online, demo-driven. Cursor + browser side-by-side delen.
|
||||
**Duur:** 180 minuten met 1 pauze van 15 minuten.
|
||||
|
||||
> **Voor de les klaarzetten:**
|
||||
> - Cursor open in lege werkmap
|
||||
> - Browser tabs open: github.com, vercel.com, pokeapi.co
|
||||
> - Terminal klaar
|
||||
> - Cursor Pro/Business actief (voor Background Agent)
|
||||
> - GitHub + Vercel ingelogd
|
||||
> - Webcam check + scherm delen ingericht
|
||||
**Online les via Teams. Voor uitgebreide voorbereiding + context per blok.**
|
||||
**Voor letterlijk voorleesbaar script: zie `Les14-Docenttekst-Autocue.md`.**
|
||||
|
||||
---
|
||||
|
||||
## BLOK 1 — Welkom + Terugblik (10 min)
|
||||
## Lesvorm en flow
|
||||
|
||||
`[SLIDE 1]`
|
||||
| Onderdeel | Indeling |
|
||||
|-----------|----------|
|
||||
| Theorie + demo | ±1 uur klassikaal (live demo via screenshare) |
|
||||
| Pauze | 15 minuten |
|
||||
| Nu jullie | ±1,5 uur zelf doen met begeleiding via chat |
|
||||
|
||||
Welkom bij Les 13. Vandaag is een belangrijke les, want we verlaten voor het eerst je laptop.
|
||||
**Setup tijdens de les:**
|
||||
- Monitor 1 (privé): autocue-document fullscreen
|
||||
- Monitor 2 (gedeeld in Teams): Cursor + browser + terminal + slides naast elkaar
|
||||
|
||||
Tot nu toe draaiden al jullie projecten op `localhost:3000`. Dat is prima om te leren, maar in de praktijk wil je dat anderen je werk kunnen zien. Klanten. Collega's. Werkgevers. Vandaag zetten we die stap.
|
||||
|
||||
We gaan drie dingen leren die je voor elk professioneel project nodig hebt. Eén: data ophalen van externe websites en APIs. Twee: je code automatisch online zetten via Vercel. Drie: efficiënt werken met Cursor agents.
|
||||
|
||||
Aan het einde van deze les heb je een live URL die je aan iedereen kunt delen.
|
||||
|
||||
`[SLIDE 2]`
|
||||
|
||||
Even kort terug. In de afgelopen lessen hebben we gewerkt aan AI-features. Les 11 was de Vercel AI SDK, Les 12 was Tool Calling. Allemaal mooi, maar steeds lokaal.
|
||||
|
||||
Vandaag stappen we even uit dat AI-pad. Geen LLM's. Geen API keys voor AI. We gaan terug naar de basis: hoe ziet een professionele Next.js-workflow eruit van localhost tot productie?
|
||||
|
||||
We gebruiken een eenvoudige Pokédex als demo-app. Waarom Pokémon? Omdat de PokéAPI gratis is, geen API key vraagt, en je krijgt direct visuele resultaten. Perfect om je te focussen op de workflow, niet op de data.
|
||||
|
||||
`[SLIDE 3]`
|
||||
|
||||
Dit is de planning voor vandaag. Honderdtachtig minuten.
|
||||
|
||||
We beginnen met theorie. Drie blokken van een kwartier: externe APIs in Next.js, Vercel met preview-omgevingen, en Cursor agents.
|
||||
|
||||
Dan vier live demo's. In demo één bouwen we de Pokédex met Composer. In demo twee deployen we naar Vercel en zetten we environment variables in. Na de pauze: demo drie met de Background Agent om een feature parallel te bouwen. Demo vier: GitHub Actions om CI in te richten.
|
||||
|
||||
Na elke demo bouw je het zelf na in je eigen repo. Vragen tussendoor altijd welkom — typ ze in de chat.
|
||||
Tip voor de online-les: zet je Teams-deelvenster zo in dat slides en code allebei zichtbaar zijn. Cursor + terminal samen, slides klein in een hoek.
|
||||
|
||||
---
|
||||
|
||||
## BLOK 2 — Theorie: Externe APIs (15 min)
|
||||
## Voorbereiding — week vooraf
|
||||
|
||||
`[SLIDE 4]`
|
||||
**Accounts checken (studenten allemaal):**
|
||||
- GitHub-account met `gh` CLI ingelogd (`gh auth login`)
|
||||
- Vercel-account met CLI: `npm i -g vercel && vercel login`
|
||||
- Cursor geïnstalleerd, Pro of Business plan actief, ingelogd
|
||||
|
||||
Wat is een externe API. In simpele woorden: andere mensen of bedrijven hebben data of functionaliteit, en die stellen ze beschikbaar via HTTP. Jij doet een fetch-request, krijgt JSON terug, en kunt daar dingen mee doen.
|
||||
|
||||
Een paar voorbeelden van APIs die je in projecten zult tegenkomen.
|
||||
|
||||
PokéAPI — onze demo van vandaag. Pokemon-data, geen key nodig.
|
||||
|
||||
Open-Meteo — gratis weerservice, ook geen key.
|
||||
|
||||
GitHub API — informatie over repositories, gebruikers, issues.
|
||||
|
||||
Tavily — web search. Die heeft wél een API key nodig.
|
||||
|
||||
Stripe — betalingen. Twee keys: een publishable en een secret.
|
||||
|
||||
Voor APIs met een key onthoud je één regel: nooit, ooit, in je code committen. Altijd in environment variables. Hier komen we straks bij Vercel uitgebreid op terug.
|
||||
|
||||
`[SLIDE 5]`
|
||||
|
||||
Dit is misschien wel de belangrijkste slide van vandaag. Drie manieren om een API te fetchen in Next.js. Static, server, of client. Welke je kiest hangt af van je use case.
|
||||
|
||||
Eerste optie: **static**. De fetch gebeurt tijdens `next build`. Eén keer, en daarna is het HTML. Snel, want geen server-werk per request. Goed voor data die zelden verandert: een blog-post, een marketing-pagina, een lijst van producten.
|
||||
|
||||
Tweede optie: **server, oftewel dynamic**. De fetch gebeurt bij elke request opnieuw. Altijd vers, maar trager. Goed voor data die per gebruiker anders is, of die altijd up-to-date moet zijn.
|
||||
|
||||
Derde optie: **ISR**, Incremental Static Regeneration. Een mix van de twee. Wordt gecached zoals static, maar elke X seconden mag Next.js de cache vervangen. Het beste van beide werelden. Voor onze Pokédex de juiste keuze voor de detail-pagina's: snel, maar elk uur kijkt Next.js of de PokéAPI iets nieuws heeft.
|
||||
|
||||
Vierde optie: **client-side fetching**, met useEffect. Dit doe je alleen als de data live moet, of als je user-interactie nodig hebt. Belangrijk: bij client-side staat je API key in de browser. Dus alleen voor publieke APIs of voor data die geen geheim is.
|
||||
|
||||
Kijk naar het code-blokje onderaan. Het verschil tussen die drie server-modes is letterlijk één optie in je fetch-call. Verder verandert je code helemaal niet. Dat is de kracht van Next.js: je kiest het render-gedrag per fetch.
|
||||
|
||||
Vandaag gebruiken we static voor de Pokédex-lijst en ISR voor de detail-pages.
|
||||
**Communicatie:** stuur een Slack-bericht een dag vooraf met deze checklist. Als iemand morgen vastloopt op de installatie ben je een kwartier kwijt.
|
||||
|
||||
---
|
||||
|
||||
## BLOK 3 — Theorie: Vercel + Cursor + GitHub (15 min)
|
||||
## Voorbereiding — dag van de les
|
||||
|
||||
`[SLIDE 6]`
|
||||
**30 minuten vooraf:**
|
||||
- Cursor open in een nieuwe lege folder voor de demo
|
||||
- Browser tabs: github.com (logged in), vercel.com/dashboard, vercel.com/new
|
||||
- Twee terminals klaar (één in de demo-folder, één voor ad-hoc commando's)
|
||||
- Slides in een aparte fullscreen of klein in een hoek
|
||||
- Autocue-document op tweede monitor
|
||||
|
||||
Korte sidetrack — straks komen we terug op Vercel. Even Cursor: twee soorten agents.
|
||||
**Backup scenario:**
|
||||
- Een eerder voltooide demo-folder klaar voor het geval live demo vastloopt
|
||||
- Screenshots van Vercel preview-URL's voor het geval Vercel traag deploy't
|
||||
- Pre-gemaakte `.cursor/rules/general.mdc` en `AGENTS.md` om snel te kunnen plakken
|
||||
|
||||
Composer is de agent die je al kent. Cmd+I openen, prompt typen, hij maakt aanpassingen in je editor. Synchroon. Jij wacht, hij werkt, jij kijkt, je accepteert de diff. Pair programming.
|
||||
|
||||
Background Agent is anders. Die draait in Cursor's cloud. Je geeft een opdracht, hij gaat aan de slag, hij maakt een branch, hij pusht naar GitHub, hij opent een Pull Request. Allemaal terwijl jij iets anders doet. Asynchroon dus.
|
||||
|
||||
Het verschil komt straks duidelijk in de demo. Voor nu: onthoud dat je twee modes hebt. Eén voor naast je, één voor namens je.
|
||||
|
||||
`[SLIDE 7]`
|
||||
|
||||
Nu Vercel. Dit is het hart van de les. Drie omgevingen, drie scopes — en het is cruciaal dat je dit begrijpt.
|
||||
|
||||
Productie is de live versie. Wordt gedeployed elke keer dat je naar de `main` branch pusht. URL is `je-app.vercel.app`. Dit ziet iedereen.
|
||||
|
||||
Preview is wat je krijgt voor élke andere branch. Push naar `feature/search` en je krijgt automatisch een URL terug. Iets als `je-app-git-feature-search-jouwnaam.vercel.app`. Ook publiek bereikbaar, maar alleen mensen met de link.
|
||||
|
||||
Development is je lokale `pnpm dev` op localhost. Alleen jij ziet dat.
|
||||
|
||||
Nu de belangrijke vraag: hoe ga je daar mee om als je verschillende databases, of verschillende API keys hebt per omgeving? Want dat wil je. Je wilt absoluut niet dat een experimentele preview-branch in je productie-database gaat schrijven.
|
||||
|
||||
Bekijk de tabel met environment variables. In Vercel ga je naar Project, Settings, Environment Variables. En daar zet je per variabele drie keer een waarde. Voor productie. Voor preview. En voor development.
|
||||
|
||||
Een paar voorbeelden. Je `DATABASE_URL` is voor productie je echte database. Voor preview een staging-database. Voor development je lokale Postgres. Drie totaal verschillende waarden, één variabele-naam.
|
||||
|
||||
Je `OPENAI_API_KEY` is in productie de echte key met hoge limit. In preview een test-key zodat experimenten je niet failliet maken. In development je persoonlijke key.
|
||||
|
||||
Voor Stripe heel concreet: productie is `sk_live_...`, preview en development zijn allebei `sk_test_...`. Een experiment dat per ongeluk een betaling doet komt nooit op een echte creditcard.
|
||||
|
||||
Onderaan staat één killer-combo: Cursor Background Agent maakt een Pull Request, Vercel ziet die PR en bouwt automatisch een preview URL, die URL komt automatisch in een comment op de PR. Reviewer klikt, opent in de browser, test live. Geen lokale setup nodig. Dit is voor mij persoonlijk een van de redenen dat ik altijd met Vercel werk.
|
||||
|
||||
`[SLIDE 8]`
|
||||
|
||||
GitHub Actions, snel. Waarom: voordat je iets merget, weet je zeker dat de code lint en bouwt op een schone machine. Niet "het werkt op mijn laptop", maar "het werkt op een verse ubuntu". Belangrijk omdat Background Agents soms lint-errors maken — CI vangt dat op.
|
||||
|
||||
Kijk naar het code-blok. Een minimale workflow. Bij elke pull request en bij elke push naar main: checkout, pnpm install, pnpm lint, pnpm build. Duurt ongeveer twee minuten op een kleine app.
|
||||
|
||||
In demo vier gaan we dit bouwen.
|
||||
**Copy-paste-klaar materiaal:**
|
||||
- Repo-naam (`mijn-portfolio` of vergelijkbaar)
|
||||
- Twee feature-prompts (zie autocue Bijlage C)
|
||||
- `.cursor/rules/general.mdc` inhoud (zie autocue Bijlage A)
|
||||
- `AGENTS.md` inhoud (zie autocue Bijlage B)
|
||||
|
||||
---
|
||||
|
||||
## BLOK 4 — Wat we vandaag bouwen (5 min)
|
||||
## Per blok — wat je extra wilt zeggen
|
||||
|
||||
`[SLIDE 9]`
|
||||
### Blok 1 — Welkom + Vandaag (slides 1–2)
|
||||
**Doel:** verwachtingen zetten — het is een live demo van begin tot eind.
|
||||
|
||||
Dit is wat we vandaag bouwen. Een Pokédex.
|
||||
Maak duidelijk dat het anders is dan eerdere lessen: je bouwt een hele cyclus voor hun ogen op. Vraag tussendoor of het tempo klopt — online is het lastig om non-verbale signalen te lezen. Laat ze hun hand opsteken (Teams reactie) als iets onduidelijk is.
|
||||
|
||||
De stack: Next.js 16 met TypeScript en Tailwind. Geen Shadcn vandaag, we houden het simpel. PokéAPI als data-bron, geen API key. Cursor voor de code. GitHub voor de versie-controle. Vercel voor de deploys.
|
||||
### Blok 2 — Scaffold + Git + Vercel (slides 3–7)
|
||||
**Doel:** in 15 minuten van leeg naar live productie-URL.
|
||||
|
||||
Vier features die we bouwen. Een lijst met de eerste 151 Pokémon. Detail-pages per Pokémon. Een zoek-functie. Type-filter chips.
|
||||
**Slide 3 — Scaffold:** ga niet door de `create-next-app` prompts heen alsof het triviaal is. Studenten hebben dit vaak nog nooit gedaan. Beschrijf elke keuze kort:
|
||||
- TypeScript: ja, want strict typing
|
||||
- ESLint: ja, standaard
|
||||
- Tailwind: ja, we gebruiken het hele jaar
|
||||
- src/ dir: ja, het houdt root schoon
|
||||
- App Router: ja, dat is de moderne flow
|
||||
- Turbopack: ja als default, geeft betere DX
|
||||
|
||||
De eerste twee bouwen we met Composer, op de `main` branch. De andere twee met de Background Agent op feature-branches.
|
||||
**Slide 4 — Git:** veelgemaakte fout is `git push` zonder eerst remote add. Laat zien wat de foutmelding eruitziet en hoe je het oplost. Wijs ook expliciet op `.gitignore` — je toont dat `node_modules` en `.env.local` NIET in de eerste commit zitten.
|
||||
|
||||
Aan het einde heb je: een productie-URL, twee preview-URL's, drie Pull Requests, een werkende CI-pipeline, en een geverifieerd begrip van static versus ISR.
|
||||
**Slide 5 — Vercel:** wacht écht de 45 seconden af. Gebruik die tijd om uit te leggen wat Vercel doet onder de motorkap (clone, install, build, deploy). Het wachten levert spanning op — gebruik het didactisch.
|
||||
|
||||
**Slide 6/7 — URLs en env vars:** dit is nog theorie, want we hebben nog geen secrets. Belofte aan studenten: "zo direct ga ik dit ook live gebruiken met Vercel CLI."
|
||||
|
||||
### Blok 3 — Vercel CLI (slides 8–10)
|
||||
**Doel:** student begrijpt het verschil tussen dashboard en CLI, en ziet één concrete workflow (`link` + `env pull`).
|
||||
|
||||
Belangrijk: maak duidelijk dat we GEEN env vars echt hebben — we tonen de syntax. Als je het wilt demonstreren met een echte var, voeg er één toe (`MIJN_NAAM=Tim`) en pull die naar lokaal, om te laten zien dat de file gevuld wordt.
|
||||
|
||||
`vercel env pull` is een eye-opener voor veel studenten. Het scheelt je manuelle gepruts elke keer als productie-vars wijzigen.
|
||||
|
||||
### Blok 4 — Cursor in detail (slides 11–17)
|
||||
**Doel:** student kent de hoofdfeatures en weet wanneer welke te gebruiken.
|
||||
|
||||
Deze 7 slides zijn de inhoudelijke kern. Niet door doorheen jakkeren. Geef per modus een micro-demo:
|
||||
|
||||
- **Slide 12 (modi):** open de chat met Cmd+L, sluit met Esc. Druk Cmd+K op een regel in code en typ "rename this variable". Toggle Plan-mode in de chat — laat zien hoe de UI verandert.
|
||||
- **Slide 13 (@-mentions):** typ `@` in chat, laat het autocomplete-menu zien. Probeer `@web:Next.js 16 release notes` voor een live web-search demo.
|
||||
- **Slide 14 (Cursor Rules):** maak live een `.cursor/rules/general.mdc` aan. Zet er één duidelijke regel in (bv. "altijd functional components, nooit class components"). Test daarna in een nieuwe chat dat de AI die regel volgt.
|
||||
- **Slide 15 (AGENTS.md):** plak je voorbereide voorbeeld. Leg uit waarom ook Claude Code en Copilot dit lezen.
|
||||
- **Slide 16 (Background agents):** demo de feature-2-prompt al hier even kort, om visueel te maken hoe een agent in een aparte tab opent.
|
||||
|
||||
Vraag aan het eind van dit blok: "welke modus zou je gebruiken voor X?" — twee of drie scenario's. Kort, voor engagement.
|
||||
|
||||
### Blok 5 — Live feature-workflow (slides 18–22)
|
||||
**Doel:** de hele cyclus laten zien — niet alleen Cursor, niet alleen Vercel, maar de combo.
|
||||
|
||||
**Slide 19 — Demo 1:** plan + apply. Maak het visueel: open de chat, plak de feature-1-prompt, wacht tot het plan verschijnt, lees het door, klik Apply. Dan met Cmd+K één tweak. Commit.
|
||||
|
||||
**Slide 20 — Push + preview:** push, switch naar browser tab Vercel, wacht op build, open preview-URL. Gebruik de wachttijd om te zeggen: "kijk, vanaf hier ben je niet meer alleen — je kunt deze URL nu delen met collega's of klanten zonder dat zij iets hoeven te installeren."
|
||||
|
||||
**Slide 21 — Demo 2 background agent:** dit is de WOW-moment van de les. Plak de feature-2-prompt, klik "open in background", en demonstreer LETTERLIJK dat je in main editor blijft werken. Doe een Cmd+K in een ander bestand. Dat illustreert het parallel-werken.
|
||||
|
||||
**Slide 22 — Merge:** open beide PR's op github.com. Toon de Vercel-bot comment. Merge één naar `main`. Switch terug naar Vercel dashboard om de productie-deploy te zien starten.
|
||||
|
||||
### Blok 6 — Recap + Nu jullie (slides 23–25)
|
||||
**Doel:** overgang naar zelf-doen.
|
||||
|
||||
Voor de pauze: vraag of er nog iets onduidelijk was uit de demo. In een online setting moet je actief uitlokken — studenten typen vaak niet uit zichzelf.
|
||||
|
||||
Na de pauze: laat een gedeelde chat open zodat studenten kunnen tonen waar ze vastlopen. Wees actief in de chat. Loop om de 15 minuten een check-in: "hoe ver iedereen?"
|
||||
|
||||
---
|
||||
|
||||
## BLOK 5 — LIVE DEMO 1: Pokédex met Composer (30 min)
|
||||
## Veelvoorkomende vragen + antwoorden
|
||||
|
||||
`[SLIDE 10]`
|
||||
**"Waarom Cursor en niet gewoon VS Code met GitHub Copilot?"**
|
||||
Beide werken. Cursor's voordeel: tighter integratie van plan/build/agent flows en background agents. VS Code + Copilot is solid voor inline-completion. Voor agent-workflows is Cursor (of Claude Code) doorgaans soepeler. Test allebei en kies.
|
||||
|
||||
Tijd voor de eerste demo. Open Cursor, lege werkmap, terminal openen.
|
||||
**"Wat kost Cursor?"**
|
||||
Hobby tier is gratis met limits. Pro is rond €20/maand. Voor de cursus genoeg.
|
||||
|
||||
Eerste commando, typ mee:
|
||||
**"Kan ik mijn API-key in Cursor gebruiken in plaats van hun subscription?"**
|
||||
Ja — Settings → Models. Voeg je OpenAI/Anthropic key toe. Goedkoper bij weinig gebruik, duurder bij veel.
|
||||
|
||||
```bash
|
||||
pnpm create next-app pokedex --typescript --tailwind --app
|
||||
```
|
||||
**"Wat als ik een typo maak in `.cursor/rules/`?"**
|
||||
Cursor parseert de YAML front-matter, gooit een waarschuwing bij syntax errors. Open de file en kijk linksonder voor de error.
|
||||
|
||||
Beantwoord de vragen: geen src directory, gebruik Turbopack, default alias is prima.
|
||||
**"Zal Cursor mijn code naar OpenAI sturen?"**
|
||||
Hangt af van je settings. Cursor heeft "Privacy mode" — code wordt niet bewaard door OpenAI of Anthropic. Check Settings → Privacy.
|
||||
|
||||
Stap in de map: `cd pokedex` en open in Cursor met `cursor .` of File → Open Folder.
|
||||
**"Hoe kies ik tussen Plan en Build?"**
|
||||
Plan voor onbekende terrein of grote features. Build als je precies weet wat je wilt en het kleiner is. Bij twijfel: Plan, dan klik je Apply als het bevalt.
|
||||
|
||||
Nu het magische moment. Druk op Cmd+I — of Ctrl+I op Windows — voor Composer. Type deze prompt letterlijk over:
|
||||
**"Werkt Vercel CLI ook voor andere frameworks?"**
|
||||
Ja — Astro, SvelteKit, Vue, Remix, alles wat Vercel ondersteunt. De link- en env-flow is identiek.
|
||||
|
||||
> Bouw een lijst-pagina met de eerste 151 Pokémon van PokéAPI. URL is `https://pokeapi.co/api/v2/pokemon?limit=151`. Gebruik Tailwind cards in een grid met de naam en sprite van elke Pokémon. De sprite staat op `https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/{id}.png` waar {id} het index-nummer is. Gebruik een static fetch — dus géén revalidate optie meegeven.
|
||||
|
||||
Composer gaat aan de slag. Je ziet diffs verschijnen in `app/page.tsx`. Lees ze met me mee. Accepteer alle wijzigingen.
|
||||
|
||||
Start lokaal:
|
||||
|
||||
```bash
|
||||
pnpm dev
|
||||
```
|
||||
|
||||
Open `localhost:3000`. Daar zou nu een grid van 151 Pokémon moeten staan. Werkt het? Mooi.
|
||||
|
||||
Tweede prompt in Composer:
|
||||
|
||||
> Voeg een detail-pagina toe op `/pokemon/[id]`. Fetch van `https://pokeapi.co/api/v2/pokemon/{id}`. Toon de official artwork — die staat in `sprites.other["official-artwork"].front_default`. Toon ook de types als chips. Gebruik ISR met `revalidate: 3600`. Zorg dat de cards op de homepage linken naar `/pokemon/[id]`.
|
||||
|
||||
Wacht op de diffs. Accepteer ze. Open een random Pokémon. Werkt? Goed.
|
||||
|
||||
Belangrijke uitleg terwijl studenten kijken:
|
||||
|
||||
Kijk naar het verschil tussen `app/page.tsx` en `app/pokemon/[id]/page.tsx`. De homepage gebruikt gewoon `fetch(URL)`. De detail-pagina gebruikt `fetch(URL, { next: { revalidate: 3600 } })`. Dat is het hele verschil tussen static en ISR. Eén optie.
|
||||
|
||||
Open de browser Network tab — F12. Klik door wat detail-pages. Je ziet dat er geen calls vanuit de browser naar PokéAPI gaan. Alles is server-side. Dat betekent: als PokéAPI morgen omvalt, blijft jouw site werken tot de revalidate triggert.
|
||||
|
||||
Optioneel — en als de tijd het toelaat — voeg een derde route toe met `cache: "no-store"`. Bijvoorbeeld een "random pokemon" pagina. Zo zien studenten alle drie de modes naast elkaar.
|
||||
|
||||
Commit alles in de terminal:
|
||||
|
||||
```bash
|
||||
git add . && git commit -m "Initial Pokédex met Composer"
|
||||
```
|
||||
**"Wat als ik géén Vercel wil gebruiken?"**
|
||||
Alternatieven: Netlify (vergelijkbare workflow), Cloudflare Pages (goedkoper bij schaal), zelf hosten op een Hetzner box met Coolify (zoals Tim doet). De Cursor-workflow blijft hetzelfde — alleen de deploy-step verandert.
|
||||
|
||||
---
|
||||
|
||||
## BLOK 6 — LIVE DEMO 2: Vercel deploy + env vars (20 min)
|
||||
## Foutmodi tijdens de les
|
||||
|
||||
`[SLIDE 11]`
|
||||
|
||||
Tijd om dit live te zetten.
|
||||
|
||||
Open github.com in een nieuwe tab. Klik "New repository". Naam `pokedex`. Public. Geen README, geen gitignore, geen license — die hebben we al lokaal.
|
||||
|
||||
Klik Create. GitHub laat een URL zien. Kopieer die.
|
||||
|
||||
Terug in de terminal:
|
||||
|
||||
```bash
|
||||
git branch -M main
|
||||
git remote add origin https://github.com/JOUWNAAM/pokedex.git
|
||||
git push -u origin main
|
||||
```
|
||||
|
||||
Refresh github.com. Daar staat je code.
|
||||
|
||||
Open vercel.com. Log in met GitHub. Klik "Add New" en kies "Project". Vercel toont een lijst met je repositories — kies `pokedex`. Klik "Deploy".
|
||||
|
||||
Wacht zestig tot negentig seconden. Je ziet de build logs voorbij komen. Aan het einde: live URL. Klik. Daar staat je Pokédex op het internet. Live.
|
||||
|
||||
Nu het belangrijkste stuk: environment variables.
|
||||
|
||||
Open `app/page.tsx` lokaal in Cursor. Voeg ergens bovenaan in de return een regel toe:
|
||||
|
||||
```tsx
|
||||
<p className="mb-4 text-sm text-zinc-500">
|
||||
Omgeving: {process.env.NEXT_PUBLIC_APP_ENV ?? "onbekend"}
|
||||
</p>
|
||||
```
|
||||
|
||||
Push naar main:
|
||||
|
||||
```bash
|
||||
git add . && git commit -m "Toon huidige omgeving"
|
||||
git push
|
||||
```
|
||||
|
||||
Vercel deployt automatisch opnieuw. Maar de regel toont "onbekend" — omdat we de variabele nog niet hebben ingesteld.
|
||||
|
||||
Ga in Vercel naar je project → Settings → Environment Variables. Klik "Add new".
|
||||
|
||||
Variable name: `NEXT_PUBLIC_APP_ENV`. Value: `production`. **Belangrijk:** vink ALLEEN Production aan. Klik Save.
|
||||
|
||||
Tweede keer toevoegen. Zelfde naam, value `preview`, en vink ALLEEN Preview aan. Save.
|
||||
|
||||
Ga naar de Deployments tab en redeploy de laatste deploy. Of push een kleine wijziging om automatisch te triggeren. Wacht. Open productie-URL — staat nu "production".
|
||||
|
||||
Nu een feature-branch. Terug in Cursor terminal:
|
||||
|
||||
```bash
|
||||
git checkout -b chore/banner
|
||||
# kleine wijziging — bv een spatie ergens
|
||||
git add . && git commit -m "Banner aanpassing"
|
||||
git push -u origin chore/banner
|
||||
```
|
||||
|
||||
Open de Vercel deployments tab. Daar verschijnt een nieuwe deployment voor `chore/banner`. Klik de preview URL. Daar zie je "preview" staan, niet "production". Twee URL's, twee waarden, één variabele.
|
||||
|
||||
Voor `.env.local` zou je `NEXT_PUBLIC_APP_ENV=development` zetten — dat doen ze in het huiswerk.
|
||||
|
||||
Belangrijke regel: voor secrets — echte API keys, database passwords — vink je in Vercel **alleen** Production aan. In Preview gebruik je een aparte test-key. Dit voorkomt dat experimenten je productie-data raken.
|
||||
|
||||
---
|
||||
|
||||
## BLOK 7 — Pauze (15 min)
|
||||
|
||||
`[SLIDE 12]`
|
||||
|
||||
Vijftien minuten pauze. Ik mute mijn microfoon en zet de camera uit. Tot zo.
|
||||
|
||||
---
|
||||
|
||||
## BLOK 8 — LIVE DEMO 3: Background Agent + preview (25 min)
|
||||
|
||||
`[SLIDE 13]`
|
||||
|
||||
Welkom terug. Nu het stuk wat veel mensen voor het eerst zien: de Cursor Background Agent.
|
||||
|
||||
Open Cursor. Druk Cmd+Shift+P voor het command palette. Type "Background Agent". Eerste keer: Cursor vraagt of je je repository wilt verbinden aan de cloud. Accepteer in de browser, autoriseer GitHub.
|
||||
|
||||
Nu kun je een Background Agent starten. Type deze prompt — letterlijk over:
|
||||
|
||||
> Maak een nieuwe branch genaamd `feature/search`. Voeg bovenaan de homepage een zoekbalk toe. Wanneer iemand typt, filter dan de Pokémon-lijst op naam. Gebruik client-side state met useState. Push de branch naar GitHub en open een Pull Request met titel "Add search bar" en een korte beschrijving in de body.
|
||||
|
||||
Klik start. De agent gaat aan de slag.
|
||||
|
||||
Wat je nu kunt doen — en dit is het hele punt: iets anders doen. Een tweede Background Agent starten met een andere taak.
|
||||
|
||||
Nieuwe Background Agent, prompt:
|
||||
|
||||
> Maak branch `feature/type-filter`. Op de homepage, naast de zoekbalk: chips om te filteren op type. Type-data moet je per Pokémon fetchen via de detail-endpoint. Push de branch en open een PR.
|
||||
|
||||
Beide agents lopen nu parallel. Klik op de eerste in het Background panel — je ziet wat hij doet. Bij vragen kan hij je stoppen voor input.
|
||||
|
||||
Wacht drie tot vijf minuten. Pull-Requests verschijnen op GitHub.
|
||||
|
||||
Open de eerste PR — "Add search bar". Onderaan zie je een comment van Vercel met de preview URL. Klik. Test de zoekbalk live in de browser. Werkt? Goed.
|
||||
|
||||
Open de code-diff in de GitHub UI. Lees mee. De agent heeft toegevoegd: een useState voor de query, een input bovenaan, een filter op de lijst. Logisch.
|
||||
|
||||
Als de code goed is, klik "Merge pull request". Push gaat naar `main`. Vercel deployt naar productie. Binnen een minuut staat de zoekbalk live.
|
||||
|
||||
Hetzelfde voor de tweede PR. Review, test op preview-URL, merge.
|
||||
|
||||
Wat hier net gebeurde: in vijf minuten zijn er twee features autonoom gebouwd door agents, getest op aparte preview-URLs, en gemerged naar productie. Zonder dat ik één regel code zelf heb geschreven.
|
||||
|
||||
---
|
||||
|
||||
## BLOK 9 — LIVE DEMO 4: GitHub Actions CI (15 min)
|
||||
|
||||
`[SLIDE 14]`
|
||||
|
||||
Laatste demo. We voegen CI toe. Lint en build moeten slagen voor elke PR.
|
||||
|
||||
Open Composer in Cursor, Cmd+I, prompt:
|
||||
|
||||
> Voeg een GitHub Actions workflow toe op `.github/workflows/ci.yml`. Bij pull request én bij push naar main: checkout, setup pnpm met `pnpm/action-setup@v4`, setup node 22 met `actions/setup-node@v4` en `cache: pnpm`, install, run lint, run build.
|
||||
|
||||
Composer maakt het bestand. Accepteer.
|
||||
|
||||
Push de wijziging:
|
||||
|
||||
```bash
|
||||
git checkout -b chore/add-ci
|
||||
git add . && git commit -m "Add GitHub Actions CI"
|
||||
git push -u origin chore/add-ci
|
||||
```
|
||||
|
||||
Open de PR op GitHub. Onder de PR zie je de "Checks" tab — de workflow draait. Wacht ongeveer twee minuten. Groene vinkjes — goed.
|
||||
|
||||
Nu de demo van een falende CI. Introduceer een typo in de code. Open `app/page.tsx` lokaal, verander een opening tag in iets fouts — bijvoorbeeld `<dvi>` in plaats van `<div>`. Commit en push naar dezelfde branch.
|
||||
|
||||
CI draait opnieuw, faalt. Rode X. Open de logs in GitHub — zie de TypeScript error.
|
||||
|
||||
Fix met Composer:
|
||||
|
||||
> Repareer de TypeScript build error in `app/page.tsx`.
|
||||
|
||||
Composer fixt. Commit, push. CI draait. Groen.
|
||||
|
||||
Laatste stap: branch protection. Ga naar GitHub repo → Settings → Branches → Add rule. Branch name pattern: `main`. Vink aan: "Require status checks to pass before merging". Selecteer de CI-workflow. Save.
|
||||
|
||||
Vanaf nu mag niemand — ook jij niet — naar main mergen zonder groene CI.
|
||||
|
||||
Merge de PR. Klaar.
|
||||
|
||||
---
|
||||
|
||||
## BLOK 10 — Composer vs Background (10 min)
|
||||
|
||||
`[SLIDE 15]`
|
||||
|
||||
Vandaag hebben we beide gebruikt. Wanneer kies je welke?
|
||||
|
||||
Composer is je pair-programmer. Je zit erbij, hij maakt aanpassingen, je accepteert per file. Goed voor: snelle wijzigingen tijdens je werk, complex refactor waar je wilt meekijken, onbekend gebied waar je niet wilt dat een agent vastloopt.
|
||||
|
||||
Background Agent is je delegate. Je geeft een welomschreven taak, gaat iets anders doen, krijgt later een PR met preview URL. Goed voor: features met heldere specs, parallel werken aan meerdere features, tijdens een meeting iets voorbereiden.
|
||||
|
||||
Mentaal model: Composer is "samen werken". Background Agent is "uitbesteden".
|
||||
|
||||
Twee waarschuwingen voor Background Agent. Een: hij kan vastlopen op rare config — debuggen is moeilijker omdat je er niet bij was. Twee: hij verbruikt credits, dus Cursor Pro of Business is wel nodig.
|
||||
|
||||
Praktische tip: te vage prompts geven matige PR's. Wees specifiek. Noem de bestanden, beschrijf de UX, geef voorbeelden van wat je wilt. Hoe meer context, hoe beter de output.
|
||||
|
||||
---
|
||||
|
||||
## BLOK 11 — Lesopdracht + Huiswerk (10 min)
|
||||
|
||||
`[SLIDE 16]`
|
||||
|
||||
Tijd voor jullie eigen werk. Twee delen.
|
||||
|
||||
**Lesopdracht — doe je nu, dertig minuten:**
|
||||
|
||||
Een. Eigen Pokédex-repo opzetten met dezelfde stappen als de demo: `pnpm create next-app`, Composer prompt voor de lijst, Composer prompt voor de detail-page.
|
||||
|
||||
Twee. Push naar GitHub.
|
||||
|
||||
Drie. Verbind met Vercel, deploy. Open de productie-URL en stuur de URL in de chat — ik wil zien dat het werkt.
|
||||
|
||||
Vier. Voeg de `NEXT_PUBLIC_APP_ENV` environment variable toe met een aparte waarde voor production en preview. Verifieer dat het werkt op beide URLs.
|
||||
|
||||
**Huiswerk — voor volgende les:**
|
||||
|
||||
A. Eén feature toevoegen met de Background Agent. Mijn voorstel: zoekbalk, type-filter, favorieten-systeem, of vergelijk-twee-pokemon. Open de PR, krijg de preview URL.
|
||||
|
||||
B. GitHub Actions CI toevoegen met lint en build.
|
||||
|
||||
C. Branch protection op `main` instellen.
|
||||
|
||||
D. Schrijf een `DEPLOY.md` in de root van je repo met: productie-URL, één preview-URL, en een reflectie van ongeveer driehonderd woorden over Composer versus Background Agent. Wanneer werkte welke beter? Geef één voorbeeld waar Background verbluffend goed of juist slecht was.
|
||||
|
||||
Bonus voor wie wil: vervang de Pokédex door een andere externe API. Open-Meteo voor weer, GitHub API voor jouw eigen repositories, of een free API naar keuze.
|
||||
|
||||
Inleveren via Teams, voor volgende les.
|
||||
|
||||
---
|
||||
|
||||
## BLOK 12 — Afsluiting (5 min)
|
||||
|
||||
`[SLIDE 17]`
|
||||
|
||||
Vandaag hebben we veel gedaan.
|
||||
|
||||
We hebben externe APIs leren fetchen vanuit Server Components. We hebben drie rendering modes gezien: static voor de lijst, ISR voor detail-pages, en kort genoemd hoe je dynamic of client-side zou doen. Dat is een knop die je per fetch omdraait.
|
||||
|
||||
We hebben Cursor Composer gebruikt voor synchrone wijzigingen, en Background Agent voor twee parallel-features. Verschillende tools voor verschillende momenten.
|
||||
|
||||
We hebben naar Vercel gedeployed. Productie op main, preview per branch. En het belangrijkste: environment variables per omgeving. Een variabele kan drie totaal verschillende waarden hebben, één per scope.
|
||||
|
||||
En tenslotte GitHub Actions CI. Zekerheid dat je code lint en bouwt voor de merge.
|
||||
|
||||
Volgende les is les 14: Agents. Nog een stap verder dan tool calling. LLM in een loop met tools, autonoom twintig tot vijftig stappen. We bouwen een research-agent from scratch met `ToolLoopAgent`, `stopWhen` en `prepareStep`. En we bespreken wanneer agents wel of niet de juiste keuze zijn.
|
||||
|
||||
Dat was 'm. Bedankt voor jullie aandacht. Vragen?
|
||||
|
||||
---
|
||||
|
||||
## VEELVOORKOMENDE FOUTEN — voor mezelf
|
||||
|
||||
| Probleem | Oplossing |
|
||||
| Probleem | Snelle fix |
|
||||
|----------|-----------|
|
||||
| Vercel deploy faalt op build | Check Build Logs — meestal type-error of missende env var |
|
||||
| Background Agent doet niks | Repo niet gekoppeld aan Cursor cloud — Cmd+Shift+P → Repository Settings |
|
||||
| Preview URL toont oude versie | Hard refresh (Cmd+Shift+R), of Vercel cache invalidate |
|
||||
| Env var komt niet door | Redeploy na toevoegen var — bestaande build pakt 'm niet automatisch |
|
||||
| `process.env.NEXT_PUBLIC_*` undefined | Moet beginnen met `NEXT_PUBLIC_` om in browser te werken |
|
||||
| GitHub Actions faalt op pnpm | `actions/setup-node` MET `cache: pnpm` — anders geen lockfile-aware install |
|
||||
| Branch protection blokkeert PR | Eerst CI groen zien voor je merge |
|
||||
| Composer 'kan repo niet vinden' | Cursor verbinden met die specifieke repo via Cmd+Shift+P |
|
||||
| `gh repo create` faalt — niet ingelogd | `gh auth login` opnieuw |
|
||||
| Vercel detecteert geen Next.js | Build directory leeg gelaten, niet `out/` typen |
|
||||
| Vercel build crasht op env var | Tijdelijk een dummy zetten in dashboard |
|
||||
| Cursor verliest context | Esc, nieuwe chat starten, opnieuw `@`-mention |
|
||||
| Background agent doet niets | Eerst zien of het Pro/Business-plan vereist |
|
||||
| `vercel link` koppelt aan verkeerd project | `rm -rf .vercel` en opnieuw |
|
||||
|
||||
---
|
||||
|
||||
## TIMING-CHECK
|
||||
## Vooruitblik Les 15
|
||||
|
||||
| Blok | Cumulatief | Duur |
|
||||
|------|-----------|------|
|
||||
| 1. Welkom + Terugblik | 0–10 | 10 min |
|
||||
| 2. Theorie Externe APIs | 10–25 | 15 min |
|
||||
| 3. Theorie Vercel + GH | 25–40 | 15 min |
|
||||
| 4. Wat we bouwen | 40–45 | 5 min |
|
||||
| 5. Demo 1 — Composer | 45–75 | 30 min |
|
||||
| 6. Demo 2 — Vercel + env | 75–95 | 20 min |
|
||||
| 7. Pauze | 95–110 | 15 min |
|
||||
| 8. Demo 3 — Background Agent | 110–135 | 25 min |
|
||||
| 9. Demo 4 — GitHub Actions | 135–150 | 15 min |
|
||||
| 10. Composer vs Background | 150–160 | 10 min |
|
||||
| 11. Lesopdracht + Huiswerk | 160–170 | 10 min |
|
||||
| 12. Afsluiting | 170–175 | 5 min |
|
||||
| Buffer voor vragen | 175–180 | 5 min |
|
||||
Volgende les: RAG met embeddings. We bouwen een PDF Q&A-app. Studenten moeten daarvoor hun Next.js-app van vandaag live hebben staan, met minstens twee PR's gemerged. Anders missen ze het fundament voor de RAG-stack.
|
||||
|
||||
Totaal: 180 minuten.
|
||||
Stuur de avond ervoor een Slack-bericht: "checklist voor Les 15 — productie-URL + 2 merged PR's — laat in chat zien als je vastzit."
|
||||
|
||||
143
Les14-Cursor-Vercel-Deploy/Les14-Docenttekst.pdf
Normal file
143
Les14-Cursor-Vercel-Deploy/Les14-Docenttekst.pdf
Normal file
@@ -0,0 +1,143 @@
|
||||
%PDF-1.4
|
||||
%<25><><EFBFBD><EFBFBD> ReportLab Generated PDF document (opensource)
|
||||
1 0 obj
|
||||
<<
|
||||
/F1 2 0 R /F2 3 0 R /F3 4 0 R /F4 8 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 /Courier /Encoding /WinAnsiEncoding /Name /F3 /Subtype /Type1 /Type /Font
|
||||
>>
|
||||
endobj
|
||||
5 0 obj
|
||||
<<
|
||||
/Contents 13 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 12 0 R /Resources <<
|
||||
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
|
||||
>> /Rotate 0 /Trans <<
|
||||
|
||||
>>
|
||||
/Type /Page
|
||||
>>
|
||||
endobj
|
||||
6 0 obj
|
||||
<<
|
||||
/Contents 14 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 12 0 R /Resources <<
|
||||
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
|
||||
>> /Rotate 0 /Trans <<
|
||||
|
||||
>>
|
||||
/Type /Page
|
||||
>>
|
||||
endobj
|
||||
7 0 obj
|
||||
<<
|
||||
/Contents 15 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 12 0 R /Resources <<
|
||||
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
|
||||
>> /Rotate 0 /Trans <<
|
||||
|
||||
>>
|
||||
/Type /Page
|
||||
>>
|
||||
endobj
|
||||
8 0 obj
|
||||
<<
|
||||
/BaseFont /Symbol /Name /F4 /Subtype /Type1 /Type /Font
|
||||
>>
|
||||
endobj
|
||||
9 0 obj
|
||||
<<
|
||||
/Contents 16 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 12 0 R /Resources <<
|
||||
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
|
||||
>> /Rotate 0 /Trans <<
|
||||
|
||||
>>
|
||||
/Type /Page
|
||||
>>
|
||||
endobj
|
||||
10 0 obj
|
||||
<<
|
||||
/PageMode /UseNone /Pages 12 0 R /Type /Catalog
|
||||
>>
|
||||
endobj
|
||||
11 0 obj
|
||||
<<
|
||||
/Author (NOVI Hogeschool Utrecht) /CreationDate (D:20260612063152+00'00') /Creator (\(unspecified\)) /Keywords () /ModDate (D:20260612063152+00'00') /Producer (ReportLab PDF Library - \(opensource\))
|
||||
/Subject (\(unspecified\)) /Title (Les 14 Docenttekst) /Trapped /False
|
||||
>>
|
||||
endobj
|
||||
12 0 obj
|
||||
<<
|
||||
/Count 4 /Kids [ 5 0 R 6 0 R 7 0 R 9 0 R ] /Type /Pages
|
||||
>>
|
||||
endobj
|
||||
13 0 obj
|
||||
<<
|
||||
/Filter [ /ASCII85Decode /FlateDecode ] /Length 1841
|
||||
>>
|
||||
stream
|
||||
Gb!;d>Bedj&:XAWR&:_?7U^G+hs<0oe!%4Sc,gVPM_p6DQ'U\:4oY,F>tJ.KM'6SS1$0h-c>C>lAekRqH8+q/B)m-cGkj,tB#AitbdC[m^Ot.uMn^oI/*YfS+5%6Y_"OnW)nE>9DHjPf#He;t'98U("tO`d*u*rCl""!S5)_9Q@06nF$/6:6]Yfo6_8NbapdrGl<u!@2$7:C1'cH1TEd7uO"cQf%BE"^I'+XicW9;Nf<Ga;9(<Djt517G&]DXB\)#TLhOX!EfA1]d"O_:Ko+-\7J(0-7@!_%^FV@->PT1,?g'uVT*n=L%g=MRn9V.KQ]3A`VucsGpp%ZF#:'O`0(@bJ^Xd$c7i3T[o(1+pMF\q(*;SH>]f5fmL'Ya;6GD-a-N7Oq.*NEIX)/I3rKof\kSVZg=P/+=WsoU9q;0Sc&qS"nhcIV[_[_UFJ,ZnCpc%@Irc;$]Op=<PgoOA>af70<?8&M*GMd!&mt]j9]Rcg%@-*tpAOea86s/lPc`K_Z1>FN+TA#o3iAm,b)lB=kkm\SfoR^Emk$DVC9q):?_XKBTg>0/-#<F&W;dm0XKMe2)j62&"_W%-kESV;u2:Q;j\[Y>VILBJELT0Lj:03A5?LJp3`XrR2,2oD/K[Z/(9m%l0:!($<PY7Rlh?)A]`Fi]4Eo@mnq?M<P'&$kbu_c*2dKVC:2^f3uX>?M.94f`"Lh`J):Jcuo\eFqD4p""PK2&Z>U#k/-[sdigDl`!UpVE,X,'AFXpTI0rO1=ESr.d[bn@O=d<rQD;&D`s.,i3Sn7GYFlMdKQ`3\:"/"Eo0qmJI@L+YgB793q@N#@GpMsff+So&8"FahjY7OVSun'OK^C8h:$&)nL1USN<?]p,a((=V)rM1G%MO05Lj<DuaD13me/&Je$c`'HiS"u`MOJ`%&:)b(FP*e^U86OF0J1eO7/_r\(KDPWq\>?YH=@1@kL?S1&dK*UNZNn_le86Z\*;]RNfV0%1h2MKAU=)IK9n'sJd"%5%!=[JJi:.J%n[(mn>c)KME=kGRS/K9'\$pYH"FSUK373c2Y:jAh?I;,6GR`AB1^U7CY]*sQn)F5cu`Q+A?ip1?Ihm4D?W55l$h9Q=j2u86!TlNn_*NZ3e>!)b?5O#)onu(X--!ej!A8M1+92lpkIGUUqR`8:1,)toi#l8lT!jiX2<05Tu(^DA5>plDaa7k4WX)?=f1HMO+$9j@'k=SmOdCXHBUr7b3S!nc!#1]gEPN^r/lj4lgXP7)>eqQN,g:UP`D5c[,n=s":V8LRg&;6&K(GmI=Fr9GKShq9r1CO.Nkb?2+82>27tRVW#=C@-';\?_FejK\@</2]ACEeFLnou4q!m-]R$E[XNj8Yao'BaOGOQ_kgKBS06t2M&mK'*2HLN_[S16j3Pbr:\3u4[ZJ\cL+G`GV">WuQZsZVQ81M%$NbAO&dTo0rajD0V7$m3lU%L&t]V%nSa)8hi3e=n[5I,Z$EfP1Lm5S=$s+.dV6>.m=EU<mY);B/o/GaNZh5K.I_?iWT=O8jK##PV<rg<18`V+!2;lWQoE80r)Jb;M$77q2V-0b35*DH0e;u>A0q-Pc27pBWFHe4:+'q`onQ&9<_:T[`1qP^;iRM[I]PKW9X:%b'D#qtI?`AIhlY3(!,[/Y8PoU2*ae',1iIJ)*f-cO3CS.Fgti6ZB;Y^sVJ4PhoNi5or@@[]>6bua2NnsaF3BBJ9hR=O$Yi%&87llD%,q`1%(^ZSpM]71*^c;G6,B]$/Y+E_upND(.nUJIWG46huNE\CqHGp1ZDU`9McF6B(H^\(<HN>c%j_n33P9'hjP>H4P%k;5K#\>D&emt[qAH!tX[i8S*;LR'I~>endstream
|
||||
endobj
|
||||
14 0 obj
|
||||
<<
|
||||
/Filter [ /ASCII85Decode /FlateDecode ] /Length 2280
|
||||
>>
|
||||
stream
|
||||
Gb!;d?#SK;&q/*0)"aZIN?sM4VZY]Z4*B!dNmYkXBcd`t.rlo;[G5?8V%rmZh_V(B@S2[9"E#1!+Ta9?B?m$83O8aonIq/e7L@JqLlQrn-jXNDTqL0*dcc;.7kajh&M.#WqQ9`gGS8@'YD9f7(Z41o]6p/D?`\EaQTKl.mtVUcX%uo-59+cleERIsSWf.=-QW#hga$!+"re3A)6WZaD/*;\K>C2!h9b@MEknmRfc8EH>.l$q#$Y_;olnaE_<7:p72`GVB?o[^X"8\[/7Lt\`j_BQc2W4%p`;)Yb@nneVL5GgC5pQ(RSqA/EHg=;]bU[(B1/FSWkMiJbJsK4R&KOJ`m#q[9$KLtH-#N:S`R^X.k7j[A]Iq^3'X"?j\2(C&kC-!WuW.t[eB7Ug[iV9'irp%]7b2T-o0$TC[l7Ve9oQMk2`]VSF)))?M71k>J0l3i#gnQ`hdn(<99^@F/)](M=R0NV9J8=0X!:,+1D2Q7eHQ'a+`&H:F*g_:0l<uQ-a[6@5npKA"0I@r59K?l7njV/SA!?qIVnqVUmICabnC5q4QB1oq^S$=tnEYPqS4AC$(Qm<T:T0YF/AK>uWMKko6Cp9Q)!Fp$X;^qnq:D(&#Po>hbYsi]T8k5*!NFW"SMd@=Pt:eEaF]?LKOO6.kIEq`QZQkCVQ`^Ktk==T(*Z6a8VP_"ra@)B1W5lS'=*6@$.Ni_ScD[*ZhA4\qt;C2Vk)mVppXTX*P8F+42%AH7L^9kRp2:g*E0RlOZg/,H2\/H,4POW,<^:''5G_aiptYq=b>D1BLA>nR+QnZq.m7iQ#UTq[t,(?b`Dn*qe8()`cdJnQ:%D]/L<EM[[mo2Jrbc4at4\%Z[%G8PcEjuoQ?'CMqrn'S6d/c_/9>Da"q0b#OOQA^GOCgt]Z?RDRjcM"ZaIdDrG0?p,o@>aIs;lg[.@EFA@/oB!+;)^!Q)SV:B@TeC/0*cbt;Fbfg;I9IbXO3?qA9i%==nPb*]QLXh36)B(ja(\3[*nUVhm71IKi5e)o'dNu?Q\N1o]E^"hAELiElqKQ?M!h_;?e0tC9b)a"E](uk.U2lV/]TtIhephH=@:,^XZZS>\V,6RleLL_'*X_G.QS"c/<0[UI<j'BIm[ObYtekHV+pG\&F&O.t[L,(u+"5)[2`b(jREh;!2o\l;g[q=[\ZMf#;c?$KVc>f!MAf9pMT`moKsF-=d?m<lE8TXiNUJ$siZQB'VAjYVG2.)!b["nQ&Q33WS93(@dFe"id-R*mHHg1Ek7#W)kT:h`4*P!Q$QRBl=%H"Q=UrN3./Bf$i-s?-4"^FQu_:e.J./oooRH#b^(O.&DtQK0f8-r#6V80g+sor9?W'V/NmtgJo"RimQ_Xoq*-XV9K(tC)J]0a2_>Vk@Jl,<JS)8S,[@DT4cP+&j!)+/00A-o9;6lCZMdBqZY?]iY(9.:$I9tEF6:*@R(V-;rZ1(d?Q*!/cJVYr2E(9ZE[&&1`$D?rZC`"'uh>HZ97YR?XAE2&&03h_o(p!7U'T0".E?$C1?E&]/1e&orZ@>iK9KiCgp="QF+_G[BK%i-ok.D3LC/9M+pqhY6:%YMMdj`X0!$nnQ(3X0XB?g9D=ZMFtCd3M!n7%Eo3VKoF9:`oF$.@NK/,VWdaUCl_)m8`\m?[\$;o&o(\Ae%mRLqhB1[Jb`b,kMH6Tl+34)`AR)$)D>iS.o9%*2\6C-YiTcRo$P4N,&!/>NnF_s@=1MUN]6_99M1Mfk]g/1+NZR:%8K]0A-7sOJ<EIZo!2jaEOiN8VV1rOk.mpn4Nj4e@#RT'Z/JHjMCp$YabK)Zd7mI7&aI"1j(YZetb6uGXQS&%?)jE8&XMW?BW-5/G(.=9uQbG!YIS#Q%a.fe8&J)Z5(q.0tci3I-Z3%$^(T)ca:gI41a&_gLdRVfST<ZQr0*T?^Lg<V2GrHJ?#L"tt#GT_,Rn2D7NP<S;MXNENGOM=Z0_>[YAf2Cl-:Onj2m"dmF\pq"]PDq>!'%[]gQ!/BD<?ZiS&o+fP=<h^A@+T&red*1Sh67Qb4l?=S5rQ:\-<^_6e0tJqN0%C%WY-m%-ru8=dCGW(AY@/BIdM$>(GPZYBJ\c@`"Ye/9k/F4eU1`hJ$D5jTN]qA[$pN,trO7-0jZ$mCYjkoop1Zd'8c1W".Cm4[6\d':r;u`feEn<K5L/\J@],jEK1U:G"Urhcgk:\\-Uq$].4caNsr<[Rq=VN\$YA@CjLdLM!gmgS6(EiI1aokK?(B:J]mJGGLE`BG*.QZ`INanEB#%UlM728I\V4G*PHKrXWCoWfm~>endstream
|
||||
endobj
|
||||
15 0 obj
|
||||
<<
|
||||
/Filter [ /ASCII85Decode /FlateDecode ] /Length 2455
|
||||
>>
|
||||
stream
|
||||
GauHLgN)&g&q'FT9^*jF6G`kMQ!P)-;B3`7N\!!VW>&\T^_0m]JRrdDO8/CGB:$)`PftVfCtUWK?uOjW4[&;6N?@&,7N'Uc&\\"j-U49OK`&")V9o`XO'_eVb$RBd&%qmeE:22Je+\U1Fr01X)[$'+>Sr`71lo&N[<XY`=T9rcC93ZD.2EZ;OsWdo9&u_t0eH*3=5I^2c"Q_o0Dl,@`tiT;T3C"lm]YJ-[e`<+OjkZJ"jLhbm;fgu2Fs)YYs8%,>@oL/=jY#&Hq4YqX#cPl-$,,rP80A1'NZfR8]jG-95W!10BrmckYtG00jNp_F/Hsi0QA+59"#M0-u#Ym/!XMRY91>?fLO`g48ajTs'I$AaE[;l?m1q?G]'JM:7Ds,`^);ObIT*A49\2b-'Y9HUXUcZ-2B_Gkkg#&lP"Hn4"VM*@uOY1W@na+=CVdKEuQQ,@j^_6MQ/;53A@h)TO95>S3^Ib]IUud/8CHAL1t;1O6LOX7mYpHc(1\N-!-66Zj(<BArIjOS/U`uX-og!5c!0blndc@if>Mnf1`!Ma'nUHapH=4J.."f+tlkl@9Tq[bf,c9!8QuJh0s_>#]4qCm=0&l_*K7UamTZta6j1bDNDHUYh_;E!9[\sV0s&tra>6V/j$f!oDKj+dJ\Bs*Y"+TN%Q2[g]0TSr;&*B3(Y"`BF72M!;0n=s)c]m0su$<Y04n]X.iAVXcF<-E@tdRPXaS$#/bGSHC/UaoaL2C$kY@N*["OL#dledBOm8/=am8;hTCd'I(Qo5-S=i4.brNE^@gXoBLa6:=;K5=)[HcHhiSI8q@Z+U+QSkiL=B0L09=K^0PPrT,K#LJ%`L?8cnuh[;ii<jb![SGCKX58TXJ6SFB=ik'E)=aV$o"pjo6fK[Zak/%sG>pV52mGZ927@8id3.<XT6(#[=,>75Q;c]B<9Wms[KaqY_WYFb3OPK"s=rl@*)UH'G.!Lt#kK?p)H[MsTpl"V6*HE%r'\bMq_n*[*erhk1JgNB9:qruj+3[?eZ]1Y\]E2o:8=B0o[7(=T)),7Z#nM9.\:,-,O34G+W6Zma,2lY#80846TI<J;iA_:.rJF!^.KbJijpCRakf"3!m8;gr.snn]e!^S<W/?D`ZE!;Z?ep$Ls!GWEe5>_?H%'TVr"22=]4mo%1Ih8k7O-^R*HA7T'V0"!qJUW^%p?CJFQAe.a$X\nte.BjAin#0eN;mJReHmbXF.86;Q,SH-A@mI=T=NB\X81SAib[kiDQDUWJgrflJ8kf=D"-O$lenc3Sp4fdr>Ptu^E4">a2PDY`U;2tX#]2^d&tPYd1,7VC,ST2L3H-^&'ga19^/fWX!X&o/_BanUM&19rS<^E..oUL?D*=36dJs2nk%\6,91EM[EIh@F,?3#VR3,%^4u+Tl"J#8L#nY1<T(VT3mIn"H^HK'b)cjZ^TkH[=M)NO?#<ZN%*!E0!?j-86G/.p?;8jr0Lbup!!f_uA(3]:0qauF2K9OdlFn8tlrj%MYP;U(Y*lV[K1UV(DP4%i8p8Z[:gAShM?^<)$fX]c+'k`VX<2+F0#rs]k%j(\c-&I7oSO"(lEd1;+BR>$gYOet)%?%(u<Q5r6brOOkabF(&2@LTFN&phF$%AdfA)="\X-DJP%:%l>NK="I=3V<4,5%Ua=f+q#g.j_S$HJ?gGgLW\h::U$&BtTn:i^ga6JR@X?)e5Q->135_CS?Y&kg@o&fa0@^L=6dgiU?7kA:*BJ$V:;j1WWT`n:j-72i01NEXF,aou3E;:qA>(1Q8+0]^MK\erXrkA(1IScC^tg3.LMS$3[EfteZp#pWq$)%9*T%jfCi$IWb3)`H%9N<.`a_Xo.DCR3_4%<u%G6JP>,I@ZY]kt^nn<rBX]%Oc^(5)s//j:m"FL48sE(\A.fHVS3"+qXm%pF)t!`FUEc2F>_@A#L9W--r#jZQk)5Y6mbtk'Akq^;'L5qaV%Ibk<HQi(AWM5^Ge"Z3a.;/@\EO.p%h@/%N1I'IF39#d]5'S/XGDL)SaAS7>(R@f1@a4lH\e"=Lq!H&dNmEU\`7')=ps(g=CiGL,5)VaOP63qOPIgr:3>^<;I9BK5W>>E.EsH[0PYl*3*X4<H5f9:sN_&M%#6M&Yn!![6Y^1nq4rK_9QXK46jmk.rA].m3;RWft6jcbK".TtUX&?:JB6c+-9bYI-hghq#c>A*[UmPe-&h^A46A"!LH.^.*8loqW=le$/+\?fI@;3uj>/lBT#TKsa\&.3qH%&uSBaW_LqEjXdOCNhpMAl%@/.PA?H$+A'X(:6?E.ZW(l`9XZ[/O^KEiki_Zi,Bq`*]XUX=WT>F,.[i+(VdSEf0>QLHllfc+G!8bI@r<[Fmm1dXXUL-6DV#I"S2\bGSF&4"*"Gj*OE4Hr03jS\6*P^FJk!^%KjE-"l,cn2o;oTL1mLql;I#C;>9c*m"]Pm(IQjGJq!aT?DSO5:I!S5brX[!X3hZ~>endstream
|
||||
endobj
|
||||
16 0 obj
|
||||
<<
|
||||
/Filter [ /ASCII85Decode /FlateDecode ] /Length 2475
|
||||
>>
|
||||
stream
|
||||
GatU4?$G!n&q0MXW8iXR1,1Ns+,9."0$,C/b]m_^2X<B8a"J&T7"_;dZs$l8lh=FQ?mgR/]+2Z6;0Q%5X]nf7nYq/-ms\"fUA#<^s5]@cct#Upcg0!>MEXd1r]K_?Q%QjkN.R0f(W!m+p,bG(_db>:";ea8H/2o,3ss%JFla.%j9.C>SXTXS1:RV2+,Yul3G@5'V^nmo;J2N?9Igc'SC:[LF1]HT1$<IgZCo>6'$Tfhm0RHVs#gPXL"Do;lT8b)Gt4BO61`\Re2mOQ%#rQr6Y/l,#i9XAQ4erPSF1:ZK195d55MAIAJO1u6D`E8j4$N[:ifZAoUZe`Y1Y?dpDS\AQ*bQ:iUkt"-nVK_\Re,X\"&i4.\&WFWCXr)4K;@!T*l'nAuT+'&lKE=Q\j*i*g:qKHFq8TD?D(_;Q3S04,5n).Sj'imA\BgPc`Ailjh?W47+o0YJ"=f"2TCYcfd_7<j42/mb^j"*D%+1idhJ4fm#@4dScI0*3l694LLK49CI\<om/9'm?j\P>&"1:a1Nc;QTC!^"DEc7c(SCR1o;a$!oUK6o#,3lH675i60CAJ&`X';*.A7L(%u[N3B?4mrej/YefQs%c_,el;Pk3iNN[hJ06mYSCpT:DA1@apa_,!RBM1jaH'M<27HN$t6ININMP!3T0Y!c[[T;K]8^3-!Z_@r1FW^5\?gV^%cnD%XqNTO]*<;c:dr,EQiucUf\Xa\DE(UbLea1[rc1+[J5X/-Se:btEZ9?)C`DqVTW1U1g)Fo>4+*LrmXq2pR9*(E2U"pEOORAs<fc7T8"jEGsaNmSGEd?M(h3r3J!Ot]u&Xnm9=Q.Q?a1km,%-4_]/@6i!1J5#T<kRm/>bgtVds8>j<ZDn:-F_q9Yqas_U&qM8S#MXK%4p%=$3')1&u5\YX+mQjr@o/_L9JG_5@M,dW[$b-qG3,-1d%GZ<eg?'n1r3$i'2:A"`MP<M;'XSF68medqb^ZiH.F)/>"uOm]^`Wd&ph^2s?&$^"B`iXafuF*>N"/cI:l*8DaPk%'KTf27Qt--L$sc:0Wu014>-Z+;oY^dpN]Zm^Z!*c6HZ,i<bYbe<ut[=^aT8%OE8`fpQjX#Pa#3.D.gKO9+M_ompakQ!3]>a%+;eem/**!Kbd5eRF7fk+&3$&]$4kaOiC'MKInFcWt9d@;:k=XZ(WI"rpXlR'`1IfM!1$]L)VjZ3DV>nc/Y:bZI>>q2MO:0!3N6FM$P^o$>IMd9X_(FMjMm3-,2)FQh5'G,VqY/1jr6p#lX(A_@j_3+'()UrmY-*lp'fQH*C\IBb]B_>i$BP5B.SQZp0_#$HAC++'Xtj5_O'*O=]&*W=,=<O3-f^ktk'.Cr7g)HV>^/DS!#K0IX^4A557%ZtNtM@uDA03UU:>8X6CjcS"0bqZR+OtKuY^1@*h'T;U9S'<%^DEL-u3R's'-ucWhDZo*kVn-U/`!]!>&a]\U'-AV+QWQgbh*5C%;S?e^F+J=@*'92?-_tXh2s<n0p+GQh3kGR!e\[r#A^U5d1s5s-.4+KUWD*=)AUi&0&O<NA'gs\O#"+WGmgY&I+gp94b2dmdQ.L1RYIAG02sLp.K._TNmrhdZ3G<[ppcf0o":+>fp)`LLh-f\#GBVf-BNo:n0le))B8IUihfk@=Js;50[Hq&!SuiBH")l]TNHWK`Es=KVeK:B"kd]/I^HRXkiAMN5438Hs6cVk0)EmG\c>64'2^'pK37UTJ.&nY1@:eVT/M]^$fVmBupEG8ched*H9[&g#enLaEC@5/PYhl*:jLI!C:T%<'/m3$L?p0EK9Q;K))0ZkG`jpo60CFI"6o=P.#NTc"!%'8[ehrqE"mo_."Ou(k8-)?7F$2"UZ>>0UASnQ_A(Z_sUrQ,joIl`"7bOHlUe7d^;eSJ4EZg$Io>bn::0t&B%@O!#QSpZ1N[?LQcZUYA7DB*6C>OO=e^`(?;[;T]oJ?b9b*\qo`D0RWHjAKRhcD,B6!:W(VaN4U<VaMb##l7P<Mr:j-15XSY)sQ(p#[R!'18/N"Xn=4XP0&`W,43k)l(',Z-k.P_L$:^q86Xf78jUIL:s`ch?=eulK%+!M:\ZuY3TXp*C5-Kl..`J]tHYo%)'I\RaXpI!I%/^^Z`DE^`W2l$XWd+$=lr/Ca=W!$=h`W+!,@7s4mW5,_s-1Enon9EOlfNl5e]>5J%6I=i>;bC\p=5K,kBG>od5Nn;2U<6+-\MDML[>rLIhC,mjrS1U'c7%@Z-&3*^6Cj1ab)ZH$5nnA`$B9Nj.V)+E26q@HLR+_3&=NDW$H1=Pp@GB/D`7*4A)5Ba=ACRiUtPl3\\[:ZA#:s*NsZbisi)c/jkW.e[<d-PZ!\\i0SZMjX2dI)7M?l4FOP;O\?`<BEBLF`1HJnn$_`S2*CZm.JD3k+SAF=NWI'X_"t>c5:W![stVRR5N3s)iBOD^h=tKGkK47(u.C*j"BaUVXKij6F=g0W`.>4t>lhIY@fnr"jG/O>-~>endstream
|
||||
endobj
|
||||
xref
|
||||
0 17
|
||||
0000000000 65535 f
|
||||
0000000061 00000 n
|
||||
0000000122 00000 n
|
||||
0000000229 00000 n
|
||||
0000000341 00000 n
|
||||
0000000446 00000 n
|
||||
0000000651 00000 n
|
||||
0000000856 00000 n
|
||||
0000001061 00000 n
|
||||
0000001138 00000 n
|
||||
0000001343 00000 n
|
||||
0000001413 00000 n
|
||||
0000001709 00000 n
|
||||
0000001787 00000 n
|
||||
0000003720 00000 n
|
||||
0000006092 00000 n
|
||||
0000008639 00000 n
|
||||
trailer
|
||||
<<
|
||||
/ID
|
||||
[<9637415b07eac78105f5700cff5859c4><9637415b07eac78105f5700cff5859c4>]
|
||||
% ReportLab generated PDF document -- digest (opensource)
|
||||
|
||||
/Info 11 0 R
|
||||
/Root 10 0 R
|
||||
/Size 17
|
||||
>>
|
||||
startxref
|
||||
11206
|
||||
%%EOF
|
||||
@@ -1,313 +0,0 @@
|
||||
# Les 13 — Huiswerk
|
||||
## Background Agent + CI + branch protection + DEPLOY.md
|
||||
|
||||
**Vak:** AI-Assisted Development
|
||||
**Opleiding:** NOVI Hogeschool Utrecht
|
||||
**Deadline:** Voor Les 14 — MCP servers
|
||||
**Inleveren:** GitHub repo URL + Vercel productie URL + DEPLOY.md
|
||||
|
||||
---
|
||||
|
||||
## Doel
|
||||
|
||||
Bouwt voort op de **lesopdracht** (Pokédex op Vercel productie). In dit huiswerk:
|
||||
|
||||
- Voeg een feature toe via **Cursor Background Agent** → preview URL
|
||||
- Voeg **GitHub Actions CI** toe (lint + build)
|
||||
- Activeer **branch protection** op `main`
|
||||
- Schrijf **`DEPLOY.md`** met reflectie
|
||||
|
||||
> Niet klaar met de lesopdracht? Eerst die afmaken — het huiswerk heeft die als startpunt.
|
||||
|
||||
---
|
||||
|
||||
## Onderdeel A — Feature met Background Agent (verplicht)
|
||||
|
||||
Voeg een feature toe via **Cursor Background Agent**, niet via Composer.
|
||||
|
||||
### Stappen
|
||||
|
||||
1. **Open Background Agent** in Cursor:
|
||||
- `Cmd+Shift+P` → "Open Background Agent"
|
||||
- Eerste keer: verbind je GitHub repo aan Cursor cloud (volg flow)
|
||||
|
||||
2. **Schrijf een specifieke prompt.** Vage prompts → matige output. Voorbeeld:
|
||||
|
||||
> Maak een nieuwe branch `feature/search`. Voeg een controlled input toe bovenaan `app/page.tsx`. Filter de Pokémon-lijst case-insensitive op naam terwijl gebruiker typt. Houd Tailwind-styling consistent met bestaande cards. Push de branch en open een PR met titel "Add search bar". Voeg in de PR body een korte beschrijving van wat je gedaan hebt.
|
||||
|
||||
3. **Wacht** (~3-5 min). Volg in Cursor panel wat agent doet.
|
||||
|
||||
4. **Open PR op GitHub.** Vercel zet een comment met preview URL. Klik die — feature werkt live.
|
||||
|
||||
5. **Review:**
|
||||
- Code-diff op GitHub
|
||||
- Live test op preview URL
|
||||
- Eventueel: comment achterlaten of nieuwe prompt aan agent
|
||||
|
||||
6. **Merge PR** (of laat open tot je CI hebt — zie B).
|
||||
|
||||
### Feature-ideeën (kies één — of bedenk eigen)
|
||||
|
||||
- **Search bar** — filter Pokémon op naam
|
||||
- **Type filter** — chips per type (fire, water, ...) om te filteren
|
||||
- **Sort** — op nummer of naam (asc/desc)
|
||||
- **Favorites** — localStorage-based, ster-icoon per card
|
||||
- **Pagination** — 20 per pagina i.p.v. 151 in één keer
|
||||
- **Eigen externe API** — voeg bv. weather widget toe (Open-Meteo, geen key)
|
||||
|
||||
### Eisen
|
||||
|
||||
- [ ] Feature gebouwd via Background Agent (niet Composer)
|
||||
- [ ] Branch `feature/...` op GitHub bestaat
|
||||
- [ ] PR open of gemerged
|
||||
- [ ] Vercel preview URL werkt
|
||||
- [ ] In `DEPLOY.md`: prompt + agent-resultaat reflectie
|
||||
|
||||
---
|
||||
|
||||
## Onderdeel B — GitHub Actions CI (verplicht)
|
||||
|
||||
Voeg een CI workflow toe die lint + build draait op elke PR.
|
||||
|
||||
### Stappen
|
||||
|
||||
1. **Via Composer (of handmatig)** maak `.github/workflows/ci.yml`:
|
||||
|
||||
```yaml
|
||||
name: CI
|
||||
on:
|
||||
pull_request:
|
||||
branches: [main]
|
||||
push:
|
||||
branches: [main]
|
||||
|
||||
jobs:
|
||||
check:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: pnpm/action-setup@v3
|
||||
with:
|
||||
version: 9
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
cache: pnpm
|
||||
- run: pnpm install --frozen-lockfile
|
||||
- run: pnpm lint
|
||||
- run: pnpm build
|
||||
```
|
||||
|
||||
2. **Push naar een nieuwe branch** `chore/add-ci`:
|
||||
|
||||
```bash
|
||||
git checkout -b chore/add-ci
|
||||
git add .github/workflows/ci.yml
|
||||
git commit -m "Add CI workflow"
|
||||
git push -u origin chore/add-ci
|
||||
```
|
||||
|
||||
3. **Open PR** op GitHub → tab "Actions" → workflow draait → groene check
|
||||
|
||||
4. **Merge** naar main
|
||||
|
||||
### Test je CI
|
||||
|
||||
- Maak een typo in code (bv. `<dvi>` in plaats van `<div>`)
|
||||
- Push naar nieuwe branch + open PR
|
||||
- CI faalt → rode X
|
||||
- Fix met Composer of Background Agent → push → groene check
|
||||
|
||||
### Eisen
|
||||
|
||||
- [ ] `.github/workflows/ci.yml` in repo
|
||||
- [ ] CI draait succesvol op merge naar main
|
||||
- [ ] Minstens 1 voorbeeld van rode CI (screenshot of beschrijving in DEPLOY.md)
|
||||
|
||||
---
|
||||
|
||||
## Onderdeel C — Branch protection (verplicht)
|
||||
|
||||
Bescherm `main` zodat niemand direct kan pushen.
|
||||
|
||||
### Stappen
|
||||
|
||||
1. GitHub repo → **Settings → Branches → Add rule**
|
||||
2. Branch name pattern: `main`
|
||||
3. Check: **Require a pull request before merging**
|
||||
4. Check: **Require status checks to pass before merging** → select `check` (van CI workflow)
|
||||
5. Check: **Do not allow bypassing the above settings** (optioneel maar aanbevolen)
|
||||
6. **Save changes**
|
||||
|
||||
### Test
|
||||
|
||||
```bash
|
||||
git checkout main
|
||||
echo "test" >> README.md
|
||||
git add . && git commit -m "direct push"
|
||||
git push
|
||||
```
|
||||
|
||||
Verwacht: `! [remote rejected] main -> main (protected branch hook declined)`.
|
||||
|
||||
Goed — protection werkt.
|
||||
|
||||
### Eisen
|
||||
|
||||
- [ ] Branch protection actief op `main`
|
||||
- [ ] PR's vereisen groene CI voor merge
|
||||
- [ ] Direct pushes naar main worden geweigerd
|
||||
|
||||
---
|
||||
|
||||
## Onderdeel D — `DEPLOY.md` (verplicht)
|
||||
|
||||
Schrijf in repo-root.
|
||||
|
||||
### Sectie 1 — URLs
|
||||
|
||||
```markdown
|
||||
- Productie: https://pokedex-XXX.vercel.app
|
||||
- Preview (feature/search): https://pokedex-git-feature-search-XXX.vercel.app
|
||||
- GitHub repo: https://github.com/USER/pokedex
|
||||
```
|
||||
|
||||
### Sectie 2 — Background Agent prompt
|
||||
|
||||
Plak je exacte prompt + wat de agent uiteindelijk leverde:
|
||||
|
||||
```markdown
|
||||
**Prompt:**
|
||||
"Maak een nieuwe branch ..."
|
||||
|
||||
**Resultaat:**
|
||||
- Branch: feature/search
|
||||
- PR #4 — Add search bar
|
||||
- Files changed: app/page.tsx, app/components/SearchBar.tsx
|
||||
- Build time: 2m 14s
|
||||
```
|
||||
|
||||
### Sectie 3 — Composer vs Background reflectie
|
||||
|
||||
Maximaal 200 woorden over:
|
||||
|
||||
- Wanneer werkte Composer beter voor jou?
|
||||
- Wanneer Background Agent?
|
||||
- Eén voorbeeld waar de agent verbluffend goed was
|
||||
- Eén voorbeeld waar het matig was (en wat je deed)
|
||||
|
||||
### Sectie 4 — CI workflow
|
||||
|
||||
- Link naar `.github/workflows/ci.yml`
|
||||
- 1 screenshot van een groene CI run
|
||||
- 1 screenshot of beschrijving van een rode CI run + hoe je het fixte
|
||||
|
||||
### Sectie 5 — Eén observatie
|
||||
|
||||
Iets wat je opviel deze week:
|
||||
|
||||
- Preview deployments — wat was makkelijker dan verwacht?
|
||||
- Branch protection — hoe veranderde je workflow?
|
||||
- Vercel logs / Cursor Background panel?
|
||||
- Iets anders?
|
||||
|
||||
### Vorm
|
||||
|
||||
- Max 600 woorden totaal
|
||||
- Concrete links, screenshots, prompts
|
||||
- Mag wat informeel
|
||||
|
||||
---
|
||||
|
||||
## Bonus (optioneel)
|
||||
|
||||
### Bonus 1 — Eigen externe API
|
||||
|
||||
Integreer een 2e externe API in je Pokédex (of als losse pagina):
|
||||
|
||||
- **Open-Meteo** (weer, geen key) — bv. "Pikachu's geboorteplaats Pallet Town weer"
|
||||
- **GitHub API** — embed repo-stats van je eigen pokedex-repo
|
||||
- **Quote API** — random quote bovenaan elke detail-pagina
|
||||
|
||||
### Bonus 2 — Environment scoping
|
||||
|
||||
- Voeg een `NEXT_PUBLIC_APP_NAME` env-var toe
|
||||
- Production scope: "Pokédex"
|
||||
- Preview scope: "Pokédex (Preview)"
|
||||
- Development scope (.env.local): "Pokédex (Dev)"
|
||||
- Toon de naam in header → zie verschil per environment
|
||||
|
||||
### Bonus 3 — Twee parallelle Background Agents
|
||||
|
||||
Start 2 features tegelijk:
|
||||
|
||||
- `feature/sort` — sorteer op naam of nummer
|
||||
- `feature/types` — type-filter chips
|
||||
|
||||
Documenteer in `DEPLOY.md` of parallel werken meerwaarde gaf, of dat het lastig was.
|
||||
|
||||
### Bonus 4 — Custom domain
|
||||
|
||||
Voeg een custom domain toe via Vercel (gratis subdomeinen via vercel.app blijven werken). Niet verplicht — leuk om eens gedaan te hebben.
|
||||
|
||||
---
|
||||
|
||||
## Inleveren
|
||||
|
||||
1. **GitHub repo URL** in Brightspace
|
||||
2. **Vercel productie URL** in Brightspace
|
||||
3. **`DEPLOY.md`** in repo-root
|
||||
4. **Updated `.github/workflows/ci.yml`** in repo
|
||||
5. **Minstens 1 merged PR** van een Background Agent
|
||||
|
||||
---
|
||||
|
||||
## Beoordeling
|
||||
|
||||
| Criterium | Punten |
|
||||
|-----------|--------|
|
||||
| A — Background Agent feature werkt + preview URL | 3 |
|
||||
| B — GitHub Actions CI werkt + bewijs van rode + groene run | 2 |
|
||||
| C — Branch protection actief op main | 1 |
|
||||
| D — DEPLOY.md compleet met 5 secties | 3 |
|
||||
| Productie URL werkt | 1 |
|
||||
| **Totaal** | **10** |
|
||||
|
||||
Voldoende = 6+. Bonus telt mee bij twijfelgevallen.
|
||||
|
||||
---
|
||||
|
||||
## Tijd-indicatie
|
||||
|
||||
| Onderdeel | Tijd |
|
||||
|-----------|------|
|
||||
| A — Background Agent feature | 40 min |
|
||||
| B — GitHub Actions CI | 25 min |
|
||||
| C — Branch protection | 5 min |
|
||||
| D — DEPLOY.md schrijven | 30 min |
|
||||
| **Totaal** | **~1,5-2 uur** |
|
||||
|
||||
---
|
||||
|
||||
## Veelvoorkomende valkuilen
|
||||
|
||||
| Probleem | Oplossing |
|
||||
|----------|-----------|
|
||||
| Background Agent niet beschikbaar | Cursor Pro nodig — gebruik trial of vraag credits |
|
||||
| Agent maakt branch niet | Prompt explicieter: "Push branch en open PR" |
|
||||
| Vercel preview URL leeg / 404 | Preview-deploy nog aan het builden — wacht 1-2 min |
|
||||
| CI faalt op `pnpm install` | `frozen-lockfile` flag aan — commit je `pnpm-lock.yaml` |
|
||||
| Branch protection blokkeert merge | CI moet eerst groen zijn — wacht of fix lint-errors |
|
||||
| Background Agent kost te veel credits | Maak prompts korter, beperk scope per agent |
|
||||
|
||||
---
|
||||
|
||||
## Tips
|
||||
|
||||
- **Specifieke prompts** voor Background Agent — vague = matig
|
||||
- **Lees PR description** die agent schrijft — staat vaak goede uitleg in
|
||||
- **Vercel logs zijn je vriend** — bouwfout? Logs lezen meestal de oplossing
|
||||
- **Houd DEPLOY.md bij tijdens werken** — screenshots maak je dan in flow, niet aan eind
|
||||
- **CI failure ≠ slechte agent** — CI vangt typo's, agent loopt soms net naast
|
||||
|
||||
Volgende les: MCP — Model Context Protocol. Een vector-DB met PostgreSQL pgvector. We gaan een tool bouwen die semantic search doet — handig om in een agent te gebruiken. Tot dan!
|
||||
@@ -1,194 +0,0 @@
|
||||
%PDF-1.4
|
||||
%<25><><EFBFBD><EFBFBD> ReportLab Generated PDF document (opensource)
|
||||
1 0 obj
|
||||
<<
|
||||
/F1 2 0 R /F2 3 0 R /F3 4 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 /Courier /Encoding /WinAnsiEncoding /Name /F3 /Subtype /Type1 /Type /Font
|
||||
>>
|
||||
endobj
|
||||
5 0 obj
|
||||
<<
|
||||
/Contents 15 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 14 0 R /Resources <<
|
||||
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
|
||||
>> /Rotate 0 /Trans <<
|
||||
|
||||
>>
|
||||
/Type /Page
|
||||
>>
|
||||
endobj
|
||||
6 0 obj
|
||||
<<
|
||||
/Contents 16 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 14 0 R /Resources <<
|
||||
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
|
||||
>> /Rotate 0 /Trans <<
|
||||
|
||||
>>
|
||||
/Type /Page
|
||||
>>
|
||||
endobj
|
||||
7 0 obj
|
||||
<<
|
||||
/Contents 17 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 14 0 R /Resources <<
|
||||
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
|
||||
>> /Rotate 0 /Trans <<
|
||||
|
||||
>>
|
||||
/Type /Page
|
||||
>>
|
||||
endobj
|
||||
8 0 obj
|
||||
<<
|
||||
/Contents 18 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 14 0 R /Resources <<
|
||||
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
|
||||
>> /Rotate 0 /Trans <<
|
||||
|
||||
>>
|
||||
/Type /Page
|
||||
>>
|
||||
endobj
|
||||
9 0 obj
|
||||
<<
|
||||
/Contents 19 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 14 0 R /Resources <<
|
||||
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
|
||||
>> /Rotate 0 /Trans <<
|
||||
|
||||
>>
|
||||
/Type /Page
|
||||
>>
|
||||
endobj
|
||||
10 0 obj
|
||||
<<
|
||||
/Contents 20 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 14 0 R /Resources <<
|
||||
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
|
||||
>> /Rotate 0 /Trans <<
|
||||
|
||||
>>
|
||||
/Type /Page
|
||||
>>
|
||||
endobj
|
||||
11 0 obj
|
||||
<<
|
||||
/Contents 21 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 14 0 R /Resources <<
|
||||
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
|
||||
>> /Rotate 0 /Trans <<
|
||||
|
||||
>>
|
||||
/Type /Page
|
||||
>>
|
||||
endobj
|
||||
12 0 obj
|
||||
<<
|
||||
/PageMode /UseNone /Pages 14 0 R /Type /Catalog
|
||||
>>
|
||||
endobj
|
||||
13 0 obj
|
||||
<<
|
||||
/Author (NOVI Hogeschool Utrecht) /CreationDate (D:20260607134605+00'00') /Creator (\(unspecified\)) /Keywords () /ModDate (D:20260607134605+00'00') /Producer (ReportLab PDF Library - \(opensource\))
|
||||
/Subject (\(unspecified\)) /Title (Les 13 Huiswerk) /Trapped /False
|
||||
>>
|
||||
endobj
|
||||
14 0 obj
|
||||
<<
|
||||
/Count 7 /Kids [ 5 0 R 6 0 R 7 0 R 8 0 R 9 0 R 10 0 R 11 0 R ] /Type /Pages
|
||||
>>
|
||||
endobj
|
||||
15 0 obj
|
||||
<<
|
||||
/Filter [ /ASCII85Decode /FlateDecode ] /Length 2335
|
||||
>>
|
||||
stream
|
||||
Gatm<8TWXr'#*s=oOTMp+p88i0S``km&nK)4H:aeYK@Z=KI#7cWK<@8RD\phY5ps4[FioP*2b%o"M4JGo^W"k./ksmR4\Ns:>ke8#)pH1#0\GY9YS;tM>+n44QMVH!"kBOQii2!rWi,O.j50i)"X,GU_/`O&&)RBb#e_cF]$($W"mN_GRR+WdRa4",RZpVm]@9Nj;_1s,RD;4GJ1C[C@bmI>3jPZmQV&r)9d\Q;M@mp$_dVJ9,RX$p%pasY3)`U(KFJN+)S..7131e7rWW85&6V"BiPB+eZC=$eP_\"F-p4MjFu/BORJ*Xpjtuio"V+-R"3NK]X^u69dfZZRHJ,_C`qqcFQu=naK;;4?mLU3JXc2&a1W&9/Nl<*3]Btcn0'-KLAK01rLjq<Ih!Xpg]M&<clc_BD*JdK&[<1C1reNmQ;cp8;K)'9n>nB(6@3FR=-e.Hbp`)1cF7/,'SFb:Gdu(Q8]_-Fk85_d7#aTQ4<8St:o&p.&3GpYPr+RcFroC@:73'Kf\M1tmRCkCNV:No20LQ&+9>u8otYsl!u>CPCiW5LFfFm,,O,PW[U<K_RkB=)`Y<'JhuY)ZOjYZG6eLiOG8&$O.:A#91IXVm-oWJ-BA*D$q_h#ak&n'a1H:k6<l%FRGE*co3[rM]XQlfcBq&0MNkPI3n`B(daYGrH0rQSM,mQ5<35Hnji,^j9LI]VTNH<HCC-Z`>k;MWT9'DQ`^@)#EFhpVtW3h>^&5t,MhijNg>=K.*MD;?B4&2'c[UgO6:YY._#l,k[mo>McH)ZfKfdPJhg55)L$qro><E0JTc@R[o0$c`?[]ALhAQ`?bRg1WeqC#&#AUuJ7<I#S6W>]rgk>U5d?;E9b<u#2\Gg=kWcO0Z]r,Q^Hmm)46Z:TM9SH.qPR/Y-4"_/&.FhO^2pDl[T-Ne1@>[e#oaiSu5(9u,;I7eJS#m]omZ'2Bp5E*38LNOBH*DUY4mfi^[I[M[%\'QJN6K)gX/YkhLdQBm@%P6b9lPueQLOkQASV)j)Img:7"*XKLn._SdIP4+;?98'\ENR_o3f-a=D3n'N.Fhu6?W0%[qkV6W'Gg3$$Ri\q'Genu/0q*tV!'TcY5sr<&pGV;2@2B/1WG3Q1Rq_JE:4Z/Ph3`*Z+4]PpCe8m,aBiQi+RQ<DAgg[e/L/h>HR?t\AcnC6FDD=6(s`^Q.:^7BVUKcCI=jI&TguePc::$l??7+Wp!CLl8aFaQ(!>'--T?:Y0d>gV;O7-i8(t^.MpBWD<Ve2<B87c\4"kTF8F*;V?S!j.nK1(Xa[b2\6]396XEpYf<bF'+"^KN4#&8XA498>-XVNP6<\VP5)EB_J63khN<2I@$:"/MA5JYHh+ota49eprp#TS?905eUoT0SRK$Bh8F(=tIabc^DD@#i8T_2&<EY1+J)6auLdD4#q_INA,oe39X7%G)fW.NlPg)F`B3uA(\Q=oUU,\Fn,)6;QU1GM0rbIQcL6jSXN:(s)&_%m[7\U!pr+Q)ZB,f^/b&os4s_JREpBEM>VboV/A)IYF+'fi/2eam9%8VEcFO<6u/4k+=0f,2Pe^9u'tohZHI<LInhd!Ba*5qd;aBias4o*Ki-mH9jX34277Vs5?:R'5_*]hNbS[ap:08UIqj?@H,>ZhEL7.tdO1N;j"+%Z5OTYM&+XqBW6&VmrU*ooCN=#?<!9Kc;S*DZGE3RNo%Pea]@Z.;]+*7TQY[-UZNOT*2,Q!NCGdcu6A@lR()^<1;-RFea%C=K_'>j`hDd&n(\F^r"+qpRe>AMK&VhjACZ(hc@]9Ejp7nUpQ-[0LYq4+9j2Yo`%AZ?<h79?sC9pBo0dc=E2f)W=N(-qQ=RUS#\Wqj<"C#1fZ%g!Ao:2^g;14dOJ2U/>(2V<)Oj(ft_q7g63Y;:-eM4@)&^fY4QmDNN*j18YQM:DK&gf](3KqLP;2bp;mP"(Uj2gl^7OQ4&r<%`^`XRdiK`X65Ir("#>b`!09_>];PLshr"Fs/$WdN7S)ES;8'1UlL2Urok?6&Ode@`M_]^d_*/nK0?Wpr&0,c;$CAV)EYc.s'\,E]:)?J^GYNNn[+D-gSa8)XhH%8&8(7K#8F$DsAg?MJ<nCVP"VD&g+daEY^F%'^WT[oreotjro'IU[Feb*\>mpi2J3Z5VReo9#cQ#=A7+'Rc;5an=k\AqBZcKMb<Mf,=aJW@JP.;cRoCIcK+\3k8&N0J?03UJmi3>g)X)_J+f+"%JUf$0bCfK*KqOF91*,8Ut&^556oMrL8r#$G09Tm&u_q$tgg:rNX1a(#uPHB[?e[0!QT$l&_cC-!KLDPLXlXmEl.lt8IV>/_HQ9j\@QmVL4m:l[YrrGKPSRq~>endstream
|
||||
endobj
|
||||
16 0 obj
|
||||
<<
|
||||
/Filter [ /ASCII85Decode /FlateDecode ] /Length 1035
|
||||
>>
|
||||
stream
|
||||
Gb!#[gN)"%&:N^lp5u.>Z$_i6Q8D0sRC"+ha\6>l&S<aI=E7sRmGD!Z5Y8IrkQ<8Y^k8cFF3^r^(^c:Oe*8a2!%rS:G"mP!cl3#YM%@9]\E^JN[YPm6OWOSnR,c.<OOu'jT[T3[+=(C^`Y=dR&Jk3#1J/LB)7Xiq'[[1C_W\?qJOH1F0cp[r6--p&oc=EFf;A75&;K_uoaO)c_M_"<TX-?<_ZmGkd"^%"Wok%N##PB6%,,(VT5NUWV-#!QV5E`q1aZ]7IFjJ:r3gZQ'V[9@KM.iq<94uiErs#scoWh#Xi:%JJG>]L4cb)J"],_IAtiD#T%%bl4;F$3],'VHMRn`\\dd*/EsW3oh\O+9?H_G\?UgrpUjR>k.TC.JBO-=Yh`?Jb20(M%fF^Kh.W;rf"^ZU(U#2FIMq%_o<u7>^ffsb:EujL;du^jmUeZ'CQG7/WVeR>ueNV1L$uGB39D13l-LZh3*OBM&G+V7O:66MC!MtaXEbuE:?O9kh-#Rdgb'cX[P;*KUY#>0De?5i4&lXE$\n^e6\kG[l3CqH%S`gnYrp_7iG5'EK$$s@-Cj9jL+;p*MJDA:Tb$.jMB"7p'*pu==4RUBtLR):]Z^l2@g0'_EdVADXOZdkMU<?dH'#!FKV1l`p456`,q"1%QR]"dUn,eC6_+mJU&W'E_C@""%UV=rp+SC%')q"0-i,6Gu]q*br>EGB!$.+3SOP=Cp9X*2"4#Asl,la[]Pt4aRriu]n#j:$umD9:5%ei!+c=\u4?sucACkZ&K@>\Y!gVXDX/#*J]B?`V`gXn,Y.eg""clt,>IX'\Sr7M$.bBf(l]iLW)_<LRLNS-[V"#X9k@4jNEn77\s23UBe\$`W)3,L+k^dj[OW^+8<k20cG+3=7EF(oK;<)Z"+KTlT8P;"r%!=Z1dS8WT3gCEEL)0Wbu7Wqj:[n5!$:5ZshD!W36kQuB`;ogiA"YSa'lQ)3#ans5&r5!Y$VcsIo=5CLcmj&j$Ygi%-1@Pcu\`GjjMqPIR7/I4$1r[B=gbC(Jo`l3#&Dm~>endstream
|
||||
endobj
|
||||
17 0 obj
|
||||
<<
|
||||
/Filter [ /ASCII85Decode /FlateDecode ] /Length 1666
|
||||
>>
|
||||
stream
|
||||
GauHK=`<%a&:WfGQq&HQ<dSh1BbWl$-9r'=:kKO+;,GbG@a%4%+ZKAmSL#SFFsJdsN(AOkX`hGSB'1EAMJq'n7k)7F3Qtec?l\<q^a,$rIg<1>hR,k`i`c1DKV9UJ5Q]nN:oq;<Hpq#M!sXKp@^H8P(bm?k^emjBVP155q:K1B[%c8F+GT^7W,L`#;4OCr8UR@d]CJ%4`mG0Mg9F#6m0CIH#MU-HTaF@>o<f,#'IFncIXVZCns(f%Fe0uMHWA^3?,6^-)P4X54tFgdXR$R+g9"*?9kq$"&^8#*r3=^.^mDnc;G_c\s.(`m$$]Chjc(jp>28^g'k\\G!RN2Ve3/uS4/SGb+rk-XSih!gi5lfZPuNikgaDK+\!D8D'2#)=XDtT$,mi%X%p/]-T#g6Kn:hh#*)sB]i4d!^%U6/\:Ieg-&0\mgIH!!ZQh>jV%Z#:3,*2FK')^.>g^/1:$nRIP[FYPHHag_]]lq6@7'@eH(EWaO"ba.2EruhjL%.mtZf_G*&Cqooh<P&J2>@42hr7:(Hkp,JhST:MY27Y9GSX;!SsAg)/R1uX[b)i[7c)+ZZ)CJ"m@rTdW""g0iNjk*S&D.`)A6gM`<mrait,PXkpA'#!Vn%C_eb`q<P3&k1C_5P+KN"Uf8J<V?<gb_q+Wl-@e)BNc>G?ua@uep:aMQ\AJ@kZ'^r2Ai&1Uo21b8jBMqW@h3Nh6/b,X1:9\*jC'Z;N<"E'SIS]%tQ,`;?$QH$^%FKDFfn+:?g.`T?D?Za1+"h050n[5T%n:nO/o47#"7<YejEk\=gOiob+D@L,*fes)1QG&bj.4Ok9P%XIcS1Rnf;;;0)F=UC92hFQNU.F<\?"?mYc3iIT>Fpd[]IIO\N:bqmj%oL7@6MZ>FrZ<Bc(K4O,,Sep*?3cE]p:\;Bfftn#;?c6[Xhe-[oNk<l>MBX5lpf-q?f_-(C^_O[;+VeX20=0huh/7o<p\'`A1-U<]cbRY2EIrWn=uTJ8"]5d5ZR7`f-$%CGCX"&,C@h$JgQR@5u;jE:-;.n?f\2?Za,"'D%/PKm@IBkaVoo5lKP^o1%rnqY-`DQl<a(_d8"YJ%E<HXW<o@s=T^BlP[A_O"BfO.Sq[J"#Il4*i)#+As,qJ(+a2'1[Z10uE1FG*[)n)FXEi$#T*<6m;E5Fi\9:+c$o:71N)pj0Fr(3%M$`8ROTm/FrgW2GNc+l7u-$=GddP7_'bB8UP?slMS9+*gU0_EO$SMUI(AsYIYe>o637^@'"%ao43ka$"*&dqKohCp>[O^]is.f]<D).Nm2f-B^`pFGO1_=*s@WYD)%NZ@)Bu^L0-Cg\[C&=%1WW:fkoqo/C]c8@=bg=0C15p/uMPTdjK,6S,:2cB3""NF8)Xc<q[pTCpX^^/H62ipR6PMNdC-uV<[@na4Y=7-1FR&BsQO6mELjgEq.4RlWT[f(7;W$`sK'o`+nf_<o;6@YN(,[F&b.acCTg=(&G;_Q:!GI0AEqqI7*,FZ-a.8I].@9'pgg?5-9"-aI0\B.LPbYI_&oj+6A;S7m=h'Tha50p_8MCo@OK/..#3J?ie<'ktQUV([98P[b(1>JVg47=bL3g6!6^?/[tL#T5WF5e!;7<#+i-j-^o95$fAQcC[l,$_)BVtKsf#8[UIb<'XSt\q!uu2]8qH)FEo.r_##32XoWG~>endstream
|
||||
endobj
|
||||
18 0 obj
|
||||
<<
|
||||
/Filter [ /ASCII85Decode /FlateDecode ] /Length 1812
|
||||
>>
|
||||
stream
|
||||
Gb"/'gN)%,&:N_CD#KGDWJqH38:l:]a0D2h1JZsi8iiNS%ho1=N-UOW88A1=$^(SUMe(L<1MNbQ`h\p9m[hC,pOEror6X_YPQJM^_lL6\."hL)-mp#2B5lY8Xt@LQk&V>p,A\`85QXIU5r#f'E'<1a/>+,%T*V%k(R,1;#[fX<pSe[WK[EGN!qAG.(EA7(Xs^kDo,ZgFL8HtN4YO<.^[Y*5pB'>3hP$G3(R<qbg41q^*DUG9qC_D=kl()Oo-p3j@p)9b4AXn!.V(A5E64IX#H)nRAJph,%cKt(AWfhfRAh"LrrW.cg/^^.G?i!*gV9`f9U7pa317bI65EY1hXb\+B-Hc)U(8*jV'_6\T),l<N`I$oCEm^W(`D:OO*cslbU)qaOX1e7M/@=]OYP7Bie7.LlUosoR)h$<]V0pTXicG$*#H+mF??iN@iSSX,'7dLT3)f"de9AO'\UJuAD+($$2uVR2NnT/%e@]0=B/8`:VHJ*hWj1$ZI^=qacF`9"bb<S0``!]3l2W^RN6S&L!Vm)\$$hN?DTk/m&kf_Le@hDg`-+I'O_bO?s;V`7Ol\>\EqIO0hl-$]ZV*%AVDLJP82[[3(V;Sk1Xo)*`Y%C+&t:kb&%*4c)t'QCL!3MXP24bAIpQRG,:Fk!jS8VTs^H2e(#>K_UD\&<HWVPXRqY0c%\$Omcpj\['&D'e_agE]q8eW1)jUuRHCaJ)Ce+V"20anWAki`qH:LTZFW7;%\]79Ei!7I`)a+K%C!=-%'Y(Ye!mA,>g1&a_DG4a(>1mI@@XG#j_Y0cZN2KCBqPq`1:l1p$[FmnYu?<0SL"Rpn+BA?$3kO8UGS5><oNQeS9uhHq$&hS<cF?S<3m2]/M09M\=dDuW231ld(o8<an%BGfiAZr)^&a"L%m'O/6<B08\T_k?Q(:E-)_,&p)ou;.%2a"#4#<_H5f+t)UaZi_MBOIL=G:EN=WL7RGQjWDem%[O.0],3b+9s2fgfIdi=(qf^4dNR;3hlMPD:',cpEgWPs;#AqWX>=<.2V:2E@R2L<=([bm@+jd=,(8,:*$T$&Wa93FL]k#W8(<pnFKOR^+W:f[B[dhH/Fc==IdB",3*;\:Sc]CiS"hkFh'mq!sY"BFsYqh[q\-pt<&'$p)`)DR7P^";kD\m;W1RHccMG;Rq!F-5MFYbQ/kGMr.0\2ZZ3H?W'2iHc4IUT+X10csj0,k1.<<5.F-MO2!jG(s+ZYi&2fj^`#;b2mcF;,;In!-+Rj:!,W1f70+*,"/$1^MNMeZH`CL^DAXo:55>!$bA!HIoKk,'m+:_1laSIkW&9Y]C.^32r\C>.m#X2_93h'N<4>rI=J)W8F]uCKYCs^.HI9lPXE+)&`hDr3"!",*P0hQUX5Q=4>-_\^XObCj!'hXKYp`UkBTq0PBFg,I7&6T6?gqF!+a8U(`)dV>3*Y!_j(TIO(C'$]XP-GO+a%6s.>%\l"TqV+$L,U*M8t;rsa>$j%dEap[9FJ*hAh!=`rBO/+Qs4_E@fQ6H5rs9=_Xn[[)3cZ$]@E&U!tR#((L>o[-Q@`OXNGThcJ;Wu7t;pf)A&s0jKWH'&VM<CeeZs6fIW0$['8ZP&$e$TX:0iZV"QHV*\?-FjZ0b0!FA]'UNUl5fICN39C+cE\T5N+JkC=%K5.**Xhh:G3gQ8U(F0:F%0j0aL"\,#g87o4R/S,-dBkYKPdqi\7u`I^,6a;Jb[20T$NH2J9Y\/du?31'A>io('cbj(.I9Fg`;aV[hi4,0<J\^qB"<[ZI8[)Y\2bC+@]malmr@qnIkg-8@@R,sJKC$.V9j=o>X9.tI:O'q_3^~>endstream
|
||||
endobj
|
||||
19 0 obj
|
||||
<<
|
||||
/Filter [ /ASCII85Decode /FlateDecode ] /Length 972
|
||||
>>
|
||||
stream
|
||||
Gb!Sj9lJ`N&A@Zck"*pDn_.c*l(q##7#_ngM*fZ!(Lmuq@!:e;J`P^4CkqJ0R^PhXJd"JQBAT,'o>VaiHLpDX*WqpUTQS)%]?odE(JI*W$I1_mmg\C`DCBm?SohWR$`r`\0/.*-JKM+hmC##G-Y+tH+=(St[O&G*/e!"2#,-P%i5!4F$)IVK\KAn#kIeh:f;6uKq*EBa56$F7QLNTZGXP%Ul\gAB5PQJ&AQ2GZL\mbUSM;rcJ%<1E:HCo`&%'X@CLJn<'[V!Y&5Fk8SQMB7!Yb?b1X,N&N2ejhc4E%WAZiH9_Xj2bS8^:`'jBN:I%"YjNb282]oLN77Gt?Q\tPkd+:[$*PJ/9?)0FW4DV(\a[Q%8kggsD"k9YAY5'!;jCWIM--.^Y$]80TI]%Q1R\,gRiUrSnL\`@P:TYC+&kF9<&77n^A3/uopC@kIOE0=+=5O?Wcno7uregf:XBQ9DRD0h"`!])[HI+n$6ED&/9LR>atZ/=\$Nf#BADA*c/UB6/U$"D=Gg<"cLKctCO_@GXqY]nQ;=XL6VM@p'PFWIp*9a]-LN/J,/>W#\tQS@$'eBdf8D^e`YjiIIF^904K!Yn-.=IKtWBs4qCMf$ENHPkqLHdHY?K;e-%(GLa]OG0@EZMeC8?LshlGdVkZ;DQpEQDq58BlnqQ5Bm84NTK(-Y^h1G?f98VShHX!4aVW@Hfn\\k6c=WU7%-6m"3rZ5i6R2D$'[ojVk\NogGoM?)n.Y[9c5lg>FO"3LSnQf+6tqK<=H=RcZ`<jJ/I^k1O]ACG;IUd_;)=o^Hj6"o\toGp_uY'<-3V';LI`*a7*q9>qO]80qr49"YO!HK?[W%S_mBj>s]qK!VG]HK28B&h2$8Er=NPkn6S!<T>V:[/`?5@6[%lYNH0?%XX4<HHr[bm^*kHYB$aq2/.='Bn&2i,#*)U%*l4(lRWTfg3iTGfVO_Y%hdoI;HU@2)&b^>F#s%m~>endstream
|
||||
endobj
|
||||
20 0 obj
|
||||
<<
|
||||
/Filter [ /ASCII85Decode /FlateDecode ] /Length 2107
|
||||
>>
|
||||
stream
|
||||
Gb"/'>BcPr&:WeDbY,Fu[S`+t+M(??R^Un5[Vj[\m[fV`H8&B9@R((#h<_?&&R`EK1n42QA&mB#D_BIc1BRdo5IC=t@XsY+*5/16)>%itEl+N"!G?i@c61>>0"6C6_R)E0`3](XA0W<"gEDTt4:8rIM/YIo6&?/E!/C)MQb&8M\9VLZ+Kf4cJccV7G(d1[h]`P?.%1h(EP0it[=1HCk7OWC%b7Yp#_jR;D,c.&EFMi#[@n\[_8f>YcJcGTaZLCj'u<n4a^AAh2.J6YT-#-;H'fCg#]Jct7OjC/;Z$n#>NiiKZ9%d9Z3e!6G8^EIk!1MC57R4pbU+'BcQ@jt%o*An1HKZ0jKMfdf+c0OO?pP.j+`8Z&JfT#"0o[Y;Oo`?,(mGE1qDi*A"k$`$$@9>]ji_8"@dGC:T0pc6T)>8*9Z#%?sBuVdjVH1&YO`6o>mU"PdV+I#0:VV"GYTE&`H0RA3IIXULSRWN/_jVkte_\7rS(_.<hs_7(kfGCZZ5`jGXD<Lhd<$P\YHn=VjCT2m48apeH\7&[m_G)6p/r)g;Vg6m^;%Xq+K8KIW*>])Hiq:2'@&@l_e/\22A"nNmC9f.9p09mA;Q$Ugqjm+=KBIF6GG_pYUnZOHY^C[/K(.X%)qKsW9]>9do(Y_$Rtd9"jW)_Ki29DmA0K.9jG_N@NB8W_g:R3`K+HfsV(:T7"t5KH0k=7@gnhG4B[J.T$GaNmM^Yf+k/`Br.N>f!kY,+4.Q8i-W9*iLnh7d,\iB7KThp9%.Cc;21rpNg12*e-/%REUc=_C1Sh:r;#^1H_Hg/*G$F?=N'Co?otVUV(,Sp/6Amd8W@%d<PT+M'#D\R@5Hi36&("keOfmIPK+.;6I2+?Gb^-\5-\MgGqLG2ug]OI'[T`Vg4tF(ol4!':=J;I$%PSUGZUHY:.pH,hoXraj:"MUE>_]K]fGuUUNHI+.Xbk(,m-%lq9=#8g.>*81se>'AKc.S)J7G?CO9UE^T)E<eK"."/AGAh"FpVhiS5!ob>58a]8k]=kA)(Ua@iU6ITN0.Z\"PRjtqZ]E!o<P:R]5M`gY7\V$0;-aAUn^bY(mQ;oaDBr6X6TEsZRRE(r<4R3%1n].'lk,`dsl#.1F[7tuC%Psr=?"r@k/E.2ELcs%XX*!RkVMC,I@QYYNbuH[4M:=XP$:8bTY@iehQ1Yigfafe(EL)"Y'5ZhEX>_;gfa)H]?,Ff]5U-0/ed;p`N#]/o6V8;<[V0])8UK4F&lX%u^f[V0nnHp>c;ZW"AO5Llgejn)7*08S_p>q5G+@]N[S&hq]P5aYgf9BnI+&qll-:;d=?/!G@?J#0flJ28m'==7I%sPcE7m]2f<Y0lU?O3hec[l]oBL$*@a>Z^j"2nm9Ipp_s(o^LV?q.bcJ[7>k3\8P]Fs6AIYii/55oD91G?mU9_)mEB;(i2)W%Aq%;Tf%LN:Z)4i^,=p[fHtRgr_+&*,jcc-,TC:!3;6R;o`4Adukjai*JSFDkr0jsRuIkEeL>\u,a>VjX#;Jnc::9oeV,cegR">+uO,S46CcA00BbCS<8L_^9XdhH!um30r<,.8?,Bg]LeWghKBfL4*(sI^B?[)G89"0MO.6j8U\HGsX1m8VFVU),J#\'1aWmq9C*@bSuk(gBc5o&cAo)3O+Aegr26?RbC;Be.U#V46,!ZN\JWlrW%;W$lojE'W1`'+jNEbY#EG";(?cQin0OsF6O.0eR9C9Jr["2V.[3&2B'Y@(i"q/JA);.^-5"DX?JS4H"I@"i6fOVAj*$f2+r?"(rMt!+>KH2XfpE2rq0N.Et:h+p6JV$0!FN?MPD4t$r\2!)fh3ZXYcDe?aBMPAMh;P2,S@]R:^n)R>+S`@Tj)Pg8!q=eS.a#2n[F98VjW/cnAtJah[YW70B#fb75@3OJlSiFXS036E-j%^5q>2W$n@X.HeilBbUbr.C7Xq]Jn*UM_W:G.`J%:jms5&VJ`O"P%/m(=S7ZkThH\R%mKR==R1m2%qY;=k.Z:u9Fi#Z@BtKAoQZ!s=J/>YP;EJLOSD[P+X=2/@8T3JN)VJSpO6Cb[0Z)72i,so,%XeS2,3J>F,1QGL%k25k21[*gZ7<^%E2'\f@m8l'9%DALiJ>9~>endstream
|
||||
endobj
|
||||
21 0 obj
|
||||
<<
|
||||
/Filter [ /ASCII85Decode /FlateDecode ] /Length 830
|
||||
>>
|
||||
stream
|
||||
GarnTgMWKG&:O:SS<li^g2(rC;lQde2JF@<c)#<X8pS6,->N]V!BX2n^9cbMNh[N2H=5mZR@,i<OtY?Mdh4X0iq,Q'3LJ*c4Q0^0_>/3FG9i]o#sBBINMILT&@$##B_#"(57g-I`!VV8721(;+sis3No4k](oe,V@=C=:&7H^!aFf;5+%3r7GZP$._l5fX#=3_S^You3HEd`6m$QpS1DkJ;9i5+of+V'XToWoEUEA+^=1C(I[HXG@"b(S'V2lW<*%/]"l=A\Y]'Uo`2d0-glB8184f/T\O);$_I*_J>kK<9Ore+H'(RHZ.5#JX+E,UnE<sc`,TNc=!)lA2LLrbo(%td*]Rh:FL94e?I"4@ZnTkiC9N[j"A_9c_LJdq.'moj59q/3DmIAAql8_6H!9V3]mm9l0;FpY74EkFPn%BTL][rJ\Y9_n>66Z.:+VsO5VGf9&\<24Y:AT);rZ&Bp1>Jq$;,3Es.=YC:GY^,2R@]_p,Z>\YTp0P87(Tt*0m7W5"BA`Dgh4('*kh;`0?K\4&/3IJEr\&F79m,:cA$l'E_0^7pB04lhOeW-rLA4nMWooh:KXrmLh6g?(e)2cKQ[*(Agl?C".N>1h:@].Y1WlNBH(W/e%S.7BES]CnA+;Igm^/sTBgU7'Z_/8NQu?u--#u?j@AUSf%^GH$8Z=s`neTPt;.2am2qeb>O.Vije(h%90:5k.JtAq[5T.u!L#G1o%RDbmMAPIRAodi?PSn(ZJPIjRW`n%dT^!aO9c]Cm!pM0rjCAV#K^fFb5N-MX*?RQ.N3\C@[/s]03AW'85[Q5^L1/uXX3Ir^Fk^7?r<"3n44O~>endstream
|
||||
endobj
|
||||
xref
|
||||
0 22
|
||||
0000000000 65535 f
|
||||
0000000061 00000 n
|
||||
0000000112 00000 n
|
||||
0000000219 00000 n
|
||||
0000000331 00000 n
|
||||
0000000436 00000 n
|
||||
0000000641 00000 n
|
||||
0000000846 00000 n
|
||||
0000001051 00000 n
|
||||
0000001256 00000 n
|
||||
0000001461 00000 n
|
||||
0000001667 00000 n
|
||||
0000001873 00000 n
|
||||
0000001943 00000 n
|
||||
0000002236 00000 n
|
||||
0000002334 00000 n
|
||||
0000004761 00000 n
|
||||
0000005888 00000 n
|
||||
0000007646 00000 n
|
||||
0000009550 00000 n
|
||||
0000010613 00000 n
|
||||
0000012812 00000 n
|
||||
trailer
|
||||
<<
|
||||
/ID
|
||||
[<9e0017c2fae42cc449cacedd8dc86476><9e0017c2fae42cc449cacedd8dc86476>]
|
||||
% ReportLab generated PDF document -- digest (opensource)
|
||||
|
||||
/Info 13 0 R
|
||||
/Root 12 0 R
|
||||
/Size 22
|
||||
>>
|
||||
startxref
|
||||
13733
|
||||
%%EOF
|
||||
@@ -1,186 +1,248 @@
|
||||
# Les 13 — Lesopdracht
|
||||
## Pokédex opzetten + eerste deploy naar Vercel
|
||||
# Les 14 — Lesopdracht
|
||||
## Bouw je eigen Cursor + Vercel flow
|
||||
|
||||
**Vak:** AI-Assisted Development
|
||||
**Opleiding:** NOVI Hogeschool Utrecht
|
||||
**Duur:** 30 min in-class
|
||||
**Status:** Tijdens de les afronden — daarna huiswerk uitbreiden
|
||||
**Werkvorm:** zelfstandig na de pauze (±1,5 uur)
|
||||
**Status:** klaar = productie-URL + PR-URL in de chat delen
|
||||
|
||||
---
|
||||
|
||||
## Doel
|
||||
|
||||
Aan het einde van deze opdracht heb je een **werkende Pokédex** op `pokedex-{jouwnaam}.vercel.app`, gebouwd met Cursor Composer, gepushed naar GitHub, en automatisch gedeployed via Vercel.
|
||||
Je doorloopt de hele cyclus die in de demo is gedaan, maar nu voor je eigen project. Aan het einde heb je een live Next.js-app op Vercel, twee features in eigen branches met preview-URLs, en één Pull Request gemerged naar productie.
|
||||
|
||||
---
|
||||
|
||||
## Vereisten
|
||||
## Wat je nodig hebt
|
||||
|
||||
- Node 20+, pnpm, git
|
||||
- **Cursor** geïnstalleerd (cursor.com) — Pro account is fijn maar niet verplicht voor lesopdracht
|
||||
- **GitHub** account
|
||||
- **Vercel** account (signup met GitHub kan)
|
||||
- GitHub-account + `gh` CLI ingelogd (`gh auth login`)
|
||||
- Vercel-account + CLI (`npm i -g vercel && vercel login`)
|
||||
- Cursor (Pro of Business) ingelogd
|
||||
|
||||
---
|
||||
|
||||
## Stap 1 — Nieuwe Next.js app
|
||||
## Stappenplan
|
||||
|
||||
### Stap 1 — Project scaffolden
|
||||
|
||||
```bash
|
||||
pnpm create next-app@latest pokedex \
|
||||
--typescript --tailwind --app --no-src-dir --import-alias "@/*"
|
||||
cd pokedex
|
||||
cd ~/web # of waar jij je projecten houdt
|
||||
npx create-next-app@latest mijn-portfolio
|
||||
```
|
||||
|
||||
Open in Cursor:
|
||||
Beantwoord de prompts:
|
||||
- TypeScript: **Ja**
|
||||
- ESLint: **Ja**
|
||||
- Tailwind CSS: **Ja**
|
||||
- `src/` directory: **Ja**
|
||||
- App Router: **Ja**
|
||||
- Turbopack: **Ja**
|
||||
- Import alias: **`@/*`**
|
||||
|
||||
```bash
|
||||
cursor .
|
||||
cd mijn-portfolio
|
||||
npm run dev
|
||||
```
|
||||
|
||||
---
|
||||
Open `http://localhost:3000` en check dat het werkt.
|
||||
|
||||
## Stap 2 — Eerste feature met Composer
|
||||
|
||||
Open Composer: `Cmd+I` (Mac) of `Ctrl+I` (Win/Linux).
|
||||
|
||||
**Prompt:**
|
||||
|
||||
> Bouw een Pokédex-startpagina op `app/page.tsx`. Haal de eerste 151 Pokémon van PokéAPI (`https://pokeapi.co/api/v2/pokemon?limit=151`) als Server Component. Toon ze in een grid van cards (Tailwind, ~4 per rij op desktop). Elke card heeft: naam (capitalized), pokeapi sprite (`https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/{id}.png`), en pokemon nummer. Make sure cards have hover effect. Gebruik `next/image` voor de sprites.
|
||||
|
||||
Accepteer de diffs. Run lokaal:
|
||||
|
||||
```bash
|
||||
pnpm dev
|
||||
```
|
||||
|
||||
Open http://localhost:3000 — Pokémon-grid zichtbaar.
|
||||
|
||||
> **Tip:** als sprites niet laden, voeg PokéAPI's domain toe aan `next.config.ts` (Composer kan dit voor je doen — vraag het hem).
|
||||
|
||||
---
|
||||
|
||||
## Stap 3 — Detail-pagina (optioneel als tijd)
|
||||
|
||||
**Composer prompt:**
|
||||
|
||||
> Voeg detail-pagina toe op `app/pokemon/[name]/page.tsx`. Toon: grote sprite, naam, types als chips (met type-kleur), stats (HP, attack, defense, etc) als progress bars, en height + weight. Link de cards op de homepage naar `/pokemon/{name}`.
|
||||
|
||||
Test in browser.
|
||||
|
||||
---
|
||||
|
||||
## Stap 4 — GitHub repo
|
||||
|
||||
Maak een nieuwe repo op GitHub:
|
||||
- Naam: `pokedex`
|
||||
- Public
|
||||
- Geen README/license (we hebben al bestanden)
|
||||
|
||||
Lokaal:
|
||||
### Stap 2 — Naar GitHub
|
||||
|
||||
```bash
|
||||
git init
|
||||
git add .
|
||||
git commit -m "Initial Pokédex with Cursor Composer"
|
||||
git commit -m "init"
|
||||
git branch -M main
|
||||
git remote add origin https://github.com/JOUW-USER/pokedex.git
|
||||
git push -u origin main
|
||||
|
||||
gh repo create mijn-portfolio --public --source=. --push
|
||||
```
|
||||
|
||||
Check op GitHub: code is daar.
|
||||
Check op github.com dat je code er staat. Controleer dat `.env.local` NIET in de commit zit.
|
||||
|
||||
---
|
||||
### Stap 3 — Vercel project aanmaken
|
||||
|
||||
## Stap 5 — Vercel verbinden
|
||||
|
||||
1. Ga naar https://vercel.com — login met GitHub
|
||||
2. **Add New → Project**
|
||||
3. Kies je `pokedex` repo
|
||||
4. Vercel detecteert Next.js — laat alle defaults staan
|
||||
1. Ga naar **vercel.com/new**
|
||||
2. Klik **Import Git Repository**
|
||||
3. Kies `mijn-portfolio`
|
||||
4. Framework: Next.js (auto-detect)
|
||||
5. Klik **Deploy**
|
||||
|
||||
Wacht ~60-90 seconden. Vercel toont:
|
||||
Wacht ±45 seconden. Open de productie-URL in een nieuw tabblad.
|
||||
|
||||
```
|
||||
🎉 Your project has been deployed
|
||||
https://pokedex-{jouwnaam}.vercel.app
|
||||
```
|
||||
|
||||
Klik de URL. Werkt? Klaar.
|
||||
|
||||
---
|
||||
|
||||
## Stap 6 — Test een tweede deploy
|
||||
|
||||
Wijzig de titel van de homepage (kun je via Composer):
|
||||
|
||||
> Verander de h1 van "Pokédex" naar "Mijn Pokédex".
|
||||
|
||||
Push:
|
||||
### Stap 4 — Vercel CLI koppelen
|
||||
|
||||
```bash
|
||||
git add .
|
||||
git commit -m "Custom title"
|
||||
cd mijn-portfolio
|
||||
vercel link
|
||||
# kies scope (je persoonlijke) en project (mijn-portfolio)
|
||||
|
||||
vercel env pull .env.local
|
||||
# trekt eventuele env vars (nu nog leeg)
|
||||
|
||||
vercel env ls
|
||||
# zie wat er staat
|
||||
```
|
||||
|
||||
### Stap 5 — Cursor configureren
|
||||
|
||||
Open je project in Cursor. Maak twee bestanden aan:
|
||||
|
||||
**`.cursor/rules/general.mdc`:**
|
||||
|
||||
```markdown
|
||||
---
|
||||
description: Algemene project-conventies voor mijn-portfolio
|
||||
alwaysApply: true
|
||||
---
|
||||
|
||||
- Gebruik TypeScript strict mode
|
||||
- Geen any-types
|
||||
- Tailwind voor styling — geen CSS modules
|
||||
- Server components by default; "use client" alleen waar nodig
|
||||
- Imports met @/ alias
|
||||
```
|
||||
|
||||
**`AGENTS.md`** in repo-root:
|
||||
|
||||
```markdown
|
||||
# Mijn Portfolio — Project Guide
|
||||
|
||||
## Stack
|
||||
- Next.js 16 (App Router)
|
||||
- TypeScript strict
|
||||
- Tailwind CSS v4
|
||||
- Vercel hosting
|
||||
|
||||
## Conventies
|
||||
- Server components by default
|
||||
- "use client" alleen voor interactie
|
||||
- @/ alias verwijst naar src/
|
||||
|
||||
## Lokaal draaien
|
||||
\`\`\`bash
|
||||
npm install
|
||||
vercel env pull .env.local
|
||||
npm run dev
|
||||
\`\`\`
|
||||
|
||||
## Deploy
|
||||
- Productie: merge naar main → Vercel deploy't automatisch
|
||||
- Preview: push naar feature branch → eigen preview-URL
|
||||
```
|
||||
|
||||
```bash
|
||||
git add .cursor AGENTS.md
|
||||
git commit -m "chore: cursor rules + agents.md"
|
||||
git push
|
||||
```
|
||||
|
||||
Check Vercel dashboard — nieuwe deploy verschijnt direct. ~30s later: live.
|
||||
### Stap 6 — Feature 1: Hero section
|
||||
|
||||
```bash
|
||||
git checkout -b feature/hero
|
||||
```
|
||||
|
||||
In Cursor (Chat + Plan mode):
|
||||
|
||||
```
|
||||
Plan een hero-section voor de homepage. Specs:
|
||||
- Gradient achtergrond (subtiel)
|
||||
- Grote titel (jouw naam)
|
||||
- Ondertitel (jouw rol)
|
||||
- Twee CTA-knoppen: een primaire en een secundaire
|
||||
- Responsive
|
||||
- Tailwind, geen custom CSS
|
||||
```
|
||||
|
||||
Wacht op het plan → Apply. Eventueel inline tweaks (Cmd+K).
|
||||
|
||||
```bash
|
||||
git add . && git commit -m "feat: hero section"
|
||||
git push -u origin feature/hero
|
||||
```
|
||||
|
||||
Check je Vercel dashboard — je preview-URL verschijnt binnen een minuut.
|
||||
|
||||
### Stap 7 — Feature 2: About pagina (Background agent)
|
||||
|
||||
```bash
|
||||
git checkout main
|
||||
git checkout -b feature/about
|
||||
```
|
||||
|
||||
In Cursor → **Open background agent** met deze prompt:
|
||||
|
||||
```
|
||||
Bouw een /about pagina met team-leden in een grid.
|
||||
- 6 team-leden (mock data)
|
||||
- 1 kolom mobiel, 2 tablet, 3 desktop
|
||||
- Per persoon: foto-placeholder, naam, rol, korte bio
|
||||
- Stijl matched onze hero-section
|
||||
- Header bovenaan: "Het team"
|
||||
```
|
||||
|
||||
Terwijl de agent werkt, doe wat anders — bv. tweak je hero. Wanneer de agent klaar is, review zijn diff en apply.
|
||||
|
||||
```bash
|
||||
git add . && git commit -m "feat: about page"
|
||||
git push -u origin feature/about
|
||||
```
|
||||
|
||||
### Stap 8 — Pull Request openen
|
||||
|
||||
Op github.com (of `gh pr create --fill --web`):
|
||||
|
||||
- Open één PR (kies welke feature je eerst wilt mergen)
|
||||
- Wacht tot Vercel-bot de preview-URL post als comment
|
||||
- Open de preview-URL en check je feature live
|
||||
|
||||
### Stap 9 — Mergen
|
||||
|
||||
Klik **Merge pull request** → **Confirm merge**.
|
||||
|
||||
Open je Vercel dashboard. Je productie-URL update binnen 30–60 seconden.
|
||||
|
||||
### Stap 10 — Delen
|
||||
|
||||
In de chat (Teams) plak je:
|
||||
|
||||
- **Productie-URL** van je app
|
||||
- **PR-URL** met de Vercel-bot comment zichtbaar
|
||||
- **Preview-URL** van je tweede feature
|
||||
|
||||
---
|
||||
|
||||
## Eisen
|
||||
## Bonus — als je tijd over hebt
|
||||
|
||||
- [ ] Repo `pokedex` op GitHub, public
|
||||
- [ ] Minimaal 1 feature toegevoegd met Composer
|
||||
- [ ] App werkt lokaal (`pnpm dev`)
|
||||
- [ ] Vercel productie URL werkt
|
||||
- [ ] Tweede push triggert automatische deploy
|
||||
- [ ] Geen errors in Vercel build logs
|
||||
- Voeg een custom domein toe (Vercel → Settings → Domains)
|
||||
- Schrijf een tweede `.cursor/rules/`-bestand met `globs: ["**/*.tsx"]` dat alleen op React-bestanden actief is
|
||||
- Probeer `@-recommended` in Cursor chat
|
||||
- Test of background agent ook op grotere taken werkt (bv. "voeg een contact-form toe met server action validatie")
|
||||
|
||||
---
|
||||
|
||||
## Tijdsindeling (30 min)
|
||||
## Op te leveren
|
||||
|
||||
| Stap | Tijd |
|
||||
|------|------|
|
||||
| 1 — Next.js setup | 3 min |
|
||||
| 2 — Composer eerste feature | 10 min |
|
||||
| 3 — Detail-pagina (optioneel) | 5 min |
|
||||
| 4 — GitHub repo + push | 5 min |
|
||||
| 5 — Vercel deploy | 5 min |
|
||||
| 6 — Tweede deploy test | 2 min |
|
||||
In de chat (Teams):
|
||||
1. Productie-URL
|
||||
2. PR-URL met Vercel-bot comment
|
||||
3. Tweede preview-URL
|
||||
|
||||
---
|
||||
|
||||
## Veelvoorkomende problemen
|
||||
## Vastgelopen?
|
||||
|
||||
| Symptoom | Oplossing |
|
||||
| Probleem | Oplossing |
|
||||
|----------|-----------|
|
||||
| `next/image` klaagt over PokéAPI domein | Voeg `images.remotePatterns` toe in `next.config.ts` (vraag Composer) |
|
||||
| `pnpm: command not found` | Installeer: `npm install -g pnpm` |
|
||||
| Vercel build error: TS-error | Composer: "fix de TypeScript build error" |
|
||||
| Cursor Composer niet zichtbaar | Cmd+I — als niets gebeurt, herstart Cursor |
|
||||
| Push naar GitHub: permission denied | Setup SSH key of gebruik HTTPS met PAT |
|
||||
| Vercel deploy mislukt | Check build logs in dashboard — meestal env-var of dep-issue |
|
||||
| `npx create-next-app` hangt | check internet, of probeer `npm cache clean --force` |
|
||||
| `gh: command not found` | `brew install gh` (Mac) of `winget install GitHub.cli` |
|
||||
| Vercel detecteert geen Next.js | Output dir leeg laten in import-scherm |
|
||||
| `.env.local` lekt in commit | `git rm --cached .env.local` + check `.gitignore` |
|
||||
| Cursor rules werken niet | Check YAML front-matter syntax (geen tabs gebruiken) |
|
||||
| Background agent reageert niet | Vereist Pro of Business plan — check je Cursor-account |
|
||||
| Preview-URL geeft 404 | Build kan nog lopen — wacht 30s, check dashboard logs |
|
||||
|
||||
---
|
||||
Vraag in de chat als je vastloopt — ik kijk mee.
|
||||
|
||||
## Klaar? Verder met huiswerk
|
||||
|
||||
Het huiswerk bouwt voort op deze setup:
|
||||
|
||||
- Feature branch met **Background Agent**
|
||||
- GitHub Actions CI workflow
|
||||
- Branch protection op `main`
|
||||
- `DEPLOY.md` met reflectie over Composer vs Background
|
||||
|
||||
Zie `Les13-Huiswerk.md` of `Les13-Huiswerk.pdf`.
|
||||
|
||||
---
|
||||
|
||||
## Tips
|
||||
|
||||
- **Lees Vercel build logs** als iets faalt — meestal staat de oplossing er letterlijk in
|
||||
- **Push klein en vaak** — kleinere commits = makkelijker rollback
|
||||
- **Composer is voor pair programming** — accepteer per file, niet blind alles
|
||||
- **Sla `pokedex` url op** — die heb je nog nodig voor huiswerk
|
||||
Veel succes — en wees trots: je hele cyclus is van leeg project naar live productie.
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
%<25><><EFBFBD><EFBFBD> ReportLab Generated PDF document (opensource)
|
||||
1 0 obj
|
||||
<<
|
||||
/F1 2 0 R /F2 3 0 R /F3 4 0 R
|
||||
/F1 2 0 R /F2 3 0 R /F3 4 0 R /F4 7 0 R
|
||||
>>
|
||||
endobj
|
||||
2 0 obj
|
||||
@@ -22,7 +22,7 @@ endobj
|
||||
endobj
|
||||
5 0 obj
|
||||
<<
|
||||
/Contents 13 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 12 0 R /Resources <<
|
||||
/Contents 14 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 13 0 R /Resources <<
|
||||
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
|
||||
>> /Rotate 0 /Trans <<
|
||||
|
||||
@@ -32,7 +32,7 @@ endobj
|
||||
endobj
|
||||
6 0 obj
|
||||
<<
|
||||
/Contents 14 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 12 0 R /Resources <<
|
||||
/Contents 15 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 13 0 R /Resources <<
|
||||
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
|
||||
>> /Rotate 0 /Trans <<
|
||||
|
||||
@@ -42,17 +42,12 @@ endobj
|
||||
endobj
|
||||
7 0 obj
|
||||
<<
|
||||
/Contents 15 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 12 0 R /Resources <<
|
||||
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
|
||||
>> /Rotate 0 /Trans <<
|
||||
|
||||
>>
|
||||
/Type /Page
|
||||
/BaseFont /Symbol /Name /F4 /Subtype /Type1 /Type /Font
|
||||
>>
|
||||
endobj
|
||||
8 0 obj
|
||||
<<
|
||||
/Contents 16 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 12 0 R /Resources <<
|
||||
/Contents 16 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 13 0 R /Resources <<
|
||||
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
|
||||
>> /Rotate 0 /Trans <<
|
||||
|
||||
@@ -62,7 +57,7 @@ endobj
|
||||
endobj
|
||||
9 0 obj
|
||||
<<
|
||||
/Contents 17 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 12 0 R /Resources <<
|
||||
/Contents 17 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 13 0 R /Resources <<
|
||||
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
|
||||
>> /Rotate 0 /Trans <<
|
||||
|
||||
@@ -72,85 +67,96 @@ endobj
|
||||
endobj
|
||||
10 0 obj
|
||||
<<
|
||||
/PageMode /UseNone /Pages 12 0 R /Type /Catalog
|
||||
/Contents 18 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 13 0 R /Resources <<
|
||||
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
|
||||
>> /Rotate 0 /Trans <<
|
||||
|
||||
>>
|
||||
/Type /Page
|
||||
>>
|
||||
endobj
|
||||
11 0 obj
|
||||
<<
|
||||
/Author (NOVI Hogeschool Utrecht) /CreationDate (D:20260607134605+00'00') /Creator (\(unspecified\)) /Keywords () /ModDate (D:20260607134605+00'00') /Producer (ReportLab PDF Library - \(opensource\))
|
||||
/Subject (\(unspecified\)) /Title (Les 13 Lesopdracht) /Trapped /False
|
||||
/PageMode /UseNone /Pages 13 0 R /Type /Catalog
|
||||
>>
|
||||
endobj
|
||||
12 0 obj
|
||||
<<
|
||||
/Count 5 /Kids [ 5 0 R 6 0 R 7 0 R 8 0 R 9 0 R ] /Type /Pages
|
||||
/Author (NOVI Hogeschool Utrecht) /CreationDate (D:20260612063152+00'00') /Creator (\(unspecified\)) /Keywords () /ModDate (D:20260612063152+00'00') /Producer (ReportLab PDF Library - \(opensource\))
|
||||
/Subject (\(unspecified\)) /Title (Les 14 Lesopdracht) /Trapped /False
|
||||
>>
|
||||
endobj
|
||||
13 0 obj
|
||||
<<
|
||||
/Filter [ /ASCII85Decode /FlateDecode ] /Length 2127
|
||||
/Count 5 /Kids [ 5 0 R 6 0 R 8 0 R 9 0 R 10 0 R ] /Type /Pages
|
||||
>>
|
||||
stream
|
||||
Gb"/'=``(P&:XAWkb<s7i>A9+Q=QRp\Z=?_9,:jr`UP@5'9@!H%tWQjs1U',Ou"o)fHj=(;RRlE?[#`1?amt(ifA66F9V0g5!4/5J8s4+";[.3p><`FrSU=6\E#,uJ-\k<b^m^Lr]trH'G*h[@idGQ<"Z6!#W3i1`u6q_;i3$&%3448(_A!mdNK1H,,s\THpK%@-orW9Pk!o65sK5:MhrF\6T+aj"N(gf0Zp)HOAH^l#kh=RgO&\Z%D4<S9TpNaAIa3u[7QE`=E(G$l'qLtVTG:X/E+/8C@\Z6RA.A"q^8Utho_DGLHHhE%b5k&\#'0Q!,lGVX:kj;4upcC"P<kC"+Ji69_(JP.2Yp%,]Gn!rTJgNH)i&e;?*IAjOf7pVp`;IXFSfPqmM/7E"0eiTi&bp>i-oc:b2g+``V#WEl)'W)GFK#_'$tp:le)ZYV`#O7ZQ)/r,B!Uk_HuoJ4iV(em+m8*8O&*Nhc,[r-u.rJWN4_WU3q&WbJ9Bm5uFZ3FT)2g?om)G1T)E)YLuHZgAMZ+q\=RH,Iu@,8Oe%N+$=JI5F'4M)(k9(j%;m7*NT-1!5ms>_#-U.93YIiir14M(>2/KT7Na7ht#]ka9;Y=X=XTI<iC8&R1]B:f`(6[P\4N!+TaP3`qh#0>Bl:,W0s)F_^It_Zb`B90!hAdfD)r,>$^A0*1HHO'`)kUn.A;n_?*rK$aH1[&j9lcS"EnefO^6#EXrsB:k@9a6/\YC)*LFfai(.Cj$lsC3hoo%UFefO:fgjlhY?HXp_sPKtu?cMR?abXI,o2dPR:nn3[D-Cm0%u>f249I2fH3n[_NW_e_P!=dl'NGD."=,rr'M!^$6t=ebL3d3#Quk02>QYg1sTg19u-9:HG2=uidH,#!A&).:@_rI@_S!S^3=O&stZeo?Cu'ne@1B`_uU54/oE:alYE-+J;00O$i4*97;a-#FgMZcZlp9>!5d3o]P*;FX!F-XJF$D^+J"KUJCl>*^!m%t^&3d.'-8\Tl:&`/`]0.F]Q<G`2&K*<u.;g?G$B0mp\G7<$imb:Qph9OgfeRisXXaIFODF6p]IEh`OQ)a1H4Rm,2IfIl&J17pNB+6`=U4HH1<%O>`LKq#5Yh8q0&H^YXfCg&M1qBJ0E^kk?p/\l\,Z$f0X*SS`#E<"Ed0i[86_8lB/-ft15)0kfbq:\\"8ZM&!*16oq?c'rF^6q,gf1fDcZZ9'2NN)<<C>BjS4*:u2.ViCp%itep:Y'Bn9%j]3/f`Bi4:_7W,m2JWlN]p.2,RI">o4P)Q^A:nI:W^2csO-cALOgP^oZ6S\j"@&R)*L8-[f51UMOh3I+f6nA(&0ci<(FlXu(0)(lSO(GM&:L].m'3Yp2[@a6,Qmpm1`Be,`Do`>08Pf:I:7LWRi#M<.+LYurlecI]:HdlS1(7BU$cfGWZUN>7Jum*jCKYl6FshC&)(9nJ)YNkut#Ub?B+iM3.bS,8'NC'%c28"j/e)l.lB.6/`E$_?Du\S&aC\hN[4o(BD[QZ](a\Q<5OF8Ff<<(443Ls%DGs*@RUr-X,d=Vp7!^+5QD*9%fQ+>P2K7oR+f=jaTJSn"J(KZ,M0As'H*/acnR^TUI_^AXh.)qs25jHfa9R[3848iuDJUK]G>LFeo/n2i2=7;T9)@U7ZG18G7:9o9tWH!r$2a:Bbrr"mc<.adj[9_;H"=89)%VY;jP4u9EU^:!#)9C8ajC.F(!B@7e<=f5XAiNYXkG@0D7U#.c_ZNR\@Q+"^7e5#XA^1nrNUh\VYa"`bk#-!Df1('p,%+=Qt<Hgj^K.AhgEZ2G/K^oT<(J=#8C[/a7QXsGiHQ/p8FdVX"nW,b;"8D3=b0aLd0lH6nLiheuG26t(K![EBmc4a\YW8l;P8me$4'%]P=R84F)t=%HpO_uX&7@\?^K7:(l`=i<,K]L4XDRJ>Z12"^o;5:GbH/]QL1P%X;Y)GRi6INVp[;?>MrU8Yj2Fbo\f>k*I+%X<UX)7lY&[)Lj)-IF78pZg;rT]KMYAt#U-HCR7,MA3<mdeOEE@7YF/WboNZ0'.]W6!i2%"X<6`q/6Y'm`^5'XE!J/>QLP8(p=4PZOTf(mT/T<mP)gT4M,>MRuiWGS+=.<!WD:N#%@BMQ0bEWa.m?-mRY~>endstream
|
||||
endobj
|
||||
14 0 obj
|
||||
<<
|
||||
/Filter [ /ASCII85Decode /FlateDecode ] /Length 1391
|
||||
/Filter [ /ASCII85Decode /FlateDecode ] /Length 1626
|
||||
>>
|
||||
stream
|
||||
Gb!#Z95iiK&AII3b[jX!88,_*8HF\mg[94e/@sNcD(iFR&oB.b7'37E5n<eH4/<ou*_gN@,KCL4br;En50f5j`JaQ\ZikWgK&oK,N5KB>9G.Z4cgY?XHj^3]4H^j!&Q=g2+Gk&ectn:@6:Fkj'*^1N8/t3_0dC\.`5'bQMHC"@Jb=#uJ4,8aYlk]G:rV^f]c`2RF=g(_<:\OfLbEr1,t&3i@@6uu_=.ah][VTa,bZcJ6<KMWq!H73(\#Oa9Ufq%/`=sX1$Q-!/']A!^GnApgUuoI#/?P=.;W:YW]Pg_%BPS\Y8P(8P41k/O7>H((75(ACn3G0K"fa(][FT.=C3ed,FIIXYY$0oWDFc\!F_3=$BmB-II^[l!mEFY+g*1h"u;)4KMQAP+Cj[F%ndQdMGQm>q+<b5P6TJS6SbGKbX5*?+]2,E(r7(FmN7lAQrXd`0I[&mU,\bC9FkP'i%#9#+d`<Y;lkWMiGB(6Bs8&Z]%kho@/3Ybi`/ZD4Qp=q>s]1L\,q%2&Y`c2h0XKq#KB_^Bm1m[pSVl+S$)19ZSgsX?K01jY.Z29CLY4^$'@!'\]=ZEFTImrkQ[:U/7dU)Be.j%hV7kf`S'R5@73GS+bp?-mCKkl>:s\Jeoa_c]>S-gkC*T.Ik("Ems[Z;YN&795X_l4eO9jqQtC7Tpt:t@X\elt<Ybdc0T]@"3L_AAI,(]=ZM&E[+\3k<n'S<S/"IK#H'Or9G6p-QC%<40fZ^_7cIQ&e"?mM]j,mbcbdr-G.3)p`$A?I'ZnC<PO^]@m?/uMC]/i!(+[OK+:#;9O^c*)c"P%?lRcY&+a3bQ7?#%T0D,SU$YNEF,c$*!Pqehgg:)IneD@CdFgodM@s*7kY-qYGlq*IdP6L4if3)<IOG:4Z5V(>uW>6;Rs<`%Eh$LI,Amh`ZmOWk)Fs+A`Dpq3P5rT"B6cA3t"TpjoQX!9St'a4!iA4%BgIp=hmP,PFm6pC<_0cu/?Pl#APYhaAJi-_C2=F@)F[;O%FL+dACMlgj.1I3H1HAgU)s,/B^D"e+uqpb)aLE'OF[FPf_=+O&)&hDNaaEQ5?.j&m0\hMcddgjeMY?eb0WtHjZ4[g%lS7"DgKq$;5^LM;SL60\k_OT"1__RqH!CWmh3o6cJMaK3A.`_Qs66/s]6\mtUiIUutqD_F+_V`C1"n,-M4aS!pI1l'H-?pSg\Hmu^%.t3kd@K9m5CQG$YYcq"p>j[OC!8!2M6"t%m`Y@G24elVkF2'VWMcMdI[/NV2s3q"'o$5iPe+mUjP;Aa`As7T^>u[ujedYsB;R_@]eu:aj8@7jAkVMN[K\RSFB?VhPuR'sK2%Bu?(f/oZbmlk]L-bp^^ZSOMi)W1)b!2ds2D_"H@qKp_uK;<i4f$qJq!d_"+6s~>endstream
|
||||
Gb"/'D,8nW&H88.1"&WnTYu*,mmnf@B^\QI$AF^nad6SP8K;QM]3Zk]+7?VqfSQ/LPCa;OWR>4jahGtTo??RFfE=m\8G/T&!%1,nr/1QK0H:2"%3(&bn6suHhBT(d/iDF7"p@'R)ST_8\.>JHl;GrO^p&48$7HaB<W7N;oM?lK?sFNq#XO%:,m:=MqWB%m1`0*T!Ig>;W#2/l4W)IP\6g`T"g;c8&C[k"f.!!;]"n"*B#.-J4H[SaAcb@W&Lj4c.[LM!_MqAs`%[55ib=*PbMU\0!GI=TPVaL`Kl*..#$q2-$1r/gh,/l'3N8J9_suF<37"@)M\+a'\VOg_LfD9,@4eF-Oi8LOUNFp"nbM2,$pMgjaSAfX(qs`2OT1W&DaN:)V+TGSIYrB?CR0_&"B#GtJ;b4-786=&e/qPSO95`F6B1\BP-6al7Ga>W(GhrH:WSb/$a(^/pj.(CUgIG.+/6,qi_u`E*fKY.%\Sj@gN7YX3.B4*G\P+/QQgl^--(=ffJrdPKLM4E2*&iNl$j<W8uZ8KZ]YHY+qMKXH!KDA7Uee`-kZh`%MKF0Q6&:hd[Cq*a0"Zp7[@\=CBKT"MLmYpVj5>->grIGi6"$J$hu9%BOd>==I+N^<X7R`oKig#$Zr>aF,frfiO=]@I#9-T$I4m'7q,=V0g>Z.*E`k$s&-?XiY\i:pHmq8#3\iN+quMf_1db*d9o(h?0W;njh!SpD?lt?n5>$k_a$UJ;d#[oUht;_F5.\*"rm&D28'.IE&DLo@37&8S&TWHnRhQTM!+/)dNXE5Leeqj[_Oq2O_spZmmqpZD?l,A=^X^F>9A3m52VQ<lboEeT4,r;0TAdn)"lWFJ/*A;3$"iV2imZURc4E,?]VB/X!:up'=>SW8Of2%1eSsC_$&oDdds>>d!nuDrt!nt+uN/l3I>P->?0ETs&)FM@lPq.-l%#fV5+Te(A*r!.D_mYiF<VZR1_(.DAKlA-"(<-16H0'HU`.`0J.)@P9nT9qegW1'kMgrN71d*Fh(34OE3<$b;alEV5/Z\\=Qdtb9/,<>QX6gOp]\amFbkdeA:YTP+M&dL@5BHH7\L;^0,>Y&(s-L/ohEM2SI?I^L"g)c'W>cJa#.i&lk"'"sL.)6>PQ:C=-4;HA%V7HAr>BGeUpID6AS\CFkLVfO*B@Y'$l@2qG&po%p`4hFB3AX3T]?"I31FkZ2DF_g?qH:dSH\BGX+K93:h4#lbksYj);f;+CGW/:+X9<n<R:NQ.,APsr2c@(&V.n"nAfqZ0r1=VcW($[5a]ho5@Bf8rD,FHB(j0fC6O+>gM1"u9Ki7d#DO,$:i]fQDjIPGi#TkuHX8b;)YZ:!aA]P)rbhq[HZJ`':o\`&1UnVN4o_nL+$g`#ZL"k,tsMg@jM%4A*6>j`PsRI#2,d<`bou-sg;2%&__/*NaBD<gK(S=(CX+cIul9Y4uG+2[^jX0%<eTGI7*'BN:,`)<OO,>,[Iir<@-#YY_eaA,NL1Bs6A.Fu8P:.>ntG3+D\\lf$jZiVrFq6>UgG-2dd\kOlHXSINZ(4\Z!X.,T!fTB]UA\Al@U"76.Opf;c:[EadB%qE^a4I*H@Z:r_`j\4INVh3elaE7)MAKfQoIf/1,HN+%L-0RY~>endstream
|
||||
endobj
|
||||
15 0 obj
|
||||
<<
|
||||
/Filter [ /ASCII85Decode /FlateDecode ] /Length 2009
|
||||
/Filter [ /ASCII85Decode /FlateDecode ] /Length 1392
|
||||
>>
|
||||
stream
|
||||
Gb"/'=``=U&:WfGf^Y/r=qE;mIV?c]SYM$_b/nbj50sufdH21,ZBJnKhqeMIOGXRj)3<4$=;mB,!daq)Ib5qKMB>n!%hjKN-SU"fJ8ps(!%*PUa`BG3=<B-3*UZN=M"%A43=oQ]5:C,E$FW;ATDK$Zl4VF.+?`1VchDtM4P+-s'FI#dYS[r'TroboGg(=nkK80<*6us=,dub.hp-0=o<`j.EE_hXc/sr,E!0s/10TAWPhE2KP6A=$]R9GfSn'WWL@ZsSiOC@:33V7nEu4PBNqUXO.P!B1"*L<s/5K5KmD.ck-4dJcJUY]eiP'E#oflM(0EHF?nnA[QeZXR-PQi67\mFF.G01f]3pT\)#XMbOO'<FV'"BomOH+E*Z+*Ms>9f%XmLi-g2ir$_*@2#5*Bo_?h3cPE1;%$:@p5-0q)H[/_<;Z9).OZM"<fhYh8^;KVlYA=N?W,=KGNsgM"-7?ZCPgK(doEKF0!<Gr\W@oI\cL4"5"c'$^&960hSsr\,_80KkGc*[`QA"&CrE(\(?1[Vq1;MIT=/D`+D2BG=hhh.`g$-)@?_L$DMd02i'HHcLNI-;PiB:SQ4lmHtq$%P7j>Q1@T;J[*@<%g17L4F8KC3jB8Gha:#Y]p6LYCD[mRVk_.GnA/)qu$oZ]APVj:4jU#kB:+YY@:%fN/Ce9]QM7pu,KL;]6mSMnc@m[4<jV!n?[]EJO18"?sXm)][m]q"tn\@[l<W>GHdIk/gA(iH.]e'[bYE+8.^:*Y1B_pJ!+Xt412hG@k88Xc7bY"?a_JSf5/lOQn/chfk'P'3,Gs9N[F-U;#pocMFeDFaUTL`(3,1dK.VdB_h$I0RmP:TB$?rgg'Jhf!R9'nq`"N`8qT6'(t+>+h-7sTpk9iP+c8*@-\'IPuS\(flO5FT#r4(T.+NAa2\XAEI7Va42J_H]Mc,fa9MZG'K#HZuS@2G2;8D'07EBrRF%n)miJoJ5VI$8;k[75*"hZ7;tmgi?22q^:WGP]Z:eOKNCjpMj.q=e&qAO_62l+FSlsO4OqY6`5(#<7C/`L10s-#gOTQeDaBV`\]jugNua9mV-rNUNEM"[Gjs;p+GEadl7\Y@A[!2D[uLPNXj)X:hcoMC=\[`H--^`T!Wo8D!p$&OL0F16D!K[de<'OOR^+W1hiF$L9uqnp<%&eCVus-kD#4ma(rK8U'dVO\tNNuVV!N+V%CjE$)UX'_10A/5u.fn,f.YPJZYh`0FD96eY!4$UM:8L(Ia`,clMpdG;pJIqU6WZ_`<[9+:4e7%Wnm%(J)5aWp$Ce,/UYok#I\5HfHhESM*_E3^<W(jmi5qn_:r?j#.:dCt6*+cN%f0?^0d6U,2A"Vq7gN2Agc-IhM]LM;HngdG#,X^JdS"@^o7RBp;BC9p"SN_Z;#H>-rb6q9i+qj/*FVjs/oCXpVoU7r&h#lt/SN[-]A*96WLU*X>CsFA]5tcFRVAiiGlk/F&[Vksbo>kO0]d^.+'pV*"?,qa`Iqe0U'&Ki^]dVS_VaB]Qip,M%Z?Ic$/KMm2b9;dH(><YPmBX-r'oq7X]sf8,cC?bC6Y@\88lVc6OP?@Joq=kNj6#k2el@tc-k"H+*9$#Cu@oTkU--";4\Lqe"`j66D5)0hhF3;\*BjQi<#47:c(``eW]CcSruodd"k*BNa?PUam68!8li/*mVpGqm,)KW3Z!LY%nk&>.Z7)qLBYs"``Hbg`$.D$T_TF:qkhRNJEH1_SLmUV3c=f9f2Get:WiIY4E+%d!f#e%/,<eeh<6odcuD->U/'o]7W.>j=6J?V,TP"F7_]T&TS`DK0W..+ZLj^&.>hk60YBik5*sSmfn\C\0U1Oi2;PD*JE=9bqd;-42c2@K206/'uqO/eoS;L%m,c1<e>[(j@@Jkj4p4^NY;[WfR]d[P+Aj1f5V^CG7!.GfSnFi0C;"6V\>oL1)+WBbc8T>;WIj(c9o4pXJ"=Wc`0Go7g4uLJr`D<)LEo@rN$UYgmp/>>9sdFVLCC>mJ$MbTC6k#FUDDE<~>endstream
|
||||
Gau`R968iG&AIa;m)0]#1P,oEHtB1A\oI#7j%`L[^%_YkYXF>OJ=tHdf69N<Ua59fN2AHqU9qkQG<^NjCqOHQ).`e4:>k8p!bF'2#0I7-aTHp\=Pk[@53Xf]Ke0%X4,a<Fbu"un'C(rLfY+>RKf3Z6"q>2.Jt__S7:7*oIab"o##C3Rn0isA:Po(]SmK<F!L*7)J1l07$,U19Z$e\j5nIe[d%`;M"5=-M#pHc;E5GkELQGZopIPpL_&uB&*nWr%&Fk:ZLHK$egY\4uh3l@LAK`?Po3JOSe]Afj4+ZidB0::fL\_:&$(;c]U_Y8*&8XJ;#@mJc/$eLbQkY&BZ**bTCkf-U27K17@kGsUp?1S`J["Bo9nIN22qf%1OAY.k(D>#GJ=.I=Ar26f:\7,?p8GJbEOk,V?:DdeOQ/CW-(a-%]h277);`@N]JC&T*S5I.SJW<qVb6$t*6+To'1mXXR:59GkPIaEn>Yj4GaO,HAH@pV6;,C_76W=3V4WZ&.:>7jlY2eUpn]HT(!.Og,HKs2Zb::_mBE5LDgQY`2QPL^X)0mff_St"@!"Jsd1V<L0`QD]-!3HhKnb<"@`B%VJk?EE$/?<,<^eK8@Lb4lR;$>oNeknd86SD*>sIT/NSs@+irfSe@bEX&T1"ml/*l2<%#J6i2k1XBrfC7='M;@VA;#"Nnk:TGIP;nr>6i)pG9\6BH/,MjQ`^A-*?[`eE7R$'KH]abEE^.$iMdf#RnX,XoPi'b9l0+kDXZhLA+3&TDU<L]]?]s1V(%[tMORkR%_>0o]1@\CN"!DgbOCiT=h[Ms4u3R[hBDA'=0kM@U&I6=-,3!:c5BdUn'!PLeZ76U7[bRdZ&>EucU`!De-Eb?1u1'<VaMk5Kj&qU8,8JuM[H%FU=R:CQ5XuCl2!qo(J2n8,In0lkYJKXMq8X<jUbsCfAD8*[oN/u&^<1"Udq[PQ&LS_Q8sM'9A*]3ial@b3g&I[^HNJsQRL/n=g()Kkqj.F)48]PGZd`'>F9m:E"C-d/7j,1chh*PJ=OO?_>hgf.C)ml3SibL'MHDNfBG:=F_H-#O._gV:>#U13FOSG#@mJclTaNbe92/kgnpF5.#*AY[#[^;kW]JZh=dZJBp(-1CLJM0_^QP,.p*^/XJ2GRL`/g'8u\hbaO=\)PY]n]e(dfPV]F":_NbdVpIp=R:'+Q3P]`G3%Y$Ntnm6lJEiD@=aB[=Vo8U9sTaWL72<;^bOKcu)Ktg34cXXVWiMY4_hJI5i/#7V,<lc@E@X3je@b^c#]a/#U/R[AbfR/C+1k/84[?Bf+^i6`6GoOCS34b4MO<K4a5nNfB`"M]78si5PMSWRc90\pY&U%`AkjU+Cg%WW4;<.[GeZ#WfAho]FUa1knh:L"k+CP-U-IK!>~>endstream
|
||||
endobj
|
||||
16 0 obj
|
||||
<<
|
||||
/Filter [ /ASCII85Decode /FlateDecode ] /Length 627
|
||||
/Filter [ /ASCII85Decode /FlateDecode ] /Length 1554
|
||||
>>
|
||||
stream
|
||||
GasId9lJN8'YO<EN-Zt=qJ&*P5IctF7AU?i'RJaO&pL[DCmH@6Ol)2jSHT/$'uqtVj70de!"Vd4n#QTI,e1E#&/*(uKHc-2*q'a8K9=[3W#-4O*]XOI;?*<BYleH0g.<Z"\KP`&PSI`2Z]H?V(h(d94C'WA17B/_5ZYhWl9FhadSbh%'ua@ght&4E[\r:Z:[A/-Ym7M;6;3/<:a8D6`$RrOiV^U6a#gr;GJ;=Z\h<N`COB0)p`cQNp"e_Pg[<hAkBQ<"Hs%OAJ*:2^q1dt)heRkJUa#&f3Q8c-/OY7GeAo3n([+ZZDLY3N2HUJUf,79*M/3`W9Wql*0bR_N*CO,O;>a`W(/uUuLU]^jPVJrk9<;[Hnih%&/QL3:'fXm'S_%CgM8aH_P2LA2RU_Yn$kHt>fM.8>#"p4,Xcnp)E%JWsbuZU5mZfN@Ot":ZiB$)(Vcc5?<j9DGI2bn"FW8.+0'6[lT589*a@RO4U52JgV&`1;gNQ3[*;&uOcT*aodCXG@am,l5_e"P.S*.5\.s1ZkoOn2YRE$3,ptQYb22AVu3VkgoZ(Lla#AF8'E09s@Oa@,SD[N+ba.mQ.?Qr;#ibP)C3T4Vh>EijbS(B(_0:kp@4>=!HCW,V/~>endstream
|
||||
Gb!#[bAu>q']&@2mPp-Ikm5:e06^@H:#*t&))kU.d\8E!9=4L%8BTK?pOZ2_j-Y!+:*kljK##4A`PFP7",^I:r'r3O0+0c8aFJ84g]E[-_Vu1l/=`mRlY:a9GTdUfL'f,9&7@ST0Zl_C_0?Ef#W\9[P;s7$i@^FOO?1ha8J$Qk=I(p%TXNf],QlUund(N1h_,<43fu4_0bG$F6a)(+b9S=LUY&?IcD5,(%1s!p#W4dk%O6E6m(K8@(`iL2*'+:fi56+Af7"^aGT/X^kOq($dXEo29tCL]9suJW\nkkrTC.eoS8f&Vo*P(=ekBL1T?iR,E<hErhl9CV_E:mX[9'qu9O(P:St3`$ZIZ##1__:Odnqu`nnbA5.b5n^o@uSLhduC'2q;c:`;0#\KE;Ao#&Q:KGu2-DX=07f.9*1qcaUlM'!;r.AHVM?L25SqrcTmF-PhlQQuJa;9HE$.@TVdZ-3[4H-B9QHo@66RdNtLSK)nPVTX*Cd6E'JNQ9l-iE8ENZW&T'YZW3LNZJ$UE[Y?*k5--@$q=V-)VbX+.3AR(N^>ImUCU;OqU-($u$n&94RiSU&Xf@TM-TJ,!%8dbuM^)%i@XEDtb!;8riq:6kG1X0p:T5X-$)t)ck7bdkeUF0@4=(GSi""3)Fh\:k%9HXV/!g!i>3ZdXrQH0lB,;XR0<RmXC3jUDW2VV+3!5q6HuAMf3^&;XaXFY@'(3BgpD56<$O(T$p'$>/*asZ4K0\tRD^u8/$merIL5/'fL(ZP!dBTl6(+AV2^a+YBC%Dg`7i?lY?:J,4?aZ%!(NhjJr(pEb./`7p4-ouAJotA5KBl,upCk%<'/ab+Zi6jHG9npRQ8;Jmpla6o2Tt+e&<?SGeEp?4@sOJGa.2cfn&^44JZYGU8h#lZP[3%Y[#&l+S%Ph!;^NJ-#`+D'Im0XOpW+K@R*kNA*%'#qfds]NB[[*GbE8<D5Jj=/XO5ET_g,,l7Q,fFOgG/T"7$+!0YE29Y,ibPU0*lJA-.Mk,kO5[OhQ7-`q3R!pIZa91.pL:?=DO1jkoIo7[9HEG]6]ZR$t=t<5$.s[k3Y_[Y:GN&g2EhOuuVU+T&%kFD^:OVg8/eWrnP,#5VdfI\H:@p4(U]P4)^gWi/apg9TuXrQ_Ke993&Go5njq&%-AbjK@+JH6e]\IXSAf3.@ekeQ&3@.P@Kq3B_?>/#0SYIQ5<g.5u*3[oue>d;WSp(2a%DCcYn_=t3_>q4D"HV`^M+QS_!VG;jYM&hlfN^72^t"<9XYS"bD>Q%fR!''lF4_r.CZ#p$kGf[h(SklI.&0lgp7!YX%\7.E@o+h`ih('F4h`QLAtUM,i1,^BW.m-YO9"LY[INPJ%;kGJKl`15ft$iGQ??]dXkIc0b(:"#g(ilO^IC`+sA/UPn/$h2kE`Q)jFYZ(<kRiG&U@Gj^iH@'>-+nY!uJC*;+aoYug)jr8NNi\s<XGX]$Wiu`X^(%JPDbS^%A;E-)]#T45b\qfu3n`H89G3$M@6Tbqj,r7k7eEU1p*_ZmKEnKBbKJT77TE"AX$gOD6#V&HpojbD^DiU.3W~>endstream
|
||||
endobj
|
||||
17 0 obj
|
||||
<<
|
||||
/Filter [ /ASCII85Decode /FlateDecode ] /Length 2100
|
||||
/Filter [ /ASCII85Decode /FlateDecode ] /Length 1865
|
||||
>>
|
||||
stream
|
||||
Gb"/'=``=U&:WfGfZ?fJ:1>FNG_,9Ma#WP<9q4aoV]EP!,Xg$,$&4-"35<08?D;7-X?DQ8i^;]+I(P;Bm'd?0-N;Hi1X#i*:#Y1q$C&AK#1OtX9TS0"eG*bijtb8+J6:U6Hj7NuoFI1kOb;[,f4R8NDL-t_2V=N4c@$02kBS-ATG0),DGVIICO4b?_PuR9pWe/E\;[+pRlIS:hk&`ja0g4WEEhg,c/oPQE!1$;l;9.&R+1XGQtESoiT'/7*6MA_iEXM>^8Oq_Es638H#R`0G>M:2V*kH)`0El)Obm=+KBWP5T5.R3I%/fBq6mbiIk3(B(>tN1U,(r+:P(RJ")nHHT5\'t0UK2iH6t#*AU<R$Xc%(!\2`9L?/((W?k-m'qe)nE4CXa;+$=&G*G#oL\SV&Zl,JBGrsUP/Vc6%VO*hr144%JAZl.=CcohY3_<s?A.))P$QY$b=p/45p>\]D\9s8(h\Mn#+H"bE]3j/*nj8PD6FSsVhlk800aM;ZB,`GZ6!C^7O41o_U+8CQ:AsmRe&+JBnh^@bC^<MI$I>ltsr3o6DndW>%"4]9RhY1@dZ[U^:eV_sF=hc>rq&D(Hl7#p=AtU;"Xk'I0Uqs/,q9BlN]G20:8ttVkMN/UrK'$tR3*c^f/.)];]G-M'#-rdRM)nn3mm=9=:Qoam=:U]n*mM-iXF&XoB9FWF&>V9r-3CTjA>Q)X`>*hiO9l)oAQHlEP[!KrgrHaB.I1;`*Y\c_Mt0b3.PJ3CB0/OgA.XTuZY*K9ZhWRK[ZknfdnW=LMA1qG7Z1(h!ks%GDJ\-<(Gd/ZY,/4HfepGY\DqOp<HCuJ'$$_G&1j'i!gOl3$?TU:b+!6K5,+/MZLPU>3Jau*AHbX8XXh^68K)$1`ml*?T5Md>ak(O:IaMEmPC"C6Hi5&1^!l$dWffIWBp-N(ZRccq,eoVLf$L\@r@T!X$=KCdnn$@`a6^OuD&9)kA)$=h&78MUB^2_gDAM[<0kB0i!3jL5EUC/'NSq+M<o+.HeS8^S6ju,3eW-,tT\*(fRi$m?C%k!&NsA$jf1u;\@Ebne./]5s5rJZW5)=-7>Z=N+[2>1dcF.gcXWNHFbbd=,]nig4!$Ou$2S-2&9N[3?c#(l_O#@o!_VPP5,jAX_F<G3XZ;?D%Kh,:R^cI#uPd=)Sj4.Z+?<gF^U"cj1HVlTFLm/Z`Wta?'c'V6uS)s+Cf/`s2j=LEO2nD/$9:QGT-rFjH!]qQSlf(n^3j&=?rth'U3+j!R&;L.e<Hl;";[Ro$D\*/Vq[AT=GE9I4@Bu<)/DJ&X'\gm)K_8g\bJi$()<7cN6HpUr=JrYhYG<^&AL;G'^6S7O("mg:a\8`GLiL^DcUY%+[@Ve9-t#1[XT5P;T+TlOI'eM&5HkZ6S1qLq$708lIc"'GCDeCiDY-HC',=OcnH2pm:$jr]cV&($@f_p1&B"q:("1#,2U2OT7hB)oCs$2"7P3+=G+VSu<MU=s<8o0"8sFUt]2MuaCLJKQq(.AZW^cbOGC',tat*qk#]cnP-5Us7?'f`;DA&/G;F-bg?47`eKMGR*pq!okLZr?Q^=k3F"m?eH+.g]>D_SjR`$?mt68`FP7Yp4"U9%n9jQSeC0)g21cII1ON+gR(AC6=1SknL;kQ>Y8>3$g.E"SijhG-TnR;*MnLI.5.JjC`)j<`;hAFUUlM6:;q1-("\E<C4cLXYM0Uct&'3LE"#W\?.9W=!RQ;AJjRDIupnkA+!4798UVHo7Le$`mAo"do%'%u1M/1b&'"1":5,[um_fg7D)Fa5UL0*'@GX,ih5#]Q'pBeDFDBT"P8Hh'G*gLH$*bdM<EJc#/E<O[Atdn?ppZBKj[EcOf7.f5o#mVjPHlfKsu7k?'\TT2>h2JG;j)DA/F"bb9:1,dRGt-qJJtg8sC.$Z4EUZK[nF9Lq%VAu:*L5;3V5^Zg>#_Mt5KB"gecI5Xt.O4M0c0`%pmF@\"6gGp6IJoeK4<hS\Ol%?cBHl#MV^#,HRSX2V`c>-RdK((NJFf'_B=QF5+X"OuM?,]k6qIm>Gigk66d\RTLlWk&^]QnjEr:+]g2+MY_R&0YU+Ri/NoKbXJpC:Eh8.j+fL@YI#rWT?e_sd~>endstream
|
||||
Gb"/'gN)%,&:N/3m.__K[jFH2:;8*HUhQ#fS60W7quW%C"H*Cr?E@kT^'e6n`Nh$Rj(>0rCf=T%MoF]Bf`P,+nO&fGE<boY"u"k3TGGc65TSk"liXng9l;>c]@4*B(Kqu2YsUi@f>6VsKUOai.#c3^ZpP4Y`C*H@E/<VM4bRY2pc]"90GnZk`!K-9i+R-4g>:m&dkUeq9@2Q0:BhaqD;s2+D-RN.Q["O#6gpP6IXBNT_-fQ1j%2;*`".HZbNA$4*63pajqpdL-UXukoAaZ,YOQ-`RK%rNe/q@ZPAV>V-JKjR\nn.P:No>rMcq1[]PJAhL04`0bEPTV>f)X?as_O?#pnP*W<*\B99o%;/NA<^SB'ae.El[;p<t)'?:Z%`#cu%N!P]="&R8APiap)e7A45nf@.$c.`'U"dtaL)5&m(YoZgFTTb8)9dDq6Hf,tsS%XE%JgU82G:-QgR(L''1FYFB]-GF>"]oUj8F1@UN10.<73LAo'p@:O\9GAIkRnZRe/JDuHfXZ\QZTR.6S:B[4Y%0!L%Qr;a]9&CNf0UfsYg?n#F]k[P0!;?8S:%oWmVqs8mVXDjK(cZB'-6AkdGm'kCppaQYr2KSQPEJ[aJg<60PZ>hq:=65n(Mfqm`LOpGa7IV?Nf6@+\HL0X!2((,VJ\WZW2itFHIk6^V;rY>8+Pk.^%qIe+CP-T0aNeKVn'Q;2F6E8j/dV']SoD9k+=>78q9H1/Q&"0h5m=37]laa;/S.!_YXIJA@'ZTMK$^AU#ma314hgSN#=**OhP$*nf7K?C+;=C/BUd#RT3l?1=37iurNVR'RhH8#,rjal=,kE;5V50d8D^jYB`85R+PG?XLk^IOFpEpuCrN\Dp!q0?"B`>pQ6480ASOY,fTnQ\Lul;Hs1:mVu%\<YI4mhQ5?$Wa\!`D3StnCk6^=Y9HtV2Amj(@:?i]nHoR?3=:l)A[OsYH_PUE`.Vq*!+#Fc=^nX*Ug?H*2"J.#)BLc1Tn8X9`1[fa*,A[IgO%<!(<]fB)ast[?R8t,7>XXrkf1u;Xj0'-LlE1E*R*q64,"(^;8U*5dSs@USKETnNPa0:bo`ToKTs!4ms:Ib%^kj5HuFOWk]Slo&I]Eal/?0'oO'CDBBD2)1IHI]+26=s9=&/e`1EFi)HNA>7`lCW-s@s''I"\jks]O>bOm\jaQ<f0fPuM*_FgZ'V(rdZA3C#&.Mekn3Wg=`<j!p;+!hs]Gl]r!$DgQXc;g\L'M^AiWYC0O=efZVZmIrRE=::.O\eR4a/_hfUdu%3<dXs@@P)E]qI;qT43fN,L_P_&`\?23GMU];*+B?r[l]"<4j\FfE,Xp1nRAV5J_Q6_b[JbS'kTR**7bOpL)0;jgYeefdY^KLOS)9RHstZWGqhLs8f9-EPaL+#@Dc15du]C]*6!4gPppLEk[DK8>a'ijrkAd=LcTTb')m;Yk+d6uAQ]*Z^5I4D>&7QsBo3kHgR=ITS&H\?O+=.=)9e3PqdZeX\u[-.R75P'o=bgkI?.YbZ+32+@"L?T``uo1L9]mgDDT!jh;<ZeYD\]-c:50QF1K?3^^AXOmu7CQK:Z:0bBI26G$B5;4I#4b!CD3aDick88P0)r1"K2@Ldn[TgFh$+&hZCY6<Ac*88%1EpCPfdKkuGn]G;qg4'j%B4Wq`H>iFk8qDd%>h@*A`l8DF-UpMi1g+h[rkKMA[.H4[Xm@!K#:AF'H]e_nO*i@g]p1p4:^Xm10pp@5hOaFhrYCnL2r8(iJ0U7=9n94RP.X@YDeMetl)&]t#3MU85m2HB3O*:pI9$Tkf[?\,brXo+b%lS)!'Hl3&c@E]Kc))bc;24TF+*F0Z7c6.B$Z$KC`q)n&P!l"XOh6amjDXFVHFX~>endstream
|
||||
endobj
|
||||
18 0 obj
|
||||
<<
|
||||
/Filter [ /ASCII85Decode /FlateDecode ] /Length 1839
|
||||
>>
|
||||
stream
|
||||
GauHL>Bedj&:XAWR,]R7%5Ouro$Z)`Y:"$&S*$64\ds]9KL"6kV%kugopo@p)$9s,jOj6JN-=AJhg>NY+ha9=*TmFS,ImT,!OH2k"m/u@N8DC3UU,Hr=F8K*/O-Nc#(^8gTf7)X*^-s!UVC46Djd,X9\-0dJN9^68[qV0C'>E_MAO'_E+do+]Yt;u.i?N>FK'D4Ut7g/m#c`9RdV;O9T(hn)25g4Qls='Y2J_WKKQI+?9s_D"jHL2YAV[)Fo.cBTmLGo2i9ONffQGH_$S`UqKCeC?od2mM9,2M=L^8k^LKRVCS\j!O0Odh\on68@NNI*":m-a.-77b<JlDnDr)?`%mF:7Op_U/n=o,(+9[&dEaGnk4k7`F^$b!\FCBR61<+,aQ3q)hk9q&BQc:g=k.Eh;pC"S2bhV25`RU[+WYH^H@#A4>-12W@Z*,0rf=)aOpU"6'&$TJdi:Il^?&V*eVVT2S>)Kn+d"5nq0ftq%5VB<p)338;%ebYNL,Dcji6Zn_,_s\J[ndVM5SOd]Q'+.$A<*Vh-'?!G.;`bImr"P)Hi@&SZqoTqPtFJC4oOMIRIaAh^`M\Zhg`28HM`"YQe+UrY^Oc;)pcZK4uJI>];5,P=Z;HXf4?8OX0U$T]%V&,_&+]A`PeFnp=1FPj6@*OI=?HeF_Ri?rqrCZ(7ig^i`W,fO<+=,g`V[.=uJP')SDu0<X7XO]O2JXFnK7phJgp`)C+qHG+cPgPh:NW"=H8SU,r=jjM22u!teoo=E4%s2K"CjVfP=l;NOfoo6?C^ZDPe'M4R.6/uSc#hG[Mhp@/<_I>-cLZ^L@3K(EOY!E(=O*`H5("p&i5<EKl3Fp4ZL7LC%6=\9Epiq]0HmDXK4EidWqqf1NdK'pF@ir4]7'LIObr@&H.FhC5E^R-4:5Rj97D<QAa?u_a@;39#e2:3FJ6fnR/L8H=%A="/,Yb82ZLjak"p+m^Dgs:)Z0$u``RU<rck6/PkkLQT"8];Ic4/n,Yhm".KoR"4&Z%MJ[QZhmH?E"&`=ePQ`GXJ`72%+hnhJu!K^b?+(5l(a-VjAZ.^Xd&qE8MN`o)-k4c#r@@qNgFgQV>TDdj7N`;8L(TYE$\HpU^/kY'nqM"'YCVBPh;4#8W[HRqYU8,i?]+`]9NSK?/21meDP]P>t&!Eo['U<^m>$HB&R^ML1.P>#fMan6QQ;/VP&YM)o6<HU"*1]HGKb@oT+IXbUc@dLK.8&9Bl4EJCh=%J0pcEPGneaGu4tAG4"cM5GQM:h?ZIp+^@:]AjN*A.8"</;%J&A*6"9mp9_/<YR$8X7aB&;tWdH[c/k*[:e\@R4[6eTYafn?O@E?$\#!e=nCpG@BGV%[45opN73r%0+j,$!\Q=[9(PB8C/@cpb+@,[5QjUcYu!!R9ZB1>+Ud[q#ul4%=7,Z+<Bq710JH(d^kT@;TZNrtB@92hZ8YE_?t?YRdBI`1S&cP2i\=*W[uI].^46.mVBsWj6-^2[&6,9YC>Dh>9S=-&<tst?=f3PE&YjY_P1*CioesjcW_Q7%:,&c76+P""-mSDZY*nUE)@WZ=J$()dA\!X2XijEA#!@o;AWM'XU:.JAJYJ'8Wb>5>7kACm?IrmLJH]a(P,^tRIUgEUEi?bk<j,_k-G5e,,qgWf.bQ@G4V3(;TD=no_U*H3q<];H*<oP0?q7M?IQk)#=EVu^5<t\:2hTlaZ,^Zmq0qDsN]f:B\cYac_b88g?0>uEfmZu,V"p?K=f<YaklMXbHpN,_qmf"*;<V_H#Vho^&J>jYFW=ogl&"(4kT<6o"DscAg^H!Ep2cbS's3S[;^'W]KSso'&.k?F$D^<h1g3)QD)Ij^%nGhXcN~>endstream
|
||||
endobj
|
||||
xref
|
||||
0 18
|
||||
0 19
|
||||
0000000000 65535 f
|
||||
0000000061 00000 n
|
||||
0000000112 00000 n
|
||||
0000000219 00000 n
|
||||
0000000331 00000 n
|
||||
0000000436 00000 n
|
||||
0000000641 00000 n
|
||||
0000000846 00000 n
|
||||
0000001051 00000 n
|
||||
0000001256 00000 n
|
||||
0000001461 00000 n
|
||||
0000001531 00000 n
|
||||
0000001827 00000 n
|
||||
0000001911 00000 n
|
||||
0000004130 00000 n
|
||||
0000005613 00000 n
|
||||
0000007714 00000 n
|
||||
0000008432 00000 n
|
||||
0000000122 00000 n
|
||||
0000000229 00000 n
|
||||
0000000341 00000 n
|
||||
0000000446 00000 n
|
||||
0000000651 00000 n
|
||||
0000000856 00000 n
|
||||
0000000933 00000 n
|
||||
0000001138 00000 n
|
||||
0000001343 00000 n
|
||||
0000001549 00000 n
|
||||
0000001619 00000 n
|
||||
0000001915 00000 n
|
||||
0000002000 00000 n
|
||||
0000003718 00000 n
|
||||
0000005202 00000 n
|
||||
0000006848 00000 n
|
||||
0000008805 00000 n
|
||||
trailer
|
||||
<<
|
||||
/ID
|
||||
[<6d65c6032a302e19cbfdfbf2e67cdcbe><6d65c6032a302e19cbfdfbf2e67cdcbe>]
|
||||
[<d68dbf84e79b8d0b36eb66a375a9e544><d68dbf84e79b8d0b36eb66a375a9e544>]
|
||||
% ReportLab generated PDF document -- digest (opensource)
|
||||
|
||||
/Info 11 0 R
|
||||
/Root 10 0 R
|
||||
/Size 18
|
||||
/Info 12 0 R
|
||||
/Root 11 0 R
|
||||
/Size 19
|
||||
>>
|
||||
startxref
|
||||
10624
|
||||
10736
|
||||
%%EOF
|
||||
|
||||
@@ -1,517 +1,447 @@
|
||||
# Les 13 — Lesstof
|
||||
## Externe APIs + Cursor agents + Vercel deploy
|
||||
# Les 14 — Lesstof
|
||||
## Cursor + Vercel: van leeg project naar preview-deploys
|
||||
|
||||
**Vak:** AI-Assisted Development
|
||||
**Opleiding:** NOVI Hogeschool Utrecht
|
||||
**Vorige les:** Les 12 — Tool Calling
|
||||
**Volgende les:** Les 14 — Agents
|
||||
**Vorige les:** Les 13 — Agents
|
||||
**Volgende les:** Les 15 — RAG + Embeddings
|
||||
|
||||
---
|
||||
|
||||
## Inhoud
|
||||
## Doel van deze les
|
||||
|
||||
1. [Externe APIs in Next.js](#1-externe-apis-in-nextjs)
|
||||
2. [Server- vs client-side fetching](#2-server--vs-client-side-fetching)
|
||||
3. [Environment variabelen](#3-environment-variabelen)
|
||||
4. [Cursor agents — Composer vs Background](#4-cursor-agents--composer-vs-background)
|
||||
5. [Vercel deployments](#5-vercel-deployments)
|
||||
6. [Preview deployments per branch](#6-preview-deployments-per-branch)
|
||||
7. [GitHub Actions CI](#7-github-actions-ci)
|
||||
8. [Branch protection](#8-branch-protection)
|
||||
9. [Werkstroom — feature van begin tot productie](#9-werkstroom--feature-van-begin-tot-productie)
|
||||
10. [Productie-overwegingen](#10-productie-overwegingen)
|
||||
Aan het einde van deze les heb je:
|
||||
|
||||
- Een vers Next.js project gescaffold via `npx create-next-app`
|
||||
- Een GitHub-repo met je code
|
||||
- Een Vercel-project met productie-URL én preview-URL's per branch
|
||||
- Vercel CLI lokaal gekoppeld + één keer `vercel env pull` gedaan
|
||||
- Een werkende `.cursor/rules/general.mdc` en `AGENTS.md` in je repo
|
||||
- Twee features gebouwd via Cursor in eigen branches → twee preview-URL's
|
||||
- Eén Pull Request gemerged naar productie
|
||||
|
||||
---
|
||||
|
||||
## 1. Externe APIs in Next.js
|
||||
## 1. Scaffolden met `npx create-next-app`
|
||||
|
||||
Een externe API is een HTTP-endpoint van iemand anders waar je data of functionaliteit kunt ophalen. Voor de meeste apps die je gaat bouwen heb je er minstens één nodig.
|
||||
### Wat het doet
|
||||
|
||||
### Wat valt eronder
|
||||
|
||||
- **Data-APIs** — PokéAPI (Pokemon), Open-Meteo (weer), GitHub API (repos), TMDB (films)
|
||||
- **AI-providers** — OpenAI, Anthropic, Tavily (web search)
|
||||
- **Payment** — Stripe, Mollie
|
||||
- **Auth** — Clerk, Auth0, Supabase Auth
|
||||
- **Email** — Resend, Postmark, SendGrid
|
||||
|
||||
### Drie smaken qua authenticatie
|
||||
|
||||
- **Geen key** — PokéAPI, Open-Meteo. Direct fetchen.
|
||||
- **API key in URL of header** — Tavily, TMDB. Key beheren als env-variabele.
|
||||
- **OAuth flow** — Google, GitHub. Complexer, vaak via libraries.
|
||||
|
||||
### Basis-pattern in Next.js
|
||||
|
||||
```typescript
|
||||
// Server Component
|
||||
async function getPokemon(name: string) {
|
||||
const res = await fetch(`https://pokeapi.co/api/v2/pokemon/${name}`);
|
||||
if (!res.ok) throw new Error("Niet gevonden");
|
||||
return res.json();
|
||||
}
|
||||
|
||||
export default async function Page({ params }) {
|
||||
const data = await getPokemon(params.name);
|
||||
return <div>{data.name}</div>;
|
||||
}
|
||||
```
|
||||
|
||||
Drie dingen om te onthouden:
|
||||
|
||||
- Server Components mogen `async` zijn — `await` direct in component
|
||||
- Errors moet je zelf afvangen (`!res.ok`)
|
||||
- `fetch` in een Server Component **wordt automatisch gecached** door Next.js
|
||||
|
||||
---
|
||||
|
||||
## 2. Vier manieren om een externe API te fetchen
|
||||
|
||||
Next.js geeft je per fetch-call de keuze hoe je data wilt renderen. Voor 95% van je werk komt het neer op drie server-modes plus client-side. De code verandert minimaal — meestal één optie in je `fetch()`.
|
||||
|
||||
### 2.1 Static (build-time)
|
||||
|
||||
```typescript
|
||||
// app/page.tsx — Server Component
|
||||
async function getPokemon() {
|
||||
const res = await fetch("https://pokeapi.co/api/v2/pokemon?limit=151");
|
||||
return res.json();
|
||||
}
|
||||
```
|
||||
|
||||
- Gefetched tijdens `next build`, daarna **HTML voor altijd**
|
||||
- Geen server-werk per request → snelste optie
|
||||
- Data ververst alleen bij een nieuwe build (= nieuwe deploy)
|
||||
- **Goed voor:** marketing-pagina's, blogs, productlijsten, alles dat zelden verandert
|
||||
- **Niet voor:** prijzen die per minuut updaten, user-specifieke data
|
||||
|
||||
### 2.2 ISR — Incremental Static Regeneration
|
||||
|
||||
```typescript
|
||||
const res = await fetch(URL, { next: { revalidate: 3600 } });
|
||||
```
|
||||
|
||||
- Eerste request: zelfde als static (HTML uit cache)
|
||||
- Na 3600 seconden: volgende request krijgt nog steeds cache, maar achter de schermen wordt de pagina vernieuwd
|
||||
- **Goed voor:** Pokédex detail-pages, een nieuws-feed die niet realtime hoeft, productdetails
|
||||
- **De sweet spot tussen snel en redelijk vers**
|
||||
|
||||
### 2.3 Dynamic (request-time / SSR)
|
||||
|
||||
```typescript
|
||||
const res = await fetch(URL, { cache: "no-store" });
|
||||
```
|
||||
|
||||
- Bij elke request opnieuw gefetched op de server
|
||||
- Altijd 100% vers, maar trager (geen cache)
|
||||
- **Goed voor:** dashboards met live data, user-specifieke views, anything per-request
|
||||
- **Let op cost:** elke request = serverless function call
|
||||
|
||||
### 2.4 Client-side (`useEffect`)
|
||||
|
||||
```typescript
|
||||
"use client";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
export function LivePokemon({ name }) {
|
||||
const [data, setData] = useState(null);
|
||||
useEffect(() => {
|
||||
fetch(`/api/pokemon/${name}`).then((r) => r.json()).then(setData);
|
||||
}, [name]);
|
||||
return data ? <div>{data.name}</div> : <p>Loading...</p>;
|
||||
}
|
||||
```
|
||||
|
||||
- Fetch gebeurt in de browser na hydration
|
||||
- Goed voor user-interactie, real-time, infinite scroll
|
||||
- **Cruciaal:** API-keys staan **in de browser** als je hier rechtstreeks naar een externe API roept. Alleen voor publieke APIs, of via een eigen `/api/...` proxy-route.
|
||||
|
||||
### Spiekbriefje
|
||||
|
||||
| Mode | `fetch()` optie | Wanneer |
|
||||
|------|----------------|---------|
|
||||
| Static | `fetch(URL)` (default) | Data verandert per deploy |
|
||||
| ISR | `{ next: { revalidate: N } }` | Data verandert traag |
|
||||
| Dynamic | `{ cache: "no-store" }` | Data verandert per request |
|
||||
| Client | `useEffect` + fetch | User-interactie / live |
|
||||
| Tag-based | `{ next: { tags: ["x"] } }` | Triggered revalidation via `revalidateTag()` |
|
||||
|
||||
---
|
||||
|
||||
## 3. Environment variabelen — server-only vs client-exposed
|
||||
|
||||
### Twee soorten variabelen
|
||||
|
||||
| Type | Voorbeeld | Waar beschikbaar | Veiligheid |
|
||||
|------|-----------|------------------|------------|
|
||||
| Server-only | `OPENAI_API_KEY`, `DATABASE_URL` | Alleen op de server | **Geheim** — nooit zichtbaar voor de browser |
|
||||
| Client-exposed | `NEXT_PUBLIC_APP_ENV`, `NEXT_PUBLIC_APP_URL` | Server + client bundle | **Publiek** — iedereen kan ze zien in DevTools |
|
||||
|
||||
**Regel:** alles met `NEXT_PUBLIC_` prefix wordt ingebakken in de browser-bundle. Zonder prefix = server-only.
|
||||
|
||||
```typescript
|
||||
// ✅ Veilig — server-side gebruik
|
||||
process.env.OPENAI_API_KEY
|
||||
|
||||
// ❌ NOOIT
|
||||
"use client";
|
||||
const key = process.env.NEXT_PUBLIC_OPENAI_KEY; // staat in de HTML response
|
||||
```
|
||||
|
||||
### 3.1 Vercel — drie omgevingen, drie scopes
|
||||
|
||||
Op Vercel heb je per project drie environments. Bij elke env-var kies je in welke scopes hij wordt geïnjecteerd:
|
||||
|
||||
| Scope | Wordt gebruikt bij | URL-patroon |
|
||||
|-------|---------------------|-------------|
|
||||
| **Production** | Push naar `main` | `je-app.vercel.app` |
|
||||
| **Preview** | Push naar elke andere branch | `je-app-git-{branch}-{user}.vercel.app` |
|
||||
| **Development** | Lokaal (`pnpm dev`) | `localhost:3000` |
|
||||
|
||||
### 3.2 Per-omgeving andere waarden
|
||||
|
||||
In **Vercel → Project → Settings → Environment Variables** zet je per variabele drie keer een waarde:
|
||||
|
||||
| Var | Production | Preview | Development |
|
||||
|-----|------------|---------|-------------|
|
||||
| `DATABASE_URL` | productie-DB | staging-DB | lokale DB |
|
||||
| `OPENAI_API_KEY` | echte key | test-key (lage limit) | jouw key |
|
||||
| `STRIPE_KEY` | `sk_live_...` | `sk_test_...` | `sk_test_...` |
|
||||
| `NEXT_PUBLIC_APP_URL` | `je-app.com` | preview-URL | `http://localhost:3000` |
|
||||
|
||||
**Hoe in te stellen:** in de UI: Add new → vink alleen de juiste environment(s) aan → Save.
|
||||
|
||||
**Belangrijke veiligheidsregels:**
|
||||
- Productie-secrets (echte API keys, prod-DB-password): **alleen** Production vinken
|
||||
- Preview krijgt een test-variant, anders kunnen experimentele branches je productie raken
|
||||
- Development: `vercel env pull` om Vercel's dev-vars naar je `.env.local` te syncen, of zelf invullen
|
||||
|
||||
### 3.3 Verandering pakt pas door bij nieuwe build
|
||||
|
||||
Bestaande deploys zien een nieuwe env-var niet automatisch. Na het toevoegen of wijzigen: **redeploy** (Deployments → ··· → Redeploy) of push een nieuwe commit.
|
||||
|
||||
---
|
||||
|
||||
## 4. Cursor agents — Composer vs Background
|
||||
|
||||
Cursor heeft twee soorten "agents". Niet hetzelfde, niet uitwisselbaar.
|
||||
|
||||
### Composer
|
||||
|
||||
- Lokaal in je editor (`Cmd+I` / `Ctrl+I`)
|
||||
- **Synchroon** — jij wacht, agent werkt, jij ziet diffs
|
||||
- Multi-file edits in één keer
|
||||
- Terminal-commands met approval
|
||||
- Voor: pair programming, snelle changes, exploreren
|
||||
|
||||
### Background Agent
|
||||
|
||||
- Runt in **Cursor's cloud sandbox**
|
||||
- **Asynchroon** — jij geeft prompt, gaat iets anders doen
|
||||
- Maakt zelf een branch + commit + opent PR
|
||||
- Kan tests draaien, dependencies installeren
|
||||
- Verbonden via Cursor cloud (eenmalige setup)
|
||||
- Voor: welomschreven features, parallel werk, PR-prep
|
||||
|
||||
### Aanroep
|
||||
|
||||
```
|
||||
Cursor Composer: Cmd+I (Mac) of Ctrl+I (Win/Linux)
|
||||
Cursor Chat: Cmd+L (kan switchen naar Agent mode)
|
||||
Background Agent: Cmd+Shift+P → "Open Background Agent"
|
||||
Of via Slack-integratie
|
||||
```
|
||||
|
||||
### Mentale model
|
||||
|
||||
- **Composer = pair programming.** Jij + AI samen aan dezelfde plek tegelijk.
|
||||
- **Background Agent = delegeren.** Je geeft taak, gaat iets anders doen, komt terug als-ie klaar is.
|
||||
|
||||
### Wanneer welke
|
||||
|
||||
| Scenario | Tool |
|
||||
|----------|------|
|
||||
| Snel iets aanpassen tijdens coden | Composer |
|
||||
| Refactor over 5 files — wil zien wat hij doet | Composer |
|
||||
| Onbekend gebied / leren | Composer |
|
||||
| Welomschreven feature, can-be-async | Background Agent |
|
||||
| Parallel werken aan 3 features | 3x Background Agent |
|
||||
| Tijdens vergadering PR voorbereiden | Background Agent |
|
||||
|
||||
### Hoe schrijf je een goede Background Agent prompt
|
||||
|
||||
Een Background Agent ziet je niet — je moet specifiek zijn.
|
||||
|
||||
**Slecht:**
|
||||
> "Voeg search toe."
|
||||
|
||||
**Goed:**
|
||||
> "Maak een nieuwe branch `feature/search`. Voeg een controlled input toe bovenaan `app/page.tsx`. Filter de Pokémon-lijst op naam terwijl gebruiker typt. Case-insensitive. Gebruik Tailwind. Houd het stijl-consistent met bestaande cards. Push de branch en open een PR met titel 'Add search bar' en korte beschrijving in PR body."
|
||||
|
||||
Specifiek: bestandsnamen, gedrag, edge cases, output-formaat (branch + PR + titel).
|
||||
|
||||
---
|
||||
|
||||
## 5. Vercel deployments
|
||||
|
||||
Vercel is hosting platform door Next.js team (zelfde bedrijf). Voor Next.js apps de simpelste deploy.
|
||||
|
||||
### Eerste deployment
|
||||
|
||||
1. Push je repo naar GitHub
|
||||
2. `vercel.com` → Add New → Import Git Repository
|
||||
3. Kies repo, klik **Deploy**
|
||||
4. ~60-90s later: live URL
|
||||
|
||||
**Geen config nodig.** Vercel detecteert Next.js automatisch.
|
||||
|
||||
### Build settings (als je ze nodig hebt)
|
||||
|
||||
- Framework: Next.js (auto)
|
||||
- Build command: `pnpm build` (auto)
|
||||
- Output directory: `.next` (auto)
|
||||
- Install command: `pnpm install`
|
||||
- Root directory: alleen aanpassen als app in subfolder zit (monorepo)
|
||||
|
||||
### CLI alternatief
|
||||
`create-next-app` is het officiële scaffold-commando van het Next.js team. Eén commando, een paar interactieve vragen, en je hebt een werkende Next.js-app met sane defaults.
|
||||
|
||||
```bash
|
||||
pnpm i -g vercel
|
||||
vercel login
|
||||
vercel # preview deploy van current dir
|
||||
vercel --prod # production deploy
|
||||
vercel env pull # download .env.local van Vercel
|
||||
npx create-next-app@latest mijn-portfolio
|
||||
```
|
||||
|
||||
### De prompts uitgelegd
|
||||
|
||||
| Vraag | Aanbevolen | Waarom |
|
||||
|-------|-----------|--------|
|
||||
| TypeScript? | **Ja** | strict typing voorkomt veel bugs |
|
||||
| ESLint? | **Ja** | code-style consistency |
|
||||
| Tailwind CSS? | **Ja** | utility-first styling, breed gebruikt |
|
||||
| `src/` directory? | **Ja** | houdt project-root schoon |
|
||||
| App Router? | **Ja** | de moderne Next.js flow (server components) |
|
||||
| Turbopack? | **Ja** | snellere dev-builds |
|
||||
| Import alias? | **`@/*`** | `@/components/foo` ipv `../../components/foo` |
|
||||
|
||||
### Wat je krijgt
|
||||
|
||||
```
|
||||
mijn-portfolio/
|
||||
├── .gitignore # node_modules, .next, .env.local zijn al gegitignored
|
||||
├── next.config.ts
|
||||
├── package.json
|
||||
├── tsconfig.json
|
||||
├── src/
|
||||
│ └── app/
|
||||
│ ├── layout.tsx # root layout
|
||||
│ ├── page.tsx # homepage
|
||||
│ └── globals.css
|
||||
└── public/
|
||||
```
|
||||
|
||||
Direct `npm run dev` → `localhost:3000` werkt. Vanaf hier ben je productief.
|
||||
|
||||
---
|
||||
|
||||
## 2. Git + GitHub
|
||||
|
||||
### Init en eerste push
|
||||
|
||||
```bash
|
||||
git init
|
||||
git add .
|
||||
git commit -m "init"
|
||||
git branch -M main
|
||||
|
||||
# Met gh CLI (snelst):
|
||||
gh repo create mijn-portfolio --public --source=. --push
|
||||
|
||||
# Of handmatig:
|
||||
# 1. github.com → New repository → naam → Create (leeg, GEEN README)
|
||||
# 2. git remote add origin <url>
|
||||
# 3. git push -u origin main
|
||||
```
|
||||
|
||||
### Veelgemaakte fouten
|
||||
|
||||
| Fout | Oplossing |
|
||||
|------|-----------|
|
||||
| `gh: command not found` | `brew install gh` (Mac) / `winget install GitHub.cli` (Windows) |
|
||||
| `Authentication required` | `gh auth login` doorlopen |
|
||||
| `error: src refspec main does not match any` | Eerst `git add . && git commit -m "init"` |
|
||||
| `fatal: remote origin already exists` | `git remote remove origin` en opnieuw |
|
||||
|
||||
### Wat zit er NIET in je commit
|
||||
|
||||
Check je `.gitignore` — Next.js zet er standaard de juiste regels in:
|
||||
|
||||
```
|
||||
node_modules/
|
||||
.next/
|
||||
.env.local
|
||||
.env*.local
|
||||
```
|
||||
|
||||
**Belangrijk:** controleer altijd na je eerste push op github.com dat `.env.local` daar NIET staat. Een gelekte secret-key is dezelfde dag onveilig.
|
||||
|
||||
---
|
||||
|
||||
## 3. Vercel — drie stappen naar productie
|
||||
|
||||
### Project importeren
|
||||
|
||||
1. Ga naar **vercel.com/new**
|
||||
2. Klik **Import Git Repository**
|
||||
3. Kies je `mijn-portfolio` repo
|
||||
4. Framework auto-detect: **Next.js**
|
||||
5. Klik **Deploy**
|
||||
|
||||
Wacht ±45 seconden. Wat Vercel doet:
|
||||
- Kloont je repo
|
||||
- Detecteert framework
|
||||
- Draait `npm install`
|
||||
- Draait `npm run build`
|
||||
- Deploy't naar Edge Network
|
||||
- Geeft je een productie-URL
|
||||
|
||||
### Twee URL-types
|
||||
|
||||
| Type | URL-format | Trigger |
|
||||
|------|------------|---------|
|
||||
| **Productie** | `mijn-portfolio-<scope>.vercel.app` | push naar `main` |
|
||||
| **Preview** | `mijn-portfolio-git-<branch>-<scope>.vercel.app` | push naar elke andere branch |
|
||||
|
||||
### Wat je vanaf nu krijgt zonder extra werk
|
||||
|
||||
- Elke push naar GitHub → automatische deploy
|
||||
- Elke nieuwe branch → eigen preview-URL
|
||||
- Pull Request → Vercel-bot post preview-URL als comment
|
||||
- Productie-URL update vanzelf bij merge naar `main`
|
||||
|
||||
---
|
||||
|
||||
## 4. Environment Variables in Vercel
|
||||
|
||||
### Drie environments
|
||||
|
||||
| Environment | Actief bij |
|
||||
|-------------|-----------|
|
||||
| Production | deploys vanaf `main` |
|
||||
| Preview | alle andere branches |
|
||||
| Development | `vercel dev` lokaal |
|
||||
|
||||
Je kunt env vars in één, twee of alle drie tegelijk zetten.
|
||||
|
||||
### `NEXT_PUBLIC_` prefix
|
||||
|
||||
| Prefix | Wat gebeurt | Voorbeeld |
|
||||
|--------|-------------|-----------|
|
||||
| `NEXT_PUBLIC_FOO` | wordt in browser-bundle gestopt | publieke API-URL, Supabase anon-key |
|
||||
| `FOO` (geen prefix) | alleen server-side beschikbaar | OpenAI key, Stripe secret, DB password |
|
||||
|
||||
**Regel:** zet `NEXT_PUBLIC_` ALLEEN op variabelen die echt publiek mogen zijn. Twijfel? Geen prefix.
|
||||
|
||||
### Via het dashboard
|
||||
|
||||
Settings → Environment Variables → Add. Vul naam + waarde, kies environments, save.
|
||||
|
||||
---
|
||||
|
||||
## 5. Vercel CLI
|
||||
|
||||
### Installeren
|
||||
|
||||
```bash
|
||||
npm i -g vercel
|
||||
vercel login # opent browser voor auth
|
||||
```
|
||||
|
||||
### Belangrijkste commando's
|
||||
|
||||
```bash
|
||||
vercel link # lokale folder → Vercel project
|
||||
vercel env pull .env.local # productie env vars → lokaal
|
||||
vercel env pull --environment=preview .env.local
|
||||
|
||||
vercel env add OPENAI_API_KEY production
|
||||
vercel env rm OPENAI_API_KEY production
|
||||
vercel env ls # lijst alle vars
|
||||
|
||||
vercel # interactieve deploy
|
||||
vercel --prod # productie deploy (handmatig)
|
||||
|
||||
vercel logs # runtime logs
|
||||
vercel logs --follow # stream logs live
|
||||
```
|
||||
|
||||
### Typische workflow
|
||||
|
||||
```bash
|
||||
# Project klaarzetten (eenmalig):
|
||||
vercel link
|
||||
|
||||
# Bij elke nieuwe env var op productie:
|
||||
vercel env pull .env.local
|
||||
|
||||
# Bij env var rotatie:
|
||||
vercel env rm DATABASE_URL production
|
||||
vercel env add DATABASE_URL production
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. Preview deployments per branch
|
||||
## 6. Cursor — de IDE
|
||||
|
||||
Hier komt de magie. Vercel maakt automatisch een preview deploy voor **elke branch en elke PR**.
|
||||
### Wat is Cursor
|
||||
|
||||
### Hoe werkt het
|
||||
Cursor is een VS Code-fork met AI als kernfeature. Niet een extensie — het is de editor. Werkt met Claude, GPT, Gemini en lokale modellen.
|
||||
|
||||
```
|
||||
git push origin main → Production deploy
|
||||
(je-app.vercel.app)
|
||||
### Cursor 3 — twee windows
|
||||
|
||||
git push origin feature/search → Preview deploy
|
||||
(je-app-git-feature-search-user.vercel.app)
|
||||
In Cursor 3 zijn er **twee hoofdvensters**:
|
||||
|
||||
PR open → Vercel comment in PR:
|
||||
"🚀 Preview: <URL>"
|
||||
| Window | Wat je er doet | Switch met |
|
||||
|--------|----------------|-----------|
|
||||
| **Editor Window** (classic) | Code schrijven en reviewen — als VS Code | Cmd+Shift+N → Editor |
|
||||
| **Agents Window** | Meerdere agents parallel managen | Cmd+Shift+N → Agents |
|
||||
|
||||
**Sneltoets om te onthouden:** `Cmd+Shift+N` switcht tussen beide.
|
||||
|
||||
**Binnen het Editor Window:**
|
||||
- **Cmd+L** — chat-paneel (kies modus: Ask / Agent / Plan)
|
||||
- **Cmd+K** — inline edit op selectie
|
||||
- **Tab** — accepteer AI auto-completion
|
||||
- **Cmd+P** — file search (zoals VS Code)
|
||||
|
||||
**Binnen het Agents Window:**
|
||||
- Overzicht met lokale, cloud en remote agents
|
||||
- Op één plek alle running + done agents
|
||||
|
||||
**Gouden regel:**
|
||||
- Code schrijven of 1 feature bouwen → **Editor Window** + **Agent mode** in chat
|
||||
- Meerdere features parallel → **Agents Window** met Background agents
|
||||
- Snelle tweak in 1 file → **Cmd+K**
|
||||
|
||||
> **Versie-noot:** info hieronder is gebaseerd op de officiële Cursor 3 documentatie (`cursor.com/docs`). Cursor 3.7 en hoger heeft alle besproken features. Sommige UI-details kunnen per minor-versie iets verschillen.
|
||||
|
||||
### Editor Window — stap voor stap
|
||||
|
||||
Je hoofdwerkplek voor code:
|
||||
|
||||
1. **Cmd+Shift+N** — switcht naar Editor Window (als je in Agents Window zat)
|
||||
2. Layout: file explorer links, editor midden, terminal beneden (klassieke VS Code-stijl)
|
||||
3. **Cmd+L** opent chat-paneel rechts
|
||||
4. Voor snelle inline edits: selecteer code → **Cmd+K** → beschrijf wijziging
|
||||
5. Cursor toont **diff inline** in de editor (niet in een aparte panel)
|
||||
6. **Tab** om te accepteren, **Esc** om te rejecten
|
||||
7. **Cmd+P** voor file search
|
||||
8. **Tab** accepteert AI auto-completion suggesties
|
||||
|
||||
**Wanneer:** je standaard coding-flow, files reviewen met split screens, VS Code extensies gebruiken (linters, formatters, debugger).
|
||||
|
||||
**Tip:** kun je Cursor laten starten in Editor Window als default? Ja. Settings → "Open Agents Window on startup" → uit. Of start vanuit terminal met `cursor --classic`.
|
||||
|
||||
### Agent mode (binnen chat in Editor Window) — stap voor stap
|
||||
|
||||
De AI-modus *binnen* het chat-paneel:
|
||||
|
||||
1. **Cmd+L** opent chat-paneel rechts (in Editor Window)
|
||||
2. Bovenaan chat: **mode-selector** → kies **"Agent"**
|
||||
3. Typ de **hele taak** (niet een specifieke kleine wijziging)
|
||||
4. *"Bouw een /about pagina met team-grid in onze huisstijl"*
|
||||
5. **Enter** — agent start zelfstandig
|
||||
6. Cursor toont een lopende **takenlijst** + voortgang per stap
|
||||
7. Agent edit meerdere files, runt terminal-commando's (vraagt vooraf), kan zelfs **web browsen**
|
||||
8. **Geen limiet** op aantal tool-calls per taak
|
||||
9. Aan het einde: samenvatting → **Keep / Accept all** of per file
|
||||
|
||||
**Wanneer:** grotere features die meerdere bestanden raken, refactors, nieuwe pagina's of API-routes.
|
||||
|
||||
**Belangrijke tip:** hou je prompt specifiek. *"Bouw een about-pagina met team-grid"* = goed. *"Bouw een mooie website"* = veel te open.
|
||||
|
||||
### Background agents (Agents Window) — stap voor stap
|
||||
|
||||
**Async cloud-based agents** in geïsoleerde VMs:
|
||||
|
||||
**Cmd+Shift+N** → Agents Window opent.
|
||||
|
||||
**Een agent aanmaken:**
|
||||
1. In Agents Window: klik **New agent** (of `+`)
|
||||
2. Kies environment: **local / cloud / remote SSH**
|
||||
3. Beschrijf de taak — specifiek, als een ticket
|
||||
4. Optioneel: branch-naam (`feature/about`)
|
||||
5. Klik **Start**
|
||||
6. **Cmd+Shift+N** terug naar Editor Window — agent babysit zichzelf
|
||||
|
||||
**Resultaat ophalen:**
|
||||
1. **Cmd+Shift+N** naar Agents Window
|
||||
2. Klare agents staan op **Done** in de agents-lijst
|
||||
3. Klik agent → zie **diff + commit message + alle stappen**
|
||||
4. **Open in PR** → automatische PR op GitHub
|
||||
5. Vercel maakt preview-URL → review → merge
|
||||
|
||||
**Triggers van buitenaf:** Background agents kun je in Cursor 3 ook starten vanuit je telefoon, Slack, GitHub of Linear — handig tijdens vergaderingen.
|
||||
|
||||
**Wanneer:** 2–4 parallelle features, lange refactors die je niet wilt babysitten, taken vanaf mobiel.
|
||||
|
||||
### Context — `@`-mentions
|
||||
|
||||
In chat type je `@` voor een autocomplete. Wat je kunt mention'en:
|
||||
|
||||
| Tag | Wat het doet |
|
||||
|-----|--------------|
|
||||
| `@file:foo.tsx` | inclusief één specifiek bestand |
|
||||
| `@folder:components` | hele map als context |
|
||||
| `@code:functionName` | losse functie of class |
|
||||
| `@docs` | externe documentatie |
|
||||
| `@web` | live web-search |
|
||||
| `@git` | git history / blame |
|
||||
| `@recommended` | Cursor suggereert zelf relevante bestanden |
|
||||
|
||||
**Tip:** hou de scope klein. Veel mentions in één chat verslechtert de output, niet verbetert.
|
||||
|
||||
### Cursor Rules
|
||||
|
||||
Markdown-bestanden in `.cursor/rules/` die aan elke prompt worden toegevoegd als systeem-instructie.
|
||||
|
||||
**Voorbeeld `.cursor/rules/general.mdc`:**
|
||||
|
||||
```markdown
|
||||
---
|
||||
description: Algemene project-conventies
|
||||
alwaysApply: true
|
||||
---
|
||||
|
||||
- TypeScript strict mode
|
||||
- Tailwind voor styling — geen CSS modules
|
||||
- Server components by default; "use client" alleen waar nodig
|
||||
- Imports met @/ alias
|
||||
- Geen any-types
|
||||
```
|
||||
|
||||
### Wat krijg je hiermee
|
||||
YAML front-matter opties:
|
||||
- `alwaysApply: true/false` — actief in elke prompt
|
||||
- `globs: ["**/*.test.ts"]` — alleen actief op matching bestanden
|
||||
- `description: "..."` — wat de rule doet
|
||||
|
||||
- **Reviewers** klikken op de URL in de PR, testen de feature live
|
||||
- **Background Agents** maken PR → Vercel zet preview neer → klikbaar
|
||||
- **Stakeholders** zien een feature voor merge, niet alleen screenshots
|
||||
- **Geen "werkt op mijn laptop"** — alles draait op Vercel infra
|
||||
### Project context — `AGENTS.md`
|
||||
|
||||
### Belangrijk om in te stellen
|
||||
Eén markdown-bestand in je repo-root dat beschrijft:
|
||||
- Tech stack + waarom die keuzes
|
||||
- Architectuur en belangrijke patronen
|
||||
- Hoe lokaal draaien, hoe deployen
|
||||
- Pointers naar belangrijke bestanden
|
||||
|
||||
- **Environment scoping:** preview-deploys mogen geen productie-keys gebruiken — gebruik aparte env vars per scope
|
||||
- **Database:** in productie wijs je naar prod-DB, in preview kun je naar test-DB wijzen — meer dan een Vercel-feature, eigen architectuur
|
||||
`AGENTS.md` wordt door Cursor, Claude Code én GitHub Copilot gelezen. Eén file, meerdere tools.
|
||||
|
||||
### Comment-bot
|
||||
### Background agents — async cloud
|
||||
|
||||
Vercel installeert een GitHub App. Die plakt een comment op je PR met:
|
||||
Tweede Cursor-sessie die asynchroon op een feature werkt, in een eigen Git branch, in een aparte tab.
|
||||
|
||||
```
|
||||
🚀 Preview Deployment: https://...
|
||||
✅ Build successful — 1m 23s
|
||||
View build log
|
||||
```
|
||||
**Workflow:**
|
||||
1. Beschrijf feature → klik "open in background"
|
||||
2. Agent werkt in eigen branch, jij blijft in main editor
|
||||
3. Agent meldt zich klaar → review diff → apply/iterate/discard
|
||||
|
||||
Per push naar de branch wordt deze comment ge-update.
|
||||
**Wanneer:** voor parallelle features. Drie of vier agents tegelijk is normaal.
|
||||
|
||||
### Top 7 tips voor power-users
|
||||
|
||||
1. **Cmd+I** voor Composer met multi-file context (niet alleen Cmd+L)
|
||||
2. **`@-recommended`** laat Cursor zelf relevante bestanden voorstellen
|
||||
3. **Plan + Apply per stap** — niet alles in één klap accepteren
|
||||
4. **`alwaysApply: false`** rules voor opt-in conventies
|
||||
5. **Auto-mode** — Cursor kiest beste model per taak
|
||||
6. **Cmd+Shift+Enter** voor diff-preview vóór accept
|
||||
7. **Notepads** — herbruikbare prompt-snippets per project
|
||||
|
||||
---
|
||||
|
||||
## 7. GitHub Actions CI
|
||||
## 7. De feature-workflow
|
||||
|
||||
Een **CI** (Continuous Integration) check draait automatisch op je code. Minimaal: lint + build.
|
||||
|
||||
### Waarom
|
||||
|
||||
- Voor merge weet je: code lint clean, types kloppen, build slaagt
|
||||
- Cursor Background Agent maakt PR → CI draait → groene check of feedback
|
||||
- Voorkomt dat broken code in main komt
|
||||
|
||||
### Minimaal workflow
|
||||
|
||||
`.github/workflows/ci.yml`:
|
||||
|
||||
```yaml
|
||||
name: CI
|
||||
on:
|
||||
pull_request:
|
||||
branches: [main]
|
||||
push:
|
||||
branches: [main]
|
||||
|
||||
jobs:
|
||||
check:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: pnpm/action-setup@v3
|
||||
with:
|
||||
version: 9
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
cache: pnpm
|
||||
- run: pnpm install --frozen-lockfile
|
||||
- run: pnpm lint
|
||||
- run: pnpm build
|
||||
```
|
||||
|
||||
### Wat gebeurt er
|
||||
|
||||
1. **Trigger:** PR open of update, of push naar `main`
|
||||
2. **Runner:** Ubuntu container op GitHub infra
|
||||
3. **Stappen:**
|
||||
- Checkout code
|
||||
- Install pnpm + Node 20
|
||||
- Install dependencies
|
||||
- Run lint (faalt op fouten)
|
||||
- Run build (faalt op TypeScript errors)
|
||||
4. **Status:** groene check of rode X op de PR
|
||||
|
||||
### Tijdsbudget
|
||||
|
||||
Voor een kleine Next.js app: 1-2 minuten. Voor grote apps met tests: 3-10 minuten.
|
||||
|
||||
### Caching
|
||||
|
||||
Met `cache: pnpm` herbruikt GH Actions de node_modules-cache tussen runs — scheelt 60-80% tijd.
|
||||
|
||||
---
|
||||
|
||||
## 8. Branch protection
|
||||
|
||||
Standaard kan iedereen pushen naar main. Voor productie wil je dat niet.
|
||||
|
||||
### Branch protection rules (op GitHub)
|
||||
|
||||
1. Repo settings → Branches → Add rule
|
||||
2. Branch name pattern: `main`
|
||||
3. Check: **Require a pull request before merging**
|
||||
4. Check: **Require status checks to pass before merging** → select CI workflow
|
||||
5. Check: **Do not allow bypassing the above settings**
|
||||
|
||||
### Effect
|
||||
|
||||
- Niemand kan direct naar `main` pushen — moet via PR
|
||||
- PR kan pas merge als CI groen is
|
||||
- Force-push naar main geblokkeerd
|
||||
- Background Agents werken automatisch via PRs (zo bedoeld)
|
||||
|
||||
### Voor solo-projecten
|
||||
|
||||
Beetje overkill, maar oefenen is goed. En het voorkomt 'fluitje van een cent' bugs naar main.
|
||||
|
||||
---
|
||||
|
||||
## 9. Werkstroom — feature van begin tot productie
|
||||
|
||||
De volledige flow zoals je 'm in dit vak gebruikt:
|
||||
### Vijf stappen, één feature
|
||||
|
||||
```
|
||||
1. Idee voor feature
|
||||
↓
|
||||
2. Background Agent prompt (specifiek!)
|
||||
↓
|
||||
3. Cursor opent branch + maakt commits + opent PR
|
||||
↓
|
||||
4. GitHub Actions CI draait — lint + build
|
||||
↓
|
||||
5. Vercel zet preview deploy neer — URL in PR comment
|
||||
↓
|
||||
6. Jij/team reviewt: code-diff + live preview
|
||||
↓
|
||||
7. Eventueel: commentaar in PR, Background Agent retried
|
||||
↓
|
||||
8. Merge PR → main
|
||||
↓
|
||||
9. Vercel productie-deploy is automatisch
|
||||
↓
|
||||
10. Klaar — live op je-app.vercel.app
|
||||
1. Plan → Cursor chat: beschrijf de feature, vraag plan
|
||||
2. Branch → git checkout -b feature/x (Cursor kan dit ook)
|
||||
3. Build → Apply het plan, eventueel inline tweaks
|
||||
4. Commit+ → git add . && git commit -m "feat: x" && git push
|
||||
Push
|
||||
5. Preview → Vercel maakt preview-URL, test & itereer
|
||||
```
|
||||
|
||||
Tijdsindicatie voor één feature: 10-30 minuten end-to-end (afhankelijk van complexiteit).
|
||||
### Eén feature ≈ 10 minuten
|
||||
|
||||
### Wat als CI rood is
|
||||
Inclusief AI denkwerk. Wat dit versnelt is niet dat AI mooier code schrijft — het is dat de **cycle korter wordt**.
|
||||
|
||||
- Klik op rode X in PR → naar GitHub Actions logs
|
||||
- Lees error: lint-error, build-error, of test-failure
|
||||
- Twee opties:
|
||||
- **Composer:** open feature branch lokaal, fix met Composer, push
|
||||
- **Background Agent:** nieuwe prompt: "fix de CI error in deze PR — lees de logs en repareer"
|
||||
### Anti-patterns
|
||||
|
||||
### Wat als preview niet werkt
|
||||
|
||||
- Open Vercel dashboard → project → Deployments → klik op preview build
|
||||
- Build logs lezen
|
||||
- Vaak: env-var mist (Vercel preview scope leeg)
|
||||
- Of: dependency die op Vercel niet beschikbaar is
|
||||
- Geen branch maken, direct op `main` werken (geen preview, geen rollback)
|
||||
- 5 features in één commit ("WIP" commits)
|
||||
- Plan niet lezen → Apply → het deugt niet → terugkrabbelen
|
||||
- Background agents starten zonder duidelijke prompt → bagger output
|
||||
|
||||
---
|
||||
|
||||
## 10. Productie-overwegingen
|
||||
## 8. Production checklist
|
||||
|
||||
### Costs
|
||||
Voordat je een app "klaar" beschouwt:
|
||||
|
||||
| Service | Free tier | Wanneer betalen |
|
||||
|---------|-----------|-----------------|
|
||||
| **Vercel** | Hobby tier — gratis voor persoonlijk gebruik | Commercieel of >100GB bandwidth |
|
||||
| **GitHub Actions** | 2000 min/maand gratis (public repos unlimited) | Bij grote teams of veel runs |
|
||||
| **Cursor** | Hobby (gratis), Pro ($20/mnd), Business | Background Agents zitten in Pro/Business |
|
||||
| **PokéAPI** | Volledig gratis, geen rate-limit issues | n.v.t. |
|
||||
|
||||
### Performance
|
||||
|
||||
Vercel doet veel automatisch:
|
||||
- Edge CDN voor static assets
|
||||
- ISR (Incremental Static Regeneration) voor `revalidate`
|
||||
- Image optimization via `next/image`
|
||||
- Automatische gzip/brotli
|
||||
|
||||
Voor sub-1s page loads: meestal niks doen.
|
||||
|
||||
### Observability
|
||||
|
||||
- **Vercel Analytics** — page views + Core Web Vitals (gratis tier beperkt)
|
||||
- **Vercel Logs** — server-side console.log zichtbaar
|
||||
- **Sentry / LogRocket** — externe tools voor error tracking
|
||||
- **GitHub Actions logs** — bewaard 90 dagen
|
||||
|
||||
### Security
|
||||
|
||||
- API keys NOOIT in client (geen `NEXT_PUBLIC_`)
|
||||
- `.env.local` in `.gitignore` (vooraf checken!)
|
||||
- Rotate keys regelmatig — vooral na PR's van Background Agents
|
||||
- Vercel environment scoping om dev/prod te scheiden
|
||||
|
||||
### Rollback
|
||||
|
||||
Als productie kapot is:
|
||||
1. Vercel dashboard → Deployments
|
||||
2. Vorige werkende deploy → "Promote to Production"
|
||||
3. Klaar, ~10 seconden — geen rebuild nodig
|
||||
| Check | Waarom |
|
||||
|-------|--------|
|
||||
| Productie-URL werkt voor alle routes | Anders krijgen gebruikers 404 |
|
||||
| Env vars staan goed (alle environments) | Anders crashen API routes |
|
||||
| `.env.local` staat in `.gitignore` | Anders lekken keys op GitHub |
|
||||
| Eerste feature-branch + PR gedaan | Bewijs dat preview-flow werkt |
|
||||
| `.cursor/rules/general.mdc` is ingevuld | Anders weet Cursor je conventies niet |
|
||||
| `AGENTS.md` is ingevuld | Anders weet AI de architectuur niet |
|
||||
| Build slaagt zonder warnings | Anders begin je met technical debt |
|
||||
| `404` en `error.tsx` pages bestaan | Voor nette foutmeldingen |
|
||||
|
||||
---
|
||||
|
||||
## Bronnen
|
||||
## 9. Verder lezen
|
||||
|
||||
- **Next.js fetching:** https://nextjs.org/docs/app/building-your-application/data-fetching
|
||||
- **Next.js caching:** https://nextjs.org/docs/app/building-your-application/caching
|
||||
- **PokéAPI:** https://pokeapi.co/
|
||||
- **Cursor docs:** https://docs.cursor.com/
|
||||
- **Cursor Background Agents:** https://docs.cursor.com/background-agent
|
||||
- **Vercel docs:** https://vercel.com/docs
|
||||
- **Vercel preview deployments:** https://vercel.com/docs/deployments/preview-deployments
|
||||
- **Vercel environment variables:** https://vercel.com/docs/environment-variables
|
||||
- **GitHub Actions:** https://docs.github.com/en/actions
|
||||
- **GitHub branch protection:** https://docs.github.com/en/repositories/configuring-branches-and-merges-in-your-repository/managing-protected-branches
|
||||
- **Cursor docs** — `docs.cursor.com` — features, shortcuts, Rules-syntax
|
||||
- **Vercel docs** — `vercel.com/docs` — alle CLI-commando's, env vars, domains
|
||||
- **Next.js docs** — `nextjs.org/docs` — App Router, server components, routing
|
||||
- **AGENTS.md spec** — `agentsmd.org` — community-gedragen format
|
||||
- **Pro Git** (gratis online) — `git-scm.com/book` — definitieve Git-referentie
|
||||
|
||||
---
|
||||
|
||||
## Samenvatting
|
||||
|
||||
Vandaag heb je geleerd:
|
||||
|
||||
- Scaffolden met `npx create-next-app` — alle defaults uitgelegd
|
||||
- Git init + GitHub push (via `gh` CLI of handmatig)
|
||||
- Vercel project koppelen — productie + preview URLs gratis erbij
|
||||
- Vercel CLI — `link`, `env pull/add/rm`, `logs`, `--prod`
|
||||
- Environment variables — drie environments, `NEXT_PUBLIC_` prefix
|
||||
- Cursor: chat, plan, build, inline edit, rules, AGENTS.md, background agents
|
||||
- Feature workflow: plan → branch → build → push → preview
|
||||
- Top tips voor Cursor power-users
|
||||
|
||||
Volgende les bouwen we hier RAG bovenop — een PDF Q&A-app met embeddings.
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
%<25><><EFBFBD><EFBFBD> ReportLab Generated PDF document (opensource)
|
||||
1 0 obj
|
||||
<<
|
||||
/F1 2 0 R /F2 3 0 R /F3 5 0 R
|
||||
/F1 2 0 R /F2 3 0 R /F3 4 0 R /F4 5 0 R /F5 7 0 R /F6 12 0 R
|
||||
>>
|
||||
endobj
|
||||
2 0 obj
|
||||
@@ -17,22 +17,17 @@ endobj
|
||||
endobj
|
||||
4 0 obj
|
||||
<<
|
||||
/Contents 19 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 18 0 R /Resources <<
|
||||
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
|
||||
>> /Rotate 0 /Trans <<
|
||||
|
||||
>>
|
||||
/Type /Page
|
||||
/BaseFont /Courier /Encoding /WinAnsiEncoding /Name /F3 /Subtype /Type1 /Type /Font
|
||||
>>
|
||||
endobj
|
||||
5 0 obj
|
||||
<<
|
||||
/BaseFont /Courier /Encoding /WinAnsiEncoding /Name /F3 /Subtype /Type1 /Type /Font
|
||||
/BaseFont /Symbol /Name /F4 /Subtype /Type1 /Type /Font
|
||||
>>
|
||||
endobj
|
||||
6 0 obj
|
||||
<<
|
||||
/Contents 20 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 18 0 R /Resources <<
|
||||
/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 <<
|
||||
|
||||
@@ -42,17 +37,12 @@ endobj
|
||||
endobj
|
||||
7 0 obj
|
||||
<<
|
||||
/Contents 21 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 18 0 R /Resources <<
|
||||
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
|
||||
>> /Rotate 0 /Trans <<
|
||||
|
||||
>>
|
||||
/Type /Page
|
||||
/BaseFont /ZapfDingbats /Name /F5 /Subtype /Type1 /Type /Font
|
||||
>>
|
||||
endobj
|
||||
8 0 obj
|
||||
<<
|
||||
/Contents 22 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 18 0 R /Resources <<
|
||||
/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 <<
|
||||
|
||||
@@ -62,7 +52,7 @@ endobj
|
||||
endobj
|
||||
9 0 obj
|
||||
<<
|
||||
/Contents 23 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 18 0 R /Resources <<
|
||||
/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 <<
|
||||
|
||||
@@ -72,7 +62,7 @@ endobj
|
||||
endobj
|
||||
10 0 obj
|
||||
<<
|
||||
/Contents 24 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 18 0 R /Resources <<
|
||||
/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 <<
|
||||
|
||||
@@ -82,7 +72,7 @@ endobj
|
||||
endobj
|
||||
11 0 obj
|
||||
<<
|
||||
/Contents 25 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 18 0 R /Resources <<
|
||||
/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 <<
|
||||
|
||||
@@ -92,17 +82,12 @@ endobj
|
||||
endobj
|
||||
12 0 obj
|
||||
<<
|
||||
/Contents 26 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 18 0 R /Resources <<
|
||||
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
|
||||
>> /Rotate 0 /Trans <<
|
||||
|
||||
>>
|
||||
/Type /Page
|
||||
/BaseFont /Helvetica-Oblique /Encoding /WinAnsiEncoding /Name /F6 /Subtype /Type1 /Type /Font
|
||||
>>
|
||||
endobj
|
||||
13 0 obj
|
||||
<<
|
||||
/Contents 27 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 18 0 R /Resources <<
|
||||
/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 <<
|
||||
|
||||
@@ -112,7 +97,7 @@ endobj
|
||||
endobj
|
||||
14 0 obj
|
||||
<<
|
||||
/Contents 28 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 18 0 R /Resources <<
|
||||
/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 <<
|
||||
|
||||
@@ -122,7 +107,7 @@ endobj
|
||||
endobj
|
||||
15 0 obj
|
||||
<<
|
||||
/Contents 29 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 18 0 R /Resources <<
|
||||
/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 <<
|
||||
|
||||
@@ -132,140 +117,153 @@ endobj
|
||||
endobj
|
||||
16 0 obj
|
||||
<<
|
||||
/PageMode /UseNone /Pages 18 0 R /Type /Catalog
|
||||
/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
|
||||
17 0 obj
|
||||
<<
|
||||
/Author (NOVI Hogeschool Utrecht) /CreationDate (D:20260607134605+00'00') /Creator (\(unspecified\)) /Keywords () /ModDate (D:20260607134605+00'00') /Producer (ReportLab PDF Library - \(opensource\))
|
||||
/Subject (\(unspecified\)) /Title (Les 13 Lesstof) /Trapped /False
|
||||
/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
|
||||
18 0 obj
|
||||
<<
|
||||
/Count 11 /Kids [ 4 0 R 6 0 R 7 0 R 8 0 R 9 0 R 10 0 R 11 0 R 12 0 R 13 0 R 14 0 R
|
||||
15 0 R ] /Type /Pages
|
||||
/PageMode /UseNone /Pages 20 0 R /Type /Catalog
|
||||
>>
|
||||
endobj
|
||||
19 0 obj
|
||||
<<
|
||||
/Filter [ /ASCII85Decode /FlateDecode ] /Length 1911
|
||||
/Author (NOVI Hogeschool Utrecht) /CreationDate (D:20260612063152+00'00') /Creator (\(unspecified\)) /Keywords () /ModDate (D:20260612063152+00'00') /Producer (ReportLab PDF Library - \(opensource\))
|
||||
/Subject (\(unspecified\)) /Title (Les 14 Lesstof) /Trapped /False
|
||||
>>
|
||||
stream
|
||||
Gatm;gN)"=&:N/3oLl3SA3H?=Y5<se:?#GYiZof&#?RaT(dLQ(La6rF?b\b!)]'aB/3j4\c#i]Jkru#aJE!&dmq;`@0n_K()))Q<#r%k$0YDt;U%QkYguY-p!"hC0!='5CrISj#V;Sl%%=#\(V]P@k)f2,Yh[b+m\Ba]5#FU7e.uOGu>jD,C&>(tcn*[:o`8qtEgVe*f@!@#6T<H-?Lg*<'@?qulL,2p8Wj+$i6CHjo0O2EHLQC>qiOt]&4IR^a^t0codB%j[c93"LT+CKTc3lXe^Z8QFC:TF?nIgTN0RT^FD:l=7:)j[\g]!2VJI0,a]MOg?ltn_Adh(/dSPQbn[3:l<J\$FF05q&c7H]D00!8D\[c`me9Hh5<5WEoZSW2HLIDO[C$^&sJ(__Q`%b#4JCG$2?$_XcE=A[:XS2F@lCZBSI<R.$`['`A-B9>V;N!-nrq:1P?ME,@p/3IO]I%Lebc7r=P8K&[pj)iqa5a0(V2mIFb:nKdtMk(+'XN"Ca7:U6?%JJokD6;4:g&K'pNB+pcb\&L]RrF,d==\NNl$$8:Dr.3!<u25k(9a`L,D38<R4#KE%n!/B$T9fnAFA!O6>!3M_<l@TYMjMejXS/4hddMEA@jH&F"kKOU=3=3!mYmmkj2N@6DC2.RAmiL1^l0BAnnu9MHH/f/7_WT%B!<e3QMotaE9$U?n@Y$qOKiR=QnMg`.WiP3ME,PLURqG>1D?1pCJRI"r2(/Y[1,!?@m^NcSc#:6>Dna+=\Uj;A&X#c`Nj!UC&^To`"Kjc*hIR9k)lF-uQq"+?hCk)3<g"qD=GN*d41r+R9Lc_ESCMhas2"NM<Ur[Vm,iE-EX:[E;nJ4-/M$&'Wj`3Zi#)>,HKs4!8Nh>`-X>Xcb-3jjV$A]%@$qR"CZ93n3>6)(#[P6Pl-MYTOLFN[5sVa"d8ZnJE+T"S6p.%S?)'0p6%;ht`ke:HP]uVK#H(\<$R$__g``Cm!uhqsDbnGBiYo0YIu)YhbrtGn_.kni1auV`"7uH81T8CHrJl&rDJW'bD\];YO9s2GqTO7Q-7Q\W60gAWmM2*MV)-^p"hN2MW?.Dms>G6X'eA&E.s6RoF"mJ(h!!`aGV<#A;Cu:AIUY]FiBGq^Y4>(O5_%Qq0IETAn_$RQ.6q,UbaN8h!IkFfabP<!m/SElV]u<@Vj^%qDKc/(;M6WY)q^%@bWpR>mJ*A83kk]=#g>9o=PJY;VuY46GqOD]#@>P"Sb=4Q$b@=$c7qpjuKu1UpDBV+Q_.C;#SCBNE`Y[Tcp&7R/#S'?JFmF'.UO-ZR[^*b01aA7:e?>c)3MC:j<l?,r@*[?SHfbi3T;nN`m*F!Q%,FD,l2WIbX^rsXD4I_E8%^AJ"IL!#t0.$I)+nQ]k:&p=pP7\us[,)k[59Wdg&#?pR.mD#qTn;^\h%)Lc'['me;26,6]X1'B[0!!h/f9re9lr`!(@V=Ze<^X,G41W:[-8I,QQoLil9VWJ5F_1*V#5T78Eg3]cq;+TSMi7>1&Id$E8oqMTL'O!g-]*.7";R051ZZXZNJpnX1>^cOMp`OK>=`6.$9\,<cc]TrKQ5_<P(`39%j*;3;NbG#RF(c*[oEWM_pmggN0c)AS^,`"I#t)W-U$4o'[LU*OJ&Os?C[',$6odcZt]1m\)HUT9!VI)D64k$Lh,bB";L;*C;\T.5^hnZN%T);8\g0Hi7oPQlqqZd_IT8APMHIBm:tNb/O;7_9fY5gIV(?P9gc73Lb6kp_;ltpl1BPKO#sq1f/Ird$dWP._sN*>8Fe_c7&9m+?ongOGWh]?aWPrAfE(+kPi;?:F!P8^.<Gf^dr7b&"3cm,nQfbu]Z?=@PWVf%4mOeUl?P]240?ul:f"tFN:p-3A*^0^ENW-a*]1HB-e4gn;4i<tfM#7=\%^`1K(U3Nd"HW~>endstream
|
||||
endobj
|
||||
20 0 obj
|
||||
<<
|
||||
/Filter [ /ASCII85Decode /FlateDecode ] /Length 2006
|
||||
/Count 10 /Kids [ 6 0 R 8 0 R 9 0 R 10 0 R 11 0 R 13 0 R 14 0 R 15 0 R 16 0 R 17 0 R ] /Type /Pages
|
||||
>>
|
||||
stream
|
||||
Gb!;d>BA7Q'RnB336UYU?s:O409TI]B?IXsB"8T]4QQUo;%g@HAS``2l1DaZO`,5\*>jO)9i='1o]_n7*1`!:=SSWPru_Mj5(p`[$C&AG!n8A'$t$ooYkPOqR)c_s!jRGjq[A:!Rm4uk]`_T$GWt^J#`BA4*H2--6MLU!Y2c$ma:q=>oI(:==^3pO"EcldXf]'h`u&^]&D-OOeb-=t_MI@gYEf[;XPO5;1MEmiMeY>lU+UXu(dn>j]?d\=V4O\LQt[SWV]o'UXFLmm*0+>G*p%[7(MXB!C7uiB`[WDY_sd_^:X!,cI)f[67Ifg+^K*?k)t9",H;_SNa_@,5?lbd3aCtb;Z!'tA$"DX)7pG=*]OrHD_KJ&^K=Js,;Veck;3,^+=cV^)0i?8;#j!,nNk!?(T\#4_%2G*GW-68&H+jY<[S<e_XT6U2J-%Y';30_?FSV]^!a4nC6-i_,&`K'WaEhj,7S!'C2sZ_mAR:Yd#;;&)Pg8S,;7f?=ACMes:hBeu>[+_41`>]5L!Y_>eS>EI/+J&)f("_i:`oZ0*ra9(Ts@)&dFkQ?_?A_ql[iC&qn0c*?L&&-]=["!"K(;N/.\gnA3u-QG;dE7<<?pG0n_P8*-sUbSfOhASE_dW?m-0h<@j=gi(%?=>Y1@"Kn?Q\,bY04k"/C&3g!9AW3Cfq3A/eF9L^KTN<]@->%]Ni&1`HI#@s:c0Fj?K3nNu(!bU`QHr1d"@=hF@*(IA@&`6sP-'7>13$n<tfh@DN3ooY9g6+D8d+33"dR?dmc]sC-.bVCQ)baLSFZ-\NT,Zs,$pe#82k0IA&SbIX+7(Sb<g]_gq]j8Z<[%!h\J,QR]@T)Oj"kM8cQ&_-%ulpA4"U'hf3q?[Xr7]r+2GKf-&6D<U%AAUL;h@33L&RA2oj7Rast1B^Q+CfYO<_>o0Ji07;._$Akn<Q8#Y`N;TpneL&n;+(L_El6ZH`:3lRT%^/OPXH.++S24ZrI=dZ2kGb_#^<4`kCKmN`[RO>?KUWlD[:cX9P"*FCPLh1aT!$Cb.Bbl`\KR(<a8@b)uITDOkc7kjPZ+S!trcKhZcANQD-k'Xk_Zju1EWl9EYk7ukl)@6jQg3^'UDAF@&<"=*-E7>`S\4YMc`YC`aG-ZN5(.ZmP5%^BN*r.>WM3B-X9nf[ld"$SA]Ug/Y[g9?LC9N(nm"Uu-SN73;1EeC#6lN;^=pfKWRfNh2$o5W^H,+7FfhqOXHMWj/4'Y?Gb-&r.UQgtCCG/ekGQ:Lk7W-*_c.DKXP7),1!_/)ZEb5geAB':qlf]s)5q@!Y[JcX.U;#*_g0eE[:<([&XHS0*,6ETO'%F]]-F&<'D1!5arMZgO-WW0)"nQ3gX(IL/uhLF"RQI]/\d[5Jd^!%fiiu1bIpSJ+dP/k93LTR95U!()k.0Rr6br'VOkXPIRu;;)8bsmErEfSdm^^\HIqqS::<'Cm:lmB"ng'h1;?D4[e,<R.PQ[X==9km,%L9[Em2>@FS$DfpRkuJj`@Vg#p@SsHXeRs_quT?A6;@7;1B)`2<7qiN<8T\<YT9IF>-6_/=D5<ci^oh(9AKA5mMnr)G0D3lp8'b69Vf?DtNh.h=H%6mGs1;IYdrFrt!V7kY!2=<+&dhW%0jL17aF$O5>Vk5l)67S"&Kd??gQdgNE!F<q)M,k#l0g_J[V[^DF]WM;nlX8q=P0F'VPb?I[3T50Ir&BCi-a<7e+p$LG)GEZPt"i[&5XW^cqk87a..*:ENILM[]B1FDO88(mYpma:^?$C;U9mcB#Y))i(DmM#D4*%q6$2&hc.GL$J*@%YR,lRJ[!,G)g-LhDlJ)uhid=*d+.PSMadcP>S+*<Z#?)N/o)8(W$t^VHS\mqhr1$`o?6&lqA&%^@oCeX=p)126!8C[(LrGd0D7rGEA#k%?+iS/A(WRTq99VP4C<a[2;r;Ff%C%<Y'B-"?NAJ^dKh-4\0lZ"@4A0hq2Ol6(&BZZ0E"'HL\5Op<cO-%X"oH44<TXD$'-Y[n[U^&J-0#X3V~>endstream
|
||||
endobj
|
||||
21 0 obj
|
||||
<<
|
||||
/Filter [ /ASCII85Decode /FlateDecode ] /Length 2085
|
||||
/Filter [ /ASCII85Decode /FlateDecode ] /Length 1961
|
||||
>>
|
||||
stream
|
||||
Gb!#\>Ar7S'RnZ;3%.hO.:SaA,`9am%)&A9ZDL((p7X*87A4QEZK+W`QQjM#h]ZQ.E\;Oa%`+;N&h3B5(S8et0qe<bbl;nln9bLof*iLOi>@qP4;]-UX.f.0+^FNPo7=E7\fh:k](uYhM3EKDL^8:I5gk2(^]R@f,%W0Rj1ZXD!>Gm\:E0Qa&2]Ii&BT;C`JsgK&Y,V0O8)%pfCuK@P?PY]Lm<.^B)RKm\:UiAZDaor..[N_L,8&,4oT[3Dt8`7j(Y*_n97m2GZqq>o)P@&p7IU];HE&s?E0hooD)'keIE#6@/*.5lJR"1-U+Q>Ndmis?nUB6XR^??q_"<_=TiUIk<8[K!>9dVBL!k`>oMX(Cim2IU1:"b#uA@\iBF5Zm9JrGJ5GOQc5`1O<?>S"i8#9c=VEl?%W@_F9LIL4&D)V"=DX>SCTIT^7#VI2pSl(".;L#'$Uh+]fV,D_.1WGBX3K4SNodN%F\GKnhtlOAYBB3!i"\Y__P;0_N"R`\GC^p?.t),lN-ER$YhI@RY8r"!n3b'2ip2D.+i`mfrWcFS<RHS5$#uDA4S?nY']W=rnMJ?7gS.#*C89eC('U+#BMXQSpqq?O;+8Z8'76cfhkqah-o^,+-lRj9CThk_RW-2u-bM?bk'00gkJ!K[*=kl[NU;r^&;!`>#2L0MXGIrZa-a(/.1DH3$]+7c3@f''B4;WZ%I=Ki[0%G3JWu_GZj!5,/00t\[=PctY4f?U[(!LKV$itW"+"0FL*Yi=$OC%V#c0PjhI$?2Hp3q\kA$;qK?m[W;dTb#BVJ$t?JJkJ)Ba-`l['H])CR)MB/,>]=^37u"qt"MX"d+-NB,^DbKX]E,b?A02eW22Ss:$kit@qMoM:]NFVp0cRo+S#?Ira='S*C9Wg]9$"(Pds:Ipudn1n*dC\2P3>/RDE]:5KWT*RB;(ASRuF]%Pb[8>$p9hqHUm=?c9Ke*'icc/gE<J[k]'Ute%QNSr(%JFJuO3QuNL8OB?X7_1H(.G*k7F"NmnZ;f9<U[]V<Yp.Mg[ru))X>)L$N[BA\g[b(o=R%_WQOr&X95n\H?e$KU?6G/g8okeQ7=`g,pG1Wh:]u#5h-GSJif79Y2V8(+WX^TrP7"#T=m=>F7gPKdc>4L5s>OTbb_;%GKQ0.b(hdSQ$"at?::[$^ncddUkE:l_->+kN5Q0KEVaWt[Bf43f8!GJrFLOc'VdTI4,6]%c1%n]R&LEHdj(ejZ'^Jn2Au=@2ZB5'R7^D3N-@7/rVVD3pNO#\c]t%X]d]jKDl%T`K&3CRbEOLM+N]cr0g^43f<6W=03-$&]7;M(Eh)N+Q;g_Ep?.1`ra(cTCr0\h>HWF3`k5u!e98YJbIGk#nNW[$W)G,.3fO_8Ea@7i0gaBJ=rPl6(`Lj3=rPl6ck_SKZ6#CQ0Fo6u(7H<<BMkcEkC'_6oeCh1#QmrQT4Bk+M[EZJmXI)8X16m6[i[*djk.Jk76Z>oKo4mWYXn+o5`IfD<`/LYn8%"J_?dW@kpT[a9:CrJ+=VlBmDN^ucS`Z*F.GdsWa=]0(F2k;=ieW!BtBQQXH>SXrIa/4klj14\0mo$L;/-oo]nR4=t?C5_Wq1m2-$+2oSQhE_NLO.@+Ug:`EKffX)kH6[B!T3M=XLU2q(S%K`0A1H#T_f`&M=XTI4+Xp?)ZNH$"pLQ4U>dQll6mm#L+A85lqah*Gj7a7q(Z8VTqOF!X\VI2NdiF;MqgMR0&u?.5,I(3@`a2q2ij#+=(Cm.#qL%KKLNJj#jD5h,$^fAC[hf*lG^W,!6('F,cn^8hLp%n*R6LH`YIU<<*T42SnSnmFb:B[Q?6`k?Ug=,920_poO&O^buupmA3"eGlMq1PR?+H30N+(hpqu>HgbR!QVZ:5[bXZ@f=]3)@[VPqDbFg;>i15`j9-`2=!;mRVn+UO-sm6B>]DZF-T5JUd-u:oq&,3S>$ee?#TB3Cg4"[qHLhkj5\$#2ZMnI7@]u"!r=JLU4pn0.Z#DK=U[<d.P_1;ZeU19'V0kSR3)'[I"CAs1%_NIXsTMO&>b\3p?"emnbm'Uh8s?S>Bu\?^IB+594'oU@ht;JP+:'Fn!Yler!Q#deee~>endstream
|
||||
GauHL8U$Xp&AII3e8:ER9K.;N1S2PF,gS-3=Gh1i+UD7,'%!ET.@OU.e]jgo`:82_Ca5BP^08+J)?L+lM#&3j!.U'giZA5T_KZF+dOc<n^t!]!@mCr)L['mqI0)$DFO)irjMT#!Ff9*/TUVN]q@n&/PWp:&"-cA<2%GRD<'c([;?^&-L,Z2!D/`:M$i%C`q=FiLk:h?6Q%-Wr0`]/=:!9dfJ>37-CNTbU7&Uta+XV27(p@?B[Lr`[n-\TN#[-^&Q9b]]@l)10T<kbT/-lC8Y[u$Z>/]ubYD^sU6cqB*Q=G5FW-=n+7&b8B,#S[UM,:+pYF*/q/m,b`MGtGCo-%@fcn:5#/kQnq418Z+cVp-pck*X;L7e'j`DN/M%RkdAI.mZuo,0'q#L4"X=pc$/*/_8d,+^N27WBhD7G4c;HoXOoSP+,/,krcK-c3#Tgg4;s#d2S^M^7GZ5@Y[Jl^gO@B8l6R*PkZqqAj4*8rST)Vl].'B05OAqC\q_qQ*k-l]jLQL6nk(JX+#[l+LrQ\3,=&HZKgMm5+eoEuTY'*nT,TeBro;huP"@j*YH]523O)*d?7.PU=dp69MPNFssWO)s#Z.\7-=,R4-u,qcW4UU>)e*X+TZ$-P7oiTZq2Wc+5J+$)(`mag;I:[33dkK(K&&IPsu<F?M*RUa*&&npNCC"Nrg$Qo9!i+ck7n_q[bJqPP1=dIh1jlC+f^Z'DcVpS\GBQt]oGDrBKZ8_+Vh>knIXJI.^"+:B::35B^/cN1/N8#CNJ4M8B'!"!!86%ZY\i-EU%[9kB+C[2cF%GoG&A^/H)-0/'h*#.9JMh3'5iulft4pSL,I\R+KQrp:&ZgRlW</piC<*2"2YRN<!,^LA_#Hg<@qokDY=.dJWUcoo]1V(-Y:-7JtqMYT"U[(dhCV4*bVH='RJPB^q-G3O4'sed6cqoJQ5AR\?&iID-p/"ZAg%/,5"A5Y'B5aC5fe^5+pZbVhg]#1J"Lm_f`<t:7;C6?5[528jRLUHf3Ai*beCRFDF&#IbVJgV\:RtsN%\om[@U*J!rI1O_Id`nG9ZPuo7;<D2f@(nLPZp9Sl<@)b;((A)qut=^`/Wn8"1<EU?lWt[4.EiY;##,NLG=P<9Gk.cKGO,8%C1ejED2P'P=N`H</2e`>,p2id)S/D7;LC,c:Y0(1-_'adtn&`f5"N:Yb'cL="8s-S2J/09GONNa;>D<e"B=U6LT+4oj$>B*9AT>(rUV(Cp%pS@">P6"dVo#>k]B^=>%mXT'pOIP(D^4P&[g=H))d-FMflhm1lh$#m]:T8X5D2%`Ju!4'S\]".`?fNdUD\%#F=tSm!m1*^e'WEKp,Sk_ot.L1Q!Yo/.LM%&P]*9CgOA_'\q,%h7?1VVnS(no^Q;l?$*$X^e]t?O>Rt+\:X=VWD'IQn%WDCH*(Lg[BaceG\u6Q<9a@AH>e9c<.qq;?,UjZP<Jg>?3N$kBNI-CKO75[Z2qX.N6Jk7\fjhj#Lf%hR2I^maW]<GCDo`97,c(7ucer9938eRoa_3E4$EL#>`!3-.ACjK%X1t[YT`l@A:KndGf.b_Nm;mfl74[\sV^fWFbjZFbcl!`;@S>rsV0X.iIAic:4L`oCC3kQjq*VTFY?r=Hk#LEP>]@Gh.Wrk@29V'$kVYYth<ie,M$W_cH\:UDkb]]s&F?ITR&Q4>U\?\PD5U.9#\;Z>b4!$k<om9gP;Nn<a](8#&79%8/@IL9<#d,VI^&1_s2&,VX>lCE1D4:)l7!M(2hrVr66Gm:m5>5SOSg5&mjq"5#=#!c7'AGSQM2a]fFK,>.c(ksUt$fY6[!`hmq&.r9uhJ/X/K5Re_lc4kmZZEAbq1uTsp*u7Q.O[uqDS7hD8ebtD)+Ii#,`.[;>\EJS?Mn._UI`@P($/*Ifg8'UiBKOmYU&KjtF6j4)7np)+eFb@+>IKGi8A6I`*p>M`\rrP#b)s>2@+<Dh0_c_$&3BF~>endstream
|
||||
endobj
|
||||
22 0 obj
|
||||
<<
|
||||
/Filter [ /ASCII85Decode /FlateDecode ] /Length 704
|
||||
/Filter [ /ASCII85Decode /FlateDecode ] /Length 1867
|
||||
>>
|
||||
stream
|
||||
Gas1\fl!<6'Rf^W*6hCpFZd)e?LjD^N>a6<2qjTn+"6Y1/H[G`-rQ=[mq!B(1hcquI(B!AEu.IT\n1W]G/3DHMfT3(OK9Q^f17Oa2IX?<=%6=5K1A[)?@i!P]hkh6J_4r%Q'ZI,Eb@8:PMA1eTIA)hBi,&8:7)0n7TcS)SW&Oa:Z<U:PTjke@q5GOnUj$Wb4FBsl:>Oe&/]]6+fQ&2dTjlG`^k84\pFDL!q49SW3]3$k4mA(+knhU.V3UC0gD`)D;Bk\]Erd*05l$-BTA5Q6up^r*T1_8GYIZLj'RNZ1'0%M;m(H!&*\CEUsK?++/u,a+JT.7`bHumo[F8gV3'P1_8`l9Hs89o($6tm]>oqGC\qS%[[_%Zg;(feG/S?nKfufQ]hcWp0?^ISZ0n:Y2UB(B?$CX7.gMtL_.Y!jBp1@LEphkpR)$YYPqb"Ma_<PG*TEu5VU(3<=>sCMh%.<\4G*#K=E$4Ca?e]eAuQ2\bY;VRf<nUlZH_t([`Vt9&$e%HKN$)qAX.sp\!u>bH&Uuk/8u9&?!>U"@!Qi7<p=JX/<?DFPs&[mE>pA"*+olORo<LkE0o_Ala.f9OV%X*'/PbWI/\dZ2PD<JcqI`=%b]%$MYbqrU)+FoauN13od%_3Zma+7dlS@!F7%TZkLZNqq@qh!2s%6I.uW*TK*7$gIa<M>24_Le&DOL8FE#%p!m'UG&c~>endstream
|
||||
Gb!#\gMYb*&:N/3$sK<C\huiQpL*H,g?kVKZ/)($$*^(6g/]D[Xs>C>If0bUS"82oSH?daNYPJK_o(G<NlZb1S([6U#Pe6;NaFSM*6r344Q=3Q4#fBI^,W3VoE,`E4D#.&:*Q<VW/%hg%s[=65hq1,0aA?'jor`Z;5iYHpgGG*iX!QZK0mHA3SH=Y7Kc4''!]M;0PZa?"rE<.^]JIp1l'JV6AYFLS6s//o/e]Y5kI1!>LD>]R`K988[Vj\jWWSH!c$d\$W6,89pSu!(*8*Q2nIXFGag@/;1Zed;>r<NZI\gDq"Xp"<\Dd5>Y.q\a"sh@?![UVa8"?#Z"D6sl_!rrkn"V!gr96P@H=s-8/Ob)U[[f56[ZCXG:;qS#_-FT\+>r7p[Jjb-9d#@7&,^/pt.21`16l4&?>oqm^;a?lf-DMMm\]*=^O\=f=3OM>Y;LUd`ZpD[_Np9I-QkC[%U,0AT<5.m]L[4S#CE_,gjRj*^f@?@9B_G)p93SU.2C0lJ*$alHsR!,\gm'V]0,0,LlIU2:[\sQq`,>X`<M#[8;XO)aRF$O3D#XN2!`7Zo:JO\Io>*W*]YoYr;IWCANDnBAqXh8%/eL8tu2++-W'J*ah#s,RspgEMGY0<+D*n1S(B=G^EtFROf^@UiF*0roR`9V<Y.q#@dmISlY)\BriK6lR=5FQ2fok]#nRZ!f3"H/%eDq9er8q%/r\3R$fXpSbdF.+S?$KI.(HnAo<cF2,!,..Nb6-G&<Zi506\J=(3f(AZ9n'[rr)e4kKE^gCBdK.[rj8c[jd;H!"SeStdj"JN30e@aMG%VKWm^B^Qi?7P'l.?p(%aDAXCQ"Wi@F'=W9f@17QA&RK-V2+%n,FBoNjS:<2V9,!$qUHuH;\MPNA6f_>ZGn>_f$0"(r'9SP*.>Q?V\K79#X%AUp7.Sp[j@+OPR_EV&I*=?$bL%jS,/!^cEu;S_$^ju@+fIh!C7.Go;D=Bfc)k#neB3(hZ*NL^(YfPhLc0JF)7QG`U!)`>V3KiCIj9lPMSLU5`!dE\/79ZeC":B.$ZT(@E^kf\[26Em%I%a_iONAEn/rV)n7^0gM9)@Oob'992e`F[R.!7?P<I:?G^].$+u4'O:5p<XZ7%6qd]-O\"5kdZbRa[P!M;YOEBo-c4qh=f$sD*O)'l^mW95u(LugG[O.O\),IBC!qmjF"TCMDo`+mk_9EA&HZN6Z%:4\@]Qqi2@kcBmgX+Mrea3-Gt-YC@Gqbn0/4t2.W(,.q)>bhJ8HC]*C`u7ECiCm9+?bjj/DrG/).[cRP`R2g,UgSkuKj8@`$7R\&2lS![a)qR)InIC(')<3G@iG_05S!MQg:3@X=K@+DjoT8T^^AXS.;CU>kj69'K'Y,YX6X@N==^TLg;J>`A%b^9PqHqr@mLFkW0q#DDSI$";U/XbS(#j\nF?.[=(eEI8,Kc1oDuEb^fBG6m?pf&7643Zr4"5^TD.cVBZ[A.R8&G+W\niFI^bY3l?+EC>ANdW.C6S/Kt,"]b`H%#[Bh4Sa!6_7hi%1h*atQKSr^h+2A$T!pn3Wt\oV_Q69](,qpEW<h/>NZ@C.X]YBP"emU=Z?]XSXRYfq2lVJ?'KkC<Z%Lc&2p`0O<FDgWb1b_E,<O*Mat7WhU:Ei,k<7$a6=G;2,lai+i3_[W?'<HkWY%OB.oPkgiLWP0g*-'>0'ojQ@M>"DYi$!&(Kd%t)Ml-:['%U,WdBE[TfVG2e=[4X5Jgi]]0Wg;1u7A5VQo7VhHc,ukVCiZ5`/?Nut!b0Iel?3.rA+.pO(?cYV4F7;0GHUXqCaRMa-Z/ZmEaE9YSYik=^S^+q#NcLjh_Q2rk&T;26@,Jp.[@'(7j!Fn19MXOiNN5/%%R=t/(F,j~>endstream
|
||||
endobj
|
||||
23 0 obj
|
||||
<<
|
||||
/Filter [ /ASCII85Decode /FlateDecode ] /Length 1519
|
||||
/Filter [ /ASCII85Decode /FlateDecode ] /Length 1726
|
||||
>>
|
||||
stream
|
||||
GasIg?$Dc-&:N_C9_5'U8*g*TQM0seBp;)dl_q8OIN5!?K@!f=7Nd=7fD5*1?!tJY[OfP91,F8ipYM<L5k2bYs'u6$IL\ji)))QlCLE;rR%M0)mbN/A#<\57&>?%Y306[u5:BT>%XpZpLdqjf%>bE<8B$B_OTLPl@E:b`2-dof(m$GEToqYTo)f_Tk:]BA3DM/_*sBu4rs#)Nn0/K%kR_A)`F*AqR\X1f+UM_/kehcMUXLk4;ts=nFHrqc,(m)2;CCcN>]Ln&nT9.`<GAki9DG1S3\.WXSaYFPr!)<PbZqjV;KCbA]Vk9qF9P><?E.:N?&+uiX#,mRn&>)9f<bu$n-3Ps'bQ%P\/2>$\0l6(P`UG)@,l7!e@0<Ho6<eSQ)Zd_<F[<]$No6-nQVL$F(9I4'OrJS)iDupo"i9\XL<*Kg49A-[eH31n1Es4;AY/#=V?d=-o-0h+<+s[7PFAO>C2Z7k#63sd7;(p9AY2tL*"#[A!CrO\,_8'L%:Mb1==hm_Wk]]IdP)bD#B+Yn)G?VYR#OO,lT(HVe(u0-G_kuds.QBSKB%C&5brP]Da(c3[QDaij=N?6%jLJDn1JIR9o.E-Td"s['^`:S7P7;LNj%S^"2EPd]-pC[j#2E&l'g(c`^qfhk*Sl]n1+#YLENm':a\GrD8YBqDE+-`<AMt]5(,J93WqUK\$U=nRNKf28Y>aSlF3!G!iEo"GBAcp.&(tPB^u(@Ju9S0H8/oq1qcQBS/)l;V&o4E$H4"HSEYQQ0Q&[>qk!WA]Up`*,(,8naEdJD?n96+L9ds6emYl`#0'co+F5^CoMQ!"R!AOlZ-7kfcuGaYL>%.%2Z*(64a_XQ/X^=[O`&m<CH"snms^>4e>t#c_adY:]>W&L`[8eVN4#L[Z\.*,#g/Fe&fSi%$njGp(+Y>5siD5mJ#bWdZ@ugb3.$qYPW=pF42hE?W,t8%W:qaquZgJq]p)8`bZ/RDQcbYeLj<'V)`pls)EY8WS6?Bq?NC&P`aNWHqX,Bep9grakW56'@5Kco(/paoW"JDbNM9ZM9\B\l:mNXgGE9N-t:dRV3HBJYI/]a;:+QHbbaXn%@N2IaG3G[A@atep$CBl8Tc^>Q`ljb7G8M>2Gg,F5h,5]3Ne<i@FH+1X@:d1rY=V,f&a+j(S',&8n36G']h[ICtUVBf!^:Q)OV,1o5!pBb$oXgm*al>V6gdQq[\"B'os+'QTW!:9q8rUkKl,9'0Q/*5fF\9%(d?:2OO,#eIMlb29*\hC*5E?7DkjYo43iOk=:o1<<n%3rc=(Y4b,%t;Eo+sF3n\!PN"K\1UnFXC600%S6`il!6WPC!6L&]1]D8g]i\8<WeTOG=4kEn663nqD]\H-U".,Z@k^)XE,INn=<41C#a<K4EnGn7B4,99c2^Z+"]KQ[BTE5fiF+R\Sr\d```!3!VRSoNg(^q"S_\$b3cJc'hS@!uQIFSWN-8o(i84ST9t;\@dp&nrgR8q_CcIfOg3;SaAQk&SZ6%oZ(VBJ<M#t?i!QG3#BE~>endstream
|
||||
Gb!;dD/\-!&H;*)1#J"l`5]E<YH!'829ket<Gdf/I]7F0li@l\9p[/?oBSJRr"^shE[XgQLr#\=R:g]7GmO_F?G-3*/-'"9i8P1N/mpL?mP5SJn)8:tIC"/EkmQWD,2s0D*oeF,[*JNt/&6d,i1%]q@t%h_W[8K1jc(:VEBuJgdU(W=)@HaV@L[NZ""O*8p2U9kLD`EV//D.`%\jo#Sf'=KoipEG>t]@knIfF?gbqcmW.*8"#(K<$R2^">Sr5O&,rB!CWT>BY'NS#pjTG9-6i\=u]4SAaggOpk9#l2qa!ucW@CDIPNM(6Bfo"-=56:tu;fL\7&<,@+ni=)/cupUCYE`<E\4h-h5D"UN"p#+L-uA?`Q5eOi@=-0,q*rG<[VH-(4(0_)=KPQ<!q(DQ\Q+&2@Z^otdTp]2n4KXaTXdEo[9k\I5)!G.Z/';0\%33qDN2d%94MbF^.$M"9WY3NIR_%.Y>(g\",tCDZ<:9uG:+SnB&dD/DVkae'-2_<8FaG`OfgKF?$$RVB:7ea^_(%^No^rU%kJ:Q5N)j`Ac_$Q/1ZlC41qN+en^lE/F&3;0D(\=g7eTY]0$XQ4EuQ_S55B>c(FICHW&p.'cBo:a-i7"'_9mQL2NA4?`mI/.$d,ORBE_2d2#V(oF@S__s]'/`7Zi:BG>4dQ^HBD#K.dUPF<45$MA@u5<uB-ATsN$M/-ffJKhipRE5b^QtK_J=C?p#hRqu2INq+':&'<<+Y[Or5o0.V28@7KTn+I]<t?8Ts.W+>gSBd-8Mg^>9?bM3KR'OFmLh,A#fH&6CY/U/[i\U)_aK,^/.8:O[GKcfn+i5tf+oD*G58Z1>^_d4Y\Y$gat_1UY&$hhm(%*^hg`);c"ueO?DHc'&o\S(%\jBS>4LOJ4?=/%(HL@?<DV<</-s3,#lVdeI$_tN%_T:4iPQD\h9WS^<`0b!`rVFm=\q.LTQD94;W=^[OtYlI*u_6@,*Tu@nkfPh!,QWjf^9?U`T(@@;=O^+$T3]aXc$VFR]-14kI@RAjT*`8!;2?PHH%(VNQ\EUNS72*ZD\GJpTf.PIkN^/+5<,AR;,@U4t$)$3@aS6EsALgXjVT#T\eSm27ZZhO3FGB*+q7+Y<3*a)X25'EGDC5\k[:o]<%ZK_=6pp@Sm7&A6<#@-Xh:+Ef&Ki%HEmg#7u1[din;YqRhSToNK5=h!NAWjatJdC/)##:kAtF%UZ@(I/)2PL[2@ldR2.5IQE.aXqP<nAA$glVJ!G5-V1#2INUD<0Prp)CKr$Cok()BgTGercfb&CkB>gqBBtXL[tm<'L\f%(pD;($UN?IVW?J0^JS6G#+cQg9Tf^74&NB%i@?K`.e,sSqX$slg\8PMV`--Fj35[[X`CugI!o6)9)P"NtU0?!TBO%K)XS_*"&^[m+L!@n8:*uo#_V<Tog2A!Wd-U;-B5$QdYnFh/rXE59P%'8$=k2?W0_U%YeMcNR>B4TnbIi=aDlbTD`#Eu?AE^?Ql8+KUki%G8($E^2jls3C>Gbjf7u4-=ThsBXg=a8pnR(p[&QchG3#&P?#[N2f$Ri)i@/YIa0b80fLg7=G7GflqHTr=WcP_GN]6DIG2DtX"q"Gr.Ngi"ha,mK5Y2jDniQh(.jFco)`Gg_?&nFAiW3k8=]eTBf$HK!"Kf1c$me_'R>usXBb7NY.>D#iHU`;.GmW+Rd&^p[Pq\L_EV,q\ole@jOVn3)nUo[`Gkmu8pY^QXB=^$7~>endstream
|
||||
endobj
|
||||
24 0 obj
|
||||
<<
|
||||
/Filter [ /ASCII85Decode /FlateDecode ] /Length 1952
|
||||
/Filter [ /ASCII85Decode /FlateDecode ] /Length 1609
|
||||
>>
|
||||
stream
|
||||
Gb!;$=`<%Qpg(Yo6\0,h'/QEoMX%DHBt@d,dbAPEr5)"3X]U?Oa[t)k^Q5sS[OI)M[1/%iaR;=7JDunqs'Z9(F:>(n))*#L;d_4t@=/BrO0+HYZ+986*Y:h<-a8.)Nqn3s=W)ISnA#MN#jN)^]AO#cKEq_5Gg?&O%pHgAZ,M7gSGL)+,+-CSh`C3;DVL!$,p)6ck4s`q@E8f^Dl?#YlQ62G*NW\LKBft"1=,]&SAN[s=77g#O*mg^E1Z5!r*=%Pc:aPW[L[!eUU-\NMc*"59\lWm?`#;'+2R^j`fCS3!IWM,M21/o_nT.S0F.)EPkhZjqc_;)9EH<OH%Rkh!HG^pe>ImX%eW[8#6f!*-lCpQguXW$[R]Uk\!C.):FLA)@*KJ3/se8agVXc0h%NuJ)@i-Bb^euE!o+T:d(sm-]r**J>mQS.G+;YChm35-m/YND95)_EM?G:IOj2jFN</4927G')1>c:.VpC@i<6!+<BZstZ1.I*PR-al@\;p&HV)!7f(W'oADa"EqGm-DTp,7;.&M-2Z?%[X@dRC<)cHqd_paM251&hF&o5pB:9#F@(:M%&'l$Sab?SW37d*>%<=$lji[sXF46!UrUX^0W,g(H/BC0ErLWtZ7nY"P$;Zk7O#WtO<VT2dY+e-TYt^5C+g\0t<s-)Dr('^hh17fsY?8p$!P:4],d\WP<HXIY\f]GP%Qea$7OSK-uKTXEqd@(^6-\_#m8&3?'CK_bUR,16*IL6g]Tg'1Z3kt)no(5#5nO>;AHK51W?MG)IYjJYChqV5d[*1a"TnHGH"H)["fMrnjamqD9*06mVQGCMA]ESXIrn,CGG\^'fJNiCY#2;Cf_gaBN"_QD5oS=&uq`bM8n3\:iGm"kOK\]&\]Q=$_V1*00MUfMeM$t/(>X?q0]O"sh9""o1>R,/pg4g$lcVQnPu-HTd4k%Q<OW.5SI.$mSthoOjiDth?,2f\O4A*ak2b#ea$1Xe*J7V=)FA1BmiL3&bk20&c`dk]%QdOO"GjNYD8lT*t:L"\]s_:`Y<lIchk?/,=dO;luMdTTn"oY%^aN%*t/FjBoqJ0pkr1`V3@^5ARK;n^=<*O*j3+MF9[gTbm%'a\QXkn9cV9M1P(_@VCu.U><(mkE5j2P8dP/RM&Z.)?TYTWeL/dLp*ka'dn&>iHl5R^DY^mJe_mgl]jrA-Sk[:ihbb103'76jbr4<#3KrUm.U<(l*9gSKB>4]N=qZ2f-<)//u'Pg.<Y^W8U]&nI-pS<N_VmCS;gqU"j+$RUQr2YtrVPWarZg-B0RQ2Yp+IK'3@)GI5k7q=Q2U$2A^;.;(Ebq$uK?8/Z"Pe+oKYQVm+C)"c-X!EJ=K@V]Z/4]Z=.^0b5'FDH`p$>L(E9ANkV-e,!lE(.-\6EB#*';'9jX[':0!=<SW]%<m3qlJK#;:)EfW.&3E?XJW*CYAlc4YG4`MOk(,e(U>\\`nl2qd0S[9)`p6qeIlARtL#=meCSBC"m8`!W:H,_urG<3-bZ&[-8uXj[5Q/?H1[`e!^O/5iF]@GAMUHa'=oFr"d=!6<moG.W49%S>-g3fO3:L#GcX@^4aEorL!JU$HZoUnt\SBDB;Tsj4?!]%3E1!^^$>J*umbu4q/"SnTd!o!m\C\_2dNV<*8+EZ.d[JGI<Y%MU(3G=q@gd%#^*C?=?:$<EN_kMO3I?$r<^5B"U#X$40H/_QiSd?8oB,F*KqB+<9#s1P`OZ.YbPu&C,Xd3"GI05*?*o%V^9C)Fu#84FBkm%J;rfkR:l0+!*d]4iHuu`<uuc+K,Z;S0"+P$$s'=YQs6ME)L'<I<A8@@>F5"oGfcjIP<mh])((rgT`@rPE8>G0u0T#U>=(galmnE%<f3@^M['JA?ur)5I%p`L9h8h*`I.UJh@V6qT8u-M-ACV2DAs9/Rh$Je]_Rl*@Olc_3J-bm&Al;@$.@s6g<-;qc]mm&7GGE/[u]h~>endstream
|
||||
GauHKgN&cS&:N/3FO`RVP?8K1J77)s/N[rM:9!6k;7ueaWW\0EKS=pVOuE=9m6TNL_6W+'Pq6>QI6#cC1CXSAhRrV)0*':QnG4.1/mpX>DD;NR"1T3AIe7k6BhKdYiGro$[^$@#o]W_\geOc\b(<Jm&:=6\KX"B_641c=JXOY7>li/,orBpdCLi*,5rM/CZbU['b8oaL%.@U!;EOYIn&C4+RE`;%/RPaXlS-VnNIP,d+^F&g-"].O?=(CQG5M_PQ?RM:i<IacLAau>6P>T6,fNc6li,N%!I/#Wj*7iE..9Q"+o39*ZuNKlDe#Ws>q:RBTJZA/j#k+d;p3,REeK\oh.&Eq;/caJ9W&/YkG;2VbgOeY_@*)$>b?608Ptb@+dm%9>JiQO+<@3mG^RSC*AXHWM+u&B"rGZ:8E35g#]@:3n4"R5&4C)13s43pYs]NAhtZ*BG,N\:;Q'pbDpn[e)ami,qmn@rl]L.^8C*Q/.F+/u(%`pZ.Dk-lg!oIg`"@bmQto>@BfeE.;TVg,mF(8e+pEs^>oIF29S,$AbYD=JCIP-I.4iSGbAo+e1L7IYQ+#atq9/1!CHI6<WWhDGLtbhaa!?UnB"CXJ$1'LbQ35:U`^)m08$&&o?5Ol2OcL3s;<gU1).m1Jh*U-RXtYfu8&T6h^FJLjHsArScTSJf$G$^3d-$GS>q'2[_$NV7-3r[0>mL\<htNKIl`-`r:%IOM#[b.L^c!bk*Ck2aJB_XWn78ErJ1t>.X&G3ZB%!F&DbZ_e:NQs>)A1s8']J_-2s**=0Y!Ui4m"aolo6.j9""1PM\*UP]_HjKmF0\Y_<%/)ht>)H$2uq[$5PS/6M)BL*:EU(juqMW7nDV7*H0-$0t);^'&o)J/6oZIrQEAmMBcB'*E?taLC'FmPmTHiS;n.Js4>>cCr^^fhHaD-_-/=jj4)Ct!KPTQSRL4o/L-nQDG!huS/fFR)G-6i0Yf]cfr%Jk31n'SA`A]^9[c?Kp#l,A@9t(dE-j?/N6:_&I;oD(>;HiT&8ks7IMV]H7$rsH_0eXL^_-&T'3>K0B9tjp$$DgD$'Mi=MYS^64bpu!1**)rjrLJU(@TVR#C4?FCh=NEgW;OSEb^/\"t4J)J@a;go#K#UH(mlADdI21d[n>/?uU,U&`Q3b(C_l3;@\(;22l0)H#AK?(@dWTmt86._/LFM9ecJYhDZm4"H/\VgY2&d=R7C(?q*6N]kfTa]rdNB\Mq"Q5E4a=&6/t_1jf'IIs;lZZC*N+B5k0cog$0?=0SQ^nF74T!Ej3aGGgOJ.st#OKOd!GG@u%*f;JkE3u6SPS:9GAi:8ico$qp^XAZTfZ<T1YMYk^.qi2,dSp_J8\1<CAlK`'FqCq3^8OU3YlRj`7QK+3g:p#F79,Up<=fQ1'1;bl;p55k\`k@+39]d8%bmRJ#RUhp]7h5a!EMjLc&6(/i%FM.lE98S!\ns:DN5jJ:%P$#q_.^&RqXn`pJ&f6*DsnN$b9P;?CB'B:R4Ge/+D>,6VM]u:ULXB+oV\qII7u\p=^>6AAJe5-i2^5"&YnT^.o/K6dA[Q217rCu_1<6WNr74p^<SJ*U1m6;?:I`"qPg"im.g>2LG7rj$`ui#Qi~>endstream
|
||||
endobj
|
||||
25 0 obj
|
||||
<<
|
||||
/Filter [ /ASCII85Decode /FlateDecode ] /Length 1559
|
||||
/Filter [ /ASCII85Decode /FlateDecode ] /Length 1846
|
||||
>>
|
||||
stream
|
||||
Gau`S9lo&I&A@[:m,ZO'R0k9+O9F=%e&H8dC#do^Ep3`O+UJbfW$GrnrUjIc9<p.;DEH<NZ63Tlmk&]!D]oHO^1d,+('&9NE.1B0176@<El)8]!T$Pa5<Qe,(@r).+T*?j5a&q]6+QInKFs%RQ;A-A9LR*U$VH(C">F'sa#&NK_>K9_J]*Zm$P#SO0G;cso`9md6Y]B5-qYPan,*jE]nAeEGN`Z97PM3Bh51D+J.nQ0E<t"pc@5TakrnrX(TL)of&lI+?)i!L_s`Q'E:JBt;,t3-<<b[d4-ePpThPDB9NNorj1#Tg3G0U*pE&=D:_h=WE2[M5Ddfj265p7$!R@kcdiKoHSYL2?L^8Eh\Sr^iD299p%sR7?GJ*+*/o",7KF>uVB5*<&-1"UF<Y'usWJ;=ETfAis0&*lg')NA=(a$'L?G-g2a)"<+dZ0fB2"mu;5Dkt!rr>#jUU^6cVFGOXd9R6l9u1:Nr$F5$/(fLH76j5k'[CQqN%DA"?>KY'85j$3q%Sb]^sm;hH:A7`1P@;n$'!(nX1A?]bska!1/R::VG%/m`F6uS%d/1CCi#rjj6"GG[jU3*c"1)5''pXiAI*@Jj&u2XaCM^V%X'mg5;g$VP;Ooa$8SYW*5&ri*V,R&Ji73_btP7)a^CfS)Gg,gjJj3UB"*#D,E*SJc_q]b8kY$+@:66K]e.bnO@h<sWJR_E9fg/C!POc_H;WBJ\pL\AWBh-SWH@:SZBTWp$/MQnpZPK^6-int:j*\l(O.BES,$c"/XXk<bZ(Rd!j6J"cJ!;nOk&JLP`5"[8_$W3^HsppJd&)E%/WQAF:s-S"ZKL($"5cK:9d$FmP0LO7e;WIDTH<#;H-=h^#K;R)%?MH,%"K<W@&:49o5-8K<#m'\pG!n'41:kd&EqeZt\8h,B%[ZYjPdLi9VI"`tKQai75&0@,0DA/:8l8!(+(jWLdn57[oYd05qDRIh8kJor/M1db)'WG+6*Mr9XkoJs`brBikZ6BbFi?$tJ&!S3hQNcY/.+#o"GAj0gXr\_Q5^G74.D6hNjckC.e<,C5j/)s.4QJFlVC6`KB//S]gLW[GdS\BYPo7nG7"1Z]-_I#!u]P)I"trn`QNakkABf@4#N:V'<Jo`1V?Vd1Dg$)9$i7X#2HIBmSgQ',^iK\@pHC@&[OnT=D+ptN?&a5I2i%VSBeR_f#rkl_B``F3kUmj'#q+Q@'*GS50hK*T;Q9E+E7Pe]!5n%nobf7.$9;*m64SS\boB.;H*:bY]^[Q=Z!;AuWrY,e)%V*^28I#f,=_X\g_Rj^*>jq(I,lI'.^b!;H@[WVfZ1V=SBf=BsSF2!bE7I'0]#X/bj'Z?`"^XROgG70YgCV*W$23V:KWZ'W1V'apS%B%X^^/JDuJ4'R6ohM^]k:$J!(WJKG]KDE_]geIeIT$oiC7eoaFhZ<O,P^S6RmDa0WSD?sq)NGpPP,\!lcK+RBdjB'hO1BY$jCMpQ"<u.\DC7Z\$I[n(dkpO%i7q+\](Pkd]o!rf=W$*$El+gX?Z/0=h]'HCKoB4Ogj`!F`cFNb\VJ&8.iZ7$g`4P6i~>endstream
|
||||
Gb!#[D3*.*&H88.0bRh#>YXRt3@ip@/hC[ej7_+HkbOJ?Y46Dc3_i8pp>FQ[3%n"[VCulo?$G$[mb%-TAEJ/R$9%n2noKro>`Rdr9+$p"p`?A,>St50X0M?NU:$iDL``paK'*@aE0u:L/4+1lq@qX6\"/UHp0_UX"d^nRa$WBK2[B]P+`GDcj`9*LCkl7j.jL\=XrVUSG[iP72IeR_$2;iLlAc%d%KZm(`G*+<UCDh"#VOZa.#X7=o?P[*]9qFNRTO_@7cDSrCH]FlVYrQ\VUk\Zbbb-QjHSnP+=sj9'4,;jLRWC@,\3/85WskF#5C:n(p$+`&IOcPF#`r@"=fs49#,kT@ZeK:(sgQu]BsHSa#q-9Ba;]Z5#ctH=ZR4[U9kh'3<[^JBq0o`J3pGIYuD&9INp>56u!)P/fZMlKbUBF.a%>N6#"2pe-U5UP;)L^??F(.I[+'9';:=k(R\AsZ0l!`Yr">W@@)NeZ*HMZI;]W]'Z]_?c.0GRor$3^oqs`MIr2KSlJmml#KnUXHV7tmpTB=sj*7`em&m;X,=NqlGla&E68K/A\-"r@Y0/geSguH:"pOD23<"4,;AUO89J*@L`+#Fg0?9cr*q>eOGC9mT-;fPH[,kIOJ@iWSdW#m7o&9$,EHdFQaHH)ROY^N$jp/_'Q_t?!dr/&o93oV,P]eD]VS;IN4rbb8W-$k%G[=o)8h)VC=/+a5O'Y\&_Mosk7Tj]S$[/]go/2_K-qj?g984]i[W^3Rj"Ce.:Sn3EBf<^rHs=mCh)ccXZK&:kJL7XP0\>.3,_kFLGo]V"BP#\.;DV3c;KL<%;DVdkMqP59T<1[j:4!+&Pt\tu78`bY'SXod+[YB<;Pl`4^7EQ._OXJ*kOB$+]p(;aC+_bEZ!;4s#LZ&qj!W4IEE&"SS]sI`0h6l0`g`enF;em9WRO!!@Pf-3Nc)$arV?H[q7<n.,E7kW?E_+\H-7-)#+j;?($;j<omf#%8!C0L<r<E/E)Xu^grp7U!md39Sn;r^=Fa?\f.X;PTGH\g0P:2!'d3)d`bF'"hMNASs'8ur=3e%2m:uN)3idm%J(TYJi>/#_Ys?=Tk)bB#7b9'F3QsphmmU2,1I9ri`#jrGp]m9J8&6]S2&.ERba=2tO_+_[MJQUdAXX_GcW.?2`@p`GArIfUO]l4:1gA?1?/n[GP;@gaSKF0d3X&7n7^3`-.8n4N3^3_%gVpGdXNGRQp!2TK_6o#Wn#pbd$/Osc=`$AOiCq_m5$h[#E>Ohj(<q_hMh<E@^ICdVLU!T`U2aMT>4!5;=(HPFkQl\nEI"/_#sFpXR/NRk4ULM9X]l-*'=Ko0mck.bC;hYEbD4Qm6Ia&C=Vu$mZ'T(>,W8E:JMW0n";8,=R<-!+`FR])>I:k4is$m5"gAeT]gKL`])X=f.!61<jggm$g'V_Zf@*G9!cDs43EqnXdnlStN,4qD2/QL*l\Vj7"dA`Gm8VpY:Z(Q2UY+u`C+I"!<2&rrnY&"nN?7S6Pf$W7Vjd.F&/cNP"[pnj7?j3mJq>8rB';WbiI?ie6JV*?3p/B7B1OG\*H85O=MS4,E=uY*A('8LZA8bOV?rEoU@D!pD:sHQ6p^@4m2sn5lFP7/a*<:1n]7gJWke2N+\eU@OQp;D,1#P4?*7j=j>6t0o^1>oAKMU_/Fsj^mVa7YE<\:qppZA*PbXP%]4uF:[744e9"*I:%$'AfX!t4"mr#c*cq6Bl"1,aZhp@G^NX!<cOLQt<.-5R2lPgqMGtN#LSkho[PA*u)(QfKc2g9nb'Y0@RAUXZTHk+K&,00#/MY%\WCa6kTr5:.6G7rd)-UDK0$<6m4ZV/Vu&n[?ALSX31Yl4epbiAL~>endstream
|
||||
endobj
|
||||
26 0 obj
|
||||
<<
|
||||
/Filter [ /ASCII85Decode /FlateDecode ] /Length 1871
|
||||
/Filter [ /ASCII85Decode /FlateDecode ] /Length 2132
|
||||
>>
|
||||
stream
|
||||
Gb"/'8TWWE'Ya0?]L`WR1(f+l$^g0h`b@>,4fRHkm_ousKI#7s_31&3o[)LP#O[a$=`RgB;TU/HO21Kj]Yt:-ITcUgV>tP3nFA^E`aLn1AeQ,\VsnS=rGE58EmJoZ#YB`rJb[*0TU8q$3%f3!mPBsS,LWp7QAeB!9Am>+o9]c:K%Y4'!V'F[8K3IrN[rpiqZUt"8(KS<>h<??ho[=+GWp+OktoUd&&E2gCR+0^KhWt:49q`4*.@\FiWMI1\LPLNed6`'$5G0C\E)Ftj-.T)=iP$L,'^/+<2/t`A^rT(?GZ9EiAJK]D,)&bD\`:OQ,!O\V6'<jJNXD'E>F'EiI"+aG!IZu"nef>6;S2@Z.%Ws5f6-$]=&*H`?!C^p9_ct*hh)L,8up9.[r-<j^X1sXU2,$L,Vd]AtXI5O6_/uH>LEd28&u1+[c@Cqnut=Q45K]%n(asTg]28&q&nsCcD*'N&2'IKiQ0Xq)5C*q;0%b*P="eD0V"E`[Kf/E<-JfKlgkm12#Ya_AY56?Usm/NUIO0)rF$do.b,an1R`QWWXN2-i1H3Vt-uUE.-FFJiS2)r2mst>04S3#F77Qek";qMb@19ZAPtKj@N1mFHJncFTJ,a*u2=9Ti+0Ki*o@eo"7@NTo@H*-$Ti%Tui2-Rmk?/OiTW/GY<\GCUMKKhPjuB]ljBWg%T.7n_=_'>/0KQf%YuSl)`Vm_%iN'\":&\&s$nT[TghMD0?aU*TYMPA\!=AKc)SrO9IoN+6)SGo'9>2''okNM=7.%I6X9?l4>88:1<]3NhZk)n=-sO8TS0:7RK.*IET.K`V1CAnd!QAm%J.6kr)cK1?^^'"W[I\@m)NKHND_iHjqs.6%G`>Fc>e9n3IbqpJC@.T)?d.';(S@)+G@E+Gk"NHoXF)`U<?+]\thjFE"]nQZ'1t#Gj0]FZ,0@OQe0bPLdI=PXj(Jhb&Tf;A`YeZ<L?j%B:6aJN`Rn,NsdA\"?Dk+@sje)7!>M]:%Ej-+;hlN<,eV@^)']Y>%F7N<-IoKJt2*[;=0)]!.cl=LP#;j$;m624%./H=Mj!eq@("o6f#[;5]06f&9U7NnL'/Q_Id89@-TKC3VGGJLBRC2''Kr^rs(iEmT@IZF9W8D2-f)m=>8Pp&j.#*VdS9N+5*5&]YZ=e\sVrne>Q#bb7U3RsVthX6gI"GYQk0ejH1:q:iSU#iZ09=155YIXo(-2_jI9VC'/SJ8r=T$ET3u`Pl_>N[?W#oGE$4aU9,Ki,uC8X&s!dLY[dBFTeYK)p"@;dK2o7>F+Iq!eN'rU;J+QKt^YJil63Z^;'mGIinTuIB`T%pVCJm0jHZSOK/Gs(-r]gmYhIul!iZ1.bkej1^L7bES^q!5Ltm]Bo(BaZq@K$.jmeIe;n8*lc!Is1;[<R]u0mI7*]c$Y*3*GVIB>,Z)?p'):%jHU0`GC@M3tdcmSCU63f2*n9X.`Nd;"qam8/[@o&M^?<S."An2;Sr7USU0(eM7D!:`6[\`%BRn\uK5*nc@C4[ZHC"m(/]mR8Xe[RI'=,#5USuJAd(@#J1Jc^RM<UK<B31sg<6g"cu2/-Wi1pTf@V()tZ`ON#:=/sQk60E!Xf@NB_bnrF6Qot$KT!ZL_"[?ICqteE?fnp0oSb%?b/cHMbE*^C`F.<=#*e]qk%u_$dK,s\0#)u:<H!=jub3(I[F!Furbo5cQ#?5.2F\u2^*6p1,lbVTbA?j#LSg;Q_&?/58caL=73*aSG;gRAEOOA'!n9SF)AJW,PgpDE&&]7Gb..'VCL89)O'B[mh.!?'Do^OaZGTEUMY>oPD(-7;2VuM]UT<k!7S>p&4r>_JbO/V!MoW*buV\mJ<7\)mb?Y)2\kEX>bU2oP-pR9bQXM4emO]>o~>endstream
|
||||
Gb!;eD0+E#&H;*)Z/_7ri"m*@+<gZZ;l==WC"bCRRXF']%VZ)Z,b6EU9]KL;]b=Lu^^*7^BcI5<\<hV]pXs32(7=`(l.7$Y\/iH0?S/Ku(e.p+N#iDb_<^(55A/R,S$$CK"++EAT-!m4fqWQZf[PuRak:\=;pSi1`qs/5eI]Xlm!d7a_spH6@"U1QeA&sJf$hoX%ggKp0B^i3lAVfnrrRrL0YC?mI%[*X&]ZY3bOVd5[@.b0e'uhbp>UQcJ^0@:p"Zr$*>mIQ+'9(^-4t=oq$jQ+MJaQ"/4R`M\oTf@noqJ9."bkQiRM;i9oaIW:a!r6.?$<KK#In@]Rs90`6h"`59DfWjk1#cnHRj-]<!R?`q3K,VO547-2]mA<#4t[VkKL8m)7r+9g.,*s1%;W=KM2;h`kNJaFgIFKTM&0N5^#N5qL.i#d,-\+VC)2m"b/W6g`**jLCNP9nB$U"tV+!bf_j)kle.3'^8@SGC-.G+7G?,&]PaK$-"R0d.PZD][62`hr--<g<CYh<ph(WJ0LBCr;M6i&[X#UaCL\`N(#b)HOFHMD=#Dg]a`7i4BW.rRaHC$E[gQJe2MX;WptM^&s1Jc)IMVU"B-Y>Mh`B(6#FU=%n],2'#4mJ$clGQJg12?XOe;t!M<hi>8GF0\L>qr7jOE'TtF#qOboSQRjmO)Wf6]&0WLB6qd)1\).H<8]bod_p*/0lnJhar=Ij);.C_2aJR4t3RQp<+,,U9LiOOu[kkS8%h_o;kX"R`al3kp9K:]4cU[Rm@8Na2JSN[98QJUW?N:bM&%PcL-!PQ&9fN`ftH8Y@Z".6s'91[B8+F2,,/]PRGpggQ_YV"HGd!Ojlj:SA[HY*([\kp+ifq(4g<30s9i7NCacpu:fNnmY?H*`2f+otW&\O\1KJ/9oU0e63_K#iJ0:dbm+39;WFb^ELR"n@mG)(I$J$5FmJP)T5WkQ_<ka-ZFspcQ,AQG1dM%Pml1Mbh<F(7HF:Z:]IR/@&O)kg;0\%Cd'C_C#V=^ZgJ$)(1D%/%'!3BQ1geO`qnS?,3_*f8dEY9K>h:)XT=ncX5.A<Y`cHn%1s!V[q&(EO3[OQ/X>?bMY\5?1hoEgt1<N%[!%rEVgLX@1e37b\(*_k_0,]*%fUMA(nu"FfX\qJetQOTe2&l9Z+6m^0<756qM>VjcR4PF*h=lbdg^,1M&+mNFV=R.cDE`T<>"(G[thfeZ]+6I1frS2Sdg8*TSsDfG@E-Rjq.j^KE+.MQrPHZf2n-_BIk]ahqnUQrELZR"@4b,oB.C=j.51_\k)-W,<dSIJc2Wk"/Bhg7Vmrmk4&INZp6T_j'$CAh6B6-^4V.cO'mV,c:9=R-O.JFK8gEW0sV_%6UJ.7t'[A%9:M)V)QpF;#G<*$*fBP_:)<!d-<`F3PU(),V1:gi:!3>J%#U\hJr<Dd\@M.>\b"YO@R^X<447U&=3tilD0XNeNK_4A0deH<)u3ZHmG;<\%NR@*(eo>>#umKeH`%!=1R=e7X\8>4%A=VYC-3"maar`l*nIH5+\74h(>2]=1VV^=EkG\r>#U/m`_:aQ#]Nu&rL]b6-;965ek8*YP'[$[]$RgSiqO3,Y'S-.V_if,kKHG<(*[0PLuiR`F=:uc__fiI,Lrl#cdGHZC'*VOAjM([U!E?b/355PZ5%/cV$I&rO"HH&/gSae(5h,-VRR56Q@TbneL,s>@H6$XeJ0W+7cLI2T^#OqLZq#7Kt7\EqN2'['$<kRL1cYALr=_[uBrC8kD8_LF7`GpQUh"6s8?XmcZ6eg%jS?QWtHu5,sQl#F<et>5tP=>7>V>(fW@(FM!*i6k'?8F"io*Z:^q\),YDk<ukqM>T1X;\i?ifYm*8lBReV1"Y#JD[(Gnnd(L`%eRfaQVV:,^d]5f";q6$9i8AqD1nq*H3O<)"SNrd0"#LPeRqnlZ7E^g^kuKnheS1:9h_`c2U<=+M?:FkdJodfVG]Sjc*FJ!e>[+TbEqX:LXXF[okRPq9AQ)S,6cN`_3k5lk8mrJQ#!C=Vh^h5s1d$WbB3jg[ii5^5FC[CZH%%9d\[M#S)cL<A.[mGJRdqc1UcZ\DW(9m`NsO3t(V8Nf^\lZ7X%a%D*1OTAJmuc-LijK4P2IS)Ok)G'=`@@r$dS_R@./:@~>endstream
|
||||
endobj
|
||||
27 0 obj
|
||||
<<
|
||||
/Filter [ /ASCII85Decode /FlateDecode ] /Length 1724
|
||||
/Filter [ /ASCII85Decode /FlateDecode ] /Length 2064
|
||||
>>
|
||||
stream
|
||||
Gatm;>Ar7S'RnZ;35@5`D.A<r.$;YN]<5EAFC!Y3gOOD`A=9lkaHA&Nl*as66cl(`pnBCA*J?2eo:>c4$n-R-o0:n.HoH]$QOF^dfb0Yp!bSXp`Q\5?&KYsA![,4OI`-e*^AU=-no>"&&k4umTVs,.$4r(,V'6'_l(hi&8e&ta*WmTR.[:".=:hM%JDMXskK$hg+:P%BoK2l5'e^ci:-')qrBi8$_0/F2<*p3Z!ZAdi"-YZ.rGi@NMNt?-F\u]=_3?u`)u.LbKP/ocB2t3t7PE\HIAaD\]@"07"LB&74gr!-pd/p4TQpA/YB:#m#@&`W/6gc9nLDdnL^#j-d&%g[aEN5.Weg+BM7trbE6h%l-sqWOKT4"SdQY7Ubi=j_.b%jeX5,3jQjie0-g8i$T%h-fVAQX9DC`SF;#h*\BC-Qa5dulKJDG;/0mq1@Qdc%5a!L7h01CU[S<,gAVoD!n`*7R_9\F@MqTG3`G+.=uM;![(^h&H;Xr-na+g/!Me[S9_r'FM$k54HE5A]g6_@Q0R7'HsWFq=V.e@_gEn16;.O]V&MH7TQCLq5RMW<_hJIX%WM/:*M`[*;Z&d,]o+OTg-&4>1`=k&jO&E1Ir].V$[%j!.+G3k9r5)98_-\2M;1hdAK1Z%X,+8[6VFISB?T'?Ac@A8,r>$-_]l>ED"qNsh"E"6%S[o0=,T^;bG^:'gW"Pdei;pcQjY%kr0C1>3^W'YT&da/s222*$=iMX]2$)c'#X(+cq]GsYA$fPe=#((r*q*:Bcn9]G`Q0Ch]QYnglf]aud2"6dOG,hk[]!k3eYr=>Ae;Q7KJg4ReEQd$f+lTff"GP&R,rSu:!nf_a16'!<iA@Z^(98SN4hNkloJ?2YTY32.B];j%"-%Bj=is!q`"Sh'$"s]0fqJ&5`N_p'NW6\YU7=I+7g.+6`C(CD_lR_q>e=UJ]R5%$g-K6l7;$+tb361BbY2K%KSE><[O>G)`(-'%YHAirB7*nIeO<7DY";p'_r6-T-Os!XSPFH#fN<mKidA5>gW2=fuOTZYV:%imtF^@ei:JfC'gsje(2JE%$&:otA=QD`O.rRq3`JtSQYicEKKt=Q*N8*oMfdK\Hk38ER_.)'\,`RglD=d/WZ3?b!g=4lTGQ%jb%1!>oTjRkm0j@*]_S#$V>Mq\h\u\eFd-*SPef?;t=Dq!iUQL%pA'lC+8/Mp^PiD^CM*M\("M:RbjZ.IPE-`Qn"=3W9\qcEdkg.g'kK/.E8=st2B*jO?Q57o]13&VrFLj#]Irmm!MWArKApQ#IM6[f[#uoY?8,Tu'O4-$NGALIPH0@<Gs)#9\`WfIiY3XMd/l-Xp2d18rd[iC7??ZjsbpD0$,!($dTmerUS/cTGLc_,Lf]dYa"`&naNmf.V$dsY;"tc4db&k&>AK0=91)"p0*p4f=Z#I4V[gO`t(\Z6WGKAgS"C%g#8qm6<X!l9.5]Q<J`S]9mISB2@:$K[^0K5r(0($$URr$Q$=iP)rk$LPirsNr2&(C>e'5u9rLIud8ikcFd-82",)m!=IZMOM'2Q)H[i@g4emCk`tME/TkR,c$60QhMON)a$E>jng+)"XT3_/D4AE..W_5b*%rAos(-gqIOi8]T=W4E`P;e!ic`Y88^+Y(05,D"*a#Cdr;.j)UW'N)+\ns"`Z4!q\&#B!d,[5(9EUrRN]&'lQHCB*L[`<V0KLaO7r/Fh,NXaMN(RM4JJN3s*'E!HZ$NT`~>endstream
|
||||
GauHL?$G!^&:Mm.R+%D!9lRABNQjJ1:6TS)mC(c1KPb(@W3?N%?q[RQLUOsH8&Bo^#;Ch<1o14g1qL8>kJN$4]km:YPQ@<KiPf.^fFIOD@(V^4r=AD/J+VW\>s7HJQ6//]%=Yd'"^]r[FnBh2I%;WT$O^f]_5XOI_!fL6=H;il>n=#3d-cPs=CF`"WJoIgSrjO$flRufNDIU/oaL[ud!r4CJ"bJ7$b'u\D6N9]lr5o&2mRPTEiQm"!lqLe]T>-%.>7gk!NDT`L_7jk92CQ_S"N=J<M?F2(?UY9"KnTF<Ph-*;p:q3CqA3V!8N37dVXMk],rA4$#2/'Xrp*UA'>NS4N6:onrg^aQtK[.Zk^\6@j747$Q'jjkDU_05f#pBB;[Fl!e+Q=.V`)2E!X?dc3b7)E,lG?;pYLlB.Ha&I5nnpI,Ps`3[`u&(__e'RTb@Y9@Lb>6SULPE9MqE3G`Zs4fL_k3^t:uV\nYa_(l_V9IW`dY:sqaqD*"PIg)D.Ri3m8,>R2$S4+#b'bFhs<(@-[9ubln^T<sTG8B3"3OcqBa#j:?KV01GeQb[dWD$\B;e:sm`#P-&1.2`UJk//0Fb9`#7k@OnV\g$db8J0],nr`]3cq=,cgP&J//8rdDf0hAVnML\G-LX)kEqE-jnrkFT_PmCYd#b<MVQBd7^l2f1#aF<'5MSnro2$IkH^Tqc"'34]er"c@A"?eAM)U]ISr!%#:'rB%Fu9p*+r.^Une_/=*U@2CIAL_"Kg;cE:^s$4L"0t7'42,O"f1)2_a#MB@2AL:l6*W<_G#pBCl.NUX5+/_(uof:=nP.UPYJH_U?Qo"3>dhr"K`2W@[)\O;59EY(dr5OPlelFUOTUb/2b[MI$DR4El;D?=iZKjd;+YaiBi;4);kWYRL\6'+/jZgD!b9f0iVGCSmTN8BROgY[3CC:$(Hic@2%Bh*+@ZYT'BiS#pj-#4KLdCZrS[jcc.7J&L"[\([qpN_0,J:HOJ1:KAPV4PPrNK7fiX1dEMB&#AriEW[UZSkDAk;Y=f"Ulq_[b'PIQkrm(G;@?`9^>3br_\C[m*=D+LdBXCYk?(,^8i%\7FjWWRV2'tVE_6YP0\Jh`BMqi,O'+0KO&n#bROTqh9LsM0ehRhs>Cs,=ke5.+W`XLS'';qO5>3fu*MaOdTBGs&rhX*"VORO$h;NedrhbWC`F##5h/a(0WDfMo?f`J!o6HoEZJeS/?JlW\`jugu?F+fAGQ1'RJOPu)a=58iA!T$q;bji8LOU-ADN^m*$h2H.BWgf!%XW<>D3AZ-S7T?/)7:Mc4\Ff_okN!2Y/]OpUf@('K^iMcCH:L$9Kd9VHhPs!UIFbcgpsZe`Tsk&R#a^U!d`X1Ag0Z0`p!S?;:3nde))k#BTg.(*+XF@s"ssD?TbtTB;Lb/;k1-XP!R@.2dF$JLQ@u1RcLpU8sulM8(;6b<5^3MMt*7rXS)&$LY]e7YMFV4L3^)B`5D+s#>>F"3DU:3$b.Ma)4&d"$T>$2$0j,3U_J,q5X_WLWZ@1AU&uYg)ql8Ldn_AcE?qA?L!,^^SDLXNF/J/Xa9O>Z[+Q_R0bZ9;W(t!_;34ub#)b5!,&,R$47:gJaTLZi%_:`60P.dq!A+G48^JpM#n&@U5%ZW>`tfX0b]nG4(aqNb14s-I%N@\\LE+biB<i%,bBP=e_B3NQp%Og1>ImP&HFDJoL)i1A<ig'YUG3K4lK?[4ZW\opXC6#/-t_C*,"$*-9i7Sfhj@j&7*h_\hk)eVHKO(._QEARBO0hS)q\1&KE8G"(\I(_3SDoclB/5jh]l1cq#R>aeb<g@)f2oTT5Jtn'PYj-O`p%KHjJI&3fa(g\V82.-QLaI6IuY_%r+Z.S2ZaX3j[AJ0O2Aff/8:hl<.tbK&'>DO"G][9=Tf+ZJ[]o6-$>)O'&iMMok8_VL5'6RVs]TP),\V$.V+q);+f!X*%oM-5-0SBhkFK@'&*4SF706Qk!5U8Fu6obAbX"o4=mP$G&[4%(5"^.4]9tWTj7<rEN3A#D(>I?96oXmFYfhM*;WR(N5<b%qZ43AP8'1D^eplq#QNT#O(fX'E~>endstream
|
||||
endobj
|
||||
28 0 obj
|
||||
<<
|
||||
/Filter [ /ASCII85Decode /FlateDecode ] /Length 1906
|
||||
/Filter [ /ASCII85Decode /FlateDecode ] /Length 1976
|
||||
>>
|
||||
stream
|
||||
Gb"/'968iG&AIa;CbHeVWF,]/8^O$mN:V#K2GFid8M@eS,UCb]$&+'!8Uc((+@X^3g!2Q@e^Bfr*oF[qliL]_n\a/^ZikV6L&E5#N5K`I9Fq6RGOT50IIRRRGpEG6^8-5%J],;$K6lKV&;2<fH-AP%Lr`,;OGl^J9Am<4MHC"hK)'JK!Js,]8HD5b+I(imof`[B8'unk@2N?WhruMjH8,^fm82($MdSMsF[l&&^*&XD;#o&Xp>5iJq&%uRKkf&4ecgFV[7Zbi:-"Bnn9B!+1jNS1#-ZRf.eL;OQ$ES_1RLA8Z:=W+QNV/^0&s]F%58mnd25#b@6Z#q]Lm_%&^X$il4ti0SYd!a`:mu!_:\!lcUZ;P?\BVVdc1@BRseZ&nVC!kjJ\-_DO)Cr$Mh1ZfZm(%^(2/V11m6VXr-?@!VG#.CZk<sCcbNoE8*!-L8/Q%Ucf:VV5,.QZmbE6Y@P,P2e-BDl7G.h.f3pi^OKe9j4G:`g$rPd1I_\;N;LC%f9_&10pABB&D"Ggl>qV#3rGB7lhH'qJAk(79DC1+Ue6uXY-h-+a`V_B&&O_-4Ks/Cg@&U%UQ[7?gT\'@YZ@I2InWaO8Q'3*8h.n^VJ;qW=DsWS4t7P'1<D$JT=(i(;V;8>;K.a7E2FaN$rjbmjH>ZbX,3s@RQEZV:$Xu>/kgU>Rf[e/?K'`k$Ii"XiV2-?h99b:UYjr0[K\Uf)r?=MpC6Riflk%3l#.SilW1Vdo<H'`m@Frtb7'$!#DM_:XYhrXR(QTZ0_l/]-7RP1bVH-ECUZp&&3bu&r#Q&[-!c:A5mGe-0'ILKQ';X,M(u$5%_tZj5OuDiH^:cqN"h=^Ujin"`V18GC8oW]Dq7rA#5?:fa1@Spegq00E*kN'0ecOINP20gZ"P\3fZbMWDK-3!q%'aB0o=R?OP3hH(l\Z3RVq[3DpK'0;$EgkG]aE2\mEVhIBg[?O5cD^*_pu[T=i`OB`gqX`kh#Bo`PI@bS'.pe*!`,!n[)@G[#1u\9t[Er+8.5#i(u-76nU@'*mK+K^G!mdB-%#nKVFdf]Zm00QQW'<FTBZ75[I3+sZtg\h1)QZr_!Ri&jfn_oXO&b,;RjOS,:3oARV*Me$q_#cUo_Fr)cGNOJ!$\N%`+o]`FrA3J#F,@.3^@KSQHj$Lut]iljbB=k6l6K>f'[[0Rs_C08_Y(mCt^bg#(l4jLTfbe!A-HF;*8'qN9pV(@C3gj#;G5CS)#tERjSis^cIogp4]1W!Rmje>];&WAK?,Fp!Q8Hcl@BQh2A&1@LA2$r,.`#S2L5WrTQPgiB,%rMI;&D`a%#/Cf(e59kT&d"GV'i"9I>-&B'qUN:/f_J!pC&4=8B5_2gs/L)^5r(E@3Q1%e@&/GocZaa*B4^]4/pi)mm3o2LZM,Rj_1"k@!%KAkop?Okb)_#I^!EthFJG)@M_ab'3eoYF2j>p$lL/7fEIHNJE'=3kj2<QMrW'#1]$c$+=mq+?GVgi^su\!"(/2?VN#2idX7XeePDO[`XSTGO+tWZ"n`K"eY[4\,JCO9Zh>%$!`E<i3@:dCh8#igH35_<cjp6lhSKVQD/YAPkUtPk<b6E:j9?r-j9YM^At-HSG,'dH&OK+X:]j,aXKYS:n4d'?eTn;M+B.SmIt\4;"_)sBH&"?144Oca@^TbW3(2[ho!<b>OqLqtF+>=Qe0N6#F&]Qgn_D_&g^Pe9')GJ;Ij;m[F,o,3-rh6HQf8N[i1,P:5iRAt.oYWr%Suj`M1!U\QQuk7QTGE0\H#Fe=)MS./EaO^m-D8AD[h`n1WQ;4A)<.PdiDEQSrVZl^n6=gD:3`'YMi/LP4b964&H-MGGZb3KA6%ji'ZH[5KiPlT&jZq?_OuHG3T=V$/HP(eTUO)+NhX#;!-<J?pRgn2DKN0O/l=lnGWM3oD-i~>endstream
|
||||
Gb!#\D/\/e&H;*)EQG%/M]#C\b.]\A\opG&FH+[W=9k(VcF+?hF=A]Bqs/HjAD)+,3_>+0E>$=Qn_SB^4NDD[kf)lFkoAXG#gRn\^g.dOJG[QGd7\^B)LNOo<\aq`R(*HXcuW;/3'Nn.D<?Rjj9NK5f^TRj=Q8!ed*k9hcaaP\0kYKr`%Z:]cj5gK;JKR@9G6b,@7_$MTa/3um@hTaW7R(PTpOiSVROj7$f%`QjL/3ceTI<Ig8!`RiHQWF5JcP%7OL8AXrjsfpQJACSM2`b".;K^Fi:9@f1%bGrAJ.l.sdAXZoha''u`rQFb+'ZXl`_&@<1.Q15$cbZnHEH5^=k/o32?\*gS3r3pY6fAQ%ibk2q3N<g!SoQBt)*'qE*BMKuDO8Y]I!Wob"&L=lS@c%4&1QV'VJ9rFp%[=QXP-WC6^Dae(G``O-FlA@09[RkqGTBcO<EU8#$%;lmW*X<HXD7L7kGs5U:/D&uIhp`KkP&N;RZ[Qc&/TfV#jp%WGF5(R/"e;M)pp_=^21Q-mf<)\7>3<9NH:g&`0@&lKlK5;EO("n8a:QKn/J)eFjopCHL_=NpjJjUO0AsM[E;NR!3hu8Z79rMZB?$g-%]d`'.WYqcNqMNOm1.lC>#2dsP\c&kG_7?TmnHaI6s@Xpf1cn4R+Dl?9dR'WLLi^69;L+IaX#c1a]CgdA856U])o6?C3/^CF3E"cV))so3kLUO/4U8G!0fDJ$a&P!"&#Jm18AfG\+D0Q+hMEJb>i6fkJOq\NNQ/oiqqfhrJ$J\Wt[]kPhCWGVHBK,P0L>?'jRe$5!SD.(Bl@IP/+ZM\AOMlR]1XPW5^%TJDcm,!Nd_'+8A^_FB,ds#Cc$lK:A^?og7g9::.\IY@Z-J\&LQ\8Dgl?esapQ(HRjh)WfcK)PPH/DM;&,50*b;;&jLk3u_bV``>^i+)bV-2SQW_2plI@9F%o?*Nns:g*"\T9,n72>j`59nVB2fdm2i138>%0S[:bD&dAehAQYfj?W^PmLC6+2!f=L3Y;%oP<#)kX1pDb#eg_>oJk4ljV3"?T.PDPF\N0tql5kmkT;@>&"b+W+#FuA=k5,;0<k;WfLmV2q:ur6XWs)h[@03i>Sf3usm!r;n-D;6pCCMJSbIkpS<`s`'/"%2PGjc8(9Te'+o'4B_gd.r'C-@VHT'NQP?D/]f"X:L2.O`alN)t\I'2_?m$QN.m67R'13$R":f$[G%%>DsqT'9d&aJE'a_^K7/rC-uJWLjiq5<0s"\N'4Tg\9k.F)+toX.c4lIYl)Ag7KG%])D#Tg#:7`fO"_oK3QBG?+bIi'<MTtS-q]s");7;YTJ&"g"&l\/\AUsfZr!U3?Nki*up&_#]&2dpH4=ZSb+lL35KcNWunjbIDC9/Xj/93,^>)tlCnuY8g]k@[^`9f[2<03qo-=Yot^@#;N8k@q"s:9jQqO;U1n5^UORULDjmLsK3A7T;ED-I]5I\^PE#$uN(<!h$e.@LN#h:]_c#"5S7V?KgI^F$n(d1tI(P$GBo[PrYR.!C:#MX;j4]9HkKj,E?."&iiUAfm%/G-1H.L3Mo:6362TI]:"rD$QQ(gZDZ=kn^l25j:mX[9DCWLc4g'$C//$P#8#:44TQN?)+Ol!n"2<Uo`4l5%'p%^,eq#>uB%ufn-m^0NuMQA9:KG%dZU6sA-[n%s,[S6UO@H@S9I%_+I\e.7qVM1ZDifpU"KE8TECA!3fhZIA8X`B"*>s_sIU$ZLE&+.%aa1iHj*f.ZRK&$H7Hg.m:&#*43\\t)VV%aY1nKeX9ciDMcgq9T*['Ib!_9WfAIR^oL[s0dP-(`PtL3.pp8/FfCbBmJ(\XWM0_#9MOJ#ZL;?n%uGFP;gl\LLLG93%85_]L7O4EQXMLc/si%Yl?r,sb@UbI0%I#dt+6m[!,c-qpK)^TI-f3NXYN>/d$0[[DlT(3H;*&.>0@AQ=]XOjg+7+7FY5m.A.VV/I\Y/U%L)#6+`iJhF,~>endstream
|
||||
endobj
|
||||
29 0 obj
|
||||
<<
|
||||
/Filter [ /ASCII85Decode /FlateDecode ] /Length 2014
|
||||
/Filter [ /ASCII85Decode /FlateDecode ] /Length 2055
|
||||
>>
|
||||
stream
|
||||
Gb"/(968iG&AII3ChGdB-&j#KI"@&b`_N#AD8pQ9-1/N))2a(rfHd/@P[%SlgUdS;MN[Y:26K]6SVVk5]DLtJ53sa5/Gu4(h^ARd*Pq4c"j14p%FcO2N'>+^UYb_d@35"\(Pp<f4((;T]D;Ujl/giL`!][4JQk=s^&nkCK_']r_gT6pIF&%u?hGfYhm7^u!9)H#\G'CG9g$GBZpS*E$eG&2rpe.EJ9[&tk[sdOb#![JTN\J+6S.O!/rVRLT7?]`"s6egMJ/_`$=?CWU.GYmN;7F52E3,m=d1+b?c63FC7:!lHjQPe(qlqcFn@aR-]#`Kec/J\5]+<'U/]aPg5@YIGmKXX'`ESK?"ie2j/Hs:@b[30ak.L$SrJlk[c9@hA3K(<qQX=!Hd!k'jm^P6j,K9jW:."ZMS"t,HL2V7:ek`$Wa,s./o[N7D:;[cF^VHcTnA9dZq'Xb])9oi'j7Z(W+R]q8F]F?A@&h#d6SiH-9Y:,ePDb5iOc+79_E^b2tbS?R>9/u(uptk$oE1=aVhKIM'96doD37CJ7f8nKKV0M0]LL\.sJtJKi[^p.]!u\\^5O%b97Jn^2!/IeQ0T^4atfP,Wh:1:)lqAqJTA2(+TU$-JP<T\k^l<Y!\9IR'f'@"6QtG]ZEYi'6p!785L=U`3AshD"?7c]ad7l06;=%?)6(-ESHR[X@CBd\,TUp\W"I=@L+BuE7o[jrD!_$R!lYNT%T&>P$?$-)<SI@)TJK[e!2u!ba*sdBPP/[S/P[?KoSR4a3dBMK,Tg>iMrO6Y`o;bKeAV4;gEG2ol?,ZNJ=nlbKumr+pDMl2q5.iT>ES3j#\-FPKc^]J:+^#]duCl`c-MW7HMK0T:8!fZ7O(-50Q@#K$I/)j&AK%6bbF67)%:G7a\3JJOHOO1F9nSMfn)&]*:=6fPJZBlXD*N)V1sUY6"G!huCd!^!8Jb:;c92SXUe>'4d-/J=e><5Ag!IW+ecX?J-qo\N&@(BJ'3`V5]8^!k"eM4KJ5;A55bF&ZnUc2uJ#TKZ,sXOZaOfppRa03V!J]^@)XMmM>GV/^G]V9B!59VN$8m1@Nm%n["$`>*ep[3RMhka6NL->[r]RIZ>??dc_H:CRq@"l<dEGhs)KDW@l./ZssL/h_!"8WcBX"\/"`AS:CBW2?K:6PY/$\W`k+tiV_<B7c,i4S:3SIqe`01=5DeBoAa7bg3OB4,4[`c<>;;F:6N[r-FpuTi2T"C8r)"09gj(RnfN$!A_-C5/o3[M=K>S6m)/u%Q`:IVa%TsM848p*Gu!X2e3H_/gX1r&(Br%#*$13b0(_X:Io)Hsq[P=3;QIIqhP#bT'H8Z$)8ouJS74:/*088V+lfsgXR]mPs#BB@)R<!]gG&n]5QB@d4kUMDa0D6]=d3`FdBT.VB5-Ul>fC,+)66TS0R/4[f/-M%ftK4h(23%Hd+EorPa7/V4/%d5@Y'57_5[LL^jA.!D2*Y]>bLX(]=@MoYhG/RC*1AP^SbM=?c,,';'@-;B_.[-bRL;cGUClVEeU('j,]Xgo91%#ruZoT3='iDU%g8FiD\guI-]kJ;BD\6,5Y/F4R+#n%;Cebn[tBM^IrTG^pPSc8Tc\S_ij'F9gP"=L9ZVn@5sefc,i8._7c<%!Nt?g'38p(N5ICj//7HD%dG(>>A9';(m$..3$->q?'0)dfXeH0SeqbRfSoBM27D%2<JPGn10(e-S4XEn>pu+^7$J'h:IJ@3c;:@FL/pI"aK-SuC_o;$Lbi%HlnEetBD,&-LZ;EO&14Gb2Qrn=7q=Q-(qD5%\i.$=ZXAkqM`];]8#.X70W&U39sp?]ES!$ifeU$P@##bR@C'YZ5H;PBO2tAi@:ZcSd[GqpL>u6L#tIL-d#.7+I#gg1p0h47qNYXTKI='qbf0k!aG8`,Y;@?Xe46.4=7fAh=KH!Qk0X;B!=gggelL:(9N<6/*/-d8`6$q@-8Sr`hmEeg$pnL<9]uU$AWGBRn\&tWNu*2UrI1,fH1cZ#ae5mgi`=*cR0%^7!pZS^ir~>endstream
|
||||
GauHLa`?,q&A@B[\o^IlYsf>amaF/MPcI\\ac%UXkQ6m."Ht#i+D<.uqIrd]Zpp3!>F&h45i_N)Y9764Mc>2D-,A7s\GAaH:q2tqYmhZ?5/E#d_1)PbcZEF0%;8#B%8=Z`9H2UBYZVFYq4NjX<YFWIH66s=[D\^jokXjAb;ZPRJe;,]V\VU&A;FZN12.u>_DEIVCUh!3]/Npk(OU@0fVW'ia)cPg_<Q6*BX08e/MFtp'_aDNEMc@MU"\esUV/j0663NVfoQ2R,%;C$3DVr:0P+ZTQW]_G_nD^kT[1I_4b=Jr*\THnSgo!G10.R/0(:qr,tl.p-/=E*QmP@-27@%A66oMW%qtkl$<p"Kr"ORG4GadM']B*hPqdQq0hD9dp`i192EQ=AbOaf8Pu(^@`NLI_KM4j>)LpGegng!sHF2k3gIeZXkt(2="J]ld9f\Ql)ph(DOC]&F-<ZLNr,)A-0]9VY*3pHT`Y+XcYQ2Z**%`B>;*^Sl-"G'r1[C.WFq;,L<]75Y\_fmCMFG9hnLi#$""!N4L7Y(RO3<-u@P<g$d+Nu"?D&Ohj3bn8\ot+f:\`a$]"$RE]^N.;SfpJb!9aPsU;V:'n>sMY<W'I\aX@l`U.kX*J/7cko(mmC'D'3&>J&F72QU2#r%XL"ba(nn7QfC6"eB#:]2BD8c;1%Ud'",^^lT"mnj5'9PZH9K8ioIfJ.)Cr,+j@V?$oCL$5[ZVK+I$V"K5\A`Q3^1%pCJ3FPrcn%aj`E#Kn)h)"JYVf0M=MB48\AJdqH:]3r`*qb^='>f2LH"lH%@QC<?HEO&i/OU:ZD#7Q*6mgh/!<=:f7e3YN$_G.#8nM6$226IG(G)$pB<0\8=d$'[9Ae\bFDm+B?'C),EK&]''0iJl1d)kD;0WDDT33Q9Y_o-RIrKakQs6eme421JM;>Is^`H)e0]?boAj]%NiShdC-F,?k_NJgcs3QHNNcQ8_7o5Iq6]nM?'m=]&_NX!+BJn)8'Ni1-=C%F30g<^&mgT"i_R)sT=A<4L.8Sm\c0g)E^q5PAB*EKF:\Eq2Z)V)%-=O]/If*sPRK:!p'$$;di=qF:eYe`5rK?jX%%W'HIHp@,4B?@s_9tdcXAG]mpaYccHo_MA]Akk=!T*</PW9:[UW90LZGa@g/cj1/PH;]ZoI=e3)0?J_]T'uE.btaYZm1j(Zk'&kR*S,ObVbQo(3*6T$DidWjC5OtYK'HUFT<50(hXLb=5RDS<h)0$'pSg>L/BOR-OO[-2d]Z7,fCU"$N1-sg/ss6$@C'i1UXgbFiB]4WLE(O&=i(YRNb:>[AW[<92;i?%O.D,C/eios@@WSi`mY.:eT\+gRS]&[@p]ID(9a!M4.t8eiEDg[+gV__.?`n"``AAG!_WtsaX(ns>>(!E7\kl33N,Hnio2X79:HNi.n$h>9AH@?([#kYU'M0PokHp#,n7je(i%p1iVThsb0+76(`GiPTs!QYck+Re?`n'+o`>BcYtdStl11+_&?3Un$sWTiR>q7FqpQkbDOB'`JaP`_?gm*#KHqJ_2C(FC5(6TALV:^<5A<GIWufqrY4L1AUnp+KH?Qe=^P@.F*:`p*P3bf+[qV4oL#=bs\O@T8@spdT^c-'G[&6#oQ^RD4<UHtBQ6RBbREI/19lc'_'%@LY2;,C+XQN$Sk"V-_Kg.n?_r=@imQa7/_]lLVX,lJMWF\9JM;;RI43j6\>j2iK20&K4E^F-8KE'CnV[;n+]c\oN15,Fuh.qN5[*p<"jR(J7H<%%&HmGr,`424&l\5!dl-$B71/$@5b*\.M>#amQQV`*o=d%u1`In,tjhDd.h-MI7YW;*id<b+\8(,Lmq!IeHYl7fGe<ZOVVerop8_R2[6PTS6rY&)tjK$'r%Qo$)N[>>kkA%F#am7?FbBYYlFNP-OC=!bZK(CVggO&_XC(fiN%&59LbgHc,EE%go\uKS8Ra]rYZM>Id@0)X[-aZ+V[LI;#4gB@hoUGBK2F"o.(:'kIQ,qBR\X/+]J#h:4n?KiRBb6fRrE!g$F[E_'TL0R)4YWu*dtat*3%H*$o`bU;r!ttm6D+~>endstream
|
||||
endobj
|
||||
30 0 obj
|
||||
<<
|
||||
/Filter [ /ASCII85Decode /FlateDecode ] /Length 1939
|
||||
>>
|
||||
stream
|
||||
Gb!;dD0)19&H;*)Z(G2I'j/+>K(P8o3j(Q9N+4AP5BnMrJe^<Q3-p*ckLXngcmqWol&;'7NpXWrh/g4*m%"/_qA@1I0*8X%_Spj($knA*0PoR"iB>s)ci-*a.`l"(n%5GiIos/IoXZsFd;.S)Rfg8-]&e#cf*'p;e/-3a/1*O/k_&7>]&U:1BVl4O@JlFMW_XpNOBR9qme6KPhh'\A9<GpLgp1dHO(GYEe?[i=X<WH)NU,B*CH!m2Lg@?,Pq0JchO7hoMnM!KSY`7-XmLboV3_da?Vf%<]n,,VL2V9h9Y7L?[dC5.s("f\BLB:681ko$i9p>P$jMEF#:]T+b;a7/*q*NgEgFOH#RN$j&Z!K(9TOk51j']5-rZL)4q03#!(Sg-1\JiWg<DN(Nh06!ho'T'1QkHg`^j7*&'sE-&/`a('QJeGQ9N&D(//;C$HA8\SPV#,%sJ6p6\1VBTr=/:4VSqFMbl1e1$AYaGRt7>a1PJC.dnP+B&n>oY:581DL(r&52$r@[oXb>RMk2tZ7M?@N?jFq$2ai8Ns)FXi#IZKZ?(^-5#&kk,pX,d*#I<g'T7!mpBBLm,m[g0;^!f)*b'F]]8;])HK9Fg;Z\5[GolfPii#ON:b3/6\1Xl"'YtIQSG&.V$8KT7iUj.o]"RnnHm8SBh"M"1pQp*^,;A@VG_TOjkOP9eX3@B?S;0;(+_*eB?\Pcj$RTA@j&fkp>Ic`1J9$J^+&OR'^a/%lbBt;s%ACum.N^,A5UhE:Zh+D6(8.s7eB3mj8e[gs8]R3t`1f#C0_?*?S'$V%rh+bi1Ym!idiq79#EEX=TF\!QR0.]H4k"b^=A_PpDL1T_+Zu<D/3[jplC1Gl10lYN(lGZn#^#b=J/\&D!Sak\!XDkH",/@98BWZTYG7P[=jZHEX2AS7C*uCH\qS"EpaaLpSGn97h*/I"f[os0%$%[d<kS,UhU6!RmkaXh]g5hVhD5GlQ*7d8d`gFHDH>XM+Nc.6dMH5Xcg)=:_m;r3?9P]^:R,J@+`sn3TUJd/^X<Uf'-T.bcfi\FePERE29sjthXri2qrp@MqJ$<TV2pC?$de$iK#6h_^J_mlfCsJ-M+&uh`QnZ*<n,bFD\;#LH2otK3?<-5?S.E;J1m=nM!:D&Mb.EIl9Y[[TY!l1GZ5)).H=5;gdEnp,%<DfF:.T:NctIaZ[4asQG-Nh\)tL`61<RD,H;jd,%&1<XJ'YdO*UH!d<m+&4E6%5=EXtO'4UW?:siOKk,nK;cND9E)C%Winee"V1o!MBT9ae#;6D,eB$JEm39C>`a#PV^R`$r'@]ZO\CG.DG"%P9SepcJ^Ci^olDgCf.d1e`VftgQgHR;AAoXodbEH@-,c2C?'Maf%\i(2*T!JX.(qsHGo)M?<-[\-gA4CRhc3SU0m7T3VWH#,SYThe+S`kYcY94$i+6I^j\l?X@AOonBDq8`%[9-NJd!]]Z[PcS;`YedH3[=j=8#ZOCCH)EbV6'A,ZWnKIGocfsVYT`E;0nou\+#P_nCn2Ad&TLV!`SBp`U2nIKUmTSe"_JrMBV4<?#En3%S^=BsGX/2%'WMq(El=j\f)K7)VN(CJX^>p\A"TV>?L,k,RZs86Ous.+R."D'g;$ln#XqQ/1[7l,VKUmI1WMBTI'^Ol;*u[C,FZt0G*@=NnthA^LRZ&5?QMC:h;;LfWRW2hjHq#-oDa_W[:T<%B;9\/9j#tt,\1m"7d<=56Z#F)&:csB2oS3W#?CpF8MP-!d*rs1:l\pJf]_!$M"g"N%MhCVe2>[d!fU\WfK54_^&Cbf<,\G@c>'OagqpF!/(gW1<nU/rW;3paILS^I),J]O1:K=(-XqX)bNX7Ncc3X#WmS.3k-;An>D/7=5(`&L,EXX8]+tYc:<0]]b?K9La-]1F)@k3=LYpt+a!/%bT.\[ho4p'.@5L;X?!4$#:?6Bf&$lEG"9~>endstream
|
||||
endobj
|
||||
xref
|
||||
0 30
|
||||
0 31
|
||||
0000000000 65535 f
|
||||
0000000061 00000 n
|
||||
0000000112 00000 n
|
||||
0000000219 00000 n
|
||||
0000000331 00000 n
|
||||
0000000536 00000 n
|
||||
0000000641 00000 n
|
||||
0000000846 00000 n
|
||||
0000001051 00000 n
|
||||
0000001256 00000 n
|
||||
0000001461 00000 n
|
||||
0000001667 00000 n
|
||||
0000001873 00000 n
|
||||
0000002079 00000 n
|
||||
0000002285 00000 n
|
||||
0000002491 00000 n
|
||||
0000002697 00000 n
|
||||
0000002767 00000 n
|
||||
0000003059 00000 n
|
||||
0000003189 00000 n
|
||||
0000005192 00000 n
|
||||
0000007290 00000 n
|
||||
0000009467 00000 n
|
||||
0000010262 00000 n
|
||||
0000011873 00000 n
|
||||
0000013917 00000 n
|
||||
0000015568 00000 n
|
||||
0000017531 00000 n
|
||||
0000019347 00000 n
|
||||
0000021345 00000 n
|
||||
0000000143 00000 n
|
||||
0000000250 00000 n
|
||||
0000000362 00000 n
|
||||
0000000467 00000 n
|
||||
0000000544 00000 n
|
||||
0000000749 00000 n
|
||||
0000000832 00000 n
|
||||
0000001037 00000 n
|
||||
0000001242 00000 n
|
||||
0000001448 00000 n
|
||||
0000001654 00000 n
|
||||
0000001770 00000 n
|
||||
0000001976 00000 n
|
||||
0000002182 00000 n
|
||||
0000002388 00000 n
|
||||
0000002594 00000 n
|
||||
0000002800 00000 n
|
||||
0000002870 00000 n
|
||||
0000003162 00000 n
|
||||
0000003284 00000 n
|
||||
0000005337 00000 n
|
||||
0000007296 00000 n
|
||||
0000009114 00000 n
|
||||
0000010815 00000 n
|
||||
0000012753 00000 n
|
||||
0000014977 00000 n
|
||||
0000017133 00000 n
|
||||
0000019201 00000 n
|
||||
0000021348 00000 n
|
||||
trailer
|
||||
<<
|
||||
/ID
|
||||
[<1e77dbe9d9f83762252045861df07f97><1e77dbe9d9f83762252045861df07f97>]
|
||||
[<9e52eef3fab94e356c048904983d520a><9e52eef3fab94e356c048904983d520a>]
|
||||
% ReportLab generated PDF document -- digest (opensource)
|
||||
|
||||
/Info 17 0 R
|
||||
/Root 16 0 R
|
||||
/Size 30
|
||||
/Info 19 0 R
|
||||
/Root 18 0 R
|
||||
/Size 31
|
||||
>>
|
||||
startxref
|
||||
23451
|
||||
23379
|
||||
%%EOF
|
||||
|
||||
@@ -1,436 +1,563 @@
|
||||
# Les 13 — Externe APIs + Cursor agents + Vercel deploy
|
||||
## Slide Overzicht (Klas A — 3 uur fysiek, demo-driven)
|
||||
# Les 14 — Cursor + Vercel: van leeg project naar preview
|
||||
## Slide Overzicht (Klas A — online, demo-driven)
|
||||
|
||||
**Lesvorm:** Tim demonstreert klassikaal. Studenten kijken. Zelf bouwen = huiswerk.
|
||||
**Demo-app:** Nieuwe kleine Pokédex (PokéAPI, geen key, visueel)
|
||||
**Vervolg op:** Les 12 — Tool Calling
|
||||
**Aansluit op:** Les 14 — Agents
|
||||
**Lesvorm:** online via Teams. Tim deelt scherm met slides + Cursor + browser.
|
||||
**Demo:** een vers `npx`-gescaffold Next.js project, gebouwd live in Cursor.
|
||||
**Aansluit op:** Les 13 (Polderfest-agent op localhost) → Les 15 (RAG).
|
||||
|
||||
---
|
||||
|
||||
## Blok-structuur
|
||||
|
||||
| Blok | Onderwerp | Slides |
|
||||
|------|-----------|--------|
|
||||
| 1 | Welkom + Vandaag | 1–2 |
|
||||
| 2 | Scaffold + Git + Vercel koppelen | 3–7 |
|
||||
| 3 | Vercel CLI + env vars | 8–10 |
|
||||
| 4 | Cursor — Editor / Agent / Background + rules | 11–19 |
|
||||
| 5 | Live feature-workflow (plan → branch → preview) | 20–24 |
|
||||
| 6 | Recap + Nu jullie | 25–27 |
|
||||
|
||||
---
|
||||
|
||||
## Slide 1: Title
|
||||
### Les 13 — Externe APIs + Cursor + Vercel
|
||||
### Les 14 — Cursor + Vercel
|
||||
|
||||
**Visual:**
|
||||
- Background: CREAM
|
||||
- "Les 13" in BLUE
|
||||
- "Externe APIs + Cursor + Vercel" in BLACK
|
||||
- Subtitle: "Van localhost naar productie — met Cursor agents en preview deploys"
|
||||
- "Les 14" in BLUE
|
||||
- "Cursor + Vercel" in BLACK
|
||||
- Subtitle: "Van leeg project naar preview-deploys — met AI"
|
||||
- Iconen onderaan: Cursor-logo, Vercel-driehoek, GitHub-octocat
|
||||
|
||||
---
|
||||
|
||||
## Slide 2: Terugblik
|
||||
### Waar staan we?
|
||||
## Slide 2: Vandaag
|
||||
### Wat we live gaan bouwen
|
||||
|
||||
**Vorige lessen:**
|
||||
- Les 11: AI SDK basics
|
||||
- Les 12: Tool Calling
|
||||
- Les 13: Agents
|
||||
**Vandaag in één lopend voorbeeld:**
|
||||
1. `npx create-next-app` — vers project
|
||||
2. Git init + push naar GitHub
|
||||
3. Vercel project koppelen → eerste deploy
|
||||
4. Vercel CLI: link, env pull/add
|
||||
5. Cursor open: Plan, Build, Context, Rules, Background agents
|
||||
6. Feature in nieuwe branch → preview-URL
|
||||
7. Tweede feature via een Background agent
|
||||
|
||||
**Tot nu toe:** alles draaide op `localhost:3000`. Geen wereld eromheen.
|
||||
**Werkwijze:** ik bouw klassikaal, jullie kijken en knopen mee. Na pauze doen jullie het zelf.
|
||||
|
||||
**Vandaag:**
|
||||
- Externe APIs aanroepen (geen LLM nodig, gewoon data ophalen)
|
||||
- App naar productie met Vercel
|
||||
- Feature branches met Cursor agents (Composer + Background)
|
||||
- GitHub Actions CI (lint + build per PR)
|
||||
|
||||
**Visual:** localhost icoon → cloud icoon met branches eruit.
|
||||
**Visual:** workflow-pijl van leeg project → Cursor-features → preview-URL.
|
||||
|
||||
---
|
||||
|
||||
## Slide 3: Planning
|
||||
### Vandaag — 180 minuten
|
||||
## Slide 3: Scaffold met `npx`
|
||||
### Vers Next.js project in 30 seconden
|
||||
|
||||
| Onderwerp | Duur |
|
||||
|-----------|------|
|
||||
| Welkom + Terugblik | 10 min |
|
||||
| Theorie: Externe APIs in Next.js | 15 min |
|
||||
| Theorie: Cursor agents — Composer vs Background | 15 min |
|
||||
| Theorie: Vercel preview deploys + GH Actions | 15 min |
|
||||
| **Live Demo 1** — Pokédex bouwen met Composer | 30 min |
|
||||
| **Live Demo 2** — Deploy naar Vercel productie | 15 min |
|
||||
| **Pauze** | 15 min |
|
||||
| **Live Demo 3** — Feature branch met Background Agent | 25 min |
|
||||
| **Live Demo 4** — GitHub Actions CI | 15 min |
|
||||
| Composer vs Background — wanneer welke? | 10 min |
|
||||
| Lesopdracht + Huiswerk | 10 min |
|
||||
| Vragen + Afsluiting | 5 min |
|
||||
|
||||
**Format:** Demo-driven, Cursor en browser side-by-side.
|
||||
|
||||
---
|
||||
|
||||
## Slide 4: Wat is een externe API?
|
||||
### Data van buiten, in jouw app
|
||||
|
||||
**Het idee:**
|
||||
- Andere mensen / bedrijven hebben data of functionaliteit
|
||||
- Ze stellen die beschikbaar via HTTP endpoints (REST)
|
||||
- Jij doet een `fetch()` en krijgt JSON terug
|
||||
|
||||
**Voorbeelden:**
|
||||
- PokéAPI — Pokemon data (geen key)
|
||||
- Open-Meteo — weer (geen key)
|
||||
- GitHub API — repos, users, issues
|
||||
- Tavily — web search (key nodig)
|
||||
- Stripe — betalingen (key + secret)
|
||||
|
||||
**API keys** horen in environment variables — nooit in je code committen.
|
||||
|
||||
**Visual:** Diagram: Next.js → fetch → externe API → JSON terug.
|
||||
|
||||
---
|
||||
|
||||
## Slide 5: Drie manieren om een API te fetchen in Next.js
|
||||
### Static, Server (dynamic), Client — wanneer welke?
|
||||
|
||||
| | Wanneer gerenderd | Data versheid | Externe API key veilig? | Wanneer kiezen |
|
||||
|---|---|---|---|---|
|
||||
| **Static** (build-time) | Tijdens `next build` | Bij elke build | Ja, server | Data verandert zelden (blog, marketing, lijst) |
|
||||
| **Server / Dynamic** (request-time) | Bij elke request | Altijd vers | Ja, server | Data per user/request anders, of telkens vers nodig |
|
||||
| **ISR** (revalidate) | Eerste keer + na X seconden | Periodiek vers | Ja, server | Sweet spot: snel + redelijk vers (Pokédex!) |
|
||||
| **Client** (`useEffect`) | In de browser na hydration | Live | **Nee** — sleutel staat in browser | User-interactie, live updates, geen geheim |
|
||||
|
||||
**Code-verschil = één optie in `fetch`:**
|
||||
|
||||
```typescript
|
||||
// 1. STATIC — gecached forever (default in Next.js!)
|
||||
const res = await fetch(URL);
|
||||
|
||||
// 2. ISR — cache met revalidate
|
||||
const res = await fetch(URL, { next: { revalidate: 3600 } });
|
||||
|
||||
// 3. DYNAMIC — altijd vers
|
||||
const res = await fetch(URL, { cache: "no-store" });
|
||||
```bash
|
||||
npx create-next-app@latest mijn-portfolio
|
||||
# kies: TypeScript, Tailwind, App Router, src/ dir
|
||||
cd mijn-portfolio
|
||||
npm run dev
|
||||
```
|
||||
|
||||
**Vandaag gebruiken we:** Static voor de Pokédex-lijst, ISR voor detail-pages.
|
||||
**Wat je krijgt:**
|
||||
- Werkende Next.js 16 app op `localhost:3000`
|
||||
- TypeScript + Tailwind preconfigured
|
||||
- ESLint klaar
|
||||
- `.gitignore` met `.env.local`, `node_modules`, `.next`
|
||||
|
||||
**Visual:** terminal-screenshot van create-next-app vragen.
|
||||
|
||||
---
|
||||
|
||||
## Slide 6: Cursor — twee soorten agents
|
||||
### Composer vs Background
|
||||
## Slide 4: Git init + push naar GitHub
|
||||
### Code online krijgen
|
||||
|
||||
| | Composer | Background Agent |
|
||||
|---|----------|------------------|
|
||||
| Waar draait | Lokaal in je editor | Cursor cloud |
|
||||
| Mode | Synchroon — jij wacht | Async — runt zonder jou |
|
||||
| Output | Diffs in editor | Branch + PR op GitHub |
|
||||
| Goed voor | Pair programming, snelle changes | Welomschreven feature, tijdens andere taak |
|
||||
| Aanroep | `Cmd+I` (Mac) / `Ctrl+I` | `Cmd+Shift+P` → Background Agent |
|
||||
| Repo nodig | Lokaal | Verbonden met Cursor cloud |
|
||||
```bash
|
||||
git init
|
||||
git add .
|
||||
git commit -m "init"
|
||||
git branch -M main
|
||||
|
||||
**Beide kanten:**
|
||||
- Composer: jij stuurt, snel feedback, niet schaalbaar voor lange taken
|
||||
- Background: schaalbaar (5 agents tegelijk), maar minder controle in flight
|
||||
|
||||
**Visual:** Twee tracks naast elkaar — synchroon (jij + composer) vs asynchroon (background agent + jij elders).
|
||||
|
||||
---
|
||||
|
||||
## Slide 7: Vercel — productie, preview, environment variables
|
||||
### Wat verandert er per omgeving?
|
||||
|
||||
**Drie omgevingen, drie scopes:**
|
||||
|
||||
| Scope | Wordt gedeployed bij | URL | Wie ziet het |
|
||||
|-------|---------------------|-----|--------------|
|
||||
| **Production** | Push naar `main` | `je-app.vercel.app` | Iedereen |
|
||||
| **Preview** | Push naar elke andere branch | `je-app-git-{branch}-{user}.vercel.app` | Iedereen met link (delen via PR) |
|
||||
| **Development** | Lokaal `pnpm dev` | `localhost:3000` | Alleen jij |
|
||||
|
||||
**Environment variables per scope:**
|
||||
|
||||
In Vercel → Project → Settings → Environment Variables zet je per var **drie keer** een waarde:
|
||||
|
||||
| Var | Production | Preview | Development |
|
||||
|-----|------------|---------|-------------|
|
||||
| `DATABASE_URL` | prod database | staging database | lokale database |
|
||||
| `OPENAI_API_KEY` | echte key | test-key (lage limit) | jouw persoonlijke key |
|
||||
| `STRIPE_KEY` | `sk_live_...` | `sk_test_...` | `sk_test_...` |
|
||||
| `NEXT_PUBLIC_APP_URL` | je-app.com | preview-url | localhost:3000 |
|
||||
|
||||
**Waarom is dit belangrijk?**
|
||||
- Preview branches mogen NIET stagecoach naar je productie-database schrijven
|
||||
- Test API-keys voorkomen kosten/quota-problemen bij experiments
|
||||
- Eén branch = één geïsoleerde wereld om in te testen
|
||||
|
||||
**Cursor Background Agent + Vercel preview = killer combo:**
|
||||
Agent opent PR → preview URL automatisch in PR-comment → reviewer test live in browser, geen lokale setup nodig.
|
||||
|
||||
---
|
||||
|
||||
## Slide 8: GitHub Actions CI
|
||||
### Lint + build per PR
|
||||
|
||||
**Waarom:**
|
||||
- Voor merge weet je: code lint clean én build slaagt
|
||||
- Voorkomt "werkte op mijn laptop"
|
||||
- Background Agent doet PR → CI draait automatisch → groene check of feedback
|
||||
|
||||
**Minimaal `.github/workflows/ci.yml`:**
|
||||
|
||||
```yaml
|
||||
name: CI
|
||||
on:
|
||||
pull_request:
|
||||
branches: [main]
|
||||
push:
|
||||
branches: [main]
|
||||
|
||||
jobs:
|
||||
check:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: pnpm/action-setup@v3
|
||||
with: { version: 9 }
|
||||
- uses: actions/setup-node@v4
|
||||
with: { node-version: 20, cache: pnpm }
|
||||
- run: pnpm install
|
||||
- run: pnpm lint
|
||||
- run: pnpm build
|
||||
gh repo create mijn-portfolio --public --source=. --push
|
||||
```
|
||||
|
||||
**Tijd per CI-run:** ~1-2 min op een kleine Next.js app.
|
||||
**Of handmatig:**
|
||||
- github.com → New repository → naam → empty
|
||||
- `git remote add origin <url>`
|
||||
- `git push -u origin main`
|
||||
|
||||
**Resultaat:** je code staat op github.com — basis voor alles wat volgt.
|
||||
|
||||
**Visual:** flow-diagram local → GitHub.
|
||||
|
||||
---
|
||||
|
||||
## Slide 9: Wat we vandaag bouwen
|
||||
### Pokédex — Next.js + PokéAPI
|
||||
## Slide 5: Vercel project koppelen
|
||||
### Van GitHub-repo naar live URL
|
||||
|
||||
**Doel:** Een mini-Pokédex om de hele pipeline te oefenen.
|
||||
**Drie stappen:**
|
||||
1. vercel.com/new → **Import Git Repository**
|
||||
2. Kies je `mijn-portfolio` repo
|
||||
3. Framework auto-detect: Next.js → **Deploy**
|
||||
|
||||
**Stack:**
|
||||
- Next.js 16 + TypeScript + Tailwind + Shadcn
|
||||
- PokéAPI (geen key, gratis, geen rate-limit bij normaal gebruik)
|
||||
- Cursor (Composer + Background Agent)
|
||||
- GitHub (repo + Actions)
|
||||
- Vercel (productie + preview deploys)
|
||||
**Wat Vercel doet:**
|
||||
- Klonet je repo
|
||||
- Draait `npm install` + `npm run build`
|
||||
- Deploy't naar het Edge Network
|
||||
- Geeft je een URL: `mijn-portfolio-<scope>.vercel.app`
|
||||
|
||||
**Features die we bouwen vandaag:**
|
||||
**Voortaan:** elke `git push` → automatische deploy. Geen extra werk.
|
||||
|
||||
| Feature | Met | Branch |
|
||||
|---------|-----|--------|
|
||||
| Pokédex list (eerste 151) | Composer | main |
|
||||
| Detail page per Pokemon | Composer | main |
|
||||
| Search met filter | Background Agent | feature/search |
|
||||
| Type-filter chips | Background Agent | feature/type-filter |
|
||||
|
||||
**Visual:** Folder-tree + GitHub branch-diagram + Vercel URLs.
|
||||
**Visual:** Vercel "Import Project" scherm.
|
||||
|
||||
---
|
||||
|
||||
## Slide 10: LIVE DEMO 1 — Pokédex met Composer + rendering modes
|
||||
### ~30 min
|
||||
## Slide 6: Productie-URL + branches
|
||||
### Wat krijg je gratis
|
||||
|
||||
**Wat ik laat zien:**
|
||||
1. `pnpm create next-app pokedex --typescript --tailwind --app`
|
||||
2. Open in Cursor
|
||||
3. **Composer (`Cmd+I`)**: "Bouw een lijst-pagina met de eerste 151 Pokémon van PokéAPI als statische pagina. Gebruik Tailwind cards. De fetch moet static zijn — geen revalidate."
|
||||
4. Composer maakt: `app/page.tsx` met `fetch(URL)` (static) + map + cards
|
||||
5. `pnpm dev` — werkt
|
||||
6. **Composer**: "Voeg detail-pagina toe op /pokemon/[name] met sprite + types + stats. Gebruik ISR met revalidate van 3600 seconden."
|
||||
7. Composer maakt: `app/pokemon/[name]/page.tsx` met `{ next: { revalidate: 3600 } }`
|
||||
8. **Bewijs in de browser:** open detail-pagina, refresh — direct geladen (statisch). Verander URL → andere Pokémon → ook snel (ISR cache).
|
||||
9. **Show de Network tab:** geen calls naar PokéAPI vanuit browser, alles server.
|
||||
10. **Bonus:** maak één pagina met `cache: "no-store"` (dynamic) zodat studenten alle drie de modi gezien hebben.
|
||||
**Twee URL-types:**
|
||||
|
||||
**Belangrijk om uit te leggen:**
|
||||
- Composer is een TAB in Cursor — niet hetzelfde als Chat
|
||||
- Multi-file edits zichtbaar als diffs — je accepteert per file
|
||||
- Terminal commands vragen toestemming
|
||||
- De rendering-mode is één optie in `fetch()` — verder verandert je code niet
|
||||
| Type | URL-formaat | Wanneer |
|
||||
|------|-------------|---------|
|
||||
| **Productie** | `mijn-portfolio-<scope>.vercel.app` | push naar `main` |
|
||||
| **Preview** | `...-git-<branch>-<scope>.vercel.app` | push naar elke andere branch |
|
||||
|
||||
**Visual:** Cursor screenshot met Composer panel.
|
||||
**Voordeel preview-URLs:**
|
||||
- Iedereen kan klikken zonder lokale setup
|
||||
- PR-bot post URL automatisch in je Pull Request
|
||||
- Per push update de preview vanzelf
|
||||
|
||||
**Visual:** twee URL-bubbels (groen = productie, blauw = preview).
|
||||
|
||||
---
|
||||
|
||||
## Slide 11: LIVE DEMO 2 — Deploy naar Vercel + Environment variables
|
||||
### ~20 min
|
||||
## Slide 7: Env vars in Vercel UI
|
||||
### Geheimen veilig op productie
|
||||
|
||||
**Wat ik laat zien:**
|
||||
1. **GitHub:** new repo `pokedex` — public
|
||||
2. **Lokaal:**
|
||||
```bash
|
||||
git init
|
||||
git add . && git commit -m "Initial Pokédex"
|
||||
git branch -M main
|
||||
git remote add origin https://github.com/USER/pokedex.git
|
||||
git push -u origin main
|
||||
```
|
||||
3. **Vercel:** Add New → Import Git Repository → kies `pokedex` → Deploy
|
||||
4. Wachten 60-90s op build → Productie URL — werkt!
|
||||
5. **Environment Variables demo:** voeg een var toe in `app/page.tsx`:
|
||||
```tsx
|
||||
<p>Omgeving: {process.env.NEXT_PUBLIC_APP_ENV}</p>
|
||||
```
|
||||
6. **Vercel → Settings → Environment Variables:**
|
||||
- `NEXT_PUBLIC_APP_ENV` = `production` → vinkje bij Production
|
||||
- `NEXT_PUBLIC_APP_ENV` = `preview` → vinkje bij Preview
|
||||
- Lokaal: `.env.local` → `NEXT_PUBLIC_APP_ENV=development`
|
||||
7. Redeploy → productie URL toont "production"
|
||||
8. Maak feature branch `chore/banner`, push → preview URL toont "preview"
|
||||
9. Lokaal `pnpm dev` → toont "development"
|
||||
**Wat je vandaag NOG niet hebt** (lege app), maar binnenkort wel:
|
||||
- Database URLs
|
||||
- API keys (OpenAI, Stripe, etc.)
|
||||
- Secret tokens
|
||||
|
||||
**Wat studenten moeten zien:**
|
||||
- Push to main = productie deploy
|
||||
- Push to andere branch = preview deploy (eigen URL)
|
||||
- Per omgeving andere env-waarden
|
||||
- Voor secrets: vinkje **alleen** bij Production (preview-key gebruiken in preview!)
|
||||
**Drie environments:**
|
||||
|
||||
| Environment | Wanneer actief |
|
||||
|-------------|----------------|
|
||||
| Production | `main` branch deploys |
|
||||
| Preview | alle andere branches |
|
||||
| Development | `vercel dev` lokaal |
|
||||
|
||||
**`NEXT_PUBLIC_` prefix** → variabele wordt in browser-bundle gestopt. Zonder prefix = server-only.
|
||||
|
||||
**Visual:** Vercel env-vars dashboard screenshot.
|
||||
|
||||
---
|
||||
|
||||
## Slide 12: Pauze
|
||||
### 15 minuten
|
||||
## Slide 8: Vercel CLI — waarom
|
||||
### Sneller dan klikken in het dashboard
|
||||
|
||||
**Wat de CLI je geeft:**
|
||||
- `vercel link` — koppel lokale folder aan Vercel project
|
||||
- `vercel env pull` — productie-env-vars → `.env.local`
|
||||
- `vercel env add` / `rm` — vars beheren zonder dashboard
|
||||
- `vercel logs` — bekijk runtime logs
|
||||
- `vercel --prod` — handmatige productie-deploy
|
||||
|
||||
**Installeren:**
|
||||
|
||||
```bash
|
||||
npm i -g vercel
|
||||
vercel login
|
||||
```
|
||||
|
||||
**Visual:** terminal-screenshot van `vercel --help`.
|
||||
|
||||
---
|
||||
|
||||
## Slide 13: LIVE DEMO 3 — Feature branch + Background Agent
|
||||
### ~25 min
|
||||
## Slide 9: `vercel link` + `vercel env pull`
|
||||
### Lokaal werken met dezelfde env vars als productie
|
||||
|
||||
**Wat ik laat zien:**
|
||||
1. **Cursor:** `Cmd+Shift+P` → "Open Background Agent"
|
||||
2. **Verbind** repo aan Cursor cloud (eenmalig — accepteren in browser)
|
||||
3. **Prompt aan Background Agent:**
|
||||
> "Maak een nieuwe branch `feature/search`. Voeg een zoekbalk toe bovenaan de homepage. Wanneer iemand typt, filter de Pokémon-lijst op naam. Gebruik client-side state. Push de branch + open een PR met titel 'Add search bar'."
|
||||
4. **Background Agent gaat aan de slag** — Tim laat zien hoe je 'm volgt in het Cursor panel
|
||||
5. **~3-5 minuten later:** PR verschijnt op GitHub
|
||||
6. **Vercel** comment in PR: preview URL → klik → live preview!
|
||||
7. **Demo:** zoeken werkt op preview URL
|
||||
8. **Code review** in GitHub UI: wat heeft de agent gedaan?
|
||||
9. **Merge** → main → productie deploy
|
||||
```bash
|
||||
cd mijn-portfolio
|
||||
vercel link
|
||||
# → kiest scope (org/team) + project
|
||||
# → maakt .vercel/project.json aan
|
||||
|
||||
**Tweede prompt — parallel:**
|
||||
> "Maak branch `feature/type-filter`. Voeg chips toe bij elk type (fire, water, ...) om te filteren."
|
||||
vercel env pull .env.local
|
||||
# → trekt ALLE env vars uit je gekoppelde environment
|
||||
# → schrijft naar .env.local (overschrijft bestaande)
|
||||
```
|
||||
|
||||
Beide branches kunnen tegelijk draaien.
|
||||
**Voordeel:** je hoeft je env vars niet handmatig te kopiëren. Eén commando = synced lokaal.
|
||||
|
||||
**Visual:** Cursor Background Agent panel + GitHub PR + Vercel preview URL.
|
||||
**Tip:** `vercel env pull --environment=preview` voor preview-specifieke vars.
|
||||
|
||||
**Visual:** terminal-output van een succesvolle env pull.
|
||||
|
||||
---
|
||||
|
||||
## Slide 14: LIVE DEMO 4 — GitHub Actions CI
|
||||
### ~15 min
|
||||
## Slide 10: `vercel env add/rm`
|
||||
### Vars wijzigen vanuit terminal
|
||||
|
||||
**Wat ik laat zien:**
|
||||
1. **Vraag aan Composer:**
|
||||
> "Voeg een GitHub Actions workflow toe: lint + build moeten slagen op PR's naar main."
|
||||
2. Composer maakt `.github/workflows/ci.yml`
|
||||
3. Push naar nieuwe branch `chore/add-ci`
|
||||
4. Open PR → GitHub Actions tab → workflow draait
|
||||
5. **Demo failing CI:** introduceer typo (`<dvi>`), push, zie rode X
|
||||
6. Fix met Composer ("repareer de TypeScript build error in de PR"), push, zie groene check
|
||||
7. **Branch protection:** main beschermen — PR's mogen pas mergen als CI groen is
|
||||
```bash
|
||||
# Toevoegen
|
||||
vercel env add OPENAI_API_KEY production
|
||||
# → vraagt om waarde
|
||||
|
||||
**Waarom belangrijk:**
|
||||
- Background Agents kunnen lint-errors maken — CI vangt dit op
|
||||
- Voor je merge: weet je dat het op een schone runner ook werkt
|
||||
- Geen "werkt op mijn laptop"-discussies
|
||||
# Voor alle 3 environments tegelijk:
|
||||
vercel env add DATABASE_URL production preview development
|
||||
|
||||
# Verwijderen
|
||||
vercel env rm OPENAI_API_KEY production
|
||||
|
||||
# Lijst
|
||||
vercel env ls
|
||||
```
|
||||
|
||||
**Wanneer handig:** als je een API-key roteert, hoef je niet via dashboard. Eén CLI-commando, klaar.
|
||||
|
||||
**Visual:** CLI-flow met prompt-balletjes.
|
||||
|
||||
---
|
||||
|
||||
## Slide 15: Composer vs Background Agent
|
||||
### Wanneer welke?
|
||||
## Slide 11: Cursor — alles in één IDE
|
||||
### Wat is het, waarom belangrijk
|
||||
|
||||
| Scenario | Tool |
|
||||
|----------|------|
|
||||
| Snelle wijziging tijdens coden | Composer |
|
||||
| Complex refactor over meerdere files | Composer (kun je begeleiden) |
|
||||
| Onbekend gebied — wil zien wat agent doet | Composer |
|
||||
| Welomschreven feature, kan op de achtergrond | Background Agent |
|
||||
| Parallel werken aan 3 features | 3x Background Agent |
|
||||
| Tijdens vergadering een PR voorbereiden | Background Agent |
|
||||
| Repo nog niet in Cursor cloud | Composer |
|
||||
**Wat is Cursor:**
|
||||
- VS Code-fork met AI native ingebouwd
|
||||
- Niet een extensie — het IS de IDE
|
||||
- Werkt met Claude, GPT, Gemini en lokale modellen
|
||||
|
||||
**Mentale model:**
|
||||
- **Composer = pair programming** — jij + AI samen aan dezelfde plek
|
||||
- **Background Agent = delegeren** — je geeft taak, gaat iets anders doen, komt terug
|
||||
**Wat je krijgt boven gewone VS Code:**
|
||||
- Chat met volledige codebase-context
|
||||
- Plan-mode (eerst denken, dan doen)
|
||||
- Build-mode (autonoom uitvoeren)
|
||||
- Inline edit (Cmd+K)
|
||||
- Background agents (cloud, async)
|
||||
- Cursor Rules (per-project instructies)
|
||||
- Tab-completion die meekijkt met je hele bestand
|
||||
|
||||
**Risico bij Background Agent:**
|
||||
- Agent kan vastlopen op rare config — debugging is moeilijker
|
||||
- Verbruikt credits (Cursor Pro of Business)
|
||||
- Te vage prompts → matige PR's. Specifiek zijn helpt.
|
||||
**Visual:** vier kwadranten met de hoofdfeatures.
|
||||
|
||||
---
|
||||
|
||||
## Slide 16: Lesopdracht + Huiswerk
|
||||
### Pokédex zelf bouwen + deploy
|
||||
## Slide 12: De twee windows + Agent mode
|
||||
### Cursor 3 — wat zit waar
|
||||
|
||||
**Lesopdracht (in-class, 30 min):**
|
||||
- Eigen Pokédex repo opzetten (zelfde setup als demo)
|
||||
- 1 feature met Composer toevoegen
|
||||
- Push naar GitHub + connect Vercel
|
||||
- Productie URL openen — werkt
|
||||
In Cursor 3 zijn er **twee hoofdvensters** die je met **Cmd+Shift+N** wisselt:
|
||||
|
||||
**Huiswerk (voor Les 16):**
|
||||
- **A:** Feature branch met Background Agent (1 feature, jouw keuze) → preview URL delen
|
||||
- **B:** GitHub Actions CI (lint + build) in werking
|
||||
- **C:** Branch protection — main mag pas merge na groene CI
|
||||
- **D:** `DEPLOY.md` met:
|
||||
- Productie URL + 1 preview URL
|
||||
- Reflectie: Composer vs Background — wanneer welke werkte beter?
|
||||
- 1 voorbeeld waar Background Agent verbluffend goed of slecht was
|
||||
| Window | Wanneer | Hoe |
|
||||
|--------|---------|-----|
|
||||
| **Editor Window** (classic) | Code schrijven en reviewen | Cmd+Shift+N → Editor |
|
||||
| **Agents Window** | Meerdere agents parallel managen | Cmd+Shift+N → Agents |
|
||||
|
||||
**Bonus:**
|
||||
- Eigen externe API integreren (Open-Meteo, GitHub, jouw keuze)
|
||||
- Vercel environment vars demo (verschillende waarde in preview vs prod)
|
||||
**Binnen Editor Window:**
|
||||
- **Cmd+L** — chat opent rechts (kies modus: Ask / Agent / Plan)
|
||||
- **Cmd+K** — inline edit op selectie
|
||||
- **Tab** — completion
|
||||
|
||||
**Binnen Agents Window:**
|
||||
- Lijst met running / done agents
|
||||
- Lokale agents, cloud (background) agents, remote SSH
|
||||
|
||||
**Gouden regel:**
|
||||
- Code schrijven of 1 ding bouwen → **Editor Window** + **Agent mode** in chat
|
||||
- Meerdere features parallel → **Agents Window** met Background agents
|
||||
|
||||
**Visual:** twee windows naast elkaar + sneltoets-pijl.
|
||||
|
||||
---
|
||||
|
||||
## Slide 17: Volgende les + Afsluiting
|
||||
### Vragen?
|
||||
## Slide 13: Editor Window — stap voor stap
|
||||
### Je hoofdwerkplek voor code
|
||||
|
||||
**Vandaag gezien:**
|
||||
- Externe API integratie in Server Components
|
||||
- Static / ISR / Dynamic / Client — vier rendering modes
|
||||
- Cursor Composer voor synchrone changes
|
||||
- Cursor Background Agent voor async features
|
||||
- Vercel productie + preview per branch
|
||||
- Environment variables per omgeving (production / preview / development)
|
||||
- GitHub Actions CI (lint + build)
|
||||
**Wat je live ziet:**
|
||||
1. **Cmd+Shift+N** — switcht naar Editor Window (classic IDE-layout)
|
||||
2. Linker zijbalk = file explorer, midden = editor, onder = terminal
|
||||
3. **Cmd+L** — opent chat-paneel rechts
|
||||
4. Voor snelle inline edits: selecteer code, druk **Cmd+K**, beschrijf wijziging
|
||||
5. Cursor toont **diff inline** in de editor
|
||||
6. **Accept** (Tab) of **Reject** (Esc)
|
||||
7. Voor file-search: **Cmd+P** (zoals VS Code)
|
||||
8. **Tab** — accepteert AI auto-completion suggesties
|
||||
|
||||
**Volgende les (Les 14): Agents**
|
||||
- LLM in een loop met tools, autonoom 20-50 stappen
|
||||
- `ToolLoopAgent`, `stopWhen`, `prepareStep`
|
||||
- Research-agent from scratch
|
||||
- Wanneer wel/niet agents gebruiken
|
||||
**Wanneer kies je dit:**
|
||||
- Je standaard coding-flow — alles wat je in VS Code zou doen
|
||||
- Wanneer je veel files tegelijk wilt zien (split screen)
|
||||
- Voor VS Code extensions (linters, formatters, debugger)
|
||||
|
||||
**Vragen? Feedback?**
|
||||
**Visual:** screenshot Editor Window met file explorer + editor + Cmd+K-diff.
|
||||
|
||||
---
|
||||
|
||||
## Slide Summary
|
||||
## Slide 14: Agent mode — stap voor stap
|
||||
### AI bouwt autonoom in je Editor Window
|
||||
|
||||
| # | Title | Type |
|
||||
|---|-------|------|
|
||||
| 1 | Title | Opening |
|
||||
| 2 | Terugblik | Recap |
|
||||
| 3 | Planning | 180-min |
|
||||
| 4 | Externe APIs | Theorie |
|
||||
| 5 | Externe API code | Theorie |
|
||||
| 6 | Cursor — Composer vs Background | Theorie |
|
||||
| 7 | Vercel preview deploys | Theorie |
|
||||
| 8 | GitHub Actions CI | Theorie |
|
||||
| 9 | Wat we bouwen | Intro demo |
|
||||
| 10 | **LIVE DEMO 1** — Pokédex + Composer | Demo |
|
||||
| 11 | **LIVE DEMO 2** — Vercel deploy | Demo |
|
||||
| 12 | Pauze | Break |
|
||||
| 13 | **LIVE DEMO 3** — Background Agent + preview | Demo |
|
||||
| 14 | **LIVE DEMO 4** — GitHub Actions CI | Demo |
|
||||
| 15 | Wanneer welke Cursor-mode | Reflectie |
|
||||
| 16 | Lesopdracht + Huiswerk | Praktijk |
|
||||
| 17 | Afsluiting + Les 13 preview | Closing |
|
||||
**Wat je live ziet:**
|
||||
1. In Editor Window: **Cmd+L** — chat opent rechts
|
||||
2. Bovenaan chat: **mode-selector** → kies **"Agent"**
|
||||
3. Typ de **hele taak**: *"Bouw een /about pagina met team-grid in onze huisstijl"*
|
||||
4. **Enter** — agent start zelfstandig
|
||||
5. Cursor toont een lopende **takenlijst** + voortgang
|
||||
6. Agent **edit meerdere files**, runt terminal-commando's (vraagt vooraf), kan **web browsen**
|
||||
7. Geen limiet op het aantal tool-calls per taak
|
||||
8. Aan het einde: samenvatting → **Keep / Accept all** of per file
|
||||
|
||||
**Wanneer kies je dit:**
|
||||
- Grotere feature die meerdere files raakt
|
||||
- Refactors, nieuwe pagina's, API-routes opzetten
|
||||
- Je vertrouwt het plan en hebt tijd om aan het eind te reviewen
|
||||
|
||||
**Visual:** chat-paneel met mode-dropdown + lopende takenlijst.
|
||||
|
||||
---
|
||||
|
||||
## Bronnen
|
||||
## Slide 15: Background agents — stap voor stap
|
||||
### Cloud-based async — in Agents Window
|
||||
|
||||
- **PokéAPI:** https://pokeapi.co/
|
||||
- **Next.js fetching:** https://nextjs.org/docs/app/building-your-application/data-fetching
|
||||
- **Cursor agents:** https://cursor.com/product
|
||||
- **Vercel preview deployments:** https://vercel.com/docs/deployments/preview-deployments
|
||||
- **Vercel environment variables:** https://vercel.com/docs/environment-variables
|
||||
- **GitHub Actions for Next.js:** https://nextjs.org/docs/app/building-your-application/deploying
|
||||
- **GitHub branch protection:** https://docs.github.com/en/repositories/configuring-branches-and-merges-in-your-repository/managing-protected-branches
|
||||
**Wat het is:**
|
||||
- Async agents die in **geïsoleerde cloud VMs** draaien
|
||||
- Werken parallel in eigen Git branches
|
||||
- Beheer via **Agents Window** (Cmd+Shift+N) of `cursor.com/agents`
|
||||
|
||||
**Hoe een agent aanmaken:**
|
||||
1. **Cmd+Shift+N** → naar Agents Window
|
||||
2. Klik **"New agent"** (of `+` in de kop)
|
||||
3. Kies **environment**: local / cloud / remote SSH
|
||||
4. Beschrijf de taak — specifiek, alsof het een ticket is
|
||||
5. Optioneel: kies branch-naam (`feature/about`)
|
||||
6. Klik **"Start"**
|
||||
7. Agent draait → je kunt switchen terug naar Editor Window en doorwerken
|
||||
|
||||
**Hoe je resultaten ophaalt:**
|
||||
1. **Cmd+Shift+N** terug naar Agents Window
|
||||
2. Klare agents staan op **"Done"** in de agents-lijst
|
||||
3. Klik agent → zie **diff + commit message + uitgevoerde stappen**
|
||||
4. **"Open in PR"** → automatische PR op GitHub
|
||||
5. Vercel maakt preview-URL → review → merge
|
||||
|
||||
**Wanneer:** 2–4 parallelle features, lange refactors die je niet wilt babysitten, of taken die je triggert vanuit je telefoon / Slack / Linear.
|
||||
|
||||
**Visual:** Agents Window screenshot met agents-lijst.
|
||||
|
||||
---
|
||||
|
||||
## Slide 16: Context — `@`-mentions
|
||||
### Hoe Cursor je codebase begrijpt
|
||||
|
||||
**Wat je kunt taggen in chat:**
|
||||
|
||||
| Tag | Betekenis |
|
||||
|-----|-----------|
|
||||
| `@file:foo.tsx` | specifiek bestand |
|
||||
| `@folder:components` | hele map |
|
||||
| `@code:functionName` | losse functie/class |
|
||||
| `@docs` | externe documentatie |
|
||||
| `@web` | live web-search |
|
||||
| `@git` | git history / blame |
|
||||
| `@recommended` | Cursor's eigen suggestie |
|
||||
|
||||
**Voorbeeld:** *"Refactor @file:UserCard.tsx zodat het @code:useUser uit @folder:hooks gebruikt"*
|
||||
|
||||
**Visual:** chat-bubble met meerdere `@`-tags highlighted.
|
||||
|
||||
---
|
||||
|
||||
## Slide 17: Cursor Rules
|
||||
### Per-project instructies aan de AI
|
||||
|
||||
**Wat zijn rules:**
|
||||
- Markdown-bestanden in `.cursor/rules/`
|
||||
- Worden aan ELKE prompt toegevoegd als systeem-instructie
|
||||
- Per-bestand-glob: alleen actief op matching bestanden
|
||||
|
||||
**Voorbeeld `.cursor/rules/general.mdc`:**
|
||||
|
||||
```markdown
|
||||
---
|
||||
description: Algemene project-conventies
|
||||
alwaysApply: true
|
||||
---
|
||||
|
||||
- Gebruik TypeScript strict mode
|
||||
- Tailwind voor styling — geen CSS modules
|
||||
- Server components by default; "use client" alleen waar nodig
|
||||
- Imports: gebruik @/ alias voor src/
|
||||
- Geen any-types
|
||||
```
|
||||
|
||||
**Visual:** mappenstructuur met `.cursor/rules/` uitgevouwen.
|
||||
|
||||
---
|
||||
|
||||
## Slide 18: Project context — `AGENTS.md`
|
||||
### High-level documentatie voor AI
|
||||
|
||||
**Naast Cursor Rules:**
|
||||
- `AGENTS.md` of `PROJECT.md` in repo root
|
||||
- Beschrijft architectuur, conventies, "hoe wij hier dingen doen"
|
||||
- Cursor + Claude Code + GitHub Copilot lezen dit alle drie
|
||||
|
||||
**Wat erin staat:**
|
||||
- Tech stack + waarom die keuzes
|
||||
- Belangrijke patronen (auth flow, error handling, etc.)
|
||||
- Hoe testen, hoe lokaal runnen, hoe deployen
|
||||
- Pointers naar belangrijke bestanden
|
||||
|
||||
**Effect:** AI begrijpt je codebase in plaats van wilde gokken te doen.
|
||||
|
||||
**Visual:** AGENTS.md structuur als outline.
|
||||
|
||||
---
|
||||
|
||||
## Slide 19: Tips & Tricks
|
||||
### Wat Cursor-power-users doen
|
||||
|
||||
**De top 7:**
|
||||
1. **Cmd+I** voor Composer met multi-file context
|
||||
2. **@-everything aanvullen** — Cursor onthoudt context per chat
|
||||
3. **Plan + Apply per stap** — niet alles tegelijk accepteren
|
||||
4. **`alwaysApply: false`** Rules voor opt-in conventies
|
||||
5. **Auto-mode** — Cursor kiest beste model per taak
|
||||
6. **Diff-preview** vóór accept — Cmd+Shift+Enter
|
||||
7. **Notepads** — herbruikbare prompt-snippets per project
|
||||
|
||||
**Anti-pattern:** alles in één gigantische chat dumpen. Hou chats focused.
|
||||
|
||||
**Visual:** tips als badges met nummering.
|
||||
|
||||
---
|
||||
|
||||
## Slide 20: De feature-workflow
|
||||
### Hoe één feature van idee naar preview-URL gaat
|
||||
|
||||
**Vijf stappen die we live doen:**
|
||||
1. **Plan** in Cursor: beschrijf de feature
|
||||
2. **Branch:** `git checkout -b feature/x` (Cursor kan dit ook)
|
||||
3. **Build:** laat Cursor de code schrijven
|
||||
4. **Commit + push** → Vercel start automatisch een preview-deploy
|
||||
5. **Open preview-URL** → test, geef feedback, itereer
|
||||
|
||||
**Cyclus duurt ±10 minuten** per feature. Inclusief AI denkwerk.
|
||||
|
||||
**Visual:** circulair diagram met de 5 stappen.
|
||||
|
||||
---
|
||||
|
||||
## Slide 21: Live demo 1 — Hero section
|
||||
### Plan → Build via Cursor Chat
|
||||
|
||||
**Wat ik demonstreer:**
|
||||
- Nieuwe branch: `git checkout -b feature/hero`
|
||||
- Cursor Chat: *"Plan een hero-section voor de homepage. Gradient achtergrond, een grote titel, ondertitel, twee CTA-knoppen."*
|
||||
- Plan bekijken → Apply
|
||||
- Inline tweaks via Cmd+K voor finetuning
|
||||
- `git add . && git commit -m "feat: hero section"`
|
||||
|
||||
**Visual:** screenshot van Cursor met Plan-output.
|
||||
|
||||
---
|
||||
|
||||
## Slide 22: Push → preview-URL
|
||||
### Vercel doet de rest
|
||||
|
||||
```bash
|
||||
git push -u origin feature/hero
|
||||
```
|
||||
|
||||
**Binnen 30–60 seconden:**
|
||||
- Vercel start build
|
||||
- Preview-URL verschijnt
|
||||
- (Als er een PR open is) Vercel-bot plakt URL onder de PR
|
||||
|
||||
**Open de preview-URL:** je nieuwe hero section staat live, gedeeld met iedereen die de link heeft.
|
||||
|
||||
**Visual:** Vercel build-progress screen.
|
||||
|
||||
---
|
||||
|
||||
## Slide 23: Live demo 2 — About page (background agent)
|
||||
### Parallel werken
|
||||
|
||||
**Wat ik demonstreer:**
|
||||
- Nieuwe branch: `git checkout -b feature/about`
|
||||
- Cursor → *"Open background agent: bouw een /about pagina met team-leden in een grid. Gebruik onze hero-section als stijlreferentie."*
|
||||
- Agent gaat in eigen tab aan de slag
|
||||
- Ik blijf in main editor verder werken
|
||||
- Agent meldt zich klaar → review diff → apply
|
||||
- Commit + push → tweede preview-URL
|
||||
|
||||
**Effect:** twee features tegelijk, beide met eigen preview.
|
||||
|
||||
**Visual:** twee preview-URLs naast elkaar.
|
||||
|
||||
---
|
||||
|
||||
## Slide 24: PR mergen → productie
|
||||
### De cyclus afsluiten
|
||||
|
||||
**Op GitHub:**
|
||||
1. Beide branches → PR's openen
|
||||
2. Reviewen in preview-URL's
|
||||
3. Mergen naar `main`
|
||||
4. Vercel detecteert merge → bouwt productie-deploy
|
||||
5. Productie-URL update vanzelf
|
||||
|
||||
**Mentaliteit:** main is altijd deploy-baar. Feature branches zijn experimenteerruimte.
|
||||
|
||||
**Visual:** PR-pagina met "Merge" knop gemarkeerd.
|
||||
|
||||
---
|
||||
|
||||
## Slide 25: Recap
|
||||
### Wat we vandaag hebben gedaan
|
||||
|
||||
- Vers Next.js project gescaffold met `npx`
|
||||
- Git repo + push naar GitHub
|
||||
- Vercel project gekoppeld + eerste deploy
|
||||
- Vercel CLI: link, env pull/add
|
||||
- Cursor in detail: Chat, Plan, Build, Inline, Rules, Background agents
|
||||
- Twee features in eigen branches → twee preview-URL's
|
||||
- PRs gemerged → productie
|
||||
|
||||
**Wat je nu kunt:** elke nieuwe app uit deze opleiding (of erbuiten) op deze manier opzetten. Cursor + Vercel zijn je standaard-werkbank.
|
||||
|
||||
**Visual:** checklist met alle stappen.
|
||||
|
||||
---
|
||||
|
||||
## Slide 26: Nu jullie — Lesopdracht
|
||||
### Bouw je eigen Cursor + Vercel flow
|
||||
|
||||
**Wat je gaat doen (in eigen tempo, ±1,5 uur):**
|
||||
1. `npx create-next-app` — je eigen project (kies een onderwerp dat je leuk vindt)
|
||||
2. Push naar GitHub
|
||||
3. Vercel project + eerste deploy
|
||||
4. `vercel link` + `vercel env pull`
|
||||
5. Open in Cursor → schrijf één `.cursor/rules/general.mdc`
|
||||
6. Schrijf één `AGENTS.md`
|
||||
7. Bouw 2 features in eigen branches via Cursor (Plan → Build)
|
||||
8. Push beide → check preview-URL's
|
||||
9. Open één PR, deel beide URL's in de chat
|
||||
|
||||
**Voor exacte stappen:** `Les14-Lesopdracht.pdf`.
|
||||
|
||||
**Visual:** opdrachtkaart met checklist.
|
||||
|
||||
---
|
||||
|
||||
## Slide 27: Volgende les
|
||||
### Wat komt eraan
|
||||
|
||||
**Les 15 — RAG + Embeddings:**
|
||||
- AI antwoorden op basis van eigen documenten
|
||||
- Embeddings + pgvector + context-injectie
|
||||
- We bouwen een PDF Q&A-app
|
||||
|
||||
**Voorbereiding:**
|
||||
- Je Next.js-app uit vandaag staat live op Vercel
|
||||
- Je hebt minstens 2 PR's met preview-URL's gemerged
|
||||
- Cursor draait, AGENTS.md en `.cursor/rules/` zijn ingevuld
|
||||
|
||||
**Visual:** plaatje document → embedding → AI-antwoord.
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,29 @@
|
||||
# Project Guide — voorbeeld voor mijn-portfolio
|
||||
|
||||
## Stack
|
||||
- Next.js 16 (App Router)
|
||||
- TypeScript strict
|
||||
- Tailwind CSS v4
|
||||
- Vercel hosting
|
||||
|
||||
## Belangrijke conventies
|
||||
- Server components by default
|
||||
- "use client" alleen voor interactie / hooks
|
||||
- @/ alias verwijst naar src/
|
||||
- ESLint moet groen zijn voor merge
|
||||
|
||||
## Hoe lokaal draaien
|
||||
```bash
|
||||
npm install
|
||||
vercel env pull .env.local
|
||||
npm run dev # http://localhost:3000
|
||||
```
|
||||
|
||||
## Hoe deployen
|
||||
- Productie: merge naar main → Vercel deploy't automatisch
|
||||
- Preview: push naar feature branch → Vercel maakt preview-URL
|
||||
|
||||
## Belangrijke bestanden
|
||||
- app/layout.tsx — root layout met fonts en theme
|
||||
- app/page.tsx — homepage met hero
|
||||
- src/components/ — herbruikbare UI-componenten
|
||||
@@ -0,0 +1,9 @@
|
||||
# Cursor templates — copy-paste klaar
|
||||
|
||||
Twee bestanden om in je nieuwe Next.js project te plakken:
|
||||
|
||||
- `general.mdc` → plaats in `.cursor/rules/general.mdc`
|
||||
- `AGENTS.md` → plaats in de repo-root
|
||||
|
||||
Beide bestanden vertellen Cursor (en Claude Code, en GitHub Copilot)
|
||||
hoe jouw project in elkaar zit en welke conventies je hanteert.
|
||||
@@ -0,0 +1,28 @@
|
||||
# Feature prompts — voor de live demo
|
||||
|
||||
## Feature 1 (Plan + Build, Cursor chat)
|
||||
|
||||
```
|
||||
Plan een hero-section voor de homepage van mijn-portfolio.
|
||||
|
||||
Specs:
|
||||
- Gradient achtergrond (subtiel, twee blauwtinten)
|
||||
- Grote titel: "Tim Rijkse"
|
||||
- Ondertitel: "AI Developer & Docent"
|
||||
- Twee CTA-knoppen: "Projecten" (primair) en "Contact" (secundair)
|
||||
- Responsive: stack op mobiel, naast elkaar op desktop
|
||||
- Gebruik Tailwind, geen custom CSS
|
||||
```
|
||||
|
||||
## Feature 2 (Background agent)
|
||||
|
||||
```
|
||||
Open background agent.
|
||||
|
||||
Bouw een /about pagina met team-leden in een grid. Specs:
|
||||
- 6 team-leden (mock data is prima)
|
||||
- Grid: 1 kolom op mobiel, 2 op tablet, 3 op desktop
|
||||
- Per persoon: foto-placeholder, naam, rol, korte bio
|
||||
- Stijl: matched onze hero-section (zelfde gradients en typografie)
|
||||
- Header bovenaan met titel "Het team"
|
||||
```
|
||||
@@ -0,0 +1,21 @@
|
||||
---
|
||||
description: Algemene project-conventies
|
||||
alwaysApply: true
|
||||
---
|
||||
|
||||
# Code Style
|
||||
- Gebruik TypeScript strict mode
|
||||
- Geen any-types; geef expliciete return types op functies
|
||||
- Tailwind voor styling — geen CSS modules
|
||||
- Server components by default; "use client" alleen waar nodig
|
||||
- Imports: gebruik @/ alias voor src/
|
||||
|
||||
# Bestandsstructuur
|
||||
- Componenten in src/components/<naam>/index.tsx
|
||||
- Page-specifieke componenten in app/<route>/_components/
|
||||
- Hooks in src/hooks/<naam>.ts
|
||||
|
||||
# Algemeen
|
||||
- Geen console.log in committed code
|
||||
- Errors: gebruik next/error of toast (geen alert)
|
||||
- Async: prefer await over .then chains
|
||||
Binary file not shown.
Binary file not shown.
Reference in New Issue
Block a user