fix: add les 8

This commit is contained in:
2026-04-07 16:58:45 +02:00
parent f65c24ffcd
commit d5599a601c
12 changed files with 815 additions and 2272 deletions

View File

@@ -1,207 +0,0 @@
# Les 8 — Docenttekst
## Van In-Memory naar Supabase
---
## Lesoverzicht
| Gegeven | Details |
|---------|---------|
| **Les** | 8 van 18 |
| **Onderwerp** | Supabase koppelen aan Next.js |
| **Duur** | 3 uur (09:00 12:00) |
| **Voorbereiding** | Werkend QuickPoll project, Supabase project met polls/options tabellen |
| **Benodigdheden** | Laptop, Cursor/VS Code, browser, Supabase account |
| **Lesmateriaal** | Lesopdracht PDF (studenten werken hier zelfstandig doorheen) |
## Leerdoelen
Na deze les kunnen studenten:
1. De Supabase JavaScript client installeren en configureren
2. Environment variables gebruiken voor API keys
3. Data ophalen via Supabase queries (select met relaties, eq, single)
4. Het verschil uitleggen tussen Server Components en Client Components
5. Een formulier bouwen dat data INSERT in Supabase
---
## Aanpak
Studenten krijgen een **Lesopdracht PDF** met alle component-code (volledige UI). Ze hoeven alleen de **Supabase queries** zelf te schrijven (gemarkeerd als TODO-blokken). De docent legt concepten uit met slides, doet een korte demo, en loopt daarna rond.
---
## Lesplanning
### 09:0009:15 | Welkom & Uitleg aanpak (15 min)
📌 Slide 1, 2, 3
**Wat te zeggen:**
- "Vorige week: werkende polling app met in-memory data."
- "Vandaag koppelen we Supabase: onze database-as-a-service."
- "Jullie werken vandaag **zelfstandig** met een PDF. Alle UI-code staat erin. Jullie schrijven de Supabase queries."
- "Ik leg eerst de concepten uit, dan gaan jullie aan de slag."
**Check:**
- Iedereen heeft Supabase account met polls en options tabellen
- Iedereen heeft QuickPoll project lokaal draaiend
- Deel de Lesopdracht PDF uit (digitaal)
---
### 09:1509:45 | Uitleg concepten (30 min)
📌 Slide 4, 5, 6
#### 09:15 | Slide 4: Van Array naar Database
**Zeg:**
"Tot nu toe stond jullie data in een array. Dat werkt, maar is weg zodra je de server herstart. Supabase geeft ons een echte PostgreSQL database."
Toon het verschil:
```
// OUD: in-memory
const polls = [{ question: "...", votes: [0, 0] }]
// NIEUW: Supabase
supabase.from("polls").select("*, options(*)")
```
#### 09:25 | Slide 5: Supabase queries
**Toon de vier belangrijkste operaties:**
1. `.from("polls").select("*, options(*)")` → Haal alles op met relaties
2. `.eq("id", 5).single()` → Filter op 1 record
3. `.insert({ question })` → Nieuw record toevoegen
4. `.rpc("vote_option", { option_id })` → Database functie aanroepen
**Demo:** Open Supabase dashboard, toon Table Editor met polls en options tabel. Laat de relatie zien (foreign key).
#### 09:35 | Slide 6: Server vs Client
**Zeg:**
"Belangrijk patroon: Server Components zijn `async` — die halen data op. Client Components hebben `'use client'` — die zijn interactief (forms, klikken). In de PDF zien jullie dit terug."
---
### 09:4510:15 | Deel 1: Setup + Queries (30 min, zelfstandig)
📌 Slide 5 (blijft staan als referentie)
**Zeg:**
"Open de Lesopdracht PDF. Werk Deel 1, 2 en 3 door. Dat is de setup, queries schrijven, en componenten kopiëren. Na de pauze doen we Deel 4: de /create pagina."
**Studenten doen nu:**
- Deel 1 (PDF): npm install, .env, supabase client, types
- Deel 2 (PDF): lib/data.ts — TODO blokken invullen (getPolls, getPollById, votePoll)
- Deel 3 (PDF): Componenten kopiëren (page.tsx, PollItem, VoteForm, poll/[id])
**Jij loopt rond. Veelvoorkomende issues:**
| Probleem | Oplossing |
|----------|-----------|
| npm install failed | Check internet, node_modules verwijderen en opnieuw |
| Env vars undefined | NEXT_PUBLIC_ prefix? Dev server herstart? |
| getPolls() returns [] | Query syntax checken. Staat er data in Supabase? |
| TypeScript errors | Import vergeten? Types kloppen met database? |
| "RLS policy violation" | RLS uitschakelen of SELECT policy toevoegen |
**Check-in (10:00):**
"Wie heeft de homepage al werkend met Supabase data? Steek je hand op."
→ Als minder dan de helft: kort voordoen op beamer.
→ Als meer dan de helft: doorgaan, help de rest individueel.
---
### 10:1510:30 | PAUZE (15 min)
📌 Slide 7
---
### 10:3010:45 | Uitleg INSERT + /create (15 min)
📌 Slide 8
**Zeg:**
"Nu gaan jullie een /create pagina bouwen. Het formulier staat al in de PDF — jullie schrijven alleen de INSERT logica."
**Toon op beamer:**
```typescript
// 1. Insert poll
const { data: poll } = await supabase
.from("polls")
.insert({ question: "Mijn vraag" })
.select()
.single();
// 2. Insert options met poll.id
await supabase.from("options").insert([
{ poll_id: poll.id, text: "Optie A", votes: 0 },
{ poll_id: poll.id, text: "Optie B", votes: 0 },
]);
```
**Zeg:**
- "Eerst insert je de poll → je krijgt het id terug"
- "Dan insert je de options met dat poll_id"
- "En dan redirect je naar de homepage"
**RLS policy:**
"Voordat het werkt: voeg INSERT policies toe. Staat in Deel 4, Stap 4.1 van de PDF."
---
### 10:4511:30 | Deel 2: /create pagina (45 min, zelfstandig)
**Studenten doen nu:**
- Deel 4 (PDF): RLS policy toevoegen, handleSubmit implementeren
- Testen: poll aanmaken → verschijnt op homepage
**Jij loopt rond. Veelvoorkomende issues:**
| Probleem | Oplossing |
|----------|-----------|
| "RLS policy violation" bij INSERT | SQL policy uitgevoerd in dashboard? |
| poll is undefined na insert | .select().single() vergeten? |
| Opties worden niet opgeslagen | poll.id doorgeven aan options insert? |
| Form refresht de pagina | e.preventDefault() in handleSubmit? |
| Redirect werkt niet | useRouter van "next/navigation"? |
**Check-in (11:15):**
"Wie heeft succesvol een poll aangemaakt? Open Supabase dashboard en toon dat ie erin staat."
→ Toon op beamer als demo.
---
### 11:3011:45 | Vragen & Reflectie (15 min)
**Mogelijke vragen:**
**V: Waarom async/await?**
A: Supabase is over het netwerk. We moeten wachten op antwoord.
**V: Wat is het verschil tussen Server en Client Component?**
A: Server = async, data fetching, geen interactiviteit. Client = 'use client', useState, onClick.
**V: Kan ik realtime updates zien?**
A: Later! Supabase heeft realtime subscriptions.
---
### 11:4512:00 | Huiswerk & Afsluiting (15 min)
📌 Slide 9, 10
**Huiswerk:**
1. /create pagina afmaken (als niet klaar)
2. Validatie: vraag niet leeg, min 2 opties, foutmeldingen
3. Extra: delete functionaliteit, styling
**Slide 10: Afsluiting**
"Volgende les: Supabase Auth. Inloggen, registreren, en bepalen wie wat mag. Tot dan!"
---
## Tips voor docenten
1. **Niet te veel voordoen.** De PDF is self-contained. Studenten leren meer door zelf te doen.
2. **Loop ronde, spot problemen vroeg.** De eerste 10 minuten na "ga aan de slag" zijn cruciaal.
3. **Check-ins doen.** Vraag om handopsteken. Als <50% het heeft: kort voordoen.
4. **Toon Supabase dashboard.** "Zie je? De data staat echt in de database!"
5. **Authenticatie is volgende les.** Zeg het af en toe, zodat ze weten dat RLS nog tijdelijk is.

View File

@@ -1,346 +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 5 0 R /F4 6 0 R /F5 8 0 R /F6 19 0 R
>>
endobj
2 0 obj
<<
/BaseFont /Helvetica /Encoding /WinAnsiEncoding /Name /F1 /Subtype /Type1 /Type /Font
>>
endobj
3 0 obj
<<
/BaseFont /Helvetica-Bold /Encoding /WinAnsiEncoding /Name /F2 /Subtype /Type1 /Type /Font
>>
endobj
4 0 obj
<<
/Contents 25 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 24 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
5 0 obj
<<
/BaseFont /Courier /Encoding /WinAnsiEncoding /Name /F3 /Subtype /Type1 /Type /Font
>>
endobj
6 0 obj
<<
/BaseFont /Helvetica-Oblique /Encoding /WinAnsiEncoding /Name /F4 /Subtype /Type1 /Type /Font
>>
endobj
7 0 obj
<<
/Contents 26 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 24 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
8 0 obj
<<
/BaseFont /Courier-Bold /Encoding /WinAnsiEncoding /Name /F5 /Subtype /Type1 /Type /Font
>>
endobj
9 0 obj
<<
/Contents 27 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 24 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
10 0 obj
<<
/Contents 28 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 24 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
11 0 obj
<<
/Contents 29 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 24 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
12 0 obj
<<
/Contents 30 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 24 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
13 0 obj
<<
/Contents 31 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 24 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
14 0 obj
<<
/Contents 32 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 24 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
15 0 obj
<<
/Contents 33 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 24 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
16 0 obj
<<
/Contents 34 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 24 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
17 0 obj
<<
/Contents 35 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 24 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
18 0 obj
<<
/Contents 36 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 24 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
19 0 obj
<<
/BaseFont /ZapfDingbats /Name /F6 /Subtype /Type1 /Type /Font
>>
endobj
20 0 obj
<<
/Contents 37 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 24 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
21 0 obj
<<
/Contents 38 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 24 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
22 0 obj
<<
/PageMode /UseNone /Pages 24 0 R /Type /Catalog
>>
endobj
23 0 obj
<<
/Author (\(anonymous\)) /CreationDate (D:20260331170259+02'00') /Creator (\(unspecified\)) /Keywords () /ModDate (D:20260331170259+02'00') /Producer (ReportLab PDF Library - \(opensource\))
/Subject (\(unspecified\)) /Title (\(anonymous\)) /Trapped /False
>>
endobj
24 0 obj
<<
/Count 14 /Kids [ 4 0 R 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 20 0 R 21 0 R ] /Type /Pages
>>
endobj
25 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 952
>>
stream
Gatm:9iKe#&A@sBCi?#<#YVI>WA?]aThlS0Rc;a'(LrB]`3C?'T``'D/(lXP#/c#QbAqd6]C5.9-59^4;Yq=b!5=2,GX?s:#k*:i`.RO\^4jk\Trk6=Lp*2_L%5KaJ\Z]Zj@'<q3esPaTpf%?FYF3j&s"d!U<2]RXN]b$gaC6E$6?td7R&Q0ces.)eZ8JZ-o/;@/:aKO\Bqh0dTM#!i=*Sa0](ZAPtqUA':_TcLPt>;A&m;kcN6lBiR6hF!E%W7?@_#n=;Z:K6"[AU7YG`WCp@7/(!$R[/rMKaUA2Ij&8Z^.Jq-bYo0<dP&5mW\Lk3-:q%S"U],3R3XG7qf80GSU_0)![=[)hY(Ie!&Gna7t[4\6/BVdd-gKTD@#U'I[q(N5W3eug?Xc/`Y.1M)@Z>DUeVI1o%D?0<N#G]&FRR<gA1i-i\<?7@o:_Zp98oR3M#`HP6kAuut^lPcp_k48T>U8%u7lBK;U(#J_C$7+=!D9^kB6Q3[l=bb1>$%\0OcCO%a*dD859?t\P27fU$<a(H#OXb0hbpIg.r)XWA=0dFV[q8lL/=Yl9qCM=\oK@NK/"f5O%UY.ck7Br4B2%7^T1s<D;=1E]%VAQD.jD@Z/8Qb#].kO,$s`/j>M0/\p7/=3n$*:K#>+H;6?ONVJ.+=%1TcoE-.,G:C:Cr)c.K&pCt#*liE7)N5Ecu9=W6(q-<)A#CAh8D!#M=l#]qkI,E#6n7Ea+4FtU8NT\'6/Ak5*@*>R;l+#'=3W8.9SiK1g!t'4T^J>joi1H3C2W_gX\FPIjY%R/X\?^j>oaQ/T0mL!B^q5SWH$oeO)8fuuA2,P&2cpLg1LN?/*-Oe4[h>Qe1W24()Q)9uX!pp`IC;7$ZYq62SYA_qfXL-r045$/P%pBCkud++ISi:hbG]2rZOIF>Dj+1g*UcR'_ZFsukPUOhm[f^=U":[0$TS.eIO$@u~>endstream
endobj
26 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 1267
>>
stream
Gau0CD0+Dj&H9tYf^ojA*6.Og2OW?+d\FKLF%KP+HT/tS,E*7/Cs/(I8__@:^Qj`a&1G7tJf2aAYG>fTT1:CD0_$@`*[Um-4_VPU5V2,)!HAMQHDf-Se\9PSgsWM;0<bKZ/[o;Q^^I@gIPrb'C,mm`[VslN"\)n#+,K5$N_,P6kp8<C"dY`Hh+saD)e1Ua2KY@WB$daE1gqgY9YuU@/=$24IHXCupKD#$JCc,1[]c;;T$7J:ZS4Y[`'j5#f%Y>_<XG.<3[VQFgm'\u@M%a-<XV[N>gbTD9L*fjd36cfDS2Q:ScHe[i*_FuL?!#.k8B"i_E]l=6n+*=8n!\+q_4#HRgU:XO9n*ZT`h`lEPsksA@hn4cX+WT#:d9ghL9iR1L.Gl)1cG/HjA5,oHApCYPsRN)Er<Hb6PlAppH^&Wq/,DUYWDq=-$FOW9A`"-0VX0!rSM5;p[';)O%gcX3QU'G!Y$YFqkV75,X/<`*C1NR>,2XP"V@"S![(S;/X2<9HZlW[)P$R<@Jll)<=K8R+SG>dL;_:_H&t]:-+:9:Jl9JAlM0?(paQHF+%s5csrpgr-[GgG0!M[K@1^c;eiDf^2C/sB#Hg-nbc$?;B7/[(!fAY"Qp-"QGB$S(!,dRh@#J#0pftp&es=@'J=X97-#Enk,e/eDfB]AI!Y?qd(R(O3^2hP#+$E-&'%R[H+Vb"nEFe"m^Q-"h?^ZPJI99V+cajncN`i"Ii[hR2!&H3L4*ot,:4m.ceai\;A>scV)5VR$L47XFJP(T:8ns8;5h^)CFQ80*tL@H,@)I'P[<PB1E9\j(iVr1;$:d3ag^K&V/91:WNlJt/D%8#>?Q-CXG%I!*20re6ZH;GbA&MeL5WIeOSL53<KN8OOr':Bmr+DNB0S[G;3Gpp_fl@iUQW=<Bn1Q?&j1_H)p?4$K8"ESI+Xu2,32j9TX9?hPnEod#ItQm0,ml&aRWRhA_XT)pX>=QQSREE(nn]oYWIEEf6V>W3sF_m?aD6$_fhP_,Pkt0_G!(d\l;4h\@SoV9hbT,Z'VW@$'hN$X"C5!b#IX?=-*GsR4fffU>Bf=,Jb%H0P>Bii\p=aL(*_"nG>-'7C_-Mdq$P<_)@K1;jPU_j/SSAJ]mUb_a5cH!L5`@H)='TgMj;;rr-QPnp[>b*ulcAr%FBlN6.`ESRcrVI@5)snsr/H0KAeA.-Rg1d.HiVL*n\dcXA5"_[7(]*cG?+6HNbPlhXq+<qXju@JRk5fIu`Si]2r*8C])9<jU@M.b+G$g2VTX~>endstream
endobj
27 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 1525
>>
stream
Gb!;d?$#!`'Re<2\BP]/CkO*adn\XhdrrL'RjLg98nZ2s(JnRW,0SZF8cF(?CdW^^$"UBd1h8:B=45VZ8&n\MYis',pNR*cgkGH?*4UOe!4]odke;gZ]6dV+]SZ7`o0O%0n6M[VcjQfp2)F1thf45#JRsT<=D40g:r>F7P:oVY*Rb](6oB[DIh/Xp`"YTE:*L]E)F[$W$/rdI3JmT0JT+V`P#r\hpB%p]-D<Zgoh\P60r_";H"a);Y*lLt,<#o_DG`Orii7a[qP/^P";_?o]I)mS:Fh_/G!`/`>r59^iE@p%<Q$+.0<R$>E.X1<$,SRUPmBbDX&<8=OP/"ua17X*Uf90&(qc+M:j3oCffE?<!3cnDYj,!gJHl9>0:2tUDemt>N1QF,`k=t_6,hThN[>kcYN^A":F)Zd6u(pqTqa/a3a^3Q(XcHTe(L%k^JJ3+fanF.R>hJsTW_S)2b"7%Q4R7]U@]&W4McopmY?u:cqj8V9?^e6;qqO2-F<"n9elN_K8&"hZ\7_J-cY`>!CTTmR[jS^f<aB/Qj]P[=Qin:i5"WCLs+^)0`Oo+Z@KN;@t?s+5-3mPh@qROAl'7#au0Cb`oAbrnK_0SDP3B=o*tsS+2^9XcYi-"?Qi=HGTM@'Vs->;kI-sWY\4([L!C./7:\<@/>#SdY2-1Blk$QrUZf+3#ISAT7o9U*Ym;#l!;U/XN9Ja"iCX7=\?__m(&hE'*u36UL-_6H>36\R@aK[)U-f\u/\:UBI5/urs&4FH&T40E`:4WmU*P-<$ToBl-9W-SJV"OJe.B28FQcmk-cCWSE,2R`r>!/[Etgt/H]Cl4+NLm'$t`@/DHNUZDHGT%"6[).!n&,ro%#7X(+tMQ7'b26,,R)*(8iJ3p]*cp7M,jJ!dZGPBr!'K%bP7c;V0MUdm[p6enh6<ZQ4@orG(2;)Pk._@nh.W?d0q^[17Nge@%3L1&2>0Q)bD*<*&'/ZK`seiY:ZUA^F8`fV9kT(lQZj_AFHOhTd;ekLd1c+o4q-GI&A,G![d<f%gO'&^F$2b45*uZ=5;r'%9=)id<QRhk?an&BLXL=ZPoFHH][QQ@dP,$]D8AnU_NDX'Pm$i.$a*1rL1DN9*=a--e=I<)At)A41Q/<L:d;/&->;:GBGU+IVT2U+&[+%1,Np0T`92HAus/a`<_7B#><53T#Tj&c4#78",aAYUEF"On:\)C@#M,nIl]12#AKLaV/k7js+."`Q[L+f4_VA^%7b0*#hMp1X=&C2b;""rl=/B%mdl:hE:$erL=pp;7;2QE&W#%mg7Qsin7[K4_HcMN5NjiS$URN)ORaL+"Fl[69tYje)Bq%`JU,&A0qNOO#>YP.Q<@cHo0,5irJMU038?Tg+`2U?"d5U+DJP/8ajMK\DYTmTtg<Z)#C+Xr_&1bGf"ZUGpGX9XYUQXS5rPI[?$7_%`V44n'WU^5=(VGn;u_L:Ia5MV"LP4DPqb"V5(]M*S[;4S^]JA@53"bn2W.kg:=$Ns'13=W/TF-(QHc3bJQ+%r<K2YL!p~>endstream
endobj
28 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 1260
>>
stream
Gatm:D0+E#&H9tYf\oIk@L5d8+d(4;,Yab'4cW`&rDNLNO;:YDl'0Z=i*lEL\N=,o5V3oW_s-9+pN^q#?O1c7Qg"Gl!Ve@#p2:(i%AP?`a)YR0%A-+LD[j#.l.VHhgHL1;YAY$1oqe1+D:u)JGC!Y>]h[1g_=G5'"+8[]3m!MP;'J`e?u$G@ru-i'&S(#7i$1lMQ=;<tKd$o(>[&oY$DY';`*`(^$4&']+WYhqTXn0T&:]=#OsQQ5:F!D!i%'Dd9VI7Fg_C]tclD#9-/o7EL'h+?&0)ji1nA&EErcZg6VK*,P@Y=MhdJHG)C!L]go*Y/ZtdtQCp"JSP8dcYgMa%:gYN*a\=0k(C#95c6.[;7[d?9*/8Jm?L6foI0K/?\59N/nBA@eO^$G<N^m,Hj$Gja<#PXjG02I?37$L&B!(q0FgR<X.1hclU#2=npD$Ero6Ps[Xb-F%(Z%)trO1H)`&li4iVqH7ja)!?R=%NDBp$^cW<Y#rTbBH$rc[k@26^<8fWLr5D/5M.Sm<*q(I!D\$QQt2L8Ti1Bp;WG)nC3*jG@i*@_?I#KL>nN<E=QcOEgAu^QZa?Cg<)q1pR8BO]%d28;0p"5Qf:oaXm,e@D+`h1!gC=?I@fNeh7Fmd/W*1Q)WHts1+kVZ0J1^<6nF&</AYP9(<=G)"fm9l:$d$r^f$SLRH?:.*QC6.-!;s*(?R"PT#u\7:4%9^BsWK?!sC@'o,F.VYdlHc73"Z]Qfc/lQh"m*dR#Y/9XhQpppm!qPLemWfU.aD0Y>cSPbl6eYW.;H;R'_Z/tt%gToM]N;V%G'b7WtP1$:M:2VS7&=f\#Fd8l*6L#XF2cu_>iQnt/UK0n*cH1Xq^Kh9L?qb*G`?J`LmKZ.E\YmW\C<j'!g_3G+gB/DmFd<&]Un4G'eQ-`4eLnS]sNX`J_CZiUAM=$5>4['?5"tL&43dWfpCM:I$a)PnM>$hn9)2;dTW@rI#QAfsG0&r&FDYWq&iLd'sWqY+q6s_X$3%%5"O)^s6ocB%Zcl)C>?HWNQ'?]nb^"@D7(U)_S*Tq5l-N4C!nXDj?Q@Yq3VVIi9f6(0a-.C-0d)s=;L3^2U7WMbq;cuT`IJT89/3i#m<AG'0^Fh0:305q"1-toRT"qf:Es6mj\_p=GX!u%Ff6jeKrU55KjjlZWf=@Sb5Aog-1H&:F+MiA#G1P,@LgYhSs"K"='[bHJMqI#+('3!gX(*A-Ot/PrMW:]fT><V^d!AM8Y+S^k]KA4I*)[+^hZ5kXLW]~>endstream
endobj
29 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 791
>>
stream
Gb!#[gMWKG&:N^l7TM%mQ53Rr*(]BpMU_@=Wg!k`aUcCq(n-.)bMnGAs1O%<<UB@@`$DcV4rDp8B?h")5f(BuY()'<i5PlZ$O(9r(BLpd)n[>Ap9]1+DXZprHJc,8$GB*4;n)5T34u=SY%.0135k8m2>mU=RdTmo9?2cSPZ'.9imbU->j)Y-"5C]a$uWZ1/^B<<k\l_Lf>[!eFKKM<>'o2W-R-p:M/)6!PZ%Rc$Z5*Xp[^'-E<BW)K7Nhfn]ak_TJ[<:A.k[dZ]Km&'>(Z_ms5HCSb8$`Z1X2FZ%XA0%["gH.9N$4":lZH*(RCI<A$8_mn6][6tE>CZl?8m]E&6a(V(VAZ6WK"!K4T#\'S&<+14W+c/2A'#$W.INb@($!9k3YJV!nNTmZi,-s",@Qq]C(_^F)jVpEqQ`WLOG\-peCi<md>:$[m7Pqk`'f*u\g3Q!,SK0-L]<82)Mn3u%.;MZWW#1_A)e'!!FaYK)7?8\%Za$*0/B+%6E9+)B:9JQ@/P4i>Lk6+F`.17:j/s=\`M8bFq.tP)T;+oo9bnZ`1(Q_I\9Z^91=;9"]/fi8_e@0A4(gke#R1U6V&gq,/ZI><='Pl+F#qm1\T9#lT3D-$/F&:<2MPRCn`94dUSluN0jD7Xm0Mb"M?bmT&@A$Zia09m=P5Hse:7dc`[L"b[>dRnNPs&kIMtC9*Cu#>o/542#k#hW.XgIFUO4q#Wp-$sD^2#UkS:#aEH@\k"g0:KONj%jW?\raOBqShQS2fella_I.IGA.R)i])qTDH%,JbBJt6)]/~>endstream
endobj
30 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 1301
>>
stream
Gau0CgN):C&:N^l3dt5:#m,sQ?pcF*Fe!L&<oRX]?7\;L;?J[`U`GH1'ZIhb7i):B(TgC=MEt.@qjbApNr]^d(s/s;#Ikd@rS@OlK]*c,QJR5P(\hiLq,T4*ac<?EH=kKRDTC+`?Dg<^d:;!Gd"MLUX-cjWVm/*HMW\Ne"J_0JY`o1UX(3A1od8=gf``ajLB0mtU\_`V`;;gDFW%lEXaTn+\db9Q!P;Nhi<p&Y.ZAl)k86t#\V"r6N6tR;N4-Tbco)T8do8TEo4.k4I)[F*E$Sn2L*D4?id?bkLHC0=*p?Kb7fgg=O7(lab^5bo1dFT>-Z2IKC('%s)#1hIAX!N])Cu!Lb+,EM(n[H,0LVMDbOlFd0m=nF;]NW3pHn!J"8_*H#tE3>hu`]bQ[NF'p0qe0X@BN$L[3j4=7eC%.seDJkZTPt>UeeTC6n_)XL<#oCX?sf+:rET-AM8hcu*qsSI1%<#,2Ei\oBnH+6!RB8b1h-6;0+,*99tAKS24E"=9@%fYCSlF1;h'*-q-i.pe%orniV4>5,^-h.;`iQ2`Y)?GLc\jXS2^7l1tD'#g4u#^IIUMBIjc?"cnf+^]ghD:Ho(5<\JfhX'RJf*$th@i"qs9)sR.>I(XoAEmROhZmOj:IB*b"URtR^g)prIY%jeQh:;6%(7dr[5%7t[&]fOkY[@_'.jQBHU5\D4]B@++-d12^nMQfW#MQ)*o&sKGWnG[Zj,bf^RXGq^D-$N9oA]cTjt=$fPrU&'W*1OekhTNNifZTDbQ*FIHTU2JPG.cn)UeMIU?B<roS#j'<Xo=P\2IB`e)+SkuC9Nc_BV62TNXn`)g*2#Xi"b1`A;U$G=?/GDsl+"Y#I_:oe?pC"q,m_t@tM]lr*1kP/[pCon\sbd?[fG$.<Y23usrB"j&i3bo?(S;"?EA8/Y[87b)Q+>A\LU^f9"eKD3!HZ$sd/&)4WV?CNi=KB-Bi)+TXS0]&^X&h7KH4_+lkRQ0H$ba-*SK(b_`\D=6UIf=`a/H9cP%%Ng4c.e!]>'"Vg<bJ>]BI[2ZTupa;03b:s4;A\idO-=i5o!$rN;kJ;'3C?l4jAg2j(H[hLX@_Q;68k-FJmB#5@t>Wr;:kBQUi;4or%=Od:]04f!M5roCW9ooMmE*<8\SC:2oKW0JRL#O_4HTRB6p=^@OTOu]$*(dL8P+$nif)$KTma6e-%^PoF5.>V%%qD3<(F)o2WhCrAVLYW8/2:UEa*o(f-EUJggm#_t;dBT,Fnm;!RXVjlV=6FDY9phru_ZBXB#YkWBm3o&$5>po[>?DIPW;Hm`ERh/~>endstream
endobj
31 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 675
>>
stream
GatU0?#Q2d'Rf.GSG%e=_I,n_ABQH6>,FpB['"(<]K!_6XFM`Z9<*mL4p+NFUhSF0!cbES?g%:>LVcq"D83Cc(^D`B'\#T+#OR^=JfB(DZj2RSHb)+TIG:Y:3'O%Ml`3!V-D'[/NfQR'j/YWR=D>]tX\5jY6Wl%K*/<(XI;_A5'aB$CU?!.uBX]ItJjd'rD25-56.R6m;-4i`c&Xjj[1jPU6C&!%\:BS\JmVaPKS#I?X&'>3d2OZoL8\XNj9sJ9>Mh>"m,@3tC-U<DjHW+VdM*q1'h]ef>^fc?n`+a+lEA1o$\b;6f=;=_%Ll"(YkM=7f__m1*AJp_K_ei(kUkTe+r>8DQFXlES,C($I]I5*jg*,#hM+7<,"d;oQEmOe`\K_=?Q&I3g;o5TNKoVpaOUQ"8qFiucb"^'mn8/;=A3pS8\ULF]hU9u\MU0-<TU*UeN#U#bm;D.P#jCNH$oa51T%'os&2HP&S2/D1jH#tL[TLg_u-\N)e<]6s+oDk:u*`P9sMiXHbj&EMqr(VZQ+,Oqqfmi;30hLp>l:mFaT[i+L^04rJ"L!\3gi4FW0Br-VC-c^U5&U#AQ@g//WEI9#OUC6dn`JN2<dU(<<^3K'#*q86Nn&/)3^fAN\"jQhU%XT4:'sa5+%/;c`E_IsT&5GUcf5SK5'Gm16b^;ul~>endstream
endobj
32 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 1259
>>
stream
Gau0BD/\E'&H9tYR.:PYabDYWCoUN.Enh[P)LC>AC.hPYG[_GIc)oTk<L!O?=hjKI8P4&@LbQ,IB:aN`SnH<A73A+Lcdr.u*aHA<^b,'DJD89Pmfiu&m8Ju\O^3lf?-D?Vlgu$<].U4\a3<gP4hCftmBkTM"jChQ5>XY*Eq6n!hO7&=?tCY.pC/t9,)0M"iF8rdBS;?NA0bOuVd#+Ki%;cl@pj\[cVm[85*U\Ggbf7\a-=\h`_BUU>85CJ];biL[j`k)6a@;$KTV>R'=^4K7O>.<&8I1198!_CfhS-J&P47hY_g7OJQ3V&&0b;9<O%H&Jd-k:#ko$g^'kR%)OB,o,[(H_$]-@1nqt8-EXBj8U#bd)o$Ier6F\B,7a+8P0YEqb`ZBeQ+Qn?I5Z]K9Aa"FqT\FUh!,Co-A0"QbQ*'0(CjMVQ[1b,M?o&9kbNcq=An&iDo=mQ2GTUF78q4J?efLD9m-h+qX5HV[ZS5^f&=)*l\LcuU0\R)a`so0QI??'mYu!W3$^6Vs&kX"Aj":G1TO"Rt\Z+*WJ)7Es>5JbJ>U(^X^5h+&=9l8lk&srla+iV)2(4&ta8F[eLH2`#2!of[\9a>R5,?VA;`_gnQ*]-Ja4tp-cL^XJreeGHa0EbjOR4jYp%<QKkmcRkdEV1W(T:\q@D%:g3hlj$UVq6sNq,X3h5\k>+f'=WED^9@cl7Olib3PrkFqVNY!3>t^P?MjWt_JVYH+R(`7%QCNL"SVYJD!t%cJ0^^C]nXUX;.?NHT3Ga4E[q;F:h3g;$p\3"+Ui&]b*^>l#;[jXM&q\PeML?\f$4E.a&G$cpnXB%H%&Q`4;]i%oQR5IV4T7p*.g;i40[7\XgP1&[V#:I)l\Y<Z[^W(LL]L-o96[&B6B!Qh9\OqUf4g&1kJ\[ha!auf#A<.8ffSA?nOINqJEHpn9<!7D+3.LcaMZcSf&"uJH(7r$Gt_Tr4I,-a;J2^#P\jsV['g9&N2[_/h+'sFNpH>X)efoP-58PU38AfMGY^HU.sO@q?8,t(Rb`toL=(uh&$^D%le+@Hl`A1,<P!QTusdSQ0k<9TR4Wu8SlI.^X@<RF=?%Z4"(XP?.k.;75?K5a@Y8O[>Rdq6#Phk#K!OCA#RqY_&f):eO]'/q..K5asX,GAmHqD8nIB<TU%oj"*G:U2dWjhAa[\G`4q<P2"p1h_Rkl$Z1a)C*L7YMX\TV9f54/pm!/';/.lRE)=pD,rMT^mRgVb.K'g_kT9DrMl@-^'M-]!L.]p,6~>endstream
endobj
33 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 1341
>>
stream
Gau0BD0+Gi%0#[%Ja,n!?:L?g,YRR<gY*u[Dsbt"rYF[?ZqWVSoi@5rd`^ae(SpsQ3OY])gGp'J'_)0J!!/b>_&kQ7-OISX+kW\,VuspT:m\[Gr!4[ff!cE#hXhiJLJCC6k>O]75=@MZ9kXbR-u?(rhEjoI<eJ0oWc3Lb7R`sbZnE-:_'NA(3DL)eT%appRGrD8k+!iFG6Gf&(r"FqI)4\mF.@-eS=31kC8KG/Wf2.$X57bLZp-;='oYmtWk%$DB@[9m%?.+drlXefErfE)j[(FZRrXm,;bl7HY$dQPUkg8.&4nuZnsJI:nba`FJOL0OQ+LlKj3.(W.G"hCo8@(J7-]mD;>R!f?'GuJl1pdNSJN9\]=rgS%Dnde%:OTHJ);FkgpTc;GC"fJgFJ"CKjOo[k6;t6:^gUD"+nF[=%70];Jt)R3#sQJ$s"/GI;h/L0+3X7,,;,`Bft"M)u?9Sc\-ms^`/YNMSfP?7$`@WEL\HCp%j"%l?QDY%+"3Vb3tRcZ:-T_.a$21;R4BsR)t>>hsEBAgT0@lN03.E7W-@]%)WO\,+WC\G\cm9Eu)fg0uZWLd1P/P[$>p@=sdB%'jQ2<\C@h)+M%?SI(4dEg?2>fZ:7?fT7G%(/LM"M=h1_g>[#]5=Ou$O:iXLP;*$X4+3T'QC;s`)T>ohVAAY"0khWRn&nMmbRuhAO[j&(MOA2W!"Xn.q0IYiX[SL7YfeA5`&OP'uDR$ZPJ!!OSa,12JoL;qqmeW"\ad=;k$PjPk%oN]B^cmF/F8Ib1Q]3H/>rmfX<kQ*!)jL3)OhW_rlf]7F)p25RhI%AFM2);R$_Cme.e,AgItF_7L*0-$8BfPn\qXS8<s*CQ:iKEjoI#$(-;e,NL<QC%r#"/f*(*\J.tSRi!>r"18'P$29?O<;$n5i[HTGl2nX/1r\r`+Sm&;??C]OKDSo^&&Xteh!<I/c.eOP0o(;<XMM;h@6HR*9>?+%Bem,LB-GA2!KVQhf654H0L3_n#oW,Brrq_Pql4XY>keU?1lG'7Jt48/*f9YI[eRD!`U%Va75HU,0Ff0KtrEI/]Z1nFH(X0rJTZZg&1pn0"-"F8QSe&A>JIe`g"jF#U=2;<"p&'Uh@;/%L?2W,Q:/Z(U3H6Y"<Cs:qZ?tBQ""'03g2d"+.RTB5l,)HE<TJ7S$Ab["AL'FA"$-\(N[\hq,_N;i`2eq6fC-$m#kK$:;o8-%`+u+parf#W^DD7SE]Y3HkYE7!YXJHNS`+bp?,9,\A&^(phRh'4m8J3YPSuS4m;>lNA6OVg3?5Dr.LQ:d`2j`&5&mj]LM5H9+:/-FtR*YSK!0C#F)b_R]qn?'9T_/EHb,<5~>endstream
endobj
34 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 466
>>
stream
GasbU?V;n(&B3Q$;]L1"7FNO,?[WU#^=3C1*u?2;>$'G&hVHZg(Vfj</g=INP_:$DABmm2-bsGQiI"l2(F:DX"GB0'LJU[<SDgb)rYFMRV3/uHkln0X9R,SHDn'C[)eZ$U>[!s-DQbIufj8nT]cqTHlD6nX-lSVem8ct<kIcF)kTVX>M];5]-H'P(=tkqV27,38jttjO0IL(5NNuqjV)_[4Xt81rm"+^E,E,L6m#fBmf:bVH6AP4&&g)1EL4<mcEd;"VSKPt^I"Kt7,K2\"LTEjJ_^<kNrONFKToX$HV,]B68o:_0diVS"<]>HnE]%<b<T?G`%k`?uku[-Y?!?FDZ$+qf;u$_77U"3C](LYM/4+$)*3pj"2oF-p!&dl-!R(f_n\_LBB&^'EQoEe]KjFY:[[-f(9Db:RoTQ`Ae;O*qLX>I!Su(pU2iDE-&'I.;j7@[DI6ZU,%#Y'`0Y$`~>endstream
endobj
35 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 1122
>>
stream
Gb"/(>u0HF'Rf.GgmF'ef9T/\V3o4,pFFoS\h7OR!tQ/.5]te[dj\[\s*\P6.MS"6D1RHO`o@'d4nn9t$R3s9"3&$%*l)J/.c1d[*SPp,L2'!N%)0/CG9rKfe'qC(Tf3P4c'jM.Aj]fN?<318%L<i4%8,WOkfF6b[O4c4J?"G4BqX8@Fo[8#KhcoHfnbeWk5[QS?3c41`-h@-\iPB8%C?WlRnN9R`lKjj^rT%`$)^YEW"q2cic9!*(h*u'b.NP?R&TS="tmn(*:YNO*VgM+U$XoE\"^#QT']$sO1Y13%CVQJCXl=k<l<@c%9UR+hb.\n`[/$$C3@^M724VRTNLd*Y\L0a/d&FtMR^Le@;Ip`XB@pMG/@[UiA9G9'eK/Ks&k_$iIWi'hE4,nP"n#$8gbG3OG-I:3:7&u`"'\+gV_:LZ-n[=!RI>cA&WN)Rj`Hu.0PO=QX"L8hX-XJ)kqU^*)F)=:Pd)fc]p+/_Hch`G-Z)Tib4;0(FA,_Uang>aQSmJRUX8bKDjl<^NPLc+;[-J.)V'SC75R-O^BN'3V8E&(R<o!6otW0,&bZ/<"blDM,Z=0o>6WBO)kU7"1s>Rq`GNJCZC"eSJiVo(R+Z(lOqp`Kt-2105mUgM"SU%NZ38LXAMi+SV9;P[Kct)ZP&&"fSm@i&96Y&6)k??`J//=P6a+FG7b=q9DohKB,+8CZY7g=0\@gaZ'7((;A*+<0u$Dq'dQarZN93'*/Af6.`D&POW>i2F=EEs&#57hJt.E+*Y9K,6SYu'C[SU?:kotu,M=oRWYTE?iOd/GP7B(Qjo)G?QRM*JJc5m*#dh;`J\DE.7NWB[c9`7l8cu%2U$C;sVq`^Ka!5OlgJ6n\/=0(N^#F]LI,CI6":CHt(PSqdn,rs('XGsudF*8UG]^*?FK?f(ZI-m,@2B.9.Yn/Hk4g,kVK#k]8%ZVi$QIq:?I=--P$>l-6uA^BbUSueFJd[[e:A-e<.74Gf3?I+!Gg'Yn2/&hp'5sN-_TZ"'usN%(%H1[3V?;c:C^S[U+?R--#W0PMWkUcl!fd#FHfeVXbp-pQ@8^3e$NDSQ\D+=,WgJ7=k>J9-JR3q<l*>;Haf11lPAJXG\Oq^3HOmY#q5`UdhM,Z~>endstream
endobj
36 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 545
>>
stream
GauHJd;GF-'Rf-pNd,;"iF3]RUs15tg@eE]lbm:>9Q6q3YVRuT\n_7/#KG>j;Pe,?1?'-eT6m_hY%.ms1toWE'sL5Kp7?j?8CsM51.?MKGH(2YTtb50!Jus^Ta5@'BG)V'SJ`bo')UhuNq(9tLP4;?,`2GWG`$"4$V/'lpWkZ+8Sj\9LjdbmMc8fY0H3UUO3/RR%:*.[$EoH?F2[qtCCobM2=q4+"d#>8";@S<ru%,djlC(+[7GVo6e6"S,kV%ice\T<[2nNDP^H\me_U.SjnYg6(.qcVm1<;ECRU<c1'q^p?$f`-E\I"^EMFSlXkN>lf#jN7V2u[fOJ_<PqW.=Z;I3eHcR,UM>K$t&8+P)2miQ=*%CE[u*cp&n4g9CaAsL`oXe1N+AL5[c&#EaGOG.h\5B?MD>IS1#XS>R,mnCC+'q/Xm"fTu.htac:qa)3!]GN`LiV[;&_cB+]H_EZ)bmk6")n6:T6PP1b(GiRnPrVg`AB!uHF=e?N>.G,2CK4FhU3WWAO(L!lQ[aeegeH+"%W4smo*\3Kj(&~>endstream
endobj
37 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 1622
>>
stream
GauHKD/\/e&H88.EQtDg6PR4;;?s:-;tjkpg*HBB!DQG<f#<q,M(1qS9n3,%PcRED<I#UH#`6eGn*YIG00EBFdUVOVM\P+2%*i0`@+d6G!R!?*Qq^sN48Nu.eN6:00ad<p!oNSY.ia8=7q0@WftQd'i<6"kg0fSoq>mi`,+DWVbq(.W2V?`;qE'7l=,70-cXDQ^`9\qsehDHJr*IgXcaOSODp%hR%Aqlr+E3W<81f)]q%0(0")!TMs$22u?f>IX`.]2N2%$E,E9gPBl.@TTf$eLaSECIB2hZkd:]S*^i3suJ!cCkOj9p4O172PUCP5F!R!<WO5i4ClgDTZ*grT)U%:AhEXMEYEmH7$07U8J`@/iY0hXVA%[R;5NObB5P8HEBR@9Vf0c3f6jP:T00.AD>P4&%HkipQ[-!IPp^dHQUR%;HLu3R8u?>fIJbPR-=u(77\4Fj<Bp!.N+:FSpV6nV0;1'Ma.O*0`!LJ0G\?9[fXY9fEs9o0[9C$ggH^0I;5u<JE+?`Bfh%V9XK]7BBV93>3!QPHa/36<)$N]f_t34l:R-8nSN_9]@Z5<GL?/Od8R4<M@:8'oXa'V`NcHT])<":?+Ye'k:Ch2A@6jbSEXP=N,&].'T^U_#`K]nBo(EpLhlpTFOuYeqIJMJ7g@.Lb,3T&#g7!0ETRBH^hA-]?Q*1H`PBo0B[GFX>lPd[d6F!L5'QuF?j0QGQ2fXOd\W>KG0@qKi?_0JT14>CoXDi@g>;8%gB8m@t_S`\KO*5ghD'-5-:u!,5U1K>CeeJflGmjZ#&i>g9L#5V);Z5fup_,X+r\S6613\T&a(S&V(nu.QL6V"K,M?[$:fsI?krlVB>TXp()GcMkZ[uhp4bl9AdYG4502o3Qj0QI4`_g89n/]\/u?_ODUibCJi^Cg$[&J\i:`T;Z"Z.PffD/P\PBFTjI1_FV<_N$X:3aU!+]+e//X?XqfiTs.4YQq!7&&h>(^)"mLK4JI].cTV.T1AB"8LPnc/Pab0f.&I]p9\+0<!9GbY<75&\f3k+.Ma](-uG]<uCQmYT#X.FA?W&R,G236AYmeq6T2&eka]9u9WK54Sn`/$J5$^b6qT[O>DC%'j5?d^b@/of'37(pgG;0=:u"36$;+U,'p!iD+\KcEEZ4"q"8E$;D\Y+HW4$5-<Fkt4DSEB06c42:&mT\7$L%:;^AGQ$Uhl9nt-N=SH<:MDqgX:`stHXN%t<TRDkRi7$#eNV?QK\Y"Gq?h.CR:r<"`2r[@@TerR<($#K'36K"Ki'Db.-j/n%J^*4C;mKo.pbgdI("-Q`u0>kB,TIDbO;9I@jJ!nZ[SsR["UD^r1rpbdD>2:R\(YaR1S]pQAhdlX;lgtlb:3G-u$9lBGa3%+aeln:7D^SN*E.8Q?.?S9%-ZnC=lAF1q[:IQ`>9_;6$n%cuBaJ90n3Pa%o&;>U``1:G_L=nf:6*jPou3UoO85:WDQRGQt@T55\QsgATf)<--(JoXd:BJl%F>nF6&7c&pu$D)54gpK*qi-8<soZRmjms#+(#>?cC^WNi6F;ThPjFkj$/,GL<q/Y_?%UumQ#81bbmN>$.@/K9i*JYAAkOl&I`hh,l/(n;TL9>0qtLaX7f@`S:B5B-6*'q86>~>endstream
endobj
38 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 1632
>>
stream
Gatm;gN)%,&:O:SD#G""j3T&r&Xg,jini*Y'5O*Sc#d>*Q6Ck\%74*Z:B/!8hF,ML.4I,EU1!>Z4bqYnQ6*P\H0+cO"*cnYb:=)c!g>_8![iN?8,+buGs(V3bD8,T64sgKiRm[`-p^(fG">$@5mWJm]7*)^8UUC?H@CI5?73rB_[lrA)8^iPK[J+=?EBNC3.o<S-Kd;H-bQlVp#GV0NOmqrpoOSFnm(8>_<-00<m]4a]Ma#Wd^ijJ(OUCEh@('U!n)q["-6(H$F>,kAW5Hp@,s=E7U._f?-Y:%L-\St>Z.5=)1>nu[V@Kd5Xb\]#*LbE%t[[g.pA0<QMu\&#JQm`VpoYi7^4>[k"NMBJ1Y%Wa6g@fE2+kQ6\_3gZs[\[htTO@6bi!f4-r();ue?J?45fp.S![]^(:sS,i#Aqad>W0SAQpcJ<Ft8681%`V:f?!8aL]lmehr)^R>,Yo_S'dd1iIQ-`&X6OpSst0[#\P_rUH+O/*0R=%fD,dJlFOmQD3l;snNPB8.pnM..;\cZ3nqa;E@Jp-G^kI.4ZZiW>^=?L^J,1b?1glX`h0Bu+-"a(t9Hi9G0?Jgp8@Tn)8cA4ghEc7n'$P`$9Cko/43!rV:-4>*%>;X,.0@>08sCGmu9/M122NZV/:Phon=E#Wc0E4duV68EEf*k,ZOZXdU--+d"UWtc_3@)Za&Nn4,X>akO^+dYO?dH(EdS+FL1VX8n*\7IZ8G[KDnYbK-<ndiWQK*6ZmM8)u+&B:igroHK#Ef%qZM9BpbVrWfu"r&E72nLU/jHh,%nONf$m7#o\6qJ=S+Q+W0'8LH*YOQ#KR/,p$Bs5O2\D(hiA5'i_J^&Q/jX'F>Z8[f(5n8NUA:LY_+_5MldN3GA<Rq.hZrL`L2qTTp!11@2H=^SR+=Zo=e6PcrkqH72Ct#OYZVGO`bWIhc;ldA+K!fW[4&Ds0%MoaG:(e\s`oRE6qcefQ;TJdub-4nE&p#u]JW%+tSNEsZl"GB$UnRLo`iXT")"+4/G5t!KRHT!(?MV4nYP$FbFnem^!YhQ%/-`J+EDV3#-)j%,1`jEj^((ZGW%;HL;DUVnk2AQKcT%Gd?^/6[P/VNb5!:u0<(&3$p:-0VgA6[7K56)%kmI<tPT;'H)u5_rV\]mc8j74[Vh(bO@/C5.4g9?@;;A'd"gVSd%&N)7&Ik?!PNL*eM2Cp!lS%L21;'X`*%hVrpI[!?WE8Ft2.,"i:rf]TB'b"`,?!0IJq";9N[3>[O8TllpAN4Z;G3t0YS=HJ*M]W(qh)c2B0)\s>h>ChB7$kdXZn>%_u$oc=,O+&Q,g,t`BJ]JLAb,;pn>YI<Y\)^YB"eX=-O49*OWeOZ8AesHX6/m.[o9R/@>;hiuFl1YKH1]&pqTci;!0=C-Y,QnZF^'G:Le$`*^P5W1gAYcADup$J,JhEt@7PY8<j[<uDP\f*@qf?Ln!g=%MeZTlA&rm(RafWF0O<qC@"1-Vg2a[M(RfT78nA6<LWIBXVeglqkK?3TYE3gtog%Na]t=#EWe2]2N5LB)C*'PT@22fUd8aE7doo&)*Sk@6<TFo[mS6jn3<>@5AYm=N"tf;3+)OV@`A:"@5Oo?Y_gP]H:5_mCP^^N(Dl-95CH[5r8P?]\qj8$7q!W~>endstream
endobj
xref
0 39
0000000000 65535 f
0000000061 00000 n
0000000143 00000 n
0000000250 00000 n
0000000362 00000 n
0000000567 00000 n
0000000672 00000 n
0000000787 00000 n
0000000992 00000 n
0000001102 00000 n
0000001307 00000 n
0000001513 00000 n
0000001719 00000 n
0000001925 00000 n
0000002131 00000 n
0000002337 00000 n
0000002543 00000 n
0000002749 00000 n
0000002955 00000 n
0000003161 00000 n
0000003245 00000 n
0000003451 00000 n
0000003657 00000 n
0000003727 00000 n
0000004008 00000 n
0000004161 00000 n
0000005204 00000 n
0000006563 00000 n
0000008180 00000 n
0000009532 00000 n
0000010414 00000 n
0000011807 00000 n
0000012573 00000 n
0000013924 00000 n
0000015357 00000 n
0000015914 00000 n
0000017128 00000 n
0000017764 00000 n
0000019478 00000 n
trailer
<<
/ID
[<e1bbb34f5161d1e1554d7a62d828d476><e1bbb34f5161d1e1554d7a62d828d476>]
% ReportLab generated PDF document -- digest (opensource)
/Info 23 0 R
/Root 22 0 R
/Size 39
>>
startxref
21202
%%EOF

View File

@@ -1,186 +0,0 @@
# Les 8 — Slide-overzicht
## Van In-Memory naar Supabase (10 slides)
---
### Slide 1: Titelslide
**Titel:** Les 8 — Van In-Memory naar Supabase
**Ondertitel:** Koppelen van Supabase aan Next.js
**Visual:** Supabase + Next.js logo's, BLUE achtergrond
---
### Slide 2: Terugblik vorige les
**Titel:** Terugblik — Waar waren we?
**Bullets:**
- Stemmen werkt lokaal (in-memory data)
- QuickPoll app heeft 2 pages: / en /poll/[id]
- VoteForm component ziet stemmen onmiddellijk
- Nu: alles naar een echte database
**Code snippet:**
```javascript
// OUD
const polls = [
{ question: "...", options: [...], votes: [...] }
];
```
---
### Slide 3: Planning vandaag
**Titel:** Planning — Les 8 (3 uur)
**Timeline:**
- 09:00-09:15 | Welkom & Uitleg aanpak (15 min)
- 09:15-09:45 | **Uitleg concepten** (30 min)
- 09:45-10:15 | **Zelfstandig: Setup + Queries** (30 min)
- 10:15-10:30 | Pauze (15 min)
- 10:30-10:45 | **Uitleg INSERT queries** (15 min)
- 10:45-11:30 | **Zelfstandig: /create pagina** (45 min)
- 11:30-11:45 | Vragen & Reflectie (15 min)
- 11:45-12:00 | Huiswerk & Afsluiting (15 min)
**Extra tekst:** "Jullie werken met de Lesopdracht PDF. Alle UI staat erin — jullie schrijven de queries!"
---
### Slide 4: Van Array naar Database
**Titel:** Van In-Memory Array naar Supabase
**Links:** In-memory (OUD)
```javascript
const polls = [
{ question: "Favoriete taal?",
options: ["JS", "Python"],
votes: [10, 5]
}
];
```
**Rechts:** Supabase Database (NIEUW)
```
polls tabel
├─ id (1)
├─ question ("Favoriete taal?")
└─ options[] (relatie)
options tabel
├─ id (1)
├─ poll_id (1)
├─ text ("JS")
├─ votes (10)
```
---
### Slide 5: Supabase Queries
**Titel:** Supabase Queries — Vier operaties
**Vier blokken:**
1. **SELECT alles** (met relaties)
```typescript
supabase.from("polls")
.select("*, options(*)")
```
2. **SELECT één** (filter + single)
```typescript
supabase.from("polls")
.select("*, options(*)")
.eq("id", 5).single()
```
3. **INSERT** (nieuw record)
```typescript
supabase.from("polls")
.insert({ question: "..." })
.select().single()
```
4. **RPC** (database functie)
```typescript
supabase.rpc("vote_option",
{ option_id: 42 })
```
---
### Slide 6: Server vs Client: Wie doet wat?
**Titel:** Server vs Client: Wie doet wat?
**Twee kolommen:**
**SERVER Component:**
- `export default async function HomePage() { ... }`
- `const polls = await getPolls()`
- Data fetching
- Direct naar database
- TypeScript compile-time
**CLIENT Component:**
- `'use client'`
- `const [voted, setVoted] = useState(...)`
- Interactief: klikken, typen, formulieren
- useEffect, event handlers
- Browser runtime
**Zeg:** "Server haalt data, Client maakt het interactief."
---
### Slide 7: Pauze
**Titel:** Pauze
**Tekst:** Setup + queries klaar? Na de pauze: /create pagina bouwen!
---
### Slide 8: Zelf Doen — /create pagina
**Titel:** Zelf Doen — /create pagina
**Ondertitel:** Het formulier staat in de PDF. Jij schrijft de INSERT logica!
**INSERT voorbeeld:**
```typescript
// 1. Insert poll → krijg id terug
const { data: poll } = await supabase
.from("polls")
.insert({ question })
.select().single();
// 2. Insert options met poll.id
await supabase.from("options").insert([
{ poll_id: poll.id, text: "...", votes: 0 }
]);
```
**Stappen:**
1. RLS INSERT policy toevoegen (Stap 4.1 in PDF)
2. handleSubmit invullen (TODO blok in PDF)
3. Testen: poll aanmaken → homepage checken
---
### Slide 9: Huiswerk
**Titel:** Huiswerk
**Verplicht:**
- /create pagina afmaken (als niet klaar)
- Validatie toevoegen (vraag niet leeg, min 2 opties)
**Extra:**
- Delete functionaliteit
- Styling verbeteren
---
### Slide 10: Afsluiting
**Titel:** Tot volgende week!
**Tekst:**
- "Volgende les: Supabase Auth"
- "Inloggen, registreren"
- "Bepalen wie wat mag doen"

View File

@@ -1,575 +0,0 @@
# Les 8 — Docenttekst
## Van In-Memory naar Supabase
---
## Lesoverzicht
| Gegeven | Details |
|---------|---------|
| **Les** | 8 van 18 |
| **Onderwerp** | Supabase koppelen aan Next.js |
| **Duur** | 3 uur (09:00 12:00) |
| **Voorbereiding** | Werkend QuickPoll project, Supabase project met polls/options tabellen |
| **Benodigdheden** | Laptop, Cursor/VS Code, browser, Supabase account |
## Leerdoelen
Na deze les kunnen studenten:
1. De Supabase JavaScript client installeren en configureren
2. Environment variables gebruiken voor API keys
3. Data ophalen via Supabase queries (select met relaties, eq, single)
4. Het verschil uitleggen tussen sync en async data ophalen
5. Het Server Component + Client Component patroon toepassen
6. Een formulier bouwen dat data INSERT in Supabase
---
## Lesplanning
### 09:0009:10 | Welkom & Terugblik (10 min)
📌 Slide 1, 2, 3
**Doel:** Studenten op dezelfde pagina brengen over waar we zijn.
**Wat te zeggen:**
- "Vorige week hebben we een werkend polling app gebouwd met in-memory data."
- "Vandaag koppelen we Supabase: onze database-as-a-service."
- "Na vandaag kunnen jullie niet alleen stemmen, maar ook nieuwe polls aanmaken."
**Check:**
- Iedereen heeft Supabase account met polls en options tabellen
- Iedereen heeft QuickPoll project lokaal runnen op localhost:3000
- Niemand heeft Auth ingesteld (dat doen we volgende les)
---
### 09:1010:15 | DEEL 1: Live Coding — Supabase koppelen (65 min)
📌 Slide 4, 5, 6
**Doel:** Live voor hen de hele flow bouwen: installatie → queries → component aanpassingen.
**Voorbereiding jij:**
1. Open Cursor met je QuickPoll project
2. Zorg dat Supabase dashboard open staat in je browser
3. `npm install @supabase/supabase-js` al gedraaid (zeker weten!)
4. Terminal gereed, dev server draait
**Stap-voor-stap Live Coding:**
#### 1. npm install @supabase/supabase-js
```bash
npm install @supabase/supabase-js
```
**Zeg:** "Dit geeft ons de client om met Supabase te praten."
#### 2. .env.local (Settings → API)
```
NEXT_PUBLIC_SUPABASE_URL=https://xxxx.supabase.co
NEXT_PUBLIC_SUPABASE_ANON_KEY=eyJhbGc...
```
**Zeg:** "Dit zijn jullie API credentials. Ziet erruit in Supabase Settings → API. De `NEXT_PUBLIC_` prefix betekent dat deze in de browser beschikbaar zijn (safe)."
**Docent tip:** Na `npm install` en .env wijzigen moet de dev server **herstarten**! Zeg dit expliciet.
#### 3. lib/supabase.ts
```typescript
import { createClient } from "@supabase/supabase-js";
export const supabase = createClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
);
```
**Zeg:** "Dit is onze Supabase client. We maken hem eenmalig aan en exporteren hem, dan kunnen alle componenten hem gebruiken."
#### 4. types/index.ts (Database matching)
```typescript
export interface Poll {
id: number;
question: string;
created_at: string;
options: Option[];
}
export interface Option {
id: number;
poll_id: number;
text: string;
votes: number;
created_at: string;
}
```
**Zeg:** "Dit matchen onze TypeScript types met de database schema. Poll bevat options als relatie."
#### 5. lib/data.ts (Supabase queries herschrijven)
**VOOR je dit toont, laat je het oude in-memory array zien:**
```typescript
// OUD:
const polls = [
{ question: "...", options: ["...", "..."], votes: [0, 0] }
];
export function getPolls() {
return polls;
}
```
**Zeg:** "Dit was in-memory. Nu halen we het uit Supabase."
**NA - Supabase queries:**
```typescript
import { supabase } from "./supabase";
import { Poll } from "@/types";
export async function getPolls(): Promise<Poll[]> {
const { data, error } = await supabase
.from("polls")
.select("*, options(*)");
if (error) {
console.error("Error fetching polls:", error);
return [];
}
return data || [];
}
export async function getPollById(id: number): Promise<Poll | null> {
const { data, error } = await supabase
.from("polls")
.select("*, options(*)")
.eq("id", id)
.single();
if (error) {
console.error("Error fetching poll:", error);
return null;
}
return data;
}
export async function votePoll(optionId: number): Promise<boolean> {
const { error } = await supabase.rpc("vote_option", { option_id: optionId });
if (error) {
console.error("Error voting:", error);
return false;
}
return true;
}
```
**Docent tips:**
- `.select("*, options(*)")` = "Haal polls op, EN daarbij hun relatie options"
- `.eq("id", id)` = "Where id = ..."
- `.single()` = "Ik verwacht exact 1 resultaat"
- `await` = Dit is nu async! Componenten moeten `async` zijn of we gebruiken een API route
#### 6. PAUZE — Slide 6: Server vs Client: Wie doet wat?
**BELANGRIJK:** Toon deze slide VOOR je componenten aanpast. Dit patroon is cruciaal.
**Zeg:**
"We hebben nu async functies. Server Components kunnen `await` direct gebruiken. Client Components niet. Daarom splitsen we:
- Server Components: /page.tsx files (halen data op met await)
- Client Components: VoteForm (useState, onClick event handlers)"
Laat code zien:
```typescript
// Server Component
export default async function HomePage() {
const polls = await getPolls();
return <>{...}</>
}
// Client Component
'use client'
export function VoteForm() {
const [voted, setVoted] = useState(false);
return <>{...}</>
}
```
---
#### 7. app/page.tsx → Server Component
```typescript
import { getPolls } from "@/lib/data";
import Link from "next/link";
import PollItem from "@/components/PollItem";
export default async function HomePage() {
const polls = await getPolls();
return (
<div className="w-full max-w-2xl mx-auto p-6">
<h1 className="text-3xl font-bold mb-6">Huidige Polls</h1>
<Link href="/create" className="text-blue-600 hover:underline mb-6 block">
+ Nieuwe Poll
</Link>
<div className="space-y-4">
{polls.map((poll) => (
<PollItem key={poll.id} poll={poll} />
))}
</div>
</div>
);
}
```
**Zeg:** "Dit is nu async! De `await getPolls()` werkt hier rechtstreeks. Link naar /create toevoegen."
#### 8. components/PollItem.tsx (Option type, percentage bars)
```typescript
'use client'
import Link from "next/link";
import { Option } from "@/types";
interface PollItemProps {
poll: {
id: number;
question: string;
options: Option[];
};
}
export default function PollItem({ poll }: PollItemProps) {
const totalVotes = poll.options.reduce((sum, opt) => sum + opt.votes, 0);
return (
<div className="border rounded p-4">
<h2 className="text-xl font-semibold mb-4">{poll.question}</h2>
<div className="space-y-2">
{poll.options.map((option) => {
const percentage = totalVotes > 0 ? (option.votes / totalVotes) * 100 : 0;
return (
<Link key={option.id} href={`/poll/${poll.id}`}>
<div className="flex items-center gap-2 cursor-pointer hover:opacity-80">
<div className="flex-1 bg-gray-200 rounded h-8 overflow-hidden">
<div
className="bg-blue-600 h-full transition-all"
style={{ width: `${percentage}%` }}
/>
</div>
<span className="text-sm font-medium w-20">
{option.text} ({option.votes})
</span>
</div>
</Link>
);
})}
</div>
</div>
);
}
```
**Zeg:** "Nu hebben we Option type beschikbaar. Percentage bars tonen stemmen visueel."
#### 9. components/VoteForm.tsx (Client Component met vote mutation)
```typescript
'use client'
import { useState } from "react";
import { votePoll } from "@/lib/data";
import { Option } from "@/types";
interface VoteFormProps {
options: Option[];
}
export default function VoteForm({ options }: VoteFormProps) {
const [loading, setLoading] = useState(false);
const [voted, setVoted] = useState(false);
const handleVote = async (optionId: number) => {
setLoading(true);
const success = await votePoll(optionId);
if (success) {
setVoted(true);
}
setLoading(false);
};
if (voted) {
return <p className="text-green-600">Dank je voor je stem!</p>;
}
return (
<div className="space-y-2">
{options.map((option) => (
<button
key={option.id}
onClick={() => handleVote(option.id)}
disabled={loading}
className="w-full bg-blue-600 text-white p-2 rounded hover:bg-blue-700 disabled:opacity-50"
>
{option.text}
</button>
))}
</div>
);
}
```
**Zeg:** "Dit is Client Component: `'use client'` bovenaan. We kunnen useState gebruiken, onClick handlers. Na stem, feedback tonen."
#### 10. app/poll/[id]/page.tsx (Server + Client combo)
```typescript
import { getPollById } from "@/lib/data";
import VoteForm from "@/components/VoteForm";
import { notFound } from "next/navigation";
export default async function PollPage({ params }: { params: { id: string } }) {
const poll = await getPollById(parseInt(params.id));
if (!poll) {
notFound();
}
return (
<div className="w-full max-w-md mx-auto p-6">
<h1 className="text-2xl font-bold mb-6">{poll.question}</h1>
<VoteForm options={poll.options} />
</div>
);
}
```
**Zeg:** "Server Component haalt data. Geeft VoteForm (Client Component) de options door. Best of both worlds!"
#### 11. app/api/polls/[id]/route.ts (GET + POST)
```typescript
import { getPollById, votePoll } from "@/lib/data";
import { NextRequest, NextResponse } from "next/server";
export async function GET(
request: NextRequest,
{ params }: { params: { id: string } }
) {
const poll = await getPollById(parseInt(params.id));
if (!poll) {
return NextResponse.json({ error: "Not found" }, { status: 404 });
}
return NextResponse.json(poll);
}
export async function POST(
request: NextRequest,
{ params }: { params: { id: string } }
) {
const { optionId } = await request.json();
const success = await votePoll(optionId);
return NextResponse.json({ success });
}
```
#### 12. Test alles!
- Homepage laden → alle polls met opties tonen
- Click poll → detail pagina, stem kan worden gegeven
- Stem geven → votes increment in Supabase
- Controleer in Supabase dashboard → votes kolom stijgt
**Docent tips bij Live Coding:**
1. **TypeScript errors:** "Soms zien we rode squigglies. Dat is TypeScript die zegt 'ik snap dit type niet'. Hover je eroverheen, meestal is het een `!` die je moet toevoegen of een import."
2. **RLS blocking:** "Nog krijgen we misschien 'RLS policy violation'. Dat fix je volgende les met Auth. Nu gebruiken we publieke SELECT."
3. **Env restart:** Na .env wijzigen ECHT herstarten. Hardnekkig bug!
4. **Queries testen:** Open Supabase dashboard → SQL Editor → test je select statements daar eerst.
---
### 10:1510:30 | PAUZE (15 min)
📌 Slide 7
---
### 10:3011:30 | DEEL 2: Zelf Doen — /create pagina (60 min)
📌 Slide 8
**Doel:** Studenten bouwen zelf een formulier om nieuwe polls aan te maken.
#### Stap 1: Theorie op beamer (15 min)
**Zeg:**
"Nu bouwen jullie zelf de /create pagina. Daarmee kunnen gebruikers nieuwe polls aanmaken. Eerst leg ik het uit, dan doen jullie het zelf."
**INSERT queries uitleggen:**
Laat dit zien:
```typescript
// 1. Insert poll
const { data: poll } = await supabase
.from("polls")
.insert({ question: "Wat is je favoriete taal?" })
.select()
.single();
// poll is nu { id: 42, question: "Wat is je favoriete taal?", ... }
// 2. Insert options (meerdere tegelijk)
await supabase.from("options").insert([
{ poll_id: 42, text: "JavaScript", votes: 0 },
{ poll_id: 42, text: "Python", votes: 0 },
{ poll_id: 42, text: "Rust", votes: 0 }
]);
```
**Zeg:**
- ".insert() = INSERT statement"
- ".select().single() = geef me terug wat je net inserted, als 1 rij"
- "poll.id gebruiken we dan voor de options"
- "Daarna .insert([...]) meerdere opties in één keer"
- "Dan router.push('/') terug naar homepage"
**RLS policy toevoegen:**
Laat dit SQL blokje zien (ze moeten dit in Supabase doen):
```sql
-- INSERT policy voor polls
CREATE POLICY "Allow public insert on polls"
ON polls FOR INSERT
TO anon
WITH CHECK (true);
-- INSERT policy voor options
CREATE POLICY "Allow public insert on options"
ON options FOR INSERT
TO anon
WITH CHECK (true);
```
**Zeg:**
"Dit zegt tegen Supabase: 'Iedereen mag INSERT-en op polls en options.' Zonder dit krijgen jullie 'RLS policy violation'. Dit is tijdelijk — volgende week beperken we dit met Auth."
**Form outline:**
```
1. Text input voor vraag
2. Meerdere text inputs voor opties (minimum 2)
3. "+ Optie toevoegen" knop
4. "Poll aanmaken" submit knop
5. Bij submit: INSERT in polls, dan INSERT in options, dan redirect("/")
```
#### Stap 2: Zelf doen (45 min)
**Wat studenten moeten doen:**
1. **RLS policy** in Supabase dashboard toevoegen (SQL Editor)
2. **app/create/page.tsx** aanmaken met:
- `'use client'` bovenaan
- useState voor question en options array
- Input voor question
- Loop over options, input per optie
- "+ Optie toevoegen" knop (addOption)
- "Poll aanmaken" button (handleSubmit)
3. **handleSubmit logica:**
- Insert poll → krijg poll.id terug
- Insert opties met die poll_id
- Error handling
- router.push("/") na succes
4. **Homepage (page.tsx) updaten:**
- Link naar /create bovenaan
**Docent loop ronde:**
- **Min 0-5:** Iedereen aan het werk?
- **Min 15:** Check of iedereen RLS policy heeft ingesteld. Help als iemand vast zit.
- **Min 25:** Toon code snippet van useState setup als mensen vragen hebben.
- **Min 30:** Check of eerste iemand INSERT werkend heeft. Toon in Supabase dashboard hoe je ziet dat poll aangemaakt is.
- **Min 45:** Ruim 5 min voor finalisatie, vragen, troubleshoot.
**Veelvoorkomende problemen:**
| Probleem | Oplossing |
|----------|-----------|
| "RLS policy violation" | Zeg: RLS policy toegevoegd in dashboard? Zien we in error message "RLS"? |
| "poll is undefined na insert" | `.select().single()` weg? Dat moet je toevoegen! |
| "Opties werken niet" | poll.id goed doorgegeven aan insert? Controleer in Supabase options tabel. |
| "Form submit refresh de pagina" | `e.preventDefault()` in handleSubmit? |
| "Redirect werkt niet" | `import { useRouter }` bovenaan? `const router = useRouter()` in component? |
| "Opties array gaat fout" | Laat code zien: `const newOptions = [...options]; newOptions[index] = value; setOptions(newOptions);` |
---
### 11:3011:45 | Vragen & Reflectie (15 min)
**Mogelijke vragen + antwoorden:**
**V: Wat happens na redirect?**
A: De homepage laadt opnieuw. `app/page.tsx` roept getPolls() aan, die hit Supabase en toont je nieuwe poll.
**V: Waarom `async`/`await`?**
A: Supabase is over het network. We wachten tot het antwoord komt. `async` zegt "dit kan tijd kosten".
**V: Kan ik realtime zien als iemand anders stemt?**
A: Volgende week! Supabase heeft realtime subscriptions. Daar leren we.
**V: Wat is `/api/` folder?**
A: Dat zijn backend endpoints. Volgende week gebruiken we die meer.
**V: Waarom `'use client'` in create en vote, maar niet in page?**
A: Client = interactief (forms, buttons, state). Server = data fetching. Next.js split dit automatisch.
---
### 11:4512:00 | Huiswerk & Afsluiting (15 min)
📌 Slide 9, 10
**Huiswerk:**
1. **/create pagina afmaken** (als nog niet klaar in klas)
2. **Validatie toevoegen:**
- Vraag mag niet leeg
- Opties moeten uniek zijn
- Minimaal 2 opties
- Error messages tonen
3. **Delete functionaliteit:**
- Delete knop op PollItem
- Verwijder poll + opties uit Supabase
4. **Extra (voor snelle studenten):**
- SQL queries schrijven (direct in Supabase SQL Editor)
- Realtime subscriptions uittesten
- Styling verbeteren
**Zeg:**
"Volgende week: Supabase Auth. Jullie gaan inloggen en registreren bouwen. En bepalen wie welke polls mag aanmaken. Tot dan!"
**Slide 10: Afsluiting**
- "Tot volgende week!"
- "Volgende les: Supabase Auth — inloggen, registreren, en bepalen wie wat mag"
---
## Veelvoorkomende problemen & Troubleshoot
| Symptoom | Oorzaak | Oplossing |
|----------|---------|-----------|
| "Cannot find module @supabase/supabase-js" | npm install niet gedraaid | `npm install @supabase/supabase-js` |
| Env vars undefined in browser console | NEXT_PUBLIC_ prefix vergeten OF dev server niet restarted | Restart dev server (`npm run dev`). Check prefix: NEXT_PUBLIC_SUPABASE_URL |
| "RLS policy violation" on SELECT | RLS enabled, geen SELECT policy | Voor nu: disable RLS in Supabase (Security → RLS → toggle OFF). Volgende les met Auth |
| "RLS policy violation" on INSERT | Geen INSERT policy of RLS restrictief | Voeg INSERT policies toe (zie Deel 2 stap 1) |
| getPolls() returns empty array | Query failed maar geen error | Check: .select() syntax correct? options(*) geindent? Controleer in Supabase SQL Editor |
| TypeScript "Cannot find name 'Poll'" | Import weg | `import { Poll } from "@/types"` bovenaan |
| "notFound() is not defined" | Import weg | `import { notFound } from "next/navigation"` |
| Percentage bars werken niet | totalVotes = 0 dus percentage = 0 | Check: votes kolom in Supabase ≠ 0? Stem eenmalig via UI |
| Client form not submitting | e.preventDefault() weg OF loading state blocked | Check handleSubmit: eerst `e.preventDefault()`, geen return-statements die vorig breken |
| Redirect naar / werkt niet na poll maken | router niet geïmporteerd OF router.push() fout | `import { useRouter }` from "next/navigation" (niet "next/router"!) |
| Supabase queries slow | Network latency / veel data | Normal! Later: replication, caching, realtime |
---
## Tips voor docenten
1. **Code live typen, niet copy-paste.** Laat typos zien, laat debugging zien. Authentiek!
2. **Veel pauzes voor vragen.** Live Coding voelt snel. Check regelmatig: "Allemaal met me mee?"
3. **Zelf Doen starten met duidelijke steps:** (1) RLS policy, (2) page.tsx, (3) form, (4) submit. Niet: "Bouw de pagina."
4. **Loop ronde, spot problemen vroeg:** Min 15-25 zijn goud voor troubleshoot.
5. **Toon Supabase dashboard often:** "Zie je? De data staat echt in de database!"
6. **Authenticatie is volgende les:** Zeg het af te toe: "Dit beperken we volgende week met Auth."
7. **Celebrate wins:** Eerste student met werkende /create? Toon het aan iedereen!

View File

@@ -1,238 +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 6 0 R /F4 7 0 R
>>
endobj
2 0 obj
<<
/BaseFont /Helvetica /Encoding /WinAnsiEncoding /Name /F1 /Subtype /Type1 /Type /Font
>>
endobj
3 0 obj
<<
/BaseFont /Helvetica-Bold /Encoding /WinAnsiEncoding /Name /F2 /Subtype /Type1 /Type /Font
>>
endobj
4 0 obj
<<
/Contents 18 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 17 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
5 0 obj
<<
/Contents 19 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 17 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
6 0 obj
<<
/BaseFont /Courier /Encoding /WinAnsiEncoding /Name /F3 /Subtype /Type1 /Type /Font
>>
endobj
7 0 obj
<<
/BaseFont /Helvetica-Oblique /Encoding /WinAnsiEncoding /Name /F4 /Subtype /Type1 /Type /Font
>>
endobj
8 0 obj
<<
/Contents 20 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 17 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
9 0 obj
<<
/Contents 21 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 17 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
10 0 obj
<<
/Contents 22 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 17 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
11 0 obj
<<
/Contents 23 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 17 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
12 0 obj
<<
/Contents 24 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 17 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
13 0 obj
<<
/Contents 25 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 17 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
14 0 obj
<<
/Contents 26 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 17 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
15 0 obj
<<
/PageMode /UseNone /Pages 17 0 R /Type /Catalog
>>
endobj
16 0 obj
<<
/Author (\(anonymous\)) /CreationDate (D:20260331162605+02'00') /Creator (\(unspecified\)) /Keywords () /ModDate (D:20260331162605+02'00') /Producer (ReportLab PDF Library - \(opensource\))
/Subject (\(unspecified\)) /Title (\(anonymous\)) /Trapped /False
>>
endobj
17 0 obj
<<
/Count 9 /Kids [ 4 0 R 5 0 R 8 0 R 9 0 R 10 0 R 11 0 R 12 0 R 13 0 R 14 0 R ] /Type /Pages
>>
endobj
18 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 697
>>
stream
Gatm8bAu;j']&X:mP8J]`Hk_QY+>2NLF/kfO:</LW"^qo,G:i^h^a<gGiEg#"bUC2]fH\i#RV_][(</.J.FA>IA/tE`'8e[+s7rBa#J;Pke9tMU'rjQTHqpLij)teCgHVCrYQd)md$_NEmTQLLJ5l3Vl2SCB;AcE',lK4nH:I^DF]-M;G?I-rDaOLN#@Q3I3/[E\nj:N0-QoSr!%JFlVR>[6Q;4W.&"5kLV.=doh;V]80D/mgb9k*qp.'1eBSL*TtMDREt`O;,?;>nfG3V&jHF&^A>qT*O`sE`MDbnlR>03>$LP)M3NNB?Fq>W"&o"'4?LTAbTqD(nfk'.A5b952mP\YIOK<8.j)iBN2AkkD&>n1dVQOf]F`+!%3oZ.glXX;#KLJb(2R>ChB][5tXiYqoDrE0g2Ha64PNaVoi1\e>m3,0$@,7+O1Di/%gQmdAN+*tb.8BbJ=n4Y*B;RD:X@K]6B^0I3#h:]8j5mtWhGGXGV$<2U>pF.[NN\0'A,-(%/>ERHHF%VSQFr"8JClQi#gKsmK7RfW*+,;^!B,7mgO*Jdf"1G-2YQq=OZJ0'*RM,Q1)8ch3]gQJ%uEN;TcO'EFUO[7@(;W$Msj*#TqB>7l&*#Gf14'agjI)u4<W@&;pZS8Hk(scD3,uH`*`o,.&(9$r%*K4Wdt,]7g5F5"X]6;3:hi4F[>L5+7kPL~>endstream
endobj
19 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 772
>>
stream
GauIu?#Q2d'F*Lmr/,Q`DA'qN.pQt-W^_lGY!KagEoW6[il#5>$sil2$q/q&+jH2t#72RXEZQ1SY6k!d+N$2m!Bt.8I7o)eaI[s9?3o4&QJ(V97P8o)e;I7o&kJb8,AE[:kKM6nNG0"phusKQ81Adtd!n:q1\77"QSM]X8l20JM>k/Bq5QKsG2LBP?l`Q>G(f3L?hUN\VFh)EkL1RA92FG1(2l.Er!j!Z!/^QX4bJifi=Xs(L&>pj"jClr"craBFNs<j*4D(YY\@m]@h$:Hehmf9J?]ILn[nLT%;b3ChB@9/HX7HAX1u+o:(o-9Z!bIGDYgZdX9R[HGE'ZIa%D!B42o\eGh$IS"=gK(kXk-(:.P/dlF<!Wfs<c;384aU"umO2%6KGPZ@hB_2hRu<-S'*H](C`P2`oWg>MFJMZk\`,/s4P;1cXX^"qIT#r1NhiZ`utKF_Qh*l^]T3Ad.mH^1L)L,u'1gBei:&U_#&9-02A5AS"?3Kf&@+8f+8p8SYo'lJJBoFX^%7p*o?*1G6J%!a[ShWGe,:s!o#cOo"<N"\Q3ATp&B*=]`epB!@!t]-O]m?of=Df==5S-saCrG-ZNR+hX@q6h(RD&hBI(%:_?M[CY6H+#A.U;=T)uj]Z&#_$f(0eDkiW(Les"cd/71FqP1`(J`chdkJZmYIR&bqMMNZ5(]lDku.tfmjO[Z?-1>t8esO%`i!e%($69I;R!Au,b(cSfCJ&=eS0@#4>FZHC2u/F^"L*ar_'$4_JE<23FVU`INlrO~>endstream
endobj
20 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 1140
>>
stream
Gatm:mr-r=&H2%3imLV?;:?lb4%4ZGR9-3WgoOP6%1!M!16]kC6rd^OAAeE@*\#33*.([e>D?fY,k'RLHZu2<_Mak0PQIA.0`6n3AJ!\u[>>@O"NFm=997X[>t3V""Va)>,;a?A>_e@HF.$Z&5V.Cq"Umq0kmnK;4ku70_Ls@m@),GAI@ie)=!83LLG6?dq)'f/grVYpM9(5=%&W#MO[]'Y_?GUr1n6f"g'5Z.D+Jc%Ijf,nRultD&nXj4erG;<M3e9=jZLh+iK>o+jt[R;![VET?:1^[kP,i*/nK']6RkJ/JCHBi1r-4,>i/^Zkahfj8>WK2^c*.T'[M=C>/=QC)^Vc7,DiT3?9CQEQ&6rG-q*cWr2r-X+7Q]PHr^If"jFq(hE/I4Z`&\$oc,D3"QY8ALe&P0!,L%A=?k0TK['&IgE(@mCQAVdaRa9$*&3ii#/:)j^-d9Q#.S:hi8g/[qh$G%&/Z$26UI*:bmD0=A^/&qq(_(WHC$XFqR\'8"LNHg-Uk:4;<)I1(G\YV)(g+GoBSf/-6%NVg-JGnl`ZF6kGltP=uX'u6iW>a[osefPnuda1Zh8cQ5*Rc#*?sKCtC$Y>EVBD4K4r0JbgD*j]HDYeNIZjAh@j_+bb5s'rtf?f$Su01*-B4?CbGn9[6D:Af'XN-c`R`e]#SNd:]<Hno-R9PkW.i2ABjn7j;ZqfD0@Bict<.HM)abf9'4^1RS^?`8fc3erUB[d=Aa@ZW"#Mi&&cB=KC^2S)[GX@^i)>]54,b,M9$"HkOD]@faUOK6[QbB`s<i>%t*p=P)r*@VQt<arrJ#TOe13j2KnfH7Gu7F8]B&l1RaYKS9g`5VEr+((S()K^Apk+dCbW.Vr`T@8GI^[Pn<TZ73QSi`VbU(8d][mI-S&4`_oMqc3S>WnatYL>'J8QK>TTgS6t8H^?te^#R02)5k"r'`=^Pa-AWdcJi3@.:`c=9ToA19qbq?GdDFk;5/:hRmn%V0JmS'kXg`Vrn?>]!Gik)&s4S8;^ci)PCqJ6g8h2&#OD=@?bYcDeC5e>Oghq'17tZqm)__?AY\`La,YA9c]QljhI01+!qLEh\M-=A\OU@%_`4-DLMc2$Q1%u;]*;<A&%RR/L5f&eieHLHCt]5BhZ/&?ok4~>endstream
endobj
21 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 904
>>
stream
Gb"/%>Aoub'Ro4H*:8SNSJ-&PA/@t&g2596[4?0lW[KSN'nO;Hej`C6qeSVuaH^l#gQ[Vei,d3,cfb0*6W^q)qMrPq"!9R:(pF$i%+ml^pcm]df;9H==#pP+4N#'EHTd$F`tNIP13,OeCO33Grq`W<)]gis3Sg9B_<OQ]=pFG$q6jNI\KVHdpeV$E[WWfL$V5N!'),)ha+-m@X)hm)-\3Js'hIiJ84T>:a%9#d,T.sd%O2&<H()TK$iG>O^C<6&l/j-,p*#obD[HBh/7:L`N$io%#KCJ@N>dO?P_>P8DaJn\?p9_'=<E!lajkM[mWgfm5M(]>6N.i;'>a:b_,6^b*m2=fS.g'RR@T!I`/0mqr!V&-lcn=n;:dKBg&.<U=EqAAW+0Uqf%\r@c.tB9!'<f'Y&&1XP(s^d6:QN7o\aaJ$#`1#5kGsh#SSQm_iTY0ZNdqE>CfeTD(/oHKs646i-icE7OX2_BQ>12Uak.L8l(Z*:)25n664(!YO/$u+1[5T<C2a[2hjT+c#-eL5eBV-8nCf6?FKV;1!Rc.Y%TrIH+?cL0)r(rh_Kg>Wd=<(%\HN&gCd\TH-#,N"S"%$PfQ@JbB,;^K2s)>7EXZp_8g#HQ`mQMq<+WP&..ff:utiP!226I!q@WD+2W[uD5V`%m2D'3+jb$%QMscp,%r!j&gJi^pe%[^$'\1(kj>\"CXn[`f]f+b"M3SABJh)]hZ;de5t/pWkYlUD1#^NNn2F0kokQ)#,HL7jbt'8A1R^Vs%^^$MF*#dPe?[2(Z%*9B\_YQY23DTWk#S\*Nji84j<khgfkN)CIbU(1An;M8hhOMZ-G<(7YRl`@oY8u7T_0O/-.A-:a`'\6am_:E'=(D*n.uR7.Sk4\6WmKtHC.=0D'R*/$^$&7#>d;eGl~>endstream
endobj
22 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 1296
>>
stream
GauHK;01_T&:WeDm.BD`orsQqN8^4!-_7&W1[.X%9)DM1DM/V?LFr=44)gDB^NV7K(D@'d+;mKkRFUTO\(XVn!aY^8p"tf1Y!ugS&0N<YVEetRL=%SXO':FFUpP5;h&2T4]OU2:i!aQGriE^6[qcF\]D_;I9CK]kV%9)6hmHn)Q*V6]Ngp(%DGPIr)N_Z>P/5]'en=W6O@Y(gfR4T*5cbrlEFZg-#HEt?+sB`c3<P6m@j+dDaUdgW1uj$'#S%@hATo=,4\`ljK9)3>n15J6l*Z:c-Ue^pX::qCT`%bF@j+0.Gqlg^8(E1<>L9$f[^5K-:JuE?-Dcg.Jd\4Z)PiI*0ce_rkq_rqXg%c'BQ@cn$WOOsn!^ET>Z:Fr)W5ej)fOk\jgEuIQ3XOE.<q<8Ze%AGm$pP@?S>[8q79VV,&74O%C[0)+hU#[JRiIA3D=.SY_MRHc1rj&V_["K?9#_A-K&O%$Vutj9D#B$3I)8*2!?suaoJbiDkbb1bfl9p\1rOo8dO37h6]CF)?\A-n4^/!+%4%V3b+7*q7n9N,)QP:4?\Y7BA4AuC+J$g]mE1oUC`PTq>FAWTd_L:</B'5NQ/5cUXR\&=oPl'V?$n+'SL>i_o(F&]?07SFjG02ICa&pmZG's6q[1p@E-`#kn6sf.I%h9amlDD#%IbGUU?WF3.@]ZgORUc1HA[:318)'9[$T:R'LC@fo#+6=Bk-3Un!_H4^h9XKhleQ5D44tAl!1p1kd9sMNbFhY=Zn8*2.FbhNj%@P/*=6bTqE55GUmAM+6j+7a.S_c,(#9]'Lt/jq<$a%a@lPEQ>$r?Cm1S!Rc>#%qph*UgkR`WShK'/A_]]j..$p%p:Rm:dYrm2>>*RH-P0D;gpCQ[([9mQOMuPI.lo]a'ltK::CW*![VEFnHkDH,u!\H`hDnb3uE4II6H3[%%dTe/IL3=KjNfJdYepupS1>s4ea[nZBc"t)9Qo(Sg@<2UnX$k3W!fid-AGXV=^]'Y/#8sVS'@h?CegK3l7h#8&;?nAIV6PdrBN>f<t@=3i6Ii#%oE.qF.A^L$Z;22%D7IJYrDZ1H;haq1I,kP7hNK7CohZn[(5X_GIiG[!dS[&2CisR.PBcf5G.KPC0Ig#ac+RiLCX2aD^lhY8X<<Jrq-Pj.B`M0@no\GUL";`44ZS&_GMagZe)7+L0bFi(O/nMZEa9mo254*Z%)ih*CSK!F,_C(-ntTM?"ra$G9kK5s+6RN\M)a'mtXZ(sIaX"cKGFrVt#u*[p-c\=*k*`[53\cPqOVJ<d-Op<4_6d/"7i^A*?~>endstream
endobj
23 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 474
>>
stream
Gau1(btc/1&;9M#ME(kN8N2lu*'mDS"JUFcKlA`jP,@:Na)1(qA%T)^."VQB;A(M?R9>#mY$'hWUCoO!#R1D=+W`p4^B\b='@D*nd$lctr?r.c69cZdi[?_Jb\s,bAjc@k<a!IfdQ5AbQR3mZ75!T8#'`(12BY4R&r(fCl_;0LCRGtUat0aW%`AKZf/C/pCKm9IDkkR48;_72mFrmF*80EA$cBT<nd-qt20F4@2GMPcB#.cEh?T%af[oN6Rqh'M+n9*TW((H'bueCRQVCb*V3;pq;BoaT'@b9/b!KU(94Y-ka@D/p2"?NbX4=kSOl+`Y#IQ&=XY=Bg)=sWsIrR4!"8?@[Aud'>pXXp\i^hlOaMZsSY3W(LT5-oCd70&oi[:5N0l30fQ0i03IXtV,`p&=6,VZmD&K&\@/QfF[EbiJ9=ZpK+R`dIi18ETSVHDGK<>e>fpHq-df]eah5=7g$'mB]=f)~>endstream
endobj
24 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 1326
>>
stream
Gau0CD0+E#&H9tYf]acRYn*BNKLD`enI'6)\fh)K1MuAu%^]Ws6;jTm,k:ZEGBK'na+cVYL(h/3gLm/7HWG+4kfqZPBF8_;6K=L:f>/I02jk!?/Tfe%k8H!*/Y-%)mVlhs>DtY06gJESl'C0]S%oG:1E7F&A)o:6-8jM@YIJSMj?a)W\]DO(Tmh\4JDQ%#-p]!g@PRC?!Q-=T@sA(C4h,EN(a&"U$]:X]6K&j]i354Z688ir'J3D62AMH@0c7f.KHUbG]`j]+;a8Le5qmOV?0*b`)HaGQ0K:&M?;\]oL!s'^Dr14M4Vp:pR/!&_%*l/cs585_8)5U:&JY(4q11L4!Y?OY9&*B.d)6'B8`i7Q4bB1K_E-9Ab@&SlZO59,ntL`\Z1gnN2.9g$Nhs."'/i>f-A40:s-RtnfsD00$7%pIGbZ7^`QE$JO-U`1N/_TTS35m29L6A>!7'LL9JMZe"r18^>(%*=WJ!e2KG<g*U*o^B`*E'MdOZ/$OlFe/eE-+LNK\$:W#%[I.#*,eFR@B3jM9T!"a<dmbk$rh576.AmdWk>nC'L:@kili=-QQ27.>9^$(_Y#P%&qdnoFL,5F_kD0DKOl6EMGp'n$39b_T-RCP/UY*fn#P:4Y,(:Kp$T73\Kdh_p0SI>XD:8ED%8Jq8FOYStc4.X*2<gh7p,MZis#(c.4&r.":Ao*a]-VhF5k>f?S9bX?rFpp&T[F;@G/D,f1aVr,VHLTg8$$NG</]e%t]bL4Gg<NSBe-)SMg]jI!umSrd'2+H!%2TKrGV"Vl&AMesR8f<OV[RfiEKMHlnH`<g42T8KJ=?.0J'i1\?`%dC9A.t>/*E*Q*8EHpr6]Y58<ROJ`43&iEqCIL:n?AotFoWs0?.mK9U_oD'c+g\aoksXA&JTm_qYDs._e^C_I<Vag4i.M&o*l)je:'YP8Amj04Lri=8<5a%08[SbOXDX0;dq^0U(:p=KP(JWGO/ACl0Re<L3[==3]gQl"/rM.FqReIQbC\TgOTmkgRHANWq1HV"OS4KCe2`AFs%-#WsPaG]'kO`\F0&_LW`8u$R.,:7,@W[j1^5j\"1]L8T/1'iKjK3L:sUh7g%WlbSBbBr*K^.q.d/l&fdq"I><m4iN!BIO<f7P,$k#E.n]iu;_>(qXIVer"@J?2J.BL/@o_bmn3H.:VPH?;RCo;f6kJETHDFLW6kclPLr5Iql[UZ`MeQ@1^[Q9H0+;/;1>USU`9<4ba9DNTaL-!cYJeV'D'oQu[gFtHb#7HF;I?"WBtjuE;"QdP<8#kl*1*$2KF+&?^Q^(qQ?C//@:*PX7LpSi.^qhPCB"@Fa_kV~>endstream
endobj
25 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 749
>>
stream
Gat$ubAQ&g&A7lj#.[-9$TXGYE/m5J[Qp=jZ-Z/""(eTG`Y,84PhXOQMO#?(;?V[RMd"EO/=3[Y\E$/5haI]RVs*"FL]lrT@MfW<I`>+9HgHS!3DST(1n!oD15s1o4QS]^k!ZK;RW;R80kumSSS77sEQNIqltm+u,0"og#`DTCp?0N@L:!0`oB8,A79N(u%%)>)$+qVApK1"XQXh,Da]1(1\3TljUi-ej0iIl.Tlf+8ClGX?#obC+E<$GWHm,dU=b>P+h%jmCYK1\aQ-u[qZ:&^a0T9ue^7r+3g-Xa-D%%/T\)B-E[AdRa<33@"@h;;l#T22`Ha[ji/AW^;jSH[QR]KCN&0>l30s9%o[s*O%GI*e9P`L?`.GR#GgqH.[eMk&I>KW5-Ka7A)U4;*k`43(<:EMWuFE,87)i^\.HpqVl&T5<6h2]$DY&)ZQ0JEN3jZ>[[fjuuLX4AmV!sXB@i3E#pZTPXaBV!_h9csXEbI4"#4t>sUlSXYkacYWthXQ)"GV1<VI%\\AC5nl,apNFAcFR;/`trTpp=PIBDhlU"]]C3\03XE\=141hr,lhkfC\M,b/ca_jQkFaotH_2<QH:%MV*467,\C(K#g/TTX_F=#iF*/2Pm$9?#dno>#@@\h42cB;c!2W,Qf#hG4;Ja%4?tt`i\B#2[\;@s2ik#_E;t2g(`qU'g,2<U+,iKX_ZM;16_XNP=rC-N3kUh&e>[G#dd\;<8R'?/^Sbl:Yh&f?Wd5L9E~>endstream
endobj
26 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 804
>>
stream
Gatm89lldX&A@sBCi?,o&51ZqjOR)^kolTm)oa74"DimQOGFHfgL&i,Nap*O'5(Jc&.H2>]j74q_2qFSms,B3.>=fU$lFcl%bN0L6$`X>FRUtl:QZ;3:(j-PLrXMVP"@ucoJttG*fn;P7ake1E!VZ+5JWQe_IP_bBh]9]\>SQZ2\:B(A@k04>sPuSoR5'\K?du.dF:DRXN(EfoGg-%fT7pFfO+AaNAe'Zo9GBMX?#!(ao("$^FkJfhr/ta*[a,I)Uio^F.AO#!=Q_"DA<=V\OGHg)Gs*Y5hsna6N%V+_$iT-(7#50U4Z&E@::HcO;\H/KrpF)$OQSO`EXfo#U^4S_+0,(lK^S7T=Rc[^+<A+_qYSr6;<fuTb'll,F<`"r[Pk\L30e!)g&$P,Fn&$&Ds:L[1E[)(m"BFgmPRi6B,h48s8.l-5Aj\-o&'!XO*Qt#Sr,1HJ5@YA@hU9Y=>)]E`Tt"GVl5-eYj0Klad-TF`28>L4.)Shhl^$pcmR2)&<*@\Ee8)UsW^0LXI3QGJSPu:A(lW2@X2;$)f>Z]i"&-*mq.OZ9[a5iPWKm4lYb]+#i]9U#!X-n2Y=@-3=Ng0mESN\a:;tM&)ciQke$e]B+kZH%)Ja:=dqi/o,MYS%d5_PSmMB=`#VS-FY]):,Sdke[XL/lE]kH7U<;i7Y#+oj&c2N(JR8da@P[Rll[$@Xe[Sp0EgFpOH&:ap)PILF25M$FreKHE6R)c)E-mn[6J\`D4<_4mXAH5eB4#?E_3A`Aa=0iM+aL9RUIETXXb<"/)gC?.LbTdHR6Sf0.`s<qZ~>endstream
endobj
xref
0 27
0000000000 65535 f
0000000061 00000 n
0000000122 00000 n
0000000229 00000 n
0000000341 00000 n
0000000546 00000 n
0000000751 00000 n
0000000856 00000 n
0000000971 00000 n
0000001176 00000 n
0000001381 00000 n
0000001587 00000 n
0000001793 00000 n
0000001999 00000 n
0000002205 00000 n
0000002411 00000 n
0000002481 00000 n
0000002762 00000 n
0000002875 00000 n
0000003663 00000 n
0000004526 00000 n
0000005758 00000 n
0000006753 00000 n
0000008141 00000 n
0000008706 00000 n
0000010124 00000 n
0000010964 00000 n
trailer
<<
/ID
[<136ccfea98a4fe3d25db4179196a9d43><136ccfea98a4fe3d25db4179196a9d43>]
% ReportLab generated PDF document -- digest (opensource)
/Info 16 0 R
/Root 15 0 R
/Size 27
>>
startxref
11859
%%EOF

View File

@@ -1,551 +0,0 @@
# Les 8 — Live Coding Guide
## Van In-Memory naar Supabase
> **Jouw spiekbriefje.** Dit bestand staat op je privéscherm. Op de beamer draait Cursor.
---
## DEEL 1: Live Coding (09:1010:15)
### Stap 1: npm install
```bash
npm install @supabase/supabase-js
```
Docent zegt: "Dit geeft ons de JavaScript client."
### Stap 2: .env.local toevoegen
Open Supabase Dashboard → Settings → API Keys
Copy deze 2:
```
NEXT_PUBLIC_SUPABASE_URL=https://[project].supabase.co
NEXT_PUBLIC_SUPABASE_ANON_KEY=eyJhbGc...
```
Plak in `.env.local`
**BELANGRIJK:** Dev server herstarten! (`npm run dev`)
---
### Stap 3: lib/supabase.ts
```typescript
import { createClient } from "@supabase/supabase-js";
export const supabase = createClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
);
```
Docent zegt: "Dit is onze Supabase client. Eenmalig aanmaken, dan overal gebruiken."
---
### Stap 4: types/index.ts
```typescript
export interface Poll {
id: number;
question: string;
created_at: string;
options: Option[];
}
export interface Option {
id: number;
poll_id: number;
text: string;
votes: number;
created_at: string;
}
```
Docent zegt: "Types matchen onze database schema."
---
### Stap 5: lib/data.ts (complete rewrite)
Laat EERST het oude code zien:
```typescript
// OUD
const polls = [
{ question: "...", options: ["...", "..."], votes: [0, 0] }
];
export function getPolls() {
return polls;
}
```
Dan: "Dit vervangen we door Supabase queries."
```typescript
import { supabase } from "./supabase";
import { Poll } from "@/types";
export async function getPolls(): Promise<Poll[]> {
const { data, error } = await supabase
.from("polls")
.select("*, options(*)");
if (error) {
console.error("Error fetching polls:", error);
return [];
}
return data || [];
}
export async function getPollById(id: number): Promise<Poll | null> {
const { data, error } = await supabase
.from("polls")
.select("*, options(*)")
.eq("id", id)
.single();
if (error) {
console.error("Error fetching poll:", error);
return null;
}
return data;
}
export async function votePoll(optionId: number): Promise<boolean> {
const { error } = await supabase.rpc("vote_option", { option_id: optionId });
if (error) {
console.error("Error voting:", error);
return false;
}
return true;
}
```
Docent tips:
- `.select("*, options(*)")` = Haal polls én hun opties op
- `.eq("id", id)` = WHERE clausa
- `.single()` = Verwacht exact 1 resultaat
- `await` = Dit is asynchroon!
---
### PAUZE VOOR SLIDE 6: Server vs Client: Wie doet wat?
**TOON DEZE SLIDE VOOR COMPONENT AANPASSINGEN**
Docent zegt: "Nu gaan we componenten aanpassen. Eerst: dit patroon!"
```typescript
// Server Component
export default async function HomePage() {
const polls = await getPolls();
return <>{...}</>
}
// Client Component
'use client'
export function VoteForm() {
const [voted, setVoted] = useState(false);
return <>{...}</>
}
```
---
### Stap 6: app/page.tsx (Server Component)
```typescript
import { getPolls } from "@/lib/data";
import Link from "next/link";
import PollItem from "@/components/PollItem";
export default async function HomePage() {
const polls = await getPolls();
return (
<div className="w-full max-w-2xl mx-auto p-6">
<h1 className="text-3xl font-bold mb-6">Huidige Polls</h1>
<Link href="/create" className="text-blue-600 hover:underline mb-6 block">
+ Nieuwe Poll
</Link>
<div className="space-y-4">
{polls.map((poll) => (
<PollItem key={poll.id} poll={poll} />
))}
</div>
</div>
);
}
```
Docent zegt: "Dit is nu async! Direct await op getPolls(). Link naar /create al meteen toevoegen."
---
### Stap 7: components/PollItem.tsx (Option type, percentage bars)
```typescript
'use client'
import Link from "next/link";
import { Option } from "@/types";
interface PollItemProps {
poll: {
id: number;
question: string;
options: Option[];
};
}
export default function PollItem({ poll }: PollItemProps) {
const totalVotes = poll.options.reduce((sum, opt) => sum + opt.votes, 0);
return (
<div className="border rounded p-4">
<h2 className="text-xl font-semibold mb-4">{poll.question}</h2>
<div className="space-y-2">
{poll.options.map((option) => {
const percentage = totalVotes > 0 ? (option.votes / totalVotes) * 100 : 0;
return (
<Link key={option.id} href={`/poll/${poll.id}`}>
<div className="flex items-center gap-2 cursor-pointer hover:opacity-80">
<div className="flex-1 bg-gray-200 rounded h-8 overflow-hidden">
<div
className="bg-blue-600 h-full transition-all"
style={{ width: `${percentage}%` }}
/>
</div>
<span className="text-sm font-medium w-20">
{option.text} ({option.votes})
</span>
</div>
</Link>
);
})}
</div>
</div>
);
}
```
Docent zegt: "Nu hebben we Option type. Percentage bars visueel!"
---
### Stap 8: components/VoteForm.tsx (Client Component)
```typescript
'use client'
import { useState } from "react";
import { votePoll } from "@/lib/data";
import { Option } from "@/types";
interface VoteFormProps {
options: Option[];
}
export default function VoteForm({ options }: VoteFormProps) {
const [loading, setLoading] = useState(false);
const [voted, setVoted] = useState(false);
const handleVote = async (optionId: number) => {
setLoading(true);
const success = await votePoll(optionId);
if (success) {
setVoted(true);
}
setLoading(false);
};
if (voted) {
return <p className="text-green-600">Dank je voor je stem!</p>;
}
return (
<div className="space-y-2">
{options.map((option) => (
<button
key={option.id}
onClick={() => handleVote(option.id)}
disabled={loading}
className="w-full bg-blue-600 text-white p-2 rounded hover:bg-blue-700 disabled:opacity-50"
>
{option.text}
</button>
))}
</div>
);
}
```
Docent zegt: "'use client' bovenaan. useState werkt. onClick handlers werken. After vote: feedback!"
---
### Stap 9: app/poll/[id]/page.tsx
```typescript
import { getPollById } from "@/lib/data";
import VoteForm from "@/components/VoteForm";
import { notFound } from "next/navigation";
export default async function PollPage({ params }: { params: { id: string } }) {
const poll = await getPollById(parseInt(params.id));
if (!poll) {
notFound();
}
return (
<div className="w-full max-w-md mx-auto p-6">
<h1 className="text-2xl font-bold mb-6">{poll.question}</h1>
<VoteForm options={poll.options} />
</div>
);
}
```
Docent zegt: "Server Component haalt data. Geeft VoteForm (Client) de options."
---
### Stap 10: app/api/polls/[id]/route.ts
```typescript
import { getPollById, votePoll } from "@/lib/data";
import { NextRequest, NextResponse } from "next/server";
export async function GET(
request: NextRequest,
{ params }: { params: { id: string } }
) {
const poll = await getPollById(parseInt(params.id));
if (!poll) {
return NextResponse.json({ error: "Not found" }, { status: 404 });
}
return NextResponse.json(poll);
}
export async function POST(
request: NextRequest,
{ params }: { params: { id: string } }
) {
const { optionId } = await request.json();
const success = await votePoll(optionId);
return NextResponse.json({ success });
}
```
---
### Stap 11: TESTEN
- http://localhost:3000 → Alle polls
- Click poll → Detail pagina
- Stem → Votes incrementen
- Controleer Supabase dashboard → votes kolom wijzigt
---
## DEEL 2: Zelf Doen — /create pagina (10:3011:30)
### Theorie op Beamer (15 min)
**Toon INSERT query uitleggen:**
```typescript
// 1. Insert poll → krijg ID terug
const { data: poll } = await supabase
.from("polls")
.insert({ question: "Wat is je favoriete taal?" })
.select()
.single();
// poll.id = 42
// 2. Insert options
await supabase.from("options").insert([
{ poll_id: 42, text: "JavaScript", votes: 0 },
{ poll_id: 42, text: "Python", votes: 0 },
{ poll_id: 42, text: "Rust", votes: 0 }
]);
```
**Docent zegt:**
- ".insert() = INSERT"
- ".select().single() = geef terug wat je insertde"
- "poll.id gebruiken voor options"
- "Meerdere rows in [{}] array"
- "Dan router.push('/') terug naar home"
---
### RLS Policy (SQL Editor in Supabase)
**Docent laat dit zien:**
```sql
-- INSERT policy voor polls
CREATE POLICY "Allow public insert on polls"
ON polls FOR INSERT
TO anon
WITH CHECK (true);
-- INSERT policy voor options
CREATE POLICY "Allow public insert on options"
ON options FOR INSERT
TO anon
WITH CHECK (true);
```
**Docent zegt:**
"Dit zegt: Iedereen mag INSERT-en. Zonder dit: RLS policy violation."
---
### Reference Code: app/create/page.tsx
Toon dit op beamer als hulp:
```typescript
'use client'
import { supabase } from "@/lib/supabase";
import { useRouter } from "next/navigation";
import { useState } from "react";
export default function CreatePoll() {
const [question, setQuestion] = useState("");
const [options, setOptions] = useState(["", ""]);
const [loading, setLoading] = useState(false);
const router = useRouter();
const addOption = () => setOptions([...options, ""]);
const updateOption = (index: number, value: string) => {
const newOptions = [...options];
newOptions[index] = value;
setOptions(newOptions);
};
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setLoading(true);
// 1. Insert poll
const { data: poll, error: pollError } = await supabase
.from("polls")
.insert({ question })
.select()
.single();
if (pollError || !poll) {
console.error("Error creating poll:", pollError);
setLoading(false);
return;
}
// 2. Insert options
const optionRows = options
.filter((opt) => opt.trim() !== "")
.map((opt) => ({
poll_id: poll.id,
text: opt,
votes: 0,
}));
const { error: optionsError } = await supabase
.from("options")
.insert(optionRows);
if (optionsError) {
console.error("Error creating options:", optionsError);
setLoading(false);
return;
}
router.push("/");
};
return (
<div className="w-full max-w-md mx-auto p-6">
<h1 className="text-2xl font-bold mb-6">Nieuwe Poll</h1>
<form onSubmit={handleSubmit} className="space-y-4">
<div>
<label className="block text-sm font-medium mb-1">Vraag</label>
<input
type="text"
value={question}
onChange={(e) => setQuestion(e.target.value)}
className="w-full p-2 border rounded"
placeholder="Stel je vraag..."
required
/>
</div>
{options.map((option, index) => (
<div key={index}>
<label className="block text-sm font-medium mb-1">
Optie {index + 1}
</label>
<input
type="text"
value={option}
onChange={(e) => updateOption(index, e.target.value)}
className="w-full p-2 border rounded"
placeholder={`Optie ${index + 1}`}
required
/>
</div>
))}
<button
type="button"
onClick={addOption}
className="text-blue-600 text-sm hover:underline"
>
+ Optie toevoegen
</button>
<button
type="submit"
disabled={loading}
className="w-full bg-blue-600 text-white p-2 rounded hover:bg-blue-700 disabled:opacity-50"
>
{loading ? "Bezig..." : "Poll aanmaken"}
</button>
</form>
</div>
);
}
```
---
### Docent Loop Ronde Timing
- **Min 0-5:** Iedereen aan het werk?
- **Min 15:** RLS policy check. Help vastlopen studenten.
- **Min 25:** Toon useState setup snippet.
- **Min 30:** Eerste werkende insert check. Toon in Supabase dashboard.
- **Min 45:** Finalisatie + vragen.
---
### Veelvoorkomende Problemen
| Probleem | Oplossing |
|----------|-----------|
| "RLS policy violation" | Policy toegevoegd in dashboard? |
| "poll is undefined" | .select().single() vergeten? |
| "Form refresh pagina" | e.preventDefault()? |
| "Redirect werkt niet" | useRouter import juist? next/navigation? |
| "Options fout" | Spread operator [...options] gebruiken? |
| "Votes niet updatend" | Supabase RLS blocking? Check policy. |
---
## Timing Summary
- **09:0009:10:** Welkom + Slide 1, 2, 3
- **09:1010:15:** Live Coding (Stap 111) + Slide 6 halverwege
- **10:1510:30:** Pauze (Slide 7)
- **10:3011:30:** Zelf Doen + Theorie (Slide 8)
- **11:3011:45:** Vragen
- **11:4512:00:** Huiswerk + Afsluiting (Slide 9, 10)

View File

@@ -1,169 +0,0 @@
# Les 8 — Slide-overzicht
## Van In-Memory naar Supabase (10 slides)
---
## Slide-indeling
### Slide 1: Titelslide
**Titel:** Les 8 — Van In-Memory naar Supabase
**Ondertitel:** Koppelen van Supabase aan Next.js
**Afbeelding:** Supabase + Next.js logo's
---
### Slide 2: Terugblik vorige les
**Titel:** Terugblik — Waar waren we?
**Bullets:**
- Stemmen werkt lokaal (in-memory data)
- QuickPoll app heeft 2 pages: / en /poll/[id]
- VoteForm component ziet stemmen onmiddellijk
- Nu: alles naar een echte database
**Code snippet (links):**
```javascript
// OUD
const polls = [
{ question: "...", options: [...], votes: [...] }
];
```
---
### Slide 3: Planning vandaag
**Titel:** Planning — Les 8 (3 uur)
**Timeline:**
- 09:00-09:10 | Welkom & Terugblik (10 min)
- 09:10-10:15 | **DEEL 1: Live Coding — Supabase koppelen** (65 min)
- 10:15-10:30 | Pauze (15 min)
- 10:30-11:30 | **DEEL 2: Zelf Doen — /create pagina** (60 min)
- 11:30-11:45 | Vragen & Reflectie (15 min)
- 11:45-12:00 | Huiswerk & Afsluiting (15 min)
---
### Slide 4: Van Array naar Database
**Titel:** Van In-Memory Array naar Supabase
**Links:** In-memory (OUD)
```javascript
const polls = [
{ question: "Favoriete taal?",
options: ["JS", "Python"],
votes: [10, 5]
}
];
```
**Rechts:** Supabase Database (NIEUW)
```
polls tabel
├─ id (1)
├─ question ("Favoriete taal?")
└─ options[] (relatie)
options tabel
├─ id (1)
├─ poll_id (1)
├─ text ("JS")
├─ votes (10)
```
---
### Slide 5: Live Coding Deel 1 — Supabase × Next.js
**Titel:** Live Coding — Deel 1: Supabase koppelen
**Ondertitel:** Stap-voor-stap
**Stappen:**
1. npm install @supabase/supabase-js
2. .env.local (API keys)
3. lib/supabase.ts (client)
4. types/index.ts (Poll + Option)
5. lib/data.ts (queries herschrijven)
6. app/page.tsx (Server Component)
7. components/PollItem.tsx (percentage bars)
8. components/VoteForm.tsx (Client Component)
9. app/poll/[id]/page.tsx (detail)
10. app/api/polls/[id]/route.ts (API)
11. Testen!
**Spreaker:** "We werken samen naar een werkende Supabase integratie."
---
### Slide 6: Server vs Client: Wie doet wat?
**Titel:** Server vs Client: Wie doet wat?
**Twee kolommen:**
**SERVER Component:**
- `export default async function HomePage() { ... }`
- `const polls = await getPolls()`
- Data fetching
- Direct naar database
- TypeScript compile-time
**CLIENT Component:**
- `'use client'`
- `const [voted, setVoted] = useState(...)`
- Interactief: klikken, typen, formulieren
- useEffect, event handlers
- Browser runtime
**Zeg:** "Server haalt data, Client maakt het interactief."
---
### Slide 7: Pauze
**Titel:** Pauze
**Tekst:** Supabase is gekoppeld! Na de pauze: /create pagina bouwen
**Icoon:** Koffie/pauze emojis
---
### Slide 8: Zelf Doen — /create pagina bouwen
**Titel:** Zelf Doen
**Ondertitel:** /create pagina bouwen
**Stappen:**
1. **RLS INSERT policy** toevoegen in Supabase dashboard
2. **Form bouwen** met vraag + minimaal 2 opties
3. **Insert logica:** Eerst poll, dan options met poll_id
4. **Redirect** naar homepage na succes
5. **Link toevoegen** op homepage naar /create
**Docent zegt:** "Zelf doen, 60 minuten. Ik loop rond!"
---
### Slide 9: Huiswerk
**Titel:** Huiswerk
**Verplicht:**
- /create pagina afmaken (als niet klaar)
- Validatie toevoegen (vraag niet leeg, min 2 opties)
**Extra:**
- Delete functionaliteit
- SQL queries direct in Supabase testen
- Realtime subscriptions uittesten
- Styling verbeteren
---
### Slide 10: Afsluiting
**Titel:** Tot volgende week!
**Voorkant:**
- "Volgende les: Supabase Auth"
- "Inloggen, registreren"
- "Bepalen wie wat mag doen"
**Achtergrond:** Supabase Auth afbeelding

View File

@@ -0,0 +1,324 @@
# Les 8 — Docenttekst
## Van In-Memory naar Supabase
---
## Lesoverzicht
| Gegeven | Details |
|---------|---------|
| **Les** | 8 van 18 |
| **Onderwerp** | Supabase koppelen aan Next.js |
| **Duur** | 3 uur (09:00 12:00) |
| **Voorbereiding** | Werkend QuickPoll project, Supabase project met polls/options tabellen |
| **Benodigdheden** | Laptop, Cursor/VS Code, browser, Supabase account |
| **Aanpak** | Deel 1-3 klassikaal (docent loopt PDF door met studenten). Deel 4 zelfstandig. |
## Leerdoelen
Na deze les kunnen studenten:
1. De Supabase JavaScript client installeren en configureren
2. Environment variables gebruiken voor API keys
3. Data ophalen via Supabase queries (select met relaties, eq, single)
4. Het verschil uitleggen tussen Server en Client Components
5. Een formulier bouwen dat data INSERT in Supabase
---
## Lesplanning
### 09:0009:15 | Welkom & Intro (15 min)
📌 Slide 1, 2, 3
**Doel:** Studenten welkom heten, plan uitleggen, en PDF uitdelen.
**Wat te zeggen:**
- "Vorige week hebben we een werkend polling app gebouwd met in-memory data."
- "Vandaag koppelen we Supabase: onze database-as-a-service."
- "Jullie krijgen een PDF met de volledige lesopdracht. We lopen Deel 1 t/m 3 samen door. Deel 4 doen jullie zelfstandig."
- "In de PDF staan grijze blokken die je kunt copy-pasten, en TODO-blokken die je zelf moet invullen."
**Check vooraf:**
- Iedereen heeft Supabase account met polls en options tabellen
- Iedereen heeft QuickPoll project lokaal runnen op localhost:3000
- Deel de Lesopdracht PDF uit (digitaal)
---
### 09:1509:45 | KLASSIKAAL: PDF Deel 1 — Setup (30 min)
📌 Slide 4 + PDF Deel 1
**Doel:** Samen de setup doorlopen. Iedereen heeft aan het einde: supabase client geïnstalleerd, .env.local, supabase.ts, en types.
**Toon Slide 4** — Van Array naar Database. Leg uit:
- "Tot nu toe sloegen we data op in een array. Dat verdwijnt bij restart."
- "Supabase geeft ons een echte PostgreSQL database in de cloud."
- Toon links de oude array, rechts de database structuur.
**Open de PDF bij Deel 1 en loop stap voor stap door:**
#### Stap 1.1 — npm install
- "Open je terminal, voer uit: `npm install @supabase/supabase-js`"
- Wacht tot iedereen klaar is
- "Herstart je dev server!"
#### Stap 1.2 — .env.local
- "Open Supabase dashboard → Settings → API"
- "Kopieer je Project URL en anon key"
- "Maak `.env.local` aan in de root van je project"
- Laat ze het invullen, loop rond en check
- **Let op:** herstart dev server na aanmaken .env.local!
#### Stap 1.3 — lib/supabase.ts
- "Dit is onze client — zo praat je app met Supabase"
- Laat ze de code copy-pasten uit de PDF
- Leg uit: `createClient` maakt de verbinding, `process.env.NEXT_PUBLIC_...` leest de env vars
#### Stap 1.4 — types/index.ts
- "Dit zijn de TypeScript types die matchen met onze database tabellen"
- Laat ze copy-pasten
- Wijs op: `id: number`, `options: Option[]` (de relatie)
#### Stap 1.5 — vote_option SQL functie aanmaken
- "Voordat we kunnen stemmen, hebben we een PostgreSQL functie nodig in Supabase."
- "Open Supabase dashboard → SQL Editor"
- Laat ze deze SQL uitvoeren (staat in PDF):
```sql
create or replace function public.vote_option(option_id bigint)
returns void
language sql
security definer
as $$
update public.options
set votes = votes + 1
where id = option_id;
$$;
```
- **Leg uit:** "Dit is een database functie. We roepen 'm straks aan met `supabase.rpc('vote_option', { option_id })`. Een RPC = Remote Procedure Call — je voert PostgreSQL code uit vanuit je app."
- **Waarom?** "We hadden ook een gewone UPDATE kunnen doen, maar met een functie hou je de logica in de database. Volgende les met Auth gaan we deze functie uitbreiden."
- **Let op:** "Zonder deze functie krijg je later een PGRST202 error bij het stemmen — 'Could not find the function public.vote_option'."
**Check:** "Heeft iedereen de setup af? Geen errors? Handen omhoog als je klaar bent."
---
### 09:4510:00 | KLASSIKAAL: PDF Deel 2 — Supabase Queries (15 min)
📌 Slide 5 + PDF Deel 2
**Doel:** Studenten begrijpen de queries en vullen de TODO-blokken in lib/data.ts in.
**Toon Slide 5** — Supabase Queries. Leg de vier operaties uit:
1. **SELECT alles:** `.from("polls").select("*, options(*)")` — de `*` haalt alles op, `options(*)` volgt de relatie
2. **SELECT een:** `.eq("id", 5).single()` — filter + verwacht 1 resultaat
3. **INSERT:** `.insert({ question }).select().single()` — maak nieuw record, krijg het terug
4. **RPC:** `.rpc("vote_option", { option_id })` — roep een database function aan
**Tip:** Schrijf deze vier queries op het whiteboard. Studenten kijken hier de rest van de les naar.
**Open de PDF bij Deel 2 — Stap 2.1:**
- "Vervang de inhoud van `lib/data.ts`. De imports staan er al."
- "Nu de TODO-blokken. Laten we de eerste samen doen."
**getPolls() — doe samen voor:**
```typescript
const { data, error } = await supabase
.from("polls")
.select("*, options(*)");
if (error) {
console.error(error);
return [];
}
return data || [];
```
- "Zie je? `.from("polls")` kiest de tabel, `.select("*, options(*)")` haalt alles op inclusief de relatie."
**getPollById() — laat ze zelf proberen (2 min), loop dan door:**
```typescript
const { data, error } = await supabase
.from("polls")
.select("*, options(*)")
.eq("id", id)
.single();
if (error) return null;
return data;
```
- "`.eq("id", id)` filtert op 1 specifieke poll. `.single()` zegt: ik verwacht 1 resultaat."
**votePoll() — laat ze zelf proberen (2 min), loop dan door:**
```typescript
const { error } = await supabase
.rpc("vote_option", { option_id: optionId });
if (error) {
console.error(error);
return false;
}
return true;
```
- "`.rpc()` roept een PostgreSQL function aan die we eerder hebben aangemaakt."
**Check:** "Heeft iedereen alle drie de functies ingevuld?"
---
### 10:0010:15 | KLASSIKAAL: PDF Deel 3 — Componenten (15 min)
📌 Slide 6 + PDF Deel 3
**Doel:** Studenten copy-pasten de vier componenten uit de PDF en testen de app.
**Toon Slide 6** — Server vs Client. Leg kort uit:
- "Server Components: async, kunnen `await getPolls()` doen. Draaien op de server."
- "Client Components: `'use client'` bovenaan, kunnen `useState`/`onClick` gebruiken. Draaien in de browser."
- "Vuistregel: Server haalt data, Client maakt het interactief."
**Open de PDF bij Deel 3 en loop door:**
#### Stap 3.1 — app/page.tsx
- "Dit is de homepage. Let op: `async function` en `await getPolls()` — dit is een Server Component."
- "Copy-paste uit de PDF."
- "De `<Link href="/create">` link werkt straks na Deel 4."
#### Stap 3.2 — components/PollItem.tsx
- "Dit is een Client Component — zie `'use client'` bovenaan."
- "Het berekent percentages en toont bars. Copy-paste."
#### Stap 3.3 — components/VoteForm.tsx
- "Nog een Client Component. Hier wordt `votePoll()` aangeroepen — de functie die jullie net geschreven hebben!"
- "Copy-paste."
#### Stap 3.4 — app/poll/[id]/page.tsx
- "De detailpagina. Weer een Server Component met `await getPollById()`."
- "Copy-paste."
**Test samen:**
- "Open http://localhost:3000 — zien jullie polls?"
- "Klik op een poll — kun je stemmen?"
- "Check in Supabase dashboard: stijgt het aantal votes?"
**Troubleshooting als het niet werkt:**
- Lege pagina → RLS SELECT policy mist
- `Cannot find module` → check import paths
- Stemmen werkt niet → vote_option RPC functie mist
---
### 10:1510:30 | Pauze (15 min)
📌 Slide 7
**Zeg voor de pauze:** "Na de pauze gaan jullie zelfstandig Deel 4 doen: de /create pagina. De volledige UI staat in de PDF — jullie schrijven alleen de INSERT query."
---
### 10:3010:45 | Uitleg INSERT + start Deel 4 (15 min)
📌 Slide 8 + PDF Deel 4 intro
**Doel:** INSERT concept uitleggen voordat ze zelfstandig aan de slag gaan.
**Wat te zeggen:**
- "Het formulier staat compleet in de PDF (Stap 4.3). Jullie hoeven alleen de handleSubmit functie in te vullen."
- "Maar eerst: Stap 4.1 — de RLS policy. Zonder INSERT policy blokkeert Supabase je."
**Loop door Stap 4.1 (RLS policy):**
- "Open Supabase dashboard → SQL Editor"
- "Voer de twee CREATE POLICY statements uit de PDF uit"
- "Dit is tijdelijk — volgende les beperken we dit met Auth"
**Leg Stap 4.2 (INSERT theorie) uit:**
- "Er zijn twee INSERT stappen:"
1. Insert de poll: `.from("polls").insert({ question }).select().single()` → je krijgt de poll met id terug
2. Insert de options: `.from("options").insert(options.map(...))` → gebruik het `poll.id` van stap 1
- Toon op whiteboard:
```
1. INSERT poll → { id: 5, question: "..." }
2. INSERT options → [{ poll_id: 5, text: "A" }, { poll_id: 5, text: "B" }]
3. router.push("/") → terug naar homepage
```
**Zeg:** "Nu zijn jullie aan de beurt. Stap 4.3 in de PDF: copy-paste het hele bestand, en vul het TODO-blok in. Ik loop rond."
---
### 10:4511:30 | ZELFSTANDIG: PDF Deel 4 — /create pagina (45 min)
📌 Studenten werken met PDF Stap 4.3
**Wat ze doen:**
1. `app/create/page.tsx` aanmaken met de code uit de PDF (Stap 4.3)
2. handleSubmit TODO-blok invullen (de INSERT logica)
3. Testen: poll aanmaken → verschijnt op homepage
**Jouw rol — loop rond en help:**
Typische problemen:
- RLS INSERT policy vergeten → "new row violates row-level security" → Stap 4.1 niet gedaan
- `options` niet gekoppeld aan poll → `poll_id` vergeten in de insert
- `router.push` werkt niet → check `import { useRouter } from "next/navigation"` (niet `"next/router"`)
- Form submit herlaadt pagina → `e.preventDefault()` check
- `poll` is undefined na insert → `.select().single()` vergeten
**Check-in na 15 min:**
- "Wie heeft de RLS policy al toegevoegd?"
- "Wie kan al een poll aanmaken?"
**Check-in na 30 min:**
- "Wie heeft een werkende /create pagina?"
- Help studenten die vastlopen
**Snelle studenten:**
- Validatie toevoegen (vraag niet leeg, min 2 opties)
- Styling verbeteren met Tailwind
- Delete functionaliteit bouwen
---
### 11:3012:00 | Vragen + Huiswerk (30 min)
📌 Slide 9, 10
**Vragen beantwoorden:**
- Open ronde: waar liepen jullie tegenaan?
- Concepten herhalen die onduidelijk waren
- Eventueel: laat een werkende /create pagina zien van een student
**Huiswerk bespreken (Slide 9):**
Verplicht:
- /create pagina afmaken (als niet klaar)
- Validatie toevoegen (vraag niet leeg, min 2 opties)
Extra:
- Delete functionaliteit
- SQL queries direct in Supabase testen
- Realtime subscriptions uittesten
- Styling verbeteren
**Vooruitblik (Slide 10):**
- "Volgende week: Supabase Auth"
- "Inloggen, registreren, en bepalen wie wat mag doen"
---
## Troubleshooting Overzicht
| Probleem | Oorzaak | Oplossing |
|----------|---------|-----------|
| `NEXT_PUBLIC_SUPABASE_URL is undefined` | .env.local niet geladen | Dev server herstarten |
| Lege pagina, geen polls | RLS policy mist | SELECT policy toevoegen in Supabase |
| "new row violates row-level security" | INSERT policy mist | INSERT policy toevoegen |
| `Cannot find module '@/types'` | Import path fout | Check tsconfig.json paths |
| `PGRST202 Could not find the function public.vote_option` | vote_option SQL functie mist | Stap 1.5 uitvoeren in SQL Editor |
| Options verschijnen niet bij poll | Foreign key mismatch | Check poll_id in options tabel |
| Stemmen werkt niet | RPC functie mist | vote_option function aanmaken in SQL editor |
| Form submit herlaadt pagina | preventDefault mist | `e.preventDefault()` in handleSubmit |
| poll is undefined na insert | .select().single() mist | Toevoegen aan insert query |
---
## Voorbereiding Checklist
- [ ] Eigen QuickPoll project werkt lokaal
- [ ] Supabase project met polls + options tabellen
- [ ] vote_option RPC functie aangemaakt
- [ ] SELECT RLS policies staan aan
- [ ] Lesopdracht PDF gedeeld met studenten (digitaal)
- [ ] Whiteboard/marker beschikbaar voor queries

View File

@@ -0,0 +1,346 @@
%PDF-1.4
%<25><><EFBFBD><EFBFBD> ReportLab Generated PDF document (opensource)
1 0 obj
<<
/F1 2 0 R /F2 3 0 R /F3 5 0 R /F4 6 0 R /F5 9 0 R /F6 19 0 R
>>
endobj
2 0 obj
<<
/BaseFont /Helvetica /Encoding /WinAnsiEncoding /Name /F1 /Subtype /Type1 /Type /Font
>>
endobj
3 0 obj
<<
/BaseFont /Helvetica-Bold /Encoding /WinAnsiEncoding /Name /F2 /Subtype /Type1 /Type /Font
>>
endobj
4 0 obj
<<
/Contents 25 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 24 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
5 0 obj
<<
/BaseFont /Courier /Encoding /WinAnsiEncoding /Name /F3 /Subtype /Type1 /Type /Font
>>
endobj
6 0 obj
<<
/BaseFont /Helvetica-Oblique /Encoding /WinAnsiEncoding /Name /F4 /Subtype /Type1 /Type /Font
>>
endobj
7 0 obj
<<
/Contents 26 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 24 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
8 0 obj
<<
/Contents 27 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 24 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
9 0 obj
<<
/BaseFont /Courier-Bold /Encoding /WinAnsiEncoding /Name /F5 /Subtype /Type1 /Type /Font
>>
endobj
10 0 obj
<<
/Contents 28 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 24 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
11 0 obj
<<
/Contents 29 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 24 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
12 0 obj
<<
/Contents 30 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 24 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
13 0 obj
<<
/Contents 31 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 24 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
14 0 obj
<<
/Contents 32 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 24 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
15 0 obj
<<
/Contents 33 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 24 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
16 0 obj
<<
/Contents 34 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 24 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
17 0 obj
<<
/Contents 35 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 24 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
18 0 obj
<<
/Contents 36 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 24 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
19 0 obj
<<
/BaseFont /ZapfDingbats /Name /F6 /Subtype /Type1 /Type /Font
>>
endobj
20 0 obj
<<
/Contents 37 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 24 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
21 0 obj
<<
/Contents 38 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 24 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
22 0 obj
<<
/PageMode /UseNone /Pages 24 0 R /Type /Catalog
>>
endobj
23 0 obj
<<
/Author (\(anonymous\)) /CreationDate (D:20260407143958+02'00') /Creator (\(unspecified\)) /Keywords () /ModDate (D:20260407143958+02'00') /Producer (ReportLab PDF Library - \(opensource\))
/Subject (\(unspecified\)) /Title (\(anonymous\)) /Trapped /False
>>
endobj
24 0 obj
<<
/Count 14 /Kids [ 4 0 R 7 0 R 8 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 20 0 R 21 0 R ] /Type /Pages
>>
endobj
25 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 952
>>
stream
Gatm:9iKe#&A@sBCi?#<#YVI>WA?]aThlS0Rc;a'(LrB]`3C?'T``'D/(lXP#/c#QbAqd6]C5.9-59^4;Yq=b!5=2,GX?s:#k*:i`.RO\^4jk\Trk6=Lp*2_L%5KaJ\Z]Zj@'<q3esPaTpf%?FYF3j&s"d!U<2]RXN]b$gaC6E$6?td7R&Q0ces.)eZ8JZ-o/;@/:aKO\Bqh0dTM#!i=*Sa0](ZAPtqUA':_TcLPt>;A&m;kcN6lBiR6hF!E%W7?@_#n=;Z:K6"[AU7YG`WCp@7/(!$R[/rMKaUA2Ij&8Z^.Jq-bYo0<dP&5mW\Lk3-:q%S"U],3R3XG7qf80GSU_0)![=[)hY(Ie!&Gna7t[4\6/BVdd-gKTD@#U'I[q(N5W3eug?Xc/`Y.1M)@Z>DUeVI1o%D?0<N#G]&FRR<gA1i-i\<?7@o:_Zp98oR3M#`HP6kAuut^lPcp_k48T>U8%u7lBK;U(#J_C$7+=!D9^kB6Q3[l=bb1>$%\0OcCO%a*dD859?t\P27fU$<a(H#OXb0hbpIg.r)XWA=0dFV[q8lL/=Yl9qCM=\oK@NK/"f5O%UY.ck7Br4B2%7^T1s<D;=1E]%VAQD.jD@Z/8Qb#].kO,$s`/j>M0/\p7/=3n$*:K#>+H;6?ONVJ.+=%1TcoE-.,G:C:Cr)c.K&pCt#*liE7)N5Ecu9=W6(q-<)A#CAh8D!#M=l#]qkI,E#6n7Ea+4FtU8NT\'6/Ak5*@*>R;l+#'=3W8.9SiK1g!t'4T^J>joi1H3C2W_gX\FPIjY%R/X\?^j>oaQ/T0mL!B^q5SWH$oeO)8fuuA2,P&2cpLg1LN?/*-Oe4[h>Qe1W24()Q)9uX!pp`IC;7$ZYq62SYA_qfXL-r045$/P%pBCkud++ISi:hbG]2rZOIF>Dj+1g*UcR'_ZFsukPUOhm[f^=U":[0$TS.eIO$@u~>endstream
endobj
26 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 1229
>>
stream
Gau0BD0+\p&H9tYf\@ip;iKfsCbk63\`VU>BjM:tQRNqE(aTH(aX7IZ]:8`m8hpm0K>Y54?n;gpXe+?sSm<SFeJFWhS-#5ST_TW0EhIUaaKn"mFl9?&qcH2nbJ:B7aGCJ8r0E&2+B&*dA'51KQKNn^L:/$]?q%6tJ@MX/T4m>R?E0$)^bJ@CCBs,h'9W?16^Pks#5rk.;o0Y'&k4u19_Ju-Tf7PZ>E.`JI5g,)4`dOkCbuElV1A],CkWqHqRR0h.063Zj<QR4]+Sb,/-LCABu)gQ,u,(>"E\8o*o`^V`Z#'.GJN_U33#Bf_!ql(b=naJ69l0G;MGjX%)scDf/"=@YBZ3q-MW84&sV4B/"ACFgD)F=p+"'9d`$q5otrF?bD5t`'a8b2oO*)I0ed9oqWERH'rl,DZ6fjI^,>(S%Gi[gG9JfJ,83/^;AH>`!;r'7dbcTo)O%g#Qqjk(VHZl@I=7,F8Rt*r&!1Yt'O:VNa(#F=92gCUK!><E>OX-T$D^L_<rXkTb;CJbaGH8tENgCX*sVV>Y>`gBN6S?[9M&!r*".XZ$)s"D5<p`L^1M)qbp?H#6\bI+(f.sfBs8NQ\jb9,8pH;Y`KS_K*AD)oj!+VWF,eo#ImNW4`:dL/9#Pnj'k0C#$7`?5MQA@"r2lc"SGgtW>htoncJK)B2chB)-f),Fc%jU7=NW152k(IE]5K6P[S"PD0fSXg"Di7$EcH@Zq!IG:A$[75c5KXPWA18<<61Tcg0F;K&;:OYZIgcWW;f0:8/G#)V)sl-TrVAj:o0D(4QKV'b[cg"h<",NS6VUhE.L=("7GF71VG$7XjgD:lQrS%K%@Y9jVFkBMBYfo;H<rLT(BO>9'GcD03U-g,cV4"EJ*bGU_>fDUuR4`PR^DDY,Lgn@R74Dmn=&G&T.bJiQ]@sn`K%,2F]ag_75<3ROq@i_=9=BrVBE,7CpZ2WA5fGDHb[N*?Y4Kh7H;d+"TSI&!&1ScO'>?GH[Zdmo*7uBXkL_2j3'/,?9T(BJ)0*VWo_n@2$gKF1'38jqLHOWqC/)*l_<N&m(.pnm6*XTU*9>Hiu):_1KLfWRo!k*"t#EJmYsKNiG"\B'GD5UMV>"B-`h.X)=D"U"]$;_a5WD!L>fAH)<aeD7DD]s89K<q7':bbCGI%s!sm=j^_3(:6LCgZ-9a(q+b#K9[7,OqUoQ>,5)BVC]&gZ+OIgi_!R*"OqKnq;EQ7A%3*gr"!`SM-i~>endstream
endobj
27 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 746
>>
stream
Gasal;,<]I&:VB$n^ZCsg31X@a$G]%@^J>K`9QmBR$\%Hh",F"?-orQs*GQB'0X$2Q7uinB(h^4k2UFJinZOt!f%(GIH06,P&'Q;"\;C.S;cl*]_;gt(<0\)0b%U3j"Ro]p=KT?a':_rZ36f,_-K+YVISDhrqqK#"e;nFC;Oh_'IfE<Q:l"3-r$gh+HSHF8lHOpB'6)%DH'Y0JgthE5Ut!H8u0<8_c]EC(WdZ=T"WkDYBJJ,aG%/r9;-ES6(@.!QZ]:>L]OIWHf'6C)NCg_YL?VmcL#q3MU6F::r/kcEr?iEp>TW$-?pB43-;&qJ5*)&Oi#/;I=l4M+r^aS:SA)MHelZe8`kKB11.<Q,C5:/9Tu\3QeJ=<(*]NFaAS^HdY5otI9G$!=g_J#:02XD\:a3nD-#h$1.Nf%WTq=gEd.<1<&[tu9;B^dh1\Zo,79%5>7A,uZ:<%6(NR*QMY(j3%*%=E5aj0@lfk[X@$tlWL2+*W)'&;B4npquLE!P#gBnJ\=RH+G4<Et+4VX).G[2Cl>Wh4RkH.o(J@f+Ur9ei=gYNsuDgIq#>uJ'CL8hSj2%[XMWtad\f^BJ7AuuGHAXC]fPP4El^n7;Yr9[]IJ8s5Q7-A3^EiXol.>1nWk6:"]1f#`9M<B1-XmL5,Ec/9hYBA(gK<+L0JD;RFVA$2\O$+P9H#YpaX["dXCfq`W^>dW;^(<V@B`[:\+?P>.=.BQWV1kf#UEcK:]QY=!0_u%8a\rN~>endstream
endobj
28 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 1400
>>
stream
Gb!SlD,91]&H88.1$+Tba2[FhVc?_og>968*X>QW1tB]f-HUjLL\7*O"T8#>Y&0=u65"UH6j\a7h-&(Jo?9<<i*b#lS+h"/E70O0NuYY3r@r8PH-h$O5P.%h*Ssm&%LZ_njq.A4j)GXZ3SJ(?i.R[4b`-EtG(@Q7rW,%Dcoq2^S,S@O38bViqY`QZVhc3M4ZH/8!a1[[@,O!f>ZC<*R$7m.'hX&=+nRF]J#!2C8?\qCipKE8CS#gQ?#s@71c]Rs!95ujo=nSm"KYr\Qm?X-@/T;_lSDQgL><Gqb#I&1[C<JMMR#pR;I&'dcJTD1W(%%OZmlV630]]<;BYPj)9rT"J4.Oj;)7oL>XEjXXP,e=`rN@2#R#-T5%t.k2Xm%;`7r<0f?nYChKWF#,;Qo;W+6AJj#(,W'Q\gR@3cNqYuF:Yg*'dhYCs7Ee(^4nhb`/Tq#gtBO\Z:.TW`^I2a[a;4sIF76K'/b`<Zp4r=>eJ@$h[1=.$n,+M19U9]S-4Q=9dC%Z0b4fo@"f,fYn2#"2-!lV<&PfCPUqb_OLb('f""H1:T$`8tqXl!H+7=[^<GNBe\X*#ZNW9i5EECQ)PT[QuHVo0Oq:KsE;THPjq8"p_6a/ff=VcnH44@"23ffn8[9?+$&0fbcm\Rnr.WK$',8aredVXd.+plNEI_UE!Bg_n'C9-:nCJ0aMj6!QBGALtqK,ie[`j^luAX:&(W;4Xh1m$KEU3PpgZTc+aKr7"m&:,]%m-?hF'\J(1+*L"l!&Gi!2_;@oND7"G`Q8<3kU@Gle?D8a.%HZV-k#Ns5,@s:lN6+dPThZ+5Z0^qDTk'4U7a-l=K[N?Fg\m<di4h.FJ'p.-m9hlM#;n1-OPc@#$CR=H4nhdC<\]6D+EjF%GQ8#NgAr.-[3+T)PCId9>>a<"1V7&[WmXg"4@/M(m9hNV`<O?f=VKFYuNWt9G[s!B;OrT^lO?F5R]IYgKjss8K&lg6!B\6NP;-%CgAZrMgg';hGXl]0H3rXV+lasU5>ISb(!-L;(pY4/9A+rWSaN<Gtk`LmE;b/uaL`:ZD>=7]J`Ma#e2&i9;:/Kl/N3[71<H6UJq1kueD/U(5Oo80\Ba?BTR#8/a.r;a$RN(l!18XNWN#uN9QU\Vi)]2nFOVf+O+dZ/p%0=Mi0G^(NZ&E-*U\,k.a<qjo11$EuXSOqKVi&od=B6;66>D\k23RMqmhXU0JU*mf*sBXCnJ;k/RmNfdUSZ[L.%G!4oeT=/"RZh_D8/ub=f,P-s1>D%1G%b+R,iVX%<n<qV`0aE#nT=IWSF![37k2$0Ys9OIG[;C7WE+A<X9ud`f0obI*kb7k:$^nLAOo/1b$O`,<;C'd^T"hrVg<K36<))Srj:)WuXemY$9(pjUr.pb]!M]nZ`1e^B!-67K*~>endstream
endobj
29 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 285
>>
stream
GapWf9hWDY&A@g>]]89#G1&9+-UC]C]ne]:-+:/6V+n74K<W,Il/FHP%.=ja:s.-rG8'K0E;)[A%<hg&WePl@ZR\cSr@<k.`bJ.[RmrMt<d[67o%rT=JMNM@A>$:]KuNsq8ESGR\Q$C>;Bco_9(Z/hHQ0Yj1%C-mEQUBS]KG+0i/)_IKq*J(Ekj>HJhBeI"qDSmNl'_eiN`qRFK9Y)i/VccfQeS$a@V&3X]m'cT\ig.M/coC)(CU+Jip.0bfPA1Kb;d'/DUi_PI`*_(HuA+quMG%;mQ~>endstream
endobj
30 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 1320
>>
stream
Gau0CD0+]+%0"PUd.$.e]@a=lN8^4#kH6JiBdQOnVI)q[/@GOcCp.cj%V.?mpM(MgMZ\t-84/.)b=[(+Xn2l'D>GsX*@(V,7aV""!HV[+"kIh-0Y3"27ZB&1[b9oUN]1n^%/RZO_ZntJjQO#3H>^LP"/k1g3s-:h5tCs+S2EhbgG:WXaFjr(0ql@c_>4`uL<^>oMBiiaV@$6MZB3O]!MdspHE0@06(_XP=D&]]CXW`Fl<4A.#&>LF/2PTF%$I(]4sQ/#%Rm2OY/pBkk6Uf+;4!@MeBgO8?k4^LkmTA>.p*&p@d:aI,)["DM;8eYLb`&=d>3P@]^1qCnB@/@lhH4kDF8ieh(f#T1N**0m5r9CTJ478Fo/t\XHi;n3\"LRfXaHGJ9%!HnO3oZ*OZ4@!'s[2=GM5>rmUB$FAO0U*7=nMU2=]c#E5=po>`]?4=mmC:rNiOZq7=>k+'I!&ld2"/U4(k\qVkbg[Y$OFo6eC!iW8OrmGb3Y\5uuYMJrb:gGMM.G2B6n+o2f-S]Re5"L>;DiN_n9Qo!LAEud0^jdep&HtNOG_-g5>7nn9XC*MH!mMfjEAVCopb^kIGKmDKm(\S/qouhhQ\r5KpIDZ+NP?r:[OSaDP./SXHX]GYQUWgM.5*(s-C6D<dQg5FKhio/><7OLCO?.Y4+lk3PtN5t._X($Es3nE>6hQH`T3_s6rh<m%f%R@LWao--&`lfLj>TLn-#q`$[Z'q0dM-\05Pi(=%f#!8jI!8c*bLap%0?62J;p,'W&V5OHHM5Ps#_!#g&lOa8/b'@5L?8PPd[KCX13qA5:'Jiq08($7&NADLRMP8ffKH:>u=lYMFrDTMcgH\qe_]6fTdm@M.Cf'AULe*Sa%b/CkCp,;LhkRe$Vd3FI-P"100;mcun2A.$=JGXaQ"ko6Y-X,Zu0p$gLlD4AI,5$f*8:)'j)or`47d!@Pk`u!coeN(aI1jg%*qJuIMa]d5&QnE`&Z?Oo2J##_QhQHs:IEVQ94Z[b7N$D'F>:bN"1R%oG'*5n%bIe#ip\i@Ulgo)I43>CC9Bn>I[nA(9lNdI7`GZ*P.Ke`)RmVf(3nA_2h_RWc4"=<m)<OqrNG):V;edO]T"^I6b>6Te0#ll7f8!!5%V\+"-7Id''DPI5Y$hha=#*B\28t7=2'6_+rqi=OEiWjGF_^-mNZ2P;`n$>f8?a!R#4(A"6j^im^P"p:MO<h?60q,NMZE):eZkAUaX,33C;2:J1O,f>3Y0r_-+*!FQiCjdRHP_S&u<mu*7Cp0(-@efNr*YD.7]pKPGfIF?CM4kniWL4(NXdBeZMh:j'V9>gdM~>endstream
endobj
31 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 729
>>
stream
Gb!#[?&tF>'Rf.GgmBusP5tkn"C&=V-JcCPRVY@fjF\ul8f&=*%(_2_hg=mg&XtWF/q*r`;/t`gq_?,A"W]8-IeFG*3u>8N$If&7"?KM;i'+qX=8AuFm5Ia,2/U7.0HAaq[7qUliGI,N/U_$E+o_86W&Lq[<85nmedr<gb`ift<Bl5hNG@E-(f1kKK=ekgLc_@WqH'M%)n;H.!7bjrgAM"@o`lY=<(Ah<*u$L2[gud.Jc"a]pq2/#>(@]AH2cVmR+'/5cY$cWN6^SZm2B<AFVDoVa0EcC)=]Ob'/,hs-f*&%8Iq=C&!ZNqC4_Lg&i*gG"-Q;1rlHJ0i>2'oR@<UKG2]EY25@TMNtM,kW]H))s4"Ie_.4]eUd&^HgWk)#dV!f:/#V/l)[4c+M4618/9K%6<k"Fh@FJd9@9U(i\aM[T@E`%#Ot=_3cVhm'3@-Okk%2I2^CWCA,id9`e]di@M(qN(M<'OrH@'8QIK9b/V\tm/%o#ncZ$!_=W:M2B=9*F?7B)Ig`dCA?;%Tu(o#X!@[@#F@\7!7fSt$ko-jbek"t@2AO(s.h?6"0l;en!e0ltJMQ;JdfA!iu2qQfrWL$Gb*r'/O\hNlnce?BUi715,[l2IZDlp[("\X&\<4I%1#5u],67/OSSCY;E'FkS=8Hcl;tX.&Z5?=0fen-+tumT/68eMH0@#Ud;$k%3n92]:UZo6L8F7X,l3p^7%b52lag+-(qO1&~>endstream
endobj
32 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 1335
>>
stream
Gau0CgN):C&:N^l3dt5:#suMa*E">ij4%+Hm5eB"0:![aWPN)?OYS_u9i:h8H?A+Hgq4s;`<auY5,Y<jkMUqZ0/re#,Co)'e8JW3)_`7agU*G(quV?V/,rZ*_Qr"%gV$)&KhgC1h-R]`cd?UaFm+_kgT-iDaJZWY6]NAud/^YHL$4s*ep#@O\?mp*CG\*1a&*HW'O'&eM)F/01:"<6"jd#ZEr$Y;Pm1R&$H"5.IP_oAU=V#fNHLa1@,.>>j0fANN4*LUmAC$:%G*+T&2L+kJg6]S.7sb9@hbW@i>SWlBaTnU63]+gS2n'MM:#TKL7VCq?Nmmr-'j)d`l&!kN!YA(+TY(MVo%%*9>8t%No5aY!>f^-(fFfjW;K/BcR=6'Ss;do=l6o1YNoHe@!hkLc\gP1Q\sU74.U7&[TD0g!O](U2nLZR997Ih%j]$ba"6^L^74LjQ>ecYpa($CjnhEF&H$@r5@gPuX,K9@9op:U5Sd<_)IWYV)+SWhOnnk5C2(C;/T\Z`rOj9C;*A<UmBlnZBHE'hCHY<eXdS>66DGMV+/,de3uKCQjsuL<'pM@]\Q/UFs8Bi=m)>i?I*J;e3+cm'M]s97AL,Tj6fALjrCm8uF[$#$*mejJ`[p%Wnd?kq?h3Lf(bAVN=^r3soVtt_r0YD,R2:Ju39"GOT1:5[&<.'<^sl'QeFP]6cTrX@)N@Q+X[2s5Du9?I/h;+:V.Ft/cn8EMomKf^@!Z6<!*1[V@QW6d*ttEdhkC!/_C!Is)n@NVSuZA5^Gra$):trF:1l%RVc(=6q6u#\T.0Gu76%g(po8*CM(!+Ua13Tji("E)_o*7\OS1DpAi4'[+.6.A\=Te6\+V3-&H@??B:Xd^#T`EtFk/%9_,?cQ)Xeh301LU8Z%UDnU28F#ku<`l&/_6IdtF1LCf&'XH#R6_S\E$o["(@]bfr/Zij"pp,teU5<R2^Mi/3L\$jQ<4KUt?D=q/-o`[Mp3UIf;:VJ)tOZ=\?,j;Q>bGB6>j%?PQZ!XqWE4JV80-p!C[s+W.)glJE<\=V5SmqV\4qSP3o4i9aT6R;,RG@:-E-^S]gPK(Y*J/e)BpO63n3mP9YIP[=u/4o+5SYPk)*^%pc2F/SZO!A[,XWKgXbRp+L4'2C3GEc(WX+(hOett5-:eWT30_doR1#;YN.BiTg0m7jI@^gK/II0)?IJUP@]MVugQ0nd_;)Hn2cbX>T\4jliG/p5`SXNa#qUQ(>AJd^-<u:4k:RGk5D0<OgJ9+`#eA2@]*>Qm!o4ngBP:Xl;EI$@F'$(Yd\C!BZi$:(CQ8Hlq;4]1OrHDLC)](oD'T.1CX0$,fb($1\poO~>endstream
endobj
33 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 630
>>
stream
GatU0d;GF-'Rf-p:A[F/@5%fkh6KY@QDmb>XXi*=02?IQ<j4$?,up4[T:N>dP"&ZR#fO9>n%Io:k*X$7%paZ4'ei>?N[UieF:-_LIAF@IP$3n^IE@Fq&g`"$""tcRM%X*lVb*Ds?BF(VIseXpLkS9\aLdE.35fE@#ro28qf,/9>m?bmBK1__et)61-./he)T>5t:hk*_UB%P28uSYQco9V$JFin$H:WO&@3S.@:s6/_!,TL6$8\j!lgaK/-I;%I2(cQT.^S;`f#r@]en(`+O_4261Af9PbBeEiAfbLMK90gn4/&WsRi`oXrl3Z2eMTZL^Q7mE#4hS.9M3mEhcRi&k6kqVkrkutdTJP0Nb[g_:.`r1FM94!oY1#haU5W&V>*qKrj'IA$K[4XWThF#na+0"3<Jd(nQ*o2=IgSi/a2H"s!\[)aejZkNLk<tdJ#D"_>Q$E=(V4.s&$9"6Z%!48(tb&CG3@UE>WU<A)cMTTsmbC0)+AH^C3E&+Rs.Ki[;`[`l1"te[GbmN(r,j^UEELIA,H5:A+-^@E'E,CJZEZ?b(&'-`ekpSd@O*,;;l/;6l^MCYUFH<ErqJRg4(qcNTq'i4c8FR'lj7I<Noi@OtZGT,fI#efVZo/sQ~>endstream
endobj
34 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 1425
>>
stream
Gau0CD,91]&H88.0pb\p"A@<h*%+`:!QPq$*%BgGC7D6W,_NF[A,Fhm\-hc`46sAgn<S\W6#dUbmQCBj4HCj0g@XD#qfi-\kemBi%bq-c)S![)nB^%FDgZ9g1=ERtgTu2G\t(J64h!rIkVfbPX6XkXfj<G$=ho?lq#_?gBMZ@*`a;OL$`lIEGs?r4#K.1D54"=F$B:.'5XkfEEH[8f%eqjLh]!CKoQgoQ[o]6(jl8LfOEXBer++TYA\:b-r4uA:coTK\MAjo26kMm%KF*4q"(G6G\eRO`&7L<T@l).Sp2*52#r3cG(oB"5+q6k,$Sa;1YscrJn,_L&ic$(DK\oP*A%#8eKSHHo'iTFti"nD+F?EQDG"qk]$A(USY-LHS7#Gr0IW^&&6Pp&K/D/-f\NMnOAE\>%TX,'"dQU7R3n782>41P/j5m,NdNS^IE>Vk[(G=%:$orA+0Ba6k@LT]EYKL#;]Wo2#k,cEfC)DZpI#b/tP06us-\F]O4Du"F%\/>s3f8W[^e\ol5pE!3A$oHNTIoAlW\/09FoUP*4'm-=!++!-$9#)S5;u[LDB#\?[_&+5Ro_(9"FHa9GJ"FWBGkd$$OYk^I@b\S8oEfsnq'LR]U,SPgT,Y(XQ`G!X.oF]1Zg&K\i`1GO5\^,g$-m+co_TOp_K$/6jl7LWF8#OMFGeOWkis0U5srM1<K*a56);7dIN4Q:]1qd3]TD#"N-O+)%Lk>W:/j\O$em?/`Z&a_3W9qiKdib&b,C/iS(>L@2B]kH!T#J=a;Gi#C&<HB'HiO!1AG)ZO)A,2D52*NYLQW(Ld^r9Mgjq!Ekl=bCY-3+Oedk4N=Ymmqj4KJZp]cQOK-)N'uP;A,81?Go;n'o3R!j.8X$=0$K&tKP45(>lg*cm35a_gp5c99B"_#P6uNhDcHN25&^_(7HYUU'&RbV&-X1(TM]8d8UK=EA*SOQGN=`eo^hQX5c%"bp[BD`><tE:Q5hJX'cpRbfX6SQ_cF)tK[nm;iX.[U>BcqZ;PUoh1dB/$)6OSrBYs^(Mc1%(9T,MNO\l[a$/)Nqil$@W_uT$Lb"*lOFI)M1N<SblKN^6]1bpJ*nmLi!R\"stGtSG/'NJ+3Fu.pQ&,k*YYjZUKs2[dH!kCsL^1uY>5hdD$1j;OP)Ugl-`;T,)pGmV9T>.k@1995[NKnMH;(ntq)=C=rQ%Gel9&fstc![C).s7]8!:`)lpPKE!6IHalWpFS'(HT?ob2h?:#Nij"5L(X!27N'Cm^3r,SfYMR;r@UMQ>%/$.?^KsXS6F^7AeP;>G,i:TR#SAoi[+HK2Ln_<3,$m:!X53`JGl-jE923A4hP5!n8E_4BT9P6_D`ZlW>aEOM3N^*QO(k->?bP11t>kU\7Cteml-[)42,-H2YR7_1R,ISto,-pG0:Nb<"0,QAYr(rWA1fG7X~>endstream
endobj
35 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 1337
>>
stream
Gau0C=``:f&:WeDFS([Q'F9@H"qn@fPLuj6AaS.>AmD;9[:$YY1,Au0'`WSc#=<qAJRbi#8^l;C-O*Q7D+)-+#N/Mk.b=kA2<\1NeFblp^;EsSn>fqB-Q=qVdAjS_\8:D!X3nR&3dRkd>KuGY$T?W6Y6Fk+V?OZ`W*VCJYdoWg_1cncr7%2PHBZG.dV+.rq"2:>8"<-s,LqrF'>'h<7Q&\sEd\Fai=-0:UIseg>H8T,nQd7LL,MP']fUl''0u#7oIY+(pOn,Yd#"o7$=Nt$n/TdXZD#]1UrV)IhKsf?Oe-]8VCF3fn.AUSF.]:lJ^4NoXp"Z.Z2(&u]UbaQk\q<bR:rk@<aZ+!0OtWb8M>c^4PTkO7bBBfcX=JM#17V)$p@ZAGk`#H$u&?W;;EN7O3gI8mM@a_J+hh$)p]&ZAAmRfhQJ_\d'OF!g>cUB[`;)0NZqN]j8D"hMqJ^Ro0FN^&TP>gG=UC5@Q/I2/e%j;nAKC$bVWf8Lc4fi-[tpD/pZd+$:.!QfC]j3%J4hd%t'RWbc$V!/pC$[[D<<B!>98`Ad?rmW<>4h(Pdrh]-H]'*F50^7ZrKD.j&srAUR_/5=BgQhMjA;8Y603`G87C/91_Dd.&Gci7B)7!@r%"P6;cZ7X(ZJn1fCAJJp6WVukqL?unMhojt+Y`U,W&MEabLZY"3%e*0,.FoO"bL_iBAB!,iX2]+MZ&`cXbG)*UQT#1@g/EHZ8-_.4D21Rn.kG\7Z6_7ZE.c&uplB\3._UgFj&aZQ@>D,kUgT5;u#qHbSUbCrfI_!^.kEAUk]#1f2:DSmLeP-^N]Bj:iDRrpD4g;U3+0mhNq;msDQk)Y*^:=lU/c*us-jW*H2c9as#&#))1qa9:Pg+b/.VhPE9+WtHp``%oRW`S-p15,G.AJ:t2LR`Y"NZe3'm]Cb^PE>M3faj<([&#m"(:X[OsJa@N06Gm'\G;bOUoKe6EL]ha#Nu-4=fG<QQu0o+H-bT@Pn!mbGFr7WKp0'*L&=L]3YNRQS$mBP"\Z%DUL[S`s:dA3[S\G*/mfpkN7Wj?Up(\C`Hk=QUhDW8cfZXrB?A<^SJ3ZQiM'o7"HiBPk:ZX5Ijnu73aoh7%Q`V?\QIU17lh\7!m)G1`^9O3\lM3qk`.E#$]mojI!keN[QKIVQf_cGoYAk%aFSAB7MslF8,QBF>EZFrpMQjDjZ.tD-FBbU>W7YH!5AbE+WYs-kH5r6]27k%dqXM)]Mp_@3ChN1E.mk<TSdEPISHPjK(!n*.o\,p_WE'8ls$PVFF'2RR^p%hSrW8F1WiUErHiI!mPb\=q.1,Y<eSTYuis1cQ46`Y'Pd`H:-Y@$q=4`~>endstream
endobj
36 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 1211
>>
stream
Gb"/(;01/N&:WeDXGl192'GO?Kdo,^C\>LUWu8q_*k[R\>e/Xdab@/Yrc^"%["Kp;@h*_p(qq8^h!OOsI_3.s_,=W%ruZsqa"B2ED6P%"7prgM(:YIEX]rE9g%.;[NnNh]?$/rDCYITf=$<IMbtb`Xm<;tR5JO7e#B3g%]5?l<dt1SYK,OuiPPn[U"1+aDK6F:)0uEJ0g6q5/M.GL7fle@cSK!/tAWeoHpYdg<0ET_3PVi(SMus(S.5YECh",m/@..`WO?N<Q7pd#c&oe2/In@>8b2-r.\BhB=]C#Fqh3MKj8<S?G+nk1m7##A[)m+G6o]k=\T<$o!Bk:TLFoXuoA?_kfa2Dou'!J*nKN/OdL6:EB#goPa#bbn`-0mG&`e4I@=dut+HL4"DPY.%hOYT@;KXtJ'9F4@s2nMU8O]:fWgi=D@r^I&WTEZAmE3KR19t":3$-KTb[Rpg(OuP^@k&sjeGV*DY\bW'V8f<9&E53-lO"nQsjZ`O0-Tkjq.`R(>&UX1(;T7KQ_DH=VdkiqC*TKR\A+ZHtft8s^o/Af)TDnfk/+i"hl)u1#MFlLD3r*\@U+@W)1F1A@0k_9!c[i*h\6@l!(c:lRj`]otK%I\=/q@PCp`iLl(g]Kn17G^/72qD%4,IYlb`c)d+WHu?1@.=g>6em''8c[daE>$85Mm4/b-9her.*]]lmtZqMUaUg.]e9a11Kr?cP>rZTZUp@cgP).57L'$rS])[1$!m*_^f]a8^[/uHfA36Et\4F.0Z?MG]C#X-RsaX;auP+N(eGW937Z8'[nhDDO%TOYXDoF+fV3P33]Ji&[)UQA]XZ17a=BEK'6r2Q-MGc[e89i(@es>]4cEU2/dK"WLuP6Xc_lX<>+3XDk[SFAJ@p?Bh!HUPYr&]<qsUlEa67%Dh][35uHZ5JQ%+#E_X8IFc^BF+"=aLP-8\'meT+ALr[m2c'TV;j[&"BIWnr?$T@)*\J&>1WPj05E*h2_*O?$IKo1Ys<F!c*Obh)?b%k"7k_e&ILVR](+6*m#YA(ice3=6sWJ'c&9T[jVE1k@jFjX'p=7i/'m[l9?HS99+$Jj7[>ISdADlhdNd;oe`GP(V#&$04[/8rY`NdB/o6YN("niaN\^3B>Q)XgNiJ.09]TCfs0@\V_3@`=ukJP`a3S#Y,=$!\)LkO]`\Te95,<DeS-KG0"U':IZ2'OeokjeEP"T_8bJc]hr~>endstream
endobj
37 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 1621
>>
stream
GauHKgMYb8&:O:S9`3tI0[9L6"e%?0S>9/+YuIA]*!@d3/5[V%oPuD$q>/Ah.>`(6;b[:^Ybh$Zbac"\fqAQr4dQ(/blD(0iU"tobD,Nr=K^W"GR/rldI!(Hm.>/4O6jr>^^CQ$nfuR-Z!mNQi-JcjU$4s'(m1SB5g580VU]]CcH(\K[W\AD%cQJ+@LCsPN3SV75DSb1`\3S6:\=#FmC<"^o"B7"+)NqJ36NSC(-MMB#gQb/Z[C"^eDMtA3*iCQ.Jn,\Ie3_[h:aG$];=>0g?k!n!b2(f0NA*F?oJ;fQDRd$*K$\@$*RJA'?jm9YY5oJcOKs=q1`TFIeU[hen2he*gegp/_Fs$[)q3:<!W,(7,]`DY[B6i+q[HrGaapp,n`[I_'Z6O\3A"JP`R$q8Fk4<1G8+!T-(l#$D3j6+C1]g!L!%Z!\\(N6-G'LJRXMI<*UeEPl:BjaEqt*J#(2L5h?]FJ"[jH/4kC:PiPNeTH_XH3A67WC=N^Y-`8XBJ/M@A/el8jQOp"=2Pjk`c6o-=R(c-oECh<'A[7[D;!+>g4ZT@FWB_eM*JQV_DRdcf1ngpG@9EoVB(/`_E!Oaor?nN5;'creH,c-a+,\8L76-nd$XG/7Dd'sXi(`CV_-_:sd5b$YWpZ=mY@G@hPu:!c:a?9FMAr!Rka?=$F*?C,LAuV$N<QL4FIC?5QUsC,6l/:HD;"3)_RAq$lTQ[RLB\#3nnWqXgBE.Io7CeYVqGR8@;?AWZJK(ZJPGe@%mSZLT"Rp8f.ms:n!:*&m'UZ?mZ1I.pG1R0*:0NPA(]JXF(88^F#HEIl?4u*[8J4P3snr-S9u$@EWNLI(:<d<!F;Ec(i6X1-rOB;]34`M2QBr,$qK5h_oEQEcT1[+,`a,/EL#O48>JS\Q--:A;i>;S0;ja!\;s2@0/#(1B<"h/ftE,$]fYmY%.Y.7kU)'7O-*7Kgul4FHp@CQr:T)WYD2Ku^-Wu6pDXQ`\:P=s[DY6Z[\p7OWHh)_,g_NY+CVfR(>fe:+D*8gOdl$S*FB7GW`^XsHWVRVAZB*[S6JP'&S\@=Z@I/f7t[j^+2Z%B8?r;%_h&cGe-TL!kgL)dRc-Ht[CbfP:#k.?<uaZaais>A6dZo6km[qJGYJr-PlrLG+0*>3;.>D]H$s`3ge_"4UA@J`12;WO^)U8P<At$)m^6uCX\desPI'Qg\YL/db%d*eGYD6bW.B0.-69/4[<Ohlo\jP4TKrmpU,hm(hQSK[6k>)Vj)**435q#7fL[Hj:Z)T?7*ndhK:3_LHb;1m<JaeiOu2s8kR`err$Ka1[Rt.mV=DPr=Qa;f<`*W?D=72Pc<J6tAoHN'oJk0M@.u?b9MJqq+7)Jk%8K;[e+I@k0U`=,$^>beX=n5A3qZODdi2,L<K^4sL8pp*'t_(Wc#\V;NErB@UHhrg)GE>El)W`G\Xn()"-4qh6(CIucMJecfDf5<\/<*k\@h\-BDlT>F%]BT)8FYhilDBM2+k[%c`8upL'MplYp.q/H"rba8;k^#^teapg"P*4dU>\+,qY6_DFE?AHaVXbB]NibBXnnX\Y:BJbDh=tM7/5+)j.Z56S6oa?jB-)"l\fUE7PT%^oScTE,6;*cCa_28,iSS4]Z\~>endstream
endobj
38 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 1634
>>
stream
Gatm;gN)%,&:O:SD#G""j3RpR&Xg,jini*Y'5O*Sc#d>*Q6Ck\9gVmE:B/!8?:7\W9M)3D&d;e>loE!:YU&/d7EY(%$Li3?QWF:qKuk>Hhsk.G&O&l`*`#=D),_i@BhVnSN#`Od1&Bk96UNp.")tooojS8'm+FVEk9l'W3aj?aL7gfT&GGuGN.,)j@`@qo1RK_$Ec*+N_J<@TcS'e$W7qbP]J`#Y`'o[h2Xd-Br.Yt1V=L:TFi3fEMV5[g1JB@l_G8l-GZ^\AR,udKA*g&s,N:s+Z"5c'SE67Y%N1`D`IQd@m")4kWkfYUP6EKs.)??T8B,[PZ(eV2oLJO6n-tR*@7G2iRQkLMkN^:rD!>+`DSNl>S"-`DV"cB+9?u_i9e96_qIT]aKb>hE&nhU@=s=L():C#.KpZPUi(bq"b%2dH*,`^s[$o_]JdI\oboZ1A?&(h[o.+p8B)[/+lbhU,B.t>hBq7Vb\<Is?ZV7ka>YWQ<8@fq:/X]p5n,LJ>pKV`4\\"],)5.clMW)E7bIK'u@n8*(#Gnct_.Qro$[C\,8h:UH_bq>]?=rPi(&Z$u]Ja+CY28k#Qm*%X$<WT[WjP+M'[pp9KNnhK[(tN6i(kHREQRf!V4N5W?5Q4$c=[?fA^)!lj+g#>SaWg-T'Nm3)"5\@J-9Y?a":HB//d8Ra_nK(C;n4:H*LMYUR(q@964Gh'M@XN\m.MGkFsXjcg?65\E+TlG[KDnY[YUQndiWQrGgnO@I3A\l[,bMroHJujV!m@&s_SL:9,8Y$tr>7Dg#4=aY/Yp8na$@@5n&N,>-X^4p<SLSLAH*G`;r,NgYBWR.b92W4U<oM8qIBA(\HG?6kO8Oi]RF#!)1.OhIm.L60MXV*3mQX/l?[E3%8*Y.mud!%r\Foub:/6<rXdXh1#sdY.r<fsGPCXHQm01P35gR9GUC(T`mKgjp*_GIUI6FCH>K2D-quh;<68PreUl=,*jBQ^GBn$!bs`n(d"cSW9R[6Q0^SCYZL6L>uU_E<e2aQHR#*jd!;:I]KZ,X2CR`';.>@A20a27",;;Pbe"Ld<PSDq`6iQWRI*4bFD56DB99%nr1KNq*itpBN7_rkL6bi7>8)MjH'0DWi`IX1q4lWT!#;Y9DD@^\rfcWEd@c&U#^X2IN!G\9>6I;g&p*m'uPIR<Zj&]dP,9n'%,Xj5&&<eT9r`!^]'B9-A,:Jk6P)WI':I49tb!hRu<!&%=R"]CrZIp2BmiL./<PPjbUh!qUg:+C>[@m/9:t0)4j*r=3%8>dJdOXo9g)%):s"sB<sbRf@HQOcf"cV\gta#OJuaBpL9aYBuBtN9)R&q%]TkG`E+q/A1K!]NfMiP93E08h7]ud5S["4)]%$%^Z-<0^oPWDeIm5JR2#GmnS1<UmI@>9L3G]'[)-1j.&`iBFIj6aJ/#-q^?-KT5AD!BrN7mu+K>sd*.,frj'NtCB845C2m`DDeYRl&4mB0F=e%HKCth_;]RTqF`!KhZDF)a[NNkhWfB&7OST&rUFutF!P8uosL$_X_(<lKOSLHX*:LSV(S\j?3$@C.f'Fr_n?;HV&Y,=gOL)i0N;3+/qOpHCS_Nof2A)R5)Z$ibI7.Yk8.5\foB1.0W>$=#$*]3J\apUuD"4e'?F8~>endstream
endobj
xref
0 39
0000000000 65535 f
0000000061 00000 n
0000000143 00000 n
0000000250 00000 n
0000000362 00000 n
0000000567 00000 n
0000000672 00000 n
0000000787 00000 n
0000000992 00000 n
0000001197 00000 n
0000001307 00000 n
0000001513 00000 n
0000001719 00000 n
0000001925 00000 n
0000002131 00000 n
0000002337 00000 n
0000002543 00000 n
0000002749 00000 n
0000002955 00000 n
0000003161 00000 n
0000003245 00000 n
0000003451 00000 n
0000003657 00000 n
0000003727 00000 n
0000004008 00000 n
0000004161 00000 n
0000005204 00000 n
0000006525 00000 n
0000007362 00000 n
0000008854 00000 n
0000009230 00000 n
0000010642 00000 n
0000011462 00000 n
0000012889 00000 n
0000013610 00000 n
0000015127 00000 n
0000016556 00000 n
0000017859 00000 n
0000019572 00000 n
trailer
<<
/ID
[<ab4fa5324f02268490d94a51d4d35c59><ab4fa5324f02268490d94a51d4d35c59>]
% ReportLab generated PDF document -- digest (opensource)
/Info 23 0 R
/Root 22 0 R
/Size 39
>>
startxref
21298
%%EOF

View File

@@ -0,0 +1,145 @@
# Les 8 — Slide-overzicht
## Van In-Memory naar Supabase (10 slides)
---
## Slide-indeling
### Slide 1: Titelslide
**Titel:** Les 8 — Van In-Memory naar Supabase
**Ondertitel:** Koppelen van Supabase aan Next.js
---
### Slide 2: Terugblik vorige les
**Titel:** Terugblik — Waar waren we?
**Bullets:**
- Stemmen werkt lokaal (in-memory data)
- QuickPoll app: / en /poll/[id] pagina's
- VoteForm component → stemmen onmiddellijk
- Nu: alles naar een echte database!
---
### Slide 3: Planning vandaag
**Titel:** Planning — Les 8 (3 uur)
**Timeline:**
- 09:00-09:15 | Welkom & Intro (15 min)
- 09:15-09:45 | **KLASSIKAAL: Setup** — PDF Deel 1 (30 min)
- 09:45-10:00 | **KLASSIKAAL: Queries** — PDF Deel 2 (15 min)
- 10:00-10:15 | **KLASSIKAAL: Componenten** — PDF Deel 3 (15 min)
- 10:15-10:30 | Pauze (15 min)
- 10:30-10:45 | Uitleg INSERT + start Deel 4 (15 min)
- 10:45-11:30 | **ZELFSTANDIG: /create pagina** — PDF Deel 4 (45 min)
- 11:30-12:00 | Vragen + Huiswerk (30 min)
**Aanpak:** Deel 1-3 klassikaal met de PDF. Deel 4 zelfstandig.
---
### Slide 4: Van Array naar Database
**Titel:** Van In-Memory Array naar Supabase
**Links:** In-memory (OUD)
```javascript
const polls = [
{ question: "Favoriete taal?",
options: ["JS", "Python"],
votes: [10, 5]
}
];
```
**Rechts:** Supabase Database (NIEUW)
```
polls tabel
├─ id (1)
├─ question ("Favoriete taal?")
└─ options[] (relatie)
options tabel
├─ poll_id (1)
├─ text ("JS")
└─ votes (10)
```
---
### Slide 5: Supabase Queries
**Titel:** Supabase Queries
**Ondertitel:** Vier operaties die je nodig hebt
**Queries:**
- SELECT alles: `.from("polls").select("*, options(*)")`
- SELECT een: `.eq("id", 5).single()`
- INSERT: `.insert({ question }).select().single()`
- RPC: `.rpc("vote_option", { option_id })`
**Tekst:** Dit zijn de TODO blokken in de PDF!
---
### Slide 6: Server vs Client: Wie doet wat?
**Titel:** Server vs Client
**Ondertitel:** Wie doet wat?
**Twee kolommen:**
**SERVER Component:**
- async function
- await getPolls()
- Data fetching
- Direct naar DB
**CLIENT Component:**
- 'use client'
- useState, onClick
- Interactief: klik, type
- useEffect
**Zeg:** "Server haalt data, Client maakt het interactief."
---
### Slide 7: Pauze
**Titel:** Pauze
**Tekst:** Deel 1-3 klaar! Na de pauze: zelfstandig /create pagina bouwen.
---
### Slide 8: Zelf Doen — /create pagina
**Titel:** Zelf Doen — PDF Deel 4
**Ondertitel:** Het formulier staat in de PDF — jij schrijft de INSERT!
**Stappen:**
1. RLS INSERT policy toevoegen (Stap 4.1)
2. Copy-paste app/create/page.tsx (Stap 4.3)
3. handleSubmit TODO invullen (de INSERT logica)
4. Testen: poll aanmaken → verschijnt op homepage
---
### Slide 9: Huiswerk
**Titel:** Huiswerk
**Verplicht:**
- /create pagina afmaken
- Validatie toevoegen (vraag niet leeg, min 2 opties)
**Extra:**
- Delete functionaliteit
- SQL queries in Supabase testen
- Styling verbeteren
---
### Slide 10: Afsluiting
**Titel:** Tot volgende week!
**Tekst:**
- Volgende les: Supabase Auth
- Inloggen, registreren & bepalen wie wat mag